001: /*
002: * $RCSfile: PNMImageEncoder.java,v $
003: *
004: * Copyright (c) 2005 Sun Microsystems, Inc. All rights reserved.
005: *
006: * Use is subject to license terms.
007: *
008: * $Revision: 1.1 $
009: * $Date: 2005/02/11 04:55:38 $
010: * $State: Exp $
011: */
012: package com.sun.media.jai.codecimpl;
013:
014: import java.awt.Rectangle;
015: import java.awt.image.ColorModel;
016: import java.awt.image.ComponentSampleModel;
017: import java.awt.image.DataBuffer;
018: import java.awt.image.DataBufferByte;
019: import java.awt.image.IndexColorModel;
020: import java.awt.image.MultiPixelPackedSampleModel;
021: import java.awt.image.Raster;
022: import java.awt.image.RenderedImage;
023: import java.awt.image.SampleModel;
024: import java.io.IOException;
025: import java.io.OutputStream;
026: import com.sun.media.jai.codec.ImageEncoderImpl;
027: import com.sun.media.jai.codec.ImageEncodeParam;
028: import com.sun.media.jai.codec.PNMEncodeParam;
029:
030: /**
031: * An ImageEncoder for the PNM family of file formats.
032: *
033: * <p> The PNM file format includes PBM for monochrome images, PGM for
034: * grey scale images, and PPM for color images. When writing the
035: * source data out, the encoder chooses the appropriate file variant
036: * based on the actual SampleModel of the source image. In case the
037: * source image data is unsuitable for the PNM file format, for
038: * example when source has 4 bands or float data type, the encoder
039: * throws an Error.
040: *
041: * <p> The raw file format is used wherever possible, unless the
042: * PNMEncodeParam object supplied to the constructor returns
043: * <code>true</code> from its <code>getRaw()</code> method.
044: *
045: *
046: * @since EA2
047: */
048: public class PNMImageEncoder extends ImageEncoderImpl {
049:
050: private static final int PBM_ASCII = '1';
051: private static final int PGM_ASCII = '2';
052: private static final int PPM_ASCII = '3';
053: private static final int PBM_RAW = '4';
054: private static final int PGM_RAW = '5';
055: private static final int PPM_RAW = '6';
056:
057: private static final int SPACE = ' ';
058:
059: private static final String COMMENT = "# written by com.sun.media.jai.codecimpl.PNMImageEncoder";
060:
061: private byte[] lineSeparator;
062:
063: private int variant;
064: private int maxValue;
065:
066: public PNMImageEncoder(OutputStream output, ImageEncodeParam param) {
067: super (output, param);
068: if (this .param == null) {
069: this .param = new PNMEncodeParam();
070: }
071: }
072:
073: /**
074: * Encodes a RenderedImage and writes the output to the
075: * OutputStream associated with this ImageEncoder.
076: */
077: public void encode(RenderedImage im) throws IOException {
078: int minX = im.getMinX();
079: int minY = im.getMinY();
080: int width = im.getWidth();
081: int height = im.getHeight();
082: int tileHeight = im.getTileHeight();
083: SampleModel sampleModel = im.getSampleModel();
084: ColorModel colorModel = im.getColorModel();
085:
086: String ls = (String) java.security.AccessController
087: .doPrivileged(new sun.security.action.GetPropertyAction(
088: "line.separator"));
089: lineSeparator = ls.getBytes();
090:
091: int dataType = sampleModel.getTransferType();
092: if ((dataType == DataBuffer.TYPE_FLOAT)
093: || (dataType == DataBuffer.TYPE_DOUBLE)) {
094: throw new RuntimeException(JaiI18N
095: .getString("PNMImageEncoder0"));
096: }
097:
098: // Raw data can only handle bytes, everything greater must be ASCII.
099: int[] sampleSize = sampleModel.getSampleSize();
100: int numBands = sampleModel.getNumBands();
101:
102: // Colormap populated for non-bilevel IndexColorModel only.
103: byte[] reds = null;
104: byte[] greens = null;
105: byte[] blues = null;
106:
107: // Flag indicating that PB data should be inverted before writing.
108: boolean isPBMInverted = false;
109:
110: if (numBands == 1) {
111: if (colorModel instanceof IndexColorModel) {
112: IndexColorModel icm = (IndexColorModel) colorModel;
113:
114: int mapSize = icm.getMapSize();
115: if (mapSize < (1 << sampleSize[0])) {
116: throw new RuntimeException(JaiI18N
117: .getString("PNMImageEncoder1"));
118: }
119:
120: if (sampleSize[0] == 1) {
121: variant = PBM_RAW;
122:
123: // Set PBM inversion flag if 1 maps to a higher color
124: // value than 0: PBM expects white-is-zero so if this
125: // does not obtain then inversion needs to occur.
126: isPBMInverted = (icm.getRed(1) + icm.getGreen(1) + icm
127: .getBlue(1)) > (icm.getRed(0)
128: + icm.getGreen(0) + icm.getBlue(0));
129: } else {
130: variant = PPM_RAW;
131:
132: reds = new byte[mapSize];
133: greens = new byte[mapSize];
134: blues = new byte[mapSize];
135:
136: icm.getReds(reds);
137: icm.getGreens(greens);
138: icm.getBlues(blues);
139: }
140: } else if (sampleSize[0] == 1) {
141: variant = PBM_RAW;
142: } else if (sampleSize[0] <= 8) {
143: variant = PGM_RAW;
144: } else {
145: variant = PGM_ASCII;
146: }
147: } else if (numBands == 3) {
148: if (sampleSize[0] <= 8 && sampleSize[1] <= 8
149: && sampleSize[2] <= 8) { // all 3 bands must be <= 8
150: variant = PPM_RAW;
151: } else {
152: variant = PPM_ASCII;
153: }
154: } else {
155: throw new RuntimeException(JaiI18N
156: .getString("PNMImageEncoder2"));
157: }
158:
159: // Read parameters
160: if (((PNMEncodeParam) param).getRaw()) {
161: if (!isRaw(variant)) {
162: boolean canUseRaw = true;
163:
164: // Make sure sampleSize for all bands no greater than 8.
165: for (int i = 0; i < sampleSize.length; i++) {
166: if (sampleSize[i] > 8) {
167: canUseRaw = false;
168: break;
169: }
170: }
171:
172: if (canUseRaw) {
173: variant += 0x3;
174: }
175: }
176: } else {
177: if (isRaw(variant)) {
178: variant -= 0x3;
179: }
180: }
181:
182: maxValue = (1 << sampleSize[0]) - 1;
183:
184: // Write PNM file.
185: output.write('P'); // magic value
186: output.write(variant);
187:
188: output.write(lineSeparator);
189: output.write(COMMENT.getBytes()); // comment line
190:
191: output.write(lineSeparator);
192: writeInteger(output, width); // width
193: output.write(SPACE);
194: writeInteger(output, height); // height
195:
196: // Writ esample max value for non-binary images
197: if ((variant != PBM_RAW) && (variant != PBM_ASCII)) {
198: output.write(lineSeparator);
199: writeInteger(output, maxValue);
200: }
201:
202: // The spec allows a single character between the
203: // last header value and the start of the raw data.
204: if (variant == PBM_RAW || variant == PGM_RAW
205: || variant == PPM_RAW) {
206: output.write('\n');
207: }
208:
209: // Set flag for optimal image writing case: row-packed data with
210: // correct band order if applicable.
211: boolean writeOptimal = false;
212: if (variant == PBM_RAW
213: && sampleModel.getTransferType() == DataBuffer.TYPE_BYTE
214: && sampleModel instanceof MultiPixelPackedSampleModel) {
215:
216: MultiPixelPackedSampleModel mppsm = (MultiPixelPackedSampleModel) sampleModel;
217:
218: // Must have left-aligned bytes with unity bit stride.
219: if (mppsm.getDataBitOffset() == 0
220: && mppsm.getPixelBitStride() == 1) {
221:
222: writeOptimal = true;
223: }
224: } else if ((variant == PGM_RAW || variant == PPM_RAW)
225: && sampleModel instanceof ComponentSampleModel
226: && !(colorModel instanceof IndexColorModel)) {
227:
228: ComponentSampleModel csm = (ComponentSampleModel) sampleModel;
229:
230: // Pixel stride must equal band count.
231: if (csm.getPixelStride() == numBands) {
232: writeOptimal = true;
233:
234: // Band offsets must equal band indices.
235: if (variant == PPM_RAW) {
236: int[] bandOffsets = csm.getBandOffsets();
237: for (int b = 0; b < numBands; b++) {
238: if (bandOffsets[b] != b) {
239: writeOptimal = false;
240: break;
241: }
242: }
243: }
244: }
245: }
246:
247: // Write using an optimal approach if possible.
248: if (writeOptimal) {
249: int bytesPerRow = variant == PBM_RAW ? (width + 7) / 8
250: : width * sampleModel.getNumBands();
251: int numYTiles = im.getNumYTiles();
252: Rectangle imageBounds = new Rectangle(im.getMinX(), im
253: .getMinY(), im.getWidth(), im.getHeight());
254: Rectangle stripRect = new Rectangle(im.getMinX(), im
255: .getMinTileY()
256: * im.getTileHeight() + im.getTileGridYOffset(), im
257: .getWidth(), im.getTileHeight());
258:
259: byte[] invertedData = null;
260: if (isPBMInverted) {
261: invertedData = new byte[bytesPerRow];
262: }
263:
264: // Loop over tiles to minimize cobbling.
265: for (int j = 0; j < numYTiles; j++) {
266: // Clamp the strip to the image bounds.
267: if (j == numYTiles - 1) {
268: stripRect.height = im.getHeight() - stripRect.y;
269: }
270:
271: Rectangle encodedRect = stripRect
272: .intersection(imageBounds);
273: // Get a strip of data.
274: Raster strip = im.getData(encodedRect);
275:
276: // Get the data array.
277: byte[] bdata = ((DataBufferByte) strip.getDataBuffer())
278: .getData();
279:
280: // Get the scanline stride.
281: int rowStride = variant == PBM_RAW ? ((MultiPixelPackedSampleModel) strip
282: .getSampleModel()).getScanlineStride()
283: : ((ComponentSampleModel) strip
284: .getSampleModel()).getScanlineStride();
285:
286: if (rowStride == bytesPerRow && !isPBMInverted) {
287: // Write the entire strip at once.
288: output.write(bdata, 0, bdata.length);
289: } else {
290: // Write the strip row-by-row.
291: int offset = 0;
292: for (int i = 0; i < encodedRect.height; i++) {
293: if (isPBMInverted) {
294: for (int k = 0; k < bytesPerRow; k++) {
295: invertedData[k] = (byte) (~(bdata[offset
296: + k] & 0xff));
297: }
298: output.write(invertedData, 0, bytesPerRow);
299: } else {
300: output.write(bdata, offset, bytesPerRow);
301: }
302: offset += rowStride;
303: }
304: }
305:
306: // Increment the strip origin.
307: stripRect.y += tileHeight;
308: }
309:
310: // Write all buffered bytes and return.
311: output.flush();
312:
313: return;
314: }
315:
316: // Buffer for up to 8 rows of pixels
317: int[] pixels = new int[8 * width * numBands];
318:
319: // Also allocate a buffer to hold the data to be written to the file,
320: // so we can use array writes.
321: byte[] bpixels = reds == null ? new byte[8 * width * numBands]
322: : new byte[8 * width * 3];
323:
324: // The index of the sample being written, used to
325: // place a line separator after every 16th sample in
326: // ASCII mode. Not used in raw mode.
327: int count = 0;
328:
329: // Process 8 rows at a time so all but the last will have
330: // a multiple of 8 pixels. This simplifies PBM_RAW encoding.
331: int lastRow = minY + height;
332: for (int row = minY; row < lastRow; row += 8) {
333: int rows = Math.min(8, lastRow - row);
334: int size = rows * width * numBands;
335:
336: // Grab the pixels
337: Raster src = im.getData(new Rectangle(minX, row, width,
338: rows));
339: src.getPixels(minX, row, width, rows, pixels);
340:
341: // Invert bits if necessary.
342: if (isPBMInverted) {
343: for (int k = 0; k < size; k++) {
344: pixels[k] ^= 0x00000001;
345: }
346: }
347:
348: switch (variant) {
349: case PBM_ASCII:
350: case PGM_ASCII:
351: for (int i = 0; i < size; i++) {
352: if ((count++ % 16) == 0) {
353: output.write(lineSeparator);
354: } else {
355: output.write(SPACE);
356: }
357: writeInteger(output, pixels[i]);
358: }
359: output.write(lineSeparator);
360: break;
361:
362: case PPM_ASCII:
363: if (reds == null) { // no need to expand
364: for (int i = 0; i < size; i++) {
365: if ((count++ % 16) == 0) {
366: output.write(lineSeparator);
367: } else {
368: output.write(SPACE);
369: }
370: writeInteger(output, pixels[i]);
371: }
372:
373: } else {
374: for (int i = 0; i < size; i++) {
375: if ((count++ % 16) == 0) {
376: output.write(lineSeparator);
377: } else {
378: output.write(SPACE);
379: }
380: writeInteger(output, (reds[pixels[i]] & 0xFF));
381: output.write(SPACE);
382: writeInteger(output, (greens[pixels[i]] & 0xFF));
383: output.write(SPACE);
384: writeInteger(output, (blues[pixels[i]] & 0xFF));
385: }
386: }
387: output.write(lineSeparator);
388: break;
389:
390: case PBM_RAW:
391: // 8 pixels packed into 1 byte, the leftovers are padded.
392: int kdst = 0;
393: int ksrc = 0;
394: for (int i = 0; i < size / 8; i++) {
395: int b = (pixels[ksrc++] << 7)
396: | (pixels[ksrc++] << 6)
397: | (pixels[ksrc++] << 5)
398: | (pixels[ksrc++] << 4)
399: | (pixels[ksrc++] << 3)
400: | (pixels[ksrc++] << 2)
401: | (pixels[ksrc++] << 1) | pixels[ksrc++];
402: bpixels[kdst++] = (byte) b;
403: }
404:
405: // Leftover pixels, only possible at the end of the file.
406: if (size % 8 > 0) {
407: int b = 0;
408: for (int i = 0; i < size % 8; i++) {
409: b |= pixels[size + i] << (7 - i);
410: }
411: bpixels[kdst++] = (byte) b;
412: }
413: output.write(bpixels, 0, (size + 7) / 8);
414:
415: break;
416:
417: case PGM_RAW:
418: for (int i = 0; i < size; i++) {
419: bpixels[i] = (byte) (pixels[i]);
420: }
421: output.write(bpixels, 0, size);
422: break;
423:
424: case PPM_RAW:
425: if (reds == null) { // no need to expand
426: for (int i = 0; i < size; i++) {
427: bpixels[i] = (byte) (pixels[i] & 0xFF);
428: }
429: } else {
430: for (int i = 0, j = 0; i < size; i++) {
431: bpixels[j++] = reds[pixels[i]];
432: bpixels[j++] = greens[pixels[i]];
433: bpixels[j++] = blues[pixels[i]];
434: }
435: }
436: output.write(bpixels, 0, bpixels.length);
437: break;
438: }
439: }
440:
441: // Force all buffered bytes to be written out.
442: output.flush();
443: }
444:
445: /** Writes an integer to the output in ASCII format. */
446: private void writeInteger(OutputStream output, int i)
447: throws IOException {
448: output.write(Integer.toString(i).getBytes());
449: }
450:
451: /** Writes a byte to the output in ASCII format. */
452: private void writeByte(OutputStream output, byte b)
453: throws IOException {
454: output.write(Byte.toString(b).getBytes());
455: }
456:
457: /** Returns true if file variant is raw format, false if ASCII. */
458: private boolean isRaw(int v) {
459: return (v >= PBM_RAW);
460: }
461: }
|