From 586359e7ab2f6b108d45a307cda46e89d3c3098b Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Sat, 8 Sep 2018 16:04:06 +0200 Subject: [PATCH] #379 TGA write --- .../imageio/plugins/tga/TGA.java | 2 + .../imageio/plugins/tga/TGAExtensions.java | 5 +- .../imageio/plugins/tga/TGAHeader.java | 143 ++++++++++-- .../imageio/plugins/tga/TGAImageReader.java | 4 +- .../plugins/tga/TGAImageWriteParam.java | 55 +++++ .../imageio/plugins/tga/TGAImageWriter.java | 218 ++++++++++++++++++ .../plugins/tga/TGAImageWriterSpi.java | 95 ++++++++ .../imageio/plugins/tga/TGAMetadata.java | 4 +- .../imageio/plugins/tga/TGAProviderInfo.java | 4 +- .../services/javax.imageio.spi.ImageWriterSpi | 1 + .../plugins/tga/TGAImageWriterTest.java | 112 +++++++++ 11 files changed, 614 insertions(+), 29 deletions(-) create mode 100644 imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAImageWriteParam.java create mode 100644 imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAImageWriter.java create mode 100644 imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAImageWriterSpi.java create mode 100644 imageio/imageio-tga/src/main/resources/META-INF/services/javax.imageio.spi.ImageWriterSpi create mode 100644 imageio/imageio-tga/src/test/java/com/twelvemonkeys/imageio/plugins/tga/TGAImageWriterTest.java diff --git a/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGA.java b/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGA.java index 5fc0e2f0..3903aaa2 100755 --- a/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGA.java +++ b/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGA.java @@ -36,6 +36,8 @@ interface TGA { /** Fixed header size: 18.*/ int HEADER_SIZE = 18; + int EXT_AREA_SIZE = 495; + /** No color map included. */ int COLORMAP_NONE = 0; /** Color map included. */ diff --git a/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAExtensions.java b/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAExtensions.java index 157ab7a1..1639f51f 100644 --- a/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAExtensions.java +++ b/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAExtensions.java @@ -36,6 +36,8 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.Calendar; +import static com.twelvemonkeys.imageio.plugins.tga.TGA.EXT_AREA_SIZE; + /** * TGAExtensions. * @@ -44,7 +46,6 @@ import java.util.Calendar; * @version $Id: TGAExtensions.java,v 1.0 27/07/15 harald.kuhr Exp$ */ final class TGAExtensions { - public static final int EXT_AREA_SIZE = 495; private String authorName; private String authorComments; @@ -77,7 +78,7 @@ final class TGAExtensions { } TGAExtensions extensions = new TGAExtensions(); - extensions.authorName = readString(stream, 41);; + extensions.authorName = readString(stream, 41); extensions.authorComments = readString(stream, 324); extensions.creationDate = readDate(stream); extensions.jobId = readString(stream, 41); diff --git a/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAHeader.java b/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAHeader.java index c3e4775e..32980eb5 100755 --- a/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAHeader.java +++ b/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAHeader.java @@ -31,11 +31,19 @@ package com.twelvemonkeys.imageio.plugins.tga; import javax.imageio.IIOException; +import javax.imageio.ImageWriteParam; import javax.imageio.stream.ImageInputStream; +import java.awt.image.ColorModel; import java.awt.image.IndexColorModel; +import java.awt.image.RenderedImage; import java.io.DataInput; +import java.io.DataOutput; import java.io.IOException; -import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +import static com.twelvemonkeys.lang.Validate.notNull; +import static java.awt.color.ColorSpace.TYPE_GRAY; +import static java.awt.color.ColorSpace.TYPE_RGB; final class TGAHeader { @@ -55,43 +63,44 @@ final class TGAHeader { private String identification; private IndexColorModel colorMap; - public int getImageType() { + int getImageType() { return imageType; } - public int getWidth() { + int getWidth() { return width; } - public int getHeight() { + int getHeight() { return height; } - public int getPixelDepth() { + int getPixelDepth() { return pixelDepth; } - public int getAttributeBits() { + int getAttributeBits() { return attributeBits; } - public int getOrigin() { + int getOrigin() { return origin; } - public int getInterleave() { + int getInterleave() { return interleave; } - public String getIdentification() { + String getIdentification() { return identification; } - public IndexColorModel getColorMap() { + IndexColorModel getColorMap() { return colorMap; } - @Override public String toString() { + @Override + public String toString() { return "TGAHeader{" + "colorMapType=" + colorMapType + ", imageType=" + imageType + @@ -110,7 +119,101 @@ final class TGAHeader { '}'; } - public static TGAHeader read(final ImageInputStream imageInput) throws IOException { + static TGAHeader from(final RenderedImage image, final ImageWriteParam param) { + notNull(image, "image"); + + ColorModel colorModel = image.getColorModel(); + IndexColorModel colorMap = colorModel instanceof IndexColorModel ? (IndexColorModel) colorModel : null; + + TGAHeader header = new TGAHeader(); + + header.colorMapType = colorMap != null ? 1 : 0; + header.imageType = getImageType(colorModel, param); + header.colorMapStart = 0; + header.colorMapSize = colorMap != null ? colorMap.getMapSize() : 0; + header.colorMapDepth = colorMap != null ? (colorMap.hasAlpha() ? 32 : 24) : 0; + + header.x = 0; + header.y = 0; + + header.width = image.getWidth(); // TODO: Param source region/subsampling might affect this + header.height = image.getHeight(); // // TODO: Param source region/subsampling might affect this + header.pixelDepth = colorModel.getPixelSize() == 15 ? 16 : colorModel.getPixelSize(); + + header.origin = TGA.ORIGIN_UPPER_LEFT; // TODO: Allow parameter to control this? + header.attributeBits = colorModel.hasAlpha() ? 8 : 0; // TODO: FixMe + + header.identification = null; + header.colorMap = colorMap; + + return header; + } + + private static int getImageType(final ColorModel colorModel, final ImageWriteParam param) { + int uncompressedType; + + if (colorModel instanceof IndexColorModel) { + uncompressedType = TGA.IMAGETYPE_COLORMAPPED; + } + else { + switch (colorModel.getColorSpace().getType()) { + case TYPE_RGB: + uncompressedType = TGA.IMAGETYPE_TRUECOLOR; + break; + case TYPE_GRAY: + uncompressedType = TGA.IMAGETYPE_MONOCHROME; + break; + + default: + throw new IllegalArgumentException("Unsupported color space for TGA: " + colorModel.getColorSpace()); + } + } + + return uncompressedType | (TGAImageWriteParam.isRLE(param) ? 8 : 0); + } + + void write(final DataOutput stream) throws IOException { + byte[] idBytes = identification != null ? identification.getBytes(StandardCharsets.US_ASCII) : new byte[0]; + + stream.writeByte(idBytes.length); + stream.writeByte(colorMapType); + stream.writeByte(imageType); + stream.writeShort(colorMapStart); + stream.writeShort(colorMapSize); + stream.writeByte(colorMapDepth); + + stream.writeShort(x); + stream.writeShort(y); + stream.writeShort(width); + stream.writeShort(height); + stream.writeByte(pixelDepth); + stream.writeByte(attributeBits | origin << 4 | interleave << 6); + + // Identification + stream.write(idBytes); + + // Color map + if (colorMap != null) { + int[] rgb = new int[colorMap.getMapSize()]; + colorMap.getRGBs(rgb); + + int components = colorMap.hasAlpha() ? 4 : 3; + byte[] cmap = new byte[rgb.length * components]; + for (int i = 0; i < rgb.length; i++) { + cmap[i * components ] = (byte) ((rgb[i] >> 16) & 0xff); + cmap[i * components + 1] = (byte) ((rgb[i] >> 8) & 0xff); + cmap[i * components + 2] = (byte) ((rgb[i] ) & 0xff); + + if (components == 4) { + cmap[i * components + 3] = (byte) ((rgb[i] >>> 24) & 0xff); + } + } + + stream.write(cmap); + } + } + + static TGAHeader read(final ImageInputStream imageInput) throws IOException { // typedef struct _TgaHeader // { // BYTE IDLength; /* 00h Size of Image ID field */ @@ -154,7 +257,7 @@ final class TGAHeader { byte[] idBytes = new byte[imageIdLength]; imageInput.readFully(idBytes); - header.identification = new String(idBytes, Charset.forName("US-ASCII")); + header.identification = new String(idBytes, StandardCharsets.US_ASCII); } // Color map, not *really* part of the header @@ -165,7 +268,7 @@ final class TGAHeader { return header; } - static IndexColorModel readColorMap(final DataInput stream, final TGAHeader header) throws IOException { + private static IndexColorModel readColorMap(final DataInput stream, final TGAHeader header) throws IOException { int size = header.colorMapSize; int depth = header.colorMapDepth; int bytes = (depth + 7) / 8; @@ -177,7 +280,7 @@ final class TGAHeader { switch (depth) { case 16: - // Expand 16 bit to 24 bit RGB + // Expand 16 (15) bit to 24 bit RGB byte[] temp = cmap; cmap = new byte[size * 3]; @@ -187,13 +290,9 @@ final class TGAHeader { byte low = temp[i * 2]; byte high = temp[i * 2 + 1]; - byte r = (byte) (8 * ((high & 0x7C) >> 2)); - byte g = (byte) (8 * ((high & 0x03) << 3 | (low & 0xE0) >> 5)); - byte b = (byte) (8 * ((low & 0x1F))); - - cmap[i * 3 ] = r; - cmap[i * 3 + 1] = g; - cmap[i * 3 + 2] = b; + cmap[i * 3 ] = (byte) (((high & 0x7C) >> 2) << 3); + cmap[i * 3 + 1] = (byte) (((high & 0x03) << 3 | (low & 0xE0) >> 5) << 3); + cmap[i * 3 + 2] = (byte) (((low & 0x1F)) << 3); } hasAlpha = false; diff --git a/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAImageReader.java b/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAImageReader.java index fbaba173..76b0c98d 100755 --- a/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAImageReader.java +++ b/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAImageReader.java @@ -58,14 +58,14 @@ import java.util.Arrays; import java.util.Iterator; import java.util.List; -public final class TGAImageReader extends ImageReaderBase { +final class TGAImageReader extends ImageReaderBase { // http://www.fileformat.info/format/tga/egff.htm // http://www.gamers.org/dEngine/quake3/TGA.txt private TGAHeader header; private TGAExtensions extensions; - protected TGAImageReader(final ImageReaderSpi provider) { + TGAImageReader(final ImageReaderSpi provider) { super(provider); } diff --git a/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAImageWriteParam.java b/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAImageWriteParam.java new file mode 100644 index 00000000..8e4e528e --- /dev/null +++ b/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAImageWriteParam.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2017, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.imageio.plugins.tga; + +import javax.imageio.ImageWriteParam; +import java.util.Locale; + +/** + * TGAImageWriteParam + */ +public final class TGAImageWriteParam extends ImageWriteParam { + @SuppressWarnings("unused") + public TGAImageWriteParam() { + this(null); + } + + @SuppressWarnings("WeakerAccess") + public TGAImageWriteParam(final Locale locale) { + super(locale); + + compressionTypes = new String[]{"None", "RLE"}; + } + + static boolean isRLE(final ImageWriteParam param) { + return param != null && param.getCompressionMode() == MODE_EXPLICIT && "RLE".equals(param.getCompressionType()); + } +} diff --git a/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAImageWriter.java b/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAImageWriter.java new file mode 100644 index 00000000..ea7944c6 --- /dev/null +++ b/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAImageWriter.java @@ -0,0 +1,218 @@ +/* + * Copyright (c) 2017, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.imageio.plugins.tga; + +import com.twelvemonkeys.imageio.ImageWriterBase; +import com.twelvemonkeys.imageio.util.ImageTypeSpecifiers; + +import javax.imageio.*; +import javax.imageio.metadata.IIOMetadata; +import javax.imageio.spi.ImageWriterSpi; +import java.awt.*; +import java.awt.color.ColorSpace; +import java.awt.image.*; +import java.io.File; +import java.io.IOException; +import java.nio.ByteOrder; + +import static com.twelvemonkeys.lang.Validate.notNull; + +/** + * TGAImageWriter + */ +final class TGAImageWriter extends ImageWriterBase { + TGAImageWriter(ImageWriterSpi provider) { + super(provider); + } + + @Override + public IIOMetadata getDefaultImageMetadata(final ImageTypeSpecifier imageType, final ImageWriteParam param) { + TGAHeader header = TGAHeader.from(imageType.createBufferedImage(1, 1), param); + return new TGAMetadata(header, null); + } + + @Override + public IIOMetadata convertImageMetadata(final IIOMetadata inData, final ImageTypeSpecifier imageType, final ImageWriteParam param) { + return null; + } + + @Override + public void setOutput(Object output) { + super.setOutput(output); + + if (imageOutput != null) { + imageOutput.setByteOrder(ByteOrder.LITTLE_ENDIAN); + } + } + + @Override + public void write(final IIOMetadata streamMetadata, final IIOImage image, final ImageWriteParam param) throws IOException { + assertOutput(); + + if (image.hasRaster()) { + throw new UnsupportedOperationException("Raster not supported"); + } + + RenderedImage renderedImage = image.getRenderedImage(); + TGAHeader header = TGAHeader.from(renderedImage, param); + + header.write(imageOutput); + + processImageStarted(0); + + WritableRaster rowRaster = header.getPixelDepth() == 32 + ? ImageTypeSpecifiers.createInterleaved(ColorSpace.getInstance(ColorSpace.CS_sRGB), new int[]{2, 1, 0, 3}, DataBuffer.TYPE_BYTE, true, false) + .createBufferedImage(renderedImage.getWidth(), 1) + .getRaster() + : renderedImage.getSampleModel().getTransferType() == DataBuffer.TYPE_INT + ? ImageTypeSpecifiers.createInterleaved(ColorSpace.getInstance(ColorSpace.CS_sRGB), new int[]{2, 1, 0}, DataBuffer.TYPE_BYTE, false, false) + .createBufferedImage(renderedImage.getWidth(), 1) + .getRaster() + : ImageTypeSpecifier.createFromRenderedImage(renderedImage) + .createBufferedImage(renderedImage.getWidth(), 1) + .getRaster(); + + DataBuffer buffer = rowRaster.getDataBuffer(); + + for (int tileY = 0; tileY < renderedImage.getNumYTiles(); tileY++) { + for (int tileX = 0; tileX < renderedImage.getNumXTiles(); tileX++) { + if (abortRequested()) { + break; + } + + // Wraps TYPE_INT rasters to TYPE_BYTE + Raster raster = asByteRaster(renderedImage.getTile(tileX, tileY), renderedImage.getColorModel()); + + for (int y = 0; y < raster.getHeight(); y++) { + if (abortRequested()) { + break; + } + + switch (buffer.getDataType()) { + case DataBuffer.TYPE_BYTE: + rowRaster.setDataElements(0, 0, raster.createChild(0, y, raster.getWidth(), 1, 0, 0, null)); + imageOutput.write(((DataBufferByte) buffer).getData()); + break; + case DataBuffer.TYPE_USHORT: + rowRaster.setDataElements(0, 0, raster.createChild(0, y, raster.getWidth(), 1, 0, 0, null)); + short[] shorts = ((DataBufferUShort) buffer).getData(); + imageOutput.writeShorts(shorts, 0, shorts.length); + break; + default: + throw new IIOException("Unsupported data"); + } + + processImageProgress(tileY * 100f / renderedImage.getNumYTiles()); + } + + } + } + + // TODO: If we have thumbnails, we need to write extension too. + + processImageComplete(); + + } + + // Vi kan lage en DataBuffer wrapper-klasse, + // som gjør TYPE_INT_RGB/INT_ARGB/INT_ARGB_PRE/INT_BGR til tilsvarende TYPE_xBYTE-klasser. + // Ytelse er ikke viktig her, siden vi uansett må konvertere når vi skal skrive/lese. + // TODO: Refactore dette til felles lag? + // TODO: Implementere writable også, slik at vi kan bruke i lesing? + private Raster asByteRaster(final Raster raster, ColorModel colorModel) { + switch (raster.getTransferType()) { + case DataBuffer.TYPE_BYTE: + return raster; + case DataBuffer.TYPE_USHORT: + return raster; // TODO: we handle ushort especially for now.. + case DataBuffer.TYPE_INT: + final int bands = colorModel.getNumComponents(); + final DataBufferInt buffer = (DataBufferInt) raster.getDataBuffer(); + + int w = raster.getWidth(); + int h = raster.getHeight(); + int size = buffer.getSize(); + + return new Raster( + new PixelInterleavedSampleModel(DataBuffer.TYPE_BYTE, w, h, bands, w * bands, createBandOffsets(colorModel)), + new DataBuffer(DataBuffer.TYPE_BYTE, size * bands) { + @Override + public int getElem(int bank, int i) { + int index = i / bands; + int shift = (i % bands) * 8; + + return (buffer.getElem(index) >>> shift) & 0xFF; + } + + @Override + public void setElem(int bank, int i, int val) { + throw new UnsupportedOperationException("Wrapped buffer is read-only"); + } + }, new Point()) {}; + default: + throw new IllegalArgumentException(String.format("Raster type %d not supported", raster.getTransferType())); + } + } + + private int[] createBandOffsets(final ColorModel colorModel) { + notNull(colorModel, "colorModel"); + + if (colorModel instanceof DirectColorModel) { + DirectColorModel dcm = (DirectColorModel) colorModel; + int[] masks = dcm.getMasks(); + int[] offs = new int[masks.length]; + + for (int i = 0; i < masks.length; i++) { + int mask = masks[i]; + int off = 0; + + // TODO: FixMe! This only works for standard 8 bit masks (0xFF) + if (mask != 0) { + while ((mask & 0xFF) == 0) { + mask >>>= 8; + off++; + } + } + + offs[i] = off; + } + + return offs; + } + + throw new IllegalArgumentException(String.format("%s not supported", colorModel.getClass().getSimpleName())); + } + + public static void main(String[] args) throws IOException { + BufferedImage image = ImageIO.read(new File(args[0])); + ImageIO.write(image, "TGA", new File("foo.tga")); + } +} diff --git a/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAImageWriterSpi.java b/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAImageWriterSpi.java new file mode 100644 index 00000000..449cfee4 --- /dev/null +++ b/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAImageWriterSpi.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2017, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.imageio.plugins.tga; + +import com.twelvemonkeys.imageio.spi.ImageWriterSpiBase; + +import javax.imageio.ImageTypeSpecifier; +import javax.imageio.ImageWriter; +import java.awt.color.ColorSpace; +import java.awt.image.BufferedImage; +import java.util.Locale; + +/** + * TGAImageWriterSpi + */ +public final class TGAImageWriterSpi extends ImageWriterSpiBase { + @SuppressWarnings("WeakerAccess") + public TGAImageWriterSpi() { + super(new TGAProviderInfo()); + } + + @Override + public boolean canEncodeImage(final ImageTypeSpecifier type) { + // Fast case, it's a known, supported type + switch (type.getBufferedImageType()) { + case BufferedImage.TYPE_INT_RGB: + case BufferedImage.TYPE_INT_ARGB: + case BufferedImage.TYPE_INT_ARGB_PRE: + case BufferedImage.TYPE_3BYTE_BGR: + case BufferedImage.TYPE_4BYTE_ABGR: + case BufferedImage.TYPE_4BYTE_ABGR_PRE: + case BufferedImage.TYPE_USHORT_555_RGB: + case BufferedImage.TYPE_BYTE_GRAY: + case BufferedImage.TYPE_BYTE_INDEXED: + return true; + case BufferedImage.TYPE_BYTE_BINARY: // Could be supported? Uncertain if the format allows < 8 bit/sample for color map entries + case BufferedImage.TYPE_INT_BGR: // TODO: Should be supported, just needs to invert band indices/offsets + case BufferedImage.TYPE_USHORT_565_RGB: + case BufferedImage.TYPE_USHORT_GRAY: + return false; + default: + // Fall through + } + + // Inspect color model etc. + ColorSpace colorSpace = type.getColorModel().getColorSpace(); + if (!(colorSpace.getType() == ColorSpace.TYPE_RGB || colorSpace.getType() == ColorSpace.TYPE_GRAY)) { + return false; + } + + + + // TODO! + + return true; + } + + @Override + public ImageWriter createWriterInstance(Object extension) { + return new TGAImageWriter(this); + } + + @Override + public String getDescription(Locale locale) { + return "TrueVision TGA image writer"; + } +} diff --git a/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAMetadata.java b/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAMetadata.java index 7df3e9aa..3932b230 100755 --- a/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAMetadata.java +++ b/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAMetadata.java @@ -37,12 +37,14 @@ import java.awt.*; import java.awt.image.IndexColorModel; import java.util.Calendar; +import static com.twelvemonkeys.lang.Validate.notNull; + final class TGAMetadata extends AbstractMetadata { private final TGAHeader header; private final TGAExtensions extensions; TGAMetadata(final TGAHeader header, final TGAExtensions extensions) { - this.header = header; + this.header = notNull(header, "header"); this.extensions = extensions; } diff --git a/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAProviderInfo.java b/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAProviderInfo.java index beda1535..72151d77 100644 --- a/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAProviderInfo.java +++ b/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAProviderInfo.java @@ -55,8 +55,8 @@ final class TGAProviderInfo extends ReaderWriterProviderInfo { }, "com.twelvemonkeys.imageio.plugins.tga.TGAImageReader", new String[] {"com.twelvemonkeys.imageio.plugins.tga.TGAImageReaderSpi"}, - null, - null, + "com.twelvemonkeys.imageio.plugins.tga.TGAImageWriter", + new String[] {"com.twelvemonkeys.imageio.plugins.tga.TGAImageWriterSpi"}, false, null, null, null, null, true, null, null, null, null ); diff --git a/imageio/imageio-tga/src/main/resources/META-INF/services/javax.imageio.spi.ImageWriterSpi b/imageio/imageio-tga/src/main/resources/META-INF/services/javax.imageio.spi.ImageWriterSpi new file mode 100644 index 00000000..8d153f1a --- /dev/null +++ b/imageio/imageio-tga/src/main/resources/META-INF/services/javax.imageio.spi.ImageWriterSpi @@ -0,0 +1 @@ +com.twelvemonkeys.imageio.plugins.tga.TGAImageWriterSpi diff --git a/imageio/imageio-tga/src/test/java/com/twelvemonkeys/imageio/plugins/tga/TGAImageWriterTest.java b/imageio/imageio-tga/src/test/java/com/twelvemonkeys/imageio/plugins/tga/TGAImageWriterTest.java new file mode 100644 index 00000000..802a00f8 --- /dev/null +++ b/imageio/imageio-tga/src/test/java/com/twelvemonkeys/imageio/plugins/tga/TGAImageWriterTest.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2017, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.imageio.plugins.tga; + +import com.twelvemonkeys.imageio.util.ImageTypeSpecifiers; +import com.twelvemonkeys.imageio.util.ImageWriterAbstractTest; +import org.junit.Test; + +import javax.imageio.ImageIO; +import javax.imageio.ImageReader; +import javax.imageio.ImageTypeSpecifier; +import javax.imageio.ImageWriter; +import javax.imageio.stream.ImageInputStream; +import javax.imageio.stream.ImageOutputStream; +import java.awt.color.ColorSpace; +import java.awt.image.BufferedImage; +import java.awt.image.DataBuffer; +import java.awt.image.RenderedImage; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Arrays; +import java.util.List; + +import static com.twelvemonkeys.imageio.util.ImageReaderAbstractTest.assertImageDataEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assume.assumeNotNull; + +/** + * TGAImageWriterTest + */ +public class TGAImageWriterTest extends ImageWriterAbstractTest { + + private static final TGAImageWriterSpi PROVIDER = new TGAImageWriterSpi(); + private static final ImageTypeSpecifier TYPE_USHORT_1555_ARGB = ImageTypeSpecifiers.createPacked(ColorSpace.getInstance(ColorSpace.CS_sRGB), 0x7C00, 0x03E0, 0x001F, 0x8000, DataBuffer.TYPE_USHORT, false); + + @Override + protected ImageWriter createImageWriter() { + return new TGAImageWriter(PROVIDER); + } + + @Override + protected List getTestData() { + return Arrays.asList( + drawSomething(new BufferedImage(10, 10, BufferedImage.TYPE_INT_RGB)), + drawSomething(new BufferedImage(10, 10, BufferedImage.TYPE_INT_ARGB)), + drawSomething(new BufferedImage(10, 10, BufferedImage.TYPE_INT_ARGB_PRE)), + drawSomething(new BufferedImage(10, 10, BufferedImage.TYPE_INT_BGR)), + drawSomething(new BufferedImage(10, 10, BufferedImage.TYPE_3BYTE_BGR)), + drawSomething(new BufferedImage(10, 10, BufferedImage.TYPE_4BYTE_ABGR)), + drawSomething(new BufferedImage(10, 10, BufferedImage.TYPE_4BYTE_ABGR_PRE)), + drawSomething(new BufferedImage(10, 10, BufferedImage.TYPE_USHORT_555_RGB)), + drawSomething(new BufferedImage(10, 10, BufferedImage.TYPE_BYTE_GRAY)), + drawSomething(new BufferedImage(10, 10, BufferedImage.TYPE_BYTE_INDEXED)), + drawSomething(TYPE_USHORT_1555_ARGB.createBufferedImage(10, 10)) + ); + } + + @Test + public void testWriteRead() throws IOException { + ImageWriter writer = createImageWriter(); + ImageReader reader = ImageIO.getImageReader(writer); + + assumeNotNull(reader); + + for (RenderedImage testData : getTestData()) { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + + try (ImageOutputStream stream = ImageIO.createImageOutputStream(buffer)) { + writer.setOutput(stream); + writer.write(drawSomething((BufferedImage) testData)); + } + + try (ImageInputStream stream = ImageIO.createImageInputStream(new ByteArrayInputStream(buffer.toByteArray()))) { + reader.setInput(stream); + + BufferedImage image = reader.read(0); + + assertNotNull(image); + assertImageDataEquals("Images differ for " + testData, (BufferedImage) testData, image); + } + } + } +} \ No newline at end of file