diff --git a/imageio/imageio-dds/pom.xml b/imageio/imageio-dds/pom.xml
index 6fb84275..0d97c670 100644
--- a/imageio/imageio-dds/pom.xml
+++ b/imageio/imageio-dds/pom.xml
@@ -43,6 +43,8 @@
+ * References: + *
+ * [1] GPU DXT Decompression. + * [2] TEXTURE COMPRESSION TECHNIQUES. + * [3] Real-Time DXT Compression by J.M.P. van Waveren + * [4] Khronos Data Format Specification v1.4 by Andrew Garrard + *
+ * + */ +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; + } + } +} \ No newline at end of file diff --git a/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSImageReader.java b/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSImageReader.java index deb77901..e6a40af2 100644 --- a/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSImageReader.java +++ b/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSImageReader.java @@ -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; diff --git a/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSImageWriter.java b/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSImageWriter.java new file mode 100644 index 00000000..2c4b1f40 --- /dev/null +++ b/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSImageWriter.java @@ -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")))); + } +} diff --git a/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSImageWriterSpi.java b/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSImageWriterSpi.java new file mode 100644 index 00000000..8a8931e7 --- /dev/null +++ b/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSImageWriterSpi.java @@ -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"; + } +} diff --git a/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSMetadata.java b/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSMetadata.java index ca168751..37e469ff 100755 --- a/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSMetadata.java +++ b/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSMetadata.java @@ -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) diff --git a/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSProviderInfo.java b/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSProviderInfo.java index 899292a9..33e8c823 100644 --- a/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSProviderInfo.java +++ b/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSProviderInfo.java @@ -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 ); } } diff --git a/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSReader.java b/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSReader.java index 8b40f963..03364fc9 100644 --- a/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSReader.java +++ b/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSReader.java @@ -70,8 +70,8 @@ import java.io.IOException; * Japanese document */ 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; diff --git a/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSWriterParam.java b/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSWriterParam.java new file mode 100644 index 00000000..a59269cc --- /dev/null +++ b/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DDSWriterParam.java @@ -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; + } + } + } +} diff --git a/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DX10DXGIFormat.java b/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DX10DXGIFormat.java index fa34ce8f..0a1678d7 100644 --- a/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DX10DXGIFormat.java +++ b/imageio/imageio-dds/src/main/java/com/twelvemonkeys/imageio/plugins/dds/DX10DXGIFormat.java @@ -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; } diff --git a/imageio/imageio-dds/src/main/resources/META-INF/services/javax.imageio.spi.ImageWriterSpi b/imageio/imageio-dds/src/main/resources/META-INF/services/javax.imageio.spi.ImageWriterSpi new file mode 100644 index 00000000..a52cc45e --- /dev/null +++ b/imageio/imageio-dds/src/main/resources/META-INF/services/javax.imageio.spi.ImageWriterSpi @@ -0,0 +1 @@ +com.twelvemonkeys.imageio.plugins.dds.DDSImageWriterSpi \ No newline at end of file diff --git a/imageio/imageio-dds/src/test/java/com/twelvemonkeys/imageio/plugins/dds/DDSImageWriterTest.java b/imageio/imageio-dds/src/test/java/com/twelvemonkeys/imageio/plugins/dds/DDSImageWriterTest.java new file mode 100644 index 00000000..2f38e086 --- /dev/null +++ b/imageio/imageio-dds/src/test/java/com/twelvemonkeys/imageio/plugins/dds/DDSImageWriterTest.java @@ -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