001 package com.keypoint;
002
003 import java.awt.Image;
004 import java.awt.image.ImageObserver;
005 import java.awt.image.PixelGrabber;
006 import java.io.ByteArrayOutputStream;
007 import java.io.IOException;
008 import java.util.zip.CRC32;
009 import java.util.zip.Deflater;
010 import java.util.zip.DeflaterOutputStream;
011
012 /**
013 * PngEncoder takes a Java Image object and creates a byte string which can be
014 * saved as a PNG file. The Image is presumed to use the DirectColorModel.
015 *
016 * <p>Thanks to Jay Denny at KeyPoint Software
017 * http://www.keypoint.com/
018 * who let me develop this code on company time.</p>
019 *
020 * <p>You may contact me with (probably very-much-needed) improvements,
021 * comments, and bug fixes at:</p>
022 *
023 * <p><code>david@catcode.com</code></p>
024 *
025 * <p>This library is free software; you can redistribute it and/or
026 * modify it under the terms of the GNU Lesser General Public
027 * License as published by the Free Software Foundation; either
028 * version 2.1 of the License, or (at your option) any later version.</p>
029 *
030 * <p>This library is distributed in the hope that it will be useful,
031 * but WITHOUT ANY WARRANTY; without even the implied warranty of
032 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
033 * Lesser General Public License for more details.</p>
034 *
035 * <p>You should have received a copy of the GNU Lesser General Public
036 * License along with this library; if not, write to the Free Software
037 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
038 * USA. A copy of the GNU LGPL may be found at
039 * <code>http://www.gnu.org/copyleft/lesser.html</code></p>
040 *
041 * @author J. David Eisenberg
042 * @version 1.5, 19 Oct 2003
043 *
044 * CHANGES:
045 * --------
046 * 19-Nov-2002 : CODING STYLE CHANGES ONLY (by David Gilbert for Object
047 * Refinery Limited);
048 * 19-Sep-2003 : Fix for platforms using EBCDIC (contributed by Paulo Soares);
049 * 19-Oct-2003 : Change private fields to protected fields so that
050 * PngEncoderB can inherit them (JDE)
051 * Fixed bug with calculation of nRows
052 * 15-Aug-2008 : Added scrunch.end() in writeImageData() method - see
053 * JFreeChart bug report 2037930 (David Gilbert);
054 */
055
056 public class PngEncoder {
057
058 /** Constant specifying that alpha channel should be encoded. */
059 public static final boolean ENCODE_ALPHA = true;
060
061 /** Constant specifying that alpha channel should not be encoded. */
062 public static final boolean NO_ALPHA = false;
063
064 /** Constants for filter (NONE). */
065 public static final int FILTER_NONE = 0;
066
067 /** Constants for filter (SUB). */
068 public static final int FILTER_SUB = 1;
069
070 /** Constants for filter (UP). */
071 public static final int FILTER_UP = 2;
072
073 /** Constants for filter (LAST). */
074 public static final int FILTER_LAST = 2;
075
076 /** IHDR tag. */
077 protected static final byte[] IHDR = {73, 72, 68, 82};
078
079 /** IDAT tag. */
080 protected static final byte[] IDAT = {73, 68, 65, 84};
081
082 /** IEND tag. */
083 protected static final byte[] IEND = {73, 69, 78, 68};
084
085 /** PHYS tag. */
086 protected static final byte[] PHYS = {(byte)'p', (byte)'H', (byte)'Y',
087 (byte)'s'};
088
089 /** The png bytes. */
090 protected byte[] pngBytes;
091
092 /** The prior row. */
093 protected byte[] priorRow;
094
095 /** The left bytes. */
096 protected byte[] leftBytes;
097
098 /** The image. */
099 protected Image image;
100
101 /** The width. */
102 protected int width;
103
104 /** The height. */
105 protected int height;
106
107 /** The byte position. */
108 protected int bytePos;
109
110 /** The maximum position. */
111 protected int maxPos;
112
113 /** CRC. */
114 protected CRC32 crc = new CRC32();
115
116 /** The CRC value. */
117 protected long crcValue;
118
119 /** Encode alpha? */
120 protected boolean encodeAlpha;
121
122 /** The filter type. */
123 protected int filter;
124
125 /** The bytes-per-pixel. */
126 protected int bytesPerPixel;
127
128 /** The physical pixel dimension : number of pixels per inch on the X axis. */
129 private int xDpi = 0;
130
131 /** The physical pixel dimension : number of pixels per inch on the Y axis. */
132 private int yDpi = 0;
133
134 /** Used for conversion of DPI to Pixels per Meter. */
135 static private float INCH_IN_METER_UNIT = 0.0254f;
136
137 /**
138 * The compression level (1 = best speed, 9 = best compression,
139 * 0 = no compression).
140 */
141 protected int compressionLevel;
142
143 /**
144 * Class constructor.
145 */
146 public PngEncoder() {
147 this(null, false, FILTER_NONE, 0);
148 }
149
150 /**
151 * Class constructor specifying Image to encode, with no alpha channel
152 * encoding.
153 *
154 * @param image A Java Image object which uses the DirectColorModel
155 * @see java.awt.Image
156 */
157 public PngEncoder(Image image) {
158 this(image, false, FILTER_NONE, 0);
159 }
160
161 /**
162 * Class constructor specifying Image to encode, and whether to encode
163 * alpha.
164 *
165 * @param image A Java Image object which uses the DirectColorModel
166 * @param encodeAlpha Encode the alpha channel? false=no; true=yes
167 * @see java.awt.Image
168 */
169 public PngEncoder(Image image, boolean encodeAlpha) {
170 this(image, encodeAlpha, FILTER_NONE, 0);
171 }
172
173 /**
174 * Class constructor specifying Image to encode, whether to encode alpha,
175 * and filter to use.
176 *
177 * @param image A Java Image object which uses the DirectColorModel
178 * @param encodeAlpha Encode the alpha channel? false=no; true=yes
179 * @param whichFilter 0=none, 1=sub, 2=up
180 * @see java.awt.Image
181 */
182 public PngEncoder(Image image, boolean encodeAlpha, int whichFilter) {
183 this(image, encodeAlpha, whichFilter, 0);
184 }
185
186
187 /**
188 * Class constructor specifying Image source to encode, whether to encode
189 * alpha, filter to use, and compression level.
190 *
191 * @param image A Java Image object
192 * @param encodeAlpha Encode the alpha channel? false=no; true=yes
193 * @param whichFilter 0=none, 1=sub, 2=up
194 * @param compLevel 0..9 (1 = best speed, 9 = best compression, 0 = no
195 * compression)
196 * @see java.awt.Image
197 */
198 public PngEncoder(Image image, boolean encodeAlpha, int whichFilter,
199 int compLevel) {
200 this.image = image;
201 this.encodeAlpha = encodeAlpha;
202 setFilter(whichFilter);
203 if (compLevel >= 0 && compLevel <= 9) {
204 this.compressionLevel = compLevel;
205 }
206 }
207
208 /**
209 * Set the image to be encoded.
210 *
211 * @param image A Java Image object which uses the DirectColorModel
212 * @see java.awt.Image
213 * @see java.awt.image.DirectColorModel
214 */
215 public void setImage(Image image) {
216 this.image = image;
217 this.pngBytes = null;
218 }
219
220 /**
221 * Returns the image to be encoded.
222 *
223 * @return The image.
224 */
225 public Image getImage() {
226 return this.image;
227 }
228
229 /**
230 * Creates an array of bytes that is the PNG equivalent of the current
231 * image, specifying whether to encode alpha or not.
232 *
233 * @param encodeAlpha boolean false=no alpha, true=encode alpha
234 * @return an array of bytes, or null if there was a problem
235 */
236 public byte[] pngEncode(boolean encodeAlpha) {
237 byte[] pngIdBytes = {-119, 80, 78, 71, 13, 10, 26, 10};
238
239 if (this.image == null) {
240 return null;
241 }
242 this.width = this.image.getWidth(null);
243 this.height = this.image.getHeight(null);
244
245 /*
246 * start with an array that is big enough to hold all the pixels
247 * (plus filter bytes), and an extra 200 bytes for header info
248 */
249 this.pngBytes = new byte[((this.width + 1) * this.height * 3) + 200];
250
251 /*
252 * keep track of largest byte written to the array
253 */
254 this.maxPos = 0;
255
256 this.bytePos = writeBytes(pngIdBytes, 0);
257 //hdrPos = bytePos;
258 writeHeader();
259 writeResolution();
260 //dataPos = bytePos;
261 if (writeImageData()) {
262 writeEnd();
263 this.pngBytes = resizeByteArray(this.pngBytes, this.maxPos);
264 }
265 else {
266 this.pngBytes = null;
267 }
268 return this.pngBytes;
269 }
270
271 /**
272 * Creates an array of bytes that is the PNG equivalent of the current
273 * image. Alpha encoding is determined by its setting in the constructor.
274 *
275 * @return an array of bytes, or null if there was a problem
276 */
277 public byte[] pngEncode() {
278 return pngEncode(this.encodeAlpha);
279 }
280
281 /**
282 * Set the alpha encoding on or off.
283 *
284 * @param encodeAlpha false=no, true=yes
285 */
286 public void setEncodeAlpha(boolean encodeAlpha) {
287 this.encodeAlpha = encodeAlpha;
288 }
289
290 /**
291 * Retrieve alpha encoding status.
292 *
293 * @return boolean false=no, true=yes
294 */
295 public boolean getEncodeAlpha() {
296 return this.encodeAlpha;
297 }
298
299 /**
300 * Set the filter to use.
301 *
302 * @param whichFilter from constant list
303 */
304 public void setFilter(int whichFilter) {
305 this.filter = FILTER_NONE;
306 if (whichFilter <= FILTER_LAST) {
307 this.filter = whichFilter;
308 }
309 }
310
311 /**
312 * Retrieve filtering scheme.
313 *
314 * @return int (see constant list)
315 */
316 public int getFilter() {
317 return this.filter;
318 }
319
320 /**
321 * Set the compression level to use.
322 *
323 * @param level the compression level (1 = best speed, 9 = best compression,
324 * 0 = no compression)
325 */
326 public void setCompressionLevel(int level) {
327 if (level >= 0 && level <= 9) {
328 this.compressionLevel = level;
329 }
330 }
331
332 /**
333 * Retrieve compression level.
334 *
335 * @return int (1 = best speed, 9 = best compression, 0 = no compression)
336 */
337 public int getCompressionLevel() {
338 return this.compressionLevel;
339 }
340
341 /**
342 * Increase or decrease the length of a byte array.
343 *
344 * @param array The original array.
345 * @param newLength The length you wish the new array to have.
346 * @return Array of newly desired length. If shorter than the
347 * original, the trailing elements are truncated.
348 */
349 protected byte[] resizeByteArray(byte[] array, int newLength) {
350 byte[] newArray = new byte[newLength];
351 int oldLength = array.length;
352
353 System.arraycopy(array, 0, newArray, 0, Math.min(oldLength, newLength));
354 return newArray;
355 }
356
357 /**
358 * Write an array of bytes into the pngBytes array.
359 * Note: This routine has the side effect of updating
360 * maxPos, the largest element written in the array.
361 * The array is resized by 1000 bytes or the length
362 * of the data to be written, whichever is larger.
363 *
364 * @param data The data to be written into pngBytes.
365 * @param offset The starting point to write to.
366 * @return The next place to be written to in the pngBytes array.
367 */
368 protected int writeBytes(byte[] data, int offset) {
369 this.maxPos = Math.max(this.maxPos, offset + data.length);
370 if (data.length + offset > this.pngBytes.length) {
371 this.pngBytes = resizeByteArray(this.pngBytes, this.pngBytes.length
372 + Math.max(1000, data.length));
373 }
374 System.arraycopy(data, 0, this.pngBytes, offset, data.length);
375 return offset + data.length;
376 }
377
378 /**
379 * Write an array of bytes into the pngBytes array, specifying number of
380 * bytes to write. Note: This routine has the side effect of updating
381 * maxPos, the largest element written in the array.
382 * The array is resized by 1000 bytes or the length
383 * of the data to be written, whichever is larger.
384 *
385 * @param data The data to be written into pngBytes.
386 * @param nBytes The number of bytes to be written.
387 * @param offset The starting point to write to.
388 * @return The next place to be written to in the pngBytes array.
389 */
390 protected int writeBytes(byte[] data, int nBytes, int offset) {
391 this.maxPos = Math.max(this.maxPos, offset + nBytes);
392 if (nBytes + offset > this.pngBytes.length) {
393 this.pngBytes = resizeByteArray(this.pngBytes, this.pngBytes.length
394 + Math.max(1000, nBytes));
395 }
396 System.arraycopy(data, 0, this.pngBytes, offset, nBytes);
397 return offset + nBytes;
398 }
399
400 /**
401 * Write a two-byte integer into the pngBytes array at a given position.
402 *
403 * @param n The integer to be written into pngBytes.
404 * @param offset The starting point to write to.
405 * @return The next place to be written to in the pngBytes array.
406 */
407 protected int writeInt2(int n, int offset) {
408 byte[] temp = {(byte) ((n >> 8) & 0xff), (byte) (n & 0xff)};
409 return writeBytes(temp, offset);
410 }
411
412 /**
413 * Write a four-byte integer into the pngBytes array at a given position.
414 *
415 * @param n The integer to be written into pngBytes.
416 * @param offset The starting point to write to.
417 * @return The next place to be written to in the pngBytes array.
418 */
419 protected int writeInt4(int n, int offset) {
420 byte[] temp = {(byte) ((n >> 24) & 0xff),
421 (byte) ((n >> 16) & 0xff),
422 (byte) ((n >> 8) & 0xff),
423 (byte) (n & 0xff)};
424 return writeBytes(temp, offset);
425 }
426
427 /**
428 * Write a single byte into the pngBytes array at a given position.
429 *
430 * @param b The integer to be written into pngBytes.
431 * @param offset The starting point to write to.
432 * @return The next place to be written to in the pngBytes array.
433 */
434 protected int writeByte(int b, int offset) {
435 byte[] temp = {(byte) b};
436 return writeBytes(temp, offset);
437 }
438
439 /**
440 * Write a PNG "IHDR" chunk into the pngBytes array.
441 */
442 protected void writeHeader() {
443
444 int startPos = this.bytePos = writeInt4(13, this.bytePos);
445 this.bytePos = writeBytes(IHDR, this.bytePos);
446 this.width = this.image.getWidth(null);
447 this.height = this.image.getHeight(null);
448 this.bytePos = writeInt4(this.width, this.bytePos);
449 this.bytePos = writeInt4(this.height, this.bytePos);
450 this.bytePos = writeByte(8, this.bytePos); // bit depth
451 this.bytePos = writeByte((this.encodeAlpha) ? 6 : 2, this.bytePos);
452 // direct model
453 this.bytePos = writeByte(0, this.bytePos); // compression method
454 this.bytePos = writeByte(0, this.bytePos); // filter method
455 this.bytePos = writeByte(0, this.bytePos); // no interlace
456 this.crc.reset();
457 this.crc.update(this.pngBytes, startPos, this.bytePos - startPos);
458 this.crcValue = this.crc.getValue();
459 this.bytePos = writeInt4((int) this.crcValue, this.bytePos);
460 }
461
462 /**
463 * Perform "sub" filtering on the given row.
464 * Uses temporary array leftBytes to store the original values
465 * of the previous pixels. The array is 16 bytes long, which
466 * will easily hold two-byte samples plus two-byte alpha.
467 *
468 * @param pixels The array holding the scan lines being built
469 * @param startPos Starting position within pixels of bytes to be filtered.
470 * @param width Width of a scanline in pixels.
471 */
472 protected void filterSub(byte[] pixels, int startPos, int width) {
473 int offset = this.bytesPerPixel;
474 int actualStart = startPos + offset;
475 int nBytes = width * this.bytesPerPixel;
476 int leftInsert = offset;
477 int leftExtract = 0;
478
479 for (int i = actualStart; i < startPos + nBytes; i++) {
480 this.leftBytes[leftInsert] = pixels[i];
481 pixels[i] = (byte) ((pixels[i] - this.leftBytes[leftExtract])
482 % 256);
483 leftInsert = (leftInsert + 1) % 0x0f;
484 leftExtract = (leftExtract + 1) % 0x0f;
485 }
486 }
487
488 /**
489 * Perform "up" filtering on the given row.
490 * Side effect: refills the prior row with current row
491 *
492 * @param pixels The array holding the scan lines being built
493 * @param startPos Starting position within pixels of bytes to be filtered.
494 * @param width Width of a scanline in pixels.
495 */
496 protected void filterUp(byte[] pixels, int startPos, int width) {
497
498 final int nBytes = width * this.bytesPerPixel;
499
500 for (int i = 0; i < nBytes; i++) {
501 final byte currentByte = pixels[startPos + i];
502 pixels[startPos + i] = (byte) ((pixels[startPos + i]
503 - this.priorRow[i]) % 256);
504 this.priorRow[i] = currentByte;
505 }
506 }
507
508 /**
509 * Write the image data into the pngBytes array.
510 * This will write one or more PNG "IDAT" chunks. In order
511 * to conserve memory, this method grabs as many rows as will
512 * fit into 32K bytes, or the whole image; whichever is less.
513 *
514 *
515 * @return true if no errors; false if error grabbing pixels
516 */
517 protected boolean writeImageData() {
518 int rowsLeft = this.height; // number of rows remaining to write
519 int startRow = 0; // starting row to process this time through
520 int nRows; // how many rows to grab at a time
521
522 byte[] scanLines; // the scan lines to be compressed
523 int scanPos; // where we are in the scan lines
524 int startPos; // where this line's actual pixels start (used
525 // for filtering)
526
527 byte[] compressedLines; // the resultant compressed lines
528 int nCompressed; // how big is the compressed area?
529
530 //int depth; // color depth ( handle only 8 or 32 )
531
532 PixelGrabber pg;
533
534 this.bytesPerPixel = (this.encodeAlpha) ? 4 : 3;
535
536 Deflater scrunch = new Deflater(this.compressionLevel);
537 ByteArrayOutputStream outBytes = new ByteArrayOutputStream(1024);
538
539 DeflaterOutputStream compBytes = new DeflaterOutputStream(outBytes,
540 scrunch);
541 try {
542 while (rowsLeft > 0) {
543 nRows = Math.min(32767 / (this.width
544 * (this.bytesPerPixel + 1)), rowsLeft);
545 nRows = Math.max(nRows, 1);
546
547 int[] pixels = new int[this.width * nRows];
548
549 pg = new PixelGrabber(this.image, 0, startRow,
550 this.width, nRows, pixels, 0, this.width);
551 try {
552 pg.grabPixels();
553 }
554 catch (Exception e) {
555 System.err.println("interrupted waiting for pixels!");
556 return false;
557 }
558 if ((pg.getStatus() & ImageObserver.ABORT) != 0) {
559 System.err.println("image fetch aborted or errored");
560 return false;
561 }
562
563 /*
564 * Create a data chunk. scanLines adds "nRows" for
565 * the filter bytes.
566 */
567 scanLines = new byte[this.width * nRows * this.bytesPerPixel
568 + nRows];
569
570 if (this.filter == FILTER_SUB) {
571 this.leftBytes = new byte[16];
572 }
573 if (this.filter == FILTER_UP) {
574 this.priorRow = new byte[this.width * this.bytesPerPixel];
575 }
576
577 scanPos = 0;
578 startPos = 1;
579 for (int i = 0; i < this.width * nRows; i++) {
580 if (i % this.width == 0) {
581 scanLines[scanPos++] = (byte) this.filter;
582 startPos = scanPos;
583 }
584 scanLines[scanPos++] = (byte) ((pixels[i] >> 16) & 0xff);
585 scanLines[scanPos++] = (byte) ((pixels[i] >> 8) & 0xff);
586 scanLines[scanPos++] = (byte) ((pixels[i]) & 0xff);
587 if (this.encodeAlpha) {
588 scanLines[scanPos++] = (byte) ((pixels[i] >> 24)
589 & 0xff);
590 }
591 if ((i % this.width == this.width - 1)
592 && (this.filter != FILTER_NONE)) {
593 if (this.filter == FILTER_SUB) {
594 filterSub(scanLines, startPos, this.width);
595 }
596 if (this.filter == FILTER_UP) {
597 filterUp(scanLines, startPos, this.width);
598 }
599 }
600 }
601
602 /*
603 * Write these lines to the output area
604 */
605 compBytes.write(scanLines, 0, scanPos);
606
607 startRow += nRows;
608 rowsLeft -= nRows;
609 }
610 compBytes.close();
611
612 /*
613 * Write the compressed bytes
614 */
615 compressedLines = outBytes.toByteArray();
616 nCompressed = compressedLines.length;
617
618 this.crc.reset();
619 this.bytePos = writeInt4(nCompressed, this.bytePos);
620 this.bytePos = writeBytes(IDAT, this.bytePos);
621 this.crc.update(IDAT);
622 this.bytePos = writeBytes(compressedLines, nCompressed,
623 this.bytePos);
624 this.crc.update(compressedLines, 0, nCompressed);
625
626 this.crcValue = this.crc.getValue();
627 this.bytePos = writeInt4((int) this.crcValue, this.bytePos);
628 scrunch.finish();
629 scrunch.end();
630 return true;
631 }
632 catch (IOException e) {
633 System.err.println(e.toString());
634 return false;
635 }
636 }
637
638 /**
639 * Write a PNG "IEND" chunk into the pngBytes array.
640 */
641 protected void writeEnd() {
642 this.bytePos = writeInt4(0, this.bytePos);
643 this.bytePos = writeBytes(IEND, this.bytePos);
644 this.crc.reset();
645 this.crc.update(IEND);
646 this.crcValue = this.crc.getValue();
647 this.bytePos = writeInt4((int) this.crcValue, this.bytePos);
648 }
649
650
651 /**
652 * Set the DPI for the X axis.
653 *
654 * @param xDpi The number of dots per inch
655 */
656 public void setXDpi(int xDpi) {
657 this.xDpi = Math.round(xDpi / INCH_IN_METER_UNIT);
658
659 }
660
661 /**
662 * Get the DPI for the X axis.
663 *
664 * @return The number of dots per inch
665 */
666 public int getXDpi() {
667 return Math.round(this.xDpi * INCH_IN_METER_UNIT);
668 }
669
670 /**
671 * Set the DPI for the Y axis.
672 *
673 * @param yDpi The number of dots per inch
674 */
675 public void setYDpi(int yDpi) {
676 this.yDpi = Math.round(yDpi / INCH_IN_METER_UNIT);
677 }
678
679 /**
680 * Get the DPI for the Y axis.
681 *
682 * @return The number of dots per inch
683 */
684 public int getYDpi() {
685 return Math.round(this.yDpi * INCH_IN_METER_UNIT);
686 }
687
688 /**
689 * Set the DPI resolution.
690 *
691 * @param xDpi The number of dots per inch for the X axis.
692 * @param yDpi The number of dots per inch for the Y axis.
693 */
694 public void setDpi(int xDpi, int yDpi) {
695 this.xDpi = Math.round(xDpi / INCH_IN_METER_UNIT);
696 this.yDpi = Math.round(yDpi / INCH_IN_METER_UNIT);
697 }
698
699 /**
700 * Write a PNG "pHYs" chunk into the pngBytes array.
701 */
702 protected void writeResolution() {
703 if (this.xDpi > 0 && this.yDpi > 0) {
704
705 final int startPos = this.bytePos = writeInt4(9, this.bytePos);
706 this.bytePos = writeBytes(PHYS, this.bytePos);
707 this.bytePos = writeInt4(this.xDpi, this.bytePos);
708 this.bytePos = writeInt4(this.yDpi, this.bytePos);
709 this.bytePos = writeByte(1, this.bytePos); // unit is the meter.
710
711 this.crc.reset();
712 this.crc.update(this.pngBytes, startPos, this.bytePos - startPos);
713 this.crcValue = this.crc.getValue();
714 this.bytePos = writeInt4((int) this.crcValue, this.bytePos);
715 }
716 }
717 }