Compare commits

..

49 Commits

Author SHA1 Message Date
Harald Kuhr 779f720aa0 Updated POMs to 2.3 release. 2010-02-07 19:27:39 +01:00
Harald Kuhr 7ea9b8f537 Removed system out println. 2010-02-07 19:27:10 +01:00
Harald Kuhr d2742b36de Fixed test cases that failed on JDK 1.6.0_17 (something's up with the JPEG encoding/default BG color of transparent images...). 2010-02-07 18:47:59 +01:00
Harald Kuhr 3d4d60c664 No longer closes the stream. The wrapped stream will take care of that. 2010-02-07 18:00:09 +01:00
Harald Kuhr 1c58bdbb81 Added a simple todo. 2010-02-07 17:58:13 +01:00
Harald Kuhr 4b7a17ba30 Fixed bug that XMLSerializer output wrong processing instructions. 2010-02-07 17:57:38 +01:00
Harald Kuhr 261c6f8038 Implemented to/from CIEXYZ (via sRGB conversio) for completeness. 2009-12-27 16:34:13 +01:00
Harald Kuhr ef09b599f6 Updated metadataformat to use new Metadata classes (how did IDEA's refactorings miss this? Hmmm...) 2009-12-27 16:33:09 +01:00
Harald Kuhr 20d6e35640 Work in progress for PSD metadata support:
- Added Rational class for EXIF datatype support
 - Added more EXIF fields/field names
 - Added/updated license in source files
2009-12-10 18:48:12 +01:00
Harald Kuhr d97a0cc00b Now allows 64 entries in the EHB pallette. 2009-12-07 13:45:41 +01:00
Harald Kuhr c7fd5b3dd9 Fixed a bug related to transcoding images with indexed color (ie. GIF) to JPEG.
Thanks to Rune Bremnes for pointing it out and providing a patch and sample image!
2009-12-03 15:29:38 +01:00
Harald Kuhr 64b21b83bb Work in progress for PSD metadata support:
- Implemented XMP Reader, Directory and Entry
 - More EXIF and IPTC changes
 - Cleaning up
2009-11-18 00:43:18 +01:00
Harald Kuhr d24c2c1b08 Work in progress for PSD metadata support:
- Cleaned up EXIF, IPTC and XMP metadata classes.
2009-11-14 22:49:45 +01:00
Harald Kuhr aad80d043f Work in progress for PSD metadata support:
- Refactored metadata support
 - Moved standard metadata support (EXIF, IPTC & XMP) to separate module
 - Changes to PSD metadata implementation
2009-11-14 22:42:21 +01:00
Harald Kuhr effd80d42f - Added SubImageInputStream with test case.
- Changed BufferedImageInputStream to return -1 instead of the hack throwing of unchecked IOException.
2009-11-09 20:17:59 +01:00
Harald Kuhr 2ab9cbadee Some minor clean-up.
Added package info to support packages.
2009-11-08 14:45:56 +01:00
Harald Kuhr d1cc3deec1 Bumping versions to 2.3-SNAPSHOT. 2009-11-08 14:44:58 +01:00
Harald Kuhr b7124585c5 Removed a misleading comment. 2009-11-08 14:43:39 +01:00
Harald Kuhr 69d77071b9 Allows passing object array to toCSVString method. 2009-11-08 14:43:16 +01:00
Harald Kuhr ceca94135b Added some TODOs.. 2009-11-08 14:42:10 +01:00
Harald Kuhr dbca2fc099 Minor changes in ANNO chunk written.
Clean-up in reader.
2009-11-08 14:40:55 +01:00
Harald Kuhr 54cf727dee Work in progress for PSD metadata support:
- Changes to native format spec
 - Implemented more of native format
 - Added several more resource type implementations
 - IPTC metadata support
2009-11-08 14:39:32 +01:00
Harald Kuhr bf5c6e9d47 Work in progress for PSD metadata support:
- Implemented more of standard support
 - Changes to native format spec
 - Implemented more of native format
 - Minor changes in various resources due to meta data implementation
2009-11-06 23:54:48 +01:00
Harald Kuhr def1d47344 Work in progress for PSD metadata support:
- Added PSDMetadata and PSDMetadataFormat
 - Implemented most of standard format
 - Start of native format definintion
 - Updated SPI and Reader to return new format
2009-11-06 02:26:47 +01:00
Harald Kuhr 5dab7eb1ff Removed an old file that shouldn't have been committed... 2009-11-05 16:59:29 +01:00
Harald Kuhr 9849aeb2a7 Removed some redundant comments. 2009-11-04 16:26:04 +01:00
Harald Kuhr 0a71af5405 Fixed reading of gray-scale IFFs.
Added some comments & clean-up.
2009-11-04 16:08:20 +01:00
Harald Kuhr 96a74e0b81 Fixed numerous long-standing bugs in the IFFImageReader and IFFImageWriter.
- Fixed EOF bug for RLE compressed BODY chunks with padding
 - Fixed alignment bug for rows (now 16 bit aligned/padded).
 - Added a couple of TODOs for more known bugs...
 - Some general clean-up.
2009-11-04 01:27:56 +01:00
Harald Kuhr c801926a02 Added default background color for transparent indexed images. 2009-11-04 01:23:38 +01:00
Harald Kuhr ebc365528a Added title to ProviderInfo.
Added toString method.
2009-11-04 01:22:35 +01:00
Harald Kuhr 317ed16814 Added package info to manifest. 2009-11-04 01:21:39 +01:00
Harald Kuhr ecc79e0478 Added extra constructor to DecoderStream to allow for correct IFF reading.
Doc clean-up.
2009-11-04 01:21:10 +01:00
Harald Kuhr 8572633686 Added manifest and provider info to all plugins. 2009-11-03 15:10:54 +01:00
Harald Kuhr 67b985bc1d ProviderInfo now returns "Unspecified" for unknown packages without version info. 2009-11-03 14:49:48 +01:00
Harald Kuhr 9c443f28e3 Fixed typos. 2009-11-02 23:24:56 +01:00
Harald Kuhr 46ab06f471 Made test cases more reliable. 2009-11-02 17:40:03 +01:00
Harald Kuhr 18b86f8d26 Added documentation and fixed typos. 2009-11-01 17:03:06 +01:00
Harald Kuhr 82fdde897d Added documentation and fixed typos. 2009-11-01 16:56:53 +01:00
Harald Kuhr f49a487c88 - Introduce new class ProviderInfo, to encapsulate Package info (vendor/version).
- Rewritten Spi classes to use new class, to have vendor and version info injected from manifest.
2009-10-31 19:16:48 +01:00
Harald Kuhr fb2c555d21 Added method to serialize a subtree (Node) only.
Added documentation.
Minor clean-up.
2009-10-31 19:14:16 +01:00
Harald Kuhr 4bd0763d48 Bumping versions to 2.3-SNAPSHOT. 2009-10-31 19:11:31 +01:00
Harald Kuhr 4674b9e344 Bumping versions to 2.3-SNAPSHOT. 2009-10-31 19:10:27 +01:00
Harald Kuhr 68b30413ba toString now shows more useful info for clipping. 2009-10-25 23:34:34 +01:00
Harald Kuhr ebd9153e40 Cleaned up reading of layers. 2009-10-25 23:33:37 +01:00
Harald Kuhr 669f575585 Cleaned up reading of layers. 2009-10-25 21:08:42 +01:00
Harald Kuhr d04d4a9a97 Made fields final but accessible. 2009-10-25 21:08:11 +01:00
Harald Kuhr 17e8de8c99 Experimental reading of layers (8 bit channels only ATM). 2009-10-25 17:05:09 +01:00
Harald Kuhr ef7029f306 Making more fields accessible, needed for reading layers. 2009-10-25 17:04:14 +01:00
Harald Kuhr 30b97483bd Starting work on 2.3 release.. 2009-10-25 16:59:56 +01:00
108 changed files with 5649 additions and 1419 deletions
+17 -1
View File
@@ -5,7 +5,7 @@
<modelVersion>4.0.0</modelVersion>
<artifactId>twelvemonkeys-core</artifactId>
<groupId>com.twelvemonkeys</groupId>
<version>2.2</version>
<version>2.3</version>
<name>TwelveMonkeys Core</name>
<description>
The TwelveMonkeys Core library. Contains common utility classes.
@@ -53,6 +53,22 @@
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.2</version>
<configuration>
<archive>
<manifestEntries>
<Implementation-Title>${project.name}</Implementation-Title>
<Implementation-Vendor>TwelveMonkeys</Implementation-Vendor>
<Implementation-Version>${project.version}</Implementation-Version>
<Implementation-URL>http://github.com/haraldk/TwelveMonkeys</Implementation-URL>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -52,6 +52,7 @@ public final class DecoderStream extends FilterInputStream {
/**
* Creates a new decoder stream and chains it to the
* input stream specified by the {@code pStream} argument.
* The stream will use a default decode buffer size.
*
* @param pStream the underlying input stream.
* @param pDecoder the decoder that will be used to decode the underlying stream
@@ -59,9 +60,24 @@ public final class DecoderStream extends FilterInputStream {
* @see java.io.FilterInputStream#in
*/
public DecoderStream(final InputStream pStream, final Decoder pDecoder) {
// TODO: Let the decoder decide preferred buffer size
this(pStream, pDecoder, 1024);
}
/**
* Creates a new decoder stream and chains it to the
* input stream specified by the {@code pStream} argument.
*
* @param pStream the underlying input stream.
* @param pDecoder the decoder that will be used to decode the underlying stream
* @param pBufferSize the size of the decode buffer
*
* @see java.io.FilterInputStream#in
*/
public DecoderStream(final InputStream pStream, final Decoder pDecoder, final int pBufferSize) {
super(pStream);
mDecoder = pDecoder;
mBuffer = new byte[1024];
mBuffer = new byte[pBufferSize];
mBufferPos = 0;
mBufferLimit = 0;
}
@@ -142,7 +158,7 @@ public final class DecoderStream extends FilterInputStream {
// Skip until we have skipped pLength bytes, or have reached EOF
long total = 0;
while (total < pLength) {
int avail = mBufferLimit - mBufferPos;
@@ -75,7 +75,7 @@ public final class PackBitsDecoder implements Decoder {
}
/**
* Creates a {@code PackBitsDecoder}.
* Creates a {@code PackBitsDecoder}, with optional compatibility mode.
* <p/>
* As some implementations of PackBits-like encoders treat {@code -128} as length of
* a compressed run, instead of a no-op, it's possible to disable no-ops
@@ -107,6 +107,7 @@ public final class PackBitsDecoder implements Decoder {
int read = 0;
final int max = pBuffer.length;
// TODO: Don't decode more than single runs, because some writers add pad bytes inside the stream...
while (read < max) {
int n;
@@ -1572,7 +1572,7 @@ public final class StringUtil {
* @param pStringArray the string array
* @return A string of comma-separated values
*/
public static String toCSVString(String[] pStringArray) {
public static String toCSVString(Object[] pStringArray) {
return toCSVString(pStringArray, ", ");
}
@@ -1584,7 +1584,7 @@ public final class StringUtil {
* @return string of delimiter separated values
* @throws IllegalArgumentException if {@code pDelimiterString == null}
*/
public static String toCSVString(String[] pStringArray, String pDelimiterString) {
public static String toCSVString(Object[] pStringArray, String pDelimiterString) {
if (pStringArray == null) {
return "";
}
@@ -115,13 +115,7 @@ public class BASE64 {
return buf.toString();
}
/**
* Quick implementation, using the undocumented
* {@code sun.misc.BASE64Decoder.decodeBuffer(String)}.
*/
public static byte[] decode(String pData) throws java.io.IOException {
//return DECODER.decodeBuffer(pData);
public static byte[] decode(String pData) throws IOException {
InputStream in = new DecoderStream(new ByteArrayInputStream(pData.getBytes()), new Base64Decoder());
ByteArrayOutputStream bytes = new FastByteArrayOutputStream(pData.length() * 3);
FileUtil.copy(in, bytes);
@@ -131,7 +125,7 @@ public class BASE64 {
//private final static sun.misc.BASE64Decoder DECODER = new sun.misc.BASE64Decoder();
public static void main(String[] pArgs) throws java.io.IOException {
public static void main(String[] pArgs) throws IOException {
if (pArgs.length == 1) {
System.out.println(encode(pArgs[0].getBytes()));
}
@@ -51,13 +51,13 @@ public class XMLSerializer {
// Main problem: Sun's Java 5 does not have LS 3.0 support
// This class has no dependencies, which probably makes it more useful
// TODO: Don't insert initial and ending line-break for text-nodes
// TODO: Support not inserting line-breaks, to preserve space
// TODO: Support line breaking (at configurable width)
// TODO: Support skipping XML declaration?
// TODO: Support standalone?
// TODO: Support more than version 1.0?
// TODO: Consider using IOException to communicate trouble, rather than RTE,
// to be more compatible...
// TODO: Support not inserting line-breaks, to preserve space
// TODO: Idea: Create a SerializationContext that stores attributes on
// serialization, to keep the serialization thread-safe
@@ -75,32 +75,51 @@ public class XMLSerializer {
mContext = new SerializationContext();
}
public void setIndentation(String pIndent) {
public final void setIndentation(String pIndent) {
mContext.indent = pIndent != null ? pIndent : " ";
}
public void setStripComments(boolean pStrip) {
public final void setStripComments(boolean pStrip) {
mContext.stripComments = pStrip;
}
/**
* Serializes the entire document, along with the XML declaration
* ({@code &lt;?xml version="1.0" encoding="..."?&gt;}).
*
* @param pDocument the document to serialize.
*/
public void serialize(final Document pDocument) {
serialize(pDocument, true);
}
/**
* Serializes the entire sub tree starting at {@code pRootNode}, along with an optional XML declaration
* ({@code &lt;?xml version="1.0" encoding="..."?&gt;}).
*
* @param pRootNode the root node to serialize.
* @param pWriteXMLDeclaration {@code true} if the XML declaration should be included, otherwise {@code false}.
*/
public void serialize(final Node pRootNode, final boolean pWriteXMLDeclaration) {
PrintWriter out = new PrintWriter(new OutputStreamWriter(mOutput, mEncoding));
try {
writeXMLDeclararion(out);
writeXML(out, pDocument, mContext.copy());
if (pWriteXMLDeclaration) {
writeXMLDeclaration(out);
}
writeXML(out, pRootNode, mContext.copy());
}
finally {
out.flush();
}
}
private void writeXMLDeclararion(final PrintWriter pOut) {
private void writeXMLDeclaration(final PrintWriter pOut) {
pOut.print("<?xml version=\"1.0\" encoding=\"");
pOut.print(mEncoding.name());
pOut.println("\"?>");
}
private void writeXML(final PrintWriter pOut, final Document pDocument, final SerializationContext pContext) {
private void writeXML(final PrintWriter pOut, final Node pDocument, final SerializationContext pContext) {
writeNodeRecursive(pOut, pDocument, pContext);
}
@@ -133,7 +152,7 @@ public class XMLSerializer {
writeComment(pOut, pNode, pContext);
break;
case Node.PROCESSING_INSTRUCTION_NODE:
writeProcessingInstruction(pOut, pNode);
writeProcessingInstruction(pOut, (ProcessingInstruction) pNode);
break;
case Node.ATTRIBUTE_NODE:
throw new IllegalArgumentException("Malformed input Document: Attribute nodes should only occur inside Element nodes");
@@ -148,9 +167,16 @@ public class XMLSerializer {
}
}
private void writeProcessingInstruction(final PrintWriter pOut, final Node pNode) {
private void writeProcessingInstruction(final PrintWriter pOut, final ProcessingInstruction pNode) {
pOut.print("\n<?");
pOut.print(pNode.getNodeValue());
pOut.print(pNode.getTarget());
String value = pNode.getData();
if (value != null) {
pOut.print(" ");
pOut.print(value);
}
pOut.println("?>");
}
@@ -161,8 +187,11 @@ public class XMLSerializer {
pOut.print(maybeEscapeElementValue(value));
}
else if (!StringUtil.isEmpty(value)) {
indentToLevel(pOut, pContext);
pOut.println(maybeEscapeElementValue(value.trim()));
String escapedValue = maybeEscapeElementValue(value.trim());
//if (escapedValue.length() + (pContext.level * pContext.indent.length()) > 78) {
indentToLevel(pOut, pContext);
//}
pOut.println(escapedValue);
}
}
@@ -183,7 +212,7 @@ public class XMLSerializer {
else if ("default".equals(space.getNodeValue())) {
pContext.preserveSpace = false;
}
// No other values are allowed per spec, ingore
// No other values are allowed per spec, ignore
}
}
}
@@ -200,7 +229,7 @@ public class XMLSerializer {
}
String value = pNode.getNodeValue();
validateCommenValue(value);
validateCommentValue(value);
if (value.startsWith(" ")) {
pOut.print("<!--");
@@ -229,7 +258,7 @@ public class XMLSerializer {
int startEscape = needsEscapeElement(pValue);
if (startEscape < 0) {
// If no escpaing is needed, simply return original
// If no escaping is needed, simply return original
return pValue;
}
else {
@@ -355,6 +384,7 @@ public class XMLSerializer {
default:
}
}
return -1;
}
@@ -365,7 +395,7 @@ public class XMLSerializer {
return pValue;
}
private static String validateCommenValue(final String pValue) {
private static String validateCommentValue(final String pValue) {
if (pValue.indexOf("--") >= 0) {
throw new IllegalArgumentException("Malformed input document: Comment may not contain the string '--'");
}
@@ -388,6 +418,9 @@ public class XMLSerializer {
// TODO: Attributes should probably include namespaces, so that it works
// even if the document was created using attributes instead of namespaces...
// In that case, prefix will be null...
// TODO: Don't insert duplicate/unnecessary namesspace declarations
// Handle namespace
String namespace = pNode.getNamespaceURI();
@@ -425,6 +458,7 @@ public class XMLSerializer {
}
}
// TODO: Consider not indenting/newline if the first child is a text node
// Iterate children if any
if (pNode.hasChildNodes()) {
pOut.print(">");
@@ -433,11 +467,9 @@ public class XMLSerializer {
}
NodeList children = pNode.getChildNodes();
//pContext.level++;
for (int i = 0; i < children.getLength(); i++) {
writeNodeRecursive(pOut, children.item(i), pContext.push());
}
//pContext.level--;
if (!pContext.preserveSpace) {
indentToLevel(pOut, pContext);
@@ -496,12 +528,15 @@ public class XMLSerializer {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true);
DocumentBuilder builder;
try {
builder = factory.newDocumentBuilder();
}
catch (ParserConfigurationException e) {
//noinspection ThrowableInstanceNeverThrown BOGUS
throw (IOException) new IOException(e.getMessage()).initCause(e);
}
DOMImplementation dom = builder.getDOMImplementation();
Document document = dom.createDocument("http://www.twelvemonkeys.com/xml/test", "test", dom.createDocumentType("test", null, null));
+2 -2
View File
@@ -5,7 +5,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>twelvemonkeys-imageio-batik</artifactId>
<version>2.2</version>
<version>2.3</version>
<name>TwelveMonkeys ImageIO Batik Plugin</name>
<description>
<![CDATA[
@@ -17,7 +17,7 @@
<parent>
<artifactId>twelvemonkeys-imageio</artifactId>
<groupId>com.twelvemonkeys</groupId>
<version>2.2</version>
<version>2.3</version>
</parent>
<dependencies>
@@ -28,6 +28,7 @@
package com.twelvemonkeys.imageio.plugins.svg;
import com.twelvemonkeys.imageio.spi.ProviderInfo;
import com.twelvemonkeys.lang.SystemUtil;
import com.twelvemonkeys.imageio.util.IIOUtil;
@@ -50,12 +51,16 @@ public class SVGImageReaderSpi extends ImageReaderSpi {
private final static boolean SVG_READER_AVAILABLE = SystemUtil.isClassAvailable("com.twelvemonkeys.imageio.plugins.svg.SVGImageReader");
/**
* Creates an SVGImageReaderSpi
* Creates an {@code SVGImageReaderSpi}.
*/
public SVGImageReaderSpi() {
this(IIOUtil.getProviderInfo(SVGImageReaderSpi.class));
}
private SVGImageReaderSpi(final ProviderInfo pProviderInfo) {
super(
"TwelveMonkeys", // Vendor name
"2.0", // Version
pProviderInfo.getVendorName(), // Vendor name
pProviderInfo.getVersion(), // Version
SVG_READER_AVAILABLE ? new String[]{"svg", "SVG"} : new String[]{""}, // Names
SVG_READER_AVAILABLE ? new String[]{"svg"} : null, // Suffixes
SVG_READER_AVAILABLE ? new String[]{"image/svg", "image/x-svg", "image/svg+xml", "image/svg-xml"} : null, // Mime-types
@@ -28,6 +28,7 @@
package com.twelvemonkeys.imageio.plugins.tiff;
import com.twelvemonkeys.imageio.spi.ProviderInfo;
import com.twelvemonkeys.lang.SystemUtil;
import com.twelvemonkeys.imageio.util.IIOUtil;
@@ -50,12 +51,16 @@ public class TIFFImageReaderSpi extends ImageReaderSpi {
final static boolean TIFF_CLASSES_AVAILABLE = SystemUtil.isClassAvailable("com.twelvemonkeys.imageio.plugins.tiff.TIFFImageReader");
/**
* Creates an SVGImageReaderSpi
* Creates a {@code TIFFImageReaderSpi}.
*/
public TIFFImageReaderSpi() {
this(IIOUtil.getProviderInfo(TIFFImageReaderSpi.class));
}
private TIFFImageReaderSpi(final ProviderInfo pProviderInfo) {
super(
"TwelveMonkeys", // Vendor name
"2.0", // Version
pProviderInfo.getVendorName(), // Vendor name
pProviderInfo.getVersion(), // Version
TIFF_CLASSES_AVAILABLE ? new String[]{"tiff", "TIFF"} : new String[] {""}, // Names
TIFF_CLASSES_AVAILABLE ? new String[]{"tiff", "tif"} : null, // Suffixes
TIFF_CLASSES_AVAILABLE ? new String[]{"image/tiff", "image/x-tiff"} : null, // Mime-types
@@ -28,6 +28,7 @@
package com.twelvemonkeys.imageio.plugins.tiff;
import com.twelvemonkeys.imageio.spi.ProviderInfo;
import com.twelvemonkeys.imageio.util.IIOUtil;
import javax.imageio.ImageTypeSpecifier;
@@ -46,12 +47,16 @@ import java.util.Locale;
public class TIFFImageWriterSpi extends ImageWriterSpi {
/**
* Creates a TIFFImageWriterSpi.
* Creates a {@code TIFFImageWriterSpi}.
*/
public TIFFImageWriterSpi() {
this(IIOUtil.getProviderInfo(TIFFImageWriterSpi.class));
}
private TIFFImageWriterSpi(final ProviderInfo pProviderInfo) {
super(
"TwelveMonkeys", // Vendor name
"2.0", // Version
pProviderInfo.getVendorName(), // Vendor name
pProviderInfo.getVersion(), // Version
new String[]{"tiff", "TIFF"}, // Names
new String[]{"tif", "tiff"}, // Suffixes
new String[]{"image/tiff", "image/x-tiff"}, // Mime-types
@@ -28,6 +28,7 @@
package com.twelvemonkeys.imageio.plugins.wmf;
import com.twelvemonkeys.imageio.spi.ProviderInfo;
import com.twelvemonkeys.lang.SystemUtil;
import com.twelvemonkeys.imageio.util.IIOUtil;
@@ -51,12 +52,16 @@ public class WMFImageReaderSpi extends ImageReaderSpi {
private final static boolean WMF_READER_AVAILABLE = SystemUtil.isClassAvailable("com.twelvemonkeys.imageio.plugins.svg.SVGImageReader");
/**
* Creates an SVGImageReaderSpi
* Creates a {@code WMFImageReaderSpi}.
*/
public WMFImageReaderSpi() {
this(IIOUtil.getProviderInfo(WMFImageReaderSpi.class));
}
private WMFImageReaderSpi(final ProviderInfo pProviderInfo) {
super(
"TwelveMonkeys", // Vendor name
"2.0", // Version
pProviderInfo.getVendorName(), // Vendor name
pProviderInfo.getVersion(), // Version
WMF_READER_AVAILABLE ? new String[]{"wmf", "WMF"} : new String[]{""}, // Names
WMF_READER_AVAILABLE ? new String[]{"wmf", "emf"} : null, // Suffixes
WMF_READER_AVAILABLE ? new String[]{"application/x-msmetafile", "image/x-wmf"} : null, // Mime-types
+2 -2
View File
@@ -5,12 +5,12 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>twelvemonkeys-imageio-core</artifactId>
<version>2.2</version>
<version>2.3</version>
<name>TwelveMonkeys ImageIO Core</name>
<parent>
<artifactId>twelvemonkeys-imageio</artifactId>
<groupId>com.twelvemonkeys</groupId>
<version>2.2</version>
<version>2.3</version>
</parent>
</project>
@@ -41,6 +41,7 @@ import java.awt.event.ActionEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage;
import java.awt.image.IndexColorModel;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
@@ -48,13 +49,14 @@ import java.util.Arrays;
import java.util.Iterator;
/**
* ImageReaderBase
* Abstract base class for image readers.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: ImageReaderBase.java,v 1.0 Sep 20, 2007 5:28:37 PM haraldk Exp$
*/
public abstract class ImageReaderBase extends ImageReader {
/**
* For convenience. Only set if the input is an {@code ImageInputStream}.
* @see #setInput(Object, boolean, boolean)
@@ -71,11 +73,10 @@ public abstract class ImageReaderBase extends ImageReader {
* the extension object is unsuitable, an
* {@code IllegalArgumentException} should be thrown.
*
* @param pOriginatingProvider the {@code ImageReaderSpi} that is
* invoking this constructor, or {@code null}.
* @param pProvider the {@code ImageReaderSpi} that is invoking this constructor, or {@code null}.
*/
protected ImageReaderBase(final ImageReaderSpi pOriginatingProvider) {
super(pOriginatingProvider);
protected ImageReaderBase(final ImageReaderSpi pProvider) {
super(pProvider);
}
/**
@@ -97,7 +98,7 @@ public abstract class ImageReaderBase extends ImageReader {
* @see ImageInputStream
*/
@Override
public void setInput(Object pInput, boolean pSeekForwardOnly, boolean pIgnoreMetadata) {
public void setInput(final Object pInput, final boolean pSeekForwardOnly, final boolean pIgnoreMetadata) {
resetMembers();
super.setInput(pInput, pSeekForwardOnly, pIgnoreMetadata);
if (pInput instanceof ImageInputStream) {
@@ -129,21 +130,21 @@ public abstract class ImageReaderBase extends ImageReader {
protected abstract void resetMembers();
/**
* Defaul implementation that always return {@code null}.
* Default implementation that always returns {@code null}.
*
* @param pImageIndex ignored, unless overriden
* @return {@code null}, unless overriden
* @throws IOException never, unless overriden.
* @param pImageIndex ignored, unless overridden
* @return {@code null}, unless overridden
* @throws IOException never, unless overridden.
*/
public IIOMetadata getImageMetadata(int pImageIndex) throws IOException {
return null;
}
/**
* Defaul implementation that always return {@code null}.
* Default implementation that always returns {@code null}.
*
* @return {@code null}, unless overriden
* @throws IOException never, unless overriden.
* @return {@code null}, unless overridden
* @throws IOException never, unless overridden.
*/
public IIOMetadata getStreamMetadata() throws IOException {
return null;
@@ -152,9 +153,9 @@ public abstract class ImageReaderBase extends ImageReader {
/**
* Default implementation that always returns {@code 1}.
*
* @param pAllowSearch ignored, unless overriden
* @return {@code 1}, unless overriden
* @throws IOException never, unless overriden
* @param pAllowSearch ignored, unless overridden
* @return {@code 1}, unless overridden
* @throws IOException never, unless overridden
*/
public int getNumImages(boolean pAllowSearch) throws IOException {
assertInput();
@@ -167,8 +168,7 @@ public abstract class ImageReaderBase extends ImageReader {
* @param pIndex the image index
*
* @throws java.io.IOException if an error occurs during reading
* @throws IndexOutOfBoundsException if not
* <tt>minIndex <= pIndex < numImages</tt>
* @throws IndexOutOfBoundsException if not {@code minIndex <= pIndex < numImages}
*/
protected void checkBounds(int pIndex) throws IOException {
assertInput();
@@ -216,10 +216,8 @@ public abstract class ImageReaderBase extends ImageReader {
* @throws IllegalArgumentException if {@code pTypes}
* is {@code null} or empty, or if an object not of type
* {@code ImageTypeSpecifier} is retrieved from it.
* Or, if the resulting image would
* have a width or height less than 1,
* or if the product of
* {@code pWidth} and {@code pHeight} is greater than
* Or, if the resulting image would have a width or height less than 1,
* or if the product of {@code pWidth} and {@code pHeight} is greater than
* {@code Integer.MAX_VALUE}.
*/
public static BufferedImage getDestination(final ImageReadParam pParam, final Iterator<ImageTypeSpecifier> pTypes,
@@ -343,12 +341,20 @@ public abstract class ImageReaderBase extends ImageReader {
private static class ImageLabel extends JLabel {
Paint mBackground;
public ImageLabel(BufferedImage pImage) {
final Paint mCheckeredBG;
final Color mDefaultBG;
public ImageLabel(final BufferedImage pImage) {
super(new BufferedImageIcon(pImage));
setOpaque(false);
setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));
mBackground = createTexture();
mCheckeredBG = createTexture();
// For indexed color, default to the color of the transparent pixel, if any
mDefaultBG = getDefaultBackground(pImage);
mBackground = mDefaultBG != null ? mDefaultBG : mCheckeredBG;
JPopupMenu popup = createBackgroundPopup();
@@ -366,7 +372,8 @@ public abstract class ImageReaderBase extends ImageReader {
private JPopupMenu createBackgroundPopup() {
JPopupMenu popup = new JPopupMenu();
ButtonGroup group = new ButtonGroup();
addCheckBoxItem(new ChangeBackgroundAction("Default", mBackground), popup, group);
addCheckBoxItem(new ChangeBackgroundAction("Checkered", mCheckeredBG), popup, group);
popup.addSeparator();
addCheckBoxItem(new ChangeBackgroundAction("White", Color.WHITE), popup, group);
addCheckBoxItem(new ChangeBackgroundAction("Light", Color.LIGHT_GRAY), popup, group);
@@ -374,7 +381,8 @@ public abstract class ImageReaderBase extends ImageReader {
addCheckBoxItem(new ChangeBackgroundAction("Dark", Color.DARK_GRAY), popup, group);
addCheckBoxItem(new ChangeBackgroundAction("Black", Color.BLACK), popup, group);
popup.addSeparator();
addCheckBoxItem(new ChooseBackgroundAction("Choose...", Color.BLUE), popup, group);
addCheckBoxItem(new ChooseBackgroundAction("Choose...", mDefaultBG != null ? mDefaultBG : Color.BLUE), popup, group);
return popup;
}
@@ -384,7 +392,19 @@ public abstract class ImageReaderBase extends ImageReader {
pPopup.add(item);
}
private Paint createTexture() {
private static Color getDefaultBackground(BufferedImage pImage) {
if (pImage.getColorModel() instanceof IndexColorModel) {
IndexColorModel cm = (IndexColorModel) pImage.getColorModel();
int transparent = cm.getTransparentPixel();
if (transparent >= 0) {
return new Color(cm.getRGB(transparent), false);
}
}
return null;
}
private static Paint createTexture() {
GraphicsConfiguration graphicsConfiguration = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration();
BufferedImage pattern = graphicsConfiguration.createCompatibleImage(20, 20);
Graphics2D g = pattern.createGraphics();
@@ -31,6 +31,7 @@ package com.twelvemonkeys.imageio;
import com.twelvemonkeys.imageio.util.IIOUtil;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.spi.ImageWriterSpi;
import javax.imageio.stream.ImageOutputStream;
@@ -39,13 +40,18 @@ import java.awt.image.BufferedImage;
import java.io.IOException;
/**
* ImageWriterBase
* Abstract base class for image writers.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: ImageWriterBase.java,v 1.0 Sep 24, 2007 12:22:28 AM haraldk Exp$
*/
public abstract class ImageWriterBase extends javax.imageio.ImageWriter {
public abstract class ImageWriterBase extends ImageWriter {
/**
* For convenience. Only set if the output is an {@code ImageInputStream}.
* @see #setOutput(Object)
*/
protected ImageOutputStream mImageOutput;
/**
@@ -59,8 +65,7 @@ public abstract class ImageWriterBase extends javax.imageio.ImageWriter {
* the extension object is unsuitable, an
* {@code IllegalArgumentException} should be thrown.
*
* @param pProvider the {@code ImageWriterSpi} that
* is constructing this object, or {@code null}.
* @param pProvider the {@code ImageWriterSpi} that is constructing this object, or {@code null}.
*/
protected ImageWriterBase(final ImageWriterSpi pProvider) {
super(pProvider);
@@ -71,8 +76,9 @@ public abstract class ImageWriterBase extends javax.imageio.ImageWriter {
}
@Override
public void setOutput(Object pOutput) {
public void setOutput(final Object pOutput) {
super.setOutput(pOutput);
if (pOutput instanceof ImageOutputStream) {
mImageOutput = (ImageOutputStream) pOutput;
}
@@ -92,10 +98,10 @@ public abstract class ImageWriterBase extends javax.imageio.ImageWriter {
/**
* Returns {@code null}
*
* @param pParam igonred.
* @param pParam ignored.
* @return {@code null}.
*/
public IIOMetadata getDefaultStreamMetadata(javax.imageio.ImageWriteParam pParam) {
public IIOMetadata getDefaultStreamMetadata(final ImageWriteParam pParam) {
return null;
}
@@ -103,20 +109,20 @@ public abstract class ImageWriterBase extends javax.imageio.ImageWriter {
* Returns {@code null}
*
* @param pInData ignored.
* @param pParam igonred.
* @param pParam ignored.
* @return {@code null}.
*/
public IIOMetadata convertStreamMetadata(IIOMetadata pInData, ImageWriteParam pParam) {
public IIOMetadata convertStreamMetadata(final IIOMetadata pInData, final ImageWriteParam pParam) {
return null;
}
protected static Rectangle getSourceRegion(ImageWriteParam pParam, int pWidth, int pHeight) {
protected static Rectangle getSourceRegion(final ImageWriteParam pParam, final int pWidth, final int pHeight) {
return IIOUtil.getSourceRegion(pParam, pWidth, pHeight);
}
/**
* Utility method for getting the area of interest (AOI) of an image.
* The AOI is defined by the {@link IIOParam#setSourceRegion(java.awt.Rectangle)}
* The AOI is defined by the {@link javax.imageio.IIOParam#setSourceRegion(java.awt.Rectangle)}
* method.
* <p/>
* Note: If it is possible for the reader to read the AOI directly, such a
@@ -129,14 +135,14 @@ public abstract class ImageWriterBase extends javax.imageio.ImageWriter {
* region), or the original image, if no source region was set, or
* {@code pParam} was {@code null}
*/
protected static BufferedImage fakeAOI(BufferedImage pImage, ImageWriteParam pParam) {
protected static BufferedImage fakeAOI(final BufferedImage pImage, final ImageWriteParam pParam) {
return IIOUtil.fakeAOI(pImage, getSourceRegion(pParam, pImage.getWidth(), pImage.getHeight()));
}
/**
* Utility method for getting the subsampled image.
* The subsampling is defined by the
* {@link IIOParam#setSourceSubsampling(int, int, int, int)}
* {@link javax.imageio.IIOParam#setSourceSubsampling(int, int, int, int)}
* method.
* <p/>
* NOTE: This method does not take the subsampling offsets into
@@ -152,7 +158,7 @@ public abstract class ImageWriterBase extends javax.imageio.ImageWriter {
* original image, if no subsampling was specified, or
* {@code pParam} was {@code null}
*/
protected static Image fakeSubsampling(Image pImage, ImageWriteParam pParam) {
protected static Image fakeSubsampling(final Image pImage, final ImageWriteParam pParam) {
return IIOUtil.fakeSubsampling(pImage, pParam);
}
}
@@ -0,0 +1,93 @@
package com.twelvemonkeys.imageio.spi;
import com.twelvemonkeys.lang.Validate;
/**
* Provides provider info, like vendor name and version,
* for {@link javax.imageio.spi.ImageReaderWriterSpi} subclasses based on information in the manifest.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: ProviderInfo.java,v 1.0 Oct 31, 2009 3:49:39 PM haraldk Exp$
*
* @see <a href="http://java.sun.com/j2se/1.5.0/docs/guide/jar/jar.html#JAR%20Manifest">JAR Manifest</a>
*/
public class ProviderInfo {
// TODO: Consider reading the META-INF/MANIFEST.MF from the class path using java.util.jar.Manifest.
// Use the manifest that is located in the same class path folder as the package.
private final String mTitle;
private final String mVendorName;
private final String mVersion;
/**
* Creates a provider information instance based on the given package.
*
* @param pPackage the package to get provider information from.
* This should typically be the package containing the Spi class.
*
* @throws IllegalArgumentException if {@code pPackage == null}
*/
public ProviderInfo(final Package pPackage) {
Validate.notNull(pPackage, "package");
String title = pPackage.getImplementationTitle();
mTitle = title != null ? title : pPackage.getName();
String vendor = pPackage.getImplementationVendor();
mVendorName = vendor != null ? vendor : fakeVendor(pPackage);
String version = pPackage.getImplementationVersion();
mVersion = version != null ? version : fakeVersion(pPackage);
}
private static String fakeVendor(final Package pPackage) {
String name = pPackage.getName();
return name.startsWith("com.twelvemonkeys") ? "TwelveMonkeys" : name;
}
private String fakeVersion(Package pPackage) {
String name = pPackage.getName();
return name.startsWith("com.twelvemonkeys") ? "DEV" : "Unspecified";
}
/**
* Returns the implementation title, as specified in the manifest entry
* {@code Implementation-Title} for the package.
* If the title is unavailable, the package name or some default name
* for known packages are used.
*
* @return the implementation title
*/
final String getImplementationTitle() {
return mTitle;
}
/**
* Returns the vendor name, as specified in the manifest entry
* {@code Implementation-Vendor} for the package.
* If the vendor name is unavailable, the package name or some default name
* for known packages are used.
*
* @return the vendor name.
*/
public final String getVendorName() {
return mVendorName;
}
/**
* Returns the version/build number string, as specified in the manifest entry
* {@code Implementation-Version} for the package.
* If the version is unavailable, some arbitrary (non-{@code null}) value is used.
*
* @return the vendor name.
*/
public final String getVersion() {
return mVersion;
}
@Override
public String toString() {
return mTitle + ", " + mVersion + " by " + mVendorName;
}
}
@@ -0,0 +1,4 @@
/**
* Provides helper classes for service provider implementations.
*/
package com.twelvemonkeys.imageio.spi;
@@ -154,7 +154,7 @@ public final class BufferedImageInputStream extends ImageInputStreamImpl impleme
@Override
public void close() throws IOException {
if (mStream != null) {
mStream.close();
//mStream.close();
mStream = null;
mBuffer = null;
}
@@ -172,14 +172,9 @@ public final class BufferedImageInputStream extends ImageInputStreamImpl impleme
try {
return mStream.length();
}
catch (IOException e) {
throw unchecked(e, RuntimeException.class);
catch (IOException ignore) {
}
}
@SuppressWarnings({"unchecked", "UnusedDeclaration"})
private <T extends Throwable> T unchecked(IOException pExcption, Class<T> pClass) {
// Ugly hack to fool the compiler..
return (T) pExcption;
return -1;
}
}
@@ -0,0 +1,102 @@
package com.twelvemonkeys.imageio.stream;
import com.twelvemonkeys.lang.Validate;
import javax.imageio.stream.ImageInputStream;
import javax.imageio.stream.ImageInputStreamImpl;
import java.io.IOException;
/**
* A wrapper for {@link ImageInputStream} to limit the number of bytes that can be read.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: SubImageInputStream.java,v 1.0 Nov 8, 2009 2:50:58 PM haraldk Exp$
*/
public final class SubImageInputStream extends ImageInputStreamImpl {
// NOTE: This class is based on com.sun.imageio.plugins.common.SubImageInputStream, but fixes some of its bugs.
private final ImageInputStream mStream;
private final long mStartPos;
private final long mLength;
/**
* Creates a {@link ImageInputStream}, reading up to a maximum number of bytes from the underlying stream.
*
* @param pStream the underlying stream
* @param pLength the maximum length to read from the stream.
* Note that {@code pStream} may contain less than this maximum number of bytes.
*
* @throws IOException if {@code pStream}'s position can't be determined.
* @throws IllegalArgumentException if {@code pStream == null} or {@code pLength < 0}
*/
public SubImageInputStream(final ImageInputStream pStream, final long pLength) throws IOException {
Validate.notNull(pStream, "stream");
if (pLength < 0) {
throw new IllegalArgumentException("length < 0");
}
mStream = pStream;
mStartPos = pStream.getStreamPosition();
mLength = pLength;
}
public int read() throws IOException {
if (streamPos >= mLength) { // Local EOF
return -1;
}
else {
int read = mStream.read();
if (read >= 0) {
streamPos++;
}
return read;
}
}
public int read(final byte[] pBytes, final int pOffset, final int pLength) throws IOException {
if (streamPos >= mLength) { // Local EOF
return -1;
}
// Safe cast, as pLength can never cause int overflow
int length = (int) Math.min(pLength, mLength - streamPos);
int count = mStream.read(pBytes, pOffset, length);
if (count >= 0) {
streamPos += count;
}
return count;
}
@Override
public long length() {
try {
long length = mStream.length();
return length < 0 ? -1 : Math.min(length - mStartPos, mLength);
}
catch (IOException ignore) {
}
return -1;
}
@Override
public void seek(final long pPosition) throws IOException {
if (pPosition < getFlushedPosition()) {
throw new IndexOutOfBoundsException("pos < flushedPosition");
}
mStream.seek(mStartPos + pPosition);
streamPos = pPosition;
}
@SuppressWarnings({"FinalizeDoesntCallSuperFinalize"})
@Override
protected void finalize() throws Throwable {
// Empty finalizer (for improved performance; no need to call super.finalize() in this case)
}
}
@@ -0,0 +1,4 @@
/**
* Provides various additional stream implementations.
*/
package com.twelvemonkeys.imageio.stream;
@@ -1,6 +1,7 @@
package com.twelvemonkeys.imageio.util;
import com.twelvemonkeys.image.ImageUtil;
import com.twelvemonkeys.imageio.spi.ProviderInfo;
import javax.imageio.IIOParam;
import javax.imageio.spi.IIOServiceProvider;
@@ -61,21 +62,6 @@ public final class IIOUtil {
return new BufferedOutputStream(new IIOOutputStreamAdapter(pStream));
}
/**
* THIS METHOD WILL ME MOVED/RENAMED, DO NOT USE.
*
* @param pRegistry the registry to unregister from
* @param pProvider the provider to unregister
* @param pCategory the category to unregister from
*
* @deprecated
*/
public static <T> void deregisterProvider(final ServiceRegistry pRegistry, final IIOServiceProvider pProvider, final Class<T> pCategory) {
// http://www.ibm.com/developerworks/java/library/j-jtp04298.html
// TODO: Consider placing this method in a ImageReaderSpiBase class or similar
pRegistry.deregisterServiceProvider(pCategory.cast(pProvider), pCategory);
}
public static Image fakeSubsampling(final Image pImage, final IIOParam pParam) {
if (pImage == null) {
return null;
@@ -134,4 +120,30 @@ public final class IIOUtil {
return pImage;
}
/**
* Creates a {@link ProviderInfo} instance for the given service provider.
*
* @param pProviderClass the provider class to get info for.
* @return the newly created {@link ProviderInfo}.
*/
public static ProviderInfo getProviderInfo(final Class<? extends IIOServiceProvider> pProviderClass) {
return new ProviderInfo(pProviderClass.getPackage());
}
/**
* THIS METHOD WILL ME MOVED/RENAMED, DO NOT USE.
*
* @param pRegistry the registry to unregister from
* @param pProvider the provider to unregister
* @param pCategory the category to unregister from
*
* @deprecated
*/
public static <T> void deregisterProvider(final ServiceRegistry pRegistry, final IIOServiceProvider pProvider, final Class<T> pCategory) {
// http://www.ibm.com/developerworks/java/library/j-jtp04298.html
// TODO: Consider placing this method in a ImageReaderSpiBase class or similar
pRegistry.deregisterServiceProvider(pCategory.cast(pProvider), pCategory);
}
}
@@ -0,0 +1,4 @@
/**
* Provides various common utilities for reading and writing images.
*/
package com.twelvemonkeys.imageio.util;
@@ -0,0 +1,97 @@
package com.twelvemonkeys.imageio.spi;
import junit.framework.TestCase;
import java.net.URL;
/**
* ProviderInfoTestCase
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: ProviderInfoTestCase.java,v 1.0 Oct 31, 2009 3:51:22 PM haraldk Exp$
*/
public class ProviderInfoTestCase extends TestCase {
public void testCreateNorma() {
new ProviderInfo(Package.getPackage("java.util"));
}
public void testCreateNullPackage() {
try {
new ProviderInfo(null);
fail("IllegalArgumentException expected for null package");
}
catch (IllegalArgumentException expected) {
assertTrue(expected.getMessage().toLowerCase().contains("package"));
}
}
public void testGetVendorUnknownNonJARPackage() {
ProviderInfo info = new ProviderInfo(mockNonJARPackage("org.foo"));
String vendor = info.getVendorName();
assertNotNull(vendor);
assertEquals("org.foo", vendor);
String version = info.getVersion();
assertNotNull(version);
assertEquals("Unspecified", version);
}
public void testGetVendorNonJARTMPackage() {
ProviderInfo info = new ProviderInfo(mockNonJARPackage("com.twelvemonkeys"));
String vendor = info.getVendorName();
assertNotNull(vendor);
assertEquals("TwelveMonkeys", vendor);
String version = info.getVersion();
assertNotNull(version);
assertEquals("DEV", version);
}
public void testGetVendorKnownJARPackage() {
ProviderInfo info = new ProviderInfo(mockJARPackage("com.acme", "1.7u4-BETA-b39", "Acme"));
String vendor = info.getVendorName();
assertNotNull(vendor);
assertEquals("Acme", vendor);
String version = info.getVersion();
assertNotNull(version);
assertEquals("1.7u4-BETA-b39", version);
}
private Package mockNonJARPackage(final String pName) {
return new MockClassLoader().mockPackage(
pName,
null, null, null,
null, null, null,
null
);
}
private Package mockJARPackage(final String pName, final String pImplVersion, final String pImplVendor) {
return new MockClassLoader().mockPackage(
pName,
"The almighty specification", "1.0", "Acme Inc",
"The buggy implementation", pImplVersion, pImplVendor,
null
);
}
private static class MockClassLoader extends ClassLoader {
protected MockClassLoader() {
super(null);
}
public Package mockPackage(String name, String specTitle, String specVersion, String specVendor, String implTitle, String implVersion, String implVendor, URL sealBase) throws IllegalArgumentException {
return definePackage(name, specTitle, specVersion, specVendor, implTitle, implVersion, implVendor, sealBase);
}
@Override
protected Package getPackage(String name) {
return null; // Allow re-createing packages
}
}
}
@@ -0,0 +1,168 @@
package com.twelvemonkeys.imageio.stream;
import junit.framework.TestCase;
import javax.imageio.stream.ImageInputStream;
import javax.imageio.stream.ImageInputStreamImpl;
import javax.imageio.stream.MemoryCacheImageInputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.Random;
/**
* SubImageInputStreamTestCase
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: SubImageInputStreamTestCase.java,v 1.0 Nov 8, 2009 3:03:32 PM haraldk Exp$
*/
public class SubImageInputStreamTestCase extends TestCase {
// TODO: Extract super test case for all stream tests
private final Random mRandom = new Random(837468l);
private ImageInputStream createStream(final int pSize) {
byte[] bytes = new byte[pSize];
mRandom.nextBytes(bytes);
return new MemoryCacheImageInputStream(new ByteArrayInputStream(bytes)) {
@Override
public long length() {
return pSize;
}
};
}
public void testCreateNullStream() throws IOException {
try {
new SubImageInputStream(null, 1);
fail("Expected IllegalArgumentException with null stream");
}
catch (IllegalArgumentException e) {
}
}
public void testCreateNegativeLength() throws IOException {
try {
new SubImageInputStream(createStream(0), -1);
fail("Expected IllegalArgumentException with negative length");
}
catch (IllegalArgumentException e) {
}
}
public void testCreate() throws IOException {
ImageInputStream stream = new SubImageInputStream(createStream(11), 7);
assertEquals(0, stream.getStreamPosition());
assertEquals(7, stream.length());
}
public void testWraphBeyondWrappedLength() throws IOException {
SubImageInputStream stream = new SubImageInputStream(createStream(5), 6);
assertEquals(5, stream.length());
}
public void testWrapUnknownLength() throws IOException {
SubImageInputStream stream = new SubImageInputStream(new ImageInputStreamImpl() {
@Override
public int read() throws IOException {
throw new UnsupportedOperationException("Method read not implemented");
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
throw new UnsupportedOperationException("Method read not implemented");
}
@Override
public long length() {
return -1;
}
}, 6);
assertEquals(-1, stream.length());
}
public void testRead() throws IOException {
ImageInputStream wrapped = createStream(42);
wrapped.skipBytes(13);
ImageInputStream stream = new SubImageInputStream(wrapped, 27);
assertEquals(0, stream.getStreamPosition());
assertEquals(27, stream.length());
stream.read();
assertEquals(1, stream.getStreamPosition());
assertEquals(27, stream.length());
stream.readFully(new byte[11]);
assertEquals(12, stream.getStreamPosition());
assertEquals(27, stream.length());
assertEquals(25, wrapped.getStreamPosition());
}
public void testReadResetRead() throws IOException {
ImageInputStream stream = new SubImageInputStream(createStream(32), 16);
stream.mark();
byte[] first = new byte[16];
stream.readFully(first);
stream.reset();
byte[] second = new byte[16];
stream.readFully(second);
assertTrue(Arrays.equals(first, second));
}
public void testSeekNegative() throws IOException {
ImageInputStream stream = new SubImageInputStream(createStream(7), 5);
try {
stream.seek(-2);
fail("Expected IndexOutOfBoundsException");
}
catch (IndexOutOfBoundsException expected) {
}
assertEquals(0, stream.getStreamPosition());
}
public void testSeekBeforeFlushedPos() throws IOException {
ImageInputStream stream = new SubImageInputStream(createStream(7), 5);
stream.seek(3);
stream.flushBefore(3);
assertEquals(3, stream.getStreamPosition());
try {
stream.seek(0);
fail("Expected IndexOutOfBoundsException");
}
catch (IndexOutOfBoundsException expected) {
}
assertEquals(3, stream.getStreamPosition());
}
public void testSeekAfterEOF() throws IOException {
ImageInputStream stream = new SubImageInputStream(createStream(7), 5);
stream.seek(6);
assertEquals(-1, stream.read());
}
public void testSeek() throws IOException {
ImageInputStream stream = new SubImageInputStream(createStream(7), 5);
stream.seek(5);
assertEquals(5, stream.getStreamPosition());
stream.seek(1);
assertEquals(1, stream.getStreamPosition());
}
}
@@ -36,6 +36,7 @@ import org.jmock.core.Stub;
import javax.imageio.*;
import javax.imageio.event.IIOReadProgressListener;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.spi.IIORegistry;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream;
@@ -1320,7 +1321,9 @@ public abstract class ImageReaderAbstractTestCase<T extends ImageReader> extends
assertEquals(type.getColorModel(), result.getColorModel());
// assertEquals(type.getSampleModel(), result.getSampleModel());
// The following logically tests
// assertEquals(type.getSampleModel(), result.getSampleModel());
// but SampleModel does not have a proper equals method.
SampleModel expectedModel = type.getSampleModel();
SampleModel resultModel = result.getSampleModel();
@@ -1335,10 +1338,6 @@ public abstract class ImageReaderAbstractTestCase<T extends ImageReader> extends
}
}
// public void testSetDestinationTypeIllegal() throws IOException {
// throw new UnsupportedOperationException("Method testSetDestinationTypeIllegal not implemented"); // TODO: Implement
// }
//
// public void testSetDestinationBands() throws IOException {
// throw new UnsupportedOperationException("Method testSetDestinationBands not implemented"); // TODO: Implement
// }
@@ -1347,6 +1346,24 @@ public abstract class ImageReaderAbstractTestCase<T extends ImageReader> extends
// throw new UnsupportedOperationException("Method testSetDestinationBands not implemented"); // TODO: Implement
// }
public void testProviderAndMetadataFormatNamesMatch() throws IOException {
ImageReaderSpi provider = createProvider();
ImageReader reader = createReader();
reader.setInput(getTestData().get(0).getInputStream());
IIOMetadata imageMetadata = reader.getImageMetadata(0);
if (imageMetadata != null) {
assertEquals(provider.getNativeImageMetadataFormatName(), imageMetadata.getNativeMetadataFormatName());
}
IIOMetadata streamMetadata = reader.getStreamMetadata();
if (streamMetadata != null) {
assertEquals(provider.getNativeStreamMetadataFormatName(), streamMetadata.getNativeMetadataFormatName());
}
}
protected URL getClassLoaderResource(final String pName) {
return getClass().getResource(pName);
}
+2 -2
View File
@@ -5,14 +5,14 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>twelvemonkeys-imageio-ico</artifactId>
<version>2.2</version>
<version>2.3</version>
<name>TwelveMonkeys ImageIO ICO plugin</name>
<description>ImageIO plugin for Windows Icon (ICO) and Cursor (CUR) format.</description>
<parent>
<artifactId>twelvemonkeys-imageio</artifactId>
<groupId>com.twelvemonkeys</groupId>
<version>2.2</version>
<version>2.3</version>
</parent>
<dependencies>
@@ -53,10 +53,10 @@ public class CURImageReader extends ICOImageReader {
}
/**
* Returns the hotspot location for the cursor.
* Returns the hot spot location for the cursor.
*
* @param pImageIndex the index of the cursor in the current input.
* @return the hotspot location for the cursor
* @return the hot spot location for the cursor
*
* @throws IOException if an I/O exception occurs during reading of image meta data
* @throws IndexOutOfBoundsException if {@code pImageIndex} is less than {@code 0} or greater than/equal to
@@ -28,6 +28,9 @@
package com.twelvemonkeys.imageio.plugins.ico;
import com.twelvemonkeys.imageio.spi.ProviderInfo;
import com.twelvemonkeys.imageio.util.IIOUtil;
import javax.imageio.ImageReader;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream;
@@ -43,9 +46,13 @@ import java.util.Locale;
public class CURImageReaderSpi extends ImageReaderSpi {
public CURImageReaderSpi() {
this(IIOUtil.getProviderInfo(CURImageReaderSpi.class));
}
private CURImageReaderSpi(final ProviderInfo pProviderInfo) {
super(
"TwelveMonkeys",
"2.1",
pProviderInfo.getVendorName(),
pProviderInfo.getVersion(),
new String[]{"cur", "CUR"},
new String[]{"cur"},
new String[]{
@@ -53,7 +53,7 @@ import java.util.List;
/**
* ImageReader for Microsoft Windows ICO (icon) format.
* 1, 4, 8 bit palette support with bitmask transparency, and 16, 24 and 32 bit
* true color support with alpha. Also supports Windows Vista PNG ecoded icons.
* true color support with alpha. Also supports Windows Vista PNG encoded icons.
* <p/>
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
@@ -28,6 +28,9 @@
package com.twelvemonkeys.imageio.plugins.ico;
import com.twelvemonkeys.imageio.spi.ProviderInfo;
import com.twelvemonkeys.imageio.util.IIOUtil;
import javax.imageio.ImageReader;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream;
@@ -43,9 +46,13 @@ import java.util.Locale;
public class ICOImageReaderSpi extends ImageReaderSpi {
public ICOImageReaderSpi() {
this(IIOUtil.getProviderInfo(ICOImageReaderSpi.class));
}
private ICOImageReaderSpi(final ProviderInfo pProviderInfo) {
super(
"TwelveMonkeys",
"2.1",
pProviderInfo.getVendorName(),
pProviderInfo.getVersion(),
new String[]{"ico", "ICO"},
new String[]{"ico"},
new String[]{
+1
View File
@@ -1 +1,2 @@
- Support all DIB formats?
- Use Sun's BMP metadata format for image metadata + separate stream metadata?
+2 -2
View File
@@ -5,7 +5,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>twelvemonkeys-imageio-iff</artifactId>
<version>2.2</version>
<version>2.3</version>
<name>TwelveMonkeys ImageIO IFF plugin</name>
<description>
ImageIO plugin for Amiga/Electronic Arts Interchange Filed Format (IFF)
@@ -15,7 +15,7 @@
<parent>
<artifactId>twelvemonkeys-imageio</artifactId>
<groupId>com.twelvemonkeys</groupId>
<version>2.2</version>
<version>2.3</version>
</parent>
<dependencies>
@@ -42,6 +42,7 @@ import java.io.IOException;
*/
class CAMGChunk extends IFFChunk {
// HIRES=0x8000, LACE=0x4
// #define CAMG_HAM 0x800 /* hold and modify */
// #define CAMG_EHB 0x80 /* extra halfbrite */
@@ -86,7 +86,7 @@ class CMAPChunk extends IFFChunk {
if (numColors == 32) {
paletteSize = 64;
}
else {
else if (numColors != 64) {
throw new IIOException("Unknown number of colors for EHB: " + numColors);
}
}
@@ -100,7 +100,9 @@ class CMAPChunk extends IFFChunk {
mGreens[i] = pInput.readByte();
mBlues[i] = pInput.readByte();
}
if (isEHB) {
if (isEHB && numColors == 32) {
// Create the half-brite colors
for (int i = 0; i < numColors; i++) {
mReds[i + numColors] = (byte) ((mReds[i] & 0xff) / 2);
mGreens[i + numColors] = (byte) ((mGreens[i] & 0xff) / 2);
@@ -91,6 +91,10 @@ import java.util.List;
* @see <a href="http://en.wikipedia.org/wiki/ILBM">Wikipedia: IFF ILBM</a>
*/
public class IFFImageReader extends ImageReaderBase {
// http://home.comcast.net/~erniew/lwsdk/docs/filefmts/ilbm.html
// http://www.fileformat.info/format/iff/spec/7866a9f0e53c42309af667c5da3bd426/view.htm
// - Contains definitions of some "new" chunks, as well as alternative FORM types
// http://amigan.1emu.net/reg/iff.html
private BMHDChunk mHeader;
private CMAPChunk mColorMap;
@@ -154,7 +158,7 @@ public class IFFImageReader extends ImageReaderBase {
remaining -= 8;
remaining -= length % 2 == 0 ? length : length + 1;
//System.out.println("Next chunk: " + toChunkStr(chunkId) + " lenght: " + length);
//System.out.println("Next chunk: " + toChunkStr(chunkId) + " length: " + length);
//System.out.println("Remaining bytes after chunk: " + remaining);
switch (chunkId) {
@@ -203,8 +207,7 @@ public class IFFImageReader extends ImageReaderBase {
mBody = new BODYChunk(length);
mBodyStart = mImageInput.getStreamPosition();
// NOTE: We don't read the body here, it's done later in the
// read(int, ImageReadParam) method
// NOTE: We don't read the body here, it's done later in the read(int, ImageReadParam) method
// Done reading meta
return;
@@ -232,6 +235,7 @@ public class IFFImageReader extends ImageReaderBase {
readBody(pParam);
}
else {
// TODO: Remove this hack when we have metadata
// In the rare case of an ILBM containing nothing but a CMAP
//System.out.println(mColorMap);
if (mColorMap != null) {
@@ -244,7 +248,6 @@ public class IFFImageReader extends ImageReaderBase {
processImageComplete();
return result;
}
@@ -294,8 +297,8 @@ public class IFFImageReader extends ImageReaderBase {
// 8 bit
// May be HAM8
if (!isHAM()) {
IndexColorModel cm = mColorMap.getIndexColorModel();
if (cm != null) {
if (mColorMap != null) {
IndexColorModel cm = mColorMap.getIndexColorModel();
specifier = IndexedImageTypeSpecifier.createFromIndexColorModel(cm);
break;
}
@@ -304,6 +307,7 @@ public class IFFImageReader extends ImageReaderBase {
break;
}
}
// NOTE: HAM modes falls through, as they are converted to RGB
case 24:
// 24 bit RGB
specifier = ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_3BYTE_BGR);
@@ -313,13 +317,13 @@ public class IFFImageReader extends ImageReaderBase {
specifier = ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_4BYTE_ABGR);
break;
default:
throw new IIOException("Bit depth not implemented: " + mHeader.mBitplanes);
throw new IIOException(String.format("Bit depth not implemented: %d", mHeader.mBitplanes));
}
return specifier;
}
private void readBody(ImageReadParam pParam) throws IOException {
mImageInput.seek(mBodyStart); // 8 for the header before length in stream
private void readBody(final ImageReadParam pParam) throws IOException {
mImageInput.seek(mBodyStart);
mByteRunStream = null;
// NOTE: mColorMap may be null for 8 bit (gray), 24 bit or 32 bit only
@@ -333,7 +337,7 @@ public class IFFImageReader extends ImageReaderBase {
}
private void readIndexed(ImageReadParam pParam, final ImageInputStream pInput, final IndexColorModel pModel) throws IOException {
private void readIndexed(final ImageReadParam pParam, final ImageInputStream pInput, final IndexColorModel pModel) throws IOException {
final int width = mHeader.mWidth;
final int height = mHeader.mHeight;
@@ -363,7 +367,9 @@ public class IFFImageReader extends ImageReaderBase {
destination = destination.createWritableChild(0, 0, destination.getWidth(), destination.getHeight(), offset.x, offset.y, destinationBands);
}
int planeWidth = (width + 7) / 8;
// NOTE: Each row of the image is stored in an integral number of 16 bit words.
// The number of words per row is words=((w+15)/16)
int planeWidth = 2 * ((width + 15) / 16);
final byte[] planeData = new byte[8 * planeWidth];
ColorModel cm;
@@ -373,10 +379,14 @@ public class IFFImageReader extends ImageReaderBase {
// TODO: If HAM6, use type USHORT_444_RGB or 2BYTE_444_RGB?
// Or create a HAMColorModel, if at all possible?
// TYPE_3BYTE_BGR
cm = new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_sRGB), new int[]{8, 8, 8},
false, false, Transparency.OPAQUE, DataBuffer.TYPE_BYTE);
cm = new ComponentColorModel(
ColorSpace.getInstance(ColorSpace.CS_sRGB), new int[]{8, 8, 8},
false, false, Transparency.OPAQUE, DataBuffer.TYPE_BYTE
);
// Create a byte raster with BGR order
raster = Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, width, 1, width * 3, 3, new int[]{2, 1, 0}, null);
raster = Raster.createInterleavedRaster(
DataBuffer.TYPE_BYTE, width, 1, width * 3, 3, new int[]{2, 1, 0}, null
);
}
else {
// TYPE_BYTE_BINARY or TYPE_BYTE_INDEXED
@@ -400,14 +410,8 @@ public class IFFImageReader extends ImageReaderBase {
ColorConvertOp converter = null;
for (int srcY = 0; srcY < height; srcY++) {
for (int p = 0; p < planes; p++) {
try {
readPlaneData(pInput, planeData, p * planeWidth, planeWidth);
}
catch (IOException e) {
// TODO: Add warning? Probbably a bug somewhere, should not catch!
}
readPlaneData(pInput, planeData, p * planeWidth, planeWidth);
}
// Skip rows outside AOI
@@ -420,11 +424,9 @@ public class IFFImageReader extends ImageReaderBase {
if (mFormType == IFF.TYPE_ILBM) {
int pixelPos = 0;
int planePos = 0;
for (int i = 0; i < planeWidth; i++) {
for (int planePos = 0; planePos < planeWidth; planePos++) {
IFFUtil.bitRotateCW(planeData, planePos, planeWidth, row, pixelPos, 1);
pixelPos += 8;
planePos++;
}
if (isHAM()) {
@@ -434,11 +436,14 @@ public class IFFImageReader extends ImageReaderBase {
raster.setDataElements(0, 0, width, 1, row);
}
}
else /*if (mType == IFFImageReader.TYPE_PBM)*/ {
// TODO: Arraycopy might not be neccessary, if it's okay with row larger than width
else if (mFormType == IFF.TYPE_PBM) {
// TODO: Arraycopy might not be necessary, if it's okay with row larger than width
System.arraycopy(planeData, 0, row, 0, mHeader.mBitplanes * planeWidth);
raster.setDataElements(0, 0, width, 1, row);
}
else {
throw new AssertionError(String.format("Unsupported FORM type: %s", mFormType));
}
int dstY = (srcY - aoi.y) / sourceYSubsampling;
// Handle non-converting raster as special case for performance
@@ -521,15 +526,17 @@ public class IFFImageReader extends ImageReaderBase {
// Ensure band settings from param are compatible with images
checkReadParamBandSettings(pParam, mHeader.mBitplanes / 8, mImage.getSampleModel().getNumBands());
int planeWidth = (width + 7) / 8;
// NOTE: Each row of the image is stored in an integral number of 16 bit words.
// The number of words per row is words=((w+15)/16)
int planeWidth = 2 * ((width + 15) / 16);
final byte[] planeData = new byte[8 * planeWidth];
WritableRaster destination = mImage.getRaster();
if (destinationBands != null || offset.x != 0 || offset.y != 0) {
destination = destination.createWritableChild(0, 0, destination.getWidth(), destination.getHeight(), offset.x, offset.y, destinationBands);
}
WritableRaster raster = mImage.getRaster().createCompatibleWritableRaster(width, 1);
// WritableRaster raster = mImage.getRaster().createCompatibleWritableRaster(width, 1);
WritableRaster raster = mImage.getRaster().createCompatibleWritableRaster(8 * planeWidth, 1);
Raster sourceRow = raster.createChild(aoi.x, 0, aoi.width, 1, 0, 0, sourceBands);
final byte[] data = ((DataBufferByte) raster.getDataBuffer()).getData();
@@ -558,16 +565,17 @@ public class IFFImageReader extends ImageReaderBase {
int off = (channels - c - 1);
int pixelPos = 0;
int planePos = 0;
for (int i = 0; i < planeWidth; i++) {
for (int planePos = 0; planePos < planeWidth; planePos++) {
IFFUtil.bitRotateCW(planeData, planePos, planeWidth, data, off + pixelPos * channels, channels);
pixelPos += 8;
planePos++;
}
}
else /*if (mType == IFFImageReader.TYPE_PBM)*/ {
else if (mFormType == IFF.TYPE_PBM) {
System.arraycopy(planeData, 0, data, srcY * 8 * planeWidth, planeWidth);
}
else {
throw new AssertionError(String.format("Unsupported FORM type: %s", mFormType));
}
}
int dstY = (srcY - aoi.y) / sourceYSubsampling;
@@ -607,16 +615,25 @@ public class IFFImageReader extends ImageReaderBase {
break;
case BMHDChunk.COMPRESSION_BYTE_RUN:
// TODO: How do we know if the last byte in the body is a pad byte or not?!
// The body consists of byte-run (PackBits) compressed rows of bit plane data.
// However, we don't know how long each compressed row is, without decoding it...
// The workaround below, is to use a decode buffer size of pPlaneWidth,
// to make sure we don't decode anything we don't have to (shouldn't).
if (mByteRunStream == null) {
mByteRunStream = new DataInputStream(new DecoderStream(
IIOUtil.createStreamAdapter(pInput, mBody.mChunkLength),
new PackBitsDecoder(true)
));
mByteRunStream = new DataInputStream(
new DecoderStream(
IIOUtil.createStreamAdapter(pInput, mBody.mChunkLength),
new PackBitsDecoder(true),
pPlaneWidth * mHeader.mBitplanes
)
);
}
mByteRunStream.readFully(pData, pOffset, pPlaneWidth);
break;
default:
throw new IIOException("Unknown compression type: " + mHeader.mCompressionType);
throw new IIOException(String.format("Unknown compression type: %d", mHeader.mCompressionType));
}
}
@@ -666,7 +683,7 @@ public class IFFImageReader extends ImageReaderBase {
}
public static void main(String[] pArgs) throws IOException {
ImageReader reader = new IFFImageReader(new IFFImageReaderSpi());
ImageReader reader = new IFFImageReader();
// ImageInputStream input = ImageIO.createImageInputStream(new File(pArgs[0]));
ImageInputStream input = new BufferedImageInputStream(ImageIO.createImageInputStream(new File(pArgs[0])));
@@ -687,7 +704,7 @@ public class IFFImageReader extends ImageReaderBase {
BufferedImage image = reader.read(0, param);
System.out.println("image = " + image);
showIt(image, "");
showIt(image, pArgs[0]);
}
}
}
@@ -28,6 +28,9 @@
package com.twelvemonkeys.imageio.plugins.iff;
import com.twelvemonkeys.imageio.spi.ProviderInfo;
import com.twelvemonkeys.imageio.util.IIOUtil;
import javax.imageio.ImageReader;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream;
@@ -46,12 +49,16 @@ public class IFFImageReaderSpi extends ImageReaderSpi {
static IFFImageReaderSpi mSharedInstance;
/**
* Creates an IFFImageReaderSpi
* Creates an {@code IFFImageReaderSpi}.
*/
public IFFImageReaderSpi() {
this(IIOUtil.getProviderInfo(IFFImageReaderSpi.class));
}
private IFFImageReaderSpi(final ProviderInfo pProviderInfo) {
super(
"TwelveMonkeys",
"2.0",
pProviderInfo.getVendorName(),
pProviderInfo.getVersion(),
new String[]{"iff", "IFF"},
new String[]{"iff", "lbm", "ham", "ham8", "ilbm"},
new String[]{"image/iff", "image/x-iff"},
@@ -47,7 +47,7 @@ import java.io.OutputStream;
/**
* Writer for Amiga (Electronic Arts) IFF ILBM (InterLeaved BitMap) format.
* The IFF format (Interchange File Format) is the standard file format
* supported by allmost all image software for the Amiga computer.
* supported by almost all image software for the Amiga computer.
* <p/>
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
@@ -58,8 +58,6 @@ import java.io.OutputStream;
*/
public class IFFImageWriter extends ImageWriterBase {
private static final byte[] ANNO_DATA = "Written by TwelveMonkeys IFFImageWriter 1.0 for Java (javax.imageio).".getBytes();
public IFFImageWriter() {
this(null);
}
@@ -121,6 +119,7 @@ public class IFFImageWriter extends ImageWriterBase {
private void packImageData(OutputStream pOutput, RenderedImage pImage, ImageWriteParam pParam) throws IOException {
// TODO: Allow param to dictate uncompressed
// TODO: Allow param to dictate type PBM?
// TODO: Subsample/AOI
final boolean compress = shouldCompress(pImage);
final OutputStream output = compress ? new EncoderStream(pOutput, new PackBitsEncoder(), true) : pOutput;
@@ -136,12 +135,14 @@ public class IFFImageWriter extends ImageWriterBase {
// 2. Perform byteRun1 compression for each plane separately
// 3. Write the plane data for each plane
final int planeWidth = (width + 7) / 8;
//final int planeWidth = (width + 7) / 8;
final int planeWidth = 2 * ((width + 15) / 16);
final byte[] planeData = new byte[8 * planeWidth];
final int channels = (model.getPixelSize() + 7) / 8;
final int planesPerChannel = channels == 1 ? model.getPixelSize() : 8;
int[] pixels = new int[8 * planeWidth];
// TODO: The spec says "Do not compress across rows!".. I think we currently do.
// NOTE: I'm a little unsure if this is correct for 4 channel (RGBA)
// data, but it is at least consistent with the IFFImageReader for now...
for (int y = 0; y < height; y++) {
@@ -174,7 +175,8 @@ public class IFFImageWriter extends ImageWriterBase {
private void writeMeta(RenderedImage pImage, int pBodyLength) throws IOException {
// Annotation ANNO chunk, 8 + annoData.length bytes
GenericChunk anno = new GenericChunk(IFFUtil.toInt("ANNO".getBytes()), ANNO_DATA);
String annotation = "Written by " + getOriginatingProvider().getDescription(null) + " by " + getOriginatingProvider().getVendorName();
GenericChunk anno = new GenericChunk(IFFUtil.toInt("ANNO".getBytes()), annotation.getBytes());
ColorModel cm = pImage.getColorModel();
IndexColorModel icm = null;
@@ -28,6 +28,9 @@
package com.twelvemonkeys.imageio.plugins.iff;
import com.twelvemonkeys.imageio.spi.ProviderInfo;
import com.twelvemonkeys.imageio.util.IIOUtil;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.ImageWriter;
import javax.imageio.spi.ImageWriterSpi;
@@ -44,12 +47,17 @@ import java.util.Locale;
public class IFFImageWriterSpi extends ImageWriterSpi {
/**
* Creates an IFFImageWriterSpi
* Creates an {@code IFFImageWriterSpi}.
*/
public IFFImageWriterSpi() {
this(IIOUtil.getProviderInfo(IFFImageWriterSpi.class));
}
private IFFImageWriterSpi(final ProviderInfo pProviderInfo) {
super(
"TwelveMonkeys",
"$Revision: 1.0 $",
pProviderInfo.getVendorName(),
pProviderInfo.getVersion(),
new String[]{"iff", "IFF"},
new String[]{"iff", "lbm", "ham", "ham8", "ilbm"},
new String[]{"image/iff", "image/x-iff"},
@@ -61,8 +69,9 @@ public class IFFImageWriterSpi extends ImageWriterSpi {
);
}
public boolean canEncodeImage(ImageTypeSpecifier pType) {
public boolean canEncodeImage(final ImageTypeSpecifier pType) {
// TODO: Probably can't store 16 bit types etc...
// TODO: Can't store CMYK (well.. it does, but they can't be read back)
return true;
}
@@ -48,7 +48,7 @@ class IFFUtil {
/**
* Creates a rotation table
* @param n
* @param n number of bits -1
*
* @return the rotation table
*/
@@ -57,7 +57,8 @@ class IFFUtil {
0x00000000l << n, 0x00000001l << n, 0x00000100l << n, 0x00000101l << n,
0x00010000l << n, 0x00010001l << n, 0x00010100l << n, 0x00010101l << n,
0x01000000l << n, 0x01000001l << n, 0x01000100l << n, 0x01000101l << n,
0x01010000l << n, 0x01010001l << n, 0x01010100l << n, 0x01010101l << n};
0x01010000l << n, 0x01010001l << n, 0x01010100l << n, 0x01010101l << n
};
}
static private final long[][] RTABLE = {
@@ -243,7 +244,7 @@ class IFFUtil {
/**
* Converts an int to a four letter String.
*
* @param pChunkId
* @param pChunkId the chunk identifier
* @return a String
*/
static String toChunkStr(int pChunkId) {
@@ -52,13 +52,13 @@ public class IFFImageReaderTestCase extends ImageReaderAbstractTestCase<IFFImage
new TestData(getClassLoaderResource("/iff/survivor.iff"), new Dimension(800, 600)), // 24 bit
// HAM6 - Ok (a lot of visual "fringe", would be interesting to see on a real HAM display)
new TestData(getClassLoaderResource("/iff/A4000T_HAM6.IFF"), new Dimension(320, 512)), // ham6
// HAM8 - Passes tests, but visuals are trashed. Have other HAM8 files that are ok
// HAM8 - Ok (PackBits decoder chokes on padding byte)
new TestData(getClassLoaderResource("/iff/A4000T_HAM8.IFF"), new Dimension(628, 512)), // ham8
// 8 color indexed - Passes tests, but trashed. Must be something special with these images
// 8 color indexed - Ok
new TestData(getClassLoaderResource("/iff/AmigaBig.iff"), new Dimension(300, 200)), // 8 color
// 8 color indexed - Ok
new TestData(getClassLoaderResource("/iff/AmigaAmiga.iff"), new Dimension(200, 150)), // 8 color
// Breaks completely... Could be bug in the packbits decoder?
// Ok (PackBits decoder chokes on padding byte)
new TestData(getClassLoaderResource("/iff/Abyss.iff"), new Dimension(320, 400))
);
}
+31
View File
@@ -0,0 +1,31 @@
<?xml version="1.0"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>twelvemonkeys-imageio-metadata</artifactId>
<version>2.3</version>
<name>TwelveMonkeys ImageIO Metadata</name>
<description>
ImageIO metadata module.
</description>
<parent>
<artifactId>twelvemonkeys-imageio</artifactId>
<groupId>com.twelvemonkeys</groupId>
<version>2.3</version>
</parent>
<dependencies>
<dependency>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>twelvemonkeys-imageio-core</artifactId>
</dependency>
<dependency>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>twelvemonkeys-imageio-core</artifactId>
<classifier>tests</classifier>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,144 @@
/*
* Copyright (c) 2009, 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.metadata;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
/**
* AbstractDirectory
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: AbstractDirectory.java,v 1.0 Nov 11, 2009 5:31:04 PM haraldk Exp$
*/
public abstract class AbstractDirectory implements Directory {
private final List<Entry> mEntries = new ArrayList<Entry>();
protected AbstractDirectory(final Collection<? extends Entry> pEntries) {
if (pEntries != null) {
mEntries.addAll(pEntries);
}
}
public Entry getEntryById(final Object pIdentifier) {
for (Entry entry : this) {
if (entry.getIdentifier().equals(pIdentifier)) {
return entry;
}
}
return null;
}
public Entry getEntryByFieldName(final String pFieldName) {
for (Entry entry : this) {
if (entry.getFieldName() != null && entry.getFieldName().equals(pFieldName)) {
return entry;
}
}
return null;
}
public Iterator<Entry> iterator() {
return mEntries.iterator();
}
/**
* Throws {@code UnsupportedOperationException} if this directory is read-only.
*
* @throws UnsupportedOperationException if this directory is read-only.
* @see #isReadOnly()
*/
protected final void assertMutable() {
if (isReadOnly()) {
throw new UnsupportedOperationException("Directory is read-only");
}
}
public boolean add(final Entry pEntry) {
assertMutable();
// TODO: Replace if entry is already present?
// Some directories may need special ordering, or may/may not support multiple entries for certain ids...
return mEntries.add(pEntry);
}
@SuppressWarnings({"SuspiciousMethodCalls"})
public boolean remove(final Object pEntry) {
assertMutable();
return mEntries.remove(pEntry);
}
public int size() {
return mEntries.size();
}
/**
* This implementation returns {@code true}.
* Subclasses should override this method, if the directory is mutable.
*
* @return {@code true}
*/
public boolean isReadOnly() {
return true;
}
/// Standard object support
@Override
public int hashCode() {
return mEntries.hashCode();
}
@Override
public boolean equals(final Object pOther) {
if (this == pOther) {
return true;
}
if (getClass() != pOther.getClass()) {
return false;
}
// Safe cast, as it must be a subclass for the classes to be equal
AbstractDirectory other = (AbstractDirectory) pOther;
return mEntries.equals(other.mEntries);
}
@Override
public String toString() {
return String.format("%s%s", getClass().getSimpleName(), mEntries.toString());
}
}
@@ -0,0 +1,127 @@
/*
* Copyright (c) 2009, 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.metadata;
import com.twelvemonkeys.lang.Validate;
import java.lang.reflect.Array;
/**
* AbstractEntry
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: AbstractEntry.java,v 1.0 Nov 12, 2009 12:43:13 AM haraldk Exp$
*/
public abstract class AbstractEntry implements Entry {
private final Object mIdentifier;
private final Object mValue; // TODO: Might need to be mutable..
protected AbstractEntry(final Object pIdentifier, final Object pValue) {
Validate.notNull(pIdentifier, "identifier");
mIdentifier = pIdentifier;
mValue = pValue;
}
public final Object getIdentifier() {
return mIdentifier;
}
/**
* Returns {@code null}, meaning unknown or undefined.
*
* @return {@code null}.
*/
public String getFieldName() {
return null;
}
public Object getValue() {
return mValue;
}
public String getValueAsString() {
return String.valueOf(mValue);
}
public String getTypeName() {
if (mValue == null) {
return null;
}
return mValue.getClass().getSimpleName();
}
public int valueCount() {
// TODO: Collection support?
if (mValue != null && mValue.getClass().isArray()) {
return Array.getLength(mValue);
}
return 1;
}
/// Object
@Override
public int hashCode() {
return mIdentifier.hashCode() + 31 * mValue.hashCode();
}
@Override
public boolean equals(final Object pOther) {
if (this == pOther) {
return true;
}
if (!(pOther instanceof AbstractEntry)) {
return false;
}
AbstractEntry other = (AbstractEntry) pOther;
return mIdentifier.equals(other.mIdentifier) && (
mValue == null && other.mValue == null || mValue != null && mValue.equals(other.mValue)
);
}
@Override
public String toString() {
String name = getFieldName();
String nameStr = name != null ? "/" + name + "" : "";
String type = getTypeName();
String typeStr = type != null ? " (" + type + ")" : "";
return String.format("%s%s: %s%s", getIdentifier(), nameStr, getValueAsString(), typeStr);
}
}
@@ -0,0 +1,61 @@
/*
* Copyright (c) 2009, 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.metadata;
/**
* Directory
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: Directory.java,v 1.0 Nov 11, 2009 4:20:58 PM haraldk Exp$
*/
public interface Directory extends Iterable<Entry> {
// TODO: Spec when more entries exist? Or make Entry support multi-values!?
// For multiple entries with same id in directory, the first entry (using the order from the stream) will be returned
Entry getEntryById(Object pIdentifier);
Entry getEntryByFieldName(String pName);
// Iterator containing the entries in
//Iterator<Entry> getBestEntries(Object pIdentifier, Object pQualifier, String pLanguage);
/// Collection-like API
// TODO: addOrReplaceIfPresent... (trouble for multi-values) Or mutable entries?
// boolean replace(Entry pEntry)??
// boolean contains(Object pIdentifier)?
boolean add(Entry pEntry);
boolean remove(Object pEntry); // Object in case we retro-fit Collection/Map..
int size();
boolean isReadOnly();
}
@@ -0,0 +1,67 @@
/*
* Copyright (c) 2009, 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.metadata;
/**
* Entry
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: Entry.java,v 1.0 Nov 11, 2009 4:21:08 PM haraldk Exp$
*/
public interface Entry {
// "tag" identifier from spec
Object getIdentifier();
// Human readable "tag" (field) name from sepc
String getFieldName();
// The internal "tag" value as stored in the stream, may be a Directory
Object getValue();
// Human readable "tag" value
String getValueAsString();
//void setValue(Object pValue); // TODO: qualifiers...
// Optional, implementation/spec specific type, describing the object returned from getValue
String getTypeName();
// TODO: Or something like getValue(qualifierType, qualifierValue) + getQualifiers()/getQualifierValues
// TODO: The problem with current model is getEntry() which only has single value support
// Optional, xml:lang-support
//String getLanguage();
// Optional, XMP alt-support. TODO: Do we need both?
//Object getQualifier();
// For arrays only
int valueCount();
}
@@ -0,0 +1,43 @@
/*
* Copyright (c) 2009, 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.metadata;
import javax.imageio.stream.ImageInputStream;
import java.io.IOException;
/**
* MetadataReader
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: MetadataReader.java,v 1.0 Nov 13, 2009 8:38:11 PM haraldk Exp$
*/
public abstract class MetadataReader {
public abstract Directory read(ImageInputStream pInput) throws IOException;
}
@@ -0,0 +1,42 @@
/*
* Copyright (c) 2009, 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.metadata.exif;
/**
* EXIF
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: EXIF.java,v 1.0 Nov 11, 2009 5:36:04 PM haraldk Exp$
*/
public interface EXIF {
int TAG_COLOR_SPACE = 40961;
int TAG_PIXEL_X_DIMENSION = 40962;
int TAG_PIXEL_Y_DIMENSION = 40963;
}
@@ -0,0 +1,47 @@
/*
* Copyright (c) 2009, 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.metadata.exif;
import com.twelvemonkeys.imageio.metadata.AbstractDirectory;
import com.twelvemonkeys.imageio.metadata.Entry;
import java.util.Collection;
/**
* EXIFDirectory
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: EXIFDirectory.java,v 1.0 Nov 11, 2009 5:02:59 PM haraldk Exp$
*/
final class EXIFDirectory extends AbstractDirectory {
EXIFDirectory(final Collection<? extends Entry> pEntries) {
super(pEntries);
}
}
@@ -0,0 +1,95 @@
/*
* Copyright (c) 2009, 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.metadata.exif;
import com.twelvemonkeys.imageio.metadata.AbstractEntry;
/**
* EXIFEntry
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: EXIFEntry.java,v 1.0 Nov 13, 2009 5:47:35 PM haraldk Exp$
*/
final class EXIFEntry extends AbstractEntry {
final private short mType;
EXIFEntry(final int pIdentifier, final Object pValue, final short pType) {
super(pIdentifier, pValue);
if (pType < 1 || pType > TIFF.TYPE_NAMES.length) {
throw new IllegalArgumentException(String.format("Illegal EXIF type: %s", pType));
}
mType = pType;
}
@Override
public String getFieldName() {
switch ((Integer) getIdentifier()) {
case TIFF.TAG_COMPRESSION:
return "Compression";
case TIFF.TAG_ORIENTATION:
return "Orientation";
case TIFF.TAG_X_RESOLUTION:
return "XResolution";
case TIFF.TAG_Y_RESOLUTION:
return "YResolution";
case TIFF.TAG_RESOLUTION_UNIT:
return "ResolutionUnit";
case TIFF.TAG_JPEG_INTERCHANGE_FORMAT:
return "JPEGInterchangeFormat";
case TIFF.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH:
return "JPEGInterchangeFormatLength";
case TIFF.TAG_SOFTWARE:
return "Software";
case TIFF.TAG_DATE_TIME:
return "DateTime";
case TIFF.TAG_ARTIST:
return "Artist";
case TIFF.TAG_COPYRIGHT:
return "Copyright";
case EXIF.TAG_COLOR_SPACE:
return "ColorSpace";
case EXIF.TAG_PIXEL_X_DIMENSION:
return "PixelXDimension";
case EXIF.TAG_PIXEL_Y_DIMENSION:
return "PixelYDimension";
// TODO: More field names
}
return null;
}
@Override
public String getTypeName() {
return TIFF.TYPE_NAMES[mType - 1];
}
}
@@ -0,0 +1,242 @@
/*
* Copyright (c) 2009, 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.metadata.exif;
import com.twelvemonkeys.imageio.metadata.Directory;
import com.twelvemonkeys.imageio.metadata.Entry;
import com.twelvemonkeys.imageio.metadata.MetadataReader;
import com.twelvemonkeys.lang.StringUtil;
import javax.imageio.IIOException;
import javax.imageio.stream.ImageInputStream;
import java.io.IOException;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.List;
/**
* EXIFReader
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: EXIFReader.java,v 1.0 Nov 13, 2009 5:42:51 PM haraldk Exp$
*/
public final class EXIFReader extends MetadataReader {
@Override
public Directory read(final ImageInputStream pInput) throws IOException {
byte[] bom = new byte[2];
pInput.readFully(bom);
if (bom[0] == 'I' && bom[1] == 'I') {
pInput.setByteOrder(ByteOrder.LITTLE_ENDIAN);
}
else if (!(bom[0] == 'M' && bom[1] == 'M')) {
throw new IIOException(String.format("Invalid TIFF byte order mark '%s', expected: 'II' or 'MM'", StringUtil.decode(bom, 0, bom.length, "ASCII")));
}
int magic = pInput.readUnsignedShort();
if (magic != TIFF.TIFF_MAGIC) {
throw new IIOException(String.format("Wrong TIFF magic in EXIF data: %04x, expected: %04x", magic, TIFF.TIFF_MAGIC));
}
long directoryOffset = pInput.readUnsignedInt();
return readDirectory(pInput, directoryOffset);
}
private EXIFDirectory readDirectory(final ImageInputStream pInput, final long pOffset) throws IOException {
List<Entry> entries = new ArrayList<Entry>();
pInput.seek(pOffset);
int entryCount = pInput.readUnsignedShort();
for (int i = 0; i < entryCount; i++) {
entries.add(readEntry(pInput));
}
long nextOffset = pInput.readUnsignedInt();
if (nextOffset != 0) {
EXIFDirectory next = readDirectory(pInput, nextOffset);
for (Entry entry : next) {
entries.add(entry);
}
}
return new EXIFDirectory(entries);
}
private EXIFEntry readEntry(final ImageInputStream pInput) throws IOException {
int tagId = pInput.readUnsignedShort();
short type = pInput.readShort();
int count = pInput.readInt(); // Number of values
Object value;
if (tagId == TIFF.IFD_EXIF || tagId == TIFF.IFD_GPS || tagId == TIFF.IFD_INTEROP) {
// Parse sub IFDs
long offset = pInput.readUnsignedInt();
pInput.mark();
try {
value = readDirectory(pInput, offset);
}
finally {
pInput.reset();
}
}
else {
int valueLength = getValueLength(type, count);
if (valueLength > 0 && valueLength <= 4) {
value = readValueInLine(pInput, type, count);
pInput.skipBytes(4 - valueLength);
}
else {
long valueOffset = pInput.readUnsignedInt(); // This is the *value* iff the value size is <= 4 bytes
value = readValue(pInput, valueOffset, type, count);
}
}
return new EXIFEntry(tagId, value, type);
}
private Object readValue(final ImageInputStream pInput, final long pOffset, final short pType, final int pCount) throws IOException {
long pos = pInput.getStreamPosition();
try {
pInput.seek(pOffset);
return readValueInLine(pInput, pType, pCount);
}
finally {
pInput.seek(pos);
}
}
private Object readValueInLine(final ImageInputStream pInput, final short pType, final int pCount) throws IOException {
return readValueDirect(pInput, pType, pCount);
}
private static Object readValueDirect(final ImageInputStream pInput, final short pType, final int pCount) throws IOException {
switch (pType) {
case 2:
// TODO: This might be UTF-8 or ISO-8859-1, even though spec says ASCII
byte[] ascii = new byte[pCount];
pInput.readFully(ascii);
return StringUtil.decode(ascii, 0, ascii.length, "UTF-8"); // UTF-8 is ASCII compatible
case 1:
if (pCount == 1) {
return pInput.readUnsignedByte();
}
case 6:
if (pCount == 1) {
return pInput.readByte();
}
case 7:
byte[] bytes = new byte[pCount];
pInput.readFully(bytes);
return bytes;
case 3:
if (pCount == 1) {
return pInput.readUnsignedShort();
}
case 8:
if (pCount == 1) {
return pInput.readShort();
}
short[] shorts = new short[pCount];
pInput.readFully(shorts, 0, shorts.length);
return shorts;
case 4:
if (pCount == 1) {
return pInput.readUnsignedInt();
}
case 9:
if (pCount == 1) {
return pInput.readInt();
}
int[] ints = new int[pCount];
pInput.readFully(ints, 0, ints.length);
return ints;
case 11:
if (pCount == 1) {
return pInput.readFloat();
}
float[] floats = new float[pCount];
pInput.readFully(floats, 0, floats.length);
return floats;
case 12:
if (pCount == 1) {
return pInput.readDouble();
}
double[] doubles = new double[pCount];
pInput.readFully(doubles, 0, doubles.length);
return doubles;
case 5:
if (pCount == 1) {
return new Rational(pInput.readUnsignedInt(), pInput.readUnsignedInt());
}
Rational[] rationals = new Rational[pCount];
for (int i = 0; i < rationals.length; i++) {
rationals[i] = new Rational(pInput.readUnsignedInt(), pInput.readUnsignedInt());
}
return rationals;
case 10:
if (pCount == 1) {
return new Rational(pInput.readInt(), pInput.readInt());
}
Rational[] srationals = new Rational[pCount];
for (int i = 0; i < srationals.length; i++) {
srationals[i] = new Rational(pInput.readInt(), pInput.readInt());
}
return srationals;
default:
throw new IIOException(String.format("Unknown EXIF type '%s'", pType));
}
}
private int getValueLength(final int pType, final int pCount) {
if (pType > 0 && pType <= TIFF.TYPE_LENGTHS.length) {
return TIFF.TYPE_LENGTHS[pType - 1] * pCount;
}
return -1;
}
}
@@ -0,0 +1,214 @@
/*
* Copyright (c) 2009, 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.
*/
/*
* Adapted from sample code featured in
* "Intro to Programming in Java: An Interdisciplinary Approach" (Addison Wesley)
* by Robert Sedgewick and Kevin Wayne. Permission granted to redistribute under BSD license.
*/
package com.twelvemonkeys.imageio.metadata.exif;
/**
* Represents a rational number with a {@code long} numerator and {@code long} denominator.
* Rational numbers are stored in reduced form with the sign stored with the numerator.
* Rationals are immutable.
* <p/>
* Adapted from sample code featured in
* <a href="http://www.cs.princeton.edu/introcs/home/">"Intro to Programming in Java: An Interdisciplinary Approach" (Addison Wesley)</a>
* by Robert Sedgewick and Kevin Wayne. Permission granted to redistribute under BSD license.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author <a href="http://www.cs.princeton.edu/introcs/92symbolic/Rational.java.html">Robert Sedgewick and Kevin Wayne (original version)</a>
* @author last modified by $Author: haraldk$
* @version $Id: Rational.java,v 1.0 Nov 18, 2009 1:12:00 AM haraldk Exp$
*/
public final class Rational extends Number implements Comparable<Rational> {
// TODO: Document public API
// TODO: Move to com.tm.lang?
// Inspired by http://www.cs.princeton.edu/introcs/92symbolic/Rational.java.html and java.lang.Integer
static final Rational ZERO = new Rational(0, 1);
private final long mNumerator;
private final long mDenominator;
public Rational(final long pNumber) {
this(pNumber, 1);
}
public Rational(final long pNumerator, final long pDenominator) {
if (pDenominator == 0) {
throw new IllegalArgumentException("denominator == 0");
}
if (pNumerator == Long.MIN_VALUE || pDenominator == Long.MIN_VALUE) {
throw new IllegalArgumentException("value == Long.MIN_VALUE");
}
// Reduce fractions
long gcd = gcd(pNumerator, pDenominator);
long num = pNumerator / gcd;
long den = pDenominator / gcd;
mNumerator = pDenominator >= 0 ? num : -num;
mDenominator = pDenominator >= 0 ? den : -den;
}
private static long gcd(final long m, final long n) {
if (m < 0) {
return gcd(n, -m);
}
return n == 0 ? m : gcd(n, m % n);
}
private static long lcm(final long m, final long n) {
if (m < 0) {
return lcm(n, -m);
}
return m * (n / gcd(m, n)); // parentheses important to avoid overflow
}
public long numerator() {
return mNumerator;
}
public long denominator() {
return mDenominator;
}
/// Number implementation
@Override
public int intValue() {
return (int) doubleValue();
}
@Override
public long longValue() {
return (long) doubleValue();
}
@Override
public float floatValue() {
return (float) doubleValue();
}
@Override
public double doubleValue() {
return mNumerator / (double) mDenominator;
}
/// Comparable implementation
public int compareTo(final Rational pOther) {
double thisVal = doubleValue();
double otherVal = pOther.doubleValue();
return thisVal < otherVal ? -1 : thisVal == otherVal ? 0 : 1;
}
/// Object overrides
@Override
public int hashCode() {
return Float.floatToIntBits(floatValue());
}
@Override
public boolean equals(final Object pOther) {
return pOther == this || pOther instanceof Rational && compareTo((Rational) pOther) == 0;
}
@Override
public String toString() {
return mDenominator == 1 ? Long.toString(mNumerator) : String.format("%s/%s", mNumerator, mDenominator);
}
/// Operations (adapted from http://www.cs.princeton.edu/introcs/92symbolic/Rational.java.html)
// TODO: Naming! multiply/divide/add/subtract or times/divides/plus/minus
// return a * b, staving off overflow as much as possible by cross-cancellation
public Rational times(final Rational pOther) {
// special cases
if (equals(ZERO) || pOther.equals(ZERO)) {
return ZERO;
}
// reduce p1/q2 and p2/q1, then multiply, where a = p1/q1 and b = p2/q2
Rational c = new Rational(mNumerator, pOther.mDenominator);
Rational d = new Rational(pOther.mNumerator, mDenominator);
return new Rational(c.mNumerator * d.mNumerator, c.mDenominator * d.mDenominator);
}
// return a + b, staving off overflow
public Rational plus(final Rational pOther) {
// special cases
if (equals(ZERO)) {
return pOther;
}
if (pOther.equals(ZERO)) {
return this;
}
// Find gcd of numerators and denominators
long f = gcd(mNumerator, pOther.mNumerator);
long g = gcd(mDenominator, pOther.mDenominator);
// add cross-product terms for numerator
// multiply back in
return new Rational(
((mNumerator / f) * (pOther.mDenominator / g) + (pOther.mNumerator / f) * (mDenominator / g)) * f,
lcm(mDenominator, pOther.mDenominator)
);
}
// return -a
public Rational negate() {
return new Rational(-mNumerator, mDenominator);
}
// return a - b
public Rational minus(final Rational pOther) {
return plus(pOther.negate());
}
public Rational reciprocal() {
return new Rational(mDenominator, mNumerator);
}
// return a / b
public Rational divides(final Rational pOther) {
if (pOther.equals(ZERO)) {
throw new ArithmeticException("/ by zero");
}
return times(pOther.reciprocal());
}
}
@@ -0,0 +1,117 @@
/*
* Copyright (c) 2009, 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.metadata.exif;
/**
* TIFF
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: TIFF.java,v 1.0 Nov 15, 2009 3:02:24 PM haraldk Exp$
*/
public interface TIFF {
int TIFF_MAGIC = 42;
/*
1 = BYTE 8-bit unsigned integer.
2 = ASCII 8-bit byte that contains a 7-bit ASCII code; the last byte
must be NUL (binary zero).
3 = SHORT 16-bit (2-byte) unsigned integer.
4 = LONG 32-bit (4-byte) unsigned integer.
5 = RATIONAL Two LONGs: the first represents the numerator of a
fraction; the second, the denominator.
TIFF 6.0 and above:
6 = SBYTE An 8-bit signed (twos-complement) integer.
7 = UNDEFINED An 8-bit byte that may contain anything, depending on
the definition of the field.
8 = SSHORT A 16-bit (2-byte) signed (twos-complement) integer.
9 = SLONG A 32-bit (4-byte) signed (twos-complement) integer.
10 = SRATIONAL Two SLONGs: the first represents the numerator of a
fraction, the second the denominator.
11 = FLOAT Single precision (4-byte) IEEE format.
12 = DOUBLE Double precision (8-byte) IEEE format.
*/
String[] TYPE_NAMES = {
"BYTE", "ASCII", "SHORT", "LONG", "RATIONAL",
"SBYTE", "UNDEFINED", "SSHORT", "SLONG", "SRATIONAL", "FLOAT", "DOUBLE",
};
int[] TYPE_LENGTHS = {
1, 1, 2, 4, 8,
1, 1, 2, 4, 8, 4, 8,
};
int IFD_EXIF = 0x8769;
int IFD_GPS = 0x8825;
int IFD_INTEROP = 0xA005;
/// A. Tags relating to image data structure:
int TAG_IMAGE_WIDTH = 256;
int TAG_IMAGE_HEIGHT = 257;
int TAG_BITS_PER_SAMPLE = 258;
int TAG_COMPRESSION = 259;
int TAG_PHOTOMETRIC_INTERPRETATION = 262;
int TAG_ORIENTATION = 274;
int TAG_SAMPLES_PER_PIXELS = 277;
int TAG_PLANAR_CONFIGURATION = 284;
int TAG_YCBCR_SUB_SAMPLING = 530;
int TAG_YCBCR_POSITIONING = 531;
int TAG_X_RESOLUTION = 282;
int TAG_Y_RESOLUTION = 283;
int TAG_RESOLUTION_UNIT = 296;
/// B. Tags relating to recording offset
int TAG_STRIP_OFFSETS = 273;
int TAG_ROWS_PER_STRIP = 278;
int TAG_STRIP_BYTE_COUNTS = 279;
int TAG_JPEG_INTERCHANGE_FORMAT = 513;
int TAG_JPEG_INTERCHANGE_FORMAT_LENGTH = 514;
/// C. Tags relating to image data characteristics
int TAG_TRANSFER_FUNCTION = 301;
int TAG_WHITE_POINT = 318;
int TAG_PRIMARY_CHROMATICITIES = 319;
int TAG_YCBCR_COEFFICIENTS = 529;
int TAG_REFERENCE_BLACK_WHITE = 532;
/// D. Other tags
int TAG_DATE_TIME = 306;
int TAG_IMAGE_DESCRIPTION = 270;
int TAG_MAKE = 271;
int TAG_MODEL = 272;
int TAG_SOFTWARE = 305;
int TAG_ARTIST = 315;
int TAG_COPYRIGHT = 33432;
}
@@ -0,0 +1,158 @@
/*
* Copyright (c) 2009, 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.metadata.iptc;
/**
* IPTC
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: IPTC.java,v 1.0 Nov 11, 2009 6:20:21 PM haraldk Exp$
*/
public interface IPTC {
static final int ENVELOPE_RECORD = 1 << 8;
static final int APPLICATION_RECORD = 2 << 8;
static final int TAG_CODED_CHARACTER_SET = ENVELOPE_RECORD | 90;
/** 2:00 Record Version (mandatory) */
public static final int TAG_RECORD_VERSION = APPLICATION_RECORD; // 0x0200
/** 2:03 Object Type Reference */
public static final int TAG_OBJECT_TYPE_REFERENCE = APPLICATION_RECORD | 3;
/** 2:04 Object Attribute Reference (repeatable) */
public static final int TAG_OBJECT_ATTRIBUTE_REFERENCE = APPLICATION_RECORD | 4;
/** 2:05 Object Name */
public static final int TAG_OBJECT_NAME = APPLICATION_RECORD | 5; // 0x0205
/** 2:07 Edit Status */
public static final int TAG_EDIT_STATUS = APPLICATION_RECORD | 7;
/** 2:08 Editorial Update */
public static final int TAG_EDITORIAL_UPDATE = APPLICATION_RECORD | 8;
/** 2:10 Urgency */
public static final int TAG_URGENCY = APPLICATION_RECORD | 10;
/** 2:12 Subect Reference (repeatable) */
public static final int TAG_SUBJECT_REFERENCE = APPLICATION_RECORD | 12;
/** 2:15 Category */
public static final int TAG_CATEGORY = APPLICATION_RECORD | 15; // 0x020f
/** 2:20 Supplemental Category (repeatable) */
public static final int TAG_SUPPLEMENTAL_CATEGORIES = APPLICATION_RECORD | 20;
/** 2:22 Fixture Identifier */
public static final int TAG_FIXTURE_IDENTIFIER = APPLICATION_RECORD | 22;
/** 2:25 Keywords (repeatable) */
public static final int TAG_KEYWORDS = APPLICATION_RECORD | 25;
/** 2:26 Content Locataion Code (repeatable) */
public static final int TAG_CONTENT_LOCATION_CODE = APPLICATION_RECORD | 26;
/** 2:27 Content Locataion Name (repeatable) */
public static final int TAG_CONTENT_LOCATION_NAME = APPLICATION_RECORD | 27;
/** 2:30 Release Date */
public static final int TAG_RELEASE_DATE = APPLICATION_RECORD | 30;
/** 2:35 Release Time */
public static final int TAG_RELEASE_TIME = APPLICATION_RECORD | 35;
/** 2:37 Expiration Date */
public static final int TAG_EXPIRATION_DATE = APPLICATION_RECORD | 37;
/** 2:38 Expiration Time */
public static final int TAG_EXPIRATION_TIME = APPLICATION_RECORD | 38;
/** 2:40 Special Instructions */
public static final int TAG_SPECIAL_INSTRUCTIONS = APPLICATION_RECORD | 40; // 0x0228
/** 2:42 Action Advised (1: Kill, 2: Replace, 3: Append, 4: Reference) */
public static final int TAG_ACTION_ADVICED = APPLICATION_RECORD | 42;
/** 2:45 Reference Service (repeatable in triplets with 2:47 and 2:50) */
public static final int TAG_REFERENCE_SERVICE = APPLICATION_RECORD | 45;
/** 2:47 Reference Date (mandatory if 2:45 present) */
public static final int TAG_REFERENCE_DATE = APPLICATION_RECORD | 47;
/** 2:50 Reference Number (mandatory if 2:45 present) */
public static final int TAG_REFERENCE_NUMBER = APPLICATION_RECORD | 50;
/** 2:55 Date Created */
public static final int TAG_DATE_CREATED = APPLICATION_RECORD | 55; // 0x0237
/** 2:60 Time Created */
public static final int TAG_TIME_CREATED = APPLICATION_RECORD | 60;
/** 2:62 Digital Creation Date */
public static final int TAG_DIGITAL_CREATION_DATE = APPLICATION_RECORD | 62;
/** 2:63 Digital Creation Date */
public static final int TAG_DIGITAL_CREATION_TIME = APPLICATION_RECORD | 63;
/** 2:65 Originating Program */
public static final int TAG_ORIGINATING_PROGRAM = APPLICATION_RECORD | 65;
/** 2:70 Program Version (only valid if 2:65 present) */
public static final int TAG_PROGRAM_VERSION = APPLICATION_RECORD | 70;
/** 2:75 Object Cycle (a: morning, p: evening, b: both) */
public static final int TAG_OBJECT_CYCLE = APPLICATION_RECORD | 75;
/** 2:80 By-line (repeatable) */
public static final int TAG_BY_LINE = APPLICATION_RECORD | 80; // 0x0250
/** 2:85 By-line Title (repeatable) */
public static final int TAG_BY_LINE_TITLE = APPLICATION_RECORD | 85; // 0x0255
/** 2:90 City */
public static final int TAG_CITY = APPLICATION_RECORD | 90; // 0x025a
/** 2:92 Sub-location */
public static final int TAG_SUB_LOCATION = APPLICATION_RECORD | 92;
/** 2:95 Province/State */
public static final int TAG_PROVINCE_OR_STATE = APPLICATION_RECORD | 95; // 0x025f
/** 2:100 Country/Primary Location Code */
public static final int TAG_COUNTRY_OR_PRIMARY_LOCATION_CODE = APPLICATION_RECORD | 100;
/** 2:101 Country/Primary Location Name */
public static final int TAG_COUNTRY_OR_PRIMARY_LOCATION = APPLICATION_RECORD | 101; // 0x0265
/** 2:103 Original Transmission Reference */
public static final int TAG_ORIGINAL_TRANSMISSION_REFERENCE = APPLICATION_RECORD | 103; // 0x0267
/** 2:105 Headline */
public static final int TAG_HEADLINE = APPLICATION_RECORD | 105; // 0x0269
/** 2:110 Credit */
public static final int TAG_CREDIT = APPLICATION_RECORD | 110; // 0x026e
/** 2:115 Source */
public static final int TAG_SOURCE = APPLICATION_RECORD | 115; // 0x0273
/** 2:116 Copyright Notice */
public static final int TAG_COPYRIGHT_NOTICE = APPLICATION_RECORD | 116; // 0x0274
/** 2:118 Contact */
public static final int TAG_CONTACT = APPLICATION_RECORD | 118;
/** 2:120 Catption/Abstract */
public static final int TAG_CAPTION = APPLICATION_RECORD | 120; // 0x0278
/** 2:122 Writer/Editor (repeatable) */
public static final int TAG_WRITER = APPLICATION_RECORD | 122; // 0x027a
/** 2:125 Rasterized Caption (binary data) */
public static final int TAG_RASTERIZED_CATPTION = APPLICATION_RECORD | 125;
/** 2:130 Image Type */
public static final int TAG_IMAGE_TYPE = APPLICATION_RECORD | 130;
/** 2:131 Image Orientation */
public static final int TAG_IMAGE_ORIENTATION = APPLICATION_RECORD | 131;
/** 2:135 Language Identifier */
public static final int TAG_LANGUAGE_IDENTIFIER = APPLICATION_RECORD | 135;
// TODO: 2:150-2:154 Audio
// TODO: Should we expose this field?
/**
* 2:199 JobMinder Assignment Data (Custom IPTC field).
* A common custom IPTC field used by a now discontinued application called JobMinder.
*
* @see <a href="http://www.jobminder.net/">JobMinder Homepage</a>
*/
static final int CUSTOM_TAG_JOBMINDER_ASSIGNMENT_DATA = APPLICATION_RECORD | 199;
// TODO: Other custom fields in 155-200 range?
// TODO: 2:200-2:202 Object Preview Data
}
@@ -0,0 +1,47 @@
/*
* Copyright (c) 2009, 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.metadata.iptc;
import com.twelvemonkeys.imageio.metadata.AbstractDirectory;
import com.twelvemonkeys.imageio.metadata.Entry;
import java.util.Collection;
/**
* IPTCDirectory
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: IPTCDirectory.java,v 1.0 Nov 11, 2009 5:02:59 PM haraldk Exp$
*/
final class IPTCDirectory extends AbstractDirectory {
IPTCDirectory(final Collection<? extends Entry> pEntries) {
super(pEntries);
}
}
@@ -0,0 +1,55 @@
/*
* Copyright (c) 2009, 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.metadata.iptc;
import com.twelvemonkeys.imageio.metadata.AbstractEntry;
/**
* IPTCEntry
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: IPTCEntry.java,v 1.0 Nov 13, 2009 8:57:04 PM haraldk Exp$
*/
class IPTCEntry extends AbstractEntry {
public IPTCEntry(final int pTagId, final Object pValue) {
super(pTagId, pValue);
}
@Override
public String getFieldName() {
switch ((Integer) getIdentifier()) {
case IPTC.TAG_SOURCE:
return "Source";
// TODO: More tags...
}
return null;
}
}
@@ -0,0 +1,153 @@
/*
* Copyright (c) 2009, 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.metadata.iptc;
import com.twelvemonkeys.imageio.metadata.Directory;
import com.twelvemonkeys.imageio.metadata.Entry;
import com.twelvemonkeys.imageio.metadata.MetadataReader;
import com.twelvemonkeys.lang.StringUtil;
import javax.imageio.IIOException;
import javax.imageio.stream.ImageInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CodingErrorAction;
import java.util.ArrayList;
import java.util.List;
/**
* IPTCReader
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: IPTCReader.java,v 1.0 Nov 13, 2009 8:37:23 PM haraldk Exp$
*/
public final class IPTCReader extends MetadataReader {
private static final int ENCODING_UNKNOWN = -1;
private static final int ENCODING_UNSPECIFIED = 0;
private static final int ENCODING_UTF_8 = 0x1b2547;
private int mEncoding = ENCODING_UNSPECIFIED;
@Override
public Directory read(final ImageInputStream pInput) throws IOException {
final List<Entry> entries = new ArrayList<Entry>();
// 0x1c identifies start of a tag
while (pInput.read() == 0x1c) {
short tagId = pInput.readShort();
int tagByteCount = pInput.readUnsignedShort();
Entry entry = readEntry(pInput, tagId, tagByteCount);
if (entry != null) {
entries.add(entry);
}
}
return new IPTCDirectory(entries);
}
private IPTCEntry readEntry(final ImageInputStream pInput, final short pTagId, final int pLength) throws IOException {
Object value = null;
switch (pTagId) {
case IPTC.TAG_CODED_CHARACTER_SET:
// TODO: Mapping from ISO 646 to Java supported character sets?
// TODO: Move somewhere else?
mEncoding = parseEncoding(pInput, pLength);
return null;
case IPTC.TAG_RECORD_VERSION:
// A single unsigned short value
value = pInput.readUnsignedShort();
break;
default:
// Skip non-Application fields, as they are typically not human readable
if ((pTagId & 0xff00) != IPTC.APPLICATION_RECORD) {
pInput.skipBytes(pLength);
return null;
}
// fall through
}
// If we don't have a value, treat it as a string
if (value == null) {
if (pLength < 1) {
value = null;
}
else {
value = parseString(pInput, pLength);
}
}
return new IPTCEntry(pTagId, value);
}
private int parseEncoding(final ImageInputStream pInput, int tagByteCount) throws IOException {
return tagByteCount == 3
&& (pInput.readUnsignedByte() << 16 | pInput.readUnsignedByte() << 8 | pInput.readUnsignedByte()) == ENCODING_UTF_8
? ENCODING_UTF_8 : ENCODING_UNKNOWN;
}
// TODO: Pass encoding as parameter? Use if specified
private String parseString(final ImageInputStream pInput, final int pLength) throws IOException {
byte[] data = new byte[pLength];
pInput.readFully(data);
// NOTE: The IPTC specification says character data should use ISO 646 or ISO 2022 encoding.
// UTF-8 contains all 646 characters, but not 2022.
// This is however close to what libiptcdata does, see: http://libiptcdata.sourceforge.net/docs/iptc-i18n.html
Charset charset = Charset.forName("UTF-8");
CharsetDecoder decoder = charset.newDecoder();
try {
// First try to decode using UTF-8 (which seems to be the de-facto standard)
// Will fail fast on illegal UTF-8-sequences
CharBuffer chars = decoder.onMalformedInput(CodingErrorAction.REPORT)
.onUnmappableCharacter(CodingErrorAction.REPORT)
.decode(ByteBuffer.wrap(data));
return chars.toString();
}
catch (CharacterCodingException notUTF8) {
if (mEncoding == ENCODING_UTF_8) {
throw new IIOException("Wrong encoding of IPTC data, explicitly set to UTF-8 in DataSet 1:90", notUTF8);
}
// Fall back to use ISO-8859-1
// This will not fail, but may may create wrong fallback-characters
return StringUtil.decode(data, 0, data.length, "ISO8859_1");
}
}
}
@@ -0,0 +1,64 @@
/*
* Copyright (c) 2009, 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.metadata.xmp;
import java.util.Collections;
import java.util.Map;
/**
* XMP
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: XMP.java,v 1.0 Nov 12, 2009 12:19:32 AM haraldk Exp$
*
* @see <a href="http://www.adobe.com/products/xmp/">Extensible Metadata Platform (XMP)</a>
*/
public interface XMP {
/** W3C Resource Description Format namespace */
String NS_RDF = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
/** Dublin Core Metadata Initiative namespace */
String NS_DC = "http://purl.org/dc/elements/1.1/";
String NS_EXIF = "http://ns.adobe.com/exif/1.0/";
String NS_PHOTOSHOP = "http://ns.adobe.com/photoshop/1.0/";
String NS_ST_REF = "http://ns.adobe.com/xap/1.0/sType/ResourceRef#";
String NS_TIFF = "http://ns.adobe.com/tiff/1.0/";
String NS_XAP = "http://ns.adobe.com/xap/1.0/";
String NS_XAP_MM = "http://ns.adobe.com/xap/1.0/mm/";
/** Contains the mapping from URI to default namespace prefix. */
Map<String, String> DEFAULT_NS_MAPPING = Collections.unmodifiableMap(new XMPNamespaceMapping());
}
@@ -0,0 +1,51 @@
/*
* Copyright (c) 2009, 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.metadata.xmp;
import com.twelvemonkeys.imageio.metadata.AbstractDirectory;
import com.twelvemonkeys.imageio.metadata.Entry;
import java.util.List;
/**
* XMPDirectory
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: XMPDirectory.java,v 1.0 Nov 17, 2009 9:38:58 PM haraldk Exp$
*/
final class XMPDirectory extends AbstractDirectory {
// TODO: Store size of root directory, to allow serializing
// TODO: XMPDirectory, maybe not even an AbstractDirectory
// - Keeping the Document would allow for easier serialization
// TODO: Or use direct SAX parsing
public XMPDirectory(List<Entry> pEntries) {
super(pEntries);
}
}
@@ -0,0 +1,57 @@
/*
* Copyright (c) 2009, 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.metadata.xmp;
import com.twelvemonkeys.imageio.metadata.AbstractEntry;
/**
* XMPEntry
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: XMPEntry.java,v 1.0 Nov 17, 2009 9:38:39 PM haraldk Exp$
*/
final class XMPEntry extends AbstractEntry {
private final String mFieldName;
public XMPEntry(final String pIdentifier, final Object pValue) {
this(pIdentifier, null, pValue);
}
public XMPEntry(final String pIdentifier, final String pFieldName, final Object pValue) {
super(pIdentifier, pValue);
mFieldName = pFieldName;
}
@SuppressWarnings({"SuspiciousMethodCalls"})
@Override
public String getFieldName() {
return mFieldName != null ? mFieldName : XMP.DEFAULT_NS_MAPPING.get(getIdentifier());
}
}
@@ -0,0 +1,51 @@
/*
* Copyright (c) 2009, 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.metadata.xmp;
import java.util.HashMap;
/**
* XMPNamespaceMapping
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: XMPNamespaceMapping.java,v 1.0 Nov 17, 2009 6:35:21 PM haraldk Exp$
*/
final class XMPNamespaceMapping extends HashMap<String, String> {
public XMPNamespaceMapping() {
put(XMP.NS_RDF, "rdf");
put(XMP.NS_DC, "dc");
put(XMP.NS_EXIF, "exif");
put(XMP.NS_PHOTOSHOP, "photoshop");
put(XMP.NS_ST_REF, "stRef");
put(XMP.NS_TIFF, "tiff");
put(XMP.NS_XAP, "xap");
put(XMP.NS_XAP_MM, "xapMM");
}
}
@@ -0,0 +1,219 @@
/*
* Copyright (c) 2009, 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.metadata.xmp;
import com.twelvemonkeys.imageio.metadata.Directory;
import com.twelvemonkeys.imageio.metadata.Entry;
import com.twelvemonkeys.imageio.metadata.MetadataReader;
import com.twelvemonkeys.imageio.util.IIOUtil;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import javax.imageio.IIOException;
import javax.imageio.stream.ImageInputStream;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.IOException;
import java.util.*;
/**
* XMPReader
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: XMPReader.java,v 1.0 Nov 14, 2009 11:04:30 PM haraldk Exp$
*/
public final class XMPReader extends MetadataReader {
@Override
public Directory read(final ImageInputStream pInput) throws IOException {
// pInput.mark();
//
// BufferedReader reader = new BufferedReader(new InputStreamReader(IIOUtil.createStreamAdapter(pInput), Charset.forName("UTF-8")));
// String line;
// while ((line = reader.readLine()) != null) {
// System.out.println(line);
// }
//
// pInput.reset();
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true);
try {
// TODO: Consider parsing using SAX?
// TODO: Determine encoding and parse using a Reader...
// TODO: Refactor scanner to return inputstream?
DocumentBuilder builder = factory.newDocumentBuilder();
Document document = builder.parse(new InputSource(IIOUtil.createStreamAdapter(pInput)));
// XMLSerializer serializer = new XMLSerializer(System.err, System.getProperty("file.encoding"));
// serializer.serialize(document);
// Each rdf:Description is a Directory (but we can't really rely on that structure.. it's only convention)
// - Each element inside the rdf:Desc is an Entry
Node rdfRoot = document.getElementsByTagNameNS(XMP.NS_RDF, "RDF").item(0);
NodeList descriptions = document.getElementsByTagNameNS(XMP.NS_RDF, "Description");
return parseDirectories(rdfRoot, descriptions);
}
catch (SAXException e) {
throw new IIOException(e.getMessage(), e);
}
catch (ParserConfigurationException e) {
throw new RuntimeException(e); // TODO: Or IOException?
}
}
private XMPDirectory parseDirectories(final Node pParentNode, NodeList pNodes) {
Map<String, List<Entry>> subdirs = new LinkedHashMap<String, List<Entry>>();
for (Node desc : asIterable(pNodes)) {
if (desc.getParentNode() != pParentNode) {
continue;
}
for (Node node : asIterable(desc.getChildNodes())) {
if (node.getNodeType() != Node.ELEMENT_NODE) {
continue;
}
// Lookup
List<Entry> dir = subdirs.get(node.getNamespaceURI());
if (dir == null) {
dir = new ArrayList<Entry>();
subdirs.put(node.getNamespaceURI(), dir);
}
Object value;
Node parseType = node.getAttributes().getNamedItemNS(XMP.NS_RDF, "parseType");
if (parseType != null && "Resource".equals(parseType.getNodeValue())) {
List<Entry> entries = new ArrayList<Entry>();
for (Node child : asIterable(node.getChildNodes())) {
if (child.getNodeType() != Node.ELEMENT_NODE) {
continue;
}
entries.add(new XMPEntry(child.getNamespaceURI() + child.getLocalName(), child.getLocalName(), getChildTextValue(child)));
}
value = new XMPDirectory(entries);
}
else {
// TODO: Support alternative RDF syntax (short-form), using attributes on desc
// NamedNodeMap attributes = node.getAttributes();
//
// for (Node attr : asIterable(attributes)) {
// System.out.println("attr.getNodeName(): " + attr.getNodeName());
// System.out.println("attr.getNodeValue(): " + attr.getNodeValue());
// }
value = getChildTextValue(node);
}
XMPEntry entry = new XMPEntry(node.getNamespaceURI() + node.getLocalName(), node.getLocalName(), value);
dir.add(entry);
}
}
// TODO: Consider flattening the somewhat artificial directory structure
List<Entry> entries = new ArrayList<Entry>();
for (Map.Entry<String, List<Entry>> entry : subdirs.entrySet()) {
entries.add(new XMPEntry(entry.getKey(), new XMPDirectory(entry.getValue())));
}
return new XMPDirectory(entries);
}
private Object getChildTextValue(Node node) {
Object value;
Node child = node.getFirstChild();
String strVal = null;
if (child != null) {
strVal = child.getNodeValue();
}
value = strVal != null ? strVal.trim() : "";
return value;
}
private Iterable<? extends Node> asIterable(final NamedNodeMap pNodeList) {
return new Iterable<Node>() {
public Iterator<Node> iterator() {
return new Iterator<Node>() {
private int mIndex;
public boolean hasNext() {
return pNodeList != null && pNodeList.getLength() > mIndex;
}
public Node next() {
return pNodeList.item(mIndex++);
}
public void remove() {
throw new UnsupportedOperationException("Method remove not supported");
}
};
}
};
}
private Iterable<? extends Node> asIterable(final NodeList pNodeList) {
return new Iterable<Node>() {
public Iterator<Node> iterator() {
return new Iterator<Node>() {
private int mIndex;
public boolean hasNext() {
return pNodeList != null && pNodeList.getLength() > mIndex;
}
public Node next() {
return pNodeList.item(mIndex++);
}
public void remove() {
throw new UnsupportedOperationException("Method remove not supported");
}
};
}
};
}
}
@@ -0,0 +1,243 @@
/*
* Copyright (c) 2009, 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.metadata.xmp;
import com.twelvemonkeys.imageio.stream.BufferedImageInputStream;
import com.twelvemonkeys.imageio.util.IIOUtil;
import javax.imageio.ImageIO;
import javax.imageio.stream.ImageInputStream;
import java.io.*;
import java.nio.charset.Charset;
/**
* XMPScanner
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: XMPScanner.java,v 1.0 Nov 11, 2009 4:49:00 PM haraldk Exp$
*/
public final class XMPScanner {
/**
* {@code &lt;?xpacket begin=}
* <p/>
* <ul>
* <li>
* 8-bit (UTF-8):
* 0x3C 0x3F 0x78 0x70 0x61 0x63 0x6B 0x65 0x74 0x20
* 0x62 0x65 0x67 0x69 0x6E 0x3D
* </li>
* <li>16-bit encoding (UCS-2, UTF-16): (either big- or little-endian order)
* 0x3C 0x00 0x3F 0x00 0x78 0x00 0x70 0x00 0x61 0x00
* 0x63 0x00 0x6B 0x00 0x65 0x00 0x74 0x00 0x20 0x00 0x62 0x00
* 0x65 0x00 0x67 0x00 0x69 0x00 0x6E 0x00 0x3D [0x00]
* </li>
* <li>32-bit encoding (UCS-4):
* As 16 bit UCS2, with three 0x00 instead of one.</li>
* </ul>
*/
private static final byte[] XMP_PACKET_BEGIN = {
0x3C, 0x3F, 0x78, 0x70, 0x61, 0x63, 0x6B, 0x65, 0x74, 0x20,
0x62, 0x65, 0x67, 0x69, 0x6E, 0x3D
};
/**
* {@code &lt;?xpacket end=}
*/
private static final byte[] XMP_PACKET_END = {
0x3C, 0x3F, 0x78, 0x70, 0x61, 0x63, 0x6B, 0x65, 0x74, 0x20,
0x65, 0x6E, 0x64, 0x3D
};
/**
* Scans the given input for an XML metadata packet.
* The scanning process involves reading every byte in the file, while searching for an XMP packet.
* This process is very inefficient, compared to reading a known file format.
* <p/>
* <em>NOTE: The XMP Specification says this method of reading an XMP packet
* should be considered a last resort.</em><br/>
* This is because files may contain multiple XMP packets, some which may be related to embedded resources,
* some which may be obsolete (or even incomplete).
*
* @param pInput the input to scan. The input may be an {@link javax.imageio.stream.ImageInputStream} or
* any object that can be passed to {@link ImageIO#createImageInputStream(Object)}.
* Typically this may be a {@link File}, {@link InputStream} or {@link java.io.RandomAccessFile}.
*
* @return a character Reader
*
* @throws java.nio.charset.UnsupportedCharsetException if the encoding specified within the BOM is not supported
* by the JRE.
* @throws IOException if an I/O exception occurs reading from {@code pInput}.
* @see ImageIO#createImageInputStream(Object)
*/
static public Reader scanForXMPPacket(final Object pInput) throws IOException {
ImageInputStream stream = pInput instanceof ImageInputStream ? (ImageInputStream) pInput : ImageIO.createImageInputStream(pInput);
// TODO: Consider if BufferedIIS is a good idea
if (!(stream instanceof BufferedImageInputStream)) {
stream = new BufferedImageInputStream(stream);
}
// TODO: Might be more than one XMP block per file (it's possible to re-start for now)..
long pos;
pos = scanForSequence(stream, XMP_PACKET_BEGIN);
if (pos >= 0) {
// Skip ' OR " (plus possible nulls for 16/32 bit)
byte quote = stream.readByte();
if (quote == '\'' || quote == '"') {
Charset cs = null;
// Read BOM
byte[] bom = new byte[4];
stream.readFully(bom);
// NOTE: Empty string should be treated as UTF-8 for backwards compatibility
if (bom[0] == (byte) 0xEF && bom[1] == (byte) 0xBB && bom[2] == (byte) 0xBF && bom[3] == quote ||
bom[0] == quote) {
// UTF-8
cs = Charset.forName("UTF-8");
}
else if (bom[0] == (byte) 0xFE && bom[1] == (byte) 0xFF && bom[2] == 0x00 && bom[3] == quote) {
// UTF-16 BIG endian
cs = Charset.forName("UTF-16BE");
}
else if (bom[0] == 0x00 && bom[1] == (byte) 0xFF && bom[2] == (byte) 0xFE && bom[3] == quote) {
stream.skipBytes(1); // Alignment
// UTF-16 little endian
cs = Charset.forName("UTF-16LE");
}
else if (bom[0] == 0x00 && bom[1] == 0x00 && bom[2] == (byte) 0xFE && bom[3] == (byte) 0xFF) {
// NOTE: 32-bit character set not supported by default
// UTF 32 BIG endian
cs = Charset.forName("UTF-32BE");
}
else if (bom[0] == (byte) 0xFF && bom[1] == (byte) 0xFE && bom[2] == 0x00 && bom[3] == 0x00) {
// TODO: FixMe..
// NOTE: 32-bit character set not supported by default
// UTF 32 little endian
cs = Charset.forName("UTF-32LE");
}
if (cs != null) {
// Read all bytes until <?xpacket end= up-front or filter stream
stream.mark();
long end = scanForSequence(stream, XMP_PACKET_END);
stream.reset();
long length = end - stream.getStreamPosition();
Reader reader = new InputStreamReader(IIOUtil.createStreamAdapter(stream, length), cs);
// Skip until ?>
while (reader.read() != '>') {
}
// Return reader?
// How to decide between w or r?!
return reader;
}
}
}
return null;
}
/**
* Scans for a given ASCII sequence.
*
* @param pStream the stream to scan
* @param pSequence the byte sequence to search for
*
* @return the start position of the given sequence.
*
* @throws IOException if an I/O exception occurs during scanning
*/
private static long scanForSequence(final ImageInputStream pStream, final byte[] pSequence) throws IOException {
long start = -1l;
int index = 0;
int nullBytes = 0;
for (int read; (read = pStream.read()) >= 0;) {
if (pSequence[index] == (byte) read) {
// If this is the first byte in the sequence, store position
if (start == -1) {
start = pStream.getStreamPosition() - 1;
}
// Inside the sequence, there might be 1 or 3 null bytes, depending on 16/32 byte encoding
if (nullBytes == 1 || nullBytes == 3) {
pStream.skipBytes(nullBytes);
}
index++;
// If we found the entire sequence, we're done, return start position
if (index == pSequence.length) {
return start;
}
}
else if (index == 1 && read == 0 && nullBytes < 3) {
// Skip 1 or 3 null bytes for 16/32 bit encoding
nullBytes++;
}
else if (index != 0) {
// Start over
index = 0;
start = -1;
nullBytes = 0;
}
}
return -1l;
}
//static public XMPDirectory parse(input);
public static void main(final String[] pArgs) throws IOException {
ImageInputStream stream = ImageIO.createImageInputStream(new File(pArgs[0]));
Reader xmp;
while ((xmp = scanForXMPPacket(stream)) != null) {
BufferedReader reader = new BufferedReader(xmp);
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
}
stream.close();
// else {
// System.err.println("XMP not found");
// }
}
}
@@ -0,0 +1,143 @@
package com.twelvemonkeys.imageio.metadata.exif;
import junit.framework.TestCase;
/**
* RationalTestCase
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: RationalTestCase.java,v 1.0 Nov 18, 2009 3:23:17 PM haraldk Exp$
*/
public class RationalTestCase extends TestCase {
public void testZeroDenominator() {
try {
new Rational(1, 0);
fail("IllegalArgumentException expected");
}
catch (IllegalArgumentException expected) {
}
}
// TODO: Find a solution to this problem, as we should be able to work with it...
public void testLongMinValue() {
try {
new Rational(Long.MIN_VALUE, 1);
fail("IllegalArgumentException expected");
}
catch (IllegalArgumentException expected) {
}
try {
new Rational(1, Long.MIN_VALUE);
fail("IllegalArgumentException expected");
}
catch (IllegalArgumentException expected) {
}
}
public void testEquals() {
assertEquals(new Rational(0, 1), new Rational(0, 999));
assertEquals(new Rational(0, 1), new Rational(0, -1));
assertEquals(new Rational(1, 2), new Rational(1000000, 2000000));
assertEquals(new Rational(1, -2), new Rational(-1, 2));
Rational x = new Rational(1, -2);
Rational y = new Rational(-1000000, 2000000);
assertEquals(x, y);
assertEquals(x.numerator(), y.numerator());
assertEquals(x.denominator(), y.denominator());
}
public void testEqualsBoundaries() {
assertEquals(new Rational(Long.MAX_VALUE, Long.MAX_VALUE), new Rational(1, 1));
// NOTE: Math.abs(Long.MIN_VALUE) == Long.MIN_VALUE... :-P
assertEquals(new Rational(Long.MIN_VALUE + 1, Long.MIN_VALUE + 1), new Rational(1, 1));
assertEquals(new Rational(Long.MIN_VALUE + 1, Long.MAX_VALUE), new Rational(-1, 1));
assertEquals(new Rational(Long.MAX_VALUE, Long.MIN_VALUE + 1), new Rational(-1, 1));
}
public void testReciprocal() {
assertEquals(new Rational(1, 99), new Rational(99, 1).reciprocal());
assertEquals(new Rational(-1, 1234567), new Rational(-1234567, 1).reciprocal());
}
public void testNegate() {
assertEquals(new Rational(-1, 99), new Rational(1, 99).negate());
assertEquals(new Rational(1, 1234567), new Rational(1, -1234567).negate());
}
public void testPlus() {
Rational x, y;
// 1/2 + 1/3 = 5/6
x = new Rational(1, 2);
y = new Rational(1, 3);
assertEquals(new Rational(5, 6), x.plus(y));
// 8/9 + 1/9 = 1
x = new Rational(8, 9);
y = new Rational(1, 9);
assertEquals(new Rational(1, 1), x.plus(y));
// 1/200000000 + 1/300000000 = 1/120000000
x = new Rational(1, 200000000);
y = new Rational(1, 300000000);
assertEquals(new Rational(1, 120000000), x.plus(y));
// 1073741789/20 + 1073741789/30 = 1073741789/12
x = new Rational(1073741789, 20);
y = new Rational(1073741789, 30);
assertEquals(new Rational(1073741789, 12), x.plus(y));
// x + 0 = x
assertEquals(x, x.plus(Rational.ZERO));
}
public void testTimes() {
Rational x, y;
// 4/17 * 17/4 = 1
x = new Rational(4, 17);
y = new Rational(17, 4);
assertEquals(new Rational(1, 1), x.times(y));
// 3037141/3247033 * 3037547/3246599 = 841/961
x = new Rational(3037141, 3247033);
y = new Rational(3037547, 3246599);
assertEquals(new Rational(841, 961), x.times(y));
// x * 0 = 0
assertEquals(Rational.ZERO, x.times(Rational.ZERO));
}
public void testMinus() {
// 1/6 - -4/-8 = -1/3
Rational x = new Rational(1, 6);
Rational y = new Rational(-4, -8);
assertEquals(new Rational(-1, 3), x.minus(y));
// x - 0 = x
assertEquals(x, x.minus(Rational.ZERO));
}
public void testDivides() {
// 3037141/3247033 / 3246599/3037547 = 841/961
Rational x = new Rational(3037141, 3247033);
Rational y = new Rational(3246599, 3037547);
assertEquals(new Rational(841, 961), x.divides(y));
// 0 / x = 0
assertEquals(Rational.ZERO, new Rational(0, 386).divides(x));
}
public void testDivideZero() {
try {
new Rational(3037141, 3247033).divides(new Rational(0, 1));
}
catch (ArithmeticException expected) {
}
}
}
@@ -0,0 +1,127 @@
package com.twelvemonkeys.imageio.metadata.xmp;
import junit.framework.TestCase;
import java.io.*;
import java.nio.charset.UnsupportedCharsetException;
import java.util.Arrays;
import java.util.Collections;
import java.util.Random;
/**
* XMPScannerTestCase
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: XMPScannerTestCase.java,v 1.0 Nov 13, 2009 3:59:43 PM haraldk Exp$
*/
public class XMPScannerTestCase extends TestCase {
static final String XMP =
"<?xpacket begin=\"\uFEFF\" id=\"W5M0MpCehiHzreSzNTczkc9d\"?>" +
"<x:xmpmeta xmlns:x=\"adobe:ns:meta/\" x:xmptk=\"Adobe XMP Core 4.1-c036 46.276720, Fri Nov 13 2009 15:59:43 \">\n"+
" <rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\">\n"+
" <rdf:Description rdf:about=\"\"\n"+
" xmlns:photoshop=\"http://ns.adobe.com/photoshop/1.0/\">\n"+
" <photoshop:Source>twelvemonkeys.com</photoshop:Source>\n"+
" </rdf:Description>\n"+
" <rdf:Description rdf:about=\"\"\n"+
" xmlns:dc=\"http://purl.org/dc/elements/1.1/\">\n"+
" <dc:format>application/vnd.adobe.photoshop</dc:format>\n"+
" </rdf:Description>\n"+
" </rdf:RDF>\n"+
"</x:xmpmeta>" +
"<?xpacket end=\"w\"?>";
final Random mRandom = new Random(4934638567l);
private InputStream createRandomStream(final int pLength) {
byte[] bytes = new byte[pLength];
mRandom.nextBytes(bytes);
return new ByteArrayInputStream(bytes);
}
private InputStream createXMPStream(final String pXMP, final String pCharsetName) {
try {
return new SequenceInputStream(
Collections.enumeration(
Arrays.asList(
createRandomStream(79),
new ByteArrayInputStream(pXMP.getBytes(pCharsetName)),
createRandomStream(31)
)
)
);
}
catch (UnsupportedEncodingException e) {
UnsupportedCharsetException uce = new UnsupportedCharsetException(pCharsetName);
uce.initCause(e);
throw uce;
}
}
public void testScanForUTF8() throws IOException {
InputStream stream = createXMPStream(XMP, "UTF-8");
Reader reader = XMPScanner.scanForXMPPacket(stream);
assertNotNull(reader);
}
public void testScanForUTF8singleQuote() throws IOException {
InputStream stream = createXMPStream(XMP, "UTF-8".replace("\"", "'"));
Reader reader = XMPScanner.scanForXMPPacket(stream);
assertNotNull(reader);
}
public void testScanForUTF16BE() throws IOException {
InputStream stream = createXMPStream(XMP, "UTF-16BE");
Reader reader = XMPScanner.scanForXMPPacket(stream);
assertNotNull(reader);
}
public void testScanForUTF16BEsingleQuote() throws IOException {
InputStream stream = createXMPStream(XMP, "UTF-16BE".replace("\"", "'"));
Reader reader = XMPScanner.scanForXMPPacket(stream);
assertNotNull(reader);
}
public void testScanForUTF16LE() throws IOException {
InputStream stream = createXMPStream(XMP, "UTF-16LE");
Reader reader = XMPScanner.scanForXMPPacket(stream);
assertNotNull(reader);
}
public void testScanForUTF16LEsingleQuote() throws IOException {
InputStream stream = createXMPStream(XMP, "UTF-16LE".replace("\"", "'"));
Reader reader = XMPScanner.scanForXMPPacket(stream);
assertNotNull(reader);
}
// TODO: Default Java installation on OS X don't seem to have UTF-32 installed. Hmmm..
// public void testUTF32BE() throws IOException {
// InputStream stream = createXMPStream("UTF-32BE");
//
// Reader reader = XMPScanner.scanForXMPPacket(stream);
//
// assertNotNull(reader);
// }
//
// public void testUTF32LE() throws IOException {
// InputStream stream = createXMPStream("UTF-32LE");
//
// Reader reader = XMPScanner.scanForXMPPacket(stream);
//
// assertNotNull(reader);
// }
}
+2 -2
View File
@@ -5,14 +5,14 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>twelvemonkeys-imageio-pict</artifactId>
<version>2.2</version>
<version>2.3</version>
<name>TwelveMonkeys ImageIO PICT plugin</name>
<description>ImageIO plugin for Apple Mac Paint Picture (PICT) format.</description>
<parent>
<artifactId>twelvemonkeys-imageio</artifactId>
<groupId>com.twelvemonkeys</groupId>
<version>2.2</version>
<version>2.3</version>
</parent>
<dependencies>
@@ -28,6 +28,9 @@
package com.twelvemonkeys.imageio.plugins.pict;
import com.twelvemonkeys.imageio.spi.ProviderInfo;
import com.twelvemonkeys.imageio.util.IIOUtil;
import javax.imageio.ImageReader;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream;
@@ -45,12 +48,16 @@ import java.util.Locale;
public class PICTImageReaderSpi extends ImageReaderSpi {
/**
* Creates an PICTImageReaderSpi
* Creates a {@code PICTImageReaderSpi}.
*/
public PICTImageReaderSpi() {
this(IIOUtil.getProviderInfo(PICTImageReaderSpi.class));
}
private PICTImageReaderSpi(final ProviderInfo pProviderInfo) {
super(
"TwelveMonkeys",
"2.2",
pProviderInfo.getVendorName(),
pProviderInfo.getVersion(),
new String[]{"pct", "PCT", "pict", "PICT"},
new String[]{"pct", "pict"},
new String[]{"image/pict", "image/x-pict"},
@@ -28,6 +28,9 @@
package com.twelvemonkeys.imageio.plugins.pict;
import com.twelvemonkeys.imageio.spi.ProviderInfo;
import com.twelvemonkeys.imageio.util.IIOUtil;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.ImageWriter;
import javax.imageio.spi.ImageWriterSpi;
@@ -44,12 +47,16 @@ import java.util.Locale;
public class PICTImageWriterSpi extends ImageWriterSpi {
/**
* Creates an PICTImageWriterSpi
* Creates a {@code PICTImageWriterSpi}.
*/
public PICTImageWriterSpi() {
this(IIOUtil.getProviderInfo(PICTImageWriterSpi.class));
}
private PICTImageWriterSpi(final ProviderInfo pProviderInfo) {
super(
"TwelveMonkeys",
"2.0",
pProviderInfo.getVendorName(),
pProviderInfo.getVersion(),
new String[]{"pct", "PCT",
"pict", "PICT"},
new String[]{"pct", "pict"},
+28 -4
View File
@@ -5,7 +5,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.twelvemonkeys</groupId>
<artifactId>twelvemonkeys-imageio</artifactId>
<version>2.2</version>
<version>2.3</version>
<packaging>pom</packaging>
<name>TwelveMonkeys ImageIO</name>
@@ -29,6 +29,7 @@
<modules>
<!-- Support -->
<module>core</module>
<module>metadata</module>
<!-- Stand-alone readers/writers -->
<module>ico</module>
@@ -46,8 +47,8 @@
</modules>
<properties>
<core.version>2.2</core.version>
<imageio.core.version>2.2</imageio.core.version>
<core.version>2.3</core.version>
<imageio.core.version>2.3</imageio.core.version>
</properties>
<dependencies>
@@ -96,6 +97,13 @@
<classifier>tests</classifier>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>twelvemonkeys-imageio-metadata</artifactId>
<version>${imageio.core.version}</version>
<scope>compile</scope>
</dependency>
</dependencies>
</dependencyManagement>
@@ -108,7 +116,23 @@
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<configuration>
<encoding>UTF-8</encoding>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.2</version>
<configuration>
<archive>
<manifestEntries>
<Implementation-Title>${project.name}</Implementation-Title>
<Implementation-Vendor>TwelveMonkeys</Implementation-Vendor>
<Implementation-Version>${project.version}</Implementation-Version>
<Implementation-URL>https://twelvemonkeys-imageio.dev.java.net/</Implementation-URL>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
+35 -31
View File
@@ -1,31 +1,35 @@
<?xml version="1.0"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>twelvemonkeys-imageio-psd</artifactId>
<version>2.2</version>
<name>TwelveMonkeys ImageIO PSD plugin</name>
<description>
ImageIO plugin for Adobe Photoshop Document (PSD).
</description>
<parent>
<artifactId>twelvemonkeys-imageio</artifactId>
<groupId>com.twelvemonkeys</groupId>
<version>2.2</version>
</parent>
<dependencies>
<dependency>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>twelvemonkeys-imageio-core</artifactId>
</dependency>
<dependency>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>twelvemonkeys-imageio-core</artifactId>
<classifier>tests</classifier>
</dependency>
</dependencies>
</project>
<?xml version="1.0"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>twelvemonkeys-imageio-psd</artifactId>
<version>2.3</version>
<name>TwelveMonkeys ImageIO PSD plugin</name>
<description>
ImageIO plugin for Adobe Photoshop Document (PSD).
</description>
<parent>
<artifactId>twelvemonkeys-imageio</artifactId>
<groupId>com.twelvemonkeys</groupId>
<version>2.3</version>
</parent>
<dependencies>
<dependency>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>twelvemonkeys-imageio-core</artifactId>
</dependency>
<dependency>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>twelvemonkeys-imageio-core</artifactId>
<classifier>tests</classifier>
</dependency>
<dependency>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>twelvemonkeys-imageio-metadata</artifactId>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,104 @@
package com.twelvemonkeys.imageio.plugins.psd;
import org.w3c.dom.Node;
import javax.imageio.metadata.IIOInvalidTreeException;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.metadata.IIOMetadataFormatImpl;
import java.util.Arrays;
/**
* AbstractMetadata
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: AbstractMetadata.java,v 1.0 Nov 13, 2009 1:02:12 AM haraldk Exp$
*/
abstract class AbstractMetadata extends IIOMetadata implements Cloneable {
// TODO: Move to core...
protected AbstractMetadata(final boolean pStandardFormatSupported,
final String pNativeFormatName, final String pNativeFormatClassName,
final String[] pExtraFormatNames, final String[] pExtraFormatClassNames) {
super(pStandardFormatSupported, pNativeFormatName, pNativeFormatClassName, pExtraFormatNames, pExtraFormatClassNames);
}
/**
* Default implementation returns {@code true}.
* Mutable subclasses should override this method.
*
* @return {@code true}.
*/
@Override
public boolean isReadOnly() {
return true;
}
@Override
public Node getAsTree(final String pFormatName) {
validateFormatName(pFormatName);
if (pFormatName.equals(nativeMetadataFormatName)) {
return getNativeTree();
}
else if (pFormatName.equals(IIOMetadataFormatImpl.standardMetadataFormatName)) {
return getStandardTree();
}
// TODO: What about extra formats??
throw new AssertionError("Unreachable");
}
@Override
public void mergeTree(final String pFormatName, final Node pRoot) throws IIOInvalidTreeException {
assertMutable();
validateFormatName(pFormatName);
if (!pRoot.getNodeName().equals(nativeMetadataFormatName)) {
throw new IIOInvalidTreeException("Root must be " + nativeMetadataFormatName, pRoot);
}
Node node = pRoot.getFirstChild();
while (node != null) {
// TODO: Merge values from node into this
// Move to the next sibling
node = node.getNextSibling();
}
}
@Override
public void reset() {
assertMutable();
}
/**
* Asserts that this meta data is mutable.
*
* @throws IllegalStateException if {@link #isReadOnly()} returns {@code true}.
*/
protected final void assertMutable() {
if (isReadOnly()) {
throw new IllegalStateException("Metadata is read-only");
}
}
protected abstract Node getNativeTree();
protected final void validateFormatName(final String pFormatName) {
String[] metadataFormatNames = getMetadataFormatNames();
if (metadataFormatNames != null) {
for (String metadataFormatName : metadataFormatNames) {
if (metadataFormatName.equals(pFormatName)) {
return; // Found, we're ok!
}
}
}
throw new IllegalArgumentException(
String.format("Bad format name: \"%s\". Expected one of %s", pFormatName, Arrays.toString(metadataFormatNames))
);
}
}
@@ -103,10 +103,10 @@ Easier to calculate if K' is calculated first, because K' = min(C,M,Y):
}
public float[] toCIEXYZ(float[] colorvalue) {
throw new UnsupportedOperationException("Method toCIEXYZ not implemented"); // TODO: Implement
return sRGB.toCIEXYZ(toRGB(colorvalue));
}
public float[] fromCIEXYZ(float[] colorvalue) {
throw new UnsupportedOperationException("Method fromCIEXYZ not implemented"); // TODO: Implement
return sRGB.fromCIEXYZ(fromRGB(colorvalue));
}
}
@@ -101,7 +101,7 @@ interface PSD {
int COMPRESSION_ZIP = 2;
/** ZIP compression with prediction */
int COMPRESSION_ZIP_PREDICTON = 3;
int COMPRESSION_ZIP_PREDICTION = 3;
// Color Modes
/** Bitmap (monochrome) */
@@ -225,11 +225,11 @@ interface PSD {
// 03f8
/** Color transfer functions */
int RES_COLOR_TRANSFER_FUNCITON = 0x03f8;
int RES_COLOR_TRANSFER_FUNCTION = 0x03f8;
// 03f9
/** Duotone transfer functions */
int RES_DUOTONE_TRANSFER_FUNCITON = 0x03f9;
int RES_DUOTONE_TRANSFER_FUNCTOON = 0x03f9;
// 03fa
/** Duotone image information */
@@ -385,7 +385,7 @@ interface PSD {
* (Photoshop 5.0) Unicode Alpha Names
* Unicode string (4 bytes length followed by string).
*/
int RES_UNICODE_ALPHA_NAME = 0x0415;
int RES_UNICODE_ALPHA_NAMES = 0x0415;
// 1046
/**
@@ -34,11 +34,11 @@ import java.util.ArrayList;
import java.util.List;
/**
* PSDAlhpaChannelInfo
* PSDAlphaChannelInfo
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: PSDAlhpaChannelInfo.java,v 1.0 May 2, 2008 5:33:40 PM haraldk Exp$
* @version $Id: PSDAlphaChannelInfo.java,v 1.0 May 2, 2008 5:33:40 PM haraldk Exp$
*/
class PSDAlphaChannelInfo extends PSDImageResource {
List<String> mNames;
@@ -48,11 +48,12 @@ class PSDAlphaChannelInfo extends PSDImageResource {
}
@Override
protected void readData(ImageInputStream pInput) throws IOException {
protected void readData(final ImageInputStream pInput) throws IOException {
mNames = new ArrayList<String>();
long left = mSize;
while (left > 0) {
String name = PSDUtil.readPascalStringByte(pInput);
String name = PSDUtil.readPascalString(pInput);
mNames.add(name);
left -= name.length() + 1;
}
@@ -36,8 +36,8 @@ package com.twelvemonkeys.imageio.plugins.psd;
* @version $Id: PSDChannelInfo.java,v 1.0 May 6, 2008 2:46:23 PM haraldk Exp$
*/
class PSDChannelInfo {
private short mChannelId;
long mLength;
final short mChannelId;
final long mLength;
// typedef struct _CLI
// {
@@ -47,7 +47,7 @@ class PSDColorData {
final byte[] mColors;
private IndexColorModel mColorModel;
PSDColorData(ImageInputStream pInput) throws IOException {
PSDColorData(final ImageInputStream pInput) throws IOException {
int length = pInput.readInt();
if (length == 0) {
throw new IIOException("No palette information in PSD");
@@ -72,7 +72,7 @@ class PSDColorData {
return mColorModel;
}
private int[] toInterleavedRGB(byte[] pColors) {
private static int[] toInterleavedRGB(final byte[] pColors) {
int[] rgb = new int[pColors.length / 3];
for (int i = 0; i < rgb.length; i++) {
@@ -40,7 +40,22 @@ import java.io.IOException;
* @version $Id: PSDResolutionInfo.java,v 1.0 May 2, 2008 3:58:19 PM haraldk Exp$
*/
class PSDDisplayInfo extends PSDImageResource {
// TODO: Size of this struct should be 14.. Does not compute...
// TODO: Size of this struct should be 14.. Does not compute... Something bogus here
// ColorSpace definitions:
// PSD_CS_RGB = 0, /* RGB */
// PSD_CS_HSB = 1, /* Hue, Saturation, Brightness */
// PSD_CS_CMYK = 2, /* CMYK */
// PSD_CS_PANTONE = 3, /* Pantone matching system (Lab)*/
// PSD_CS_FOCOLTONE = 4, /* Focoltone colour system (CMYK)*/
// PSD_CS_TRUMATCH = 5, /* Trumatch color (CMYK)*/
// PSD_CS_TOYO = 6, /* Toyo 88 colorfinder 1050 (Lab)*/
// PSD_CS_LAB = 7, /* L*a*b*/
// PSD_CS_GRAYSCALE = 8, /* Grey scale */
// PSD_CS_HKS = 10, /* HKS colors (CMYK)*/
// PSD_CS_DIC = 11, /* DIC color guide (Lab)*/
// PSD_CS_ANPA = 3000, /* Anpa color (Lab)*/
//typedef _DisplayInfo
//{
// WORD ColorSpace;
@@ -50,10 +65,10 @@ class PSDDisplayInfo extends PSDImageResource {
// BYTE Padding; /* Always zero */
//} DISPLAYINFO;
private int mColorSpace;
private short[] mColors;
private short mOpacity;
private byte mKind;
int mColorSpace;
short[] mColors;
short mOpacity;
byte mKind;
PSDDisplayInfo(final short pId, final ImageInputStream pInput) throws IOException {
super(pId, pInput);
@@ -67,20 +82,20 @@ class PSDDisplayInfo extends PSDImageResource {
// long left = mSize;
// while (left > 0) {
mColorSpace = pInput.readShort();
mColorSpace = pInput.readShort();
// Color[4]...?
// Color[4]...?
mColors = new short[4];
mColors[0] = pInput.readShort();
mColors[1] = pInput.readShort();
mColors[2] = pInput.readShort();
mColors[3] = pInput.readShort();
mColors[0] = pInput.readShort();
mColors[1] = pInput.readShort();
mColors[2] = pInput.readShort();
mColors[3] = pInput.readShort();
mOpacity = pInput.readShort();
mOpacity = pInput.readShort();
mKind = pInput.readByte();
mKind = pInput.readByte();
pInput.readByte(); // Pad
pInput.readByte(); // Pad
// left -= 14;
// }
pInput.skipBytes(mSize - 14);
@@ -1,16 +1,10 @@
package com.twelvemonkeys.imageio.plugins.psd;
import com.twelvemonkeys.imageio.util.IIOUtil;
import com.twelvemonkeys.lang.StringUtil;
import com.twelvemonkeys.imageio.metadata.Directory;
import com.twelvemonkeys.imageio.metadata.exif.EXIFReader;
import javax.imageio.IIOException;
import javax.imageio.stream.ImageInputStream;
import javax.imageio.stream.MemoryCacheImageInputStream;
import java.io.IOException;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* EXIF metadata.
@@ -24,7 +18,6 @@ import java.util.List;
* @see <a href="http://partners.adobe.com/public/developer/tiff/index.html">Adobe TIFF developer resources</a>
*/
final class PSDEXIF1Data extends PSDImageResource {
// protected byte[] mData;
protected Directory mDirectory;
PSDEXIF1Data(final short pId, final ImageInputStream pInput) throws IOException {
@@ -34,294 +27,16 @@ final class PSDEXIF1Data extends PSDImageResource {
@Override
protected void readData(final ImageInputStream pInput) throws IOException {
// This is in essence an embedded TIFF file.
// TODO: Extract TIFF parsing to more general purpose package
// TODO: Instead, read the byte data, store for later parsing
MemoryCacheImageInputStream stream = new MemoryCacheImageInputStream(IIOUtil.createStreamAdapter(pInput, mSize));
byte[] bom = new byte[2];
stream.readFully(bom);
if (bom[0] == 'I' && bom[1] == 'I') {
stream.setByteOrder(ByteOrder.LITTLE_ENDIAN);
}
else if (!(bom[0] == 'M' && bom[1] == 'M')) {
throw new IIOException(String.format("Invalid byte order marker '%s'", StringUtil.decode(bom, 0, bom.length, "ASCII")));
}
if (stream.readUnsignedShort() != 42) {
throw new IIOException("Wrong TIFF magic in EXIF data.");
}
long directoryOffset = stream.readUnsignedInt();
mDirectory = Directory.read(stream, directoryOffset);
// TODO: Instead, read the byte data, store for later parsing (or better yet, store offset, and read on request)
mDirectory = new EXIFReader().read(pInput);
}
@Override
public String toString() {
StringBuilder builder = toStringBuilder();
builder.append(", ").append(mDirectory);
builder.append("]");
return builder.toString();
}
// TIFF Image file directory (IFD)
private static class Directory {
List<Entry> mEntries = new ArrayList<Entry>();
private Directory() {}
public static Directory read(final ImageInputStream pInput, final long pOffset) throws IOException {
Directory directory = new Directory();
pInput.seek(pOffset);
int entryCount = pInput.readUnsignedShort();
for (int i = 0; i < entryCount; i++) {
directory.mEntries.add(Entry.read(pInput));
}
long nextOffset = pInput.readUnsignedInt();
if (nextOffset != 0) {
Directory next = Directory.read(pInput, nextOffset);
directory.mEntries.addAll(next.mEntries);
}
return directory;
}
@Override
public String toString() {
return String.format("Directory%s", mEntries);
}
}
// TIFF IFD Entry
private static class Entry {
private static final int EXIF_IFD = 0x8769;
private final static String[] TYPE_NAMES = {
"BYTE", "ASCII", "SHORT", "LONG", "RATIONAL",
"SBYTE", "UNDEFINED", "SSHORT", "SLONG", "SRATIONAL", "FLOAT", "DOUBLE",
};
private final static int[] TYPE_LENGTHS = {
1, 1, 2, 4, 8,
1, 1, 2, 4, 8, 4, 8,
};
private int mTag;
/*
1 = BYTE 8-bit unsigned integer.
2 = ASCII 8-bit byte that contains a 7-bit ASCII code; the last byte
must be NUL (binary zero).
3 = SHORT 16-bit (2-byte) unsigned integer.
4 = LONG 32-bit (4-byte) unsigned integer.
5 = RATIONAL Two LONGs: the first represents the numerator of a
fraction; the second, the denominator.
TIFF 6.0 and above:
6 = SBYTE An 8-bit signed (twos-complement) integer.
7 = UNDEFINED An 8-bit byte that may contain anything, depending on
the definition of the field.
8 = SSHORT A 16-bit (2-byte) signed (twos-complement) integer.
9 = SLONG A 32-bit (4-byte) signed (twos-complement) integer.
10 = SRATIONAL Two SLONGs: the first represents the numerator of a
fraction, the second the denominator.
11 = FLOAT Single precision (4-byte) IEEE format.
12 = DOUBLE Double precision (8-byte) IEEE format.
*/
private short mType;
private int mCount;
private long mValueOffset;
private Object mValue;
private Entry() {}
public static Entry read(final ImageInputStream pInput) throws IOException {
Entry entry = new Entry();
entry.mTag = pInput.readUnsignedShort();
entry.mType = pInput.readShort();
entry.mCount = pInput.readInt(); // Number of values
// TODO: Handle other sub-IFDs
if (entry.mTag == EXIF_IFD) {
long offset = pInput.readUnsignedInt();
pInput.mark();
try {
entry.mValue = Directory.read(pInput, offset);
}
finally {
pInput.reset();
}
}
else {
int valueLength = entry.getValueLength();
if (valueLength > 0 && valueLength <= 4) {
entry.readValueInLine(pInput);
pInput.skipBytes(4 - valueLength);
}
else {
entry.mValueOffset = pInput.readUnsignedInt(); // This is the *value* iff the value size is <= 4 bytes
entry.readValue(pInput);
}
}
return entry;
}
private void readValue(final ImageInputStream pInput) throws IOException {
long pos = pInput.getStreamPosition();
try {
pInput.seek(mValueOffset);
readValueInLine(pInput);
}
finally {
pInput.seek(pos);
}
}
private void readValueInLine(ImageInputStream pInput) throws IOException {
mValue = readValueDirect(pInput, mType, mCount);
}
private static Object readValueDirect(final ImageInputStream pInput, final short pType, final int pCount) throws IOException {
switch (pType) {
case 2:
// TODO: This might be UTF-8 or ISO-8859-1, even though against the spec
byte[] ascii = new byte[pCount];
pInput.readFully(ascii);
return StringUtil.decode(ascii, 0, ascii.length, "ASCII");
case 1:
if (pCount == 1) {
return pInput.readUnsignedByte();
}
case 6:
if (pCount == 1) {
return pInput.readByte();
}
case 7:
byte[] bytes = new byte[pCount];
pInput.readFully(bytes);
return bytes;
case 3:
if (pCount == 1) {
return pInput.readUnsignedShort();
}
case 8:
if (pCount == 1) {
return pInput.readShort();
}
short[] shorts = new short[pCount];
pInput.readFully(shorts, 0, shorts.length);
return shorts;
case 4:
if (pCount == 1) {
return pInput.readUnsignedInt();
}
case 9:
if (pCount == 1) {
return pInput.readInt();
}
int[] ints = new int[pCount];
pInput.readFully(ints, 0, ints.length);
return ints;
case 11:
if (pCount == 1) {
return pInput.readFloat();
}
float[] floats = new float[pCount];
pInput.readFully(floats, 0, floats.length);
return floats;
case 12:
if (pCount == 1) {
return pInput.readDouble();
}
double[] doubles = new double[pCount];
pInput.readFully(doubles, 0, doubles.length);
return doubles;
// TODO: Consider using a Rational class
case 5:
if (pCount == 1) {
return pInput.readUnsignedInt() / (double) pInput.readUnsignedInt();
}
double[] rationals = new double[pCount];
for (int i = 0; i < rationals.length; i++) {
rationals[i] = pInput.readUnsignedInt() / (double) pInput.readUnsignedInt();
}
return rationals;
case 10:
if (pCount == 1) {
return pInput.readInt() / (double) pInput.readInt();
}
double[] srationals = new double[pCount];
for (int i = 0; i < srationals.length; i++) {
srationals[i] = pInput.readInt() / (double) pInput.readInt();
}
return srationals;
default:
throw new IIOException(String.format("Unknown EXIF type '%s'", pType));
}
}
private int getValueLength() {
if (mType > 0 && mType <= TYPE_LENGTHS.length) {
return TYPE_LENGTHS[mType - 1] * mCount;
}
return -1;
}
private String getTypeName() {
if (mType > 0 && mType <= TYPE_NAMES.length) {
return TYPE_NAMES[mType - 1];
}
return "Unknown type";
}
// TODO: Tag names!
@Override
public String toString() {
return String.format("0x%04x: %s (%s, %d)", mTag, getValueAsString(), getTypeName(), mCount);
}
public String getValueAsString() {
if (mValue instanceof String) {
return String.format("\"%s\"", mValue);
}
if (mValue != null && mValue.getClass().isArray()) {
Class<?> type = mValue.getClass().getComponentType();
if (byte.class == type) {
return Arrays.toString((byte[]) mValue);
}
if (short.class == type) {
return Arrays.toString((short[]) mValue);
}
if (int.class == type) {
return Arrays.toString((int[]) mValue);
}
if (float.class == type) {
return Arrays.toString((float[]) mValue);
}
if (double.class == type) {
return Arrays.toString((double[]) mValue);
}
}
return String.valueOf(mValue);
}
}
}
@@ -39,26 +39,28 @@ import java.io.IOException;
* @version $Id: PSDGlobalLayerMask.java,v 1.0 May 8, 2008 5:33:48 PM haraldk Exp$
*/
class PSDGlobalLayerMask {
private int mColorSpace;
private int mColor1;
private int mColor2;
private int mColor3;
private int mColor4;
private int mOpacity;
private int mKind;
final int mColorSpace;
final int mColor1;
final int mColor2;
final int mColor3;
final int mColor4;
final int mOpacity;
final int mKind;
PSDGlobalLayerMask(ImageInputStream pInput) throws IOException {
mColorSpace = pInput.readUnsignedShort();
PSDGlobalLayerMask(final ImageInputStream pInput) throws IOException {
mColorSpace = pInput.readUnsignedShort(); // Undocumented
mColor1 = pInput.readUnsignedShort();
mColor2 = pInput.readUnsignedShort();
mColor3 = pInput.readUnsignedShort();
mColor4 = pInput.readUnsignedShort();
mOpacity = pInput.readUnsignedShort();
mOpacity = pInput.readUnsignedShort(); // 0-100
mKind = pInput.readUnsignedByte(); // 0: Selected (ie inverted), 1: Color protected, 128: Use value stored per layer
// TODO: Variable: Filler zeros
mKind = pInput.readUnsignedByte();
pInput.readByte(); // Pad
}
@@ -0,0 +1,58 @@
package com.twelvemonkeys.imageio.plugins.psd;
import javax.imageio.stream.ImageInputStream;
import java.io.IOException;
/**
* PSDGridAndGuideInfo
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: PSDGridAndGuideInfo.java,v 1.0 Nov 7, 2009 8:46:13 PM haraldk Exp$
*/
final class PSDGridAndGuideInfo extends PSDImageResource {
/* Grid & guide header */
//typedef struct {
// guint32 fVersion; /* Version - always 1 for PS */
// guint32 fGridCycleV; /* Vertical grid size */
// guint32 fGridCycleH; /* Horizontal grid size */
// guint32 fGuideCount; /* Number of guides */
//} GuideHeader;
/* Guide resource block */
//typedef struct {
// guint32 fLocation; /* Guide position in Pixels * 100 */
// gchar fDirection; /* Guide orientation */
//} GuideResource;
int mVersion;
int mGridCycleVertical;
int mGridCycleHorizontal;
int mGuideCount;
GuideResource[] mGuides;
PSDGridAndGuideInfo(final short pId, final ImageInputStream pInput) throws IOException {
super(pId, pInput);
}
@Override
protected void readData(final ImageInputStream pInput) throws IOException {
mVersion = pInput.readInt();
mGridCycleVertical = pInput.readInt();
mGridCycleHorizontal = pInput.readInt();
mGuideCount = pInput.readInt();
mGuides = new GuideResource[mGuideCount];
for (GuideResource guide : mGuides) {
guide.mLocation = pInput.readInt();
guide.mDirection = pInput.readByte();
}
}
static class GuideResource {
int mLocation;
byte mDirection; // 0: vertical, 1: horizontal
}
}
@@ -28,6 +28,9 @@
package com.twelvemonkeys.imageio.plugins.psd;
import org.w3c.dom.Node;
import javax.imageio.metadata.IIOMetadataNode;
import javax.imageio.stream.ImageInputStream;
import javax.imageio.IIOException;
import java.io.IOException;
@@ -61,7 +64,7 @@ class PSDHeader {
final short mBits;
final short mMode;
PSDHeader(ImageInputStream pInput) throws IOException {
PSDHeader(final ImageInputStream pInput) throws IOException {
int signature = pInput.readInt();
if (signature != PSD.SIGNATURE_8BPS) {
throw new IIOException("Not a PSD document, expected signature \"8BPS\": \"" + PSDUtil.intToStr(signature) + "\" (0x" + Integer.toHexString(signature) + ")");
@@ -0,0 +1,37 @@
package com.twelvemonkeys.imageio.plugins.psd;
import com.twelvemonkeys.imageio.metadata.Directory;
import com.twelvemonkeys.imageio.metadata.iptc.IPTCReader;
import javax.imageio.stream.ImageInputStream;
import java.io.IOException;
/**
* PSDIPTCData
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @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 mDirectory;
PSDIPTCData(final short pId, final ImageInputStream pInput) throws IOException {
super(pId, pInput);
}
@Override
protected void readData(final ImageInputStream pInput) throws IOException {
// Read IPTC directory
mDirectory = new IPTCReader().read(pInput);
}
@Override
public String toString() {
StringBuilder builder = toStringBuilder();
builder.append(", ").append(mDirectory);
builder.append("]");
return builder.toString();
}
}
@@ -31,8 +31,15 @@ package com.twelvemonkeys.imageio.plugins.psd;
import com.twelvemonkeys.image.ImageUtil;
import com.twelvemonkeys.imageio.ImageReaderBase;
import com.twelvemonkeys.imageio.util.IndexedImageTypeSpecifier;
import com.twelvemonkeys.xml.XMLSerializer;
import org.w3c.dom.Node;
import javax.imageio.*;
import javax.imageio.IIOException;
import javax.imageio.ImageIO;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.metadata.IIOMetadataFormatImpl;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream;
import java.awt.*;
@@ -43,13 +50,11 @@ import java.awt.image.*;
import java.io.DataInputStream;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.*;
import java.util.List;
/**
* ImageReader for Adobe Photoshop Document format.
* ImageReader for Adobe Photoshop Document (PSD) format.
*
* @see <a href="http://www.fileformat.info/format/psd/egff.htm">Adobe Photoshop File Format Summary<a>
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
@@ -57,19 +62,21 @@ import java.util.List;
* @version $Id: PSDImageReader.java,v 1.0 Apr 29, 2008 4:45:52 PM haraldk Exp$
*/
// TODO: Implement ImageIO meta data interface
// TODO: Implement layer reading
// TODO: Allow reading separate (or some?) layers
// TODO: Allow reading the extra alpha channels (index after composite data)
// TODO: Support for PSDVersionInfo hasRealMergedData=false (no real composite data, layers will be in index 0)
// TODO: Support for API for reading separate layers (index after composite data, and optional alpha channels)
// TODO: Consider Romain Guy's Java 2D implementation of PS filters for the blending modes in layers
// http://www.curious-creature.org/2006/09/20/new-blendings-modes-for-java2d/
// See http://www.codeproject.com/KB/graphics/PSDParser.aspx
// See http://www.adobeforums.com/webx?14@@.3bc381dc/0
public class PSDImageReader extends ImageReaderBase {
private PSDHeader mHeader;
private PSDColorData mColorData;
private List<PSDImageResource> mImageResources;
private PSDGlobalLayerMask mGlobalLayerMask;
private List<PSDLayerInfo> mLayerInfo;
// private PSDColorData mColorData;
// private List<PSDImageResource> mImageResources;
// private PSDGlobalLayerMask mGlobalLayerMask;
// private List<PSDLayerInfo> mLayerInfo;
private ICC_ColorSpace mColorSpace;
protected PSDMetadata mMetadata;
protected PSDImageReader(final ImageReaderSpi pOriginatingProvider) {
super(pOriginatingProvider);
@@ -77,8 +84,9 @@ public class PSDImageReader extends ImageReaderBase {
protected void resetMembers() {
mHeader = null;
mColorData = null;
mImageResources = null;
// mColorData = null;
// mImageResources = null;
mMetadata = null;
mColorSpace = null;
}
@@ -116,9 +124,9 @@ public class PSDImageReader extends ImageReaderBase {
);
case PSD.COLOR_MODE_INDEXED:
// TODO: 16 bit indexed?!
// TODO: 16 bit indexed?! Does it exist?
if (mHeader.mChannels == 1 && mHeader.mBits == 8) {
return IndexedImageTypeSpecifier.createFromIndexColorModel(mColorData.getIndexColorModel());
return IndexedImageTypeSpecifier.createFromIndexColorModel(mMetadata.mColorData.getIndexColorModel());
}
throw new IIOException(
@@ -185,6 +193,11 @@ public class PSDImageReader extends ImageReaderBase {
throw new IIOException(
String.format("Unsupported channel count/bit depth for CMYK PSD: %d channels/%d bits", mHeader.mChannels, mHeader.mBits)
);
case PSD.COLOR_MODE_MULTICHANNEL:
// TODO: Implement
case PSD.COLOR_MODE_LAB:
// TODO: Implement
default:
throw new IIOException(
String.format("Unsupported PSD MODE: %s (%d channels/%d bits)", mHeader.mMode, mHeader.mChannels, mHeader.mBits)
@@ -255,7 +268,7 @@ public class PSDImageReader extends ImageReaderBase {
if (mColorSpace == null) {
ICC_Profile profile = null;
for (PSDImageResource resource : mImageResources) {
for (PSDImageResource resource : mMetadata.mImageResources) {
if (resource instanceof ICCProfile) {
profile = ((ICCProfile) resource).getProfile();
break;
@@ -322,22 +335,24 @@ public class PSDImageReader extends ImageReaderBase {
processImageStarted(pIndex);
int[] offsets = null;
int[] byteCounts = null;
int compression = mImageInput.readShort();
// TODO: Need to make sure compression is set in metadata, even without reading the image data!
mMetadata.mCompression = compression;
switch (compression) {
case PSD.COMPRESSION_NONE:
break;
case PSD.COMPRESSION_RLE:
// NOTE: Offsets will allow us to easily skip rows before AOI
offsets = new int[mHeader.mChannels * mHeader.mHeight];
for (int i = 0; i < offsets.length; i++) {
offsets[i] = mImageInput.readUnsignedShort();
// NOTE: Byte counts will allow us to easily skip rows before AOI
byteCounts = new int[mHeader.mChannels * mHeader.mHeight];
for (int i = 0; i < byteCounts.length; i++) {
byteCounts[i] = mImageInput.readUnsignedShort();
}
break;
case PSD.COMPRESSION_ZIP:
// TODO: Could probably use the ZIPDecoder (DeflateDecoder) here..
case PSD.COMPRESSION_ZIP_PREDICTON:
case PSD.COMPRESSION_ZIP_PREDICTION:
// TODO: Need to find out if the normal java.util.zip can handle this...
// Could be same as PNG prediction? Read up...
throw new IIOException("ZIP compression not supported yet");
@@ -351,7 +366,7 @@ public class PSDImageReader extends ImageReaderBase {
}
// What we read here is the "composite layer" of the PSD file
readImageData(image, rawType.getColorModel(), source, dest, xSub, ySub, offsets, compression);
readImageData(image, rawType.getColorModel(), source, dest, xSub, ySub, byteCounts, compression);
if (abortRequested()) {
processReadAborted();
@@ -366,7 +381,7 @@ public class PSDImageReader extends ImageReaderBase {
private void readImageData(final BufferedImage pImage,
final ColorModel pSourceCM, final Rectangle pSource, final Rectangle pDest,
final int pXSub, final int pYSub,
final int[] pOffsets, final int pCompression) throws IOException {
final int[] pByteCounts, final int pCompression) throws IOException {
final WritableRaster raster = pImage.getRaster();
// TODO: Conversion if destination cm is not compatible
@@ -388,24 +403,24 @@ public class PSDImageReader extends ImageReaderBase {
DataBufferByte buffer1 = (DataBufferByte) raster.getDataBuffer();
byte[] data1 = banded ? buffer1.getData(c) : buffer1.getData();
read1bitChannel(c, data1, interleavedBands, bandOffset, pSourceCM, row1, pSource, pDest, pXSub, pYSub, pOffsets, pCompression == PSD.COMPRESSION_RLE);
read1bitChannel(c, mHeader.mChannels, data1, interleavedBands, bandOffset, pSourceCM, row1, pSource, pDest, pXSub, pYSub, mHeader.mWidth, mHeader.mHeight, pByteCounts, pCompression == PSD.COMPRESSION_RLE);
break;
case 8:
byte[] row8 = ((DataBufferByte) rowRaster.getDataBuffer()).getData();
DataBufferByte buffer8 = (DataBufferByte) raster.getDataBuffer();
byte[] data8 = banded ? buffer8.getData(c) : buffer8.getData();
read8bitChannel(c, data8, interleavedBands, bandOffset, pSourceCM, row8, pSource, pDest, pXSub, pYSub, pOffsets, pCompression == PSD.COMPRESSION_RLE);
read8bitChannel(c, mHeader.mChannels, data8, interleavedBands, bandOffset, pSourceCM, row8, pSource, pDest, pXSub, pYSub, mHeader.mWidth, mHeader.mHeight, pByteCounts, c * mHeader.mHeight, pCompression == PSD.COMPRESSION_RLE);
break;
case 16:
short[] row16 = ((DataBufferUShort) rowRaster.getDataBuffer()).getData();
DataBufferUShort buffer16 = (DataBufferUShort) raster.getDataBuffer();
short[] data16 = banded ? buffer16.getData(c) : buffer16.getData();
read16bitChannel(c, data16, interleavedBands, bandOffset, pSourceCM, row16, pSource, pDest, pXSub, pYSub, pOffsets, pCompression == PSD.COMPRESSION_RLE);
read16bitChannel(c, mHeader.mChannels, data16, interleavedBands, bandOffset, pSourceCM, row16, pSource, pDest, pXSub, pYSub, mHeader.mWidth, mHeader.mHeight, pByteCounts, c * mHeader.mHeight, pCompression == PSD.COMPRESSION_RLE);
break;
default:
throw new IIOException("Unknown PSD bit depth: " + mHeader.mBits);
throw new IIOException(String.format("Unknown PSD bit depth: %s", mHeader.mBits));
}
if (abortRequested()) {
@@ -419,19 +434,22 @@ public class PSDImageReader extends ImageReaderBase {
}
}
private void read16bitChannel(final int pChannel,
private void read16bitChannel(final int pChannel, final int pChannelCount,
final short[] pData, final int pBands, final int pBandOffset,
final ColorModel pSourceColorModel,
final short[] pRow,
final Rectangle pSource, final Rectangle pDest, final int pXSub, final int pYSub,
final int[] pRowOffsets, final boolean pRLECompressed) throws IOException {
final Rectangle pSource, final Rectangle pDest,
final int pXSub, final int pYSub,
final int pChannelWidth, final int pChannelHeight,
final int[] pRowByteCounts, final int pRowOffset,
final boolean pRLECompressed) throws IOException {
final boolean isCMYK = pSourceColorModel.getColorSpace().getType() == ColorSpace.TYPE_CMYK;
final int colorComponents = pSourceColorModel.getColorSpace().getNumComponents();
for (int y = 0; y < mHeader.mHeight; y++) {
for (int y = 0; y < pChannelHeight; y++) {
// NOTE: Length is in *16 bit values* (shorts)
int length = 2 * (pRLECompressed ? pRowOffsets[pChannel * mHeader.mHeight + y] : mHeader.mWidth);
int length = 2 * (pRLECompressed ? pRowByteCounts[pRowOffset + y] : pChannelWidth);
// TODO: Sometimes need to read the line y == source.y + source.height...
// Read entire line, if within source region and sampling
@@ -439,7 +457,7 @@ public class PSDImageReader extends ImageReaderBase {
if (pRLECompressed) {
DataInputStream input = PSDUtil.createPackBitsStream(mImageInput, length);
try {
for (int x = 0; x < mHeader.mWidth; x++) {
for (int x = 0; x < pChannelWidth; x++) {
pRow[x] = input.readShort();
}
}
@@ -448,7 +466,7 @@ public class PSDImageReader extends ImageReaderBase {
}
}
else {
mImageInput.readFully(pRow, 0, mHeader.mWidth);
mImageInput.readFully(pRow, 0, pChannelWidth);
}
// TODO: Destination offset...??
@@ -472,22 +490,25 @@ public class PSDImageReader extends ImageReaderBase {
if (abortRequested()) {
break;
}
processImageProgress((pChannel * y * 100) / mHeader.mChannels * mHeader.mHeight);
processImageProgress((pChannel * y * 100) / pChannelCount * pChannelHeight);
}
}
private void read8bitChannel(final int pChannel,
private void read8bitChannel(final int pChannel, final int pChannelCount,
final byte[] pData, final int pBands, final int pBandOffset,
final ColorModel pSourceColorModel,
final byte[] pRow,
final Rectangle pSource, final Rectangle pDest, final int pXSub, final int pYSub,
final int[] pRowOffsets, final boolean pRLECompressed) throws IOException {
final Rectangle pSource, final Rectangle pDest,
final int pXSub, final int pYSub,
final int pChannelWidth, final int pChannelHeight,
final int[] pRowByteCounts, final int pRowOffset,
final boolean pRLECompressed) throws IOException {
final boolean isCMYK = pSourceColorModel.getColorSpace().getType() == ColorSpace.TYPE_CMYK;
final int colorComponents = pSourceColorModel.getColorSpace().getNumComponents();
for (int y = 0; y < mHeader.mHeight; y++) {
int length = pRLECompressed ? pRowOffsets[pChannel * mHeader.mHeight + y] : mHeader.mWidth;
for (int y = 0; y < pChannelHeight; y++) {
int length = pRLECompressed ? pRowByteCounts[pRowOffset + y] : pChannelWidth;
// TODO: Sometimes need to read the line y == source.y + source.height...
// Read entire line, if within source region and sampling
@@ -495,14 +516,14 @@ public class PSDImageReader extends ImageReaderBase {
if (pRLECompressed) {
DataInputStream input = PSDUtil.createPackBitsStream(mImageInput, length);
try {
input.readFully(pRow, 0, mHeader.mWidth);
input.readFully(pRow, 0, pChannelWidth);
}
finally {
input.close();
}
}
else {
mImageInput.readFully(pRow, 0, mHeader.mWidth);
mImageInput.readFully(pRow, 0, pChannelWidth);
}
// TODO: If banded and not sub sampling/cmyk, we could just copy using System.arraycopy
@@ -527,22 +548,25 @@ public class PSDImageReader extends ImageReaderBase {
if (abortRequested()) {
break;
}
processImageProgress((pChannel * y * 100) / mHeader.mChannels * mHeader.mHeight);
processImageProgress((pChannel * y * 100) / pChannelCount * pChannelHeight);
}
}
private void read1bitChannel(final int pChannel,
@SuppressWarnings({"UnusedDeclaration"})
private void read1bitChannel(final int pChannel, final int pChannelCount,
final byte[] pData, final int pBands, final int pBandOffset,
final ColorModel pSourceColorModel,
final byte[] pRow,
final Rectangle pSource, final Rectangle pDest, final int pXSub, final int pYSub,
final int[] pRowOffsets, boolean pRLECompressed) throws IOException {
final Rectangle pSource, final Rectangle pDest,
final int pXSub, final int pYSub,
final int pChannelWidth, final int pChannelHeight,
final int[] pRowByteCounts, boolean pRLECompressed) throws IOException {
// NOTE: 1 bit channels only occurs once
final int destWidth = (pDest.width + 7) / 8;
for (int y = 0; y < mHeader.mHeight; y++) {
int length = pRLECompressed ? pRowOffsets[y] : mHeader.mWidth;
for (int y = 0; y < pChannelHeight; y++) {
int length = pRLECompressed ? pRowByteCounts[y] : pChannelWidth;
// TODO: Sometimes need to read the line y == source.y + source.height...
// Read entire line, if within source region and sampling
@@ -602,7 +626,7 @@ public class PSDImageReader extends ImageReaderBase {
if (abortRequested()) {
break;
}
processImageProgress((pChannel * y * 100) / mHeader.mChannels * mHeader.mHeight);
processImageProgress((pChannel * y * 100) / pChannelCount * pChannelHeight);
}
}
@@ -675,6 +699,9 @@ public class PSDImageReader extends ImageReaderBase {
if (mHeader == null) {
mHeader = new PSDHeader(mImageInput);
mMetadata = new PSDMetadata();
mMetadata.mHeader = mHeader;
/*
Contains the required data to define the color mode.
@@ -687,9 +714,10 @@ public class PSDImageReader extends ImageReaderBase {
around as a black box for use when saving the file.
*/
if (mHeader.mMode == PSD.COLOR_MODE_INDEXED) {
mColorData = new PSDColorData(mImageInput);
mMetadata.mColorData = new PSDColorData(mImageInput);
}
else {
// TODO: We need to store the duotone spec if we decide to create a writer...
// Skip color mode data for other modes
long length = mImageInput.readUnsignedInt();
mImageInput.skipBytes(length);
@@ -701,6 +729,7 @@ public class PSDImageReader extends ImageReaderBase {
}
// TODO: Flags or list of interesting resources to parse
// TODO: Obey ignoreMetadata
private void readImageResources(final boolean pParseData) throws IOException {
// TODO: Avoid unnecessary stream repositioning
long pos = mImageInput.getFlushedPosition();
@@ -709,14 +738,14 @@ public class PSDImageReader extends ImageReaderBase {
long length = mImageInput.readUnsignedInt();
if (pParseData && length > 0) {
if (mImageResources == null) {
mImageResources = new ArrayList<PSDImageResource>();
if (mMetadata.mImageResources == null) {
mMetadata.mImageResources = new ArrayList<PSDImageResource>();
long expectedEnd = mImageInput.getStreamPosition() + length;
while (mImageInput.getStreamPosition() < expectedEnd) {
// TODO: Have PSDImageResources defer actual parsing? (Just store stream offsets)
PSDImageResource resource = PSDImageResource.read(mImageInput);
mImageResources.add(resource);
mMetadata.mImageResources.add(resource);
}
if (mImageInput.getStreamPosition() != expectedEnd) {
@@ -728,6 +757,8 @@ public class PSDImageReader extends ImageReaderBase {
mImageInput.seek(pos + length + 4);
}
// TODO: Flags or list of interesting resources to parse
// TODO: Obey ignoreMetadata
private void readLayerAndMaskInfo(final boolean pParseData) throws IOException {
// TODO: Make sure we are positioned correctly
long length = mImageInput.readUnsignedInt();
@@ -743,63 +774,32 @@ public class PSDImageReader extends ImageReaderBase {
*/
// TODO: Figure out what the last part of that sentence means in practice...
int layers = mImageInput.readShort();
// System.out.println("layers: " + layers);
PSDLayerInfo[] layerInfo = new PSDLayerInfo[Math.abs(layers)];
for (int i = 0; i < layerInfo.length; i++) {
layerInfo[i] = new PSDLayerInfo(mImageInput);
// System.out.println("layerInfo[" + i + "]: " + layerInfo[i]);
PSDLayerInfo[] layerInfos = new PSDLayerInfo[Math.abs(layers)];
for (int i = 0; i < layerInfos.length; i++) {
layerInfos[i] = new PSDLayerInfo(mImageInput);
}
mLayerInfo = Arrays.asList(layerInfo);
mMetadata.mLayerInfo = Arrays.asList(layerInfos);
for (PSDLayerInfo info : layerInfo) {
for (PSDChannelInfo channelInfo : info.mChannelInfo) {
int compression = mImageInput.readUnsignedShort();
// 0: None, 1: PackBits RLE, 2: Zip, 3: Zip w/prediction
switch (compression) {
case PSD.COMPRESSION_NONE:
// System.out.println("Compression: None");
break;
case PSD.COMPRESSION_RLE:
// System.out.println("Compression: PackBits RLE");
break;
case PSD.COMPRESSION_ZIP:
// System.out.println("Compression: ZIP");
break;
case PSD.COMPRESSION_ZIP_PREDICTON:
// System.out.println("Compression: ZIP with prediction");
break;
default:
// TODO: Do we care, as we can just skip the data?
// We could issue a warning to the warning listener
throw new IIOException(String.format(
"Unknown PSD compression: %d. Expected 0 (none), 1 (RLE), 2 (ZIP) or 3 (ZIP w/prediction).",
compression
));
}
// TODO: Clean-up
mImageInput.mark();
ImageTypeSpecifier raw = getRawImageTypeInternal(0);
ImageTypeSpecifier imageType = getImageTypes(0).next();
mImageInput.reset();
// TODO: If RLE, the the image data starts with the byte counts
// for all the scan lines in the channel (LayerBottom*LayerTop), with
// each count stored as a two*byte value.
// if (compression == 1) {
// mImageInput.skipBytes(channelInfo.mLength);
// }
for (PSDLayerInfo layerInfo : layerInfos) {
// TODO: If not explicitly needed, skip layers...
BufferedImage layer = readLayerData(layerInfo, raw, imageType);
// TODO: Read channel image data (same format as composite image channel data)
mImageInput.skipBytes(channelInfo.mLength - 2);
// if (channelInfo.mLength % 2 != 0) {
// mImageInput.readByte();
// }
}
// TODO: Don't show! Store in meta data somehow...
// if (layer != null) {
// showIt(layer, layerInfo.mLayerName + " " + layerInfo.mBlendMode.toString());
// }
}
// TODO: We seem to have some alignment issues here...
// I'm always reading two bytes off..
long read = mImageInput.getStreamPosition() - pos;
// System.out.println("layerInfoLength: " + layerInfoLength);
// System.out.println("layer info read: " + (read - 4)); // - 4 for the layerInfoLength field itself
long diff = layerInfoLength - (read - 4);
long diff = layerInfoLength - (read - 4); // - 4 for the layerInfoLength field itself
// System.out.println("diff: " + diff);
mImageInput.skipBytes(diff);
@@ -808,7 +808,8 @@ public class PSDImageReader extends ImageReaderBase {
long layerMaskInfoLength = mImageInput.readUnsignedInt();
// System.out.println("GlobalLayerMaskInfo length: " + layerMaskInfoLength);
if (layerMaskInfoLength > 0) {
mGlobalLayerMask = new PSDGlobalLayerMask(mImageInput);
mMetadata.mGlobalLayerMask = new PSDGlobalLayerMask(mImageInput);
// System.out.println("mGlobalLayerMask: " + mGlobalLayerMask);
}
read = mImageInput.getStreamPosition() - pos;
@@ -818,10 +819,197 @@ public class PSDImageReader extends ImageReaderBase {
mImageInput.skipBytes(toSkip);
}
else {
// Skip entire layer and mask section
mImageInput.skipBytes(length);
}
}
private BufferedImage readLayerData(final PSDLayerInfo pLayerInfo, final ImageTypeSpecifier pRawType, final ImageTypeSpecifier pImageType) throws IOException {
final int width = pLayerInfo.mRight - pLayerInfo.mLeft;
final int height = pLayerInfo.mBottom - pLayerInfo.mTop;
// Even if raw/imageType has no alpha, the layers may still have alpha...
ImageTypeSpecifier imageType = getImageTypeForLayer(pImageType, pLayerInfo);
// Create image (or dummy, if h/w are <= 0)
BufferedImage layer = imageType.createBufferedImage(Math.max(1, width), Math.max(1, height));
// Source/destination area
Rectangle area = new Rectangle(width, height);
final int xsub = 1;
final int ysub = 1;
final WritableRaster raster = layer.getRaster();
// TODO: Conversion if destination cm is not compatible
final ColorModel destCM = layer.getColorModel();
// TODO: This raster is 3-5 times longer than needed, depending on number of channels...
ColorModel sourceCM = pRawType.getColorModel();
final WritableRaster rowRaster = width > 0 ? sourceCM.createCompatibleWritableRaster(width, 1) : null;
// final int channels = rowRaster.getNumBands();
final boolean banded = raster.getDataBuffer().getNumBanks() > 1;
final int interleavedBands = banded ? 1 : raster.getNumBands();
for (PSDChannelInfo channelInfo : pLayerInfo.mChannelInfo) {
int compression = mImageInput.readUnsignedShort();
// Skip layer if we can't read it
// channelId == -2 means "user supplied layer mask", whatever that is...
if (width <= 0 || height <= 0 || channelInfo.mChannelId == -2 ||
(compression != PSD.COMPRESSION_NONE && compression != PSD.COMPRESSION_RLE)) {
mImageInput.skipBytes(channelInfo.mLength - 2);
}
else {
// 0 = red, 1 = green, etc
// -1 = transparency mask; -2 = user supplied layer mask
int c = channelInfo.mChannelId == -1 ? pLayerInfo.mChannelInfo.length - 1 : channelInfo.mChannelId;
// NOTE: For layers, byte counts are written per channel, while for the composite data
// byte counts are written for all channels before the image data.
// This is the reason for the current code duplication
int[] byteCounts = null;
// 0: None, 1: PackBits RLE, 2: Zip, 3: Zip w/prediction
switch (compression) {
case PSD.COMPRESSION_NONE:
break;
case PSD.COMPRESSION_RLE:
// If RLE, the the image data starts with the byte counts
// for all the scan lines in the channel (LayerBottom-LayerTop), with
// each count stored as a two*byte value.
byteCounts = new int[pLayerInfo.mBottom - pLayerInfo.mTop];
for (int i = 0; i < byteCounts.length; i++) {
byteCounts[i] = mImageInput.readUnsignedShort();
}
break;
case PSD.COMPRESSION_ZIP:
case PSD.COMPRESSION_ZIP_PREDICTION:
default:
// Explicitly skipped above
throw new AssertionError(String.format("Unsupported layer data. Compression: %d", compression));
}
int bandOffset = banded ? 0 : interleavedBands - 1 - c;
switch (mHeader.mBits) {
case 1:
byte[] row1 = ((DataBufferByte) rowRaster.getDataBuffer()).getData();
DataBufferByte buffer1 = (DataBufferByte) raster.getDataBuffer();
byte[] data1 = banded ? buffer1.getData(c) : buffer1.getData();
read1bitChannel(c, imageType.getNumBands(), data1, interleavedBands, bandOffset, sourceCM, row1, area, area, xsub, ysub, width, height, byteCounts, compression == PSD.COMPRESSION_RLE);
break;
case 8:
byte[] row8 = ((DataBufferByte) rowRaster.getDataBuffer()).getData();
DataBufferByte buffer8 = (DataBufferByte) raster.getDataBuffer();
byte[] data8 = banded ? buffer8.getData(c) : buffer8.getData();
read8bitChannel(c, imageType.getNumBands(), data8, interleavedBands, bandOffset, sourceCM, row8, area, area, xsub, ysub, width, height, byteCounts, 0, compression == PSD.COMPRESSION_RLE);
break;
case 16:
short[] row16 = ((DataBufferUShort) rowRaster.getDataBuffer()).getData();
DataBufferUShort buffer16 = (DataBufferUShort) raster.getDataBuffer();
short[] data16 = banded ? buffer16.getData(c) : buffer16.getData();
read16bitChannel(c, imageType.getNumBands(), data16, interleavedBands, bandOffset, sourceCM, row16, area, area, xsub, ysub, width, height, byteCounts, 0, compression == PSD.COMPRESSION_RLE);
break;
default:
throw new IIOException(String.format("Unknown PSD bit depth: %s", mHeader.mBits));
}
if (abortRequested()) {
break;
}
}
}
return layer;
}
private ImageTypeSpecifier getImageTypeForLayer(final ImageTypeSpecifier pOriginal, final PSDLayerInfo pLayerInfo) {
// If layer has more channels than composite data, it's normally extra alpha...
if (pLayerInfo.mChannelInfo.length > pOriginal.getNumBands()) {
// ...but, it could also be just the user mask...
boolean userMask = false;
for (PSDChannelInfo channelInfo : pLayerInfo.mChannelInfo) {
if (channelInfo.mChannelId == -2) {
userMask = true;
break;
}
}
int newBandNum = pLayerInfo.mChannelInfo.length - (userMask ? 1 : 0);
// If there really is more channels, then create new imageTypeSpec
if (newBandNum > pOriginal.getNumBands()) {
int[] offs = new int[newBandNum];
for (int i = 0, offsLength = offs.length; i < offsLength; i++) {
offs[i] = offsLength - i;
}
return ImageTypeSpecifier.createInterleaved(pOriginal.getColorModel().getColorSpace(), offs, pOriginal.getSampleModel().getDataType(), true, false);
}
}
return pOriginal;
}
/// Layer support
// TODO: For now, leave as Metadata
/*
int getNumLayers(int pImageIndex) throws IOException;
boolean hasLayers(int pImageIndex) throws IOException;
BufferedImage readLayer(int pImageIndex, int pLayerIndex, ImageReadParam pParam) throws IOException;
int getLayerWidth(int pImageIndex, int pLayerIndex) throws IOException;
int getLayerHeight(int pImageIndex, int pLayerIndex) throws IOException;
// ?
Point getLayerOffset(int pImageIndex, int pLayerIndex) throws IOException;
*/
/// Metadata support
// TODO
@Override
public IIOMetadata getStreamMetadata() throws IOException {
// null might be appropriate here
// "For image formats that contain a single image, only image metadata is used."
return super.getStreamMetadata();
}
@Override
public IIOMetadata getImageMetadata(final int pImageIndex) throws IOException {
// TODO: Implement
checkBounds(pImageIndex);
readHeader();
readImageResources(true);
readLayerAndMaskInfo(true);
// TODO: Need to make sure compression is set in metadata, even without reading the image data!
mMetadata.mCompression = mImageInput.readShort();
// mMetadata.mHeader = mHeader;
// mMetadata.mColorData = mColorData;
// mMetadata.mImageResources = mImageResources;
return mMetadata; // TODO: clone if we change to mutable metadata
}
@Override
public IIOMetadata getImageMetadata(final int imageIndex, final String formatName, final Set<String> nodeNames) throws IOException {
// TODO: It might make sense to overload this, as there's loads of meta data in the file
return super.getImageMetadata(imageIndex, formatName, nodeNames);
}
/// Thumbnail support
@Override
public boolean readerSupportsThumbnails() {
@@ -835,14 +1023,14 @@ public class PSDImageReader extends ImageReaderBase {
List<PSDThumbnail> thumbnails = null;
if (mImageResources == null) {
if (mMetadata.mImageResources == null) {
// TODO: Need flag here, to specify what resources to read...
readImageResources(true);
// TODO: Skip this, requires storing some stream offsets
readLayerAndMaskInfo(false);
}
for (PSDImageResource resource : mImageResources) {
for (PSDImageResource resource : mMetadata.mImageResources) {
if (resource instanceof PSDThumbnail) {
if (thumbnails == null) {
thumbnails = new ArrayList<PSDThumbnail>();
@@ -856,13 +1044,13 @@ public class PSDImageReader extends ImageReaderBase {
}
@Override
public int getNumThumbnails(int pIndex) throws IOException {
public int getNumThumbnails(final int pIndex) throws IOException {
List<PSDThumbnail> thumbnails = getThumbnailResources(pIndex);
return thumbnails == null ? 0 : thumbnails.size();
}
private PSDThumbnail getThumbnailResource(int pImageIndex, int pThumbnailIndex) throws IOException {
private PSDThumbnail getThumbnailResource(final int pImageIndex, final int pThumbnailIndex) throws IOException {
List<PSDThumbnail> thumbnails = getThumbnailResources(pImageIndex);
if (thumbnails == null) {
@@ -873,17 +1061,17 @@ public class PSDImageReader extends ImageReaderBase {
}
@Override
public int getThumbnailWidth(int pImageIndex, int pThumbnailIndex) throws IOException {
public int getThumbnailWidth(final int pImageIndex, final int pThumbnailIndex) throws IOException {
return getThumbnailResource(pImageIndex, pThumbnailIndex).getWidth();
}
@Override
public int getThumbnailHeight(int pImageIndex, int pThumbnailIndex) throws IOException {
public int getThumbnailHeight(final int pImageIndex, final int pThumbnailIndex) throws IOException {
return getThumbnailResource(pImageIndex, pThumbnailIndex).getHeight();
}
@Override
public BufferedImage readThumbnail(int pImageIndex, int pThumbnailIndex) throws IOException {
public BufferedImage readThumbnail(final int pImageIndex, final int pThumbnailIndex) throws IOException {
// TODO: Thumbnail progress listeners...
PSDThumbnail thumbnail = getThumbnailResource(pImageIndex, pThumbnailIndex);
@@ -892,6 +1080,7 @@ public class PSDImageReader extends ImageReaderBase {
processThumbnailStarted(pImageIndex, pThumbnailIndex);
processThumbnailComplete();
// TODO: Returning a cached mutable thumbnail is not really safe...
return thumbnail.getThumbnail();
}
@@ -938,15 +1127,32 @@ public class PSDImageReader extends ImageReaderBase {
File file = new File(pArgs[idx]);
ImageInputStream stream = ImageIO.createImageInputStream(file);
imageReader.setInput(stream);
imageReader.readHeader();
System.out.println("imageReader.mHeader: " + imageReader.mHeader);
// System.out.println("imageReader.mHeader: " + imageReader.mHeader);
imageReader.readImageResources(true);
System.out.println("imageReader.mImageResources: " + imageReader.mImageResources);
System.out.println("imageReader.mImageResources: " + imageReader.mMetadata.mImageResources);
System.out.println();
imageReader.readLayerAndMaskInfo(true);
System.out.println("imageReader.mLayerInfo: " + imageReader.mLayerInfo);
System.out.println("imageReader.mGlobalLayerMask: " + imageReader.mGlobalLayerMask);
System.out.println("imageReader.mLayerInfo: " + imageReader.mMetadata.mLayerInfo);
// System.out.println("imageReader.mGlobalLayerMask: " + imageReader.mGlobalLayerMask);
System.out.println();
IIOMetadata metadata = imageReader.getImageMetadata(0);
Node node;
XMLSerializer serializer;
node = metadata.getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName);
serializer = new XMLSerializer(System.out, System.getProperty("file.encoding"));
serializer.setIndentation(" ");
serializer.serialize(node, true);
System.out.println();
node = metadata.getAsTree(PSDMetadata.NATIVE_METADATA_FORMAT_NAME);
// serializer = new XMLSerializer(System.out, System.getProperty("file.encoding"));
serializer.serialize(node, true);
if (imageReader.hasThumbnails(0)) {
int thumbnails = imageReader.getNumThumbnails(0);
@@ -28,9 +28,12 @@
package com.twelvemonkeys.imageio.plugins.psd;
import com.twelvemonkeys.imageio.spi.ProviderInfo;
import com.twelvemonkeys.imageio.util.IIOUtil;
import javax.imageio.ImageReader;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream;
import javax.imageio.ImageReader;
import java.io.IOException;
import java.util.Locale;
@@ -44,28 +47,36 @@ import java.util.Locale;
public class PSDImageReaderSpi extends ImageReaderSpi {
/**
* Creates an PSDImageReaderSpi
* Creates a {@code PSDImageReaderSpi}.
*/
public PSDImageReaderSpi() {
this(IIOUtil.getProviderInfo(PSDImageReaderSpi.class));
}
private PSDImageReaderSpi(final ProviderInfo pProviderInfo) {
super(
"TwelveMonkeys",
"2.0",
pProviderInfo.getVendorName(),
pProviderInfo.getVersion(),
new String[]{"psd", "PSD"},
new String[]{"psd"},
new String[]{
"application/vnd.adobe.photoshop", // This one seems official, used in XMP
"application/vnd.adobe.photoshop", // This one seems official, used in XMP
"image/x-psd", "application/x-photoshop", "image/x-photoshop"
},
"com.twelvemkonkeys.imageio.plugins.psd.PSDImageReader",
STANDARD_INPUT_TYPE,
// new String[]{"com.twelvemkonkeys.imageio.plugins.psd.PSDImageWriterSpi"},
null,
true, null, null, null, null,
true, null, null, null, null
true, // supports standard stream metadata
null, null, // native stream format name and class
null, null, // extra stream formats
true, // supports standard image metadata
PSDMetadata.NATIVE_METADATA_FORMAT_NAME, PSDMetadata.NATIVE_METADATA_FORMAT_CLASS_NAME,
null, null // extra image metadata formats
);
}
public boolean canDecodeInput(Object pSource) throws IOException {
public boolean canDecodeInput(final Object pSource) throws IOException {
if (!(pSource instanceof ImageInputStream)) {
return false;
}
@@ -82,11 +93,11 @@ public class PSDImageReaderSpi extends ImageReaderSpi {
}
}
public ImageReader createReaderInstance(Object pExtension) throws IOException {
public ImageReader createReaderInstance(final Object pExtension) throws IOException {
return new PSDImageReader(this);
}
public String getDescription(Locale pLocale) {
public String getDescription(final Locale pLocale) {
return "Adobe Photoshop Document (PSD) image reader";
}
}
@@ -28,6 +28,9 @@
package com.twelvemonkeys.imageio.plugins.psd;
import com.twelvemonkeys.imageio.stream.SubImageInputStream;
import com.twelvemonkeys.lang.StringUtil;
import javax.imageio.stream.ImageInputStream;
import javax.imageio.IIOException;
import java.io.IOException;
@@ -41,6 +44,9 @@ import java.lang.reflect.Field;
* @version $Id: PSDImageResource.java,v 1.0 Apr 29, 2008 5:49:06 PM haraldk Exp$
*/
class PSDImageResource {
// TODO: Refactor image resources to separate package
// TODO: Change constructor to store stream offset and length only (+ possibly the name), defer reading
final short mId;
final String mName;
final long mSize;
@@ -50,10 +56,23 @@ class PSDImageResource {
mName = PSDUtil.readPascalString(pInput);
mSize = pInput.readUnsignedInt();
readData(pInput);
// Skip pad
int nameSize = mName.length() + 1;
if (nameSize % 2 != 0) {
pInput.readByte();
}
// Data is even-padded
mSize = pInput.readUnsignedInt();
long startPos = pInput.getStreamPosition();
readData(new SubImageInputStream(pInput, mSize));
// NOTE: This should never happen, however it's safer to keep it here to
if (pInput.getStreamPosition() != startPos + mSize) {
pInput.seek(startPos + mSize);
}
// Data is even-padded (word aligned)
if (mSize % 2 != 0) {
pInput.read();
}
@@ -84,7 +103,10 @@ class PSDImageResource {
protected StringBuilder toStringBuilder() {
StringBuilder builder = new StringBuilder(getClass().getSimpleName());
builder.append(resourceTypeForId(mId));
String fakeType = resourceTypeForId(mId);
if (fakeType != null) {
builder.append("(").append(fakeType).append(")");
}
builder.append("[ID: 0x");
builder.append(Integer.toHexString(mId));
@@ -103,26 +125,32 @@ class PSDImageResource {
case PSD.RES_ALPHA_CHANNEL_INFO:
case PSD.RES_DISPLAY_INFO:
case PSD.RES_PRINT_FLAGS:
case PSD.RES_IPTC_NAA:
case PSD.RES_GRID_AND_GUIDES_INFO:
case PSD.RES_THUMBNAIL_PS4:
case PSD.RES_THUMBNAIL:
case PSD.RES_ICC_PROFILE:
case PSD.RES_VERSION_INFO:
case PSD.RES_EXIF_DATA_1:
// case PSD.RES_EXIF_DATA_3:
case PSD.RES_XMP_DATA:
case PSD.RES_PRINT_SCALE:
case PSD.RES_PIXEL_ASPECT_RATIO:
case PSD.RES_PRINT_FLAGS_INFORMATION:
return "";
return null;
default:
try {
for (Field field : PSD.class.getDeclaredFields()) {
if (field.getName().startsWith("RES_") && field.getInt(null) == pId) {
return "(" + field.getName().substring(4) + ")";
String name = field.getName().substring(4);
return StringUtil.lispToCamel(name.replace("_", "-").toLowerCase(), true);
}
}
}
catch (IllegalAccessException ignore) {
}
return "(unknown resource)";
return "UnknownResource";
}
}
@@ -144,15 +172,27 @@ class PSDImageResource {
return new PSDDisplayInfo(id, pInput);
case PSD.RES_PRINT_FLAGS:
return new PSDPrintFlags(id, pInput);
case PSD.RES_IPTC_NAA:
return new PSDIPTCData(id, pInput);
case PSD.RES_GRID_AND_GUIDES_INFO:
return new PSDGridAndGuideInfo(id, pInput);
case PSD.RES_THUMBNAIL_PS4:
case PSD.RES_THUMBNAIL:
return new PSDThumbnail(id, pInput);
case PSD.RES_ICC_PROFILE:
return new ICCProfile(id, pInput);
case PSD.RES_UNICODE_ALPHA_NAMES:
return new PSDUnicodeAlphaNames(id, pInput);
case PSD.RES_VERSION_INFO:
return new PSDVersionInfo(id, pInput);
case PSD.RES_EXIF_DATA_1:
return new PSDEXIF1Data(id, pInput);
case PSD.RES_XMP_DATA:
return new PSDXMPData(id, pInput);
case PSD.RES_PRINT_SCALE:
return new PSDPrintScale(id, pInput);
case PSD.RES_PIXEL_ASPECT_RATIO:
return new PSDPixelAspectRatio(id, pInput);
case PSD.RES_PRINT_FLAGS_INFORMATION:
return new PSDPrintFlagsInformation(id, pInput);
default:
@@ -45,7 +45,7 @@ class PSDLayerBlendMode {
final int mClipping; // 0: base, 1: non-base
final int mFlags;
public PSDLayerBlendMode(ImageInputStream pInput) throws IOException {
public PSDLayerBlendMode(final ImageInputStream pInput) throws IOException {
int blendModeSig = pInput.readInt();
if (blendModeSig != PSD.RESOURCE_TYPE) { // TODO: Is this really just a resource?
throw new IIOException("Illegal PSD Blend Mode signature, expected 8BIM: " + PSDUtil.intToStr(blendModeSig));
@@ -68,40 +68,54 @@ class PSDLayerBlendMode {
builder.append("mode: \"").append(PSDUtil.intToStr(mBlendMode));
builder.append("\", opacity: ").append(mOpacity);
builder.append(", clipping: ").append(mClipping);
switch (mClipping) {
case 0:
builder.append(" (base)");
break;
case 1:
builder.append(" (non-base)");
break;
default:
builder.append(" (unknown)");
break;
}
builder.append(", flags: ").append(byteToBinary(mFlags));
// TODO: Maybe the flag bits have oposite order?
/*
bit 0 = transparency protected; bit 1 = visible; bit 2 = obsolete;
bit 3 = 1 for Photoshop 5.0 and later, tells if bit 4 has useful information;
bit 4 = pixel data irrelevant to appearance of document
*/
builder.append(" (");
if ((mFlags & 0x01) != 0) {
builder.append("Transp. protected ");
}
else {
builder.append("Transp. open");
builder.append("Transp. protected, ");
}
if ((mFlags & 0x02) != 0) {
builder.append(", Visible");
}
else {
builder.append(", Hidden");
builder.append("Hidden, ");
}
if ((mFlags & 0x04) != 0) {
builder.append(", Obsolete bit");
builder.append("Obsolete bit, ");
}
if ((mFlags & 0x08) != 0) {
builder.append(", Photoshop 5 data");
builder.append("PS 5.0 data present, "); // "tells if next bit has useful information"...
}
if ((mFlags & 0x10) != 0) {
builder.append(", Pixel data irrelevant");
builder.append("Pixel data irrelevant, ");
}
if ((mFlags & 0x20) != 0) {
builder.append(", Unknown bit 5");
builder.append("Unknown bit 5, ");
}
if ((mFlags & 0x40) != 0) {
builder.append(", Unknown bit 6");
builder.append("Unknown bit 6, ");
}
if ((mFlags & 0x80) != 0) {
builder.append(", Unknown bit 7");
builder.append("Unknown bit 7, ");
}
// Stupidity...
if (mFlags != 0) {
builder.delete(builder.length() - 2, builder.length());
}
builder.append(")");
builder.append("]");
@@ -41,16 +41,16 @@ import java.util.Arrays;
* @version $Id: PSDLayerInfo.java,v 1.0 Apr 29, 2008 6:01:12 PM haraldk Exp$
*/
class PSDLayerInfo {
private int mTop;
private int mLeft;
private int mBottom;
private int mRight;
final int mTop;
final int mLeft;
final int mBottom;
final int mRight;
PSDChannelInfo[] mChannelInfo;
private PSDLayerBlendMode mBlendMode;
private PSDLayerMaskData mLayerMaskData;
private PSDChannelSourceDestinationRange[] mRanges;
private String mLayerName;
final PSDChannelInfo[] mChannelInfo;
final PSDLayerBlendMode mBlendMode;
final PSDLayerMaskData mLayerMaskData;
final PSDChannelSourceDestinationRange[] mRanges;
final String mLayerName;
PSDLayerInfo(ImageInputStream pInput) throws IOException {
mTop = pInput.readInt();
@@ -80,6 +80,9 @@ class PSDLayerInfo {
if (layerMaskDataSize != 0) {
mLayerMaskData = new PSDLayerMaskData(pInput, layerMaskDataSize);
}
else {
mLayerMaskData = null;
}
int layerBlendingDataSize = pInput.readInt();
if (layerBlendingDataSize % 8 != 0) {
@@ -95,14 +98,12 @@ class PSDLayerInfo {
mLayerName = PSDUtil.readPascalString(pInput);
int layerNameSize = mLayerName.length() + 1;
// readPascalString has already read pad byte for word alignment
if (layerNameSize % 2 != 0) {
layerNameSize++;
}
// Skip two more pad bytes if needed
// Skip pad bytes for long word alignment
if (layerNameSize % 4 != 0) {
pInput.skipBytes(2);
layerNameSize += 2;
int skip = layerNameSize % 4;
pInput.skipBytes(skip);
layerNameSize += skip;
}
// TODO: There's some data skipped here...
@@ -0,0 +1,759 @@
package com.twelvemonkeys.imageio.plugins.psd;
import com.twelvemonkeys.imageio.metadata.Directory;
import com.twelvemonkeys.imageio.metadata.Entry;
import com.twelvemonkeys.imageio.metadata.exif.TIFF;
import com.twelvemonkeys.imageio.metadata.iptc.IPTC;
import com.twelvemonkeys.lang.StringUtil;
import com.twelvemonkeys.util.FilterIterator;
import org.w3c.dom.Node;
import javax.imageio.metadata.IIOMetadataNode;
import java.awt.image.IndexColorModel;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
/**
* PSDMetadata
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: PSDMetadata.java,v 1.0 Nov 4, 2009 5:28:12 PM haraldk Exp$
*/
public final class PSDMetadata extends AbstractMetadata {
// TODO: Decide on image/stream metadata...
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";
PSDHeader mHeader;
PSDColorData mColorData;
int mCompression = -1;
List<PSDImageResource> mImageResources;
PSDGlobalLayerMask mGlobalLayerMask;
List<PSDLayerInfo> mLayerInfo;
static final String[] COLOR_MODES = {
"MONOCHROME", "GRAYSCALE", "INDEXED", "RGB", "CMYK", null, null, "MULTICHANNEL", "DUOTONE", "LAB"
};
static final String[] DISPLAY_INFO_CS = {
"RGB", "HSB", "CMYK", "PANTONE", "FOCOLTONE", "TRUMATCH", "TOYO", "LAB", "GRAYSCALE", null, "HKS", "DIC",
null, // TODO: ... (until index 2999),
"ANPA"
};
static final String[] DISPLAY_INFO_KINDS = {"selected", "protected"};
static final String[] RESOLUTION_UNITS = {null, "pixels/inch", "pixels/cm"};
static final String[] DIMENSION_UNITS = {null, "in", "cm", "pt", "picas", "columns"};
static final String[] JAVA_CS = {
"XYZ", "Lab", "Yuv", "YCbCr", "Yxy", "RGB", "GRAY", "HSV", "HLS", "CMYK", "CMY",
"2CLR", "3CLR", "4CLR", "5CLR", "6CLR", "7CLR", "8CLR", "9CLR", "ACLR", "BCLR", "CCLR", "DCLR", "ECLR", "FCLR"
};
static final String[] GUIDE_ORIENTATIONS = {"vertical", "horizontal"};
static final String[] PRINT_SCALE_STYLES = {"centered", "scaleToFit", "userDefined"};
protected PSDMetadata() {
// TODO: Allow XMP, EXIF and IPTC as extra formats?
super(true, NATIVE_METADATA_FORMAT_NAME, NATIVE_METADATA_FORMAT_CLASS_NAME, null, null);
}
/// Native format support
@Override
protected Node getNativeTree() {
IIOMetadataNode root = new IIOMetadataNode(NATIVE_METADATA_FORMAT_NAME);
root.appendChild(createHeaderNode());
if (mHeader.mMode == PSD.COLOR_MODE_INDEXED) {
root.appendChild(createPaletteNode());
}
if (mImageResources != null && !mImageResources.isEmpty()) {
root.appendChild(createImageResourcesNode());
}
return root;
}
private Node createHeaderNode() {
IIOMetadataNode header = new IIOMetadataNode("Header");
header.setAttribute("type", "PSD");
header.setAttribute("version", "1");
header.setAttribute("channels", Integer.toString(mHeader.mChannels));
header.setAttribute("height", Integer.toString(mHeader.mHeight));
header.setAttribute("width", Integer.toString(mHeader.mWidth));
header.setAttribute("bits", Integer.toString(mHeader.mBits));
header.setAttribute("mode", COLOR_MODES[mHeader.mMode]);
return header;
}
private Node createImageResourcesNode() {
IIOMetadataNode resource = new IIOMetadataNode("ImageResources");
IIOMetadataNode node;
for (PSDImageResource imageResource : mImageResources) {
// TODO: Always add name (if set) and id (as resourceId) to all nodes?
// Resource Id is useful for people with access to the PSD spec..
if (imageResource instanceof ICCProfile) {
ICCProfile profile = (ICCProfile) imageResource;
// TODO: Format spec
node = new IIOMetadataNode("ICCProfile");
node.setAttribute("colorSpaceType", JAVA_CS[profile.getProfile().getColorSpaceType()]);
//
// FastByteArrayOutputStream data = new FastByteArrayOutputStream(0);
// EncoderStream base64 = new EncoderStream(data, new Base64Encoder(), true);
//
// try {
// base64.write(profile.getProfile().getData());
// }
// catch (IOException ignore) {
// }
//
// byte[] bytes = data.toByteArray();
// node.setAttribute("data", StringUtil.decode(bytes, 0, bytes.length, "ASCII"));
node.setUserObject(profile.getProfile());
}
else if (imageResource instanceof PSDAlphaChannelInfo) {
PSDAlphaChannelInfo alphaChannelInfo = (PSDAlphaChannelInfo) imageResource;
node = new IIOMetadataNode("AlphaChannelInfo");
for (String name : alphaChannelInfo.mNames) {
IIOMetadataNode nameNode = new IIOMetadataNode("Name");
nameNode.setAttribute("value", name);
node.appendChild(nameNode);
}
}
else if (imageResource instanceof PSDDisplayInfo) {
PSDDisplayInfo displayInfo = (PSDDisplayInfo) imageResource;
node = new IIOMetadataNode("DisplayInfo");
node.setAttribute("colorSpace", DISPLAY_INFO_CS[displayInfo.mColorSpace]);
StringBuilder builder = new StringBuilder();
for (short color : displayInfo.mColors) {
if (builder.length() > 0) {
builder.append(" ");
}
builder.append(Integer.toString(color));
}
node.setAttribute("colors", builder.toString());
node.setAttribute("opacity", Integer.toString(displayInfo.mOpacity));
node.setAttribute("kind", DISPLAY_INFO_KINDS[displayInfo.mKind]);
}
else if (imageResource instanceof PSDGridAndGuideInfo) {
PSDGridAndGuideInfo info = (PSDGridAndGuideInfo) imageResource;
node = new IIOMetadataNode("GridAndGuideInfo");
node.setAttribute("version", String.valueOf(info.mVersion));
node.setAttribute("verticalGridCycle", String.valueOf(info.mGridCycleVertical));
node.setAttribute("horizontalGridCycle", String.valueOf(info.mGridCycleHorizontal));
for (PSDGridAndGuideInfo.GuideResource guide : info.mGuides) {
IIOMetadataNode guideNode = new IIOMetadataNode("Guide");
guideNode.setAttribute("location", Integer.toString(guide.mLocation));
guideNode.setAttribute("orientation", GUIDE_ORIENTATIONS[guide.mDirection]);
}
}
else if (imageResource instanceof PSDPixelAspectRatio) {
PSDPixelAspectRatio aspectRatio = (PSDPixelAspectRatio) imageResource;
node = new IIOMetadataNode("PixelAspectRatio");
node.setAttribute("version", String.valueOf(aspectRatio.mVersion));
node.setAttribute("aspectRatio", String.valueOf(aspectRatio.mAspect));
}
else if (imageResource instanceof PSDPrintFlags) {
PSDPrintFlags flags = (PSDPrintFlags) imageResource;
node = new IIOMetadataNode("PrintFlags");
node.setAttribute("labels", String.valueOf(flags.mLabels));
node.setAttribute("cropMarks", String.valueOf(flags.mCropMasks));
node.setAttribute("colorBars", String.valueOf(flags.mColorBars));
node.setAttribute("registrationMarks", String.valueOf(flags.mRegistrationMarks));
node.setAttribute("negative", String.valueOf(flags.mNegative));
node.setAttribute("flip", String.valueOf(flags.mFlip));
node.setAttribute("interpolate", String.valueOf(flags.mInterpolate));
node.setAttribute("caption", String.valueOf(flags.mCaption));
}
else if (imageResource instanceof PSDPrintFlagsInformation) {
PSDPrintFlagsInformation information = (PSDPrintFlagsInformation) imageResource;
node = new IIOMetadataNode("PrintFlagsInformation");
node.setAttribute("version", String.valueOf(information.mVersion));
node.setAttribute("cropMarks", String.valueOf(information.mCropMasks));
node.setAttribute("field", String.valueOf(information.mField));
node.setAttribute("bleedWidth", String.valueOf(information.mBleedWidth));
node.setAttribute("bleedScale", String.valueOf(information.mBleedScale));
}
else if (imageResource instanceof PSDPrintScale) {
PSDPrintScale printScale = (PSDPrintScale) imageResource;
node = new IIOMetadataNode("PrintScale");
node.setAttribute("style", PRINT_SCALE_STYLES[printScale.mStyle]);
node.setAttribute("xLocation", String.valueOf(printScale.mXLocation));
node.setAttribute("yLocation", String.valueOf(printScale.mYlocation));
node.setAttribute("scale", String.valueOf(printScale.mScale));
}
else if (imageResource instanceof PSDResolutionInfo) {
PSDResolutionInfo information = (PSDResolutionInfo) imageResource;
node = new IIOMetadataNode("ResolutionInfo");
node.setAttribute("horizontalResolution", String.valueOf(information.mHRes));
node.setAttribute("horizontalResolutionUnit", RESOLUTION_UNITS[information.mHResUnit]);
node.setAttribute("widthUnit", DIMENSION_UNITS[information.mWidthUnit]);
node.setAttribute("verticalResolution", String.valueOf(information.mVRes));
node.setAttribute("verticalResolutionUnit", RESOLUTION_UNITS[information.mVResUnit]);
node.setAttribute("heightUnit", DIMENSION_UNITS[information.mHeightUnit]);
}
else if (imageResource instanceof PSDUnicodeAlphaNames) {
PSDUnicodeAlphaNames alphaNames = (PSDUnicodeAlphaNames) imageResource;
node = new IIOMetadataNode("UnicodeAlphaNames");
for (String name : alphaNames.mNames) {
IIOMetadataNode nameNode = new IIOMetadataNode("Name");
nameNode.setAttribute("value", name);
node.appendChild(nameNode);
}
}
else if (imageResource instanceof PSDVersionInfo) {
PSDVersionInfo information = (PSDVersionInfo) imageResource;
node = new IIOMetadataNode("VersionInfo");
node.setAttribute("version", String.valueOf(information.mVersion));
node.setAttribute("hasRealMergedData", String.valueOf(information.mHasRealMergedData));
node.setAttribute("writer", information.mWriter);
node.setAttribute("reader", information.mReader);
node.setAttribute("fileVersion", String.valueOf(information.mFileVersion));
}
else if (imageResource instanceof PSDThumbnail) {
// TODO: Revise/rethink this...
PSDThumbnail thumbnail = (PSDThumbnail) imageResource;
node = new IIOMetadataNode("Thumbnail");
// TODO: Thumbnail attributes + access to data, to avoid JPEG re-compression problems
node.setUserObject(thumbnail.getThumbnail());
}
else if (imageResource instanceof PSDIPTCData) {
// TODO: Revise/rethink this...
PSDIPTCData iptc = (PSDIPTCData) imageResource;
node = new IIOMetadataNode("DirectoryResource");
node.setAttribute("type", "IPTC");
node.setUserObject(iptc.mDirectory);
appendEntries(node, "IPTC", iptc.mDirectory);
}
else if (imageResource instanceof PSDEXIF1Data) {
// TODO: Revise/rethink this...
PSDEXIF1Data exif = (PSDEXIF1Data) imageResource;
node = new IIOMetadataNode("DirectoryResource");
node.setAttribute("type", "EXIF");
// TODO: Set byte[] data instead
node.setUserObject(exif.mDirectory);
appendEntries(node, "EXIF", exif.mDirectory);
}
else if (imageResource instanceof PSDXMPData) {
// TODO: Revise/rethink this... Would it be possible to parse XMP as IIOMetadataNodes? Or is that just stupid...
// Or maybe use the Directory approach used by IPTC and EXIF..
PSDXMPData xmp = (PSDXMPData) imageResource;
node = new IIOMetadataNode("DirectoryResource");
node.setAttribute("type", "XMP");
appendEntries(node, "XMP", xmp.mDirectory);
// Set the entire XMP document as user data
node.setUserObject(xmp.mData);
}
else {
// Generic resource..
node = new IIOMetadataNode("ImageResource");
String value = PSDImageResource.resourceTypeForId(imageResource.mId);
if (!"UnknownResource".equals(value)) {
node.setAttribute("name", value);
}
node.setAttribute("length", String.valueOf(imageResource.mSize));
// TODO: Set user object: byte array
}
// TODO: More resources
node.setAttribute("resourceId", String.format("0x%04x", imageResource.mId));
resource.appendChild(node);
}
// TODO: Layers and layer info
// TODO: Global mask etc..
return resource;
}
private void appendEntries(final IIOMetadataNode pNode, final String pType, final Directory pDirectory) {
for (Entry entry : pDirectory) {
Object tagId = entry.getIdentifier();
IIOMetadataNode tag = new IIOMetadataNode("Entry");
tag.setAttribute("tag", String.format("%s", tagId));
String field = entry.getFieldName();
if (field != null) {
tag.setAttribute("field", String.format("%s", field));
}
else {
if ("IPTC".equals(pType)) {
tag.setAttribute("field", String.format("%s:%s", (Integer) tagId >> 8, (Integer) tagId & 0xff));
}
}
if (entry.getValue() instanceof Directory) {
appendEntries(tag, pType, (Directory) entry.getValue());
tag.setAttribute("type", "Directory");
}
else {
tag.setAttribute("value", entry.getValueAsString());
tag.setAttribute("type", entry.getTypeName());
}
pNode.appendChild(tag);
}
}
/// Standard format support
@Override
protected IIOMetadataNode getStandardChromaNode() {
IIOMetadataNode chroma_node = new IIOMetadataNode("Chroma");
IIOMetadataNode node; // scratch node
node = new IIOMetadataNode("ColorSpaceType");
String cs;
switch (mHeader.mMode) {
case PSD.COLOR_MODE_MONOCHROME:
case PSD.COLOR_MODE_GRAYSCALE:
case PSD.COLOR_MODE_DUOTONE: // Rationale: Spec says treat as gray...
cs = "GRAY";
break;
case PSD.COLOR_MODE_RGB:
case PSD.COLOR_MODE_INDEXED:
cs = "RGB";
break;
case PSD.COLOR_MODE_CMYK:
cs = "CMYK";
break;
case PSD.COLOR_MODE_MULTICHANNEL:
cs = getMultiChannelCS(mHeader.mChannels);
break;
case PSD.COLOR_MODE_LAB:
cs = "Lab";
break;
default:
throw new AssertionError("Unreachable");
}
node.setAttribute("name", cs);
chroma_node.appendChild(node);
// TODO: Channels might be 5 for RGB + A + Mask... Probably not correct
node = new IIOMetadataNode("NumChannels");
node.setAttribute("value", Integer.toString(mHeader.mChannels));
chroma_node.appendChild(node);
// TODO: Check if this is correct with bitmap (monchrome)
node = new IIOMetadataNode("BlackIsZero");
node.setAttribute("value", "true");
chroma_node.appendChild(node);
if (mHeader.mMode == PSD.COLOR_MODE_INDEXED) {
node = createPaletteNode();
chroma_node.appendChild(node);
}
// TODO: Hardcode background color to white?
// if (bKGD_present) {
// if (bKGD_colorType == PNGImageReader.PNG_COLOR_PALETTE) {
// node = new IIOMetadataNode("BackgroundIndex");
// node.setAttribute("value", Integer.toString(bKGD_index));
// } else {
// node = new IIOMetadataNode("BackgroundColor");
// int r, g, b;
//
// if (bKGD_colorType == PNGImageReader.PNG_COLOR_GRAY) {
// r = g = b = bKGD_gray;
// } else {
// r = bKGD_red;
// g = bKGD_green;
// b = bKGD_blue;
// }
// node.setAttribute("red", Integer.toString(r));
// node.setAttribute("green", Integer.toString(g));
// node.setAttribute("blue", Integer.toString(b));
// }
// chroma_node.appendChild(node);
// }
return chroma_node;
}
private IIOMetadataNode createPaletteNode() {
IIOMetadataNode node = new IIOMetadataNode("Palette");
IndexColorModel cm = mColorData.getIndexColorModel();
for (int i = 0; i < cm.getMapSize(); i++) {
IIOMetadataNode entry = new IIOMetadataNode("PaletteEntry");
entry.setAttribute("index", Integer.toString(i));
entry.setAttribute("red", Integer.toString(cm.getRed(i)));
entry.setAttribute("green", Integer.toString(cm.getGreen(i)));
entry.setAttribute("blue", Integer.toString(cm.getBlue(i)));
node.appendChild(entry);
}
return node;
}
private String getMultiChannelCS(short pChannels) {
if (pChannels < 16) {
return String.format("%xCLR", pChannels);
}
throw new UnsupportedOperationException("Standard meta data format does not support more than 15 channels");
}
@Override
protected IIOMetadataNode getStandardCompressionNode() {
IIOMetadataNode compressionNode = new IIOMetadataNode("Compression");
IIOMetadataNode node; // scratch node
node = new IIOMetadataNode("CompressionTypeName");
String compression;
switch (mCompression) {
case PSD.COMPRESSION_NONE:
compression = "none";
break;
case PSD.COMPRESSION_RLE:
compression = "PackBits";
break;
case PSD.COMPRESSION_ZIP:
case PSD.COMPRESSION_ZIP_PREDICTION:
compression = "Deflate"; // TODO: ZLib? (TIFF native metadata format specifies both.. :-P)
break;
default:
throw new AssertionError("Unreachable");
}
node.setAttribute("value", compression);
compressionNode.appendChild(node);
// TODO: Does it make sense to specify lossless for compression "none"?
node = new IIOMetadataNode("Lossless");
node.setAttribute("value", "true");
compressionNode.appendChild(node);
return compressionNode;
}
@Override
protected IIOMetadataNode getStandardDataNode() {
IIOMetadataNode dataNode = new IIOMetadataNode("Data");
IIOMetadataNode node; // scratch node
node = new IIOMetadataNode("PlanarConfiguration");
node.setAttribute("value", "PlaneInterleaved"); // TODO: Check with spec
dataNode.appendChild(node);
node = new IIOMetadataNode("SampleFormat");
node.setAttribute("value", mHeader.mMode == PSD.COLOR_MODE_INDEXED ? "Index" : "UnsignedIntegral");
dataNode.appendChild(node);
String bitDepth = Integer.toString(mHeader.mBits); // bits per plane
// TODO: Channels might be 5 for RGB + A + Mask...
String[] bps = new String[mHeader.mChannels];
Arrays.fill(bps, bitDepth);
node = new IIOMetadataNode("BitsPerSample");
node.setAttribute("value", StringUtil.toCSVString(bps, " "));
dataNode.appendChild(node);
// TODO: SampleMSB? Or is network (aka Motorola/big endian) byte order assumed?
return dataNode;
}
@Override
protected IIOMetadataNode getStandardDimensionNode() {
IIOMetadataNode dimensionNode = new IIOMetadataNode("Dimension");
IIOMetadataNode node; // scratch node
node = new IIOMetadataNode("PixelAspectRatio");
// TODO: This is not correct wrt resolution info
float aspect = 1f;
Iterator<PSDPixelAspectRatio> ratios = getResources(PSDPixelAspectRatio.class);
if (ratios.hasNext()) {
PSDPixelAspectRatio ratio = ratios.next();
aspect = (float) ratio.mAspect;
}
node.setAttribute("value", Float.toString(aspect));
dimensionNode.appendChild(node);
node = new IIOMetadataNode("ImageOrientation");
node.setAttribute("value", "Normal");
dimensionNode.appendChild(node);
// TODO: If no PSDResolutionInfo, this might still be available in the EXIF data...
Iterator<PSDResolutionInfo> resolutionInfos = getResources(PSDResolutionInfo.class);
if (!resolutionInfos.hasNext()) {
PSDResolutionInfo resolutionInfo = resolutionInfos.next();
node = new IIOMetadataNode("HorizontalPixelSize");
node.setAttribute("value", Float.toString(asMM(resolutionInfo.mHResUnit, resolutionInfo.mHRes)));
dimensionNode.appendChild(node);
node = new IIOMetadataNode("VerticalPixelSize");
node.setAttribute("value", Float.toString(asMM(resolutionInfo.mVResUnit, resolutionInfo.mVRes)));
dimensionNode.appendChild(node);
}
// TODO:
/*
<!ELEMENT "HorizontalPixelOffset" EMPTY>
<!-- The horizonal position, in pixels, where the image should be
rendered onto a raster display -->
<!ATTLIST "HorizontalPixelOffset" "value" #CDATA #REQUIRED>
<!-- Data type: Integer -->
<!ELEMENT "VerticalPixelOffset" EMPTY>
<!-- The vertical position, in pixels, where the image should be
rendered onto a raster display -->
<!ATTLIST "VerticalPixelOffset" "value" #CDATA #REQUIRED>
<!-- Data type: Integer -->
<!ELEMENT "HorizontalScreenSize" EMPTY>
<!-- The width, in pixels, of the raster display into which the
image should be rendered -->
<!ATTLIST "HorizontalScreenSize" "value" #CDATA #REQUIRED>
<!-- Data type: Integer -->
<!ELEMENT "VerticalScreenSize" EMPTY>
<!-- The height, in pixels, of the raster display into which the
image should be rendered -->
<!ATTLIST "VerticalScreenSize" "value" #CDATA #REQUIRED>
<!-- Data type: Integer -->
*/
return dimensionNode;
}
private static float asMM(final short pUnit, final float pResolution) {
// Unit: 1 -> pixels per inch, 2 -> pixels pr cm
return (pUnit == 1 ? 25.4f : 10) / pResolution;
}
@Override
protected IIOMetadataNode getStandardDocumentNode() {
IIOMetadataNode document_node = new IIOMetadataNode("Document");
IIOMetadataNode node; // scratch node
node = new IIOMetadataNode("FormatVersion");
node.setAttribute("value", "1"); // PSD format version is always 1
document_node.appendChild(node);
// Get EXIF data if present
Iterator<PSDEXIF1Data> exif = getResources(PSDEXIF1Data.class);
if (exif.hasNext()) {
PSDEXIF1Data data = exif.next();
// Get the EXIF DateTime (aka ModifyDate) tag if present
Entry dateTime = data.mDirectory.getEntryById(TIFF.TAG_DATE_TIME);
if (dateTime != null) {
node = new IIOMetadataNode("ImageCreationTime"); // As TIFF, but could just as well be ImageModificationTime
// Format: "YYYY:MM:DD hh:mm:ss"
String value = dateTime.getValueAsString();
node.setAttribute("year", value.substring(0, 4));
node.setAttribute("month", value.substring(5, 7));
node.setAttribute("day", value.substring(8, 10));
node.setAttribute("hour", value.substring(11, 13));
node.setAttribute("minute", value.substring(14, 16));
node.setAttribute("second", value.substring(17, 19));
document_node.appendChild(node);
}
}
return document_node;
}
@Override
protected IIOMetadataNode getStandardTextNode() {
// TODO: TIFF uses
// DocumentName, ImageDescription, Make, Model, PageName, Software, Artist, HostComputer, InkNames, Copyright:
// /Text/TextEntry@keyword = field name, /Text/TextEntry@value = field value.
// Example: TIFF Software field => /Text/TextEntry@keyword = "Software",
// /Text/TextEntry@value = Name and version number of the software package(s) used to create the image.
Iterator<PSDImageResource> textResources = getResources(PSD.RES_IPTC_NAA, PSD.RES_EXIF_DATA_1, PSD.RES_XMP_DATA);
if (!textResources.hasNext()) {
return null;
}
IIOMetadataNode text = new IIOMetadataNode("Text");
IIOMetadataNode node;
// TODO: Alpha channel names? (PSDAlphaChannelInfo/PSDUnicodeAlphaNames)
// TODO: Reader/writer (PSDVersionInfo)
while (textResources.hasNext()) {
PSDImageResource textResource = textResources.next();
if (textResource instanceof PSDIPTCData) {
PSDIPTCData iptc = (PSDIPTCData) textResource;
appendTextEntriesFlat(text, iptc.mDirectory, new FilterIterator.Filter<Entry>() {
public boolean accept(final Entry pEntry) {
Integer tagId = (Integer) pEntry.getIdentifier();
switch (tagId) {
case IPTC.TAG_SOURCE:
return true;
default:
return false;
}
}
});
}
else if (textResource instanceof PSDEXIF1Data) {
PSDEXIF1Data exif = (PSDEXIF1Data) textResource;
appendTextEntriesFlat(text, exif.mDirectory, new FilterIterator.Filter<Entry>() {
public boolean accept(final Entry pEntry) {
Integer tagId = (Integer) pEntry.getIdentifier();
switch (tagId) {
case TIFF.TAG_SOFTWARE:
case TIFF.TAG_ARTIST:
case TIFF.TAG_COPYRIGHT:
return true;
default:
return false;
}
}
});
}
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/TIFFF NativeDigest field to validate if the values are in sync...
PSDXMPData xmp = (PSDXMPData) textResource;
}
}
return text;
}
private void appendTextEntriesFlat(final IIOMetadataNode pNode, final Directory pDirectory, final FilterIterator.Filter<Entry> pFilter) {
FilterIterator<Entry> pEntries = new FilterIterator<Entry>(pDirectory.iterator(), pFilter);
while (pEntries.hasNext()) {
Entry entry = pEntries.next();
if (entry.getValue() instanceof Directory) {
appendTextEntriesFlat(pNode, (Directory) entry.getValue(), pFilter);
}
else if (entry.getValue() instanceof String) {
IIOMetadataNode tag = new IIOMetadataNode("TextEntry");
String fieldName = entry.getFieldName();
if (fieldName != null) {
tag.setAttribute("keyword", String.format("%s", fieldName));
}
else {
// TODO: This should never happen, as we filter out only specific nodes
tag.setAttribute("keyword", String.format("%s", entry.getIdentifier()));
}
tag.setAttribute("value", entry.getValueAsString());
pNode.appendChild(tag);
}
}
}
@Override
protected IIOMetadataNode getStandardTileNode() {
return super.getStandardTileNode();
}
@Override
protected IIOMetadataNode getStandardTransparencyNode() {
IIOMetadataNode transparency_node = new IIOMetadataNode("Transparency");
IIOMetadataNode node; // scratch node
node = new IIOMetadataNode("Alpha");
node.setAttribute("value", hasAlpha() ? "nonpremultiplied" : "none"); // TODO: Check spec
transparency_node.appendChild(node);
return transparency_node;
}
private boolean hasAlpha() {
return mHeader.mMode == PSD.COLOR_MODE_RGB && mHeader.mChannels >= 4 ||
mHeader.mMode == PSD.COLOR_MODE_CMYK & mHeader.mChannels >= 5;
}
<T extends PSDImageResource> Iterator<T> getResources(final Class<T> pResourceType) {
// NOTE: The cast here is wrong, strictly speaking, but it does not matter...
@SuppressWarnings({"unchecked"})
Iterator<T> iterator = (Iterator<T>) mImageResources.iterator();
return new FilterIterator<T>(iterator, new FilterIterator.Filter<T>() {
public boolean accept(final T pElement) {
return pResourceType.isInstance(pElement);
}
});
}
Iterator<PSDImageResource> getResources(final int... pResourceTypes) {
Iterator<PSDImageResource> iterator = mImageResources.iterator();
return new FilterIterator<PSDImageResource>(iterator, new FilterIterator.Filter<PSDImageResource>() {
public boolean accept(final PSDImageResource pResource) {
for (int type : pResourceTypes) {
if (type == pResource.mId) {
return true;
}
}
return false;
}
});
}
@Override
public Object clone() {
// TODO: Make it a deep clone
try {
return super.clone();
}
catch (CloneNotSupportedException e) {
throw new RuntimeException(e);
}
}
}
@@ -0,0 +1,212 @@
package com.twelvemonkeys.imageio.plugins.psd;
import com.twelvemonkeys.imageio.metadata.Directory;
import org.w3c.dom.Document;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.metadata.IIOMetadataFormatImpl;
import java.awt.image.BufferedImage;
import java.util.Arrays;
/**
* PSDMetadataFormat
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: PSDMetadataFormat.java,v 1.0 Nov 4, 2009 5:27:53 PM haraldk Exp$
*/
public final class PSDMetadataFormat extends IIOMetadataFormatImpl {
private final static PSDMetadataFormat sInstance = new PSDMetadataFormat();
/**
* Private constructor.
* <p/>
* The {@link javax.imageio.metadata.IIOMetadata} class will instantiate this class
* by reflection, invoking the static {@code getInstance()} method.
*
* @see javax.imageio.metadata.IIOMetadata#getMetadataFormat
* @see #getInstance()
*/
private PSDMetadataFormat() {
// Defines the root element
super(PSDMetadata.NATIVE_METADATA_FORMAT_NAME, CHILD_POLICY_SOME);
// root -> PSDHeader
// TODO: How do I specify that the header is required?
addElement("Header", PSDMetadata.NATIVE_METADATA_FORMAT_NAME, CHILD_POLICY_EMPTY);
addAttribute("Header", "type", DATATYPE_STRING, false, "PSD", Arrays.asList("PSD", "PSB"));
addAttribute("Header", "version", DATATYPE_INTEGER, false, "1", Arrays.asList("1"));
addAttribute("Header", "channels", DATATYPE_INTEGER, true, null, "1", "24", true, true);
// rows?
addAttribute("Header", "height", DATATYPE_INTEGER, true, null, "1", "30000", true, true);
// columns?
addAttribute("Header", "width", DATATYPE_INTEGER, true, null, "1", "30000", true, true);
addAttribute("Header", "bits", DATATYPE_INTEGER, true, null, Arrays.asList("1", "8", "16"));
addAttribute("Header", "mode", DATATYPE_STRING, true, null, Arrays.asList(PSDMetadata.COLOR_MODES));
/*
Contains the required data to define the color mode.
For indexed color images, the count will be equal to 768, and the mode data
will contain the color table for the image, in non-interleaved order.
For duotone images, the mode data will contain the duotone specification,
the format of which is not documented. Non-Photoshop readers can treat
the duotone image as a grayscale image, and keep the duotone specification
around as a black box for use when saving the file.
*/
// root -> Palette
// Color map for indexed, optional
// NOTE: Palette, PaletteEntry naming taken from the standard format, native PSD naming is ColorModeData
// NOTE: PSD stores these as 256 Red, 256 Green, 256 Blue.. Should we do the same in the meta data?
addElement("Palette", PSDMetadata.NATIVE_METADATA_FORMAT_NAME, 256, 256); // 768 = 256 * 3
addElement("PaletteEntry", "Palette", CHILD_POLICY_EMPTY);
addAttribute("PaletteEntry", "index", DATATYPE_INTEGER, true, null, "0", "255", true, true);
addAttribute("PaletteEntry", "red", DATATYPE_INTEGER, true, null, "0", "255", true, true);
addAttribute("PaletteEntry", "green", DATATYPE_INTEGER, true, null, "0", "255", true, true);
addAttribute("PaletteEntry", "blue", DATATYPE_INTEGER, true, null, "0", "255", true, true);
// No alpha allowed in indexed color PSD
// TODO: Duotone spec, optional (use same element as palette?)
// Or use object or raw bytes..
// root -> ImageResources
// Image resources, optional
addElement("ImageResources", PSDMetadata.NATIVE_METADATA_FORMAT_NAME, CHILD_POLICY_SEQUENCE); // SOME?
// root -> ImageResources -> ImageResource
// Generic resource
addElement("ImageResource", "ImageResources", CHILD_POLICY_ALL);
// TODO: Allow arbitrary values to be added as a generic resource...
// root -> ImageResources -> AlphaChannelInfo
addElement("AlphaChannelInfo", "ImageResources", 0, Integer.MAX_VALUE); // The format probably does not support that many layers..
addElement("Name", "AlphaChannelInfo", CHILD_POLICY_EMPTY);
addAttribute("Name", "value", DATATYPE_STRING, true, null);
// root -> ImageResources -> DisplayInfo
addElement("DisplayInfo", "ImageResources", CHILD_POLICY_EMPTY);
// TODO: Consider using human readable strings
// TODO: Limit values (0-8, 10, 11, 3000)
addAttribute("DisplayInfo", "colorSpace", DATATYPE_STRING, true, null, Arrays.asList(PSDMetadata.DISPLAY_INFO_CS));
addAttribute("DisplayInfo", "colors", DATATYPE_INTEGER, true, 4, 4);
addAttribute("DisplayInfo", "opacity", DATATYPE_INTEGER, true, null, "0", "100", true, true);
// TODO: Consider using human readable strings
addAttribute("DisplayInfo", "kind", DATATYPE_STRING, true, null, Arrays.asList(PSDMetadata.DISPLAY_INFO_KINDS));
// root -> ImageResources -> EXIF
addElement("EXIF", "ImageResources", CHILD_POLICY_EMPTY);
addObjectValue("EXIF", Directory.class, true, null);
// TODO: Incorporate EXIF / TIFF metadata here somehow... (or treat as opaque bytes?)
// root -> ImageResources -> GridAndGuideInfo
addElement("GridAndGuideInfo", "ImageResources", 0, Integer.MAX_VALUE);
addAttribute("GridAndGuideInfo", "version", DATATYPE_INTEGER, false, "1");
addAttribute("GridAndGuideInfo", "verticalGridCycle", DATATYPE_INTEGER, false, "576");
addAttribute("GridAndGuideInfo", "horizontalGridCycle", DATATYPE_INTEGER, false, "576");
addElement("Guide", "GridAndGuideInfo", CHILD_POLICY_EMPTY);
addAttribute("Guide", "location", DATATYPE_INTEGER, true, null, "0", Integer.toString(Integer.MAX_VALUE), true, true);
addAttribute("Guide", "orientation", DATATYPE_STRING, true, null, Arrays.asList(PSDMetadata.GUIDE_ORIENTATIONS));
// root -> ImageResources -> ICCProfile
addElement("ICCProfile", "ImageResources", CHILD_POLICY_EMPTY);
addAttribute("ICCProfile", "colorSpaceType", DATATYPE_STRING, true, null, Arrays.asList(PSDMetadata.JAVA_CS));
// root -> ImageResources -> IPTC
addElement("IPTC", "ImageResources", CHILD_POLICY_EMPTY);
addObjectValue("IPTC", Directory.class, true, null);
// TODO: Incorporate IPTC metadata here somehow... (or treat as opaque bytes?)
// root -> ImageResources -> PixelAspectRatio
addElement("PixelAspectRatio", "ImageResources", CHILD_POLICY_EMPTY);
addAttribute("PixelAspectRatio", "version", DATATYPE_STRING, false, "1");
addAttribute("PixelAspectRatio", "aspectRatio", DATATYPE_DOUBLE, true, null, "0", Double.toString(Double.POSITIVE_INFINITY), true, false);
// root -> ImageResources -> PrintFlags
addElement("PrintFlags", "ImageResources", CHILD_POLICY_EMPTY);
addBooleanAttribute("PrintFlags", "labels", false, false);
addBooleanAttribute("PrintFlags", "cropMasks", false, false);
addBooleanAttribute("PrintFlags", "colorBars", false, false);
addBooleanAttribute("PrintFlags", "registrationMarks", false, false);
addBooleanAttribute("PrintFlags", "negative", false, false);
addBooleanAttribute("PrintFlags", "flip", false, false);
addBooleanAttribute("PrintFlags", "interpolate", false, false);
addBooleanAttribute("PrintFlags", "caption", false, false);
// root -> ImageResources -> PrintFlagsInformation
addElement("PrintFlagsInformation", "ImageResources", CHILD_POLICY_EMPTY);
addAttribute("PrintFlagsInformation", "version", DATATYPE_INTEGER, false, "1");
addBooleanAttribute("PrintFlagsInformation", "cropMarks", false, false);
addAttribute("PrintFlagsInformation", "field", DATATYPE_INTEGER, true, "0");
addAttribute("PrintFlagsInformation", "bleedWidth", DATATYPE_INTEGER, true, null, "0", String.valueOf(Long.MAX_VALUE), true, true); // TODO: LONG??!
addAttribute("PrintFlagsInformation", "bleedScale", DATATYPE_INTEGER, true, null, "0", String.valueOf(Integer.MAX_VALUE), true, true);
// root -> ImageResources -> PrintScale
addElement("PrintScale", "ImageResources", CHILD_POLICY_EMPTY);
addAttribute("PrintScale", "style", DATATYPE_STRING, false, null, Arrays.asList(PSDMetadata.PRINT_SCALE_STYLES));
addAttribute("PrintScale", "xLocation", DATATYPE_FLOAT, true, null);
addAttribute("PrintScale", "yLocation", DATATYPE_FLOAT, true, null);
addAttribute("PrintScale", "scale", DATATYPE_FLOAT, true, null);
// root -> ImageResources -> ResolutionInfo
addElement("ResolutionInfo", "ImageResources", CHILD_POLICY_EMPTY);
addAttribute("ResolutionInfo", "hRes", DATATYPE_FLOAT, true, null);
addAttribute("ResolutionInfo", "hResUnit", DATATYPE_STRING, true, null, Arrays.asList(PSDMetadata.RESOLUTION_UNITS));
addAttribute("ResolutionInfo", "widthUnit", DATATYPE_STRING, true, null, Arrays.asList(PSDMetadata.DIMENSION_UNITS));
addAttribute("ResolutionInfo", "vRes", DATATYPE_FLOAT, true, null);
addAttribute("ResolutionInfo", "vResUnit", DATATYPE_STRING, true, null, Arrays.asList(PSDMetadata.RESOLUTION_UNITS));
addAttribute("ResolutionInfo", "heightUnit", DATATYPE_STRING, true, null, Arrays.asList(PSDMetadata.DIMENSION_UNITS));
// root -> ImageResources -> UnicodeAlphaNames
addElement("UnicodeAlphaNames", "ImageResources", 0, Integer.MAX_VALUE);
addChildElement("UnicodeAlphaNames", "Name"); // TODO: Does this really work?
// root -> ImageResources -> VersionInfo
addElement("VersionInfo", "ImageResources", CHILD_POLICY_EMPTY);
addAttribute("VersionInfo", "version", DATATYPE_INTEGER, false, "1");
addBooleanAttribute("VersionInfo", "hasRealMergedData", false, false);
addAttribute("VersionInfo", "writer", DATATYPE_STRING, true, null);
addAttribute("VersionInfo", "reader", DATATYPE_STRING, true, null);
addAttribute("VersionInfo", "fileVersion", DATATYPE_INTEGER, true, "1");
// root -> ImageResources -> Thumbnail
addElement("Thumbnail", "ImageResources", CHILD_POLICY_EMPTY);
addObjectValue("Thumbnail", BufferedImage.class, true, null);
// root -> ImageResources -> UnicodeAlphaName
addElement("UnicodeAlphaName", "ImageResources", CHILD_POLICY_EMPTY);
addAttribute("UnicodeAlphaName", "value", DATATYPE_STRING, true, null);
// root -> ImageResources -> XMP
addElement("XMP", "ImageResources", CHILD_POLICY_CHOICE);
// TODO: Incorporate XMP metadata here somehow (or treat as opaque bytes?)
addObjectValue("XMP", Document.class, true, null);
// TODO: Layers
//addElement("ChannelSourceDestinationRange", "LayerSomething", CHILD_POLICY_CHOICE);
// TODO: Global layer mask info
}
@Override
public boolean canNodeAppear(final String pElementName, final ImageTypeSpecifier pImageType) {
// TODO: PSDColorData and PaletteEntry only for indexed color model
throw new UnsupportedOperationException("Method canNodeAppear not implemented"); // TODO: Implement
}
/**
* Returns the shared instance of the {@code PSDMetadataFormat}.
*
* @return the shared instance.
* @see javax.imageio.metadata.IIOMetadata#getMetadataFormat
*/
public static PSDMetadataFormat getInstance() {
return sInstance;
}
}
@@ -0,0 +1,27 @@
package com.twelvemonkeys.imageio.plugins.psd;
import javax.imageio.stream.ImageInputStream;
import java.io.IOException;
/**
* PSDPixelAspectRatio
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: PSDPixelAspectRatio.java,v 1.0 Nov 7, 2009 8:23:09 PM haraldk Exp$
*/
final class PSDPixelAspectRatio extends PSDImageResource {
// 4 bytes (version = 1), 8 bytes double, x / y of a pixel
int mVersion;
double mAspect;
PSDPixelAspectRatio(final short pId, final ImageInputStream pInput) throws IOException {
super(pId, pInput);
}
@Override
protected void readData(final ImageInputStream pInput) throws IOException {
mVersion = pInput.readInt();
mAspect = pInput.readDouble();
}
}
@@ -11,14 +11,14 @@ import java.io.IOException;
* @version $Id: PSDPrintFlagsInfo.java,v 1.0 Jul 28, 2009 5:16:27 PM haraldk Exp$
*/
final class PSDPrintFlags extends PSDImageResource {
private boolean mLabels;
private boolean mCropMasks;
private boolean mColorBars;
private boolean mRegistrationMarks;
private boolean mNegative;
private boolean mFlip;
private boolean mInterpolate;
private boolean mCaption;
boolean mLabels;
boolean mCropMasks;
boolean mColorBars;
boolean mRegistrationMarks;
boolean mNegative;
boolean mFlip;
boolean mInterpolate;
boolean mCaption;
PSDPrintFlags(final short pId, final ImageInputStream pInput) throws IOException {
super(pId, pInput);
@@ -26,14 +26,14 @@ final class PSDPrintFlags extends PSDImageResource {
@Override
protected void readData(final ImageInputStream pInput) throws IOException {
mLabels = pInput.readUnsignedByte() != 0;
mCropMasks = pInput.readUnsignedByte() != 0;
mColorBars = pInput.readUnsignedByte() != 0;
mRegistrationMarks = pInput.readUnsignedByte() != 0;
mNegative = pInput.readUnsignedByte() != 0;
mFlip = pInput.readUnsignedByte() != 0;
mInterpolate = pInput.readUnsignedByte() != 0;
mCaption = pInput.readUnsignedByte() != 0;
mLabels = pInput.readBoolean();
mCropMasks = pInput.readBoolean();
mColorBars = pInput.readBoolean();
mRegistrationMarks = pInput.readBoolean();
mNegative = pInput.readBoolean();
mFlip = pInput.readBoolean();
mInterpolate = pInput.readBoolean();
mCaption = pInput.readBoolean();
pInput.skipBytes(mSize - 8);
}
@@ -11,11 +11,11 @@ import java.io.IOException;
* @version $Id: PSDPrintFlagsInfo.java,v 1.0 Jul 28, 2009 5:16:27 PM haraldk Exp$
*/
final class PSDPrintFlagsInformation extends PSDImageResource {
private int mVersion;
private boolean mCropMasks;
private int mField;
private long mBleedWidth;
private int mBleedScale;
int mVersion;
boolean mCropMasks;
int mField;
long mBleedWidth;
int mBleedScale;
PSDPrintFlagsInformation(final short pId, final ImageInputStream pInput) throws IOException {
super(pId, pInput);
@@ -24,8 +24,8 @@ final class PSDPrintFlagsInformation extends PSDImageResource {
@Override
protected void readData(final ImageInputStream pInput) throws IOException {
mVersion = pInput.readUnsignedShort();
mCropMasks = pInput.readUnsignedByte() != 0;
mField = pInput.readUnsignedByte();
mCropMasks = pInput.readBoolean();
mField = pInput.readUnsignedByte(); // TODO: Is this really pad?
mBleedWidth = pInput.readUnsignedInt();
mBleedScale = pInput.readUnsignedShort();
@@ -0,0 +1,35 @@
package com.twelvemonkeys.imageio.plugins.psd;
import javax.imageio.stream.ImageInputStream;
import java.io.IOException;
/**
* PSDPrintScale
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: PSDPrintScale.java,v 1.0 Nov 7, 2009 9:41:17 PM haraldk Exp$
*/
final class PSDPrintScale extends PSDImageResource {
// 2 bytes style (0 = centered, 1 = size to fit, 2 = user defined).
// 4 bytes x location (floating point).
// 4 bytes y location (floating point).
// 4 bytes scale (floating point)
short mStyle;
float mXLocation;
float mYlocation;
float mScale;
PSDPrintScale(final short pId, final ImageInputStream pInput) throws IOException {
super(pId, pInput);
}
@Override
protected void readData(final ImageInputStream pInput) throws IOException {
mStyle = pInput.readShort();
mXLocation = pInput.readFloat();
mYlocation = pInput.readFloat();
mScale = pInput.readFloat();
}
}
@@ -1,555 +0,0 @@
package com.twelvemonkeys.imageio.plugins.psd;
import java.awt.*;
import java.awt.image.*;
import java.net.*;
import java.io.*;
/**
* Class PSDReader - Decodes a PhotoShop (.psd) file into one or more frames.
* Supports uncompressed or RLE-compressed RGB files only. Each layer may be
* retrieved as a full frame BufferedImage, or as a smaller image with an
* offset if the layer does not occupy the full frame size. Transparency
* in the original psd file is preserved in the returned BufferedImage's.
* Does not support additional features in PS versions higher than 3.0.
* Example:
* <br><pre>
* PSDReader r = new PSDReader();
* r.readData("sample.psd");
* int n = r.getFrameCount();
* for (int i = 0; i < n; i++) {
* BufferedImage image = r.getLayer(i);
* Point offset = r.getLayerOffset(i);
* // do something with image
* }
* </pre>
* No copyright asserted on the source code of this class. May be used for
* any purpose. Please forward any corrections to kweiner@fmsware.com.
*
* @author Kevin Weiner, FM Software.
* @version 1.1 January 2004 [bug fix; add RLE support]
*
*/
// SEE: http://www.fileformat.info/format/psd/egff.htm#ADOBEPHO-DMYID.4
class PSDReader {
/**
* File readData status: No errors.
*/
public static final int STATUS_OK = 0;
/**
* File readData status: Error decoding file (may be partially decoded)
*/
public static final int STATUS_FORMAT_ERROR = 1;
/**
* File readData status: Unable to open source.
*/
public static final int STATUS_OPEN_ERROR = 2;
/**
* File readData status: Unsupported format
*/
public static final int STATUS_UNSUPPORTED = 3;
public static int ImageType = BufferedImage.TYPE_INT_ARGB;
protected BufferedInputStream input;
protected int frameCount;
protected BufferedImage[] frames;
protected int status = 0;
protected int nChan;
protected int width;
protected int height;
protected int nLayers;
protected int miscLen;
protected boolean hasLayers;
protected LayerInfo[] layers;
protected short[] lineLengths;
protected int lineIndex;
protected boolean rleEncoded;
protected class LayerInfo {
int x, y, w, h;
int nChan;
int[] chanID;
int alpha;
}
/**
* Gets the number of layers readData from file.
* @return frame count
*/
public int getFrameCount() {
return frameCount;
}
protected void setInput(InputStream stream) {
// open input stream
init();
if (stream == null) {
status = STATUS_OPEN_ERROR;
} else {
if (stream instanceof BufferedInputStream)
input = (BufferedInputStream) stream;
else
input = new BufferedInputStream(stream);
}
}
protected void setInput(String name) {
// open input file
init();
try {
name = name.trim();
if (name.startsWith("file:")) {
name = name.substring(5);
while (name.startsWith("/"))
name = name.substring(1);
}
if (name.indexOf("://") > 0) {
URL url = new URL(name);
input = new BufferedInputStream(url.openStream());
} else {
input = new BufferedInputStream(new FileInputStream(name));
}
} catch (IOException e) {
status = STATUS_OPEN_ERROR;
}
}
/**
* Gets display duration for specified frame. Always returns 0.
*
*/
public int getDelay(int forFrame) {
return 0;
}
/**
* Gets the image contents of frame n. Note that this expands the image
* to the full frame size (if the layer was smaller) and any subsequent
* use of getLayer() will return the full image.
*
* @return BufferedImage representation of frame, or null if n is invalid.
*/
public BufferedImage getFrame(int n) {
BufferedImage im = null;
if ((n >= 0) && (n < nLayers)) {
im = frames[n];
LayerInfo info = layers[n];
if ((info.w != width) || (info.h != height)) {
BufferedImage temp =
new BufferedImage(width, height, ImageType);
Graphics2D gc = temp.createGraphics();
gc.drawImage(im, info.x, info.y, null);
gc.dispose();
im = temp;
frames[n] = im;
}
}
return im;
}
/**
* Gets maximum image size. Individual layers may be smaller.
*
* @return maximum image dimensions
*/
public Dimension getFrameSize() {
return new Dimension(width, height);
}
/**
* Gets the first (or only) image readData.
*
* @return BufferedImage containing first frame, or null if none.
*/
public BufferedImage getImage() {
return getFrame(0);
}
/**
* Gets the image contents of layer n. May be smaller than full frame
* size - use getFrameOffset() to obtain position of subimage within
* main image area.
*
* @return BufferedImage representation of layer, or null if n is invalid.
*/
public BufferedImage getLayer(int n) {
BufferedImage im = null;
if ((n >= 0) && (n < nLayers)) {
im = frames[n];
}
return im;
}
/**
* Gets the subimage offset of layer n if it is smaller than the
* full frame size.
*
* @return Point indicating offset from upper left corner of frame.
*/
public Point getLayerOffset(int n) {
Point p = null;
if ((n >= 0) && (n < nLayers)) {
int x = layers[n].x;
int y = layers[n].y;
p = new Point(x, y);
}
if (p == null) {
p = new Point(0, 0);
}
return p;
}
/**
* Reads PhotoShop layers from stream.
*
* @param InputStream in PhotoShop format.
* @return readData status code (0 = no errors)
*/
public int read(InputStream stream) {
setInput(stream);
process();
return status;
}
/**
* Reads PhotoShop file from specified source (file or URL string)
*
* @param name String containing source
* @return readData status code (0 = no errors)
*/
public int read(String name) {
setInput(name);
process();
return status;
}
/**
* Closes input stream and discards contents of all frames.
*
*/
public void reset() {
init();
}
protected void close() {
if (input != null) {
try {
input.close();
} catch (Exception e) {}
input = null;
}
}
protected boolean err() {
return status != STATUS_OK;
}
protected byte[] fillBytes(int size, int value) {
// create byte array filled with given value
byte[] b = new byte[size];
if (value != 0) {
byte v = (byte) value;
for (int i = 0; i < size; i++) {
b[i] = v;
}
}
return b;
}
protected void init() {
close();
frameCount = 0;
frames = null;
layers = null;
hasLayers = true;
status = STATUS_OK;
}
protected void makeDummyLayer() {
// creat dummy layer for non-layered image
rleEncoded = readShort() == 1;
hasLayers = false;
nLayers = 1;
layers = new LayerInfo[1];
LayerInfo layer = new LayerInfo();
layers[0] = layer;
layer.h = height;
layer.w = width;
int nc = Math.min(nChan, 4);
if (rleEncoded) {
// get list of rle encoded line lengths for all channels
readLineLengths(height * nc);
}
layer.nChan = nc;
layer.chanID = new int[nc];
for (int i = 0; i < nc; i++) {
int id = i;
if (i == 3) id = -1;
layer.chanID[i] = id;
}
}
protected void readLineLengths(int nLines) {
// readData list of rle encoded line lengths
lineLengths = new short[nLines];
for (int i = 0; i < nLines; i++) {
lineLengths[i] = readShort();
}
lineIndex = 0;
}
protected BufferedImage makeImage(int w, int h, byte[] r, byte[] g, byte[] b, byte[] a) {
// create image from given plane data
BufferedImage im = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
int[] data = ((DataBufferInt) im.getRaster().getDataBuffer()).getData();
int n = w * h;
int j = 0;
while (j < n) {
try {
int ac = a[j] & 0xff;
int rc = r[j] & 0xff;
int gc = g[j] & 0xff;
int bc = b[j] & 0xff;
data[j] = (((((ac << 8) | rc) << 8) | gc) << 8) | bc;
} catch (Exception e) {}
j++;
}
return im;
}
protected void process() {
// decode PSD file
if (err()) return;
readHeader();
if (err()) return;
readLayerInfo();
if (err()) return;
if (nLayers == 0) {
makeDummyLayer();
if (err()) return;
}
readLayers();
}
protected int readByte() {
// readData single byte from input
int curByte = 0;
try {
curByte = input.read();
} catch (IOException e) {
status = STATUS_FORMAT_ERROR;
}
return curByte;
}
protected int readBytes(byte[] bytes, int n) {
// readData multiple bytes from input
if (bytes == null) return 0;
int r = 0;
try {
r = input.read(bytes, 0, n);
} catch (IOException e) {
status = STATUS_FORMAT_ERROR;
}
if (r < n) {
status = STATUS_FORMAT_ERROR;
}
return r;
}
protected void readHeader() {
// readData PSD header info
String sig = readString(4);
int ver = readShort();
skipBytes(6);
nChan = readShort();
height = readInt();
width = readInt();
int depth = readShort();
int mode = readShort();
int cmLen = readInt();
skipBytes(cmLen);
int imResLen = readInt();
skipBytes(imResLen);
// require 8-bit RGB data
if ((!sig.equals("8BPS")) || (ver != 1)) {
status = STATUS_FORMAT_ERROR;
} else if ((depth != 8) || (mode != 3)) {
status = STATUS_UNSUPPORTED;
}
}
protected int readInt() {
// readData big-endian 32-bit integer
return (((((readByte() << 8) | readByte()) << 8) | readByte()) << 8)
| readByte();
}
protected void readLayerInfo() {
// readData layer header info
miscLen = readInt();
if (miscLen == 0) {
return; // no layers, only base image
}
int layerInfoLen = readInt();
nLayers = readShort();
if (nLayers > 0) {
layers = new LayerInfo[nLayers];
}
for (int i = 0; i < nLayers; i++) {
LayerInfo info = new LayerInfo();
layers[i] = info;
info.y = readInt();
info.x = readInt();
info.h = readInt() - info.y;
info.w = readInt() - info.x;
info.nChan = readShort();
info.chanID = new int[info.nChan];
for (int j = 0; j < info.nChan; j++) {
int id = readShort();
int size = readInt();
info.chanID[j] = id;
}
String s = readString(4);
if (!s.equals("8BIM")) {
status = STATUS_FORMAT_ERROR;
return;
}
skipBytes(4); // blend mode
info.alpha = readByte();
int clipping = readByte();
int flags = readByte();
readByte(); // filler
int extraSize = readInt();
skipBytes(extraSize);
}
}
protected void readLayers() {
// readData and convert each layer to BufferedImage
frameCount = nLayers;
frames = new BufferedImage[nLayers];
for (int i = 0; i < nLayers; i++) {
LayerInfo info = layers[i];
byte[] r = null, g = null, b = null, a = null;
for (int j = 0; j < info.nChan; j++) {
int id = info.chanID[j];
switch (id) {
case 0 : r = readPlane(info.w, info.h); break;
case 1 : g = readPlane(info.w, info.h); break;
case 2 : b = readPlane(info.w, info.h); break;
case -1 : a = readPlane(info.w, info.h); break;
default : readPlane(info.w, info.h);
}
if (err()) break;
}
if (err()) break;
int n = info.w * info.h;
if (r == null) r = fillBytes(n, 0);
if (g == null) g = fillBytes(n, 0);
if (b == null) b = fillBytes(n, 0);
if (a == null) a = fillBytes(n, 255);
BufferedImage im = makeImage(info.w, info.h, r, g, b, a);
frames[i] = im;
}
lineLengths = null;
if ((miscLen > 0) && !err()) {
int n = readInt(); // global layer mask info len
skipBytes(n);
}
}
protected byte[] readPlane(int w, int h) {
// readData a single color plane
byte[] b = null;
int size = w * h;
if (hasLayers) {
// get RLE compression info for channel
rleEncoded = readShort() == 1;
if (rleEncoded) {
// list of encoded line lengths
readLineLengths(h);
}
}
if (rleEncoded) {
b = readPlaneCompressed(w, h);
} else {
b = new byte[size];
readBytes(b, size);
}
return b;
}
protected byte[] readPlaneCompressed(int w, int h) {
byte[] b = new byte[w * h];
byte[] s = new byte[w * 2];
int pos = 0;
for (int i = 0; i < h; i++) {
if (lineIndex >= lineLengths.length) {
status = STATUS_FORMAT_ERROR;
return null;
}
int len = lineLengths[lineIndex++];
readBytes(s, len);
decodeRLE(s, 0, len, b, pos);
pos += w;
}
return b;
}
protected void decodeRLE(byte[] src, int sindex, int slen, byte[] dst, int dindex) {
try {
int max = sindex + slen;
while (sindex < max) {
byte b = src[sindex++];
int n = (int) b;
if (n < 0) {
// dup next byte 1-n times
n = 1 - n;
b = src[sindex++];
for (int i = 0; i < n; i++) {
dst[dindex++] = b;
}
} else {
// copy next n+1 bytes
n = n + 1;
System.arraycopy(src, sindex, dst, dindex, n);
dindex += n;
sindex += n;
}
}
} catch (Exception e) {
status = STATUS_FORMAT_ERROR;
}
}
protected short readShort() {
// readData big-endian 16-bit integer
return (short) ((readByte() << 8) | readByte());
}
protected String readString(int len) {
// readData string of specified length
String s = "";
for (int i = 0; i < len; i++) {
s = s + (char) readByte();
}
return s;
}
protected void skipBytes(int n) {
// skip over n input bytes
for (int i = 0; i < n; i++) {
readByte();
}
}
}
@@ -50,13 +50,12 @@ class PSDResolutionInfo extends PSDImageResource {
// WORD HeightUnit; /* 1=in, 2=cm, 3=pt, 4=picas, 5=columns */
// } RESOLUTIONINFO;
private float mHRes;
private short mHResUnit;
private short mWidthUnit;
private float mVRes;
private short mVResUnit;
private short mHeightUnit;
float mHRes;
short mHResUnit;
short mWidthUnit;
float mVRes;
short mVResUnit;
short mHeightUnit;
PSDResolutionInfo(final short pId, final ImageInputStream pInput) throws IOException {
super(pId, pInput);
@@ -37,7 +37,7 @@ class PSDThumbnail extends PSDImageResource {
*/
@Override
protected void readData(final ImageInputStream pInput) throws IOException {
// TODO: Support for RAW RGB (format == 0)
// TODO: Support for RAW RGB (format == 0): Extract RAW reader from PICT RAW QuickTime decompressor
int format = pInput.readInt();
switch (format) {
case 0:
@@ -0,0 +1,33 @@
package com.twelvemonkeys.imageio.plugins.psd;
import javax.imageio.stream.ImageInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* PSDUnicodeAlphaNames
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: PSDUnicodeAlphaNames.java,v 1.0 Nov 7, 2009 9:16:56 PM haraldk Exp$
*/
final class PSDUnicodeAlphaNames extends PSDImageResource {
List<String> mNames;
PSDUnicodeAlphaNames(final short pId, final ImageInputStream pInput) throws IOException {
super(pId, pInput);
}
@Override
protected void readData(final ImageInputStream pInput) throws IOException {
mNames = new ArrayList<String>();
long left = mSize;
while (left > 0) {
String name = PSDUtil.readUnicodeString(pInput);
mNames.add(name);
left -= name.length() * 2 + 4;
}
}
}
@@ -31,8 +31,10 @@ package com.twelvemonkeys.imageio.plugins.psd;
import com.twelvemonkeys.imageio.util.IIOUtil;
import com.twelvemonkeys.io.enc.DecoderStream;
import com.twelvemonkeys.io.enc.PackBitsDecoder;
import com.twelvemonkeys.lang.StringUtil;
import javax.imageio.stream.ImageInputStream;
import java.io.DataInput;
import java.io.DataInputStream;
import java.io.IOException;
import java.util.zip.ZipInputStream;
@@ -57,23 +59,32 @@ final class PSDUtil {
);
}
// TODO: Proably also useful for PICT reader, move to some common util?
static String readPascalString(ImageInputStream pInput) throws IOException {
// TODO: Proably also useful for PICT reader, move to some common util?
static String readPascalString(final DataInput pInput) throws IOException {
int length = pInput.readUnsignedByte();
// int length = pInput.readUnsignedShort();
if (length == 0) {
return "";
}
byte[] bytes = new byte[length];
pInput.readFully(bytes);
if (length % 2 == 0) {
pInput.readByte(); // Pad
}
return new String(bytes);
return StringUtil.decode(bytes, 0, bytes.length, "ASCII");
}
static String readPascalStringByte(ImageInputStream pInput) throws IOException {
int length = pInput.readUnsignedByte();
byte[] bytes = new byte[length];
// TODO: Proably also useful for PICT reader, move to some common util?
static String readUnicodeString(final DataInput pInput) throws IOException {
int length = pInput.readInt();
if (length == 0) {
return "";
}
byte[] bytes = new byte[length * 2];
pInput.readFully(bytes);
return new String(bytes);
return StringUtil.decode(bytes, 0, bytes.length, "UTF-16");
}
static DataInputStream createPackBitsStream(final ImageInputStream pInput, long pLength) {
@@ -0,0 +1,57 @@
package com.twelvemonkeys.imageio.plugins.psd;
import javax.imageio.stream.ImageInputStream;
import java.io.IOException;
/**
* PSDVersionInfo
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: PSDVersionInfo.java,v 1.0 Nov 6, 2009 1:02:19 PM haraldk Exp$
*/
final class PSDVersionInfo extends PSDImageResource {
int mVersion;
boolean mHasRealMergedData;
String mWriter;
String mReader;
int mFileVersion;
PSDVersionInfo(final short pId, final ImageInputStream pInput) throws IOException {
super(pId, pInput);
}
@Override
protected void readData(final ImageInputStream pInput) throws IOException {
/*
4 bytes version
1 byte hasRealMergedData
Unicode string: writer name
Unicode string: reader name
4 bytes file version.
*/
mVersion = pInput.readInt();
mHasRealMergedData = pInput.readBoolean();
mWriter = PSDUtil.readUnicodeString(pInput);
mReader = PSDUtil.readUnicodeString(pInput);
mFileVersion = pInput.readInt();
}
@Override
public String toString() {
StringBuilder builder = toStringBuilder();
builder.append(", version: ").append(mVersion);
builder.append(", hasRealMergedData: ").append(mHasRealMergedData);
builder.append(", writer: ").append(mWriter);
builder.append(", reader: ").append(mReader);
builder.append(", file version: ").append(mFileVersion);
builder.append("]");
return builder.toString();
}
}
@@ -1,5 +1,7 @@
package com.twelvemonkeys.imageio.plugins.psd;
import com.twelvemonkeys.imageio.metadata.Directory;
import com.twelvemonkeys.imageio.metadata.xmp.XMPReader;
import com.twelvemonkeys.lang.StringUtil;
import javax.imageio.stream.ImageInputStream;
@@ -21,6 +23,7 @@ import java.nio.charset.Charset;
*/
final class PSDXMPData extends PSDImageResource {
protected byte[] mData;
Directory mDirectory;
PSDXMPData(final short pId, final ImageInputStream pInput) throws IOException {
super(pId, pInput);
@@ -29,7 +32,9 @@ final class PSDXMPData extends PSDImageResource {
@Override
protected void readData(final ImageInputStream pInput) throws IOException {
mData = new byte[(int) mSize]; // TODO: Fix potential overflow, or document why that can't happen (read spec)
pInput.readFully(mData);
//pInput.readFully(mData);
mDirectory = new XMPReader().read(pInput);
}
@Override
@@ -47,7 +52,7 @@ final class PSDXMPData extends PSDImageResource {
builder.append("\"]");
return builder.toString();
}
}
/**
* Returns a character stream containing the XMP metadata (XML).
+2 -1
View File
@@ -2,4 +2,5 @@ Implement source subsampling and region of interest
Separate package for the resources (seems to be a lot)?
Possibility to read only some resources? readResources(int[] resourceKeys)?
- Probably faster when we only need the color space
PSDImageWriter
Support for Photoshop specific TIFF tags (extension for TIFFImageReader)?
PSDImageWriter?
+2 -2
View File
@@ -5,7 +5,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>twelvemonkeys-imageio-reference</artifactId>
<version>2.2</version>
<version>2.3</version>
<name>TwelveMonkeys ImageIO reference test cases</name>
<description>
Test cases for the JDK provided ImageReader implementations for reference.
@@ -14,7 +14,7 @@
<parent>
<artifactId>twelvemonkeys-imageio</artifactId>
<groupId>com.twelvemonkeys</groupId>
<version>2.2</version>
<version>2.3</version>
</parent>
<dependencies>

Some files were not shown because too many files have changed in this diff Show More