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            String ls = (String)java.security.AccessController.doPrivileged(
103               new sun.security.action.GetPropertyAction("line.separator"));
104            lineSeparator = ls.getBytes();
105        }
106    }
107
108    /** The output stream to write into */
109    private ImageOutputStream stream = null;
110
111    /** Constructs <code>PNMImageWriter</code> based on the provided
112     *  <code>ImageWriterSpi</code>.
113     */
114    public PNMImageWriter(ImageWriterSpi originator) {
115        super(originator);
116    }
117
118    public void setOutput(Object output) {
119        super.setOutput(output); // validates output
120        if (output != null) {
121            if (!(output instanceof ImageOutputStream))
122                throw new IllegalArgumentException(I18N.getString("PNMImageWriter0"));
123            this.stream = (ImageOutputStream)output;
124        } else
125            this.stream = null;
126    }
127
128    public ImageWriteParam getDefaultWriteParam() {
129        return new PNMImageWriteParam();
130    }
131
132    public IIOMetadata getDefaultStreamMetadata(ImageWriteParam param) {
133        return null;
134    }
135
136    public IIOMetadata getDefaultImageMetadata(ImageTypeSpecifier imageType,
137                                               ImageWriteParam param) {
138        return new PNMMetadata(imageType, param);
139    }
140
141    public IIOMetadata convertStreamMetadata(IIOMetadata inData,
142                                             ImageWriteParam param) {
143        return null;
144    }
145
146    public IIOMetadata convertImageMetadata(IIOMetadata inData,
147                                            ImageTypeSpecifier imageType,
148                                            ImageWriteParam param) {
149        // Check arguments.
150        if(inData == null) {
151            throw new IllegalArgumentException("inData == null!");
152        }
153        if(imageType == null) {
154            throw new IllegalArgumentException("imageType == null!");
155        }
156
157        PNMMetadata outData = null;
158
159        // Obtain a PNMMetadata object.
160        if(inData instanceof PNMMetadata) {
161            // Clone the input metadata.
162            outData = (PNMMetadata)((PNMMetadata)inData).clone();
163        } else {
164            try {
165                outData = new PNMMetadata(inData);
166            } catch(IIOInvalidTreeException e) {
167                // XXX Warning
168                outData = new PNMMetadata();
169            }
170        }
171
172        // Update the metadata per the image type and param.
173        outData.initialize(imageType, param);
174
175        return outData;
176    }
177
178    public boolean canWriteRasters() {
179        return true;
180    }
181
182    public void write(IIOMetadata streamMetadata,
183                      IIOImage image,
184                      ImageWriteParam param) throws IOException {
185        clearAbortRequest();
186        processImageStarted(0);
187        if (param == null)
188            param = getDefaultWriteParam();
189
190        RenderedImage input = null;
191        Raster inputRaster = null;
192        boolean writeRaster = image.hasRaster();
193        Rectangle sourceRegion = param.getSourceRegion();
194        SampleModel sampleModel = null;
195        ColorModel colorModel = null;
196
197        if (writeRaster) {
198            inputRaster = image.getRaster();
199            sampleModel = inputRaster.getSampleModel();
200            if (sourceRegion == null)
201                sourceRegion = inputRaster.getBounds();
202            else
203                sourceRegion = sourceRegion.intersection(inputRaster.getBounds());
204        } else {
205            input = image.getRenderedImage();
206            sampleModel = input.getSampleModel();
207            colorModel = input.getColorModel();
208            Rectangle rect = new Rectangle(input.getMinX(), input.getMinY(),
209                                           input.getWidth(), input.getHeight());
210            if (sourceRegion == null)
211                sourceRegion = rect;
212            else
213                sourceRegion = sourceRegion.intersection(rect);
214        }
215
216        if (sourceRegion.isEmpty())
217            throw new RuntimeException(I18N.getString("PNMImageWrite1"));
218
219        ImageUtil.canEncodeImage(this, colorModel, sampleModel);
220
221        int scaleX = param.getSourceXSubsampling();
222        int scaleY = param.getSourceYSubsampling();
223        int xOffset = param.getSubsamplingXOffset();
224        int yOffset = param.getSubsamplingYOffset();
225
226        sourceRegion.translate(xOffset, yOffset);
227        sourceRegion.width -= xOffset;
228        sourceRegion.height -= yOffset;
229
230        int minX = sourceRegion.x / scaleX;
231        int minY = sourceRegion.y / scaleY;
232        int w = (sourceRegion.width + scaleX - 1) / scaleX;
233        int h = (sourceRegion.height + scaleY - 1) / scaleY;
234
235        Rectangle destinationRegion = new Rectangle(minX, minY, w, h);
236
237        int tileHeight = sampleModel.getHeight();
238        int tileWidth = sampleModel.getWidth();
239
240        // Raw data can only handle bytes, everything greater must be ASCII.
241        int[] sampleSize = sampleModel.getSampleSize();
242        int[] sourceBands = param.getSourceBands();
243        boolean noSubband = true;
244        int numBands = sampleModel.getNumBands();
245
246        if (sourceBands != null) {
247            sampleModel = sampleModel.createSubsetSampleModel(sourceBands);
248            colorModel = null;
249            noSubband = false;
250            numBands = sampleModel.getNumBands();
251        } else {
252            sourceBands = new int[numBands];
253            for (int i = 0; i < numBands; i++)
254                sourceBands[i] = i;
255        }
256
257        // Colormap populated for non-bilevel IndexColorModel only.
258        byte[] reds = null;
259        byte[] greens = null;
260        byte[] blues = null;
261
262        // Flag indicating that PB data should be inverted before writing.
263        boolean isPBMInverted = false;
264
265        if (numBands == 1) {
266            if (colorModel instanceof IndexColorModel) {
267                IndexColorModel icm = (IndexColorModel)colorModel;
268
269                int mapSize = icm.getMapSize();
270                if (mapSize < (1 << sampleSize[0]))
271                    throw new RuntimeException(I18N.getString("PNMImageWrite2"));
272
273                if(sampleSize[0] == 1) {
274                    variant = PBM_RAW;
275
276                    // Set PBM inversion flag if 1 maps to a higher color
277                    // value than 0: PBM expects white-is-zero so if this
278                    // does not obtain then inversion needs to occur.
279                    isPBMInverted = icm.getRed(1) > icm.getRed(0);
280                } else {
281                    variant = PPM_RAW;
282
283                    reds = new byte[mapSize];
284                    greens = new byte[mapSize];
285                    blues = new byte[mapSize];
286
287                    icm.getReds(reds);
288                    icm.getGreens(greens);
289                    icm.getBlues(blues);
290                }
291            } else if (sampleSize[0] == 1) {
292                variant = PBM_RAW;
293            } else if (sampleSize[0] <= 8) {
294                variant = PGM_RAW;
295            } else {
296                variant = PGM_ASCII;
297            }
298        } else if (numBands == 3) {
299            if (sampleSize[0] <= 8 && sampleSize[1] <= 8 &&
300                sampleSize[2] <= 8) {   // all 3 bands must be <= 8
301                variant = PPM_RAW;
302            } else {
303                variant = PPM_ASCII;
304            }
305        } else {
306            throw new RuntimeException(I18N.getString("PNMImageWrite3"));
307        }
308
309        IIOMetadata inputMetadata = image.getMetadata();
310        ImageTypeSpecifier imageType;
311        if(colorModel != null) {
312            imageType = new ImageTypeSpecifier(colorModel, sampleModel);
313        } else {
314            int dataType = sampleModel.getDataType();
315            switch(numBands) {
316            case 1:
317                imageType =
318                    ImageTypeSpecifier.createGrayscale(sampleSize[0], dataType,
319                                                       false);
320                break;
321            case 3:
322                ColorSpace cs = ColorSpace.getInstance(ColorSpace.CS_sRGB);
323                imageType =
324                    ImageTypeSpecifier.createInterleaved(cs,
325                                                         new int[] {0, 1, 2},
326                                                         dataType,
327                                                         false, false);
328                break;
329            default:
330                throw new IIOException("Cannot encode image with "+
331                                       numBands+" bands!");
332            }
333        }
334
335        PNMMetadata metadata;
336        if(inputMetadata != null) {
337            // Convert metadata.
338            metadata = (PNMMetadata)convertImageMetadata(inputMetadata,
339                                                         imageType, param);
340        } else {
341            // Use default.
342            metadata = (PNMMetadata)getDefaultImageMetadata(imageType, param);
343        }
344
345        // Read parameters
346        boolean isRawPNM;
347        if(param instanceof PNMImageWriteParam) {
348            isRawPNM = ((PNMImageWriteParam)param).getRaw();
349        } else {
350            isRawPNM = metadata.isRaw();
351        }
352
353        maxValue = metadata.getMaxValue();
354        for (int i = 0; i < sampleSize.length; i++) {
355            int v = (1 << sampleSize[i]) - 1;
356            if (v > maxValue) {
357                maxValue = v;
358            }
359        }
360
361        if (isRawPNM) {
362            // Raw output is desired.
363            int maxBitDepth = metadata.getMaxBitDepth();
364            if (!isRaw(variant) && maxBitDepth <= 8) {
365                // Current variant is ASCII and the bit depth is acceptable
366                // so convert to RAW variant by adding '3' to variant.
367                variant += 0x3;
368            } else if(isRaw(variant) && maxBitDepth > 8) {
369                // Current variant is RAW and the bit depth it too large for
370                // RAW so convert to ASCII.
371                variant -= 0x3;
372            }
373            // Omitted cases are (variant == RAW && max <= 8) and
374            // (variant == ASCII && max > 8) neither of which requires action.
375        } else if(isRaw(variant)) {
376            // Raw output is NOT desired so convert to ASCII
377            variant -= 0x3;
378        }
379
380        // Write PNM file.
381        stream.writeByte('P');                  // magic value: 'P'
382        stream.writeByte(variant);
383
384        stream.write(lineSeparator);
385        stream.write(COMMENT.getBytes());       // comment line
386
387        // Write the comments provided in the metadata
388        Iterator comments = metadata.getComments();
389        if(comments != null) {
390            while(comments.hasNext()) {
391                stream.write(lineSeparator);
392                String comment = "# " + (String)comments.next();
393                stream.write(comment.getBytes());
394            }
395        }
396
397        stream.write(lineSeparator);
398        writeInteger(stream, w);                // width
399        stream.write(SPACE);
400        writeInteger(stream, h);                // height
401
402        // Write sample max value for non-binary images
403        if ((variant != PBM_RAW) && (variant != PBM_ASCII)) {
404            stream.write(lineSeparator);
405            writeInteger(stream, maxValue);
406        }
407
408        // The spec allows a single character between the
409        // last header value and the start of the raw data.
410        if (variant == PBM_RAW ||
411            variant == PGM_RAW ||
412            variant == PPM_RAW) {
413            stream.write('\n');
414        }
415
416        // Set flag for optimal image writing case: row-packed data with
417        // correct band order if applicable.
418        boolean writeOptimal = false;
419        if (variant == PBM_RAW &&
420            sampleModel.getTransferType() == DataBuffer.TYPE_BYTE &&
421            sampleModel instanceof MultiPixelPackedSampleModel) {
422
423            MultiPixelPackedSampleModel mppsm =
424                (MultiPixelPackedSampleModel)sampleModel;
425
426            int originX = 0;
427            if (writeRaster)
428                originX = inputRaster.getMinX();
429            else
430                originX = input.getMinX();
431
432            // Must have left-aligned bytes with unity bit stride.
433            if(mppsm.getBitOffset((sourceRegion.x - originX) % tileWidth) == 0 &&
434               mppsm.getPixelBitStride() == 1 && scaleX == 1)
435                writeOptimal = true;
436        } else if ((variant == PGM_RAW || variant == PPM_RAW) &&
437                   sampleModel instanceof ComponentSampleModel &&
438                   !(colorModel instanceof IndexColorModel)) {
439
440            ComponentSampleModel csm =
441                (ComponentSampleModel)sampleModel;
442
443            // Pixel stride must equal band count.
444            if(csm.getPixelStride() == numBands && scaleX == 1) {
445                writeOptimal = true;
446
447                // Band offsets must equal band indices.
448                if(variant == PPM_RAW) {
449                    int[] bandOffsets = csm.getBandOffsets();
450                    for(int b = 0; b < numBands; b++) {
451                        if(bandOffsets[b] != b) {
452                            writeOptimal = false;
453                            break;
454                        }
455                    }
456                }
457            }
458        }
459
460        // Write using an optimal approach if possible.
461        if(writeOptimal) {
462            int bytesPerRow = variant == PBM_RAW ?
463                (w + 7)/8 : w * sampleModel.getNumBands();
464            byte[] bdata = null;
465            byte[] invertedData = new byte[bytesPerRow];
466
467            // Loop over tiles to minimize cobbling.
468            for(int j = 0; j < sourceRegion.height; j++) {
469                if (abortRequested())
470                    break;
471                Raster lineRaster = null;
472                if (writeRaster) {
473                    lineRaster = inputRaster.createChild(sourceRegion.x,
474                                                         j,
475                                                         sourceRegion.width,
476                                                         1, 0, 0, null);
477                } else {
478                    lineRaster =
479                        input.getData(new Rectangle(sourceRegion.x,
480                                                    sourceRegion.y + j,
481                                                    w, 1));
482                    lineRaster = lineRaster.createTranslatedChild(0, 0);
483                }
484
485                bdata = ((DataBufferByte)lineRaster.getDataBuffer()).getData();
486
487                sampleModel = lineRaster.getSampleModel();
488                int offset = 0;
489                if (sampleModel instanceof ComponentSampleModel) {
490                    offset =
491                        ((ComponentSampleModel)sampleModel).getOffset(lineRaster.getMinX()-lineRaster.getSampleModelTranslateX(),
492                                                                      lineRaster.getMinY()-lineRaster.getSampleModelTranslateY());
493                } else if (sampleModel instanceof MultiPixelPackedSampleModel) {
494                    offset = ((MultiPixelPackedSampleModel)sampleModel).getOffset(lineRaster.getMinX() -
495                                                                        lineRaster.getSampleModelTranslateX(),
496                                                                      lineRaster.getMinX()-lineRaster.getSampleModelTranslateY());
497                }
498
499                if (isPBMInverted) {
500                    for(int k = offset, m = 0; m < bytesPerRow; k++, m++)
501                        invertedData[m] = (byte)~bdata[k];
502                    bdata = invertedData;
503                    offset = 0;
504                }
505
506                stream.write(bdata, offset, bytesPerRow);
507                processImageProgress(100.0F * j / sourceRegion.height);
508            }
509
510            // Write all buffered bytes and return.
511            stream.flush();
512            if (abortRequested())
513                processWriteAborted();
514            else
515                processImageComplete();
516            return;
517        }
518
519        // Buffer for 1 rows of original pixels
520        int size = sourceRegion.width * numBands;
521
522        int[] pixels = new int[size];
523
524        // Also allocate a buffer to hold the data to be written to the file,
525        // so we can use array writes.
526        byte[] bpixels =
527            reds == null ? new byte[w * numBands] : new byte[w * 3];
528
529        // The index of the sample being written, used to
530        // place a line separator after every 16th sample in
531        // ASCII mode.  Not used in raw mode.
532        int count = 0;
533
534        // Process line by line
535        int lastRow = sourceRegion.y + sourceRegion.height;
536
537        for (int row = sourceRegion.y; row < lastRow; row += scaleY) {
538            if (abortRequested())
539                break;
540            // Grab the pixels
541            Raster src = null;
542
543            if (writeRaster)
544                src = inputRaster.createChild(sourceRegion.x,
545                                              row,
546                                              sourceRegion.width, 1,
547                                              sourceRegion.x, row, sourceBands);
548            else
549                src = input.getData(new Rectangle(sourceRegion.x, row,
550                                                  sourceRegion.width, 1));
551            src.getPixels(sourceRegion.x, row, sourceRegion.width, 1, pixels);
552
553            if (isPBMInverted)
554                for (int i = 0; i < size; i += scaleX)
555                    bpixels[i] ^= 1;
556
557            switch (variant) {
558            case PBM_ASCII:
559            case PGM_ASCII:
560                for (int i = 0; i < size; i += scaleX) {
561                    if ((count++ % 16) == 0)
562                        stream.write(lineSeparator);
563                    else
564                        stream.write(SPACE);
565
566                    writeInteger(stream, pixels[i]);
567                }
568                stream.write(lineSeparator);
569                break;
570
571            case PPM_ASCII:
572                if (reds == null) {     // no need to expand
573                    for (int i = 0; i < size; i += scaleX * numBands) {
574                        for (int j = 0; j < numBands; j++) {
575                            if ((count++ % 16) == 0)
576                                stream.write(lineSeparator);
577                            else
578                                stream.write(SPACE);
579
580                            writeInteger(stream, pixels[i + j]);
581                        }
582                    }
583                } else {
584                    for (int i = 0; i < size; i += scaleX) {
585                        if ((count++ % 5) == 0)
586                            stream.write(lineSeparator);
587                        else
588                            stream.write(SPACE);
589
590                        writeInteger(stream, (reds[pixels[i]] & 0xFF));
591                        stream.write(SPACE);
592                        writeInteger(stream, (greens[pixels[i]] & 0xFF));
593                        stream.write(SPACE);
594                        writeInteger(stream, (blues[pixels[i]] & 0xFF));
595                    }
596                }
597                stream.write(lineSeparator);
598                break;
599
600            case PBM_RAW:
601                // 8 pixels packed into 1 byte, the leftovers are padded.
602                int kdst = 0;
603                int ksrc = 0;
604                int b = 0;
605                int pos = 7;
606                for (int i = 0; i < size; i += scaleX) {
607                    b |= pixels[i] << pos;
608                    pos--;
609                    if (pos == -1) {
610                        bpixels[kdst++] = (byte)b;
611                        b = 0;
612                        pos = 7;
613                    }
614                }
615
616                if (pos != 7)
617                    bpixels[kdst++] = (byte)b;
618
619                stream.write(bpixels, 0, kdst);
620                break;
621
622            case PGM_RAW:
623                for (int i = 0, j = 0; i < size; i += scaleX) {
624                    bpixels[j++] = (byte)(pixels[i]);
625                }
626                stream.write(bpixels, 0, w);
627                break;
628
629            case PPM_RAW:
630                if (reds == null) {     // no need to expand
631                    for (int i = 0, k = 0; i < size; i += scaleX * numBands) {
632                        for (int j = 0; j < numBands; j++)
633                          bpixels[k++] = (byte)(pixels[i + j] & 0xFF);
634                    }
635                } else {
636                    for (int i = 0, j = 0; i < size; i += scaleX) {
637                        bpixels[j++] = reds[pixels[i]];
638                        bpixels[j++] = greens[pixels[i]];
639                        bpixels[j++] = blues[pixels[i]];
640                    }
641                }
642                stream.write(bpixels, 0, bpixels.length);
643                break;
644            }
645
646            processImageProgress(100.0F * (row - sourceRegion.y) /
647                                 sourceRegion.height);
648        }
649
650        // Force all buffered bytes to be written out.
651        stream.flush();
652
653        if (abortRequested())
654            processWriteAborted();
655        else
656            processImageComplete();
657    }
658
659    public void reset() {
660        super.reset();
661        stream = null;
662    }
663
664    /** Writes an integer to the output in ASCII format. */
665    private void writeInteger(ImageOutputStream output, int i) throws IOException {
666        output.write(Integer.toString(i).getBytes());
667    }
668
669    /** Writes a byte to the output in ASCII format. */
670    private void writeByte(ImageOutputStream output, byte b) throws IOException {
671        output.write(Byte.toString(b).getBytes());
672    }
673
674    /** Returns true if file variant is raw format, false if ASCII. */
675    private boolean isRaw(int v) {
676        return (v >= PBM_RAW);
677    }
678}