Compare commits

...

14 Commits

Author SHA1 Message Date
Harald Kuhr 75d7d5cdef Using new sequence support in DDSImageWriter
+ some minor bonus clean-up
2026-03-13 11:40:06 +01:00
Harald Kuhr 177eec06cc Using new sequence support in exising writers. 2026-03-13 11:38:28 +01:00
Harald Kuhr dc3d77ad95 New class for simpler sequence write support. 2026-03-13 11:37:47 +01:00
Harald Kuhr 263fb75d1d DDS cleanup (#1262)
* Refactorings and code clean-up
* Major rework/standardization:
 * DDSEncoderType, DX10DXGIFormat merged with DDSType for a single way to describe a DDS format
 * Added constants for DXGI formats
 * DDSImageWriteParam is now mutable and supports standard way of setting compression type
 * DDSImageMetadata now supports more of the format
 Performance:
 * DDSReader now use seek() to jump to correct mipmap instead of reading all bytes
 * DDSImageWriter now uses getTile(0, 0) instead of getData() for better performance
* Fix JavaDoc 🎉
* Sonar issues + roll back accidental check-in
* More clean-up: Removed optional flags from param, header size validation, metadata now reports compresion as lossy
* More clean-up: Now keeps stream byte order consistent (LE), support for Raster, more tests
* Mipmap support using ImageIO sequence API
* Added raster write test
+ fixed a small issue for PAM
* Sonar issues
2026-03-11 21:09:26 +01:00
dependabot[bot] e61ec45737 Bump org.apache.maven.plugins:maven-shade-plugin from 3.6.1 to 3.6.2
Bumps [org.apache.maven.plugins:maven-shade-plugin](https://github.com/apache/maven-shade-plugin) from 3.6.1 to 3.6.2.
- [Release notes](https://github.com/apache/maven-shade-plugin/releases)
- [Commits](https://github.com/apache/maven-shade-plugin/compare/maven-shade-plugin-3.6.1...maven-shade-plugin-3.6.2)

---
updated-dependencies:
- dependency-name: org.apache.maven.plugins:maven-shade-plugin
  dependency-version: 3.6.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-10 16:03:03 +01:00
dependabot[bot] 6e063f263c Bump actions/upload-artifact from 6.0.0 to 7.0.0 in /.github/workflows
---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-10 16:02:33 +01:00
dependabot[bot] e2cf529000 Bump org.apache.maven.plugins:maven-resources-plugin from 3.4.0 to 3.5.0
Bumps [org.apache.maven.plugins:maven-resources-plugin](https://github.com/apache/maven-resources-plugin) from 3.4.0 to 3.5.0.
- [Release notes](https://github.com/apache/maven-resources-plugin/releases)
- [Commits](https://github.com/apache/maven-resources-plugin/compare/v3.4.0...maven-resources-plugin-3.5.0)

---
updated-dependencies:
- dependency-name: org.apache.maven.plugins:maven-resources-plugin
  dependency-version: 3.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-10 14:21:22 +01:00
dependabot[bot] 9d1c418d8d Bump mikepenz/action-junit-report in /.github/workflows
Bumps [mikepenz/action-junit-report](https://github.com/mikepenz/action-junit-report) from 6.2.0 to 6.3.1.
- [Release notes](https://github.com/mikepenz/action-junit-report/releases)
- [Commits](https://github.com/mikepenz/action-junit-report/compare/74626db7353a25a20a72816467ebf035f674c5f8...49b2ca06f62aa7ef83ae6769a2179271e160d8e4)

---
updated-dependencies:
- dependency-name: mikepenz/action-junit-report
  dependency-version: 6.3.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-10 14:20:41 +01:00
KhanhTypo a7a4445ce8 [DDS] Adding Block Compression 1 -> 5 Encoding Support (#1237)
* dds dxt10 support, with some certain supported DXGI Formats only.
* expand the supporting range for some DX10 DXGI Format in the DXGI_FORMAT enumeration
* readability and maintainability fixes, adding DXT10 test cases.
* java.awt.* -> java.awt.Dimension
* DDS header & BC1 writer
* BC4 Writer
* BC3 Writer
* BC1-5 writer support
* remove unused methods
* code fixes
* BC4 fix to resolve unwanted blocky effect.
* CI test fixes
* change bitflag setter functions
* temporary disable formats that does not have an encoder yet.
* resolving SonaQube issues.
2026-03-04 10:35:40 +01:00
Harald Kuhr 4c1b268325 Javadoc cleanup (#1255)
* #1234: Fixed JavaDoc for Java 21 + JavaDoc verification step
* #1234: Simplify build matrix
* #1234: Javadoc in parallell
2026-03-04 10:35:04 +01:00
dependabot[bot] e3c3f640a4 Bump github/codeql-action from 4.32.2 to 4.32.4 in /.github/workflows
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.32.2 to 4.32.4.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2...89a39a4e59826350b863aa6b6252a07ad50cf83e)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.32.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-02 10:01:45 +01:00
dependabot[bot] ecc938a666 Bump org.apache.maven.plugins:maven-surefire-plugin from 3.5.4 to 3.5.5
Bumps [org.apache.maven.plugins:maven-surefire-plugin](https://github.com/apache/maven-surefire) from 3.5.4 to 3.5.5.
- [Release notes](https://github.com/apache/maven-surefire/releases)
- [Commits](https://github.com/apache/maven-surefire/compare/surefire-3.5.4...surefire-3.5.5)

---
updated-dependencies:
- dependency-name: org.apache.maven.plugins:maven-surefire-plugin
  dependency-version: 3.5.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-25 19:23:46 +01:00
dependabot[bot] 25fdfb1947 Bump org.apache.maven.plugins:maven-surefire-report-plugin
Bumps [org.apache.maven.plugins:maven-surefire-report-plugin](https://github.com/apache/maven-surefire) from 3.5.4 to 3.5.5.
- [Release notes](https://github.com/apache/maven-surefire/releases)
- [Commits](https://github.com/apache/maven-surefire/compare/surefire-3.5.4...surefire-3.5.5)

---
updated-dependencies:
- dependency-name: org.apache.maven.plugins:maven-surefire-report-plugin
  dependency-version: 3.5.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-25 19:23:19 +01:00
dependabot[bot] 0c05918d8a Bump junit.jupiter.version from 5.14.2 to 5.14.3
Bumps `junit.jupiter.version` from 5.14.2 to 5.14.3.

Updates `org.junit.jupiter:junit-jupiter-api` from 5.14.2 to 5.14.3
- [Release notes](https://github.com/junit-team/junit-framework/releases)
- [Commits](https://github.com/junit-team/junit-framework/compare/r5.14.2...r5.14.3)

Updates `org.junit.jupiter:junit-jupiter-params` from 5.14.2 to 5.14.3
- [Release notes](https://github.com/junit-team/junit-framework/releases)
- [Commits](https://github.com/junit-team/junit-framework/compare/r5.14.2...r5.14.3)

---
updated-dependencies:
- dependency-name: org.junit.jupiter:junit-jupiter-api
  dependency-version: 5.14.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.junit.jupiter:junit-jupiter-params
  dependency-version: 5.14.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-23 10:26:02 +01:00
44 changed files with 2604 additions and 420 deletions
+28 -12
View File
@@ -17,7 +17,10 @@ jobs:
fail-fast: false
matrix:
os: [ ubuntu-latest, windows-latest, macos-latest ]
java: [ 11, 17, 21, 25 ]
java: [ 8, 11, 17, 21, 25 ]
exclude:
- os: macos-latest
java: 8
runs-on: ${{ matrix.os }}
permissions:
checks: write
@@ -32,19 +35,15 @@ jobs:
- name: Run Tests
run: mvn --batch-mode --no-transfer-progress test
- name: Publish Test Report
uses: mikepenz/action-junit-report@74626db7353a25a20a72816467ebf035f674c5f8 # v5
uses: mikepenz/action-junit-report@49b2ca06f62aa7ef83ae6769a2179271e160d8e4 # v5
if: ${{ !cancelled() }}
with:
report_paths: "**/target/surefire-reports/TEST*.xml"
check_name: Unit Test Results for OpenJDK ${{ matrix.java }} on ${{ matrix.os }}
test-jdk8:
name: Test OpenJDK 8 on ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ ubuntu-latest, windows-latest, macos-14 ]
runs-on: ${{ matrix.os }}
test-jdk8-macos:
name: Test OpenJDK 8 on macos-14
runs-on: macos-14
permissions:
checks: write
steps:
@@ -58,7 +57,7 @@ jobs:
- name: Run Tests
run: mvn --batch-mode --no-transfer-progress test
- name: Publish Test Report
uses: mikepenz/action-junit-report@74626db7353a25a20a72816467ebf035f674c5f8 # v5
uses: mikepenz/action-junit-report@49b2ca06f62aa7ef83ae6769a2179271e160d8e4 # v5
if: ${{ !cancelled() }}
with:
report_paths: "**/target/surefire-reports/TEST*.xml"
@@ -92,15 +91,32 @@ jobs:
- name: Run Tests
run: mvn --batch-mode --no-transfer-progress test
- name: Publish Test Report
uses: mikepenz/action-junit-report@74626db7353a25a20a72816467ebf035f674c5f8 # v5
uses: mikepenz/action-junit-report@49b2ca06f62aa7ef83ae6769a2179271e160d8e4 # v5
if: ${{ !cancelled() }}
with:
report_paths: "**/target/surefire-reports/TEST*.xml"
check_name: Unit Test Results for Oracle JDK 8 with KCMS=${{ matrix.kcms }}
javadoc:
name: Build JavaDoc on OpenJDK ${{ matrix.java }}
runs-on: ubuntu-latest
strategy:
matrix:
java: [8, 11, 25 ] # We only need a few versions here
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
distribution: 'temurin'
java-version: ${{ matrix.java }}
java-package: jdk
cache: 'maven'
- name: Create JavaDoc
run: mvn --batch-mode --no-transfer-progress -DskipTests package javadoc:javadoc
release:
name: Deploy
needs: [ test, test-jdk8, test-oracle ]
needs: [ test, test-jdk8-macos, test-oracle, javadoc ]
if: github.ref == 'refs/heads/master' # only perform on latest master
runs-on: ubuntu-latest
steps:
+3 -3
View File
@@ -37,7 +37,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2
uses: github/codeql-action/init@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -51,7 +51,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2
uses: github/codeql-action/autobuild@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
# ℹ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@@ -64,6 +64,6 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2
uses: github/codeql-action/analyze@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
with:
category: "/language:${{matrix.language}}"
+2 -2
View File
@@ -49,7 +49,7 @@ jobs:
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
# format to the repository Actions tab.
- name: "Upload artifact"
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: SARIF file
path: results.sarif
@@ -57,6 +57,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2
uses: github/codeql-action/upload-sarif@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
with:
sarif_file: results.sarif
@@ -47,10 +47,10 @@ import java.util.Random;
* |3|5|1|
* - - -->
* </p>
* <table border="1" cellpadding="4" cellspacing="0">
* <table border="1">
* <caption>Floyd-Steinberg error-diffusion weights</caption>
* <tr><td bgcolor="#000000">&nbsp;</td><td class="TableHeadingColor"
* align="center">x</td><td>7/16</td></tr>
* <tr><td style="background:#000000">&nbsp;</td><td class="TableHeadingColor"
* style="text-align:center">x</td><td>7/16</td></tr>
* <tr><td>3/16</td><td>5/16</td><td>1/16</td></tr>
* </table>
* <p>
@@ -162,7 +162,7 @@ public final class ImageUtil {
/**
* The sharpen kernel. Uses the following 3 by 3 matrix:
* <table border="1" cellspacing="0">
* <table border="1">
* <caption>Sharpen Kernel Matrix</caption>
* <tr><td>0.0</td><td>-0.3</td><td>0.0</td></tr>
* <tr><td>-0.3</td><td>2.2</td><td>-0.3</td></tr>
@@ -1078,7 +1078,7 @@ public final class ImageUtil {
/**
* Sharpens an image using a convolution matrix.
* The sharpen kernel used, is defined by the following 3 by 3 matrix:
* <table border="1" cellspacing="0">
* <table border="1">
* <caption>Sharpen Kernel Matrix</caption>
* <tr><td>0.0</td><td>-0.3</td><td>0.0</td></tr>
* <tr><td>-0.3</td><td>2.2</td><td>-0.3</td></tr>
@@ -1100,7 +1100,7 @@ public final class ImageUtil {
/**
* Sharpens an image using a convolution matrix.
* The sharpen kernel used, is defined by the following 3 by 3 matrix:
* <table border="1" cellspacing="0">
* <table border="1">
* <caption>Sharpen Kernel Matrix</caption>
* <tr><td>0.0</td><td>-{@code pAmount}</td><td>0.0</td></tr>
* <tr><td>-{@code pAmount}</td>
@@ -1765,12 +1765,11 @@ public final class StringUtil {
* expression.
* <p>
* An invocation of this method of the form
* <tt>matches(<i>str</i>, <i>regex</i>)</tt> yields exactly the
* {@code matches(str, regex)} yields exactly the
* same result as the expression
* </p>
* <blockquote><tt> {@link Pattern}.
* {@link Pattern#matches(String, CharSequence) matches}
* (<i>regex</i>, <i>str</i>)</tt></blockquote>
* <blockquote>{@link Pattern}.
* {@link Pattern#matches(String, CharSequence) matches(regex, str)}</blockquote>
*
* @param pString the string
* @param pRegex the regular expression to which this string is to be matched
@@ -1789,16 +1788,14 @@ public final class StringUtil {
* regular expression with the given pReplacement.
* <p>
* An invocation of this method of the form
* <tt>
* replaceFirst(<i>str</i>, <i>regex</i>, <i>repl</i>)
* </tt>
* {@code replaceFirst(str, regex, repl)}
* yields exactly the same result as the expression:
* </p>
* <blockquote><tt>
* {@link Pattern}.{@link Pattern#compile(String) compile}(<i>regex</i>).
* {@link Pattern#matcher matcher}(<i>str</i>).
* {@link java.util.regex.Matcher#replaceFirst replaceFirst}(<i>repl</i>)
* </tt></blockquote>
* <blockquote>
* {@link Pattern#compile(String) Pattern.compile(regex)}
* {@link Pattern#matcher .matcher(str)}
* {@link java.util.regex.Matcher#replaceFirst .replaceFirst(repl)}
* </blockquote>
*
* @param pString the string
* @param pRegex the regular expression to which this string is to be matched
@@ -1817,14 +1814,14 @@ public final class StringUtil {
* regular expression with the given pReplacement.
* <p>
* An invocation of this method of the form
* <tt>replaceAll(<i>str</i>, <i>pRegex</i>, <i>repl</i>)</tt>
* {@code replaceAll(str, pRegex, repl)}
* yields exactly the same result as the expression
* </p>
* <blockquote><tt>
* {@link Pattern}.{@link Pattern#compile(String) compile}(<i>pRegex</i>).
* {@link Pattern#matcher matcher}(<i>str</i>{@code ).
* {@link java.util.regex.Matcher#replaceAll replaceAll}(}<i>repl</i>{@code )}
* </tt></blockquote>
* <blockquote>
* {@link Pattern#compile(String) Pattern.compile(pRegex)}
* {@link Pattern#matcher .matcher(str)}
* {@link java.util.regex.Matcher#replaceAll .replaceAll(repl)}
* </blockquote>
*
* @param pString the string
* @param pRegex the regular expression to which this string is to be matched
@@ -1862,12 +1859,12 @@ public final class StringUtil {
* </p>
* <p>
* An invocation of this method of the form
* <tt>split(<i>str</i>, <i>regex</i>, <i>n</i>)</tt>
* {@code split(str, regex, n)}
* yields the same result as the expression:
* </p>
* <blockquote>{@link Pattern}.
* {@link Pattern#compile(String) compile}<tt>(<i>regex</i>).
* {@link Pattern#split(CharSequence,int) split}(<i>str</i>, <i>n</i>)</tt>
* {@link Pattern#compile(String) compile(regex)}.
* {@link Pattern#split(CharSequence,int) split(str, n)}
* </blockquote>
*
* @param pString the string
+1 -1
View File
@@ -21,7 +21,7 @@
</modules>
<properties>
<junit.jupiter.version>5.14.2</junit.jupiter.version>
<junit.jupiter.version>5.14.3</junit.jupiter.version>
</properties>
<dependencyManagement>
+1 -1
View File
@@ -14,7 +14,7 @@
</description>
<properties>
<junit.jupiter.version>5.14.2</junit.jupiter.version>
<junit.jupiter.version>5.14.3</junit.jupiter.version>
</properties>
@@ -32,6 +32,7 @@ package com.twelvemonkeys.imageio.plugins.bmp;
import com.twelvemonkeys.imageio.stream.SubImageOutputStream;
import com.twelvemonkeys.imageio.util.ProgressListenerBase;
import com.twelvemonkeys.imageio.util.SequenceSupport;
import javax.imageio.IIOException;
import javax.imageio.IIOImage;
@@ -64,7 +65,7 @@ public final class ICOImageWriter extends DIBImageWriter {
private static final int ICO_MAX_DIMENSION = 256;
private static final int INITIAL_ENTRY_COUNT = 8;
private int sequenceIndex = -1;
private final SequenceSupport sequence = new SequenceSupport();
private ImageWriter pngDelegate;
@@ -74,7 +75,7 @@ public final class ICOImageWriter extends DIBImageWriter {
@Override
protected void resetMembers() {
sequenceIndex = -1;
sequence.reset();
if (pngDelegate != null) {
pngDelegate.dispose();
@@ -107,16 +108,12 @@ public final class ICOImageWriter extends DIBImageWriter {
@Override
public void prepareWriteSequence(final IIOMetadata streamMetadata) throws IOException {
assertOutput();
if (sequenceIndex >= 0) {
throw new IllegalStateException("writeSequence already started");
}
sequence.start();
writeICOHeader();
// Count: Needs to be updated for each new image
imageOutput.writeShort(0);
sequenceIndex = 0;
// TODO: Allow passing the initial size of the directory in the stream metadata?
// - as this is much more efficient than growing...
@@ -130,27 +127,19 @@ public final class ICOImageWriter extends DIBImageWriter {
@Override
public void endWriteSequence() {
assertOutput();
if (sequenceIndex < 0) {
throw new IllegalStateException("prepareWriteSequence not called");
}
sequenceIndex = -1;
sequence.end();
}
@Override
public void writeToSequence(final IIOImage image, final ImageWriteParam param) throws IOException {
assertOutput();
if (sequenceIndex < 0) {
throw new IllegalStateException("prepareWriteSequence not called");
}
int imageIndex = sequence.advance();
if (image.hasRaster()) {
throw new UnsupportedOperationException("Raster not supported");
}
if (sequenceIndex >= INITIAL_ENTRY_COUNT) {
if (imageIndex >= INITIAL_ENTRY_COUNT) {
growIfNecessary();
}
@@ -172,7 +161,7 @@ public final class ICOImageWriter extends DIBImageWriter {
// Uncompressed, RLE4/RLE8 or PNG compressed
boolean pngCompression = param != null && "BI_PNG".equals(param.getCompressionType());
processImageStarted(sequenceIndex);
processImageStarted(imageIndex);
if (pngCompression) {
// NOTE: Embedding a PNG in a ICO is slightly different than a BMP with BI_PNG compression,
@@ -198,17 +187,15 @@ public final class ICOImageWriter extends DIBImageWriter {
// Update count
imageOutput.seek(4);
imageOutput.writeShort(sequenceIndex + 1);
imageOutput.writeShort(imageIndex + 1);
// Write entry
int entryPosition = 6 + sequenceIndex * ENTRY_SIZE;
int entryPosition = 6 + imageIndex * ENTRY_SIZE;
imageOutput.seek(entryPosition);
long size = nextPosition - imageOffset;
writeEntry(width, height, colorModel, (int) size, (int) imageOffset);
sequenceIndex++;
imageOutput.seek(nextPosition);
}
@@ -265,7 +252,7 @@ public final class ICOImageWriter extends DIBImageWriter {
pngDelegate.addIIOWriteWarningListener(new IIOWriteWarningListener() {
@Override
public void warningOccurred(ImageWriter source, int imageIndex, String warning) {
processWarningOccurred(sequenceIndex, warning);
processWarningOccurred(sequence.current(), warning);
}
});
}
@@ -23,6 +23,7 @@ import static com.twelvemonkeys.lang.Validate.notNull;
* {@link ImageTypeSpecifier}.
* Other values or overrides may be specified using the builder.
*
* @see <a href="https://docs.oracle.com/en/java/javase/11/docs/api/java.desktop/javax/imageio/metadata/doc-files/standard_metadata.html">Standard (Plug-in Neutral) Metadata Format Specification</a>
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
*/
public class StandardImageMetadataSupport extends AbstractMetadata {
@@ -79,11 +80,11 @@ public class StandardImageMetadataSupport extends AbstractMetadata {
textEntries = builder.textEntries;
}
public static Builder builder(ImageTypeSpecifier type) {
protected static Builder builder(ImageTypeSpecifier type) {
return new Builder(type);
}
public static class Builder {
protected static class Builder {
private final ImageTypeSpecifier type;
private ColorSpaceType colorSpaceType;
private boolean blackIsZero = true;
@@ -35,6 +35,8 @@ import com.twelvemonkeys.lang.Validate;
import javax.imageio.IIOParam;
import javax.imageio.ImageIO;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageWriteParam;
import javax.imageio.spi.IIOServiceProvider;
import javax.imageio.spi.ServiceRegistry;
import javax.imageio.stream.ImageInputStream;
@@ -45,7 +47,9 @@ import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Objects;
import java.util.SortedSet;
import java.util.TreeSet;
@@ -68,7 +72,7 @@ public final class IIOUtil {
*/
public static InputStream createStreamAdapter(final ImageInputStream pStream) {
// 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));
}
@@ -82,7 +86,7 @@ public final class IIOUtil {
*/
public static InputStream createStreamAdapter(final ImageInputStream pStream, final long pLength) {
// 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));
}
@@ -359,4 +363,115 @@ public final class IIOUtil {
System.arraycopy(srcRow, srcPos + x, destRow, destPos + x / samplePeriod, pixelStride);
}
}
/**
* Copies all the standard param values from source to destination.
* <p>
* Typical use (in some imaginary {@code FooImageWriter} class):
* </p>
*
* <pre>
* ImageWriteParam param = ...
* FooImageWriteparam fooParam = param instanceof FooImageWriteParam
* ? (FooImageWriteParam) param
* : copyStandardParams(param, getDefaultWriteParam());
* </pre>
*
* May also be useful for {@code ImageReader}s that delegate reading to other plugins
* (like a TIFF plugin delegating JPEG format decoding to a {@code JPEGImageReader}).
*
* @param source the source parameter, may be {@code null}
* @param destination the destination parameter
* @return destination
*
* @param <T> the plugin specific subclass of {@code IIOParam}
*
* @throws NullPointerException if destination is {@code null}
*/
public static <T extends IIOParam> T copyStandardParams(IIOParam source, T destination) {
Objects.requireNonNull(destination);
Validate.isTrue(source != destination, "source must be different from destination");
if (source != null) {
copyIIOParams(source, destination);
// TODO: API & usage... Is it ever useful to copy from a read to a write param or vice versa?
// If not, maybe throw an IllegalArgumentException instead
if (source instanceof ImageReadParam && destination instanceof ImageReadParam) {
copyImageReadParams((ImageReadParam) source, (ImageReadParam) destination);
}
if (source instanceof ImageWriteParam && destination instanceof ImageWriteParam) {
copyImageWriteParams((ImageWriteParam) source, (ImageWriteParam) destination);
}
}
return destination;
}
private static void copyImageWriteParams(ImageWriteParam source, ImageWriteParam destination) {
// TODO: Usage... It's very unlikely that compression settings of one plugin is compatible with another...
// Is the the below useful?
// Also, is it okay to just silently ignore settings from one format that isn't compatible with another?
// Quirky API, we can't query for compression mode, unless source.canWriteCompressed is true...
if (source.canWriteCompressed() && destination.canWriteCompressed()) {
int compressionMode = source.getCompressionMode();
destination.setCompressionMode(compressionMode);
if (compressionMode == ImageWriteParam.MODE_EXPLICIT
&& source.getCompressionType() != null
&& Arrays.asList(destination.getCompressionTypes()).contains(source.getCompressionType())) {
destination.setCompressionType(source.getCompressionType());
destination.setCompressionQuality(source.getCompressionQuality());
}
}
if (source.canWriteProgressive() && destination.canWriteProgressive()) {
destination.setProgressiveMode(source.getProgressiveMode());
}
if (source.canWriteTiles() && destination.canWriteTiles()) {
int tilingMode = source.getTilingMode();
destination.setTilingMode(tilingMode);
if (tilingMode == ImageWriteParam.MODE_EXPLICIT) {
// TODO: What if source can offset (and has offsets) and dest can't? Is it ok to just ignore the setting?
boolean canWriteOffsetTiles = source.canOffsetTiles() && destination.canOffsetTiles();
destination.setTiling(
source.getTileWidth(), source.getTileHeight(),
canWriteOffsetTiles ? source.getTileGridXOffset() : 0,
canWriteOffsetTiles ? source.getTileGridYOffset() : 0
);
}
}
}
private static void copyImageReadParams(ImageReadParam source, ImageReadParam destination) {
destination.setDestination(source.getDestination());
destination.setDestinationBands(source.getDestinationBands());
if (destination.canSetSourceRenderSize()) {
destination.setSourceRenderSize(source.getSourceRenderSize());
}
destination.setSourceProgressivePasses(
source.getSourceMinProgressivePass(),
source.getSourceMaxProgressivePass()
);
}
private static void copyIIOParams(IIOParam source, IIOParam destination) {
destination.setController(source.getController());
destination.setSourceSubsampling(
source.getSourceXSubsampling(), source.getSourceYSubsampling(),
source.getSubsamplingXOffset(), source.getSubsamplingYOffset()
);
destination.setSourceRegion(source.getSourceRegion());
destination.setSourceBands(source.getSourceBands());
destination.setDestinationOffset(source.getDestinationOffset());
destination.setDestinationType(source.getDestinationType());
}
}
@@ -0,0 +1,115 @@
/*
* Copyright (c) 2026, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.imageio.util;
import javax.imageio.IIOImage;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.metadata.IIOMetadata;
/**
* A tiny utility class that keeps state for sequences.
* For use by {@code ImageWriter} implementations that supports sequence (multiple images in same stream).
*
* @see ImageWriter#canWriteSequence()
*/
public final class SequenceSupport {
// Initial state, no sequence running
private int index = -1;
/**
* Resets the sequence to initial state, regardless of the current sequence state.
*/
public void reset() {
index = -1;
}
/**
* Starts a new sequence.
*
* @throws IllegalStateException if a sequence is already running.
* @see ImageWriter#prepareWriteSequence(IIOMetadata)
*/
public void start() {
if (index >= 0) {
throw new IllegalStateException("prepareWriteSequence already invoked");
}
index = 0;
}
/**
* Advances the current sequence.
*
* @return the current sequence index.
* @throws IllegalStateException if a sequence is not running.
* @see ImageWriter#writeToSequence(IIOImage, ImageWriteParam)
*/
public int advance() {
if (index < 0) {
throw new IllegalStateException("prepareWriteSequence not invoked");
}
return index++;
}
/**
* Gets the current sequence index.
*
* @return the current sequence index, or {@code -1} if a sequence is not running.
*/
public int current() {
// This method does not throw IllegalStateException, to allow
// ImageWriters to use the index in cases that may or may not
// happen "inside" a sequence.
// I'm not entirely sure if this is a good idea...
return index;
}
/**
* Ends the current sequence.
* The sequence is reset to initial state, and a new sequence may be started.
*
* @return the current (last) sequence index
* @throws IllegalStateException if a sequence is not running.
* @see ImageWriter#endWriteSequence()
*/
public int end() {
if (index < 0) {
throw new IllegalStateException("prepareWriteSequence not invoked");
}
int last = index;
index = -1;
return last;
}
}
@@ -1,8 +1,19 @@
package com.twelvemonkeys.imageio.util;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.ImageWriteParam;
import javax.imageio.plugins.bmp.BMPImageWriteParam;
import javax.imageio.plugins.jpeg.JPEGImageReadParam;
/**
* IIOUtilTest
*/
@@ -204,4 +215,222 @@ public class IIOUtilTest {
private int divCeil(int numerator, int denominator) {
return (numerator + denominator - 1) / denominator;
}
@Test
void copyStandardParamsDestinationNull() {
ImageReadParam param = new ImageReadParam();
assertThrows(NullPointerException.class, () -> IIOUtil.copyStandardParams(null, null));
assertThrows(NullPointerException.class, () -> IIOUtil.copyStandardParams(param, null));
}
@Test
void copyStandardParamsSame() {
ImageReadParam param = new ImageReadParam();
assertThrows(IllegalArgumentException.class, () -> IIOUtil.copyStandardParams(param, param));
}
@Test
void copyStandardParamsSourceNull() {
ImageReadParam param = new ImageReadParam() {
@Override
public void setSourceRegion(Rectangle sourceRegion) {
fail("Should not be invoked");
}
};
assertSame(param, IIOUtil.copyStandardParams(null, param));
}
@Test
void copyStandardParamsImageReadParam() {
int sourceXSubsampling = 3;
int sourceYSubsampling = 4;
int subsamplingXOffset = 1;
int subsamplingYOffset = 2;
Rectangle sourceRegion = new Rectangle(1, 2, 42, 43);
int[] sourceBands = { 0, 1, 2 };
Point destinationOffset = new Point(7, 9);
int[] destinationBands = { 2, 1, 0 };
ImageReadParam sourceParam = new ImageReadParam();
sourceParam.setSourceRegion(sourceRegion);
sourceParam.setSourceSubsampling(sourceXSubsampling, sourceYSubsampling, subsamplingXOffset, subsamplingYOffset);
sourceParam.setSourceBands(sourceBands);
sourceParam.setDestinationOffset(destinationOffset);
sourceParam.setDestinationBands(destinationBands);
JPEGImageReadParam jpegParam = IIOUtil.copyStandardParams(sourceParam, new JPEGImageReadParam());
assertEquals(sourceRegion, jpegParam.getSourceRegion());
assertEquals(sourceXSubsampling, jpegParam.getSourceXSubsampling());
assertEquals(sourceYSubsampling, jpegParam.getSourceYSubsampling());
assertEquals(subsamplingXOffset, jpegParam.getSubsamplingXOffset());
assertEquals(subsamplingYOffset, jpegParam.getSubsamplingYOffset());
assertArrayEquals(sourceBands, jpegParam.getSourceBands());
assertEquals(destinationOffset, jpegParam.getDestinationOffset());
assertArrayEquals(destinationBands, jpegParam.getDestinationBands());
}
@Test
void copyStandardParamsImageReadParamDestination() {
// Destination and destination type is mutually exclusive
BufferedImage destination = new BufferedImage(2, 2, BufferedImage.TYPE_INT_ARGB);
ImageReadParam sourceParam = new ImageReadParam();
sourceParam.setDestination(destination);
assertEquals(destination, IIOUtil.copyStandardParams(sourceParam, new JPEGImageReadParam()).getDestination());
}
@Test
void copyStandardParamsImageReadParamDestinationType() {
// Destination and destination type is mutually exclusive
ImageTypeSpecifier destinationType = ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_BYTE_GRAY);
ImageReadParam sourceParam = new ImageReadParam();
sourceParam.setDestinationType(destinationType);
assertEquals(destinationType, IIOUtil.copyStandardParams(sourceParam, new JPEGImageReadParam()).getDestinationType());
}
@Test
void copyStandardParamsReadToWrite() {
int sourceXSubsampling = 3;
int sourceYSubsampling = 4;
int subsamplingXOffset = 1;
int subsamplingYOffset = 2;
Rectangle sourceRegion = new Rectangle(1, 2, 42, 43);
int[] sourceBands = { 0, 1, 2 };
Point destinationOffset = new Point(7, 9);
ImageWriteParam sourceParam = new ImageWriteParam(null);
sourceParam.setSourceRegion(sourceRegion);
sourceParam.setSourceSubsampling(sourceXSubsampling, sourceYSubsampling, subsamplingXOffset, subsamplingYOffset);
sourceParam.setSourceBands(sourceBands);
sourceParam.setDestinationOffset(destinationOffset);
JPEGImageReadParam jpegParam = IIOUtil.copyStandardParams(sourceParam, new JPEGImageReadParam());
assertEquals(sourceRegion, jpegParam.getSourceRegion());
assertEquals(sourceXSubsampling, jpegParam.getSourceXSubsampling());
assertEquals(sourceYSubsampling, jpegParam.getSourceYSubsampling());
assertEquals(subsamplingXOffset, jpegParam.getSubsamplingXOffset());
assertEquals(subsamplingYOffset, jpegParam.getSubsamplingYOffset());
assertArrayEquals(sourceBands, jpegParam.getSourceBands());
assertEquals(destinationOffset, jpegParam.getDestinationOffset());
assertNull(jpegParam.getDestinationBands()); // Only in read param
}
@Test
void copyStandardParamsImageWriteParam() {
int sourceXSubsampling = 3;
int sourceYSubsampling = 4;
int subsamplingXOffset = 1;
int subsamplingYOffset = 2;
Rectangle sourceRegion = new Rectangle(1, 2, 42, 43);
int[] sourceBands = { 0, 1, 2 };
Point destinationOffset = new Point(7, 9);
ImageWriteParam sourceParam = new ImageWriteParam(null);
sourceParam.setSourceRegion(sourceRegion);
sourceParam.setSourceSubsampling(sourceXSubsampling, sourceYSubsampling, subsamplingXOffset, subsamplingYOffset);
sourceParam.setSourceBands(sourceBands);
sourceParam.setDestinationOffset(destinationOffset);
BMPImageWriteParam fooParam = IIOUtil.copyStandardParams(sourceParam, new BMPImageWriteParam());
assertEquals(sourceRegion, fooParam.getSourceRegion());
assertEquals(sourceXSubsampling, fooParam.getSourceXSubsampling());
assertEquals(sourceYSubsampling, fooParam.getSourceYSubsampling());
assertEquals(subsamplingXOffset, fooParam.getSubsamplingXOffset());
assertEquals(subsamplingYOffset, fooParam.getSubsamplingYOffset());
assertArrayEquals(sourceBands, fooParam.getSourceBands());
assertEquals(destinationOffset, fooParam.getDestinationOffset());
}
@Test
void copyStandardParamsImageWriteParamEverything() {
int sourceXSubsampling = 3;
int sourceYSubsampling = 4;
int subsamplingXOffset = 1;
int subsamplingYOffset = 2;
Rectangle sourceRegion = new Rectangle(1, 2, 42, 43);
int[] sourceBands = { 0, 1, 2 };
Point destinationOffset = new Point(7, 9);
String compressionType = "Foo";
float quality = 0.42f;
ImageWriteParam sourceParam = new ImageWriteParam() {
{
canWriteProgressive = true;
canWriteTiles = true;
canOffsetTiles = true;
canWriteCompressed = true;
compressionTypes = new String[] { "Foo", "Bar" };
}
};
sourceParam.setSourceRegion(sourceRegion);
sourceParam.setSourceSubsampling(sourceXSubsampling, sourceYSubsampling, subsamplingXOffset, subsamplingYOffset);
sourceParam.setSourceBands(sourceBands);
sourceParam.setDestinationOffset(destinationOffset);
sourceParam.setProgressiveMode(ImageWriteParam.MODE_DEFAULT); // Default is COPY_FROM_METADATA...
sourceParam.setTilingMode(ImageWriteParam.MODE_EXPLICIT);
sourceParam.setTiling(1, 2, 3, 4);
sourceParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
sourceParam.setCompressionType(compressionType);
sourceParam.setCompressionQuality(quality);
FooImageWriteParam fooParam = IIOUtil.copyStandardParams(sourceParam, new FooImageWriteParam());
assertEquals(sourceRegion, fooParam.getSourceRegion());
assertEquals(sourceXSubsampling, fooParam.getSourceXSubsampling());
assertEquals(sourceYSubsampling, fooParam.getSourceYSubsampling());
assertEquals(subsamplingXOffset, fooParam.getSubsamplingXOffset());
assertEquals(subsamplingYOffset, fooParam.getSubsamplingYOffset());
assertArrayEquals(sourceBands, fooParam.getSourceBands());
assertEquals(destinationOffset, fooParam.getDestinationOffset());
assertEquals(ImageWriteParam.MODE_DEFAULT, fooParam.getProgressiveMode());
assertEquals(ImageWriteParam.MODE_EXPLICIT, fooParam.getTilingMode());
assertEquals(1, fooParam.getTileWidth());
assertEquals(2, fooParam.getTileHeight());
assertEquals(3, fooParam.getTileGridXOffset());
assertEquals(4, fooParam.getTileGridYOffset());
assertEquals(ImageWriteParam.MODE_EXPLICIT, fooParam.getCompressionMode());
assertEquals(compressionType, fooParam.getCompressionType());
assertEquals(quality, fooParam.getCompressionQuality());
}
// A basic param that supports "everything"
static class FooImageWriteParam extends ImageWriteParam {
FooImageWriteParam() {
canWriteProgressive = true;
canWriteTiles = true;
canOffsetTiles = true;
canWriteCompressed = true;
compressionType = "Unset";
compressionTypes = new String[] { "Bar", "Foo" };
}
}
}
@@ -34,6 +34,7 @@ import com.twelvemonkeys.imageio.stream.URLImageInputStreamSpi;
import org.mockito.InOrder;
import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
@@ -84,6 +85,7 @@ public abstract class ImageWriterAbstractTest<T extends ImageWriter> {
protected static BufferedImage drawSomething(final BufferedImage image) {
Graphics2D g = image.createGraphics();
try {
int width = image.getWidth();
int height = image.getHeight();
@@ -131,18 +133,54 @@ public abstract class ImageWriterAbstractTest<T extends ImageWriter> {
public void testWrite() throws IOException {
ImageWriter writer = createWriter();
for (RenderedImage testData : getTestData()) {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
try {
for (RenderedImage testData : getTestData()) {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
try (ImageOutputStream stream = ImageIO.createImageOutputStream(buffer)) {
writer.setOutput(stream);
writer.write(drawSomething((BufferedImage) testData));
try (ImageOutputStream stream = ImageIO.createImageOutputStream(buffer)) {
writer.setOutput(stream);
writer.write(drawSomething((BufferedImage) testData));
}
catch (IOException e) {
throw new AssertionError(e.getMessage(), e);
}
assertTrue(buffer.size() > 0, "No image data written");
}
catch (IOException e) {
throw new AssertionError(e.getMessage(), e);
}
finally {
writer.dispose();
}
}
@Test
public void testWriteRaster() throws IOException {
ImageWriter writer = createWriter();
try {
if (!writer.canWriteRasters()) {
return;
}
assertTrue(buffer.size() > 0, "No image data written");
ImageWriteParam param = writer.getDefaultWriteParam();
for (RenderedImage testData : getTestData()) {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
try (ImageOutputStream stream = ImageIO.createImageOutputStream(buffer)) {
writer.setOutput(stream);
writer.write(null, new IIOImage(testData.getTile(0, 0), null, null), param);
}
catch (IOException e) {
throw new AssertionError(e.getMessage(), e);
}
assertTrue(buffer.size() > 0, "No image data written");
}
}
finally {
writer.dispose();
}
}
@@ -0,0 +1,89 @@
package com.twelvemonkeys.imageio.util;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
class SequenceSupportTest {
@Test
void happyCase() {
SequenceSupport sequence = new SequenceSupport();
sequence.start();
assertEquals(0, sequence.current());
for (int i = 0; i < Byte.MAX_VALUE; i++) {
assertEquals(i, sequence.advance());
assertEquals(i + 1, sequence.current());
}
assertEquals(127, sequence.end());
assertEquals(-1, sequence.current());
assertThrows(IllegalStateException.class, sequence::advance);
}
@Test
void reset() {
SequenceSupport sequence = new SequenceSupport();
sequence.reset();
assertEquals(-1, sequence.current());
assertThrows(IllegalStateException.class, sequence::end);
sequence.start();
sequence.reset();
assertEquals(-1, sequence.current());
assertThrows(IllegalStateException.class, sequence::end);
sequence.start();
sequence.advance();
sequence.reset();
assertEquals(-1, sequence.current());
assertThrows(IllegalStateException.class, sequence::end);
sequence.start();
sequence.end();
sequence.reset();
assertEquals(-1, sequence.current());
assertThrows(IllegalStateException.class, sequence::end);
}
@Test
void startEnd() {
SequenceSupport sequence = new SequenceSupport();
sequence.start();
sequence.end();
assertEquals(-1, sequence.current());
assertThrows(IllegalStateException.class, sequence::end);
}
@Test
void startAlreadyStarted() {
SequenceSupport sequence = new SequenceSupport();
sequence.start();
assertThrows(IllegalStateException.class, sequence::start);
}
@Test
void advanceNotStarted() {
SequenceSupport sequence = new SequenceSupport();
assertThrows(IllegalStateException.class, sequence::advance);
}
@Test
void currentNotStarted() {
SequenceSupport sequence = new SequenceSupport();
assertEquals(-1, sequence.current());
}
@Test
void endNotStarted() {
SequenceSupport sequence = new SequenceSupport();
assertThrows(IllegalStateException.class, sequence::end);
}
}
+2
View File
@@ -43,6 +43,8 @@
<Provide-Capability>
osgi.serviceloader;
osgi.serviceloader=javax.imageio.spi.ImageReaderSpi
osgi.serviceloader;
osgi.serviceloader=javax.imageio.spi.ImageWriterSpi
</Provide-Capability>
</instructions>
</configuration>
@@ -0,0 +1,11 @@
package com.twelvemonkeys.imageio.plugins.dds;
enum BlockCompression {
BC1,
BC2,
BC3,
BC4,
BC5,
// BC6H,
// BC7
}
@@ -30,25 +30,35 @@
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;
// Header Flags
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.
int FLAG_CAPS = 1; // Required in every .dds file.
int FLAG_HEIGHT = 1 << 1; // Required in every .dds file.
int FLAG_WIDTH = 1 << 2; // Required in every .dds file.
int FLAG_PIXELFORMAT = 1 << 12; // Required in every .dds file.
int FLAG_PITCH = 1 << 3; // Required when pitch is provided for an uncompressed texture.
int FLAG_MIPMAPCOUNT = 1 << 17; // Required in a mipmapped texture.
int FLAG_LINEARSIZE = 1 << 19; // Required when pitch is provided for a compressed texture.
int FLAG_DEPTH = 1 << 23; // Required in a depth texture.
// Pixel Format Flags
int PIXEL_FORMAT_FLAG_ALPHAPIXELS = 0x1;
int PIXEL_FORMAT_FLAG_ALPHA = 0x2;
int PIXEL_FORMAT_FLAG_FOURCC = 0x04;
int PIXEL_FORMAT_FLAG_RGB = 0x40;
//DX10 Resource Dimensions
int D3D10_RESOURCE_DIMENSION_TEXTURE2D = 3;
//dwCaps
int DDSCAPS_COMPLEX = 0x8;
int DDSCAPS_MIPMAP = 0x400000;
int DDSCAPS_TEXTURE = 0x1000;
}
@@ -30,15 +30,29 @@
package com.twelvemonkeys.imageio.plugins.dds;
import static com.twelvemonkeys.imageio.plugins.dds.DDSReader.A1R5G5B5_MASKS;
import static com.twelvemonkeys.imageio.plugins.dds.DDSReader.A4R4G4B4_MASKS;
import static com.twelvemonkeys.imageio.plugins.dds.DDSReader.A8B8G8R8_MASKS;
import static com.twelvemonkeys.imageio.plugins.dds.DDSReader.A8R8G8B8_MASKS;
import static com.twelvemonkeys.imageio.plugins.dds.DDSReader.R5G6B5_MASKS;
import static com.twelvemonkeys.imageio.plugins.dds.DDSReader.R8G8B8_MASKS;
import static com.twelvemonkeys.imageio.plugins.dds.DDSReader.X1R5G5B5_MASKS;
import static com.twelvemonkeys.imageio.plugins.dds.DDSReader.X4R4G4B4_MASKS;
import static com.twelvemonkeys.imageio.plugins.dds.DDSReader.X8B8G8R8_MASKS;
import static com.twelvemonkeys.imageio.plugins.dds.DDSReader.X8R8G8B8_MASKS;
import javax.imageio.IIOException;
import javax.imageio.stream.ImageInputStream;
import java.awt.Dimension;
import java.io.IOException;
import java.nio.ByteOrder;
import java.util.Arrays;
/**
* @see <a href="https://learn.microsoft.com/en-us/windows/win32/direct3ddds/dds-header">DDS_HEADER structure</a>
* @see <a href="https://learn.microsoft.com/en-us/windows/win32/direct3ddds/dx-graphics-dds-pguide">Programming Guide for DDS</a>
*/
final class DDSHeader {
// https://learn.microsoft.com/en-us/windows/win32/direct3ddds/dx-graphics-dds-pguide
private int flags;
private int mipMapCount;
@@ -52,20 +66,12 @@ final class DDSHeader {
private int blueMask;
private int alphaMask;
DXT10Header dxt10Header;
@SuppressWarnings("unused")
static DDSHeader read(final ImageInputStream imageInput) throws IOException {
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
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));
@@ -73,11 +79,9 @@ final class DDSHeader {
// 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.toBinaryString(header.flags));
if (!header.hasFlag(DDS.FLAG_CAPS | DDS.FLAG_HEIGHT | DDS.FLAG_WIDTH | DDS.FLAG_PIXELFORMAT)) {
// NOTE: The Microsoft DDS documentation mention that readers should not rely on these flags...
throw new IIOException("Required DDS flag missing in header: " + Integer.toBinaryString(header.flags));
}
// Read Height & Width
@@ -97,6 +101,9 @@ final class DDSHeader {
// DDS_PIXELFORMAT structure
int px_dwSize = imageInput.readInt(); // [76,79]
if (px_dwSize != DDS.PIXELFORMAT_SIZE) {
throw new IIOException(String.format("Invalid DDS pixel format structure size (expected %d): %d", DDS.PIXELFORMAT_SIZE, dwSize));
}
header.pixelFormatFlags = imageInput.readInt(); // [80,83]
header.fourCC = imageInput.readInt(); // [84,87]
@@ -113,6 +120,11 @@ final class DDSHeader {
int dwReserved2 = imageInput.readInt(); // [124,127]
if (header.fourCC == DDSType.DXT10.fourCC()) {
// If DXT10, the DXT10 header will follow immediately
header.dxt10Header = DXT10Header.read(imageInput);
}
return header;
}
@@ -128,8 +140,8 @@ final class DDSHeader {
}
}
private boolean getFlag(int mask) {
return (flags & mask) != 0;
private boolean hasFlag(int mask) {
return (flags & mask) == mask;
}
int getWidth(int imageIndex) {
@@ -146,31 +158,101 @@ final class DDSHeader {
return mipMapCount;
}
int getBitCount() {
return bitCount;
DDSType getType() throws IIOException {
if (dxt10Header != null) {
return dxt10Header.getType();
}
return getRawType();
}
int getFourCC() {
return fourCC;
DDSType getRawType() throws IIOException {
if ((pixelFormatFlags & DDS.PIXEL_FORMAT_FLAG_FOURCC) != 0) {
// DXT
return DDSType.fromFourCC(fourCC);
}
else if ((pixelFormatFlags & DDS.PIXEL_FORMAT_FLAG_RGB) != 0) {
// RGB
int alphaMask = ((pixelFormatFlags & 0x01) != 0) ? this.alphaMask : 0; // 0x01 alpha
if (bitCount == 16) {
if (redMask == A1R5G5B5_MASKS[0] && greenMask == A1R5G5B5_MASKS[1] && blueMask == A1R5G5B5_MASKS[2] && alphaMask == A1R5G5B5_MASKS[3]) {
// A1R5G5B5
return DDSType.A1R5G5B5;
}
else if (redMask == X1R5G5B5_MASKS[0] && greenMask == X1R5G5B5_MASKS[1] && blueMask == X1R5G5B5_MASKS[2] && alphaMask == X1R5G5B5_MASKS[3]) {
// X1R5G5B5
return DDSType.X1R5G5B5;
}
else if (redMask == A4R4G4B4_MASKS[0] && greenMask == A4R4G4B4_MASKS[1] && blueMask == A4R4G4B4_MASKS[2] && alphaMask == A4R4G4B4_MASKS[3]) {
// A4R4G4B4
return DDSType.A4R4G4B4;
}
else if (redMask == X4R4G4B4_MASKS[0] && greenMask == X4R4G4B4_MASKS[1] && blueMask == X4R4G4B4_MASKS[2] && alphaMask == X4R4G4B4_MASKS[3]) {
// X4R4G4B4
return DDSType.X4R4G4B4;
}
else if (redMask == R5G6B5_MASKS[0] && greenMask == R5G6B5_MASKS[1] && blueMask == R5G6B5_MASKS[2] && alphaMask == R5G6B5_MASKS[3]) {
// R5G6B5
return DDSType.R5G6B5;
}
throw new IIOException("Unsupported 16bit RGB image.");
}
else if (bitCount == 24) {
if (redMask == R8G8B8_MASKS[0] && greenMask == R8G8B8_MASKS[1] && blueMask == R8G8B8_MASKS[2] && alphaMask == R8G8B8_MASKS[3]) {
// R8G8B8
return DDSType.R8G8B8;
}
throw new IIOException("Unsupported 24bit RGB image.");
}
else if (bitCount == 32) {
if (redMask == A8B8G8R8_MASKS[0] && greenMask == A8B8G8R8_MASKS[1] && blueMask == A8B8G8R8_MASKS[2] && alphaMask == A8B8G8R8_MASKS[3]) {
// A8B8G8R8
return DDSType.A8B8G8R8;
}
else if (redMask == X8B8G8R8_MASKS[0] && greenMask == X8B8G8R8_MASKS[1] && blueMask == X8B8G8R8_MASKS[2] && alphaMask == X8B8G8R8_MASKS[3]) {
// X8B8G8R8
return DDSType.X8B8G8R8;
}
else if (redMask == A8R8G8B8_MASKS[0] && greenMask == A8R8G8B8_MASKS[1] && blueMask == A8R8G8B8_MASKS[2] && alphaMask == A8R8G8B8_MASKS[3]) {
// A8R8G8B8
return DDSType.A8R8G8B8;
}
else if (redMask == X8R8G8B8_MASKS[0] && greenMask == X8R8G8B8_MASKS[1] && blueMask == X8R8G8B8_MASKS[2] && alphaMask == X8R8G8B8_MASKS[3]) {
// X8R8G8B8
return DDSType.X8R8G8B8;
}
throw new IIOException("Unsupported 32bit RGB image.");
}
throw new IIOException("Unsupported bit count: " + bitCount);
}
throw new IIOException("Unsupported YUV or LUMINANCE image.");
}
int getPixelFormatFlags() {
return pixelFormatFlags;
@Override
public String toString() {
return "DDSHeader{" +
"flags=" + Integer.toBinaryString(flags) +
", mipMapCount=" + mipMapCount +
", dimensions=" + Arrays.toString(Arrays.stream(dimensions)
.map(DDSHeader::dimensionToString)
.toArray(String[]::new)) +
", pixelFormatFlags=" + Integer.toBinaryString(pixelFormatFlags) +
", fourCC=" + fourCC +
", bitCount=" + bitCount +
", redMask=" + redMask +
", greenMask=" + greenMask +
", blueMask=" + blueMask +
", alphaMask=" + alphaMask +
'}';
}
int getRedMask() {
return redMask;
}
int getGreenMask() {
return greenMask;
}
int getBlueMask() {
return blueMask;
}
int getAlphaMask() {
return alphaMask;
private static String dimensionToString(Dimension dimension) {
return String.format("%dx%d", dimension.width, dimension.height);
}
}
@@ -0,0 +1,477 @@
package com.twelvemonkeys.imageio.plugins.dds;
import javax.imageio.stream.ImageOutputStream;
import java.awt.Color;
import java.awt.image.Raster;
import java.io.IOException;
import static com.twelvemonkeys.imageio.plugins.dds.DDSReader.ARGB_ORDER;
import static com.twelvemonkeys.imageio.plugins.dds.DDSReader.BIT5;
import static com.twelvemonkeys.imageio.plugins.dds.DDSReader.BIT6;
import static com.twelvemonkeys.imageio.plugins.dds.DDSReader.RGB_16_ORDER;
/**
* A designated class to encode image data to binary.
*
* @see <a href="https://www.ludicon.com/castano/blog/2009/03/gpu-dxt-decompression/">GPU DXT Decompression</a>.
* @see <a href="https://sv-journal.org/2014-1/06/en/index.php">TEXTURE COMPRESSION TECHNIQUES</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>
* @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>
*/
class DDSImageDataEncoder {
private DDSImageDataEncoder() {}
//A cap for alpha value for BC1 where if alpha value is smaller than this, the 4x4 block will enable alpha mode.
private static final int BC1_ALPHA_CAP = 124;
private static final int BC4_CHANNEL_RED = 0; //default for BC4.
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, 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, raster);
break;
case BC2:
new BlockCompressor2().encode(imageOutput, raster);
break;
case BC3:
new BlockCompressor3().encode(imageOutput, raster);
break;
case BC4:
new BlockCompressor4(BC4_CHANNEL_RED).encode(imageOutput, raster);
break;
case BC5:
new BlockCompressor5().encode(imageOutput, raster);
break;
default:
throw new IllegalArgumentException("DDS block compression is not supported yet: " + compression);
}
}
private static class BlockCompressor1 extends BlockCompressorBase {
private final boolean forceOpaque;
//color0,1 : space 565
//color2,3 : space 888
private final int[] palettes;
private final MutableColor[] color32s;
private BlockCompressor1(boolean forceOpaque) {
super();
this.forceOpaque = forceOpaque;
palettes = new int[4];
color32s = new MutableColor[16];
for (int i = 0; i < 16; i++) {
color32s[i] = new MutableColor();
}
}
//pack 32 bits of the colors to a single int value.
private static int color888ToInt(int r, int g, int b, int a) {
return (a << ARGB_ORDER.alphaShift) | (r << ARGB_ORDER.redShift) | (g << ARGB_ORDER.greenShift) | (b << ARGB_ORDER.blueShift);
}
void startEncodeBlock(ImageOutputStream imageOutput, int[] sampled) throws IOException {
boolean alphaMode = getBlockEndpoints(sampled, palettes);
imageOutput.writeShort((short) palettes[0]);
imageOutput.writeShort((short) palettes[1]);
//simulating color2,3
interpolate(alphaMode, palettes);
//indices encoding start.
int indices = encodeBlockIndices(alphaMode, sampled, palettes);
imageOutput.writeInt(indices);
}
//all palettes now in 8:8:8 space
int encodeBlockIndices(boolean alphaMode, int[] sampled, int[] palettes) {
int i = 0;
int colorPos = 0;
int indices = 0;
Color c0 = convertTo888(palettes[0]);
Color c1 = convertTo888(palettes[1]);
Color c2 = color888ToObject(palettes[2]);
Color c3 = color888ToObject(palettes[3]);
while (i < 64) {
Color c = setColorFor(colorPos, sampled[i++], sampled[i++], sampled[i++]);
byte index;
int a = sampled[i++];
if (alphaMode && isAlphaBelowCap(a)) {
index = 0b11;
} else {
double distance0 = calculateDistance(c, c0);
double distance1 = calculateDistance(c, c1);
double distance2 = calculateDistance(c, c2);
double distance3 = calculateDistance(c, c3);
index = getClosest(distance0, distance1, distance2, distance3);
}
indices |= (index << (colorPos * 2));
colorPos++;
}
return indices;
}
private Color setColorFor(int index, int r, int g, int b) {
color32s[index].setColor(r, g, b);
return color32s[index];
}
//color space 888
private static double calculateDistance(Color color1, Color color0) {
float r = Math.abs(color0.getRed() - color1.getRed());
float g = Math.abs(color0.getGreen() - color1.getGreen());
float b = Math.abs(color0.getBlue() - color1.getBlue());
return Math.sqrt(r * r + g * g + b * b);
}
private static byte getClosest(double d0, double d1, double d2, double d3) {
double min = Math.min(d0, Math.min(d1, Math.min(d2, d3)));
if (min == d0) return 0b00;
if (min == d1) return 0b01;
if (min == d2) return 0b10;
return 0b11;
}
//this method, we work in 888 space
@SuppressWarnings("DuplicatedCode")
//just in case intellij warns for 'duplication'
void interpolate(boolean alphaMode, int[] palettes) {
Color rgb0 = convertTo888(palettes[0]);
Color rgb1 = convertTo888(palettes[1]);
int rgb2;
int rgb3;
if (alphaMode) {
//alpha mode
int r2 = (rgb0.getRed() + rgb1.getRed()) / 2;
int g2 = (rgb0.getGreen() + rgb1.getGreen()) / 2;
int b2 = (rgb0.getBlue() + rgb1.getBlue()) / 2;
rgb2 = color888ToInt(r2, g2, b2, 0xff);
rgb3 = 0;
} else {
//opaque mode
int r2 = (2 * rgb0.getRed() + rgb1.getRed()) / 3;
int g2 = (2 * rgb0.getGreen() + rgb1.getGreen()) / 3;
int b2 = (2 * rgb0.getBlue() + rgb1.getBlue()) / 3;
rgb2 = color888ToInt(r2, g2, b2, 0xff);
int r3 = (rgb0.getRed() + 2 * rgb1.getRed()) / 3;
int g3 = (rgb0.getGreen() + 2 * rgb1.getGreen()) / 3;
int b3 = (rgb0.getBlue() + 2 * rgb1.getBlue()) / 3;
rgb3 = color888ToInt(r3, g3, b3, 0xff);
}
palettes[2] = rgb2;
palettes[3] = rgb3;
}
//this method, we work in 888 space, return color0&1 in 565 space
boolean getBlockEndpoints(int[] sampledColors, int[] paletteBuffer) {
if (sampledColors.length != 64)
throw new IllegalStateException("Unintended behaviour, expecting sampled colors of block to be 64, got " + sampledColors.length);
int minR = 0xff;
int minG = 0xff;
int minB = 0xff;
int maxR = 0;
int maxG = 0;
int maxB = 0;
boolean alphaMode = false;
int i = 0;
while (i < 64) {
int r = sampledColors[i++];
int g = sampledColors[i++];
int b = sampledColors[i++];
int a = sampledColors[i++];
if (!forceOpaque && isAlphaBelowCap(a)) {
alphaMode = true;
continue;
}
minR = Math.min(minR, r);
minG = Math.min(minG, g);
minB = Math.min(minB, b);
maxR = Math.max(maxR, r);
maxG = Math.max(maxG, g);
maxB = Math.max(maxB, b);
}
int color0 = convertTo565(maxR, maxG, maxB);
int color1 = convertTo565(minR, minG, minB);
if ((alphaMode && color0 > color1) || (!alphaMode && color0 < color1)) {
paletteBuffer[0] = color1;
paletteBuffer[1] = color0;
} else {
paletteBuffer[0] = color0;
paletteBuffer[1] = color1;
}
return alphaMode;
}
//Reference [3] Page 7
boolean getBlockEndpoints2(int[] sampled, int[] paletteBuffer) {
int maxDistance = -1;
boolean alphaMode = false;
for (int i = 0; i < 60; i += 4) {
for (int j = i + 4; j < 64; j += 4) {
if (!forceOpaque && isAlphaBelowCap(Math.min(sampled[i + 3], sampled[j + 3]))) {
alphaMode = true;
continue;
}
int distance = getColorDistance(sampled[i], sampled[i + 1], sampled[i + 2], sampled[j], sampled[j + 1], sampled[j + 2]);
if (distance > maxDistance) {
maxDistance = distance;
paletteBuffer[0] = convertTo565(sampled[i], sampled[i + 1], sampled[i + 2]);
paletteBuffer[1] = convertTo565(sampled[j], sampled[j + 1], sampled[j + 2]);
}
}
}
if ((alphaMode && paletteBuffer[0] > paletteBuffer[1]) || (!alphaMode && paletteBuffer[1] > paletteBuffer[0])) {
int a = paletteBuffer[0];
paletteBuffer[0] = paletteBuffer[1];
paletteBuffer[1] = a;
}
return alphaMode;
}
private static int getColorDistance(int r1, int g1, int b1, int r2, int g2, int b2) {
int r3 = r1 - r2;
int g3 = g1 - g2;
int b3 = b1 - b2;
return r3 * r3 + g3 * g3 + b3 * b3;
}
private static Color convertTo888(int c565) {
int r8 = BIT5[(c565 & 0xF800) >> 11];
int g8 = BIT6[(c565 & 0x07E0) >> 5];
int b8 = BIT5[(c565 & 0x001F)];
return new Color(r8, g8, b8, 0xff);
}
private static Color color888ToObject(int c888) {
return new Color(
(c888 & 0xFF0000) >> ARGB_ORDER.redShift,
(c888 & 0x00FF00) >> ARGB_ORDER.greenShift,
(c888 & 0x0000FF) >> ARGB_ORDER.blueShift,
(c888) >>> ARGB_ORDER.alphaShift
);
}
}
private static final class BlockCompressor2 extends BlockCompressor1 {
private BlockCompressor2() {
super(true);
}
@Override
void startEncodeBlock(ImageOutputStream imageOutput, int[] sampled) throws IOException {
//write 64 bit alpha first (4 bit alpha per pixel)
long alphaData = 0;
for (int i = 0; i < 16; i++) {
int alpha = sampled[i * 4 + 3] >> 4;
alphaData |= ((long) alpha) << (i * 4);
}
imageOutput.writeLong(alphaData);
super.startEncodeBlock(imageOutput, sampled);
}
}
private static final class BlockCompressor3 extends BlockCompressor1 {
private final BlockCompressor4 bc4;
private BlockCompressor3() {
super(true);
bc4 = new BlockCompressor4(BC4_CHANNEL_ALPHA);
}
@Override
void startEncodeBlock(ImageOutputStream imageOutput, int[] sampled) throws IOException {
bc4.startEncodeBlock(imageOutput, sampled);
super.startEncodeBlock(imageOutput, sampled);
}
}
private static final class BlockCompressor4 extends BlockCompressorBase {
private final int channelIndex;
private final int[] reds;
private BlockCompressor4(int channelIndex) {
super();
this.channelIndex = channelIndex;
this.reds = new int[8];
}
void startEncodeBlock(ImageOutputStream imageOutput, int[] samples) throws IOException {
getColorRange(samples, reds);
interpolate(reds);
long data = calculateIndices(samples, reds);
data |= (((long) (reds[1] & 0xff) << 8) | (reds[0] & 0xff));
imageOutput.writeLong(data);
}
// 6 bytes MSB will be for indices, the LSB is for the 2 red endpoints,
// as we write to file in LE the bytes will be swapped back to the desired order
private long calculateIndices(int[] samples, int[] reds) {
long data = 0;
for (int i = 0; i < 16; i++) {
int index;
int rSample = samples[i * 4 + channelIndex];
index = getNearest(rSample, reds);
data |= ((long) index << (16 + i * 3));
}
return data;
}
private int getNearest(int r, int[] reds) {
int nearest = 0;
int nearestValue = 255;
for (int i = 0; i < 8; i++) {
int v = Math.abs(r - reds[i]);
if (nearestValue > v) {
nearest = i;
nearestValue = v;
}
}
return nearest;
}
private void interpolate(int[] reds) {
int r0 = reds[0];
int r1 = reds[1];
for (int i = 0; i < 8; i++) {
reds[i] = DDSReader.getDXT5Alpha(r0, r1, i);
}
}
//r0 > r1 : use 6 interpolated color values
//r0 <= r1 : use 4
private void getColorRange(int[] samples, int[] red01) {
int r0 = 0;
int r1 = 255;
for (int i = 0; i < 16; i++) {
int r = samples[i * 4 + channelIndex];
r0 = Math.max(r0, r);
r1 = Math.min(r1, r);
}
red01[0] = r0;
red01[1] = r1;
}
}
private static final class BlockCompressor5 extends BlockCompressorBase {
private final BlockCompressor4 bc4r;
private final BlockCompressor4 bc4g;
public BlockCompressor5() {
bc4r = new BlockCompressor4(BC4_CHANNEL_RED);
bc4g = new BlockCompressor4(BC4_CHANNEL_GREEN);
}
@Override
void startEncodeBlock(ImageOutputStream imageOutput, int[] samples) throws IOException {
bc4r.startEncodeBlock(imageOutput, samples);
bc4g.startEncodeBlock(imageOutput, samples);
}
}
//https://rgbcolorpicker.com/565
//pack 32 bits color into a single 5:6:5 16bits value
static int convertTo565(int r8, int g8, int b8) {
int r5 = (r8 >> 3);
int g6 = (g8 >> 2);
int b5 = (b8 >> 3);
return color565ToInt(r5, g6, b5);
}
//pack 16 bits of the colors to a single int value.
private static int color565ToInt(int r5, int g6, int b5) {
return (r5 << RGB_16_ORDER.redShift) | (g6 << RGB_16_ORDER.greenShift) | (b5 << RGB_16_ORDER.blueShift);
}
private abstract static class BlockCompressorBase {
final int[] samples;
BlockCompressorBase() {
this.samples = new int[64];
}
//workaround for 24 dpi (no alpha) -> 32dpi (with alpha default to 0xff)
//as this mess the color0 & color1 up spectacularly bc alpha is not present in 24dpi
private static void adjustSampledBands(Raster raster, int[] samples) {
if (raster.getNumBands() == 4) return;
for (int i = 15; i >= 0; i--) {
int r24Index = i * 3;
int r32Index = i * 4;
samples[r32Index + 3] = 0xFF;
samples[r32Index + 2] = samples[r24Index + 2]; //b24 -> b32
samples[r32Index + 1] = samples[r24Index + 1]; //g24 -> g32
samples[r32Index] = samples[r24Index]; //r24 -> r32
}
}
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++) {
raster.getPixels(blockX * 4, blockY * 4, 4, 4, samples);
adjustSampledBands(raster, samples);
startEncodeBlock(imageOutput, samples);
}
}
}
boolean isAlphaBelowCap(int alpha) {
return alpha < BC1_ALPHA_CAP;
}
abstract void startEncodeBlock(ImageOutputStream imageOutput, int[] samples) throws IOException;
}
private static final class MutableColor extends Color {
int mutableValue;
public MutableColor() {
super(0, 0, 0);
this.mutableValue = 0;
}
void setColor(int red, int green, int blue) {
mutableValue = red << ARGB_ORDER.redShift;
mutableValue |= green << ARGB_ORDER.greenShift;
mutableValue |= blue << ARGB_ORDER.blueShift;
}
@Override
public int getRGB() {
return this.mutableValue;
}
//intellij generated
@Override
public boolean equals(Object object) {
if (!(object instanceof MutableColor)) return false;
if (!super.equals(object)) return false;
MutableColor that = (MutableColor) object;
return mutableValue == that.mutableValue;
}
@Override
public int hashCode() {
int result = super.hashCode();
result = 31 * result + mutableValue;
return result;
}
}
}
@@ -30,29 +30,51 @@
package com.twelvemonkeys.imageio.plugins.dds;
import javax.imageio.ImageTypeSpecifier;
import com.twelvemonkeys.imageio.StandardImageMetadataSupport;
final class DDSMetadata extends StandardImageMetadataSupport {
DDSMetadata(ImageTypeSpecifier type, DDSHeader header) {
super(builder(type)
.withCompressionTypeName(compressionName(header))
.withFormatVersion("1.0")
import javax.imageio.ImageTypeSpecifier;
final class DDSImageMetadata extends StandardImageMetadataSupport {
DDSImageMetadata(ImageTypeSpecifier specifier, DDSType type) {
super(builder(specifier)
.withCompressionTypeName(compressionName(type))
.withCompressionLossless(!type.isBlockCompression())
.withBitsPerSample(bitsPerSample(type))
.withFormatVersion("1.0")
);
}
private static String compressionName(DDSHeader header) {
// If the fourCC is valid, compression is one of the DXTn versions, otherwise None
int flags = header.getPixelFormatFlags();
if ((flags & DDS.PIXEL_FORMAT_FLAG_FOURCC) != 0) {
// DXTn
DDSType type = DDSType.valueOf(header.getFourCC());
private static String compressionName(DDSType type) {
if (type != null && type.isFourCC()) {
return type.name();
}
return "None";
}
private static int[] bitsPerSample(DDSType type) {
if (type.isBlockCompression()) {
return null; // Use defaults
}
int[] bitsPerSample = new int[4];
for (int i = 0; i < bitsPerSample.length; i++) {
bitsPerSample[i] = countMaskBits(type.rgbaMasks[i]);
}
return bitsPerSample;
}
private static int countMaskBits(int mask) {
// See https://graphics.stanford.edu/~seander/bithacks.html#CountBitsSetKernighan
int count;
for (count = 0; mask != 0; count++) {
mask &= mask - 1; // clear the least significant bit set
}
return count;
}
}
@@ -30,18 +30,16 @@
package com.twelvemonkeys.imageio.plugins.dds;
import static com.twelvemonkeys.imageio.util.IIOUtil.subsampleRow;
import com.twelvemonkeys.imageio.ImageReaderBase;
import com.twelvemonkeys.imageio.util.ImageTypeSpecifiers;
import javax.imageio.IIOException;
import javax.imageio.ImageIO;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.spi.ImageReaderSpi;
import java.awt.*;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
@@ -49,6 +47,14 @@ import java.nio.ByteOrder;
import java.util.Collections;
import java.util.Iterator;
import static com.twelvemonkeys.imageio.util.IIOUtil.subsampleRow;
/**
* ImageReader implementation for Microsoft DirectDraw Surface (DDS) format.
*
* @author Paul Allen
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
*/
public final class DDSImageReader extends ImageReaderBase {
private DDSHeader header;
@@ -91,7 +97,16 @@ public final class DDSImageReader extends ImageReaderBase {
checkBounds(imageIndex);
readHeader();
// TODO: Implement for the specific formats...
DDSType type = header.getType();
if (!type.isBlockCompression() && type.rgbaMasks[3] == 0) {
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);
}
@@ -147,14 +162,19 @@ public final class DDSImageReader extends ImageReaderBase {
public IIOMetadata getImageMetadata(int imageIndex) throws IOException {
ImageTypeSpecifier imageType = getRawImageType(imageIndex);
return new DDSMetadata(imageType, header);
return new DDSImageMetadata(imageType, header.getType());
}
private void readHeader() throws IOException {
if (header == null) {
imageInput.setByteOrder(ByteOrder.LITTLE_ENDIAN);
header = DDSHeader.read(imageInput);
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));
}
header = DDSHeader.read(imageInput);
imageInput.flushBefore(imageInput.getStreamPosition());
}
@@ -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,10 +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 {
}
finally {
stream.setByteOrder(byteOrder);
stream.reset();
}
}
@@ -67,6 +73,6 @@ public final class DDSImageReaderSpi extends ImageReaderSpiBase {
@Override
public String getDescription(Locale locale) {
return "Direct DrawSurface (DDS) Image Reader";
return "DirectDraw Surface (DDS) Image Reader";
}
}
@@ -0,0 +1,62 @@
package com.twelvemonkeys.imageio.plugins.dds;
import javax.imageio.ImageWriteParam;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public final class DDSImageWriteParam extends ImageWriteParam {
static final DDSType DEFAULT_TYPE = DDSType.DXT5;
private static final String[] COMPRESSION_TYPES = compressionTypes();
private static String[] compressionTypes() {
// TODO: Maybe hardcode subset of values that we actually support writing?
List<String> compressionTypes = Arrays.stream(DDSType.values())
.filter(DDSType::isBlockCompression)
.map(Enum::name)
.collect(Collectors.toList());
compressionTypes.add(0, "None");
return compressionTypes.toArray(new String[0]);
}
private boolean writeDXT10;
DDSImageWriteParam() {
canWriteCompressed = true;
compressionTypes = COMPRESSION_TYPES;
compressionType = DEFAULT_TYPE.name();
}
public void setWriteDX10() {
writeDXT10 = true;
}
public void clearWriteDX10() {
writeDXT10 = false;
}
public boolean isWriteDXT10() {
return writeDXT10;
}
DDSType type() {
if (compressionType == null || compressionType.equals("None")) {
return null;
}
return DDSType.valueOf(compressionType);
}
int getDxgiFormat() {
DDSType type = type();
if (type != null) {
return type.dxgiFormat();
}
return DXGI.DXGI_FORMAT_UNKNOWN;
}
}
@@ -0,0 +1,330 @@
package com.twelvemonkeys.imageio.plugins.dds;
import com.twelvemonkeys.imageio.ImageWriterBase;
import com.twelvemonkeys.imageio.util.IIOUtil;
import com.twelvemonkeys.imageio.util.SequenceSupport;
import javax.imageio.IIOException;
import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.ImageWriteParam;
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;
import java.io.IOException;
import java.nio.ByteOrder;
import java.nio.file.Files;
import java.nio.file.Paths;
/**
* ImageWriter implementation for Microsoft DirectDraw Surface (DDS) format.
*
* @author KhanTypo
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
*/
class DDSImageWriter extends ImageWriterBase {
private final SequenceSupport mipmapSequence = new SequenceSupport();
private long headerStartPos;
private DDSType mipmapType;
private Dimension mipmapDimension;
protected DDSImageWriter(ImageWriterSpi provider) {
super(provider);
}
@Override
public DDSImageWriteParam getDefaultWriteParam() {
return new DDSImageWriteParam();
}
@Override
protected void resetMembers() {
headerStartPos = 0;
mipmapSequence.reset();
mipmapType = null;
mipmapDimension = null;
}
@Override
public boolean canWriteRasters() {
return true;
}
@Override
public boolean canWriteSequence() {
return true;
}
@Override
public void prepareWriteSequence(IIOMetadata streamMetadata) throws IOException {
assertOutput();
mipmapSequence.start();
imageOutput.setByteOrder(ByteOrder.LITTLE_ENDIAN);
imageOutput.writeInt(DDS.MAGIC);
imageOutput.flush();
headerStartPos = imageOutput.getStreamPosition();
}
@Override
public void endWriteSequence() throws IOException {
int mipmapCount = mipmapSequence.end();
// Go back and update header
updateHeader(mipmapCount);
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 {
int mipmapIndex = mipmapSequence.advance();
Raster raster = getRaster(image);
ensureImageChannels(raster);
ensureTextureDimension(raster);
mipmapDimension = new Dimension(raster.getWidth(), raster.getHeight());
DDSImageWriteParam ddsParam = param instanceof DDSImageWriteParam
? ((DDSImageWriteParam) param)
: IIOUtil.copyStandardParams(param, getDefaultWriteParam());
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");
}
if (mipmapIndex == 0) {
writeHeader(raster.getWidth(), raster.getHeight(), mipmapType, ddsParam.isWriteDXT10());
if (ddsParam.isWriteDXT10()) {
writeDXT10Header(ddsParam.getDxgiFormat());
}
}
processImageStarted(mipmapIndex);
processImageProgress(0f);
DDSImageDataEncoder.writeImageData(imageOutput, raster, mipmapType.compression);
processImageProgress(100f);
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(Raster data) throws IIOException {
int numBands = data.getNumBands();
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 IIOException("Only image with 8 bits/channel is supported, got " + sampleSize);
}
}
/**
* 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 ensureTextureDimension(Raster raster) throws IIOException {
int width = raster.getWidth();
int height = raster.getHeight();
// 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));
}
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 {
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);
imageOutput.writeInt(height);
imageOutput.writeInt(width);
writePitchOrLinearSize(height, width, type);
//dwDepth
imageOutput.writeInt(0);
//dwMipmapCount
imageOutput.writeInt(1); // Should probably write 0 here for non-mipmap?
//reserved
imageOutput.write(new byte[44]);
//pixFmt
writePixelFormat(type, writeDXT10);
//dwCaps, right now we keep it simple by only using DDSCAP_TEXTURE as it is required.
imageOutput.writeInt(DDS.DDSCAPS_TEXTURE);
//dwCaps2, unused for now as we are not working with cube maps
imageOutput.writeInt(0);
//dwCaps3, dwCaps4, dwReserved2 : 3 unused integers
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(headerStartPos + 4); // Seek back to header start, skip 4 byte 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);
writePixelFormatFlags(type, writeDXT10);
writeFourCC(type, writeDXT10);
writeRGBAData(type, writeDXT10);
}
private void writeDXT10Header(int dxgiFormat) throws IOException {
//dxgiFormat
imageOutput.writeInt(dxgiFormat);
//resourceDimension
imageOutput.writeInt(DDS.D3D10_RESOURCE_DIMENSION_TEXTURE2D);
//miscFlag
imageOutput.writeInt(0);
//arraySize
imageOutput.writeInt(1);
//miscFlag2
imageOutput.writeInt(0);
}
private void writeRGBAData(DDSType type, boolean writeDXT10) throws IOException {
if (!writeDXT10 && !type.isFourCC()) {
//dwRGBBitCount
imageOutput.writeInt(type.blockSize() * 8); // TODO: Is bitcount always a multiple of 8?
//dwRBitMask
imageOutput.writeInt(type.rgbaMasks[0]);
//dwGBitMask
imageOutput.writeInt(type.rgbaMasks[1]);
//dwBBitMask
imageOutput.writeInt(type.rgbaMasks[2]);
//dwABitMask
imageOutput.writeInt(type.rgbaMasks[3]);
}
else {
//write 5 zero integers as fourCC is used
imageOutput.write(new byte[20]);
}
}
private void writeFourCC(DDSType type, boolean writeDXT10) throws IOException {
if (writeDXT10) {
imageOutput.writeInt(DDSType.DXT10.fourCC());
}
else if (type.isFourCC()) {
imageOutput.writeInt(type.fourCC());
}
else {
// No fourCC, custom format...
imageOutput.writeInt(0);
}
}
private void writePixelFormatFlags(DDSType type, boolean writeDXT10) throws IOException {
if (writeDXT10 || type.isFourCC()) {
imageOutput.writeInt(DDS.PIXEL_FORMAT_FLAG_FOURCC);
}
else {
imageOutput.writeInt(DDS.PIXEL_FORMAT_FLAG_RGB
| (type.rgbaMasks != null && type.rgbaMasks[3] != 0 ? DDS.PIXEL_FORMAT_FLAG_ALPHAPIXELS : 0));
}
}
private void writePitchOrLinearSize(int height, int width, DDSType type) throws IOException {
if (type.isBlockCompression()) {
imageOutput.writeInt(((width + 3) / 4) * ((height + 3) / 4) * type.blockSize());
}
else {
imageOutput.writeInt(width * type.blockSize());
}
}
@Override
public IIOMetadata getDefaultImageMetadata(ImageTypeSpecifier imageType, ImageWriteParam param) {
DDSType type = param instanceof DDSImageWriteParam
? ((DDSImageWriteParam) param).type()
: DDSImageWriteParam.DEFAULT_TYPE;
return new DDSImageMetadata(imageType, type);
}
@Override
public IIOMetadata convertImageMetadata(IIOMetadata inData, ImageTypeSpecifier imageType, ImageWriteParam param) {
// Nothing useful to convert here...
return getDefaultImageMetadata(imageType, param);
}
public static void main(String[] args) throws IOException {
if (args.length != 1) {
throw new IllegalArgumentException("Use 1 input file at a time.");
}
ImageIO.write(ImageIO.read(new File(args[0])), "dds", new MemoryCacheImageOutputStream(Files.newOutputStream(Paths.get("output.dds"))));
}
}
@@ -0,0 +1,34 @@
package com.twelvemonkeys.imageio.plugins.dds;
import com.twelvemonkeys.imageio.spi.ImageWriterSpiBase;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.ImageWriter;
import java.util.Locale;
public final class DDSImageWriterSpi extends ImageWriterSpiBase {
public DDSImageWriterSpi() {
super(new DDSProviderInfo());
}
@Override
public boolean canEncodeImage(ImageTypeSpecifier type) {
int numBands = type.getNumBands();
if (numBands < 3 || numBands > 4) {
return false;
}
return type.getSampleModel().getSampleSize(0) == 8;
}
@Override
public ImageWriter createWriterInstance(Object extension) {
return new DDSImageWriter(this);
}
@Override
public String getDescription(Locale locale) {
return "DirectDraw Surface (DDS) Image Writer";
}
}
@@ -35,16 +35,16 @@ import com.twelvemonkeys.imageio.spi.ReaderWriterProviderInfo;
final class DDSProviderInfo extends ReaderWriterProviderInfo {
DDSProviderInfo() {
super(
DDSProviderInfo.class,
new String[]{"DDS", "dds"},
new String[]{"dds"},
new String[]{"image/vnd-ms.dds"},
"com.twelvemonkeys.imageio.plugins.dds.DDSImageReader",
new String[]{"com.twelvemonkeys.imageio.plugins.dds.DDSImageReaderSpi"},
null,
null,
false, null, null, null, null,
true, null, null, null, null
DDSProviderInfo.class,
new String[] { "DDS", "dds" },
new String[] { "dds" },
new String[] { "image/vnd-ms.dds" },
"com.twelvemonkeys.imageio.plugins.dds.DDSImageReader",
new String[] { "com.twelvemonkeys.imageio.plugins.dds.DDSImageReaderSpi" },
"com.twelvemonkeys.imageio.plugins.dds.DDSImageWriter",
new String[] { "com.twelvemonkeys.imageio.plugins.dds.DDSImageWriterSpi" },
false, null, null, null, null,
true, null, null, null, null
);
}
}
@@ -70,11 +70,10 @@ import java.io.IOException;
* <a href="http://3dtech.jp/wiki/index.php?DDSReader">Japanese document</a>
*/
final class DDSReader {
static final Order ARGB_ORDER = new Order(16, 8, 0, 24);
static final Order ARGB_ORDER = new Order(16, 8, 0, 24); // 8 alpha | 8 red | 8 green | 8 blue
static final Order RGB_16_ORDER = new Order(11, 5, 0, -1); // no alpha | 5 red | 6 green | 5 blue
private final DDSHeader header;
private DX10Header dxt10Header;
DDSReader(DDSHeader header) {
this.header = header;
@@ -82,18 +81,20 @@ final class DDSReader {
int[] read(ImageInputStream imageInput, int imageIndex) throws IOException {
// type
DDSType type = getType();
if (type == DDSType.DXT10) {
dxt10Header = DX10Header.read(imageInput);
type = dxt10Header.getDDSType();
}
DDSType type = header.getType();
// offset buffer to index mipmap image
byte[] buffer = null;
for (int i = 0; i <= imageIndex; i++) {
int len = getLength(type, i);
buffer = new byte[len];
imageInput.readFully(buffer);
int len = getBufferLength(type, i);
if (i == imageIndex) {
buffer = new byte[len];
imageInput.readFully(buffer);
}
else {
imageInput.seek(imageInput.getStreamPosition() + len);
}
}
int width = header.getWidth(imageIndex);
@@ -135,82 +136,17 @@ final class DDSReader {
}
}
private DDSType getType() throws IIOException {
int flags = header.getPixelFormatFlags();
if ((flags & DDS.PIXEL_FORMAT_FLAG_FOURCC) != 0) {
// DXT
int type = header.getFourCC();
return DDSType.valueOf(type);
} else if ((flags & DDS.PIXEL_FORMAT_FLAG_RGB) != 0) {
// RGB
int bitCount = header.getBitCount();
int redMask = header.getRedMask();
int greenMask = header.getGreenMask();
int blueMask = header.getBlueMask();
int alphaMask = ((flags & 0x01) != 0) ? header.getAlphaMask() : 0; // 0x01 alpha
if (bitCount == 16) {
if (redMask == A1R5G5B5_MASKS[0] && greenMask == A1R5G5B5_MASKS[1] && blueMask == A1R5G5B5_MASKS[2] && alphaMask == A1R5G5B5_MASKS[3]) {
// A1R5G5B5
return DDSType.A1R5G5B5;
} else if (redMask == X1R5G5B5_MASKS[0] && greenMask == X1R5G5B5_MASKS[1] && blueMask == X1R5G5B5_MASKS[2] && alphaMask == X1R5G5B5_MASKS[3]) {
// X1R5G5B5
return DDSType.X1R5G5B5;
} else if (redMask == A4R4G4B4_MASKS[0] && greenMask == A4R4G4B4_MASKS[1] && blueMask == A4R4G4B4_MASKS[2] && alphaMask == A4R4G4B4_MASKS[3]) {
// A4R4G4B4
return DDSType.A4R4G4B4;
} else if (redMask == X4R4G4B4_MASKS[0] && greenMask == X4R4G4B4_MASKS[1] && blueMask == X4R4G4B4_MASKS[2] && alphaMask == X4R4G4B4_MASKS[3]) {
// X4R4G4B4
return DDSType.X4R4G4B4;
} else if (redMask == R5G6B5_MASKS[0] && greenMask == R5G6B5_MASKS[1] && blueMask == R5G6B5_MASKS[2] && alphaMask == R5G6B5_MASKS[3]) {
// R5G6B5
return DDSType.R5G6B5;
} else {
throw new IIOException("Unsupported 16bit RGB image.");
}
} else if (bitCount == 24) {
if (redMask == R8G8B8_MASKS[0] && greenMask == R8G8B8_MASKS[1] && blueMask == R8G8B8_MASKS[2] && alphaMask == R8G8B8_MASKS[3]) {
// R8G8B8
return DDSType.R8G8B8;
} else {
throw new IIOException("Unsupported 24bit RGB image.");
}
} else if (bitCount == 32) {
if (redMask == A8B8G8R8_MASKS[0] && greenMask == A8B8G8R8_MASKS[1] && blueMask == A8B8G8R8_MASKS[2] && alphaMask == A8B8G8R8_MASKS[3]) {
// A8B8G8R8
return DDSType.A8B8G8R8;
} else if (redMask == X8B8G8R8_MASKS[0] && greenMask == X8B8G8R8_MASKS[1] && blueMask == X8B8G8R8_MASKS[2] && alphaMask == X8B8G8R8_MASKS[3]) {
// X8B8G8R8
return DDSType.X8B8G8R8;
} else if (redMask == A8R8G8B8_MASKS[0] && greenMask == A8R8G8B8_MASKS[1] && blueMask == A8R8G8B8_MASKS[2] && alphaMask == A8R8G8B8_MASKS[3]) {
// A8R8G8B8
return DDSType.A8R8G8B8;
} else if (redMask == X8R8G8B8_MASKS[0] && greenMask == X8R8G8B8_MASKS[1] && blueMask == X8R8G8B8_MASKS[2] && alphaMask == X8R8G8B8_MASKS[3]) {
// X8R8G8B8
return DDSType.X8R8G8B8;
} else {
throw new IIOException("Unsupported 32bit RGB image.");
}
} else {
throw new IIOException("Unsupported bit count: " + bitCount);
}
} else {
throw new IIOException("Unsupported YUV or LUMINANCE image.");
}
}
private int getLength(DDSType type, int imageIndex) throws IIOException {
private int getBufferLength(DDSType type, int imageIndex) throws IIOException {
int width = header.getWidth(imageIndex);
int height = header.getHeight(imageIndex);
switch (type) {
case DXT1:
return 8 * ((width + 3) / 4) * ((height + 3) / 4);
case DXT2:
case DXT3:
case DXT4:
case DXT5:
return 16 * ((width + 3) / 4) * ((height + 3) / 4);
return type.blockSize() * ((width + 3) / 4) * ((height + 3) / 4);
case A1R5G5B5:
case X1R5G5B5:
case A4R4G4B4:
@@ -221,9 +157,9 @@ final class DDSReader {
case X8B8G8R8:
case A8R8G8B8:
case X8R8G8B8:
return (type.value() & 0xFF) * width * height;
return type.blockSize() * width * height;
default:
throw new IIOException("Unknown type: " + Integer.toHexString(type.value()));
throw new IIOException("Unknown type: " + type);
}
}
@@ -503,7 +439,7 @@ final class DDSReader {
return pixels;
}
private static int getDXTColor(int c0, int c1, int a, int t) {
static int getDXTColor(int c0, int c1, int a, int t) {
switch (t) {
case 0:
return getDXTColor1(c0, a);
@@ -519,7 +455,7 @@ final class DDSReader {
private static int getDXTColor2_1(int c0, int c1, int a) {
// 2*c0/3 + c1/3
int r = (2 * BIT5[(c0 & 0xFC00) >> 11] + BIT5[(c1 & 0xFC00) >> 11]) / 3;
int r = (2 * BIT5[(c0 & 0xF800) >> 11] + BIT5[(c1 & 0xF800) >> 11]) / 3;
int g = (2 * BIT6[(c0 & 0x07E0) >> 5] + BIT6[(c1 & 0x07E0) >> 5]) / 3;
int b = (2 * BIT5[c0 & 0x001F] + BIT5[c1 & 0x001F]) / 3;
return (a << ARGB_ORDER.alphaShift) | (r << ARGB_ORDER.redShift) | (g << ARGB_ORDER.greenShift) | (b << ARGB_ORDER.blueShift);
@@ -527,20 +463,20 @@ final class DDSReader {
private static int getDXTColor1_1(int c0, int c1, int a) {
// (c0+c1) / 2
int r = (BIT5[(c0 & 0xFC00) >> 11] + BIT5[(c1 & 0xFC00) >> 11]) / 2;
int r = (BIT5[(c0 & 0xF800) >> 11] + BIT5[(c1 & 0xF800) >> 11]) / 2;
int g = (BIT6[(c0 & 0x07E0) >> 5] + BIT6[(c1 & 0x07E0) >> 5]) / 2;
int b = (BIT5[c0 & 0x001F] + BIT5[c1 & 0x001F]) / 2;
return (a << ARGB_ORDER.alphaShift) | (r << ARGB_ORDER.redShift) | (g << ARGB_ORDER.greenShift) | (b << ARGB_ORDER.blueShift);
}
private static int getDXTColor1(int c, int a) {
int r = BIT5[(c & 0xFC00) >> 11];
int r = BIT5[(c & 0xF800) >> 11];
int g = BIT6[(c & 0x07E0) >> 5];
int b = BIT5[(c & 0x001F)];
return (a << ARGB_ORDER.alphaShift) | (r << ARGB_ORDER.redShift) | (g << ARGB_ORDER.greenShift) | (b << ARGB_ORDER.blueShift);
}
private static int getDXT5Alpha(int a0, int a1, int t) {
static int getDXT5Alpha(int a0, int a1, int t) {
if (a0 > a1) switch (t) {
case 0:
return a0;
@@ -581,22 +517,22 @@ final class DDSReader {
}
// RGBA Masks
private static final int[] A1R5G5B5_MASKS = {0x7C00, 0x03E0, 0x001F, 0x8000};
private static final int[] X1R5G5B5_MASKS = {0x7C00, 0x03E0, 0x001F, 0x0000};
private static final int[] A4R4G4B4_MASKS = {0x0F00, 0x00F0, 0x000F, 0xF000};
private static final int[] X4R4G4B4_MASKS = {0x0F00, 0x00F0, 0x000F, 0x0000};
private static final int[] R5G6B5_MASKS = {0xF800, 0x07E0, 0x001F, 0x0000};
private static final int[] R8G8B8_MASKS = {0xFF0000, 0x00FF00, 0x0000FF, 0x000000};
private static final int[] A8B8G8R8_MASKS = {0x000000FF, 0x0000FF00, 0x00FF0000, 0xFF000000};
private static final int[] X8B8G8R8_MASKS = {0x000000FF, 0x0000FF00, 0x00FF0000, 0x00000000};
private static final int[] A8R8G8B8_MASKS = {0x00FF0000, 0x0000FF00, 0x000000FF, 0xFF000000};
private static final int[] X8R8G8B8_MASKS = {0x00FF0000, 0x0000FF00, 0x000000FF, 0x00000000};
static final int[] A1R5G5B5_MASKS = {0x7C00, 0x03E0, 0x001F, 0x8000};
static final int[] X1R5G5B5_MASKS = {0x7C00, 0x03E0, 0x001F, 0x0000};
static final int[] A4R4G4B4_MASKS = {0x0F00, 0x00F0, 0x000F, 0xF000};
static final int[] X4R4G4B4_MASKS = {0x0F00, 0x00F0, 0x000F, 0x0000};
static final int[] R5G6B5_MASKS = {0xF800, 0x07E0, 0x001F, 0x0000};
static final int[] R8G8B8_MASKS = {0xFF0000, 0x00FF00, 0x0000FF, 0x000000};
static final int[] A8B8G8R8_MASKS = {0x000000FF, 0x0000FF00, 0x00FF0000, 0xFF000000};
static final int[] X8B8G8R8_MASKS = {0x000000FF, 0x0000FF00, 0x00FF0000, 0x00000000};
static final int[] A8R8G8B8_MASKS = {0x00FF0000, 0x0000FF00, 0x000000FF, 0xFF000000};
static final int[] X8R8G8B8_MASKS = {0x00FF0000, 0x0000FF00, 0x000000FF, 0x00000000};
// BIT4 = 17 * index;
private static final int[] BIT5 = {0, 8, 16, 25, 33, 41, 49, 58, 66, 74, 82, 90, 99, 107, 115, 123, 132, 140, 148, 156, 165, 173, 181, 189, 197, 206, 214, 222, 230, 239, 247, 255};
private static final int[] BIT6 = {0, 4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 97, 101, 105, 109, 113, 117, 121, 125, 130, 134, 138, 142, 146, 150, 154, 158, 162, 166, 170, 174, 178, 182, 186, 190, 194, 198, 202, 206, 210, 215, 219, 223, 227, 231, 235, 239, 243, 247, 251, 255};
static final int[] BIT5 = {0, 8, 16, 25, 33, 41, 49, 58, 66, 74, 82, 90, 99, 107, 115, 123, 132, 140, 148, 156, 165, 173, 181, 189, 197, 206, 214, 222, 230, 239, 247, 255};
static final int[] BIT6 = {0, 4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 97, 101, 105, 109, 113, 117, 121, 125, 130, 134, 138, 142, 146, 150, 154, 158, 162, 166, 170, 174, 178, 182, 186, 190, 194, 198, 202, 206, 210, 215, 219, 223, 227, 231, 235, 239, 243, 247, 251, 255};
private static final class Order {
static final class Order {
Order(int redShift, int greenShift, int blueShift, int alphaShift) {
this.redShift = redShift;
this.greenShift = greenShift;
@@ -30,41 +30,156 @@
package com.twelvemonkeys.imageio.plugins.dds;
import static com.twelvemonkeys.imageio.plugins.dds.BlockCompression.*;
import static com.twelvemonkeys.imageio.plugins.dds.DDSReader.*;
/**
* <a href="https://learn.microsoft.com/en-us/windows/win32/direct3d10/d3d10-graphics-programming-guide-resources-block-compression#compression-algorithms">Compression Algorithms</a>
* <a href="https://github.com/microsoft/DirectXTK12/wiki/DDSTextureLoader#remarks">An extended Non-DX10 FourCC list</a>
*/
enum DDSType {
DXT1(0x31545844),
DXT2(0x32545844),
DXT3(0x33545844),
DXT4(0x34545844),
DXT5(0x35545844),
DXT10(0x30315844),
A1R5G5B5((1 << 16) | 2),
X1R5G5B5((2 << 16) | 2),
A4R4G4B4((3 << 16) | 2),
X4R4G4B4((4 << 16) | 2),
R5G6B5((5 << 16) | 2),
R8G8B8((1 << 16) | 3),
A8B8G8R8((1 << 16) | 4),
X8B8G8R8((2 << 16) | 4),
A8R8G8B8((3 << 16) | 4),
X8R8G8B8((4 << 16) | 4);
// Compressed types
DXT1('D' + ('X' << 8) + ('T' << 16) + ('1' << 24), 8, DXGI.DXGI_FORMAT_BC1_UNORM, BC1),
DXT2('D' + ('X' << 8) + ('T' << 16) + ('2' << 24), 16, DXGI.DXGI_FORMAT_BC2_UNORM, BC2),
DXT3('D' + ('X' << 8) + ('T' << 16) + ('3' << 24), 16, DXGI.DXGI_FORMAT_BC2_UNORM, BC2),
DXT4('D' + ('X' << 8) + ('T' << 16) + ('4' << 24), 16, DXGI.DXGI_FORMAT_BC3_UNORM, BC3),
DXT5('D' + ('X' << 8) + ('T' << 16) + ('5' << 24), 16, DXGI.DXGI_FORMAT_BC3_UNORM, BC3),
private final int value;
ATI1('A' + ('T' << 8) + ('I' << 16) + ('1' << 24), 8, DXGI.DXGI_FORMAT_BC4_UNORM, BC4), // AKA BC4U
BC4U('B' + ('C' << 8) + ('4' << 16) + ('U' << 24), 8, DXGI.DXGI_FORMAT_BC4_UNORM, BC4),
BC4S('B' + ('C' << 8) + ('4' << 16) + ('S' << 24), 8, DXGI.DXGI_FORMAT_BC4_SNORM, BC4),
ATI2('A' + ('T' << 8) + ('I' << 16) + ('2' << 24), 16, DXGI.DXGI_FORMAT_BC5_UNORM, BC5), // AKA BC5U
BC5U('B' + ('C' << 8) + ('5' << 16) + ('U' << 24), 16, DXGI.DXGI_FORMAT_BC5_UNORM, BC5),
BC5S('B' + ('C' << 8) + ('5' << 16) + ('S' << 24), 16, DXGI.DXGI_FORMAT_BC5_SNORM, BC5),
DDSType(int value) {
this.value = value;
// Special case, see DXT10Header.dxgiFormat for real format
DXT10('D' + ('X' << 8) + ('1' << 16) + ('0' << 24), -1, DXGI.DXGI_FORMAT_UNKNOWN, null),
// Custom uncompressed pixel formats
// TODO: Consider swapping byte order to reflect the DXGI format?
A1R5G5B5(2, DXGI.DXGI_FORMAT_B5G5R5A1_UNORM, A1R5G5B5_MASKS),
X1R5G5B5(2, DXGI.DXGI_FORMAT_UNKNOWN, X1R5G5B5_MASKS),
A4R4G4B4(2, DXGI.DXGI_FORMAT_B4G4R4A4_UNORM, A4R4G4B4_MASKS),
X4R4G4B4(2, DXGI.DXGI_FORMAT_UNKNOWN, X4R4G4B4_MASKS),
R5G6B5( 2, DXGI.DXGI_FORMAT_B5G6R5_UNORM, R5G6B5_MASKS),
R8G8B8( 3, DXGI.DXGI_FORMAT_UNKNOWN, R8G8B8_MASKS),
A8B8G8R8(4, DXGI.DXGI_FORMAT_R8G8B8A8_UNORM, A8B8G8R8_MASKS),
X8B8G8R8(4, DXGI.DXGI_FORMAT_UNKNOWN, X8B8G8R8_MASKS),
A8R8G8B8(4, DXGI.DXGI_FORMAT_B8G8R8A8_UNORM, A8R8G8B8_MASKS),
X8R8G8B8(4, DXGI.DXGI_FORMAT_B8G8R8X8_UNORM, X8R8G8B8_MASKS);
private final int fourCC;
private final int blockSize;
private final int dxgiFormat;
final BlockCompression compression;
final int[] rgbaMasks;
DDSType(int fourCC, int blockSize, int dxgiFormat, BlockCompression compression) {
this(fourCC, blockSize, dxgiFormat, compression, null);
}
public int value() {
return value;
DDSType(int blockSize, int dxgiFormat, int[] rgbaMasks) {
this(0, blockSize, dxgiFormat, null, rgbaMasks);
}
public static DDSType valueOf(int value) {
for (DDSType type : DDSType.values()) {
if (value == type.value()) {
return type;
DDSType(int fourCC, int blockSize, int dxgiFormat, BlockCompression compression, int[] rgbaMasks) {
this.fourCC = fourCC;
this.blockSize = blockSize;
this.dxgiFormat = dxgiFormat;
this.compression = compression;
this.rgbaMasks = rgbaMasks;
}
public int fourCC() {
return fourCC;
}
public int blockSize() {
return blockSize;
}
public boolean isFourCC() {
return fourCC != 0;
}
public boolean isBlockCompression() {
return compression != null;
}
public int dxgiFormat() {
return dxgiFormat;
}
public static DDSType fromFourCC(int fourCC) {
if (fourCC != 0) {
for (DDSType type : values()) {
if (fourCC == type.fourCC()) {
return type;
}
}
}
throw new IllegalArgumentException(String.format("Unknown type: 0x%08x", value));
throw new IllegalArgumentException(String.format("Unknown type: 0x%08x", fourCC));
}
public static DDSType fromDXGIFormat(int dxgiFormat) {
switch (dxgiFormat) {
case DXGI.DXGI_FORMAT_R8G8B8A8_TYPELESS:
case DXGI.DXGI_FORMAT_R8G8B8A8_UNORM:
case DXGI.DXGI_FORMAT_R8G8B8A8_UNORM_SRGB:
case DXGI.DXGI_FORMAT_R8G8B8A8_UINT:
return A8B8G8R8; // ABGR
case DXGI.DXGI_FORMAT_B8G8R8A8_TYPELESS:
case DXGI.DXGI_FORMAT_B8G8R8A8_UNORM:
case DXGI.DXGI_FORMAT_B8G8R8A8_UNORM_SRGB:
return A8R8G8B8; // ARGB
case DXGI.DXGI_FORMAT_B8G8R8X8_TYPELESS:
case DXGI.DXGI_FORMAT_B8G8R8X8_UNORM:
case DXGI.DXGI_FORMAT_B8G8R8X8_UNORM_SRGB:
return X8R8G8B8;
case DXGI.DXGI_FORMAT_B5G5R5A1_UNORM:
return A1R5G5B5;
case DXGI.DXGI_FORMAT_B4G4R4A4_UNORM:
return A4R4G4B4;
case DXGI.DXGI_FORMAT_B5G6R5_UNORM:
return R5G6B5;
case DXGI.DXGI_FORMAT_BC1_TYPELESS:
case DXGI.DXGI_FORMAT_BC1_UNORM:
case DXGI.DXGI_FORMAT_BC1_UNORM_SRGB:
return DXT1;
case DXGI.DXGI_FORMAT_BC2_TYPELESS:
case DXGI.DXGI_FORMAT_BC2_UNORM:
case DXGI.DXGI_FORMAT_BC2_UNORM_SRGB:
return DXT2;
case DXGI.DXGI_FORMAT_BC3_TYPELESS:
case DXGI.DXGI_FORMAT_BC3_UNORM:
case DXGI.DXGI_FORMAT_BC3_UNORM_SRGB:
return DXT4;
case DXGI.DXGI_FORMAT_BC4_TYPELESS:
case DXGI.DXGI_FORMAT_BC4_UNORM:
return BC4U;
case DXGI.DXGI_FORMAT_BC4_SNORM:
return BC4S;
case DXGI.DXGI_FORMAT_BC5_TYPELESS:
case DXGI.DXGI_FORMAT_BC5_UNORM:
return BC5U;
case DXGI.DXGI_FORMAT_BC5_SNORM:
return BC5S;
default:
throw new IllegalArgumentException("Unsupported DXGI_FORMAT: " + dxgiFormat);
}
}
}
@@ -1,50 +0,0 @@
package com.twelvemonkeys.imageio.plugins.dds;
import java.util.Arrays;
import java.util.function.IntPredicate;
/**
* Enum that lists a certain types of DXGI Format this reader supports to read.
*
* <a href="https://learn.microsoft.com/en-us/windows/win32/api/dxgiformat/ne-dxgiformat-dxgi_format">DXGI Format List</a>
*/
public enum DX10DXGIFormat {
BC1(DDSType.DXT1, rangeInclusive(70, 72)),
BC2(DDSType.DXT2, rangeInclusive(73, 75)),
BC3(DDSType.DXT5, rangeInclusive(76, 78)),
//BC7(99),
B8G8R8A8(DDSType.A8B8G8R8, exactly(87, 90, 91)),
B8G8R8X8(DDSType.X8B8G8R8, exactly(88, 92, 93)),
R8G8B8A8(DDSType.A8R8G8B8, rangeInclusive(27, 32));
private final DDSType ddsType;
private final IntPredicate dxgiFormat;
DX10DXGIFormat(DDSType ddsType, IntPredicate dxgiFormat) {
this.ddsType = ddsType;
this.dxgiFormat = dxgiFormat;
}
DDSType getCorrespondingType() {
return ddsType;
}
static DX10DXGIFormat getFormat(int value) {
for (DX10DXGIFormat format : values()) {
if (format.dxgiFormat.test(value)) return format;
}
throw new IllegalArgumentException("Unsupported DXGI_FORMAT : " + value);
}
/**
* @param acceptedValues values in DXGI Formats List, passed values are expected to be in ascending order
*/
private static IntPredicate exactly(int ... acceptedValues) {
return test -> Arrays.binarySearch(acceptedValues, test) >= 0;
}
private static IntPredicate rangeInclusive(int from, int to) {
return test -> from <= test && test <= to;
}
}
@@ -1,33 +0,0 @@
package com.twelvemonkeys.imageio.plugins.dds;
import javax.imageio.stream.ImageInputStream;
import java.io.IOException;
//https://learn.microsoft.com/en-us/windows/win32/direct3ddds/dds-header-dxt10
public final class DX10Header {
final DX10DXGIFormat dxgiFormat;
final int resourceDimension, miscFlag, arraySize, miscFlags2;
private DX10Header(int dxgiFormat, int resourceDimension, int miscFlag, int arraySize, int miscFlags2) {
this.dxgiFormat = DX10DXGIFormat.getFormat(dxgiFormat);
this.resourceDimension = resourceDimension;
if (this.resourceDimension != DDS.D3D10_RESOURCE_DIMENSION_TEXTURE2D)
throw new IllegalArgumentException("Resource dimension " + resourceDimension + " is not supported, expected 3.");
this.miscFlag = miscFlag;
this.arraySize = arraySize;
this.miscFlags2 = miscFlags2;
}
static DX10Header read(ImageInputStream inputStream) throws IOException {
int dxgiFormat = inputStream.readInt();
int resourceDimension = inputStream.readInt();
int miscFlag = inputStream.readInt();
int arraySize = inputStream.readInt();
int miscFlags2 = inputStream.readInt();
return new DX10Header(dxgiFormat, resourceDimension, miscFlag, arraySize, miscFlags2);
}
DDSType getDDSType() {
return dxgiFormat.getCorrespondingType();
}
}
@@ -0,0 +1,129 @@
package com.twelvemonkeys.imageio.plugins.dds;
/**
* <a href="https://learn.microsoft.com/en-us/windows/win32/api/dxgiformat/ne-dxgiformat-dxgi_format">DXGI Format List</a>
*/
interface DXGI {
int DXGI_FORMAT_UNKNOWN = 0;
int DXGI_FORMAT_R32G32B32A32_TYPELESS = 1;
int DXGI_FORMAT_R32G32B32A32_FLOAT = 2;
int DXGI_FORMAT_R32G32B32A32_UINT = 3;
int DXGI_FORMAT_R32G32B32A32_SINT = 4;
int DXGI_FORMAT_R32G32B32_TYPELESS = 5;
int DXGI_FORMAT_R32G32B32_FLOAT = 6;
int DXGI_FORMAT_R32G32B32_UINT = 7;
int DXGI_FORMAT_R32G32B32_SINT = 8;
int DXGI_FORMAT_R16G16B16A16_TYPELESS = 9;
int DXGI_FORMAT_R16G16B16A16_FLOAT = 10;
int DXGI_FORMAT_R16G16B16A16_UNORM = 11;
int DXGI_FORMAT_R16G16B16A16_UINT = 12;
int DXGI_FORMAT_R16G16B16A16_SNORM = 13;
int DXGI_FORMAT_R16G16B16A16_SINT = 14;
int DXGI_FORMAT_R32G32_TYPELESS = 15;
int DXGI_FORMAT_R32G32_FLOAT = 16;
int DXGI_FORMAT_R32G32_UINT = 17;
int DXGI_FORMAT_R32G32_SINT = 18;
int DXGI_FORMAT_R32G8X24_TYPELESS = 19;
int DXGI_FORMAT_D32_FLOAT_S8X24_UINT = 20;
int DXGI_FORMAT_R32_FLOAT_X8X24_TYPELESS = 21;
int DXGI_FORMAT_X32_TYPELESS_G8X24_UINT = 22;
int DXGI_FORMAT_R10G10B10A2_TYPELESS = 23;
int DXGI_FORMAT_R10G10B10A2_UNORM = 24;
int DXGI_FORMAT_R10G10B10A2_UINT = 25;
int DXGI_FORMAT_R11G11B10_FLOAT = 26;
int DXGI_FORMAT_R8G8B8A8_TYPELESS = 27;
int DXGI_FORMAT_R8G8B8A8_UNORM = 28;
int DXGI_FORMAT_R8G8B8A8_UNORM_SRGB = 29;
int DXGI_FORMAT_R8G8B8A8_UINT = 30;
int DXGI_FORMAT_R8G8B8A8_SNORM = 31;
int DXGI_FORMAT_R8G8B8A8_SINT = 32;
int DXGI_FORMAT_R16G16_TYPELESS = 33;
int DXGI_FORMAT_R16G16_FLOAT = 34;
int DXGI_FORMAT_R16G16_UNORM = 35;
int DXGI_FORMAT_R16G16_UINT = 36;
int DXGI_FORMAT_R16G16_SNORM = 37;
int DXGI_FORMAT_R16G16_SINT = 38;
int DXGI_FORMAT_R32_TYPELESS = 39;
int DXGI_FORMAT_D32_FLOAT = 40;
int DXGI_FORMAT_R32_FLOAT = 41;
int DXGI_FORMAT_R32_UINT = 42;
int DXGI_FORMAT_R32_SINT = 43;
int DXGI_FORMAT_R24G8_TYPELESS = 44;
int DXGI_FORMAT_D24_UNORM_S8_UINT = 45;
int DXGI_FORMAT_R24_UNORM_X8_TYPELESS = 46;
int DXGI_FORMAT_X24_TYPELESS_G8_UINT = 47;
int DXGI_FORMAT_R8G8_TYPELESS = 48;
int DXGI_FORMAT_R8G8_UNORM = 49;
int DXGI_FORMAT_R8G8_UINT = 50;
int DXGI_FORMAT_R8G8_SNORM = 51;
int DXGI_FORMAT_R8G8_SINT = 52;
int DXGI_FORMAT_R16_TYPELESS = 53;
int DXGI_FORMAT_R16_FLOAT = 54;
int DXGI_FORMAT_D16_UNORM = 55;
int DXGI_FORMAT_R16_UNORM = 56;
int DXGI_FORMAT_R16_UINT = 57;
int DXGI_FORMAT_R16_SNORM = 58;
int DXGI_FORMAT_R16_SINT = 59;
int DXGI_FORMAT_R8_TYPELESS = 60;
int DXGI_FORMAT_R8_UNORM = 61;
int DXGI_FORMAT_R8_UINT = 62;
int DXGI_FORMAT_R8_SNORM = 63;
int DXGI_FORMAT_R8_SINT = 64;
int DXGI_FORMAT_A8_UNORM = 65;
int DXGI_FORMAT_R1_UNORM = 66;
int DXGI_FORMAT_R9G9B9E5_SHAREDEXP = 67;
int DXGI_FORMAT_R8G8_B8G8_UNORM = 68;
int DXGI_FORMAT_G8R8_G8B8_UNORM = 69;
int DXGI_FORMAT_BC1_TYPELESS = 70;
int DXGI_FORMAT_BC1_UNORM = 71;
int DXGI_FORMAT_BC1_UNORM_SRGB = 72;
int DXGI_FORMAT_BC2_TYPELESS = 73;
int DXGI_FORMAT_BC2_UNORM = 74;
int DXGI_FORMAT_BC2_UNORM_SRGB = 75;
int DXGI_FORMAT_BC3_TYPELESS = 76;
int DXGI_FORMAT_BC3_UNORM = 77;
int DXGI_FORMAT_BC3_UNORM_SRGB = 78;
int DXGI_FORMAT_BC4_TYPELESS = 79;
int DXGI_FORMAT_BC4_UNORM = 80;
int DXGI_FORMAT_BC4_SNORM = 81;
int DXGI_FORMAT_BC5_TYPELESS = 82;
int DXGI_FORMAT_BC5_UNORM = 83;
int DXGI_FORMAT_BC5_SNORM = 84;
int DXGI_FORMAT_B5G6R5_UNORM = 85;
int DXGI_FORMAT_B5G5R5A1_UNORM = 86;
int DXGI_FORMAT_B8G8R8A8_UNORM = 87;
int DXGI_FORMAT_B8G8R8X8_UNORM = 88;
int DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM = 89;
int DXGI_FORMAT_B8G8R8A8_TYPELESS = 90;
int DXGI_FORMAT_B8G8R8A8_UNORM_SRGB = 91;
int DXGI_FORMAT_B8G8R8X8_TYPELESS = 92;
int DXGI_FORMAT_B8G8R8X8_UNORM_SRGB = 93;
int DXGI_FORMAT_BC6H_TYPELESS = 94;
int DXGI_FORMAT_BC6H_UF16 = 95;
int DXGI_FORMAT_BC6H_SF16 = 96;
int DXGI_FORMAT_BC7_TYPELESS = 97;
int DXGI_FORMAT_BC7_UNORM = 98;
int DXGI_FORMAT_BC7_UNORM_SRGB = 99;
int DXGI_FORMAT_AYUV = 100;
int DXGI_FORMAT_Y410 = 101;
int DXGI_FORMAT_Y416 = 102;
int DXGI_FORMAT_NV12 = 103;
int DXGI_FORMAT_P010 = 104;
int DXGI_FORMAT_P016 = 105;
int DXGI_FORMAT_420_OPAQUE = 106;
int DXGI_FORMAT_YUY2 = 107;
int DXGI_FORMAT_Y210 = 108;
int DXGI_FORMAT_Y216 = 109;
int DXGI_FORMAT_NV11 = 110;
int DXGI_FORMAT_AI44 = 111;
int DXGI_FORMAT_IA44 = 112;
int DXGI_FORMAT_P8 = 113;
int DXGI_FORMAT_A8P8 = 114;
int DXGI_FORMAT_B4G4R4A4_UNORM = 115;
int DXGI_FORMAT_P208 = 130;
int DXGI_FORMAT_V208 = 131;
int DXGI_FORMAT_V408 = 132;
int DXGI_FORMAT_SAMPLER_FEEDBACK_MIN_MIP_OPAQUE = 189;
int DXGI_FORMAT_SAMPLER_FEEDBACK_MIP_REGION_USED_OPAQUE = 190;
int DXGI_FORMAT_FORCE_UINT = 0xffffffff;
}
@@ -0,0 +1,45 @@
package com.twelvemonkeys.imageio.plugins.dds;
import javax.imageio.stream.ImageInputStream;
import java.io.IOException;
/**
* @see <a href="https://learn.microsoft.com/en-us/windows/win32/direct3ddds/dds-header-dxt10">DDS_HEADER_DXT10 structure</a>
*/
final class DXT10Header {
final int dxgiFormat;
final int resourceDimension;
final int miscFlag;
final int arraySize;
final int miscFlags2;
private final DDSType type;
private DXT10Header(int dxgiFormat, int resourceDimension, int miscFlag, int arraySize, int miscFlags2) {
type = DDSType.fromDXGIFormat(dxgiFormat); // Validates dxgiFormat
if (resourceDimension != DDS.D3D10_RESOURCE_DIMENSION_TEXTURE2D) {
throw new IllegalArgumentException(String.format("Resource dimension %d is not supported, expected: %d",
resourceDimension, DDS.D3D10_RESOURCE_DIMENSION_TEXTURE2D));
}
this.dxgiFormat = dxgiFormat;
this.resourceDimension = resourceDimension;
this.miscFlag = miscFlag;
this.arraySize = arraySize;
this.miscFlags2 = miscFlags2;
}
static DXT10Header read(ImageInputStream inputStream) throws IOException {
int dxgiFormat = inputStream.readInt();
int resourceDimension = inputStream.readInt();
int miscFlag = inputStream.readInt();
int arraySize = inputStream.readInt();
int miscFlags2 = inputStream.readInt();
return new DXT10Header(dxgiFormat, resourceDimension, miscFlag, arraySize, miscFlags2);
}
DDSType getType() {
return type;
}
}
@@ -0,0 +1 @@
com.twelvemonkeys.imageio.plugins.dds.DDSImageWriterSpi
@@ -0,0 +1,101 @@
package com.twelvemonkeys.imageio.plugins.dds;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.awt.image.BufferedImage;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.metadata.IIOMetadataFormatImpl;
import javax.imageio.metadata.IIOMetadataNode;
import org.junit.jupiter.api.Test;
import org.w3c.dom.NodeList;
class DDSImageMetadataTest {
@Test
void standardMetadataDXT1() {
DDSImageMetadata metadata = createDDSImageMetadata(BufferedImage.TYPE_INT_ARGB, DDSType.DXT1);
IIOMetadataNode tree = (IIOMetadataNode) metadata.getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName);
NodeList compressionTypeNames = tree.getElementsByTagName("CompressionTypeName");
assertEquals(1, compressionTypeNames.getLength());
IIOMetadataNode compressionTypeName = (IIOMetadataNode) compressionTypeNames.item(0);
assertEquals("DXT1", 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"));
}
@Test
void standardMetadataA8R8G8B8() {
DDSImageMetadata metadata = createDDSImageMetadata(BufferedImage.TYPE_INT_ARGB, DDSType.A8R8G8B8);
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("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"));
}
@Test
void standardMetadataX8R8G8B8() {
DDSImageMetadata metadata = createDDSImageMetadata(BufferedImage.TYPE_INT_RGB, DDSType.X8R8G8B8);
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("8 8 8 0", bitsPerSample.getAttribute("value")); // Or just 8 8 8?
NodeList alphas = tree.getElementsByTagName("Alpha");
assertEquals(1, alphas.getLength());
IIOMetadataNode alpha = (IIOMetadataNode) alphas.item(0);
assertEquals("none", alpha.getAttribute("value"));
}
@Test
void standardMetadataX1R5G5B5() {
DDSImageMetadata metadata = createDDSImageMetadata(BufferedImage.TYPE_INT_RGB, DDSType.X1R5G5B5);
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 5 5 0", bitsPerSample.getAttribute("value")); // Or just 5 5 5?
NodeList alphas = tree.getElementsByTagName("Alpha");
assertEquals(1, alphas.getLength());
IIOMetadataNode alpha = (IIOMetadataNode) alphas.item(0);
assertEquals("none", alpha.getAttribute("value"));
}
private static DDSImageMetadata createDDSImageMetadata(int bufferedImageType, DDSType ddsType) {
return new DDSImageMetadata(ImageTypeSpecifier.createFromBufferedImageType(bufferedImageType), ddsType);
}
}
@@ -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() {
@@ -110,4 +123,67 @@ public class DDSImageReaderTest extends ImageReaderAbstractTest<DDSImageReader>
protected List<String> getMIMETypes() {
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();
}
}
}
@@ -0,0 +1,55 @@
package com.twelvemonkeys.imageio.plugins.dds;
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();
String[] compressionTypes = param.getCompressionTypes();
DDSType[] values = Arrays.stream(DDSType.values())
.filter(DDSType::isBlockCompression)
.toArray(DDSType[]::new);
assertEquals(values.length + 1, compressionTypes.length);
for (int i = 0; i < values.length; i++) {
DDSType type = values[i];
assertEquals(type.name(), compressionTypes[i + 1]);
}
assertEquals("None", compressionTypes[0]);
}
@Test
void setCompression() {
DDSImageWriteParam param = new DDSImageWriteParam();
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());
}
}
}
}
@@ -0,0 +1,186 @@
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.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;
class DDSImageWriterTest extends ImageWriterAbstractTest<DDSImageWriter> {
@Override
protected ImageWriterSpi createProvider() {
return new DDSImageWriterSpi();
}
@Override
protected List<BufferedImage> getTestData() {
return Arrays.asList(
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 writeRasters() throws IOException {
ImageWriter writer = createWriter();
assertTrue(writer.canWriteRasters());
// Full tests in super class
}
@Test
void writeMipmap() throws IOException {
ImageWriter writer = createWriter();
try {
assertTrue(writer.canWriteSequence());
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();
}
}
}
@@ -33,6 +33,7 @@ package com.twelvemonkeys.imageio.plugins.icns;
import com.twelvemonkeys.imageio.ImageWriterBase;
import com.twelvemonkeys.imageio.stream.SubImageOutputStream;
import com.twelvemonkeys.imageio.util.ProgressListenerBase;
import com.twelvemonkeys.imageio.util.SequenceSupport;
import javax.imageio.IIOException;
import javax.imageio.IIOImage;
@@ -55,7 +56,7 @@ import java.util.Iterator;
*/
public final class ICNSImageWriter extends ImageWriterBase {
private int sequenceIndex = -1;
private final SequenceSupport sequence = new SequenceSupport();
private ImageWriter pngDelegate;
ICNSImageWriter(ImageWriterSpi provider) {
@@ -64,7 +65,7 @@ public final class ICNSImageWriter extends ImageWriterBase {
@Override
protected void resetMembers() {
sequenceIndex = -1;
sequence.reset();
if (pngDelegate != null) {
pngDelegate.dispose();
@@ -97,41 +98,29 @@ public final class ICNSImageWriter extends ImageWriterBase {
@Override
public void prepareWriteSequence(final IIOMetadata streamMetadata) throws IOException {
assertOutput();
sequence.start();
// TODO: Allow TOC resource to be passed as stream metadata?
// - We only need number of icons to be written later
// - The contents of the TOC could be updated while adding to the sequence
if (sequenceIndex >= 0) {
throw new IllegalStateException("writeSequence already started");
}
writeICNSHeader();
sequenceIndex = 0;
}
@SuppressWarnings("RedundantThrows")
@Override
public void endWriteSequence() throws IOException {
assertOutput();
if (sequenceIndex < 0) {
throw new IllegalStateException("prepareWriteSequence not called");
}
sequence.end();
// TODO: Now that we know the number of icon resources, we could move all data backwards
// and write a TOC... But I don't think the benefit will outweigh the cost.
sequenceIndex = -1;
}
@Override
public void writeToSequence(final IIOImage image, final ImageWriteParam param) throws IOException {
assertOutput();
if (sequenceIndex < 0) {
throw new IllegalStateException("prepareWriteSequence not called");
}
int imageIndex = sequence.advance();
if (image.hasRaster()) {
throw new UnsupportedOperationException("image has a Raster");
@@ -148,7 +137,7 @@ public final class ICNSImageWriter extends ImageWriterBase {
imageOutput.writeInt(IconResource.typeFromImage(image.getRenderedImage(), "PNG"));
imageOutput.writeInt(0); // Size, update later
processImageStarted(sequenceIndex);
processImageStarted(imageIndex);
// Write icon in PNG format
ImageWriter writer = getPNGDelegate();
@@ -208,7 +197,7 @@ public final class ICNSImageWriter extends ImageWriterBase {
pngDelegate.addIIOWriteWarningListener(new IIOWriteWarningListener() {
@Override
public void warningOccurred(ImageWriter source, int imageIndex, String warning) {
processWarningOccurred(sequenceIndex, warning);
processWarningOccurred(sequence.current(), warning);
}
});
}
@@ -111,6 +111,7 @@ enum TupleType {
static TupleType forPAM(Raster raster) {
SampleModel sampleModel = raster.getSampleModel();
switch (sampleModel.getTransferType()) {
case DataBuffer.TYPE_BYTE:
case DataBuffer.TYPE_USHORT:
@@ -145,8 +146,12 @@ enum TupleType {
return TupleType.RGB;
}
else if (bands == 4) {
// Ambiguous, could also be CMYK...
return TupleType.RGB_ALPHA;
}
else if (bands == 5) {
return TupleType.CMYK_ALPHA;
}
// ...else fall through...
}
@@ -154,7 +159,7 @@ enum TupleType {
}
static TupleType forPAM(ImageTypeSpecifier type) {
// Support only 1 bit b/w, 8-16 bit gray and 8-16 bit/sample RGB
// Support only 1 bit b/w, 8-16 bit gray, 8-16 bit/sample RGB and 8-16 bit/sample CMYK
switch (type.getBufferedImageType()) {
// 1 bit b/w or b/w + a
case BufferedImage.TYPE_BYTE_BINARY:
@@ -42,6 +42,7 @@ import com.twelvemonkeys.imageio.stream.SubImageOutputStream;
import com.twelvemonkeys.imageio.util.IIOUtil;
import com.twelvemonkeys.imageio.util.ImageTypeSpecifiers;
import com.twelvemonkeys.imageio.util.ProgressListenerBase;
import com.twelvemonkeys.imageio.util.SequenceSupport;
import com.twelvemonkeys.io.enc.EncoderStream;
import com.twelvemonkeys.io.enc.PackBitsEncoder;
import com.twelvemonkeys.lang.Validate;
@@ -110,12 +111,7 @@ public final class TIFFImageWriter extends ImageWriterBase {
// Support storing multiple images in one stream (multi-page TIFF)
// Support more of the ImageIO metadata (ie. compression from metadata, etc)
/**
* Flag for active sequence writing
*/
private boolean writingSequence = false;
private int sequenceIndex = 0;
private final SequenceSupport sequence = new SequenceSupport();
/**
* Metadata writer for sequence writing
@@ -751,7 +747,7 @@ public final class TIFFImageWriter extends ImageWriterBase {
ifd = ((TIFFImageMetadata) inData).getIFD();
}
else {
TIFFImageMetadata outData = new TIFFImageMetadata(Collections.<Entry>emptySet());
TIFFImageMetadata outData = new TIFFImageMetadata(Collections.emptySet());
try {
if (Arrays.asList(inData.getMetadataFormatNames()).contains(SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME)) {
@@ -766,7 +762,7 @@ public final class TIFFImageWriter extends ImageWriterBase {
}
}
catch (IIOInvalidTreeException e) {
processWarningOccurred(sequenceIndex, "Could not convert image meta data: " + e.getMessage());
processWarningOccurred(sequence.current(), "Could not convert image meta data: " + e.getMessage());
}
ifd = outData.getIFD();
@@ -966,14 +962,11 @@ public final class TIFFImageWriter extends ImageWriterBase {
@Override
public void prepareWriteSequence(final IIOMetadata streamMetadata) throws IOException {
if (writingSequence) {
throw new IllegalStateException("sequence writing has already been started!");
}
sequence.start();
assertOutput();
configureStreamByteOrder(streamMetadata, imageOutput);
writingSequence = true;
sequenceTIFFWriter = new TIFFWriter(isBigTIFF() ? 8 : 4);
sequenceTIFFWriter.writeTIFFHeader(imageOutput);
sequenceLastIFDPos = imageOutput.getStreamPosition();
@@ -985,26 +978,20 @@ public final class TIFFImageWriter extends ImageWriterBase {
@Override
public void writeToSequence(final IIOImage image, final ImageWriteParam param) throws IOException {
if (!writingSequence) {
throw new IllegalStateException("prepareWriteSequence() must be called before writeToSequence()!");
}
int sequenceIndex = sequence.advance();
if (sequenceIndex > 0) {
imageOutput.flushBefore(sequenceLastIFDPos);
imageOutput.seek(imageOutput.length());
}
sequenceLastIFDPos = writePage(sequenceIndex++, image, param, sequenceTIFFWriter, sequenceLastIFDPos);
sequenceLastIFDPos = writePage(sequenceIndex, image, param, sequenceTIFFWriter, sequenceLastIFDPos);
}
@Override
public void endWriteSequence() throws IOException {
if (!writingSequence) {
throw new IllegalStateException("prepareWriteSequence() must be called before endWriteSequence()!");
}
sequence.end();
writingSequence = false;
sequenceIndex = 0;
sequenceTIFFWriter = null;
sequenceLastIFDPos = -1;
imageOutput.flush();
@@ -1014,8 +1001,7 @@ public final class TIFFImageWriter extends ImageWriterBase {
protected void resetMembers() {
super.resetMembers();
writingSequence = false;
sequenceIndex = 0;
sequence.reset();
sequenceTIFFWriter = null;
sequenceLastIFDPos = -1;
}
+1 -1
View File
@@ -61,7 +61,7 @@
</modules>
<properties>
<junit.jupiter.version>5.14.2</junit.jupiter.version>
<junit.jupiter.version>5.14.3</junit.jupiter.version>
</properties>
<dependencies>
+3 -3
View File
@@ -191,7 +191,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>3.4.0</version>
<version>3.5.0</version>
<configuration>
<encoding>UTF-8</encoding>
</configuration>
@@ -265,7 +265,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.4</version>
<version>3.5.5</version>
<configuration>
<systemProperties>
<property>
@@ -300,7 +300,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-report-plugin</artifactId>
<version>3.5.4</version>
<version>3.5.5</version>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
+2 -2
View File
@@ -15,7 +15,7 @@
</description>
<properties>
<junit.jupiter.version>5.14.2</junit.jupiter.version>
<junit.jupiter.version>5.14.3</junit.jupiter.version>
</properties>
<dependencies>
@@ -82,7 +82,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.6.1</version>
<version>3.6.2</version>
<executions>
<execution>
<id>jakarta</id>