[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.
This commit is contained in:
KhanhTypo
2026-03-04 16:35:40 +07:00
committed by GitHub
parent 4c1b268325
commit a7a4445ce8
15 changed files with 985 additions and 40 deletions

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>

View File

@@ -30,25 +30,44 @@
package com.twelvemonkeys.imageio.plugins.dds;
@SuppressWarnings("unused")
interface DDS {
int MAGIC = ('D' << 24) + ('D' << 16) + ('S' << 8) + ' '; //Big-Endian
int HEADER_SIZE = 124;
// 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 DDSPF_SIZE = 32;
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;
//DXGI Formats (DX10)
int DXGI_FORMAT_BC1_UNORM = 71;
int DXGI_FORMAT_BC2_UNORM = 72;
int DXGI_FORMAT_BC3_UNORM = 77;
int DXGI_FORMAT_BC4_UNORM = 80;
int DXGI_FORMAT_BC5_UNORM = 83;
int DXGI_FORMAT_B8G8R8A8_UNORM_SRGB = 91;
int DXGI_FORMAT_B8G8R8X8_UNORM_SRGB = 93;
int DXGI_FORMAT_R8G8B8A8_UNORM_SRGB = 29;
//dwCaps
int DDSCAPS_COMPLEX = 0x8;
int DDSCAPS_MIPMAP = 0x400000;
int DDSCAPS_TEXTURE = 0x1000;
}

View File

@@ -0,0 +1,64 @@
package com.twelvemonkeys.imageio.plugins.dds;
/**
* Lists a number of supported encoders for block compressors and uncompressed types.
* <a href="https://learn.microsoft.com/en-us/windows/win32/api/dxgiformat/ne-dxgiformat-dxgi_format">DXGI Format List</a>
* <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>
*/
public enum DDSEncoderType {
BC1(DDSType.DXT1.value(), DDS.DXGI_FORMAT_BC1_UNORM, 8),
BC2(DDSType.DXT2.value(), DDS.DXGI_FORMAT_BC2_UNORM, 16),
BC3(DDSType.DXT5.value(), DDS.DXGI_FORMAT_BC3_UNORM, 16),
BC4(0x31495441, DDS.DXGI_FORMAT_BC4_UNORM, 8),
BC5(0x32495441, DDS.DXGI_FORMAT_BC5_UNORM, 16);
private final int fourCC;
private final int dx10DxgiFormat;
private final int bitCountOrBlockSize;
private final int[] rgbaMask;
//fourCC constructor
DDSEncoderType(int fourCC, int dx10DxgiFormat, int blockSize) {
this.fourCC = fourCC;
this.dx10DxgiFormat = dx10DxgiFormat;
bitCountOrBlockSize = blockSize;
rgbaMask = null;
}
//non-fourCC constructor (e.g. A8R8G8B8)
DDSEncoderType(int dx10DxgiFormat, int bitCount, int[] masks) {
fourCC = 0;
this.dx10DxgiFormat = dx10DxgiFormat;
bitCountOrBlockSize = bitCount;
rgbaMask = masks;
}
boolean isFourCC() {
return fourCC != 0;
}
int getFourCC() {
return fourCC;
}
boolean isAlphaMaskSupported() {
return !isFourCC() && rgbaMask[3] > 0;
}
boolean isBlockCompression() {
return this.isFourCC();
}
int getBitsOrBlockSize() {
return bitCountOrBlockSize;
}
public int[] getRGBAMask() {
return rgbaMask;
}
public int getDx10Format() {
return dx10DxgiFormat;
}
}

View File

@@ -71,7 +71,7 @@ final class DDSHeader {
throw new IIOException(String.format("Invalid DDS header size (expected %d): %d", DDS.HEADER_SIZE, dwSize));
}
// Verify flags
// Verify setFlags
header.flags = imageInput.readInt(); // [8,11]
if (!header.getFlag(DDS.FLAG_CAPS
| DDS.FLAG_HEIGHT

View File

@@ -0,0 +1,475 @@
package com.twelvemonkeys.imageio.plugins.dds;
import javax.imageio.stream.ImageOutputStream;
import java.awt.Color;
import java.awt.image.Raster;
import java.awt.image.RenderedImage;
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.
* <p>
* References:
* <p>
* [1] <a href="https://www.ludicon.com/castano/blog/2009/03/gpu-dxt-decompression/">GPU DXT Decompression</a>.
* [2] <a href="https://sv-journal.org/2014-1/06/en/index.php">TEXTURE COMPRESSION TECHNIQUES</a>.
* [3] <a href="https://mrelusive.com/publications/papers/Real-Time-Dxt-Compression.pdf">Real-Time DXT Compression by J.M.P. van Waveren</a>
* [4] <a href="https://registry.khronos.org/DataFormat/specs/1.4/dataformat.1.4.pdf">Khronos Data Format Specification v1.4 by Andrew Garrard</a>
* </p>
* </p>
*/
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, RenderedImage renderedImage, DDSEncoderType type) throws IOException {
switch (type) {
case BC1:
new BlockCompressor1(false).encode(imageOutput, renderedImage);
break;
case BC2:
new BlockCompressor2().encode(imageOutput, renderedImage);
break;
case BC3:
new BlockCompressor3().encode(imageOutput, renderedImage);
break;
case BC4:
new BlockCompressor4(BC4_CHANNEL_RED).encode(imageOutput, renderedImage);
break;
case BC5:
new BlockCompressor5().encode(imageOutput, renderedImage);
break;
default:
throw new IllegalArgumentException("DDS Type is not supported for encoder yet : " + type);
}
}
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, RenderedImage image) throws IOException {
int blocksXCount = (image.getWidth() + 3) / 4;
int blocksYCount = (image.getHeight() + 3) / 4;
Raster raster = image.getData();
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;
}
}
}

