IFF: Read support for TVPaint DEEP and TVPP

+ Bonus: Massive code clean-up/refactor.

(cherry picked from commit 73ad024833)
This commit is contained in:
Harald Kuhr
2022-02-03 17:26:41 +01:00
parent 3cf6a4b836
commit b7b2a61c93
30 changed files with 1599 additions and 768 deletions
@@ -69,14 +69,14 @@ abstract class AbstractMultiPaletteChunk extends IFFChunk implements MultiPalett
protected WeakReference<IndexColorModel> originalPalette; protected WeakReference<IndexColorModel> originalPalette;
protected MutableIndexColorModel mutablePalette; protected MutableIndexColorModel mutablePalette;
public AbstractMultiPaletteChunk(int pChunkId, int pChunkLength) { public AbstractMultiPaletteChunk(int chunkId, int chunkLength) {
super(pChunkId, pChunkLength); super(chunkId, chunkLength);
} }
@Override @Override
void readChunk(final DataInput pInput) throws IOException { void readChunk(final DataInput input) throws IOException {
if (chunkId == IFF.CHUNK_SHAM) { if (chunkId == IFF.CHUNK_SHAM) {
pInput.readUnsignedShort(); // Version, typically 0, skipped input.readUnsignedShort(); // Version, typically 0, skipped
} }
int rows = chunkLength / 32; /* sizeof(word) * 16 */ int rows = chunkLength / 32; /* sizeof(word) * 16 */
@@ -91,7 +91,7 @@ abstract class AbstractMultiPaletteChunk extends IFFChunk implements MultiPalett
} }
for (int i = 0; i < 16; i++ ) { for (int i = 0; i < 16; i++ ) {
int data = pInput.readUnsignedShort(); int data = input.readUnsignedShort();
changes[row][i].index = i; changes[row][i].index = i;
changes[row][i].r = (byte) (((data & 0x0f00) >> 8) * FACTOR_4BIT); changes[row][i].r = (byte) (((data & 0x0f00) >> 8) * FACTOR_4BIT);
@@ -102,7 +102,7 @@ abstract class AbstractMultiPaletteChunk extends IFFChunk implements MultiPalett
} }
@Override @Override
void writeChunk(DataOutput pOutput) { void writeChunk(DataOutput output) {
throw new UnsupportedOperationException("Method writeChunk not implemented"); throw new UnsupportedOperationException("Method writeChunk not implemented");
} }
@@ -109,64 +109,65 @@ final class BMHDChunk extends IFFChunk {
int pageWidth; int pageWidth;
int pageHeight; int pageHeight;
BMHDChunk(int pChunkLength) { BMHDChunk(int chunkLength) {
super(IFF.CHUNK_BMHD, pChunkLength); super(IFF.CHUNK_BMHD, chunkLength);
} }
BMHDChunk(int pWidth, int pHeight, int pBitplanes, int pMaskType, int pCompressionType, int pTransparentIndex) { BMHDChunk(int width, int height, int bitplanes, int maskType, int compressionType, int transparentIndex) {
super(IFF.CHUNK_BMHD, 20); super(IFF.CHUNK_BMHD, 20);
width = pWidth; this.width = width;
height = pHeight; this.height = height;
xPos = 0; xPos = 0;
yPos = 0; yPos = 0;
bitplanes = pBitplanes; this.bitplanes = bitplanes;
maskType = pMaskType; this.maskType = maskType;
compressionType = pCompressionType; this.compressionType = compressionType;
transparentIndex = pTransparentIndex; this.transparentIndex = transparentIndex;
xAspect = 1; xAspect = 1;
yAspect = 1; yAspect = 1;
pageWidth = Math.min(pWidth, Short.MAX_VALUE); // For some reason, these are signed? pageWidth = Math.min(width, Short.MAX_VALUE); // For some reason, these are signed?
pageHeight = Math.min(pHeight, Short.MAX_VALUE); pageHeight = Math.min(height, Short.MAX_VALUE);
} }
@Override @Override
void readChunk(final DataInput pInput) throws IOException { void readChunk(final DataInput input) throws IOException {
if (chunkLength != 20) { if (chunkLength != 20) {
throw new IIOException("Unknown BMHD chunk length: " + chunkLength); throw new IIOException("Unknown BMHD chunk length: " + chunkLength);
} }
width = pInput.readUnsignedShort();
height = pInput.readUnsignedShort(); width = input.readUnsignedShort();
xPos = pInput.readShort(); height = input.readUnsignedShort();
yPos = pInput.readShort(); xPos = input.readShort();
bitplanes = pInput.readUnsignedByte(); yPos = input.readShort();
maskType = pInput.readUnsignedByte(); bitplanes = input.readUnsignedByte();
compressionType = pInput.readUnsignedByte(); maskType = input.readUnsignedByte();
pInput.readByte(); // PAD compressionType = input.readUnsignedByte();
transparentIndex = pInput.readUnsignedShort(); input.readByte(); // PAD
xAspect = pInput.readUnsignedByte(); transparentIndex = input.readUnsignedShort();
yAspect = pInput.readUnsignedByte(); xAspect = input.readUnsignedByte();
pageWidth = pInput.readShort(); yAspect = input.readUnsignedByte();
pageHeight = pInput.readShort(); pageWidth = input.readShort();
pageHeight = input.readShort();
} }
@Override @Override
void writeChunk(final DataOutput pOutput) throws IOException { void writeChunk(final DataOutput output) throws IOException {
pOutput.writeInt(chunkId); output.writeInt(chunkId);
pOutput.writeInt(chunkLength); output.writeInt(chunkLength);
pOutput.writeShort(width); output.writeShort(width);
pOutput.writeShort(height); output.writeShort(height);
pOutput.writeShort(xPos); output.writeShort(xPos);
pOutput.writeShort(yPos); output.writeShort(yPos);
pOutput.writeByte(bitplanes); output.writeByte(bitplanes);
pOutput.writeByte(maskType); output.writeByte(maskType);
pOutput.writeByte(compressionType); output.writeByte(compressionType);
pOutput.writeByte(0); // PAD output.writeByte(0); // PAD
pOutput.writeShort(transparentIndex); output.writeShort(transparentIndex);
pOutput.writeByte(xAspect); output.writeByte(xAspect);
pOutput.writeByte(yAspect); output.writeByte(yAspect);
pOutput.writeShort(pageWidth); output.writeShort(pageWidth);
pOutput.writeShort(pageHeight); output.writeShort(pageHeight);
} }
@Override @Override
@@ -33,6 +33,8 @@ package com.twelvemonkeys.imageio.plugins.iff;
import java.io.DataInput; import java.io.DataInput;
import java.io.DataOutput; import java.io.DataOutput;
import static com.twelvemonkeys.lang.Validate.isTrue;
/** /**
* BODYChunk * BODYChunk
* *
@@ -40,17 +42,20 @@ import java.io.DataOutput;
* @version $Id: BODYChunk.java,v 1.0 28.feb.2006 01:25:49 haku Exp$ * @version $Id: BODYChunk.java,v 1.0 28.feb.2006 01:25:49 haku Exp$
*/ */
final class BODYChunk extends IFFChunk { final class BODYChunk extends IFFChunk {
BODYChunk(int pChunkLength) { final long chunkOffset;
super(IFF.CHUNK_BODY, pChunkLength);
BODYChunk(int chunkId, int chunkLength, long chunkOffset) {
super(isTrue(chunkId == IFF.CHUNK_BODY || chunkId == IFF.CHUNK_DBOD, chunkId, "Illegal body chunk: '%s'"), chunkLength);
this.chunkOffset = chunkOffset;
} }
@Override @Override
void readChunk(final DataInput pInput) { void readChunk(final DataInput input) {
throw new InternalError("BODY chunk should only be read from IFFImageReader"); throw new InternalError("BODY chunk should only be read from IFFImageReader");
} }
@Override @Override
void writeChunk(final DataOutput pOutput) { void writeChunk(final DataOutput output) {
throw new InternalError("BODY chunk should only be written from IFFImageWriter"); throw new InternalError("BODY chunk should only be written from IFFImageWriter");
} }
} }
@@ -48,21 +48,21 @@ final class CAMGChunk extends IFFChunk {
int camg; int camg;
CAMGChunk(int pLength) { CAMGChunk(int chunkLength) {
super(IFF.CHUNK_CAMG, pLength); super(IFF.CHUNK_CAMG, chunkLength);
} }
@Override @Override
void readChunk(final DataInput pInput) throws IOException { void readChunk(final DataInput input) throws IOException {
if (chunkLength != 4) { if (chunkLength != 4) {
throw new IIOException("Unknown CAMG chunk length: " + chunkLength); throw new IIOException("Unknown CAMG chunk length: " + chunkLength);
} }
camg = pInput.readInt(); camg = input.readInt();
} }
@Override @Override
void writeChunk(final DataOutput pOutput) { void writeChunk(final DataOutput output) {
throw new InternalError("Not implemented: writeChunk()"); throw new InternalError("Not implemented: writeChunk()");
} }
@@ -31,9 +31,7 @@
package com.twelvemonkeys.imageio.plugins.iff; package com.twelvemonkeys.imageio.plugins.iff;
import javax.imageio.IIOException; import javax.imageio.IIOException;
import java.awt.image.BufferedImage;
import java.awt.image.IndexColorModel; import java.awt.image.IndexColorModel;
import java.awt.image.WritableRaster;
import java.io.DataInput; import java.io.DataInput;
import java.io.DataOutput; import java.io.DataOutput;
import java.io.IOException; import java.io.IOException;
@@ -69,7 +67,7 @@ final class CMAPChunk extends IFFChunk {
} }
@Override @Override
void readChunk(final DataInput pInput) throws IOException { void readChunk(final DataInput input) throws IOException {
int numColors = chunkLength / 3; int numColors = chunkLength / 3;
reds = new byte[numColors]; reds = new byte[numColors];
@@ -77,9 +75,9 @@ final class CMAPChunk extends IFFChunk {
blues = reds.clone(); blues = reds.clone();
for (int i = 0; i < numColors; i++) { for (int i = 0; i < numColors; i++) {
reds[i] = pInput.readByte(); reds[i] = input.readByte();
greens[i] = pInput.readByte(); greens[i] = input.readByte();
blues[i] = pInput.readByte(); blues[i] = input.readByte();
} }
// TODO: When reading in a CMAP for 8-bit-per-gun display or // TODO: When reading in a CMAP for 8-bit-per-gun display or
@@ -92,25 +90,25 @@ final class CMAPChunk extends IFFChunk {
// All chunks are WORD aligned (even sized), may need to read pad... // All chunks are WORD aligned (even sized), may need to read pad...
if (chunkLength % 2 != 0) { if (chunkLength % 2 != 0) {
pInput.readByte(); input.readByte();
} }
} }
@Override @Override
void writeChunk(final DataOutput pOutput) throws IOException { void writeChunk(final DataOutput output) throws IOException {
pOutput.writeInt(chunkId); output.writeInt(chunkId);
pOutput.writeInt(chunkLength); output.writeInt(chunkLength);
final int length = model.getMapSize(); final int length = model.getMapSize();
for (int i = 0; i < length; i++) { for (int i = 0; i < length; i++) {
pOutput.writeByte(model.getRed(i)); output.writeByte(model.getRed(i));
pOutput.writeByte(model.getGreen(i)); output.writeByte(model.getGreen(i));
pOutput.writeByte(model.getBlue(i)); output.writeByte(model.getBlue(i));
} }
if (chunkLength % 2 != 0) { if (chunkLength % 2 != 0) {
pOutput.writeByte(0); // PAD output.writeByte(0); // PAD
} }
} }
@@ -119,25 +117,11 @@ final class CMAPChunk extends IFFChunk {
return super.toString() + " {colorMap=" + model + "}"; return super.toString() + " {colorMap=" + model + "}";
} }
BufferedImage createPaletteImage(final BMHDChunk header, boolean isEHB) throws IIOException { public IndexColorModel getIndexColorModel(final Form.ILBMForm header) throws IIOException {
// Create a 1 x colors.length image
IndexColorModel cm = getIndexColorModel(header, isEHB);
WritableRaster raster = cm.createCompatibleWritableRaster(cm.getMapSize(), 1);
byte[] pixel = null;
for (int x = 0; x < cm.getMapSize(); x++) {
pixel = (byte[]) cm.getDataElements(cm.getRGB(x), pixel);
raster.setDataElements(x, 0, pixel);
}
return new BufferedImage(cm, raster, cm.isAlphaPremultiplied(), null);
}
public IndexColorModel getIndexColorModel(final BMHDChunk header, boolean isEHB) throws IIOException {
if (model == null) { if (model == null) {
int numColors = reds.length; // All arrays are same size int numColors = reds.length; // All arrays are same size
if (isEHB) { if (header.isEHB()) {
if (numColors == 32) { if (numColors == 32) {
reds = Arrays.copyOf(reds, numColors * 2); reds = Arrays.copyOf(reds, numColors * 2);
blues = Arrays.copyOf(blues, numColors * 2); blues = Arrays.copyOf(blues, numColors * 2);
@@ -160,8 +144,9 @@ final class CMAPChunk extends IFFChunk {
// Would it work to double to numbers of colors, and create an indexcolormodel, // Would it work to double to numbers of colors, and create an indexcolormodel,
// with alpha, where all colors above the original color is all transparent? // with alpha, where all colors above the original color is all transparent?
// This is a waste of time and space, of course... // This is a waste of time and space, of course...
int transparent = header.maskType == BMHDChunk.MASK_TRANSPARENT_COLOR ? header.transparentIndex : -1; int transparent = header.transparentIndex();
int bitplanes = header.bitplanes == 25 ? 8 : header.bitplanes; int bitplanes = header.bitplanes() == 25 ? 8 : header.bitplanes();
model = new IndexColorModel(bitplanes, reds.length, reds, greens, blues, transparent); // https://github.com/haraldk/TwelveMonkeys/issues/15 model = new IndexColorModel(bitplanes, reds.length, reds, greens, blues, transparent); // https://github.com/haraldk/TwelveMonkeys/issues/15
} }
@@ -38,7 +38,7 @@ package com.twelvemonkeys.imageio.plugins.iff;
* @version $Id: CTBLChunk.java,v 1.0 30.03.12 14:53 haraldk Exp$ * @version $Id: CTBLChunk.java,v 1.0 30.03.12 14:53 haraldk Exp$
*/ */
final class CTBLChunk extends AbstractMultiPaletteChunk { final class CTBLChunk extends AbstractMultiPaletteChunk {
CTBLChunk(int pChunkLength) { CTBLChunk(int chunkLength) {
super(IFF.CHUNK_CTBL, pChunkLength); super(IFF.CHUNK_CTBL, chunkLength);
} }
} }
@@ -0,0 +1,102 @@
/*
* Copyright (c) 2022, 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.iff;
import javax.imageio.IIOException;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
/**
* DGBLChunk
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @version $Id: DGBLChunk.java,v 1.0 28.feb.2006 02:10:07 haku Exp$
*/
final class DGBLChunk extends IFFChunk {
/*
//
struct DGBL = {
//
// Size of source display
//
UWORD DisplayWidth,DisplayHeight;
//
// Type of compression
//
UWORD Compression;
//
// Pixel aspect, a ration w:h
//
UBYTE xAspect,yAspect;
};
*/
int displayWidth;
int displayHeight;
int compressionType;
int xAspect;
int yAspect;
DGBLChunk(int chunkLength) {
super(IFF.CHUNK_DGBL, chunkLength);
}
@Override
void readChunk(final DataInput input) throws IOException {
if (chunkLength != 8) {
throw new IIOException("Unknown DBGL chunk length: " + chunkLength);
}
displayWidth = input.readUnsignedShort();
displayHeight = input.readUnsignedShort();
compressionType = input.readUnsignedShort();
xAspect = input.readUnsignedByte();
yAspect = input.readUnsignedByte();
}
@Override
void writeChunk(final DataOutput output) {
throw new InternalError("Not implemented: writeChunk()");
}
@Override
public String toString() {
return super.toString() +
"{displayWidth=" + displayWidth +
", displayHeight=" + displayHeight +
", compression=" + compressionType +
", xAspect=" + xAspect +
", yAspect=" + yAspect +
'}';
}
}
@@ -0,0 +1,80 @@
/*
* Copyright (c) 2022, 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.iff;
import javax.imageio.IIOException;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
/**
* DLOCChunk.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: DLOCChunk.java,v 1.0 31/01/2022 haraldk Exp$
*/
final class DLOCChunk extends IFFChunk {
int width;
int height;
int x;
int y;
DLOCChunk(final int chunkLength) {
super(IFF.CHUNK_DLOC, chunkLength);
}
@Override
void readChunk(final DataInput input) throws IOException {
if (chunkLength != 8) {
throw new IIOException("Unknown DLOC chunk length: " + chunkLength);
}
width = input.readUnsignedShort();
height = input.readUnsignedShort();
x = input.readShort();
y = input.readShort();
}
@Override
void writeChunk(final DataOutput output) {
throw new InternalError("Not implemented: writeChunk()");
}
@Override
public String toString() {
return super.toString() +
"{width=" + width +
", height=" + height +
", x=" + x +
", y=" + y + '}';
}
}
@@ -0,0 +1,103 @@
package com.twelvemonkeys.imageio.plugins.iff;
import javax.imageio.IIOException;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.util.Arrays;
/**
* DPELChunk.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: DPELChunk.java,v 1.0 01/02/2022 haraldk Exp$
*/
final class DPELChunk extends IFFChunk {
/*
//
// Chunk DPEL
// ----------
struct DPEL = {
//
// Number of pixel components
//
ULONG nElements;
//
// The TypeDepth structure is repeated nElement times to identify
// the content of every pixel. Pixels will always be padded to
// byte boundaries. The DBOD chunk will be padded to an even
// longword boundary.
//
struct TypeDepth = {
//
// Type of data
//
UWORD cType;
//
// Bit depth of this type
//
UWORD cBitDepth;
} typedepth[Nelements];
};
*/
TypeDepth[] typeDepths;
DPELChunk(final int chunkLength) {
super(IFF.CHUNK_DPEL, chunkLength);
}
@Override
void readChunk(final DataInput input) throws IOException {
int components = input.readInt(); // Strictly, it's unsigned, but that many components is unlikely...
if (chunkLength != 4 + components * 4) {
throw new IIOException("Unsupported DPEL chunk length: " + chunkLength);
}
typeDepths = new TypeDepth[components];
for (int i = 0; i < components; i++) {
typeDepths[i] = new TypeDepth(input.readUnsignedShort(), input.readUnsignedShort());
}
}
@Override
void writeChunk(final DataOutput output) {
throw new InternalError("Not implemented: writeChunk()");
}
@Override
public String toString() {
return super.toString()
+ "{typeDepths=" + Arrays.toString(typeDepths) + '}';
}
public int bitsPerPixel() {
int bitCount = 0;
for (TypeDepth typeDepth : typeDepths) {
bitCount += typeDepth.bitDepth;
}
return bitCount;
}
static class TypeDepth {
final int type;
final int bitDepth;
TypeDepth(final int type, final int bitDepth) {
this.type = type;
this.bitDepth = bitDepth;
}
@Override
public String toString() {
return "TypeDepth{" +
"type=" + type +
", bits=" + bitDepth +
'}';
}
}
}
@@ -0,0 +1,364 @@
package com.twelvemonkeys.imageio.plugins.iff;
import javax.imageio.IIOException;
import java.awt.image.ColorModel;
import java.awt.image.IndexColorModel;
import java.util.ArrayList;
import java.util.List;
import static com.twelvemonkeys.imageio.plugins.iff.IFFUtil.toChunkStr;
/**
* Form.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: Form.java,v 1.0 31/01/2022 haraldk Exp$
*/
abstract class Form {
final int formType;
final List<GenericChunk> meta = new ArrayList<>();
Form(int formType) {
this.formType = formType;
}
abstract int width();
abstract int height();
abstract int xAspect();
abstract int yAspect();
abstract int bitplanes();
abstract int compressionType();
boolean isMultiPalette() {
return false;
}
boolean isHAM() {
return false;
}
public boolean premultiplied() {
return false;
}
public int sampleSize() {
return 1;
}
public int transparentIndex() {
return -1;
}
public IndexColorModel colorMap() throws IIOException {
return null;
}
public ColorModel colorMapForRow(IndexColorModel colorModel, int row) {
throw new UnsupportedOperationException();
}
abstract long bodyOffset();
abstract long bodyLength();
@Override
public String toString() {
return toChunkStr(formType);
}
Form with(final IFFChunk chunk) throws IIOException {
if (chunk instanceof GenericChunk) {
// TODO: This feels kind of hackish, as it breaks the immutable design, perhaps we should just reconsider...
meta.add((GenericChunk) chunk);
return this;
}
throw new IllegalArgumentException(chunk + " not supported in FORM type " + toChunkStr(formType));
}
static Form ofType(int formType) {
switch (formType) {
case IFF.TYPE_ACBM:
case IFF.TYPE_ILBM:
case IFF.TYPE_PBM:
case IFF.TYPE_RGB8:
return new ILBMForm(formType);
case IFF.TYPE_DEEP:
case IFF.TYPE_TVPP:
return new DEEPForm(formType);
default:
throw new IllegalArgumentException("FORM type " + toChunkStr(formType) + " not supported");
}
}
/**
* The set of chunks used in the "original" ILBM,
* and also ACBM, PBM and RGB8 FORMs.
*/
static final class ILBMForm extends Form {
private final BMHDChunk bitmapHeader;
private final CAMGChunk viewMode;
private final CMAPChunk colorMap;
private final AbstractMultiPaletteChunk multiPalette;
private final BODYChunk body;
ILBMForm(int formType) {
this(formType, null, null, null, null, null);
}
private ILBMForm(final int formType, final BMHDChunk bitmapHeader, final CAMGChunk viewMode, final CMAPChunk colorMap, final AbstractMultiPaletteChunk multiPalette, final BODYChunk body) {
super(formType);
this.bitmapHeader = bitmapHeader;
this.viewMode = viewMode;
this.colorMap = colorMap;
this.multiPalette = multiPalette;
this.body = body;
}
@Override
int width() {
return bitmapHeader.width;
}
@Override
int height() {
return bitmapHeader.height;
}
@Override
int bitplanes() {
return bitmapHeader.bitplanes;
}
@Override
int compressionType() {
return bitmapHeader.compressionType;
}
@Override
int xAspect() {
return bitmapHeader.xAspect;
}
@Override
int yAspect() {
return bitmapHeader.yAspect;
}
@Override
boolean isMultiPalette() {
return multiPalette != null;
}
boolean isEHB() {
return viewMode != null && viewMode.isEHB();
}
@Override
boolean isHAM() {
return viewMode != null && viewMode.isHAM();
}
boolean isLaced() {
return viewMode != null && viewMode.isLaced();
}
@Override
public int transparentIndex() {
return bitmapHeader.maskType == BMHDChunk.MASK_TRANSPARENT_COLOR ? bitmapHeader.transparentIndex : -1;
}
@Override
public IndexColorModel colorMap() throws IIOException {
return colorMap != null ? colorMap.getIndexColorModel(this) : null;
}
@Override
public ColorModel colorMapForRow(final IndexColorModel colorModel, final int row) {
return multiPalette != null ? multiPalette.getColorModel(colorModel, row, isLaced()) : null;
}
@Override
long bodyOffset() {
return body.chunkOffset;
}
@Override
long bodyLength() {
return body.chunkLength;
}
@Override
ILBMForm with(final IFFChunk chunk) throws IIOException {
if (chunk instanceof BMHDChunk) {
if (bitmapHeader != null) {
throw new IIOException("Multiple BMHD chunks not allowed");
}
return new ILBMForm(formType, (BMHDChunk) chunk, null, colorMap, multiPalette, body);
}
else if (chunk instanceof CAMGChunk) {
if (viewMode != null) {
throw new IIOException("Multiple CAMG chunks not allowed");
}
return new ILBMForm(formType, bitmapHeader, (CAMGChunk) chunk, colorMap, multiPalette, body);
}
else if (chunk instanceof CMAPChunk) {
if (colorMap != null) {
throw new IIOException("Multiple CMAP chunks not allowed");
}
return new ILBMForm(formType, bitmapHeader, viewMode, (CMAPChunk) chunk, multiPalette, body);
}
else if (chunk instanceof AbstractMultiPaletteChunk) {
// NOTE: We prefer PHCG over SHAM/CTBL style palette changes, if both are present
if (multiPalette instanceof PCHGChunk) {
if (chunk instanceof PCHGChunk) {
throw new IIOException("Multiple PCHG/SHAM/CTBL chunks not allowed");
}
return this;
}
return new ILBMForm(formType, bitmapHeader, viewMode, colorMap, (AbstractMultiPaletteChunk) chunk, body);
}
else if (chunk instanceof BODYChunk) {
if (body != null) {
throw new IIOException("Multiple " + toChunkStr(chunk.chunkId) + " chunks not allowed");
}
return new ILBMForm(formType, bitmapHeader, viewMode, colorMap, multiPalette, (BODYChunk) chunk);
}
else if (chunk instanceof GRABChunk) {
// Ignored for now
return this;
}
return (ILBMForm) super.with(chunk);
}
@Override
public String toString() {
return super.toString() + '{' + bitmapHeader +
(viewMode != null ? ", " + viewMode : "" ) +
(colorMap != null ? ", " + colorMap : "" ) +
(multiPalette != null ? ", " + multiPalette : "" ) +
'}';
}
}
/**
* The set of chunks used in DEEP and TVPP FORMs.
*/
private static final class DEEPForm extends Form {
private final DGBLChunk deepGlobal;
private final DLOCChunk deepLocation;
private final DPELChunk deepPixel;
private final BODYChunk body;
DEEPForm(int formType) {
this(formType, null, null, null, null);
}
private DEEPForm(final int formType, final DGBLChunk deepGlobal, final DLOCChunk deepLocation, final DPELChunk deepPixel, final BODYChunk body) {
super(formType);
this.deepGlobal = deepGlobal;
this.deepLocation = deepLocation;
this.deepPixel = deepPixel;
this.body = body;
}
@Override
int width() {
return deepLocation.width;
}
@Override
int height() {
return deepLocation.height;
}
@Override
int bitplanes() {
return deepPixel.bitsPerPixel();
}
@Override
public int sampleSize() {
return bitplanes() / 8;
}
@Override
public boolean premultiplied() {
return true;
}
@Override
int compressionType() {
return deepGlobal.compressionType;
}
@Override
int xAspect() {
return deepGlobal.xAspect;
}
@Override
int yAspect() {
return deepGlobal.yAspect;
}
@Override
long bodyOffset() {
return body.chunkOffset;
}
@Override
long bodyLength() {
return body.chunkLength;
}
@Override
DEEPForm with(final IFFChunk chunk) throws IIOException {
if (chunk instanceof DGBLChunk) {
if (deepGlobal != null) {
throw new IIOException("Multiple DGBL chunks not allowed");
}
return new DEEPForm(formType, (DGBLChunk) chunk, null, null, body);
}
else if (chunk instanceof DLOCChunk) {
if (deepLocation != null) {
throw new IIOException("Multiple DLOC chunks not allowed");
}
return new DEEPForm(formType, deepGlobal, (DLOCChunk) chunk, deepPixel, body);
}
else if (chunk instanceof DPELChunk) {
if (deepPixel != null) {
throw new IIOException("Multiple DPEL chunks not allowed");
}
return new DEEPForm(formType, deepGlobal, deepLocation, (DPELChunk) chunk, body);
}
else if (chunk instanceof BODYChunk) {
if (body != null) {
throw new IIOException("Multiple " + toChunkStr(chunk.chunkId) + " chunks not allowed");
}
return new DEEPForm(formType, deepGlobal, deepLocation, deepPixel, (BODYChunk) chunk);
}
return (DEEPForm) super.with(chunk);
}
@Override
public String toString() {
return super.toString() + '{' + deepGlobal + ", " + deepLocation + ", " + deepPixel + '}';
}
}
}
@@ -50,25 +50,25 @@ final class GRABChunk extends IFFChunk {
Point2D point; Point2D point;
GRABChunk(int pChunkLength) { GRABChunk(int chunkLength) {
super(IFF.CHUNK_GRAB, pChunkLength); super(IFF.CHUNK_GRAB, chunkLength);
} }
GRABChunk(Point2D pPoint) { GRABChunk(Point2D point) {
super(IFF.CHUNK_GRAB, 4); super(IFF.CHUNK_GRAB, 4);
point = pPoint; this.point = point;
} }
void readChunk(DataInput pInput) throws IOException { void readChunk(DataInput input) throws IOException {
if (chunkLength != 4) { if (chunkLength != 4) {
throw new IIOException("Unknown GRAB chunk size: " + chunkLength); throw new IIOException("Unknown GRAB chunk size: " + chunkLength);
} }
point = new Point(pInput.readShort(), pInput.readShort()); point = new Point(input.readShort(), input.readShort());
} }
void writeChunk(DataOutput pOutput) throws IOException { void writeChunk(DataOutput output) throws IOException {
pOutput.writeShort((int) point.getX()); output.writeShort((int) point.getX());
pOutput.writeShort((int) point.getY()); output.writeShort((int) point.getY());
} }
public String toString() { public String toString() {
@@ -44,31 +44,31 @@ final class GenericChunk extends IFFChunk {
byte[] data; byte[] data;
GenericChunk(int pChunkId, int pChunkLength) { GenericChunk(int chunkId, int chunkLength) {
super(pChunkId, pChunkLength); super(chunkId, chunkLength);
data = new byte[chunkLength]; data = new byte[this.chunkLength];
} }
GenericChunk(int pChunkId, byte[] pChunkData) { GenericChunk(int chunkId, byte[] chunkData) {
super(pChunkId, pChunkData.length); super(chunkId, chunkData.length);
data = pChunkData; data = chunkData;
} }
@Override @Override
void readChunk(final DataInput pInput) throws IOException { void readChunk(final DataInput input) throws IOException {
pInput.readFully(data, 0, data.length); input.readFully(data, 0, data.length);
skipData(pInput, chunkLength, data.length); skipData(input, chunkLength, data.length);
} }
@Override @Override
void writeChunk(final DataOutput pOutput) throws IOException { void writeChunk(final DataOutput output) throws IOException {
pOutput.writeInt(chunkId); output.writeInt(chunkId);
pOutput.writeInt(chunkLength); output.writeInt(chunkLength);
pOutput.write(data, 0, data.length); output.write(data, 0, data.length);
if (data.length % 2 != 0) { if (data.length % 2 != 0) {
pOutput.writeByte(0); // PAD output.writeByte(0); // PAD
} }
} }
@@ -49,6 +49,8 @@ interface IFF {
// TODO: // TODO:
/** IFF DEEP form type (TVPaint) */ /** IFF DEEP form type (TVPaint) */
int TYPE_DEEP = ('D' << 24) + ('E' << 16) + ('E' << 8) + 'P'; int TYPE_DEEP = ('D' << 24) + ('E' << 16) + ('E' << 8) + 'P';
/** IFF TVPP form type (TVPaint Project) */
int TYPE_TVPP = ('T' << 24) + ('V' << 16) + ('P' << 8) + 'P';
/** IFF RGB8 form type (TurboSilver) */ /** IFF RGB8 form type (TurboSilver) */
int TYPE_RGB8 = ('R' << 24) + ('G' << 16) + ('B' << 8) + '8'; int TYPE_RGB8 = ('R' << 24) + ('G' << 16) + ('B' << 8) + '8';
/** IFF RGBN form type (TurboSilver) */ /** IFF RGBN form type (TurboSilver) */
@@ -92,7 +94,7 @@ interface IFF {
int CHUNK_COPY = ('(' << 24) + ('c' << 16) + (')' << 8) + ' '; int CHUNK_COPY = ('(' << 24) + ('c' << 16) + (')' << 8) + ' ';
/** EA IFF 85 Generic annotation chunk (usually used for Software) */ /** EA IFF 85 Generic annotation chunk (usually used for Software) */
int CHUNK_ANNO = ('A' << 24) + ('N' << 16) + ('N' << 8) + 'O';; int CHUNK_ANNO = ('A' << 24) + ('N' << 16) + ('N' << 8) + 'O';
/** Third-party defined UTF-8 text. */ /** Third-party defined UTF-8 text. */
int CHUNK_UTF8 = ('U' << 24) + ('T' << 16) + ('F' << 8) + '8'; int CHUNK_UTF8 = ('U' << 24) + ('T' << 16) + ('F' << 8) + '8';
@@ -129,6 +131,16 @@ interface IFF {
int CHUNK_SHAM = ('S' << 24) + ('H' << 16) + ('A' << 8) + 'M'; int CHUNK_SHAM = ('S' << 24) + ('H' << 16) + ('A' << 8) + 'M';
/** ACBM body chunk */ /** ACBM body chunk */
int CHUNK_ABIT = ('A' << 24) + ('B' << 16) + ('I' << 8) + 'T'; int CHUNK_ABIT = ('A' << 24) + ('B' << 16) + ('I' << 8) + 'T';
/** unofficial direct color */ /** Unofficial direct color */
int CHUNK_DCOL = ('D' << 24) + ('C' << 16) + ('O' << 8) + 'L'; int CHUNK_DCOL = ('D' << 24) + ('C' << 16) + ('O' << 8) + 'L';
/** TVPaint Deep GloBaL information */
int CHUNK_DGBL = ('D' << 24) + ('G' << 16) + ('B' << 8) + 'L';
/** TVPaint Deep Pixel ELements */
int CHUNK_DPEL = ('D' << 24) + ('P' << 16) + ('E' << 8) + 'L';
/** TVPaint Deep LOCation information */
int CHUNK_DLOC = ('D' << 24) + ('L' << 16) + ('O' << 8) + 'C';
/** TVPaint Deep BODy */
int CHUNK_DBOD = ('D' << 24) + ('B' << 16) + ('O' << 8) + 'D';
/** TVPaint Deep CHanGe buffer */
int CHUNK_DCHG = ('D' << 24) + ('C' << 16) + ('H' << 8) + 'G';
} }
@@ -44,25 +44,25 @@ abstract class IFFChunk {
int chunkId; int chunkId;
int chunkLength; int chunkLength;
protected IFFChunk(int pChunkId, int pChunkLength) { protected IFFChunk(int chunkId, int chunkLength) {
chunkId = pChunkId; this.chunkId = chunkId;
chunkLength = pChunkLength; this.chunkLength = chunkLength;
} }
abstract void readChunk(DataInput pInput) throws IOException; abstract void readChunk(DataInput input) throws IOException;
abstract void writeChunk(DataOutput pOutput) throws IOException; abstract void writeChunk(DataOutput output) throws IOException;
protected static void skipData(final DataInput pInput, final int chunkLength, final int dataReadSoFar) throws IOException { protected static void skipData(final DataInput input, final int chunkLength, final int dataReadSoFar) throws IOException {
int toSkip = chunkLength - dataReadSoFar; int toSkip = chunkLength - dataReadSoFar;
while (toSkip > 0) { while (toSkip > 0) {
toSkip -= pInput.skipBytes(toSkip); toSkip -= input.skipBytes(toSkip);
} }
// Read pad // Read pad
if (chunkLength % 2 != 0) { if (chunkLength % 2 != 0) {
pInput.readByte(); input.readByte();
} }
} }
@@ -13,31 +13,29 @@ import static com.twelvemonkeys.lang.Validate.isTrue;
import static com.twelvemonkeys.lang.Validate.notNull; import static com.twelvemonkeys.lang.Validate.notNull;
final class IFFImageMetadata extends AbstractMetadata { final class IFFImageMetadata extends AbstractMetadata {
private final int formType; private final Form header;
private final BMHDChunk header;
private final IndexColorModel colorMap; private final IndexColorModel colorMap;
private final CAMGChunk viewPort;
private final List<GenericChunk> meta; private final List<GenericChunk> meta;
IFFImageMetadata(int formType, BMHDChunk header, IndexColorModel colorMap, CAMGChunk viewPort, List<GenericChunk> meta) { IFFImageMetadata(Form header, IndexColorModel colorMap) {
this.formType = isTrue(validFormType(formType), formType, "Unknown IFF Form type: %s");
this.header = notNull(header, "header"); this.header = notNull(header, "header");
isTrue(validFormType(header.formType), header.formType, "Unknown IFF Form type: %s");
this.colorMap = colorMap; this.colorMap = colorMap;
this.viewPort = viewPort; this.meta = header.meta;
this.meta = meta;
} }
private boolean validFormType(int formType) { private boolean validFormType(int formType) {
switch (formType) { switch (formType) {
default:
return false;
case TYPE_ACBM: case TYPE_ACBM:
case TYPE_DEEP: case TYPE_DEEP:
case TYPE_ILBM: case TYPE_ILBM:
case TYPE_PBM: case TYPE_PBM:
case TYPE_RGB8: case TYPE_RGB8:
case TYPE_RGBN: case TYPE_RGBN:
case TYPE_TVPP:
return true; return true;
default:
return false;
} }
} }
@@ -48,7 +46,7 @@ final class IFFImageMetadata extends AbstractMetadata {
IIOMetadataNode csType = new IIOMetadataNode("ColorSpaceType"); IIOMetadataNode csType = new IIOMetadataNode("ColorSpaceType");
chroma.appendChild(csType); chroma.appendChild(csType);
switch (header.bitplanes) { switch (header.bitplanes()) {
case 8: case 8:
if (colorMap == null) { if (colorMap == null) {
csType.setAttribute("name", "GRAY"); csType.setAttribute("name", "GRAY");
@@ -73,10 +71,10 @@ final class IFFImageMetadata extends AbstractMetadata {
// NOTE: 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"); IIOMetadataNode numChannels = new IIOMetadataNode("NumChannels");
chroma.appendChild(numChannels); chroma.appendChild(numChannels);
if (colorMap == null && header.bitplanes == 8) { if (colorMap == null && header.bitplanes() == 8) {
numChannels.setAttribute("value", Integer.toString(1)); numChannels.setAttribute("value", Integer.toString(1));
} }
else if (header.bitplanes == 32) { else if (header.bitplanes() == 25 || header.bitplanes() == 32) {
numChannels.setAttribute("value", Integer.toString(4)); numChannels.setAttribute("value", Integer.toString(4));
} }
else { else {
@@ -103,9 +101,16 @@ final class IFFImageMetadata extends AbstractMetadata {
paletteEntry.setAttribute("green", Integer.toString(colorMap.getGreen(i))); paletteEntry.setAttribute("green", Integer.toString(colorMap.getGreen(i)));
paletteEntry.setAttribute("blue", Integer.toString(colorMap.getBlue(i))); paletteEntry.setAttribute("blue", Integer.toString(colorMap.getBlue(i)));
} }
if (colorMap.getTransparentPixel() != -1) {
IIOMetadataNode backgroundIndex = new IIOMetadataNode("BackgroundIndex");
chroma.appendChild(backgroundIndex);
backgroundIndex.setAttribute("value", Integer.toString(colorMap.getTransparentPixel()));
}
} }
// TODO: Background color is the color of the transparent index in the color model? // TODO: TVPP TVPaint Project files have a MIXR chunk with a background color
// and also a BGP1 (background pen 1?) and BGP2 chunks
// if (extensions != null && extensions.getBackgroundColor() != 0) { // if (extensions != null && extensions.getBackgroundColor() != 0) {
// Color background = new Color(extensions.getBackgroundColor(), true); // Color background = new Color(extensions.getBackgroundColor(), true);
// //
@@ -122,7 +127,7 @@ final class IFFImageMetadata extends AbstractMetadata {
@Override @Override
protected IIOMetadataNode getStandardCompressionNode() { protected IIOMetadataNode getStandardCompressionNode() {
if (header.compressionType == BMHDChunk.COMPRESSION_NONE) { if (header.compressionType() == BMHDChunk.COMPRESSION_NONE) {
return null; // All defaults return null; // All defaults
} }
@@ -145,7 +150,9 @@ final class IFFImageMetadata extends AbstractMetadata {
// PlanarConfiguration // PlanarConfiguration
IIOMetadataNode planarConfiguration = new IIOMetadataNode("PlanarConfiguration"); IIOMetadataNode planarConfiguration = new IIOMetadataNode("PlanarConfiguration");
switch (formType) { switch (header.formType) {
case TYPE_DEEP:
case TYPE_TVPP:
case TYPE_RGB8: case TYPE_RGB8:
case TYPE_PBM: case TYPE_PBM:
planarConfiguration.setAttribute("value", "PixelInterleaved"); planarConfiguration.setAttribute("value", "PixelInterleaved");
@@ -154,7 +161,7 @@ final class IFFImageMetadata extends AbstractMetadata {
planarConfiguration.setAttribute("value", "PlaneInterleaved"); planarConfiguration.setAttribute("value", "PlaneInterleaved");
break; break;
default: default:
planarConfiguration.setAttribute("value", "Unknown " + IFFUtil.toChunkStr(formType)); planarConfiguration.setAttribute("value", "Unknown " + IFFUtil.toChunkStr(header.formType));
break; break;
} }
data.appendChild(planarConfiguration); data.appendChild(planarConfiguration);
@@ -165,7 +172,7 @@ final class IFFImageMetadata extends AbstractMetadata {
// BitsPerSample // BitsPerSample
IIOMetadataNode bitsPerSample = new IIOMetadataNode("BitsPerSample"); IIOMetadataNode bitsPerSample = new IIOMetadataNode("BitsPerSample");
String value = bitsPerSampleValue(header.bitplanes); String value = bitsPerSampleValue(header.bitplanes());
bitsPerSample.setAttribute("value", value); bitsPerSample.setAttribute("value", value);
data.appendChild(bitsPerSample); data.appendChild(bitsPerSample);
@@ -173,7 +180,6 @@ final class IFFImageMetadata extends AbstractMetadata {
// SampleMSB not in format // SampleMSB not in format
return data; return data;
} }
private String bitsPerSampleValue(int bitplanes) { private String bitsPerSampleValue(int bitplanes) {
@@ -190,8 +196,8 @@ final class IFFImageMetadata extends AbstractMetadata {
case 24: case 24:
return "8 8 8"; return "8 8 8";
case 25: case 25:
if (formType != TYPE_RGB8) { if (header.formType != TYPE_RGB8) {
throw new IllegalArgumentException(String.format("25 bit depth only supported for FORM type RGB8: %s", IFFUtil.toChunkStr(formType))); throw new IllegalArgumentException(String.format("25 bit depth only supported for FORM type RGB8: %s", IFFUtil.toChunkStr(header.formType)));
} }
return "8 8 8 1"; return "8 8 8 1";
@@ -204,7 +210,7 @@ final class IFFImageMetadata extends AbstractMetadata {
@Override @Override
protected IIOMetadataNode getStandardDimensionNode() { protected IIOMetadataNode getStandardDimensionNode() {
if (viewPort == null) { if (header.xAspect() == 0 || header.yAspect() == 0) {
return null; return null;
} }
@@ -212,7 +218,7 @@ final class IFFImageMetadata extends AbstractMetadata {
// PixelAspectRatio // PixelAspectRatio
IIOMetadataNode pixelAspectRatio = new IIOMetadataNode("PixelAspectRatio"); IIOMetadataNode pixelAspectRatio = new IIOMetadataNode("PixelAspectRatio");
pixelAspectRatio.setAttribute("value", String.valueOf((viewPort.isHires() ? 2f : 1f) / (viewPort.isLaced() ? 2f : 1f))); pixelAspectRatio.setAttribute("value", String.valueOf(header.xAspect() / (float) header.yAspect()));
dimension.appendChild(pixelAspectRatio); dimension.appendChild(pixelAspectRatio);
// TODO: HorizontalScreenSize? // TODO: HorizontalScreenSize?
@@ -254,16 +260,15 @@ final class IFFImageMetadata extends AbstractMetadata {
@Override @Override
protected IIOMetadataNode getStandardTransparencyNode() { protected IIOMetadataNode getStandardTransparencyNode() {
// TODO: Make sure 25 bit is only RGB8... if ((colorMap == null || !colorMap.hasAlpha()) && header.bitplanes() != 32 && header.bitplanes() != 25) {
if ((colorMap == null || !colorMap.hasAlpha()) && header.bitplanes != 32 && header.bitplanes != 25) {
return null; return null;
} }
IIOMetadataNode transparency = new IIOMetadataNode("Transparency"); IIOMetadataNode transparency = new IIOMetadataNode("Transparency");
if (header.bitplanes == 25 || header.bitplanes == 32) { if (header.bitplanes() == 25 || header.bitplanes() == 32) {
IIOMetadataNode alpha = new IIOMetadataNode("Alpha"); IIOMetadataNode alpha = new IIOMetadataNode("Alpha");
alpha.setAttribute("value", "nonpremultiplied"); alpha.setAttribute("value", header.premultiplied() ? "premultiplied" : "nonpremultiplied");
transparency.appendChild(alpha); transparency.appendChild(alpha);
} }
@@ -48,14 +48,15 @@ import java.awt.image.*;
import java.io.DataInputStream; import java.io.DataInputStream;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import static com.twelvemonkeys.imageio.plugins.iff.IFFUtil.toChunkStr;
/** /**
* Reader for Commodore Amiga (Electronic Arts) IFF ILBM (InterLeaved BitMap) and PBM * Reader for Commodore Amiga (Electronic Arts) IFF ILBM (InterLeaved BitMap) and PBM
* format (Packed BitMap). * format (Packed BitMap). Also supports IFF RGB8 (Impulse) and IFF DEEP (TVPaint).
* The IFF format (Interchange File Format) is the standard file format * The IFF format (Interchange File Format) is the standard file format
* supported by allmost all image software for the Amiga computer. * supported by allmost all image software for the Amiga computer.
* <p> * <p>
@@ -104,20 +105,12 @@ public final class IFFImageReader extends ImageReaderBase {
// - Contains definitions of some "new" chunks, as well as alternative FORM types // - Contains definitions of some "new" chunks, as well as alternative FORM types
// http://amigan.1emu.net/index/iff.html // http://amigan.1emu.net/index/iff.html
// TODO: XS24 chunk seems to be a raw 24 bit thumbnail for TVPaint images: XS24 <4 byte len> <2 byte width> <2 byte height> <pixel data...>
// TODO: Allow reading rasters for HAM6/HAM8 and multipalette images that are expanded to RGB (24 bit) during read. // TODO: Allow reading rasters for HAM6/HAM8 and multipalette images that are expanded to RGB (24 bit) during read.
private BMHDChunk header; final static boolean DEBUG = "true".equalsIgnoreCase(System.getProperty("com.twelvemonkeys.imageio.plugins.iff.debug"));
private CMAPChunk colorMap;
private BODYChunk body;
@SuppressWarnings({"FieldCanBeLocal"})
private GRABChunk grab;
private CAMGChunk viewPort;
private MultiPalette paletteChange;
private final List<GenericChunk> meta = new ArrayList<>();
private int formType;
private long bodyStart;
private BufferedImage image; private Form header;
private DataInputStream byteRunStream; private DataInputStream byteRunStream;
IFFImageReader(ImageReaderSpi pProvider) { IFFImageReader(ImageReaderSpi pProvider) {
@@ -135,35 +128,30 @@ public final class IFFImageReader extends ImageReaderBase {
@Override @Override
protected void resetMembers() { protected void resetMembers() {
header = null; header = null;
colorMap = null;
paletteChange = null;
body = null;
viewPort = null;
formType = 0;
meta.clear();
image = null;
byteRunStream = null; byteRunStream = null;
} }
private void readMeta() throws IOException { private void readMeta() throws IOException {
int chunkType = imageInput.readInt(); int chunkType = imageInput.readInt();
if (chunkType != IFF.CHUNK_FORM) { if (chunkType != IFF.CHUNK_FORM) {
throw new IIOException(String.format("Unknown file format for IFFImageReader, expected 'FORM': %s", IFFUtil.toChunkStr(chunkType))); throw new IIOException(String.format("Unknown file format for IFFImageReader, expected 'FORM': %s", toChunkStr(chunkType)));
} }
int remaining = imageInput.readInt() - 4; // We'll read 4 more in a sec int remaining = imageInput.readInt() - 4; // We'll read 4 more in a sec
formType = imageInput.readInt(); int formType = imageInput.readInt();
if (formType != IFF.TYPE_ILBM && formType != IFF.TYPE_PBM && formType != IFF.TYPE_RGB8 && formType != IFF.TYPE_DEEP) { if (formType != IFF.TYPE_ILBM && formType != IFF.TYPE_PBM && formType != IFF.TYPE_RGB8 && formType != IFF.TYPE_DEEP && formType != IFF.TYPE_TVPP) {
throw new IIOException(String.format("Only IFF FORM types 'ILBM' and 'PBM ' supported: %s", IFFUtil.toChunkStr(formType))); throw new IIOException(String.format("Only IFF FORM types 'ILBM' and 'PBM ' supported: %s", toChunkStr(formType)));
} }
//System.out.println("IFF type FORM " + toChunkStr(type)); if (DEBUG) {
System.out.println("IFF type FORM '" + toChunkStr(formType) + "', len: " + (remaining + 4));
System.out.println("Reading Chunks...");
}
grab = null; header = Form.ofType(formType);
viewPort = null;
// TODO: Delegate the FORM reading to the Form class or a FormReader class?
while (remaining > 0) { while (remaining > 0) {
int chunkId = imageInput.readInt(); int chunkId = imageInput.readInt();
int length = imageInput.readInt(); int length = imageInput.readInt();
@@ -171,104 +159,82 @@ public final class IFFImageReader extends ImageReaderBase {
remaining -= 8; remaining -= 8;
remaining -= length % 2 == 0 ? length : length + 1; remaining -= length % 2 == 0 ? length : length + 1;
//System.out.println("Next chunk: " + toChunkStr(chunkId) + " length: " + length); if (DEBUG) {
//System.out.println("Remaining bytes after chunk: " + remaining); System.out.println("Next chunk: " + toChunkStr(chunkId) + " @ pos: " + (imageInput.getStreamPosition() - 8) + ", len: " + length);
System.out.println("Remaining bytes after chunk: " + remaining);
}
switch (chunkId) { switch (chunkId) {
case IFF.CHUNK_BMHD: case IFF.CHUNK_BMHD:
if (header != null) { BMHDChunk bitmapHeader = new BMHDChunk(length);
throw new IIOException("Multiple BMHD chunks not allowed"); bitmapHeader.readChunk(imageInput);
} header = header.with(bitmapHeader);
header = new BMHDChunk(length);
header.readChunk(imageInput);
//System.out.println(header);
break; break;
case IFF.CHUNK_DGBL:
DGBLChunk deepGlobal = new DGBLChunk(length);
deepGlobal.readChunk(imageInput);
header = header.with(deepGlobal);
break;
case IFF.CHUNK_DLOC:
DLOCChunk deepLocation = new DLOCChunk(length);
deepLocation.readChunk(imageInput);
header = header.with(deepLocation);
break;
case IFF.CHUNK_DPEL:
DPELChunk deepPixel = new DPELChunk(length);
deepPixel.readChunk(imageInput);
header = header.with(deepPixel);
break;
case IFF.CHUNK_CMAP: case IFF.CHUNK_CMAP:
if (colorMap != null) { CMAPChunk colorMap = new CMAPChunk(length);
throw new IIOException("Multiple CMAP chunks not allowed");
}
colorMap = new CMAPChunk(length);
colorMap.readChunk(imageInput); colorMap.readChunk(imageInput);
header = header.with(colorMap);
//System.out.println(colorMap);
break; break;
case IFF.CHUNK_GRAB: case IFF.CHUNK_GRAB:
if (grab != null) { GRABChunk grab = new GRABChunk(length);
throw new IIOException("Multiple GRAB chunks not allowed");
}
grab = new GRABChunk(length);
grab.readChunk(imageInput); grab.readChunk(imageInput);
header = header.with(grab);
//System.out.println(grab);
break; break;
case IFF.CHUNK_CAMG: case IFF.CHUNK_CAMG:
if (viewPort != null) { CAMGChunk viewMode = new CAMGChunk(length);
throw new IIOException("Multiple CAMG chunks not allowed"); viewMode.readChunk(imageInput);
} header = header.with(viewMode);
viewPort = new CAMGChunk(length);
viewPort.readChunk(imageInput);
// System.out.println(viewPort);
break; break;
case IFF.CHUNK_PCHG:
if (paletteChange instanceof PCHGChunk) {
throw new IIOException("Multiple PCHG chunks not allowed");
}
case IFF.CHUNK_PCHG:
PCHGChunk pchg = new PCHGChunk(length); PCHGChunk pchg = new PCHGChunk(length);
pchg.readChunk(imageInput); pchg.readChunk(imageInput);
header = header.with(pchg);
// Always prefer PCHG style palette changes
paletteChange = pchg;
// System.out.println(pchg);
break; break;
case IFF.CHUNK_SHAM: case IFF.CHUNK_SHAM:
if (paletteChange instanceof SHAMChunk) {
throw new IIOException("Multiple SHAM chunks not allowed");
}
SHAMChunk sham = new SHAMChunk(length); SHAMChunk sham = new SHAMChunk(length);
sham.readChunk(imageInput); sham.readChunk(imageInput);
header = header.with(sham);
// NOTE: We prefer PHCG to SHAM style palette changes, if both are present
if (paletteChange == null) {
paletteChange = sham;
}
// System.out.println(sham);
break; break;
case IFF.CHUNK_CTBL: case IFF.CHUNK_CTBL:
if (paletteChange instanceof CTBLChunk) {
throw new IIOException("Multiple CTBL chunks not allowed");
}
CTBLChunk ctbl = new CTBLChunk(length); CTBLChunk ctbl = new CTBLChunk(length);
ctbl.readChunk(imageInput); ctbl.readChunk(imageInput);
header = header.with(ctbl);
// NOTE: We prefer PHCG to CTBL style palette changes, if both are present
if (paletteChange == null) {
paletteChange = ctbl;
}
// System.out.println(ctbl);
break; break;
case IFF.CHUNK_BODY: case IFF.CHUNK_BODY:
if (body != null) { case IFF.CHUNK_DBOD:
throw new IIOException("Multiple BODY chunks not allowed"); BODYChunk body = new BODYChunk(chunkId, length, imageInput.getStreamPosition());
}
body = new BODYChunk(length);
bodyStart = imageInput.getStreamPosition();
// NOTE: We don't read the body here, it's done later in the read(int, ImageReadParam) method // NOTE: We don't read the body here, it's done later in the read(int, ImageReadParam) method
header = header.with(body);
// Done reading meta // Done reading meta
if (DEBUG) {
System.out.println("header = " + header);
}
return; return;
case IFF.CHUNK_ANNO: case IFF.CHUNK_ANNO:
@@ -279,45 +245,32 @@ public final class IFFImageReader extends ImageReaderBase {
case IFF.CHUNK_UTF8: case IFF.CHUNK_UTF8:
GenericChunk generic = new GenericChunk(chunkId, length); GenericChunk generic = new GenericChunk(chunkId, length);
generic.readChunk(imageInput); generic.readChunk(imageInput);
meta.add(generic); header = header.with(generic);
// System.out.println(generic);
break; break;
case IFF.CHUNK_JUNK: case IFF.CHUNK_JUNK:
// Always skip junk chunks // Always skip junk chunks
default: default:
// TODO: SHAM, DEST, SPRT and more // TODO: DEST, SPRT and more
// Everything else, we'll just skip // Everything else, we'll just skip
IFFChunk.skipData(imageInput, length, 0); IFFChunk.skipData(imageInput, length, 0);
break; break;
} }
} }
if (DEBUG) {
System.out.println("header = " + header);
System.out.println("No BODY chunk found...");
}
} }
@Override @Override
public BufferedImage read(int pIndex, ImageReadParam pParam) throws IOException { public BufferedImage read(int imageIndex, ImageReadParam param) throws IOException {
init(pIndex); init(imageIndex);
processImageStarted(imageIndex);
processImageStarted(pIndex); BufferedImage result = getDestination(param, getImageTypes(imageIndex), getWidth(imageIndex), getHeight(imageIndex));
readBody(param, result);
image = getDestination(pParam, getImageTypes(pIndex), header.width, header.height);
//System.out.println(body);
if (body != null) {
//System.out.println("Read body");
readBody(pParam);
}
else {
// TODO: Remove this hack when we have metadata
// In the rare case of an ILBM containing nothing but a CMAP
//System.out.println(colorMap);
if (colorMap != null) {
//System.out.println("Creating palette!");
image = colorMap.createPaletteImage(header, isEHB());
}
}
BufferedImage result = image;
processImageComplete(); processImageComplete();
@@ -325,77 +278,81 @@ public final class IFFImageReader extends ImageReaderBase {
} }
@Override @Override
public int getWidth(int pIndex) throws IOException { public int getWidth(int imageIndex) throws IOException {
init(pIndex); init(imageIndex);
return header.width; return header.width();
} }
@Override @Override
public int getHeight(int pIndex) throws IOException { public int getHeight(int imageIndex) throws IOException {
init(pIndex); init(imageIndex);
return header.height; return header.height();
} }
@Override @Override
public IIOMetadata getImageMetadata(int imageIndex) throws IOException { public IIOMetadata getImageMetadata(int imageIndex) throws IOException {
init(imageIndex); init(imageIndex);
return new IFFImageMetadata(formType, header, colorMap != null ? colorMap.getIndexColorModel(header, isEHB()) : null, viewPort, meta); return new IFFImageMetadata(header, header.colorMap());
} }
@Override @Override
public Iterator<ImageTypeSpecifier> getImageTypes(int pIndex) throws IOException { public Iterator<ImageTypeSpecifier> getImageTypes(int imageIndex) throws IOException {
init(pIndex); init(imageIndex);
List<ImageTypeSpecifier> types = Arrays.asList( int bitplanes = header.bitplanes();
getRawImageType(pIndex), List<ImageTypeSpecifier> types =
ImageTypeSpecifiers.createFromBufferedImageType(header.bitplanes == 32 ? BufferedImage.TYPE_4BYTE_ABGR : BufferedImage.TYPE_3BYTE_BGR) header.formType == IFF.TYPE_DEEP || header.formType == IFF.TYPE_TVPP // TODO: Make a header attribute here
// TODO: ImageTypeSpecifier.createFromBufferedImageType(header.bitplanes == 32 ? BufferedImage.TYPE_INT_ARGB : BufferedImage.TYPE_INT_RGB), ? Arrays.asList(
// TODO: Allow 32 bit always. Allow RGB and discard alpha, if present? ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_4BYTE_ABGR_PRE),
); getRawImageType(imageIndex)
)
: Arrays.asList(
getRawImageType(imageIndex),
ImageTypeSpecifiers.createFromBufferedImageType(bitplanes == 32 ? BufferedImage.TYPE_4BYTE_ABGR : BufferedImage.TYPE_3BYTE_BGR)
);
// TODO: Allow 32 bit INT types?
return types.iterator(); return types.iterator();
} }
@Override @Override
public ImageTypeSpecifier getRawImageType(int pIndex) throws IOException { public ImageTypeSpecifier getRawImageType(int pIndex) throws IOException {
init(pIndex); init(pIndex);
// TODO: Stay DRY...
// TODO: Use this for creating the Image/Buffer in the read code below...
// NOTE: colorMap may be null for 8 bit (gray), 24 bit or 32 bit only // NOTE: colorMap may be null for 8 bit (gray), 24 bit or 32 bit only
ImageTypeSpecifier specifier; switch (header.bitplanes()) {
switch (header.bitplanes) {
case 1: case 1:
// 1 bit // -> 1 bit IndexColorModel
case 2: case 2:
// 2 bit // -> 2 bit IndexColorModel
case 3: case 3:
case 4: case 4:
// 4 bit // -> 4 bit IndexColorModel
case 5: case 5:
case 6: case 6:
// May be HAM6 // May be EHB or HAM6
// May be EHB
case 7: case 7:
case 8: case 8:
// 8 bit
// May be HAM8 // May be HAM8
if (!isConvertToRGB()) { // otherwise -> 8 bit IndexColorModel
if (colorMap != null) { if (!needsConversionToRGB()) {
IndexColorModel cm = colorMap.getIndexColorModel(header, isEHB()); IndexColorModel indexColorModel = header.colorMap();
return ImageTypeSpecifiers.createFromIndexColorModel(cm);
} if (indexColorModel != null) {
else { return ImageTypeSpecifiers.createFromIndexColorModel(indexColorModel);
return ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_BYTE_GRAY);
} }
return ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_BYTE_GRAY);
} }
// NOTE: HAM modes falls through, as they are converted to RGB // NOTE: HAM modes falls through, as they are converted to RGB
case 24: case 24:
// 24 bit RGB // 24 bit RGB
return ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_3BYTE_BGR); return ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_3BYTE_BGR);
case 25: // TYPE_RGB8: 24 bit + 1 bit mask (we'll convert to full alpha during decoding) case 25:
if (formType != IFF.TYPE_RGB8) { // For TYPE_RGB8: 24 bit + 1 bit mask (we'll convert to full alpha during decoding)
throw new IIOException(String.format("25 bit depth only supported for FORM type RGB8: %s", IFFUtil.toChunkStr(formType))); if (header.formType != IFF.TYPE_RGB8) {
throw new IIOException(String.format("25 bit depth only supported for FORM type RGB8: %s", toChunkStr(header.formType)));
} }
return ImageTypeSpecifiers.createInterleaved(ColorSpaces.getColorSpace(ColorSpace.CS_sRGB), return ImageTypeSpecifiers.createInterleaved(ColorSpaces.getColorSpace(ColorSpace.CS_sRGB),
@@ -403,40 +360,49 @@ public final class IFFImageReader extends ImageReaderBase {
case 32: case 32:
// 32 bit ARGB // 32 bit ARGB
return ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_4BYTE_ABGR); return header.formType == IFF.TYPE_DEEP || header.formType == IFF.TYPE_TVPP
// R G B A
? ImageTypeSpecifiers.createInterleaved(ColorSpaces.getColorSpace(ColorSpace.CS_sRGB), new int[] {1, 2, 3, 0}, DataBuffer.TYPE_BYTE, true, header.premultiplied()) // TODO: Create based on DPEL!
: ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_4BYTE_ABGR);
default: default:
throw new IIOException(String.format("Bit depth not implemented: %d", header.bitplanes)); throw new IIOException(String.format("Bit depth not implemented: %d", header.bitplanes()));
} }
} }
private boolean isConvertToRGB() { private boolean needsConversionToRGB() {
return isHAM() || isPCHG() || isSHAM(); return header.isHAM() || header.isMultiPalette();
} }
private void readBody(final ImageReadParam pParam) throws IOException { private void readBody(final ImageReadParam param, final BufferedImage destination) throws IOException {
imageInput.seek(bodyStart); if (DEBUG) {
System.out.println("Reading body");
System.out.println("pos: " + imageInput.getStreamPosition());
System.out.println("body offset: " + header.bodyOffset());
}
imageInput.seek(header.bodyOffset());
byteRunStream = null; byteRunStream = null;
if (formType == IFF.TYPE_RGB8) { if (header.formType == IFF.TYPE_RGB8 || header.formType == IFF.TYPE_DEEP || header.formType == IFF.TYPE_TVPP) {
readRGB8(pParam, imageInput); readChunky(param, destination, imageInput);
} }
else if (colorMap != null) { else if (header.colorMap() != null) {
// NOTE: colorMap may be null for 8 bit (gray), 24 bit or 32 bit only // NOTE: For ILBM types, colorMap may be null for 8 bit (gray), 24 bit or 32 bit only
IndexColorModel cm = colorMap.getIndexColorModel(header, isEHB()); IndexColorModel palette = header.colorMap();
readIndexed(pParam, imageInput, cm); readInterleavedIndexed(param, destination, palette, imageInput);
} }
else { else {
readTrueColor(pParam, imageInput); readInterleaved(param, destination, imageInput);
} }
} }
private void readIndexed(final ImageReadParam pParam, final ImageInputStream pInput, final IndexColorModel pModel) throws IOException { private void readInterleavedIndexed(final ImageReadParam param, final BufferedImage destination, final IndexColorModel palette, final ImageInputStream input) throws IOException {
final int width = header.width; final int width = header.width();
final int height = header.height; final int height = header.height();
final Rectangle aoi = getSourceRegion(pParam, width, height); final Rectangle aoi = getSourceRegion(param, width, height);
final Point offset = pParam == null ? new Point(0, 0) : pParam.getDestinationOffset(); final Point offset = param == null ? new Point(0, 0) : param.getDestinationOffset();
// Set everything to default values // Set everything to default values
int sourceXSubsampling = 1; int sourceXSubsampling = 1;
@@ -445,20 +411,20 @@ public final class IFFImageReader extends ImageReaderBase {
int[] destinationBands = null; int[] destinationBands = null;
// Get values from the ImageReadParam, if any // Get values from the ImageReadParam, if any
if (pParam != null) { if (param != null) {
sourceXSubsampling = pParam.getSourceXSubsampling(); sourceXSubsampling = param.getSourceXSubsampling();
sourceYSubsampling = pParam.getSourceYSubsampling(); sourceYSubsampling = param.getSourceYSubsampling();
sourceBands = pParam.getSourceBands(); sourceBands = param.getSourceBands();
destinationBands = pParam.getDestinationBands(); destinationBands = param.getDestinationBands();
} }
// Ensure band settings from param are compatible with images // Ensure band settings from param are compatible with images
checkReadParamBandSettings(pParam, isConvertToRGB() ? 3 : 1, image.getSampleModel().getNumBands()); checkReadParamBandSettings(param, needsConversionToRGB() ? 3 : 1, destination.getSampleModel().getNumBands());
WritableRaster destination = image.getRaster(); WritableRaster destRaster = destination.getRaster();
if (destinationBands != null || offset.x != 0 || offset.y != 0) { if (destinationBands != null || offset.x != 0 || offset.y != 0) {
destination = destination.createWritableChild(0, 0, destination.getWidth(), destination.getHeight(), offset.x, offset.y, destinationBands); destRaster = destRaster.createWritableChild(0, 0, destRaster.getWidth(), destRaster.getHeight(), offset.x, offset.y, destinationBands);
} }
// NOTE: Each row of the image is stored in an integral number of 16 bit words. // NOTE: Each row of the image is stored in an integral number of 16 bit words.
@@ -467,31 +433,31 @@ public final class IFFImageReader extends ImageReaderBase {
final byte[] planeData = new byte[8 * planeWidth]; final byte[] planeData = new byte[8 * planeWidth];
ColorModel cm; ColorModel cm;
WritableRaster raster; WritableRaster rowRaster;
if (isConvertToRGB()) { if (needsConversionToRGB()) {
// TODO: If HAM6, use type USHORT_444_RGB or 2BYTE_444_RGB? // TODO: Create a HAMColorModel, if at all possible?
// Or create a HAMColorModel, if at all possible?
// TYPE_3BYTE_BGR // TYPE_3BYTE_BGR
cm = new ComponentColorModel( cm = new ComponentColorModel(
ColorSpace.getInstance(ColorSpace.CS_sRGB), new int[]{8, 8, 8}, ColorSpace.getInstance(ColorSpace.CS_sRGB), new int[] {8, 8, 8},
false, false, Transparency.OPAQUE, DataBuffer.TYPE_BYTE false, false, Transparency.OPAQUE, DataBuffer.TYPE_BYTE
); );
// Create a byte raster with BGR order // Create a byte raster with BGR order
raster = Raster.createInterleavedRaster( rowRaster = Raster.createInterleavedRaster(
DataBuffer.TYPE_BYTE, width, 1, width * 3, 3, new int[]{2, 1, 0}, null DataBuffer.TYPE_BYTE, width, 1, width * 3, 3, new int[] {2, 1, 0}, null
); );
} }
else { else {
// TYPE_BYTE_BINARY or TYPE_BYTE_INDEXED // TYPE_BYTE_BINARY or TYPE_BYTE_INDEXED
cm = pModel; cm = palette;
raster = pModel.createCompatibleWritableRaster(width, 1); rowRaster = palette.createCompatibleWritableRaster(width, 1);
} }
Raster sourceRow = raster.createChild(aoi.x, 0, aoi.width, 1, 0, 0, sourceBands);
Raster sourceRow = rowRaster.createChild(aoi.x, 0, aoi.width, 1, 0, 0, sourceBands);
final byte[] row = new byte[width * 8]; final byte[] row = new byte[width * 8];
final byte[] data = ((DataBufferByte) raster.getDataBuffer()).getData(); final byte[] data = ((DataBufferByte) rowRaster.getDataBuffer()).getData();
final int planes = header.bitplanes; final int planes = header.bitplanes();
Object dataElements = null; Object dataElements = null;
Object outDataElements = null; Object outDataElements = null;
@@ -499,7 +465,7 @@ public final class IFFImageReader extends ImageReaderBase {
for (int srcY = 0; srcY < height; srcY++) { for (int srcY = 0; srcY < height; srcY++) {
for (int p = 0; p < planes; p++) { for (int p = 0; p < planes; p++) {
readPlaneData(pInput, planeData, p * planeWidth, planeWidth); readPlaneData(planeData, p * planeWidth, planeWidth, input);
} }
// Skip rows outside AOI // Skip rows outside AOI
@@ -510,72 +476,71 @@ public final class IFFImageReader extends ImageReaderBase {
return; return;
} }
if (formType == IFF.TYPE_ILBM) { if (header.formType == IFF.TYPE_ILBM) {
int pixelPos = 0; int pixelPos = 0;
for (int planePos = 0; planePos < planeWidth; planePos++) { for (int planePos = 0; planePos < planeWidth; planePos++) {
IFFUtil.bitRotateCW(planeData, planePos, planeWidth, row, pixelPos, 1); IFFUtil.bitRotateCW(planeData, planePos, planeWidth, row, pixelPos, 1);
pixelPos += 8; pixelPos += 8;
} }
if (isHAM()) { if (header.isHAM()) {
hamToRGB(row, pModel, data, 0); hamToRGB(row, palette, data, 0);
} }
else if (isConvertToRGB()) { else if (needsConversionToRGB()) {
multiPaletteToRGB(srcY, row, pModel, data, 0); multiPaletteToRGB(srcY, row, palette, data, 0);
} }
else { else {
raster.setDataElements(0, 0, width, 1, row); rowRaster.setDataElements(0, 0, width, 1, row);
} }
} }
else if (formType == IFF.TYPE_PBM) { else if (header.formType == IFF.TYPE_PBM) {
raster.setDataElements(0, 0, width, 1, planeData); rowRaster.setDataElements(0, 0, width, 1, planeData);
} }
else { else {
throw new AssertionError(String.format("Unsupported FORM type: %s", formType)); throw new AssertionError(String.format("Unsupported FORM type: %s", toChunkStr(header.formType)));
} }
int dstY = (srcY - aoi.y) / sourceYSubsampling; int dstY = (srcY - aoi.y) / sourceYSubsampling;
// Handle non-converting raster as special case for performance // Handle non-converting raster as special case for performance
if (cm.isCompatibleRaster(destination)) { if (cm.isCompatibleRaster(destRaster)) {
// Rasters are compatible, just write to destination // Rasters are compatible, just write to destination
if (sourceXSubsampling == 1) { if (sourceXSubsampling == 1) {
destination.setRect(offset.x, dstY, sourceRow); destRaster.setRect(offset.x, dstY, sourceRow);
} }
else { else {
for (int srcX = 0; srcX < sourceRow.getWidth(); srcX += sourceXSubsampling) { for (int srcX = 0; srcX < sourceRow.getWidth(); srcX += sourceXSubsampling) {
dataElements = sourceRow.getDataElements(srcX, 0, dataElements); dataElements = sourceRow.getDataElements(srcX, 0, dataElements);
int dstX = /*offset.x +*/ srcX / sourceXSubsampling; int dstX = /*offset.x +*/ srcX / sourceXSubsampling;
destination.setDataElements(dstX, dstY, dataElements); destRaster.setDataElements(dstX, dstY, dataElements);
} }
} }
} }
else { else {
if (cm instanceof IndexColorModel) { if (cm instanceof IndexColorModel) {
// TODO: Optimize this thing... Maybe it's faster to just get the data indexed, and use drawImage?
IndexColorModel icm = (IndexColorModel) cm; IndexColorModel icm = (IndexColorModel) cm;
for (int srcX = 0; srcX < sourceRow.getWidth(); srcX += sourceXSubsampling) { for (int srcX = 0; srcX < sourceRow.getWidth(); srcX += sourceXSubsampling) {
dataElements = sourceRow.getDataElements(srcX, 0, dataElements); dataElements = sourceRow.getDataElements(srcX, 0, dataElements);
int rgb = icm.getRGB(dataElements); int rgb = icm.getRGB(dataElements);
outDataElements = image.getColorModel().getDataElements(rgb, outDataElements); outDataElements = destination.getColorModel().getDataElements(rgb, outDataElements);
int dstX = srcX / sourceXSubsampling; int dstX = srcX / sourceXSubsampling;
destination.setDataElements(dstX, dstY, outDataElements); destRaster.setDataElements(dstX, dstY, outDataElements);
} }
} }
else { else {
// TODO: This branch is never tested, and is probably "dead" // TODO: This branch is never tested, and is probably "dead"
// ColorConvertOp // ColorConvertOp
if (converter == null) { if (converter == null) {
converter = new ColorConvertOp(cm.getColorSpace(), image.getColorModel().getColorSpace(), null); converter = new ColorConvertOp(cm.getColorSpace(), destination.getColorModel().getColorSpace(), null);
} }
converter.filter( converter.filter(
raster.createChild(aoi.x, 0, aoi.width, 1, 0, 0, null), rowRaster.createChild(aoi.x, 0, aoi.width, 1, 0, 0, null),
destination.createWritableChild(offset.x, offset.y + srcY - aoi.y, aoi.width, 1, 0, 0, null) destRaster.createWritableChild(offset.x, offset.y + srcY - aoi.y, aoi.width, 1, 0, 0, null)
); );
} }
} }
processImageProgress(srcY * 100f / header.width); processImageProgress(srcY * 100f / width);
if (abortRequested()) { if (abortRequested()) {
processReadAborted(); processReadAborted();
break; break;
@@ -583,12 +548,12 @@ public final class IFFImageReader extends ImageReaderBase {
} }
} }
private void readRGB8(ImageReadParam pParam, ImageInputStream pInput) throws IOException { private void readChunky(final ImageReadParam param, final BufferedImage destination, final ImageInputStream input) throws IOException {
final int width = header.width; final int width = header.width();
final int height = header.height; final int height = header.height();
final Rectangle aoi = getSourceRegion(pParam, width, height); final Rectangle aoi = getSourceRegion(param, width, height);
final Point offset = pParam == null ? new Point(0, 0) : pParam.getDestinationOffset(); final Point offset = param == null ? new Point(0, 0) : param.getDestinationOffset();
// Set everything to default values // Set everything to default values
int sourceXSubsampling = 1; int sourceXSubsampling = 1;
@@ -597,50 +562,49 @@ public final class IFFImageReader extends ImageReaderBase {
int[] destinationBands = null; int[] destinationBands = null;
// Get values from the ImageReadParam, if any // Get values from the ImageReadParam, if any
if (pParam != null) { if (param != null) {
sourceXSubsampling = pParam.getSourceXSubsampling(); sourceXSubsampling = param.getSourceXSubsampling();
sourceYSubsampling = pParam.getSourceYSubsampling(); sourceYSubsampling = param.getSourceYSubsampling();
sourceBands = pParam.getSourceBands(); sourceBands = param.getSourceBands();
destinationBands = pParam.getDestinationBands(); destinationBands = param.getDestinationBands();
} }
// Ensure band settings from param are compatible with images // Ensure band settings from param are compatible with images
checkReadParamBandSettings(pParam, 4, image.getSampleModel().getNumBands()); checkReadParamBandSettings(param, 4, destination.getSampleModel().getNumBands());
WritableRaster destination = image.getRaster(); WritableRaster destRaster = destination.getRaster();
if (destinationBands != null || offset.x != 0 || offset.y != 0) { if (destinationBands != null || offset.x != 0 || offset.y != 0) {
destination = destination.createWritableChild(0, 0, destination.getWidth(), destination.getHeight(), offset.x, offset.y, destinationBands); destRaster = destRaster.createWritableChild(0, 0, destRaster.getWidth(), destRaster.getHeight(), offset.x, offset.y, destinationBands);
} }
WritableRaster raster = image.getRaster().createCompatibleWritableRaster(width, 1); ImageTypeSpecifier rawType = getRawImageType(0);
Raster sourceRow = raster.createChild(aoi.x, 0, aoi.width, 1, 0, 0, sourceBands); WritableRaster rowRaster = rawType.createBufferedImage(width, 1).getRaster();
Raster sourceRow = rowRaster.createChild(aoi.x, 0, aoi.width, 1, 0, 0, sourceBands);
int planeWidth = width * 4; int planeWidth = width * 4;
final byte[] data = ((DataBufferByte) raster.getDataBuffer()).getData(); final byte[] data = ((DataBufferByte) rowRaster.getDataBuffer()).getData();
final int channels = (header.bitplanes + 7) / 8;
Object dataElements = null; Object dataElements = null;
for (int srcY = 0; srcY < height; srcY++) { for (int srcY = 0; srcY < height; srcY++) {
readPlaneData(pInput, data, 0, planeWidth); readPlaneData(data, 0, planeWidth, input);
if (srcY >= aoi.y && (srcY - aoi.y) % sourceYSubsampling == 0) { if (srcY >= aoi.y && (srcY - aoi.y) % sourceYSubsampling == 0) {
int dstY = (srcY - aoi.y) / sourceYSubsampling; int dstY = (srcY - aoi.y) / sourceYSubsampling;
if (sourceXSubsampling == 1) { if (sourceXSubsampling == 1) {
destination.setRect(0, dstY, sourceRow); destRaster.setRect(0, dstY, sourceRow);
} }
else { else {
for (int srcX = 0; srcX < sourceRow.getWidth(); srcX += sourceXSubsampling) { for (int srcX = 0; srcX < sourceRow.getWidth(); srcX += sourceXSubsampling) {
dataElements = sourceRow.getDataElements(srcX, 0, dataElements); dataElements = sourceRow.getDataElements(srcX, 0, dataElements);
int dstX = srcX / sourceXSubsampling; int dstX = srcX / sourceXSubsampling;
destination.setDataElements(dstX, dstY, dataElements); destRaster.setDataElements(dstX, dstY, dataElements);
} }
} }
} }
processImageProgress(srcY * 100f / header.width); processImageProgress(srcY * 100f / width);
if (abortRequested()) { if (abortRequested()) {
processReadAborted(); processReadAborted();
break; break;
@@ -653,12 +617,12 @@ public final class IFFImageReader extends ImageReaderBase {
// followed by green and blue. The first plane holds the least significant // followed by green and blue. The first plane holds the least significant
// bit of the red value for each pixel, and the last holds the most // bit of the red value for each pixel, and the last holds the most
// significant bit of the blue value. // significant bit of the blue value.
private void readTrueColor(ImageReadParam pParam, final ImageInputStream pInput) throws IOException { private void readInterleaved(final ImageReadParam param, final BufferedImage destination, final ImageInputStream input) throws IOException {
final int width = header.width; final int width = header.width();
final int height = header.height; final int height = header.height();
final Rectangle aoi = getSourceRegion(pParam, width, height); final Rectangle aoi = getSourceRegion(param, width, height);
final Point offset = pParam == null ? new Point(0, 0) : pParam.getDestinationOffset(); final Point offset = param == null ? new Point(0, 0) : param.getDestinationOffset();
// Set everything to default values // Set everything to default values
int sourceXSubsampling = 1; int sourceXSubsampling = 1;
@@ -667,39 +631,39 @@ public final class IFFImageReader extends ImageReaderBase {
int[] destinationBands = null; int[] destinationBands = null;
// Get values from the ImageReadParam, if any // Get values from the ImageReadParam, if any
if (pParam != null) { if (param != null) {
sourceXSubsampling = pParam.getSourceXSubsampling(); sourceXSubsampling = param.getSourceXSubsampling();
sourceYSubsampling = pParam.getSourceYSubsampling(); sourceYSubsampling = param.getSourceYSubsampling();
sourceBands = pParam.getSourceBands(); sourceBands = param.getSourceBands();
destinationBands = pParam.getDestinationBands(); destinationBands = param.getDestinationBands();
} }
// Ensure band settings from param are compatible with images // Ensure band settings from param are compatible with images
checkReadParamBandSettings(pParam, header.bitplanes / 8, image.getSampleModel().getNumBands()); checkReadParamBandSettings(param, header.bitplanes() / 8, destination.getSampleModel().getNumBands());
// NOTE: Each row of the image is stored in an integral number of 16 bit words. // NOTE: Each row of the image is stored in an integral number of 16 bit words.
// The number of words per row is words=((w+15)/16) // The number of words per row is words=((w+15)/16)
int planeWidth = 2 * ((width + 15) / 16); int planeWidth = 2 * ((width + 15) / 16);
final byte[] planeData = new byte[8 * planeWidth]; final byte[] planeData = new byte[8 * planeWidth];
WritableRaster destination = image.getRaster(); WritableRaster destRaster = destination.getRaster();
if (destinationBands != null || offset.x != 0 || offset.y != 0) { if (destinationBands != null || offset.x != 0 || offset.y != 0) {
destination = destination.createWritableChild(0, 0, destination.getWidth(), destination.getHeight(), offset.x, offset.y, destinationBands); destRaster = destRaster.createWritableChild(0, 0, destRaster.getWidth(), destRaster.getHeight(), offset.x, offset.y, destinationBands);
} }
// WritableRaster raster = image.getRaster().createCompatibleWritableRaster(width, 1);
WritableRaster raster = image.getRaster().createCompatibleWritableRaster(8 * planeWidth, 1);
Raster sourceRow = raster.createChild(aoi.x, 0, aoi.width, 1, 0, 0, sourceBands);
final byte[] data = ((DataBufferByte) raster.getDataBuffer()).getData(); WritableRaster rowRaster = destination.getRaster().createCompatibleWritableRaster(8 * planeWidth, 1);
final int channels = (header.bitplanes + 7) / 8; Raster sourceRow = rowRaster.createChild(aoi.x, 0, aoi.width, 1, 0, 0, sourceBands);
final byte[] data = ((DataBufferByte) rowRaster.getDataBuffer()).getData();
final int channels = (header.bitplanes() + 7) / 8;
final int planesPerChannel = 8; final int planesPerChannel = 8;
Object dataElements = null; Object dataElements = null;
for (int srcY = 0; srcY < height; srcY++) { for (int srcY = 0; srcY < height; srcY++) {
for (int c = 0; c < channels; c++) { for (int c = 0; c < channels; c++) {
for (int p = 0; p < planesPerChannel; p++) { for (int p = 0; p < planesPerChannel; p++) {
readPlaneData(pInput, planeData, p * planeWidth, planeWidth); readPlaneData(planeData, p * planeWidth, planeWidth, input);
} }
// Skip rows outside AOI // Skip rows outside AOI
@@ -710,7 +674,7 @@ public final class IFFImageReader extends ImageReaderBase {
continue; continue;
} }
if (formType == IFF.TYPE_ILBM) { if (header.formType == IFF.TYPE_ILBM) {
// NOTE: Using (channels - c - 1) instead of just c, // NOTE: Using (channels - c - 1) instead of just c,
// effectively reverses the channel order from RGBA to ABGR // effectively reverses the channel order from RGBA to ABGR
int off = (channels - c - 1); int off = (channels - c - 1);
@@ -721,33 +685,30 @@ public final class IFFImageReader extends ImageReaderBase {
pixelPos += 8; pixelPos += 8;
} }
} }
else if (formType == IFF.TYPE_PBM) { else if (header.formType == IFF.TYPE_PBM) {
System.arraycopy(planeData, 0, data, srcY * 8 * planeWidth, planeWidth); System.arraycopy(planeData, 0, data, srcY * 8 * planeWidth, planeWidth);
} }
else { else {
throw new AssertionError(String.format("Unsupported FORM type: %s", formType)); throw new AssertionError(String.format("Unsupported FORM type: %s", toChunkStr(header.formType)));
} }
} }
if (srcY >= aoi.y && (srcY - aoi.y) % sourceYSubsampling == 0) { if (srcY >= aoi.y && (srcY - aoi.y) % sourceYSubsampling == 0) {
int dstY = (srcY - aoi.y) / sourceYSubsampling; int dstY = (srcY - aoi.y) / sourceYSubsampling;
// TODO: Support conversion to INT (A)RGB rasters (maybe using ColorConvertOp?)
// TODO: Avoid createChild if no region? // TODO: Avoid createChild if no region?
if (sourceXSubsampling == 1) { if (sourceXSubsampling == 1) {
destination.setRect(0, dstY, sourceRow); destRaster.setRect(0, dstY, sourceRow);
// dataElements = raster.getDataElements(aoi.x, 0, aoi.width, 1, dataElements);
// destination.setDataElements(offset.x, offset.y + (srcY - aoi.y) / sourceYSubsampling, aoi.width, 1, dataElements);
} }
else { else {
for (int srcX = 0; srcX < sourceRow.getWidth(); srcX += sourceXSubsampling) { for (int srcX = 0; srcX < sourceRow.getWidth(); srcX += sourceXSubsampling) {
dataElements = sourceRow.getDataElements(srcX, 0, dataElements); dataElements = sourceRow.getDataElements(srcX, 0, dataElements);
int dstX = srcX / sourceXSubsampling; int dstX = srcX / sourceXSubsampling;
destination.setDataElements(dstX, dstY, dataElements); destRaster.setDataElements(dstX, dstY, dataElements);
} }
} }
} }
processImageProgress(srcY * 100f / header.width); processImageProgress(srcY * 100f / width);
if (abortRequested()) { if (abortRequested()) {
processReadAborted(); processReadAborted();
break; break;
@@ -755,16 +716,15 @@ public final class IFFImageReader extends ImageReaderBase {
} }
} }
private void readPlaneData(final ImageInputStream pInput, final byte[] pData, final int pOffset, final int pPlaneWidth) private void readPlaneData(final byte[] destination, final int offset, final int planeWidth, final ImageInputStream input)
throws IOException { throws IOException {
switch (header.compressionType()) {
switch (header.compressionType) {
case BMHDChunk.COMPRESSION_NONE: case BMHDChunk.COMPRESSION_NONE:
pInput.readFully(pData, pOffset, pPlaneWidth); input.readFully(destination, offset, planeWidth);
// Uncompressed rows must have even number of bytes // Uncompressed rows must have an even number of bytes
if ((header.bitplanes * pPlaneWidth) % 2 != 0) { if ((header.bitplanes() * planeWidth) % 2 != 0) {
pInput.readByte(); input.readByte();
} }
break; break;
@@ -773,48 +733,46 @@ public final class IFFImageReader extends ImageReaderBase {
// TODO: How do we know if the last byte in the body is a pad byte or not?! // TODO: How do we know if the last byte in the body is a pad byte or not?!
// The body consists of byte-run (PackBits) compressed rows of bit plane data. // The body consists of byte-run (PackBits) compressed rows of bit plane data.
// However, we don't know how long each compressed row is, without decoding it... // However, we don't know how long each compressed row is, without decoding it...
// The workaround below, is to use a decode buffer size of pPlaneWidth, // The workaround below, is to use a decode buffer size of planeWidth,
// to make sure we don't decode anything we don't have to (shouldn't). // to make sure we don't decode anything we don't have to (shouldn't).
if (byteRunStream == null) { if (byteRunStream == null) {
byteRunStream = new DataInputStream( byteRunStream = new DataInputStream(
new DecoderStream( new DecoderStream(
IIOUtil.createStreamAdapter(pInput, body.chunkLength), IIOUtil.createStreamAdapter(input, header.bodyLength()),
new PackBitsDecoder(true), new PackBitsDecoder(header.sampleSize(), true),
pPlaneWidth * header.bitplanes planeWidth * (header.sampleSize() > 1 ? 1 : header.bitplanes())
) )
); );
} }
byteRunStream.readFully(pData, pOffset, pPlaneWidth); byteRunStream.readFully(destination, offset, planeWidth);
break; break;
case 4: // Compression type 4 means different things for different FORM types... :-P case 4: // Compression type 4 means different things for different FORM types... :-P
if (formType == IFF.TYPE_RGB8) { if (header.formType == IFF.TYPE_RGB8) {
// Impulse RGB8 RLE compression: 24 bit RGB + 1 bit mask + 7 bit run count // Impulse RGB8 RLE compression: 24 bit RGB + 1 bit mask + 7 bit run count
if (byteRunStream == null) { if (byteRunStream == null) {
byteRunStream = new DataInputStream( byteRunStream = new DataInputStream(
new DecoderStream( new DecoderStream(
IIOUtil.createStreamAdapter(pInput, body.chunkLength), IIOUtil.createStreamAdapter(input, header.bodyLength()),
new RGB8RLEDecoder(), new RGB8RLEDecoder(), 1024
pPlaneWidth * 4
) )
); );
} }
byteRunStream.readFully(pData, pOffset, pPlaneWidth); byteRunStream.readFully(destination, offset, planeWidth);
break; break;
} }
default: default:
throw new IIOException(String.format("Unknown compression type: %d", header.compressionType)); throw new IIOException(String.format("Unknown compression type: %d", header.compressionType()));
} }
} }
private void multiPaletteToRGB(final int row, final byte[] indexed, final IndexColorModel colorModel, final byte[] dest, final int destOffset) { private void multiPaletteToRGB(final int row, final byte[] indexed, final IndexColorModel colorModel, final byte[] dest, @SuppressWarnings("SameParameterValue") final int destOffset) {
final int width = header.width; final int width = header.width();
ColorModel palette = paletteChange.getColorModel(colorModel, row, isLaced()); ColorModel palette = header.colorMapForRow(colorModel, row);
for (int x = 0; x < width; x++) { for (int x = 0; x < width; x++) {
int pixel = indexed[x] & 0xff; int pixel = indexed[x] & 0xff;
@@ -828,12 +786,14 @@ public final class IFFImageReader extends ImageReaderBase {
} }
} }
private void hamToRGB(final byte[] indexed, final IndexColorModel colorModel, final byte[] dest, final int destOffset) { private void hamToRGB(final byte[] indexed, final IndexColorModel colorModel, final byte[] dest, @SuppressWarnings("SameParameterValue") final int destOffset) {
final int bits = header.bitplanes; final int bits = header.bitplanes();
final int width = header.width; final int width = header.width();
int lastRed = 0;
int lastGreen = 0; // Initialize to the "border color" (index 0)
int lastBlue = 0; int lastRed = colorModel.getRed(0);
int lastGreen = colorModel.getGreen(0);
int lastBlue = colorModel.getBlue(0);
for (int x = 0; x < width; x++) { for (int x = 0; x < width; x++) {
int pixel = indexed[x] & 0xff; int pixel = indexed[x] & 0xff;
@@ -867,32 +827,11 @@ public final class IFFImageReader extends ImageReaderBase {
} }
} }
private boolean isSHAM() { public static void main(String[] args) {
// TODO:
return false;
}
private boolean isPCHG() {
return paletteChange != null;
}
private boolean isEHB() {
return viewPort != null && viewPort.isEHB();
}
private boolean isHAM() {
return viewPort != null && viewPort.isHAM();
}
public boolean isLaced() {
return viewPort != null && viewPort.isLaced();
}
public static void main(String[] pArgs) throws IOException {
ImageReader reader = new IFFImageReader(new IFFImageReaderSpi()); ImageReader reader = new IFFImageReader(new IFFImageReaderSpi());
boolean scale = false; boolean scale = false;
for (String arg : pArgs) { for (String arg : args) {
if (arg.startsWith("-")) { if (arg.startsWith("-")) {
scale = true; scale = true;
continue; continue;
@@ -53,40 +53,41 @@ public final class IFFImageReaderSpi extends ImageReaderSpiBase {
} }
@Override @Override
public boolean canDecodeInput(Object pSource) throws IOException { public boolean canDecodeInput(final Object source) throws IOException {
return pSource instanceof ImageInputStream && canDecode((ImageInputStream) pSource); return source instanceof ImageInputStream && canDecode((ImageInputStream) source);
} }
private static boolean canDecode(ImageInputStream pInput) throws IOException { private static boolean canDecode(final ImageInputStream input) throws IOException {
pInput.mark(); input.mark();
try { try {
// Is it IFF // Is it IFF
if (pInput.readInt() == IFF.CHUNK_FORM) { if (input.readInt() == IFF.CHUNK_FORM) {
pInput.readInt();// Skip length field input.readInt();// Skip length field
int type = pInput.readInt(); int type = input.readInt();
if (type == IFF.TYPE_ILBM || type == IFF.TYPE_PBM if (type == IFF.TYPE_ILBM || type == IFF.TYPE_PBM
|| type == IFF.TYPE_RGB8) { // Impulse RGB8 || type == IFF.TYPE_RGB8 // Impulse RGB8 format
|| type == IFF.TYPE_DEEP || type == IFF.TYPE_TVPP) { // TVPaint DEEP format
return true; return true;
} }
} }
} }
finally { finally {
pInput.reset(); input.reset();
} }
return false; return false;
} }
@Override @Override
public ImageReader createReaderInstance(Object pExtension) throws IOException { public ImageReader createReaderInstance(final Object extension) {
return new IFFImageReader(this); return new IFFImageReader(this);
} }
@Override @Override
public String getDescription(Locale pLocale) { public String getDescription(Locale locale) {
return "Commodore Amiga/Electronic Arts Image Interchange Format (IFF) image reader"; return "Commodore Amiga/Electronic Arts Image Interchange Format (IFF) image reader";
} }
} }
@@ -30,31 +30,22 @@
package com.twelvemonkeys.imageio.plugins.iff; package com.twelvemonkeys.imageio.plugins.iff;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.IndexColorModel;
import java.awt.image.Raster;
import java.awt.image.RenderedImage;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.spi.ImageWriterSpi;
import com.twelvemonkeys.imageio.ImageWriterBase; import com.twelvemonkeys.imageio.ImageWriterBase;
import com.twelvemonkeys.imageio.util.IIOUtil; import com.twelvemonkeys.imageio.util.IIOUtil;
import com.twelvemonkeys.io.FastByteArrayOutputStream; import com.twelvemonkeys.io.FastByteArrayOutputStream;
import com.twelvemonkeys.io.enc.EncoderStream; import com.twelvemonkeys.io.enc.EncoderStream;
import com.twelvemonkeys.io.enc.PackBitsEncoder; import com.twelvemonkeys.io.enc.PackBitsEncoder;
import javax.imageio.*;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.spi.ImageWriterSpi;
import java.awt.*;
import java.awt.image.*;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
/** /**
* Writer for Commodore Amiga (Electronic Arts) IFF ILBM (InterLeaved BitMap) format. * Writer for Commodore Amiga (Electronic Arts) IFF ILBM (InterLeaved BitMap) format.
* The IFF format (Interchange File Format) is the standard file format * The IFF format (Interchange File Format) is the standard file format
@@ -68,8 +59,8 @@ import com.twelvemonkeys.io.enc.PackBitsEncoder;
*/ */
public final class IFFImageWriter extends ImageWriterBase { public final class IFFImageWriter extends ImageWriterBase {
IFFImageWriter(ImageWriterSpi pProvider) { IFFImageWriter(ImageWriterSpi provider) {
super(pProvider); super(provider);
} }
@Override @Override
@@ -83,23 +74,29 @@ public final class IFFImageWriter extends ImageWriterBase {
} }
@Override @Override
public void write(IIOMetadata pStreamMetadata, IIOImage pImage, ImageWriteParam pParam) throws IOException { public ImageWriteParam getDefaultWriteParam() {
return new IFFWriteParam(getLocale());
}
@Override
public void write(IIOMetadata streamMetadata, IIOImage image, ImageWriteParam param) throws IOException {
assertOutput(); assertOutput();
if (pImage.hasRaster()) { if (image.hasRaster()) {
throw new UnsupportedOperationException("Cannot write raster"); throw new UnsupportedOperationException("Cannot write raster");
} }
processImageStarted(0); processImageStarted(0);
RenderedImage renderedImage = image.getRenderedImage();
boolean compress = shouldCompress(renderedImage, param);
// Prepare image data to be written // Prepare image data to be written
ByteArrayOutputStream imageData = new FastByteArrayOutputStream(1024); ByteArrayOutputStream imageData = new FastByteArrayOutputStream(1024);
packImageData(imageData, pImage.getRenderedImage(), pParam); packImageData(imageData, renderedImage, compress);
//System.out.println("Image data: " + imageData.size());
// Write metadata // Write metadata
writeMeta(pImage.getRenderedImage(), imageData.size()); writeMeta(renderedImage, imageData.size(), compress);
// Write image data // Write image data
writeBody(imageData); writeBody(imageData);
@@ -107,34 +104,31 @@ public final class IFFImageWriter extends ImageWriterBase {
processImageComplete(); processImageComplete();
} }
private void writeBody(ByteArrayOutputStream pImageData) throws IOException { private void writeBody(ByteArrayOutputStream imageData) throws IOException {
imageOutput.writeInt(IFF.CHUNK_BODY); imageOutput.writeInt(IFF.CHUNK_BODY);
imageOutput.writeInt(pImageData.size()); imageOutput.writeInt(imageData.size());
// NOTE: This is much faster than imageOutput.write(pImageData.toByteArray()) // NOTE: This is much faster than imageOutput.write(imageData.toByteArray())
// as the data array is not duplicated // as the data array is not duplicated
try (OutputStream adapter = IIOUtil.createStreamAdapter(imageOutput)) { try (OutputStream adapter = IIOUtil.createStreamAdapter(imageOutput)) {
pImageData.writeTo(adapter); imageData.writeTo(adapter);
} }
if (pImageData.size() % 2 == 0) { if (imageData.size() % 2 == 0) {
imageOutput.writeByte(0); // PAD imageOutput.writeByte(0); // PAD
} }
imageOutput.flush(); imageOutput.flush();
} }
private void packImageData(OutputStream pOutput, RenderedImage pImage, ImageWriteParam pParam) throws IOException { private void packImageData(OutputStream outputStream, RenderedImage image, final boolean compress) throws IOException {
// TODO: Allow param to dictate uncompressed
// TODO: Allow param to dictate type PBM?
// TODO: Subsample/AOI // TODO: Subsample/AOI
final boolean compress = shouldCompress(pImage); final OutputStream output = compress ? new EncoderStream(outputStream, new PackBitsEncoder(), true) : outputStream;
final OutputStream output = compress ? new EncoderStream(pOutput, new PackBitsEncoder(), true) : pOutput; final ColorModel model = image.getColorModel();
final ColorModel model = pImage.getColorModel(); final Raster raster = image.getData();
final Raster raster = pImage.getData();
final int width = pImage.getWidth(); final int width = image.getWidth();
final int height = pImage.getHeight(); final int height = image.getHeight();
// Store each row of pixels // Store each row of pixels
// 0. Loop pr channel // 0. Loop pr channel
@@ -142,7 +136,6 @@ public final class IFFImageWriter extends ImageWriterBase {
// 2. Perform byteRun1 compression for each plane separately // 2. Perform byteRun1 compression for each plane separately
// 3. Write the plane data for each plane // 3. Write the plane data for each plane
//final int planeWidth = (width + 7) / 8;
final int planeWidth = 2 * ((width + 15) / 16); final int planeWidth = 2 * ((width + 15) / 16);
final byte[] planeData = new byte[8 * planeWidth]; final byte[] planeData = new byte[8 * planeWidth];
final int channels = (model.getPixelSize() + 7) / 8; final int channels = (model.getPixelSize() + 7) / 8;
@@ -167,10 +160,6 @@ public final class IFFImageWriter extends ImageWriterBase {
for (int p = 0; p < planesPerChannel; p++) { for (int p = 0; p < planesPerChannel; p++) {
output.write(planeData, p * planeWidth, planeWidth); output.write(planeData, p * planeWidth, planeWidth);
if (!compress && planeWidth % 2 != 0) {
output.write(0); // PAD
}
} }
} }
@@ -182,17 +171,16 @@ public final class IFFImageWriter extends ImageWriterBase {
output.flush(); output.flush();
} }
private void writeMeta(RenderedImage pImage, int pBodyLength) throws IOException { private void writeMeta(RenderedImage image, int bodyLength, boolean compress) throws IOException {
// Annotation ANNO chunk, 8 + annoData.length bytes // Annotation ANNO chunk, 8 + annoData.length bytes
String annotation = String.format("Written by %s IFFImageWriter %s", getOriginatingProvider().getVendorName(), getOriginatingProvider().getVersion()); String annotation = String.format("Written by %s IFFImageWriter %s", getOriginatingProvider().getVendorName(), getOriginatingProvider().getVersion());
GenericChunk anno = new GenericChunk(IFFUtil.toInt("ANNO".getBytes()), annotation.getBytes()); GenericChunk anno = new GenericChunk(IFFUtil.toInt("ANNO".getBytes()), annotation.getBytes());
ColorModel cm = pImage.getColorModel(); ColorModel cm = image.getColorModel();
IndexColorModel icm = null; IndexColorModel icm = null;
// Bitmap header BMHD chunk, 8 + 20 bytes // Bitmap header BMHD chunk, 8 + 20 bytes
// By default, don't compress narrow images int compression = compress ? BMHDChunk.COMPRESSION_BYTE_RUN : BMHDChunk.COMPRESSION_NONE;
int compression = shouldCompress(pImage) ? BMHDChunk.COMPRESSION_BYTE_RUN : BMHDChunk.COMPRESSION_NONE;
BMHDChunk header; BMHDChunk header;
if (cm instanceof IndexColorModel) { if (cm instanceof IndexColorModel) {
@@ -200,12 +188,12 @@ public final class IFFImageWriter extends ImageWriterBase {
icm = (IndexColorModel) cm; icm = (IndexColorModel) cm;
int trans = icm.getTransparency() == Transparency.BITMASK ? BMHDChunk.MASK_TRANSPARENT_COLOR : BMHDChunk.MASK_NONE; int trans = icm.getTransparency() == Transparency.BITMASK ? BMHDChunk.MASK_TRANSPARENT_COLOR : BMHDChunk.MASK_NONE;
int transPixel = icm.getTransparency() == Transparency.BITMASK ? icm.getTransparentPixel() : 0; int transPixel = icm.getTransparency() == Transparency.BITMASK ? icm.getTransparentPixel() : 0;
header = new BMHDChunk(pImage.getWidth(), pImage.getHeight(), icm.getPixelSize(), header = new BMHDChunk(image.getWidth(), image.getHeight(), icm.getPixelSize(),
trans, compression, transPixel); trans, compression, transPixel);
} }
else { else {
//System.out.println(cm.getClass().getName()); //System.out.println(cm.getClass().getName());
header = new BMHDChunk(pImage.getWidth(), pImage.getHeight(), cm.getPixelSize(), header = new BMHDChunk(image.getWidth(), image.getHeight(), cm.getPixelSize(),
BMHDChunk.MASK_NONE, compression, 0); BMHDChunk.MASK_NONE, compression, 0);
} }
@@ -217,7 +205,7 @@ public final class IFFImageWriter extends ImageWriterBase {
} }
// ILBM(4) + anno(8+len) + header(8+20) + cmap(8+len)? + body(8+len); // ILBM(4) + anno(8+len) + header(8+20) + cmap(8+len)? + body(8+len);
int size = 4 + 8 + anno.chunkLength + 28 + 8 + pBodyLength; int size = 4 + 8 + anno.chunkLength + 28 + 8 + bodyLength;
if (cmap != null) { if (cmap != null) {
size += 8 + cmap.chunkLength; size += 8 + cmap.chunkLength;
} }
@@ -231,21 +219,30 @@ public final class IFFImageWriter extends ImageWriterBase {
header.writeChunk(imageOutput); header.writeChunk(imageOutput);
if (cmap != null) { if (cmap != null) {
//System.out.println("CMAP written");
cmap.writeChunk(imageOutput); cmap.writeChunk(imageOutput);
} }
} }
private boolean shouldCompress(RenderedImage pImage) { private boolean shouldCompress(final RenderedImage image, final ImageWriteParam param) {
return pImage.getWidth() >= 32; if (param != null && param.canWriteCompressed()) {
switch (param.getCompressionMode()) {
case ImageWriteParam.MODE_DISABLED:
return false;
case ImageWriteParam.MODE_EXPLICIT:
return IFFWriteParam.COMPRESSION_TYPES[1].equals(param.getCompressionType());
default:
// Fall through
}
}
return image.getWidth() >= 32;
} }
public static void main(String[] pArgs) throws IOException { public static void main(String[] args) throws IOException {
BufferedImage image = ImageIO.read(new File(pArgs[0])); BufferedImage image = ImageIO.read(new File(args[0]));
ImageWriter writer = new IFFImageWriter(new IFFImageWriterSpi()); ImageWriter writer = new IFFImageWriter(new IFFImageWriterSpi());
writer.setOutput(ImageIO.createImageOutputStream(new File(pArgs[1]))); writer.setOutput(ImageIO.createImageOutputStream(new File(args[1])));
//writer.addIIOWriteProgressListener(new ProgressListenerBase() { //writer.addIIOWriteProgressListener(new ProgressListenerBase() {
// int mCurrPct = 0; // int mCurrPct = 0;
// //
@@ -30,13 +30,11 @@
package com.twelvemonkeys.imageio.plugins.iff; package com.twelvemonkeys.imageio.plugins.iff;
import java.io.IOException; import com.twelvemonkeys.imageio.spi.ImageWriterSpiBase;
import java.util.Locale;
import javax.imageio.ImageTypeSpecifier; import javax.imageio.ImageTypeSpecifier;
import javax.imageio.ImageWriter; import javax.imageio.ImageWriter;
import java.util.Locale;
import com.twelvemonkeys.imageio.spi.ImageWriterSpiBase;
/** /**
* IFFImageWriterSpi * IFFImageWriterSpi
@@ -53,19 +51,19 @@ public class IFFImageWriterSpi extends ImageWriterSpiBase {
super(new IFFProviderInfo()); super(new IFFProviderInfo());
} }
public boolean canEncodeImage(final ImageTypeSpecifier pType) { public boolean canEncodeImage(final ImageTypeSpecifier type) {
// TODO: Probably can't store 16 bit types etc... // TODO: Probably can't store 16 bit types etc...
// TODO: Can't store CMYK (well.. it does, but they can't be read back) // TODO: Can't store CMYK (well.. it does, but they can't be read back)
return true; return true;
} }
@Override @Override
public ImageWriter createWriterInstance(Object pExtension) throws IOException { public ImageWriter createWriterInstance(Object extension) {
return new IFFImageWriter(this); return new IFFImageWriter(this);
} }
@Override @Override
public String getDescription(Locale pLocale) { public String getDescription(Locale locale) {
return "Commodore Amiga/Electronic Arts Image Interchange Format (IFF) image writer"; return "Commodore Amiga/Electronic Arts Image Interchange Format (IFF) image writer";
} }
} }
@@ -40,11 +40,11 @@ import com.twelvemonkeys.imageio.spi.ReaderWriterProviderInfo;
* @version $Id: IFFProviderInfo.java,v 1.0 20/03/15 harald.kuhr Exp$ * @version $Id: IFFProviderInfo.java,v 1.0 20/03/15 harald.kuhr Exp$
*/ */
final class IFFProviderInfo extends ReaderWriterProviderInfo { final class IFFProviderInfo extends ReaderWriterProviderInfo {
protected IFFProviderInfo() { IFFProviderInfo() {
super( super(
IFFProviderInfo.class, IFFProviderInfo.class,
new String[] {"iff", "IFF"}, new String[] {"iff", "IFF"},
new String[] {"iff", "lbm", "ham", "ham8", "ilbm"}, new String[] {"iff", "lbm", "ham", "ham8", "ilbm", "rgb8", "deep"},
new String[] {"image/iff", "image/x-iff"}, new String[] {"image/iff", "image/x-iff"},
"com.twelvemonkeys.imageio.plugins.iff.IFFImageReader", "com.twelvemonkeys.imageio.plugins.iff.IFFImageReader",
new String[]{"com.twelvemonkeys.imageio.plugins.iff.IFFImageReaderSpi"}, new String[]{"com.twelvemonkeys.imageio.plugins.iff.IFFImageReaderSpi"},
@@ -56,11 +56,11 @@ final class IFFUtil {
* @return the rotation table * @return the rotation table
*/ */
static private long[] rtable(int n) { static private long[] rtable(int n) {
return new long[]{ return new long[] {
0x00000000l << n, 0x00000001l << n, 0x00000100l << n, 0x00000101l << n, 0x00000000L , 0x00000001L << n, 0x00000100L << n, 0x00000101L << n,
0x00010000l << n, 0x00010001l << n, 0x00010100l << n, 0x00010101l << n, 0x00010000L << n, 0x00010001L << n, 0x00010100L << n, 0x00010101L << n,
0x01000000l << n, 0x01000001l << n, 0x01000100l << n, 0x01000101l << n, 0x01000000L << n, 0x01000001L << n, 0x01000100L << n, 0x01000101L << n,
0x01010000l << n, 0x01010001l << n, 0x01010100l << n, 0x01010101l << n 0x01010000L << n, 0x01010001L << n, 0x01010100L << n, 0x01010101L << n
}; };
} }
@@ -75,16 +75,16 @@ final class IFFUtil {
* Bits from the source are rotated 90 degrees clockwise written to the * Bits from the source are rotated 90 degrees clockwise written to the
* destination. * destination.
* *
* @param pSrc source pixel data * @param src source pixel data
* @param pSrcPos starting index of 8 x 8 bit source tile * @param srcPos starting index of 8 x 8 bit source tile
* @param pSrcStep byte offset between adjacent rows in source * @param srcStep byte offset between adjacent rows in source
* @param pDst destination pixel data * @param dst destination pixel data
* @param pDstPos starting index of 8 x 8 bit destination tile * @param dstPos starting index of 8 x 8 bit destination tile
* @param pDstStep byte offset between adjacent rows in destination * @param dstStep byte offset between adjacent rows in destination
*/ */
static void bitRotateCW(final byte[] pSrc, int pSrcPos, int pSrcStep, static void bitRotateCW(final byte[] src, int srcPos, int srcStep,
final byte[] pDst, int pDstPos, int pDstStep) { final byte[] dst, int dstPos, int dstStep) {
int idx = pSrcPos; int idx = srcPos;
int lonyb; int lonyb;
int hinyb; int hinyb;
@@ -92,41 +92,41 @@ final class IFFUtil {
long hi = 0; long hi = 0;
for (int i = 0; i < 8; i++) { for (int i = 0; i < 8; i++) {
lonyb = pSrc[idx] & 0xF; lonyb = src[idx] & 0xF;
hinyb = (pSrc[idx] >> 4) & 0xF; hinyb = (src[idx] >> 4) & 0xF;
lo |= RTABLE[i][lonyb]; lo |= RTABLE[i][lonyb];
hi |= RTABLE[i][hinyb]; hi |= RTABLE[i][hinyb];
idx += pSrcStep; idx += srcStep;
} }
idx = pDstPos; idx = dstPos;
pDst[idx] = (byte)((hi >> 24) & 0xFF); dst[idx] = (byte)((hi >> 24) & 0xFF);
idx += pDstStep; idx += dstStep;
if (idx < pDst.length) { if (idx < dst.length) {
pDst[idx] = (byte)((hi >> 16) & 0xFF); dst[idx] = (byte)((hi >> 16) & 0xFF);
idx += pDstStep; idx += dstStep;
if (idx < pDst.length) { if (idx < dst.length) {
pDst[idx] = (byte)((hi >> 8) & 0xFF); dst[idx] = (byte)((hi >> 8) & 0xFF);
idx += pDstStep; idx += dstStep;
if (idx < pDst.length) { if (idx < dst.length) {
pDst[idx] = (byte)(hi & 0xFF); dst[idx] = (byte)(hi & 0xFF);
idx += pDstStep; idx += dstStep;
} }
} }
} }
if (idx < pDst.length) { if (idx < dst.length) {
pDst[idx] = (byte)((lo >> 24) & 0xFF); dst[idx] = (byte)((lo >> 24) & 0xFF);
idx += pDstStep; idx += dstStep;
if (idx < pDst.length) { if (idx < dst.length) {
pDst[idx] = (byte)((lo >> 16) & 0xFF); dst[idx] = (byte)((lo >> 16) & 0xFF);
idx += pDstStep; idx += dstStep;
if (idx < pDst.length) { if (idx < dst.length) {
pDst[idx] = (byte)((lo >> 8) & 0xFF); dst[idx] = (byte)((lo >> 8) & 0xFF);
idx += pDstStep; idx += dstStep;
if (idx < pDst.length) { if (idx < dst.length) {
pDst[idx] = (byte)(lo & 0xFF); dst[idx] = (byte)(lo & 0xFF);
} }
} }
} }
@@ -137,16 +137,16 @@ final class IFFUtil {
* Rotate bits counterclockwise. * Rotate bits counterclockwise.
* The IFFImageWriter uses this to convert pixel bits from chunky to planar. * The IFFImageWriter uses this to convert pixel bits from chunky to planar.
* *
* @param pSrc source pixel data (only lower 8 bits used) * @param src source pixel data (only lower 8 bits used)
* @param pSrcPos starting index of 8 x 8 bit source tile * @param srcPos starting index of 8 x 8 bit source tile
* @param pSrcStep byte offset between adjacent rows in source * @param srcStep byte offset between adjacent rows in source
* @param pDst destination pixel data * @param dst destination pixel data
* @param pDstPos starting index of 8 x 8 bit destination tile * @param dstPos starting index of 8 x 8 bit destination tile
* @param pDstStep byte offset between adjacent rows in destination * @param dstStep byte offset between adjacent rows in destination
*/ */
static void bitRotateCCW(final int[] pSrc, int pSrcPos, int pSrcStep, static void bitRotateCCW(final int[] src, int srcPos, @SuppressWarnings("SameParameterValue") int srcStep,
final byte[] pDst, int pDstPos, int pDstStep) { final byte[] dst, int dstPos, int dstStep) {
int idx = pSrcPos; int idx = srcPos;
int lonyb; int lonyb;
int hinyb; int hinyb;
@@ -154,48 +154,49 @@ final class IFFUtil {
long hi = 0; long hi = 0;
for (int i = 7; i >= 0; i--) { for (int i = 7; i >= 0; i--) {
lonyb = pSrc[idx] & 0xF; lonyb = src[idx] & 0xF;
hinyb = (pSrc[idx] >> 4) & 0xF; hinyb = (src[idx] >> 4) & 0xF;
lo |= RTABLE[i][lonyb]; lo |= RTABLE[i][lonyb];
hi |= RTABLE[i][hinyb]; hi |= RTABLE[i][hinyb];
idx += pSrcStep; idx += srcStep;
} }
idx = pDstPos; idx = dstPos;
pDst[idx] = (byte)(lo & 0xFF); dst[idx] = (byte)(lo & 0xFF);
idx += pDstStep; idx += dstStep;
pDst[idx] = (byte)((lo >> 8) & 0xFF); dst[idx] = (byte)((lo >> 8) & 0xFF);
idx += pDstStep; idx += dstStep;
pDst[idx] = (byte)((lo >> 16) & 0xFF); dst[idx] = (byte)((lo >> 16) & 0xFF);
idx += pDstStep; idx += dstStep;
pDst[idx] = (byte)((lo >> 24) & 0xFF); dst[idx] = (byte)((lo >> 24) & 0xFF);
idx += pDstStep; idx += dstStep;
pDst[idx] = (byte)(hi & 0xFF); dst[idx] = (byte)(hi & 0xFF);
idx += pDstStep; idx += dstStep;
pDst[idx] = (byte)((hi >> 8) & 0xFF); dst[idx] = (byte)((hi >> 8) & 0xFF);
idx += pDstStep; idx += dstStep;
pDst[idx] = (byte)((hi >> 16) & 0xFF); dst[idx] = (byte)((hi >> 16) & 0xFF);
idx += pDstStep; idx += dstStep;
pDst[idx] = (byte)((hi >> 24) & 0xFF); dst[idx] = (byte)((hi >> 24) & 0xFF);
} }
/** /**
* Rotate bits counterclockwise. * Rotate bits counterclockwise.
* The IFFImageWriter uses this to convert pixel bits from chunky to planar. * The IFFImageWriter uses this to convert pixel bits from chunky to planar.
* *
* @param pSrc source pixel data * @param src source pixel data
* @param pSrcPos starting index of 8 x 8 bit source tile * @param srcPos starting index of 8 x 8 bit source tile
* @param pSrcStep byte offset between adjacent rows in source * @param srcStep byte offset between adjacent rows in source
* @param pDst destination pixel data * @param dst destination pixel data
* @param pDstPos starting index of 8 x 8 bit destination tile * @param dstPos starting index of 8 x 8 bit destination tile
* @param pDstStep byte offset between adjacent rows in destination * @param dstStep byte offset between adjacent rows in destination
*/ */
static void bitRotateCCW(final byte[] pSrc, int pSrcPos, int pSrcStep, @SuppressWarnings("unused")
final byte[] pDst, int pDstPos, int pDstStep) { static void bitRotateCCW(final byte[] src, int srcPos, int srcStep,
int idx = pSrcPos; final byte[] dst, int dstPos, int dstStep) {
int idx = srcPos;
int lonyb; int lonyb;
int hinyb; int hinyb;
@@ -203,57 +204,57 @@ final class IFFUtil {
long hi = 0; long hi = 0;
for (int i = 7; i >= 0; i--) { for (int i = 7; i >= 0; i--) {
lonyb = pSrc[idx] & 0xF; lonyb = src[idx] & 0xF;
hinyb = (pSrc[idx] >> 4) & 0xF; hinyb = (src[idx] >> 4) & 0xF;
lo |= RTABLE[i][lonyb]; lo |= RTABLE[i][lonyb];
hi |= IFFUtil.RTABLE[i][hinyb]; hi |= IFFUtil.RTABLE[i][hinyb];
idx += pSrcStep; idx += srcStep;
} }
idx = pDstPos; idx = dstPos;
pDst[idx] = (byte)(lo & 0xFF); dst[idx] = (byte)(lo & 0xFF);
idx += pDstStep; idx += dstStep;
pDst[idx] = (byte)((lo >> 8) & 0xFF); dst[idx] = (byte)((lo >> 8) & 0xFF);
idx += pDstStep; idx += dstStep;
pDst[idx] = (byte)((lo >> 16) & 0xFF); dst[idx] = (byte)((lo >> 16) & 0xFF);
idx += pDstStep; idx += dstStep;
pDst[idx] = (byte)((lo >> 24) & 0xFF); dst[idx] = (byte)((lo >> 24) & 0xFF);
idx += pDstStep; idx += dstStep;
pDst[idx] = (byte)(hi & 0xFF); dst[idx] = (byte)(hi & 0xFF);
idx += pDstStep; idx += dstStep;
pDst[idx] = (byte)((hi >> 8) & 0xFF); dst[idx] = (byte)((hi >> 8) & 0xFF);
idx += pDstStep; idx += dstStep;
pDst[idx] = (byte)((hi >> 16) & 0xFF); dst[idx] = (byte)((hi >> 16) & 0xFF);
idx += pDstStep; idx += dstStep;
pDst[idx] = (byte)((hi >> 24) & 0xFF); dst[idx] = (byte)((hi >> 24) & 0xFF);
} }
/** /**
* Converts a byte array to an int. * Converts a byte array to an int.
* *
* @param pBytes a byte array of length 4 * @param bytes a byte array of length 4
* @return the bytes converted to an int * @return the bytes converted to an int
* *
* @throws ArrayIndexOutOfBoundsException if length is < 4 * @throws ArrayIndexOutOfBoundsException if length is < 4
*/ */
static int toInt(final byte[] pBytes) { static int toInt(final byte[] bytes) {
return (pBytes[0] & 0xff) << 24 | (pBytes[1] & 0xff) << 16 return (bytes[0] & 0xff) << 24 | (bytes[1] & 0xff) << 16
| (pBytes[2] & 0xff) << 8 | (pBytes[3] & 0xff); | (bytes[2] & 0xff) << 8 | (bytes[3] & 0xff);
} }
/** /**
* Converts an int to a four letter String. * Converts an int to a four letter String.
* *
* @param pChunkId the chunk identifier * @param chunkId the chunk identifier
* @return a String * @return a String
*/ */
static String toChunkStr(int pChunkId) { static String toChunkStr(int chunkId) {
return new String(new byte[] {(byte) ((pChunkId & 0xff000000) >> 24), return new String(new byte[] {(byte) ((chunkId & 0xff000000) >> 24),
(byte) ((pChunkId & 0x00ff0000) >> 16), (byte) ((chunkId & 0x00ff0000) >> 16),
(byte) ((pChunkId & 0x0000ff00) >> 8), (byte) ((chunkId & 0x0000ff00) >> 8),
(byte) ((pChunkId & 0x000000ff))}); (byte) ((chunkId & 0x000000ff))});
} }
} }
@@ -0,0 +1,25 @@
package com.twelvemonkeys.imageio.plugins.iff;
import javax.imageio.ImageWriteParam;
import java.util.Locale;
/**
* IFFWriteParam.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: IFFWriteParam.java,v 1.0 03/02/2022 haraldk Exp$
*/
public final class IFFWriteParam extends ImageWriteParam {
static final String[] COMPRESSION_TYPES = {"NONE", "RLE"};
public IFFWriteParam(final Locale locale) {
super(locale);
compressionTypes = COMPRESSION_TYPES;
compressionType = compressionTypes[1];
canWriteCompressed = true;
}
}
@@ -80,9 +80,9 @@ final class MutableIndexColorModel extends ColorModel {
// TODO: Move validation to chunk (when reading) // TODO: Move validation to chunk (when reading)
if (index >= rgbs.length) { if (index >= rgbs.length) {
// TODO: Issue IIO warning // TODO: Issue IIO warning
System.err.printf("warning - palette change register out of range\n"); System.err.println("warning - palette change register out of range");
System.err.printf(" change structure %d index=%d (max %d)\n", i, index, getMapSize() - 1); System.err.printf(" change structure %d index=%d (max %d)\n", i, index, getMapSize() - 1);
System.err.printf(" ignoring it... colors might get messed up from here\n"); System.err.println(" ignoring it... colors might get messed up from here");
} }
else if (index != MP_REG_IGNORE) { else if (index != MP_REG_IGNORE) {
updateRGB(index, ((changes[i].r & 0xff) << 16) | ((changes[i].g & 0xff) << 8) | (changes[i].b & 0xff)); updateRGB(index, ((changes[i].r & 0xff) << 16) | ((changes[i].g & 0xff) << 8) | (changes[i].b & 0xff));
@@ -72,42 +72,43 @@ final class PCHGChunk extends AbstractMultiPaletteChunk {
private int totalChanges; private int totalChanges;
private int minReg; private int minReg;
PCHGChunk(int pChunkLength) { PCHGChunk(int chunkLength) {
super(IFF.CHUNK_PCHG, pChunkLength); super(IFF.CHUNK_PCHG, chunkLength);
} }
@Override @Override
void readChunk(final DataInput pInput) throws IOException { void readChunk(final DataInput input) throws IOException {
int compression = pInput.readUnsignedShort(); int compression = input.readUnsignedShort();
int flags = pInput.readUnsignedShort(); int flags = input.readUnsignedShort();
startLine = pInput.readShort(); startLine = input.readShort();
lineCount = pInput.readUnsignedShort(); lineCount = input.readUnsignedShort();
changedLines = pInput.readUnsignedShort(); changedLines = input.readUnsignedShort();
minReg = pInput.readUnsignedShort(); minReg = input.readUnsignedShort();
int maxReg = pInput.readUnsignedShort(); int maxReg = input.readUnsignedShort();
/*int maxChangesPerLine = */pInput.readUnsignedShort(); // We don't really care, as we're not limited by the Amiga display hardware /*int maxChangesPerLine = */
totalChanges = pInput.readInt(); input.readUnsignedShort(); // We don't really care, as we're not limited by the Amiga display hardware
totalChanges = input.readInt();
byte[] data; byte[] data;
switch (compression) { switch (compression) {
case PCHG_COMP_NONE: case PCHG_COMP_NONE:
data = new byte[chunkLength - 20]; data = new byte[chunkLength - 20];
pInput.readFully(data); input.readFully(data);
break; break;
case PCHG_COMP_HUFFMAN: case PCHG_COMP_HUFFMAN:
// NOTE: Huffman decompression is completely untested, due to lack of source data (read: Probably broken). // NOTE: Huffman decompression is completely untested, due to lack of source data (read: Probably broken).
int compInfoSize = pInput.readInt(); int compInfoSize = input.readInt();
int originalDataSize = pInput.readInt(); int originalDataSize = input.readInt();
short[] compTree = new short[compInfoSize / 2]; short[] compTree = new short[compInfoSize / 2];
for (int i = 0; i < compTree.length; i++) { for (int i = 0; i < compTree.length; i++) {
compTree[i] = pInput.readShort(); compTree[i] = input.readShort();
} }
byte[] compData = new byte[chunkLength - 20 - 8 - compInfoSize]; byte[] compData = new byte[chunkLength - 20 - 8 - compInfoSize];
pInput.readFully(compData); input.readFully(compData);
data = new byte[originalDataSize]; data = new byte[originalDataSize];
@@ -1,3 +1,33 @@
/*
* Copyright (c) 2022, 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.iff; package com.twelvemonkeys.imageio.plugins.iff;
import com.twelvemonkeys.io.enc.DecodeException; import com.twelvemonkeys.io.enc.DecodeException;
@@ -13,7 +43,7 @@ import java.nio.ByteBuffer;
* *
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a> * @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$ * @author last modified by $Author: haraldk$
* @version $Id: RGB8Stream.java,v 1.0 28/01/2022 haraldk Exp$ * @version $Id: RGB8RLEDecoder.java,v 1.0 28/01/2022 haraldk Exp$
* *
* @see <a href="https://wiki.amigaos.net/wiki/RGBN_and_RGB8_IFF_Image_Data">RGBN and RGB8 IFF Image Data</a> * @see <a href="https://wiki.amigaos.net/wiki/RGBN_and_RGB8_IFF_Image_Data">RGBN and RGB8 IFF Image Data</a>
*/ */
@@ -38,8 +38,8 @@ package com.twelvemonkeys.imageio.plugins.iff;
* @version $Id: SHAMChunk.java,v 1.0 30.03.12 14:53 haraldk Exp$ * @version $Id: SHAMChunk.java,v 1.0 30.03.12 14:53 haraldk Exp$
*/ */
final class SHAMChunk extends AbstractMultiPaletteChunk { final class SHAMChunk extends AbstractMultiPaletteChunk {
SHAMChunk(int pChunkLength) { SHAMChunk(int chunkLength) {
super(IFF.CHUNK_SHAM, pChunkLength); super(IFF.CHUNK_SHAM, chunkLength);
} }
@Override @Override
@@ -1,26 +1,24 @@
package com.twelvemonkeys.imageio.plugins.iff; package com.twelvemonkeys.imageio.plugins.iff;
import static org.junit.Assert.*;
import java.awt.image.IndexColorModel;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import javax.imageio.metadata.IIOMetadataFormatImpl;
import javax.imageio.metadata.IIOMetadataNode;
import org.junit.Test; import org.junit.Test;
import org.junit.function.ThrowingRunnable; import org.junit.function.ThrowingRunnable;
import org.w3c.dom.Node; import org.w3c.dom.Node;
import javax.imageio.IIOException;
import javax.imageio.metadata.IIOMetadataFormatImpl;
import javax.imageio.metadata.IIOMetadataNode;
import java.awt.image.IndexColorModel;
import java.nio.charset.StandardCharsets;
import static org.junit.Assert.*;
public class IFFImageMetadataTest { public class IFFImageMetadataTest {
@Test @Test
public void testStandardFeatures() { public void testStandardFeatures() throws IIOException {
BMHDChunk header = new BMHDChunk(300, 200, 24, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0); Form header = Form.ofType(IFF.TYPE_ILBM)
.with(new BMHDChunk(300, 200, 24, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0));
final IFFImageMetadata metadata = new IFFImageMetadata(IFF.TYPE_ILBM, header, null, null, Collections.<GenericChunk>emptyList()); final IFFImageMetadata metadata = new IFFImageMetadata(header, null);
// Standard metadata format // Standard metadata format
assertTrue(metadata.isStandardMetadataFormatSupported()); assertTrue(metadata.isStandardMetadataFormatSupported());
@@ -49,10 +47,11 @@ public class IFFImageMetadataTest {
} }
@Test @Test
public void testStandardChromaGray() { public void testStandardChromaGray() throws IIOException {
BMHDChunk header = new BMHDChunk(300, 200, 8, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0); Form header = Form.ofType(IFF.TYPE_ILBM)
.with(new BMHDChunk(300, 200, 8, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0));
IFFImageMetadata metadata = new IFFImageMetadata(IFF.TYPE_ILBM, header, null, null, Collections.<GenericChunk>emptyList()); IFFImageMetadata metadata = new IFFImageMetadata(header, null);
IIOMetadataNode chroma = metadata.getStandardChromaNode(); IIOMetadataNode chroma = metadata.getStandardChromaNode();
assertNotNull(chroma); assertNotNull(chroma);
@@ -75,10 +74,11 @@ public class IFFImageMetadataTest {
} }
@Test @Test
public void testStandardChromaRGB() { public void testStandardChromaRGB() throws IIOException {
BMHDChunk header = new BMHDChunk(300, 200, 24, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0); Form header = Form.ofType(IFF.TYPE_ILBM)
.with(new BMHDChunk(300, 200, 24, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0));
IFFImageMetadata metadata = new IFFImageMetadata(IFF.TYPE_ILBM, header, null, null, Collections.<GenericChunk>emptyList()); IFFImageMetadata metadata = new IFFImageMetadata(header, null);
IIOMetadataNode chroma = metadata.getStandardChromaNode(); IIOMetadataNode chroma = metadata.getStandardChromaNode();
assertNotNull(chroma); assertNotNull(chroma);
@@ -101,16 +101,17 @@ public class IFFImageMetadataTest {
} }
@Test @Test
public void testStandardChromaPalette() { public void testStandardChromaPalette() throws IIOException {
BMHDChunk header = new BMHDChunk(300, 200, 1, BMHDChunk.MASK_TRANSPARENT_COLOR, BMHDChunk.COMPRESSION_BYTE_RUN, 1); Form header = Form.ofType(IFF.TYPE_ILBM)
.with(new BMHDChunk(300, 200, 1, BMHDChunk.MASK_TRANSPARENT_COLOR, BMHDChunk.COMPRESSION_BYTE_RUN, 1));
byte[] bw = {0, (byte) 0xff}; byte[] bw = {0, (byte) 0xff};
IFFImageMetadata metadata = new IFFImageMetadata(IFF.TYPE_ILBM, header, new IndexColorModel(header.bitplanes, bw.length, bw, bw, bw, header.transparentIndex), null, Collections.<GenericChunk>emptyList()); IFFImageMetadata metadata = new IFFImageMetadata(header, new IndexColorModel(header.bitplanes(), bw.length, bw, bw, bw, header.transparentIndex()));
IIOMetadataNode chroma = metadata.getStandardChromaNode(); IIOMetadataNode chroma = metadata.getStandardChromaNode();
assertNotNull(chroma); assertNotNull(chroma);
assertEquals("Chroma", chroma.getNodeName()); assertEquals("Chroma", chroma.getNodeName());
assertEquals(4, chroma.getLength()); assertEquals(5, chroma.getLength());
IIOMetadataNode colorSpaceType = (IIOMetadataNode) chroma.getFirstChild(); IIOMetadataNode colorSpaceType = (IIOMetadataNode) chroma.getFirstChild();
assertEquals("ColorSpaceType", colorSpaceType.getNodeName()); assertEquals("ColorSpaceType", colorSpaceType.getNodeName());
@@ -138,14 +139,21 @@ public class IFFImageMetadataTest {
assertEquals(rgb, item0.getAttribute("blue")); assertEquals(rgb, item0.getAttribute("blue"));
} }
// TODO: BackgroundIndex == 1?? // BackgroundIndex == 1
IIOMetadataNode backgroundIndex = (IIOMetadataNode) palette.getNextSibling();
assertEquals("BackgroundIndex", backgroundIndex.getNodeName());
assertEquals("1", backgroundIndex.getAttribute("value"));
// No more elements
assertNull(backgroundIndex.getNextSibling());
} }
@Test @Test
public void testStandardCompressionRLE() { public void testStandardCompressionRLE() throws IIOException {
BMHDChunk header = new BMHDChunk(300, 200, 24, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0); Form header = Form.ofType(IFF.TYPE_ILBM)
.with(new BMHDChunk(300, 200, 24, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0));
IFFImageMetadata metadata = new IFFImageMetadata(IFF.TYPE_ILBM, header, null, null, Collections.<GenericChunk>emptyList()); IFFImageMetadata metadata = new IFFImageMetadata(header, null);
IIOMetadataNode compression = metadata.getStandardCompressionNode(); IIOMetadataNode compression = metadata.getStandardCompressionNode();
assertNotNull(compression); assertNotNull(compression);
@@ -164,19 +172,21 @@ public class IFFImageMetadataTest {
} }
@Test @Test
public void testStandardCompressionNone() { public void testStandardCompressionNone() throws IIOException {
BMHDChunk header = new BMHDChunk(300, 200, 24, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_NONE, 0); Form header = Form.ofType(IFF.TYPE_ILBM)
.with(new BMHDChunk(300, 200, 24, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_NONE, 0));
IFFImageMetadata metadata = new IFFImageMetadata(IFF.TYPE_ILBM, header, null, null, Collections.<GenericChunk>emptyList()); IFFImageMetadata metadata = new IFFImageMetadata(header, null);
assertNull(metadata.getStandardCompressionNode()); // No compression, all default... assertNull(metadata.getStandardCompressionNode()); // No compression, all default...
} }
@Test @Test
public void testStandardDataILBM_Gray() { public void testStandardDataILBM_Gray() throws IIOException {
BMHDChunk header = new BMHDChunk(300, 200, 8, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0); Form header = Form.ofType(IFF.TYPE_ILBM)
.with(new BMHDChunk(300, 200, 8, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0));
IFFImageMetadata metadata = new IFFImageMetadata(IFF.TYPE_ILBM, header, null, null, Collections.<GenericChunk>emptyList()); IFFImageMetadata metadata = new IFFImageMetadata(header, null);
IIOMetadataNode data = metadata.getStandardDataNode(); IIOMetadataNode data = metadata.getStandardDataNode();
assertNotNull(data); assertNotNull(data);
@@ -199,10 +209,11 @@ public class IFFImageMetadataTest {
} }
@Test @Test
public void testStandardDataILBM_RGB() { public void testStandardDataILBM_RGB() throws IIOException {
BMHDChunk header = new BMHDChunk(300, 200, 24, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0); Form header = Form.ofType(IFF.TYPE_ILBM)
.with(new BMHDChunk(300, 200, 24, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0));
IFFImageMetadata metadata = new IFFImageMetadata(IFF.TYPE_ILBM, header, null, null, Collections.<GenericChunk>emptyList()); IFFImageMetadata metadata = new IFFImageMetadata(header, null);
IIOMetadataNode data = metadata.getStandardDataNode(); IIOMetadataNode data = metadata.getStandardDataNode();
assertNotNull(data); assertNotNull(data);
@@ -225,10 +236,11 @@ public class IFFImageMetadataTest {
} }
@Test @Test
public void testStandardDataILBM_RGBA() { public void testStandardDataILBM_RGBA() throws IIOException {
BMHDChunk header = new BMHDChunk(300, 200, 32, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0); Form header = Form.ofType(IFF.TYPE_ILBM)
.with(new BMHDChunk(300, 200, 32, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0));
IFFImageMetadata metadata = new IFFImageMetadata(IFF.TYPE_ILBM, header, null, null, Collections.<GenericChunk>emptyList()); IFFImageMetadata metadata = new IFFImageMetadata(header, null);
IIOMetadataNode data = metadata.getStandardDataNode(); IIOMetadataNode data = metadata.getStandardDataNode();
assertNotNull(data); assertNotNull(data);
@@ -251,12 +263,13 @@ public class IFFImageMetadataTest {
} }
@Test @Test
public void testStandardDataILBM_Palette() { public void testStandardDataILBM_Palette() throws IIOException {
for (int i = 1; i <= 8; i++) { for (int i = 1; i <= 8; i++) {
BMHDChunk header = new BMHDChunk(300, 200, i, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0); Form header = Form.ofType(IFF.TYPE_ILBM)
.with(new BMHDChunk(300, 200, i, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0));
byte[] rgb = new byte[2 << i]; // Colors doesn't really matter here byte[] rgb = new byte[2 << i]; // Colors doesn't really matter here
IFFImageMetadata metadata = new IFFImageMetadata(IFF.TYPE_ILBM, header, new IndexColorModel(header.bitplanes, rgb.length, rgb, rgb, rgb, 0), null, Collections.<GenericChunk>emptyList()); IFFImageMetadata metadata = new IFFImageMetadata(header, new IndexColorModel(header.bitplanes(), rgb.length, rgb, rgb, rgb, 0));
IIOMetadataNode data = metadata.getStandardDataNode(); IIOMetadataNode data = metadata.getStandardDataNode();
assertNotNull(data); assertNotNull(data);
@@ -280,10 +293,11 @@ public class IFFImageMetadataTest {
} }
@Test @Test
public void testStandardDataPBM_Gray() { public void testStandardDataPBM_Gray() throws IIOException {
BMHDChunk header = new BMHDChunk(300, 200, 8, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0); Form header = Form.ofType(IFF.TYPE_PBM)
.with(new BMHDChunk(300, 200, 8, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0));
IFFImageMetadata metadata = new IFFImageMetadata(IFF.TYPE_PBM, header, null, null, Collections.<GenericChunk>emptyList()); IFFImageMetadata metadata = new IFFImageMetadata(header, null);
IIOMetadataNode data = metadata.getStandardDataNode(); IIOMetadataNode data = metadata.getStandardDataNode();
assertNotNull(data); assertNotNull(data);
@@ -306,10 +320,11 @@ public class IFFImageMetadataTest {
} }
@Test @Test
public void testStandardDataPBM_RGB() { public void testStandardDataPBM_RGB() throws IIOException {
BMHDChunk header = new BMHDChunk(300, 200, 24, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0); Form header = Form.ofType(IFF.TYPE_PBM)
.with(new BMHDChunk(300, 200, 24, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0));
IFFImageMetadata metadata = new IFFImageMetadata(IFF.TYPE_PBM, header, null, null, Collections.<GenericChunk>emptyList()); IFFImageMetadata metadata = new IFFImageMetadata(header, null);
IIOMetadataNode data = metadata.getStandardDataNode(); IIOMetadataNode data = metadata.getStandardDataNode();
assertNotNull(data); assertNotNull(data);
@@ -333,40 +348,57 @@ public class IFFImageMetadataTest {
@Test @Test
public void testStandardDimensionNoViewport() { public void testStandardDimensionNoViewport() throws IIOException {
BMHDChunk header = new BMHDChunk(300, 200, 8, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0); BMHDChunk bitmapHeader = new BMHDChunk(300, 200, 8, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0);
bitmapHeader.xAspect = 0;
bitmapHeader.yAspect = 0;
IFFImageMetadata metadata = new IFFImageMetadata(IFF.TYPE_ILBM, header, null, null, Collections.<GenericChunk>emptyList()); Form header = Form.ofType(IFF.TYPE_ILBM)
.with(bitmapHeader);
IFFImageMetadata metadata = new IFFImageMetadata(header, null);
IIOMetadataNode dimension = metadata.getStandardDimensionNode(); IIOMetadataNode dimension = metadata.getStandardDimensionNode();
assertNull(dimension); assertNull(dimension);
} }
@Test @Test
public void testStandardDimensionNormal() { public void testStandardDimensionNormal() throws IIOException {
BMHDChunk header = new BMHDChunk(300, 200, 8, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0); Form header = Form.ofType(IFF.TYPE_ILBM)
.with(new BMHDChunk(300, 200, 8, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0))
.with(new CAMGChunk(4));
IFFImageMetadata metadata = new IFFImageMetadata(IFF.TYPE_ILBM, header, null, new CAMGChunk(4), Collections.<GenericChunk>emptyList()); IFFImageMetadata metadata = new IFFImageMetadata(header, null);
IIOMetadataNode dimension = metadata.getStandardDimensionNode(); IIOMetadataNode dimension = metadata.getStandardDimensionNode();
assertNotNull(dimension);
assertEquals("Dimension", dimension.getNodeName());
assertEquals(1, dimension.getLength());
IIOMetadataNode pixelAspectRatio = (IIOMetadataNode) dimension.getFirstChild(); // No Dimension node is okay, or one with an aspect ratio of 1.0
assertEquals("PixelAspectRatio", pixelAspectRatio.getNodeName()); if (dimension != null) {
assertEquals("1.0", pixelAspectRatio.getAttribute("value")); assertEquals("Dimension", dimension.getNodeName());
assertEquals(1, dimension.getLength());
assertNull(pixelAspectRatio.getNextSibling()); // No more children IIOMetadataNode pixelAspectRatio = (IIOMetadataNode) dimension.getFirstChild();
assertEquals("PixelAspectRatio", pixelAspectRatio.getNodeName());
assertEquals("1.0", pixelAspectRatio.getAttribute("value"));
assertNull(pixelAspectRatio.getNextSibling()); // No more children
}
} }
@Test @Test
public void testStandardDimensionHires() { public void testStandardDimensionHires() throws IIOException {
BMHDChunk header = new BMHDChunk(300, 200, 8, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0); BMHDChunk bitmapHeader = new BMHDChunk(300, 200, 8, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0);
bitmapHeader.xAspect = 2;
bitmapHeader.yAspect = 1;
CAMGChunk viewPort = new CAMGChunk(4); CAMGChunk viewPort = new CAMGChunk(4);
viewPort.camg = 0x8000; viewPort.camg = 0x8000;
IFFImageMetadata metadata = new IFFImageMetadata(IFF.TYPE_ILBM, header, null, viewPort, Collections.<GenericChunk>emptyList()); Form header = Form.ofType(IFF.TYPE_ILBM)
.with(bitmapHeader)
.with(viewPort);
IFFImageMetadata metadata = new IFFImageMetadata(header, null);
IIOMetadataNode dimension = metadata.getStandardDimensionNode(); IIOMetadataNode dimension = metadata.getStandardDimensionNode();
assertNotNull(dimension); assertNotNull(dimension);
@@ -381,13 +413,19 @@ public class IFFImageMetadataTest {
} }
@Test @Test
public void testStandardDimensionInterlaced() { public void testStandardDimensionInterlaced() throws IIOException {
BMHDChunk header = new BMHDChunk(300, 200, 8, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0); BMHDChunk bitmapHeader = new BMHDChunk(300, 200, 8, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0);
bitmapHeader.xAspect = 1;
bitmapHeader.yAspect = 2;
CAMGChunk viewPort = new CAMGChunk(4); CAMGChunk viewPort = new CAMGChunk(4);
viewPort.camg = 0x4; viewPort.camg = 0x4;
IFFImageMetadata metadata = new IFFImageMetadata(IFF.TYPE_ILBM, header, null, viewPort, Collections.<GenericChunk>emptyList()); Form header = Form.ofType(IFF.TYPE_ILBM)
.with(bitmapHeader)
.with(viewPort);
IFFImageMetadata metadata = new IFFImageMetadata(header, null);
IIOMetadataNode dimension = metadata.getStandardDimensionNode(); IIOMetadataNode dimension = metadata.getStandardDimensionNode();
assertNotNull(dimension); assertNotNull(dimension);
@@ -402,13 +440,14 @@ public class IFFImageMetadataTest {
} }
@Test @Test
public void testStandardDimensionHiresInterlaced() { public void testStandardDimensionHiresInterlaced() throws IIOException {
BMHDChunk header = new BMHDChunk(300, 200, 8, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0);
CAMGChunk viewPort = new CAMGChunk(4); CAMGChunk viewPort = new CAMGChunk(4);
viewPort.camg = 0x8004; viewPort.camg = 0x8004;
Form header = Form.ofType(IFF.TYPE_ILBM)
.with(new BMHDChunk(300, 200, 8, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0))
.with(viewPort);
IFFImageMetadata metadata = new IFFImageMetadata(IFF.TYPE_ILBM, header, null, viewPort, Collections.<GenericChunk>emptyList()); IFFImageMetadata metadata = new IFFImageMetadata(header, null);
IIOMetadataNode dimension = metadata.getStandardDimensionNode(); IIOMetadataNode dimension = metadata.getStandardDimensionNode();
assertNotNull(dimension); assertNotNull(dimension);
@@ -423,10 +462,11 @@ public class IFFImageMetadataTest {
} }
@Test @Test
public void testStandardDocument() { public void testStandardDocument() throws IIOException {
BMHDChunk header = new BMHDChunk(300, 200, 8, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0); Form header = Form.ofType(IFF.TYPE_ILBM)
.with(new BMHDChunk(300, 200, 8, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0));
IFFImageMetadata metadata = new IFFImageMetadata(IFF.TYPE_ILBM, header, null, null, Collections.<GenericChunk>emptyList()); IFFImageMetadata metadata = new IFFImageMetadata(header, null);
IIOMetadataNode document = metadata.getStandardDocumentNode(); IIOMetadataNode document = metadata.getStandardDocumentNode();
assertNotNull(document); assertNotNull(document);
@@ -441,13 +481,15 @@ public class IFFImageMetadataTest {
} }
@Test @Test
public void testStandardText() { public void testStandardText() throws IIOException {
BMHDChunk header = new BMHDChunk(300, 200, 8, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0); int[] chunks = {IFF.CHUNK_ANNO, IFF.CHUNK_UTF8};
String[] texts = {"annotation", "äñnótâtïøñ"}; String[] texts = {"annotation", "äñnótâtïøñ"};
List<GenericChunk> meta = Arrays.asList(new GenericChunk(IFF.CHUNK_ANNO, texts[0].getBytes(StandardCharsets.US_ASCII)), Form header = Form.ofType(IFF.TYPE_ILBM)
new GenericChunk(IFF.CHUNK_UTF8, texts[1].getBytes(StandardCharsets.UTF_8))); .with(new BMHDChunk(300, 200, 8, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0))
IFFImageMetadata metadata = new IFFImageMetadata(IFF.TYPE_ILBM, header, null, null, meta); .with(new GenericChunk(chunks[0], texts[0].getBytes(StandardCharsets.US_ASCII)))
.with(new GenericChunk(chunks[1], texts[1].getBytes(StandardCharsets.UTF_8)));
IFFImageMetadata metadata = new IFFImageMetadata(header, null);
IIOMetadataNode text = metadata.getStandardTextNode(); IIOMetadataNode text = metadata.getStandardTextNode();
assertNotNull(text); assertNotNull(text);
@@ -457,26 +499,28 @@ public class IFFImageMetadataTest {
for (int i = 0; i < texts.length; i++) { for (int i = 0; i < texts.length; i++) {
IIOMetadataNode textEntry = (IIOMetadataNode) text.item(i); IIOMetadataNode textEntry = (IIOMetadataNode) text.item(i);
assertEquals("TextEntry", textEntry.getNodeName()); assertEquals("TextEntry", textEntry.getNodeName());
assertEquals(IFFUtil.toChunkStr(meta.get(i).chunkId), textEntry.getAttribute("keyword")); assertEquals(IFFUtil.toChunkStr(chunks[i]), textEntry.getAttribute("keyword"));
assertEquals(texts[i], textEntry.getAttribute("value")); assertEquals(texts[i], textEntry.getAttribute("value"));
} }
} }
@Test @Test
public void testStandardTransparencyRGB() { public void testStandardTransparencyRGB() throws IIOException {
BMHDChunk header = new BMHDChunk(300, 200, 24, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0); Form header = Form.ofType(IFF.TYPE_ILBM)
.with(new BMHDChunk(300, 200, 24, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0));
IFFImageMetadata metadata = new IFFImageMetadata(IFF.TYPE_ILBM, header, null, null, Collections.<GenericChunk>emptyList()); IFFImageMetadata metadata = new IFFImageMetadata(header, null);
IIOMetadataNode transparency = metadata.getStandardTransparencyNode(); IIOMetadataNode transparency = metadata.getStandardTransparencyNode();
assertNull(transparency); // No transparency, just defaults assertNull(transparency); // No transparency, just defaults
} }
@Test @Test
public void testStandardTransparencyRGBA() { public void testStandardTransparencyRGBA() throws IIOException {
BMHDChunk header = new BMHDChunk(300, 200, 32, BMHDChunk.MASK_HAS_MASK, BMHDChunk.COMPRESSION_BYTE_RUN, 0); Form header = Form.ofType(IFF.TYPE_ILBM)
.with(new BMHDChunk(300, 200, 32, BMHDChunk.MASK_HAS_MASK, BMHDChunk.COMPRESSION_BYTE_RUN, 0));
IFFImageMetadata metadata = new IFFImageMetadata(IFF.TYPE_ILBM, header, null, null, Collections.<GenericChunk>emptyList()); IFFImageMetadata metadata = new IFFImageMetadata(header, null);
IIOMetadataNode transparency = metadata.getStandardTransparencyNode(); IIOMetadataNode transparency = metadata.getStandardTransparencyNode();
assertNotNull(transparency); assertNotNull(transparency);
@@ -491,11 +535,12 @@ public class IFFImageMetadataTest {
} }
@Test @Test
public void testStandardTransparencyPalette() { public void testStandardTransparencyPalette() throws IIOException {
BMHDChunk header = new BMHDChunk(300, 200, 1, BMHDChunk.MASK_TRANSPARENT_COLOR, BMHDChunk.COMPRESSION_BYTE_RUN, 1); Form header = Form.ofType(IFF.TYPE_ILBM)
.with(new BMHDChunk(300, 200, 1, BMHDChunk.MASK_TRANSPARENT_COLOR, BMHDChunk.COMPRESSION_BYTE_RUN, 1));
byte[] bw = {0, (byte) 0xff}; byte[] bw = {0, (byte) 0xff};
IFFImageMetadata metadata = new IFFImageMetadata(IFF.TYPE_ILBM, header, new IndexColorModel(header.bitplanes, bw.length, bw, bw, bw, header.transparentIndex), null, Collections.<GenericChunk>emptyList()); IFFImageMetadata metadata = new IFFImageMetadata(header, new IndexColorModel(header.bitplanes(), bw.length, bw, bw, bw, header.transparentIndex()));
IIOMetadataNode transparency = metadata.getStandardTransparencyNode(); IIOMetadataNode transparency = metadata.getStandardTransparencyNode();
assertNotNull(transparency); assertNotNull(transparency);
@@ -508,4 +553,131 @@ public class IFFImageMetadataTest {
assertNull(pixelAspectRatio.getNextSibling()); // No more children assertNull(pixelAspectRatio.getNextSibling()); // No more children
} }
@Test
public void testStandardRGB8() throws IIOException {
Form header = Form.ofType(IFF.TYPE_RGB8)
.with(new BMHDChunk(300, 200, 25, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0));
IFFImageMetadata metadata = new IFFImageMetadata(header, null);
// Chroma
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("4", numChannels.getAttribute("value"));
IIOMetadataNode blackIsZero = (IIOMetadataNode) numChannels.getNextSibling();
assertEquals("BlackIsZero", blackIsZero.getNodeName());
assertEquals("TRUE", blackIsZero.getAttribute("value"));
assertNull(blackIsZero.getNextSibling()); // No more children
// Data
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 sampleFormat = (IIOMetadataNode) planarConfiguration.getNextSibling();
assertEquals("SampleFormat", sampleFormat.getNodeName());
assertEquals("UnsignedIntegral", sampleFormat.getAttribute("value"));
IIOMetadataNode bitsPerSample = (IIOMetadataNode) sampleFormat.getNextSibling();
assertEquals("BitsPerSample", bitsPerSample.getNodeName());
assertEquals("8 8 8 1", bitsPerSample.getAttribute("value"));
assertNull(bitsPerSample.getNextSibling()); // No more children
// Transparency
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 testStandardDEEP() throws IIOException {
DPELChunk dpel = new DPELChunk(20);
dpel.typeDepths = new DPELChunk.TypeDepth[4];
for (int i = 0; i < dpel.typeDepths.length; i++) {
dpel.typeDepths[i] = new DPELChunk.TypeDepth(i == 0 ? 11 : i, 8);
}
Form header = Form.ofType(IFF.TYPE_DEEP)
.with(new DGBLChunk(8))
.with(dpel);
IFFImageMetadata metadata = new IFFImageMetadata(header, null);
// Chroma
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("4", numChannels.getAttribute("value"));
IIOMetadataNode blackIsZero = (IIOMetadataNode) numChannels.getNextSibling();
assertEquals("BlackIsZero", blackIsZero.getNodeName());
assertEquals("TRUE", blackIsZero.getAttribute("value"));
// TODO: BackgroundColor = 0x666666
assertNull(blackIsZero.getNextSibling()); // No more children
// Data
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 sampleFormat = (IIOMetadataNode) planarConfiguration.getNextSibling();
assertEquals("SampleFormat", sampleFormat.getNodeName());
assertEquals("UnsignedIntegral", sampleFormat.getAttribute("value"));
IIOMetadataNode bitsPerSample = (IIOMetadataNode) sampleFormat.getNextSibling();
assertEquals("BitsPerSample", bitsPerSample.getNodeName());
assertEquals("8 8 8 8", bitsPerSample.getAttribute("value"));
assertNull(bitsPerSample.getNextSibling()); // No more children
// Transparency
IIOMetadataNode transparency = metadata.getStandardTransparencyNode();
assertNotNull(transparency);
assertEquals("Transparency", transparency.getNodeName());
assertEquals(1, transparency.getLength());
IIOMetadataNode alpha = (IIOMetadataNode) transparency.getFirstChild();
assertEquals("Alpha", alpha.getNodeName());
assertEquals("premultiplied", alpha.getAttribute("value"));
assertNull(alpha.getNextSibling()); // No more children
}
} }
@@ -92,7 +92,12 @@ public class IFFImageReaderTest extends ImageReaderAbstractTest<IFFImageReader>
// Impulse RGB8 format straight from Imagine 2.0 // Impulse RGB8 format straight from Imagine 2.0
new TestData(getClassLoaderResource("/iff/glowsphere2.rgb8"), new Dimension(640, 480)), new TestData(getClassLoaderResource("/iff/glowsphere2.rgb8"), new Dimension(640, 480)),
// Impulse RGB8 format written by ASDG ADPro, with cross boundary runs, which is probably not as per spec... // Impulse RGB8 format written by ASDG ADPro, with cross boundary runs, which is probably not as per spec...
new TestData(getClassLoaderResource("/iff/tunnel04-adpro-cross-boundary-runs.rgb8"), new Dimension(640, 480)) new TestData(getClassLoaderResource("/iff/tunnel04-adpro-cross-boundary-runs.rgb8"), new Dimension(640, 480)),
// TVPaint (TecSoft) DEEP format
new TestData(getClassLoaderResource("/iff/arch.deep"), new Dimension(800, 600)),
// TVPaint Project (TVPP is effectively same as the DEEP format, but multiple layers, background color etc.)
// TODO: This file contains one more image/layer, second DBOD chunk @1868908, len: 1199144!
new TestData(getClassLoaderResource("/iff/warm-and-bright.pro"), new Dimension(800, 600))
); );
} }
@@ -103,7 +108,7 @@ public class IFFImageReaderTest extends ImageReaderAbstractTest<IFFImageReader>
@Override @Override
protected List<String> getSuffixes() { protected List<String> getSuffixes() {
return Arrays.asList("iff", "ilbm", "ham", "ham8", "lbm"); return Arrays.asList("iff", "ilbm", "ham", "ham8", "lbm", "rgb8", "deep");
} }
@Override @Override
@@ -138,9 +143,14 @@ public class IFFImageReaderTest extends ImageReaderAbstractTest<IFFImageReader>
for (int i = 0; i < 32; i++) { for (int i = 0; i < 32; i++) {
// Make sure the color model is really EHB // Make sure the color model is really EHB
assertEquals("red", (reds[i] & 0xff) / 2, reds[i + 32] & 0xff); try {
assertEquals("blue", (blues[i] & 0xff) / 2, blues[i + 32] & 0xff); assertEquals("red", (reds[i] & 0xff) / 2, reds[i + 32] & 0xff);
assertEquals("green", (greens[i] & 0xff) / 2, greens[i + 32] & 0xff); assertEquals("blue", (blues[i] & 0xff) / 2, blues[i + 32] & 0xff);
assertEquals("green", (greens[i] & 0xff) / 2, greens[i + 32] & 0xff);
}
catch (AssertionError err) {
throw new AssertionError("Color " + i + " " + err.getMessage(), err);
}
} }
} }
} }
Binary file not shown.