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',
084: (byte) 'Y', (byte) 's' };
085:
086: /** The png bytes. */
087: protected byte[] pngBytes;
088:
089: /** The prior row. */
090: protected byte[] priorRow;
091:
092: /** The left bytes. */
093: protected byte[] leftBytes;
094:
095: /** The image. */
096: protected Image image;
097:
098: /** The width. */
099: protected int width;
100:
101: /** The height. */
102: protected int height;
103:
104: /** The byte position. */
105: protected int bytePos;
106:
107: /** The maximum position. */
108: protected int maxPos;
109:
110: /** CRC. */
111: protected CRC32 crc = new CRC32();
112:
113: /** The CRC value. */
114: protected long crcValue;
115:
116: /** Encode alpha? */
117: protected boolean encodeAlpha;
118:
119: /** The filter type. */
120: protected int filter;
121:
122: /** The bytes-per-pixel. */
123: protected int bytesPerPixel;
124:
125: /** The physical pixel dimension : number of pixels per inch on the X axis. */
126: private int xDpi = 0;
127:
128: /** The physical pixel dimension : number of pixels per inch on the Y axis. */
129: private int yDpi = 0;
130:
131: /** Used for conversion of DPI to Pixels per Meter. */
132: static private float INCH_IN_METER_UNIT = 0.0254f;
133:
134: /**
135: * The compression level (1 = best speed, 9 = best compression,
136: * 0 = no compression).
137: */
138: protected int compressionLevel;
139:
140: /**
141: * Class constructor.
142: */
143: public PngEncoder() {
144: this (null, false, FILTER_NONE, 0);
145: }
146:
147: /**
148: * Class constructor specifying Image to encode, with no alpha channel
149: * encoding.
150: *
151: * @param image A Java Image object which uses the DirectColorModel
152: * @see java.awt.Image
153: */
154: public PngEncoder(Image image) {
155: this (image, false, FILTER_NONE, 0);
156: }
157:
158: /**
159: * Class constructor specifying Image to encode, and whether to encode
160: * alpha.
161: *
162: * @param image A Java Image object which uses the DirectColorModel
163: * @param encodeAlpha Encode the alpha channel? false=no; true=yes
164: * @see java.awt.Image
165: */
166: public PngEncoder(Image image, boolean encodeAlpha) {
167: this (image, encodeAlpha, FILTER_NONE, 0);
168: }
169:
170: /**
171: * Class constructor specifying Image to encode, whether to encode alpha,
172: * and filter to use.
173: *
174: * @param image A Java Image object which uses the DirectColorModel
175: * @param encodeAlpha Encode the alpha channel? false=no; true=yes
176: * @param whichFilter 0=none, 1=sub, 2=up
177: * @see java.awt.Image
178: */
179: public PngEncoder(Image image, boolean encodeAlpha, int whichFilter) {
180: this (image, encodeAlpha, whichFilter, 0);
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,
195: int whichFilter, 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: } else {
259: this .pngBytes = null;
260: }
261: return this .pngBytes;
262: }
263:
264: /**
265: * Creates an array of bytes that is the PNG equivalent of the current
266: * image. Alpha encoding is determined by its setting in the constructor.
267: *
268: * @return an array of bytes, or null if there was a problem
269: */
270: public byte[] pngEncode() {
271: return pngEncode(this .encodeAlpha);
272: }
273:
274: /**
275: * Set the alpha encoding on or off.
276: *
277: * @param encodeAlpha false=no, true=yes
278: */
279: public void setEncodeAlpha(boolean encodeAlpha) {
280: this .encodeAlpha = encodeAlpha;
281: }
282:
283: /**
284: * Retrieve alpha encoding status.
285: *
286: * @return boolean false=no, true=yes
287: */
288: public boolean getEncodeAlpha() {
289: return this .encodeAlpha;
290: }
291:
292: /**
293: * Set the filter to use.
294: *
295: * @param whichFilter from constant list
296: */
297: public void setFilter(int whichFilter) {
298: this .filter = FILTER_NONE;
299: if (whichFilter <= FILTER_LAST) {
300: this .filter = whichFilter;
301: }
302: }
303:
304: /**
305: * Retrieve filtering scheme.
306: *
307: * @return int (see constant list)
308: */
309: public int getFilter() {
310: return this .filter;
311: }
312:
313: /**
314: * Set the compression level to use.
315: *
316: * @param level the compression level (1 = best speed, 9 = best compression,
317: * 0 = no compression)
318: */
319: public void setCompressionLevel(int level) {
320: if (level >= 0 && level <= 9) {
321: this .compressionLevel = level;
322: }
323: }
324:
325: /**
326: * Retrieve compression level.
327: *
328: * @return int (1 = best speed, 9 = best compression, 0 = no compression)
329: */
330: public int getCompressionLevel() {
331: return this .compressionLevel;
332: }
333:
334: /**
335: * Increase or decrease the length of a byte array.
336: *
337: * @param array The original array.
338: * @param newLength The length you wish the new array to have.
339: * @return Array of newly desired length. If shorter than the
340: * original, the trailing elements are truncated.
341: */
342: protected byte[] resizeByteArray(byte[] array, int newLength) {
343: byte[] newArray = new byte[newLength];
344: int oldLength = array.length;
345:
346: System.arraycopy(array, 0, newArray, 0, Math.min(oldLength,
347: 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,
366: this .pngBytes.length + 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,
388: this .pngBytes.length + 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), (byte) ((n >> 8) & 0xff),
416: (byte) (n & 0xff) };
417: return writeBytes(temp, offset);
418: }
419:
420: /**
421: * Write a single byte into the pngBytes array at a given position.
422: *
423: * @param b The integer to be written into pngBytes.
424: * @param offset The starting point to write to.
425: * @return The next place to be written to in the pngBytes array.
426: */
427: protected int writeByte(int b, int offset) {
428: byte[] temp = { (byte) b };
429: return writeBytes(temp, offset);
430: }
431:
432: /**
433: * Write a PNG "IHDR" chunk into the pngBytes array.
434: */
435: protected void writeHeader() {
436:
437: int startPos = this .bytePos = writeInt4(13, this .bytePos);
438: this .bytePos = writeBytes(IHDR, this .bytePos);
439: this .width = this .image.getWidth(null);
440: this .height = this .image.getHeight(null);
441: this .bytePos = writeInt4(this .width, this .bytePos);
442: this .bytePos = writeInt4(this .height, this .bytePos);
443: this .bytePos = writeByte(8, this .bytePos); // bit depth
444: this .bytePos = writeByte((this .encodeAlpha) ? 6 : 2,
445: 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
452: - startPos);
453: this .crcValue = this .crc.getValue();
454: this .bytePos = writeInt4((int) this .crcValue, this .bytePos);
455: }
456:
457: /**
458: * Perform "sub" filtering on the given row.
459: * Uses temporary array leftBytes to store the original values
460: * of the previous pixels. The array is 16 bytes long, which
461: * will easily hold two-byte samples plus two-byte alpha.
462: *
463: * @param pixels The array holding the scan lines being built
464: * @param startPos Starting position within pixels of bytes to be filtered.
465: * @param width Width of a scanline in pixels.
466: */
467: protected void filterSub(byte[] pixels, int startPos, int width) {
468: int offset = this .bytesPerPixel;
469: int actualStart = startPos + offset;
470: int nBytes = width * this .bytesPerPixel;
471: int leftInsert = offset;
472: int leftExtract = 0;
473:
474: for (int i = actualStart; i < startPos + nBytes; i++) {
475: this .leftBytes[leftInsert] = pixels[i];
476: pixels[i] = (byte) ((pixels[i] - this .leftBytes[leftExtract]) % 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] - this .priorRow[i]) % 256);
497: this .priorRow[i] = currentByte;
498: }
499: }
500:
501: /**
502: * Write the image data into the pngBytes array.
503: * This will write one or more PNG "IDAT" chunks. In order
504: * to conserve memory, this method grabs as many rows as will
505: * fit into 32K bytes, or the whole image; whichever is less.
506: *
507: *
508: * @return true if no errors; false if error grabbing pixels
509: */
510: protected boolean writeImageData() {
511: int rowsLeft = this .height; // number of rows remaining to write
512: int startRow = 0; // starting row to process this time through
513: int nRows; // how many rows to grab at a time
514:
515: byte[] scanLines; // the scan lines to be compressed
516: int scanPos; // where we are in the scan lines
517: int startPos; // where this line's actual pixels start (used
518: // for filtering)
519:
520: byte[] compressedLines; // the resultant compressed lines
521: int nCompressed; // how big is the compressed area?
522:
523: //int depth; // color depth ( handle only 8 or 32 )
524:
525: PixelGrabber pg;
526:
527: this .bytesPerPixel = (this .encodeAlpha) ? 4 : 3;
528:
529: Deflater scrunch = new Deflater(this .compressionLevel);
530: ByteArrayOutputStream outBytes = new ByteArrayOutputStream(1024);
531:
532: DeflaterOutputStream compBytes = new DeflaterOutputStream(
533: outBytes, scrunch);
534: try {
535: while (rowsLeft > 0) {
536: nRows = Math
537: .min(
538: 32767 / (this .width * (this .bytesPerPixel + 1)),
539: rowsLeft);
540: nRows = Math.max(nRows, 1);
541:
542: int[] pixels = new int[this .width * nRows];
543:
544: pg = new PixelGrabber(this .image, 0, startRow,
545: this .width, nRows, pixels, 0, this .width);
546: try {
547: pg.grabPixels();
548: } catch (Exception e) {
549: System.err
550: .println("interrupted waiting for pixels!");
551: return false;
552: }
553: if ((pg.getStatus() & ImageObserver.ABORT) != 0) {
554: System.err
555: .println("image fetch aborted or errored");
556: return false;
557: }
558:
559: /*
560: * Create a data chunk. scanLines adds "nRows" for
561: * the filter bytes.
562: */
563: scanLines = new byte[this .width * nRows
564: * this .bytesPerPixel + nRows];
565:
566: if (this .filter == FILTER_SUB) {
567: this .leftBytes = new byte[16];
568: }
569: if (this .filter == FILTER_UP) {
570: this .priorRow = new byte[this .width
571: * this .bytesPerPixel];
572: }
573:
574: scanPos = 0;
575: startPos = 1;
576: for (int i = 0; i < this .width * nRows; i++) {
577: if (i % this .width == 0) {
578: scanLines[scanPos++] = (byte) this .filter;
579: startPos = scanPos;
580: }
581: scanLines[scanPos++] = (byte) ((pixels[i] >> 16) & 0xff);
582: scanLines[scanPos++] = (byte) ((pixels[i] >> 8) & 0xff);
583: scanLines[scanPos++] = (byte) ((pixels[i]) & 0xff);
584: if (this .encodeAlpha) {
585: scanLines[scanPos++] = (byte) ((pixels[i] >> 24) & 0xff);
586: }
587: if ((i % this .width == this .width - 1)
588: && (this .filter != FILTER_NONE)) {
589: if (this .filter == FILTER_SUB) {
590: filterSub(scanLines, startPos, this .width);
591: }
592: if (this .filter == FILTER_UP) {
593: filterUp(scanLines, startPos, this .width);
594: }
595: }
596: }
597:
598: /*
599: * Write these lines to the output area
600: */
601: compBytes.write(scanLines, 0, scanPos);
602:
603: startRow += nRows;
604: rowsLeft -= nRows;
605: }
606: compBytes.close();
607:
608: /*
609: * Write the compressed bytes
610: */
611: compressedLines = outBytes.toByteArray();
612: nCompressed = compressedLines.length;
613:
614: this .crc.reset();
615: this .bytePos = writeInt4(nCompressed, this .bytePos);
616: this .bytePos = writeBytes(IDAT, this .bytePos);
617: this .crc.update(IDAT);
618: this .bytePos = writeBytes(compressedLines, nCompressed,
619: this .bytePos);
620: this .crc.update(compressedLines, 0, nCompressed);
621:
622: this .crcValue = this .crc.getValue();
623: this .bytePos = writeInt4((int) this .crcValue, this .bytePos);
624: scrunch.finish();
625: return true;
626: } catch (IOException e) {
627: System.err.println(e.toString());
628: return false;
629: }
630: }
631:
632: /**
633: * Write a PNG "IEND" chunk into the pngBytes array.
634: */
635: protected void writeEnd() {
636: this .bytePos = writeInt4(0, this .bytePos);
637: this .bytePos = writeBytes(IEND, this .bytePos);
638: this .crc.reset();
639: this .crc.update(IEND);
640: this .crcValue = this .crc.getValue();
641: this .bytePos = writeInt4((int) this .crcValue, this .bytePos);
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: }
|