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 e845fcb0..116ec2df 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 @@ -29,6 +29,8 @@ package com.twelvemonkeys.imageio.plugins.tga; interface TGA { + byte[] MAGIC = {'T', 'R', 'U', 'E', 'V', 'I', 'S', 'I', 'O', 'N', '-', 'X', 'F', 'I', 'L', 'E', '.', 0}; + /** Fixed header size: 18.*/ int HEADER_SIZE = 18; 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 new file mode 100644 index 00000000..ad806de9 --- /dev/null +++ b/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAExtensions.java @@ -0,0 +1,187 @@ +package com.twelvemonkeys.imageio.plugins.tga; + +import javax.imageio.IIOException; +import javax.imageio.stream.ImageInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Calendar; + +/** + * TGAExtensions. + * + * @author Harald Kuhr + * @author last modified by $Author: harald.kuhr$ + * @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; + + private Calendar creationDate; + private String jobId; + + private String softwareId; + private String softwareVersion; + + private int backgroundColor; + private double pixelAspectRatio; + private double gamma; + + private long colorCorrectionOffset; + private long postageStampOffset; + private long scanLineOffset; + + private int attributeType; + + private TGAExtensions() { + } + + static TGAExtensions read(final ImageInputStream stream) throws IOException { + int extSize = stream.readUnsignedShort(); + + // Should always be 495 for version 2.0, no newer version exists... + if (extSize < EXT_AREA_SIZE) { + throw new IIOException(String.format("TGA Extension Area size less than %d: %d", EXT_AREA_SIZE, extSize)); + } + + TGAExtensions extensions = new TGAExtensions(); + extensions.authorName = readString(stream, 41);; + extensions.authorComments = readString(stream, 324); + extensions.creationDate = readDate(stream); + extensions.jobId = readString(stream, 41); + + stream.skipBytes(6); // Job time, 3 shorts, hours/minutes/seconds elapsed + + extensions.softwareId = readString(stream, 41); + + // Software version (* 100) short + single byte ASCII (ie. 101 'b' for 1.01b) + int softwareVersion = stream.readUnsignedShort(); + int softwareLetter = stream.readByte(); + + extensions.softwareVersion = softwareVersion != 0 && softwareLetter != ' ' + ? String.format("%d.%d%d", softwareVersion / 100, softwareVersion % 100, softwareLetter).trim() + : null; + + extensions.backgroundColor = stream.readInt(); // ARGB + + extensions.pixelAspectRatio = readRational(stream); + extensions.gamma = readRational(stream); + + extensions.colorCorrectionOffset = stream.readUnsignedInt(); + extensions.postageStampOffset = stream.readUnsignedInt(); + extensions.scanLineOffset = stream.readUnsignedInt(); + + // Offset 494 specifies Attribute type: + // 0: no Alpha data included (bits 3-0 of field 5.6 should also be set to zero) + // 1: undefined data in the Alpha field, can be ignored + // 2: undefined data in the Alpha field, but should be retained + // 3: useful Alpha channel data is present + // 4: pre-multiplied Alpha (see description below) + // 5 -127: RESERVED + // 128-255: Un-assigned + extensions.attributeType = stream.readUnsignedByte(); + + return extensions; + } + + private static double readRational(final ImageInputStream stream) throws IOException { + int numerator = stream.readUnsignedShort(); + int denominator = stream.readUnsignedShort(); + + return denominator != 0 ? numerator / (double) denominator : 1; + } + + private static Calendar readDate(final ImageInputStream stream) throws IOException { + Calendar calendar = Calendar.getInstance(); + calendar.clear(); + + int month = stream.readUnsignedShort(); + int date = stream.readUnsignedShort(); + int year = stream.readUnsignedShort(); + + int hourOfDay = stream.readUnsignedShort(); + int minute = stream.readUnsignedShort(); + int second = stream.readUnsignedShort(); + + // Unused + if (month == 0 && year == 0 && date == 0 && hourOfDay == 0 && minute == 0 && second == 0) { + return null; + } + + calendar.set(year, month - 1, date, hourOfDay, minute, second); + + return calendar; + } + + private static String readString(final ImageInputStream stream, final int maxLength) throws IOException { + byte[] data = new byte[maxLength]; + stream.readFully(data); + + return asZeroTerminatedASCIIString(data); + } + + private static String asZeroTerminatedASCIIString(final byte[] data) { + int len = data.length; + + for (int i = 0; i < data.length; i++) { + if (data[i] == 0) { + len = i; + } + } + + return new String(data, 0, len, StandardCharsets.US_ASCII); + } + + public boolean hasAlpha() { + switch (attributeType) { + case 3: + case 4: + return true; + default: + return false; + } + } + + public boolean isAlphaPremultiplied() { + switch (attributeType) { + case 4: + return true; + default: + return false; + } + } + + public long getThumbnailOffset() { + return postageStampOffset; + } + + public String getAuthorName() { + return authorName; + } + + public String getAuthorComments() { + return authorComments; + } + + public Calendar getCreationDate() { + return creationDate; + } + + public String getSoftware() { + return softwareId; + } + + public String getSoftwareVersion() { + return softwareVersion; + } + + public double getPixelAspectRatio() { + return pixelAspectRatio; + } + + public int getBackgroundColor() { + return backgroundColor; + } +} 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 5465ed03..bceccb8b 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 @@ -33,6 +33,7 @@ import com.twelvemonkeys.imageio.util.IIOUtil; import com.twelvemonkeys.imageio.util.ImageTypeSpecifiers; import com.twelvemonkeys.io.LittleEndianDataInputStream; import com.twelvemonkeys.io.enc.DecoderStream; +import com.twelvemonkeys.lang.Validate; import com.twelvemonkeys.xml.XMLSerializer; import javax.imageio.IIOException; @@ -51,6 +52,7 @@ import java.io.File; import java.io.IOException; import java.nio.ByteOrder; import java.util.ArrayList; +import java.util.Arrays; import java.util.Iterator; import java.util.List; @@ -59,6 +61,7 @@ public final class TGAImageReader extends ImageReaderBase { // http://www.gamers.org/dEngine/quake3/TGA.txt private TGAHeader header; + private TGAExtensions extensions; protected TGAImageReader(final ImageReaderSpi provider) { super(provider); @@ -67,6 +70,7 @@ public final class TGAImageReader extends ImageReaderBase { @Override protected void resetMembers() { header = null; + extensions = null; } @Override @@ -89,7 +93,7 @@ public final class TGAImageReader extends ImageReaderBase { public Iterator getImageTypes(final int imageIndex) throws IOException { ImageTypeSpecifier rawType = getRawImageType(imageIndex); - List specifiers = new ArrayList(); + List specifiers = new ArrayList<>(); // TODO: Implement specifiers.add(rawType); @@ -110,19 +114,29 @@ public final class TGAImageReader extends ImageReaderBase { return ImageTypeSpecifiers.createFromIndexColorModel(header.getColorMap()); case TGA.IMAGETYPE_MONOCHROME: case TGA.IMAGETYPE_MONOCHROME_RLE: - return ImageTypeSpecifiers.createGrayscale(1, DataBuffer.TYPE_BYTE); + return ImageTypeSpecifiers.createGrayscale(8, DataBuffer.TYPE_BYTE); case TGA.IMAGETYPE_TRUECOLOR: case TGA.IMAGETYPE_TRUECOLOR_RLE: ColorSpace sRGB = ColorSpace.getInstance(ColorSpace.CS_sRGB); + boolean hasAlpha = header.getAttributeBits() > 0 && extensions != null && extensions.hasAlpha(); + boolean isAlphaPremultiplied = extensions != null && extensions.isAlphaPremultiplied(); + switch (header.getPixelDepth()) { case 16: + if (hasAlpha) { + // USHORT_1555_ARGB... + return ImageTypeSpecifiers.createPacked(sRGB, 0x7C00, 0x03E0, 0x001F, 0x8000, DataBuffer.TYPE_USHORT, isAlphaPremultiplied); + } + // Default mask out alpha return ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_USHORT_555_RGB); case 24: return ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_3BYTE_BGR); case 32: - // 4BYTE_BGRA... - return ImageTypeSpecifiers.createInterleaved(sRGB, new int[] {2, 1, 0, 3}, DataBuffer.TYPE_BYTE, true, false); + // 4BYTE_BGRX... + // Can't mask out alpha (efficiently) for 4BYTE, so we'll ignore it while reading instead, + // if hasAlpha is false + return ImageTypeSpecifiers.createInterleaved(sRGB, new int[] {2, 1, 0, 3}, DataBuffer.TYPE_BYTE, true, isAlphaPremultiplied); default: throw new IIOException("Unknown pixel depth for truecolor: " + header.getPixelDepth()); } @@ -166,31 +180,32 @@ public final class TGAImageReader extends ImageReaderBase { DataInput input; if (imageType == TGA.IMAGETYPE_COLORMAPPED_RLE || imageType == TGA.IMAGETYPE_TRUECOLOR_RLE || imageType == TGA.IMAGETYPE_MONOCHROME_RLE) { input = new LittleEndianDataInputStream(new DecoderStream(IIOUtil.createStreamAdapter(imageInput), new RLEDecoder(header.getPixelDepth()))); - } else { + } + else { input = imageInput; } for (int y = 0; y < height; y++) { switch (header.getPixelDepth()) { - case 8: - case 24: - case 32: - byte[] rowDataByte = ((DataBufferByte) rowRaster.getDataBuffer()).getData(); - readRowByte(input, height, srcRegion, header.getOrigin(), xSub, ySub, rowDataByte, destRaster, clippedRow, y); - break; - case 16: - short[] rowDataUShort = ((DataBufferUShort) rowRaster.getDataBuffer()).getData(); - readRowUShort(input, height, srcRegion, header.getOrigin(), xSub, ySub, rowDataUShort, destRaster, clippedRow, y); - break; - default: - throw new AssertionError("Unsupported pixel depth: " + header.getPixelDepth()); - } - - processImageProgress(100f * y / height); - - if (height - 1 - y < srcRegion.y) { + case 8: + case 24: + case 32: + byte[] rowDataByte = ((DataBufferByte) rowRaster.getDataBuffer()).getData(); + readRowByte(input, height, srcRegion, header.getOrigin(), xSub, ySub, rowDataByte, destRaster, clippedRow, y); break; - } + case 16: + short[] rowDataUShort = ((DataBufferUShort) rowRaster.getDataBuffer()).getData(); + readRowUShort(input, height, srcRegion, header.getOrigin(), xSub, ySub, rowDataUShort, destRaster, clippedRow, y); + break; + default: + throw new AssertionError("Unsupported pixel depth: " + header.getPixelDepth()); + } + + processImageProgress(100f * y / height); + + if (height - 1 - y < srcRegion.y) { + break; + } if (abortRequested()) { processReadAborted(); @@ -212,11 +227,11 @@ public final class TGAImageReader extends ImageReaderBase { return; } - input.readFully(rowDataByte, 0, rowDataByte.length); - if (srcChannel.getNumBands() == 4) { - invertAlpha(rowDataByte); + if (srcChannel.getNumBands() == 4 && (header.getAttributeBits() == 0 || extensions != null && !extensions.hasAlpha())) { + // Remove the alpha channel (make pixels opaque) if there are no "attribute bits" (alpha bits) + removeAlpha32(rowDataByte); } // Subsample horizontal @@ -240,9 +255,9 @@ public final class TGAImageReader extends ImageReaderBase { } } - private void invertAlpha(final byte[] rowDataByte) { - for (int i = 3; i < rowDataByte.length; i += 4) { - rowDataByte[i] = (byte) (0xFF - rowDataByte[i]); + private void removeAlpha32(final byte[] rowData) { + for (int i = 3; i < rowData.length; i += 4) { + rowData[i] = (byte) 0xFF; } } @@ -313,21 +328,154 @@ public final class TGAImageReader extends ImageReaderBase { private void readHeader() throws IOException { if (header == null) { imageInput.setByteOrder(ByteOrder.LITTLE_ENDIAN); + + // Read header header = TGAHeader.read(imageInput); // System.err.println("header: " + header); imageInput.flushBefore(imageInput.getStreamPosition()); + + // Read footer, if 2.0 format (ends with TRUEVISION-XFILE\0) + skipToEnd(imageInput); + imageInput.seek(imageInput.getStreamPosition() - 26); + + long extOffset = imageInput.readInt(); + /*long devOffset = */imageInput.readInt(); // Ignored for now + + byte[] magic = new byte[18]; + imageInput.readFully(magic); + + if (Arrays.equals(magic, TGA.MAGIC)) { + if (extOffset > 0) { + imageInput.seek(extOffset); + extensions = TGAExtensions.read(imageInput); + } + } } imageInput.seek(imageInput.getFlushedPosition()); } - @Override public IIOMetadata getImageMetadata(final int imageIndex) throws IOException { + // TODO: Candidate util method + private static void skipToEnd(final ImageInputStream stream) throws IOException { + if (stream.length() > 0) { + // Seek to end of file + stream.seek(stream.length()); + } + else { + // Skip to end + long lastGood = stream.getStreamPosition(); + + while (stream.read() != -1) { + lastGood = stream.getStreamPosition(); + stream.skipBytes(1024); + } + + stream.seek(lastGood); + + while (true) { + if (stream.read() == -1) { + break; + } + // Just continue reading to EOF... + } + } + } + + // Thumbnail support + + @Override + public boolean readerSupportsThumbnails() { + return true; + } + + @Override + public boolean hasThumbnails(final int imageIndex) throws IOException { checkBounds(imageIndex); readHeader(); - return new TGAMetadata(header); + return extensions != null && extensions.getThumbnailOffset() > 0; + } + + @Override + public int getNumThumbnails(final int imageIndex) throws IOException { + return hasThumbnails(imageIndex) ? 1 : 0; + } + + @Override + public int getThumbnailWidth(final int imageIndex, final int thumbnailIndex) throws IOException { + checkBounds(imageIndex); + Validate.isTrue(thumbnailIndex >= 0 && thumbnailIndex < getNumThumbnails(imageIndex), "thumbnailIndex >= numThumbnails"); + + imageInput.seek(extensions.getThumbnailOffset()); + + return imageInput.readUnsignedByte(); + } + + @Override + public int getThumbnailHeight(final int imageIndex, final int thumbnailIndex) throws IOException { + getThumbnailWidth(imageIndex, thumbnailIndex); // Laziness... + + return imageInput.readUnsignedByte(); + } + + @Override + public BufferedImage readThumbnail(final int imageIndex, final int thumbnailIndex) throws IOException { + Iterator imageTypes = getImageTypes(imageIndex); + ImageTypeSpecifier rawType = getRawImageType(imageIndex); + + int width = getThumbnailWidth(imageIndex, thumbnailIndex); + int height = getThumbnailHeight(imageIndex, thumbnailIndex); + + // For thumbnail, always read entire image + Rectangle srcRegion = new Rectangle(width, height); + + BufferedImage destination = getDestination(null, imageTypes, width, height); + WritableRaster destRaster = destination.getRaster(); + WritableRaster rowRaster = rawType.createBufferedImage(width, 1).getRaster(); + + processThumbnailStarted(imageIndex, thumbnailIndex); + + // Thumbnail is always stored non-compressed, no need for RLE support + imageInput.seek(extensions.getThumbnailOffset() + 2); + + for (int y = 0; y < height; y++) { + switch (header.getPixelDepth()) { + case 8: + case 24: + case 32: + byte[] rowDataByte = ((DataBufferByte) rowRaster.getDataBuffer()).getData(); + readRowByte(imageInput, height, srcRegion, header.getOrigin(), 1, 1, rowDataByte, destRaster, rowRaster, y); + break; + case 16: + short[] rowDataUShort = ((DataBufferUShort) rowRaster.getDataBuffer()).getData(); + readRowUShort(imageInput, height, srcRegion, header.getOrigin(), 1, 1, rowDataUShort, destRaster, rowRaster, y); + break; + default: + throw new AssertionError("Unsupported pixel depth: " + header.getPixelDepth()); + } + + processThumbnailProgress(100f * y / height); + + if (height - 1 - y < srcRegion.y) { + break; + } + } + + processThumbnailComplete(); + + return destination; + } + + // Metadata support + + @Override + public IIOMetadata getImageMetadata(final int imageIndex) throws IOException { + checkBounds(imageIndex); + readHeader(); + + return new TGAMetadata(header, extensions); } public static void main(String[] args) throws IOException { diff --git a/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAImageReaderSpi.java b/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAImageReaderSpi.java index 2948915a..5a741ba3 100755 --- a/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAImageReaderSpi.java +++ b/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAImageReaderSpi.java @@ -45,7 +45,8 @@ public final class TGAImageReaderSpi extends ImageReaderSpiBase { super(new TGAProviderInfo()); } - @Override public boolean canDecodeInput(final Object source) throws IOException { + @Override + public boolean canDecodeInput(final Object source) throws IOException { if (!(source instanceof ImageInputStream)) { return false; } @@ -58,7 +59,7 @@ public final class TGAImageReaderSpi extends ImageReaderSpiBase { try { stream.setByteOrder(ByteOrder.LITTLE_ENDIAN); - // NOTE: The TGA format does not have a magic identifier, so this is guesswork... + // NOTE: The original TGA format does not have a magic identifier, so this is guesswork... // We'll try to match sane values, and hope no other files contains the same sequence. stream.readUnsignedByte(); @@ -88,11 +89,11 @@ public final class TGAImageReaderSpi extends ImageReaderSpiBase { int colorMapStart = stream.readUnsignedShort(); int colorMapSize = stream.readUnsignedShort(); - int colorMapDetph = stream.readUnsignedByte(); + int colorMapDepth = stream.readUnsignedByte(); if (colorMapSize == 0) { // No color map, all 3 fields should be 0 - if (colorMapStart!= 0 || colorMapDetph != 0) { + if (colorMapStart != 0 || colorMapDepth != 0) { return false; } } @@ -106,7 +107,7 @@ public final class TGAImageReaderSpi extends ImageReaderSpiBase { if (colorMapStart >= colorMapSize) { return false; } - if (colorMapDetph != 15 && colorMapDetph != 16 && colorMapDetph != 24 && colorMapDetph != 32) { + if (colorMapDepth != 15 && colorMapDepth != 16 && colorMapDepth != 24 && colorMapDepth != 32) { return false; } } @@ -134,6 +135,7 @@ public final class TGAImageReaderSpi extends ImageReaderSpiBase { // We're pretty sure by now, but there can still be false positives... // For 2.0 format, we could skip to end, and read "TRUEVISION-XFILE.\0" but it would be too slow + // unless we are working with a local file (and the file may still be a valid original TGA without it). return true; } finally { 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 f2d8f138..bcca2bb6 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 @@ -31,13 +31,17 @@ package com.twelvemonkeys.imageio.plugins.tga; import com.twelvemonkeys.imageio.AbstractMetadata; import javax.imageio.metadata.IIOMetadataNode; +import java.awt.*; import java.awt.image.IndexColorModel; +import java.util.Calendar; final class TGAMetadata extends AbstractMetadata { private final TGAHeader header; + private final TGAExtensions extensions; - TGAMetadata(final TGAHeader header) { + TGAMetadata(final TGAHeader header, final TGAExtensions extensions) { this.header = header; + this.extensions = extensions; } @Override @@ -45,6 +49,8 @@ final class TGAMetadata extends AbstractMetadata { IIOMetadataNode chroma = new IIOMetadataNode("Chroma"); IIOMetadataNode csType = new IIOMetadataNode("ColorSpaceType"); + chroma.appendChild(csType); + switch (header.getImageType()) { case TGA.IMAGETYPE_MONOCHROME: case TGA.IMAGETYPE_MONOCHROME_RLE: @@ -62,15 +68,22 @@ final class TGAMetadata extends AbstractMetadata { default: csType.setAttribute("name", "Unknown"); } - chroma.appendChild(csType); - // TODO: Channels in chroma node reflects channels in color model (see data node, for channels in data) + // NOTE: Channels in chroma node reflects channels in color model (see data node, for channels in data) IIOMetadataNode numChannels = new IIOMetadataNode("NumChannels"); + chroma.appendChild(numChannels); switch (header.getPixelDepth()) { case 8: - case 16: numChannels.setAttribute("value", Integer.toString(1)); break; + case 16: + if (header.getAttributeBits() > 0 && extensions != null && extensions.hasAlpha()) { + numChannels.setAttribute("value", Integer.toString(4)); + } + else { + numChannels.setAttribute("value", Integer.toString(3)); + } + break; case 24: numChannels.setAttribute("value", Integer.toString(3)); break; @@ -78,11 +91,10 @@ final class TGAMetadata extends AbstractMetadata { numChannels.setAttribute("value", Integer.toString(4)); break; } - chroma.appendChild(numChannels); IIOMetadataNode blackIsZero = new IIOMetadataNode("BlackIsZero"); - blackIsZero.setAttribute("value", "TRUE"); chroma.appendChild(blackIsZero); + blackIsZero.setAttribute("value", "TRUE"); // NOTE: TGA files may contain a color map, even if true color... // Not sure if this is a good idea to expose to the meta data, @@ -94,16 +106,26 @@ final class TGAMetadata extends AbstractMetadata { for (int i = 0; i < colorMap.getMapSize(); i++) { IIOMetadataNode paletteEntry = new IIOMetadataNode("PaletteEntry"); + palette.appendChild(paletteEntry); paletteEntry.setAttribute("index", Integer.toString(i)); paletteEntry.setAttribute("red", Integer.toString(colorMap.getRed(i))); paletteEntry.setAttribute("green", Integer.toString(colorMap.getGreen(i))); paletteEntry.setAttribute("blue", Integer.toString(colorMap.getBlue(i))); - - palette.appendChild(paletteEntry); } } + if (extensions != null && extensions.getBackgroundColor() != 0) { + Color background = new Color(extensions.getBackgroundColor(), true); + + IIOMetadataNode backgroundColor = new IIOMetadataNode("BackgroundColor"); + chroma.appendChild(backgroundColor); + + backgroundColor.setAttribute("red", Integer.toString(background.getRed())); + backgroundColor.setAttribute("green", Integer.toString(background.getGreen())); + backgroundColor.setAttribute("blue", Integer.toString(background.getBlue())); + } + return chroma; } @@ -116,15 +138,16 @@ final class TGAMetadata extends AbstractMetadata { case TGA.IMAGETYPE_COLORMAPPED_HUFFMAN: case TGA.IMAGETYPE_COLORMAPPED_HUFFMAN_QUADTREE: IIOMetadataNode node = new IIOMetadataNode("Compression"); + 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"; compressionTypeName.setAttribute("value", value); - node.appendChild(compressionTypeName); IIOMetadataNode lossless = new IIOMetadataNode("Lossless"); - lossless.setAttribute("value", "TRUE"); node.appendChild(lossless); + lossless.setAttribute("value", "TRUE"); return node; default: @@ -138,10 +161,12 @@ final class TGAMetadata extends AbstractMetadata { IIOMetadataNode node = new IIOMetadataNode("Data"); IIOMetadataNode planarConfiguration = new IIOMetadataNode("PlanarConfiguration"); - planarConfiguration.setAttribute("value", "PixelInterleaved"); node.appendChild(planarConfiguration); + planarConfiguration.setAttribute("value", "PixelInterleaved"); IIOMetadataNode sampleFormat = new IIOMetadataNode("SampleFormat"); + node.appendChild(sampleFormat); + switch (header.getImageType()) { case TGA.IMAGETYPE_COLORMAPPED: case TGA.IMAGETYPE_COLORMAPPED_RLE: @@ -154,13 +179,19 @@ final class TGAMetadata extends AbstractMetadata { break; } - node.appendChild(sampleFormat); - IIOMetadataNode bitsPerSample = new IIOMetadataNode("BitsPerSample"); + node.appendChild(bitsPerSample); + switch (header.getPixelDepth()) { case 8: - case 16: bitsPerSample.setAttribute("value", createListValue(1, Integer.toString(header.getPixelDepth()))); + case 16: + if (header.getAttributeBits() > 0 && extensions != null && extensions.hasAlpha()) { + bitsPerSample.setAttribute("value", "5, 5, 5, 1"); + } + else { + bitsPerSample.setAttribute("value", createListValue(3, "5")); + } break; case 24: bitsPerSample.setAttribute("value", createListValue(3, Integer.toString(8))); @@ -170,12 +201,6 @@ final class TGAMetadata extends AbstractMetadata { break; } - node.appendChild(bitsPerSample); - - // TODO: Do we need MSB? -// IIOMetadataNode sampleMSB = new IIOMetadataNode("SampleMSB"); -// sampleMSB.setAttribute("value", createListValue(header.getChannels(), "0")); - return node; } @@ -198,6 +223,7 @@ final class TGAMetadata extends AbstractMetadata { IIOMetadataNode dimension = new IIOMetadataNode("Dimension"); IIOMetadataNode imageOrientation = new IIOMetadataNode("ImageOrientation"); + dimension.appendChild(imageOrientation); switch (header.getOrigin()) { case TGA.ORIGIN_LOWER_LEFT: @@ -214,28 +240,64 @@ final class TGAMetadata extends AbstractMetadata { break; } - dimension.appendChild(imageOrientation); + IIOMetadataNode pixelAspectRatio = new IIOMetadataNode("PixelAspectRatio"); + dimension.appendChild(pixelAspectRatio); + pixelAspectRatio.setAttribute("value", extensions != null ? String.valueOf(extensions.getPixelAspectRatio()) : "1.0"); return dimension; } - // No document node + @Override + protected IIOMetadataNode getStandardDocumentNode() { + IIOMetadataNode document = new IIOMetadataNode("Document"); + + IIOMetadataNode formatVersion = new IIOMetadataNode("FormatVersion"); + document.appendChild(formatVersion); + formatVersion.setAttribute("value", extensions == null ? "1.0" : "2.0"); + + // ImageCreationTime from extensions date + if (extensions != null && extensions.getCreationDate() != null) { + IIOMetadataNode imageCreationTime = new IIOMetadataNode("ImageCreationTime"); + document.appendChild(imageCreationTime); + + Calendar date = extensions.getCreationDate(); + + imageCreationTime.setAttribute("year", String.valueOf(date.get(Calendar.YEAR))); + imageCreationTime.setAttribute("month", String.valueOf(date.get(Calendar.MONTH) + 1)); + imageCreationTime.setAttribute("day", String.valueOf(date.get(Calendar.DAY_OF_MONTH))); + imageCreationTime.setAttribute("hour", String.valueOf(date.get(Calendar.HOUR_OF_DAY))); + imageCreationTime.setAttribute("minute", String.valueOf(date.get(Calendar.MINUTE))); + imageCreationTime.setAttribute("second", String.valueOf(date.get(Calendar.SECOND))); + } + + return document; + } @Override protected IIOMetadataNode getStandardTextNode() { - // TODO: Extra "developer area" and other stuff might go here... + IIOMetadataNode text = new IIOMetadataNode("Text"); + + // NOTE: Names corresponds to equivalent fields in TIFF if (header.getIdentification() != null && !header.getIdentification().isEmpty()) { - IIOMetadataNode text = new IIOMetadataNode("Text"); - - IIOMetadataNode textEntry = new IIOMetadataNode("TextEntry"); - textEntry.setAttribute("keyword", "identification"); - textEntry.setAttribute("value", header.getIdentification()); - text.appendChild(textEntry); - - return text; + appendTextEntry(text, "DocumentName", header.getIdentification()); } - return null; + if (extensions != null) { + appendTextEntry(text, "Software", extensions.getSoftwareVersion() == null ? extensions.getSoftware() : extensions.getSoftware() + " " + extensions.getSoftwareVersion()); + appendTextEntry(text, "Artist", extensions.getAuthorName()); + appendTextEntry(text, "UserComment", extensions.getAuthorComments()); + } + + return text.hasChildNodes() ? text : null; + } + + private void appendTextEntry(final IIOMetadataNode parent, final String keyword, final String value) { + if (value != null) { + IIOMetadataNode textEntry = new IIOMetadataNode("TextEntry"); + parent.appendChild(textEntry); + textEntry.setAttribute("keyword", keyword); + textEntry.setAttribute("value", value); + } } // No tiling @@ -245,9 +307,23 @@ final class TGAMetadata extends AbstractMetadata { IIOMetadataNode transparency = new IIOMetadataNode("Transparency"); IIOMetadataNode alpha = new IIOMetadataNode("Alpha"); - alpha.setAttribute("value", header.getPixelDepth() == 32 ? "nonpremultiplied" : "none"); transparency.appendChild(alpha); + if (extensions != null) { + if (extensions.hasAlpha()) { + alpha.setAttribute("value", extensions.isAlphaPremultiplied() ? "premultiplied" : "nonpremultiplied"); + } + else { + alpha.setAttribute("value", "none"); + } + } + else if (header.getAttributeBits() == 8) { + alpha.setAttribute("value", "nonpremultiplied"); + } + else { + alpha.setAttribute("value", "none"); + } + return transparency; } }