Skip to content

Commit f706c62

Browse files
committed
Merge branch 'bugfix-issue225' into develop
2 parents 4e7e1d2 + 36a5829 commit f706c62

2 files changed

Lines changed: 226 additions & 15 deletions

File tree

src/main/java/net/coobird/thumbnailator/resizers/ProgressiveBilinearResizer.java

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@
4848
*
4949
*/
5050
public class ProgressiveBilinearResizer extends AbstractResizer {
51+
/**
52+
* A resizer that's used when a single-step resize is needed.
53+
*/
54+
private final BilinearResizer bilinearResizer;
55+
5156
/**
5257
* Instantiates a {@link ProgressiveBilinearResizer} with default
5358
* rendering hints.
@@ -64,11 +69,15 @@ public ProgressiveBilinearResizer() {
6469
*/
6570
public ProgressiveBilinearResizer(Map<RenderingHints.Key, Object> hints) {
6671
super(RenderingHints.VALUE_INTERPOLATION_BILINEAR, hints);
72+
bilinearResizer = new BilinearResizer(getRenderingHints());
6773
}
6874

6975
/**
7076
* Resizes an image using the progressive bilinear scaling technique.
7177
* <p>
78+
* When the source image isn't at least twice as large as the destination
79+
* image for both dimensions, a regular one-step scaling is performed.
80+
* <p>
7281
* If the source and/or destination image is {@code null}, then a
7382
* {@link NullPointerException} will be thrown.
7483
*
@@ -89,24 +98,16 @@ public void resize(BufferedImage srcImage, BufferedImage destImage)
8998
final int targetWidth = destImage.getWidth();
9099
final int targetHeight = destImage.getHeight();
91100

92-
// If multi-step downscaling is not required, perform one-step.
101+
/*
102+
* Only perform a progressive bilinear scaling when both width and
103+
* height are at least twice as large as the target.
104+
* In other situations, fallback to using a one-step bilinear resize.
105+
*/
93106
if ((targetWidth * 2 >= currentWidth) && (targetHeight * 2 >= currentHeight)) {
94-
Graphics2D g = createGraphics(destImage);
95-
g.drawImage(srcImage, 0, 0, targetWidth, targetHeight, null);
96-
g.dispose();
107+
bilinearResizer.resize(srcImage, destImage);
97108
return;
98109
}
99110

100-
// Temporary image used for in-place resizing of image.
101-
BufferedImage tempImage = new BufferedImageBuilder(
102-
currentWidth,
103-
currentHeight,
104-
destImage.getType()
105-
).build();
106-
107-
Graphics2D g = createGraphics(tempImage);
108-
g.setComposite(AlphaComposite.Src);
109-
110111
/*
111112
* Determine the size of the first resize step should be.
112113
* 1) Beginning from the target size
@@ -120,15 +121,32 @@ public void resize(BufferedImage srcImage, BufferedImage destImage)
120121
startWidth *= 2;
121122
startHeight *= 2;
122123
}
123-
124+
125+
// FIXME This probably should have been rounded rather than truncated.
124126
currentWidth = startWidth / 2;
125127
currentHeight = startHeight / 2;
126128

129+
// Temporary image used for in-place resizing of image.
130+
/*
131+
* Special case for `width` and `height` when they're `0`.
132+
* This can happen when the target dimension is `1`.
133+
* This is caused by integer truncation in the previous lines.
134+
*/
135+
BufferedImage tempImage = new BufferedImageBuilder(
136+
Math.max(1, currentWidth),
137+
Math.max(1, currentHeight),
138+
destImage.getType()
139+
).build();
140+
141+
Graphics2D g = createGraphics(tempImage);
142+
g.setComposite(AlphaComposite.Src);
143+
127144
// Perform first resize step.
128145
g.drawImage(srcImage, 0, 0, currentWidth, currentHeight, null);
129146

130147
// Perform an in-place progressive bilinear resize.
131148
while ( (currentWidth >= targetWidth * 2) && (currentHeight >= targetHeight * 2) ) {
149+
// FIXME Probably should be rounding rather than truncating.
132150
currentWidth /= 2;
133151
currentHeight /= 2;
134152

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
/*
2+
* Thumbnailator - a thumbnail generation library
3+
*
4+
* Copyright (c) 2008-2025 Chris Kroells
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in
14+
* all copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22+
* THE SOFTWARE.
23+
*/
24+
25+
package net.coobird.thumbnailator.resizers;
26+
27+
import org.junit.Test;
28+
import org.junit.runner.RunWith;
29+
import org.junit.runners.Parameterized;
30+
31+
import java.awt.Color;
32+
import java.awt.Dimension;
33+
import java.awt.Graphics;
34+
import java.awt.image.BufferedImage;
35+
import java.util.ArrayList;
36+
import java.util.Arrays;
37+
import java.util.Collection;
38+
import java.util.List;
39+
40+
import static org.junit.Assert.assertEquals;
41+
42+
@RunWith(Parameterized.class)
43+
public class Issue225ResizeTest {
44+
45+
private static final Color[][] COLOR_BLOCKS = new Color[][] {
46+
new Color[] { Color.red, Color.green, Color.blue },
47+
new Color[] { Color.green, Color.blue, Color.red },
48+
new Color[] { Color.blue, Color.red, Color.green }
49+
};
50+
51+
private static final int SOURCE_WIDTH = 120;
52+
private static final int SOURCE_HEIGHT = 240;
53+
54+
/**
55+
* Creates a RGB test pattern consisting of 3 columns by 6 rows.
56+
*/
57+
private BufferedImage createTestImage() {
58+
int width = SOURCE_WIDTH;
59+
int height = SOURCE_HEIGHT;
60+
61+
BufferedImage img = new BufferedImage(width, height, imageType);
62+
Graphics g = img.createGraphics();
63+
g.setColor(Color.white);
64+
g.fillRect(0, 0, width, height);
65+
66+
int blockSize = 40;
67+
int i = 0;
68+
for (int y = 0; y < height; y += blockSize) {
69+
g.setColor(COLOR_BLOCKS[i % 3][0]);
70+
g.fillRect(0, y, blockSize, blockSize);
71+
g.setColor(COLOR_BLOCKS[i % 3][1]);
72+
g.fillRect(blockSize, y, blockSize, blockSize);
73+
g.setColor(COLOR_BLOCKS[i % 3][2]);
74+
g.fillRect(blockSize * 2, y, blockSize, blockSize);
75+
i++;
76+
}
77+
78+
g.dispose();
79+
return img;
80+
}
81+
82+
private static int round(double v) {
83+
// Floor should be close enough for most test cases.
84+
// It's required for the 3x6 destination size case to work.
85+
return (int) Math.floor(v);
86+
}
87+
88+
/**
89+
* Check each color block to verify if expected color is present.
90+
*/
91+
private static void assertImage(BufferedImage img) {
92+
int width = img.getWidth();
93+
int height = img.getHeight();
94+
95+
double blockWidth = width / 3.0;
96+
double blockHeight = height / 6.0;
97+
double halfBlockWidth = blockWidth / 2.0;
98+
double halfBlockHeight = blockHeight / 2.0;
99+
100+
for (int i = 0; i < 6; i++) {
101+
for (int j = 0; j < 3; j++) {
102+
int x = round((blockWidth * j) + halfBlockWidth);
103+
int y = round((blockHeight * (i % 3)) + halfBlockHeight);
104+
assertEquals(
105+
String.format("mismatch at i=%s, j=%s", i, j),
106+
COLOR_BLOCKS[i % 3][j],
107+
new Color(img.getRGB(x, y))
108+
);
109+
}
110+
}
111+
}
112+
113+
@Parameterized.Parameters(name = "width={0}, height={1}, imageType={2}")
114+
public static Collection<Object[]> testCases() {
115+
List<Integer> imageTypes = Arrays.asList(
116+
BufferedImage.TYPE_INT_ARGB,
117+
BufferedImage.TYPE_INT_RGB
118+
);
119+
120+
List<Dimension> dimensions = Arrays.asList(
121+
new Dimension(SOURCE_WIDTH, SOURCE_HEIGHT),
122+
new Dimension(SOURCE_WIDTH * 2, SOURCE_HEIGHT * 2),
123+
new Dimension(SOURCE_WIDTH * 3, SOURCE_HEIGHT * 3),
124+
new Dimension(SOURCE_WIDTH / 2, SOURCE_HEIGHT / 2),
125+
new Dimension(SOURCE_WIDTH / 3, SOURCE_HEIGHT / 3),
126+
127+
// Test cases for aspect ratio not preserved
128+
new Dimension(SOURCE_WIDTH * 2, SOURCE_HEIGHT / 2),
129+
new Dimension(SOURCE_WIDTH / 2, SOURCE_HEIGHT * 2),
130+
new Dimension(SOURCE_WIDTH * 3, SOURCE_HEIGHT / 3),
131+
new Dimension(SOURCE_WIDTH / 3, SOURCE_HEIGHT * 3),
132+
133+
// Smallest possible dimensions without losing color detail.
134+
new Dimension(3, 6),
135+
new Dimension(6, 12)
136+
);
137+
138+
List<Object[]> testCases = new ArrayList<Object[]>();
139+
for (int imageType : imageTypes) {
140+
for (Dimension dimension : dimensions) {
141+
testCases.add(
142+
new Object[] {
143+
dimension.width,
144+
dimension.height,
145+
imageType
146+
}
147+
);
148+
}
149+
}
150+
151+
return testCases;
152+
}
153+
154+
@Parameterized.Parameter
155+
public int width;
156+
157+
@Parameterized.Parameter(1)
158+
public int height;
159+
160+
@Parameterized.Parameter(2)
161+
public int imageType;
162+
163+
private void resizerTest(Resizer resizer) {
164+
// given
165+
BufferedImage sourceImage = createTestImage();
166+
BufferedImage thumbnail = new BufferedImage(width, height, imageType);
167+
168+
// when
169+
resizer.resize(sourceImage, thumbnail);
170+
171+
// then
172+
assertEquals(
173+
new Dimension(width, height),
174+
new Dimension(thumbnail.getWidth(), thumbnail.getHeight())
175+
);
176+
assertImage(thumbnail);
177+
}
178+
179+
@Test
180+
public void bilinearResizerTest() {
181+
resizerTest(Resizers.BILINEAR);
182+
}
183+
184+
@Test
185+
public void bicubicResizerTest() {
186+
resizerTest(Resizers.BICUBIC);
187+
}
188+
189+
@Test
190+
public void progressiveResizerTest() {
191+
resizerTest(Resizers.PROGRESSIVE);
192+
}
193+
}

0 commit comments

Comments
 (0)