Skip to content

Commit b1b024e

Browse files
committed
Improve rounding in Downsampler and add emulator test.
------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=168236016
1 parent f5ba374 commit b1b024e

File tree

2 files changed

+101
-261
lines changed

2 files changed

+101
-261
lines changed

library/src/main/java/com/bumptech/glide/load/resource/bitmap/Downsampler.java

+101-26
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import android.util.Log;
1111
import com.bumptech.glide.load.DecodeFormat;
1212
import com.bumptech.glide.load.ImageHeaderParser;
13+
import com.bumptech.glide.load.ImageHeaderParser.ImageType;
1314
import com.bumptech.glide.load.ImageHeaderParserUtils;
1415
import com.bumptech.glide.load.Option;
1516
import com.bumptech.glide.load.Options;
@@ -121,6 +122,9 @@ public void onDecodeComplete(BitmapPool bitmapPool, Bitmap downsampled) throws I
121122
// 5MB. This is the max image header size we can handle, we preallocate a much smaller buffer
122123
// but will resize up to this amount if necessary.
123124
private static final int MARK_POSITION = 5 * 1024 * 1024;
125+
// Defines the level of precision we get when using inDensity/inTargetDensity to calculate an
126+
// arbitrary float scale factor.
127+
private static final int DENSITY_PRECISION_MULTIPLIER = 1000000000;
124128

