/*
* 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.imageutils;
import javax.annotation.Nullable;
import java.io.IOException;
import java.io.InputStream;
import android.util.Pair;
/**
* This class contains utility method in order to manage the WebP format metadata
*/
public class WebpUtil {
/**
* Header for VP8 (lossy WebP). Take care of the space into the String
*/
private static final String VP8_HEADER = "VP8 ";
/**
* Header for Lossless WebP images
*/
private static final String VP8L_HEADER = "VP8L";
/**
* Header for WebP enhanced
*/
private static final String VP8X_HEADER = "VP8X";
private WebpUtil() {
}
/**
* This method checks for the dimension of the WebP image from the given InputStream. We don't
* support mark/reset and the Stream is always closed.
*
* @param is The InputStream used for read WebP data
* @return The Size of the WebP image if any or null if the size is not available
*/
@Nullable public static Pair<Integer, Integer> getSize(InputStream is) {
// Here we have to parse the WebP data skipping all the information which are not
// the size
Pair<Integer, Integer> result = null;
byte[] headerBuffer = new byte[4];
try {
is.read(headerBuffer);
// These must be RIFF
if (!compare(headerBuffer, "RIFF")) {
return null;
}
// Next there's the file size
getInt(is);
// Next the WEBP header
is.read(headerBuffer);
if (!compare(headerBuffer, "WEBP")) {
return null;
}
// Now we can have different headers
is.read(headerBuffer);
final String headerAsString = getHeader(headerBuffer);
if (VP8_HEADER.equals(headerAsString)) {
return getVP8Dimension(is);
} else if (VP8L_HEADER.equals(headerAsString)) {
return getVP8LDimension(is);
} else if (VP8X_HEADER.equals(headerAsString)) {
return getVP8XDimension(is);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
// In this case we don't have the dimension
return null;
}
/**
* We manage the Simple WebP case
*
* @param is The InputStream we're reading
* @return The dimensions if any
* @throws IOException In case or error reading from the InputStream
*/
private static Pair<Integer, Integer> getVP8Dimension(final InputStream is) throws IOException {
// We need to skip 7 bytes
is.skip(7);
// And then check the signature
final short sign1 = getShort(is);
final short sign2 = getShort(is);
final short sign3 = getShort(is);
if (sign1 != 0x9D || sign2 != 0x01 || sign3 != 0x2A) {
// Signature error
return null;
}
// We read the dimensions
return new Pair<>(get2BytesAsInt(is), get2BytesAsInt(is));
}
/**
* We manage the Lossless WebP case
*
* @param is The InputStream we're reading
* @return The dimensions if any
* @throws IOException In case or error reading from the InputStream
*/
private static Pair<Integer, Integer> getVP8LDimension(final InputStream is) throws IOException {
// Skip 4 bytes
getInt(is);
//We have a check here
final byte check = getByte(is);
if (check != 0x2F) {
return null;
}
int data1 = ((byte) is.read()) & 0xFF;
int data2 = ((byte) is.read()) & 0xFF;
int data3 = ((byte) is.read()) & 0xFF;
int data4 = ((byte) is.read()) & 0xFF;
// In this case the bits for size are 14!!! The sizes are -1!!!
final int width = ((data2 & 0x3F) << 8 | data1) + 1;
final int height = ((data4 & 0x0F) << 10 | data3 << 2 | (data2 & 0xC0) >> 6) + 1;
return new Pair<>(width, height);
}
/**
* We manage the Extended WebP case
*
* @param is The InputStream we're reading
* @return The dimensions if any
* @throws IOException In case or error reading from the InputStream
*/
private static Pair<Integer, Integer> getVP8XDimension(final InputStream is) throws IOException {
// We have to skip 8 bytes
is.skip(8);
// Read 3 bytes for width and height
return new Pair<>(read3Bytes(is) + 1, read3Bytes(is) + 1);
}
/**
* Compares some bytes with the text we're expecting
*
* @param what The bytes to compare
* @param with The string those bytes should contains
* @return True if they match and false otherwise
*/
private static boolean compare(byte[] what, String with) {
if (what.length != with.length()) {
return false;
}
for (int i = 0; i < what.length; i++) {
if (with.charAt(i) != what[i]) {
return false;
}
}
return true;
}
private static String getHeader(byte[] header) {
StringBuilder str = new StringBuilder();
for (int i = 0; i < header.length; i++) {
str.append((char) header[i]);
}
return str.toString();
}
private static int getInt(InputStream is) throws IOException {
byte byte1 = (byte) is.read();
byte byte2 = (byte) is.read();
byte byte3 = (byte) is.read();
byte byte4 = (byte) is.read();
return (byte4 << 24) & 0xFF000000 |
(byte3 << 16) & 0xFF0000 |
(byte2 << 8) & 0xFF00 |
(byte1) & 0xFF;
}
public static int get2BytesAsInt(InputStream is) throws IOException {
byte byte1 = (byte) is.read();
byte byte2 = (byte) is.read();
return (byte2 << 8 & 0xFF00) | (byte1 & 0xFF);
}
private static int read3Bytes(InputStream is) throws IOException {
byte byte1 = getByte(is);
byte byte2 = getByte(is);
byte byte3 = getByte(is);
return (((int) byte3) << 16 & 0xFF0000) |
(((int) byte2) << 8 & 0xFF00) |
(((int) byte1) & 0xFF);
}
private static short getShort(InputStream is) throws IOException {
return (short) (is.read() & 0xFF);
}
private static byte getByte(InputStream is) throws IOException {
return (byte) (is.read() & 0xFF);
}
private static boolean isBitOne(byte input, int bitIndex) {
return ((input >> (bitIndex % 8)) & 1) == 1;
}
}