001    /*
002     * PCDCodec
003     * 
004     * Copyright (c) 2000, 2001, 2002, 2003 Marco Schmidt.
005     * All rights reserved.
006     */
007    
008    package net.sourceforge.jiu.codecs;
009    
010    import java.io.IOException;
011    import java.io.RandomAccessFile;
012    import net.sourceforge.jiu.codecs.ImageCodec;
013    import net.sourceforge.jiu.codecs.InvalidFileStructureException;
014    import net.sourceforge.jiu.codecs.UnsupportedTypeException;
015    import net.sourceforge.jiu.codecs.WrongFileFormatException;
016    import net.sourceforge.jiu.color.YCbCrIndex;
017    import net.sourceforge.jiu.color.conversion.PCDYCbCrConversion;
018    import net.sourceforge.jiu.data.Gray8Image;
019    import net.sourceforge.jiu.data.IntegerImage;
020    import net.sourceforge.jiu.data.MemoryGray8Image;
021    import net.sourceforge.jiu.data.MemoryRGB24Image;
022    import net.sourceforge.jiu.data.RGB24Image;
023    import net.sourceforge.jiu.ops.MissingParameterException;
024    import net.sourceforge.jiu.ops.OperationFailedException;
025    import net.sourceforge.jiu.util.ArrayRotation;
026    import net.sourceforge.jiu.util.ArrayScaling;
027    
028    /**
029     * A codec to read Kodak Photo-CD (image pac) image files. 
030     * Typical file extension is <code>.pcd</code>.
031     * PCD is designed to store the same image in several resolutions.
032     * Not all resolutions are always present in a file.
033     * Typically, the first five resolutions are available and the file size
034     * is between four and six megabytes.
035     * Lossless compression (Huffman encoding) is used to store the higher resolution images.
036     * All images are in 24 bit YCbCr colorspace, with a component subsampling of 4:1:1 (Y:Cb:Cr) 
037     * in both horizontal and vertical direction.
038     * <h3>Limitations</h3>
039     * Only the lowest three resolutions are supported by this codec.
040     * <h3>Sample PCD files</h3>
041     * You can download sample PCD image files from
042     * <a href="http://www.kodak.com/digitalImaging/samples/imageIntro.shtml">Kodak's
043     * website</a>.
044     *
045     * @author Marco Schmidt
046     */
047    public class PCDCodec extends ImageCodec implements YCbCrIndex
048    {
049            /**
050             * Base/16, the minimum pixel resolution, 192 x 128 pixels.
051             */
052            public static final int PCD_RESOLUTION_1 = 0;
053    
054            /**
055             * Base/4, the second pixel resolution, 384 x 256 pixels.
056             */
057            public static final int PCD_RESOLUTION_2 = 1;
058    
059            /**
060             * Base, the third pixel resolution, 768 x 512 pixels.
061             */
062            public static final int PCD_RESOLUTION_3 = 2;
063    
064            /**
065             * Base*4, the fourth pixel resolution, 1536 x 1024 pixels. <em>Unsupported</em>
066             */
067            public static final int PCD_RESOLUTION_4 = 3;
068    
069            /**
070             * Base*16, the fifth pixel resolution, 3072 x 2048 pixels. <em>Unsupported</em>
071             */
072            public static final int PCD_RESOLUTION_5 = 4;
073    
074            /**
075             * Base*64, the sixth pixel resolution, 6144 x 4096 pixels. <em>Unsupported</em>
076             */
077            public static final int PCD_RESOLUTION_6 = 5;
078    
079            /**
080             * Index for the default resolution , Base ({@link #PCD_RESOLUTION_3}).
081             */
082            public static final int PCD_RESOLUTION_DEFAULT = PCD_RESOLUTION_3;
083    
084            /**
085             * This two-dimensional int array holds all possible pixel resolutions for
086             * a PCD file. Use one of the PCD resolution constants (e.g.
087             * {@link #PCD_RESOLUTION_3} as first index.
088             * The second index must be 0 or 1 and leads to either width or
089             * height.
090             * Example: <code>PCD_RESOLUTION[PCD_RESOLUTION_3][1]</code> will evalute
091             * as 512, which can be width or height, depending on the image being
092             * in landscape or portrait mode.
093             * You may want to use these resolution values in your program
094             * to prompt the user which resolution to load from the file.
095             */
096            public static final int[][] PCD_RESOLUTIONS =
097                    {{192, 128}, {384, 256}, {768, 512},
098                     {1536, 1024}, {3072, 2048}, {6144, 4096}};
099            // offsets into the file for the three uncompressed resolutions
100            private static final long[] PCD_FILE_OFFSETS =
101                    {0x2000, 0xb800, 0x30000};
102            private static final long[] PCD_BASE_LENGTH =
103                    {0x2000, 0xb800, 0x30000};
104            // some constants to understand the orientation of an image
105            private static final int NO_ROTATION = 0;
106            private static final int ROTATE_90_LEFT = 1;
107            private static final int ROTATE_180 = 2;
108            private static final int ROTATE_90_RIGHT = 3;
109            // 2048 bytes
110            private static final int SECTOR_SIZE = 0x800;
111            // "PCD_IPI"
112            private static final byte[] MAGIC =
113                    {0x50, 0x43, 0x44, 0x5f, 0x49, 0x50, 0x49};
114            private boolean performColorConversion;
115            private boolean monochrome;
116            private int numChannels;
117            private int resolutionIndex;
118            private RandomAccessFile in;
119            private byte[][] data;
120    
121            /**
122             * This constructor chooses the default settings for PCD image loading:
123             * <ul>
124             * <li>load color image (all channels, not only luminance)</li>
125             * <li>perform color conversion from PCD's native YCbCr color space to RGB</li>
126             * <li>load the image in the default resolution 
127             *  {@link #PCD_RESOLUTION_DEFAULT}, 768 x 512 pixels (or vice versa)</li>
128             * </ul>
129             */
130            public PCDCodec()
131            {
132                    super();
133                    setColorConversion(true);
134                    setMonochrome(false);
135                    setResolutionIndex(PCD_RESOLUTION_DEFAULT);
136            }
137    
138            private byte[][] allocateMemory()
139            {
140                    int numPixels = PCD_RESOLUTIONS[resolutionIndex][0] *
141                            PCD_RESOLUTIONS[resolutionIndex][1];
142                    byte[][] result = new byte[numChannels][];
143                    for (int i = 0; i < numChannels; i++)
144                    {
145                            result[i] = new byte[numPixels];
146                    }
147                    return result;
148            }
149    
150            private void checkByteArray(
151                    byte[][] data, 
152                    int numPixels) throws IllegalArgumentException
153            {
154                    // check if array is non-null
155                    if (data == null)
156                    {
157                            throw new IllegalArgumentException("Error: Image channel array is not initialized.");
158                    }
159                    // check if array has enough entries
160                    int channels;
161                    if (monochrome)
162                    {
163                            channels = 1;
164                            if (data.length < 1)
165                            {
166                                    throw new IllegalArgumentException("Error: Image channel " +
167                                            "array must have at least one channel for monochrome " +
168                                            "images.");
169                            }
170                    }
171                    else
172                    {
173                            channels = 3;
174                            if (data.length < 3)
175                            {
176                                    throw new IllegalArgumentException("Error: Image channel " +
177                                            "array must have at least three channels for color images.");
178                            }
179                    }
180                    // check if each channel has enough entries for the samples
181                    for (int i = 0; i < channels; i++)
182                    {
183                            if (data[i].length < numPixels)
184                            {
185                                    throw new IllegalArgumentException("Error: Image channel #" + i + 
186                                            " is not large enough (" + numPixels + " entries required, " +
187                                            data[i].length + " found).");
188                            }
189                    }
190            }
191    
192            private void convertToRgb(int width, int height)
193            {
194                    byte[] red = new byte[width];
195                    byte[] green = new byte[width];
196                    byte[] blue = new byte[width];
197                    int offset = 0;
198                    for (int y = 0; y < height; y++)
199                    {
200                            PCDYCbCrConversion.convertYccToRgb(
201                                    data[INDEX_Y], 
202                                    data[INDEX_CB], 
203                                    data[INDEX_CR], 
204                                    offset,
205                                    red, 
206                                    green, 
207                                    blue, 
208                                    0, 
209                                    width);
210                            System.arraycopy(red, 0, data[0], offset, width);
211                            System.arraycopy(green, 0, data[1], offset, width);
212                            System.arraycopy(blue, 0, data[2], offset, width);
213                            offset += width;
214                    }
215            }
216    
217            private IntegerImage createImage(int width, int height)
218            {
219                    if (monochrome)
220                    {
221                            Gray8Image image = new MemoryGray8Image(width, height);
222                            int offset = 0;
223                            for (int y = 0; y < height; y++)
224                            {
225                                    for (int x = 0; x < width; x++)
226                                    {
227                                            image.putByteSample(0, x, y, data[0][offset++]);
228                                    }
229                            }
230                            return image;
231                    }
232                    else
233                    if (performColorConversion)
234                    {
235                            RGB24Image image = new MemoryRGB24Image(width, height);
236                            int offset = 0;
237                            for (int y = 0; y < height; y++)
238                            {
239                                    for (int x = 0; x < width; x++)
240                                    {
241                                            image.putByteSample(RGB24Image.INDEX_RED, x, y, data[0][offset]);
242                                            image.putByteSample(RGB24Image.INDEX_GREEN, x, y, data[1][offset]);
243                                            image.putByteSample(RGB24Image.INDEX_BLUE, x, y, data[2][offset]);
244                                            offset++;
245                                    }
246                            }
247                            return image;
248                    }
249                    else
250                    {
251                            return null;
252                    }
253            }
254    
255            public String[] getFileExtensions()
256            {
257                    return new String[] {".pcd"};
258            }
259    
260            public String getFormatName()
261            {
262                    return "Kodak Photo-CD (PCD)";
263            }
264    
265            public String[] getMimeTypes()
266            {
267                    return new String[] {"image/x-pcd"};
268            }
269    
270            public boolean isLoadingSupported()
271            {
272                    return true;
273            }
274    
275            public boolean isSavingSupported()
276            {
277                    return false;
278            }
279    
280            /**
281             * Attempts to load an image.
282             * The codec must have been given an input stream, all other
283             * parameters (do not convert color to RGB, load monochrome channel 
284             * only, load other resolution than default) can optionally be
285             * chosen by calling the corresponding methods.
286             *
287             * @return loaded image
288             * @throws IOException if there were reading errors
289             * @throws OutOfMemoryException if there was not enough free memory 
290             *  available
291             * @throws InvalidFileStructureException if the file seems to be a PCD
292             *  stream but has logical errors in it
293             * @throws WrongFileFormatException if this is not a PCD file
294             */
295            private void load() throws
296                    InvalidFileStructureException,
297                    IOException, 
298                    UnsupportedTypeException,
299                    WrongFileFormatException
300            {
301                    if (resolutionIndex != PCD_RESOLUTION_1 &&
302                        resolutionIndex != PCD_RESOLUTION_2 &&
303                        resolutionIndex != PCD_RESOLUTION_3)
304                    {
305                            throw new UnsupportedTypeException("Error reading PCD input " +
306                                    "stream. Only the three lowest resolutions are supported.");
307                    }
308                    if (in == null)
309                    {
310                            throw new IllegalArgumentException("Input file is missing " +
311                                    "(use PCDCodec.setInput(RandomAccessFile).");
312                    }
313                    if (in.length() < 16 * 1024)
314                    {
315                            throw new WrongFileFormatException("Not a PCD file.");
316                    }
317                    byte[] sector = new byte[SECTOR_SIZE];
318                    // read first sector; first 7 bytes must be 0xff
319                    in.readFully(sector);
320                    for (int i = 0; i < 7; i++)
321                    {
322                            if (sector[i] != -1)
323                            {
324                                    throw new WrongFileFormatException("Input is not a valid PCD " +
325                                            "file (wrong magic byte sequence).");
326                            }
327                    }
328                    // read second sector and check more magic bytes
329                    in.readFully(sector);
330                    for (int i = 0; i < MAGIC.length; i++)
331                    {
332                            if (sector[i] != MAGIC[i])
333                            {
334                                    throw new WrongFileFormatException("Input is not a valid PCD " +
335                                            "file (wrong magic byte sequence).");
336                            }
337                    }
338                    // get image orientation and resolution
339                    int rotationAngle = sector[0x602] & 0x03;
340                    int width = PCD_RESOLUTIONS[resolutionIndex][0];
341                    int height = PCD_RESOLUTIONS[resolutionIndex][1];
342                    int realWidth = width;
343                    int realHeight = height;
344                    if (rotationAngle == ROTATE_90_LEFT || rotationAngle == ROTATE_90_RIGHT)
345                    {
346                            realWidth = height;
347                            realHeight = width;
348                    }
349                    if (!hasBounds())
350                    {
351                            setBounds(0, 0, realWidth - 1, realHeight - 1);
352                    }
353                    // determine which uncompressed image will be loaded
354                    int uncompressedResolution = resolutionIndex;
355                    if (resolutionIndex > PCD_RESOLUTION_3)
356                    {
357                            uncompressedResolution = PCD_RESOLUTION_3;
358                    }
359                    // load uncompressed image
360                    data = allocateMemory();
361                    loadUncompressedImage(uncompressedResolution);
362                    // reverse color subsampling if necessary
363                    if (!monochrome)
364                    {
365                            ArrayScaling.scaleUp200Percent(data[INDEX_CB],
366                                    PCD_RESOLUTIONS[uncompressedResolution][0] / 2,
367                                    PCD_RESOLUTIONS[uncompressedResolution][1] / 2);
368                            ArrayScaling.scaleUp200Percent(data[INDEX_CR],
369                                    PCD_RESOLUTIONS[uncompressedResolution][0] / 2,
370                                    PCD_RESOLUTIONS[uncompressedResolution][1] / 2);
371                    }
372                    // TODO load higher resolution by decoding differences to uncompressed image
373                    // ...
374                    // convert to RGB color space if possible and desired
375                    if ((!monochrome) && performColorConversion)
376                    {
377                            convertToRgb(width, height);
378                    }
379                    // rotate the image if necessary
380                    rotateArrays(rotationAngle, width, height);
381                    // adjust width and height
382                    if (rotationAngle == ROTATE_90_LEFT || rotationAngle == ROTATE_90_RIGHT)
383                    {
384                            int temp = width;
385                            width = height;
386                            height = temp;
387                    }
388                    setImage(createImage(width, height));
389            }
390    
391            /**
392             * Loads one of the three lowest resolution images from the file.
393             * First skips as many bytes as there are between the current
394             * stream offset and the offset of the image in the PCD file
395             * (first three images are at fixed positions).
396             * Then reads the pixels from in to data.
397         * <p>
398             * Note that there are <code>width</code> times <code>height</code>
399             * samples for Y, but only one fourth that many samples for each Cb and Cr
400             * (because of the 4:1:1 subsampling of the two chroma components).
401             * <p>
402             * @param resolution one of PCD_RESOLUTION_1, PCD_RESOLUTION_2 or PCD_RESOLUTION_3
403             * @throws an IOException if there were any reading errors
404             */
405            private void loadUncompressedImage(int resolution)
406                    throws IllegalArgumentException, IOException
407            {
408                    if (resolution != PCD_RESOLUTION_1 &&
409                        resolution != PCD_RESOLUTION_2 &&
410                        resolution != PCD_RESOLUTION_3)
411                    {
412                            throw new IllegalArgumentException("Error loading " +
413                                    "PCD image, only the lowest three resolutions are " +
414                                    "uncompressed.");
415                    }
416                    in.seek(PCD_FILE_OFFSETS[resolution]);
417                    int fullWidth = PCD_RESOLUTIONS[resolution][0];
418                    int fullHeight = PCD_RESOLUTIONS[resolution][1];
419                    int halfWidth = fullWidth / 2;
420                    int halfHeight = fullHeight / 2;
421                    int offset1 = 0;
422                    int offset2 = 0;
423                    for (int y = 0; y < halfHeight; y++)
424                    {
425                            // read two luminance rows
426                            in.readFully(data[INDEX_Y], offset1, fullWidth * 2);
427                            offset1 += (fullWidth * 2);
428                            if (monochrome)
429                            {
430                                    if (in.skipBytes(fullWidth) != fullWidth)
431                                    {
432                                            throw new IOException("Could not skip " + fullWidth +
433                                                    " bytes.");
434                                    }
435                            }
436                            else
437                            {
438                                    // read one row for each cb and cr
439                                    in.readFully(data[INDEX_CB], offset2, halfWidth);
440                                    in.readFully(data[INDEX_CR], offset2, halfWidth);
441                                    offset2 += halfWidth;
442                            }
443                    }
444            }
445    
446            /**
447             * Checks the parameter and loads an image.
448             */
449            public void process() throws
450                    InvalidFileStructureException,
451                    MissingParameterException,
452                    OperationFailedException,
453                    UnsupportedTypeException,
454                    WrongFileFormatException
455            {
456                    in = getRandomAccessFile();
457                    if (in == null)
458                    {
459                            throw new MissingParameterException("RandomAccessFile object needed in PCDCodec.");
460                    }
461                    if (getMode() != CodecMode.LOAD)
462                    {
463                            throw new UnsupportedTypeException("PCDCodec can only load images.");
464                    }
465                    try
466                    {
467                            load();
468                    }
469                    catch (IOException ioe)
470                    {
471                            throw new OperationFailedException("I/O error: " + ioe.toString());
472                    }
473            }
474    
475            private void rotateArrays(int rotationAngle, int width, int height)
476            {
477                    if (rotationAngle == NO_ROTATION)
478                    {
479                            return; 
480                    }
481                    int numPixels = width * height;
482                    for (int i = 0; i < numChannels; i++)
483                    {
484                            byte[] dest = new byte[numPixels];
485                            switch(rotationAngle)
486                            {
487                                    case(ROTATE_90_LEFT):
488                                    {
489                                            ArrayRotation.rotate90Left(width, height, data[i], 0, dest, 0);
490                                            break;
491                                    }
492                                    case(ROTATE_90_RIGHT):
493                                    {
494                                            ArrayRotation.rotate90Right(width, height, data[i], 0, dest, 0);
495                                            break;
496                                    }
497                                    case(ROTATE_180):
498                                    {
499                                            ArrayRotation.rotate180(width, height, data[i], 0, dest, 0);
500                                            break;
501                                    }
502                            }
503                            System.arraycopy(dest, 0, data[i], 0, numPixels);
504                    }
505            }
506    
507            private void scaleUp(int currentResolution)
508            {
509                    int width = PCD_RESOLUTIONS[currentResolution][0];
510                    int height = PCD_RESOLUTIONS[currentResolution][1];
511                    ArrayScaling.scaleUp200Percent(data[INDEX_Y], width, height);
512                    if (!monochrome)
513                    {
514                            ArrayScaling.scaleUp200Percent(data[INDEX_CB], width, height);
515                            ArrayScaling.scaleUp200Percent(data[INDEX_CR], width, height);
516                    }
517            }
518    
519            /**
520             * Specify whether color is converted from PCD's YCbCr color space to
521             * RGB color space.
522             * The default is <code>true</code>, and you should only change this
523             * if you really know what you are doing.
524             * If you simply want the luminance (gray) channel, use 
525             * {@link #setMonochrome(boolean)} with <code>true</code> as parameter.
526             * @param performColorConversion boolean that determines whether color conversion is applied
527             */
528            public void setColorConversion(boolean performColorConversion)
529            {
530                    this.performColorConversion = performColorConversion;
531            }
532    
533            public void setFile(String fileName, CodecMode codecMode) throws 
534                    IOException, 
535                    UnsupportedCodecModeException
536            {
537                    if (codecMode == CodecMode.LOAD)
538                    {
539                            setRandomAccessFile(new RandomAccessFile(fileName, "r"), CodecMode.LOAD); 
540                    }
541                    else
542                    {
543                            throw new UnsupportedCodecModeException("This PCD codec can only load images.");
544                    }
545            }
546    
547            /**
548             * Specifies whether the image is to be loaded as gray or color image.
549             * If argument is true, only the gray channel is loaded.
550             */
551            public void setMonochrome(boolean monochrome)
552            {
553                    this.monochrome = monochrome;
554                    if (monochrome)
555                    {
556                            numChannels = 1;
557                    }
558                    else
559                    {
560                            numChannels = 3;
561                    }
562            }
563    
564            public void setResolutionIndex(int resolutionIndex)
565            {
566                    this.resolutionIndex = resolutionIndex;
567            }
568    }