View File

@@ -30,8 +30,6 @@
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;
@@ -40,8 +38,7 @@ 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 +46,8 @@ import java.nio.ByteOrder;
import java.util.Collections;
import java.util.Iterator;
import static com.twelvemonkeys.imageio.util.IIOUtil.subsampleRow;
public final class DDSImageReader extends ImageReaderBase {
private DDSHeader header;

View File

@@ -0,0 +1,194 @@
package com.twelvemonkeys.imageio.plugins.dds;
import com.twelvemonkeys.imageio.ImageWriterBase;
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.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;
/**
* A designated class to begin writing DDS file with headers, class {@link DDSImageDataEncoder} will handle image data encoding process
*/
class DDSImageWriter extends ImageWriterBase {
protected DDSImageWriter(ImageWriterSpi provider) {
super(provider);
}
@Override
public DDSWriterParam getDefaultWriteParam() {
return DDSWriterParam.DEFAULT_PARAM;
}
@Override
public void write(IIOMetadata streamMetadata, IIOImage image, ImageWriteParam param) throws IOException {
assertOutput();
RenderedImage renderedImage = image.getRenderedImage();
ensureTextureSize(renderedImage);
ensureImageChannels(renderedImage);
DDSWriterParam ddsParam = param instanceof DDSWriterParam ? ((DDSWriterParam) param) : this.getDefaultWriteParam();
processImageStarted(0);
imageOutput.setByteOrder(ByteOrder.BIG_ENDIAN);
imageOutput.writeInt(DDS.MAGIC);
imageOutput.setByteOrder(ByteOrder.LITTLE_ENDIAN);
writeHeader(image, ddsParam);
writeDXT10Header(ddsParam);
//image data encoding
processImageProgress(0f);
DDSImageDataEncoder.writeImageData(imageOutput, renderedImage, ddsParam.getEncoderType());
processImageProgress(100f);
imageOutput.flush();
processImageComplete();
}
/**
* Checking if the image has 3 channels (RGB) or 4 channels (RGBA) and if image has 8 bits/channel.
*/
private void ensureImageChannels(RenderedImage renderedImage) {
Raster data = renderedImage.getData();
int numBands = data.getNumBands();
if (numBands < 3)
throw new IllegalStateException("Only image with 3 channels (RGB) or 4 channels (RGBA) is supported, got " + numBands + " channels");
int sampleSize = data.getSampleModel().getSampleSize(0);
if (sampleSize != 8)
throw new IllegalStateException("Only image with 8 bits/channel is supported, got " + sampleSize);
}
/**
* Checking if an image can be evenly divided into blocks of 4x4, ideally a power of 2.
* e.g. 16x16, 32x32, 512x128, 512x512, 1024x512, 1024x1024, 2048x1024, ...
*/
private void ensureTextureSize(RenderedImage renderedImage) {
int w = renderedImage.getWidth();
int h = renderedImage.getHeight();
if (w % 4 != 0 || h % 4 != 0)
throw new IllegalStateException(String.format("Image size must be dividable by 4, ideally a power of 2; got (%d x %d)", w, h));
}
private void writeHeader(IIOImage image, DDSWriterParam param) throws IOException {
imageOutput.writeInt(DDS.HEADER_SIZE);
imageOutput.writeInt(DDS.FLAG_CAPS | DDS.FLAG_HEIGHT | DDS.FLAG_WIDTH | DDS.FLAG_PIXELFORMAT | param.getOptionalBitFlags());
RenderedImage renderedImage = image.getRenderedImage();
int height = renderedImage.getHeight();
imageOutput.writeInt(height);
int width = renderedImage.getWidth();
imageOutput.writeInt(width);
writePitchOrLinearSize(height, width, param);
//dwDepth
imageOutput.writeInt(0);
//dwMipmapCount
imageOutput.writeInt(1);
//reserved
imageOutput.write(new byte[44]);
//pixFmt
writePixelFormat(param);
//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]);
}
//https://learn.microsoft.com/en-us/windows/win32/direct3ddds/dds-pixelformat
private void writePixelFormat(DDSWriterParam param) throws IOException {
imageOutput.writeInt(DDS.DDSPF_SIZE);
writePixelFormatFlags(param);
writeFourCC(param);
writeRGBAData(param);
}
private void writeDXT10Header(DDSWriterParam param) throws IOException {
if (param.isUsingDxt10()) {
//dxgiFormat
imageOutput.writeInt(param.getDxgiFormat());
//resourceDimension
imageOutput.writeInt(DDS.D3D10_RESOURCE_DIMENSION_TEXTURE2D);
//miscFlag
imageOutput.writeInt(0);
//arraySize
imageOutput.writeInt(1);
//miscFlag2
imageOutput.writeInt(0);
}
}
private void writeRGBAData(DDSWriterParam param) throws IOException {
if (!param.isUsingDxt10() && !param.getEncoderType().isFourCC()) {
//dwRGBBitCount
imageOutput.writeInt(param.getEncoderType().getBitsOrBlockSize());
int[] mask = param.getEncoderType().getRGBAMask();
//dwRBitMask
imageOutput.writeInt(mask[0]);
//dwGBitMask
imageOutput.writeInt(mask[1]);
//dwBitMask
imageOutput.writeInt(mask[2]);
//dwABitMask
imageOutput.writeInt(mask[3]);
} else {
//write 5 zero integers as fourCC is used
imageOutput.write(new byte[20]);
}
}
private void writeFourCC(DDSWriterParam param) throws IOException {
if (param.isUsingDxt10()) {
imageOutput.writeInt(DDSType.DXT10.value());
} else if (param.getEncoderType().isFourCC())
imageOutput.writeInt(param.getEncoderType().getFourCC());
}
private void writePixelFormatFlags(DDSWriterParam param) throws IOException {
if (param.isUsingDxt10() || param.getEncoderType().isFourCC()) {
imageOutput.writeInt(DDS.PIXEL_FORMAT_FLAG_FOURCC);
} else {
imageOutput.writeInt(DDS.PIXEL_FORMAT_FLAG_RGB | (param.getEncoderType().isAlphaMaskSupported() ? DDS.PIXEL_FORMAT_FLAG_ALPHAPIXELS : 0));
}
}
private void writePitchOrLinearSize(int height, int width, DDSWriterParam param) throws IOException {
DDSEncoderType type = param.getEncoderType();
int bitsOrBlockSize = type.getBitsOrBlockSize();
if (type.isBlockCompression()) {
imageOutput.writeInt(((width + 3) / 4) * ((height + 3) / 4) * bitsOrBlockSize);
} else {
imageOutput.writeInt(width * bitsOrBlockSize);
}
}
@Override
public IIOMetadata getDefaultImageMetadata(ImageTypeSpecifier imageType, ImageWriteParam param) {
throw new UnsupportedOperationException("Direct Draw Surface does not support metadata.");
}
@Override
public IIOMetadata convertImageMetadata(IIOMetadata inData, ImageTypeSpecifier imageType, ImageWriteParam param) {
throw new UnsupportedOperationException("Direct Draw Surface does not support metadata.");
}
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"))));
}
}

