KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > com > keypoint > PngEncoder


1 package com.keypoint;
2
3 import java.awt.Image JavaDoc;
4 import java.awt.image.ImageObserver JavaDoc;
5 import java.awt.image.PixelGrabber JavaDoc;
6 import java.io.ByteArrayOutputStream JavaDoc;
7 import java.io.IOException JavaDoc;
8 import java.util.zip.CRC32 JavaDoc;
9 import java.util.zip.Deflater JavaDoc;
10 import java.util.zip.DeflaterOutputStream JavaDoc;
11
12 /**
13  * PngEncoder takes a Java Image object and creates a byte string which can be
14  * saved as a PNG file. The Image is presumed to use the DirectColorModel.
15  *
16  * <p>Thanks to Jay Denny at KeyPoint Software
17  * http://www.keypoint.com/
18  * who let me develop this code on company time.</p>
19  *
20  * <p>You may contact me with (probably very-much-needed) improvements,
21  * comments, and bug fixes at:</p>
22  *
23  * <p><code>david@catcode.com</code></p>
24  *
25  * <p>This library is free software; you can redistribute it and/or
26  * modify it under the terms of the GNU Lesser General Public
27  * License as published by the Free Software Foundation; either
28  * version 2.1 of the License, or (at your option) any later version.</p>
29  *
30  * <p>This library is distributed in the hope that it will be useful,
31  * but WITHOUT ANY WARRANTY; without even the implied warranty of
32  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
33  * Lesser General Public License for more details.</p>
34  *
35  * <p>You should have received a copy of the GNU Lesser General Public
36  * License along with this library; if not, write to the Free Software
37  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
38  * USA. A copy of the GNU LGPL may be found at
39  * <code>http://www.gnu.org/copyleft/lesser.html</code></p>
40  *
41  * @author J. David Eisenberg
42  * @version 1.5, 19 Oct 2003
43  *
44  * CHANGES:
45  * --------
46  * 19-Nov-2002 : CODING STYLE CHANGES ONLY (by David Gilbert for Object
47  * Refinery Limited);
48  * 19-Sep-2003 : Fix for platforms using EBCDIC (contributed by Paulo Soares);
49  * 19-Oct-2003 : Change private fields to protected fields so that
50  * PngEncoderB can inherit them (JDE)
51  * Fixed bug with calculation of nRows
52  */

