IFF: Thumbnail support for XS24 chunk.

(cherry picked from commit f5cfa0e619211b627d26417b13c8407b3063641b)
This commit is contained in:
Harald Kuhr 2022-02-04 11:46:32 +01:00
parent b7b2a61c93
commit 954dffd213
5 changed files with 221 additions and 16 deletions

View File

@ -1,6 +1,7 @@
package com.twelvemonkeys.imageio.plugins.iff; package com.twelvemonkeys.imageio.plugins.iff;
import javax.imageio.IIOException; import javax.imageio.IIOException;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel; import java.awt.image.ColorModel;
import java.awt.image.IndexColorModel; import java.awt.image.IndexColorModel;
import java.util.ArrayList; import java.util.ArrayList;
@ -59,6 +60,14 @@ abstract class Form {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }
public abstract boolean hasThumbnail();
public abstract int thumbnailWidth();
public abstract int thumbnailHeight();
public abstract BufferedImage thumbnail();
abstract long bodyOffset(); abstract long bodyOffset();
abstract long bodyLength(); abstract long bodyLength();
@ -102,18 +111,20 @@ abstract class Form {
private final CAMGChunk viewMode; private final CAMGChunk viewMode;
private final CMAPChunk colorMap; private final CMAPChunk colorMap;
private final AbstractMultiPaletteChunk multiPalette; private final AbstractMultiPaletteChunk multiPalette;
private final XS24Chunk thumbnail;
private final BODYChunk body; private final BODYChunk body;
ILBMForm(int formType) { ILBMForm(int formType) {
this(formType, null, null, null, null, null); this(formType, null, 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) { private ILBMForm(final int formType, final BMHDChunk bitmapHeader, final CAMGChunk viewMode, final CMAPChunk colorMap, final AbstractMultiPaletteChunk multiPalette, final XS24Chunk thumbnail, final BODYChunk body) {
super(formType); super(formType);
this.bitmapHeader = bitmapHeader; this.bitmapHeader = bitmapHeader;
this.viewMode = viewMode; this.viewMode = viewMode;
this.colorMap = colorMap; this.colorMap = colorMap;
this.multiPalette = multiPalette; this.multiPalette = multiPalette;
this.thumbnail = thumbnail;
this.body = body; this.body = body;
} }
@ -180,6 +191,26 @@ abstract class Form {
return multiPalette != null ? multiPalette.getColorModel(colorModel, row, isLaced()) : null; return multiPalette != null ? multiPalette.getColorModel(colorModel, row, isLaced()) : null;
} }
@Override
public boolean hasThumbnail() {
return thumbnail != null;
}
@Override
public int thumbnailWidth() {
return thumbnail != null ? thumbnail.width : -1;
}
@Override
public int thumbnailHeight() {
return thumbnail != null ? thumbnail.height : -1;
}
@Override
public BufferedImage thumbnail() {
return thumbnail != null ? thumbnail.thumbnail() : null;
}
@Override @Override
long bodyOffset() { long bodyOffset() {
return body.chunkOffset; return body.chunkOffset;
@ -197,21 +228,21 @@ abstract class Form {
throw new IIOException("Multiple BMHD chunks not allowed"); throw new IIOException("Multiple BMHD chunks not allowed");
} }
return new ILBMForm(formType, (BMHDChunk) chunk, null, colorMap, multiPalette, body); return new ILBMForm(formType, (BMHDChunk) chunk, null, colorMap, multiPalette, thumbnail, body);
} }
else if (chunk instanceof CAMGChunk) { else if (chunk instanceof CAMGChunk) {
if (viewMode != null) { if (viewMode != null) {
throw new IIOException("Multiple CAMG chunks not allowed"); throw new IIOException("Multiple CAMG chunks not allowed");
} }
return new ILBMForm(formType, bitmapHeader, (CAMGChunk) chunk, colorMap, multiPalette, body); return new ILBMForm(formType, bitmapHeader, (CAMGChunk) chunk, colorMap, multiPalette, thumbnail, body);
} }
else if (chunk instanceof CMAPChunk) { else if (chunk instanceof CMAPChunk) {
if (colorMap != null) { if (colorMap != null) {
throw new IIOException("Multiple CMAP chunks not allowed"); throw new IIOException("Multiple CMAP chunks not allowed");
} }
return new ILBMForm(formType, bitmapHeader, viewMode, (CMAPChunk) chunk, multiPalette, body); return new ILBMForm(formType, bitmapHeader, viewMode, (CMAPChunk) chunk, multiPalette, thumbnail, body);
} }
else if (chunk instanceof AbstractMultiPaletteChunk) { else if (chunk instanceof AbstractMultiPaletteChunk) {
// NOTE: We prefer PHCG over SHAM/CTBL style palette changes, if both are present // NOTE: We prefer PHCG over SHAM/CTBL style palette changes, if both are present
@ -223,14 +254,21 @@ abstract class Form {
return this; return this;
} }
return new ILBMForm(formType, bitmapHeader, viewMode, colorMap, (AbstractMultiPaletteChunk) chunk, body); return new ILBMForm(formType, bitmapHeader, viewMode, colorMap, (AbstractMultiPaletteChunk) chunk, thumbnail, body);
}
else if (chunk instanceof XS24Chunk) {
if (thumbnail != null) {
throw new IIOException("Multiple XS24 chunks not allowed");
}
return new ILBMForm(formType, bitmapHeader, viewMode, colorMap, multiPalette, (XS24Chunk) chunk, body);
} }
else if (chunk instanceof BODYChunk) { else if (chunk instanceof BODYChunk) {
if (body != null) { if (body != null) {
throw new IIOException("Multiple " + toChunkStr(chunk.chunkId) + " chunks not allowed"); throw new IIOException("Multiple " + toChunkStr(chunk.chunkId) + " chunks not allowed");
} }
return new ILBMForm(formType, bitmapHeader, viewMode, colorMap, multiPalette, (BODYChunk) chunk); return new ILBMForm(formType, bitmapHeader, viewMode, colorMap, multiPalette, thumbnail, (BODYChunk) chunk);
} }
else if (chunk instanceof GRABChunk) { else if (chunk instanceof GRABChunk) {
// Ignored for now // Ignored for now
@ -257,17 +295,19 @@ abstract class Form {
private final DGBLChunk deepGlobal; private final DGBLChunk deepGlobal;
private final DLOCChunk deepLocation; private final DLOCChunk deepLocation;
private final DPELChunk deepPixel; private final DPELChunk deepPixel;
private final XS24Chunk thumbnail;
private final BODYChunk body; private final BODYChunk body;
DEEPForm(int formType) { DEEPForm(int formType) {
this(formType, null, null, null, null); this(formType, null, null, null, null, null);
} }
private DEEPForm(final int formType, final DGBLChunk deepGlobal, final DLOCChunk deepLocation, final DPELChunk deepPixel, final BODYChunk body) { private DEEPForm(final int formType, final DGBLChunk deepGlobal, final DLOCChunk deepLocation, final DPELChunk deepPixel, final XS24Chunk thumbnail, final BODYChunk body) {
super(formType); super(formType);
this.deepGlobal = deepGlobal; this.deepGlobal = deepGlobal;
this.deepLocation = deepLocation; this.deepLocation = deepLocation;
this.deepPixel = deepPixel; this.deepPixel = deepPixel;
this.thumbnail = thumbnail;
this.body = body; this.body = body;
} }
@ -312,6 +352,26 @@ abstract class Form {
return deepGlobal.yAspect; return deepGlobal.yAspect;
} }
@Override
public boolean hasThumbnail() {
return thumbnail != null;
}
@Override
public int thumbnailWidth() {
return thumbnail != null ? thumbnail.width : -1;
}
@Override
public int thumbnailHeight() {
return thumbnail != null ? thumbnail.height : -1;
}
@Override
public BufferedImage thumbnail() {
return thumbnail != null ? thumbnail.thumbnail() : null;
}
@Override @Override
long bodyOffset() { long bodyOffset() {
return body.chunkOffset; return body.chunkOffset;
@ -329,28 +389,35 @@ abstract class Form {
throw new IIOException("Multiple DGBL chunks not allowed"); throw new IIOException("Multiple DGBL chunks not allowed");
} }
return new DEEPForm(formType, (DGBLChunk) chunk, null, null, body); return new DEEPForm(formType, (DGBLChunk) chunk, null, null, thumbnail, body);
} }
else if (chunk instanceof DLOCChunk) { else if (chunk instanceof DLOCChunk) {
if (deepLocation != null) { if (deepLocation != null) {
throw new IIOException("Multiple DLOC chunks not allowed"); throw new IIOException("Multiple DLOC chunks not allowed");
} }
return new DEEPForm(formType, deepGlobal, (DLOCChunk) chunk, deepPixel, body); return new DEEPForm(formType, deepGlobal, (DLOCChunk) chunk, deepPixel, thumbnail, body);
} }
else if (chunk instanceof DPELChunk) { else if (chunk instanceof DPELChunk) {
if (deepPixel != null) { if (deepPixel != null) {
throw new IIOException("Multiple DPEL chunks not allowed"); throw new IIOException("Multiple DPEL chunks not allowed");
} }
return new DEEPForm(formType, deepGlobal, deepLocation, (DPELChunk) chunk, body); return new DEEPForm(formType, deepGlobal, deepLocation, (DPELChunk) chunk, thumbnail, body);
}
else if (chunk instanceof XS24Chunk) {
if (thumbnail != null) {
throw new IIOException("Multiple XS24 chunks not allowed");
}
return new DEEPForm(formType, deepGlobal, deepLocation, deepPixel, (XS24Chunk) chunk, body);
} }
else if (chunk instanceof BODYChunk) { else if (chunk instanceof BODYChunk) {
if (body != null) { if (body != null) {
throw new IIOException("Multiple " + toChunkStr(chunk.chunkId) + " chunks not allowed"); throw new IIOException("Multiple " + toChunkStr(chunk.chunkId) + " chunks not allowed");
} }
return new DEEPForm(formType, deepGlobal, deepLocation, deepPixel, (BODYChunk) chunk); return new DEEPForm(formType, deepGlobal, deepLocation, deepPixel, thumbnail, (BODYChunk) chunk);
} }
return (DEEPForm) super.with(chunk); return (DEEPForm) super.with(chunk);

View File

@ -143,4 +143,6 @@ interface IFF {
int CHUNK_DBOD = ('D' << 24) + ('B' << 16) + ('O' << 8) + 'D'; int CHUNK_DBOD = ('D' << 24) + ('B' << 16) + ('O' << 8) + 'D';
/** TVPaint Deep CHanGe buffer */ /** TVPaint Deep CHanGe buffer */
int CHUNK_DCHG = ('D' << 24) + ('C' << 16) + ('H' << 8) + 'G'; int CHUNK_DCHG = ('D' << 24) + ('C' << 16) + ('H' << 8) + 'G';
/** TVPaint 24 bit thumbnail */
int CHUNK_XS24 = ('X' << 24) + ('S' << 16) + ('2' << 8) + '4';
} }

