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 java.awt.Dimension;
20  import java.awt.image.BufferedImage;
21  import java.io.ByteArrayInputStream;
22  import java.io.IOException;
23  import java.io.InputStream;
24  import java.io.OutputStream;
25  import java.io.PrintWriter;
26  import java.nio.ByteOrder;
27  import java.nio.charset.StandardCharsets;
28  import java.util.ArrayList;
29  import java.util.List;
30  import java.util.logging.Level;
31  import java.util.logging.Logger;
32  
33  import org.apache.commons.imaging.AbstractImageParser;
34  import org.apache.commons.imaging.FormatCompliance;
35  import org.apache.commons.imaging.ImageFormat;
36  import org.apache.commons.imaging.ImageFormats;
37  import org.apache.commons.imaging.ImageInfo;
38  import org.apache.commons.imaging.ImagingException;
39  import org.apache.commons.imaging.bytesource.ByteSource;
40  import org.apache.commons.imaging.common.AbstractBinaryOutputStream;
41  import org.apache.commons.imaging.common.Allocator;
42  import org.apache.commons.imaging.common.BinaryFunctions;
43  import org.apache.commons.imaging.common.ImageBuilder;
44  import org.apache.commons.imaging.common.ImageMetadata;
45  import org.apache.commons.imaging.common.XmpEmbeddable;
46  import org.apache.commons.imaging.common.XmpImagingParameters;
47  import org.apache.commons.imaging.mylzw.MyLzwCompressor;
48  import org.apache.commons.imaging.mylzw.MyLzwDecompressor;
49  import org.apache.commons.imaging.palette.Palette;
50  import org.apache.commons.imaging.palette.PaletteFactory;
51  
52  public class GifImageParser extends AbstractImageParser<GifImagingParameters> implements XmpEmbeddable<GifImagingParameters> {
53  
54      private static final Logger LOGGER = Logger.getLogger(GifImageParser.class.getName());
55  
56      private static final String DEFAULT_EXTENSION = ImageFormats.GIF.getDefaultExtension();
57      private static final String[] ACCEPTED_EXTENSIONS = ImageFormats.GIF.getExtensions();
58      private static final byte[] GIF_HEADER_SIGNATURE = { 71, 73, 70 };
59      private static final int EXTENSION_CODE = 0x21;
60      private static final int IMAGE_SEPARATOR = 0x2C;
61      private static final int GRAPHIC_CONTROL_EXTENSION = EXTENSION_CODE << 8 | 0xf9;
62      private static final int COMMENT_EXTENSION = 0xfe;
63      private static final int PLAIN_TEXT_EXTENSION = 0x01;
64      private static final int XMP_EXTENSION = 0xff;
65      private static final int TERMINATOR_BYTE = 0x3b;
66      private static final int APPLICATION_EXTENSION_LABEL = 0xff;
67      private static final int XMP_COMPLETE_CODE = EXTENSION_CODE << 8 | XMP_EXTENSION;
68      private static final int LOCAL_COLOR_TABLE_FLAG_MASK = 1 << 7;
69      private static final int INTERLACE_FLAG_MASK = 1 << 6;
70      private static final int SORT_FLAG_MASK = 1 << 5;
71      private static final byte[] XMP_APPLICATION_ID_AND_AUTH_CODE = { 0x58, // X
72              0x4D, // M
73              0x50, // P
74              0x20, //
75              0x44, // D
76              0x61, // a
77              0x74, // t
78              0x61, // a
79              0x58, // X
80              0x4D, // M
81              0x50, // P
82      };
83  
84      // Made internal for testability.
85      static DisposalMethod createDisposalMethodFromIntValue(final int value) throws ImagingException {
86          switch (value) {
87          case 0:
88              return DisposalMethod.UNSPECIFIED;
89          case 1:
90              return DisposalMethod.DO_NOT_DISPOSE;
91          case 2:
92              return DisposalMethod.RESTORE_TO_BACKGROUND;
93          case 3:
94              return DisposalMethod.RESTORE_TO_PREVIOUS;
95          case 4:
96              return DisposalMethod.TO_BE_DEFINED_1;
97          case 5:
98              return DisposalMethod.TO_BE_DEFINED_2;
99          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 }