From fac9f1a9274a35263a425b37ddb98736cd586162 Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Thu, 8 Apr 2021 19:29:26 +0200 Subject: [PATCH] #532 Write TGA with RLE compression. --- .../io/enc/EncoderAbstractTest.java | 32 +- .../imageio/plugins/tga/RLEDecoder.java | 3 +- .../imageio/plugins/tga/RLEEncoder.java | 117 +++++ .../imageio/plugins/tga/TGAExtensions.java | 30 +- .../imageio/plugins/tga/TGAHeader.java | 13 +- .../plugins/tga/TGAImageWriteParam.java | 27 +- .../imageio/plugins/tga/TGAImageWriter.java | 56 +- .../imageio/plugins/tga/TGAMetadata.java | 15 +- .../imageio/plugins/tga/RLEDecoderTest.java | 140 +++++ .../imageio/plugins/tga/RLEEncoderTest.java | 135 +++++ .../plugins/tga/TGAImageWriteParamTest.java | 143 ++++++ .../plugins/tga/TGAImageWriterTest.java | 135 ++++- .../imageio/plugins/tga/TGAMetadataTest.java | 486 ++++++++++++++++++ 13 files changed, 1265 insertions(+), 67 deletions(-) create mode 100755 imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/RLEEncoder.java create mode 100644 imageio/imageio-tga/src/test/java/com/twelvemonkeys/imageio/plugins/tga/RLEDecoderTest.java create mode 100644 imageio/imageio-tga/src/test/java/com/twelvemonkeys/imageio/plugins/tga/RLEEncoderTest.java create mode 100644 imageio/imageio-tga/src/test/java/com/twelvemonkeys/imageio/plugins/tga/TGAImageWriteParamTest.java create mode 100644 imageio/imageio-tga/src/test/java/com/twelvemonkeys/imageio/plugins/tga/TGAMetadataTest.java diff --git a/common/common-io/src/test/java/com/twelvemonkeys/io/enc/EncoderAbstractTest.java b/common/common-io/src/test/java/com/twelvemonkeys/io/enc/EncoderAbstractTest.java index 23b4e057..863f04b7 100644 --- a/common/common-io/src/test/java/com/twelvemonkeys/io/enc/EncoderAbstractTest.java +++ b/common/common-io/src/test/java/com/twelvemonkeys/io/enc/EncoderAbstractTest.java @@ -32,13 +32,13 @@ package com.twelvemonkeys.io.enc; import com.twelvemonkeys.io.FileUtil; import com.twelvemonkeys.lang.ObjectAbstractTest; + import org.junit.Test; import java.io.*; -import java.util.Arrays; import java.util.Random; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.fail; /** @@ -73,7 +73,7 @@ public abstract class EncoderAbstractTest extends ObjectAbstractTest { } } - private byte[] createData(final int pLength) throws Exception { + private byte[] createData(final int pLength) { byte[] bytes = new byte[pLength]; RANDOM.nextBytes(bytes); return bytes; @@ -82,9 +82,8 @@ public abstract class EncoderAbstractTest extends ObjectAbstractTest { private void runStreamTest(final int pLength) throws Exception { byte[] data = createData(pLength); ByteArrayOutputStream outBytes = new ByteArrayOutputStream(); - OutputStream out = new EncoderStream(outBytes, createEncoder(), true); - try { + try (OutputStream out = new EncoderStream(outBytes, createEncoder(), true)) { // Provoke failure for encoders that doesn't take array offset properly into account int off = (data.length + 1) / 2; out.write(data, 0, off); @@ -92,9 +91,6 @@ public abstract class EncoderAbstractTest extends ObjectAbstractTest { out.write(data, off, data.length - off); } } - finally { - out.close(); - } byte[] encoded = outBytes.toByteArray(); @@ -102,7 +98,7 @@ public abstract class EncoderAbstractTest extends ObjectAbstractTest { // System.err.println("encoded: " + Arrays.toString(encoded)); byte[] decoded = FileUtil.read(new DecoderStream(new ByteArrayInputStream(encoded), createCompatibleDecoder())); - assertTrue(Arrays.equals(data, decoded)); + assertArrayEquals(data, decoded); InputStream in = new DecoderStream(new ByteArrayInputStream(encoded), createCompatibleDecoder()); outBytes = new ByteArrayOutputStream(); @@ -116,7 +112,7 @@ public abstract class EncoderAbstractTest extends ObjectAbstractTest { } decoded = outBytes.toByteArray(); - assertTrue(Arrays.equals(data, decoded)); + assertArrayEquals(data, decoded); } @Test @@ -129,10 +125,6 @@ public abstract class EncoderAbstractTest extends ObjectAbstractTest { e.printStackTrace(); fail(e.getMessage() + ": " + i); } - catch (Exception e) { - e.printStackTrace(); - fail(e.getMessage() + ": " + i); - } } for (int i = 100; i < 2000; i += 250) { @@ -143,10 +135,6 @@ public abstract class EncoderAbstractTest extends ObjectAbstractTest { e.printStackTrace(); fail(e.getMessage() + ": " + i); } - catch (Exception e) { - e.printStackTrace(); - fail(e.getMessage() + ": " + i); - } } for (int i = 2000; i < 80000; i += 1000) { @@ -157,14 +145,8 @@ public abstract class EncoderAbstractTest extends ObjectAbstractTest { e.printStackTrace(); fail(e.getMessage() + ": " + i); } - catch (Exception e) { - e.printStackTrace(); - fail(e.getMessage() + ": " + i); - } } } - // TODO: Test that the transition from byte[] to ByteBuffer didn't introduce bugs when writing to a wrapped array with offset. - - + // TODO: Test that the transition from byte[] to ByteBuffer didn't introduce bugs when writing to a wrapped array with offset. } diff --git a/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/RLEDecoder.java b/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/RLEDecoder.java index 6a1ee98e..dbfbda11 100755 --- a/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/RLEDecoder.java +++ b/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/RLEDecoder.java @@ -63,7 +63,8 @@ final class RLEDecoder implements Decoder { buffer.put((byte) data); } - } else { + } + else { for (int b = 0; b < pixel.length; b++) { int data = stream.read(); if (data < 0) { diff --git a/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/RLEEncoder.java b/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/RLEEncoder.java new file mode 100755 index 00000000..6cc33585 --- /dev/null +++ b/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/RLEEncoder.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2021, 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.io.enc.Encoder; +import com.twelvemonkeys.lang.Validate; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteBuffer; + +final class RLEEncoder implements Encoder { + + private final int pixelSize; + + RLEEncoder(final int pixelDepth) { + Validate.isTrue(pixelDepth % Byte.SIZE == 0, "Depth must be a multiple of bytes (8 bits)"); + pixelSize = pixelDepth / Byte.SIZE; + } + + public void encode(final OutputStream stream, final ByteBuffer buffer) throws IOException { + encode(stream, buffer.array(), buffer.arrayOffset() + buffer.position(), buffer.remaining()); + buffer.position(buffer.remaining()); + } + + private void encode(final OutputStream stream, final byte[] buffer, final int pOffset, final int length) throws IOException { + // NOTE: It's best to encode a 2 byte repeat + // run as a replicate run except when preceded and followed by a + // literal run, in which case it's best to merge the three into one + // literal run. Always encode 3 byte repeats as replicate runs. + // Worst case: output = input + (input + 127) / 128 + + int offset = pOffset; + final int max = pOffset + length - pixelSize; + final int maxMinus1 = max - pixelSize; + + while (offset <= max) { + // Compressed run + int run = 1; + while (run < 127 && offset < max && equalPixel(buffer, offset, offset + pixelSize)) { + offset += pixelSize; + run++; + } + + if (run > 1) { + stream.write(0x80 | (run - 1)); + stream.write(buffer, offset, pixelSize); + offset += pixelSize; + } + + // Literal run + int runStart = offset; + run = 0; + while ((run < 127 && ((offset < max && !(equalPixel(buffer, offset, offset + pixelSize))) + || (offset < maxMinus1 && !(equalPixel(buffer, offset, offset + 2 * pixelSize)))))) { + offset += pixelSize; + run++; + } + + // If last pixel, include it in literal run, if space + if (offset == max && run > 0 && run < 127) { + offset += pixelSize; + run++; + } + + if (run > 0) { + stream.write(run - 1); + stream.write(buffer, runStart, run * pixelSize); + } + + // If last pixel, and not space, start new literal run + if (offset == max && (run <= 0 || run >= 127)) { + stream.write(0); + stream.write(buffer, offset, pixelSize); + offset += pixelSize; + } + } + } + + private boolean equalPixel(final byte[] buffer, final int offset, int compareOffset) { + for (int i = 0; i < pixelSize; i++) { + if (buffer[offset + i] != buffer[compareOffset + i]) { + return false; + } + } + + return true; + } +} 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 39ee721c..0b3048c9 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 @@ -47,26 +47,26 @@ import static com.twelvemonkeys.imageio.plugins.tga.TGA.EXT_AREA_SIZE; */ final class TGAExtensions { - private String authorName; - private String authorComments; + String authorName; + String authorComments; - private Calendar creationDate; - private String jobId; + Calendar creationDate; + String jobId; - private String softwareId; - private String softwareVersion; + String softwareId; + String softwareVersion; - private int backgroundColor; - private double pixelAspectRatio; - private double gamma; + int backgroundColor; + double pixelAspectRatio; + double gamma; - private long colorCorrectionOffset; - private long postageStampOffset; - private long scanLineOffset; + long colorCorrectionOffset; + long postageStampOffset; + long scanLineOffset; - private int attributeType; + int attributeType; - private TGAExtensions() { + TGAExtensions() { } static TGAExtensions read(final ImageInputStream stream) throws IOException { @@ -142,6 +142,7 @@ final class TGAExtensions { return null; } + //noinspection MagicConstant calendar.set(year, month - 1, date, hourOfDay, minute, second); return calendar; @@ -176,6 +177,7 @@ final class TGAExtensions { } } + @SuppressWarnings("SwitchStatementWithTooFewBranches") public boolean isAlphaPremultiplied() { switch (attributeType) { case 4: 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 8b58fb23..e40e6fc6 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,7 +31,6 @@ 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; @@ -58,9 +57,9 @@ final class TGAHeader { private int height; private int pixelDepth; private int attributeBits; - private int origin; + int origin; private int interleave; - private String identification; + String identification; private IndexColorModel colorMap; int getImageType() { @@ -119,7 +118,7 @@ final class TGAHeader { '}'; } - static TGAHeader from(final RenderedImage image, final ImageWriteParam param) { + static TGAHeader from(final RenderedImage image, final boolean compressed) { notNull(image, "image"); ColorModel colorModel = image.getColorModel(); @@ -128,7 +127,7 @@ final class TGAHeader { TGAHeader header = new TGAHeader(); header.colorMapType = colorMap != null ? 1 : 0; - header.imageType = getImageType(colorModel, param); + header.imageType = getImageType(colorModel, compressed); header.colorMapStart = 0; header.colorMapSize = colorMap != null ? colorMap.getMapSize() : 0; header.colorMapDepth = colorMap != null ? (colorMap.hasAlpha() ? 32 : 24) : 0; @@ -149,7 +148,7 @@ final class TGAHeader { return header; } - private static int getImageType(final ColorModel colorModel, final ImageWriteParam param) { + private static int getImageType(final ColorModel colorModel, final boolean compressed) { int uncompressedType; if (colorModel instanceof IndexColorModel) { @@ -169,7 +168,7 @@ final class TGAHeader { } } - return uncompressedType | (TGAImageWriteParam.isRLE(param) ? 8 : 0); + return uncompressedType | (compressed ? 8 : 0); } void write(final DataOutput stream) throws IOException { 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 index 8e4e528e..7c9bb0be 100644 --- 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 @@ -30,7 +30,13 @@ package com.twelvemonkeys.imageio.plugins.tga; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + import javax.imageio.ImageWriteParam; +import javax.imageio.metadata.IIOMetadata; +import javax.imageio.metadata.IIOMetadataFormatImpl; +import javax.imageio.metadata.IIOMetadataNode; import java.util.Locale; /** @@ -42,14 +48,29 @@ public final class TGAImageWriteParam extends ImageWriteParam { this(null); } - @SuppressWarnings("WeakerAccess") public TGAImageWriteParam(final Locale locale) { super(locale); + canWriteCompressed = true; compressionTypes = new String[]{"None", "RLE"}; } - static boolean isRLE(final ImageWriteParam param) { - return param != null && param.getCompressionMode() == MODE_EXPLICIT && "RLE".equals(param.getCompressionType()); + static boolean isRLE(final ImageWriteParam param, final IIOMetadata metadata) { + return (param == null || param.canWriteCompressed() && param.getCompressionMode() == MODE_COPY_FROM_METADATA) && "RLE".equals(compressionTypeFromMetadata(metadata)) + || param != null && param.canWriteCompressed() && param.getCompressionMode() == MODE_EXPLICIT && "RLE".equals(param.getCompressionType()); + } + + private static String compressionTypeFromMetadata(final IIOMetadata metadata) { + if (metadata != null) { + IIOMetadataNode root = (IIOMetadataNode) metadata.getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName); + NodeList compressionTypeName = root.getElementsByTagName("CompressionTypeName"); + + if (compressionTypeName.getLength() > 0) { + Node value = compressionTypeName.item(0).getAttributes().getNamedItem("value"); + return value != null ? value.getNodeValue() : null; + } + } + + return null; } } 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 index b5696876..9dbedc87 100644 --- 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 @@ -31,18 +31,26 @@ package com.twelvemonkeys.imageio.plugins.tga; import com.twelvemonkeys.imageio.ImageWriterBase; +import com.twelvemonkeys.imageio.util.IIOUtil; import com.twelvemonkeys.imageio.util.ImageTypeSpecifiers; +import com.twelvemonkeys.io.LittleEndianDataOutputStream; +import com.twelvemonkeys.io.enc.EncoderStream; +import com.twelvemonkeys.lang.Validate; import javax.imageio.*; import javax.imageio.metadata.IIOMetadata; import javax.imageio.spi.ImageWriterSpi; +import javax.imageio.stream.ImageOutputStream; import java.awt.*; import java.awt.color.ColorSpace; import java.awt.image.*; +import java.io.DataOutput; import java.io.File; import java.io.IOException; +import java.nio.ByteBuffer; import java.nio.ByteOrder; +import static com.twelvemonkeys.imageio.plugins.tga.TGAImageWriteParam.isRLE; import static com.twelvemonkeys.lang.Validate.notNull; /** @@ -55,13 +63,23 @@ final class TGAImageWriter extends ImageWriterBase { @Override public IIOMetadata getDefaultImageMetadata(final ImageTypeSpecifier imageType, final ImageWriteParam param) { - TGAHeader header = TGAHeader.from(imageType.createBufferedImage(1, 1), param); + Validate.notNull(imageType, "imageType"); + + TGAHeader header = TGAHeader.from(imageType.createBufferedImage(1, 1), isRLE(param, null)); return new TGAMetadata(header, null); } @Override public IIOMetadata convertImageMetadata(final IIOMetadata inData, final ImageTypeSpecifier imageType, final ImageWriteParam param) { - return null; + Validate.notNull(inData, "inData"); + Validate.notNull(imageType, "imageType"); + + if (inData instanceof TGAMetadata) { + return inData; + } + + // TODO: Make metadata mutable, and do actual merge + return getDefaultImageMetadata(imageType, param); } @Override @@ -73,16 +91,23 @@ final class TGAImageWriter extends ImageWriterBase { } } + @Override + public ImageWriteParam getDefaultWriteParam() { + return new TGAImageWriteParam(getLocale()); + } + @Override public void write(final IIOMetadata streamMetadata, final IIOImage image, final ImageWriteParam param) throws IOException { assertOutput(); + Validate.notNull(image, "image"); if (image.hasRaster()) { throw new UnsupportedOperationException("Raster not supported"); } + final boolean compressed = isRLE(param, image.getMetadata()); RenderedImage renderedImage = image.getRenderedImage(); - TGAHeader header = TGAHeader.from(renderedImage, param); + TGAHeader header = TGAHeader.from(renderedImage, compressed); header.write(imageOutput); @@ -94,7 +119,7 @@ final class TGAImageWriter extends ImageWriterBase { ? 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(); + final DataBuffer buffer = rowRaster.getDataBuffer(); for (int tileY = 0; tileY < renderedImage.getNumYTiles(); tileY++) { for (int tileX = 0; tileX < renderedImage.getNumXTiles(); tileX++) { @@ -110,6 +135,8 @@ final class TGAImageWriter extends ImageWriterBase { break; } + DataOutput imageOutput = compressed ? createRLEStream(header, this.imageOutput) : this.imageOutput; + switch (buffer.getDataType()) { case DataBuffer.TYPE_BYTE: rowRaster.setDataElements(0, 0, raster.createChild(0, y, raster.getWidth(), 1, 0, 0, null)); @@ -118,22 +145,37 @@ final class TGAImageWriter extends ImageWriterBase { 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); + + // TODO: Get rid of this, due to stupid design in EncoderStream... + ByteBuffer bb = ByteBuffer.allocate(shorts.length * 2); + bb.order(ByteOrder.LITTLE_ENDIAN); + bb.asShortBuffer().put(shorts); + imageOutput.write(bb.array()); + // TODO: The below should work just as good +// for (short value : shorts) { +// imageOutput.writeShort(value); +// } break; default: - throw new IIOException("Unsupported data"); + throw new IIOException("Unsupported data type"); } - processImageProgress(tileY * 100f / renderedImage.getNumYTiles()); + if (compressed) { + ((LittleEndianDataOutputStream) imageOutput).close(); + } } + processImageProgress(tileY * 100f / renderedImage.getNumYTiles()); } } // TODO: If we have thumbnails, we need to write extension too. processImageComplete(); + } + private static LittleEndianDataOutputStream createRLEStream(final TGAHeader header, final ImageOutputStream stream) { + return new LittleEndianDataOutputStream(new EncoderStream(IIOUtil.createStreamAdapter(stream), new RLEEncoder(header.getPixelDepth()))); } // TODO: Refactor to common util 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 241a800a..f3755c9c 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 @@ -78,7 +78,12 @@ final class TGAMetadata extends AbstractMetadata { chroma.appendChild(numChannels); switch (header.getPixelDepth()) { case 8: - numChannels.setAttribute("value", Integer.toString(1)); + if (header.getImageType() == TGA.IMAGETYPE_MONOCHROME || header.getImageType() == TGA.IMAGETYPE_MONOCHROME_RLE) { + numChannels.setAttribute("value", Integer.toString(1)); + } + else { + numChannels.setAttribute("value", Integer.toString(3)); + } break; case 16: if (header.getAttributeBits() > 0 && extensions != null && extensions.hasAlpha()) { @@ -146,7 +151,7 @@ final class TGAMetadata extends AbstractMetadata { IIOMetadataNode compressionTypeName = new IIOMetadataNode("CompressionTypeName"); node.appendChild(compressionTypeName); String value = header.getImageType() == TGA.IMAGETYPE_COLORMAPPED_HUFFMAN || header.getImageType() == TGA.IMAGETYPE_COLORMAPPED_HUFFMAN_QUADTREE - ? "Uknown" : "RLE"; + ? "Unknown" : "RLE"; compressionTypeName.setAttribute("value", value); IIOMetadataNode lossless = new IIOMetadataNode("Lossless"); @@ -155,7 +160,7 @@ final class TGAMetadata extends AbstractMetadata { return node; default: - // No compreesion + // No compression return null; } } @@ -199,10 +204,10 @@ final class TGAMetadata extends AbstractMetadata { } break; case 24: - bitsPerSample.setAttribute("value", createListValue(3, Integer.toString(8))); + bitsPerSample.setAttribute("value", createListValue(3, "8")); break; case 32: - bitsPerSample.setAttribute("value", createListValue(4, Integer.toString(8))); + bitsPerSample.setAttribute("value", createListValue(4, "8")); break; } diff --git a/imageio/imageio-tga/src/test/java/com/twelvemonkeys/imageio/plugins/tga/RLEDecoderTest.java b/imageio/imageio-tga/src/test/java/com/twelvemonkeys/imageio/plugins/tga/RLEDecoderTest.java new file mode 100644 index 00000000..8ce2e9b1 --- /dev/null +++ b/imageio/imageio-tga/src/test/java/com/twelvemonkeys/imageio/plugins/tga/RLEDecoderTest.java @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2021, 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.io.enc.Decoder; +import com.twelvemonkeys.io.enc.DecoderAbstractTest; +import com.twelvemonkeys.io.enc.Encoder; + +import org.junit.Test; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Arrays; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +/** + * RLEDecoderTest. + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: RLEDecoderTest.java,v 1.0 07/04/2021 haraldk Exp$ + */ +public class RLEDecoderTest extends DecoderAbstractTest { + public Decoder createDecoder() { + return new RLEDecoder(8); + } + + public Encoder createCompatibleEncoder() { + return new RLEEncoder(8); + } + + @Test + public void testRLE8() throws IOException { + RLEDecoder decoder = new RLEDecoder(8); + + ByteBuffer buffer = ByteBuffer.allocate(256); + // Literal run, 2 bytes, compressed run, 8 bytes + ByteArrayInputStream stream = new ByteArrayInputStream(new byte[] {1, (byte) 0xFF, (byte) 0xF1, (byte) 0x87, (byte) 0xFE}); + + int decoded = decoder.decode(stream, buffer); + + assertEquals(10, decoded); + assertArrayEquals(new byte[] {(byte) 0xFF, (byte) 0xF1, (byte) 0xFE, (byte) 0xFE, (byte) 0xFE, (byte) 0xFE, (byte) 0xFE, (byte) 0xFE, (byte) 0xFE, (byte) 0xFE}, Arrays.copyOf(buffer.array(), 10)); + } + + @Test + public void testRLE16() throws IOException { + RLEDecoder decoder = new RLEDecoder(16); + + ByteBuffer buffer = ByteBuffer.allocate(512); + // Literal run, 2 * 2 bytes, compressed run, 8 * 2 bytes + ByteArrayInputStream stream = new ByteArrayInputStream(new byte[] {1, (byte) 0x00, (byte) 0xFF, (byte) 0x00, (byte) 0xF1, (byte) 0x87, (byte) 0x00, (byte) 0xFE}); + + int decoded = decoder.decode(stream, buffer); + + assertEquals(20, decoded); + assertArrayEquals(new byte[] { + (byte) 0x00, (byte) 0xFF, (byte) 0x00, (byte) 0xF1, + (byte) 0x00, (byte) 0xFE, (byte) 0x00, (byte) 0xFE, + (byte) 0x00, (byte) 0xFE, (byte) 0x00, (byte) 0xFE, + (byte) 0x00, (byte) 0xFE, (byte) 0x00, (byte) 0xFE, + (byte) 0x00, (byte) 0xFE, (byte) 0x00, (byte) 0xFE + }, + Arrays.copyOf(buffer.array(), 20)); + } + + @Test + public void testRLE24() throws IOException { + RLEDecoder decoder = new RLEDecoder(24); + + ByteBuffer buffer = ByteBuffer.allocate(1024); + // Literal run, 2 * 3 bytes, compressed run, 8 * 3 bytes + ByteArrayInputStream stream = new ByteArrayInputStream(new byte[] {1, (byte) 0x00, (byte) 0xFF, (byte) 0xFF, (byte) 0x00, (byte) 0xF1, (byte) 0xF1, (byte) 0x87, (byte) 0x00, (byte) 0xFE, (byte) 0xFE}); + + int decoded = decoder.decode(stream, buffer); + + assertEquals(30, decoded); + assertArrayEquals(new byte[] { + (byte) 0x00, (byte) 0xFF, (byte) 0xFF, (byte) 0x00, (byte) 0xF1, (byte) 0xF1, + (byte) 0x00, (byte) 0xFE, (byte) 0xFE, (byte) 0x00, (byte) 0xFE, (byte) 0xFE, + (byte) 0x00, (byte) 0xFE, (byte) 0xFE, (byte) 0x00, (byte) 0xFE, (byte) 0xFE, + (byte) 0x00, (byte) 0xFE, (byte) 0xFE, (byte) 0x00, (byte) 0xFE, (byte) 0xFE, + (byte) 0x00, (byte) 0xFE, (byte) 0xFE, (byte) 0x00, (byte) 0xFE, (byte) 0xFE + }, + Arrays.copyOf(buffer.array(), 30)); + } + + @Test + public void testRLE32() throws IOException { + RLEDecoder decoder = new RLEDecoder(32); + + ByteBuffer buffer = ByteBuffer.allocate(1024); + // Literal run, 2 * 4 bytes, compressed run, 8 * 4 bytes + ByteArrayInputStream stream = new ByteArrayInputStream(new byte[] {1, (byte) 0x00, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0x00, (byte) 0xF1, (byte) 0xF1, (byte) 0xF1, (byte) 0x87, (byte) 0x00, (byte) 0xFE, (byte) 0xFE, (byte) 0xFE}); + + int decoded = decoder.decode(stream, buffer); + + assertEquals(40, decoded); + assertArrayEquals(new byte[] { + (byte) 0x00, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0x00, (byte) 0xF1, (byte) 0xF1, (byte) 0xF1, + (byte) 0x00, (byte) 0xFE, (byte) 0xFE, (byte) 0xFE, (byte) 0x00, (byte) 0xFE, (byte) 0xFE, (byte) 0xFE, + (byte) 0x00, (byte) 0xFE, (byte) 0xFE, (byte) 0xFE, (byte) 0x00, (byte) 0xFE, (byte) 0xFE, (byte) 0xFE, + (byte) 0x00, (byte) 0xFE, (byte) 0xFE, (byte) 0xFE, (byte) 0x00, (byte) 0xFE, (byte) 0xFE, (byte) 0xFE, + (byte) 0x00, (byte) 0xFE, (byte) 0xFE, (byte) 0xFE, (byte) 0x00, (byte) 0xFE, (byte) 0xFE, (byte) 0xFE + }, + Arrays.copyOf(buffer.array(), 40)); + } + +} \ No newline at end of file diff --git a/imageio/imageio-tga/src/test/java/com/twelvemonkeys/imageio/plugins/tga/RLEEncoderTest.java b/imageio/imageio-tga/src/test/java/com/twelvemonkeys/imageio/plugins/tga/RLEEncoderTest.java new file mode 100644 index 00000000..be8a4d8c --- /dev/null +++ b/imageio/imageio-tga/src/test/java/com/twelvemonkeys/imageio/plugins/tga/RLEEncoderTest.java @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2021, 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.io.enc.Decoder; +import com.twelvemonkeys.io.enc.Encoder; +import com.twelvemonkeys.io.enc.EncoderAbstractTest; + +import org.junit.Test; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; + +import static org.junit.Assert.assertArrayEquals; + +/** + * RLEEncoderTest. + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: RLEEncoderTest.java,v 1.0 07/04/2021 haraldk Exp$ + */ + +public class RLEEncoderTest extends EncoderAbstractTest { + @Override + protected Encoder createEncoder() { + return new RLEEncoder(8); + } + + @Override + protected Decoder createCompatibleDecoder() { + return new RLEDecoder(8); + } + + @Test + public void testRLE8() throws IOException { + RLEEncoder encoder = new RLEEncoder(8); + + // Literal run, 2 bytes, compressed run, 8 bytes + ByteBuffer buffer = ByteBuffer.wrap(new byte[] {(byte) 0xFF, (byte) 0xF1, (byte) 0xFE, (byte) 0xFE, (byte) 0xFE, (byte) 0xFE, (byte) 0xFE, (byte) 0xFE, (byte) 0xFE, (byte) 0xFE}); + ByteArrayOutputStream stream = new ByteArrayOutputStream(10); + + encoder.encode(stream, buffer); + + assertArrayEquals(new byte[] {1, (byte) 0xFF, (byte) 0xF1, (byte) 0x87, (byte) 0xFE}, stream.toByteArray()); + } + + @Test + public void testRLE16() throws IOException { + RLEEncoder encoder = new RLEEncoder(16); + + // Literal run, 2 * 2 bytes, compressed run, 8 * 2 bytes + ByteBuffer buffer = ByteBuffer.wrap(new byte[] {(byte) 0x00, (byte) 0xFF, (byte) 0x00, (byte) 0xF1, + (byte) 0x00, (byte) 0xFE, (byte) 0x00, (byte) 0xFE, (byte) 0x00, (byte) 0xFE, (byte) 0x00, (byte) 0xFE, + (byte) 0x00, (byte) 0xFE, (byte) 0x00, (byte) 0xFE, (byte) 0x00, (byte) 0xFE, (byte) 0x00, (byte) 0xFE}); + ByteArrayOutputStream stream = new ByteArrayOutputStream(20); + + encoder.encode(stream, buffer); + + assertArrayEquals(new byte[] {1, (byte) 0x00, (byte) 0xFF, (byte) 0x00, (byte) 0xF1, (byte) 0x87, (byte) 0x00, (byte) 0xFE}, stream.toByteArray()); + } + + @Test + public void testRLE24() throws IOException { + RLEEncoder encoder = new RLEEncoder(24); + + // Literal run, 2 * 3 bytes, compressed run, 8 * 3 bytes + ByteBuffer buffer = ByteBuffer.wrap(new byte[] { + (byte) 0x00, (byte) 0xFF, (byte) 0xFF, (byte) 0x00, (byte) 0xF1, (byte) 0xF1, + (byte) 0x00, (byte) 0xFE, (byte) 0xFE, (byte) 0x00, (byte) 0xFE, (byte) 0xFE, + (byte) 0x00, (byte) 0xFE, (byte) 0xFE, (byte) 0x00, (byte) 0xFE, (byte) 0xFE, + (byte) 0x00, (byte) 0xFE, (byte) 0xFE, (byte) 0x00, (byte) 0xFE, (byte) 0xFE, + (byte) 0x00, (byte) 0xFE, (byte) 0xFE, (byte) 0x00, (byte) 0xFE, (byte) 0xFE + }); + ByteArrayOutputStream stream = new ByteArrayOutputStream(30); + + encoder.encode(stream, buffer); + + assertArrayEquals(new byte[] { + 1, (byte) 0x00, (byte) 0xFF, (byte) 0xFF, (byte) 0x00, (byte) 0xF1, (byte) 0xF1, + (byte) 0x87, (byte) 0x00, (byte) 0xFE, (byte) 0xFE + }, stream.toByteArray()); + } + + @Test + public void testRLE32() throws IOException { + RLEEncoder encoder = new RLEEncoder(32); + + // Literal run, 2 * 4 bytes, compressed run, 8 * 4 bytes + ByteBuffer buffer = ByteBuffer.wrap(new byte[] { + (byte) 0x00, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0x00, (byte) 0xF1, (byte) 0xF1, (byte) 0xF1, + (byte) 0x00, (byte) 0xFE, (byte) 0xFE, (byte) 0xFE, (byte) 0x00, (byte) 0xFE, (byte) 0xFE, (byte) 0xFE, + (byte) 0x00, (byte) 0xFE, (byte) 0xFE, (byte) 0xFE, (byte) 0x00, (byte) 0xFE, (byte) 0xFE, (byte) 0xFE, + (byte) 0x00, (byte) 0xFE, (byte) 0xFE, (byte) 0xFE, (byte) 0x00, (byte) 0xFE, (byte) 0xFE, (byte) 0xFE, + (byte) 0x00, (byte) 0xFE, (byte) 0xFE, (byte) 0xFE, (byte) 0x00, (byte) 0xFE, (byte) 0xFE, (byte) 0xFE + }); + ByteArrayOutputStream stream = new ByteArrayOutputStream(40); + + encoder.encode(stream, buffer); + + assertArrayEquals(new byte[] { + 1, (byte) 0x00, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0x00, (byte) 0xF1, (byte) 0xF1, (byte) 0xF1, + (byte) 0x87, (byte) 0x00, (byte) 0xFE, (byte) 0xFE, (byte) 0xFE + }, stream.toByteArray()); + } +} \ No newline at end of file diff --git a/imageio/imageio-tga/src/test/java/com/twelvemonkeys/imageio/plugins/tga/TGAImageWriteParamTest.java b/imageio/imageio-tga/src/test/java/com/twelvemonkeys/imageio/plugins/tga/TGAImageWriteParamTest.java new file mode 100644 index 00000000..d8ff3059 --- /dev/null +++ b/imageio/imageio-tga/src/test/java/com/twelvemonkeys/imageio/plugins/tga/TGAImageWriteParamTest.java @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2021, 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 org.junit.Test; + +import javax.imageio.ImageWriteParam; +import java.awt.image.BufferedImage; + +import static org.junit.Assert.*; +import static org.junit.Assume.assumeFalse; + +/** + * TGAImageWriteParamTest. + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: TGAImageWriteParamTest.java,v 1.0 08/04/2021 haraldk Exp$ + */ +public class TGAImageWriteParamTest { + @Test + public void testDefaultCopyFromMetadata() { + TGAImageWriteParam param = new TGAImageWriteParam(); + assertTrue(param.canWriteCompressed()); + assertEquals(ImageWriteParam.MODE_COPY_FROM_METADATA, param.getCompressionMode()); + } + + @Test + public void testIsRLENoParamNoMetadata() { + assertFalse(TGAImageWriteParam.isRLE(null, null)); + } + + @Test + public void testIsRLEParamCantWriteCompressedNoMetadata() { + // Base class has canWriteCompressed == false, need to test + ImageWriteParam param = new ImageWriteParam(null); + assumeFalse(param.canWriteCompressed()); + + assertFalse(TGAImageWriteParam.isRLE(param, null)); + } + + @Test + public void testIsRLEParamDefaultNoMetadata() { + TGAImageWriteParam param = new TGAImageWriteParam(); + param.setCompressionMode(ImageWriteParam.MODE_DEFAULT); + assertFalse(TGAImageWriteParam.isRLE(param, null)); + } + + @Test + public void testIsRLEParamExplicitNoMetadata() { + TGAImageWriteParam param = new TGAImageWriteParam(); + param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); + + assertFalse(TGAImageWriteParam.isRLE(param, null)); + + param.setCompressionType("RLE"); + assertTrue(TGAImageWriteParam.isRLE(param, null)); + } + + @Test + public void testIsRLEParamDisabledNoMetadata() { + TGAImageWriteParam param = new TGAImageWriteParam(); + param.setCompressionMode(ImageWriteParam.MODE_DISABLED); + + assertFalse(TGAImageWriteParam.isRLE(param, null)); + } + + @Test + public void testIsRLEParamCopyNoMetadata() { + ImageWriteParam param = new TGAImageWriteParam(); + param.setCompressionMode(ImageWriteParam.MODE_COPY_FROM_METADATA); + + assertFalse(TGAImageWriteParam.isRLE(param, null)); + } + + @Test + public void testIsRLEParamCantWriteCompressedAndMetadata() { + // Base class has canWriteCompressed == false, need to test + ImageWriteParam param = new ImageWriteParam(null); + assumeFalse(param.canWriteCompressed()); + + assertFalse(TGAImageWriteParam.isRLE(param, new TGAMetadata(TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_3BYTE_BGR), false), null))); + assertFalse(TGAImageWriteParam.isRLE(param, new TGAMetadata(TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_3BYTE_BGR), true), null))); + } + + @Test + public void testIsRLEParamCopyAndMetadataNoCompression() { + ImageWriteParam param = new TGAImageWriteParam(); + param.setCompressionMode(ImageWriteParam.MODE_COPY_FROM_METADATA); + + assertTrue(TGAImageWriteParam.isRLE(param, new TGAMetadata(TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_3BYTE_BGR), true), null))); + } + + @Test + public void testIsRLEParamCopyAndMetadataRLE() { + ImageWriteParam param = new TGAImageWriteParam(); + param.setCompressionMode(ImageWriteParam.MODE_COPY_FROM_METADATA); + + assertTrue(TGAImageWriteParam.isRLE(param, new TGAMetadata(TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_3BYTE_BGR), true), null))); + } + + @Test + public void testIsRLEParamExplicitAndMetadata() { + TGAImageWriteParam param = new TGAImageWriteParam(); + param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); + + assertFalse(TGAImageWriteParam.isRLE(param, new TGAMetadata(TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_3BYTE_BGR), false), null))); + assertFalse(TGAImageWriteParam.isRLE(param, new TGAMetadata(TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_3BYTE_BGR), true), null))); + + param.setCompressionType("RLE"); + assertTrue(TGAImageWriteParam.isRLE(param, new TGAMetadata(TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_3BYTE_BGR), false), null))); + assertTrue(TGAImageWriteParam.isRLE(param, new TGAMetadata(TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_3BYTE_BGR), true), null))); + } + +} \ No newline at end of file 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 index 8d5e5b45..8d7e4f37 100644 --- 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 @@ -30,15 +30,18 @@ package com.twelvemonkeys.imageio.plugins.tga; +import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream; import com.twelvemonkeys.imageio.util.ImageTypeSpecifiers; import com.twelvemonkeys.imageio.util.ImageWriterAbstractTest; +import com.twelvemonkeys.io.FastByteArrayOutputStream; import org.junit.Test; +import org.w3c.dom.NodeList; -import javax.imageio.ImageIO; -import javax.imageio.ImageReader; -import javax.imageio.ImageTypeSpecifier; -import javax.imageio.ImageWriter; +import javax.imageio.*; +import javax.imageio.metadata.IIOMetadata; +import javax.imageio.metadata.IIOMetadataFormatImpl; +import javax.imageio.metadata.IIOMetadataNode; import javax.imageio.spi.ImageWriterSpi; import javax.imageio.stream.ImageInputStream; import javax.imageio.stream.ImageOutputStream; @@ -53,7 +56,7 @@ 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.Assert.*; import static org.junit.Assume.assumeNotNull; /** @@ -84,6 +87,13 @@ public class TGAImageWriterTest extends ImageWriterAbstractTest ); } + @Test + public void testDefaultParamIsTGA() throws IOException { + ImageWriter writer = createWriter(); + assertEquals(writer.getDefaultWriteParam().getClass(), TGAImageWriteParam.class); + writer.dispose(); + } + @Test public void testWriteRead() throws IOException { ImageWriter writer = createWriter(); @@ -108,5 +118,120 @@ public class TGAImageWriterTest extends ImageWriterAbstractTest assertImageDataEquals("Images differ for " + testData, (BufferedImage) testData, image); } } + + writer.dispose(); + reader.dispose(); + } + + @Test + public void testWriteReadRLE() throws IOException { + ImageWriter writer = createWriter(); + ImageReader reader = ImageIO.getImageReader(writer); + + assumeNotNull(reader); + + ImageWriteParam param = writer.getDefaultWriteParam(); + param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); + param.setCompressionType("RLE"); + + for (RenderedImage testData : getTestData()) { + FastByteArrayOutputStream buffer = new FastByteArrayOutputStream(4096); + + try (ImageOutputStream stream = ImageIO.createImageOutputStream(buffer)) { + writer.setOutput(stream); + writer.write(null, new IIOImage(drawSomething((BufferedImage) testData), null, null), param); + } + + try (ImageInputStream stream = new ByteArrayImageInputStream(buffer.toByteArray())) { + reader.setInput(stream); + + BufferedImage image = reader.read(0); + + assertNotNull(image); + assertImageDataEquals("Images differ for " + testData, (BufferedImage) testData, image); + } + } + + writer.dispose(); + reader.dispose(); + } + + @Test + public void testRewriteCompressionCopyFromMetadataUncompressed() throws IOException { + ImageWriter writer = createWriter(); + ImageReader reader = ImageIO.getImageReader(writer); + + assumeNotNull(reader); + + try (ImageInputStream input = ImageIO.createImageInputStream(getClassLoaderResource("/tga/UTC24.TGA"))) { + reader.setInput(input); + IIOImage image = reader.readAll(0, null); + assertNull(findCompressionType(image.getMetadata())); // Sanity + + FastByteArrayOutputStream buffer = new FastByteArrayOutputStream(65536); + + try (ImageOutputStream output = ImageIO.createImageOutputStream(buffer)) { + writer.setOutput(output); + + // Copy from metadata should be default, we'll validate here + ImageWriteParam param = writer.getDefaultWriteParam(); + assertEquals(ImageWriteParam.MODE_COPY_FROM_METADATA, param.getCompressionMode()); + + writer.write(null, image, param); + } + + try (ImageInputStream stream = new ByteArrayImageInputStream(buffer.toByteArray())) { + reader.setInput(stream); + IIOMetadata metadata = reader.getImageMetadata(0); + + assertNull(findCompressionType(metadata)); + } + } + + writer.dispose(); + reader.dispose(); + } + + @Test + public void testRewriteCompressionCopyFromMetadataRLE() throws IOException { + ImageWriter writer = createWriter(); + ImageReader reader = ImageIO.getImageReader(writer); + + assumeNotNull(reader); + + try (ImageInputStream input = ImageIO.createImageInputStream(getClassLoaderResource("/tga/CTC24.TGA"))) { + reader.setInput(input); + IIOImage image = reader.readAll(0, null); + assertEquals("RLE", findCompressionType(image.getMetadata())); // Sanity + + FastByteArrayOutputStream buffer = new FastByteArrayOutputStream(32768); + + try (ImageOutputStream output = ImageIO.createImageOutputStream(buffer)) { + writer.setOutput(output); + + // Copy from metadata should be default, we'll just go with no param here + writer.write(null, image, null); + } + + try (ImageInputStream inputStream = new ByteArrayImageInputStream(buffer.toByteArray())) { + reader.setInput(inputStream); + IIOMetadata metadata = reader.getImageMetadata(0); + + assertEquals("RLE", findCompressionType(metadata)); + } + } + + writer.dispose(); + reader.dispose(); + } + + private String findCompressionType(IIOMetadata metadata) { + IIOMetadataNode root = (IIOMetadataNode) metadata.getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName); + NodeList compressionTypeName = root.getElementsByTagName("CompressionTypeName"); + if (compressionTypeName.getLength() > 0) { + return compressionTypeName.item(0).getAttributes().getNamedItem("value").getNodeValue(); + } + + return null; } } \ No newline at end of file diff --git a/imageio/imageio-tga/src/test/java/com/twelvemonkeys/imageio/plugins/tga/TGAMetadataTest.java b/imageio/imageio-tga/src/test/java/com/twelvemonkeys/imageio/plugins/tga/TGAMetadataTest.java new file mode 100644 index 00000000..c1bd18e1 --- /dev/null +++ b/imageio/imageio-tga/src/test/java/com/twelvemonkeys/imageio/plugins/tga/TGAMetadataTest.java @@ -0,0 +1,486 @@ +/* + * Copyright (c) 2021, 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 org.junit.Test; +import org.junit.function.ThrowingRunnable; +import org.w3c.dom.Node; + +import javax.imageio.metadata.IIOMetadataFormatImpl; +import javax.imageio.metadata.IIOMetadataNode; +import java.awt.image.BufferedImage; +import java.awt.image.IndexColorModel; +import java.util.Calendar; + +import static org.junit.Assert.*; + +/** + * TGAMetadataTest. + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: TGAMetadataTest.java,v 1.0 08/04/2021 haraldk Exp$ + */ +public class TGAMetadataTest { + @Test + public void testStandardFeatures() { + TGAHeader header = TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_3BYTE_BGR), false); + final TGAMetadata metadata = new TGAMetadata(header, null); + + // Standard metadata format + assertTrue(metadata.isStandardMetadataFormatSupported()); + Node root = metadata.getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName); + assertNotNull(root); + assertTrue(root instanceof IIOMetadataNode); + + // Other formats + assertNull(metadata.getNativeMetadataFormatName()); + assertNull(metadata.getExtraMetadataFormatNames()); + assertThrows(IllegalArgumentException.class, new ThrowingRunnable() { + @Override + public void run() { + metadata.getAsTree("com_foo_bar_1.0"); + } + }); + + // Read-only + assertTrue(metadata.isReadOnly()); + assertThrows(IllegalStateException.class, new ThrowingRunnable() { + @Override + public void run() throws Throwable { + metadata.mergeTree(IIOMetadataFormatImpl.standardMetadataFormatName, new IIOMetadataNode(IIOMetadataFormatImpl.standardMetadataFormatName)); + } + }); + } + + @Test + public void testStandardChromaGray() { + TGAHeader header = TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_GRAY), false); + TGAMetadata metadata = new TGAMetadata(header, null); + + IIOMetadataNode chroma = metadata.getStandardChromaNode(); + assertNotNull(chroma); + assertEquals("Chroma", chroma.getNodeName()); + assertEquals(3, chroma.getLength()); + + IIOMetadataNode colorSpaceType = (IIOMetadataNode) chroma.getFirstChild(); + assertEquals("ColorSpaceType", colorSpaceType.getNodeName()); + assertEquals("GRAY", colorSpaceType.getAttribute("name")); + + IIOMetadataNode numChannels = (IIOMetadataNode) colorSpaceType.getNextSibling(); + assertEquals("NumChannels", numChannels.getNodeName()); + assertEquals("1", numChannels.getAttribute("value")); + + IIOMetadataNode blackIsZero = (IIOMetadataNode) numChannels.getNextSibling(); + assertEquals("BlackIsZero", blackIsZero.getNodeName()); + assertEquals("TRUE", blackIsZero.getAttribute("value")); + + assertNull(blackIsZero.getNextSibling()); // No more children + } + + @Test + public void testStandardChromaRGB() { + TGAHeader header = TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_3BYTE_BGR), false); + TGAMetadata metadata = new TGAMetadata(header, null); + + IIOMetadataNode chroma = metadata.getStandardChromaNode(); + assertNotNull(chroma); + assertEquals("Chroma", chroma.getNodeName()); + assertEquals(3, chroma.getLength()); + + IIOMetadataNode colorSpaceType = (IIOMetadataNode) chroma.getFirstChild(); + assertEquals("ColorSpaceType", colorSpaceType.getNodeName()); + assertEquals("RGB", colorSpaceType.getAttribute("name")); + + IIOMetadataNode numChannels = (IIOMetadataNode) colorSpaceType.getNextSibling(); + assertEquals("NumChannels", numChannels.getNodeName()); + assertEquals("3", numChannels.getAttribute("value")); + + IIOMetadataNode blackIsZero = (IIOMetadataNode) numChannels.getNextSibling(); + assertEquals("BlackIsZero", blackIsZero.getNodeName()); + assertEquals("TRUE", blackIsZero.getAttribute("value")); + + assertNull(blackIsZero.getNextSibling()); // No more children + } + + @Test + public void testStandardChromaPalette() { + byte[] bw = {0, (byte) 0xff}; + IndexColorModel indexColorModel = new IndexColorModel(8, bw.length, bw, bw, bw, -1); + TGAHeader header = TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_INDEXED, indexColorModel), false); + TGAMetadata metadata = new TGAMetadata(header, null); + + IIOMetadataNode chroma = metadata.getStandardChromaNode(); + assertNotNull(chroma); + assertEquals("Chroma", chroma.getNodeName()); + assertEquals(4, chroma.getLength()); + + IIOMetadataNode colorSpaceType = (IIOMetadataNode) chroma.getFirstChild(); + assertEquals("ColorSpaceType", colorSpaceType.getNodeName()); + assertEquals("RGB", colorSpaceType.getAttribute("name")); + + IIOMetadataNode numChannels = (IIOMetadataNode) colorSpaceType.getNextSibling(); + assertEquals("NumChannels", numChannels.getNodeName()); + assertEquals("3", numChannels.getAttribute("value")); + + IIOMetadataNode blackIsZero = (IIOMetadataNode) numChannels.getNextSibling(); + assertEquals("BlackIsZero", blackIsZero.getNodeName()); + assertEquals("TRUE", blackIsZero.getAttribute("value")); + + IIOMetadataNode palette = (IIOMetadataNode) blackIsZero.getNextSibling(); + assertEquals("Palette", palette.getNodeName()); + assertEquals(bw.length, palette.getLength()); + + for (int i = 0; i < palette.getLength(); i++) { + IIOMetadataNode item0 = (IIOMetadataNode) palette.item(i); + assertEquals("PaletteEntry", item0.getNodeName()); + assertEquals(String.valueOf(i), item0.getAttribute("index")); + String rgb = String.valueOf(bw[i] & 0xff); + assertEquals(rgb, item0.getAttribute("red")); + assertEquals(rgb, item0.getAttribute("green")); + assertEquals(rgb, item0.getAttribute("blue")); + } + + // TODO: BackgroundIndex == 1?? + } + + @Test + public void testStandardCompressionRLE() { + TGAHeader header = TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_3BYTE_BGR), true); + TGAMetadata metadata = new TGAMetadata(header, null); + + IIOMetadataNode compression = metadata.getStandardCompressionNode(); + assertNotNull(compression); + assertEquals("Compression", compression.getNodeName()); + assertEquals(2, compression.getLength()); + + IIOMetadataNode compressionTypeName = (IIOMetadataNode) compression.getFirstChild(); + assertEquals("CompressionTypeName", compressionTypeName.getNodeName()); + assertEquals("RLE", compressionTypeName.getAttribute("value")); + + IIOMetadataNode lossless = (IIOMetadataNode) compressionTypeName.getNextSibling(); + assertEquals("Lossless", lossless.getNodeName()); + assertEquals("TRUE", lossless.getAttribute("value")); + + assertNull(lossless.getNextSibling()); // No more children + } + + @Test + public void testStandardCompressionNone() { + TGAHeader header = TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_3BYTE_BGR), false); + TGAMetadata metadata = new TGAMetadata(header, null); + + assertNull(metadata.getStandardCompressionNode()); // No compression, all default... + } + + @Test + public void testStandardDataGray() { + TGAHeader header = TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_GRAY), true); + TGAMetadata metadata = new TGAMetadata(header, null); + + IIOMetadataNode data = metadata.getStandardDataNode(); + assertNotNull(data); + assertEquals("Data", data.getNodeName()); + assertEquals(3, data.getLength()); + + IIOMetadataNode planarConfiguration = (IIOMetadataNode) data.getFirstChild(); + assertEquals("PlanarConfiguration", planarConfiguration.getNodeName()); + assertEquals("PixelInterleaved", planarConfiguration.getAttribute("value")); + + IIOMetadataNode sampleFomat = (IIOMetadataNode) planarConfiguration.getNextSibling(); + assertEquals("SampleFormat", sampleFomat.getNodeName()); + assertEquals("UnsignedIntegral", sampleFomat.getAttribute("value")); + + IIOMetadataNode bitsPerSample = (IIOMetadataNode) sampleFomat.getNextSibling(); + assertEquals("BitsPerSample", bitsPerSample.getNodeName()); + assertEquals("8", bitsPerSample.getAttribute("value")); + + assertNull(bitsPerSample.getNextSibling()); // No more children + } + + @Test + public void testStandardDataRGB() { + TGAHeader header = TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_3BYTE_BGR), true); + TGAMetadata metadata = new TGAMetadata(header, null); + + IIOMetadataNode data = metadata.getStandardDataNode(); + assertNotNull(data); + assertEquals("Data", data.getNodeName()); + assertEquals(3, data.getLength()); + + IIOMetadataNode planarConfiguration = (IIOMetadataNode) data.getFirstChild(); + assertEquals("PlanarConfiguration", planarConfiguration.getNodeName()); + assertEquals("PixelInterleaved", planarConfiguration.getAttribute("value")); + + IIOMetadataNode sampleFomat = (IIOMetadataNode) planarConfiguration.getNextSibling(); + assertEquals("SampleFormat", sampleFomat.getNodeName()); + assertEquals("UnsignedIntegral", sampleFomat.getAttribute("value")); + + IIOMetadataNode bitsPerSample = (IIOMetadataNode) sampleFomat.getNextSibling(); + assertEquals("BitsPerSample", bitsPerSample.getNodeName()); + assertEquals("8 8 8", bitsPerSample.getAttribute("value")); + + assertNull(bitsPerSample.getNextSibling()); // No more children + } + + @Test + public void testStandardDataRGBA() { + TGAHeader header = TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB), true); + TGAMetadata metadata = new TGAMetadata(header, null); + + IIOMetadataNode data = metadata.getStandardDataNode(); + assertNotNull(data); + assertEquals("Data", data.getNodeName()); + assertEquals(3, data.getLength()); + + IIOMetadataNode planarConfiguration = (IIOMetadataNode) data.getFirstChild(); + assertEquals("PlanarConfiguration", planarConfiguration.getNodeName()); + assertEquals("PixelInterleaved", planarConfiguration.getAttribute("value")); + + IIOMetadataNode sampleFomat = (IIOMetadataNode) planarConfiguration.getNextSibling(); + assertEquals("SampleFormat", sampleFomat.getNodeName()); + assertEquals("UnsignedIntegral", sampleFomat.getAttribute("value")); + + IIOMetadataNode bitsPerSample = (IIOMetadataNode) sampleFomat.getNextSibling(); + assertEquals("BitsPerSample", bitsPerSample.getNodeName()); + assertEquals("8 8 8 8", bitsPerSample.getAttribute("value")); + + assertNull(bitsPerSample.getNextSibling()); // No more children + } + + @Test + public void testStandardDataPalette() { + byte[] rgb = new byte[1 << 8]; // Colors doesn't really matter here + IndexColorModel indexColorModel = new IndexColorModel(8, rgb.length, rgb, rgb, rgb, 0); + TGAHeader header = TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_INDEXED, indexColorModel), true); + TGAMetadata metadata = new TGAMetadata(header, null); + + IIOMetadataNode data = metadata.getStandardDataNode(); + assertNotNull(data); + assertEquals("Data", data.getNodeName()); + assertEquals(3, data.getLength()); + + IIOMetadataNode planarConfiguration = (IIOMetadataNode) data.getFirstChild(); + assertEquals("PlanarConfiguration", planarConfiguration.getNodeName()); + assertEquals("PixelInterleaved", planarConfiguration.getAttribute("value")); + + IIOMetadataNode sampleFomat = (IIOMetadataNode) planarConfiguration.getNextSibling(); + assertEquals("SampleFormat", sampleFomat.getNodeName()); + assertEquals("Index", sampleFomat.getAttribute("value")); + + IIOMetadataNode bitsPerSample = (IIOMetadataNode) sampleFomat.getNextSibling(); + assertEquals("BitsPerSample", bitsPerSample.getNodeName()); + assertEquals("8", bitsPerSample.getAttribute("value")); + + assertNull(bitsPerSample.getNextSibling()); // No more children + } + + @Test + public void testStandardDimensionNormal() { + TGAHeader header = TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_GRAY), true); + TGAMetadata metadata = new TGAMetadata(header, null); + + IIOMetadataNode dimension = metadata.getStandardDimensionNode(); + assertNotNull(dimension); + assertEquals("Dimension", dimension.getNodeName()); + assertEquals(2, dimension.getLength()); + + IIOMetadataNode imageOrientation = (IIOMetadataNode) dimension.getFirstChild(); + assertEquals("ImageOrientation", imageOrientation.getNodeName()); + assertEquals("Normal", imageOrientation.getAttribute("value")); + + IIOMetadataNode pixelAspectRatio = (IIOMetadataNode) imageOrientation.getNextSibling(); + assertEquals("PixelAspectRatio", pixelAspectRatio.getNodeName()); + assertEquals("1.0", pixelAspectRatio.getAttribute("value")); + + assertNull(pixelAspectRatio.getNextSibling()); // No more children + } + + @Test + public void testStandardDimensionFlipH() { + TGAHeader header = TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_GRAY), true); + header.origin = TGA.ORIGIN_LOWER_LEFT; + TGAMetadata metadata = new TGAMetadata(header, null); + + IIOMetadataNode dimension = metadata.getStandardDimensionNode(); + assertNotNull(dimension); + assertEquals("Dimension", dimension.getNodeName()); + assertEquals(2, dimension.getLength()); + + IIOMetadataNode imageOrientation = (IIOMetadataNode) dimension.getFirstChild(); + assertEquals("ImageOrientation", imageOrientation.getNodeName()); + assertEquals("FlipH", imageOrientation.getAttribute("value")); + + IIOMetadataNode pixelAspectRatio = (IIOMetadataNode) imageOrientation.getNextSibling(); + assertEquals("PixelAspectRatio", pixelAspectRatio.getNodeName()); + assertEquals("1.0", pixelAspectRatio.getAttribute("value")); + + assertNull(pixelAspectRatio.getNextSibling()); // No more children + } + + @Test + public void testStandardDocument() { + TGAHeader header = TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_GRAY), true); + TGAMetadata metadata = new TGAMetadata(header, null); + + IIOMetadataNode document = metadata.getStandardDocumentNode(); + assertNotNull(document); + assertEquals("Document", document.getNodeName()); + assertEquals(1, document.getLength()); + + IIOMetadataNode formatVersion = (IIOMetadataNode) document.getFirstChild(); + assertEquals("FormatVersion", formatVersion.getNodeName()); + assertEquals("1.0", formatVersion.getAttribute("value")); + + assertNull(formatVersion.getNextSibling()); // No more children + } + + @Test + public void testStandardDocumentExtensions() { + TGAHeader header = TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_GRAY), true); + TGAExtensions extensions = new TGAExtensions(); + extensions.creationDate = Calendar.getInstance(); + extensions.creationDate.set(2021, Calendar.APRIL, 8, 18, 55, 0); + TGAMetadata metadata = new TGAMetadata(header, extensions); + + IIOMetadataNode document = metadata.getStandardDocumentNode(); + assertNotNull(document); + assertEquals("Document", document.getNodeName()); + assertEquals(2, document.getLength()); + + IIOMetadataNode formatVersion = (IIOMetadataNode) document.getFirstChild(); + assertEquals("FormatVersion", formatVersion.getNodeName()); + assertEquals("2.0", formatVersion.getAttribute("value")); + + IIOMetadataNode imageCreationTime = (IIOMetadataNode) formatVersion.getNextSibling(); + assertEquals("ImageCreationTime", imageCreationTime.getNodeName()); + assertEquals("2021", imageCreationTime.getAttribute("year")); + assertEquals("4", imageCreationTime.getAttribute("month")); + assertEquals("8", imageCreationTime.getAttribute("day")); + assertEquals("18", imageCreationTime.getAttribute("hour")); + assertEquals("55", imageCreationTime.getAttribute("minute")); + assertEquals("0", imageCreationTime.getAttribute("second")); + + assertNull(imageCreationTime.getNextSibling()); // No more children + } + + @Test + public void testStandardText() { + TGAHeader header = TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_GRAY), true); + header.identification = "MY_FILE.TGA"; + + TGAExtensions extensions = new TGAExtensions(); + extensions.softwareId = "TwelveMonkeys"; + extensions.authorName = "Harald K"; + extensions.authorComments = "Comments, comments... "; + + TGAMetadata metadata = new TGAMetadata(header, extensions); + + IIOMetadataNode text = metadata.getStandardTextNode(); + assertNotNull(text); + assertEquals("Text", text.getNodeName()); + assertEquals(4, text.getLength()); + + IIOMetadataNode textEntry = (IIOMetadataNode) text.item(0); + assertEquals("TextEntry", textEntry.getNodeName()); + assertEquals("DocumentName", textEntry.getAttribute("keyword")); + assertEquals(header.getIdentification(), textEntry.getAttribute("value")); + + textEntry = (IIOMetadataNode) text.item(1); + assertEquals("TextEntry", textEntry.getNodeName()); + assertEquals("Software", textEntry.getAttribute("keyword")); + assertEquals(extensions.getSoftware(), textEntry.getAttribute("value")); + + textEntry = (IIOMetadataNode) text.item(2); + assertEquals("TextEntry", textEntry.getNodeName()); + assertEquals("Artist", textEntry.getAttribute("keyword")); + assertEquals(extensions.getAuthorName(), textEntry.getAttribute("value")); + + textEntry = (IIOMetadataNode) text.item(3); + assertEquals("TextEntry", textEntry.getNodeName()); + assertEquals("UserComment", textEntry.getAttribute("keyword")); + assertEquals(extensions.getAuthorComments(), textEntry.getAttribute("value")); + } + + @Test + public void testStandardTransparencyRGB() { + TGAHeader header = TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_3BYTE_BGR), true); + TGAMetadata metadata = new TGAMetadata(header, null); + + IIOMetadataNode transparency = metadata.getStandardTransparencyNode(); + assertNotNull(transparency); + assertEquals("Transparency", transparency.getNodeName()); + assertEquals(1, transparency.getLength()); + + IIOMetadataNode alpha = (IIOMetadataNode) transparency.getFirstChild(); + assertEquals("Alpha", alpha.getNodeName()); + assertEquals("none", alpha.getAttribute("value")); + + assertNull(alpha.getNextSibling()); // No more children + } + + @Test + public void testStandardTransparencyRGBA() { + TGAHeader header = TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_4BYTE_ABGR), true); + TGAMetadata metadata = new TGAMetadata(header, null); + + IIOMetadataNode transparency = metadata.getStandardTransparencyNode(); + assertNotNull(transparency); + assertEquals("Transparency", transparency.getNodeName()); + assertEquals(1, transparency.getLength()); + + IIOMetadataNode alpha = (IIOMetadataNode) transparency.getFirstChild(); + assertEquals("Alpha", alpha.getNodeName()); + assertEquals("nonpremultiplied", alpha.getAttribute("value")); + + assertNull(alpha.getNextSibling()); // No more children + } + + @Test + public void testStandardTransparencyPalette() { + byte[] bw = {0, (byte) 0xff}; + IndexColorModel indexColorModel = new IndexColorModel(8, bw.length, bw, bw, bw, 1); + TGAHeader header = TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_INDEXED, indexColorModel), true); + TGAMetadata metadata = new TGAMetadata(header, null); + + IIOMetadataNode transparency = metadata.getStandardTransparencyNode(); + assertNotNull(transparency); + assertEquals("Transparency", transparency.getNodeName()); + assertEquals(1, transparency.getLength()); + + IIOMetadataNode alpha = (IIOMetadataNode) transparency.getFirstChild(); + assertEquals("Alpha", alpha.getNodeName()); + assertEquals("nonpremultiplied", alpha.getAttribute("value")); + + assertNull(alpha.getNextSibling()); // No more children + } + +} \ No newline at end of file