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;
}
}