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