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