diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..2bbb3f61 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,6 @@ +language: java +jdk: + - oraclejdk8 + - oraclejdk7 +# Some JPEGImageReader tests fail on OpenJDK, need to investigate/fix before enabling +# - openjdk7 diff --git a/README.md b/README.md index 325fdef4..5b8b0e42 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,15 @@ -## Background +## Latest -TwelveMonkeys ImageIO is a collection of plug-ins for Java's ImageIO. +Master branch build status: [![Build Status](https://travis-ci.org/haraldk/TwelveMonkeys.svg?branch=master)](https://travis-ci.org/haraldk/TwelveMonkeys) + +TwelveMonkeys ImageIO [3.2.1](http://search.maven.org/#search%7Cga%7C1%7Cg%3Acom.twelvemonkeys*%20AND%20v%3A%223.2.1%22) is released (Dec. 11th, 2015). + +## About + +TwelveMonkeys ImageIO is a collection of plugins and extensions for Java's ImageIO. These plugins extends the number of image file formats supported in Java, using the javax.imageio.* package. -The main purpose of this project is to provide support for formats not covered by the JDK itself. +The main purpose of this project is to provide support for formats not covered by the JRE itself. Support for formats is important, both to be able to read data found "in the wild", as well as to maintain access to data in legacy formats. @@ -16,27 +22,38 @@ The goal is to create a set of efficient and robust ImageIO plug-ins, that can b Mainstream format support +#### BMP - MS Windows/IBM OS/2 Device Independent Bitmap + +* Read support for all known versions of the DIB/BMP format + * Indexed color, 1, 4 and 8 bit, including 4 and 8 bit RLE + * RGB, 16, 24 and 32 bit + * Embedded PNG and JPEG data + * Windows and OS/2 versions +* Native and standard metadata format + #### JPEG -* Read support for the following JPEG flavors: +* Read support for the following JPEG "flavors": + * All JFIF compliant JPEGs + * All Exif compliant JPEGs * YCbCr JPEGs without JFIF segment (converted to RGB, using embedded ICC profile) - * CMYK JPEGs (converted to RGB by default or as CMYK, using embedded ICC profile ) + * CMYK JPEGs (converted to RGB by default or as CMYK, using embedded ICC profile) * Adobe YCCK JPEGs (converted to RGB by default or as CMYK, using embedded ICC profile) - * JPEGs containing ICC profiles with interpretation other than 'Perceptual' - * JPEGs containing ICC profiles with class other than 'Display' - * JPEGs containing ICC profiles that are incompatible with stream data - * JPEGs with corrupted ICC profiles - * JPEGs with corrupted `ICC_PROFILE` segments + * JPEGs containing ICC profiles with interpretation other than 'Perceptual' or class other than 'Display' + * JPEGs containing ICC profiles that are incompatible with stream data, corrupted ICC profiles or corrupted `ICC_PROFILE` segments * JPEGs using non-standard color spaces, unsupported by Java 2D - * Issues warnings instead of throwing exceptions in cases of corrupted or non-conformant data where ever the image data can still be read in a reasonable way + * JPEGs with APP14/Adobe segments with length other than 14 bytes + * 8 bit JPEGs with 16 bit DQT segments + * Issues warnings instead of throwing exceptions in cases of corrupted or non-conformant data where ever the image + data can still be read in a reasonable way * Thumbnail support: - * JFIF thumbnails (even if stream contains inconsistent metadata) + * JFIF thumbnails (even if stream contains "inconsistent metadata") * JFXX thumbnails (JPEG, Indexed and RGB) * EXIF thumbnails (JPEG, RGB and YCbCr) * Metadata support: - * JPEG metadata in both standard and native formats (even if stream contains inconsistent metadata) + * JPEG metadata in both standard and native formats (even if stream contains "inconsistent metadata") * `javax_imageio_jpeg_image_1.0` format (currently as native format, may change in the future) - * Illegal combinations of JFIF, Exif and Adobe markers, using "unknown" segments in the + * Non-conforming combinations of JFIF, Exif and Adobe markers, using "unknown" segments in the "MarkerSequence" tag for the unsupported segments (for `javax_imageio_jpeg_image_1.0` format) * Extended write support in progress: * CMYK JPEGs @@ -46,10 +63,13 @@ Mainstream format support * Possibly coming in the future, pending some license issues. -If you are one of the authors, or know one of the authors and/or the current license holders of either the original jj2000 package or the JAI ImageIO project, please contact me -(I've tried to get in touch in various ways, without success so far). +If you are one of the authors, or know one of the authors and/or the current license holders of either the original +jj2000 package or the JAI ImageIO project, please contact me (I've tried to get in touch in various ways, +without success so far). -#### NetPBM Portable Any Map (PNM) *3.1* +Alternatively, if you have or know of a JPEG-2000 implementation in Java with a suitable license, get in touch. :-) + +#### PNM - NetPBM Portable Any Map * Read support for the following file types: * PBM in 'P1' (ASCII) and 'P4' (binary) formats, 1 bit per pixel @@ -60,8 +80,9 @@ If you are one of the authors, or know one of the authors and/or the current lic * Write support for the following formats: * PPM in 'P6' (binary) format * PAM in 'P7' (binary) format +* Standard metadata support -#### Adobe Photoshop Document (PSD) +#### PSD - Adobe Photoshop Document * Read support for the following file types: * Monochrome, 1 channel, 1 bit @@ -72,15 +93,16 @@ If you are one of the authors, or know one of the authors and/or the current lic * CMYK, 4-5 channels, 8, 16 and 32 bit * Read support for the following compression types: * Uncompressed - * RLE (PackBits) + * RLE (PackBits)< * Layer support * Image layers only, in all of the above types * Thumbnail support * JPEG * RAW (RGB) * Support for "Large Document Format" (PSB) +* Native and Standard metadata support -#### Aldus/Adobe Tagged Image File Format (TIFF) +#### TIFF - Aldus/Adobe Tagged Image File Format * Read support for the following "Baseline" TIFF file types: * Class B (Bi-level), all relevant compression types, 1 bit per sample @@ -89,27 +111,42 @@ If you are one of the authors, or know one of the authors and/or the current lic * Class R (RGB), all relevant compression types, 8 or 16 bits per sample, unsigned integer * Read support for the following TIFF extensions: * Tiling + * Class F (Facsimile), CCITT Modified Huffman RLE, T4 and T6 (type 2, 3 and 4) compressions. * LZW Compression (type 5) * "Old-style" JPEG Compression (type 6), as a best effort, as the spec is not well-defined * JPEG Compression (type 7) * ZLib (aka Adobe-style Deflate) Compression (type 8) * Deflate Compression (type 32946) * Horizontal differencing Predictor (type 2) for LZW, ZLib, Deflate and PackBits compression - * Alpha channel (ExtraSamples type 1/Associated Alpha) + * Alpha channel (ExtraSamples type 1/Associated Alpha and type 2/Unassociated Alpha) * CMYK data (PhotometricInterpretation type 5/Separated) * YCbCr data (PhotometricInterpretation type 6/YCbCr) for JPEG + * CIELab data in TIFF, ITU and ICC variants (PhotometricInterpretation type 9, 10 and 11) * Planar data (PlanarConfiguration type 2/Planar) * ICC profiles (ICCProfile) * BitsPerSample values up to 16 for most PhotometricInterpretations * Multiple images (pages) in one file -* Write support in progress - * Will support writing most "Baseline" TIFF file types +* Write support for most "Baseline" TIFF options + * Uncompressed, PackBits, ZLib and Deflate + * Additional support for CCITT T4 and and T6 compressions. + * Additional support for LZW and JPEG (type 7) compressions + * Horizontal differencing Predictor (type 2) for LZW, ZLib, Deflate +* Native and Standard metadata support Legacy formats -#### Commodore Amiga/Electronic Arts Interchange File Format (IFF) +#### HDR - Radiance High Dynamic Range RGBE Format -* Legacy format, allows reading popular image from the Commodore Amiga computer. +* Read support for the most common RGBE (.hdr) format +* Samples are converted to 32 bit floating point (`float`) and normalized using a global tone mapper by default. + * Support for custom global tone mappers + * Alternatively, use a "null-tone mapper", for unnormalized data (allows local tone mapping) +* Unconverted RGBE samples accessible using `readRaster` +* Standard metadata support + +#### IFF - Commodore Amiga/Electronic Arts Interchange File Format + +* Legacy format, allows reading popular image format from the Commodore Amiga computer. * Read support for the following file types: * ILBM Indexed color, 1-8 interleaved bit planes, including 6 bit EHB * ILBM Gray, 8 bit interleaved bit planes @@ -125,7 +162,7 @@ Legacy formats * Uncompressed * RLE (PackBits) -#### ZSoft Paintbrush Format (PCX) *3.1* +#### PCX - ZSoft Paintbrush Format * Read support for the following file types: * Indexed color, 1, 2, 4 or 8 bits per pixel, bit planes or interleaved @@ -135,8 +172,9 @@ Legacy formats * Support for the following compression types: * Uncompressed (experimental) * RLE compressed +* Standard metadata support -#### Apple Mac Paint Picture Format (PICT) +#### PICT - Apple Mac Paint Picture Format * Legacy format, especially useful for reading OS X clipboard data. * Read support for the following file types: @@ -147,7 +185,7 @@ Legacy formats * Write support for RGB pixel data: * QuickDraw pixmap -#### Silicon Graphics Image Format (SGI) *3.1* +#### SGI - Silicon Graphics Image Format * Read support for the following file types: * 1, 2, 3 or 4 channel image data @@ -155,8 +193,9 @@ Legacy formats * Support for the following compression types: * Uncompressed * RLE compressed +* Standard metadata support -#### Truevision TGA Image Format (TGA) *3.1* +#### TGA - Truevision TGA Image Format * Read support for the following file types: * ColorMapped @@ -165,38 +204,43 @@ Legacy formats * Support for the following compression types: * Uncompressed * RLE compressed +* Standard metadata support Icon/other formats -#### Apple Icon Image (ICNS) +#### ICNS - Apple Icon Image * Read support for the following icon types: * All known "native" icon types * Large PNG encoded icons * Large JPEG 2000 encoded icons (requires JPEG 2000 ImageIO plugin or fallback to `sips` command line tool) -#### MS Windows Icon and Cursor Formats (ICO & CUR) +#### ICO & CUR - MS Windows Icon and Cursor Formats * Read support for the following file types: * ICO Indexed color, 1, 4 and 8 bit * ICO RGB, 16, 24 and 32 bit * CUR Indexed color, 1, 4 and 8 bit * CUR RGB, 16, 24 and 32 bit +* *3.1* Note: These formats are now part of the BMP plugin -#### MS Windows Thumbs DB (Thumbs.db) +#### Thumbs.db - MS Windows Thumbs DB * Read support Other formats, using 3rd party libraries -#### Scalable Vector Graphics (SVG) +#### SVG - Scalable Vector Graphics * Read-only support using Batik -#### MS Windows MetaFile (WMF) +#### WMF - MS Windows MetaFile * Limited read-only support using Batik +**Important note on using Batik:** *Please read [The Apache™ XML Graphics Project - Security](http://xmlgraphics.apache.org/security.html), and make sure you use +either version 1.6.1, 1.7.1 or 1.8+.* + ## Basic usage @@ -323,11 +367,12 @@ Unless you add `ImageIO.scanForPlugins()` somewhere in your code, the plugins mi I addition, servlet contexts dynamically loads and unloads classes (using a new class loader per context). If you restart your application, old classes will by default remain in memory forever (because the next time `scanForPlugins` is called, it's another `ClassLoader` that scans/loads classes, and thus they will be new instances -in the registry). If a read is attempted using one of the remaining ("old") readers, weird exceptions -(like `NullPointerException`s when accessing `static final` initialized fields) may occur. +in the registry). If a read is attempted using one of the remaining "old" readers, weird exceptions +(like `NullPointerException`s when accessing `static final` initialized fields or `NoClassDefFoundError`s +for uninitialized inner classes) may occur. To work around both the discovery problem and the resource leak, -it is recommended to use the `IIOProviderContextListener` that implements +it is *strongly recommended* to use the `IIOProviderContextListener` that implements dynamic loading and unloading of ImageIO plugins for web applications. @@ -343,6 +388,12 @@ dynamic loading and unloading of ImageIO plugins for web applications. +Loading plugins from `WEB-INF/lib` without the context listener installed is unsupported and will not work correctly. + +The context listener has no dependencies to the TwelveMonkeys ImageIO plugins, and may be used with JAI ImageIO +or other ImageIO plugins as well. + +Another safe option, is to place the JAR files in the application server's shared or common lib folder. #### Using the ResampleOp @@ -388,9 +439,9 @@ Build the project (using [Maven](http://maven.apache.org/download.cgi)): $ mvn package -Currently, the only supported JDK for making a build is Oracle JDK 7.x. +Currently, the recommended JDK for making a build is Oracle JDK 7.x or 8.x. -It's possible to build using OpenJDK, but some tests will fail due to some minor differences between the color management systems used. You will need to either disable the tests in question, or build without tests altogether. To build using JDK 8, you need to pass `-Dsun.java2d.cmm=sun.java2d.cmm.kcms.KcmsServiceProvider` to revert to the color manangement system used in Java 7. +It's possible to build using OpenJDK, but some tests might fail due to some minor differences between the color management systems used. You will need to either disable the tests in question, or build without tests altogether. Because the unit tests needs quite a bit of memory to run, you might have to set the environment variable `MAVEN_OPTS` to give the Java process that runs Maven more memory. I suggest something like `-Xmx512m -XX:MaxPermSize=256m`. @@ -416,7 +467,7 @@ To verify that the JPEG plugin is installed and used at run-time, you could use The first line should print: - reader: com.twelvemonkeys.imageio.jpeg.JPEGImageReader@somehash + reader: com.twelvemonkeys.imageio.plugins.jpeg.JPEGImageReader@somehash #### Maven dependency example @@ -428,12 +479,12 @@ To depend on the JPEG and TIFF plugin using Maven, add the following to your POM com.twelvemonkeys.imageio imageio-jpeg - 3.0.2 + 3.2.1 com.twelvemonkeys.imageio imageio-tiff - 3.0.2 + 3.2.1 @@ -441,25 +492,66 @@ To depend on the JPEG and TIFF plugin using Maven, add the following to your POM To depend on the JPEG and TIFF plugin in your IDE or program, add all of the following JARs to your class path: - twelvemonkeys-common-lang-3.0.2.jar - twelvemonkeys-common-io-3.0.2.jar - twelvemonkeys-common-image-3.0.2.jar - twelvemonkeys-imageio-core-3.0.2.jar - twelvemonkeys-imageio-metadata-3.0.2.jar - twelvemonkeys-imageio-jpeg-3.0.2.jar - twelvemonkeys-imageio-tiff-3.0.2.jar + twelvemonkeys-common-lang-3.2.1.jar + twelvemonkeys-common-io-3.2.1.jar + twelvemonkeys-common-image-3.2.1.jar + twelvemonkeys-imageio-core-3.2.1.jar + twelvemonkeys-imageio-metadata-3.2.1.jar + twelvemonkeys-imageio-jpeg-3.2.1.jar + twelvemonkeys-imageio-tiff-3.2.1.jar ### Links to prebuilt binaries +##### Latest version (3.2.x) + +Requires Java 7 or later. + +Common dependencies +* [common-lang-3.2.1.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/common/common-lang/3.2.1/common-lang-3.2.1.jar) +* [common-io-3.2.1.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/common/common-io/3.2.1/common-io-3.2.1.jar) +* [common-image-3.2.1.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/common/common-image/3.2.1/common-image-3.2.1.jar) + +ImageIO dependencies +* [imageio-core-3.2.1.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/imageio/imageio-core/3.2.1/imageio-core-3.2.1.jar) +* [imageio-metadata-3.2.1.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/imageio/imageio-metadata/3.2.1/imageio-metadata-3.2.1.jar) + +ImageIO plugins +* [imageio-bmp-3.2.1.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/imageio/imageio-bmp/3.2.1/imageio-bmp-3.2.1.jar) +* [imageio-jpeg-3.2.1.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/imageio/imageio-jpeg/3.2.1/imageio-jpeg-3.2.1.jar) +* [imageio-tiff-3.2.1.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/imageio/imageio-tiff/3.2.1/imageio-tiff-3.2.1.jar) +* [imageio-pnm-3.2.1.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/imageio/imageio-pnm/3.2.1/imageio-pnm-3.2.1.jar) +* [imageio-psd-3.2.1.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/imageio/imageio-psd/3.2.1/imageio-psd-3.2.1.jar) +* [imageio-hdr-3.2.1.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/imageio/imageio-hdr/3.2.1/imageio-hdr-3.2.1.jar) +* [imageio-iff-3.2.1.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/imageio/imageio-iff/3.2.1/imageio-iff-3.2.1.jar) +* [imageio-pcx-3.2.1.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/imageio/imageio-pcx/3.2.1/imageio-pcx-3.2.1.jar) +* [imageio-pict-3.2.1.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/imageio/imageio-pict/3.2.1/imageio-pict-3.2.1.jar) +* [imageio-sgi-3.2.1.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/imageio/imageio-sgi/3.2.1/imageio-sgi-3.2.1.jar) +* [imageio-tga-3.2.1.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/imageio/imageio-tga/3.2.1/imageio-tga-3.2.1.jar) +* [imageio-icns-3.2.1.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/imageio/imageio-icns/3.2.1/imageio-icns-3.2.1.jar) +* [imageio-thumbsdb-3.2.1.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/imageio/imageio-thumbsdb/3.2.1/imageio-thumbsdb-3.2.1.jar) + +ImageIO plugins requiring 3rd party libs +* [imageio-batik-3.2.1.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/imageio/imageio-batik/3.2.1/imageio-batik-3.2.1.jar) + +Photoshop Path support for ImageIO +* [imageio-clippath-3.2.1.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/imageio/imageio-clippath/3.2.1/imageio-clippath-3.2.1.jar) + +Servlet support +* [servlet-3.2.1.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/servlet/servlet/3.2.1/servlet-3.2.1.jar) + +##### Old version (3.0.x) + +Use this version for projects that requires Java 6 or need the JMagick support. *Does not support Java 8*. + Common dependencies * [common-lang-3.0.2.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/common/common-lang/3.0.2/common-lang-3.0.2.jar) * [common-io-3.0.2.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/common/common-io/3.0.2/common-io-3.0.2.jar) * [common-image-3.0.2.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/common/common-image/3.0.2/common-image-3.0.2.jar) - + ImageIO dependencies * [imageio-core-3.0.2.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/imageio/imageio-core/3.0.2/imageio-core-3.0.2.jar) * [imageio-metadata-3.0.2.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/imageio/imageio-metadata/3.0.2/imageio-metadata-3.0.2.jar) - + ImageIO plugins * [imageio-jpeg-3.0.2.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/imageio/imageio-jpeg/3.0.2/imageio-jpeg-3.0.2.jar) * [imageio-tiff-3.0.2.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/imageio/imageio-tiff/3.0.2/imageio-tiff-3.0.2.jar) @@ -469,19 +561,20 @@ ImageIO plugins * [imageio-icns-3.0.2.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/imageio/imageio-icns/3.0.2/imageio-icns-3.0.2.jar) * [imageio-ico-3.0.2.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/imageio/imageio-ico/3.0.2/imageio-ico-3.0.2.jar) * [imageio-thumbsdb-3.0.2.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/imageio/imageio-thumbsdb/3.0.2/imageio-thumbsdb-3.0.2.jar) - + ImageIO plugins requiring 3rd party libs * [imageio-batik-3.0.2.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/imageio/imageio-batik/3.0.2/imageio-batik-3.0.2.jar) * [imageio-jmagick-3.0.2.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/imageio/imageio-jmagick/3.0.2/imageio-jmagick-3.0.2.jar) - + Servlet support * [servlet-3.0.2.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/servlet/servlet/3.0.2/servlet-3.0.2.jar) + ## License The project is distributed under the OSI approved [BSD license](http://opensource.org/licenses/BSD-3-Clause): - Copyright (c) 2008-2013, Harald Kuhr + Copyright (c) 2008-2015, Harald Kuhr All rights reserved. Redistribution and use in source and binary forms, with or without @@ -521,7 +614,7 @@ a: The easiest way is to build your own project using Maven, and just add depend q: What changes do I have to make to my code in order to use the plug-ins? -a: The short answer is: None. For basic usage, like ImageIO.read(...) or ImageIO.getImageReaders(...), there is no need +a: The short answer is: None. For basic usage, like `ImageIO.read(...)` or `ImageIO.getImageReaders(...)`, there is no need to change your code. Most of the functionality is available through standard ImageIO APIs, and great care has been taken not to introduce extra API where none is necessary. @@ -538,11 +631,11 @@ All you have have to do, is to make sure you have the TwelveMonkeys JARs in your You can read more about the registry and the lookup mechanism in the [IIORegistry API doc](http://docs.oracle.com/javase/7/docs/api/javax/imageio/spi/IIORegistry.html). -The fine print: The TwelveMonkeys service providers for TIFF and JPEG overrides the onRegistration method, and -utilizes the pairwise partial ordering mechanism of the IIOServiceRegistry to make sure it is installed before -the Sun/Oracle provided JPEGImageReader and the Apple provided TIFFImageReader on OS X, respectively. -Using the pairwise ordering will not remove any functionality form these implementations, but in most cases you'll end -up using the TwelveMonkeys plug-ins instead. +The fine print: The TwelveMonkeys service providers for JPEG, BMP and TIFF, overrides the onRegistration method, and +utilizes the pairwise partial ordering mechanism of the `IIOServiceRegistry` to make sure it is installed before +the Sun/Oracle provided `JPEGImageReader` and `BMPImageReader`, and the Apple provided `TIFFImageReader` on OS X, +respectively. Using the pairwise ordering will not remove any functionality form these implementations, but in most +cases you'll end up using the TwelveMonkeys plug-ins instead. q: What about JAI? Several of the formats are already supported by JAI. diff --git a/bom/pom.xml b/bom/pom.xml new file mode 100644 index 00000000..a684765c --- /dev/null +++ b/bom/pom.xml @@ -0,0 +1,142 @@ + + + 4.0.0 + + + com.twelvemonkeys + twelvemonkeys + 3.3-SNAPSHOT + + + com.twelvemonkeys.bom + bom + pom + TwelveMonkeys :: BOM + + TwelveMonkeys "Bill of Materials" (BOM). + + + + + + + com.twelvemonkeys.common + common-lang + ${project.version} + + + com.twelvemonkeys.common + common-io + ${project.version} + + + com.twelvemonkeys.common + common-image + ${project.version} + + + + + com.twelvemonkeys.imageio + imageio-core + ${project.version} + + + com.twelvemonkeys.imageio + imageio-metadata + ${project.version} + + + com.twelvemonkeys.imageio + imageio-clippath + ${project.version} + + + + + com.twelvemonkeys.imageio + imageio-bmp + ${project.version} + + + com.twelvemonkeys.imageio + imageio-hdr + ${project.version} + + + com.twelvemonkeys.imageio + imageio-icns + ${project.version} + + + com.twelvemonkeys.imageio + imageio-iff + ${project.version} + + + com.twelvemonkeys.imageio + imageio-jpeg + ${project.version} + + + com.twelvemonkeys.imageio + imageio-pcx + ${project.version} + + + com.twelvemonkeys.imageio + imageio-pdf + ${project.version} + + + com.twelvemonkeys.imageio + imageio-pict + ${project.version} + + + com.twelvemonkeys.imageio + imageio-pnm + ${project.version} + + + com.twelvemonkeys.imageio + imageio-psd + ${project.version} + + + com.twelvemonkeys.imageio + imageio-sgi + ${project.version} + + + com.twelvemonkeys.imageio + imageio-tga + ${project.version} + + + com.twelvemonkeys.imageio + imageio-thumbsdb + ${project.version} + + + com.twelvemonkeys.imageio + imageio-tiff + ${project.version} + + + + + com.twelvemonkeys.imageio + imageio-batik + ${project.version} + + + + + com.twelvemonkeys.servlet + servlet + ${project.version} + + + + \ No newline at end of file diff --git a/common/common-image/pom.xml b/common/common-image/pom.xml index f4cd382f..55cfc26c 100644 --- a/common/common-image/pom.xml +++ b/common/common-image/pom.xml @@ -4,7 +4,7 @@ com.twelvemonkeys.common common - 3.1-SNAPSHOT + 3.3-SNAPSHOT common-image jar diff --git a/common/common-image/src/main/java/com/twelvemonkeys/image/ImageUtil.java b/common/common-image/src/main/java/com/twelvemonkeys/image/ImageUtil.java index 4298a88d..88fd7f1d 100644 --- a/common/common-image/src/main/java/com/twelvemonkeys/image/ImageUtil.java +++ b/common/common-image/src/main/java/com/twelvemonkeys/image/ImageUtil.java @@ -358,8 +358,9 @@ public final class ImageUtil { } /** - * Creates a copy of the given image. The image will have the same - * color model and raster type, but will not share image (pixel) data. + * Creates a deep copy of the given image. The image will have the same + * color model and raster type, but will not share image (pixel) data + * with the input image. * * @param pImage the image to clone. * @@ -378,7 +379,7 @@ public final class ImageUtil { cm.createCompatibleWritableRaster(pImage.getWidth(), pImage.getHeight()), cm.isAlphaPremultiplied(), null); - drawOnto(pImage, img); + drawOnto(img, pImage); return img; } diff --git a/common/common-image/src/main/java/com/twelvemonkeys/image/ResampleOp.java b/common/common-image/src/main/java/com/twelvemonkeys/image/ResampleOp.java index c2132eed..73cc9ccb 100644 --- a/common/common-image/src/main/java/com/twelvemonkeys/image/ResampleOp.java +++ b/common/common-image/src/main/java/com/twelvemonkeys/image/ResampleOp.java @@ -547,7 +547,7 @@ public class ResampleOp implements BufferedImageOp/* TODO: RasterOp */ { // TODO: What if output != null and wrong size? Create new? Render on only a part? Document? - // If filter type != POINT or BOX an input has IndexColorModel, convert + // If filter type != POINT or BOX and input has IndexColorModel, convert // to true color, with alpha reflecting that of the original color model. BufferedImage temp; ColorModel cm; @@ -590,7 +590,7 @@ public class ResampleOp implements BufferedImageOp/* TODO: RasterOp */ { /* // TODO: This idea from Chet and Romain is actually not too bad.. // It reuses the image/raster/graphics... - // However, they forget to end with a halve operation.. + // However, they don't end with a halve operation.. private static BufferedImage getFasterScaledInstance(BufferedImage img, int targetWidth, int targetHeight, Object hint, boolean progressiveBilinear) { @@ -895,7 +895,7 @@ public class ResampleOp implements BufferedImageOp/* TODO: RasterOp */ { * filter function definitions */ - static interface InterpolationFilter { + interface InterpolationFilter { double filter(double t); double support(); @@ -1336,7 +1336,7 @@ public class ResampleOp implements BufferedImageOp/* TODO: RasterOp */ { } //contribX.n = 0; - contribX.p = new Contributor[(int) (width * 2.0 + 1.0)]; + contribX.p = new Contributor[(int) (width * 2.0 + 1.0 + 0.5)]; center = (double) i / xscale; int left = (int) Math.ceil(center - width);// Note: Assumes width <= .5 @@ -1387,7 +1387,7 @@ public class ResampleOp implements BufferedImageOp/* TODO: RasterOp */ { else { /* Expanding image */ //contribX.n = 0; - contribX.p = new Contributor[(int) (fwidth * 2.0 + 1.0)]; + contribX.p = new Contributor[(int) (fwidth * 2.0 + 1.0 + 0.5)]; center = (double) i / xscale; int left = (int) Math.ceil(center - fwidth); @@ -1465,7 +1465,7 @@ public class ResampleOp implements BufferedImageOp/* TODO: RasterOp */ { for (int i = 0; i < dstHeight; i++) { //contribY[i].n = 0; - contribY[i].p = new Contributor[(int) (width * 2.0 + 1)]; + contribY[i].p = new Contributor[(int) (width * 2.0 + 1 + 0.5)]; double center = (double) i / yscale; int left = (int) Math.ceil(center - width); @@ -1516,7 +1516,7 @@ public class ResampleOp implements BufferedImageOp/* TODO: RasterOp */ { else { for (int i = 0; i < dstHeight; ++i) { //contribY[i].n = 0; - contribY[i].p = new Contributor[(int) (fwidth * 2 + 1)]; + contribY[i].p = new Contributor[(int) (fwidth * 2 + 1 + 0.5)]; double center = (double) i / yscale; double left = Math.ceil(center - fwidth); diff --git a/common/common-image/src/test/java/com/twelvemonkeys/image/ImageUtilTestCase.java b/common/common-image/src/test/java/com/twelvemonkeys/image/ImageUtilTestCase.java index a35d71dc..4f8cd92d 100755 --- a/common/common-image/src/test/java/com/twelvemonkeys/image/ImageUtilTestCase.java +++ b/common/common-image/src/test/java/com/twelvemonkeys/image/ImageUtilTestCase.java @@ -103,11 +103,6 @@ public class ImageUtilTestCase { // Should have same dimensions assertEquals(scaled.getWidth(null), bufferedScaled.getWidth()); assertEquals(scaled.getHeight(null), bufferedScaled.getHeight()); - - // Hmmm... - assertTrue(new Integer(42).equals(bufferedScaled.getProperty("lucky-number")) - || bufferedScaled.getPropertyNames() == null - || bufferedScaled.getPropertyNames().length == 0); } @Test diff --git a/common/common-image/src/test/java/com/twelvemonkeys/image/ResampleOpTestCase.java b/common/common-image/src/test/java/com/twelvemonkeys/image/ResampleOpTestCase.java index b7d1d036..316fac6e 100644 --- a/common/common-image/src/test/java/com/twelvemonkeys/image/ResampleOpTestCase.java +++ b/common/common-image/src/test/java/com/twelvemonkeys/image/ResampleOpTestCase.java @@ -69,7 +69,7 @@ public class ResampleOpTestCase { } private void assertResampleBufferedImageTypes(final int pFilterType) { - List exceptions = new ArrayList(); + List exceptions = new ArrayList<>(); // Test all image types in BufferedImage for (int type = BufferedImage.TYPE_INT_ARGB; type <= BufferedImage.TYPE_BYTE_INDEXED; type++) { @@ -304,6 +304,30 @@ public class ResampleOpTestCase { assertResampleBufferedImageTypes(ResampleOp.FILTER_LANCZOS); } + // https://github.com/haraldk/TwelveMonkeys/issues/195 + @Test + public void testAIOOBEHeight() { + BufferedImage myImage = new BufferedImage(100, 354, BufferedImage.TYPE_INT_ARGB); + + for (int i = 19; i > 0; i--) { + ResampleOp resampler = new ResampleOp(100, i, ResampleOp.FILTER_LANCZOS); + BufferedImage resizedImage = resampler.filter(myImage, null); + assertNotNull(resizedImage); + } + } + + // https://github.com/haraldk/TwelveMonkeys/issues/195 + @Test + public void testAIOOBEWidth() { + BufferedImage myImage = new BufferedImage(2832, 283, BufferedImage.TYPE_INT_ARGB); + + for (int i = 145; i > 143; i--) { + ResampleOp resampler = new ResampleOp(i, 14, ResampleOp.FILTER_LANCZOS); + BufferedImage resizedImage = resampler.filter(myImage, null); + assertNotNull(resizedImage); + } + } + @Ignore("Not for general unit testing") @Test public void testTime() { diff --git a/common/common-io/pom.xml b/common/common-io/pom.xml index 2d59a1c9..b4d3207c 100644 --- a/common/common-io/pom.xml +++ b/common/common-io/pom.xml @@ -4,7 +4,7 @@ com.twelvemonkeys.common common - 3.1-SNAPSHOT + 3.3-SNAPSHOT common-io jar @@ -22,7 +22,7 @@ ${project.groupId} common-lang - tests + test-jar test diff --git a/common/common-lang/pom.xml b/common/common-lang/pom.xml index 57789000..f3436902 100644 --- a/common/common-lang/pom.xml +++ b/common/common-lang/pom.xml @@ -4,7 +4,7 @@ com.twelvemonkeys.common common - 3.1-SNAPSHOT + 3.3-SNAPSHOT common-lang jar diff --git a/common/common-lang/src/main/java/com/twelvemonkeys/lang/DateUtil.java b/common/common-lang/src/main/java/com/twelvemonkeys/lang/DateUtil.java index 09e9f5e9..1234eb3c 100755 --- a/common/common-lang/src/main/java/com/twelvemonkeys/lang/DateUtil.java +++ b/common/common-lang/src/main/java/com/twelvemonkeys/lang/DateUtil.java @@ -142,7 +142,7 @@ public final class DateUtil { * @param pTime time * @return the time rounded to the closest second. */ - public static long roundToSecond(long pTime) { + public static long roundToSecond(final long pTime) { return (pTime / SECOND) * SECOND; } @@ -152,7 +152,7 @@ public final class DateUtil { * @param pTime time * @return the time rounded to the closest minute. */ - public static long roundToMinute(long pTime) { + public static long roundToMinute(final long pTime) { return (pTime / MINUTE) * MINUTE; } @@ -162,9 +162,20 @@ public final class DateUtil { * @param pTime time * @return the time rounded to the closest hour. */ - public static long roundToHour(long pTime) { - // TODO: What if timezone offset is sub hour? Are there any? I think so... - return ((pTime / HOUR) * HOUR); + public static long roundToHour(final long pTime) { + return roundToHour(pTime, TimeZone.getDefault()); + } + + /** + * Rounds the given time down to the closest hour, using the given timezone. + * + * @param pTime time + * @param pTimeZone the timezone to use when rounding + * @return the time rounded to the closest hour. + */ + public static long roundToHour(final long pTime, final TimeZone pTimeZone) { + int offset = pTimeZone.getOffset(pTime); + return ((pTime / HOUR) * HOUR) - offset; } /** @@ -173,7 +184,7 @@ public final class DateUtil { * @param pTime time * @return the time rounded to the closest day. */ - public static long roundToDay(long pTime) { + public static long roundToDay(final long pTime) { return roundToDay(pTime, TimeZone.getDefault()); } @@ -184,7 +195,7 @@ public final class DateUtil { * @param pTimeZone the timezone to use when rounding * @return the time rounded to the closest day. */ - public static long roundToDay(long pTime, TimeZone pTimeZone) { + public static long roundToDay(final long pTime, final TimeZone pTimeZone) { int offset = pTimeZone.getOffset(pTime); return (((pTime + offset) / DAY) * DAY) - offset; } diff --git a/common/common-lang/src/main/java/com/twelvemonkeys/lang/Platform.java b/common/common-lang/src/main/java/com/twelvemonkeys/lang/Platform.java index 7fb98e4c..d69f37c3 100755 --- a/common/common-lang/src/main/java/com/twelvemonkeys/lang/Platform.java +++ b/common/common-lang/src/main/java/com/twelvemonkeys/lang/Platform.java @@ -204,7 +204,7 @@ public final class Platform { * @author Harald Kuhr * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/Platform.java#1 $ */ - public static enum Architecture { + public enum Architecture { X86("x86"), I386("i386"), I686("i686"), @@ -215,7 +215,7 @@ public final class Platform { final String name;// for debug only - private Architecture(String pName) { + Architecture(String pName) { name = pName; } @@ -233,7 +233,7 @@ public final class Platform { * @author Harald Kuhr * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/Platform.java#1 $ */ - public static enum OperatingSystem { + public enum OperatingSystem { Windows("Windows", "win"), Linux("Linux", "lnx"), Solaris("Solaris", "sun"), @@ -244,7 +244,7 @@ public final class Platform { final String id; final String name;// for debug only - private OperatingSystem(String pName, String pId) { + OperatingSystem(String pName, String pId) { name = pName; id = pId != null ? pId : pName.toLowerCase(); } diff --git a/common/common-lang/src/test/java/com/twelvemonkeys/lang/DateUtilTest.java b/common/common-lang/src/test/java/com/twelvemonkeys/lang/DateUtilTest.java index f6dce2ab..6f4959e1 100644 --- a/common/common-lang/src/test/java/com/twelvemonkeys/lang/DateUtilTest.java +++ b/common/common-lang/src/test/java/com/twelvemonkeys/lang/DateUtilTest.java @@ -29,11 +29,15 @@ package com.twelvemonkeys.lang; import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import java.util.Arrays; import java.util.Calendar; +import java.util.List; import java.util.TimeZone; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; /** * DateUtilTest @@ -42,9 +46,30 @@ import static org.junit.Assert.*; * @author last modified by $Author: haraldk$ * @version $Id: DateUtilTest.java,v 1.0 11.04.12 16:21 haraldk Exp$ */ +@RunWith(Parameterized.class) public class DateUtilTest { - private static Calendar getCalendar(long time) { - Calendar calendar = Calendar.getInstance(TimeZone.getDefault()); + + private final TimeZone timeZone; + + @Parameterized.Parameters + public static List timeZones() { + return Arrays.asList(new Object[][] { + {TimeZone.getTimeZone("UTC")}, + {TimeZone.getTimeZone("CET")}, + {TimeZone.getTimeZone("IST")}, // 30 min off + }); + } + + public DateUtilTest(final TimeZone timeZone) { + this.timeZone = timeZone; + } + + private Calendar getCalendar(long time) { + return getCalendar(time, TimeZone.getDefault()); + } + + private Calendar getCalendar(long time, final TimeZone timeZone) { + Calendar calendar = Calendar.getInstance(timeZone); calendar.setTimeInMillis(time); return calendar; @@ -74,6 +99,15 @@ public class DateUtilTest { assertEquals(0, calendar.get(Calendar.MINUTE)); } + @Test + public void testRoundToHourTZ() { + Calendar calendar = getCalendar(DateUtil.roundToHour(System.currentTimeMillis(), timeZone), timeZone); + + assertEquals(0, calendar.get(Calendar.MILLISECOND)); + assertEquals(0, calendar.get(Calendar.SECOND)); + assertEquals(0, calendar.get(Calendar.MINUTE)); + } + @Test public void testRoundToDay() { Calendar calendar = getCalendar(DateUtil.roundToDay(System.currentTimeMillis())); @@ -84,6 +118,16 @@ public class DateUtilTest { assertEquals(0, calendar.get(Calendar.HOUR_OF_DAY)); } + @Test + public void testRoundToDayTZ() { + Calendar calendar = getCalendar(DateUtil.roundToDay(System.currentTimeMillis(), timeZone), timeZone); + + assertEquals(0, calendar.get(Calendar.MILLISECOND)); + assertEquals(0, calendar.get(Calendar.SECOND)); + assertEquals(0, calendar.get(Calendar.MINUTE)); + assertEquals(0, calendar.get(Calendar.HOUR_OF_DAY)); + } + @Test public void testCurrentTimeSecond() { Calendar calendar = getCalendar(DateUtil.currentTimeSecond()); diff --git a/common/pom.xml b/common/pom.xml index cd536b41..b6b9f6f7 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -4,7 +4,7 @@ com.twelvemonkeys twelvemonkeys - 3.1-SNAPSHOT + 3.3-SNAPSHOT com.twelvemonkeys.common common @@ -32,7 +32,7 @@ ${project.groupId} common-lang ${project.version} - tests + test-jar test diff --git a/contrib/pom.xml b/contrib/pom.xml new file mode 100644 index 00000000..9a73d3c6 --- /dev/null +++ b/contrib/pom.xml @@ -0,0 +1,72 @@ + + + 4.0.0 + + com.twelvemonkeys + twelvemonkeys + 3.3-SNAPSHOT + + com.twelvemonkeys.contrib + contrib + TwelveMonkeys :: Contrib + + Contributions to TwelveMonkeys which are not matching into the ImageIO plug-ins. + + + + + com.twelvemonkeys.common + common-lang + ${project.version} + + + com.twelvemonkeys.common + common-io + ${project.version} + + + com.twelvemonkeys.common + common-image + ${project.version} + + + com.twelvemonkeys.common + common-lang + ${project.version} + test-jar + test + + + com.twelvemonkeys.common + common-io + ${project.version} + test-jar + test + + + com.twelvemonkeys.imageio + imageio-metadata + ${project.version} + + + com.twelvemonkeys.imageio + imageio-tiff + ${project.version} + test + + + com.twelvemonkeys.imageio + imageio-tiff + ${project.version} + test-jar + test + + + + junit + junit + 4.7 + test + + + diff --git a/contrib/src/main/java/com/twelvemonkeys/contrib/tiff/TIFFUtilities.java b/contrib/src/main/java/com/twelvemonkeys/contrib/tiff/TIFFUtilities.java new file mode 100644 index 00000000..d1b57623 --- /dev/null +++ b/contrib/src/main/java/com/twelvemonkeys/contrib/tiff/TIFFUtilities.java @@ -0,0 +1,603 @@ +/* + * Copyright (c) 2013, 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.contrib.tiff; + +import com.twelvemonkeys.imageio.metadata.*; +import com.twelvemonkeys.imageio.metadata.exif.EXIFReader; +import com.twelvemonkeys.imageio.metadata.exif.EXIFWriter; +import com.twelvemonkeys.imageio.metadata.exif.Rational; +import com.twelvemonkeys.imageio.metadata.exif.TIFF; +import com.twelvemonkeys.lang.Validate; + +import javax.imageio.IIOException; +import javax.imageio.ImageIO; +import javax.imageio.stream.ImageInputStream; +import javax.imageio.stream.ImageOutputStream; +import java.awt.geom.AffineTransform; +import java.awt.image.AffineTransformOp; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; + +/** + * TIFFUtilities for manipulation TIFF Images and Metadata + * + * @author Oliver Schmidtmer + * @author last modified by $Author$ + * @version $Id$ + */ +public class TIFFUtilities { + private TIFFUtilities() { + } + + /** + * Merges all pages from the input TIFF files into one TIFF file at the + * output location. + * + * @param inputFiles + * @param outputFile + * @throws IOException + */ + public static void merge(List inputFiles, File outputFile) throws IOException { + ImageOutputStream output = null; + try { + output = ImageIO.createImageOutputStream(outputFile); + + for (File file : inputFiles) { + ImageInputStream input = null; + try { + input = ImageIO.createImageInputStream(file); + List pages = getPages(input); + writePages(output, pages); + } + finally { + if (input != null) { + input.close(); + } + } + } + } + finally { + if (output != null) { + output.flush(); + output.close(); + } + } + } + + /** + * Splits all pages from the input TIFF file to one file per page in the + * output directory. + * + * @param inputFile + * @param outputDirectory + * @return generated files + * @throws IOException + */ + public static List split(File inputFile, File outputDirectory) throws IOException { + ImageInputStream input = null; + List outputFiles = new ArrayList<>(); + try { + input = ImageIO.createImageInputStream(inputFile); + List pages = getPages(input); + int pageNo = 1; + for (TIFFPage tiffPage : pages) { + ArrayList outputPages = new ArrayList(1); + ImageOutputStream outputStream = null; + try { + File outputFile = new File(outputDirectory, String.format("%04d", pageNo) + ".tif"); + outputStream = ImageIO.createImageOutputStream(outputFile); + outputPages.clear(); + outputPages.add(tiffPage); + writePages(outputStream, outputPages); + outputFiles.add(outputFile); + } + finally { + if (outputStream != null) { + outputStream.flush(); + outputStream.close(); + } + } + ++pageNo; + } + } + finally { + if (input != null) { + input.close(); + } + } + return outputFiles; + } + + /** + * Rotates all pages of a TIFF file by changing TIFF.TAG_ORIENTATION. + *

+ * NOTICE: TIFF.TAG_ORIENTATION is an advice how the image is meant do be + * displayed. Other metadata, such as width and height, relate to the image + * as how it is stored. The ImageIO TIFF plugin does not handle orientation. + * Use {@link TIFFUtilities#applyOrientation(BufferedImage, int)} for + * applying TIFF.TAG_ORIENTATION. + *

+ * + * @param imageInput + * @param imageOutput + * @param degree Rotation amount, supports 90�, 180� and 270�. + * @throws IOException + */ + public static void rotatePages(ImageInputStream imageInput, ImageOutputStream imageOutput, int degree) + throws IOException { + rotatePage(imageInput, imageOutput, degree, -1); + } + + /** + * Rotates a page of a TIFF file by changing TIFF.TAG_ORIENTATION. + *

+ * NOTICE: TIFF.TAG_ORIENTATION is an advice how the image is meant do be + * displayed. Other metadata, such as width and height, relate to the image + * as how it is stored. The ImageIO TIFF plugin does not handle orientation. + * Use {@link TIFFUtilities#applyOrientation(BufferedImage, int)} for + * applying TIFF.TAG_ORIENTATION. + *

+ * + * @param imageInput + * @param imageOutput + * @param degree Rotation amount, supports 90�, 180� and 270�. + * @param pageIndex page which should be rotated or -1 for all pages. + * @throws IOException + */ + public static void rotatePage(ImageInputStream imageInput, ImageOutputStream imageOutput, int degree, int pageIndex) + throws IOException { + ImageInputStream input = null; + try { + List pages = getPages(imageInput); + if (pageIndex != -1) { + pages.get(pageIndex).rotate(degree); + } + else { + for (TIFFPage tiffPage : pages) { + tiffPage.rotate(degree); + } + } + writePages(imageOutput, pages); + } + finally { + if (input != null) { + input.close(); + } + } + } + + public static List getPages(ImageInputStream imageInput) throws IOException { + ArrayList pages = new ArrayList(); + + CompoundDirectory IFDs = (CompoundDirectory) new EXIFReader().read(imageInput); + + int pageCount = IFDs.directoryCount(); + for (int pageIndex = 0; pageIndex < pageCount; pageIndex++) { + pages.add(new TIFFPage(IFDs.getDirectory(pageIndex), imageInput)); + } + + return pages; + } + + public static void writePages(ImageOutputStream imageOutput, List pages) throws IOException { + EXIFWriter exif = new EXIFWriter(); + long nextPagePos = imageOutput.getStreamPosition(); + if (nextPagePos == 0) { + exif.writeTIFFHeader(imageOutput); + nextPagePos = imageOutput.getStreamPosition(); + imageOutput.writeInt(0); + } + else { + // already has pages, so remember place of EOF to replace with + // IFD offset + nextPagePos -= 4; + } + + for (TIFFPage tiffPage : pages) { + long ifdOffset = tiffPage.write(imageOutput, exif); + + long tmp = imageOutput.getStreamPosition(); + imageOutput.seek(nextPagePos); + imageOutput.writeInt((int) ifdOffset); + imageOutput.seek(tmp); + nextPagePos = tmp; + imageOutput.writeInt(0); + } + } + + public static BufferedImage applyOrientation(BufferedImage input, int orientation) { + boolean flipExtends = false; + int w = input.getWidth(); + int h = input.getHeight(); + double cW = w / 2.0; + double cH = h / 2.0; + + AffineTransform orientationTransform = new AffineTransform(); + switch (orientation) { + case TIFFBaseline.ORIENTATION_TOPLEFT: + // normal + return input; + case TIFFExtension.ORIENTATION_TOPRIGHT: + // flipped vertically + orientationTransform.translate(cW, cH); + orientationTransform.scale(-1, 1); + orientationTransform.translate(-cW, -cH); + break; + case TIFFExtension.ORIENTATION_BOTRIGHT: + // rotated 180 + orientationTransform.quadrantRotate(2, cW, cH); + break; + case TIFFExtension.ORIENTATION_BOTLEFT: + // flipped horizontally + orientationTransform.translate(cW, cH); + orientationTransform.scale(1, -1); + orientationTransform.translate(-cW, -cH); + break; + case TIFFExtension.ORIENTATION_LEFTTOP: + orientationTransform.translate(cW, cH); + orientationTransform.scale(-1, 1); + orientationTransform.quadrantRotate(1); + orientationTransform.translate(-cW, -cH); + flipExtends = true; + break; + case TIFFExtension.ORIENTATION_RIGHTTOP: + // rotated 90 + orientationTransform.quadrantRotate(1, cW, cH); + flipExtends = true; + break; + case TIFFExtension.ORIENTATION_RIGHTBOT: + orientationTransform.translate(cW, cH); + orientationTransform.scale(1, -1); + orientationTransform.quadrantRotate(1); + orientationTransform.translate(-cW, -cH); + flipExtends = true; + break; + case TIFFExtension.ORIENTATION_LEFTBOT: + // rotated 270 + orientationTransform.quadrantRotate(3, cW, cH); + flipExtends = true; + break; + } + + int newW, newH; + if (flipExtends) { + newW = h; + newH = w; + } + else { + newW = w; + newH = h; + } + + AffineTransform transform = AffineTransform.getTranslateInstance((newW - w) / 2.0, (newH - h) / 2.0); + transform.concatenate(orientationTransform); + AffineTransformOp transformOp = new AffineTransformOp(transform, null); + return transformOp.filter(input, null); + } + + public static class TIFFPage { + private Directory IFD; + private ImageInputStream stream; + + private TIFFPage(Directory IFD, ImageInputStream stream) { + this.IFD = IFD; + this.stream = stream; + } + + private long write(ImageOutputStream outputStream, EXIFWriter exifWriter) throws IOException { + List newIFD = writeDirectoryData(IFD, outputStream); + return exifWriter.writeIFD(newIFD, outputStream); + } + + private List writeDirectoryData(Directory IFD, ImageOutputStream outputStream) throws IOException { + ArrayList newIFD = new ArrayList(); + Iterator it = IFD.iterator(); + while (it.hasNext()) { + Entry e = it.next(); + if (e.getValue() instanceof Directory) { + List subIFD = writeDirectoryData((Directory) e.getValue(), outputStream); + new TIFFEntry((Integer) e.getIdentifier(), TIFF.TYPE_IFD, new AbstractDirectory(subIFD) { + }); + } + + newIFD.add(e); + } + + long[] offsets = new long[0]; + long[] byteCounts = new long[0]; + int[] newOffsets = new int[0]; + + Entry stripOffsetsEntry = IFD.getEntryById(TIFF.TAG_STRIP_OFFSETS); + Entry stripByteCountsEntry = IFD.getEntryById(TIFF.TAG_STRIP_BYTE_COUNTS); + if (stripOffsetsEntry != null && stripByteCountsEntry != null) { + offsets = getValueAsLongArray(stripOffsetsEntry); + byteCounts = getValueAsLongArray(stripByteCountsEntry); + + newOffsets = writeData(offsets, byteCounts, outputStream); + + newIFD.remove(stripOffsetsEntry); + newIFD.add(new TIFFEntry(TIFF.TAG_STRIP_OFFSETS, newOffsets)); + } + + Entry oldJpegData = IFD.getEntryById(TIFF.TAG_JPEG_INTERCHANGE_FORMAT); + Entry oldJpegDataLength = IFD.getEntryById(TIFF.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH); + if (oldJpegData != null && oldJpegData.valueCount() > 0 && oldJpegDataLength != null && oldJpegDataLength.valueCount() > 0) { + if (!Arrays.equals(getValueAsLongArray(oldJpegData), offsets) || !Arrays.equals(getValueAsLongArray(oldJpegDataLength), byteCounts)) { + // data already written from TIFF.TAG_STRIP_OFFSETS + offsets = getValueAsLongArray(oldJpegData); + byteCounts = getValueAsLongArray(oldJpegDataLength); + newOffsets = writeData(offsets, byteCounts, outputStream); + } + newIFD.remove(oldJpegData); + newIFD.add(new TIFFEntry(TIFF.TAG_JPEG_INTERCHANGE_FORMAT, newOffsets)); + } + + Entry oldJpegTable; + long[] tableOffsets; + + oldJpegTable = IFD.getEntryById(TIFF.TAG_OLD_JPEG_AC_TABLES); + if (oldJpegTable != null && oldJpegTable.valueCount() > 0) { + tableOffsets = getValueAsLongArray(oldJpegTable); + byteCounts = new long[tableOffsets.length]; + Arrays.fill(byteCounts, 64); + newOffsets = writeData(tableOffsets, byteCounts, outputStream); + newIFD.remove(oldJpegTable); + newIFD.add(new TIFFEntry(TIFF.TAG_OLD_JPEG_AC_TABLES, newOffsets)); + } + + oldJpegTable = IFD.getEntryById(TIFF.TAG_OLD_JPEG_Q_TABLES); + if (oldJpegTable != null && oldJpegTable.valueCount() > 0) { + tableOffsets = getValueAsLongArray(oldJpegTable); + byteCounts = new long[tableOffsets.length]; + Arrays.fill(byteCounts, 64); + newOffsets = writeData(tableOffsets, byteCounts, outputStream); + newIFD.remove(oldJpegTable); + newIFD.add(new TIFFEntry(TIFF.TAG_OLD_JPEG_Q_TABLES, newOffsets)); + } + + oldJpegTable = IFD.getEntryById(TIFF.TAG_OLD_JPEG_DC_TABLES); + if (oldJpegTable != null && oldJpegTable.valueCount() > 0) { + tableOffsets = getValueAsLongArray(oldJpegTable); + byteCounts = new long[tableOffsets.length]; + Arrays.fill(byteCounts, 64); + newOffsets = writeData(tableOffsets, byteCounts, outputStream); + newIFD.remove(oldJpegTable); + newIFD.add(new TIFFEntry(TIFF.TAG_OLD_JPEG_DC_TABLES, newOffsets)); + } + + return newIFD; + } + + private int[] writeData(long[] offsets, long[] byteCounts, ImageOutputStream outputStream) throws IOException { + int[] newOffsets = new int[offsets.length]; + for (int i = 0; i < offsets.length; i++) { + newOffsets[i] = (int) outputStream.getStreamPosition(); + stream.seek(offsets[i]); + + byte[] buffer = new byte[(int) byteCounts[i]]; + stream.readFully(buffer); + outputStream.write(buffer); + } + return newOffsets; + } + + private long[] getValueAsLongArray(Entry entry) throws IIOException { + //TODO: code duplication from TIFFReader, should be extracted to metadata api + long[] value; + + if (entry.valueCount() == 1) { + // For single entries, this will be a boxed type + value = new long[] {((Number) entry.getValue()).longValue()}; + } + else if (entry.getValue() instanceof short[]) { + short[] shorts = (short[]) entry.getValue(); + value = new long[shorts.length]; + + for (int i = 0, length = value.length; i < length; i++) { + value[i] = shorts[i]; + } + } + else if (entry.getValue() instanceof int[]) { + int[] ints = (int[]) entry.getValue(); + value = new long[ints.length]; + + for (int i = 0, length = value.length; i < length; i++) { + value[i] = ints[i]; + } + } + else if (entry.getValue() instanceof long[]) { + value = (long[]) entry.getValue(); + } + else { + throw new IIOException(String.format("Unsupported %s type: %s (%s)", entry.getFieldName(), entry.getTypeName(), entry.getValue().getClass())); + } + + return value; + } + + /** + * Rotates the image by changing TIFF.TAG_ORIENTATION. + *

+ * NOTICE: TIFF.TAG_ORIENTATION is an advice how the image is meant do + * be displayed. Other metadata, such as width and height, relate to the + * image as how it is stored. The ImageIO TIFF plugin does not handle + * orientation. Use + * {@link TIFFUtilities#applyOrientation(BufferedImage, int)} for + * applying TIFF.TAG_ORIENTATION. + *

+ * + * @param degree Rotation amount, supports 90�, 180� and 270�. + */ + public void rotate(int degree) { + Validate.isTrue(degree % 90 == 0 && degree > 0 && degree < 360, + "Only rotations by 90, 180 and 270 degree are supported"); + + ArrayList newIDFData = new ArrayList<>(); + Iterator it = IFD.iterator(); + while (it.hasNext()) { + newIDFData.add(it.next()); + } + + short orientation = TIFFBaseline.ORIENTATION_TOPLEFT; + Entry orientationEntry = IFD.getEntryById(TIFF.TAG_ORIENTATION); + if (orientationEntry != null) { + orientation = ((Number) orientationEntry.getValue()).shortValue(); + newIDFData.remove(orientationEntry); + } + + int steps = degree / 90; + for (int i = 0; i < steps; i++) { + switch (orientation) { + case TIFFBaseline.ORIENTATION_TOPLEFT: + orientation = TIFFExtension.ORIENTATION_RIGHTTOP; + break; + case TIFFExtension.ORIENTATION_TOPRIGHT: + orientation = TIFFExtension.ORIENTATION_RIGHTBOT; + break; + case TIFFExtension.ORIENTATION_BOTRIGHT: + orientation = TIFFExtension.ORIENTATION_LEFTBOT; + break; + case TIFFExtension.ORIENTATION_BOTLEFT: + orientation = TIFFExtension.ORIENTATION_LEFTTOP; + break; + case TIFFExtension.ORIENTATION_LEFTTOP: + orientation = TIFFExtension.ORIENTATION_TOPRIGHT; + break; + case TIFFExtension.ORIENTATION_RIGHTTOP: + orientation = TIFFExtension.ORIENTATION_BOTRIGHT; + break; + case TIFFExtension.ORIENTATION_RIGHTBOT: + orientation = TIFFExtension.ORIENTATION_BOTLEFT; + break; + case TIFFExtension.ORIENTATION_LEFTBOT: + orientation = TIFFBaseline.ORIENTATION_TOPLEFT; + break; + } + } + newIDFData.add(new TIFFEntry(TIFF.TAG_ORIENTATION, (short) orientation)); + IFD = new AbstractDirectory(newIDFData) { + }; + } + } + + /** + * TODO: Temporary clone, to be removed after TMI204 has been closed + */ + public static final class TIFFEntry extends AbstractEntry { + // TODO: Expose a merge of this and the EXIFEntry class... + private final short type; + + private static short guessType(final Object val) { + // TODO: This code is duplicated in EXIFWriter.getType, needs refactor! + Object value = Validate.notNull(val); + + boolean array = value.getClass().isArray(); + if (array) { + value = Array.get(value, 0); + } + + // Note: This "narrowing" is to keep data consistent between read/write. + // TODO: Check for negative values and use signed types? + if (value instanceof Byte) { + return TIFF.TYPE_BYTE; + } + if (value instanceof Short) { + if (!array && (Short) value < Byte.MAX_VALUE) { + return TIFF.TYPE_BYTE; + } + + return TIFF.TYPE_SHORT; + } + if (value instanceof Integer) { + if (!array && (Integer) value < Short.MAX_VALUE) { + return TIFF.TYPE_SHORT; + } + + return TIFF.TYPE_LONG; + } + if (value instanceof Long) { + if (!array && (Long) value < Integer.MAX_VALUE) { + return TIFF.TYPE_LONG; + } + } + + if (value instanceof Rational) { + return TIFF.TYPE_RATIONAL; + } + + if (value instanceof String) { + return TIFF.TYPE_ASCII; + } + + // TODO: More types + + throw new UnsupportedOperationException(String.format("Method guessType not implemented for value of type %s", value.getClass())); + } + + public TIFFEntry(final int identifier, final Object value) { + this(identifier, guessType(value), value); + } + + TIFFEntry(int identifier, short type, Object value) { + super(identifier, value); + this.type = type; + } + + @Override + public String getTypeName() { + return TIFF.TYPE_NAMES[type]; + } + } + + /** + * TODO: Temporary clone, to be removed after TMI204 has been closed + */ + public interface TIFFExtension { + int ORIENTATION_TOPRIGHT = 2; + int ORIENTATION_BOTRIGHT = 3; + int ORIENTATION_BOTLEFT = 4; + int ORIENTATION_LEFTTOP = 5; + int ORIENTATION_RIGHTTOP = 6; + int ORIENTATION_RIGHTBOT = 7; + int ORIENTATION_LEFTBOT = 8; + } + + /** + * TODO: Temporary clone, to be removed after TMI204 has been closed + */ + public interface TIFFBaseline { + int ORIENTATION_TOPLEFT = 1; + } +} diff --git a/contrib/src/test/java/com/twelvemonkeys/contrib/tiff/TIFFUtilitiesTest.java b/contrib/src/test/java/com/twelvemonkeys/contrib/tiff/TIFFUtilitiesTest.java new file mode 100644 index 00000000..4c3b8732 --- /dev/null +++ b/contrib/src/test/java/com/twelvemonkeys/contrib/tiff/TIFFUtilitiesTest.java @@ -0,0 +1,204 @@ +/* + * Copyright (c) 2013, 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.contrib.tiff; + +import com.twelvemonkeys.contrib.tiff.TIFFUtilities.TIFFExtension; +import com.twelvemonkeys.imageio.plugins.tiff.TIFFMedataFormat; +import com.twelvemonkeys.io.FileUtil; +import org.junit.Assert; +import org.junit.Test; +import org.w3c.dom.Node; + +import javax.imageio.ImageIO; +import javax.imageio.ImageReader; +import javax.imageio.stream.ImageInputStream; +import javax.imageio.stream.ImageOutputStream; +import javax.xml.xpath.*; +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferByte; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.nio.file.Files; +import java.util.Arrays; +import java.util.List; + +/** + * TIFFUtilitiesTest + * + * @author Oliver Schmidtmer + * @author last modified by $Author$ + * @version $Id$ + */ +public class TIFFUtilitiesTest { + + @Test + public void testMerge() throws IOException { + // Files from ImageIO TIFF Plugin + InputStream stream1 = getClassLoaderResource("/tiff/ccitt/group3_1d.tif").openStream(); + InputStream stream2 = getClassLoaderResource("/tiff/ccitt/group3_2d.tif").openStream(); + InputStream stream3 = getClassLoaderResource("/tiff/ccitt/group4.tif").openStream(); + + File file1 = File.createTempFile("imageiotest", ".tif"); + File file2 = File.createTempFile("imageiotest", ".tif"); + File file3 = File.createTempFile("imageiotest", ".tif"); + File output = File.createTempFile("imageiotest", ".tif"); + + byte[] data; + + data = FileUtil.read(stream1); + FileUtil.write(file1, data); + stream1.close(); + + data = FileUtil.read(stream2); + FileUtil.write(file2, data); + stream2.close(); + + data = FileUtil.read(stream3); + FileUtil.write(file3, data); + stream3.close(); + + List input = Arrays.asList(file1, file2, file3); + TIFFUtilities.merge(input, output); + + ImageInputStream iis = ImageIO.createImageInputStream(output); + ImageReader reader = ImageIO.getImageReaders(iis).next(); + reader.setInput(iis); + Assert.assertEquals(3, reader.getNumImages(true)); + + iis.close(); + output.delete(); + file1.delete(); + file2.delete(); + file3.delete(); + } + + @Test + public void testSplit() throws IOException { + InputStream inputStream = getClassLoaderResource("/contrib/tiff/multipage.tif").openStream(); + File inputFile = File.createTempFile("imageiotest", "tif"); + byte[] data = FileUtil.read(inputStream); + FileUtil.write(inputFile, data); + inputStream.close(); + + File outputDirectory = Files.createTempDirectory("imageio").toFile(); + + TIFFUtilities.split(inputFile, outputDirectory); + + ImageReader reader = ImageIO.getImageReadersByFormatName("TIF").next(); + + File[] outputFiles = outputDirectory.listFiles(); + Assert.assertEquals(3, outputFiles.length); + for (File outputFile : outputFiles) { + ImageInputStream iis = ImageIO.createImageInputStream(outputFile); + reader.setInput(iis); + Assert.assertEquals(1, reader.getNumImages(true)); + iis.close(); + outputFile.delete(); + } + outputDirectory.delete(); + inputFile.delete(); + } + + @Test + public void testRotate() throws IOException, XPathExpressionException { + ImageReader reader = ImageIO.getImageReadersByFormatName("TIF").next(); + + InputStream inputStream = getClassLoaderResource("/contrib/tiff/multipage.tif").openStream(); + File inputFile = File.createTempFile("imageiotest", ".tif"); + byte[] data = FileUtil.read(inputStream); + FileUtil.write(inputFile, data); + inputStream.close(); + + XPath xPath = XPathFactory.newInstance().newXPath(); + XPathExpression expression = xPath.compile("TIFFIFD/TIFFField[@number='274']/TIFFBytes/TIFFByte/@value"); + + // rotate all pages + ImageInputStream inputTest1 = ImageIO.createImageInputStream(inputFile); + File outputTest1 = File.createTempFile("imageiotest", ".tif"); + ImageOutputStream iosTest1 = ImageIO.createImageOutputStream(outputTest1); + TIFFUtilities.rotatePages(inputTest1, iosTest1, 90); + iosTest1.close(); + + ImageInputStream checkTest1 = ImageIO.createImageInputStream(outputTest1); + reader.setInput(checkTest1); + for (int i = 0; i < 3; i++) { + Node metaData = reader.getImageMetadata(i) + .getAsTree(TIFFMedataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME); + short orientation = ((Number) expression.evaluate(metaData, XPathConstants.NUMBER)).shortValue(); + Assert.assertEquals(orientation, TIFFExtension.ORIENTATION_RIGHTTOP); + } + checkTest1.close(); + + // rotate single page further + ImageInputStream inputTest2 = ImageIO.createImageInputStream(outputTest1); + File outputTest2 = File.createTempFile("imageiotest", ".tif"); + ImageOutputStream iosTest2 = ImageIO.createImageOutputStream(outputTest2); + TIFFUtilities.rotatePage(inputTest2, iosTest2, 90, 1); + iosTest2.close(); + + ImageInputStream checkTest2 = ImageIO.createImageInputStream(outputTest2); + reader.setInput(checkTest2); + for (int i = 0; i < 3; i++) { + Node metaData = reader.getImageMetadata(i) + .getAsTree(TIFFMedataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME); + short orientation = ((Number) expression.evaluate(metaData, XPathConstants.NUMBER)).shortValue(); + Assert.assertEquals(orientation, i == 1 + ? TIFFExtension.ORIENTATION_BOTRIGHT + : TIFFExtension.ORIENTATION_RIGHTTOP); + } + checkTest2.close(); + } + + @Test + public void testApplyOrientation() throws IOException { + InputStream inputStream = getClassLoaderResource("/contrib/tiff/multipage.tif").openStream(); + File inputFile = File.createTempFile("imageiotest", "tif"); + byte[] data = FileUtil.read(inputStream); + FileUtil.write(inputFile, data); + inputStream.close(); + + BufferedImage image = ImageIO.read(inputFile); + + // rotate by 90� + BufferedImage image90 = TIFFUtilities.applyOrientation(image, TIFFExtension.ORIENTATION_RIGHTTOP); + // rotate by 270� + BufferedImage image360 = TIFFUtilities.applyOrientation(image90, TIFFExtension.ORIENTATION_LEFTBOT); + + byte[] original = ((DataBufferByte) image.getData().getDataBuffer()).getData(); + byte[] rotated = ((DataBufferByte) image360.getData().getDataBuffer()).getData(); + + Assert.assertArrayEquals(original, rotated); + } + + protected URL getClassLoaderResource(final String pName) { + return getClass().getResource(pName); + } +} diff --git a/contrib/src/test/resources/contrib/tiff/multipage.tif b/contrib/src/test/resources/contrib/tiff/multipage.tif new file mode 100644 index 00000000..2579cb34 Binary files /dev/null and b/contrib/src/test/resources/contrib/tiff/multipage.tif differ diff --git a/imageio/imageio-batik/pom.xml b/imageio/imageio-batik/pom.xml index 0b773fe3..1f31c074 100644 --- a/imageio/imageio-batik/pom.xml +++ b/imageio/imageio-batik/pom.xml @@ -4,7 +4,7 @@ com.twelvemonkeys.imageio imageio - 3.1-SNAPSHOT + 3.3-SNAPSHOT imageio-batik TwelveMonkeys :: ImageIO :: Batik Plugin @@ -23,27 +23,54 @@ com.twelvemonkeys.imageio imageio-core - tests + test-jar - batik + org.apache.xmlgraphics batik-rasterizer-ext - 1.6-1 + ${batik.version} + provided + + + org.apache.xmlgraphics + batik-extensions + + + + + + org.apache.xmlgraphics + batik-extension + ${batik.version} provided - batik + org.apache.xmlgraphics + xmlgraphics-commons + 2.0.1 + provided + + + + org.apache.xmlgraphics + batik-anim + ${batik.version} + provided + + + + org.apache.xmlgraphics batik-svggen - 1.6-1 + ${batik.version} provided - batik + org.apache.xmlgraphics batik-transcoder - 1.6-1 + ${batik.version} provided " and start over + // - else if next is "!DOCTYPE " skip any whitespace + // - compare next 3 bytes against "svg", return result + // - else + // - compare next 3 bytes against "svg", return result - // If this is not a comment, or the DOCTYPE declaration, the doc - // has no DOCTYPE and it can't be svg - if (pInput.read() != '!') { + byte[] buffer = new byte[4]; + while (true) { + pInput.readFully(buffer); + + if (buffer[0] == '?') { + // This is the XML declaration or a processing instruction + while (!(pInput.read() == '?' && pInput.read() == '>')) { + // Skip until end of XML declaration or processing instruction + } + } + else if (buffer[0] == '!') { + if (buffer[1] == '-' && buffer[2] == '-') { + // This is a comment + while (!(pInput.read() == '-' && pInput.read() == '-' && pInput.read() == '>')) { + // Skip until end of comment + } + } + else if (buffer[1] == 'D' && buffer[2] == 'O' && buffer[3] == 'C' + && pInput.read() == 'T' && pInput.read() == 'Y' + && pInput.read() == 'P' && pInput.read() == 'E') { + // This is the DOCTYPE declaration + while (Character.isWhitespace((char) (b = pInput.read()))) { + // Skip over WS + } + + if (b == 's' && pInput.read() == 'v' && pInput.read() == 'g') { + // It's SVG, identified by DOCTYPE + return true; + } + + // DOCTYPE found, but not SVG + return false; + } + + // Something else, we'll skip + } + else { + // This is a normal tag + if (buffer[0] == 's' && buffer[1] == 'v' && buffer[2] == 'g' + && (Character.isWhitespace((char) buffer[3]) || buffer[3] == ':')) { + // It's SVG, identified by root tag + // TODO: Support svg with prefix + recognize namespace (http://www.w3.org/2000/svg)! + return true; + } + + // If the tag is not "svg", this isn't SVG return false; } - // There might be comments before the doctype, unfortunately... - // If next is "--", this is a comment - if ((b = pInput.read()) == '-' && pInput.read() == '-') { - while (!(pInput.read() == '-' && pInput.read() == '-' && pInput.read() == '>')) { - // Skip until end of comment - } - } - - // If we are lucky, this is DOCTYPE declaration - if (b == 'D' && pInput.read() == 'O' && pInput.read() == 'C' - && pInput.read() == 'T' && pInput.read() == 'Y' && pInput.read() == 'P' - && pInput.read() == 'E') { - docTypeFound = true; - while (Character.isWhitespace((char) (b = pInput.read()))) { - // Skip over WS - } - if (b == 's' && pInput.read() == 'v' && pInput.read() == 'g') { - //System.out.println("It's svg!"); - return true; - } + while (pInput.read() != '<') { + // Skip over, until next begin tag } } + } + catch (EOFException ignore) { + // Possible for small files... return false; } finally { @@ -146,18 +151,17 @@ public class SVGImageReaderSpi extends ImageReaderSpi { } } - - public ImageReader createReaderInstance(Object extension) throws IOException { + public ImageReader createReaderInstance(final Object extension) throws IOException { return new SVGImageReader(this); } - public String getDescription(Locale locale) { - return "Scaleable Vector Graphics (SVG) format image reader"; + public String getDescription(final Locale locale) { + return "Scalable Vector Graphics (SVG) format image reader"; } @SuppressWarnings({"deprecation"}) @Override - public void onRegistration(ServiceRegistry registry, Class category) { + public void onRegistration(final ServiceRegistry registry, final Class category) { if (!SVG_READER_AVAILABLE) { try { // NOTE: This will break, but it gives us some useful debug info @@ -170,5 +174,6 @@ public class SVGImageReaderSpi extends ImageReaderSpi { IIOUtil.deregisterProvider(registry, this, category); } - }} + } +} diff --git a/imageio/imageio-batik/src/main/java/com/twelvemonkeys/imageio/plugins/svg/SVGProviderInfo.java b/imageio/imageio-batik/src/main/java/com/twelvemonkeys/imageio/plugins/svg/SVGProviderInfo.java new file mode 100644 index 00000000..5e83e1af --- /dev/null +++ b/imageio/imageio-batik/src/main/java/com/twelvemonkeys/imageio/plugins/svg/SVGProviderInfo.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2015, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.imageio.plugins.svg; + +import com.twelvemonkeys.imageio.spi.ReaderWriterProviderInfo; +import com.twelvemonkeys.lang.SystemUtil; + +/** + * SVGProviderInfo. + * + * @author Harald Kuhr + * @author last modified by $Author: harald.kuhr$ + * @version $Id: SVGProviderInfo.java,v 1.0 20/03/15 harald.kuhr Exp$ + */ +final class SVGProviderInfo extends ReaderWriterProviderInfo { + final static boolean SVG_READER_AVAILABLE = SystemUtil.isClassAvailable("com.twelvemonkeys.imageio.plugins.svg.SVGImageReader"); + + protected SVGProviderInfo() { + super( + SVGProviderInfo.class, + 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 + "com.twelvemonkeys.imageio.plugins.svg.SVGImageReader", // Reader class name + new String[] {"com.twelvemonkeys.imageio.plugins.svg.SVGImageReaderSpi"}, + null, + null, + false, null, null, null, null, + true, null, null, null, null + ); + } +} diff --git a/imageio/imageio-batik/src/main/java/com/twelvemonkeys/imageio/plugins/wmf/WMFImageReaderSpi.java b/imageio/imageio-batik/src/main/java/com/twelvemonkeys/imageio/plugins/wmf/WMFImageReaderSpi.java index d9aecc2e..a8106b91 100755 --- a/imageio/imageio-batik/src/main/java/com/twelvemonkeys/imageio/plugins/wmf/WMFImageReaderSpi.java +++ b/imageio/imageio-batik/src/main/java/com/twelvemonkeys/imageio/plugins/wmf/WMFImageReaderSpi.java @@ -28,64 +28,38 @@ package com.twelvemonkeys.imageio.plugins.wmf; -import com.twelvemonkeys.imageio.spi.ProviderInfo; -import com.twelvemonkeys.lang.SystemUtil; +import com.twelvemonkeys.imageio.spi.ImageReaderSpiBase; import com.twelvemonkeys.imageio.util.IIOUtil; import javax.imageio.ImageReader; -import javax.imageio.spi.ImageReaderSpi; import javax.imageio.spi.ServiceRegistry; import javax.imageio.stream.ImageInputStream; import java.io.IOException; import java.util.Locale; +import static com.twelvemonkeys.imageio.plugins.wmf.WMFProviderInfo.WMF_READER_AVAILABLE; + /** * WMFImageReaderSpi *

- * + * * @author Harald Kuhr * @version $Id: WMFImageReaderSpi.java,v 1.1 2003/12/02 16:45:00 wmhakur Exp $ */ -public class WMFImageReaderSpi extends ImageReaderSpi { - - // This is correct, as we rely on the SVG reader - private final static boolean WMF_READER_AVAILABLE = SystemUtil.isClassAvailable("com.twelvemonkeys.imageio.plugins.svg.SVGImageReader"); +public final class WMFImageReaderSpi extends ImageReaderSpiBase { /** * Creates a {@code WMFImageReaderSpi}. */ public WMFImageReaderSpi() { - this(IIOUtil.getProviderInfo(WMFImageReaderSpi.class)); + super(new WMFProviderInfo()); } - private WMFImageReaderSpi(final ProviderInfo pProviderInfo) { - super( - 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 - "com.twelvemonkeys.imageio.plugins.wmf.WMFImageReader", // Reader class name..? - new Class[] {ImageInputStream.class}, // Input types - null, // Writer SPI names - true, // Supports standard stream metadata format - null, // Native stream metadata format name - null, // Native stream metadata format class name - null, // Extra stream metadata format names - null, // Extra stream metadata format class names - true, // Supports standard image metadata format - null, // Native image metadata format name - null, // Native image metadata format class name - null, // Extra image metadata format names - null // Extra image metadata format class names - ); - } - - public boolean canDecodeInput(Object source) throws IOException { + public boolean canDecodeInput(final Object source) throws IOException { return source instanceof ImageInputStream && WMF_READER_AVAILABLE && canDecode((ImageInputStream) source); } - public static boolean canDecode(ImageInputStream pInput) throws IOException { + public static boolean canDecode(final ImageInputStream pInput) throws IOException { if (pInput == null) { throw new IllegalArgumentException("input == null"); } @@ -96,7 +70,6 @@ public class WMFImageReaderSpi extends ImageReaderSpi { for (byte header : WMF.HEADER) { int read = (byte) pInput.read(); if (header != read) { - // System.out.println("--> " + i + ": " + read + " (expected " + header + ")"); return false; } } @@ -108,18 +81,17 @@ public class WMFImageReaderSpi extends ImageReaderSpi { } } - - public ImageReader createReaderInstance(Object extension) throws IOException { + public ImageReader createReaderInstance(final Object extension) throws IOException { return new WMFImageReader(this); } - public String getDescription(Locale locale) { + public String getDescription(final Locale locale) { return "Windows Meta File (WMF) image reader"; } @SuppressWarnings({"deprecation"}) @Override - public void onRegistration(ServiceRegistry registry, Class category) { + public void onRegistration(final ServiceRegistry registry, final Class category) { if (!WMF_READER_AVAILABLE) { IIOUtil.deregisterProvider(registry, this, category); } diff --git a/imageio/imageio-batik/src/main/java/com/twelvemonkeys/imageio/plugins/wmf/WMFProviderInfo.java b/imageio/imageio-batik/src/main/java/com/twelvemonkeys/imageio/plugins/wmf/WMFProviderInfo.java new file mode 100644 index 00000000..6db9a085 --- /dev/null +++ b/imageio/imageio-batik/src/main/java/com/twelvemonkeys/imageio/plugins/wmf/WMFProviderInfo.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2015, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.imageio.plugins.wmf; + +import com.twelvemonkeys.imageio.spi.ReaderWriterProviderInfo; +import com.twelvemonkeys.lang.SystemUtil; + +/** + * WMFProviderInfo. + * + * @author Harald Kuhr + * @author last modified by $Author: harald.kuhr$ + * @version $Id: WMFProviderInfo.java,v 1.0 20/03/15 harald.kuhr Exp$ + */ +final class WMFProviderInfo extends ReaderWriterProviderInfo { + // This is correct, as we rely on the SVG reader + final static boolean WMF_READER_AVAILABLE = SystemUtil.isClassAvailable("com.twelvemonkeys.imageio.plugins.svg.SVGImageReader"); + + protected WMFProviderInfo() { + super( + WMFProviderInfo.class, + 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 + "com.twelvemonkeys.imageio.plugins.wmf.WMFImageReader", // Reader class name..? + new String[] {"com.twelvemonkeys.imageio.plugins.wmf.WMFImageReaderSpi"}, + null, + null, + false, null, null, null, null, + true, null, null, null, null + ); + } +} diff --git a/imageio/imageio-batik/src/test/java/com/twelvemonkeys/imageio/plugins/svg/SVGImageReaderTestCase.java b/imageio/imageio-batik/src/test/java/com/twelvemonkeys/imageio/plugins/svg/SVGImageReaderTest.java similarity index 67% rename from imageio/imageio-batik/src/test/java/com/twelvemonkeys/imageio/plugins/svg/SVGImageReaderTestCase.java rename to imageio/imageio-batik/src/test/java/com/twelvemonkeys/imageio/plugins/svg/SVGImageReaderTest.java index 8324c311..9f0e8b52 100755 --- a/imageio/imageio-batik/src/test/java/com/twelvemonkeys/imageio/plugins/svg/SVGImageReaderTestCase.java +++ b/imageio/imageio-batik/src/test/java/com/twelvemonkeys/imageio/plugins/svg/SVGImageReaderTest.java @@ -28,30 +28,39 @@ package com.twelvemonkeys.imageio.plugins.svg; -import com.twelvemonkeys.imageio.util.ImageReaderAbstractTestCase; +import com.twelvemonkeys.imageio.util.ImageReaderAbstractTest; import org.junit.Ignore; import org.junit.Test; +import javax.imageio.ImageReadParam; +import javax.imageio.ImageReader; import javax.imageio.spi.ImageReaderSpi; import java.awt.*; +import java.awt.image.BufferedImage; import java.awt.image.ImagingOpException; import java.io.IOException; import java.util.Arrays; +import java.util.Collections; import java.util.List; +import static org.junit.Assert.assertEquals; + /** - * SVGImageReaderTestCase + * SVGImageReaderTest * * @author Harald Kuhr * @author last modified by $Author: haraldk$ - * @version $Id: SVGImageReaderTestCase.java,v 1.0 Apr 1, 2008 10:39:17 PM haraldk Exp$ + * @version $Id: SVGImageReaderTest.java,v 1.0 Apr 1, 2008 10:39:17 PM haraldk Exp$ */ -public class SVGImageReaderTestCase extends ImageReaderAbstractTestCase { +public class SVGImageReaderTest extends ImageReaderAbstractTest { private SVGImageReaderSpi provider = new SVGImageReaderSpi(); protected List getTestData() { return Arrays.asList( - new TestData(getClassLoaderResource("/svg/batikLogo.svg"), new Dimension(450, 500)) + new TestData(getClassLoaderResource("/svg/batikLogo.svg"), new Dimension(450, 500)), + new TestData(getClassLoaderResource("/svg/red-square.svg"), new Dimension(100, 100)), + new TestData(getClassLoaderResource("/svg/blue-square.svg"), new Dimension(100, 100)), + new TestData(getClassLoaderResource("/svg/Android_robot.svg"), new Dimension(400, 400)) ); } @@ -69,15 +78,15 @@ public class SVGImageReaderTestCase extends ImageReaderAbstractTestCase getFormatNames() { - return Arrays.asList("svg"); + return Collections.singletonList("svg"); } protected List getSuffixes() { - return Arrays.asList("svg"); + return Collections.singletonList("svg"); } protected List getMIMETypes() { - return Arrays.asList("image/svg+xml"); + return Collections.singletonList("image/svg+xml"); } @Test @@ -110,4 +119,22 @@ public class SVGImageReaderTestCase extends ImageReaderAbstractTestCaseHarald Kuhr + * @author last modified by $Author: harald.kuhr$ + * @version $Id: SVGProviderInfoTest.java,v 1.0 02/06/16 harald.kuhr Exp$ + */ +public class SVGProviderInfoTest extends ReaderWriterProviderInfoTest { + + @Override + protected ReaderWriterProviderInfo createProviderInfo() { + return new SVGProviderInfo(); + } +} \ No newline at end of file diff --git a/imageio/imageio-batik/src/test/java/com/twelvemonkeys/imageio/plugins/wmf/WMFImageReaderTestCase.java b/imageio/imageio-batik/src/test/java/com/twelvemonkeys/imageio/plugins/wmf/WMFImageReaderTest.java similarity index 87% rename from imageio/imageio-batik/src/test/java/com/twelvemonkeys/imageio/plugins/wmf/WMFImageReaderTestCase.java rename to imageio/imageio-batik/src/test/java/com/twelvemonkeys/imageio/plugins/wmf/WMFImageReaderTest.java index 23c55193..9220d115 100755 --- a/imageio/imageio-batik/src/test/java/com/twelvemonkeys/imageio/plugins/wmf/WMFImageReaderTestCase.java +++ b/imageio/imageio-batik/src/test/java/com/twelvemonkeys/imageio/plugins/wmf/WMFImageReaderTest.java @@ -28,7 +28,7 @@ package com.twelvemonkeys.imageio.plugins.wmf; -import com.twelvemonkeys.imageio.util.ImageReaderAbstractTestCase; +import com.twelvemonkeys.imageio.util.ImageReaderAbstractTest; import org.junit.Ignore; import org.junit.Test; @@ -36,22 +36,22 @@ import javax.imageio.spi.ImageReaderSpi; import java.awt.*; import java.io.IOException; import java.util.Arrays; +import java.util.Collections; import java.util.List; /** - * SVGImageReaderTestCase + * WMFImageReaderTest * * @author Harald Kuhr * @author last modified by $Author: haraldk$ - * @version $Id: SVGImageReaderTestCase.java,v 1.0 Apr 1, 2008 10:39:17 PM haraldk Exp$ + * @version $Id: WMFImageReaderTest.java,v 1.0 Apr 1, 2008 10:39:17 PM haraldk Exp$ */ -public class WMFImageReaderTestCase extends ImageReaderAbstractTestCase { +public class WMFImageReaderTest extends ImageReaderAbstractTest { private WMFImageReaderSpi provider = new WMFImageReaderSpi(); protected List getTestData() { - return Arrays.asList( - // TODO: Dimensions does not look right... - new TestData(getClassLoaderResource("/wmf/test.wmf"), new Dimension(841, 673)) + return Collections.singletonList( + new TestData(getClassLoaderResource("/wmf/test.wmf"), new Dimension(133, 106)) ); } @@ -69,7 +69,7 @@ public class WMFImageReaderTestCase extends ImageReaderAbstractTestCase getFormatNames() { - return Arrays.asList("wmf"); + return Collections.singletonList("wmf"); } protected List getSuffixes() { diff --git a/imageio/imageio-batik/src/test/java/com/twelvemonkeys/imageio/plugins/wmf/WMFProviderInfoTest.java b/imageio/imageio-batik/src/test/java/com/twelvemonkeys/imageio/plugins/wmf/WMFProviderInfoTest.java new file mode 100644 index 00000000..6b96d2cf --- /dev/null +++ b/imageio/imageio-batik/src/test/java/com/twelvemonkeys/imageio/plugins/wmf/WMFProviderInfoTest.java @@ -0,0 +1,19 @@ +package com.twelvemonkeys.imageio.plugins.wmf; + +import com.twelvemonkeys.imageio.spi.ReaderWriterProviderInfo; +import com.twelvemonkeys.imageio.spi.ReaderWriterProviderInfoTest; + +/** + * WMFProviderInfoTest. + * + * @author Harald Kuhr + * @author last modified by $Author: harald.kuhr$ + * @version $Id: WMFProviderInfoTest.java,v 1.0 02/06/16 harald.kuhr Exp$ + */ +public class WMFProviderInfoTest extends ReaderWriterProviderInfoTest { + + @Override + protected ReaderWriterProviderInfo createProviderInfo() { + return new WMFProviderInfo(); + } +} \ No newline at end of file diff --git a/imageio/imageio-batik/src/test/resources/svg/Android_robot.svg b/imageio/imageio-batik/src/test/resources/svg/Android_robot.svg new file mode 100644 index 00000000..e32e907b --- /dev/null +++ b/imageio/imageio-batik/src/test/resources/svg/Android_robot.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/imageio/imageio-batik/src/test/resources/svg/blue-square.svg b/imageio/imageio-batik/src/test/resources/svg/blue-square.svg new file mode 100644 index 00000000..fdf43634 --- /dev/null +++ b/imageio/imageio-batik/src/test/resources/svg/blue-square.svg @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/imageio/imageio-batik/src/test/resources/svg/red-square.svg b/imageio/imageio-batik/src/test/resources/svg/red-square.svg new file mode 100644 index 00000000..2ae064db --- /dev/null +++ b/imageio/imageio-batik/src/test/resources/svg/red-square.svg @@ -0,0 +1,7 @@ + + + + + + diff --git a/imageio/imageio-bmp/pom.xml b/imageio/imageio-bmp/pom.xml index 50234fdf..8b6aa174 100644 --- a/imageio/imageio-bmp/pom.xml +++ b/imageio/imageio-bmp/pom.xml @@ -4,7 +4,7 @@ com.twelvemonkeys.imageio imageio - 3.1-SNAPSHOT + 3.3-SNAPSHOT imageio-bmp TwelveMonkeys :: ImageIO :: BMP plugin @@ -18,7 +18,7 @@ com.twelvemonkeys.imageio imageio-core - tests + test-jar diff --git a/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/BMPMetadata.java b/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/BMPMetadata.java index 9abbff29..1988d376 100755 --- a/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/BMPMetadata.java +++ b/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/BMPMetadata.java @@ -28,17 +28,16 @@ package com.twelvemonkeys.imageio.plugins.bmp; +import com.twelvemonkeys.imageio.AbstractMetadata; import com.twelvemonkeys.lang.Validate; import org.w3c.dom.Node; -import javax.imageio.metadata.IIOMetadata; -import javax.imageio.metadata.IIOMetadataFormatImpl; import javax.imageio.metadata.IIOMetadataNode; /** * BMPMetadata. */ -final class BMPMetadata extends IIOMetadata { +final class BMPMetadata extends AbstractMetadata { /** We return metadata in the exact same form as the JRE built-in, to be compatible with the BMPImageWriter. */ public static final String nativeMetadataFormatName = "javax_imageio_bmp_1.0"; @@ -46,46 +45,13 @@ final class BMPMetadata extends IIOMetadata { private final int[] colorMap; BMPMetadata(final DIBHeader header, final int[] colorMap) { + super(true, nativeMetadataFormatName, "com.sun.imageio.plugins.bmp.BMPMetadataFormat", null, null); this.header = Validate.notNull(header, "header"); this.colorMap = colorMap == null || colorMap.length == 0 ? null : colorMap; - - standardFormatSupported = true; - } - - @Override public boolean isReadOnly() { - return true; - } - - @Override public Node getAsTree(final String formatName) { - if (IIOMetadataFormatImpl.standardMetadataFormatName.equals(formatName)) { - return getStandardTree(); - } - else if (nativeMetadataFormatName.equals(formatName)) { - return getNativeTree(); - } - else { - throw new IllegalArgumentException("Unsupported metadata format: " + formatName); - } - } - - @Override public void mergeTree(final String formatName, final Node root) { - if (isReadOnly()) { - throw new IllegalStateException("Metadata is read-only"); - } - } - - @Override public void reset() { - if (isReadOnly()) { - throw new IllegalStateException("Metadata is read-only"); - } } @Override - public String getNativeMetadataFormatName() { - return nativeMetadataFormatName; - } - - private Node getNativeTree() { + protected Node getNativeTree() { IIOMetadataNode root = new IIOMetadataNode(nativeMetadataFormatName); addChildNode(root, "BMPVersion", header.getBMPVersion()); @@ -170,7 +136,8 @@ final class BMPMetadata extends IIOMetadata { return child; } - @Override protected IIOMetadataNode getStandardChromaNode() { + @Override + protected IIOMetadataNode getStandardChromaNode() { // NOTE: BMP files may contain a color map, even if true color... // Not sure if this is a good idea to expose to the meta data, // as it might be unexpected... Then again... @@ -197,7 +164,8 @@ final class BMPMetadata extends IIOMetadata { return null; } - @Override protected IIOMetadataNode getStandardCompressionNode() { + @Override + protected IIOMetadataNode getStandardCompressionNode() { IIOMetadataNode compression = new IIOMetadataNode("Compression"); IIOMetadataNode compressionTypeName = addChildNode(compression, "CompressionTypeName", null); compressionTypeName.setAttribute("value", "NONE"); @@ -229,7 +197,8 @@ final class BMPMetadata extends IIOMetadata { // } } - @Override protected IIOMetadataNode getStandardDataNode() { + @Override + protected IIOMetadataNode getStandardDataNode() { IIOMetadataNode node = new IIOMetadataNode("Data"); // IIOMetadataNode planarConfiguration = new IIOMetadataNode("PlanarConfiguration"); @@ -294,7 +263,8 @@ final class BMPMetadata extends IIOMetadata { return buffer.toString(); } - @Override protected IIOMetadataNode getStandardDimensionNode() { + @Override + protected IIOMetadataNode getStandardDimensionNode() { if (header.xPixelsPerMeter > 0 || header.yPixelsPerMeter > 0) { IIOMetadataNode dimension = new IIOMetadataNode("Dimension"); @@ -302,16 +272,16 @@ final class BMPMetadata extends IIOMetadata { addChildNode(dimension, "HorizontalPhysicalPixelSpacing", null); addChildNode(dimension, "VerticalPhysicalPixelSpacing", null); - // IIOMetadataNode imageOrientation = new IIOMetadataNode("ImageOrientation"); - // - // if (header.topDown) { - // imageOrientation.setAttribute("value", "FlipH"); - // } - // else { - // imageOrientation.setAttribute("value", "Normal"); - // } - // - // dimension.appendChild(imageOrientation); +// IIOMetadataNode imageOrientation = new IIOMetadataNode("ImageOrientation"); +// +// if (header.topDown) { +// imageOrientation.setAttribute("value", "FlipH"); +// } +// else { +// imageOrientation.setAttribute("value", "Normal"); +// } +// +// dimension.appendChild(imageOrientation); return dimension; } @@ -325,7 +295,8 @@ final class BMPMetadata extends IIOMetadata { // No tiling - @Override protected IIOMetadataNode getStandardTransparencyNode() { + @Override + protected IIOMetadataNode getStandardTransparencyNode() { return null; // IIOMetadataNode transparency = new IIOMetadataNode("Transparency"); diff --git a/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/BitmapDescriptor.java b/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/BitmapDescriptor.java index 3587d662..a5a00f6a 100755 --- a/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/BitmapDescriptor.java +++ b/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/BitmapDescriptor.java @@ -43,6 +43,7 @@ abstract class BitmapDescriptor { protected final DIBHeader header; protected BufferedImage image; + protected BitmapMask mask; public BitmapDescriptor(final DirectoryEntry pEntry, final DIBHeader pHeader) { Validate.notNull(pEntry, "entry"); @@ -69,4 +70,17 @@ abstract class BitmapDescriptor { protected final int getBitCount() { return entry.getBitCount() != 0 ? entry.getBitCount() : header.getBitCount(); } + + @Override + public String toString() { + return getClass().getSimpleName() + "[" + entry + ", " + header + "]"; + } + + public final void setMask(final BitmapMask mask) { + this.mask = mask; + } + + public final boolean hasMask() { + return header.getHeight() == getHeight() * 2; + } } diff --git a/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/BitmapIndexed.java b/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/BitmapIndexed.java index c5d33293..cb2c413a 100755 --- a/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/BitmapIndexed.java +++ b/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/BitmapIndexed.java @@ -28,8 +28,6 @@ package com.twelvemonkeys.imageio.plugins.bmp; -import com.twelvemonkeys.image.InverseColorMapIndexColorModel; - import java.awt.image.BufferedImage; import java.awt.image.DataBuffer; import java.awt.image.IndexColorModel; @@ -46,8 +44,6 @@ class BitmapIndexed extends BitmapDescriptor { protected final int[] bits; protected final int[] colors; - private BitmapMask mask; - public BitmapIndexed(final DirectoryEntry pEntry, final DIBHeader pHeader) { super(pEntry, pHeader); bits = new int[getWidth() * getHeight()]; @@ -65,7 +61,7 @@ class BitmapIndexed extends BitmapDescriptor { // This is slightly obscure, and should probably be moved.. Hashtable properties = null; if (entry instanceof DirectoryEntry.CUREntry) { - properties = new Hashtable(1); + properties = new Hashtable<>(1); properties.put("cursor_hotspot", ((DirectoryEntry.CUREntry) this.entry).getHotspot()); } @@ -89,8 +85,6 @@ class BitmapIndexed extends BitmapDescriptor { raster.setSamples(0, 0, getWidth(), getHeight(), 0, bits); - //System.out.println("Image: " + image); - return image; } @@ -100,40 +94,40 @@ class BitmapIndexed extends BitmapDescriptor { IndexColorModel createColorModel() { // NOTE: This is a hack to make room for transparent pixel for mask int bits = getBitCount(); - + int colors = this.colors.length; - int trans = -1; + int transparent = -1; // Try to avoid USHORT transfertype, as it results in BufferedImage TYPE_CUSTOM // NOTE: This code assumes icons are small, and is NOT optimized for performance... if (colors > (1 << getBitCount())) { - int index = findTransIndexMaybeRemap(this.colors, this.bits); + int index = findTransparentIndexMaybeRemap(this.colors, this.bits); if (index == -1) { // No duplicate found, increase bitcount bits++; - trans = this.colors.length - 1; + transparent = this.colors.length - 1; } else { - // Found a duplicate, use it as trans - trans = index; + // Found a duplicate, use it as transparent + transparent = index; colors--; } } // NOTE: Setting hasAlpha to true, makes things work on 1.2 - return new InverseColorMapIndexColorModel( - bits, colors, this.colors, 0, true, trans, + return new IndexColorModel( + bits, colors, this.colors, 0, true, transparent, bits <= 8 ? DataBuffer.TYPE_BYTE : DataBuffer.TYPE_USHORT ); } - private static int findTransIndexMaybeRemap(final int[] pColors, final int[] pBits) { + private static int findTransparentIndexMaybeRemap(final int[] colors, final int[] bits) { // Look for unused colors, to use as transparent - final boolean[] used = new boolean[pColors.length - 1]; - for (int pBit : pBits) { - if (!used[pBit]) { - used[pBit] = true; + boolean[] used = new boolean[colors.length - 1]; + for (int bit : bits) { + if (!used[bit]) { + used[bit] = true; } } @@ -144,38 +138,35 @@ class BitmapIndexed extends BitmapDescriptor { } // Try to find duplicates in colormap, and remap - int trans = -1; + int transparent = -1; int duplicate = -1; - for (int i = 0; trans == -1 && i < pColors.length - 1; i++) { - for (int j = i + 1; j < pColors.length - 1; j++) { - if (pColors[i] == pColors[j]) { - trans = j; + for (int i = 0; transparent == -1 && i < colors.length - 1; i++) { + for (int j = i + 1; j < colors.length - 1; j++) { + if (colors[i] == colors[j]) { + transparent = j; duplicate = i; break; } } } - if (trans != -1) { + if (transparent != -1) { // Remap duplicate - for (int i = 0; i < pBits.length; i++) { - if (pBits[i] == trans) { - pBits[i] = duplicate; + for (int i = 0; i < bits.length; i++) { + if (bits[i] == transparent) { + bits[i] = duplicate; } } } - return trans; + return transparent; } public BufferedImage getImage() { if (image == null) { image = createImageIndexed(); } + return image; } - - public void setMask(final BitmapMask pMask) { - mask = pMask; - } } diff --git a/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/BitmapMask.java b/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/BitmapMask.java index c4831680..5d619cf6 100755 --- a/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/BitmapMask.java +++ b/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/BitmapMask.java @@ -38,19 +38,19 @@ import java.awt.image.BufferedImage; * @version $Id: BitmapMask.java,v 1.0 25.feb.2006 00:29:44 haku Exp$ */ class BitmapMask extends BitmapDescriptor { - protected final BitmapIndexed mask; + protected final BitmapIndexed bitMask; public BitmapMask(final DirectoryEntry pParent, final DIBHeader pHeader) { super(pParent, pHeader); - mask = new BitmapIndexed(pParent, pHeader); + bitMask = new BitmapIndexed(pParent, pHeader); } boolean isTransparent(final int pX, final int pY) { // NOTE: 1: Fully transparent, 0: Opaque... - return mask.bits[pX + pY * getWidth()] != 0; + return bitMask.bits[pX + pY * getWidth()] != 0; } public BufferedImage getImage() { - return mask.getImage(); + return bitMask.getImage(); } } diff --git a/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/BitmapRGB.java b/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/BitmapRGB.java index 11dc17d4..05f43600 100755 --- a/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/BitmapRGB.java +++ b/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/BitmapRGB.java @@ -28,7 +28,9 @@ package com.twelvemonkeys.imageio.plugins.bmp; +import java.awt.*; import java.awt.image.BufferedImage; +import java.awt.image.WritableRaster; /** * Describes an RGB/true color bitmap structure (16, 24 and 32 bits per pixel). @@ -43,6 +45,38 @@ class BitmapRGB extends BitmapDescriptor { } public BufferedImage getImage() { + // Test is mask != null rather than hasMask(), as 32 bit (w/alpha) + // might still have bitmask, but we don't read or use it. + if (mask != null) { + image = createMaskedImage(); + mask = null; + } + return image; } + + private BufferedImage createMaskedImage() { + BufferedImage masked = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_4BYTE_ABGR); + + Graphics2D graphics = masked.createGraphics(); + try { + graphics.drawImage(image, 0, 0, null); + } + finally { + graphics.dispose(); + } + + WritableRaster alphaRaster = masked.getAlphaRaster(); + + byte[] trans = {0x0}; + for (int y = 0; y < getHeight(); y++) { + for (int x = 0; x < getWidth(); x++) { + if (mask.isTransparent(x, y)) { + alphaRaster.setDataElements(x, y, trans); + } + } + } + + return masked; + } } diff --git a/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/CURProviderInfo.java b/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/CURProviderInfo.java index 45831e58..6444d5a6 100644 --- a/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/CURProviderInfo.java +++ b/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/CURProviderInfo.java @@ -1,3 +1,31 @@ +/* + * Copyright (c) 2015, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + package com.twelvemonkeys.imageio.plugins.bmp; import com.twelvemonkeys.imageio.spi.ReaderWriterProviderInfo; diff --git a/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/DIBImageReader.java b/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/DIBImageReader.java index bd8087ff..12d4e5fe 100644 --- a/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/DIBImageReader.java +++ b/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/DIBImageReader.java @@ -66,14 +66,15 @@ import java.util.List; // TODO: Decide whether DirectoryEntry or DIBHeader should be primary source for color count/bit count // TODO: Support loading icons from DLLs, see // MSDN -// Known issue: 256x256 PNG encoded icons does not have IndexColorModel even if stated in DirectoryEntry (seem impossible as the PNGs are all true color) +// Known issue: 256x256 PNG encoded icons does not have IndexColorModel even if stated in DirectoryEntry +// (seem impossible as the PNGs are all true color) abstract class DIBImageReader extends ImageReaderBase { // TODO: Consider moving the reading to inner classes (subclasses of BitmapDescriptor) private Directory directory; // TODO: Review these, make sure we don't have a memory leak - private Map headers = new WeakHashMap(); - private Map descriptors = new WeakWeakMap(); + private Map headers = new WeakHashMap<>(); + private Map descriptors = new WeakWeakMap<>(); private ImageReader pngImageReader; @@ -101,7 +102,7 @@ abstract class DIBImageReader extends ImageReaderBase { return getImageTypesPNG(entry); } - List types = new ArrayList(); + List types = new ArrayList<>(); DIBHeader header = getHeader(entry); // Use data from header to create specifier @@ -121,10 +122,13 @@ abstract class DIBImageReader extends ImageReaderBase { specifier = ImageTypeSpecifiers.createFromIndexColorModel(indexed.createColorModel()); break; case 16: + // TODO: May have mask?! specifier = ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_USHORT_555_RGB); break; case 24: - specifier = ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_3BYTE_BGR); + specifier = new BitmapRGB(entry, header).hasMask() + ? ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_4BYTE_ABGR) + : ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_3BYTE_BGR); break; case 32: specifier = ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_INT_ARGB); @@ -184,6 +188,7 @@ abstract class DIBImageReader extends ImageReaderBase { } else { Graphics2D g = destination.createGraphics(); + try { g.setComposite(AlphaComposite.Src); g.drawImage(image, 0, 0, null); @@ -335,7 +340,7 @@ abstract class DIBImageReader extends ImageReaderBase { } BitmapMask mask = new BitmapMask(pBitmap.entry, pBitmap.header); - readBitmapIndexed1(mask.mask, true); + readBitmapIndexed1(mask.bitMask, true); pBitmap.setMask(mask); } @@ -370,7 +375,7 @@ abstract class DIBImageReader extends ImageReaderBase { } } - // NOTE: If we are reading the mask, we don't abort or progress + // NOTE: If we are reading the mask, we don't abort or report progress if (!pAsMask) { if (abortRequested()) { processReadAborted(); @@ -455,7 +460,7 @@ abstract class DIBImageReader extends ImageReaderBase { short[] pixels = new short[pBitmap.getWidth() * pBitmap.getHeight()]; // TODO: Support TYPE_USHORT_565 and the RGB 444/ARGB 4444 layouts - // Will create TYPE_USHORT_555; + // Will create TYPE_USHORT_555 DirectColorModel cm = new DirectColorModel(16, 0x7C00, 0x03E0, 0x001F); DataBuffer buffer = new DataBufferShort(pixels, pixels.length); WritableRaster raster = Raster.createPackedRaster( @@ -480,6 +485,8 @@ abstract class DIBImageReader extends ImageReaderBase { processImageProgress(100 * y / (float) pBitmap.getHeight()); } + + // TODO: Might be mask!? } private void readBitmap24(final BitmapDescriptor pBitmap) throws IOException { @@ -494,16 +501,19 @@ abstract class DIBImageReader extends ImageReaderBase { cs, nBits, false, false, Transparency.OPAQUE, DataBuffer.TYPE_BYTE ); + int scanlineStride = pBitmap.getWidth() * 3; + // BMP rows are padded to 4 byte boundary + int rowSizeBytes = ((8 * scanlineStride + 31) / 32) * 4; + WritableRaster raster = Raster.createInterleavedRaster( - buffer, pBitmap.getWidth(), pBitmap.getHeight(), pBitmap.getWidth(), 3, bOffs, null + buffer, pBitmap.getWidth(), pBitmap.getHeight(), scanlineStride, 3, bOffs, null ); pBitmap.image = new BufferedImage(cm, raster, cm.isAlphaPremultiplied(), null); - - for (int y = 0; y < pBitmap.getHeight(); y++) { - int offset = (pBitmap.getHeight() - y - 1) * pBitmap.getWidth(); - imageInput.readFully(pixels, offset, pBitmap.getWidth() * 3); - // TODO: Possibly read padding byte here! + for (int y = 0; y < pBitmap.getHeight(); y++) { + int offset = (pBitmap.getHeight() - y - 1) * scanlineStride; + imageInput.readFully(pixels, offset, scanlineStride); + imageInput.skipBytes(rowSizeBytes - scanlineStride); if (abortRequested()) { processReadAborted(); @@ -512,6 +522,14 @@ abstract class DIBImageReader extends ImageReaderBase { processImageProgress(100 * y / (float) pBitmap.getHeight()); } + + // 24 bit icons usually have a bit mask + if (pBitmap.hasMask()) { + BitmapMask mask = new BitmapMask(pBitmap.entry, pBitmap.header); + readBitmapIndexed1(mask.bitMask, true); + + pBitmap.setMask(mask); + } } private void readBitmap32(final BitmapDescriptor pBitmap) throws IOException { @@ -535,6 +553,9 @@ abstract class DIBImageReader extends ImageReaderBase { } processImageProgress(100 * y / (float) pBitmap.getHeight()); } + + // There might be a mask here as well, but we'll ignore it, + // and use the 8 bit alpha channel in the ARGB pixel data } private Directory getDirectory() throws IOException { diff --git a/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/ICOProviderInfo.java b/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/ICOProviderInfo.java index c6d8fd36..9b29e1fc 100644 --- a/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/ICOProviderInfo.java +++ b/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/ICOProviderInfo.java @@ -1,3 +1,31 @@ +/* + * Copyright (c) 2015, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + package com.twelvemonkeys.imageio.plugins.bmp; import com.twelvemonkeys.imageio.spi.ReaderWriterProviderInfo; diff --git a/imageio/imageio-bmp/src/test/java/com/twelvemonkeys/imageio/plugins/bmp/BMPImageReaderTest.java b/imageio/imageio-bmp/src/test/java/com/twelvemonkeys/imageio/plugins/bmp/BMPImageReaderTest.java index 80ae803b..c426d7ee 100755 --- a/imageio/imageio-bmp/src/test/java/com/twelvemonkeys/imageio/plugins/bmp/BMPImageReaderTest.java +++ b/imageio/imageio-bmp/src/test/java/com/twelvemonkeys/imageio/plugins/bmp/BMPImageReaderTest.java @@ -1,6 +1,6 @@ package com.twelvemonkeys.imageio.plugins.bmp; -import com.twelvemonkeys.imageio.util.ImageReaderAbstractTestCase; +import com.twelvemonkeys.imageio.util.ImageReaderAbstractTest; import org.junit.Test; import org.mockito.InOrder; import org.w3c.dom.Node; @@ -19,10 +19,12 @@ import java.io.IOException; import java.lang.reflect.Constructor; import java.net.URISyntaxException; import java.util.Arrays; +import java.util.Collections; import java.util.Iterator; import java.util.List; import static org.junit.Assert.*; +import static org.junit.Assume.assumeNoException; import static org.mockito.Matchers.anyInt; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.*; @@ -34,7 +36,7 @@ import static org.mockito.Mockito.*; * @author last modified by $Author: haraldk$ * @version $Id: BMPImageReaderTest.java,v 1.0 Apr 1, 2008 10:39:17 PM haraldk Exp$ */ -public class BMPImageReaderTest extends ImageReaderAbstractTestCase { +public class BMPImageReaderTest extends ImageReaderAbstractTest { protected List getTestData() { return Arrays.asList( // BMP Suite "Good" @@ -90,7 +92,7 @@ public class BMPImageReaderTest extends ImageReaderAbstractTestCase getFormatNames() { - return Arrays.asList("bmp"); + return Collections.singletonList("bmp"); } protected List getSuffixes() { @@ -132,7 +134,7 @@ public class BMPImageReaderTest extends ImageReaderAbstractTestCase getMIMETypes() { - return Arrays.asList("image/bmp"); + return Collections.singletonList("image/bmp"); } @Override @@ -234,7 +236,6 @@ public class BMPImageReaderTest extends ImageReaderAbstractTestCaseHarald Kuhr + * @author last modified by $Author: harald.kuhr$ + * @version $Id: BMPProviderInfoTest.java,v 1.0 02/06/16 harald.kuhr Exp$ + */ +public class BMPProviderInfoTest extends ReaderWriterProviderInfoTest { + + @Override + protected ReaderWriterProviderInfo createProviderInfo() { + return new BMPProviderInfo(); + } +} \ No newline at end of file diff --git a/imageio/imageio-bmp/src/test/java/com/twelvemonkeys/imageio/plugins/bmp/CURImageReaderTest.java b/imageio/imageio-bmp/src/test/java/com/twelvemonkeys/imageio/plugins/bmp/CURImageReaderTest.java index 45a20c02..fe885d86 100755 --- a/imageio/imageio-bmp/src/test/java/com/twelvemonkeys/imageio/plugins/bmp/CURImageReaderTest.java +++ b/imageio/imageio-bmp/src/test/java/com/twelvemonkeys/imageio/plugins/bmp/CURImageReaderTest.java @@ -1,6 +1,6 @@ package com.twelvemonkeys.imageio.plugins.bmp; -import com.twelvemonkeys.imageio.util.ImageReaderAbstractTestCase; +import com.twelvemonkeys.imageio.util.ImageReaderAbstractTest; import org.junit.Ignore; import org.junit.Test; @@ -10,6 +10,7 @@ import java.awt.*; import java.awt.image.BufferedImage; import java.io.IOException; import java.util.Arrays; +import java.util.Collections; import java.util.List; import static org.junit.Assert.*; @@ -21,7 +22,7 @@ import static org.junit.Assert.*; * @author last modified by $Author: haraldk$ * @version $Id: CURImageReaderTest.java,v 1.0 Apr 1, 2008 10:39:17 PM haraldk Exp$ */ -public class CURImageReaderTest extends ImageReaderAbstractTestCase { +public class CURImageReaderTest extends ImageReaderAbstractTest { protected List getTestData() { return Arrays.asList( new TestData(getClassLoaderResource("/cur/hand.cur"), new Dimension(32, 32)), @@ -43,11 +44,11 @@ public class CURImageReaderTest extends ImageReaderAbstractTestCase getFormatNames() { - return Arrays.asList("cur"); + return Collections.singletonList("cur"); } protected List getSuffixes() { - return Arrays.asList("cur"); + return Collections.singletonList("cur"); } protected List getMIMETypes() { diff --git a/imageio/imageio-bmp/src/test/java/com/twelvemonkeys/imageio/plugins/bmp/CURProviderInfoTest.java b/imageio/imageio-bmp/src/test/java/com/twelvemonkeys/imageio/plugins/bmp/CURProviderInfoTest.java new file mode 100644 index 00000000..61a814b2 --- /dev/null +++ b/imageio/imageio-bmp/src/test/java/com/twelvemonkeys/imageio/plugins/bmp/CURProviderInfoTest.java @@ -0,0 +1,19 @@ +package com.twelvemonkeys.imageio.plugins.bmp; + +import com.twelvemonkeys.imageio.spi.ReaderWriterProviderInfo; +import com.twelvemonkeys.imageio.spi.ReaderWriterProviderInfoTest; + +/** + * CURProviderInfoTest. + * + * @author Harald Kuhr + * @author last modified by $Author: harald.kuhr$ + * @version $Id: CURProviderInfoTest.java,v 1.0 02/06/16 harald.kuhr Exp$ + */ +public class CURProviderInfoTest extends ReaderWriterProviderInfoTest { + + @Override + protected ReaderWriterProviderInfo createProviderInfo() { + return new CURProviderInfo(); + } +} \ No newline at end of file diff --git a/imageio/imageio-bmp/src/test/java/com/twelvemonkeys/imageio/plugins/bmp/ICOImageReaderTest.java b/imageio/imageio-bmp/src/test/java/com/twelvemonkeys/imageio/plugins/bmp/ICOImageReaderTest.java index 858a26a5..ed5a2adc 100755 --- a/imageio/imageio-bmp/src/test/java/com/twelvemonkeys/imageio/plugins/bmp/ICOImageReaderTest.java +++ b/imageio/imageio-bmp/src/test/java/com/twelvemonkeys/imageio/plugins/bmp/ICOImageReaderTest.java @@ -1,6 +1,6 @@ package com.twelvemonkeys.imageio.plugins.bmp; -import com.twelvemonkeys.imageio.util.ImageReaderAbstractTestCase; +import com.twelvemonkeys.imageio.util.ImageReaderAbstractTest; import org.junit.Ignore; import org.junit.Test; @@ -8,6 +8,7 @@ import javax.imageio.spi.ImageReaderSpi; import java.awt.*; import java.io.IOException; import java.util.Arrays; +import java.util.Collections; import java.util.List; /** @@ -17,7 +18,7 @@ import java.util.List; * @author last modified by $Author: haraldk$ * @version $Id: ICOImageReaderTest.java,v 1.0 Apr 1, 2008 10:39:17 PM haraldk Exp$ */ -public class ICOImageReaderTest extends ImageReaderAbstractTestCase { +public class ICOImageReaderTest extends ImageReaderAbstractTest { protected List getTestData() { return Arrays.asList( new TestData( @@ -38,7 +39,9 @@ public class ICOImageReaderTest extends ImageReaderAbstractTestCase getFormatNames() { - return Arrays.asList("ico"); + return Collections.singletonList("ico"); } protected List getSuffixes() { - return Arrays.asList("ico"); + return Collections.singletonList("ico"); } protected List getMIMETypes() { diff --git a/imageio/imageio-bmp/src/test/java/com/twelvemonkeys/imageio/plugins/bmp/ICOProviderInfoTest.java b/imageio/imageio-bmp/src/test/java/com/twelvemonkeys/imageio/plugins/bmp/ICOProviderInfoTest.java new file mode 100644 index 00000000..832d0ec9 --- /dev/null +++ b/imageio/imageio-bmp/src/test/java/com/twelvemonkeys/imageio/plugins/bmp/ICOProviderInfoTest.java @@ -0,0 +1,19 @@ +package com.twelvemonkeys.imageio.plugins.bmp; + +import com.twelvemonkeys.imageio.spi.ReaderWriterProviderInfo; +import com.twelvemonkeys.imageio.spi.ReaderWriterProviderInfoTest; + +/** + * ICOProviderInfoTest. + * + * @author Harald Kuhr + * @author last modified by $Author: harald.kuhr$ + * @version $Id: ICOProviderInfoTest.java,v 1.0 02/06/16 harald.kuhr Exp$ + */ +public class ICOProviderInfoTest extends ReaderWriterProviderInfoTest { + + @Override + protected ReaderWriterProviderInfo createProviderInfo() { + return new ICOProviderInfo(); + } +} \ No newline at end of file diff --git a/imageio/imageio-bmp/src/test/resources/ico/rgb24bitmask.ico b/imageio/imageio-bmp/src/test/resources/ico/rgb24bitmask.ico new file mode 100644 index 00000000..a2bcda1c Binary files /dev/null and b/imageio/imageio-bmp/src/test/resources/ico/rgb24bitmask.ico differ diff --git a/imageio/imageio-clippath/pom.xml b/imageio/imageio-clippath/pom.xml index d7ab389c..85d4852a 100755 --- a/imageio/imageio-clippath/pom.xml +++ b/imageio/imageio-clippath/pom.xml @@ -1,12 +1,10 @@ - + 4.0.0 com.twelvemonkeys.imageio imageio - 3.1-SNAPSHOT + 3.3-SNAPSHOT imageio-clippath TwelveMonkeys :: ImageIO :: Photoshop Path Support @@ -22,7 +20,7 @@ com.twelvemonkeys.imageio imageio-core - tests + test-jar com.twelvemonkeys.imageio diff --git a/imageio/imageio-core/pom.xml b/imageio/imageio-core/pom.xml index 6d78c59d..3c295d76 100644 --- a/imageio/imageio-core/pom.xml +++ b/imageio/imageio-core/pom.xml @@ -4,7 +4,7 @@ com.twelvemonkeys.imageio imageio - 3.1-SNAPSHOT + 3.3-SNAPSHOT imageio-core TwelveMonkeys :: ImageIO :: Core diff --git a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/AbstractMetadata.java b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/AbstractMetadata.java similarity index 66% rename from imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/AbstractMetadata.java rename to imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/AbstractMetadata.java index 091246c7..d8ff9607 100644 --- a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/AbstractMetadata.java +++ b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/AbstractMetadata.java @@ -26,7 +26,7 @@ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package com.twelvemonkeys.imageio.plugins.psd; +package com.twelvemonkeys.imageio; import org.w3c.dom.Node; @@ -42,13 +42,15 @@ import java.util.Arrays; * @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... +public abstract class AbstractMetadata extends IIOMetadata implements Cloneable { + protected AbstractMetadata(final boolean standardFormatSupported, + final String nativeFormatName, final String nativeFormatClassName, + final String[] extraFormatNames, final String[] extraFormatClassNames) { + super(standardFormatSupported, nativeFormatName, nativeFormatClassName, extraFormatNames, extraFormatClassNames); + } - protected AbstractMetadata(final boolean pStandardFormatSupported, - final String pNativeFormatName, final String pNativeFormatClassName, - final String[] pExtraFormatNames, final String[] pExtraFormatClassNames) { - super(pStandardFormatSupported, pNativeFormatName, pNativeFormatClassName, pExtraFormatNames, pExtraFormatClassNames); + protected AbstractMetadata() { + super(true, null, null, null, null); } /** @@ -63,36 +65,38 @@ abstract class AbstractMetadata extends IIOMetadata implements Cloneable { } @Override - public Node getAsTree(final String pFormatName) { - validateFormatName(pFormatName); + public Node getAsTree(final String formatName) { + validateFormatName(formatName); - if (pFormatName.equals(nativeMetadataFormatName)) { + if (formatName.equals(nativeMetadataFormatName)) { return getNativeTree(); } - else if (pFormatName.equals(IIOMetadataFormatImpl.standardMetadataFormatName)) { + else if (formatName.equals(IIOMetadataFormatImpl.standardMetadataFormatName)) { return getStandardTree(); } - // TODO: What about extra formats?? - throw new AssertionError("Unreachable"); + // Subclasses that supports extra formats need to check for these formats themselves... + return null; + } + + /** + * Default implementation that throws {@code UnsupportedOperationException}. + * Subclasses that supports formats other than standard metadata should override this method. + * + * @throws UnsupportedOperationException + */ + protected Node getNativeTree() { + throw new UnsupportedOperationException("getNativeTree"); } @Override - public void mergeTree(final String pFormatName, final Node pRoot) throws IIOInvalidTreeException { + public void mergeTree(final String formatName, final Node root) throws IIOInvalidTreeException { assertMutable(); - validateFormatName(pFormatName); + validateFormatName(formatName); - 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(); + if (!root.getNodeName().equals(formatName)) { + throw new IIOInvalidTreeException("Root must be " + formatName, root); } } @@ -112,21 +116,19 @@ abstract class AbstractMetadata extends IIOMetadata implements Cloneable { } } - protected abstract Node getNativeTree(); - - protected final void validateFormatName(final String pFormatName) { + protected final void validateFormatName(final String formatName) { String[] metadataFormatNames = getMetadataFormatNames(); if (metadataFormatNames != null) { for (String metadataFormatName : metadataFormatNames) { - if (metadataFormatName.equals(pFormatName)) { + if (metadataFormatName.equals(formatName)) { return; // Found, we're ok! } } } throw new IllegalArgumentException( - String.format("Bad format name: \"%s\". Expected one of %s", pFormatName, Arrays.toString(metadataFormatNames)) + String.format("Bad format name: \"%s\". Expected one of %s", formatName, Arrays.toString(metadataFormatNames)) ); } diff --git a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/ImageReaderBase.java b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/ImageReaderBase.java index fbfa7825..90e16063 100644 --- a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/ImageReaderBase.java +++ b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/ImageReaderBase.java @@ -29,6 +29,7 @@ package com.twelvemonkeys.imageio; import com.twelvemonkeys.image.BufferedImageIcon; +import com.twelvemonkeys.image.ImageUtil; import com.twelvemonkeys.imageio.util.IIOUtil; import javax.imageio.*; @@ -37,6 +38,9 @@ import javax.imageio.spi.ImageReaderSpi; import javax.imageio.stream.ImageInputStream; import javax.swing.*; import java.awt.*; +import java.awt.datatransfer.DataFlavor; +import java.awt.datatransfer.Transferable; +import java.awt.datatransfer.UnsupportedFlavorException; import java.awt.event.*; import java.awt.image.BufferedImage; import java.awt.image.IndexColorModel; @@ -424,6 +428,8 @@ public abstract class ImageReaderBase extends ImageReader { static final String ZOOM_OUT = "zoom-out"; static final String ZOOM_ACTUAL = "zoom-actual"; + private BufferedImage image; + Paint backgroundPaint; final Paint checkeredBG; @@ -434,6 +440,7 @@ public abstract class ImageReaderBase extends ImageReader { setOpaque(false); setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR)); + image = pImage; checkeredBG = createTexture(); // For indexed color, default to the color of the transparent pixel, if any @@ -441,7 +448,7 @@ public abstract class ImageReaderBase extends ImageReader { backgroundPaint = defaultBG != null ? defaultBG : checkeredBG; - setupActions(pImage); + setupActions(); setComponentPopupMenu(createPopupMenu()); addMouseListener(new MouseAdapter() { @Override @@ -451,16 +458,59 @@ public abstract class ImageReaderBase extends ImageReader { } } }); + + setTransferHandler(new TransferHandler() { + @Override + public int getSourceActions(JComponent c) { + return COPY; + } + + @Override + protected Transferable createTransferable(JComponent c) { + return new ImageTransferable(image); + } + + @Override + public boolean importData(JComponent comp, Transferable t) { + if (canImport(comp, t.getTransferDataFlavors())) { + try { + Image transferData = (Image) t.getTransferData(DataFlavor.imageFlavor); + image = ImageUtil.toBuffered(transferData); + setIcon(new BufferedImageIcon(image)); + + return true; + } + catch (UnsupportedFlavorException | IOException ignore) { + } + } + + return false; + } + + @Override + public boolean canImport(JComponent comp, DataFlavor[] transferFlavors) { + for (DataFlavor flavor : transferFlavors) { + if (flavor.equals(DataFlavor.imageFlavor)) { + return true; + } + } + + return false; + } + }); } - private void setupActions(final BufferedImage pImage) { + private void setupActions() { // Mac weirdness... VK_MINUS/VK_PLUS seems to map to english key map always... - bindAction(new ZoomAction("Zoom in", pImage, 2), ZOOM_IN, KeyStroke.getKeyStroke('+'), KeyStroke.getKeyStroke(KeyEvent.VK_ADD, 0)); - bindAction(new ZoomAction("Zoom out", pImage, .5), ZOOM_OUT, KeyStroke.getKeyStroke('-'), KeyStroke.getKeyStroke(KeyEvent.VK_SUBTRACT, 0)); - bindAction(new ZoomAction("Zoom actual", pImage), ZOOM_ACTUAL, KeyStroke.getKeyStroke('0'), KeyStroke.getKeyStroke(KeyEvent.VK_0, 0)); + bindAction(new ZoomAction("Zoom in", 2), ZOOM_IN, KeyStroke.getKeyStroke('+'), KeyStroke.getKeyStroke(KeyEvent.VK_ADD, 0)); + bindAction(new ZoomAction("Zoom out", .5), ZOOM_OUT, KeyStroke.getKeyStroke('-'), KeyStroke.getKeyStroke(KeyEvent.VK_SUBTRACT, 0)); + bindAction(new ZoomAction("Zoom actual"), ZOOM_ACTUAL, KeyStroke.getKeyStroke('0'), KeyStroke.getKeyStroke(KeyEvent.VK_0, 0)); + + bindAction(TransferHandler.getCopyAction(), (String) TransferHandler.getCopyAction().getValue(Action.NAME), KeyStroke.getKeyStroke(KeyEvent.VK_C, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); + bindAction(TransferHandler.getPasteAction(), (String) TransferHandler.getPasteAction().getValue(Action.NAME), KeyStroke.getKeyStroke(KeyEvent.VK_V, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); } - private void bindAction(final AbstractAction action, final String key, final KeyStroke... keyStrokes) { + private void bindAction(final Action action, final String key, final KeyStroke... keyStrokes) { for (KeyStroke keyStroke : keyStrokes) { getInputMap(WHEN_IN_FOCUSED_WINDOW).put(keyStroke, key); } @@ -588,20 +638,18 @@ public abstract class ImageReaderBase extends ImageReader { } private class ZoomAction extends AbstractAction { - private final BufferedImage image; private final double zoomFactor; - public ZoomAction(final String name, final BufferedImage image, final double zoomFactor) { + public ZoomAction(final String name, final double zoomFactor) { super(name); - this.image = image; this.zoomFactor = zoomFactor; } - public ZoomAction(final String name, final BufferedImage image) { - this(name, image, 0); + public ZoomAction(final String name) { + this(name, 0); } - public void actionPerformed(ActionEvent e) { + public void actionPerformed(final ActionEvent e) { if (zoomFactor <= 0) { setIcon(new BufferedImageIcon(image)); } @@ -614,6 +662,33 @@ public abstract class ImageReaderBase extends ImageReader { } } } + + private static class ImageTransferable implements Transferable { + private final BufferedImage image; + + public ImageTransferable(final BufferedImage image) { + this.image = image; + } + + @Override + public DataFlavor[] getTransferDataFlavors() { + return new DataFlavor[] {DataFlavor.imageFlavor}; + } + + @Override + public boolean isDataFlavorSupported(final DataFlavor flavor) { + return DataFlavor.imageFlavor.equals(flavor); + } + + @Override + public Object getTransferData(final DataFlavor flavor) throws UnsupportedFlavorException, IOException { + if (isDataFlavorSupported(flavor)) { + return image; + } + + throw new UnsupportedFlavorException(flavor); + } + } } private static class ExitIfNoWindowPresentHandler extends WindowAdapter { diff --git a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/color/CIELabColorConverter.java b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/color/CIELabColorConverter.java new file mode 100644 index 00000000..3abe5f47 --- /dev/null +++ b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/color/CIELabColorConverter.java @@ -0,0 +1,151 @@ +package com.twelvemonkeys.imageio.color; + +import com.twelvemonkeys.lang.Validate; + +/** + * Converts between CIE L*a*b* and sRGB color spaces. + */ +// Code adapted from ImageJ's Color_Space_Converter.java (Public Domain): +// http://rsb.info.nih.gov/ij/plugins/download/Color_Space_Converter.java +public final class CIELabColorConverter { + // TODO: Create interface in the color package? + // TODO: Make YCbCr/YCCK -> RGB/CMYK implement same interface? + + public enum Illuminant { + D50(new float[] {96.4212f, 100.0f, 82.5188f}), + D65(new float[] {95.0429f, 100.0f, 108.8900f}); + + private final float[] whitePoint; + + Illuminant(final float[] wp) { + whitePoint = Validate.isTrue(wp != null && wp.length == 3, wp, "Bad white point definition: %s"); + } + + public float[] getWhitePoint() { + return whitePoint; + } + } + + private final float[] whitePoint; + + public CIELabColorConverter(final Illuminant illuminant) { + whitePoint = Validate.notNull(illuminant, "illuminant").getWhitePoint(); + } + + private float clamp(float x) { + if (x < 0.0f) { + return 0.0f; + } + else if (x > 255.0f) { + return 255.0f; + } + else { + return x; + } + } + + public void toRGB(float l, float a, float b, float[] rgbResult) { + XYZtoRGB(LABtoXYZ(l, a, b, rgbResult), rgbResult); + } + + /** + * Convert LAB to XYZ. + * @param L + * @param a + * @param b + * @return XYZ values + */ + private float[] LABtoXYZ(float L, float a, float b, float[] xyzResult) { + // Significant speedup: Removing Math.pow + float y = (L + 16.0f) / 116.0f; + float y3 = y * y * y; // Math.pow(y, 3.0); + float x = (a / 500.0f) + y; + float x3 = x * x * x; // Math.pow(x, 3.0); + float z = y - (b / 200.0f); + float z3 = z * z * z; // Math.pow(z, 3.0); + + if (y3 > 0.008856f) { + y = y3; + } + else { + y = (y - (16.0f / 116.0f)) / 7.787f; + } + + if (x3 > 0.008856f) { + x = x3; + } + else { + x = (x - (16.0f / 116.0f)) / 7.787f; + } + + if (z3 > 0.008856f) { + z = z3; + } + else { + z = (z - (16.0f / 116.0f)) / 7.787f; + } + + xyzResult[0] = x * whitePoint[0]; + xyzResult[1] = y * whitePoint[1]; + xyzResult[2] = z * whitePoint[2]; + + return xyzResult; + } + + /** + * Convert XYZ to RGB + * @param xyz + * @return RGB values + */ + private float[] XYZtoRGB(final float[] xyz, final float[] rgbResult) { + return XYZtoRGB(xyz[0], xyz[1], xyz[2], rgbResult); + } + + private float[] XYZtoRGB(final float X, final float Y, final float Z, float[] rgbResult) { + float x = X / 100.0f; + float y = Y / 100.0f; + float z = Z / 100.0f; + + float r = x * 3.2406f + y * -1.5372f + z * -0.4986f; + float g = x * -0.9689f + y * 1.8758f + z * 0.0415f; + float b = x * 0.0557f + y * -0.2040f + z * 1.0570f; + + // assume sRGB + if (r > 0.0031308f) { + r = ((1.055f * (float) pow(r, 1.0 / 2.4)) - 0.055f); + } + else { + r = (r * 12.92f); + } + + if (g > 0.0031308f) { + g = ((1.055f * (float) pow(g, 1.0 / 2.4)) - 0.055f); + } + else { + g = (g * 12.92f); + } + + if (b > 0.0031308f) { + b = ((1.055f * (float) pow(b, 1.0 / 2.4)) - 0.055f); + } + else { + b = (b * 12.92f); + } + + // convert 0..1 into 0..255 + rgbResult[0] = clamp(r * 255); + rgbResult[1] = clamp(g * 255); + rgbResult[2] = clamp(b * 255); + + return rgbResult; + } + + // TODO: Test, to figure out if accuracy is good enough. + // Visual inspection looks good! The author claims 5-12% error, worst case up to 25%... + // http://martin.ankerl.com/2007/10/04/optimized-pow-approximation-for-java-and-c-c/ + static double pow(final double a, final double b) { + long tmp = Double.doubleToLongBits(a); + long tmp2 = (long) (b * (tmp - 4606921280493453312L)) + 4606921280493453312L; + return Double.longBitsToDouble(tmp2); + } +} diff --git a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/color/ColorSpaces.java b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/color/ColorSpaces.java index 953c8cbd..1e5337fa 100644 --- a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/color/ColorSpaces.java +++ b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/color/ColorSpaces.java @@ -52,7 +52,7 @@ import java.util.Properties; *

* Color profiles may be configured by placing a property-file * {@code com/twelvemonkeys/imageio/color/icc_profiles.properties} - * on the classpath, specifying the full path to the profile. + * on the classpath, specifying the full path to the profiles. * ICC color profiles are probably already present on your system, or * can be downloaded from * ICC, @@ -84,11 +84,11 @@ public final class ColorSpaces { public static final int CS_GENERIC_CMYK = 5001; // Weak references to hold the color spaces while cached - private static WeakReference adobeRGB1998 = new WeakReference(null); - private static WeakReference genericCMYK = new WeakReference(null); + private static WeakReference adobeRGB1998 = new WeakReference<>(null); + private static WeakReference genericCMYK = new WeakReference<>(null); // Cache for the latest used color spaces - private static final Map cache = new LRUHashMap(10); + private static final Map cache = new LRUHashMap<>(10); private ColorSpaces() {} @@ -100,7 +100,8 @@ public final class ColorSpaces { * * @param profile the ICC color profile. May not be {@code null}. * @return an ICC color space - * @throws IllegalArgumentException if {@code profile} is {@code null} + * @throws IllegalArgumentException if {@code profile} is {@code null}. + * @throws java.awt.color.CMMException if {@code profile} is invalid. */ public static ICC_ColorSpace createColorSpace(final ICC_Profile profile) { Validate.notNull(profile, "profile"); @@ -161,6 +162,11 @@ public final class ColorSpaces { if (cs == null) { cs = new ICC_ColorSpace(profile); + + // Validate the color space, to avoid caching bad color spaces + // Will throw IllegalArgumentException or CMMException if the profile is bad + cs.fromRGB(new float[] {1f, 0f, 0f}); + cache.put(key, cs); } @@ -195,7 +201,7 @@ public final class ColorSpaces { * @return {@code true} if known to be offending, {@code false} otherwise * @throws IllegalArgumentException if {@code profile} is {@code null} */ - public static boolean isOffendingColorProfile(final ICC_Profile profile) { + static boolean isOffendingColorProfile(final ICC_Profile profile) { Validate.notNull(profile, "profile"); // NOTE: @@ -213,6 +219,26 @@ public final class ColorSpaces { return data[ICC_Profile.icHdrRenderingIntent] != 0; } + /** + * Tests whether an ICC color profile is valid. + * Invalid profiles are known to cause problems for {@link java.awt.image.ColorConvertOp}. + *

+ * + * Note that this method only tests if a color conversion using this profile is known to fail. + * There's no guarantee that the color conversion will succeed even if this method returns {@code false}. + * + * + * @param profile the ICC color profile. May not be {@code null}. + * @return {@code profile} if valid. + * @throws IllegalArgumentException if {@code profile} is {@code null} + * @throws java.awt.color.CMMException if {@code profile} is invalid. + */ + public static ICC_Profile validateProfile(final ICC_Profile profile) { + createColorSpace(profile); // Creating a color space will fail if the profile is bad + + return profile; + } + /** * Returns the color space specified by the given color space constant. *

@@ -249,7 +275,7 @@ public final class ColorSpaces { } } - adobeRGB1998 = new WeakReference(profile); + adobeRGB1998 = new WeakReference<>(profile); } } @@ -272,7 +298,7 @@ public final class ColorSpaces { return CMYKColorSpace.getInstance(); } - genericCMYK = new WeakReference(profile); + genericCMYK = new WeakReference<>(profile); } } @@ -317,7 +343,7 @@ public final class ColorSpaces { try { return ICC_Profile.getInstance(profilePath); } - catch (IOException ignore) { + catch (SecurityException | IOException ignore) { if (DEBUG) { ignore.printStackTrace(); } @@ -367,15 +393,18 @@ public final class ColorSpaces { } private static class Profiles { - private static final Properties PROFILES = loadProfiles(Platform.os()); + private static final Properties PROFILES = loadProfiles(); - private static Properties loadProfiles(final Platform.OperatingSystem os) { + private static Properties loadProfiles() { Properties systemDefaults; try { - systemDefaults = SystemUtil.loadProperties(ColorSpaces.class, "com/twelvemonkeys/imageio/color/icc_profiles_" + os.id()); + systemDefaults = SystemUtil.loadProperties( + ColorSpaces.class, + "com/twelvemonkeys/imageio/color/icc_profiles_" + Platform.os().id() + ); } - catch (IOException ignore) { + catch (SecurityException | IOException ignore) { System.err.printf( "Warning: Could not load system default ICC profile locations from %s, will use bundled fallback profiles.\n", ignore.getMessage() @@ -392,10 +421,14 @@ public final class ColorSpaces { Properties profiles = new Properties(systemDefaults); try { - Properties userOverrides = SystemUtil.loadProperties(ColorSpaces.class, "com/twelvemonkeys/imageio/color/icc_profiles"); + Properties userOverrides = SystemUtil.loadProperties( + ColorSpaces.class, + "com/twelvemonkeys/imageio/color/icc_profiles" + ); profiles.putAll(userOverrides); } - catch (IOException ignore) { + catch (SecurityException | IOException ignore) { + // Most likely, this file won't be there } if (DEBUG) { diff --git a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/color/ICCProfileSanitizer.java b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/color/ICCProfileSanitizer.java index 0156c5ec..10352384 100644 --- a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/color/ICCProfileSanitizer.java +++ b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/color/ICCProfileSanitizer.java @@ -14,7 +14,7 @@ import java.awt.color.ICC_Profile; interface ICCProfileSanitizer { void fixProfile(ICC_Profile profile, byte[] profileHeader); - static class Factory { + class Factory { static ICCProfileSanitizer get() { // Strategy pattern: // - KCMSSanitizerStrategy - Current behaviour, default for Java 1.6 and Oracle JRE 1.7 diff --git a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/color/Int16ComponentColorModel.java b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/color/Int16ComponentColorModel.java new file mode 100644 index 00000000..3a38023f --- /dev/null +++ b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/color/Int16ComponentColorModel.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2014, 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.color; + +import java.awt.color.ColorSpace; +import java.awt.image.ComponentColorModel; +import java.awt.image.DataBuffer; + +/** + * ComponentColorModel subclass that correctly handles full 16 bit {@code TYPE_SHORT} signed integral samples. + ** + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: UInt32ColorModel.java,v 1.0 24.01.11 17.51 haraldk Exp$ + */ +public final class Int16ComponentColorModel extends ComponentColorModel { + private final ComponentColorModel delegate; + + public Int16ComponentColorModel(final ColorSpace cs, final boolean hasAlpha, boolean isAlphaPremultiplied) { + super(cs, hasAlpha, isAlphaPremultiplied, hasAlpha ? TRANSLUCENT : OPAQUE, DataBuffer.TYPE_SHORT); + + delegate = new ComponentColorModel(cs, hasAlpha, isAlphaPremultiplied, hasAlpha ? TRANSLUCENT : OPAQUE, DataBuffer.TYPE_USHORT); + } + + private void remap(final short[] s, final int i) { + // MIN ... -1 -> 0 ... MAX + // 0 ... MAX -> MIN ... -1 + short sample = s[i]; + + if (sample < 0) { + s[i] = (short) (sample - Short.MIN_VALUE); + } + else { + s[i] = (short) (sample + Short.MIN_VALUE); + } + } + + @Override + public int getRed(final Object inData) { + remap((short[]) inData, 0); + + return delegate.getRed(inData); + } + + @Override + public int getGreen(final Object inData) { + remap((short[]) inData, 1); + + return delegate.getGreen(inData); + } + + @Override + public int getBlue(final Object inData) { + remap((short[]) inData, 2); + + return delegate.getBlue(inData); + } +} diff --git a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/color/YCbCrConverter.java b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/color/YCbCrConverter.java new file mode 100644 index 00000000..f21f712c --- /dev/null +++ b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/color/YCbCrConverter.java @@ -0,0 +1,81 @@ +package com.twelvemonkeys.imageio.color; + +/** + * Fast YCbCr to RGB conversion. + * + * @author Harald Kuhr + * @author Original code by Werner Randelshofer (used by permission). + */ +public final class YCbCrConverter { + /** + * Define tables for YCC->RGB color space conversion. + */ + private final static int SCALEBITS = 16; + private final static int MAXJSAMPLE = 255; + private final static int CENTERJSAMPLE = 128; + private final static int ONE_HALF = 1 << (SCALEBITS - 1); + + private final static int[] Cr_R_LUT = new int[MAXJSAMPLE + 1]; + private final static int[] Cb_B_LUT = new int[MAXJSAMPLE + 1]; + private final static int[] Cr_G_LUT = new int[MAXJSAMPLE + 1]; + private final static int[] Cb_G_LUT = new int[MAXJSAMPLE + 1]; + + /** + * Initializes tables for YCC->RGB color space conversion. + */ + private static void buildYCCtoRGBtable() { + if (ColorSpaces.DEBUG) { + System.err.println("Building YCC conversion table"); + } + + for (int i = 0, x = -CENTERJSAMPLE; i <= MAXJSAMPLE; i++, x++) { + // i is the actual input pixel value, in the range 0..MAXJSAMPLE + // The Cb or Cr value we are thinking of is x = i - CENTERJSAMPLE + // Cr=>R value is nearest int to 1.40200 * x + Cr_R_LUT[i] = (int) ((1.40200 * (1 << SCALEBITS) + 0.5) * x + ONE_HALF) >> SCALEBITS; + // Cb=>B value is nearest int to 1.77200 * x + Cb_B_LUT[i] = (int) ((1.77200 * (1 << SCALEBITS) + 0.5) * x + ONE_HALF) >> SCALEBITS; + // Cr=>G value is scaled-up -0.71414 * x + Cr_G_LUT[i] = -(int) (0.71414 * (1 << SCALEBITS) + 0.5) * x; + // Cb=>G value is scaled-up -0.34414 * x + // We also add in ONE_HALF so that need not do it in inner loop + Cb_G_LUT[i] = -(int) ((0.34414) * (1 << SCALEBITS) + 0.5) * x + ONE_HALF; + } + } + + static { + buildYCCtoRGBtable(); + } + + public static void convertYCbCr2RGB(final byte[] yCbCr, final byte[] rgb, final double[] coefficients, final int offset) { + double y = (yCbCr[offset] & 0xff); + double cb = (yCbCr[offset + 1] & 0xff) - 128; + double cr = (yCbCr[offset + 2] & 0xff) - 128; + + double lumaRed = coefficients[0]; + double lumaGreen = coefficients[1]; + double lumaBlue = coefficients[2]; + + int red = (int) Math.round(cr * (2 - 2 * lumaRed) + y); + int blue = (int) Math.round(cb * (2 - 2 * lumaBlue) + y); + int green = (int) Math.round((y - lumaRed * red - lumaBlue * blue) / lumaGreen); + + rgb[offset] = clamp(red); + rgb[offset + 2] = clamp(blue); + rgb[offset + 1] = clamp(green); + } + + public static void convertYCbCr2RGB(final byte[] yCbCr, final byte[] rgb, final int offset) { + int y = yCbCr[offset] & 0xff; + int cr = yCbCr[offset + 2] & 0xff; + int cb = yCbCr[offset + 1] & 0xff; + + rgb[offset] = clamp(y + Cr_R_LUT[cr]); + rgb[offset + 1] = clamp(y + (Cb_G_LUT[cb] + Cr_G_LUT[cr] >> SCALEBITS)); + rgb[offset + 2] = clamp(y + Cb_B_LUT[cb]); + } + + private static byte clamp(int val) { + return (byte) Math.max(0, Math.min(255, val)); + } +} diff --git a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/spi/ProviderInfo.java b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/spi/ProviderInfo.java index a03fa076..61048791 100644 --- a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/spi/ProviderInfo.java +++ b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/spi/ProviderInfo.java @@ -73,7 +73,7 @@ public class ProviderInfo { return name.startsWith("com.twelvemonkeys") ? "TwelveMonkeys" : name; } - private String fakeVersion(Package pPackage) { + private String fakeVersion(final Package pPackage) { String name = pPackage.getName(); return name.startsWith("com.twelvemonkeys") ? "DEV" : "Unspecified"; } diff --git a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/spi/ReaderWriterProviderInfo.java b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/spi/ReaderWriterProviderInfo.java index 3b30a3af..a6138816 100644 --- a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/spi/ReaderWriterProviderInfo.java +++ b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/spi/ReaderWriterProviderInfo.java @@ -1,3 +1,31 @@ +/* + * Copyright (c) 2015, 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.spi; import javax.imageio.stream.ImageInputStream; @@ -24,7 +52,7 @@ public abstract class ReaderWriterProviderInfo extends ProviderInfo { private final String[] writerSpiClassNames; private final Class[] outputTypes = new Class[] {ImageOutputStream.class}; private final boolean supportsStandardStreamMetadata; - private final String nativeStreameMetadataFormatName; + private final String nativeStreamMetadataFormatName; private final String nativeStreamMetadataFormatClassName; private final String[] extraStreamMetadataFormatNames; private final String[] extraStreamMetadataFormatClassNames; @@ -38,7 +66,8 @@ public abstract class ReaderWriterProviderInfo extends ProviderInfo { * Creates a provider information instance based on the given class. * * @param infoClass the class to get provider information from. - * The provider info will be taken from the class' package. @throws IllegalArgumentException if {@code pPackage == null} + * The provider info will be taken from the class' package. + * @throws IllegalArgumentException if {@code pPackage == null} */ protected ReaderWriterProviderInfo(final Class infoClass, final String[] formatNames, @@ -68,7 +97,7 @@ public abstract class ReaderWriterProviderInfo extends ProviderInfo { this.writerClassName = writerClassName; this.writerSpiClassNames = writerSpiClassNames; this.supportsStandardStreamMetadata = supportsStandardStreamMetadata; - this.nativeStreameMetadataFormatName = nativeStreameMetadataFormatName; + this.nativeStreamMetadataFormatName = nativeStreameMetadataFormatName; this.nativeStreamMetadataFormatClassName = nativeStreamMetadataFormatClassName; this.extraStreamMetadataFormatNames = extraStreamMetadataFormatNames; this.extraStreamMetadataFormatClassNames = extraStreamMetadataFormatClassNames; @@ -120,7 +149,7 @@ public abstract class ReaderWriterProviderInfo extends ProviderInfo { } public String nativeStreamMetadataFormatName() { - return nativeStreameMetadataFormatName; + return nativeStreamMetadataFormatName; } public String nativeStreamMetadataFormatClassName() { diff --git a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/SubImageOutputStream.java b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/SubImageOutputStream.java new file mode 100644 index 00000000..372e5758 --- /dev/null +++ b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/SubImageOutputStream.java @@ -0,0 +1,55 @@ +package com.twelvemonkeys.imageio.stream; + +import javax.imageio.stream.ImageOutputStream; +import javax.imageio.stream.ImageOutputStreamImpl; +import java.io.IOException; + +/** + * SubImageOutputStream. + * + * @author Harald Kuhr + * @author last modified by $Author: harald.kuhr$ + * @version $Id: SubImageOutputStream.java,v 1.0 30/03/15 harald.kuhr Exp$ + */ +public class SubImageOutputStream extends ImageOutputStreamImpl { + private final ImageOutputStream stream; + + public SubImageOutputStream(final ImageOutputStream stream) { + this.stream = stream; + } + + @Override + public void write(int b) throws IOException { + stream.write(b); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + stream.write(b, off, len); + } + + @Override + public int read() throws IOException { + return stream.read(); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + return stream.read(b, off, len); + } + + @Override + public boolean isCached() { + return stream.isCached(); + } + + @Override + public boolean isCachedMemory() { + return stream.isCachedMemory(); + } + + @Override + public boolean isCachedFile() { + return stream.isCachedFile(); + } +} diff --git a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/util/ImageTypeSpecifiers.java b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/util/ImageTypeSpecifiers.java index 165e4152..05cf4468 100644 --- a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/util/ImageTypeSpecifiers.java +++ b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/util/ImageTypeSpecifiers.java @@ -28,14 +28,19 @@ package com.twelvemonkeys.imageio.util; +import com.twelvemonkeys.lang.Validate; + import javax.imageio.ImageTypeSpecifier; import java.awt.color.ColorSpace; -import java.awt.image.DataBuffer; -import java.awt.image.IndexColorModel; +import java.awt.image.*; + +import static com.twelvemonkeys.lang.Validate.isTrue; +import static com.twelvemonkeys.lang.Validate.notNull; /** * Factory class for creating {@code ImageTypeSpecifier}s. - * In most cases, this class will delegate to the corresponding methods in {@link ImageTypeSpecifier}. + * Fixes some subtle bugs in {@code ImageTypeSpecifier}'s factory methods, but + * in most cases, this class will delegate to the corresponding methods in {@link ImageTypeSpecifier}. * * @see javax.imageio.ImageTypeSpecifier * @author Harald Kuhr @@ -54,6 +59,20 @@ public final class ImageTypeSpecifiers { final int redMask, final int greenMask, final int blueMask, final int alphaMask, final int transferType, boolean isAlphaPremultiplied) { + if (transferType == DataBuffer.TYPE_BYTE || transferType == DataBuffer.TYPE_USHORT) { + // ImageTypeSpecifier unconditionally uses bits == 32, we'll use a workaround for BYTE/USHORT types + notNull(colorSpace, "colorSpace"); + isTrue(colorSpace.getType() == ColorSpace.TYPE_RGB, colorSpace, "ColorSpace must be TYPE_RGB"); + isTrue(redMask != 0 || greenMask != 0 || blueMask != 0 || alphaMask != 0, "No mask has at least 1 bit set"); + + int bits = transferType == DataBuffer.TYPE_BYTE ? 8 : 16; + + ColorModel colorModel = new DirectColorModel(colorSpace, bits, redMask, greenMask, blueMask, alphaMask, + isAlphaPremultiplied, transferType); + + return new ImageTypeSpecifier(colorModel, colorModel.createCompatibleSampleModel(1, 1)); + } + return ImageTypeSpecifier.createPacked(colorSpace, redMask, greenMask, blueMask, alphaMask, transferType, isAlphaPremultiplied); } @@ -79,7 +98,11 @@ public final class ImageTypeSpecifiers { } public static ImageTypeSpecifier createGrayscale(final int bits, final int dataType) { - if (bits == 32 && dataType == DataBuffer.TYPE_INT) { + if (bits == 16 && dataType == DataBuffer.TYPE_SHORT) { + // As the ComponentColorModel is broken for 16 bit signed int, we'll use our own version + return new Int16ImageTypeSpecifier(ColorSpace.getInstance(ColorSpace.CS_GRAY), new int[] {0}, false, false); + } + else if (bits == 32 && dataType == DataBuffer.TYPE_INT) { // As the ComponentColorModel is broken for 32 bit unsigned int, we'll use our own version return new UInt32ImageTypeSpecifier(ColorSpace.getInstance(ColorSpace.CS_GRAY), new int[] {0}, false, false); } @@ -89,7 +112,11 @@ public final class ImageTypeSpecifiers { } public static ImageTypeSpecifier createGrayscale(final int bits, final int dataType, final boolean isAlphaPremultiplied) { - if (bits == 32 && dataType == DataBuffer.TYPE_INT) { + if (bits == 16 && dataType == DataBuffer.TYPE_SHORT) { + // As the ComponentColorModel is broken for 16 bit signed int, we'll use our own version + return new Int16ImageTypeSpecifier(ColorSpace.getInstance(ColorSpace.CS_GRAY), new int[] {0, 1}, true, isAlphaPremultiplied); + } + else if (bits == 32 && dataType == DataBuffer.TYPE_INT) { // As the ComponentColorModel is broken for 32 bit unsigned int, we'll use our own version return new UInt32ImageTypeSpecifier(ColorSpace.getInstance(ColorSpace.CS_GRAY), new int[] {0, 1}, true, isAlphaPremultiplied); } @@ -98,6 +125,34 @@ public final class ImageTypeSpecifiers { return ImageTypeSpecifier.createGrayscale(bits, dataType, false, isAlphaPremultiplied); } + public static ImageTypeSpecifier createPackedGrayscale(final ColorSpace colorSpace, final int bits, final int dataType) { + notNull(colorSpace, "colorSpace"); + isTrue(colorSpace.getType() == ColorSpace.TYPE_GRAY, colorSpace, "ColorSpace must be TYPE_GRAY"); + isTrue(bits == 1 || bits == 2 || bits == 4, bits, "bits must be 1, 2, or 4: %s"); + isTrue(dataType == DataBuffer.TYPE_BYTE, dataType, "dataType must be TYPE_BYTE: %s"); + + int numEntries = 1 << bits; + + byte[] arr = new byte[numEntries]; + byte[] arg = new byte[numEntries]; + byte[] arb = new byte[numEntries]; + + // Scale array values according to color profile.. + for (int i = 0; i < numEntries; i++) { + float[] gray = new float[]{i / (float) (numEntries - 1)}; + float[] rgb = colorSpace.toRGB(gray); + + arr[i] = (byte) (rgb[0] * 255); + arg[i] = (byte) (rgb[1] * 255); + arb[i] = (byte) (rgb[2]* 255); + } + + ColorModel colorModel = new IndexColorModel(bits, numEntries, arr, arg, arb); + SampleModel sampleModel = new MultiPixelPackedSampleModel(dataType, 1, 1, bits); + + return new ImageTypeSpecifier(colorModel, sampleModel); + } + public static ImageTypeSpecifier createIndexed(final byte[] redLUT, final byte[] greenLUT, final byte[] blueLUT, final byte[] alphaLUT, final int bits, final int dataType) { diff --git a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/util/Int16ImageTypeSpecifier.java b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/util/Int16ImageTypeSpecifier.java new file mode 100644 index 00000000..e79ed3d3 --- /dev/null +++ b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/util/Int16ImageTypeSpecifier.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2014, 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.util; + +import com.twelvemonkeys.imageio.color.Int16ComponentColorModel; + +import javax.imageio.ImageTypeSpecifier; +import java.awt.color.ColorSpace; +import java.awt.image.DataBuffer; +import java.awt.image.PixelInterleavedSampleModel; + +/** + * ImageTypeSpecifier for interleaved 16 bit signed integral samples. + * + * @see com.twelvemonkeys.imageio.color.Int16ColorModel + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: Int16ImageTypeSpecifier.java,v 1.0 24.01.11 17.51 haraldk Exp$ + */ +final class Int16ImageTypeSpecifier extends ImageTypeSpecifier { + Int16ImageTypeSpecifier(final ColorSpace cs, int[] bandOffsets, final boolean hasAlpha, final boolean isAlphaPremultiplied) { + super( + new Int16ComponentColorModel(cs, hasAlpha, isAlphaPremultiplied), + new PixelInterleavedSampleModel( + DataBuffer.TYPE_SHORT, 1, 1, + cs.getNumComponents() + (hasAlpha ? 1 : 0), + cs.getNumComponents() + (hasAlpha ? 1 : 0), + bandOffsets + ) + ); + } +} diff --git a/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/color/CIELabColorConverterTest.java b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/color/CIELabColorConverterTest.java new file mode 100644 index 00000000..ff3ddbeb --- /dev/null +++ b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/color/CIELabColorConverterTest.java @@ -0,0 +1,68 @@ +package com.twelvemonkeys.imageio.color; + +import com.twelvemonkeys.imageio.color.CIELabColorConverter.Illuminant; +import org.junit.Test; + +import static org.junit.Assert.assertArrayEquals; + +/** + * CIELabColorConverterTest. + * + * @author Harald Kuhr + * @author last modified by $Author: harald.kuhr$ + * @version $Id: CIELabColorConverterTest.java,v 1.0 22/10/15 harald.kuhr Exp$ + */ +public class CIELabColorConverterTest { + @Test(expected = IllegalArgumentException.class) + public void testNoIllumninant() { + new CIELabColorConverter(null); + } + + @Test + public void testD50() { + CIELabColorConverter converter = new CIELabColorConverter(Illuminant.D50); + float[] rgb = new float[3]; + + converter.toRGB(100, -128, -128, rgb); + assertArrayEquals(new float[] {0, 255, 255}, rgb, 1); + + converter.toRGB(100, 0, 0, rgb); + assertArrayEquals(new float[] {255, 252, 220}, rgb, 5); + + converter.toRGB(0, 0, 0, rgb); + assertArrayEquals(new float[] {0, 0, 0}, rgb, 1); + + converter.toRGB(100, 0, 127, rgb); + assertArrayEquals(new float[] {255, 249, 0}, rgb, 5); + + converter.toRGB(50, -128, 127, rgb); + assertArrayEquals(new float[] {0, 152, 0}, rgb, 2); + + converter.toRGB(50, 127, -128, rgb); + assertArrayEquals(new float[] {222, 0, 255}, rgb, 2); + } + + @Test + public void testD65() { + CIELabColorConverter converter = new CIELabColorConverter(Illuminant.D65); + float[] rgb = new float[3]; + + converter.toRGB(100, -128, -128, rgb); + assertArrayEquals(new float[] {0, 255, 255}, rgb, 1); + + converter.toRGB(100, 0, 0, rgb); + assertArrayEquals(new float[] {255, 252, 255}, rgb, 5); + + converter.toRGB(0, 0, 0, rgb); + assertArrayEquals(new float[] {0, 0, 0}, rgb, 1); + + converter.toRGB(100, 0, 127, rgb); + assertArrayEquals(new float[] {255, 250, 0}, rgb, 5); + + converter.toRGB(50, -128, 127, rgb); + assertArrayEquals(new float[] {0, 152, 0}, rgb, 3); + + converter.toRGB(50, 127, -128, rgb); + assertArrayEquals(new float[] {184, 0, 255}, rgb, 5); + } +} diff --git a/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/spi/ReaderWriterProviderInfoTest.java b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/spi/ReaderWriterProviderInfoTest.java new file mode 100644 index 00000000..af0df7ed --- /dev/null +++ b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/spi/ReaderWriterProviderInfoTest.java @@ -0,0 +1,160 @@ +package com.twelvemonkeys.imageio.spi; + +import org.hamcrest.Description; +import org.junit.Test; +import org.junit.internal.matchers.TypeSafeMatcher; + +import javax.imageio.ImageReader; +import javax.imageio.ImageWriter; +import javax.imageio.metadata.IIOMetadataFormat; +import javax.imageio.spi.ImageReaderSpi; +import javax.imageio.spi.ImageWriterSpi; +import java.util.List; + +import static java.util.Arrays.asList; +import static org.junit.Assert.*; + +/** + * ReaderWriterProviderInfoTest. + * + * @author Harald Kuhr + * @author last modified by $Author: harald.kuhr$ + * @version $Id: ReaderWriterProviderInfoTest.java,v 1.0 02/06/16 harald.kuhr Exp$ + */ +public abstract class ReaderWriterProviderInfoTest { + + private final ReaderWriterProviderInfo providerInfo = createProviderInfo(); + + protected abstract ReaderWriterProviderInfo createProviderInfo(); + + protected final ReaderWriterProviderInfo getProviderInfo() { + return providerInfo; + } + + @Test + public void readerClassName() throws Exception { + assertClassExists(providerInfo.readerClassName(), ImageReader.class); + } + + @Test + public void readerSpiClassNames() throws Exception { + assertClassesExist(providerInfo.readerSpiClassNames(), ImageReaderSpi.class); + } + + @Test + public void inputTypes() throws Exception { + assertNotNull(providerInfo.inputTypes()); + } + + @Test + public void writerClassName() throws Exception { + assertClassExists(providerInfo.writerClassName(), ImageWriter.class); + } + + @Test + public void writerSpiClassNames() throws Exception { + assertClassesExist(providerInfo.writerSpiClassNames(), ImageWriterSpi.class); + } + + @Test + public void outputTypes() throws Exception { + assertNotNull(providerInfo.outputTypes()); + } + + @Test + public void nativeStreamMetadataFormatClassName() throws Exception { + assertClassExists(providerInfo.nativeStreamMetadataFormatClassName(), IIOMetadataFormat.class); + } + + @Test + public void extraStreamMetadataFormatClassNames() throws Exception { + assertClassesExist(providerInfo.extraStreamMetadataFormatClassNames(), IIOMetadataFormat.class); + } + + @Test + public void nativeImageMetadataFormatClassName() throws Exception { + assertClassExists(providerInfo.nativeImageMetadataFormatClassName(), IIOMetadataFormat.class); + } + + @Test + public void extraImageMetadataFormatClassNames() throws Exception { + assertClassesExist(providerInfo.extraImageMetadataFormatClassNames(), IIOMetadataFormat.class); + } + + @Test + public void formatNames() { + String[] names = providerInfo.formatNames(); + assertNotNull(names); + assertFalse(names.length == 0); + + List list = asList(names); + + for (String name : list) { + assertNotNull(name); + assertFalse(name.isEmpty()); + + assertTrue(list.contains(name.toLowerCase())); + assertTrue(list.contains(name.toUpperCase())); + } + } + + @Test + public void suffixes() { + String[] suffixes = providerInfo.suffixes(); + assertNotNull(suffixes); + assertFalse(suffixes.length == 0); + + for (String suffix : suffixes) { + assertNotNull(suffix); + assertFalse(suffix.isEmpty()); + } + } + + @Test + public void mimeTypes() { + String[] mimeTypes = providerInfo.mimeTypes(); + assertNotNull(mimeTypes); + assertFalse(mimeTypes.length == 0); + + for (String mimeType : mimeTypes) { + assertNotNull(mimeType); + assertFalse(mimeType.isEmpty()); + + assertTrue(mimeType.length() > 1); + assertTrue(mimeType.indexOf('/') > 0); + assertTrue(mimeType.indexOf('/') < mimeType.length() - 1); + } + } + + public static void assertClassExists(final String className, final Class type) { + if (className != null) { + try { + final Class cl = Class.forName(className); + + assertThat(cl, new TypeSafeMatcher>() { + @Override + public boolean matchesSafely(Class item) { + return type.isAssignableFrom(cl); + } + + @Override + public void describeTo(Description description) { + description.appendText("is subclass of ").appendValue(type); + } + }); + } + catch (ClassNotFoundException e) { + e.printStackTrace(); + fail("Class not found: " + e.getMessage()); + } + } + } + + public static void assertClassesExist(final String[] classNames, final Class type) { + if (classNames != null) { + for (String className : classNames) { + assertClassExists(className, type); + } + } + } +} \ No newline at end of file diff --git a/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/ImageReaderAbstractTestCase.java b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/ImageReaderAbstractTest.java similarity index 98% rename from imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/ImageReaderAbstractTestCase.java rename to imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/ImageReaderAbstractTest.java index 7b1f2a14..a92f75c2 100644 --- a/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/ImageReaderAbstractTestCase.java +++ b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/ImageReaderAbstractTest.java @@ -57,13 +57,13 @@ import static org.junit.Assert.*; import static org.mockito.Mockito.*; /** - * ImageReaderAbstractTestCase + * ImageReaderAbstractTest * * @author Harald Kuhr * @author last modified by $Author: haraldk$ - * @version $Id: ImageReaderAbstractTestCase.java,v 1.0 Apr 1, 2008 10:36:46 PM haraldk Exp$ + * @version $Id: ImageReaderAbstractTest.java,v 1.0 Apr 1, 2008 10:36:46 PM haraldk Exp$ */ -public abstract class ImageReaderAbstractTestCase { +public abstract class ImageReaderAbstractTest { // TODO: Should we really test if the provider is installed? // - Pro: Tests the META-INF/services config // - Con: Not all providers should be installed at runtime... @@ -83,10 +83,7 @@ public abstract class ImageReaderAbstractTestCase { try { return getReaderClass().newInstance(); } - catch (InstantiationException e) { - throw new RuntimeException(e); - } - catch (IllegalAccessException e) { + catch (InstantiationException | IllegalAccessException e) { throw new RuntimeException(e); } } @@ -1002,7 +999,7 @@ public abstract class ImageReaderAbstractTestCase { /*aspectRatio =*/ reader.getAspectRatio(-1); //assertEquals("Wrong aspect ratio", data.getDimension().width / (float) data.getDimension().height, aspectRatio, 0f); } - catch (IndexOutOfBoundsException expected) { + catch (IndexOutOfBoundsException ignore) { } catch (IOException e) { fail("Could not read image aspect ratio" + e); @@ -1391,7 +1388,7 @@ public abstract class ImageReaderAbstractTestCase { reader.read(0, param); fail("Expected to throw exception with illegal type specifier"); } - catch (IIOException expected) { + catch (IIOException | IllegalArgumentException expected) { // TODO: This is thrown by ImageReader.getDestination. But are we happy with that? String message = expected.getMessage().toLowerCase(); if (!(message.contains("destination") && message.contains("type"))) { @@ -1399,23 +1396,16 @@ public abstract class ImageReaderAbstractTestCase { throw expected; } } - catch (IllegalArgumentException expected) { - String message = expected.getMessage().toLowerCase(); - if (!(message.contains("destination") && message.contains("type"))) { - // Allow this to bubble up, du to a bug in the Sun PNGImageReader - throw expected; - } - } } } private List createIllegalTypes(Iterator pValidTypes) { - List allTypes = new ArrayList(); + List allTypes = new ArrayList<>(); for (int i = BufferedImage.TYPE_INT_RGB; i < BufferedImage.TYPE_BYTE_INDEXED; i++) { allTypes.add(ImageTypeSpecifier.createFromBufferedImageType(i)); } - List illegalTypes = new ArrayList(allTypes); + List illegalTypes = new ArrayList<>(allTypes); while (pValidTypes.hasNext()) { ImageTypeSpecifier valid = pValidTypes.next(); boolean removed = illegalTypes.remove(valid); @@ -1454,6 +1444,7 @@ public abstract class ImageReaderAbstractTestCase { assertEquals(reader.getHeight(0) + point.y, image.getHeight()); } + @SuppressWarnings("ConstantConditions") @Test public void testSetDestinationOffsetNull() throws IOException { final ImageReader reader = createReader(); @@ -1629,12 +1620,12 @@ public abstract class ImageReaderAbstractTestCase { throw new IllegalArgumentException("input == null"); } - sizes = new ArrayList(); - images = new ArrayList(); + sizes = new ArrayList<>(); + images = new ArrayList<>(); List sizes = pSizes; if (sizes == null) { - sizes = new ArrayList(); + sizes = new ArrayList<>(); if (pImages != null) { for (BufferedImage image : pImages) { sizes.add(new Dimension(image.getWidth(), image.getHeight())); @@ -1690,6 +1681,7 @@ public abstract class ImageReaderAbstractTestCase { return sizes.get(pIndex); } + @SuppressWarnings("unused") public BufferedImage getImage(final int pIndex) { return images.get(pIndex); } diff --git a/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/ImageTypeSpecifiersTest.java b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/ImageTypeSpecifiersTest.java index 88451917..9a286bbf 100644 --- a/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/ImageTypeSpecifiersTest.java +++ b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/ImageTypeSpecifiersTest.java @@ -1,12 +1,11 @@ package com.twelvemonkeys.imageio.util; +import com.twelvemonkeys.lang.Validate; import org.junit.Test; import javax.imageio.ImageTypeSpecifier; import java.awt.color.ColorSpace; -import java.awt.image.BufferedImage; -import java.awt.image.DataBuffer; -import java.awt.image.IndexColorModel; +import java.awt.image.*; import static org.junit.Assert.assertEquals; @@ -40,7 +39,7 @@ public class ImageTypeSpecifiersTest { } @Test - public void testCreatePacked() { + public void testCreatePacked32() { // TYPE_INT_RGB assertEquals( ImageTypeSpecifier.createPacked(sRGB, DCM_RED_MASK, DCM_GREEN_MASK, DCM_BLUE_MASK, 0, DataBuffer.TYPE_INT, false), @@ -61,31 +60,70 @@ public class ImageTypeSpecifiersTest { ImageTypeSpecifier.createPacked(sRGB, DCM_BGR_RED_MASK, DCM_BGR_GRN_MASK, DCM_BGR_BLU_MASK, 0, DataBuffer.TYPE_INT, false), ImageTypeSpecifiers.createPacked(sRGB, DCM_BGR_RED_MASK, DCM_BGR_GRN_MASK, DCM_BGR_BLU_MASK, 0, DataBuffer.TYPE_INT, false) ); + } + + @Test + public void testCreatePacked16() { // TYPE_USHORT_555_RGB assertEquals( - ImageTypeSpecifier.createPacked(sRGB, DCM_555_RED_MASK, DCM_555_GRN_MASK, DCM_555_BLU_MASK, 0, DataBuffer.TYPE_USHORT, false), + createPacked(sRGB, DCM_555_RED_MASK, DCM_555_GRN_MASK, DCM_555_BLU_MASK, 0, DataBuffer.TYPE_USHORT, false), ImageTypeSpecifiers.createPacked(sRGB, DCM_555_RED_MASK, DCM_555_GRN_MASK, DCM_555_BLU_MASK, 0, DataBuffer.TYPE_USHORT, false) ); - // "SHORT 555 RGB" (impossible for some reason) -// assertEquals( -// ImageTypeSpecifier.createPacked(sRGB, DCM_555_RED_MASK, DCM_555_GRN_MASK, DCM_555_BLU_MASK, 0, DataBuffer.TYPE_SHORT, false), -// ImageTypeSpecifiers.createPacked(sRGB, DCM_555_RED_MASK, DCM_555_GRN_MASK, DCM_555_BLU_MASK, 0, DataBuffer.TYPE_SHORT, false) -// ); + // "SHORT 555 RGB" (impossible, only BYTE, USHORT, INT supported) + // TYPE_USHORT_565_RGB assertEquals( - ImageTypeSpecifier.createPacked(sRGB, DCM_565_RED_MASK, DCM_565_GRN_MASK, DCM_565_BLU_MASK, 0, DataBuffer.TYPE_USHORT, false), + createPacked(sRGB, DCM_565_RED_MASK, DCM_565_GRN_MASK, DCM_565_BLU_MASK, 0, DataBuffer.TYPE_USHORT, false), ImageTypeSpecifiers.createPacked(sRGB, DCM_565_RED_MASK, DCM_565_GRN_MASK, DCM_565_BLU_MASK, 0, DataBuffer.TYPE_USHORT, false) ); // "USHORT 4444 ARGB" assertEquals( - ImageTypeSpecifier.createPacked(sRGB, 0xf00, 0xf0, 0xf, 0xf000, DataBuffer.TYPE_USHORT, false), + createPacked(sRGB, 0xf00, 0xf0, 0xf, 0xf000, DataBuffer.TYPE_USHORT, false), ImageTypeSpecifiers.createPacked(sRGB, 0xf00, 0xf0, 0xf, 0xf000, DataBuffer.TYPE_USHORT, false) ); // "USHORT 4444 ARGB PRE" assertEquals( - ImageTypeSpecifier.createPacked(sRGB, 0xf00, 0xf0, 0xf, 0xf000, DataBuffer.TYPE_USHORT, true), + createPacked(sRGB, 0xf00, 0xf0, 0xf, 0xf000, DataBuffer.TYPE_USHORT, true), ImageTypeSpecifiers.createPacked(sRGB, 0xf00, 0xf0, 0xf, 0xf000, DataBuffer.TYPE_USHORT, true) ); + + // Extra: Make sure color models bits is actually 16 (ImageTypeSpecifier equivalent returns 32) + assertEquals(16, ImageTypeSpecifiers.createPacked(sRGB, DCM_565_RED_MASK, DCM_565_GRN_MASK, DCM_565_BLU_MASK, 0, DataBuffer.TYPE_USHORT, false).getColorModel().getPixelSize()); + } + + @Test + public void testCreatePacked8() { + // "BYTE 332 RGB" + assertEquals( + createPacked(sRGB, 0xe0, 0x1c, 0x03, 0x0, DataBuffer.TYPE_BYTE, false), + ImageTypeSpecifiers.createPacked(sRGB, 0xe0, 0x1c, 0x3, 0x0, DataBuffer.TYPE_BYTE, false) + ); + // "BYTE 2222 ARGB" + assertEquals( + createPacked(sRGB, 0xc0, 0x30, 0x0c, 0x03, DataBuffer.TYPE_BYTE, false), + ImageTypeSpecifiers.createPacked(sRGB, 0xc0, 0x30, 0x0c, 0x03, DataBuffer.TYPE_BYTE, false) + ); + // "BYTE 2222 ARGB PRE" + assertEquals( + createPacked(sRGB, 0xc0, 0x30, 0x0c, 0x03, DataBuffer.TYPE_BYTE, true), + ImageTypeSpecifiers.createPacked(sRGB, 0xc0, 0x30, 0x0c, 0x03, DataBuffer.TYPE_BYTE, true) + ); + + // Extra: Make sure color models bits is actually 8 (ImageTypeSpecifiers equivalent returns 32) + assertEquals(8, ImageTypeSpecifiers.createPacked(sRGB, 0xc0, 0x30, 0x0c, 0x03, DataBuffer.TYPE_BYTE, false).getColorModel().getPixelSize()); + } + + private ImageTypeSpecifier createPacked(final ColorSpace colorSpace, + final int redMask, final int greenMask, final int blueMask, final int alphaMask, + final int transferType, final boolean isAlphaPremultiplied) { + Validate.isTrue(transferType == DataBuffer.TYPE_BYTE || transferType == DataBuffer.TYPE_USHORT, transferType, "transferType: %s"); + + int bits = transferType == DataBuffer.TYPE_BYTE ? 8 : 16; + + ColorModel colorModel = + new DirectColorModel(colorSpace, bits, redMask, greenMask, blueMask, alphaMask, isAlphaPremultiplied, transferType); + + return new ImageTypeSpecifier(colorModel, colorModel.createCompatibleSampleModel(1, 1)); } @Test @@ -337,11 +375,7 @@ public class ImageTypeSpecifiersTest { ); assertEquals( - ImageTypeSpecifier.createGrayscale(16, DataBuffer.TYPE_SHORT, false), // NOTE: Unsigned TYPE_SHORT makes no sense... - ImageTypeSpecifiers.createGrayscale(16, DataBuffer.TYPE_SHORT) - ); - assertEquals( - ImageTypeSpecifier.createGrayscale(16, DataBuffer.TYPE_SHORT, true), + new Int16ImageTypeSpecifier(ColorSpace.getInstance(ColorSpace.CS_GRAY), new int[] {0}, false, false), ImageTypeSpecifiers.createGrayscale(16, DataBuffer.TYPE_SHORT) ); } @@ -400,19 +434,11 @@ public class ImageTypeSpecifiersTest { ); assertEquals( - ImageTypeSpecifier.createGrayscale(16, DataBuffer.TYPE_SHORT, false, false), + new Int16ImageTypeSpecifier(ColorSpace.getInstance(ColorSpace.CS_GRAY), new int[] {0, 1}, true, false), ImageTypeSpecifiers.createGrayscale(16, DataBuffer.TYPE_SHORT, false) ); assertEquals( - ImageTypeSpecifier.createGrayscale(16, DataBuffer.TYPE_SHORT, false, true), - ImageTypeSpecifiers.createGrayscale(16, DataBuffer.TYPE_SHORT, true) - ); - assertEquals( - ImageTypeSpecifier.createGrayscale(16, DataBuffer.TYPE_SHORT, true, false), - ImageTypeSpecifiers.createGrayscale(16, DataBuffer.TYPE_SHORT, false) - ); - assertEquals( - ImageTypeSpecifier.createGrayscale(16, DataBuffer.TYPE_SHORT, true, true), + new Int16ImageTypeSpecifier(ColorSpace.getInstance(ColorSpace.CS_GRAY), new int[] {0, 1}, true, true), ImageTypeSpecifiers.createGrayscale(16, DataBuffer.TYPE_SHORT, true) ); } @@ -437,6 +463,30 @@ public class ImageTypeSpecifiersTest { ); } + @Test + public void testCreatePackedGrayscale1() { + assertEquals( + ImageTypeSpecifier.createGrayscale(1, DataBuffer.TYPE_BYTE, false), + ImageTypeSpecifiers.createPackedGrayscale(GRAY, 1, DataBuffer.TYPE_BYTE) + ); + } + + @Test + public void testCreatePackedGrayscale2() { + assertEquals( + ImageTypeSpecifier.createGrayscale(2, DataBuffer.TYPE_BYTE, false), + ImageTypeSpecifiers.createPackedGrayscale(GRAY, 2, DataBuffer.TYPE_BYTE) + ); + } + + @Test + public void testCreatePackedGrayscale4() { + assertEquals( + ImageTypeSpecifier.createGrayscale(4, DataBuffer.TYPE_BYTE, false), + ImageTypeSpecifiers.createPackedGrayscale(GRAY, 4, DataBuffer.TYPE_BYTE) + ); + } + @Test public void testCreateIndexedByteArrays1to8() { for (int bits = 1; bits <= 8; bits <<= 1) { @@ -562,7 +612,6 @@ public class ImageTypeSpecifiersTest { } - private static byte[] createByteLut(final int count) { byte[] lut = new byte[count]; for (int i = 0; i < count; i++) { diff --git a/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/ImageWriterAbstractTestCase.java b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/ImageWriterAbstractTestCase.java index 67ecd60f..15f9a31d 100755 --- a/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/ImageWriterAbstractTestCase.java +++ b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/ImageWriterAbstractTestCase.java @@ -63,6 +63,7 @@ public abstract class ImageWriterAbstractTestCase { static { IIORegistry.getDefaultInstance().registerServiceProvider(new URLImageInputStreamSpi()); + ImageIO.setUseCache(false); } protected abstract ImageWriter createImageWriter(); @@ -120,23 +121,20 @@ public abstract class ImageWriterAbstractTestCase { for (RenderedImage testData : getTestData()) { ByteArrayOutputStream buffer = new ByteArrayOutputStream(); - ImageOutputStream stream = ImageIO.createImageOutputStream(buffer); - writer.setOutput(stream); - try { + try (ImageOutputStream stream = ImageIO.createImageOutputStream(buffer)) { + writer.setOutput(stream); writer.write(drawSomething((BufferedImage) testData)); } catch (IOException e) { fail(e.getMessage()); } - finally { - stream.close(); // Force data to be written - } assertTrue("No image data written", buffer.size() > 0); } } + @SuppressWarnings("ConstantConditions") @Test public void testWriteNull() throws IOException { ImageWriter writer = createImageWriter(); diff --git a/imageio/imageio-core/todo.txt b/imageio/imageio-core/todo.txt index 4e48a7fa..fd469c86 100755 --- a/imageio/imageio-core/todo.txt +++ b/imageio/imageio-core/todo.txt @@ -1,8 +1,7 @@ - Rename to imageio-common? -- Separate modules for more for more plugins - - The BMP reader supports some special formats not supported by Sun reader - - PNM package is pretty complete, but useless, as it's provided by Sun? License? - - WBMP? - - XBM? +- Add stream support for NIO and NIO2 stuff? Path, ByteChannel +- Faster RAF support (need to buffer the slow readShort/readInt/readLong/readUnsignedShort/readUnsignedInt) +- See if it's possible to use FileImageInputStream for FileChannels and FileInputStream to avoid the caching +- And the above for output streams too... DONE: - Split up into separate plugins (modules), to allow easier configuration \ No newline at end of file diff --git a/imageio/imageio-hdr/pom.xml b/imageio/imageio-hdr/pom.xml new file mode 100644 index 00000000..20efae92 --- /dev/null +++ b/imageio/imageio-hdr/pom.xml @@ -0,0 +1,30 @@ + + + 4.0.0 + + com.twelvemonkeys.imageio + imageio + 3.3-SNAPSHOT + + imageio-hdr + TwelveMonkeys :: ImageIO :: HDR plugin + + ImageIO plugin for Radiance RGBE High Dynaimc Range format (HDR). + + + + + com.twelvemonkeys.imageio + imageio-core + + + com.twelvemonkeys.imageio + imageio-core + test-jar + + + com.twelvemonkeys.imageio + imageio-metadata + + + diff --git a/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/HDR.java b/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/HDR.java new file mode 100644 index 00000000..170b8da0 --- /dev/null +++ b/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/HDR.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2015, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.imageio.plugins.hdr; + +/** + * HDR. + * + * @author Harald Kuhr + * @author last modified by $Author: harald.kuhr$ + * @version $Id: HDR.java,v 1.0 27/07/15 harald.kuhr Exp$ + */ +interface HDR { + byte[] RADIANCE_MAGIC = new byte[] {'#', '?', 'R', 'A', 'D', 'I', 'A', 'N', 'C', 'E'}; + byte[] RGBE_MAGIC = new byte[] {'#', '?', 'R', 'G', 'B', 'E'}; +} diff --git a/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/HDRHeader.java b/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/HDRHeader.java new file mode 100644 index 00000000..7ce3ffa3 --- /dev/null +++ b/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/HDRHeader.java @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2015, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.imageio.plugins.hdr; + +import javax.imageio.IIOException; +import javax.imageio.stream.ImageInputStream; +import java.io.IOException; + +/** + * HDRHeader. + * + * @author Harald Kuhr + * @author last modified by $Author: harald.kuhr$ + * @version $Id: HDRHeader.java,v 1.0 27/07/15 harald.kuhr Exp$ + */ +final class HDRHeader { + private static final String KEY_FORMAT = "FORMAT="; + private static final String KEY_PRIMARIES = "PRIMARIES="; + private static final String KEY_EXPOSURE = "EXPOSURE="; + private static final String KEY_GAMMA = "GAMMA="; + private static final String KEY_SOFTWARE = "SOFTWARE="; + + private int width; + private int height; + + private String software; + + public static HDRHeader read(final ImageInputStream stream) throws IOException { + HDRHeader header = new HDRHeader(); + + while (true) { + String line = stream.readLine().trim(); + + if (line.isEmpty()) { + // This is the last line before the dimensions + break; + } + + if (line.startsWith("#?")) { + // Program specifier, don't need that... + } + else if (line.startsWith("#")) { + // Comment (ignore) + } + else if (line.startsWith(KEY_FORMAT)) { + String format = line.substring(KEY_FORMAT.length()).trim(); + + if (!format.equals("32-bit_rle_rgbe")) { + throw new IIOException("Unsupported format \"" + format + "\"(expected \"32-bit_rle_rgbe\")"); + } + // TODO: Support the 32-bit_rle_xyze format + } + else if (line.startsWith(KEY_PRIMARIES)) { + // TODO: We are going to need these values... + // Should contain 8 (RGB + white point) coordinates + } + else if (line.startsWith(KEY_EXPOSURE)) { + // TODO: We are going to need these values... + } + else if (line.startsWith(KEY_GAMMA)) { + // TODO: We are going to need these values... + } + else if (line.startsWith(KEY_SOFTWARE)) { + header.software = line.substring(KEY_SOFTWARE.length()).trim(); + } + else { + // ...ignore + } + } + + // TODO: Proper parsing of width/height and orientation! + String dimensionsLine = stream.readLine().trim(); + String[] dims = dimensionsLine.split("\\s"); + + if (dims[0].equals("-Y") && dims[2].equals("+X")) { + header.height = Integer.parseInt(dims[1]); + header.width = Integer.parseInt(dims[3]); + + return header; + } + else { + throw new IIOException("Unsupported RGBE orientation (expected \"-Y ... +X ...\")"); + } + } + + public int getWidth() { + return width; + } + + public int getHeight() { + return height; + } + + public String getSoftware() { + return software; + } +} diff --git a/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/HDRImageReadParam.java b/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/HDRImageReadParam.java new file mode 100644 index 00000000..32490da5 --- /dev/null +++ b/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/HDRImageReadParam.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2015, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.imageio.plugins.hdr; + +import com.twelvemonkeys.imageio.plugins.hdr.tonemap.DefaultToneMapper; +import com.twelvemonkeys.imageio.plugins.hdr.tonemap.ToneMapper; + +import javax.imageio.ImageReadParam; + +/** + * HDRImageReadParam. + * + * @author Harald Kuhr + * @author last modified by $Author: harald.kuhr$ + * @version $Id: HDRImageReadParam.java,v 1.0 28/07/15 harald.kuhr Exp$ + */ +public final class HDRImageReadParam extends ImageReadParam { + static final ToneMapper DEFAULT_TONE_MAPPER = new DefaultToneMapper(.1f); + + private ToneMapper toneMapper = DEFAULT_TONE_MAPPER; + + public ToneMapper getToneMapper() { + return toneMapper; + } + + public void setToneMapper(final ToneMapper toneMapper) { + this.toneMapper = toneMapper != null ? toneMapper : DEFAULT_TONE_MAPPER; + } +} diff --git a/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/HDRImageReader.java b/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/HDRImageReader.java new file mode 100644 index 00000000..e24fb5fc --- /dev/null +++ b/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/HDRImageReader.java @@ -0,0 +1,258 @@ +/* + * Copyright (c) 2015, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.imageio.plugins.hdr; + +import com.twelvemonkeys.imageio.ImageReaderBase; +import com.twelvemonkeys.imageio.plugins.hdr.tonemap.ToneMapper; +import com.twelvemonkeys.imageio.util.ImageTypeSpecifiers; + +import javax.imageio.ImageIO; +import javax.imageio.ImageReadParam; +import javax.imageio.ImageTypeSpecifier; +import javax.imageio.metadata.IIOMetadata; +import javax.imageio.spi.ImageReaderSpi; +import java.awt.*; +import java.awt.color.ColorSpace; +import java.awt.image.BufferedImage; +import java.awt.image.DataBuffer; +import java.awt.image.Raster; +import java.awt.image.WritableRaster; +import java.io.File; +import java.io.IOException; +import java.util.Collections; +import java.util.Iterator; + +/** + * HDRImageReader. + * + * @author Harald Kuhr + * @author last modified by $Author: harald.kuhr$ + * @version $Id: HDRImageReader.java,v 1.0 27/07/15 harald.kuhr Exp$ + */ +public final class HDRImageReader extends ImageReaderBase { + // Specs: http://radsite.lbl.gov/radiance/refer/filefmts.pdf + + private HDRHeader header; + + protected HDRImageReader(final ImageReaderSpi provider) { + super(provider); + } + + @Override + protected void resetMembers() { + header = null; + } + + private void readHeader() throws IOException { + if (header == null) { + header = HDRHeader.read(imageInput); + + imageInput.flushBefore(imageInput.getStreamPosition()); + } + + imageInput.seek(imageInput.getFlushedPosition()); + } + + @Override + public int getWidth(int imageIndex) throws IOException { + checkBounds(imageIndex); + readHeader(); + + return header.getWidth(); + } + + @Override + public int getHeight(int imageIndex) throws IOException { + checkBounds(imageIndex); + readHeader(); + + return header.getHeight(); + } + + @Override + public Iterator getImageTypes(int imageIndex) throws IOException { + checkBounds(imageIndex); + readHeader(); + + ColorSpace sRGB = ColorSpace.getInstance(ColorSpace.CS_sRGB); + return Collections.singletonList(ImageTypeSpecifiers.createInterleaved(sRGB, new int[] {0, 1, 2}, DataBuffer.TYPE_FLOAT, false, false)).iterator(); + } + + @Override + public BufferedImage read(final int imageIndex, final ImageReadParam param) throws IOException { + checkBounds(imageIndex); + readHeader(); + + int width = getWidth(imageIndex); + int height = getHeight(imageIndex); + + BufferedImage destination = getDestination(param, getImageTypes(imageIndex), width, height); + + Rectangle srcRegion = new Rectangle(); + Rectangle destRegion = new Rectangle(); + computeRegions(param, width, height, destination, srcRegion, destRegion); + + WritableRaster raster = destination.getRaster() + .createWritableChild(destRegion.x, destRegion.y, destRegion.width, destRegion.height, 0, 0, null); + + int xSub = param != null ? param.getSourceXSubsampling() : 1; + int ySub = param != null ? param.getSourceYSubsampling() : 1; + + // Allow pluggable tone mapper via ImageReadParam + ToneMapper toneMapper = param instanceof HDRImageReadParam + ? ((HDRImageReadParam) param).getToneMapper() + : HDRImageReadParam.DEFAULT_TONE_MAPPER; + + byte[] rowRGBE = new byte[width * 4]; + float[] rgb = new float[3]; + + processImageStarted(imageIndex); + + // Process one scanline of RGBE data at a time + for (int srcY = 0; srcY < height; srcY++) { + int dstY = ((srcY - srcRegion.y) / ySub) + destRegion.y; + if (dstY >= destRegion.height) { + break; + } + + RGBE.readPixelsRawRLE(imageInput, rowRGBE, 0, width, 1); + + if (srcY % ySub == 0 && dstY >= destRegion.y) { + for (int srcX = srcRegion.x; srcX < srcRegion.x + srcRegion.width; srcX += xSub) { + int dstX = ((srcX - srcRegion.x) / xSub) + destRegion.x; + if (dstX >= destRegion.width) { + break; + } + + RGBE.rgbe2float(rgb, rowRGBE, srcX * 4); + + // Map/clamp RGB values into visible range, normally [0...1] + toneMapper.map(rgb); + + raster.setDataElements(dstX, dstY, rgb); + } + } + + processImageProgress(srcY * 100f / height); + + if (abortRequested()) { + processReadAborted(); + break; + } + } + + processImageComplete(); + + return destination; + } + + @Override + public boolean canReadRaster() { + return true; + } + + @Override + public Raster readRaster(final int imageIndex, final ImageReadParam param) throws IOException { + checkBounds(imageIndex); + readHeader(); + + int width = getWidth(imageIndex); + int height = getHeight(imageIndex); + + Rectangle srcRegion = new Rectangle(); + Rectangle destRegion = new Rectangle(); + computeRegions(param, width, height, null, srcRegion, destRegion); + destRegion = srcRegion; // We don't really care about destination for raster + + BufferedImage destination = new BufferedImage(srcRegion.width, srcRegion.height, BufferedImage.TYPE_4BYTE_ABGR); + WritableRaster raster = destination.getRaster(); + + int xSub = param != null ? param.getSourceXSubsampling() : 1; + int ySub = param != null ? param.getSourceYSubsampling() : 1; + + byte[] rowRGBE = new byte[width * 4]; + byte[] pixelRGBE = new byte[width]; + + processImageStarted(imageIndex); + + // Process one scanline of RGBE data at a time + for (int srcY = 0; srcY < height; srcY++) { + int dstY = ((srcY - srcRegion.y) / ySub) + destRegion.y; + if (dstY >= destRegion.height) { + break; + } + + RGBE.readPixelsRawRLE(imageInput, rowRGBE, 0, width, 1); + + if (srcY % ySub == 0 && dstY >= destRegion.y) { + for (int srcX = srcRegion.x; srcX < srcRegion.x + srcRegion.width; srcX += xSub) { + int dstX = ((srcX - srcRegion.x) / xSub) + destRegion.x; + if (dstX >= destRegion.width) { + break; + } + + System.arraycopy(rowRGBE, srcX * 4, pixelRGBE, 0, 4); + raster.setDataElements(dstX, dstY, pixelRGBE); + } + } + + processImageProgress(srcY * 100f / height); + + if (abortRequested()) { + processReadAborted(); + break; + } + } + + processImageComplete(); + + return destination.getRaster(); + } + + @Override + public ImageReadParam getDefaultReadParam() { + return new HDRImageReadParam(); + } + + @Override + public IIOMetadata getImageMetadata(int imageIndex) throws IOException { + checkBounds(imageIndex); + readHeader(); + + return new HDRMetadata(header); + } + + public static void main(final String[] args) throws IOException { + File file = new File(args[0]); + + BufferedImage image = ImageIO.read(file); + + showIt(image, file.getName()); + } +} diff --git a/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/HDRImageReaderSpi.java b/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/HDRImageReaderSpi.java new file mode 100644 index 00000000..380313be --- /dev/null +++ b/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/HDRImageReaderSpi.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2015, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.imageio.plugins.hdr; + +import com.twelvemonkeys.imageio.spi.ImageReaderSpiBase; + +import javax.imageio.ImageReader; +import javax.imageio.stream.ImageInputStream; +import java.io.IOException; +import java.util.Arrays; +import java.util.Locale; + +/** + * HDRImageReaderSpi. + * + * @author Harald Kuhr + * @author last modified by $Author: harald.kuhr$ + * @version $Id: HDRImageReaderSpi.java,v 1.0 27/07/15 harald.kuhr Exp$ + */ +public final class HDRImageReaderSpi extends ImageReaderSpiBase { + public HDRImageReaderSpi() { + super(new HDRProviderInfo()); + } + + @Override + public boolean canDecodeInput(final Object source) throws IOException { + if (!(source instanceof ImageInputStream)) { + return false; + } + + ImageInputStream stream = (ImageInputStream) source; + + stream.mark(); + + try { + // NOTE: All images I have found starts with #?RADIANCE (or has no #? line at all), + // although some sources claim that #?RGBE is also used. + byte[] magic = new byte[HDR.RADIANCE_MAGIC.length]; + stream.readFully(magic); + + return Arrays.equals(HDR.RADIANCE_MAGIC, magic) + || Arrays.equals(HDR.RGBE_MAGIC, Arrays.copyOf(magic, 6)); + } + finally { + stream.reset(); + } + } + + @Override + public ImageReader createReaderInstance(Object extension) throws IOException { + return new HDRImageReader(this); + } + + @Override + public String getDescription(final Locale locale) { + return "Radiance RGBE High Dynaimc Range (HDR) image reader"; + } +} diff --git a/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/HDRMetadata.java b/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/HDRMetadata.java new file mode 100755 index 00000000..cd5422eb --- /dev/null +++ b/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/HDRMetadata.java @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2015, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.imageio.plugins.hdr; + +import com.twelvemonkeys.imageio.AbstractMetadata; + +import javax.imageio.metadata.IIOMetadataNode; + +final class HDRMetadata extends AbstractMetadata { + private final HDRHeader header; + + HDRMetadata(final HDRHeader header) { + this.header = header; + } + + @Override + protected IIOMetadataNode getStandardChromaNode() { + IIOMetadataNode chroma = new IIOMetadataNode("Chroma"); + + IIOMetadataNode csType = new IIOMetadataNode("ColorSpaceType"); + chroma.appendChild(csType); + csType.setAttribute("name", "RGB"); + // TODO: Support XYZ + + IIOMetadataNode numChannels = new IIOMetadataNode("NumChannels"); + numChannels.setAttribute("value", "3"); + chroma.appendChild(numChannels); + + IIOMetadataNode blackIsZero = new IIOMetadataNode("BlackIsZero"); + blackIsZero.setAttribute("value", "TRUE"); + chroma.appendChild(blackIsZero); + + return chroma; + } + + // No compression + + @Override + protected IIOMetadataNode getStandardCompressionNode() { + IIOMetadataNode node = new IIOMetadataNode("Compression"); + + IIOMetadataNode compressionTypeName = new IIOMetadataNode("CompressionTypeName"); + compressionTypeName.setAttribute("value", "RLE"); + node.appendChild(compressionTypeName); + + IIOMetadataNode lossless = new IIOMetadataNode("Lossless"); + lossless.setAttribute("value", "TRUE"); + node.appendChild(lossless); + + return node; + } + + @Override + protected IIOMetadataNode getStandardDataNode() { + IIOMetadataNode node = new IIOMetadataNode("Data"); + + IIOMetadataNode sampleFormat = new IIOMetadataNode("SampleFormat"); + sampleFormat.setAttribute("value", "UnsignedIntegral"); + node.appendChild(sampleFormat); + + IIOMetadataNode bitsPerSample = new IIOMetadataNode("BitsPerSample"); + bitsPerSample.setAttribute("value", "8 8 8 8"); + node.appendChild(bitsPerSample); + + return node; + } + + @Override + protected IIOMetadataNode getStandardDimensionNode() { + IIOMetadataNode dimension = new IIOMetadataNode("Dimension"); + + // TODO: Support other orientations + IIOMetadataNode imageOrientation = new IIOMetadataNode("ImageOrientation"); + imageOrientation.setAttribute("value", "Normal"); + dimension.appendChild(imageOrientation); + + return dimension; + } + + // No document node + + @Override + protected IIOMetadataNode getStandardTextNode() { + if (header.getSoftware() != null) { + IIOMetadataNode text = new IIOMetadataNode("Text"); + + IIOMetadataNode textEntry = new IIOMetadataNode("TextEntry"); + textEntry.setAttribute("keyword", "Software"); + textEntry.setAttribute("value", header.getSoftware()); + text.appendChild(textEntry); + + return text; + } + + return null; + } + + // No tiling + + // No transparency +} diff --git a/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/HDRProviderInfo.java b/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/HDRProviderInfo.java new file mode 100644 index 00000000..a11a215f --- /dev/null +++ b/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/HDRProviderInfo.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2015, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.imageio.plugins.hdr; + +import com.twelvemonkeys.imageio.spi.ReaderWriterProviderInfo; + +/** + * HDRProviderInfo. + * + * @author Harald Kuhr + * @author last modified by $Author: harald.kuhr$ + * @version $Id: HDRProviderInfo.java,v 1.0 27/07/15 harald.kuhr Exp$ + */ +final class HDRProviderInfo extends ReaderWriterProviderInfo { + protected HDRProviderInfo() { + super( + HDRProviderInfo.class, + new String[] {"HDR", "hdr", "RGBE", "rgbe"}, + new String[] {"hdr", "rgbe", "xyze", "pic"}, + new String[] {"image/vnd.radiance"}, + "com.twelvemonkeys.imageio.plugins.hdr.HDRImageReader", + new String[]{"com.twelvemonkeys.imageio.plugins.hdr.HDRImageReaderSpi"}, + null, + null, + false, null, null, null, null, + true, null, null, null, null + ); + } +} diff --git a/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/RGBE.java b/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/RGBE.java new file mode 100644 index 00000000..b64607e1 --- /dev/null +++ b/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/RGBE.java @@ -0,0 +1,494 @@ +package com.twelvemonkeys.imageio.plugins.hdr; + +import java.io.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * This file contains code to read and write four byte rgbe file format + * developed by Greg Ward. It handles the conversions between rgbe and + * pixels consisting of floats. The data is assumed to be an array of floats. + * By default there are three floats per pixel in the order red, green, blue. + * (RGBE_DATA_??? values control this.) Only the mimimal header reading and + * writing is implemented. Each routine does error checking and will return + * a status value as defined below. This code is intended as a skeleton so + * feel free to modify it to suit your needs.

+ *

+ * Ported to Java and restructured by Kenneth Russell.
+ * posted to http://www.graphics.cornell.edu/~bjw/
+ * written by Bruce Walter (bjw@graphics.cornell.edu) 5/26/95
+ * based on code written by Greg Ward
+ *

+ * Source: https://java.net/projects/jogl-demos/sources/svn/content/trunk/src/demos/hdr/RGBE.java + */ +final class RGBE { + // Flags indicating which fields in a Header are valid + private static final int VALID_PROGRAMTYPE = 0x01; + private static final int VALID_GAMMA = 0x02; + private static final int VALID_EXPOSURE = 0x04; + + private static final String gammaString = "GAMMA="; + private static final String exposureString = "EXPOSURE="; + + private static final Pattern widthHeightPattern = Pattern.compile("-Y (\\d+) \\+X (\\d+)"); + + public static class Header { + // Indicates which fields are valid + private int valid; + + // Listed at beginning of file to identify it after "#?". + // Defaults to "RGBE" + private String programType; + + // Image has already been gamma corrected with given gamma. + // Defaults to 1.0 (no correction) + private float gamma; + + // A value of 1.0 in an image corresponds to + // watts/steradian/m^2. Defaults to 1.0. + private float exposure; + + // Width and height of image + private int width; + private int height; + + private Header(int valid, + String programType, + float gamma, + float exposure, + int width, + int height) { + this.valid = valid; + this.programType = programType; + this.gamma = gamma; + this.exposure = exposure; + this.width = width; + this.height = height; + } + + public boolean isProgramTypeValid() { + return ((valid & VALID_PROGRAMTYPE) != 0); + } + + public boolean isGammaValid() { + return ((valid & VALID_GAMMA) != 0); + } + + public boolean isExposureValid() { + return ((valid & VALID_EXPOSURE) != 0); + } + + public String getProgramType() { + return programType; + } + + public float getGamma() { + return gamma; + } + + public float getExposure() { + return exposure; + } + + public int getWidth() { + return width; + } + + public int getHeight() { + return height; + } + + public String toString() { + StringBuffer buf = new StringBuffer(); + if (isProgramTypeValid()) { + buf.append(" Program type: "); + buf.append(getProgramType()); + } + buf.append(" Gamma"); + if (isGammaValid()) { + buf.append(" [valid]"); + } + buf.append(": "); + buf.append(getGamma()); + buf.append(" Exposure"); + if (isExposureValid()) { + buf.append(" [valid]"); + } + buf.append(": "); + buf.append(getExposure()); + buf.append(" Width: "); + buf.append(getWidth()); + buf.append(" Height: "); + buf.append(getHeight()); + return buf.toString(); + } + } + + public static Header readHeader(final DataInput in) throws IOException { + int valid = 0; + String programType = null; + float gamma = 1.0f; + float exposure = 1.0f; + int width = 0; + int height = 0; + + String buf = in.readLine(); + if (buf == null) { + throw new IOException("Unexpected EOF reading magic token"); + } + if (buf.charAt(0) == '#' && buf.charAt(1) == '?') { + valid |= VALID_PROGRAMTYPE; + programType = buf.substring(2); + buf = in.readLine(); + if (buf == null) { + throw new IOException("Unexpected EOF reading line after magic token"); + } + } + + boolean foundFormat = false; + boolean done = false; + while (!done) { + if (buf.equals("FORMAT=32-bit_rle_rgbe")) { + foundFormat = true; + } + else if (buf.startsWith(gammaString)) { + valid |= VALID_GAMMA; + gamma = Float.parseFloat(buf.substring(gammaString.length())); + } + else if (buf.startsWith(exposureString)) { + valid |= VALID_EXPOSURE; + exposure = Float.parseFloat(buf.substring(exposureString.length())); + } + else { + Matcher m = widthHeightPattern.matcher(buf); + if (m.matches()) { + width = Integer.parseInt(m.group(2)); + height = Integer.parseInt(m.group(1)); + done = true; + } + } + + if (!done) { + buf = in.readLine(); + if (buf == null) { + throw new IOException("Unexpected EOF reading header"); + } + } + } + + if (!foundFormat) { + throw new IOException("No FORMAT specifier found"); + } + + return new Header(valid, programType, gamma, exposure, width, height); + } + + /** + * Simple read routine. Will not correctly handle run length encoding. + */ + public static void readPixels(DataInput in, float[] data, int numpixels) throws IOException { + byte[] rgbe = new byte[4]; + float[] rgb = new float[3]; + int offset = 0; + + while (numpixels-- > 0) { + in.readFully(rgbe); + + rgbe2float(rgb, rgbe, 0); + + data[offset++] = rgb[0]; + data[offset++] = rgb[1]; + data[offset++] = rgb[2]; + } + } + + public static void readPixelsRaw(DataInput in, byte[] data, int offset, int numpixels) throws IOException { + int numExpected = 4 * numpixels; + in.readFully(data, offset, numExpected); + } + + public static void readPixelsRawRLE(DataInput in, byte[] data, int offset, + int scanline_width, int num_scanlines) throws IOException { + byte[] rgbe = new byte[4]; + byte[] scanline_buffer = null; + int ptr, ptr_end; + int count; + byte[] buf = new byte[2]; + + if ((scanline_width < 8) || (scanline_width > 0x7fff)) { + // run length encoding is not allowed so read flat + readPixelsRaw(in, data, offset, scanline_width * num_scanlines); + } + + // read in each successive scanline + while (num_scanlines > 0) { + in.readFully(rgbe); + + if ((rgbe[0] != 2) || (rgbe[1] != 2) || ((rgbe[2] & 0x80) != 0)) { + // this file is not run length encoded + data[offset++] = rgbe[0]; + data[offset++] = rgbe[1]; + data[offset++] = rgbe[2]; + data[offset++] = rgbe[3]; + readPixelsRaw(in, data, offset, scanline_width * num_scanlines - 1); + } + + if ((((rgbe[2] & 0xFF) << 8) | (rgbe[3] & 0xFF)) != scanline_width) { + throw new IOException("Wrong scanline width " + + (((rgbe[2] & 0xFF) << 8) | (rgbe[3] & 0xFF)) + + ", expected " + scanline_width); + } + + if (scanline_buffer == null) { + scanline_buffer = new byte[4 * scanline_width]; + } + + ptr = 0; + // read each of the four channels for the scanline into the buffer + for (int i = 0; i < 4; i++) { + ptr_end = (i + 1) * scanline_width; + while (ptr < ptr_end) { + in.readFully(buf); + + if ((buf[0] & 0xFF) > 128) { + // a run of the same value + count = (buf[0] & 0xFF) - 128; + if ((count == 0) || (count > ptr_end - ptr)) { + throw new IOException("Bad scanline data"); + } + while (count-- > 0) { + scanline_buffer[ptr++] = buf[1]; + } + } + else { + // a non-run + count = buf[0] & 0xFF; + if ((count == 0) || (count > ptr_end - ptr)) { + throw new IOException("Bad scanline data"); + } + scanline_buffer[ptr++] = buf[1]; + if (--count > 0) { + in.readFully(scanline_buffer, ptr, count); + ptr += count; + } + } + } + } + // copy byte data to output + for (int i = 0; i < scanline_width; i++) { + data[offset++] = scanline_buffer[i]; + data[offset++] = scanline_buffer[i + scanline_width]; + data[offset++] = scanline_buffer[i + 2 * scanline_width]; + data[offset++] = scanline_buffer[i + 3 * scanline_width]; + } + num_scanlines--; + } + } + + /** + * Standard conversion from float pixels to rgbe pixels. + */ + public static void float2rgbe(byte[] rgbe, float red, float green, float blue) { + float v; + int e; + + v = red; + if (green > v) { + v = green; + } + if (blue > v) { + v = blue; + } + if (v < 1e-32f) { + rgbe[0] = rgbe[1] = rgbe[2] = rgbe[3] = 0; + } + else { + FracExp fe = frexp(v); + v = (float) (fe.getFraction() * 256.0 / v); + rgbe[0] = (byte) (red * v); + rgbe[1] = (byte) (green * v); + rgbe[2] = (byte) (blue * v); + rgbe[3] = (byte) (fe.getExponent() + 128); + } + } + + /** + * Standard conversion from rgbe to float pixels. Note: Ward uses + * ldexp(col+0.5,exp-(128+8)). However we wanted pixels in the + * range [0,1] to map back into the range [0,1]. + */ + public static void rgbe2float(float[] rgb, byte[] rgbe, int startRGBEOffset) { + float f; + + if (rgbe[startRGBEOffset + 3] != 0) { // nonzero pixel + f = (float) ldexp(1.0, (rgbe[startRGBEOffset + 3] & 0xFF) - (128 + 8)); + rgb[0] = (rgbe[startRGBEOffset + 0] & 0xFF) * f; + rgb[1] = (rgbe[startRGBEOffset + 1] & 0xFF) * f; + rgb[2] = (rgbe[startRGBEOffset + 2] & 0xFF) * f; + } + else { + rgb[0] = 0; + rgb[1] = 0; + rgb[2] = 0; + } + } + + public static double ldexp(double value, int exp) { + if (!finite(value) || value == 0.0) { + return value; + } + value = scalbn(value, exp); + // No good way to indicate errno (want to avoid throwing + // exceptions because don't know about stability of calculations) + // if(!finite(value)||value==0.0) errno = ERANGE; + return value; + } + + //---------------------------------------------------------------------- + // Internals only below this point + // + + //---------------------------------------------------------------------- + // Math routines, some fdlibm-derived + // + + static class FracExp { + private double fraction; + private int exponent; + + public FracExp(double fraction, int exponent) { + this.fraction = fraction; + this.exponent = exponent; + } + + public double getFraction() { + return fraction; + } + + public int getExponent() { + return exponent; + } + } + + private static final double two54 = 1.80143985094819840000e+16; // 43500000 00000000 + private static final double twom54 = 5.55111512312578270212e-17; // 0x3C900000 0x00000000 + private static final double huge = 1.0e+300; + private static final double tiny = 1.0e-300; + + private static int hi(double x) { + long bits = Double.doubleToRawLongBits(x); + return (int) (bits >>> 32); + } + + private static int lo(double x) { + long bits = Double.doubleToRawLongBits(x); + return (int) bits; + } + + private static double fromhilo(int hi, int lo) { + return Double.longBitsToDouble((((long) hi) << 32) | + (((long) lo) & 0xFFFFFFFFL)); + } + + private static FracExp frexp(double x) { + int hx = hi(x); + int ix = 0x7fffffff & hx; + int lx = lo(x); + int e = 0; + if (ix >= 0x7ff00000 || ((ix | lx) == 0)) { + return new FracExp(x, e); // 0,inf,nan + } + if (ix < 0x00100000) { // subnormal + x *= two54; + hx = hi(x); + ix = hx & 0x7fffffff; + e = -54; + } + e += (ix >> 20) - 1022; + hx = (hx & 0x800fffff) | 0x3fe00000; + lx = lo(x); + return new FracExp(fromhilo(hx, lx), e); + } + + private static boolean finite(double x) { + int hx; + hx = hi(x); + return (((hx & 0x7fffffff) - 0x7ff00000) >> 31) != 0; + } + + /** + * copysign(double x, double y)
+ * copysign(x,y) returns a value with the magnitude of x and + * with the sign bit of y. + */ + private static double copysign(double x, double y) { + return fromhilo((hi(x) & 0x7fffffff) | (hi(y) & 0x80000000), lo(x)); + } + + /** + * scalbn (double x, int n)
+ * scalbn(x,n) returns x* 2**n computed by exponent + * manipulation rather than by actually performing an + * exponentiation or a multiplication. + */ + private static double scalbn(double x, int n) { + int hx = hi(x); + int lx = lo(x); + int k = (hx & 0x7ff00000) >> 20; // extract exponent + if (k == 0) { // 0 or subnormal x + if ((lx | (hx & 0x7fffffff)) == 0) { + return x; // +-0 + } + x *= two54; + hx = hi(x); + k = ((hx & 0x7ff00000) >> 20) - 54; + if (n < -50000) { + return tiny * x; // underflow + } + } + if (k == 0x7ff) { + return x + x; // NaN or Inf + } + k = k + n; + if (k > 0x7fe) { + return huge * copysign(huge, x); // overflow + } + if (k > 0) { + // normal result + return fromhilo((hx & 0x800fffff) | (k << 20), lo(x)); + } + if (k <= -54) { + if (n > 50000) { + // in case integer overflow in n+k + return huge * copysign(huge, x); // overflow + } + else { + return tiny * copysign(tiny, x); // underflow + } + } + k += 54; // subnormal result + x = fromhilo((hx & 0x800fffff) | (k << 20), lo(x)); + return x * twom54; + } + + //---------------------------------------------------------------------- + // Test harness + // + + public static void main(String[] args) { + for (int i = 0; i < args.length; i++) { + try { + DataInputStream in = new DataInputStream(new BufferedInputStream(new FileInputStream(args[i]))); + Header header = RGBE.readHeader(in); + System.err.println("Header for file \"" + args[i] + "\":"); + System.err.println(" " + header); + byte[] data = new byte[header.getWidth() * header.getHeight() * 4]; + readPixelsRawRLE(in, data, 0, header.getWidth(), header.getHeight()); + in.close(); + } + catch (IOException e) { + e.printStackTrace(); + } + } + } +} diff --git a/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/tonemap/DefaultToneMapper.java b/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/tonemap/DefaultToneMapper.java new file mode 100644 index 00000000..1a211886 --- /dev/null +++ b/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/tonemap/DefaultToneMapper.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2015, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.imageio.plugins.hdr.tonemap; + +/** + * DefaultToneMapper. + *

+ * Normalizes values to range [0...1] using: + * + *

Vout = Vin / (Vin + C)

+ * + * Where C is constant. + * + * @author Harald Kuhr + * @author last modified by $Author: harald.kuhr$ + * @version $Id: DefaultToneMapper.java,v 1.0 28/07/15 harald.kuhr Exp$ + */ +public final class DefaultToneMapper implements ToneMapper { + + private final float constant; + + public DefaultToneMapper() { + this(1); + } + + public DefaultToneMapper(final float constant) { + this.constant = constant; + } + + @Override + public void map(final float[] rgb) { + // Default Vo = Vi / (Vi + 1) + for (int i = 0; i < rgb.length; i++) { + rgb[i] = rgb[i] / (rgb[i] + constant); + } + } +} diff --git a/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/tonemap/GammaToneMapper.java b/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/tonemap/GammaToneMapper.java new file mode 100644 index 00000000..285b6955 --- /dev/null +++ b/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/tonemap/GammaToneMapper.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2015, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.imageio.plugins.hdr.tonemap; + +/** + * GammaToneMapper. + *

+ * Normalizes values to range [0...1] using: + * + *

Vout = A Vin\u03b3

+ * + * Where A is constant and \u03b3 is the gamma. + * Values > 1 are clamped. + * + * @author Harald Kuhr + * @author last modified by $Author: harald.kuhr$ + * @version $Id: GammaToneMapper.java,v 1.0 28/07/15 harald.kuhr Exp$ + */ +public final class GammaToneMapper implements ToneMapper { + + private final float constant; + private final float gamma; + + public GammaToneMapper() { + this(0.5f, .25f); + } + + public GammaToneMapper(final float constant, final float gamma) { + this.constant = constant; + this.gamma = gamma; + } + + @Override + public void map(final float[] rgb) { + // Gamma Vo = A * Vi^y + for (int i = 0; i < rgb.length; i++) { + rgb[i] = Math.min(1f, (float) (constant * Math.pow(rgb[i], gamma))); + } + } +} diff --git a/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/tonemap/NullToneMapper.java b/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/tonemap/NullToneMapper.java new file mode 100644 index 00000000..33de6478 --- /dev/null +++ b/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/tonemap/NullToneMapper.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2015, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.imageio.plugins.hdr.tonemap; + +/** + * NullToneMapper. + *

+ * This {@code ToneMapper} does *not* normalize or clamp values + * to range [0...1], but leaves the values as-is. + * Useful for applications that implements custom tone mapping. + * + * @author Harald Kuhr + * @author last modified by $Author: harald.kuhr$ + * @version $Id: NullToneMapper.java,v 1.0 28/07/15 harald.kuhr Exp$ + */ +public final class NullToneMapper implements ToneMapper { + @Override + public void map(float[] rgb) { + } +} diff --git a/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/tonemap/ToneMapper.java b/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/tonemap/ToneMapper.java new file mode 100644 index 00000000..37d8f222 --- /dev/null +++ b/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/tonemap/ToneMapper.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2015, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.imageio.plugins.hdr.tonemap; + +/** + * ToneMapper. + * + * @author Harald Kuhr + * @author last modified by $Author: harald.kuhr$ + * @version $Id: ToneMapper.java,v 1.0 28/07/15 harald.kuhr Exp$ + */ +public interface ToneMapper { + void map(float[] rgb); +} diff --git a/imageio/imageio-hdr/src/main/resources/META-INF/services/javax.imageio.spi.ImageReaderSpi b/imageio/imageio-hdr/src/main/resources/META-INF/services/javax.imageio.spi.ImageReaderSpi new file mode 100755 index 00000000..7af7febe --- /dev/null +++ b/imageio/imageio-hdr/src/main/resources/META-INF/services/javax.imageio.spi.ImageReaderSpi @@ -0,0 +1 @@ +com.twelvemonkeys.imageio.plugins.hdr.HDRImageReaderSpi diff --git a/imageio/imageio-hdr/src/test/java/com/twelvemonkeys/imageio/plugins/hdr/HDRImageReaderTest.java b/imageio/imageio-hdr/src/test/java/com/twelvemonkeys/imageio/plugins/hdr/HDRImageReaderTest.java new file mode 100755 index 00000000..7baae6be --- /dev/null +++ b/imageio/imageio-hdr/src/test/java/com/twelvemonkeys/imageio/plugins/hdr/HDRImageReaderTest.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2015, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.imageio.plugins.hdr; + +import com.twelvemonkeys.imageio.util.ImageReaderAbstractTest; + +import javax.imageio.spi.ImageReaderSpi; +import java.awt.*; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * TGAImageReaderTest + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: TGAImageReaderTest.java,v 1.0 03.07.14 22:28 haraldk Exp$ + */ +public class HDRImageReaderTest extends ImageReaderAbstractTest { + @Override + protected List getTestData() { + return Arrays.asList( + new TestData(getClassLoaderResource("/hdr/memorial_o876.hdr"), new Dimension(512, 768)) + ); + } + + @Override + protected ImageReaderSpi createProvider() { + return new HDRImageReaderSpi(); + } + + @Override + protected Class getReaderClass() { + return HDRImageReader.class; + } + + @Override + protected HDRImageReader createReader() { + return new HDRImageReader(createProvider()); + } + + @Override + protected List getFormatNames() { + return Arrays.asList("HDR", "hdr", "RGBE", "rgbe"); + } + + @Override + protected List getSuffixes() { + return Arrays.asList("hdr", "rgbe", "xyze"); + } + + @Override + protected List getMIMETypes() { + return Collections.singletonList( + "image/vnd.radiance" + ); + } +} diff --git a/imageio/imageio-hdr/src/test/java/com/twelvemonkeys/imageio/plugins/hdr/HDRProviderInfoTest.java b/imageio/imageio-hdr/src/test/java/com/twelvemonkeys/imageio/plugins/hdr/HDRProviderInfoTest.java new file mode 100644 index 00000000..153f3b45 --- /dev/null +++ b/imageio/imageio-hdr/src/test/java/com/twelvemonkeys/imageio/plugins/hdr/HDRProviderInfoTest.java @@ -0,0 +1,19 @@ +package com.twelvemonkeys.imageio.plugins.hdr; + +import com.twelvemonkeys.imageio.spi.ReaderWriterProviderInfo; +import com.twelvemonkeys.imageio.spi.ReaderWriterProviderInfoTest; + +/** + * HDRProviderInfoTest. + * + * @author Harald Kuhr + * @author last modified by $Author: harald.kuhr$ + * @version $Id: HDRProviderInfoTest.java,v 1.0 02/06/16 harald.kuhr Exp$ + */ +public class HDRProviderInfoTest extends ReaderWriterProviderInfoTest { + + @Override + protected ReaderWriterProviderInfo createProviderInfo() { + return new HDRProviderInfo(); + } +} \ No newline at end of file diff --git a/imageio/imageio-hdr/src/test/resources/hdr/memorial_o876.hdr b/imageio/imageio-hdr/src/test/resources/hdr/memorial_o876.hdr new file mode 100644 index 00000000..c135ae99 Binary files /dev/null and b/imageio/imageio-hdr/src/test/resources/hdr/memorial_o876.hdr differ diff --git a/imageio/imageio-icns/pom.xml b/imageio/imageio-icns/pom.xml index 3389a889..37937b29 100644 --- a/imageio/imageio-icns/pom.xml +++ b/imageio/imageio-icns/pom.xml @@ -4,7 +4,7 @@ com.twelvemonkeys.imageio imageio - 3.1-SNAPSHOT + 3.3-SNAPSHOT imageio-icns TwelveMonkeys :: ImageIO :: ICNS plugin @@ -18,7 +18,7 @@ com.twelvemonkeys.imageio imageio-core - tests + test-jar diff --git a/imageio/imageio-icns/src/main/java/com/twelvemonkeys/imageio/plugins/icns/ICNSProviderInfo.java b/imageio/imageio-icns/src/main/java/com/twelvemonkeys/imageio/plugins/icns/ICNSProviderInfo.java index d72ca7bb..59cb4e9e 100644 --- a/imageio/imageio-icns/src/main/java/com/twelvemonkeys/imageio/plugins/icns/ICNSProviderInfo.java +++ b/imageio/imageio-icns/src/main/java/com/twelvemonkeys/imageio/plugins/icns/ICNSProviderInfo.java @@ -1,3 +1,31 @@ +/* + * Copyright (c) 2015, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + package com.twelvemonkeys.imageio.plugins.icns; import com.twelvemonkeys.imageio.spi.ReaderWriterProviderInfo; @@ -19,7 +47,7 @@ final class ICNSProviderInfo extends ReaderWriterProviderInfo { "image/x-apple-icons", // Common extension MIME }, "com.twelvemonkeys.imageio.plugins.icns.ICNSImageReader", - new String[] {"com.twelvemonkeys.imageio.plugins.ics.ICNImageReaderSpi"}, + new String[] {"com.twelvemonkeys.imageio.plugins.icns.ICNSImageReaderSpi"}, null, null, false, null, null, null, null, true, null, null, null, null diff --git a/imageio/imageio-icns/src/test/java/com/twelvemonkeys/imageio/plugins/icns/ICNSImageReaderTest.java b/imageio/imageio-icns/src/test/java/com/twelvemonkeys/imageio/plugins/icns/ICNSImageReaderTest.java index 8dc95484..84131b60 100644 --- a/imageio/imageio-icns/src/test/java/com/twelvemonkeys/imageio/plugins/icns/ICNSImageReaderTest.java +++ b/imageio/imageio-icns/src/test/java/com/twelvemonkeys/imageio/plugins/icns/ICNSImageReaderTest.java @@ -28,7 +28,7 @@ package com.twelvemonkeys.imageio.plugins.icns; -import com.twelvemonkeys.imageio.util.ImageReaderAbstractTestCase; +import com.twelvemonkeys.imageio.util.ImageReaderAbstractTest; import org.junit.Ignore; import org.junit.Test; @@ -37,6 +37,7 @@ import javax.imageio.spi.ImageReaderSpi; import java.awt.*; import java.io.IOException; import java.util.Arrays; +import java.util.Collections; import java.util.List; /** @@ -46,7 +47,7 @@ import java.util.List; * @author last modified by $Author: haraldk$ * @version $Id: ICNSImageReaderTest.java,v 1.0 25.10.11 18:44 haraldk Exp$ */ -public class ICNSImageReaderTest extends ImageReaderAbstractTestCase { +public class ICNSImageReaderTest extends ImageReaderAbstractTest { @Override protected List getTestData() { return Arrays.asList( @@ -119,17 +120,17 @@ public class ICNSImageReaderTest extends ImageReaderAbstractTestCase { @Override protected List getFormatNames() { - return Arrays.asList("icns"); + return Collections.singletonList("icns"); } @Override protected List getSuffixes() { - return Arrays.asList("icns"); + return Collections.singletonList("icns"); } @Override protected List getMIMETypes() { - return Arrays.asList("image/x-apple-icons"); + return Collections.singletonList("image/x-apple-icons"); } @Test diff --git a/imageio/imageio-icns/src/test/java/com/twelvemonkeys/imageio/plugins/icns/ICNSProviderInfoTest.java b/imageio/imageio-icns/src/test/java/com/twelvemonkeys/imageio/plugins/icns/ICNSProviderInfoTest.java new file mode 100644 index 00000000..c5772ae6 --- /dev/null +++ b/imageio/imageio-icns/src/test/java/com/twelvemonkeys/imageio/plugins/icns/ICNSProviderInfoTest.java @@ -0,0 +1,19 @@ +package com.twelvemonkeys.imageio.plugins.icns; + +import com.twelvemonkeys.imageio.spi.ReaderWriterProviderInfo; +import com.twelvemonkeys.imageio.spi.ReaderWriterProviderInfoTest; + +/** + * ICNSProviderInfoTest. + * + * @author Harald Kuhr + * @author last modified by $Author: harald.kuhr$ + * @version $Id: ICNSProviderInfoTest.java,v 1.0 02/06/16 harald.kuhr Exp$ + */ +public class ICNSProviderInfoTest extends ReaderWriterProviderInfoTest { + + @Override + protected ReaderWriterProviderInfo createProviderInfo() { + return new ICNSProviderInfo(); + } +} \ No newline at end of file diff --git a/imageio/imageio-iff/pom.xml b/imageio/imageio-iff/pom.xml index 5bf6aeb5..1bac81f2 100644 --- a/imageio/imageio-iff/pom.xml +++ b/imageio/imageio-iff/pom.xml @@ -4,7 +4,7 @@ com.twelvemonkeys.imageio imageio - 3.1-SNAPSHOT + 3.3-SNAPSHOT imageio-iff TwelveMonkeys :: ImageIO :: IFF plugin @@ -21,7 +21,7 @@ com.twelvemonkeys.imageio imageio-core - tests + test-jar diff --git a/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/IFFProviderInfo.java b/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/IFFProviderInfo.java index cb47612c..62f8cf89 100644 --- a/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/IFFProviderInfo.java +++ b/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/IFFProviderInfo.java @@ -1,3 +1,31 @@ +/* + * Copyright (c) 2015, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + package com.twelvemonkeys.imageio.plugins.iff; import com.twelvemonkeys.imageio.spi.ReaderWriterProviderInfo; diff --git a/imageio/imageio-iff/src/test/java/com/twelvemonkeys/imageio/plugins/iff/IFFImageReaderTest.java b/imageio/imageio-iff/src/test/java/com/twelvemonkeys/imageio/plugins/iff/IFFImageReaderTest.java index 11d88b74..90783b11 100755 --- a/imageio/imageio-iff/src/test/java/com/twelvemonkeys/imageio/plugins/iff/IFFImageReaderTest.java +++ b/imageio/imageio-iff/src/test/java/com/twelvemonkeys/imageio/plugins/iff/IFFImageReaderTest.java @@ -28,7 +28,7 @@ package com.twelvemonkeys.imageio.plugins.iff; -import com.twelvemonkeys.imageio.util.ImageReaderAbstractTestCase; +import com.twelvemonkeys.imageio.util.ImageReaderAbstractTest; import org.junit.Test; import javax.imageio.ImageIO; @@ -39,11 +39,10 @@ import java.awt.image.ColorModel; import java.awt.image.IndexColorModel; import java.io.IOException; import java.util.Arrays; +import java.util.Collections; import java.util.List; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.*; /** * IFFImageReaderTestCase @@ -52,7 +51,7 @@ import static org.junit.Assert.assertTrue; * @author last modified by $Author: haraldk$ * @version $Id: IFFImageReaderTestCase.java,v 1.0 Apr 1, 2008 10:39:17 PM haraldk Exp$ */ -public class IFFImageReaderTest extends ImageReaderAbstractTestCase { +public class IFFImageReaderTest extends ImageReaderAbstractTest { protected List getTestData() { return Arrays.asList( // 32 bit - Ok @@ -93,7 +92,7 @@ public class IFFImageReaderTest extends ImageReaderAbstractTestCase getFormatNames() { - return Arrays.asList("iff"); + return Collections.singletonList("iff"); } protected List getSuffixes() { diff --git a/imageio/imageio-iff/src/test/java/com/twelvemonkeys/imageio/plugins/iff/IFFProviderInfoTest.java b/imageio/imageio-iff/src/test/java/com/twelvemonkeys/imageio/plugins/iff/IFFProviderInfoTest.java new file mode 100644 index 00000000..ec666c9a --- /dev/null +++ b/imageio/imageio-iff/src/test/java/com/twelvemonkeys/imageio/plugins/iff/IFFProviderInfoTest.java @@ -0,0 +1,19 @@ +package com.twelvemonkeys.imageio.plugins.iff; + +import com.twelvemonkeys.imageio.spi.ReaderWriterProviderInfo; +import com.twelvemonkeys.imageio.spi.ReaderWriterProviderInfoTest; + +/** + * IFFProviderInfoTest. + * + * @author Harald Kuhr + * @author last modified by $Author: harald.kuhr$ + * @version $Id: IFFProviderInfoTest.java,v 1.0 02/06/16 harald.kuhr Exp$ + */ +public class IFFProviderInfoTest extends ReaderWriterProviderInfoTest { + + @Override + protected ReaderWriterProviderInfo createProviderInfo() { + return new IFFProviderInfo(); + } +} \ No newline at end of file diff --git a/imageio/imageio-jpeg/pom.xml b/imageio/imageio-jpeg/pom.xml index 90e89fa1..1a2c62e0 100644 --- a/imageio/imageio-jpeg/pom.xml +++ b/imageio/imageio-jpeg/pom.xml @@ -4,7 +4,7 @@ com.twelvemonkeys.imageio imageio - 3.1-SNAPSHOT + 3.3-SNAPSHOT imageio-jpeg TwelveMonkeys :: ImageIO :: JPEG plugin @@ -20,7 +20,7 @@ com.twelvemonkeys.imageio imageio-core - tests + test-jar com.twelvemonkeys.imageio diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnailReader.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnailReader.java index 704dc1c9..20b5b50b 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnailReader.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnailReader.java @@ -28,6 +28,7 @@ package com.twelvemonkeys.imageio.plugins.jpeg; +import com.twelvemonkeys.imageio.color.YCbCrConverter; import com.twelvemonkeys.imageio.metadata.Directory; import com.twelvemonkeys.imageio.metadata.Entry; import com.twelvemonkeys.imageio.metadata.exif.TIFF; @@ -102,7 +103,7 @@ final class EXIFThumbnailReader extends ThumbnailReader { thumbnail = readJPEG(); } - cachedThumbnail = pixelsExposed ? null : new SoftReference(thumbnail); + cachedThumbnail = pixelsExposed ? null : new SoftReference<>(thumbnail); return thumbnail; } @@ -132,14 +133,10 @@ final class EXIFThumbnailReader extends ThumbnailReader { input = new SequenceInputStream(new ByteArrayInputStream(fakeEmptyExif), input); try { - MemoryCacheImageInputStream stream = new MemoryCacheImageInputStream(input); - try { + try (MemoryCacheImageInputStream stream = new MemoryCacheImageInputStream(input)) { return readJPEGThumbnail(reader, stream); } - finally { - stream.close(); - } } finally { input.close(); @@ -195,15 +192,15 @@ final class EXIFThumbnailReader extends ThumbnailReader { break; case 6: // YCbCr - for (int i = 0, thumbDataLength = thumbData.length; i < thumbDataLength; i += 3) { - JPEGImageReader.YCbCrConverter.convertYCbCr2RGB(thumbData, thumbData, i); + for (int i = 0; i < thumbSize; i += 3) { + YCbCrConverter.convertYCbCr2RGB(thumbData, thumbData, i); } break; default: throw new IIOException("Unknown PhotometricInterpretation value for uncompressed EXIF thumbnail (expected 2 or 6): " + interpretation); } - return ThumbnailReader.readRawThumbnail(thumbData, thumbData.length, 0, w, h); + return ThumbnailReader.readRawThumbnail(thumbData, thumbSize, 0, w, h); } throw new IIOException("Missing StripOffsets tag for uncompressed EXIF thumbnail"); diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java index 1c2a82c7..6264cb81 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java @@ -28,9 +28,9 @@ package com.twelvemonkeys.imageio.plugins.jpeg; -import com.twelvemonkeys.image.ImageUtil; import com.twelvemonkeys.imageio.ImageReaderBase; import com.twelvemonkeys.imageio.color.ColorSpaces; +import com.twelvemonkeys.imageio.color.YCbCrConverter; import com.twelvemonkeys.imageio.metadata.CompoundDirectory; import com.twelvemonkeys.imageio.metadata.Directory; import com.twelvemonkeys.imageio.metadata.Entry; @@ -112,7 +112,7 @@ public class JPEGImageReader extends ImageReaderBase { private static final Map> SEGMENT_IDENTIFIERS = createSegmentIds(); private static Map> createSegmentIds() { - Map> map = new LinkedHashMap>(); + Map> map = new LinkedHashMap<>(); // Need all APP markers to be able to re-generate proper metadata later for (int appMarker = JPEG.APP0; appMarker <= JPEG.APP15; appMarker++) { @@ -152,10 +152,10 @@ public class JPEGImageReader extends ImageReaderBase { /** Cached list of JPEG segments we filter from the underlying stream */ private List segments; - JPEGImageReader(final ImageReaderSpi provider, final ImageReader delegate) { + protected JPEGImageReader(final ImageReaderSpi provider, final ImageReader delegate) { super(provider); - this.delegate = Validate.notNull(delegate); + this.delegate = Validate.notNull(delegate); progressDelegator = new ProgressDelegator(); } @@ -224,7 +224,7 @@ public class JPEGImageReader extends ImageReaderBase { JPEGColorSpace csType = getSourceCSType(getJFIF(), getAdobeDCT(), getSOF()); if (types == null || !types.hasNext() || csType == JPEGColorSpace.CMYK || csType == JPEGColorSpace.YCCK) { - ArrayList typeList = new ArrayList(); + ArrayList typeList = new ArrayList<>(); // Add the standard types, we can always convert to these typeList.add(ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_3BYTE_BGR)); typeList.add(ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_INT_RGB)); @@ -268,10 +268,15 @@ public class JPEGImageReader extends ImageReaderBase { @Override public ImageTypeSpecifier getRawImageType(int imageIndex) throws IOException { // If delegate can determine the spec, we'll just go with that - ImageTypeSpecifier rawType = delegate.getRawImageType(imageIndex); + try { + ImageTypeSpecifier rawType = delegate.getRawImageType(imageIndex); - if (rawType != null) { - return rawType; + if (rawType != null) { + return rawType; + } + } + catch (NullPointerException ignore) { + // Fall through } // Otherwise, consult the image metadata @@ -298,7 +303,9 @@ public class JPEGImageReader extends ImageReaderBase { super.setInput(input, seekForwardOnly, ignoreMetadata); // JPEGSegmentImageInputStream that filters out/skips bad/unnecessary segments - delegate.setInput(imageInput != null ? new JPEGSegmentImageInputStream(imageInput) : null, seekForwardOnly, ignoreMetadata); + delegate.setInput(imageInput != null + ? new JPEGSegmentImageInputStream(imageInput) + : null, seekForwardOnly, ignoreMetadata); } @Override @@ -311,22 +318,10 @@ public class JPEGImageReader extends ImageReaderBase { assertInput(); checkBounds(imageIndex); -// CompoundDirectory exif = getExif(); -// if (exif != null) { -// System.err.println("exif: " + exif); -// System.err.println("Orientation: " + exif.getEntryById(TIFF.TAG_ORIENTATION)); -// Entry exifIFDEntry = exif.getEntryById(TIFF.TAG_EXIF_IFD); -// -// if (exifIFDEntry != null) { -// Directory exifIFD = (Directory) exifIFDEntry.getValue(); -// System.err.println("PixelXDimension: " + exifIFD.getEntryById(EXIF.TAG_PIXEL_X_DIMENSION)); -// System.err.println("PixelYDimension: " + exifIFD.getEntryById(EXIF.TAG_PIXEL_Y_DIMENSION)); -// } -// } - SOFSegment sof = getSOF(); ICC_Profile profile = getEmbeddedICCProfile(false); AdobeDCTSegment adobeDCT = getAdobeDCT(); + boolean bogusAdobeDCT = false; if (adobeDCT != null && (adobeDCT.getTransform() == AdobeDCTSegment.YCC && sof.componentsInFrame() != 3 || adobeDCT.getTransform() == AdobeDCTSegment.YCCK && sof.componentsInFrame() != 4)) { @@ -337,6 +332,7 @@ public class JPEGImageReader extends ImageReaderBase { sof.marker & 0xf, sof.componentsInFrame() )); + bogusAdobeDCT = true; adobeDCT = null; } @@ -345,11 +341,11 @@ public class JPEGImageReader extends ImageReaderBase { // We need to apply ICC profile unless the profile is sRGB/default gray (whatever that is) // - or only filter out the bad ICC profiles in the JPEGSegmentImageInputStream. if (delegate.canReadRaster() && ( + bogusAdobeDCT || sourceCSType == JPEGColorSpace.CMYK || sourceCSType == JPEGColorSpace.YCCK || - adobeDCT != null && adobeDCT.getTransform() == AdobeDCTSegment.YCCK || - profile != null && !ColorSpaces.isCS_sRGB(profile)) || - sourceCSType == JPEGColorSpace.YCbCr && getRawImageType(imageIndex) != null) { // TODO: Issue warning? + profile != null && !ColorSpaces.isCS_sRGB(profile) || + sourceCSType == JPEGColorSpace.YCbCr && getRawImageType(imageIndex) != null)) { // TODO: Issue warning? if (DEBUG) { System.out.println("Reading using raster and extra conversion"); System.out.println("ICC color profile: " + profile); @@ -371,6 +367,10 @@ public class JPEGImageReader extends ImageReaderBase { int origHeight = getHeight(imageIndex); Iterator imageTypes = getImageTypes(imageIndex); + // TODO: Avoid creating destination here, if possible (as it saves time and memory) + // If YCbCr or RGB, we could instead create a BufferedImage around the converted raster directly. + // If YCCK or CMYK, we could instead create a BufferedImage around the converted raster, + // leaving the fourth band as alpha (or pretend it's not there, by creating a child raster). BufferedImage image = getDestination(param, imageTypes, origWidth, origHeight); WritableRaster destination = image.getRaster(); @@ -404,6 +404,7 @@ public class JPEGImageReader extends ImageReaderBase { if (DEBUG) { System.err.println("Converting from " + intendedCS + " to " + (image.getColorModel().getColorSpace().isCS_sRGB() ? "sRGB" : image.getColorModel().getColorSpace())); } + convert = new ColorConvertOp(intendedCS, image.getColorModel().getColorSpace(), null); } // Else, pass through with no conversion @@ -462,10 +463,13 @@ public class JPEGImageReader extends ImageReaderBase { // Apply source color conversion from implicit color space if (csType == JPEGColorSpace.YCbCr || csType == JPEGColorSpace.YCbCrA) { - YCbCrConverter.convertYCbCr2RGB(raster); + convertYCbCr2RGB(raster); } else if (csType == JPEGColorSpace.YCCK) { - YCbCrConverter.convertYCCK2CMYK(raster); + // TODO: Need to rethink this (non-) inversion, see #147 + // TODO: Allow param to specify inversion, or possibly the PDF decode array + // flag0 bit 15, blend = 1 see http://graphicdesign.stackexchange.com/questions/12894/cmyk-jpegs-extracted-from-pdf-appear-inverted + convertYCCK2CMYK(raster); } else if (csType == JPEGColorSpace.CMYK) { invertCMYK(raster); @@ -620,7 +624,7 @@ public class JPEGImageReader extends ImageReaderBase { } } - private ICC_Profile ensureDisplayProfile(final ICC_Profile profile) { + protected ICC_Profile ensureDisplayProfile(final ICC_Profile profile) { // NOTE: This is probably not the right way to do it... :-P // TODO: Consider moving method to ColorSpaces class or new class in imageio.color package @@ -670,16 +674,11 @@ public class JPEGImageReader extends ImageReaderBase { segments = JPEGSegmentUtil.readSegments(imageInput, SEGMENT_IDENTIFIERS); } - catch (IIOException ignore) { + catch (IIOException | IllegalArgumentException ignore) { if (DEBUG) { ignore.printStackTrace(); } } - catch (IllegalArgumentException foo) { - if (DEBUG) { - foo.printStackTrace(); - } - } finally { imageInput.reset(); } @@ -699,7 +698,7 @@ public class JPEGImageReader extends ImageReaderBase { if ((marker == ALL_APP_MARKERS && segment.marker() >= JPEG.APP0 && segment.marker() <= JPEG.APP15 || segment.marker() == marker) && (identifier == null || identifier.equals(segment.identifier()))) { if (appSegments == Collections.EMPTY_LIST) { - appSegments = new ArrayList(segments.size()); + appSegments = new ArrayList<>(segments.size()); } appSegments.add(segment); @@ -831,7 +830,7 @@ public class JPEGImageReader extends ImageReaderBase { return data; } - ICC_Profile getEmbeddedICCProfile(final boolean allowBadIndexes) throws IOException { + protected ICC_Profile getEmbeddedICCProfile(final boolean allowBadIndexes) throws IOException { // ICC v 1.42 (2006) annex B: // APP2 marker (0xFFE2) + 2 byte length + ASCII 'ICC_PROFILE' + 0 (termination) // + 1 byte chunk number + 1 byte chunk count (allows ICC profiles chunked in multiple APP2 segments) @@ -853,7 +852,7 @@ public class JPEGImageReader extends ImageReaderBase { return null; } - return readICCProfileSafe(stream); + return readICCProfileSafe(stream, allowBadIndexes); } else if (!segments.isEmpty()) { // NOTE: This is probably over-complicated, as I've never encountered ICC_PROFILE chunks out of order... @@ -900,15 +899,17 @@ public class JPEGImageReader extends ImageReaderBase { streams[badICC ? i : chunkNumber - 1] = stream; } - return readICCProfileSafe(new SequenceInputStream(Collections.enumeration(Arrays.asList(streams)))); + return readICCProfileSafe(new SequenceInputStream(Collections.enumeration(Arrays.asList(streams))), allowBadIndexes); } return null; } - private ICC_Profile readICCProfileSafe(final InputStream stream) throws IOException { + private ICC_Profile readICCProfileSafe(final InputStream stream, final boolean allowBadProfile) throws IOException { try { - return ICC_Profile.getInstance(stream); + ICC_Profile profile = ICC_Profile.getInstance(stream); + + return allowBadProfile ? profile : ColorSpaces.validateProfile(profile); } catch (RuntimeException e) { // NOTE: Throws either IllegalArgumentException or CMMException, depending on platform. @@ -940,6 +941,11 @@ public class JPEGImageReader extends ImageReaderBase { delegate.abort(); } + @Override + public ImageReadParam getDefaultReadParam() { + return delegate.getDefaultReadParam(); + } + @Override public boolean readerSupportsThumbnails() { return true; // We support EXIF, JFIF and JFXX style thumbnails @@ -949,7 +955,7 @@ public class JPEGImageReader extends ImageReaderBase { checkBounds(imageIndex); if (thumbnails == null) { - thumbnails = new ArrayList(); + thumbnails = new ArrayList<>(); ThumbnailReadProgressListener thumbnailProgressDelegator = new ThumbnailProgressDelegate(); // Read JFIF thumbnails if present @@ -1101,105 +1107,32 @@ public class JPEGImageReader extends ImageReaderBase { } } - /** - * Static inner class for lazy-loading of conversion tables. - * - * @author Harald Kuhr - * @author Original code by Werner Randelshofer - */ - static final class YCbCrConverter { - /** Define tables for YCC->RGB color space conversion. */ - private final static int SCALEBITS = 16; - private final static int MAXJSAMPLE = 255; - private final static int CENTERJSAMPLE = 128; - private final static int ONE_HALF = 1 << (SCALEBITS - 1); + public static void convertYCbCr2RGB(final Raster raster) { + final int height = raster.getHeight(); + final int width = raster.getWidth(); + final byte[] data = ((DataBufferByte) raster.getDataBuffer()).getData(); - private final static int[] Cr_R_LUT = new int[MAXJSAMPLE + 1]; - private final static int[] Cb_B_LUT = new int[MAXJSAMPLE + 1]; - private final static int[] Cr_G_LUT = new int[MAXJSAMPLE + 1]; - private final static int[] Cb_G_LUT = new int[MAXJSAMPLE + 1]; - - /** - * Initializes tables for YCC->RGB color space conversion. - */ - private static void buildYCCtoRGBtable() { - if (DEBUG) { - System.err.println("Building YCC conversion table"); - } - - for (int i = 0, x = -CENTERJSAMPLE; i <= MAXJSAMPLE; i++, x++) { - // i is the actual input pixel value, in the range 0..MAXJSAMPLE - // The Cb or Cr value we are thinking of is x = i - CENTERJSAMPLE - // Cr=>R value is nearest int to 1.40200 * x - Cr_R_LUT[i] = (int) ((1.40200 * (1 << SCALEBITS) + 0.5) * x + ONE_HALF) >> SCALEBITS; - // Cb=>B value is nearest int to 1.77200 * x - Cb_B_LUT[i] = (int) ((1.77200 * (1 << SCALEBITS) + 0.5) * x + ONE_HALF) >> SCALEBITS; - // Cr=>G value is scaled-up -0.71414 * x - Cr_G_LUT[i] = -(int) (0.71414 * (1 << SCALEBITS) + 0.5) * x; - // Cb=>G value is scaled-up -0.34414 * x - // We also add in ONE_HALF so that need not do it in inner loop - Cb_G_LUT[i] = -(int) ((0.34414) * (1 << SCALEBITS) + 0.5) * x + ONE_HALF; + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + YCbCrConverter.convertYCbCr2RGB(data, data, (x + y * width) * 3); } } + } - static { - buildYCCtoRGBtable(); - } + public static void convertYCCK2CMYK(final Raster raster) { + final int height = raster.getHeight(); + final int width = raster.getWidth(); + final byte[] data = ((DataBufferByte) raster.getDataBuffer()).getData(); - static void convertYCbCr2RGB(final Raster raster) { - final int height = raster.getHeight(); - final int width = raster.getWidth(); - final byte[] data = ((DataBufferByte) raster.getDataBuffer()).getData(); - - for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) { - convertYCbCr2RGB(data, data, (x + y * width) * 3); - } + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + int offset = (x + y * width) * 4; + // YCC -> CMY + YCbCrConverter.convertYCbCr2RGB(data, data, offset); + // Inverse K + data[offset + 3] = (byte) (0xff - data[offset + 3] & 0xff); } } - - static void convertYCbCr2RGB(final byte[] yCbCr, final byte[] rgb, final int offset) { - int y = yCbCr[offset ] & 0xff; - int cr = yCbCr[offset + 2] & 0xff; - int cb = yCbCr[offset + 1] & 0xff; - - rgb[offset ] = clamp(y + Cr_R_LUT[cr]); - rgb[offset + 1] = clamp(y + (Cb_G_LUT[cb] + Cr_G_LUT[cr] >> SCALEBITS)); - rgb[offset + 2] = clamp(y + Cb_B_LUT[cb]); - } - - static void convertYCCK2CMYK(final Raster raster) { - final int height = raster.getHeight(); - final int width = raster.getWidth(); - final byte[] data = ((DataBufferByte) raster.getDataBuffer()).getData(); - - for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) { - convertYCCK2CMYK(data, data, (x + y * width) * 4); - } - } - } - - private static void convertYCCK2CMYK(byte[] ycck, byte[] cmyk, int offset) { - // Inverted - int y = 255 - ycck[offset ] & 0xff; - int cb = 255 - ycck[offset + 1] & 0xff; - int cr = 255 - ycck[offset + 2] & 0xff; - int k = 255 - ycck[offset + 3] & 0xff; - - int cmykC = MAXJSAMPLE - (y + Cr_R_LUT[cr]); - int cmykM = MAXJSAMPLE - (y + (Cb_G_LUT[cb] + Cr_G_LUT[cr] >> SCALEBITS)); - int cmykY = MAXJSAMPLE - (y + Cb_B_LUT[cb]); - - cmyk[offset ] = clamp(cmykC); - cmyk[offset + 1] = clamp(cmykM); - cmyk[offset + 2] = clamp(cmykY); - cmyk[offset + 3] = (byte) k; // K passes through unchanged - } - - private static byte clamp(int val) { - return (byte) Math.max(0, Math.min(255, val)); - } } private class ProgressDelegator extends ProgressListenerBase implements IIOReadUpdateListener, IIOReadWarningListener { @@ -1297,7 +1230,68 @@ public class JPEGImageReader extends ImageReaderBase { } public static void main(final String[] args) throws IOException { - for (final String arg : args) { + ImageIO.setUseCache(false); + + int subX = 1; + int subY = 1; + int xOff = 0; + int yOff = 0; + Rectangle roi = null; + boolean metadata = false; + boolean thumbnails = false; + + for (int argIdx = 0; argIdx < args.length; argIdx++) { + final String arg = args[argIdx]; + + if (arg.charAt(0) == '-') { + if (arg.equals("-s") || arg.equals("--subsample") && args.length > argIdx) { + String[] sub = args[++argIdx].split(","); + + try { + if (sub.length >= 4) { + subX = Integer.parseInt(sub[0]); + subY = Integer.parseInt(sub[1]); + xOff = Integer.parseInt(sub[2]); + yOff = Integer.parseInt(sub[3]); + } + else { + subX = Integer.parseInt(sub[0]); + subY = sub.length > 1 ? Integer.parseInt(sub[1]) : subX; + } + } + catch (NumberFormatException e) { + System.err.println("Bad sub sampling (x,y): '" + args[argIdx] + "'"); + } + } + else if (arg.equals("-r") || arg.equals("--roi") && args.length > argIdx) { + String[] region = args[++argIdx].split(","); + + try { + if (region.length >= 4) { + roi = new Rectangle(Integer.parseInt(region[0]), Integer.parseInt(region[2]), Integer.parseInt(region[2]), Integer.parseInt(region[3])); + } + else { + roi = new Rectangle(Integer.parseInt(region[0]), Integer.parseInt(region[2])); + } + } + catch (IndexOutOfBoundsException | NumberFormatException e) { + System.err.println("Bad source region ([x,y,]w, h): '" + args[argIdx] + "'"); + } + } + else if (arg.equals("-m") || arg.equals("--metadata")) { + metadata = true; + } + else if (arg.equals("-t") || arg.equals("--thumbnails")) { + thumbnails = true; + } + else { + System.err.println("Unknown argument: '" + arg + "'"); + System.exit(-1); + } + + continue; + } + File file = new File(arg); ImageInputStream input = ImageIO.createImageInputStream(file); @@ -1313,15 +1307,15 @@ public class JPEGImageReader extends ImageReaderBase { continue; } - ImageReader reader = readers.next(); -// System.err.println("Reading using: " + reader); + final ImageReader reader = readers.next(); + System.err.println("Reading using: " + reader); reader.addIIOReadWarningListener(new IIOReadWarningListener() { public void warningOccurred(ImageReader source, String warning) { System.err.println("Warning: " + arg + ": " + warning); } }); - reader.addIIOReadProgressListener(new ProgressListenerBase() { + final ProgressListenerBase listener = new ProgressListenerBase() { private static final int MAX_W = 78; int lastProgress = 0; @@ -1350,29 +1344,35 @@ public class JPEGImageReader extends ImageReaderBase { System.out.println("]"); } - }); + }; + reader.addIIOReadProgressListener(listener); reader.setInput(input); - // For a tables-only image, we can't read image, but we should get metadata. - if (reader.getNumImages(true) == 0) { - IIOMetadata streamMetadata = reader.getStreamMetadata(); - IIOMetadataNode streamNativeTree = (IIOMetadataNode) streamMetadata.getAsTree(streamMetadata.getNativeMetadataFormatName()); - new XMLSerializer(System.out, System.getProperty("file.encoding")).serialize(streamNativeTree, false); - continue; - } - try { + // For a tables-only image, we can't read image, but we should get metadata. + if (reader.getNumImages(true) == 0) { + IIOMetadata streamMetadata = reader.getStreamMetadata(); + IIOMetadataNode streamNativeTree = (IIOMetadataNode) streamMetadata.getAsTree(streamMetadata.getNativeMetadataFormatName()); + new XMLSerializer(System.out, System.getProperty("file.encoding")).serialize(streamNativeTree, false); + continue; + } + + BufferedImage image; ImageReadParam param = reader.getDefaultReadParam(); -// if (args.length > 1) { -// int sub = Integer.parseInt(args[1]); -// int sub = 4; -// param.setSourceSubsampling(sub, sub, 0, 0); -// } - BufferedImage image = reader.getImageTypes(0).next().createBufferedImage(reader.getWidth(0), reader.getHeight(0)); + if (subX > 1 || subY > 1 || roi != null) { + param.setSourceSubsampling(subX, subY, xOff, yOff); + param.setSourceRegion(roi); + + image = reader.getImageTypes(0).next().createBufferedImage((reader.getWidth(0) + subX - 1)/ subX, (reader.getHeight(0) + subY - 1) / subY); + } + else { + image = reader.getImageTypes(0).next().createBufferedImage(reader.getWidth(0), reader.getHeight(0)); + } param.setDestination(image); -// long start = System.currentTimeMillis(); + long start = DEBUG ? System.currentTimeMillis() : 0; + try { image = reader.read(0, param); } @@ -1383,12 +1383,13 @@ public class JPEGImageReader extends ImageReaderBase { continue; } } -// System.err.println("Read time: " + (System.currentTimeMillis() - start) + " ms"); -// System.err.println("image: " + image); + if (DEBUG) { + System.err.println("Read time: " + (System.currentTimeMillis() - start) + " ms"); + System.err.println("image: " + image); + } -// image = new ResampleOp(reader.getWidth(0) / 4, reader.getHeight(0) / 4, ResampleOp.FILTER_LANCZOS).filter(image, null); - + /* int maxW = 1280; int maxH = 800; if (image.getWidth() > maxW || image.getHeight() > maxH) { @@ -1402,28 +1403,45 @@ public class JPEGImageReader extends ImageReaderBase { } // System.err.println("Scale time: " + (System.currentTimeMillis() - start) + " ms"); } + */ showIt(image, String.format("Image: %s [%d x %d]", file.getName(), reader.getWidth(0), reader.getHeight(0))); - try { - IIOMetadata imageMetadata = reader.getImageMetadata(0); - System.out.println("Metadata for File: " + file.getName()); - System.out.println("Native:"); - new XMLSerializer(System.out, System.getProperty("file.encoding")).serialize(imageMetadata.getAsTree(imageMetadata.getNativeMetadataFormatName()), false); - System.out.println("Standard:"); - new XMLSerializer(System.out, System.getProperty("file.encoding")).serialize(imageMetadata.getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName), false); - System.out.println(); + if (metadata) { + try { + IIOMetadata imageMetadata = reader.getImageMetadata(0); + System.out.println("Metadata for File: " + file.getName()); - int numThumbnails = reader.getNumThumbnails(0); - for (int i = 0; i < numThumbnails; i++) { - BufferedImage thumbnail = reader.readThumbnail(0, i); -// System.err.println("thumbnail: " + thumbnail); - showIt(thumbnail, String.format("Thumbnail: %s [%d x %d]", file.getName(), thumbnail.getWidth(), thumbnail.getHeight())); + if (imageMetadata.getNativeMetadataFormatName() != null) { + System.out.println("Native:"); + new XMLSerializer(System.out, System.getProperty("file.encoding")).serialize(imageMetadata.getAsTree(imageMetadata.getNativeMetadataFormatName()), false); + } + if (imageMetadata.isStandardMetadataFormatSupported()) { + System.out.println("Standard:"); + new XMLSerializer(System.out, System.getProperty("file.encoding")).serialize(imageMetadata.getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName), false); + } + + System.out.println(); + } + catch (IIOException e) { + System.err.println("Could not read thumbnails: " + arg + ": " + e.getMessage()); + e.printStackTrace(); } } - catch (IIOException e) { - System.err.println("Could not read thumbnails: " + arg + ": " + e.getMessage()); - e.printStackTrace(); + + if (thumbnails) { + try { + int numThumbnails = reader.getNumThumbnails(0); + for (int i = 0; i < numThumbnails; i++) { + BufferedImage thumbnail = reader.readThumbnail(0, i); + // System.err.println("thumbnail: " + thumbnail); + showIt(thumbnail, String.format("Thumbnail: %s [%d x %d]", file.getName(), thumbnail.getWidth(), thumbnail.getHeight())); + } + } + catch (IIOException e) { + System.err.println("Could not read thumbnails: " + arg + ": " + e.getMessage()); + e.printStackTrace(); + } } } catch (Throwable t) { diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReaderSpi.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReaderSpi.java index efe0f4de..adf926e5 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReaderSpi.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReaderSpi.java @@ -29,6 +29,7 @@ package com.twelvemonkeys.imageio.plugins.jpeg; import com.twelvemonkeys.imageio.spi.ImageReaderSpiBase; +import com.twelvemonkeys.imageio.spi.ReaderWriterProviderInfo; import com.twelvemonkeys.imageio.util.IIOUtil; import com.twelvemonkeys.lang.Validate; @@ -48,14 +49,14 @@ import java.util.Locale; * @version $Id: JPEGImageReaderSpi.java,v 1.0 24.01.11 22.12 haraldk Exp$ */ public class JPEGImageReaderSpi extends ImageReaderSpiBase { - private ImageReaderSpi delegateProvider; + protected ImageReaderSpi delegateProvider; /** * Constructor for use by {@link javax.imageio.spi.IIORegistry} only. * The instance created will not work without being properly registered. */ public JPEGImageReaderSpi() { - super(new JPEGProviderInfo()); + this(new JPEGProviderInfo()); } /** @@ -69,6 +70,15 @@ public class JPEGImageReaderSpi extends ImageReaderSpiBase { this.delegateProvider = Validate.notNull(delegateProvider); } + /** + * Constructor for subclasses. + * + * @param info + */ + protected JPEGImageReaderSpi(final ReaderWriterProviderInfo info) { + super(info); + } + static ImageReaderSpi lookupDelegateProvider(final ServiceRegistry registry) { Iterator providers = registry.getServiceProviders(ImageReaderSpi.class, true); @@ -83,7 +93,7 @@ public class JPEGImageReaderSpi extends ImageReaderSpiBase { return null; } - @SuppressWarnings({"unchecked"}) + @SuppressWarnings({"unchecked", "deprecation"}) @Override public void onRegistration(final ServiceRegistry registry, final Class category) { if (delegateProvider == null) { diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGProviderInfo.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGProviderInfo.java index 961a877c..2865efaf 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGProviderInfo.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGProviderInfo.java @@ -1,3 +1,31 @@ +/* + * Copyright (c) 2015, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + package com.twelvemonkeys.imageio.plugins.jpeg; import com.twelvemonkeys.imageio.spi.ReaderWriterProviderInfo; diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGSegmentImageInputStream.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGSegmentImageInputStream.java index a5f11ecb..f7a2a727 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGSegmentImageInputStream.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGSegmentImageInputStream.java @@ -240,16 +240,21 @@ final class JPEGSegmentImageInputStream extends ImageInputStreamImpl { private void streamInit() throws IOException { stream.seek(0); - int soi = stream.readUnsignedShort(); - if (soi != JPEG.SOI) { - throw new IIOException(String.format("Not a JPEG stream (starts with: 0x%04x, expected SOI: 0x%04x)", soi, JPEG.SOI)); - } - else { + try { + int soi = stream.readUnsignedShort(); + + if (soi != JPEG.SOI) { + throw new IIOException(String.format("Not a JPEG stream (starts with: 0x%04x, expected SOI: 0x%04x)", soi, JPEG.SOI)); + } + segment = new Segment(soi, 0, 0, 2); segments.add(segment); currentSegment = segments.size() - 1; // 0 } + catch (EOFException eof) { + throw new IIOException(String.format("Not a JPEG stream (short stream. expected SOI: 0x%04x)", JPEG.SOI), eof); + } } static boolean isAppSegmentMarker(final int marker) { diff --git a/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReaderTest.java b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReaderTest.java index 30969f76..1f495e35 100644 --- a/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReaderTest.java +++ b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReaderTest.java @@ -28,7 +28,7 @@ package com.twelvemonkeys.imageio.plugins.jpeg; -import com.twelvemonkeys.imageio.util.ImageReaderAbstractTestCase; +import com.twelvemonkeys.imageio.util.ImageReaderAbstractTest; import org.hamcrest.core.IsInstanceOf; import org.junit.Ignore; import org.junit.Test; @@ -59,10 +59,11 @@ import java.util.*; import java.util.List; import static org.junit.Assert.*; +import static org.junit.Assume.assumeNoException; +import static org.junit.Assume.assumeNotNull; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.*; /** * JPEGImageReaderTest @@ -71,11 +72,11 @@ import static org.mockito.Mockito.verify; * @author last modified by $Author: haraldk$ * @version $Id: JPEGImageReaderTest.java,v 1.0 24.01.11 22.04 haraldk Exp$ */ -public class JPEGImageReaderTest extends ImageReaderAbstractTestCase { +public class JPEGImageReaderTest extends ImageReaderAbstractTest { - private static final JPEGImageReaderSpi SPI = new JPEGImageReaderSpi(lookupDelegateProvider()); + protected static final JPEGImageReaderSpi SPI = new JPEGImageReaderSpi(lookupDelegateProvider()); - private static ImageReaderSpi lookupDelegateProvider() { + protected static ImageReaderSpi lookupDelegateProvider() { return JPEGImageReaderSpi.lookupDelegateProvider(IIORegistry.getDefaultInstance()); } @@ -85,6 +86,7 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTestCase getMIMETypes() { - return Arrays.asList("image/jpeg"); + return Collections.singletonList("image/jpeg"); } // TODO: Test that subsampling is actually reading something @@ -368,7 +370,7 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTestCase(0)); + assertThat(markerSequence.getChildNodes().getLength(), new GreaterThan<>(0)); NodeList unknowns = markerSequence.getElementsByTagName("unknown"); for (int j = 0; j < unknowns.getLength(); j++) { @@ -1157,10 +1159,6 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTestCase spiClass = (Class) Class.forName("com.sun.imageio.plugins.jpeg.JPEGImageReaderSpi"); ImageReaderSpi provider = spiClass.newInstance(); - return provider.createReaderInstance(); + ImageReader reader = provider.createReaderInstance(); + assumeNotNull(reader); + return reader; } catch (Throwable t) { - System.err.println("WARNING: Could not create ImageReader for reference (missing dependency): " + t.getMessage()); - - return null; + assumeNoException(t); } + + return null; } private void assertTreesEquals(String message, Node expectedTree, Node actualTree) { @@ -1290,7 +1290,7 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTestCase sortNodes(final NodeList nodes) { - ArrayList sortedNodes = new ArrayList(new AbstractList() { + ArrayList sortedNodes = new ArrayList<>(new AbstractList() { @Override public IIOMetadataNode get(int index) { return (IIOMetadataNode) nodes.item(index); @@ -1438,7 +1438,6 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTestCaseHarald Kuhr + * @author last modified by $Author: harald.kuhr$ + * @version $Id: JPEGProviderInfoTest.java,v 1.0 02/06/16 harald.kuhr Exp$ + */ +public class JPEGProviderInfoTest extends ReaderWriterProviderInfoTest { + + @Override + protected ReaderWriterProviderInfo createProviderInfo() { + return new JPEGProviderInfo(); + } +} \ No newline at end of file diff --git a/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGSegmentImageInputStreamTest.java b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGSegmentImageInputStreamTest.java index f6af554b..2def2e06 100644 --- a/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGSegmentImageInputStreamTest.java +++ b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGSegmentImageInputStreamTest.java @@ -74,6 +74,24 @@ public class JPEGSegmentImageInputStreamTest { stream.read(); } + @Test(expected = IIOException.class) + public void testStreamNonJPEGArray() throws IOException { + ImageInputStream stream = new JPEGSegmentImageInputStream(ImageIO.createImageInputStream(new ByteArrayInputStream(new byte[] {42, 42, 0, 0, 77, 99}))); + stream.readFully(new byte[1]); + } + + @Test(expected = IIOException.class) + public void testStreamEmpty() throws IOException { + ImageInputStream stream = new JPEGSegmentImageInputStream(ImageIO.createImageInputStream(new ByteArrayInputStream(new byte[0]))); + stream.read(); + } + + @Test(expected = IIOException.class) + public void testStreamEmptyArray() throws IOException { + ImageInputStream stream = new JPEGSegmentImageInputStream(ImageIO.createImageInputStream(new ByteArrayInputStream(new byte[0]))); + stream.readFully(new byte[1]); + } + @Test public void testStreamRealData() throws IOException { ImageInputStream stream = new JPEGSegmentImageInputStream(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/invalid-icc-duplicate-sequence-numbers-rgb-internal-kodak-srgb-jfif.jpg"))); diff --git a/imageio/imageio-jpeg/src/test/resources/jpeg/corrupted-icc-srgb.jpg b/imageio/imageio-jpeg/src/test/resources/jpeg/corrupted-icc-srgb.jpg new file mode 100644 index 00000000..c775a0f7 Binary files /dev/null and b/imageio/imageio-jpeg/src/test/resources/jpeg/corrupted-icc-srgb.jpg differ diff --git a/imageio/imageio-jpeg/src/test/resources/jpeg/exif-jfif-app13-app14ycck-3channel.jpg b/imageio/imageio-jpeg/src/test/resources/jpeg/exif-jfif-app13-app14ycck-3channel.jpg new file mode 100644 index 00000000..754292d8 Binary files /dev/null and b/imageio/imageio-jpeg/src/test/resources/jpeg/exif-jfif-app13-app14ycck-3channel.jpg differ diff --git a/imageio/imageio-metadata/pom.xml b/imageio/imageio-metadata/pom.xml index 77b62d9f..40993b3f 100644 --- a/imageio/imageio-metadata/pom.xml +++ b/imageio/imageio-metadata/pom.xml @@ -3,7 +3,7 @@ com.twelvemonkeys.imageio imageio - 3.1-SNAPSHOT + 3.3-SNAPSHOT 4.0.0 imageio-metadata @@ -20,7 +20,7 @@ com.twelvemonkeys.imageio imageio-core - tests + test-jar diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/Entry.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/Entry.java index ce7c6074..be03f90c 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/Entry.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/Entry.java @@ -64,4 +64,8 @@ public interface Entry { // For arrays only int valueCount(); + + // TODO: getValueAsInt, UnsignedInt, Short, UnsignedShort, Byte, UnsignedByte etc + // TODO: getValueAsIntArray, ShortArray, ByteArray, StringArray etc (also for non-arrays, to return a single element array) + } diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/MetadataWriter.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/MetadataWriter.java new file mode 100644 index 00000000..325fbdad --- /dev/null +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/MetadataWriter.java @@ -0,0 +1,15 @@ +package com.twelvemonkeys.imageio.metadata; + +import javax.imageio.stream.ImageOutputStream; +import java.io.IOException; + +/** + * MetadataWriter. + * + * @author Harald Kuhr + * @author last modified by $Author: harald.kuhr$ + * @version $Id: MetadataWriter.java,v 1.0 28/05/15 harald.kuhr Exp$ + */ +public abstract class MetadataWriter { + abstract public boolean write(Directory directory, ImageOutputStream stream) throws IOException; +} diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFEntry.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFEntry.java index 381c0913..0c6f5403 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFEntry.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFEntry.java @@ -38,13 +38,14 @@ import com.twelvemonkeys.imageio.metadata.AbstractEntry; * @version $Id: EXIFEntry.java,v 1.0 Nov 13, 2009 5:47:35 PM haraldk Exp$ */ final class EXIFEntry extends AbstractEntry { + // TODO: Expose as TIFFEntry final private short type; EXIFEntry(final int identifier, final Object value, final short type) { super(identifier, value); - if (type < 1 || type > TIFF.TYPE_NAMES.length) { - throw new IllegalArgumentException(String.format("Illegal EXIF type: %s", type)); + if (type < 1 || type >= TIFF.TYPE_NAMES.length) { + throw new IllegalArgumentException(String.format("Illegal TIFF type: %s", type)); } // TODO: Validate that type is applicable to value? @@ -71,6 +72,8 @@ final class EXIFEntry extends AbstractEntry { return "IPTC"; case TIFF.TAG_PHOTOSHOP: return "Adobe"; + case TIFF.TAG_PHOTOSHOP_IMAGE_SOURCE_DATA: + return "ImageSourceData"; case TIFF.TAG_ICC_PROFILE: return "ICCProfile"; @@ -84,8 +87,16 @@ final class EXIFEntry extends AbstractEntry { return "Compression"; case TIFF.TAG_PHOTOMETRIC_INTERPRETATION: return "PhotometricInterpretation"; + case TIFF.TAG_FILL_ORDER: + return "FillOrder"; + case TIFF.TAG_DOCUMENT_NAME: + return "DocumentName"; case TIFF.TAG_IMAGE_DESCRIPTION: return "ImageDescription"; + case TIFF.TAG_MAKE: + return "Make"; + case TIFF.TAG_MODEL: + return "Model"; case TIFF.TAG_STRIP_OFFSETS: return "StripOffsets"; case TIFF.TAG_ORIENTATION: @@ -104,14 +115,10 @@ final class EXIFEntry extends AbstractEntry { return "PlanarConfiguration"; 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_MAKE: - return "Make"; - case TIFF.TAG_MODEL: - return "Model"; + case TIFF.TAG_PAGE_NAME: + return "PageName"; + case TIFF.TAG_PAGE_NUMBER: + return "PageNumber"; case TIFF.TAG_SOFTWARE: return "Software"; case TIFF.TAG_DATE_TIME: @@ -138,10 +145,20 @@ final class EXIFEntry extends AbstractEntry { return "YCbCrPositioning"; case TIFF.TAG_COLOR_MAP: return "ColorMap"; + case TIFF.TAG_INK_SET: + return "InkSet"; + case TIFF.TAG_INK_NAMES: + return "InkNames"; case TIFF.TAG_EXTRA_SAMPLES: return "ExtraSamples"; case TIFF.TAG_SAMPLE_FORMAT: return "SampleFormat"; + case TIFF.TAG_JPEG_TABLES: + return "JPEGTables"; + case TIFF.TAG_JPEG_INTERCHANGE_FORMAT: + return "JPEGInterchangeFormat"; + case TIFF.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH: + return "JPEGInterchangeFormatLength"; case TIFF.TAG_SUB_IFD: return "SubIFD"; @@ -176,6 +193,8 @@ final class EXIFEntry extends AbstractEntry { return "Flash"; case EXIF.TAG_FOCAL_LENGTH: return "FocalLength"; + case EXIF.TAG_SENSING_METHOD: + return "SensingMethod"; case EXIF.TAG_FILE_SOURCE: return "FileSource"; case EXIF.TAG_SCENE_TYPE: @@ -189,7 +208,7 @@ final class EXIFEntry extends AbstractEntry { case EXIF.TAG_WHITE_BALANCE: return "WhiteBalance"; case EXIF.TAG_DIGITAL_ZOOM_RATIO: - return "DigitalZoomRation"; + return "DigitalZoomRatio"; case EXIF.TAG_FOCAL_LENGTH_IN_35_MM_FILM: return "FocalLengthIn35mmFilm"; case EXIF.TAG_SCENE_CAPTURE_TYPE: @@ -202,6 +221,8 @@ final class EXIFEntry extends AbstractEntry { return "Saturation"; case EXIF.TAG_SHARPNESS: return "Sharpness"; + case EXIF.TAG_IMAGE_UNIQUE_ID: + return "ImageUniqueID"; case EXIF.TAG_FLASHPIX_VERSION: return "FlashpixVersion"; @@ -214,6 +235,8 @@ final class EXIFEntry extends AbstractEntry { return "DateTimeDigitized"; case EXIF.TAG_IMAGE_NUMBER: return "ImageNumber"; + case EXIF.TAG_MAKER_NOTE: + return "MakerNote"; case EXIF.TAG_USER_COMMENT: return "UserComment"; @@ -259,6 +282,6 @@ final class EXIFEntry extends AbstractEntry { @Override public String getTypeName() { - return TIFF.TYPE_NAMES[type - 1]; + return TIFF.TYPE_NAMES[type]; } } diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFReader.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFReader.java index ac13be42..2c872b45 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFReader.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFReader.java @@ -71,7 +71,7 @@ public final class EXIFReader extends MetadataReader { else if (bom[0] == 'M' && bom[1] == 'M') { input.setByteOrder(ByteOrder.BIG_ENDIAN); } - else { + else { throw new IIOException(String.format("Invalid TIFF byte order mark '%s', expected: 'II' or 'MM'", StringUtil.decode(bom, 0, bom.length, "ASCII"))); } @@ -79,7 +79,7 @@ public final class EXIFReader extends MetadataReader { // http://www.awaresystems.be/imaging/tiff/bigtiff.html int magic = input.readUnsignedShort(); if (magic != TIFF.TIFF_MAGIC) { - throw new IIOException(String.format("Wrong TIFF magic in EXIF data: %04x, expected: %04x", magic, TIFF.TIFF_MAGIC)); + throw new IIOException(String.format("Wrong TIFF magic in EXIF data: %04x, expected: %04x", magic, TIFF.TIFF_MAGIC)); } long directoryOffset = input.readUnsignedInt(); @@ -89,8 +89,8 @@ public final class EXIFReader extends MetadataReader { // TODO: Consider re-writing so that the linked IFD parsing is done externally to the method protected Directory readDirectory(final ImageInputStream pInput, final long pOffset, final boolean readLinked) throws IOException { - List ifds = new ArrayList(); - List entries = new ArrayList(); + List ifds = new ArrayList<>(); + List entries = new ArrayList<>(); pInput.seek(pOffset); long nextOffset = -1; @@ -105,21 +105,27 @@ public final class EXIFReader extends MetadataReader { } for (int i = 0; i < entryCount; i++) { - EXIFEntry entry = readEntry(pInput); + try { + EXIFEntry entry = readEntry(pInput); - if (entry == null) { -// System.err.println("Expected: " + entryCount + " values, found only " + i); - // TODO: Log warning? - nextOffset = 0; + if (entry != null) { + entries.add(entry); + } + } + catch (IIOException e) { break; } - - entries.add(entry); } if (readLinked) { if (nextOffset == -1) { - nextOffset = pInput.readUnsignedInt(); + try { + nextOffset = pInput.readUnsignedInt(); + } + catch (EOFException e) { + // catch EOF here as missing EOF marker + nextOffset = 0; + } } // Read linked IFDs @@ -138,7 +144,7 @@ public final class EXIFReader extends MetadataReader { ); ifds.add(0, new IFD(entries)); - + return new EXIFDirectory(ifds); } @@ -156,7 +162,7 @@ public final class EXIFReader extends MetadataReader { try { if (KNOWN_IFDS.contains(tagId)) { long[] pointerOffsets = getPointerOffsets(entry); - List subIFDs = new ArrayList(pointerOffsets.length); + List subIFDs = new ArrayList<>(pointerOffsets.length); for (long pointerOffset : pointerOffsets) { CompoundDirectory subDirectory = (CompoundDirectory) readDirectory(input, pointerOffset, false); @@ -170,15 +176,18 @@ public final class EXIFReader extends MetadataReader { // Replace the entry with parsed data entries.set(i, new EXIFEntry(tagId, subIFDs.get(0), entry.getType())); } - else { + else { // Replace the entry with parsed data entries.set(i, new EXIFEntry(tagId, subIFDs.toArray(new IFD[subIFDs.size()]), entry.getType())); } } } catch (IIOException e) { - // TODO: Issue warning without crashing...? - e.printStackTrace(); + if (DEBUG) { + // TODO: Issue warning without crashing...? + System.err.println("Error parsing sub-IFD: " + tagId); + e.printStackTrace(); + } } } } @@ -204,7 +213,9 @@ public final class EXIFReader extends MetadataReader { offsets = (long[]) value; } else { - throw new IIOException(String.format("Unknown pointer type: %s", (value != null ? value.getClass() : null))); + throw new IIOException(String.format("Unknown pointer type: %s", (value != null + ? value.getClass() + : null))); } return offsets; @@ -215,11 +226,6 @@ public final class EXIFReader extends MetadataReader { int tagId = pInput.readUnsignedShort(); short type = pInput.readShort(); - // This isn't really an entry, and the directory entry count was wrong OR bad data... - if (tagId == 0 && type == 0) { - return null; - } - int count = pInput.readInt(); // Number of values // It's probably a spec violation to have count 0, but we'll be lenient about it @@ -228,32 +234,33 @@ public final class EXIFReader extends MetadataReader { } if (type <= 0 || type > 13) { + pInput.skipBytes(4); // read Value + // Invalid tag, this is just for debugging - long offset = pInput.getStreamPosition() - 8l; + long offset = pInput.getStreamPosition() - 12l; if (DEBUG) { System.err.printf("Bad EXIF data @%08x\n", pInput.getStreamPosition()); System.err.println("tagId: " + tagId + (tagId <= 0 ? " (INVALID)" : "")); System.err.println("type: " + type + " (INVALID)"); System.err.println("count: " + count); - } - pInput.mark(); - pInput.seek(offset); + pInput.mark(); + pInput.seek(offset); - try { - byte[] bytes = new byte[8 + Math.min(120, Math.max(24, count))]; - int len = pInput.read(bytes); + try { + byte[] bytes = new byte[8 + Math.min(120, Math.max(24, count))]; + int len = pInput.read(bytes); - if (DEBUG) { - System.err.print(HexDump.dump(offset, bytes, 0, len)); - System.err.println(len < count ? "[...]" : ""); + if (DEBUG) { + System.err.print(HexDump.dump(offset, bytes, 0, len)); + System.err.println(len < count ? "[...]" : ""); + } + } + finally { + pInput.reset(); } } - finally { - pInput.reset(); - } - return null; } @@ -446,8 +453,8 @@ public final class EXIFReader extends MetadataReader { } static int getValueLength(final int pType, final int pCount) { - if (pType > 0 && pType <= TIFF.TYPE_LENGTHS.length) { - return TIFF.TYPE_LENGTHS[pType - 1] * pCount; + if (pType > 0 && pType < TIFF.TYPE_LENGTHS.length) { + return TIFF.TYPE_LENGTHS[pType] * pCount; } return -1; @@ -501,7 +508,8 @@ public final class EXIFReader extends MetadataReader { ////////////////////// // TODO: Stream based hex dump util? public static class HexDump { - private HexDump() {} + private HexDump() { + } private static final int WIDTH = 32; @@ -515,7 +523,7 @@ public final class EXIFReader extends MetadataReader { int i; for (i = 0; i < len; i++) { if (i % WIDTH == 0) { - if (i > 0 ) { + if (i > 0) { builder.append("\n"); } builder.append(String.format("%08x: ", i + off + offset)); diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFWriter.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFWriter.java index 1983ac04..af0ac041 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFWriter.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFWriter.java @@ -31,6 +31,7 @@ package com.twelvemonkeys.imageio.metadata.exif; import com.twelvemonkeys.imageio.metadata.CompoundDirectory; import com.twelvemonkeys.imageio.metadata.Directory; import com.twelvemonkeys.imageio.metadata.Entry; +import com.twelvemonkeys.imageio.metadata.MetadataWriter; import com.twelvemonkeys.lang.Validate; import javax.imageio.IIOException; @@ -39,6 +40,7 @@ import java.io.IOException; import java.lang.reflect.Array; import java.nio.ByteOrder; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.util.*; /** @@ -48,7 +50,7 @@ import java.util.*; * @author last modified by $Author: haraldk$ * @version $Id: EXIFWriter.java,v 1.0 17.07.13 10:20 haraldk Exp$ */ -public class EXIFWriter { +public final class EXIFWriter extends MetadataWriter { static final int WORD_LENGTH = 2; static final int LONGWORD_LENGTH = 4; @@ -58,6 +60,7 @@ public class EXIFWriter { return write(new IFD(entries), stream); } + @Override public boolean write(final Directory directory, final ImageOutputStream stream) throws IOException { Validate.notNull(directory); Validate.notNull(stream); @@ -92,11 +95,11 @@ public class EXIFWriter { stream.writeShort(42); } - public long writeIFD(final Collection entries, ImageOutputStream stream) throws IOException { + public long writeIFD(final Collection entries, final ImageOutputStream stream) throws IOException { return writeIFD(new IFD(entries), stream, false); } - private long writeIFD(final Directory original, ImageOutputStream stream, boolean isSubIFD) throws IOException { + private long writeIFD(final Directory original, final ImageOutputStream stream, final boolean isSubIFD) throws IOException { // TIFF spec says tags should be in increasing order, enforce that when writing Directory ordered = ensureOrderedDirectory(original); @@ -155,6 +158,10 @@ public class EXIFWriter { return WORD_LENGTH + computeDataSize(new IFD(directory)) + directory.size() * ENTRY_LENGTH; } + public long computeIFDOffsetSize(final Collection directory) { + return computeDataSize(new IFD(directory)) + LONGWORD_LENGTH; + } + private long computeDataSize(final Directory directory) { long dataSize = 0; @@ -181,7 +188,7 @@ public class EXIFWriter { private Directory ensureOrderedDirectory(final Directory directory) { if (!isSorted(directory)) { - List entries = new ArrayList(directory.size()); + List entries = new ArrayList<>(directory.size()); for (Entry entry : directory) { entries.add(entry); @@ -215,7 +222,7 @@ public class EXIFWriter { return true; } - private long writeValue(Entry entry, long dataOffset, ImageOutputStream stream) throws IOException { + private long writeValue(final Entry entry, final long dataOffset, final ImageOutputStream stream) throws IOException { short type = getType(entry); int valueLength = EXIFReader.getValueLength(type, getCount(entry)); @@ -236,18 +243,22 @@ public class EXIFWriter { } } - private int getCount(Entry entry) { + private int getCount(final Entry entry) { Object value = entry.getValue(); return value instanceof String ? ((String) value).getBytes(Charset.forName("UTF-8")).length + 1 : entry.valueCount(); } - private void writeValueInline(Object value, short type, ImageOutputStream stream) throws IOException { + private void writeValueInline(final Object value, final short type, final ImageOutputStream stream) throws IOException { if (value.getClass().isArray()) { switch (type) { + case TIFF.TYPE_UNDEFINED: case TIFF.TYPE_BYTE: + case TIFF.TYPE_SBYTE: stream.write((byte[]) value); break; + case TIFF.TYPE_SHORT: + case TIFF.TYPE_SSHORT: short[] shorts; if (value instanceof short[]) { @@ -276,7 +287,9 @@ public class EXIFWriter { stream.writeShorts(shorts, 0, shorts.length); break; + case TIFF.TYPE_LONG: + case TIFF.TYPE_SLONG: int[] ints; if (value instanceof int[]) { @@ -291,21 +304,49 @@ public class EXIFWriter { } } else { - throw new IllegalArgumentException("Unsupported type for TIFF SHORT: " + value.getClass()); + throw new IllegalArgumentException("Unsupported type for TIFF LONG: " + value.getClass()); } stream.writeInts(ints, 0, ints.length); - break; case TIFF.TYPE_RATIONAL: + case TIFF.TYPE_SRATIONAL: Rational[] rationals = (Rational[]) value; for (Rational rational : rationals) { stream.writeInt((int) rational.numerator()); stream.writeInt((int) rational.denominator()); } - // TODO: More types + break; + + case TIFF.TYPE_FLOAT: + float[] floats; + + if (value instanceof float[]) { + floats = (float[]) value; + } + else { + throw new IllegalArgumentException("Unsupported type for TIFF FLOAT: " + value.getClass()); + } + + stream.writeFloats(floats, 0, floats.length); + + break; + + case TIFF.TYPE_DOUBLE: + double[] doubles; + + if (value instanceof double[]) { + doubles = (double[]) value; + } + else { + throw new IllegalArgumentException("Unsupported type for TIFF FLOAT: " + value.getClass()); + } + + stream.writeDoubles(doubles, 0, doubles.length); + + break; default: throw new IllegalArgumentException("Unsupported TIFF type: " + type); @@ -317,25 +358,35 @@ public class EXIFWriter { else { switch (type) { case TIFF.TYPE_BYTE: - stream.writeByte((Integer) value); + case TIFF.TYPE_SBYTE: + case TIFF.TYPE_UNDEFINED: + stream.writeByte(((Number) value).intValue()); break; case TIFF.TYPE_ASCII: - byte[] bytes = ((String) value).getBytes(Charset.forName("UTF-8")); + byte[] bytes = ((String) value).getBytes(StandardCharsets.UTF_8); stream.write(bytes); stream.write(0); break; case TIFF.TYPE_SHORT: - stream.writeShort((Integer) value); + case TIFF.TYPE_SSHORT: + stream.writeShort(((Number) value).intValue()); break; case TIFF.TYPE_LONG: + case TIFF.TYPE_SLONG: stream.writeInt(((Number) value).intValue()); break; case TIFF.TYPE_RATIONAL: + case TIFF.TYPE_SRATIONAL: Rational rational = (Rational) value; stream.writeInt((int) rational.numerator()); stream.writeInt((int) rational.denominator()); break; - // TODO: More types + case TIFF.TYPE_FLOAT: + stream.writeFloat(((Number) value).floatValue()); + break; + case TIFF.TYPE_DOUBLE: + stream.writeDouble(((Number) value).doubleValue()); + break; default: throw new IllegalArgumentException("Unsupported TIFF type: " + type); @@ -343,7 +394,7 @@ public class EXIFWriter { } } - private void writeValueAt(long dataOffset, Object value, short type, ImageOutputStream stream) throws IOException { + private void writeValueAt(final long dataOffset, final Object value, final short type, final ImageOutputStream stream) throws IOException { stream.writeInt(assertIntegerOffset(dataOffset)); long position = stream.getStreamPosition(); stream.seek(dataOffset); @@ -351,12 +402,27 @@ public class EXIFWriter { stream.seek(position); } - private short getType(Entry entry) { + private short getType(final Entry entry) { + // TODO: What a MESS! Rewrite and expose EXIFEntry as TIFFEntry or so... + + // For internal entries use type directly if (entry instanceof EXIFEntry) { EXIFEntry exifEntry = (EXIFEntry) entry; return exifEntry.getType(); } + // For other entries, use name if it matches + String typeName = entry.getTypeName(); + + if (typeName != null) { + for (int i = 1; i < TIFF.TYPE_NAMES.length; i++) { + if (typeName.equals(TIFF.TYPE_NAMES[i])) { + return (short) i; + } + } + } + + // Otherwise, fall back to the native Java type Object value = Validate.notNull(entry.getValue()); boolean array = value.getClass().isArray(); diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/TIFF.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/TIFF.java index d169e690..aeca1fd0 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/TIFF.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/TIFF.java @@ -88,13 +88,16 @@ public interface TIFF { Should probably all map to Java long (and fail if high bit is set for the unsigned types???) */ String[] TYPE_NAMES = { + null, "BYTE", "ASCII", "SHORT", "LONG", "RATIONAL", "SBYTE", "UNDEFINED", "SSHORT", "SLONG", "SRATIONAL", "FLOAT", "DOUBLE", "IFD", null, null, "LONG8", "SLONG8", "IFD8" }; + /** Length of the corresponding type, in bytes. */ int[] TYPE_LENGTHS = { + -1, 1, 1, 2, 4, 8, 1, 1, 2, 4, 8, 4, 8, 4, @@ -124,6 +127,8 @@ public interface TIFF { int TAG_YCBCR_POSITIONING = 531; int TAG_X_RESOLUTION = 282; int TAG_Y_RESOLUTION = 283; + int TAG_X_POSITION = 286; + int TAG_Y_POSITION = 287; int TAG_RESOLUTION_UNIT = 296; /// B. Tags relating to recording offset @@ -131,9 +136,13 @@ public interface TIFF { int TAG_STRIP_OFFSETS = 273; int TAG_ROWS_PER_STRIP = 278; int TAG_STRIP_BYTE_COUNTS = 279; + int TAG_FREE_OFFSETS = 288; // "Not recommended for general interchange." // "Old-style" JPEG (still used as EXIF thumbnail) int TAG_JPEG_INTERCHANGE_FORMAT = 513; int TAG_JPEG_INTERCHANGE_FORMAT_LENGTH = 514; + + int TAG_GROUP3OPTIONS = 292; + int TAG_GROUP4OPTIONS = 293; /// C. Tags relating to image data characteristics @@ -153,9 +162,11 @@ public interface TIFF { /// D. Other tags int TAG_DATE_TIME = 306; + int TAG_DOCUMENT_NAME = 269; int TAG_IMAGE_DESCRIPTION = 270; int TAG_MAKE = 271; int TAG_MODEL = 272; + int TAG_PAGE_NAME = 285; int TAG_PAGE_NUMBER = 297; int TAG_SOFTWARE = 305; int TAG_ARTIST = 315; @@ -184,6 +195,18 @@ public interface TIFF { */ int TAG_PHOTOSHOP = 34377; + /** + * Photoshop layer and mask information (byte order follows TIFF container). + * Layer and mask information found in a typical layered Photoshop file. + * Starts with a character string of "Adobe Photoshop Document Data Block" + * (or "Adobe Photoshop Document Data V0002" for PSB) + * including the null termination character. + * @see com.twelvemonkeys.imageio.metadata.psd.PSD + */ + int TAG_PHOTOSHOP_IMAGE_SOURCE_DATA = 37724; + + int TAG_PHOTOSHOP_ANNOTATIONS = 50255; + /** * ICC Color Profile. * @see java.awt.color.ICC_Profile diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/iptc/IPTC.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/iptc/IPTC.java index a149aa80..adee2645 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/iptc/IPTC.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/iptc/IPTC.java @@ -36,110 +36,115 @@ package com.twelvemonkeys.imageio.metadata.iptc; * @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; + int ENVELOPE_RECORD = 1 << 8; + int APPLICATION_RECORD = 2 << 8; - static final int TAG_CODED_CHARACTER_SET = ENVELOPE_RECORD | 90; + /** 1:05: Destination */ + int TAG_DESTINATION = ENVELOPE_RECORD | 5; + /** 1:50: Product ID */ + int TAG_PRODUCT_ID = ENVELOPE_RECORD | 50; + /** 1:90: Coded Character Set */ + int TAG_CODED_CHARACTER_SET = ENVELOPE_RECORD | 90; /** 2:00 Record Version (mandatory) */ - public static final int TAG_RECORD_VERSION = APPLICATION_RECORD; // 0x0200 + int TAG_RECORD_VERSION = APPLICATION_RECORD; // 0x0200 /** 2:03 Object Type Reference */ - public static final int TAG_OBJECT_TYPE_REFERENCE = APPLICATION_RECORD | 3; + 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; + int TAG_OBJECT_ATTRIBUTE_REFERENCE = APPLICATION_RECORD | 4; /** 2:05 Object Name */ - public static final int TAG_OBJECT_NAME = APPLICATION_RECORD | 5; // 0x0205 + int TAG_OBJECT_NAME = APPLICATION_RECORD | 5; // 0x0205 /** 2:07 Edit Status */ - public static final int TAG_EDIT_STATUS = APPLICATION_RECORD | 7; + int TAG_EDIT_STATUS = APPLICATION_RECORD | 7; /** 2:08 Editorial Update */ - public static final int TAG_EDITORIAL_UPDATE = APPLICATION_RECORD | 8; + int TAG_EDITORIAL_UPDATE = APPLICATION_RECORD | 8; /** 2:10 Urgency */ - public static final int TAG_URGENCY = APPLICATION_RECORD | 10; + int TAG_URGENCY = APPLICATION_RECORD | 10; /** 2:12 Subect Reference (repeatable) */ - public static final int TAG_SUBJECT_REFERENCE = APPLICATION_RECORD | 12; + int TAG_SUBJECT_REFERENCE = APPLICATION_RECORD | 12; /** 2:15 Category */ - public static final int TAG_CATEGORY = APPLICATION_RECORD | 15; // 0x020f + int TAG_CATEGORY = APPLICATION_RECORD | 15; // 0x020f /** 2:20 Supplemental Category (repeatable) */ - public static final int TAG_SUPPLEMENTAL_CATEGORIES = APPLICATION_RECORD | 20; + int TAG_SUPPLEMENTAL_CATEGORIES = APPLICATION_RECORD | 20; /** 2:22 Fixture Identifier */ - public static final int TAG_FIXTURE_IDENTIFIER = APPLICATION_RECORD | 22; + int TAG_FIXTURE_IDENTIFIER = APPLICATION_RECORD | 22; /** 2:25 Keywords (repeatable) */ - public static final int TAG_KEYWORDS = APPLICATION_RECORD | 25; + int TAG_KEYWORDS = APPLICATION_RECORD | 25; /** 2:26 Content Locataion Code (repeatable) */ - public static final int TAG_CONTENT_LOCATION_CODE = APPLICATION_RECORD | 26; + 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; + int TAG_CONTENT_LOCATION_NAME = APPLICATION_RECORD | 27; /** 2:30 Release Date */ - public static final int TAG_RELEASE_DATE = APPLICATION_RECORD | 30; + int TAG_RELEASE_DATE = APPLICATION_RECORD | 30; /** 2:35 Release Time */ - public static final int TAG_RELEASE_TIME = APPLICATION_RECORD | 35; + int TAG_RELEASE_TIME = APPLICATION_RECORD | 35; /** 2:37 Expiration Date */ - public static final int TAG_EXPIRATION_DATE = APPLICATION_RECORD | 37; + int TAG_EXPIRATION_DATE = APPLICATION_RECORD | 37; /** 2:38 Expiration Time */ - public static final int TAG_EXPIRATION_TIME = APPLICATION_RECORD | 38; + int TAG_EXPIRATION_TIME = APPLICATION_RECORD | 38; /** 2:40 Special Instructions */ - public static final int TAG_SPECIAL_INSTRUCTIONS = APPLICATION_RECORD | 40; // 0x0228 + 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; + 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; + 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; + 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; + int TAG_REFERENCE_NUMBER = APPLICATION_RECORD | 50; /** 2:55 Date Created */ - public static final int TAG_DATE_CREATED = APPLICATION_RECORD | 55; // 0x0237 + int TAG_DATE_CREATED = APPLICATION_RECORD | 55; // 0x0237 /** 2:60 Time Created */ - public static final int TAG_TIME_CREATED = APPLICATION_RECORD | 60; + int TAG_TIME_CREATED = APPLICATION_RECORD | 60; /** 2:62 Digital Creation Date */ - public static final int TAG_DIGITAL_CREATION_DATE = APPLICATION_RECORD | 62; + int TAG_DIGITAL_CREATION_DATE = APPLICATION_RECORD | 62; /** 2:63 Digital Creation Date */ - public static final int TAG_DIGITAL_CREATION_TIME = APPLICATION_RECORD | 63; + int TAG_DIGITAL_CREATION_TIME = APPLICATION_RECORD | 63; /** 2:65 Originating Program */ - public static final int TAG_ORIGINATING_PROGRAM = APPLICATION_RECORD | 65; + 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; + 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; + int TAG_OBJECT_CYCLE = APPLICATION_RECORD | 75; /** 2:80 By-line (repeatable) */ - public static final int TAG_BY_LINE = APPLICATION_RECORD | 80; // 0x0250 + 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 + int TAG_BY_LINE_TITLE = APPLICATION_RECORD | 85; // 0x0255 /** 2:90 City */ - public static final int TAG_CITY = APPLICATION_RECORD | 90; // 0x025a + int TAG_CITY = APPLICATION_RECORD | 90; // 0x025a /** 2:92 Sub-location */ - public static final int TAG_SUB_LOCATION = APPLICATION_RECORD | 92; + int TAG_SUB_LOCATION = APPLICATION_RECORD | 92; /** 2:95 Province/State */ - public static final int TAG_PROVINCE_OR_STATE = APPLICATION_RECORD | 95; // 0x025f + 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; + 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 + 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 + int TAG_ORIGINAL_TRANSMISSION_REFERENCE = APPLICATION_RECORD | 103; // 0x0267 /** 2:105 Headline */ - public static final int TAG_HEADLINE = APPLICATION_RECORD | 105; // 0x0269 + int TAG_HEADLINE = APPLICATION_RECORD | 105; // 0x0269 /** 2:110 Credit */ - public static final int TAG_CREDIT = APPLICATION_RECORD | 110; // 0x026e + int TAG_CREDIT = APPLICATION_RECORD | 110; // 0x026e /** 2:115 Source */ - public static final int TAG_SOURCE = APPLICATION_RECORD | 115; // 0x0273 + int TAG_SOURCE = APPLICATION_RECORD | 115; // 0x0273 /** 2:116 Copyright Notice */ - public static final int TAG_COPYRIGHT_NOTICE = APPLICATION_RECORD | 116; // 0x0274 + int TAG_COPYRIGHT_NOTICE = APPLICATION_RECORD | 116; // 0x0274 /** 2:118 Contact */ - public static final int TAG_CONTACT = APPLICATION_RECORD | 118; + int TAG_CONTACT = APPLICATION_RECORD | 118; /** 2:120 Catption/Abstract */ - public static final int TAG_CAPTION = APPLICATION_RECORD | 120; // 0x0278 + int TAG_CAPTION = APPLICATION_RECORD | 120; // 0x0278 /** 2:122 Writer/Editor (repeatable) */ - public static final int TAG_WRITER = APPLICATION_RECORD | 122; // 0x027a + int TAG_WRITER = APPLICATION_RECORD | 122; // 0x027a /** 2:125 Rasterized Caption (binary data) */ - public static final int TAG_RASTERIZED_CATPTION = APPLICATION_RECORD | 125; + int TAG_RASTERIZED_CATPTION = APPLICATION_RECORD | 125; /** 2:130 Image Type */ - public static final int TAG_IMAGE_TYPE = APPLICATION_RECORD | 130; + int TAG_IMAGE_TYPE = APPLICATION_RECORD | 130; /** 2:131 Image Orientation */ - public static final int TAG_IMAGE_ORIENTATION = APPLICATION_RECORD | 131; + int TAG_IMAGE_ORIENTATION = APPLICATION_RECORD | 131; /** 2:135 Language Identifier */ - public static final int TAG_LANGUAGE_IDENTIFIER = APPLICATION_RECORD | 135; + int TAG_LANGUAGE_IDENTIFIER = APPLICATION_RECORD | 135; // TODO: 2:150-2:154 Audio @@ -150,9 +155,28 @@ public interface IPTC { * * @see JobMinder Homepage */ - static final int CUSTOM_TAG_JOBMINDER_ASSIGNMENT_DATA = APPLICATION_RECORD | 199; + 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 + + final class Tags { + static boolean isArray(final short tagId) { + switch (tagId) { + case IPTC.TAG_DESTINATION: + case IPTC.TAG_PRODUCT_ID: + case IPTC.TAG_SUBJECT_REFERENCE: + case IPTC.TAG_SUPPLEMENTAL_CATEGORIES: + case IPTC.TAG_KEYWORDS: + case IPTC.TAG_CONTENT_LOCATION_CODE: + case IPTC.TAG_CONTENT_LOCATION_NAME: + case IPTC.TAG_BY_LINE: + return true; + + default: + return false; + } + } + } } diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/iptc/IPTCDirectory.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/iptc/IPTCDirectory.java index 133f07bc..54a644ca 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/iptc/IPTCDirectory.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/iptc/IPTCDirectory.java @@ -42,6 +42,7 @@ import java.util.Collection; */ final class IPTCDirectory extends AbstractDirectory { IPTCDirectory(final Collection entries) { + // TODO: Normalize multiple entries with same key to single entry w/array super(entries); } } \ No newline at end of file diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/iptc/IPTCEntry.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/iptc/IPTCEntry.java index 6bca3f91..956b6ac1 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/iptc/IPTCEntry.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/iptc/IPTCEntry.java @@ -47,6 +47,30 @@ class IPTCEntry extends AbstractEntry { switch ((Integer) getIdentifier()) { case IPTC.TAG_RECORD_VERSION: return "RecordVersion"; + case IPTC.TAG_KEYWORDS: + return "Keywords"; + case IPTC.TAG_SPECIAL_INSTRUCTIONS: + return "Instructions"; + case IPTC.TAG_DIGITAL_CREATION_DATE: + return "DigitalCreationDate"; + case IPTC.TAG_DIGITAL_CREATION_TIME: + return "DigitalCreationTime"; + case IPTC.TAG_DATE_CREATED: + return "DateCreated"; + case IPTC.TAG_TIME_CREATED: + return "TimeCreated"; + case IPTC.TAG_BY_LINE_TITLE: + return "ByLineTitle"; + case IPTC.TAG_CITY: + return "City"; + case IPTC.TAG_SUB_LOCATION: + return "SubLocation"; + case IPTC.TAG_PROVINCE_OR_STATE: + return "StateProvince"; + case IPTC.TAG_COUNTRY_OR_PRIMARY_LOCATION_CODE: + return "CountryCode"; + case IPTC.TAG_COUNTRY_OR_PRIMARY_LOCATION: + return "Country"; case IPTC.TAG_SOURCE: return "Source"; case IPTC.TAG_CAPTION: diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/iptc/IPTCReader.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/iptc/IPTCReader.java index a9fad969..34a8f1a5 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/iptc/IPTCReader.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/iptc/IPTCReader.java @@ -43,8 +43,9 @@ 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; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.Map; /** * IPTCReader @@ -60,61 +61,88 @@ public final class IPTCReader extends MetadataReader { private int encoding = ENCODING_UNSPECIFIED; - @Override public Directory read(final ImageInputStream input) throws IOException { Validate.notNull(input, "input"); - List entries = new ArrayList(); + Map entries = new LinkedHashMap<>(); // 0x1c identifies start of a tag while (input.read() == 0x1c) { short tagId = input.readShort(); int tagByteCount = input.readUnsignedShort(); - Entry entry = readEntry(input, tagId, tagByteCount); + + boolean array = IPTC.Tags.isArray(tagId); + Entry entry = readEntry(input, tagId, tagByteCount, array, array ? entries.get(tagId) : null); if (entry != null) { - entries.add(entry); + entries.put(tagId, entry); } } - return new IPTCDirectory(entries); + return new IPTCDirectory(entries.values()); } - private IPTCEntry readEntry(final ImageInputStream pInput, final short pTagId, final int pLength) throws IOException { - Object value = null; + private IPTCEntry mergeEntries(final short tagId, final Object newValue, final Entry oldEntry) { + Object[] oldValue = oldEntry != null ? (Object[]) oldEntry.getValue() : null; + Object[] value; + + if (newValue instanceof String) { + if (oldValue == null) { + value = new String[] {(String) newValue}; + } + else { + String[] array = (String[]) oldValue; + value = Arrays.copyOf(array, array.length + 1); + value[value.length - 1] = newValue; + } + } + else { + if (oldValue == null) { + value = new Object[] {newValue}; + } + else { + value = Arrays.copyOf(oldValue, oldValue.length + 1); + value [value .length - 1] = newValue; + } + } + + return new IPTCEntry(tagId, value); + } + + private IPTCEntry readEntry(final ImageInputStream pInput, final short pTagId, final int pLength, final boolean array, final Entry oldEntry) throws IOException { + Object value; switch (pTagId) { case IPTC.TAG_CODED_CHARACTER_SET: // TODO: Mapping from ISO 646 to Java supported character sets? - // TODO: Move somewhere else? encoding = parseEncoding(pInput, pLength); return null; case IPTC.TAG_RECORD_VERSION: + // TODO: Assert length == 2? // 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; + // TODO: Create Tags.getType(tag), to allow for more flexible types + if ((pTagId & 0xff00) == IPTC.APPLICATION_RECORD) { + // Treat Application records as Strings + if (pLength < 1) { + value = null; + } + else { + value = parseString(pInput, pLength); + } + } + else { + // Non-Application fields, typically not human readable + byte[] data = new byte[pLength]; + pInput.readFully(data); + value = data; } - - // 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); + return array ? mergeEntries(pTagId, value, oldEntry) : new IPTCEntry(pTagId, value); } private int parseEncoding(final ImageInputStream pInput, int tagByteCount) throws IOException { @@ -148,7 +176,7 @@ public final class IPTCReader extends MetadataReader { } // Fall back to use ISO-8859-1 - // This will not fail, but may may create wrong fallback-characters + // This will not fail, but may create wrong fallback-characters return StringUtil.decode(data, 0, data.length, "ISO8859_1"); } } diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/iptc/IPTCWriter.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/iptc/IPTCWriter.java new file mode 100644 index 00000000..11904a7f --- /dev/null +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/iptc/IPTCWriter.java @@ -0,0 +1,69 @@ +package com.twelvemonkeys.imageio.metadata.iptc; + +import com.twelvemonkeys.imageio.metadata.Directory; +import com.twelvemonkeys.imageio.metadata.Entry; +import com.twelvemonkeys.imageio.metadata.MetadataWriter; + +import javax.imageio.stream.ImageOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import static com.twelvemonkeys.lang.Validate.notNull; + +/** + * IPTCWriter. + * + * @author Harald Kuhr + * @author last modified by $Author: harald.kuhr$ + * @version $Id: IPTCWriter.java,v 1.0 28/05/15 harald.kuhr Exp$ + */ +public final class IPTCWriter extends MetadataWriter { + @Override + public boolean write(final Directory directory, final ImageOutputStream stream) throws IOException { + notNull(directory, "directory"); + notNull(stream, "stream"); + + // TODO: Make sure we always write application record version (2.00) + // TODO: Write encoding UTF8? + + for (Entry entry : directory) { + int tag = (Integer) entry.getIdentifier(); + Object value = entry.getValue(); + + if (IPTC.Tags.isArray((short) tag)) { + Object[] values = (Object[]) value; + + for (Object v : values) { + stream.write(0x1c); + stream.writeShort(tag); + writeValue(stream, v); + } + } + else { + stream.write(0x1c); + stream.writeShort(tag); + writeValue(stream, value); + } + } + + return false; + } + + private void writeValue(final ImageOutputStream stream, final Object value) throws IOException { + if (value instanceof String) { + byte[] data = ((String) value).getBytes(StandardCharsets.UTF_8); + stream.writeShort(data.length); + stream.write(data); + } + else if (value instanceof byte[]) { + byte[] data = (byte[]) value; + stream.writeShort(data.length); + stream.write(data); + } + else if (value instanceof Integer) { + // TODO: Need to know types from tag + stream.writeShort(2); + stream.writeShort((Integer) value); + } + } +} diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/psd/PSDEntry.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/psd/PSDEntry.java index b89d4eb4..f07a4453 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/psd/PSDEntry.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/psd/PSDEntry.java @@ -66,7 +66,8 @@ class PSDEntry extends AbstractEntry { field.setAccessible(true); if (field.get(null).equals(getIdentifier())) { - return StringUtil.lispToCamel(field.getName().substring(4).replace("_", "-").toLowerCase(), true); + String fieldName = StringUtil.lispToCamel(field.getName().substring(4).replace("_", "-").toLowerCase(), true); + return name != null ? fieldName + ": " + name : fieldName; } } } diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/psd/PSDReader.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/psd/PSDReader.java index f6e67a46..a8904a50 100755 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/psd/PSDReader.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/psd/PSDReader.java @@ -59,7 +59,7 @@ public final class PSDReader extends MetadataReader { public Directory read(final ImageInputStream input) throws IOException { Validate.notNull(input, "input"); - List entries = new ArrayList(); + List entries = new ArrayList<>(); while (true) { try { diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/xmp/XMPReader.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/xmp/XMPReader.java index 642ecf7f..28abce4f 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/xmp/XMPReader.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/xmp/XMPReader.java @@ -71,6 +71,7 @@ public final class XMPReader extends MetadataReader { // TODO: Consider parsing using SAX? // TODO: Determine encoding and parse using a Reader... // TODO: Refactor scanner to return inputstream? + // TODO: Be smarter about ASCII-NULL termination/padding (the SAXParser aka Xerces DOMParser doesn't like it)... DocumentBuilder builder = factory.newDocumentBuilder(); Document document = builder.parse(new InputSource(IIOUtil.createStreamAdapter(input))); diff --git a/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/MetadataReaderAbstractTest.java b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/MetadataReaderAbstractTest.java index 189d6c28..24c36fa7 100644 --- a/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/MetadataReaderAbstractTest.java +++ b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/MetadataReaderAbstractTest.java @@ -79,7 +79,7 @@ public abstract class MetadataReaderAbstractTest { assertNotNull(directory); } - protected final Matcher hasValue(final Object value) { + protected static Matcher hasValue(final Object value) { return new EntryHasValue(value); } diff --git a/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/MetadataWriterAbstractTest.java b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/MetadataWriterAbstractTest.java new file mode 100644 index 00000000..845ebc97 --- /dev/null +++ b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/MetadataWriterAbstractTest.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2012, 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.imageio.stream.URLImageInputStreamSpi; +import org.junit.Test; + +import javax.imageio.ImageIO; +import javax.imageio.spi.IIORegistry; +import javax.imageio.stream.ImageInputStream; +import javax.imageio.stream.MemoryCacheImageOutputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.ArrayList; + +/** + * ReaderAbstractTest + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: ReaderAbstractTest.java,v 1.0 04.01.12 09:40 haraldk Exp$ + */ +public abstract class MetadataWriterAbstractTest { + static { + IIORegistry.getDefaultInstance().registerServiceProvider(new URLImageInputStreamSpi()); + ImageIO.setUseCache(false); + } + + protected final URL getResource(final String name) throws IOException { + return getClass().getResource(name); + } + + protected final ImageInputStream getDataAsIIS() throws IOException { + return ImageIO.createImageInputStream(getData()); + } + + protected abstract InputStream getData() throws IOException; + + protected abstract MetadataWriter createWriter(); + + @Test(expected = IllegalArgumentException.class) + public void testWriteNullDirectory() throws IOException { + createWriter().write(null, new MemoryCacheImageOutputStream(new ByteArrayOutputStream())); + } + + @Test(expected = IllegalArgumentException.class) + public void testWriteNullStream() throws IOException { + createWriter().write(new AbstractDirectory(new ArrayList()) { + }, null); + } +} diff --git a/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/exif/EXIFReaderTest.java b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/exif/EXIFReaderTest.java index 1249de6c..2d2468ac 100644 --- a/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/exif/EXIFReaderTest.java +++ b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/exif/EXIFReaderTest.java @@ -276,4 +276,20 @@ public class EXIFReaderTest extends MetadataReaderAbstractTest { assertNotNull(interop); assertEquals(0, interop.size()); } + + @Test + public void testReadExifWithMissingEOFMarker() throws IOException { + try (ImageInputStream stream = ImageIO.createImageInputStream(getResource("/exif/noeof.tif"))) { + CompoundDirectory directory = (CompoundDirectory) createReader().read(stream); + assertEquals(15, directory.size()); + assertEquals(1, directory.directoryCount()); + } + } + + public void testReadExifWithEmptyTag() throws IOException { + try (ImageInputStream stream = ImageIO.createImageInputStream(getResource("/exif/emptyexiftag.tif"))) { + CompoundDirectory directory = (CompoundDirectory) createReader().read(stream); + assertEquals(3, directory.directoryCount()); + } + } } diff --git a/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/exif/EXIFWriterTest.java b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/exif/EXIFWriterTest.java index a425a7c7..f682fd00 100644 --- a/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/exif/EXIFWriterTest.java +++ b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/exif/EXIFWriterTest.java @@ -28,24 +28,17 @@ package com.twelvemonkeys.imageio.metadata.exif; -import com.twelvemonkeys.imageio.metadata.AbstractDirectory; -import com.twelvemonkeys.imageio.metadata.AbstractEntry; -import com.twelvemonkeys.imageio.metadata.Directory; -import com.twelvemonkeys.imageio.metadata.Entry; +import com.twelvemonkeys.imageio.metadata.*; import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream; -import com.twelvemonkeys.imageio.stream.URLImageInputStreamSpi; import com.twelvemonkeys.io.FastByteArrayOutputStream; import org.junit.Test; import javax.imageio.ImageIO; -import javax.imageio.spi.IIORegistry; -import javax.imageio.stream.ImageInputStream; import javax.imageio.stream.ImageOutputStream; import javax.imageio.stream.ImageOutputStreamImpl; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; -import java.net.URL; import java.nio.ByteOrder; import java.util.ArrayList; import java.util.Collections; @@ -61,37 +54,25 @@ import static org.junit.Assert.assertNotNull; * @author last modified by $Author: haraldk$ * @version $Id: EXIFWriterTest.java,v 1.0 18.07.13 09:53 haraldk Exp$ */ -public class EXIFWriterTest { - static { - IIORegistry.getDefaultInstance().registerServiceProvider(new URLImageInputStreamSpi()); - ImageIO.setUseCache(false); - } +public class EXIFWriterTest extends MetadataWriterAbstractTest { - protected final URL getResource(final String name) throws IOException { - return getClass().getResource(name); - } - - protected final ImageInputStream getDataAsIIS() throws IOException { - return ImageIO.createImageInputStream(getData()); - } - - // @Override + @Override protected InputStream getData() throws IOException { return getResource("/exif/exif-jpeg-segment.bin").openStream(); } -// @Override protected EXIFReader createReader() { return new EXIFReader(); } + @Override protected EXIFWriter createWriter() { return new EXIFWriter(); } @Test public void testWriteReadSimple() throws IOException { - ArrayList entries = new ArrayList(); + ArrayList entries = new ArrayList<>(); entries.add(new EXIFEntry(TIFF.TAG_ORIENTATION, 1, TIFF.TYPE_SHORT)); entries.add(new EXIFEntry(TIFF.TAG_IMAGE_WIDTH, 1600, TIFF.TYPE_SHORT)); entries.add(new AbstractEntry(TIFF.TAG_IMAGE_HEIGHT, 900) {}); @@ -139,7 +120,7 @@ public class EXIFWriterTest { @Test public void testWriteMotorola() throws IOException { - ArrayList entries = new ArrayList(); + ArrayList entries = new ArrayList<>(); entries.add(new AbstractEntry(TIFF.TAG_SOFTWARE, "TwelveMonkeys ImageIO") {}); entries.add(new EXIFEntry(TIFF.TAG_IMAGE_WIDTH, Integer.MAX_VALUE, TIFF.TYPE_LONG)); Directory directory = new AbstractDirectory(entries) {}; @@ -174,7 +155,7 @@ public class EXIFWriterTest { @Test public void testWriteIntel() throws IOException { - ArrayList entries = new ArrayList(); + ArrayList entries = new ArrayList<>(); entries.add(new AbstractEntry(TIFF.TAG_SOFTWARE, "TwelveMonkeys ImageIO") {}); entries.add(new EXIFEntry(TIFF.TAG_IMAGE_WIDTH, Integer.MAX_VALUE, TIFF.TYPE_LONG)); Directory directory = new AbstractDirectory(entries) {}; @@ -254,7 +235,7 @@ public class EXIFWriterTest { @Test public void testComputeIFDSize() throws IOException { - ArrayList entries = new ArrayList(); + ArrayList entries = new ArrayList<>(); entries.add(new EXIFEntry(TIFF.TAG_ORIENTATION, 1, TIFF.TYPE_SHORT)); entries.add(new EXIFEntry(TIFF.TAG_IMAGE_WIDTH, 1600, TIFF.TYPE_SHORT)); entries.add(new AbstractEntry(TIFF.TAG_IMAGE_HEIGHT, 900) {}); diff --git a/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/iptc/IPTCWriterTest.java b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/iptc/IPTCWriterTest.java new file mode 100644 index 00000000..e5f277db --- /dev/null +++ b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/iptc/IPTCWriterTest.java @@ -0,0 +1,71 @@ +package com.twelvemonkeys.imageio.metadata.iptc; + +import com.twelvemonkeys.imageio.metadata.Directory; +import com.twelvemonkeys.imageio.metadata.MetadataWriter; +import com.twelvemonkeys.imageio.metadata.MetadataWriterAbstractTest; +import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream; +import org.junit.Test; + +import javax.imageio.stream.MemoryCacheImageOutputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assume.assumeNotNull; + +/** + * IPTCWriterTest. + * + * @author Harald Kuhr + * @author last modified by $Author: harald.kuhr$ + * @version $Id: IPTCWriterTest.java,v 1.0 05/06/15 harald.kuhr Exp$ + */ +public class IPTCWriterTest extends MetadataWriterAbstractTest { + @Override + protected InputStream getData() throws IOException { + return getResource("/iptc/iptc-jpeg-segment.bin").openStream(); + } + + @Override + protected MetadataWriter createWriter() { + return new IPTCWriter(); + } + + private IPTCReader createReader() { + return new IPTCReader(); + } + + @Test + public void testRewriteExisting() throws IOException { + IPTCReader reader = createReader(); + Directory iptc = reader.read(getDataAsIIS()); + assumeNotNull(iptc); + + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + MemoryCacheImageOutputStream stream = new MemoryCacheImageOutputStream(bytes); + createWriter().write(iptc, stream); + stream.close(); + + Directory written = reader.read(new ByteArrayImageInputStream(bytes.toByteArray())); + assertEquals(iptc, written); + } + + @Test + public void testWrite() throws IOException { + List entries = new ArrayList<>(); + entries.add(new IPTCEntry(IPTC.TAG_KEYWORDS, new String[] {"Uno", "Due", "Tre"})); + + Directory iptc = new IPTCDirectory(entries); + + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + MemoryCacheImageOutputStream stream = new MemoryCacheImageOutputStream(bytes); + createWriter().write(iptc, stream); + stream.close(); + + Directory written = createReader().read(new ByteArrayImageInputStream(bytes.toByteArray())); + assertEquals(iptc, written); + } +} \ No newline at end of file diff --git a/imageio/imageio-metadata/src/test/resources/exif/emptyexiftag.tif b/imageio/imageio-metadata/src/test/resources/exif/emptyexiftag.tif new file mode 100644 index 00000000..40fa5507 Binary files /dev/null and b/imageio/imageio-metadata/src/test/resources/exif/emptyexiftag.tif differ diff --git a/imageio/imageio-metadata/src/test/resources/exif/noeof.tif b/imageio/imageio-metadata/src/test/resources/exif/noeof.tif new file mode 100644 index 00000000..19e68a2d Binary files /dev/null and b/imageio/imageio-metadata/src/test/resources/exif/noeof.tif differ diff --git a/imageio/imageio-pcx/pom.xml b/imageio/imageio-pcx/pom.xml index 511724a1..0f49aa20 100755 --- a/imageio/imageio-pcx/pom.xml +++ b/imageio/imageio-pcx/pom.xml @@ -1,12 +1,10 @@ - + 4.0.0 com.twelvemonkeys.imageio imageio - 3.1-SNAPSHOT + 3.3-SNAPSHOT imageio-pcx TwelveMonkeys :: ImageIO :: PCX plugin @@ -22,7 +20,7 @@ com.twelvemonkeys.imageio imageio-core - tests + test-jar diff --git a/imageio/imageio-pcx/src/main/java/com/twelvemonkeys/imageio/plugins/dcx/DCXImageReaderSpi.java b/imageio/imageio-pcx/src/main/java/com/twelvemonkeys/imageio/plugins/dcx/DCXImageReaderSpi.java index 1a4e31b1..b5749027 100755 --- a/imageio/imageio-pcx/src/main/java/com/twelvemonkeys/imageio/plugins/dcx/DCXImageReaderSpi.java +++ b/imageio/imageio-pcx/src/main/java/com/twelvemonkeys/imageio/plugins/dcx/DCXImageReaderSpi.java @@ -28,49 +28,21 @@ package com.twelvemonkeys.imageio.plugins.dcx; -import com.twelvemonkeys.imageio.spi.ProviderInfo; -import com.twelvemonkeys.imageio.util.IIOUtil; +import com.twelvemonkeys.imageio.spi.ImageReaderSpiBase; import javax.imageio.ImageReader; -import javax.imageio.spi.ImageReaderSpi; import javax.imageio.stream.ImageInputStream; import java.io.IOException; import java.nio.ByteOrder; import java.util.Locale; -public final class DCXImageReaderSpi extends ImageReaderSpi { +public final class DCXImageReaderSpi extends ImageReaderSpiBase { /** * Creates a {@code DCXImageReaderSpi}. */ public DCXImageReaderSpi() { - this(IIOUtil.getProviderInfo(DCXImageReaderSpi.class)); - } - - private DCXImageReaderSpi(final ProviderInfo providerInfo) { - super( - providerInfo.getVendorName(), - providerInfo.getVersion(), - new String[]{ - "dcx", - "DCX" - }, - new String[]{"dcx"}, - new String[]{ - // No official IANA record exists - "image/dcx", - "image/x-dcx", - }, - "com.twelvemkonkeys.imageio.plugins.dcx.DCXImageReader", - new Class[] {ImageInputStream.class}, - null, - true, // supports standard stream metadata - null, null, // native stream format name and class - null, null, // extra stream formats - true, // supports standard image metadata - null, null, - null, null // extra image metadata formats - ); + super(new DCXProviderInfo()); } @Override public boolean canDecodeInput(final Object source) throws IOException { diff --git a/imageio/imageio-pcx/src/main/java/com/twelvemonkeys/imageio/plugins/dcx/DCXProviderInfo.java b/imageio/imageio-pcx/src/main/java/com/twelvemonkeys/imageio/plugins/dcx/DCXProviderInfo.java index 264a7701..434119e3 100644 --- a/imageio/imageio-pcx/src/main/java/com/twelvemonkeys/imageio/plugins/dcx/DCXProviderInfo.java +++ b/imageio/imageio-pcx/src/main/java/com/twelvemonkeys/imageio/plugins/dcx/DCXProviderInfo.java @@ -1,3 +1,31 @@ +/* + * Copyright (c) 2015, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + package com.twelvemonkeys.imageio.plugins.dcx; import com.twelvemonkeys.imageio.spi.ReaderWriterProviderInfo; @@ -23,8 +51,8 @@ final class DCXProviderInfo extends ReaderWriterProviderInfo { "image/dcx", "image/x-dcx", }, - "com.twelvemkonkeys.imageio.plugins.dcx.DCXImageReader", - new String[] {"com.twelvemkonkeys.imageio.plugins.dcx.DCXImageReaderSpi"}, + "com.twelvemonkeys.imageio.plugins.dcx.DCXImageReader", + new String[] {"com.twelvemonkeys.imageio.plugins.dcx.DCXImageReaderSpi"}, null, null, false, null, null, null, null, true, null, null, null, null diff --git a/imageio/imageio-pcx/src/main/java/com/twelvemonkeys/imageio/plugins/pcx/PCXImageReader.java b/imageio/imageio-pcx/src/main/java/com/twelvemonkeys/imageio/plugins/pcx/PCXImageReader.java index afe9e496..604e012a 100755 --- a/imageio/imageio-pcx/src/main/java/com/twelvemonkeys/imageio/plugins/pcx/PCXImageReader.java +++ b/imageio/imageio-pcx/src/main/java/com/twelvemonkeys/imageio/plugins/pcx/PCXImageReader.java @@ -277,13 +277,13 @@ public final class PCXImageReader extends ImageReaderBase { throw new AssertionError(); } - processImageProgress(100f * y / height * c / header.getChannels()); - if (abortRequested()) { break; } } + processImageProgress(100f * y / height); + if (y >= srcRegion.y + srcRegion.height) { break; } @@ -372,7 +372,7 @@ public final class PCXImageReader extends ImageReaderBase { readPalette = true; // Wee can't simply skip to an offset, as the RLE compression makes the file size unpredictable - skiptToEOF(imageInput); + skipToEOF(imageInput); // Seek backwards from EOF long paletteStart = imageInput.getStreamPosition() - 769; @@ -400,7 +400,7 @@ public final class PCXImageReader extends ImageReaderBase { } // TODO: Candidate util method - private static long skiptToEOF(final ImageInputStream stream) throws IOException { + private static long skipToEOF(final ImageInputStream stream) throws IOException { long length = stream.length(); if (length > 0) { diff --git a/imageio/imageio-pcx/src/main/java/com/twelvemonkeys/imageio/plugins/pcx/PCXMetadata.java b/imageio/imageio-pcx/src/main/java/com/twelvemonkeys/imageio/plugins/pcx/PCXMetadata.java index 25b376a7..2c6d1ca6 100755 --- a/imageio/imageio-pcx/src/main/java/com/twelvemonkeys/imageio/plugins/pcx/PCXMetadata.java +++ b/imageio/imageio-pcx/src/main/java/com/twelvemonkeys/imageio/plugins/pcx/PCXMetadata.java @@ -28,52 +28,22 @@ package com.twelvemonkeys.imageio.plugins.pcx; -import org.w3c.dom.Node; +import com.twelvemonkeys.imageio.AbstractMetadata; -import javax.imageio.metadata.IIOMetadata; -import javax.imageio.metadata.IIOMetadataFormatImpl; import javax.imageio.metadata.IIOMetadataNode; import java.awt.image.IndexColorModel; -final class PCXMetadata extends IIOMetadata { - // TODO: Clean up & extend AbstractMetadata (after moving from PSD -> Core) - +final class PCXMetadata extends AbstractMetadata { private final PCXHeader header; private final IndexColorModel vgaPalette; PCXMetadata(final PCXHeader header, final IndexColorModel vgaPalette) { this.header = header; this.vgaPalette = vgaPalette; - - standardFormatSupported = true; } - @Override public boolean isReadOnly() { - return true; - } - - @Override public Node getAsTree(final String formatName) { - if (IIOMetadataFormatImpl.standardMetadataFormatName.equals(formatName)) { - return getStandardTree(); - } - else { - throw new IllegalArgumentException("Unsupported metadata format: " + formatName); - } - } - - @Override public void mergeTree(final String formatName, final Node root) { - if (isReadOnly()) { - throw new IllegalStateException("Metadata is read-only"); - } - } - - @Override public void reset() { - if (isReadOnly()) { - throw new IllegalStateException("Metadata is read-only"); - } - } - - @Override protected IIOMetadataNode getStandardChromaNode() { + @Override + protected IIOMetadataNode getStandardChromaNode() { IIOMetadataNode chroma = new IIOMetadataNode("Chroma"); IndexColorModel palette = null; @@ -141,7 +111,8 @@ final class PCXMetadata extends IIOMetadata { // No compression - @Override protected IIOMetadataNode getStandardCompressionNode() { + @Override + protected IIOMetadataNode getStandardCompressionNode() { if (header.getCompression() != PCX.COMPRESSION_NONE) { IIOMetadataNode node = new IIOMetadataNode("Compression"); @@ -159,7 +130,8 @@ final class PCXMetadata extends IIOMetadata { return null; } - @Override protected IIOMetadataNode getStandardDataNode() { + @Override + protected IIOMetadataNode getStandardDataNode() { IIOMetadataNode node = new IIOMetadataNode("Data"); // Planar configuration only makes sense for multi-channel images @@ -202,7 +174,8 @@ final class PCXMetadata extends IIOMetadata { return buffer.toString(); } - @Override protected IIOMetadataNode getStandardDimensionNode() { + @Override + protected IIOMetadataNode getStandardDimensionNode() { IIOMetadataNode dimension = new IIOMetadataNode("Dimension"); IIOMetadataNode imageOrientation = new IIOMetadataNode("ImageOrientation"); @@ -218,7 +191,8 @@ final class PCXMetadata extends IIOMetadata { // No tiling - @Override protected IIOMetadataNode getStandardTransparencyNode() { + @Override + protected IIOMetadataNode getStandardTransparencyNode() { // NOTE: There doesn't seem to be any god way to determine transparency, other than by convention // 1 channel: Gray, 2 channel: Gray + Alpha, 3 channel: RGB, 4 channel: RGBA (hopefully never CMYK...) diff --git a/imageio/imageio-pcx/src/main/java/com/twelvemonkeys/imageio/plugins/pcx/PCXProviderInfo.java b/imageio/imageio-pcx/src/main/java/com/twelvemonkeys/imageio/plugins/pcx/PCXProviderInfo.java index e55e8c0c..8f734fda 100644 --- a/imageio/imageio-pcx/src/main/java/com/twelvemonkeys/imageio/plugins/pcx/PCXProviderInfo.java +++ b/imageio/imageio-pcx/src/main/java/com/twelvemonkeys/imageio/plugins/pcx/PCXProviderInfo.java @@ -1,3 +1,31 @@ +/* + * Copyright (c) 2015, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + package com.twelvemonkeys.imageio.plugins.pcx; import com.twelvemonkeys.imageio.spi.ReaderWriterProviderInfo; @@ -23,8 +51,8 @@ final class PCXProviderInfo extends ReaderWriterProviderInfo { "image/pcx", "image/x-pcx", }, - "com.twelvemkonkeys.imageio.plugins.pcx.PCXImageReader", - new String[] {"com.twelvemkonkeys.imageio.plugins.pcx.PCXImageReaderSpi"}, + "com.twelvemonkeys.imageio.plugins.pcx.PCXImageReader", + new String[] {"com.twelvemonkeys.imageio.plugins.pcx.PCXImageReaderSpi"}, null, null, false, null, null, null, null, true, null, null, null, null diff --git a/imageio/imageio-pcx/src/test/java/com/twelvemonkeys/imageio/plugins/dcx/DCXImageReaderTest.java b/imageio/imageio-pcx/src/test/java/com/twelvemonkeys/imageio/plugins/dcx/DCXImageReaderTest.java index e5a9cc5b..51ef59f3 100755 --- a/imageio/imageio-pcx/src/test/java/com/twelvemonkeys/imageio/plugins/dcx/DCXImageReaderTest.java +++ b/imageio/imageio-pcx/src/test/java/com/twelvemonkeys/imageio/plugins/dcx/DCXImageReaderTest.java @@ -28,15 +28,13 @@ package com.twelvemonkeys.imageio.plugins.dcx; -import java.awt.*; -import java.io.IOException; -import java.util.Arrays; -import java.util.List; +import com.twelvemonkeys.imageio.util.ImageReaderAbstractTest; import javax.imageio.spi.ImageReaderSpi; - -import com.twelvemonkeys.imageio.util.ImageReaderAbstractTestCase; -import org.junit.Test; +import java.awt.*; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; /** * DCXImageReaderTest @@ -45,10 +43,10 @@ import org.junit.Test; * @author last modified by $Author: haraldk$ * @version $Id: DCXImageReaderTest.java,v 1.0 03.07.14 22:28 haraldk Exp$ */ -public class DCXImageReaderTest extends ImageReaderAbstractTestCase { +public class DCXImageReaderTest extends ImageReaderAbstractTest { @Override protected List getTestData() { - return Arrays.asList( + return Collections.singletonList( new TestData(getClassLoaderResource("/dcx/input.dcx"), new Dimension(70, 46)) // RLE encoded RGB (the only sample I've found) ); } @@ -75,7 +73,7 @@ public class DCXImageReaderTest extends ImageReaderAbstractTestCase getSuffixes() { - return Arrays.asList("dcx"); + return Collections.singletonList("dcx"); } @Override diff --git a/imageio/imageio-pcx/src/test/java/com/twelvemonkeys/imageio/plugins/dcx/DCXProviderInfoTest.java b/imageio/imageio-pcx/src/test/java/com/twelvemonkeys/imageio/plugins/dcx/DCXProviderInfoTest.java new file mode 100644 index 00000000..5e3893e7 --- /dev/null +++ b/imageio/imageio-pcx/src/test/java/com/twelvemonkeys/imageio/plugins/dcx/DCXProviderInfoTest.java @@ -0,0 +1,19 @@ +package com.twelvemonkeys.imageio.plugins.dcx; + +import com.twelvemonkeys.imageio.spi.ReaderWriterProviderInfo; +import com.twelvemonkeys.imageio.spi.ReaderWriterProviderInfoTest; + +/** + * DCXProviderInfoTest. + * + * @author Harald Kuhr + * @author last modified by $Author: harald.kuhr$ + * @version $Id: DCXProviderInfoTest.java,v 1.0 02/06/16 harald.kuhr Exp$ + */ +public class DCXProviderInfoTest extends ReaderWriterProviderInfoTest { + + @Override + protected ReaderWriterProviderInfo createProviderInfo() { + return new DCXProviderInfo(); + } +} \ No newline at end of file diff --git a/imageio/imageio-pcx/src/test/java/com/twelvemonkeys/imageio/plugins/pcx/PCXImageReaderTest.java b/imageio/imageio-pcx/src/test/java/com/twelvemonkeys/imageio/plugins/pcx/PCXImageReaderTest.java index 69d2cc86..078e6a05 100755 --- a/imageio/imageio-pcx/src/test/java/com/twelvemonkeys/imageio/plugins/pcx/PCXImageReaderTest.java +++ b/imageio/imageio-pcx/src/test/java/com/twelvemonkeys/imageio/plugins/pcx/PCXImageReaderTest.java @@ -28,13 +28,14 @@ package com.twelvemonkeys.imageio.plugins.pcx; -import com.twelvemonkeys.imageio.util.ImageReaderAbstractTestCase; +import com.twelvemonkeys.imageio.util.ImageReaderAbstractTest; import org.junit.Test; import javax.imageio.spi.ImageReaderSpi; import java.awt.*; import java.io.IOException; import java.util.Arrays; +import java.util.Collections; import java.util.List; /** @@ -44,7 +45,7 @@ import java.util.List; * @author last modified by $Author: haraldk$ * @version $Id: PCXImageReaderTest.java,v 1.0 03.07.14 22:28 haraldk Exp$ */ -public class PCXImageReaderTest extends ImageReaderAbstractTestCase { +public class PCXImageReaderTest extends ImageReaderAbstractTest { @Override protected List getTestData() { return Arrays.asList( @@ -91,7 +92,7 @@ public class PCXImageReaderTest extends ImageReaderAbstractTestCase getSuffixes() { - return Arrays.asList("pcx"); + return Collections.singletonList("pcx"); } @Override diff --git a/imageio/imageio-pcx/src/test/java/com/twelvemonkeys/imageio/plugins/pcx/PCXProviderInfoTest.java b/imageio/imageio-pcx/src/test/java/com/twelvemonkeys/imageio/plugins/pcx/PCXProviderInfoTest.java new file mode 100644 index 00000000..d82951a0 --- /dev/null +++ b/imageio/imageio-pcx/src/test/java/com/twelvemonkeys/imageio/plugins/pcx/PCXProviderInfoTest.java @@ -0,0 +1,19 @@ +package com.twelvemonkeys.imageio.plugins.pcx; + +import com.twelvemonkeys.imageio.spi.ReaderWriterProviderInfo; +import com.twelvemonkeys.imageio.spi.ReaderWriterProviderInfoTest; + +/** + * PCXProviderInfoTest. + * + * @author Harald Kuhr + * @author last modified by $Author: harald.kuhr$ + * @version $Id: PCXProviderInfoTest.java,v 1.0 02/06/16 harald.kuhr Exp$ + */ +public class PCXProviderInfoTest extends ReaderWriterProviderInfoTest { + + @Override + protected ReaderWriterProviderInfo createProviderInfo() { + return new PCXProviderInfo(); + } +} \ No newline at end of file diff --git a/imageio/imageio-pdf/pom.xml b/imageio/imageio-pdf/pom.xml index 8d0628f9..9f19c32e 100644 --- a/imageio/imageio-pdf/pom.xml +++ b/imageio/imageio-pdf/pom.xml @@ -4,7 +4,7 @@ com.twelvemonkeys.imageio imageio - 3.1-SNAPSHOT + 3.3-SNAPSHOT imageio-pdf TwelveMonkeys :: ImageIO :: PDF plugin @@ -21,7 +21,7 @@ com.twelvemonkeys.imageio imageio-core - tests + test-jar diff --git a/imageio/imageio-pict/pom.xml b/imageio/imageio-pict/pom.xml index 43f2abbd..1b9afc34 100644 --- a/imageio/imageio-pict/pom.xml +++ b/imageio/imageio-pict/pom.xml @@ -4,7 +4,7 @@ com.twelvemonkeys.imageio imageio - 3.1-SNAPSHOT + 3.3-SNAPSHOT imageio-pict TwelveMonkeys :: ImageIO :: PICT plugin @@ -18,7 +18,7 @@ com.twelvemonkeys.imageio imageio-core - tests + test-jar diff --git a/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PICTProviderInfo.java b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PICTProviderInfo.java index d5117e2f..20b16eb2 100644 --- a/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PICTProviderInfo.java +++ b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PICTProviderInfo.java @@ -1,3 +1,31 @@ +/* + * Copyright (c) 2015, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + package com.twelvemonkeys.imageio.plugins.pict; import com.twelvemonkeys.imageio.spi.ReaderWriterProviderInfo; @@ -16,10 +44,10 @@ final class PICTProviderInfo extends ReaderWriterProviderInfo { new String[] {"pct", "PCT", "pict", "PICT"}, new String[] {"pct", "pict"}, new String[] {"image/pict", "image/x-pict"}, - "com.twelvemkonkeys.imageio.plugins.pict.PICTImageReader", + "com.twelvemonkeys.imageio.plugins.pict.PICTImageReader", new String[] {"com.twelvemonkeys.imageio.plugins.pict.PICTImageReaderSpi"}, "com.twelvemonkeys.imageio.plugins.pict.PICTImageWriter", - new String[] {"com.twelvemkonkeys.imageio.plugins.pict.PICTImageWriterSpi"}, + new String[] {"com.twelvemonkeys.imageio.plugins.pict.PICTImageWriterSpi"}, false, null, null, null, null, true, null, null, null, null ); diff --git a/imageio/imageio-pict/src/test/java/com/twelvemonkeys/imageio/plugins/pict/PICTImageReaderTest.java b/imageio/imageio-pict/src/test/java/com/twelvemonkeys/imageio/plugins/pict/PICTImageReaderTest.java index 41efe9f5..9c46904c 100644 --- a/imageio/imageio-pict/src/test/java/com/twelvemonkeys/imageio/plugins/pict/PICTImageReaderTest.java +++ b/imageio/imageio-pict/src/test/java/com/twelvemonkeys/imageio/plugins/pict/PICTImageReaderTest.java @@ -2,7 +2,7 @@ package com.twelvemonkeys.imageio.plugins.pict; import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream; import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStreamSpi; -import com.twelvemonkeys.imageio.util.ImageReaderAbstractTestCase; +import com.twelvemonkeys.imageio.util.ImageReaderAbstractTest; import org.junit.Test; import javax.imageio.spi.IIORegistry; @@ -11,6 +11,7 @@ import java.awt.*; import java.awt.image.BufferedImage; import java.io.IOException; import java.util.Arrays; +import java.util.Collections; import java.util.List; import static org.junit.Assert.assertFalse; @@ -22,7 +23,7 @@ import static org.junit.Assert.assertFalse; * @author last modified by $Author: haraldk$ * @version $Id: ICOImageReaderTestCase.java,v 1.0 Apr 1, 2008 10:39:17 PM haraldk Exp$ */ -public class PICTImageReaderTest extends ImageReaderAbstractTestCase { +public class PICTImageReaderTest extends ImageReaderAbstractTest { static { IIORegistry.getDefaultInstance().registerServiceProvider(new ByteArrayImageInputStreamSpi()); @@ -71,7 +72,7 @@ public class PICTImageReaderTest extends ImageReaderAbstractTestCase getFormatNames() { - return Arrays.asList("pict"); + return Collections.singletonList("pict"); } protected List getSuffixes() { diff --git a/imageio/imageio-pict/src/test/java/com/twelvemonkeys/imageio/plugins/pict/PICTProviderInfoTest.java b/imageio/imageio-pict/src/test/java/com/twelvemonkeys/imageio/plugins/pict/PICTProviderInfoTest.java new file mode 100644 index 00000000..920e2c91 --- /dev/null +++ b/imageio/imageio-pict/src/test/java/com/twelvemonkeys/imageio/plugins/pict/PICTProviderInfoTest.java @@ -0,0 +1,19 @@ +package com.twelvemonkeys.imageio.plugins.pict; + +import com.twelvemonkeys.imageio.spi.ReaderWriterProviderInfo; +import com.twelvemonkeys.imageio.spi.ReaderWriterProviderInfoTest; + +/** + * PICTProviderInfoTest. + * + * @author Harald Kuhr + * @author last modified by $Author: harald.kuhr$ + * @version $Id: PICTProviderInfoTest.java,v 1.0 02/06/16 harald.kuhr Exp$ + */ +public class PICTProviderInfoTest extends ReaderWriterProviderInfoTest { + + @Override + protected ReaderWriterProviderInfo createProviderInfo() { + return new PICTProviderInfo(); + } +} \ No newline at end of file diff --git a/imageio/imageio-pnm/pom.xml b/imageio/imageio-pnm/pom.xml index 2d808f38..647d5ae1 100755 --- a/imageio/imageio-pnm/pom.xml +++ b/imageio/imageio-pnm/pom.xml @@ -1,12 +1,10 @@ - + 4.0.0 com.twelvemonkeys.imageio imageio - 3.1-SNAPSHOT + 3.3-SNAPSHOT imageio-pnm TwelveMonkeys :: ImageIO :: PNM plugin @@ -23,7 +21,7 @@ com.twelvemonkeys.imageio imageio-core - tests + test-jar diff --git a/imageio/imageio-pnm/src/main/java/com/twelvemonkeys/imageio/plugins/pnm/PNMImageReaderSpi.java b/imageio/imageio-pnm/src/main/java/com/twelvemonkeys/imageio/plugins/pnm/PNMImageReaderSpi.java index c35e64d0..6b10f6d2 100755 --- a/imageio/imageio-pnm/src/main/java/com/twelvemonkeys/imageio/plugins/pnm/PNMImageReaderSpi.java +++ b/imageio/imageio-pnm/src/main/java/com/twelvemonkeys/imageio/plugins/pnm/PNMImageReaderSpi.java @@ -60,11 +60,11 @@ public final class PNMImageReaderSpi extends ImageReaderSpi { "image/x-portable-anymap", "image/x-portable-arbitrarymap" // PAM }, - "com.twelvemkonkeys.imageio.plugins.pnm.PNMImageReader", + "com.twelvemonkeys.imageio.plugins.pnm.PNMImageReader", new Class[] {ImageInputStream.class}, new String[] { - "com.twelvemkonkeys.imageio.plugins.pnm.PNMImageWriterSpi", - "com.twelvemkonkeys.imageio.plugins.pnm.PAMImageWriterSpi" + "com.twelvemonkeys.imageio.plugins.pnm.PNMImageWriterSpi", + "com.twelvemonkeys.imageio.plugins.pnm.PAMImageWriterSpi" }, true, // supports standard stream metadata null, null, // native stream format name and class diff --git a/imageio/imageio-pnm/src/main/java/com/twelvemonkeys/imageio/plugins/pnm/PNMMetadata.java b/imageio/imageio-pnm/src/main/java/com/twelvemonkeys/imageio/plugins/pnm/PNMMetadata.java index 267439b5..58578603 100755 --- a/imageio/imageio-pnm/src/main/java/com/twelvemonkeys/imageio/plugins/pnm/PNMMetadata.java +++ b/imageio/imageio-pnm/src/main/java/com/twelvemonkeys/imageio/plugins/pnm/PNMMetadata.java @@ -28,51 +28,22 @@ package com.twelvemonkeys.imageio.plugins.pnm; -import org.w3c.dom.Node; +import com.twelvemonkeys.imageio.AbstractMetadata; -import javax.imageio.metadata.IIOMetadata; -import javax.imageio.metadata.IIOMetadataFormatImpl; import javax.imageio.metadata.IIOMetadataNode; import java.awt.*; import java.awt.image.DataBuffer; import java.nio.ByteOrder; -final class PNMMetadata extends IIOMetadata { - // TODO: Clean up & extend AbstractMetadata (after moving from PSD -> Core) - +final class PNMMetadata extends AbstractMetadata { private final PNMHeader header; PNMMetadata(final PNMHeader header) { this.header = header; - standardFormatSupported = true; } - @Override public boolean isReadOnly() { - return true; - } - - @Override public Node getAsTree(final String formatName) { - if (IIOMetadataFormatImpl.standardMetadataFormatName.equals(formatName)) { - return getStandardTree(); - } - else { - throw new IllegalArgumentException("Unsupported metadata format: " + formatName); - } - } - - @Override public void mergeTree(final String formatName, final Node root) { - if (isReadOnly()) { - throw new IllegalStateException("Metadata is read-only"); - } - } - - @Override public void reset() { - if (isReadOnly()) { - throw new IllegalStateException("Metadata is read-only"); - } - } - - @Override protected IIOMetadataNode getStandardChromaNode() { + @Override + protected IIOMetadataNode getStandardChromaNode() { IIOMetadataNode chroma = new IIOMetadataNode("Chroma"); IIOMetadataNode csType = new IIOMetadataNode("ColorSpaceType"); @@ -105,7 +76,9 @@ final class PNMMetadata extends IIOMetadata { // TODO: Might make sense to set gamma? IIOMetadataNode blackIsZero = new IIOMetadataNode("BlackIsZero"); - blackIsZero.setAttribute("value", header.getTupleType() == TupleType.BLACKANDWHITE_WHITE_IS_ZERO ? "FALSE" : "TRUE"); + blackIsZero.setAttribute("value", header.getTupleType() == TupleType.BLACKANDWHITE_WHITE_IS_ZERO + ? "FALSE" + : "TRUE"); chroma.appendChild(blackIsZero); return chroma; @@ -113,11 +86,14 @@ final class PNMMetadata extends IIOMetadata { // No compression - @Override protected IIOMetadataNode getStandardDataNode() { + @Override + protected IIOMetadataNode getStandardDataNode() { IIOMetadataNode node = new IIOMetadataNode("Data"); IIOMetadataNode sampleFormat = new IIOMetadataNode("SampleFormat"); - sampleFormat.setAttribute("value", header.getTransferType() == DataBuffer.TYPE_FLOAT ? "Real" : "UnsignedIntegral"); + sampleFormat.setAttribute("value", header.getTransferType() == DataBuffer.TYPE_FLOAT + ? "Real" + : "UnsignedIntegral"); node.appendChild(sampleFormat); IIOMetadataNode bitsPerSample = new IIOMetadataNode("BitsPerSample"); @@ -128,7 +104,9 @@ final class PNMMetadata extends IIOMetadata { significantBitsPerSample.setAttribute("value", createListValue(header.getSamplesPerPixel(), Integer.toString(computeSignificantBits()))); node.appendChild(significantBitsPerSample); - String msb = header.getByteOrder() == ByteOrder.BIG_ENDIAN ? "0" : Integer.toString(header.getBitsPerSample() - 1); + String msb = header.getByteOrder() == ByteOrder.BIG_ENDIAN + ? "0" + : Integer.toString(header.getBitsPerSample() - 1); IIOMetadataNode sampleMSB = new IIOMetadataNode("SampleMSB"); sampleMSB.setAttribute("value", createListValue(header.getSamplesPerPixel(), msb)); @@ -166,7 +144,8 @@ final class PNMMetadata extends IIOMetadata { return buffer.toString(); } - @Override protected IIOMetadataNode getStandardDimensionNode() { + @Override + protected IIOMetadataNode getStandardDimensionNode() { IIOMetadataNode dimension = new IIOMetadataNode("Dimension"); IIOMetadataNode imageOrientation = new IIOMetadataNode("ImageOrientation"); @@ -178,7 +157,8 @@ final class PNMMetadata extends IIOMetadata { // No document node - @Override protected IIOMetadataNode getStandardTextNode() { + @Override + protected IIOMetadataNode getStandardTextNode() { if (!header.getComments().isEmpty()) { IIOMetadataNode text = new IIOMetadataNode("Text"); @@ -197,7 +177,8 @@ final class PNMMetadata extends IIOMetadata { // No tiling - @Override protected IIOMetadataNode getStandardTransparencyNode() { + @Override + protected IIOMetadataNode getStandardTransparencyNode() { IIOMetadataNode transparency = new IIOMetadataNode("Transparency"); IIOMetadataNode alpha = new IIOMetadataNode("Alpha"); diff --git a/imageio/imageio-pnm/src/test/java/com/twelvemonkeys/imageio/plugins/pnm/PAMImageWriterSpiTest.java b/imageio/imageio-pnm/src/test/java/com/twelvemonkeys/imageio/plugins/pnm/PAMImageWriterSpiTest.java new file mode 100644 index 00000000..c6efb07b --- /dev/null +++ b/imageio/imageio-pnm/src/test/java/com/twelvemonkeys/imageio/plugins/pnm/PAMImageWriterSpiTest.java @@ -0,0 +1,38 @@ +package com.twelvemonkeys.imageio.plugins.pnm; + +import org.junit.Test; + +import javax.imageio.ImageWriter; +import javax.imageio.spi.ImageReaderSpi; +import javax.imageio.spi.ImageWriterSpi; + +import static com.twelvemonkeys.imageio.spi.ReaderWriterProviderInfoTest.assertClassExists; +import static com.twelvemonkeys.imageio.spi.ReaderWriterProviderInfoTest.assertClassesExist; +import static org.junit.Assert.assertNotNull; + +/** + * PAMImageWriterSpiTest. + * + * @author Harald Kuhr + * @author last modified by $Author: harald.kuhr$ + * @version $Id: PAMImageWriterSpiTest.java,v 1.0 02/06/16 harald.kuhr Exp$ + */ +public class PAMImageWriterSpiTest { + + private final ImageWriterSpi spi = new PAMImageWriterSpi(); + + @Test + public void getPluginClassName() { + assertClassExists(spi.getPluginClassName(), ImageWriter.class); + } + + @Test + public void getImageReaderSpiNames() { + assertClassesExist(spi.getImageReaderSpiNames(), ImageReaderSpi.class); + } + + @Test + public void getOutputTypes() { + assertNotNull(spi.getOutputTypes()); + } +} \ No newline at end of file diff --git a/imageio/imageio-pnm/src/test/java/com/twelvemonkeys/imageio/plugins/pnm/PNMImageReaderSpiTest.java b/imageio/imageio-pnm/src/test/java/com/twelvemonkeys/imageio/plugins/pnm/PNMImageReaderSpiTest.java new file mode 100644 index 00000000..2ed5daef --- /dev/null +++ b/imageio/imageio-pnm/src/test/java/com/twelvemonkeys/imageio/plugins/pnm/PNMImageReaderSpiTest.java @@ -0,0 +1,38 @@ +package com.twelvemonkeys.imageio.plugins.pnm; + +import org.junit.Test; + +import javax.imageio.ImageReader; +import javax.imageio.spi.ImageReaderSpi; +import javax.imageio.spi.ImageWriterSpi; + +import static com.twelvemonkeys.imageio.spi.ReaderWriterProviderInfoTest.assertClassExists; +import static com.twelvemonkeys.imageio.spi.ReaderWriterProviderInfoTest.assertClassesExist; +import static org.junit.Assert.assertNotNull; + +/** + * PNMImageReaderSpiTest. + * + * @author Harald Kuhr + * @author last modified by $Author: harald.kuhr$ + * @version $Id: PNMImageReaderSpiTest.java,v 1.0 02/06/16 harald.kuhr Exp$ + */ +public class PNMImageReaderSpiTest { + + private final ImageReaderSpi spi = new PNMImageReaderSpi(); + + @Test + public void getPluginClassName() { + assertClassExists(spi.getPluginClassName(), ImageReader.class); + } + + @Test + public void getImageWriterSpiNames() { + assertClassesExist(spi.getImageWriterSpiNames(), ImageWriterSpi.class); + } + + @Test + public void getInputTypes() { + assertNotNull(spi.getInputTypes()); + } +} \ No newline at end of file diff --git a/imageio/imageio-pnm/src/test/java/com/twelvemonkeys/imageio/plugins/pnm/PNMImageReaderTest.java b/imageio/imageio-pnm/src/test/java/com/twelvemonkeys/imageio/plugins/pnm/PNMImageReaderTest.java index 2efceaa6..c8a3dde1 100755 --- a/imageio/imageio-pnm/src/test/java/com/twelvemonkeys/imageio/plugins/pnm/PNMImageReaderTest.java +++ b/imageio/imageio-pnm/src/test/java/com/twelvemonkeys/imageio/plugins/pnm/PNMImageReaderTest.java @@ -28,7 +28,7 @@ package com.twelvemonkeys.imageio.plugins.pnm; -import com.twelvemonkeys.imageio.util.ImageReaderAbstractTestCase; +import com.twelvemonkeys.imageio.util.ImageReaderAbstractTest; import org.junit.Test; import javax.imageio.ImageReader; @@ -39,9 +39,10 @@ import java.io.IOException; import java.util.Arrays; import java.util.List; -import static org.junit.Assert.*; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; -public class PNMImageReaderTest extends ImageReaderAbstractTestCase{ +public class PNMImageReaderTest extends ImageReaderAbstractTest { @Override protected List getTestData() { return Arrays.asList( new TestData(getClassLoaderResource("/ppm/lena.ppm"), new Dimension(128, 128)), // P6 (PPM RAW) diff --git a/imageio/imageio-pnm/src/test/java/com/twelvemonkeys/imageio/plugins/pnm/PNMImageWriterSpiTest.java b/imageio/imageio-pnm/src/test/java/com/twelvemonkeys/imageio/plugins/pnm/PNMImageWriterSpiTest.java new file mode 100644 index 00000000..4bbb4936 --- /dev/null +++ b/imageio/imageio-pnm/src/test/java/com/twelvemonkeys/imageio/plugins/pnm/PNMImageWriterSpiTest.java @@ -0,0 +1,38 @@ +package com.twelvemonkeys.imageio.plugins.pnm; + +import org.junit.Test; + +import javax.imageio.ImageWriter; +import javax.imageio.spi.ImageReaderSpi; +import javax.imageio.spi.ImageWriterSpi; + +import static com.twelvemonkeys.imageio.spi.ReaderWriterProviderInfoTest.assertClassExists; +import static com.twelvemonkeys.imageio.spi.ReaderWriterProviderInfoTest.assertClassesExist; +import static org.junit.Assert.assertNotNull; + +/** + * PNMImageWriterSpiTest. + * + * @author Harald Kuhr + * @author last modified by $Author: harald.kuhr$ + * @version $Id: PNMImageWriterSpiTest.java,v 1.0 02/06/16 harald.kuhr Exp$ + */ +public class PNMImageWriterSpiTest { + + private final ImageWriterSpi spi = new PNMImageWriterSpi(); + + @Test + public void getPluginClassName() { + assertClassExists(spi.getPluginClassName(), ImageWriter.class); + } + + @Test + public void getImageReaderSpiNames() { + assertClassesExist(spi.getImageReaderSpiNames(), ImageReaderSpi.class); + } + + @Test + public void getOutputTypes() { + assertNotNull(spi.getOutputTypes()); + } +} \ No newline at end of file diff --git a/imageio/imageio-pnm/src/test/java/com/twelvemonkeys/imageio/plugins/pnm/PNMProviderInfoTest.java b/imageio/imageio-pnm/src/test/java/com/twelvemonkeys/imageio/plugins/pnm/PNMProviderInfoTest.java new file mode 100644 index 00000000..7ae7e892 --- /dev/null +++ b/imageio/imageio-pnm/src/test/java/com/twelvemonkeys/imageio/plugins/pnm/PNMProviderInfoTest.java @@ -0,0 +1,21 @@ +package com.twelvemonkeys.imageio.plugins.pnm; + +import org.junit.Test; + +import static org.junit.Assert.assertNotNull; + +/** + * PNMProviderInfoTest. + * + * @author Harald Kuhr + * @author last modified by $Author: harald.kuhr$ + * @version $Id: PNMProviderInfoTest.java,v 1.0 02/06/16 harald.kuhr Exp$ + */ +public class PNMProviderInfoTest { + @Test + public void vendorVersion() { + PNMProviderInfo providerInfo = new PNMProviderInfo(); + assertNotNull(providerInfo.getVendorName()); + assertNotNull(providerInfo.getVersion()); + } +} \ No newline at end of file diff --git a/imageio/imageio-psd/pom.xml b/imageio/imageio-psd/pom.xml index c02f7e83..c58d19b4 100644 --- a/imageio/imageio-psd/pom.xml +++ b/imageio/imageio-psd/pom.xml @@ -4,7 +4,7 @@ com.twelvemonkeys.imageio imageio - 3.1-SNAPSHOT + 3.3-SNAPSHOT imageio-psd TwelveMonkeys :: ImageIO :: PSD plugin @@ -20,7 +20,7 @@ com.twelvemonkeys.imageio imageio-core - tests + test-jar com.twelvemonkeys.imageio diff --git a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSD.java b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSD.java index b1c0539a..82f506de 100755 --- a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSD.java +++ b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSD.java @@ -51,6 +51,7 @@ interface PSD { /** PSD Resource type identifier "8BIM" */ int RESOURCE_TYPE = ('8' << 24) + ('B' << 16) + ('I' << 8) + 'M'; + int RESOURCE_TYPE_LONG = ('8' << 24) + ('B' << 16) + ('6' << 8) + '4';; // Blending modes /** Pass through blending mode "pass"*/ @@ -578,6 +579,94 @@ interface PSD { */ int RES_ALTERNATE_SPOT_COLORS = 0x042B; + /** + * (Photoshop CS2) Layer Selection ID(s). + * 2 bytes count, following is repeated for each count: 4 bytes layer ID. + */ + int RES_LAYER_SELECTION_IDS = 0x042D; + + /** + * (Photoshop CS2) HDR Toning information + */ + int RES_HDR_TONING_INFO = 0x042E; + + /** + * (Photoshop CS2) Print info + */ + int RES_PRINT_INFO = 0x042F; + + /** + * (Photoshop CS2) Layer Group(s) Enabled ID. + * 1 byte for each layer in the document, repeated by length of the resource. + * NOTE: Layer groups have start and end markers. + */ + int RES_LAYER_GROUPS_ENABLED = 0x0430; + + /** + * (Photoshop CS3) Color samplers resource. + * Also see ID 1038 for old format. + * See Color samplers resource format. + */ + int RES_COLOR_SAMPLERS_RESOURCE = 0x0431; + + /** + * (Photoshop CS3) Measurement Scale. + * 4 bytes (descriptor version = 16), Descriptor (see Descriptor structure) + */ + int RES_MEASUREMENT_SCALE = 0x0432; + + /** + * (Photoshop CS3) Timeline Information. + * 4 bytes (descriptor version = 16), Descriptor (see Descriptor structure) + */ + int RES_TIMELINE_INFO = 0x0433; + + /** + * (Photoshop CS3) Sheet Disclosure. + * 4 bytes (descriptor version = 16), Descriptor (see See Descriptor structure) + */ + int RES_SHEET_DISCLOSURE = 0x0434; + + /** + * (Photoshop CS3) DisplayInfo structure to support floating point colors. + * Also see ID 1007. See Appendix A in Photoshop API Guide.pdf . + */ + int RES_DISPLAY_INFO_FP = 0x0435; + + /** + * (Photoshop CS3) Onion Skins. + * 4 bytes (descriptor version = 16), Descriptor (see See Descriptor structure) + */ + int RES_ONION_SKINS = 0x0436; + + /** + * (Photoshop CS4) Count Information. + * 4 bytes (descriptor version = 16), Descriptor (see See Descriptor structure). + * Information about the count in the document. See the Count Tool. + */ + int RES_COUNT_INFO = 0x0438; + + /** + * (Photoshop CS5) Print Information. + * 4 bytes (descriptor version = 16), Descriptor (see See Descriptor structure). + * Information about the current print settings in the document. The color management options. + */ + int RES_PRINT_INFO_CMM = 0x043A; + + /** + * (Photoshop CS5) Print Style. + * 4 bytes (descriptor version = 16), Descriptor (see See Descriptor structure). + * Information about the current print style in the document. The printing marks, labels, ornaments, etc. + */ + int RES_PRINT_STYLE = 0x043B; + + /** + * (Photoshop CC) Path Selection State. + * 4 bytes (descriptor version = 16), Descriptor (see See Descriptor structure). + * Information about the current path selection state. + */ + int RES_PATH_SELECTION_STATE = 0x0440; + // 07d0-0bb6 /* Saved path information */ @@ -601,4 +690,22 @@ interface PSD { /** Plug-In resource(s). Resources added by a plug-in. See the plug-in API found in the SDK documentation */ int RES_PLUGIN_MAX = 0x1387; + + // TODO: Better naming of these.. It's a kind of resource blocks as well.. + // "Additional Layer Information" + int LMsk = 'L' << 24 | 'M' << 16 | 's' << 8 | 'k'; + int Lr16 = 'L' << 24 | 'r' << 16 | '1' << 8 | '6'; + int Lr32 = 'L' << 24 | 'r' << 16 | '3' << 8 | '2'; + int Layr = 'L' << 24 | 'a' << 16 | 'y' << 8 | 'r'; + int Mt16 = 'M' << 24 | 't' << 16 | '1' << 8 | '6'; + int Mt32 = 'M' << 24 | 't' << 16 | '3' << 8 | '2'; + int Mtrn = 'M' << 24 | 't' << 16 | 'r' << 8 | 'n'; + int Alph = 'A' << 24 | 'l' << 16 | 'p' << 8 | 'h'; + int FMsk = 'F' << 24 | 'M' << 16 | 's' << 8 | 'k'; + int lnk2 = 'l' << 24 | 'n' << 16 | 'k' << 8 | '2'; + int FEid = 'F' << 24 | 'E' << 16 | 'i' << 8 | 'd'; + int FXid = 'F' << 24 | 'X' << 16 | 'i' << 8 | 'd'; + int PxSD = 'P' << 24 | 'x' << 16 | 'S' << 8 | 'D'; + int luni = 'l' << 24 | 'u' << 16 | 'n' << 8 | 'i'; + int lyid = 'l' << 24 | 'y' << 16 | 'i' << 8 | 'd'; } diff --git a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDColorData.java b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDColorData.java index b3f71c20..993929af 100755 --- a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDColorData.java +++ b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDColorData.java @@ -28,8 +28,6 @@ package com.twelvemonkeys.imageio.plugins.psd; -import com.twelvemonkeys.image.InverseColorMapIndexColorModel; - import javax.imageio.IIOException; import javax.imageio.stream.ImageInputStream; import java.awt.image.DataBuffer; @@ -66,7 +64,7 @@ final class PSDColorData { IndexColorModel getIndexColorModel() { if (colorModel == null) { int[] rgb = toInterleavedRGB(colors); - colorModel = new InverseColorMapIndexColorModel(8, rgb.length, rgb, 0, false, -1, DataBuffer.TYPE_BYTE); + colorModel = new IndexColorModel(8, rgb.length, rgb, 0, false, -1, DataBuffer.TYPE_BYTE); } return colorModel; diff --git a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDImageReader.java b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDImageReader.java old mode 100755 new mode 100644 index ea1d40d3..d5f45e16 --- a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDImageReader.java +++ b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDImageReader.java @@ -54,11 +54,11 @@ import java.util.List; /** * ImageReader for Adobe Photoshop Document (PSD) format. * - * @see Adobe Photoshop File Formats Specification - * @see Adobe Photoshop File Format Summary * @author Harald Kuhr * @author last modified by $Author: haraldk$ * @version $Id: PSDImageReader.java,v 1.0 Apr 29, 2008 4:45:52 PM haraldk Exp$ + * @see Adobe Photoshop File Formats Specification + * @see Adobe Photoshop File Format Summary */ // TODO: Implement ImageIO meta data interface // TODO: Figure out of we should assume Adobe RGB (1998) color model, if no embedded profile? @@ -171,13 +171,13 @@ public final class PSDImageReader extends ImageReaderBase { } if (header.channels == 1 && header.bits == 8) { - return ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_BYTE_GRAY); + return ImageTypeSpecifiers.createBanded(cs, new int[] {0}, new int[] {0}, DataBuffer.TYPE_BYTE, false, false); } else if (header.channels == 2 && header.bits == 8) { return ImageTypeSpecifiers.createBanded(cs, new int[] {0, 1}, new int[] {0, 0}, DataBuffer.TYPE_BYTE, true, false); } else if (header.channels == 1 && header.bits == 16) { - return ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_USHORT_GRAY); + return ImageTypeSpecifiers.createBanded(cs, new int[] {0}, new int[] {0}, DataBuffer.TYPE_USHORT, false, false); } else if (header.channels == 2 && header.bits == 16) { return ImageTypeSpecifiers.createBanded(cs, new int[] {0, 1}, new int[] {0, 0}, DataBuffer.TYPE_USHORT, true, false); @@ -224,22 +224,22 @@ public final class PSDImageReader extends ImageReaderBase { cs = ColorSpaces.getColorSpace(ColorSpaces.CS_GENERIC_CMYK); } - if (header.channels == 4 && header.bits == 8) { + if (header.channels == 4 && header.bits == 8) { return ImageTypeSpecifiers.createBanded(cs, new int[] {0, 1, 2, 3}, new int[] {0, 0, 0, 0}, DataBuffer.TYPE_BYTE, false, false); } - else if (header.channels == 5 && header.bits == 8) { + else if (header.channels == 5 && header.bits == 8) { return ImageTypeSpecifiers.createBanded(cs, new int[] {0, 1, 2, 3, 4}, new int[] {0, 0, 0, 0, 0}, DataBuffer.TYPE_BYTE, true, false); } - else if (header.channels == 4 && header.bits == 16) { + else if (header.channels == 4 && header.bits == 16) { return ImageTypeSpecifiers.createBanded(cs, new int[] {0, 1, 2, 3}, new int[] {0, 0, 0, 0}, DataBuffer.TYPE_USHORT, false, false); } - else if (header.channels == 5 && header.bits == 16) { + else if (header.channels == 5 && header.bits == 16) { return ImageTypeSpecifiers.createBanded(cs, new int[] {0, 1, 2, 3, 4}, new int[] {0, 0, 0, 0, 0}, DataBuffer.TYPE_USHORT, true, false); } - else if (header.channels == 4 && header.bits == 32) { + else if (header.channels == 4 && header.bits == 32) { return ImageTypeSpecifiers.createBanded(cs, new int[] {0, 1, 2, 3}, new int[] {0, 0, 0, 0}, DataBuffer.TYPE_INT, false, false); } - else if (header.channels == 5 && header.bits == 32) { + else if (header.channels == 5 && header.bits == 32) { return ImageTypeSpecifiers.createBanded(cs, new int[] {0, 1, 2, 3, 4}, new int[] {0, 0, 0, 0, 0}, DataBuffer.TYPE_INT, true, false); } @@ -250,6 +250,7 @@ public final class PSDImageReader extends ImageReaderBase { case PSD.COLOR_MODE_LAB: // TODO: Implement // TODO: If there's a color profile embedded, it should be easy, otherwise we're out of luck... + // TODO: See the LAB color handling in TIFF default: throw new IIOException(String.format("Unsupported PSD MODE: %s (%d channels/%d bits)", header.mode, header.channels, header.bits)); } @@ -263,7 +264,7 @@ public final class PSDImageReader extends ImageReaderBase { ImageTypeSpecifier rawType = getRawImageTypeInternal(imageIndex); ColorSpace cs = rawType.getColorModel().getColorSpace(); - List types = new ArrayList(); + List types = new ArrayList<>(); switch (header.mode) { case PSD.COLOR_MODE_RGB: @@ -308,7 +309,7 @@ public final class PSDImageReader extends ImageReaderBase { types.add(ImageTypeSpecifiers.createInterleaved(cs, new int[] {4, 3, 2, 1, 0}, DataBuffer.TYPE_BYTE, true, false)); } else if (rawType.getNumBands() == 4 && rawType.getBitsPerBand(0) == 16) { - types.add(ImageTypeSpecifiers.createInterleaved(cs, new int[]{3, 2, 1, 0}, DataBuffer.TYPE_USHORT, false, false)); + types.add(ImageTypeSpecifiers.createInterleaved(cs, new int[] {3, 2, 1, 0}, DataBuffer.TYPE_USHORT, false, false)); } else if (rawType.getNumBands() == 5 && rawType.getBitsPerBand(0) == 16) { types.add(ImageTypeSpecifiers.createInterleaved(cs, new int[] {4, 3, 2, 1, 0}, DataBuffer.TYPE_USHORT, true, false)); @@ -362,25 +363,6 @@ public final class PSDImageReader extends ImageReaderBase { final Rectangle dest = new Rectangle(); computeRegions(param, header.width, header.height, image, source, dest); - /* - NOTE: It seems safe to just leave this out for now. The only thing we need is to support sub sampling. - Sun's readers does not support arbitrary destination formats. - - // TODO: Create temp raster in native format w * 1 - // Read (sub-sampled) row into temp raster (skip other rows) - // Copy pixels from temp raster - // If possible, leave the destination image "untouched" (accelerated) - // See Jim Grahams comments: - // http://forums.java.net/jive/message.jspa?messageID=295758#295758 - - // TODO: Banding... - - ImageTypeSpecifier spec = getRawImageType(imageIndex); - BufferedImage temp = spec.createBufferedImage(getWidth(imageIndex), 1); - temp.getRaster(); - - */ - final int xSub; final int ySub; @@ -449,20 +431,19 @@ public final class PSDImageReader extends ImageReaderBase { return layersStart; } - private void readImageData(final BufferedImage pImage, + private void readImageData(final BufferedImage destination, final ColorModel pSourceCM, final Rectangle pSource, final Rectangle pDest, final int pXSub, final int pYSub, final int[] pByteCounts, final int pCompression) throws IOException { - final WritableRaster raster = pImage.getRaster(); - final ColorModel destCM = pImage.getColorModel(); + WritableRaster destRaster = destination.getRaster(); + ColorModel destCM = destination.getColorModel(); - // TODO: This raster is 3-5 times longer than needed, depending on number of channels... - final WritableRaster rowRaster = pSourceCM.createCompatibleWritableRaster(header.width, 1); - - final int channels = rowRaster.getNumBands(); - final boolean banded = raster.getDataBuffer().getNumBanks() > 1; - final int interleavedBands = banded ? 1 : raster.getNumBands(); + int channels = pSourceCM.createCompatibleSampleModel(1, 1).getNumBands(); + ImageTypeSpecifier singleBandRowSpec = ImageTypeSpecifiers.createGrayscale(header.bits, pSourceCM.getTransferType()); + WritableRaster rowRaster = singleBandRowSpec.createBufferedImage(header.width, 1).getRaster(); + boolean banded = destRaster.getDataBuffer().getNumBanks() > 1; + int interleavedBands = banded ? 1 : destRaster.getNumBands(); for (int c = 0; c < channels; c++) { int bandOffset = banded ? 0 : interleavedBands - 1 - c; @@ -470,19 +451,19 @@ public final class PSDImageReader extends ImageReaderBase { switch (header.bits) { case 1: byte[] row1 = ((DataBufferByte) rowRaster.getDataBuffer()).getData(); - read1bitChannel(c, header.channels, raster.getDataBuffer(), interleavedBands, bandOffset, pSourceCM, row1, pSource, pDest, pXSub, pYSub, header.width, header.height, pByteCounts, pCompression == PSD.COMPRESSION_RLE); + read1bitChannel(c, header.channels, destRaster.getDataBuffer(), interleavedBands, bandOffset, pSourceCM, row1, pSource, pDest, pXSub, pYSub, header.width, header.height, pByteCounts, pCompression == PSD.COMPRESSION_RLE); break; case 8: byte[] row8 = ((DataBufferByte) rowRaster.getDataBuffer()).getData(); - read8bitChannel(c, header.channels, raster.getDataBuffer(), interleavedBands, bandOffset, pSourceCM, row8, pSource, pDest, pXSub, pYSub, header.width, header.height, pByteCounts, c * header.height, pCompression == PSD.COMPRESSION_RLE); + read8bitChannel(c, header.channels, destRaster.getDataBuffer(), interleavedBands, bandOffset, pSourceCM, row8, pSource, pDest, pXSub, pYSub, header.width, header.height, pByteCounts, c * header.height, pCompression == PSD.COMPRESSION_RLE); break; case 16: short[] row16 = ((DataBufferUShort) rowRaster.getDataBuffer()).getData(); - read16bitChannel(c, header.channels, raster.getDataBuffer(), interleavedBands, bandOffset, pSourceCM, row16, pSource, pDest, pXSub, pYSub, header.width, header.height, pByteCounts, c * header.height, pCompression == PSD.COMPRESSION_RLE); + read16bitChannel(c, header.channels, destRaster.getDataBuffer(), interleavedBands, bandOffset, pSourceCM, row16, pSource, pDest, pXSub, pYSub, header.width, header.height, pByteCounts, c * header.height, pCompression == PSD.COMPRESSION_RLE); break; case 32: int[] row32 = ((DataBufferInt) rowRaster.getDataBuffer()).getData(); - read32bitChannel(c, header.channels, raster.getDataBuffer(), interleavedBands, bandOffset, pSourceCM, row32, pSource, pDest, pXSub, pYSub, header.width, header.height, pByteCounts, c * header.height, pCompression == PSD.COMPRESSION_RLE); + read32bitChannel(c, header.channels, destRaster.getDataBuffer(), interleavedBands, bandOffset, pSourceCM, row32, pSource, pDest, pXSub, pYSub, header.width, header.height, pByteCounts, c * header.height, pCompression == PSD.COMPRESSION_RLE); break; default: throw new IIOException(String.format("Unsupported PSD bit depth: %s", header.bits)); @@ -495,12 +476,12 @@ public final class PSDImageReader extends ImageReaderBase { if (header.bits == 8) { // Compose out the background of the semi-transparent pixels, as PS somehow has the background composed in - decomposeAlpha(destCM, raster.getDataBuffer(), pDest.width, pDest.height, raster.getNumBands()); + decomposeAlpha(destCM, destRaster.getDataBuffer(), pDest.width, pDest.height, destRaster.getNumBands()); } // NOTE: ColorSpace uses Object.equals(), so we rely on using same instances! - if (!pSourceCM.getColorSpace().equals(pImage.getColorModel().getColorSpace())) { - convertToDestinationCS(pSourceCM, pImage.getColorModel(), raster); + if (!pSourceCM.getColorSpace().equals(destination.getColorModel().getColorSpace())) { + convertToDestinationCS(pSourceCM, destination.getColorModel(), destRaster); } } @@ -546,28 +527,24 @@ public final class PSDImageReader extends ImageReaderBase { 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(); + boolean isCMYK = pSourceColorModel.getColorSpace().getType() == ColorSpace.TYPE_CMYK; + int colorComponents = pSourceColorModel.getColorSpace().getNumComponents(); + final boolean invert = isCMYK && pChannel < colorComponents; final boolean banded = pData.getNumBanks() > 1; for (int y = 0; y < pChannelHeight; y++) { - // NOTE: Length is in *16 bit values* (shorts) - int length = 2 * (pRLECompressed ? pRowByteCounts[pRowOffset + y] : pChannelWidth); + int length = (pRLECompressed ? pRowByteCounts[pRowOffset + y] : 4 * pChannelWidth); // TODO: Sometimes need to read the line y == source.y + source.height... // Read entire line, if within source region and sampling if (y >= pSource.y && y < pSource.y + pSource.height && y % pYSub == 0) { if (pRLECompressed) { - DataInputStream input = PSDUtil.createPackBitsStream(imageInput, length); - try { + try (DataInputStream input = PSDUtil.createPackBitsStream(imageInput, length)) { for (int x = 0; x < pChannelWidth; x++) { pRow[x] = input.readInt(); } } - finally { - input.close(); - } } else { imageInput.readFully(pRow, 0, pChannelWidth); @@ -580,7 +557,7 @@ public final class PSDImageReader extends ImageReaderBase { int value = pRow[pSource.x + x * pXSub]; // CMYK values are stored inverted, but alpha is not - if (isCMYK && pChannel < colorComponents) { + if (invert) { value = 0xffffffff - value; } @@ -609,27 +586,23 @@ public final class PSDImageReader extends ImageReaderBase { 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(); + boolean isCMYK = pSourceColorModel.getColorSpace().getType() == ColorSpace.TYPE_CMYK; + int colorComponents = pSourceColorModel.getColorSpace().getNumComponents(); + final boolean invert = isCMYK && pChannel < colorComponents; final boolean banded = pData.getNumBanks() > 1; for (int y = 0; y < pChannelHeight; y++) { - // NOTE: Length is in *16 bit values* (shorts) - int length = 2 * (pRLECompressed ? pRowByteCounts[pRowOffset + y] : pChannelWidth); + int length = (pRLECompressed ? pRowByteCounts[pRowOffset + y] : 2 * pChannelWidth); // TODO: Sometimes need to read the line y == source.y + source.height... // Read entire line, if within source region and sampling if (y >= pSource.y && y < pSource.y + pSource.height && y % pYSub == 0) { if (pRLECompressed) { - DataInputStream input = PSDUtil.createPackBitsStream(imageInput, length); - try { + try (DataInputStream input = PSDUtil.createPackBitsStream(imageInput, length)) { for (int x = 0; x < pChannelWidth; x++) { pRow[x] = input.readShort(); } } - finally { - input.close(); - } } else { imageInput.readFully(pRow, 0, pChannelWidth); @@ -642,8 +615,8 @@ public final class PSDImageReader extends ImageReaderBase { short value = pRow[pSource.x + x * pXSub]; // CMYK values are stored inverted, but alpha is not - if (isCMYK && pChannel < colorComponents) { - value = (short) (65535 - value & 0xffff); + if (invert) { + value = (short) (0xffff - value & 0xffff); } pData.setElem(banded ? pChannel : 0, offset + x * pBands, value); @@ -671,8 +644,9 @@ public final class PSDImageReader extends ImageReaderBase { 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(); + boolean isCMYK = pSourceColorModel.getColorSpace().getType() == ColorSpace.TYPE_CMYK; + int colorComponents = pSourceColorModel.getColorSpace().getNumComponents(); + final boolean invert = isCMYK && pChannel < colorComponents; final boolean banded = pData.getNumBanks() > 1; for (int y = 0; y < pChannelHeight; y++) { @@ -682,13 +656,9 @@ public final class PSDImageReader extends ImageReaderBase { // Read entire line, if within source region and sampling if (y >= pSource.y && y < pSource.y + pSource.height && y % pYSub == 0) { if (pRLECompressed) { - DataInputStream input = PSDUtil.createPackBitsStream(imageInput, length); - try { + try (DataInputStream input = PSDUtil.createPackBitsStream(imageInput, length)) { input.readFully(pRow, 0, pChannelWidth); } - finally { - input.close(); - } } else { imageInput.readFully(pRow, 0, pChannelWidth); @@ -701,8 +671,8 @@ public final class PSDImageReader extends ImageReaderBase { byte value = pRow[pSource.x + x * pXSub]; // CMYK values are stored inverted, but alpha is not - if (isCMYK && pChannel < colorComponents) { - value = (byte) (255 - value & 0xff); + if (invert) { + value = (byte) (0xff - value & 0xff); } pData.setElem(banded ? pChannel : 0, offset + x * pBands, value); @@ -741,13 +711,9 @@ public final class PSDImageReader extends ImageReaderBase { // Read entire line, if within source region and sampling if (y >= pSource.y && y < pSource.y + pSource.height && y % pYSub == 0) { if (pRLECompressed) { - DataInputStream input = PSDUtil.createPackBitsStream(imageInput, length); - try { + try (DataInputStream input = PSDUtil.createPackBitsStream(imageInput, length)) { input.readFully(pRow, 0, pRow.length); } - finally { - input.close(); - } } else { imageInput.readFully(pRow, 0, pRow.length); @@ -868,8 +834,8 @@ public final class PSDImageReader extends ImageReaderBase { if (!header.hasValidDimensions()) { processWarningOccurred(String.format("Dimensions exceed maximum allowed for %s: %dx%d (max %dx%d)", - header.largeFormat ? "PSB" : "PSD", - header.width, header.height, header.getMaxSize(), header.getMaxSize())); + header.largeFormat ? "PSB" : "PSD", + header.width, header.height, header.getMaxSize(), header.getMaxSize())); } metadata = new PSDMetadata(); @@ -898,6 +864,10 @@ public final class PSDImageReader extends ImageReaderBase { // Don't need the header again imageInput.flushBefore(imageInput.getStreamPosition()); + + if (DEBUG) { + System.out.println("header: " + header); + } } } @@ -913,7 +883,7 @@ public final class PSDImageReader extends ImageReaderBase { if (pParseData && imageResourcesLength > 0) { if (metadata.imageResources == null) { - metadata.imageResources = new ArrayList(); + metadata.imageResources = new ArrayList<>(); long expectedEnd = imageInput.getStreamPosition() + imageResourcesLength; while (imageInput.getStreamPosition() < expectedEnd) { @@ -921,6 +891,10 @@ public final class PSDImageReader extends ImageReaderBase { metadata.imageResources.add(resource); } + if (DEBUG) { + System.out.println("imageResources: " + metadata.imageResources); + } + if (imageInput.getStreamPosition() != expectedEnd) { throw new IIOException("Corrupt PSD document"); // ..or maybe just a bug in the reader.. ;-) } @@ -970,10 +944,13 @@ public final class PSDImageReader extends ImageReaderBase { long read = imageInput.getStreamPosition() - pos; - long diff = layerInfoLength - (read - (header.largeFormat ? 8 : 4)); // - 4 for the layerInfoLength field itself + long diff = layerInfoLength - (read - (header.largeFormat + ? 8 + : 4)); // - 4 for the layerInfoLength field itself imageInput.skipBytes(diff); - } else { + } + else { metadata.layerInfo = Collections.emptyList(); } @@ -990,6 +967,10 @@ public final class PSDImageReader extends ImageReaderBase { // TODO: We should now be able to flush input // imageInput.seek(metadata.layerAndMaskInfoStart + layerAndMaskInfoLength + (header.largeFormat ? 8 : 4)); // imageInput.flushBefore(metadata.layerAndMaskInfoStart + layerAndMaskInfoLength + (header.largeFormat ? 8 : 4)); + + if (DEBUG) { + System.out.println("layerInfo: " + metadata.layerInfo); + } } } @@ -1001,20 +982,18 @@ public final class PSDImageReader extends ImageReaderBase { final int width = getLayerWidth(layerIndex); final int height = getLayerHeight(layerIndex); + // TODO: This behaviour must be documented! + // If layer has no pixel data, return null + if (width <= 0 || height <= 0) { + return null; + } + PSDLayerInfo layerInfo = metadata.layerInfo.get(layerIndex); // final int width = layerInfo.right - layerInfo.left; // final int height = layerInfo.bottom - layerInfo.top; // Even if raw/imageType has no alpha, the layers may still have alpha... ImageTypeSpecifier imageType = getRawImageTypeForLayer(layerIndex); - - // TODO: Find a better way of handling layers of size 0 - // - Return null? Return a BufferedImage subclass that has no data (0 x 0)? - // Create image (or dummy, if h/w are <= 0) -// BufferedImage layer = imageType.createBufferedImage(Math.max(1, width), Math.max(1, height)); - if (width <= 0 || height <= 0) { - return null; - } BufferedImage layer = getDestination(param, getImageTypes(layerIndex + 1), Math.max(1, width), Math.max(1, height)); imageInput.seek(findLayerStartPos(layerIndex)); @@ -1028,9 +1007,8 @@ public final class PSDImageReader extends ImageReaderBase { final WritableRaster raster = layer.getRaster(); final ColorModel destCM = layer.getColorModel(); - // TODO: This raster is 3-5 times longer than needed, depending on number of channels... ColorModel sourceCM = imageType.getColorModel(); - final WritableRaster rowRaster = width > 0 ? sourceCM.createCompatibleWritableRaster(width, 1) : null; + final WritableRaster rowRaster = sourceCM.createCompatibleWritableRaster((int) Math.ceil(width / (double) sourceCM.getNumComponents()), 1); final boolean banded = raster.getDataBuffer().getNumBanks() > 1; final int interleavedBands = banded ? 1 : raster.getNumBands(); @@ -1134,7 +1112,6 @@ public final class PSDImageReader extends ImageReaderBase { } } - // If there really is more channels, then create new imageTypeSpec if (newBandNum > compositeType.getNumBands()) { int[] indices = new int[newBandNum]; @@ -1228,7 +1205,7 @@ public final class PSDImageReader extends ImageReaderBase { for (PSDImageResource resource : metadata.imageResources) { if (resource instanceof PSDThumbnail) { if (thumbnails == null) { - thumbnails = new ArrayList(); + thumbnails = new ArrayList<>(); } thumbnails.add((PSDThumbnail) resource); @@ -1386,7 +1363,7 @@ public final class PSDImageReader extends ImageReaderBase { System.out.println("read time: " + (System.currentTimeMillis() - start)); System.out.println("image: " + image); - if (image.getColorModel().getColorSpace().getType() == ColorSpace.TYPE_CMYK) { + if (image.getType() == BufferedImage.TYPE_CUSTOM) { try { ColorConvertOp op = new ColorConvertOp(ColorSpace.getInstance(ColorSpace.CS_sRGB), null); GraphicsConfiguration gc = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration(); @@ -1407,8 +1384,24 @@ public final class PSDImageReader extends ImageReaderBase { for (int i = 1; i < images; i++) { start = System.currentTimeMillis(); BufferedImage layer = imageReader.read(i); + System.out.println("layer read time: " + (System.currentTimeMillis() - start)); System.err.println("layer: " + layer); + + if (layer != null && layer.getType() == BufferedImage.TYPE_CUSTOM) { + try { + ColorConvertOp op = new ColorConvertOp(ColorSpace.getInstance(ColorSpace.CS_sRGB), null); + GraphicsConfiguration gc = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration(); + layer = op.filter(layer, gc.createCompatibleImage(layer.getWidth(), layer.getHeight(), layer.getTransparency())); + } + catch (Exception e) { + e.printStackTrace(); + layer = ImageUtil.accelerate(layer); + } + System.out.println("layer conversion time: " + (System.currentTimeMillis() - start)); + System.out.println("layer: " + layer); + } + showIt(layer, "layer " + i); } } diff --git a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDLayerInfo.java b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDLayerInfo.java index 1fc8fd82..7b92883c 100755 --- a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDLayerInfo.java +++ b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDLayerInfo.java @@ -50,7 +50,10 @@ final class PSDLayerInfo { final PSDLayerBlendMode blendMode; final PSDLayerMaskData layerMaskData; final PSDChannelSourceDestinationRange[] ranges; - final String layerName; + private final String layerName; + + private String unicodeLayerName; + private int layerId; PSDLayerInfo(final boolean largeFormat, final ImageInputStream pInput) throws IOException { top = pInput.readInt(); @@ -70,11 +73,11 @@ final class PSDLayerInfo { blendMode = new PSDLayerBlendMode(pInput); - // Length of layer mask data + // Length of layer extra data long extraDataSize = pInput.readUnsignedInt(); // Layer mask/adjustment layer data - int layerMaskDataSize = pInput.readInt(); // May be 0, 20 or 36 bytes... + int layerMaskDataSize = pInput.readInt(); // May be 0, 20 or variable (up to 55) bytes... if (layerMaskDataSize != 0) { layerMaskData = new PSDLayerMaskData(pInput, layerMaskDataSize); } @@ -92,19 +95,78 @@ final class PSDLayerInfo { ranges[i] = new PSDChannelSourceDestinationRange(pInput, (i == 0 ? "Gray" : "Channel " + (i - 1))); } + // Layer name layerName = PSDUtil.readPascalString(pInput); - int layerNameSize = layerName.length() + 1; // Skip pad bytes for long word alignment if (layerNameSize % 4 != 0) { - int skip = layerNameSize % 4; + int skip = 4 - (layerNameSize % 4); pInput.skipBytes(skip); layerNameSize += skip; } - // TODO: Consider reading this: Adjustment layer info etc... - pInput.skipBytes(extraDataSize - layerMaskDataSize - 4 - layerBlendingDataSize - 4 - layerNameSize); + // Parse "Additional layer data" + long additionalLayerInfoStart = pInput.getStreamPosition(); + long expectedEnd = additionalLayerInfoStart + extraDataSize - layerMaskDataSize - 4 - layerBlendingDataSize - 4 - layerNameSize; + while (pInput.getStreamPosition() < expectedEnd) { + // 8BIM or 8B64 + int resourceSignature = pInput.readInt(); + + if (resourceSignature != PSD.RESOURCE_TYPE && resourceSignature != PSD.RESOURCE_TYPE_LONG) { + // Could be a corrupt document, or some new resource (type) we don't know about, + // we'll just leave it and carry on, as this is all secondary information for the reader. + break; + } + + int resourceKey = pInput.readInt(); + + // NOTE: Only SOME resources have long length fields... + boolean largeResource = resourceSignature != PSD.RESOURCE_TYPE; + long resourceLength = largeResource ? pInput.readLong() : pInput.readUnsignedInt(); + long resourceStart = pInput.getStreamPosition(); + +// System.out.printf("signature: %s 0x%08x\n", PSDUtil.intToStr(resourceSignature), resourceSignature); +// System.out.println("key: " + PSDUtil.intToStr(resourceKey)); +// System.out.println("length: " + resourceLength); + + switch (resourceKey) { + case PSD.luni: + unicodeLayerName = PSDUtil.readUnicodeString(pInput); + // There's usually a 0-pad here, but it is skipped in the general re-aligning code below + break; + + case PSD.lyid: + if (resourceLength != 4) { + throw new IIOException(String.format("Expected layerId length == 4: %d", resourceLength)); + } + layerId = pInput.readInt(); + break; + + default: + // TODO: Parse more data... + pInput.skipBytes(resourceLength); + break; + } + + // Re-align in case we got the length incorrect + if (pInput.getStreamPosition() != resourceStart + resourceLength) { + pInput.seek(resourceStart + resourceLength); + } + } + + // Re-align in case we got the length incorrect + if (pInput.getStreamPosition() != expectedEnd) { + pInput.seek(expectedEnd); + } + } + + String getLayerName() { + return unicodeLayerName != null ? unicodeLayerName : layerName; + } + + int getLayerId() { + return layerId; } @Override @@ -122,7 +184,7 @@ final class PSDLayerInfo { builder.append(", layer mask data: ").append(layerMaskData); } builder.append(", ranges: ").append(Arrays.toString(ranges)); - builder.append(", layer name: \"").append(layerName).append("\""); + builder.append(", layer name: \"").append(getLayerName()).append("\""); builder.append("]"); return builder.toString(); diff --git a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDLayerMaskData.java b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDLayerMaskData.java index aa13943c..28e27b9c 100755 --- a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDLayerMaskData.java +++ b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDLayerMaskData.java @@ -47,42 +47,90 @@ final class PSDLayerMaskData { private int defaultColor; private int flags; - private boolean large; - private int realFlags; - private int realUserBackground; - private int realTop; - private int realLeft; - private int realBottom; - private int realRight; + private int maskParams; + private int userMaskDensity; + private double userMaskFeather; + private int vectorMaskDensity; + private double vectorMaskFeather; PSDLayerMaskData(final ImageInputStream pInput, final int pSize) throws IOException { - if (pSize != 20 && pSize != 36) { - throw new IIOException("Illegal PSD Layer Mask data size: " + pSize + " (expected 20 or 36)"); + if (pSize < 20 || pSize > 55) { + throw new IIOException("Illegal PSD Layer Mask data size: " + pSize + " (expected between 20 and 55)"); } + // Rectangle enclosing layer mask: Top, left, bottom, right. top = pInput.readInt(); left = pInput.readInt(); bottom = pInput.readInt(); right = pInput.readInt(); + // Default color. 0 or 255 defaultColor = pInput.readUnsignedByte(); + // Flags. + // bit 0 = position relative to layer + // bit 1 = layer mask disabled + // bit 2 = invert layer mask when blending (Obsolete) + // bit 3 = indicates that the user mask actually came from rendering other data + // bit 4 = indicates that the user and/or vector masks have parameters applied to them flags = pInput.readUnsignedByte(); - if (pSize == 20) { + int dataLeft = pSize - 18; + + if ((flags & 0x10) != 0) { + // Mask Parameters. Only present if bit 4 of Flags set above. + maskParams = pInput.readUnsignedByte(); + dataLeft--; + + // Mask Parameters bit flags present as follows: + // bit 0 = user mask density, 1 byte + // bit 1 = user mask feather, 8 byte, double + // bit 2 = vector mask density, 1 byte + // bit 3 = vector mask feather, 8 bytes, double + if ((maskParams & 0x01) != 0) { + userMaskDensity = pInput.readByte(); + dataLeft--; + } + if ((maskParams & 0x02) != 0) { + userMaskFeather = pInput.readDouble(); + dataLeft -= 8; + } + if ((maskParams & 0x04) != 0) { + vectorMaskDensity = pInput.readByte(); + dataLeft--; + } + if ((maskParams & 0x08) != 0) { + vectorMaskFeather = pInput.readDouble(); + dataLeft -= 8; + } + } + + // Padding. Only present if size = 20. Otherwise the following is present + if (pSize == 20 && dataLeft == 2) { pInput.readShort(); // Pad + dataLeft -= 2; } else { - // TODO: What to make out of this? - large = true; + if (dataLeft >= 2) { + // Real Flags. Same as Flags information above. + flags = pInput.readUnsignedByte(); + dataLeft--; + // Real user mask background. 0 or 255. + defaultColor = pInput.readUnsignedByte(); + dataLeft--; + } + if (dataLeft >= 16) { + // Rectangle enclosing layer mask: Top, left, bottom, right. + top = pInput.readInt(); + left = pInput.readInt(); + bottom = pInput.readInt(); + right = pInput.readInt(); + dataLeft -= 16; + } + } - realFlags = pInput.readUnsignedByte(); - realUserBackground = pInput.readUnsignedByte(); - - realTop = pInput.readInt(); - realLeft = pInput.readInt(); - realBottom = pInput.readInt(); - realRight = pInput.readInt(); + if (dataLeft > 0) { + pInput.skipBytes(dataLeft); } } @@ -97,40 +145,54 @@ final class PSDLayerMaskData { builder.append(", default color: ").append(defaultColor); builder.append(", flags: ").append(Integer.toBinaryString(flags)); - // TODO: Maybe the flag bits have oposite order? builder.append(" ("); if ((flags & 0x01) != 0) { - builder.append("Pos. rel. to layer"); + builder.append("relative"); } else { - builder.append("Pos. abs."); + builder.append("absolute"); } if ((flags & 0x02) != 0) { - builder.append(", Mask disabled"); + builder.append(", disabled"); } else { - builder.append(", Mask enabled"); + builder.append(", enabled"); } if ((flags & 0x04) != 0) { - builder.append(", Invert mask"); + builder.append(", inverted"); } if ((flags & 0x08) != 0) { - builder.append(", Unknown bit 3"); + builder.append(", from rendered data"); } if ((flags & 0x10) != 0) { - builder.append(", Unknown bit 4"); + builder.append(", has parameters"); } if ((flags & 0x20) != 0) { - builder.append(", Unknown bit 5"); + builder.append(", unknown flag (bit 5)"); } if ((flags & 0x40) != 0) { - builder.append(", Unknown bit 6"); + builder.append(", unknown flag (bit 6)"); } if ((flags & 0x80) != 0) { - builder.append(", Unknown bit 7"); + builder.append(", unknown flag (bit 7)"); } builder.append(")"); + if ((flags & 0x10) != 0) { + if ((maskParams & 0x01) != 0) { + builder.append(", userMaskDensity: ").append(userMaskDensity); + } + if ((maskParams & 0x02) != 0) { + builder.append(", userMaskFeather: ").append(userMaskFeather); + } + if ((maskParams & 0x04) != 0) { + builder.append(", vectorMaskDensity: ").append(vectorMaskDensity); + } + if ((maskParams & 0x08) != 0) { + builder.append(", vectorMaskFeather: ").append(vectorMaskFeather); + } + } + builder.append("]"); return builder.toString(); } diff --git a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDMetadata.java b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDMetadata.java index 19bd5d7f..f59cb0b3 100755 --- a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDMetadata.java +++ b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDMetadata.java @@ -28,6 +28,7 @@ package com.twelvemonkeys.imageio.plugins.psd; +import com.twelvemonkeys.imageio.AbstractMetadata; import com.twelvemonkeys.imageio.metadata.Directory; import com.twelvemonkeys.imageio.metadata.Entry; import com.twelvemonkeys.imageio.metadata.exif.TIFF; @@ -374,7 +375,7 @@ public final class PSDMetadata extends AbstractMetadata { for (PSDLayerInfo psdLayerInfo : layerInfo) { // TODO: Group in layer and use sub node for blend mode? node = new IIOMetadataNode("LayerInfo"); - node.setAttribute("name", psdLayerInfo.layerName); + node.setAttribute("name", psdLayerInfo.getLayerName()); node.setAttribute("top", String.valueOf(psdLayerInfo.top)); node.setAttribute("left", String.valueOf(psdLayerInfo.left)); node.setAttribute("bottom", String.valueOf(psdLayerInfo.bottom)); @@ -571,9 +572,8 @@ public final class PSDMetadata extends AbstractMetadata { compressionNode.appendChild(compressionTypeName); if (compression != PSD.COMPRESSION_NONE) { - IIOMetadataNode lossless = new IIOMetadataNode("Lossless"); - lossless.setAttribute("value", "true"); - compressionNode.appendChild(lossless); + compressionNode.appendChild(new IIOMetadataNode("Lossless")); + // "value" defaults to TRUE, all PSD compressions are lossless } return compressionNode; @@ -755,7 +755,7 @@ public final class PSDMetadata extends AbstractMetadata { } private void appendTextEntriesFlat(final IIOMetadataNode node, final Directory directory, final FilterIterator.Filter filter) { - FilterIterator entries = new FilterIterator(directory.iterator(), filter); + FilterIterator entries = new FilterIterator<>(directory.iterator(), filter); while (entries.hasNext()) { Entry entry = entries.next(); @@ -807,7 +807,7 @@ public final class PSDMetadata extends AbstractMetadata { @SuppressWarnings({"unchecked"}) Iterator iterator = (Iterator) imageResources.iterator(); - return new FilterIterator(iterator, new FilterIterator.Filter() { + return new FilterIterator<>(iterator, new FilterIterator.Filter() { public boolean accept(final T pElement) { return resourceType.isInstance(pElement); } @@ -817,7 +817,7 @@ public final class PSDMetadata extends AbstractMetadata { Iterator getResources(final int... resourceTypes) { Iterator iterator = imageResources.iterator(); - return new FilterIterator(iterator, new FilterIterator.Filter() { + return new FilterIterator<>(iterator, new FilterIterator.Filter() { public boolean accept(final PSDImageResource pResource) { for (int type : resourceTypes) { if (type == pResource.id) { diff --git a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDMetadataFormat.java b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDMetadataFormat.java index 50e99677..1110ca86 100755 --- a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDMetadataFormat.java +++ b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDMetadataFormat.java @@ -34,7 +34,9 @@ import org.w3c.dom.Document; import javax.imageio.ImageTypeSpecifier; import javax.imageio.metadata.IIOMetadataFormatImpl; import java.awt.image.BufferedImage; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; /** @@ -46,8 +48,6 @@ import java.util.List; */ public final class PSDMetadataFormat extends IIOMetadataFormatImpl { - private static final PSDMetadataFormat instance = new PSDMetadataFormat(); - static final List PSD_BLEND_MODES = Arrays.asList( PSDUtil.intToStr(PSD.BLEND_PASS), PSDUtil.intToStr(PSD.BLEND_NORM), PSDUtil.intToStr(PSD.BLEND_DISS), PSDUtil.intToStr(PSD.BLEND_DARK), PSDUtil.intToStr(PSD.BLEND_MUL), PSDUtil.intToStr(PSD.BLEND_IDIV), @@ -61,6 +61,8 @@ public final class PSDMetadataFormat extends IIOMetadataFormatImpl { PSDUtil.intToStr(PSD.BLEND_LUM) ); + private static final PSDMetadataFormat instance = new PSDMetadataFormat(); + /** * Private constructor. *

@@ -79,7 +81,7 @@ public final class PSDMetadataFormat extends IIOMetadataFormatImpl { 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", "version", DATATYPE_INTEGER, false, "1", Collections.singletonList("1")); addAttribute("Header", "channels", DATATYPE_INTEGER, true, null, "1", "24", true, true); // rows? @@ -87,7 +89,8 @@ public final class PSDMetadataFormat extends IIOMetadataFormatImpl { // 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)); + + addAttribute("Header", "mode", DATATYPE_STRING, true, null, asListNoNulls(PSDMetadata.COLOR_MODES)); /* Contains the required data to define the color mode. @@ -133,7 +136,7 @@ public final class PSDMetadataFormat extends IIOMetadataFormatImpl { 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", "colorSpace", DATATYPE_STRING, true, null, asListNoNulls(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 @@ -196,17 +199,16 @@ public final class PSDMetadataFormat extends IIOMetadataFormatImpl { // 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", "hResUnit", DATATYPE_STRING, true, null, asListNoNulls(PSDMetadata.RESOLUTION_UNITS)); + addAttribute("ResolutionInfo", "widthUnit", DATATYPE_STRING, true, null, asListNoNulls(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)); + addAttribute("ResolutionInfo", "vResUnit", DATATYPE_STRING, true, null, asListNoNulls(PSDMetadata.RESOLUTION_UNITS)); + addAttribute("ResolutionInfo", "heightUnit", DATATYPE_STRING, true, null, asListNoNulls(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"); @@ -229,10 +231,10 @@ public final class PSDMetadataFormat extends IIOMetadataFormatImpl { addObjectValue("XMP", Document.class, true, null); // root -> Layers - addElement("Layers", PSDMetadata.NATIVE_METADATA_FORMAT_NAME, CHILD_POLICY_REPEAT); + addElement("Layers", PSDMetadata.NATIVE_METADATA_FORMAT_NAME, 0, Short.MAX_VALUE); // root -> Layers -> LayerInfo - addElement("LayerInfo", "Layers", CHILD_POLICY_REPEAT); + addElement("LayerInfo", "Layers", CHILD_POLICY_EMPTY); addAttribute("LayerInfo", "name", DATATYPE_STRING, false, ""); addAttribute("LayerInfo", "top", DATATYPE_INTEGER, false, "0"); addAttribute("LayerInfo", "left", DATATYPE_INTEGER, false, "0"); @@ -258,6 +260,18 @@ public final class PSDMetadataFormat extends IIOMetadataFormatImpl { addAttribute("GlobalLayerMask", "kind", DATATYPE_STRING, false, "layer", Arrays.asList("selected", "protected", "layer")); } + private static List asListNoNulls(final T[] values) { + List list = new ArrayList<>(values.length); + + for (T value : values) { + if (value != null) { + list.add(value); + } + } + + return list; + } + @Override public boolean canNodeAppear(final String elementName, final ImageTypeSpecifier imageType) { // TODO: PSDColorData and PaletteEntry only for indexed color model diff --git a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDProviderInfo.java b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDProviderInfo.java index 2b52c770..1767cc50 100644 --- a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDProviderInfo.java +++ b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDProviderInfo.java @@ -1,3 +1,31 @@ +/* + * Copyright (c) 2015, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + package com.twelvemonkeys.imageio.plugins.psd; import com.twelvemonkeys.imageio.spi.ReaderWriterProviderInfo; @@ -22,10 +50,10 @@ final class PSDProviderInfo extends ReaderWriterProviderInfo { "application/x-photoshop", "image/x-photoshop" }, - "com.twelvemkonkeys.imageio.plugins.psd.PSDImageReader", + "com.twelvemonkeys.imageio.plugins.psd.PSDImageReader", new String[] {"com.twelvemonkeys.imageio.plugins.psd.PSDImageReaderSpi"}, null, - null, // new String[] {"com.twelvemkonkeys.imageio.plugins.psd.PSDImageWriterSpi"}, + null, // new String[] {"com.twelvemonkeys.imageio.plugins.psd.PSDImageWriterSpi"}, false, null, null, null, null, true, PSDMetadata.NATIVE_METADATA_FORMAT_NAME, PSDMetadata.NATIVE_METADATA_FORMAT_CLASS_NAME, null, null ); diff --git a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDUtil.java b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDUtil.java index 7945f837..5cd5f0c2 100644 --- a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDUtil.java +++ b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDUtil.java @@ -51,7 +51,7 @@ final class PSDUtil { static String intToStr(int value) { return new String( new byte[]{ - (byte) ((value & 0xff000000) >> 24), + (byte) ((value & 0xff000000) >>> 24), (byte) ((value & 0x00ff0000) >> 16), (byte) ((value & 0x0000ff00) >> 8), (byte) ((value & 0x000000ff)) diff --git a/imageio/imageio-psd/src/test/java/com/twelvemonkeys/imageio/plugins/psd/PSDImageReaderTestCase.java b/imageio/imageio-psd/src/test/java/com/twelvemonkeys/imageio/plugins/psd/PSDImageReaderTest.java similarity index 91% rename from imageio/imageio-psd/src/test/java/com/twelvemonkeys/imageio/plugins/psd/PSDImageReaderTestCase.java rename to imageio/imageio-psd/src/test/java/com/twelvemonkeys/imageio/plugins/psd/PSDImageReaderTest.java index a5808c20..73ca0e32 100755 --- a/imageio/imageio-psd/src/test/java/com/twelvemonkeys/imageio/plugins/psd/PSDImageReaderTestCase.java +++ b/imageio/imageio-psd/src/test/java/com/twelvemonkeys/imageio/plugins/psd/PSDImageReaderTest.java @@ -28,32 +28,35 @@ package com.twelvemonkeys.imageio.plugins.psd; -import com.twelvemonkeys.imageio.util.ImageReaderAbstractTestCase; +import com.twelvemonkeys.imageio.util.ImageReaderAbstractTest; import com.twelvemonkeys.imageio.util.ProgressListenerBase; import org.junit.Test; +import org.w3c.dom.NodeList; +import javax.imageio.ImageIO; import javax.imageio.ImageReadParam; -import javax.imageio.ImageTypeSpecifier; -import javax.imageio.spi.ImageReaderSpi; import javax.imageio.ImageReader; +import javax.imageio.ImageTypeSpecifier; +import javax.imageio.metadata.IIOMetadata; +import javax.imageio.metadata.IIOMetadataNode; +import javax.imageio.spi.ImageReaderSpi; +import javax.imageio.stream.ImageInputStream; import java.awt.*; import java.awt.image.BufferedImage; -import java.util.Arrays; -import java.util.Iterator; -import java.util.List; -import java.util.ArrayList; import java.io.IOException; +import java.util.*; +import java.util.List; import static org.junit.Assert.*; /** - * PSDImageReaderTestCase + * PSDImageReaderTest * * @author Harald Kuhr * @author last modified by $Author: haraldk$ - * @version $Id: PSDImageReaderTestCase.java,v 1.0 Apr 1, 2008 10:39:17 PM haraldk Exp$ + * @version $Id: PSDImageReaderTest.java,v 1.0 Apr 1, 2008 10:39:17 PM haraldk Exp$ */ -public class PSDImageReaderTestCase extends ImageReaderAbstractTestCase { +public class PSDImageReaderTest extends ImageReaderAbstractTest { private static final ImageReaderSpi provider = new PSDImageReaderSpi(); @@ -110,11 +113,11 @@ public class PSDImageReaderTestCase extends ImageReaderAbstractTestCase getFormatNames() { - return Arrays.asList("psd"); + return Collections.singletonList("psd"); } protected List getSuffixes() { - return Arrays.asList("psd"); + return Collections.singletonList("psd"); } protected List getMIMETypes() { @@ -243,7 +246,7 @@ public class PSDImageReaderTestCase extends ImageReaderAbstractTestCase sequnce = new ArrayList(); + final List sequnce = new ArrayList<>(); imageReader.addIIOReadProgressListener(new ProgressListenerBase() { private float mLastPercentageDone = 0; @@ -387,4 +390,20 @@ public class PSDImageReaderTestCase extends ImageReaderAbstractTestCaseHarald Kuhr + * @author last modified by $Author: harald.kuhr$ + * @version $Id: PSDProviderInfoTest.java,v 1.0 02/06/16 harald.kuhr Exp$ + */ +public class PSDProviderInfoTest extends ReaderWriterProviderInfoTest { + + @Override + protected ReaderWriterProviderInfo createProviderInfo() { + return new PSDProviderInfo(); + } +} \ No newline at end of file diff --git a/imageio/imageio-psd/src/test/resources/psd/long-layer-names.psd b/imageio/imageio-psd/src/test/resources/psd/long-layer-names.psd new file mode 100644 index 00000000..452d61e6 Binary files /dev/null and b/imageio/imageio-psd/src/test/resources/psd/long-layer-names.psd differ diff --git a/imageio/imageio-psd/todo.txt b/imageio/imageio-psd/todo.txt index 7d731b62..2b0ec7fb 100755 --- a/imageio/imageio-psd/todo.txt +++ b/imageio/imageio-psd/todo.txt @@ -1,4 +1,3 @@ -- Implement source subsampling and region of interest - Separate package for the resources (seems to be a lot)? - Move to metadata package, or implement metadata interface, as this is (similar|equivalent) to TIFF/JPEG APP13 segment tag? - Possibility to read only some resources? readResources(int[] resourceKeys)? diff --git a/imageio/imageio-reference/pom.xml b/imageio/imageio-reference/pom.xml index c636b8ed..00899b72 100644 --- a/imageio/imageio-reference/pom.xml +++ b/imageio/imageio-reference/pom.xml @@ -4,7 +4,7 @@ com.twelvemonkeys.imageio imageio - 3.1-SNAPSHOT + 3.3-SNAPSHOT imageio-reference TwelveMonkeys :: ImageIO :: reference test cases @@ -20,7 +20,7 @@ com.twelvemonkeys.imageio imageio-core - tests + test-jar diff --git a/imageio/imageio-reference/src/test/java/com/twelvemonkeys/imageio/reference/JPEGImageReaderTestCase.java b/imageio/imageio-reference/src/test/java/com/twelvemonkeys/imageio/reference/JPEGImageReaderTest.java similarity index 68% rename from imageio/imageio-reference/src/test/java/com/twelvemonkeys/imageio/reference/JPEGImageReaderTestCase.java rename to imageio/imageio-reference/src/test/java/com/twelvemonkeys/imageio/reference/JPEGImageReaderTest.java index 69da521b..be36f205 100644 --- a/imageio/imageio-reference/src/test/java/com/twelvemonkeys/imageio/reference/JPEGImageReaderTestCase.java +++ b/imageio/imageio-reference/src/test/java/com/twelvemonkeys/imageio/reference/JPEGImageReaderTest.java @@ -1,34 +1,47 @@ package com.twelvemonkeys.imageio.reference; -import com.sun.imageio.plugins.jpeg.JPEGImageReader; -import com.sun.imageio.plugins.jpeg.JPEGImageReaderSpi; -import com.twelvemonkeys.imageio.util.ImageReaderAbstractTestCase; +import com.twelvemonkeys.imageio.util.ImageReaderAbstractTest; import com.twelvemonkeys.lang.SystemUtil; import org.junit.Ignore; import org.junit.Test; import javax.imageio.IIOException; +import javax.imageio.ImageReader; import javax.imageio.spi.ImageReaderSpi; import java.awt.*; import java.io.IOException; import java.util.Arrays; +import java.util.Collections; import java.util.List; +import static org.junit.Assume.assumeNoException; + /** - * JPEGImageReaderTestCase + * JPEGImageReaderTest * * @author Harald Kuhr * @author last modified by $Author: haraldk$ - * @version $Id: JPEGImageReaderTestCase.java,v 1.0 Oct 9, 2009 3:37:25 PM haraldk Exp$ + * @version $Id: JPEGImageReaderTest.java,v 1.0 Oct 9, 2009 3:37:25 PM haraldk Exp$ */ -public class JPEGImageReaderTestCase extends ImageReaderAbstractTestCase { +public class JPEGImageReaderTest extends ImageReaderAbstractTest { private static final boolean IS_JAVA_6 = SystemUtil.isClassAvailable("java.util.Deque"); - protected JPEGImageReaderSpi provider = new JPEGImageReaderSpi(); + protected final ImageReaderSpi provider = lookupSpi(); + + private ImageReaderSpi lookupSpi() { + try { + return (ImageReaderSpi) Class.forName("com.sun.imageio.plugins.jpeg.JPEGImageReaderSpi").newInstance(); + } + catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) { + assumeNoException(e); + } + + return null; + } @Override protected List getTestData() { - return Arrays.asList( + return Collections.singletonList( new TestData(getClassLoaderResource("/jpeg/R-7439-1151526181.jpeg"), new Dimension(386, 396)) ); } @@ -39,14 +52,21 @@ public class JPEGImageReaderTestCase extends ImageReaderAbstractTestCase getReaderClass() { - return JPEGImageReader.class; + protected Class getReaderClass() { + try { + return Class.forName("com.sun.imageio.plugins.jpeg.JPEGImageReader"); + } + catch (ClassNotFoundException e) { + assumeNoException(e); + } + + return null; } @Override - protected JPEGImageReader createReader() { + protected ImageReader createReader() { try { - return (JPEGImageReader) provider.createReaderInstance(); + return provider.createReaderInstance(); } catch (IOException e) { throw new RuntimeException(e); diff --git a/imageio/imageio-reference/src/test/java/com/twelvemonkeys/imageio/reference/PNGImageReaderTestCase.java b/imageio/imageio-reference/src/test/java/com/twelvemonkeys/imageio/reference/PNGImageReaderTest.java similarity index 56% rename from imageio/imageio-reference/src/test/java/com/twelvemonkeys/imageio/reference/PNGImageReaderTestCase.java rename to imageio/imageio-reference/src/test/java/com/twelvemonkeys/imageio/reference/PNGImageReaderTest.java index 8b1a7269..4bbf5522 100644 --- a/imageio/imageio-reference/src/test/java/com/twelvemonkeys/imageio/reference/PNGImageReaderTestCase.java +++ b/imageio/imageio-reference/src/test/java/com/twelvemonkeys/imageio/reference/PNGImageReaderTest.java @@ -1,30 +1,43 @@ package com.twelvemonkeys.imageio.reference; -import com.sun.imageio.plugins.png.PNGImageReader; -import com.sun.imageio.plugins.png.PNGImageReaderSpi; -import com.twelvemonkeys.imageio.util.ImageReaderAbstractTestCase; +import com.twelvemonkeys.imageio.util.ImageReaderAbstractTest; import org.junit.Test; import javax.imageio.IIOException; +import javax.imageio.ImageReader; import javax.imageio.spi.ImageReaderSpi; import java.awt.*; import java.io.IOException; import java.util.Arrays; +import java.util.Collections; import java.util.List; +import static org.junit.Assume.assumeNoException; + /** - * PNGImageReaderTestCase + * PNGImageReaderTest * * @author Harald Kuhr * @author last modified by $Author: haraldk$ - * @version $Id: PNGImageReaderTestCase.java,v 1.0 Oct 9, 2009 3:37:25 PM haraldk Exp$ + * @version $Id: PNGImageReaderTest.java,v 1.0 Oct 9, 2009 3:37:25 PM haraldk Exp$ */ -public class PNGImageReaderTestCase extends ImageReaderAbstractTestCase { - protected PNGImageReaderSpi provider = new PNGImageReaderSpi(); +public class PNGImageReaderTest extends ImageReaderAbstractTest { + protected final ImageReaderSpi provider = lookupSpi(); + + private ImageReaderSpi lookupSpi() { + try { + return (ImageReaderSpi) Class.forName("com.sun.imageio.plugins.png.PNGImageReaderSpi").newInstance(); + } + catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) { + assumeNoException(e); + } + + return null; + } @Override protected List getTestData() { - return Arrays.asList( + return Collections.singletonList( new TestData(getClassLoaderResource("/png/12monkeys-splash.png"), new Dimension(300, 410)) ); } @@ -35,14 +48,21 @@ public class PNGImageReaderTestCase extends ImageReaderAbstractTestCase getReaderClass() { - return PNGImageReader.class; + protected Class getReaderClass() { + try { + return Class.forName("com.sun.imageio.plugins.png.PNGImageReader"); + } + catch (ClassNotFoundException e) { + assumeNoException(e); + } + + return null; } @Override - protected PNGImageReader createReader() { + protected ImageReader createReader() { try { - return (PNGImageReader) provider.createReaderInstance(); + return provider.createReaderInstance(); } catch (IOException e) { throw new RuntimeException(e); diff --git a/imageio/imageio-sgi/pom.xml b/imageio/imageio-sgi/pom.xml index 6dee4429..0296d502 100755 --- a/imageio/imageio-sgi/pom.xml +++ b/imageio/imageio-sgi/pom.xml @@ -1,12 +1,10 @@ - + 4.0.0 com.twelvemonkeys.imageio imageio - 3.1-SNAPSHOT + 3.3-SNAPSHOT imageio-sgi TwelveMonkeys :: ImageIO :: SGI plugin @@ -22,7 +20,7 @@ com.twelvemonkeys.imageio imageio-core - tests + test-jar diff --git a/imageio/imageio-sgi/src/main/java/com/twelvemonkeys/imageio/plugins/sgi/SGIMetadata.java b/imageio/imageio-sgi/src/main/java/com/twelvemonkeys/imageio/plugins/sgi/SGIMetadata.java index 16e8b8b6..5e4614fd 100755 --- a/imageio/imageio-sgi/src/main/java/com/twelvemonkeys/imageio/plugins/sgi/SGIMetadata.java +++ b/imageio/imageio-sgi/src/main/java/com/twelvemonkeys/imageio/plugins/sgi/SGIMetadata.java @@ -28,48 +28,19 @@ package com.twelvemonkeys.imageio.plugins.sgi; -import org.w3c.dom.Node; +import com.twelvemonkeys.imageio.AbstractMetadata; -import javax.imageio.metadata.IIOMetadata; -import javax.imageio.metadata.IIOMetadataFormatImpl; import javax.imageio.metadata.IIOMetadataNode; -final class SGIMetadata extends IIOMetadata { - // TODO: Clean up & extend AbstractMetadata (after moving from PSD -> Core) - +final class SGIMetadata extends AbstractMetadata { private final SGIHeader header; SGIMetadata(final SGIHeader header) { this.header = header; - standardFormatSupported = true; } - @Override public boolean isReadOnly() { - return true; - } - - @Override public Node getAsTree(final String formatName) { - if (IIOMetadataFormatImpl.standardMetadataFormatName.equals(formatName)) { - return getStandardTree(); - } - else { - throw new IllegalArgumentException("Unsupported metadata format: " + formatName); - } - } - - @Override public void mergeTree(final String formatName, final Node root) { - if (isReadOnly()) { - throw new IllegalStateException("Metadata is read-only"); - } - } - - @Override public void reset() { - if (isReadOnly()) { - throw new IllegalStateException("Metadata is read-only"); - } - } - - @Override protected IIOMetadataNode getStandardChromaNode() { + @Override + protected IIOMetadataNode getStandardChromaNode() { IIOMetadataNode chroma = new IIOMetadataNode("Chroma"); // NOTE: There doesn't seem to be any god way to determine color space, other than by convention @@ -117,12 +88,15 @@ final class SGIMetadata extends IIOMetadata { // No compression - @Override protected IIOMetadataNode getStandardCompressionNode() { + @Override + protected IIOMetadataNode getStandardCompressionNode() { if (header.getCompression() != SGI.COMPRESSION_NONE) { IIOMetadataNode node = new IIOMetadataNode("Compression"); IIOMetadataNode compressionTypeName = new IIOMetadataNode("CompressionTypeName"); - compressionTypeName.setAttribute("value", header.getCompression() == SGI.COMPRESSION_RLE ? "RLE" : "Uknown"); + compressionTypeName.setAttribute("value", header.getCompression() == SGI.COMPRESSION_RLE + ? "RLE" + : "Uknown"); node.appendChild(compressionTypeName); IIOMetadataNode lossless = new IIOMetadataNode("Lossless"); @@ -135,7 +109,8 @@ final class SGIMetadata extends IIOMetadata { return null; } - @Override protected IIOMetadataNode getStandardDataNode() { + @Override + protected IIOMetadataNode getStandardDataNode() { IIOMetadataNode node = new IIOMetadataNode("Data"); IIOMetadataNode sampleFormat = new IIOMetadataNode("SampleFormat"); @@ -183,7 +158,8 @@ final class SGIMetadata extends IIOMetadata { return buffer.toString(); } - @Override protected IIOMetadataNode getStandardDimensionNode() { + @Override + protected IIOMetadataNode getStandardDimensionNode() { IIOMetadataNode dimension = new IIOMetadataNode("Dimension"); IIOMetadataNode imageOrientation = new IIOMetadataNode("ImageOrientation"); @@ -195,7 +171,8 @@ final class SGIMetadata extends IIOMetadata { // No document node - @Override protected IIOMetadataNode getStandardTextNode() { + @Override + protected IIOMetadataNode getStandardTextNode() { if (!header.getName().isEmpty()) { IIOMetadataNode text = new IIOMetadataNode("Text"); @@ -212,14 +189,17 @@ final class SGIMetadata extends IIOMetadata { // No tiling - @Override protected IIOMetadataNode getStandardTransparencyNode() { + @Override + protected IIOMetadataNode getStandardTransparencyNode() { // NOTE: There doesn't seem to be any god way to determine transparency, other than by convention // 1 channel: Gray, 2 channel: Gray + Alpha, 3 channel: RGB, 4 channel: RGBA (hopefully never CMYK...) IIOMetadataNode transparency = new IIOMetadataNode("Transparency"); IIOMetadataNode alpha = new IIOMetadataNode("Alpha"); - alpha.setAttribute("value", header.getChannels() == 1 || header.getChannels() == 3 ? "none" : "nonpremultiplied"); + alpha.setAttribute("value", header.getChannels() == 1 || header.getChannels() == 3 + ? "none" + : "nonpremultiplied"); transparency.appendChild(alpha); return transparency; diff --git a/imageio/imageio-sgi/src/main/java/com/twelvemonkeys/imageio/plugins/sgi/SGIProviderInfo.java b/imageio/imageio-sgi/src/main/java/com/twelvemonkeys/imageio/plugins/sgi/SGIProviderInfo.java index bcfd503b..d49a948c 100644 --- a/imageio/imageio-sgi/src/main/java/com/twelvemonkeys/imageio/plugins/sgi/SGIProviderInfo.java +++ b/imageio/imageio-sgi/src/main/java/com/twelvemonkeys/imageio/plugins/sgi/SGIProviderInfo.java @@ -1,3 +1,31 @@ +/* + * Copyright (c) 2015, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + package com.twelvemonkeys.imageio.plugins.sgi; import com.twelvemonkeys.imageio.spi.ReaderWriterProviderInfo; @@ -23,7 +51,7 @@ final class SGIProviderInfo extends ReaderWriterProviderInfo { "image/sgi", "image/x-sgi", }, - "com.twelvemkonkeys.imageio.plugins.sgi.SGIImageReader", + "com.twelvemonkeys.imageio.plugins.sgi.SGIImageReader", new String[] {"com.twelvemonkeys.imageio.plugins.sgi.SGIImageReaderSpi"}, null, null, diff --git a/imageio/imageio-sgi/src/test/java/com/twelvemonkeys/imageio/plugins/sgi/SGIImageReaderTest.java b/imageio/imageio-sgi/src/test/java/com/twelvemonkeys/imageio/plugins/sgi/SGIImageReaderTest.java index 209d8efd..8d6d8112 100755 --- a/imageio/imageio-sgi/src/test/java/com/twelvemonkeys/imageio/plugins/sgi/SGIImageReaderTest.java +++ b/imageio/imageio-sgi/src/test/java/com/twelvemonkeys/imageio/plugins/sgi/SGIImageReaderTest.java @@ -28,11 +28,12 @@ package com.twelvemonkeys.imageio.plugins.sgi; -import com.twelvemonkeys.imageio.util.ImageReaderAbstractTestCase; +import com.twelvemonkeys.imageio.util.ImageReaderAbstractTest; import javax.imageio.spi.ImageReaderSpi; import java.awt.*; import java.util.Arrays; +import java.util.Collections; import java.util.List; /** @@ -42,10 +43,10 @@ import java.util.List; * @author last modified by $Author: haraldk$ * @version $Id: SGIImageReaderTest.java,v 1.0 03.07.14 22:28 haraldk Exp$ */ -public class SGIImageReaderTest extends ImageReaderAbstractTestCase { +public class SGIImageReaderTest extends ImageReaderAbstractTest { @Override protected List getTestData() { - return Arrays.asList( + return Collections.singletonList( new TestData(getClassLoaderResource("/sgi/MARBLES.SGI"), new Dimension(1419, 1001)) // RLE encoded RGB ); } @@ -72,9 +73,7 @@ public class SGIImageReaderTest extends ImageReaderAbstractTestCase getSuffixes() { - return Arrays.asList( - "sgi" - ); + return Collections.singletonList("sgi"); } @Override diff --git a/imageio/imageio-sgi/src/test/java/com/twelvemonkeys/imageio/plugins/sgi/SGIProviderInfoTest.java b/imageio/imageio-sgi/src/test/java/com/twelvemonkeys/imageio/plugins/sgi/SGIProviderInfoTest.java new file mode 100644 index 00000000..7a84ff48 --- /dev/null +++ b/imageio/imageio-sgi/src/test/java/com/twelvemonkeys/imageio/plugins/sgi/SGIProviderInfoTest.java @@ -0,0 +1,19 @@ +package com.twelvemonkeys.imageio.plugins.sgi; + +import com.twelvemonkeys.imageio.spi.ReaderWriterProviderInfo; +import com.twelvemonkeys.imageio.spi.ReaderWriterProviderInfoTest; + +/** + * SGIProviderInfoTest. + * + * @author Harald Kuhr + * @author last modified by $Author: harald.kuhr$ + * @version $Id: SGIProviderInfoTest.java,v 1.0 02/06/16 harald.kuhr Exp$ + */ +public class SGIProviderInfoTest extends ReaderWriterProviderInfoTest { + + @Override + protected ReaderWriterProviderInfo createProviderInfo() { + return new SGIProviderInfo(); + } +} \ No newline at end of file diff --git a/imageio/imageio-tga/pom.xml b/imageio/imageio-tga/pom.xml index fe5f36eb..2683485a 100755 --- a/imageio/imageio-tga/pom.xml +++ b/imageio/imageio-tga/pom.xml @@ -1,12 +1,10 @@ - + 4.0.0 com.twelvemonkeys.imageio imageio - 3.1-SNAPSHOT + 3.3-SNAPSHOT imageio-tga TwelveMonkeys :: ImageIO :: TGA plugin @@ -22,7 +20,7 @@ com.twelvemonkeys.imageio imageio-core - tests + test-jar diff --git a/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGA.java b/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGA.java index e845fcb0..116ec2df 100755 --- a/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGA.java +++ b/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGA.java @@ -29,6 +29,8 @@ package com.twelvemonkeys.imageio.plugins.tga; interface TGA { + byte[] MAGIC = {'T', 'R', 'U', 'E', 'V', 'I', 'S', 'I', 'O', 'N', '-', 'X', 'F', 'I', 'L', 'E', '.', 0}; + /** Fixed header size: 18.*/ int HEADER_SIZE = 18; diff --git a/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAExtensions.java b/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAExtensions.java new file mode 100644 index 00000000..ad806de9 --- /dev/null +++ b/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAExtensions.java @@ -0,0 +1,187 @@ +package com.twelvemonkeys.imageio.plugins.tga; + +import javax.imageio.IIOException; +import javax.imageio.stream.ImageInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Calendar; + +/** + * TGAExtensions. + * + * @author Harald Kuhr + * @author last modified by $Author: harald.kuhr$ + * @version $Id: TGAExtensions.java,v 1.0 27/07/15 harald.kuhr Exp$ + */ +final class TGAExtensions { + public static final int EXT_AREA_SIZE = 495; + + private String authorName; + private String authorComments; + + private Calendar creationDate; + private String jobId; + + private String softwareId; + private String softwareVersion; + + private int backgroundColor; + private double pixelAspectRatio; + private double gamma; + + private long colorCorrectionOffset; + private long postageStampOffset; + private long scanLineOffset; + + private int attributeType; + + private TGAExtensions() { + } + + static TGAExtensions read(final ImageInputStream stream) throws IOException { + int extSize = stream.readUnsignedShort(); + + // Should always be 495 for version 2.0, no newer version exists... + if (extSize < EXT_AREA_SIZE) { + throw new IIOException(String.format("TGA Extension Area size less than %d: %d", EXT_AREA_SIZE, extSize)); + } + + TGAExtensions extensions = new TGAExtensions(); + extensions.authorName = readString(stream, 41);; + extensions.authorComments = readString(stream, 324); + extensions.creationDate = readDate(stream); + extensions.jobId = readString(stream, 41); + + stream.skipBytes(6); // Job time, 3 shorts, hours/minutes/seconds elapsed + + extensions.softwareId = readString(stream, 41); + + // Software version (* 100) short + single byte ASCII (ie. 101 'b' for 1.01b) + int softwareVersion = stream.readUnsignedShort(); + int softwareLetter = stream.readByte(); + + extensions.softwareVersion = softwareVersion != 0 && softwareLetter != ' ' + ? String.format("%d.%d%d", softwareVersion / 100, softwareVersion % 100, softwareLetter).trim() + : null; + + extensions.backgroundColor = stream.readInt(); // ARGB + + extensions.pixelAspectRatio = readRational(stream); + extensions.gamma = readRational(stream); + + extensions.colorCorrectionOffset = stream.readUnsignedInt(); + extensions.postageStampOffset = stream.readUnsignedInt(); + extensions.scanLineOffset = stream.readUnsignedInt(); + + // Offset 494 specifies Attribute type: + // 0: no Alpha data included (bits 3-0 of field 5.6 should also be set to zero) + // 1: undefined data in the Alpha field, can be ignored + // 2: undefined data in the Alpha field, but should be retained + // 3: useful Alpha channel data is present + // 4: pre-multiplied Alpha (see description below) + // 5 -127: RESERVED + // 128-255: Un-assigned + extensions.attributeType = stream.readUnsignedByte(); + + return extensions; + } + + private static double readRational(final ImageInputStream stream) throws IOException { + int numerator = stream.readUnsignedShort(); + int denominator = stream.readUnsignedShort(); + + return denominator != 0 ? numerator / (double) denominator : 1; + } + + private static Calendar readDate(final ImageInputStream stream) throws IOException { + Calendar calendar = Calendar.getInstance(); + calendar.clear(); + + int month = stream.readUnsignedShort(); + int date = stream.readUnsignedShort(); + int year = stream.readUnsignedShort(); + + int hourOfDay = stream.readUnsignedShort(); + int minute = stream.readUnsignedShort(); + int second = stream.readUnsignedShort(); + + // Unused + if (month == 0 && year == 0 && date == 0 && hourOfDay == 0 && minute == 0 && second == 0) { + return null; + } + + calendar.set(year, month - 1, date, hourOfDay, minute, second); + + return calendar; + } + + private static String readString(final ImageInputStream stream, final int maxLength) throws IOException { + byte[] data = new byte[maxLength]; + stream.readFully(data); + + return asZeroTerminatedASCIIString(data); + } + + private static String asZeroTerminatedASCIIString(final byte[] data) { + int len = data.length; + + for (int i = 0; i < data.length; i++) { + if (data[i] == 0) { + len = i; + } + } + + return new String(data, 0, len, StandardCharsets.US_ASCII); + } + + public boolean hasAlpha() { + switch (attributeType) { + case 3: + case 4: + return true; + default: + return false; + } + } + + public boolean isAlphaPremultiplied() { + switch (attributeType) { + case 4: + return true; + default: + return false; + } + } + + public long getThumbnailOffset() { + return postageStampOffset; + } + + public String getAuthorName() { + return authorName; + } + + public String getAuthorComments() { + return authorComments; + } + + public Calendar getCreationDate() { + return creationDate; + } + + public String getSoftware() { + return softwareId; + } + + public String getSoftwareVersion() { + return softwareVersion; + } + + public double getPixelAspectRatio() { + return pixelAspectRatio; + } + + public int getBackgroundColor() { + return backgroundColor; + } +} diff --git a/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAImageReader.java b/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAImageReader.java index 5465ed03..bceccb8b 100755 --- a/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAImageReader.java +++ b/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAImageReader.java @@ -33,6 +33,7 @@ import com.twelvemonkeys.imageio.util.IIOUtil; import com.twelvemonkeys.imageio.util.ImageTypeSpecifiers; import com.twelvemonkeys.io.LittleEndianDataInputStream; import com.twelvemonkeys.io.enc.DecoderStream; +import com.twelvemonkeys.lang.Validate; import com.twelvemonkeys.xml.XMLSerializer; import javax.imageio.IIOException; @@ -51,6 +52,7 @@ import java.io.File; import java.io.IOException; import java.nio.ByteOrder; import java.util.ArrayList; +import java.util.Arrays; import java.util.Iterator; import java.util.List; @@ -59,6 +61,7 @@ public final class TGAImageReader extends ImageReaderBase { // http://www.gamers.org/dEngine/quake3/TGA.txt private TGAHeader header; + private TGAExtensions extensions; protected TGAImageReader(final ImageReaderSpi provider) { super(provider); @@ -67,6 +70,7 @@ public final class TGAImageReader extends ImageReaderBase { @Override protected void resetMembers() { header = null; + extensions = null; } @Override @@ -89,7 +93,7 @@ public final class TGAImageReader extends ImageReaderBase { public Iterator getImageTypes(final int imageIndex) throws IOException { ImageTypeSpecifier rawType = getRawImageType(imageIndex); - List specifiers = new ArrayList(); + List specifiers = new ArrayList<>(); // TODO: Implement specifiers.add(rawType); @@ -110,19 +114,29 @@ public final class TGAImageReader extends ImageReaderBase { return ImageTypeSpecifiers.createFromIndexColorModel(header.getColorMap()); case TGA.IMAGETYPE_MONOCHROME: case TGA.IMAGETYPE_MONOCHROME_RLE: - return ImageTypeSpecifiers.createGrayscale(1, DataBuffer.TYPE_BYTE); + return ImageTypeSpecifiers.createGrayscale(8, DataBuffer.TYPE_BYTE); case TGA.IMAGETYPE_TRUECOLOR: case TGA.IMAGETYPE_TRUECOLOR_RLE: ColorSpace sRGB = ColorSpace.getInstance(ColorSpace.CS_sRGB); + boolean hasAlpha = header.getAttributeBits() > 0 && extensions != null && extensions.hasAlpha(); + boolean isAlphaPremultiplied = extensions != null && extensions.isAlphaPremultiplied(); + switch (header.getPixelDepth()) { case 16: + if (hasAlpha) { + // USHORT_1555_ARGB... + return ImageTypeSpecifiers.createPacked(sRGB, 0x7C00, 0x03E0, 0x001F, 0x8000, DataBuffer.TYPE_USHORT, isAlphaPremultiplied); + } + // Default mask out alpha return ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_USHORT_555_RGB); case 24: return ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_3BYTE_BGR); case 32: - // 4BYTE_BGRA... - return ImageTypeSpecifiers.createInterleaved(sRGB, new int[] {2, 1, 0, 3}, DataBuffer.TYPE_BYTE, true, false); + // 4BYTE_BGRX... + // Can't mask out alpha (efficiently) for 4BYTE, so we'll ignore it while reading instead, + // if hasAlpha is false + return ImageTypeSpecifiers.createInterleaved(sRGB, new int[] {2, 1, 0, 3}, DataBuffer.TYPE_BYTE, true, isAlphaPremultiplied); default: throw new IIOException("Unknown pixel depth for truecolor: " + header.getPixelDepth()); } @@ -166,31 +180,32 @@ public final class TGAImageReader extends ImageReaderBase { DataInput input; if (imageType == TGA.IMAGETYPE_COLORMAPPED_RLE || imageType == TGA.IMAGETYPE_TRUECOLOR_RLE || imageType == TGA.IMAGETYPE_MONOCHROME_RLE) { input = new LittleEndianDataInputStream(new DecoderStream(IIOUtil.createStreamAdapter(imageInput), new RLEDecoder(header.getPixelDepth()))); - } else { + } + else { input = imageInput; } for (int y = 0; y < height; y++) { switch (header.getPixelDepth()) { - case 8: - case 24: - case 32: - byte[] rowDataByte = ((DataBufferByte) rowRaster.getDataBuffer()).getData(); - readRowByte(input, height, srcRegion, header.getOrigin(), xSub, ySub, rowDataByte, destRaster, clippedRow, y); - break; - case 16: - short[] rowDataUShort = ((DataBufferUShort) rowRaster.getDataBuffer()).getData(); - readRowUShort(input, height, srcRegion, header.getOrigin(), xSub, ySub, rowDataUShort, destRaster, clippedRow, y); - break; - default: - throw new AssertionError("Unsupported pixel depth: " + header.getPixelDepth()); - } - - processImageProgress(100f * y / height); - - if (height - 1 - y < srcRegion.y) { + case 8: + case 24: + case 32: + byte[] rowDataByte = ((DataBufferByte) rowRaster.getDataBuffer()).getData(); + readRowByte(input, height, srcRegion, header.getOrigin(), xSub, ySub, rowDataByte, destRaster, clippedRow, y); break; - } + case 16: + short[] rowDataUShort = ((DataBufferUShort) rowRaster.getDataBuffer()).getData(); + readRowUShort(input, height, srcRegion, header.getOrigin(), xSub, ySub, rowDataUShort, destRaster, clippedRow, y); + break; + default: + throw new AssertionError("Unsupported pixel depth: " + header.getPixelDepth()); + } + + processImageProgress(100f * y / height); + + if (height - 1 - y < srcRegion.y) { + break; + } if (abortRequested()) { processReadAborted(); @@ -212,11 +227,11 @@ public final class TGAImageReader extends ImageReaderBase { return; } - input.readFully(rowDataByte, 0, rowDataByte.length); - if (srcChannel.getNumBands() == 4) { - invertAlpha(rowDataByte); + if (srcChannel.getNumBands() == 4 && (header.getAttributeBits() == 0 || extensions != null && !extensions.hasAlpha())) { + // Remove the alpha channel (make pixels opaque) if there are no "attribute bits" (alpha bits) + removeAlpha32(rowDataByte); } // Subsample horizontal @@ -240,9 +255,9 @@ public final class TGAImageReader extends ImageReaderBase { } } - private void invertAlpha(final byte[] rowDataByte) { - for (int i = 3; i < rowDataByte.length; i += 4) { - rowDataByte[i] = (byte) (0xFF - rowDataByte[i]); + private void removeAlpha32(final byte[] rowData) { + for (int i = 3; i < rowData.length; i += 4) { + rowData[i] = (byte) 0xFF; } } @@ -313,21 +328,154 @@ public final class TGAImageReader extends ImageReaderBase { private void readHeader() throws IOException { if (header == null) { imageInput.setByteOrder(ByteOrder.LITTLE_ENDIAN); + + // Read header header = TGAHeader.read(imageInput); // System.err.println("header: " + header); imageInput.flushBefore(imageInput.getStreamPosition()); + + // Read footer, if 2.0 format (ends with TRUEVISION-XFILE\0) + skipToEnd(imageInput); + imageInput.seek(imageInput.getStreamPosition() - 26); + + long extOffset = imageInput.readInt(); + /*long devOffset = */imageInput.readInt(); // Ignored for now + + byte[] magic = new byte[18]; + imageInput.readFully(magic); + + if (Arrays.equals(magic, TGA.MAGIC)) { + if (extOffset > 0) { + imageInput.seek(extOffset); + extensions = TGAExtensions.read(imageInput); + } + } } imageInput.seek(imageInput.getFlushedPosition()); } - @Override public IIOMetadata getImageMetadata(final int imageIndex) throws IOException { + // TODO: Candidate util method + private static void skipToEnd(final ImageInputStream stream) throws IOException { + if (stream.length() > 0) { + // Seek to end of file + stream.seek(stream.length()); + } + else { + // Skip to end + long lastGood = stream.getStreamPosition(); + + while (stream.read() != -1) { + lastGood = stream.getStreamPosition(); + stream.skipBytes(1024); + } + + stream.seek(lastGood); + + while (true) { + if (stream.read() == -1) { + break; + } + // Just continue reading to EOF... + } + } + } + + // Thumbnail support + + @Override + public boolean readerSupportsThumbnails() { + return true; + } + + @Override + public boolean hasThumbnails(final int imageIndex) throws IOException { checkBounds(imageIndex); readHeader(); - return new TGAMetadata(header); + return extensions != null && extensions.getThumbnailOffset() > 0; + } + + @Override + public int getNumThumbnails(final int imageIndex) throws IOException { + return hasThumbnails(imageIndex) ? 1 : 0; + } + + @Override + public int getThumbnailWidth(final int imageIndex, final int thumbnailIndex) throws IOException { + checkBounds(imageIndex); + Validate.isTrue(thumbnailIndex >= 0 && thumbnailIndex < getNumThumbnails(imageIndex), "thumbnailIndex >= numThumbnails"); + + imageInput.seek(extensions.getThumbnailOffset()); + + return imageInput.readUnsignedByte(); + } + + @Override + public int getThumbnailHeight(final int imageIndex, final int thumbnailIndex) throws IOException { + getThumbnailWidth(imageIndex, thumbnailIndex); // Laziness... + + return imageInput.readUnsignedByte(); + } + + @Override + public BufferedImage readThumbnail(final int imageIndex, final int thumbnailIndex) throws IOException { + Iterator imageTypes = getImageTypes(imageIndex); + ImageTypeSpecifier rawType = getRawImageType(imageIndex); + + int width = getThumbnailWidth(imageIndex, thumbnailIndex); + int height = getThumbnailHeight(imageIndex, thumbnailIndex); + + // For thumbnail, always read entire image + Rectangle srcRegion = new Rectangle(width, height); + + BufferedImage destination = getDestination(null, imageTypes, width, height); + WritableRaster destRaster = destination.getRaster(); + WritableRaster rowRaster = rawType.createBufferedImage(width, 1).getRaster(); + + processThumbnailStarted(imageIndex, thumbnailIndex); + + // Thumbnail is always stored non-compressed, no need for RLE support + imageInput.seek(extensions.getThumbnailOffset() + 2); + + for (int y = 0; y < height; y++) { + switch (header.getPixelDepth()) { + case 8: + case 24: + case 32: + byte[] rowDataByte = ((DataBufferByte) rowRaster.getDataBuffer()).getData(); + readRowByte(imageInput, height, srcRegion, header.getOrigin(), 1, 1, rowDataByte, destRaster, rowRaster, y); + break; + case 16: + short[] rowDataUShort = ((DataBufferUShort) rowRaster.getDataBuffer()).getData(); + readRowUShort(imageInput, height, srcRegion, header.getOrigin(), 1, 1, rowDataUShort, destRaster, rowRaster, y); + break; + default: + throw new AssertionError("Unsupported pixel depth: " + header.getPixelDepth()); + } + + processThumbnailProgress(100f * y / height); + + if (height - 1 - y < srcRegion.y) { + break; + } + } + + processThumbnailComplete(); + + return destination; + } + + // Metadata support + + @Override + public IIOMetadata getImageMetadata(final int imageIndex) throws IOException { + checkBounds(imageIndex); + readHeader(); + + return new TGAMetadata(header, extensions); } public static void main(String[] args) throws IOException { diff --git a/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAImageReaderSpi.java b/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAImageReaderSpi.java index 2948915a..5a741ba3 100755 --- a/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAImageReaderSpi.java +++ b/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAImageReaderSpi.java @@ -45,7 +45,8 @@ public final class TGAImageReaderSpi extends ImageReaderSpiBase { super(new TGAProviderInfo()); } - @Override public boolean canDecodeInput(final Object source) throws IOException { + @Override + public boolean canDecodeInput(final Object source) throws IOException { if (!(source instanceof ImageInputStream)) { return false; } @@ -58,7 +59,7 @@ public final class TGAImageReaderSpi extends ImageReaderSpiBase { try { stream.setByteOrder(ByteOrder.LITTLE_ENDIAN); - // NOTE: The TGA format does not have a magic identifier, so this is guesswork... + // NOTE: The original TGA format does not have a magic identifier, so this is guesswork... // We'll try to match sane values, and hope no other files contains the same sequence. stream.readUnsignedByte(); @@ -88,11 +89,11 @@ public final class TGAImageReaderSpi extends ImageReaderSpiBase { int colorMapStart = stream.readUnsignedShort(); int colorMapSize = stream.readUnsignedShort(); - int colorMapDetph = stream.readUnsignedByte(); + int colorMapDepth = stream.readUnsignedByte(); if (colorMapSize == 0) { // No color map, all 3 fields should be 0 - if (colorMapStart!= 0 || colorMapDetph != 0) { + if (colorMapStart != 0 || colorMapDepth != 0) { return false; } } @@ -106,7 +107,7 @@ public final class TGAImageReaderSpi extends ImageReaderSpiBase { if (colorMapStart >= colorMapSize) { return false; } - if (colorMapDetph != 15 && colorMapDetph != 16 && colorMapDetph != 24 && colorMapDetph != 32) { + if (colorMapDepth != 15 && colorMapDepth != 16 && colorMapDepth != 24 && colorMapDepth != 32) { return false; } } @@ -134,6 +135,7 @@ public final class TGAImageReaderSpi extends ImageReaderSpiBase { // We're pretty sure by now, but there can still be false positives... // For 2.0 format, we could skip to end, and read "TRUEVISION-XFILE.\0" but it would be too slow + // unless we are working with a local file (and the file may still be a valid original TGA without it). return true; } finally { diff --git a/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAMetadata.java b/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAMetadata.java index d78cf8f3..bcca2bb6 100755 --- a/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAMetadata.java +++ b/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAMetadata.java @@ -28,53 +28,29 @@ package com.twelvemonkeys.imageio.plugins.tga; -import org.w3c.dom.Node; +import com.twelvemonkeys.imageio.AbstractMetadata; -import javax.imageio.metadata.IIOMetadata; -import javax.imageio.metadata.IIOMetadataFormatImpl; import javax.imageio.metadata.IIOMetadataNode; +import java.awt.*; import java.awt.image.IndexColorModel; +import java.util.Calendar; -final class TGAMetadata extends IIOMetadata { - // TODO: Clean up & extend AbstractMetadata (after moving from PSD -> Core) - +final class TGAMetadata extends AbstractMetadata { private final TGAHeader header; + private final TGAExtensions extensions; - TGAMetadata(final TGAHeader header) { + TGAMetadata(final TGAHeader header, final TGAExtensions extensions) { this.header = header; - standardFormatSupported = true; + this.extensions = extensions; } - @Override public boolean isReadOnly() { - return true; - } - - @Override public Node getAsTree(final String formatName) { - if (IIOMetadataFormatImpl.standardMetadataFormatName.equals(formatName)) { - return getStandardTree(); - } - else { - throw new IllegalArgumentException("Unsupported metadata format: " + formatName); - } - } - - @Override public void mergeTree(final String formatName, final Node root) { - if (isReadOnly()) { - throw new IllegalStateException("Metadata is read-only"); - } - } - - @Override public void reset() { - if (isReadOnly()) { - throw new IllegalStateException("Metadata is read-only"); - } - } - - - @Override protected IIOMetadataNode getStandardChromaNode() { + @Override + protected IIOMetadataNode getStandardChromaNode() { IIOMetadataNode chroma = new IIOMetadataNode("Chroma"); IIOMetadataNode csType = new IIOMetadataNode("ColorSpaceType"); + chroma.appendChild(csType); + switch (header.getImageType()) { case TGA.IMAGETYPE_MONOCHROME: case TGA.IMAGETYPE_MONOCHROME_RLE: @@ -92,15 +68,22 @@ final class TGAMetadata extends IIOMetadata { default: csType.setAttribute("name", "Unknown"); } - chroma.appendChild(csType); - // TODO: Channels in chroma node reflects channels in color model (see data node, for channels in data) + // NOTE: Channels in chroma node reflects channels in color model (see data node, for channels in data) IIOMetadataNode numChannels = new IIOMetadataNode("NumChannels"); + chroma.appendChild(numChannels); switch (header.getPixelDepth()) { case 8: - case 16: numChannels.setAttribute("value", Integer.toString(1)); break; + case 16: + if (header.getAttributeBits() > 0 && extensions != null && extensions.hasAlpha()) { + numChannels.setAttribute("value", Integer.toString(4)); + } + else { + numChannels.setAttribute("value", Integer.toString(3)); + } + break; case 24: numChannels.setAttribute("value", Integer.toString(3)); break; @@ -108,11 +91,10 @@ final class TGAMetadata extends IIOMetadata { numChannels.setAttribute("value", Integer.toString(4)); break; } - chroma.appendChild(numChannels); IIOMetadataNode blackIsZero = new IIOMetadataNode("BlackIsZero"); - blackIsZero.setAttribute("value", "TRUE"); chroma.appendChild(blackIsZero); + blackIsZero.setAttribute("value", "TRUE"); // NOTE: TGA files may contain a color map, even if true color... // Not sure if this is a good idea to expose to the meta data, @@ -124,20 +106,31 @@ final class TGAMetadata extends IIOMetadata { for (int i = 0; i < colorMap.getMapSize(); i++) { IIOMetadataNode paletteEntry = new IIOMetadataNode("PaletteEntry"); + palette.appendChild(paletteEntry); paletteEntry.setAttribute("index", Integer.toString(i)); paletteEntry.setAttribute("red", Integer.toString(colorMap.getRed(i))); paletteEntry.setAttribute("green", Integer.toString(colorMap.getGreen(i))); paletteEntry.setAttribute("blue", Integer.toString(colorMap.getBlue(i))); - - palette.appendChild(paletteEntry); } } + if (extensions != null && extensions.getBackgroundColor() != 0) { + Color background = new Color(extensions.getBackgroundColor(), true); + + IIOMetadataNode backgroundColor = new IIOMetadataNode("BackgroundColor"); + chroma.appendChild(backgroundColor); + + backgroundColor.setAttribute("red", Integer.toString(background.getRed())); + backgroundColor.setAttribute("green", Integer.toString(background.getGreen())); + backgroundColor.setAttribute("blue", Integer.toString(background.getBlue())); + } + return chroma; } - @Override protected IIOMetadataNode getStandardCompressionNode() { + @Override + protected IIOMetadataNode getStandardCompressionNode() { switch (header.getImageType()) { case TGA.IMAGETYPE_COLORMAPPED_RLE: case TGA.IMAGETYPE_TRUECOLOR_RLE: @@ -145,15 +138,16 @@ final class TGAMetadata extends IIOMetadata { case TGA.IMAGETYPE_COLORMAPPED_HUFFMAN: case TGA.IMAGETYPE_COLORMAPPED_HUFFMAN_QUADTREE: IIOMetadataNode node = new IIOMetadataNode("Compression"); + IIOMetadataNode compressionTypeName = new IIOMetadataNode("CompressionTypeName"); - String value = header.getImageType() == TGA.IMAGETYPE_COLORMAPPED_HUFFMAN || header.getImageType() == TGA.IMAGETYPE_COLORMAPPED_HUFFMAN_QUADTREE - ? "Uknown" : "RLE"; - compressionTypeName.setAttribute("value", value); node.appendChild(compressionTypeName); + String value = header.getImageType() == TGA.IMAGETYPE_COLORMAPPED_HUFFMAN || header.getImageType() == TGA.IMAGETYPE_COLORMAPPED_HUFFMAN_QUADTREE + ? "Uknown" : "RLE"; + compressionTypeName.setAttribute("value", value); IIOMetadataNode lossless = new IIOMetadataNode("Lossless"); - lossless.setAttribute("value", "TRUE"); node.appendChild(lossless); + lossless.setAttribute("value", "TRUE"); return node; default: @@ -162,14 +156,17 @@ final class TGAMetadata extends IIOMetadata { } } - @Override protected IIOMetadataNode getStandardDataNode() { + @Override + protected IIOMetadataNode getStandardDataNode() { IIOMetadataNode node = new IIOMetadataNode("Data"); IIOMetadataNode planarConfiguration = new IIOMetadataNode("PlanarConfiguration"); - planarConfiguration.setAttribute("value", "PixelInterleaved"); node.appendChild(planarConfiguration); + planarConfiguration.setAttribute("value", "PixelInterleaved"); IIOMetadataNode sampleFormat = new IIOMetadataNode("SampleFormat"); + node.appendChild(sampleFormat); + switch (header.getImageType()) { case TGA.IMAGETYPE_COLORMAPPED: case TGA.IMAGETYPE_COLORMAPPED_RLE: @@ -182,13 +179,19 @@ final class TGAMetadata extends IIOMetadata { break; } - node.appendChild(sampleFormat); - IIOMetadataNode bitsPerSample = new IIOMetadataNode("BitsPerSample"); + node.appendChild(bitsPerSample); + switch (header.getPixelDepth()) { case 8: - case 16: bitsPerSample.setAttribute("value", createListValue(1, Integer.toString(header.getPixelDepth()))); + case 16: + if (header.getAttributeBits() > 0 && extensions != null && extensions.hasAlpha()) { + bitsPerSample.setAttribute("value", "5, 5, 5, 1"); + } + else { + bitsPerSample.setAttribute("value", createListValue(3, "5")); + } break; case 24: bitsPerSample.setAttribute("value", createListValue(3, Integer.toString(8))); @@ -198,12 +201,6 @@ final class TGAMetadata extends IIOMetadata { break; } - node.appendChild(bitsPerSample); - - // TODO: Do we need MSB? -// IIOMetadataNode sampleMSB = new IIOMetadataNode("SampleMSB"); -// sampleMSB.setAttribute("value", createListValue(header.getChannels(), "0")); - return node; } @@ -221,10 +218,12 @@ final class TGAMetadata extends IIOMetadata { return buffer.toString(); } - @Override protected IIOMetadataNode getStandardDimensionNode() { + @Override + protected IIOMetadataNode getStandardDimensionNode() { IIOMetadataNode dimension = new IIOMetadataNode("Dimension"); IIOMetadataNode imageOrientation = new IIOMetadataNode("ImageOrientation"); + dimension.appendChild(imageOrientation); switch (header.getOrigin()) { case TGA.ORIGIN_LOWER_LEFT: @@ -241,38 +240,90 @@ final class TGAMetadata extends IIOMetadata { break; } - dimension.appendChild(imageOrientation); + IIOMetadataNode pixelAspectRatio = new IIOMetadataNode("PixelAspectRatio"); + dimension.appendChild(pixelAspectRatio); + pixelAspectRatio.setAttribute("value", extensions != null ? String.valueOf(extensions.getPixelAspectRatio()) : "1.0"); return dimension; } - // No document node + @Override + protected IIOMetadataNode getStandardDocumentNode() { + IIOMetadataNode document = new IIOMetadataNode("Document"); - @Override protected IIOMetadataNode getStandardTextNode() { - // TODO: Extra "developer area" and other stuff might go here... - if (header.getIdentification() != null && !header.getIdentification().isEmpty()) { - IIOMetadataNode text = new IIOMetadataNode("Text"); + IIOMetadataNode formatVersion = new IIOMetadataNode("FormatVersion"); + document.appendChild(formatVersion); + formatVersion.setAttribute("value", extensions == null ? "1.0" : "2.0"); - IIOMetadataNode textEntry = new IIOMetadataNode("TextEntry"); - textEntry.setAttribute("keyword", "identification"); - textEntry.setAttribute("value", header.getIdentification()); - text.appendChild(textEntry); + // ImageCreationTime from extensions date + if (extensions != null && extensions.getCreationDate() != null) { + IIOMetadataNode imageCreationTime = new IIOMetadataNode("ImageCreationTime"); + document.appendChild(imageCreationTime); - return text; + Calendar date = extensions.getCreationDate(); + + imageCreationTime.setAttribute("year", String.valueOf(date.get(Calendar.YEAR))); + imageCreationTime.setAttribute("month", String.valueOf(date.get(Calendar.MONTH) + 1)); + imageCreationTime.setAttribute("day", String.valueOf(date.get(Calendar.DAY_OF_MONTH))); + imageCreationTime.setAttribute("hour", String.valueOf(date.get(Calendar.HOUR_OF_DAY))); + imageCreationTime.setAttribute("minute", String.valueOf(date.get(Calendar.MINUTE))); + imageCreationTime.setAttribute("second", String.valueOf(date.get(Calendar.SECOND))); } - return null; + return document; + } + + @Override + protected IIOMetadataNode getStandardTextNode() { + IIOMetadataNode text = new IIOMetadataNode("Text"); + + // NOTE: Names corresponds to equivalent fields in TIFF + if (header.getIdentification() != null && !header.getIdentification().isEmpty()) { + appendTextEntry(text, "DocumentName", header.getIdentification()); + } + + if (extensions != null) { + appendTextEntry(text, "Software", extensions.getSoftwareVersion() == null ? extensions.getSoftware() : extensions.getSoftware() + " " + extensions.getSoftwareVersion()); + appendTextEntry(text, "Artist", extensions.getAuthorName()); + appendTextEntry(text, "UserComment", extensions.getAuthorComments()); + } + + return text.hasChildNodes() ? text : null; + } + + private void appendTextEntry(final IIOMetadataNode parent, final String keyword, final String value) { + if (value != null) { + IIOMetadataNode textEntry = new IIOMetadataNode("TextEntry"); + parent.appendChild(textEntry); + textEntry.setAttribute("keyword", keyword); + textEntry.setAttribute("value", value); + } } // No tiling - @Override protected IIOMetadataNode getStandardTransparencyNode() { + @Override + protected IIOMetadataNode getStandardTransparencyNode() { IIOMetadataNode transparency = new IIOMetadataNode("Transparency"); IIOMetadataNode alpha = new IIOMetadataNode("Alpha"); - alpha.setAttribute("value", header.getPixelDepth() == 32 ? "nonpremultiplied" : "none"); transparency.appendChild(alpha); + if (extensions != null) { + if (extensions.hasAlpha()) { + alpha.setAttribute("value", extensions.isAlphaPremultiplied() ? "premultiplied" : "nonpremultiplied"); + } + else { + alpha.setAttribute("value", "none"); + } + } + else if (header.getAttributeBits() == 8) { + alpha.setAttribute("value", "nonpremultiplied"); + } + else { + alpha.setAttribute("value", "none"); + } + return transparency; } } diff --git a/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAProviderInfo.java b/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAProviderInfo.java index c48d47e2..1009093b 100644 --- a/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAProviderInfo.java +++ b/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAProviderInfo.java @@ -1,3 +1,31 @@ +/* + * Copyright (c) 2015, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + package com.twelvemonkeys.imageio.plugins.tga; import com.twelvemonkeys.imageio.spi.ReaderWriterProviderInfo; @@ -23,7 +51,7 @@ final class TGAProviderInfo extends ReaderWriterProviderInfo { "image/tga", "image/x-tga", "image/targa", "image/x-targa", }, - "com.twelvemkonkeys.imageio.plugins.tga.TGAImageReader", + "com.twelvemonkeys.imageio.plugins.tga.TGAImageReader", new String[] {"com.twelvemonkeys.imageio.plugins.tga.TGAImageReaderSpi"}, null, null, diff --git a/imageio/imageio-tga/src/test/java/com/twelvemonkeys/imageio/plugins/tga/TGAImageReaderTest.java b/imageio/imageio-tga/src/test/java/com/twelvemonkeys/imageio/plugins/tga/TGAImageReaderTest.java index 59a85d49..601df0a9 100755 --- a/imageio/imageio-tga/src/test/java/com/twelvemonkeys/imageio/plugins/tga/TGAImageReaderTest.java +++ b/imageio/imageio-tga/src/test/java/com/twelvemonkeys/imageio/plugins/tga/TGAImageReaderTest.java @@ -28,7 +28,7 @@ package com.twelvemonkeys.imageio.plugins.tga; -import com.twelvemonkeys.imageio.util.ImageReaderAbstractTestCase; +import com.twelvemonkeys.imageio.util.ImageReaderAbstractTest; import javax.imageio.spi.ImageReaderSpi; import java.awt.*; @@ -42,7 +42,7 @@ import java.util.List; * @author last modified by $Author: haraldk$ * @version $Id: TGAImageReaderTest.java,v 1.0 03.07.14 22:28 haraldk Exp$ */ -public class TGAImageReaderTest extends ImageReaderAbstractTestCase { +public class TGAImageReaderTest extends ImageReaderAbstractTest { @Override protected List getTestData() { return Arrays.asList( diff --git a/imageio/imageio-tga/src/test/java/com/twelvemonkeys/imageio/plugins/tga/TGAProviderInfoTest.java b/imageio/imageio-tga/src/test/java/com/twelvemonkeys/imageio/plugins/tga/TGAProviderInfoTest.java new file mode 100644 index 00000000..99a85127 --- /dev/null +++ b/imageio/imageio-tga/src/test/java/com/twelvemonkeys/imageio/plugins/tga/TGAProviderInfoTest.java @@ -0,0 +1,19 @@ +package com.twelvemonkeys.imageio.plugins.tga; + +import com.twelvemonkeys.imageio.spi.ReaderWriterProviderInfo; +import com.twelvemonkeys.imageio.spi.ReaderWriterProviderInfoTest; + +/** + * TGAProviderInfoTest. + * + * @author Harald Kuhr + * @author last modified by $Author: harald.kuhr$ + * @version $Id: TGAProviderInfoTest.java,v 1.0 02/06/16 harald.kuhr Exp$ + */ +public class TGAProviderInfoTest extends ReaderWriterProviderInfoTest { + + @Override + protected ReaderWriterProviderInfo createProviderInfo() { + return new TGAProviderInfo(); + } +} \ No newline at end of file diff --git a/imageio/imageio-thumbsdb/pom.xml b/imageio/imageio-thumbsdb/pom.xml index 1e674aaf..7ade94f8 100644 --- a/imageio/imageio-thumbsdb/pom.xml +++ b/imageio/imageio-thumbsdb/pom.xml @@ -4,7 +4,7 @@ com.twelvemonkeys.imageio imageio - 3.1-SNAPSHOT + 3.3-SNAPSHOT imageio-thumbsdb TwelveMonkeys :: ImageIO :: Thumbs.db plugin @@ -20,7 +20,7 @@ com.twelvemonkeys.imageio imageio-core - tests + test-jar diff --git a/imageio/imageio-thumbsdb/src/main/java/com/twelvemonkeys/imageio/plugins/thumbsdb/ThumbsDBImageReader.java b/imageio/imageio-thumbsdb/src/main/java/com/twelvemonkeys/imageio/plugins/thumbsdb/ThumbsDBImageReader.java index 13152c22..6d31b19f 100644 --- a/imageio/imageio-thumbsdb/src/main/java/com/twelvemonkeys/imageio/plugins/thumbsdb/ThumbsDBImageReader.java +++ b/imageio/imageio-thumbsdb/src/main/java/com/twelvemonkeys/imageio/plugins/thumbsdb/ThumbsDBImageReader.java @@ -62,7 +62,7 @@ import java.util.SortedSet; * @see com.twelvemonkeys.io.ole2.CompoundDocument * @see Harald Kuhr * @version $Id: ThumbsDBImageReaderSpi.java,v 1.0 28.feb.2006 19:21:05 haku Exp$ */ -public class ThumbsDBImageReaderSpi extends ImageReaderSpiBase { +public final class ThumbsDBImageReaderSpi extends ImageReaderSpiBase { private ImageReaderSpi jpegProvider; /** @@ -61,7 +61,7 @@ public class ThumbsDBImageReaderSpi extends ImageReaderSpiBase { return source instanceof ImageInputStream && canDecode((ImageInputStream) source); } - public boolean canDecode(final ImageInputStream pInput) throws IOException { + boolean canDecode(final ImageInputStream pInput) throws IOException { maybeInitJPEGProvider(); // If this is a OLE 2 CompoundDocument, we could try... // TODO: How do we know it's thumbs.db format (structure), without reading quite a lot? diff --git a/imageio/imageio-thumbsdb/src/main/java/com/twelvemonkeys/imageio/plugins/thumbsdb/ThumbsDBProviderInfo.java b/imageio/imageio-thumbsdb/src/main/java/com/twelvemonkeys/imageio/plugins/thumbsdb/ThumbsDBProviderInfo.java index 861dab33..a7585f00 100644 --- a/imageio/imageio-thumbsdb/src/main/java/com/twelvemonkeys/imageio/plugins/thumbsdb/ThumbsDBProviderInfo.java +++ b/imageio/imageio-thumbsdb/src/main/java/com/twelvemonkeys/imageio/plugins/thumbsdb/ThumbsDBProviderInfo.java @@ -1,3 +1,31 @@ +/* + * Copyright (c) 2015, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + package com.twelvemonkeys.imageio.plugins.thumbsdb; import com.twelvemonkeys.imageio.spi.ReaderWriterProviderInfo; @@ -13,9 +41,9 @@ final class ThumbsDBProviderInfo extends ReaderWriterProviderInfo { protected ThumbsDBProviderInfo() { super( ThumbsDBProviderInfo.class, - new String[]{"thumbs", "THUMBS", "Thumbs DB"}, - new String[]{"db"}, - new String[]{"image/x-thumbs-db", "application/octet-stream"}, // TODO: Check IANA et al... + new String[] {"thumbs", "THUMBS", "Thumbs DB"}, + new String[] {"db"}, + new String[] {"image/x-thumbs-db", "application/octet-stream"}, // TODO: Check IANA et al... "com.twelvemonkeys.imageio.plugins.thumbsdb.ThumbsDBImageReader", new String[] {"com.twelvemonkeys.imageio.plugins.thumbsdb.ThumbsDBImageReaderSpi"}, null, diff --git a/imageio/imageio-thumbsdb/src/test/java/com/twelvemonkeys/imageio/plugins/thumbsdb/ThumbsDBImageReaderTestCase.java b/imageio/imageio-thumbsdb/src/test/java/com/twelvemonkeys/imageio/plugins/thumbsdb/ThumbsDBImageReaderTest.java similarity index 90% rename from imageio/imageio-thumbsdb/src/test/java/com/twelvemonkeys/imageio/plugins/thumbsdb/ThumbsDBImageReaderTestCase.java rename to imageio/imageio-thumbsdb/src/test/java/com/twelvemonkeys/imageio/plugins/thumbsdb/ThumbsDBImageReaderTest.java index ee1e7440..8b6ad255 100644 --- a/imageio/imageio-thumbsdb/src/test/java/com/twelvemonkeys/imageio/plugins/thumbsdb/ThumbsDBImageReaderTestCase.java +++ b/imageio/imageio-thumbsdb/src/test/java/com/twelvemonkeys/imageio/plugins/thumbsdb/ThumbsDBImageReaderTest.java @@ -29,7 +29,7 @@ package com.twelvemonkeys.imageio.plugins.thumbsdb; import com.twelvemonkeys.imageio.stream.BufferedImageInputStream; -import com.twelvemonkeys.imageio.util.ImageReaderAbstractTestCase; +import com.twelvemonkeys.imageio.util.ImageReaderAbstractTest; import com.twelvemonkeys.io.ole2.CompoundDocument; import com.twelvemonkeys.io.ole2.Entry; import com.twelvemonkeys.lang.SystemUtil; @@ -43,16 +43,19 @@ import java.awt.*; import java.io.IOException; import java.nio.ByteOrder; import java.util.Arrays; +import java.util.Collections; import java.util.List; +import static org.junit.Assert.assertNotNull; + /** - * ICOImageReaderTestCase + * ICOImageReaderTest * * @author Harald Kuhr * @author last modified by $Author: haraldk$ - * @version $Id: ICOImageReaderTestCase.java,v 1.0 Apr 1, 2008 10:39:17 PM haraldk Exp$ + * @version $Id: ICOImageReaderTest.java,v 1.0 Apr 1, 2008 10:39:17 PM haraldk Exp$ */ -public class ThumbsDBImageReaderTestCase extends ImageReaderAbstractTestCase { +public class ThumbsDBImageReaderTest extends ImageReaderAbstractTest { private static final boolean IS_JAVA_6 = SystemUtil.isClassAvailable("java.util.Deque"); private ThumbsDBImageReaderSpi provider = new ThumbsDBImageReaderSpi(); @@ -97,15 +100,15 @@ public class ThumbsDBImageReaderTestCase extends ImageReaderAbstractTestCase getFormatNames() { - return Arrays.asList("thumbs"); + return Collections.singletonList("thumbs"); } protected List getSuffixes() { - return Arrays.asList("db"); + return Collections.singletonList("db"); } protected List getMIMETypes() { - return Arrays.asList("image/x-thumbs-db"); + return Collections.singletonList("image/x-thumbs-db"); } @Test @@ -115,7 +118,9 @@ public class ThumbsDBImageReaderTestCase extends ImageReaderAbstractTestCaseHarald Kuhr + * @author last modified by $Author: harald.kuhr$ + * @version $Id: ThumbsDBProviderInfoTest.java,v 1.0 02/06/16 harald.kuhr Exp$ + */ +public class ThumbsDBProviderInfoTest extends ReaderWriterProviderInfoTest { + + @Override + protected ReaderWriterProviderInfo createProviderInfo() { + return new ThumbsDBProviderInfo(); + } + + @Override + public void formatNames() { + String[] names = getProviderInfo().formatNames(); + assertNotNull(names); + assertFalse(names.length == 0); + + List list = new ArrayList<>(asList(names)); + assertTrue(list.remove("Thumbs DB")); // No dupes of this name + + for (String name : list) { + assertNotNull(name); + assertFalse(name.isEmpty()); + + assertTrue(list.contains(name.toLowerCase())); + assertTrue(list.contains(name.toUpperCase())); + } + } +} \ No newline at end of file diff --git a/imageio/imageio-tiff/pom.xml b/imageio/imageio-tiff/pom.xml index 7f8f118f..11f66f34 100644 --- a/imageio/imageio-tiff/pom.xml +++ b/imageio/imageio-tiff/pom.xml @@ -4,7 +4,7 @@ com.twelvemonkeys.imageio imageio - 3.1-SNAPSHOT + 3.3-SNAPSHOT imageio-tiff TwelveMonkeys :: ImageIO :: TIFF plugin @@ -24,7 +24,7 @@ com.twelvemonkeys.imageio imageio-core - tests + test-jar diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxDecoderStream.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxDecoderStream.java index 79cec686..16ac23b8 100644 --- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxDecoderStream.java +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxDecoderStream.java @@ -34,11 +34,13 @@ import java.io.EOFException; import java.io.FilterInputStream; import java.io.IOException; import java.io.InputStream; +import java.util.Arrays; /** * CCITT Modified Huffman RLE, Group 3 (T4) and Group 4 (T6) fax compression. * * @author Harald Kuhr + * @author Oliver Schmidtmer * @author last modified by $Author: haraldk$ * @version $Id: CCITTFaxDecoderStream.java,v 1.0 23.05.12 15:55 haraldk Exp$ */ @@ -51,33 +53,59 @@ final class CCITTFaxDecoderStream extends FilterInputStream { private int decodedLength; private int decodedPos; - private int bitBuffer; - private int bitBufferLength; - // Need to take fill order into account (?) (use flip table?) private final int fillOrder; private final int type; - private final int[] changes; - private int changesCount; + private int[] changesReferenceRow; + private int[] changesCurrentRow; + private int changesReferenceRowCount; + private int changesCurrentRowCount; - private static final int EOL_CODE = 0x01; // 12 bit + private int lastChangingElement = 0; - public CCITTFaxDecoderStream(final InputStream stream, final int columns, final int type, final int fillOrder) { + private boolean optionG32D = false; + + @SuppressWarnings("unused") // Leading zeros for aligning EOL + private boolean optionG3Fill = false; + + private boolean optionUncompressed = false; + + public CCITTFaxDecoderStream(final InputStream stream, final int columns, final int type, final int fillOrder, + final long options) { super(Validate.notNull(stream, "stream")); this.columns = Validate.isTrue(columns > 0, columns, "width must be greater than 0"); // We know this is only used for b/w (1 bit) this.decodedRow = new byte[(columns + 7) / 8]; - this.type = Validate.isTrue(type == TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE, type, "Only CCITT Modified Huffman RLE compression (2) supported: %s"); // TODO: Implement group 3 and 4 - this.fillOrder = Validate.isTrue(fillOrder == 1, fillOrder, "Only fill order 1 supported: %s"); // TODO: Implement fillOrder == 2 + this.type = Validate.isTrue( + type == TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE || + type == TIFFExtension.COMPRESSION_CCITT_T4 || type == TIFFExtension.COMPRESSION_CCITT_T6, + type, "Only CCITT Modified Huffman RLE compression (2), CCITT T4 (3) or CCITT T6 (4) supported: %s" + ); + this.fillOrder = Validate.isTrue( + fillOrder == TIFFBaseline.FILL_LEFT_TO_RIGHT || fillOrder == TIFFExtension.FILL_RIGHT_TO_LEFT, + fillOrder, "Expected fill order 1 or 2: %s" + ); - this.changes = new int[columns]; + this.changesReferenceRow = new int[columns + 1]; + this.changesCurrentRow = new int[columns + 1]; + + switch (type) { + case TIFFExtension.COMPRESSION_CCITT_T4: + optionG32D = (options & TIFFExtension.GROUP3OPT_2DENCODING) != 0; + optionG3Fill = (options & TIFFExtension.GROUP3OPT_FILLBITS) != 0; + optionUncompressed = (options & TIFFExtension.GROUP3OPT_UNCOMPRESSED) != 0; + break; + case TIFFExtension.COMPRESSION_CCITT_T6: + optionUncompressed = (options & TIFFExtension.GROUP4OPT_UNCOMPRESSED) != 0; + break; + } + + Validate.isTrue(!optionUncompressed, optionUncompressed, + "CCITT GROUP 3/4 OPTION UNCOMPRESSED is not supported"); } - // IDEA: Would it be faster to keep all bit combos of each length (>=2) that is NOT a code, to find bit length, then look up value in table? - // -- If white run, start at 4 bits to determine length, if black, start at 2 bits - private void fetch() throws IOException { if (decodedPos >= decodedLength) { decodedLength = 0; @@ -91,7 +119,8 @@ final class CCITTFaxDecoderStream extends FilterInputStream { throw e; } - // ..otherwise, just client code trying to read past the end of stream + // ..otherwise, just client code trying to read past the end of + // stream decodedLength = -1; } @@ -99,154 +128,300 @@ final class CCITTFaxDecoderStream extends FilterInputStream { } } - private void decodeRow() throws IOException { - resetBuffer(); + private void decode1D() throws IOException { + int index = 0; + boolean white = true; + changesCurrentRowCount = 0; - boolean literalRun = true; + do { + int completeRun; - /* - if (type == TIFFExtension.COMPRESSION_CCITT_T4) { - int eol = readBits(12); - System.err.println("eol: " + eol); - while (eol != EOL_CODE) { - eol = readBits(1); - System.err.println("eol: " + eol); -// throw new IOException("Missing EOL"); + if (white) { + completeRun = decodeRun(whiteRunTree); + } + else { + completeRun = decodeRun(blackRunTree); } - literalRun = readBits(1) == 1; + index += completeRun; + changesCurrentRow[changesCurrentRowCount++] = index; + + // Flip color for next run + white = !white; + } while (index < columns); + } + + private void decode2D() throws IOException { + changesReferenceRowCount = changesCurrentRowCount; + int[] tmp = changesCurrentRow; + changesCurrentRow = changesReferenceRow; + changesReferenceRow = tmp; + + boolean white = true; + int index = 0; + changesCurrentRowCount = 0; + + mode: while (index < columns) { + // read mode + Node n = codeTree.root; + + while (true) { + n = n.walk(readBit()); + + if (n == null) { + continue mode; + } + else if (n.isLeaf) { + switch (n.value) { + case VALUE_HMODE: + int runLength; + runLength = decodeRun(white ? whiteRunTree : blackRunTree); + index += runLength; + changesCurrentRow[changesCurrentRowCount++] = index; + + runLength = decodeRun(white ? blackRunTree : whiteRunTree); + index += runLength; + changesCurrentRow[changesCurrentRowCount++] = index; + break; + + case VALUE_PASSMODE: + int pChangingElement = getNextChangingElement(index, white) + 1; + + if (pChangingElement >= changesReferenceRowCount) { + index = columns; + } + else { + index = changesReferenceRow[pChangingElement]; + } + + break; + + default: + // Vertical mode (-3 to 3) + int vChangingElement = getNextChangingElement(index, white); + + if (vChangingElement >= changesReferenceRowCount || vChangingElement == -1) { + index = columns + n.value; + } + else { + index = changesReferenceRow[vChangingElement] + n.value; + } + + changesCurrentRow[changesCurrentRowCount] = index; + changesCurrentRowCount++; + white = !white; + + break; + } + + continue mode; + } + } + } + } + + private int getNextChangingElement(final int a0, final boolean white) throws IOException { + int start = (lastChangingElement & 0xFFFF_FFFE) + (white ? 0 : 1); + if (start > 2) { + start -= 2; } - System.err.println("literalRun: " + literalRun); - */ - int index = 0; + if (a0 == 0) { + return start; + } - if (literalRun) { - changesCount = 0; - boolean white = true; - - do { - int completeRun = 0; - - int run; - do { - if (white) { - run = decodeRun(WHITE_CODES, WHITE_RUN_LENGTHS, 4); - } - else { - run = decodeRun(BLACK_CODES, BLACK_RUN_LENGTHS, 2); - } - - completeRun += run; - } - while (run >= 64); // Additional makeup codes are packed into both b/w codes, terminating codes are < 64 bytes - - changes[changesCount++] = index + completeRun; - -// System.err.printf("%s run: %d\n", white ? "white" : "black", run); - - // TODO: Optimize with lookup for 0-7 bits? - // Fill bits to byte boundary... - while (index % 8 != 0 && completeRun-- > 0) { - decodedRow[index++ / 8] |= (white ? 1 << 8 - (index % 8) : 0); - } - - // ...then fill complete bytes to either 0xff or 0x00... - if (index % 8 == 0) { - final byte value = (byte) (white ? 0xff : 0x00); - - while (completeRun > 7) { - decodedRow[index / 8] = value; - completeRun -= 8; - index += 8; - } - } - - // ...finally fill any remaining bits - while (completeRun-- > 0) { - decodedRow[index++ / 8] |= (white ? 1 << 8 - (index % 8) : 0); - } - - // Flip color for next run - white = !white; + for (int i = start; i < changesReferenceRowCount; i += 2) { + if (a0 < changesReferenceRow[i]) { + lastChangingElement = i; + return i; } - while (index < columns); + } + + return -1; + } + + private void decodeRowType2() throws IOException { + resetBuffer(); + decode1D(); + } + + private void decodeRowType4() throws IOException { + eof: while (true) { + // read till next EOL code + Node n = eolOnlyTree.root; + + while (true) { + n = n.walk(readBit()); + + if (n == null) { + continue eof; + } + + if (n.isLeaf) { + break eof; + } + } + } + + if (!optionG32D || readBit()) { + decode1D(); } else { - // non-literal run + decode2D(); + } + } + + private void decodeRowType6() throws IOException { + decode2D(); + } + + private void decodeRow() throws IOException { + switch (type) { + case TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE: + decodeRowType2(); + break; + case TIFFExtension.COMPRESSION_CCITT_T4: + decodeRowType4(); + break; + case TIFFExtension.COMPRESSION_CCITT_T6: + decodeRowType6(); + break; } - if (type == TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE && index != columns) { + int index = 0; + boolean white = true; + + lastChangingElement = 0; + for (int i = 0; i <= changesCurrentRowCount; i++) { + int nextChange = columns; + + if (i != changesCurrentRowCount) { + nextChange = changesCurrentRow[i]; + } + + if (nextChange > columns) { + nextChange = columns; + } + + int byteIndex = index / 8; + + while (index % 8 != 0 && (nextChange - index) > 0) { + decodedRow[byteIndex] |= (white ? 0 : 1 << (7 - ((index) % 8))); + index++; + } + + if (index % 8 == 0) { + byteIndex = index / 8; + final byte value = (byte) (white ? 0x00 : 0xff); + + while ((nextChange - index) > 7) { + decodedRow[byteIndex] = value; + index += 8; + ++byteIndex; + } + } + + while ((nextChange - index) > 0) { + if (index % 8 == 0) { + decodedRow[byteIndex] = 0; + } + + decodedRow[byteIndex] |= (white ? 0 : 1 << (7 - ((index) % 8))); + index++; + } + + white = !white; + } + + if (index != columns) { throw new IOException("Sum of run-lengths does not equal scan line width: " + index + " > " + columns); } decodedLength = (index + 7) / 8; } - private int decodeRun(short[][] codes, short[][] runLengths, int minCodeSize) throws IOException { - // TODO: Optimize... - // Looping and comparing is the most straight-forward, but probably not the most effective way... - int code = readBits(minCodeSize); + private int decodeRun(final Tree tree) throws IOException { + int total = 0; - for (int bits = 0; bits < codes.length; bits++) { - short[] bitCodes = codes[bits]; + Node n = tree.root; - for (int i = 0; i < bitCodes.length; i++) { - if (bitCodes[i] == code) { -// System.err.println("code: " + code); + while (true) { + boolean bit = readBit(); + n = n.walk(bit); - // Code found, return matching run length - return runLengths[bits][i]; - } + if (n == null) { + throw new IOException("Unknown code in Huffman RLE stream"); } - // No code found, read one more bit and try again - code = fillOrder == 1 ? (code << 1) | readBits(1) : readBits(1) << (bits + minCodeSize) | code; + if (n.isLeaf) { + total += n.value; + if (n.value < 64) { + return total; + } + else { + n = tree.root; + } + } } - - throw new IOException("Unknown code in Huffman RLE stream"); } - private void resetBuffer() { + private void resetBuffer() throws IOException { for (int i = 0; i < decodedRow.length; i++) { decodedRow[i] = 0; } - bitBuffer = 0; - bitBufferLength = 0; + while (true) { + if (bufferPos == -1) { + return; + } + + readBit(); + } } - private int readBits(int bitCount) throws IOException { - while (bitBufferLength < bitCount) { - int read = in.read(); - if (read == -1) { + int buffer = -1; + int bufferPos = -1; + + private boolean readBit() throws IOException { + if (bufferPos < 0 || bufferPos > 7) { + buffer = in.read(); + + if (buffer == -1) { throw new EOFException("Unexpected end of Huffman RLE stream"); } - int bits = read & 0xff; - bitBuffer = (bitBuffer << 8) | bits; - bitBufferLength += 8; + bufferPos = 0; } - // TODO: Take fill order into account - bitBufferLength -= bitCount; - int result = bitBuffer >> bitBufferLength; - bitBuffer &= (1 << bitBufferLength) - 1; + boolean isSet; - return result; + if (fillOrder == TIFFBaseline.FILL_LEFT_TO_RIGHT) { + isSet = ((buffer >> (7 - bufferPos)) & 1) == 1; + } + else { + isSet = ((buffer >> (bufferPos)) & 1) == 1; + } + + bufferPos++; + + if (bufferPos > 7) { + bufferPos = -1; + } + + return isSet; } @Override public int read() throws IOException { if (decodedLength < 0) { - return -1; + return 0x0; } if (decodedPos >= decodedLength) { fetch(); if (decodedLength < 0) { - return -1; + return 0x0; } } @@ -256,14 +431,16 @@ final class CCITTFaxDecoderStream extends FilterInputStream { @Override public int read(byte[] b, int off, int len) throws IOException { if (decodedLength < 0) { - return -1; + Arrays.fill(b, off, off + len, (byte) 0x0); + return len; } if (decodedPos >= decodedLength) { fetch(); if (decodedLength < 0) { - return -1; + Arrays.fill(b, off, off + len, (byte) 0x0); + return len; } } @@ -304,150 +481,320 @@ final class CCITTFaxDecoderStream extends FilterInputStream { throw new IOException("mark/reset not supported"); } + private static final class Node { + Node left; + Node right; + + int value; // > 63 non term. + + boolean canBeFill = false; + boolean isLeaf = false; + + void set(final boolean next, final Node node) { + if (!next) { + left = node; + } + else { + right = node; + } + } + + Node walk(final boolean next) { + return next ? right : left; + } + + @Override + public String toString() { + return "[leaf=" + isLeaf + ", value=" + value + ", canBeFill=" + canBeFill + "]"; + } + } + + private static final class Tree { + final Node root = new Node(); + + void fill(final int depth, final int path, final int value) throws IOException { + Node current = root; + + for (int i = 0; i < depth; i++) { + int bitPos = depth - 1 - i; + boolean isSet = ((path >> bitPos) & 1) == 1; + Node next = current.walk(isSet); + + if (next == null) { + next = new Node(); + + if (i == depth - 1) { + next.value = value; + next.isLeaf = true; + } + + if (path == 0) { + next.canBeFill = true; + } + + current.set(isSet, next); + } + else { + if (next.isLeaf) { + throw new IOException("node is leaf, no other following"); + } + } + + current = next; + } + } + + void fill(final int depth, final int path, final Node node) throws IOException { + Node current = root; + + for (int i = 0; i < depth; i++) { + int bitPos = depth - 1 - i; + boolean isSet = ((path >> bitPos) & 1) == 1; + Node next = current.walk(isSet); + + if (next == null) { + if (i == depth - 1) { + next = node; + } + else { + next = new Node(); + } + + if (path == 0) { + next.canBeFill = true; + } + + current.set(isSet, next); + } + else { + if (next.isLeaf) { + throw new IOException("node is leaf, no other following"); + } + } + + current = next; + } + } + } + static final short[][] BLACK_CODES = { { // 2 bits - 0x2, 0x3, - }, + 0x2, 0x3, + }, { // 3 bits - 0x2, 0x3, - }, + 0x2, 0x3, + }, { // 4 bits - 0x2, 0x3, - }, + 0x2, 0x3, + }, { // 5 bits - 0x3, - }, + 0x3, + }, { // 6 bits - 0x4, 0x5, - }, + 0x4, 0x5, + }, { // 7 bits - 0x4, 0x5, 0x7, - }, + 0x4, 0x5, 0x7, + }, { // 8 bits - 0x4, 0x7, - }, + 0x4, 0x7, + }, { // 9 bits - 0x18, - }, + 0x18, + }, { // 10 bits - 0x17, 0x18, 0x37, 0x8, 0xf, - }, + 0x17, 0x18, 0x37, 0x8, 0xf, + }, { // 11 bits - 0x17, 0x18, 0x28, 0x37, 0x67, 0x68, 0x6c, 0x8, 0xc, 0xd, - }, + 0x17, 0x18, 0x28, 0x37, 0x67, 0x68, 0x6c, 0x8, 0xc, 0xd, + }, { // 12 bits - 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x1c, 0x1d, 0x1e, 0x1f, 0x24, 0x27, 0x28, 0x2b, 0x2c, 0x33, - 0x34, 0x35, 0x37, 0x38, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x5b, 0x64, 0x65, - 0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0xc8, 0xc9, 0xca, 0xcb, 0xcc, 0xcd, 0xd2, 0xd3, - 0xd4, 0xd5, 0xd6, 0xd7, 0xda, 0xdb, - }, + 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x1c, 0x1d, 0x1e, 0x1f, 0x24, 0x27, 0x28, 0x2b, 0x2c, 0x33, + 0x34, 0x35, 0x37, 0x38, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x5b, 0x64, 0x65, + 0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0xc8, 0xc9, 0xca, 0xcb, 0xcc, 0xcd, 0xd2, 0xd3, + 0xd4, 0xd5, 0xd6, 0xd7, 0xda, 0xdb, + }, { // 13 bits - 0x4a, 0x4b, 0x4c, 0x4d, 0x52, 0x53, 0x54, 0x55, 0x5a, 0x5b, 0x64, 0x65, 0x6c, 0x6d, 0x72, 0x73, - 0x74, 0x75, 0x76, 0x77, - } + 0x4a, 0x4b, 0x4c, 0x4d, 0x52, 0x53, 0x54, 0x55, 0x5a, 0x5b, 0x64, 0x65, 0x6c, 0x6d, 0x72, 0x73, + 0x74, 0x75, 0x76, 0x77, + } }; static final short[][] BLACK_RUN_LENGTHS = { { // 2 bits - 3, 2, - }, + 3, 2, + }, { // 3 bits - 1, 4, - }, + 1, 4, + }, { // 4 bits - 6, 5, - }, + 6, 5, + }, { // 5 bits - 7, - }, + 7, + }, { // 6 bits - 9, 8, - }, + 9, 8, + }, { // 7 bits - 10, 11, 12, - }, + 10, 11, 12, + }, { // 8 bits - 13, 14, - }, + 13, 14, + }, { // 9 bits - 15, - }, + 15, + }, { // 10 bits - 16, 17, 0, 18, 64, - }, + 16, 17, 0, 18, 64, + }, { // 11 bits - 24, 25, 23, 22, 19, 20, 21, 1792, 1856, 1920, - }, + 24, 25, 23, 22, 19, 20, 21, 1792, 1856, 1920, + }, { // 12 bits - 1984, 2048, 2112, 2176, 2240, 2304, 2368, 2432, 2496, 2560, 52, 55, 56, 59, 60, 320, - 384, 448, 53, 54, 50, 51, 44, 45, 46, 47, 57, 58, 61, 256, 48, 49, - 62, 63, 30, 31, 32, 33, 40, 41, 128, 192, 26, 27, 28, 29, 34, 35, - 36, 37, 38, 39, 42, 43, - }, + 1984, 2048, 2112, 2176, 2240, 2304, 2368, 2432, 2496, 2560, 52, 55, 56, 59, 60, 320, 384, 448, 53, + 54, 50, 51, 44, 45, 46, 47, 57, 58, 61, 256, 48, 49, 62, 63, 30, 31, 32, 33, 40, 41, 128, 192, 26, + 27, 28, 29, 34, 35, 36, 37, 38, 39, 42, 43, + }, { // 13 bits - 640, 704, 768, 832, 1280, 1344, 1408, 1472, 1536, 1600, 1664, 1728, 512, 576, 896, 960, - 1024, 1088, 1152, 1216, - } + 640, 704, 768, 832, 1280, 1344, 1408, 1472, 1536, 1600, 1664, 1728, 512, 576, 896, 960, 1024, 1088, + 1152, 1216, + } }; public static final short[][] WHITE_CODES = { { // 4 bits - 0x7, 0x8, 0xb, 0xc, 0xe, 0xf, - }, + 0x7, 0x8, 0xb, 0xc, 0xe, 0xf, + }, { // 5 bits - 0x12, 0x13, 0x14, 0x1b, 0x7, 0x8, - }, + 0x12, 0x13, 0x14, 0x1b, 0x7, 0x8, + }, { // 6 bits - 0x17, 0x18, 0x2a, 0x2b, 0x3, 0x34, 0x35, 0x7, 0x8, - }, + 0x17, 0x18, 0x2a, 0x2b, 0x3, 0x34, 0x35, 0x7, 0x8, + }, { // 7 bits - 0x13, 0x17, 0x18, 0x24, 0x27, 0x28, 0x2b, 0x3, 0x37, 0x4, 0x8, 0xc, - }, + 0x13, 0x17, 0x18, 0x24, 0x27, 0x28, 0x2b, 0x3, 0x37, 0x4, 0x8, 0xc, + }, { // 8 bits - 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x1a, 0x1b, 0x2, 0x24, 0x25, 0x28, 0x29, 0x2a, 0x2b, 0x2c, - 0x2d, 0x3, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x4, 0x4a, 0x4b, 0x5, 0x52, 0x53, 0x54, 0x55, - 0x58, 0x59, 0x5a, 0x5b, 0x64, 0x65, 0x67, 0x68, 0xa, 0xb, - }, + 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x1a, 0x1b, 0x2, 0x24, 0x25, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, + 0x3, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x4, 0x4a, 0x4b, 0x5, 0x52, 0x53, 0x54, 0x55, 0x58, 0x59, + 0x5a, 0x5b, 0x64, 0x65, 0x67, 0x68, 0xa, 0xb, + }, { // 9 bits - 0x98, 0x99, 0x9a, 0x9b, 0xcc, 0xcd, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xdb, - }, + 0x98, 0x99, 0x9a, 0x9b, 0xcc, 0xcd, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xdb, + }, { // 10 bits }, { // 11 bits - 0x8, 0xc, 0xd, - }, + 0x8, 0xc, 0xd, + }, { // 12 bits - 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x1c, 0x1d, 0x1e, 0x1f, - } + 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x1c, 0x1d, 0x1e, 0x1f, + } }; public static final short[][] WHITE_RUN_LENGTHS = { { // 4 bits - 2, 3, 4, 5, 6, 7, - }, + 2, 3, 4, 5, 6, 7, + }, { // 5 bits - 128, 8, 9, 64, 10, 11, - }, + 128, 8, 9, 64, 10, 11, + }, { // 6 bits - 192, 1664, 16, 17, 13, 14, 15, 1, 12, - }, + 192, 1664, 16, 17, 13, 14, 15, 1, 12, + }, { // 7 bits - 26, 21, 28, 27, 18, 24, 25, 22, 256, 23, 20, 19, - }, + 26, 21, 28, 27, 18, 24, 25, 22, 256, 23, 20, 19, + }, { // 8 bits - 33, 34, 35, 36, 37, 38, 31, 32, 29, 53, 54, 39, 40, 41, 42, 43, - 44, 30, 61, 62, 63, 0, 320, 384, 45, 59, 60, 46, 49, 50, 51, - 52, 55, 56, 57, 58, 448, 512, 640, 576, 47, 48, - }, + 33, 34, 35, 36, 37, 38, 31, 32, 29, 53, 54, 39, 40, 41, 42, 43, 44, 30, 61, 62, 63, 0, 320, 384, 45, + 59, 60, 46, 49, 50, 51, 52, 55, 56, 57, 58, 448, 512, 640, 576, 47, 48, + }, { // 9 bits - 1472, 1536, 1600, 1728, 704, 768, 832, 896, 960, 1024, 1088, 1152, 1216, 1280, 1344, 1408, - }, + 1472, 1536, 1600, 1728, 704, 768, 832, 896, 960, 1024, 1088, 1152, 1216, 1280, 1344, 1408, + }, { // 10 bits }, { // 11 bits - 1792, 1856, 1920, - }, + 1792, 1856, 1920, + }, { // 12 bits - 1984, 2048, 2112, 2176, 2240, 2304, 2368, 2432, 2496, 2560, - } + 1984, 2048, 2112, 2176, 2240, 2304, 2368, 2432, 2496, 2560, + } }; + + final static Node EOL; + final static Node FILL; + final static Tree blackRunTree; + final static Tree whiteRunTree; + final static Tree eolOnlyTree; + final static Tree codeTree; + + final static int VALUE_EOL = -2000; + final static int VALUE_FILL = -1000; + final static int VALUE_PASSMODE = -3000; + final static int VALUE_HMODE = -4000; + + static { + EOL = new Node(); + EOL.isLeaf = true; + EOL.value = VALUE_EOL; + FILL = new Node(); + FILL.value = VALUE_FILL; + FILL.left = FILL; + FILL.right = EOL; + + eolOnlyTree = new Tree(); + try { + eolOnlyTree.fill(12, 0, FILL); + eolOnlyTree.fill(12, 1, EOL); + } + catch (IOException e) { + throw new AssertionError(e); + } + + blackRunTree = new Tree(); + try { + for (int i = 0; i < BLACK_CODES.length; i++) { + for (int j = 0; j < BLACK_CODES[i].length; j++) { + blackRunTree.fill(i + 2, BLACK_CODES[i][j], BLACK_RUN_LENGTHS[i][j]); + } + } + blackRunTree.fill(12, 0, FILL); + blackRunTree.fill(12, 1, EOL); + } + catch (IOException e) { + throw new AssertionError(e); + } + + whiteRunTree = new Tree(); + try { + for (int i = 0; i < WHITE_CODES.length; i++) { + for (int j = 0; j < WHITE_CODES[i].length; j++) { + whiteRunTree.fill(i + 4, WHITE_CODES[i][j], WHITE_RUN_LENGTHS[i][j]); + } + } + + whiteRunTree.fill(12, 0, FILL); + whiteRunTree.fill(12, 1, EOL); + } + catch (IOException e) { + throw new AssertionError(e); + } + + codeTree = new Tree(); + try { + codeTree.fill(4, 1, VALUE_PASSMODE); // pass mode + codeTree.fill(3, 1, VALUE_HMODE); // H mode + codeTree.fill(1, 1, 0); // V(0) + codeTree.fill(3, 3, 1); // V_R(1) + codeTree.fill(6, 3, 2); // V_R(2) + codeTree.fill(7, 3, 3); // V_R(3) + codeTree.fill(3, 2, -1); // V_L(1) + codeTree.fill(6, 2, -2); // V_L(2) + codeTree.fill(7, 2, -3); // V_L(3) + } + catch (IOException e) { + throw new AssertionError(e); + } + } } diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxEncoderStream.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxEncoderStream.java new file mode 100644 index 00000000..8a250ece --- /dev/null +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxEncoderStream.java @@ -0,0 +1,396 @@ +/* + * Copyright (c) 2013, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.imageio.plugins.tiff; + +import com.twelvemonkeys.lang.Validate; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * CCITT Modified Huffman RLE, Group 3 (T4) and Group 4 (T6) fax compression. + * + * @author Oliver Schmidtmer + * @author last modified by $Author$ + * @version $Id$ + */ +final class CCITTFaxEncoderStream extends OutputStream { + + private int currentBufferLength = 0; + private final byte[] inputBuffer; + private final int inputBufferLength; + private int columns; + private int rows; + + private int[] changesCurrentRow; + private int[] changesReferenceRow; + private int currentRow = 0; + private int changesCurrentRowLength = 0; + private int changesReferenceRowLength = 0; + private byte outputBuffer = 0; + private byte outputBufferBitLength = 0; + private int type; + private int fillOrder; + private boolean optionG32D; + private boolean optionG3Fill; + private boolean optionUncompressed; + private OutputStream stream; + + public CCITTFaxEncoderStream(final OutputStream stream, final int columns, final int rows, final int type, final int fillOrder, + final long options) { + + this.stream = stream; + this.type = type; + this.columns = columns; + this.rows = rows; + this.fillOrder = fillOrder; + + this.changesReferenceRow = new int[columns]; + this.changesCurrentRow = new int[columns]; + + switch (type) { + case TIFFExtension.COMPRESSION_CCITT_T4: + optionG32D = (options & TIFFExtension.GROUP3OPT_2DENCODING) != 0; + optionG3Fill = (options & TIFFExtension.GROUP3OPT_FILLBITS) != 0; + optionUncompressed = (options & TIFFExtension.GROUP3OPT_UNCOMPRESSED) != 0; + break; + case TIFFExtension.COMPRESSION_CCITT_T6: + optionUncompressed = (options & TIFFExtension.GROUP4OPT_UNCOMPRESSED) != 0; + break; + } + + inputBufferLength = (columns + 7) / 8; + inputBuffer = new byte[inputBufferLength]; + + Validate.isTrue(!optionUncompressed, optionUncompressed, + "CCITT GROUP 3/4 OPTION UNCOMPRESSED is not supported"); + } + + @Override + public void write(int b) throws IOException { + inputBuffer[currentBufferLength] = (byte) b; + currentBufferLength++; + + if (currentBufferLength == inputBufferLength) { + encodeRow(); + currentBufferLength = 0; + } + } + + @Override + public void flush() throws IOException { + stream.flush(); + } + + @Override + public void close() throws IOException { + stream.close(); + } + + private void encodeRow() throws IOException { + currentRow++; + int[] tmp = changesReferenceRow; + changesReferenceRow = changesCurrentRow; + changesCurrentRow = tmp; + changesReferenceRowLength = changesCurrentRowLength; + changesCurrentRowLength = 0; + + int index = 0; + boolean white = true; + while (index < columns) { + int byteIndex = index / 8; + int bit = index % 8; + if ((((inputBuffer[byteIndex] >> (7 - bit)) & 1) == 1) == (white)) { + changesCurrentRow[changesCurrentRowLength] = index; + changesCurrentRowLength++; + white = !white; + } + index++; + } + + switch (type) { + case TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE: + encodeRowType2(); + break; + case TIFFExtension.COMPRESSION_CCITT_T4: + encodeRowType4(); + break; + case TIFFExtension.COMPRESSION_CCITT_T6: + encodeRowType6(); + break; + } + + if (currentRow == rows) { + if (type == TIFFExtension.COMPRESSION_CCITT_T6) { + writeEOL(); + writeEOL(); + } + fill(); + } + } + + private void encodeRowType2() throws IOException { + encode1D(); + fill(); + } + + private void encodeRowType4() throws IOException { + writeEOL(); + if (optionG32D) { + // do k=1 only on first line. Detect first line by missing reference + // line. + if (changesReferenceRowLength == 0) { + write(1, 1); + encode1D(); + } + else { + write(0, 1); + encode2D(); + } + } + else { + encode1D(); + } + if (optionG3Fill) { + fill(); + } + } + + private void encodeRowType6() throws IOException { + encode2D(); + } + + private void encode1D() throws IOException { + int index = 0; + boolean white = true; + while (index < columns) { + int[] nextChanges = getNextChanges(index, white); + int runLength = nextChanges[0] - index; + writeRun(runLength, white); + index += runLength; + white = !white; + } + } + + private int[] getNextChanges(int pos, boolean white) { + int[] result = new int[] {columns, columns}; + for (int i = 0; i < changesCurrentRowLength; i++) { + if (pos < changesCurrentRow[i] || (pos == 0 && white)) { + result[0] = changesCurrentRow[i]; + if ((i + 1) < changesCurrentRowLength) { + result[1] = changesCurrentRow[i + 1]; + } + break; + } + } + + return result; + } + + private void writeRun(int runLength, boolean white) throws IOException { + int nonterm = runLength / 64; + Code[] codes = white ? WHITE_NONTERMINATING_CODES : BLACK_NONTERMINATING_CODES; + while (nonterm > 0) { + if (nonterm >= codes.length) { + write(codes[codes.length - 1].code, codes[codes.length - 1].length); + nonterm -= codes.length; + } + else { + write(codes[nonterm - 1].code, codes[nonterm - 1].length); + nonterm = 0; + } + } + + Code c = white ? WHITE_TERMINATING_CODES[runLength % 64] : BLACK_TERMINATING_CODES[runLength % 64]; + write(c.code, c.length); + } + + private void encode2D() throws IOException { + boolean white = true; + int index = 0; // a0 + while (index < columns) { + int[] nextChanges = getNextChanges(index, white); // a1, a2 + + int[] nextRefs = getNextRefChanges(index, white); // b1, b2 + + int difference = nextChanges[0] - nextRefs[0]; + if (nextChanges[0] > nextRefs[1]) { + // PMODE + write(1, 4); + index = nextRefs[1]; + } + else if (difference > 3 || difference < -3) { + // HMODE + write(1, 3); + writeRun(nextChanges[0] - index, white); + writeRun(nextChanges[1] - nextChanges[0], !white); + index = nextChanges[1]; + + } + else { + // VMODE + switch (difference) { + case 0: + write(1, 1); + break; + case 1: + write(3, 3); + break; + case 2: + write(3, 6); + break; + case 3: + write(3, 7); + break; + case -1: + write(2, 3); + break; + case -2: + write(2, 6); + break; + case -3: + write(2, 7); + break; + } + white = !white; + index = nextRefs[0] + difference; + } + } + } + + private int[] getNextRefChanges(int a0, boolean white) { + int[] result = new int[] {columns, columns}; + for (int i = (white ? 0 : 1); i < changesReferenceRowLength; i += 2) { + if (changesReferenceRow[i] > a0 || (a0 == 0 && i == 0)) { + result[0] = changesReferenceRow[i]; + if ((i + 1) < changesReferenceRowLength) { + result[1] = changesReferenceRow[i + 1]; + } + break; + } + } + return result; + } + + private void write(int code, int codeLength) throws IOException { + + for (int i = 0; i < codeLength; i++) { + boolean codeBit = ((code >> (codeLength - i - 1)) & 1) == 1; + if (fillOrder == TIFFBaseline.FILL_LEFT_TO_RIGHT) { + outputBuffer |= (codeBit ? 1 << (7 - ((outputBufferBitLength) % 8)) : 0); + } + else { + outputBuffer |= (codeBit ? 1 << (((outputBufferBitLength) % 8)) : 0); + } + outputBufferBitLength++; + + if (outputBufferBitLength == 8) { + stream.write(outputBuffer); + clearOutputBuffer(); + } + } + } + + private void writeEOL() throws IOException { + if (optionG3Fill) { + // Fill up so EOL ends on a byte-boundary + while (outputBufferBitLength != 4) { + write(0, 1); + } + } + write(1, 12); + } + + private void fill() throws IOException { + if (outputBufferBitLength != 0) { + stream.write(outputBuffer); + } + clearOutputBuffer(); + } + + private void clearOutputBuffer() { + outputBuffer = 0; + outputBufferBitLength = 0; + } + + public static class Code { + private Code(int code, int length) { + this.code = code; + this.length = length; + } + + final int code; + final int length; + } + + public static final Code[] WHITE_TERMINATING_CODES; + + public static final Code[] WHITE_NONTERMINATING_CODES; + + public static final Code[] BLACK_TERMINATING_CODES; + + public static final Code[] BLACK_NONTERMINATING_CODES; + + static { + // Setup HUFFMAN Codes + WHITE_TERMINATING_CODES = new Code[64]; + WHITE_NONTERMINATING_CODES = new Code[40]; + for (int i = 0; i < CCITTFaxDecoderStream.WHITE_CODES.length; i++) { + int bitLength = i + 4; + for (int j = 0; j < CCITTFaxDecoderStream.WHITE_CODES[i].length; j++) { + int value = CCITTFaxDecoderStream.WHITE_RUN_LENGTHS[i][j]; + int code = CCITTFaxDecoderStream.WHITE_CODES[i][j]; + + if (value < 64) { + WHITE_TERMINATING_CODES[value] = new Code(code, bitLength); + } + else { + WHITE_NONTERMINATING_CODES[(value / 64) - 1] = new Code(code, bitLength); + } + } + } + + BLACK_TERMINATING_CODES = new Code[64]; + BLACK_NONTERMINATING_CODES = new Code[40]; + for (int i = 0; i < CCITTFaxDecoderStream.BLACK_CODES.length; i++) { + int bitLength = i + 2; + for (int j = 0; j < CCITTFaxDecoderStream.BLACK_CODES[i].length; j++) { + int value = CCITTFaxDecoderStream.BLACK_RUN_LENGTHS[i][j]; + int code = CCITTFaxDecoderStream.BLACK_CODES[i][j]; + + if (value < 64) { + BLACK_TERMINATING_CODES[value] = new Code(code, bitLength); + } + else { + BLACK_NONTERMINATING_CODES[(value / 64) - 1] = new Code(code, bitLength); + } + } + } + } +} diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/LZWDecoder.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/LZWDecoder.java index 87c328c3..251b3812 100644 --- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/LZWDecoder.java +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/LZWDecoder.java @@ -190,7 +190,6 @@ abstract class LZWDecoder implements Decoder { public static Decoder create(boolean oldBitReversedStream) { return oldBitReversedStream ? new LZWCompatibilityDecoder() : new LZWSpecDecoder(); -// return oldBitReversedStream ? new LZWCompatibilityDecoder() : new LZWTreeDecoder(); } static final class LZWSpecDecoder extends LZWDecoder { diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/LZWEncoder.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/LZWEncoder.java index ee3cdf17..47afa666 100644 --- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/LZWEncoder.java +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/LZWEncoder.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014, Harald Kuhr + * Copyright (c) 2015, Harald Kuhr * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -33,21 +33,22 @@ import com.twelvemonkeys.io.enc.Encoder; import java.io.IOException; import java.io.OutputStream; import java.nio.ByteBuffer; -import java.util.Map; -import java.util.TreeMap; - -import static com.twelvemonkeys.imageio.plugins.tiff.LZWDecoder.LZWString; +import java.util.Arrays; /** * LZWEncoder + *

+ * Inspired by LZWTreeEncoder by Wen Yu and the + * algorithm described by Bob Montgomery + * which + * "[...] uses a tree method to search if a new string is already in the table, + * which is much simpler, faster, and easier to understand than hashing." * * @author Harald Kuhr * @author last modified by $Author: haraldk$ * @version $Id: LZWEncoder.java,v 1.0 02.12.13 14:13 haraldk Exp$ */ final class LZWEncoder implements Encoder { - // TODO: Consider extracting LZWStringTable from LZWDecoder - /** Clear: Re-initialize tables. */ static final int CLEAR_CODE = 256; /** End of Information. */ @@ -58,159 +59,136 @@ final class LZWEncoder implements Encoder { private static final int TABLE_SIZE = 1 << MAX_BITS; - private int remaining; + // A child is made up of a parent (or prefix) code plus a suffix byte + // and siblings are strings with a common parent(or prefix) and different + // suffix bytes + private final short[] CHILDREN = new short[TABLE_SIZE]; + private final short[] SIBLINGS = new short[TABLE_SIZE]; + private final short[] SUFFIXES = new short[TABLE_SIZE]; - private final LZWString[] table = new LZWString[TABLE_SIZE]; -// private final Map reverseTable = new HashMap<>(TABLE_SIZE - 256); // This is foobar - private final Map reverseTable = new TreeMap<>(); // This is foobar - private int tableLength; - LZWString omega = LZWString.EMPTY; + // Initial setup + private int parent = -1; + private int bitsPerCode = MIN_BITS; + private int nextValidCode = EOI_CODE + 1; + private int maxCode = maxValue(bitsPerCode); - int bitsPerCode; - private int oldCode = CLEAR_CODE; - private int maxCode; - int bitMask; + // Buffer for partial codes + private int bits = 0; + private int bitPos = 0; - int bits; - int bitPos; + // Keep track of how many bytes we will write, to make sure we write EOI at correct position + private long remaining; - protected LZWEncoder(final int length) { - this.remaining = length; - - // First 258 entries of table is always fixed - for (int i = 0; i < 256; i++) { - table[i] = new LZWString((byte) i); - } - - init(); - } - - private static int bitmaskFor(final int bits) { - return (1 << bits) - 1; - } - - private void init() { - tableLength = 258; - bitsPerCode = MIN_BITS; - bitMask = bitmaskFor(bitsPerCode); - maxCode = maxCode(); -// omega = LZWString.EMPTY; - reverseTable.clear(); - } - - protected int maxCode() { - return bitMask; + LZWEncoder(final long length) { + remaining = length; } + @Override public void encode(final OutputStream stream, final ByteBuffer buffer) throws IOException { -// InitializeStringTable(); -// WriteCode(ClearCode); -// Ω = the empty string; -// for each character in the strip { -// K = GetNextCharacter(); -// if Ω+K is in the string table { -// Ω = Ω+K;/* string concatenation */ -// } -// else{ -// WriteCode (CodeFromString( Ω)); -// AddTableEntry(Ω+K); -// Ω=K; -// } }/*end of for loop*/ -// WriteCode (CodeFromString(Ω)); -// WriteCode (EndOfInformation); + encodeBytes(stream, buffer); - if (remaining < 0) { - throw new IOException("Write past end of stream"); - } - - // TODO: Write 9 bit clear code ONLY first time! - if (oldCode == CLEAR_CODE) { - writeCode(stream, CLEAR_CODE); - } - - int len = buffer.remaining(); - - while (buffer.hasRemaining()) { - byte k = buffer.get(); - - LZWString string = omega.concatenate(k); - - int tableIndex = isInTable(string); - if (tableIndex >= 0) { - omega = string; - oldCode = tableIndex; - } - else { - writeCode(stream, oldCode); - addStringToTable(string); - oldCode = k & 0xff; - omega = table[k & 0xff]; - - // Handle table (almost) full - if (tableLength >= TABLE_SIZE - 2) { - writeCode(stream, CLEAR_CODE); - init(); - } - } - } - - remaining -= len; - - // Write EOI when er are done (the API isn't very supportive of this) if (remaining <= 0) { - writeCode(stream, oldCode); + // Write EOI when er are done (the API isn't very supportive of this at the moment) + writeCode(stream, parent); writeCode(stream, EOI_CODE); + + // Flush partial codes by writing 0 pad if (bitPos > 0) { writeCode(stream, 0); } } } - private int isInTable(final LZWString string) { - if (string.length == 1) { - return string.value & 0xff; + void encodeBytes(final OutputStream stream, final ByteBuffer buffer) throws IOException { + int length = buffer.remaining(); + + if (length == 0) { + return; } - Integer index = reverseTable.get(string); - return index != null ? index : -1; + if (parent == -1) { + // Init stream + writeCode(stream, CLEAR_CODE); + parent = buffer.get() & 0xff; + } - // TODO: Needs optimization :-) -// for (int i = 258; i < tableLength; i++) { -// if (table[i].equals(string)) { -// return i; -// } -// } + while (buffer.hasRemaining()) { + int value = buffer.get() & 0xff; + int child = CHILDREN[parent]; -// return -1; + if (child > 0) { + if (SUFFIXES[child] == value) { + parent = child; + } + else { + int sibling = child; + + while (true) { + if (SIBLINGS[sibling] > 0) { + sibling = SIBLINGS[sibling]; + + if (SUFFIXES[sibling] == value) { + parent = sibling; + break; + } + } + else { + SIBLINGS[sibling] = (short) nextValidCode; + SUFFIXES[nextValidCode] = (short) value; + writeCode(stream, parent); + parent = value; + nextValidCode++; + + increaseCodeSizeOrResetIfNeeded(stream); + + break; + } + } + } + } + else { + CHILDREN[parent] = (short) nextValidCode; + SUFFIXES[nextValidCode] = (short) value; + writeCode(stream, parent); + parent = value; + nextValidCode++; + + increaseCodeSizeOrResetIfNeeded(stream); + } + } + + remaining -= length; } - private int addStringToTable(final LZWString string) { -// System.err.println("LZWEncoder.addStringToTable: " + string); - final int index = tableLength++; - table[index] = string; - reverseTable.put(string, index); + private void increaseCodeSizeOrResetIfNeeded(final OutputStream stream) throws IOException { + if (nextValidCode > maxCode) { + if (bitsPerCode == MAX_BITS) { + // Reset stream by writing Clear code + writeCode(stream, CLEAR_CODE); - if (tableLength > maxCode) { - bitsPerCode++; - - if (bitsPerCode > MAX_BITS) { - throw new IllegalStateException(String.format("TIFF LZW with more than %d bits per code encountered (table overflow)", MAX_BITS)); + // Reset tables + resetTables(); + } + else { + // Increase code size + bitsPerCode++; + maxCode = maxValue(bitsPerCode); } - - bitMask = bitmaskFor(bitsPerCode); - maxCode = maxCode(); } + } -// if (string.length > maxString) { -// maxString = string.length; -// } + private void resetTables() { + Arrays.fill(CHILDREN, (short) 0); + Arrays.fill(SIBLINGS, (short) 0); - return index; + bitsPerCode = MIN_BITS; + maxCode = maxValue(bitsPerCode); + nextValidCode = EOI_CODE + 1; } private void writeCode(final OutputStream stream, final int code) throws IOException { // System.err.printf("LZWEncoder.writeCode: 0x%04x\n", code); - bits = (bits << bitsPerCode) | (code & bitMask); + bits = (bits << bitsPerCode) | (code & maxCode); bitPos += bitsPerCode; while (bitPos >= 8) { @@ -222,4 +200,12 @@ final class LZWEncoder implements Encoder { bits &= bitmaskFor(bitPos); } -} + + private static int maxValue(final int codeLen) { + return (1 << codeLen) - 1; + } + + private static int bitmaskFor(final int bits) { + return maxValue(bits); + } +} \ No newline at end of file diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFBaseline.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFBaseline.java index 2fbd54e5..6d9a8cee 100644 --- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFBaseline.java +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFBaseline.java @@ -59,4 +59,13 @@ interface TIFFBaseline { int RESOLUTION_UNIT_NONE = 1; int RESOLUTION_UNIT_DPI = 2; // Default int RESOLUTION_UNIT_CENTIMETER = 3; + + int FILL_LEFT_TO_RIGHT = 1; // Default + + // NOTE: These are bit flags that can be ORed together! + int FILETYPE_REDUCEDIMAGE = 1; + int FILETYPE_PAGE = 2; + int FILETYPE_MASK = 4; + + int ORIENTATION_TOPLEFT = 1; } diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFCustom.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFCustom.java index 3a03ea31..d62652a2 100644 --- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFCustom.java +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFCustom.java @@ -50,6 +50,7 @@ interface TIFFCustom { int COMPRESSION_SGILOG = 34676; int COMPRESSION_SGILOG24 = 34677; int COMPRESSION_JPEG2000 = 34712; + // TODO: Aperio SVS JPEG2000: 33003 (YCbCr) and 33005 (RGB), see http://openslide.org/formats/aperio/ int PHOTOMETRIC_LOGL = 32844; int PHOTOMETRIC_LOGLUV = 32845; diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFExtension.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFExtension.java index de714606..f6444464 100644 --- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFExtension.java +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFExtension.java @@ -62,6 +62,8 @@ interface TIFFExtension { int PREDICTOR_HORIZONTAL_DIFFERENCING = 2; int PREDICTOR_HORIZONTAL_FLOATINGPOINT = 3; + int FILL_RIGHT_TO_LEFT = 2; + int SAMPLEFORMAT_INT = 2; int SAMPLEFORMAT_FP = 3; int SAMPLEFORMAT_UNDEFINED = 4; @@ -83,4 +85,17 @@ interface TIFFExtension { * description of the inks to be used. */ int INKSET_NOT_CMYK = 2; + + int ORIENTATION_TOPRIGHT = 2; + int ORIENTATION_BOTRIGHT = 3; + int ORIENTATION_BOTLEFT = 4; + int ORIENTATION_LEFTTOP = 5; + int ORIENTATION_RIGHTTOP = 6; + int ORIENTATION_RIGHTBOT = 7; + int ORIENTATION_LEFTBOT = 8; + + int GROUP3OPT_2DENCODING = 1; + int GROUP3OPT_UNCOMPRESSED = 2; + int GROUP3OPT_FILLBITS = 4; + int GROUP4OPT_UNCOMPRESSED = 2; } diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageMetadata.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageMetadata.java new file mode 100644 index 00000000..d652d81e --- /dev/null +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageMetadata.java @@ -0,0 +1,1347 @@ +/* + * Copyright (c) 2015, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.imageio.plugins.tiff; + +import com.twelvemonkeys.imageio.AbstractMetadata; +import com.twelvemonkeys.imageio.metadata.AbstractDirectory; +import com.twelvemonkeys.imageio.metadata.Directory; +import com.twelvemonkeys.imageio.metadata.Entry; +import com.twelvemonkeys.imageio.metadata.exif.Rational; +import com.twelvemonkeys.imageio.metadata.exif.TIFF; +import com.twelvemonkeys.lang.Validate; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import javax.imageio.metadata.IIOInvalidTreeException; +import javax.imageio.metadata.IIOMetadataFormatImpl; +import javax.imageio.metadata.IIOMetadataNode; +import java.lang.reflect.Array; +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.*; + +/** + * TIFFImageMetadata. + * + * @author Harald Kuhr + * @author last modified by $Author: harald.kuhr$ + * @version $Id: TIFFImageMetadata.java,v 1.0 17/04/15 harald.kuhr Exp$ + */ +public final class TIFFImageMetadata extends AbstractMetadata { + + static final int RATIONAL_SCALE_FACTOR = 100000; + + private final Directory original; + private Directory ifd; + + /** + * Creates an empty TIFF metadata object. + * + * Client code can update or change the metadata using the + * {@link #setFromTree(String, Node)} + * or {@link #mergeTree(String, Node)} methods. + */ + public TIFFImageMetadata() { + this(new TIFFIFD(Collections.emptyList())); + } + + /** + * Creates a TIFF metadata object, using the values from the given IFD. + * + * Client code can update or change the metadata using the + * {@link #setFromTree(String, Node)} + * or {@link #mergeTree(String, Node)} methods. + */ + public TIFFImageMetadata(final Directory ifd) { + super(true, TIFFMedataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME, TIFFMedataFormat.class.getName(), null, null); + this.ifd = Validate.notNull(ifd, "IFD"); + this.original = ifd; + } + + /** + * Creates a TIFF metadata object, using the values from the given entries. + * + * Client code can update or change the metadata using the + * {@link #setFromTree(String, Node)} + * or {@link #mergeTree(String, Node)} methods. + */ + public TIFFImageMetadata(final Collection entries) { + this(new TIFFIFD(entries)); + } + + protected IIOMetadataNode getNativeTree() { + IIOMetadataNode root = new IIOMetadataNode(nativeMetadataFormatName); + root.appendChild(asTree(ifd)); + + return root; + } + + private IIOMetadataNode asTree(final Directory ifd) { + IIOMetadataNode ifdNode = new IIOMetadataNode("TIFFIFD"); + + for (Entry tag : ifd) { + IIOMetadataNode tagNode; + Object value = tag.getValue(); + + if (value instanceof Directory) { + // TODO: Don't expand non-TIFF IFDs... + tagNode = asTree((Directory) value); + tagNode.setAttribute("parentTagNumber", String.valueOf(tag.getIdentifier())); + String fieldName = tag.getFieldName(); + if (fieldName != null) { + tagNode.setAttribute("parentTagName", fieldName); + } + + // TODO: tagSets is REQUIRED! + } + else { + tagNode = new IIOMetadataNode("TIFFField"); + tagNode.setAttribute("number", String.valueOf(tag.getIdentifier())); + + String fieldName = tag.getFieldName(); + if (fieldName != null) { + tagNode.setAttribute("name", fieldName); + } + + int count = tag.valueCount(); + + if (TIFF.TYPE_NAMES[TIFF.TYPE_UNDEFINED].equals(tag.getTypeName())) { + // Why does "undefined" need special handling?! It's just a byte array.. :-P + // Or maybe rather, why isn't all types implemented like this..? + // TODO: Consider handling IPTC, Photoshop/Adobe, XMP and ICC Profile as Undefined always + // (even if older software wrote as Byte), as it's more compact? + IIOMetadataNode valueNode = new IIOMetadataNode("TIFFUndefined"); + tagNode.appendChild(valueNode); + + if (count == 1) { + valueNode.setAttribute("value", String.valueOf(value)); + } + else { + valueNode.setAttribute("value", Arrays.toString((byte[]) value).replaceAll("\\[?\\]?", "")); + } + } + else { + String arrayTypeName = getMetadataArrayType(tag); + IIOMetadataNode valueNode = new IIOMetadataNode(arrayTypeName); + tagNode.appendChild(valueNode); + + boolean unsigned = !isSignedType(tag); + String typeName = getMetadataType(tag); + + // NOTE: ASCII/Strings have count 1, always. This seems consistent with the JAI ImageIO version. + if (count == 1) { + IIOMetadataNode elementNode = new IIOMetadataNode(typeName); + valueNode.appendChild(elementNode); + + setTIFFNativeValue(value, unsigned, elementNode); + } + else { + for (int i = 0; i < count; i++) { + Object val = Array.get(value, i); + IIOMetadataNode elementNode = new IIOMetadataNode(typeName); + valueNode.appendChild(elementNode); + + setTIFFNativeValue(val, unsigned, elementNode); + } + } + } + } + + ifdNode.appendChild(tagNode); + } + + return ifdNode; + } + + private void setTIFFNativeValue(final Object value, final boolean unsigned, final IIOMetadataNode elementNode) { + if (unsigned && value instanceof Byte) { + elementNode.setAttribute("value", String.valueOf((Byte) value & 0xFF)); + } + else if (unsigned && value instanceof Short) { + elementNode.setAttribute("value", String.valueOf((Short) value & 0xFFFF)); + } + else if (unsigned && value instanceof Integer) { + elementNode.setAttribute("value", String.valueOf((Integer) value & 0xFFFFFFFFl)); + } + else { + elementNode.setAttribute("value", String.valueOf(value)); + } + } + + private boolean isSignedType(final Entry tag) { + String typeName = tag.getTypeName(); + + // Stupid special cases implementation, until we can access the type id... + if ("SBYTE".equals(typeName)) { + return true; + } + if ("SSHORT".equals(typeName)) { + return true; + } + if ("SLONG".equals(typeName)) { + return true; + } + if ("SRATIONAL".equals(typeName)) { + return true; + } + if ("FLOAT".equals(typeName)) { + return true; + } + if ("DOUBLE".equals(typeName)) { + return true; + } + if ("SLONG8".equals(typeName)) { + return true; + } + // IFD8 not used + + return false; + } + + private String getMetadataArrayType(final Entry tag) { + String typeName = tag.getTypeName(); + + // Stupid special cases implementation, until we can access the type id... + if ("BYTE".equals(typeName)) { + return "TIFFBytes"; + } + if ("ASCII".equals(typeName)) { + return "TIFFAsciis"; + } + if ("SHORT".equals(typeName)) { + return "TIFFShorts"; + } + if ("LONG".equals(typeName)) { + return "TIFFLongs"; + } + if ("RATIONAL".equals(typeName)) { + return "TIFFRationals"; + } + // UNDEFINED not used... + if ("SBYTE".equals(typeName)) { + return "TIFFSBytes"; + } + if ("SSHORT".equals(typeName)) { + return "TIFFSShorts"; + } + if ("SLONG".equals(typeName)) { + return "TIFFSLongs"; + } + if ("SRATIONAL".equals(typeName)) { + return "TIFFSRationals"; + } + if ("FLOAT".equals(typeName)) { + return "TIFFFloats"; + } + if ("DOUBLE".equals(typeName)) { + return "TIFFDoubles"; + } + // IFD not used + if ("LONG8".equals(typeName)) { + return "TIFFLong8s"; + } + if ("SLONG8".equals(typeName)) { + return "TIFFSLong8s"; + } + // IFD8 not used + + throw new IllegalArgumentException(typeName); + } + + private String getMetadataType(final Entry tag) { + String typeName = tag.getTypeName(); + + // Stupid special cases implementation, until we can access the type id... + if ("BYTE".equals(typeName)) { + return "TIFFByte"; + } + if ("ASCII".equals(typeName)) { + return "TIFFAscii"; + } + if ("SHORT".equals(typeName)) { + return "TIFFShort"; + } + if ("LONG".equals(typeName)) { + return "TIFFLong"; + } + if ("RATIONAL".equals(typeName)) { + return "TIFFRational"; + } + // UNDEFINED not used... + if ("SBYTE".equals(typeName)) { + return "TIFFSByte"; + } + if ("SSHORT".equals(typeName)) { + return "TIFFSShort"; + } + if ("SLONG".equals(typeName)) { + return "TIFFSLong"; + } + if ("SRATIONAL".equals(typeName)) { + return "TIFFSRational"; + } + if ("FLOAT".equals(typeName)) { + return "TIFFFloat"; + } + if ("DOUBLE".equals(typeName)) { + return "TIFFDouble"; + } + // IFD not used + if ("LONG8".equals(typeName)) { + return "TIFFLong8"; + } + if ("SLONG8".equals(typeName)) { + return "TIFFSLong8"; + } + // IFD8 not used + + throw new IllegalArgumentException(typeName); + } + + // TODO: Candidate superclass method! + private IIOMetadataNode addChildNode(final IIOMetadataNode parent, + final String name, + final Object object) { + IIOMetadataNode child = new IIOMetadataNode(name); + + if (object != null) { + child.setUserObject(object); // TODO: Should we always store user object?!?! + child.setNodeValue(object.toString()); // TODO: Fix this line + } + + parent.appendChild(child); + + return child; + } + + /// Standard metadata + // See: http://download.java.net/media/jai-imageio/javadoc/1.1/com/sun/media/imageio/plugins/tiff/package-summary.html + + @Override + protected IIOMetadataNode getStandardChromaNode() { + IIOMetadataNode chroma = new IIOMetadataNode("Chroma"); + + // Handle ColorSpaceType (RGB/CMYK/YCbCr etc)... + Entry photometricTag = ifd.getEntryById(TIFF.TAG_PHOTOMETRIC_INTERPRETATION); + int photometricValue = getValueAsInt(photometricTag); // No default for this tag! + int numChannelsValue = getSamplesPerPixelWithFallback(); + + IIOMetadataNode colorSpaceType = new IIOMetadataNode("ColorSpaceType"); + chroma.appendChild(colorSpaceType); + switch (photometricValue) { + case TIFFBaseline.PHOTOMETRIC_WHITE_IS_ZERO: + case TIFFBaseline.PHOTOMETRIC_BLACK_IS_ZERO: + case TIFFBaseline.PHOTOMETRIC_MASK: // It's really a transparency mask/alpha channel, but... + colorSpaceType.setAttribute("value", "GRAY"); + break; + case TIFFBaseline.PHOTOMETRIC_RGB: + case TIFFBaseline.PHOTOMETRIC_PALETTE: + colorSpaceType.setAttribute("value", "RGB"); + break; + case TIFFExtension.PHOTOMETRIC_YCBCR: + colorSpaceType.setAttribute("value", "YCbCr"); + break; + case TIFFExtension.PHOTOMETRIC_CIELAB: + case TIFFExtension.PHOTOMETRIC_ICCLAB: + case TIFFExtension.PHOTOMETRIC_ITULAB: + colorSpaceType.setAttribute("value", "Lab"); + break; + case TIFFExtension.PHOTOMETRIC_SEPARATED: + // TODO: May be CMYK, or something else... Consult InkSet and NumberOfInks! + if (numChannelsValue == 3) { + colorSpaceType.setAttribute("value", "CMY"); + } + else { + colorSpaceType.setAttribute("value", "CMYK"); + } + break; + case TIFFCustom.PHOTOMETRIC_LOGL: // ..? + case TIFFCustom.PHOTOMETRIC_LOGLUV: + colorSpaceType.setAttribute("value", "Luv"); + break; + case TIFFCustom.PHOTOMETRIC_CFA: + case TIFFCustom.PHOTOMETRIC_LINEAR_RAW: // ...or is this RGB? + colorSpaceType.setAttribute("value", "3CLR"); + break; + default: + colorSpaceType.setAttribute("value", Integer.toHexString(numChannelsValue) + "CLR"); + break; + } + + // NumChannels + IIOMetadataNode numChannels = new IIOMetadataNode("NumChannels"); + chroma.appendChild(numChannels); + if (photometricValue == TIFFBaseline.PHOTOMETRIC_PALETTE) { + numChannels.setAttribute("value", "3"); + } + else { + numChannels.setAttribute("value", Integer.toString(numChannelsValue)); + } + + // BlackIsZero (defaults to TRUE) + IIOMetadataNode blackIsZero = new IIOMetadataNode("BlackIsZero"); + chroma.appendChild(blackIsZero); + switch (photometricValue) { + case TIFFBaseline.PHOTOMETRIC_WHITE_IS_ZERO: + blackIsZero.setAttribute("value", "FALSE"); + break; + default: + break; + } + + Entry colorMapTag = ifd.getEntryById(TIFF.TAG_COLOR_MAP); + + if (colorMapTag != null) { + int[] colorMapValues = (int[]) colorMapTag.getValue(); + + IIOMetadataNode palette = new IIOMetadataNode("Palette"); + chroma.appendChild(palette); + + int count = colorMapValues.length / 3; + for (int i = 0; i < count; i++) { + IIOMetadataNode paletteEntry = new IIOMetadataNode("PaletteEntry"); + paletteEntry.setAttribute("index", Integer.toString(i)); + + // TODO: See TIFFImageReader createIndexColorModel, to detect 8 bit colorMap + paletteEntry.setAttribute("red", Integer.toString((colorMapValues[i] >> 8) & 0xff)); + paletteEntry.setAttribute("green", Integer.toString((colorMapValues[i + count] >> 8) & 0xff)); + paletteEntry.setAttribute("blue", Integer.toString((colorMapValues[i + count * 2] >> 8) & 0xff)); + + palette.appendChild(paletteEntry); + } + } + + return chroma; + } + + private int getSamplesPerPixelWithFallback() { + // SamplePerPixel defaults to 1, but we'll check BitsPerSample to be sure + Entry samplesPerPixelTag = ifd.getEntryById(TIFF.TAG_SAMPLES_PER_PIXEL); + Entry bitsPerSampleTag = ifd.getEntryById(TIFF.TAG_BITS_PER_SAMPLE); + + return samplesPerPixelTag != null + ? getValueAsInt(samplesPerPixelTag) + : bitsPerSampleTag != null ? bitsPerSampleTag.valueCount() : 1; + } + + @Override + protected IIOMetadataNode getStandardCompressionNode() { + IIOMetadataNode compression = new IIOMetadataNode("Compression"); + IIOMetadataNode compressionTypeName = addChildNode(compression, "CompressionTypeName", null); + + Entry compressionTag = ifd.getEntryById(TIFF.TAG_COMPRESSION); + int compressionValue = compressionTag == null + ? TIFFBaseline.COMPRESSION_NONE + : getValueAsInt(compressionTag); + + // Naming is identical to JAI ImageIO metadata as far as possible + switch (compressionValue) { + case TIFFBaseline.COMPRESSION_NONE: + compressionTypeName.setAttribute("value", "None"); + break; + case TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE: + compressionTypeName.setAttribute("value", "CCITT RLE"); + break; + case TIFFExtension.COMPRESSION_CCITT_T4: + compressionTypeName.setAttribute("value", "CCITT T4"); + break; + case TIFFExtension.COMPRESSION_CCITT_T6: + compressionTypeName.setAttribute("value", "CCITT T6"); + break; + case TIFFExtension.COMPRESSION_LZW: + compressionTypeName.setAttribute("value", "LZW"); + break; + case TIFFExtension.COMPRESSION_OLD_JPEG: + compressionTypeName.setAttribute("value", "Old JPEG"); + break; + case TIFFExtension.COMPRESSION_JPEG: + compressionTypeName.setAttribute("value", "JPEG"); + break; + case TIFFExtension.COMPRESSION_ZLIB: + compressionTypeName.setAttribute("value", "ZLib"); + break; + case TIFFExtension.COMPRESSION_DEFLATE: + compressionTypeName.setAttribute("value", "Deflate"); + break; + case TIFFBaseline.COMPRESSION_PACKBITS: + compressionTypeName.setAttribute("value", "PackBits"); + break; + case TIFFCustom.COMPRESSION_CCITTRLEW: + compressionTypeName.setAttribute("value", "CCITT RLEW"); + break; + case TIFFCustom.COMPRESSION_DCS: + compressionTypeName.setAttribute("value", "DCS"); + break; + case TIFFCustom.COMPRESSION_IT8BL: + compressionTypeName.setAttribute("value", "IT8BL"); + break; + case TIFFCustom.COMPRESSION_IT8CTPAD: + compressionTypeName.setAttribute("value", "IT8CTPAD"); + break; + case TIFFCustom.COMPRESSION_IT8LW: + compressionTypeName.setAttribute("value", "IT8LW"); + break; + case TIFFCustom.COMPRESSION_IT8MP: + compressionTypeName.setAttribute("value", "IT8MP"); + break; + case TIFFCustom.COMPRESSION_JBIG: + compressionTypeName.setAttribute("value", "JBIG"); + break; + case TIFFCustom.COMPRESSION_JPEG2000: + compressionTypeName.setAttribute("value", "JPEG 2000"); + break; + case TIFFCustom.COMPRESSION_NEXT: + compressionTypeName.setAttribute("value", "NEXT"); + break; + case TIFFCustom.COMPRESSION_PIXARFILM: + compressionTypeName.setAttribute("value", "Pixar Film"); + break; + case TIFFCustom.COMPRESSION_PIXARLOG: + compressionTypeName.setAttribute("value", "Pixar Log"); + break; + case TIFFCustom.COMPRESSION_SGILOG: + compressionTypeName.setAttribute("value", "SGI Log"); + break; + case TIFFCustom.COMPRESSION_SGILOG24: + compressionTypeName.setAttribute("value", "SGI Log24"); + break; + case TIFFCustom.COMPRESSION_THUNDERSCAN: + compressionTypeName.setAttribute("value", "ThunderScan"); + break; + default: + compressionTypeName.setAttribute("value", "Unknown " + compressionValue); + break; + } + + if (compressionValue != TIFFBaseline.COMPRESSION_NONE) { + // Lossless (defaults to TRUE) + IIOMetadataNode lossless = new IIOMetadataNode("Lossless"); + compression.appendChild(lossless); + + switch (compressionValue) { + case TIFFExtension.COMPRESSION_OLD_JPEG: + case TIFFExtension.COMPRESSION_JPEG: + case TIFFCustom.COMPRESSION_JBIG: + case TIFFCustom.COMPRESSION_JPEG2000: + lossless.setAttribute("value", "FALSE"); + break; + default: + break; + } + } + + return compression; + } + + @Override + protected IIOMetadataNode getStandardDataNode() { + IIOMetadataNode node = new IIOMetadataNode("Data"); + + IIOMetadataNode planarConfiguration = new IIOMetadataNode("PlanarConfiguration"); + Entry planarConfigurationTag = ifd.getEntryById(TIFF.TAG_PLANAR_CONFIGURATION); + int planarConfigurationValue = planarConfigurationTag == null + ? TIFFBaseline.PLANARCONFIG_CHUNKY + : getValueAsInt(planarConfigurationTag); + + switch (planarConfigurationValue) { + case TIFFBaseline.PLANARCONFIG_CHUNKY: + planarConfiguration.setAttribute("value", "PixelInterleaved"); + break; + case TIFFExtension.PLANARCONFIG_PLANAR: + planarConfiguration.setAttribute("value", "PlaneInterleaved"); + break; + default: + planarConfiguration.setAttribute("value", "Unknown " + planarConfigurationValue); + } + node.appendChild(planarConfiguration); + + Entry photometricInterpretationTag = ifd.getEntryById(TIFF.TAG_PHOTOMETRIC_INTERPRETATION); + int photometricInterpretationValue = photometricInterpretationTag == null + ? TIFFBaseline.PHOTOMETRIC_WHITE_IS_ZERO + : getValueAsInt(photometricInterpretationTag); + + Entry samleFormatTag = ifd.getEntryById(TIFF.TAG_SAMPLE_FORMAT); + // TODO: Fix for sampleformat 1 1 1 (as int[]) ??!?!? + int sampleFormatValue = samleFormatTag == null + ? TIFFBaseline.SAMPLEFORMAT_UINT + : getValueAsInt(samleFormatTag); + IIOMetadataNode sampleFormat = new IIOMetadataNode("SampleFormat"); + node.appendChild(sampleFormat); + + switch (sampleFormatValue) { + case TIFFBaseline.SAMPLEFORMAT_UINT: + if (photometricInterpretationValue == TIFFBaseline.PHOTOMETRIC_PALETTE) { + sampleFormat.setAttribute("value", "Index"); + } + else { + sampleFormat.setAttribute("value", "UnsignedIntegral"); + } + break; + case TIFFExtension.SAMPLEFORMAT_INT: + sampleFormat.setAttribute("value", "SignedIntegral"); + break; + case TIFFExtension.SAMPLEFORMAT_FP: + sampleFormat.setAttribute("value", "Real"); + break; + default: + sampleFormat.setAttribute("value", "Unknown " + sampleFormatValue); + break; + } + + // TODO: See TIFFImageReader.getBitsPerSample + fix the metadata to have getAsXxxArray methods. + // BitsPerSample (not required field for Class B/Bilevel, defaults to 1) + Entry bitsPerSampleTag = ifd.getEntryById(TIFF.TAG_BITS_PER_SAMPLE); + String bitsPerSampleValue = bitsPerSampleTag == null + ? "1" + : bitsPerSampleTag.getValueAsString().replaceAll("\\[?\\]?,?", ""); + + IIOMetadataNode bitsPerSample = new IIOMetadataNode("BitsPerSample"); + node.appendChild(bitsPerSample); + bitsPerSample.setAttribute("value", bitsPerSampleValue); + + int numChannelsValue = getSamplesPerPixelWithFallback(); + + // SampleMSB + Entry fillOrderTag = ifd.getEntryById(TIFF.TAG_FILL_ORDER); + int fillOrder = fillOrderTag != null + ? getValueAsInt(fillOrderTag) + : TIFFBaseline.FILL_LEFT_TO_RIGHT; + IIOMetadataNode sampleMSB = new IIOMetadataNode("SampleMSB"); + node.appendChild(sampleMSB); + if (fillOrder == TIFFBaseline.FILL_LEFT_TO_RIGHT) { + sampleMSB.setAttribute("value", createListValue(numChannelsValue, "0")); + } + else { + if ("1".equals(bitsPerSampleValue)) { + sampleMSB.setAttribute("value", createListValue(numChannelsValue, "7")); + } + else { + // TODO: FixMe for bitsPerSample > 8 + sampleMSB.setAttribute("value", createListValue(numChannelsValue, "7")); + } + } + + return node; + } + + private static int getValueAsInt(final Entry entry) { + Object value = entry.getValue(); + + if (value instanceof Number) { + return ((Number) value).intValue(); + } + else if (value instanceof short[]) { + return ((short[]) value)[0]; + } + else if (value instanceof int[]) { + return ((int[]) value)[0]; + } + + throw new IllegalArgumentException("Unsupported type: " + entry); + } + + // TODO: Candidate superclass method! + private String createListValue(final int itemCount, final String... values) { + StringBuilder buffer = new StringBuilder(); + + for (int i = 0; i < itemCount; i++) { + if (buffer.length() > 0) { + buffer.append(' '); + } + + buffer.append(values[i % values.length]); + } + + return buffer.toString(); + } + + @Override + protected IIOMetadataNode getStandardDimensionNode() { + IIOMetadataNode dimension = new IIOMetadataNode("Dimension"); + + // PixelAspectRatio + Entry xResTag = ifd.getEntryById(TIFF.TAG_X_RESOLUTION); + Entry yResTag = ifd.getEntryById(TIFF.TAG_Y_RESOLUTION); + double xSizeValue = 1 / (xResTag == null ? 72.0 : ((Number) xResTag.getValue()).doubleValue()); + double ySizeValue = 1 / (xResTag == null ? 72.0 : ((Number) yResTag.getValue()).doubleValue()); + + IIOMetadataNode pixelAspectRatio = new IIOMetadataNode("PixelAspectRatio"); + dimension.appendChild(pixelAspectRatio); + pixelAspectRatio.setAttribute("value", String.valueOf(xSizeValue / ySizeValue)); + + // ImageOrientation + Entry orientationTag = ifd.getEntryById(TIFF.TAG_ORIENTATION); + if (orientationTag != null) { + int orientationValue = getValueAsInt(orientationTag); + + String value = null; + switch (orientationValue) { + case TIFFBaseline.ORIENTATION_TOPLEFT: + value = "Normal"; + break; + case TIFFExtension.ORIENTATION_TOPRIGHT: + value = "FlipH"; + break; + case TIFFExtension.ORIENTATION_BOTRIGHT: + value = "Rotate180"; + break; + case TIFFExtension.ORIENTATION_BOTLEFT: + value = "FlipV"; + break; + case TIFFExtension.ORIENTATION_LEFTTOP: + value = "FlipHRotate90"; + break; + case TIFFExtension.ORIENTATION_RIGHTTOP: + value = "Rotate270"; + break; + case TIFFExtension.ORIENTATION_RIGHTBOT: + value = "FlipVRotate90"; + break; + case TIFFExtension.ORIENTATION_LEFTBOT: + value = "Rotate90"; + break; + } + + if (value != null) { + IIOMetadataNode imageOrientation = new IIOMetadataNode("ImageOrientation"); + dimension.appendChild(imageOrientation); + imageOrientation.setAttribute("value", value); + } + + } + + Entry resUnitTag = ifd.getEntryById(TIFF.TAG_RESOLUTION_UNIT); + int resUnitValue = resUnitTag == null ? TIFFBaseline.RESOLUTION_UNIT_DPI : getValueAsInt(resUnitTag); + if (resUnitValue == TIFFBaseline.RESOLUTION_UNIT_CENTIMETER || resUnitValue == TIFFBaseline.RESOLUTION_UNIT_DPI) { + // 10 mm in 1 cm or 25.4 mm in 1 inch + double scale = resUnitValue == TIFFBaseline.RESOLUTION_UNIT_CENTIMETER ? 10 : 25.4; + + // HorizontalPixelSize + // VerticalPixelSize + IIOMetadataNode horizontalPixelSize = new IIOMetadataNode("HorizontalPixelSize"); + dimension.appendChild(horizontalPixelSize); + horizontalPixelSize.setAttribute("value", String.valueOf(xSizeValue * scale)); + + IIOMetadataNode verticalPixelSize = new IIOMetadataNode("VerticalPixelSize"); + dimension.appendChild(verticalPixelSize); + verticalPixelSize.setAttribute("value", String.valueOf(ySizeValue * scale)); + + // HorizontalPosition + // VerticalPosition + Entry xPosTag = ifd.getEntryById(TIFF.TAG_X_POSITION); + Entry yPosTag = ifd.getEntryById(TIFF.TAG_Y_POSITION); + + if (xPosTag != null && yPosTag != null) { + double xPosValue = ((Number) xPosTag.getValue()).doubleValue(); + double yPosValue = ((Number) yPosTag.getValue()).doubleValue(); + + IIOMetadataNode horizontalPosition = new IIOMetadataNode("HorizontalPosition"); + dimension.appendChild(horizontalPosition); + horizontalPosition.setAttribute("value", String.valueOf(xPosValue * scale)); + + IIOMetadataNode verticalPosition = new IIOMetadataNode("VerticalPosition"); + dimension.appendChild(verticalPosition); + verticalPosition.setAttribute("value", String.valueOf(yPosValue * scale)); + } + } + + return dimension; + } + + @Override + protected IIOMetadataNode getStandardTransparencyNode() { + // Consult ExtraSamples + Entry extraSamplesTag = ifd.getEntryById(TIFF.TAG_EXTRA_SAMPLES); + + if (extraSamplesTag != null) { + int extraSamplesValue = (extraSamplesTag.getValue() instanceof Number) + ? getValueAsInt(extraSamplesTag) + : ((Number) Array.get(extraSamplesTag.getValue(), 0)).intValue(); + + // Other values exists, these are not alpha + if (extraSamplesValue == TIFFBaseline.EXTRASAMPLE_ASSOCIATED_ALPHA || extraSamplesValue == TIFFBaseline.EXTRASAMPLE_UNASSOCIATED_ALPHA) { + IIOMetadataNode transparency = new IIOMetadataNode("Transparency"); + IIOMetadataNode alpha = new IIOMetadataNode("Alpha"); + transparency.appendChild(alpha); + + alpha.setAttribute("value", extraSamplesValue == TIFFBaseline.EXTRASAMPLE_ASSOCIATED_ALPHA + ? "premultiplied" + : "nonpremultiplied"); + + return transparency; + } + } + + return null; + + } + + @Override + protected IIOMetadataNode getStandardDocumentNode() { + IIOMetadataNode document = new IIOMetadataNode("Document"); + + // FormatVersion, hardcoded to 6.0 (the current TIFF specification version), + // as there's no format information in the TIFF structure. + IIOMetadataNode formatVersion = new IIOMetadataNode("FormatVersion"); + document.appendChild(formatVersion); + formatVersion.setAttribute("value", "6.0"); + + // SubImageInterpretation from SubImageInterpretation (if applicable) + Entry subFileTypeTag = ifd.getEntryById(TIFF.TAG_SUBFILE_TYPE); + if (subFileTypeTag != null) { + // NOTE: The JAI metadata is somewhat broken here, as these are bit flags, not values... + String value = null; + int subFileTypeValue = getValueAsInt(subFileTypeTag); + if ((subFileTypeValue & TIFFBaseline.FILETYPE_MASK) != 0) { + value = "TransparencyMask"; + } + else if ((subFileTypeValue & TIFFBaseline.FILETYPE_REDUCEDIMAGE) != 0) { + value = "ReducedResolution"; + } + else if ((subFileTypeValue & TIFFBaseline.FILETYPE_PAGE) != 0) { + value = "SinglePage"; + } + + // If no flag is set, we don't know... + if (value != null) { + IIOMetadataNode subImageInterpretation = new IIOMetadataNode("SubImageInterpretation"); + document.appendChild(subImageInterpretation); + subImageInterpretation.setAttribute("value", value); + } + } + + // ImageCreationTime from DateTime + Entry dateTimeTag = ifd.getEntryById(TIFF.TAG_DATE_TIME); + if (dateTimeTag != null) { + DateFormat format = new SimpleDateFormat("yyyy:MM:dd hh:mm:ss"); + + try { + IIOMetadataNode imageCreationTime = new IIOMetadataNode("ImageCreationTime"); + document.appendChild(imageCreationTime); + + Calendar date = Calendar.getInstance(); + date.setTime(format.parse(dateTimeTag.getValueAsString())); + + imageCreationTime.setAttribute("year", String.valueOf(date.get(Calendar.YEAR))); + imageCreationTime.setAttribute("month", String.valueOf(date.get(Calendar.MONTH) + 1)); + imageCreationTime.setAttribute("day", String.valueOf(date.get(Calendar.DAY_OF_MONTH))); + imageCreationTime.setAttribute("hour", String.valueOf(date.get(Calendar.HOUR_OF_DAY))); + imageCreationTime.setAttribute("minute", String.valueOf(date.get(Calendar.MINUTE))); + imageCreationTime.setAttribute("second", String.valueOf(date.get(Calendar.SECOND))); + } + catch (ParseException ignore) { + // Bad format... + } + } + + return document; + } + + @Override + protected IIOMetadataNode getStandardTextNode() { + IIOMetadataNode text = new IIOMetadataNode("Text"); + + // DocumentName, ImageDescription, Make, Model, PageName, Software, Artist, HostComputer, InkNames, Copyright: + // /Text/TextEntry@keyword = field name, /Text/TextEntry@value = field value. + addTextEntryIfPresent(text, TIFF.TAG_DOCUMENT_NAME); + addTextEntryIfPresent(text, TIFF.TAG_IMAGE_DESCRIPTION); + addTextEntryIfPresent(text, TIFF.TAG_MAKE); + addTextEntryIfPresent(text, TIFF.TAG_MODEL); + addTextEntryIfPresent(text, TIFF.TAG_PAGE_NAME); + addTextEntryIfPresent(text, TIFF.TAG_SOFTWARE); + addTextEntryIfPresent(text, TIFF.TAG_ARTIST); + addTextEntryIfPresent(text, TIFF.TAG_HOST_COMPUTER); + addTextEntryIfPresent(text, TIFF.TAG_INK_NAMES); + addTextEntryIfPresent(text, TIFF.TAG_COPYRIGHT); + + return text.hasChildNodes() ? text : null; + } + + private void addTextEntryIfPresent(final IIOMetadataNode text, final int tag) { + Entry entry = ifd.getEntryById(tag); + if (entry != null) { + IIOMetadataNode node = new IIOMetadataNode("TextEntry"); + text.appendChild(node); + node.setAttribute("keyword", entry.getFieldName()); + node.setAttribute("value", entry.getValueAsString()); + } + } + + @Override + protected IIOMetadataNode getStandardTileNode() { + // TODO! Woot?! This node is not documented in the DTD (although the page mentions a "tile" node)..? + // See http://docs.oracle.com/javase/7/docs/api/javax/imageio/metadata/doc-files/standard_metadata.html + // See http://stackoverflow.com/questions/30910719/javax-imageio-1-0-standard-plug-in-neutral-metadata-format-tiling-information + return super.getStandardTileNode(); + } + + /// Mutation + + @Override + public boolean isReadOnly() { + return false; + } + + public void setFromTree(final String formatName, final Node root) throws IIOInvalidTreeException { + // Standard validation + super.mergeTree(formatName, root); + + // Set by "merging" with empty map + LinkedHashMap entries = new LinkedHashMap<>(); + mergeEntries(formatName, root, entries); + + // TODO: Consistency validation? + + // Finally create a new IFD from merged values + ifd = new TIFFIFD(entries.values()); + } + + @Override + public void mergeTree(final String formatName, final Node root) throws IIOInvalidTreeException { + // Standard validation + super.mergeTree(formatName, root); + + // Clone entries (shallow clone, as entries themselves are immutable) + LinkedHashMap entries = new LinkedHashMap<>(ifd.size() + 10); + + for (Entry entry : ifd) { + entries.put((Integer) entry.getIdentifier(), entry); + } + + mergeEntries(formatName, root, entries); + + // TODO: Consistency validation? + + // Finally create a new IFD from merged values + ifd = new TIFFIFD(entries.values()); + } + + private void mergeEntries(final String formatName, final Node root, final Map entries) throws IIOInvalidTreeException { + // Merge from both native and standard trees + if (getNativeMetadataFormatName().equals(formatName)) { + mergeNativeTree(root, entries); + } + else if (IIOMetadataFormatImpl.standardMetadataFormatName.equals(formatName)) { + mergeStandardTree(root, entries); + } + else { + // Should already be checked for + throw new AssertionError(); + } + } + + private void mergeStandardTree(final Node root, final Map entries) throws IIOInvalidTreeException { + NodeList nodes = root.getChildNodes(); + + // Merge selected values from standard tree + for (int i = 0; i < nodes.getLength(); i++) { + Node node = nodes.item(i); + + if ("Dimension".equals(node.getNodeName())) { + mergeFromStandardDimensionNode(node, entries); + } + else if ("Document".equals(node.getNodeName())) { + mergeFromStandardDocumentNode(node, entries); + } + else if ("Text".equals(node.getNodeName())) { + mergeFromStandardTextNode(node, entries); + } + } + } + + private void mergeFromStandardDimensionNode(final Node dimensionNode, final Map entries) { + // Dimension: xRes/yRes + // - If set, set res unit to pixels per cm as this better reflects values? + // - Or, convert to DPI, if we already had values in DPI?? + // Also, if we have only aspect, set these values, and use unknown as unit? + // TODO: ImageOrientation => Orientation + NodeList children = dimensionNode.getChildNodes(); + + Float aspect = null; + Float xRes = null; + Float yRes = null; + + for (int i = 0; i < children.getLength(); i++) { + Node child = children.item(i); + String nodeName = child.getNodeName(); + + if ("PixelAspectRatio".equals(nodeName)) { + aspect = Float.parseFloat(getAttribute(child, "value")); + } + else if ("HorizontalPixelSize".equals(nodeName)) { + xRes = Float.parseFloat(getAttribute(child, "value")); + } + else if ("VerticalPixelSize".equals(nodeName)) { + yRes = Float.parseFloat(getAttribute(child, "value")); + } + } + + // If we have one size compute the other + if (xRes == null && yRes != null) { + xRes = yRes * (aspect != null ? aspect : 1f); + } + else if (yRes == null && xRes != null) { + yRes = xRes / (aspect != null ? aspect : 1f); + } + + // If we have resolution + if (xRes != null && yRes != null) { + // If old unit was DPI, convert values and keep DPI, otherwise use PPCM + Entry resUnitEntry = entries.get(TIFF.TAG_RESOLUTION_UNIT); + int resUnitValue = resUnitEntry != null && resUnitEntry.getValue() != null + && ((Number) resUnitEntry.getValue()).intValue() == TIFFBaseline.RESOLUTION_UNIT_DPI + ? TIFFBaseline.RESOLUTION_UNIT_DPI + : TIFFBaseline.RESOLUTION_UNIT_CENTIMETER; + + // Units from standard format are pixels per mm, convert to cm or inches + float scale = resUnitValue == TIFFBaseline.RESOLUTION_UNIT_CENTIMETER ? 10 : 25.4f; + + int x = Math.round(xRes * scale * RATIONAL_SCALE_FACTOR); + int y = Math.round(yRes * scale * RATIONAL_SCALE_FACTOR); + + entries.put(TIFF.TAG_X_RESOLUTION, new TIFFImageWriter.TIFFEntry(TIFF.TAG_X_RESOLUTION, new Rational(x, RATIONAL_SCALE_FACTOR))); + entries.put(TIFF.TAG_Y_RESOLUTION, new TIFFImageWriter.TIFFEntry(TIFF.TAG_Y_RESOLUTION, new Rational(y, RATIONAL_SCALE_FACTOR))); + entries.put(TIFF.TAG_RESOLUTION_UNIT, + new TIFFImageWriter.TIFFEntry(TIFF.TAG_RESOLUTION_UNIT, TIFF.TYPE_SHORT, resUnitValue)); + } + else if (aspect != null) { + if (aspect >= 1) { + int v = Math.round(aspect * RATIONAL_SCALE_FACTOR); + entries.put(TIFF.TAG_X_RESOLUTION, new TIFFImageWriter.TIFFEntry(TIFF.TAG_X_RESOLUTION, new Rational(v, RATIONAL_SCALE_FACTOR))); + entries.put(TIFF.TAG_Y_RESOLUTION, new TIFFImageWriter.TIFFEntry(TIFF.TAG_Y_RESOLUTION, new Rational(1))); + } + else { + int v = Math.round(RATIONAL_SCALE_FACTOR / aspect); + entries.put(TIFF.TAG_X_RESOLUTION, new TIFFImageWriter.TIFFEntry(TIFF.TAG_X_RESOLUTION, new Rational(1))); + entries.put(TIFF.TAG_Y_RESOLUTION, new TIFFImageWriter.TIFFEntry(TIFF.TAG_Y_RESOLUTION, new Rational(v, RATIONAL_SCALE_FACTOR))); + } + + entries.put(TIFF.TAG_RESOLUTION_UNIT, + new TIFFImageWriter.TIFFEntry(TIFF.TAG_RESOLUTION_UNIT, TIFF.TYPE_SHORT, TIFFBaseline.RESOLUTION_UNIT_NONE)); + } + // Else give up... + } + + private void mergeFromStandardDocumentNode(final Node documentNode, final Map entries) { + // Document: SubfileType, CreationDate + NodeList children = documentNode.getChildNodes(); + + for (int i = 0; i < children.getLength(); i++) { + Node child = children.item(i); + String nodeName = child.getNodeName(); + + if ("SubimageInterpretation".equals(nodeName)) { + // TODO: SubFileType + } + else if ("ImageCreationTime".equals(nodeName)) { + // TODO: CreationDate + } + } + } + + private void mergeFromStandardTextNode(final Node textNode, final Map entries) throws IIOInvalidTreeException { + NodeList textEntries = textNode.getChildNodes(); + + for (int i = 0; i < textEntries.getLength(); i++) { + Node textEntry = textEntries.item(i); + + if (!"TextEntry".equals(textEntry.getNodeName())) { + throw new IIOInvalidTreeException("Text node should only contain TextEntry nodes", textNode); + } + + String keyword = getAttribute(textEntry, "keyword"); + String value = getAttribute(textEntry, "value"); + + // DocumentName, ImageDescription, Make, Model, PageName, + // Software, Artist, HostComputer, InkNames, Copyright + if (value != null && !value.isEmpty() && keyword != null) { + // We do all comparisons in lower case, for compatibility + keyword = keyword.toLowerCase(); + + TIFFImageWriter.TIFFEntry entry; + + if ("documentname".equals(keyword)) { + entry = new TIFFImageWriter.TIFFEntry(TIFF.TAG_DOCUMENT_NAME, TIFF.TYPE_ASCII, value); + } + else if ("imagedescription".equals(keyword)) { + entry = new TIFFImageWriter.TIFFEntry(TIFF.TAG_IMAGE_DESCRIPTION, TIFF.TYPE_ASCII, value); + } + else if ("make".equals(keyword)) { + entry = new TIFFImageWriter.TIFFEntry(TIFF.TAG_MAKE, TIFF.TYPE_ASCII, value); + } + else if ("model".equals(keyword)) { + entry = new TIFFImageWriter.TIFFEntry(TIFF.TAG_MODEL, TIFF.TYPE_ASCII, value); + } + else if ("pagename".equals(keyword)) { + entry = new TIFFImageWriter.TIFFEntry(TIFF.TAG_PAGE_NAME, TIFF.TYPE_ASCII, value); + } + else if ("software".equals(keyword)) { + entry = new TIFFImageWriter.TIFFEntry(TIFF.TAG_SOFTWARE, TIFF.TYPE_ASCII, value); + } + else if ("artist".equals(keyword)) { + entry = new TIFFImageWriter.TIFFEntry(TIFF.TAG_ARTIST, TIFF.TYPE_ASCII, value); + } + else if ("hostcomputer".equals(keyword)) { + entry = new TIFFImageWriter.TIFFEntry(TIFF.TAG_HOST_COMPUTER, TIFF.TYPE_ASCII, value); + } + else if ("inknames".equals(keyword)) { + entry = new TIFFImageWriter.TIFFEntry(TIFF.TAG_INK_NAMES, TIFF.TYPE_ASCII, value); + } + else if ("copyright".equals(keyword)) { + entry = new TIFFImageWriter.TIFFEntry(TIFF.TAG_COPYRIGHT, TIFF.TYPE_ASCII, value); + } + else { + continue; + } + + entries.put((Integer) entry.getIdentifier(), entry); + } + } + } + + private void mergeNativeTree(final Node root, final Map entries) throws IIOInvalidTreeException { + Directory ifd = toIFD(root.getFirstChild()); + + // Merge (overwrite) entries with entries from IFD + for (Entry entry : ifd) { + entries.put((Integer) entry.getIdentifier(), entry); + } + } + + private Directory toIFD(final Node ifdNode) throws IIOInvalidTreeException { + if (ifdNode == null || !ifdNode.getNodeName().equals("TIFFIFD")) { + throw new IIOInvalidTreeException("Expected \"TIFFIFD\" node", ifdNode); + } + + List entries = new ArrayList<>(); + NodeList nodes = ifdNode.getChildNodes(); + + for (int i = 0; i < nodes.getLength(); i++) { + entries.add(toEntry(nodes.item(i))); + } + + return new TIFFIFD(entries); + } + + private Entry toEntry(final Node node) throws IIOInvalidTreeException { + String name = node.getNodeName(); + + if (name.equals("TIFFIFD")) { + int tag = Integer.parseInt(getAttribute(node, "parentTagNumber")); + Directory subIFD = toIFD(node); + + return new TIFFImageWriter.TIFFEntry(tag, TIFF.TYPE_IFD, subIFD); + } + else if (name.equals("TIFFField")) { + int tag = Integer.parseInt(getAttribute(node, "number")); + short type = getTIFFType(node); + Object value = getValue(node, type); + + return value != null ? new TIFFImageWriter.TIFFEntry(tag, type, value) : null; + } + else { + throw new IIOInvalidTreeException("Expected \"TIFFIFD\" or \"TIFFField\" node: " + name, node); + } + } + + private short getTIFFType(final Node node) throws IIOInvalidTreeException { + Node containerNode = node.getFirstChild(); + if (containerNode == null) { + throw new IIOInvalidTreeException("Missing value wrapper node", node); + } + + String nodeName = containerNode.getNodeName(); + if (!nodeName.startsWith("TIFF")) { + throw new IIOInvalidTreeException("Unexpected value wrapper node, expected type", containerNode); + } + + String typeName = nodeName.substring(4); + + if (typeName.equals("Undefined")) { + return TIFF.TYPE_UNDEFINED; + } + + typeName = typeName.substring(0, typeName.length() - 1).toUpperCase(); + + for (int i = 1; i < TIFF.TYPE_NAMES.length; i++) { + if (typeName.equals(TIFF.TYPE_NAMES[i])) { + return (short) i; + } + } + + throw new IIOInvalidTreeException("Unknown TIFF type: " + typeName, containerNode); + } + + private Object getValue(final Node node, final short type) throws IIOInvalidTreeException { + Node child = node.getFirstChild(); + + if (child != null) { + String typeName = child.getNodeName(); + + if (type == TIFF.TYPE_UNDEFINED) { + String values = getAttribute(child, "value"); + String[] vals = values.split(",\\s?"); + + byte[] bytes = new byte[vals.length]; + for (int i = 0; i < vals.length; i++) { + bytes[i] = Byte.parseByte(vals[i]); + } + + return bytes; + } + else { + NodeList valueNodes = child.getChildNodes(); + + // Create array for each type + int count = valueNodes.getLength(); + Object value = createArrayForType(type, count); + + // Parse each value + for (int i = 0; i < count; i++) { + Node valueNode = valueNodes.item(i); + + if (!typeName.startsWith(valueNode.getNodeName())) { + throw new IIOInvalidTreeException("Value node does not match container node", child); + } + + String stringValue = getAttribute(valueNode, "value"); + + // NOTE: The reason for parsing "wider" type, is to allow for unsigned values + switch (type) { + case TIFF.TYPE_BYTE: + case TIFF.TYPE_SBYTE: + ((byte[]) value)[i] = (byte) Short.parseShort(stringValue); + break; + case TIFF.TYPE_ASCII: + ((String[]) value)[i] = stringValue; + break; + case TIFF.TYPE_SHORT: + case TIFF.TYPE_SSHORT: + ((short[]) value)[i] = (short) Integer.parseInt(stringValue); + break; + case TIFF.TYPE_LONG: + case TIFF.TYPE_SLONG: + ((int[]) value)[i] = (int) Long.parseLong(stringValue); + break; + case TIFF.TYPE_RATIONAL: + case TIFF.TYPE_SRATIONAL: + String[] numDenom = stringValue.split("/"); + ((Rational[]) value)[i] = numDenom.length > 1 + ? new Rational(Long.parseLong(numDenom[0]), Long.parseLong(numDenom[1])) + : new Rational(Long.parseLong(numDenom[0])); + break; + case TIFF.TYPE_FLOAT: + ((float[]) value)[i] = Float.parseFloat(stringValue); + break; + case TIFF.TYPE_DOUBLE: + ((double[]) value)[i] = Double.parseDouble(stringValue); + break; + default: + throw new AssertionError("Unsupported TIFF type: " + type); + } + } + + // Normalize value + if (count == 0) { + return null; + } + if (count == 1) { + return Array.get(value, 0); + } + + return value; + } + } + + throw new IIOInvalidTreeException("Empty TIFField node", node); + } + + private Object createArrayForType(final short type, final int length) { + switch (type) { + case TIFF.TYPE_ASCII: + return new String[length]; + case TIFF.TYPE_BYTE: + case TIFF.TYPE_SBYTE: + case TIFF.TYPE_UNDEFINED: // Not used here, but for completeness + return new byte[length]; + case TIFF.TYPE_SHORT: + case TIFF.TYPE_SSHORT: + return new short[length]; + case TIFF.TYPE_LONG: + case TIFF.TYPE_SLONG: + return new int[length]; + case TIFF.TYPE_IFD: + return new long[length]; + case TIFF.TYPE_RATIONAL: + case TIFF.TYPE_SRATIONAL: + return new Rational[length]; + case TIFF.TYPE_FLOAT: + return new float[length]; + case TIFF.TYPE_DOUBLE: + return new double[length]; + default: + throw new AssertionError("Unsupported TIFF type: " + type); + } + } + + private String getAttribute(final Node node, final String attribute) { + return node instanceof Element ? ((Element) node).getAttribute(attribute) : null; + } + + @Override + public void reset() { + super.reset(); + + ifd = original; + } + + Directory getIFD() { + return ifd; + } + + /** + * Returns an Entry which contains the data of the requested TIFF field. + * + * @param tagNumber Tag number of the TIFF field. + * + * @return the TIFF field, or null. + */ + public Entry getTIFFField(final int tagNumber) { + return ifd.getEntryById(tagNumber); + } + + // TODO: Replace with IFD class when moved to new package and made public! + private final static class TIFFIFD extends AbstractDirectory { + public TIFFIFD(final Collection entries) { + super(entries); + } + } +} diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReader.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReader.java index 685878b6..790b70e5 100755 --- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReader.java +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReader.java @@ -28,16 +28,21 @@ package com.twelvemonkeys.imageio.plugins.tiff; -import com.sun.imageio.plugins.jpeg.JPEGImageReader; import com.twelvemonkeys.imageio.ImageReaderBase; +import com.twelvemonkeys.imageio.color.CIELabColorConverter; +import com.twelvemonkeys.imageio.color.CIELabColorConverter.Illuminant; import com.twelvemonkeys.imageio.color.ColorSpaces; +import com.twelvemonkeys.imageio.color.YCbCrConverter; import com.twelvemonkeys.imageio.metadata.CompoundDirectory; import com.twelvemonkeys.imageio.metadata.Directory; import com.twelvemonkeys.imageio.metadata.Entry; import com.twelvemonkeys.imageio.metadata.exif.EXIFReader; import com.twelvemonkeys.imageio.metadata.exif.Rational; import com.twelvemonkeys.imageio.metadata.exif.TIFF; +import com.twelvemonkeys.imageio.metadata.iptc.IPTCReader; import com.twelvemonkeys.imageio.metadata.jpeg.JPEG; +import com.twelvemonkeys.imageio.metadata.psd.PSDReader; +import com.twelvemonkeys.imageio.metadata.xmp.XMPReader; import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream; import com.twelvemonkeys.imageio.stream.SubImageInputStream; import com.twelvemonkeys.imageio.util.IIOUtil; @@ -47,7 +52,6 @@ import com.twelvemonkeys.io.FastByteArrayOutputStream; import com.twelvemonkeys.io.LittleEndianDataInputStream; import com.twelvemonkeys.io.enc.DecoderStream; import com.twelvemonkeys.io.enc.PackBitsDecoder; -import com.twelvemonkeys.xml.XMLSerializer; import javax.imageio.*; import javax.imageio.event.IIOReadWarningListener; @@ -58,11 +62,16 @@ import javax.imageio.spi.ImageReaderSpi; import javax.imageio.spi.ServiceRegistry; import javax.imageio.stream.ImageInputStream; import java.awt.*; +import java.awt.color.CMMException; import java.awt.color.ColorSpace; import java.awt.color.ICC_Profile; import java.awt.image.*; import java.io.*; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.nio.ByteBuffer; import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; import java.util.*; import java.util.zip.Inflater; import java.util.zip.InflaterInputStream; @@ -80,15 +89,16 @@ import java.util.zip.InflaterInputStream; * In addition, it supports many common TIFF extensions such as: *

    *
  • Tiling
  • + *
  • Class F (Facsimile), CCITT T.4 and T.6 compression (types 3 and 4), 1 bit per sample
  • *
  • LZW Compression (type 5)
  • *
  • "Old-style" JPEG Compression (type 6), as a best effort, as the spec is not well-defined
  • *
  • JPEG Compression (type 7)
  • *
  • ZLib (aka Adobe-style Deflate) Compression (type 8)
  • *
  • Deflate Compression (type 32946)
  • *
  • Horizontal differencing Predictor (type 2) for LZW, ZLib, Deflate and PackBits compression
  • - *
  • Alpha channel (ExtraSamples type 1/Associated Alpha)
  • - *
  • CMYK data (PhotometricInterpretation type 5/Separated)
  • - *
  • YCbCr data (PhotometricInterpretation type 6/YCbCr) for JPEG
  • + *
  • Alpha channel (ExtraSamples types 1/Associated Alpha and 2/Unassociated Alpha)
  • + *
  • Class S, CMYK data (PhotometricInterpretation type 5/Separated)
  • + *
  • Class Y, YCbCr data (PhotometricInterpretation type 6/YCbCr for both JPEG and other compressions
  • *
  • Planar data (PlanarConfiguration type 2/Planar)
  • *
  • ICC profiles (ICCProfile)
  • *
  • BitsPerSample values up to 16 for most PhotometricInterpretations
  • @@ -106,7 +116,6 @@ import java.util.zip.InflaterInputStream; public class TIFFImageReader extends ImageReaderBase { // TODOs ImageIO basic functionality: // TODO: Thumbnail support - // TODO: TIFFImageWriter + Spi // TODOs Full BaseLine support: // TODO: Support ExtraSamples (an array, if multiple extra samples!) @@ -116,27 +125,30 @@ public class TIFFImageReader extends ImageReaderBase { // TODO: Tiling support (readTile, readTileRaster) // TODO: Implement readAsRenderedImage to allow tiled RenderedImage? // For some layouts, we could do reads super-fast with a memory mapped buffer. - // TODO: Implement readAsRaster directly - // TODO: IIOMetadata (stay close to Sun's TIFF metadata) + // TODO: Implement readAsRaster directly (100% correctly) // http://download.java.net/media/jai-imageio/javadoc/1.1/com/sun/media/imageio/plugins/tiff/package-summary.html#ImageMetadata // TODOs Extension support - // TODO: Support PlanarConfiguration 2, look at PCXImageReader // TODO: Auto-rotate based on Orientation - // TODO: Support ICCProfile (fully) - // TODO: Support Compression 3 & 4 (CCITT T.4 & T.6) // TODO: Support Compression 34712 (JPEG2000)? Depends on JPEG2000 ImageReader // TODO: Support Compression 34661 (JBIG)? Depends on JBIG ImageReader // DONE: - // Handle SampleFormat (and give up if not == 1) + // Handle SampleFormat // Support Compression 6 ('Old-style' JPEG) // Support Compression 2 (CCITT Modified Huffman RLE) for bi-level images // Source region // Subsampling + // IIOMetadata (stay close to Sun's TIFF metadata) + // Support ICCProfile + // Support PlanarConfiguration 2 + // Support Compression 3 & 4 (CCITT T.4 & T.6) final static boolean DEBUG = "true".equalsIgnoreCase(System.getProperty("com.twelvemonkeys.imageio.plugins.tiff.debug")); + // NOTE: DO NOT MODIFY OR EXPOSE THIS ARRAY OUTSIDE PACKAGE! + static final double[] CCIR_601_1_COEFFICIENTS = new double[] {299.0 / 1000.0, 587.0 / 1000.0, 114.0 / 1000.0}; + private CompoundDirectory IFDs; private Directory currentIFD; @@ -165,6 +177,73 @@ public class TIFFImageReader extends ImageReaderBase { for (int i = 0; i < IFDs.directoryCount(); i++) { System.err.printf("IFD %d: %s\n", i, IFDs.getDirectory(i)); } + + Entry tiffXMP = IFDs.getEntryById(TIFF.TAG_XMP); + if (tiffXMP != null) { + byte[] value = (byte[]) tiffXMP.getValue(); + + // The XMPReader doesn't like null-termination... + int len = value.length; + for (int i = len - 1; i > 0; i--) { + if (value[i] == 0) { + len--; + } + else { + break; + } + } + + Directory xmp = new XMPReader().read(new ByteArrayImageInputStream(value, 0, len)); + System.err.println("-----------------------------------------------------------------------------"); + System.err.println("xmp: " + xmp); + } + + Entry tiffIPTC = IFDs.getEntryById(TIFF.TAG_IPTC); + if (tiffIPTC != null) { + Object value = tiffIPTC.getValue(); + if (value instanceof short[]) { + System.err.println("short[]: " + value); + } + if (value instanceof long[]) { + // As seen in a Magick produced image... + System.err.println("long[]: " + value); + long[] longs = (long[]) value; + value = new byte[longs.length * 8]; + ByteBuffer.wrap((byte[]) value).asLongBuffer().put(longs); + } + if (value instanceof float[]) { + System.err.println("float[]: " + value); + } + if (value instanceof double[]) { + System.err.println("double[]: " + value); + } + + Directory iptc = new IPTCReader().read(new ByteArrayImageInputStream((byte[]) value)); + System.err.println("-----------------------------------------------------------------------------"); + System.err.println("iptc: " + iptc); + } + + Entry tiffPSD = IFDs.getEntryById(TIFF.TAG_PHOTOSHOP); + if (tiffPSD != null) { + Directory psd = new PSDReader().read(new ByteArrayImageInputStream((byte[]) tiffPSD.getValue())); + System.err.println("-----------------------------------------------------------------------------"); + System.err.println("psd: " + psd); + } + Entry tiffPSD2 = IFDs.getEntryById(TIFF.TAG_PHOTOSHOP_IMAGE_SOURCE_DATA); + if (tiffPSD2 != null) { + byte[] value = (byte[]) tiffPSD2.getValue(); + String foo = "Adobe Photoshop Document Data Block"; + + if (Arrays.equals(foo.getBytes(StandardCharsets.US_ASCII), Arrays.copyOf(value, foo.length()))) { + System.err.println("foo: " + foo); +// int offset = foo.length() + 1; +// ImageInputStream input = new ByteArrayImageInputStream(value, offset, value.length - offset); +// input.setByteOrder(ByteOrder.LITTLE_ENDIAN); // TODO: WHY???! +// Directory psd2 = new PSDReader().read(input); +// System.err.println("-----------------------------------------------------------------------------"); +// System.err.println("psd2: " + psd2); + } + } } } } @@ -235,12 +314,29 @@ public class TIFFImageReader extends ImageReaderBase { readIFD(imageIndex); int sampleFormat = getSampleFormat(); - int planarConfiguration = getValueAsIntWithDefault(TIFF.TAG_PLANAR_CONFIGURATION, TIFFExtension.PLANARCONFIG_PLANAR); - int interpretation = getValueAsInt(TIFF.TAG_PHOTOMETRIC_INTERPRETATION, "PhotometricInterpretation"); + int planarConfiguration = getValueAsIntWithDefault(TIFF.TAG_PLANAR_CONFIGURATION, TIFFBaseline.PLANARCONFIG_CHUNKY); + int interpretation = getPhotometricInterpretationWithFallback(); int samplesPerPixel = getValueAsIntWithDefault(TIFF.TAG_SAMPLES_PER_PIXEL, 1); int bitsPerSample = getBitsPerSample(); int dataType = getDataType(sampleFormat, bitsPerSample); + int opaqueSamplesPerPixel = getOpaqueSamplesPerPixel(interpretation); + + // Spec says ExtraSamples are mandatory of extra samples, however known encoders + // (ie. SeaShore) writes ARGB TIFFs without ExtraSamples. + long[] extraSamples = getValueAsLongArray(TIFF.TAG_EXTRA_SAMPLES, "ExtraSamples", false); + if (extraSamples == null && samplesPerPixel > opaqueSamplesPerPixel) { + // TODO: Log warning! + // First extra is alpha, rest is "unspecified" + extraSamples = new long[samplesPerPixel - opaqueSamplesPerPixel]; + extraSamples[0] = TIFFBaseline.EXTRASAMPLE_UNASSOCIATED_ALPHA; + } + + // Determine alpha + boolean hasAlpha = extraSamples != null; + boolean isAlphaPremultiplied = hasAlpha && extraSamples[0] == TIFFBaseline.EXTRASAMPLE_ASSOCIATED_ALPHA; + int significantSamples = opaqueSamplesPerPixel + (hasAlpha ? 1 : 0); + // Read embedded cs ICC_Profile profile = getICCProfile(); ColorSpace cs; @@ -250,14 +346,14 @@ public class TIFFImageReader extends ImageReaderBase { case TIFFBaseline.PHOTOMETRIC_WHITE_IS_ZERO: // WhiteIsZero // NOTE: We handle this by inverting the values when reading, as Java has no ColorModel that easily supports this. - // TODO: Consider returning null? case TIFFBaseline.PHOTOMETRIC_BLACK_IS_ZERO: // BlackIsZero // Gray scale or B/W - switch (samplesPerPixel) { + switch (significantSamples) { case 1: // TIFF 6.0 Spec says: 1, 4 or 8 for baseline (1 for bi-level, 4/8 for gray) - // ImageTypeSpecifier supports 1, 2, 4, 8 or 16 bits, we'll go with that for now + // ImageTypeSpecifier supports 1, 2, 4, 8 or 16 bits per sample, we'll support 32 bits as well. + // (Chunky or planar makes no difference for a single channel). if (profile != null && profile.getColorSpaceType() != ColorSpace.TYPE_GRAY) { processWarningOccurred(String.format("Embedded ICC color profile (type %s), is incompatible with image data (GRAY/type 6). Ignoring profile.", profile.getColorSpaceType())); profile = null; @@ -268,13 +364,51 @@ public class TIFFImageReader extends ImageReaderBase { if (cs == ColorSpace.getInstance(ColorSpace.CS_GRAY) && (bitsPerSample == 1 || bitsPerSample == 2 || bitsPerSample == 4 || bitsPerSample == 8 || bitsPerSample == 16 || bitsPerSample == 32)) { return ImageTypeSpecifiers.createGrayscale(bitsPerSample, dataType); } - else if (bitsPerSample == 1 || bitsPerSample == 2 || bitsPerSample == 4 || bitsPerSample == 8 || bitsPerSample == 16 || bitsPerSample == 32) { + else if (bitsPerSample == 1 || bitsPerSample == 2 || bitsPerSample == 4 ) { + // Use packed format for 1/2/4 bits + return ImageTypeSpecifiers.createPackedGrayscale(cs, bitsPerSample, dataType); + } + else if (bitsPerSample == 8 || bitsPerSample == 16 || bitsPerSample == 32) { return ImageTypeSpecifiers.createInterleaved(cs, new int[] {0}, dataType, false, false); } - default: - // TODO: If ExtraSamples is used, PlanarConfiguration must be taken into account also for gray data - throw new IIOException(String.format("Unsupported SamplesPerPixel/BitsPerSample combination for Bi-level/Gray TIFF (expected 1/1, 1/2, 1/4, 1/8 or 1/16): %d/%d", samplesPerPixel, bitsPerSample)); + throw new IIOException(String.format("Unsupported BitsPerSample for Bi-level/Gray TIFF (expected 1, 2, 4, 8, 16 or 32): %d", bitsPerSample)); + + case 2: + // Gray + alpha. We'll support: + // * 8, 16 or 32 bits per sample + // * Associated (pre-multiplied) or unassociated (non-pre-multiplied) alpha + // * Chunky (interleaved) or planar (banded) data + if (profile != null && profile.getColorSpaceType() != ColorSpace.TYPE_GRAY) { + processWarningOccurred(String.format("Embedded ICC color profile (type %s), is incompatible with image data (GRAY/type 6). Ignoring profile.", profile.getColorSpaceType())); + profile = null; + } + + cs = profile == null ? ColorSpace.getInstance(ColorSpace.CS_GRAY) : ColorSpaces.createColorSpace(profile); + + if (cs == ColorSpace.getInstance(ColorSpace.CS_GRAY) && (bitsPerSample == 8 || bitsPerSample == 16 || bitsPerSample == 32)) { + switch (planarConfiguration) { + case TIFFBaseline.PLANARCONFIG_CHUNKY: + return ImageTypeSpecifiers.createGrayscale(bitsPerSample, dataType, isAlphaPremultiplied); + case TIFFExtension.PLANARCONFIG_PLANAR: + return ImageTypeSpecifiers.createBanded(cs, new int[] {0, 1}, new int[] {0, 0}, dataType, true, isAlphaPremultiplied); + } + } + else if (/*bitsPerSample == 1 || bitsPerSample == 2 || bitsPerSample == 4 ||*/ bitsPerSample == 8 || bitsPerSample == 16 || bitsPerSample == 32) { + // TODO: Should use packed format for 1/2/4 chunky. + // TODO: For 1/2/4 bit planar, we might need to fix while reading... Look at IFFImageReader? + switch (planarConfiguration) { + case TIFFBaseline.PLANARCONFIG_CHUNKY: + return ImageTypeSpecifiers.createInterleaved(cs, new int[] {0, 1}, dataType, true, isAlphaPremultiplied); + case TIFFExtension.PLANARCONFIG_PLANAR: + return ImageTypeSpecifiers.createBanded(cs, new int[] {0, 1}, new int[] {0, 0}, dataType, true, isAlphaPremultiplied); + } + } + + throw new IIOException(String.format("Unsupported BitsPerSample for Gray + Alpha TIFF (expected 8, 16 or 32): %d", bitsPerSample)); + + default: + throw new IIOException(String.format("Unsupported SamplesPerPixel/BitsPerSample combination for Bi-level/Gray TIFF (expected 1/1, 1/2, 1/4, 1/8, 1/16 or 1/32, or 2/8, 2/16 or 2/32): %d/%d", samplesPerPixel, bitsPerSample)); } case TIFFExtension.PHOTOMETRIC_YCBCR: @@ -289,15 +423,12 @@ public class TIFFImageReader extends ImageReaderBase { cs = profile == null ? ColorSpace.getInstance(ColorSpace.CS_sRGB) : ColorSpaces.createColorSpace(profile); - switch (samplesPerPixel) { + switch (significantSamples) { case 3: if (bitsPerSample == 8 || bitsPerSample == 16 || bitsPerSample == 32) { switch (planarConfiguration) { case TIFFBaseline.PLANARCONFIG_CHUNKY: - if (bitsPerSample == 8 && cs.isCS_sRGB()) { - return ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_3BYTE_BGR); - } - + // "TYPE_3_BYTE_RGB" if cs.isCS_sRGB() return ImageTypeSpecifiers.createInterleaved(cs, new int[] {0, 1, 2}, dataType, false, false); case TIFFExtension.PLANARCONFIG_PLANAR: @@ -306,22 +437,18 @@ public class TIFFImageReader extends ImageReaderBase { } case 4: if (bitsPerSample == 8 || bitsPerSample == 16 || bitsPerSample == 32) { - // ExtraSamples 0=unspecified, 1=associated (pre-multiplied), 2=unassociated (TODO: Support unspecified, not alpha) - long[] extraSamples = getValueAsLongArray(TIFF.TAG_EXTRA_SAMPLES, "ExtraSamples", true); - switch (planarConfiguration) { case TIFFBaseline.PLANARCONFIG_CHUNKY: - if (bitsPerSample == 8 && cs.isCS_sRGB()) { - return ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_4BYTE_ABGR); - } - - return ImageTypeSpecifiers.createInterleaved(cs, new int[]{ 0, 1, 2, 3}, dataType, true, extraSamples[0] == 1); + // "TYPE_4_BYTE_RGBA" if cs.isCS_sRGB() + return ImageTypeSpecifiers.createInterleaved(cs, new int[] {0, 1, 2, 3}, dataType, true, isAlphaPremultiplied); case TIFFExtension.PLANARCONFIG_PLANAR: - return ImageTypeSpecifiers.createBanded(cs, new int[] {0, 1, 2, 3}, new int[] {0, 0, 0, 0}, dataType, true, extraSamples[0] == 1); + return ImageTypeSpecifiers.createBanded(cs, new int[] {0, 1, 2, 3}, new int[] {0, 0, 0, 0}, dataType, true, isAlphaPremultiplied); } } - // TODO: More samples might be ok, if multiple alpha or unknown samples + else if (bitsPerSample == 4) { + return ImageTypeSpecifiers.createPacked(cs, 0xF000, 0xF00, 0xF0, 0xF, DataBuffer.TYPE_USHORT, isAlphaPremultiplied); + } default: throw new IIOException(String.format("Unsupported SamplesPerPixels/BitsPerSample combination for RGB TIFF (expected 3/8, 4/8, 3/16 or 4/16): %d/%d", samplesPerPixel, bitsPerSample)); } @@ -333,8 +460,8 @@ public class TIFFImageReader extends ImageReaderBase { else if (bitsPerSample <= 0 || bitsPerSample > 16) { throw new IIOException("Bad BitsPerSample value for Palette TIFF (expected <= 16): " + bitsPerSample); } - // NOTE: If ExtraSamples is used, PlanarConfiguration must be taken into account also for pixel data + // NOTE: If ExtraSamples is used, PlanarConfiguration must be taken into account also for pixel data Entry colorMap = currentIFD.getEntryById(TIFF.TAG_COLOR_MAP); if (colorMap == null) { throw new IIOException("Missing ColorMap for Palette TIFF"); @@ -364,7 +491,7 @@ public class TIFFImageReader extends ImageReaderBase { cs = profile == null ? ColorSpaces.getColorSpace(ColorSpaces.CS_GENERIC_CMYK) : ColorSpaces.createColorSpace(profile); - switch (samplesPerPixel) { + switch (significantSamples) { case 4: if (bitsPerSample == 8 || bitsPerSample == 16) { switch (planarConfiguration) { @@ -376,46 +503,136 @@ public class TIFFImageReader extends ImageReaderBase { } case 5: if (bitsPerSample == 8 || bitsPerSample == 16) { - // ExtraSamples 0=unspecified, 1=associated (pre-multiplied), 2=unassociated (TODO: Support unspecified, not alpha) - long[] extraSamples = getValueAsLongArray(TIFF.TAG_EXTRA_SAMPLES, "ExtraSamples", true); - switch (planarConfiguration) { case TIFFBaseline.PLANARCONFIG_CHUNKY: - return ImageTypeSpecifiers.createInterleaved(cs, new int[] {0, 1, 2, 3, 4}, dataType, true, extraSamples[0] == 1); + return ImageTypeSpecifiers.createInterleaved(cs, new int[] {0, 1, 2, 3, 4}, dataType, true, isAlphaPremultiplied); case TIFFExtension.PLANARCONFIG_PLANAR: - return ImageTypeSpecifiers.createBanded(cs, new int[] {0, 1, 2, 3, 4}, new int[] {0, 0, 0, 0, 0}, dataType, true, extraSamples[0] == 1); + return ImageTypeSpecifiers.createBanded(cs, new int[] {0, 1, 2, 3, 4}, new int[] {0, 0, 0, 0, 0}, dataType, true, isAlphaPremultiplied); } } - // TODO: More samples might be ok, if multiple alpha or unknown samples, consult ExtraSamples - default: throw new IIOException( - String.format("Unsupported TIFF SamplesPerPixels/BitsPerSample combination for Separated TIFF (expected 4/8, 4/16, 5/8 or 5/16): %d/%s", samplesPerPixel, bitsPerSample) + String.format("Unsupported SamplesPerPixels/BitsPerSample combination for Separated TIFF (expected 4/8, 4/16, 5/8 or 5/16): %d/%s", samplesPerPixel, bitsPerSample) + ); + } + case TIFFExtension.PHOTOMETRIC_CIELAB: + case TIFFExtension.PHOTOMETRIC_ICCLAB: + case TIFFExtension.PHOTOMETRIC_ITULAB: + // TODO: Would probably be more correct to handle using a CIELabColorSpace for RAW type? + // L*a*b* color. Handled using conversion to sRGB + cs = ColorSpace.getInstance(ColorSpace.CS_sRGB); + switch (planarConfiguration) { + case TIFFBaseline.PLANARCONFIG_CHUNKY: + return ImageTypeSpecifiers.createInterleaved(cs, new int[] {0, 1, 2}, dataType, false, false); + case TIFFExtension.PLANARCONFIG_PLANAR: + // TODO: Reading works fine, but we can't convert the Lab values properly yet. Need to rewrite normalizeColor + //return ImageTypeSpecifiers.createBanded(cs, new int[] {0, 1, 2}, new int[] {0, 0, 0}, dataType, false, false); + default: + throw new IIOException( + String.format("Unsupported PlanarConfiguration for Lab color TIFF (expected 1): %d", planarConfiguration) ); } case TIFFBaseline.PHOTOMETRIC_MASK: // Transparency mask - + // TODO: Treat as grey? + case TIFFCustom.PHOTOMETRIC_LOGL: + case TIFFCustom.PHOTOMETRIC_LOGLUV: + // Log + case TIFFCustom.PHOTOMETRIC_CFA: + case TIFFCustom.PHOTOMETRIC_LINEAR_RAW: + // RAW (DNG) throw new IIOException("Unsupported TIFF PhotometricInterpretation value: " + interpretation); default: throw new IIOException("Unknown TIFF PhotometricInterpretation value: " + interpretation); } } + private int getPhotometricInterpretationWithFallback() throws IIOException { + // PhotometricInterpretation is a required TAG, but as it can be guessed this does a fallback that is equal to JAI ImageIO. + int interpretation = getValueAsIntWithDefault(TIFF.TAG_PHOTOMETRIC_INTERPRETATION, "PhotometricInterpretation", -1); + if (interpretation == -1) { + int compression = getValueAsIntWithDefault(TIFF.TAG_COMPRESSION, TIFFBaseline.COMPRESSION_NONE); + int samplesPerPixel = getValueAsIntWithDefault(TIFF.TAG_SAMPLES_PER_PIXEL, 1); + Entry extraSamplesEntry = currentIFD.getEntryById(TIFF.TAG_EXTRA_SAMPLES); + int extraSamples = extraSamplesEntry == null ? 0 : extraSamplesEntry.valueCount(); + + if (compression == TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE + || compression == TIFFExtension.COMPRESSION_CCITT_T4 + || compression == TIFFExtension.COMPRESSION_CCITT_T6) { + interpretation = TIFFBaseline.PHOTOMETRIC_WHITE_IS_ZERO; + } + else if (currentIFD.getEntryById(TIFF.TAG_COLOR_MAP) != null) { + interpretation = TIFFBaseline.PHOTOMETRIC_PALETTE; + } + else if ((samplesPerPixel - extraSamples) == 3) { + interpretation = TIFFBaseline.PHOTOMETRIC_RGB; + } + else if ((samplesPerPixel - extraSamples) == 4) { + interpretation = TIFFExtension.PHOTOMETRIC_SEPARATED; + } + else { + interpretation = TIFFBaseline.PHOTOMETRIC_BLACK_IS_ZERO; + } + processWarningOccurred("Missing PhotometricInterpretation, determining fallback: " + interpretation); + } + return interpretation; + } + + private int getOpaqueSamplesPerPixel(final int photometricInterpretation) throws IIOException { + switch (photometricInterpretation) { + case TIFFBaseline.PHOTOMETRIC_WHITE_IS_ZERO: + case TIFFBaseline.PHOTOMETRIC_BLACK_IS_ZERO: + case TIFFBaseline.PHOTOMETRIC_PALETTE: + case TIFFBaseline.PHOTOMETRIC_MASK: + return 1; + case TIFFBaseline.PHOTOMETRIC_RGB: + case TIFFExtension.PHOTOMETRIC_YCBCR: + case TIFFExtension.PHOTOMETRIC_CIELAB: + case TIFFExtension.PHOTOMETRIC_ICCLAB: + case TIFFExtension.PHOTOMETRIC_ITULAB: + return 3; + case TIFFExtension.PHOTOMETRIC_SEPARATED: + return getValueAsIntWithDefault(TIFF.TAG_NUMBER_OF_INKS, 4); + + case TIFFCustom.PHOTOMETRIC_LOGL: + case TIFFCustom.PHOTOMETRIC_LOGLUV: + case TIFFCustom.PHOTOMETRIC_CFA: + case TIFFCustom.PHOTOMETRIC_LINEAR_RAW: + throw new IIOException("Unsupported TIFF PhotometricInterpretation value: " + photometricInterpretation); + default: + throw new IIOException("Unknown TIFF PhotometricInterpretation value: " + photometricInterpretation); + } + } + private int getDataType(int sampleFormat, int bitsPerSample) throws IIOException { switch (sampleFormat) { + case TIFFExtension.SAMPLEFORMAT_UNDEFINED: + // Spec says: + // A field value of “undefined” is a statement by the writer that it did not know how + // to interpret the data samples; for example, if it were copying an existing image. A + // reader would typically treat an image with “undefined” data as if the field were + // not present (i.e. as unsigned integer data). case TIFFBaseline.SAMPLEFORMAT_UINT: return bitsPerSample <= 8 ? DataBuffer.TYPE_BYTE : bitsPerSample <= 16 ? DataBuffer.TYPE_USHORT : DataBuffer.TYPE_INT; case TIFFExtension.SAMPLEFORMAT_INT: - if (bitsPerSample == 16) { - return DataBuffer.TYPE_SHORT; + switch (bitsPerSample) { + case 8: + return DataBuffer.TYPE_BYTE; + case 16: + return DataBuffer.TYPE_SHORT; + case 32: + return DataBuffer.TYPE_INT; } - throw new IIOException("Unsupported BitPerSample for SampleFormat 2/Signed Integer (expected 16): " + bitsPerSample); + + throw new IIOException("Unsupported BitsPerSample for SampleFormat 2/Signed Integer (expected 8/16/32): " + bitsPerSample); + case TIFFExtension.SAMPLEFORMAT_FP: - throw new IIOException("Unsupported TIFF SampleFormat: (3/Floating point)"); - case TIFFExtension.SAMPLEFORMAT_UNDEFINED: - throw new IIOException("Unsupported TIFF SampleFormat (4/Undefined)"); + if (bitsPerSample == 32) { + return DataBuffer.TYPE_FLOAT; + } + + throw new IIOException("Unsupported BitsPerSample for SampleFormat 3/Floating Point (expected 32): " + bitsPerSample); default: throw new IIOException("Unknown TIFF SampleFormat (expected 1, 2, 3 or 4): " + sampleFormat); } @@ -486,9 +703,14 @@ public class TIFFImageReader extends ImageReaderBase { else { int bitsPerSample = (int) value[0]; - for (int i = 1; i < value.length; i++) { - if (value[i] != bitsPerSample) { - throw new IIOException("Variable BitsPerSample not supported: " + Arrays.toString(value)); + if (value.length == 3 && (value[0] == 5 && value[1] == 6 && value[2] == 5)) { + // Special case for UINT_565. We're good. + } + else { + for (int i = 1; i < value.length; i++) { + if (value[i] != bitsPerSample) { + throw new IIOException("Variable BitsPerSample not supported: " + Arrays.toString(value)); + } } } @@ -501,10 +723,9 @@ public class TIFFImageReader extends ImageReaderBase { readIFD(imageIndex); ImageTypeSpecifier rawType = getRawImageType(imageIndex); - Set specs = new LinkedHashSet(5); + Set specs = new LinkedHashSet<>(5); // TODO: Based on raw type, we can probably convert to most RGB types at least, maybe gray etc - // TODO: Planar to chunky by default if (rawType.getColorModel().getColorSpace().getType() == ColorSpace.TYPE_RGB) { if (rawType.getNumBands() == 3 && rawType.getBitsPerBand(0) == 8) { specs.add(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_3BYTE_BGR)); @@ -543,7 +764,7 @@ public class TIFFImageReader extends ImageReaderBase { WritableRaster destRaster = clipToRect(destination.getRaster(), dstRegion, param != null ? param.getDestinationBands() : null); - final int interpretation = getValueAsInt(TIFF.TAG_PHOTOMETRIC_INTERPRETATION, "PhotometricInterpretation"); + final int interpretation = getPhotometricInterpretationWithFallback(); final int compression = getValueAsIntWithDefault(TIFF.TAG_COMPRESSION, TIFFBaseline.COMPRESSION_NONE); final int predictor = getValueAsIntWithDefault(TIFF.TAG_PREDICTOR, 1); final int planarConfiguration = getValueAsIntWithDefault(TIFF.TAG_PLANAR_CONFIGURATION, TIFFBaseline.PLANARCONFIG_CHUNKY); @@ -582,7 +803,9 @@ public class TIFFImageReader extends ImageReaderBase { int tilesAcross = (width + stripTileWidth - 1) / stripTileWidth; int tilesDown = (height + stripTileHeight - 1) / stripTileHeight; - WritableRaster rowRaster = rawType.getColorModel().createCompatibleWritableRaster(stripTileWidth, 1); + // TODO: If extrasamples, we might need to create a raster with more samples... + WritableRaster rowRaster = rawType.createBufferedImage(stripTileWidth, 1).getRaster(); + Rectangle clip = new Rectangle(srcRegion); int row = 0; switch (compression) { @@ -600,14 +823,14 @@ public class TIFFImageReader extends ImageReaderBase { case TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE: // CCITT modified Huffman // Additionally, the specification defines these values as part of the TIFF extensions: -// case TIFFExtension.COMPRESSION_CCITT_T4: + case TIFFExtension.COMPRESSION_CCITT_T4: // CCITT Group 3 fax encoding -// case TIFFExtension.COMPRESSION_CCITT_T6: + case TIFFExtension.COMPRESSION_CCITT_T6: // CCITT Group 4 fax encoding int[] yCbCrSubsampling = null; int yCbCrPos = 1; - double[] yCbCrCoefficients = null; +// double[] yCbCrCoefficients = null; if (interpretation == TIFFExtension.PHOTOMETRIC_YCBCR) { // getRawImageType does the lookup/conversion for these if (rowRaster.getNumBands() != 3) { @@ -646,15 +869,15 @@ public class TIFFImageReader extends ImageReaderBase { yCbCrSubsampling = new int[] {2, 2}; } - Entry coefficients = currentIFD.getEntryById(TIFF.TAG_YCBCR_COEFFICIENTS); - if (coefficients != null) { - Rational[] value = (Rational[]) coefficients.getValue(); - yCbCrCoefficients = new double[] {value[0].doubleValue(), value[1].doubleValue(), value[2].doubleValue()}; - } - else { - // Default to y CCIR Recommendation 601-1 values - yCbCrCoefficients = YCbCrUpsamplerStream.CCIR_601_1_COEFFICIENTS; - } +// Entry coefficients = currentIFD.getEntryById(TIFF.TAG_YCBCR_COEFFICIENTS); +// if (coefficients != null) { +// Rational[] value = (Rational[]) coefficients.getValue(); +// yCbCrCoefficients = new double[] {value[0].doubleValue(), value[1].doubleValue(), value[2].doubleValue()}; +// } +// else { +// // Default to y CCIR Recommendation 601-1 values +// yCbCrCoefficients = YCbCrUpsamplerStream.CCIR_601_1_COEFFICIENTS; +// } } // Read data @@ -685,10 +908,10 @@ public class TIFFImageReader extends ImageReaderBase { adapter = createUnpredictorStream(predictor, stripTileWidth, numBands, getBitsPerSample(), adapter, imageInput.getByteOrder()); if (interpretation == TIFFExtension.PHOTOMETRIC_YCBCR && rowRaster.getTransferType() == DataBuffer.TYPE_BYTE) { - adapter = new YCbCrUpsamplerStream(adapter, yCbCrSubsampling, yCbCrPos, colsInTile, yCbCrCoefficients); + adapter = new YCbCrUpsamplerStream(adapter, yCbCrSubsampling, yCbCrPos, colsInTile); } else if (interpretation == TIFFExtension.PHOTOMETRIC_YCBCR && rowRaster.getTransferType() == DataBuffer.TYPE_USHORT) { - adapter = new YCbCr16UpsamplerStream(adapter, yCbCrSubsampling, yCbCrPos, colsInTile, yCbCrCoefficients, imageInput.getByteOrder()); + adapter = new YCbCr16UpsamplerStream(adapter, yCbCrSubsampling, yCbCrPos, colsInTile, imageInput.getByteOrder()); } else if (interpretation == TIFFExtension.PHOTOMETRIC_YCBCR) { // Handled in getRawImageType @@ -702,7 +925,6 @@ public class TIFFImageReader extends ImageReaderBase { } // Clip the stripTile rowRaster to not exceed the srcRegion - Rectangle clip = new Rectangle(srcRegion); clip.width = Math.min((colsInTile + xSub - 1) / xSub, srcRegion.width); Raster clippedRow = clipRowToRect(rowRaster, clip, param != null ? param.getSourceBands() : null, @@ -735,8 +957,7 @@ public class TIFFImageReader extends ImageReaderBase { // TODO: Refactor all JPEG reading out to separate JPEG support class? // TODO: Cache the JPEG reader for later use? Remember to reset to avoid resource leaks - // TIFF is strictly ISO JPEG, so we should probably stick to the standard reader - ImageReader jpegReader = new JPEGImageReader(getOriginatingProvider()); + ImageReader jpegReader = createJPEGDelegate(); JPEGImageReadParam jpegParam = (JPEGImageReadParam) jpegReader.getDefaultReadParam(); // JPEG_TABLES should be a full JPEG 'abbreviated table specification', containing: @@ -772,19 +993,25 @@ public class TIFFImageReader extends ImageReaderBase { // Read only tiles that lies within region if (new Rectangle(col, row, colsInTile, rowsInTile).intersects(srcRegion)) { imageInput.seek(stripTileOffsets[i]); - ImageInputStream subStream = new SubImageInputStream(imageInput, stripTileByteCounts != null ? (int) stripTileByteCounts[i] : Short.MAX_VALUE); - try { + int length = stripTileByteCounts != null ? (int) stripTileByteCounts[i] : Short.MAX_VALUE; + + try (ImageInputStream subStream = new SubImageInputStream(imageInput, length)) { jpegReader.setInput(subStream); jpegParam.setSourceRegion(new Rectangle(0, 0, colsInTile, rowsInTile)); - jpegParam.setDestinationOffset(new Point(col - srcRegion.x, row - srcRegion.y)); - jpegParam.setDestination(destination); - // TODO: This works only if Gray/YCbCr/RGB, not CMYK/LAB/etc... - // In the latter case we will have to use readAsRaster and do color conversion ourselves - jpegReader.read(0, jpegParam); - } - finally { - subStream.close(); + + if (interpretation == TIFFExtension.PHOTOMETRIC_YCBCR || interpretation == TIFFBaseline.PHOTOMETRIC_RGB) { + jpegParam.setDestinationOffset(new Point(col - srcRegion.x, row - srcRegion.y)); + jpegParam.setDestination(destination); + jpegReader.read(0, jpegParam); + } + else { + // Otherwise, it's likely CMYK or some other interpretation we don't need to convert. + // We'll have to use readAsRaster and later apply color space conversion ourselves + Raster raster = jpegReader.readRaster(0, jpegParam); + normalizeColor(interpretation, ((DataBufferByte) raster.getDataBuffer()).getData()); + destination.getRaster().setDataElements(col - srcRegion.x, row - srcRegion.y, raster); + } } } @@ -823,25 +1050,20 @@ public class TIFFImageReader extends ImageReaderBase { throw new IIOException("Unknown TIFF JPEGProcessingMode value: " + mode); } - // May use normal tiling?? - - // TIFF is strictly ISO JPEG, so we should probably stick to the standard reader - jpegReader = new JPEGImageReader(getOriginatingProvider()); + jpegReader = createJPEGDelegate(); jpegParam = (JPEGImageReadParam) jpegReader.getDefaultReadParam(); - // 513/JPEGInterchangeFormat (may be absent...) + // 513/JPEGInterchangeFormat (may be absent or 0) int jpegOffset = getValueAsIntWithDefault(TIFF.TAG_JPEG_INTERCHANGE_FORMAT, -1); - // 514/JPEGInterchangeFormatLength (may be absent...) - int jpegLenght = getValueAsIntWithDefault(TIFF.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH, -1); + // 514/JPEGInterchangeFormatLength (may be absent) + int jpegLength = getValueAsIntWithDefault(TIFF.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH, -1); // TODO: 515/JPEGRestartInterval (may be absent) // Currently ignored (for lossless only) // 517/JPEGLosslessPredictors // 518/JPEGPointTransforms - ImageInputStream stream; - - if (jpegOffset != -1) { + if (jpegOffset > 0) { // Straight forward case: We're good to go! We'll disregard tiling and any tables tags if (currentIFD.getEntryById(TIFF.TAG_OLD_JPEG_Q_TABLES) != null || currentIFD.getEntryById(TIFF.TAG_OLD_JPEG_DC_TABLES) != null @@ -878,23 +1100,49 @@ public class TIFFImageReader extends ImageReaderBase { } } - imageInput.seek(realJPEGOffset); - - stream = new SubImageInputStream(imageInput, jpegLenght != -1 ? jpegLenght : Short.MAX_VALUE); - jpegReader.setInput(stream); + // Determine correct JPEG stream length + int length; + if (jpegLength == -1) { + // If have no length, we'll just try to decode, as long as we can + length = Integer.MAX_VALUE; + processWarningOccurred("Missing JPEGInterchangeFormatLength tag"); + } + else if (stripTileOffsets != null && stripTileOffsets.length == 1 && stripTileOffsets[0] >= jpegOffset + jpegLength) { + // NOTE: Some known TIFF encoder writes obviously bogus JPEGInterchangeFormatLength value, + // but the real stream length can be determined from the StripByteCounts (may include padding). + if (stripTileByteCounts != null && stripTileByteCounts.length == 1 && stripTileByteCounts[0] > jpegLength) { + length = (int) (jpegLength + stripTileByteCounts[0]); + processWarningOccurred("Incorrect JPEGInterchangeFormatLength tag encountered, using StripByteCounts instead"); + } + else { + // No StripByteCounts, we'll just try tro decode as much as we can + length = Integer.MAX_VALUE; + processWarningOccurred("Incorrect JPEGInterchangeFormatLength tag encountered, ignoring tag value"); + } + } + else { + // Ok! We'll go with JPEGInterchangeFormatLength + length = jpegLength; + } // Read data processImageStarted(imageIndex); // Better yet, would be to delegate read progress here... + imageInput.seek(realJPEGOffset); - try { - jpegParam.setSourceRegion(new Rectangle(0, 0, width, height)); - jpegParam.setDestination(destination); - // TODO: This works only if Gray/YCbCr/RGB, not CMYK/LAB/etc... - // In the latter case we will have to use readAsRaster and do color conversion ourselves - jpegReader.read(0, jpegParam); - } - finally { - stream.close(); + try (ImageInputStream stream = new SubImageInputStream(imageInput, length)) { + jpegReader.setInput(stream); + jpegParam.setSourceRegion(srcRegion); + + if (interpretation == TIFFExtension.PHOTOMETRIC_YCBCR || interpretation == TIFFBaseline.PHOTOMETRIC_RGB) { + jpegParam.setDestination(destination); + jpegReader.read(0, jpegParam); + } + else { + // Otherwise, it's likely CMYK or some other interpretation we don't need to convert. + // We'll have to use readAsRaster and later apply color space conversion ourselves + Raster raster = jpegReader.readRaster(0, jpegParam); + destination.getRaster().setDataElements(0, 0, raster); + } } processImageProgress(100f); @@ -969,26 +1217,31 @@ public class TIFFImageReader extends ImageReaderBase { // Read only tiles that lies within region if (new Rectangle(col, row, colsInTile, rowsInTile).intersects(srcRegion)) { imageInput.seek(stripTileOffsets[i]); - stream = ImageIO.createImageInputStream(new SequenceInputStream(Collections.enumeration( + + try (ImageInputStream stream = ImageIO.createImageInputStream(new SequenceInputStream(Collections.enumeration( Arrays.asList( - createJFIFStream(destRaster, stripTileWidth, stripTileHeight, qTables, dcTables, acTables), - IIOUtil.createStreamAdapter(imageInput, stripTileByteCounts != null ? (int) stripTileByteCounts[i] : Short.MAX_VALUE), + createJFIFStream(destRaster.getNumBands(), stripTileWidth, stripTileHeight, qTables, dcTables, acTables), + IIOUtil.createStreamAdapter(imageInput, stripTileByteCounts != null + ? (int) stripTileByteCounts[i] + : Short.MAX_VALUE), new ByteArrayInputStream(new byte[] {(byte) 0xff, (byte) 0xd9}) // EOI ) - ))); - - jpegReader.setInput(stream); - - try { + )))) { + jpegReader.setInput(stream); jpegParam.setSourceRegion(new Rectangle(0, 0, colsInTile, rowsInTile)); jpegParam.setDestinationOffset(new Point(col - srcRegion.x, row - srcRegion.y)); jpegParam.setDestination(destination); - // TODO: This works only if Gray/YCbCr/RGB, not CMYK/LAB/etc... - // In the latter case we will have to use readAsRaster and do color conversion ourselves - jpegReader.read(0, jpegParam); - } - finally { - stream.close(); + + if (interpretation == TIFFExtension.PHOTOMETRIC_YCBCR || interpretation == TIFFBaseline.PHOTOMETRIC_RGB) { + jpegParam.setDestination(destination); + jpegReader.read(0, jpegParam); + } + else { + // Otherwise, it's likely CMYK or some other interpretation we don't need to convert. + // We'll have to use readAsRaster and later apply color space conversion ourselves + Raster raster = jpegReader.readRaster(0, jpegParam); + destination.getRaster().setDataElements(0, 0, raster); + } } } @@ -1013,10 +1266,6 @@ public class TIFFImageReader extends ImageReaderBase { break; // Additionally, the specification defines these values as part of the TIFF extensions: - case TIFFExtension.COMPRESSION_CCITT_T4: - // CCITT Group 3 fax encoding - case TIFFExtension.COMPRESSION_CCITT_T6: - // CCITT Group 4 fax encoding // Known, but unsupported compression types case TIFFCustom.COMPRESSION_NEXT: @@ -1039,31 +1288,57 @@ public class TIFFImageReader extends ImageReaderBase { throw new IIOException("Unknown TIFF Compression value: " + compression); } + // TODO: Convert color space from source to destination + processImageComplete(); return destination; } - private static InputStream createJFIFStream(WritableRaster raster, int stripTileWidth, int stripTileHeight, byte[][] qTables, byte[][] dcTables, byte[][] acTables) throws IOException { + private ImageReader createJPEGDelegate() throws IIOException { + // TIFF is strictly ISO JPEG, so we should probably stick to the standard reader + try { + @SuppressWarnings("unchecked") + Class readerClass = (Class) Class.forName("com.sun.imageio.plugins.jpeg.JPEGImageReader"); + Constructor constructor = readerClass.getConstructor(ImageReaderSpi.class); + return constructor.newInstance(getOriginatingProvider()); + } + catch (ClassNotFoundException | NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException ignore) { + if (DEBUG) { + ignore.printStackTrace(); + } + // Fall back to default reader below + } + + // If we can't get the standard reader, fall back to the default (first) reader + Iterator readers = ImageIO.getImageReadersByFormatName("JPEG"); + if (!readers.hasNext()) { + throw new IIOException("Could not instantiate JPEGImageReader"); + } + + return readers.next(); + } + + private static InputStream createJFIFStream(int bands, int stripTileWidth, int stripTileHeight, byte[][] qTables, byte[][] dcTables, byte[][] acTables) throws IOException { FastByteArrayOutputStream stream = new FastByteArrayOutputStream( - 2 + 2 + 2 + 6 + 3 * raster.getNumBands() + + 2 + 2 + 2 + 6 + 3 * bands + 5 * qTables.length + qTables.length * qTables[0].length + 5 * dcTables.length + dcTables.length * dcTables[0].length + 5 * acTables.length + acTables.length * acTables[0].length + - 8 + 2 * raster.getNumBands() + 8 + 2 * bands ); DataOutputStream out = new DataOutputStream(stream); out.writeShort(JPEG.SOI); out.writeShort(JPEG.SOF0); - out.writeShort(2 + 6 + 3 * raster.getNumBands()); // SOF0 len - out.writeByte(8); // bits TODO: Consult raster/transfer type or BitsPerSample for 12/16 bits support + out.writeShort(2 + 6 + 3 * bands); // SOF0 len + out.writeByte(8); // bits TODO: Consult bands/transfer type or BitsPerSample for 12/16 bits support out.writeShort(stripTileHeight); // height out.writeShort(stripTileWidth); // width - out.writeByte(raster.getNumBands()); // Number of components + out.writeByte(bands); // Number of components - for (int comp = 0; comp < raster.getNumBands(); comp++) { + for (int comp = 0; comp < bands; comp++) { out.writeByte(comp); // Component id out.writeByte(comp == 0 ? 0x22 : 0x11); // h/v subsampling TODO: FixMe, consult YCbCrSubsampling out.writeByte(comp); // Q table selector TODO: Consider merging if tables are equal @@ -1097,10 +1372,10 @@ public class TIFFImageReader extends ImageReaderBase { } out.writeShort(JPEG.SOS); - out.writeShort(6 + 2 * raster.getNumBands()); // SOS length - out.writeByte(raster.getNumBands()); // Num comp + out.writeShort(6 + 2 * bands); // SOS length + out.writeByte(bands); // Num comp - for (int component = 0; component < raster.getNumBands(); component++) { + for (int component = 0; component < bands; component++) { out.writeByte(component); // Comp id out.writeByte(component == 0 ? component : 0x10 + (component & 0xf)); // dc/ac selector } @@ -1138,94 +1413,188 @@ public class TIFFImageReader extends ImageReaderBase { final int colsInTile, final int rowsInTile, final DataInput input) throws IOException { + DataBuffer dataBuffer = tileRowRaster.getDataBuffer(); + int bands = dataBuffer.getNumBanks(); + boolean banded = bands > 1; + switch (tileRowRaster.getTransferType()) { case DataBuffer.TYPE_BYTE: - byte[] rowDataByte = ((DataBufferByte) tileRowRaster.getDataBuffer()).getData(); - for (int row = startRow; row < startRow + rowsInTile; row++) { - if (row >= srcRegion.y + srcRegion.height) { - break; // We're done with this tile - } + for (int band = 0; band < bands; band++) { + int bank = banded ? ((BandedSampleModel) tileRowRaster.getSampleModel()).getBankIndices()[band] : band; + byte[] rowDataByte = ((DataBufferByte) dataBuffer).getData(bank); + WritableRaster destChannel = banded + ? raster.createWritableChild(raster.getMinX(), raster.getMinY(), raster.getWidth(), raster.getHeight(), 0, 0, new int[] {band}) + : raster; + Raster srcChannel = banded + ? tileRowRaster.createChild(tileRowRaster.getMinX(), 0, tileRowRaster.getWidth(), 1, 0, 0, new int[] {band}) + : tileRowRaster; - input.readFully(rowDataByte); - - if (row % ySub == 0 && row >= srcRegion.y) { - normalizeBlack(interpretation, rowDataByte); - - // Subsample horizontal - if (xSub != 1) { - for (int x = srcRegion.x / xSub * numBands; x < ((srcRegion.x + srcRegion.width) / xSub) * numBands; x += numBands) { - for (int b = 0; b < numBands; b++) { - rowDataByte[x + b] = rowDataByte[x * xSub + b]; - } - } + for (int row = startRow; row < startRow + rowsInTile; row++) { + if (row >= srcRegion.y + srcRegion.height) { + break; // We're done with this tile } - raster.setDataElements(startCol, (row - srcRegion.y) / ySub, tileRowRaster); + input.readFully(rowDataByte); + + if (row % ySub == 0 && row >= srcRegion.y) { + if (!banded) { + normalizeColor(interpretation, rowDataByte); + } + + // Subsample horizontal + if (xSub != 1) { + for (int x = srcRegion.x / xSub * numBands; x < ((srcRegion.x + srcRegion.width) / xSub) * numBands; x += numBands) { + System.arraycopy(rowDataByte, x * xSub, rowDataByte, x, numBands); + } + } + + destChannel.setDataElements(startCol, (row - srcRegion.y) / ySub, srcChannel); + } + // Else skip data } - // Else skip data } +// if (banded) { +// // TODO: Normalize colors for tile (need to know tile region and sample model) +// // Unfortunately, this will disable acceleration... +// } + break; case DataBuffer.TYPE_USHORT: case DataBuffer.TYPE_SHORT: - short[] rowDataShort = tileRowRaster.getTransferType() == DataBuffer.TYPE_USHORT - ? ((DataBufferUShort) tileRowRaster.getDataBuffer()).getData() - : ((DataBufferShort) tileRowRaster.getDataBuffer()).getData(); + for (int band = 0; band < bands; band++) { + short[] rowDataShort = dataBuffer.getDataType() == DataBuffer.TYPE_USHORT + ? ((DataBufferUShort) dataBuffer).getData(band) + : ((DataBufferShort) dataBuffer).getData(band); - for (int row = startRow; row < startRow + rowsInTile; row++) { - if (row >= srcRegion.y + srcRegion.height) { - break; // We're done with this tile - } + WritableRaster destChannel = banded + ? raster.createWritableChild(raster.getMinX(), raster.getMinY(), raster.getWidth(), raster.getHeight(), 0, 0, new int[] {band}) + : raster; + Raster srcChannel = banded + ? tileRowRaster.createChild(tileRowRaster.getMinX(), 0, tileRowRaster.getWidth(), 1, 0, 0, new int[] {band}) + : tileRowRaster; - readFully(input, rowDataShort); - - if (row >= srcRegion.y) { - normalizeBlack(interpretation, rowDataShort); - - // Subsample horizontal - if (xSub != 1) { - for (int x = srcRegion.x / xSub * numBands; x < ((srcRegion.x + srcRegion.width) / xSub) * numBands; x += numBands) { - for (int b = 0; b < numBands; b++) { - rowDataShort[x + b] = rowDataShort[x * xSub + b]; - } - } + for (int row = startRow; row < startRow + rowsInTile; row++) { + if (row >= srcRegion.y + srcRegion.height) { + break; // We're done with this tile } - raster.setDataElements(startCol, row - srcRegion.y, tileRowRaster); + readFully(input, rowDataShort); + + if (row >= srcRegion.y) { + normalizeColor(interpretation, rowDataShort); + + // Subsample horizontal + if (xSub != 1) { + for (int x = srcRegion.x / xSub * numBands; x < ((srcRegion.x + srcRegion.width) / xSub) * numBands; x += numBands) { + System.arraycopy(rowDataShort, x * xSub, rowDataShort, x, numBands); + } + } + + destChannel.setDataElements(startCol, row - srcRegion.y, srcChannel); + // TODO: Possible speedup ~30%!: +// raster.setDataElements(startCol, row - srcRegion.y, colsInTile, 1, rowDataShort); + } + // Else skip data } - // Else skip data } break; case DataBuffer.TYPE_INT: - int[] rowDataInt = ((DataBufferInt) tileRowRaster.getDataBuffer()).getData(); + for (int band = 0; band < bands; band++) { + int[] rowDataInt = ((DataBufferInt) dataBuffer).getData(band); - for (int row = startRow; row < startRow + rowsInTile; row++) { - if (row >= srcRegion.y + srcRegion.height) { - break; // We're done with this tile - } + WritableRaster destChannel = banded + ? raster.createWritableChild(raster.getMinX(), raster.getMinY(), raster.getWidth(), raster.getHeight(), 0, 0, new int[] {band}) + : raster; + Raster srcChannel = banded + ? tileRowRaster.createChild(tileRowRaster.getMinX(), 0, tileRowRaster.getWidth(), 1, 0, 0, new int[] {band}) + : tileRowRaster; - readFully(input, rowDataInt); - - if (row >= srcRegion.y) { - normalizeBlack(interpretation, rowDataInt); - - // Subsample horizontal - if (xSub != 1) { - for (int x = srcRegion.x / xSub * numBands; x < ((srcRegion.x + srcRegion.width) / xSub) * numBands; x += numBands) { - for (int b = 0; b < numBands; b++) { - rowDataInt[x + b] = rowDataInt[x * xSub + b]; - } - } + for (int row = startRow; row < startRow + rowsInTile; row++) { + if (row >= srcRegion.y + srcRegion.height) { + break; // We're done with this tile } - raster.setDataElements(startCol, row - srcRegion.y, tileRowRaster); + readFully(input, rowDataInt); + + if (row >= srcRegion.y) { + normalizeColor(interpretation, rowDataInt); + + // Subsample horizontal + if (xSub != 1) { + for (int x = srcRegion.x / xSub * numBands; x < ((srcRegion.x + srcRegion.width) / xSub) * numBands; x += numBands) { + System.arraycopy(rowDataInt, x * xSub, rowDataInt, x, numBands); + } + } + + destChannel.setDataElements(startCol, row - srcRegion.y, srcChannel); + } + // Else skip data } - // Else skip data } break; + + case DataBuffer.TYPE_FLOAT: + for (int band = 0; band < bands; band++) { + float[] rowDataFloat = ((DataBufferFloat) tileRowRaster.getDataBuffer()).getData(band); + + WritableRaster destChannel = banded + ? raster.createWritableChild(raster.getMinX(), raster.getMinY(), raster.getWidth(), raster.getHeight(), 0, 0, new int[] {band}) + : raster; + Raster srcChannel = banded + ? tileRowRaster.createChild(tileRowRaster.getMinX(), 0, tileRowRaster.getWidth(), 1, 0, 0, new int[] {band}) + : tileRowRaster; + + for (int row = startRow; row < startRow + rowsInTile; row++) { + if (row >= srcRegion.y + srcRegion.height) { + break; // We're done with this tile + } + + readFully(input, rowDataFloat); + + if (row >= srcRegion.y) { + // TODO: Allow param to decide tone mapping strategy, like in the HDRImageReader + clamp(rowDataFloat); + normalizeColor(interpretation, rowDataFloat); + + // Subsample horizontal + if (xSub != 1) { + for (int x = srcRegion.x / xSub * numBands; x < ((srcRegion.x + srcRegion.width) / xSub) * numBands; x += numBands) { + System.arraycopy(rowDataFloat, x * xSub, rowDataFloat, x, numBands); + } + } + + destChannel.setDataElements(startCol, row - srcRegion.y, srcChannel); + } + // Else skip data + } + } + + break; + } + } + + private void clamp(float[] rowDataFloat) { + for (int i = 0; i < rowDataFloat.length; i++) { + if (rowDataFloat[i] > 1) { + rowDataFloat[i] = 1; + } + } + } + + // TODO: Candidate util method (with off/len + possibly byte order) + private void readFully(final DataInput input, final float[] rowDataFloat) throws IOException { + if (input instanceof ImageInputStream) { + ImageInputStream imageInputStream = (ImageInputStream) input; + imageInputStream.readFully(rowDataFloat, 0, rowDataFloat.length); + } + else { + for (int k = 0; k < rowDataFloat.length; k++) { + rowDataFloat[k] = input.readFloat(); + } } } @@ -1255,49 +1624,223 @@ public class TIFFImageReader extends ImageReaderBase { } } - private void normalizeBlack(int photometricInterpretation, short[] data) { - if (photometricInterpretation == TIFFBaseline.PHOTOMETRIC_WHITE_IS_ZERO) { - // Inverse values - for (int i = 0; i < data.length; i++) { - data[i] = (short) (0xffff - data[i] & 0xffff); - } + private void normalizeColor(int photometricInterpretation, byte[] data) { + switch (photometricInterpretation) { + case TIFFBaseline.PHOTOMETRIC_WHITE_IS_ZERO: + // Inverse values + for (int i = 0; i < data.length; i++) { + data[i] ^= -1; + } + + break; + + case TIFFExtension.PHOTOMETRIC_CIELAB: + case TIFFExtension.PHOTOMETRIC_ICCLAB: + case TIFFExtension.PHOTOMETRIC_ITULAB: + // TODO: Whitepoint may be encoded in separate tag + CIELabColorConverter converter = new CIELabColorConverter( + photometricInterpretation == TIFFExtension.PHOTOMETRIC_CIELAB + ? Illuminant.D65 + : Illuminant.D50 + ); + float[] temp = new float[3]; + + for (int i = 0; i < data.length; i += 3) { + // Unsigned scaled form 0...100 + float LStar = (data[i] & 0xff) * 100f / 255.0f; + float aStar; + float bStar; + + if (photometricInterpretation == TIFFExtension.PHOTOMETRIC_CIELAB) { + // -128...127 + aStar = data[i + 1]; + bStar = data[i + 2]; + } + else { + // Assumes same data for ICC and ITU (unsigned) + // 0...255 + aStar = (data[i + 1] & 0xff) - 128; + bStar = (data[i + 2] & 0xff) - 128; + } + + converter.toRGB(LStar, aStar, bStar, temp); + + data[i ] = (byte) temp[0]; + data[i + 1] = (byte) temp[1]; + data[i + 2] = (byte) temp[2]; + } + + break; + + case TIFFExtension.PHOTOMETRIC_YCBCR: + Entry coefficients = currentIFD.getEntryById(TIFF.TAG_YCBCR_COEFFICIENTS); + + if (coefficients == null) { + for (int i = 0; i < data.length; i += 3) { + YCbCrConverter.convertYCbCr2RGB(data, data, i); + } + } + else { + Rational[] value = (Rational[]) coefficients.getValue(); + double[] yCbCrCoefficients = new double[] {value[0].doubleValue(), value[1].doubleValue(), value[2].doubleValue()}; + + for (int i = 0; i < data.length; i += 3) { + YCbCrConverter.convertYCbCr2RGB(data, data, yCbCrCoefficients, i); + } + } + + break; } } - private void normalizeBlack(int photometricInterpretation, int[] data) { - if (photometricInterpretation == TIFFBaseline.PHOTOMETRIC_WHITE_IS_ZERO) { - // Inverse values - for (int i = 0; i < data.length; i++) { - data[i] = (0xffffffff - data[i]); - } + private void normalizeColor(int photometricInterpretation, short[] data) { + switch (photometricInterpretation) { + case TIFFBaseline.PHOTOMETRIC_WHITE_IS_ZERO: + // Inverse values + for (int i = 0; i < data.length; i++) { + data[i] ^= -1; + } + + break; + + case TIFFExtension.PHOTOMETRIC_CIELAB: + case TIFFExtension.PHOTOMETRIC_ICCLAB: + case TIFFExtension.PHOTOMETRIC_ITULAB: + // TODO: Whitepoint may be encoded in separate tag + CIELabColorConverter converter = new CIELabColorConverter( + photometricInterpretation == TIFFExtension.PHOTOMETRIC_ITULAB + ? Illuminant.D65 + : Illuminant.D50 + ); + + float[] temp = new float[3]; + float scaleL = photometricInterpretation == TIFFExtension.PHOTOMETRIC_CIELAB ? 65535f : 65280f; // Is for ICC lab, assumes the same for ITU.... + + for (int i = 0; i < data.length; i += 3) { + // Unsigned scaled form 0...100 + float LStar = (data[i] & 0xffff) * 100.0f / scaleL; + float aStar; + float bStar; + + if (photometricInterpretation == TIFFExtension.PHOTOMETRIC_CIELAB) { + // -32768...32767 + aStar = data[i + 1] / 256f; + bStar = data[i + 2] / 256f; + } + else { + // Assumes same data for ICC and ITU (unsigned) + // 0...65535f + aStar = ((data[i + 1] & 0xffff) - 32768) / 256f; + bStar = ((data[i + 2] & 0xffff) - 32768) / 256f; + } + + converter.toRGB(LStar, aStar, bStar, temp); + + data[i ] = (short) (temp[0] * 257f); + data[i + 1] = (short) (temp[1] * 257f); + data[i + 2] = (short) (temp[2] * 257f); + } + + break; + + case TIFFExtension.PHOTOMETRIC_YCBCR: + double[] coefficients; + + Entry coefficientsTag = currentIFD.getEntryById(TIFF.TAG_YCBCR_COEFFICIENTS); + if (coefficientsTag != null) { + Rational[] value = (Rational[]) coefficientsTag.getValue(); + coefficients = new double[] {value[0].doubleValue(), value[1].doubleValue(), value[2].doubleValue()}; + } + else { + coefficients = CCIR_601_1_COEFFICIENTS; + } + + for (int i = 0; i < data.length; i += 3) { + convertYCbCr2RGB(data, data, coefficients, i); + } } } - private void normalizeBlack(int photometricInterpretation, byte[] data) { - if (photometricInterpretation == TIFFBaseline.PHOTOMETRIC_WHITE_IS_ZERO) { - // Inverse values - for (int i = 0; i < data.length; i++) { - data[i] = (byte) (0xff - data[i] & 0xff); - } + private void normalizeColor(int photometricInterpretation, int[] data) { + switch (photometricInterpretation) { + case TIFFBaseline.PHOTOMETRIC_WHITE_IS_ZERO: + // Inverse values + for (int i = 0; i < data.length; i++) { + data[i] ^= -1; + } + + break; + + case TIFFExtension.PHOTOMETRIC_CIELAB: + case TIFFExtension.PHOTOMETRIC_ICCLAB: + case TIFFExtension.PHOTOMETRIC_ITULAB: + case TIFFExtension.PHOTOMETRIC_YCBCR: + // Not supported + break; } } + private void normalizeColor(int photometricInterpretation, float[] data) { + switch (photometricInterpretation) { + case TIFFBaseline.PHOTOMETRIC_WHITE_IS_ZERO: + case TIFFExtension.PHOTOMETRIC_CIELAB: + case TIFFExtension.PHOTOMETRIC_ICCLAB: + case TIFFExtension.PHOTOMETRIC_ITULAB: + case TIFFExtension.PHOTOMETRIC_YCBCR: + // Not supported + break; + } + } + + private void convertYCbCr2RGB(final short[] yCbCr, final short[] rgb, final double[] coefficients, final int offset) { + int y; + int cb; + int cr; + + y = (yCbCr[offset + 0] & 0xffff); + cb = (yCbCr[offset + 1] & 0xffff) - 32768; + cr = (yCbCr[offset + 2] & 0xffff) - 32768; + + double lumaRed = coefficients[0]; + double lumaGreen = coefficients[1]; + double lumaBlue = coefficients[2]; + + int red = (int) Math.round(cr * (2 - 2 * lumaRed) + y); + int blue = (int) Math.round(cb * (2 - 2 * lumaBlue) + y); + int green = (int) Math.round((y - lumaRed * (red) - lumaBlue * (blue)) / lumaGreen); + + short r = clampShort(red); + short g = clampShort(green); + short b = clampShort(blue); + + // Short values, depends on byte order! + rgb[offset] = r; + rgb[offset + 1] = g; + rgb[offset + 2] = b; + } + + private short clampShort(int val) { + return (short) Math.max(0, Math.min(0xffff, val)); + } + private InputStream createDecompressorStream(final int compression, final int width, final int bands, final InputStream stream) throws IOException { - switch (compression) { + switch (compression) { case TIFFBaseline.COMPRESSION_NONE: return stream; case TIFFBaseline.COMPRESSION_PACKBITS: return new DecoderStream(stream, new PackBitsDecoder(), 1024); case TIFFExtension.COMPRESSION_LZW: - return new DecoderStream(stream, LZWDecoder.create(LZWDecoder.isOldBitReversedStream(stream)), width * bands); + return new DecoderStream(stream, LZWDecoder.create(LZWDecoder.isOldBitReversedStream(stream)), Math.max(width * bands, 1024)); case TIFFExtension.COMPRESSION_ZLIB: // TIFFphotoshop.pdf (aka TIFF specification, supplement 2) says ZLIB (8) and DEFLATE (32946) algorithms are identical case TIFFExtension.COMPRESSION_DEFLATE: return new InflaterInputStream(stream, new Inflater(), 1024); case TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE: + return new CCITTFaxDecoderStream(stream, width, compression, getValueAsIntWithDefault(TIFF.TAG_FILL_ORDER, 1),0L); case TIFFExtension.COMPRESSION_CCITT_T4: + return new CCITTFaxDecoderStream(stream, width, compression, getValueAsIntWithDefault(TIFF.TAG_FILL_ORDER, 1),getValueAsLongWithDefault(TIFF.TAG_GROUP3OPTIONS, 0L)); case TIFFExtension.COMPRESSION_CCITT_T6: - return new CCITTFaxDecoderStream(stream, width, compression, getValueAsIntWithDefault(TIFF.TAG_FILL_ORDER, 1)); + return new CCITTFaxDecoderStream(stream, width, compression, getValueAsIntWithDefault(TIFF.TAG_FILL_ORDER, 1),getValueAsLongWithDefault(TIFF.TAG_GROUP4OPTIONS, 0L)); default: throw new IllegalArgumentException("Unsupported TIFF compression: " + compression); } @@ -1318,6 +1861,7 @@ public class TIFFImageReader extends ImageReaderBase { private long[] getValueAsLongArray(final int tag, final String tagName, boolean required) throws IIOException { Entry entry = currentIFD.getEntryById(tag); + if (entry == null) { if (required) { throw new IIOException("Missing TIFF tag " + tagName); @@ -1358,14 +1902,24 @@ public class TIFFImageReader extends ImageReaderBase { return value; } - public ICC_Profile getICCProfile() { + private ICC_Profile getICCProfile() throws IOException { Entry entry = currentIFD.getEntryById(TIFF.TAG_ICC_PROFILE); - if (entry == null) { - return null; + + if (entry != null) { + byte[] value = (byte[]) entry.getValue(); + + try { + // WEIRDNESS: Reading profile from InputStream is somehow more compatible + // than reading from byte array (chops off extra bytes + validates profile). + ICC_Profile profile = ICC_Profile.getInstance(new ByteArrayInputStream(value)); + return ColorSpaces.validateProfile(profile); + } + catch (CMMException | IllegalArgumentException ignore) { + processWarningOccurred("Ignoring broken/incompatible ICC profile: " + ignore.getMessage()); + } } - byte[] value = (byte[]) entry.getValue(); - return ICC_Profile.getInstance(value); + return null; } // TODO: Tiling support @@ -1377,7 +1931,24 @@ public class TIFFImageReader extends ImageReaderBase { // TODO: Thumbnail support + /// Metadata + + @Override + public IIOMetadata getImageMetadata(int imageIndex) throws IOException { + readIFD(imageIndex); + + return new TIFFImageMetadata(currentIFD); + } + + @Override + public IIOMetadata getStreamMetadata() throws IOException { + // TODO: + return super.getStreamMetadata(); + } + public static void main(final String[] args) throws IOException { + ImageIO.setUseCache(false); + for (final String arg : args) { File file = new File(arg); @@ -1447,22 +2018,30 @@ public class TIFFImageReader extends ImageReaderBase { // param.setSourceSubsampling(sub, sub, 0, 0); // } - long start = System.currentTimeMillis(); + try { + long start = System.currentTimeMillis(); // int width = reader.getWidth(imageNo); // int height = reader.getHeight(imageNo); // param.setSourceRegion(new Rectangle(width / 4, height / 4, width / 2, height / 2)); // param.setSourceRegion(new Rectangle(100, 300, 400, 400)); +// param.setSourceRegion(new Rectangle(3, 3, 9, 9)); // param.setDestinationOffset(new Point(50, 150)); // param.setSourceSubsampling(2, 2, 0, 0); - BufferedImage image = reader.read(imageNo, param); - System.err.println("Read time: " + (System.currentTimeMillis() - start) + " ms"); + BufferedImage image = reader.read(imageNo, param); + System.err.println("Read time: " + (System.currentTimeMillis() - start) + " ms"); - IIOMetadata metadata = reader.getImageMetadata(imageNo); - if (metadata != null) { - new XMLSerializer(System.out, "UTF-8").serialize(metadata.getAsTree(metadata.getNativeMetadataFormatName()), false); - } +// IIOMetadata metadata = reader.getImageMetadata(imageNo); +// if (metadata != null) { +// if (metadata.getNativeMetadataFormatName() != null) { +// new XMLSerializer(System.out, "UTF-8").serialize(metadata.getAsTree(metadata.getNativeMetadataFormatName()), false); +// } +// /*else*/ +// if (metadata.isStandardMetadataFormatSupported()) { +// new XMLSerializer(System.out, "UTF-8").serialize(metadata.getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName), false); +// } +// } - System.err.println("image: " + image); + System.err.println("image: " + image); // File tempFile = File.createTempFile("lzw-", ".bin"); // byte[] data = ((DataBufferByte) image.getRaster().getDataBuffer()).getData(); @@ -1478,7 +2057,7 @@ public class TIFFImageReader extends ImageReaderBase { // // System.err.println("tempFile: " + tempFile.getAbsolutePath()); - // image = new ResampleOp(reader.getWidth(0) / 4, reader.getHeight(0) / 4, ResampleOp.FILTER_LANCZOS).filter(image, null); + // image = new ResampleOp(reader.getWidth(0) / 4, reader.getHeight(0) / 4, ResampleOp.FILTER_LANCZOS).filter(image, null); // // int maxW = 800; // int maxH = 800; @@ -1495,30 +2074,35 @@ public class TIFFImageReader extends ImageReaderBase { // // System.err.println("Scale time: " + (System.currentTimeMillis() - start) + " ms"); // } - if (image.getType() == BufferedImage.TYPE_CUSTOM) { - start = System.currentTimeMillis(); - image = new ColorConvertOp(null).filter(image, new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_ARGB)); - System.err.println("Conversion time: " + (System.currentTimeMillis() - start) + " ms"); - } + if (image.getType() == BufferedImage.TYPE_CUSTOM) { + start = System.currentTimeMillis(); + image = new ColorConvertOp(null).filter(image, new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_ARGB)); + System.err.println("Conversion time: " + (System.currentTimeMillis() - start) + " ms"); + } - showIt(image, String.format("Image: %s [%d x %d]", file.getName(), reader.getWidth(imageNo), reader.getHeight(imageNo))); + showIt(image, String.format("Image: %s [%d x %d]", file.getName(), reader.getWidth(imageNo), reader.getHeight(imageNo))); - try { - int numThumbnails = reader.getNumThumbnails(0); - for (int thumbnailNo = 0; thumbnailNo < numThumbnails; thumbnailNo++) { - BufferedImage thumbnail = reader.readThumbnail(imageNo, thumbnailNo); - // System.err.println("thumbnail: " + thumbnail); - showIt(thumbnail, String.format("Thumbnail: %s [%d x %d]", file.getName(), thumbnail.getWidth(), thumbnail.getHeight())); + try { + int numThumbnails = reader.getNumThumbnails(0); + for (int thumbnailNo = 0; thumbnailNo < numThumbnails; thumbnailNo++) { + BufferedImage thumbnail = reader.readThumbnail(imageNo, thumbnailNo); + // System.err.println("thumbnail: " + thumbnail); + showIt(thumbnail, String.format("Thumbnail: %s [%d x %d]", file.getName(), thumbnail.getWidth(), thumbnail.getHeight())); + } + } + catch (IIOException e) { + System.err.println("Could not read thumbnails: " + e.getMessage()); + e.printStackTrace(); } } - catch (IIOException e) { - System.err.println("Could not read thumbnails: " + e.getMessage()); - e.printStackTrace(); + catch (Throwable t) { + System.err.println(file + " image " + imageNo + " can't be read:"); + t.printStackTrace(); } } } catch (Throwable t) { - System.err.println(file); + System.err.println(file + " can't be read:"); t.printStackTrace(); } finally { diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriteParam.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriteParam.java index 69eba0c1..4ed13ee6 100644 --- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriteParam.java +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriteParam.java @@ -39,16 +39,20 @@ import java.util.Locale; * @version $Id: TIFFImageWriteParam.java,v 1.0 18.09.13 12:47 haraldk Exp$ */ public final class TIFFImageWriteParam extends ImageWriteParam { - // TODO: Support no compression (None/1) - // TODO: Support ZLIB (/Deflate) compression (8) - // TODO: Support PackBits compression (32773) - // TODO: Support JPEG compression (7) - // TODO: Support CCITT Modified Huffman compression (2) - // TODO: Support LZW compression (5)? + // TODO: Support CCITT Modified Huffman compression (2) BASELINE!! + // TODO: Support CCITT T.4 (3) + // TODO: Support CCITT T.6 (4) // TODO: Support JBIG compression via ImageIO plugin/delegate? // TODO: Support JPEG2000 compression via ImageIO plugin/delegate? // TODO: Support tiling - // TODO: Support predictor. See TIFF 6.0 Specification, Section 14: "Differencing Predictor", page 64. + // TODO: Support OPTIONAL predictor. See TIFF 6.0 Specification, Section 14: "Differencing Predictor", page 64. + + // DONE: + // Support no compression (None/1) + // Support ZLIB (/Deflate) compression (8) + // Support PackBits compression (32773) + // Support LZW compression (5)? + // Support JPEG compression (7) TIFFImageWriteParam() { this(Locale.getDefault()); @@ -59,7 +63,12 @@ public final class TIFFImageWriteParam extends ImageWriteParam { // NOTE: We use the same spelling/casing as the JAI equivalent to be as compatible as possible // See: http://download.java.net/media/jai-imageio/javadoc/1.1/com/sun/media/imageio/plugins/tiff/TIFFImageWriteParam.html - compressionTypes = new String[] {"None", /* "CCITT RLE", "CCITT T.4", "CCITT T.6", */ "LZW", "JPEG", "ZLib", "PackBits", "Deflate", /* "EXIF JPEG" */ }; + compressionTypes = new String[] { + "None", + "CCITT RLE", "CCITT T.4", "CCITT T.6", + "LZW", "JPEG", "ZLib", "PackBits", "Deflate", + null/* "EXIF JPEG" */ // A well-defined form of "Old-style JPEG", no tables/process, only 513 (offset) and 514 (length) + }; compressionType = compressionTypes[0]; canWriteCompressed = true; } @@ -102,6 +111,18 @@ public final class TIFFImageWriteParam extends ImageWriteParam { else if (param.getCompressionType().equals("JPEG")) { return TIFFExtension.COMPRESSION_JPEG; } + else if (param.getCompressionType().equals("CCITT RLE")) { + return TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE; + } + else if (param.getCompressionType().equals("CCITT T.4")) { + return TIFFExtension.COMPRESSION_CCITT_T4; + } + else if (param.getCompressionType().equals("CCITT T.6")) { + return TIFFExtension.COMPRESSION_CCITT_T6; + } +// else if (param.getCompressionType().equals("EXIF JPEG")) { +// return TIFFExtension.COMPRESSION_OLD_JPEG; +// } throw new IllegalArgumentException(String.format("Unsupported compression type: %s", param.getCompressionType())); } diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriter.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriter.java index 93eac986..15d934fa 100644 --- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriter.java +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriter.java @@ -31,16 +31,21 @@ package com.twelvemonkeys.imageio.plugins.tiff; import com.twelvemonkeys.image.ImageUtil; import com.twelvemonkeys.imageio.ImageWriterBase; import com.twelvemonkeys.imageio.metadata.AbstractEntry; +import com.twelvemonkeys.imageio.metadata.Directory; import com.twelvemonkeys.imageio.metadata.Entry; import com.twelvemonkeys.imageio.metadata.exif.EXIFWriter; import com.twelvemonkeys.imageio.metadata.exif.Rational; import com.twelvemonkeys.imageio.metadata.exif.TIFF; +import com.twelvemonkeys.imageio.stream.SubImageOutputStream; import com.twelvemonkeys.imageio.util.IIOUtil; import com.twelvemonkeys.io.enc.EncoderStream; import com.twelvemonkeys.io.enc.PackBitsEncoder; +import com.twelvemonkeys.lang.Validate; import javax.imageio.*; +import javax.imageio.metadata.IIOInvalidTreeException; import javax.imageio.metadata.IIOMetadata; +import javax.imageio.metadata.IIOMetadataFormatImpl; import javax.imageio.spi.ImageWriterSpi; import javax.imageio.stream.ImageInputStream; import javax.imageio.stream.ImageOutputStream; @@ -49,10 +54,9 @@ import java.awt.color.ColorSpace; import java.awt.color.ICC_ColorSpace; import java.awt.image.*; import java.io.*; +import java.lang.reflect.Array; import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; +import java.util.*; import java.util.zip.Deflater; import java.util.zip.DeflaterOutputStream; @@ -65,21 +69,20 @@ import java.util.zip.DeflaterOutputStream; */ public final class TIFFImageWriter extends ImageWriterBase { // Short term - // TODO: Support JPEG compression (7) - might need extra input to allow multiple images with single DQT - // TODO: Use sensible defaults for compression based on input? None is sensible... :-) + // TODO: Support more of the ImageIO metadata (ie. compression from metadata, etc) // Long term // TODO: Support tiling // TODO: Support thumbnails - // TODO: Support ImageIO metadata // TODO: Support CCITT Modified Huffman compression (2) - // TODO: Full "Baseline TIFF" support - // TODO: Support LZW compression (5)? + // TODO: Full "Baseline TIFF" support (pending CCITT compression 2) + // TODO: CCITT compressions T.4 and T.6 + // TODO: Support JPEG compression of CMYK data (pending JPEGImageWriter CMYK write support) // ---- // TODO: Support storing multiple images in one stream (multi-page TIFF) // TODO: Support use-case: Transcode multi-layer PSD to multi-page TIFF with metadata // TODO: Support use-case: Transcode multi-page TIFF to multiple single-page TIFFs with metadata - // TODO: Support use-case: Losslessly transcode JPEG to JPEG in TIFF with (EXIF) metadata (and back) + // TODO: Support use-case: Losslessly transcode JPEG to JPEG-in-TIFF with (EXIF) metadata (and back) // Very long term... // TODO: Support JBIG compression via ImageIO plugin/delegate? Pending support in Reader @@ -91,24 +94,111 @@ public final class TIFFImageWriter extends ImageWriterBase { // Support predictor. See TIFF 6.0 Specification, Section 14: "Differencing Predictor", page 64. // Support PackBits compression (32773) - easy - BASELINE // Support ZLIB (/Deflate) compression (8) - easy + // Support LZW compression (5) + // Support JPEG compression (7) - might need extra input to allow multiple images with single DQT + // Use sensible defaults for compression based on input? None is sensible... :-) + // Support resolution, resolution unit and software tags from ImageIO metadata public static final Rational STANDARD_DPI = new Rational(72); + /** + * Flag for active sequence writing + */ + private boolean isWritingSequence = false; + + /** + * Metadata writer for sequence writing + */ + private EXIFWriter sequenceExifWriter = null; + + /** + * Position of last IFD Pointer on active sequence writing + */ + private long sequenceLastIFDPos = -1; + TIFFImageWriter(final ImageWriterSpi provider) { super(provider); } + @Override + public void setOutput(final Object output) { + super.setOutput(output); + + // TODO: Allow appending/partly overwrite of existing file... + } + static final class TIFFEntry extends AbstractEntry { - TIFFEntry(Object identifier, Object value) { + // TODO: Expose a merge of this and the EXIFEntry class... + private final short type; + + private static short guessType(final Object val) { + // TODO: This code is duplicated in EXIFWriter.getType, needs refactor! + Object value = Validate.notNull(val); + + boolean array = value.getClass().isArray(); + if (array) { + value = Array.get(value, 0); + } + + // Note: This "narrowing" is to keep data consistent between read/write. + // TODO: Check for negative values and use signed types? + if (value instanceof Byte) { + return TIFF.TYPE_BYTE; + } + if (value instanceof Short) { + if (!array && (Short) value < Byte.MAX_VALUE) { + return TIFF.TYPE_BYTE; + } + + return TIFF.TYPE_SHORT; + } + if (value instanceof Integer) { + if (!array && (Integer) value < Short.MAX_VALUE) { + return TIFF.TYPE_SHORT; + } + + return TIFF.TYPE_LONG; + } + if (value instanceof Long) { + if (!array && (Long) value < Integer.MAX_VALUE) { + return TIFF.TYPE_LONG; + } + } + + if (value instanceof Rational) { + return TIFF.TYPE_RATIONAL; + } + + if (value instanceof String) { + return TIFF.TYPE_ASCII; + } + + // TODO: More types + + throw new UnsupportedOperationException(String.format("Method guessType not implemented for value of type %s", value.getClass())); + } + + TIFFEntry(final int identifier, final Object value) { + this(identifier, guessType(value), value); + } + + TIFFEntry(int identifier, short type, Object value) { super(identifier, value); + this.type = type; + } + + @Override + public String getTypeName() { + return TIFF.TYPE_NAMES[type]; } } @Override - public void write(IIOMetadata streamMetadata, IIOImage image, ImageWriteParam param) throws IOException { + public void write(final IIOMetadata streamMetadata, final IIOImage image, final ImageWriteParam param) throws IOException { // TODO: Validate input assertOutput(); + // TODO: streamMetadata? // TODO: Consider writing TIFF header, offset to IFD0 (leave blank), write image data with correct // tiling/compression/etc, then write IFD0, go back and update IFD0 offset? @@ -116,22 +206,38 @@ public final class TIFFImageWriter extends ImageWriterBase { // Write minimal TIFF header (required "Baseline" fields) // Use EXIFWriter to write leading metadata (TODO: consider rename to TTIFFWriter, again...) // TODO: Make TIFFEntry and possibly TIFFDirectory? public + EXIFWriter exifWriter = new EXIFWriter(); + exifWriter.writeTIFFHeader(imageOutput); + long IFD0Pos = imageOutput.getStreamPosition(); + writePage(image, param, exifWriter, IFD0Pos); + imageOutput.writeInt(0); // EOF + imageOutput.flush(); + } + + private long writePage(IIOImage image, ImageWriteParam param, EXIFWriter exifWriter, long lastIFDPointer) + throws IOException { RenderedImage renderedImage = image.getRenderedImage(); ColorModel colorModel = renderedImage.getColorModel(); int numComponents = colorModel.getNumComponents(); + TIFFImageMetadata metadata; + if (image.getMetadata() != null) { + metadata = convertImageMetadata(image.getMetadata(), ImageTypeSpecifier.createFromRenderedImage(renderedImage), param); + } + else { + metadata = initMeta(null, ImageTypeSpecifier.createFromRenderedImage(renderedImage), param); + } + SampleModel sampleModel = renderedImage.getSampleModel(); int[] bandOffsets; int[] bitOffsets; if (sampleModel instanceof ComponentSampleModel) { bandOffsets = ((ComponentSampleModel) sampleModel).getBandOffsets(); -// System.err.println("bandOffsets: " + Arrays.toString(bandOffsets)); - bitOffsets = null; + bitOffsets = null; } else if (sampleModel instanceof SinglePixelPackedSampleModel) { bitOffsets = ((SinglePixelPackedSampleModel) sampleModel).getBitOffsets(); -// System.err.println("bitOffsets: " + Arrays.toString(bitOffsets)); bandOffsets = null; } else if (sampleModel instanceof MultiPixelPackedSampleModel) { @@ -142,117 +248,207 @@ public final class TIFFImageWriter extends ImageWriterBase { throw new IllegalArgumentException("Unknown bit/bandOffsets for sample model: " + sampleModel); } - List entries = new ArrayList(); - entries.add(new TIFFEntry(TIFF.TAG_IMAGE_WIDTH, renderedImage.getWidth())); - entries.add(new TIFFEntry(TIFF.TAG_IMAGE_HEIGHT, renderedImage.getHeight())); -// entries.add(new TIFFEntry(TIFF.TAG_ORIENTATION, 1)); // (optional) - entries.add(new TIFFEntry(TIFF.TAG_BITS_PER_SAMPLE, asShortArray(sampleModel.getSampleSize()))); - // If numComponents > 3, write ExtraSamples - if (numComponents > 3) { - // TODO: Write per component > 3 + Map entries = new LinkedHashMap<>(); + entries.put(TIFF.TAG_IMAGE_WIDTH, new TIFFEntry(TIFF.TAG_IMAGE_WIDTH, renderedImage.getWidth())); + entries.put(TIFF.TAG_IMAGE_HEIGHT, new TIFFEntry(TIFF.TAG_IMAGE_HEIGHT, renderedImage.getHeight())); + // entries.add(new TIFFEntry(TIFF.TAG_ORIENTATION, 1)); // (optional) + entries.put(TIFF.TAG_BITS_PER_SAMPLE, new TIFFEntry(TIFF.TAG_BITS_PER_SAMPLE, asShortArray(sampleModel.getSampleSize()))); + // If numComponents > numColorComponents, write ExtraSamples + if (numComponents > colorModel.getNumColorComponents()) { + // TODO: Write per component > numColorComponents if (colorModel.hasAlpha()) { - entries.add(new TIFFEntry(TIFF.TAG_EXTRA_SAMPLES, colorModel.isAlphaPremultiplied() ? TIFFBaseline.EXTRASAMPLE_ASSOCIATED_ALPHA : TIFFBaseline.EXTRASAMPLE_UNASSOCIATED_ALPHA)); + entries.put(TIFF.TAG_EXTRA_SAMPLES, new TIFFEntry(TIFF.TAG_EXTRA_SAMPLES, colorModel.isAlphaPremultiplied() + ? TIFFBaseline.EXTRASAMPLE_ASSOCIATED_ALPHA + : TIFFBaseline.EXTRASAMPLE_UNASSOCIATED_ALPHA)); } else { - entries.add(new TIFFEntry(TIFF.TAG_EXTRA_SAMPLES, TIFFBaseline.EXTRASAMPLE_UNSPECIFIED)); + entries.put(TIFF.TAG_EXTRA_SAMPLES, new TIFFEntry(TIFF.TAG_EXTRA_SAMPLES, TIFFBaseline.EXTRASAMPLE_UNSPECIFIED)); } } + // Write compression field from param or metadata - int compression = TIFFImageWriteParam.getCompressionType(param); - entries.add(new TIFFEntry(TIFF.TAG_COMPRESSION, compression)); - // TODO: Let param control + int compression; + if ((param == null || param.getCompressionMode() == TIFFImageWriteParam.MODE_COPY_FROM_METADATA) + && image.getMetadata() != null && metadata.getIFD().getEntryById(TIFF.TAG_COMPRESSION) != null) { + compression = (int) metadata.getIFD().getEntryById(TIFF.TAG_COMPRESSION).getValue(); + } + else { + compression = TIFFImageWriteParam.getCompressionType(param); + } + entries.put(TIFF.TAG_COMPRESSION, new TIFFEntry(TIFF.TAG_COMPRESSION, compression)); + + // TODO: Let param/metadata control predictor + // TODO: Depending on param.getCompressionMode(): DISABLED/EXPLICIT/COPY_FROM_METADATA/DEFAULT switch (compression) { case TIFFExtension.COMPRESSION_ZLIB: case TIFFExtension.COMPRESSION_DEFLATE: case TIFFExtension.COMPRESSION_LZW: - entries.add(new TIFFEntry(TIFF.TAG_PREDICTOR, TIFFExtension.PREDICTOR_HORIZONTAL_DIFFERENCING)); + entries.put(TIFF.TAG_PREDICTOR, new TIFFEntry(TIFF.TAG_PREDICTOR, TIFFExtension.PREDICTOR_HORIZONTAL_DIFFERENCING)); + break; + case TIFFExtension.COMPRESSION_CCITT_T4: + Entry group3options = metadata.getIFD().getEntryById(TIFF.TAG_GROUP3OPTIONS); + if (group3options == null) { + group3options = new TIFFEntry(TIFF.TAG_GROUP3OPTIONS, (long) TIFFExtension.GROUP3OPT_2DENCODING); + } + entries.put(TIFF.TAG_GROUP3OPTIONS, group3options); + break; + case TIFFExtension.COMPRESSION_CCITT_T6: + Entry group4options = metadata.getIFD().getEntryById(TIFF.TAG_GROUP4OPTIONS); + if (group4options == null) { + group4options = new TIFFEntry(TIFF.TAG_GROUP4OPTIONS, 0L); + } + entries.put(TIFF.TAG_GROUP4OPTIONS, group4options); + break; default: } - int photometric = getPhotometricInterpretation(colorModel); - entries.add(new TIFFEntry(TIFF.TAG_PHOTOMETRIC_INTERPRETATION, photometric)); + // TODO: We might want to support CMYK in JPEG as well... Pending JPEG CMYK write support. + int photometric = compression == TIFFExtension.COMPRESSION_JPEG ? + TIFFExtension.PHOTOMETRIC_YCBCR : + getPhotometricInterpretation(colorModel); + entries.put(TIFF.TAG_PHOTOMETRIC_INTERPRETATION, new TIFFEntry(TIFF.TAG_PHOTOMETRIC_INTERPRETATION, photometric)); if (photometric == TIFFBaseline.PHOTOMETRIC_PALETTE && colorModel instanceof IndexColorModel) { - entries.add(new TIFFEntry(TIFF.TAG_COLOR_MAP, createColorMap((IndexColorModel) colorModel))); - entries.add(new TIFFEntry(TIFF.TAG_SAMPLES_PER_PIXEL, 1)); + entries.put(TIFF.TAG_COLOR_MAP, new TIFFEntry(TIFF.TAG_COLOR_MAP, createColorMap((IndexColorModel) colorModel))); + entries.put(TIFF.TAG_SAMPLES_PER_PIXEL, new TIFFEntry(TIFF.TAG_SAMPLES_PER_PIXEL, 1)); } else { - entries.add(new TIFFEntry(TIFF.TAG_SAMPLES_PER_PIXEL, numComponents)); + if (colorModel.getPixelSize() == 1) { + numComponents = 1; + } - // TODO: What is the default TIFF color space? + entries.put(TIFF.TAG_SAMPLES_PER_PIXEL, new TIFFEntry(TIFF.TAG_SAMPLES_PER_PIXEL, numComponents)); + + // Note: Assuming sRGB to be the default RGB interpretation ColorSpace colorSpace = colorModel.getColorSpace(); - if (colorSpace instanceof ICC_ColorSpace) { - entries.add(new TIFFEntry(TIFF.TAG_ICC_PROFILE, ((ICC_ColorSpace) colorSpace).getProfile().getData())); + if (colorSpace instanceof ICC_ColorSpace && !colorSpace.isCS_sRGB()) { + entries.put(TIFF.TAG_ICC_PROFILE, new TIFFEntry(TIFF.TAG_ICC_PROFILE, ((ICC_ColorSpace) colorSpace).getProfile().getData())); } } - if (sampleModel.getDataType() == DataBuffer.TYPE_SHORT /* TODO: if (isSigned(sampleModel.getDataType) or getSampleFormat(sampleModel) != 0 */) { - entries.add(new TIFFEntry(TIFF.TAG_SAMPLE_FORMAT, TIFFExtension.SAMPLEFORMAT_INT)); + // Default sample format SAMPLEFORMAT_UINT need not be written + if (sampleModel.getDataType() == DataBuffer.TYPE_SHORT/* TODO: if isSigned(sampleModel.getDataType) or getSampleFormat(sampleModel) != 0 */) { + entries.put(TIFF.TAG_SAMPLE_FORMAT, new TIFFEntry(TIFF.TAG_SAMPLE_FORMAT, TIFFExtension.SAMPLEFORMAT_INT)); + } + // TODO: Float values! + + // Get Software from metadata, or use default + Entry software = metadata.getIFD().getEntryById(TIFF.TAG_SOFTWARE); + entries.put(TIFF.TAG_SOFTWARE, software != null + ? software + : new TIFFEntry(TIFF.TAG_SOFTWARE, "TwelveMonkeys ImageIO TIFF writer " + originatingProvider.getVersion())); + + // Copy metadata to output + int[] copyTags = { + TIFF.TAG_ORIENTATION, + TIFF.TAG_DATE_TIME, + TIFF.TAG_DOCUMENT_NAME, + TIFF.TAG_IMAGE_DESCRIPTION, + TIFF.TAG_MAKE, + TIFF.TAG_MODEL, + TIFF.TAG_PAGE_NAME, + TIFF.TAG_PAGE_NUMBER, + TIFF.TAG_ARTIST, + TIFF.TAG_HOST_COMPUTER, + TIFF.TAG_COPYRIGHT + }; + for (int tagID : copyTags) { + Entry entry = metadata.getIFD().getEntryById(tagID); + if (entry != null) { + entries.put(tagID, entry); + } } - entries.add(new TIFFEntry(TIFF.TAG_SOFTWARE, "TwelveMonkeys ImageIO TIFF writer")); // TODO: Get from metadata (optional) + fill in version number - - entries.add(new TIFFEntry(TIFF.TAG_X_RESOLUTION, STANDARD_DPI)); - entries.add(new TIFFEntry(TIFF.TAG_Y_RESOLUTION, STANDARD_DPI)); - entries.add(new TIFFEntry(TIFF.TAG_RESOLUTION_UNIT, TIFFBaseline.RESOLUTION_UNIT_DPI)); + // Get X/YResolution and ResolutionUnit from metadata if set, otherwise use defaults + // TODO: Add logic here OR in metadata merging, to make sure these 3 values are consistent. + Entry xRes = metadata.getIFD().getEntryById(TIFF.TAG_X_RESOLUTION); + entries.put(TIFF.TAG_X_RESOLUTION, xRes != null ? xRes : new TIFFEntry(TIFF.TAG_X_RESOLUTION, STANDARD_DPI)); + Entry yRes = metadata.getIFD().getEntryById(TIFF.TAG_Y_RESOLUTION); + entries.put(TIFF.TAG_Y_RESOLUTION, yRes != null ? yRes : new TIFFEntry(TIFF.TAG_Y_RESOLUTION, STANDARD_DPI)); + Entry resUnit = metadata.getIFD().getEntryById(TIFF.TAG_RESOLUTION_UNIT); + entries.put(TIFF.TAG_RESOLUTION_UNIT, + resUnit != null ? resUnit : new TIFFEntry(TIFF.TAG_RESOLUTION_UNIT, TIFFBaseline.RESOLUTION_UNIT_DPI)); // TODO: RowsPerStrip - can be entire image (or even 2^32 -1), but it's recommended to write "about 8K bytes" per strip - entries.add(new TIFFEntry(TIFF.TAG_ROWS_PER_STRIP, Integer.MAX_VALUE)); // TODO: Allowed but not recommended + entries.put(TIFF.TAG_ROWS_PER_STRIP, new TIFFEntry(TIFF.TAG_ROWS_PER_STRIP, Integer.MAX_VALUE)); // TODO: Allowed but not recommended // - StripByteCounts - for no compression, entire image data... (TODO: How to know the byte counts prior to writing data?) TIFFEntry dummyStripByteCounts = new TIFFEntry(TIFF.TAG_STRIP_BYTE_COUNTS, -1); - entries.add(dummyStripByteCounts); // Updated later + entries.put(TIFF.TAG_STRIP_BYTE_COUNTS, dummyStripByteCounts); // Updated later // - StripOffsets - can be offset to single strip only (TODO: but how large is the IFD data...???) TIFFEntry dummyStripOffsets = new TIFFEntry(TIFF.TAG_STRIP_OFFSETS, -1); - entries.add(dummyStripOffsets); // Updated later + entries.put(TIFF.TAG_STRIP_OFFSETS, dummyStripOffsets); // Updated later - // TODO: If tiled, write tile indexes etc, or always do that? - - EXIFWriter exifWriter = new EXIFWriter(); + // TODO: If tiled, write tile indexes etc + // Depending on param.getTilingMode + long nextIFDPointer = -1; + long stripOffset = -1; + long stripByteCount = 0; if (compression == TIFFBaseline.COMPRESSION_NONE) { - // This implementation, allows semi-streaming-compatible uncompressed TIFFs - long streamOffset = exifWriter.computeIFDSize(entries) + 12; // 12 == 4 byte magic, 4 byte IDD 0 pointer, 4 byte EOF + long ifdOffset = exifWriter.computeIFDOffsetSize(entries.values()); + long dataLength = renderedImage.getWidth() * renderedImage.getHeight() * numComponents; + long pointerPos = imageOutput.getStreamPosition() + dataLength + 4 + ifdOffset; + imageOutput.writeInt((int) pointerPos); + } + else { + imageOutput.writeInt(0); // Update IFD Pointer later + } - entries.remove(dummyStripByteCounts); - entries.add(new TIFFEntry(TIFF.TAG_STRIP_BYTE_COUNTS, renderedImage.getWidth() * renderedImage.getHeight() * numComponents)); + stripOffset = imageOutput.getStreamPosition(); + // TODO: Create compressor stream per Tile/Strip + if (compression == TIFFExtension.COMPRESSION_JPEG) { + Iterator writers = ImageIO.getImageWritersByFormatName("JPEG"); + + if (!writers.hasNext()) { + // This can only happen if someone deliberately uninstalled it + throw new IIOException("No JPEG ImageWriter found!"); + } + + ImageWriter jpegWriter = writers.next(); + try { + jpegWriter.setOutput(new SubImageOutputStream(imageOutput)); + jpegWriter.write(renderedImage); + } + finally { + jpegWriter.dispose(); + } + } + else { + // Write image data + writeImageData(createCompressorStream(renderedImage, param, entries), renderedImage, numComponents, bandOffsets, + bitOffsets); + } + stripByteCount = imageOutput.getStreamPosition() - stripOffset; + + // Update IFD0-pointer, and write IFD + if (compression != TIFFBaseline.COMPRESSION_NONE) { entries.remove(dummyStripOffsets); - entries.add(new TIFFEntry(TIFF.TAG_STRIP_OFFSETS, streamOffset)); + entries.put(TIFF.TAG_STRIP_OFFSETS, new TIFFEntry(TIFF.TAG_STRIP_OFFSETS, stripOffset)); + entries.remove(dummyStripByteCounts); + entries.put(TIFF.TAG_STRIP_BYTE_COUNTS, new TIFFEntry(TIFF.TAG_STRIP_BYTE_COUNTS, stripByteCount)); - exifWriter.write(entries, imageOutput); // NOTE: Writer takes case of ordering tags + long idfOffset = exifWriter.writeIFD(entries.values(), imageOutput); // NOTE: Writer takes case of ordering tags + nextIFDPointer = imageOutput.getStreamPosition(); + imageOutput.seek(lastIFDPointer); + imageOutput.writeInt((int) idfOffset); + imageOutput.seek(nextIFDPointer); imageOutput.flush(); } else { - // Unless compression == 1 / COMPRESSION_NONE (and all offsets known), write only TIFF header/magic + leave room for IFD0 offset - exifWriter.writeTIFFHeader(imageOutput); - imageOutput.writeInt(-1); // IFD0 pointer, will be updated later - } - - // TODO: Create compressor stream per Tile/Strip - // Write image data - writeImageData(createCompressorStream(renderedImage, param), renderedImage, numComponents, bandOffsets, bitOffsets); - - // TODO: Update IFD0-pointer, and write IFD - if (compression != TIFFBaseline.COMPRESSION_NONE) { - long streamPosition = imageOutput.getStreamPosition(); - entries.remove(dummyStripOffsets); - entries.add(new TIFFEntry(TIFF.TAG_STRIP_OFFSETS, 8)); + entries.put(TIFF.TAG_STRIP_OFFSETS, new TIFFEntry(TIFF.TAG_STRIP_OFFSETS, stripOffset)); entries.remove(dummyStripByteCounts); - entries.add(new TIFFEntry(TIFF.TAG_STRIP_BYTE_COUNTS, streamPosition - 8)); + entries.put(TIFF.TAG_STRIP_BYTE_COUNTS, new TIFFEntry(TIFF.TAG_STRIP_BYTE_COUNTS, stripByteCount)); - long ifdOffset = exifWriter.writeIFD(entries, imageOutput); - imageOutput.writeInt(0); // Next IFD (none) - streamPosition = imageOutput.getStreamPosition(); - - // Update IFD0 pointer - imageOutput.seek(4); - imageOutput.writeInt((int) ifdOffset); - imageOutput.seek(streamPosition); + exifWriter.writeIFD(entries.values(), imageOutput); // NOTE: Writer takes case of ordering tags + nextIFDPointer = imageOutput.getStreamPosition(); imageOutput.flush(); } + + return nextIFDPointer; } - private DataOutput createCompressorStream(RenderedImage image, ImageWriteParam param) { + private DataOutput createCompressorStream(RenderedImage image, ImageWriteParam param, Map entries) { /* 36 MB test data: @@ -304,8 +500,9 @@ public final class TIFFImageWriter extends ImageWriterBase { output.length: 12600399 */ - // TODO: Use predictor only by default for -PackBits,- LZW and ZLib/Deflate, unless explicitly disabled (ImageWriteParam) - int compression = TIFFImageWriteParam.getCompressionType(param); + // Use predictor by default for LZW and ZLib/Deflate + // TODO: Unless explicitly disabled in TIFFImageWriteParam + int compression = (int) entries.get(TIFF.TAG_COMPRESSION).getValue(); OutputStream stream; switch (compression) { @@ -321,7 +518,7 @@ public final class TIFFImageWriter extends ImageWriterBase { case TIFFExtension.COMPRESSION_ZLIB: case TIFFExtension.COMPRESSION_DEFLATE: - int deflateSetting = Deflater.BEST_SPEED; // This is consistent with default compression quality being 1.0 and 0 meaning max compression.... + int deflateSetting = Deflater.BEST_SPEED; // This is consistent with default compression quality being 1.0 and 0 meaning max compression... if (param.getCompressionMode() == ImageWriteParam.MODE_EXPLICIT) { // TODO: Determine how to interpret compression quality... // Docs says: @@ -348,8 +545,27 @@ public final class TIFFImageWriter extends ImageWriterBase { case TIFFExtension.COMPRESSION_LZW: stream = IIOUtil.createStreamAdapter(imageOutput); - stream = new EncoderStream(stream, new LZWEncoder((image.getTileWidth() * image.getTileHeight() * image.getTile(0, 0).getNumBands() * image.getColorModel().getComponentSize(0) + 7) / 8)); - stream = new HorizontalDifferencingStream(stream, image.getTileWidth(), image.getTile(0, 0).getNumBands(), image.getColorModel().getComponentSize(0), imageOutput.getByteOrder()); + stream = new EncoderStream(stream, new LZWEncoder((image.getTileWidth() * image.getTileHeight() + * image.getTile(0, 0).getNumBands() * image.getColorModel().getComponentSize(0) + 7) / 8)); + stream = new HorizontalDifferencingStream(stream, image.getTileWidth(), image.getTile(0, 0).getNumBands(), + image.getColorModel().getComponentSize(0), imageOutput.getByteOrder()); + + return new DataOutputStream(stream); + case TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE: + case TIFFExtension.COMPRESSION_CCITT_T4: + case TIFFExtension.COMPRESSION_CCITT_T6: + long option = 0L; + if (compression != TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE) { + option = (long) entries.get(compression == TIFFExtension.COMPRESSION_CCITT_T4 + ? TIFF.TAG_GROUP3OPTIONS + : TIFF.TAG_GROUP4OPTIONS).getValue(); + } + Entry fillOrderEntry = entries.get(TIFF.TAG_FILL_ORDER); + int fillOrder = (int) (fillOrderEntry != null + ? fillOrderEntry.getValue() + : TIFFBaseline.FILL_LEFT_TO_RIGHT); + stream = IIOUtil.createStreamAdapter(imageOutput); + stream = new CCITTFaxEncoderStream(stream, image.getTileWidth(), image.getTileHeight(), compression, fillOrder, option); return new DataOutputStream(stream); } @@ -358,12 +574,12 @@ public final class TIFFImageWriter extends ImageWriterBase { } private int getPhotometricInterpretation(final ColorModel colorModel) { - if (colorModel.getNumComponents() == 1 && colorModel.getComponentSize(0) == 1) { + if (colorModel.getPixelSize() == 1) { if (colorModel instanceof IndexColorModel) { - if (colorModel.getRGB(0) == 0xFFFFFF && colorModel.getRGB(1) == 0x000000) { + if (colorModel.getRGB(0) == 0xFFFFFFFF && colorModel.getRGB(1) == 0xFF000000) { return TIFFBaseline.PHOTOMETRIC_WHITE_IS_ZERO; } - else if (colorModel.getRGB(0) != 0x000000 || colorModel.getRGB(1) != 0xFFFFFF) { + else if (colorModel.getRGB(0) != 0xFF000000 || colorModel.getRGB(1) != 0xFFFFFFFF) { return TIFFBaseline.PHOTOMETRIC_PALETTE; } // Else, fall through to default, BLACK_IS_ZERO @@ -396,9 +612,9 @@ public final class TIFFImageWriter extends ImageWriterBase { for (int i = 0; i < colorModel.getMapSize(); i++) { int color = colorModel.getRGB(i); - colorMap[i ] = (short) upScale((color >> 16) & 0xff); - colorMap[i + colorMap.length / 3] = (short) upScale((color >> 8) & 0xff); - colorMap[i + 2 * colorMap.length / 3] = (short) upScale((color ) & 0xff); + colorMap[i] = (short) upScale((color >> 16) & 0xff); + colorMap[i + colorMap.length / 3] = (short) upScale((color >> 8) & 0xff); + colorMap[i + 2 * colorMap.length / 3] = (short) upScale((color) & 0xff); } return colorMap; @@ -438,16 +654,20 @@ public final class TIFFImageWriter extends ImageWriterBase { // TODO: SampleSize may differ between bands/banks int sampleSize = renderedImage.getSampleModel().getSampleSize(0); - final ByteBuffer buffer = ByteBuffer.allocate(tileWidth * renderedImage.getSampleModel().getNumBands() * sampleSize / 8); - -// System.err.println("tileWidth: " + tileWidth); + final ByteBuffer buffer; + if (sampleSize == 1) { + buffer = ByteBuffer.allocate((tileWidth + 7) / 8); + } + else { + buffer = ByteBuffer.allocate(tileWidth * renderedImage.getSampleModel().getNumBands() * sampleSize / 8); + } + // System.err.println("tileWidth: " + tileWidth); for (int yTile = minTileY; yTile < maxYTiles; yTile++) { for (int xTile = minTileX; xTile < maxXTiles; xTile++) { final Raster tile = renderedImage.getTile(xTile, yTile); final DataBuffer dataBuffer = tile.getDataBuffer(); final int numBands = tile.getNumBands(); -// final SampleModel sampleModel = tile.getSampleModel(); switch (dataBuffer.getDataType()) { case DataBuffer.TYPE_BYTE: @@ -455,9 +675,10 @@ public final class TIFFImageWriter extends ImageWriterBase { // System.err.println("Writing " + numBands + "BYTE -> " + numBands + "BYTE"); for (int b = 0; b < dataBuffer.getNumBanks(); b++) { for (int y = 0; y < tileHeight; y++) { - final int yOff = y * tileWidth * numBands; + int steps = sampleSize == 1 ? (tileWidth + 7) / 8 : tileWidth; + final int yOff = y * steps * numBands; - for (int x = 0; x < tileWidth; x++) { + for (int x = 0; x < steps; x++) { final int xOff = yOff + x * numBands; for (int s = 0; s < numBands; s++) { @@ -585,13 +806,75 @@ public final class TIFFImageWriter extends ImageWriterBase { // Metadata @Override - public IIOMetadata getDefaultImageMetadata(ImageTypeSpecifier imageType, ImageWriteParam param) { - return null; + public TIFFImageMetadata getDefaultImageMetadata(final ImageTypeSpecifier imageType, final ImageWriteParam param) { + return initMeta(null, imageType, param); } @Override - public IIOMetadata convertImageMetadata(IIOMetadata inData, ImageTypeSpecifier imageType, ImageWriteParam param) { - return null; + public TIFFImageMetadata convertImageMetadata(final IIOMetadata inData, + final ImageTypeSpecifier imageType, + final ImageWriteParam param) { + Validate.notNull(inData, "inData"); + Validate.notNull(imageType, "imageType"); + + Directory ifd; + + if (inData instanceof TIFFImageMetadata) { + ifd = ((TIFFImageMetadata) inData).getIFD(); + } + else { + TIFFImageMetadata outData = new TIFFImageMetadata(Collections.emptySet()); + + try { + if (Arrays.asList(inData.getMetadataFormatNames()).contains(TIFFMedataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME)) { + outData.setFromTree(TIFFMedataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME, inData.getAsTree(TIFFMedataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME)); + } + else if (inData.isStandardMetadataFormatSupported()) { + outData.setFromTree(IIOMetadataFormatImpl.standardMetadataFormatName, inData.getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName)); + } + else { + // Unknown format, we can't convert it + return null; + } + } + catch (IIOInvalidTreeException e) { + // TODO: How to issue warning when warning requires imageIndex??? Use -1? + } + + ifd = outData.getIFD(); + } + + // Overwrite in values with values from imageType and param as needed + return initMeta(ifd, imageType, param); + } + + private TIFFImageMetadata initMeta(final Directory ifd, final ImageTypeSpecifier imageType, final ImageWriteParam param) { + Validate.notNull(imageType, "imageType"); + + Map entries = new LinkedHashMap<>(ifd != null ? ifd.size() + 10 : 20); + + if (ifd != null) { + for (Entry entry : ifd) { + entries.put((Integer) entry.getIdentifier(), entry); + } + } + + // TODO: Set values from imageType + entries.put(TIFF.TAG_PHOTOMETRIC_INTERPRETATION, new TIFFEntry(TIFF.TAG_PHOTOMETRIC_INTERPRETATION, TIFF.TYPE_SHORT, getPhotometricInterpretation(imageType.getColorModel()))); + + // TODO: Set values from param if != null + combined values... + + return new TIFFImageMetadata(entries.values()); + } + + @Override + public IIOMetadata getDefaultStreamMetadata(final ImageWriteParam param) { + return super.getDefaultStreamMetadata(param); + } + + @Override + public IIOMetadata convertStreamMetadata(final IIOMetadata inData, final ImageWriteParam param) { + return super.convertStreamMetadata(inData, param); } // Param @@ -601,13 +884,61 @@ public final class TIFFImageWriter extends ImageWriterBase { return new TIFFImageWriteParam(); } + @Override + public boolean canWriteSequence() { + return true; + } + + @Override + public void prepareWriteSequence(IIOMetadata streamMetadata) throws IOException { + if (isWritingSequence) { + throw new IllegalStateException("sequence writing has already been started!"); + } + + // Ignore streamMetadata. ByteOrder is determined from OutputStream + assertOutput(); + isWritingSequence = true; + sequenceExifWriter = new EXIFWriter(); + sequenceExifWriter.writeTIFFHeader(imageOutput); + sequenceLastIFDPos = imageOutput.getStreamPosition(); + } + + @Override + public void writeToSequence(IIOImage image, ImageWriteParam param) throws IOException { + if (!isWritingSequence) { + throw new IllegalStateException("prepareWriteSequence() must be called before writeToSequence()!"); + } + sequenceLastIFDPos = writePage(image, param, sequenceExifWriter, sequenceLastIFDPos); + } + + @Override + public void endWriteSequence() throws IOException { + if (!isWritingSequence) { + throw new IllegalStateException("prepareWriteSequence() must be called before endWriteSequence()!"); + } + imageOutput.writeInt(0); // EOF + isWritingSequence = false; + sequenceExifWriter = null; + sequenceLastIFDPos = -1; + imageOutput.flush(); + } + + @Override + protected void resetMembers() { + super.resetMembers(); + + isWritingSequence = false; + sequenceExifWriter = null; + sequenceLastIFDPos = -1; + } + // Test public static void main(String[] args) throws IOException { int argIdx = 0; // TODO: Proper argument parsing: -t -c - int type = args.length > argIdx + 1 ? Integer.parseInt(args[argIdx++]) : -1; + int type = args.length > argIdx + 1 ? Integer.parseInt(args[argIdx++]) : -1; int compression = args.length > argIdx + 1 ? Integer.parseInt(args[argIdx++]) : 0; if (args.length <= argIdx) { @@ -663,7 +994,7 @@ public final class TIFFImageWriter extends ImageWriterBase { // BufferedImage image = new BufferedImage(original.getWidth(), original.getHeight(), BufferedImage.TYPE_INT_BGR); // BufferedImage image = new BufferedImage(original.getWidth(), original.getHeight(), BufferedImage.TYPE_3BYTE_BGR); BufferedImage image; - if (type < 0 || type == original.getType()) { + if (type <= 0 || type == original.getType()) { image = original; } else if (type == BufferedImage.TYPE_BYTE_INDEXED) { @@ -732,7 +1063,6 @@ public final class TIFFImageWriter extends ImageWriterBase { // } // writer.dispose(); - image = null; BufferedImage read = ImageIO.read(output); diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFMedataFormat.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFMedataFormat.java new file mode 100644 index 00000000..54e9e1b9 --- /dev/null +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFMedataFormat.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2016, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.imageio.plugins.tiff; + +import javax.imageio.ImageTypeSpecifier; +import javax.imageio.metadata.IIOMetadataFormatImpl; + +/** + * TIFFMedataFormat. + * + * @author Harald Kuhr + * @author last modified by $Author: harald.kuhr$ + * @version $Id: TIFFMedataFormat.java,v 1.0 17/04/15 harald.kuhr Exp$ + */ +public final class TIFFMedataFormat extends IIOMetadataFormatImpl { + // TODO: Fix typo in class name + rename to TIFFImageMetadataFormat + private static final TIFFMedataFormat INSTANCE = new TIFFMedataFormat(); + + // We'll reuse the metadata formats defined for JAI + public static final String SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME = "com_sun_media_imageio_plugins_tiff_image_1.0"; + + public TIFFMedataFormat() { + super(SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME, CHILD_POLICY_SOME); + + // TODO: Implement! + } + + @Override + public boolean canNodeAppear(String elementName, ImageTypeSpecifier imageType) { + return true; + } + + public static TIFFMedataFormat getInstance() { + return INSTANCE; + } +} diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFProviderInfo.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFProviderInfo.java index c1e24e58..0d2a9a5d 100644 --- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFProviderInfo.java +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFProviderInfo.java @@ -1,3 +1,31 @@ +/* + * Copyright (c) 2015, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + package com.twelvemonkeys.imageio.plugins.tiff; import com.twelvemonkeys.imageio.spi.ReaderWriterProviderInfo; @@ -13,17 +41,17 @@ final class TIFFProviderInfo extends ReaderWriterProviderInfo { protected TIFFProviderInfo() { super( TIFFProviderInfo.class, - new String[] {"tiff", "TIFF"}, + new String[] {"tiff", "TIFF", "tif", "TIF"}, new String[] {"tif", "tiff"}, new String[] { "image/tiff", "image/x-tiff" }, - "com.twelvemkonkeys.imageio.plugins.tiff.TIFFImageReader", + "com.twelvemonkeys.imageio.plugins.tiff.TIFFImageReader", new String[] {"com.twelvemonkeys.imageio.plugins.tiff.TIFFImageReaderSpi"}, "com.twelvemonkeys.imageio.plugins.tiff.TIFFImageWriter", - new String[] {"com.twelvemkonkeys.imageio.plugins.tif.TIFFImageWriterSpi"}, - false, null, null, null, null, - true, null, null, null, null + new String[] {"com.twelvemonkeys.imageio.plugins.tiff.TIFFImageWriterSpi"}, + false, TIFFStreamMetadata.SUN_NATIVE_STREAM_METADATA_FORMAT_NAME, "com.twelvemonkeys.imageio.plugins.tiff.TIFFStreamMetadataFormat", null, null, + true, TIFFMedataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME, "com.twelvemonkeys.imageio.plugins.tiff.TIFFMedataFormat", null, null ); } } diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFStreamMetadata.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFStreamMetadata.java new file mode 100644 index 00000000..af1e38ab --- /dev/null +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFStreamMetadata.java @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2016, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.imageio.plugins.tiff; + +import com.twelvemonkeys.lang.Validate; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; + +import javax.imageio.metadata.IIOInvalidTreeException; +import javax.imageio.metadata.IIOMetadata; +import javax.imageio.metadata.IIOMetadataNode; +import java.nio.ByteOrder; + +import static java.nio.ByteOrder.BIG_ENDIAN; + +/** + * TIFFStreamMetadata. + * + * @author Harald Kuhr + * @author last modified by $Author: harald.kuhr$ + * @version $Id: TIFFStreamMetadata.java,v 1.0 02/06/16 harald.kuhr Exp$ + */ +public final class TIFFStreamMetadata extends IIOMetadata { + + public static final String SUN_NATIVE_STREAM_METADATA_FORMAT_NAME = "com_sun_media_imageio_plugins_tiff_stream_1.0"; + + ByteOrder byteOrder = BIG_ENDIAN; + + public TIFFStreamMetadata() { + super(false, SUN_NATIVE_STREAM_METADATA_FORMAT_NAME, null, null, null); + } + + + @Override + public boolean isReadOnly() { + return false; + } + + @Override + public Node getAsTree(final String formatName) { + Validate.isTrue(nativeMetadataFormatName.equals(formatName), formatName, "Unsupported metadata format: %s"); + + IIOMetadataNode root = new IIOMetadataNode(nativeMetadataFormatName); + + IIOMetadataNode byteOrderNode = new IIOMetadataNode("ByteOrder"); + root.appendChild(byteOrderNode); + byteOrderNode.setAttribute("value", byteOrder.toString()); + + return root; + } + + @Override + public void mergeTree(final String formatName, final Node root) throws IIOInvalidTreeException { + Validate.isTrue(nativeMetadataFormatName.equals(formatName), formatName, "Unsupported metadata format: %s"); + Validate.notNull(root, "root"); + + if (!nativeMetadataFormatName.equals(root.getNodeName())) { + throw new IIOInvalidTreeException("Root must be " + nativeMetadataFormatName, root); + } + + Node node = root.getFirstChild(); + if (node == null || !node.getNodeName().equals("ByteOrder")) { + throw new IIOInvalidTreeException("Missing \"ByteOrder\" node", node); + } + + NamedNodeMap attributes = node.getAttributes(); + String value = attributes.getNamedItem("value").getNodeValue(); + + if (value == null) { + throw new IIOInvalidTreeException("Missing \"value\" attribute in \"ByteOrder\" node", node); + } + + ByteOrder order = getByteOrder(value.toUpperCase()); + if (order == null) { + throw new IIOInvalidTreeException("Unknown ByteOrder \"value\" attribute: " + value, node); + } + else { + byteOrder = order; + } + } + + private ByteOrder getByteOrder(final String value) throws IIOInvalidTreeException { + switch (value) { + case "BIG_ENDIAN": + return ByteOrder.BIG_ENDIAN; + case "LITTLE_ENDIAN": + return ByteOrder.LITTLE_ENDIAN; + default: + return null; + } + } + + @Override + public void reset() { + // Big endian is always the default + byteOrder = BIG_ENDIAN; + } +} diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFStreamMetadataFormat.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFStreamMetadataFormat.java new file mode 100644 index 00000000..e81d38e4 --- /dev/null +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFStreamMetadataFormat.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2016, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.imageio.plugins.tiff; + +import javax.imageio.ImageTypeSpecifier; +import javax.imageio.metadata.IIOMetadataFormatImpl; +import java.nio.ByteOrder; +import java.util.Arrays; + +/** + * TIFFStreamMetadataFormat. + * + * @author Harald Kuhr + * @author last modified by $Author: harald.kuhr$ + * @version $Id: TIFFStreamMetadataFormat.java,v 1.0 17/04/15 harald.kuhr Exp$ + */ +public final class TIFFStreamMetadataFormat extends IIOMetadataFormatImpl { + private static final TIFFStreamMetadataFormat INSTANCE = new TIFFStreamMetadataFormat(); + + // We'll reuse the metadata formats defined for JAI + public static final String SUN_NATIVE_STREAM_METADATA_FORMAT_NAME = "com_sun_media_imageio_plugins_tiff_stream_1.0"; + + private TIFFStreamMetadataFormat() { + super(SUN_NATIVE_STREAM_METADATA_FORMAT_NAME, CHILD_POLICY_ALL); + + addElement("ByteOrder", SUN_NATIVE_STREAM_METADATA_FORMAT_NAME, CHILD_POLICY_EMPTY); + addAttribute("ByteOrder", "value", DATATYPE_STRING, true, ByteOrder.BIG_ENDIAN.toString(), Arrays.asList(ByteOrder.BIG_ENDIAN.toString(), ByteOrder.LITTLE_ENDIAN.toString())); + } + + @Override + public boolean canNodeAppear(final String elementName, final ImageTypeSpecifier imageType) { + return true; + } + + public static TIFFStreamMetadataFormat getInstance() { + return INSTANCE; + } +} diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/YCbCr16UpsamplerStream.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/YCbCr16UpsamplerStream.java index 8f3d591a..8045e632 100644 --- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/YCbCr16UpsamplerStream.java +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/YCbCr16UpsamplerStream.java @@ -37,21 +37,17 @@ import java.io.InputStream; import java.nio.ByteOrder; /** - * Input stream that provides on-the-fly conversion and upsampling of TIFF subsampled YCbCr 16 bit samples - * to (raw) RGB 16 bit samples. + * Input stream that provides on-the-fly upsampling of TIFF subsampled YCbCr 16 bit samples. * * @author Harald Kuhr * @author last modified by $Author: haraldk$ * @version $Id: YCbCrUpsamplerStream.java,v 1.0 31.01.13 09:25 haraldk Exp$ */ final class YCbCr16UpsamplerStream extends FilterInputStream { - // TODO: As we deal with short/16 bit samples, we need to take byte order into account private final int horizChromaSub; private final int vertChromaSub; private final int yCbCrPos; private final int columns; - private final double[] coefficients; - private final ByteOrder byteOrder; private final int units; private final int unitSize; @@ -65,7 +61,7 @@ final class YCbCr16UpsamplerStream extends FilterInputStream { int bufferLength; int bufferPos; - public YCbCr16UpsamplerStream(final InputStream stream, final int[] chromaSub, final int yCbCrPos, final int columns, final double[] coefficients, final ByteOrder byteOrder) { + public YCbCr16UpsamplerStream(final InputStream stream, final int[] chromaSub, final int yCbCrPos, final int columns, final ByteOrder byteOrder) { super(Validate.notNull(stream, "stream")); Validate.notNull(chromaSub, "chromaSub"); @@ -76,8 +72,6 @@ final class YCbCr16UpsamplerStream extends FilterInputStream { this.vertChromaSub = chromaSub[1]; this.yCbCrPos = yCbCrPos; this.columns = columns; - this.coefficients = coefficients == null ? YCbCrUpsamplerStream.CCIR_601_1_COEFFICIENTS : coefficients; - this.byteOrder = byteOrder; // In TIFF, subsampled streams are stored in "units" of horiz * vert pixels. // For a 4:2 subsampled stream like this: @@ -150,7 +144,7 @@ final class YCbCr16UpsamplerStream extends FilterInputStream { decodedRows[pixelOff + 5] = cr2; // Convert to RGB - convertYCbCr2RGB(decodedRows, decodedRows, coefficients, pixelOff); +// convertYCbCr2RGB(decodedRows, decodedRows, coefficients, pixelOff); } } @@ -228,56 +222,4 @@ final class YCbCr16UpsamplerStream extends FilterInputStream { public synchronized void reset() throws IOException { throw new IOException("mark/reset not supported"); } - - private void convertYCbCr2RGB(final byte[] yCbCr, final byte[] rgb, final double[] coefficients, final int offset) { - int y; - int cb; - int cr; - - // Short values, depends on byte order! - if (byteOrder == ByteOrder.BIG_ENDIAN) { - y = ((yCbCr[offset ] & 0xff) << 8) | (yCbCr[offset + 1] & 0xff); - cb = (((yCbCr[offset + 2] & 0xff) << 8) | (yCbCr[offset + 3] & 0xff)) - 32768; - cr = (((yCbCr[offset + 4] & 0xff) << 8) | (yCbCr[offset + 5] & 0xff)) - 32768; - } - else { - y = ((yCbCr[offset + 1] & 0xff) << 8) | (yCbCr[offset ] & 0xff); - cb = (((yCbCr[offset + 3] & 0xff) << 8) | (yCbCr[offset + 2] & 0xff)) - 32768; - cr = (((yCbCr[offset + 5] & 0xff) << 8) | (yCbCr[offset + 4] & 0xff)) - 32768; - } - - double lumaRed = coefficients[0]; - double lumaGreen = coefficients[1]; - double lumaBlue = coefficients[2]; - - int red = (int) Math.round(cr * (2 - 2 * lumaRed) + y); - int blue = (int) Math.round(cb * (2 - 2 * lumaBlue) + y); - int green = (int) Math.round((y - lumaRed * (red) - lumaBlue * (blue)) / lumaGreen); - - short r = clampShort(red); - short g = clampShort(green); - short b = clampShort(blue); - - // Short values, depends on byte order! - if (byteOrder == ByteOrder.BIG_ENDIAN) { - rgb[offset ] = (byte) ((r >>> 8) & 0xff); - rgb[offset + 1] = (byte) (r & 0xff); - rgb[offset + 2] = (byte) ((g >>> 8) & 0xff); - rgb[offset + 3] = (byte) (g & 0xff); - rgb[offset + 4] = (byte) ((b >>> 8) & 0xff); - rgb[offset + 5] = (byte) (b & 0xff); - } - else { - rgb[offset ] = (byte) (r & 0xff); - rgb[offset + 1] = (byte) ((r >>> 8) & 0xff); - rgb[offset + 2] = (byte) (g & 0xff); - rgb[offset + 3] = (byte) ((g >>> 8) & 0xff); - rgb[offset + 4] = (byte) (b & 0xff); - rgb[offset + 5] = (byte) ((b >>> 8) & 0xff); - } - } - - private short clampShort(int val) { - return (short) Math.max(0, Math.min(0xffff, val)); - } } diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/YCbCrUpsamplerStream.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/YCbCrUpsamplerStream.java index 82ab8a17..560845d7 100644 --- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/YCbCrUpsamplerStream.java +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/YCbCrUpsamplerStream.java @@ -30,30 +30,24 @@ package com.twelvemonkeys.imageio.plugins.tiff; import com.twelvemonkeys.lang.Validate; -import java.awt.image.DataBufferByte; -import java.awt.image.Raster; import java.io.EOFException; import java.io.FilterInputStream; import java.io.IOException; import java.io.InputStream; -import java.util.Arrays; /** - * Input stream that provides on-the-fly conversion and upsampling of TIFF subsampled YCbCr samples to (raw) RGB samples. + * Input stream that provides on-the-fly upsampling of TIFF subsampled YCbCr samples. * * @author Harald Kuhr * @author last modified by $Author: haraldk$ * @version $Id: YCbCrUpsamplerStream.java,v 1.0 31.01.13 09:25 haraldk Exp$ */ final class YCbCrUpsamplerStream extends FilterInputStream { - // NOTE: DO NOT MODIFY OR EXPOSE THIS ARRAY OUTSIDE PACKAGE! - static final double[] CCIR_601_1_COEFFICIENTS = new double[] {299.0 / 1000.0, 587.0 / 1000.0, 114.0 / 1000.0}; private final int horizChromaSub; private final int vertChromaSub; private final int yCbCrPos; private final int columns; - private final double[] coefficients; private final int units; private final int unitSize; @@ -66,7 +60,7 @@ final class YCbCrUpsamplerStream extends FilterInputStream { int bufferLength; int bufferPos; - public YCbCrUpsamplerStream(final InputStream stream, final int[] chromaSub, final int yCbCrPos, final int columns, final double[] coefficients) { + public YCbCrUpsamplerStream(final InputStream stream, final int[] chromaSub, final int yCbCrPos, final int columns) { super(Validate.notNull(stream, "stream")); Validate.notNull(chromaSub, "chromaSub"); @@ -76,7 +70,6 @@ final class YCbCrUpsamplerStream extends FilterInputStream { this.vertChromaSub = chromaSub[1]; this.yCbCrPos = yCbCrPos; this.columns = columns; - this.coefficients = Arrays.equals(CCIR_601_1_COEFFICIENTS, coefficients) ? null : coefficients; // In TIFF, subsampled streams are stored in "units" of horiz * vert pixels. // For a 4:2 subsampled stream like this: @@ -141,14 +134,6 @@ final class YCbCrUpsamplerStream extends FilterInputStream { decodedRows[pixelOff] = buffer[bufferPos++]; decodedRows[pixelOff + 1] = cb; decodedRows[pixelOff + 2] = cr; - - // Convert to RGB - if (coefficients == null) { - YCbCrConverter.convertYCbCr2RGB(decodedRows, decodedRows, pixelOff); - } - else { - convertYCbCr2RGB(decodedRows, decodedRows, coefficients, pixelOff); - } } } @@ -227,120 +212,7 @@ final class YCbCrUpsamplerStream extends FilterInputStream { throw new IOException("mark/reset not supported"); } - private void convertYCbCr2RGB(final byte[] yCbCr, final byte[] rgb, final double[] coefficients, final int offset) { - double y = (yCbCr[offset ] & 0xff); - double cb = (yCbCr[offset + 1] & 0xff) - 128; - double cr = (yCbCr[offset + 2] & 0xff) - 128; - - double lumaRed = coefficients[0]; - double lumaGreen = coefficients[1]; - double lumaBlue = coefficients[2]; - - int red = (int) Math.round(cr * (2 - 2 * lumaRed) + y); - int blue = (int) Math.round(cb * (2 - 2 * lumaBlue) + y); - int green = (int) Math.round((y - lumaRed * red - lumaBlue * blue) / lumaGreen); - - rgb[offset ] = clamp(red); - rgb[offset + 2] = clamp(blue); - rgb[offset + 1] = clamp(green); - } - private static byte clamp(int val) { return (byte) Math.max(0, Math.min(255, val)); } - - // TODO: This code is copied from JPEG package, make it "more" public: com.tm.imageio.color package? - /** - * Static inner class for lazy-loading of conversion tables. - */ - static final class YCbCrConverter { - /** Define tables for YCC->RGB color space conversion. */ - private final static int SCALEBITS = 16; - private final static int MAXJSAMPLE = 255; - private final static int CENTERJSAMPLE = 128; - private final static int ONE_HALF = 1 << (SCALEBITS - 1); - - private final static int[] Cr_R_LUT = new int[MAXJSAMPLE + 1]; - private final static int[] Cb_B_LUT = new int[MAXJSAMPLE + 1]; - private final static int[] Cr_G_LUT = new int[MAXJSAMPLE + 1]; - private final static int[] Cb_G_LUT = new int[MAXJSAMPLE + 1]; - - /** - * Initializes tables for YCC->RGB color space conversion. - */ - private static void buildYCCtoRGBtable() { - if (TIFFImageReader.DEBUG) { - System.err.println("Building YCC conversion table"); - } - - for (int i = 0, x = -CENTERJSAMPLE; i <= MAXJSAMPLE; i++, x++) { - // i is the actual input pixel value, in the range 0..MAXJSAMPLE - // The Cb or Cr value we are thinking of is x = i - CENTERJSAMPLE - // Cr=>R value is nearest int to 1.40200 * x - Cr_R_LUT[i] = (int) ((1.40200 * (1 << SCALEBITS) + 0.5) * x + ONE_HALF) >> SCALEBITS; - // Cb=>B value is nearest int to 1.77200 * x - Cb_B_LUT[i] = (int) ((1.77200 * (1 << SCALEBITS) + 0.5) * x + ONE_HALF) >> SCALEBITS; - // Cr=>G value is scaled-up -0.71414 * x - Cr_G_LUT[i] = -(int) (0.71414 * (1 << SCALEBITS) + 0.5) * x; - // Cb=>G value is scaled-up -0.34414 * x - // We also add in ONE_HALF so that need not do it in inner loop - Cb_G_LUT[i] = -(int) ((0.34414) * (1 << SCALEBITS) + 0.5) * x + ONE_HALF; - } - } - - static { - buildYCCtoRGBtable(); - } - - static void convertYCbCr2RGB(final Raster raster) { - final int height = raster.getHeight(); - final int width = raster.getWidth(); - final byte[] data = ((DataBufferByte) raster.getDataBuffer()).getData(); - - for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) { - convertYCbCr2RGB(data, data, (x + y * width) * 3); - } - } - } - - static void convertYCbCr2RGB(final byte[] yCbCr, final byte[] rgb, final int offset) { - int y = yCbCr[offset ] & 0xff; - int cr = yCbCr[offset + 2] & 0xff; - int cb = yCbCr[offset + 1] & 0xff; - - rgb[offset ] = clamp(y + Cr_R_LUT[cr]); - rgb[offset + 1] = clamp(y + (Cb_G_LUT[cb] + Cr_G_LUT[cr] >> SCALEBITS)); - rgb[offset + 2] = clamp(y + Cb_B_LUT[cb]); - } - - static void convertYCCK2CMYK(final Raster raster) { - final int height = raster.getHeight(); - final int width = raster.getWidth(); - final byte[] data = ((DataBufferByte) raster.getDataBuffer()).getData(); - - for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) { - convertYCCK2CMYK(data, data, (x + y * width) * 4); - } - } - } - - private static void convertYCCK2CMYK(byte[] ycck, byte[] cmyk, int offset) { - // Inverted - int y = 255 - ycck[offset ] & 0xff; - int cb = 255 - ycck[offset + 1] & 0xff; - int cr = 255 - ycck[offset + 2] & 0xff; - int k = 255 - ycck[offset + 3] & 0xff; - - int cmykC = MAXJSAMPLE - (y + Cr_R_LUT[cr]); - int cmykM = MAXJSAMPLE - (y + (Cb_G_LUT[cb] + Cr_G_LUT[cr] >> SCALEBITS)); - int cmykY = MAXJSAMPLE - (y + Cb_B_LUT[cb]); - - cmyk[offset ] = clamp(cmykC); - cmyk[offset + 1] = clamp(cmykM); - cmyk[offset + 2] = clamp(cmykY); - cmyk[offset + 3] = (byte) k; // K passes through unchanged - } - } } diff --git a/imageio/imageio-tiff/src/main/resources/META-INF/services/javax.imageio.spi.ImageWriterSpi b/imageio/imageio-tiff/src/main/resources/META-INF/services/javax.imageio.spi.ImageWriterSpi new file mode 100755 index 00000000..208d610d --- /dev/null +++ b/imageio/imageio-tiff/src/main/resources/META-INF/services/javax.imageio.spi.ImageWriterSpi @@ -0,0 +1 @@ +com.twelvemonkeys.imageio.plugins.tiff.TIFFImageWriterSpi diff --git a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxDecoderStreamTest.java b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxDecoderStreamTest.java index 8c735d82..599d26f7 100644 --- a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxDecoderStreamTest.java +++ b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxDecoderStreamTest.java @@ -33,41 +33,74 @@ import org.junit.Test; import java.awt.image.BufferedImage; import java.awt.image.DataBufferByte; -import java.io.ByteArrayInputStream; -import java.io.DataInputStream; -import java.io.IOException; -import java.io.InputStream; +import java.io.*; +import java.util.Arrays; -import static org.junit.Assert.*; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; /** * CCITTFaxDecoderStreamTest * * @author Harald Kuhr * @author last modified by $Author: haraldk$ - * @version $Id: CCITTFaxDecoderStreamTest.java,v 1.0 09.03.13 14:44 haraldk Exp$ + * @version $Id: CCITTFaxDecoderStreamTest.java,v 1.0 09.03.13 14:44 haraldk + * Exp$ */ public class CCITTFaxDecoderStreamTest { + // group3_1d.tif: EOL|3W|1B|2W|EOL|3W|1B|2W|EOL|3W|1B|2W|EOL|2W|2B|2W|5*F + static final byte[] DATA_G3_1D = { 0x00, 0x18, 0x4E, 0x00, 0x30, (byte) 0x9C, 0x00, 0x61, 0x38, 0x00, (byte) 0xBE, + (byte) 0xE0 }; + + // group3_1d_fill.tif + static final byte[] DATA_G3_1D_FILL = { 0x00, 0x01, (byte) 0x84, (byte) 0xE0, 0x01, (byte) 0x84, (byte) 0xE0, 0x01, + (byte) 0x84, (byte) 0xE0, 0x1, 0x7D, (byte) 0xC0 }; + + // group3_2d.tif: EOL|k=1|3W|1B|2W|EOL|k=0|V|V|V|EOL|k=1|3W|1B|2W|EOL|k=0|V-1|V|V|6*F + static final byte[] DATA_G3_2D = { 0x00, 0x1C, 0x27, 0x00, 0x17, 0x00, 0x1C, 0x27, 0x00, 0x12, (byte) 0xC0 }; + + // group3_2d_fill.tif + static final byte[] DATA_G3_2D_FILL = { 0x00, 0x01, (byte) 0xC2, 0x70, 0x01, 0x70, 0x01, (byte) 0xC2, 0x70, 0x01, + 0x2C }; + + static final byte[] DATA_G3_2D_lsb2msb = { 0x00, 0x38, (byte) 0xE4, 0x00, (byte) 0xE8, 0x00, 0x38, (byte) 0xE4, + 0x00, 0x48, 0x03 }; + + // group4.tif: + // Line 1: V-3, V-2, V0 + // Line 2: V0 V0 V0 + // Line 3: V0 V0 V0 + // Line 4: V-1, V0, V0 EOL EOL + static final byte[] DATA_G4 = { 0x04, 0x17, (byte) 0xF5, (byte) 0x80, 0x08, 0x00, (byte) 0x80 }; + // TODO: Better tests (full A4 width scan lines?) // From http://www.mikekohn.net/file_formats/tiff.php - static final byte[] DATA_TYPE_2 = { - (byte) 0x84, (byte) 0xe0, // 10000100 11100000 + static final byte[] DATA_TYPE_2 = { (byte) 0x84, (byte) 0xe0, // 10000100 + // 11100000 (byte) 0x84, (byte) 0xe0, // 10000100 11100000 (byte) 0x84, (byte) 0xe0, // 10000100 11100000 (byte) 0x7d, (byte) 0xc0, // 01111101 11000000 }; - static final byte[] DATA_TYPE_3 = { - 0x00, 0x01, (byte) 0xc2, 0x70, - 0x00, 0x01, 0x70, - 0x01, + static final byte[] DATA_TYPE_3 = { 0x00, 0x01, (byte) 0xc2, 0x70, // 00000000 + // 00000001 + // 11000010 + // 01110000 + 0x00, 0x01, 0x78, // 00000000 00000001 01111000 + 0x00, 0x01, 0x78, // 00000000 00000001 01110000 + 0x00, 0x01, 0x56, // 00000000 00000001 01010110 + // 0x01, // 00000001 }; - static final byte[] DATA_TYPE_4 = { - 0x26, (byte) 0xb0, 95, (byte) 0xfa, (byte) 0xc0 + // 001 00110101 10 000010 1 1 1 1 1 1 1 1 1 1 010 11 (000000 padding) + static final byte[] DATA_TYPE_4 = { 0x26, // 001 00110 + (byte) 0xb0, // 101 10 000 + 0x5f, // 010 1 1 1 1 1 + (byte) 0xfa, // 1 1 1 1 1 010 + (byte) 0xc0 // 11 (000000 padding) }; // Image should be (6 x 4): @@ -75,92 +108,139 @@ public class CCITTFaxDecoderStreamTest { // 1 1 1 0 1 1 x x // 1 1 1 0 1 1 x x // 1 1 0 0 1 1 x x - BufferedImage image; + final BufferedImage image = new BufferedImage(6, 4, BufferedImage.TYPE_BYTE_BINARY);; @Before public void init() { - image = new BufferedImage(6, 4, BufferedImage.TYPE_BYTE_BINARY); + for (int y = 0; y < 4; y++) { for (int x = 0; x < 6; x++) { - image.setRGB(x, y, x == 3 ? 0xff000000 : 0xffffffff); + image.setRGB(x, y, x != 3 ? 0xff000000 : 0xffffffff); } } - image.setRGB(2, 3, 0xff000000); - } - - @Test - public void testReadCountType2() throws IOException { - InputStream stream = new CCITTFaxDecoderStream(new ByteArrayInputStream(DATA_TYPE_2), 6, TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE, 1); - - int count = 0; - int read; - while ((read = stream.read()) >= 0) { - count++; - } - - // Just make sure we'll have 4 bytes - assertEquals(4, count); - - // Verify that we don't return arbitrary values - assertEquals(-1, read); + image.setRGB(2, 3, 0xffffffff); } @Test public void testDecodeType2() throws IOException { - InputStream stream = new CCITTFaxDecoderStream(new ByteArrayInputStream(DATA_TYPE_2), 6, TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE, 1); + InputStream stream = new CCITTFaxDecoderStream(new ByteArrayInputStream(DATA_TYPE_2), 6, + TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE, 1, 0L); byte[] imageData = ((DataBufferByte) image.getData().getDataBuffer()).getData(); byte[] bytes = new byte[imageData.length]; new DataInputStream(stream).readFully(bytes); - -// JPanel panel = new JPanel(); -// panel.add(new JLabel("Expected", new BufferedImageIcon(image, 300, 300, true), JLabel.CENTER)); -// panel.add(new JLabel("Actual", new BufferedImageIcon(new BufferedImage(image.getColorModel(), Raster.createPackedRaster(new DataBufferByte(bytes, bytes.length), 6, 4, 1, null), false, null), 300, 300, true), JLabel.CENTER)); -// JOptionPane.showConfirmDialog(null, panel); - assertArrayEquals(imageData, bytes); } - @Test(expected = IllegalArgumentException.class) - public void testDecodeType3() throws IOException { - InputStream stream = new CCITTFaxDecoderStream(new ByteArrayInputStream(DATA_TYPE_3), 6, TIFFExtension.COMPRESSION_CCITT_T4, 1); + @Test + public void testDecodeType3_1D() throws IOException { + InputStream stream = new CCITTFaxDecoderStream(new ByteArrayInputStream(DATA_G3_1D), 6, + TIFFExtension.COMPRESSION_CCITT_T4, 1, 0L); byte[] imageData = ((DataBufferByte) image.getData().getDataBuffer()).getData(); byte[] bytes = new byte[imageData.length]; - DataInputStream dataInput = new DataInputStream(stream); - - for (int y = 0; y < image.getHeight(); y++) { - System.err.println("y: " + y); - dataInput.readFully(bytes, y * image.getWidth(), image.getWidth()); - } - -// JPanel panel = new JPanel(); -// panel.add(new JLabel("Expected", new BufferedImageIcon(image, 300, 300, true), JLabel.CENTER)); -// panel.add(new JLabel("Actual", new BufferedImageIcon(new BufferedImage(image.getColorModel(), Raster.createPackedRaster(new DataBufferByte(bytes, bytes.length), 6, 4, 1, null), false, null), 300, 300, true), JLabel.CENTER)); -// JOptionPane.showConfirmDialog(null, panel); - + new DataInputStream(stream).readFully(bytes); assertArrayEquals(imageData, bytes); } - @Test(expected = IllegalArgumentException.class) + @Test + public void testDecodeType3_1D_FILL() throws IOException { + InputStream stream = new CCITTFaxDecoderStream(new ByteArrayInputStream(DATA_G3_1D_FILL), 6, + TIFFExtension.COMPRESSION_CCITT_T4, 1, TIFFExtension.GROUP3OPT_FILLBITS); + + byte[] imageData = ((DataBufferByte) image.getData().getDataBuffer()).getData(); + byte[] bytes = new byte[imageData.length]; + new DataInputStream(stream).readFully(bytes); + assertArrayEquals(imageData, bytes); + } + + @Test + public void testDecodeType3_2D() throws IOException { + InputStream stream = new CCITTFaxDecoderStream(new ByteArrayInputStream(DATA_G3_2D), 6, + TIFFExtension.COMPRESSION_CCITT_T4, 1, TIFFExtension.GROUP3OPT_2DENCODING); + + byte[] imageData = ((DataBufferByte) image.getData().getDataBuffer()).getData(); + byte[] bytes = new byte[imageData.length]; + new DataInputStream(stream).readFully(bytes); + assertArrayEquals(imageData, bytes); + } + + @Test + public void testDecodeType3_2D_FILL() throws IOException { + InputStream stream = new CCITTFaxDecoderStream(new ByteArrayInputStream(DATA_G3_2D_FILL), 6, + TIFFExtension.COMPRESSION_CCITT_T4, 1, + TIFFExtension.GROUP3OPT_2DENCODING | TIFFExtension.GROUP3OPT_FILLBITS); + + byte[] imageData = ((DataBufferByte) image.getData().getDataBuffer()).getData(); + byte[] bytes = new byte[imageData.length]; + new DataInputStream(stream).readFully(bytes); + assertArrayEquals(imageData, bytes); + } + + @Test + public void testDecodeType3_2D_REVERSED() throws IOException { + InputStream stream = new CCITTFaxDecoderStream(new ByteArrayInputStream(DATA_G3_2D_lsb2msb), 6, + TIFFExtension.COMPRESSION_CCITT_T4, 2, TIFFExtension.GROUP3OPT_2DENCODING); + + byte[] imageData = ((DataBufferByte) image.getData().getDataBuffer()).getData(); + byte[] bytes = new byte[imageData.length]; + new DataInputStream(stream).readFully(bytes); + assertArrayEquals(imageData, bytes); + } + + @Test public void testDecodeType4() throws IOException { - InputStream stream = new CCITTFaxDecoderStream(new ByteArrayInputStream(DATA_TYPE_4), 6, TIFFExtension.COMPRESSION_CCITT_T6, 1); + InputStream stream = new CCITTFaxDecoderStream(new ByteArrayInputStream(DATA_G4), 6, + TIFFExtension.COMPRESSION_CCITT_T6, 1, 0L); byte[] imageData = ((DataBufferByte) image.getData().getDataBuffer()).getData(); byte[] bytes = new byte[imageData.length]; - DataInputStream dataInput = new DataInputStream(stream); + new DataInputStream(stream).readFully(bytes); + assertArrayEquals(imageData, bytes); + } - for (int y = 0; y < image.getHeight(); y++) { - System.err.println("y: " + y); - dataInput.readFully(bytes, y * image.getWidth(), image.getWidth()); + @Test + public void testDecodeMissingRows() throws IOException { + // See https://github.com/haraldk/TwelveMonkeys/pull/225 and https://github.com/haraldk/TwelveMonkeys/issues/232 + InputStream inputStream = getClass().getResourceAsStream("/tiff/ccitt_tolessrows.tif"); + + // Skip until StripOffsets: 8 + for (int i = 0; i < 8; i++) { + inputStream.read(); } -// JPanel panel = new JPanel(); -// panel.add(new JLabel("Expected", new BufferedImageIcon(image, 300, 300, true), JLabel.CENTER)); -// panel.add(new JLabel("Actual", new BufferedImageIcon(new BufferedImage(image.getColorModel(), Raster.createPackedRaster(new DataBufferByte(bytes, bytes.length), 6, 4, 1, null), false, null), 300, 300, true), JLabel.CENTER)); -// JOptionPane.showConfirmDialog(null, panel); + // Read until StripByteCounts: 7 + byte[] data = new byte[7]; + new DataInputStream(inputStream).readFully(data); + InputStream stream = new CCITTFaxDecoderStream(new ByteArrayInputStream(data), + 6, TIFFExtension.COMPRESSION_CCITT_T6, 1, 0L); + + byte[] bytes = new byte[6]; // 6 x 6 pixel, 1 bpp => 6 bytes + new DataInputStream(stream).readFully(bytes); + + // Pad image data with 0s + byte[] imageData = Arrays.copyOf(((DataBufferByte) image.getData().getDataBuffer()).getData(), 6); assertArrayEquals(imageData, bytes); + + // Ideally, we should have no more data now, but the stream don't know that... + // assertEquals("Should contain no more data", -1, stream.read()); + } + + @Test + public void testMoreChangesThanColumns() throws IOException { + // Produces an CCITT Stream with 9 changes on 8 columns. + byte[] data = new byte[] {(byte) 0b10101010}; + ByteArrayOutputStream imageOutput = new ByteArrayOutputStream(); + OutputStream outputSteam = new CCITTFaxEncoderStream(imageOutput, 8, 1, TIFFExtension.COMPRESSION_CCITT_T6, 1, 0L); + outputSteam.write(data); + outputSteam.close(); + + byte[] encoded = imageOutput.toByteArray(); + InputStream inputStream = new CCITTFaxDecoderStream(new ByteArrayInputStream(encoded), 8, + TIFFExtension.COMPRESSION_CCITT_T6, 1, 0L); + byte decoded = (byte) inputStream.read(); + assertEquals(data[0], decoded); } } diff --git a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxEncoderStreamTest.java b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxEncoderStreamTest.java new file mode 100644 index 00000000..a9eb0599 --- /dev/null +++ b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxEncoderStreamTest.java @@ -0,0 +1,193 @@ +/* + * Copyright (c) 2013, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.imageio.plugins.tiff; + +import com.twelvemonkeys.imageio.plugins.tiff.CCITTFaxEncoderStream.Code; +import org.junit.Before; +import org.junit.Test; + +import javax.imageio.IIOImage; +import javax.imageio.ImageIO; +import javax.imageio.ImageReader; +import javax.imageio.ImageWriter; +import javax.imageio.stream.ImageInputStream; +import javax.imageio.stream.ImageOutputStream; +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferByte; +import java.io.*; +import java.net.URL; +import java.util.Arrays; + +import static org.junit.Assert.*; + +/** + * CCITTFaxEncoderStreamTest + * + * @author Oliver Schmidtmer + * @author last modified by $Author$ + * @version $Id$ + */ +public class CCITTFaxEncoderStreamTest { + + // Image should be (6 x 4): + // 1 1 1 0 1 1 x x + // 1 1 1 0 1 1 x x + // 1 1 1 0 1 1 x x + // 1 1 0 0 1 1 x x + BufferedImage image; + + @Before + public void init() { + image = new BufferedImage(6, 4, BufferedImage.TYPE_BYTE_BINARY); + for (int y = 0; y < 4; y++) { + for (int x = 0; x < 6; x++) { + image.setRGB(x, y, x != 3 ? 0xff000000 : 0xffffffff); + } + } + + image.setRGB(2, 3, 0xffffffff); + } + + @Test + public void testBuildCodes() throws IOException { + assertTrue(CCITTFaxEncoderStream.WHITE_TERMINATING_CODES.length == 64); + for (Code code : CCITTFaxEncoderStream.WHITE_TERMINATING_CODES) { + assertNotNull(code); + } + assertTrue(CCITTFaxEncoderStream.WHITE_NONTERMINATING_CODES.length == 40); + for (Code code : CCITTFaxEncoderStream.WHITE_NONTERMINATING_CODES) { + assertNotNull(code); + } + assertTrue(CCITTFaxEncoderStream.BLACK_TERMINATING_CODES.length == 64); + for (Code code : CCITTFaxEncoderStream.BLACK_TERMINATING_CODES) { + assertNotNull(code); + } + assertTrue(CCITTFaxEncoderStream.BLACK_NONTERMINATING_CODES.length == 40); + for (Code code : CCITTFaxEncoderStream.BLACK_NONTERMINATING_CODES) { + assertNotNull(code); + } + } + + @Test + public void testType2() throws IOException { + testStreamEncodeDecode(TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE, 1, 0L); + } + + @Test + public void testType4() throws IOException { + testStreamEncodeDecode(TIFFExtension.COMPRESSION_CCITT_T4, 1, 0L); + testStreamEncodeDecode(TIFFExtension.COMPRESSION_CCITT_T4, 1, TIFFExtension.GROUP3OPT_FILLBITS); + testStreamEncodeDecode(TIFFExtension.COMPRESSION_CCITT_T4, 1, TIFFExtension.GROUP3OPT_2DENCODING); + testStreamEncodeDecode(TIFFExtension.COMPRESSION_CCITT_T4, 1, + TIFFExtension.GROUP3OPT_FILLBITS | TIFFExtension.GROUP3OPT_2DENCODING); + } + + @Test + public void testType6() throws IOException { + testStreamEncodeDecode(TIFFExtension.COMPRESSION_CCITT_T6, 1, 0L); + } + + @Test + public void testReversedFillOrder() throws IOException { + testStreamEncodeDecode(TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE, 2, 0L); + testStreamEncodeDecode(TIFFExtension.COMPRESSION_CCITT_T6, 2, 0L); + } + + @Test + public void testReencodeImages() throws IOException { + try (ImageInputStream iis = ImageIO.createImageInputStream(getClassLoaderResource("/tiff/fivepages-scan-causingerrors.tif").openStream())) { + ImageReader reader = ImageIO.getImageReaders(iis).next(); + reader.setInput(iis, true); + + ByteArrayOutputStream outputBuffer = new ByteArrayOutputStream(); + ImageWriter writer = ImageIO.getImageWritersByFormatName("TIFF").next(); + BufferedImage originalImage; + + try (ImageOutputStream output = ImageIO.createImageOutputStream(outputBuffer)) { + writer.setOutput(output); + originalImage = reader.read(0); + + IIOImage outputImage = new IIOImage(originalImage, null, reader.getImageMetadata(0)); + writer.write(outputImage); + } + + byte[] originalData = ((DataBufferByte) originalImage.getData().getDataBuffer()).getData(); + + BufferedImage reencodedImage = ImageIO.read(new ByteArrayInputStream(outputBuffer.toByteArray())); + byte[] reencodedData = ((DataBufferByte) reencodedImage.getData().getDataBuffer()).getData(); + + assertArrayEquals(originalData, reencodedData); + } + } + + @Test + public void testRunlengthIssue() throws IOException { + // Test for "Fixed an issue with long runlengths in CCITTFax writing #188" + byte[] data = new byte[400]; + Arrays.fill(data, (byte) 0xFF); + data[0] = 0; + data[399] = 0; + + ByteArrayOutputStream imageOutput = new ByteArrayOutputStream(); + OutputStream outputSteam = new CCITTFaxEncoderStream(imageOutput, 3200, 1, TIFFExtension.COMPRESSION_CCITT_T6, 1, 0L); + outputSteam.write(data); + outputSteam.close(); + byte[] encodedData = imageOutput.toByteArray(); + + byte[] decodedData = new byte[data.length]; + CCITTFaxDecoderStream inputStream = new CCITTFaxDecoderStream(new ByteArrayInputStream(encodedData), 3200, TIFFExtension.COMPRESSION_CCITT_T6, 1, 0L); + new DataInputStream(inputStream).readFully(decodedData); + inputStream.close(); + + assertArrayEquals(data, decodedData); + } + + protected URL getClassLoaderResource(final String pName) { + return getClass().getResource(pName); + } + + private void testStreamEncodeDecode(int type, int fillOrder, long options) throws IOException { + byte[] imageData = ((DataBufferByte) image.getData().getDataBuffer()).getData(); + byte[] redecodedData = new byte[imageData.length]; + + ByteArrayOutputStream imageOutput = new ByteArrayOutputStream(); + OutputStream outputSteam = new CCITTFaxEncoderStream(imageOutput, 6, 4, type, fillOrder, options); + outputSteam.write(imageData); + outputSteam.close(); + byte[] encodedData = imageOutput.toByteArray(); + + try (CCITTFaxDecoderStream inputStream = + new CCITTFaxDecoderStream(new ByteArrayInputStream(encodedData), 6, type, fillOrder, options)) { + new DataInputStream(inputStream).readFully(redecodedData); + } + + assertArrayEquals(imageData, redecodedData); + } + +} diff --git a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageMetadataTest.java b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageMetadataTest.java new file mode 100644 index 00000000..242044d0 --- /dev/null +++ b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageMetadataTest.java @@ -0,0 +1,659 @@ +package com.twelvemonkeys.imageio.plugins.tiff; + +import com.twelvemonkeys.imageio.metadata.Directory; +import com.twelvemonkeys.imageio.metadata.Entry; +import com.twelvemonkeys.imageio.metadata.exif.EXIFReader; +import com.twelvemonkeys.imageio.metadata.exif.Rational; +import com.twelvemonkeys.imageio.metadata.exif.TIFF; +import com.twelvemonkeys.imageio.stream.URLImageInputStreamSpi; +import com.twelvemonkeys.lang.StringUtil; +import org.junit.Test; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import javax.imageio.ImageIO; +import javax.imageio.metadata.IIOInvalidTreeException; +import javax.imageio.metadata.IIOMetadata; +import javax.imageio.metadata.IIOMetadataFormatImpl; +import javax.imageio.metadata.IIOMetadataNode; +import javax.imageio.spi.IIORegistry; +import javax.imageio.stream.ImageInputStream; +import java.io.IOException; +import java.net.URL; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import static org.junit.Assert.*; + +/** + * TIFFImageMetadataTest. + * + * @author Harald Kuhr + * @author last modified by $Author: harald.kuhr$ + * @version $Id: TIFFImageMetadataTest.java,v 1.0 30/07/15 harald.kuhr Exp$ + */ +public class TIFFImageMetadataTest { + + static { + IIORegistry.getDefaultInstance().registerServiceProvider(new URLImageInputStreamSpi()); + ImageIO.setUseCache(false); + } + + // TODO: Candidate super method + private URL getClassLoaderResource(final String resource) { + return getClass().getResource(resource); + } + + // TODO: Candidate abstract super method + private IIOMetadata createMetadata(final String resource) throws IOException { + try (ImageInputStream input = ImageIO.createImageInputStream(getClassLoaderResource(resource))) { + Directory ifd = new EXIFReader().read(input); +// System.err.println("ifd: " + ifd); + return new TIFFImageMetadata(ifd); + } + } + + @Test + public void testMetadataStandardFormat() throws IOException { + IIOMetadata metadata = createMetadata("/tiff/smallliz.tif"); + Node root = metadata.getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName); + + // Root: "javax_imageio_1.0" + assertNotNull(root); + assertEquals(IIOMetadataFormatImpl.standardMetadataFormatName, root.getNodeName()); + assertEquals(6, root.getChildNodes().getLength()); + + // "Chroma" + Node chroma = root.getFirstChild(); + assertEquals("Chroma", chroma.getNodeName()); + + assertEquals(3, chroma.getChildNodes().getLength()); + + Node colorSpaceType = chroma.getFirstChild(); + assertEquals("ColorSpaceType", colorSpaceType.getNodeName()); + assertEquals("YCbCr", ((Element) colorSpaceType).getAttribute("value")); + + Node numChannels = colorSpaceType.getNextSibling(); + assertEquals("NumChannels", numChannels.getNodeName()); + assertEquals("3", ((Element) numChannels).getAttribute("value")); + + Node blackIsZero = numChannels.getNextSibling(); + assertEquals("BlackIsZero", blackIsZero.getNodeName()); + assertEquals(0, blackIsZero.getAttributes().getLength()); + + // "Compression" + Node compression = chroma.getNextSibling(); + assertEquals("Compression", compression.getNodeName()); + assertEquals(2, compression.getChildNodes().getLength()); + + Node compressionTypeName = compression.getFirstChild(); + assertEquals("CompressionTypeName", compressionTypeName.getNodeName()); + assertEquals("Old JPEG", ((Element) compressionTypeName).getAttribute("value")); + + Node lossless = compressionTypeName.getNextSibling(); + assertEquals("Lossless", lossless.getNodeName()); + assertEquals("FALSE", ((Element) lossless).getAttribute("value")); + + // "Data" + Node data = compression.getNextSibling(); + assertEquals("Data", data.getNodeName()); + assertEquals(4, data.getChildNodes().getLength()); + + Node planarConfiguration = data.getFirstChild(); + assertEquals("PlanarConfiguration", planarConfiguration.getNodeName()); + assertEquals("PixelInterleaved", ((Element) planarConfiguration).getAttribute("value")); + + Node sampleFormat = planarConfiguration.getNextSibling(); + assertEquals("SampleFormat", sampleFormat.getNodeName()); + assertEquals("UnsignedIntegral", ((Element) sampleFormat).getAttribute("value")); + + Node bitsPerSample = sampleFormat.getNextSibling(); + assertEquals("BitsPerSample", bitsPerSample.getNodeName()); + assertEquals("8 8 8", ((Element) bitsPerSample).getAttribute("value")); + + Node sampleMSB = bitsPerSample.getNextSibling(); + assertEquals("SampleMSB", sampleMSB.getNodeName()); + assertEquals("0 0 0", ((Element) sampleMSB).getAttribute("value")); + + // "Dimension" + Node dimension = data.getNextSibling(); + assertEquals("Dimension", dimension.getNodeName()); + assertEquals(3, dimension.getChildNodes().getLength()); + + Node pixelAspectRatio = dimension.getFirstChild(); + assertEquals("PixelAspectRatio", pixelAspectRatio.getNodeName()); + assertEquals("1.0", ((Element) pixelAspectRatio).getAttribute("value")); + + Node horizontalPixelSize = pixelAspectRatio.getNextSibling(); + assertEquals("HorizontalPixelSize", horizontalPixelSize.getNodeName()); + assertEquals("0.254", ((Element) horizontalPixelSize).getAttribute("value")); + + Node verticalPixelSize = horizontalPixelSize.getNextSibling(); + assertEquals("VerticalPixelSize", verticalPixelSize.getNodeName()); + assertEquals("0.254", ((Element) verticalPixelSize).getAttribute("value")); + + // "Document" + Node document = dimension.getNextSibling(); + assertEquals("Document", document.getNodeName()); + assertEquals(1, document.getChildNodes().getLength()); + + Node formatVersion = document.getFirstChild(); + assertEquals("FormatVersion", formatVersion.getNodeName()); + assertEquals("6.0", ((Element) formatVersion).getAttribute("value")); + + // "Text" + Node text = document.getNextSibling(); + assertEquals("Text", text.getNodeName()); + assertEquals(1, text.getChildNodes().getLength()); + + // NOTE: Could be multiple "TextEntry" elements, with different "keyword" attributes + Node textEntry = text.getFirstChild(); + assertEquals("TextEntry", textEntry.getNodeName()); + assertEquals("Software", ((Element) textEntry).getAttribute("keyword")); + assertEquals("HP IL v1.1", ((Element) textEntry).getAttribute("value")); + } + + @Test + public void testMetadataNativeFormat() throws IOException { + IIOMetadata metadata = createMetadata("/tiff/quad-lzw.tif"); + Node root = metadata.getAsTree(TIFFMedataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME); + + // Root: "com_sun_media_imageio_plugins_tiff_image_1.0" + assertNotNull(root); + assertEquals(TIFFMedataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME, root.getNodeName()); + assertEquals(1, root.getChildNodes().getLength()); + + // IFD: "TIFFIFD" + Node ifd = root.getFirstChild(); + assertEquals("TIFFIFD", ifd.getNodeName()); + + NodeList entries = ifd.getChildNodes(); + assertEquals(13, entries.getLength()); + + String[] stripOffsets = { + "8", "150", "292", "434", "576", "718", "860", "1002", "1144", "1286", + "1793", "3823", "7580", "12225", "17737", "23978", "30534", "36863", "42975", "49180", + "55361", "61470", "67022", "71646", "74255", "75241", "75411", "75553", "75695", "75837", + "75979", "76316", "77899", "80466", "84068", "88471", "93623", "99105", "104483", "109663", + "114969", "120472", "126083", "131289", "135545", "138810", "140808", "141840", "141982", "142124", + "142266", "142408", "142615", "144074", "146327", "149721", "154066", "158927", "164022", "169217", + "174409", "179657", "185166", "190684", "196236", "201560", "206064", "209497", "211612", "212419", + "212561", "212703", "212845", "212987", "213129", "213271", "213413" + }; + + String[] stripByteCounts = { + "142", "142", "142", "142", "142", "142", "142", "142", "142", "507", + "2030", "3757", "4645", "5512", "6241", "6556", "6329", "6112", "6205", "6181", + "6109", "5552", "4624", "2609", "986", "170", "142", "142", "142", "142", + "337", "1583", "2567", "3602", "4403", "5152", "5482", "5378", "5180", "5306", + "5503", "5611", "5206", "4256", "3265", "1998", "1032", "142", "142", "142", + "142", "207", "1459", "2253", "3394", "4345", "4861", "5095", "5195", "5192", + "5248", "5509", "5518", "5552", "5324", "4504", "3433", "2115", "807", "142", + "142", "142", "142", "142", "142", "142", "128" + }; + + // The 13 entries + assertSingleNodeWithValue(entries, TIFF.TAG_IMAGE_WIDTH, TIFF.TYPE_SHORT, "512"); + assertSingleNodeWithValue(entries, TIFF.TAG_IMAGE_HEIGHT, TIFF.TYPE_SHORT, "384"); + assertSingleNodeWithValue(entries, TIFF.TAG_BITS_PER_SAMPLE, TIFF.TYPE_SHORT, "8", "8", "8"); + assertSingleNodeWithValue(entries, TIFF.TAG_COMPRESSION, TIFF.TYPE_SHORT, "5"); + assertSingleNodeWithValue(entries, TIFF.TAG_PHOTOMETRIC_INTERPRETATION, TIFF.TYPE_SHORT, "2"); + assertSingleNodeWithValue(entries, TIFF.TAG_STRIP_OFFSETS, TIFF.TYPE_LONG, stripOffsets); + assertSingleNodeWithValue(entries, TIFF.TAG_SAMPLES_PER_PIXEL, TIFF.TYPE_SHORT, "3"); + assertSingleNodeWithValue(entries, TIFF.TAG_ROWS_PER_STRIP, TIFF.TYPE_LONG, "5"); + assertSingleNodeWithValue(entries, TIFF.TAG_STRIP_BYTE_COUNTS, TIFF.TYPE_LONG, stripByteCounts); + assertSingleNodeWithValue(entries, TIFF.TAG_PLANAR_CONFIGURATION, TIFF.TYPE_SHORT, "1"); + assertSingleNodeWithValue(entries, TIFF.TAG_X_POSITION, TIFF.TYPE_RATIONAL, "0"); + assertSingleNodeWithValue(entries, TIFF.TAG_Y_POSITION, TIFF.TYPE_RATIONAL, "0"); + assertSingleNodeWithValue(entries, 32995, TIFF.TYPE_SHORT, "0"); // Matteing tag, obsoleted by ExtraSamples tag in TIFF 6.0 + } + + @Test + public void testTreeDetached() throws IOException { + IIOMetadata metadata = createMetadata("/tiff/sm_colors_tile.tif"); + + Node nativeTree = metadata.getAsTree(TIFFMedataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME); + assertNotNull(nativeTree); + + Node nativeTree2 = metadata.getAsTree(TIFFMedataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME); + assertNotNull(nativeTree2); + + assertNotSame(nativeTree, nativeTree2); + assertNodeEquals("Unmodified trees differs", nativeTree, nativeTree2); // Both not modified + + // Modify one of the trees + Node ifdNode = nativeTree2.getFirstChild(); + ifdNode.removeChild(ifdNode.getFirstChild()); + IIOMetadataNode tiffField = new IIOMetadataNode("TIFFField"); + ifdNode.appendChild(tiffField); + + assertNodeNotEquals("Modified tree does not differ", nativeTree, nativeTree2); + } + + @Test + public void testMergeTree() throws IOException { + TIFFImageMetadata metadata = (TIFFImageMetadata) createMetadata("/tiff/sm_colors_tile.tif"); + + String nativeFormat = TIFFMedataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME; + + Node nativeTree = metadata.getAsTree(nativeFormat); + assertNotNull(nativeTree); + + IIOMetadataNode newTree = new IIOMetadataNode("com_sun_media_imageio_plugins_tiff_image_1.0"); + IIOMetadataNode ifdNode = new IIOMetadataNode("TIFFIFD"); + newTree.appendChild(ifdNode); + + createTIFFFieldNode(ifdNode, TIFF.TAG_RESOLUTION_UNIT, TIFF.TYPE_SHORT, TIFFBaseline.RESOLUTION_UNIT_DPI); + createTIFFFieldNode(ifdNode, TIFF.TAG_X_RESOLUTION, TIFF.TYPE_RATIONAL, new Rational(300)); + createTIFFFieldNode(ifdNode, TIFF.TAG_Y_RESOLUTION, TIFF.TYPE_RATIONAL, new Rational(30001, 100)); + + metadata.mergeTree(nativeFormat, newTree); + + Directory ifd = metadata.getIFD(); + + assertNotNull(ifd.getEntryById(TIFF.TAG_X_RESOLUTION)); + assertEquals(new Rational(300), ifd.getEntryById(TIFF.TAG_X_RESOLUTION).getValue()); + assertNotNull(ifd.getEntryById(TIFF.TAG_Y_RESOLUTION)); + assertEquals(new Rational(30001, 100), ifd.getEntryById(TIFF.TAG_Y_RESOLUTION).getValue()); + assertNotNull(ifd.getEntryById(TIFF.TAG_RESOLUTION_UNIT)); + assertEquals(TIFFBaseline.RESOLUTION_UNIT_DPI, ((Number) ifd.getEntryById(TIFF.TAG_RESOLUTION_UNIT).getValue()).intValue()); + + Node mergedTree = metadata.getAsTree(nativeFormat); + NodeList fields = mergedTree.getFirstChild().getChildNodes(); + + // Validate there's one and only one resolution unit, x res and y res + // Validate resolution unit == 1, x res & y res + assertSingleNodeWithValue(fields, TIFF.TAG_RESOLUTION_UNIT, TIFF.TYPE_SHORT, String.valueOf(TIFFBaseline.RESOLUTION_UNIT_DPI)); + assertSingleNodeWithValue(fields, TIFF.TAG_X_RESOLUTION, TIFF.TYPE_RATIONAL, "300"); + assertSingleNodeWithValue(fields, TIFF.TAG_Y_RESOLUTION, TIFF.TYPE_RATIONAL, "30001/100"); + } + + @Test + public void testMergeTreeStandardFormat() throws IOException { + TIFFImageMetadata metadata = (TIFFImageMetadata) createMetadata("/tiff/zackthecat.tif"); + + String standardFormat = IIOMetadataFormatImpl.standardMetadataFormatName; + + Node standardTree = metadata.getAsTree(standardFormat); + assertNotNull(standardTree); + + IIOMetadataNode newTree = new IIOMetadataNode(standardFormat); + IIOMetadataNode dimensionNode = new IIOMetadataNode("Dimension"); + newTree.appendChild(dimensionNode); + + IIOMetadataNode horizontalPixelSize = new IIOMetadataNode("HorizontalPixelSize"); + dimensionNode.appendChild(horizontalPixelSize); + horizontalPixelSize.setAttribute("value", String.valueOf(300 / 25.4)); + + IIOMetadataNode verticalPixelSize = new IIOMetadataNode("VerticalPixelSize"); + dimensionNode.appendChild(verticalPixelSize); + verticalPixelSize.setAttribute("value", String.valueOf(300 / 25.4)); + + metadata.mergeTree(standardFormat, newTree); + + Directory ifd = metadata.getIFD(); + + assertNotNull(ifd.getEntryById(TIFF.TAG_X_RESOLUTION)); + assertEquals(new Rational(300), ifd.getEntryById(TIFF.TAG_X_RESOLUTION).getValue()); + assertNotNull(ifd.getEntryById(TIFF.TAG_Y_RESOLUTION)); + assertEquals(new Rational(300), ifd.getEntryById(TIFF.TAG_Y_RESOLUTION).getValue()); + + // Should keep DPI as unit + assertNotNull(ifd.getEntryById(TIFF.TAG_RESOLUTION_UNIT)); + assertEquals(TIFFBaseline.RESOLUTION_UNIT_DPI, ((Number) ifd.getEntryById(TIFF.TAG_RESOLUTION_UNIT).getValue()).intValue()); + } + + @Test + public void testMergeTreeStandardFormatAspectOnly() throws IOException { + TIFFImageMetadata metadata = (TIFFImageMetadata) createMetadata("/tiff/sm_colors_tile.tif"); + + String standardFormat = IIOMetadataFormatImpl.standardMetadataFormatName; + + Node standardTree = metadata.getAsTree(standardFormat); + assertNotNull(standardTree); + + IIOMetadataNode newTree = new IIOMetadataNode(standardFormat); + IIOMetadataNode dimensionNode = new IIOMetadataNode("Dimension"); + newTree.appendChild(dimensionNode); + + IIOMetadataNode aspectRatio = new IIOMetadataNode("PixelAspectRatio"); + dimensionNode.appendChild(aspectRatio); + aspectRatio.setAttribute("value", String.valueOf(3f / 2f)); + + metadata.mergeTree(standardFormat, newTree); + + Directory ifd = metadata.getIFD(); + + assertNotNull(ifd.getEntryById(TIFF.TAG_X_RESOLUTION)); + assertEquals(new Rational(3, 2), ifd.getEntryById(TIFF.TAG_X_RESOLUTION).getValue()); + assertNotNull(ifd.getEntryById(TIFF.TAG_Y_RESOLUTION)); + assertEquals(new Rational(1), ifd.getEntryById(TIFF.TAG_Y_RESOLUTION).getValue()); + assertNotNull(ifd.getEntryById(TIFF.TAG_RESOLUTION_UNIT)); + assertEquals(TIFFBaseline.RESOLUTION_UNIT_NONE, ((Number) ifd.getEntryById(TIFF.TAG_RESOLUTION_UNIT).getValue()).intValue()); + } + + @Test(expected = IllegalArgumentException.class) + public void testMergeTreeUnsupportedFormat() throws IOException { + IIOMetadata metadata = createMetadata("/tiff/sm_colors_tile.tif"); + + String nativeFormat = "com_foo_bar_tiff_42"; + metadata.mergeTree(nativeFormat, new IIOMetadataNode(nativeFormat)); + } + + @Test(expected = IIOInvalidTreeException.class) + public void testMergeTreeFormatMisMatch() throws IOException { + IIOMetadata metadata = createMetadata("/tiff/sm_colors_tile.tif"); + + String nativeFormat = TIFFMedataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME; + metadata.mergeTree(nativeFormat, new IIOMetadataNode("com_foo_bar_tiff_42")); + } + + @Test(expected = IIOInvalidTreeException.class) + public void testMergeTreeInvalid() throws IOException { + IIOMetadata metadata = createMetadata("/tiff/sm_colors_tile.tif"); + + String nativeFormat = TIFFMedataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME; + metadata.mergeTree(nativeFormat, new IIOMetadataNode(nativeFormat)); // Requires at least one child node + } + + // TODO: Test that failed merge leaves metadata unchanged + + @Test + public void testSetFromTreeEmpty() throws IOException { + // Read from file, set empty to see that all is cleared + TIFFImageMetadata metadata = (TIFFImageMetadata) createMetadata("/tiff/sm_colors_tile.tif"); + + String nativeFormat = TIFFMedataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME; + IIOMetadataNode root = new IIOMetadataNode(nativeFormat); + root.appendChild(new IIOMetadataNode("TIFFIFD")); + + metadata.setFromTree(nativeFormat, root); + + Directory ifd = metadata.getIFD(); + assertNotNull(ifd); + assertEquals(0, ifd.size()); + + Node tree = metadata.getAsTree(nativeFormat); + + assertNotNull(tree); + assertNotNull(tree.getFirstChild()); + assertEquals(1, tree.getChildNodes().getLength()); + } + + @Test + public void testSetFromTree() throws IOException { + String softwareString = "12M UberTIFF 1.0"; + + TIFFImageMetadata metadata = new TIFFImageMetadata(Collections.emptySet()); + + String nativeFormat = TIFFMedataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME; + IIOMetadataNode root = new IIOMetadataNode(nativeFormat); + + IIOMetadataNode ifdNode = new IIOMetadataNode("TIFFIFD"); + root.appendChild(ifdNode); + + createTIFFFieldNode(ifdNode, TIFF.TAG_SOFTWARE, TIFF.TYPE_ASCII, softwareString); + + metadata.setFromTree(nativeFormat, root); + + Directory ifd = metadata.getIFD(); + assertNotNull(ifd); + assertEquals(1, ifd.size()); + + assertNotNull(ifd.getEntryById(TIFF.TAG_SOFTWARE)); + assertEquals(softwareString, ifd.getEntryById(TIFF.TAG_SOFTWARE).getValue()); + + Node tree = metadata.getAsTree(nativeFormat); + + assertNotNull(tree); + assertNotNull(tree.getFirstChild()); + assertEquals(1, tree.getChildNodes().getLength()); + } + + @Test + public void testSetFromTreeStandardFormat() throws IOException { + String softwareString = "12M UberTIFF 1.0"; + String copyrightString = "Copyright (C) TwelveMonkeys, 2015"; + + TIFFImageMetadata metadata = new TIFFImageMetadata(Collections.emptySet()); + + String standardFormat = IIOMetadataFormatImpl.standardMetadataFormatName; + IIOMetadataNode root = new IIOMetadataNode(standardFormat); + + IIOMetadataNode textNode = new IIOMetadataNode("Text"); + root.appendChild(textNode); + + IIOMetadataNode textEntry = new IIOMetadataNode("TextEntry"); + textNode.appendChild(textEntry); + + textEntry.setAttribute("keyword", "SOFTWARE"); // Spelling should not matter + textEntry.setAttribute("value", softwareString); + + textEntry = new IIOMetadataNode("TextEntry"); + textNode.appendChild(textEntry); + + textEntry.setAttribute("keyword", "copyright"); // Spelling should not matter + textEntry.setAttribute("value", copyrightString); + + metadata.setFromTree(standardFormat, root); + + Directory ifd = metadata.getIFD(); + assertNotNull(ifd); + assertEquals(2, ifd.size()); + + assertNotNull(ifd.getEntryById(TIFF.TAG_SOFTWARE)); + assertEquals(softwareString, ifd.getEntryById(TIFF.TAG_SOFTWARE).getValue()); + + assertNotNull(ifd.getEntryById(TIFF.TAG_COPYRIGHT)); + assertEquals(copyrightString, ifd.getEntryById(TIFF.TAG_COPYRIGHT).getValue()); + } + + @Test(expected = IllegalArgumentException.class) + public void testSetFromTreeUnsupportedFormat() throws IOException { + IIOMetadata metadata = createMetadata("/tiff/sm_colors_tile.tif"); + + String nativeFormat = "com_foo_bar_tiff_42"; + metadata.setFromTree(nativeFormat, new IIOMetadataNode(nativeFormat)); + } + + @Test(expected = IIOInvalidTreeException.class) + public void testSetFromTreeFormatMisMatch() throws IOException { + IIOMetadata metadata = createMetadata("/tiff/sm_colors_tile.tif"); + + String nativeFormat = TIFFMedataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME; + metadata.setFromTree(nativeFormat, new IIOMetadataNode("com_foo_bar_tiff_42")); + } + + @Test(expected = IIOInvalidTreeException.class) + public void testSetFromTreeInvalid() throws IOException { + IIOMetadata metadata = createMetadata("/tiff/sm_colors_tile.tif"); + + String nativeFormat = TIFFMedataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME; + metadata.setFromTree(nativeFormat, new IIOMetadataNode(nativeFormat)); // Requires at least one child node + } + + @Test + public void testStandardChromaSamplesPerPixel() { + Set entries = new HashSet<>(); + entries.add(new TIFFImageWriter.TIFFEntry(TIFF.TAG_PHOTOMETRIC_INTERPRETATION, TIFFBaseline.PHOTOMETRIC_RGB)); + entries.add(new TIFFImageWriter.TIFFEntry(TIFF.TAG_SAMPLES_PER_PIXEL, 4)); + entries.add(new TIFFImageWriter.TIFFEntry(TIFF.TAG_BITS_PER_SAMPLE, new int[] {8, 8, 8})); // This is incorrect, just making sure the correct value is selected + + IIOMetadataNode chromaNode = new TIFFImageMetadata(entries).getStandardChromaNode(); + assertNotNull(chromaNode); + + IIOMetadataNode numChannels = (IIOMetadataNode) chromaNode.getElementsByTagName("NumChannels").item(0); + assertEquals("4", numChannels.getAttribute("value")); + } + + @Test + public void testStandardChromaSamplesPerPixelFallbackBitsPerSample() { + Set entries = new HashSet<>(); + entries.add(new TIFFImageWriter.TIFFEntry(TIFF.TAG_PHOTOMETRIC_INTERPRETATION, TIFFBaseline.PHOTOMETRIC_RGB)); + entries.add(new TIFFImageWriter.TIFFEntry(TIFF.TAG_BITS_PER_SAMPLE, new int[] {8, 8, 8})); + + IIOMetadataNode chromaNode = new TIFFImageMetadata(entries).getStandardChromaNode(); + assertNotNull(chromaNode); + + IIOMetadataNode numChannels = (IIOMetadataNode) chromaNode.getElementsByTagName("NumChannels").item(0); + assertEquals("3", numChannels.getAttribute("value")); + } + + @Test + public void testStandardChromaSamplesPerPixelFallbackDefault() { + Set entries = new HashSet<>(); + entries.add(new TIFFImageWriter.TIFFEntry(TIFF.TAG_PHOTOMETRIC_INTERPRETATION, TIFFBaseline.PHOTOMETRIC_BLACK_IS_ZERO)); + + IIOMetadataNode chromaNode = new TIFFImageMetadata(entries).getStandardChromaNode(); + assertNotNull(chromaNode); + IIOMetadataNode numChannels = (IIOMetadataNode) chromaNode.getElementsByTagName("NumChannels").item(0); + assertEquals("1", numChannels.getAttribute("value")); + } + + @Test + public void testStandardDataBitsPerSampleFallbackDefault() { + Set entries = new HashSet<>(); + entries.add(new TIFFImageWriter.TIFFEntry(TIFF.TAG_PHOTOMETRIC_INTERPRETATION, TIFFBaseline.PHOTOMETRIC_BLACK_IS_ZERO)); + + IIOMetadataNode dataNode = new TIFFImageMetadata(entries).getStandardDataNode(); + assertNotNull(dataNode); + IIOMetadataNode numChannels = (IIOMetadataNode) dataNode.getElementsByTagName("BitsPerSample").item(0); + assertEquals("1", numChannels.getAttribute("value")); + } + + @Test + public void testStandardNodeSamplesPerPixelFallbackDefault() { + Set entries = new HashSet<>(); + entries.add(new TIFFImageWriter.TIFFEntry(TIFF.TAG_PHOTOMETRIC_INTERPRETATION, TIFFBaseline.PHOTOMETRIC_RGB)); + + // Just to make sure we haven't accidentally missed something + IIOMetadataNode standardTree = (IIOMetadataNode) new TIFFImageMetadata(entries).getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName); + assertNotNull(standardTree); + } + + // TODO: Test that failed set leaves metadata unchanged + + private void assertSingleNodeWithValue(final NodeList fields, final int tag, int type, final String... expectedValue) { + String tagNumber = String.valueOf(tag); + String typeName = StringUtil.capitalize(TIFF.TYPE_NAMES[type].toLowerCase()); + + boolean foundTag = false; + + for (int i = 0; i < fields.getLength(); i++) { + Element field = (Element) fields.item(i); + + if (tagNumber.equals(field.getAttribute("number"))) { + assertFalse("Duplicate tag " + tagNumber + " found", foundTag); + + assertEquals(1, field.getChildNodes().getLength()); + Node containerNode = field.getFirstChild(); + assertEquals("TIFF" + typeName + "s", containerNode.getNodeName()); + + NodeList valueNodes = containerNode.getChildNodes(); + assertEquals("Unexpected number of values for tag " + tagNumber, expectedValue.length, valueNodes.getLength()); + + for (int j = 0; j < expectedValue.length; j++) { + Element valueNode = (Element) valueNodes.item(j); + assertEquals("TIFF" + typeName, valueNode.getNodeName()); + assertEquals("Unexpected tag " + tagNumber + " value", expectedValue[j], valueNode.getAttribute("value")); + } + + foundTag = true; + } + } + + assertTrue("No tag " + tagNumber + " found", foundTag); + } + + static void createTIFFFieldNode(final IIOMetadataNode parentIFDNode, int tag, short type, Object value) { + IIOMetadataNode fieldNode = new IIOMetadataNode("TIFFField"); + parentIFDNode.appendChild(fieldNode); + + fieldNode.setAttribute("number", String.valueOf(tag)); + + switch (type) { + case TIFF.TYPE_ASCII: + createTIFFFieldContainerNode(fieldNode, "Ascii", value); + break; + case TIFF.TYPE_BYTE: + createTIFFFieldContainerNode(fieldNode, "Byte", value); + break; + case TIFF.TYPE_SHORT: + createTIFFFieldContainerNode(fieldNode, "Short", value); + break; + case TIFF.TYPE_RATIONAL: + createTIFFFieldContainerNode(fieldNode, "Rational", value); + break; + default: + throw new IllegalArgumentException("Unsupported type: " + type); + } + } + + static void createTIFFFieldContainerNode(final IIOMetadataNode fieldNode, final String type, final Object value) { + IIOMetadataNode containerNode = new IIOMetadataNode("TIFF" + type + "s"); + fieldNode.appendChild(containerNode); + + IIOMetadataNode valueNode = new IIOMetadataNode("TIFF" + type); + valueNode.setAttribute("value", String.valueOf(value)); + containerNode.appendChild(valueNode); + } + + private void assertNodeNotEquals(final String message, final Node expected, final Node actual) { + // Lame, lazy implementation... + try { + assertNodeEquals(message, expected, actual); + } + catch (AssertionError ignore) { + return; + } + + fail(message); + } + + private void assertNodeEquals(final String message, final Node expected, final Node actual) { + assertEquals(message + " class differs", expected.getClass(), actual.getClass()); + assertEquals(message, expected.getNodeValue(), actual.getNodeValue()); + + if (expected instanceof IIOMetadataNode) { + IIOMetadataNode expectedIIO = (IIOMetadataNode) expected; + IIOMetadataNode actualIIO = (IIOMetadataNode) actual; + + assertEquals(message, expectedIIO.getUserObject(), actualIIO.getUserObject()); + } + + NodeList expectedChildNodes = expected.getChildNodes(); + NodeList actualChildNodes = actual.getChildNodes(); + + assertEquals(message + " child length differs: " + toString(expectedChildNodes) + " != " + toString(actualChildNodes), + expectedChildNodes.getLength(), actualChildNodes.getLength()); + + for (int i = 0; i < expectedChildNodes.getLength(); i++) { + Node expectedChild = expectedChildNodes.item(i); + Node actualChild = actualChildNodes.item(i); + + assertEquals(message + " node name differs", expectedChild.getLocalName(), actualChild.getLocalName()); + assertNodeEquals(message + "/" + expectedChild.getLocalName(), expectedChild, actualChild); + } + } + + private String toString(final NodeList list) { + if (list.getLength() == 0) { + return "[]"; + } + + StringBuilder builder = new StringBuilder("["); + for (int i = 0; i < list.getLength(); i++) { + if (i > 0) { + builder.append(", "); + } + + Node node = list.item(i); + builder.append(node.getLocalName()); + } + builder.append("]"); + + return builder.toString(); + } +} \ No newline at end of file diff --git a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReaderTest.java b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReaderTest.java index 69b56e2a..bd81a43e 100644 --- a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReaderTest.java +++ b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReaderTest.java @@ -26,10 +26,12 @@ package com.twelvemonkeys.imageio.plugins.tiff;/* * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -import com.twelvemonkeys.imageio.util.ImageReaderAbstractTestCase; +import com.twelvemonkeys.imageio.util.ImageReaderAbstractTest; import org.junit.Test; +import javax.imageio.ImageIO; import javax.imageio.ImageReadParam; +import javax.imageio.ImageReader; import javax.imageio.event.IIOReadWarningListener; import javax.imageio.spi.ImageReaderSpi; import javax.imageio.stream.ImageInputStream; @@ -37,10 +39,11 @@ import java.awt.*; import java.awt.image.BufferedImage; import java.io.IOException; import java.util.Arrays; +import java.util.Collections; import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.*; import static org.mockito.Matchers.contains; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.*; @@ -52,7 +55,7 @@ import static org.mockito.Mockito.*; * @author last modified by $Author: haraldk$ * @version $Id: TIFFImageReaderTest.java,v 1.0 08.05.12 15:25 haraldk Exp$ */ -public class TIFFImageReaderTest extends ImageReaderAbstractTestCase { +public class TIFFImageReaderTest extends ImageReaderAbstractTest { private static final TIFFImageReaderSpi SPI = new TIFFImageReaderSpi(); @@ -77,7 +80,54 @@ public class TIFFImageReaderTest extends ImageReaderAbstractTestCase getMIMETypes() { - return Arrays.asList("image/tiff"); + return Collections.singletonList("image/tiff"); } @@ -138,9 +188,8 @@ public class TIFFImageReaderTest extends ImageReaderAbstractTestCase getTestData() { - BufferedImage image = new BufferedImage(300, 200, BufferedImage.TYPE_INT_ARGB); - Graphics2D graphics = image.createGraphics(); - try { - graphics.setColor(Color.RED); - graphics.fillRect(0, 0, 100, 200); - graphics.setColor(Color.BLUE); - graphics.fillRect(100, 0, 100, 200); - graphics.clearRect(200, 0, 100, 200); + return Arrays.asList( + new BufferedImage(300, 200, BufferedImage.TYPE_INT_RGB), + new BufferedImage(300, 200, BufferedImage.TYPE_INT_ARGB), + new BufferedImage(300, 200, BufferedImage.TYPE_3BYTE_BGR), + new BufferedImage(300, 200, BufferedImage.TYPE_4BYTE_ABGR), + new BufferedImage(300, 200, BufferedImage.TYPE_BYTE_GRAY), + new BufferedImage(300, 200, BufferedImage.TYPE_USHORT_GRAY), +// new BufferedImage(300, 200, BufferedImage.TYPE_BYTE_BINARY), // TODO! + new BufferedImage(300, 200, BufferedImage.TYPE_BYTE_INDEXED) + ); + } + + // TODO: Test write bilevel stays bilevel + // TODO: Test write indexed stays indexed + + @Test + public void testWriteWithCustomResolutionNative() throws IOException { + // Issue 139 Writing TIFF files with custom resolution value + Rational resolutionValue = new Rational(1200); + int resolutionUnitValue = TIFFBaseline.RESOLUTION_UNIT_CENTIMETER; + + RenderedImage image = getTestData(0); + + ImageWriter writer = createImageWriter(); + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + + try (ImageOutputStream stream = ImageIO.createImageOutputStream(buffer)) { + writer.setOutput(stream); + + String nativeFormat = TIFFMedataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME; + IIOMetadata metadata = writer.getDefaultImageMetadata(ImageTypeSpecifier.createFromRenderedImage(image), null); + + IIOMetadataNode customMeta = new IIOMetadataNode(nativeFormat); + + IIOMetadataNode ifd = new IIOMetadataNode("TIFFIFD"); + customMeta.appendChild(ifd); + + createTIFFFieldNode(ifd, TIFF.TAG_RESOLUTION_UNIT, TIFF.TYPE_SHORT, resolutionUnitValue); + createTIFFFieldNode(ifd, TIFF.TAG_X_RESOLUTION, TIFF.TYPE_RATIONAL, resolutionValue); + createTIFFFieldNode(ifd, TIFF.TAG_Y_RESOLUTION, TIFF.TYPE_RATIONAL, resolutionValue); + + metadata.mergeTree(nativeFormat, customMeta); + + writer.write(null, new IIOImage(image, null, metadata), null); } - finally { - graphics.dispose(); + catch (IOException e) { + e.printStackTrace(); + fail(e.getMessage()); } - return Arrays.asList(image); + assertTrue("No image data written", buffer.size() > 0); + + Directory ifds = new EXIFReader().read(new ByteArrayImageInputStream(buffer.toByteArray())); + + Entry resolutionUnit = ifds.getEntryById(TIFF.TAG_RESOLUTION_UNIT); + assertNotNull(resolutionUnit); + assertEquals(resolutionUnitValue, ((Number) resolutionUnit.getValue()).intValue()); + + Entry xResolution = ifds.getEntryById(TIFF.TAG_X_RESOLUTION); + assertNotNull(xResolution); + assertEquals(resolutionValue, xResolution.getValue()); + + Entry yResolution = ifds.getEntryById(TIFF.TAG_Y_RESOLUTION); + assertNotNull(yResolution); + assertEquals(resolutionValue, yResolution.getValue()); + } + + @Test + public void testWriteWithCustomSoftwareNative() throws IOException { + String softwareString = "12M TIFF Test 1.0 (build $foo$)"; + + RenderedImage image = getTestData(0); + + ImageWriter writer = createImageWriter(); + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + + try (ImageOutputStream stream = ImageIO.createImageOutputStream(buffer)) { + writer.setOutput(stream); + + String nativeFormat = TIFFMedataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME; + IIOMetadata metadata = writer.getDefaultImageMetadata(ImageTypeSpecifier.createFromRenderedImage(image), null); + + IIOMetadataNode customMeta = new IIOMetadataNode(nativeFormat); + + IIOMetadataNode ifd = new IIOMetadataNode("TIFFIFD"); + customMeta.appendChild(ifd); + + createTIFFFieldNode(ifd, TIFF.TAG_SOFTWARE, TIFF.TYPE_ASCII, softwareString); + + metadata.mergeTree(nativeFormat, customMeta); + + writer.write(null, new IIOImage(image, null, metadata), null); + } + catch (IOException e) { + e.printStackTrace(); + fail(e.getMessage()); + } + + assertTrue("No image data written", buffer.size() > 0); + + Directory ifds = new EXIFReader().read(new ByteArrayImageInputStream(buffer.toByteArray())); + Entry software = ifds.getEntryById(TIFF.TAG_SOFTWARE); + assertNotNull(software); + assertEquals(softwareString, software.getValueAsString()); + } + + @Test + public void testWriteWithCustomResolutionStandard() throws IOException { + // Issue 139 Writing TIFF files with custom resolution value + double resolutionValue = 300 / 25.4; // 300 dpi, 1 inch = 2.54 cm or 25.4 mm + int resolutionUnitValue = TIFFBaseline.RESOLUTION_UNIT_CENTIMETER; + Rational expectedResolutionValue = new Rational(Math.round(resolutionValue * 10 * TIFFImageMetadata.RATIONAL_SCALE_FACTOR), TIFFImageMetadata.RATIONAL_SCALE_FACTOR); + + RenderedImage image = getTestData(0); + + ImageWriter writer = createImageWriter(); + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + + try (ImageOutputStream stream = ImageIO.createImageOutputStream(buffer)) { + writer.setOutput(stream); + + String standardFormat = IIOMetadataFormatImpl.standardMetadataFormatName; + IIOMetadata metadata = writer.getDefaultImageMetadata(ImageTypeSpecifier.createFromRenderedImage(image), null); + + IIOMetadataNode customMeta = new IIOMetadataNode(standardFormat); + + IIOMetadataNode dimension = new IIOMetadataNode("Dimension"); + customMeta.appendChild(dimension); + + IIOMetadataNode xSize = new IIOMetadataNode("HorizontalPixelSize"); + dimension.appendChild(xSize); + xSize.setAttribute("value", String.valueOf(resolutionValue)); + + IIOMetadataNode ySize = new IIOMetadataNode("VerticalPixelSize"); + dimension.appendChild(ySize); + ySize.setAttribute("value", String.valueOf(resolutionValue)); + + metadata.mergeTree(standardFormat, customMeta); + + writer.write(null, new IIOImage(image, null, metadata), null); + } + catch (IOException e) { + e.printStackTrace(); + fail(e.getMessage()); + } + + assertTrue("No image data written", buffer.size() > 0); + + Directory ifds = new EXIFReader().read(new ByteArrayImageInputStream(buffer.toByteArray())); + + Entry resolutionUnit = ifds.getEntryById(TIFF.TAG_RESOLUTION_UNIT); + assertNotNull(resolutionUnit); + assertEquals(resolutionUnitValue, ((Number) resolutionUnit.getValue()).intValue()); + + Entry xResolution = ifds.getEntryById(TIFF.TAG_X_RESOLUTION); + assertNotNull(xResolution); + assertEquals(expectedResolutionValue, xResolution.getValue()); + + Entry yResolution = ifds.getEntryById(TIFF.TAG_Y_RESOLUTION); + assertNotNull(yResolution); + assertEquals(expectedResolutionValue, yResolution.getValue()); + } + + @Test + public void testWriteWithCustomSoftwareStandard() throws IOException { + String softwareString = "12M TIFF Test 1.0 (build $foo$)"; + + RenderedImage image = getTestData(0); + + ImageWriter writer = createImageWriter(); + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + + try (ImageOutputStream stream = ImageIO.createImageOutputStream(buffer)) { + writer.setOutput(stream); + + String standardFormat = IIOMetadataFormatImpl.standardMetadataFormatName; + IIOMetadata metadata = writer.getDefaultImageMetadata(ImageTypeSpecifier.createFromRenderedImage(image), null); + + IIOMetadataNode customMeta = new IIOMetadataNode(standardFormat); + + IIOMetadataNode dimension = new IIOMetadataNode("Text"); + customMeta.appendChild(dimension); + + IIOMetadataNode textEntry = new IIOMetadataNode("TextEntry"); + dimension.appendChild(textEntry); + textEntry.setAttribute("keyword", "Software"); + textEntry.setAttribute("value", softwareString); + + metadata.mergeTree(standardFormat, customMeta); + + writer.write(null, new IIOImage(image, null, metadata), null); + } + catch (IOException e) { + e.printStackTrace(); + fail(e.getMessage()); + } + + assertTrue("No image data written", buffer.size() > 0); + + Directory ifds = new EXIFReader().read(new ByteArrayImageInputStream(buffer.toByteArray())); + Entry software = ifds.getEntryById(TIFF.TAG_SOFTWARE); + assertNotNull(software); + assertEquals(softwareString, software.getValueAsString()); + } + + @Test + public void testSequenceWriter() throws IOException { + ImageWriter writer = createImageWriter(); + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + ImageOutputStream stream = ImageIO.createImageOutputStream(buffer); + writer.setOutput(stream); + + Graphics2D g2d = null; + BufferedImage image[] = new BufferedImage[] { + new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB), + new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB), + new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB) + }; + g2d = image[0].createGraphics(); + g2d.setColor(Color.red); + g2d.fillRect(0,0,100,100); + g2d.dispose(); + g2d = image[1].createGraphics(); + g2d.setColor(Color.green); + g2d.fillRect(0,0,100,100); + g2d.dispose(); + g2d = image[2].createGraphics(); + g2d.setColor(Color.blue); + g2d.fillRect(0,0,100,100); + g2d.dispose(); + + + ImageWriteParam params = writer.getDefaultWriteParam(); + params.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); + + assertTrue("", writer.canWriteSequence()); + + try { + writer.prepareWriteSequence(null); + + params.setCompressionType("JPEG"); + writer.writeToSequence(new IIOImage(image[0], null, null), params); + params.setCompressionType("None"); + writer.writeToSequence(new IIOImage(image[1], null, null), params); + params.setCompressionType("JPEG"); + writer.writeToSequence(new IIOImage(image[2], null, null), params); + g2d.dispose(); + writer.endWriteSequence(); + } + catch (IOException e) { + fail(e.getMessage()); + } + finally { + stream.close(); // Force data to be written + } + + ImageInputStream input = ImageIO.createImageInputStream(new ByteArrayInputStream(buffer.toByteArray())); + ImageReader reader = ImageIO.getImageReaders(input).next(); + reader.setInput(input); + assertEquals("wrong image count", 3, reader.getNumImages(true)); + for(int i = 0; i < reader.getNumImages(true); i++){ + reader.read(i); + } } } diff --git a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFProviderInfoTest.java b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFProviderInfoTest.java new file mode 100644 index 00000000..bdb8df3b --- /dev/null +++ b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFProviderInfoTest.java @@ -0,0 +1,19 @@ +package com.twelvemonkeys.imageio.plugins.tiff; + +import com.twelvemonkeys.imageio.spi.ReaderWriterProviderInfo; +import com.twelvemonkeys.imageio.spi.ReaderWriterProviderInfoTest; + +/** + * TIFFProviderInfoTest. + * + * @author Harald Kuhr + * @author last modified by $Author: harald.kuhr$ + * @version $Id: TIFFProviderInfoTest.java,v 1.0 02/06/16 harald.kuhr Exp$ + */ +public class TIFFProviderInfoTest extends ReaderWriterProviderInfoTest { + + @Override + protected ReaderWriterProviderInfo createProviderInfo() { + return new TIFFProviderInfo(); + } +} \ No newline at end of file diff --git a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/YCbCr16UpsamplerStreamTest.java b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/YCbCr16UpsamplerStreamTest.java index 04b65723..f998ae29 100644 --- a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/YCbCr16UpsamplerStreamTest.java +++ b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/YCbCr16UpsamplerStreamTest.java @@ -38,7 +38,8 @@ import java.io.InputStream; import java.nio.ByteBuffer; import java.nio.ByteOrder; -import static org.junit.Assert.*; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; /** * YCbCr16UpsamplerStreamTest @@ -50,22 +51,22 @@ import static org.junit.Assert.*; public class YCbCr16UpsamplerStreamTest { @Test(expected = IllegalArgumentException.class) public void testCreateNullStream() { - new YCbCr16UpsamplerStream(null, new int[2], 7, 5, null, ByteOrder.LITTLE_ENDIAN); + new YCbCr16UpsamplerStream(null, new int[2], 7, 5, ByteOrder.LITTLE_ENDIAN); } @Test(expected = IllegalArgumentException.class) public void testCreateNullChroma() { - new YCbCr16UpsamplerStream(new ByteArrayInputStream(new byte[0]), new int[3], 7, 5, null, ByteOrder.LITTLE_ENDIAN); + new YCbCr16UpsamplerStream(new ByteArrayInputStream(new byte[0]), new int[3], 7, 5, ByteOrder.LITTLE_ENDIAN); } @Test(expected = IllegalArgumentException.class) public void testCreateShortChroma() { - new YCbCr16UpsamplerStream(new ByteArrayInputStream(new byte[0]), new int[1], 7, 5, null, ByteOrder.LITTLE_ENDIAN); + new YCbCr16UpsamplerStream(new ByteArrayInputStream(new byte[0]), new int[1], 7, 5, ByteOrder.LITTLE_ENDIAN); } @Test(expected = IllegalArgumentException.class) public void testCreateNoByteOrder() { - new YCbCr16UpsamplerStream(new ByteArrayInputStream(new byte[0]), new int[] {2, 2}, 7, 5, null, null); + new YCbCr16UpsamplerStream(new ByteArrayInputStream(new byte[0]), new int[] {2, 2}, 7, 5, null); } // TODO: The expected values seems bogus... @@ -81,11 +82,11 @@ public class YCbCr16UpsamplerStreamTest { byte[] bytes = new byte[shorts.length * 2]; ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer().put(shorts); - InputStream stream = new YCbCr16UpsamplerStream(new ByteArrayInputStream(bytes), new int[] {2, 2}, TIFFExtension.YCBCR_POSITIONING_CENTERED, 8, null, ByteOrder.LITTLE_ENDIAN); + InputStream stream = new YCbCr16UpsamplerStream(new ByteArrayInputStream(bytes), new int[] {2, 2}, TIFFExtension.YCBCR_POSITIONING_CENTERED, 8, ByteOrder.LITTLE_ENDIAN); short[] expected = new short[] { - 0, -30864, 0, 0, -30863, 0, 0, -30966, 0, 0, -30965, 0, 0, -30870, 0, 0, -30869, 0, 0, -30815, 0, 0, -30761, 0, - 0, -30862, 0, 0, -30861, 0, 0, -30931, 0, 0, -30877, 0, 0, -30868, 0, 0, -30867, 0, 0, -30858, 0, 0, -30858, 0 + 1, 5, 6, 2, 5, 6, 7, 108, 109, 8, 108, 109, 110, 114, 115, 111, 114, 115, 43, 0, 0, 97, 0, 0, + 3, 5, 6, 4, 5, 6, 42, 108, 109, 96, 108, 109, 112, 114, 115, 113, 114, 115, 0, 0, 0, 0, 0, 0 }; short[] upsampled = new short[expected.length]; @@ -107,10 +108,10 @@ public class YCbCr16UpsamplerStreamTest { byte[] bytes = new byte[shorts.length * 2]; ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).asShortBuffer().put(shorts); - InputStream stream = new YCbCr16UpsamplerStream(new ByteArrayInputStream(bytes), new int[] {2, 1}, TIFFExtension.YCBCR_POSITIONING_CENTERED, 4, null, ByteOrder.BIG_ENDIAN); + InputStream stream = new YCbCr16UpsamplerStream(new ByteArrayInputStream(bytes), new int[] {2, 1}, TIFFExtension.YCBCR_POSITIONING_CENTERED, 4, ByteOrder.BIG_ENDIAN); short[] expected = new short[] { - 0, -30861, 0, 0, -30860, 0, 0, -30923, 0, 0, -30869, 0, 0, -30816, 0, 0, -30815, 0, 0, -30868, 0, 0, -30922, 0 + 1, 3, 4, 2, 3, 4, 42, 77, 112, 96, 77, 112, 113, 115, 43, 114, 115, 43, 97, 77, 112, 43, 77, 112 }; short[] upsampled = new short[expected.length]; @@ -132,10 +133,10 @@ public class YCbCr16UpsamplerStreamTest { byte[] bytes = new byte[shorts.length * 2]; ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).asShortBuffer().put(shorts); - InputStream stream = new YCbCr16UpsamplerStream(new ByteArrayInputStream(bytes), new int[] {1, 2}, TIFFExtension.YCBCR_POSITIONING_CENTERED, 4, null, ByteOrder.BIG_ENDIAN); + InputStream stream = new YCbCr16UpsamplerStream(new ByteArrayInputStream(bytes), new int[] {1, 2}, TIFFExtension.YCBCR_POSITIONING_CENTERED, 4, ByteOrder.BIG_ENDIAN); short[] expected = new short[] { - 0, -30861, 0, 0, -30923, 0, 0, -30816, 0, 0, -30761, 0, 0, -30860, 0, 0, -30869, 0, 0, -30815, 0, 0, -30815, 0 + 1, 3, 4, 42, 77, 112, 113, 115, 43, 97, 0, 0, 2, 3, 4, 96, 77, 112, 114, 115, 43, 43, 0, 0 }; short[] upsampled = new short[expected.length]; diff --git a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/YCbCrUpsamplerStreamTest.java b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/YCbCrUpsamplerStreamTest.java index a0faaced..3b0b6c73 100644 --- a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/YCbCrUpsamplerStreamTest.java +++ b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/YCbCrUpsamplerStreamTest.java @@ -35,7 +35,8 @@ import java.io.DataInputStream; import java.io.IOException; import java.io.InputStream; -import static org.junit.Assert.*; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; /** * YCbCrUpsamplerStreamTest @@ -47,17 +48,17 @@ import static org.junit.Assert.*; public class YCbCrUpsamplerStreamTest { @Test(expected = IllegalArgumentException.class) public void testCreateNullStream() { - new YCbCrUpsamplerStream(null, new int[2], 7, 5, null); + new YCbCrUpsamplerStream(null, new int[2], 7, 5); } @Test(expected = IllegalArgumentException.class) public void testCreateNullChroma() { - new YCbCrUpsamplerStream(new ByteArrayInputStream(new byte[0]), new int[3], 7, 5, null); + new YCbCrUpsamplerStream(new ByteArrayInputStream(new byte[0]), new int[3], 7, 5); } @Test(expected = IllegalArgumentException.class) public void testCreateShortChroma() { - new YCbCrUpsamplerStream(new ByteArrayInputStream(new byte[0]), new int[1], 7, 5, null); + new YCbCrUpsamplerStream(new ByteArrayInputStream(new byte[0]), new int[1], 7, 5); } @Test @@ -66,16 +67,17 @@ public class YCbCrUpsamplerStreamTest { 1, 2, 3, 4, 5, 6, 7, 8, 42, 96, 108, 109, 110, 111, 112, 113, 114, 115, 43, 97 }; - InputStream stream = new YCbCrUpsamplerStream(new ByteArrayInputStream(bytes), new int[] {2, 2}, TIFFExtension.YCBCR_POSITIONING_CENTERED, 8, null); + InputStream stream = new YCbCrUpsamplerStream(new ByteArrayInputStream(bytes), new int[] {2, 2}, TIFFExtension.YCBCR_POSITIONING_CENTERED, 8); byte[] expected = new byte[] { - 0, -126, 0, 0, -125, 0, 0, 27, 0, 0, 28, 0, 92, 124, 85, 93, 125, 86, 0, -78, 0, 0, -24, 0, - 0, -124, 0, 0, -123, 0, 15, 62, 7, 69, 116, 61, 94, 126, 87, 95, 127, 88, 0, -121, 0, 0, -121, 0 + 1, 5, 6, 2, 5, 6, 7, 108, 109, 8, 108, 109, 110, 114, 115, 111, 114, 115, 43, 0, 0, 97, 0, 0, + 3, 5, 6, 4, 5, 6, 42, 108, 109, 96, 108, 109, 112, 114, 115, 113, 114, 115, 0, 0, 0, 0, 0, 0 }; byte[] upsampled = new byte[expected.length]; new DataInputStream(stream).readFully(upsampled); + assertArrayEquals(expected, upsampled); assertEquals(-1, stream.read()); } @@ -86,10 +88,10 @@ public class YCbCrUpsamplerStreamTest { 1, 2, 3, 4, 42, 96, 77, 112, 113, 114, 115, 43, 97, 43 }; - InputStream stream = new YCbCrUpsamplerStream(new ByteArrayInputStream(bytes), new int[] {2, 1}, TIFFExtension.YCBCR_POSITIONING_CENTERED, 4, null); + InputStream stream = new YCbCrUpsamplerStream(new ByteArrayInputStream(bytes), new int[] {2, 1}, TIFFExtension.YCBCR_POSITIONING_CENTERED, 4); byte[] expected = new byte[] { - 0, -123, 0, 0, -122, 0, 20, 71, 0, 74, 125, 6, 0, -78, 90, 0, -77, 91, 75, 126, 7, 21, 72, 0 + 1, 3, 4, 2, 3, 4, 42, 77, 112, 96, 77, 112, 113, 115, 43, 114, 115, 43, 97, 77, 112, 43, 77, 112 }; byte[] upsampled = new byte[expected.length]; @@ -105,10 +107,10 @@ public class YCbCrUpsamplerStreamTest { 1, 2, 3, 4, 42, 96, 77, 112, 113, 114, 115, 43, 97, 43 }; - InputStream stream = new YCbCrUpsamplerStream(new ByteArrayInputStream(bytes), new int[] {1, 2}, TIFFExtension.YCBCR_POSITIONING_CENTERED, 4, null); + InputStream stream = new YCbCrUpsamplerStream(new ByteArrayInputStream(bytes), new int[] {1, 2}, TIFFExtension.YCBCR_POSITIONING_CENTERED, 4); byte[] expected = new byte[] { - 0, -123, 0, 20, 71, 0, 0, -78, 90, 0, -24, 0, 0, -122, 0, 74, 125, 6, 0, -77, 91, 0, -78, 0 + 1, 3, 4, 42, 77, 112, 113, 115, 43, 97, 0, 0, 2, 3, 4, 96, 77, 112, 114, 115, 43, 43, 0, 0 }; byte[] upsampled = new byte[expected.length]; diff --git a/imageio/imageio-tiff/src/test/resources/tiff/CCITTgetNextChangingElement.tif b/imageio/imageio-tiff/src/test/resources/tiff/CCITTgetNextChangingElement.tif new file mode 100644 index 00000000..2422087a Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/CCITTgetNextChangingElement.tif differ diff --git a/imageio/imageio-tiff/src/test/resources/tiff/ColorCheckerCalculator.tif b/imageio/imageio-tiff/src/test/resources/tiff/ColorCheckerCalculator.tif new file mode 100755 index 00000000..125b9310 Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/ColorCheckerCalculator.tif differ diff --git a/imageio/imageio-tiff/src/test/resources/tiff/ccitt/group3_1d.tif b/imageio/imageio-tiff/src/test/resources/tiff/ccitt/group3_1d.tif new file mode 100644 index 00000000..5a87ee3d Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/ccitt/group3_1d.tif differ diff --git a/imageio/imageio-tiff/src/test/resources/tiff/ccitt/group3_1d_fill.tif b/imageio/imageio-tiff/src/test/resources/tiff/ccitt/group3_1d_fill.tif new file mode 100644 index 00000000..e1ce7e06 Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/ccitt/group3_1d_fill.tif differ diff --git a/imageio/imageio-tiff/src/test/resources/tiff/ccitt/group3_2d.tif b/imageio/imageio-tiff/src/test/resources/tiff/ccitt/group3_2d.tif new file mode 100644 index 00000000..9d0a4ff9 Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/ccitt/group3_2d.tif differ diff --git a/imageio/imageio-tiff/src/test/resources/tiff/ccitt/group3_2d_fill.tif b/imageio/imageio-tiff/src/test/resources/tiff/ccitt/group3_2d_fill.tif new file mode 100644 index 00000000..70518f64 Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/ccitt/group3_2d_fill.tif differ diff --git a/imageio/imageio-tiff/src/test/resources/tiff/ccitt/group3_2d_lsb2msb.tif b/imageio/imageio-tiff/src/test/resources/tiff/ccitt/group3_2d_lsb2msb.tif new file mode 100644 index 00000000..ff1dd6dc Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/ccitt/group3_2d_lsb2msb.tif differ diff --git a/imageio/imageio-tiff/src/test/resources/tiff/ccitt/group4.tif b/imageio/imageio-tiff/src/test/resources/tiff/ccitt/group4.tif new file mode 100644 index 00000000..aa06b432 Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/ccitt/group4.tif differ diff --git a/imageio/imageio-tiff/src/test/resources/tiff/ccitt_tolessrows.tif b/imageio/imageio-tiff/src/test/resources/tiff/ccitt_tolessrows.tif new file mode 100644 index 00000000..e8c46c33 Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/ccitt_tolessrows.tif differ diff --git a/imageio/imageio-tiff/src/test/resources/tiff/cmyk_jpeg.tif b/imageio/imageio-tiff/src/test/resources/tiff/cmyk_jpeg.tif new file mode 100644 index 00000000..85d0bc92 Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/cmyk_jpeg.tif differ diff --git a/imageio/imageio-tiff/src/test/resources/tiff/cmyk_jpeg_no_profile.tif b/imageio/imageio-tiff/src/test/resources/tiff/cmyk_jpeg_no_profile.tif new file mode 100644 index 00000000..fa6d491e Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/cmyk_jpeg_no_profile.tif differ diff --git a/imageio/imageio-tiff/src/test/resources/tiff/depth/README.txt b/imageio/imageio-tiff/src/test/resources/tiff/depth/README.txt new file mode 100644 index 00000000..dd74611f --- /dev/null +++ b/imageio/imageio-tiff/src/test/resources/tiff/depth/README.txt @@ -0,0 +1,9 @@ +These sample TIFF image files are prepared by Bob Friesenhahn + using a development version of +GraphicsMagick 1.2. + +See the file summary.txt for a description of the images. + +These files are hereby placed in the public domain. + + diff --git a/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-minisblack-02.tif b/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-minisblack-02.tif new file mode 100644 index 00000000..2278aff1 Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-minisblack-02.tif differ diff --git a/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-minisblack-04.tif b/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-minisblack-04.tif new file mode 100644 index 00000000..2583c42f Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-minisblack-04.tif differ diff --git a/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-minisblack-06.tif b/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-minisblack-06.tif new file mode 100644 index 00000000..a9c0c3dd Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-minisblack-06.tif differ diff --git a/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-minisblack-08.tif b/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-minisblack-08.tif new file mode 100644 index 00000000..f7d83407 Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-minisblack-08.tif differ diff --git a/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-minisblack-10.tif b/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-minisblack-10.tif new file mode 100644 index 00000000..d93960cb Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-minisblack-10.tif differ diff --git a/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-minisblack-12.tif b/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-minisblack-12.tif new file mode 100644 index 00000000..1673be84 Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-minisblack-12.tif differ diff --git a/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-minisblack-14.tif b/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-minisblack-14.tif new file mode 100644 index 00000000..1d53867f Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-minisblack-14.tif differ diff --git a/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-minisblack-16.tif b/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-minisblack-16.tif new file mode 100644 index 00000000..3c2a6dd7 Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-minisblack-16.tif differ diff --git a/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-minisblack-24.tif b/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-minisblack-24.tif new file mode 100644 index 00000000..5035df45 Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-minisblack-24.tif differ diff --git a/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-minisblack-32.tif b/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-minisblack-32.tif new file mode 100644 index 00000000..36d48d4d Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-minisblack-32.tif differ diff --git a/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-palette-02.tif b/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-palette-02.tif new file mode 100644 index 00000000..6bd445de Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-palette-02.tif differ diff --git a/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-palette-04.tif b/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-palette-04.tif new file mode 100644 index 00000000..b50ac8cc Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-palette-04.tif differ diff --git a/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-palette-08.tif b/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-palette-08.tif new file mode 100644 index 00000000..b778037e Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-palette-08.tif differ diff --git a/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-palette-16.tif b/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-palette-16.tif new file mode 100644 index 00000000..cf3de609 Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-palette-16.tif differ diff --git a/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-rgb-contig-02.tif b/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-rgb-contig-02.tif new file mode 100644 index 00000000..3813313d Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-rgb-contig-02.tif differ diff --git a/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-rgb-contig-04.tif b/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-rgb-contig-04.tif new file mode 100644 index 00000000..b469f946 Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-rgb-contig-04.tif differ diff --git a/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-rgb-contig-08.tif b/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-rgb-contig-08.tif new file mode 100644 index 00000000..2ea09440 Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-rgb-contig-08.tif differ diff --git a/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-rgb-contig-10.tif b/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-rgb-contig-10.tif new file mode 100644 index 00000000..b6a22402 Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-rgb-contig-10.tif differ diff --git a/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-rgb-contig-12.tif b/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-rgb-contig-12.tif new file mode 100644 index 00000000..20485733 Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-rgb-contig-12.tif differ diff --git a/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-rgb-contig-14.tif b/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-rgb-contig-14.tif new file mode 100644 index 00000000..c9e27c02 Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-rgb-contig-14.tif differ diff --git a/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-rgb-contig-16.tif b/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-rgb-contig-16.tif new file mode 100644 index 00000000..869fe05a Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-rgb-contig-16.tif differ diff --git a/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-rgb-contig-24.tif b/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-rgb-contig-24.tif new file mode 100644 index 00000000..6037f52f Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-rgb-contig-24.tif differ diff --git a/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-rgb-contig-32.tif b/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-rgb-contig-32.tif new file mode 100644 index 00000000..eb13d6c2 Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-rgb-contig-32.tif differ diff --git a/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-rgb-planar-02.tif b/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-rgb-planar-02.tif new file mode 100644 index 00000000..afd364c7 Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-rgb-planar-02.tif differ diff --git a/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-rgb-planar-04.tif b/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-rgb-planar-04.tif new file mode 100644 index 00000000..ec97dc3b Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-rgb-planar-04.tif differ diff --git a/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-rgb-planar-08.tif b/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-rgb-planar-08.tif new file mode 100644 index 00000000..bbe287fb Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-rgb-planar-08.tif differ diff --git a/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-rgb-planar-10.tif b/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-rgb-planar-10.tif new file mode 100644 index 00000000..9d21bbe9 Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-rgb-planar-10.tif differ diff --git a/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-rgb-planar-12.tif b/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-rgb-planar-12.tif new file mode 100644 index 00000000..34a39449 Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-rgb-planar-12.tif differ diff --git a/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-rgb-planar-14.tif b/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-rgb-planar-14.tif new file mode 100644 index 00000000..2c690024 Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-rgb-planar-14.tif differ diff --git a/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-rgb-planar-16.tif b/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-rgb-planar-16.tif new file mode 100644 index 00000000..93566664 Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-rgb-planar-16.tif differ diff --git a/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-rgb-planar-24.tif b/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-rgb-planar-24.tif new file mode 100644 index 00000000..e9365d18 Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-rgb-planar-24.tif differ diff --git a/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-rgb-planar-32.tif b/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-rgb-planar-32.tif new file mode 100644 index 00000000..000391cf Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-rgb-planar-32.tif differ diff --git a/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-separated-contig-08.tif b/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-separated-contig-08.tif new file mode 100644 index 00000000..7eefd539 Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-separated-contig-08.tif differ diff --git a/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-separated-contig-16.tif b/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-separated-contig-16.tif new file mode 100644 index 00000000..5df4246f Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-separated-contig-16.tif differ diff --git a/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-separated-planar-08.tif b/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-separated-planar-08.tif new file mode 100644 index 00000000..6994a129 Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-separated-planar-08.tif differ diff --git a/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-separated-planar-16.tif b/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-separated-planar-16.tif new file mode 100644 index 00000000..380b9b4f Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/depth/flower-separated-planar-16.tif differ diff --git a/imageio/imageio-tiff/src/test/resources/tiff/depth/summary.txt b/imageio/imageio-tiff/src/test/resources/tiff/depth/summary.txt new file mode 100644 index 00000000..19f3aabd --- /dev/null +++ b/imageio/imageio-tiff/src/test/resources/tiff/depth/summary.txt @@ -0,0 +1,37 @@ + +flower-minisblack-02.tif 73x43 2-bit minisblack gray image +flower-minisblack-04.tif 73x43 4-bit minisblack gray image +flower-minisblack-06.tif 73x43 6-bit minisblack gray image +flower-minisblack-08.tif 73x43 8-bit minisblack gray image +flower-minisblack-10.tif 73x43 10-bit minisblack gray image +flower-minisblack-12.tif 73x43 12-bit minisblack gray image +flower-minisblack-14.tif 73x43 14-bit minisblack gray image +flower-minisblack-16.tif 73x43 16-bit minisblack gray image +flower-minisblack-24.tif 73x43 24-bit minisblack gray image +flower-minisblack-32.tif 73x43 32-bit minisblack gray image +flower-palette-02.tif 73x43 4-entry colormapped image +flower-palette-04.tif 73x43 16-entry colormapped image +flower-palette-08.tif 73x43 256-entry colormapped image +flower-palette-16.tif 73x43 65536-entry colormapped image +flower-rgb-contig-02.tif 73x43 2-bit contiguous RGB image +flower-rgb-contig-04.tif 73x43 4-bit contiguous RGB image +flower-rgb-contig-08.tif 73x43 8-bit contiguous RGB image +flower-rgb-contig-10.tif 73x43 10-bit contiguous RGB image +flower-rgb-contig-12.tif 73x43 12-bit contiguous RGB image +flower-rgb-contig-14.tif 73x43 14-bit contiguous RGB image +flower-rgb-contig-16.tif 73x43 16-bit contiguous RGB image +flower-rgb-contig-24.tif 73x43 24-bit contiguous RGB image +flower-rgb-contig-32.tif 73x43 32-bit contiguous RGB image +flower-rgb-planar-02.tif 73x43 2-bit seperated RGB image +flower-rgb-planar-04.tif 73x43 4-bit seperated RGB image +flower-rgb-planar-08.tif 73x43 8-bit seperated RGB image +flower-rgb-planar-10.tif 73x43 10-bit seperated RGB image +flower-rgb-planar-12.tif 73x43 12-bit seperated RGB image +flower-rgb-planar-14.tif 73x43 14-bit seperated RGB image +flower-rgb-planar-16.tif 73x43 16-bit seperated RGB image +flower-rgb-planar-24.tif 73x43 24-bit seperated RGB image +flower-rgb-planar-32.tif 73x43 32-bit seperated RGB image +flower-separated-contig-08.tif 73x43 8-bit contiguous CMYK image +flower-separated-contig-16.tif 73x43 16-bit contiguous CMYK image +flower-separated-planar-08.tif 73x43 8-bit separated CMYK image +flower-separated-planar-16.tif 73x43 16-bit separated CMYK image diff --git a/imageio/imageio-tiff/src/test/resources/tiff/fivepages-scan-causingerrors.tif b/imageio/imageio-tiff/src/test/resources/tiff/fivepages-scan-causingerrors.tif new file mode 100644 index 00000000..1e3dc912 Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/fivepages-scan-causingerrors.tif differ diff --git a/imageio/imageio-tiff/src/test/resources/tiff/floatingpoint-32bit.tif b/imageio/imageio-tiff/src/test/resources/tiff/floatingpoint-32bit.tif new file mode 100644 index 00000000..a1e24a81 Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/floatingpoint-32bit.tif differ diff --git a/imageio/imageio-tiff/src/test/resources/tiff/general-cmm-error.tif b/imageio/imageio-tiff/src/test/resources/tiff/general-cmm-error.tif new file mode 100644 index 00000000..feb25c6a Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/general-cmm-error.tif differ diff --git a/imageio/imageio-tiff/src/test/resources/tiff/grayscale-alpha.tiff b/imageio/imageio-tiff/src/test/resources/tiff/grayscale-alpha.tiff new file mode 100644 index 00000000..6b951e77 Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/grayscale-alpha.tiff differ diff --git a/imageio/imageio-tiff/src/test/resources/tiff/guessPhotometric/flower-rgb-contig-08.tif b/imageio/imageio-tiff/src/test/resources/tiff/guessPhotometric/flower-rgb-contig-08.tif new file mode 100644 index 00000000..62a1b19d Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/guessPhotometric/flower-rgb-contig-08.tif differ diff --git a/imageio/imageio-tiff/src/test/resources/tiff/guessPhotometric/flower-separated-planar-08.tif b/imageio/imageio-tiff/src/test/resources/tiff/guessPhotometric/flower-separated-planar-08.tif new file mode 100644 index 00000000..ad0bfea8 Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/guessPhotometric/flower-separated-planar-08.tif differ diff --git a/imageio/imageio-tiff/src/test/resources/tiff/guessPhotometric/group4.tif b/imageio/imageio-tiff/src/test/resources/tiff/guessPhotometric/group4.tif new file mode 100644 index 00000000..0dee7750 Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/guessPhotometric/group4.tif differ diff --git a/imageio/imageio-tiff/src/test/resources/tiff/lzw-buffer-overflow.tif b/imageio/imageio-tiff/src/test/resources/tiff/lzw-buffer-overflow.tif new file mode 100644 index 00000000..d9b08dc3 Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/lzw-buffer-overflow.tif differ diff --git a/imageio/imageio-tiff/src/test/resources/tiff/lzw-rgba-4444.tif b/imageio/imageio-tiff/src/test/resources/tiff/lzw-rgba-4444.tif new file mode 100644 index 00000000..4c1e60a8 Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/lzw-rgba-4444.tif differ diff --git a/imageio/imageio-tiff/src/test/resources/tiff/lzw-rgba-padded-icc.tif b/imageio/imageio-tiff/src/test/resources/tiff/lzw-rgba-padded-icc.tif new file mode 100644 index 00000000..c8801c07 Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/lzw-rgba-padded-icc.tif differ diff --git a/imageio/imageio-tiff/src/test/resources/tiff/old-style-jpeg-bogus-jpeginterchangeformatlength.tif b/imageio/imageio-tiff/src/test/resources/tiff/old-style-jpeg-bogus-jpeginterchangeformatlength.tif new file mode 100755 index 00000000..4930b9bd Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/old-style-jpeg-bogus-jpeginterchangeformatlength.tif differ diff --git a/imageio/imageio-tiff/src/test/resources/tiff/scan-mono-iccgray.tif b/imageio/imageio-tiff/src/test/resources/tiff/scan-mono-iccgray.tif new file mode 100755 index 00000000..197b6d30 Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/scan-mono-iccgray.tif differ diff --git a/imageio/imageio-tiff/src/test/resources/tiff/signed-integral-8bit.tif b/imageio/imageio-tiff/src/test/resources/tiff/signed-integral-8bit.tif new file mode 100644 index 00000000..fa80da15 Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/signed-integral-8bit.tif differ diff --git a/imageio/pom.xml b/imageio/pom.xml index c25c074a..1c30e3b5 100644 --- a/imageio/pom.xml +++ b/imageio/pom.xml @@ -3,7 +3,7 @@ com.twelvemonkeys twelvemonkeys - 3.1-SNAPSHOT + 3.3-SNAPSHOT 4.0.0 com.twelvemonkeys.imageio @@ -26,10 +26,12 @@ imageio-core imageio-metadata + imageio-clippath imageio-bmp + imageio-hdr imageio-icns imageio-iff imageio-jpeg @@ -75,14 +77,14 @@ com.twelvemonkeys.common common-lang ${project.version} - tests + test-jar test com.twelvemonkeys.common common-io ${project.version} - tests + test-jar test @@ -137,7 +139,7 @@ ${project.groupId} imageio-core ${project.version} - tests + test-jar test diff --git a/pom.xml b/pom.xml index afcfe4f9..872c37e2 100755 --- a/pom.xml +++ b/pom.xml @@ -8,16 +8,26 @@ com.twelvemonkeys twelvemonkeys - 3.1-SNAPSHOT + 3.3-SNAPSHOT pom Twelvemonkeys + + + The BSD License + https://github.com/haraldk/TwelveMonkeys#license + repo + + + common servlet imageio + contrib + bom @@ -41,8 +51,27 @@ maven-guru + + Oliver Schmidtmer + mail@trek7891.de + + contributor + + + + Jason Palmer + jpalmer@itemmaster.com + + contributor + + + + GitHub + https://github.com/haraldk/TwelveMonkeys/issues + + scm:git:https://github.com/haraldk/TwelveMonkeys scm:git:https://github.com/haraldk/TwelveMonkeys @@ -94,7 +123,7 @@ org.apache.maven.plugins maven-resources-plugin - 2.5 + 2.7 UTF-8 @@ -102,7 +131,7 @@ org.apache.maven.plugins maven-jar-plugin - 2.4 + 2.6 true @@ -117,23 +146,23 @@ org.apache.maven.plugins maven-javadoc-plugin - 2.9.1 + 2.10.3 org.apache.maven.plugins maven-source-plugin - 2.2.1 + 2.4 org.apache.maven.plugins maven-compiler-plugin - 2.3.2 + 3.3 true 1.7 1.7 false - -g:lines + source,lines iso-8859-1 @@ -143,7 +172,7 @@ org.apache.maven.plugins maven-surefire-plugin - 2.10 + 2.18.1 @@ -154,22 +183,10 @@ - - org.apache.maven.plugins - maven-idea-plugin - true - 2.2 - - 1.7 - 1.7 - true - - - org.apache.maven.plugins maven-release-plugin - 2.4.2 + 2.5.2 org.apache.maven.scm @@ -189,22 +206,22 @@ org.apache.maven.plugins maven-javadoc-plugin - 2.9.1 + 2.10.3 org.apache.maven.plugins maven-surefire-report-plugin - 2.16 + 2.18.1 org.codehaus.mojo cobertura-maven-plugin - 2.6 + 2.7 org.apache.maven.plugins maven-pmd-plugin - 3.0.1 + 3.4 1.7 @@ -212,7 +229,7 @@ org.apache.maven.plugins maven-checkstyle-plugin - 2.10 + 2.15 diff --git a/sandbox/pom.xml b/sandbox/pom.xml index a8f332cd..cdcb591f 100644 --- a/sandbox/pom.xml +++ b/sandbox/pom.xml @@ -91,14 +91,14 @@ common-io ${project.version} test - tests + test-jar com.twelvemonkeys.common common-lang ${project.version} test - tests + test-jar diff --git a/sandbox/sandbox-common/pom.xml b/sandbox/sandbox-common/pom.xml index 596e8a41..31f6b315 100644 --- a/sandbox/sandbox-common/pom.xml +++ b/sandbox/sandbox-common/pom.xml @@ -44,14 +44,14 @@ com.twelvemonkeys.common common-io test - tests + test-jar com.twelvemonkeys.common common-lang test - tests + test-jar diff --git a/sandbox/sandbox-imageio/pom.xml b/sandbox/sandbox-imageio/pom.xml index a7b23040..0d831a2f 100644 --- a/sandbox/sandbox-imageio/pom.xml +++ b/sandbox/sandbox-imageio/pom.xml @@ -72,14 +72,14 @@ com.twelvemonkeys.common common-io test - tests + test-jar com.twelvemonkeys.common common-lang test - tests + test-jar diff --git a/servlet/pom.xml b/servlet/pom.xml index c44ea86e..2f2fc663 100644 --- a/servlet/pom.xml +++ b/servlet/pom.xml @@ -3,7 +3,7 @@ com.twelvemonkeys twelvemonkeys - 3.1-SNAPSHOT + 3.3-SNAPSHOT 4.0.0 @@ -32,14 +32,14 @@ com.twelvemonkeys.common common-lang ${project.version} - tests + test-jar test com.twelvemonkeys.common common-io ${project.version} - tests + test-jar test