Add aspect-preserving opaque image fitting
This commit is contained in:
@@ -0,0 +1,14 @@
|
|||||||
|
# Opaque image fitting
|
||||||
|
|
||||||
|
`ImageScaler.fitOpaque(source, maxWidth, maxHeight, background)` returns an
|
||||||
|
independent RGB BufferedImage. It fits within positive bounds without enlargement,
|
||||||
|
keeps aspect ratio to the nearest pixel (minimum one pixel per dimension), and
|
||||||
|
composites transparent pixels onto the supplied opaque AWT Color. Bilinear
|
||||||
|
interpolation is used. The source is unchanged and graphics resources are released.
|
||||||
|
|
||||||
|
This operation is independent of file format and wire encoding. Callers own image
|
||||||
|
decoding, any source subsampling, and target quantization. Existing PNG scaling
|
||||||
|
methods retain their original behavior.
|
||||||
|
|
||||||
|
`ImageScalerTest` verifies fitting, no enlargement, transparent/partial-alpha
|
||||||
|
composition, narrow images, source preservation, and invalid bounds.
|
||||||
@@ -17,6 +17,41 @@ import java.io.InputStream;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
public class ImageScaler {
|
public class ImageScaler {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fits an image within positive bounds without enlargement, retaining aspect ratio
|
||||||
|
* to the nearest pixel (at least one pixel per dimension). Composites alpha onto
|
||||||
|
* the supplied opaque background and returns an independent RGB image.
|
||||||
|
*
|
||||||
|
* @param source source pixels, never modified
|
||||||
|
* @param maxWidth maximum output width
|
||||||
|
* @param maxHeight maximum output height
|
||||||
|
* @param background opaque background colour
|
||||||
|
* @return fitted image with no alpha channel
|
||||||
|
* @throws IllegalArgumentException for non-positive bounds or translucent background
|
||||||
|
*/
|
||||||
|
public static BufferedImage fitOpaque(final BufferedImage source, final int maxWidth,
|
||||||
|
final int maxHeight, final Color background) {
|
||||||
|
java.util.Objects.requireNonNull(source, "source");
|
||||||
|
java.util.Objects.requireNonNull(background, "background");
|
||||||
|
if (maxWidth < 1 || maxHeight < 1 || background.getAlpha() != 255)
|
||||||
|
throw new IllegalArgumentException("Positive bounds and an opaque background are required");
|
||||||
|
double scale = Math.min(1.0, Math.min((double) maxWidth / source.getWidth(),
|
||||||
|
(double) maxHeight / source.getHeight()));
|
||||||
|
int width = Math.max(1, (int) Math.round(source.getWidth() * scale));
|
||||||
|
int height = Math.max(1, (int) Math.round(source.getHeight() * scale));
|
||||||
|
BufferedImage result = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
|
||||||
|
Graphics2D graphics = result.createGraphics();
|
||||||
|
try {
|
||||||
|
graphics.setColor(background);
|
||||||
|
graphics.fillRect(0, 0, width, height);
|
||||||
|
graphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
|
||||||
|
graphics.drawImage(source, 0, 0, width, height, null);
|
||||||
|
} finally {
|
||||||
|
graphics.dispose();
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Specifies the target image format for scaling operations.
|
* Specifies the target image format for scaling operations.
|
||||||
* <p>
|
* <p>
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package de.neitzel.core.image;
|
||||||
|
|
||||||
|
import java.awt.Color;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
class ImageScalerTest {
|
||||||
|
@Test void fitsWithoutUpscalingAndCompositesTransparency() {
|
||||||
|
BufferedImage source = new BufferedImage(128,64,BufferedImage.TYPE_INT_ARGB);
|
||||||
|
BufferedImage result = ImageScaler.fitOpaque(source,64,64,Color.BLUE);
|
||||||
|
assertEquals(64,result.getWidth());
|
||||||
|
assertEquals(32,result.getHeight());
|
||||||
|
assertEquals(Color.BLUE.getRGB(),result.getRGB(0,0));
|
||||||
|
assertEquals(0,source.getRGB(0,0));
|
||||||
|
BufferedImage tiny = new BufferedImage(1,1,BufferedImage.TYPE_INT_ARGB);
|
||||||
|
tiny.setRGB(0,0,0x80ff0000);
|
||||||
|
result = ImageScaler.fitOpaque(tiny,64,64,Color.BLUE);
|
||||||
|
assertEquals(1,result.getWidth());
|
||||||
|
Color mixed = new Color(result.getRGB(0,0));
|
||||||
|
assertEquals(128,mixed.getRed(),1);
|
||||||
|
assertEquals(127,mixed.getBlue(),1);
|
||||||
|
BufferedImage narrow = ImageScaler.fitOpaque(new BufferedImage(1,1000,BufferedImage.TYPE_INT_ARGB),64,64,Color.BLACK);
|
||||||
|
assertEquals(1,narrow.getWidth()); assertEquals(64,narrow.getHeight());
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> ImageScaler.fitOpaque(tiny,0,64,Color.BLACK));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user