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