TGAHeader.java
/*
* Copyright (c) 2014, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.imageio.plugins.tga;
import javax.imageio.IIOException;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.stream.ImageInputStream;
import java.awt.image.*;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import static com.twelvemonkeys.lang.Validate.notNull;
import static java.awt.color.ColorSpace.*;
final class TGAHeader {
private int colorMapType;
private int imageType;
private int colorMapStart;
private int colorMapSize;
private int colorMapDepth;
private int x;
private int y;
private int width;
private int height;
private int pixelDepth;
private int attributeBits;
int origin;
private int interleave;
String identification;
private IndexColorModel colorMap;
int getImageType() {
return imageType;
}
int getWidth() {
return width;
}
int getHeight() {
return height;
}
int getPixelDepth() {
return pixelDepth;
}
int getAttributeBits() {
return attributeBits;
}
int getOrigin() {
return origin;
}
int getInterleave() {
return interleave;
}
String getIdentification() {
return identification;
}
IndexColorModel getColorMap() {
return colorMap;
}
@Override
public String toString() {
return "TGAHeader{" +
"colorMapType=" + colorMapType +
", imageType=" + imageType +
", colorMapStart=" + colorMapStart +
", colorMapSize=" + colorMapSize +
", colorMapDepth=" + colorMapDepth +
", x=" + x +
", y=" + y +
", width=" + width +
", height=" + height +
", pixelDepth=" + pixelDepth +
", attributeBits=" + attributeBits +
", origin=" + origin +
", interleave=" + interleave +
(identification != null ? ", identification='" + identification + '\'' : "") +
'}';
}
static TGAHeader from(final ImageTypeSpecifier type, final boolean compressed) {
return from(type, 0, 0, compressed);
}
static TGAHeader from(final ImageTypeSpecifier type, int width, int height, final boolean compressed) {
notNull(type, "type");
ColorModel colorModel = type.getColorModel();
IndexColorModel colorMap = colorModel instanceof IndexColorModel ? (IndexColorModel) colorModel : null;
TGAHeader header = new TGAHeader();
header.colorMapType = colorMap != null ? 1 : 0;
header.imageType = getImageType(colorModel, compressed);
header.colorMapStart = 0;
header.colorMapSize = colorMap != null ? colorMap.getMapSize() : 0;
header.colorMapDepth = colorMap != null ? (colorMap.hasAlpha() ? 32 : 24) : 0;
header.x = 0;
header.y = 0;
header.width = width;
header.height = height;
header.pixelDepth = colorModel.getPixelSize() == 15 ? 16 : colorModel.getPixelSize();
header.origin = TGA.ORIGIN_UPPER_LEFT; // TODO: Allow parameter to control this?
header.attributeBits = colorModel.hasAlpha() ? 8 : 0; // TODO: FixMe
header.identification = null;
header.colorMap = colorMap;
return header;
}
private static int getImageType(final ColorModel colorModel, final boolean compressed) {
int uncompressedType;
if (colorModel instanceof IndexColorModel) {
uncompressedType = TGA.IMAGETYPE_COLORMAPPED;
}
else {
switch (colorModel.getColorSpace().getType()) {
case TYPE_RGB:
uncompressedType = TGA.IMAGETYPE_TRUECOLOR;
break;
case TYPE_GRAY:
uncompressedType = TGA.IMAGETYPE_MONOCHROME;
break;
default:
throw new IllegalArgumentException("Unsupported color space for TGA: " + colorModel.getColorSpace());
}
}
return uncompressedType | (compressed ? 8 : 0);
}
void write(final DataOutput stream) throws IOException {
byte[] idBytes = identification != null ? identification.getBytes(StandardCharsets.US_ASCII) : new byte[0];
stream.writeByte(idBytes.length);
stream.writeByte(colorMapType);
stream.writeByte(imageType);
stream.writeShort(colorMapStart);
stream.writeShort(colorMapSize);
stream.writeByte(colorMapDepth);
stream.writeShort(x);
stream.writeShort(y);
stream.writeShort(width);
stream.writeShort(height);
stream.writeByte(pixelDepth);
stream.writeByte(attributeBits | origin << 4 | interleave << 6);
// Identification
stream.write(idBytes);
// Color map
if (colorMap != null) {
int[] rgb = new int[colorMap.getMapSize()];
colorMap.getRGBs(rgb);
int components = colorMap.hasAlpha() ? 4 : 3;
byte[] cmap = new byte[rgb.length * components];
for (int i = 0; i < rgb.length; i++) {
cmap[i * components ] = (byte) ((rgb[i] ) & 0xff); // B
cmap[i * components + 1] = (byte) ((rgb[i] >> 8) & 0xff); // G
cmap[i * components + 2] = (byte) ((rgb[i] >> 16) & 0xff); // R
if (components == 4) {
cmap[i * components + 3] = (byte) ((rgb[i] >>> 24) & 0xff); // A
}
}
stream.write(cmap);
}
}
static TGAHeader read(final ImageInputStream imageInput) throws IOException {
// typedef struct _TgaHeader
// {
// BYTE IDLength; /* 00h Size of Image ID field */
// BYTE ColorMapType; /* 01h Color map type */
// BYTE ImageType; /* 02h Image type code */
// WORD CMapStart; /* 03h Color map origin */
// WORD CMapLength; /* 05h Color map length */
// BYTE CMapDepth; /* 07h Depth of color map entries */
// WORD XOffset; /* 08h X origin of image */
// WORD YOffset; /* 0Ah Y origin of image */
// WORD Width; /* 0Ch Width of image */
// WORD Height; /* 0Eh Height of image */
// BYTE PixelDepth; /* 10h Image pixel size */
// BYTE ImageDescriptor; /* 11h Image descriptor byte */
// } TGAHEAD;
TGAHeader header = new TGAHeader();
int imageIdLength = imageInput.readUnsignedByte();
header.colorMapType = imageInput.readUnsignedByte(); // 1: palette, 0: no palette, other: Unspecified... (< 127 reserved, 128-256: free for devs)
header.imageType = imageInput.readUnsignedByte(); // 0: no image data, 1: Colormap, 2: Truecolor, 3: Monochrome, 9: Colormap + RLE, 10: Truecolor + RLE, 11: Monochrome + RLE, other: Unspecified.
// Color map specification
header.colorMapStart = imageInput.readUnsignedShort();
header.colorMapSize = imageInput.readUnsignedShort(); // number of colors, not bytes..?
header.colorMapDepth = imageInput.readUnsignedByte(); // 15, 16, 24 or 32!
// Image specification
header.x = imageInput.readUnsignedShort();
header.y = imageInput.readUnsignedShort();
header.width = imageInput.readUnsignedShort();
header.height = imageInput.readUnsignedShort();
header.pixelDepth = imageInput.readUnsignedByte();
int imageDescriptor = imageInput.readUnsignedByte();
header.attributeBits = imageDescriptor & 0xf; // Bit 0-3: number of "attribute bits" per pixel
header.origin = (imageDescriptor & 0x30) >> 4; // Bit 4-6: origin 0: lower left, 2: upper left
header.interleave = (imageDescriptor & 0xC0) >> 6; // Bit 7-8: interleave 0: non-interleaved, 1: two-way, 2: four way, 3: reserved
// Image ID section, not *really* part of the header, but let's get rid of it...
if (imageIdLength > 0) {
header.identification = readString(imageInput, imageIdLength);
}
// Color map, not *really* part of the header
if (header.colorMapType == TGA.COLORMAP_PALETTE) {
header.colorMap = readColorMap(imageInput, header);
}
return header;
}
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;
break;
}
}
return new String(data, 0, len, StandardCharsets.US_ASCII);
}
private static IndexColorModel readColorMap(final DataInput stream, final TGAHeader header) throws IOException {
int size = header.colorMapSize;
int depth = header.colorMapDepth;
int bytes = (depth + 7) / 8;
byte[] cmap = new byte[size * bytes];
stream.readFully(cmap);
boolean hasAlpha;
switch (depth) {
case 16:
// Expand 16 (15) bit to 24 bit RGB
byte[] temp = cmap;
cmap = new byte[size * 3];
for (int i = 0; i < temp.length / 2; i++) {
// TODO: Handle attribute bit (A)??
// GGGB BBBB - ARRR RRGG
byte low = temp[i * 2];
byte high = temp[i * 2 + 1];
cmap[i * 3 ] = (byte) (((high & 0x7C) >> 2) << 3);
cmap[i * 3 + 1] = (byte) (((high & 0x03) << 3 | (low & 0xE0) >> 5) << 3);
cmap[i * 3 + 2] = (byte) (((low & 0x1F)) << 3);
}
hasAlpha = false;
break;
case 24:
// BGR -> RGB
for (int i = 0; i < cmap.length; i += 3) {
byte b = cmap[i];
cmap[i ] = cmap[i + 2];
cmap[i + 2] = b;
}
hasAlpha = false;
break;
case 32:
// BGRA -> RGBA
for (int i = 0; i < cmap.length; i += 4) {
byte b = cmap[i];
cmap[i ] = cmap[i + 2];
cmap[i + 2] = b;
}
hasAlpha = true;
break;
default:
throw new IIOException("Unsupported color map depth: " + header.colorMapDepth);
}
return new IndexColorModel(header.pixelDepth, size, cmap, header.colorMapStart, hasAlpha);
}
}