001 /* 002 * PNGCodec 003 * 004 * Copyright (c) 2003 Marco Schmidt. 005 * All rights reserved. 006 */ 007 008 package net.sourceforge.jiu.codecs; 009 010 import java.io.BufferedInputStream; 011 import java.io.DataInputStream; 012 import java.io.DataOutput; 013 import java.io.FileInputStream; 014 import java.io.InputStream; 015 import java.io.IOException; 016 import java.util.Calendar; 017 import java.util.GregorianCalendar; 018 import java.util.SimpleTimeZone; 019 import java.util.zip.CheckedInputStream; 020 import java.util.zip.Deflater; 021 import java.util.zip.InflaterInputStream; 022 import java.util.zip.CRC32; 023 import net.sourceforge.jiu.data.BilevelImage; 024 import net.sourceforge.jiu.data.Gray16Image; 025 import net.sourceforge.jiu.data.Gray8Image; 026 import net.sourceforge.jiu.data.IntegerImage; 027 import net.sourceforge.jiu.data.MemoryBilevelImage; 028 import net.sourceforge.jiu.data.MemoryGray16Image; 029 import net.sourceforge.jiu.data.MemoryGray8Image; 030 import net.sourceforge.jiu.data.MemoryPaletted8Image; 031 import net.sourceforge.jiu.data.MemoryRGB24Image; 032 import net.sourceforge.jiu.data.MemoryRGB48Image; 033 import net.sourceforge.jiu.data.Palette; 034 import net.sourceforge.jiu.data.Paletted8Image; 035 import net.sourceforge.jiu.data.PixelImage; 036 import net.sourceforge.jiu.data.RGB24Image; 037 import net.sourceforge.jiu.data.RGB48Image; 038 import net.sourceforge.jiu.data.RGBIndex; 039 import net.sourceforge.jiu.ops.MissingParameterException; 040 import net.sourceforge.jiu.ops.OperationFailedException; 041 import net.sourceforge.jiu.util.ArrayConverter; 042 043 /** 044 * An input stream that reads from an underlying stream of PNG 045 * IDAT chunks and skips all header information. 046 * PNG uses one or more IDAT chunks to store image data. 047 * The resulting stream looks like that: 048 * <code>IDAT [chunk size] [compressed data] [checksum] 049 * IDAT [chunk size] [compressed data] [checksum] ...</code> 050 * This stream class expects an input stream where the first IDAT chunk name and chunk 051 * size have been read already, the stream is thus pointing to the 052 * first byte of the first [compressed data] section. 053 * The size of that section is given to the constructor. 054 * This class then returns calls to read(), counts the bytes it has given 055 * away and, whenever a compressed data section has been consumed, it reads 056 * the IDAT chunk and stores its size, using it to determine when the 057 * next compressed data section will end. 058 * That way, for the caller the stream appears to be one large compressed 059 * section. 060 * <p> 061 * According to the PNG specs the reason for multiple IDAT chunks is as 062 * follows: 063 * <blockquote> 064 * (Multiple IDAT chunks are allowed so that encoders can work in a fixed 065 * amount of memory; typically the chunk size will correspond to the encoder's 066 * buffer size.) 067 * </blockquote> 068 * <a target="_top" href="http://www.w3.org/TR/PNG#C.IDAT">4.1.3. IDAT Image data</a> 069 * <p> 070 * If there is a more elegant approach to read multiple IDAT chunks, please 071 * let me know. 072 * However, reading everything into memory is not an option. 073 * @author Marco Schmidt 074 * @since 0.12.0 075 */ 076 class PngIdatInputStream extends InputStream 077 { 078 private static final int IDAT = 0x49444154; 079 private DataInputStream in; 080 private long bytesLeft; 081 082 public PngIdatInputStream(DataInputStream input, long bytes) 083 { 084 in = input; 085 bytesLeft = bytes; 086 } 087 088 public int read() throws IOException 089 { 090 if (bytesLeft == 0) 091 { 092 skipHeaders(); 093 } 094 bytesLeft--; 095 return in.read(); 096 } 097 098 private void skipHeaders() throws IOException 099 { 100 do 101 { 102 int crc = in.readInt(); 103 bytesLeft = in.readInt() & 0xffffffffL; 104 int type = in.readInt(); 105 if (IDAT != type) 106 { 107 throw new IOException("Expected IDAT chunk type, got " + 108 Integer.toHexString(type)); 109 } 110 } 111 while (bytesLeft == 0); 112 } 113 } 114 115 /** 116 * A codec for the Portable Network Graphics (PNG) format. 117 * Supports both loading and saving of images. 118 * <h3>Usage examples</h3> 119 * <h4>Load an image</h4> 120 * The following example code loads an image from a PNG file. 121 * Note that you could also use {@link ImageLoader} or {@link net.sourceforge.jiu.gui.awt.ToolkitLoader} 122 * which require only a single line of code and can load all formats 123 * supported by JIU, including PNG. 124 * <pre> PNGCodec codec = new PNGCodec(); 125 * codec.setFile("image.png", CodecMode.LOAD); 126 * codec.process(); 127 * PixelImage image = codec.getImage();</pre> 128 * <h4>Save an image</h4> 129 * <pre> PNGCodec codec = new PNGCodec(); 130 * codec.setFile("out.png", CodecMode.SAVE); 131 * codec.setImage(image); 132 * codec.setCompressionLevel(Deflater.BEST_COMPRESSION); 133 * codec.appendComment("Copyright (c) 1992 John Doe"); 134 * // sets last modification time to current time 135 * codec.setModification(new GregorianCalendar( 136 * new SimpleTimeZone(0, "UTC"))); 137 * codec.process();</pre> 138 * <h3>Supported storage order types</h3> 139 * <h4>Loading</h4> 140 * This codec reads both non-interlaced and Adam7 interlaced PNG files. 141 * <h4>Saving</h4> 142 * This codec only writes non-interlaced PNG files. 143 * <h3>Supported color types</h3> 144 * <h4>Loading</h4> 145 * <ul> 146 * <li>Grayscale 1 bit streams are loaded as {@link net.sourceforge.jiu.data.BilevelImage} objects, 147 * 2, 4 and 8 bit streams as {@link net.sourceforge.jiu.data.Gray8Image} and 16 bit as 148 * {@link net.sourceforge.jiu.data.Gray16Image} objects.</li> 149 * <li>Indexed 1, 2, 4 and 8 bit streams are all loaded as {@link net.sourceforge.jiu.data.Paletted8Image}.</li> 150 * <li>RGB truecolor 24 bit streams are loaded as {@link net.sourceforge.jiu.data.RGB24Image}, 151 * 48 bit streams as {@link net.sourceforge.jiu.data.RGB48Image} objects.</li> 152 * </ul> 153 * <h4>Saving</h4> 154 * <ul> 155 * <li>{@link net.sourceforge.jiu.data.BilevelImage} objects are stored as grayscale 1 bit PNG streams.</li> 156 * <li>{@link net.sourceforge.jiu.data.Paletted8Image} objects are stored as indexed 8 bit PNG streams. 157 * Images will always be stored as 8 bit files, even if the palette has only 16, 4 or 2 entries. 158 * </li> 159 * <li>{@link net.sourceforge.jiu.data.Gray8Image} objects are stored as 8 bit grayscale PNG streams.</li> 160 * <li>{@link net.sourceforge.jiu.data.Gray16Image} objects are stored as 16 bit grayscale PNG streams.</li> 161 * <li>{@link net.sourceforge.jiu.data.RGB24Image} objects are stored as 24 bit RGB truecolor PNG streams.</li> 162 * <li>{@link net.sourceforge.jiu.data.RGB48Image} objects are stored as 48 bit RGB truecolor PNG streams.</li> 163 * </ul> 164 * <h3>Transparency information</h3> 165 * PNG allows to store different types of transparency information. 166 * Full alpha channels, transparent index values, and more. 167 * Right now, this JIU codec does not make use of this information and simply 168 * skips over it when encountered. 169 * <h3>Bounds</h3> 170 * This codec regards the bounds concept. 171 * If bounds are specified with {@link #setBounds}, the codec will only load or save 172 * part of an image. 173 * <h3>Metadata</h3> 174 * <h4>Loading</h4> 175 * <ul> 176 * <li>Physical resolution information is loaded from <code>pHYs</code> chunks. 177 * Use {@link #getDpiX} and {@link #getDpiY} to retrieve that information. 178 * after the call to {@link #process}.</li> 179 * <li>Textual comments are read from <code>tEXt</code> chunks and can be retrieved 180 * with {@link #getComment} after the call to {@link #process}.</li> 181 * </ul> 182 * <h4>Saving</h4> 183 * <ul> 184 * <li>Physical resolution information (specified with {@link #setDpi}) 185 * is stored in a <code>pHYs</code> chunk.</li> 186 * <li>Textual comments (specified with {@link #appendComment}) are stored as <code>tEXt</code> chunks. 187 * The keyword used is <code>Comment</code>. 188 * Each of the {@link #getNumComments} is stored in a chunk of its own.</li> 189 * <li>Time of modification is stored in a <code>tIME</code> chunk. 190 * Use {@link #setModification(Calendar)} to give a point in time to this codec.</li> 191 * </ul> 192 * <h3>Implementation details</h3> 193 * This class relies heavily on the Java runtime library for decompression and 194 * checksum creation. 195 * <h3>Background</h3> 196 * To learn more about the PNG file format, visit its 197 * <a target="_top" href="http://www.libpng.org/pub/png/">official homepage</a>. 198 * There you can find a detailed specification, 199 * test images and existing PNG libraries and PNG-aware applications. 200 * The book <em>PNG - The Definitive Guide</em> by Greg Roelofs, published by O'Reilly, 1999, 201 * ISBN 1-56592-542-4 is a valuable source of information on PNG. 202 * It is out of print, but it can be viewed online and downloaded for offline reading 203 * in its entirety from the site. 204 * @author Marco Schmidt 205 * @since 0.12.0 206 */ 207 public class PNGCodec extends ImageCodec 208 { 209 private final int CHUNK_CRC32_IEND = 0xae426082; 210 private final int CHUNK_SIZE_IHDR = 0x0000000d; 211 private final int CHUNK_TYPE_IDAT = 0x49444154; 212 private final int CHUNK_TYPE_IEND = 0x49454e44; 213 private final int CHUNK_TYPE_IHDR = 0x49484452; 214 private final int CHUNK_TYPE_PHYS = 0x70485973; 215 private final int CHUNK_TYPE_PLTE = 0x504c5445; 216 private final int CHUNK_TYPE_TEXT = 0x74455874; 217 private final int CHUNK_TYPE_TIME = 0x74494d45; 218 private final int COLOR_TYPE_GRAY = 0; 219 private final int COLOR_TYPE_GRAY_ALPHA = 4; 220 private final int COLOR_TYPE_INDEXED = 3; 221 private final int COLOR_TYPE_RGB = 2; 222 private final int COLOR_TYPE_RGB_ALPHA = 6; 223 private final int COLOR_TYPE_ALPHA = 4; 224 private final int FILTER_TYPE_NONE = 0; 225 private final int FILTER_TYPE_SUB = 1; 226 private final int FILTER_TYPE_UP = 2; 227 private final int FILTER_TYPE_AVERAGE = 3; 228 private final int FILTER_TYPE_PAETH = 4; 229 private final int COMPRESSION_DEFLATE = 0; 230 private final int INTERLACING_NONE = 0; 231 private final int INTERLACING_ADAM7 = 1; 232 private final int FILTERING_ADAPTIVE = 0; 233 private final int MAX_TEXT_SIZE = 512; 234 private final int ADAM7_NUM_PASSES = 7; 235 private final int DEFAULT_ENCODING_MIN_IDAT_SIZE = 32 * 1024; 236 private final int[] ADAM7_COLUMN_INCREMENT = {8, 8, 4, 4, 2, 2, 1}; 237 private final int[] ADAM7_FIRST_COLUMN = {0, 4, 0, 2, 0, 1, 0}; 238 private final int[] ADAM7_FIRST_ROW = {0, 0, 4, 0, 2, 0, 1}; 239 private final int[] ADAM7_ROW_INCREMENT = {8, 8, 8, 4, 4, 2, 2}; 240 private final byte[] MAGIC_BYTES = 241 {(byte)0x89, (byte)0x50, (byte)0x4e, (byte)0x47, 242 (byte)0x0d, (byte)0x0a, (byte)0x1a, (byte)0x0a}; 243 244 private boolean alpha; 245 private byte[][] buffers; 246 private int bpp; 247 private CRC32 checksum; 248 private CheckedInputStream checkedIn; 249 private int chunkCounter; 250 private int colorType; 251 private int compressionType; 252 private int currentBufferIndex; 253 private int deflateLevel = Deflater.DEFAULT_COMPRESSION; 254 private int deflateStrategy = Deflater.DEFAULT_STRATEGY; 255 private int encodingMinIdatSize = DEFAULT_ENCODING_MIN_IDAT_SIZE; 256 private int filterType; 257 private boolean hasIhdr; 258 private int height; 259 private IntegerImage image; 260 private DataInputStream in; 261 private InflaterInputStream infl; 262 private int interlaceType; 263 private Calendar modification; 264 private int numChannels; 265 private DataOutput out; 266 private Palette palette; 267 private int precision; 268 private int previousBufferIndex; 269 private byte[] streamInBuffer; 270 private byte[] streamOutBuffer; 271 private int width; 272 273 /** 274 * Allocates the right image to private field <code>image</code>, 275 * taking into consideration the fields width, height, precision and colorType. 276 * Assumes that an IHDR chunk has been read and the above mentioned 277 * fields have been initialized and checked for their validity. 278 */ 279 private void allocateImage() throws InvalidFileStructureException, UnsupportedTypeException 280 { 281 setBoundsIfNecessary(width, height); 282 int w = getBoundsWidth(); 283 int h = getBoundsHeight(); 284 if (colorType == COLOR_TYPE_GRAY || colorType == COLOR_TYPE_GRAY_ALPHA) 285 { 286 if (precision == 1) 287 { 288 image = new MemoryBilevelImage(w, h); 289 } 290 else 291 if (precision <= 8) 292 { 293 image = new MemoryGray8Image(w, h); 294 } 295 else 296 if (precision == 16) 297 { 298 image = new MemoryGray16Image(w, h); 299 } 300 } 301 else 302 if (colorType == COLOR_TYPE_INDEXED) 303 { 304 if (palette == null) 305 { 306 throw new InvalidFileStructureException("No palette found when trying to load indexed image."); 307 } 308 image = new MemoryPaletted8Image(w, h, palette); 309 } 310 else 311 if (colorType == COLOR_TYPE_RGB || colorType == COLOR_TYPE_RGB_ALPHA) 312 { 313 if (precision == 8) 314 { 315 image = new MemoryRGB24Image(w, h); 316 } 317 else 318 { 319 image = new MemoryRGB48Image(w, h); 320 } 321 } 322 else 323 { 324 throw new UnsupportedTypeException("Unsupported image type encountered"); 325 } 326 } 327 328 /** 329 * Checks values {@link #precision} and {@link #colorType}. 330 * A lot of combinations possibly found in an IHDR chunk 331 * are invalid. 332 * Also initializes {@link #alpha} and {@link #numChannels}. 333 * @throws UnsupportedTypeException if an invalid combination 334 * of precision and colorType is found 335 */ 336 private void checkColorTypeAndPrecision() throws UnsupportedTypeException 337 { 338 if (colorType != COLOR_TYPE_GRAY && 339 colorType != COLOR_TYPE_RGB && 340 colorType != COLOR_TYPE_INDEXED && 341 colorType != COLOR_TYPE_GRAY_ALPHA && 342 colorType != COLOR_TYPE_RGB_ALPHA) 343 { 344 throw new UnsupportedTypeException("Not a valid color type: " + colorType); 345 } 346 if (precision != 1 && precision != 2 && precision != 4 && precision != 8 && precision != 16) 347 { 348 throw new UnsupportedTypeException("Invalid precision value: " + precision); 349 } 350 if (colorType == COLOR_TYPE_INDEXED && precision > 8) 351 { 352 throw new UnsupportedTypeException("More than eight bits of precision are not allowed for indexed images."); 353 } 354 if (colorType == COLOR_TYPE_RGB && precision < 8) 355 { 356 throw new UnsupportedTypeException("Less than eight bits of precision are not allowed for RGB images."); 357 } 358 alpha = (colorType & COLOR_TYPE_ALPHA) != 0; 359 if (colorType == COLOR_TYPE_RGB || 360 colorType == COLOR_TYPE_RGB_ALPHA) 361 { 362 numChannels = 3; 363 } 364 else 365 { 366 numChannels = 1; 367 } 368 bpp = computeBytesPerRow(1); 369 } 370 371 /** 372 * Computes a number of bytes for a given number of pixels, 373 * regarding precision and availability of an alpha channel. 374 * @param numPixels the number of pixels for which the number 375 * of bytes necessary to store them is to be computed 376 * @return number of bytes 377 */ 378 private int computeBytesPerRow(int numPixels) 379 { 380 if (precision < 8) 381 { 382 return (numPixels + ((8 / precision) - 1)) / (8 / precision); 383 } 384 else 385 { 386 return (numChannels + (alpha ? 1 : 0)) * (precision / 8) * numPixels; 387 } 388 } 389 390 private int computeColumnsAdam7(int pass) 391 { 392 switch(pass) 393 { 394 case(0): return (width + 7) / 8; 395 case(1): return (width + 3) / 8; 396 case(2): return (width + 3) / 4; 397 case(3): return (width + 1) / 4; 398 case(4): return (width + 1) / 2; 399 case(5): return width / 2; 400 case(6): return width; 401 default: throw new IllegalArgumentException("Not a valid pass index: " + pass); 402 } 403 } 404 405 private void fillRowBuffer(int y, byte[] row, int offs) 406 { 407 PixelImage image = getImage(); 408 int x1 = getBoundsX1(); 409 int x2 = getBoundsX2(); 410 int w = getBoundsWidth(); 411 if (image instanceof BilevelImage) 412 { 413 BilevelImage bilevelImage = (BilevelImage)image; 414 bilevelImage.getPackedBytes(x1, y, w, row, offs, 0); 415 } 416 else 417 if (image instanceof Gray16Image) 418 { 419 Gray16Image grayImage = (Gray16Image)image; 420 while (w-- > 0) 421 { 422 short sample = grayImage.getShortSample(x1++, y); 423 ArrayConverter.setShortBE(row, offs, sample); 424 offs += 2; 425 } 426 } 427 else 428 if (image instanceof Gray8Image) 429 { 430 Gray8Image grayImage = (Gray8Image)image; 431 grayImage.getByteSamples(0, getBoundsX1(), y, getBoundsWidth(), 1, row, offs); 432 } 433 else 434 if (image instanceof Paletted8Image) 435 { 436 Paletted8Image palImage = (Paletted8Image)image; 437 palImage.getByteSamples(0, getBoundsX1(), y, getBoundsWidth(), 1, row, offs); 438 } 439 else 440 if (image instanceof RGB24Image) 441 { 442 RGB24Image rgbImage = (RGB24Image)image; 443 while (w-- > 0) 444 { 445 row[offs++] = rgbImage.getByteSample(RGBIndex.INDEX_RED, x1, y); 446 row[offs++] = rgbImage.getByteSample(RGBIndex.INDEX_GREEN, x1, y); 447 row[offs++] = rgbImage.getByteSample(RGBIndex.INDEX_BLUE, x1, y); 448 x1++; 449 } 450 } 451 else 452 if (image instanceof RGB48Image) 453 { 454 RGB48Image rgbImage = (RGB48Image)image; 455 while (w-- > 0) 456 { 457 short sample = rgbImage.getShortSample(RGBIndex.INDEX_RED, x1, y); 458 ArrayConverter.setShortBE(row, offs, sample); 459 offs += 2; 460 461 sample = rgbImage.getShortSample(RGBIndex.INDEX_GREEN, x1, y); 462 ArrayConverter.setShortBE(row, offs, sample); 463 offs += 2; 464 465 sample = rgbImage.getShortSample(RGBIndex.INDEX_BLUE, x1, y); 466 ArrayConverter.setShortBE(row, offs, sample); 467 offs += 2; 468 469 x1++; 470 } 471 } 472 } 473 474 /** 475 * Creates a four-letter String from the parameter, an <code>int</code> 476 * value, supposed to be storing a chunk name. 477 * @return the chunk name 478 */ 479 private static String getChunkName(int chunk) 480 { 481 StringBuffer result = new StringBuffer(4); 482 for (int i = 24; i >= 0; i -= 8) 483 { 484 result.append((char)((chunk >> i) & 0xff)); 485 } 486 return result.toString(); 487 } 488 489 public String getFormatName() 490 { 491 return "Portable Network Graphics (PNG)"; 492 } 493 494 public String[] getMimeTypes() 495 { 496 return new String[] {"image/png"}; 497 } 498 499 private static int getPaeth(byte l, byte u, byte nw) 500 { 501 int a = l & 0xff; 502 int b = u & 0xff; 503 int c = nw & 0xff; 504 int p = a + b - c; 505 int pa = p - a; 506 if (pa < 0) 507 { 508 pa = -pa; 509 } 510 int pb = p - b; 511 if (pb < 0) 512 { 513 pb = -pb; 514 } 515 int pc = p - c; 516 if (pc < 0) 517 { 518 pc = -pc; 519 } 520 if (pa <= pb && pa <= pc) 521 { 522 return a; 523 } 524 if (pb <= pc) 525 { 526 return b; 527 } 528 return c; 529 } 530 531 private void inflateBytes(byte[] buffer, int numBytes) throws InvalidFileStructureException, IOException 532 { 533 int offset = 0; 534 do 535 { 536 try 537 { 538 int toRead = numBytes - offset; 539 int numRead = infl.read(buffer, offset, toRead); 540 if (numRead < 0) 541 { 542 throw new InvalidFileStructureException("Cannot fill buffer"); 543 } 544 offset += numRead; 545 } 546 catch (IOException ioe) 547 { 548 throw new InvalidFileStructureException("Stopped decompressing " + ioe.toString()); 549 } 550 } 551 while (offset != numBytes); 552 } 553 554 public boolean isLoadingSupported() 555 { 556 return true; 557 } 558 559 public boolean isSavingSupported() 560 { 561 return true; 562 } 563 564 private void load() throws 565 InvalidFileStructureException, 566 IOException, 567 UnsupportedTypeException, 568 WrongFileFormatException 569 { 570 byte[] magic = new byte[MAGIC_BYTES.length]; 571 in.readFully(magic); 572 for (int i = 0; i < MAGIC_BYTES.length; i++) 573 { 574 if (magic[i] != MAGIC_BYTES[i]) 575 { 576 throw new WrongFileFormatException("Not a valid PNG input " + 577 "stream, wrong magic byte sequence."); 578 } 579 } 580 chunkCounter = 0; 581 do 582 { 583 loadChunk(); 584 chunkCounter++; 585 } 586 while (image == null); 587 close(); 588 setImage(image); 589 } 590 591 private void loadChunk() throws InvalidFileStructureException, IOException, UnsupportedTypeException 592 { 593 /* 594 * read chunk size; according to the PNG specs, the size value must not be larger 595 * than 2^31 - 1; to be safe, we treat the value as an unsigned 596 * 32 bit value anyway 597 */ 598 long chunkSize = in.readInt() & 0xffffffffL; 599 checksum.reset(); 600 int chunkName = in.readInt(); 601 // first chunk must be IHDR 602 if (chunkCounter == 0 && chunkName != CHUNK_TYPE_IHDR) 603 { 604 throw new InvalidFileStructureException("First chunk was not IHDR but " + getChunkName(chunkName)); 605 } 606 switch (chunkName) 607 { 608 // image data chunk 609 case(CHUNK_TYPE_IDAT): 610 { 611 loadImage(chunkSize); 612 break; 613 } 614 // end of image chunk 615 case(CHUNK_TYPE_IEND): 616 { 617 throw new InvalidFileStructureException("Reached IEND chunk but could not load image."); 618 } 619 case(CHUNK_TYPE_IHDR): 620 { 621 if (hasIhdr) 622 { 623 throw new InvalidFileStructureException("More than one IHDR chunk found."); 624 } 625 if (chunkCounter != 0) 626 { 627 throw new InvalidFileStructureException("IHDR chunk must be first; found to be chunk #" + (chunkCounter + 1)); 628 } 629 if (chunkSize != CHUNK_SIZE_IHDR) 630 { 631 throw new InvalidFileStructureException("Expected PNG " + 632 "IHDR chunk length to be " + CHUNK_SIZE_IHDR + ", got " + 633 chunkSize + "."); 634 } 635 hasIhdr = true; 636 loadImageHeader(); 637 break; 638 } 639 case(CHUNK_TYPE_PHYS): 640 { 641 if (chunkSize == 9) 642 { 643 byte[] phys = new byte[9]; 644 in.readFully(phys); 645 int x = ArrayConverter.getIntBE(phys, 0); 646 int y = ArrayConverter.getIntBE(phys, 4); 647 if (phys[8] == 1) 648 { 649 // unit is meters 650 final double INCHES_PER_METER = 100 / 2.54; 651 setDpi((int)(x / INCHES_PER_METER), (int)(y / INCHES_PER_METER)); 652 } 653 } 654 else 655 { 656 skip(chunkSize); 657 } 658 break; 659 } 660 case(CHUNK_TYPE_PLTE): 661 { 662 if ((chunkSize % 3) != 0) 663 { 664 throw new InvalidFileStructureException("Not a valid palette chunk size: " + chunkSize); 665 } 666 loadPalette(chunkSize / 3); 667 break; 668 } 669 case(CHUNK_TYPE_TEXT): 670 { 671 if (chunkSize == 0) 672 { 673 } 674 else 675 if (chunkSize > MAX_TEXT_SIZE) 676 { 677 skip(chunkSize); 678 } 679 else 680 { 681 StringBuffer text = new StringBuffer((int)chunkSize); 682 int i = 0; 683 char c; 684 do 685 { 686 c = (char)in.read(); 687 if (c == 0) 688 { 689 skip(chunkSize - i - 1); 690 break; 691 } 692 text.append(c); 693 i++; 694 } 695 while (i < chunkSize); 696 //System.out.println("text=\"" + text.toString() + "\""); 697 } 698 break; 699 } 700 default: 701 { 702 skip(chunkSize); 703 } 704 } 705 int createdChecksum = (int)checksum.getValue(); 706 if (image == null) 707 { 708 // this code doesn't work anymore if we have just read an image 709 int chunkChecksum = in.readInt(); 710 if (createdChecksum != chunkChecksum) 711 { 712 throw new InvalidFileStructureException("Checksum created on chunk " + 713 getChunkName(chunkName) + " " + Integer.toHexString(createdChecksum) + 714 " is not equal to checksum read from stream " + 715 Integer.toHexString(chunkChecksum) + 716 "; file is corrupted."); 717 } 718 } 719 } 720 721 /** 722 * Load an image from the current position in the file. 723 * Assumes the last things read from input are an IDAT chunk type and 724 * its size, which is the sole argument of this method. 725 * @param chunkSize size of the IDAT chunk that was just read 726 * @throws InvalidFileStructureException if there are values in the PNG stream that make it invalid 727 * @throws IOException if there were I/O errors when reading 728 * @throws UnsupportedTypeException if something was encountered in the stream that is valid but not supported by this codec 729 */ 730 private void loadImage(long chunkSize) throws InvalidFileStructureException, IOException, UnsupportedTypeException 731 { 732 // allocate two byte buffers for current and previous row 733 buffers = new byte[2][]; 734 int numBytes = computeBytesPerRow(width); 735 currentBufferIndex = 0; 736 previousBufferIndex = 1; 737 buffers[currentBufferIndex] = new byte[numBytes]; 738 buffers[previousBufferIndex] = new byte[numBytes]; 739 for (int i = 0; i < buffers[previousBufferIndex].length; i++) 740 { 741 buffers[previousBufferIndex][i] = (byte)0; 742 } 743 // allocate the correct type of image object for the image type read in the IHDR chunk 744 allocateImage(); 745 // create a PngIdatInputStream which will skip header information when 746 // multiple IDAT chunks are in the input stream 747 infl = new InflaterInputStream(new PngIdatInputStream(in, chunkSize)); 748 switch(interlaceType) 749 { 750 case(INTERLACING_NONE): 751 { 752 loadImageNonInterlaced(); 753 break; 754 } 755 case(INTERLACING_ADAM7): 756 { 757 loadImageInterlacedAdam7(); 758 break; 759 } 760 } 761 } 762 763 /** 764 * Reads data from an IHDR chunk and initializes private fields with it. 765 * Does a lot of checking if read values are valid and supported by this class. 766 * @throws IOException 767 * @throws InvalidFileStructureException 768 * @throws UnsupportedTypeException 769 */ 770 private void loadImageHeader() throws IOException, InvalidFileStructureException, UnsupportedTypeException 771 { 772 // WIDTH -- horizontal resolution 773 width = in.readInt(); 774 if (width < 1) 775 { 776 throw new InvalidFileStructureException("Width must be larger than 0; got " + width); 777 } 778 // HEIGHT -- vertical resolution 779 height = in.readInt(); 780 if (height < 1) 781 { 782 throw new InvalidFileStructureException("Height must be larger than 0; got " + height); 783 } 784 // PRECISION -- bits per sample 785 precision = in.read(); 786 // COLOR TYPE -- indexed, paletted, grayscale, optionally alpha 787 colorType = in.read(); 788 // check for invalid combinations of color type and precision 789 // and initialize alpha and numChannels 790 checkColorTypeAndPrecision(); 791 // COMPRESSION TYPE -- only Deflate is defined 792 compressionType = in.read(); 793 if (compressionType != COMPRESSION_DEFLATE) 794 { 795 throw new UnsupportedTypeException("Unsupported compression type: " + 796 compressionType + "."); 797 } 798 // FILTER TYPE -- only Adaptive is defined 799 filterType = in.read(); 800 if (filterType != FILTERING_ADAPTIVE) 801 { 802 throw new UnsupportedTypeException("Only 'adaptive filtering' is supported right now; got " + filterType); 803 } 804 // INTERLACE TYPE -- order of storage of image data 805 interlaceType = in.read(); 806 if (interlaceType != INTERLACING_NONE && 807 interlaceType != INTERLACING_ADAM7) 808 { 809 throw new UnsupportedTypeException("Only 'no interlacing' and 'Adam7 interlacing' are supported; got " + interlaceType); 810 } 811 } 812 813 private void loadImageInterlacedAdam7() throws InvalidFileStructureException, IOException, UnsupportedTypeException 814 { 815 final int TOTAL_LINES = ADAM7_NUM_PASSES * height; 816 for (int pass = 0; pass < ADAM7_NUM_PASSES; pass++) 817 { 818 currentBufferIndex = 0; 819 previousBufferIndex = 1; 820 byte[] previousBuffer = buffers[previousBufferIndex]; 821 for (int x = 0; x < previousBuffer.length; x++) 822 { 823 previousBuffer[x] = 0; 824 } 825 int y = ADAM7_FIRST_ROW[pass]; 826 int destY = y - getBoundsY1(); 827 int numColumns = computeColumnsAdam7(pass); 828 if (numColumns == 0) 829 { 830 // this pass contains no data; skip to next pass 831 setProgress((pass + 1) * height, TOTAL_LINES); 832 continue; 833 } 834 int numBytes = computeBytesPerRow(numColumns); 835 while (y < height) 836 { 837 previousBuffer = buffers[previousBufferIndex]; 838 byte[] currentBuffer = buffers[currentBufferIndex]; 839 int rowFilterType = readFilterType(); 840 inflateBytes(currentBuffer, numBytes); 841 reverseFilter(rowFilterType, currentBuffer, previousBuffer, numBytes); 842 if (isRowRequired(y)) 843 { 844 storeInterlacedAdam7(pass, destY, currentBuffer); 845 } 846 int progressY = y; 847 if (pass > 0) 848 { 849 progressY += pass * height; 850 } 851 setProgress(progressY, TOTAL_LINES); 852 y += ADAM7_ROW_INCREMENT[pass]; 853 destY += ADAM7_ROW_INCREMENT[pass]; 854 currentBufferIndex = 1 - currentBufferIndex; 855 previousBufferIndex = 1 - previousBufferIndex; 856 } 857 } 858 } 859 860 private void loadImageNonInterlaced() throws InvalidFileStructureException, IOException, UnsupportedTypeException 861 { 862 int linesToRead = getBoundsY2() + 1; 863 int rowLength = computeBytesPerRow(width); 864 for (int y = 0, destY = - getBoundsY1(); y <= getBoundsY2(); y++, destY++) 865 { 866 byte[] currentBuffer = buffers[currentBufferIndex]; 867 byte[] previousBuffer = buffers[previousBufferIndex]; 868 int rowFilterType = readFilterType(); 869 inflateBytes(currentBuffer, rowLength); 870 reverseFilter(rowFilterType, currentBuffer, previousBuffer, rowLength); 871 if (isRowRequired(y)) 872 { 873 storeNonInterlaced(destY, currentBuffer); 874 } 875 setProgress(y, linesToRead); 876 previousBufferIndex = 1 - previousBufferIndex; 877 currentBufferIndex = 1 - currentBufferIndex; 878 } 879 } 880 881 private void loadPalette(long numEntries) throws InvalidFileStructureException, IOException 882 { 883 if (palette != null) 884 { 885 throw new InvalidFileStructureException("More than one palette in input stream."); 886 } 887 if (numEntries < 1) 888 { 889 throw new InvalidFileStructureException("Number of palette entries must be at least 1."); 890 } 891 if (numEntries > 256) 892 { 893 throw new InvalidFileStructureException("Number of palette entries larger than 256: " + numEntries); 894 } 895 palette = new Palette((int)numEntries); 896 int index = 0; 897 do 898 { 899 palette.putSample(Palette.INDEX_RED, index, in.read() & 0xff); 900 palette.putSample(Palette.INDEX_GREEN, index, in.read() & 0xff); 901 palette.putSample(Palette.INDEX_BLUE, index, in.read() & 0xff); 902 index++; 903 } 904 while (index != numEntries); 905 } 906 907 public static void main(String[] args) throws Exception 908 { 909 PNGCodec codec = new PNGCodec(); 910 codec.setFile(args[0], CodecMode.LOAD); 911 codec.process(); 912 codec.close(); 913 PixelImage image = codec.getImage(); 914 codec = new PNGCodec(); 915 codec.setFile(args[1], CodecMode.SAVE); 916 codec.setImage(image); 917 codec.setDpi(300, 300); 918 codec.appendComment("Test comment #1."); 919 codec.appendComment("And test comment #2."); 920 codec.setModification(new GregorianCalendar(new SimpleTimeZone(0, "UTC"))); 921 codec.process(); 922 codec.close(); 923 } 924 925 public void process() throws 926 InvalidFileStructureException, 927 MissingParameterException, 928 OperationFailedException, 929 UnsupportedTypeException, 930 WrongFileFormatException 931 { 932 initModeFromIOObjects(); 933 if (getMode() == CodecMode.LOAD) 934 { 935 try 936 { 937 if (getImageIndex() != 0) 938 { 939 throw new InvalidImageIndexException("PNG streams can only store one image; " + 940 "index " + getImageIndex() + " is thus not valid."); 941 } 942 InputStream input = getInputStream(); 943 if (input == null) 944 { 945 throw new MissingParameterException("InputStream object missing."); 946 } 947 checksum = new CRC32(); 948 checkedIn = new CheckedInputStream(input, checksum); 949 in = new DataInputStream(checkedIn); 950 load(); 951 } 952 catch (IOException ioe) 953 { 954 throw new OperationFailedException("I/O failure: " + ioe.toString()); 955 } 956 } 957 else 958 if (getMode() == CodecMode.SAVE) 959 { 960 try 961 { 962 PixelImage image = getImage(); 963 if (image == null) 964 { 965 throw new MissingParameterException("Need image for saving."); 966 } 967 out = getOutputAsDataOutput(); 968 if (out == null) 969 { 970 throw new MissingParameterException("Could not retrieve non-null DataOutput object for saving."); 971 } 972 setBoundsIfNecessary(image.getWidth(), image.getHeight()); 973 save(); 974 } 975 catch (IOException ioe) 976 { 977 throw new OperationFailedException("I/O failure: " + ioe.toString()); 978 } 979 } 980 else 981 { 982 throw new OperationFailedException("Unknown codec mode: " + getMode()); 983 } 984 } 985 986 private int readFilterType() throws InvalidFileStructureException, IOException 987 { 988 int filterType = infl.read(); 989 if (filterType >= 0 && filterType <= 4) 990 { 991 return filterType; 992 } 993 else 994 { 995 throw new InvalidFileStructureException("Valid filter types are from 0 to 4; got " + filterType); 996 } 997 } 998 private void reverseFilter(int rowFilterType, byte[] buffer, byte[] prev, int numBytes) throws UnsupportedTypeException 999 { 1000 switch(rowFilterType) 1001 { 1002 case(FILTER_TYPE_NONE): 1003 { 1004 break; 1005 } 1006 case(FILTER_TYPE_SUB): 1007 { 1008 for (int x = 0, px = -bpp; x < numBytes; x++, px++) 1009 { 1010 byte currXMinusBpp; 1011 if (px < 0) 1012 { 1013 currXMinusBpp = 0; 1014 } 1015 else 1016 { 1017 currXMinusBpp = buffer[px]; 1018 } 1019 buffer[x] = (byte)(buffer[x] + currXMinusBpp); 1020 } 1021 break; 1022 } 1023 case(FILTER_TYPE_UP): 1024 { 1025 for (int x = 0; x < numBytes; x++) 1026 { 1027 buffer[x] = (byte)(buffer[x] + prev[x]); 1028 } 1029 break; 1030 } 1031 case(FILTER_TYPE_AVERAGE): 1032 { 1033 for (int x = 0, px = -bpp; x < numBytes; x++, px++) 1034 { 1035 int currX = buffer[x] & 0xff; 1036 int currXMinus1; 1037 if (px < 0) 1038 { 1039 currXMinus1 = 0; 1040 } 1041 else 1042 { 1043 currXMinus1 = buffer[px] & 0xff; 1044 } 1045 int prevX = prev[x] & 0xff; 1046 int result = currX + ((currXMinus1 + prevX) / 2); 1047 byte byteResult = (byte)result; 1048 buffer[x] = byteResult; 1049 } 1050 break; 1051 } 1052 case(FILTER_TYPE_PAETH): 1053 { 1054 for (int x = 0, px = -bpp; x < numBytes; x++, px++) 1055 { 1056 byte currXMinusBpp; 1057 byte prevXMinusBpp; 1058 if (px < 0) 1059 { 1060 currXMinusBpp = 0; 1061 prevXMinusBpp = 0; 1062 } 1063 else 1064 { 1065 currXMinusBpp = buffer[px]; 1066 prevXMinusBpp = prev[px]; 1067 } 1068 buffer[x] = (byte)(buffer[x] + getPaeth(currXMinusBpp, prev[x], prevXMinusBpp)); 1069 } 1070 break; 1071 } 1072 default: 1073 { 1074 throw new UnsupportedTypeException("Unknown filter type: " + rowFilterType); 1075 } 1076 } 1077 } 1078 1079 private void save() throws IOException 1080 { 1081 // write 8 byte PNG signature 1082 out.write(MAGIC_BYTES); 1083 // write IHDR (image header) chunk 1084 saveIhdrChunk(); 1085 // write pHYs chunk (physical resolution) if data is available 1086 savePhysChunk(); 1087 // write tEXt chunks if comments are available 1088 saveTextChunks(); 1089 // write tIME chunk if modification time was set 1090 saveTimeChunk(); 1091 // write PLTE chunk if necessary 1092 savePlteChunk(); 1093 // write IDAT chunk 1094 saveImage(); 1095 // write IEND chunk 1096 saveIendChunk(); 1097 close(); 1098 } 1099 1100 private void saveChunk(int chunkType, int chunkSize, byte[] data) throws IOException 1101 { 1102 // set up array with chunk size and type 1103 byte[] intArray = new byte[8]; 1104 ArrayConverter.setIntBE(intArray, 0, chunkSize); 1105 ArrayConverter.setIntBE(intArray, 4, chunkType); 1106 // write chunk size, type and data 1107 out.write(intArray, 0, 8); 1108 out.write(data, 0, chunkSize); 1109 // create checksum on type and data 1110 CRC32 checksum = new CRC32(); 1111 checksum.reset(); 1112 checksum.update(intArray, 4, 4); 1113 checksum.update(data, 0, chunkSize); 1114 // put checksum into byte array 1115 ArrayConverter.setIntBE(intArray, 0, (int)checksum.getValue()); 1116 // and write it to output 1117 out.write(intArray, 0, 4); 1118 } 1119 1120 private void saveIendChunk() throws IOException 1121 { 1122 out.writeInt(0); 1123 out.writeInt(CHUNK_TYPE_IEND); 1124 out.writeInt(CHUNK_CRC32_IEND); 1125 } 1126 1127 private void saveIhdrChunk() throws IOException 1128 { 1129 byte[] buffer = new byte[CHUNK_SIZE_IHDR]; 1130 width = getBoundsWidth(); 1131 ArrayConverter.setIntBE(buffer, 0, width); 1132 height = getBoundsHeight(); 1133 ArrayConverter.setIntBE(buffer, 4, height); 1134 PixelImage image = getImage(); 1135 alpha = false; 1136 numChannels = 1; 1137 if (image instanceof BilevelImage) 1138 { 1139 precision = 1; 1140 colorType = COLOR_TYPE_GRAY; 1141 } 1142 else 1143 if (image instanceof Gray16Image) 1144 { 1145 precision = 16; 1146 colorType = COLOR_TYPE_GRAY; 1147 } 1148 else 1149 if (image instanceof Gray8Image) 1150 { 1151 precision = 8; 1152 colorType = COLOR_TYPE_GRAY; 1153 } 1154 else 1155 if (image instanceof Paletted8Image) 1156 { 1157 precision = 8; 1158 colorType = COLOR_TYPE_INDEXED; 1159 } 1160 else 1161 if (image instanceof RGB24Image) 1162 { 1163 numChannels = 3; 1164 precision = 8; 1165 colorType = COLOR_TYPE_RGB; 1166 } 1167 else 1168 if (image instanceof RGB48Image) 1169 { 1170 numChannels = 3; 1171 precision = 16; 1172 colorType = COLOR_TYPE_RGB; 1173 } 1174 buffer[8] = (byte)precision; 1175 buffer[9] = (byte)colorType; 1176 compressionType = COMPRESSION_DEFLATE; 1177 buffer[10] = (byte)compressionType; 1178 filterType = FILTERING_ADAPTIVE; 1179 buffer[11] = (byte)filterType; 1180 interlaceType = INTERLACING_NONE; 1181 buffer[12] = (byte)interlaceType; 1182 saveChunk(CHUNK_TYPE_IHDR, CHUNK_SIZE_IHDR, buffer); 1183 } 1184 1185 private void saveImage() throws IOException 1186 { 1187 switch(interlaceType) 1188 { 1189 case(INTERLACING_NONE): 1190 { 1191 saveImageNonInterlaced(); 1192 break; 1193 } 1194 } 1195 } 1196 1197 private void saveImageNonInterlaced() throws IOException 1198 { 1199 PixelImage image = getImage(); 1200 int bytesPerRow = computeBytesPerRow(getBoundsWidth()); 1201 byte[] rowBuffer = new byte[bytesPerRow + 1]; 1202 byte[] outBuffer = new byte[Math.max(encodingMinIdatSize, bytesPerRow + 1)]; 1203 int outOffset = 0; 1204 int numDeflated; 1205 Deflater defl = new Deflater(deflateLevel); 1206 for (int y = getBoundsY1(); y <= getBoundsY2(); y++) 1207 { 1208 // fill row buffer 1209 rowBuffer[0] = 0; // row filter 'None' 1210 fillRowBuffer(y, rowBuffer, 1); 1211 // give it to compressor 1212 defl.setInput(rowBuffer); 1213 // store compressed data in outBuffer 1214 do 1215 { 1216 numDeflated = defl.deflate(outBuffer, outOffset, outBuffer.length - outOffset); 1217 outOffset += numDeflated; 1218 if (outOffset == outBuffer.length) 1219 { 1220 saveChunk(CHUNK_TYPE_IDAT, outOffset, outBuffer); 1221 outOffset = 0; 1222 } 1223 } 1224 while (numDeflated > 0); 1225 setProgress(y - getBoundsY1(), getBoundsHeight()); 1226 } 1227 // tell Deflater that it got all the input 1228 defl.finish(); 1229 // retrieve remaining compressed data from defl to outBuffer 1230 do 1231 { 1232 numDeflated = defl.deflate(outBuffer, outOffset, outBuffer.length - outOffset); 1233 outOffset += numDeflated; 1234 if (outOffset == outBuffer.length) 1235 { 1236 saveChunk(CHUNK_TYPE_IDAT, outOffset, outBuffer); 1237 outOffset = 0; 1238 } 1239 } 1240 while (numDeflated > 0); 1241 // write final IDAT chunk if necessary 1242 if (outOffset > 0) 1243 { 1244 saveChunk(CHUNK_TYPE_IDAT, outOffset, outBuffer); 1245 } 1246 } 1247 1248 private void savePhysChunk() throws IOException 1249 { 1250 int dpiX = getDpiX(); 1251 int dpiY = getDpiY(); 1252 if (dpiX < 1 || dpiY < 1) 1253 { 1254 return; 1255 } 1256 byte[] data = new byte[9]; 1257 int ppuX = (int)(dpiX * (100 / 2.54)); 1258 int ppuY = (int)(dpiY * (100 / 2.54)); 1259 ArrayConverter.setIntBE(data, 0, ppuX); 1260 ArrayConverter.setIntBE(data, 4, ppuY); 1261 data[8] = 1; // unit is the meter 1262 saveChunk(CHUNK_TYPE_PHYS, data.length, data); 1263 } 1264 1265 private void savePlteChunk() throws IOException 1266 { 1267 if (colorType != COLOR_TYPE_INDEXED) 1268 { 1269 return; 1270 } 1271 Paletted8Image image = (Paletted8Image)getImage(); 1272 Palette pal = image.getPalette(); 1273 int numEntries = pal.getNumEntries(); 1274 byte[] data = new byte[numEntries * 3]; 1275 for (int i = 0, j = 0; i < numEntries; i++, j += 3) 1276 { 1277 data[j] = (byte)pal.getSample(RGBIndex.INDEX_RED, i); 1278 data[j + 1] = (byte)pal.getSample(RGBIndex.INDEX_GREEN, i); 1279 data[j + 2] = (byte)pal.getSample(RGBIndex.INDEX_BLUE, i); 1280 } 1281 saveChunk(CHUNK_TYPE_PLTE, data.length, data); 1282 } 1283 1284 private void saveTextChunks() throws IOException 1285 { 1286 int index = 0; 1287 while (index < getNumComments()) 1288 { 1289 String comment = getComment(index++); 1290 comment = "Comment\000" + comment; 1291 byte[] data = comment.getBytes("ISO-8859-1"); 1292 saveChunk(CHUNK_TYPE_TEXT, data.length, data); 1293 } 1294 } 1295 1296 private void saveTimeChunk() throws IOException 1297 { 1298 if (modification == null) 1299 { 1300 return; 1301 } 1302 byte[] data = new byte[7]; 1303 ArrayConverter.setShortBE(data, 0, (short)modification.get(Calendar.YEAR)); 1304 data[2] = (byte)(modification.get(Calendar.MONTH) + 1); 1305 data[3] = (byte)modification.get(Calendar.DAY_OF_MONTH); 1306 data[4] = (byte)modification.get(Calendar.HOUR_OF_DAY); 1307 data[5] = (byte)modification.get(Calendar.MINUTE); 1308 data[6] = (byte)modification.get(Calendar.SECOND); 1309 saveChunk(CHUNK_TYPE_TIME, data.length, data); 1310 } 1311 1312 /** 1313 * Sets the compression level to be used with the underlying 1314 * {@link java.util.zip.Deflater} object which does the compression. 1315 * If no value is specified, {@link java.util.zip.Deflater#DEFAULT_COMPRESSION} 1316 * is used. 1317 * @param newLevel compression level, from 0 to 9, 0 being fastest 1318 * and compressing worst and 9 offering highest compression and taking 1319 * the most time 1320 */ 1321 public void setCompressionLevel(int newLevel) 1322 { 1323 if (newLevel >= 0 && newLevel <= 9) 1324 { 1325 deflateLevel = newLevel; 1326 } 1327 else 1328 { 1329 throw new IllegalArgumentException("Compression level must be from 0..9; got " + newLevel); 1330 } 1331 } 1332 1333 /** 1334 * Sets the compression strategy to be used with the underlying 1335 * {@link java.util.zip.Deflater} object which does the compression. 1336 * If no value is specified, {@link java.util.zip.Deflater#DEFAULT_STRATEGY} 1337 * is used. 1338 * @param newStrategy one of Deflater's strategy values: 1339 * {@link java.util.zip.Deflater#DEFAULT_STRATEGY}, 1340 * {@link java.util.zip.Deflater#FILTERED}, 1341 * {@link java.util.zip.Deflater#HUFFMAN_ONLY} 1342 */ 1343 public void setCompressionStrategy(int newStrategy) 1344 { 1345 if (newStrategy == Deflater.FILTERED || 1346 newStrategy == Deflater.DEFAULT_STRATEGY || 1347 newStrategy == Deflater.HUFFMAN_ONLY) 1348 { 1349 deflateStrategy = newStrategy; 1350 } 1351 else 1352 { 1353 throw new IllegalArgumentException("Unknown compression strategy: " + newStrategy); 1354 } 1355 } 1356 1357 /** 1358 * Sets the size of IDAT chunks generated when encoding. 1359 * If this method is never called, a default value of 32768 bytes (32 KB) is used. 1360 * Note that a byte array of the size of the value you specify here is allocated, 1361 * so make sure that you keep the value small enough to stay within a 1362 * system's memory. 1363 * <p> 1364 * Compressed image data is spread over several IDAT chunks by this codec. 1365 * The length of the compressed data of a complete image is known only after the complete image 1366 * has been encoded. 1367 * With PNG, that length value has to be stored before the compressed data as a chunk size value. 1368 * This codec is supposed to work with {@link java.io.OutputStream} objects, 1369 * so seeking back to adjust the chunk size value of an IDAT chunk is not 1370 * possible. 1371 * That's why all data of a chunk is compressed into a memory buffer. 1372 * Whenever the buffer gets full, it is written to output as an IDAT chunk. 1373 * <p> 1374 * Note that the last IDAT chunk may be smaller than the size defined here. 1375 * @param newSize size of encoding compressed data buffer 1376 */ 1377 public void setEncodingIdatSize(int newSize) 1378 { 1379 if (newSize < 1) 1380 { 1381 throw new IllegalArgumentException("Minimum IDAT chunk size must be 1 or larger."); 1382 } 1383 encodingMinIdatSize = newSize; 1384 } 1385 1386 public void setFile(String fileName, CodecMode codecMode) throws IOException, UnsupportedCodecModeException 1387 { 1388 if (codecMode == CodecMode.LOAD) 1389 { 1390 setInputStream(new BufferedInputStream(new FileInputStream(fileName))); 1391 } 1392 else 1393 { 1394 super.setFile(fileName, codecMode); 1395 } 1396 } 1397 1398 /** 1399 * Sets date and time of last modification of the image to be stored in a PNG stream 1400 * when saving. 1401 * Make sure the argument object has UTC as time zone 1402 * (<a target="_top" href="http://www.w3.org/TR/PNG#C.tIME">as 1403 * demanded by the PNG specs)</a>. 1404 * If you want the current time and date, use 1405 * <code>new GregorianCalendar(new SimpleTimeZone(0, "UTC"))</code> 1406 * as parameter for this method. 1407 * @param time time of last modification of the image 1408 */ 1409 public void setModification(Calendar time) 1410 { 1411 modification = time; 1412 } 1413 1414 /** 1415 * Skips a number of bytes in the input stream. 1416 * @param num number of bytes to be skipped 1417 * @throws IOException if there were I/O errors 1418 */ 1419 private void skip(long num) throws IOException 1420 { 1421 while (num > 0) 1422 { 1423 long numSkipped = in.skip(num); 1424 if (numSkipped > 0) 1425 { 1426 num -= numSkipped; 1427 } 1428 } 1429 } 1430 1431 private void storeInterlacedAdam7(int pass, int y, byte[] buffer) 1432 { 1433 switch(colorType) 1434 { 1435 case(COLOR_TYPE_GRAY): 1436 { 1437 storeInterlacedAdam7Gray(pass, y, buffer); 1438 break; 1439 } 1440 case(COLOR_TYPE_RGB): 1441 { 1442 storeInterlacedAdam7Rgb(pass, y, buffer); 1443 break; 1444 } 1445 case(COLOR_TYPE_RGB_ALPHA): 1446 { 1447 storeInterlacedAdam7RgbAlpha(pass, y, buffer); 1448 break; 1449 } 1450 case(COLOR_TYPE_GRAY_ALPHA): 1451 { 1452 storeInterlacedAdam7GrayAlpha(pass, y, buffer); 1453 break; 1454 } 1455 case(COLOR_TYPE_INDEXED): 1456 { 1457 storeInterlacedAdam7Indexed(pass, y, buffer); 1458 break; 1459 } 1460 } 1461 } 1462 1463 private void storeInterlacedAdam7Gray(int pass, int y, byte[] buffer) 1464 { 1465 int x = ADAM7_FIRST_COLUMN[pass]; 1466 final int incr = ADAM7_COLUMN_INCREMENT[pass]; 1467 final int x1 = getBoundsX1(); 1468 final int x2 = getBoundsX2(); 1469 int offset = 0; 1470 int numColumns = computeColumnsAdam7(pass); 1471 int numPackedBytes = computeBytesPerRow(numColumns); 1472 byte[] dest = new byte[numColumns + 7]; 1473 switch(precision) 1474 { 1475 case(1): 1476 { 1477 BilevelImage bilevelImage = (BilevelImage)image; 1478 ArrayConverter.decodePacked1Bit(buffer, 0, dest, 0, numPackedBytes); 1479 while (x <= x2) 1480 { 1481 if (x >= x1) 1482 { 1483 if (dest[offset] == 0) 1484 { 1485 bilevelImage.putBlack(x - x1, y); 1486 } 1487 else 1488 { 1489 bilevelImage.putWhite(x - x1, y); 1490 } 1491 } 1492 x += incr; 1493 offset++; 1494 } 1495 break; 1496 } 1497 case(2): 1498 { 1499 Gray8Image grayImage = (Gray8Image)image; 1500 ArrayConverter.convertPacked2BitIntensityTo8Bit(buffer, 0, dest, 0, numPackedBytes); 1501 while (x <= x2) 1502 { 1503 if (x >= x1) 1504 { 1505 grayImage.putByteSample(x - x1, y, dest[offset]); 1506 } 1507 x += incr; 1508 offset++; 1509 } 1510 break; 1511 } 1512 case(4): 1513 { 1514 Gray8Image grayImage = (Gray8Image)image; 1515 ArrayConverter.convertPacked4BitIntensityTo8Bit(buffer, 0, dest, 0, numPackedBytes); 1516 while (x <= x2) 1517 { 1518 if (x >= x1) 1519 { 1520 grayImage.putByteSample(x - x1, y, dest[offset]); 1521 } 1522 x += incr; 1523 offset++; 1524 } 1525 break; 1526 } 1527 case(8): 1528 { 1529 Gray8Image grayImage = (Gray8Image)image; 1530 while (x <= x2) 1531 { 1532 if (x >= x1) 1533 { 1534 grayImage.putSample(x - x1, y, buffer[offset]); 1535 } 1536 x += incr; 1537 offset++; 1538 } 1539 break; 1540 } 1541 case(16): 1542 { 1543 Gray16Image grayImage = (Gray16Image)image; 1544 while (x <= x2) 1545 { 1546 if (x >= x1) 1547 { 1548 int sample = (buffer[offset] & 0xff) << 8; 1549 sample |= (buffer[offset + 1] & 0xff); 1550 grayImage.putSample(x, y, sample); 1551 } 1552 x += incr; 1553 offset += 2; 1554 } 1555 break; 1556 } 1557 } 1558 } 1559 1560 private void storeInterlacedAdam7GrayAlpha(int pass, int y, byte[] buffer) 1561 { 1562 int x = ADAM7_FIRST_COLUMN[pass]; 1563 final int incr = ADAM7_COLUMN_INCREMENT[pass]; 1564 final int x1 = getBoundsX1(); 1565 final int x2 = getBoundsX2(); 1566 int offset = 0; 1567 switch(precision) 1568 { 1569 case(8): 1570 { 1571 Gray8Image grayImage = (Gray8Image)image; 1572 while (x <= x2) 1573 { 1574 if (x >= x1) 1575 { 1576 grayImage.putSample(x - x1, y, buffer[offset]); 1577 // alpha 1578 } 1579 x += incr; 1580 offset += 2; 1581 } 1582 break; 1583 } 1584 case(16): 1585 { 1586 Gray16Image grayImage = (Gray16Image)image; 1587 while (x <= x2) 1588 { 1589 if (x >= x1) 1590 { 1591 int sample = (buffer[offset] & 0xff) << 8; 1592 sample |= (buffer[offset + 1] & 0xff); 1593 grayImage.putSample(x, y, sample); 1594 // store alpha 1595 } 1596 x += incr; 1597 offset += 4; 1598 } 1599 break; 1600 } 1601 } 1602 } 1603 1604 private void storeInterlacedAdam7Indexed(int pass, int y, byte[] buffer) 1605 { 1606 Paletted8Image palImage = (Paletted8Image)image; 1607 int x = ADAM7_FIRST_COLUMN[pass]; 1608 final int incr = ADAM7_COLUMN_INCREMENT[pass]; 1609 final int x1 = getBoundsX1(); 1610 final int x2 = getBoundsX2(); 1611 int offset = 0; 1612 int numColumns = computeColumnsAdam7(pass); 1613 int numPackedBytes = computeBytesPerRow(numColumns); 1614 byte[] dest = new byte[numColumns + 7]; 1615 switch(precision) 1616 { 1617 case(1): 1618 { 1619 ArrayConverter.decodePacked1Bit(buffer, 0, dest, 0, numPackedBytes); 1620 while (x <= x2) 1621 { 1622 if (x >= x1) 1623 { 1624 palImage.putByteSample(x - x1, y, dest[offset]); 1625 } 1626 x += incr; 1627 offset++; 1628 } 1629 break; 1630 } 1631 case(2): 1632 { 1633 ArrayConverter.decodePacked2Bit(buffer, 0, dest, 0, numPackedBytes); 1634 while (x <= x2) 1635 { 1636 if (x >= x1) 1637 { 1638 palImage.putByteSample(x - x1, y, dest[offset]); 1639 } 1640 x += incr; 1641 offset++; 1642 } 1643 break; 1644 } 1645 case(4): 1646 { 1647 ArrayConverter.decodePacked4Bit(buffer, 0, dest, 0, numPackedBytes); 1648 while (x <= x2) 1649 { 1650 if (x >= x1) 1651 { 1652 palImage.putByteSample(x - x1, y, dest[offset]); 1653 } 1654 x += incr; 1655 offset++; 1656 } 1657 break; 1658 } 1659 case(8): 1660 { 1661 while (x <= x2) 1662 { 1663 if (x >= x1) 1664 { 1665 palImage.putSample(x - x1, y, buffer[offset]); 1666 } 1667 x += incr; 1668 offset++; 1669 } 1670 break; 1671 } 1672 } 1673 } 1674 1675 private void storeInterlacedAdam7Rgb(int pass, int y, byte[] buffer) 1676 { 1677 int x = ADAM7_FIRST_COLUMN[pass]; 1678 final int x1 = getBoundsX1(); 1679 final int x2 = getBoundsX2(); 1680 final int incr = ADAM7_COLUMN_INCREMENT[pass]; 1681 int offset = 0; 1682 if (precision == 8) 1683 { 1684 RGB24Image rgbImage = (RGB24Image)image; 1685 while (x <= x2) 1686 { 1687 if (x >= x1) 1688 { 1689 rgbImage.putSample(RGB24Image.INDEX_RED, x, y, buffer[offset]); 1690 rgbImage.putSample(RGB24Image.INDEX_GREEN, x, y, buffer[offset + 1]); 1691 rgbImage.putSample(RGB24Image.INDEX_BLUE, x, y, buffer[offset + 2]); 1692 } 1693 x += incr; 1694 offset += 3; 1695 } 1696 } 1697 else 1698 if (precision == 16) 1699 { 1700 RGB48Image rgbImage = (RGB48Image)image; 1701 while (x <= x2) 1702 { 1703 if (x >= x1) 1704 { 1705 int red = (buffer[offset] & 0xff) << 8; 1706 red |= buffer[offset + 1] & 0xff; 1707 rgbImage.putSample(RGB24Image.INDEX_RED, x, y, red); 1708 1709 int green = (buffer[offset + 2] & 0xff) << 8; 1710 green |= buffer[offset + 3] & 0xff; 1711 rgbImage.putSample(RGB24Image.INDEX_GREEN, x, y, green); 1712 1713 int blue = (buffer[offset + 4] & 0xff) << 8; 1714 blue |= buffer[offset + 5] & 0xff; 1715 rgbImage.putSample(RGB24Image.INDEX_BLUE, x, y, blue); 1716 } 1717 x += incr; 1718 offset += 6; 1719 } 1720 } 1721 } 1722 1723 private void storeInterlacedAdam7RgbAlpha(int pass, int y, byte[] buffer) 1724 { 1725 int x = ADAM7_FIRST_COLUMN[pass]; 1726 final int x1 = getBoundsX1(); 1727 final int x2 = getBoundsX2(); 1728 final int incr = ADAM7_COLUMN_INCREMENT[pass]; 1729 int offset = 0; 1730 if (precision == 8) 1731 { 1732 RGB24Image rgbImage = (RGB24Image)image; 1733 while (x <= x2) 1734 { 1735 if (x >= x1) 1736 { 1737 rgbImage.putSample(RGB24Image.INDEX_RED, x, y, buffer[offset]); 1738 rgbImage.putSample(RGB24Image.INDEX_GREEN, x, y, buffer[offset + 1]); 1739 rgbImage.putSample(RGB24Image.INDEX_BLUE, x, y, buffer[offset + 2]); 1740 // store alpha 1741 } 1742 x += incr; 1743 offset += 4; 1744 } 1745 } 1746 else 1747 if (precision == 16) 1748 { 1749 RGB48Image rgbImage = (RGB48Image)image; 1750 while (x <= x2) 1751 { 1752 if (x >= x1) 1753 { 1754 int red = (buffer[offset] & 0xff) << 8; 1755 red |= buffer[offset + 1] & 0xff; 1756 rgbImage.putSample(RGB24Image.INDEX_RED, x, y, red); 1757 1758 int green = (buffer[offset + 2] & 0xff) << 8; 1759 green |= buffer[offset + 3] & 0xff; 1760 rgbImage.putSample(RGB24Image.INDEX_GREEN, x, y, green); 1761 1762 int blue = (buffer[offset + 4] & 0xff) << 8; 1763 blue |= buffer[offset + 5] & 0xff; 1764 rgbImage.putSample(RGB24Image.INDEX_BLUE, x, y, blue); 1765 1766 // store alpha 1767 } 1768 x += incr; 1769 offset += 8; 1770 } 1771 } 1772 } 1773 1774 1775 private void storeNonInterlaced(int y, byte[] buffer) 1776 { 1777 switch(colorType) 1778 { 1779 case(COLOR_TYPE_GRAY): 1780 { 1781 storeNonInterlacedGray(y, buffer); 1782 break; 1783 } 1784 case(COLOR_TYPE_GRAY_ALPHA): 1785 { 1786 storeNonInterlacedGrayAlpha(y, buffer); 1787 break; 1788 } 1789 case(COLOR_TYPE_INDEXED): 1790 { 1791 storeNonInterlacedIndexed(y, buffer); 1792 break; 1793 } 1794 case(COLOR_TYPE_RGB): 1795 { 1796 storeNonInterlacedRgb(y, buffer); 1797 break; 1798 } 1799 case(COLOR_TYPE_RGB_ALPHA): 1800 { 1801 storeNonInterlacedRgbAlpha(y, buffer); 1802 break; 1803 } 1804 } 1805 } 1806 1807 private void storeNonInterlacedGray(int y, byte[] buffer) 1808 { 1809 switch(precision) 1810 { 1811 case(1): 1812 { 1813 BilevelImage bilevelImage = (BilevelImage)image; 1814 int x1 = getBoundsX1(); 1815 bilevelImage.putPackedBytes(0, y, getBoundsWidth(), buffer, x1 / 8, x1 % 8); 1816 break; 1817 } 1818 case(2): 1819 { 1820 Gray8Image grayImage = (Gray8Image)image; 1821 byte[] dest = new byte[width + 3]; 1822 ArrayConverter.convertPacked2BitIntensityTo8Bit(buffer, 0, dest, 0, buffer.length); 1823 grayImage.putByteSamples(0, 0, y, getBoundsWidth(), 1, dest, getBoundsX1()); 1824 break; 1825 } 1826 case(4): 1827 { 1828 Gray8Image grayImage = (Gray8Image)image; 1829 byte[] dest = new byte[width + 1]; 1830 ArrayConverter.convertPacked4BitIntensityTo8Bit(buffer, 0, dest, 0, buffer.length); 1831 grayImage.putByteSamples(0, 0, y, getBoundsWidth(), 1, dest, getBoundsX1()); 1832 break; 1833 } 1834 case(8): 1835 { 1836 Gray8Image grayImage = (Gray8Image)image; 1837 int offset = getBoundsX1(); 1838 int x = 0; 1839 int k = getBoundsWidth(); 1840 while (k > 0) 1841 { 1842 grayImage.putSample(0, x++, y, buffer[offset++]); 1843 k--; 1844 } 1845 break; 1846 } 1847 case(16): 1848 { 1849 Gray16Image grayImage = (Gray16Image)image; 1850 int offset = getBoundsX1(); 1851 int x = 0; 1852 int k = getBoundsWidth(); 1853 while (k > 0) 1854 { 1855 int sample = (buffer[offset++] & 0xff) << 8; 1856 sample |= (buffer[offset++] & 0xff); 1857 grayImage.putSample(x++, y, sample); 1858 k--; 1859 } 1860 break; 1861 } 1862 } 1863 } 1864 1865 private void storeNonInterlacedGrayAlpha(int y, byte[] buffer) 1866 { 1867 switch(precision) 1868 { 1869 case(8): 1870 { 1871 Gray8Image grayImage = (Gray8Image)image; 1872 int offset = getBoundsX1(); 1873 int x = 0; 1874 int k = getBoundsWidth(); 1875 while (k > 0) 1876 { 1877 grayImage.putSample(0, x++, y, buffer[offset++]); 1878 offset++; // skip alpha; should be stored in a TransparencyInformation object 1879 k--; 1880 } 1881 break; 1882 } 1883 case(16): 1884 { 1885 Gray16Image grayImage = (Gray16Image)image; 1886 int offset = getBoundsX1(); 1887 int x = 0; 1888 int k = getBoundsWidth(); 1889 while (k > 0) 1890 { 1891 int sample = (buffer[offset++] & 0xff) << 8; 1892 sample |= (buffer[offset++] & 0xff); 1893 grayImage.putSample(x++, y, sample); 1894 offset += 2; // skip alpha; TODO: store in TransparencyInformation object 1895 k--; 1896 } 1897 break; 1898 } 1899 } 1900 } 1901 1902 private void storeNonInterlacedIndexed(int y, byte[] buffer) 1903 { 1904 Paletted8Image palImage = (Paletted8Image)image; 1905 switch(precision) 1906 { 1907 case(1): 1908 { 1909 byte[] dest = new byte[width + 7]; 1910 ArrayConverter.decodePacked1Bit(buffer, 0, dest, 0, buffer.length); 1911 palImage.putByteSamples(0, 0, y, getBoundsWidth(), 1, dest, getBoundsX1()); 1912 break; 1913 } 1914 case(2): 1915 { 1916 byte[] dest = new byte[width + 3]; 1917 ArrayConverter.decodePacked2Bit(buffer, 0, dest, 0, buffer.length); 1918 palImage.putByteSamples(0, 0, y, getBoundsWidth(), 1, dest, getBoundsX1()); 1919 break; 1920 } 1921 case(4): 1922 { 1923 byte[] dest = new byte[width + 1]; 1924 ArrayConverter.decodePacked4Bit(buffer, 0, dest, 0, buffer.length); 1925 palImage.putByteSamples(0, 0, y, getBoundsWidth(), 1, dest, getBoundsX1()); 1926 break; 1927 } 1928 case(8): 1929 { 1930 int offset = getBoundsX1(); 1931 int x = 0; 1932 int k = getBoundsWidth(); 1933 while (k > 0) 1934 { 1935 palImage.putSample(0, x++, y, buffer[offset++]); 1936 k--; 1937 } 1938 break; 1939 } 1940 } 1941 } 1942 1943 private void storeNonInterlacedRgb(int y, byte[] buffer) 1944 { 1945 if (precision == 8) 1946 { 1947 RGB24Image rgbImage = (RGB24Image)image; 1948 int offset = getBoundsX1() * 3; 1949 int x = 0; 1950 int k = getBoundsWidth(); 1951 while (k > 0) 1952 { 1953 rgbImage.putSample(RGB24Image.INDEX_RED, x, y, buffer[offset++]); 1954 rgbImage.putSample(RGB24Image.INDEX_GREEN, x, y, buffer[offset++]); 1955 rgbImage.putSample(RGB24Image.INDEX_BLUE, x, y, buffer[offset++]); 1956 x++; 1957 k--; 1958 } 1959 } 1960 else 1961 if (precision == 16) 1962 { 1963 RGB48Image rgbImage = (RGB48Image)image; 1964 int offset = getBoundsX1() * 6; 1965 int x = 0; 1966 int k = getBoundsWidth(); 1967 while (k > 0) 1968 { 1969 int red = (buffer[offset++] & 0xff) << 8; 1970 red |= buffer[offset++] & 0xff; 1971 rgbImage.putSample(RGB24Image.INDEX_RED, x, y, red); 1972 1973 int green = (buffer[offset++] & 0xff) << 8; 1974 green |= buffer[offset++] & 0xff; 1975 rgbImage.putSample(RGB24Image.INDEX_GREEN, x, y, green); 1976 1977 int blue = (buffer[offset++] & 0xff) << 8; 1978 blue |= buffer[offset++] & 0xff; 1979 rgbImage.putSample(RGB24Image.INDEX_BLUE, x, y, blue); 1980 1981 x++; 1982 k--; 1983 } 1984 } 1985 } 1986 1987 private void storeNonInterlacedRgbAlpha(int y, byte[] buffer) 1988 { 1989 switch(precision) 1990 { 1991 case(8): 1992 { 1993 RGB24Image rgbImage = (RGB24Image)image; 1994 int offset = getBoundsX1() * 3; 1995 int x = 0; 1996 int k = getBoundsWidth(); 1997 while (k > 0) 1998 { 1999 rgbImage.putSample(RGB24Image.INDEX_RED, x, y, buffer[offset++]); 2000 rgbImage.putSample(RGB24Image.INDEX_GREEN, x, y, buffer[offset++]); 2001 rgbImage.putSample(RGB24Image.INDEX_BLUE, x, y, buffer[offset++]); 2002 offset++; // skip alpha; TODO: store in TransparencyInformation object 2003 x++; 2004 k--; 2005 } 2006 break; 2007 } 2008 case(16): 2009 { 2010 RGB48Image rgbImage = (RGB48Image)image; 2011 int offset = getBoundsX1() * 8; 2012 int x = 0; 2013 int k = getBoundsWidth(); 2014 while (k > 0) 2015 { 2016 int red = (buffer[offset++] & 0xff) << 8; 2017 red |= buffer[offset++] & 0xff; 2018 rgbImage.putSample(RGB24Image.INDEX_RED, x, y, red); 2019 2020 int green = (buffer[offset++] & 0xff) << 8; 2021 green |= buffer[offset++] & 0xff; 2022 rgbImage.putSample(RGB24Image.INDEX_GREEN, x, y, green); 2023 2024 int blue = (buffer[offset++] & 0xff) << 8; 2025 blue |= buffer[offset++] & 0xff; 2026 rgbImage.putSample(RGB24Image.INDEX_BLUE, x, y, blue); 2027 2028 offset += 2; // skip alpha; TODO: store in TransparencyInformation object 2029 x++; 2030 k--; 2031 } 2032 break; 2033 } 2034 } 2035 } 2036 2037 public String suggestFileExtension(PixelImage image) 2038 { 2039 return ".png"; 2040 } 2041 }