Skip to content

Commit

Permalink
Microsoft Direct DrawSurface (DDS) support. (#1021)
Browse files Browse the repository at this point in the history
* Basic wrapper around DDSReader.

Nasty hack to get initial plugin working...
Mark and Reset the imageinput stream when reading the header.  Then read the whole buffer into DDSReader and generate a fixed BufferedImage ignoring the getDestination() BufferedImage.

* Read header (DDSHeader) and pass into DDSReader

Remove header methods using buffer offset addressing.
Switch endian for DX1-DX5 types

* Fix pixel order to ARGB

* Push ImageInputStream into DDSReader

Unable to determine buffer length (so as a hack I over allocate buffer and read)

```
byte[] buffer = new byte[width * height * 4];
int len = imageInput.read(buffer);
```

Added test files for all supported formats.

* Added processImageStarted and stubbed out processImageProgress and processReadAborted

Added all DDS format test cases to getTestData

* Remove offset and use imageInput.readFully

Reads next image calculating/updating width/height from mipmapLevel.
Probably will never get used as we only want the largest image returned.

* Code cleanup and added exception handling.

* Use Enum DDSType instead of int values.

Pass imageIndex into mipmap.

* Update imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSHeader.java

Improve IIOException "Invalid DDS header size"

Co-authored-by: Harald Kuhr <[email protected]>

* Update imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSHeader.java

Improve Magic IIOException

Co-authored-by: Harald Kuhr <[email protected]>

* Delete .run directory

* Format tabs -> 4 spaces

* Convert 4byte array into %08x string format.

* Don't need to expose classes outside the package

DDSHeader, DDSReader, DDSType

* (fix) wrong public...

* Move setByteOrder to DDSImageReader.readHeader

* Use imageIndex to calculate height/width and buffer offset.

* Delete .gitignore

* Revert "Delete .gitignore"

This reverts commit 71a4e73.

* Undelete/Restore .gitignore

* Simplify String.format into one.

* Override failing tests and ignore.

* Revert formatting on PNGImageReaderTest.java

---------

Co-authored-by: Harald Kuhr <[email protected]>
  • Loading branch information
p4paul and haraldk authored Sep 25, 2024
1 parent 4fc9c35 commit 3c01071
Show file tree
Hide file tree
Showing 47 changed files with 1,177 additions and 0 deletions.
5 changes: 5 additions & 0 deletions bom/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@
<artifactId>imageio-hdr</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio-dds</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio-icns</artifactId>
Expand Down
52 changes: 52 additions & 0 deletions imageio/imageio-dds/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?xml version="1.0"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId>
<version>3.11.1-SNAPSHOT</version>
</parent>
<artifactId>imageio-dds</artifactId>
<name>TwelveMonkeys :: ImageIO :: DDS plugin</name>
<description>
ImageIO plugin for Microsoft Direct DrawSurface (DDS).
</description>

<properties>
<project.jpms.module.name>com.twelvemonkeys.imageio.dds</project.jpms.module.name>
</properties>

<dependencies>
<dependency>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio-core</artifactId>
</dependency>
<dependency>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio-core</artifactId>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio-metadata</artifactId>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.felix</groupId>
<artifactId>maven-bundle-plugin</artifactId>
<configuration>
<instructions>
<Provide-Capability>
osgi.serviceloader;
osgi.serviceloader=javax.imageio.spi.ImageReaderSpi
</Provide-Capability>
</instructions>
</configuration>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.twelvemonkeys.imageio.plugins.dds;

interface DDS {
byte[] MAGIC = new byte[]{'D', 'D', 'S', ' '};
int HEADER_SIZE = 124;

int FLAG_CAPS = 0x1; // Required in every .dds file.
int FLAG_HEIGHT = 0x2; // Required in every .dds file.
int FLAG_WIDTH = 0x4; // Required in every .dds file.
int FLAG_PITCH = 0x8; // Required when pitch is provided for an uncompressed texture.
int FLAG_PIXELFORMAT = 0x1000; // Required in every .dds file.
int FLAG_MIPMAPCOUNT = 0x20000; // Required in a mipmapped texture.
int FLAG_LINEARSIZE = 0x80000; // Required when pitch is provided for a compressed texture.
int FLAG_DEPTH = 0x800000; // Required in a depth texture.
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package com.twelvemonkeys.imageio.plugins.dds;

import javax.imageio.IIOException;
import javax.imageio.stream.ImageInputStream;
import java.awt.Dimension;
import java.io.IOException;
import java.math.BigInteger;
import java.util.Arrays;

final class DDSHeader {

// https://learn.microsoft.com/en-us/windows/win32/direct3ddds/dx-graphics-dds-pguide
private int flags;

private int mipMapCount;
private Dimension[] dimensions;

private int pixelFormatFlags;
private int fourCC;
private int bitCount;
private int redMask;
private int greenMask;
private int blueMask;
private int alphaMask;

public static DDSHeader read(final ImageInputStream imageInput) throws IOException {
DDSHeader header = new DDSHeader();

// Read MAGIC bytes [0,3]
byte[] magic = new byte[DDS.MAGIC.length];
imageInput.readFully(magic);
if (!Arrays.equals(DDS.MAGIC, magic)) {
throw new IIOException(String.format("Not a DDS file. Expected DDS magic %08x, read %08x", new BigInteger(1, DDS.MAGIC), new BigInteger(1, magic)));
}

// DDS_HEADER structure
// https://learn.microsoft.com/en-us/windows/win32/direct3ddds/dds-header
int dwSize = imageInput.readInt(); // [4,7]
if (dwSize != DDS.HEADER_SIZE) {
throw new IIOException(String.format("Invalid DDS header size (expected %d): %d", DDS.HEADER_SIZE, dwSize));
}

// Verify flags
header.flags = imageInput.readInt(); // [8,11]
if (header.getFlag(DDS.FLAG_CAPS
& DDS.FLAG_HEIGHT
& DDS.FLAG_WIDTH
& DDS.FLAG_PIXELFORMAT)) {
throw new IIOException("Required DDS Flag missing in header: " + Integer.toHexString(header.flags));
}

// Read Height & Width
int dwHeight = imageInput.readInt(); // [12,15]
int dwWidth = imageInput.readInt(); // [16,19]

int dwPitchOrLinearSize = imageInput.readInt(); // [20,23]
int dwDepth = imageInput.readInt(); // [24,27]
header.mipMapCount = imageInput.readInt(); // [28,31]

// build dimensions list
header.addDimensions(dwWidth, dwHeight);

byte[] dwReserved1 = new byte[11 * 4]; // [32,75]
imageInput.readFully(dwReserved1);

// DDS_PIXELFORMAT structure
int px_dwSize = imageInput.readInt(); // [76,79]

header.pixelFormatFlags = imageInput.readInt(); // [80,83]
header.fourCC = imageInput.readInt(); // [84,87]
header.bitCount = imageInput.readInt(); // [88,91]
header.redMask = imageInput.readInt(); // [92,95]
header.greenMask = imageInput.readInt(); // [96,99]
header.blueMask = imageInput.readInt(); // [100,103]
header.alphaMask = imageInput.readInt(); // [104,107]

int dwCaps = imageInput.readInt(); // [108,111]
int dwCaps2 = imageInput.readInt(); // [112,115]
int dwCaps3 = imageInput.readInt(); // [116,119]
int dwCaps4 = imageInput.readInt(); // [120,123]

int dwReserved2 = imageInput.readInt(); // [124,127]

return header;
}

private void addDimensions(int width, int height) {
dimensions = new Dimension[getMipMapCount()];

int w = width;
int h = height;
for (int i = 0; i < getMipMapCount(); i++) {
dimensions[i] = new Dimension(w, h);
w /= 2;
h /= 2;
}
}

private boolean getFlag(int mask) {
return (flags & mask) != 0;
}

public int getWidth(int imageIndex) {
int lim = dimensions[imageIndex].width;
return (lim <= 0) ? 1 : lim;
}

public int getHeight(int imageIndex) {
int lim = dimensions[imageIndex].height;
return (lim <= 0) ? 1 : lim;
}

public int getMipMapCount() {
// 0 = (unused) or 1 = (1 level), but still only one 'base' image
return (mipMapCount == 0) ? 1 : mipMapCount;
}

public int getAlphaMask() {
return alphaMask;
}

public int getBitCount() {
return bitCount;
}

public int getBlueMask() {
return blueMask;
}

public int getFlags() {
return flags;
}

public int getFourCC() {
return fourCC;
}

public int getGreenMask() {
return greenMask;
}

public int getPixelFormatFlags() {
return pixelFormatFlags;
}

public int getRedMask() {
return redMask;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package com.twelvemonkeys.imageio.plugins.dds;

import com.twelvemonkeys.imageio.ImageReaderBase;
import com.twelvemonkeys.imageio.util.ImageTypeSpecifiers;

import javax.imageio.ImageIO;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.spi.ImageReaderSpi;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

public final class DDSImageReader extends ImageReaderBase {

private DDSHeader header;

public DDSImageReader(final ImageReaderSpi provider) {
super(provider);
}

@Override
protected void resetMembers() {
header = null;
}

@Override
public int getWidth(final int imageIndex) throws IOException {
checkBounds(imageIndex);
readHeader();

return header.getWidth(imageIndex);
}

@Override
public int getHeight(int imageIndex) throws IOException {
checkBounds(imageIndex);
readHeader();

return header.getHeight(imageIndex);
}

@Override
public int getNumImages(final boolean allowSearch) throws IOException {
readHeader();

return header.getMipMapCount();
}

@Override
public ImageTypeSpecifier getRawImageType(int imageIndex) throws IOException {
checkBounds(imageIndex);
readHeader();

return ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_INT_ARGB);
}

@Override
public Iterator<ImageTypeSpecifier> getImageTypes(int imageIndex) throws IOException {
return Collections.singletonList(getRawImageType(imageIndex)).iterator();
}

@Override
public BufferedImage read(int imageIndex, ImageReadParam param) throws IOException {
checkBounds(imageIndex);
readHeader();

processImageStarted(imageIndex);

DDSReader dds = new DDSReader(header);
int[] pixels = dds.read(imageInput, imageIndex);

int width = getWidth(imageIndex);
int height = getHeight(imageIndex);

BufferedImage destination = getDestination(param, getImageTypes(imageIndex), width, height);
destination.setRGB(0, 0, width, height, pixels, 0, width);

// TODO: break read into raster line and add progress and abort checks
processImageProgress(100f);
if (abortRequested()) {
processReadAborted();
}

processImageComplete();

return destination;
}

private void readHeader() throws IOException {
if (header == null) {
imageInput.setByteOrder(ByteOrder.LITTLE_ENDIAN);
header = DDSHeader.read(imageInput);

imageInput.flushBefore(imageInput.getStreamPosition());
}

imageInput.seek(imageInput.getFlushedPosition());
}

public static void main(final String[] args) throws IOException {

String parentDir = "imageio/imageio-dds/src/test/resources/dds";

List<File> testFiles = new ArrayList<>();
testFiles.add(new File(parentDir, "dds_A1R5G5B5.dds"));
testFiles.add(new File(parentDir, "dds_A1R5G5B5_mipmap.dds"));
testFiles.add(new File(parentDir, "dds_A4R4G4B4.dds"));
testFiles.add(new File(parentDir, "dds_A4R4G4B4_mipmap.dds"));
testFiles.add(new File(parentDir, "dds_A8B8G8R8.dds"));
testFiles.add(new File(parentDir, "dds_A8B8G8R8_mipmap.dds"));
testFiles.add(new File(parentDir, "dds_A8R8G8B8.dds"));
testFiles.add(new File(parentDir, "dds_A8R8G8B8_mipmap.dds"));
testFiles.add(new File(parentDir, "dds_DXT1.dds"));
testFiles.add(new File(parentDir, "dds_DXT1_mipmap.dds"));
testFiles.add(new File(parentDir, "dds_DXT2.dds"));
testFiles.add(new File(parentDir, "dds_DXT2_mipmap.dds"));
testFiles.add(new File(parentDir, "dds_DXT3.dds"));
testFiles.add(new File(parentDir, "dds_DXT3_mipmap.dds"));
testFiles.add(new File(parentDir, "dds_DXT4.dds"));
testFiles.add(new File(parentDir, "dds_DXT4_mipmap.dds"));
testFiles.add(new File(parentDir, "dds_DXT5.dds"));
testFiles.add(new File(parentDir, "dds_DXT5_mipmap.dds"));
testFiles.add(new File(parentDir, "dds_R5G6B5.dds"));
testFiles.add(new File(parentDir, "dds_R5G6B5_mipmap.dds"));
testFiles.add(new File(parentDir, "dds_R8G8B8.dds"));
testFiles.add(new File(parentDir, "dds_R8G8B8_mipmap.dds"));
testFiles.add(new File(parentDir, "dds_X1R5G5B5.dds"));
testFiles.add(new File(parentDir, "dds_X1R5G5B5_mipmap.dds"));
testFiles.add(new File(parentDir, "dds_X4R4G4B4.dds"));
testFiles.add(new File(parentDir, "dds_X4R4G4B4_mipmap.dds"));
testFiles.add(new File(parentDir, "dds_X8B8G8R8.dds"));
testFiles.add(new File(parentDir, "dds_X8B8G8R8_mipmap.dds"));
testFiles.add(new File(parentDir, "dds_X8R8G8B8.dds"));
testFiles.add(new File(parentDir, "dds_X8R8G8B8_mipmap.dds"));

for (File file : testFiles) {
BufferedImage image = ImageIO.read(file);
showIt(image, file.getName());
}

}
}
Loading

0 comments on commit 3c01071

Please sign in to comment.