View File

@@ -0,0 +1,28 @@
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) {
return true;
}
@Override
public ImageWriter createWriterInstance(Object extension) {
return new DDSImageWriter(this);
}
@Override
public String getDescription(Locale locale) {
return "Direct Draw Surface (DDS) Image Writer";
}
}

View File

@@ -30,10 +30,10 @@
package com.twelvemonkeys.imageio.plugins.dds;
import javax.imageio.ImageTypeSpecifier;
import com.twelvemonkeys.imageio.StandardImageMetadataSupport;
import javax.imageio.ImageTypeSpecifier;
final class DDSMetadata extends StandardImageMetadataSupport {
DDSMetadata(ImageTypeSpecifier type, DDSHeader header) {
super(builder(type)

View File

@@ -41,10 +41,12 @@ final class DDSProviderInfo extends ReaderWriterProviderInfo {
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
"com.twelvemonkeys.imageio.plugins.dds.DDSImageWriter",
new String[]{"com.twelvemonkeys.imageio.plugins.dds.DDSImageWriterSpi"},
false, null, null,
null, null, true,
null, null, null,
null
);
}
}

View File

@@ -70,8 +70,8 @@ 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;
@@ -503,7 +503,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 +519,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 +527,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 +581,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;

View File

@@ -0,0 +1,139 @@
package com.twelvemonkeys.imageio.plugins.dds;
import javax.imageio.ImageWriteParam;
import java.util.Objects;
public class DDSWriterParam extends ImageWriteParam {
public static final DDSWriterParam DEFAULT_PARAM = DDSWriterParam.builder().formatBC5().build();
private final int optionalBitFlags;
private final DDSEncoderType encoderType;
private final boolean enableDxt10;
DDSWriterParam(int optionalBitFlags, DDSEncoderType encoderType, boolean isUsingDxt10) {
super();
this.optionalBitFlags = optionalBitFlags;
this.encoderType = encoderType;
this.enableDxt10 = isUsingDxt10;
}
public static Builder builder() {
return new Builder();
}
int getOptionalBitFlags() {
return this.optionalBitFlags;
}
DDSEncoderType getEncoderType() {
return this.encoderType;
}
public boolean isUsingDxt10() {
return enableDxt10;
}
int getDxgiFormat() {
return getEncoderType().getDx10Format();
}
public static final class Builder {
//we use Set collection to prevent duplications of bitflag setter calls
private int optionalBitFlag;
private DDSEncoderType encoderType;
private boolean isUsingDxt10;
public Builder() {
this.optionalBitFlag = 0;
encoderType = null;
isUsingDxt10 = false;
}
/**
* Enable saving file as Direct3D 10+ format.
*/
public Builder enableDX10() {
isUsingDxt10 = true;
return this;
}
/**
* Set the compression type to be BC1 (DXT1).
* If DXT10 is enabled, this will set DXGI Format to DXGI_FORMAT_BC1_UNORM.
*/
public Builder formatBC1() {
encoderType = DDSEncoderType.BC1;
return setFlag(DDSFlags.DDSD_LINEARSIZE);
}
/**
* Set the compression type to be BC2 (DXT3).
* If DXT10 is enabled, this will set DXGI Format to DXGI_FORMAT_BC2_UNORM.
*/
public Builder formatBC2() {
encoderType = DDSEncoderType.BC2;
return setFlag(DDSFlags.DDSD_LINEARSIZE);
}
/**
* Set the compression type to be BC3 (DXT5).
* If DXT10 is enabled, this will set DXGI Format to DXGI_FORMAT_BC3_UNORM.
*/
public Builder formatBC3() {
encoderType = DDSEncoderType.BC3;
return setFlag(DDSFlags.DDSD_LINEARSIZE);
}
/**
* Set the compression type to be BC4 (ATI1).
* If DXT10 is enabled, This will set DXGI Format to DXGI_FORMAT_BC4_UNORM.
*/
public Builder formatBC4() {
encoderType = DDSEncoderType.BC4;
return setFlag(DDSFlags.DDSD_LINEARSIZE);
}
/**
* Set the compression type to be BC5 (ATI2).
* This will set DXGI Format to DXGI_FORMAT_BC5_UNORM.
*/
public Builder formatBC5() {
encoderType = DDSEncoderType.BC5;
return setFlag(DDSFlags.DDSD_LINEARSIZE);
}
public Builder setFlag(DDSFlags flag) {
optionalBitFlag |= flag.getValue();
return this;
}
/**
* Set other optional flags for the DDS Header.
*/
public Builder setFlags(DDSFlags... flags) {
for (DDSFlags flag : flags)
setFlag(flag);
return this;
}
public DDSWriterParam build() {
Objects.requireNonNull(encoderType, "no DDS format specified.");
return new DDSWriterParam(optionalBitFlag, encoderType, isUsingDxt10);
}
public enum DDSFlags {
DDSD_PITCH(DDS.FLAG_PITCH),// Required when pitch is provided for an uncompressed texture.
DDSD_MIPMAPCOUNT(DDS.FLAG_MIPMAPCOUNT),// Required in a mipmapped texture.
DDSD_LINEARSIZE(DDS.FLAG_LINEARSIZE),// Required when pitch is provided for a compressed texture.
DDSD_DEPTH(DDS.FLAG_DEPTH);// Required in a depth texture.
private final int flag;
DDSFlags(int flag) {
this.flag = flag;
}
public int getValue() {
return flag;
}
}
}
}

View File

@@ -40,7 +40,7 @@ public enum DX10DXGIFormat {
/**
* @param acceptedValues values in DXGI Formats List, passed values are expected to be in ascending order
*/
private static IntPredicate exactly(int ... acceptedValues) {
private static IntPredicate exactly(int... acceptedValues) {
return test -> Arrays.binarySearch(acceptedValues, test) >= 0;
}

View File

@@ -0,0 +1 @@
com.twelvemonkeys.imageio.plugins.dds.DDSImageWriterSpi

View File

@@ -0,0 +1,22 @@
package com.twelvemonkeys.imageio.plugins.dds;
import com.twelvemonkeys.imageio.util.ImageWriterAbstractTest;
import javax.imageio.spi.ImageWriterSpi;
import java.awt.image.BufferedImage;
import java.util.Collections;
import java.util.List;
public class DDSImageWriterTest extends ImageWriterAbstractTest<DDSImageWriter> {
@Override
protected ImageWriterSpi createProvider() {
return new DDSImageWriterSpi();
}
@Override
protected List<BufferedImage> getTestData() {
return Collections.singletonList(
new BufferedImage(256, 256, BufferedImage.TYPE_INT_ARGB_PRE)
);
}
}