Major rework/standardization:

* DDSEncoderType, DX10DXGIFormat merged with DDSType for a single way to describe a DDS format
 * Added constants for DXGI formats
 * DDSImageWriteParam is now mutable and supports standard way of setting compression type
 * DDSImageMetadata now supports more of the format
 Performance:
 * DDSReader now use seek() to jump to correct mipmap instead of reading all bytes
 * DDSImageWriter now uses getTile(0, 0) instead of getData() for better performance
This commit is contained in:
Harald Kuhr
2026-03-10 22:38:58 +01:00
parent 7f2eb517ef
commit 0a717ea0af
24 changed files with 1167 additions and 513 deletions
@@ -23,6 +23,7 @@ import static com.twelvemonkeys.lang.Validate.notNull;
* {@link ImageTypeSpecifier}. * {@link ImageTypeSpecifier}.
* Other values or overrides may be specified using the builder. * Other values or overrides may be specified using the builder.
* *
* @see <a href="https://docs.oracle.com/en/java/javase/11/docs/api/java.desktop/javax/imageio/metadata/doc-files/standard_metadata.html">Standard (Plug-in Neutral) Metadata Format Specification</a>
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a> * @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
*/ */
public class StandardImageMetadataSupport extends AbstractMetadata { public class StandardImageMetadataSupport extends AbstractMetadata {
@@ -79,11 +80,11 @@ public class StandardImageMetadataSupport extends AbstractMetadata {
textEntries = builder.textEntries; textEntries = builder.textEntries;
} }
public static Builder builder(ImageTypeSpecifier type) { protected static Builder builder(ImageTypeSpecifier type) {
return new Builder(type); return new Builder(type);
} }
public static class Builder { protected static class Builder {
private final ImageTypeSpecifier type; private final ImageTypeSpecifier type;
private ColorSpaceType colorSpaceType; private ColorSpaceType colorSpaceType;
private boolean blackIsZero = true; private boolean blackIsZero = true;
@@ -35,6 +35,8 @@ import com.twelvemonkeys.lang.Validate;
import javax.imageio.IIOParam; import javax.imageio.IIOParam;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageWriteParam;
import javax.imageio.spi.IIOServiceProvider; import javax.imageio.spi.IIOServiceProvider;
import javax.imageio.spi.ServiceRegistry; import javax.imageio.spi.ServiceRegistry;
import javax.imageio.stream.ImageInputStream; import javax.imageio.stream.ImageInputStream;
@@ -45,7 +47,9 @@ import java.io.BufferedInputStream;
import java.io.BufferedOutputStream; import java.io.BufferedOutputStream;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.util.Arrays;
import java.util.Iterator; import java.util.Iterator;
import java.util.Objects;
import java.util.SortedSet; import java.util.SortedSet;
import java.util.TreeSet; import java.util.TreeSet;
@@ -359,4 +363,109 @@ public final class IIOUtil {
System.arraycopy(srcRow, srcPos + x, destRow, destPos + x / samplePeriod, pixelStride); System.arraycopy(srcRow, srcPos + x, destRow, destPos + x / samplePeriod, pixelStride);
} }
} }
/**
* Copies all the standard param values from source to destination.
* <p>
* Typical use (in some imaginary {@code FooImageWriter} class):
* <pre>
* ImageWriteParam param = ...
* FooImageWriteparam fooParam = param instanceof FooImageWriteParam
* ? (FooImageWriteParam) param
* : copyStandardParams(param, getDefaultWriteParam());
* </pre>
* </p>
* May also be useful for {@code ImageReader}s that delegate reading to other plugins
* (like a TIFF plugin delegating JPEG format decoding to a {@code JPEGImageReader}).
*
* @param source the source parameter, may be {@code null}
* @param destination the destination parameter
* @return destination
*
* @param <T> the plugin specific subclass of {@code IIOParam}
*
* @throws NullPointerException if destination is {@code null}
*/
public static <T extends IIOParam> T copyStandardParams(IIOParam source, T destination) {
Objects.requireNonNull(destination);
Validate.isTrue(source != destination, "source must be different from destination");
if (source != null) {
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());
// 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) {
ImageReadParam sourceReadParam = (ImageReadParam) source;
ImageReadParam destinationReadParam = (ImageReadParam) destination;
destinationReadParam.setDestination(sourceReadParam.getDestination());
destinationReadParam.setDestinationBands(sourceReadParam.getDestinationBands());
if (destinationReadParam.canSetSourceRenderSize()) {
destinationReadParam.setSourceRenderSize(sourceReadParam.getSourceRenderSize());
}
destinationReadParam.setSourceProgressivePasses(
sourceReadParam.getSourceMinProgressivePass(),
sourceReadParam.getSourceMaxProgressivePass()
);
}
if (source instanceof ImageWriteParam && destination instanceof ImageWriteParam) {
ImageWriteParam sourceWriteParam = (ImageWriteParam) source;
ImageWriteParam destinationWriteParam = (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 (sourceWriteParam.canWriteCompressed() && destinationWriteParam.canWriteCompressed()) {
int compressionMode = sourceWriteParam.getCompressionMode();
destinationWriteParam.setCompressionMode(compressionMode);
if (compressionMode == ImageWriteParam.MODE_EXPLICIT
&& sourceWriteParam.getCompressionType() != null) {
if (Arrays.asList(destinationWriteParam.getCompressionTypes())
.contains(sourceWriteParam.getCompressionType())) {
destinationWriteParam.setCompressionType(sourceWriteParam.getCompressionType());
destinationWriteParam.setCompressionQuality(sourceWriteParam.getCompressionQuality());
}
}
}
if (sourceWriteParam.canWriteProgressive() && destinationWriteParam.canWriteProgressive()) {
destinationWriteParam.setProgressiveMode(sourceWriteParam.getProgressiveMode());
}
if (sourceWriteParam.canWriteTiles() && destinationWriteParam.canWriteTiles()) {
int tilingMode = sourceWriteParam.getTilingMode();
destinationWriteParam.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 =
sourceWriteParam.canOffsetTiles() && destinationWriteParam.canOffsetTiles();
destinationWriteParam.setTiling(
sourceWriteParam.getTileWidth(), sourceWriteParam.getTileHeight(),
canWriteOffsetTiles ? sourceWriteParam.getTileGridXOffset() : 0,
canWriteOffsetTiles ? sourceWriteParam.getTileGridYOffset() : 0
);
}
}
}
}
return destination;
}
} }
@@ -1,8 +1,20 @@
package com.twelvemonkeys.imageio.util; package com.twelvemonkeys.imageio.util;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.function.Executable;
import static org.junit.jupiter.api.Assertions.*; 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 * IIOUtilTest
*/ */
@@ -204,4 +216,220 @@ public class IIOUtilTest {
private int divCeil(int numerator, int denominator) { private int divCeil(int numerator, int denominator) {
return (numerator + denominator - 1) / denominator; return (numerator + denominator - 1) / denominator;
} }
@Test
void copyStandardParamsDestinationNull() {
assertThrows(NullPointerException.class, () -> IIOUtil.copyStandardParams(null, null));
assertThrows(NullPointerException.class, () -> IIOUtil.copyStandardParams(new ImageReadParam(), 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" };
}
}
} }
@@ -0,0 +1,11 @@
package com.twelvemonkeys.imageio.plugins.dds;
enum BlockCompression {
BC1,
BC2,
BC3,
BC4,
BC5,
// BC6H,
// BC7
}
@@ -33,7 +33,7 @@ package com.twelvemonkeys.imageio.plugins.dds;
@SuppressWarnings("unused") @SuppressWarnings("unused")
interface DDS { interface DDS {
int MAGIC = ('D' << 24) + ('D' << 16) + ('S' << 8) + ' '; //Big-Endian int MAGIC = ('D' << 24) + ('D' << 16) + ('S' << 8) + ' '; // Big-Endian
int HEADER_SIZE = 124; int HEADER_SIZE = 124;
// Header Flags // Header Flags
@@ -56,16 +56,6 @@ interface DDS {
//DX10 Resource Dimensions //DX10 Resource Dimensions
int D3D10_RESOURCE_DIMENSION_TEXTURE2D = 3; 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 //dwCaps
int DDSCAPS_COMPLEX = 0x8; int DDSCAPS_COMPLEX = 0x8;
int DDSCAPS_MIPMAP = 0x400000; int DDSCAPS_MIPMAP = 0x400000;
@@ -1,56 +0,0 @@
package com.twelvemonkeys.imageio.plugins.dds;
/**
* Lists a number of supported encoders for block compressors and uncompressed types.
* <a href="https://learn.microsoft.com/en-us/windows/win32/api/dxgiformat/ne-dxgiformat-dxgi_format">DXGI Format List</a>
* <a href="https://learn.microsoft.com/en-us/windows/win32/direct3d10/d3d10-graphics-programming-guide-resources-block-compression#compression-algorithms">Compression Algorithms</a>
* <a href="https://github.com/microsoft/DirectXTK12/wiki/DDSTextureLoader#remarks">An extended Non-DX10 FourCC list</a>
*/
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;
}
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;
}
}
@@ -30,11 +30,23 @@
package com.twelvemonkeys.imageio.plugins.dds; 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.IIOException;
import javax.imageio.stream.ImageInputStream; import javax.imageio.stream.ImageInputStream;
import java.awt.Dimension; import java.awt.Dimension;
import java.io.IOException; import java.io.IOException;
import java.nio.ByteOrder; import java.nio.ByteOrder;
import java.util.Arrays;
final class DDSHeader { final class DDSHeader {
@@ -52,6 +64,8 @@ final class DDSHeader {
private int blueMask; private int blueMask;
private int alphaMask; private int alphaMask;
DXT10Header dxt10Header;
@SuppressWarnings("unused") @SuppressWarnings("unused")
static DDSHeader read(final ImageInputStream imageInput) throws IOException { static DDSHeader read(final ImageInputStream imageInput) throws IOException {
DDSHeader header = new DDSHeader(); DDSHeader header = new DDSHeader();
@@ -113,6 +127,10 @@ final class DDSHeader {
int dwReserved2 = imageInput.readInt(); // [124,127] int dwReserved2 = imageInput.readInt(); // [124,127]
if (header.fourCC == DDSType.DXT10.fourCC()) {
header.dxt10Header = DXT10Header.read(imageInput);
}
return header; return header;
} }
@@ -146,31 +164,95 @@ final class DDSHeader {
return mipMapCount; return mipMapCount;
} }
int getBitCount() { DDSType getType() throws IIOException {
return bitCount; if (dxt10Header != null) {
return dxt10Header.getType();
}
return getRawType();
} }
int getFourCC() { DDSType getRawType() throws IIOException {
return fourCC; 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() { @Override
return pixelFormatFlags; public String toString() {
} return "DDSHeader{" +
"flags=" + flags +
int getRedMask() { ", mipMapCount=" + mipMapCount +
return redMask; ", dimensions=" + Arrays.toString(dimensions) +
} ", pixelFormatFlags=" + pixelFormatFlags +
", fourCC=" + fourCC +
int getGreenMask() { ", bitCount=" + bitCount +
return greenMask; ", redMask=" + redMask +
} ", greenMask=" + greenMask +
", blueMask=" + blueMask +
int getBlueMask() { ", alphaMask=" + alphaMask +
return blueMask; '}';
}
int getAlphaMask() {
return alphaMask;
} }
} }
@@ -31,8 +31,10 @@ 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_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 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 { static void writeImageData(ImageOutputStream imageOutput, RenderedImage renderedImage, BlockCompression compression) throws IOException {
switch (type) { // TODO: compression == null for custom RGB data?
switch (compression) {
case BC1: case BC1:
new BlockCompressor1(false).encode(imageOutput, renderedImage); new BlockCompressor1(false).encode(imageOutput, renderedImage);
break; break;
@@ -49,7 +51,7 @@ class DDSImageDataEncoder {
new BlockCompressor5().encode(imageOutput, renderedImage); new BlockCompressor5().encode(imageOutput, renderedImage);
break; break;
default: default:
throw new IllegalArgumentException("DDS Type is not supported for encoder yet : " + type); throw new IllegalArgumentException("DDS block compression is not supported yet: " + compression);
} }
} }
@@ -423,7 +425,12 @@ class DDSImageDataEncoder {
void encode(ImageOutputStream imageOutput, RenderedImage image) throws IOException { void encode(ImageOutputStream imageOutput, RenderedImage image) throws IOException {
int blocksXCount = (image.getWidth() + 3) / 4; int blocksXCount = (image.getWidth() + 3) / 4;
int blocksYCount = (image.getHeight() + 3) / 4; int blocksYCount = (image.getHeight() + 3) / 4;
Raster raster = image.getData();
if (image.getNumXTiles() != 1 || image.getNumYTiles() != 1) {
throw new IllegalArgumentException("Only single tile images supported");
}
Raster raster = image.getTile(0, 0);
for (int blockY = 0; blockY < blocksYCount; blockY++) { for (int blockY = 0; blockY < blocksYCount; blockY++) {
for (int blockX = 0; blockX < blocksXCount; blockX++) { for (int blockX = 0; blockX < blocksXCount; blockX++) {
raster.getPixels(blockX * 4, blockY * 4, 4, 4, samples); raster.getPixels(blockX * 4, blockY * 4, 4, 4, samples);
@@ -35,24 +35,46 @@ import com.twelvemonkeys.imageio.StandardImageMetadataSupport;
import javax.imageio.ImageTypeSpecifier; import javax.imageio.ImageTypeSpecifier;
final class DDSImageMetadata extends StandardImageMetadataSupport { final class DDSImageMetadata extends StandardImageMetadataSupport {
DDSImageMetadata(ImageTypeSpecifier type, DDSHeader header) {
super(builder(type) DDSImageMetadata(ImageTypeSpecifier specifier, DDSType type) {
.withCompressionTypeName(compressionName(header)) super(builder(specifier)
.withFormatVersion("1.0") .withCompressionTypeName(compressionName(type))
.withBitsPerSample(bitsPerSample(type))
.withFormatVersion("1.0")
); );
} }
private static String compressionName(DDSHeader header) { private static String compressionName(DDSType type) {
// If the fourCC is valid, compression is one of the DXTn versions, otherwise None if (type != null && type.isFourCC()) {
int flags = header.getPixelFormatFlags();
if ((flags & DDS.PIXEL_FORMAT_FLAG_FOURCC) != 0) {
// DXTn
DDSType type = DDSType.valueOf(header.getFourCC());
return type.name(); return type.name();
} }
return "None"; 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;
}
} }
@@ -48,6 +48,12 @@ import java.util.Iterator;
import static com.twelvemonkeys.imageio.util.IIOUtil.subsampleRow; import static com.twelvemonkeys.imageio.util.IIOUtil.subsampleRow;
/**
* ImageReader implementation for Microsoft DirectDraw Surface (DDS) format.
*
* @author Paul Allen
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
*/
public final class DDSImageReader extends ImageReaderBase { public final class DDSImageReader extends ImageReaderBase {
private DDSHeader header; private DDSHeader header;
@@ -90,7 +96,11 @@ public final class DDSImageReader extends ImageReaderBase {
checkBounds(imageIndex); checkBounds(imageIndex);
readHeader(); 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);
}
return ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_INT_ARGB); return ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_INT_ARGB);
} }
@@ -146,14 +156,13 @@ public final class DDSImageReader extends ImageReaderBase {
public IIOMetadata getImageMetadata(int imageIndex) throws IOException { public IIOMetadata getImageMetadata(int imageIndex) throws IOException {
ImageTypeSpecifier imageType = getRawImageType(imageIndex); ImageTypeSpecifier imageType = getRawImageType(imageIndex);
return new DDSImageMetadata(imageType, header); return new DDSImageMetadata(imageType, header.getType());
} }
private void readHeader() throws IOException { private void readHeader() throws IOException {
if (header == null) { if (header == null) {
imageInput.setByteOrder(ByteOrder.LITTLE_ENDIAN); imageInput.setByteOrder(ByteOrder.LITTLE_ENDIAN);
header = DDSHeader.read(imageInput); header = DDSHeader.read(imageInput);
imageInput.flushBefore(imageInput.getStreamPosition()); imageInput.flushBefore(imageInput.getStreamPosition());
} }
@@ -55,7 +55,8 @@ public final class DDSImageReaderSpi extends ImageReaderSpiBase {
try { try {
return stream.readInt() == DDS.MAGIC; return stream.readInt() == DDS.MAGIC;
} finally { }
finally {
stream.reset(); stream.reset();
} }
} }
@@ -67,6 +68,6 @@ public final class DDSImageReaderSpi extends ImageReaderSpiBase {
@Override @Override
public String getDescription(Locale locale) { public String getDescription(Locale locale) {
return "Direct DrawSurface (DDS) Image Reader"; return "DirectDraw Surface (DDS) Image Reader";
} }
} }
@@ -0,0 +1,99 @@
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<String> 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 int optionalBitFlags;
private boolean writeDXT10;
DDSImageWriteParam() {
canWriteCompressed = true;
compressionTypes = COMPRESSION_TYPES;
compressionType = DEFAULT_TYPE.name();
setLinearSize();
}
// TODO: Set this always for compressed images?
public void setLinearSize() {
optionalBitFlags |= DDS.FLAG_LINEARSIZE;
}
public void clearLinearSize() {
optionalBitFlags &= ~DDS.FLAG_LINEARSIZE;
}
// TODO: Set this always for uncompressed images?
public void setPitch() {
optionalBitFlags |= DDS.FLAG_PITCH;
}
public void clearPitch() {
optionalBitFlags &= ~DDS.FLAG_PITCH;
}
// TODO: Other flags?
public int optionalBitFlags() {
return optionalBitFlags;
}
public void setWriteDX10() {
writeDXT10 = true;
}
public void clearWriteDX10() {
writeDXT10 = false;
}
public boolean isWriteDXT10() {
return writeDXT10;
}
BlockCompression compression() {
DDSType type = type();
if (type != null) {
return type.compression;
}
return null;
}
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;
}
}
@@ -1,6 +1,7 @@
package com.twelvemonkeys.imageio.plugins.dds; package com.twelvemonkeys.imageio.plugins.dds;
import com.twelvemonkeys.imageio.ImageWriterBase; import com.twelvemonkeys.imageio.ImageWriterBase;
import com.twelvemonkeys.imageio.util.IIOUtil;
import javax.imageio.IIOImage; import javax.imageio.IIOImage;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
@@ -9,6 +10,7 @@ import javax.imageio.ImageWriteParam;
import javax.imageio.metadata.IIOMetadata; import javax.imageio.metadata.IIOMetadata;
import javax.imageio.spi.ImageWriterSpi; import javax.imageio.spi.ImageWriterSpi;
import javax.imageio.stream.MemoryCacheImageOutputStream; import javax.imageio.stream.MemoryCacheImageOutputStream;
import java.awt.image.Raster; import java.awt.image.Raster;
import java.awt.image.RenderedImage; import java.awt.image.RenderedImage;
import java.io.File; import java.io.File;
@@ -18,8 +20,10 @@ import java.nio.file.Files;
import java.nio.file.Paths; import java.nio.file.Paths;
/** /**
* A designated class to begin writing DDS file with headers, * ImageWriter implementation for Microsoft DirectDraw Surface (DDS) format.
* {@link DDSImageDataEncoder} will handle image data encoding process *
* @author KhanTypo
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
*/ */
class DDSImageWriter extends ImageWriterBase { class DDSImageWriter extends ImageWriterBase {
protected DDSImageWriter(ImageWriterSpi provider) { protected DDSImageWriter(ImageWriterSpi provider) {
@@ -27,31 +31,34 @@ class DDSImageWriter extends ImageWriterBase {
} }
@Override @Override
public DDSImageWriterParam getDefaultWriteParam() { public DDSImageWriteParam getDefaultWriteParam() {
return DDSImageWriterParam.builder().formatBC5().build(); return new DDSImageWriteParam();
} }
@Override @Override
public void write(IIOMetadata streamMetadata, IIOImage image, ImageWriteParam param) throws IOException { public void write(IIOMetadata streamMetadata, IIOImage image, ImageWriteParam param) throws IOException {
assertOutput(); assertOutput();
RenderedImage renderedImage = image.getRenderedImage();
RenderedImage renderedImage = image.getRenderedImage(); // TODO: Support raster?
ensureTextureSize(renderedImage); ensureTextureSize(renderedImage);
ensureImageChannels(renderedImage); ensureImageChannels(renderedImage);
// TODO: Need to copy params from 'param' here in case of non-DDS param... DDSImageWriteParam ddsParam = param instanceof DDSImageWriteParam
DDSImageWriterParam ddsParam = param instanceof DDSImageWriterParam ? ((DDSImageWriterParam) param) : getDefaultWriteParam(); ? ((DDSImageWriteParam) param)
: IIOUtil.copyStandardParams(param, getDefaultWriteParam());
processImageStarted(0); processImageStarted(0);
imageOutput.setByteOrder(ByteOrder.BIG_ENDIAN); imageOutput.setByteOrder(ByteOrder.BIG_ENDIAN);
imageOutput.writeInt(DDS.MAGIC); imageOutput.writeInt(DDS.MAGIC);
imageOutput.setByteOrder(ByteOrder.LITTLE_ENDIAN); imageOutput.setByteOrder(ByteOrder.LITTLE_ENDIAN);
writeHeader(image, ddsParam); writeHeader(image, ddsParam.type(), ddsParam.isWriteDXT10(), ddsParam.optionalBitFlags());
writeDXT10Header(ddsParam); if (ddsParam.isWriteDXT10()) {
writeDXT10Header(ddsParam.getDxgiFormat());
}
//image data encoding
processImageProgress(0f); processImageProgress(0f);
DDSImageDataEncoder.writeImageData(imageOutput, renderedImage, ddsParam.getEncoderType()); DDSImageDataEncoder.writeImageData(imageOutput, renderedImage, ddsParam.compression());
processImageProgress(100f); processImageProgress(100f);
imageOutput.flush(); imageOutput.flush();
@@ -62,13 +69,18 @@ class DDSImageWriter extends ImageWriterBase {
* Checking if the image has 3 channels (RGB) or 4 channels (RGBA) and if image has 8 bits/channel. * Checking if the image has 3 channels (RGB) or 4 channels (RGBA) and if image has 8 bits/channel.
*/ */
private void ensureImageChannels(RenderedImage renderedImage) { private void ensureImageChannels(RenderedImage renderedImage) {
Raster data = renderedImage.getData(); Raster data = renderedImage.getTile(0, 0);
int numBands = data.getNumBands(); int numBands = data.getNumBands();
if (numBands < 3) if (numBands < 3) {
throw new IllegalStateException("Only image with 3 channels (RGB) or 4 channels (RGBA) is supported, got " + numBands + " channels"); throw new IllegalStateException(
"Only image with 3 channels (RGB) or 4 channels (RGBA) is supported, got " + numBands + " channels");
}
int sampleSize = data.getSampleModel().getSampleSize(0); int sampleSize = data.getSampleModel().getSampleSize(0);
if (sampleSize != 8) if (sampleSize != 8) {
throw new IllegalStateException("Only image with 8 bits/channel is supported, got " + sampleSize); throw new IllegalStateException("Only image with 8 bits/channel is supported, got " + sampleSize);
}
} }
/** /**
@@ -78,20 +90,23 @@ class DDSImageWriter extends ImageWriterBase {
private void ensureTextureSize(RenderedImage renderedImage) { private void ensureTextureSize(RenderedImage renderedImage) {
int w = renderedImage.getWidth(); int w = renderedImage.getWidth();
int h = renderedImage.getHeight(); int h = renderedImage.getHeight();
if (w % 4 != 0 || h % 4 != 0)
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)); 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 writeHeader(IIOImage image, DDSType type, boolean writeDXT10, int optionalBitFlags) throws IOException {
private void writeHeader(IIOImage image, DDSImageWriterParam param) throws IOException {
imageOutput.writeInt(DDS.HEADER_SIZE); imageOutput.writeInt(DDS.HEADER_SIZE);
imageOutput.writeInt(DDS.FLAG_CAPS | DDS.FLAG_HEIGHT | DDS.FLAG_WIDTH | DDS.FLAG_PIXELFORMAT | param.getOptionalBitFlags()); imageOutput.writeInt(DDS.FLAG_CAPS | DDS.FLAG_HEIGHT | DDS.FLAG_WIDTH | DDS.FLAG_PIXELFORMAT | optionalBitFlags);
RenderedImage renderedImage = image.getRenderedImage(); RenderedImage renderedImage = image.getRenderedImage();
int height = renderedImage.getHeight(); int height = renderedImage.getHeight();
imageOutput.writeInt(height);
int width = renderedImage.getWidth(); int width = renderedImage.getWidth();
imageOutput.writeInt(height);
imageOutput.writeInt(width); imageOutput.writeInt(width);
writePitchOrLinearSize(height, width, param); writePitchOrLinearSize(height, width, type);
//dwDepth //dwDepth
imageOutput.writeInt(0); imageOutput.writeInt(0);
//dwMipmapCount //dwMipmapCount
@@ -99,7 +114,7 @@ class DDSImageWriter extends ImageWriterBase {
//reserved //reserved
imageOutput.write(new byte[44]); imageOutput.write(new byte[44]);
//pixFmt //pixFmt
writePixelFormat(param); writePixelFormat(type, writeDXT10);
//dwCaps, right now we keep it simple by only using DDSCAP_TEXTURE as it is required. //dwCaps, right now we keep it simple by only using DDSCAP_TEXTURE as it is required.
imageOutput.writeInt(DDS.DDSCAPS_TEXTURE); imageOutput.writeInt(DDS.DDSCAPS_TEXTURE);
//dwCaps2, unused for now as we are not working with cube maps //dwCaps2, unused for now as we are not working with cube maps
@@ -110,86 +125,95 @@ class DDSImageWriter extends ImageWriterBase {
} }
//https://learn.microsoft.com/en-us/windows/win32/direct3ddds/dds-pixelformat //https://learn.microsoft.com/en-us/windows/win32/direct3ddds/dds-pixelformat
private void writePixelFormat(DDSImageWriterParam param) throws IOException { private void writePixelFormat(DDSType type, boolean writeDXT10) throws IOException {
imageOutput.writeInt(DDS.DDSPF_SIZE); imageOutput.writeInt(DDS.DDSPF_SIZE);
writePixelFormatFlags(param); writePixelFormatFlags(type, writeDXT10);
writeFourCC(param); writeFourCC(type, writeDXT10);
writeRGBAData(param); writeRGBAData(type, writeDXT10);
} }
private void writeDXT10Header(DDSImageWriterParam param) throws IOException { private void writeDXT10Header(int dxgiFormat) throws IOException {
if (param.isUsingDxt10()) { //dxgiFormat
//dxgiFormat imageOutput.writeInt(dxgiFormat);
imageOutput.writeInt(param.getDxgiFormat()); //resourceDimension
//resourceDimension imageOutput.writeInt(DDS.D3D10_RESOURCE_DIMENSION_TEXTURE2D);
imageOutput.writeInt(DDS.D3D10_RESOURCE_DIMENSION_TEXTURE2D); //miscFlag
//miscFlag imageOutput.writeInt(0);
imageOutput.writeInt(0); //arraySize
//arraySize imageOutput.writeInt(1);
imageOutput.writeInt(1); //miscFlag2
//miscFlag2 imageOutput.writeInt(0);
imageOutput.writeInt(0);
}
} }
private void writeRGBAData(DDSImageWriterParam param) throws IOException { private void writeRGBAData(DDSType type, boolean writeDXT10) throws IOException {
if (!param.isUsingDxt10() && !param.getEncoderType().isFourCC()) { if (!writeDXT10 && !type.isFourCC()) {
//dwRGBBitCount //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 //dwRBitMask
imageOutput.writeInt(mask[0]); imageOutput.writeInt(mask[0]);
//dwGBitMask //dwGBitMask
imageOutput.writeInt(mask[1]); imageOutput.writeInt(mask[1]);
//dwBitMask //dwBBitMask
imageOutput.writeInt(mask[2]); imageOutput.writeInt(mask[2]);
//dwABitMask //dwABitMask
imageOutput.writeInt(mask[3]); imageOutput.writeInt(mask[3]);
} else { }
else {
//write 5 zero integers as fourCC is used //write 5 zero integers as fourCC is used
imageOutput.write(new byte[20]); imageOutput.write(new byte[20]);
} }
} }
private void writeFourCC(DDSImageWriterParam param) throws IOException { private void writeFourCC(DDSType type, boolean writeDXT10) throws IOException {
if (param.isUsingDxt10()) { if (writeDXT10) {
imageOutput.writeInt(DDSType.DXT10.value()); imageOutput.writeInt(DDSType.DXT10.fourCC());
} else if (param.getEncoderType().isFourCC()) }
imageOutput.writeInt(param.getEncoderType().getFourCC()); else if (type.isFourCC()) {
imageOutput.writeInt(type.fourCC());
} }
else {
private void writePixelFormatFlags(DDSImageWriterParam param) throws IOException { // No fourCC, custom format...
if (param.isUsingDxt10() || param.getEncoderType().isFourCC()) { imageOutput.writeInt(0);
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 writePitchOrLinearSize(int height, int width, DDSImageWriterParam param) throws IOException { private void writePixelFormatFlags(DDSType type, boolean writeDXT10) throws IOException {
DDSEncoderType type = param.getEncoderType(); if (writeDXT10 || type.isFourCC()) {
int bitsOrBlockSize = type.getBitsOrBlockSize(); imageOutput.writeInt(DDS.PIXEL_FORMAT_FLAG_FOURCC);
if (type.isBlockCompression()) {
imageOutput.writeInt(((width + 3) / 4) * ((height + 3) / 4) * bitsOrBlockSize);
} else { } else {
imageOutput.writeInt(width * bitsOrBlockSize); 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) * type.blockSize());
} else {
imageOutput.writeInt(width * type.blockSize());
} }
} }
@Override @Override
public IIOMetadata getDefaultImageMetadata(ImageTypeSpecifier imageType, ImageWriteParam param) { 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 @Override
public IIOMetadata convertImageMetadata(IIOMetadata inData, ImageTypeSpecifier imageType, ImageWriteParam param) { 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 { 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")))); ImageIO.write(ImageIO.read(new File(args[0])), "dds", new MemoryCacheImageOutputStream(Files.newOutputStream(Paths.get("output.dds"))));
} }
} }
@@ -1,146 +0,0 @@
package com.twelvemonkeys.imageio.plugins.dds;
import javax.imageio.ImageWriteParam;
import java.util.Objects;
public final class DDSImageWriterParam extends ImageWriteParam {
// TODO: Rewrite to use more standard "compressionType":
// See metadata format for how to create compression name based on fourCC, we probably need to change that as well (?)
// At least they need to agree on what the compression names are... BC1, BC2, etc? DXT1, DXT2, etc?
// Extra bit flags etc, may be set using custom methods
private final int optionalBitFlags;
private final DDSEncoderType encoderType;
private final boolean enableDxt10;
DDSImageWriterParam(int optionalBitFlags, DDSEncoderType encoderType, boolean isUsingDxt10) {
super();
canWriteCompressed = true; // always compressed
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() {
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 DDSImageWriterParam build() {
Objects.requireNonNull(encoderType, "no DDS format specified.");
return new DDSImageWriterParam(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;
}
}
}
}
@@ -23,6 +23,6 @@ public final class DDSImageWriterSpi extends ImageWriterSpiBase {
@Override @Override
public String getDescription(Locale locale) { public String getDescription(Locale locale) {
return "Direct Draw Surface (DDS) Image Writer"; return "DirectDraw Surface (DDS) Image Writer";
} }
} }
@@ -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 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 final DDSHeader header;
private DX10Header dxt10Header;
DDSReader(DDSHeader header) { DDSReader(DDSHeader header) {
this.header = header; this.header = header;
@@ -82,18 +81,20 @@ final class DDSReader {
int[] read(ImageInputStream imageInput, int imageIndex) throws IOException { int[] read(ImageInputStream imageInput, int imageIndex) throws IOException {
// type // type
DDSType type = getType(); DDSType type = header.getType();
if (type == DDSType.DXT10) {
dxt10Header = DX10Header.read(imageInput);
type = dxt10Header.getDDSType();
}
// offset buffer to index mipmap image // offset buffer to index mipmap image
byte[] buffer = null; byte[] buffer = null;
for (int i = 0; i <= imageIndex; i++) { for (int i = 0; i <= imageIndex; i++) {
int len = getLength(type, i); int len = getBufferLength(type, i);
buffer = new byte[len];
imageInput.readFully(buffer); if (i == imageIndex) {
buffer = new byte[len];
imageInput.readFully(buffer);
}
else {
imageInput.seek(imageInput.getStreamPosition() + len);
}
} }
int width = header.getWidth(imageIndex); int width = header.getWidth(imageIndex);
@@ -135,82 +136,17 @@ final class DDSReader {
} }
} }
private DDSType getType() throws IIOException { private int getBufferLength(DDSType type, int imageIndex) 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 {
int width = header.getWidth(imageIndex); int width = header.getWidth(imageIndex);
int height = header.getHeight(imageIndex); int height = header.getHeight(imageIndex);
switch (type) { switch (type) {
case DXT1: case DXT1:
return 8 * ((width + 3) / 4) * ((height + 3) / 4);
case DXT2: case DXT2:
case DXT3: case DXT3:
case DXT4: case DXT4:
case DXT5: case DXT5:
return 16 * ((width + 3) / 4) * ((height + 3) / 4); return type.blockSize() * ((width + 3) / 4) * ((height + 3) / 4);
case A1R5G5B5: case A1R5G5B5:
case X1R5G5B5: case X1R5G5B5:
case A4R4G4B4: case A4R4G4B4:
@@ -221,9 +157,9 @@ final class DDSReader {
case X8B8G8R8: case X8B8G8R8:
case A8R8G8B8: case A8R8G8B8:
case X8R8G8B8: case X8R8G8B8:
return (type.value() & 0xFF) * width * height; return type.blockSize() * width * height;
default: default:
throw new IIOException("Unknown type: " + Integer.toHexString(type.value())); throw new IIOException("Unknown type: " + type);
} }
} }
@@ -30,41 +30,155 @@
package com.twelvemonkeys.imageio.plugins.dds; package com.twelvemonkeys.imageio.plugins.dds;
import static com.twelvemonkeys.imageio.plugins.dds.BlockCompression.*;
import static com.twelvemonkeys.imageio.plugins.dds.DDSReader.*;
/**
* <a href="https://learn.microsoft.com/en-us/windows/win32/direct3d10/d3d10-graphics-programming-guide-resources-block-compression#compression-algorithms">Compression Algorithms</a>
* <a href="https://github.com/microsoft/DirectXTK12/wiki/DDSTextureLoader#remarks">An extended Non-DX10 FourCC list</a>
*/
enum DDSType { enum DDSType {
DXT1(0x31545844), // Compressed types
DXT2(0x32545844), DXT1('D' + ('X' << 8) + ('T' << 16) + ('1' << 24), 8, DXGI.DXGI_FORMAT_BC1_UNORM, BC1),
DXT3(0x33545844), DXT2('D' + ('X' << 8) + ('T' << 16) + ('2' << 24), 16, DXGI.DXGI_FORMAT_BC2_UNORM, BC2),
DXT4(0x34545844), DXT3('D' + ('X' << 8) + ('T' << 16) + ('3' << 24), 16, DXGI.DXGI_FORMAT_BC2_UNORM, BC2),
DXT5(0x35545844), DXT4('D' + ('X' << 8) + ('T' << 16) + ('4' << 24), 16, DXGI.DXGI_FORMAT_BC3_UNORM, BC3),
DXT10(0x30315844), DXT5('D' + ('X' << 8) + ('T' << 16) + ('5' << 24), 16, DXGI.DXGI_FORMAT_BC3_UNORM, BC3),
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);
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) { // Special case, see DXT10Header.dxgiFormat for real format
this.value = value; 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() { DDSType(int blockSize, int dxgiFormat, int[] rgbaMasks) {
return value; this(0, blockSize, dxgiFormat, null, rgbaMasks);
} }
public static DDSType valueOf(int value) { DDSType(int fourCC, int blockSize, int dxgiFormat, BlockCompression compression, int[] rgbaMasks) {
for (DDSType type : DDSType.values()) { this.fourCC = fourCC;
if (value == type.value()) { this.blockSize = blockSize;
return type; 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 ATI1;
case DXGI.DXGI_FORMAT_BC4_SNORM:
return BC4S;
case DXGI.DXGI_FORMAT_BC5_TYPELESS:
case DXGI.DXGI_FORMAT_BC5_UNORM:
return ATI2;
case DXGI.DXGI_FORMAT_BC5_SNORM:
return BC5S;
}
throw new IllegalArgumentException("Unsupported DXGI_FORMAT: " + dxgiFormat);
} }
} }
@@ -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.
*
* <a href="https://learn.microsoft.com/en-us/windows/win32/api/dxgiformat/ne-dxgiformat-dxgi_format">DXGI Format List</a>
*/
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;
}
}
@@ -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
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();
}
}
@@ -0,0 +1,129 @@
package com.twelvemonkeys.imageio.plugins.dds;
/**
* <a href="https://learn.microsoft.com/en-us/windows/win32/api/dxgiformat/ne-dxgiformat-dxgi_format">DXGI Format List</a>
*/
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;
}
@@ -0,0 +1,45 @@
package com.twelvemonkeys.imageio.plugins.dds;
import javax.imageio.stream.ImageInputStream;
import java.io.IOException;
/**
* @see <a href="https://learn.microsoft.com/en-us/windows/win32/direct3ddds/dds-header-dxt10">DDS_HEADER_DXT10 structure</a>
*/
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;
}
}
@@ -0,0 +1,93 @@
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 compressions = tree.getElementsByTagName("CompressionTypeName");
assertEquals(1, compressions.getLength());
IIOMetadataNode compression = (IIOMetadataNode) compressions.item(0);
assertEquals("DXT1", compression.getAttribute("value"));
// TODO: This should probably not have alpha...
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);
}
}
@@ -110,4 +110,6 @@ public class DDSImageReaderTest extends ImageReaderAbstractTest<DDSImageReader>
protected List<String> getMIMETypes() { protected List<String> getMIMETypes() {
return Collections.singletonList("image/vnd-ms.dds"); return Collections.singletonList("image/vnd-ms.dds");
} }
} }
@@ -0,0 +1,37 @@
package com.twelvemonkeys.imageio.plugins.dds;
import static org.junit.jupiter.api.Assertions.*;
import java.util.Arrays;
import org.junit.jupiter.api.Test;
class DDSImageWriteParamTest {
@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 defaultParam() {
DDSImageWriteParam param = new DDSImageWriteParam();
// param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); // Meh...
assertEquals(DDSImageWriteParam.DEFAULT_TYPE, param.type());
// assertEquals(DDSImageWriterParam.DEFAULT_TYPE.name(), param.getCompressionType());
}
}