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