Source for com.keypoint.PngEncoder

   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
  14:  * saved as a PNG file.  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., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
  38:  * USA. 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
  47:  *               Refinery Limited);
  48:  * 19-Sep-2003 : Fix for platforms using EBCDIC (contributed by Paulo Soares);
  49:  * 19-Oct-2003 : Change private fields to protected fields so that
  50:  *               PngEncoderB can inherit them (JDE)
  51:  *               Fixed bug with calculation of nRows
  52:  * 15-Aug-2008 : Added scrunch.end() in writeImageData() method - see
  53:  *               JFreeChart bug report 2037930 (David Gilbert);
  54:  */
  55: 
  56: public class PngEncoder {
  57: 
  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 filter (NONE). */
  65:     public static final int FILTER_NONE = 0;
  66: 
  67:     /** Constants for filter (SUB). */
  68:     public static final int FILTER_SUB = 1;
  69: 
  70:     /** Constants for filter (UP). */
  71:     public static final int FILTER_UP = 2;
  72: 
  73:     /** Constants for filter (LAST). */
  74:     public static final int FILTER_LAST = 2;
  75: 
  76:     /** IHDR tag. */
  77:     protected static final byte[] IHDR = {73, 72, 68, 82};
  78: 
  79:     /** IDAT tag. */
  80:     protected static final byte[] IDAT = {73, 68, 65, 84};
  81: 
  82:     /** IEND tag. */
  83:     protected static final byte[] IEND = {73, 69, 78, 68};
  84: 
  85:     /** PHYS tag. */
  86:     protected static final byte[] PHYS = {(byte)'p', (byte)'H', (byte)'Y',
  87:         (byte)'s'};
  88: 
  89:     /** The png bytes. */
  90:     protected byte[] pngBytes;
  91: 
  92:     /** The prior row. */
  93:     protected byte[] priorRow;
  94: 
  95:     /** The left bytes. */
  96:     protected byte[] leftBytes;
  97: 
  98:     /** The image. */
  99:     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: }