#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.
This commit is contained in:
Harald Kuhr 2017-11-05 10:29:49 +01:00
parent 6b966a2d4f
commit 1c27b58598
9 changed files with 169 additions and 74 deletions

View File

@ -39,6 +39,7 @@ import org.w3c.dom.Node;
import org.w3c.dom.NodeList; import org.w3c.dom.NodeList;
import org.xml.sax.InputSource; import org.xml.sax.InputSource;
import org.xml.sax.SAXException; import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
import javax.imageio.IIOException; import javax.imageio.IIOException;
import javax.imageio.stream.ImageInputStream; import javax.imageio.stream.ImageInputStream;
@ -73,6 +74,7 @@ public final class XMPReader extends MetadataReader {
// TODO: Refactor scanner to return inputstream? // TODO: Refactor scanner to return inputstream?
// TODO: Be smarter about ASCII-NULL termination/padding (the SAXParser aka Xerces DOMParser doesn't like it)... // TODO: Be smarter about ASCII-NULL termination/padding (the SAXParser aka Xerces DOMParser doesn't like it)...
DocumentBuilder builder = factory.newDocumentBuilder(); DocumentBuilder builder = factory.newDocumentBuilder();
builder.setErrorHandler(new DefaultHandler());
Document document = builder.parse(new InputSource(IIOUtil.createStreamAdapter(input))); Document document = builder.parse(new InputSource(IIOUtil.createStreamAdapter(input)));
// XMLSerializer serializer = new XMLSerializer(System.err, System.getProperty("file.encoding")); // XMLSerializer serializer = new XMLSerializer(System.err, System.getProperty("file.encoding"));

View File

@ -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();
}
}

View File

