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