mirror of
https://github.com/haraldk/TwelveMonkeys.git
synced 2025-08-05 20:45:29 -04:00
Added test case for icon containing TOC_ + IC10 resources + fixed PNG reading and skipping of unknown resources.
Added test case for icon with no 8 bit mask + fixed fallback to 1 bit mask. Added test case for icon with no mask + fixed transparency issue.
This commit is contained in:
parent
3a9ad582f2
commit
9742af9a5d
@ -96,9 +96,12 @@ interface ICNS {
|
|||||||
/** 1024×1024 PNG icon (10.7+)*/
|
/** 1024×1024 PNG icon (10.7+)*/
|
||||||
int ic10 = ('i' << 24) + ('c' << 16) + ('1' << 8) + '0';
|
int ic10 = ('i' << 24) + ('c' << 16) + ('1' << 8) + '0';
|
||||||
|
|
||||||
/** Unknown */
|
/** Unknown (Version) */
|
||||||
int icnV = ('i' << 24) + ('c' << 16) + ('n' << 8) + 'V';
|
int icnV = ('i' << 24) + ('c' << 16) + ('n' << 8) + 'V';
|
||||||
|
|
||||||
|
/** Unknown (Table of Contents) */
|
||||||
|
int TOC_ = ('T' << 24) + ('O' << 16) + ('C' << 8) + ' ';
|
||||||
|
|
||||||
/** JPEG 2000 magic header */
|
/** JPEG 2000 magic header */
|
||||||
byte[] JPEG_2000_MAGIC = new byte[] {0x00, 0x00, 0x00, 0x0C, 'j', 'P', 0x20, 0x20, 0x0D, 0x0A, (byte) 0x87, 0x0A};
|
byte[] JPEG_2000_MAGIC = new byte[] {0x00, 0x00, 0x00, 0x0C, 'j', 'P', 0x20, 0x20, 0x0D, 0x0A, (byte) 0x87, 0x0A};
|
||||||
|
|
||||||
|
@ -59,9 +59,14 @@ import java.util.List;
|
|||||||
* @see <a href="http://en.wikipedia.org/wiki/Apple_Icon_Image_format">Apple Icon Image format (Wikipedia)</a>
|
* @see <a href="http://en.wikipedia.org/wiki/Apple_Icon_Image_format">Apple Icon Image format (Wikipedia)</a>
|
||||||
*/
|
*/
|
||||||
public final class ICNSImageReader extends ImageReaderBase {
|
public final class ICNSImageReader extends ImageReaderBase {
|
||||||
private static final int HEADER_SIZE = 8;
|
// TODO: Support ToC resource for faster parsing/faster determine number of icons?
|
||||||
private List<IconHeader> icons = new ArrayList<IconHeader>();
|
// TODO: Subsampled reading for completeness, even if never used?
|
||||||
private List<IconHeader> masks = new ArrayList<IconHeader>();
|
|
||||||
|
private static final int RESOURCE_HEADER_SIZE = 8;
|
||||||
|
|
||||||
|
private List<IconResource> icons = new ArrayList<IconResource>();
|
||||||
|
private List<IconResource> masks = new ArrayList<IconResource>();
|
||||||
|
private IconResource lastResourceRead;
|
||||||
|
|
||||||
private int length;
|
private int length;
|
||||||
|
|
||||||
@ -76,26 +81,27 @@ public final class ICNSImageReader extends ImageReaderBase {
|
|||||||
@Override
|
@Override
|
||||||
protected void resetMembers() {
|
protected void resetMembers() {
|
||||||
length = 0;
|
length = 0;
|
||||||
|
|
||||||
|
lastResourceRead = null;
|
||||||
icons.clear();
|
icons.clear();
|
||||||
masks.clear();
|
masks.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getWidth(int imageIndex) throws IOException {
|
public int getWidth(int imageIndex) throws IOException {
|
||||||
return readIconHeader(imageIndex).size().width;
|
return readIconResource(imageIndex).size().width;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getHeight(int imageIndex) throws IOException {
|
public int getHeight(int imageIndex) throws IOException {
|
||||||
return readIconHeader(imageIndex).size().height;
|
return readIconResource(imageIndex).size().height;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ImageTypeSpecifier getRawImageType(int imageIndex) throws IOException {
|
public ImageTypeSpecifier getRawImageType(int imageIndex) throws IOException {
|
||||||
IconHeader header = readIconHeader(imageIndex);
|
IconResource resource = readIconResource(imageIndex);
|
||||||
|
|
||||||
switch (header.depth()) {
|
switch (resource.depth()) {
|
||||||
case 1:
|
case 1:
|
||||||
return IndexedImageTypeSpecifier.createFromIndexColorModel(ICNS1BitColorModel.INSTANCE);
|
return IndexedImageTypeSpecifier.createFromIndexColorModel(ICNS1BitColorModel.INSTANCE);
|
||||||
case 4:
|
case 4:
|
||||||
@ -103,13 +109,22 @@ public final class ICNSImageReader extends ImageReaderBase {
|
|||||||
case 8:
|
case 8:
|
||||||
return IndexedImageTypeSpecifier.createFromIndexColorModel(ICNS8BitColorModel.INSTANCE);
|
return IndexedImageTypeSpecifier.createFromIndexColorModel(ICNS8BitColorModel.INSTANCE);
|
||||||
case 32:
|
case 32:
|
||||||
return ImageTypeSpecifier.createBanded(
|
if (resource.isCompressed()) {
|
||||||
ColorSpace.getInstance(ColorSpace.CS_sRGB),
|
return ImageTypeSpecifier.createBanded(
|
||||||
new int[]{0, 1, 2, 3}, createBandOffsets(header.size().width * header.size().height),
|
ColorSpace.getInstance(ColorSpace.CS_sRGB),
|
||||||
DataBuffer.TYPE_BYTE, true, false
|
new int[]{0, 1, 2, 3}, createBandOffsets(resource.size().width * resource.size().height),
|
||||||
);
|
DataBuffer.TYPE_BYTE, true, false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return ImageTypeSpecifier.createInterleaved(
|
||||||
|
ColorSpace.getInstance(ColorSpace.CS_sRGB),
|
||||||
|
new int[]{1, 2, 3, 0},
|
||||||
|
DataBuffer.TYPE_BYTE, true, false
|
||||||
|
);
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
throw new IllegalStateException(String.format("Unknown bit depth: %d", header.depth()));
|
throw new IllegalStateException(String.format("Unknown bit depth: %d", resource.depth()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,11 +135,11 @@ public final class ICNSImageReader extends ImageReaderBase {
|
|||||||
@Override
|
@Override
|
||||||
public Iterator<ImageTypeSpecifier> getImageTypes(int imageIndex) throws IOException {
|
public Iterator<ImageTypeSpecifier> getImageTypes(int imageIndex) throws IOException {
|
||||||
ImageTypeSpecifier rawType = getRawImageType(imageIndex);
|
ImageTypeSpecifier rawType = getRawImageType(imageIndex);
|
||||||
IconHeader header = readIconHeader(imageIndex);
|
IconResource resource = readIconResource(imageIndex);
|
||||||
|
|
||||||
List<ImageTypeSpecifier> specifiers = new ArrayList<ImageTypeSpecifier>();
|
List<ImageTypeSpecifier> specifiers = new ArrayList<ImageTypeSpecifier>();
|
||||||
|
|
||||||
switch (header.depth()) {
|
switch (resource.depth()) {
|
||||||
case 1:
|
case 1:
|
||||||
case 4:
|
case 4:
|
||||||
case 8:
|
case 8:
|
||||||
@ -134,7 +149,7 @@ public final class ICNSImageReader extends ImageReaderBase {
|
|||||||
specifiers.add(ImageTypeSpecifier.createInterleaved(ColorSpace.getInstance(ColorSpace.CS_sRGB), new int[]{3, 2, 1, 0}, DataBuffer.TYPE_BYTE, true, false));
|
specifiers.add(ImageTypeSpecifier.createInterleaved(ColorSpace.getInstance(ColorSpace.CS_sRGB), new int[]{3, 2, 1, 0}, DataBuffer.TYPE_BYTE, true, false));
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new IllegalStateException(String.format("Unknown bit depth: %d", header.depth()));
|
throw new IllegalStateException(String.format("Unknown bit depth: %d", resource.depth()));
|
||||||
}
|
}
|
||||||
|
|
||||||
specifiers.add(rawType);
|
specifiers.add(rawType);
|
||||||
@ -154,7 +169,7 @@ public final class ICNSImageReader extends ImageReaderBase {
|
|||||||
int num = icons.size();
|
int num = icons.size();
|
||||||
while (true) {
|
while (true) {
|
||||||
try {
|
try {
|
||||||
readIconHeader(num++);
|
readIconResource(num++);
|
||||||
}
|
}
|
||||||
catch (IndexOutOfBoundsException expected) {
|
catch (IndexOutOfBoundsException expected) {
|
||||||
break;
|
break;
|
||||||
@ -166,20 +181,20 @@ public final class ICNSImageReader extends ImageReaderBase {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public BufferedImage read(int imageIndex, ImageReadParam param) throws IOException {
|
public BufferedImage read(int imageIndex, ImageReadParam param) throws IOException {
|
||||||
IconHeader header = readIconHeader(imageIndex);
|
IconResource resource = readIconResource(imageIndex);
|
||||||
|
|
||||||
imageInput.seek(header.start + HEADER_SIZE);
|
imageInput.seek(resource.start + RESOURCE_HEADER_SIZE);
|
||||||
|
|
||||||
// Special handling of PNG/JPEG 2000 icons
|
// Special handling of PNG/JPEG 2000 icons
|
||||||
if (header.isForeignFormat()) {
|
if (resource.isForeignFormat()) {
|
||||||
return readForeignFormat(param, header);
|
return readForeignFormat(param, resource);
|
||||||
}
|
}
|
||||||
|
|
||||||
return readICNSFormat(imageIndex, param, header);
|
return readICNSFormat(imageIndex, param, resource);
|
||||||
}
|
}
|
||||||
|
|
||||||
private BufferedImage readICNSFormat(final int imageIndex, final ImageReadParam param, final IconHeader header) throws IOException {
|
private BufferedImage readICNSFormat(final int imageIndex, final ImageReadParam param, final IconResource resource) throws IOException {
|
||||||
Dimension size = header.size();
|
Dimension size = resource.size();
|
||||||
|
|
||||||
int width = size.width;
|
int width = size.width;
|
||||||
int height = size.height;
|
int height = size.height;
|
||||||
@ -202,11 +217,11 @@ public final class ICNSImageReader extends ImageReaderBase {
|
|||||||
|
|
||||||
// Read image data
|
// Read image data
|
||||||
byte[] data;
|
byte[] data;
|
||||||
if (header.isCompressed()) {
|
if (resource.isCompressed()) {
|
||||||
// Only 32 bit icons may be compressed
|
// Only 32 bit icons may be compressed
|
||||||
data = new byte[width * height * header.depth() / 8];
|
data = new byte[width * height * resource.depth() / 8];
|
||||||
|
|
||||||
int packedSize = header.length - HEADER_SIZE;
|
int packedSize = resource.length - RESOURCE_HEADER_SIZE;
|
||||||
|
|
||||||
if (width >= 128 && height >= 128) {
|
if (width >= 128 && height >= 128) {
|
||||||
// http://www.macdisk.com/maciconen.php:
|
// http://www.macdisk.com/maciconen.php:
|
||||||
@ -225,14 +240,14 @@ public final class ICNSImageReader extends ImageReaderBase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
data = new byte[header.length - HEADER_SIZE];
|
data = new byte[resource.length - RESOURCE_HEADER_SIZE];
|
||||||
imageInput.readFully(data);
|
imageInput.readFully(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (header.depth() == 1) {
|
if (resource.depth() == 1) {
|
||||||
// Binary
|
// Binary
|
||||||
DataBufferByte buffer = new DataBufferByte(data, data.length / 2, 0);
|
DataBufferByte buffer = new DataBufferByte(data, data.length / 2, 0);
|
||||||
WritableRaster raster = Raster.createPackedRaster(buffer, width, height, header.depth(), null);
|
WritableRaster raster = Raster.createPackedRaster(buffer, width, height, resource.depth(), null);
|
||||||
|
|
||||||
if (image.getType() == rawType.getBufferedImageType() && ((IndexColorModel) image.getColorModel()).getMapSize() == 2) {
|
if (image.getType() == rawType.getBufferedImageType() && ((IndexColorModel) image.getColorModel()).getMapSize() == 2) {
|
||||||
// Preserve raw data as read (binary), discard mask
|
// Preserve raw data as read (binary), discard mask
|
||||||
@ -241,7 +256,7 @@ public final class ICNSImageReader extends ImageReaderBase {
|
|||||||
else {
|
else {
|
||||||
// Convert to 32 bit ARGB
|
// Convert to 32 bit ARGB
|
||||||
DataBufferByte maskBuffer = new DataBufferByte(data, data.length / 2, data.length / 2);
|
DataBufferByte maskBuffer = new DataBufferByte(data, data.length / 2, data.length / 2);
|
||||||
WritableRaster mask = Raster.createPackedRaster(maskBuffer, width, height, header.depth(), null);
|
WritableRaster mask = Raster.createPackedRaster(maskBuffer, width, height, resource.depth(), null);
|
||||||
|
|
||||||
Graphics2D graphics = image.createGraphics();
|
Graphics2D graphics = image.createGraphics();
|
||||||
|
|
||||||
@ -261,10 +276,10 @@ public final class ICNSImageReader extends ImageReaderBase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (header.depth() <= 8) {
|
else if (resource.depth() <= 8) {
|
||||||
// Indexed
|
// Indexed
|
||||||
DataBufferByte buffer = new DataBufferByte(data, data.length);
|
DataBufferByte buffer = new DataBufferByte(data, data.length);
|
||||||
WritableRaster raster = Raster.createPackedRaster(buffer, width, height, header.depth(), null);
|
WritableRaster raster = Raster.createPackedRaster(buffer, width, height, resource.depth(), null);
|
||||||
|
|
||||||
if (image.getType() == rawType.getBufferedImageType()) {
|
if (image.getType() == rawType.getBufferedImageType()) {
|
||||||
// Preserve raw data as read (indexed), discard mask
|
// Preserve raw data as read (indexed), discard mask
|
||||||
@ -285,8 +300,12 @@ public final class ICNSImageReader extends ImageReaderBase {
|
|||||||
processImageProgress(50f);
|
processImageProgress(50f);
|
||||||
|
|
||||||
// Read mask and apply
|
// Read mask and apply
|
||||||
Raster mask = readMask(findMask(header));
|
IconResource maskResource = findMaskResource(resource);
|
||||||
image.getAlphaRaster().setRect(mask);
|
|
||||||
|
if (maskResource != null) {
|
||||||
|
Raster mask = readMask(maskResource);
|
||||||
|
image.getAlphaRaster().setRect(mask);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@ -294,14 +313,35 @@ public final class ICNSImageReader extends ImageReaderBase {
|
|||||||
int bandLen = data.length / 4;
|
int bandLen = data.length / 4;
|
||||||
|
|
||||||
DataBufferByte buffer = new DataBufferByte(data, data.length);
|
DataBufferByte buffer = new DataBufferByte(data, data.length);
|
||||||
WritableRaster raster = Raster.createBandedRaster(buffer, width, height, width, new int[]{0, 0, 0, 0}, createBandOffsets(bandLen), null);
|
|
||||||
|
WritableRaster raster;
|
||||||
|
|
||||||
|
if (resource.isCompressed()) {
|
||||||
|
raster = Raster.createBandedRaster(buffer, width, height, width, new int[]{0, 0, 0, 0}, createBandOffsets(bandLen), null);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// NOTE: Uncompressed 32bit is interleaved RGBA, not banded...
|
||||||
|
raster = Raster.createInterleavedRaster(buffer, width, height, width * 4, 4, new int[]{1, 2, 3, 0}, null);
|
||||||
|
}
|
||||||
|
|
||||||
image.setData(raster);
|
image.setData(raster);
|
||||||
|
|
||||||
processImageProgress(75f);
|
processImageProgress(75f);
|
||||||
|
|
||||||
// Read mask and apply
|
// Read mask and apply
|
||||||
Raster mask = readMask(findMask(header));
|
IconResource maskResource = findMaskResource(resource);
|
||||||
image.getAlphaRaster().setRect(mask);
|
|
||||||
|
if (maskResource != null) {
|
||||||
|
Raster mask = readMask(maskResource);
|
||||||
|
image.getAlphaRaster().setRect(mask);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// TODO: This is simply stupid. Rewrite to use no alpha instead?
|
||||||
|
byte[] solid = new byte[width * height];
|
||||||
|
Arrays.fill(solid, (byte) -1);
|
||||||
|
WritableRaster mask = Raster.createBandedRaster(new DataBufferByte(solid, solid.length), width, height, width, new int[]{0}, new int[]{0}, null);
|
||||||
|
image.getAlphaRaster().setRect(mask);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// For now: Make listener tests happy
|
// For now: Make listener tests happy
|
||||||
@ -318,28 +358,52 @@ public final class ICNSImageReader extends ImageReaderBase {
|
|||||||
return image;
|
return image;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Raster readMask(IconHeader header) throws IOException {
|
private Raster readMask(final IconResource resource) throws IOException {
|
||||||
Dimension size = header.size();
|
Dimension size = resource.size();
|
||||||
|
|
||||||
int width = size.width;
|
int width = size.width;
|
||||||
int height = size.height;
|
int height = size.height;
|
||||||
|
|
||||||
byte[] alpha = new byte[header.length - HEADER_SIZE];
|
byte[] mask = new byte[width * height];
|
||||||
|
imageInput.seek(resource.start + RESOURCE_HEADER_SIZE);
|
||||||
|
|
||||||
imageInput.seek(header.start + HEADER_SIZE);
|
if (resource.isMaskType()) {
|
||||||
imageInput.readFully(alpha);
|
// 8 bit mask
|
||||||
|
imageInput.readFully(mask, 0, resource.length - RESOURCE_HEADER_SIZE);
|
||||||
|
}
|
||||||
|
else if (resource.hasMask()) {
|
||||||
|
// Embedded 1bit mask
|
||||||
|
byte[] maskData = new byte[(resource.length - RESOURCE_HEADER_SIZE) / 2];
|
||||||
|
imageInput.skipBytes(maskData.length); // Skip the 1 bit image data
|
||||||
|
imageInput.readFully(maskData);
|
||||||
|
|
||||||
return Raster.createBandedRaster(new DataBufferByte(alpha, alpha.length), width, height, width, new int[]{0}, new int[]{0}, null);
|
// Unpack 1bit mask to 8 bit
|
||||||
|
int bitPos = 0x80;
|
||||||
|
|
||||||
|
for (int i = 0, maskLength = mask.length; i < maskLength; i++) {
|
||||||
|
mask[i] = (byte) ((maskData[i / 8] & bitPos) != 0 ? 0xff : 0x00);
|
||||||
|
|
||||||
|
if ((bitPos >>= 1) == 0) {
|
||||||
|
bitPos = 0x80;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw new IllegalArgumentException(String.format("Not a mask resource: %s", resource));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Raster.createBandedRaster(new DataBufferByte(mask, mask.length), width, height, width, new int[]{0}, new int[]{0}, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private IconHeader findMask(final IconHeader icon) throws IOException {
|
private IconResource findMaskResource(final IconResource iconResource) throws IOException {
|
||||||
|
// Find 8 bit mask
|
||||||
try {
|
try {
|
||||||
int i = 0;
|
int i = 0;
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
IconHeader mask = i < masks.size() ? masks.get(i++) : readNextIconHeader();
|
IconResource mask = i < masks.size() ? masks.get(i++) : readNextIconResource();
|
||||||
|
|
||||||
if (mask.isMask() && mask.size().equals(icon.size())) {
|
if (mask.isMaskType() && mask.size().equals(iconResource.size())) {
|
||||||
return mask;
|
return mask;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -347,11 +411,18 @@ public final class ICNSImageReader extends ImageReaderBase {
|
|||||||
catch (IndexOutOfBoundsException ignore) {
|
catch (IndexOutOfBoundsException ignore) {
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new IIOException(String.format("No mask for icon: %s", icon));
|
// Fall back to mask from 1 bit resource if no 8 bit mask
|
||||||
|
for (IconResource resource : icons) {
|
||||||
|
if (resource.hasMask() && resource.size().equals(iconResource.size())) {
|
||||||
|
return resource;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private BufferedImage readForeignFormat(final ImageReadParam param, final IconHeader header) throws IOException {
|
private BufferedImage readForeignFormat(final ImageReadParam param, final IconResource resource) throws IOException {
|
||||||
ImageInputStream stream = ImageIO.createImageInputStream(IIOUtil.createStreamAdapter(imageInput, header.length));
|
ImageInputStream stream = ImageIO.createImageInputStream(IIOUtil.createStreamAdapter(imageInput, resource.length));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Iterator<ImageReader> readers = ImageIO.getImageReaders(stream);
|
Iterator<ImageReader> readers = ImageIO.getImageReaders(stream);
|
||||||
@ -364,9 +435,12 @@ public final class ICNSImageReader extends ImageReaderBase {
|
|||||||
return reader.read(0, param);
|
return reader.read(0, param);
|
||||||
}
|
}
|
||||||
catch (IOException ignore) {
|
catch (IOException ignore) {
|
||||||
}
|
if (stream.getFlushedPosition() <= 0) {
|
||||||
finally {
|
stream.seek(0);
|
||||||
stream.seek(0);
|
}
|
||||||
|
else {
|
||||||
|
stream = ImageIO.createImageInputStream(IIOUtil.createStreamAdapter(imageInput, resource.length));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -377,7 +451,7 @@ public final class ICNSImageReader extends ImageReaderBase {
|
|||||||
// TODO: Create JPEG 2000 reader..? :-P
|
// TODO: Create JPEG 2000 reader..? :-P
|
||||||
throw new IIOException(String.format(
|
throw new IIOException(String.format(
|
||||||
"Cannot read %s format in type '%s' icon (no reader; installed: %s)",
|
"Cannot read %s format in type '%s' icon (no reader; installed: %s)",
|
||||||
getForeignFormat(stream), ICNSUtil.intToStr(header.type), Arrays.toString(ImageIO.getReaderFormatNames())
|
getForeignFormat(stream), ICNSUtil.intToStr(resource.type), Arrays.toString(ImageIO.getReaderFormatNames())
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
@ -454,25 +528,19 @@ public final class ICNSImageReader extends ImageReaderBase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private IconHeader readIconHeader(final int imageIndex) throws IOException {
|
private IconResource readIconResource(final int imageIndex) throws IOException {
|
||||||
checkBounds(imageIndex);
|
checkBounds(imageIndex);
|
||||||
readeFileHeader();
|
readeFileHeader();
|
||||||
|
|
||||||
while (icons.size() <= imageIndex) {
|
while (icons.size() <= imageIndex) {
|
||||||
readNextIconHeader();
|
readNextIconResource();
|
||||||
}
|
}
|
||||||
|
|
||||||
return icons.get(imageIndex);
|
return icons.get(imageIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
private IconHeader readNextIconHeader() throws IOException {
|
private IconResource readNextIconResource() throws IOException {
|
||||||
IconHeader lastIcon = icons.isEmpty() ? null : icons.get(icons.size() - 1);
|
long lastReadPos = lastResourceRead == null ? RESOURCE_HEADER_SIZE : lastResourceRead.start + lastResourceRead.length;
|
||||||
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);
|
imageInput.seek(lastReadPos);
|
||||||
|
|
||||||
@ -480,17 +548,20 @@ public final class ICNSImageReader extends ImageReaderBase {
|
|||||||
throw new IndexOutOfBoundsException();
|
throw new IndexOutOfBoundsException();
|
||||||
}
|
}
|
||||||
|
|
||||||
IconHeader header = IconHeader.read(imageInput);
|
IconResource resource = IconResource.read(imageInput);
|
||||||
|
// System.err.println("resource: " + resource);
|
||||||
|
|
||||||
// Filter out special case icnV (version?), as this isn't really an icon..
|
lastResourceRead = resource;
|
||||||
if (header.isMask() || header.type == ICNS.icnV) {
|
|
||||||
masks.add(header);
|
// Filter out special cases like 'icnV' or 'TOC ' resources
|
||||||
|
if (resource.isMaskType()) {
|
||||||
|
masks.add(resource);
|
||||||
}
|
}
|
||||||
else {
|
else if (!resource.isUnknownType()) {
|
||||||
icons.add(header);
|
icons.add(resource);
|
||||||
}
|
}
|
||||||
|
|
||||||
return header;
|
return resource;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void readeFileHeader() throws IOException {
|
private void readeFileHeader() throws IOException {
|
||||||
@ -507,13 +578,13 @@ public final class ICNSImageReader extends ImageReaderBase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Rewrite using subclasses!
|
// TODO: Rewrite using subclasses/instances!
|
||||||
static final class IconHeader {
|
static final class IconResource {
|
||||||
private final long start;
|
private final long start;
|
||||||
private final int type;
|
private final int type;
|
||||||
private final int length;
|
private final int length;
|
||||||
|
|
||||||
IconHeader(long start, int type, int length) {
|
IconResource(long start, int type, int length) {
|
||||||
validate(type, length);
|
validate(type, length);
|
||||||
|
|
||||||
this.start = start;
|
this.start = start;
|
||||||
@ -521,8 +592,8 @@ public final class ICNSImageReader extends ImageReaderBase {
|
|||||||
this.length = length;
|
this.length = length;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IconHeader read(ImageInputStream input) throws IOException {
|
public static IconResource read(ImageInputStream input) throws IOException {
|
||||||
return new IconHeader(input.getStreamPosition(), input.readInt(), input.readInt());
|
return new IconResource(input.getStreamPosition(), input.readInt(), input.readInt());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void validate(int type, int length) {
|
private void validate(int type, int length) {
|
||||||
@ -534,7 +605,7 @@ public final class ICNSImageReader extends ImageReaderBase {
|
|||||||
validateLengthForType(type, length, 256);
|
validateLengthForType(type, length, 256);
|
||||||
break;
|
break;
|
||||||
case ICNS.icm_:
|
case ICNS.icm_:
|
||||||
validateLengthForType(type, length, 24);
|
validateLengthForType(type, length, 48);
|
||||||
break;
|
break;
|
||||||
case ICNS.icm4:
|
case ICNS.icm4:
|
||||||
validateLengthForType(type, length, 96);
|
validateLengthForType(type, length, 96);
|
||||||
@ -579,25 +650,28 @@ public final class ICNSImageReader extends ImageReaderBase {
|
|||||||
case ICNS.ic08:
|
case ICNS.ic08:
|
||||||
case ICNS.ic09:
|
case ICNS.ic09:
|
||||||
case ICNS.ic10:
|
case ICNS.ic10:
|
||||||
if (length > 0) {
|
if (length > RESOURCE_HEADER_SIZE) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
throw new IllegalArgumentException(String.format("Wrong combination of icon type '%s' and length: %d", ICNSUtil.intToStr(type), length));
|
throw new IllegalArgumentException(String.format("Wrong combination of icon type '%s' and length: %d", ICNSUtil.intToStr(type), length));
|
||||||
case ICNS.icnV:
|
case ICNS.icnV:
|
||||||
validateLengthForType(type, length, 4);
|
validateLengthForType(type, length, 4);
|
||||||
break;
|
break;
|
||||||
|
case ICNS.TOC_:
|
||||||
default:
|
default:
|
||||||
throw new IllegalStateException(String.format("Unknown icon type: '%s'", ICNSUtil.intToStr(type)));
|
if (length > RESOURCE_HEADER_SIZE) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
throw new IllegalStateException(String.format("Unknown icon type: '%s' length: %d", ICNSUtil.intToStr(type), length));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void validateLengthForType(int type, int length, final int expectedLength) {
|
private void validateLengthForType(int type, int length, final int expectedLength) {
|
||||||
Validate.isTrue(
|
Validate.isTrue(
|
||||||
length == expectedLength + HEADER_SIZE, // Compute to make lengths more logical
|
length == expectedLength + RESOURCE_HEADER_SIZE, // Compute to make lengths more logical
|
||||||
String.format(
|
String.format(
|
||||||
"Wrong combination of icon type '%s' and length: %d (expected: %d)",
|
"Wrong combination of icon type '%s' and length: %d (expected: %d)",
|
||||||
ICNSUtil.intToStr(type), length - HEADER_SIZE, expectedLength
|
ICNSUtil.intToStr(type), length - RESOURCE_HEADER_SIZE, expectedLength
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -677,7 +751,52 @@ public final class ICNSImageReader extends ImageReaderBase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isMask() {
|
public boolean isUnknownType() {
|
||||||
|
// These should simply be skipped
|
||||||
|
switch (type) {
|
||||||
|
case ICNS.ICON:
|
||||||
|
case ICNS.ICN_:
|
||||||
|
case ICNS.icm_:
|
||||||
|
case ICNS.ics_:
|
||||||
|
case ICNS.ich_:
|
||||||
|
case ICNS.icm4:
|
||||||
|
case ICNS.ics4:
|
||||||
|
case ICNS.icl4:
|
||||||
|
case ICNS.ich4:
|
||||||
|
case ICNS.icm8:
|
||||||
|
case ICNS.ics8:
|
||||||
|
case ICNS.icl8:
|
||||||
|
case ICNS.ich8:
|
||||||
|
case ICNS.s8mk:
|
||||||
|
case ICNS.l8mk:
|
||||||
|
case ICNS.h8mk:
|
||||||
|
case ICNS.t8mk:
|
||||||
|
case ICNS.is32:
|
||||||
|
case ICNS.il32:
|
||||||
|
case ICNS.ih32:
|
||||||
|
case ICNS.it32:
|
||||||
|
case ICNS.ic08:
|
||||||
|
case ICNS.ic09:
|
||||||
|
case ICNS.ic10:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasMask() {
|
||||||
|
switch (type) {
|
||||||
|
case ICNS.ICN_:
|
||||||
|
case ICNS.icm_:
|
||||||
|
case ICNS.ics_:
|
||||||
|
case ICNS.ich_:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isMaskType() {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case ICNS.s8mk:
|
case ICNS.s8mk:
|
||||||
case ICNS.l8mk:
|
case ICNS.l8mk:
|
||||||
@ -698,7 +817,7 @@ public final class ICNSImageReader extends ImageReaderBase {
|
|||||||
// http://www.macdisk.com/maciconen.php
|
// http://www.macdisk.com/maciconen.php
|
||||||
// "One should check whether the data length corresponds to the theoretical length (width * height)."
|
// "One should check whether the data length corresponds to the theoretical length (width * height)."
|
||||||
Dimension size = size();
|
Dimension size = size();
|
||||||
if (length != size.width * size.height) {
|
if (length != (size.width * size.height * depth() / 8 + RESOURCE_HEADER_SIZE)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -723,16 +842,16 @@ public final class ICNSImageReader extends ImageReaderBase {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(Object other) {
|
public boolean equals(Object other) {
|
||||||
return other == this || other != null && other.getClass() == getClass() && isEqual((IconHeader) other);
|
return other == this || other != null && other.getClass() == getClass() && isEqual((IconResource) other);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isEqual(IconHeader other) {
|
private boolean isEqual(IconResource other) {
|
||||||
return start == other.start && type == other.type && length == other.length;
|
return start == other.start && type == other.type && length == other.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return String.format("%s['%s' start: %d, length: %d]", getClass().getSimpleName(), ICNSUtil.intToStr(type), start, length);
|
return String.format("%s['%s' start: %d, length: %d%s]", getClass().getSimpleName(), ICNSUtil.intToStr(type), start, length, isCompressed() ? " (compressed)" : "");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -746,22 +865,50 @@ public final class ICNSImageReader extends ImageReaderBase {
|
|||||||
requested = Integer.parseInt(args[argIndex++]);
|
requested = Integer.parseInt(args[argIndex++]);
|
||||||
}
|
}
|
||||||
|
|
||||||
File input = new File(args[argIndex++]);
|
int imagesRead = 0;
|
||||||
|
int imagesSkipped = 0;
|
||||||
ImageReader reader = new ICNSImageReader();
|
ImageReader reader = new ICNSImageReader();
|
||||||
reader.setInput(ImageIO.createImageInputStream(input));
|
|
||||||
|
|
||||||
int start = requested != -1 ? requested : 0;
|
while(argIndex < args.length) {
|
||||||
int numImages = requested != -1 ? requested + 1 : reader.getNumImages(true);
|
File input = new File(args[argIndex++]);
|
||||||
for (int i = start; i < numImages; i++) {
|
ImageInputStream stream = ImageIO.createImageInputStream(input);
|
||||||
try {
|
|
||||||
BufferedImage image = reader.read(i);
|
if (stream == null) {
|
||||||
// System.err.println("image: " + image);
|
System.err.printf("Cannot read: %s\n", input.getAbsolutePath());
|
||||||
showIt(image, String.format("%s - %d", input.getName(), i));
|
continue;
|
||||||
}
|
}
|
||||||
catch (IIOException e) {
|
|
||||||
|
try {
|
||||||
|
reader.setInput(stream);
|
||||||
|
|
||||||
|
int start = requested != -1 ? requested : 0;
|
||||||
|
int numImages = requested != -1 ? requested + 1 : reader.getNumImages(true);
|
||||||
|
for (int i = start; i < numImages; i++) {
|
||||||
|
try {
|
||||||
|
BufferedImage image = reader.read(i);
|
||||||
|
imagesRead++;
|
||||||
|
// System.err.println("image: " + image);
|
||||||
|
showIt(image, String.format("%s - %d", input.getName(), i));
|
||||||
|
}
|
||||||
|
catch (IOException e) {
|
||||||
|
imagesSkipped++;
|
||||||
|
if (e.getMessage().contains("JPEG 2000")) {
|
||||||
|
// System.err.printf("%s: %s\n", input, e.getMessage());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
System.err.printf("%s: ", input);
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e) {
|
||||||
|
System.err.printf("%s: ", input);
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
System.err.printf("Read %s images (%d skipped) in %d files\n", imagesRead, imagesSkipped, args.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final class ICNSBitMaskColorModel extends IndexColorModel {
|
private static final class ICNSBitMaskColorModel extends IndexColorModel {
|
||||||
|
@ -50,10 +50,10 @@ public class ICNSImageReaderTest extends ImageReaderAbstractTestCase {
|
|||||||
new TestData(
|
new TestData(
|
||||||
getClassLoaderResource("/icns/GenericJavaApp.icns"),
|
getClassLoaderResource("/icns/GenericJavaApp.icns"),
|
||||||
new Dimension(16, 16), // 1 bit + 1 bit mask
|
new Dimension(16, 16), // 1 bit + 1 bit mask
|
||||||
new Dimension(16, 16), new Dimension(16, 16), // 8 bit CMAP, 32 bit
|
new Dimension(16, 16), new Dimension(16, 16), // 8 bit CMAP, 24 bit + 8 bit mask
|
||||||
new Dimension(32, 32), // 1 bit + 1 bit mask
|
new Dimension(32, 32), // 1 bit + 1 bit mask
|
||||||
new Dimension(32, 32), new Dimension(32, 32), // 8 bit CMAP, 32 bit
|
new Dimension(32, 32), new Dimension(32, 32), // 8 bit CMAP, 24 bit + 8 bit mask
|
||||||
new Dimension(128, 128) // 32 bit
|
new Dimension(128, 128) // 24 bit + 8 bit mask
|
||||||
),
|
),
|
||||||
new TestData(
|
new TestData(
|
||||||
getClassLoaderResource("/icns/Apple Retro.icns"),
|
getClassLoaderResource("/icns/Apple Retro.icns"),
|
||||||
@ -61,16 +61,40 @@ public class ICNSImageReaderTest extends ImageReaderAbstractTestCase {
|
|||||||
new Dimension(32, 32), // 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(48, 48), // 24 bit + 8 bit mask
|
||||||
new Dimension(128, 128) // 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(256, 256), // JPEG 2000 ic08, not readable without JAI or other JPEG 2000 support
|
||||||
// new Dimension(512, 512) // JPEG 2000
|
// new Dimension(512, 512) // JPEG 2000 ic09
|
||||||
),
|
),
|
||||||
new TestData(
|
new TestData(
|
||||||
getClassLoaderResource("/icns/7zIcon.icns"), // Contains the icnV resource, that isn't an icon
|
getClassLoaderResource("/icns/7zIcon.icns"), // Contains the icnV resource, that isn't an icon
|
||||||
new Dimension(16, 16), // 24 bit + 8 bit mask
|
new Dimension(16, 16), // 24 bit + 8 bit mask
|
||||||
new Dimension(32, 32), // 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(128, 128) // 24 bit + 8 bit mask
|
||||||
//, new Dimension(256, 256), // JPEG 2000
|
//, new Dimension(256, 256), // JPEG 2000 ic08
|
||||||
// new Dimension(512, 512) // JPEG 2000
|
// new Dimension(512, 512) // JPEG 2000 ic09
|
||||||
|
),
|
||||||
|
new TestData(
|
||||||
|
getClassLoaderResource("/icns/appStore.icns"), // Contains the 'TOC ' and icnV resources
|
||||||
|
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), // PNG ic08
|
||||||
|
new Dimension(512, 512), // PNG ic09
|
||||||
|
new Dimension(1024, 1024) // PNG ic10
|
||||||
|
),
|
||||||
|
new TestData(
|
||||||
|
getClassLoaderResource("/icns/XLW.icns"), // No 8 bit mask for 16x16 & 32x32, test fall back to 1 bit mask
|
||||||
|
new Dimension(16, 16), // 1 bit + 1 bit mask
|
||||||
|
new Dimension(16, 16), new Dimension(16, 16), // 4 bit CMAP, 8 bit CMAP (no 8 bit mask)
|
||||||
|
new Dimension(32, 32), // 1 bit + 1 bit mask
|
||||||
|
new Dimension(32, 32), new Dimension(32, 32), // 4 bit CMAP, 8 bit CMAP (no 8 bit mask)
|
||||||
|
new Dimension(128, 128) // 24 bit + 8 bit mask
|
||||||
|
),
|
||||||
|
new TestData(
|
||||||
|
getClassLoaderResource("/icns/XMLExport.icns"), // No masks at all, uncompressed 32 bit data
|
||||||
|
new Dimension(128, 128), // 32 bit interleaved
|
||||||
|
new Dimension(48, 48), // 32 bit interleaved
|
||||||
|
new Dimension(32, 32), // 32 bit interleaved
|
||||||
|
new Dimension(16, 16) // 32 bit interleaved
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
BIN
imageio/imageio-icns/src/test/resources/icns/XLW.icns
Normal file
BIN
imageio/imageio-icns/src/test/resources/icns/XLW.icns
Normal file
Binary file not shown.
BIN
imageio/imageio-icns/src/test/resources/icns/XMLExport.icns
Normal file
BIN
imageio/imageio-icns/src/test/resources/icns/XMLExport.icns
Normal file
Binary file not shown.
BIN
imageio/imageio-icns/src/test/resources/icns/appStore.icns
Normal file
BIN
imageio/imageio-icns/src/test/resources/icns/appStore.icns
Normal file
Binary file not shown.
@ -0,0 +1,5 @@
|
|||||||
|
The icon files in this folder may contain copyrighted artwork. However, I believe that using them for test purposes
|
||||||
|
(without actually displaying the artwork) must be considered fair use.
|
||||||
|
If you disagree for any reason, please send me a note, and I will remove your icon from the distribution.
|
||||||
|
|
||||||
|
-- harald.kuhr@gmail.com
|
Loading…
x
Reference in New Issue
Block a user