125129
private final BitmapPool bitmapPool;
126130
private final DisplayMetrics displayMetrics;
@@ -231,8 +235,20 @@ private Bitmap decodeFromWrappedStreams(InputStream is,
231235
int targetWidth = requestedWidth == Target.SIZE_ORIGINAL ? sourceWidth : requestedWidth;
232236
int targetHeight = requestedHeight == Target.SIZE_ORIGINAL ? sourceHeight : requestedHeight;
233237

234-
calculateScaling(downsampleStrategy, degreesToRotate, sourceWidth, sourceHeight, targetWidth,
235-
targetHeight, options);
238+
ImageType imageType = ImageHeaderParserUtils.getType(parsers, is, byteArrayPool);
239+
240+
calculateScaling(
241+
imageType,
242+
is,
243+
callbacks,
244+
bitmapPool,
245+
downsampleStrategy,
246+
degreesToRotate,
247+
sourceWidth,
248+
sourceHeight,
249+
targetWidth,
250+
targetHeight,
251+
options);
236252
calculateConfig(
237253
is,
238254
decodeFormat,
@@ -244,8 +260,7 @@ private Bitmap decodeFromWrappedStreams(InputStream is,
244260

245261
boolean isKitKatOrGreater = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
246262
// Prior to KitKat, the inBitmap size must exactly match the size of the bitmap we're decoding.
247-
if ((options.inSampleSize == 1 || isKitKatOrGreater)
248-
&& shouldUsePool(is)) {
263+
if ((options.inSampleSize == 1 || isKitKatOrGreater) && shouldUsePool(imageType)) {
249264
int expectedWidth;
250265
int expectedHeight;
251266
if (fixBitmapToRequestedDimensions && isKitKatOrGreater) {
@@ -299,10 +314,18 @@ && shouldUsePool(is)) {
299314
}
300315

301316
// Visible for testing.
302-
static void calculateScaling(DownsampleStrategy downsampleStrategy,
317+
static void calculateScaling(
318+
ImageType imageType,
319+
InputStream is,
320+
DecodeCallbacks decodeCallbacks,
321+
BitmapPool bitmapPool,
322+
DownsampleStrategy downsampleStrategy,
303323
int degreesToRotate,
304-
int sourceWidth, int sourceHeight, int targetWidth, int targetHeight,
305-
BitmapFactory.Options options) {
324+
int sourceWidth,
325+
int sourceHeight,
326+
int targetWidth,
327+
int targetHeight,
328+
BitmapFactory.Options options) throws IOException {
306329
// We can't downsample source content if we can't determine its dimensions.
307330
if (sourceWidth <= 0 || sourceHeight <= 0) {
308331
return;
@@ -323,16 +346,18 @@ static void calculateScaling(DownsampleStrategy downsampleStrategy,
323346

324347
if (exactScaleFactor <= 0f) {
325348
throw new IllegalArgumentException("Cannot scale with factor: " + exactScaleFactor
326-
+ " from: " + downsampleStrategy);
349+
+ " from: " + downsampleStrategy
350+
+ ", source: [" + sourceWidth + "x" + sourceHeight + "]"
351+
+ ", target: [" + targetWidth + "x" + targetHeight + "]");
327352
}
328353
SampleSizeRounding rounding = downsampleStrategy.getSampleSizeRounding(sourceWidth,
329354
sourceHeight, targetWidth, targetHeight);
330355
if (rounding == null) {
331356
throw new IllegalArgumentException("Cannot round with null rounding");
332357
}
333358

334-
int outWidth = (int) (exactScaleFactor * sourceWidth + 0.5f);
335-
int outHeight = (int) (exactScaleFactor * sourceHeight + 0.5f);
359+
int outWidth = round(exactScaleFactor * sourceWidth);
360+
int outHeight = round(exactScaleFactor * sourceHeight);
336361

337362
int widthScaleFactor = sourceWidth / outWidth;
338363
int heightScaleFactor = sourceHeight / outHeight;
@@ -354,14 +379,53 @@ static void calculateScaling(DownsampleStrategy downsampleStrategy,
354379
}
355380
}
356381

357-
float adjustedScaleFactor = powerOfTwoSampleSize * exactScaleFactor;
358-
382+
// Here we mimic framework logic for determining how inSampleSize division is rounded on various
383+
// versions of Android. The logic here has been tested on emulators for Android versions 15-26.
384+
// PNG - Always uses floor
385+
// JPEG - Always uses ceiling
386+
// Webp - Prior to N, always uses floor. At and after N, always uses round.
359387
options.inSampleSize = powerOfTwoSampleSize;
388+
final int powerOfTwoWidth;
389+
final int powerOfTwoHeight;
390+
// Jpeg rounds with ceiling on all API verisons.
391+
if (imageType == ImageType.JPEG) {
392+
powerOfTwoWidth = (int) Math.ceil(sourceWidth / (float) powerOfTwoSampleSize);
393+
powerOfTwoHeight = (int) Math.ceil(sourceHeight / (float) powerOfTwoSampleSize);
394+
} else if (imageType == ImageType.PNG || imageType == ImageType.PNG_A) {
395+
powerOfTwoWidth = (int) Math.floor(sourceWidth / (float) powerOfTwoSampleSize);
396+
powerOfTwoHeight = (int) Math.floor(sourceHeight / (float) powerOfTwoSampleSize);
397+
} else if (imageType == ImageType.WEBP || imageType == ImageType.WEBP_A) {
398+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
399+
powerOfTwoWidth = Math.round(sourceWidth / (float) powerOfTwoSampleSize);
400+
powerOfTwoHeight = Math.round(sourceHeight / (float) powerOfTwoSampleSize);
401+
} else {
402+
powerOfTwoWidth = (int) Math.floor(sourceWidth / (float) powerOfTwoSampleSize);
403+
powerOfTwoHeight = (int) Math.floor(sourceHeight / (float) powerOfTwoSampleSize);
404+
}
405+
} else if (
406+
sourceWidth % powerOfTwoSampleSize != 0 || sourceHeight % powerOfTwoSampleSize != 0) {
407+
// If we're not confident the image is in one of our types, fall back to checking the
408+
// dimensions again. inJustDecodeBounds decodes do obey inSampleSize.
409+
int[] dimensions = getDimensions(is, options, decodeCallbacks, bitmapPool);
410+
// Power of two downsampling in BitmapFactory uses a variety of random factors to determine
411+
// rounding that we can't reliably replicate for all image formats. Use ceiling here to make
412+
// sure that we at least provide a Bitmap that's large enough to fit the content we're going
413+
// to load.
414+
powerOfTwoWidth = dimensions[0];
415+
powerOfTwoHeight = dimensions[1];
416+
} else {
417+
powerOfTwoWidth = sourceWidth / powerOfTwoSampleSize;
418+
powerOfTwoHeight = sourceHeight / powerOfTwoSampleSize;
419+
}
420+
421+
double adjustedScaleFactor = downsampleStrategy.getScaleFactor(
422+
powerOfTwoWidth, powerOfTwoHeight, targetWidth, targetHeight);
423+
360424
// Density scaling is only supported if inBitmap is null prior to KitKat. Avoid setting
361425
// densities here so we calculate the final Bitmap size correctly.
362426
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
363-
options.inTargetDensity = (int) (1000 * adjustedScaleFactor + 0.5f);
364-
options.inDensity = 1000;
427+
options.inTargetDensity = adjustTargetDensityForError(adjustedScaleFactor);
428+
options.inDensity = DENSITY_PRECISION_MULTIPLIER;
365429
}
366430
if (isScaling(options)) {
367431
options.inScaled = true;
@@ -373,6 +437,7 @@ static void calculateScaling(DownsampleStrategy downsampleStrategy,
373437
Log.v(TAG, "Calculate scaling"
374438
+ ", source: [" + sourceWidth + "x" + sourceHeight + "]"
375439
+ ", target: [" + targetWidth + "x" + targetHeight + "]"
440+
+ ", power of two scaled: [" + powerOfTwoWidth + "x" + powerOfTwoHeight + "]"
376441
+ ", exact scale factor: " + exactScaleFactor
377442
+ ", power of 2 sample size: " + powerOfTwoSampleSize
378443
+ ", adjusted scale factor: " + adjustedScaleFactor
@@ -381,24 +446,34 @@ static void calculateScaling(DownsampleStrategy downsampleStrategy,
381446
}
382447
}
383448

384-
private boolean shouldUsePool(InputStream is) throws IOException {
449+
/**
450+
* BitmapFactory calculates the density scale factor as a float. This introduces some non-trivial
451+
* error. This method attempts to account for that error by adjusting the inTargetDensity so that
452+
* the final scale factor is as close to our target as possible.
453+
*/
454+
private static int adjustTargetDensityForError(double adjustedScaleFactor) {
455+
int targetDensity = round(DENSITY_PRECISION_MULTIPLIER * adjustedScaleFactor);
456+
float scaleFactorWithError = targetDensity / (float) DENSITY_PRECISION_MULTIPLIER;
457+
double difference = adjustedScaleFactor / scaleFactorWithError;
458+
return round(difference * targetDensity);
459+
}
460+
461+
// This is weird, but it matches the logic in a bunch of Android views/framework classes for
462+
// rounding.
463+
private static int round(double value) {
464+
return (int) (value + 0.5d);
465+
}
466+
467+
private boolean shouldUsePool(ImageType imageType) throws IOException {
385468
// On KitKat+, any bitmap (of a given config) can be used to decode any other bitmap
386469
// (with the same config).
387470
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
388471
return true;
389472
}
390473

391-
try {
392-
ImageHeaderParser.ImageType type = ImageHeaderParserUtils.getType(parsers, is, byteArrayPool);
393-
// We cannot reuse bitmaps when decoding images that are not PNG or JPG prior to KitKat.
394-
// See: https://groups.google.com/forum/#!msg/android-developers/Mp0MFVFi1Fo/e8ZQ9FGdWdEJ
395-
return TYPES_THAT_USE_POOL_PRE_KITKAT.contains(type);
396-
} catch (IOException e) {
397-
if (Log.isLoggable(TAG, Log.DEBUG)) {
398-
Log.d(TAG, "Cannot determine the image type from header", e);
399-
}
400-
}
401-
return false;
474+
// We cannot reuse bitmaps when decoding images that are not PNG or JPG prior to KitKat.
475+
// See: https://groups.google.com/forum/#!msg/android-developers/Mp0MFVFi1Fo/e8ZQ9FGdWdEJ
476+
return TYPES_THAT_USE_POOL_PRE_KITKAT.contains(imageType);
402477
}
403478

404479
private void calculateConfig(

0 commit comments

Comments
 (0)