001package com.keypoint; 002 003import java.awt.Image; 004import java.awt.image.ImageObserver; 005import java.awt.image.PixelGrabber; 006import java.io.ByteArrayOutputStream; 007import java.io.IOException; 008import java.util.zip.CRC32; 009import java.util.zip.Deflater; 010import 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 * 15-Aug-2008 : Added scrunch.end() in writeImageData() method - see 053 * JFreeChart bug report 2037930 (David Gilbert); 054 */ 055 056public class PngEncoder { 057 058 /** Constant specifying that alpha channel should be encoded. */ 059 public static final boolean ENCODE_ALPHA = true; 060 061 /** Constant specifying that alpha channel should not be encoded. */ 062 public static final boolean NO_ALPHA = false; 063 064 /** Constants for filter (NONE). */ 065 public static final int FILTER_NONE = 0; 066 067 /** Constants for filter (SUB). */ 068 public static final int FILTER_SUB = 1; 069 070 /** Constants for filter (UP). */ 071 public static final int FILTER_UP = 2; 072 073 /** Constants for filter (LAST). */ 074 public static final int FILTER_LAST = 2; 075 076 /** IHDR tag. */ 077 protected static final byte[] IHDR = {73, 72, 68, 82}; 078 079 /** IDAT tag. */ 080 protected static final byte[] IDAT = {73, 68, 65, 84}; 081 082 /** IEND tag. */ 083 protected static final byte[] IEND = {73, 69, 78, 68}; 084 085 /** PHYS tag. */ 086 protected static final byte[] PHYS = {(byte)'p', (byte)'H', (byte)'Y', 087 (byte)'s'}; 088 089 /** The png bytes. */ 090 protected byte[] pngBytes; 091 092 /** The prior row. */ 093 protected byte[] priorRow; 094 095 /** The left bytes. */ 096 protected byte[] leftBytes; 097 098 /** The image. */ 099 protected Image image; 100 101 /** The width. */ 102 protected int width; 103 104 /** The height. */ 105 protected int height; 106 107 /** The byte position. */ 108 protected int bytePos; 109 110 /** The maximum position. */ 111 protected int maxPos; 112 113 /** CRC. */ 114 protected CRC32 crc = new CRC32(); 115 116 /** The CRC value. */ 117 protected long crcValue; 118 119 /** Encode alpha? */ 120 protected boolean encodeAlpha; 121 122 /** The filter type. */ 123 protected int filter; 124 125 /** The bytes-per-pixel. */ 126 protected int bytesPerPixel; 127 128 /** The physical pixel dimension : number of pixels per inch on the X axis. */ 129 private int xDpi = 0; 130 131 /** The physical pixel dimension : number of pixels per inch on the Y axis. */ 132 private int yDpi = 0; 133 134 /** Used for conversion of DPI to Pixels per Meter. */ 135 static private float INCH_IN_METER_UNIT = 0.0254f; 136 137 /** 138 * The compression level (1 = best speed, 9 = best compression, 139 * 0 = no compression). 140 */ 141 protected int compressionLevel; 142 143 /** 144 * Class constructor. 145 */ 146 public PngEncoder() { 147 this(null, false, FILTER_NONE, 0); 148 } 149 150 /** 151 * Class constructor specifying Image to encode, with no alpha channel 152 * encoding. 153 * 154 * @param image A Java Image object which uses the DirectColorModel 155 * @see java.awt.Image 156 */ 157 public PngEncoder(Image image) { 158 this(image, false, FILTER_NONE, 0); 159 } 160 161 /** 162 * Class constructor specifying Image to encode, and whether to encode 163 * alpha. 164 * 165 * @param image A Java Image object which uses the DirectColorModel 166 * @param encodeAlpha Encode the alpha channel? false=no; true=yes 167 * @see java.awt.Image 168 */ 169 public PngEncoder(Image image, boolean encodeAlpha) { 170 this(image, encodeAlpha, FILTER_NONE, 0); 171 } 172 173 /** 174 * Class constructor specifying Image to encode, whether to encode alpha, 175 * and filter to use. 176 * 177 * @param image A Java Image object which uses the DirectColorModel 178 * @param encodeAlpha Encode the alpha channel? false=no; true=yes 179 * @param whichFilter 0=none, 1=sub, 2=up 180 * @see java.awt.Image 181 */ 182 public PngEncoder(Image image, boolean encodeAlpha, int whichFilter) { 183 this(image, encodeAlpha, whichFilter, 0); 184 } 185 186 187 /** 188 * Class constructor specifying Image source to encode, whether to encode 189 * alpha, filter to use, and compression level. 190 * 191 * @param image A Java Image object 192 * @param encodeAlpha Encode the alpha channel? false=no; true=yes 193 * @param whichFilter 0=none, 1=sub, 2=up 194 * @param compLevel 0..9 (1 = best speed, 9 = best compression, 0 = no 195 * compression) 196 * @see java.awt.Image 197 */ 198 public PngEncoder(Image image, boolean encodeAlpha, int whichFilter, 199 int compLevel) { 200 this.image = image; 201 this.encodeAlpha = encodeAlpha; 202 setFilter(whichFilter); 203 if (compLevel >= 0 && compLevel <= 9) { 204 this.compressionLevel = compLevel; 205 } 206 } 207 208 /** 209 * Set the image to be encoded. 210 * 211 * @param image A Java Image object which uses the DirectColorModel 212 * @see java.awt.Image 213 * @see java.awt.image.DirectColorModel 214 */ 215 public void setImage(Image image) { 216 this.image = image; 217 this.pngBytes = null; 218 } 219 220 /** 221 * Returns the image to be encoded. 222 * 223 * @return The image. 224 */ 225 public Image getImage() { 226 return this.image; 227 } 228 229 /** 230 * Creates an array of bytes that is the PNG equivalent of the current 231 * image, specifying whether to encode alpha or not. 232 * 233 * @param encodeAlpha boolean false=no alpha, true=encode alpha 234 * @return an array of bytes, or null if there was a problem 235 */ 236 public byte[] pngEncode(boolean encodeAlpha) { 237 byte[] pngIdBytes = {-119, 80, 78, 71, 13, 10, 26, 10}; 238 239 if (this.image == null) { 240 return null; 241 } 242 this.width = this.image.getWidth(null); 243 this.height = this.image.getHeight(null); 244 245 /* 246 * start with an array that is big enough to hold all the pixels 247 * (plus filter bytes), and an extra 200 bytes for header info 248 */ 249 this.pngBytes = new byte[((this.width + 1) * this.height * 3) + 200]; 250 251 /* 252 * keep track of largest byte written to the array 253 */ 254 this.maxPos = 0; 255 256 this.bytePos = writeBytes(pngIdBytes, 0); 257 //hdrPos = bytePos; 258 writeHeader(); 259 writeResolution(); 260 //dataPos = bytePos; 261 if (writeImageData()) { 262 writeEnd(); 263 this.pngBytes = resizeByteArray(this.pngBytes, this.maxPos); 264 } 265 else { 266 this.pngBytes = null; 267 } 268 return this.pngBytes; 269 } 270 271 /** 272 * Creates an array of bytes that is the PNG equivalent of the current 273 * image. Alpha encoding is determined by its setting in the constructor. 274 * 275 * @return an array of bytes, or null if there was a problem 276 */ 277 public byte[] pngEncode() { 278 return pngEncode(this.encodeAlpha); 279 } 280 281 /** 282 * Set the alpha encoding on or off. 283 * 284 * @param encodeAlpha false=no, true=yes 285 */ 286 public void setEncodeAlpha(boolean encodeAlpha) { 287 this.encodeAlpha = encodeAlpha; 288 } 289 290 /** 291 * Retrieve alpha encoding status. 292 * 293 * @return boolean false=no, true=yes 294 */ 295 public boolean getEncodeAlpha() { 296 return this.encodeAlpha; 297 } 298 299 /** 300 * Set the filter to use. 301 * 302 * @param whichFilter from constant list 303 */ 304 public void setFilter(int whichFilter) { 305 this.filter = FILTER_NONE; 306 if (whichFilter <= FILTER_LAST) { 307 this.filter = whichFilter; 308 } 309 } 310 311 /** 312 * Retrieve filtering scheme. 313 * 314 * @return int (see constant list) 315 */ 316 public int getFilter() { 317 return this.filter; 318 } 319 320 /** 321 * Set the compression level to use. 322 * 323 * @param level the compression level (1 = best speed, 9 = best compression, 324 * 0 = no compression) 325 */ 326 public void setCompressionLevel(int level) { 327 if (level >= 0 && level <= 9) { 328 this.compressionLevel = level; 329 } 330 } 331 332 /** 333 * Retrieve compression level. 334 * 335 * @return int (1 = best speed, 9 = best compression, 0 = no compression) 336 */ 337 public int getCompressionLevel() { 338 return this.compressionLevel; 339 } 340 341 /** 342 * Increase or decrease the length of a byte array. 343 * 344 * @param array The original array. 345 * @param newLength The length you wish the new array to have. 346 * @return Array of newly desired length. If shorter than the 347 * original, the trailing elements are truncated. 348 */ 349 protected byte[] resizeByteArray(byte[] array, int newLength) { 350 byte[] newArray = new byte[newLength]; 351 int oldLength = array.length; 352 353 System.arraycopy(array, 0, newArray, 0, Math.min(oldLength, newLength)); 354 return newArray; 355 } 356 357 /** 358 * Write an array of bytes into the pngBytes array. 359 * Note: This routine has the side effect of updating 360 * maxPos, the largest element written in the array. 361 * The array is resized by 1000 bytes or the length 362 * of the data to be written, whichever is larger. 363 * 364 * @param data The data to be written into pngBytes. 365 * @param offset The starting point to write to. 366 * @return The next place to be written to in the pngBytes array. 367 */ 368 protected int writeBytes(byte[] data, int offset) { 369 this.maxPos = Math.max(this.maxPos, offset + data.length); 370 if (data.length + offset > this.pngBytes.length) { 371 this.pngBytes = resizeByteArray(this.pngBytes, this.pngBytes.length 372 + Math.max(1000, data.length)); 373 } 374 System.arraycopy(data, 0, this.pngBytes, offset, data.length); 375 return offset + data.length; 376 } 377 378 /** 379 * Write an array of bytes into the pngBytes array, specifying number of 380 * bytes to write. Note: This routine has the side effect of updating 381 * maxPos, the largest element written in the array. 382 * The array is resized by 1000 bytes or the length 383 * of the data to be written, whichever is larger. 384 * 385 * @param data The data to be written into pngBytes. 386 * @param nBytes The number of bytes to be written. 387 * @param offset The starting point to write to. 388 * @return The next place to be written to in the pngBytes array. 389 */ 390 protected int writeBytes(byte[] data, int nBytes, int offset) { 391 this.maxPos = Math.max(this.maxPos, offset + nBytes); 392 if (nBytes + offset > this.pngBytes.length) { 393 this.pngBytes = resizeByteArray(this.pngBytes, this.pngBytes.length 394 + Math.max(1000, nBytes)); 395 } 396 System.arraycopy(data, 0, this.pngBytes, offset, nBytes); 397 return offset + nBytes; 398 } 399 400 /** 401 * Write a two-byte integer into the pngBytes array at a given position. 402 * 403 * @param n The integer to be written into pngBytes. 404 * @param offset The starting point to write to. 405 * @return The next place to be written to in the pngBytes array. 406 */ 407 protected int writeInt2(int n, int offset) { 408 byte[] temp = {(byte) ((n >> 8) & 0xff), (byte) (n & 0xff)}; 409 return writeBytes(temp, offset); 410 } 411 412 /** 413 * Write a four-byte integer into the pngBytes array at a given position. 414 * 415 * @param n The integer to be written into pngBytes. 416 * @param offset The starting point to write to. 417 * @return The next place to be written to in the pngBytes array. 418 */ 419 protected int writeInt4(int n, int offset) { 420 byte[] temp = {(byte) ((n >> 24) & 0xff), 421 (byte) ((n >> 16) & 0xff), 422 (byte) ((n >> 8) & 0xff), 423 (byte) (n & 0xff)}; 424 return writeBytes(temp, offset); 425 } 426 427 /** 428 * Write a single byte into the pngBytes array at a given position. 429 * 430 * @param b The integer to be written into pngBytes. 431 * @param offset The starting point to write to. 432 * @return The next place to be written to in the pngBytes array. 433 */ 434 protected int writeByte(int b, int offset) { 435 byte[] temp = {(byte) b}; 436 return writeBytes(temp, offset); 437 } 438 439 /** 440 * Write a PNG "IHDR" chunk into the pngBytes array. 441 */ 442 protected void writeHeader() { 443 444 int startPos = this.bytePos = writeInt4(13, this.bytePos); 445 this.bytePos = writeBytes(IHDR, this.bytePos); 446 this.width = this.image.getWidth(null); 447 this.height = this.image.getHeight(null); 448 this.bytePos = writeInt4(this.width, this.bytePos); 449 this.bytePos = writeInt4(this.height, this.bytePos); 450 this.bytePos = writeByte(8, this.bytePos); // bit depth 451 this.bytePos = writeByte((this.encodeAlpha) ? 6 : 2, this.bytePos); 452 // direct model 453 this.bytePos = writeByte(0, this.bytePos); // compression method 454 this.bytePos = writeByte(0, this.bytePos); // filter method 455 this.bytePos = writeByte(0, this.bytePos); // no interlace 456 this.crc.reset(); 457 this.crc.update(this.pngBytes, startPos, this.bytePos - startPos); 458 this.crcValue = this.crc.getValue(); 459 this.bytePos = writeInt4((int) this.crcValue, this.bytePos); 460 } 461 462 /** 463 * Perform "sub" filtering on the given row. 464 * Uses temporary array leftBytes to store the original values 465 * of the previous pixels. The array is 16 bytes long, which 466 * will easily hold two-byte samples plus two-byte alpha. 467 * 468 * @param pixels The array holding the scan lines being built 469 * @param startPos Starting position within pixels of bytes to be filtered. 470 * @param width Width of a scanline in pixels. 471 */ 472 protected void filterSub(byte[] pixels, int startPos, int width) { 473 int offset = this.bytesPerPixel; 474 int actualStart = startPos + offset; 475 int nBytes = width * this.bytesPerPixel; 476 int leftInsert = offset; 477 int leftExtract = 0; 478 479 for (int i = actualStart; i < startPos + nBytes; i++) { 480 this.leftBytes[leftInsert] = pixels[i]; 481 pixels[i] = (byte) ((pixels[i] - this.leftBytes[leftExtract]) 482 % 256); 483 leftInsert = (leftInsert + 1) % 0x0f; 484 leftExtract = (leftExtract + 1) % 0x0f; 485 } 486 } 487 488 /** 489 * Perform "up" filtering on the given row. 490 * Side effect: refills the prior row with current row 491 * 492 * @param pixels The array holding the scan lines being built 493 * @param startPos Starting position within pixels of bytes to be filtered. 494 * @param width Width of a scanline in pixels. 495 */ 496 protected void filterUp(byte[] pixels, int startPos, int width) { 497 498 final int nBytes = width * this.bytesPerPixel; 499 500 for (int i = 0; i < nBytes; i++) { 501 final byte currentByte = pixels[startPos + i]; 502 pixels[startPos + i] = (byte) ((pixels[startPos + i] 503 - this.priorRow[i]) % 256); 504 this.priorRow[i] = currentByte; 505 } 506 } 507 508 /** 509 * Write the image data into the pngBytes array. 510 * This will write one or more PNG "IDAT" chunks. In order 511 * to conserve memory, this method grabs as many rows as will 512 * fit into 32K bytes, or the whole image; whichever is less. 513 * 514 * 515 * @return true if no errors; false if error grabbing pixels 516 */ 517 protected boolean writeImageData() { 518 int rowsLeft = this.height; // number of rows remaining to write 519 int startRow = 0; // starting row to process this time through 520 int nRows; // how many rows to grab at a time 521 522 byte[] scanLines; // the scan lines to be compressed 523 int scanPos; // where we are in the scan lines 524 int startPos; // where this line's actual pixels start (used 525 // for filtering) 526 527 byte[] compressedLines; // the resultant compressed lines 528 int nCompressed; // how big is the compressed area? 529 530 //int depth; // color depth ( handle only 8 or 32 ) 531 532 PixelGrabber pg; 533 534 this.bytesPerPixel = (this.encodeAlpha) ? 4 : 3; 535 536 Deflater scrunch = new Deflater(this.compressionLevel); 537 ByteArrayOutputStream outBytes = new ByteArrayOutputStream(1024); 538 539 DeflaterOutputStream compBytes = new DeflaterOutputStream(outBytes, 540 scrunch); 541 try { 542 while (rowsLeft > 0) { 543 nRows = Math.min(32767 / (this.width 544 * (this.bytesPerPixel + 1)), rowsLeft); 545 nRows = Math.max(nRows, 1); 546 547 int[] pixels = new int[this.width * nRows]; 548 549 pg = new PixelGrabber(this.image, 0, startRow, 550 this.width, nRows, pixels, 0, this.width); 551 try { 552 pg.grabPixels(); 553 } 554 catch (Exception e) { 555 System.err.println("interrupted waiting for pixels!"); 556 return false; 557 } 558 if ((pg.getStatus() & ImageObserver.ABORT) != 0) { 559 System.err.println("image fetch aborted or errored"); 560 return false; 561 } 562 563 /* 564 * Create a data chunk. scanLines adds "nRows" for 565 * the filter bytes. 566 */ 567 scanLines = new byte[this.width * nRows * this.bytesPerPixel 568 + nRows]; 569 570 if (this.filter == FILTER_SUB) { 571 this.leftBytes = new byte[16]; 572 } 573 if (this.filter == FILTER_UP) { 574 this.priorRow = new byte[this.width * this.bytesPerPixel]; 575 } 576 577 scanPos = 0; 578 startPos = 1; 579 for (int i = 0; i < this.width * nRows; i++) { 580 if (i % this.width == 0) { 581 scanLines[scanPos++] = (byte) this.filter; 582 startPos = scanPos; 583 } 584 scanLines[scanPos++] = (byte) ((pixels[i] >> 16) & 0xff); 585 scanLines[scanPos++] = (byte) ((pixels[i] >> 8) & 0xff); 586 scanLines[scanPos++] = (byte) ((pixels[i]) & 0xff); 587 if (this.encodeAlpha) { 588 scanLines[scanPos++] = (byte) ((pixels[i] >> 24) 589 & 0xff); 590 } 591 if ((i % this.width == this.width - 1) 592 && (this.filter != FILTER_NONE)) { 593 if (this.filter == FILTER_SUB) { 594 filterSub(scanLines, startPos, this.width); 595 } 596 if (this.filter == FILTER_UP) { 597 filterUp(scanLines, startPos, this.width); 598 } 599 } 600 } 601 602 /* 603 * Write these lines to the output area 604 */ 605 compBytes.write(scanLines, 0, scanPos); 606 607 startRow += nRows; 608 rowsLeft -= nRows; 609 } 610 compBytes.close(); 611 612 /* 613 * Write the compressed bytes 614 */ 615 compressedLines = outBytes.toByteArray(); 616 nCompressed = compressedLines.length; 617 618 this.crc.reset(); 619 this.bytePos = writeInt4(nCompressed, this.bytePos); 620 this.bytePos = writeBytes(IDAT, this.bytePos); 621 this.crc.update(IDAT); 622 this.bytePos = writeBytes(compressedLines, nCompressed, 623 this.bytePos); 624 this.crc.update(compressedLines, 0, nCompressed); 625 626 this.crcValue = this.crc.getValue(); 627 this.bytePos = writeInt4((int) this.crcValue, this.bytePos); 628 scrunch.finish(); 629 scrunch.end(); 630 return true; 631 } 632 catch (IOException e) { 633 System.err.println(e.toString()); 634 return false; 635 } 636 } 637 638 /** 639 * Write a PNG "IEND" chunk into the pngBytes array. 640 */ 641 protected void writeEnd() { 642 this.bytePos = writeInt4(0, this.bytePos); 643 this.bytePos = writeBytes(IEND, this.bytePos); 644 this.crc.reset(); 645 this.crc.update(IEND); 646 this.crcValue = this.crc.getValue(); 647 this.bytePos = writeInt4((int) this.crcValue, this.bytePos); 648 } 649 650 651 /** 652 * Set the DPI for the X axis. 653 * 654 * @param xDpi The number of dots per inch 655 */ 656 public void setXDpi(int xDpi) { 657 this.xDpi = Math.round(xDpi / INCH_IN_METER_UNIT); 658 659 } 660 661 /** 662 * Get the DPI for the X axis. 663 * 664 * @return The number of dots per inch 665 */ 666 public int getXDpi() { 667 return Math.round(this.xDpi * INCH_IN_METER_UNIT); 668 } 669 670 /** 671 * Set the DPI for the Y axis. 672 * 673 * @param yDpi The number of dots per inch 674 */ 675 public void setYDpi(int yDpi) { 676 this.yDpi = Math.round(yDpi / INCH_IN_METER_UNIT); 677 } 678 679 /** 680 * Get the DPI for the Y axis. 681 * 682 * @return The number of dots per inch 683 */ 684 public int getYDpi() { 685 return Math.round(this.yDpi * INCH_IN_METER_UNIT); 686 } 687 688 /** 689 * Set the DPI resolution. 690 * 691 * @param xDpi The number of dots per inch for the X axis. 692 * @param yDpi The number of dots per inch for the Y axis. 693 */ 694 public void setDpi(int xDpi, int yDpi) { 695 this.xDpi = Math.round(xDpi / INCH_IN_METER_UNIT); 696 this.yDpi = Math.round(yDpi / INCH_IN_METER_UNIT); 697 } 698 699 /** 700 * Write a PNG "pHYs" chunk into the pngBytes array. 701 */ 702 protected void writeResolution() { 703 if (this.xDpi > 0 && this.yDpi > 0) { 704 705 final int startPos = this.bytePos = writeInt4(9, this.bytePos); 706 this.bytePos = writeBytes(PHYS, this.bytePos); 707 this.bytePos = writeInt4(this.xDpi, this.bytePos); 708 this.bytePos = writeInt4(this.yDpi, this.bytePos); 709 this.bytePos = writeByte(1, this.bytePos); // unit is the meter. 710 711 this.crc.reset(); 712 this.crc.update(this.pngBytes, startPos, this.bytePos - startPos); 713 this.crcValue = this.crc.getValue(); 714 this.bytePos = writeInt4((int) this.crcValue, this.bytePos); 715 } 716 } 717}