Merge branch 'pr-37'
This closes #37
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 052f04a..c2ae80f 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -45,6 +45,9 @@
</properties>
<body>
<release version="1.0-alpha2" date="2020-??-??" description="Second 1.0 alpha release">
+ <action issue="IMAGING-248" dev="kinow" type="add" due-to="Greg Shrago">
+ ICNS: missing element types; some safety checks
+ </action>
<action issue="IMAGING-246" dev="kinow" type="fix">
Invalid Block Size error prevents handling of block 1084, Macintosh NSPrintInfo
</action>
diff --git a/src/main/java/org/apache/commons/imaging/formats/icns/IcnsDecoder.java b/src/main/java/org/apache/commons/imaging/formats/icns/IcnsDecoder.java
index 664e194..c68b544 100644
--- a/src/main/java/org/apache/commons/imaging/formats/icns/IcnsDecoder.java
+++ b/src/main/java/org/apache/commons/imaging/formats/icns/IcnsDecoder.java
@@ -21,6 +21,7 @@
import java.util.List;
import org.apache.commons.imaging.ImageReadException;
+import org.apache.commons.imaging.Imaging;
import org.apache.commons.imaging.common.ImageBuilder;
import org.apache.commons.imaging.formats.icns.IcnsImageParser.IcnsElement;
@@ -191,66 +192,77 @@
}
public static List<BufferedImage> decodeAllImages(final IcnsImageParser.IcnsElement[] icnsElements)
- throws ImageReadException {
+ throws ImageReadException {
final List<BufferedImage> result = new ArrayList<>();
- for (final IcnsElement imageElement : icnsElements) {
- final IcnsType imageType = IcnsType.findImageType(imageElement.type);
- if (imageType == null) {
- continue;
+ for (int i = 0; i < icnsElements.length; i++) {
+ BufferedImage image = decodeImage(icnsElements, i);
+ if (image != null) {
+ result.add(image);
}
+ }
+ return result;
+ }
- IcnsType maskType;
- IcnsImageParser.IcnsElement maskElement = null;
- if (imageType.hasMask()) {
- maskType = imageType;
- maskElement = imageElement;
+ public static BufferedImage decodeImage(final IcnsImageParser.IcnsElement[] icnsElements, int index)
+ throws ImageReadException {
+ IcnsImageParser.IcnsElement imageElement = icnsElements[index];
+ final IcnsType imageType = IcnsType.findImageType(imageElement.type);
+ if (imageType == null) {
+ return null;
+ }
+
+ // PNG or JPEG 2000
+ if (imageType == IcnsType.ICNS_16x16_32BIT_ARGB_IMAGE
+ || imageType == IcnsType.ICNS_32x32_32BIT_ARGB_IMAGE
+ || imageType == IcnsType.ICNS_64x64_32BIT_ARGB_IMAGE
+ || imageType == IcnsType.ICNS_128x128_32BIT_ARGB_IMAGE
+ || imageType == IcnsType.ICNS_256x256_32BIT_ARGB_IMAGE
+ || imageType == IcnsType.ICNS_512x512_32BIT_ARGB_IMAGE
+ || imageType == IcnsType.ICNS_1024x1024_32BIT_ARGB_IMAGE
+ || imageType == IcnsType.ICNS_32x32_2x_32BIT_ARGB_IMAGE
+ || imageType == IcnsType.ICNS_64x64_2x_32BIT_ARGB_IMAGE
+ || imageType == IcnsType.ICNS_256x256_2x_32BIT_ARGB_IMAGE
+ || imageType == IcnsType.ICNS_512x512_2x_32BIT_ARGB_IMAGE) {
+ BufferedImage image = null;
+ try {
+ image = Imaging.getBufferedImage(imageElement.data);
+ } catch (Exception ex) {
+ if (imageType.getWidth() <= 32) {
+ try {
+ image = decodeImageImpl(imageType, imageElement, icnsElements);
+ } catch (Exception ignored) { }
+ }
+ if (image == null) {
+ image = new BufferedImage(imageType.getWidth(), imageType.getHeight(), BufferedImage.TYPE_INT_ARGB);
+ }
+ }
+ return image;
+ }
+
+ return decodeImageImpl(imageType, imageElement, icnsElements);
+ }
+
+ private static BufferedImage decodeImageImpl(IcnsType imageType,
+ IcnsElement imageElement,
+ IcnsElement[] icnsElements) throws ImageReadException {
+ final int expectedSize = (imageType.getWidth() * imageType.getHeight()
+ * imageType.getBitsPerPixel() + 7) / 8;
+ byte[] imageData;
+ if (imageElement.data.length < expectedSize) {
+ if (imageType.getBitsPerPixel() == 32) {
+ imageData = Rle24Compression.decompress(
+ imageType.getWidth(), imageType.getHeight(),
+ imageElement.data);
} else {
- maskType = IcnsType.find8BPPMaskType(imageType);
- if (maskType != null) {
- for (final IcnsElement icnsElement : icnsElements) {
- if (icnsElement.type == maskType.getType()) {
- maskElement = icnsElement;
- break;
- }
- }
- }
- if (maskElement == null) {
- maskType = IcnsType.find1BPPMaskType(imageType);
- if (maskType != null) {
- for (final IcnsElement icnsElement : icnsElements) {
- if (icnsElement.type == maskType.getType()) {
- maskElement = icnsElement;
- break;
- }
- }
- }
- }
+ throw new ImageReadException("Short image data but not a 32 bit compressed type");
}
+ } else {
+ imageData = imageElement.data;
+ }
- // FIXME: don't skip these when JPEG 2000 support is added:
- if (imageType == IcnsType.ICNS_256x256_32BIT_ARGB_IMAGE
- || imageType == IcnsType.ICNS_512x512_32BIT_ARGB_IMAGE) {
- continue;
- }
-
- final int expectedSize = (imageType.getWidth() * imageType.getHeight()
- * imageType.getBitsPerPixel() + 7) / 8;
- byte[] imageData;
- if (imageElement.data.length < expectedSize) {
- if (imageType.getBitsPerPixel() == 32) {
- imageData = Rle24Compression.decompress(
- imageType.getWidth(), imageType.getHeight(),
- imageElement.data);
- } else {
- throw new ImageReadException("Short image data but not a 32 bit compressed type");
- }
- } else {
- imageData = imageElement.data;
- }
-
- final ImageBuilder imageBuilder = new ImageBuilder(imageType.getWidth(),
- imageType.getHeight(), true);
- switch (imageType.getBitsPerPixel()) {
+ final ImageBuilder imageBuilder = new ImageBuilder(imageType.getWidth(),
+ imageType.getHeight(), true);
+ switch (imageType.getBitsPerPixel()) {
case 1:
decode1BPPImage(imageType, imageData, imageBuilder);
break;
@@ -265,20 +277,46 @@
break;
default:
throw new ImageReadException("Unsupported bit depth " + imageType.getBitsPerPixel());
- }
+ }
- if (maskElement != null) {
- if (maskType.getBitsPerPixel() == 1) {
- apply1BPPMask(maskElement.data, imageBuilder);
- } else if (maskType.getBitsPerPixel() == 8) {
- apply8BPPMask(maskElement.data, imageBuilder);
- } else {
- throw new ImageReadException("Unsupport mask bit depth " + maskType.getBitsPerPixel());
+ IcnsType maskType;
+ IcnsElement maskElement = null;
+ if (imageType.hasMask()) {
+ maskType = imageType;
+ maskElement = imageElement;
+ } else {
+ maskType = IcnsType.find8BPPMaskType(imageType);
+ if (maskType != null) {
+ for (final IcnsElement icnsElement : icnsElements) {
+ if (icnsElement.type == maskType.getType()) {
+ maskElement = icnsElement;
+ break;
+ }
}
}
-
- result.add(imageBuilder.getBufferedImage());
+ if (maskElement == null) {
+ maskType = IcnsType.find1BPPMaskType(imageType);
+ if (maskType != null) {
+ for (final IcnsElement icnsElement : icnsElements) {
+ if (icnsElement.type == maskType.getType()) {
+ maskElement = icnsElement;
+ break;
+ }
+ }
+ }
+ }
}
- return result;
+
+ if (maskElement != null) {
+ if (maskType.getBitsPerPixel() == 1) {
+ apply1BPPMask(maskElement.data, imageBuilder);
+ } else if (maskType.getBitsPerPixel() == 8) {
+ apply8BPPMask(maskElement.data, imageBuilder);
+ } else {
+ throw new ImageReadException("Unsupported mask bit depth " + maskType.getBitsPerPixel());
+ }
+ }
+
+ return imageBuilder.getBufferedImage();
}
}
diff --git a/src/main/java/org/apache/commons/imaging/formats/icns/IcnsImageParser.java b/src/main/java/org/apache/commons/imaging/formats/icns/IcnsImageParser.java
index faa87aa..0851774 100644
--- a/src/main/java/org/apache/commons/imaging/formats/icns/IcnsImageParser.java
+++ b/src/main/java/org/apache/commons/imaging/formats/icns/IcnsImageParser.java
@@ -186,21 +186,16 @@
}
}
- private IcnsElement readIcnsElement(final InputStream is) throws IOException {
- final int type = read4Bytes("Type", is, "Not a Valid ICNS File", getByteOrder()); // Icon type
- // (4 bytes)
- final int elementSize = read4Bytes("ElementSize", is, "Not a Valid ICNS File", getByteOrder()); // Length
- // of
- // data
- // (4
- // bytes),
- // in
- // bytes,
- // including
- // this
- // header
- final byte[] data = readBytes("Data", is, elementSize - 8,
- "Not a Valid ICNS File");
+ private IcnsElement readIcnsElement(final InputStream is, final int remainingSize) throws IOException {
+ // Icon type (4 bytes)
+ final int type = read4Bytes("Type", is, "Not a valid ICNS file", getByteOrder());
+ // Length of data (4 bytes), in bytes, including this header
+ final int elementSize = read4Bytes("ElementSize", is, "Not a valid ICNS file", getByteOrder());
+ if (elementSize > remainingSize) {
+ throw new IOException(String.format("Corrupted ICNS file: element size %d is greater than "
+ + "remaining size %d", elementSize, remainingSize));
+ }
+ final byte[] data = readBytes("Data", is, elementSize - 8, "Not a valid ICNS file");
return new IcnsElement(type, elementSize, data);
}
@@ -223,7 +218,7 @@
final List<IcnsElement> icnsElementList = new ArrayList<>();
for (int remainingSize = icnsHeader.fileSize - 8; remainingSize > 0;) {
- final IcnsElement icnsElement = readIcnsElement(is);
+ final IcnsElement icnsElement = readIcnsElement(is, remainingSize);
icnsElementList.add(icnsElement);
remainingSize -= icnsElement.elementSize;
}
diff --git a/src/main/java/org/apache/commons/imaging/formats/icns/IcnsType.java b/src/main/java/org/apache/commons/imaging/formats/icns/IcnsType.java
index 91b6468..7cefa09 100644
--- a/src/main/java/org/apache/commons/imaging/formats/icns/IcnsType.java
+++ b/src/main/java/org/apache/commons/imaging/formats/icns/IcnsType.java
@@ -31,6 +31,7 @@
ICNS_16x16_32BIT_IMAGE("is32", 16, 16, 32, false),
ICNS_32x32_8BIT_MASK("l8mk", 32, 32, 8, true),
+ ICNS_32x32_1BIT_IMAGE("ICON", 32, 32, 1, false),
ICNS_32x32_1BIT_IMAGE_AND_MASK("ICN#", 32, 32, 1, true),
ICNS_32x32_4BIT_IMAGE("icl4", 32, 32, 4, false),
ICNS_32x32_8BIT_IMAGE("icl8", 32, 32, 8, false),
@@ -45,9 +46,17 @@
ICNS_128x128_8BIT_MASK("t8mk", 128, 128, 8, true),
ICNS_128x128_32BIT_IMAGE("it32", 128, 128, 32, false),
+ ICNS_16x16_32BIT_ARGB_IMAGE("icp4", 16, 16, 32, false),
+ ICNS_32x32_32BIT_ARGB_IMAGE("icp5", 32, 32, 32, false),
+ ICNS_64x64_32BIT_ARGB_IMAGE("icp6", 64, 64, 32, false),
+ ICNS_128x128_32BIT_ARGB_IMAGE("ic07", 128, 128, 32, false),
ICNS_256x256_32BIT_ARGB_IMAGE("ic08", 256, 256, 32, false),
-
- ICNS_512x512_32BIT_ARGB_IMAGE("ic09", 512, 512, 32, false);
+ ICNS_512x512_32BIT_ARGB_IMAGE("ic09", 512, 512, 32, false),
+ ICNS_1024x1024_32BIT_ARGB_IMAGE("ic10", 1024, 1024, 32, false),
+ ICNS_32x32_2x_32BIT_ARGB_IMAGE("ic11", 32, 32, 32, false),
+ ICNS_64x64_2x_32BIT_ARGB_IMAGE("ic12", 64, 64, 32, false),
+ ICNS_256x256_2x_32BIT_ARGB_IMAGE("ic13", 256, 256, 32, false),
+ ICNS_512x512_2x_32BIT_ARGB_IMAGE("ic14", 512, 512, 32, false);
private static final IcnsType[] ALL_IMAGE_TYPES = {
ICNS_16x12_1BIT_IMAGE_AND_MASK,
@@ -57,6 +66,7 @@
ICNS_16x16_4BIT_IMAGE,
ICNS_16x16_8BIT_IMAGE,
ICNS_16x16_32BIT_IMAGE,
+ ICNS_32x32_1BIT_IMAGE,
ICNS_32x32_1BIT_IMAGE_AND_MASK,
ICNS_32x32_4BIT_IMAGE,
ICNS_32x32_8BIT_IMAGE,
@@ -66,8 +76,17 @@
ICNS_48x48_8BIT_IMAGE,
ICNS_48x48_32BIT_IMAGE,
ICNS_128x128_32BIT_IMAGE,
+ ICNS_16x16_32BIT_ARGB_IMAGE,
+ ICNS_32x32_32BIT_ARGB_IMAGE,
+ ICNS_64x64_32BIT_ARGB_IMAGE,
+ ICNS_128x128_32BIT_ARGB_IMAGE,
ICNS_256x256_32BIT_ARGB_IMAGE,
- ICNS_512x512_32BIT_ARGB_IMAGE};
+ ICNS_512x512_32BIT_ARGB_IMAGE,
+ ICNS_1024x1024_32BIT_ARGB_IMAGE,
+ ICNS_32x32_2x_32BIT_ARGB_IMAGE,
+ ICNS_64x64_2x_32BIT_ARGB_IMAGE,
+ ICNS_256x256_2x_32BIT_ARGB_IMAGE,
+ ICNS_512x512_2x_32BIT_ARGB_IMAGE};
private static final IcnsType[] ALL_MASK_TYPES = {
ICNS_16x12_1BIT_IMAGE_AND_MASK,
diff --git a/src/test/java/org/apache/commons/imaging/formats/icns/IcnsReadTest.java b/src/test/java/org/apache/commons/imaging/formats/icns/IcnsReadTest.java
index 14376f4..0c74ebb 100644
--- a/src/test/java/org/apache/commons/imaging/formats/icns/IcnsReadTest.java
+++ b/src/test/java/org/apache/commons/imaging/formats/icns/IcnsReadTest.java
@@ -17,18 +17,24 @@
package org.apache.commons.imaging.formats.icns;
+import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import java.awt.image.BufferedImage;
import java.io.File;
+import java.io.IOException;
+import java.util.Arrays;
import java.util.Collections;
+import java.util.List;
import java.util.stream.Stream;
import org.apache.commons.imaging.ImageInfo;
+import org.apache.commons.imaging.ImageReadException;
import org.apache.commons.imaging.Imaging;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
public class IcnsReadTest extends IcnsBaseTest {
@@ -37,6 +43,23 @@
return getIcnsImages().stream();
}
+ /**
+ * Provide data for tests of ICNS images with mono and/or JPEG png data. The
+ * logo in the issue IMAGING-248 was the groovy.icns. But the Python project
+ * also provides an image that contains a ic09 image.
+ *
+ * <p>icns2png was used to retrieve the number of images (i.e. icns2png -l
+ * image_file.icns, then counted the number of ico entries, ignoring the masks).</p>
+ * @return stream of test arguments
+ */
+ public static Stream<Arguments> provideIcnsImagesWithMonoAndJpegPngData() {
+ return Arrays
+ .asList(
+ Arguments.of("/images/icns/IMAGING-248/python.icns", 7),
+ Arguments.of("/images/icns/IMAGING-248/groovy.icns", 3))
+ .stream();
+ }
+
@Disabled(value = "RoundtripTest has to be fixed befor implementation can throw UnsupportedOperationException")
@ParameterizedTest
@MethodSource("data")
@@ -60,4 +83,18 @@
assertNotNull(image);
// TODO assert more
}
+
+ /**
+ * Test ICNS types such as mono (ICON) and some types for either JPEG2000 or PNG
+ * (icp4, icp5, ic11, etc). For IMAGING-248.
+ * @throws IOException if it fails to read the input stream
+ * @throws ImageReadException if the image is corrupted or invalid
+ */
+ @ParameterizedTest()
+ @MethodSource("provideIcnsImagesWithMonoAndJpegPngData")
+ public void testIcnsElementMonoPngJpeg(String file, int numberOfImages) throws ImageReadException, IOException {
+ File testFile = new File(IcnsReadTest.class.getResource(file).getFile());
+ List<BufferedImage> images = new IcnsImageParser().getAllBufferedImages(testFile);
+ assertEquals(numberOfImages, images.size());
+ }
}
diff --git a/src/test/resources/images/icns/IMAGING-248/groovy.icns b/src/test/resources/images/icns/IMAGING-248/groovy.icns
new file mode 100644
index 0000000..3e661dc
--- /dev/null
+++ b/src/test/resources/images/icns/IMAGING-248/groovy.icns
Binary files differ
diff --git a/src/test/resources/images/icns/IMAGING-248/python.icns b/src/test/resources/images/icns/IMAGING-248/python.icns
new file mode 100644
index 0000000..fc53e02
--- /dev/null
+++ b/src/test/resources/images/icns/IMAGING-248/python.icns
Binary files differ