diff --git a/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSImageWriteParam.java b/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSImageWriteParam.java
index 18ec5a53..6b0ea889 100644
--- a/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSImageWriteParam.java
+++ b/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSImageWriteParam.java
@@ -42,16 +42,6 @@ public final class DDSImageWriteParam extends ImageWriteParam {
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;
@@ -68,6 +58,5 @@ public final class DDSImageWriteParam extends ImageWriteParam {
}
return DXGI.DXGI_FORMAT_UNKNOWN;
-
}
}
diff --git a/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSImageWriter.java b/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSImageWriter.java
index fca98b6a..690fa371 100644
--- a/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSImageWriter.java
+++ b/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSImageWriter.java
@@ -12,6 +12,7 @@ import javax.imageio.metadata.IIOMetadata;
import javax.imageio.spi.ImageWriterSpi;
import javax.imageio.stream.MemoryCacheImageOutputStream;
+import java.awt.Dimension;
import java.awt.image.Raster;
import java.awt.image.RenderedImage;
import java.io.File;
@@ -27,6 +28,13 @@ import java.nio.file.Paths;
* @author Harald Kuhr
*/
class DDSImageWriter extends ImageWriterBase {
+
+ private long startPos;
+ // TODO: Create a SequenceSupport class that handles sequence prepare/write/end
+ private int mipmapIndex = -1;
+ private DDSType mipmapType;
+ private Dimension mipmapDimension;
+
protected DDSImageWriter(ImageWriterSpi provider) {
super(provider);
}
@@ -36,9 +44,12 @@ class DDSImageWriter extends ImageWriterBase {
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
+ protected void resetMembers() {
+ mipmapIndex = -1;
+ mipmapType = null;
+ mipmapDimension = null;
+ }
@Override
public boolean canWriteRasters() {
@@ -46,36 +57,92 @@ class DDSImageWriter extends ImageWriterBase {
}
@Override
- public void write(IIOMetadata streamMetadata, IIOImage image, ImageWriteParam param) throws IOException {
+ public boolean canWriteSequence() {
+ return true;
+ }
+
+ @Override
+ public void prepareWriteSequence(IIOMetadata streamMetadata) throws IOException {
assertOutput();
+ if (mipmapIndex >= 0) {
+ throw new IllegalStateException("writeSequence already started");
+ }
+ mipmapIndex = 0;
+
+ startPos = imageOutput.getStreamPosition();
+ imageOutput.setByteOrder(ByteOrder.LITTLE_ENDIAN);
+ imageOutput.writeInt(DDS.MAGIC);
+ imageOutput.flush();
+ }
+
+ @Override
+ public void endWriteSequence() throws IOException {
+ assertOutput();
+
+ if (mipmapIndex < 0) {
+ throw new IllegalStateException("prepareWriteSequence not called");
+ }
+
+ // Go back and update hader
+ updateHeader(mipmapIndex);
+
+ mipmapIndex = -1;
+ mipmapType = null;
+ mipmapDimension = null;
+
+ imageOutput.flush();
+ }
+
+ @Override
+ public void write(IIOMetadata streamMetadata, IIOImage image, ImageWriteParam param) throws IOException {
+ prepareWriteSequence(streamMetadata);
+ writeToSequence(image, param);
+ endWriteSequence();
+ }
+
+ @Override
+ public void writeToSequence(IIOImage image, ImageWriteParam param) throws IOException {
+ if (mipmapIndex < 0) {
+ throw new IllegalStateException("prepareWriteSequence not called");
+ }
+
Raster raster = getRaster(image);
- ensureTextureSize(raster);
ensureImageChannels(raster);
+ ensureTextureDimension(raster);
DDSImageWriteParam ddsParam = param instanceof DDSImageWriteParam
? ((DDSImageWriteParam) param)
: IIOUtil.copyStandardParams(param, getDefaultWriteParam());
- if (ddsParam.compression() == null) {
+ DDSType type = ddsParam.type();
+ if (mipmapType == null) {
+ mipmapType = type;
+ }
+ else if (type != mipmapType) {
+ processWarningOccurred(mipmapIndex, "All images in DDS MipMap must use same pixel format and compression");
+ }
+ if (mipmapType == null) {
throw new IIOException("Only compressed DDS using DXT1-5 or DXT10 with block compression is currently supported");
}
- imageOutput.setByteOrder(ByteOrder.LITTLE_ENDIAN);
- imageOutput.writeInt(DDS.MAGIC);
-
- writeHeader(raster.getWidth(), raster.getHeight(), ddsParam.type(), ddsParam.isWriteDXT10());
- if (ddsParam.isWriteDXT10()) {
- writeDXT10Header(ddsParam.getDxgiFormat());
+ if (mipmapIndex == 0) {
+ writeHeader(raster.getWidth(), raster.getHeight(), mipmapType, ddsParam.isWriteDXT10());
+ if (ddsParam.isWriteDXT10()) {
+ writeDXT10Header(ddsParam.getDxgiFormat());
+ }
}
- processImageStarted(0);
+ processImageStarted(mipmapIndex);
processImageProgress(0f);
- DDSImageDataEncoder.writeImageData(imageOutput, raster, ddsParam.compression());
- processImageProgress(100f);
- imageOutput.flush();
+ DDSImageDataEncoder.writeImageData(imageOutput, raster, mipmapType.compression);
+
+ processImageProgress(100f);
processImageComplete();
+
+ mipmapDimension = new Dimension(raster.getWidth(), raster.getHeight());
+ mipmapIndex++;
}
private static Raster getRaster(IIOImage image) throws IIOException {
@@ -113,9 +180,9 @@ 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, ...
+ * e.g. 16x16, 32x32, 512x128, 512x512, 1024x512, 1024x1024, 2048x1024...
*/
- private void ensureTextureSize(Raster raster) throws IIOException {
+ private void ensureTextureDimension(Raster raster) throws IIOException {
int width = raster.getWidth();
int height = raster.getHeight();
@@ -123,6 +190,13 @@ class DDSImageWriter extends ImageWriterBase {
if (width % 4 != 0 || height % 4 != 0) {
throw new IIOException(String.format("Image dimensions must be dividable by 4, ideally a power of 2; got %dx%d", width, height));
}
+
+ if (mipmapDimension != null && (mipmapDimension.width != width * 2|| mipmapDimension.height != height * 2)) {
+ throw new IIOException(
+ String.format("For mipmap, image dimensions must be exactly half of previous (%dx%d); got %dx%d",
+ mipmapDimension.width, mipmapDimension.height, width, height)
+ );
+ }
}
private void writeHeader(int width, int height, DDSType type, boolean writeDXT10) throws IOException {
@@ -149,6 +223,25 @@ class DDSImageWriter extends ImageWriterBase {
imageOutput.write(new byte[12]);
}
+ private void updateHeader(int mipmapCount) throws IOException {
+ if (mipmapCount == 1) {
+ // Fast case, nothing to do
+ return;
+ }
+
+ long streamPosition = imageOutput.getStreamPosition();
+ imageOutput.seek(startPos + 8); // Seek back to start + 4 magic + 4 header size
+
+ int flags = imageOutput.readInt();
+ imageOutput.seek(imageOutput.getStreamPosition() - 4);
+ imageOutput.writeInt(flags | DDS.FLAG_MIPMAPCOUNT);
+
+ imageOutput.seek(imageOutput.getStreamPosition() + 16);
+ imageOutput.writeInt(mipmapCount);
+
+ imageOutput.seek(streamPosition); // Restore pos
+ }
+
//https://learn.microsoft.com/en-us/windows/win32/direct3ddds/dds-pixelformat
private void writePixelFormat(DDSType type, boolean writeDXT10) throws IOException {
imageOutput.writeInt(DDS.PIXELFORMAT_SIZE);
diff --git a/imageio/imageio-dds/src/test/java/com/twelvemonkeys/imageio/plugins/dds/DDSImageWriteParamTest.java b/imageio/imageio-dds/src/test/java/com/twelvemonkeys/imageio/plugins/dds/DDSImageWriteParamTest.java
index cf509ff9..beeb2a59 100644
--- a/imageio/imageio-dds/src/test/java/com/twelvemonkeys/imageio/plugins/dds/DDSImageWriteParamTest.java
+++ b/imageio/imageio-dds/src/test/java/com/twelvemonkeys/imageio/plugins/dds/DDSImageWriteParamTest.java
@@ -49,7 +49,6 @@ class DDSImageWriteParamTest {
DDSType type = DDSType.valueOf(compressionType);
assertEquals(type, param.type());
- assertEquals(type.compression, param.compression());
}
}
}
diff --git a/imageio/imageio-dds/src/test/java/com/twelvemonkeys/imageio/plugins/dds/DDSImageWriterTest.java b/imageio/imageio-dds/src/test/java/com/twelvemonkeys/imageio/plugins/dds/DDSImageWriterTest.java
index 39e2d2ae..8182ab14 100644
--- a/imageio/imageio-dds/src/test/java/com/twelvemonkeys/imageio/plugins/dds/DDSImageWriterTest.java
+++ b/imageio/imageio-dds/src/test/java/com/twelvemonkeys/imageio/plugins/dds/DDSImageWriterTest.java
@@ -1,12 +1,38 @@
package com.twelvemonkeys.imageio.plugins.dds;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+
import com.twelvemonkeys.imageio.util.ImageWriterAbstractTest;
+import javax.imageio.IIOException;
+import javax.imageio.IIOImage;
+import javax.imageio.ImageIO;
+import javax.imageio.ImageReader;
+import javax.imageio.ImageWriteParam;
+import javax.imageio.ImageWriter;
+import javax.imageio.event.IIOWriteWarningListener;
import javax.imageio.spi.ImageWriterSpi;
+import javax.imageio.stream.ImageInputStream;
+import javax.imageio.stream.ImageOutputStream;
+
+import java.awt.Image;
import java.awt.image.BufferedImage;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
import java.util.Arrays;
import java.util.List;
+import org.junit.jupiter.api.Test;
+
public class DDSImageWriterTest extends ImageWriterAbstractTest {
@Override
protected ImageWriterSpi createProvider() {
@@ -16,11 +42,135 @@ public class DDSImageWriterTest extends ImageWriterAbstractTest
@Override
protected List getTestData() {
return Arrays.asList(
- new BufferedImage(256, 256, BufferedImage.TYPE_INT_ARGB_PRE),
+ new BufferedImage(256, 256, BufferedImage.TYPE_INT_ARGB),
new BufferedImage(128, 128, BufferedImage.TYPE_INT_ARGB),
new BufferedImage(64, 64, BufferedImage.TYPE_INT_RGB),
new BufferedImage(32, 32, BufferedImage.TYPE_4BYTE_ABGR),
new BufferedImage(16, 16, BufferedImage.TYPE_3BYTE_BGR)
);
}
+
+ @Test
+ void writeMipmap() throws IOException {
+ ImageWriter writer = createWriter();
+
+ try {
+ List testData = getTestData();
+ ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+ int previousSize = 0;
+
+ try (ImageOutputStream stream = ImageIO.createImageOutputStream(buffer)) {
+ writer.setOutput(stream);
+ writer.prepareWriteSequence(null);
+ ImageWriteParam param = writer.getDefaultWriteParam();
+
+ assertTrue(buffer.size() > previousSize);
+ previousSize = buffer.size();
+
+ for (BufferedImage image : testData) {
+ writer.writeToSequence(new IIOImage(drawSomething(image), null, null), param);
+ }
+
+ writer.endWriteSequence();
+ assertTrue(buffer.size() > previousSize, "No image data written");
+ }
+ catch (IOException e) {
+ throw new AssertionError(e.getMessage(), e);
+ }
+
+ // Verify that we can read the file back...
+ ImageReader reader = ImageIO.getImageReader(writer);
+
+ try (ImageInputStream stream = ImageIO.createImageInputStream(new ByteArrayInputStream(buffer.toByteArray()))) {
+ stream.seek(0);
+ reader.setInput(stream);
+
+ assertEquals(testData.size(), reader.getNumImages(false));
+
+ for (int i = 0; i < testData.size(); i++) {
+ BufferedImage image = reader.read(i, null);
+
+ assertNotNull(image);
+ assertEquals(testData.get(i).getWidth(), image.getWidth());
+ assertEquals(testData.get(i).getHeight(), image.getHeight());
+ }
+ }
+ finally {
+ reader.dispose();
+ }
+
+ }
+ finally {
+ writer.dispose();
+ }
+ }
+
+ @Test
+ void writeMipmapDifferentCompression() throws IOException {
+ ImageWriter writer = createWriter();
+
+ try {
+ ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+
+ try (ImageOutputStream stream = ImageIO.createImageOutputStream(buffer)) {
+ IIOWriteWarningListener listener = mock();
+ writer.addIIOWriteWarningListener(listener);
+ writer.setOutput(stream);
+
+ writer.prepareWriteSequence(null);
+ ImageWriteParam param = writer.getDefaultWriteParam();
+ param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
+ param.setCompressionType("DXT2");
+
+ // Write first with DXT2
+ List testData = getTestData();
+ writer.writeToSequence(new IIOImage(drawSomething(testData.get(0)), null, null), param);
+
+ // Repeat with different type
+ IIOImage image = new IIOImage(drawSomething(testData.get(1)), null, null);
+ param.setCompressionType("DXT1");
+
+ writer.writeToSequence(image, param);
+
+ // Verify warning is issued
+ verify(listener).warningOccurred(eq(writer), eq(1), anyString());
+ verifyNoMoreInteractions(listener);
+ }
+ catch (IOException e) {
+ throw new AssertionError(e.getMessage(), e);
+ }
+ }
+ finally {
+ writer.dispose();
+ }
+ }
+
+ @Test
+ void writeMipmapUnexpectedSize() throws IOException {
+ ImageWriter writer = createWriter();
+
+ try {
+ ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+
+ try (ImageOutputStream stream = ImageIO.createImageOutputStream(buffer)) {
+ writer.setOutput(stream);
+
+ writer.prepareWriteSequence(null);
+ ImageWriteParam param = writer.getDefaultWriteParam();
+ BufferedImage testData = getTestData().get(0);
+
+ IIOImage image = new IIOImage(drawSomething(testData), null, null);
+ writer.writeToSequence(image, param);
+
+ // Repeat with same size... boom.
+ assertThrows(IIOException.class, () -> writer.writeToSequence(image, param));
+ }
+ catch (IOException e) {
+ throw new AssertionError(e.getMessage(), e);
+ }
+ }
+ finally {
+ writer.dispose();
+ }
+ }
}