More clean-up: Removed optional flags from param, header size validation, metadata now reports compresion as lossy

This commit is contained in:
Harald Kuhr
2026-03-11 14:51:01 +01:00
parent 2a0b15f33f
commit dc59c66209
9 changed files with 50 additions and 52 deletions
@@ -72,7 +72,7 @@ public final class IIOUtil {
*/ */
public static InputStream createStreamAdapter(final ImageInputStream pStream) { public static InputStream createStreamAdapter(final ImageInputStream pStream) {
// TODO: Include stream start pos? // 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)); return new BufferedInputStream(new IIOInputStreamAdapter(pStream));
} }
@@ -86,7 +86,7 @@ public final class IIOUtil {
*/ */
public static InputStream createStreamAdapter(final ImageInputStream pStream, final long pLength) { public static InputStream createStreamAdapter(final ImageInputStream pStream, final long pLength) {
// TODO: Include stream start pos? // 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)); return new BufferedInputStream(new IIOInputStreamAdapter(pStream, pLength));
} }
@@ -35,6 +35,7 @@ package com.twelvemonkeys.imageio.plugins.dds;
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;
int PIXELFORMAT_SIZE = 32;
// Header Flags // Header Flags
int FLAG_CAPS = 1; // Required in every .dds file. int FLAG_CAPS = 1; // Required in every .dds file.
@@ -111,6 +111,9 @@ final class DDSHeader {
// DDS_PIXELFORMAT structure // DDS_PIXELFORMAT structure
int px_dwSize = imageInput.readInt(); // [76,79] 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.pixelFormatFlags = imageInput.readInt(); // [80,83]
header.fourCC = imageInput.readInt(); // [84,87] header.fourCC = imageInput.readInt(); // [84,87]
@@ -243,10 +246,12 @@ final class DDSHeader {
@Override @Override
public String toString() { public String toString() {
return "DDSHeader{" + return "DDSHeader{" +
"flags=" + flags + "flags=" + Integer.toBinaryString(flags) +
", mipMapCount=" + mipMapCount + ", mipMapCount=" + mipMapCount +
", dimensions=" + Arrays.toString(dimensions) + ", dimensions=" + Arrays.toString(Arrays.stream(dimensions)
", pixelFormatFlags=" + pixelFormatFlags + .map(DDSHeader::dimensionToString)
.toArray(String[]::new)) +
", pixelFormatFlags=" + Integer.toBinaryString(pixelFormatFlags) +
", fourCC=" + fourCC + ", fourCC=" + fourCC +
", bitCount=" + bitCount + ", bitCount=" + bitCount +
", redMask=" + redMask + ", redMask=" + redMask +
@@ -255,4 +260,8 @@ final class DDSHeader {
", alphaMask=" + alphaMask + ", alphaMask=" + alphaMask +
'}'; '}';
} }
private static String dimensionToString(Dimension dimension) {
return String.format("%dx%d", dimension.width, dimension.height);
}
} }
@@ -13,15 +13,11 @@ import static com.twelvemonkeys.imageio.plugins.dds.DDSReader.RGB_16_ORDER;
/** /**
* A designated class to encode image data to binary. * A designated class to encode image data to binary.
* <p> *
* References: * @see <a href="https://www.ludicon.com/castano/blog/2009/03/gpu-dxt-decompression/">GPU DXT Decompression</a>.
* <p> * @see <a href="https://sv-journal.org/2014-1/06/en/index.php">TEXTURE COMPRESSION TECHNIQUES</a>.
* [1] <a href="https://www.ludicon.com/castano/blog/2009/03/gpu-dxt-decompression/">GPU DXT Decompression</a>. * @see <a href="https://mrelusive.com/publications/papers/Real-Time-Dxt-Compression.pdf">Real-Time DXT Compression by J.M.P. van Waveren</a>
* [2] <a href="https://sv-journal.org/2014-1/06/en/index.php">TEXTURE COMPRESSION TECHNIQUES</a>. * @see <a href="https://registry.khronos.org/DataFormat/specs/1.4/dataformat.1.4.pdf">Khronos Data Format Specification v1.4 by Andrew Garrard</a>
* [3] <a href="https://mrelusive.com/publications/papers/Real-Time-Dxt-Compression.pdf">Real-Time DXT Compression by J.M.P. van Waveren</a>
* [4] <a href="https://registry.khronos.org/DataFormat/specs/1.4/dataformat.1.4.pdf">Khronos Data Format Specification v1.4 by Andrew Garrard</a>
* </p>
* </p>
*/ */
class DDSImageDataEncoder { class DDSImageDataEncoder {
private DDSImageDataEncoder() {} private DDSImageDataEncoder() {}
@@ -32,7 +28,7 @@ class DDSImageDataEncoder {
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, BlockCompression compression) throws IOException { static void writeImageData(ImageOutputStream imageOutput, RenderedImage renderedImage, BlockCompression compression) throws IOException {
// TODO: compression == null for custom RGB data? // TODO: Support compression == null for uncompressed RGB(A/X) data?
switch (compression) { switch (compression) {
case BC1: case BC1:
@@ -39,6 +39,7 @@ final class DDSImageMetadata extends StandardImageMetadataSupport {
DDSImageMetadata(ImageTypeSpecifier specifier, DDSType type) { DDSImageMetadata(ImageTypeSpecifier specifier, DDSType type) {
super(builder(specifier) super(builder(specifier)
.withCompressionTypeName(compressionName(type)) .withCompressionTypeName(compressionName(type))
.withCompressionLossless(!type.isBlockCompression())
.withBitsPerSample(bitsPerSample(type)) .withBitsPerSample(bitsPerSample(type))
.withFormatVersion("1.0") .withFormatVersion("1.0")
); );
@@ -101,6 +101,11 @@ public final class DDSImageReader extends ImageReaderBase {
return ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_INT_RGB); 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); return ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_INT_ARGB);
} }
@@ -161,9 +166,11 @@ public final class DDSImageReader extends ImageReaderBase {
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); // TODO: Move to setInput?
header = DDSHeader.read(imageInput); header = DDSHeader.read(imageInput);
imageInput.flushBefore(imageInput.getStreamPosition()); imageInput.flushBefore(imageInput.getStreamPosition());
System.out.println("header = " + header);
} }
imageInput.seek(imageInput.getFlushedPosition()); imageInput.seek(imageInput.getFlushedPosition());
@@ -22,38 +22,12 @@ public final class DDSImageWriteParam extends ImageWriteParam {
return compressionTypes.toArray(new String[0]); return compressionTypes.toArray(new String[0]);
} }
private int optionalBitFlags;
private boolean writeDXT10; private boolean writeDXT10;
DDSImageWriteParam() { DDSImageWriteParam() {
canWriteCompressed = true; canWriteCompressed = true;
compressionTypes = COMPRESSION_TYPES; compressionTypes = COMPRESSION_TYPES;
compressionType = DEFAULT_TYPE.name(); 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() { public void setWriteDX10() {
@@ -35,6 +35,10 @@ class DDSImageWriter extends ImageWriterBase {
return new DDSImageWriteParam(); return new DDSImageWriteParam();
} }
// TODO: Suppport MipMaps using sequence methods
// This involves seeking backwards, updating the mipmap flag and mipmapcount in the header... :-/
// + ensuring that each level is half the size of the previous, but still a multiple of 4...
@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();
@@ -52,7 +56,7 @@ class DDSImageWriter extends ImageWriterBase {
imageOutput.writeInt(DDS.MAGIC); imageOutput.writeInt(DDS.MAGIC);
imageOutput.setByteOrder(ByteOrder.LITTLE_ENDIAN); imageOutput.setByteOrder(ByteOrder.LITTLE_ENDIAN);
writeHeader(image, ddsParam.type(), ddsParam.isWriteDXT10(), ddsParam.optionalBitFlags()); writeHeader(image, ddsParam.type(), ddsParam.isWriteDXT10());
if (ddsParam.isWriteDXT10()) { if (ddsParam.isWriteDXT10()) {
writeDXT10Header(ddsParam.getDxgiFormat()); writeDXT10Header(ddsParam.getDxgiFormat());
} }
@@ -96,9 +100,10 @@ class DDSImageWriter extends ImageWriterBase {
} }
} }
private void writeHeader(IIOImage image, DDSType type, boolean writeDXT10, int optionalBitFlags) throws IOException { private void writeHeader(IIOImage image, DDSType type, boolean writeDXT10) 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 | optionalBitFlags); 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);
RenderedImage renderedImage = image.getRenderedImage(); RenderedImage renderedImage = image.getRenderedImage();
int height = renderedImage.getHeight(); int height = renderedImage.getHeight();
@@ -182,7 +187,8 @@ class DDSImageWriter extends ImageWriterBase {
private void writePixelFormatFlags(DDSType type, boolean writeDXT10) throws IOException { private void writePixelFormatFlags(DDSType type, boolean writeDXT10) throws IOException {
if (writeDXT10 || type.isFourCC()) { if (writeDXT10 || type.isFourCC()) {
imageOutput.writeInt(DDS.PIXEL_FORMAT_FLAG_FOURCC); imageOutput.writeInt(DDS.PIXEL_FORMAT_FLAG_FOURCC);
} else { }
else {
imageOutput.writeInt(DDS.PIXEL_FORMAT_FLAG_RGB | (type.rgbaMasks != null ? DDS.PIXEL_FORMAT_FLAG_ALPHAPIXELS : 0)); imageOutput.writeInt(DDS.PIXEL_FORMAT_FLAG_RGB | (type.rgbaMasks != null ? DDS.PIXEL_FORMAT_FLAG_ALPHAPIXELS : 0));
} }
} }
@@ -190,7 +196,8 @@ class DDSImageWriter extends ImageWriterBase {
private void writePitchOrLinearSize(int height, int width, DDSType type) throws IOException { private void writePitchOrLinearSize(int height, int width, DDSType type) throws IOException {
if (type.isBlockCompression()) { if (type.isBlockCompression()) {
imageOutput.writeInt(((width + 3) / 4) * ((height + 3) / 4) * type.blockSize()); imageOutput.writeInt(((width + 3) / 4) * ((height + 3) / 4) * type.blockSize());
} else { }
else {
imageOutput.writeInt(width * type.blockSize()); imageOutput.writeInt(width * type.blockSize());
} }
} }
@@ -17,13 +17,16 @@ class DDSImageMetadataTest {
DDSImageMetadata metadata = createDDSImageMetadata(BufferedImage.TYPE_INT_ARGB, DDSType.DXT1); DDSImageMetadata metadata = createDDSImageMetadata(BufferedImage.TYPE_INT_ARGB, DDSType.DXT1);
IIOMetadataNode tree = (IIOMetadataNode) metadata.getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName); IIOMetadataNode tree = (IIOMetadataNode) metadata.getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName);
NodeList compressions = tree.getElementsByTagName("CompressionTypeName"); NodeList compressionTypeNames = tree.getElementsByTagName("CompressionTypeName");
assertEquals(1, compressions.getLength()); assertEquals(1, compressionTypeNames.getLength());
IIOMetadataNode compressionTypeName = (IIOMetadataNode) compressionTypeNames.item(0);
assertEquals("DXT1", compressionTypeName.getAttribute("value"));
IIOMetadataNode compression = (IIOMetadataNode) compressions.item(0); NodeList losslesses = tree.getElementsByTagName("Lossless");
assertEquals("DXT1", compression.getAttribute("value")); assertEquals(1, losslesses.getLength());
IIOMetadataNode lossless = (IIOMetadataNode) losslesses.item(0);
assertEquals("FALSE", lossless.getAttribute("value"));
// TODO: This should probably not have alpha...
NodeList alphas = tree.getElementsByTagName("Alpha"); NodeList alphas = tree.getElementsByTagName("Alpha");
assertEquals(1, alphas.getLength()); assertEquals(1, alphas.getLength());
IIOMetadataNode alpha = (IIOMetadataNode) alphas.item(0); IIOMetadataNode alpha = (IIOMetadataNode) alphas.item(0);