/*
* 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.imagepipeline.webp;
import java.io.UnsupportedEncodingException;
import android.graphics.BitmapFactory;
import android.os.Build;
import android.util.Base64;
public class WebpSupportStatus {
public static final boolean sIsWebpSupportRequired =
Build.VERSION.SDK_INT <= Build.VERSION_CODES.JELLY_BEAN_MR1;
public static final boolean sIsSimpleWebpSupported =
Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH;
public static final boolean sIsExtendedWebpSupported = isExtendedWebpSupported();
/**
* BASE64 encoded extended WebP image.
*/
private static final String VP8X_WEBP_BASE64 = "UklGRkoAAABXRUJQVlA4WAoAAAAQAAAAAAAAAAAAQUxQSAw" +
"AAAARBxAR/Q9ERP8DAABWUDggGAAAABQBAJ0BKgEAAQAAAP4AAA3AAP7mtQAAAA==";
/**
* Helper method that transforms provided string into its byte representation
* using ASCII encoding
* @param value bytes value
* @return byte array representing ascii encoded value
*/
private static byte[] asciiBytes(String value) {
try {
return value.getBytes("ASCII");
} catch (UnsupportedEncodingException uee) {
// won't happen
throw new RuntimeException("ASCII not found!", uee);
}
}
/**
* Each WebP header should consist of at least 20 bytes and start
* with "RIFF" bytes followed by some 4 bytes and "WEBP" bytes.
* A 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 a "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 whether underlying platform supports extended WebPs
*/
private static boolean isExtendedWebpSupported() {
// Lossless and extended formats are supported on Android 4.2.1+
// Unfortunately SDK_INT is not enough to distinguish 4.2 and 4.2.1
// (both are API level 17 (JELLY_BEAN_MR1))
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) {
return false;
}
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN_MR1) {
// Let's test if extended webp is supported
// To this end we will try to decode bounds of vp8x webp with alpha channel
byte[] decodedBytes = Base64.decode(VP8X_WEBP_BASE64, Base64.DEFAULT);
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inJustDecodeBounds = true;
BitmapFactory.decodeByteArray(decodedBytes, 0, decodedBytes.length, opts);
// If Android managed to find appropriate decoder then opts.outHeight and opts.outWidth
// should be set. We can not assume that outMimeType is set.
// Android guys forgot to update logic for mime types when they introduced support for webp.
// For example, on 4.2.2 this field is not set for webp images.
if (opts.outHeight != 1 || opts.outWidth != 1) {
return false;
}
}
return true;
}
public static boolean isWebpPlatformSupported(
final byte[] imageHeaderBytes,
final int offset,
final int headerSize) {
if (isSimpleWebpHeader(imageHeaderBytes, offset)) {
return sIsSimpleWebpSupported;
}
if (isLosslessWebpHeader(imageHeaderBytes, offset)) {
return sIsExtendedWebpSupported;
}
if (isExtendedWebpHeader(imageHeaderBytes, offset, headerSize)) {
if (isAnimatedWebpHeader(imageHeaderBytes, offset)) {
return false;
}
return sIsExtendedWebpSupported;
}
return false;
}
public static boolean isAnimatedWebpHeader(final byte[] imageHeaderBytes, final int offset) {
boolean isVp8x = matchBytePattern(imageHeaderBytes, offset + 12, WEBP_VP8X_BYTES);
// ANIM is 2nd bit (00000010 == 2) on 21st byte (imageHeaderBytes[20])
boolean hasAnimationBit = (imageHeaderBytes[offset + 20] & 2) == 2;
return isVp8x && hasAnimationBit;
}
public static boolean isSimpleWebpHeader(final byte[] imageHeaderBytes, final int offset) {
return matchBytePattern(imageHeaderBytes, offset + 12, WEBP_VP8_BYTES);
}
public static boolean isLosslessWebpHeader(final byte[] imageHeaderBytes, final int offset) {
return matchBytePattern(imageHeaderBytes, offset + 12, WEBP_VP8L_BYTES);
}
public static boolean isExtendedWebpHeader(
final byte[] imageHeaderBytes,
final int offset,
final int headerSize) {
return headerSize >= EXTENDED_WEBP_HEADER_LENGTH &&
matchBytePattern(imageHeaderBytes, offset + 12, WEBP_VP8X_BYTES);
}
public static boolean isExtendedWebpHeaderWithAlpha(
final byte[] imageHeaderBytes,
final int offset) {
boolean isVp8x = matchBytePattern(imageHeaderBytes, offset + 12, WEBP_VP8X_BYTES);
// Has ALPHA is 5th bit (00010000 == 16) on 21st byte (imageHeaderBytes[20])
boolean hasAlphaBit = (imageHeaderBytes[offset + 20] & 16) == 16;
return isVp8x && hasAlphaBit;
}
/**
* 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 image header bytes
* @return true if imageHeaderBytes contains a valid webp header
*/
public static boolean isWebpHeader(
final byte[] imageHeaderBytes,
final int offset,
final int headerSize) {
return headerSize >= SIMPLE_WEBP_HEADER_LENGTH &&
matchBytePattern(imageHeaderBytes, offset, WEBP_RIFF_BYTES) &&
matchBytePattern(imageHeaderBytes, offset + 8, WEBP_NAME_BYTES);
}
private static boolean matchBytePattern(
final byte[] byteArray,
final int offset,
final byte[] pattern) {
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;
}
}