/*
* 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 java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import com.facebook.common.internal.ByteStreams;
import com.facebook.common.internal.Closeables;
import com.facebook.common.internal.Ints;
import com.facebook.common.internal.Preconditions;
import com.facebook.common.internal.Throwables;
/**
* Detects the format of an encoded image.
*/
public class ImageFormatChecker {
private ImageFormatChecker() {}
/**
* Tries to match imageHeaderByte and headerSize against every known image format.
* If any match succeeds, corresponding ImageFormat is returned.
* @param imageHeaderBytes
* @param headerSize
* @return ImageFormat for given imageHeaderBytes or UNKNOWN if no such type could be recognized
*/
private static ImageFormat doGetImageFormat(
final byte[] imageHeaderBytes,
final int headerSize) {
Preconditions.checkNotNull(imageHeaderBytes);
if (isWebpHeader(imageHeaderBytes, headerSize)) {
return getWebpFormat(imageHeaderBytes, headerSize);
}
if (isJpegHeader(imageHeaderBytes, headerSize)) {
return ImageFormat.JPEG;
}
if (isPngHeader(imageHeaderBytes, headerSize)) {
return ImageFormat.PNG;
}
if (isGifHeader(imageHeaderBytes, headerSize)) {
return ImageFormat.GIF;
}
return ImageFormat.UNKNOWN;
}
/**
* Reads up to MAX_HEADER_LENGTH bytes from is InputStream. If mark is supported by is, it is
* used to restore content of the stream after appropriate amount of data is read.
* Read bytes are stored in imageHeaderBytes, which should be capable of storing
* MAX_HEADER_LENGTH bytes.
* @param is
* @param imageHeaderBytes
* @return number of bytes read from is
* @throws IOException
*/
private static int readHeaderFromStream(
final InputStream is,
final byte[] imageHeaderBytes)
throws IOException {
Preconditions.checkNotNull(is);
Preconditions.checkNotNull(imageHeaderBytes);
Preconditions.checkArgument(imageHeaderBytes.length >= MAX_HEADER_LENGTH);
// If mark is supported by the stream, use it to let the owner of the stream re-read the same
// data. Otherwise, just consume some data.
if (is.markSupported()) {
try {
is.mark(MAX_HEADER_LENGTH);
return ByteStreams.read(is, imageHeaderBytes, 0, MAX_HEADER_LENGTH);
} finally {
is.reset();
}
} else {
return ByteStreams.read(is, imageHeaderBytes, 0, MAX_HEADER_LENGTH);
}
}
/**
* Tries to read up to MAX_HEADER_LENGTH bytes from InputStream is and use read bytes to
* determine type of the image contained in is. If provided input stream does not support mark,
* then this method consumes data from is and it is not safe to read further bytes from is after
* this method returns. Otherwise, if mark is supported, it will be used to preserve oryginal
* content of is.
* @param is
* @return ImageFormat matching content of is InputStream or UNKNOWN if no type is suitable
* @throws IOException if exception happens during read
*/
public static ImageFormat getImageFormat(final InputStream is) throws IOException {
Preconditions.checkNotNull(is);
final byte[] imageHeaderBytes = new byte[MAX_HEADER_LENGTH];
final int headerSize = readHeaderFromStream(is, imageHeaderBytes);
return doGetImageFormat(imageHeaderBytes, headerSize);
}
/*
* A variant of getImageFormat that wraps IOException with RuntimeException.
* This relieves clients of implementing dummy rethrow try-catch block.
*/
public static ImageFormat getImageFormat_WrapIOException(final InputStream is) {
try {
return getImageFormat(is);
} catch (IOException ioe) {
throw Throwables.propagate(ioe);
}
}
/**
* Reads image header from a file indicated by provided filename and determines
* its format. This method does not throw IOException if one occurs. In this case,
* ImageFormat.UNKNOWN will be returned.
* @param filename
* @return ImageFormat for image stored in filename
*/
public static ImageFormat getImageFormat(String filename) {
FileInputStream fileInputStream = null;
try {
fileInputStream = new FileInputStream(filename);
return getImageFormat(fileInputStream);
} catch (IOException ioe) {
return ImageFormat.UNKNOWN;
} finally {
Closeables.closeQuietly(fileInputStream);
}
}
/**
* Checks if byteArray interpreted as sequence of bytes has a subsequence equal to pattern
* starting at position equal to offset.
* @param byteArray
* @param offset
* @param pattern
* @return true if match succeeds, false otherwise
*/
private static boolean matchBytePattern(
final byte[] byteArray,
final int offset,
final byte[] pattern) {
Preconditions.checkNotNull(byteArray);
Preconditions.checkNotNull(pattern);
Preconditions.checkArgument(offset >= 0);
if (pattern.length + offset > byteArray.length) {
return false;
}
for (int i = 0; i < pattern.length; ++i) {
if (byteArray[i + offset] != pattern[i]) {
return false;
}
}
return true;
}
/**
* Helper method that transforms provided string into it's byte representation
* using ASCII encoding
* @param value
* @return byte array representing ascii encoded value
*/
private static byte[] asciiBytes(String value) {
Preconditions.checkNotNull(value);
try {
return value.getBytes("ASCII");
} catch (UnsupportedEncodingException uee) {
// won't happen
throw new RuntimeException("ASCII not found!", uee);
}
}
/**
* Each WebP header should cosist of at least 20 bytes and start
* with "RIFF" bytes followed by some 4 bytes and "WEBP" bytes.
* More detailed description if WebP can be found here:
* <a href="https://developers.google.com/speed/webp/docs/riff_container">
* https://developers.google.com/speed/webp/docs/riff_container</a>
*/
private static final int SIMPLE_WEBP_HEADER_LENGTH = 20;
/**
* Each VP8X WebP image has "features" byte following its ChunkHeader('VP8X')
*/
private static final int EXTENDED_WEBP_HEADER_LENGTH = 21;
private static final byte[] WEBP_RIFF_BYTES = asciiBytes("RIFF");
private static final byte[] WEBP_NAME_BYTES = asciiBytes("WEBP");
/**
* This is a constant used to detect different WebP's formats: vp8, vp8l and vp8x.
*/
private static final byte[] WEBP_VP8_BYTES = asciiBytes("VP8 ");
private static final byte[] WEBP_VP8L_BYTES = asciiBytes("VP8L");
private static final byte[] WEBP_VP8X_BYTES = asciiBytes("VP8X");
/**
* Checks if a WebP image is animated one
* @param imageHeaderBytes - byte array containing valid WebP header
* @return true if imageHeaderBytes is a header of animated webp
*/
private static boolean isAnimatedWebpHeader(final byte[] imageHeaderBytes) {
boolean isVp8x = matchBytePattern(imageHeaderBytes, 12, WEBP_VP8X_BYTES);
// ANIM is 2nd bit (00000010 == 2) on 21st byte (imageHeaderBytes[20])
boolean hasAnimationBit = (imageHeaderBytes[20] & 2) == 2;
return isVp8x && hasAnimationBit;
}
private static boolean isSimpleWebpHeader(final byte[] imageHeaderBytes) {
return matchBytePattern(imageHeaderBytes, 12, WEBP_VP8_BYTES);
}
private static boolean isLosslessWebpHeader(final byte[] imageHeaderBytes) {
return matchBytePattern(imageHeaderBytes, 12, WEBP_VP8L_BYTES);
}
private static boolean isExtendedWebpHeaderWithAlpha(final byte[] imageHeaderBytes) {
boolean isVp8x = matchBytePattern(imageHeaderBytes, 12, WEBP_VP8X_BYTES);
// Has ALPHA is 5th bit (00010000 == 16) on 21st byte (imageHeaderBytes[20])
boolean hasAlphaBit = (imageHeaderBytes[20] & 16) == 16;
return isVp8x && hasAlphaBit;
}
private static boolean isExtendedWebpHeader(final byte[] imageHeaderBytes, final int headerSize) {
return headerSize >= EXTENDED_WEBP_HEADER_LENGTH &&
matchBytePattern(imageHeaderBytes, 12, WEBP_VP8X_BYTES);
}
/**
* Checks if imageHeaderBytes contains WEBP_RIFF_BYTES and WEBP_NAME_BYTES and if the
* header is long enough to be WebP's header.
* WebP file format can be found here:
* <a href="https://developers.google.com/speed/webp/docs/riff_container">
* https://developers.google.com/speed/webp/docs/riff_container</a>
* @param imageHeaderBytes
* @return true if imageHeaderBytes contains a valid webp header
*/
private static boolean isWebpHeader(final byte[] imageHeaderBytes, final int headerSize) {
Preconditions.checkNotNull(imageHeaderBytes);
return headerSize >= SIMPLE_WEBP_HEADER_LENGTH &&
matchBytePattern(imageHeaderBytes, 0, WEBP_RIFF_BYTES) &&
matchBytePattern(imageHeaderBytes, 8, WEBP_NAME_BYTES);
}
/**
* Determines type of WebP image. imageHeaderBytes has to be header of a WebP image
*/
private static ImageFormat getWebpFormat(final byte[] imageHeaderBytes, final int headerSize) {
Preconditions.checkArgument(isWebpHeader(imageHeaderBytes, headerSize));
if (isSimpleWebpHeader(imageHeaderBytes)) {
return ImageFormat.WEBP_SIMPLE;
}
if (isLosslessWebpHeader(imageHeaderBytes)) {
return ImageFormat.WEBP_LOSSLESS;
}
if (isExtendedWebpHeader(imageHeaderBytes, headerSize)) {
if (isAnimatedWebpHeader(imageHeaderBytes)) {
return ImageFormat.WEBP_ANIMATED;
}
if (isExtendedWebpHeaderWithAlpha(imageHeaderBytes)) {
return ImageFormat.WEBP_EXTENDED_WITH_ALPHA;
}
return ImageFormat.WEBP_EXTENDED;
}
return ImageFormat.UNKNOWN;
}
/**
* Every JPEG image should start with SOI mark (0xFF, 0xD8) followed by beginning
* of another segment (0xFF)
*/
private static final byte[] JPEG_HEADER = new byte[] {(byte)0xFF, (byte)0xD8, (byte)0xFF};
/**
* Checks if imageHeaderBytes starts with SOI (start of image) marker, followed by 0xFF.
* If headerSize is lower than 3 false is returned.
* Description of jpeg format can be found here:
* <a href="http://www.w3.org/Graphics/JPEG/itu-t81.pdf">
* http://www.w3.org/Graphics/JPEG/itu-t81.pdf</a>
* Annex B deals with compressed data format
* @param imageHeaderBytes
* @param headerSize
* @return true if imageHeaderBytes starts with SOI_BYTES and headerSize >= 3
*/
private static boolean isJpegHeader(final byte[] imageHeaderBytes, final int headerSize) {
return headerSize >= JPEG_HEADER.length && matchBytePattern(imageHeaderBytes, 0, JPEG_HEADER);
}
/**
* Every PNG image starts with 8 byte signature consisting of
* following bytes
*/
private static final byte[] PNG_HEADER = new byte[] {
(byte) 0x89,
'P', 'N', 'G',
(byte) 0x0D, (byte) 0x0A, (byte) 0x1A, (byte) 0x0A};
/**
* Checks if array consisting of first headerSize bytes of imageHeaderBytes
* starts with png signature. More information on PNG can be found there:
* <a href="http://en.wikipedia.org/wiki/Portable_Network_Graphics">
* http://en.wikipedia.org/wiki/Portable_Network_Graphics</a>
* @param imageHeaderBytes
* @param headerSize
* @return true if imageHeaderBytes starts with PNG_HEADER
*/
private static boolean isPngHeader(final byte[] imageHeaderBytes, final int headerSize) {
return headerSize >= PNG_HEADER.length && matchBytePattern(imageHeaderBytes, 0, PNG_HEADER);
}
/**
* Every gif image starts with "GIF" bytes followed by
* bytes indicating version of gif standard
*/
private static final byte[] GIF_HEADER_87A = asciiBytes("GIF87a");
private static final byte[] GIF_HEADER_89A = asciiBytes("GIF89a");
private static final int GIF_HEADER_LENGTH = 6;
/**
* Checks if first headerSize bytes of imageHeaderBytes constitute a valid header for a gif image.
* Details on GIF header can be found <a href="http://www.w3.org/Graphics/GIF/spec-gif89a.txt">
* on page 7</a>
* @param imageHeaderBytes
* @param headerSize
* @return true if imageHeaderBytes is a valid header for a gif image
*/
private static boolean isGifHeader(final byte[] imageHeaderBytes, final int headerSize) {
if (headerSize < GIF_HEADER_LENGTH) {
return false;
}
return matchBytePattern(imageHeaderBytes, 0, GIF_HEADER_87A) ||
matchBytePattern(imageHeaderBytes, 0, GIF_HEADER_89A);
}
/**
* Maximum header size for any image type.
*
* <p>This determines how much data {@link #getImageFormat(InputStream)
* reads from a stream. After changing any of the type detection algorithms, or adding a new one,
* this value should be edited.
*/
private static final int MAX_HEADER_LENGTH = Ints.max(
EXTENDED_WEBP_HEADER_LENGTH,
SIMPLE_WEBP_HEADER_LENGTH,
JPEG_HEADER.length,
PNG_HEADER.length,
GIF_HEADER_LENGTH);
}