/* * 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 javax.annotation.Nullable; import java.io.InputStream; import com.facebook.common.internal.Ints; import com.facebook.common.internal.Preconditions; import com.facebook.common.webp.WebpSupportStatus; /** * Default image format checker that is able to determine all {@link DefaultImageFormats}. */ public class DefaultImageFormatChecker implements ImageFormat.FormatChecker { /** * Maximum header size for any image type. * * <p>This determines how much data {@link ImageFormatChecker#getImageFormat(InputStream) reads * from a stream. After changing any of the type detection algorithms, or adding a new one, this * value should be edited. */ final int MAX_HEADER_LENGTH = Ints.max( EXTENDED_WEBP_HEADER_LENGTH, SIMPLE_WEBP_HEADER_LENGTH, JPEG_HEADER_LENGTH, PNG_HEADER_LENGTH, GIF_HEADER_LENGTH, BMP_HEADER_LENGTH); @Override public int getHeaderSize() { return MAX_HEADER_LENGTH; } /** * Tries to match imageHeaderByte and headerSize against every known image format. If any match * succeeds, corresponding ImageFormat is returned. * * @param headerBytes the header bytes to check * @param headerSize the available header size * @return ImageFormat for given imageHeaderBytes or UNKNOWN if no such type could be recognized */ @Nullable @Override public final ImageFormat determineFormat(byte[] headerBytes, int headerSize) { Preconditions.checkNotNull(headerBytes); if (WebpSupportStatus.isWebpHeader(headerBytes, 0, headerSize)) { return getWebpFormat(headerBytes, headerSize); } if (isJpegHeader(headerBytes, headerSize)) { return DefaultImageFormats.JPEG; } if (isPngHeader(headerBytes, headerSize)) { return DefaultImageFormats.PNG; } if (isGifHeader(headerBytes, headerSize)) { return DefaultImageFormats.GIF; } if (isBmpHeader(headerBytes, headerSize)) { return DefaultImageFormats.BMP; } return ImageFormat.UNKNOWN; } /** * Each WebP header should consist 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; /** * 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(WebpSupportStatus.isWebpHeader(imageHeaderBytes, 0, headerSize)); if (WebpSupportStatus.isSimpleWebpHeader(imageHeaderBytes, 0)) { return DefaultImageFormats.WEBP_SIMPLE; } if (WebpSupportStatus.isLosslessWebpHeader(imageHeaderBytes, 0)) { return DefaultImageFormats.WEBP_LOSSLESS; } if (WebpSupportStatus.isExtendedWebpHeader(imageHeaderBytes, 0, headerSize)) { if (WebpSupportStatus.isAnimatedWebpHeader(imageHeaderBytes, 0)) { return DefaultImageFormats.WEBP_ANIMATED; } if (WebpSupportStatus.isExtendedWebpHeaderWithAlpha(imageHeaderBytes, 0)) { return DefaultImageFormats.WEBP_EXTENDED_WITH_ALPHA; } return DefaultImageFormats.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}; private static final int JPEG_HEADER_LENGTH = JPEG_HEADER.length; /** * 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 && ImageFormatCheckerUtils.startsWithPattern(imageHeaderBytes, 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}; private static final int PNG_HEADER_LENGTH = PNG_HEADER.length; /** * 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 && ImageFormatCheckerUtils.startsWithPattern(imageHeaderBytes, PNG_HEADER); } /** * Every gif image starts with "GIF" bytes followed by * bytes indicating version of gif standard */ private static final byte[] GIF_HEADER_87A = ImageFormatCheckerUtils.asciiBytes("GIF87a"); private static final byte[] GIF_HEADER_89A = ImageFormatCheckerUtils.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 ImageFormatCheckerUtils.startsWithPattern(imageHeaderBytes, GIF_HEADER_87A) || ImageFormatCheckerUtils.startsWithPattern(imageHeaderBytes, GIF_HEADER_89A); } /** * Every bmp image starts with "BM" bytes */ private static final byte[] BMP_HEADER = ImageFormatCheckerUtils.asciiBytes("BM"); private static final int BMP_HEADER_LENGTH = BMP_HEADER.length; /** * Checks if first headerSize bytes of imageHeaderBytes constitute a valid header for a bmp image. * Details on BMP header can be found <a href="http://www.onicos.com/staff/iz/formats/bmp.html"> * </a> * @param imageHeaderBytes * @param headerSize * @return true if imageHeaderBytes is a valid header for a bmp image */ private static boolean isBmpHeader(final byte[] imageHeaderBytes, final int headerSize) { if (headerSize < BMP_HEADER.length) { return false; } return ImageFormatCheckerUtils.startsWithPattern(imageHeaderBytes, BMP_HEADER); } }