KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > nextapp > echo2 > webcontainer > image > PngEncoder


1 /*
2  * This file is part of the Echo Web Application Framework (hereinafter "Echo").
3  * Copyright (C) 2002-2005 NextApp, Inc.
4  *
5  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
6  *
7  * The contents of this file are subject to the Mozilla Public License Version
8  * 1.1 (the "License"); you may not use this file except in compliance with
9  * the License. You may obtain a copy of the License at
10  * http://www.mozilla.org/MPL/
11  *
12  * Software distributed under the License is distributed on an "AS IS" basis,
13  * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
14  * for the specific language governing rights and limitations under the
15  * License.
16  *
17  * Alternatively, the contents of this file may be used under the terms of
18  * either the GNU General Public License Version 2 or later (the "GPL"), or
19  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
20  * in which case the provisions of the GPL or the LGPL are applicable instead
21  * of those above. If you wish to allow use of your version of this file only
22  * under the terms of either the GPL or the LGPL, and not to allow others to
23  * use your version of this file under the terms of the MPL, indicate your
24  * decision by deleting the provisions above and replace them with the notice
25  * and other provisions required by the GPL or the LGPL. If you do not delete
26  * the provisions above, a recipient may use your version of this file under
27  * the terms of any one of the MPL, the GPL or the LGPL.
28  */

29
30 package nextapp.echo2.webcontainer.image;
31
32 import java.awt.Image JavaDoc;
33 import java.awt.image.BufferedImage JavaDoc;
34 import java.awt.image.DataBuffer JavaDoc;
35 import java.awt.image.IndexColorModel JavaDoc;
36 import java.awt.image.Raster JavaDoc;
37 import java.io.ByteArrayOutputStream JavaDoc;
38 import java.io.IOException JavaDoc;
39 import java.io.OutputStream JavaDoc;
40 import java.util.Arrays JavaDoc;
41 import java.util.zip.CheckedOutputStream JavaDoc;
42 import java.util.zip.Checksum JavaDoc;
43 import java.util.zip.CRC32 JavaDoc;
44 import java.util.zip.Deflater JavaDoc;
45 import java.util.zip.DeflaterOutputStream JavaDoc;
46
47 /**
48  * Encodes a java.awt.Image into PNG format.
49  * For more information on the PNG specification, see the W3C PNG page at
50  * <a HREF="http://www.w3.org/TR/REC-png.html">http://www.w3.org/TR/REC-png.html</a>.
51  */

