001/*
002 * $RCSfile: PNMImageWriter.java,v $
003 *
004 * 
005 * Copyright (c) 2005 Sun Microsystems, Inc. All  Rights Reserved.
006 * 
007 * Redistribution and use in source and binary forms, with or without
008 * modification, are permitted provided that the following conditions
009 * are met: 
010 * 
011 * - Redistribution of source code must retain the above copyright 
012 *   notice, this  list of conditions and the following disclaimer.
013 * 
014 * - Redistribution in binary form must reproduce the above copyright
015 *   notice, this list of conditions and the following disclaimer in 
016 *   the documentation and/or other materials provided with the
017 *   distribution.
018 * 
019 * Neither the name of Sun Microsystems, Inc. or the names of 
020 * contributors may be used to endorse or promote products derived 
021 * from this software without specific prior written permission.
022 * 
023 * This software is provided "AS IS," without a warranty of any 
024 * kind. ALL EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND 
025 * WARRANTIES, INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, 
026 * FITNESS FOR A PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY
027 * EXCLUDED. SUN MIDROSYSTEMS, INC. ("SUN") AND ITS LICENSORS SHALL 
028 * NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF 
029 * USING, MODIFYING OR DISTRIBUTING THIS SOFTWARE OR ITS
030 * DERIVATIVES. IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR 
031 * ANY LOST REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL,
032 * CONSEQUENTIAL, INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED AND
033 * REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT OF THE USE OF OR
034 * INABILITY TO USE THIS SOFTWARE, EVEN IF SUN HAS BEEN ADVISED OF THE
035 * POSSIBILITY OF SUCH DAMAGES. 
036 * 
037 * You acknowledge that this software is not designed or intended for 
038 * use in the design, construction, operation or maintenance of any 
039 * nuclear facility. 
040 *
041 * $Revision: 1.1 $
042 * $Date: 2005/02/11 05:01:40 $
043 * $State: Exp $
044 */
045package com.github.jaiimageio.impl.plugins.pnm;
046
047import java.awt.Rectangle;
048import java.awt.color.ColorSpace;
049import java.awt.image.ColorModel;
050import java.awt.image.ComponentSampleModel;
051import java.awt.image.DataBuffer;
052import java.awt.image.DataBufferByte;
053import java.awt.image.IndexColorModel;
054import java.awt.image.MultiPixelPackedSampleModel;
055import java.awt.image.Raster;
056import java.awt.image.RenderedImage;
057import java.awt.image.SampleModel;
058import java.io.IOException;
059import java.util.Iterator;
060
061import javax.imageio.IIOException;
062import javax.imageio.IIOImage;
063import javax.imageio.ImageTypeSpecifier;
064import javax.imageio.ImageWriteParam;
065import javax.imageio.ImageWriter;
066import javax.imageio.metadata.IIOInvalidTreeException;
067import javax.imageio.metadata.IIOMetadata;
068import javax.imageio.spi.ImageWriterSpi;
069import javax.imageio.stream.ImageOutputStream;
070
071import com.github.jaiimageio.impl.common.ImageUtil;
072import com.github.jaiimageio.plugins.pnm.PNMImageWriteParam;
073/**
074 * The Java Image IO plugin writer for encoding a binary RenderedImage into
075 * a PNM format.
076 *
077 * The encoding process may clip, subsample using the parameters
078 * specified in the <code>ImageWriteParam</code>.
079 *
080 * @see com.github.jaiimageio.plugins.PNMImageWriteParam
081 */
082public class PNMImageWriter extends ImageWriter {
083    private static final int PBM_ASCII  = '1';
084    private static final int PGM_ASCII  = '2';
085    private static final int PPM_ASCII  = '3';
086    private static final int PBM_RAW    = '4';
087    private static final int PGM_RAW    = '5';
088    private static final int PPM_RAW    = '6';
089
090    private static final int SPACE      = ' ';
091
092    private static final String COMMENT =
093        "# written by com.github.jaiimageio.impl.PNMImageWriter";
094
095    private static byte[] lineSeparator;
096
097    private int variant;
098    private int maxValue;
099
100    static {
101        if (lineSeparator == null) {
102            lineSeparator = System.getProperty("line.separator").getBytes();
103        }
104    }
105
106    /** The output stream to write into */
107    private ImageOutputStream stream = null;
108
109    /** Constructs <code>PNMImageWriter</code> based on the provided
110     *  <code>ImageWriterSpi</code>.
111     */
112    public PNMImageWriter(ImageWriterSpi originator) {
113        super(originator);
114    }
115
116    public void setOutput(Object output) {
117        super.setOutput(output); // validates output
118        if (output != null) {
119            if (!(output instanceof ImageOutputStream))
120                throw new IllegalArgumentException(I18N.getString("PNMImageWriter0"));
121            this.stream = (ImageOutputStream)output;
122        } else
123            this.stream = null;
124    }
125
126    public ImageWriteParam getDefaultWriteParam() {
127        return new PNMImageWriteParam();
128    }
129
130    public IIOMetadata getDefaultStreamMetadata(ImageWriteParam param) {
131        return null;
132    }
133
134    public IIOMetadata getDefaultImageMetadata(ImageTypeSpecifier imageType,
135                                               ImageWriteParam param) {
136        return new PNMMetadata(imageType, param);
137    }
138
139    public IIOMetadata convertStreamMetadata(IIOMetadata inData,
140                                             ImageWriteParam param) {
141        return null;
142    }
143
144    public IIOMetadata convertImageMetadata(IIOMetadata inData,
145                                            ImageTypeSpecifier imageType,
146                                            ImageWriteParam param) {
147        // Check arguments.
148        if(inData == null) {
149            throw new IllegalArgumentException("inData == null!");
150        }
151        if(imageType == null) {
152            throw new IllegalArgumentException("imageType == null!");
153        }
154
155        PNMMetadata outData = null;
156
157        // Obtain a PNMMetadata object.
158        if(inData instanceof PNMMetadata) {
159            // Clone the input metadata.
160            outData = (PNMMetadata)((PNMMetadata)inData).clone();
161        } else {
162            try {
163                outData = new PNMMetadata(inData);
164            } catch(IIOInvalidTreeException e) {
165                // XXX Warning
166                outData = new PNMMetadata();
167            }
168        }
169
170        // Update the metadata per the image type and param.
171        outData.initialize(imageType, param);
172
173        return outData;
174    }
175
176    public boolean canWriteRasters() {
177        return true;
178    }
179
180    public void write(IIOMetadata streamMetadata,
181                      IIOImage image,
182                      ImageWriteParam param) throws IOException {
183        clearAbortRequest();
184        processImageStarted(0);
185        if (param == null)
186            param = getDefaultWriteParam();
187
188        RenderedImage input = null;
189        Raster inputRaster = null;
190        boolean writeRaster = image.hasRaster();
191        Rectangle sourceRegion = param.getSourceRegion();
192        SampleModel sampleModel = null;
193        ColorModel colorModel = null;
194
195        if (writeRaster) {
196            inputRaster = image.getRaster();
197            sampleModel = inputRaster.getSampleModel();
198            if (sourceRegion == null)
199                sourceRegion = inputRaster.getBounds();
200            else
201                sourceRegion = sourceRegion.intersection(inputRaster.getBounds());
202        } else {
203            input = image.getRenderedImage();
204            sampleModel = input.getSampleModel();
205            colorModel = input.getColorModel();
206            Rectangle rect = new Rectangle(input.getMinX(), input.getMinY(),
207                                           input.getWidth(), input.getHeight());
208            if (sourceRegion == null)
209                sourceRegion = rect;
210            else
211                sourceRegion = sourceRegion.intersection(rect);
212        }
213
214        if (sourceRegion.isEmpty())
215            throw new RuntimeException(I18N.getString("PNMImageWrite1"));
216
217        ImageUtil.canEncodeImage(this, colorModel, sampleModel);
218
219        int scaleX = param.getSourceXSubsampling();
220        int scaleY = param.getSourceYSubsampling();
221        int xOffset = param.getSubsamplingXOffset();
222        int yOffset = param.getSubsamplingYOffset();
223
224        sourceRegion.translate(xOffset, yOffset);
225        sourceRegion.width -= xOffset;
226        sourceRegion.height -= yOffset;
227
228        int minX = sourceRegion.x / scaleX;
229        int minY = sourceRegion.y / scaleY;
230        int w = (sourceRegion.width + scaleX - 1) / scaleX;
231        int h = (sourceRegion.height + scaleY - 1) / scaleY;
232
233        Rectangle destinationRegion = new Rectangle(minX, minY, w, h);
234
235        int tileHeight = sampleModel.getHeight();
236        int tileWidth = sampleModel.getWidth();
237
238        // Raw data can only handle bytes, everything greater must be ASCII.
239        int[] sampleSize = sampleModel.getSampleSize();
240        int[] sourceBands = param.getSourceBands();
241        boolean noSubband = true;
242        int numBands = sampleModel.getNumBands();
243
244        if (sourceBands != null) {
245            sampleModel = sampleModel.createSubsetSampleModel(sourceBands);
246            colorModel = null;
247            noSubband = false;
248            numBands = sampleModel.getNumBands();
249        } else {
250            sourceBands = new int[numBands];
251            for (int i = 0; i < numBands; i++)
252                sourceBands[i] = i;
253        }
254
255        // Colormap populated for non-bilevel IndexColorModel only.
256        byte[] reds = null;
257        byte[] greens = null;
258        byte[] blues = null;
259
260        // Flag indicating that PB data should be inverted before writing.
261        boolean isPBMInverted = false;
262
263        if (numBands == 1) {
264            if (colorModel instanceof IndexColorModel) {
265                IndexColorModel icm = (IndexColorModel)colorModel;
266
267                int mapSize = icm.getMapSize();
268                if (mapSize < (1 << sampleSize[0]))
269                    throw new RuntimeException(I18N.getString("PNMImageWrite2"));
270
271                if(sampleSize[0] == 1) {
272                    variant = PBM_RAW;
273
274                    // Set PBM inversion flag if 1 maps to a higher color
275                    // value than 0: PBM expects white-is-zero so if this
276                    // does not obtain then inversion needs to occur.
277                    isPBMInverted = icm.getRed(1) > icm.getRed(0);
278                } else {
279                    variant = PPM_RAW;
280
281                    reds = new byte[mapSize];
282                    greens = new byte[mapSize];
283                    blues = new byte[mapSize];
284
285                    icm.getReds(reds);
286                    icm.getGreens(greens);
287                    icm.getBlues(blues);
288                }
289            } else if (sampleSize[0] == 1) {
290                variant = PBM_RAW;
291            } else if (sampleSize[0] <= 8) {
292                variant = PGM_RAW;
293            } else {
294                variant = PGM_ASCII;
295            }
296        } else if (numBands == 3) {
297            if (sampleSize[0] <= 8 && sampleSize[1] <= 8 &&
298                sampleSize[2] <= 8) {   // all 3 bands must be <= 8
299                variant = PPM_RAW;
300            } else {
301                variant = PPM_ASCII;
302            }
303        } else {
304            throw new RuntimeException(I18N.getString("PNMImageWrite3"));
305        }
306
307        IIOMetadata inputMetadata = image.getMetadata();
308        ImageTypeSpecifier imageType;
309        if(colorModel != null) {
310            imageType = new ImageTypeSpecifier(colorModel, sampleModel);
311        } else {
312            int dataType = sampleModel.getDataType();
313            switch(numBands) {
314            case 1:
315                imageType =
316                    ImageTypeSpecifier.createGrayscale(sampleSize[0], dataType,
317                                                       false);
318                break;
319            case 3:
320                ColorSpace cs = ColorSpace.getInstance(ColorSpace.CS_sRGB);
321                imageType =
322                    ImageTypeSpecifier.createInterleaved(cs,
323                                                         new int[] {0, 1, 2},
324                                                         dataType,
325                                                         false, false);
326                break;
327            default:
328                throw new IIOException("Cannot encode image with "+
329                                       numBands+" bands!");
330            }
331        }
332
333        PNMMetadata metadata;
334        if(inputMetadata != null) {
335            // Convert metadata.
336            metadata = (PNMMetadata)convertImageMetadata(inputMetadata,
337                                                         imageType, param);
338        } else {
339            // Use default.
340            metadata = (PNMMetadata)getDefaultImageMetadata(imageType, param);
341        }
342
343        // Read parameters
344        boolean isRawPNM;
345        if(param instanceof PNMImageWriteParam) {
346            isRawPNM = ((PNMImageWriteParam)param).getRaw();
347        } else {
348            isRawPNM = metadata.isRaw();
349        }
350
351        maxValue = metadata.getMaxValue();
352        for (int i = 0; i < sampleSize.length; i++) {
353            int v = (1 << sampleSize[i]) - 1;
354            if (v > maxValue) {
355                maxValue = v;
356            }
357        }
358
359        if (isRawPNM) {
360            // Raw output is desired.
361            int maxBitDepth = metadata.getMaxBitDepth();
362            if (!isRaw(variant) && maxBitDepth <= 8) {
363                // Current variant is ASCII and the bit depth is acceptable
364                // so convert to RAW variant by adding '3' to variant.
365                variant += 0x3;
366            } else if(isRaw(variant) && maxBitDepth > 8) {
367                // Current variant is RAW and the bit depth it too large for
368                // RAW so convert to ASCII.
369                variant -= 0x3;
370            }
371            // Omitted cases are (variant == RAW && max <= 8) and
372            // (variant == ASCII && max > 8) neither of which requires action.
373        } else if(isRaw(variant)) {
374            // Raw output is NOT desired so convert to ASCII
375            variant -= 0x3;
376        }
377
378        // Write PNM file.
379        stream.writeByte('P');                  // magic value: 'P'
380        stream.writeByte(variant);
381
382        stream.write(lineSeparator);
383        stream.write(COMMENT.getBytes());       // comment line
384
385        // Write the comments provided in the metadata
386        Iterator comments = metadata.getComments();
387        if(comments != null) {
388            while(comments.hasNext()) {
389                stream.write(lineSeparator);
390                String comment = "# " + (String)comments.next();
391                stream.write(comment.getBytes());
392            }
393        }
394
395        stream.write(lineSeparator);
396        writeInteger(stream, w);                // width
397        stream.write(SPACE);
398        writeInteger(stream, h);                // height
399
400        // Write sample max value for non-binary images
401        if ((variant != PBM_RAW) && (variant != PBM_ASCII)) {
402            stream.write(lineSeparator);
403            writeInteger(stream, maxValue);
404        }
405
406        // The spec allows a single character between the
407        // last header value and the start of the raw data.
408        if (variant == PBM_RAW ||
409            variant == PGM_RAW ||
410            variant == PPM_RAW) {
411            stream.write('\n');
412        }
413
414        // Set flag for optimal image writing case: row-packed data with
415        // correct band order if applicable.
416        boolean writeOptimal = false;
417        if (variant == PBM_RAW &&
418            sampleModel.getTransferType() == DataBuffer.TYPE_BYTE &&
419            sampleModel instanceof MultiPixelPackedSampleModel) {
420
421            MultiPixelPackedSampleModel mppsm =
422                (MultiPixelPackedSampleModel)sampleModel;
423
424            int originX = 0;
425            if (writeRaster)
426                originX = inputRaster.getMinX();
427            else
428                originX = input.getMinX();
429
430            // Must have left-aligned bytes with unity bit stride.
431            if(mppsm.getBitOffset((sourceRegion.x - originX) % tileWidth) == 0 &&
432               mppsm.getPixelBitStride() == 1 && scaleX == 1)
433                writeOptimal = true;
434        } else if ((variant == PGM_RAW || variant == PPM_RAW) &&
435                   sampleModel instanceof ComponentSampleModel &&
436                   !(colorModel instanceof IndexColorModel)) {
437
438            ComponentSampleModel csm =
439                (ComponentSampleModel)sampleModel;
440
441            // Pixel stride must equal band count.
442            if(csm.getPixelStride() == numBands && scaleX == 1) {
443                writeOptimal = true;
444
445                // Band offsets must equal band indices.
446                if(variant == PPM_RAW) {
447                    int[] bandOffsets = csm.getBandOffsets();
448                    for(int b = 0; b < numBands; b++) {
449                        if(bandOffsets[b] != b) {
450                            writeOptimal = false;
451                            break;
452                        }
453                    }
454                }
455            }
456        }
457
458        // Write using an optimal approach if possible.
459        if(writeOptimal) {
460            int bytesPerRow = variant == PBM_RAW ?
461                (w + 7)/8 : w * sampleModel.getNumBands();
462            byte[] bdata = null;
463            byte[] invertedData = new byte[bytesPerRow];
464
465            // Loop over tiles to minimize cobbling.
466            for(int j = 0; j < sourceRegion.height; j++) {
467                if (abortRequested())
468                    break;
469                Raster lineRaster = null;
470                if (writeRaster) {
471                    lineRaster = inputRaster.createChild(sourceRegion.x,
472                                                         j,
473                                                         sourceRegion.width,
474                                                         1, 0, 0, null);
475                } else {
476                    lineRaster =
477                        input.getData(new Rectangle(sourceRegion.x,
478                                                    sourceRegion.y + j,
479                                                    w, 1));
480                    lineRaster = lineRaster.createTranslatedChild(0, 0);
481                }
482
483                bdata = ((DataBufferByte)lineRaster.getDataBuffer()).getData();
484
485                sampleModel = lineRaster.getSampleModel();
486                int offset = 0;
487                if (sampleModel instanceof ComponentSampleModel) {
488                    offset =
489                        ((ComponentSampleModel)sampleModel).getOffset(lineRaster.getMinX()-lineRaster.getSampleModelTranslateX(),
490                                                                      lineRaster.getMinY()-lineRaster.getSampleModelTranslateY());
491                } else if (sampleModel instanceof MultiPixelPackedSampleModel) {
492                    offset = ((MultiPixelPackedSampleModel)sampleModel).getOffset(lineRaster.getMinX() -
493                                                                        lineRaster.getSampleModelTranslateX(),
494                                                                      lineRaster.getMinX()-lineRaster.getSampleModelTranslateY());
495                }
496
497                if (isPBMInverted) {
498                    for(int k = offset, m = 0; m < bytesPerRow; k++, m++)
499                        invertedData[m] = (byte)~bdata[k];
500                    bdata = invertedData;
501                    offset = 0;
502                }
503
504                stream.write(bdata, offset, bytesPerRow);
505                processImageProgress(100.0F * j / sourceRegion.height);
506            }
507
508            // Write all buffered bytes and return.
509            stream.flush();
510            if (abortRequested())
511                processWriteAborted();
512            else
513                processImageComplete();
514            return;
515        }
516
517        // Buffer for 1 rows of original pixels
518        int size = sourceRegion.width * numBands;
519
520        int[] pixels = new int[size];
521
522        // Also allocate a buffer to hold the data to be written to the file,
523        // so we can use array writes.
524        byte[] bpixels =
525            reds == null ? new byte[w * numBands] : new byte[w * 3];
526
527        // The index of the sample being written, used to
528        // place a line separator after every 16th sample in
529        // ASCII mode.  Not used in raw mode.
530        int count = 0;
531
532        // Process line by line
533        int lastRow = sourceRegion.y + sourceRegion.height;
534
535        for (int row = sourceRegion.y; row < lastRow; row += scaleY) {
536            if (abortRequested())
537                break;
538            // Grab the pixels
539            Raster src = null;
540
541            if (writeRaster)
542                src = inputRaster.createChild(sourceRegion.x,
543                                              row,
544                                              sourceRegion.width, 1,
545                                              sourceRegion.x, row, sourceBands);
546            else
547                src = input.getData(new Rectangle(sourceRegion.x, row,
548                                                  sourceRegion.width, 1));
549            src.getPixels(sourceRegion.x, row, sourceRegion.width, 1, pixels);
550
551            if (isPBMInverted)
552                for (int i = 0; i < size; i += scaleX)
553                    bpixels[i] ^= 1;
554
555            switch (variant) {
556            case PBM_ASCII:
557            case PGM_ASCII:
558                for (int i = 0; i < size; i += scaleX) {
559                    if ((count++ % 16) == 0)
560                        stream.write(lineSeparator);
561                    else
562                        stream.write(SPACE);
563
564                    writeInteger(stream, pixels[i]);
565                }
566                stream.write(lineSeparator);
567                break;
568
569            case PPM_ASCII:
570                if (reds == null) {     // no need to expand
571                    for (int i = 0; i < size; i += scaleX * numBands) {
572                        for (int j = 0; j < numBands; j++) {
573                            if ((count++ % 16) == 0)
574                                stream.write(lineSeparator);
575                            else
576                                stream.write(SPACE);
577
578                            writeInteger(stream, pixels[i + j]);
579                        }
580                    }
581                } else {
582                    for (int i = 0; i < size; i += scaleX) {
583                        if ((count++ % 5) == 0)
584                            stream.write(lineSeparator);
585                        else
586                            stream.write(SPACE);
587
588                        writeInteger(stream, (reds[pixels[i]] & 0xFF));
589                        stream.write(SPACE);
590                        writeInteger(stream, (greens[pixels[i]] & 0xFF));
591                        stream.write(SPACE);
592                        writeInteger(stream, (blues[pixels[i]] & 0xFF));
593                    }
594                }
595                stream.write(lineSeparator);
596                break;
597
598            case PBM_RAW:
599                // 8 pixels packed into 1 byte, the leftovers are padded.
600                int kdst = 0;
601                int ksrc = 0;
602                int b = 0;
603                int pos = 7;
604                for (int i = 0; i < size; i += scaleX) {
605                    b |= pixels[i] << pos;
606                    pos--;
607                    if (pos == -1) {
608                        bpixels[kdst++] = (byte)b;
609                        b = 0;
610                        pos = 7;
611                    }
612                }
613
614                if (pos != 7)
615                    bpixels[kdst++] = (byte)b;
616
617                stream.write(bpixels, 0, kdst);
618                break;
619
620            case PGM_RAW:
621                for (int i = 0, j = 0; i < size; i += scaleX) {
622                    bpixels[j++] = (byte)(pixels[i]);
623                }
624                stream.write(bpixels, 0, w);
625                break;
626
627            case PPM_RAW:
628                if (reds == null) {     // no need to expand
629                    for (int i = 0, k = 0; i < size; i += scaleX * numBands) {
630                        for (int j = 0; j < numBands; j++)
631                          bpixels[k++] = (byte)(pixels[i + j] & 0xFF);
632                    }
633                } else {
634                    for (int i = 0, j = 0; i < size; i += scaleX) {
635                        bpixels[j++] = reds[pixels[i]];
636                        bpixels[j++] = greens[pixels[i]];
637                        bpixels[j++] = blues[pixels[i]];
638                    }
639                }
640                stream.write(bpixels, 0, bpixels.length);
641                break;
642            }
643
644            processImageProgress(100.0F * (row - sourceRegion.y) /
645                                 sourceRegion.height);
646        }
647
648        // Force all buffered bytes to be written out.
649        stream.flush();
650
651        if (abortRequested())
652            processWriteAborted();
653        else
654            processImageComplete();
655    }
656
657    public void reset() {
658        super.reset();
659        stream = null;
660    }
661
662    /** Writes an integer to the output in ASCII format. */
663    private void writeInteger(ImageOutputStream output, int i) throws IOException {
664        output.write(Integer.toString(i).getBytes());
665    }
666
667    /** Writes a byte to the output in ASCII format. */
668    private void writeByte(ImageOutputStream output, byte b) throws IOException {
669        output.write(Byte.toString(b).getBytes());
670    }
671
672    /** Returns true if file variant is raw format, false if ASCII. */
673    private boolean isRaw(int v) {
674        return (v >= PBM_RAW);
675    }
676}