/*
* 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.webpsupport;
import java.io.BufferedInputStream;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import javax.annotation.Nullable;
import android.annotation.SuppressLint;
import android.content.res.Resources;
import android.graphics.Rect;
import android.os.Build;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.util.DisplayMetrics;
import android.util.TypedValue;
import com.facebook.common.internal.DoNotStrip;
import com.facebook.common.webp.BitmapCreator;
import com.facebook.common.webp.WebpBitmapFactory;
import com.facebook.common.webp.WebpSupportStatus;
import com.facebook.imagepipeline.nativecode.StaticWebpNativeLoader;
import static com.facebook.common.webp.WebpSupportStatus.isWebpHeader;
@DoNotStrip
public class WebpBitmapFactoryImpl implements WebpBitmapFactory {
private static final int HEADER_SIZE = 20;
private static final int IN_TEMP_BUFFER_SIZE = 8 * 1024;
public static final boolean IN_BITMAP_SUPPORTED =
Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB;
private static WebpErrorLogger mWebpErrorLogger;
private static BitmapCreator mBitmapCreator;
@Override
public void setBitmapCreator(final BitmapCreator bitmapCreator) {
mBitmapCreator = bitmapCreator;
}
private static InputStream wrapToMarkSupportedStream(InputStream inputStream) {
if (!inputStream.markSupported()) {
inputStream = new BufferedInputStream(inputStream, HEADER_SIZE);
}
return inputStream;
}
private static byte[] getWebpHeader(InputStream inputStream, BitmapFactory.Options opts) {
inputStream.mark(HEADER_SIZE);
byte[] header;
if (opts != null && opts.inTempStorage != null && opts.inTempStorage.length >= HEADER_SIZE) {
header = opts.inTempStorage;
} else {
header = new byte[HEADER_SIZE];
}
try {
inputStream.read(header, 0, HEADER_SIZE);
inputStream.reset();
} catch (IOException exp) {
return null;
}
return header;
}
private static void setDensityFromOptions(Bitmap outputBitmap, BitmapFactory.Options opts) {
if (outputBitmap == null || opts == null) {
return;
}
final int density = opts.inDensity;
if (density != 0) {
outputBitmap.setDensity(density);
final int targetDensity = opts.inTargetDensity;
if (targetDensity == 0 || density == targetDensity || density == opts.inScreenDensity) {
return;
}
if (opts.inScaled) {
outputBitmap.setDensity(targetDensity);
}
} else if (IN_BITMAP_SUPPORTED && opts.inBitmap != null) {
// bitmap was reused, ensure density is reset
outputBitmap.setDensity(DisplayMetrics.DENSITY_DEFAULT);
}
}
@Override
public void setWebpErrorLogger(WebpErrorLogger webpErrorLogger) {
this.mWebpErrorLogger = webpErrorLogger;
}
@Override
public Bitmap decodeFileDescriptor(
FileDescriptor fd,
Rect outPadding,
BitmapFactory.Options opts) {
return hookDecodeFileDescriptor(fd, outPadding, opts);
}
@Override
public Bitmap decodeStream(
InputStream inputStream,
Rect outPadding,
BitmapFactory.Options opts) {
return hookDecodeStream(inputStream, outPadding, opts);
}
@Override
public Bitmap decodeFile(
String pathName,
BitmapFactory.Options opts) {
return hookDecodeFile(pathName, opts);
}
@Override
public Bitmap decodeByteArray(
byte[] array,
int offset,
int length,
BitmapFactory.Options opts) {
return hookDecodeByteArray(array, offset, length, opts);
}
@DoNotStrip
public static Bitmap hookDecodeByteArray(
byte[] array,
int offset,
int length,
BitmapFactory.Options opts) {
StaticWebpNativeLoader.ensure();
Bitmap bitmap;
if (WebpSupportStatus.sIsWebpSupportRequired && isWebpHeader(array, offset, length)) {
bitmap = nativeDecodeByteArray(
array,
offset,
length,
opts,
getScaleFromOptions(opts),
getInTempStorageFromOptions(opts));
// We notify that the direct decoding failed
if (bitmap == null) {
sendWebpErrorLog("webp_direct_decode_array");
}
setWebpBitmapOptions(bitmap, opts);
} else {
bitmap = originalDecodeByteArray(array, offset, length, opts);
if (bitmap == null) {
sendWebpErrorLog("webp_direct_decode_array_failed_on_no_webp");
}
}
return bitmap;
}
@DoNotStrip
private static Bitmap originalDecodeByteArray(
byte[] array,
int offset,
int length,
BitmapFactory.Options opts) {
return BitmapFactory.decodeByteArray(array, offset, length, opts);
}
@DoNotStrip
public static Bitmap hookDecodeByteArray(
byte[] array,
int offset,
int length) {
return hookDecodeByteArray(array, offset, length, null);
}
@DoNotStrip
private static Bitmap originalDecodeByteArray(
byte[] array,
int offset,
int length) {
return BitmapFactory.decodeByteArray(array, offset, length);
}
@DoNotStrip
public static Bitmap hookDecodeStream(
InputStream inputStream,
Rect outPadding,
BitmapFactory.Options opts) {
StaticWebpNativeLoader.ensure();
inputStream = wrapToMarkSupportedStream(inputStream);
Bitmap bitmap;
byte[] header = getWebpHeader(inputStream, opts);
if (WebpSupportStatus.sIsWebpSupportRequired && isWebpHeader(header, 0, HEADER_SIZE)) {
bitmap = nativeDecodeStream(
inputStream,
opts,
getScaleFromOptions(opts),
getInTempStorageFromOptions(opts));
// We notify that the direct decoder failed
if (bitmap == null) {
sendWebpErrorLog("webp_direct_decode_stream");
}
setWebpBitmapOptions(bitmap, opts);
setPaddingDefaultValues(outPadding);
} else {
bitmap = originalDecodeStream(inputStream, outPadding, opts);
if (bitmap == null) {
sendWebpErrorLog("webp_direct_decode_stream_failed_on_no_webp");
}
}
return bitmap;
}
@DoNotStrip
private static Bitmap originalDecodeStream(
InputStream inputStream,
Rect outPadding,
BitmapFactory.Options opts) {
return BitmapFactory.decodeStream(inputStream, outPadding, opts);
}
@DoNotStrip
public static Bitmap hookDecodeStream(
InputStream inputStream) {
return hookDecodeStream(inputStream, null, null);
}
@DoNotStrip
private static Bitmap originalDecodeStream(
InputStream inputStream) {
return BitmapFactory.decodeStream(inputStream);
}
@DoNotStrip
public static Bitmap hookDecodeFile(
String pathName,
BitmapFactory.Options opts) {
Bitmap bitmap = null;
try (InputStream stream = new FileInputStream(pathName)) {
bitmap = hookDecodeStream(stream, null, opts);
} catch (Exception e) {
// Ignore, will just return null
}
return bitmap;
}
@DoNotStrip
public static Bitmap hookDecodeFile(String pathName) {
return hookDecodeFile(pathName, null);
}
@DoNotStrip
public static Bitmap hookDecodeResourceStream(
Resources res,
TypedValue value,
InputStream is,
Rect pad,
BitmapFactory.Options opts) {
if (opts == null) {
opts = new BitmapFactory.Options();
}
if (opts.inDensity == 0 && value != null) {
final int density = value.density;
if (density == TypedValue.DENSITY_DEFAULT) {
opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
} else if (density != TypedValue.DENSITY_NONE) {
opts.inDensity = density;
}
}
if (opts.inTargetDensity == 0 && res != null) {
opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
}
return hookDecodeStream(is, pad, opts);
}
@DoNotStrip
private static Bitmap originalDecodeResourceStream(
Resources res,
TypedValue value,
InputStream is,
Rect pad,
BitmapFactory.Options opts) {
return BitmapFactory.decodeResourceStream(res, value, is, pad, opts);
}
@DoNotStrip
public static Bitmap hookDecodeResource(
Resources res,
int id,
BitmapFactory.Options opts) {
Bitmap bm = null;
TypedValue value = new TypedValue();
try (InputStream is = res.openRawResource(id, value)) {
bm = hookDecodeResourceStream(res, value, is, null, opts);
} catch (Exception e) {
// Keep resulting bitmap as null
}
if (IN_BITMAP_SUPPORTED && bm == null && opts != null && opts.inBitmap != null) {
throw new IllegalArgumentException("Problem decoding into existing bitmap");
}
return bm;
}
@DoNotStrip
private static Bitmap originalDecodeResource(
Resources res,
int id,
BitmapFactory.Options opts) {
return BitmapFactory.decodeResource(res, id, opts);
}
@DoNotStrip
public static Bitmap hookDecodeResource(
Resources res,
int id) {
return hookDecodeResource(res, id, null);
}
@DoNotStrip
private static Bitmap originalDecodeResource(
Resources res,
int id) {
return BitmapFactory.decodeResource(res, id);
}
@DoNotStrip
private static boolean setOutDimensions(
BitmapFactory.Options options,
int imageWidth,
int imageHeight) {
if (options != null && options.inJustDecodeBounds) {
options.outWidth = imageWidth;
options.outHeight = imageHeight;
return true;
}
return false;
}
@DoNotStrip
private static void setPaddingDefaultValues(@Nullable Rect padding) {
if (padding != null) {
padding.top = -1;
padding.left = -1;
padding.bottom = -1;
padding.right = -1;
}
}
@DoNotStrip
private static void setBitmapSize(
@Nullable BitmapFactory.Options options,
int width,
int height) {
if (options != null) {
options.outWidth = width;
options.outHeight = height;
}
}
@DoNotStrip
private static Bitmap originalDecodeFile(
String pathName,
BitmapFactory.Options opts) {
return BitmapFactory.decodeFile(pathName, opts);
}
@DoNotStrip
private static Bitmap originalDecodeFile(String pathName) {
return BitmapFactory.decodeFile(pathName);
}
@DoNotStrip
public static Bitmap hookDecodeFileDescriptor(
FileDescriptor fd,
Rect outPadding,
BitmapFactory.Options opts) {
StaticWebpNativeLoader.ensure();
Bitmap bitmap;
long originalSeekPosition = nativeSeek(fd, 0, false);
if (originalSeekPosition != -1) {
InputStream inputStream = wrapToMarkSupportedStream(new FileInputStream(fd));
try {
byte[] header = getWebpHeader(inputStream, opts);
if (WebpSupportStatus.sIsWebpSupportRequired && isWebpHeader(header, 0, HEADER_SIZE)) {
bitmap = nativeDecodeStream(
inputStream,
opts,
getScaleFromOptions(opts),
getInTempStorageFromOptions(opts));
// We send error if the direct decode failed
if (bitmap == null) {
sendWebpErrorLog("webp_direct_decode_fd");
}
setPaddingDefaultValues(outPadding);
setWebpBitmapOptions(bitmap, opts);
} else {
nativeSeek(fd, originalSeekPosition, true);
bitmap = originalDecodeFileDescriptor(fd, outPadding, opts);
if (bitmap == null) {
sendWebpErrorLog("webp_direct_decode_fd_failed_on_no_webp");
}
}
} finally {
try {
inputStream.close();
} catch (Throwable t) {
/* ignore */
}
}
} else {
bitmap = hookDecodeStream(new FileInputStream(fd), outPadding, opts);
setPaddingDefaultValues(outPadding);
}
return bitmap;
}
@DoNotStrip
private static Bitmap originalDecodeFileDescriptor(
FileDescriptor fd,
Rect outPadding,
BitmapFactory.Options opts) {
return BitmapFactory.decodeFileDescriptor(fd, outPadding, opts);
}
@DoNotStrip
public static Bitmap hookDecodeFileDescriptor(FileDescriptor fd) {
return hookDecodeFileDescriptor(fd, null, null);
}
@DoNotStrip
private static Bitmap originalDecodeFileDescriptor(FileDescriptor fd) {
return BitmapFactory.decodeFileDescriptor(fd);
}
private static void setWebpBitmapOptions(Bitmap bitmap, BitmapFactory.Options opts) {
setDensityFromOptions(bitmap, opts);
if (opts != null) {
opts.outMimeType = "image/webp";
}
}
@DoNotStrip
@SuppressLint("NewApi")
private static boolean shouldPremultiply(BitmapFactory.Options options) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && options != null) {
return options.inPremultiplied;
}
return true;
}
@DoNotStrip
private static Bitmap createBitmap(int width, int height, BitmapFactory.Options options) {
if (IN_BITMAP_SUPPORTED &&
options != null &&
options.inBitmap != null &&
options.inBitmap.isMutable()) {
return options.inBitmap;
}
return mBitmapCreator.createNakedBitmap(width, height, Bitmap.Config.ARGB_8888);
}
@DoNotStrip
private static native Bitmap nativeDecodeStream(
InputStream is,
BitmapFactory.Options options,
float scale,
byte[] inTempStorage);
@DoNotStrip
private static native Bitmap nativeDecodeByteArray(
byte[] data,
int offset,
int length,
BitmapFactory.Options opts,
float scale,
byte[] inTempStorage);
@DoNotStrip
private static native long nativeSeek(FileDescriptor fd, long offset, boolean absolute);
@DoNotStrip
private static byte[] getInTempStorageFromOptions(@Nullable final BitmapFactory.Options options) {
if (options != null && options.inTempStorage != null) {
return options.inTempStorage;
} else {
return new byte[IN_TEMP_BUFFER_SIZE];
}
}
@DoNotStrip
private static float getScaleFromOptions(BitmapFactory.Options options) {
float scale = 1.0f;
if (options != null) {
int sampleSize = options.inSampleSize;
if (sampleSize > 1) {
scale = 1.0f / (float) sampleSize;
}
if (options.inScaled) {
int density = options.inDensity;
int targetDensity = options.inTargetDensity;
int screenDensity = options.inScreenDensity;
if (density != 0 && targetDensity != 0 && density != screenDensity) {
scale = targetDensity / (float) density;
}
}
}
return scale;
}
private static void sendWebpErrorLog(String message) {
// We want to track only when bitmap is null after native decoding
if (mWebpErrorLogger != null) {
mWebpErrorLogger.onWebpErrorLog(message, "decoding_failure");
}
}
}