View Javadoc

1   package org.freehep.graphicsio.png;
2   
3   //package com.keypoint;
4   
5   /**
6    * PngEncoder takes a Java Image object and creates a byte string which can be saved as a PNG file.
7    * The Image is presumed to use the DirectColorModel.
8    *
9    * Thanks to Jay Denny at KeyPoint Software
10   *    http://www.keypoint.com/
11   * who let me develop this code on company time.
12   *
13   * You may contact me with (probably very-much-needed) improvements,
14   * comments, and bug fixes at:
15   *
16   *   david@catcode.com
17   *
18   * This library is free software; you can redistribute it and/or
19   * modify it under the terms of the GNU Lesser General Public
20   * License as published by the Free Software Foundation; either
21   * version 2.1 of the License, or (at your option) any later version.
22   *
23   * This library is distributed in the hope that it will be useful,
24   * but WITHOUT ANY WARRANTY; without even the implied warranty of
25   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
26   * Lesser General Public License for more details.
27   *
28   * You should have received a copy of the GNU Lesser General Public
29   * License along with this library; if not, write to the Free Software
30   * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
31   * A copy of the GNU LGPL may be found at
32   * http://www.gnu.org/copyleft/lesser.html,
33   *
34   * @author J. David Eisenberg
35   * @version 1.4, 31 March 2000
36   */
37  
38  /**
39   * Added ImageObserver so that getHeight, getWidth calls work properly.
40   *
41   * @author M.Donszelmann
42   */
43  
44  import java.awt.Image;
45  import java.awt.Toolkit;
46  import java.awt.image.ImageObserver;
47  import java.awt.image.PixelGrabber;
48  import java.io.ByteArrayOutputStream;
49  import java.io.IOException;
50  import java.util.ArrayList;
51  import java.util.Iterator;
52  import java.util.List;
53  import java.util.zip.CRC32;
54  import java.util.zip.Deflater;
55  import java.util.zip.DeflaterOutputStream;
56  
57  public class PNGEncoder extends Object implements ImageObserver {
58      /** Constant specifying that alpha channel should be encoded. */
59      public static final boolean ENCODE_ALPHA = true;
60  
61      /** Constant specifying that alpha channel should not be encoded. */
62      public static final boolean NO_ALPHA = false;
63  
64      /** Constants for filters */
65      public static final int FILTER_NONE = 0;
66  
67      public static final int FILTER_SUB = 1;
68  
69      public static final int FILTER_UP = 2;
70  
71      public static final int FILTER_LAST = 2;
72  
73      protected byte[] pngBytes;
74  
75      protected byte[] priorRow;
76  
77      protected byte[] leftBytes;
78  
79      protected Image image;
80  
81      protected int width, height;
82  
83      protected int bytePos, maxPos;
84  
85      protected int hdrPos, dataPos, endPos;
86  
87      protected CRC32 crc = new CRC32();
88  
89      protected long crcValue;
90  
91      protected boolean encodeAlpha;
92  
93      protected int filter;
94  
95      protected int bytesPerPixel;
96  
97      protected int compressionLevel;
98  
99      protected List keys = new ArrayList();
100 
101     protected List text = new ArrayList();
102 
103     public PNGEncoder() {
104         this(null, false, FILTER_NONE, 0);
105     }
106 
107     /**
108      * Class constructor specifying Image to encode, with no alpha channel
109      * encoding.
110      * 
111      * @param image A Java Image object which uses the DirectColorModel
112      * @see java.awt.Image
113      */
114     public PNGEncoder(Image image) {
115         this(image, false, FILTER_NONE, 0);
116     }
117 
118     /**
119      * Class constructor specifying Image to encode, and whether to encode
120      * alpha.
121      * 
122      * @param image A Java Image object which uses the DirectColorModel
123      * @param encodeAlpha Encode the alpha channel? false=no; true=yes
124      * @see java.awt.Image
125      */
126     public PNGEncoder(Image image, boolean encodeAlpha) {
127         this(image, encodeAlpha, FILTER_NONE, 0);
128     }
129 
130     /**
131      * Class constructor specifying Image to encode, whether to encode alpha,
132      * and filter to use.
133      * 
134      * @param image A Java Image object which uses the DirectColorModel
135      * @param encodeAlpha Encode the alpha channel? false=no; true=yes
136      * @param whichFilter 0=none, 1=sub, 2=up
137      * @see java.awt.Image
138      */
139     public PNGEncoder(Image image, boolean encodeAlpha, int whichFilter) {
140         this(image, encodeAlpha, whichFilter, 0);
141     }
142 
143     /**
144      * Class constructor specifying Image source to encode, whether to encode
145      * alpha, filter to use, and compression level.
146      * 
147      * @param image A Java Image object
148      * @param encodeAlpha Encode the alpha channel? false=no; true=yes
149      * @param whichFilter 0=none, 1=sub, 2=up
150      * @param compLevel 0..9
151      * @see java.awt.Image
152      */
153     public PNGEncoder(Image image, boolean encodeAlpha, int whichFilter,
154             int compLevel) {
155         this.image = image;
156         this.encodeAlpha = encodeAlpha;
157         setFilter(whichFilter);
158         if (compLevel >= 0 && compLevel <= 9) {
159             this.compressionLevel = compLevel;
160         }
161     }
162 
163     public void addText(String key, String value) {
164         if ((key == null) || (key.length() == 0))
165             key = "Comment";
166         keys.add(key.substring(0, Math.min(79, key.length())));
167         text.add(value);
168     }
169 
170     /**
171      * Set the image to be encoded
172      * 
173      * @param image A Java Image object which uses the DirectColorModel
174      * @see java.awt.Image
175      */
176     public void setImage(Image image) {
177         this.image = image;
178         pngBytes = null;
179     }
180 
181     /** method to wait for image */
182     private int imageStatus;
183 
184     public boolean imageUpdate(Image image, int flags, int x, int y, int width,
185             int height) {
186         imageStatus = flags;
187         if (((flags & ALLBITS) == ALLBITS) || ((flags & (ABORT | ERROR)) != 0)) {
188             return false;
189         }
190         return true;
191     }
192 
193     /**
194      * Creates an array of bytes that is the PNG equivalent of the current
195      * image, specifying whether to encode alpha or not.
196      * 
197      * @param encodeAlpha boolean false=no alpha, true=encode alpha
198      * @return an array of bytes, or null if there was a problem
199      */
200     public byte[] pngEncode(boolean encodeAlpha) {
201         byte[] pngIdBytes = { -119, 80, 78, 71, 13, 10, 26, 10 };
202 
203         if (image == null) {
204             return null;
205         }
206 
207         imageStatus = 0;
208         boolean status = Toolkit.getDefaultToolkit().prepareImage(image, -1,
209                 -1, this);
210 
211         if (!status) {
212             while (((imageStatus & (ALLBITS)) == 0)
213                     && ((imageStatus & (ABORT | ERROR)) == 0)) {
214                 try {
215                     Thread.sleep(100);
216                 } catch (Exception e) {
217                 }
218             }
219             // FIXED: moved this inside the "if (!status)" area
220             if ((imageStatus & (ALLBITS)) == 0) {
221                 return null;
222             }
223         }
224 
225         width = image.getWidth(null);
226         height = image.getHeight(null);
227 
228         /*
229          * start with an array that is big enough to hold all the pixels (plus
230          * filter bytes), and an extra 200 bytes for header info
231          */
232         pngBytes = new byte[((width + 1) * height * 3) + 200];
233 
234         /*
235          * keep track of largest byte written to the array
236          */
237         maxPos = 0;
238 
239         bytePos = writeBytes(pngIdBytes, 0);
240         hdrPos = bytePos;
241         writeHeader();
242         for (Iterator ik = keys.iterator(), iv = text.iterator(); ik.hasNext()
243                 && iv.hasNext();) {
244             writeText((String) ik.next(), (String) iv.next());
245         }
246         dataPos = bytePos;
247         if (writeImageData()) {
248             writeEnd();
249             pngBytes = resizeByteArray(pngBytes, maxPos);
250         } else {
251             pngBytes = null;
252         }
253         return pngBytes;
254     }
255 
256     /**
257      * Creates an array of bytes that is the PNG equivalent of the current
258      * image. Alpha encoding is determined by its setting in the constructor.
259      * 
260      * @return an array of bytes, or null if there was a problem
261      */
262     public byte[] pngEncode() {
263         return pngEncode(encodeAlpha);
264     }
265 
266     /**
267      * Set the alpha encoding on or off.
268      * 
269      * @param encodeAlpha false=no, true=yes
270      */
271     public void setEncodeAlpha(boolean encodeAlpha) {
272         this.encodeAlpha = encodeAlpha;
273     }
274 
275     /**
276      * Retrieve alpha encoding status.
277      * 
278      * @return boolean false=no, true=yes
279      */
280     public boolean getEncodeAlpha() {
281         return encodeAlpha;
282     }
283 
284     /**
285      * Set the filter to use
286      * 
287      * @param whichFilter from constant list
288      */
289     public void setFilter(int whichFilter) {
290         this.filter = FILTER_NONE;
291         if (whichFilter <= FILTER_LAST) {
292             this.filter = whichFilter;
293         }
294     }
295 
296     /**
297      * Retrieve filtering scheme
298      * 
299      * @return int (see constant list)
300      */
301     public int getFilter() {
302         return filter;
303     }
304 
305     /**
306      * Set the compression level to use
307      * 
308      * @param level 0 through 9
309      */
310     public void setCompressionLevel(int level) {
311         if (level >= 0 && level <= 9) {
312             this.compressionLevel = level;
313         }
314     }
315 
316     /**
317      * Retrieve compression level
318      * 
319      * @return int in range 0-9
320      */
321     public int getCompressionLevel() {
322         return compressionLevel;
323     }
324 
325     /**
326      * Increase or decrease the length of a byte array.
327      * 
328      * @param array The original array.
329      * @param newLength The length you wish the new array to have.
330      * @return Array of newly desired length. If shorter than the original, the
331      *         trailing elements are truncated.
332      */
333     protected byte[] resizeByteArray(byte[] array, int newLength) {
334         byte[] newArray = new byte[newLength];
335         int oldLength = array.length;
336 
337         System.arraycopy(array, 0, newArray, 0, Math.min(oldLength, newLength));
338         return newArray;
339     }
340 
341     /**
342      * Write an array of bytes into the pngBytes array. Note: This routine has
343      * the side effect of updating maxPos, the largest element written in the
344      * array. The array is resized by 1000 bytes or the length of the data to be
345      * written, whichever is larger.
346      * 
347      * @param data The data to be written into pngBytes.
348      * @param offset The starting point to write to.
349      * @return The next place to be written to in the pngBytes array.
350      */
351     protected int writeBytes(byte[] data, int offset) {
352         maxPos = Math.max(maxPos, offset + data.length);
353         if (data.length + offset > pngBytes.length) {
354             pngBytes = resizeByteArray(pngBytes, pngBytes.length
355                     + Math.max(1000, data.length));
356         }
357         System.arraycopy(data, 0, pngBytes, offset, data.length);
358         return offset + data.length;
359     }
360 
361     /**
362      * Write an array of bytes into the pngBytes array, specifying number of
363      * bytes to write. Note: This routine has the side effect of updating
364      * maxPos, the largest element written in the array. The array is resized by
365      * 1000 bytes or the length of the data to be written, whichever is larger.
366      * 
367      * @param data The data to be written into pngBytes.
368      * @param nBytes The number of bytes to be written.
369      * @param offset The starting point to write to.
370      * @return The next place to be written to in the pngBytes array.
371      */
372     protected int writeBytes(byte[] data, int nBytes, int offset) {
373         maxPos = Math.max(maxPos, offset + nBytes);
374         if (nBytes + offset > pngBytes.length) {
375             pngBytes = resizeByteArray(pngBytes, pngBytes.length
376                     + Math.max(1000, nBytes));
377         }
378         System.arraycopy(data, 0, pngBytes, offset, nBytes);
379         return offset + nBytes;
380     }
381 
382     /**
383      * Write a two-byte integer into the pngBytes array at a given position.
384      * 
385      * @param n The integer to be written into pngBytes.
386      * @param offset The starting point to write to.
387      * @return The next place to be written to in the pngBytes array.
388      */
389     protected int writeInt2(int n, int offset) {
390         byte[] temp = { (byte) ((n >> 8) & 0xff), (byte) (n & 0xff) };
391         return writeBytes(temp, offset);
392     }
393 
394     /**
395      * Write a four-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 writeInt4(int n, int offset) {
402         byte[] temp = { (byte) ((n >> 24) & 0xff), (byte) ((n >> 16) & 0xff),
403                 (byte) ((n >> 8) & 0xff), (byte) (n & 0xff) };
404         return writeBytes(temp, offset);
405     }
406 
407     /**
408      * Write a single byte into the pngBytes array at a given position.
409      * 
410      * @param b The integer to be written into pngBytes.
411      * @param offset The starting point to write to.
412      * @return The next place to be written to in the pngBytes array.
413      */
414     protected int writeByte(int b, int offset) {
415         byte[] temp = { (byte) b };
416         return writeBytes(temp, offset);
417     }
418 
419     /**
420      * Write a string into the pngBytes array at a given position. This uses the
421      * getBytes method, so the encoding used will be its default.
422      * 
423      * @param s The String 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      * @see java.lang.String#getBytes()
427      */
428     protected int writeString(String s, int offset) {
429         return writeBytes(s.getBytes(), offset);
430     }
431 
432     /**
433      * Write a PNG "IHDR" chunk into the pngBytes array.
434      */
435     protected void writeHeader() {
436         int startPos;
437 
438         startPos = bytePos = writeInt4(13, bytePos);
439         bytePos = writeString("IHDR", bytePos);
440         width = image.getWidth(null);
441         height = image.getHeight(null);
442         bytePos = writeInt4(width, bytePos);
443         bytePos = writeInt4(height, bytePos);
444         bytePos = writeByte(8, bytePos); // bit depth
445         bytePos = writeByte((encodeAlpha) ? 6 : 2, bytePos); // direct model
446         bytePos = writeByte(0, bytePos); // compression method
447         bytePos = writeByte(0, bytePos); // filter method
448         bytePos = writeByte(0, bytePos); // no interlace
449         crc.reset();
450         crc.update(pngBytes, startPos, bytePos - startPos);
451         crcValue = crc.getValue();
452         bytePos = writeInt4((int) crcValue, bytePos);
453     }
454 
455     protected void writeText(String key, String value) {
456         int startPos;
457         int len = key.length() + 1 + value.length();
458         startPos = bytePos = writeInt4(len, bytePos);
459         bytePos = writeString("tEXt", bytePos);
460         bytePos = writeString(key, bytePos);
461         bytePos = writeByte(0, bytePos);
462         bytePos = writeString(value, bytePos);
463         crc.reset();
464         crc.update(pngBytes, startPos, bytePos - startPos);
465         crcValue = crc.getValue();
466         bytePos = writeInt4((int) crcValue, bytePos);
467     }
468 
469     /**
470      * Perform "sub" filtering on the given row. Uses temporary array leftBytes
471      * to store the original values of the previous pixels. The array is 16
472      * bytes long, which will easily hold two-byte samples plus two-byte alpha.
473      * 
474      * @param pixels The array holding the scan lines being built
475      * @param startPos Starting position within pixels of bytes to be filtered.
476      * @param width Width of a scanline in pixels.
477      */
478     protected void filterSub(byte[] pixels, int startPos, int width) {
479         int i;
480         int offset = bytesPerPixel;
481         int actualStart = startPos + offset;
482         int nBytes = width * bytesPerPixel;
483         int leftInsert = offset;
484         int leftExtract = 0;
485 
486         for (i = actualStart; i < startPos + nBytes; i++) {
487             leftBytes[leftInsert] = pixels[i];
488             pixels[i] = (byte) ((pixels[i] - leftBytes[leftExtract]) % 256);
489             leftInsert = (leftInsert + 1) % 0x0f;
490             leftExtract = (leftExtract + 1) % 0x0f;
491         }
492     }
493 
494     /**
495      * Perform "up" filtering on the given row. Side effect: refills the prior
496      * row with current row
497      * 
498      * @param pixels The array holding the scan lines being built
499      * @param startPos Starting position within pixels of bytes to be filtered.
500      * @param width Width of a scanline in pixels.
501      */
502     protected void filterUp(byte[] pixels, int startPos, int width) {
503         int i, nBytes;
504         byte current_byte;
505 
506         nBytes = width * bytesPerPixel;
507 
508         for (i = 0; i < nBytes; i++) {
509             current_byte = pixels[startPos + i];
510             pixels[startPos + i] = (byte) ((pixels[startPos + i] - priorRow[i]) % 256);
511             priorRow[i] = current_byte;
512         }
513     }
514 
515     /**
516      * Write the image data into the pngBytes array. This will write one or more
517      * PNG "IDAT" chunks. In order to conserve memory, this method grabs as many
518      * rows as will fit into 32K bytes, or the whole image; whichever is less.
519      * 
520      * 
521      * @return true if no errors; false if error grabbing pixels
522      */
523     protected boolean writeImageData() {
524         int rowsLeft = height; // number of rows remaining to write
525         int startRow = 0; // starting row to process this time through
526         int nRows; // how many rows to grab at a time
527 
528         byte[] scanLines; // the scan lines to be compressed
529         int scanPos; // where we are in the scan lines
530         int startPos; // where this line's actual pixels start (used for
531                         // filtering)
532 
533         byte[] compressedLines; // the resultant compressed lines
534         int nCompressed; // how big is the compressed area?
535 
536         PixelGrabber pg;
537 
538         bytesPerPixel = (encodeAlpha) ? 4 : 3;
539 
540         Deflater scrunch = new Deflater(compressionLevel);
541         ByteArrayOutputStream outBytes = new ByteArrayOutputStream(1024);
542 
543         DeflaterOutputStream compBytes = new DeflaterOutputStream(outBytes,
544                 scrunch);
545         try {
546             while (rowsLeft > 0) {
547                 nRows = Math.min(32767 / (width * (bytesPerPixel + 1)),
548                         rowsLeft);
549                 // nRows = rowsLeft;
550                 int[] pixels = new int[width * nRows];
551 
552                 pg = new PixelGrabber(image, 0, startRow, width, nRows, pixels,
553                         0, width);
554                 try {
555                     pg.grabPixels();
556                 } catch (Exception e) {
557                     System.err.println("interrupted waiting for pixels!");
558                     return false;
559                 }
560                 if ((pg.getStatus() & ImageObserver.ABORT) != 0) {
561                     System.err.println("image fetch aborted or errored");
562                     return false;
563                 }
564 
565                 /*
566                  * Create a data chunk. scanLines adds "nRows" for the filter
567                  * bytes.
568                  */
569                 scanLines = new byte[width * nRows * bytesPerPixel + nRows];
570 
571                 if (filter == FILTER_SUB) {
572                     leftBytes = new byte[16];
573                 }
574                 if (filter == FILTER_UP) {
575                     priorRow = new byte[width * bytesPerPixel];
576                 }
577 
578                 scanPos = 0;
579                 startPos = 1;
580                 for (int i = 0; i < width * nRows; i++) {
581                     if (i % width == 0) {
582                         scanLines[scanPos++] = (byte) filter;
583                         startPos = scanPos;
584                     }
585                     scanLines[scanPos++] = (byte) ((pixels[i] >> 16) & 0xff);
586                     scanLines[scanPos++] = (byte) ((pixels[i] >> 8) & 0xff);
587                     scanLines[scanPos++] = (byte) ((pixels[i]) & 0xff);
588                     if (encodeAlpha) {
589                         scanLines[scanPos++] = (byte) ((pixels[i] >> 24) & 0xff);
590                     }
591                     if ((i % width == width - 1) && (filter != FILTER_NONE)) {
592                         if (filter == FILTER_SUB) {
593                             filterSub(scanLines, startPos, width);
594                         }
595                         if (filter == FILTER_UP) {
596                             filterUp(scanLines, startPos, width);
597                         }
598                     }
599                 }
600 
601                 /*
602                  * Write these lines to the output area
603                  */
604                 compBytes.write(scanLines, 0, scanPos);
605 
606                 startRow += nRows;
607                 rowsLeft -= nRows;
608             }
609             compBytes.close();
610 
611             /*
612              * Write the compressed bytes
613              */
614             compressedLines = outBytes.toByteArray();
615             nCompressed = compressedLines.length;
616 
617             crc.reset();
618             bytePos = writeInt4(nCompressed, bytePos);
619             bytePos = writeString("IDAT", bytePos);
620             crc.update("IDAT".getBytes());
621             bytePos = writeBytes(compressedLines, nCompressed, bytePos);
622             crc.update(compressedLines, 0, nCompressed);
623 
624             crcValue = crc.getValue();
625             bytePos = writeInt4((int) crcValue, bytePos);
626             scrunch.finish();
627             return true;
628         } catch (IOException e) {
629             System.err.println(e.toString());
630             return false;
631         }
632     }
633 
634     /**
635      * Write a PNG "IEND" chunk into the pngBytes array.
636      */
637     protected void writeEnd() {
638         bytePos = writeInt4(0, bytePos);
639         bytePos = writeString("IEND", bytePos);
640         crc.reset();
641         crc.update("IEND".getBytes());
642         crcValue = crc.getValue();
643         bytePos = writeInt4((int) crcValue, bytePos);
644     }
645 }