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