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.
This commit is contained in:
Harald Kuhr 2011-11-28 12:02:00 +01:00
parent 905a3da97b
commit c006f22ac2
4 changed files with 216 additions and 16 deletions

View File

@ -184,7 +184,7 @@ public final class ICNSImageReader extends ImageReaderBase {
// Special handling of PNG/JPEG 2000 icons // Special handling of PNG/JPEG 2000 icons
if (resource.isForeignFormat()) { if (resource.isForeignFormat()) {
return readForeignFormat(param, resource); return readForeignFormat(imageIndex, param, resource);
} }
return readICNSFormat(imageIndex, param, resource); return readICNSFormat(imageIndex, param, resource);
@ -418,10 +418,11 @@ public final class ICNSImageReader extends ImageReaderBase {
return null; 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)); ImageInputStream stream = ImageIO.createImageInputStream(IIOUtil.createStreamAdapter(imageInput, resource.length));
try { try {
// Try first using ImageIO
Iterator<ImageReader> readers = ImageIO.getImageReaders(stream); Iterator<ImageReader> readers = ImageIO.getImageReaders(stream);
while (readers.hasNext()) { while (readers.hasNext()) {
@ -436,20 +437,38 @@ public final class ICNSImageReader extends ImageReaderBase {
stream.seek(0); stream.seek(0);
} }
else { else {
stream.close();
stream = ImageIO.createImageInputStream(IIOUtil.createStreamAdapter(imageInput, resource.length)); 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). // 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: // Return blank icon + issue warning. We know the image dimensions, we just can't read the data.
// TODO: Return blank icon + issue warning? We know the image dimensions, we just can't read the data... processWarningOccurred(String.format(
// 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)", "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 { finally {
stream.close(); stream.close();
@ -458,6 +477,7 @@ public final class ICNSImageReader extends ImageReaderBase {
private String getForeignFormat(final ImageInputStream stream) throws IOException { private String getForeignFormat(final ImageInputStream stream) throws IOException {
byte[] magic = new byte[12]; // Length of JPEG 2000 magic byte[] magic = new byte[12]; // Length of JPEG 2000 magic
try { try {
stream.readFully(magic); stream.readFully(magic);
} }
@ -466,6 +486,7 @@ public final class ICNSImageReader extends ImageReaderBase {
} }
String format; String format;
if (Arrays.equals(ICNS.PNG_MAGIC, magic)) { if (Arrays.equals(ICNS.PNG_MAGIC, magic)) {
format = "PNG"; format = "PNG";
} }
@ -575,7 +596,7 @@ public final class ICNSImageReader extends ImageReaderBase {
catch (IOException e) { catch (IOException e) {
imagesSkipped++; imagesSkipped++;
if (e.getMessage().contains("JPEG 2000")) { 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 { else {
System.err.printf("%s: ", input); System.err.printf("%s: ", input);

View File

@ -216,7 +216,7 @@ final class IconResource {
} }
public boolean isUnknownType() { public boolean isUnknownType() {
// These should simply be skipped // Unknown types should simply be skipped when reading
switch (type) { switch (type) {
case ICNS.ICON: case ICNS.ICON:
case ICNS.ICN_: case ICNS.ICN_:
@ -290,12 +290,14 @@ final class IconResource {
} }
public boolean isForeignFormat() { public boolean isForeignFormat() {
// Recent entries contains full JPEG 2000 or PNG streams
switch (type) { switch (type) {
case ICNS.ic08: case ICNS.ic08:
case ICNS.ic09: case ICNS.ic09:
case ICNS.ic10: case ICNS.ic10:
return true; return true;
} }
return false; return false;
} }
@ -310,6 +312,7 @@ final class IconResource {
} }
private boolean isEqual(IconResource other) { 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; return start == other.start && type == other.type && length == other.length;
} }

View File

@ -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 <temp>}.
* Reads image back using ImageIO and known format (png).
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @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<ImageReader> 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;
}
}

View File

@ -61,19 +61,19 @@ 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 ic08, not readable without JAI or other JPEG 2000 support , new Dimension(256, 256), // JPEG 2000 ic08
// new Dimension(512, 512) // JPEG 2000 ic09 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 ic08 , new Dimension(256, 256), // JPEG 2000 ic08
// new Dimension(512, 512) // JPEG 2000 ic09 new Dimension(512, 512) // JPEG 2000 ic09
), ),
new TestData( 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(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
@ -82,7 +82,7 @@ public class ICNSImageReaderTest extends ImageReaderAbstractTestCase {
new Dimension(1024, 1024) // PNG ic10 new Dimension(1024, 1024) // PNG ic10
), ),
new TestData( 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), // 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(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), // 1 bit + 1 bit mask