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