Skip to content

Commit 3df5445

Browse files
committed
Improve comments and consistency of sampling in DownsampleStrategies.
FitCenter, CenterInside, and CenterOutside now all reliably prefer quality on versions < KitKat where we can only do power of two downsampling. I’ve also fixed a bug in FitCenter and CenterInside where we would downsample to 2x the requested size in some cases where there was an exact power of 2 match in one dimension but not the other. The logic to fix the bug isn’t perfect, but it keeps this change small and avoids making any API changes.
1 parent cd37a54 commit 3df5445

File tree

4 files changed

+79
-39
lines changed

4 files changed

+79
-39
lines changed

instrumentation/src/androidTest/java/com/bumptech/glide/load/resource/bitmap/DownsamplerEmulatorTest.java

+15-6
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,17 @@ public void calculateScaling_withAtMost() throws IOException {
8787
.with(formats(JPEG, WEBP).expect(13, 100), formats(PNG).expect(12, 100)),
8888
below(VERSION_CODES.N)
8989
.with(formats(JPEG).expect(13, 100), formats(PNG, WEBP).expect(12, 100)))
90+
.givenImageWithDimensionsOf(
91+
801,
92+
100,
93+
below(KITKAT)
94+
.with(
95+
// JPEG is correct because CENTER_INSIDE wants to give a subsequent
96+
// transformation an image that is greater in size than the requested size. On
97+
// Api > VERSION_CODES.KITKAT, CENTER_INSIDE can do the transformation itself.
98+
// On < VERSION_CODES.KITKAT, it has to assume a subsequent transformation will
99+
// be called.
100+
formats(JPEG).expect(50, 6), formats(PNG, WEBP).expect(50, 6)))
90101
.givenImageWithDimensionsOf(87, 78, onAllApisAndAllFormatsExpect(87, 78))
91102
// This set of examples demonstrate that webp uses round on N+ and floor < N.
92103
.setTargetDimensions(13, 13)
@@ -145,7 +156,6 @@ public void calculateScaling_withCenterInside() throws IOException {
145156
3024,
146157
4032,
147158
atAndAbove(KITKAT).with(allFormats().expect(1977, 2636)),
148-
// TODO(b/134182995): This shouldn't be preserving quality.
149159
below(KITKAT).with(allFormats().expect(3024, 4032)))
150160
.setTargetDimensions(100, 100)
151161
.givenSquareImageWithDimensionOf(100, onAllApisAndAllFormatsExpect(100, 100))
@@ -164,7 +174,7 @@ public void calculateScaling_withCenterInside() throws IOException {
164174
800,
165175
100,
166176
atAndAbove(KITKAT).with(allFormats().expect(100, 13)),
167-
below(KITKAT).with(allFormats().expect(200, 25)))
177+
below(KITKAT).with(formats(JPEG).expect(100, 13), formats(PNG, WEBP).expect(100, 12)))
168178
.givenImageWithDimensionsOf(
169179
801,
170180
100,
@@ -184,7 +194,7 @@ public void calculateScaling_withCenterInside() throws IOException {
184194
100,
185195
800,
186196
atAndAbove(KITKAT).with(allFormats().expect(13, 100)),
187-
below(KITKAT).with(allFormats().expect(25, 200)))
197+
below(KITKAT).with(formats(JPEG).expect(13, 100), formats(PNG, WEBP).expect(12, 100)))
188198
.givenImageWithDimensionsOf(87, 78, onAllApisAndAllFormatsExpect(87, 78))
189199
.setTargetDimensions(897, 897)
190200
.givenImageWithDimensionsOf(
@@ -278,7 +288,6 @@ public void calculateScaling_withFitCenter() throws IOException {
278288
3024,
279289
4032,
280290
atAndAbove(KITKAT).with(allFormats().expect(1977, 2636)),
281-
// TODO(b/134182995): This shouldn't be preserving quality.
282291
below(KITKAT).with(allFormats().expect(3024, 4032)))
283292
.setTargetDimensions(100, 100)
284293
.givenSquareImageWithDimensionOf(100, onAllApisAndAllFormatsExpect(100, 100))
@@ -298,7 +307,7 @@ public void calculateScaling_withFitCenter() throws IOException {
298307
800,
299308
100,
300309
atAndAbove(KITKAT).with(allFormats().expect(100, 13)),
301-
below(KITKAT).with(allFormats().expect(200, 25)))
310+
below(KITKAT).with(formats(JPEG).expect(100, 13), formats(PNG, WEBP).expect(100, 12)))
302311
.givenImageWithDimensionsOf(
303312
801,
304313
100,
@@ -318,7 +327,7 @@ public void calculateScaling_withFitCenter() throws IOException {
318327
100,
319328
800,
320329
atAndAbove(KITKAT).with(allFormats().expect(13, 100)),
321-
below(KITKAT).with(allFormats().expect(25, 200)))
330+
below(KITKAT).with(formats(JPEG).expect(13, 100), formats(PNG, WEBP).expect(12, 100)))
322331
.givenImageWithDimensionsOf(
323332
87,
324333
78,

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

+60-32
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.bumptech.glide.load.resource.bitmap;
22

3+
import android.os.Build;
34
import com.bumptech.glide.load.Option;
45
import com.bumptech.glide.util.Synthetic;
56

@@ -17,11 +18,34 @@
1718
* com.bumptech.glide.load.ResourceDecoder}s are listed below, but the list is not comprehensive
1819
* because {@link DownsampleStrategy} only controls it's output scale value, not how that output
1920
* value is used.
21+
*
22+
* <p>On some versions of Android, precise scaling is not possible. In those cases, the strategies
23+
* can only pick between downsampling to between 1x the requested size and 2x the requested size and
24+
* between 0.5x the requested size and 1x the requested size because only power of two downsampling
25+
* is supported. To preserve the potential for a {@link com.bumptech.glide.load.Transformation} to
26+
* scale precisely without a loss in quality, all but {@link #AT_MOST} will prefer to downsample to
27+
* between 1x and 2x the requested size.
2028
*/
2129
// Public API.
2230
@SuppressWarnings("WeakerAccess")
2331
public abstract class DownsampleStrategy {
2432

33+
/**
34+
* Downsamples so the image's smallest dimension is between the given dimensions and 2x the given
35+
* dimensions, with no size restrictions on the image's largest dimension.
36+
*
37+
* <p>Does not upscale if the requested dimensions are larger than the original dimensions.
38+
*/
39+
public static final DownsampleStrategy AT_LEAST = new AtLeast();
40+
41+
/**
42+
* Downsamples so the image's largest dimension is between 1/2 the given dimensions and the given
43+
* dimensions, with no restrictions on the image's smallest dimension.
44+
*
45+
* <p>Does not upscale if the requested dimensions are larger than the original dimensions.
46+
*/
47+
public static final DownsampleStrategy AT_MOST = new AtMost();
48+
2549
/**
2650
* Scales, maintaining the original aspect ratio, so that one of the image's dimensions is exactly
2751
* 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 {
3155
* and height. To avoid upscaling, use {@link #AT_LEAST}, {@link #AT_MOST} or {@link
3256
* #CENTER_INSIDE}.
3357
*
34-
* <p>On pre-KitKat devices, {@link Downsampler} treats this as equivalent to {@link #AT_MOST}
35-
* because only power of two downsampling can be used.
58+
* <p>On pre-KitKat devices, {@code FIT_CENTER} will downsample by a power of two only so that one
59+
* of the image's dimensions is greater than or equal to the requested size. No guarantees are
60+
* made about the second dimensions. This is <em>NOT</em> the same as {@link #AT_LEAST} because
61+
* only one dimension, not both, are greater than or equal to the requested dimensions, the other
62+
* may be smaller.
3663
*/
3764
public static final DownsampleStrategy FIT_CENTER = new FitCenter();
3865

66+
/** Identical to {@link #FIT_CENTER}, but never upscales. */
67+
public static final DownsampleStrategy CENTER_INSIDE = new CenterInside();
68+
3969
/**
4070
* Scales, maintaining the original aspect ratio, so that one of the image's dimensions is exactly
4171
* 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 {
5080
*/
5181
public static final DownsampleStrategy CENTER_OUTSIDE = new CenterOutside();
5282

53-
/**
54-
* Downsamples so the image's smallest dimension is between the given dimensions and 2x the given
55-
* dimensions, with no size restrictions on the image's largest dimension.
56-
*
57-
* <p>Does not upscale if the requested dimensions are larger than the original dimensions.
58-
*/
59-
public static final DownsampleStrategy AT_LEAST = new AtLeast();
60-
61-
/**
62-
* Downsamples so the image's largest dimension is between 1/2 the given dimensions and the given
63-
* dimensions, with no restrictions on the image's smallest dimension.
64-
*
65-
* <p>Does not upscale if the requested dimensions are larger than the original dimensions.
66-
*/
67-
public static final DownsampleStrategy AT_MOST = new AtMost();
68-
69-
/**
70-
* Returns the original image if it is smaller than the target, otherwise it will be downscaled
71-
* maintaining its original aspect ratio, so that one of the image's dimensions is exactly equal
72-
* to the requested size and the other is less or equal than the requested size.
73-
*
74-
* <p>Does not upscale if the requested dimensions are larger than the original dimensions.
75-
*/
76-
public static final DownsampleStrategy CENTER_INSIDE = new CenterInside();
77-
7883
/** Performs no downsampling or scaling. */
7984
public static final DownsampleStrategy NONE = new None();
8085

@@ -92,6 +97,10 @@ public abstract class DownsampleStrategy {
9297
Option.memory(
9398
"com.bumptech.glide.load.resource.bitmap.Downsampler.DownsampleStrategy", DEFAULT);
9499

100+
@Synthetic
101+
static final boolean IS_BITMAP_FACTORY_SCALING_SUPPORTED =
102+
Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
103+
95104
/**
96105
* Returns a float (0, +infinity) indicating a scale factor to apply to the source width and
97106
* height when displayed in the requested width and height.
@@ -133,15 +142,31 @@ private static class FitCenter extends DownsampleStrategy {
133142
@Override
134143
public float getScaleFactor(
135144
int sourceWidth, int sourceHeight, int requestedWidth, int requestedHeight) {
136-
float widthPercentage = requestedWidth / (float) sourceWidth;
137-
float heightPercentage = requestedHeight / (float) sourceHeight;
138-
return Math.min(widthPercentage, heightPercentage);
145+
if (IS_BITMAP_FACTORY_SCALING_SUPPORTED) {
146+
float widthPercentage = requestedWidth / (float) sourceWidth;
147+
float heightPercentage = requestedHeight / (float) sourceHeight;
148+
149+
return Math.min(widthPercentage, heightPercentage);
150+
} else {
151+
// Similar to AT_LEAST, but only require one dimension or the other to be >= requested
152+
// rather than both.
153+
int maxIntegerFactor =
154+
Math.max(sourceHeight / requestedHeight, sourceWidth / requestedWidth);
155+
return maxIntegerFactor == 0 ? 1f : 1f / Integer.highestOneBit(maxIntegerFactor);
156+
}
139157
}
140158

141159
@Override
142160
public SampleSizeRounding getSampleSizeRounding(
143161
int sourceWidth, int sourceHeight, int requestedWidth, int requestedHeight) {
144-
return SampleSizeRounding.QUALITY;
162+
if (IS_BITMAP_FACTORY_SCALING_SUPPORTED) {
163+
return SampleSizeRounding.QUALITY;
164+
} else {
165+
// TODO: This doesn't seem right, but otherwise we can skip a sample size because QUALITY
166+
// prefers the smaller of the the width and height scale factor. MEMORY is a hack that
167+
// lets us prefer the larger of the two.
168+
return SampleSizeRounding.MEMORY;
169+
}
145170
}
146171
}
147172

@@ -246,7 +271,10 @@ public float getScaleFactor(
246271
@Override
247272
public SampleSizeRounding getSampleSizeRounding(
248273
int sourceWidth, int sourceHeight, int requestedWidth, int requestedHeight) {
249-
return SampleSizeRounding.QUALITY;
274+
return getScaleFactor(sourceWidth, sourceHeight, requestedWidth, requestedHeight) == 1.f
275+
? SampleSizeRounding.QUALITY
276+
: FIT_CENTER.getSampleSizeRounding(
277+
sourceWidth, sourceHeight, requestedWidth, requestedHeight);
250278
}
251279
}
252280

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

+3
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,9 @@ private static void calculateScaling(
430430
int widthScaleFactor = orientedSourceWidth / outWidth;
431431
int heightScaleFactor = orientedSourceHeight / outHeight;
432432

433+
// TODO: This isn't really right for both CenterOutside and CenterInside. Consider allowing
434+
// DownsampleStrategy to pick, or trying to do something more sophisticated like picking the
435+
// scale factor that leads to an exact match.
433436
int scaleFactor =
434437
rounding == SampleSizeRounding.MEMORY
435438
? Math.max(widthScaleFactor, heightScaleFactor)

library/test/src/test/java/com/bumptech/glide/load/resource/bitmap/DownsampleStrategyTest.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import org.robolectric.annotation.Config;
99

1010
@RunWith(RobolectricTestRunner.class)
11-
@Config(sdk = 18)
11+
@Config(sdk = 21)
1212
public class DownsampleStrategyTest {
1313

1414
@Test

0 commit comments

Comments
 (0)