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