DDS cleanup (#1262)

* Refactorings and code clean-up
* Major rework/standardization:
 * DDSEncoderType, DX10DXGIFormat merged with DDSType for a single way to describe a DDS format
 * Added constants for DXGI formats
 * DDSImageWriteParam is now mutable and supports standard way of setting compression type
 * DDSImageMetadata now supports more of the format
 Performance:
 * DDSReader now use seek() to jump to correct mipmap instead of reading all bytes
 * DDSImageWriter now uses getTile(0, 0) instead of getData() for better performance
* Fix JavaDoc 🎉
* Sonar issues + roll back accidental check-in
* More clean-up: Removed optional flags from param, header size validation, metadata now reports compresion as lossy
* More clean-up: Now keeps stream byte order consistent (LE), support for Raster, more tests
* Mipmap support using ImageIO sequence API
* Added raster write test
+ fixed a small issue for PAM
* Sonar issues
This commit is contained in:
Harald Kuhr
2026-03-11 21:09:26 +01:00
committed by GitHub
parent e61ec45737
commit 263fb75d1d
28 changed files with 1653 additions and 584 deletions
@@ -23,6 +23,7 @@ import static com.twelvemonkeys.lang.Validate.notNull;
* {@link ImageTypeSpecifier}.
* Other values or overrides may be specified using the builder.
*
* @see <a href="https://docs.oracle.com/en/java/javase/11/docs/api/java.desktop/javax/imageio/metadata/doc-files/standard_metadata.html">Standard (Plug-in Neutral) Metadata Format Specification</a>
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
*/
public class StandardImageMetadataSupport extends AbstractMetadata {
@@ -79,11 +80,11 @@ public class StandardImageMetadataSupport extends AbstractMetadata {
textEntries = builder.textEntries;
}
public static Builder builder(ImageTypeSpecifier type) {
protected static Builder builder(ImageTypeSpecifier type) {
return new Builder(type);
}
public static class Builder {
protected static class Builder {
private final ImageTypeSpecifier type;
private ColorSpaceType colorSpaceType;
private boolean blackIsZero = true;
@@ -35,6 +35,8 @@ import com.twelvemonkeys.lang.Validate;
import javax.imageio.IIOParam;
import javax.imageio.ImageIO;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageWriteParam;
import javax.imageio.spi.IIOServiceProvider;
import javax.imageio.spi.ServiceRegistry;
import javax.imageio.stream.ImageInputStream;
@@ -45,7 +47,9 @@ import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Objects;
import java.util.SortedSet;
import java.util.TreeSet;
@@ -68,7 +72,7 @@ public final class IIOUtil {
*/
public static InputStream createStreamAdapter(final ImageInputStream pStream) {
// TODO: Include stream start pos?
// TODO: Skip buffering for known in-memory implementations?
// TODO: Skip buffering for known in-memory implementations? pStream.isCachedMemory
return new BufferedInputStream(new IIOInputStreamAdapter(pStream));
}
@@ -82,7 +86,7 @@ public final class IIOUtil {
*/
public static InputStream createStreamAdapter(final ImageInputStream pStream, final long pLength) {
// TODO: Include stream start pos?
// TODO: Skip buffering for known in-memory implementations?
// TODO: Skip buffering for known in-memory implementations? pStream.isCachedMemory
return new BufferedInputStream(new IIOInputStreamAdapter(pStream, pLength));
}
@@ -359,4 +363,115 @@ public final class IIOUtil {
System.arraycopy(srcRow, srcPos + x, destRow, destPos + x / samplePeriod, pixelStride);
}
}
/**
* Copies all the standard param values from source to destination.
* <p>
* Typical use (in some imaginary {@code FooImageWriter} class):
* </p>
*
* <pre>
* ImageWriteParam param = ...
* FooImageWriteparam fooParam = param instanceof FooImageWriteParam
* ? (FooImageWriteParam) param
* : copyStandardParams(param, getDefaultWriteParam());
* </pre>
*
* May also be useful for {@code ImageReader}s that delegate reading to other plugins
* (like a TIFF plugin delegating JPEG format decoding to a {@code JPEGImageReader}).
*
* @param source the source parameter, may be {@code null}
* @param destination the destination parameter
* @return destination
*
* @param <T> the plugin specific subclass of {@code IIOParam}
*
* @throws NullPointerException if destination is {@code null}
*/
public static <T extends IIOParam> T copyStandardParams(IIOParam source, T destination) {
Objects.requireNonNull(destination);
Validate.isTrue(source != destination, "source must be different from destination");
if (source != null) {
copyIIOParams(source, destination);
// TODO: API & usage... Is it ever useful to copy from a read to a write param or vice versa?
// If not, maybe throw an IllegalArgumentException instead
if (source instanceof ImageReadParam && destination instanceof ImageReadParam) {
copyImageReadParams((ImageReadParam) source, (ImageReadParam) destination);
}
if (source instanceof ImageWriteParam && destination instanceof ImageWriteParam) {
copyImageWriteParams((ImageWriteParam) source, (ImageWriteParam) destination);
}
}
return destination;
}
private static void copyImageWriteParams(ImageWriteParam source, ImageWriteParam destination) {
// TODO: Usage... It's very unlikely that compression settings of one plugin is compatible with another...
// Is the the below useful?
// Also, is it okay to just silently ignore settings from one format that isn't compatible with another?
// Quirky API, we can't query for compression mode, unless source.canWriteCompressed is true...
if (source.canWriteCompressed() && destination.canWriteCompressed()) {
int compressionMode = source.getCompressionMode();
destination.setCompressionMode(compressionMode);
if (compressionMode == ImageWriteParam.MODE_EXPLICIT
&& source.getCompressionType() != null
&& Arrays.asList(destination.getCompressionTypes()).contains(source.getCompressionType())) {
destination.setCompressionType(source.getCompressionType());
destination.setCompressionQuality(source.getCompressionQuality());
}
}
if (source.canWriteProgressive() && destination.canWriteProgressive()) {
destination.setProgressiveMode(source.getProgressiveMode());
}
if (source.canWriteTiles() && destination.canWriteTiles()) {
int tilingMode = source.getTilingMode();
destination.setTilingMode(tilingMode);
if (tilingMode == ImageWriteParam.MODE_EXPLICIT) {
// TODO: What if source can offset (and has offsets) and dest can't? Is it ok to just ignore the setting?
boolean canWriteOffsetTiles = source.canOffsetTiles() && destination.canOffsetTiles();
destination.setTiling(
source.getTileWidth(), source.getTileHeight(),
canWriteOffsetTiles ? source.getTileGridXOffset() : 0,
canWriteOffsetTiles ? source.getTileGridYOffset() : 0
);
}
}
}
private static void copyImageReadParams(ImageReadParam source, ImageReadParam destination) {
destination.setDestination(source.getDestination());
destination.setDestinationBands(source.getDestinationBands());
if (destination.canSetSourceRenderSize()) {
destination.setSourceRenderSize(source.getSourceRenderSize());
}
destination.setSourceProgressivePasses(
source.getSourceMinProgressivePass(),
source.getSourceMaxProgressivePass()
);
}
private static void copyIIOParams(IIOParam source, IIOParam destination) {
destination.setController(source.getController());
destination.setSourceSubsampling(
source.getSourceXSubsampling(), source.getSourceYSubsampling(),
source.getSubsamplingXOffset(), source.getSubsamplingYOffset()
);
destination.setSourceRegion(source.getSourceRegion());
destination.setSourceBands(source.getSourceBands());
destination.setDestinationOffset(source.getDestinationOffset());
destination.setDestinationType(source.getDestinationType());
}
}
@@ -1,8 +1,19 @@
package com.twelvemonkeys.imageio.util;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.ImageWriteParam;
import javax.imageio.plugins.bmp.BMPImageWriteParam;
import javax.imageio.plugins.jpeg.JPEGImageReadParam;
/**
* IIOUtilTest
*/
@@ -204,4 +215,222 @@ public class IIOUtilTest {
private int divCeil(int numerator, int denominator) {
return (numerator + denominator - 1) / denominator;
}
@Test
void copyStandardParamsDestinationNull() {
ImageReadParam param = new ImageReadParam();
assertThrows(NullPointerException.class, () -> IIOUtil.copyStandardParams(null, null));
assertThrows(NullPointerException.class, () -> IIOUtil.copyStandardParams(param, null));
}
@Test
void copyStandardParamsSame() {
ImageReadParam param = new ImageReadParam();
assertThrows(IllegalArgumentException.class, () -> IIOUtil.copyStandardParams(param, param));
}
@Test
void copyStandardParamsSourceNull() {
ImageReadParam param = new ImageReadParam() {
@Override
public void setSourceRegion(Rectangle sourceRegion) {
fail("Should not be invoked");
}
};
assertSame(param, IIOUtil.copyStandardParams(null, param));
}
@Test
void copyStandardParamsImageReadParam() {
int sourceXSubsampling = 3;
int sourceYSubsampling = 4;
int subsamplingXOffset = 1;
int subsamplingYOffset = 2;
Rectangle sourceRegion = new Rectangle(1, 2, 42, 43);
int[] sourceBands = { 0, 1, 2 };
Point destinationOffset = new Point(7, 9);
int[] destinationBands = { 2, 1, 0 };
ImageReadParam sourceParam = new ImageReadParam();
sourceParam.setSourceRegion(sourceRegion);
sourceParam.setSourceSubsampling(sourceXSubsampling, sourceYSubsampling, subsamplingXOffset, subsamplingYOffset);
sourceParam.setSourceBands(sourceBands);
sourceParam.setDestinationOffset(destinationOffset);
sourceParam.setDestinationBands(destinationBands);
JPEGImageReadParam jpegParam = IIOUtil.copyStandardParams(sourceParam, new JPEGImageReadParam());
assertEquals(sourceRegion, jpegParam.getSourceRegion());
assertEquals(sourceXSubsampling, jpegParam.getSourceXSubsampling());
assertEquals(sourceYSubsampling, jpegParam.getSourceYSubsampling());
assertEquals(subsamplingXOffset, jpegParam.getSubsamplingXOffset());
assertEquals(subsamplingYOffset, jpegParam.getSubsamplingYOffset());
assertArrayEquals(sourceBands, jpegParam.getSourceBands());
assertEquals(destinationOffset, jpegParam.getDestinationOffset());
assertArrayEquals(destinationBands, jpegParam.getDestinationBands());
}
@Test
void copyStandardParamsImageReadParamDestination() {
// Destination and destination type is mutually exclusive
BufferedImage destination = new BufferedImage(2, 2, BufferedImage.TYPE_INT_ARGB);
ImageReadParam sourceParam = new ImageReadParam();
sourceParam.setDestination(destination);
assertEquals(destination, IIOUtil.copyStandardParams(sourceParam, new JPEGImageReadParam()).getDestination());
}
@Test
void copyStandardParamsImageReadParamDestinationType() {
// Destination and destination type is mutually exclusive
ImageTypeSpecifier destinationType = ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_BYTE_GRAY);
ImageReadParam sourceParam = new ImageReadParam();
sourceParam.setDestinationType(destinationType);
assertEquals(destinationType, IIOUtil.copyStandardParams(sourceParam, new JPEGImageReadParam()).getDestinationType());
}
@Test
void copyStandardParamsReadToWrite() {
int sourceXSubsampling = 3;
int sourceYSubsampling = 4;
int subsamplingXOffset = 1;
int subsamplingYOffset = 2;
Rectangle sourceRegion = new Rectangle(1, 2, 42, 43);
int[] sourceBands = { 0, 1, 2 };
Point destinationOffset = new Point(7, 9);
ImageWriteParam sourceParam = new ImageWriteParam(null);
sourceParam.setSourceRegion(sourceRegion);
sourceParam.setSourceSubsampling(sourceXSubsampling, sourceYSubsampling, subsamplingXOffset, subsamplingYOffset);
sourceParam.setSourceBands(sourceBands);
sourceParam.setDestinationOffset(destinationOffset);
JPEGImageReadParam jpegParam = IIOUtil.copyStandardParams(sourceParam, new JPEGImageReadParam());
assertEquals(sourceRegion, jpegParam.getSourceRegion());
assertEquals(sourceXSubsampling, jpegParam.getSourceXSubsampling());
assertEquals(sourceYSubsampling, jpegParam.getSourceYSubsampling());
assertEquals(subsamplingXOffset, jpegParam.getSubsamplingXOffset());
assertEquals(subsamplingYOffset, jpegParam.getSubsamplingYOffset());
assertArrayEquals(sourceBands, jpegParam.getSourceBands());
assertEquals(destinationOffset, jpegParam.getDestinationOffset());
assertNull(jpegParam.getDestinationBands()); // Only in read param
}
@Test
void copyStandardParamsImageWriteParam() {
int sourceXSubsampling = 3;
int sourceYSubsampling = 4;
int subsamplingXOffset = 1;
int subsamplingYOffset = 2;
Rectangle sourceRegion = new Rectangle(1, 2, 42, 43);
int[] sourceBands = { 0, 1, 2 };
Point destinationOffset = new Point(7, 9);
ImageWriteParam sourceParam = new ImageWriteParam(null);
sourceParam.setSourceRegion(sourceRegion);
sourceParam.setSourceSubsampling(sourceXSubsampling, sourceYSubsampling, subsamplingXOffset, subsamplingYOffset);
sourceParam.setSourceBands(sourceBands);
sourceParam.setDestinationOffset(destinationOffset);
BMPImageWriteParam fooParam = IIOUtil.copyStandardParams(sourceParam, new BMPImageWriteParam());
assertEquals(sourceRegion, fooParam.getSourceRegion());
assertEquals(sourceXSubsampling, fooParam.getSourceXSubsampling());
assertEquals(sourceYSubsampling, fooParam.getSourceYSubsampling());
assertEquals(subsamplingXOffset, fooParam.getSubsamplingXOffset());
assertEquals(subsamplingYOffset, fooParam.getSubsamplingYOffset());
assertArrayEquals(sourceBands, fooParam.getSourceBands());
assertEquals(destinationOffset, fooParam.getDestinationOffset());
}
@Test
void copyStandardParamsImageWriteParamEverything() {
int sourceXSubsampling = 3;
int sourceYSubsampling = 4;
int subsamplingXOffset = 1;
int subsamplingYOffset = 2;
Rectangle sourceRegion = new Rectangle(1, 2, 42, 43);
int[] sourceBands = { 0, 1, 2 };
Point destinationOffset = new Point(7, 9);
String compressionType = "Foo";
float quality = 0.42f;
ImageWriteParam sourceParam = new ImageWriteParam() {
{
canWriteProgressive = true;
canWriteTiles = true;
canOffsetTiles = true;
canWriteCompressed = true;
compressionTypes = new String[] { "Foo", "Bar" };
}
};
sourceParam.setSourceRegion(sourceRegion);
sourceParam.setSourceSubsampling(sourceXSubsampling, sourceYSubsampling, subsamplingXOffset, subsamplingYOffset);
sourceParam.setSourceBands(sourceBands);
sourceParam.setDestinationOffset(destinationOffset);
sourceParam.setProgressiveMode(ImageWriteParam.MODE_DEFAULT); // Default is COPY_FROM_METADATA...
sourceParam.setTilingMode(ImageWriteParam.MODE_EXPLICIT);
sourceParam.setTiling(1, 2, 3, 4);
sourceParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
sourceParam.setCompressionType(compressionType);
sourceParam.setCompressionQuality(quality);
FooImageWriteParam fooParam = IIOUtil.copyStandardParams(sourceParam, new FooImageWriteParam());
assertEquals(sourceRegion, fooParam.getSourceRegion());
assertEquals(sourceXSubsampling, fooParam.getSourceXSubsampling());
assertEquals(sourceYSubsampling, fooParam.getSourceYSubsampling());
assertEquals(subsamplingXOffset, fooParam.getSubsamplingXOffset());
assertEquals(subsamplingYOffset, fooParam.getSubsamplingYOffset());
assertArrayEquals(sourceBands, fooParam.getSourceBands());
assertEquals(destinationOffset, fooParam.getDestinationOffset());
assertEquals(ImageWriteParam.MODE_DEFAULT, fooParam.getProgressiveMode());
assertEquals(ImageWriteParam.MODE_EXPLICIT, fooParam.getTilingMode());
assertEquals(1, fooParam.getTileWidth());
assertEquals(2, fooParam.getTileHeight());
assertEquals(3, fooParam.getTileGridXOffset());
assertEquals(4, fooParam.getTileGridYOffset());
assertEquals(ImageWriteParam.MODE_EXPLICIT, fooParam.getCompressionMode());
assertEquals(compressionType, fooParam.getCompressionType());
assertEquals(quality, fooParam.getCompressionQuality());
}
// A basic param that supports "everything"
static class FooImageWriteParam extends ImageWriteParam {
FooImageWriteParam() {
canWriteProgressive = true;
canWriteTiles = true;
canOffsetTiles = true;
canWriteCompressed = true;
compressionType = "Unset";
compressionTypes = new String[] { "Bar", "Foo" };
}
}
}
@@ -34,6 +34,7 @@ import com.twelvemonkeys.imageio.stream.URLImageInputStreamSpi;
import org.mockito.InOrder;
import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
@@ -84,6 +85,7 @@ public abstract class ImageWriterAbstractTest<T extends ImageWriter> {
protected static BufferedImage drawSomething(final BufferedImage image) {
Graphics2D g = image.createGraphics();
try {
int width = image.getWidth();
int height = image.getHeight();
@@ -131,18 +133,54 @@ public abstract class ImageWriterAbstractTest<T extends ImageWriter> {
public void testWrite() throws IOException {
ImageWriter writer = createWriter();
for (RenderedImage testData : getTestData()) {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
try {
for (RenderedImage testData : getTestData()) {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
try (ImageOutputStream stream = ImageIO.createImageOutputStream(buffer)) {
writer.setOutput(stream);
writer.write(drawSomething((BufferedImage) testData));
try (ImageOutputStream stream = ImageIO.createImageOutputStream(buffer)) {
writer.setOutput(stream);
writer.write(drawSomething((BufferedImage) testData));
}
catch (IOException e) {
throw new AssertionError(e.getMessage(), e);
}
assertTrue(buffer.size() > 0, "No image data written");
}
catch (IOException e) {
throw new AssertionError(e.getMessage(), e);
}
finally {
writer.dispose();
}
}
@Test
public void testWriteRaster() throws IOException {
ImageWriter writer = createWriter();
try {
if (!writer.canWriteRasters()) {
return;
}
assertTrue(buffer.size() > 0, "No image data written");
ImageWriteParam param = writer.getDefaultWriteParam();
for (RenderedImage testData : getTestData()) {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
try (ImageOutputStream stream = ImageIO.createImageOutputStream(buffer)) {
writer.setOutput(stream);
writer.write(null, new IIOImage(testData.getTile(0, 0), null, null), param);
}
catch (IOException e) {
throw new AssertionError(e.getMessage(), e);
}
assertTrue(buffer.size() > 0, "No image data written");
}
}
finally {
writer.dispose();
}
}