52 public class PngEncoder {
53
54     public static final Filter SUB_FILTER = new SubFilter();
55     public static final Filter UP_FILTER = new UpFilter();
56     public static final Filter AVERAGE_FILTER = new AverageFilter();
57     public static final Filter PAETH_FILTER = new PaethFilter();
58     
59     private static final byte[] SIGNATURE = { (byte)0x89, (byte)0x50, (byte)0x4e, (byte)0x47,
60                                               (byte)0x0d, (byte)0x0a, (byte)0x1a, (byte)0x0a };
61     private static final byte[] IHDR = { (byte) 'I', (byte) 'H', (byte) 'D', (byte) 'R' };
62     private static final byte[] PLTE = { (byte) 'P', (byte) 'L', (byte) 'T', (byte) 'E' };
63     private static final byte[] IDAT = { (byte) 'I', (byte) 'D', (byte) 'A', (byte) 'T' };
64     private static final byte[] IEND = { (byte) 'I', (byte) 'E', (byte) 'N', (byte) 'D' };
65     
66     private static final int SUB_FILTER_TYPE = 1;
67     private static final int UP_FILTER_TYPE = 2;
68     private static final int AVERAGE_FILTER_TYPE = 3;
69     private static final int PAETH_FILTER_TYPE = 4;
70
71     private static final byte BIT_DEPTH = (byte) 8;
72
73     private static final byte COLOR_TYPE_INDEXED = (byte) 3;
74     private static final byte COLOR_TYPE_RGB = (byte) 2;
75     private static final byte COLOR_TYPE_RGBA = (byte) 6;
76
77     private static final int[] INT_TRANSLATOR_CHANNEL_MAP = new int[]{2, 1, 0, 3};
78     
79     /**
80      * Writes an 32-bit integer value to the output stream.
81      *
82      * @param out the stream
83      * @param i the value
84      */

85     private static void writeInt(OutputStream JavaDoc out, int i)
86     throws IOException JavaDoc {
87         out.write(new byte[]{(byte) (i >> 24),
88                              (byte) ((i >> 16) & 0xff),
89                              (byte) ((i >> 8) & 0xff),
90                              (byte) (i & 0xff)});
91     }
92
93     /**
94      * An interface for PNG filters. Filters are used to modify the method in
95      * which pixels of the image are stored in ways that will achieve better
96      * compression.
97      */

98     public interface Filter {
99     
100         /**
101          * Filters the data in a given row of the image.
102          *
103          * @param currentRow a byte array containing the data of the row of the
104          * image to be filtered
105          * @param previousRow a byte array containing the data of the previous
106          * row of the image to be filtered
107          * @param filterOutput a byte array into which the filtered data will
108          * be placed
109          */

110         public void filter(byte[] filterOutput, byte[] currentRow, byte[] previousRow, int outputBpp);
111         
112         /**
113          * Returns the PNG type code for the filter.
114          */

115         public int getType();
116     }
117     
118     /**
119      * An implementation of a "Sub" filter.
120      */

121     private static class SubFilter
122     implements Filter {
123     
124         /**
125          * @see nextapp.echo2.webcontainer.image.PngEncoder.Filter#filter(byte[], byte[], byte[], int)
126          */

127         public void filter(byte[] filterOutput, byte[] currentRow, byte[] previousRow, int outputBpp) {
128             for (int index = 0; index < filterOutput.length; ++index) {
129                 if (index < outputBpp) {
130                     filterOutput[index] = currentRow[index];
131                 } else {
132                     filterOutput[index] = (byte) (currentRow[index] - currentRow[index - outputBpp]);
133                 }
134             }
135         }
136
137         /**
138          * @see nextapp.echo2.webcontainer.image.PngEncoder.Filter#getType()
139          */

140         public int getType() {
141             return SUB_FILTER_TYPE;
142         }
143     }
144         
145     /**
146      * An implementation of an "Up" filter.
147      */

148     private static class UpFilter
149     implements Filter {
150
151         /**
152          * @see nextapp.echo2.webcontainer.image.PngEncoder.Filter#filter(byte[], byte[], byte[], int)
153          */

154         public void filter(byte[] filterOutput, byte[] currentRow, byte[] previousRow, int outputBpp) {
155             for (int index = 0; index < currentRow.length; ++index) {
156                 filterOutput[index] = (byte) (currentRow[index] - previousRow[index]);
157             }
158         }
159
160         /**
161          * @see nextapp.echo2.webcontainer.image.PngEncoder.Filter#getType()
162          */

163         public int getType() {
164             return UP_FILTER_TYPE;
165         }
166     }
167     
168     /**
169      * An implementation of an "Average" filter.
170      */

171     private static class AverageFilter
172     implements Filter {
173         
174         /**
175          * @see nextapp.echo2.webcontainer.image.PngEncoder.Filter#filter(byte[], byte[], byte[], int)
176          */

177         public void filter(byte[] filterOutput, byte[] currentRow, byte[] previousRow, int outputBpp) {
178             int w, n;
179             
180             for (int index = 0; index < filterOutput.length; ++index) {
181                 n = (previousRow[index] + 0x100) & 0xff;
182                 if (index < outputBpp) {
183                     w = 0;
184                 } else {
185                     w = (currentRow[index - outputBpp] + 0x100) & 0xff;
186                 }
187                 filterOutput[index] = (byte) (currentRow[index] - (byte) ((w + n) / 2));
188             }
189         }
190
191         /**
192          * @see nextapp.echo2.webcontainer.image.PngEncoder.Filter#getType()
193          */

194         public int getType() {
195             return AVERAGE_FILTER_TYPE;
196         }
197     }
198
199     /**
200      * An implementation of a "Paeth" filter.
201      */

202     private static class PaethFilter
203     implements Filter {
204     
205         /**
206          * @see nextapp.echo2.webcontainer.image.PngEncoder.Filter#filter(byte[], byte[], byte[], int)
207          */

208         public void filter(byte[] filterOutput, byte[] currentRow, byte[] previousRow, int outputBpp) {
209             byte pv;
210             int n, w, nw, p, pn, pw, pnw;
211             
212             for (int index = 0; index < filterOutput.length; ++index) {
213                 n = (previousRow[index] + 0x100) & 0xff;
214                 if (index < outputBpp) {
215                     w = 0;
216                     nw = 0;
217                 } else {
218                     w = (currentRow[index - outputBpp] + 0x100) & 0xff;
219                     nw = (previousRow[index - outputBpp] + 0x100) & 0xff;
220                 }
221                 
222                 p = w + n - nw;
223                 pw = Math.abs(p - w);
224                 pn = Math.abs(p - n);
225                 pnw = Math.abs(p - w);
226                 if (pw <= pn && pw <= pnw) {
227                     pv = (byte) w;
228                 } else if (pn <= pnw) {
229                     pv = (byte) n;
230                 } else {
231                     pv = (byte) nw;
232                 }
233                 
234                 filterOutput[index] = (byte) (currentRow[index] - pv);
235             }
236         }
237
238         /**
239          * @see nextapp.echo2.webcontainer.image.PngEncoder.Filter#getType()
240          */

241         public int getType() {
242             return PAETH_FILTER_TYPE;
243         }
244     }
245     
246     /**
247      * An interface for translators, which translate pixel data from a
248      * writable raster into an R/G/B/A ordering required by the PNG
249      * specification. Pixel data in the raster might be available
250      * in three bytes per pixel, four bytes per pixel, or as integers.
251      */

252     interface Translator {
253     
254         /**
255          * Translates a row of the image into a byte array ordered
256          * properly for a PNG image.
257          *
258          * @param outputPixelQueue the byte array in which to store the
259          * translated pixels
260          * @param row the row index of the image to translate
261          */

262         public void translate(byte[] outputPixelQueue, int row);
263     }
264     
265     /**
266      * Translates byte-based rasters.
267      */

268     private class ByteTranslator
269     implements Translator {
270     
271         int rowWidth = width * outputBpp; // size of image data in a row in bytes.
272
byte[] inputPixelQueue = new byte[rowWidth + outputBpp];
273         int column;
274         int channel;
275
276         /**
277          * @see nextapp.echo2.webcontainer.image.PngEncoder.Translator#translate(byte[], int)
278          */

279         public void translate(byte[] outputPixelQueue, int row) {
280             raster.getDataElements(0, row, width, 1, inputPixelQueue);
281             for (column = 0; column < width; ++column) {
282                 for (channel = 0; channel < outputBpp; ++channel) {
283                     outputPixelQueue[column * outputBpp + channel]
284                             = inputPixelQueue[column * inputBpp + channel];
285                 }
286             }
287         }
288     }
289     
290     /**
291      * Translates integer-based rasters.
292      */

293     private class IntTranslator
294     implements Translator {
295     
296         int[] inputPixelQueue = new int[width];
297         int column;
298         int channel;
299
300         /**
301          * @see nextapp.echo2.webcontainer.image.PngEncoder.Translator#translate(byte[], int)
302          */

303         public void translate(byte[] outputPixelQueue, int row) {
304         
305             image.getRGB(0, row, width, 1, inputPixelQueue, 0, width);
306
307             // Line below replaces line above, almost halving time to encode, but doesn't work with certain pixel arrangements.
308
// Need to find method of determining pixel order (BGR vs RGB, ARGB, etc)
309
// raster.getDataElements(0, row, width, 1, inputPixelQueue);
310

311             for (column = 0; column < width; ++column) {
312                 for (channel = 0; channel < outputBpp; ++channel) {
313                     outputPixelQueue[column * outputBpp + channel]
314                             = (byte) (inputPixelQueue[column] >> (INT_TRANSLATOR_CHANNEL_MAP[channel] * 8));
315                 }
316             }
317         }
318     }
319     
320     private BufferedImage JavaDoc image;
321     private Filter filter;
322     private int compressionLevel;
323     private int width;
324     private int height;
325     private int transferType;
326     private Raster JavaDoc raster;
327     private int inputBpp;
328     private int outputBpp;
329     private Translator translator;
330     
331     /**
332      * Creates a PNG encoder for an image.
333      *
334      * @param image the image to be encoded
335      * @param encodeAlpha true if the image's alpha channel should be encoded
336      * @param filter The filter to be applied to the image data, one of the
337      * following values:
338      * <ul>
339      * <li>SUB_FILTER</li>
340      * <li>UP_FILTER</li>
341      * <li>AVERAGE_FILTER</li>
342      * <li>PAETH_FILTER</li>
343      * </ul>
344      * If a null value is specified, no filtering will be performed.
345      * @param compressionLevel the deflater compression level that will be used
346      * for compressing the image data: Valid values range from 0 to 9.
347      * Higher values result in smaller files and therefore decrease
348      * network traffic, but require more CPU time to encode. The normal
349      * compromise value is 3.
350      */

351     public PngEncoder(Image JavaDoc image, boolean encodeAlpha, Filter filter, int compressionLevel) {
352         super();
353         
354         this.image = ImageToBufferedImage.toBufferedImage(image);
355         this.filter = filter;
356         this.compressionLevel = compressionLevel;
357         
358         width = this.image.getWidth(null);
359         height = this.image.getHeight(null);
360         raster = this.image.getRaster();
361         transferType = raster.getTransferType();
362
363         // Establish storage information
364
int dataBytes = raster.getNumDataElements();
365         if (transferType == DataBuffer.TYPE_BYTE && dataBytes == 4) {
366             outputBpp = encodeAlpha ? 4 : 3;
367             inputBpp = 4;
368             translator = new ByteTranslator();
369         } else if (transferType == DataBuffer.TYPE_BYTE && dataBytes == 3) {
370             outputBpp = 3;
371             inputBpp = 3;
372             encodeAlpha = false;
373             translator = new ByteTranslator();
374         } else if (transferType == DataBuffer.TYPE_INT && dataBytes == 1) {
375             outputBpp = encodeAlpha ? 4 : 3;
376             inputBpp = 4;
377             translator = new IntTranslator();
378         } else if (transferType == DataBuffer.TYPE_BYTE && dataBytes == 1) {
379             throw new UnsupportedOperationException JavaDoc("Encoding indexed-color images not yet supported.");
380         } else {
381             throw new IllegalArgumentException JavaDoc(
382                     "Cannot determine appropriate bits-per-pixel for provided image.");
383         }
384     }
385     
386     /**
387      * Encodes the image.
388      *
389      * @param out an OutputStream to which the encoded image will be
390      * written
391      * @throws IOException if a problem is encountered writing the output
392      */

393     public synchronized void encode(OutputStream JavaDoc out)
394     throws IOException JavaDoc {
395         Checksum JavaDoc csum = new CRC32 JavaDoc();
396         out = new CheckedOutputStream JavaDoc(out, csum);
397     
398         out.write(SIGNATURE);
399
400         writeIhdrChunk(out, csum);
401
402         if (outputBpp == 1) {
403             writePlteChunk(out, csum);
404         }
405         
406         writeIdatChunks(out, csum);
407         
408         writeIendChunk(out, csum);
409     }
410     
411     /**
412      * Writes the IDAT (Image data) chunks to the output stream.
413      *
414      * @param out the OutputStream to write the chunk to
415      * @param csum the Checksum that is updated as data is written
416      * to the passed-in OutputStream
417      * @throws IOException if a problem is encountered writing the output
418      */

419     private void writeIdatChunks(OutputStream JavaDoc out, Checksum JavaDoc csum)
420     throws IOException JavaDoc {
421         int rowWidth = width * outputBpp; // size of image data in a row in bytes.
422

423         int row = 0;
424                 
425         Deflater JavaDoc deflater = new Deflater JavaDoc(compressionLevel);
426         ByteArrayOutputStream JavaDoc byteOut = new ByteArrayOutputStream JavaDoc();
427         DeflaterOutputStream JavaDoc defOut = new DeflaterOutputStream JavaDoc(byteOut, deflater);
428
429         byte[] filteredPixelQueue = new byte[rowWidth];
430
431         // Output Pixel Queues
432
byte[][] outputPixelQueue = new byte[2][rowWidth];
433         Arrays.fill(outputPixelQueue[1], (byte) 0);
434         int outputPixelQueueRow = 0;
435         int outputPixelQueuePrevRow = 1;
436
437         while (row < height) {
438             if (filter == null) {
439                 defOut.write(0);
440                 translator.translate(outputPixelQueue[outputPixelQueueRow], row);
441                 defOut.write(outputPixelQueue[outputPixelQueueRow], 0, rowWidth);
442             } else {
443                 defOut.write(filter.getType());
444                 translator.translate(outputPixelQueue[outputPixelQueueRow], row);
445                 filter.filter(filteredPixelQueue, outputPixelQueue[outputPixelQueueRow],
446                         outputPixelQueue[outputPixelQueuePrevRow], outputBpp);
447                 defOut.write(filteredPixelQueue, 0, rowWidth);
448             }
449             
450             ++row;
451             outputPixelQueueRow = row & 1;
452             outputPixelQueuePrevRow = outputPixelQueueRow ^ 1;
453         }
454         defOut.finish();
455         byteOut.close();
456         
457         writeInt(out, byteOut.size());
458         csum.reset();
459         out.write(IDAT);
460         byteOut.writeTo(out);
461         writeInt(out, (int) csum.getValue());
462     }
463     
464     /**
465      * Writes the IEND (End-of-file) chunk to the output stream.
466      *
467      * @param out the OutputStream to write the chunk to
468      * @param csum the Checksum that is updated as data is written
469      * to the passed-in OutputStream
470      * @throws IOException if a problem is encountered writing the output
471      */

472     private void writeIendChunk(OutputStream JavaDoc out, Checksum JavaDoc csum)
473     throws IOException JavaDoc {
474         writeInt(out, 0);
475         csum.reset();
476         out.write(IEND);
477         writeInt(out, (int) csum.getValue());
478     }
479     
480     /**
481      * writes the IHDR (Image Header) chunk to the output stream
482      *
483      * @param out the OutputStream to write the chunk to
484      * @param csum the Checksum that is updated as data is written
485      * to the passed-in OutputStream
486      * @throws IOException if a problem is encountered writing the output
487      */

488     private void writeIhdrChunk(OutputStream JavaDoc out, Checksum JavaDoc csum)
489     throws IOException JavaDoc {
490         writeInt(out, 13); // Chunk Size
491
csum.reset();
492         out.write(IHDR);
493         writeInt(out, width);
494         writeInt(out, height);
495         out.write(BIT_DEPTH);
496         switch (outputBpp) {
497         case 1:
498             out.write(COLOR_TYPE_INDEXED);
499             break;
500         case 3:
501             out.write(COLOR_TYPE_RGB);
502             break;
503         case 4:
504             out.write(COLOR_TYPE_RGBA);
505             break;
506         default:
507             throw new IllegalStateException JavaDoc("Invalid bytes per pixel");
508         }
509         out.write(0); // Compression Method
510
out.write(0); // Filter Method
511
out.write(0); // Interlace
512
writeInt(out, (int) csum.getValue());
513     }
514     
515     /**
516      * Writes the PLTE (Palette) chunk to the output stream.
517      *
518      * @param out the OutputStream to write the chunk to
519      * @param csum the Checksum that is updated as data is written
520      * to the passed-in OutputStream
521      * @throws IOException if a problem is encountered writing the output
522      */

523     private void writePlteChunk(OutputStream JavaDoc out, Checksum JavaDoc csum)
524     throws IOException JavaDoc {
525         IndexColorModel JavaDoc icm = (IndexColorModel JavaDoc) image.getColorModel();
526         
527         writeInt(out, 768); // Chunk Size
528
csum.reset();
529         out.write(PLTE);
530         
531         byte[] reds = new byte[256];
532         icm.getReds(reds);
533         
534         byte[] greens = new byte[256];
535         icm.getGreens(greens);
536         
537         byte[] blues = new byte[256];
538         icm.getBlues(blues);
539         
540         for (int index = 0; index < 256; ++index) {
541             out.write(reds[index]);
542             out.write(greens[index]);
543             out.write(blues[index]);
544         }
545                 
546         writeInt(out, (int) csum.getValue());
547     }
548 }
549
Popular Tags