53
54 public class PngEncoder {
55
56     /** Constant specifying that alpha channel should be encoded. */
57     public static final boolean ENCODE_ALPHA = true;
58
59     /** Constant specifying that alpha channel should not be encoded. */
60     public static final boolean NO_ALPHA = false;
61
62     /** Constants for filter (NONE). */
63     public static final int FILTER_NONE = 0;
64
65     /** Constants for filter (SUB). */
66     public static final int FILTER_SUB = 1;
67
68     /** Constants for filter (UP). */
69     public static final int FILTER_UP = 2;
70
71     /** Constants for filter (LAST). */
72     public static final int FILTER_LAST = 2;
73     
74     /** IHDR tag. */
75     protected static final byte[] IHDR = {73, 72, 68, 82};
76     
77     /** IDAT tag. */
78     protected static final byte[] IDAT = {73, 68, 65, 84};
79     
80     /** IEND tag. */
81     protected static final byte[] IEND = {73, 69, 78, 68};
82
83     protected static final byte[] PHYS = {(byte)'p', (byte)'H', (byte)'Y', (byte)'s'};
84
85     /** The png bytes. */
86     protected byte[] pngBytes;
87
88     /** The prior row. */
89     protected byte[] priorRow;
90
91     /** The left bytes. */
92     protected byte[] leftBytes;
93
94     /** The image. */
95     protected Image JavaDoc image;
96
97     /** The width. */
98     protected int width;
99
100     /** The height. */
101     protected int height;
102
103     /** The byte position. */
104     protected int bytePos;
105
106     /** The maximum position. */
107     protected int maxPos;
108
109     /** CRC. */
110     protected CRC32 JavaDoc crc = new CRC32 JavaDoc();
111
112     /** The CRC value. */
113     protected long crcValue;
114
115     /** Encode alpha? */
116     protected boolean encodeAlpha;
117
118     /** The filter type. */
119     protected int filter;
120
121     /** The bytes-per-pixel. */
122     protected int bytesPerPixel;
123
124     /** The physical pixel dimension : number of pixels per inch on the X axis. */
125     private int xDpi = 0;
126
127     /** The physical pixel dimension : number of pixels per inch on the Y axis. */
128     private int yDpi = 0;
129
130     /** Used for conversion of DPI to Pixels per Meter. */
131     static private float INCH_IN_METER_UNIT = 0.0254f;
132
133     /**
134      * The compression level (1 = best speed, 9 = best compression,
135      * 0 = no compression).
136      */

137     protected int compressionLevel;
138
139     /**
140      * Class constructor.
141      */

142     public PngEncoder() {
143         this(null, false, FILTER_NONE, 0);
144     }
145
146     /**
147      * Class constructor specifying Image to encode, with no alpha channel
148      * encoding.
149      *
150      * @param image A Java Image object which uses the DirectColorModel
151      * @see java.awt.Image
152      */

153     public PngEncoder(Image JavaDoc image) {
154         this(image, false, FILTER_NONE, 0);
155     }
156
157     /**
158      * Class constructor specifying Image to encode, and whether to encode
159      * alpha.
160      *
161      * @param image A Java Image object which uses the DirectColorModel
162      * @param encodeAlpha Encode the alpha channel? false=no; true=yes
163      * @see java.awt.Image
164      */

165     public PngEncoder(Image JavaDoc image, boolean encodeAlpha) {
166         this(image, encodeAlpha, FILTER_NONE, 0);
167     }
168
169     /**
170      * Class constructor specifying Image to encode, whether to encode alpha,
171      * and filter to use.
172      *
173      * @param image A Java Image object which uses the DirectColorModel
174      * @param encodeAlpha Encode the alpha channel? false=no; true=yes
175      * @param whichFilter 0=none, 1=sub, 2=up
176      * @see java.awt.Image
177      */

178     public PngEncoder(Image JavaDoc image, boolean encodeAlpha, int whichFilter) {
179         this(image, encodeAlpha, whichFilter, 0);
180     }
181
182
183     /**
184      * Class constructor specifying Image source to encode, whether to encode
185      * alpha, filter to use, and compression level.
186      *
187      * @param image A Java Image object
188      * @param encodeAlpha Encode the alpha channel? false=no; true=yes
189      * @param whichFilter 0=none, 1=sub, 2=up
190      * @param compLevel 0..9 (1 = best speed, 9 = best compression, 0 = no
191      * compression)
192      * @see java.awt.Image
193      */

194     public PngEncoder(Image JavaDoc image, boolean encodeAlpha, int whichFilter,
195             int compLevel) {
196         this.image = image;
197         this.encodeAlpha = encodeAlpha;
198         setFilter(whichFilter);
199         if (compLevel >= 0 && compLevel <= 9) {
200             this.compressionLevel = compLevel;
201         }
202     }
203
204     /**
205      * Set the image to be encoded.
206      *
207      * @param image A Java Image object which uses the DirectColorModel
208      * @see java.awt.Image
209      * @see java.awt.image.DirectColorModel
210      */

211     public void setImage(Image JavaDoc image) {
212         this.image = image;
213         this.pngBytes = null;
214     }
215
216     /**
217      * Returns the image to be encoded.
218      */

219     public Image JavaDoc getImage() {
220       return image;
221     }
222
223   /**
224      * Creates an array of bytes that is the PNG equivalent of the current
225      * image, specifying whether to encode alpha or not.
226      *
227      * @param encodeAlpha boolean false=no alpha, true=encode alpha
228      * @return an array of bytes, or null if there was a problem
229      */

230     public byte[] pngEncode(boolean encodeAlpha) {
231         byte[] pngIdBytes = {-119, 80, 78, 71, 13, 10, 26, 10};
232
233         if (this.image == null) {
234             return null;
235         }
236         this.width = this.image.getWidth(null);
237         this.height = this.image.getHeight(null);
238
239         /*
240          * start with an array that is big enough to hold all the pixels
241          * (plus filter bytes), and an extra 200 bytes for header info
242          */

243         this.pngBytes = new byte[((this.width + 1) * this.height * 3) + 200];
244
245         /*
246          * keep track of largest byte written to the array
247          */

248         this.maxPos = 0;
249
250         this.bytePos = writeBytes(pngIdBytes, 0);
251         //hdrPos = bytePos;
252
writeHeader();
253         writeResolution();
254         //dataPos = bytePos;
255
if (writeImageData()) {
256             writeEnd();
257             this.pngBytes = resizeByteArray(this.pngBytes, this.maxPos);
258         }
259         else {
260             this.pngBytes = null;
261         }
262         return this.pngBytes;
263     }
264
265     /**
266      * Creates an array of bytes that is the PNG equivalent of the current
267      * image. Alpha encoding is determined by its setting in the constructor.
268      *
269      * @return an array of bytes, or null if there was a problem
270      */

271     public byte[] pngEncode() {
272         return pngEncode(this.encodeAlpha);
273     }
274
275     /**
276      * Set the alpha encoding on or off.
277      *
278      * @param encodeAlpha false=no, true=yes
279      */

280     public void setEncodeAlpha(boolean encodeAlpha) {
281         this.encodeAlpha = encodeAlpha;
282     }
283
284     /**
285      * Retrieve alpha encoding status.
286      *
287      * @return boolean false=no, true=yes
288      */

289     public boolean getEncodeAlpha() {
290         return this.encodeAlpha;
291     }
292
293     /**
294      * Set the filter to use.
295      *
296      * @param whichFilter from constant list
297      */

298     public void setFilter(int whichFilter) {
299         this.filter = FILTER_NONE;
300         if (whichFilter <= FILTER_LAST) {
301             this.filter = whichFilter;
302         }
303     }
304
305     /**
306      * Retrieve filtering scheme.
307      *
308      * @return int (see constant list)
309      */

310     public int getFilter() {
311         return this.filter;
312     }
313
314     /**
315      * Set the compression level to use.
316      *
317      * @param level the compression level (1 = best speed, 9 = best compression,
318      * 0 = no compression)
319      */

320     public void setCompressionLevel(int level) {
321         if (level >= 0 && level <= 9) {
322             this.compressionLevel = level;
323         }
324     }
325
326     /**
327      * Retrieve compression level.
328      *
329      * @return int (1 = best speed, 9 = best compression, 0 = no compression)
330      */

331     public int getCompressionLevel() {
332         return this.compressionLevel;
333     }
334
335     /**
336      * Increase or decrease the length of a byte array.
337      *
338      * @param array The original array.
339      * @param newLength The length you wish the new array to have.
340      * @return Array of newly desired length. If shorter than the
341      * original, the trailing elements are truncated.
342      */

343     protected byte[] resizeByteArray(byte[] array, int newLength) {
344         byte[] newArray = new byte[newLength];
345         int oldLength = array.length;
346
347         System.arraycopy(array, 0, newArray, 0, Math.min(oldLength, newLength));
348         return newArray;
349     }
350
351     /**
352      * Write an array of bytes into the pngBytes array.
353      * Note: This routine has the side effect of updating
354      * maxPos, the largest element written in the array.
355      * The array is resized by 1000 bytes or the length
356      * of the data to be written, whichever is larger.
357      *
358      * @param data The data to be written into pngBytes.
359      * @param offset The starting point to write to.
360      * @return The next place to be written to in the pngBytes array.
361      */

362     protected int writeBytes(byte[] data, int offset) {
363         this.maxPos = Math.max(this.maxPos, offset + data.length);
364         if (data.length + offset > this.pngBytes.length) {
365             this.pngBytes = resizeByteArray(this.pngBytes, this.pngBytes.length
366                     + Math.max(1000, data.length));
367         }
368         System.arraycopy(data, 0, this.pngBytes, offset, data.length);
369         return offset + data.length;
370     }
371
372     /**
373      * Write an array of bytes into the pngBytes array, specifying number of
374      * bytes to write. Note: This routine has the side effect of updating
375      * maxPos, the largest element written in the array.
376      * The array is resized by 1000 bytes or the length
377      * of the data to be written, whichever is larger.
378      *
379      * @param data The data to be written into pngBytes.
380      * @param nBytes The number of bytes to be written.
381      * @param offset The starting point to write to.
382      * @return The next place to be written to in the pngBytes array.
383      */

384     protected int writeBytes(byte[] data, int nBytes, int offset) {
385         this.maxPos = Math.max(this.maxPos, offset + nBytes);
386         if (nBytes + offset > this.pngBytes.length) {
387             this.pngBytes = resizeByteArray(this.pngBytes, this.pngBytes.length
388                     + Math.max(1000, nBytes));
389         }
390         System.arraycopy(data, 0, this.pngBytes, offset, nBytes);
391         return offset + nBytes;
392     }
393
394     /**
395      * Write a two-byte integer into the pngBytes array at a given position.
396      *
397      * @param n The integer to be written into pngBytes.
398      * @param offset The starting point to write to.
399      * @return The next place to be written to in the pngBytes array.
400      */

401     protected int writeInt2(int n, int offset) {
402         byte[] temp = {(byte) ((n >> 8) & 0xff), (byte) (n & 0xff)};
403         return writeBytes(temp, offset);
404     }
405
406     /**
407      * Write a four-byte integer into the pngBytes array at a given position.
408      *
409      * @param n The integer to be written into pngBytes.
410      * @param offset The starting point to write to.
411      * @return The next place to be written to in the pngBytes array.
412      */

413     protected int writeInt4(int n, int offset) {
414         byte[] temp = {(byte) ((n >> 24) & 0xff),
415                        (byte) ((n >> 16) & 0xff),
416                        (byte) ((n >> 8) & 0xff),
417                        (byte) (n & 0xff)};
418         return writeBytes(temp, offset);
419     }
420
421     /**
422      * Write a single byte into the pngBytes array at a given position.
423      *
424      * @param b The integer to be written into pngBytes.
425      * @param offset The starting point to write to.
426      * @return The next place to be written to in the pngBytes array.
427      */

428     protected int writeByte(int b, int offset) {
429         byte[] temp = {(byte) b};
430         return writeBytes(temp, offset);
431     }
432
433     /**
434      * Write a PNG "IHDR" chunk into the pngBytes array.
435      */

436     protected void writeHeader() {
437
438         int startPos = this.bytePos = writeInt4(13, this.bytePos);
439         this.bytePos = writeBytes(IHDR, this.bytePos);
440         this.width = this.image.getWidth(null);
441         this.height = this.image.getHeight(null);
442         this.bytePos = writeInt4(this.width, this.bytePos);
443         this.bytePos = writeInt4(this.height, this.bytePos);
444         this.bytePos = writeByte(8, this.bytePos); // bit depth
445
this.bytePos = writeByte((this.encodeAlpha) ? 6 : 2, this.bytePos);
446             // direct model
447
this.bytePos = writeByte(0, this.bytePos); // compression method
448
this.bytePos = writeByte(0, this.bytePos); // filter method
449
this.bytePos = writeByte(0, this.bytePos); // no interlace
450
this.crc.reset();
451         this.crc.update(this.pngBytes, startPos, this.bytePos - startPos);
452         this.crcValue = this.crc.getValue();
453         this.bytePos = writeInt4((int) this.crcValue, this.bytePos);
454     }
455
456     /**
457      * Perform "sub" filtering on the given row.
458      * Uses temporary array leftBytes to store the original values
459      * of the previous pixels. The array is 16 bytes long, which
460      * will easily hold two-byte samples plus two-byte alpha.
461      *
462      * @param pixels The array holding the scan lines being built
463      * @param startPos Starting position within pixels of bytes to be filtered.
464      * @param width Width of a scanline in pixels.
465      */

466     protected void filterSub(byte[] pixels, int startPos, int width) {
467         int offset = this.bytesPerPixel;
468         int actualStart = startPos + offset;
469         int nBytes = width * this.bytesPerPixel;
470         int leftInsert = offset;
471         int leftExtract = 0;
472
473         for (int i = actualStart; i < startPos + nBytes; i++) {
474             this.leftBytes[leftInsert] = pixels[i];
475             pixels[i] = (byte) ((pixels[i] - this.leftBytes[leftExtract])
476                      % 256);
477             leftInsert = (leftInsert + 1) % 0x0f;
478             leftExtract = (leftExtract + 1) % 0x0f;
479         }
480     }
481
482     /**
483      * Perform "up" filtering on the given row.
484      * Side effect: refills the prior row with current row
485      *
486      * @param pixels The array holding the scan lines being built
487      * @param startPos Starting position within pixels of bytes to be filtered.
488      * @param width Width of a scanline in pixels.
489      */

490     protected void filterUp(byte[] pixels, int startPos, int width) {
491
492         final int nBytes = width * this.bytesPerPixel;
493
494         for (int i = 0; i < nBytes; i++) {
495             final byte currentByte = pixels[startPos + i];
496             pixels[startPos + i] = (byte) ((pixels[startPos + i]
497                     - this.priorRow[i]) % 256);
498             this.priorRow[i] = currentByte;
499         }
500     }
501
502     /**
503      * Write the image data into the pngBytes array.
504      * This will write one or more PNG "IDAT" chunks. In order
505      * to conserve memory, this method grabs as many rows as will
506      * fit into 32K bytes, or the whole image; whichever is less.
507      *
508      *
509      * @return true if no errors; false if error grabbing pixels
510      */

511     protected boolean writeImageData() {
512         int rowsLeft = this.height; // number of rows remaining to write
513
int startRow = 0; // starting row to process this time through
514
int nRows; // how many rows to grab at a time
515

516         byte[] scanLines; // the scan lines to be compressed
517
int scanPos; // where we are in the scan lines
518
int startPos; // where this line's actual pixels start (used
519
// for filtering)
520

521         byte[] compressedLines; // the resultant compressed lines
522
int nCompressed; // how big is the compressed area?
523

524         //int depth; // color depth ( handle only 8 or 32 )
525

526         PixelGrabber JavaDoc pg;
527
528         this.bytesPerPixel = (this.encodeAlpha) ? 4 : 3;
529
530         Deflater JavaDoc scrunch = new Deflater JavaDoc(this.compressionLevel);
531         ByteArrayOutputStream JavaDoc outBytes = new ByteArrayOutputStream JavaDoc(1024);
532
533         DeflaterOutputStream JavaDoc compBytes = new DeflaterOutputStream JavaDoc(outBytes,
534                 scrunch);
535         try {
536             while (rowsLeft > 0) {
537                 nRows = Math.min(32767 / (this.width
538                         * (this.bytesPerPixel + 1)), rowsLeft);
539                 nRows = Math.max(nRows, 1);
540
541                 int[] pixels = new int[this.width * nRows];
542
543                 pg = new PixelGrabber JavaDoc(this.image, 0, startRow,
544                         this.width, nRows, pixels, 0, this.width);
545                 try {
546                     pg.grabPixels();
547                 }
548                 catch (Exception JavaDoc e) {
549                     System.err.println("interrupted waiting for pixels!");
550                     return false;
551                 }
552                 if ((pg.getStatus() & ImageObserver.ABORT) != 0) {
553                     System.err.println("image fetch aborted or errored");
554                     return false;
555                 }
556
557                 /*
558                  * Create a data chunk. scanLines adds "nRows" for
559                  * the filter bytes.
560                  */

561                 scanLines = new byte[this.width * nRows * this.bytesPerPixel
562                                      + nRows];
563
564                 if (this.filter == FILTER_SUB) {
565                     this.leftBytes = new byte[16];
566                 }
567                 if (this.filter == FILTER_UP) {
568                     this.priorRow = new byte[this.width * this.bytesPerPixel];
569                 }
570
571                 scanPos = 0;
572                 startPos = 1;
573                 for (int i = 0; i < this.width * nRows; i++) {
574                     if (i % this.width == 0) {
575                         scanLines[scanPos++] = (byte) this.filter;
576                         startPos = scanPos;
577                     }
578                     scanLines[scanPos++] = (byte) ((pixels[i] >> 16) & 0xff);
579                     scanLines[scanPos++] = (byte) ((pixels[i] >> 8) & 0xff);
580                     scanLines[scanPos++] = (byte) ((pixels[i]) & 0xff);
581                     if (this.encodeAlpha) {
582                         scanLines[scanPos++] = (byte) ((pixels[i] >> 24)
583                                 & 0xff);
584                     }
585                     if ((i % this.width == this.width - 1)
586                             && (this.filter != FILTER_NONE)) {
587                         if (this.filter == FILTER_SUB) {
588                             filterSub(scanLines, startPos, this.width);
589                         }
590                         if (this.filter == FILTER_UP) {
591                             filterUp(scanLines, startPos, this.width);
592                         }
593                     }
594                 }
595
596                 /*
597                  * Write these lines to the output area
598                  */

599                 compBytes.write(scanLines, 0, scanPos);
600
601                 startRow += nRows;
602                 rowsLeft -= nRows;
603             }
604             compBytes.close();
605
606             /*
607              * Write the compressed bytes
608              */

609             compressedLines = outBytes.toByteArray();
610             nCompressed = compressedLines.length;
611
612             this.crc.reset();
613             this.bytePos = writeInt4(nCompressed, this.bytePos);
614             this.bytePos = writeBytes(IDAT, this.bytePos);
615             this.crc.update(IDAT);
616             this.bytePos = writeBytes(compressedLines, nCompressed,
617                     this.bytePos);
618             this.crc.update(compressedLines, 0, nCompressed);
619
620             this.crcValue = this.crc.getValue();
621             this.bytePos = writeInt4((int) this.crcValue, this.bytePos);
622             scrunch.finish();
623             return true;
624         }
625         catch (IOException JavaDoc e) {
626             System.err.println(e.toString());
627             return false;
628         }
629     }
630
631     /**
632      * Write a PNG "IEND" chunk into the pngBytes array.
633      */

634     protected void writeEnd() {
635         this.bytePos = writeInt4(0, this.bytePos);
636         this.bytePos = writeBytes(IEND, this.bytePos);
637         this.crc.reset();
638         this.crc.update(IEND);
639         this.crcValue = this.crc.getValue();
640         this.bytePos = writeInt4((int) this.crcValue, this.bytePos);
641     }
642
643
644     /**
645      * Set the DPI for the X axis.
646      *
647      * @param xDpi The number of dots per inch
648      */

649     public void setXDpi(int xDpi) {
650         this.xDpi = Math.round(xDpi / INCH_IN_METER_UNIT);
651
652     }
653
654     /**
655      * Get the DPI for the X axis.
656      *
657      * @return The number of dots per inch
658      */

659     public int getXDpi() {
660         return Math.round(xDpi * INCH_IN_METER_UNIT);
661     }
662
663     /**
664      * Set the DPI for the Y axis.
665      *
666      * @param yDpi The number of dots per inch
667      */

668     public void setYDpi(int yDpi) {
669         this.yDpi = Math.round(yDpi / INCH_IN_METER_UNIT);
670     }
671
672     /**
673      * Get the DPI for the Y axis.
674      *
675      * @return The number of dots per inch
676      */

677     public int getYDpi() {
678         return Math.round(yDpi * INCH_IN_METER_UNIT);
679     }
680
681     /**
682      * Set the DPI resolution.
683      *
684      * @param xDpi The number of dots per inch for the X axis.
685      * @param yDpi The number of dots per inch for the Y axis.
686      */

687     public void setDpi(int xDpi, int yDpi) {
688         this.xDpi = Math.round(xDpi / INCH_IN_METER_UNIT);
689         this.yDpi = Math.round(yDpi / INCH_IN_METER_UNIT);
690     }
691
692     /**
693      * Write a PNG "pHYs" chunk into the pngBytes array.
694      */

695     protected void writeResolution() {
696         if (xDpi > 0 && yDpi > 0) {
697
698             final int startPos = bytePos = writeInt4(9, bytePos);
699             bytePos = writeBytes(PHYS, bytePos);
700             bytePos = writeInt4(xDpi, bytePos);
701             bytePos = writeInt4(yDpi, bytePos);
702             bytePos = writeByte(1, bytePos); // unit is the meter.
703

704             crc.reset();
705             crc.update(pngBytes, startPos, bytePos - startPos);
706             crcValue = crc.getValue();
707             bytePos = writeInt4((int) crcValue, bytePos);
708         }
709     }
710 }
711
Popular Tags