4 import java.awt.image.ImageObserver;
5 import java.awt.image.PixelGrabber;
6 import java.io.ByteArrayOutputStream;
7 import java.io.IOException;
8 import java.util.zip.CRC32;
9 import java.util.zip.Deflater;
10 import java.util.zip.DeflaterOutputStream;
13 * PngEncoder takes a Java Image object and creates a byte string which can be saved as a PNG file.
14 * The Image is presumed to use the DirectColorModel.
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>
20 * <p>You may contact me with (probably very-much-needed) improvements,
21 * comments, and bug fixes at:</p>
23 * <p><code>david@catcode.com</code></p>
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>
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>
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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
38 * A copy of the GNU LGPL may be found at
39 * <code>http://www.gnu.org/copyleft/lesser.html</code></p>
41 * @author J. David Eisenberg
42 * @version 1.5, 19 Oct 2003
46 * 19-Nov-2002 : CODING STYLE CHANGES ONLY (by David Gilbert for Object Refinery Limited);
47 * 19-Sep-2003 : Fix for platforms using EBCDIC (contributed by Paulo Soares);
48 * 19-Oct-2003 : Change private fields to protected fields so that
49 * PngEncoderB can inherit them (JDE)
50 * Fixed bug with calculation of nRows
53 public class PngEncoder extends Object {
55 /** Constant specifying that alpha channel should be encoded. */
56 public static final boolean ENCODE_ALPHA = true;
58 /** Constant specifying that alpha channel should not be encoded. */
59 public static final boolean NO_ALPHA = false;
61 /** Constants for filter (NONE) */
62 public static final int FILTER_NONE = 0;
64 /** Constants for filter (SUB) */
65 public static final int FILTER_SUB = 1;
67 /** Constants for filter (UP) */
68 public static final int FILTER_UP = 2;
70 /** Constants for filter (LAST) */
71 public static final int FILTER_LAST = 2;
74 protected static final byte IHDR[] = {73, 72, 68, 82};
77 protected static final byte IDAT[] = {73, 68, 65, 84};
80 protected static final byte IEND[] = {73, 69, 78, 68};
83 protected byte[] pngBytes;
86 protected byte[] priorRow;
88 /** The left bytes. */
89 protected byte[] leftBytes;
92 protected Image image;
95 protected int width, height;
97 /** The byte position. */
98 protected int bytePos, maxPos;
101 protected CRC32 crc = new CRC32();
103 /** The CRC value. */
104 protected long crcValue;
107 protected boolean encodeAlpha;
109 /** The filter type. */
110 protected int filter;
112 /** The bytes-per-pixel. */
113 protected int bytesPerPixel;
115 /** The compression level. */
116 protected int compressionLevel;
121 public PngEncoder() {
122 this(null, false, FILTER_NONE, 0);
126 * Class constructor specifying Image to encode, with no alpha channel encoding.
128 * @param image A Java Image object which uses the DirectColorModel
129 * @see java.awt.Image
131 public PngEncoder(Image image) {
132 this(image, false, FILTER_NONE, 0);
136 * Class constructor specifying Image to encode, and whether to encode alpha.
138 * @param image A Java Image object which uses the DirectColorModel
139 * @param encodeAlpha Encode the alpha channel? false=no; true=yes
140 * @see java.awt.Image
142 public PngEncoder(Image image, boolean encodeAlpha) {
143 this(image, encodeAlpha, FILTER_NONE, 0);
147 * Class constructor specifying Image to encode, whether to encode alpha, and filter to use.
149 * @param image A Java Image object which uses the DirectColorModel
150 * @param encodeAlpha Encode the alpha channel? false=no; true=yes
151 * @param whichFilter 0=none, 1=sub, 2=up
152 * @see java.awt.Image
154 public PngEncoder(Image image, boolean encodeAlpha, int whichFilter) {
155 this(image, encodeAlpha, whichFilter, 0);
160 * Class constructor specifying Image source to encode, whether to encode alpha, filter to use,
161 * and compression level.
163 * @param image A Java Image object
164 * @param encodeAlpha Encode the alpha channel? false=no; true=yes
165 * @param whichFilter 0=none, 1=sub, 2=up
166 * @param compLevel 0..9
167 * @see java.awt.Image
169 public PngEncoder(Image image, boolean encodeAlpha, int whichFilter, int compLevel) {
171 this.encodeAlpha = encodeAlpha;
172 setFilter(whichFilter);
173 if (compLevel >= 0 && compLevel <= 9) {
174 this.compressionLevel = compLevel;
179 * Set the image to be encoded
181 * @param image A Java Image object which uses the DirectColorModel
182 * @see java.awt.Image
183 * @see java.awt.image.DirectColorModel
185 public void setImage(Image image) {
191 * Creates an array of bytes that is the PNG equivalent of the current image, specifying
192 * whether to encode alpha or not.
194 * @param encodeAlpha boolean false=no alpha, true=encode alpha
195 * @return an array of bytes, or null if there was a problem
197 public byte[] pngEncode(boolean encodeAlpha) {
198 byte[] pngIdBytes = {-119, 80, 78, 71, 13, 10, 26, 10};
203 width = image.getWidth(null);
204 height = image.getHeight(null);
207 * start with an array that is big enough to hold all the pixels
208 * (plus filter bytes), and an extra 200 bytes for header info
210 pngBytes = new byte[((width + 1) * height * 3) + 200];
213 * keep track of largest byte written to the array
217 bytePos = writeBytes(pngIdBytes, 0);
221 if (writeImageData()) {
223 pngBytes = resizeByteArray(pngBytes, maxPos);
232 * Creates an array of bytes that is the PNG equivalent of the current image.
233 * Alpha encoding is determined by its setting in the constructor.
235 * @return an array of bytes, or null if there was a problem
237 public byte[] pngEncode() {
238 return pngEncode(encodeAlpha);
242 * Set the alpha encoding on or off.
244 * @param encodeAlpha false=no, true=yes
246 public void setEncodeAlpha(boolean encodeAlpha) {
247 this.encodeAlpha = encodeAlpha;
251 * Retrieve alpha encoding status.
253 * @return boolean false=no, true=yes
255 public boolean getEncodeAlpha() {
260 * Set the filter to use
262 * @param whichFilter from constant list
264 public void setFilter(int whichFilter) {
265 this.filter = FILTER_NONE;
266 if (whichFilter <= FILTER_LAST) {
267 this.filter = whichFilter;
272 * Retrieve filtering scheme
274 * @return int (see constant list)
276 public int getFilter() {
281 * Set the compression level to use
283 * @param level 0 through 9
285 public void setCompressionLevel(int level) {
286 if (level >= 0 && level <= 9) {
287 this.compressionLevel = level;
292 * Retrieve compression level
294 * @return int in range 0-9
296 public int getCompressionLevel() {
297 return compressionLevel;
301 * Increase or decrease the length of a byte array.
303 * @param array The original array.
304 * @param newLength The length you wish the new array to have.
305 * @return Array of newly desired length. If shorter than the
306 * original, the trailing elements are truncated.
308 protected byte[] resizeByteArray(byte[] array, int newLength) {
309 byte[] newArray = new byte[newLength];
310 int oldLength = array.length;
312 System.arraycopy(array, 0, newArray, 0, Math.min(oldLength, newLength));
317 * Write an array of bytes into the pngBytes array.
318 * Note: This routine has the side effect of updating
319 * maxPos, the largest element written in the array.
320 * The array is resized by 1000 bytes or the length
321 * of the data to be written, whichever is larger.
323 * @param data The data to be written into pngBytes.
324 * @param offset The starting point to write to.
325 * @return The next place to be written to in the pngBytes array.
327 protected int writeBytes(byte[] data, int offset) {
328 maxPos = Math.max(maxPos, offset + data.length);
329 if (data.length + offset > pngBytes.length) {
330 pngBytes = resizeByteArray(pngBytes, pngBytes.length + Math.max(1000, data.length));
332 System.arraycopy(data, 0, pngBytes, offset, data.length);
333 return offset + data.length;
337 * Write an array of bytes into the pngBytes array, specifying number of bytes to write.
338 * Note: This routine has the side effect of updating
339 * maxPos, the largest element written in the array.
340 * The array is resized by 1000 bytes or the length
341 * of the data to be written, whichever is larger.
343 * @param data The data to be written into pngBytes.
344 * @param nBytes The number of bytes to be written.
345 * @param offset The starting point to write to.
346 * @return The next place to be written to in the pngBytes array.
348 protected int writeBytes(byte[] data, int nBytes, int offset) {
349 maxPos = Math.max(maxPos, offset + nBytes);
350 if (nBytes + offset > pngBytes.length) {
351 pngBytes = resizeByteArray(pngBytes, pngBytes.length + Math.max(1000, nBytes));
353 System.arraycopy(data, 0, pngBytes, offset, nBytes);
354 return offset + nBytes;
358 * Write a two-byte integer into the pngBytes array at a given position.
360 * @param n The integer to be written into pngBytes.
361 * @param offset The starting point to write to.
362 * @return The next place to be written to in the pngBytes array.
364 protected int writeInt2(int n, int offset) {
365 byte[] temp = {(byte) ((n >> 8) & 0xff), (byte) (n & 0xff)};
366 return writeBytes(temp, offset);
370 * Write a four-byte integer into the pngBytes array at a given position.
372 * @param n The integer to be written into pngBytes.
373 * @param offset The starting point to write to.
374 * @return The next place to be written to in the pngBytes array.
376 protected int writeInt4(int n, int offset) {
377 byte[] temp = {(byte) ((n >> 24) & 0xff),
378 (byte) ((n >> 16) & 0xff),
379 (byte) ((n >> 8) & 0xff),
381 return writeBytes(temp, offset);
385 * Write a single byte into the pngBytes array at a given position.
387 * @param b The integer to be written into pngBytes.
388 * @param offset The starting point to write to.
389 * @return The next place to be written to in the pngBytes array.
391 protected int writeByte(int b, int offset) {
392 byte[] temp = {(byte) b};
393 return writeBytes(temp, offset);
397 * Write a PNG "IHDR" chunk into the pngBytes array.
399 protected void writeHeader() {
402 startPos = bytePos = writeInt4(13, bytePos);
403 bytePos = writeBytes(IHDR, bytePos);
404 width = image.getWidth(null);
405 height = image.getHeight(null);
406 bytePos = writeInt4(width, bytePos);
407 bytePos = writeInt4(height, bytePos);
408 bytePos = writeByte(8, bytePos); // bit depth
409 bytePos = writeByte((encodeAlpha) ? 6 : 2, bytePos); // direct model
410 bytePos = writeByte(0, bytePos); // compression method
411 bytePos = writeByte(0, bytePos); // filter method
412 bytePos = writeByte(0, bytePos); // no interlace
414 crc.update(pngBytes, startPos, bytePos - startPos);
415 crcValue = crc.getValue();
416 bytePos = writeInt4((int) crcValue, bytePos);
420 * Perform "sub" filtering on the given row.
421 * Uses temporary array leftBytes to store the original values
422 * of the previous pixels. The array is 16 bytes long, which
423 * will easily hold two-byte samples plus two-byte alpha.
425 * @param pixels The array holding the scan lines being built
426 * @param startPos Starting position within pixels of bytes to be filtered.
427 * @param width Width of a scanline in pixels.
429 protected void filterSub(byte[] pixels, int startPos, int width) {
431 int offset = bytesPerPixel;
432 int actualStart = startPos + offset;
433 int nBytes = width * bytesPerPixel;
434 int leftInsert = offset;
437 for (i = actualStart; i < startPos + nBytes; i++) {
438 leftBytes[leftInsert] = pixels[i];
439 pixels[i] = (byte) ((pixels[i] - leftBytes[leftExtract]) % 256);
440 leftInsert = (leftInsert + 1) % 0x0f;
441 leftExtract = (leftExtract + 1) % 0x0f;
446 * Perform "up" filtering on the given row.
447 * Side effect: refills the prior row with current row
449 * @param pixels The array holding the scan lines being built
450 * @param startPos Starting position within pixels of bytes to be filtered.
451 * @param width Width of a scanline in pixels.
453 protected void filterUp(byte[] pixels, int startPos, int width) {
457 nBytes = width * bytesPerPixel;
459 for (i = 0; i < nBytes; i++) {
460 currentByte = pixels[startPos + i];
461 pixels[startPos + i] = (byte) ((pixels[startPos + i] - priorRow[i]) % 256);
462 priorRow[i] = currentByte;
467 * Write the image data into the pngBytes array.
468 * This will write one or more PNG "IDAT" chunks. In order
469 * to conserve memory, this method grabs as many rows as will
470 * fit into 32K bytes, or the whole image; whichever is less.
473 * @return true if no errors; false if error grabbing pixels
475 protected boolean writeImageData() {
476 int rowsLeft = height; // number of rows remaining to write
477 int startRow = 0; // starting row to process this time through
478 int nRows; // how many rows to grab at a time
480 byte[] scanLines; // the scan lines to be compressed
481 int scanPos; // where we are in the scan lines
482 int startPos; // where this line's actual pixels start (used for filtering)
484 byte[] compressedLines; // the resultant compressed lines
485 int nCompressed; // how big is the compressed area?
487 //int depth; // color depth ( handle only 8 or 32 )
491 bytesPerPixel = (encodeAlpha) ? 4 : 3;
493 Deflater scrunch = new Deflater(compressionLevel);
494 ByteArrayOutputStream outBytes = new ByteArrayOutputStream(1024);
496 DeflaterOutputStream compBytes = new DeflaterOutputStream(outBytes, scrunch);
498 while (rowsLeft > 0) {
499 nRows = Math.min(32767 / (width * (bytesPerPixel + 1)), rowsLeft);
500 nRows = Math.max( nRows, 1 );
502 int[] pixels = new int[width * nRows];
504 pg = new PixelGrabber(image, 0, startRow,
505 width, nRows, pixels, 0, width);
509 catch (Exception e) {
510 System.err.println("interrupted waiting for pixels!");
513 if ((pg.getStatus() & ImageObserver.ABORT) != 0) {
514 System.err.println("image fetch aborted or errored");
519 * Create a data chunk. scanLines adds "nRows" for
522 scanLines = new byte[width * nRows * bytesPerPixel + nRows];
524 if (filter == FILTER_SUB) {
525 leftBytes = new byte[16];
527 if (filter == FILTER_UP) {
528 priorRow = new byte[width * bytesPerPixel];
533 for (int i = 0; i < width * nRows; i++) {
534 if (i % width == 0) {
535 scanLines[scanPos++] = (byte) filter;
538 scanLines[scanPos++] = (byte) ((pixels[i] >> 16) & 0xff);
539 scanLines[scanPos++] = (byte) ((pixels[i] >> 8) & 0xff);
540 scanLines[scanPos++] = (byte) ((pixels[i]) & 0xff);
542 scanLines[scanPos++] = (byte) ((pixels[i] >> 24) & 0xff);
544 if ((i % width == width - 1) && (filter != FILTER_NONE)) {
545 if (filter == FILTER_SUB) {
546 filterSub(scanLines, startPos, width);
548 if (filter == FILTER_UP) {
549 filterUp(scanLines, startPos, width);
555 * Write these lines to the output area
557 compBytes.write(scanLines, 0, scanPos);
565 * Write the compressed bytes
567 compressedLines = outBytes.toByteArray();
568 nCompressed = compressedLines.length;
571 bytePos = writeInt4(nCompressed, bytePos);
572 bytePos = writeBytes(IDAT, bytePos);
574 bytePos = writeBytes(compressedLines, nCompressed, bytePos);
575 crc.update(compressedLines, 0, nCompressed);
577 crcValue = crc.getValue();
578 bytePos = writeInt4((int) crcValue, bytePos);
582 catch (IOException e) {
583 System.err.println(e.toString());
589 * Write a PNG "IEND" chunk into the pngBytes array.
591 protected void writeEnd() {
592 bytePos = writeInt4(0, bytePos);
593 bytePos = writeBytes(IEND, bytePos);
596 crcValue = crc.getValue();
597 bytePos = writeInt4((int) crcValue, bytePos);