View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.imaging.formats.gif;
18  
19  import static org.apache.commons.imaging.common.BinaryFunctions.compareBytes;
20  import static org.apache.commons.imaging.common.BinaryFunctions.logByteBits;
21  import static org.apache.commons.imaging.common.BinaryFunctions.logCharQuad;
22  import static org.apache.commons.imaging.common.BinaryFunctions.read2Bytes;
23  import static org.apache.commons.imaging.common.BinaryFunctions.readByte;
24  import static org.apache.commons.imaging.common.BinaryFunctions.readBytes;
25  
26  import java.awt.Dimension;
27  import java.awt.image.BufferedImage;
28  import java.io.ByteArrayInputStream;
29  import java.io.IOException;
30  import java.io.InputStream;
31  import java.io.OutputStream;
32  import java.io.PrintWriter;
33  import java.nio.ByteOrder;
34  import java.nio.charset.StandardCharsets;
35  import java.util.ArrayList;
36  import java.util.List;
37  import java.util.logging.Level;
38  import java.util.logging.Logger;
39  
40  import org.apache.commons.imaging.AbstractImageParser;
41  import org.apache.commons.imaging.FormatCompliance;
42  import org.apache.commons.imaging.ImageFormat;
43  import org.apache.commons.imaging.ImageFormats;
44  import org.apache.commons.imaging.ImageInfo;
45  import org.apache.commons.imaging.ImagingException;
46  import org.apache.commons.imaging.bytesource.ByteSource;
47  import org.apache.commons.imaging.common.Allocator;
48  import org.apache.commons.imaging.common.BinaryOutputStream;
49  import org.apache.commons.imaging.common.ImageBuilder;
50  import org.apache.commons.imaging.common.ImageMetadata;
51  import org.apache.commons.imaging.common.XmpEmbeddable;
52  import org.apache.commons.imaging.common.XmpImagingParameters;
53  import org.apache.commons.imaging.mylzw.MyLzwCompressor;
54  import org.apache.commons.imaging.mylzw.MyLzwDecompressor;
55  import org.apache.commons.imaging.palette.Palette;
56  import org.apache.commons.imaging.palette.PaletteFactory;
57  
58  public class GifImageParser extends AbstractImageParser<GifImagingParameters> implements XmpEmbeddable<GifImagingParameters> {
59  
60      private static final Logger LOGGER = Logger.getLogger(GifImageParser.class.getName());
61  
62      private static final String DEFAULT_EXTENSION = ImageFormats.GIF.getDefaultExtension();
63      private static final String[] ACCEPTED_EXTENSIONS = ImageFormats.GIF.getExtensions();
64      private static final byte[] GIF_HEADER_SIGNATURE = { 71, 73, 70 };
65      private static final int EXTENSION_CODE = 0x21;
66      private static final int IMAGE_SEPARATOR = 0x2C;
67      private static final int GRAPHIC_CONTROL_EXTENSION = EXTENSION_CODE << 8 | 0xf9;
68      private static final int COMMENT_EXTENSION = 0xfe;
69      private static final int PLAIN_TEXT_EXTENSION = 0x01;
70      private static final int XMP_EXTENSION = 0xff;
71      private static final int TERMINATOR_BYTE = 0x3b;
72      private static final int APPLICATION_EXTENSION_LABEL = 0xff;
73      private static final int XMP_COMPLETE_CODE = EXTENSION_CODE << 8 | XMP_EXTENSION;
74      private static final int LOCAL_COLOR_TABLE_FLAG_MASK = 1 << 7;
75      private static final int INTERLACE_FLAG_MASK = 1 << 6;
76      private static final int SORT_FLAG_MASK = 1 << 5;
77      private static final byte[] XMP_APPLICATION_ID_AND_AUTH_CODE = { 0x58, // X
78              0x4D, // M
79              0x50, // P
80              0x20, //
81              0x44, // D
82              0x61, // a
83              0x74, // t
84              0x61, // a
85              0x58, // X
86              0x4D, // M
87              0x50, // P
88      };
89  
90      // Made internal for testability.
91      static DisposalMethod createDisposalMethodFromIntValue(final int value) throws ImagingException {
92          switch (value) {
93          case 0:
94              return DisposalMethod.UNSPECIFIED;
95          case 1:
96              return DisposalMethod.DO_NOT_DISPOSE;
97          case 2:
98              return DisposalMethod.RESTORE_TO_BACKGROUND;
99          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 }