001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.commons.imaging.formats.gif; 018 019import static org.apache.commons.imaging.ImagingConstants.PARAM_KEY_FORMAT; 020import static org.apache.commons.imaging.ImagingConstants.PARAM_KEY_XMP_XML; 021import static org.apache.commons.imaging.common.BinaryFunctions.compareBytes; 022import static org.apache.commons.imaging.common.BinaryFunctions.printByteBits; 023import static org.apache.commons.imaging.common.BinaryFunctions.printCharQuad; 024import static org.apache.commons.imaging.common.BinaryFunctions.read2Bytes; 025import static org.apache.commons.imaging.common.BinaryFunctions.readByte; 026import static org.apache.commons.imaging.common.BinaryFunctions.readBytes; 027 028import java.awt.Dimension; 029import java.awt.image.BufferedImage; 030import java.io.ByteArrayInputStream; 031import java.io.IOException; 032import java.io.InputStream; 033import java.io.OutputStream; 034import java.io.PrintWriter; 035import java.nio.ByteOrder; 036import java.nio.charset.StandardCharsets; 037import java.util.ArrayList; 038import java.util.HashMap; 039import java.util.List; 040import java.util.Map; 041import java.util.logging.Level; 042import java.util.logging.Logger; 043 044import org.apache.commons.imaging.FormatCompliance; 045import org.apache.commons.imaging.ImageFormat; 046import org.apache.commons.imaging.ImageFormats; 047import org.apache.commons.imaging.ImageInfo; 048import org.apache.commons.imaging.ImageParser; 049import org.apache.commons.imaging.ImageReadException; 050import org.apache.commons.imaging.ImageWriteException; 051import org.apache.commons.imaging.common.BinaryOutputStream; 052import org.apache.commons.imaging.common.ImageBuilder; 053import org.apache.commons.imaging.common.ImageMetadata; 054import org.apache.commons.imaging.common.XmpEmbeddable; 055import org.apache.commons.imaging.common.bytesource.ByteSource; 056import org.apache.commons.imaging.common.mylzw.MyLzwCompressor; 057import org.apache.commons.imaging.common.mylzw.MyLzwDecompressor; 058import org.apache.commons.imaging.palette.Palette; 059import org.apache.commons.imaging.palette.PaletteFactory; 060 061public class GifImageParser extends ImageParser implements XmpEmbeddable { 062 063 private static final Logger LOGGER = Logger.getLogger(GifImageParser.class.getName()); 064 065 private static final String DEFAULT_EXTENSION = ".gif"; 066 private static final String[] ACCEPTED_EXTENSIONS = { DEFAULT_EXTENSION, }; 067 private static final byte[] GIF_HEADER_SIGNATURE = { 71, 73, 70 }; 068 private static final int EXTENSION_CODE = 0x21; 069 private static final int IMAGE_SEPARATOR = 0x2C; 070 private static final int GRAPHIC_CONTROL_EXTENSION = (EXTENSION_CODE << 8) | 0xf9; 071 private static final int COMMENT_EXTENSION = 0xfe; 072 private static final int PLAIN_TEXT_EXTENSION = 0x01; 073 private static final int XMP_EXTENSION = 0xff; 074 private static final int TERMINATOR_BYTE = 0x3b; 075 private static final int APPLICATION_EXTENSION_LABEL = 0xff; 076 private static final int XMP_COMPLETE_CODE = (EXTENSION_CODE << 8) 077 | XMP_EXTENSION; 078 private static final int LOCAL_COLOR_TABLE_FLAG_MASK = 1 << 7; 079 private static final int INTERLACE_FLAG_MASK = 1 << 6; 080 private static final int SORT_FLAG_MASK = 1 << 5; 081 private static final byte[] XMP_APPLICATION_ID_AND_AUTH_CODE = { 082 0x58, // X 083 0x4D, // M 084 0x50, // P 085 0x20, // 086 0x44, // D 087 0x61, // a 088 0x74, // t 089 0x61, // a 090 0x58, // X 091 0x4D, // M 092 0x50, // P 093 }; 094 095 public GifImageParser() { 096 super.setByteOrder(ByteOrder.LITTLE_ENDIAN); 097 } 098 099 @Override 100 public String getName() { 101 return "Graphics Interchange Format"; 102 } 103 104 @Override 105 public String getDefaultExtension() { 106 return DEFAULT_EXTENSION; 107 } 108 109 @Override 110 protected String[] getAcceptedExtensions() { 111 return ACCEPTED_EXTENSIONS; 112 } 113 114 @Override 115 protected ImageFormat[] getAcceptedTypes() { 116 return new ImageFormat[] { ImageFormats.GIF, // 117 }; 118 } 119 120 private GifHeaderInfo readHeader(final InputStream is, 121 final FormatCompliance formatCompliance) throws ImageReadException, 122 IOException { 123 final byte identifier1 = readByte("identifier1", is, "Not a Valid GIF File"); 124 final byte identifier2 = readByte("identifier2", is, "Not a Valid GIF File"); 125 final byte identifier3 = readByte("identifier3", is, "Not a Valid GIF File"); 126 127 final byte version1 = readByte("version1", is, "Not a Valid GIF File"); 128 final byte version2 = readByte("version2", is, "Not a Valid GIF File"); 129 final byte version3 = readByte("version3", is, "Not a Valid GIF File"); 130 131 if (formatCompliance != null) { 132 formatCompliance.compareBytes("Signature", GIF_HEADER_SIGNATURE, 133 new byte[]{identifier1, identifier2, identifier3,}); 134 formatCompliance.compare("version", 56, version1); 135 formatCompliance.compare("version", new int[] { 55, 57, }, version2); 136 formatCompliance.compare("version", 97, version3); 137 } 138 139 if (LOGGER.isLoggable(Level.FINEST)) { 140 printCharQuad("identifier: ", ((identifier1 << 16) 141 | (identifier2 << 8) | (identifier3 << 0))); 142 printCharQuad("version: ", 143 ((version1 << 16) | (version2 << 8) | (version3 << 0))); 144 } 145 146 final int logicalScreenWidth = read2Bytes("Logical Screen Width", is, "Not a Valid GIF File", getByteOrder()); 147 final int logicalScreenHeight = read2Bytes("Logical Screen Height", is, "Not a Valid GIF File", getByteOrder()); 148 149 if (formatCompliance != null) { 150 formatCompliance.checkBounds("Width", 1, Integer.MAX_VALUE, 151 logicalScreenWidth); 152 formatCompliance.checkBounds("Height", 1, Integer.MAX_VALUE, 153 logicalScreenHeight); 154 } 155 156 final byte packedFields = readByte("Packed Fields", is, 157 "Not a Valid GIF File"); 158 final byte backgroundColorIndex = readByte("Background Color Index", is, 159 "Not a Valid GIF File"); 160 final byte pixelAspectRatio = readByte("Pixel Aspect Ratio", is, 161 "Not a Valid GIF File"); 162 163 if (LOGGER.isLoggable(Level.FINEST)) { 164 printByteBits("PackedFields bits", packedFields); 165 } 166 167 final boolean globalColorTableFlag = ((packedFields & 128) > 0); 168 if (LOGGER.isLoggable(Level.FINEST)) { 169 LOGGER.finest("GlobalColorTableFlag: " + globalColorTableFlag); 170 } 171 final byte colorResolution = (byte) ((packedFields >> 4) & 7); 172 if (LOGGER.isLoggable(Level.FINEST)) { 173 LOGGER.finest("ColorResolution: " + colorResolution); 174 } 175 final boolean sortFlag = ((packedFields & 8) > 0); 176 if (LOGGER.isLoggable(Level.FINEST)) { 177 LOGGER.finest("SortFlag: " + sortFlag); 178 } 179 final byte sizeofGlobalColorTable = (byte) (packedFields & 7); 180 if (LOGGER.isLoggable(Level.FINEST)) { 181 LOGGER.finest("SizeofGlobalColorTable: " 182 + sizeofGlobalColorTable); 183 } 184 185 if (formatCompliance != null) { 186 if (globalColorTableFlag && backgroundColorIndex != -1) { 187 formatCompliance.checkBounds("Background Color Index", 0, 188 convertColorTableSize(sizeofGlobalColorTable), 189 backgroundColorIndex); 190 } 191 } 192 193 return new GifHeaderInfo(identifier1, identifier2, identifier3, 194 version1, version2, version3, logicalScreenWidth, 195 logicalScreenHeight, packedFields, backgroundColorIndex, 196 pixelAspectRatio, globalColorTableFlag, colorResolution, 197 sortFlag, sizeofGlobalColorTable); 198 } 199 200 private GraphicControlExtension readGraphicControlExtension(final int code, 201 final InputStream is) throws IOException { 202 readByte("block_size", is, "GIF: corrupt GraphicControlExt"); 203 final int packed = readByte("packed fields", is, 204 "GIF: corrupt GraphicControlExt"); 205 206 final int dispose = (packed & 0x1c) >> 2; // disposal method 207 final boolean transparency = (packed & 1) != 0; 208 209 final int delay = read2Bytes("delay in milliseconds", is, "GIF: corrupt GraphicControlExt", getByteOrder()); 210 final int transparentColorIndex = 0xff & readByte("transparent color index", 211 is, "GIF: corrupt GraphicControlExt"); 212 readByte("block terminator", is, "GIF: corrupt GraphicControlExt"); 213 214 return new GraphicControlExtension(code, packed, dispose, transparency, 215 delay, transparentColorIndex); 216 } 217 218 private byte[] readSubBlock(final InputStream is) throws IOException { 219 final int blockSize = 0xff & readByte("block_size", is, "GIF: corrupt block"); 220 221 return readBytes("block", is, blockSize, "GIF: corrupt block"); 222 } 223 224 private GenericGifBlock readGenericGIFBlock(final InputStream is, final int code) 225 throws IOException { 226 return readGenericGIFBlock(is, code, null); 227 } 228 229 private GenericGifBlock readGenericGIFBlock(final InputStream is, final int code, 230 final byte[] first) throws IOException { 231 final List<byte[]> subblocks = new ArrayList<>(); 232 233 if (first != null) { 234 subblocks.add(first); 235 } 236 237 while (true) { 238 final byte[] bytes = readSubBlock(is); 239 if (bytes.length < 1) { 240 break; 241 } 242 subblocks.add(bytes); 243 } 244 245 return new GenericGifBlock(code, subblocks); 246 } 247 248 private List<GifBlock> readBlocks(final GifHeaderInfo ghi, final InputStream is, 249 final boolean stopBeforeImageData, final FormatCompliance formatCompliance) 250 throws ImageReadException, IOException { 251 final List<GifBlock> result = new ArrayList<>(); 252 253 while (true) { 254 final int code = is.read(); 255 256 switch (code) { 257 case -1: 258 throw new ImageReadException("GIF: unexpected end of data"); 259 260 case IMAGE_SEPARATOR: 261 final ImageDescriptor id = readImageDescriptor(ghi, code, is, 262 stopBeforeImageData, formatCompliance); 263 result.add(id); 264 // if (stopBeforeImageData) 265 // return result; 266 267 break; 268 269 case EXTENSION_CODE: { 270 final int extensionCode = is.read(); 271 final int completeCode = ((0xff & code) << 8) 272 | (0xff & extensionCode); 273 274 switch (extensionCode) { 275 case 0xf9: 276 final GraphicControlExtension gce = readGraphicControlExtension( 277 completeCode, is); 278 result.add(gce); 279 break; 280 281 case COMMENT_EXTENSION: 282 case PLAIN_TEXT_EXTENSION: { 283 final GenericGifBlock block = readGenericGIFBlock(is, 284 completeCode); 285 result.add(block); 286 break; 287 } 288 289 case APPLICATION_EXTENSION_LABEL: { 290 // 255 (hex 0xFF) Application 291 // Extension Label 292 final byte[] label = readSubBlock(is); 293 294 if (formatCompliance != null) { 295 formatCompliance.addComment( 296 "Unknown Application Extension (" 297 + new String(label, StandardCharsets.US_ASCII) + ")", 298 completeCode); 299 } 300 301 // if (label == new String("ICCRGBG1")) 302 //{ 303 // GIF's can have embedded ICC Profiles - who knew? 304 //} 305 306 if ((label != null) && (label.length > 0)) { 307 final GenericGifBlock block = readGenericGIFBlock(is, 308 completeCode, label); 309 result.add(block); 310 } 311 break; 312 } 313 314 default: { 315 316 if (formatCompliance != null) { 317 formatCompliance.addComment("Unknown block", 318 completeCode); 319 } 320 321 final GenericGifBlock block = readGenericGIFBlock(is, 322 completeCode); 323 result.add(block); 324 break; 325 } 326 } 327 } 328 break; 329 330 case TERMINATOR_BYTE: 331 return result; 332 333 case 0x00: // bad byte, but keep going and see what happens 334 break; 335 336 default: 337 throw new ImageReadException("GIF: unknown code: " + code); 338 } 339 } 340 } 341 342 private ImageDescriptor readImageDescriptor(final GifHeaderInfo ghi, 343 final int blockCode, final InputStream is, final boolean stopBeforeImageData, 344 final FormatCompliance formatCompliance) throws ImageReadException, 345 IOException { 346 final int imageLeftPosition = read2Bytes("Image Left Position", is, "Not a Valid GIF File", getByteOrder()); 347 final int imageTopPosition = read2Bytes("Image Top Position", is, "Not a Valid GIF File", getByteOrder()); 348 final int imageWidth = read2Bytes("Image Width", is, "Not a Valid GIF File", getByteOrder()); 349 final int imageHeight = read2Bytes("Image Height", is, "Not a Valid GIF File", getByteOrder()); 350 final byte packedFields = readByte("Packed Fields", is, "Not a Valid GIF File"); 351 352 if (formatCompliance != null) { 353 formatCompliance.checkBounds("Width", 1, ghi.logicalScreenWidth, imageWidth); 354 formatCompliance.checkBounds("Height", 1, ghi.logicalScreenHeight, imageHeight); 355 formatCompliance.checkBounds("Left Position", 0, ghi.logicalScreenWidth - imageWidth, imageLeftPosition); 356 formatCompliance.checkBounds("Top Position", 0, ghi.logicalScreenHeight - imageHeight, imageTopPosition); 357 } 358 359 if (LOGGER.isLoggable(Level.FINEST)) { 360 printByteBits("PackedFields bits", packedFields); 361 } 362 363 final boolean localColorTableFlag = (((packedFields >> 7) & 1) > 0); 364 if (LOGGER.isLoggable(Level.FINEST)) { 365 LOGGER.finest("LocalColorTableFlag: " + localColorTableFlag); 366 } 367 final boolean interlaceFlag = (((packedFields >> 6) & 1) > 0); 368 if (LOGGER.isLoggable(Level.FINEST)) { 369 LOGGER.finest("Interlace Flag: " + interlaceFlag); 370 } 371 final boolean sortFlag = (((packedFields >> 5) & 1) > 0); 372 if (LOGGER.isLoggable(Level.FINEST)) { 373 LOGGER.finest("Sort Flag: " + sortFlag); 374 } 375 376 final byte sizeOfLocalColorTable = (byte) (packedFields & 7); 377 if (LOGGER.isLoggable(Level.FINEST)) { 378 LOGGER.finest("SizeofLocalColorTable: " + sizeOfLocalColorTable); 379 } 380 381 byte[] localColorTable = null; 382 if (localColorTableFlag) { 383 localColorTable = readColorTable(is, sizeOfLocalColorTable); 384 } 385 386 byte[] imageData = null; 387 if (!stopBeforeImageData) { 388 final int lzwMinimumCodeSize = is.read(); 389 390 final GenericGifBlock block = readGenericGIFBlock(is, -1); 391 final byte[] bytes = block.appendSubBlocks(); 392 final InputStream bais = new ByteArrayInputStream(bytes); 393 394 final int size = imageWidth * imageHeight; 395 final MyLzwDecompressor myLzwDecompressor = new MyLzwDecompressor( 396 lzwMinimumCodeSize, ByteOrder.LITTLE_ENDIAN); 397 imageData = myLzwDecompressor.decompress(bais, size); 398 } else { 399 final int LZWMinimumCodeSize = is.read(); 400 if (LOGGER.isLoggable(Level.FINEST)) { 401 LOGGER.finest("LZWMinimumCodeSize: " + LZWMinimumCodeSize); 402 } 403 404 readGenericGIFBlock(is, -1); 405 } 406 407 return new ImageDescriptor(blockCode, 408 imageLeftPosition, imageTopPosition, imageWidth, imageHeight, 409 packedFields, localColorTableFlag, interlaceFlag, sortFlag, 410 sizeOfLocalColorTable, localColorTable, imageData); 411 } 412 413 private int simplePow(final int base, final int power) { 414 int result = 1; 415 416 for (int i = 0; i < power; i++) { 417 result *= base; 418 } 419 420 return result; 421 } 422 423 private int convertColorTableSize(final int tableSize) { 424 return 3 * simplePow(2, tableSize + 1); 425 } 426 427 private byte[] readColorTable(final InputStream is, final int tableSize) throws IOException { 428 final int actualSize = convertColorTableSize(tableSize); 429 430 return readBytes("block", is, actualSize, "GIF: corrupt Color Table"); 431 } 432 433 private GifBlock findBlock(final List<GifBlock> blocks, final int code) { 434 for (final GifBlock gifBlock : blocks) { 435 if (gifBlock.blockCode == code) { 436 return gifBlock; 437 } 438 } 439 return null; 440 } 441 442 /** 443 * See {@link GifImageParser#readBlocks} for reference how the blocks are created. They should match 444 * the code we are giving here, returning the correct class type. Internal only. 445 */ 446 @SuppressWarnings("unchecked") 447 private <T extends GifBlock> List<T> findAllBlocks(final List<GifBlock> blocks, final int code) { 448 final List<T> filteredBlocks = new ArrayList<>(); 449 for (final GifBlock gifBlock : blocks) { 450 if (gifBlock.blockCode == code) { 451 filteredBlocks.add((T) gifBlock); 452 } 453 } 454 return filteredBlocks; 455 } 456 457 private GifImageContents readFile(final ByteSource byteSource, 458 final boolean stopBeforeImageData) throws ImageReadException, IOException { 459 return readFile(byteSource, stopBeforeImageData, 460 FormatCompliance.getDefault()); 461 } 462 463 private GifImageContents readFile(final ByteSource byteSource, 464 final boolean stopBeforeImageData, final FormatCompliance formatCompliance) 465 throws ImageReadException, IOException { 466 try (InputStream is = byteSource.getInputStream()) { 467 final GifHeaderInfo ghi = readHeader(is, formatCompliance); 468 469 byte[] globalColorTable = null; 470 if (ghi.globalColorTableFlag) { 471 globalColorTable = readColorTable(is, 472 ghi.sizeOfGlobalColorTable); 473 } 474 475 final List<GifBlock> blocks = readBlocks(ghi, is, stopBeforeImageData, 476 formatCompliance); 477 478 final GifImageContents result = new GifImageContents(ghi, globalColorTable, 479 blocks); 480 return result; 481 } 482 } 483 484 @Override 485 public byte[] getICCProfileBytes(final ByteSource byteSource, final Map<String, Object> params) 486 throws ImageReadException, IOException { 487 return null; 488 } 489 490 @Override 491 public Dimension getImageSize(final ByteSource byteSource, final Map<String, Object> params) 492 throws ImageReadException, IOException { 493 final GifImageContents blocks = readFile(byteSource, false); 494 495 if (blocks == null) { 496 throw new ImageReadException("GIF: Couldn't read blocks"); 497 } 498 499 final GifHeaderInfo bhi = blocks.gifHeaderInfo; 500 if (bhi == null) { 501 throw new ImageReadException("GIF: Couldn't read Header"); 502 } 503 504 // The logical screen width and height defines the overall dimensions of the image 505 // space from the top left corner. This does not necessarily match the dimensions 506 // of any individual image, or even the dimensions created by overlapping all 507 // images (since each images might have an offset from the top left corner). 508 // Nevertheless, these fields indicate the desired screen dimensions when rendering the GIF. 509 return new Dimension(bhi.logicalScreenWidth, bhi.logicalScreenHeight); 510 } 511 512 // Made internal for testability. 513 static DisposalMethod createDisposalMethodFromIntValue(int value) throws ImageReadException { 514 switch (value) { 515 case 0: 516 return DisposalMethod.UNSPECIFIED; 517 case 1: 518 return DisposalMethod.DO_NOT_DISPOSE; 519 case 2: 520 return DisposalMethod.RESTORE_TO_BACKGROUND; 521 case 3: 522 return DisposalMethod.RESTORE_TO_PREVIOUS; 523 case 4: 524 return DisposalMethod.TO_BE_DEFINED_1; 525 case 5: 526 return DisposalMethod.TO_BE_DEFINED_2; 527 case 6: 528 return DisposalMethod.TO_BE_DEFINED_3; 529 case 7: 530 return DisposalMethod.TO_BE_DEFINED_4; 531 default: 532 throw new ImageReadException("GIF: Invalid parsing of disposal method"); 533 } 534 } 535 536 @Override 537 public ImageMetadata getMetadata(final ByteSource byteSource, final Map<String, Object> params) 538 throws ImageReadException, IOException { 539 final GifImageContents imageContents = readFile(byteSource, false); 540 541 if (imageContents == null) { 542 throw new ImageReadException("GIF: Couldn't read blocks"); 543 } 544 545 final GifHeaderInfo bhi = imageContents.gifHeaderInfo; 546 if (bhi == null) { 547 throw new ImageReadException("GIF: Couldn't read Header"); 548 } 549 550 final List<GifImageData> imageData = findAllImageData(imageContents); 551 List<GifImageMetadataItem> metadataItems = new ArrayList<>(imageData.size()); 552 for(GifImageData id : imageData) { 553 DisposalMethod disposalMethod = createDisposalMethodFromIntValue(id.gce.dispose); 554 metadataItems.add(new GifImageMetadataItem(id.gce.delay, id.descriptor.imageLeftPosition, id.descriptor.imageTopPosition, disposalMethod)); 555 } 556 return new GifImageMetadata(bhi.logicalScreenWidth, bhi.logicalScreenHeight, metadataItems); 557 } 558 559 private List<String> getComments(final List<GifBlock> blocks) throws IOException { 560 final List<String> result = new ArrayList<>(); 561 final int code = 0x21fe; 562 563 for (final GifBlock block : blocks) { 564 if (block.blockCode == code) { 565 final byte[] bytes = ((GenericGifBlock) block).appendSubBlocks(); 566 result.add(new String(bytes, StandardCharsets.US_ASCII)); 567 } 568 } 569 570 return result; 571 } 572 573 @Override 574 public ImageInfo getImageInfo(final ByteSource byteSource, final Map<String, Object> params) 575 throws ImageReadException, IOException { 576 final GifImageContents blocks = readFile(byteSource, false); 577 578 if (blocks == null) { 579 throw new ImageReadException("GIF: Couldn't read blocks"); 580 } 581 582 final GifHeaderInfo bhi = blocks.gifHeaderInfo; 583 if (bhi == null) { 584 throw new ImageReadException("GIF: Couldn't read Header"); 585 } 586 587 final ImageDescriptor id = (ImageDescriptor) findBlock(blocks.blocks, 588 IMAGE_SEPARATOR); 589 if (id == null) { 590 throw new ImageReadException("GIF: Couldn't read ImageDescriptor"); 591 } 592 593 final GraphicControlExtension gce = (GraphicControlExtension) findBlock( 594 blocks.blocks, GRAPHIC_CONTROL_EXTENSION); 595 596 final int height = bhi.logicalScreenHeight; 597 final int width = bhi.logicalScreenWidth; 598 599 final List<String> comments = getComments(blocks.blocks); 600 final int bitsPerPixel = (bhi.colorResolution + 1); 601 final ImageFormat format = ImageFormats.GIF; 602 final String formatName = "GIF Graphics Interchange Format"; 603 final String mimeType = "image/gif"; 604 605 final int numberOfImages = findAllBlocks(blocks.blocks, IMAGE_SEPARATOR).size(); 606 607 final boolean progressive = id.interlaceFlag; 608 609 final int physicalWidthDpi = 72; 610 final float physicalWidthInch = (float) ((double) width / (double) physicalWidthDpi); 611 final int physicalHeightDpi = 72; 612 final float physicalHeightInch = (float) ((double) height / (double) physicalHeightDpi); 613 614 final String formatDetails = "Gif " + ((char) blocks.gifHeaderInfo.version1) 615 + ((char) blocks.gifHeaderInfo.version2) 616 + ((char) blocks.gifHeaderInfo.version3); 617 618 boolean transparent = false; 619 if (gce != null && gce.transparency) { 620 transparent = true; 621 } 622 623 final boolean usesPalette = true; 624 final ImageInfo.ColorType colorType = ImageInfo.ColorType.RGB; 625 final ImageInfo.CompressionAlgorithm compressionAlgorithm = ImageInfo.CompressionAlgorithm.LZW; 626 627 return new ImageInfo(formatDetails, bitsPerPixel, comments, 628 format, formatName, height, mimeType, numberOfImages, 629 physicalHeightDpi, physicalHeightInch, physicalWidthDpi, 630 physicalWidthInch, width, progressive, transparent, 631 usesPalette, colorType, compressionAlgorithm); 632 } 633 634 @Override 635 public boolean dumpImageFile(final PrintWriter pw, final ByteSource byteSource) 636 throws ImageReadException, IOException { 637 pw.println("gif.dumpImageFile"); 638 639 final ImageInfo imageData = getImageInfo(byteSource); 640 if (imageData == null) { 641 return false; 642 } 643 644 imageData.toString(pw, ""); 645 646 final GifImageContents blocks = readFile(byteSource, false); 647 648 pw.println("gif.blocks: " + blocks.blocks.size()); 649 for (int i = 0; i < blocks.blocks.size(); i++) { 650 final GifBlock gifBlock = blocks.blocks.get(i); 651 this.debugNumber(pw, "\t" + i + " (" 652 + gifBlock.getClass().getName() + ")", 653 gifBlock.blockCode, 4); 654 } 655 656 pw.println(""); 657 658 return true; 659 } 660 661 private int[] getColorTable(final byte[] bytes) throws ImageReadException { 662 if ((bytes.length % 3) != 0) { 663 throw new ImageReadException("Bad Color Table Length: " 664 + bytes.length); 665 } 666 final int length = bytes.length / 3; 667 668 final int[] result = new int[length]; 669 670 for (int i = 0; i < length; i++) { 671 final int red = 0xff & bytes[(i * 3) + 0]; 672 final int green = 0xff & bytes[(i * 3) + 1]; 673 final int blue = 0xff & bytes[(i * 3) + 2]; 674 675 final int alpha = 0xff; 676 677 final int rgb = (alpha << 24) | (red << 16) | (green << 8) | (blue << 0); 678 result[i] = rgb; 679 } 680 681 return result; 682 } 683 684 @Override 685 public FormatCompliance getFormatCompliance(final ByteSource byteSource) 686 throws ImageReadException, IOException { 687 final FormatCompliance result = new FormatCompliance( 688 byteSource.getDescription()); 689 690 readFile(byteSource, false, result); 691 692 return result; 693 } 694 695 private List<GifImageData> findAllImageData(GifImageContents imageContents) throws ImageReadException { 696 final List<ImageDescriptor> descriptors = findAllBlocks(imageContents.blocks, IMAGE_SEPARATOR); 697 698 if (descriptors.isEmpty()) { 699 throw new ImageReadException("GIF: Couldn't read Image Descriptor"); 700 } 701 702 final List<GraphicControlExtension> gcExtensions = findAllBlocks(imageContents.blocks, GRAPHIC_CONTROL_EXTENSION); 703 704 if (!gcExtensions.isEmpty() && gcExtensions.size() != descriptors.size()) { 705 throw new ImageReadException("GIF: Invalid amount of Graphic Control Extensions"); 706 } 707 708 List<GifImageData> imageData = new ArrayList<>(descriptors.size()); 709 for(int i = 0; i < descriptors.size(); i++) { 710 final ImageDescriptor descriptor = descriptors.get(i); 711 if (descriptor == null) { 712 throw new ImageReadException(String.format("GIF: Couldn't read Image Descriptor of image number %d", i)); 713 } 714 715 final GraphicControlExtension gce = gcExtensions.isEmpty() ? null : gcExtensions.get(i); 716 717 imageData.add(new GifImageData(descriptor, gce)); 718 } 719 720 return imageData; 721 } 722 723 private GifImageData findFirstImageData(GifImageContents imageContents) throws ImageReadException { 724 final ImageDescriptor descriptor = (ImageDescriptor) findBlock(imageContents.blocks, 725 IMAGE_SEPARATOR); 726 727 if (descriptor == null) { 728 throw new ImageReadException("GIF: Couldn't read Image Descriptor"); 729 } 730 731 final GraphicControlExtension gce = (GraphicControlExtension) findBlock( 732 imageContents.blocks, GRAPHIC_CONTROL_EXTENSION); 733 734 return new GifImageData(descriptor, gce); 735 } 736 737 private BufferedImage getBufferedImage(GifHeaderInfo headerInfo, GifImageData imageData, byte[] globalColorTable) 738 throws ImageReadException { 739 final ImageDescriptor id = imageData.descriptor; 740 final GraphicControlExtension gce = imageData.gce; 741 742 final int width = id.imageWidth; 743 final int height = id.imageHeight; 744 745 boolean hasAlpha = false; 746 if (gce != null && gce.transparency) { 747 hasAlpha = true; 748 } 749 750 final ImageBuilder imageBuilder = new ImageBuilder(width, height, hasAlpha); 751 752 int[] colorTable; 753 if (id.localColorTable != null) { 754 colorTable = getColorTable(id.localColorTable); 755 } else if (globalColorTable != null) { 756 colorTable = getColorTable(globalColorTable); 757 } else { 758 throw new ImageReadException("Gif: No Color Table"); 759 } 760 761 int transparentIndex = -1; 762 if (gce != null && hasAlpha) { 763 transparentIndex = gce.transparentColorIndex; 764 } 765 766 int counter = 0; 767 768 final int rowsInPass1 = (height + 7) / 8; 769 final int rowsInPass2 = (height + 3) / 8; 770 final int rowsInPass3 = (height + 1) / 4; 771 final int rowsInPass4 = (height) / 2; 772 773 for (int row = 0; row < height; row++) { 774 int y; 775 if (id.interlaceFlag) { 776 int theRow = row; 777 if (theRow < rowsInPass1) { 778 y = theRow * 8; 779 } else { 780 theRow -= rowsInPass1; 781 if (theRow < (rowsInPass2)) { 782 y = 4 + (theRow * 8); 783 } else { 784 theRow -= rowsInPass2; 785 if (theRow < (rowsInPass3)) { 786 y = 2 + (theRow * 4); 787 } else { 788 theRow -= rowsInPass3; 789 if (theRow < (rowsInPass4)) { 790 y = 1 + (theRow * 2); 791 } else { 792 throw new ImageReadException("Gif: Strange Row"); 793 } 794 } 795 } 796 } 797 } else { 798 y = row; 799 } 800 801 for (int x = 0; x < width; x++) { 802 final int index = 0xff & id.imageData[counter++]; 803 int rgb = colorTable[index]; 804 805 if (transparentIndex == index) { 806 rgb = 0x00; 807 } 808 imageBuilder.setRGB(x,y, rgb); 809 } 810 } 811 812 return imageBuilder.getBufferedImage(); 813 } 814 815 @Override 816 public List<BufferedImage> getAllBufferedImages(final ByteSource byteSource) 817 throws ImageReadException, IOException { 818 final GifImageContents imageContents = readFile(byteSource, false); 819 820 if (imageContents == null) { 821 throw new ImageReadException("GIF: Couldn't read blocks"); 822 } 823 824 final GifHeaderInfo ghi = imageContents.gifHeaderInfo; 825 if (ghi == null) { 826 throw new ImageReadException("GIF: Couldn't read Header"); 827 } 828 829 final List<GifImageData> imageData = findAllImageData(imageContents); 830 List<BufferedImage> result = new ArrayList<>(imageData.size()); 831 for(GifImageData id : imageData) { 832 result.add(getBufferedImage(ghi, id, imageContents.globalColorTable)); 833 } 834 return result; 835 } 836 837 @Override 838 public BufferedImage getBufferedImage(final ByteSource byteSource, final Map<String, Object> params) 839 throws ImageReadException, IOException { 840 final GifImageContents imageContents = readFile(byteSource, false); 841 842 if (imageContents == null) { 843 throw new ImageReadException("GIF: Couldn't read blocks"); 844 } 845 846 final GifHeaderInfo ghi = imageContents.gifHeaderInfo; 847 if (ghi == null) { 848 throw new ImageReadException("GIF: Couldn't read Header"); 849 } 850 851 final GifImageData imageData = findFirstImageData(imageContents); 852 853 return getBufferedImage(ghi, imageData, imageContents.globalColorTable); 854 } 855 856 private void writeAsSubBlocks(final OutputStream os, final byte[] bytes) throws IOException { 857 int index = 0; 858 859 while (index < bytes.length) { 860 final int blockSize = Math.min(bytes.length - index, 255); 861 os.write(blockSize); 862 os.write(bytes, index, blockSize); 863 index += blockSize; 864 } 865 os.write(0); // last block 866 } 867 868 @Override 869 public void writeImage(final BufferedImage src, final OutputStream os, Map<String, Object> params) 870 throws ImageWriteException, IOException { 871 // make copy of params; we'll clear keys as we consume them. 872 params = new HashMap<>(params); 873 874 // clear format key. 875 if (params.containsKey(PARAM_KEY_FORMAT)) { 876 params.remove(PARAM_KEY_FORMAT); 877 } 878 879 String xmpXml = null; 880 if (params.containsKey(PARAM_KEY_XMP_XML)) { 881 xmpXml = (String) params.get(PARAM_KEY_XMP_XML); 882 params.remove(PARAM_KEY_XMP_XML); 883 } 884 885 if (!params.isEmpty()) { 886 final Object firstKey = params.keySet().iterator().next(); 887 throw new ImageWriteException("Unknown parameter: " + firstKey); 888 } 889 890 final int width = src.getWidth(); 891 final int height = src.getHeight(); 892 893 final boolean hasAlpha = new PaletteFactory().hasTransparency(src); 894 895 final int maxColors = hasAlpha ? 255 : 256; 896 897 Palette palette2 = new PaletteFactory().makeExactRgbPaletteSimple(src, maxColors); 898 // int palette[] = new PaletteFactory().makePaletteSimple(src, 256); 899 // Map palette_map = paletteToMap(palette); 900 901 if (palette2 == null) { 902 palette2 = new PaletteFactory().makeQuantizedRgbPalette(src, maxColors); 903 if (LOGGER.isLoggable(Level.FINE)) { 904 LOGGER.fine("quantizing"); 905 } 906 } else if (LOGGER.isLoggable(Level.FINE)) { 907 LOGGER.fine("exact palette"); 908 } 909 910 if (palette2 == null) { 911 throw new ImageWriteException("Gif: can't write images with more than 256 colors"); 912 } 913 final int paletteSize = palette2.length() + (hasAlpha ? 1 : 0); 914 915 final BinaryOutputStream bos = new BinaryOutputStream(os, ByteOrder.LITTLE_ENDIAN); 916 917 // write Header 918 os.write(0x47); // G magic numbers 919 os.write(0x49); // I 920 os.write(0x46); // F 921 922 os.write(0x38); // 8 version magic numbers 923 os.write(0x39); // 9 924 os.write(0x61); // a 925 926 // Logical Screen Descriptor. 927 928 bos.write2Bytes(width); 929 bos.write2Bytes(height); 930 931 final int colorTableScaleLessOne = (paletteSize > 128) ? 7 932 : (paletteSize > 64) ? 6 : (paletteSize > 32) ? 5 933 : (paletteSize > 16) ? 4 : (paletteSize > 8) ? 3 934 : (paletteSize > 4) ? 2 935 : (paletteSize > 2) ? 1 : 0; 936 937 final int colorTableSizeInFormat = 1 << (colorTableScaleLessOne + 1); 938 { 939 final byte colorResolution = (byte) colorTableScaleLessOne; // TODO: 940 final int packedFields = (7 & colorResolution) * 16; 941 bos.write(packedFields); // one byte 942 } 943 { 944 final byte backgroundColorIndex = 0; 945 bos.write(backgroundColorIndex); 946 } 947 { 948 final byte pixelAspectRatio = 0; 949 bos.write(pixelAspectRatio); 950 } 951 952 //{ 953 // write Global Color Table. 954 955 //} 956 957 { // ALWAYS write GraphicControlExtension 958 bos.write(EXTENSION_CODE); 959 bos.write((byte) 0xf9); 960 // bos.write(0xff & (kGraphicControlExtension >> 8)); 961 // bos.write(0xff & (kGraphicControlExtension >> 0)); 962 963 bos.write((byte) 4); // block size; 964 final int packedFields = hasAlpha ? 1 : 0; // transparency flag 965 bos.write((byte) packedFields); 966 bos.write((byte) 0); // Delay Time 967 bos.write((byte) 0); // Delay Time 968 bos.write((byte) (hasAlpha ? palette2.length() : 0)); // Transparent 969 // Color 970 // Index 971 bos.write((byte) 0); // terminator 972 } 973 974 if (null != xmpXml) { 975 bos.write(EXTENSION_CODE); 976 bos.write(APPLICATION_EXTENSION_LABEL); 977 978 bos.write(XMP_APPLICATION_ID_AND_AUTH_CODE.length); // 0x0B 979 bos.write(XMP_APPLICATION_ID_AND_AUTH_CODE); 980 981 final byte[] xmpXmlBytes = xmpXml.getBytes(StandardCharsets.UTF_8); 982 bos.write(xmpXmlBytes); 983 984 // write "magic trailer" 985 for (int magic = 0; magic <= 0xff; magic++) { 986 bos.write(0xff - magic); 987 } 988 989 bos.write((byte) 0); // terminator 990 991 } 992 993 { // Image Descriptor. 994 bos.write(IMAGE_SEPARATOR); 995 bos.write2Bytes(0); // Image Left Position 996 bos.write2Bytes(0); // Image Top Position 997 bos.write2Bytes(width); // Image Width 998 bos.write2Bytes(height); // Image Height 999 1000 { 1001 final boolean localColorTableFlag = true; 1002 // boolean LocalColorTableFlag = false; 1003 final boolean interlaceFlag = false; 1004 final boolean sortFlag = false; 1005 final int sizeOfLocalColorTable = colorTableScaleLessOne; 1006 1007 // int SizeOfLocalColorTable = 0; 1008 1009 final int packedFields; 1010 if (localColorTableFlag) { 1011 packedFields = (LOCAL_COLOR_TABLE_FLAG_MASK 1012 | (interlaceFlag ? INTERLACE_FLAG_MASK : 0) 1013 | (sortFlag ? SORT_FLAG_MASK : 0) 1014 | (7 & sizeOfLocalColorTable)); 1015 } else { 1016 packedFields = (0 1017 | (interlaceFlag ? INTERLACE_FLAG_MASK : 0) 1018 | (sortFlag ? SORT_FLAG_MASK : 0) 1019 | (7 & sizeOfLocalColorTable)); 1020 } 1021 bos.write(packedFields); // one byte 1022 } 1023 } 1024 1025 { // write Local Color Table. 1026 for (int i = 0; i < colorTableSizeInFormat; i++) { 1027 if (i < palette2.length()) { 1028 final int rgb = palette2.getEntry(i); 1029 1030 final int red = 0xff & (rgb >> 16); 1031 final int green = 0xff & (rgb >> 8); 1032 final int blue = 0xff & (rgb >> 0); 1033 1034 bos.write(red); 1035 bos.write(green); 1036 bos.write(blue); 1037 } else { 1038 bos.write(0); 1039 bos.write(0); 1040 bos.write(0); 1041 } 1042 } 1043 } 1044 1045 { // get Image Data. 1046// int image_data_total = 0; 1047 1048 int lzwMinimumCodeSize = colorTableScaleLessOne + 1; 1049 // LZWMinimumCodeSize = Math.max(8, LZWMinimumCodeSize); 1050 if (lzwMinimumCodeSize < 2) { 1051 lzwMinimumCodeSize = 2; 1052 } 1053 1054 // TODO: 1055 // make 1056 // better 1057 // choice 1058 // here. 1059 bos.write(lzwMinimumCodeSize); 1060 1061 final MyLzwCompressor compressor = new MyLzwCompressor( 1062 lzwMinimumCodeSize, ByteOrder.LITTLE_ENDIAN, false); // GIF 1063 // Mode); 1064 1065 final byte[] imagedata = new byte[width * height]; 1066 for (int y = 0; y < height; y++) { 1067 for (int x = 0; x < width; x++) { 1068 final int argb = src.getRGB(x, y); 1069 final int rgb = 0xffffff & argb; 1070 int index; 1071 1072 if (hasAlpha) { 1073 final int alpha = 0xff & (argb >> 24); 1074 final int alphaThreshold = 255; 1075 if (alpha < alphaThreshold) { 1076 index = palette2.length(); // is transparent 1077 } else { 1078 index = palette2.getPaletteIndex(rgb); 1079 } 1080 } else { 1081 index = palette2.getPaletteIndex(rgb); 1082 } 1083 1084 imagedata[y * width + x] = (byte) index; 1085 } 1086 } 1087 1088 final byte[] compressed = compressor.compress(imagedata); 1089 writeAsSubBlocks(bos, compressed); 1090// image_data_total += compressed.length; 1091 } 1092 1093 // palette2.dump(); 1094 1095 bos.write(TERMINATOR_BYTE); 1096 1097 bos.close(); 1098 os.close(); 1099 } 1100 1101 /** 1102 * Extracts embedded XML metadata as XML string. 1103 * <p> 1104 * 1105 * @param byteSource 1106 * File containing image data. 1107 * @param params 1108 * Map of optional parameters, defined in ImagingConstants. 1109 * @return Xmp Xml as String, if present. Otherwise, returns null. 1110 */ 1111 @Override 1112 public String getXmpXml(final ByteSource byteSource, final Map<String, Object> params) 1113 throws ImageReadException, IOException { 1114 try (InputStream is = byteSource.getInputStream()) { 1115 final FormatCompliance formatCompliance = null; 1116 final GifHeaderInfo ghi = readHeader(is, formatCompliance); 1117 1118 if (ghi.globalColorTableFlag) { 1119 readColorTable(is, ghi.sizeOfGlobalColorTable); 1120 } 1121 1122 final List<GifBlock> blocks = readBlocks(ghi, is, true, formatCompliance); 1123 1124 final List<String> result = new ArrayList<>(); 1125 for (final GifBlock block : blocks) { 1126 if (block.blockCode != XMP_COMPLETE_CODE) { 1127 continue; 1128 } 1129 1130 final GenericGifBlock genericBlock = (GenericGifBlock) block; 1131 1132 final byte[] blockBytes = genericBlock.appendSubBlocks(true); 1133 if (blockBytes.length < XMP_APPLICATION_ID_AND_AUTH_CODE.length) { 1134 continue; 1135 } 1136 1137 if (!compareBytes(blockBytes, 0, 1138 XMP_APPLICATION_ID_AND_AUTH_CODE, 0, 1139 XMP_APPLICATION_ID_AND_AUTH_CODE.length)) { 1140 continue; 1141 } 1142 1143 final byte[] GIF_MAGIC_TRAILER = new byte[256]; 1144 for (int magic = 0; magic <= 0xff; magic++) { 1145 GIF_MAGIC_TRAILER[magic] = (byte) (0xff - magic); 1146 } 1147 1148 if (blockBytes.length < XMP_APPLICATION_ID_AND_AUTH_CODE.length 1149 + GIF_MAGIC_TRAILER.length) { 1150 continue; 1151 } 1152 if (!compareBytes(blockBytes, blockBytes.length 1153 - GIF_MAGIC_TRAILER.length, GIF_MAGIC_TRAILER, 0, 1154 GIF_MAGIC_TRAILER.length)) { 1155 throw new ImageReadException( 1156 "XMP block in GIF missing magic trailer."); 1157 } 1158 1159 // XMP is UTF-8 encoded xml. 1160 final String xml = new String( 1161 blockBytes, 1162 XMP_APPLICATION_ID_AND_AUTH_CODE.length, 1163 blockBytes.length 1164 - (XMP_APPLICATION_ID_AND_AUTH_CODE.length + GIF_MAGIC_TRAILER.length), 1165 StandardCharsets.UTF_8); 1166 result.add(xml); 1167 } 1168 1169 if (result.isEmpty()) { 1170 return null; 1171 } 1172 if (result.size() > 1) { 1173 throw new ImageReadException("More than one XMP Block in GIF."); 1174 } 1175 return result.get(0); 1176 } 1177 } 1178}