mirror of
https://github.com/haraldk/TwelveMonkeys.git
synced 2026-03-20 00:00:03 -04:00
Mipmap support using ImageIO sequence API
This commit is contained in:
@@ -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;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||
*/
|
||||
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);
|
||||
|
||||
@@ -49,7 +49,6 @@ class DDSImageWriteParamTest {
|
||||
DDSType type = DDSType.valueOf(compressionType);
|
||||
|
||||
assertEquals(type, param.type());
|
||||
assertEquals(type.compression, param.compression());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<DDSImageWriter> {
|
||||
@Override
|
||||
protected ImageWriterSpi createProvider() {
|
||||
@@ -16,11 +42,135 @@ public class DDSImageWriterTest extends ImageWriterAbstractTest<DDSImageWriter>
|
||||
@Override
|
||||
protected List<BufferedImage> 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<BufferedImage> 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<BufferedImage> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user