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