diff --git a/instrumentation/src/androidTest/java/com/bumptech/glide/load/resource/bitmap/DownsamplerEmulatorTest.java b/instrumentation/src/androidTest/java/com/bumptech/glide/load/resource/bitmap/DownsamplerEmulatorTest.java index e35252637e..463dd6a87b 100644 --- a/instrumentation/src/androidTest/java/com/bumptech/glide/load/resource/bitmap/DownsamplerEmulatorTest.java +++ b/instrumentation/src/androidTest/java/com/bumptech/glide/load/resource/bitmap/DownsamplerEmulatorTest.java @@ -87,6 +87,17 @@ public void calculateScaling_withAtMost() throws IOException { .with(formats(JPEG, WEBP).expect(13, 100), formats(PNG).expect(12, 100)), below(VERSION_CODES.N) .with(formats(JPEG).expect(13, 100), formats(PNG, WEBP).expect(12, 100))) + .givenImageWithDimensionsOf( + 801, + 100, + below(KITKAT) + .with( + // JPEG is correct because CENTER_INSIDE wants to give a subsequent + // transformation an image that is greater in size than the requested size. On + // Api > VERSION_CODES.KITKAT, CENTER_INSIDE can do the transformation itself. + // On < VERSION_CODES.KITKAT, it has to assume a subsequent transformation will + // be called. + formats(JPEG).expect(50, 6), formats(PNG, WEBP).expect(50, 6))) .givenImageWithDimensionsOf(87, 78, onAllApisAndAllFormatsExpect(87, 78)) // This set of examples demonstrate that webp uses round on N+ and floor < N. .setTargetDimensions(13, 13) @@ -145,7 +156,6 @@ public void calculateScaling_withCenterInside() throws IOException { 3024, 4032, atAndAbove(KITKAT).with(allFormats().expect(1977, 2636)), - // TODO(b/134182995): This shouldn't be preserving quality. below(KITKAT).with(allFormats().expect(3024, 4032))) .setTargetDimensions(100, 100) .givenSquareImageWithDimensionOf(100, onAllApisAndAllFormatsExpect(100, 100)) @@ -164,7 +174,7 @@ public void calculateScaling_withCenterInside() throws IOException { 800, 100, atAndAbove(KITKAT).with(allFormats().expect(100, 13)), - below(KITKAT).with(allFormats().expect(200, 25))) + below(KITKAT).with(formats(JPEG).expect(100, 13), formats(PNG, WEBP).expect(100, 12))) .givenImageWithDimensionsOf( 801, 100, @@ -184,7 +194,7 @@ public void calculateScaling_withCenterInside() throws IOException { 100, 800, atAndAbove(KITKAT).with(allFormats().expect(13, 100)), - below(KITKAT).with(allFormats().expect(25, 200))) + below(KITKAT).with(formats(JPEG).expect(13, 100), formats(PNG, WEBP).expect(12, 100))) .givenImageWithDimensionsOf(87, 78, onAllApisAndAllFormatsExpect(87, 78)) .setTargetDimensions(897, 897) .givenImageWithDimensionsOf( @@ -278,7 +288,6 @@ public void calculateScaling_withFitCenter() throws IOException { 3024, 4032, atAndAbove(KITKAT).with(allFormats().expect(1977, 2636)), - // TODO(b/134182995): This shouldn't be preserving quality. below(KITKAT).with(allFormats().expect(3024, 4032))) .setTargetDimensions(100, 100) .givenSquareImageWithDimensionOf(100, onAllApisAndAllFormatsExpect(100, 100)) @@ -298,7 +307,7 @@ public void calculateScaling_withFitCenter() throws IOException { 800, 100, atAndAbove(KITKAT).with(allFormats().expect(100, 13)), - below(KITKAT).with(allFormats().expect(200, 25))) + below(KITKAT).with(formats(JPEG).expect(100, 13), formats(PNG, WEBP).expect(100, 12))) .givenImageWithDimensionsOf( 801, 100, @@ -318,7 +327,7 @@ public void calculateScaling_withFitCenter() throws IOException { 100, 800, atAndAbove(KITKAT).with(allFormats().expect(13, 100)), - below(KITKAT).with(allFormats().expect(25, 200))) + below(KITKAT).with(formats(JPEG).expect(13, 100), formats(PNG, WEBP).expect(12, 100))) .givenImageWithDimensionsOf( 87, 78, diff --git a/library/src/main/java/com/bumptech/glide/load/resource/bitmap/DownsampleStrategy.java b/library/src/main/java/com/bumptech/glide/load/resource/bitmap/DownsampleStrategy.java index deb3488cda..604bb2255c 100644 --- a/library/src/main/java/com/bumptech/glide/load/resource/bitmap/DownsampleStrategy.java +++ b/library/src/main/java/com/bumptech/glide/load/resource/bitmap/DownsampleStrategy.java @@ -1,5 +1,6 @@ package com.bumptech.glide.load.resource.bitmap; +import android.os.Build; import com.bumptech.glide.load.Option; import com.bumptech.glide.util.Synthetic; @@ -17,11 +18,34 @@ * com.bumptech.glide.load.ResourceDecoder}s are listed below, but the list is not comprehensive * because {@link DownsampleStrategy} only controls it's output scale value, not how that output * value is used. + * + *

On some versions of Android, precise scaling is not possible. In those cases, the strategies + * can only pick between downsampling to between 1x the requested size and 2x the requested size and + * between 0.5x the requested size and 1x the requested size because only power of two downsampling + * is supported. To preserve the potential for a {@link com.bumptech.glide.load.Transformation} to + * scale precisely without a loss in quality, all but {@link #AT_MOST} will prefer to downsample to + * between 1x and 2x the requested size. */ // Public API. @SuppressWarnings("WeakerAccess") public abstract class DownsampleStrategy { + /** + * Downsamples so the image's smallest dimension is between the given dimensions and 2x the given + * dimensions, with no size restrictions on the image's largest dimension. + * + *

Does not upscale if the requested dimensions are larger than the original dimensions. + */ + public static final DownsampleStrategy AT_LEAST = new AtLeast(); + + /** + * Downsamples so the image's largest dimension is between 1/2 the given dimensions and the given + * dimensions, with no restrictions on the image's smallest dimension. + * + *

Does not upscale if the requested dimensions are larger than the original dimensions. + */ + public static final DownsampleStrategy AT_MOST = new AtMost(); + /** * Scales, maintaining the original aspect ratio, so that one of the image's dimensions is exactly * equal to the requested size and the other dimension is less than or equal to the requested @@ -31,11 +55,17 @@ public abstract class DownsampleStrategy { * and height. To avoid upscaling, use {@link #AT_LEAST}, {@link #AT_MOST} or {@link * #CENTER_INSIDE}. * - *

On pre-KitKat devices, {@link Downsampler} treats this as equivalent to {@link #AT_MOST} - * because only power of two downsampling can be used. + *

On pre-KitKat devices, {@code FIT_CENTER} will downsample by a power of two only so that one + * of the image's dimensions is greater than or equal to the requested size. No guarantees are + * made about the second dimensions. This is NOT the same as {@link #AT_LEAST} because + * only one dimension, not both, are greater than or equal to the requested dimensions, the other + * may be smaller. */ public static final DownsampleStrategy FIT_CENTER = new FitCenter(); + /** Identical to {@link #FIT_CENTER}, but never upscales. */ + public static final DownsampleStrategy CENTER_INSIDE = new CenterInside(); + /** * Scales, maintaining the original aspect ratio, so that one of the image's dimensions is exactly * equal to the requested size and the other dimension is greater than or equal to the requested @@ -50,31 +80,6 @@ public abstract class DownsampleStrategy { */ public static final DownsampleStrategy CENTER_OUTSIDE = new CenterOutside(); - /** - * Downsamples so the image's smallest dimension is between the given dimensions and 2x the given - * dimensions, with no size restrictions on the image's largest dimension. - * - *

Does not upscale if the requested dimensions are larger than the original dimensions. - */ - public static final DownsampleStrategy AT_LEAST = new AtLeast(); - - /** - * Downsamples so the image's largest dimension is between 1/2 the given dimensions and the given - * dimensions, with no restrictions on the image's smallest dimension. - * - *

Does not upscale if the requested dimensions are larger than the original dimensions. - */ - public static final DownsampleStrategy AT_MOST = new AtMost(); - - /** - * Returns the original image if it is smaller than the target, otherwise it will be downscaled - * maintaining its original aspect ratio, so that one of the image's dimensions is exactly equal - * to the requested size and the other is less or equal than the requested size. - * - *

Does not upscale if the requested dimensions are larger than the original dimensions. - */ - public static final DownsampleStrategy CENTER_INSIDE = new CenterInside(); - /** Performs no downsampling or scaling. */ public static final DownsampleStrategy NONE = new None(); @@ -92,6 +97,10 @@ public abstract class DownsampleStrategy { Option.memory( "com.bumptech.glide.load.resource.bitmap.Downsampler.DownsampleStrategy", DEFAULT); + @Synthetic + static final boolean IS_BITMAP_FACTORY_SCALING_SUPPORTED = + Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; + /** * Returns a float (0, +infinity) indicating a scale factor to apply to the source width and * height when displayed in the requested width and height. @@ -133,15 +142,31 @@ private static class FitCenter extends DownsampleStrategy { @Override public float getScaleFactor( int sourceWidth, int sourceHeight, int requestedWidth, int requestedHeight) { - float widthPercentage = requestedWidth / (float) sourceWidth; - float heightPercentage = requestedHeight / (float) sourceHeight; - return Math.min(widthPercentage, heightPercentage); + if (IS_BITMAP_FACTORY_SCALING_SUPPORTED) { + float widthPercentage = requestedWidth / (float) sourceWidth; + float heightPercentage = requestedHeight / (float) sourceHeight; + + return Math.min(widthPercentage, heightPercentage); + } else { + // Similar to AT_LEAST, but only require one dimension or the other to be >= requested + // rather than both. + int maxIntegerFactor = + Math.max(sourceHeight / requestedHeight, sourceWidth / requestedWidth); + return maxIntegerFactor == 0 ? 1f : 1f / Integer.highestOneBit(maxIntegerFactor); + } } @Override public SampleSizeRounding getSampleSizeRounding( int sourceWidth, int sourceHeight, int requestedWidth, int requestedHeight) { - return SampleSizeRounding.QUALITY; + if (IS_BITMAP_FACTORY_SCALING_SUPPORTED) { + return SampleSizeRounding.QUALITY; + } else { + // TODO: This doesn't seem right, but otherwise we can skip a sample size because QUALITY + // prefers the smaller of the the width and height scale factor. MEMORY is a hack that + // lets us prefer the larger of the two. + return SampleSizeRounding.MEMORY; + } } } @@ -246,7 +271,10 @@ public float getScaleFactor( @Override public SampleSizeRounding getSampleSizeRounding( int sourceWidth, int sourceHeight, int requestedWidth, int requestedHeight) { - return SampleSizeRounding.QUALITY; + return getScaleFactor(sourceWidth, sourceHeight, requestedWidth, requestedHeight) == 1.f + ? SampleSizeRounding.QUALITY + : FIT_CENTER.getSampleSizeRounding( + sourceWidth, sourceHeight, requestedWidth, requestedHeight); } } diff --git a/library/src/main/java/com/bumptech/glide/load/resource/bitmap/Downsampler.java b/library/src/main/java/com/bumptech/glide/load/resource/bitmap/Downsampler.java index 9ab9da8ac0..9bf7c51401 100644 --- a/library/src/main/java/com/bumptech/glide/load/resource/bitmap/Downsampler.java +++ b/library/src/main/java/com/bumptech/glide/load/resource/bitmap/Downsampler.java @@ -430,6 +430,9 @@ private static void calculateScaling( int widthScaleFactor = orientedSourceWidth / outWidth; int heightScaleFactor = orientedSourceHeight / outHeight; + // TODO: This isn't really right for both CenterOutside and CenterInside. Consider allowing + // DownsampleStrategy to pick, or trying to do something more sophisticated like picking the + // scale factor that leads to an exact match. int scaleFactor = rounding == SampleSizeRounding.MEMORY ? Math.max(widthScaleFactor, heightScaleFactor) diff --git a/library/test/src/test/java/com/bumptech/glide/load/resource/bitmap/DownsampleStrategyTest.java b/library/test/src/test/java/com/bumptech/glide/load/resource/bitmap/DownsampleStrategyTest.java index 7184c4b66d..74b4b27895 100644 --- a/library/test/src/test/java/com/bumptech/glide/load/resource/bitmap/DownsampleStrategyTest.java +++ b/library/test/src/test/java/com/bumptech/glide/load/resource/bitmap/DownsampleStrategyTest.java @@ -8,7 +8,7 @@ import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) -@Config(sdk = 18) +@Config(sdk = 21) public class DownsampleStrategyTest { @Test