diff --git a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/StandardImageMetadataSupport.java b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/StandardImageMetadataSupport.java index 9b6793ec..bac40138 100644 --- a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/StandardImageMetadataSupport.java +++ b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/StandardImageMetadataSupport.java @@ -23,6 +23,7 @@ import static com.twelvemonkeys.lang.Validate.notNull; * {@link ImageTypeSpecifier}. * Other values or overrides may be specified using the builder. * + * @see Standard (Plug-in Neutral) Metadata Format Specification * @author Harald Kuhr */ 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; diff --git a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/util/IIOUtil.java b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/util/IIOUtil.java index fded5f04..85ad435e 100644 --- a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/util/IIOUtil.java +++ b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/util/IIOUtil.java @@ -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. + *

+ * Typical use (in some imaginary {@code FooImageWriter} class): + *

+ * + *
+     * ImageWriteParam param = ...
+     * FooImageWriteparam fooParam = param instanceof FooImageWriteParam
+     *      ? (FooImageWriteParam) param
+     *      : copyStandardParams(param, getDefaultWriteParam());
+     * 
+ * + * 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 the plugin specific subclass of {@code IIOParam} + * + * @throws NullPointerException if destination is {@code null} + */ + public static 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()); + } } \ No newline at end of file diff --git a/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/IIOUtilTest.java b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/IIOUtilTest.java index 01f32d62..f9aa4f08 100644 --- a/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/IIOUtilTest.java +++ b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/IIOUtilTest.java @@ -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" }; + } + } } \ No newline at end of file diff --git a/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/ImageWriterAbstractTest.java b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/ImageWriterAbstractTest.java index 4fbc57aa..c564dc06 100755 --- a/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/ImageWriterAbstractTest.java +++ b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/ImageWriterAbstractTest.java @@ -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 { 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 { 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(); } } diff --git a/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/BlockCompression.java b/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/BlockCompression.java new file mode 100644 index 00000000..ffcf3945 --- /dev/null +++ b/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/BlockCompression.java @@ -0,0 +1,11 @@ +package com.twelvemonkeys.imageio.plugins.dds; + +enum BlockCompression { + BC1, + BC2, + BC3, + BC4, + BC5, +// BC6H, +// BC7 +} diff --git a/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDS.java b/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDS.java index 7dbb9716..a2a6b6a5 100644 --- a/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDS.java +++ b/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDS.java @@ -33,8 +33,10 @@ package com.twelvemonkeys.imageio.plugins.dds; @SuppressWarnings("unused") interface DDS { - int MAGIC = ('D' << 24) + ('D' << 16) + ('S' << 8) + ' '; //Big-Endian + int MAGIC = 'D' + ('D' << 8) + ('S' << 16) + (' ' << 24); // Little-Endian + int HEADER_SIZE = 124; + int PIXELFORMAT_SIZE = 32; // Header Flags int FLAG_CAPS = 1; // Required in every .dds file. @@ -47,7 +49,6 @@ interface DDS { int FLAG_DEPTH = 1 << 23; // Required in a depth texture. // Pixel Format Flags - int DDSPF_SIZE = 32; int PIXEL_FORMAT_FLAG_ALPHAPIXELS = 0x1; int PIXEL_FORMAT_FLAG_ALPHA = 0x2; int PIXEL_FORMAT_FLAG_FOURCC = 0x04; @@ -56,16 +57,6 @@ interface DDS { //DX10 Resource Dimensions int D3D10_RESOURCE_DIMENSION_TEXTURE2D = 3; - //DXGI Formats (DX10) - int DXGI_FORMAT_BC1_UNORM = 71; - int DXGI_FORMAT_BC2_UNORM = 72; - int DXGI_FORMAT_BC3_UNORM = 77; - int DXGI_FORMAT_BC4_UNORM = 80; - int DXGI_FORMAT_BC5_UNORM = 83; - int DXGI_FORMAT_B8G8R8A8_UNORM_SRGB = 91; - int DXGI_FORMAT_B8G8R8X8_UNORM_SRGB = 93; - int DXGI_FORMAT_R8G8B8A8_UNORM_SRGB = 29; - //dwCaps int DDSCAPS_COMPLEX = 0x8; int DDSCAPS_MIPMAP = 0x400000; diff --git a/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSEncoderType.java b/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSEncoderType.java deleted file mode 100644 index d780dd92..00000000 --- a/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSEncoderType.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.twelvemonkeys.imageio.plugins.dds; - -/** - * Lists a number of supported encoders for block compressors and uncompressed types. - * DXGI Format List - * Compression Algorithms - * An extended Non-DX10 FourCC list - */ -public enum DDSEncoderType { - BC1(DDSType.DXT1.value(), DDS.DXGI_FORMAT_BC1_UNORM, 8), - BC2(DDSType.DXT2.value(), DDS.DXGI_FORMAT_BC2_UNORM, 16), - BC3(DDSType.DXT5.value(), DDS.DXGI_FORMAT_BC3_UNORM, 16), - BC4(0x31495441, DDS.DXGI_FORMAT_BC4_UNORM, 8), - BC5(0x32495441, DDS.DXGI_FORMAT_BC5_UNORM, 16); - - private final int fourCC; - private final int dx10DxgiFormat; - private final int bitCountOrBlockSize; - private final int[] rgbaMask; - - //fourCC constructor - DDSEncoderType(int fourCC, int dx10DxgiFormat, int blockSize) { - this.fourCC = fourCC; - this.dx10DxgiFormat = dx10DxgiFormat; - bitCountOrBlockSize = blockSize; - rgbaMask = null; - } - - //non-fourCC constructor (e.g. A8R8G8B8) - DDSEncoderType(int dx10DxgiFormat, int bitCount, int[] masks) { - fourCC = 0; - this.dx10DxgiFormat = dx10DxgiFormat; - bitCountOrBlockSize = bitCount; - rgbaMask = masks; - } - - boolean isFourCC() { - return fourCC != 0; - } - - int getFourCC() { - return fourCC; - } - - boolean isAlphaMaskSupported() { - return !isFourCC() && rgbaMask[3] > 0; - } - - boolean isBlockCompression() { - return this.isFourCC(); - } - - int getBitsOrBlockSize() { - return bitCountOrBlockSize; - } - - public int[] getRGBAMask() { - return rgbaMask; - } - - public int getDx10Format() { - return dx10DxgiFormat; - } -} diff --git a/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSHeader.java b/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSHeader.java index 6fe4e2b4..a9a7898a 100644 --- a/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSHeader.java +++ b/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSHeader.java @@ -30,11 +30,22 @@ package com.twelvemonkeys.imageio.plugins.dds; +import static com.twelvemonkeys.imageio.plugins.dds.DDSReader.A1R5G5B5_MASKS; +import static com.twelvemonkeys.imageio.plugins.dds.DDSReader.A4R4G4B4_MASKS; +import static com.twelvemonkeys.imageio.plugins.dds.DDSReader.A8B8G8R8_MASKS; +import static com.twelvemonkeys.imageio.plugins.dds.DDSReader.A8R8G8B8_MASKS; +import static com.twelvemonkeys.imageio.plugins.dds.DDSReader.R5G6B5_MASKS; +import static com.twelvemonkeys.imageio.plugins.dds.DDSReader.R8G8B8_MASKS; +import static com.twelvemonkeys.imageio.plugins.dds.DDSReader.X1R5G5B5_MASKS; +import static com.twelvemonkeys.imageio.plugins.dds.DDSReader.X4R4G4B4_MASKS; +import static com.twelvemonkeys.imageio.plugins.dds.DDSReader.X8B8G8R8_MASKS; +import static com.twelvemonkeys.imageio.plugins.dds.DDSReader.X8R8G8B8_MASKS; + import javax.imageio.IIOException; import javax.imageio.stream.ImageInputStream; import java.awt.Dimension; import java.io.IOException; -import java.nio.ByteOrder; +import java.util.Arrays; final class DDSHeader { @@ -52,17 +63,17 @@ final class DDSHeader { private int blueMask; private int alphaMask; + DXT10Header dxt10Header; + @SuppressWarnings("unused") static DDSHeader read(final ImageInputStream imageInput) throws IOException { DDSHeader header = new DDSHeader(); // Read MAGIC bytes [0,3] - imageInput.setByteOrder(ByteOrder.BIG_ENDIAN); int magic = imageInput.readInt(); if (magic != DDS.MAGIC) { throw new IIOException(String.format("Not a DDS file. Expected DDS magic 0x%8x', read 0x%8x", DDS.MAGIC, magic)); } - imageInput.setByteOrder(ByteOrder.LITTLE_ENDIAN); // DDS_HEADER structure // https://learn.microsoft.com/en-us/windows/win32/direct3ddds/dds-header @@ -97,6 +108,9 @@ final class DDSHeader { // DDS_PIXELFORMAT structure int px_dwSize = imageInput.readInt(); // [76,79] + if (px_dwSize != DDS.PIXELFORMAT_SIZE) { + throw new IIOException(String.format("Invalid DDS PIXELFORMAT size (expected %d): %d", DDS.PIXELFORMAT_SIZE, dwSize)); + } header.pixelFormatFlags = imageInput.readInt(); // [80,83] header.fourCC = imageInput.readInt(); // [84,87] @@ -113,6 +127,10 @@ final class DDSHeader { int dwReserved2 = imageInput.readInt(); // [124,127] + if (header.fourCC == DDSType.DXT10.fourCC()) { + header.dxt10Header = DXT10Header.read(imageInput); + } + return header; } @@ -146,31 +164,101 @@ final class DDSHeader { return mipMapCount; } - int getBitCount() { - return bitCount; + DDSType getType() throws IIOException { + if (dxt10Header != null) { + return dxt10Header.getType(); + } + + return getRawType(); } - int getFourCC() { - return fourCC; + DDSType getRawType() throws IIOException { + if ((pixelFormatFlags & DDS.PIXEL_FORMAT_FLAG_FOURCC) != 0) { + // DXT + return DDSType.fromFourCC(fourCC); + } + else if ((pixelFormatFlags & DDS.PIXEL_FORMAT_FLAG_RGB) != 0) { + // RGB + int alphaMask = ((pixelFormatFlags & 0x01) != 0) ? this.alphaMask : 0; // 0x01 alpha + + if (bitCount == 16) { + if (redMask == A1R5G5B5_MASKS[0] && greenMask == A1R5G5B5_MASKS[1] && blueMask == A1R5G5B5_MASKS[2] && alphaMask == A1R5G5B5_MASKS[3]) { + // A1R5G5B5 + return DDSType.A1R5G5B5; + } + else if (redMask == X1R5G5B5_MASKS[0] && greenMask == X1R5G5B5_MASKS[1] && blueMask == X1R5G5B5_MASKS[2] && alphaMask == X1R5G5B5_MASKS[3]) { + // X1R5G5B5 + return DDSType.X1R5G5B5; + } + else if (redMask == A4R4G4B4_MASKS[0] && greenMask == A4R4G4B4_MASKS[1] && blueMask == A4R4G4B4_MASKS[2] && alphaMask == A4R4G4B4_MASKS[3]) { + // A4R4G4B4 + return DDSType.A4R4G4B4; + } + else if (redMask == X4R4G4B4_MASKS[0] && greenMask == X4R4G4B4_MASKS[1] && blueMask == X4R4G4B4_MASKS[2] && alphaMask == X4R4G4B4_MASKS[3]) { + // X4R4G4B4 + return DDSType.X4R4G4B4; + } + else if (redMask == R5G6B5_MASKS[0] && greenMask == R5G6B5_MASKS[1] && blueMask == R5G6B5_MASKS[2] && alphaMask == R5G6B5_MASKS[3]) { + // R5G6B5 + return DDSType.R5G6B5; + } + + throw new IIOException("Unsupported 16bit RGB image."); + } + else if (bitCount == 24) { + if (redMask == R8G8B8_MASKS[0] && greenMask == R8G8B8_MASKS[1] && blueMask == R8G8B8_MASKS[2] && alphaMask == R8G8B8_MASKS[3]) { + // R8G8B8 + return DDSType.R8G8B8; + } + + throw new IIOException("Unsupported 24bit RGB image."); + } + else if (bitCount == 32) { + if (redMask == A8B8G8R8_MASKS[0] && greenMask == A8B8G8R8_MASKS[1] && blueMask == A8B8G8R8_MASKS[2] && alphaMask == A8B8G8R8_MASKS[3]) { + // A8B8G8R8 + return DDSType.A8B8G8R8; + } + else if (redMask == X8B8G8R8_MASKS[0] && greenMask == X8B8G8R8_MASKS[1] && blueMask == X8B8G8R8_MASKS[2] && alphaMask == X8B8G8R8_MASKS[3]) { + // X8B8G8R8 + return DDSType.X8B8G8R8; + } + else if (redMask == A8R8G8B8_MASKS[0] && greenMask == A8R8G8B8_MASKS[1] && blueMask == A8R8G8B8_MASKS[2] && alphaMask == A8R8G8B8_MASKS[3]) { + // A8R8G8B8 + return DDSType.A8R8G8B8; + } + else if (redMask == X8R8G8B8_MASKS[0] && greenMask == X8R8G8B8_MASKS[1] && blueMask == X8R8G8B8_MASKS[2] && alphaMask == X8R8G8B8_MASKS[3]) { + // X8R8G8B8 + return DDSType.X8R8G8B8; + } + + throw new IIOException("Unsupported 32bit RGB image."); + } + + throw new IIOException("Unsupported bit count: " + bitCount); + } + + throw new IIOException("Unsupported YUV or LUMINANCE image."); } - int getPixelFormatFlags() { - return pixelFormatFlags; + @Override + public String toString() { + return "DDSHeader{" + + "flags=" + Integer.toBinaryString(flags) + + ", mipMapCount=" + mipMapCount + + ", dimensions=" + Arrays.toString(Arrays.stream(dimensions) + .map(DDSHeader::dimensionToString) + .toArray(String[]::new)) + + ", pixelFormatFlags=" + Integer.toBinaryString(pixelFormatFlags) + + ", fourCC=" + fourCC + + ", bitCount=" + bitCount + + ", redMask=" + redMask + + ", greenMask=" + greenMask + + ", blueMask=" + blueMask + + ", alphaMask=" + alphaMask + + '}'; } - int getRedMask() { - return redMask; - } - - int getGreenMask() { - return greenMask; - } - - int getBlueMask() { - return blueMask; - } - - int getAlphaMask() { - return alphaMask; + private static String dimensionToString(Dimension dimension) { + return String.format("%dx%d", dimension.width, dimension.height); } } diff --git a/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSImageDataEncoder.java b/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSImageDataEncoder.java index a8ed5f44..d993fa83 100644 --- a/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSImageDataEncoder.java +++ b/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSImageDataEncoder.java @@ -3,7 +3,6 @@ package com.twelvemonkeys.imageio.plugins.dds; import javax.imageio.stream.ImageOutputStream; import java.awt.Color; import java.awt.image.Raster; -import java.awt.image.RenderedImage; import java.io.IOException; import static com.twelvemonkeys.imageio.plugins.dds.DDSReader.ARGB_ORDER; @@ -13,15 +12,11 @@ import static com.twelvemonkeys.imageio.plugins.dds.DDSReader.RGB_16_ORDER; /** * A designated class to encode image data to binary. - *

