IFF: Read support for TVPaint DEEP and TVPP

+ Bonus: Massive code clean-up/refactor.

(cherry picked from commit 73ad024833ed74618b81f44c95614845defc1282)
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

View File

@ -69,14 +69,14 @@ abstract class AbstractMultiPaletteChunk extends IFFChunk implements MultiPalett
protected WeakReference<IndexColorModel> originalPalette;
protected MutableIndexColorModel mutablePalette;
public AbstractMultiPaletteChunk(int pChunkId, int pChunkLength) {
super(pChunkId, pChunkLength);
public AbstractMultiPaletteChunk(int chunkId, int chunkLength) {
super(chunkId, chunkLength);
}
@Override
void readChunk(final DataInput pInput) throws IOException {
void readChunk(final DataInput input) throws IOException {
if (chunkId == IFF.CHUNK_SHAM) {
pInput.readUnsignedShort(); // Version, typically 0, skipped
input.readUnsignedShort(); // Version, typically 0, skipped
}
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++ ) {
int data = pInput.readUnsignedShort();
int data = input.readUnsignedShort();
changes[row][i].index = i;
changes[row][i].r = (byte) (((data & 0x0f00) >> 8) * FACTOR_4BIT);
@ -102,7 +102,7 @@ abstract class AbstractMultiPaletteChunk extends IFFChunk implements MultiPalett
}
@Override
void writeChunk(DataOutput pOutput) {
void writeChunk(DataOutput output) {
throw new UnsupportedOperationException("Method writeChunk not implemented");
}

View File

@ -109,64 +109,65 @@ final class BMHDChunk extends IFFChunk {
int pageWidth;
int pageHeight;
BMHDChunk(int pChunkLength) {
super(IFF.CHUNK_BMHD, pChunkLength);
BMHDChunk(int chunkLength) {
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);
width = pWidth;
height = pHeight;
this.width = width;
this.height = height;
xPos = 0;
yPos = 0;
bitplanes = pBitplanes;
maskType = pMaskType;
compressionType = pCompressionType;
transparentIndex = pTransparentIndex;
this.bitplanes = bitplanes;
this.maskType = maskType;
this.compressionType = compressionType;
this.transparentIndex = transparentIndex;
xAspect = 1;
yAspect = 1;
pageWidth = Math.min(pWidth, Short.MAX_VALUE); // For some reason, these are signed?
pageHeight = Math.min(pHeight, Short.MAX_VALUE);
pageWidth = Math.min(width, Short.MAX_VALUE); // For some reason, these are signed?
pageHeight = Math.min(height, Short.MAX_VALUE);
}
@Override
void readChunk(final DataInput pInput) throws IOException {
void readChunk(final DataInput input) throws IOException {
if (chunkLength != 20) {
throw new IIOException("Unknown BMHD chunk length: " + chunkLength);
}
width = pInput.readUnsignedShort();
height = pInput.readUnsignedShort();
xPos = pInput.readShort();
yPos = pInput.readShort();
bitplanes = pInput.readUnsignedByte();
maskType = pInput.readUnsignedByte();
compressionType = pInput.readUnsignedByte();
pInput.readByte(); // PAD
transparentIndex = pInput.readUnsignedShort();
xAspect = pInput.readUnsignedByte();
yAspect = pInput.readUnsignedByte();
pageWidth = pInput.readShort();
pageHeight = pInput.readShort();
width = input.readUnsignedShort();
height = input.readUnsignedShort();
xPos = input.readShort();
yPos = input.readShort();
bitplanes = input.readUnsignedByte();
maskType = input.readUnsignedByte();
compressionType = input.readUnsignedByte();
input.readByte(); // PAD
transparentIndex = input.readUnsignedShort();
xAspect = input.readUnsignedByte();
yAspect = input.readUnsignedByte();
pageWidth = input.readShort();
pageHeight = input.readShort();
}
@Override
void writeChunk(final DataOutput pOutput) throws IOException {
pOutput.writeInt(chunkId);
pOutput.writeInt(chunkLength);
void writeChunk(final DataOutput output) throws IOException {
output.writeInt(chunkId);
output.writeInt(chunkLength);
pOutput.writeShort(width);
pOutput.writeShort(height);
pOutput.writeShort(xPos);
pOutput.writeShort(yPos);
pOutput.writeByte(bitplanes);
pOutput.writeByte(maskType);
pOutput.writeByte(compressionType);
pOutput.writeByte(0); // PAD
pOutput.writeShort(transparentIndex);
pOutput.writeByte(xAspect);
pOutput.writeByte(yAspect);
pOutput.writeShort(pageWidth);
pOutput.writeShort(pageHeight);
output.writeShort(width);
output.writeShort(height);
output.writeShort(xPos);
output.writeShort(yPos);
output.writeByte(bitplanes);
output.writeByte(maskType);
output.writeByte(compressionType);
output.writeByte(0); // PAD
output.writeShort(transparentIndex);
output.writeByte(xAspect);
output.writeByte(yAspect);
output.writeShort(pageWidth);
output.writeShort(pageHeight);
}
@Override

View File

@ -33,6 +33,8 @@ package com.twelvemonkeys.imageio.plugins.iff;
import java.io.DataInput;
import java.io.DataOutput;
import static com.twelvemonkeys.lang.Validate.isTrue;
/**
* BODYChunk
*
@ -40,17 +42,20 @@ import java.io.DataOutput;
* @version $Id: BODYChunk.java,v 1.0 28.feb.2006 01:25:49 haku Exp$
*/
final class BODYChunk extends IFFChunk {
BODYChunk(int pChunkLength) {
super(IFF.CHUNK_BODY, pChunkLength);
final long chunkOffset;
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
void readChunk(final DataInput pInput) {
void readChunk(final DataInput input) {
throw new InternalError("BODY chunk should only be read from IFFImageReader");
}
@Override
void writeChunk(final DataOutput pOutput) {
void writeChunk(final DataOutput output) {
throw new InternalError("BODY chunk should only be written from IFFImageWriter");
}
}

View File

@ -48,21 +48,21 @@ final class CAMGChunk extends IFFChunk {
int camg;
CAMGChunk(int pLength) {
super(IFF.CHUNK_CAMG, pLength);
CAMGChunk(int chunkLength) {
super(IFF.CHUNK_CAMG, chunkLength);
}
@Override
void readChunk(final DataInput pInput) throws IOException {
void readChunk(final DataInput input) throws IOException {
if (chunkLength != 4) {
throw new IIOException("Unknown CAMG chunk length: " + chunkLength);
}
camg = pInput.readInt();
camg = input.readInt();
}
@Override
void writeChunk(final DataOutput pOutput) {
void writeChunk(final DataOutput output) {
throw new InternalError("Not implemented: writeChunk()");
}

View File

@ -31,9 +31,7 @@
package com.twelvemonkeys.imageio.plugins.iff;
import javax.imageio.IIOException;
import java.awt.image.BufferedImage;
import java.awt.image.IndexColorModel;
import java.awt.image.WritableRaster;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
@ -69,7 +67,7 @@ final class CMAPChunk extends IFFChunk {
}
@Override
void readChunk(final DataInput pInput) throws IOException {
void readChunk(final DataInput input) throws IOException {
int numColors = chunkLength / 3;
reds = new byte[numColors];
@ -77,9 +75,9 @@ final class CMAPChunk extends IFFChunk {
blues = reds.clone();
for (int i = 0; i < numColors; i++) {
reds[i] = pInput.readByte();
greens[i] = pInput.readByte();
blues[i] = pInput.readByte();
reds[i] = input.readByte();
greens[i] = input.readByte();
blues[i] = input.readByte();
}
// 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...
if (chunkLength % 2 != 0) {
pInput.readByte();
input.readByte();
}
}
@Override
void writeChunk(final DataOutput pOutput) throws IOException {
pOutput.writeInt(chunkId);
pOutput.writeInt(chunkLength);
void writeChunk(final DataOutput output) throws IOException {
output.writeInt(chunkId);
output.writeInt(chunkLength);
final int length = model.getMapSize();
for (int i = 0; i < length; i++) {
pOutput.writeByte(model.getRed(i));
pOutput.writeByte(model.getGreen(i));
pOutput.writeByte(model.getBlue(i));
output.writeByte(model.getRed(i));
output.writeByte(model.getGreen(i));
output.writeByte(model.getBlue(i));
}
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 + "}";
}
BufferedImage createPaletteImage(final BMHDChunk header, boolean isEHB) 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 {
public IndexColorModel getIndexColorModel(final Form.ILBMForm header) throws IIOException {
if (model == null) {
int numColors = reds.length; // All arrays are same size
if (isEHB) {
if (header.isEHB()) {
if (numColors == 32) {
reds = Arrays.copyOf(reds, 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,
// with alpha, where all colors above the original color is all transparent?
// This is a waste of time and space, of course...
int transparent = header.maskType == BMHDChunk.MASK_TRANSPARENT_COLOR ? header.transparentIndex : -1;
int bitplanes = header.bitplanes == 25 ? 8 : header.bitplanes;
int transparent = header.transparentIndex();
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
}

View File

@ -38,7 +38,7 @@ package com.twelvemonkeys.imageio.plugins.iff;
* @version $Id: CTBLChunk.java,v 1.0 30.03.12 14:53 haraldk Exp$
*/
final class CTBLChunk extends AbstractMultiPaletteChunk {
CTBLChunk(int pChunkLength) {
super(IFF.CHUNK_CTBL, pChunkLength);
CTBLChunk(int chunkLength) {
super(IFF.CHUNK_CTBL, chunkLength);
}
}

View File

@ -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 +
'}';
}
}

View File

@ -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 + '}';
}
}

View File

@ -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 +
'}';
}
}
}

View File

@ -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 + '}';
}
}
}

View File

@ -50,25 +50,25 @@ final class GRABChunk extends IFFChunk {
Point2D point;
GRABChunk(int pChunkLength) {
super(IFF.CHUNK_GRAB, pChunkLength);
GRABChunk(int chunkLength) {
super(IFF.CHUNK_GRAB, chunkLength);
}
GRABChunk(Point2D pPoint) {
GRABChunk(Point2D point) {
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) {
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 {
pOutput.writeShort((int) point.getX());
pOutput.writeShort((int) point.getY());
void writeChunk(DataOutput output) throws IOException {
output.writeShort((int) point.getX());
output.writeShort((int) point.getY());
}
public String toString() {

View File

@ -44,31 +44,31 @@ final class GenericChunk extends IFFChunk {
byte[] data;
GenericChunk(int pChunkId, int pChunkLength) {
super(pChunkId, pChunkLength);
data = new byte[chunkLength];
GenericChunk(int chunkId, int chunkLength) {
super(chunkId, chunkLength);
data = new byte[this.chunkLength];
}
GenericChunk(int pChunkId, byte[] pChunkData) {
super(pChunkId, pChunkData.length);
data = pChunkData;
GenericChunk(int chunkId, byte[] chunkData) {
super(chunkId, chunkData.length);
data = chunkData;
}
@Override
void readChunk(final DataInput pInput) throws IOException {
pInput.readFully(data, 0, data.length);
void readChunk(final DataInput input) throws IOException {
input.readFully(data, 0, data.length);
skipData(pInput, chunkLength, data.length);
skipData(input, chunkLength, data.length);
}
@Override
void writeChunk(final DataOutput pOutput) throws IOException {
pOutput.writeInt(chunkId);
pOutput.writeInt(chunkLength);
pOutput.write(data, 0, data.length);
void writeChunk(final DataOutput output) throws IOException {
output.writeInt(chunkId);
output.writeInt(chunkLength);
output.write(data, 0, data.length);
if (data.length % 2 != 0) {
pOutput.writeByte(0); // PAD
output.writeByte(0); // PAD
}
}

View File

@ -49,6 +49,8 @@ interface IFF {
// TODO:
/** IFF DEEP form type (TVPaint) */
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) */
int TYPE_RGB8 = ('R' << 24) + ('G' << 16) + ('B' << 8) + '8';
/** IFF RGBN form type (TurboSilver) */
@ -92,7 +94,7 @@ interface IFF {
int CHUNK_COPY = ('(' << 24) + ('c' << 16) + (')' << 8) + ' ';
/** 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. */
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';
/** ACBM body chunk */
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';
/** 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';
}

View File

@ -44,25 +44,25 @@ abstract class IFFChunk {
int chunkId;
int chunkLength;
protected IFFChunk(int pChunkId, int pChunkLength) {
chunkId = pChunkId;
chunkLength = pChunkLength;
protected IFFChunk(int chunkId, int chunkLength) {
this.chunkId = chunkId;
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;
while (toSkip > 0) {
toSkip -= pInput.skipBytes(toSkip);
toSkip -= input.skipBytes(toSkip);
}
// Read pad
if (chunkLength % 2 != 0) {
pInput.readByte();
input.readByte();
}
}

View File

@ -13,31 +13,29 @@ import static com.twelvemonkeys.lang.Validate.isTrue;
import static com.twelvemonkeys.lang.Validate.notNull;
final class IFFImageMetadata extends AbstractMetadata {
private final int formType;
private final BMHDChunk header;
private final Form header;
private final IndexColorModel colorMap;
private final CAMGChunk viewPort;
private final List<GenericChunk> meta;
IFFImageMetadata(int formType, BMHDChunk header, IndexColorModel colorMap, CAMGChunk viewPort, List<GenericChunk> meta) {
this.formType = isTrue(validFormType(formType), formType, "Unknown IFF Form type: %s");
IFFImageMetadata(Form header, IndexColorModel colorMap) {
this.header = notNull(header, "header");
isTrue(validFormType(header.formType), header.formType, "Unknown IFF Form type: %s");
this.colorMap = colorMap;
this.viewPort = viewPort;
this.meta = meta;
this.meta = header.meta;
}
private boolean validFormType(int formType) {
switch (formType) {
default:
return false;
case TYPE_ACBM:
case TYPE_DEEP:
case TYPE_ILBM:
case TYPE_PBM:
case TYPE_RGB8:
case TYPE_RGBN:
case TYPE_TVPP:
return true;
default:
return false;
}
}
@ -48,7 +46,7 @@ final class IFFImageMetadata extends AbstractMetadata {
IIOMetadataNode csType = new IIOMetadataNode("ColorSpaceType");
chroma.appendChild(csType);
switch (header.bitplanes) {
switch (header.bitplanes()) {
case 8:
if (colorMap == null) {
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)
IIOMetadataNode numChannels = new IIOMetadataNode("NumChannels");
chroma.appendChild(numChannels);
if (colorMap == null && header.bitplanes == 8) {
if (colorMap == null && header.bitplanes() == 8) {
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));
}
else {
@ -103,9 +101,16 @@ final class IFFImageMetadata extends AbstractMetadata {
paletteEntry.setAttribute("green", Integer.toString(colorMap.getGreen(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) {
// Color background = new Color(extensions.getBackgroundColor(), true);
//
@ -122,7 +127,7 @@ final class IFFImageMetadata extends AbstractMetadata {
@Override
protected IIOMetadataNode getStandardCompressionNode() {
if (header.compressionType == BMHDChunk.COMPRESSION_NONE) {
if (header.compressionType() == BMHDChunk.COMPRESSION_NONE) {
return null; // All defaults
}
@ -145,7 +150,9 @@ final class IFFImageMetadata extends AbstractMetadata {
// PlanarConfiguration
IIOMetadataNode planarConfiguration = new IIOMetadataNode("PlanarConfiguration");
switch (formType) {
switch (header.formType) {
case TYPE_DEEP:
case TYPE_TVPP:
case TYPE_RGB8:
case TYPE_PBM:
planarConfiguration.setAttribute("value", "PixelInterleaved");
@ -154,7 +161,7 @@ final class IFFImageMetadata extends AbstractMetadata {
planarConfiguration.setAttribute("value", "PlaneInterleaved");
break;
default:
planarConfiguration.setAttribute("value", "Unknown " + IFFUtil.toChunkStr(formType));
planarConfiguration.setAttribute("value", "Unknown " + IFFUtil.toChunkStr(header.formType));
break;
}
data.appendChild(planarConfiguration);
@ -165,7 +172,7 @@ final class IFFImageMetadata extends AbstractMetadata {
// BitsPerSample
IIOMetadataNode bitsPerSample = new IIOMetadataNode("BitsPerSample");
String value = bitsPerSampleValue(header.bitplanes);
String value = bitsPerSampleValue(header.bitplanes());
bitsPerSample.setAttribute("value", value);
data.appendChild(bitsPerSample);
@ -173,7 +180,6 @@ final class IFFImageMetadata extends AbstractMetadata {
// SampleMSB not in format
return data;
}
private String bitsPerSampleValue(int bitplanes) {
@ -190,8 +196,8 @@ final class IFFImageMetadata extends AbstractMetadata {
case 24:
return "8 8 8";
case 25:
if (formType != TYPE_RGB8) {
throw new IllegalArgumentException(String.format("25 bit depth only supported for FORM type RGB8: %s", IFFUtil.toChunkStr(formType)));
if (header.formType != TYPE_RGB8) {
throw new IllegalArgumentException(String.format("25 bit depth only supported for FORM type RGB8: %s", IFFUtil.toChunkStr(header.formType)));
}
return "8 8 8 1";
@ -204,7 +210,7 @@ final class IFFImageMetadata extends AbstractMetadata {
@Override
protected IIOMetadataNode getStandardDimensionNode() {
if (viewPort == null) {
if (header.xAspect() == 0 || header.yAspect() == 0) {
return null;
}
@ -212,7 +218,7 @@ final class IFFImageMetadata extends AbstractMetadata {
// 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);
// TODO: HorizontalScreenSize?
@ -254,16 +260,15 @@ final class IFFImageMetadata extends AbstractMetadata {
@Override
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;
}
IIOMetadataNode transparency = new IIOMetadataNode("Transparency");
if (header.bitplanes == 25 || header.bitplanes == 32) {
if (header.bitplanes() == 25 || header.bitplanes() == 32) {
IIOMetadataNode alpha = new IIOMetadataNode("Alpha");
alpha.setAttribute("value", "nonpremultiplied");
alpha.setAttribute("value", header.premultiplied() ? "premultiplied" : "nonpremultiplied");
transparency.appendChild(alpha);
}

View File

@ -48,14 +48,15 @@ import java.awt.image.*;
import java.io.DataInputStream;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
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
* 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
* supported by allmost all image software for the Amiga computer.
* <p>
@ -104,20 +105,12 @@ public final class IFFImageReader extends ImageReaderBase {
// - Contains definitions of some "new" chunks, as well as alternative FORM types
// 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.
private BMHDChunk header;
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;
final static boolean DEBUG = "true".equalsIgnoreCase(System.getProperty("com.twelvemonkeys.imageio.plugins.iff.debug"));
private BufferedImage image;
private Form header;
private DataInputStream byteRunStream;
IFFImageReader(ImageReaderSpi pProvider) {
@ -135,35 +128,30 @@ public final class IFFImageReader extends ImageReaderBase {
@Override
protected void resetMembers() {
header = null;
colorMap = null;
paletteChange = null;
body = null;
viewPort = null;
formType = 0;
meta.clear();
image = null;
byteRunStream = null;
}
private void readMeta() throws IOException {
int chunkType = imageInput.readInt();
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
formType = imageInput.readInt();
if (formType != IFF.TYPE_ILBM && formType != IFF.TYPE_PBM && formType != IFF.TYPE_RGB8 && formType != IFF.TYPE_DEEP) {
throw new IIOException(String.format("Only IFF FORM types 'ILBM' and 'PBM ' supported: %s", IFFUtil.toChunkStr(formType)));
int formType = imageInput.readInt();
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", 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;
viewPort = null;
header = Form.ofType(formType);
// TODO: Delegate the FORM reading to the Form class or a FormReader class?
while (remaining > 0) {
int chunkId = imageInput.readInt();
int length = imageInput.readInt();
@ -171,104 +159,82 @@ public final class IFFImageReader extends ImageReaderBase {
remaining -= 8;
remaining -= length % 2 == 0 ? length : length + 1;
//System.out.println("Next chunk: " + toChunkStr(chunkId) + " length: " + length);
//System.out.println("Remaining bytes after chunk: " + remaining);
if (DEBUG) {
System.out.println("Next chunk: " + toChunkStr(chunkId) + " @ pos: " + (imageInput.getStreamPosition() - 8) + ", len: " + length);
System.out.println("Remaining bytes after chunk: " + remaining);
}
switch (chunkId) {
case IFF.CHUNK_BMHD:
if (header != null) {
throw new IIOException("Multiple BMHD chunks not allowed");
}
header = new BMHDChunk(length);
header.readChunk(imageInput);
//System.out.println(header);
BMHDChunk bitmapHeader = new BMHDChunk(length);
bitmapHeader.readChunk(imageInput);
header = header.with(bitmapHeader);
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:
if (colorMap != null) {
throw new IIOException("Multiple CMAP chunks not allowed");
}
colorMap = new CMAPChunk(length);
CMAPChunk colorMap = new CMAPChunk(length);
colorMap.readChunk(imageInput);
//System.out.println(colorMap);
header = header.with(colorMap);
break;
case IFF.CHUNK_GRAB:
if (grab != null) {
throw new IIOException("Multiple GRAB chunks not allowed");
}
grab = new GRABChunk(length);
GRABChunk grab = new GRABChunk(length);
grab.readChunk(imageInput);
//System.out.println(grab);
header = header.with(grab);
break;
case IFF.CHUNK_CAMG:
if (viewPort != null) {
throw new IIOException("Multiple CAMG chunks not allowed");
}
viewPort = new CAMGChunk(length);
viewPort.readChunk(imageInput);
// System.out.println(viewPort);
CAMGChunk viewMode = new CAMGChunk(length);
viewMode.readChunk(imageInput);
header = header.with(viewMode);
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);
pchg.readChunk(imageInput);
// Always prefer PCHG style palette changes
paletteChange = pchg;
// System.out.println(pchg);
header = header.with(pchg);
break;
case IFF.CHUNK_SHAM:
if (paletteChange instanceof SHAMChunk) {
throw new IIOException("Multiple SHAM chunks not allowed");
}
SHAMChunk sham = new SHAMChunk(length);
sham.readChunk(imageInput);
// NOTE: We prefer PHCG to SHAM style palette changes, if both are present
if (paletteChange == null) {
paletteChange = sham;
}
// System.out.println(sham);
header = header.with(sham);
break;
case IFF.CHUNK_CTBL:
if (paletteChange instanceof CTBLChunk) {
throw new IIOException("Multiple CTBL chunks not allowed");
}
CTBLChunk ctbl = new CTBLChunk(length);
ctbl.readChunk(imageInput);
// NOTE: We prefer PHCG to CTBL style palette changes, if both are present
if (paletteChange == null) {
paletteChange = ctbl;
}
// System.out.println(ctbl);
header = header.with(ctbl);
break;
case IFF.CHUNK_BODY:
if (body != null) {
throw new IIOException("Multiple BODY chunks not allowed");
}
body = new BODYChunk(length);
bodyStart = imageInput.getStreamPosition();
case IFF.CHUNK_DBOD:
BODYChunk body = new BODYChunk(chunkId, length, imageInput.getStreamPosition());
// 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
if (DEBUG) {
System.out.println("header = " + header);
}
return;
case IFF.CHUNK_ANNO:
@ -279,45 +245,32 @@ public final class IFFImageReader extends ImageReaderBase {
case IFF.CHUNK_UTF8:
GenericChunk generic = new GenericChunk(chunkId, length);
generic.readChunk(imageInput);
meta.add(generic);
// System.out.println(generic);
header = header.with(generic);
break;
case IFF.CHUNK_JUNK:
// Always skip junk chunks
default:
// TODO: SHAM, DEST, SPRT and more
// TODO: DEST, SPRT and more
// Everything else, we'll just skip
IFFChunk.skipData(imageInput, length, 0);
break;
}
}
if (DEBUG) {
System.out.println("header = " + header);
System.out.println("No BODY chunk found...");
}
}
@Override
public BufferedImage read(int pIndex, ImageReadParam pParam) throws IOException {
init(pIndex);
public BufferedImage read(int imageIndex, ImageReadParam param) throws IOException {
init(imageIndex);
processImageStarted(imageIndex);
processImageStarted(pIndex);
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;
BufferedImage result = getDestination(param, getImageTypes(imageIndex), getWidth(imageIndex), getHeight(imageIndex));
readBody(param, result);
processImageComplete();
@ -325,77 +278,81 @@ public final class IFFImageReader extends ImageReaderBase {
}
@Override
public int getWidth(int pIndex) throws IOException {
init(pIndex);
return header.width;
public int getWidth(int imageIndex) throws IOException {
init(imageIndex);
return header.width();
}
@Override
public int getHeight(int pIndex) throws IOException {
init(pIndex);
return header.height;
public int getHeight(int imageIndex) throws IOException {
init(imageIndex);
return header.height();
}
@Override
public IIOMetadata getImageMetadata(int imageIndex) throws IOException {
init(imageIndex);
return new IFFImageMetadata(formType, header, colorMap != null ? colorMap.getIndexColorModel(header, isEHB()) : null, viewPort, meta);
return new IFFImageMetadata(header, header.colorMap());
}
@Override
public Iterator<ImageTypeSpecifier> getImageTypes(int pIndex) throws IOException {
init(pIndex);
public Iterator<ImageTypeSpecifier> getImageTypes(int imageIndex) throws IOException {
init(imageIndex);
List<ImageTypeSpecifier> types = Arrays.asList(
getRawImageType(pIndex),
ImageTypeSpecifiers.createFromBufferedImageType(header.bitplanes == 32 ? BufferedImage.TYPE_4BYTE_ABGR : BufferedImage.TYPE_3BYTE_BGR)
// TODO: ImageTypeSpecifier.createFromBufferedImageType(header.bitplanes == 32 ? BufferedImage.TYPE_INT_ARGB : BufferedImage.TYPE_INT_RGB),
// TODO: Allow 32 bit always. Allow RGB and discard alpha, if present?
);
int bitplanes = header.bitplanes();
List<ImageTypeSpecifier> types =
header.formType == IFF.TYPE_DEEP || header.formType == IFF.TYPE_TVPP // TODO: Make a header attribute here
? Arrays.asList(
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();
}
@Override
public ImageTypeSpecifier getRawImageType(int pIndex) throws IOException {
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
ImageTypeSpecifier specifier;
switch (header.bitplanes) {
switch (header.bitplanes()) {
case 1:
// 1 bit
// -> 1 bit IndexColorModel
case 2:
// 2 bit
// -> 2 bit IndexColorModel
case 3:
case 4:
// 4 bit
// -> 4 bit IndexColorModel
case 5:
case 6:
// May be HAM6
// May be EHB
// May be EHB or HAM6
case 7:
case 8:
// 8 bit
// May be HAM8
if (!isConvertToRGB()) {
if (colorMap != null) {
IndexColorModel cm = colorMap.getIndexColorModel(header, isEHB());
return ImageTypeSpecifiers.createFromIndexColorModel(cm);
}
else {
return ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_BYTE_GRAY);
// otherwise -> 8 bit IndexColorModel
if (!needsConversionToRGB()) {
IndexColorModel indexColorModel = header.colorMap();
if (indexColorModel != null) {
return ImageTypeSpecifiers.createFromIndexColorModel(indexColorModel);
}
return ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_BYTE_GRAY);
}
// NOTE: HAM modes falls through, as they are converted to RGB
case 24:
// 24 bit RGB
return ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_3BYTE_BGR);
case 25: // TYPE_RGB8: 24 bit + 1 bit mask (we'll convert to full alpha during decoding)
if (formType != IFF.TYPE_RGB8) {
throw new IIOException(String.format("25 bit depth only supported for FORM type RGB8: %s", IFFUtil.toChunkStr(formType)));
case 25:
// For TYPE_RGB8: 24 bit + 1 bit mask (we'll convert to full alpha during decoding)
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),
@ -403,40 +360,49 @@ public final class IFFImageReader extends ImageReaderBase {
case 32:
// 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:
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() {
return isHAM() || isPCHG() || isSHAM();
private boolean needsConversionToRGB() {
return header.isHAM() || header.isMultiPalette();
}
private void readBody(final ImageReadParam pParam) throws IOException {
imageInput.seek(bodyStart);
private void readBody(final ImageReadParam param, final BufferedImage destination) throws IOException {
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;
if (formType == IFF.TYPE_RGB8) {
readRGB8(pParam, imageInput);
if (header.formType == IFF.TYPE_RGB8 || header.formType == IFF.TYPE_DEEP || header.formType == IFF.TYPE_TVPP) {
readChunky(param, destination, imageInput);
}
else if (colorMap != null) {
// NOTE: colorMap may be null for 8 bit (gray), 24 bit or 32 bit only
IndexColorModel cm = colorMap.getIndexColorModel(header, isEHB());
readIndexed(pParam, imageInput, cm);
else if (header.colorMap() != null) {
// NOTE: For ILBM types, colorMap may be null for 8 bit (gray), 24 bit or 32 bit only
IndexColorModel palette = header.colorMap();
readInterleavedIndexed(param, destination, palette, imageInput);
}
else {
readTrueColor(pParam, imageInput);
readInterleaved(param, destination, imageInput);
}
}
private void readIndexed(final ImageReadParam pParam, final ImageInputStream pInput, final IndexColorModel pModel) throws IOException {
final int width = header.width;
final int height = header.height;
private void readInterleavedIndexed(final ImageReadParam param, final BufferedImage destination, final IndexColorModel palette, final ImageInputStream input) throws IOException {
final int width = header.width();
final int height = header.height();
final Rectangle aoi = getSourceRegion(pParam, width, height);
final Point offset = pParam == null ? new Point(0, 0) : pParam.getDestinationOffset();
final Rectangle aoi = getSourceRegion(param, width, height);
final Point offset = param == null ? new Point(0, 0) : param.getDestinationOffset();
// Set everything to default values
int sourceXSubsampling = 1;
@ -445,20 +411,20 @@ public final class IFFImageReader extends ImageReaderBase {
int[] destinationBands = null;
// Get values from the ImageReadParam, if any
if (pParam != null) {
sourceXSubsampling = pParam.getSourceXSubsampling();
sourceYSubsampling = pParam.getSourceYSubsampling();
if (param != null) {
sourceXSubsampling = param.getSourceXSubsampling();
sourceYSubsampling = param.getSourceYSubsampling();
sourceBands = pParam.getSourceBands();
destinationBands = pParam.getDestinationBands();
sourceBands = param.getSourceBands();
destinationBands = param.getDestinationBands();
}
// 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) {
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.
@ -467,31 +433,31 @@ public final class IFFImageReader extends ImageReaderBase {
final byte[] planeData = new byte[8 * planeWidth];
ColorModel cm;
WritableRaster raster;
WritableRaster rowRaster;
if (isConvertToRGB()) {
// TODO: If HAM6, use type USHORT_444_RGB or 2BYTE_444_RGB?
// Or create a HAMColorModel, if at all possible?
if (needsConversionToRGB()) {
// TODO: Create a HAMColorModel, if at all possible?
// TYPE_3BYTE_BGR
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
);
// Create a byte raster with BGR order
raster = Raster.createInterleavedRaster(
DataBuffer.TYPE_BYTE, width, 1, width * 3, 3, new int[]{2, 1, 0}, null
rowRaster = Raster.createInterleavedRaster(
DataBuffer.TYPE_BYTE, width, 1, width * 3, 3, new int[] {2, 1, 0}, null
);
}
else {
// TYPE_BYTE_BINARY or TYPE_BYTE_INDEXED
cm = pModel;
raster = pModel.createCompatibleWritableRaster(width, 1);
cm = palette;
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[] data = ((DataBufferByte) raster.getDataBuffer()).getData();
final int planes = header.bitplanes;
final byte[] data = ((DataBufferByte) rowRaster.getDataBuffer()).getData();
final int planes = header.bitplanes();
Object dataElements = null;
Object outDataElements = null;
@ -499,7 +465,7 @@ public final class IFFImageReader extends ImageReaderBase {
for (int srcY = 0; srcY < height; srcY++) {
for (int p = 0; p < planes; p++) {
readPlaneData(pInput, planeData, p * planeWidth, planeWidth);
readPlaneData(planeData, p * planeWidth, planeWidth, input);
}
// Skip rows outside AOI
@ -510,72 +476,71 @@ public final class IFFImageReader extends ImageReaderBase {
return;
}
if (formType == IFF.TYPE_ILBM) {
if (header.formType == IFF.TYPE_ILBM) {
int pixelPos = 0;
for (int planePos = 0; planePos < planeWidth; planePos++) {
IFFUtil.bitRotateCW(planeData, planePos, planeWidth, row, pixelPos, 1);
pixelPos += 8;
}
if (isHAM()) {
hamToRGB(row, pModel, data, 0);
if (header.isHAM()) {
hamToRGB(row, palette, data, 0);
}
else if (isConvertToRGB()) {
multiPaletteToRGB(srcY, row, pModel, data, 0);
else if (needsConversionToRGB()) {
multiPaletteToRGB(srcY, row, palette, data, 0);
}
else {
raster.setDataElements(0, 0, width, 1, row);
rowRaster.setDataElements(0, 0, width, 1, row);
}
}
else if (formType == IFF.TYPE_PBM) {
raster.setDataElements(0, 0, width, 1, planeData);
else if (header.formType == IFF.TYPE_PBM) {
rowRaster.setDataElements(0, 0, width, 1, planeData);
}
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;
// Handle non-converting raster as special case for performance
if (cm.isCompatibleRaster(destination)) {
if (cm.isCompatibleRaster(destRaster)) {
// Rasters are compatible, just write to destination
if (sourceXSubsampling == 1) {
destination.setRect(offset.x, dstY, sourceRow);
destRaster.setRect(offset.x, dstY, sourceRow);
}
else {
for (int srcX = 0; srcX < sourceRow.getWidth(); srcX += sourceXSubsampling) {
dataElements = sourceRow.getDataElements(srcX, 0, dataElements);
int dstX = /*offset.x +*/ srcX / sourceXSubsampling;
destination.setDataElements(dstX, dstY, dataElements);
destRaster.setDataElements(dstX, dstY, dataElements);
}
}
}
else {
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;
for (int srcX = 0; srcX < sourceRow.getWidth(); srcX += sourceXSubsampling) {
dataElements = sourceRow.getDataElements(srcX, 0, dataElements);
int rgb = icm.getRGB(dataElements);
outDataElements = image.getColorModel().getDataElements(rgb, outDataElements);
outDataElements = destination.getColorModel().getDataElements(rgb, outDataElements);
int dstX = srcX / sourceXSubsampling;
destination.setDataElements(dstX, dstY, outDataElements);
destRaster.setDataElements(dstX, dstY, outDataElements);
}
}
else {
// TODO: This branch is never tested, and is probably "dead"
// ColorConvertOp
if (converter == null) {
converter = new ColorConvertOp(cm.getColorSpace(), image.getColorModel().getColorSpace(), null);
converter = new ColorConvertOp(cm.getColorSpace(), destination.getColorModel().getColorSpace(), null);
}
converter.filter(
raster.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)
rowRaster.createChild(aoi.x, 0, 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()) {
processReadAborted();
break;
@ -583,12 +548,12 @@ public final class IFFImageReader extends ImageReaderBase {
}
}
private void readRGB8(ImageReadParam pParam, ImageInputStream pInput) throws IOException {
final int width = header.width;
final int height = header.height;
private void readChunky(final ImageReadParam param, final BufferedImage destination, final ImageInputStream input) throws IOException {
final int width = header.width();
final int height = header.height();
final Rectangle aoi = getSourceRegion(pParam, width, height);
final Point offset = pParam == null ? new Point(0, 0) : pParam.getDestinationOffset();
final Rectangle aoi = getSourceRegion(param, width, height);
final Point offset = param == null ? new Point(0, 0) : param.getDestinationOffset();
// Set everything to default values
int sourceXSubsampling = 1;
@ -597,50 +562,49 @@ public final class IFFImageReader extends ImageReaderBase {
int[] destinationBands = null;
// Get values from the ImageReadParam, if any
if (pParam != null) {
sourceXSubsampling = pParam.getSourceXSubsampling();
sourceYSubsampling = pParam.getSourceYSubsampling();
if (param != null) {
sourceXSubsampling = param.getSourceXSubsampling();
sourceYSubsampling = param.getSourceYSubsampling();
sourceBands = pParam.getSourceBands();
destinationBands = pParam.getDestinationBands();
sourceBands = param.getSourceBands();
destinationBands = param.getDestinationBands();
}
// 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) {
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);
Raster sourceRow = raster.createChild(aoi.x, 0, aoi.width, 1, 0, 0, sourceBands);
ImageTypeSpecifier rawType = getRawImageType(0);
WritableRaster rowRaster = rawType.createBufferedImage(width, 1).getRaster();
Raster sourceRow = rowRaster.createChild(aoi.x, 0, aoi.width, 1, 0, 0, sourceBands);
int planeWidth = width * 4;
final byte[] data = ((DataBufferByte) raster.getDataBuffer()).getData();
final int channels = (header.bitplanes + 7) / 8;
final byte[] data = ((DataBufferByte) rowRaster.getDataBuffer()).getData();
Object dataElements = null;
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) {
int dstY = (srcY - aoi.y) / sourceYSubsampling;
if (sourceXSubsampling == 1) {
destination.setRect(0, dstY, sourceRow);
destRaster.setRect(0, dstY, sourceRow);
}
else {
for (int srcX = 0; srcX < sourceRow.getWidth(); srcX += sourceXSubsampling) {
dataElements = sourceRow.getDataElements(srcX, 0, dataElements);
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()) {
processReadAborted();
break;
@ -653,12 +617,12 @@ public final class IFFImageReader extends ImageReaderBase {
// 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
// significant bit of the blue value.
private void readTrueColor(ImageReadParam pParam, final ImageInputStream pInput) throws IOException {
final int width = header.width;
final int height = header.height;
private void readInterleaved(final ImageReadParam param, final BufferedImage destination, final ImageInputStream input) throws IOException {
final int width = header.width();
final int height = header.height();
final Rectangle aoi = getSourceRegion(pParam, width, height);
final Point offset = pParam == null ? new Point(0, 0) : pParam.getDestinationOffset();
final Rectangle aoi = getSourceRegion(param, width, height);
final Point offset = param == null ? new Point(0, 0) : param.getDestinationOffset();
// Set everything to default values
int sourceXSubsampling = 1;
@ -667,39 +631,39 @@ public final class IFFImageReader extends ImageReaderBase {
int[] destinationBands = null;
// Get values from the ImageReadParam, if any
if (pParam != null) {
sourceXSubsampling = pParam.getSourceXSubsampling();
sourceYSubsampling = pParam.getSourceYSubsampling();
if (param != null) {
sourceXSubsampling = param.getSourceXSubsampling();
sourceYSubsampling = param.getSourceYSubsampling();
sourceBands = pParam.getSourceBands();
destinationBands = pParam.getDestinationBands();
sourceBands = param.getSourceBands();
destinationBands = param.getDestinationBands();
}
// 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.
// The number of words per row is words=((w+15)/16)
int planeWidth = 2 * ((width + 15) / 16);
final byte[] planeData = new byte[8 * planeWidth];
WritableRaster destination = image.getRaster();
WritableRaster destRaster = destination.getRaster();
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();
final int channels = (header.bitplanes + 7) / 8;
WritableRaster rowRaster = destination.getRaster().createCompatibleWritableRaster(8 * planeWidth, 1);
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;
Object dataElements = null;
for (int srcY = 0; srcY < height; srcY++) {
for (int c = 0; c < channels; c++) {
for (int p = 0; p < planesPerChannel; p++) {
readPlaneData(pInput, planeData, p * planeWidth, planeWidth);
readPlaneData(planeData, p * planeWidth, planeWidth, input);
}
// Skip rows outside AOI
@ -710,7 +674,7 @@ public final class IFFImageReader extends ImageReaderBase {
continue;
}
if (formType == IFF.TYPE_ILBM) {
if (header.formType == IFF.TYPE_ILBM) {
// NOTE: Using (channels - c - 1) instead of just c,
// effectively reverses the channel order from RGBA to ABGR
int off = (channels - c - 1);
@ -721,33 +685,30 @@ public final class IFFImageReader extends ImageReaderBase {
pixelPos += 8;
}
}
else if (formType == IFF.TYPE_PBM) {
else if (header.formType == IFF.TYPE_PBM) {
System.arraycopy(planeData, 0, data, srcY * 8 * planeWidth, planeWidth);
}
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) {
int dstY = (srcY - aoi.y) / sourceYSubsampling;
// TODO: Support conversion to INT (A)RGB rasters (maybe using ColorConvertOp?)
// TODO: Avoid createChild if no region?
if (sourceXSubsampling == 1) {
destination.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);
destRaster.setRect(0, dstY, sourceRow);
}
else {
for (int srcX = 0; srcX < sourceRow.getWidth(); srcX += sourceXSubsampling) {
dataElements = sourceRow.getDataElements(srcX, 0, dataElements);
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()) {
processReadAborted();
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 {
switch (header.compressionType) {
switch (header.compressionType()) {
case BMHDChunk.COMPRESSION_NONE:
pInput.readFully(pData, pOffset, pPlaneWidth);
input.readFully(destination, offset, planeWidth);
// Uncompressed rows must have even number of bytes
if ((header.bitplanes * pPlaneWidth) % 2 != 0) {
pInput.readByte();
// Uncompressed rows must have an even number of bytes
if ((header.bitplanes() * planeWidth) % 2 != 0) {
input.readByte();
}
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?!
// 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...
// 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).
if (byteRunStream == null) {
byteRunStream = new DataInputStream(
new DecoderStream(
IIOUtil.createStreamAdapter(pInput, body.chunkLength),
new PackBitsDecoder(true),
pPlaneWidth * header.bitplanes
IIOUtil.createStreamAdapter(input, header.bodyLength()),
new PackBitsDecoder(header.sampleSize(), true),
planeWidth * (header.sampleSize() > 1 ? 1 : header.bitplanes())
)
);
}
byteRunStream.readFully(pData, pOffset, pPlaneWidth);
byteRunStream.readFully(destination, offset, planeWidth);
break;
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
if (byteRunStream == null) {
byteRunStream = new DataInputStream(
new DecoderStream(
IIOUtil.createStreamAdapter(pInput, body.chunkLength),
new RGB8RLEDecoder(),
pPlaneWidth * 4
IIOUtil.createStreamAdapter(input, header.bodyLength()),
new RGB8RLEDecoder(), 1024
)
);
}
byteRunStream.readFully(pData, pOffset, pPlaneWidth);
byteRunStream.readFully(destination, offset, planeWidth);
break;
}
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) {
final int width = header.width;
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();
ColorModel palette = paletteChange.getColorModel(colorModel, row, isLaced());
ColorModel palette = header.colorMapForRow(colorModel, row);
for (int x = 0; x < width; x++) {
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) {
final int bits = header.bitplanes;
final int width = header.width;
int lastRed = 0;
int lastGreen = 0;
int lastBlue = 0;
private void hamToRGB(final byte[] indexed, final IndexColorModel colorModel, final byte[] dest, @SuppressWarnings("SameParameterValue") final int destOffset) {
final int bits = header.bitplanes();
final int width = header.width();
// Initialize to the "border color" (index 0)
int lastRed = colorModel.getRed(0);
int lastGreen = colorModel.getGreen(0);
int lastBlue = colorModel.getBlue(0);
for (int x = 0; x < width; x++) {
int pixel = indexed[x] & 0xff;
@ -867,32 +827,11 @@ public final class IFFImageReader extends ImageReaderBase {
}
}
private boolean isSHAM() {
// 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 {
public static void main(String[] args) {
ImageReader reader = new IFFImageReader(new IFFImageReaderSpi());
boolean scale = false;
for (String arg : pArgs) {
for (String arg : args) {
if (arg.startsWith("-")) {
scale = true;
continue;

View File

@ -53,40 +53,41 @@ public final class IFFImageReaderSpi extends ImageReaderSpiBase {
}
@Override
public boolean canDecodeInput(Object pSource) throws IOException {
return pSource instanceof ImageInputStream && canDecode((ImageInputStream) pSource);
public boolean canDecodeInput(final Object source) throws IOException {
return source instanceof ImageInputStream && canDecode((ImageInputStream) source);
}
private static boolean canDecode(ImageInputStream pInput) throws IOException {
pInput.mark();
private static boolean canDecode(final ImageInputStream input) throws IOException {
input.mark();
try {
// Is it IFF
if (pInput.readInt() == IFF.CHUNK_FORM) {
pInput.readInt();// Skip length field
if (input.readInt() == IFF.CHUNK_FORM) {
input.readInt();// Skip length field
int type = pInput.readInt();
int type = input.readInt();
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;
}
}
}
finally {
pInput.reset();
input.reset();
}
return false;
}
@Override
public ImageReader createReaderInstance(Object pExtension) throws IOException {
public ImageReader createReaderInstance(final Object extension) {
return new IFFImageReader(this);
}
@Override
public String getDescription(Locale pLocale) {
public String getDescription(Locale locale) {
return "Commodore Amiga/Electronic Arts Image Interchange Format (IFF) image reader";
}
}

View File

@ -30,31 +30,22 @@
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.util.IIOUtil;
import com.twelvemonkeys.io.FastByteArrayOutputStream;
import com.twelvemonkeys.io.enc.EncoderStream;
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.
* 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 {
IFFImageWriter(ImageWriterSpi pProvider) {
super(pProvider);
IFFImageWriter(ImageWriterSpi provider) {
super(provider);
}
@Override
@ -83,23 +74,29 @@ public final class IFFImageWriter extends ImageWriterBase {
}
@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();
if (pImage.hasRaster()) {
if (image.hasRaster()) {
throw new UnsupportedOperationException("Cannot write raster");
}
processImageStarted(0);
RenderedImage renderedImage = image.getRenderedImage();
boolean compress = shouldCompress(renderedImage, param);
// Prepare image data to be written
ByteArrayOutputStream imageData = new FastByteArrayOutputStream(1024);
packImageData(imageData, pImage.getRenderedImage(), pParam);
//System.out.println("Image data: " + imageData.size());
packImageData(imageData, renderedImage, compress);
// Write metadata
writeMeta(pImage.getRenderedImage(), imageData.size());
writeMeta(renderedImage, imageData.size(), compress);
// Write image data
writeBody(imageData);
@ -107,34 +104,31 @@ public final class IFFImageWriter extends ImageWriterBase {
processImageComplete();
}
private void writeBody(ByteArrayOutputStream pImageData) throws IOException {
private void writeBody(ByteArrayOutputStream imageData) throws IOException {
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
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.flush();
}
private void packImageData(OutputStream pOutput, RenderedImage pImage, ImageWriteParam pParam) throws IOException {
// TODO: Allow param to dictate uncompressed
// TODO: Allow param to dictate type PBM?
private void packImageData(OutputStream outputStream, RenderedImage image, final boolean compress) throws IOException {
// TODO: Subsample/AOI
final boolean compress = shouldCompress(pImage);
final OutputStream output = compress ? new EncoderStream(pOutput, new PackBitsEncoder(), true) : pOutput;
final ColorModel model = pImage.getColorModel();
final Raster raster = pImage.getData();
final OutputStream output = compress ? new EncoderStream(outputStream, new PackBitsEncoder(), true) : outputStream;
final ColorModel model = image.getColorModel();
final Raster raster = image.getData();
final int width = pImage.getWidth();
final int height = pImage.getHeight();
final int width = image.getWidth();
final int height = image.getHeight();
// Store each row of pixels
// 0. Loop pr channel
@ -142,7 +136,6 @@ public final class IFFImageWriter extends ImageWriterBase {
// 2. Perform byteRun1 compression for each plane separately
// 3. Write the plane data for each plane
//final int planeWidth = (width + 7) / 8;
final int planeWidth = 2 * ((width + 15) / 16);
final byte[] planeData = new byte[8 * planeWidth];
final int channels = (model.getPixelSize() + 7) / 8;
@ -167,10 +160,6 @@ public final class IFFImageWriter extends ImageWriterBase {
for (int p = 0; p < planesPerChannel; p++) {
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();
}
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
String annotation = String.format("Written by %s IFFImageWriter %s", getOriginatingProvider().getVendorName(), getOriginatingProvider().getVersion());
GenericChunk anno = new GenericChunk(IFFUtil.toInt("ANNO".getBytes()), annotation.getBytes());
ColorModel cm = pImage.getColorModel();
ColorModel cm = image.getColorModel();
IndexColorModel icm = null;
// Bitmap header BMHD chunk, 8 + 20 bytes
// By default, don't compress narrow images
int compression = shouldCompress(pImage) ? BMHDChunk.COMPRESSION_BYTE_RUN : BMHDChunk.COMPRESSION_NONE;
int compression = compress ? BMHDChunk.COMPRESSION_BYTE_RUN : BMHDChunk.COMPRESSION_NONE;
BMHDChunk header;
if (cm instanceof IndexColorModel) {
@ -200,12 +188,12 @@ public final class IFFImageWriter extends ImageWriterBase {
icm = (IndexColorModel) cm;
int trans = icm.getTransparency() == Transparency.BITMASK ? BMHDChunk.MASK_TRANSPARENT_COLOR : BMHDChunk.MASK_NONE;
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);
}
else {
//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);
}
@ -217,7 +205,7 @@ public final class IFFImageWriter extends ImageWriterBase {
}
// 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) {
size += 8 + cmap.chunkLength;
}
@ -231,21 +219,30 @@ public final class IFFImageWriter extends ImageWriterBase {
header.writeChunk(imageOutput);
if (cmap != null) {
//System.out.println("CMAP written");
cmap.writeChunk(imageOutput);
}
}
private boolean shouldCompress(RenderedImage pImage) {
return pImage.getWidth() >= 32;
private boolean shouldCompress(final RenderedImage image, final ImageWriteParam param) {
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 {
BufferedImage image = ImageIO.read(new File(pArgs[0]));
public static void main(String[] args) throws IOException {
BufferedImage image = ImageIO.read(new File(args[0]));
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() {
// int mCurrPct = 0;
//

View File

@ -30,13 +30,11 @@
package com.twelvemonkeys.imageio.plugins.iff;
import java.io.IOException;
import java.util.Locale;
import com.twelvemonkeys.imageio.spi.ImageWriterSpiBase;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.ImageWriter;
import com.twelvemonkeys.imageio.spi.ImageWriterSpiBase;
import java.util.Locale;
/**
* IFFImageWriterSpi
@ -53,19 +51,19 @@ public class IFFImageWriterSpi extends ImageWriterSpiBase {
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: Can't store CMYK (well.. it does, but they can't be read back)
return true;
}
@Override
public ImageWriter createWriterInstance(Object pExtension) throws IOException {
public ImageWriter createWriterInstance(Object extension) {
return new IFFImageWriter(this);
}
@Override
public String getDescription(Locale pLocale) {
public String getDescription(Locale locale) {
return "Commodore Amiga/Electronic Arts Image Interchange Format (IFF) image writer";
}
}

View File

@ -40,11 +40,11 @@ import com.twelvemonkeys.imageio.spi.ReaderWriterProviderInfo;
* @version $Id: IFFProviderInfo.java,v 1.0 20/03/15 harald.kuhr Exp$
*/
final class IFFProviderInfo extends ReaderWriterProviderInfo {
protected IFFProviderInfo() {
IFFProviderInfo() {
super(
IFFProviderInfo.class,
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"},
"com.twelvemonkeys.imageio.plugins.iff.IFFImageReader",
new String[]{"com.twelvemonkeys.imageio.plugins.iff.IFFImageReaderSpi"},

View File

@ -56,11 +56,11 @@ final class IFFUtil {
* @return the rotation table
*/
static private long[] rtable(int n) {
return new long[]{
0x00000000l << n, 0x00000001l << n, 0x00000100l << n, 0x00000101l << n,
0x00010000l << n, 0x00010001l << n, 0x00010100l << n, 0x00010101l << n,
0x01000000l << n, 0x01000001l << n, 0x01000100l << n, 0x01000101l << n,
0x01010000l << n, 0x01010001l << n, 0x01010100l << n, 0x01010101l << n
return new long[] {
0x00000000L , 0x00000001L << n, 0x00000100L << n, 0x00000101L << n,
0x00010000L << n, 0x00010001L << n, 0x00010100L << n, 0x00010101L << n,
0x01000000L << n, 0x01000001L << n, 0x01000100L << n, 0x01000101L << 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
* destination.
*
* @param pSrc source pixel data
* @param pSrcPos starting index of 8 x 8 bit source tile
* @param pSrcStep byte offset between adjacent rows in source
* @param pDst destination pixel data
* @param pDstPos starting index of 8 x 8 bit destination tile
* @param pDstStep byte offset between adjacent rows in destination
* @param src source pixel data
* @param srcPos starting index of 8 x 8 bit source tile
* @param srcStep byte offset between adjacent rows in source
* @param dst destination pixel data
* @param dstPos starting index of 8 x 8 bit destination tile
* @param dstStep byte offset between adjacent rows in destination
*/
static void bitRotateCW(final byte[] pSrc, int pSrcPos, int pSrcStep,
final byte[] pDst, int pDstPos, int pDstStep) {
int idx = pSrcPos;
static void bitRotateCW(final byte[] src, int srcPos, int srcStep,
final byte[] dst, int dstPos, int dstStep) {
int idx = srcPos;
int lonyb;
int hinyb;
@ -92,41 +92,41 @@ final class IFFUtil {
long hi = 0;
for (int i = 0; i < 8; i++) {
lonyb = pSrc[idx] & 0xF;
hinyb = (pSrc[idx] >> 4) & 0xF;
lonyb = src[idx] & 0xF;
hinyb = (src[idx] >> 4) & 0xF;
lo |= RTABLE[i][lonyb];
hi |= RTABLE[i][hinyb];
idx += pSrcStep;
idx += srcStep;
}
idx = pDstPos;
idx = dstPos;
pDst[idx] = (byte)((hi >> 24) & 0xFF);
idx += pDstStep;
if (idx < pDst.length) {
pDst[idx] = (byte)((hi >> 16) & 0xFF);
idx += pDstStep;
if (idx < pDst.length) {
pDst[idx] = (byte)((hi >> 8) & 0xFF);
idx += pDstStep;
if (idx < pDst.length) {
pDst[idx] = (byte)(hi & 0xFF);
idx += pDstStep;
dst[idx] = (byte)((hi >> 24) & 0xFF);
idx += dstStep;
if (idx < dst.length) {
dst[idx] = (byte)((hi >> 16) & 0xFF);
idx += dstStep;
if (idx < dst.length) {
dst[idx] = (byte)((hi >> 8) & 0xFF);
idx += dstStep;
if (idx < dst.length) {
dst[idx] = (byte)(hi & 0xFF);
idx += dstStep;
}
}
}
if (idx < pDst.length) {
pDst[idx] = (byte)((lo >> 24) & 0xFF);
idx += pDstStep;
if (idx < pDst.length) {
pDst[idx] = (byte)((lo >> 16) & 0xFF);
idx += pDstStep;
if (idx < pDst.length) {
pDst[idx] = (byte)((lo >> 8) & 0xFF);
idx += pDstStep;
if (idx < pDst.length) {
pDst[idx] = (byte)(lo & 0xFF);
if (idx < dst.length) {
dst[idx] = (byte)((lo >> 24) & 0xFF);
idx += dstStep;
if (idx < dst.length) {
dst[idx] = (byte)((lo >> 16) & 0xFF);
idx += dstStep;
if (idx < dst.length) {
dst[idx] = (byte)((lo >> 8) & 0xFF);
idx += dstStep;
if (idx < dst.length) {
dst[idx] = (byte)(lo & 0xFF);
}
}
}
@ -137,16 +137,16 @@ final class IFFUtil {
* Rotate bits counterclockwise.
* The IFFImageWriter uses this to convert pixel bits from chunky to planar.
*
* @param pSrc source pixel data (only lower 8 bits used)
* @param pSrcPos starting index of 8 x 8 bit source tile
* @param pSrcStep byte offset between adjacent rows in source
* @param pDst destination pixel data
* @param pDstPos starting index of 8 x 8 bit destination tile
* @param pDstStep byte offset between adjacent rows in destination
* @param src source pixel data (only lower 8 bits used)
* @param srcPos starting index of 8 x 8 bit source tile
* @param srcStep byte offset between adjacent rows in source
* @param dst destination pixel data
* @param dstPos starting index of 8 x 8 bit destination tile
* @param dstStep byte offset between adjacent rows in destination
*/
static void bitRotateCCW(final int[] pSrc, int pSrcPos, int pSrcStep,
final byte[] pDst, int pDstPos, int pDstStep) {
int idx = pSrcPos;
static void bitRotateCCW(final int[] src, int srcPos, @SuppressWarnings("SameParameterValue") int srcStep,
final byte[] dst, int dstPos, int dstStep) {
int idx = srcPos;
int lonyb;
int hinyb;
@ -154,48 +154,49 @@ final class IFFUtil {
long hi = 0;
for (int i = 7; i >= 0; i--) {
lonyb = pSrc[idx] & 0xF;
hinyb = (pSrc[idx] >> 4) & 0xF;
lonyb = src[idx] & 0xF;
hinyb = (src[idx] >> 4) & 0xF;
lo |= RTABLE[i][lonyb];
hi |= RTABLE[i][hinyb];
idx += pSrcStep;
idx += srcStep;
}
idx = pDstPos;
idx = dstPos;
pDst[idx] = (byte)(lo & 0xFF);
idx += pDstStep;
pDst[idx] = (byte)((lo >> 8) & 0xFF);
idx += pDstStep;
pDst[idx] = (byte)((lo >> 16) & 0xFF);
idx += pDstStep;
pDst[idx] = (byte)((lo >> 24) & 0xFF);
dst[idx] = (byte)(lo & 0xFF);
idx += dstStep;
dst[idx] = (byte)((lo >> 8) & 0xFF);
idx += dstStep;
dst[idx] = (byte)((lo >> 16) & 0xFF);
idx += dstStep;
dst[idx] = (byte)((lo >> 24) & 0xFF);
idx += pDstStep;
idx += dstStep;
pDst[idx] = (byte)(hi & 0xFF);
idx += pDstStep;
pDst[idx] = (byte)((hi >> 8) & 0xFF);
idx += pDstStep;
pDst[idx] = (byte)((hi >> 16) & 0xFF);
idx += pDstStep;
pDst[idx] = (byte)((hi >> 24) & 0xFF);
dst[idx] = (byte)(hi & 0xFF);
idx += dstStep;
dst[idx] = (byte)((hi >> 8) & 0xFF);
idx += dstStep;
dst[idx] = (byte)((hi >> 16) & 0xFF);
idx += dstStep;
dst[idx] = (byte)((hi >> 24) & 0xFF);
}
/**
* Rotate bits counterclockwise.
* The IFFImageWriter uses this to convert pixel bits from chunky to planar.
*
* @param pSrc source pixel data
* @param pSrcPos starting index of 8 x 8 bit source tile
* @param pSrcStep byte offset between adjacent rows in source
* @param pDst destination pixel data
* @param pDstPos starting index of 8 x 8 bit destination tile
* @param pDstStep byte offset between adjacent rows in destination
* @param src source pixel data
* @param srcPos starting index of 8 x 8 bit source tile
* @param srcStep byte offset between adjacent rows in source
* @param dst destination pixel data
* @param dstPos starting index of 8 x 8 bit destination tile
* @param dstStep byte offset between adjacent rows in destination
*/
static void bitRotateCCW(final byte[] pSrc, int pSrcPos, int pSrcStep,
final byte[] pDst, int pDstPos, int pDstStep) {
int idx = pSrcPos;
@SuppressWarnings("unused")
static void bitRotateCCW(final byte[] src, int srcPos, int srcStep,
final byte[] dst, int dstPos, int dstStep) {
int idx = srcPos;
int lonyb;
int hinyb;
@ -203,57 +204,57 @@ final class IFFUtil {
long hi = 0;
for (int i = 7; i >= 0; i--) {
lonyb = pSrc[idx] & 0xF;
hinyb = (pSrc[idx] >> 4) & 0xF;
lonyb = src[idx] & 0xF;
hinyb = (src[idx] >> 4) & 0xF;
lo |= RTABLE[i][lonyb];
hi |= IFFUtil.RTABLE[i][hinyb];
idx += pSrcStep;
idx += srcStep;
}
idx = pDstPos;
idx = dstPos;
pDst[idx] = (byte)(lo & 0xFF);
idx += pDstStep;
pDst[idx] = (byte)((lo >> 8) & 0xFF);
idx += pDstStep;
pDst[idx] = (byte)((lo >> 16) & 0xFF);
idx += pDstStep;
pDst[idx] = (byte)((lo >> 24) & 0xFF);
dst[idx] = (byte)(lo & 0xFF);
idx += dstStep;
dst[idx] = (byte)((lo >> 8) & 0xFF);
idx += dstStep;
dst[idx] = (byte)((lo >> 16) & 0xFF);
idx += dstStep;
dst[idx] = (byte)((lo >> 24) & 0xFF);
idx += pDstStep;
idx += dstStep;
pDst[idx] = (byte)(hi & 0xFF);
idx += pDstStep;
pDst[idx] = (byte)((hi >> 8) & 0xFF);
idx += pDstStep;
pDst[idx] = (byte)((hi >> 16) & 0xFF);
idx += pDstStep;
pDst[idx] = (byte)((hi >> 24) & 0xFF);
dst[idx] = (byte)(hi & 0xFF);
idx += dstStep;
dst[idx] = (byte)((hi >> 8) & 0xFF);
idx += dstStep;
dst[idx] = (byte)((hi >> 16) & 0xFF);
idx += dstStep;
dst[idx] = (byte)((hi >> 24) & 0xFF);
}
/**
* 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
*
* @throws ArrayIndexOutOfBoundsException if length is < 4
*/
static int toInt(final byte[] pBytes) {
return (pBytes[0] & 0xff) << 24 | (pBytes[1] & 0xff) << 16
| (pBytes[2] & 0xff) << 8 | (pBytes[3] & 0xff);
static int toInt(final byte[] bytes) {
return (bytes[0] & 0xff) << 24 | (bytes[1] & 0xff) << 16
| (bytes[2] & 0xff) << 8 | (bytes[3] & 0xff);
}
/**
* Converts an int to a four letter String.
*
* @param pChunkId the chunk identifier
* @param chunkId the chunk identifier
* @return a String
*/
static String toChunkStr(int pChunkId) {
return new String(new byte[] {(byte) ((pChunkId & 0xff000000) >> 24),
(byte) ((pChunkId & 0x00ff0000) >> 16),
(byte) ((pChunkId & 0x0000ff00) >> 8),
(byte) ((pChunkId & 0x000000ff))});
static String toChunkStr(int chunkId) {
return new String(new byte[] {(byte) ((chunkId & 0xff000000) >> 24),
(byte) ((chunkId & 0x00ff0000) >> 16),
(byte) ((chunkId & 0x0000ff00) >> 8),
(byte) ((chunkId & 0x000000ff))});
}
}

View File

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

View File

@ -80,9 +80,9 @@ final class MutableIndexColorModel extends ColorModel {
// TODO: Move validation to chunk (when reading)
if (index >= rgbs.length) {
// 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(" 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) {
updateRGB(index, ((changes[i].r & 0xff) << 16) | ((changes[i].g & 0xff) << 8) | (changes[i].b & 0xff));

View File

@ -72,42 +72,43 @@ final class PCHGChunk extends AbstractMultiPaletteChunk {
private int totalChanges;
private int minReg;
PCHGChunk(int pChunkLength) {
super(IFF.CHUNK_PCHG, pChunkLength);
PCHGChunk(int chunkLength) {
super(IFF.CHUNK_PCHG, chunkLength);
}
@Override
void readChunk(final DataInput pInput) throws IOException {
int compression = pInput.readUnsignedShort();
int flags = pInput.readUnsignedShort();
startLine = pInput.readShort();
lineCount = pInput.readUnsignedShort();
changedLines = pInput.readUnsignedShort();
minReg = pInput.readUnsignedShort();
int maxReg = pInput.readUnsignedShort();
/*int maxChangesPerLine = */pInput.readUnsignedShort(); // We don't really care, as we're not limited by the Amiga display hardware
totalChanges = pInput.readInt();
void readChunk(final DataInput input) throws IOException {
int compression = input.readUnsignedShort();
int flags = input.readUnsignedShort();
startLine = input.readShort();
lineCount = input.readUnsignedShort();
changedLines = input.readUnsignedShort();
minReg = input.readUnsignedShort();
int maxReg = input.readUnsignedShort();
/*int maxChangesPerLine = */
input.readUnsignedShort(); // We don't really care, as we're not limited by the Amiga display hardware
totalChanges = input.readInt();
byte[] data;
switch (compression) {
case PCHG_COMP_NONE:
data = new byte[chunkLength - 20];
pInput.readFully(data);
input.readFully(data);
break;
case PCHG_COMP_HUFFMAN:
// NOTE: Huffman decompression is completely untested, due to lack of source data (read: Probably broken).
int compInfoSize = pInput.readInt();
int originalDataSize = pInput.readInt();
int compInfoSize = input.readInt();
int originalDataSize = input.readInt();
short[] compTree = new short[compInfoSize / 2];
for (int i = 0; i < compTree.length; i++) {
compTree[i] = pInput.readShort();
compTree[i] = input.readShort();
}
byte[] compData = new byte[chunkLength - 20 - 8 - compInfoSize];
pInput.readFully(compData);
input.readFully(compData);
data = new byte[originalDataSize];

View File

@ -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;
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 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>
*/

View File

@ -38,8 +38,8 @@ package com.twelvemonkeys.imageio.plugins.iff;
* @version $Id: SHAMChunk.java,v 1.0 30.03.12 14:53 haraldk Exp$
*/
final class SHAMChunk extends AbstractMultiPaletteChunk {
SHAMChunk(int pChunkLength) {
super(IFF.CHUNK_SHAM, pChunkLength);
SHAMChunk(int chunkLength) {
super(IFF.CHUNK_SHAM, chunkLength);
}
@Override

View File

@ -1,26 +1,24 @@
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.function.ThrowingRunnable;
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 {
@Test
public void testStandardFeatures() {
BMHDChunk header = new BMHDChunk(300, 200, 24, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0);
public void testStandardFeatures() throws IIOException {
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
assertTrue(metadata.isStandardMetadataFormatSupported());
@ -49,10 +47,11 @@ public class IFFImageMetadataTest {
}
@Test
public void testStandardChromaGray() {
BMHDChunk header = new BMHDChunk(300, 200, 8, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0);
public void testStandardChromaGray() throws IIOException {
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();
assertNotNull(chroma);
@ -75,10 +74,11 @@ public class IFFImageMetadataTest {
}
@Test
public void testStandardChromaRGB() {
BMHDChunk header = new BMHDChunk(300, 200, 24, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0);
public void testStandardChromaRGB() throws IIOException {
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();
assertNotNull(chroma);
@ -101,16 +101,17 @@ public class IFFImageMetadataTest {
}
@Test
public void testStandardChromaPalette() {
BMHDChunk header = new BMHDChunk(300, 200, 1, BMHDChunk.MASK_TRANSPARENT_COLOR, BMHDChunk.COMPRESSION_BYTE_RUN, 1);
public void testStandardChromaPalette() throws IIOException {
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};
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();
assertNotNull(chroma);
assertEquals("Chroma", chroma.getNodeName());
assertEquals(4, chroma.getLength());
assertEquals(5, chroma.getLength());
IIOMetadataNode colorSpaceType = (IIOMetadataNode) chroma.getFirstChild();
assertEquals("ColorSpaceType", colorSpaceType.getNodeName());
@ -138,14 +139,21 @@ public class IFFImageMetadataTest {
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
public void testStandardCompressionRLE() {
BMHDChunk header = new BMHDChunk(300, 200, 24, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0);
public void testStandardCompressionRLE() throws IIOException {
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();
assertNotNull(compression);
@ -164,19 +172,21 @@ public class IFFImageMetadataTest {
}
@Test
public void testStandardCompressionNone() {
BMHDChunk header = new BMHDChunk(300, 200, 24, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_NONE, 0);
public void testStandardCompressionNone() throws IIOException {
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...
}
@Test
public void testStandardDataILBM_Gray() {
BMHDChunk header = new BMHDChunk(300, 200, 8, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0);
public void testStandardDataILBM_Gray() throws IIOException {
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();
assertNotNull(data);
@ -199,10 +209,11 @@ public class IFFImageMetadataTest {
}
@Test
public void testStandardDataILBM_RGB() {
BMHDChunk header = new BMHDChunk(300, 200, 24, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0);
public void testStandardDataILBM_RGB() throws IIOException {
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();
assertNotNull(data);
@ -225,10 +236,11 @@ public class IFFImageMetadataTest {
}
@Test
public void testStandardDataILBM_RGBA() {
BMHDChunk header = new BMHDChunk(300, 200, 32, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0);
public void testStandardDataILBM_RGBA() throws IIOException {
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();
assertNotNull(data);
@ -251,12 +263,13 @@ public class IFFImageMetadataTest {
}
@Test
public void testStandardDataILBM_Palette() {
public void testStandardDataILBM_Palette() throws IIOException {
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
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();
assertNotNull(data);
@ -280,10 +293,11 @@ public class IFFImageMetadataTest {
}
@Test
public void testStandardDataPBM_Gray() {
BMHDChunk header = new BMHDChunk(300, 200, 8, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0);
public void testStandardDataPBM_Gray() throws IIOException {
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();
assertNotNull(data);
@ -306,10 +320,11 @@ public class IFFImageMetadataTest {
}
@Test
public void testStandardDataPBM_RGB() {
BMHDChunk header = new BMHDChunk(300, 200, 24, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0);
public void testStandardDataPBM_RGB() throws IIOException {
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();
assertNotNull(data);
@ -333,40 +348,57 @@ public class IFFImageMetadataTest {
@Test
public void testStandardDimensionNoViewport() {
BMHDChunk header = new BMHDChunk(300, 200, 8, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0);
public void testStandardDimensionNoViewport() throws IIOException {
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();
assertNull(dimension);
}
@Test
public void testStandardDimensionNormal() {
BMHDChunk header = new BMHDChunk(300, 200, 8, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0);
public void testStandardDimensionNormal() throws IIOException {
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();
assertNotNull(dimension);
assertEquals("Dimension", dimension.getNodeName());
assertEquals(1, dimension.getLength());
IIOMetadataNode pixelAspectRatio = (IIOMetadataNode) dimension.getFirstChild();
assertEquals("PixelAspectRatio", pixelAspectRatio.getNodeName());
assertEquals("1.0", pixelAspectRatio.getAttribute("value"));
// No Dimension node is okay, or one with an aspect ratio of 1.0
if (dimension != null) {
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
public void testStandardDimensionHires() {
BMHDChunk header = new BMHDChunk(300, 200, 8, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0);
public void testStandardDimensionHires() throws IIOException {
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);
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();
assertNotNull(dimension);
@ -381,13 +413,19 @@ public class IFFImageMetadataTest {
}
@Test
public void testStandardDimensionInterlaced() {
BMHDChunk header = new BMHDChunk(300, 200, 8, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0);
public void testStandardDimensionInterlaced() throws IIOException {
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);
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();
assertNotNull(dimension);
@ -402,13 +440,14 @@ public class IFFImageMetadataTest {
}
@Test
public void testStandardDimensionHiresInterlaced() {
BMHDChunk header = new BMHDChunk(300, 200, 8, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0);
public void testStandardDimensionHiresInterlaced() throws IIOException {
CAMGChunk viewPort = new CAMGChunk(4);
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();
assertNotNull(dimension);
@ -423,10 +462,11 @@ public class IFFImageMetadataTest {
}
@Test
public void testStandardDocument() {
BMHDChunk header = new BMHDChunk(300, 200, 8, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0);
public void testStandardDocument() throws IIOException {
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();
assertNotNull(document);
@ -441,13 +481,15 @@ public class IFFImageMetadataTest {
}
@Test
public void testStandardText() {
BMHDChunk header = new BMHDChunk(300, 200, 8, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0);
public void testStandardText() throws IIOException {
int[] chunks = {IFF.CHUNK_ANNO, IFF.CHUNK_UTF8};
String[] texts = {"annotation", "äñnótâtïøñ"};
List<GenericChunk> meta = Arrays.asList(new GenericChunk(IFF.CHUNK_ANNO, texts[0].getBytes(StandardCharsets.US_ASCII)),
new GenericChunk(IFF.CHUNK_UTF8, texts[1].getBytes(StandardCharsets.UTF_8)));
IFFImageMetadata metadata = new IFFImageMetadata(IFF.TYPE_ILBM, header, null, null, meta);
Form header = Form.ofType(IFF.TYPE_ILBM)
.with(new BMHDChunk(300, 200, 8, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0))
.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();
assertNotNull(text);
@ -457,26 +499,28 @@ public class IFFImageMetadataTest {
for (int i = 0; i < texts.length; i++) {
IIOMetadataNode textEntry = (IIOMetadataNode) text.item(i);
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"));
}
}
@Test
public void testStandardTransparencyRGB() {
BMHDChunk header = new BMHDChunk(300, 200, 24, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0);
public void testStandardTransparencyRGB() throws IIOException {
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();
assertNull(transparency); // No transparency, just defaults
}
@Test
public void testStandardTransparencyRGBA() {
BMHDChunk header = new BMHDChunk(300, 200, 32, BMHDChunk.MASK_HAS_MASK, BMHDChunk.COMPRESSION_BYTE_RUN, 0);
public void testStandardTransparencyRGBA() throws IIOException {
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();
assertNotNull(transparency);
@ -491,11 +535,12 @@ public class IFFImageMetadataTest {
}
@Test
public void testStandardTransparencyPalette() {
BMHDChunk header = new BMHDChunk(300, 200, 1, BMHDChunk.MASK_TRANSPARENT_COLOR, BMHDChunk.COMPRESSION_BYTE_RUN, 1);
public void testStandardTransparencyPalette() throws IIOException {
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};
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();
assertNotNull(transparency);
@ -508,4 +553,131 @@ public class IFFImageMetadataTest {
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
}
}

View File

@ -92,7 +92,12 @@ public class IFFImageReaderTest extends ImageReaderAbstractTest<IFFImageReader>
// Impulse RGB8 format straight from Imagine 2.0
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...
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
protected List<String> getSuffixes() {
return Arrays.asList("iff", "ilbm", "ham", "ham8", "lbm");
return Arrays.asList("iff", "ilbm", "ham", "ham8", "lbm", "rgb8", "deep");
}
@Override
@ -138,9 +143,14 @@ public class IFFImageReaderTest extends ImageReaderAbstractTest<IFFImageReader>
for (int i = 0; i < 32; i++) {
// Make sure the color model is really EHB
assertEquals("red", (reds[i] & 0xff) / 2, reds[i + 32] & 0xff);
assertEquals("blue", (blues[i] & 0xff) / 2, blues[i + 32] & 0xff);
assertEquals("green", (greens[i] & 0xff) / 2, greens[i + 32] & 0xff);
try {
assertEquals("red", (reds[i] & 0xff) / 2, reds[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.