From c006f22ac2b81097760810816b68a3e83ba253a4 Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Mon, 28 Nov 2011 12:02:00 +0100 Subject: [PATCH] Rewrote handling of JPEG 2000 icons. Now returns blank image with correct dimensions + issues warning if can't be read, instead of exception. Added quick fix conversion/reading for OS X using sips command line. Updated test cases. --- .../imageio/plugins/icns/ICNSImageReader.java | 39 +++- .../imageio/plugins/icns/IconResource.java | 5 +- .../imageio/plugins/icns/SipsJP2Reader.java | 176 ++++++++++++++++++ .../plugins/icns/ICNSImageReaderTest.java | 12 +- 4 files changed, 216 insertions(+), 16 deletions(-) create mode 100644 imageio/imageio-icns/src/main/java/com/twelvemonkeys/imageio/plugins/icns/SipsJP2Reader.java 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 7f6cb105..b85434dc 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 @@ -184,7 +184,7 @@ public final class ICNSImageReader extends ImageReaderBase { // Special handling of PNG/JPEG 2000 icons if (resource.isForeignFormat()) { - return readForeignFormat(param, resource); + return readForeignFormat(imageIndex, param, resource); } return readICNSFormat(imageIndex, param, resource); @@ -418,10 +418,11 @@ public final class ICNSImageReader extends ImageReaderBase { return null; } - private BufferedImage readForeignFormat(final ImageReadParam param, final IconResource resource) throws IOException { + private BufferedImage readForeignFormat(int imageIndex, final ImageReadParam param, final IconResource resource) throws IOException { ImageInputStream stream = ImageIO.createImageInputStream(IIOUtil.createStreamAdapter(imageInput, resource.length)); try { + // Try first using ImageIO Iterator readers = ImageIO.getImageReaders(stream); while (readers.hasNext()) { @@ -436,20 +437,38 @@ public final class ICNSImageReader extends ImageReaderBase { stream.seek(0); } else { + stream.close(); stream = ImageIO.createImageInputStream(IIOUtil.createStreamAdapter(imageInput, resource.length)); } } + finally { + reader.dispose(); + } + } + + String format = getForeignFormat(stream); + + // OS X quick fix + if ("JPEG 2000".equals(format) && SipsJP2Reader.isAvailable()) { + SipsJP2Reader reader = new SipsJP2Reader(); + reader.setInput(stream); + BufferedImage image = reader.read(0, param); + + if (image != null) { + return image; + } } // 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( + // Return blank icon + issue warning. We know the image dimensions, we just can't read the data. + processWarningOccurred(String.format( "Cannot read %s format in type '%s' icon (no reader; installed: %s)", - getForeignFormat(stream), ICNSUtil.intToStr(resource.type), Arrays.toString(ImageIO.getReaderFormatNames()) + format, ICNSUtil.intToStr(resource.type), Arrays.toString(ImageIO.getReaderFormatNames()) )); + + Dimension size = resource.size(); + + return getDestination(param, getImageTypes(imageIndex), size.width, size.height); } finally { stream.close(); @@ -458,6 +477,7 @@ public final class ICNSImageReader extends ImageReaderBase { private String getForeignFormat(final ImageInputStream stream) throws IOException { byte[] magic = new byte[12]; // Length of JPEG 2000 magic + try { stream.readFully(magic); } @@ -466,6 +486,7 @@ public final class ICNSImageReader extends ImageReaderBase { } String format; + if (Arrays.equals(ICNS.PNG_MAGIC, magic)) { format = "PNG"; } @@ -575,7 +596,7 @@ public final class ICNSImageReader extends ImageReaderBase { catch (IOException e) { imagesSkipped++; if (e.getMessage().contains("JPEG 2000")) { -// System.err.printf("%s: %s\n", input, e.getMessage()); + System.err.printf("%s: %s\n", input, e.getMessage()); } else { System.err.printf("%s: ", input); diff --git a/imageio/imageio-icns/src/main/java/com/twelvemonkeys/imageio/plugins/icns/IconResource.java b/imageio/imageio-icns/src/main/java/com/twelvemonkeys/imageio/plugins/icns/IconResource.java index 04ce6d90..8eb05ef2 100644 --- a/imageio/imageio-icns/src/main/java/com/twelvemonkeys/imageio/plugins/icns/IconResource.java +++ b/imageio/imageio-icns/src/main/java/com/twelvemonkeys/imageio/plugins/icns/IconResource.java @@ -216,7 +216,7 @@ final class IconResource { } public boolean isUnknownType() { - // These should simply be skipped + // Unknown types should simply be skipped when reading switch (type) { case ICNS.ICON: case ICNS.ICN_: @@ -290,12 +290,14 @@ final class IconResource { } public boolean isForeignFormat() { + // Recent entries contains full JPEG 2000 or PNG streams switch (type) { case ICNS.ic08: case ICNS.ic09: case ICNS.ic10: return true; } + return false; } @@ -310,6 +312,7 @@ final class IconResource { } private boolean isEqual(IconResource other) { + // This isn't strictly true, as resource must reside in same stream as well, but good enough for now return start == other.start && type == other.type && length == other.length; } diff --git a/imageio/imageio-icns/src/main/java/com/twelvemonkeys/imageio/plugins/icns/SipsJP2Reader.java b/imageio/imageio-icns/src/main/java/com/twelvemonkeys/imageio/plugins/icns/SipsJP2Reader.java new file mode 100644 index 00000000..c5834abb --- /dev/null +++ b/imageio/imageio-icns/src/main/java/com/twelvemonkeys/imageio/plugins/icns/SipsJP2Reader.java @@ -0,0 +1,176 @@ +/* + * 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 com.twelvemonkeys.imageio.util.IIOUtil; +import com.twelvemonkeys.io.FileUtil; + +import javax.imageio.IIOException; +import javax.imageio.ImageIO; +import javax.imageio.ImageReadParam; +import javax.imageio.ImageReader; +import javax.imageio.stream.ImageInputStream; +import java.awt.image.BufferedImage; +import java.io.*; +import java.util.Iterator; + +/** + * QuickFix for OS X (where ICNS are most useful) and JPEG 2000. + * Dumps the stream to disk and converts using sips command line tool: + * {@code sips -s format png }. + * Reads image back using ImageIO and known format (png). + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: JPEG2000Reader.java,v 1.0 25.11.11 14:17 haraldk Exp$ + */ +final class SipsJP2Reader { + + private static final File SIPS_COMMAND = new File("/usr/bin/sips"); + private static final boolean SIPS_EXISTS_AND_EXECUTES = existsAndExecutes(SIPS_COMMAND); + + private static boolean existsAndExecutes(final File cmd) { + try { + return cmd.exists() && cmd.canExecute(); + } + catch (SecurityException ignore) { + } + + return false; + } + + private ImageInputStream input; + + public BufferedImage read(final int imageIndex, final ImageReadParam param) throws IOException { + // Test if we have sips before dumping to be fail-fast + if (SIPS_EXISTS_AND_EXECUTES) { + File tempFile = dumpToFile(input); + + if (convertToPNG(tempFile)) { + ImageInputStream stream = ImageIO.createImageInputStream(tempFile); + Iterator readers = ImageIO.getImageReaders(stream); + + while (readers.hasNext()) { + ImageReader reader = readers.next(); + reader.setInput(stream); + + try { + return reader.read(imageIndex, param); + } + catch (IOException ignore) { + if (stream.getFlushedPosition() <= 0) { + stream.seek(0); + } + else { + stream.close(); + stream = ImageIO.createImageInputStream(tempFile); + } + } + finally { + reader.dispose(); + } + } + } + } + + return null; + } + + public void setInput(final ImageInputStream input) { + this.input = input; + } + + private static boolean convertToPNG(final File tempFile) throws IIOException { + try { + Process process = Runtime.getRuntime().exec(buildCommand(SIPS_COMMAND, tempFile)); + + // NOTE: sips return status is 0, even if error, need to check error message + int status = process.waitFor(); + String message = checkErrorMessage(process); + + if (status == 0 && message == null) { + return true; + } + else { + throw new IOException(message); + } + } + catch (InterruptedException e) { + throw new IIOException("Interrupted converting JPEG 2000 format", e); + } + catch (SecurityException e) { + // Exec might need permissions in sandboxed environment + throw new IIOException("Cannot convert JPEG 2000 format without file permissions", e); + } + catch (IOException e) { + throw new IIOException("Error converting JPEG 2000 format: " + e.getMessage(), e); + } + } + + private static String checkErrorMessage(final Process process) throws IOException { + InputStream stream = process.getErrorStream(); + + try { + BufferedReader reader = new BufferedReader(new InputStreamReader(stream)); + String message = reader.readLine(); + + return message != null && message.startsWith("Error: ") ? message.substring(7) : null; + } + finally { + stream.close(); + } + } + + private static String[] buildCommand(final File sipsCommand, final File tempFile) { + return new String[]{ + sipsCommand.getAbsolutePath(), "-s", "format", "png", tempFile.getAbsolutePath() + }; + } + + + private static File dumpToFile(final ImageInputStream stream) throws IOException { + File tempFile = File.createTempFile("imageio-icns-", ".png"); + tempFile.deleteOnExit(); + + FileOutputStream out = new FileOutputStream(tempFile); + + try { + FileUtil.copy(IIOUtil.createStreamAdapter(stream), out); + } + finally { + out.close(); + } + + return tempFile; + } + + static boolean isAvailable() { + return SIPS_EXISTS_AND_EXECUTES; + } +} 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 36b92573..67082b1a 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 @@ -61,19 +61,19 @@ public class ICNSImageReaderTest extends ImageReaderAbstractTestCase { 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 ic08, not readable without JAI or other JPEG 2000 support -// new Dimension(512, 512) // JPEG 2000 ic09 +, new Dimension(256, 256), // JPEG 2000 ic08 + new Dimension(512, 512) // JPEG 2000 ic09 ), 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 ic08 -// new Dimension(512, 512) // JPEG 2000 ic09 +, new Dimension(256, 256), // JPEG 2000 ic08 + new Dimension(512, 512) // JPEG 2000 ic09 ), new TestData( - getClassLoaderResource("/icns/appStore.icns"), // Contains the 'TOC ' and icnV resources + getClassLoaderResource("/icns/appStore.icns"), // Contains the 'TOC ' and icnV resources + PNGs in ic08-10 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 @@ -82,7 +82,7 @@ public class ICNSImageReaderTest extends ImageReaderAbstractTestCase { 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 + getClassLoaderResource("/icns/XLW.icns"), // No 8 bit mask for 16x16 & 32x32, 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