From 1c27b58598d69ef30b44f5e518c8260a62e7fc1e Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Sun, 5 Nov 2017 10:29:49 +0100 Subject: [PATCH] #390 Deferred parsing of embedded resources. Allows reading pixel data for images with unparseable metadata. Broken metadata is now ignored + warning, rather than causing exceptions. --- .../imageio/metadata/xmp/XMPReader.java | 2 + .../plugins/psd/PSDDirectoryResource.java | 82 +++++++++++++++++++ .../imageio/plugins/psd/PSDEXIF1Data.java | 17 ++-- .../plugins/psd/PSDGlobalLayerMask.java | 9 ++ .../imageio/plugins/psd/PSDHeader.java | 7 +- .../imageio/plugins/psd/PSDIPTCData.java | 16 ++-- .../imageio/plugins/psd/PSDImageReader.java | 30 +++++-- .../imageio/plugins/psd/PSDMetadata.java | 37 +++++---- .../imageio/plugins/psd/PSDXMPData.java | 43 +--------- 9 files changed, 169 insertions(+), 74 deletions(-) create mode 100644 imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDDirectoryResource.java diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/xmp/XMPReader.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/xmp/XMPReader.java index 28abce4f..aeb47bb7 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/xmp/XMPReader.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/xmp/XMPReader.java @@ -39,6 +39,7 @@ import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.InputSource; import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; import javax.imageio.IIOException; import javax.imageio.stream.ImageInputStream; @@ -73,6 +74,7 @@ public final class XMPReader extends MetadataReader { // TODO: Refactor scanner to return inputstream? // TODO: Be smarter about ASCII-NULL termination/padding (the SAXParser aka Xerces DOMParser doesn't like it)... DocumentBuilder builder = factory.newDocumentBuilder(); + builder.setErrorHandler(new DefaultHandler()); Document document = builder.parse(new InputSource(IIOUtil.createStreamAdapter(input))); // XMLSerializer serializer = new XMLSerializer(System.err, System.getProperty("file.encoding")); diff --git a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDDirectoryResource.java b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDDirectoryResource.java new file mode 100644 index 00000000..1a2e0205 --- /dev/null +++ b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDDirectoryResource.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2017, 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.psd; + +import com.twelvemonkeys.imageio.metadata.Directory; +import com.twelvemonkeys.lang.StringUtil; + +import javax.imageio.stream.ImageInputStream; +import java.io.IOException; + +/** + * PSDDirectoryResource + */ +abstract class PSDDirectoryResource extends PSDImageResource { + byte[] data; + private Directory directory; + + PSDDirectoryResource(short resourceId, ImageInputStream input) throws IOException { + super(resourceId, input); + } + + @Override + protected void readData(final ImageInputStream pInput) throws IOException { + data = new byte[(int) size]; // TODO: Fix potential overflow, or document why that can't happen (read spec) + pInput.readFully(data); + } + + abstract Directory parseDirectory() throws IOException; + + final void initDirectory() throws IOException { + if (directory == null) { + directory = parseDirectory(); + } + } + + Directory getDirectory() { + return directory; + } + + @Override + public String toString() { + StringBuilder builder = toStringBuilder(); + + int length = Math.min(256, data.length); + String data = StringUtil.decode(this.data, 0, length, "UTF-8").replace('\n', ' ').replaceAll("\\s+", " "); + builder.append(", data: \"").append(data); + + if (length < this.data.length) { + builder.append("..."); + } + + builder.append("\"]"); + + return builder.toString(); + } +} diff --git a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDEXIF1Data.java b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDEXIF1Data.java index 8fe3c3c8..d952f215 100644 --- a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDEXIF1Data.java +++ b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDEXIF1Data.java @@ -30,6 +30,7 @@ package com.twelvemonkeys.imageio.plugins.psd; import com.twelvemonkeys.imageio.metadata.Directory; import com.twelvemonkeys.imageio.metadata.tiff.TIFFReader; +import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream; import javax.imageio.stream.ImageInputStream; import java.io.IOException; @@ -45,22 +46,26 @@ import java.io.IOException; * @see Aware systems TIFF tag reference * @see Adobe TIFF developer resources */ -final class PSDEXIF1Data extends PSDImageResource { - protected Directory directory; +final class PSDEXIF1Data extends PSDDirectoryResource { PSDEXIF1Data(final short pId, final ImageInputStream pInput) throws IOException { super(pId, pInput); } @Override - protected void readData(final ImageInputStream pInput) throws IOException { - // This is in essence an embedded TIFF file. - // TODO: Instead, read the byte data, store for later parsing (or better yet, store offset, and read on request) - directory = new TIFFReader().read(pInput); + Directory parseDirectory() throws IOException { + // The data is in essence an embedded TIFF file. + return new TIFFReader().read(new ByteArrayImageInputStream(data)); } @Override public String toString() { + Directory directory = getDirectory(); + + if (directory == null) { + return super.toString(); + } + StringBuilder builder = toStringBuilder(); builder.append(", ").append(directory); builder.append("]"); diff --git a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDGlobalLayerMask.java b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDGlobalLayerMask.java index 6d26f396..ba9563ac 100755 --- a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDGlobalLayerMask.java +++ b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDGlobalLayerMask.java @@ -39,6 +39,9 @@ import java.io.IOException; * @version $Id: PSDGlobalLayerMask.java,v 1.0 May 8, 2008 5:33:48 PM haraldk Exp$ */ final class PSDGlobalLayerMask { + + static final PSDGlobalLayerMask NULL_MASK = new PSDGlobalLayerMask(); + final int colorSpace; final short[] colors = new short[4]; final int opacity; @@ -58,6 +61,12 @@ final class PSDGlobalLayerMask { pInput.skipBytes(globalLayerMaskLength - 17); } + private PSDGlobalLayerMask() { + colorSpace = 0; + opacity = 0; + kind = 0; + } + @Override public String toString() { StringBuilder builder = new StringBuilder(getClass().getSimpleName()); diff --git a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDHeader.java b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDHeader.java index 8892a6a8..12a4c07b 100755 --- a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDHeader.java +++ b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDHeader.java @@ -40,9 +40,7 @@ import java.io.IOException; * @version $Id: PSDHeader.java,v 1.0 Apr 29, 2008 5:18:22 PM haraldk Exp$ */ final class PSDHeader { - private static final int PSD_MAX_SIZE = 30000; - private static final int PSB_MAX_SIZE = 300000; - // The header is 26 bytes in length and is structured as follows: +// The header is 26 bytes in length and is structured as follows: // // typedef struct _PSD_HEADER // { @@ -57,6 +55,9 @@ final class PSDHeader { // WORD Mode; /* Color mode */ // } PSD_HEADER; + private static final int PSD_MAX_SIZE = 30000; + private static final int PSB_MAX_SIZE = 300000; + final short channels; final int width; final int height; diff --git a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDIPTCData.java b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDIPTCData.java index 367ee46b..b3952ee8 100644 --- a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDIPTCData.java +++ b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDIPTCData.java @@ -30,6 +30,7 @@ package com.twelvemonkeys.imageio.plugins.psd; import com.twelvemonkeys.imageio.metadata.Directory; import com.twelvemonkeys.imageio.metadata.iptc.IPTCReader; +import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream; import javax.imageio.stream.ImageInputStream; import java.io.IOException; @@ -41,21 +42,24 @@ import java.io.IOException; * @author last modified by $Author: haraldk$ * @version $Id: PSDIPTCData.java,v 1.0 Nov 7, 2009 9:52:14 PM haraldk Exp$ */ -final class PSDIPTCData extends PSDImageResource { - Directory directory; - +final class PSDIPTCData extends PSDDirectoryResource { PSDIPTCData(final short pId, final ImageInputStream pInput) throws IOException { super(pId, pInput); } @Override - protected void readData(final ImageInputStream pInput) throws IOException { - // Read IPTC directory - directory = new IPTCReader().read(pInput); + Directory parseDirectory() throws IOException { + return new IPTCReader().read(new ByteArrayImageInputStream(data)); } @Override public String toString() { + Directory directory = getDirectory(); + + if (directory == null) { + return super.toString(); + } + StringBuilder builder = toStringBuilder(); builder.append(", ").append(directory); builder.append("]"); diff --git a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDImageReader.java b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDImageReader.java index 0b50a536..b19f3e4c 100644 --- a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDImageReader.java +++ b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDImageReader.java @@ -892,8 +892,8 @@ public final class PSDImageReader extends ImageReaderBase { long imageResourcesLength = imageInput.readUnsignedInt(); if (pParseData && metadata.imageResources == null && imageResourcesLength > 0) { - metadata.imageResources = new ArrayList<>(); long expectedEnd = imageInput.getStreamPosition() + imageResourcesLength; + metadata.imageResources = new ArrayList<>(); while (imageInput.getStreamPosition() < expectedEnd) { PSDImageResource resource = PSDImageResource.read(imageInput); @@ -975,6 +975,9 @@ public final class PSDImageReader extends ImageReaderBase { } // TODO: Else skip? } + else { + metadata.globalLayerMask = PSDGlobalLayerMask.NULL_MASK; + } // TODO: Parse "Additional layer information" @@ -982,9 +985,9 @@ public final class PSDImageReader extends ImageReaderBase { // imageInput.seek(metadata.layerAndMaskInfoStart + layerAndMaskInfoLength + (header.largeFormat ? 8 : 4)); // imageInput.flushBefore(metadata.layerAndMaskInfoStart + layerAndMaskInfoLength + (header.largeFormat ? 8 : 4)); - if (DEBUG) { + if (pParseData && DEBUG) { System.out.println("layerInfo: " + metadata.layerInfo); - System.out.println("globalLayerMask: " + metadata.globalLayerMask); + System.out.println("globalLayerMask: " + (metadata.globalLayerMask != PSDGlobalLayerMask.NULL_MASK ? metadata.globalLayerMask : null)); } //} } @@ -1171,8 +1174,25 @@ public final class PSDImageReader extends ImageReaderBase { readLayerAndMaskInfo(true); // NOTE: Need to make sure compression is set in metadata, even without reading the image data! - imageInput.seek(metadata.imageDataStart); - metadata.compression = imageInput.readShort(); + // TODO: Move this to readLayerAndMaskInfo? + if (metadata.compression == -1) { + imageInput.seek(metadata.imageDataStart); + metadata.compression = imageInput.readShort(); + } + + // Initialize XMP data etc. + for (PSDImageResource resource : metadata.imageResources) { + if (resource instanceof PSDDirectoryResource) { + PSDDirectoryResource directoryResource = (PSDDirectoryResource) resource; + + try { + directoryResource.initDirectory(); + } + catch (IOException e) { + processWarningOccurred(String.format("Error parsing %s: %s", resource.getClass().getSimpleName(), e.getMessage())); + } + } + } return metadata; // TODO: clone if we change to mutable metadata } diff --git a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDMetadata.java b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDMetadata.java index 2f2ba8be..8a9303eb 100755 --- a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDMetadata.java +++ b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDMetadata.java @@ -53,7 +53,7 @@ import java.util.List; */ public final class PSDMetadata extends AbstractMetadata { - public static final String NATIVE_METADATA_FORMAT_NAME = "com_twelvemonkeys_imageio_psd_image_1.0"; + static final String NATIVE_METADATA_FORMAT_NAME = "com_twelvemonkeys_imageio_psd_image_1.0"; static final String NATIVE_METADATA_FORMAT_CLASS_NAME = "com.twelvemonkeys.imageio.plugins.psd.PSDMetadataFormat"; // TODO: Support TIFF metadata, based on EXIF/XMP + merge in PSD specifics @@ -93,7 +93,7 @@ public final class PSDMetadata extends AbstractMetadata { static final String[] PRINT_SCALE_STYLES = {"centered", "scaleToFit", "userDefined"}; - protected PSDMetadata() { + PSDMetadata() { // TODO: Allow XMP, EXIF (TIFF) and IPTC as extra formats? super(true, NATIVE_METADATA_FORMAT_NAME, NATIVE_METADATA_FORMAT_CLASS_NAME, null, null); } @@ -118,7 +118,7 @@ public final class PSDMetadata extends AbstractMetadata { root.appendChild(createLayerInfoNode()); } - if (globalLayerMask != null) { + if (globalLayerMask != null && globalLayerMask != PSDGlobalLayerMask.NULL_MASK) { root.appendChild(createGlobalLayerMaskNode()); } @@ -291,9 +291,11 @@ public final class PSDMetadata extends AbstractMetadata { node = new IIOMetadataNode("DirectoryResource"); node.setAttribute("type", "IPTC"); - node.setUserObject(iptc.directory); + node.setUserObject(iptc.data); - appendEntries(node, "IPTC", iptc.directory); + if (iptc.getDirectory() != null) { + appendEntries(node, "IPTC", iptc.getDirectory()); + } } else if (imageResource instanceof PSDEXIF1Data) { // TODO: Revise/rethink this... @@ -302,9 +304,11 @@ public final class PSDMetadata extends AbstractMetadata { node = new IIOMetadataNode("DirectoryResource"); node.setAttribute("type", "TIFF"); // TODO: Set byte[] data instead - node.setUserObject(exif.directory); + node.setUserObject(exif.data); - appendEntries(node, "EXIF", exif.directory); + if (exif.getDirectory() != null) { + appendEntries(node, "EXIF", exif.getDirectory()); + } } else if (imageResource instanceof PSDXMPData) { // TODO: Revise/rethink this... Would it be possible to parse XMP as IIOMetadataNodes? Or is that just stupid... @@ -313,10 +317,12 @@ public final class PSDMetadata extends AbstractMetadata { node = new IIOMetadataNode("DirectoryResource"); node.setAttribute("type", "XMP"); - appendEntries(node, "XMP", xmp.directory); - // Set the entire XMP document as user data node.setUserObject(xmp.data); + + if (xmp.getDirectory() != null) { + appendEntries(node, "XMP", xmp.getDirectory()); + } } else { // Generic resource.. @@ -662,7 +668,7 @@ public final class PSDMetadata extends AbstractMetadata { PSDEXIF1Data data = exif.next(); // Get the EXIF DateTime (aka ModifyDate) tag if present - Entry dateTime = data.directory.getEntryById(TIFF.TAG_DATE_TIME); + Entry dateTime = data.getDirectory().getEntryById(TIFF.TAG_DATE_TIME); if (dateTime != null) { IIOMetadataNode imageCreationTime = new IIOMetadataNode("ImageCreationTime"); // As TIFF, but could just as well be ImageModificationTime // Format: "YYYY:MM:DD hh:mm:ss" @@ -707,7 +713,7 @@ public final class PSDMetadata extends AbstractMetadata { if (textResource instanceof PSDIPTCData) { PSDIPTCData iptc = (PSDIPTCData) textResource; - appendTextEntriesFlat(text, iptc.directory, new FilterIterator.Filter() { + appendTextEntriesFlat(text, iptc.getDirectory(), new FilterIterator.Filter() { public boolean accept(final Entry pEntry) { Integer tagId = (Integer) pEntry.getIdentifier(); @@ -727,7 +733,7 @@ public final class PSDMetadata extends AbstractMetadata { else if (textResource instanceof PSDEXIF1Data) { PSDEXIF1Data exif = (PSDEXIF1Data) textResource; - appendTextEntriesFlat(text, exif.directory, new FilterIterator.Filter() { + appendTextEntriesFlat(text, exif.getDirectory(), new FilterIterator.Filter() { public boolean accept(final Entry pEntry) { Integer tagId = (Integer) pEntry.getIdentifier(); @@ -743,11 +749,12 @@ public final class PSDMetadata extends AbstractMetadata { } }); } - else if (textResource instanceof PSDXMPData) { + //else if (textResource instanceof PSDXMPData) { // TODO: Parse XMP (heavy) ONLY if we don't have required fields from IPTC/EXIF? // TODO: Use XMP IPTC/EXIF/TIFF NativeDigest field to validate if the values are in sync..? - PSDXMPData xmp = (PSDXMPData) textResource; - } + // TODO: Use XMP IPTC/EXIF/TIFF NativeDigest field to validate if the values are in sync..? + //PSDXMPData xmp = (PSDXMPData) textResource; + //} } return text; diff --git a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDXMPData.java b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDXMPData.java index dfd6c76c..78cc002c 100644 --- a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDXMPData.java +++ b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDXMPData.java @@ -31,11 +31,9 @@ package com.twelvemonkeys.imageio.plugins.psd; import com.twelvemonkeys.imageio.metadata.Directory; import com.twelvemonkeys.imageio.metadata.xmp.XMPReader; import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream; -import com.twelvemonkeys.lang.StringUtil; import javax.imageio.stream.ImageInputStream; -import java.io.*; -import java.nio.charset.Charset; +import java.io.IOException; /** * XMP metadata. @@ -47,19 +45,12 @@ import java.nio.charset.Charset; * @see Adobe Extensible Metadata Platform (XMP) * @see Adobe XMP Developer Center */ -final class PSDXMPData extends PSDImageResource { - protected byte[] data; - Directory directory; - +final class PSDXMPData extends PSDDirectoryResource { PSDXMPData(final short pId, final ImageInputStream pInput) throws IOException { super(pId, pInput); } - @Override - protected void readData(final ImageInputStream pInput) throws IOException { - data = new byte[(int) size]; // TODO: Fix potential overflow, or document why that can't happen (read spec) - pInput.readFully(data); - + Directory parseDirectory() throws IOException { // Chop off potential trailing null-termination/padding that SAX parsers don't like... int len = data.length; for (; len > 0; len--) { @@ -68,32 +59,6 @@ final class PSDXMPData extends PSDImageResource { } } - directory = new XMPReader().read(new ByteArrayImageInputStream(data, 0, len)); - } - - @Override - public String toString() { - StringBuilder builder = toStringBuilder(); - - int length = Math.min(256, data.length); - String data = StringUtil.decode(this.data, 0, length, "UTF-8").replace('\n', ' ').replaceAll("\\s+", " "); - builder.append(", data: \"").append(data); - - if (length < this.data.length) { - builder.append("..."); - } - - builder.append("\"]"); - - return builder.toString(); - } - - /** - * Returns a character stream containing the XMP metadata (XML). - * - * @return the XMP metadata. - */ - public Reader getData() { - return new InputStreamReader(new ByteArrayInputStream(data), Charset.forName("UTF-8")); + return new XMPReader().read(new ByteArrayImageInputStream(data, 0, len)); } }