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 }