    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}