- * References: - *

- * [1] GPU DXT Decompression. - * [2] TEXTURE COMPRESSION TECHNIQUES. - * [3] Real-Time DXT Compression by J.M.P. van Waveren - * [4] Khronos Data Format Specification v1.4 by Andrew Garrard - *

- *

+ * + * @see GPU DXT Decompression. + * @see TEXTURE COMPRESSION TECHNIQUES. + * @see Real-Time DXT Compression by J.M.P. van Waveren + * @see Khronos Data Format Specification v1.4 by Andrew Garrard */ class DDSImageDataEncoder { private DDSImageDataEncoder() {} @@ -31,25 +26,27 @@ class DDSImageDataEncoder { private static final int BC4_CHANNEL_ALPHA = 3; //BC3 reuses algorithm from BC4 but uses alpha channelIndex for sampling. private static final int BC4_CHANNEL_GREEN = 1; //same re-usage as BC3 but for green channel BC5 uses - static void writeImageData(ImageOutputStream imageOutput, RenderedImage renderedImage, DDSEncoderType type) throws IOException { - switch (type) { + static void writeImageData(ImageOutputStream imageOutput, Raster raster, BlockCompression compression) throws IOException { + // TODO: Support compression == null for uncompressed RGB(A/X) data? + + switch (compression) { case BC1: - new BlockCompressor1(false).encode(imageOutput, renderedImage); + new BlockCompressor1(false).encode(imageOutput, raster); break; case BC2: - new BlockCompressor2().encode(imageOutput, renderedImage); + new BlockCompressor2().encode(imageOutput, raster); break; case BC3: - new BlockCompressor3().encode(imageOutput, renderedImage); + new BlockCompressor3().encode(imageOutput, raster); break; case BC4: - new BlockCompressor4(BC4_CHANNEL_RED).encode(imageOutput, renderedImage); + new BlockCompressor4(BC4_CHANNEL_RED).encode(imageOutput, raster); break; case BC5: - new BlockCompressor5().encode(imageOutput, renderedImage); + new BlockCompressor5().encode(imageOutput, raster); break; default: - throw new IllegalArgumentException("DDS Type is not supported for encoder yet : " + type); + throw new IllegalArgumentException("DDS block compression is not supported yet: " + compression); } } @@ -173,8 +170,14 @@ class DDSImageDataEncoder { boolean getBlockEndpoints(int[] sampledColors, int[] paletteBuffer) { if (sampledColors.length != 64) throw new IllegalStateException("Unintended behaviour, expecting sampled colors of block to be 64, got " + sampledColors.length); - int minR = 0xff; int minG = 0xff; int minB = 0xff; - int maxR = 0; int maxG = 0; int maxB = 0; + int minR = 0xff; + int minG = 0xff; + int minB = 0xff; + + int maxR = 0; + int maxG = 0; + int maxB = 0; + boolean alphaMode = false; int i = 0; while (i < 64) { @@ -209,7 +212,6 @@ class DDSImageDataEncoder { return alphaMode; } - //Reference [3] Page 7 boolean getBlockEndpoints2(int[] sampled, int[] paletteBuffer) { int maxDistance = -1; @@ -415,10 +417,10 @@ class DDSImageDataEncoder { } } - void encode(ImageOutputStream imageOutput, RenderedImage image) throws IOException { - int blocksXCount = (image.getWidth() + 3) / 4; - int blocksYCount = (image.getHeight() + 3) / 4; - Raster raster = image.getData(); + void encode(ImageOutputStream imageOutput, Raster raster) throws IOException { + int blocksXCount = (raster.getWidth() + 3) / 4; + int blocksYCount = (raster.getHeight() + 3) / 4; + for (int blockY = 0; blockY < blocksYCount; blockY++) { for (int blockX = 0; blockX < blocksXCount; blockX++) { raster.getPixels(blockX * 4, blockY * 4, 4, 4, samples); diff --git a/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSMetadata.java b/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSImageMetadata.java similarity index 61% rename from imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSMetadata.java rename to imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSImageMetadata.java index 37e469ff..ff77fef9 100755 --- a/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSMetadata.java +++ b/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSImageMetadata.java @@ -34,25 +34,48 @@ import com.twelvemonkeys.imageio.StandardImageMetadataSupport; import javax.imageio.ImageTypeSpecifier; -final class DDSMetadata extends StandardImageMetadataSupport { - DDSMetadata(ImageTypeSpecifier type, DDSHeader header) { - super(builder(type) - .withCompressionTypeName(compressionName(header)) - .withFormatVersion("1.0") +final class DDSImageMetadata extends StandardImageMetadataSupport { + + DDSImageMetadata(ImageTypeSpecifier specifier, DDSType type) { + super(builder(specifier) + .withCompressionTypeName(compressionName(type)) + .withCompressionLossless(!type.isBlockCompression()) + .withBitsPerSample(bitsPerSample(type)) + .withFormatVersion("1.0") ); } - private static String compressionName(DDSHeader header) { - // If the fourCC is valid, compression is one of the DXTn versions, otherwise None - int flags = header.getPixelFormatFlags(); - - if ((flags & DDS.PIXEL_FORMAT_FLAG_FOURCC) != 0) { - // DXTn - DDSType type = DDSType.valueOf(header.getFourCC()); - + private static String compressionName(DDSType type) { + if (type != null && type.isFourCC()) { return type.name(); } return "None"; } + + private static int[] bitsPerSample(DDSType type) { + if (type.isBlockCompression()) { + return null; // Use defaults + } + + int[] bitsPerSample = new int[4]; + + for (int i = 0; i < bitsPerSample.length; i++) { + bitsPerSample[i] = countMaskBits(type.rgbaMasks[i]); + } + + return bitsPerSample; + } + + private static int countMaskBits(int mask) { + // See https://graphics.stanford.edu/~seander/bithacks.html#CountBitsSetKernighan + int count; + + for (count = 0; mask != 0; count++) { + mask &= mask - 1; // clear the least significant bit set + } + + return count; + } + } diff --git a/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSImageReader.java b/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSImageReader.java index e6a40af2..bef6f42e 100644 --- a/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSImageReader.java +++ b/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSImageReader.java @@ -48,6 +48,12 @@ import java.util.Iterator; import static com.twelvemonkeys.imageio.util.IIOUtil.subsampleRow; +/** + * ImageReader implementation for Microsoft DirectDraw Surface (DDS) format. + * + * @author Paul Allen + * @author Harald Kuhr + */ public final class DDSImageReader extends ImageReaderBase { private DDSHeader header; @@ -90,7 +96,16 @@ public final class DDSImageReader extends ImageReaderBase { checkBounds(imageIndex); readHeader(); - // TODO: Implement for the specific formats... + DDSType type = header.getType(); + if (!type.isBlockCompression() && type.rgbaMasks[3] == 0) { + return ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_INT_RGB); + } + + // TODO: DXT1 can have 1 bit alpha, usually don't... + // DXT3/5 have alpha + // DXT2/4 ...? + + return ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_INT_ARGB); } @@ -146,14 +161,13 @@ public final class DDSImageReader extends ImageReaderBase { public IIOMetadata getImageMetadata(int imageIndex) throws IOException { ImageTypeSpecifier imageType = getRawImageType(imageIndex); - return new DDSMetadata(imageType, header); + return new DDSImageMetadata(imageType, header.getType()); } private void readHeader() throws IOException { if (header == null) { imageInput.setByteOrder(ByteOrder.LITTLE_ENDIAN); header = DDSHeader.read(imageInput); - imageInput.flushBefore(imageInput.getStreamPosition()); } diff --git a/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSImageReaderSpi.java b/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSImageReaderSpi.java index 28af3768..b6ade378 100644 --- a/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSImageReaderSpi.java +++ b/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSImageReaderSpi.java @@ -35,6 +35,7 @@ import com.twelvemonkeys.imageio.spi.ImageReaderSpiBase; import javax.imageio.ImageReader; import javax.imageio.stream.ImageInputStream; import java.io.IOException; +import java.nio.ByteOrder; import java.util.Locale; public final class DDSImageReaderSpi extends ImageReaderSpiBase { @@ -52,10 +53,15 @@ public final class DDSImageReaderSpi extends ImageReaderSpiBase { ImageInputStream stream = (ImageInputStream) source; stream.mark(); + ByteOrder byteOrder = stream.getByteOrder(); try { + stream.setByteOrder(ByteOrder.LITTLE_ENDIAN); + return stream.readInt() == DDS.MAGIC; - } finally { + } + finally { + stream.setByteOrder(byteOrder); stream.reset(); } } @@ -67,6 +73,6 @@ public final class DDSImageReaderSpi extends ImageReaderSpiBase { @Override public String getDescription(Locale locale) { - return "Direct DrawSurface (DDS) Image Reader"; + return "DirectDraw Surface (DDS) Image Reader"; } } diff --git a/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSImageWriteParam.java b/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSImageWriteParam.java new file mode 100644 index 00000000..6b0ea889 --- /dev/null +++ b/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSImageWriteParam.java @@ -0,0 +1,62 @@ +package com.twelvemonkeys.imageio.plugins.dds; + +import javax.imageio.ImageWriteParam; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +public final class DDSImageWriteParam extends ImageWriteParam { + + static final DDSType DEFAULT_TYPE = DDSType.DXT5; + private static final String[] COMPRESSION_TYPES = compressionTypes(); + + private static String[] compressionTypes() { + // TODO: Maybe hardcode subset of values that we actually support writing? + List compressionTypes = Arrays.stream(DDSType.values()) + .filter(DDSType::isBlockCompression) + .map(Enum::name) + .collect(Collectors.toList()); + compressionTypes.add(0, "None"); + + return compressionTypes.toArray(new String[0]); + } + + private boolean writeDXT10; + + DDSImageWriteParam() { + canWriteCompressed = true; + compressionTypes = COMPRESSION_TYPES; + compressionType = DEFAULT_TYPE.name(); + } + + public void setWriteDX10() { + writeDXT10 = true; + } + + public void clearWriteDX10() { + writeDXT10 = false; + } + + public boolean isWriteDXT10() { + return writeDXT10; + } + + DDSType type() { + if (compressionType == null || compressionType.equals("None")) { + return null; + } + + return DDSType.valueOf(compressionType); + } + + int getDxgiFormat() { + DDSType type = type(); + + if (type != null) { + return type.dxgiFormat(); + } + + return DXGI.DXGI_FORMAT_UNKNOWN; + } +} diff --git a/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSImageWriter.java b/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSImageWriter.java index 2c4b1f40..690fa371 100644 --- a/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSImageWriter.java +++ b/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSImageWriter.java @@ -1,7 +1,9 @@ package com.twelvemonkeys.imageio.plugins.dds; import com.twelvemonkeys.imageio.ImageWriterBase; +import com.twelvemonkeys.imageio.util.IIOUtil; +import javax.imageio.IIOException; import javax.imageio.IIOImage; import javax.imageio.ImageIO; import javax.imageio.ImageTypeSpecifier; @@ -9,6 +11,8 @@ import javax.imageio.ImageWriteParam; import javax.imageio.metadata.IIOMetadata; import javax.imageio.spi.ImageWriterSpi; import javax.imageio.stream.MemoryCacheImageOutputStream; + +import java.awt.Dimension; import java.awt.image.Raster; import java.awt.image.RenderedImage; import java.io.File; @@ -18,79 +22,191 @@ import java.nio.file.Files; import java.nio.file.Paths; /** - * A designated class to begin writing DDS file with headers, class {@link DDSImageDataEncoder} will handle image data encoding process + * ImageWriter implementation for Microsoft DirectDraw Surface (DDS) format. + * + * @author KhanTypo + * @author Harald Kuhr */ class DDSImageWriter extends ImageWriterBase { + + private long startPos; + // TODO: Create a SequenceSupport class that handles sequence prepare/write/end + private int mipmapIndex = -1; + private DDSType mipmapType; + private Dimension mipmapDimension; + protected DDSImageWriter(ImageWriterSpi provider) { super(provider); } @Override - public DDSWriterParam getDefaultWriteParam() { - return DDSWriterParam.DEFAULT_PARAM; + public DDSImageWriteParam getDefaultWriteParam() { + return new DDSImageWriteParam(); + } + + @Override + protected void resetMembers() { + mipmapIndex = -1; + mipmapType = null; + mipmapDimension = null; + } + + @Override + public boolean canWriteRasters() { + return true; + } + + @Override + public boolean canWriteSequence() { + return true; + } + + @Override + public void prepareWriteSequence(IIOMetadata streamMetadata) throws IOException { + assertOutput(); + + if (mipmapIndex >= 0) { + throw new IllegalStateException("writeSequence already started"); + } + mipmapIndex = 0; + + startPos = imageOutput.getStreamPosition(); + imageOutput.setByteOrder(ByteOrder.LITTLE_ENDIAN); + imageOutput.writeInt(DDS.MAGIC); + imageOutput.flush(); + } + + @Override + public void endWriteSequence() throws IOException { + assertOutput(); + + if (mipmapIndex < 0) { + throw new IllegalStateException("prepareWriteSequence not called"); + } + + // Go back and update hader + updateHeader(mipmapIndex); + + mipmapIndex = -1; + mipmapType = null; + mipmapDimension = null; + + imageOutput.flush(); } @Override public void write(IIOMetadata streamMetadata, IIOImage image, ImageWriteParam param) throws IOException { - assertOutput(); - RenderedImage renderedImage = image.getRenderedImage(); - ensureTextureSize(renderedImage); - ensureImageChannels(renderedImage); + prepareWriteSequence(streamMetadata); + writeToSequence(image, param); + endWriteSequence(); + } - DDSWriterParam ddsParam = param instanceof DDSWriterParam ? ((DDSWriterParam) param) : this.getDefaultWriteParam(); + @Override + public void writeToSequence(IIOImage image, ImageWriteParam param) throws IOException { + if (mipmapIndex < 0) { + throw new IllegalStateException("prepareWriteSequence not called"); + } - processImageStarted(0); - imageOutput.setByteOrder(ByteOrder.BIG_ENDIAN); - imageOutput.writeInt(DDS.MAGIC); - imageOutput.setByteOrder(ByteOrder.LITTLE_ENDIAN); + Raster raster = getRaster(image); + ensureImageChannels(raster); + ensureTextureDimension(raster); - writeHeader(image, ddsParam); - writeDXT10Header(ddsParam); + DDSImageWriteParam ddsParam = param instanceof DDSImageWriteParam + ? ((DDSImageWriteParam) param) + : IIOUtil.copyStandardParams(param, getDefaultWriteParam()); - //image data encoding + DDSType type = ddsParam.type(); + if (mipmapType == null) { + mipmapType = type; + } + else if (type != mipmapType) { + processWarningOccurred(mipmapIndex, "All images in DDS MipMap must use same pixel format and compression"); + } + if (mipmapType == null) { + throw new IIOException("Only compressed DDS using DXT1-5 or DXT10 with block compression is currently supported"); + } + + if (mipmapIndex == 0) { + writeHeader(raster.getWidth(), raster.getHeight(), mipmapType, ddsParam.isWriteDXT10()); + if (ddsParam.isWriteDXT10()) { + writeDXT10Header(ddsParam.getDxgiFormat()); + } + } + + processImageStarted(mipmapIndex); processImageProgress(0f); - DDSImageDataEncoder.writeImageData(imageOutput, renderedImage, ddsParam.getEncoderType()); - processImageProgress(100f); - imageOutput.flush(); + DDSImageDataEncoder.writeImageData(imageOutput, raster, mipmapType.compression); + + processImageProgress(100f); processImageComplete(); + + mipmapDimension = new Dimension(raster.getWidth(), raster.getHeight()); + mipmapIndex++; + } + + private static Raster getRaster(IIOImage image) throws IIOException { + if (image.hasRaster()) { + return image.getRaster(); + } + else { + RenderedImage renderedImage = image.getRenderedImage(); + + if (renderedImage.getNumXTiles() != 1 || renderedImage.getNumYTiles() != 1) { + throw new IIOException("Only single tile images supported"); + } + + return renderedImage.getTile(0, 0); + } } /** * Checking if the image has 3 channels (RGB) or 4 channels (RGBA) and if image has 8 bits/channel. + * + * @see DDSImageWriterSpi#canEncodeImage(ImageTypeSpecifier) */ - - private void ensureImageChannels(RenderedImage renderedImage) { - Raster data = renderedImage.getData(); + private void ensureImageChannels(Raster data) throws IIOException { int numBands = data.getNumBands(); - if (numBands < 3) - throw new IllegalStateException("Only image with 3 channels (RGB) or 4 channels (RGBA) is supported, got " + numBands + " channels"); + if (numBands < 3 || numBands > 4) { + throw new IIOException( + "Only image with 3 channels (RGB) or 4 channels (RGBA) is supported, got " + numBands + " channels"); + } + int sampleSize = data.getSampleModel().getSampleSize(0); - if (sampleSize != 8) - throw new IllegalStateException("Only image with 8 bits/channel is supported, got " + sampleSize); + if (sampleSize != 8) { + throw new IIOException("Only image with 8 bits/channel is supported, got " + sampleSize); + } } /** * Checking if an image can be evenly divided into blocks of 4x4, ideally a power of 2. - * e.g. 16x16, 32x32, 512x128, 512x512, 1024x512, 1024x1024, 2048x1024, ... + * e.g. 16x16, 32x32, 512x128, 512x512, 1024x512, 1024x1024, 2048x1024... */ - private void ensureTextureSize(RenderedImage renderedImage) { - int w = renderedImage.getWidth(); - int h = renderedImage.getHeight(); - if (w % 4 != 0 || h % 4 != 0) - throw new IllegalStateException(String.format("Image size must be dividable by 4, ideally a power of 2; got (%d x %d)", w, h)); + private void ensureTextureDimension(Raster raster) throws IIOException { + int width = raster.getWidth(); + int height = raster.getHeight(); + + // Should also allow mipmaps 2x2 and 1x1? + if (width % 4 != 0 || height % 4 != 0) { + throw new IIOException(String.format("Image dimensions must be dividable by 4, ideally a power of 2; got %dx%d", width, height)); + } + + if (mipmapDimension != null && (mipmapDimension.width != width * 2|| mipmapDimension.height != height * 2)) { + throw new IIOException( + String.format("For mipmap, image dimensions must be exactly half of previous (%dx%d); got %dx%d", + mipmapDimension.width, mipmapDimension.height, width, height) + ); + } } - - private void writeHeader(IIOImage image, DDSWriterParam param) throws IOException { + private void writeHeader(int width, int height, DDSType type, boolean writeDXT10) throws IOException { imageOutput.writeInt(DDS.HEADER_SIZE); - imageOutput.writeInt(DDS.FLAG_CAPS | DDS.FLAG_HEIGHT | DDS.FLAG_WIDTH | DDS.FLAG_PIXELFORMAT | param.getOptionalBitFlags()); - RenderedImage renderedImage = image.getRenderedImage(); - int height = renderedImage.getHeight(); + int linearSizeOrPitch = type.isBlockCompression() ? DDS.FLAG_LINEARSIZE : DDS.FLAG_PITCH; + imageOutput.writeInt(DDS.FLAG_CAPS | DDS.FLAG_HEIGHT | DDS.FLAG_WIDTH | DDS.FLAG_PIXELFORMAT | linearSizeOrPitch); + imageOutput.writeInt(height); - int width = renderedImage.getWidth(); imageOutput.writeInt(width); - writePitchOrLinearSize(height, width, param); + writePitchOrLinearSize(height, width, type); //dwDepth imageOutput.writeInt(0); //dwMipmapCount @@ -98,97 +214,126 @@ class DDSImageWriter extends ImageWriterBase { //reserved imageOutput.write(new byte[44]); //pixFmt - writePixelFormat(param); + writePixelFormat(type, writeDXT10); //dwCaps, right now we keep it simple by only using DDSCAP_TEXTURE as it is required. imageOutput.writeInt(DDS.DDSCAPS_TEXTURE); //dwCaps2, unused for now as we are not working with cube maps imageOutput.writeInt(0); //dwCaps3, dwCaps4, dwReserved2 : 3 unused integers imageOutput.write(new byte[12]); + } + private void updateHeader(int mipmapCount) throws IOException { + if (mipmapCount == 1) { + // Fast case, nothing to do + return; + } + + long streamPosition = imageOutput.getStreamPosition(); + imageOutput.seek(startPos + 8); // Seek back to start + 4 magic + 4 header size + + int flags = imageOutput.readInt(); + imageOutput.seek(imageOutput.getStreamPosition() - 4); + imageOutput.writeInt(flags | DDS.FLAG_MIPMAPCOUNT); + + imageOutput.seek(imageOutput.getStreamPosition() + 16); + imageOutput.writeInt(mipmapCount); + + imageOutput.seek(streamPosition); // Restore pos } //https://learn.microsoft.com/en-us/windows/win32/direct3ddds/dds-pixelformat - private void writePixelFormat(DDSWriterParam param) throws IOException { - imageOutput.writeInt(DDS.DDSPF_SIZE); - writePixelFormatFlags(param); - writeFourCC(param); - writeRGBAData(param); + private void writePixelFormat(DDSType type, boolean writeDXT10) throws IOException { + imageOutput.writeInt(DDS.PIXELFORMAT_SIZE); + writePixelFormatFlags(type, writeDXT10); + writeFourCC(type, writeDXT10); + writeRGBAData(type, writeDXT10); } - private void writeDXT10Header(DDSWriterParam param) throws IOException { - if (param.isUsingDxt10()) { - //dxgiFormat - imageOutput.writeInt(param.getDxgiFormat()); - //resourceDimension - imageOutput.writeInt(DDS.D3D10_RESOURCE_DIMENSION_TEXTURE2D); - //miscFlag - imageOutput.writeInt(0); - //arraySize - imageOutput.writeInt(1); - //miscFlag2 - imageOutput.writeInt(0); - } + private void writeDXT10Header(int dxgiFormat) throws IOException { + //dxgiFormat + imageOutput.writeInt(dxgiFormat); + //resourceDimension + imageOutput.writeInt(DDS.D3D10_RESOURCE_DIMENSION_TEXTURE2D); + //miscFlag + imageOutput.writeInt(0); + //arraySize + imageOutput.writeInt(1); + //miscFlag2 + imageOutput.writeInt(0); } - private void writeRGBAData(DDSWriterParam param) throws IOException { - if (!param.isUsingDxt10() && !param.getEncoderType().isFourCC()) { + private void writeRGBAData(DDSType type, boolean writeDXT10) throws IOException { + if (!writeDXT10 && !type.isFourCC()) { //dwRGBBitCount - imageOutput.writeInt(param.getEncoderType().getBitsOrBlockSize()); + imageOutput.writeInt(type.blockSize() * 8); // TODO: Is bitcount always a multiple of 8? - int[] mask = param.getEncoderType().getRGBAMask(); + int[] mask = type.rgbaMasks; //dwRBitMask imageOutput.writeInt(mask[0]); //dwGBitMask imageOutput.writeInt(mask[1]); - //dwBitMask + //dwBBitMask imageOutput.writeInt(mask[2]); //dwABitMask imageOutput.writeInt(mask[3]); - } else { + } + else { //write 5 zero integers as fourCC is used imageOutput.write(new byte[20]); } } - private void writeFourCC(DDSWriterParam param) throws IOException { - if (param.isUsingDxt10()) { - imageOutput.writeInt(DDSType.DXT10.value()); - } else if (param.getEncoderType().isFourCC()) - imageOutput.writeInt(param.getEncoderType().getFourCC()); - - } - - private void writePixelFormatFlags(DDSWriterParam param) throws IOException { - if (param.isUsingDxt10() || param.getEncoderType().isFourCC()) { - imageOutput.writeInt(DDS.PIXEL_FORMAT_FLAG_FOURCC); - } else { - imageOutput.writeInt(DDS.PIXEL_FORMAT_FLAG_RGB | (param.getEncoderType().isAlphaMaskSupported() ? DDS.PIXEL_FORMAT_FLAG_ALPHAPIXELS : 0)); + private void writeFourCC(DDSType type, boolean writeDXT10) throws IOException { + if (writeDXT10) { + imageOutput.writeInt(DDSType.DXT10.fourCC()); + } + else if (type.isFourCC()) { + imageOutput.writeInt(type.fourCC()); + } + else { + // No fourCC, custom format... + imageOutput.writeInt(0); } } - private void writePitchOrLinearSize(int height, int width, DDSWriterParam param) throws IOException { - DDSEncoderType type = param.getEncoderType(); - int bitsOrBlockSize = type.getBitsOrBlockSize(); + private void writePixelFormatFlags(DDSType type, boolean writeDXT10) throws IOException { + if (writeDXT10 || type.isFourCC()) { + imageOutput.writeInt(DDS.PIXEL_FORMAT_FLAG_FOURCC); + } + else { + imageOutput.writeInt(DDS.PIXEL_FORMAT_FLAG_RGB | (type.rgbaMasks != null ? DDS.PIXEL_FORMAT_FLAG_ALPHAPIXELS : 0)); + } + } + + private void writePitchOrLinearSize(int height, int width, DDSType type) throws IOException { if (type.isBlockCompression()) { - imageOutput.writeInt(((width + 3) / 4) * ((height + 3) / 4) * bitsOrBlockSize); - } else { - imageOutput.writeInt(width * bitsOrBlockSize); + imageOutput.writeInt(((width + 3) / 4) * ((height + 3) / 4) * type.blockSize()); + } + else { + imageOutput.writeInt(width * type.blockSize()); } } @Override public IIOMetadata getDefaultImageMetadata(ImageTypeSpecifier imageType, ImageWriteParam param) { - throw new UnsupportedOperationException("Direct Draw Surface does not support metadata."); + DDSType type = param instanceof DDSImageWriteParam + ? ((DDSImageWriteParam) param).type() + : DDSImageWriteParam.DEFAULT_TYPE; + + return new DDSImageMetadata(imageType, type); } @Override public IIOMetadata convertImageMetadata(IIOMetadata inData, ImageTypeSpecifier imageType, ImageWriteParam param) { - throw new UnsupportedOperationException("Direct Draw Surface does not support metadata."); + // Nothing useful to convert here... + return getDefaultImageMetadata(imageType, param); } public static void main(String[] args) throws IOException { - if (args.length != 1) throw new IllegalArgumentException("Use 1 input file at a time."); + if (args.length != 1) { + throw new IllegalArgumentException("Use 1 input file at a time."); + } ImageIO.write(ImageIO.read(new File(args[0])), "dds", new MemoryCacheImageOutputStream(Files.newOutputStream(Paths.get("output.dds")))); } } diff --git a/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSImageWriterSpi.java b/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSImageWriterSpi.java index 8a8931e7..4d75aeb0 100644 --- a/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSImageWriterSpi.java +++ b/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSImageWriterSpi.java @@ -4,6 +4,7 @@ import com.twelvemonkeys.imageio.spi.ImageWriterSpiBase; import javax.imageio.ImageTypeSpecifier; import javax.imageio.ImageWriter; + import java.util.Locale; public final class DDSImageWriterSpi extends ImageWriterSpiBase { @@ -13,7 +14,12 @@ public final class DDSImageWriterSpi extends ImageWriterSpiBase { @Override public boolean canEncodeImage(ImageTypeSpecifier type) { - return true; + int numBands = type.getNumBands(); + if (numBands < 3 || numBands > 4) { + return false; + } + + return type.getSampleModel().getSampleSize(0) == 8; } @Override @@ -23,6 +29,6 @@ public final class DDSImageWriterSpi extends ImageWriterSpiBase { @Override public String getDescription(Locale locale) { - return "Direct Draw Surface (DDS) Image Writer"; + return "DirectDraw Surface (DDS) Image Writer"; } } diff --git a/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSProviderInfo.java b/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSProviderInfo.java index 33e8c823..0baf8593 100644 --- a/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSProviderInfo.java +++ b/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSProviderInfo.java @@ -35,18 +35,16 @@ import com.twelvemonkeys.imageio.spi.ReaderWriterProviderInfo; final class DDSProviderInfo extends ReaderWriterProviderInfo { DDSProviderInfo() { super( - DDSProviderInfo.class, - new String[]{"DDS", "dds"}, - new String[]{"dds"}, - new String[]{"image/vnd-ms.dds"}, - "com.twelvemonkeys.imageio.plugins.dds.DDSImageReader", - new String[]{"com.twelvemonkeys.imageio.plugins.dds.DDSImageReaderSpi"}, - "com.twelvemonkeys.imageio.plugins.dds.DDSImageWriter", - new String[]{"com.twelvemonkeys.imageio.plugins.dds.DDSImageWriterSpi"}, - false, null, null, - null, null, true, - null, null, null, - null + DDSProviderInfo.class, + new String[] { "DDS", "dds" }, + new String[] { "dds" }, + new String[] { "image/vnd-ms.dds" }, + "com.twelvemonkeys.imageio.plugins.dds.DDSImageReader", + new String[] { "com.twelvemonkeys.imageio.plugins.dds.DDSImageReaderSpi" }, + "com.twelvemonkeys.imageio.plugins.dds.DDSImageWriter", + new String[] { "com.twelvemonkeys.imageio.plugins.dds.DDSImageWriterSpi" }, + false, null, null, null, null, + true, null, null, null, null ); } } diff --git a/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSReader.java b/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSReader.java index 03364fc9..1c480a9e 100644 --- a/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSReader.java +++ b/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSReader.java @@ -74,7 +74,6 @@ final class DDSReader { static final Order RGB_16_ORDER = new Order(11, 5, 0, -1); // no alpha | 5 red | 6 green | 5 blue private final DDSHeader header; - private DX10Header dxt10Header; DDSReader(DDSHeader header) { this.header = header; @@ -82,18 +81,20 @@ final class DDSReader { int[] read(ImageInputStream imageInput, int imageIndex) throws IOException { // type - DDSType type = getType(); - if (type == DDSType.DXT10) { - dxt10Header = DX10Header.read(imageInput); - type = dxt10Header.getDDSType(); - } + DDSType type = header.getType(); // offset buffer to index mipmap image byte[] buffer = null; for (int i = 0; i <= imageIndex; i++) { - int len = getLength(type, i); - buffer = new byte[len]; - imageInput.readFully(buffer); + int len = getBufferLength(type, i); + + if (i == imageIndex) { + buffer = new byte[len]; + imageInput.readFully(buffer); + } + else { + imageInput.seek(imageInput.getStreamPosition() + len); + } } int width = header.getWidth(imageIndex); @@ -135,82 +136,17 @@ final class DDSReader { } } - private DDSType getType() throws IIOException { - int flags = header.getPixelFormatFlags(); - - if ((flags & DDS.PIXEL_FORMAT_FLAG_FOURCC) != 0) { - // DXT - int type = header.getFourCC(); - return DDSType.valueOf(type); - } else if ((flags & DDS.PIXEL_FORMAT_FLAG_RGB) != 0) { - // RGB - int bitCount = header.getBitCount(); - int redMask = header.getRedMask(); - int greenMask = header.getGreenMask(); - int blueMask = header.getBlueMask(); - int alphaMask = ((flags & 0x01) != 0) ? header.getAlphaMask() : 0; // 0x01 alpha - if (bitCount == 16) { - if (redMask == A1R5G5B5_MASKS[0] && greenMask == A1R5G5B5_MASKS[1] && blueMask == A1R5G5B5_MASKS[2] && alphaMask == A1R5G5B5_MASKS[3]) { - // A1R5G5B5 - return DDSType.A1R5G5B5; - } else if (redMask == X1R5G5B5_MASKS[0] && greenMask == X1R5G5B5_MASKS[1] && blueMask == X1R5G5B5_MASKS[2] && alphaMask == X1R5G5B5_MASKS[3]) { - // X1R5G5B5 - return DDSType.X1R5G5B5; - } else if (redMask == A4R4G4B4_MASKS[0] && greenMask == A4R4G4B4_MASKS[1] && blueMask == A4R4G4B4_MASKS[2] && alphaMask == A4R4G4B4_MASKS[3]) { - // A4R4G4B4 - return DDSType.A4R4G4B4; - } else if (redMask == X4R4G4B4_MASKS[0] && greenMask == X4R4G4B4_MASKS[1] && blueMask == X4R4G4B4_MASKS[2] && alphaMask == X4R4G4B4_MASKS[3]) { - // X4R4G4B4 - return DDSType.X4R4G4B4; - } else if (redMask == R5G6B5_MASKS[0] && greenMask == R5G6B5_MASKS[1] && blueMask == R5G6B5_MASKS[2] && alphaMask == R5G6B5_MASKS[3]) { - // R5G6B5 - return DDSType.R5G6B5; - } else { - throw new IIOException("Unsupported 16bit RGB image."); - } - } else if (bitCount == 24) { - if (redMask == R8G8B8_MASKS[0] && greenMask == R8G8B8_MASKS[1] && blueMask == R8G8B8_MASKS[2] && alphaMask == R8G8B8_MASKS[3]) { - // R8G8B8 - return DDSType.R8G8B8; - } else { - throw new IIOException("Unsupported 24bit RGB image."); - } - } else if (bitCount == 32) { - if (redMask == A8B8G8R8_MASKS[0] && greenMask == A8B8G8R8_MASKS[1] && blueMask == A8B8G8R8_MASKS[2] && alphaMask == A8B8G8R8_MASKS[3]) { - // A8B8G8R8 - return DDSType.A8B8G8R8; - } else if (redMask == X8B8G8R8_MASKS[0] && greenMask == X8B8G8R8_MASKS[1] && blueMask == X8B8G8R8_MASKS[2] && alphaMask == X8B8G8R8_MASKS[3]) { - // X8B8G8R8 - return DDSType.X8B8G8R8; - } else if (redMask == A8R8G8B8_MASKS[0] && greenMask == A8R8G8B8_MASKS[1] && blueMask == A8R8G8B8_MASKS[2] && alphaMask == A8R8G8B8_MASKS[3]) { - // A8R8G8B8 - return DDSType.A8R8G8B8; - } else if (redMask == X8R8G8B8_MASKS[0] && greenMask == X8R8G8B8_MASKS[1] && blueMask == X8R8G8B8_MASKS[2] && alphaMask == X8R8G8B8_MASKS[3]) { - // X8R8G8B8 - return DDSType.X8R8G8B8; - } else { - throw new IIOException("Unsupported 32bit RGB image."); - } - } else { - throw new IIOException("Unsupported bit count: " + bitCount); - } - } else { - throw new IIOException("Unsupported YUV or LUMINANCE image."); - } - } - - private int getLength(DDSType type, int imageIndex) throws IIOException { + private int getBufferLength(DDSType type, int imageIndex) throws IIOException { int width = header.getWidth(imageIndex); int height = header.getHeight(imageIndex); switch (type) { case DXT1: - return 8 * ((width + 3) / 4) * ((height + 3) / 4); case DXT2: case DXT3: case DXT4: case DXT5: - return 16 * ((width + 3) / 4) * ((height + 3) / 4); + return type.blockSize() * ((width + 3) / 4) * ((height + 3) / 4); case A1R5G5B5: case X1R5G5B5: case A4R4G4B4: @@ -221,9 +157,9 @@ final class DDSReader { case X8B8G8R8: case A8R8G8B8: case X8R8G8B8: - return (type.value() & 0xFF) * width * height; + return type.blockSize() * width * height; default: - throw new IIOException("Unknown type: " + Integer.toHexString(type.value())); + throw new IIOException("Unknown type: " + type); } } diff --git a/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSType.java b/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSType.java index da552c85..234f2718 100644 --- a/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSType.java +++ b/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSType.java @@ -30,41 +30,156 @@ package com.twelvemonkeys.imageio.plugins.dds; +import static com.twelvemonkeys.imageio.plugins.dds.BlockCompression.*; +import static com.twelvemonkeys.imageio.plugins.dds.DDSReader.*; + +/** + * Compression Algorithms + * An extended Non-DX10 FourCC list + */ enum DDSType { - DXT1(0x31545844), - DXT2(0x32545844), - DXT3(0x33545844), - DXT4(0x34545844), - DXT5(0x35545844), - DXT10(0x30315844), - A1R5G5B5((1 << 16) | 2), - X1R5G5B5((2 << 16) | 2), - A4R4G4B4((3 << 16) | 2), - X4R4G4B4((4 << 16) | 2), - R5G6B5((5 << 16) | 2), - R8G8B8((1 << 16) | 3), - A8B8G8R8((1 << 16) | 4), - X8B8G8R8((2 << 16) | 4), - A8R8G8B8((3 << 16) | 4), - X8R8G8B8((4 << 16) | 4); + // Compressed types + DXT1('D' + ('X' << 8) + ('T' << 16) + ('1' << 24), 8, DXGI.DXGI_FORMAT_BC1_UNORM, BC1), + DXT2('D' + ('X' << 8) + ('T' << 16) + ('2' << 24), 16, DXGI.DXGI_FORMAT_BC2_UNORM, BC2), + DXT3('D' + ('X' << 8) + ('T' << 16) + ('3' << 24), 16, DXGI.DXGI_FORMAT_BC2_UNORM, BC2), + DXT4('D' + ('X' << 8) + ('T' << 16) + ('4' << 24), 16, DXGI.DXGI_FORMAT_BC3_UNORM, BC3), + DXT5('D' + ('X' << 8) + ('T' << 16) + ('5' << 24), 16, DXGI.DXGI_FORMAT_BC3_UNORM, BC3), - private final int value; + ATI1('A' + ('T' << 8) + ('I' << 16) + ('1' << 24), 8, DXGI.DXGI_FORMAT_BC4_UNORM, BC4), // AKA BC4U + BC4U('B' + ('C' << 8) + ('4' << 16) + ('U' << 24), 8, DXGI.DXGI_FORMAT_BC4_UNORM, BC4), + BC4S('B' + ('C' << 8) + ('4' << 16) + ('S' << 24), 8, DXGI.DXGI_FORMAT_BC4_SNORM, BC4), + ATI2('A' + ('T' << 8) + ('I' << 16) + ('2' << 24), 16, DXGI.DXGI_FORMAT_BC5_UNORM, BC5), // AKA BC5U + BC5U('B' + ('C' << 8) + ('5' << 16) + ('U' << 24), 16, DXGI.DXGI_FORMAT_BC5_UNORM, BC5), + BC5S('B' + ('C' << 8) + ('5' << 16) + ('S' << 24), 16, DXGI.DXGI_FORMAT_BC5_SNORM, BC5), - DDSType(int value) { - this.value = value; + // Special case, see DXT10Header.dxgiFormat for real format + DXT10('D' + ('X' << 8) + ('1' << 16) + ('0' << 24), -1, DXGI.DXGI_FORMAT_UNKNOWN, null), + + // Custom uncompressed pixel formats + // TODO: Consider swapping byte order to reflect the DXGI format? + A1R5G5B5(2, DXGI.DXGI_FORMAT_B5G5R5A1_UNORM, A1R5G5B5_MASKS), + X1R5G5B5(2, DXGI.DXGI_FORMAT_UNKNOWN, X1R5G5B5_MASKS), + A4R4G4B4(2, DXGI.DXGI_FORMAT_B4G4R4A4_UNORM, A4R4G4B4_MASKS), + X4R4G4B4(2, DXGI.DXGI_FORMAT_UNKNOWN, X4R4G4B4_MASKS), + R5G6B5( 2, DXGI.DXGI_FORMAT_B5G6R5_UNORM, R5G6B5_MASKS), + R8G8B8( 3, DXGI.DXGI_FORMAT_UNKNOWN, R8G8B8_MASKS), + A8B8G8R8(4, DXGI.DXGI_FORMAT_R8G8B8A8_UNORM, A8B8G8R8_MASKS), + X8B8G8R8(4, DXGI.DXGI_FORMAT_UNKNOWN, X8B8G8R8_MASKS), + A8R8G8B8(4, DXGI.DXGI_FORMAT_B8G8R8A8_UNORM, A8R8G8B8_MASKS), + X8R8G8B8(4, DXGI.DXGI_FORMAT_B8G8R8X8_UNORM, X8R8G8B8_MASKS); + + private final int fourCC; + private final int blockSize; + private final int dxgiFormat; + final BlockCompression compression; + final int[] rgbaMasks; + + DDSType(int fourCC, int blockSize, int dxgiFormat, BlockCompression compression) { + this(fourCC, blockSize, dxgiFormat, compression, null); } - public int value() { - return value; + DDSType(int blockSize, int dxgiFormat, int[] rgbaMasks) { + this(0, blockSize, dxgiFormat, null, rgbaMasks); } - public static DDSType valueOf(int value) { - for (DDSType type : DDSType.values()) { - if (value == type.value()) { - return type; + DDSType(int fourCC, int blockSize, int dxgiFormat, BlockCompression compression, int[] rgbaMasks) { + this.fourCC = fourCC; + this.blockSize = blockSize; + this.dxgiFormat = dxgiFormat; + this.compression = compression; + this.rgbaMasks = rgbaMasks; + } + + public int fourCC() { + return fourCC; + } + + public int blockSize() { + return blockSize; + } + + public boolean isFourCC() { + return fourCC != 0; + } + + public boolean isBlockCompression() { + return compression != null; + } + + public int dxgiFormat() { + return dxgiFormat; + } + + public static DDSType fromFourCC(int fourCC) { + if (fourCC != 0) { + for (DDSType type : values()) { + if (fourCC == type.fourCC()) { + return type; + } } } - throw new IllegalArgumentException(String.format("Unknown type: 0x%08x", value)); + throw new IllegalArgumentException(String.format("Unknown type: 0x%08x", fourCC)); + } + + public static DDSType fromDXGIFormat(int dxgiFormat) { + switch (dxgiFormat) { + case DXGI.DXGI_FORMAT_R8G8B8A8_TYPELESS: + case DXGI.DXGI_FORMAT_R8G8B8A8_UNORM: + case DXGI.DXGI_FORMAT_R8G8B8A8_UNORM_SRGB: + case DXGI.DXGI_FORMAT_R8G8B8A8_UINT: + return A8B8G8R8; // ABGR + + case DXGI.DXGI_FORMAT_B8G8R8A8_TYPELESS: + case DXGI.DXGI_FORMAT_B8G8R8A8_UNORM: + case DXGI.DXGI_FORMAT_B8G8R8A8_UNORM_SRGB: + return A8R8G8B8; // ARGB + + case DXGI.DXGI_FORMAT_B8G8R8X8_TYPELESS: + case DXGI.DXGI_FORMAT_B8G8R8X8_UNORM: + case DXGI.DXGI_FORMAT_B8G8R8X8_UNORM_SRGB: + return X8R8G8B8; + + case DXGI.DXGI_FORMAT_B5G5R5A1_UNORM: + return A1R5G5B5; + + case DXGI.DXGI_FORMAT_B4G4R4A4_UNORM: + return A4R4G4B4; + + case DXGI.DXGI_FORMAT_B5G6R5_UNORM: + return R5G6B5; + + case DXGI.DXGI_FORMAT_BC1_TYPELESS: + case DXGI.DXGI_FORMAT_BC1_UNORM: + case DXGI.DXGI_FORMAT_BC1_UNORM_SRGB: + return DXT1; + + case DXGI.DXGI_FORMAT_BC2_TYPELESS: + case DXGI.DXGI_FORMAT_BC2_UNORM: + case DXGI.DXGI_FORMAT_BC2_UNORM_SRGB: + return DXT2; + + case DXGI.DXGI_FORMAT_BC3_TYPELESS: + case DXGI.DXGI_FORMAT_BC3_UNORM: + case DXGI.DXGI_FORMAT_BC3_UNORM_SRGB: + return DXT4; + + case DXGI.DXGI_FORMAT_BC4_TYPELESS: + case DXGI.DXGI_FORMAT_BC4_UNORM: + return BC4U; + + case DXGI.DXGI_FORMAT_BC4_SNORM: + return BC4S; + + case DXGI.DXGI_FORMAT_BC5_TYPELESS: + case DXGI.DXGI_FORMAT_BC5_UNORM: + return BC5U; + + case DXGI.DXGI_FORMAT_BC5_SNORM: + return BC5S; + + default: + throw new IllegalArgumentException("Unsupported DXGI_FORMAT: " + dxgiFormat); + } } } diff --git a/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSWriterParam.java b/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSWriterParam.java deleted file mode 100644 index a59269cc..00000000 --- a/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSWriterParam.java +++ /dev/null @@ -1,139 +0,0 @@ -package com.twelvemonkeys.imageio.plugins.dds; - -import javax.imageio.ImageWriteParam; -import java.util.Objects; - -public class DDSWriterParam extends ImageWriteParam { - public static final DDSWriterParam DEFAULT_PARAM = DDSWriterParam.builder().formatBC5().build(); - private final int optionalBitFlags; - private final DDSEncoderType encoderType; - private final boolean enableDxt10; - - DDSWriterParam(int optionalBitFlags, DDSEncoderType encoderType, boolean isUsingDxt10) { - super(); - this.optionalBitFlags = optionalBitFlags; - this.encoderType = encoderType; - this.enableDxt10 = isUsingDxt10; - } - - public static Builder builder() { - return new Builder(); - } - - int getOptionalBitFlags() { - return this.optionalBitFlags; - } - - DDSEncoderType getEncoderType() { - return this.encoderType; - } - - public boolean isUsingDxt10() { - return enableDxt10; - } - - int getDxgiFormat() { - return getEncoderType().getDx10Format(); - } - - public static final class Builder { - //we use Set collection to prevent duplications of bitflag setter calls - private int optionalBitFlag; - private DDSEncoderType encoderType; - private boolean isUsingDxt10; - - public Builder() { - this.optionalBitFlag = 0; - encoderType = null; - isUsingDxt10 = false; - } - - /** - * Enable saving file as Direct3D 10+ format. - */ - public Builder enableDX10() { - isUsingDxt10 = true; - return this; - } - - /** - * Set the compression type to be BC1 (DXT1). - * If DXT10 is enabled, this will set DXGI Format to DXGI_FORMAT_BC1_UNORM. - */ - public Builder formatBC1() { - encoderType = DDSEncoderType.BC1; - return setFlag(DDSFlags.DDSD_LINEARSIZE); - } - - /** - * Set the compression type to be BC2 (DXT3). - * If DXT10 is enabled, this will set DXGI Format to DXGI_FORMAT_BC2_UNORM. - */ - public Builder formatBC2() { - encoderType = DDSEncoderType.BC2; - return setFlag(DDSFlags.DDSD_LINEARSIZE); - } - - /** - * Set the compression type to be BC3 (DXT5). - * If DXT10 is enabled, this will set DXGI Format to DXGI_FORMAT_BC3_UNORM. - */ - public Builder formatBC3() { - encoderType = DDSEncoderType.BC3; - return setFlag(DDSFlags.DDSD_LINEARSIZE); - } - - /** - * Set the compression type to be BC4 (ATI1). - * If DXT10 is enabled, This will set DXGI Format to DXGI_FORMAT_BC4_UNORM. - */ - public Builder formatBC4() { - encoderType = DDSEncoderType.BC4; - return setFlag(DDSFlags.DDSD_LINEARSIZE); - } - - /** - * Set the compression type to be BC5 (ATI2). - * This will set DXGI Format to DXGI_FORMAT_BC5_UNORM. - */ - public Builder formatBC5() { - encoderType = DDSEncoderType.BC5; - return setFlag(DDSFlags.DDSD_LINEARSIZE); - } - - public Builder setFlag(DDSFlags flag) { - optionalBitFlag |= flag.getValue(); - return this; - } - - /** - * Set other optional flags for the DDS Header. - */ - public Builder setFlags(DDSFlags... flags) { - for (DDSFlags flag : flags) - setFlag(flag); - return this; - } - - public DDSWriterParam build() { - Objects.requireNonNull(encoderType, "no DDS format specified."); - return new DDSWriterParam(optionalBitFlag, encoderType, isUsingDxt10); - } - - public enum DDSFlags { - DDSD_PITCH(DDS.FLAG_PITCH),// Required when pitch is provided for an uncompressed texture. - DDSD_MIPMAPCOUNT(DDS.FLAG_MIPMAPCOUNT),// Required in a mipmapped texture. - DDSD_LINEARSIZE(DDS.FLAG_LINEARSIZE),// Required when pitch is provided for a compressed texture. - DDSD_DEPTH(DDS.FLAG_DEPTH);// Required in a depth texture. - - private final int flag; - DDSFlags(int flag) { - this.flag = flag; - } - - public int getValue() { - return flag; - } - } - } -} diff --git a/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DX10DXGIFormat.java b/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DX10DXGIFormat.java deleted file mode 100644 index 0a1678d7..00000000 --- a/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DX10DXGIFormat.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.twelvemonkeys.imageio.plugins.dds; - -import java.util.Arrays; -import java.util.function.IntPredicate; - -/** - * Enum that lists a certain types of DXGI Format this reader supports to read. - * - * DXGI Format List - */ -public enum DX10DXGIFormat { - BC1(DDSType.DXT1, rangeInclusive(70, 72)), - BC2(DDSType.DXT2, rangeInclusive(73, 75)), - BC3(DDSType.DXT5, rangeInclusive(76, 78)), - //BC7(99), - B8G8R8A8(DDSType.A8B8G8R8, exactly(87, 90, 91)), - B8G8R8X8(DDSType.X8B8G8R8, exactly(88, 92, 93)), - R8G8B8A8(DDSType.A8R8G8B8, rangeInclusive(27, 32)); - private final DDSType ddsType; - private final IntPredicate dxgiFormat; - - DX10DXGIFormat(DDSType ddsType, IntPredicate dxgiFormat) { - this.ddsType = ddsType; - this.dxgiFormat = dxgiFormat; - } - - DDSType getCorrespondingType() { - return ddsType; - } - - static DX10DXGIFormat getFormat(int value) { - for (DX10DXGIFormat format : values()) { - if (format.dxgiFormat.test(value)) return format; - } - - throw new IllegalArgumentException("Unsupported DXGI_FORMAT : " + value); - } - - - /** - * @param acceptedValues values in DXGI Formats List, passed values are expected to be in ascending order - */ - private static IntPredicate exactly(int... acceptedValues) { - return test -> Arrays.binarySearch(acceptedValues, test) >= 0; - } - - private static IntPredicate rangeInclusive(int from, int to) { - return test -> from <= test && test <= to; - } -} diff --git a/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DX10Header.java b/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DX10Header.java deleted file mode 100644 index d599d7f8..00000000 --- a/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DX10Header.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.twelvemonkeys.imageio.plugins.dds; - -import javax.imageio.stream.ImageInputStream; -import java.io.IOException; - -//https://learn.microsoft.com/en-us/windows/win32/direct3ddds/dds-header-dxt10 -public final class DX10Header { - final DX10DXGIFormat dxgiFormat; - final int resourceDimension, miscFlag, arraySize, miscFlags2; - - private DX10Header(int dxgiFormat, int resourceDimension, int miscFlag, int arraySize, int miscFlags2) { - this.dxgiFormat = DX10DXGIFormat.getFormat(dxgiFormat); - this.resourceDimension = resourceDimension; - if (this.resourceDimension != DDS.D3D10_RESOURCE_DIMENSION_TEXTURE2D) - throw new IllegalArgumentException("Resource dimension " + resourceDimension + " is not supported, expected 3."); - this.miscFlag = miscFlag; - this.arraySize = arraySize; - this.miscFlags2 = miscFlags2; - } - - static DX10Header read(ImageInputStream inputStream) throws IOException { - int dxgiFormat = inputStream.readInt(); - int resourceDimension = inputStream.readInt(); - int miscFlag = inputStream.readInt(); - int arraySize = inputStream.readInt(); - int miscFlags2 = inputStream.readInt(); - return new DX10Header(dxgiFormat, resourceDimension, miscFlag, arraySize, miscFlags2); - } - - DDSType getDDSType() { - return dxgiFormat.getCorrespondingType(); - } -} diff --git a/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DXGI.java b/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DXGI.java new file mode 100644 index 00000000..318048df --- /dev/null +++ b/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DXGI.java @@ -0,0 +1,129 @@ +package com.twelvemonkeys.imageio.plugins.dds; + +/** + * DXGI Format List + */ +interface DXGI { + int DXGI_FORMAT_UNKNOWN = 0; + int DXGI_FORMAT_R32G32B32A32_TYPELESS = 1; + int DXGI_FORMAT_R32G32B32A32_FLOAT = 2; + int DXGI_FORMAT_R32G32B32A32_UINT = 3; + int DXGI_FORMAT_R32G32B32A32_SINT = 4; + int DXGI_FORMAT_R32G32B32_TYPELESS = 5; + int DXGI_FORMAT_R32G32B32_FLOAT = 6; + int DXGI_FORMAT_R32G32B32_UINT = 7; + int DXGI_FORMAT_R32G32B32_SINT = 8; + int DXGI_FORMAT_R16G16B16A16_TYPELESS = 9; + int DXGI_FORMAT_R16G16B16A16_FLOAT = 10; + int DXGI_FORMAT_R16G16B16A16_UNORM = 11; + int DXGI_FORMAT_R16G16B16A16_UINT = 12; + int DXGI_FORMAT_R16G16B16A16_SNORM = 13; + int DXGI_FORMAT_R16G16B16A16_SINT = 14; + int DXGI_FORMAT_R32G32_TYPELESS = 15; + int DXGI_FORMAT_R32G32_FLOAT = 16; + int DXGI_FORMAT_R32G32_UINT = 17; + int DXGI_FORMAT_R32G32_SINT = 18; + int DXGI_FORMAT_R32G8X24_TYPELESS = 19; + int DXGI_FORMAT_D32_FLOAT_S8X24_UINT = 20; + int DXGI_FORMAT_R32_FLOAT_X8X24_TYPELESS = 21; + int DXGI_FORMAT_X32_TYPELESS_G8X24_UINT = 22; + int DXGI_FORMAT_R10G10B10A2_TYPELESS = 23; + int DXGI_FORMAT_R10G10B10A2_UNORM = 24; + int DXGI_FORMAT_R10G10B10A2_UINT = 25; + int DXGI_FORMAT_R11G11B10_FLOAT = 26; + int DXGI_FORMAT_R8G8B8A8_TYPELESS = 27; + int DXGI_FORMAT_R8G8B8A8_UNORM = 28; + int DXGI_FORMAT_R8G8B8A8_UNORM_SRGB = 29; + int DXGI_FORMAT_R8G8B8A8_UINT = 30; + int DXGI_FORMAT_R8G8B8A8_SNORM = 31; + int DXGI_FORMAT_R8G8B8A8_SINT = 32; + int DXGI_FORMAT_R16G16_TYPELESS = 33; + int DXGI_FORMAT_R16G16_FLOAT = 34; + int DXGI_FORMAT_R16G16_UNORM = 35; + int DXGI_FORMAT_R16G16_UINT = 36; + int DXGI_FORMAT_R16G16_SNORM = 37; + int DXGI_FORMAT_R16G16_SINT = 38; + int DXGI_FORMAT_R32_TYPELESS = 39; + int DXGI_FORMAT_D32_FLOAT = 40; + int DXGI_FORMAT_R32_FLOAT = 41; + int DXGI_FORMAT_R32_UINT = 42; + int DXGI_FORMAT_R32_SINT = 43; + int DXGI_FORMAT_R24G8_TYPELESS = 44; + int DXGI_FORMAT_D24_UNORM_S8_UINT = 45; + int DXGI_FORMAT_R24_UNORM_X8_TYPELESS = 46; + int DXGI_FORMAT_X24_TYPELESS_G8_UINT = 47; + int DXGI_FORMAT_R8G8_TYPELESS = 48; + int DXGI_FORMAT_R8G8_UNORM = 49; + int DXGI_FORMAT_R8G8_UINT = 50; + int DXGI_FORMAT_R8G8_SNORM = 51; + int DXGI_FORMAT_R8G8_SINT = 52; + int DXGI_FORMAT_R16_TYPELESS = 53; + int DXGI_FORMAT_R16_FLOAT = 54; + int DXGI_FORMAT_D16_UNORM = 55; + int DXGI_FORMAT_R16_UNORM = 56; + int DXGI_FORMAT_R16_UINT = 57; + int DXGI_FORMAT_R16_SNORM = 58; + int DXGI_FORMAT_R16_SINT = 59; + int DXGI_FORMAT_R8_TYPELESS = 60; + int DXGI_FORMAT_R8_UNORM = 61; + int DXGI_FORMAT_R8_UINT = 62; + int DXGI_FORMAT_R8_SNORM = 63; + int DXGI_FORMAT_R8_SINT = 64; + int DXGI_FORMAT_A8_UNORM = 65; + int DXGI_FORMAT_R1_UNORM = 66; + int DXGI_FORMAT_R9G9B9E5_SHAREDEXP = 67; + int DXGI_FORMAT_R8G8_B8G8_UNORM = 68; + int DXGI_FORMAT_G8R8_G8B8_UNORM = 69; + int DXGI_FORMAT_BC1_TYPELESS = 70; + int DXGI_FORMAT_BC1_UNORM = 71; + int DXGI_FORMAT_BC1_UNORM_SRGB = 72; + int DXGI_FORMAT_BC2_TYPELESS = 73; + int DXGI_FORMAT_BC2_UNORM = 74; + int DXGI_FORMAT_BC2_UNORM_SRGB = 75; + int DXGI_FORMAT_BC3_TYPELESS = 76; + int DXGI_FORMAT_BC3_UNORM = 77; + int DXGI_FORMAT_BC3_UNORM_SRGB = 78; + int DXGI_FORMAT_BC4_TYPELESS = 79; + int DXGI_FORMAT_BC4_UNORM = 80; + int DXGI_FORMAT_BC4_SNORM = 81; + int DXGI_FORMAT_BC5_TYPELESS = 82; + int DXGI_FORMAT_BC5_UNORM = 83; + int DXGI_FORMAT_BC5_SNORM = 84; + int DXGI_FORMAT_B5G6R5_UNORM = 85; + int DXGI_FORMAT_B5G5R5A1_UNORM = 86; + int DXGI_FORMAT_B8G8R8A8_UNORM = 87; + int DXGI_FORMAT_B8G8R8X8_UNORM = 88; + int DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM = 89; + int DXGI_FORMAT_B8G8R8A8_TYPELESS = 90; + int DXGI_FORMAT_B8G8R8A8_UNORM_SRGB = 91; + int DXGI_FORMAT_B8G8R8X8_TYPELESS = 92; + int DXGI_FORMAT_B8G8R8X8_UNORM_SRGB = 93; + int DXGI_FORMAT_BC6H_TYPELESS = 94; + int DXGI_FORMAT_BC6H_UF16 = 95; + int DXGI_FORMAT_BC6H_SF16 = 96; + int DXGI_FORMAT_BC7_TYPELESS = 97; + int DXGI_FORMAT_BC7_UNORM = 98; + int DXGI_FORMAT_BC7_UNORM_SRGB = 99; + int DXGI_FORMAT_AYUV = 100; + int DXGI_FORMAT_Y410 = 101; + int DXGI_FORMAT_Y416 = 102; + int DXGI_FORMAT_NV12 = 103; + int DXGI_FORMAT_P010 = 104; + int DXGI_FORMAT_P016 = 105; + int DXGI_FORMAT_420_OPAQUE = 106; + int DXGI_FORMAT_YUY2 = 107; + int DXGI_FORMAT_Y210 = 108; + int DXGI_FORMAT_Y216 = 109; + int DXGI_FORMAT_NV11 = 110; + int DXGI_FORMAT_AI44 = 111; + int DXGI_FORMAT_IA44 = 112; + int DXGI_FORMAT_P8 = 113; + int DXGI_FORMAT_A8P8 = 114; + int DXGI_FORMAT_B4G4R4A4_UNORM = 115; + int DXGI_FORMAT_P208 = 130; + int DXGI_FORMAT_V208 = 131; + int DXGI_FORMAT_V408 = 132; + int DXGI_FORMAT_SAMPLER_FEEDBACK_MIN_MIP_OPAQUE = 189; + int DXGI_FORMAT_SAMPLER_FEEDBACK_MIP_REGION_USED_OPAQUE = 190; + int DXGI_FORMAT_FORCE_UINT = 0xffffffff; +} diff --git a/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DXT10Header.java b/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DXT10Header.java new file mode 100644 index 00000000..e052df81 --- /dev/null +++ b/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DXT10Header.java @@ -0,0 +1,45 @@ +package com.twelvemonkeys.imageio.plugins.dds; + +import javax.imageio.stream.ImageInputStream; +import java.io.IOException; + +/** + * @see DDS_HEADER_DXT10 structure + */ +final class DXT10Header { + final int dxgiFormat; + final int resourceDimension; + final int miscFlag; + final int arraySize; + final int miscFlags2; + + private final DDSType type; + + private DXT10Header(int dxgiFormat, int resourceDimension, int miscFlag, int arraySize, int miscFlags2) { + type = DDSType.fromDXGIFormat(dxgiFormat); // Validates dxgiFormat + if (resourceDimension != DDS.D3D10_RESOURCE_DIMENSION_TEXTURE2D) { + throw new IllegalArgumentException(String.format("Resource dimension %d is not supported, expected: %d", + resourceDimension, DDS.D3D10_RESOURCE_DIMENSION_TEXTURE2D)); + } + + this.dxgiFormat = dxgiFormat; + this.resourceDimension = resourceDimension; + this.miscFlag = miscFlag; + this.arraySize = arraySize; + this.miscFlags2 = miscFlags2; + } + + static DXT10Header read(ImageInputStream inputStream) throws IOException { + int dxgiFormat = inputStream.readInt(); + int resourceDimension = inputStream.readInt(); + int miscFlag = inputStream.readInt(); + int arraySize = inputStream.readInt(); + int miscFlags2 = inputStream.readInt(); + + return new DXT10Header(dxgiFormat, resourceDimension, miscFlag, arraySize, miscFlags2); + } + + DDSType getType() { + return type; + } +} diff --git a/imageio/imageio-dds/src/test/java/com/twelvemonkeys/imageio/plugins/dds/DDSImageMetadataTest.java b/imageio/imageio-dds/src/test/java/com/twelvemonkeys/imageio/plugins/dds/DDSImageMetadataTest.java new file mode 100644 index 00000000..41840da4 --- /dev/null +++ b/imageio/imageio-dds/src/test/java/com/twelvemonkeys/imageio/plugins/dds/DDSImageMetadataTest.java @@ -0,0 +1,101 @@ +package com.twelvemonkeys.imageio.plugins.dds; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.awt.image.BufferedImage; + +import javax.imageio.ImageTypeSpecifier; +import javax.imageio.metadata.IIOMetadataFormatImpl; +import javax.imageio.metadata.IIOMetadataNode; + +import org.junit.jupiter.api.Test; +import org.w3c.dom.NodeList; + +class DDSImageMetadataTest { + @Test + void standardMetadataDXT1() { + DDSImageMetadata metadata = createDDSImageMetadata(BufferedImage.TYPE_INT_ARGB, DDSType.DXT1); + IIOMetadataNode tree = (IIOMetadataNode) metadata.getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName); + + NodeList compressionTypeNames = tree.getElementsByTagName("CompressionTypeName"); + assertEquals(1, compressionTypeNames.getLength()); + IIOMetadataNode compressionTypeName = (IIOMetadataNode) compressionTypeNames.item(0); + assertEquals("DXT1", compressionTypeName.getAttribute("value")); + + NodeList losslesses = tree.getElementsByTagName("Lossless"); + assertEquals(1, losslesses.getLength()); + IIOMetadataNode lossless = (IIOMetadataNode) losslesses.item(0); + assertEquals("FALSE", lossless.getAttribute("value")); + + NodeList bitsPerSamples = tree.getElementsByTagName("BitsPerSample"); + assertEquals(1, bitsPerSamples.getLength()); + IIOMetadataNode bitsPerSample = (IIOMetadataNode) bitsPerSamples.item(0); + assertEquals("8 8 8 8", bitsPerSample.getAttribute("value")); + + NodeList alphas = tree.getElementsByTagName("Alpha"); + assertEquals(1, alphas.getLength()); + IIOMetadataNode alpha = (IIOMetadataNode) alphas.item(0); + assertEquals("nonpremultiplied", alpha.getAttribute("value")); + } + + @Test + void standardMetadataA8R8G8B8() { + DDSImageMetadata metadata = createDDSImageMetadata(BufferedImage.TYPE_INT_ARGB, DDSType.A8R8G8B8); + IIOMetadataNode tree = (IIOMetadataNode) metadata.getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName); + + NodeList compressions = tree.getElementsByTagName("Compression"); + assertEquals(0, compressions.getLength()); + + NodeList bitsPerSamples = tree.getElementsByTagName("BitsPerSample"); + assertEquals(1, bitsPerSamples.getLength()); + IIOMetadataNode bitsPerSample = (IIOMetadataNode) bitsPerSamples.item(0); + assertEquals("8 8 8 8", bitsPerSample.getAttribute("value")); + + NodeList alphas = tree.getElementsByTagName("Alpha"); + assertEquals(1, alphas.getLength()); + IIOMetadataNode alpha = (IIOMetadataNode) alphas.item(0); + assertEquals("nonpremultiplied", alpha.getAttribute("value")); + } + + @Test + void standardMetadataX8R8G8B8() { + DDSImageMetadata metadata = createDDSImageMetadata(BufferedImage.TYPE_INT_RGB, DDSType.X8R8G8B8); + IIOMetadataNode tree = (IIOMetadataNode) metadata.getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName); + + NodeList compressions = tree.getElementsByTagName("Compression"); + assertEquals(0, compressions.getLength()); + + NodeList bitsPerSamples = tree.getElementsByTagName("BitsPerSample"); + assertEquals(1, bitsPerSamples.getLength()); + IIOMetadataNode bitsPerSample = (IIOMetadataNode) bitsPerSamples.item(0); + assertEquals("8 8 8 0", bitsPerSample.getAttribute("value")); // Or just 8 8 8? + + NodeList alphas = tree.getElementsByTagName("Alpha"); + assertEquals(1, alphas.getLength()); + IIOMetadataNode alpha = (IIOMetadataNode) alphas.item(0); + assertEquals("none", alpha.getAttribute("value")); + } + + @Test + void standardMetadataX1R5G5B5() { + DDSImageMetadata metadata = createDDSImageMetadata(BufferedImage.TYPE_INT_RGB, DDSType.X1R5G5B5); + IIOMetadataNode tree = (IIOMetadataNode) metadata.getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName); + + NodeList compressions = tree.getElementsByTagName("Compression"); + assertEquals(0, compressions.getLength()); + + NodeList bitsPerSamples = tree.getElementsByTagName("BitsPerSample"); + assertEquals(1, bitsPerSamples.getLength()); + IIOMetadataNode bitsPerSample = (IIOMetadataNode) bitsPerSamples.item(0); + assertEquals("5 5 5 0", bitsPerSample.getAttribute("value")); // Or just 5 5 5? + + NodeList alphas = tree.getElementsByTagName("Alpha"); + assertEquals(1, alphas.getLength()); + IIOMetadataNode alpha = (IIOMetadataNode) alphas.item(0); + assertEquals("none", alpha.getAttribute("value")); + } + + private static DDSImageMetadata createDDSImageMetadata(int bufferedImageType, DDSType ddsType) { + return new DDSImageMetadata(ImageTypeSpecifier.createFromBufferedImageType(bufferedImageType), ddsType); + } +} \ No newline at end of file diff --git a/imageio/imageio-dds/src/test/java/com/twelvemonkeys/imageio/plugins/dds/DDSImageReaderTest.java b/imageio/imageio-dds/src/test/java/com/twelvemonkeys/imageio/plugins/dds/DDSImageReaderTest.java index 5b4c347b..c8662a12 100644 --- a/imageio/imageio-dds/src/test/java/com/twelvemonkeys/imageio/plugins/dds/DDSImageReaderTest.java +++ b/imageio/imageio-dds/src/test/java/com/twelvemonkeys/imageio/plugins/dds/DDSImageReaderTest.java @@ -30,14 +30,27 @@ package com.twelvemonkeys.imageio.plugins.dds; +import static org.junit.jupiter.api.Assertions.assertEquals; + import com.twelvemonkeys.imageio.util.ImageReaderAbstractTest; +import javax.imageio.ImageIO; +import javax.imageio.ImageReader; +import javax.imageio.metadata.IIOMetadata; +import javax.imageio.metadata.IIOMetadataFormatImpl; +import javax.imageio.metadata.IIOMetadataNode; import javax.imageio.spi.ImageReaderSpi; +import javax.imageio.stream.ImageInputStream; + import java.awt.Dimension; +import java.io.IOException; import java.util.Arrays; import java.util.Collections; import java.util.List; +import org.junit.jupiter.api.Test; +import org.w3c.dom.NodeList; + public class DDSImageReaderTest extends ImageReaderAbstractTest { @Override protected ImageReaderSpi createProvider() { @@ -110,4 +123,67 @@ public class DDSImageReaderTest extends ImageReaderAbstractTest protected List getMIMETypes() { return Collections.singletonList("image/vnd-ms.dds"); } + + @Test + void metadataDXT5() throws IOException { + ImageReader reader = createReader(); + + try (ImageInputStream inputStream = ImageIO.createImageInputStream(getClassLoaderResource("/dds/dds_DXT5.dds"))) { + reader.setInput(inputStream); + + IIOMetadata metadata = reader.getImageMetadata(0); + IIOMetadataNode tree = (IIOMetadataNode) metadata.getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName); + + NodeList compressionTypeNames = tree.getElementsByTagName("CompressionTypeName"); + assertEquals(1, compressionTypeNames.getLength()); + IIOMetadataNode compressionTypeName = (IIOMetadataNode) compressionTypeNames.item(0); + assertEquals("DXT5", compressionTypeName.getAttribute("value")); + + NodeList losslesses = tree.getElementsByTagName("Lossless"); + assertEquals(1, losslesses.getLength()); + IIOMetadataNode lossless = (IIOMetadataNode) losslesses.item(0); + assertEquals("FALSE", lossless.getAttribute("value")); + + NodeList bitsPerSamples = tree.getElementsByTagName("BitsPerSample"); + assertEquals(1, bitsPerSamples.getLength()); + IIOMetadataNode bitsPerSample = (IIOMetadataNode) bitsPerSamples.item(0); + assertEquals("8 8 8 8", bitsPerSample.getAttribute("value")); + + NodeList alphas = tree.getElementsByTagName("Alpha"); + assertEquals(1, alphas.getLength()); + IIOMetadataNode alpha = (IIOMetadataNode) alphas.item(0); + assertEquals("nonpremultiplied", alpha.getAttribute("value")); + } + finally { + reader.dispose(); + } + } + + @Test + void metadataRGB565() throws IOException { + ImageReader reader = createReader(); + + try (ImageInputStream inputStream = ImageIO.createImageInputStream(getClassLoaderResource("/dds/dds_R5G6B5.dds"))) { + reader.setInput(inputStream); + + IIOMetadata metadata = reader.getImageMetadata(0); + IIOMetadataNode tree = (IIOMetadataNode) metadata.getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName); + + NodeList compressions = tree.getElementsByTagName("Compression"); + assertEquals(0, compressions.getLength()); + + NodeList bitsPerSamples = tree.getElementsByTagName("BitsPerSample"); + assertEquals(1, bitsPerSamples.getLength()); + IIOMetadataNode bitsPerSample = (IIOMetadataNode) bitsPerSamples.item(0); + assertEquals("5 6 5 0", bitsPerSample.getAttribute("value")); // or "5 6 5" + + NodeList alphas = tree.getElementsByTagName("Alpha"); + assertEquals(1, alphas.getLength()); + IIOMetadataNode alpha = (IIOMetadataNode) alphas.item(0); + assertEquals("none", alpha.getAttribute("value")); + } + finally { + reader.dispose(); + } + } } diff --git a/imageio/imageio-dds/src/test/java/com/twelvemonkeys/imageio/plugins/dds/DDSImageWriteParamTest.java b/imageio/imageio-dds/src/test/java/com/twelvemonkeys/imageio/plugins/dds/DDSImageWriteParamTest.java new file mode 100644 index 00000000..beeb2a59 --- /dev/null +++ b/imageio/imageio-dds/src/test/java/com/twelvemonkeys/imageio/plugins/dds/DDSImageWriteParamTest.java @@ -0,0 +1,55 @@ +package com.twelvemonkeys.imageio.plugins.dds; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.Arrays; + +import javax.imageio.ImageWriteParam; + +import org.junit.jupiter.api.Test; + +class DDSImageWriteParamTest { + @Test + void defaultParam() { + DDSImageWriteParam param = new DDSImageWriteParam(); + assertEquals(DDSImageWriteParam.DEFAULT_TYPE, param.type()); + } + + @Test + void compressionTypes() { + DDSImageWriteParam param = new DDSImageWriteParam(); + + String[] compressionTypes = param.getCompressionTypes(); + DDSType[] values = Arrays.stream(DDSType.values()) + .filter(DDSType::isBlockCompression) + .toArray(DDSType[]::new); + + assertEquals(values.length + 1, compressionTypes.length); + + for (int i = 0; i < values.length; i++) { + DDSType type = values[i]; + assertEquals(type.name(), compressionTypes[i + 1]); + } + + assertEquals("None", compressionTypes[0]); + } + + @Test + void setCompression() { + DDSImageWriteParam param = new DDSImageWriteParam(); + + param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); + + String[] compressionTypes = param.getCompressionTypes(); + for (String compressionType : compressionTypes) { + param.setCompressionType(compressionType); + assertEquals(compressionType, param.getCompressionType()); + + if (!"None".equals(compressionType)) { + DDSType type = DDSType.valueOf(compressionType); + + assertEquals(type, param.type()); + } + } + } +} \ No newline at end of file diff --git a/imageio/imageio-dds/src/test/java/com/twelvemonkeys/imageio/plugins/dds/DDSImageWriterTest.java b/imageio/imageio-dds/src/test/java/com/twelvemonkeys/imageio/plugins/dds/DDSImageWriterTest.java index 2f38e086..4903c07f 100644 --- a/imageio/imageio-dds/src/test/java/com/twelvemonkeys/imageio/plugins/dds/DDSImageWriterTest.java +++ b/imageio/imageio-dds/src/test/java/com/twelvemonkeys/imageio/plugins/dds/DDSImageWriterTest.java @@ -1,13 +1,38 @@ package com.twelvemonkeys.imageio.plugins.dds; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + import com.twelvemonkeys.imageio.util.ImageWriterAbstractTest; +import javax.imageio.IIOException; +import javax.imageio.IIOImage; +import javax.imageio.ImageIO; +import javax.imageio.ImageReader; +import javax.imageio.ImageWriteParam; +import javax.imageio.ImageWriter; +import javax.imageio.event.IIOWriteWarningListener; import javax.imageio.spi.ImageWriterSpi; +import javax.imageio.stream.ImageInputStream; +import javax.imageio.stream.ImageOutputStream; + import java.awt.image.BufferedImage; -import java.util.Collections; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Arrays; import java.util.List; -public class DDSImageWriterTest extends ImageWriterAbstractTest { +import org.junit.jupiter.api.Test; + +class DDSImageWriterTest extends ImageWriterAbstractTest { @Override protected ImageWriterSpi createProvider() { return new DDSImageWriterSpi(); @@ -15,8 +40,147 @@ public class DDSImageWriterTest extends ImageWriterAbstractTest @Override protected List getTestData() { - return Collections.singletonList( - new BufferedImage(256, 256, BufferedImage.TYPE_INT_ARGB_PRE) + return Arrays.asList( + new BufferedImage(256, 256, BufferedImage.TYPE_INT_ARGB), + new BufferedImage(128, 128, BufferedImage.TYPE_INT_ARGB), + new BufferedImage(64, 64, BufferedImage.TYPE_INT_RGB), + new BufferedImage(32, 32, BufferedImage.TYPE_4BYTE_ABGR), + new BufferedImage(16, 16, BufferedImage.TYPE_3BYTE_BGR) ); } + + @Test + void writeRasters() throws IOException { + ImageWriter writer = createWriter(); + + assertTrue(writer.canWriteRasters()); + + // Full tests in super class + } + + @Test + void writeMipmap() throws IOException { + ImageWriter writer = createWriter(); + + try { + assertTrue(writer.canWriteSequence()); + + List testData = getTestData(); + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + int previousSize = 0; + + try (ImageOutputStream stream = ImageIO.createImageOutputStream(buffer)) { + writer.setOutput(stream); + writer.prepareWriteSequence(null); + ImageWriteParam param = writer.getDefaultWriteParam(); + + assertTrue(buffer.size() > previousSize); + previousSize = buffer.size(); + + for (BufferedImage image : testData) { + writer.writeToSequence(new IIOImage(drawSomething(image), null, null), param); + } + + writer.endWriteSequence(); + assertTrue(buffer.size() > previousSize, "No image data written"); + } + catch (IOException e) { + throw new AssertionError(e.getMessage(), e); + } + + // Verify that we can read the file back... + ImageReader reader = ImageIO.getImageReader(writer); + + try (ImageInputStream stream = ImageIO.createImageInputStream(new ByteArrayInputStream(buffer.toByteArray()))) { + stream.seek(0); + reader.setInput(stream); + + assertEquals(testData.size(), reader.getNumImages(false)); + + for (int i = 0; i < testData.size(); i++) { + BufferedImage image = reader.read(i, null); + + assertNotNull(image); + assertEquals(testData.get(i).getWidth(), image.getWidth()); + assertEquals(testData.get(i).getHeight(), image.getHeight()); + } + } + finally { + reader.dispose(); + } + + } + finally { + writer.dispose(); + } + } + + @Test + void writeMipmapDifferentCompression() throws IOException { + ImageWriter writer = createWriter(); + + try { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + + try (ImageOutputStream stream = ImageIO.createImageOutputStream(buffer)) { + IIOWriteWarningListener listener = mock(); + writer.addIIOWriteWarningListener(listener); + writer.setOutput(stream); + + writer.prepareWriteSequence(null); + ImageWriteParam param = writer.getDefaultWriteParam(); + param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); + param.setCompressionType("DXT2"); + + // Write first with DXT2 + List testData = getTestData(); + writer.writeToSequence(new IIOImage(drawSomething(testData.get(0)), null, null), param); + + // Repeat with different type + IIOImage image = new IIOImage(drawSomething(testData.get(1)), null, null); + param.setCompressionType("DXT1"); + + writer.writeToSequence(image, param); + + // Verify warning is issued + verify(listener).warningOccurred(eq(writer), eq(1), anyString()); + verifyNoMoreInteractions(listener); + } + catch (IOException e) { + throw new AssertionError(e.getMessage(), e); + } + } + finally { + writer.dispose(); + } + } + + @Test + void writeMipmapUnexpectedSize() throws IOException { + ImageWriter writer = createWriter(); + + try { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + + try (ImageOutputStream stream = ImageIO.createImageOutputStream(buffer)) { + writer.setOutput(stream); + + writer.prepareWriteSequence(null); + ImageWriteParam param = writer.getDefaultWriteParam(); + BufferedImage testData = getTestData().get(0); + + IIOImage image = new IIOImage(drawSomething(testData), null, null); + writer.writeToSequence(image, param); + + // Repeat with same size... boom. + assertThrows(IIOException.class, () -> writer.writeToSequence(image, param)); + } + catch (IOException e) { + throw new AssertionError(e.getMessage(), e); + } + } + finally { + writer.dispose(); + } + } } diff --git a/imageio/imageio-pnm/src/main/java/com/twelvemonkeys/imageio/plugins/pnm/TupleType.java b/imageio/imageio-pnm/src/main/java/com/twelvemonkeys/imageio/plugins/pnm/TupleType.java index 24c11e23..b8a91302 100755 --- a/imageio/imageio-pnm/src/main/java/com/twelvemonkeys/imageio/plugins/pnm/TupleType.java +++ b/imageio/imageio-pnm/src/main/java/com/twelvemonkeys/imageio/plugins/pnm/TupleType.java @@ -111,6 +111,7 @@ enum TupleType { static TupleType forPAM(Raster raster) { SampleModel sampleModel = raster.getSampleModel(); + switch (sampleModel.getTransferType()) { case DataBuffer.TYPE_BYTE: case DataBuffer.TYPE_USHORT: @@ -145,8 +146,12 @@ enum TupleType { return TupleType.RGB; } else if (bands == 4) { + // Ambiguous, could also be CMYK... return TupleType.RGB_ALPHA; } + else if (bands == 5) { + return TupleType.CMYK_ALPHA; + } // ...else fall through... } @@ -154,7 +159,7 @@ enum TupleType { } static TupleType forPAM(ImageTypeSpecifier type) { - // Support only 1 bit b/w, 8-16 bit gray and 8-16 bit/sample RGB + // Support only 1 bit b/w, 8-16 bit gray, 8-16 bit/sample RGB and 8-16 bit/sample CMYK switch (type.getBufferedImageType()) { // 1 bit b/w or b/w + a case BufferedImage.TYPE_BYTE_BINARY: