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