/* * Copyright (c) 2015-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. */ package com.facebook.imageformat; import com.facebook.common.internal.Preconditions; import com.facebook.common.internal.VisibleForTesting; import java.io.IOException; import java.io.InputStream; /** * Detects the format of an encoded gif. */ public final class GifFormatChecker { private static final int FRAME_HEADER_SIZE = 10; private GifFormatChecker() {} /** * Every GIF frame header starts with a 4 byte static sequence consisting of the following bytes */ private static final byte[] FRAME_HEADER_START = new byte[]{ (byte) 0x00, (byte) 0x21, (byte) 0xF9, (byte) 0x04}; /** * Every GIF frame header ends with a 2 byte static sequence consisting of one of * the following two sequences of bytes */ private static final byte[] FRAME_HEADER_END_1 = new byte[]{ (byte) 0x00, (byte) 0x2C}; private static final byte[] FRAME_HEADER_END_2 = new byte[]{ (byte) 0x00, (byte) 0x21}; /** * Checks if source contains more than one frame header in it in order to decide whether a GIF * image is animated or not. * * @return true if source contains more than one frame header in its bytes */ public static boolean isAnimated(InputStream source) { final byte[] buffer = new byte[FRAME_HEADER_SIZE]; try { source.read(buffer, 0, FRAME_HEADER_SIZE); int offset = 0; int frameHeaders = 0; // Read bytes into a circular buffer and check if it matches one of the frame header // sequences. First byte can be ignored as it will be part of the GIF static header. while (source.read(buffer, offset, 1) > 0) { // This sequence of bytes might be found in the data section of the file, worst case // scenario this method will return true meaning that a static gif is animated. if (circularBufferMatchesBytePattern(buffer, offset + 1, FRAME_HEADER_START) && (circularBufferMatchesBytePattern(buffer, offset + 9, FRAME_HEADER_END_1) || circularBufferMatchesBytePattern(buffer, offset + 9, FRAME_HEADER_END_2))) { frameHeaders++; if (frameHeaders > 1) { return true; } } offset = (offset + 1) % buffer.length; } } catch (IOException ioe) { throw new RuntimeException(ioe); } return false; } /** * Checks if the byte array matches a pattern. * * <p>Instead of doing a normal scan, we treat the array as a circular buffer, with 'offset' * determining the start point. * * @return true if match succeeds, false otherwise */ @VisibleForTesting static boolean circularBufferMatchesBytePattern( byte[] byteArray, int offset, byte[] pattern) { Preconditions.checkNotNull(byteArray); Preconditions.checkNotNull(pattern); Preconditions.checkArgument(offset >= 0); if (pattern.length > byteArray.length) { return false; } for (int i = 0; i < pattern.length; i++) { if (byteArray[(i + offset) % byteArray.length] != pattern[i]) { return false; } } return true; } }