package com.bumptech.glide.gifdecoder; import static com.bumptech.glide.gifdecoder.GifDecoder.STATUS_FORMAT_ERROR; import static com.bumptech.glide.gifdecoder.GifFrame.DISPOSAL_NONE; import static com.bumptech.glide.gifdecoder.GifFrame.DISPOSAL_UNSPECIFIED; import android.util.Log; import java.nio.BufferUnderflowException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.Arrays; /** * A class responsible for creating {@link com.bumptech.glide.gifdecoder.GifHeader}s from data * representing animated GIFs. * * @see <a href="https://www.w3.org/Graphics/GIF/spec-gif89a.txt">GIF 89a Specification</a> */ public class GifHeaderParser { public static final String TAG = "GifHeaderParser"; private static final int MASK_INT_LOWEST_BYTE = 0x000000FF; /** Identifies the beginning of an Image Descriptor. */ private static final int IMAGE_SEPARATOR = 0x2C; /** Identifies the beginning of an extension block. */ private static final int EXTENSION_INTRODUCER = 0x21; /** This block is a single-field block indicating the end of the GIF Data Stream. */ private static final int TRAILER = 0x3B; // Possible labels that identify the current extension block. private static final int LABEL_GRAPHIC_CONTROL_EXTENSION = 0xF9; private static final int LABEL_APPLICATION_EXTENSION = 0xFF; private static final int LABEL_COMMENT_EXTENSION = 0xFE; private static final int LABEL_PLAIN_TEXT_EXTENSION = 0x01; // Graphic Control Extension packed field masks private static final int GCE_MASK_RESERVED_BITS = 0b11100000; /** * Mask (bits 4-2) to extract Disposal Method of the current frame. * * @see GifFrame.GifDisposalMethod possible values */ private static final int GCE_MASK_DISPOSAL_METHOD = 0b00011100; /** * Shift so the Disposal Method extracted from the packed value is on the least significant bit. */ private static final int GCE_DISPOSAL_METHOD_SHIFT = 2; private static final int GCE_MASK_USER_INPUT_FLAG = 0b00000010; /** * Mask (bit 0) to extract Transparent Color Flag of the current frame. * <p><b>GIF89a</b>: <i>Indicates whether a transparency index is given * in the Transparent Index field.</i></p> * Possible values are:<ul> * <li>0 - Transparent Index is not given.</li> * <li>1 - Transparent Index is given.</li> * </ul> */ private static final int GCE_MASK_TRANSPARENT_COLOR_FLAG = 0b00000001; // Image Descriptor packed field masks (describing Local Color Table) /** * Mask (bit 7) to extract Local Color Table Flag of the current image. * <p><b>GIF89a</b>: <i>Indicates the presence of a Local Color Table * immediately following this Image Descriptor.</i></p> */ private static final int DESCRIPTOR_MASK_LCT_FLAG = 0b10000000; /** * Mask (bit 6) to extract Interlace Flag of the current image. * <p><b>GIF89a</b>: <i>Indicates if the image is interlaced. * An image is interlaced in a four-pass interlace pattern.</i></p> * Possible values are:<ul> * <li>0 - Image is not interlaced.</li> * <li>1 - Image is interlaced.</li> * </ul> */ private static final int DESCRIPTOR_MASK_INTERLACE_FLAG = 0b01000000; private static final int DESCRIPTOR_MASK_SORT_FLAG = 0b00100000; private static final int DESCRIPTOR_MASK_RESERVED = 0b00011000; /** * Mask (bits 2-0) to extract Size of the Local Color Table of the current image. * <p><b>GIF89a</b>: <i>If the Local Color Table Flag is set to 1, the value in this * field is used to calculate the number of bytes contained in the Local Color Table. * To determine that actual size of the color table, raise 2 to [the value of the field + 1]. * This value should be 0 if there is no Local Color Table specified.</i></p> */ private static final int DESCRIPTOR_MASK_LCT_SIZE = 0b00000111; // Logical Screen Descriptor packed field masks (describing Global Color Table) /** * Mask (bit 7) to extract Global Color Table Flag of the current image. * <p><b>GIF89a</b>: <i>Indicates the presence of a Global Color Table * immediately following this Image Descriptor.</i></p> * Possible values are:<ul> * <li>0 - No Global Color Table follows, the Background Color Index field is meaningless.</li> * <li>1 - A Global Color Table will immediately follow, * the Background Color Index field is meaningful.</li> * </ul> */ private static final int LSD_MASK_GCT_FLAG = 0b10000000; private static final int LSD_MASK_COLOR_RESOLUTION = 0b01110000; private static final int LSD_MASK_SORT_FLAG = 0b00001000; /** * Mask (bits 2-0) to extract Size of the Global Color Table of the current image. * <p><b>GIF89a</b>: <i>If the Global Color Table Flag is set to 1, the value in this * field is used to calculate the number of bytes contained in the Global Color Table. * To determine that actual size of the color table, raise 2 to [the value of the field + 1]. * Even if there is no Global Color Table specified, set this field according to the above * formula so that decoders can choose the best graphics mode to display the stream in.</i></p> */ private static final int LSD_MASK_GCT_SIZE = 0b00000111; /** The minimum frame delay in hundredths of a second. */ static final int MIN_FRAME_DELAY = 2; /** * The default frame delay in hundredths of a second. * This is used for GIFs with frame delays less than the minimum. */ static final int DEFAULT_FRAME_DELAY = 10; private static final int MAX_BLOCK_SIZE = 256; // Raw data read working array. private final byte[] block = new byte[MAX_BLOCK_SIZE]; private ByteBuffer rawData; private GifHeader header; private int blockSize = 0; public GifHeaderParser setData(ByteBuffer data) { reset(); rawData = data.asReadOnlyBuffer(); rawData.position(0); rawData.order(ByteOrder.LITTLE_ENDIAN); return this; } public GifHeaderParser setData(byte[] data) { if (data != null) { setData(ByteBuffer.wrap(data)); } else { rawData = null; header.status = GifDecoder.STATUS_OPEN_ERROR; } return this; } public void clear() { rawData = null; header = null; } private void reset() { rawData = null; Arrays.fill(block, (byte) 0); header = new GifHeader(); blockSize = 0; } public GifHeader parseHeader() { if (rawData == null) { throw new IllegalStateException("You must call setData() before parseHeader()"); } if (err()) { return header; } readHeader(); if (!err()) { readContents(); if (header.frameCount < 0) { header.status = STATUS_FORMAT_ERROR; } } return header; } /** * Determines if the GIF is animated by trying to read in the first 2 frames * This method re-parses the data even if the header has already been read. */ public boolean isAnimated() { readHeader(); if (!err()) { readContents(2 /* maxFrames */); } return header.frameCount > 1; } /** * Main file parser. Reads GIF content blocks. */ private void readContents() { readContents(Integer.MAX_VALUE /* maxFrames */); } /** * Main file parser. Reads GIF content blocks. Stops after reading maxFrames */ private void readContents(int maxFrames) { // Read GIF file content blocks. boolean done = false; while (!(done || err() || header.frameCount > maxFrames)) { int code = read(); switch (code) { case IMAGE_SEPARATOR: // The Graphic Control Extension is optional, but will always come first if it exists. // If one did exist, there will be a non-null current frame which we should use. // However if one did not exist, the current frame will be null // and we must create it here. See issue #134. if (header.currentFrame == null) { header.currentFrame = new GifFrame(); } readBitmap(); break; case EXTENSION_INTRODUCER: int extensionLabel = read(); switch (extensionLabel) { case LABEL_GRAPHIC_CONTROL_EXTENSION: // Start a new frame. header.currentFrame = new GifFrame(); readGraphicControlExt(); break; case LABEL_APPLICATION_EXTENSION: readBlock(); String app = ""; for (int i = 0; i < 11; i++) { app += (char) block[i]; } if (app.equals("NETSCAPE2.0")) { readNetscapeExt(); } else { // Don't care. skip(); } break; case LABEL_COMMENT_EXTENSION: skip(); break; case LABEL_PLAIN_TEXT_EXTENSION: skip(); break; default: // Uninteresting extension. skip(); } break; case TRAILER: // This block is a single-field block indicating the end of the GIF Data Stream. done = true; break; // Bad byte, but keep going and see what happens case 0x00: default: header.status = STATUS_FORMAT_ERROR; } } } /** * Reads Graphic Control Extension values. */ private void readGraphicControlExt() { // Block size. read(); /* * Graphic Control Extension packed field: * 7 6 5 4 3 2 1 0 * +---------------+ * 1 | | | | | * * Reserved 3 Bits * Disposal Method 3 Bits * User Input Flag 1 Bit * Transparent Color Flag 1 Bit */ int packed = read(); // Disposal method. //noinspection WrongConstant field has to be extracted from packed value header.currentFrame.dispose = (packed & GCE_MASK_DISPOSAL_METHOD) >> GCE_DISPOSAL_METHOD_SHIFT; if (header.currentFrame.dispose == DISPOSAL_UNSPECIFIED) { // Elect to keep old image if discretionary. header.currentFrame.dispose = DISPOSAL_NONE; } header.currentFrame.transparency = (packed & GCE_MASK_TRANSPARENT_COLOR_FLAG) != 0; // Delay in milliseconds. int delayInHundredthsOfASecond = readShort(); // TODO: consider allowing -1 to indicate show forever. if (delayInHundredthsOfASecond < MIN_FRAME_DELAY) { delayInHundredthsOfASecond = DEFAULT_FRAME_DELAY; } header.currentFrame.delay = delayInHundredthsOfASecond * 10; // Transparent color index header.currentFrame.transIndex = read(); // Block terminator read(); } /** * Reads next frame image. */ private void readBitmap() { // (sub)image position & size. header.currentFrame.ix = readShort(); header.currentFrame.iy = readShort(); header.currentFrame.iw = readShort(); header.currentFrame.ih = readShort(); /* * Image Descriptor packed field: * 7 6 5 4 3 2 1 0 * +---------------+ * 9 | | | | | | * * Local Color Table Flag 1 Bit * Interlace Flag 1 Bit * Sort Flag 1 Bit * Reserved 2 Bits * Size of Local Color Table 3 Bits */ int packed = read(); boolean lctFlag = (packed & DESCRIPTOR_MASK_LCT_FLAG) != 0; int lctSize = (int) Math.pow(2, (packed & DESCRIPTOR_MASK_LCT_SIZE) + 1); header.currentFrame.interlace = (packed & DESCRIPTOR_MASK_INTERLACE_FLAG) != 0; if (lctFlag) { header.currentFrame.lct = readColorTable(lctSize); } else { // No local color table. header.currentFrame.lct = null; } // Save this as the decoding position pointer. header.currentFrame.bufferFrameStart = rawData.position(); // False decode pixel data to advance buffer. skipImageData(); if (err()) { return; } header.frameCount++; // Add image to frame. header.frames.add(header.currentFrame); } /** * Reads Netscape extension to obtain iteration count. */ private void readNetscapeExt() { do { readBlock(); if (block[0] == 1) { // Loop count sub-block. int b1 = ((int) block[1]) & MASK_INT_LOWEST_BYTE; int b2 = ((int) block[2]) & MASK_INT_LOWEST_BYTE; header.loopCount = (b2 << 8) | b1; } } while ((blockSize > 0) && !err()); } /** * Reads GIF file header information. */ private void readHeader() { String id = ""; for (int i = 0; i < 6; i++) { id += (char) read(); } if (!id.startsWith("GIF")) { header.status = STATUS_FORMAT_ERROR; return; } readLSD(); if (header.gctFlag && !err()) { header.gct = readColorTable(header.gctSize); header.bgColor = header.gct[header.bgIndex]; } } /** * Reads Logical Screen Descriptor. */ private void readLSD() { // Logical screen size. header.width = readShort(); header.height = readShort(); /* * Logical Screen Descriptor packed field: * 7 6 5 4 3 2 1 0 * +---------------+ * 4 | | | | | * * Global Color Table Flag 1 Bit * Color Resolution 3 Bits * Sort Flag 1 Bit * Size of Global Color Table 3 Bits */ int packed = read(); header.gctFlag = (packed & LSD_MASK_GCT_FLAG) != 0; header.gctSize = (int) Math.pow(2, (packed & LSD_MASK_GCT_SIZE) + 1); // Background color index. header.bgIndex = read(); // Pixel aspect ratio header.pixelAspect = read(); } /** * Reads color table as 256 RGB integer values. * * @param nColors int number of colors to read. * @return int array containing 256 colors (packed ARGB with full alpha). */ private int[] readColorTable(int nColors) { int nBytes = 3 * nColors; int[] tab = null; byte[] c = new byte[nBytes]; try { rawData.get(c); // TODO: what bounds checks are we avoiding if we know the number of colors? // Max size to avoid bounds checks. tab = new int[MAX_BLOCK_SIZE]; int i = 0; int j = 0; while (i < nColors) { int r = ((int) c[j++]) & MASK_INT_LOWEST_BYTE; int g = ((int) c[j++]) & MASK_INT_LOWEST_BYTE; int b = ((int) c[j++]) & MASK_INT_LOWEST_BYTE; tab[i++] = 0xFF000000 | (r << 16) | (g << 8) | b; } } catch (BufferUnderflowException e) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Format Error Reading Color Table", e); } header.status = STATUS_FORMAT_ERROR; } return tab; } /** * Skips LZW image data for a single frame to advance buffer. */ private void skipImageData() { // lzwMinCodeSize read(); // data sub-blocks skip(); } /** * Skips variable length blocks up to and including next zero length block. */ private void skip() { int blockSize; do { blockSize = read(); int newPosition = Math.min(rawData.position() + blockSize, rawData.limit()); rawData.position(newPosition); } while (blockSize > 0); } /** * Reads next variable length block from input. * * @return number of bytes stored in "buffer" */ private int readBlock() { blockSize = read(); int n = 0; if (blockSize > 0) { int count = 0; try { while (n < blockSize) { count = blockSize - n; rawData.get(block, n, count); n += count; } } catch (Exception e) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Error Reading Block n: " + n + " count: " + count + " blockSize: " + blockSize, e); } header.status = STATUS_FORMAT_ERROR; } } return n; } /** * Reads a single byte from the input stream. */ private int read() { int currByte = 0; try { currByte = rawData.get() & MASK_INT_LOWEST_BYTE; } catch (Exception e) { header.status = STATUS_FORMAT_ERROR; } return currByte; } /** * Reads next 16-bit value, LSB first. */ private int readShort() { // Read 16-bit value. return rawData.getShort(); } private boolean err() { return header.status != GifDecoder.STATUS_OK; } }