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