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 */
053
054 public class PngEncoder {
055
056 /** Constant specifying that alpha channel should be encoded. */
057 public static final boolean ENCODE_ALPHA = true;
058
059 /** Constant specifying that alpha channel should not be encoded. */
060 public static final boolean NO_ALPHA = false;
061
062 /** Constants for filter (NONE). */
063 public static final int FILTER_NONE = 0;
064
065 /** Constants for filter (SUB). */
066 public static final int FILTER_SUB = 1;
067
068 /** Constants for filter (UP). */
069 public static final int FILTER_UP = 2;
070
071 /** Constants for filter (LAST). */
072 public static final int FILTER_LAST = 2;
073
074 /** IHDR tag. */
075 protected static final byte[] IHDR = {73, 72, 68, 82};
076
077 /** IDAT tag. */
078 protected static final byte[] IDAT = {73, 68, 65, 84};
079
080 /** IEND tag. */
081 protected static final byte[] IEND = {73, 69, 78, 68};
082
083 protected static final byte[] PHYS = {(byte)'p', (byte)'H', (byte)'Y', (byte)'s'};
084
085 /** The png bytes. */
086 protected byte[] pngBytes;
087
088 /** The prior row. */
089 protected byte[] priorRow;
090
091 /** The left bytes. */
092 protected byte[] leftBytes;
093
094 /** The image. */
095 protected Image image;
096
097 /** The width. */
098 protected int width;
099
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 crc = new CRC32();
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 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 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 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 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 image) {
212 this.image = image;
213 this.pngBytes = null;
214 }
215
216 /**
217 * Returns the image to be encoded.
218 */
219 public Image 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 pg;
527
528 this.bytesPerPixel = (this.encodeAlpha) ? 4 : 3;
529
530 Deflater scrunch = new Deflater(this.compressionLevel);
531 ByteArrayOutputStream outBytes = new ByteArrayOutputStream(1024);
532
533 DeflaterOutputStream compBytes = new DeflaterOutputStream(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(this.image, 0, startRow,
544 this.width, nRows, pixels, 0, this.width);
545 try {
546 pg.grabPixels();
547 }
548 catch (Exception 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 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 }