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}