View File

@ -255,7 +255,6 @@ final class IFFImageMetadata extends AbstractMetadata {
} }
return text; return text;
} }
@Override @Override

View File

@ -105,7 +105,6 @@ public final class IFFImageReader extends ImageReaderBase {
// - Contains definitions of some "new" chunks, as well as alternative FORM types // - Contains definitions of some "new" chunks, as well as alternative FORM types
// http://amigan.1emu.net/index/iff.html // http://amigan.1emu.net/index/iff.html
// TODO: XS24 chunk seems to be a raw 24 bit thumbnail for TVPaint images: XS24 <4 byte len> <2 byte width> <2 byte height> <pixel data...>
// TODO: Allow reading rasters for HAM6/HAM8 and multipalette images that are expanded to RGB (24 bit) during read. // TODO: Allow reading rasters for HAM6/HAM8 and multipalette images that are expanded to RGB (24 bit) during read.
final static boolean DEBUG = "true".equalsIgnoreCase(System.getProperty("com.twelvemonkeys.imageio.plugins.iff.debug")); final static boolean DEBUG = "true".equalsIgnoreCase(System.getProperty("com.twelvemonkeys.imageio.plugins.iff.debug"));
@ -189,6 +188,12 @@ public final class IFFImageReader extends ImageReaderBase {
header = header.with(deepPixel); header = header.with(deepPixel);
break; break;
case IFF.CHUNK_XS24:
XS24Chunk thumbnail = new XS24Chunk(length);
thumbnail.readChunk(imageInput);
header = header.with(thumbnail);
break;
case IFF.CHUNK_CMAP: case IFF.CHUNK_CMAP:
CMAPChunk colorMap = new CMAPChunk(length); CMAPChunk colorMap = new CMAPChunk(length);
colorMap.readChunk(imageInput); colorMap.readChunk(imageInput);
@ -227,8 +232,8 @@ public final class IFFImageReader extends ImageReaderBase {
case IFF.CHUNK_BODY: case IFF.CHUNK_BODY:
case IFF.CHUNK_DBOD: 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 // NOTE: We don't read the body here, it's done later in the read(int, ImageReadParam) method
BODYChunk body = new BODYChunk(chunkId, length, imageInput.getStreamPosition());
header = header.with(body); header = header.with(body);
// Done reading meta // Done reading meta
@ -277,6 +282,60 @@ public final class IFFImageReader extends ImageReaderBase {
return result; return result;
} }
@Override
public boolean readerSupportsThumbnails() {
return true;
}
@Override
public boolean hasThumbnails(final int imageIndex) throws IOException {
init(imageIndex);
return header.hasThumbnail();
}
@Override
public int getNumThumbnails(final int imageIndex) throws IOException {
init(imageIndex);
return header.hasThumbnail() ? 1 : 0;
}
@Override
public int getThumbnailWidth(final int imageIndex, final int thumbnailIndex) throws IOException {
init(imageIndex);
if (!header.hasThumbnail() || thumbnailIndex > 1) {
throw new IndexOutOfBoundsException("thumbnailIndex out of bounds: " + thumbnailIndex);
}
return header.thumbnailWidth();
}
@Override
public int getThumbnailHeight(final int imageIndex, final int thumbnailIndex) throws IOException {
init(imageIndex);
if (!header.hasThumbnail() || thumbnailIndex > 1) {
throw new IndexOutOfBoundsException("thumbnailIndex out of bounds: " + thumbnailIndex);
}
return header.thumbnailHeight();
}
@Override
public BufferedImage readThumbnail(final int imageIndex, final int thumbnailIndex) throws IOException {
init(imageIndex);
if (!header.hasThumbnail() || thumbnailIndex > 1) {
throw new IndexOutOfBoundsException("thumbnailIndex out of bounds: " + thumbnailIndex);
}
processThumbnailStarted(imageIndex, thumbnailIndex);
BufferedImage thumbnail = header.thumbnail();
processThumbnailProgress(100f);
processThumbnailComplete();
return thumbnail;
}
@Override @Override
public int getWidth(int imageIndex) throws IOException { public int getWidth(int imageIndex) throws IOException {
init(imageIndex); init(imageIndex);

View File

@ -0,0 +1,78 @@
package com.twelvemonkeys.imageio.plugins.iff;
import com.twelvemonkeys.imageio.color.ColorSpaces;
import javax.imageio.IIOException;
import java.awt.*;
import java.awt.color.ColorSpace;
import java.awt.image.*;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
/**
* XS24Chunk.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: XS24Chunk.java,v 1.0 01/02/2022 haraldk Exp$
*/
final class XS24Chunk extends IFFChunk {
private byte[] data;
int width;
int height;
XS24Chunk(final int chunkLength) {
super(IFF.CHUNK_XS24, chunkLength);
}
@Override
void readChunk(final DataInput input) throws IOException {
width = input.readUnsignedShort();
height = input.readUnsignedShort();
input.readShort(); // Not sure what this is?
int dataLength = width * height * 3;
if (dataLength > chunkLength - 6) {
throw new IIOException("Bad XS24 chunk: " + width + " * " + height + " * 3 > chunk length (" + chunkLength + ")");
}
System.err.println("chunkLength: " + chunkLength);
System.err.println("dataLength: " + dataLength);
data = new byte[dataLength];
input.readFully(data);
// Skip pad
for (int i = 0; i < chunkLength - dataLength - 6; i++) {
input.readByte();
}
}
@Override
void writeChunk(final DataOutput output) {
throw new InternalError("Not implemented: writeChunk()");
}
@Override
public String toString() {
return super.toString()
+ "{thumbnail=" + data.length + '}';
}
public BufferedImage thumbnail() {
WritableRaster raster = Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, width, height, width * 3, 3, new int[] {0, 1, 2}, null);
// WritableRaster raster = Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, width, height, width * 3, 3, new int[] {2, 1, 0}, null);
ColorModel colorModel = new ComponentColorModel(ColorSpaces.getColorSpace(ColorSpace.CS_sRGB), false, false, Transparency.OPAQUE, DataBuffer.TYPE_BYTE);
// raster.setDataElements(0, 0, width, height, data);
System.arraycopy(data, 0, ((DataBufferByte) raster.getDataBuffer()).getData(), 0, data.length);
// BufferedImage thumbnail = ImageTypeSpecifiers.createInterleaved(ColorSpaces.getColorSpace(ColorSpace.CS_sRGB), new int[] {1, 2, 0}, DataBuffer.TYPE_BYTE, false, false)
// // BufferedImage thumbnail = ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_3BYTE_BGR)
// .createBufferedImage(width, height);
// thumbnail.getRaster().setDataElements(0, 0, width, height, data);
// return thumbnail;
return new BufferedImage(colorModel, raster, colorModel.isAlphaPremultiplied(), null);
}
}