提交 b1b024eb 编写于 作者: J judds 提交者: Sam Judd

Improve rounding in Downsampler and add emulator test.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=168236016
上级 f5ba3740
......@@ -10,6 +10,7 @@ import android.util.DisplayMetrics;
import android.util.Log;
import com.bumptech.glide.load.DecodeFormat;
import com.bumptech.glide.load.ImageHeaderParser;
import com.bumptech.glide.load.ImageHeaderParser.ImageType;
import com.bumptech.glide.load.ImageHeaderParserUtils;
import com.bumptech.glide.load.Option;
import com.bumptech.glide.load.Options;
......@@ -121,6 +122,9 @@ public final class Downsampler {
// 5MB. This is the max image header size we can handle, we preallocate a much smaller buffer
// but will resize up to this amount if necessary.
private static final int MARK_POSITION = 5 * 1024 * 1024;
// Defines the level of precision we get when using inDensity/inTargetDensity to calculate an
// arbitrary float scale factor.
private static final int DENSITY_PRECISION_MULTIPLIER = 1000000000;
private final BitmapPool bitmapPool;
private final DisplayMetrics displayMetrics;
......@@ -231,8 +235,20 @@ public final class Downsampler {
int targetWidth = requestedWidth == Target.SIZE_ORIGINAL ? sourceWidth : requestedWidth;
int targetHeight = requestedHeight == Target.SIZE_ORIGINAL ? sourceHeight : requestedHeight;
calculateScaling(downsampleStrategy, degreesToRotate, sourceWidth, sourceHeight, targetWidth,
targetHeight, options);
ImageType imageType = ImageHeaderParserUtils.getType(parsers, is, byteArrayPool);
calculateScaling(
imageType,
is,
callbacks,
bitmapPool,
downsampleStrategy,
degreesToRotate,
sourceWidth,
sourceHeight,
targetWidth,
targetHeight,
options);
calculateConfig(
is,
decodeFormat,
......@@ -244,8 +260,7 @@ public final class Downsampler {
boolean isKitKatOrGreater = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
// Prior to KitKat, the inBitmap size must exactly match the size of the bitmap we're decoding.
if ((options.inSampleSize == 1 || isKitKatOrGreater)
&& shouldUsePool(is)) {
if ((options.inSampleSize == 1 || isKitKatOrGreater) && shouldUsePool(imageType)) {
int expectedWidth;
int expectedHeight;
if (fixBitmapToRequestedDimensions && isKitKatOrGreater) {
......@@ -299,10 +314,18 @@ public final class Downsampler {
}
// Visible for testing.
static void calculateScaling(DownsampleStrategy downsampleStrategy,
static void calculateScaling(
ImageType imageType,
InputStream is,
DecodeCallbacks decodeCallbacks,
BitmapPool bitmapPool,
DownsampleStrategy downsampleStrategy,
int degreesToRotate,
int sourceWidth, int sourceHeight, int targetWidth, int targetHeight,
BitmapFactory.Options options) {
int sourceWidth,
int sourceHeight,
int targetWidth,
int targetHeight,
BitmapFactory.Options options) throws IOException {
// We can't downsample source content if we can't determine its dimensions.
if (sourceWidth <= 0 || sourceHeight <= 0) {
return;
......@@ -323,7 +346,9 @@ public final class Downsampler {
if (exactScaleFactor <= 0f) {
throw new IllegalArgumentException("Cannot scale with factor: " + exactScaleFactor
+ " from: " + downsampleStrategy);
+ " from: " + downsampleStrategy
+ ", source: [" + sourceWidth + "x" + sourceHeight + "]"
+ ", target: [" + targetWidth + "x" + targetHeight + "]");
}
SampleSizeRounding rounding = downsampleStrategy.getSampleSizeRounding(sourceWidth,
sourceHeight, targetWidth, targetHeight);
......@@ -331,8 +356,8 @@ public final class Downsampler {
throw new IllegalArgumentException("Cannot round with null rounding");
}
int outWidth = (int) (exactScaleFactor * sourceWidth + 0.5f);
int outHeight = (int) (exactScaleFactor * sourceHeight + 0.5f);
int outWidth = round(exactScaleFactor * sourceWidth);
int outHeight = round(exactScaleFactor * sourceHeight);
int widthScaleFactor = sourceWidth / outWidth;
int heightScaleFactor = sourceHeight / outHeight;
......@@ -354,14 +379,53 @@ public final class Downsampler {
}
}
float adjustedScaleFactor = powerOfTwoSampleSize * exactScaleFactor;
// Here we mimic framework logic for determining how inSampleSize division is rounded on various
// versions of Android. The logic here has been tested on emulators for Android versions 15-26.
// PNG - Always uses floor
// JPEG - Always uses ceiling
// Webp - Prior to N, always uses floor. At and after N, always uses round.
options.inSampleSize = powerOfTwoSampleSize;
final int powerOfTwoWidth;
final int powerOfTwoHeight;
// Jpeg rounds with ceiling on all API verisons.
if (imageType == ImageType.JPEG) {
powerOfTwoWidth = (int) Math.ceil(sourceWidth / (float) powerOfTwoSampleSize);
powerOfTwoHeight = (int) Math.ceil(sourceHeight / (float) powerOfTwoSampleSize);
} else if (imageType == ImageType.PNG || imageType == ImageType.PNG_A) {
powerOfTwoWidth = (int) Math.floor(sourceWidth / (float) powerOfTwoSampleSize);
powerOfTwoHeight = (int) Math.floor(sourceHeight / (float) powerOfTwoSampleSize);
} else if (imageType == ImageType.WEBP || imageType == ImageType.WEBP_A) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
powerOfTwoWidth = Math.round(sourceWidth / (float) powerOfTwoSampleSize);
powerOfTwoHeight = Math.round(sourceHeight / (float) powerOfTwoSampleSize);
} else {
powerOfTwoWidth = (int) Math.floor(sourceWidth / (float) powerOfTwoSampleSize);
powerOfTwoHeight = (int) Math.floor(sourceHeight / (float) powerOfTwoSampleSize);
}
} else if (
sourceWidth % powerOfTwoSampleSize != 0 || sourceHeight % powerOfTwoSampleSize != 0) {
// If we're not confident the image is in one of our types, fall back to checking the
// dimensions again. inJustDecodeBounds decodes do obey inSampleSize.
int[] dimensions = getDimensions(is, options, decodeCallbacks, bitmapPool);
// Power of two downsampling in BitmapFactory uses a variety of random factors to determine
// rounding that we can't reliably replicate for all image formats. Use ceiling here to make
// sure that we at least provide a Bitmap that's large enough to fit the content we're going
// to load.
powerOfTwoWidth = dimensions[0];
powerOfTwoHeight = dimensions[1];
} else {
powerOfTwoWidth = sourceWidth / powerOfTwoSampleSize;
powerOfTwoHeight = sourceHeight / powerOfTwoSampleSize;
}
double adjustedScaleFactor = downsampleStrategy.getScaleFactor(
powerOfTwoWidth, powerOfTwoHeight, targetWidth, targetHeight);
// Density scaling is only supported if inBitmap is null prior to KitKat. Avoid setting
// densities here so we calculate the final Bitmap size correctly.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
options.inTargetDensity = (int) (1000 * adjustedScaleFactor + 0.5f);
options.inDensity = 1000;
options.inTargetDensity = adjustTargetDensityForError(adjustedScaleFactor);
options.inDensity = DENSITY_PRECISION_MULTIPLIER;
}
if (isScaling(options)) {
options.inScaled = true;
......@@ -373,6 +437,7 @@ public final class Downsampler {
Log.v(TAG, "Calculate scaling"
+ ", source: [" + sourceWidth + "x" + sourceHeight + "]"
+ ", target: [" + targetWidth + "x" + targetHeight + "]"
+ ", power of two scaled: [" + powerOfTwoWidth + "x" + powerOfTwoHeight + "]"
+ ", exact scale factor: " + exactScaleFactor
+ ", power of 2 sample size: " + powerOfTwoSampleSize
+ ", adjusted scale factor: " + adjustedScaleFactor
......@@ -381,24 +446,34 @@ public final class Downsampler {
}
}
private boolean shouldUsePool(InputStream is) throws IOException {
/**
* BitmapFactory calculates the density scale factor as a float. This introduces some non-trivial
* error. This method attempts to account for that error by adjusting the inTargetDensity so that
* the final scale factor is as close to our target as possible.
*/
private static int adjustTargetDensityForError(double adjustedScaleFactor) {
int targetDensity = round(DENSITY_PRECISION_MULTIPLIER * adjustedScaleFactor);
float scaleFactorWithError = targetDensity / (float) DENSITY_PRECISION_MULTIPLIER;
double difference = adjustedScaleFactor / scaleFactorWithError;
return round(difference * targetDensity);
}
// This is weird, but it matches the logic in a bunch of Android views/framework classes for
// rounding.
private static int round(double value) {
return (int) (value + 0.5d);
}
private boolean shouldUsePool(ImageType imageType) throws IOException {
// On KitKat+, any bitmap (of a given config) can be used to decode any other bitmap
// (with the same config).
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
return true;
}
try {
ImageHeaderParser.ImageType type = ImageHeaderParserUtils.getType(parsers, is, byteArrayPool);
// We cannot reuse bitmaps when decoding images that are not PNG or JPG prior to KitKat.
// See: https://groups.google.com/forum/#!msg/android-developers/Mp0MFVFi1Fo/e8ZQ9FGdWdEJ
return TYPES_THAT_USE_POOL_PRE_KITKAT.contains(type);
} catch (IOException e) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Cannot determine the image type from header", e);
}
}
return false;
// We cannot reuse bitmaps when decoding images that are not PNG or JPG prior to KitKat.
// See: https://groups.google.com/forum/#!msg/android-developers/Mp0MFVFi1Fo/e8ZQ9FGdWdEJ
return TYPES_THAT_USE_POOL_PRE_KITKAT.contains(imageType);
}
private void calculateConfig(
......
package com.bumptech.glide.load.resource.bitmap;
import static com.google.common.collect.Range.closed;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Mockito.when;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Build;
import android.util.DisplayMetrics;
import com.bumptech.glide.load.DecodeFormat;
import com.bumptech.glide.load.ImageHeaderParser;
import com.bumptech.glide.load.Options;
import com.bumptech.glide.load.engine.Resource;
import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool;
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
import com.bumptech.glide.load.resource.bitmap.DownsamplerTest.AllocationSizeBitmap;
import com.bumptech.glide.tests.Util;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Matchers;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.shadows.ShadowBitmap;
@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE, sdk = 19, shadows = AllocationSizeBitmap.class)
public class DownsamplerTest {
@Mock private BitmapPool bitmapPool;
@Mock private ArrayPool byteArrayPool;
private Downsampler downsampler;
private Options options;
private int initialSdkVersion;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
options = new Options();
DisplayMetrics displayMetrics =
RuntimeEnvironment.application.getResources().getDisplayMetrics();
when(byteArrayPool.get(anyInt(), Matchers.eq(byte[].class)))
.thenReturn(new byte[ArrayPool.STANDARD_BUFFER_SIZE_BYTES]);
List<ImageHeaderParser> parsers = new ArrayList<ImageHeaderParser>();
parsers.add(new DefaultImageHeaderParser());
downsampler = new Downsampler(parsers, displayMetrics, bitmapPool, byteArrayPool);
initialSdkVersion = Build.VERSION.SDK_INT;
}
@After
public void tearDown() {
Util.setSdkVersionInt(initialSdkVersion);
}
@Test
public void testAlwaysArgb8888() throws IOException {
Bitmap rgb565 = Bitmap.createBitmap(100, 100, Bitmap.Config.RGB_565);
InputStream stream = compressBitmap(rgb565, Bitmap.CompressFormat.JPEG);
options.set(Downsampler.DECODE_FORMAT, DecodeFormat.PREFER_ARGB_8888);
Resource<Bitmap> result = downsampler.decode(stream, 100, 100, options);
assertEquals(Bitmap.Config.ARGB_8888, result.get().getConfig());
}
@Test
public void testPreferRgb565() throws IOException {
Bitmap rgb565 = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
InputStream stream = compressBitmap(rgb565, Bitmap.CompressFormat.JPEG);
options.set(Downsampler.DECODE_FORMAT, DecodeFormat.PREFER_RGB_565);
Resource<Bitmap> result = downsampler.decode(stream, 100, 100, options);
assertEquals(Bitmap.Config.RGB_565, result.get().getConfig());
}
@Test
public void testCalculateScaling_withInvalidSourceSizes_doesNotCrash() {
runScaleTest(0, 0, 100, 100, DownsampleStrategy.AT_MOST, 0, 0);
runScaleTest(-1, -1, 100, 100, DownsampleStrategy.AT_MOST, -1, -1);
runScaleTest(0, 0, 100, 100, DownsampleStrategy.AT_LEAST, 0, 0);
runScaleTest(-1, -1, 100, 100, DownsampleStrategy.CENTER_OUTSIDE, -1, -1);
}
@Test
public void testCalculateScaling_withAtMost() {
DownsampleStrategy strategy = DownsampleStrategy.AT_MOST;
runScaleTest(100, 100, 100, 100, strategy, 100, 100);
runScaleTest(200, 200, 100, 100, strategy, 100, 100);
runScaleTest(400, 400, 100, 100, strategy, 100, 100);
runScaleTest(300, 300, 100, 100, strategy, 75, 75);
runScaleTest(799, 100, 100, 100, strategy, 100, 13);
runScaleTest(800, 100, 100, 100, strategy, 100, 13);
runScaleTest(801, 100, 100, 100, strategy, 50, 6);
runScaleTest(100, 800, 100, 100, strategy, 13, 100);
runScaleTest(87, 78, 100, 100, strategy, 87, 78);
}
@Test
public void testCalculateScaling_withAtLeast() {
DownsampleStrategy strategy = DownsampleStrategy.AT_LEAST;
runScaleTest(100, 100, 100, 100, strategy, 100, 100);
runScaleTest(200, 200, 100, 100, strategy, 100, 100);
runScaleTest(400, 400, 100, 100, strategy, 100, 100);
runScaleTest(300, 300, 100, 100, strategy, 150, 150);
runScaleTest(799, 100, 100, 100, strategy, 799, 100);
runScaleTest(800, 100, 100, 100, strategy, 800, 100);
runScaleTest(801, 100, 100, 100, strategy, 801, 100);
runScaleTest(100, 800, 100, 100, strategy, 100, 800);
runScaleTest(87, 78, 100, 100, strategy, 87, 78);
}
@Test
public void testCalculateScaling_withCenterInside() {
DownsampleStrategy strategy = DownsampleStrategy.FIT_CENTER;
runScaleTest(100, 100, 100, 100, strategy, 100, 100);
runScaleTest(200, 200, 100, 100, strategy, 100, 100);
runScaleTest(400, 400, 100, 100, strategy, 100, 100);
runScaleTest(300, 300, 100, 100, strategy, 100, 100);
runScaleTest(799, 100, 100, 100, strategy, 100, 13);
runScaleTest(800, 100, 100, 100, strategy, 100, 13);
runScaleTest(801, 100, 100, 100, strategy, 100, 13);
runScaleTest(100, 800, 100, 100, strategy, 13, 100);
runScaleTest(87, 78, 100, 100, strategy, 100, 90);
}
@Test
public void testCalculateScaling_withCenterOutside() {
DownsampleStrategy strategy = DownsampleStrategy.CENTER_OUTSIDE;
runScaleTest(100, 100, 100, 100, strategy, 100, 100);
runScaleTest(200, 200, 100, 100, strategy, 100, 100);
runScaleTest(400, 400, 100, 100, strategy, 100, 100);
runScaleTest(300, 300, 100, 100, strategy, 100, 100);
runScaleTest(799, 100, 100, 100, strategy, 799, 100);
runScaleTest(800, 100, 100, 100, strategy, 800, 100);
runScaleTest(801, 100, 100, 100, strategy, 801, 100);
runScaleTest(100, 800, 100, 100, strategy, 100, 800);
runScaleTest(87, 78, 100, 100, strategy, 112, 100);
}
@Test
public void testCalculateScaling_withNone() {
DownsampleStrategy strategy = DownsampleStrategy.NONE;
runScaleTest(100, 100, 100, 100, strategy, 100, 100);
runScaleTest(200, 200, 100, 100, strategy, 200, 200);
runScaleTest(400, 400, 100, 100, strategy, 400, 400);
runScaleTest(300, 300, 100, 100, strategy, 300, 300);
runScaleTest(799, 100, 100, 100, strategy, 799, 100);
runScaleTest(800, 100, 100, 100, strategy, 800, 100);
runScaleTest(801, 100, 100, 100, strategy, 801, 100);
runScaleTest(100, 800, 100, 100, strategy, 100, 800);
runScaleTest(87, 78, 100, 100, strategy, 87, 78);
}
// BitmapFactory does not support downsampling wbmp files on platforms <=M. See b/27305903.
@Test
public void testCalculateScaling_withWbmp() {
Util.setSdkVersionInt(23);
DownsampleStrategy strategy = DownsampleStrategy.FIT_CENTER;
BitmapFactory.Options options = new BitmapFactory.Options();
options.outMimeType = "image/vnd.wap.wbmp";
runScaleTest(100, 100, 100, 100, strategy, 100, 100, options);
runScaleTest(200, 200, 100, 100, strategy, 100, 100, options);
runScaleTest(400, 400, 100, 100, strategy, 100, 100, options);
runScaleTest(300, 300, 100, 100, strategy, 100, 100, options);
runScaleTest(799, 100, 100, 100, strategy, 100, 13, options);
runScaleTest(800, 100, 100, 100, strategy, 100, 13, options);
runScaleTest(801, 100, 100, 100, strategy, 100, 13, options);
runScaleTest(100, 800, 100, 100, strategy, 13, 100, options);
runScaleTest(87, 78, 100, 100, strategy, 100, 90, options);
}
private static void runScaleTest(int sourceWidth, int sourceHeight, int targetWidth,
int targetHeight, DownsampleStrategy strategy, int expectedWidth, int expectedHeight) {
runScaleTest(sourceWidth, sourceHeight, targetWidth, targetHeight, strategy, expectedWidth,
expectedHeight, new BitmapFactory.Options());
}
private static void runScaleTest(int sourceWidth, int sourceHeight, int targetWidth,
int targetHeight, DownsampleStrategy strategy, int expectedWidth, int expectedHeight,
BitmapFactory.Options options) {
Downsampler.calculateScaling(strategy, 0, sourceWidth, sourceHeight, targetWidth, targetHeight,
options);
assertSize(sourceWidth, sourceHeight, expectedWidth, expectedHeight, options);
}
private static void assertSize(int sourceWidth, int sourceHeight, int expectedWidth,
int expectedHeight, BitmapFactory.Options options) {
float sampleSize = Math.max(1, options.inSampleSize);
int downsampledWidth = (int) ((sourceWidth / sampleSize) + 0.5f);
int downsampledHeight = (int) ((sourceHeight / sampleSize) + 0.5f);
float scaleFactor = options.inScaled && options.inTargetDensity > 0 && options.inDensity > 0
? options.inTargetDensity / (float) options.inDensity : 1f;
int scaledWidth = (int) Math.ceil(downsampledWidth * scaleFactor);
int scaledHeight = (int) Math.ceil(downsampledHeight * scaleFactor);
assertThat(scaledWidth).isIn(closed(expectedWidth, expectedWidth + 1));
assertThat(scaledHeight).isIn(closed(expectedHeight, expectedHeight + 1));
}
private InputStream compressBitmap(Bitmap bitmap, Bitmap.CompressFormat compressFormat)
throws FileNotFoundException {
ByteArrayOutputStream os = new ByteArrayOutputStream();
bitmap.compress(compressFormat, 100, os);
return new ByteArrayInputStream(os.toByteArray());
}
// Robolectric doesn't implement getAllocationByteCount correctly.
@Implements(Bitmap.class)
public static class AllocationSizeBitmap extends ShadowBitmap {
@Implementation
public int getAllocationByteCount() {
return getWidth() * getHeight() * (getConfig() == Bitmap.Config.ARGB_8888 ? 4 : 2);
}
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册