001: /*
002: * This file is part of the Echo Web Application Framework (hereinafter "Echo").
003: * Copyright (C) 2002-2005 NextApp, Inc.
004: *
005: * Version: MPL 1.1/GPL 2.0/LGPL 2.1
006: *
007: * The contents of this file are subject to the Mozilla Public License Version
008: * 1.1 (the "License"); you may not use this file except in compliance with
009: * the License. You may obtain a copy of the License at
010: * http://www.mozilla.org/MPL/
011: *
012: * Software distributed under the License is distributed on an "AS IS" basis,
013: * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
014: * for the specific language governing rights and limitations under the
015: * License.
016: *
017: * Alternatively, the contents of this file may be used under the terms of
018: * either the GNU General Public License Version 2 or later (the "GPL"), or
019: * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
020: * in which case the provisions of the GPL or the LGPL are applicable instead
021: * of those above. If you wish to allow use of your version of this file only
022: * under the terms of either the GPL or the LGPL, and not to allow others to
023: * use your version of this file under the terms of the MPL, indicate your
024: * decision by deleting the provisions above and replace them with the notice
025: * and other provisions required by the GPL or the LGPL. If you do not delete
026: * the provisions above, a recipient may use your version of this file under
027: * the terms of any one of the MPL, the GPL or the LGPL.
028: */
029:
030: package nextapp.echo2.webcontainer.image;
031:
032: import java.awt.Image;
033: import java.awt.image.BufferedImage;
034: import java.awt.image.DataBuffer;
035: import java.awt.image.IndexColorModel;
036: import java.awt.image.Raster;
037: import java.io.ByteArrayOutputStream;
038: import java.io.IOException;
039: import java.io.OutputStream;
040: import java.util.Arrays;
041: import java.util.zip.CheckedOutputStream;
042: import java.util.zip.Checksum;
043: import java.util.zip.CRC32;
044: import java.util.zip.Deflater;
045: import java.util.zip.DeflaterOutputStream;
046:
047: /**
048: * Encodes a java.awt.Image into PNG format.
049: * For more information on the PNG specification, see the W3C PNG page at
050: * <a href="http://www.w3.org/TR/REC-png.html">http://www.w3.org/TR/REC-png.html</a>.
051: */
052: public class PngEncoder {
053:
054: public static final Filter SUB_FILTER = new SubFilter();
055: public static final Filter UP_FILTER = new UpFilter();
056: public static final Filter AVERAGE_FILTER = new AverageFilter();
057: public static final Filter PAETH_FILTER = new PaethFilter();
058:
059: private static final byte[] SIGNATURE = { (byte) 0x89, (byte) 0x50,
060: (byte) 0x4e, (byte) 0x47, (byte) 0x0d, (byte) 0x0a,
061: (byte) 0x1a, (byte) 0x0a };
062: private static final byte[] IHDR = { (byte) 'I', (byte) 'H',
063: (byte) 'D', (byte) 'R' };
064: private static final byte[] PLTE = { (byte) 'P', (byte) 'L',
065: (byte) 'T', (byte) 'E' };
066: private static final byte[] IDAT = { (byte) 'I', (byte) 'D',
067: (byte) 'A', (byte) 'T' };
068: private static final byte[] IEND = { (byte) 'I', (byte) 'E',
069: (byte) 'N', (byte) 'D' };
070:
071: private static final int SUB_FILTER_TYPE = 1;
072: private static final int UP_FILTER_TYPE = 2;
073: private static final int AVERAGE_FILTER_TYPE = 3;
074: private static final int PAETH_FILTER_TYPE = 4;
075:
076: private static final byte BIT_DEPTH = (byte) 8;
077:
078: private static final byte COLOR_TYPE_INDEXED = (byte) 3;
079: private static final byte COLOR_TYPE_RGB = (byte) 2;
080: private static final byte COLOR_TYPE_RGBA = (byte) 6;
081:
082: private static final int[] INT_TRANSLATOR_CHANNEL_MAP = new int[] {
083: 2, 1, 0, 3 };
084:
085: /**
086: * Writes an 32-bit integer value to the output stream.
087: *
088: * @param out the stream
089: * @param i the value
090: */
091: private static void writeInt(OutputStream out, int i)
092: throws IOException {
093: out.write(new byte[] { (byte) (i >> 24),
094: (byte) ((i >> 16) & 0xff), (byte) ((i >> 8) & 0xff),
095: (byte) (i & 0xff) });
096: }
097:
098: /**
099: * An interface for PNG filters. Filters are used to modify the method in
100: * which pixels of the image are stored in ways that will achieve better
101: * compression.
102: */
103: public interface Filter {
104:
105: /**
106: * Filters the data in a given row of the image.
107: *
108: * @param currentRow a byte array containing the data of the row of the
109: * image to be filtered
110: * @param previousRow a byte array containing the data of the previous
111: * row of the image to be filtered
112: * @param filterOutput a byte array into which the filtered data will
113: * be placed
114: */
115: public void filter(byte[] filterOutput, byte[] currentRow,
116: byte[] previousRow, int outputBpp);
117:
118: /**
119: * Returns the PNG type code for the filter.
120: */
121: public int getType();
122: }
123:
124: /**
125: * An implementation of a "Sub" filter.
126: */
127: private static class SubFilter implements Filter {
128:
129: /**
130: * @see nextapp.echo2.webcontainer.image.PngEncoder.Filter#filter(byte[], byte[], byte[], int)
131: */
132: public void filter(byte[] filterOutput, byte[] currentRow,
133: byte[] previousRow, int outputBpp) {
134: for (int index = 0; index < filterOutput.length; ++index) {
135: if (index < outputBpp) {
136: filterOutput[index] = currentRow[index];
137: } else {
138: filterOutput[index] = (byte) (currentRow[index] - currentRow[index
139: - outputBpp]);
140: }
141: }
142: }
143:
144: /**
145: * @see nextapp.echo2.webcontainer.image.PngEncoder.Filter#getType()
146: */
147: public int getType() {
148: return SUB_FILTER_TYPE;
149: }
150: }
151:
152: /**
153: * An implementation of an "Up" filter.
154: */
155: private static class UpFilter implements Filter {
156:
157: /**
158: * @see nextapp.echo2.webcontainer.image.PngEncoder.Filter#filter(byte[], byte[], byte[], int)
159: */
160: public void filter(byte[] filterOutput, byte[] currentRow,
161: byte[] previousRow, int outputBpp) {
162: for (int index = 0; index < currentRow.length; ++index) {
163: filterOutput[index] = (byte) (currentRow[index] - previousRow[index]);
164: }
165: }
166:
167: /**
168: * @see nextapp.echo2.webcontainer.image.PngEncoder.Filter#getType()
169: */
170: public int getType() {
171: return UP_FILTER_TYPE;
172: }
173: }
174:
175: /**
176: * An implementation of an "Average" filter.
177: */
178: private static class AverageFilter implements Filter {
179:
180: /**
181: * @see nextapp.echo2.webcontainer.image.PngEncoder.Filter#filter(byte[], byte[], byte[], int)
182: */
183: public void filter(byte[] filterOutput, byte[] currentRow,
184: byte[] previousRow, int outputBpp) {
185: int w, n;
186:
187: for (int index = 0; index < filterOutput.length; ++index) {
188: n = (previousRow[index] + 0x100) & 0xff;
189: if (index < outputBpp) {
190: w = 0;
191: } else {
192: w = (currentRow[index - outputBpp] + 0x100) & 0xff;
193: }
194: filterOutput[index] = (byte) (currentRow[index] - (byte) ((w + n) / 2));
195: }
196: }
197:
198: /**
199: * @see nextapp.echo2.webcontainer.image.PngEncoder.Filter#getType()
200: */
201: public int getType() {
202: return AVERAGE_FILTER_TYPE;
203: }
204: }
205:
206: /**
207: * An implementation of a "Paeth" filter.
208: */
209: private static class PaethFilter implements Filter {
210:
211: /**
212: * @see nextapp.echo2.webcontainer.image.PngEncoder.Filter#filter(byte[], byte[], byte[], int)
213: */
214: public void filter(byte[] filterOutput, byte[] currentRow,
215: byte[] previousRow, int outputBpp) {
216: byte pv;
217: int n, w, nw, p, pn, pw, pnw;
218:
219: for (int index = 0; index < filterOutput.length; ++index) {
220: n = (previousRow[index] + 0x100) & 0xff;
221: if (index < outputBpp) {
222: w = 0;
223: nw = 0;
224: } else {
225: w = (currentRow[index - outputBpp] + 0x100) & 0xff;
226: nw = (previousRow[index - outputBpp] + 0x100) & 0xff;
227: }
228:
229: p = w + n - nw;
230: pw = Math.abs(p - w);
231: pn = Math.abs(p - n);
232: pnw = Math.abs(p - w);
233: if (pw <= pn && pw <= pnw) {
234: pv = (byte) w;
235: } else if (pn <= pnw) {
236: pv = (byte) n;
237: } else {
238: pv = (byte) nw;
239: }
240:
241: filterOutput[index] = (byte) (currentRow[index] - pv);
242: }
243: }
244:
245: /**
246: * @see nextapp.echo2.webcontainer.image.PngEncoder.Filter#getType()
247: */
248: public int getType() {
249: return PAETH_FILTER_TYPE;
250: }
251: }
252:
253: /**
254: * An interface for translators, which translate pixel data from a
255: * writable raster into an R/G/B/A ordering required by the PNG
256: * specification. Pixel data in the raster might be available
257: * in three bytes per pixel, four bytes per pixel, or as integers.
258: */
259: interface Translator {
260:
261: /**
262: * Translates a row of the image into a byte array ordered
263: * properly for a PNG image.
264: *
265: * @param outputPixelQueue the byte array in which to store the
266: * translated pixels
267: * @param row the row index of the image to translate
268: */
269: public void translate(byte[] outputPixelQueue, int row);
270: }
271:
272: /**
273: * Translates byte-based rasters.
274: */
275: private class ByteTranslator implements Translator {
276:
277: int rowWidth = width * outputBpp; // size of image data in a row in bytes.
278: byte[] inputPixelQueue = new byte[rowWidth + outputBpp];
279: int column;
280: int channel;
281:
282: /**
283: * @see nextapp.echo2.webcontainer.image.PngEncoder.Translator#translate(byte[], int)
284: */
285: public void translate(byte[] outputPixelQueue, int row) {
286: raster.getDataElements(0, row, width, 1, inputPixelQueue);
287: for (column = 0; column < width; ++column) {
288: for (channel = 0; channel < outputBpp; ++channel) {
289: outputPixelQueue[column * outputBpp + channel] = inputPixelQueue[column
290: * inputBpp + channel];
291: }
292: }
293: }
294: }
295:
296: /**
297: * Translates integer-based rasters.
298: */
299: private class IntTranslator implements Translator {
300:
301: int[] inputPixelQueue = new int[width];
302: int column;
303: int channel;
304:
305: /**
306: * @see nextapp.echo2.webcontainer.image.PngEncoder.Translator#translate(byte[], int)
307: */
308: public void translate(byte[] outputPixelQueue, int row) {
309:
310: image.getRGB(0, row, width, 1, inputPixelQueue, 0, width);
311:
312: // Line below replaces line above, almost halving time to encode, but doesn't work with certain pixel arrangements.
313: // Need to find method of determining pixel order (BGR vs RGB, ARGB, etc)
314: // raster.getDataElements(0, row, width, 1, inputPixelQueue);
315:
316: for (column = 0; column < width; ++column) {
317: for (channel = 0; channel < outputBpp; ++channel) {
318: outputPixelQueue[column * outputBpp + channel] = (byte) (inputPixelQueue[column] >> (INT_TRANSLATOR_CHANNEL_MAP[channel] * 8));
319: }
320: }
321: }
322: }
323:
324: private BufferedImage image;
325: private Filter filter;
326: private int compressionLevel;
327: private int width;
328: private int height;
329: private int transferType;
330: private Raster raster;
331: private int inputBpp;
332: private int outputBpp;
333: private Translator translator;
334:
335: /**
336: * Creates a PNG encoder for an image.
337: *
338: * @param image the image to be encoded
339: * @param encodeAlpha true if the image's alpha channel should be encoded
340: * @param filter The filter to be applied to the image data, one of the
341: * following values:
342: * <ul>
343: * <li>SUB_FILTER</li>
344: * <li>UP_FILTER</li>
345: * <li>AVERAGE_FILTER</li>
346: * <li>PAETH_FILTER</li>
347: * </ul>
348: * If a null value is specified, no filtering will be performed.
349: * @param compressionLevel the deflater compression level that will be used
350: * for compressing the image data: Valid values range from 0 to 9.
351: * Higher values result in smaller files and therefore decrease
352: * network traffic, but require more CPU time to encode. The normal
353: * compromise value is 3.
354: */
355: public PngEncoder(Image image, boolean encodeAlpha, Filter filter,
356: int compressionLevel) {
357: super ();
358:
359: this .image = ImageToBufferedImage.toBufferedImage(image);
360: this .filter = filter;
361: this .compressionLevel = compressionLevel;
362:
363: width = this .image.getWidth(null);
364: height = this .image.getHeight(null);
365: raster = this .image.getRaster();
366: transferType = raster.getTransferType();
367:
368: // Establish storage information
369: int dataBytes = raster.getNumDataElements();
370: if (transferType == DataBuffer.TYPE_BYTE && dataBytes == 4) {
371: outputBpp = encodeAlpha ? 4 : 3;
372: inputBpp = 4;
373: translator = new ByteTranslator();
374: } else if (transferType == DataBuffer.TYPE_BYTE
375: && dataBytes == 3) {
376: outputBpp = 3;
377: inputBpp = 3;
378: encodeAlpha = false;
379: translator = new ByteTranslator();
380: } else if (transferType == DataBuffer.TYPE_INT
381: && dataBytes == 1) {
382: outputBpp = encodeAlpha ? 4 : 3;
383: inputBpp = 4;
384: translator = new IntTranslator();
385: } else if (transferType == DataBuffer.TYPE_BYTE
386: && dataBytes == 1) {
387: throw new UnsupportedOperationException(
388: "Encoding indexed-color images not yet supported.");
389: } else {
390: throw new IllegalArgumentException(
391: "Cannot determine appropriate bits-per-pixel for provided image.");
392: }
393: }
394:
395: /**
396: * Encodes the image.
397: *
398: * @param out an OutputStream to which the encoded image will be
399: * written
400: * @throws IOException if a problem is encountered writing the output
401: */
402: public synchronized void encode(OutputStream out)
403: throws IOException {
404: Checksum csum = new CRC32();
405: out = new CheckedOutputStream(out, csum);
406:
407: out.write(SIGNATURE);
408:
409: writeIhdrChunk(out, csum);
410:
411: if (outputBpp == 1) {
412: writePlteChunk(out, csum);
413: }
414:
415: writeIdatChunks(out, csum);
416:
417: writeIendChunk(out, csum);
418: }
419:
420: /**
421: * Writes the IDAT (Image data) chunks to the output stream.
422: *
423: * @param out the OutputStream to write the chunk to
424: * @param csum the Checksum that is updated as data is written
425: * to the passed-in OutputStream
426: * @throws IOException if a problem is encountered writing the output
427: */
428: private void writeIdatChunks(OutputStream out, Checksum csum)
429: throws IOException {
430: int rowWidth = width * outputBpp; // size of image data in a row in bytes.
431:
432: int row = 0;
433:
434: Deflater deflater = new Deflater(compressionLevel);
435: ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
436: DeflaterOutputStream defOut = new DeflaterOutputStream(byteOut,
437: deflater);
438:
439: byte[] filteredPixelQueue = new byte[rowWidth];
440:
441: // Output Pixel Queues
442: byte[][] outputPixelQueue = new byte[2][rowWidth];
443: Arrays.fill(outputPixelQueue[1], (byte) 0);
444: int outputPixelQueueRow = 0;
445: int outputPixelQueuePrevRow = 1;
446:
447: while (row < height) {
448: if (filter == null) {
449: defOut.write(0);
450: translator.translate(
451: outputPixelQueue[outputPixelQueueRow], row);
452: defOut.write(outputPixelQueue[outputPixelQueueRow], 0,
453: rowWidth);
454: } else {
455: defOut.write(filter.getType());
456: translator.translate(
457: outputPixelQueue[outputPixelQueueRow], row);
458: filter.filter(filteredPixelQueue,
459: outputPixelQueue[outputPixelQueueRow],
460: outputPixelQueue[outputPixelQueuePrevRow],
461: outputBpp);
462: defOut.write(filteredPixelQueue, 0, rowWidth);
463: }
464:
465: ++row;
466: outputPixelQueueRow = row & 1;
467: outputPixelQueuePrevRow = outputPixelQueueRow ^ 1;
468: }
469: defOut.finish();
470: byteOut.close();
471:
472: writeInt(out, byteOut.size());
473: csum.reset();
474: out.write(IDAT);
475: byteOut.writeTo(out);
476: writeInt(out, (int) csum.getValue());
477: }
478:
479: /**
480: * Writes the IEND (End-of-file) chunk to the output stream.
481: *
482: * @param out the OutputStream to write the chunk to
483: * @param csum the Checksum that is updated as data is written
484: * to the passed-in OutputStream
485: * @throws IOException if a problem is encountered writing the output
486: */
487: private void writeIendChunk(OutputStream out, Checksum csum)
488: throws IOException {
489: writeInt(out, 0);
490: csum.reset();
491: out.write(IEND);
492: writeInt(out, (int) csum.getValue());
493: }
494:
495: /**
496: * writes the IHDR (Image Header) chunk to the output stream
497: *
498: * @param out the OutputStream to write the chunk to
499: * @param csum the Checksum that is updated as data is written
500: * to the passed-in OutputStream
501: * @throws IOException if a problem is encountered writing the output
502: */
503: private void writeIhdrChunk(OutputStream out, Checksum csum)
504: throws IOException {
505: writeInt(out, 13); // Chunk Size
506: csum.reset();
507: out.write(IHDR);
508: writeInt(out, width);
509: writeInt(out, height);
510: out.write(BIT_DEPTH);
511: switch (outputBpp) {
512: case 1:
513: out.write(COLOR_TYPE_INDEXED);
514: break;
515: case 3:
516: out.write(COLOR_TYPE_RGB);
517: break;
518: case 4:
519: out.write(COLOR_TYPE_RGBA);
520: break;
521: default:
522: throw new IllegalStateException("Invalid bytes per pixel");
523: }
524: out.write(0); // Compression Method
525: out.write(0); // Filter Method
526: out.write(0); // Interlace
527: writeInt(out, (int) csum.getValue());
528: }
529:
530: /**
531: * Writes the PLTE (Palette) chunk to the output stream.
532: *
533: * @param out the OutputStream to write the chunk to
534: * @param csum the Checksum that is updated as data is written
535: * to the passed-in OutputStream
536: * @throws IOException if a problem is encountered writing the output
537: */
538: private void writePlteChunk(OutputStream out, Checksum csum)
539: throws IOException {
540: IndexColorModel icm = (IndexColorModel) image.getColorModel();
541:
542: writeInt(out, 768); // Chunk Size
543: csum.reset();
544: out.write(PLTE);
545:
546: byte[] reds = new byte[256];
547: icm.getReds(reds);
548:
549: byte[] greens = new byte[256];
550: icm.getGreens(greens);
551:
552: byte[] blues = new byte[256];
553: icm.getBlues(blues);
554:
555: for (int index = 0; index < 256; ++index) {
556: out.write(reds[index]);
557: out.write(greens[index]);
558: out.write(blues[index]);
559: }
560:
561: writeInt(out, (int) csum.getValue());
562: }
563: }
|