Now correctly merges masks and color data.

Added test case with icnV resource.
This commit is contained in:
Harald Kuhr 2011-11-07 18:13:24 +01:00
parent 17f3b97699
commit e867c2125c
7 changed files with 546 additions and 201 deletions

View File

@ -51,7 +51,7 @@ interface ICNS {
/** 16×12 8 bit icon */
int icm8 = ('i' << 24) + ('c' << 16) + ('m' << 8) + '8';
/** 16×16 1-bit mask */
/** 16×16 1-bit icon with 1-bit mask */
int ics_ = ('i' << 24) + ('c' << 16) + ('s' << 8) + '#';
/** 16×16 4-bit icon */
int ics4 = ('i' << 24) + ('c' << 16) + ('s' << 8) + '4';
@ -71,7 +71,7 @@ interface ICNS {
/** 32×32 8-bit mask */
int l8mk = ('l' << 24) + ('8' << 16) + ('m' << 8) + 'k';
/** 48×48 1-bit mask */
/** 48×48 1-bit icon with 1 bit mask */
int ich_ = ('i' << 24) + ('c' << 16) + ('h' << 8) + '#';
/** 48×48 4-bit icon */
int ich4 = ('i' << 24) + ('c' << 16) + ('h' << 8) + '4';
@ -87,41 +87,21 @@ interface ICNS {
/** 128×128 8-bit mask */
int t8mk = ('t' << 24) + ('8' << 16) + ('m' << 8) + 'k';
/** 256×256 JPEG 2000 or PNG icon */
/** 256×256 JPEG 2000 or PNG icon (10.x+) */
int ic08 = ('i' << 24) + ('c' << 16) + ('0' << 8) + '8';
/** 512×512 JPEG 2000 or PNG icon */
/** 512×512 JPEG 2000 or PNG icon (10.x+) */
int ic09 = ('i' << 24) + ('c' << 16) + ('0' << 8) + '9';
/** 1024×1024 PNG icon (10.7)*/
/** 1024×1024 PNG icon (10.7+)*/
int ic10 = ('i' << 24) + ('c' << 16) + ('1' << 8) + '0';
/*
ICN# 256 32 32×32 1-bit mono icon with 1-bit mask
icm# 24 16 16×12 1 bit mask
icm4 96 16 16×12 4 bit icon
icm8 192 16 16×12 8 bit icon
ics# 32 16 16×16 1-bit mask
ics4 128 16 16×16 4-bit icon
ics8 256 16 16x16 8 bit icon
is32 varies (768) 16 16×16 24-bit icon
s8mk 256 16 16x16 8-bit mask
icl4 512 32 32×32 4-bit icon
icl8 1,024 32 32×32 8-bit icon
il32 varies (3,072) 32 32x32 24-bit icon
l8mk 1,024 32 32×32 8-bit mask
ich# 288 48 48×48 1-bit mask
ich4 1,152 48 48×48 4-bit icon
ich8 2,304 48 48×48 8-bit icon
ih32 varies (6,912) 48 48×48 24-bit icon
h8mk 2,304 48 48×48 8-bit mask
it32 varies (49,152) 128 128×128 24-bit icon
t8mk 16,384 128 128×128 8-bit mask
ic08 varies 256 256×256 icon in JPEG 2000 or PNG format
ic09 varies 512 512×512 icon in JPEG 2000 or PNG format
ic10 varies 1024 1024×1024 icon in PNG format (added in Mac OS X 10.7)
*/
/** Unknown */
int icnV = ('i' << 24) + ('c' << 16) + ('n' << 8) + 'V';
/** JPEG 2000 magic header */
byte[] JPEG_2000_MAGIC = new byte[] {0x00, 0x00, 0x00, 0x0C, 'j', 'P', 0x20, 0x20, 0x0D, 0x0A, (byte) 0x87, 0x0A};
/** PNG magic header */
byte[] PNG_MAGIC = new byte[] {(byte) 0x89, (byte) 'P', (byte) 'N', (byte) 'G', 0x0d, 0x0a, 0x1a, 0x0a};
}

View File

@ -0,0 +1,52 @@
/*
* Copyright (c) 2011, 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 "TwelveMonkeys" 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 OWNER 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.icns;
import java.awt.image.DataBuffer;
import java.awt.image.IndexColorModel;
/**
* ICNS1BitColorModel
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: ICNS1BitColorModel.java,v 1.0 07.11.11 10:49 haraldk Exp$
*/
final class ICNS1BitColorModel extends IndexColorModel {
private static final int[] CMAP = {
// Inverted from default Java 1 bit...
0xffffffff, 0xff000000
};
static final IndexColorModel INSTANCE = new ICNS1BitColorModel();
private ICNS1BitColorModel() {
super(1, 2, CMAP, 0, false, -1, DataBuffer.TYPE_BYTE);
}
}

View File

@ -0,0 +1,52 @@
/*
* Copyright (c) 2011, 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 "TwelveMonkeys" 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 OWNER 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.icns;
import java.awt.image.DataBuffer;
import java.awt.image.IndexColorModel;
/**
* ICNS4BitColorModel
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: ICNS4BitColorModel.java,v 1.0 07.11.11 10:49 haraldk Exp$
*/
final class ICNS4BitColorModel extends IndexColorModel {
private static final int[] CMAP = {
0xffffffff, 0xfffcf305, 0xffff6402, 0xffdd0806, 0xfff20884, 0xff4600a5, 0xff0000d4, 0xff02abea,
0xff1fb714, 0xff006411, 0xff562c05, 0xff90713a, 0xffc0c0c0, 0xff808080, 0xff404040, 0xff000000
};
static final IndexColorModel INSTANCE = new ICNS4BitColorModel();
private ICNS4BitColorModel() {
super(4, 16, CMAP, 0, false, -1, DataBuffer.TYPE_BYTE);
}
}

View File

@ -0,0 +1,82 @@
/*
* Copyright (c) 2011, 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 "TwelveMonkeys" 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 OWNER 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.icns;
import java.awt.image.DataBuffer;
import java.awt.image.IndexColorModel;
/**
* ICNS8BitColorModel
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: ICNS8BitColorModel.java,v 1.0 07.11.11 10:49 haraldk Exp$
*/
final class ICNS8BitColorModel extends IndexColorModel {
private static final int[] CMAP = {
0xffffffff, 0xffffffcc, 0xffffff99, 0xffffff66, 0xffffff33, 0xffffff00, 0xffffccff, 0xffffcccc,
0xffffcc99, 0xffffcc66, 0xffffcc33, 0xffffcc00, 0xffff99ff, 0xffff99cc, 0xffff9999, 0xffff9966,
0xffff9933, 0xffff9900, 0xffff66ff, 0xffff66cc, 0xffff6699, 0xffff6666, 0xffff6633, 0xffff6600,
0xffff33ff, 0xffff33cc, 0xffff3399, 0xffff3366, 0xffff3333, 0xffff3300, 0xffff00ff, 0xffff00cc,
0xffff0099, 0xffff0066, 0xffff0033, 0xffff0000, 0xffccffff, 0xffccffcc, 0xffccff99, 0xffccff66,
0xffccff33, 0xffccff00, 0xffccccff, 0xffcccccc, 0xffcccc99, 0xffcccc66, 0xffcccc33, 0xffcccc00,
0xffcc99ff, 0xffcc99cc, 0xffcc9999, 0xffcc9966, 0xffcc9933, 0xffcc9900, 0xffcc66ff, 0xffcc66cc,
0xffcc6699, 0xffcc6666, 0xffcc6633, 0xffcc6600, 0xffcc33ff, 0xffcc33cc, 0xffcc3399, 0xffcc3366,
0xffcc3333, 0xffcc3300, 0xffcc00ff, 0xffcc00cc, 0xffcc0099, 0xffcc0066, 0xffcc0033, 0xffcc0000,
0xff99ffff, 0xff99ffcc, 0xff99ff99, 0xff99ff66, 0xff99ff33, 0xff99ff00, 0xff99ccff, 0xff99cccc,
0xff99cc99, 0xff99cc66, 0xff99cc33, 0xff99cc00, 0xff9999ff, 0xff9999cc, 0xff999999, 0xff999966,
0xff999933, 0xff999900, 0xff9966ff, 0xff9966cc, 0xff996699, 0xff996666, 0xff996633, 0xff996600,
0xff9933ff, 0xff9933cc, 0xff993399, 0xff993366, 0xff993333, 0xff993300, 0xff9900ff, 0xff9900cc,
0xff990099, 0xff990066, 0xff990033, 0xff990000, 0xff66ffff, 0xff66ffcc, 0xff66ff99, 0xff66ff66,
0xff66ff33, 0xff66ff00, 0xff66ccff, 0xff66cccc, 0xff66cc99, 0xff66cc66, 0xff66cc33, 0xff66cc00,
0xff6699ff, 0xff6699cc, 0xff669999, 0xff669966, 0xff669933, 0xff669900, 0xff6666ff, 0xff6666cc,
0xff666699, 0xff666666, 0xff666633, 0xff666600, 0xff6633ff, 0xff6633cc, 0xff663399, 0xff663366,
0xff663333, 0xff663300, 0xff6600ff, 0xff6600cc, 0xff660099, 0xff660066, 0xff660033, 0xff660000,
0xff33ffff, 0xff33ffcc, 0xff33ff99, 0xff33ff66, 0xff33ff33, 0xff33ff00, 0xff33ccff, 0xff33cccc,
0xff33cc99, 0xff33cc66, 0xff33cc33, 0xff33cc00, 0xff3399ff, 0xff3399cc, 0xff339999, 0xff339966,
0xff339933, 0xff339900, 0xff3366ff, 0xff3366cc, 0xff336699, 0xff336666, 0xff336633, 0xff336600,
0xff3333ff, 0xff3333cc, 0xff333399, 0xff333366, 0xff333333, 0xff333300, 0xff3300ff, 0xff3300cc,
0xff330099, 0xff330066, 0xff330033, 0xff330000, 0xff00ffff, 0xff00ffcc, 0xff00ff99, 0xff00ff66,
0xff00ff33, 0xff00ff00, 0xff00ccff, 0xff00cccc, 0xff00cc99, 0xff00cc66, 0xff00cc33, 0xff00cc00,
0xff0099ff, 0xff0099cc, 0xff009999, 0xff009966, 0xff009933, 0xff009900, 0xff0066ff, 0xff0066cc,
0xff006699, 0xff006666, 0xff006633, 0xff006600, 0xff0033ff, 0xff0033cc, 0xff003399, 0xff003366,
0xff003333, 0xff003300, 0xff0000ff, 0xff0000cc, 0xff000099, 0xff000066, 0xff000033, 0xffee0000,
0xffdd0000, 0xffbb0000, 0xffaa0000, 0xff880000, 0xff770000, 0xff550000, 0xff440000, 0xff220000,
0xff110000, 0xff00ee00, 0xff00dd00, 0xff00bb00, 0xff00aa00, 0xff008800, 0xff007700, 0xff005500,
0xff004400, 0xff002200, 0xff001100, 0xff0000ee, 0xff0000dd, 0xff0000bb, 0xff0000aa, 0xff000088,
0xff000077, 0xff000055, 0xff000044, 0xff000022, 0xff000011, 0xffeeeeee, 0xffdddddd, 0xffbbbbbb,
0xffaaaaaa, 0xff888888, 0xff777777, 0xff555555, 0xff444444, 0xff222222, 0xff111111, 0xff000000
};
static final IndexColorModel INSTANCE = new ICNS8BitColorModel();
private ICNS8BitColorModel() {
super(8, 256, CMAP, 0, false, -1, DataBuffer.TYPE_BYTE);
}
}

View File

@ -30,6 +30,8 @@ package com.twelvemonkeys.imageio.plugins.icns;
import com.twelvemonkeys.imageio.ImageReaderBase;
import com.twelvemonkeys.imageio.util.IIOUtil;
import com.twelvemonkeys.imageio.util.IndexedImageTypeSpecifier;
import com.twelvemonkeys.lang.Validate;
import javax.imageio.*;
import javax.imageio.spi.ImageReaderSpi;
@ -57,7 +59,9 @@ public final class ICNSImageReader extends ImageReaderBase {
// TODO: Merge masks with icon in front + calculate image count based on this...
private static final int HEADER_SIZE = 8;
private List<IconHeader> iconHeaders = new ArrayList<IconHeader>();
private List<IconHeader> icons = new ArrayList<IconHeader>();
private List<IconHeader> masks = new ArrayList<IconHeader>();
private int length;
public ICNSImageReader() {
@ -70,6 +74,10 @@ public final class ICNSImageReader extends ImageReaderBase {
@Override
protected void resetMembers() {
length = 0;
icons.clear();
masks.clear();
}
@Override
@ -82,31 +90,52 @@ public final class ICNSImageReader extends ImageReaderBase {
return readIconHeader(imageIndex).size().height;
}
@Override
public ImageTypeSpecifier getRawImageType(int imageIndex) throws IOException {
IconHeader header = readIconHeader(imageIndex);
switch (header.depth()) {
case 1:
return IndexedImageTypeSpecifier.createFromIndexColorModel(ICNS1BitColorModel.INSTANCE);
case 4:
return IndexedImageTypeSpecifier.createFromIndexColorModel(ICNS4BitColorModel.INSTANCE);
case 8:
return IndexedImageTypeSpecifier.createFromIndexColorModel(ICNS8BitColorModel.INSTANCE);
case 32:
int bandLen = header.size().width * header.size().height;
return ImageTypeSpecifier.createBanded(ColorSpace.getInstance(ColorSpace.CS_sRGB), new int[]{0, 1, 2, 3}, new int[]{0, bandLen, 2 * bandLen, 3 * bandLen}, DataBuffer.TYPE_BYTE, true, false);
default:
throw new IllegalStateException(String.format("Unknown bit depth: %d", header.depth()));
}
}
@Override
public Iterator<ImageTypeSpecifier> getImageTypes(int imageIndex) throws IOException {
ImageTypeSpecifier rawType = getRawImageType(imageIndex);
IconHeader header = readIconHeader(imageIndex);
List<ImageTypeSpecifier> specifiers = new ArrayList<ImageTypeSpecifier>();
switch (header.depth()) {
case 1:
specifiers.add(ImageTypeSpecifier.createGrayscale(1, DataBuffer.TYPE_BYTE, false));
// Fall through
// break;
// TODO: Fall through & convert during read?
case 4:
specifiers.add(ImageTypeSpecifier.createGrayscale(4, DataBuffer.TYPE_BYTE, false));
// Fall through
// break;
// TODO: Fall through & convert during read?
case 8:
specifiers.add(ImageTypeSpecifier.createGrayscale(8, DataBuffer.TYPE_BYTE, false));
// Fall through
case 24:
specifiers.add(ImageTypeSpecifier.createInterleaved(ColorSpace.getInstance(ColorSpace.CS_sRGB), new int[]{0, 1, 2}, DataBuffer.TYPE_BYTE, false, false));
// break;
// TODO: Fall through & convert during read?
case 32:
specifiers.add(ImageTypeSpecifier.createInterleaved(ColorSpace.getInstance(ColorSpace.CS_sRGB), new int[]{0, 1, 2, 3}, DataBuffer.TYPE_BYTE, true, false));
specifiers.add(ImageTypeSpecifier.createPacked(ColorSpace.getInstance(ColorSpace.CS_sRGB), 0xff0000, 0x00ff00, 0x0000ff, 0xff000000, DataBuffer.TYPE_INT, false));
specifiers.add(ImageTypeSpecifier.createInterleaved(ColorSpace.getInstance(ColorSpace.CS_sRGB), new int[]{3, 2, 1, 0}, DataBuffer.TYPE_BYTE, true, false));
break;
default:
throw new IllegalStateException(String.format("Unknown bit depth: %d", header.depth()));
}
specifiers.add(rawType);
return specifiers.iterator();
}
@ -115,21 +144,21 @@ public final class ICNSImageReader extends ImageReaderBase {
assertInput();
if (!allowSearch) {
// Return icons.size if we know we have read all?
return -1;
}
int num = iconHeaders.size();
int num = icons.size();
while (true) {
try {
readIconHeader(num);
num++;
readIconHeader(num++);
}
catch (IndexOutOfBoundsException expected) {
break;
}
}
return num;
return icons.size();
}
@Override
@ -139,86 +168,55 @@ public final class ICNSImageReader extends ImageReaderBase {
imageInput.seek(header.start + HEADER_SIZE);
// TODO: Extract in separate method/class
// Special handling of PNG/JPEG 2000 icons
if (header.isForeignFormat()) {
ImageInputStream stream = ImageIO.createImageInputStream(IIOUtil.createStreamAdapter(imageInput, header.length));
try {
// 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
// JPEG2000 magic bytes: 00 00 00 0C 6A 50 20 20 0D 0A 87 0A 00 00 00 14 66 74 79 70 6A 70 32
// 00 00 00 0C 6A 50 20 20 0D 0A 87 0A
// 12 j P sp sp \r \n
byte[] magic = new byte[12];
stream.readFully(magic);
// System.out.println("magic: " + Arrays.toString(magic));
String format;
if (Arrays.equals(ICNS.PNG_MAGIC, magic)) {
format = "PNG";
}
else if (Arrays.equals(ICNS.JPEG_2000_MAGIC, magic)) {
format = "JPEG 2000";
}
else {
format = "unknown";
}
stream.seek(0);
Iterator<ImageReader> readers = ImageIO.getImageReaders(stream);
while (readers.hasNext()) {
ImageReader reader = readers.next();
reader.setInput(stream);
try {
return reader.read(0, param);
}
catch (IOException ignore) {
}
stream.seek(0);
}
// TODO: There's no JPEG 2000 reader installed in ImageIO by default (requires JAI ImageIO installed)
// TODO: Return blank icon? We know the image dimensions, we just can't read the data... Return blank image? Pretend it's not in the stream? ;-)
// TODO: Create JPEG 2000 reader..? :-P
throw new IIOException(String.format(
"Cannot read %s format in type '%s' icon (no reader; installed: %s)",
format, ICNSUtil.intToStr(header.type), Arrays.toString(ImageIO.getReaderFormatNames())
));
}
finally {
stream.close();
}
return readForeignFormat(param, header);
}
return readICNSFormat(imageIndex, param, header);
}
private BufferedImage readICNSFormat(final int imageIndex, final ImageReadParam param, final IconHeader header) throws IOException {
Dimension size = header.size();
int width = size.width;
int height = size.height;
BufferedImage image = getDestination(param, getImageTypes(imageIndex), width, height);
ImageTypeSpecifier rawType = getRawImageType(imageIndex);
checkReadParamBandSettings(param, rawType.getNumBands(), image.getSampleModel().getNumBands());
if (rawType instanceof IndexedImageTypeSpecifier && rawType.getBufferedImageType() != image.getType()) {
checkReadParamBandSettings(param, 4, image.getSampleModel().getNumBands());
}
else {
checkReadParamBandSettings(param, rawType.getNumBands(), image.getSampleModel().getNumBands());
}
final Rectangle source = new Rectangle();
final Rectangle dest = new Rectangle();
computeRegions(param, width, height, image, source, dest);
processImageStarted(imageIndex);
// Read image data
byte[] data;
if (header.isPackbits()) {
if (header.isCompressed()) {
// Only 32 bit icons may be compressed
data = new byte[width * height * header.depth() / 8];
int packedSize = header.length - HEADER_SIZE;
if (width >= 128 && height >= 128) {
imageInput.skipBytes(4);
imageInput.skipBytes(4); // Seems to be 4 byte 0-pad
packedSize -= 4;
}
InputStream input = IIOUtil.createStreamAdapter(imageInput, packedSize);
unpackbits(new DataInputStream(input), data, 0, data.length);
input.close();
try {
decompress(new DataInputStream(input), data, 0, (data.length * 24) / 32); // 24 bit data
}
finally {
input.close();
}
}
else {
data = new byte[header.length - HEADER_SIZE];
@ -232,47 +230,183 @@ public final class ICNSImageReader extends ImageReaderBase {
break;
case 8:
break;
case 24:
case 32:
break;
default:
throw new IllegalStateException(String.format("Unknown bit depth for icon: %d", header.depth()));
}
if (header.depth() <= 8) {
if (header.depth() == 1) {
DataBufferByte buffer = new DataBufferByte(data, data.length / 2, 0);
WritableRaster raster = Raster.createPackedRaster(buffer, width, height, header.depth(), null);
if (image.getType() == rawType.getBufferedImageType() && ((IndexColorModel) image.getColorModel()).getMapSize() == 2) {
image.setData(raster);
}
else {
DataBufferByte maskBuffer = new DataBufferByte(data, data.length / 2, data.length / 2);
WritableRaster mask = Raster.createPackedRaster(maskBuffer, width, height, header.depth(), null);
Graphics2D graphics = image.createGraphics();
try {
BufferedImage temp = new BufferedImage(rawType.getColorModel(), raster, false, null);
graphics.drawImage(temp, 0, 0, null);
temp = new BufferedImage(ICNSBitMaskColorModel.INSTANCE, mask, false, null);
temp.setData(mask);
graphics.setComposite(AlphaComposite.DstIn);
graphics.drawImage(temp, 0, 0, null);
}
finally {
graphics.dispose();
}
}
}
else if (header.depth() <= 8) {
DataBufferByte buffer = new DataBufferByte(data, data.length);
image.setData(Raster.createPackedRaster(buffer, width, height, header.depth(), null));
WritableRaster raster = Raster.createPackedRaster(buffer, width, height, header.depth(), null);
if (image.getType() == rawType.getBufferedImageType()) {
image.setData(raster);
}
else {
Graphics2D graphics = image.createGraphics();
try {
BufferedImage temp = new BufferedImage(rawType.getColorModel(), raster, false, null);
graphics.drawImage(temp, 0, 0, null);
}
finally {
graphics.dispose();
}
processImageProgress(50f);
// Look up/read mask from later IconHeader and apply
Raster mask = readMask(findMask(header));
image.getAlphaRaster().setRect(mask);
}
}
else {
// System.err.println("image: " + image);
// DataBufferByte buffer = new DataBufferByte(data, data.length);
// WritableRaster raster = Raster.createInterleavedRaster(buffer, width, height, width * header.depth() / 8, header.depth() / 8, new int[]{0, 1, 2}, null);
// WritableRaster raster = Raster.createInterleavedRaster(buffer, width, height, width * header.depth() / 8, header.depth() / 8, new int[]{0, 1, 2, 3}, null);
// int bandLen = data.length / 4;
// DataBufferByte buffer = new DataBufferByte(data, data.length);
// WritableRaster raster = Raster.createBandedRaster(buffer, width, height, width, new int[]{0, 0, 0, 0}, new int[]{0, bandLen, bandLen * 2, bandLen * 3}, null);
int bandLen = data.length / 3;
int bandLen = data.length / 4;
DataBufferByte buffer = new DataBufferByte(data, data.length);
WritableRaster raster = Raster.createBandedRaster(buffer, width, height, width, new int[]{0, 0, 0}, new int[]{0, bandLen, bandLen * 2}, null);
ColorModel cm = new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_sRGB), false, false, Transparency.OPAQUE, DataBuffer.TYPE_BYTE);
WritableRaster raster = Raster.createBandedRaster(buffer, width, height, width, new int[]{0, 0, 0, 0}, new int[]{0, bandLen, bandLen * 2, bandLen * 3}, null);
image.setData(raster);
BufferedImage temp = new BufferedImage(cm, raster, cm.isAlphaPremultiplied(), null);
// showIt(temp, "foo");
processImageProgress(75f);
// image.setData(raster);
Graphics2D graphics = image.createGraphics();
try {
graphics.drawImage(temp, 0, 0, null);
}
finally {
graphics.dispose();
}
// Read mask from later IconHeader and apply
Raster mask = readMask(findMask(header));
image.getAlphaRaster().setRect(mask);
}
// For now: Make listener tests happy
// TODO: Implement more sophisticated reading
processImageProgress(100f);
if (abortRequested()) {
processReadAborted();
}
else {
processImageComplete();
}
return image;
}
private Raster readMask(IconHeader header) throws IOException {
Dimension size = header.size();
int width = size.width;
int height = size.height;
byte[] alpha = new byte[header.length - HEADER_SIZE];
imageInput.seek(header.start + HEADER_SIZE);
imageInput.readFully(alpha);
return Raster.createBandedRaster(new DataBufferByte(alpha, alpha.length), width, height, width, new int[]{0}, new int[]{0}, null);
}
private IconHeader findMask(final IconHeader icon) throws IOException {
try {
int i = 0;
while (true) {
IconHeader mask = i < masks.size() ? masks.get(i++) : readNextIconHeader();
if (mask.isMask() && mask.size().equals(icon.size())) {
return mask;
}
}
}
catch (IndexOutOfBoundsException ignore) {
}
throw new IIOException(String.format("No mask for icon: %s", icon));
}
private BufferedImage readForeignFormat(final ImageReadParam param, final IconHeader header) throws IOException {
ImageInputStream stream = ImageIO.createImageInputStream(IIOUtil.createStreamAdapter(imageInput, header.length));
try {
Iterator<ImageReader> readers = ImageIO.getImageReaders(stream);
while (readers.hasNext()) {
ImageReader reader = readers.next();
reader.setInput(stream);
try {
return reader.read(0, param);
}
catch (IOException ignore) {
}
finally {
stream.seek(0);
}
}
// There's no JPEG 2000 reader installed in ImageIO by default (requires JAI ImageIO installed).
// The current implementation is correct, but a bit harsh maybe..? Other options:
// TODO: Return blank icon + issue warning? We know the image dimensions, we just can't read the data...
// TODO: Pretend it's not in the stream + issue warning?
// TODO: Create JPEG 2000 reader..? :-P
throw new IIOException(String.format(
"Cannot read %s format in type '%s' icon (no reader; installed: %s)",
getForeignFormat(stream), ICNSUtil.intToStr(header.type), Arrays.toString(ImageIO.getReaderFormatNames())
));
}
finally {
stream.close();
}
}
private String getForeignFormat(final ImageInputStream stream) throws IOException {
byte[] magic = new byte[12]; // Length of JPEG 2000 magic
try {
stream.readFully(magic);
}
finally {
stream.seek(0);
}
String format;
if (Arrays.equals(ICNS.PNG_MAGIC, magic)) {
format = "PNG";
}
else if (Arrays.equals(ICNS.JPEG_2000_MAGIC, magic)) {
format = "JPEG 2000";
}
else {
format = "unknown";
}
return format;
}
// http://www.macdisk.com/maciconen.php
// TODO: Is this really packbits?! Don't think so, but it's very close...
static void unpackbits(final DataInputStream input, final byte[] result, int offset, int length) throws IOException {
static void decompress(final DataInputStream input, final byte[] result, int offset, int length) throws IOException {
int resultPos = offset;
int remaining = length;
@ -282,11 +416,11 @@ public final class ICNSImageReader extends ImageReaderBase {
if ((run & 0x80) != 0) {
// Repeated run
runLength = run + 131; // Packbits says: -run + 1 and 0x80 should be no-op... This inverts the lengths, but allows longer runs...
runLength = run + 131; // Packbits: -run + 1 and run == 0x80 is no-op... This allows 1 byte longer runs...
byte runData = input.readByte();
for (int i = 0; i < runLength; i++) {
result[resultPos++] = runData;
result[resultPos++] = runData;
}
}
else {
@ -301,27 +435,43 @@ public final class ICNSImageReader extends ImageReaderBase {
}
}
private IconHeader readIconHeader(int imageIndex) throws IOException {
private IconHeader readIconHeader(final int imageIndex) throws IOException {
checkBounds(imageIndex);
readeFileHeader();
if (iconHeaders.size() <= imageIndex) {
int lastReadIndex = iconHeaders.size() - 1;
IconHeader lastRead = iconHeaders.isEmpty() ? null : iconHeaders.get(lastReadIndex);
for (int i = lastReadIndex; i < imageIndex; i++) {
imageInput.seek(lastRead == null ? HEADER_SIZE : lastRead.start + lastRead.length);
if (imageInput.getStreamPosition() >= length) {
throw new IndexOutOfBoundsException();
}
lastRead = IconHeader.read(imageInput);
iconHeaders.add(lastRead);
}
while (icons.size() <= imageIndex) {
readNextIconHeader();
}
return iconHeaders.get(imageIndex);
return icons.get(imageIndex);
}
private IconHeader readNextIconHeader() throws IOException {
IconHeader lastIcon = icons.isEmpty() ? null : icons.get(icons.size() - 1);
IconHeader lastMask = masks.isEmpty() ? null : masks.get(masks.size() - 1);
long lastReadPos = Math.max(
lastIcon == null ? HEADER_SIZE : lastIcon.start + lastIcon.length,
lastMask == null ? HEADER_SIZE : lastMask.start + lastMask.length
);
imageInput.seek(lastReadPos);
if (imageInput.getStreamPosition() >= length) {
throw new IndexOutOfBoundsException();
}
IconHeader header = IconHeader.read(imageInput);
// Filter out special case icnV (version?), as this isn't really an icon..
if (header.isMask() || header.type == ICNS.icnV) {
masks.add(header);
}
else {
icons.add(header);
}
return header;
}
private void readeFileHeader() throws IOException {
@ -359,64 +509,51 @@ public final class ICNSImageReader extends ImageReaderBase {
private void validate(int type, int length) {
switch (type) {
case ICNS.ICON:
if (length == 128) {
return;
}
validateLengthForType(type, length, 128);
break;
case ICNS.ICN_:
if (length == 256) {
return;
}
validateLengthForType(type, length, 256);
break;
case ICNS.icm_:
if (length == 24) {
return;
}
validateLengthForType(type, length, 24);
break;
case ICNS.icm4:
if (length == 96) {
return;
}
validateLengthForType(type, length, 96);
break;
case ICNS.icm8:
if (length == 192) {
return;
}
validateLengthForType(type, length, 192);
break;
case ICNS.ics_:
if (length == 32) {
return;
}
validateLengthForType(type, length, 64);
break;
case ICNS.ics4:
if (length == 128) {
return;
}
validateLengthForType(type, length, 128);
break;
case ICNS.ics8:
case ICNS.s8mk:
if (length == 256) {
return;
}
validateLengthForType(type, length, 256);
break;
case ICNS.icl4:
if (length == 512) {
return;
}
validateLengthForType(type, length, 512);
break;
case ICNS.icl8:
case ICNS.l8mk:
if (length == 1024) {
return;
}
validateLengthForType(type, length, 1024);
break;
case ICNS.ich_:
if (length == 288) {
return;
}
// validateLengthForType(type, length, 288);
validateLengthForType(type, length, 576);
break;
case ICNS.ich4:
if (length == 1152) {
return;
}
validateLengthForType(type, length, 1152);
break;
case ICNS.ich8:
case ICNS.h8mk:
if (length == 2034) {
return;
}
validateLengthForType(type, length, 2304);
break;
case ICNS.t8mk:
if (length == 16384) {
return;
}
validateLengthForType(type, length, 16384);
break;
case ICNS.ih32:
case ICNS.is32:
case ICNS.il32:
@ -425,12 +562,26 @@ public final class ICNSImageReader extends ImageReaderBase {
case ICNS.ic09:
case ICNS.ic10:
if (length > 0) {
return;
break;
}
throw new IllegalArgumentException(String.format("Wrong combination of icon type '%s' and length: %d", ICNSUtil.intToStr(type), length));
case ICNS.icnV:
validateLengthForType(type, length, 4);
break;
default:
throw new IllegalStateException(String.format("Unknown icon type: '%s'", ICNSUtil.intToStr(type)));
}
}
private void validateLengthForType(int type, int length, final int expectedLength) {
Validate.isTrue(
length == expectedLength + HEADER_SIZE, // Compute to make lengths more logical
String.format(
"Wrong combination of icon type '%s' and length: %d (expected: %d)",
ICNSUtil.intToStr(type), length - HEADER_SIZE, expectedLength
)
);
}
public Dimension size() {
@ -502,17 +653,29 @@ public final class ICNSImageReader extends ImageReaderBase {
case ICNS.ic08:
case ICNS.ic09:
case ICNS.ic10:
return 24;
return 32;
default:
throw new IllegalStateException(String.format("Unknown icon type: '%s'", ICNSUtil.intToStr(type)));
}
}
public boolean isPackbits() {
public boolean isMask() {
switch (type) {
case ICNS.s8mk:
case ICNS.l8mk:
case ICNS.h8mk:
case ICNS.t8mk:
return true;
}
return false;
}
public boolean isCompressed() {
switch (type) {
case ICNS.ih32:
case ICNS.il32:
case ICNS.is32:
case ICNS.il32:
case ICNS.ih32:
case ICNS.it32:
return true;
}
@ -550,6 +713,7 @@ public final class ICNSImageReader extends ImageReaderBase {
}
}
@SuppressWarnings({"UnusedAssignment"})
public static void main(String[] args) throws IOException {
int argIndex = 0;
@ -568,7 +732,7 @@ public final class ICNSImageReader extends ImageReaderBase {
for (int i = start; i < numImages; i++) {
try {
BufferedImage image = reader.read(i);
System.err.println("image: " + image);
// System.err.println("image: " + image);
showIt(image, String.format("%s - %d", input.getName(), i));
}
catch (IIOException e) {
@ -576,4 +740,12 @@ public final class ICNSImageReader extends ImageReaderBase {
}
}
}
private static final class ICNSBitMaskColorModel extends IndexColorModel {
static final IndexColorModel INSTANCE = new ICNSBitMaskColorModel();
private ICNSBitMaskColorModel() {
super(1, 2, new int[]{0, 0xffffffff}, 0, true, 0, DataBuffer.TYPE_BYTE);
}
}
}

View File

@ -49,20 +49,28 @@ public class ICNSImageReaderTest extends ImageReaderAbstractTestCase {
return Arrays.asList(
new TestData(
getClassLoaderResource("/icns/GenericJavaApp.icns"),
new Dimension(16, 16), new Dimension(16, 16), // 1 bit, 8 bit
new Dimension(16, 16), new Dimension(16, 16), // 24 bit + 8 bit mask
new Dimension(32, 32), new Dimension(32, 32),
new Dimension(32, 32), new Dimension(32, 32),
new Dimension(128, 128), new Dimension(128, 128)
new Dimension(16, 16), // 1 bit + 1 bit mask
new Dimension(16, 16), new Dimension(16, 16), // 8 bit CMAP, 32 bit
new Dimension(32, 32), // 1 bit + 1 bit mask
new Dimension(32, 32), new Dimension(32, 32), // 8 bit CMAP, 32 bit
new Dimension(128, 128) // 32 bit
),
new TestData(
getClassLoaderResource("/icns/Apple Retro.icns"),
new Dimension(16, 16), new Dimension(16, 16), // 24 bit + 8 bit mask
new Dimension(32, 32), new Dimension(32, 32), // 24 bit + 8 bit mask
new Dimension(48, 48), new Dimension(48, 48) ,// 24 bit + 8 bit mask
new Dimension(128, 128), new Dimension(128, 128), // 24 bit + 8 bit mask
new Dimension(256, 256), // JPEG 2000
new Dimension(512, 512) // JPEG 2000
new Dimension(16, 16), // 24 bit + 8 bit mask
new Dimension(32, 32), // 24 bit + 8 bit mask
new Dimension(48, 48), // 24 bit + 8 bit mask
new Dimension(128, 128) // 24 bit + 8 bit mask
//, new Dimension(256, 256), // JPEG 2000, not readable without JAI or other JPEG 2000 support
// new Dimension(512, 512) // JPEG 2000
),
new TestData(
getClassLoaderResource("/icns/7zIcon.icns"), // Contains the icnV resource, that isn't an icon
new Dimension(16, 16), // 24 bit + 8 bit mask
new Dimension(32, 32), // 24 bit + 8 bit mask
new Dimension(128, 128) // 24 bit + 8 bit mask
//, new Dimension(256, 256), // JPEG 2000
// new Dimension(512, 512) // JPEG 2000
)
);
}
@ -87,7 +95,6 @@ public class ICNSImageReaderTest extends ImageReaderAbstractTestCase {
return Arrays.asList("icns");
}
@Override
protected List<String> getSuffixes() {
return Arrays.asList("icns");

Binary file not shown.