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.bytesource.ByteSource; 055import org.apache.commons.imaging.common.mylzw.MyLzwCompressor; 056import org.apache.commons.imaging.common.mylzw.MyLzwDecompressor; 057import org.apache.commons.imaging.palette.Palette; 058import org.apache.commons.imaging.palette.PaletteFactory; 059 060public class GifImageParser extends ImageParser { 061 062 private static final Logger LOGGER = Logger.getLogger(GifImageParser.class.getName()); 063 064 private static final String DEFAULT_EXTENSION = ".gif"; 065 private static final String[] ACCEPTED_EXTENSIONS = { DEFAULT_EXTENSION, }; 066 private static final byte[] GIF_HEADER_SIGNATURE = { 71, 73, 70 }; 067 private static final int EXTENSION_CODE = 0x21; 068 private static final int IMAGE_SEPARATOR = 0x2C; 069 private static final int GRAPHIC_CONTROL_EXTENSION = (EXTENSION_CODE << 8) | 0xf9; 070 private static final int COMMENT_EXTENSION = 0xfe; 071 private static final int PLAIN_TEXT_EXTENSION = 0x01; 072 private static final int XMP_EXTENSION = 0xff; 073 private static final int TERMINATOR_BYTE = 0x3b; 074 private static final int APPLICATION_EXTENSION_LABEL = 0xff; 075 private static final int XMP_COMPLETE_CODE = (EXTENSION_CODE << 8) 076 | XMP_EXTENSION; 077 private static final int LOCAL_COLOR_TABLE_FLAG_MASK = 1 << 7; 078 private static final int INTERLACE_FLAG_MASK = 1 << 6; 079 private static final int SORT_FLAG_MASK = 1 << 5; 080 private static final byte[] XMP_APPLICATION_ID_AND_AUTH_CODE = { 081 0x58, // X 082 0x4D, // M 083 0x50, // P 084 0x20, // 085 0x44, // D 086 0x61, // a 087 0x74, // t 088 0x61, // a 089 0x58, // X 090 0x4D, // M 091 0x50, // P 092 }; 093 094 public GifImageParser() { 095 super.setByteOrder(ByteOrder.LITTLE_ENDIAN); 096 } 097 098 @Override 099 public String getName() { 100 return "Graphics Interchange Format"; 101 } 102 103 @Override 104 public String getDefaultExtension() { 105 return DEFAULT_EXTENSION; 106 } 107 108 @Override 109 protected String[] getAcceptedExtensions() { 110 return ACCEPTED_EXTENSIONS; 111 } 112 113 @Override 114 protected ImageFormat[] getAcceptedTypes() { 115 return new ImageFormat[] { ImageFormats.GIF, // 116 }; 117 } 118 119 private GifHeaderInfo readHeader(final InputStream is, 120 final FormatCompliance formatCompliance) throws ImageReadException, 121 IOException { 122 final byte identifier1 = readByte("identifier1", is, "Not a Valid GIF File"); 123 final byte identifier2 = readByte("identifier2", is, "Not a Valid GIF File"); 124 final byte identifier3 = readByte("identifier3", is, "Not a Valid GIF File"); 125 126 final byte version1 = readByte("version1", is, "Not a Valid GIF File"); 127 final byte version2 = readByte("version2", is, "Not a Valid GIF File"); 128 final byte version3 = readByte("version3", is, "Not a Valid GIF File"); 129 130 if (formatCompliance != null) { 131 formatCompliance.compareBytes("Signature", GIF_HEADER_SIGNATURE, 132 new byte[]{identifier1, identifier2, identifier3,}); 133 formatCompliance.compare("version", 56, version1); 134 formatCompliance.compare("version", new int[] { 55, 57, }, version2); 135 formatCompliance.compare("version", 97, version3); 136 } 137 138 if (LOGGER.isLoggable(Level.FINEST)) { 139 printCharQuad("identifier: ", ((identifier1 << 16) 140 | (identifier2 << 8) | (identifier3 << 0))); 141 printCharQuad("version: ", 142 ((version1 << 16) | (version2 << 8) | (version3 << 0))); 143 } 144 145 final int logicalScreenWidth = read2Bytes("Logical Screen Width", is, "Not a Valid GIF File", getByteOrder()); 146 final int logicalScreenHeight = read2Bytes("Logical Screen Height", is, "Not a Valid GIF File", getByteOrder()); 147 148 if (formatCompliance != null) { 149 formatCompliance.checkBounds("Width", 1, Integer.MAX_VALUE, 150 logicalScreenWidth); 151 formatCompliance.checkBounds("Height", 1, Integer.MAX_VALUE, 152 logicalScreenHeight); 153 } 154 155 final byte packedFields = readByte("Packed Fields", is, 156 "Not a Valid GIF File"); 157 final byte backgroundColorIndex = readByte("Background Color Index", is, 158 "Not a Valid GIF File"); 159 final byte pixelAspectRatio = readByte("Pixel Aspect Ratio", is, 160 "Not a Valid GIF File"); 161 162 if (LOGGER.isLoggable(Level.FINEST)) { 163 printByteBits("PackedFields bits", packedFields); 164 } 165 166 final boolean globalColorTableFlag = ((packedFields & 128) > 0); 167 if (LOGGER.isLoggable(Level.FINEST)) { 168 LOGGER.finest("GlobalColorTableFlag: " + globalColorTableFlag); 169 } 170 final byte colorResolution = (byte) ((packedFields >> 4) & 7); 171 if (LOGGER.isLoggable(Level.FINEST)) { 172 LOGGER.finest("ColorResolution: " + colorResolution); 173 } 174 final boolean sortFlag = ((packedFields & 8) > 0); 175 if (LOGGER.isLoggable(Level.FINEST)) { 176 LOGGER.finest("SortFlag: " + sortFlag); 177 } 178 final byte sizeofGlobalColorTable = (byte) (packedFields & 7); 179 if (LOGGER.isLoggable(Level.FINEST)) { 180 LOGGER.finest("SizeofGlobalColorTable: " 181 + sizeofGlobalColorTable); 182 } 183 184 if (formatCompliance != null) { 185 if (globalColorTableFlag && backgroundColorIndex != -1) { 186 formatCompliance.checkBounds("Background Color Index", 0, 187 convertColorTableSize(sizeofGlobalColorTable), 188 backgroundColorIndex); 189 } 190 } 191 192 return new GifHeaderInfo(identifier1, identifier2, identifier3, 193 version1, version2, version3, logicalScreenWidth, 194 logicalScreenHeight, packedFields, backgroundColorIndex, 195 pixelAspectRatio, globalColorTableFlag, colorResolution, 196 sortFlag, sizeofGlobalColorTable); 197 } 198 199 private GraphicControlExtension readGraphicControlExtension(final int code, 200 final InputStream is) throws IOException { 201 readByte("block_size", is, "GIF: corrupt GraphicControlExt"); 202 final int packed = readByte("packed fields", is, 203 "GIF: corrupt GraphicControlExt"); 204 205 final int dispose = (packed & 0x1c) >> 2; // disposal method 206 final boolean transparency = (packed & 1) != 0; 207 208 final int delay = read2Bytes("delay in milliseconds", is, "GIF: corrupt GraphicControlExt", getByteOrder()); 209 final int transparentColorIndex = 0xff & readByte("transparent color index", 210 is, "GIF: corrupt GraphicControlExt"); 211 readByte("block terminator", is, "GIF: corrupt GraphicControlExt"); 212 213 return new GraphicControlExtension(code, packed, dispose, transparency, 214 delay, transparentColorIndex); 215 } 216 217 private byte[] readSubBlock(final InputStream is) throws IOException { 218 final int blockSize = 0xff & readByte("block_size", is, "GIF: corrupt block"); 219 220 return readBytes("block", is, blockSize, "GIF: corrupt block"); 221 } 222 223 private GenericGifBlock readGenericGIFBlock(final InputStream is, final int code) 224 throws IOException { 225 return readGenericGIFBlock(is, code, null); 226 } 227 228 private GenericGifBlock readGenericGIFBlock(final InputStream is, final int code, 229 final byte[] first) throws IOException { 230 final List<byte[]> subblocks = new ArrayList<>(); 231 232 if (first != null) { 233 subblocks.add(first); 234 } 235 236 while (true) { 237 final byte[] bytes = readSubBlock(is); 238 if (bytes.length < 1) { 239 break; 240 } 241 subblocks.add(bytes); 242 } 243 244 return new GenericGifBlock(code, subblocks); 245 } 246 247 private List<GifBlock> readBlocks(final GifHeaderInfo ghi, final InputStream is, 248 final boolean stopBeforeImageData, final FormatCompliance formatCompliance) 249 throws ImageReadException, IOException { 250 final List<GifBlock> result = new ArrayList<>(); 251 252 while (true) { 253 final int code = is.read(); 254 255 switch (code) { 256 case -1: 257 throw new ImageReadException("GIF: unexpected end of data"); 258 259 case IMAGE_SEPARATOR: 260 final ImageDescriptor id = readImageDescriptor(ghi, code, is, 261 stopBeforeImageData, formatCompliance); 262 result.add(id); 263 // if (stopBeforeImageData) 264 // return result; 265 266 break; 267 268 case EXTENSION_CODE: // extension 269 { 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: // 255 (hex 0xFF) Application 290 // Extension Label 291 { 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 private GifImageContents readFile(final ByteSource byteSource, 443 final boolean stopBeforeImageData) throws ImageReadException, IOException { 444 return readFile(byteSource, stopBeforeImageData, 445 FormatCompliance.getDefault()); 446 } 447 448 private GifImageContents readFile(final ByteSource byteSource, 449 final boolean stopBeforeImageData, final FormatCompliance formatCompliance) 450 throws ImageReadException, IOException { 451 try (InputStream is = byteSource.getInputStream()) { 452 final GifHeaderInfo ghi = readHeader(is, formatCompliance); 453 454 byte[] globalColorTable = null; 455 if (ghi.globalColorTableFlag) { 456 globalColorTable = readColorTable(is, 457 ghi.sizeOfGlobalColorTable); 458 } 459 460 final List<GifBlock> blocks = readBlocks(ghi, is, stopBeforeImageData, 461 formatCompliance); 462 463 final GifImageContents result = new GifImageContents(ghi, globalColorTable, 464 blocks); 465 return result; 466 } 467 } 468 469 @Override 470 public byte[] getICCProfileBytes(final ByteSource byteSource, final Map<String, Object> params) 471 throws ImageReadException, IOException { 472 return null; 473 } 474 475 @Override 476 public Dimension getImageSize(final ByteSource byteSource, final Map<String, Object> params) 477 throws ImageReadException, IOException { 478 final GifImageContents blocks = readFile(byteSource, false); 479 480 if (blocks == null) { 481 throw new ImageReadException("GIF: Couldn't read blocks"); 482 } 483 484 final GifHeaderInfo bhi = blocks.gifHeaderInfo; 485 if (bhi == null) { 486 throw new ImageReadException("GIF: Couldn't read Header"); 487 } 488 489 final ImageDescriptor id = (ImageDescriptor) findBlock(blocks.blocks, 490 IMAGE_SEPARATOR); 491 if (id == null) { 492 throw new ImageReadException("GIF: Couldn't read ImageDescriptor"); 493 } 494 495 // Prefer the size information in the ImageDescriptor; it is more 496 // reliable 497 // than the size information in the header. 498 return new Dimension(id.imageWidth, id.imageHeight); 499 } 500 501 // FIXME should throw UOE 502 @Override 503 public ImageMetadata getMetadata(final ByteSource byteSource, final Map<String, Object> params) 504 throws ImageReadException, IOException { 505 return null; 506 } 507 508 private List<String> getComments(final List<GifBlock> blocks) throws IOException { 509 final List<String> result = new ArrayList<>(); 510 final int code = 0x21fe; 511 512 for (final GifBlock block : blocks) { 513 if (block.blockCode == code) { 514 final byte[] bytes = ((GenericGifBlock) block).appendSubBlocks(); 515 result.add(new String(bytes, StandardCharsets.US_ASCII)); 516 } 517 } 518 519 return result; 520 } 521 522 @Override 523 public ImageInfo getImageInfo(final ByteSource byteSource, final Map<String, Object> params) 524 throws ImageReadException, IOException { 525 final GifImageContents blocks = readFile(byteSource, false); 526 527 if (blocks == null) { 528 throw new ImageReadException("GIF: Couldn't read blocks"); 529 } 530 531 final GifHeaderInfo bhi = blocks.gifHeaderInfo; 532 if (bhi == null) { 533 throw new ImageReadException("GIF: Couldn't read Header"); 534 } 535 536 final ImageDescriptor id = (ImageDescriptor) findBlock(blocks.blocks, 537 IMAGE_SEPARATOR); 538 if (id == null) { 539 throw new ImageReadException("GIF: Couldn't read ImageDescriptor"); 540 } 541 542 final GraphicControlExtension gce = (GraphicControlExtension) findBlock( 543 blocks.blocks, GRAPHIC_CONTROL_EXTENSION); 544 545 // Prefer the size information in the ImageDescriptor; it is more 546 // reliable than the size information in the header. 547 final int height = id.imageHeight; 548 final int width = id.imageWidth; 549 550 final List<String> comments = getComments(blocks.blocks); 551 final int bitsPerPixel = (bhi.colorResolution + 1); 552 final ImageFormat format = ImageFormats.GIF; 553 final String formatName = "GIF Graphics Interchange Format"; 554 final String mimeType = "image/gif"; 555 // we ought to count images, but don't yet. 556 final int numberOfImages = -1; 557 558 final boolean progressive = id.interlaceFlag; 559 560 final int physicalWidthDpi = 72; 561 final float physicalWidthInch = (float) ((double) width / (double) physicalWidthDpi); 562 final int physicalHeightDpi = 72; 563 final float physicalHeightInch = (float) ((double) height / (double) physicalHeightDpi); 564 565 final String formatDetails = "Gif " + ((char) blocks.gifHeaderInfo.version1) 566 + ((char) blocks.gifHeaderInfo.version2) 567 + ((char) blocks.gifHeaderInfo.version3); 568 569 boolean transparent = false; 570 if (gce != null && gce.transparency) { 571 transparent = true; 572 } 573 574 final boolean usesPalette = true; 575 final ImageInfo.ColorType colorType = ImageInfo.ColorType.RGB; 576 final ImageInfo.CompressionAlgorithm compressionAlgorithm = ImageInfo.CompressionAlgorithm.LZW; 577 578 return new ImageInfo(formatDetails, bitsPerPixel, comments, 579 format, formatName, height, mimeType, numberOfImages, 580 physicalHeightDpi, physicalHeightInch, physicalWidthDpi, 581 physicalWidthInch, width, progressive, transparent, 582 usesPalette, colorType, compressionAlgorithm); 583 } 584 585 @Override 586 public boolean dumpImageFile(final PrintWriter pw, final ByteSource byteSource) 587 throws ImageReadException, IOException { 588 pw.println("gif.dumpImageFile"); 589 590 final ImageInfo imageData = getImageInfo(byteSource); 591 if (imageData == null) { 592 return false; 593 } 594 595 imageData.toString(pw, ""); 596 597 final GifImageContents blocks = readFile(byteSource, false); 598 599 pw.println("gif.blocks: " + blocks.blocks.size()); 600 for (int i = 0; i < blocks.blocks.size(); i++) { 601 final GifBlock gifBlock = blocks.blocks.get(i); 602 this.debugNumber(pw, "\t" + i + " (" 603 + gifBlock.getClass().getName() + ")", 604 gifBlock.blockCode, 4); 605 } 606 607 pw.println(""); 608 609 return true; 610 } 611 612 private int[] getColorTable(final byte[] bytes) throws ImageReadException { 613 if ((bytes.length % 3) != 0) { 614 throw new ImageReadException("Bad Color Table Length: " 615 + bytes.length); 616 } 617 final int length = bytes.length / 3; 618 619 final int[] result = new int[length]; 620 621 for (int i = 0; i < length; i++) { 622 final int red = 0xff & bytes[(i * 3) + 0]; 623 final int green = 0xff & bytes[(i * 3) + 1]; 624 final int blue = 0xff & bytes[(i * 3) + 2]; 625 626 final int alpha = 0xff; 627 628 final int rgb = (alpha << 24) | (red << 16) | (green << 8) | (blue << 0); 629 result[i] = rgb; 630 } 631 632 return result; 633 } 634 635 @Override 636 public FormatCompliance getFormatCompliance(final ByteSource byteSource) 637 throws ImageReadException, IOException { 638 final FormatCompliance result = new FormatCompliance( 639 byteSource.getDescription()); 640 641 readFile(byteSource, false, result); 642 643 return result; 644 } 645 646 @Override 647 public BufferedImage getBufferedImage(final ByteSource byteSource, final Map<String, Object> params) 648 throws ImageReadException, IOException { 649 final GifImageContents imageContents = readFile(byteSource, false); 650 651 if (imageContents == null) { 652 throw new ImageReadException("GIF: Couldn't read blocks"); 653 } 654 655 final GifHeaderInfo ghi = imageContents.gifHeaderInfo; 656 if (ghi == null) { 657 throw new ImageReadException("GIF: Couldn't read Header"); 658 } 659 660 final ImageDescriptor id = (ImageDescriptor) findBlock(imageContents.blocks, 661 IMAGE_SEPARATOR); 662 if (id == null) { 663 throw new ImageReadException("GIF: Couldn't read Image Descriptor"); 664 } 665 final GraphicControlExtension gce = (GraphicControlExtension) findBlock( 666 imageContents.blocks, GRAPHIC_CONTROL_EXTENSION); 667 668 // Prefer the size information in the ImageDescriptor; it is more 669 // reliable 670 // than the size information in the header. 671 final int width = id.imageWidth; 672 final int height = id.imageHeight; 673 674 boolean hasAlpha = false; 675 if (gce != null && gce.transparency) { 676 hasAlpha = true; 677 } 678 679 final ImageBuilder imageBuilder = new ImageBuilder(width, height, hasAlpha); 680 681 int[] colorTable; 682 if (id.localColorTable != null) { 683 colorTable = getColorTable(id.localColorTable); 684 } else if (imageContents.globalColorTable != null) { 685 colorTable = getColorTable(imageContents.globalColorTable); 686 } else { 687 throw new ImageReadException("Gif: No Color Table"); 688 } 689 690 int transparentIndex = -1; 691 if (gce != null && hasAlpha) { 692 transparentIndex = gce.transparentColorIndex; 693 } 694 695 int counter = 0; 696 697 final int rowsInPass1 = (height + 7) / 8; 698 final int rowsInPass2 = (height + 3) / 8; 699 final int rowsInPass3 = (height + 1) / 4; 700 final int rowsInPass4 = (height) / 2; 701 702 for (int row = 0; row < height; row++) { 703 int y; 704 if (id.interlaceFlag) { 705 int theRow = row; 706 if (theRow < rowsInPass1) { 707 y = theRow * 8; 708 } else { 709 theRow -= rowsInPass1; 710 if (theRow < (rowsInPass2)) { 711 y = 4 + (theRow * 8); 712 } else { 713 theRow -= rowsInPass2; 714 if (theRow < (rowsInPass3)) { 715 y = 2 + (theRow * 4); 716 } else { 717 theRow -= rowsInPass3; 718 if (theRow < (rowsInPass4)) { 719 y = 1 + (theRow * 2); 720 } else { 721 throw new ImageReadException("Gif: Strange Row"); 722 } 723 } 724 } 725 } 726 } else { 727 y = row; 728 } 729 730 for (int x = 0; x < width; x++) { 731 final int index = 0xff & id.imageData[counter++]; 732 int rgb = colorTable[index]; 733 734 if (transparentIndex == index) { 735 rgb = 0x00; 736 } 737 738 imageBuilder.setRGB(x, y, rgb); 739 } 740 741 } 742 743 return imageBuilder.getBufferedImage(); 744 745 } 746 747 private void writeAsSubBlocks(final OutputStream os, final byte[] bytes) throws IOException { 748 int index = 0; 749 750 while (index < bytes.length) { 751 final int blockSize = Math.min(bytes.length - index, 255); 752 os.write(blockSize); 753 os.write(bytes, index, blockSize); 754 index += blockSize; 755 } 756 os.write(0); // last block 757 } 758 759 @Override 760 public void writeImage(final BufferedImage src, final OutputStream os, Map<String, Object> params) 761 throws ImageWriteException, IOException { 762 // make copy of params; we'll clear keys as we consume them. 763 params = new HashMap<>(params); 764 765 // clear format key. 766 if (params.containsKey(PARAM_KEY_FORMAT)) { 767 params.remove(PARAM_KEY_FORMAT); 768 } 769 770 String xmpXml = null; 771 if (params.containsKey(PARAM_KEY_XMP_XML)) { 772 xmpXml = (String) params.get(PARAM_KEY_XMP_XML); 773 params.remove(PARAM_KEY_XMP_XML); 774 } 775 776 if (!params.isEmpty()) { 777 final Object firstKey = params.keySet().iterator().next(); 778 throw new ImageWriteException("Unknown parameter: " + firstKey); 779 } 780 781 final int width = src.getWidth(); 782 final int height = src.getHeight(); 783 784 final boolean hasAlpha = new PaletteFactory().hasTransparency(src); 785 786 final int maxColors = hasAlpha ? 255 : 256; 787 788 Palette palette2 = new PaletteFactory().makeExactRgbPaletteSimple(src, maxColors); 789 // int palette[] = new PaletteFactory().makePaletteSimple(src, 256); 790 // Map palette_map = paletteToMap(palette); 791 792 if (palette2 == null) { 793 palette2 = new PaletteFactory().makeQuantizedRgbPalette(src, maxColors); 794 if (LOGGER.isLoggable(Level.FINE)) { 795 LOGGER.fine("quantizing"); 796 } 797 } else if (LOGGER.isLoggable(Level.FINE)) { 798 LOGGER.fine("exact palette"); 799 } 800 801 if (palette2 == null) { 802 throw new ImageWriteException("Gif: can't write images with more than 256 colors"); 803 } 804 final int paletteSize = palette2.length() + (hasAlpha ? 1 : 0); 805 806 final BinaryOutputStream bos = new BinaryOutputStream(os, ByteOrder.LITTLE_ENDIAN); 807 808 // write Header 809 os.write(0x47); // G magic numbers 810 os.write(0x49); // I 811 os.write(0x46); // F 812 813 os.write(0x38); // 8 version magic numbers 814 os.write(0x39); // 9 815 os.write(0x61); // a 816 817 // Logical Screen Descriptor. 818 819 bos.write2Bytes(width); 820 bos.write2Bytes(height); 821 822 final int colorTableScaleLessOne = (paletteSize > 128) ? 7 823 : (paletteSize > 64) ? 6 : (paletteSize > 32) ? 5 824 : (paletteSize > 16) ? 4 : (paletteSize > 8) ? 3 825 : (paletteSize > 4) ? 2 826 : (paletteSize > 2) ? 1 : 0; 827 828 final int colorTableSizeInFormat = 1 << (colorTableScaleLessOne + 1); 829 { 830 final byte colorResolution = (byte) colorTableScaleLessOne; // TODO: 831 final int packedFields = (7 & colorResolution) * 16; 832 bos.write(packedFields); // one byte 833 } 834 { 835 final byte backgroundColorIndex = 0; 836 bos.write(backgroundColorIndex); 837 } 838 { 839 final byte pixelAspectRatio = 0; 840 bos.write(pixelAspectRatio); 841 } 842 843 //{ 844 // write Global Color Table. 845 846 //} 847 848 { // ALWAYS write GraphicControlExtension 849 bos.write(EXTENSION_CODE); 850 bos.write((byte) 0xf9); 851 // bos.write(0xff & (kGraphicControlExtension >> 8)); 852 // bos.write(0xff & (kGraphicControlExtension >> 0)); 853 854 bos.write((byte) 4); // block size; 855 final int packedFields = hasAlpha ? 1 : 0; // transparency flag 856 bos.write((byte) packedFields); 857 bos.write((byte) 0); // Delay Time 858 bos.write((byte) 0); // Delay Time 859 bos.write((byte) (hasAlpha ? palette2.length() : 0)); // Transparent 860 // Color 861 // Index 862 bos.write((byte) 0); // terminator 863 } 864 865 if (null != xmpXml) { 866 bos.write(EXTENSION_CODE); 867 bos.write(APPLICATION_EXTENSION_LABEL); 868 869 bos.write(XMP_APPLICATION_ID_AND_AUTH_CODE.length); // 0x0B 870 bos.write(XMP_APPLICATION_ID_AND_AUTH_CODE); 871 872 final byte[] xmpXmlBytes = xmpXml.getBytes(StandardCharsets.UTF_8); 873 bos.write(xmpXmlBytes); 874 875 // write "magic trailer" 876 for (int magic = 0; magic <= 0xff; magic++) { 877 bos.write(0xff - magic); 878 } 879 880 bos.write((byte) 0); // terminator 881 882 } 883 884 { // Image Descriptor. 885 bos.write(IMAGE_SEPARATOR); 886 bos.write2Bytes(0); // Image Left Position 887 bos.write2Bytes(0); // Image Top Position 888 bos.write2Bytes(width); // Image Width 889 bos.write2Bytes(height); // Image Height 890 891 { 892 final boolean localColorTableFlag = true; 893 // boolean LocalColorTableFlag = false; 894 final boolean interlaceFlag = false; 895 final boolean sortFlag = false; 896 final int sizeOfLocalColorTable = colorTableScaleLessOne; 897 898 // int SizeOfLocalColorTable = 0; 899 900 final int packedFields; 901 if (localColorTableFlag) { 902 packedFields = (LOCAL_COLOR_TABLE_FLAG_MASK 903 | (interlaceFlag ? INTERLACE_FLAG_MASK : 0) 904 | (sortFlag ? SORT_FLAG_MASK : 0) 905 | (7 & sizeOfLocalColorTable)); 906 } else { 907 packedFields = (0 908 | (interlaceFlag ? INTERLACE_FLAG_MASK : 0) 909 | (sortFlag ? SORT_FLAG_MASK : 0) 910 | (7 & sizeOfLocalColorTable)); 911 } 912 bos.write(packedFields); // one byte 913 } 914 } 915 916 { // write Local Color Table. 917 for (int i = 0; i < colorTableSizeInFormat; i++) { 918 if (i < palette2.length()) { 919 final int rgb = palette2.getEntry(i); 920 921 final int red = 0xff & (rgb >> 16); 922 final int green = 0xff & (rgb >> 8); 923 final int blue = 0xff & (rgb >> 0); 924 925 bos.write(red); 926 bos.write(green); 927 bos.write(blue); 928 } else { 929 bos.write(0); 930 bos.write(0); 931 bos.write(0); 932 } 933 } 934 } 935 936 { // get Image Data. 937// int image_data_total = 0; 938 939 int lzwMinimumCodeSize = colorTableScaleLessOne + 1; 940 // LZWMinimumCodeSize = Math.max(8, LZWMinimumCodeSize); 941 if (lzwMinimumCodeSize < 2) { 942 lzwMinimumCodeSize = 2; 943 } 944 945 // TODO: 946 // make 947 // better 948 // choice 949 // here. 950 bos.write(lzwMinimumCodeSize); 951 952 final MyLzwCompressor compressor = new MyLzwCompressor( 953 lzwMinimumCodeSize, ByteOrder.LITTLE_ENDIAN, false); // GIF 954 // Mode); 955 956 final byte[] imagedata = new byte[width * height]; 957 for (int y = 0; y < height; y++) { 958 for (int x = 0; x < width; x++) { 959 final int argb = src.getRGB(x, y); 960 final int rgb = 0xffffff & argb; 961 int index; 962 963 if (hasAlpha) { 964 final int alpha = 0xff & (argb >> 24); 965 final int alphaThreshold = 255; 966 if (alpha < alphaThreshold) { 967 index = palette2.length(); // is transparent 968 } else { 969 index = palette2.getPaletteIndex(rgb); 970 } 971 } else { 972 index = palette2.getPaletteIndex(rgb); 973 } 974 975 imagedata[y * width + x] = (byte) index; 976 } 977 } 978 979 final byte[] compressed = compressor.compress(imagedata); 980 writeAsSubBlocks(bos, compressed); 981// image_data_total += compressed.length; 982 } 983 984 // palette2.dump(); 985 986 bos.write(TERMINATOR_BYTE); 987 988 bos.close(); 989 os.close(); 990 } 991 992 /** 993 * Extracts embedded XML metadata as XML string. 994 * <p> 995 * 996 * @param byteSource 997 * File containing image data. 998 * @param params 999 * Map of optional parameters, defined in ImagingConstants. 1000 * @return Xmp Xml as String, if present. Otherwise, returns null. 1001 */ 1002 @Override 1003 public String getXmpXml(final ByteSource byteSource, final Map<String, Object> params) 1004 throws ImageReadException, IOException { 1005 try (InputStream is = byteSource.getInputStream()) { 1006 final FormatCompliance formatCompliance = null; 1007 final GifHeaderInfo ghi = readHeader(is, formatCompliance); 1008 1009 if (ghi.globalColorTableFlag) { 1010 readColorTable(is, ghi.sizeOfGlobalColorTable); 1011 } 1012 1013 final List<GifBlock> blocks = readBlocks(ghi, is, true, formatCompliance); 1014 1015 final List<String> result = new ArrayList<>(); 1016 for (final GifBlock block : blocks) { 1017 if (block.blockCode != XMP_COMPLETE_CODE) { 1018 continue; 1019 } 1020 1021 final GenericGifBlock genericBlock = (GenericGifBlock) block; 1022 1023 final byte[] blockBytes = genericBlock.appendSubBlocks(true); 1024 if (blockBytes.length < XMP_APPLICATION_ID_AND_AUTH_CODE.length) { 1025 continue; 1026 } 1027 1028 if (!compareBytes(blockBytes, 0, 1029 XMP_APPLICATION_ID_AND_AUTH_CODE, 0, 1030 XMP_APPLICATION_ID_AND_AUTH_CODE.length)) { 1031 continue; 1032 } 1033 1034 final byte[] GIF_MAGIC_TRAILER = new byte[256]; 1035 for (int magic = 0; magic <= 0xff; magic++) { 1036 GIF_MAGIC_TRAILER[magic] = (byte) (0xff - magic); 1037 } 1038 1039 if (blockBytes.length < XMP_APPLICATION_ID_AND_AUTH_CODE.length 1040 + GIF_MAGIC_TRAILER.length) { 1041 continue; 1042 } 1043 if (!compareBytes(blockBytes, blockBytes.length 1044 - GIF_MAGIC_TRAILER.length, GIF_MAGIC_TRAILER, 0, 1045 GIF_MAGIC_TRAILER.length)) { 1046 throw new ImageReadException( 1047 "XMP block in GIF missing magic trailer."); 1048 } 1049 1050 // XMP is UTF-8 encoded xml. 1051 final String xml = new String( 1052 blockBytes, 1053 XMP_APPLICATION_ID_AND_AUTH_CODE.length, 1054 blockBytes.length 1055 - (XMP_APPLICATION_ID_AND_AUTH_CODE.length + GIF_MAGIC_TRAILER.length), 1056 StandardCharsets.UTF_8); 1057 result.add(xml); 1058 } 1059 1060 if (result.size() < 1) { 1061 return null; 1062 } 1063 if (result.size() > 1) { 1064 throw new ImageReadException("More than one XMP Block in GIF."); 1065 } 1066 return result.get(0); 1067 } 1068 } 1069}