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