diff --git a/imageio/imageio-icns/src/main/java/com/twelvemonkeys/imageio/plugins/icns/ICNS.java b/imageio/imageio-icns/src/main/java/com/twelvemonkeys/imageio/plugins/icns/ICNS.java index 87cb0815..12309f6c 100644 --- a/imageio/imageio-icns/src/main/java/com/twelvemonkeys/imageio/plugins/icns/ICNS.java +++ b/imageio/imageio-icns/src/main/java/com/twelvemonkeys/imageio/plugins/icns/ICNS.java @@ -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}; } diff --git a/imageio/imageio-icns/src/main/java/com/twelvemonkeys/imageio/plugins/icns/ICNS1BitColorModel.java b/imageio/imageio-icns/src/main/java/com/twelvemonkeys/imageio/plugins/icns/ICNS1BitColorModel.java new file mode 100644 index 00000000..12a42ff4 --- /dev/null +++ b/imageio/imageio-icns/src/main/java/com/twelvemonkeys/imageio/plugins/icns/ICNS1BitColorModel.java @@ -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 Harald Kuhr + * @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); + } +} diff --git a/imageio/imageio-icns/src/main/java/com/twelvemonkeys/imageio/plugins/icns/ICNS4BitColorModel.java b/imageio/imageio-icns/src/main/java/com/twelvemonkeys/imageio/plugins/icns/ICNS4BitColorModel.java new file mode 100644 index 00000000..10a90f18 --- /dev/null +++ b/imageio/imageio-icns/src/main/java/com/twelvemonkeys/imageio/plugins/icns/ICNS4BitColorModel.java @@ -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 Harald Kuhr + * @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); + } +} diff --git a/imageio/imageio-icns/src/main/java/com/twelvemonkeys/imageio/plugins/icns/ICNS8BitColorModel.java b/imageio/imageio-icns/src/main/java/com/twelvemonkeys/imageio/plugins/icns/ICNS8BitColorModel.java new file mode 100644 index 00000000..19d12f86 --- /dev/null +++ b/imageio/imageio-icns/src/main/java/com/twelvemonkeys/imageio/plugins/icns/ICNS8BitColorModel.java @@ -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 Harald Kuhr + * @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); + } +} diff --git a/imageio/imageio-icns/src/main/java/com/twelvemonkeys/imageio/plugins/icns/ICNSImageReader.java b/imageio/imageio-icns/src/main/java/com/twelvemonkeys/imageio/plugins/icns/ICNSImageReader.java index a4e34d80..842f9a41 100644 --- a/imageio/imageio-icns/src/main/java/com/twelvemonkeys/imageio/plugins/icns/ICNSImageReader.java +++ b/imageio/imageio-icns/src/main/java/com/twelvemonkeys/imageio/plugins/icns/ICNSImageReader.java @@ -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 iconHeaders = new ArrayList(); + private List icons = new ArrayList(); + private List masks = new ArrayList(); + 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 getImageTypes(int imageIndex) throws IOException { + ImageTypeSpecifier rawType = getRawImageType(imageIndex); IconHeader header = readIconHeader(imageIndex); List specifiers = new ArrayList(); 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 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 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); + } + } } diff --git a/imageio/imageio-icns/src/test/java/com/twelvemonkeys/imageio/plugins/icns/ICNSImageReaderTest.java b/imageio/imageio-icns/src/test/java/com/twelvemonkeys/imageio/plugins/icns/ICNSImageReaderTest.java index c8534631..c31da2bd 100644 --- a/imageio/imageio-icns/src/test/java/com/twelvemonkeys/imageio/plugins/icns/ICNSImageReaderTest.java +++ b/imageio/imageio-icns/src/test/java/com/twelvemonkeys/imageio/plugins/icns/ICNSImageReaderTest.java @@ -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 getSuffixes() { return Arrays.asList("icns"); diff --git a/imageio/imageio-icns/src/test/resources/icns/7zIcon.icns b/imageio/imageio-icns/src/test/resources/icns/7zIcon.icns new file mode 100755 index 00000000..32f1cd6a Binary files /dev/null and b/imageio/imageio-icns/src/test/resources/icns/7zIcon.icns differ