@ -30,6 +30,7 @@ package com.twelvemonkeys.imageio.plugins.psd;
import com.twelvemonkeys.imageio.metadata.Directory; import com.twelvemonkeys.imageio.metadata.Directory;
import com.twelvemonkeys.imageio.metadata.tiff.TIFFReader; import com.twelvemonkeys.imageio.metadata.tiff.TIFFReader;
import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream;
import javax.imageio.stream.ImageInputStream; import javax.imageio.stream.ImageInputStream;
import java.io.IOException; import java.io.IOException;
@ -45,22 +46,26 @@ import java.io.IOException;
* @see <a href="http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html">Aware systems TIFF tag reference</a> * @see <a href="http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html">Aware systems TIFF tag reference</a>
* @see <a href="http://partners.adobe.com/public/developer/tiff/index.html">Adobe TIFF developer resources</a> * @see <a href="http://partners.adobe.com/public/developer/tiff/index.html">Adobe TIFF developer resources</a>
*/ */
final class PSDEXIF1Data extends PSDImageResource { final class PSDEXIF1Data extends PSDDirectoryResource {
protected Directory directory;
PSDEXIF1Data(final short pId, final ImageInputStream pInput) throws IOException { PSDEXIF1Data(final short pId, final ImageInputStream pInput) throws IOException {
super(pId, pInput); super(pId, pInput);
} }
@Override @Override
protected void readData(final ImageInputStream pInput) throws IOException { Directory parseDirectory() throws IOException {
// This is in essence an embedded TIFF file. // The data 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) return new TIFFReader().read(new ByteArrayImageInputStream(data));
directory = new TIFFReader().read(pInput);
} }
@Override @Override
public String toString() { public String toString() {
Directory directory = getDirectory();
if (directory == null) {
return super.toString();
}
StringBuilder builder = toStringBuilder(); StringBuilder builder = toStringBuilder();
builder.append(", ").append(directory); builder.append(", ").append(directory);
builder.append("]"); builder.append("]");

View File

@ -39,6 +39,9 @@ import java.io.IOException;
* @version $Id: PSDGlobalLayerMask.java,v 1.0 May 8, 2008 5:33:48 PM haraldk Exp$ * @version $Id: PSDGlobalLayerMask.java,v 1.0 May 8, 2008 5:33:48 PM haraldk Exp$
*/ */
final class PSDGlobalLayerMask { final class PSDGlobalLayerMask {
static final PSDGlobalLayerMask NULL_MASK = new PSDGlobalLayerMask();
final int colorSpace; final int colorSpace;
final short[] colors = new short[4]; final short[] colors = new short[4];
final int opacity; final int opacity;
@ -58,6 +61,12 @@ final class PSDGlobalLayerMask {
pInput.skipBytes(globalLayerMaskLength - 17); pInput.skipBytes(globalLayerMaskLength - 17);
} }
private PSDGlobalLayerMask() {
colorSpace = 0;
opacity = 0;
kind = 0;
}
@Override @Override
public String toString() { public String toString() {
StringBuilder builder = new StringBuilder(getClass().getSimpleName()); StringBuilder builder = new StringBuilder(getClass().getSimpleName());

View File

@ -40,8 +40,6 @@ import java.io.IOException;
* @version $Id: PSDHeader.java,v 1.0 Apr 29, 2008 5:18:22 PM haraldk Exp$ * @version $Id: PSDHeader.java,v 1.0 Apr 29, 2008 5:18:22 PM haraldk Exp$
*/ */
final class PSDHeader { 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 // typedef struct _PSD_HEADER
@ -57,6 +55,9 @@ final class PSDHeader {
// WORD Mode; /* Color mode */ // WORD Mode; /* Color mode */
// } PSD_HEADER; // } PSD_HEADER;
private static final int PSD_MAX_SIZE = 30000;
private static final int PSB_MAX_SIZE = 300000;
final short channels; final short channels;
final int width; final int width;
final int height; final int height;

View File

@ -30,6 +30,7 @@ package com.twelvemonkeys.imageio.plugins.psd;
import com.twelvemonkeys.imageio.metadata.Directory; import com.twelvemonkeys.imageio.metadata.Directory;
import com.twelvemonkeys.imageio.metadata.iptc.IPTCReader; import com.twelvemonkeys.imageio.metadata.iptc.IPTCReader;
import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream;
import javax.imageio.stream.ImageInputStream; import javax.imageio.stream.ImageInputStream;
import java.io.IOException; import java.io.IOException;
@ -41,21 +42,24 @@ import java.io.IOException;
* @author last modified by $Author: haraldk$ * @author last modified by $Author: haraldk$
* @version $Id: PSDIPTCData.java,v 1.0 Nov 7, 2009 9:52:14 PM haraldk Exp$ * @version $Id: PSDIPTCData.java,v 1.0 Nov 7, 2009 9:52:14 PM haraldk Exp$
*/ */
final class PSDIPTCData extends PSDImageResource { final class PSDIPTCData extends PSDDirectoryResource {
Directory directory;
PSDIPTCData(final short pId, final ImageInputStream pInput) throws IOException { PSDIPTCData(final short pId, final ImageInputStream pInput) throws IOException {
super(pId, pInput); super(pId, pInput);
} }
@Override @Override
protected void readData(final ImageInputStream pInput) throws IOException { Directory parseDirectory() throws IOException {
// Read IPTC directory return new IPTCReader().read(new ByteArrayImageInputStream(data));
directory = new IPTCReader().read(pInput);
} }
@Override @Override
public String toString() { public String toString() {
Directory directory = getDirectory();
if (directory == null) {
return super.toString();
}
StringBuilder builder = toStringBuilder(); StringBuilder builder = toStringBuilder();
builder.append(", ").append(directory); builder.append(", ").append(directory);
builder.append("]"); builder.append("]");

View File

@ -892,8 +892,8 @@ public final class PSDImageReader extends ImageReaderBase {
long imageResourcesLength = imageInput.readUnsignedInt(); long imageResourcesLength = imageInput.readUnsignedInt();
if (pParseData && metadata.imageResources == null && imageResourcesLength > 0) { if (pParseData && metadata.imageResources == null && imageResourcesLength > 0) {
metadata.imageResources = new ArrayList<>();
long expectedEnd = imageInput.getStreamPosition() + imageResourcesLength; long expectedEnd = imageInput.getStreamPosition() + imageResourcesLength;
metadata.imageResources = new ArrayList<>();
while (imageInput.getStreamPosition() < expectedEnd) { while (imageInput.getStreamPosition() < expectedEnd) {
PSDImageResource resource = PSDImageResource.read(imageInput); PSDImageResource resource = PSDImageResource.read(imageInput);
@ -975,6 +975,9 @@ public final class PSDImageReader extends ImageReaderBase {
} }
// TODO: Else skip? // TODO: Else skip?
} }
else {
metadata.globalLayerMask = PSDGlobalLayerMask.NULL_MASK;
}
// TODO: Parse "Additional layer information" // 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.seek(metadata.layerAndMaskInfoStart + layerAndMaskInfoLength + (header.largeFormat ? 8 : 4));
// imageInput.flushBefore(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("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); readLayerAndMaskInfo(true);
// NOTE: Need to make sure compression is set in metadata, even without reading the image data! // NOTE: Need to make sure compression is set in metadata, even without reading the image data!
// TODO: Move this to readLayerAndMaskInfo?
if (metadata.compression == -1) {
imageInput.seek(metadata.imageDataStart); imageInput.seek(metadata.imageDataStart);
metadata.compression = imageInput.readShort(); 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 return metadata; // TODO: clone if we change to mutable metadata
} }

View File

@ -53,7 +53,7 @@ import java.util.List;
*/ */
public final class PSDMetadata extends AbstractMetadata { 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"; 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 // 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"}; static final String[] PRINT_SCALE_STYLES = {"centered", "scaleToFit", "userDefined"};
protected PSDMetadata() { PSDMetadata() {
// TODO: Allow XMP, EXIF (TIFF) and IPTC as extra formats? // TODO: Allow XMP, EXIF (TIFF) and IPTC as extra formats?
super(true, NATIVE_METADATA_FORMAT_NAME, NATIVE_METADATA_FORMAT_CLASS_NAME, null, null); 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()); root.appendChild(createLayerInfoNode());
} }
if (globalLayerMask != null) { if (globalLayerMask != null && globalLayerMask != PSDGlobalLayerMask.NULL_MASK) {
root.appendChild(createGlobalLayerMaskNode()); root.appendChild(createGlobalLayerMaskNode());
} }
@ -291,9 +291,11 @@ public final class PSDMetadata extends AbstractMetadata {
node = new IIOMetadataNode("DirectoryResource"); node = new IIOMetadataNode("DirectoryResource");
node.setAttribute("type", "IPTC"); 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) { else if (imageResource instanceof PSDEXIF1Data) {
// TODO: Revise/rethink this... // TODO: Revise/rethink this...
@ -302,9 +304,11 @@ public final class PSDMetadata extends AbstractMetadata {
node = new IIOMetadataNode("DirectoryResource"); node = new IIOMetadataNode("DirectoryResource");
node.setAttribute("type", "TIFF"); node.setAttribute("type", "TIFF");
// TODO: Set byte[] data instead // 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) { else if (imageResource instanceof PSDXMPData) {
// TODO: Revise/rethink this... Would it be possible to parse XMP as IIOMetadataNodes? Or is that just stupid... // 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 = new IIOMetadataNode("DirectoryResource");
node.setAttribute("type", "XMP"); node.setAttribute("type", "XMP");
appendEntries(node, "XMP", xmp.directory);
// Set the entire XMP document as user data // Set the entire XMP document as user data
node.setUserObject(xmp.data); node.setUserObject(xmp.data);
if (xmp.getDirectory() != null) {
appendEntries(node, "XMP", xmp.getDirectory());
}
} }
else { else {
// Generic resource.. // Generic resource..
@ -662,7 +668,7 @@ public final class PSDMetadata extends AbstractMetadata {
PSDEXIF1Data data = exif.next(); PSDEXIF1Data data = exif.next();
// Get the EXIF DateTime (aka ModifyDate) tag if present // 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) { if (dateTime != null) {
IIOMetadataNode imageCreationTime = new IIOMetadataNode("ImageCreationTime"); // As TIFF, but could just as well be ImageModificationTime IIOMetadataNode imageCreationTime = new IIOMetadataNode("ImageCreationTime"); // As TIFF, but could just as well be ImageModificationTime
// Format: "YYYY:MM:DD hh:mm:ss" // Format: "YYYY:MM:DD hh:mm:ss"
@ -707,7 +713,7 @@ public final class PSDMetadata extends AbstractMetadata {
if (textResource instanceof PSDIPTCData) { if (textResource instanceof PSDIPTCData) {
PSDIPTCData iptc = (PSDIPTCData) textResource; PSDIPTCData iptc = (PSDIPTCData) textResource;
appendTextEntriesFlat(text, iptc.directory, new FilterIterator.Filter<Entry>() { appendTextEntriesFlat(text, iptc.getDirectory(), new FilterIterator.Filter<Entry>() {
public boolean accept(final Entry pEntry) { public boolean accept(final Entry pEntry) {
Integer tagId = (Integer) pEntry.getIdentifier(); Integer tagId = (Integer) pEntry.getIdentifier();
@ -727,7 +733,7 @@ public final class PSDMetadata extends AbstractMetadata {
else if (textResource instanceof PSDEXIF1Data) { else if (textResource instanceof PSDEXIF1Data) {
PSDEXIF1Data exif = (PSDEXIF1Data) textResource; PSDEXIF1Data exif = (PSDEXIF1Data) textResource;
appendTextEntriesFlat(text, exif.directory, new FilterIterator.Filter<Entry>() { appendTextEntriesFlat(text, exif.getDirectory(), new FilterIterator.Filter<Entry>() {
public boolean accept(final Entry pEntry) { public boolean accept(final Entry pEntry) {
Integer tagId = (Integer) pEntry.getIdentifier(); 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: 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..? // 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; return text;

View File

@ -31,11 +31,9 @@ package com.twelvemonkeys.imageio.plugins.psd;
import com.twelvemonkeys.imageio.metadata.Directory; import com.twelvemonkeys.imageio.metadata.Directory;
import com.twelvemonkeys.imageio.metadata.xmp.XMPReader; import com.twelvemonkeys.imageio.metadata.xmp.XMPReader;
import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream; import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream;
import com.twelvemonkeys.lang.StringUtil;
import javax.imageio.stream.ImageInputStream; import javax.imageio.stream.ImageInputStream;
import java.io.*; import java.io.IOException;
import java.nio.charset.Charset;
/** /**
* XMP metadata. * XMP metadata.
@ -47,19 +45,12 @@ import java.nio.charset.Charset;
* @see <a href="http://www.adobe.com/products/xmp/">Adobe Extensible Metadata Platform (XMP)</a> * @see <a href="http://www.adobe.com/products/xmp/">Adobe Extensible Metadata Platform (XMP)</a>
* @see <a href="http://www.adobe.com/devnet/xmp/">Adobe XMP Developer Center</a> * @see <a href="http://www.adobe.com/devnet/xmp/">Adobe XMP Developer Center</a>
*/ */
final class PSDXMPData extends PSDImageResource { final class PSDXMPData extends PSDDirectoryResource {
protected byte[] data;
Directory directory;
PSDXMPData(final short pId, final ImageInputStream pInput) throws IOException { PSDXMPData(final short pId, final ImageInputStream pInput) throws IOException {
super(pId, pInput); super(pId, pInput);
} }
@Override Directory parseDirectory() throws IOException {
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);
// Chop off potential trailing null-termination/padding that SAX parsers don't like... // Chop off potential trailing null-termination/padding that SAX parsers don't like...
int len = data.length; int len = data.length;
for (; len > 0; len--) { for (; len > 0; len--) {
@ -68,32 +59,6 @@ final class PSDXMPData extends PSDImageResource {
} }
} }
directory = new XMPReader().read(new ByteArrayImageInputStream(data, 0, len)); return 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"));
} }
} }