More clean-up: Now keeps stream byte order consistent (LE), support for Raster, more tests

This commit is contained in:
Harald Kuhr
2026-03-11 15:59:48 +01:00
parent dc59c66209
commit 3f356a8197
12 changed files with 184 additions and 58 deletions

View File

@@ -33,7 +33,8 @@ 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;
@@ -48,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;

View File

@@ -71,12 +71,10 @@ final class DDSHeader {
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

View File

@@ -1,5 +1,6 @@
package com.twelvemonkeys.imageio.plugins.dds;
import javax.imageio.IIOException;
import javax.imageio.stream.ImageOutputStream;
import java.awt.Color;
import java.awt.image.Raster;
@@ -27,24 +28,24 @@ 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, BlockCompression compression) throws IOException {
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 block compression is not supported yet: " + compression);
@@ -418,14 +419,9 @@ class DDSImageDataEncoder {
}
}
void encode(ImageOutputStream imageOutput, RenderedImage image) throws IOException {
int blocksXCount = (image.getWidth() + 3) / 4;
int blocksYCount = (image.getHeight() + 3) / 4;
if (image.getNumXTiles() != 1 || image.getNumYTiles() != 1) {
throw new IllegalArgumentException("Only single tile images supported");
}
Raster raster = image.getTile(0, 0);
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++) {

View File

@@ -166,11 +166,9 @@ public final class DDSImageReader extends ImageReaderBase {
private void readHeader() throws IOException {
if (header == null) {
imageInput.setByteOrder(ByteOrder.LITTLE_ENDIAN); // TODO: Move to setInput?
imageInput.setByteOrder(ByteOrder.LITTLE_ENDIAN);
header = DDSHeader.read(imageInput);
imageInput.flushBefore(imageInput.getStreamPosition());
System.out.println("header = " + header);
}
imageInput.seek(imageInput.getFlushedPosition());

View File

@@ -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,11 +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 {
stream.setByteOrder(byteOrder);
stream.reset();
}
}

View File

@@ -3,6 +3,7 @@ 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;
@@ -39,51 +40,74 @@ class DDSImageWriter extends ImageWriterBase {
// 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
public boolean canWriteRasters() {
return true;
}
@Override
public void write(IIOMetadata streamMetadata, IIOImage image, ImageWriteParam param) throws IOException {
assertOutput();
RenderedImage renderedImage = image.getRenderedImage(); // TODO: Support raster?
ensureTextureSize(renderedImage);
ensureImageChannels(renderedImage);
Raster raster = getRaster(image);
ensureTextureSize(raster);
ensureImageChannels(raster);
DDSImageWriteParam ddsParam = param instanceof DDSImageWriteParam
? ((DDSImageWriteParam) param)
: IIOUtil.copyStandardParams(param, getDefaultWriteParam());
processImageStarted(0);
imageOutput.setByteOrder(ByteOrder.BIG_ENDIAN);
imageOutput.writeInt(DDS.MAGIC);
imageOutput.setByteOrder(ByteOrder.LITTLE_ENDIAN);
if (ddsParam.compression() == null) {
throw new IIOException("Only compressed DDS using DXT1-5 or DXT10 with block compression is currently supported");
}
writeHeader(image, ddsParam.type(), ddsParam.isWriteDXT10());
imageOutput.setByteOrder(ByteOrder.LITTLE_ENDIAN);
imageOutput.writeInt(DDS.MAGIC);
writeHeader(raster.getWidth(), raster.getHeight(), ddsParam.type(), ddsParam.isWriteDXT10());
if (ddsParam.isWriteDXT10()) {
writeDXT10Header(ddsParam.getDxgiFormat());
}
processImageStarted(0);
processImageProgress(0f);
DDSImageDataEncoder.writeImageData(imageOutput, renderedImage, ddsParam.compression());
DDSImageDataEncoder.writeImageData(imageOutput, raster, ddsParam.compression());
processImageProgress(100f);
imageOutput.flush();
processImageComplete();
}
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.getTile(0, 0);
private void ensureImageChannels(Raster data) throws IIOException {
int numBands = data.getNumBands();
if (numBands < 3) {
throw new IllegalStateException(
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);
throw new IIOException("Only image with 8 bits/channel is supported, got " + sampleSize);
}
}
@@ -91,24 +115,21 @@ class DDSImageWriter extends ImageWriterBase {
* 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, ...
*/
private void ensureTextureSize(RenderedImage renderedImage) {
int w = renderedImage.getWidth();
int h = renderedImage.getHeight();
private void ensureTextureSize(Raster raster) throws IIOException {
int width = raster.getWidth();
int height = raster.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));
// 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));
}
}
private void writeHeader(IIOImage image, DDSType type, boolean writeDXT10) throws IOException {
private void writeHeader(int width, int height, DDSType type, boolean writeDXT10) throws IOException {
imageOutput.writeInt(DDS.HEADER_SIZE);
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();
int height = renderedImage.getHeight();
int width = renderedImage.getWidth();
imageOutput.writeInt(height);
imageOutput.writeInt(width);
writePitchOrLinearSize(height, width, type);
@@ -126,12 +147,11 @@ class DDSImageWriter extends ImageWriterBase {
imageOutput.writeInt(0);
//dwCaps3, dwCaps4, dwReserved2 : 3 unused integers
imageOutput.write(new byte[12]);
}
//https://learn.microsoft.com/en-us/windows/win32/direct3ddds/dds-pixelformat
private void writePixelFormat(DDSType type, boolean writeDXT10) throws IOException {
imageOutput.writeInt(DDS.DDSPF_SIZE);
imageOutput.writeInt(DDS.PIXELFORMAT_SIZE);
writePixelFormatFlags(type, writeDXT10);
writeFourCC(type, writeDXT10);
writeRGBAData(type, writeDXT10);

View File

@@ -4,6 +4,8 @@ import com.twelvemonkeys.imageio.spi.ImageWriterSpiBase;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.ImageWriter;
import java.awt.image.Raster;
import java.util.Locale;
public final class DDSImageWriterSpi extends ImageWriterSpiBase {
@@ -13,7 +15,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

View File

@@ -166,14 +166,14 @@ enum DDSType {
case DXGI.DXGI_FORMAT_BC4_TYPELESS:
case DXGI.DXGI_FORMAT_BC4_UNORM:
return ATI1;
return BC4U;
case DXGI.DXGI_FORMAT_BC4_SNORM:
return BC4S;
case DXGI.DXGI_FORMAT_BC5_TYPELESS:
case DXGI.DXGI_FORMAT_BC5_UNORM:
return ATI2;
return BC5U;
case DXGI.DXGI_FORMAT_BC5_SNORM:
return BC5S;

View File

@@ -27,6 +27,11 @@ class DDSImageMetadataTest {
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);

View File

@@ -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<DDSImageReader> {
@Override
protected ImageReaderSpi createProvider() {
@@ -111,5 +124,66 @@ public class DDSImageReaderTest extends ImageReaderAbstractTest<DDSImageReader>
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();
}
}
}

View File

@@ -4,9 +4,17 @@ 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();
@@ -27,11 +35,22 @@ class DDSImageWriteParamTest {
}
@Test
void defaultParam() {
void setCompression() {
DDSImageWriteParam param = new DDSImageWriteParam();
// param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); // Meh...
assertEquals(DDSImageWriteParam.DEFAULT_TYPE, param.type());
// assertEquals(DDSImageWriterParam.DEFAULT_TYPE.name(), param.getCompressionType());
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());
assertEquals(type.compression, param.compression());
}
}
}
}

View File

@@ -4,7 +4,7 @@ import com.twelvemonkeys.imageio.util.ImageWriterAbstractTest;
import javax.imageio.spi.ImageWriterSpi;
import java.awt.image.BufferedImage;
import java.util.Collections;
import java.util.Arrays;
import java.util.List;
public class DDSImageWriterTest extends ImageWriterAbstractTest<DDSImageWriter> {
@@ -15,8 +15,12 @@ public class DDSImageWriterTest extends ImageWriterAbstractTest<DDSImageWriter>
@Override
protected List<BufferedImage> getTestData() {
return Collections.singletonList(
new BufferedImage(256, 256, BufferedImage.TYPE_INT_ARGB_PRE)
return Arrays.asList(
new BufferedImage(256, 256, BufferedImage.TYPE_INT_ARGB_PRE),
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)
);
}
}