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