mirror of
https://github.com/haraldk/TwelveMonkeys.git
synced 2026-03-17 00:00:06 -04:00
[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:
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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"))));
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
com.twelvemonkeys.imageio.plugins.dds.DDSImageWriterSpi
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user