001    /*
002     * PNMCodec
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.DataOutput;
011    import java.io.InputStream;
012    import java.io.IOException;
013    import java.io.PushbackInputStream;
014    import java.util.NoSuchElementException;
015    import java.util.StringTokenizer;
016    import net.sourceforge.jiu.data.BilevelImage;
017    import net.sourceforge.jiu.data.GrayImage;
018    import net.sourceforge.jiu.data.Gray16Image;
019    import net.sourceforge.jiu.data.Gray8Image;
020    import net.sourceforge.jiu.data.GrayIntegerImage;
021    import net.sourceforge.jiu.data.IntegerImage;
022    import net.sourceforge.jiu.data.MemoryBilevelImage;
023    import net.sourceforge.jiu.data.MemoryGray16Image;
024    import net.sourceforge.jiu.data.MemoryGray8Image;
025    import net.sourceforge.jiu.data.MemoryRGB24Image;
026    import net.sourceforge.jiu.data.MemoryRGB48Image;
027    import net.sourceforge.jiu.data.PixelImage;
028    import net.sourceforge.jiu.data.RGB24Image;
029    import net.sourceforge.jiu.data.RGB48Image;
030    import net.sourceforge.jiu.data.RGBIndex;
031    import net.sourceforge.jiu.data.RGBIntegerImage;
032    import net.sourceforge.jiu.ops.MissingParameterException;
033    import net.sourceforge.jiu.ops.OperationFailedException;
034    import net.sourceforge.jiu.ops.WrongParameterException;
035    
036    /**
037     * A codec to read and write Portable Anymap (PNM) image files.
038     * This format includes three file types well-known in the Unix world:
039     * <ul>
040     * <li>PBM (Portable Bitmap - 1 bit per pixel bilevel image),</li>
041     * <li>PGM (Portable Graymap - grayscale image) and</li>
042     * <li>PPM (Portable Pixmap - RGB truecolor image).</li>
043     * </ul>
044     * <p>
045     *
046     * <h3>Compression</h3>
047     * The file format only allows for uncompressed storage.
048     *
049     * <h3>ASCII mode / binary mode</h3>
050     * PNM streams can be stored in binary mode or ASCII mode.
051     * ASCII mode files are text files with numbers representing the pixels.
052     * They become larger than their binary counterparts, but as they are
053     * very redundant they can be compressed well with archive programs.
054     * ASCII PGM and PPM files can have all kinds of maximum sample values,
055     * thus allowing for arbitrary precision.
056     * They are not restricted by byte limits.
057     * PBM streams always have two colors, no matter if they are ASCII or binary.
058     *
059     * <h3>Color depth for PGM / PPM</h3>
060     * <p>
061     * The header of a PGM and PPM file stores a maximum sample value
062     * (such a value is not stored for PBM, where the maximum value is always 1).
063     * When in binary mode, PGM and PPM typically have a maximum sample value of 255,
064     * which makes PGM 8 bits per pixel and PPM 24 bits per pixel large.
065     * One sample will be stored as a single byte.
066     * However, there also exist binary PGM files with a maximum sample value larger than
067     * 255 and smaller than 65536.
068     * These files use two bytes per sample, in network byte order (big endian).
069     * I have yet to see PPM files with that property, but they are of course imagineable.
070     * 16 bpp
071     * </p>
072     *
073     * <h3>DPI values</h3>
074     * PNM files cannot store the physical resolution in DPI.
075     *
076     * <h3>Number of images</h3>
077     * Only one image can be stored in a PNM file.
078     *
079     * <h3>Usage example - load an image from a PNM file</h3>
080     * <pre>
081     * PNMCodec codec = new PNMCodec();
082     * codec.setFile("test.ppm", CodecMode.LOAD);
083     * codec.process();
084     * codec.close();
085     * PixelImage image = codec.getImage();
086     * </pre>
087     *
088     * <h3>Usage example - save an image to a PNM file</h3>
089     * <pre>
090     * PNMCodec codec = new PNMCodec();
091     * BilevelImage myFax = ...; // initialize
092     * codec.setImage(myFax);
093     * codec.setFile("out.pbm", CodecMode.SAVE);
094     * codec.process();
095     * codec.close();
096     * </pre>
097     *
098     * @author Marco Schmidt
099     */
100    public class PNMCodec extends ImageCodec
101    {
102            /**
103             * Image type constant for images of unknown type.
104             */
105            public static final int IMAGE_TYPE_UNKNOWN = -1;
106    
107            /**
108             * Image type constant for bilevel images, stored in PBM files.
109             */
110            public static final int IMAGE_TYPE_BILEVEL = 0;
111    
112            /**
113             * Image type constant for grayscale images, stored in PGM files.
114             */
115            public static final int IMAGE_TYPE_GRAY = 1;
116    
117            /**
118             * Image type constant for RGB truecolor images, stored in PPM files.
119             */
120            public static final int IMAGE_TYPE_COLOR = 2;
121            private static final String[] IMAGE_TYPE_FILE_EXTENSIONS = 
122                    {".pbm", ".pgm", ".ppm"};
123            private Boolean ascii;
124            private int columns;
125            private int imageType;
126            private PushbackInputStream in;
127            private DataOutput out;
128            private int height;
129            private int maxSample;
130            private int width;
131    
132            /**
133             * Attempts to find the appropriate image type by looking at a file's name.     
134             * Ignores case when comparing.
135             * Returns {@link #IMAGE_TYPE_BILEVEL} for <code>.pbm</code>,
136             * {@link #IMAGE_TYPE_GRAY} for <code>.pgm</code> and
137             * {@link #IMAGE_TYPE_COLOR} for <code>.ppm</code>.
138             * Otherwise, {@link #IMAGE_TYPE_UNKNOWN} is returned.
139             * To get a file extension given that you have an image type, use
140             * {@link #getTypicalFileExtension}.
141             * 
142             * @param fileName the file name to be examined
143             * @return one of the <code>IMAGE_TYPE_xxx</code> constants of this class
144             */
145            public static int determineImageTypeFromFileName(String fileName)
146            {
147                    if (fileName == null || fileName.length() < 4)
148                    {
149                            return IMAGE_TYPE_UNKNOWN;
150                    }
151                    String ext = fileName.substring(fileName.length() - 3);
152                    ext = ext.toLowerCase();
153                    for (int i = 0; i < IMAGE_TYPE_FILE_EXTENSIONS.length; i++)
154                    {
155                            if (IMAGE_TYPE_FILE_EXTENSIONS[i].equals(ext))
156                            {
157                                    return i;
158                            }
159                    }
160                    return IMAGE_TYPE_UNKNOWN;
161            }
162    
163            /**
164             * Returns if ASCII mode was used for loading an image or will
165             * be used to store an image.
166             * @return true for ASCII mode, false for binary mode, null if that information is not available
167             * @see #setAscii
168             */
169            public Boolean getAscii()
170            {
171                    return ascii;
172            }
173    
174            public String getFormatName()
175            {
176                    return "Portable Anymap (PBM, PGM, PPM)";
177            }
178    
179            public String[] getMimeTypes()
180            {
181                    return new String[] {"image/x-ppm", "image/x-pgm", "image/x-pbm", "image/x-pnm", 
182                            "image/x-portable-pixmap", "image/x-portable-bitmap", "image/x-portable-graymap", 
183                            "image/x-portable-anymap"};
184            }
185    
186            /**
187             * Returns the typical file extension (including leading dot) for an
188             * image type.
189             * Returns <code>null</code> for {@link #IMAGE_TYPE_UNKNOWN}.
190             * To get the image type given that you have a file name, use
191             * {@link #determineImageTypeFromFileName}.
192             *
193             * @param imageType the image type for which the extension is required
194             * @return the file extension or null
195             */
196            public static String getTypicalFileExtension(int imageType)
197            {
198                    if (imageType >= 0 && imageType < IMAGE_TYPE_FILE_EXTENSIONS.length)
199                    {
200                            return IMAGE_TYPE_FILE_EXTENSIONS[imageType];
201                    }
202                    else
203                    {
204                            return null;
205                    }
206            }
207    
208            public boolean isLoadingSupported()
209            {
210                    return true;
211            }
212    
213            public boolean isSavingSupported()
214            {
215                    return true;
216            }
217    
218            /**
219             * Loads an image from a PNM input stream.
220             * It is assumed that a stream was given to this codec using {@link #setInputStream(InputStream)}.
221             *
222             * @return the image as an instance of a class that implements {@link IntegerImage}
223             * @throws InvalidFileStructureException if the input stream is not a valid PNM stream (or unsupported)
224             * @throws java.io.IOException if there were problems reading from the input stream
225             */
226            private void load() throws 
227                    InvalidFileStructureException,
228                    IOException,
229                    MissingParameterException,
230                    UnsupportedTypeException,
231                    WrongFileFormatException,
232                    WrongParameterException
233            {
234                    InputStream is = getInputStream();
235                    if (is != null)
236                    {
237                            if (is instanceof PushbackInputStream)
238                            {
239                                    in = (PushbackInputStream)is;
240                            }
241                            else
242                            {
243                                    in = new PushbackInputStream(is);
244                            }
245                    }
246                    else
247                    {
248                            throw new MissingParameterException("InputStream object required for loading.");
249                    }
250                    loadType();
251                    String resolutionLine = loadTextLine();
252                    setResolution(resolutionLine);
253                    setBoundsIfNecessary(width, height);
254                    if (imageType == IMAGE_TYPE_BILEVEL)
255                    {
256                            maxSample = 1;
257                    }
258                    else
259                    {
260                            // load maximum value
261                            String maxSampleLine = loadTextLine();
262                            setMaximumSample(maxSampleLine);
263                    }
264                    if (maxSample > 65535)
265                    {
266                            throw new UnsupportedTypeException("Cannot deal with samples larger than 65535.");
267                    }
268                    checkImageResolution();
269                    switch (imageType)
270                    {
271                            case(IMAGE_TYPE_BILEVEL):
272                            {
273                                    loadBilevelImage();
274                                    break;
275                            }
276                            case(IMAGE_TYPE_COLOR):
277                            {
278                                    loadColorImage();
279                                    break;
280                            }
281                            case(IMAGE_TYPE_GRAY):
282                            {
283                                    loadGrayImage();
284                                    break;
285                            }
286                            default:
287                            {
288                                    throw new UnsupportedTypeException("Cannot deal with image type.");
289                            }
290                    }
291            }
292    
293            private int loadAsciiNumber() throws
294                    InvalidFileStructureException,
295                    IOException
296            {
297                    boolean hasDigit = false;
298                    int result = -1;
299                    do
300                    {
301                            int b = in.read();
302                            if (b >= 48 && b <= 57)
303                            {
304                                    // decimal digit
305                                    if (hasDigit)
306                                    {
307                                            result = result * 10 + (b - 48);
308                                    }
309                                    else
310                                    {
311                                            hasDigit = true;
312                                            result = b - 48;
313                                    }
314                            }
315                            else
316                            if (b == 32 || b == 10 || b == 13 || b == 9)
317                            {
318                                    // whitespace
319                                    if (hasDigit)
320                                    {
321                                            if (result > maxSample)
322                                            {
323                                                    throw new InvalidFileStructureException("Read number " +
324                                                            "from PNM stream that is larger than allowed " +
325                                                            "maximum sample value " + maxSample + " (" + result + ").");
326                                            }
327                                            return result;
328                                    }
329                                    // ignore whitespace
330                            }
331                            else
332                            if (b == 35) 
333                            {
334                                    // the # character, indicating a comment row
335                                    if (hasDigit)
336                                    {
337                                            in.unread(b);
338                                            if (result > maxSample)
339                                            {
340                                                    throw new InvalidFileStructureException("Read " +
341                                                            "number from PNM stream that is larger than " +
342                                                            "allowed maximum sample value " + maxSample + 
343                                                            " (" + result + ").");
344                                            }
345                                            return result;
346                                    }
347                                    StringBuffer sb = new StringBuffer();
348                                    do
349                                    {
350                                            b = in.read();
351                                    }
352                                    while (b != -1 && b != 10 && b != 13);
353                                    if (b == 13)
354                                    {
355                                    }
356                                    // put it into the comment list
357                            }
358                            else
359                            if (b == -1)
360                            {
361                                    // the end of file character
362                                    if (hasDigit)
363                                    {
364                                            if (result > maxSample)
365                                            {
366                                                    throw new InvalidFileStructureException("Read number from PNM stream that is larger than allowed maximum sample value " +
367                                                            maxSample + " (" + result + ")");
368                                            }
369                                            return result;
370                                    }
371                                    throw new InvalidFileStructureException("Unexpected end of file while reading ASCII number from PNM stream.");
372                            }
373                            else
374                            {
375                                    throw new InvalidFileStructureException("Read invalid character from PNM stream: " + b +
376                                            " dec.");
377                            }
378                    }
379                    while(true);
380            }
381    
382            private void loadBilevelImage() throws
383                    InvalidFileStructureException,
384                    IOException,
385                    WrongParameterException
386            {
387                    PixelImage image = getImage();
388                    if (image == null)
389                    {
390                            setImage(new MemoryBilevelImage(getBoundsWidth(), getBoundsHeight()));
391                    }
392                    else
393                    {
394                            if (!(image instanceof BilevelImage))
395                            {
396                                    throw new WrongParameterException("Specified input image must implement BilevelImage for this image type.");
397                            }
398                    }
399                    if (getAscii().booleanValue())
400                    {
401                            loadBilevelImageAscii();
402                    }
403                    else
404                    {
405                            loadBilevelImageBinary();
406                    }
407            }
408    
409            private void loadBilevelImageAscii() throws
410                    InvalidFileStructureException,
411                    IOException
412            {
413                    BilevelImage image = (BilevelImage)getImage();
414                    // skip the pixels of the first getBoundsY1() rows
415                    int pixelsToSkip = width * getBoundsY1();
416                    for (int i = 0; i < pixelsToSkip; i++)
417                    {
418                            int value = loadAsciiNumber();
419                    }
420                    final int NUM_ROWS = getBoundsHeight();
421                    final int COLUMNS = getBoundsWidth();
422                    final int X1 = getBoundsX1();
423                    int[] row = new int[width];
424                    // now read and store getBoundsHeight() rows
425                    for (int y = 0; y < NUM_ROWS; y++)
426                    {
427                            for (int x = 0; x < width; x++)
428                            {
429                                    int value = loadAsciiNumber();
430                                    if (value == 0)
431                                    {
432                                            row[x] = BilevelImage.WHITE;
433                                    }
434                                    else
435                                    if (value == 1)
436                                    {
437                                            row[x] = BilevelImage.BLACK;
438                                    }
439                                    else
440                                    {
441                                            throw new InvalidFileStructureException("Loaded " +
442                                                    "number for position x=" + x + ", y=" + (y + getBoundsY1()) + 
443                                                    " is neither 0 nor 1 in PBM stream: " + value);
444                                    }
445                            }
446                            image.putSamples(0, 0, y, COLUMNS, 1, row, X1);
447                            setProgress(y, NUM_ROWS);
448                    }
449            }
450    
451            private void loadBilevelImageBinary() throws
452                    InvalidFileStructureException,
453                    IOException
454            {
455                    BilevelImage image = (BilevelImage)getImage();
456                    int bytesPerRow = (width + 7) / 8;
457                    // skip the first getBoundsY1() rows
458                    long bytesToSkip = (long)getBoundsY1() * (long)bytesPerRow;
459                    // Note:
460                    // removed in.skip(bytesToSkip) because that was only available in Java 1.2
461                    // instead the following while loop is used
462                    while (bytesToSkip-- > 0)
463                    {
464                            int value = in.read();
465                    }
466                    // allocate buffer large enough for a complete row
467                    byte[] row = new byte[bytesPerRow];
468                    final int numRows = getBoundsHeight();
469                    // read and store the next getBoundsHeight() rows
470                    for (int y = 0; y < numRows; y++)
471                    {
472                            // read bytesPerRow bytes into row
473                            int bytesToRead = bytesPerRow;
474                            int index = 0;
475                            while (bytesToRead > 0)
476                            {
477                                    int result = in.read(row, index, bytesToRead);
478                                    if (result >= 0)
479                                    {
480                                            index += result;
481                                            bytesToRead -= result;
482                                    }
483                                    else
484                                    {
485                                            throw new InvalidFileStructureException("Unexpected end of input stream while reading.");
486                                    }
487                            }
488                            // invert values
489                            for (int x = 0; x < row.length; x++)
490                            {
491                                    row[x] = (byte)~row[x];
492                            }
493                            //image.putPackedBytes(0, y, bytesPerRow, buffer, 0);
494                            if (isRowRequired(y))
495                            {
496                                    image.putPackedBytes(0, y - getBoundsY1(), getBoundsWidth(), row, getBoundsX1() >> 3, getBoundsX1() & 7);
497                            }
498                            setProgress(y, numRows);
499                    }
500            }
501    
502            private void loadColorImage() throws InvalidFileStructureException, IOException
503            {
504                    RGBIntegerImage image = null;
505                    RGB24Image image24 = null;
506                    if (maxSample <= 255)
507                    {
508                            image24 = new MemoryRGB24Image(width, height);
509                            image = image24;
510                            setImage(image);
511                    }
512                    else
513                    {
514                            image = new MemoryRGB48Image(width, height);
515                            setImage(image);
516                    }
517                    for (int y = 0, destY = - getBoundsY1(); y < height; y++, destY++)
518                    {
519                            if (getAscii().booleanValue())
520                            {
521                                    for (int x = 0; x < width; x++)
522                                    {
523                                            int red = loadAsciiNumber();
524                                            if (red < 0 || red > maxSample)
525                                            {
526                                                    throw new InvalidFileStructureException("Invalid " +
527                                                            "sample value " + red + " for red sample at " +
528                                                            "(x=" + x + ", y=" + y + ").");
529                                            }
530                                            image.putSample(RGBIndex.INDEX_RED, x, y, red);
531    
532                                            int green = loadAsciiNumber();
533                                            if (green < 0 || green > maxSample)
534                                            {
535                                                    throw new InvalidFileStructureException("Invalid " +
536                                                            "sample value " + green + " for green sample at " +
537                                                            "(x=" + x + ", y=" + y + ").");
538                                            }
539                                            image.putSample(RGBIndex.INDEX_GREEN, x, y, green);
540    
541                                            int blue = loadAsciiNumber();
542                                            if (blue < 0 || blue > maxSample)
543                                            {
544                                                    throw new InvalidFileStructureException("Invalid " +
545                                                            "sample value " + blue + " for blue sample at " +
546                                                            "(x=" + x + ", y=" + y + ").");
547                                            }
548                                            image.putSample(RGBIndex.INDEX_BLUE, x, y, blue);
549                                    }
550                            }
551                            else
552                            {
553                                    if (image24 != null)
554                                    {
555                                            for (int x = 0; x < width; x++)
556                                            {
557                                                    int red = in.read();
558                                                    if (red == -1)
559                                                    {
560                                                            throw new InvalidFileStructureException("Unexpected " +
561                                                                    "end of file while reading red sample for pixel " +
562                                                                    "x=" + x + ", y=" + y + ".");
563                                                    }
564                                                    image24.putByteSample(RGBIndex.INDEX_RED, x, y, (byte)(red & 0xff));
565                                                    int green = in.read();
566                                                    if (green == -1)
567                                                    {
568                                                            throw new InvalidFileStructureException("Unexpected " +
569                                                                    "end of file while reading green sample for pixel " +
570                                                                    "x=" + x + ", y=" + y + ".");
571                                                    }
572                                                    image24.putByteSample(RGBIndex.INDEX_GREEN, x, y, (byte)(green & 0xff));
573                                                    int blue = in.read();
574                                                    if (blue == -1)
575                                                    {
576                                                            throw new InvalidFileStructureException("Unexpected " +
577                                                                    "end of file while reading blue sample for pixel " +
578                                                                    "x=" + x + ", y=" + y + ".");
579                                                    }
580                                                    image24.putByteSample(RGBIndex.INDEX_BLUE, x, y, (byte)(blue & 0xff));
581                                            }
582                                    }
583                            }
584                            setProgress(y, getBoundsHeight());
585                    }
586            }
587    
588            private void loadGrayImage() throws InvalidFileStructureException, IOException, UnsupportedTypeException
589            {
590                    final int WIDTH = getBoundsWidth();
591                    final int HEIGHT = getBoundsHeight();
592                    PixelImage pimage = getImage();
593                    if (pimage == null)
594                    {
595                            if (maxSample < 256)
596                            {
597                                    pimage = new MemoryGray8Image(WIDTH, HEIGHT);
598                            }
599                            else
600                            if (maxSample < 65536)
601                            {
602                                    pimage = new MemoryGray16Image(WIDTH, HEIGHT);
603                            }
604                            else
605                            {
606                                    throw new UnsupportedTypeException("Gray images with more than 16 bits per pixel are not supported.");
607                            }
608                            setImage(pimage);
609                    }
610                    else
611                    {
612                    }
613                    GrayIntegerImage image = (GrayIntegerImage)pimage;
614                    int[] buffer = new int[width];
615                    for (int y = 0, destY = -getBoundsY1(); destY < getBoundsHeight(); y++, destY++)
616                    {
617                            if (getAscii().booleanValue())
618                            {
619                                    for (int x = 0; x < width; x++)
620                                    {
621                                            buffer[x] = loadAsciiNumber();
622                                    }
623                            }
624                            else
625                            {
626                                    if (maxSample < 256)
627                                    {
628                                            for (int x = 0; x < width; x++)
629                                            {
630                                                    buffer[x] = in.read();
631                                            }
632                                    }
633                                    else
634                                    {
635                                            for (int x = 0; x < width; x++)
636                                            {
637                                                    int msb = in.read();
638                                                    int lsb = in.read();
639                                                    buffer[x] = (msb << 8) | lsb;
640                                            }
641                                    }
642                            }
643                            if (destY >= 0 && destY < getBoundsHeight())
644                            {
645                                    image.putSamples(0, 0, destY, getBoundsWidth(), 1, buffer, getBoundsX1());
646                            }
647                            setProgress(y, getBoundsY2() + 1);
648                    }
649            }
650    
651            private String loadTextLine() throws InvalidFileStructureException, IOException
652            {
653                    // load text lines until
654                    // 1) a normal text line is found
655                    // 2) an error occurs
656                    // any comment lines starting with # are added to the
657                    // comments Vector
658                    boolean isComment;
659                    StringBuffer sb;
660                    do
661                    {
662                            sb = new StringBuffer();
663                            int b;
664                            boolean crOrLf;
665                            do
666                            {
667                                    b = in.read();
668                                    if (b == -1)
669                                    {
670                                            throw new InvalidFileStructureException("Unexpected end of file in PNM stream.");
671                                    }
672                                    crOrLf = (b == 0x0a || b == 0x0d);
673                                    if (!crOrLf)
674                                    {
675                                            sb.append((char)b);
676                                    }
677                            }
678                            while (!crOrLf);
679                            if (b == 0x0d)
680                            {
681                                    b = in.read();
682                                    if (b != 0x0a)
683                                    {
684                                            throw new InvalidFileStructureException("Unexpected end of file in PNM stream.");
685                                    }
686                            }
687                            isComment = (sb.length() > 0 && sb.charAt(0) == '#');
688                            if (isComment)
689                            {
690                                    //sb.deleteCharAt(0);
691                                    //sb.delete(0, 1);
692                                    StringBuffer result = new StringBuffer(sb.length() - 1);
693                                    int i = 1;
694                                    while (i < sb.length())
695                                    {
696                                            result.append(sb.charAt(i++));
697                                    }
698                                    appendComment(result.toString());
699                            }
700                    }
701                    while (isComment);
702                    return sb.toString();
703            }
704    
705            /**
706             * Loads the first two characters (which are expected to be a capital P
707             * followed by a decimal digit between 1 and 6, inclusively) and skips
708             * following LF and CR characters.
709             * This method not only checks the two bytes, it also initializes internal fields
710             * for storage mode (ASCII or binary) and image type.
711             *
712             * @throws WrongFileFormatException if the input stream is not a PNM stream
713             * @throws InvalidFileStructureException if the format that
714             *  is described above was not encountered
715             * @throws java.io.IOException if there were errors reading data
716             * @throws java.lang.IllegalArgumentException if the input stream was not given to this codec
717             */
718            private void loadType() throws InvalidFileStructureException, IOException, WrongFileFormatException 
719            {
720                    // read two bytes
721                    int v1 = in.read();
722                    int v2 = in.read();
723                    // check if first byte is P
724                    if (v1 != 0x50)
725                    {
726                            throw new WrongFileFormatException("Not a PNM stream. First byte " +
727                                    "in PNM stream is expected to be 0x50 ('P'); found: " +
728                                    v1 + " (dec).");
729                    }
730                    // check if second byte is ASCII of digit from 1 to 6
731                    if (v2 < 0x31 || v2 > 0x36)
732                    {
733                            throw new WrongFileFormatException("Not a PNM stream. Second byte " +
734                                    "in PNM stream is expected to be the ASCII value of decimal " +
735                                    "digit between 1 and 6 (49 dec to 54 dec); found " +
736                                    v2 + " dec.");
737                    }
738                    // determine mode (ASCII or binary) from second byte
739                    ascii = new Boolean(v2 < 0x34);
740                    // determine image type from second byte
741                    v2 = v2 - 0x30;
742                    imageType = (v2 - 1) % 3;
743                    // skip LF and CR
744                    int b;
745                    do
746                    {
747                            b = in.read();
748                    }
749                    while (b == 0x0a || b == 0x0d || b == ' ');
750                    if (b == -1)
751                    {
752                            throw new InvalidFileStructureException("Read type (" +
753                                    v2 + "). Unexpected end of file in input PNM stream.");
754                    }
755                    in.unread(b);
756            }
757    
758            public void process() throws
759                    MissingParameterException,
760                    OperationFailedException
761            {
762                    initModeFromIOObjects();
763                    try
764                    {
765                            if (getMode() == CodecMode.LOAD)
766                            {
767                                    load();
768                            }
769                            else
770                            {
771                                    save();
772                            }
773                    }
774                    catch (IOException ioe)
775                    {
776                            throw new OperationFailedException("I/O error: " + ioe.toString());
777                    }
778            }
779    
780            private void save() throws
781                    IOException, 
782                    MissingParameterException,
783                    WrongParameterException
784            {
785                    out = getOutputAsDataOutput();
786                    if (out == null)
787                    {
788                            throw new WrongParameterException("Cannot get a DataOutput object to use for saving.");
789                    }
790                    PixelImage pi = getImage();
791                    if (pi == null)
792                    {
793                            throw new MissingParameterException("Input image missing.");
794                    }
795                    if (!(pi instanceof IntegerImage))
796                    {
797                            throw new WrongParameterException("Input image must implement IntegerImage.");
798                    }
799                    IntegerImage image = (IntegerImage)pi;
800                    width = image.getWidth();
801                    height = image.getHeight();
802                    setBoundsIfNecessary(width, height);
803                    if (image instanceof RGB24Image)
804                    {
805                            imageType = IMAGE_TYPE_COLOR;
806                            maxSample = 255;
807                            save((RGB24Image)image);
808                    }
809                    else
810                    if (image instanceof RGB48Image)
811                    {
812                            imageType = IMAGE_TYPE_COLOR;
813                            maxSample = 65535;
814                            save((RGB48Image)image);
815                    }
816                    else
817                    if (image instanceof BilevelImage)
818                    {
819                            imageType = IMAGE_TYPE_BILEVEL;
820                            maxSample = 1;
821                            save((BilevelImage)image);
822                    }
823                    else
824                    if (image instanceof Gray8Image)
825                    {
826                            imageType = IMAGE_TYPE_GRAY;
827                            maxSample = 255;
828                            save((Gray8Image)image);
829                    }
830                    else
831                    if (image instanceof Gray16Image)
832                    {
833                            imageType = IMAGE_TYPE_GRAY;
834                            maxSample = 65535;
835                            save((Gray16Image)image);
836                    }
837                    else
838                    {
839                            throw new WrongParameterException("Unsupported input image type: " +
840                                    image.getClass().getName());
841                    }
842                    close();
843            }
844    
845            private void save(BilevelImage image) throws IOException
846            {
847                    saveHeader();
848                    final int WIDTH = getBoundsWidth();
849                    final int HEIGHT = getBoundsHeight();
850                    final int BYTES_PER_ROW = (WIDTH + 7) / 8;
851                    byte[] buffer = new byte[BYTES_PER_ROW];
852                    for (int y = 0, srcY = getBoundsY1(); y < HEIGHT; y++, srcY++)
853                    {
854                            if (getAscii().booleanValue())
855                            {
856                                    for (int x = 0, srcX = getBoundsX1(); x < WIDTH; x++, srcX++)
857                                    {
858                                            if (image.isBlack(srcX, srcY))
859                                            {
860                                                    out.write(49); // 1
861                                            }
862                                            else
863                                            {
864                                                    out.write(48); // 0
865                                            }
866                                            columns ++;
867                                            if (columns > 70)
868                                            {
869                                                    columns = 0;
870                                                    out.write(10);
871                                            }
872                                            else
873                                            {
874                                                    out.write(32);
875                                                    columns++;
876                                            }
877                                    }
878                            }
879                            else
880                            {
881                                    image.getPackedBytes(getBoundsX1(), srcY, WIDTH, buffer, 0, 0);
882                                    for (int x = 0; x < buffer.length; x++)
883                                    {
884                                            buffer[x] = (byte)(~buffer[x]);
885                                    }
886                                    out.write(buffer);
887                            }
888                            setProgress(y, HEIGHT);
889                    }
890            }
891    
892            private void save(Gray8Image image) throws IOException
893            {
894                    saveHeader();
895                    final int HEIGHT = getBoundsHeight();
896                    final int WIDTH = getBoundsWidth();
897                    final int X1 = getBoundsX1();
898                    System.out.println(WIDTH + " " + HEIGHT + " " + X1);
899                    byte[] buffer = new byte[WIDTH];
900                    for (int y = 0, srcY = getBoundsY1(); y < HEIGHT; y++, srcY++)
901                    {
902                            image.getByteSamples(0, X1, srcY, WIDTH, 1, buffer, 0);
903                            if (getAscii().booleanValue())
904                            {
905                                    for (int x = 0; x < WIDTH; x++)
906                                    {
907                                            saveAsciiNumber(buffer[x] & 0xff);
908                                            out.write(32);
909                                            columns += 2;
910                                            if (columns > 70)
911                                            {
912                                                    columns = 0;
913                                                    out.write(10);
914                                            }
915                                            else
916                                            {
917                                                    out.write(32);
918                                                    columns++;
919                                            }
920                                    }
921                            }
922                            else
923                            {
924                                    out.write(buffer);
925                            }
926                            setProgress(y, HEIGHT);
927                    }
928            }
929    
930            private void save(Gray16Image image) throws IOException
931            {
932                    saveHeader();
933                    final int HEIGHT = getBoundsHeight();
934                    final int WIDTH = getBoundsWidth();
935                    final int X1 = getBoundsX1();
936                    short[] buffer = new short[WIDTH];
937                    for (int y = 0, srcY = getBoundsY1(); y < HEIGHT; y++, srcY++)
938                    {
939                            image.getShortSamples(0, X1, srcY, WIDTH, 1, buffer, 0);
940                            if (getAscii().booleanValue())
941                            {
942                                    for (int x = 0; x < WIDTH; x++)
943                                    {
944                                            saveAsciiNumber(buffer[x] & 0xffff);
945                                            out.write(32);
946                                            columns += 4;
947                                            if (columns > 70)
948                                            {
949                                                    columns = 0;
950                                                    out.write(10);
951                                            }
952                                            else
953                                            {
954                                                    out.write(32);
955                                                    columns++;
956                                            }
957                                    }
958                            }
959                            else
960                            {
961                                    for (int x = 0; x < WIDTH; x++)
962                                    {
963                                            int sample = buffer[x] & 0xffff;
964                                            out.write((sample >> 8) & 0xff);
965                                            out.write(sample & 0xff);
966                                    }
967                            }
968                            setProgress(y, HEIGHT);
969                    }
970            }
971    
972            private void save(RGB24Image image) throws IOException
973            {
974                    saveHeader();
975                    final int WIDTH = getBoundsWidth();
976                    final int HEIGHT = getBoundsHeight();
977                    for (int y = 0, srcY = getBoundsY1(); y < HEIGHT; y++, srcY++)
978                    {
979                            if (getAscii().booleanValue())
980                            {
981                                    for (int x = 0, srcX = getBoundsX1(); x < WIDTH; x++, srcX++)
982                                    {
983                                            int red = image.getSample(RGBIndex.INDEX_RED, srcX, srcY);
984                                            int green = image.getSample(RGBIndex.INDEX_GREEN, srcX, srcY);
985                                            int blue = image.getSample(RGBIndex.INDEX_BLUE, srcX, srcY);
986                                            saveAsciiNumber(red);
987                                            out.write(32);
988                                            saveAsciiNumber(green);
989                                            out.write(32);
990                                            saveAsciiNumber(blue);
991                                            columns += 11;
992                                            if (columns > 80)
993                                            {
994                                                    columns = 0;
995                                                    out.write(10);
996                                            }
997                                            else
998                                            {
999                                                    out.write(32);
1000                                                    columns++;
1001                                            }
1002                                    }
1003                            }
1004                            else
1005                            {
1006                                    for (int x = 0, srcX = getBoundsX1(); x < WIDTH; x++, srcX++)
1007                                    {
1008                                            out.write(image.getSample(RGBIndex.INDEX_RED, srcX, srcY));
1009                                            out.write(image.getSample(RGBIndex.INDEX_GREEN, srcX, srcY));
1010                                            out.write(image.getSample(RGBIndex.INDEX_BLUE, srcX, srcY));
1011                                    }
1012                            }
1013                            setProgress(y, HEIGHT);
1014                    }
1015            }
1016    
1017            private void save(RGB48Image image) throws IOException
1018            {
1019                    saveHeader();
1020                    final int WIDTH = getBoundsWidth();
1021                    final int HEIGHT = getBoundsHeight();
1022                    for (int y = 0, srcY = getBoundsY1(); y < HEIGHT; y++, srcY++)
1023                    {
1024                            if (getAscii().booleanValue())
1025                            {
1026                                    for (int x = 0, srcX = getBoundsX1(); x < WIDTH; x++, srcX++)
1027                                    {
1028                                            int red = image.getSample(RGBIndex.INDEX_RED, srcX, srcY);
1029                                            int green = image.getSample(RGBIndex.INDEX_GREEN, srcX, srcY);
1030                                            int blue = image.getSample(RGBIndex.INDEX_BLUE, srcX, srcY);
1031                                            saveAsciiNumber(red);
1032                                            out.write(32);
1033                                            saveAsciiNumber(green);
1034                                            out.write(32);
1035                                            saveAsciiNumber(blue);
1036                                            columns += 13;
1037                                            if (columns > 80)
1038                                            {
1039                                                    columns = 0;
1040                                                    out.write(10);
1041                                            }
1042                                            else
1043                                            {
1044                                                    out.write(32);
1045                                                    columns++;
1046                                            }
1047                                    }
1048                            }
1049                            else
1050                            {
1051                                    /*
1052                                    for (int x = 0, srcX = getBoundsX1(); x < WIDTH; x++, srcX++)
1053                                    {
1054                                            out.write(image.getSample(RGBIndex.INDEX_RED, srcX, srcY));
1055                                            out.write(image.getSample(RGBIndex.INDEX_GREEN, srcX, srcY));
1056                                            out.write(image.getSample(RGBIndex.INDEX_BLUE, srcX, srcY));
1057                                    }
1058                                    */
1059                            }
1060                            setProgress(y, HEIGHT);
1061                    }
1062            }
1063    
1064            private void saveAsciiNumber(int number) throws
1065                    IOException
1066            {
1067                    String s = Integer.toString(number);
1068                    for (int i = 0; i < s.length(); i++)
1069                    {
1070                            char c = s.charAt(i);
1071                            out.write(c);
1072                    }
1073                    columns += s.length();
1074            }
1075    
1076            private void saveHeader() throws IOException
1077            {
1078                    out.write(80); // 'P'
1079                    int pnmType = 49 + imageType;
1080                    if (getAscii() == null)
1081                    {
1082                            setAscii(maxSample > 255);
1083                    }
1084                    if (!getAscii().booleanValue())
1085                    {
1086                            pnmType += 3;
1087                    }
1088                    out.write(pnmType); // '1' .. '6'
1089                    out.write(10); // line feed
1090                    saveAsciiNumber(getBoundsWidth());
1091                    out.write(32); // space
1092                    saveAsciiNumber(getBoundsHeight());
1093                    out.write(10); // line feed
1094                    if (imageType != IMAGE_TYPE_BILEVEL)
1095                    {
1096                            // bilevel max sample is always 1 and MUST NOT be saved
1097                            saveAsciiNumber(maxSample);
1098                            out.write(10);// line feed
1099                    }
1100            }
1101    
1102            /**
1103             * Specify whether ASCII mode is to be used when saving an image.
1104             * Default is binary mode.
1105             * @param asciiMode if true, ASCII mode is used, binary mode otherwise
1106             */
1107            public void setAscii(boolean asciiMode)
1108            {
1109                    ascii = new Boolean(asciiMode);
1110            }
1111    
1112            private void setMaximumSample(String line) throws InvalidFileStructureException
1113            {
1114                    line = line.trim();
1115                    try
1116                    {
1117                            maxSample = Integer.parseInt(line);
1118                    }
1119                    catch (NumberFormatException nfe)
1120                    {
1121                            throw new InvalidFileStructureException("Not a valid value for the maximum sample: " + line);
1122                    }
1123                    if (maxSample < 0)
1124                    {
1125                            throw new InvalidFileStructureException("The value for the maximum sample must not be negative; found " + maxSample);
1126                    }
1127            }
1128    
1129            /*
1130             * Reads resolution from argument String and sets private variables
1131             * width and height.
1132             */
1133            private void setResolution(String line) throws InvalidFileStructureException
1134            {
1135                    line = line.trim();
1136                    StringTokenizer st = new StringTokenizer(line, " ");
1137                    try
1138                    {
1139                            if (!st.hasMoreTokens())
1140                            {
1141                                    throw new InvalidFileStructureException("No width value found in line \"" +
1142                                            line + "\".");
1143                            }
1144                            String number = st.nextToken();
1145                            try
1146                            {
1147                                    width = Integer.parseInt(number);
1148                            }
1149                            catch (NumberFormatException nfe)
1150                            {
1151                                    throw new InvalidFileStructureException("Not a valid int value for width: " +
1152                                            number);
1153                            }
1154                            if (width < 1)
1155                            {
1156                                    throw new InvalidFileStructureException("The width value must be larger than " +
1157                                            "zero; found " + width + ".");
1158                            }
1159                            if (!st.hasMoreTokens())
1160                            {
1161                                    throw new InvalidFileStructureException("No height value found in line \"" +
1162                                            line + "\".");
1163                            }
1164                            number = st.nextToken();
1165                            try
1166                            {
1167                                    height = Integer.parseInt(number);
1168                            }
1169                            catch (NumberFormatException nfe)
1170                            {
1171                                    throw new InvalidFileStructureException("Not a valid int value for height: " +
1172                                            number);
1173                            }
1174                            if (height < 1)
1175                            {
1176                                    throw new InvalidFileStructureException("The height value must be larger than " +
1177                                            "zero; found " + width + ".");
1178                            }
1179                    }
1180                    catch (NoSuchElementException nsee)
1181                    {
1182                            // should not happen because we always check if there is a token
1183                    }
1184            }
1185    
1186            public String suggestFileExtension(PixelImage image)
1187            {
1188                    if (image == null)
1189                    {
1190                            return null;
1191                    }
1192                    if (image instanceof BilevelImage)
1193                    {
1194                            return IMAGE_TYPE_FILE_EXTENSIONS[IMAGE_TYPE_BILEVEL];
1195                    }
1196                    else
1197                    if (image instanceof GrayImage)
1198                    {
1199                            return IMAGE_TYPE_FILE_EXTENSIONS[IMAGE_TYPE_GRAY];
1200                    }
1201                    else
1202                    if (image instanceof RGB24Image)
1203                    {
1204                            return IMAGE_TYPE_FILE_EXTENSIONS[IMAGE_TYPE_COLOR];
1205                    }
1206                    return null;
1207            }
1208    }