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