mirror of
https://github.com/haraldk/TwelveMonkeys.git
synced 2026-01-24 00:00:05 -05:00
Merge branch 'master' of https://github.com/haraldk/TwelveMonkeys
Conflicts: sandbox/sandbox-common/src/main/java/com/twelvemonkeys/io/enc/DeflateEncoder.java sandbox/sandbox-common/src/main/java/com/twelvemonkeys/io/enc/InflateDecoder.java servlet/src/main/java/com/twelvemonkeys/servlet/GenericFilter.java servlet/src/main/java/com/twelvemonkeys/servlet/GenericServlet.java servlet/src/main/java/com/twelvemonkeys/servlet/HttpServlet.java servlet/src/main/java/com/twelvemonkeys/servlet/ServletResponseStreamDelegate.java servlet/src/main/java/com/twelvemonkeys/servlet/cache/CacheResponseWrapper.java servlet/src/main/java/com/twelvemonkeys/servlet/cache/HTTPCache.java servlet/src/main/java/com/twelvemonkeys/servlet/cache/SerlvetCacheResponseWrapper.java servlet/src/main/java/com/twelvemonkeys/servlet/cache/WritableCachedResponseImpl.java servlet/src/main/java/com/twelvemonkeys/servlet/image/ImageServletResponseImpl.java
This commit is contained in:
498
README.md
Normal file
498
README.md
Normal file
@@ -0,0 +1,498 @@
|
||||
## Background
|
||||
|
||||
TwelveMonkeys ImageIO is a collection of plug-ins 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.
|
||||
|
||||
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.
|
||||
Because there is lots of legacy data out there, we see the need for open implementations of readers for popular formats.
|
||||
The goal is to create a set of efficient and robust ImageIO plug-ins, that can be distributed independently.
|
||||
|
||||
----
|
||||
|
||||
## Features
|
||||
|
||||
Mainstream format support
|
||||
|
||||
#### JPEG
|
||||
|
||||
* Read support for the following JPEG flavors:
|
||||
* 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 )
|
||||
* 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 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
|
||||
* Thumbnail support:
|
||||
* 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)
|
||||
* `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
|
||||
"MarkerSequence" tag for the unsupported segments (for `javax_imageio_jpeg_image_1.0` format)
|
||||
* Extended write support in progress:
|
||||
* CMYK JPEGs
|
||||
* YCCK JPEGs
|
||||
|
||||
#### JPEG-2000
|
||||
|
||||
* 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).
|
||||
|
||||
#### Adobe Photoshop Document (PSD)
|
||||
|
||||
* Read support for the following file types:
|
||||
* Monochrome, 1 channel, 1 bit
|
||||
* Indexed, 1 channel, 8 bit
|
||||
* Gray, 1 channel, 8 and 16 bit
|
||||
* Duotone, 1 channel, 8 and 16 bit
|
||||
* RGB, 3-4 channels, 8 and 16 bit
|
||||
* CMYK, 4-5 channels, 8 and 16 bit
|
||||
* Read support for the following compression types:
|
||||
* Uncompressed
|
||||
* RLE (PackBits)
|
||||
* Layer support
|
||||
* Image layers only, in all of the above types
|
||||
* Thumbnail support
|
||||
* JPEG
|
||||
* RAW (RGB)
|
||||
|
||||
#### Aldus/Adobe Tagged Image File Format (TIFF)
|
||||
|
||||
* Read support for the following "Baseline" TIFF file types:
|
||||
* Class B (Bi-level), all relevant compression types, 1 bit per sample
|
||||
* Class G (Gray), all relevant compression types, 2, 4, 8, 16 or 32 bits per sample, unsigned integer
|
||||
* Class P (Palette/indexed color), all relevant compression types, 1, 2, 4, 8 or 16 bits per sample, unsigned integer
|
||||
* Class R (RGB), all relevant compression types, 8 or 16 bits per sample, unsigned integer
|
||||
* Read support for the following TIFF extensions:
|
||||
* Tiling
|
||||
* 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
|
||||
* 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
|
||||
|
||||
#### Apple Mac Paint Picture Format (PICT)
|
||||
|
||||
* Legacy format, especially useful for reading OS X clipboard data.
|
||||
* Read support for the following file types:
|
||||
* QuickDraw (format support is not complete, but supports most OS X clipboard data as well as RGB pixel data)
|
||||
* QuickDraw bitmap
|
||||
* QuickDraw pixmap
|
||||
* QuickTime stills
|
||||
* Write support for RGB pixel data:
|
||||
* QuickDraw pixmap
|
||||
|
||||
#### Commodore Amiga/Electronic Arts Interchange File Format (IFF)
|
||||
|
||||
* Legacy format, allows reading popular image 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
|
||||
* ILBM RGB, 24 and 32 bit interleaved bit planes
|
||||
* ILBM HAM6 and HAM8
|
||||
* PBM Indexed color, 1-8 bit,
|
||||
* PBM Gray, 8 bit
|
||||
* PBM RGB, 24 and 32 bit
|
||||
* PBM HAM6 and HAM8
|
||||
* Write support
|
||||
* ILBM Indexed color, 1-8 bits per sample, 8 bit gray, 24 and 32 bit true color.
|
||||
* Support for the following compression types (read/write):
|
||||
* Uncompressed
|
||||
* RLE (PackBits)
|
||||
|
||||
Icon/other formats
|
||||
|
||||
#### Apple Icon Image (ICNS)
|
||||
|
||||
* 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)
|
||||
|
||||
* 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
|
||||
|
||||
#### MS Windows Thumbs DB (Thumbs.db)
|
||||
|
||||
* Read support
|
||||
|
||||
Other formats, using 3rd party libraries
|
||||
|
||||
#### Scalable Vector Graphics (SVG)
|
||||
|
||||
* Read-only support using Batik
|
||||
|
||||
#### MS Windows MetaFile (WMF)
|
||||
|
||||
* Limited read-only support using Batik
|
||||
|
||||
|
||||
## Basic usage
|
||||
|
||||
Most of the time, all you need to do is simply include the plugins in your project and write:
|
||||
|
||||
BufferedImage image = ImageIO.read(file);
|
||||
|
||||
This will load the first image of the file, entirely into memory.
|
||||
|
||||
The basic and simplest form of writing is:
|
||||
|
||||
if (!ImageIO.write(image, format, file)) {
|
||||
// Handle image not written case
|
||||
}
|
||||
|
||||
This will write the entire image into a single file, using the default settings for the given format.
|
||||
|
||||
The plugins are discovered automatically at run time. See the [FAQ](#faq) for more info on how this mechanism works.
|
||||
|
||||
## Advanced usage
|
||||
|
||||
If you need more control of read parameters and the reading process, the common idiom for reading is something like:
|
||||
|
||||
// Create input stream
|
||||
ImageInputStream input = ImageIO.createImageInputStream(file);
|
||||
|
||||
try {
|
||||
// Get the reader
|
||||
Iterator<ImageReader> readers = ImageIO.getImageReaders(input);
|
||||
|
||||
if (!readers.hasNext()) {
|
||||
throw new IllegalArgumentException("No reader for: " + file);
|
||||
}
|
||||
|
||||
ImageReader reader = readers.next();
|
||||
|
||||
try {
|
||||
reader.setInput(input);
|
||||
|
||||
// Optionally, listen for read warnings, progress, etc.
|
||||
reader.addIIOReadWarningListener(...);
|
||||
reader.addIIOReadProgressListener(...);
|
||||
|
||||
ImageReadParam param = reader.getDefaultReadParam();
|
||||
|
||||
// Optionally, control read settings like sub sampling, source region or destination etc.
|
||||
param.setSourceSubsampling(...);
|
||||
param.setSourceRegion(...);
|
||||
param.setDestination(...);
|
||||
// ...
|
||||
|
||||
// Finally read the image, using settings from param
|
||||
BufferedImage image = reader.read(0, param);
|
||||
|
||||
// Optionally, read thumbnails, meta data, etc...
|
||||
int numThumbs = reader.getNumThumbnails(0);
|
||||
// ...
|
||||
}
|
||||
finally {
|
||||
// Dispose reader in finally block to avoid memory leaks
|
||||
reader.dispose();
|
||||
}
|
||||
}
|
||||
finally {
|
||||
// Close stream in finally block to avoid resource leaks
|
||||
input.close();
|
||||
}
|
||||
|
||||
Query the reader for source image dimensions using `reader.getWidth(n)` and `reader.getHeight(n)` without reading the
|
||||
entire image into memory first.
|
||||
|
||||
It's also possible to read multiple images from the same file in a loop, using `reader.getNumImages()`.
|
||||
|
||||
|
||||
If you need more control of write parameters and the writing process, the common idiom for writing is something like:
|
||||
|
||||
// Get the writer
|
||||
Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName(format);
|
||||
|
||||
if (!writers.hasNext()) {
|
||||
throw new IllegalArgumentException("No writer for: " + format);
|
||||
}
|
||||
|
||||
ImageWriter writer = writers.next();
|
||||
|
||||
try {
|
||||
// Create output stream
|
||||
ImageOutputStream output = ImageIO.createImageOutputStream(file);
|
||||
|
||||
try {
|
||||
writer.setOutput(output);
|
||||
|
||||
// Optionally, listen to progress, warnings, etc.
|
||||
|
||||
ImageWriteParam param = writer.getDefaultWriteParam();
|
||||
|
||||
// Optionally, control format specific settings of param (requires casting), or
|
||||
// control generic write settings like sub sampling, source region, output type etc.
|
||||
|
||||
// Optionally, provide thumbnails and image/stream metadata
|
||||
writer.write(..., new IIOImage(..., image, ...), param);
|
||||
}
|
||||
finally {
|
||||
// Close stream in finally block to avoid resource leaks
|
||||
output.close();
|
||||
}
|
||||
}
|
||||
finally {
|
||||
// Dispose writer in finally block to avoid memory leaks
|
||||
writer.dispose();
|
||||
}
|
||||
|
||||
For more advanced usage, and information on how to use the ImageIO API, I suggest you read the
|
||||
[Java Image I/O API Guide](http://docs.oracle.com/javase/7/docs/technotes/guides/imageio/spec/imageio_guideTOC.fm.html)
|
||||
from Oracle.
|
||||
|
||||
|
||||
#### Deploying the plugins in a web app
|
||||
|
||||
Because the `ImageIO` plugin registry (the `IIORegistry`) is "VM global", it doesn't by default work well with
|
||||
servlet contexts. This is especially evident if you load plugins from the `WEB-INF/lib` or `classes` folder.
|
||||
Unless you add `ImageIO.scanForPlugins()` somewhere in your code, the plugins might never be available at all.
|
||||
|
||||
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.
|
||||
|
||||
To work around both the discovery problem and the resource leak,
|
||||
it is recommended to use the `IIOProviderContextListener` that implements
|
||||
dynamic loading and unloading of ImageIO plugins for web applications.
|
||||
|
||||
<web-app ...>
|
||||
|
||||
...
|
||||
|
||||
<listener>
|
||||
<display-name>ImageIO service provider loader/unloader</display-name>
|
||||
<listener-class>com.twelvemonkeys.servlet.image.IIOProviderContextListener</listener-class>
|
||||
</listener>
|
||||
|
||||
...
|
||||
|
||||
</web-app>
|
||||
|
||||
|
||||
#### Using the ResampleOp
|
||||
|
||||
The library comes with a resampling (image resizing) operation, that contains many different algorithms
|
||||
to provide excellent results at reasonable speed.
|
||||
|
||||
import com.twelvemonkeys.image.ResampleOp;
|
||||
|
||||
...
|
||||
|
||||
BufferedImage input = ...; // Image to resample
|
||||
int width, height = ...; // new width/height
|
||||
|
||||
BufferedImageOp resampler = new ResampleOp(width, height, ResampleOp.FILTER_LANCZOS); // A good default filter, see class documentation for more info
|
||||
BufferedImage output = resampler.filter(input, null);
|
||||
|
||||
|
||||
#### Using the DiffusionDither
|
||||
|
||||
The library comes with a dithering operation, that can be used to convert `BufferedImage`s to `IndexColorModel` using
|
||||
Floyd-Steinberg error-diffusion dither.
|
||||
|
||||
import com.twelvemonkeys.image.DiffusionDither;
|
||||
|
||||
...
|
||||
|
||||
BufferedImage input = ...; // Image to dither
|
||||
|
||||
BufferedImageOp ditherer = new DiffusionDither();
|
||||
BufferedImage output = ditherer.filter(input, null);
|
||||
|
||||
|
||||
## Building
|
||||
|
||||
Download the project (using [Git](http://git-scm.com/downloads)):
|
||||
|
||||
$ git clone git@github.com:haraldk/TwelveMonkeys.git
|
||||
|
||||
This should create a folder named `TwelveMonkeys` in your current directory. Change directory to the `TwelveMonkeys`
|
||||
folder, and issue the command below to build.
|
||||
|
||||
Build the project (using [Maven](http://maven.apache.org/download.cgi)):
|
||||
|
||||
$ mvn package
|
||||
|
||||
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`.
|
||||
|
||||
Optionally, you can install the project in your local Maven repository using:
|
||||
|
||||
$ mvn install
|
||||
|
||||
## Installing
|
||||
|
||||
To install the plug-ins,
|
||||
either use Maven and add the necessary dependencies to your project,
|
||||
or manually add the needed JARs along with required dependencies in class-path.
|
||||
|
||||
The ImageIO registry and service lookup mechanism will make sure the plugins are available for use.
|
||||
|
||||
To verify that the JPEG plugin is installed and used at run-time, you could use the following code:
|
||||
|
||||
Iterator<ImageReader> readers = ImageIO.getImageReadersByFormatName("JPEG");
|
||||
while (readers.hasNext()) {
|
||||
System.out.println("reader: " + readers.next());
|
||||
}
|
||||
|
||||
The first line should print:
|
||||
|
||||
reader: com.twelvemonkeys.imageio.jpeg.JPEGImageReader@somehash
|
||||
|
||||
#### Maven dependency example
|
||||
|
||||
To depend on the JPEG and TIFF plugin using Maven, add the following to your POM:
|
||||
|
||||
...
|
||||
<dependencies>
|
||||
...
|
||||
<dependency>
|
||||
<groupId>com.twelvemonkeys.imageio</groupId>
|
||||
<artifactId>imageio-jpeg</artifactId>
|
||||
<version>3.0-SNAPSHOT</version> <!-- Alternatively, build your own 3.0-something version -->
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.twelvemonkeys.imageio</groupId>
|
||||
<artifactId>imageio-tiff</artifactId>
|
||||
<version>3.0-SNAPSHOT</version> <!-- Alternatively, build your own 3.0-something version -->
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
#### Manual dependency example
|
||||
|
||||
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-SNAPSHOT.jar
|
||||
twelvemonkeys-common-io-3.0-SNAPSHOT.jar
|
||||
twelvemonkeys-common-image-3.0-SNAPSHOT.jar
|
||||
twelvemonkeys-imageio-core-3.0-SNAPSHOT.jar
|
||||
twelvemonkeys-imageio-metadata-3.0-SNAPSHOT.jar
|
||||
twelvemonkeys-imageio-jpeg-3.0-SNAPSHOT.jar
|
||||
twelvemonkeys-imageio-tiff-3.0-SNAPSHOT.jar
|
||||
|
||||
### Links to prebuilt binaries
|
||||
|
||||
There's no prebuilt binaries yet.
|
||||
|
||||
## License
|
||||
|
||||
The project is distributed under the OSI approved [BSD license](http://opensource.org/licenses/BSD-3-Clause):
|
||||
|
||||
Copyright (c) 2008-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:
|
||||
|
||||
o Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
o 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.
|
||||
|
||||
o 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.
|
||||
|
||||
|
||||
## FAQ
|
||||
|
||||
q: How do I use it?
|
||||
|
||||
a: The easiest way is to build your own project using Maven, and just add dependencies to the specific plug-ins you need.
|
||||
If you don't use Maven, make sure you have all the necessary JARs in classpath. See the Install section above.
|
||||
|
||||
|
||||
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
|
||||
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.
|
||||
|
||||
Should you want to use very specific/advanced features of some of the formats, you might have to use specific APIs, like
|
||||
setting base URL for an SVG image that consists of multiple files,
|
||||
or controlling the output compression of a TIFF file.
|
||||
|
||||
|
||||
q: How does it work?
|
||||
|
||||
a: The TwelveMonkeys ImageIO project contains plug-ins for ImageIO.
|
||||
|
||||
ImageIO uses a service lookup mechanism, to discover plug-ins at runtime.
|
||||
|
||||
TODO: Describe SPI mechanism.
|
||||
|
||||
All you have have to do, is to make sure you have the TwelveMonkeys JARs in your classpath.
|
||||
|
||||
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.
|
||||
|
||||
|
||||
q: What about JAI? Several of the formats are already supported by JAI.
|
||||
|
||||
a: While JAI (and jai-imageio in particular) have support for some of the formats, JAI has some major issues.
|
||||
The most obvious being:
|
||||
- It's not actively developed. No issues has been fixed for years.
|
||||
- To get full format support, you need native libs.
|
||||
Native libs does not exist for several popular platforms/architectures, and further the native libs are not open source.
|
||||
Some environments may also prevent deployment of native libs, which brings us back to square one.
|
||||
|
||||
|
||||
q: What about JMagick or IM4Java? Can't you just use what´s already available?
|
||||
|
||||
a: While great libraries with a wide range of formats support, the ImageMagick-based libraries has some disadvantages
|
||||
compared to ImageIO.
|
||||
- No real stream support, these libraries only work with files.
|
||||
- No easy access to pixel data through standard Java2D/BufferedImage API.
|
||||
- Not a pure Java solution, requires system specific native libs.
|
||||
|
||||
|
||||
-----
|
||||
|
||||
We did it
|
||||
@@ -60,6 +60,8 @@ import java.awt.image.RGBImageFilter;
|
||||
|
||||
public class BrightnessContrastFilter extends RGBImageFilter {
|
||||
|
||||
// TODO: Replace with RescaleOp?
|
||||
|
||||
// This filter can filter IndexColorModel, as it is does not depend on
|
||||
// the pixels' location
|
||||
{
|
||||
|
||||
@@ -17,7 +17,7 @@ import java.util.Random;
|
||||
* This {@code BufferedImageOp/RasterOp} implements basic
|
||||
* Floyd-Steinberg error-diffusion algorithm for dithering.
|
||||
* <P/>
|
||||
* The weights used are 7/16 3/16 5/16 1/16, distributed like this:
|
||||
* The weights used are 7/16, 3/16, 5/16 and 1/16, distributed like this:
|
||||
* <!-- - -
|
||||
* | |x|7|
|
||||
* - - - -
|
||||
@@ -37,7 +37,6 @@ import java.util.Random;
|
||||
* @author last modified by $Author: haku $
|
||||
*
|
||||
* @version $Id: DiffusionDither.java#1 $
|
||||
*
|
||||
*/
|
||||
public class DiffusionDither implements BufferedImageOp, RasterOp {
|
||||
|
||||
@@ -72,6 +71,7 @@ public class DiffusionDither implements BufferedImageOp, RasterOp {
|
||||
* Sets the scan mode. If the parameter is true, error distribution for
|
||||
* every even line will be left-to-right, while odd lines will be
|
||||
* right-to-left.
|
||||
* The default is {@code true}.
|
||||
*
|
||||
* @param pUse {@code true} if scan mode should be alternating left/right
|
||||
*/
|
||||
@@ -219,7 +219,7 @@ public class DiffusionDither implements BufferedImageOp, RasterOp {
|
||||
* Floyd-Steinberg error-diffusion to the image.
|
||||
*
|
||||
* @param pSource the source image
|
||||
* @param pDest the destiantion image
|
||||
* @param pDest the destination image
|
||||
*
|
||||
* @return the destination image, or a new image, if {@code pDest} was
|
||||
* {@code null}.
|
||||
@@ -243,8 +243,8 @@ public class DiffusionDither implements BufferedImageOp, RasterOp {
|
||||
* Performs a single-input/single-output dither operation, applying basic
|
||||
* Floyd-Steinberg error-diffusion to the image.
|
||||
*
|
||||
* @param pSource
|
||||
* @param pDest
|
||||
* @param pSource the source raster, assumed to be in sRGB
|
||||
* @param pDest the destination raster, may be {@code null}
|
||||
*
|
||||
* @return the destination raster, or a new raster, if {@code pDest} was
|
||||
* {@code null}.
|
||||
@@ -271,9 +271,9 @@ public class DiffusionDither implements BufferedImageOp, RasterOp {
|
||||
* Performs a single-input/single-output dither operation, applying basic
|
||||
* Floyd-Steinberg error-diffusion to the image.
|
||||
*
|
||||
* @param pSource
|
||||
* @param pDest
|
||||
* @param pColorModel
|
||||
* @param pSource the source raster, assumed to be in sRGB
|
||||
* @param pDest the destination raster, may be {@code null}
|
||||
* @param pColorModel the indexed color model to use
|
||||
*
|
||||
* @return the destination raster, or a new raster, if {@code pDest} was
|
||||
* {@code null}.
|
||||
@@ -298,11 +298,6 @@ public class DiffusionDither implements BufferedImageOp, RasterOp {
|
||||
// Random errors in [-1 .. 1] - for first row
|
||||
for (int i = 0; i < width + 2; i++) {
|
||||
// Note: This is broken for the strange cases where nextInt returns Integer.MIN_VALUE
|
||||
/*
|
||||
currErr[i][0] = (Math.abs(RANDOM.nextInt()) % (FS_SCALE * 2)) - FS_SCALE;
|
||||
currErr[i][1] = (Math.abs(RANDOM.nextInt()) % (FS_SCALE * 2)) - FS_SCALE;
|
||||
currErr[i][2] = (Math.abs(RANDOM.nextInt()) % (FS_SCALE * 2)) - FS_SCALE;
|
||||
*/
|
||||
currErr[i][0] = RANDOM.nextInt(FS_SCALE * 2) - FS_SCALE;
|
||||
currErr[i][1] = RANDOM.nextInt(FS_SCALE * 2) - FS_SCALE;
|
||||
currErr[i][2] = RANDOM.nextInt(FS_SCALE * 2) - FS_SCALE;
|
||||
|
||||
@@ -32,6 +32,8 @@ import magick.*;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.color.ColorSpace;
|
||||
import java.awt.color.ICC_ColorSpace;
|
||||
import java.awt.color.ICC_Profile;
|
||||
import java.awt.image.*;
|
||||
|
||||
/**
|
||||
@@ -160,7 +162,11 @@ public final class MagickUtil {
|
||||
image = rgbToBuffered(pImage, true);
|
||||
break;
|
||||
case ImageType.ColorSeparationType:
|
||||
image = cmykToBuffered(pImage, false);
|
||||
break;
|
||||
case ImageType.ColorSeparationMatteType:
|
||||
image = cmykToBuffered(pImage, true);
|
||||
break;
|
||||
case ImageType.OptimizeType:
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown JMagick image type: " + pImage.getImageType());
|
||||
@@ -546,4 +552,60 @@ public final class MagickUtil {
|
||||
|
||||
return new BufferedImage(pAlpha ? CM_COLOR_ALPHA : CM_COLOR_OPAQUE, raster, pAlpha, null);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Converts an {@code MagickImage} to a {@code BufferedImage} which holds an CMYK ICC profile
|
||||
*
|
||||
* @param pImage the original {@code MagickImage}
|
||||
* @param pAlpha keep alpha channel
|
||||
* @return a new {@code BufferedImage}
|
||||
*
|
||||
* @throws MagickException if an exception occurs during conversion
|
||||
*
|
||||
* @see BufferedImage
|
||||
*/
|
||||
private static BufferedImage cmykToBuffered(MagickImage pImage, boolean pAlpha) throws MagickException {
|
||||
Dimension size = pImage.getDimension();
|
||||
int length = size.width * size.height;
|
||||
|
||||
// Retreive the ICC profile
|
||||
ICC_Profile profile = ICC_Profile.getInstance(pImage.getColorProfile().getInfo());
|
||||
ColorSpace cs = new ICC_ColorSpace(profile);
|
||||
|
||||
int bands = cs.getNumComponents() + (pAlpha ? 1 : 0);
|
||||
|
||||
int[] bits = new int[bands];
|
||||
for (int i = 0; i < bands; i++) {
|
||||
bits[i] = 8;
|
||||
}
|
||||
|
||||
ColorModel cm = pAlpha ?
|
||||
new ComponentColorModel(cs, bits, true, true, Transparency.TRANSLUCENT, DataBuffer.TYPE_BYTE) :
|
||||
new ComponentColorModel(cs, bits, false, false, Transparency.OPAQUE, DataBuffer.TYPE_BYTE);
|
||||
|
||||
byte[] pixels = new byte[length * bands];
|
||||
|
||||
// TODO: If we do multiple dispatches (one per line, typically), we could provide listener
|
||||
// feedback. But it's currently a lot slower than fetching all the pixels in one go.
|
||||
// TODO: handle more generic cases if profile is not CMYK
|
||||
// TODO: Test "ACMYK"
|
||||
pImage.dispatchImage(0, 0, size.width, size.height, pAlpha ? "ACMYK" : "CMYK", pixels);
|
||||
|
||||
// Init databuffer with array, to avoid allocation of empty array
|
||||
DataBuffer buffer = new DataBufferByte(pixels, pixels.length);
|
||||
|
||||
// TODO: build array from bands variable, here it just works for CMYK
|
||||
// The values has not been tested with an alpha picture actually...
|
||||
int[] bandOffsets = pAlpha ? new int[] {0, 1, 2, 3, 4} : new int[] {0, 1, 2, 3};
|
||||
|
||||
WritableRaster raster =
|
||||
Raster.createInterleavedRaster(buffer, size.width, size.height,
|
||||
size.width * bands, bands, bandOffsets, LOCATION_UPPER_LEFT);
|
||||
|
||||
return new BufferedImage(cm, raster, pAlpha, null);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,8 +52,6 @@
|
||||
|
||||
package com.twelvemonkeys.image;
|
||||
|
||||
import com.twelvemonkeys.lang.SystemUtil;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.geom.AffineTransform;
|
||||
import java.awt.geom.Point2D;
|
||||
@@ -294,7 +292,6 @@ public class ResampleOp implements BufferedImageOp/* TODO: RasterOp */ {
|
||||
int height;
|
||||
|
||||
int filterType;
|
||||
private static final boolean TRANSFORM_OP_BICUBIC_SUPPORT = SystemUtil.isFieldAvailable(AffineTransformOp.class.getName(), "TYPE_BICUBIC");
|
||||
|
||||
/**
|
||||
* RendereingHints.Key implementation, works only with Value values.
|
||||
@@ -320,7 +317,7 @@ public class ResampleOp implements BufferedImageOp/* TODO: RasterOp */ {
|
||||
}
|
||||
|
||||
/**
|
||||
* RenderingHints value implementaion, works with Key keys.
|
||||
* RenderingHints value implementation, works with Key keys.
|
||||
*/
|
||||
// TODO: Extract abstract Value class, and move to AbstractBufferedImageOp
|
||||
static final class Value {
|
||||
@@ -331,8 +328,7 @@ public class ResampleOp implements BufferedImageOp/* TODO: RasterOp */ {
|
||||
public Value(final RenderingHints.Key pKey, final String pName, final int pType) {
|
||||
key = pKey;
|
||||
name = pName;
|
||||
validateFilterType(pType);
|
||||
type = pType;// TODO: test for duplicates
|
||||
type = validateFilterType(pType);
|
||||
}
|
||||
|
||||
public boolean isCompatibleKey(Key pKey) {
|
||||
@@ -422,11 +418,10 @@ public class ResampleOp implements BufferedImageOp/* TODO: RasterOp */ {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
|
||||
validateFilterType(filterType);
|
||||
this.filterType = filterType;
|
||||
this.filterType = validateFilterType(filterType);
|
||||
}
|
||||
|
||||
private static void validateFilterType(int pFilterType) {
|
||||
private static int validateFilterType(int pFilterType) {
|
||||
switch (pFilterType) {
|
||||
case FILTER_UNDEFINED:
|
||||
case FILTER_POINT:
|
||||
@@ -444,7 +439,7 @@ public class ResampleOp implements BufferedImageOp/* TODO: RasterOp */ {
|
||||
case FILTER_LANCZOS:
|
||||
case FILTER_BLACKMAN_BESSEL:
|
||||
case FILTER_BLACKMAN_SINC:
|
||||
break;
|
||||
return pFilterType;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown filter type: " + pFilterType);
|
||||
}
|
||||
@@ -529,8 +524,8 @@ public class ResampleOp implements BufferedImageOp/* TODO: RasterOp */ {
|
||||
}
|
||||
// Else fall through
|
||||
case FILTER_QUADRATIC:
|
||||
if (input.getType() != BufferedImage.TYPE_CUSTOM && TRANSFORM_OP_BICUBIC_SUPPORT) {
|
||||
return fastResample(input, output, width, height, 3); // AffineTransformOp.TYPE_BICUBIC
|
||||
if (input.getType() != BufferedImage.TYPE_CUSTOM) {
|
||||
return fastResample(input, output, width, height, AffineTransformOp.TYPE_BICUBIC);
|
||||
}
|
||||
// Else fall through
|
||||
default:
|
||||
|
||||
@@ -79,7 +79,7 @@ public final class FileUtil {
|
||||
|
||||
/*
|
||||
* Method main for test only.
|
||||
*/
|
||||
*
|
||||
public static void main0(String[] pArgs) {
|
||||
if (pArgs.length != 2) {
|
||||
System.out.println("usage: java Copy in out");
|
||||
@@ -94,6 +94,7 @@ public final class FileUtil {
|
||||
System.out.println(e.getMessage());
|
||||
}
|
||||
}
|
||||
//*/
|
||||
|
||||
// Avoid instances/constructor showing up in API doc
|
||||
private FileUtil() {}
|
||||
@@ -204,7 +205,7 @@ public final class FileUtil {
|
||||
close(out);
|
||||
}
|
||||
|
||||
return true; // If we got here, everything's probably okay.. ;-)
|
||||
return true; // If we got here, everything is probably okay.. ;-)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -581,7 +582,7 @@ public final class FileUtil {
|
||||
* @throws IOException if an i/o error occurs during read.
|
||||
*/
|
||||
public static byte[] read(InputStream pInput) throws IOException {
|
||||
// Create bytearray
|
||||
// Create byte array
|
||||
ByteArrayOutputStream bytes = new FastByteArrayOutputStream(BUF_SIZE);
|
||||
|
||||
// Copy from stream to byte array
|
||||
|
||||
@@ -31,6 +31,7 @@ package com.twelvemonkeys.io.enc;
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* Abstract base class for RLE decoding as specified by in the Windows BMP (aka DIB) file format.
|
||||
@@ -49,13 +50,13 @@ abstract class AbstractRLEDecoder implements Decoder {
|
||||
protected int dstY;
|
||||
|
||||
/**
|
||||
* Creates an RLEDecoder. As RLE encoded BMP's may contain x and y deltas,
|
||||
* Creates an RLEDecoder. As RLE encoded BMPs may contain x and y deltas,
|
||||
* etc, we need to know height and width of the image.
|
||||
*
|
||||
* @param pWidth width of the image
|
||||
* @param pHeight heigth of the image
|
||||
* @param pHeight height of the image
|
||||
*/
|
||||
AbstractRLEDecoder(int pWidth, int pHeight) {
|
||||
AbstractRLEDecoder(final int pWidth, final int pHeight) {
|
||||
width = pWidth;
|
||||
int bytesPerRow = width;
|
||||
int mod = bytesPerRow % 4;
|
||||
@@ -76,35 +77,34 @@ abstract class AbstractRLEDecoder implements Decoder {
|
||||
/**
|
||||
* Decodes one full row of image data.
|
||||
*
|
||||
* @param pStream the input stream containint RLE data
|
||||
* @param pStream the input stream containing RLE data
|
||||
*
|
||||
* @throws IOException if an I/O related exception ocurs while reading
|
||||
* @throws IOException if an I/O related exception occurs while reading
|
||||
*/
|
||||
protected abstract void decodeRow(InputStream pStream) throws IOException;
|
||||
protected abstract void decodeRow(final InputStream pStream) throws IOException;
|
||||
|
||||
/**
|
||||
* Decodes as much data as possible, from the stream into the buffer.
|
||||
*
|
||||
* @param pStream the input stream containing RLE data
|
||||
* @param pBuffer tge buffer to decode the data to
|
||||
* @param stream the input stream containing RLE data
|
||||
* @param buffer the buffer to decode the data to
|
||||
*
|
||||
* @return the number of bytes decoded from the stream, to the buffer
|
||||
*
|
||||
* @throws IOException if an I/O related exception ocurs while reading
|
||||
*/
|
||||
public final int decode(InputStream pStream, byte[] pBuffer) throws IOException {
|
||||
int decoded = 0;
|
||||
|
||||
while (decoded < pBuffer.length && dstY >= 0) {
|
||||
public final int decode(final InputStream stream, final ByteBuffer buffer) throws IOException {
|
||||
while (buffer.hasRemaining() && dstY >= 0) {
|
||||
// NOTE: Decode only full rows, don't decode if y delta
|
||||
if (dstX == 0 && srcY == dstY) {
|
||||
decodeRow(pStream);
|
||||
decodeRow(stream);
|
||||
}
|
||||
|
||||
int length = Math.min(row.length - dstX, pBuffer.length - decoded);
|
||||
System.arraycopy(row, dstX, pBuffer, decoded, length);
|
||||
int length = Math.min(row.length - dstX, buffer.remaining());
|
||||
// System.arraycopy(row, dstX, buffer, decoded, length);
|
||||
buffer.put(row, 0, length);
|
||||
dstX += length;
|
||||
decoded += length;
|
||||
// decoded += length;
|
||||
|
||||
if (dstX == row.length) {
|
||||
dstX = 0;
|
||||
@@ -120,7 +120,7 @@ abstract class AbstractRLEDecoder implements Decoder {
|
||||
}
|
||||
}
|
||||
|
||||
return decoded;
|
||||
return buffer.position();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -131,7 +131,7 @@ abstract class AbstractRLEDecoder implements Decoder {
|
||||
*
|
||||
* @throws EOFException if {@code pByte} is negative
|
||||
*/
|
||||
protected static int checkEOF(int pByte) throws EOFException {
|
||||
protected static int checkEOF(final int pByte) throws EOFException {
|
||||
if (pByte < 0) {
|
||||
throw new EOFException("Premature end of file");
|
||||
}
|
||||
|
||||
@@ -1,671 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2008, 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.
|
||||
*/
|
||||
/*
|
||||
* Copyright (c) 2004, Mikael Grev, MiG InfoCom AB. (base64 @ miginfocom . com)
|
||||
* All rights reserved.
|
||||
* <p/>
|
||||
* 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 of the MiG InfoCom AB nor the names of its contributors may be
|
||||
* used to endorse or promote products derived from this software without specific
|
||||
* prior written permission.
|
||||
* <p/>
|
||||
* 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.io.enc;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* A very fast and memory efficient class to encode and decode to and from
|
||||
* BASE64 in full accordance with RFC 2045.
|
||||
* <p/>
|
||||
* On Windows XP sp1 with 1.4.2_04 and later ;), this encoder and decoder is
|
||||
* about 10 times faster on small arrays (10 - 1000 bytes) and 2-3 times as fast
|
||||
* on larger arrays (10000 - 1000000 bytes) compared to
|
||||
* {@code sun.misc.Encoder()/Decoder()}.
|
||||
* <p/>
|
||||
* On byte arrays the encoder is about 20% faster than
|
||||
* <a href="http://jakarta.apache.org/commons/codec/">Jakarta Commons Base64 Codec</a>
|
||||
* for encode and about 50% faster for decoding large arrays. This
|
||||
* implementation is about twice as fast on very small arrays (< 30 bytes).
|
||||
* If source/destination is a {@code String} this version is about three times
|
||||
* as fast due to the fact that the Commons Codec result has to be recoded
|
||||
* to a {@code String} from {@code byte[]}, which is very expensive.
|
||||
* <p/>
|
||||
* This encode/decode algorithm doesn't create any temporary arrays as many
|
||||
* other codecs do, it only allocates the resulting array. This produces less
|
||||
* garbage and it is possible to handle arrays twice as large as algorithms that
|
||||
* create a temporary array. (E.g. Jakarta Commons Codec). It is unknown
|
||||
* whether Sun's {@code sun.misc.Encoder()/Decoder()} produce temporary arrays
|
||||
* but since performance is quite low it probably does.
|
||||
* <p/>
|
||||
* The encoder produces the same output as the Sun one except that Sun's encoder
|
||||
* appends a trailing line separator if the last character isn't a pad.
|
||||
* Unclear why but it only adds to the length and is probably a side effect.
|
||||
* Both are in conformance with RFC 2045 though.<br>
|
||||
* Commons codec seem to always add a trailing line separator.
|
||||
* <p/>
|
||||
* <b>Note!</b>
|
||||
* The encode/decode method pairs (types) come in three versions with the
|
||||
* <b>exact</b> same algorithm and thus a lot of code redundancy. This is to not
|
||||
* create any temporary arrays for transcoding to/from different
|
||||
* format types. The methods not used can simply be commented out.
|
||||
* <p/>
|
||||
* There is also a "fast" version of all decode methods that works the same way
|
||||
* as the normal ones, but har a few demands on the decoded input. Normally
|
||||
* though, these fast verions should be used if the source if
|
||||
* the input is known and it hasn't bee tampered with.
|
||||
* <p/>
|
||||
* If you find the code useful or you find a bug, please send me a note at
|
||||
* base64 @ miginfocom . com.
|
||||
* <p/>
|
||||
*
|
||||
* @author Mikael Grev, 2004-aug-02 11:31:11
|
||||
* @version 2.2
|
||||
*/
|
||||
final class Base64 {
|
||||
private static final char[] CA = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".toCharArray();
|
||||
private static final int[] IA = new int[256];
|
||||
|
||||
static {
|
||||
Arrays.fill(IA, -1);
|
||||
for (int i = 0, iS = CA.length; i < iS; i++) {
|
||||
IA[CA[i]] = i;
|
||||
}
|
||||
IA['='] = 0;
|
||||
}
|
||||
|
||||
// ****************************************************************************************
|
||||
// * char[] version
|
||||
// ****************************************************************************************
|
||||
|
||||
/**
|
||||
* Encodes a raw byte array into a BASE64 {@code char[]} representation im
|
||||
* accordance with RFC 2045.
|
||||
*
|
||||
* @param sArr The bytes to convert. If {@code null} or length 0 an
|
||||
* empty array will be returned.
|
||||
* @param lineSep Optional "\r\n" after 76 characters, unless end of file.
|
||||
* <br>
|
||||
* No line separator will be in breach of RFC 2045 which
|
||||
* specifies max 76 per line but will be a little faster.
|
||||
* @return A BASE64 encoded array. Never {@code null}.
|
||||
*/
|
||||
public static char[] encodeToChar(byte[] sArr, boolean lineSep) {
|
||||
// Check special case
|
||||
int sLen = sArr != null ? sArr.length : 0;
|
||||
if (sLen == 0) {
|
||||
return new char[0];
|
||||
}
|
||||
|
||||
int eLen = (sLen / 3) * 3;// Length of even 24-bits.
|
||||
int cCnt = ((sLen - 1) / 3 + 1) << 2;// Returned character count
|
||||
int dLen = cCnt + (lineSep ? (cCnt - 1) / 76 << 1 : 0);// Length of returned array
|
||||
char[] dArr = new char[dLen];
|
||||
|
||||
// Encode even 24-bits
|
||||
for (int s = 0, d = 0, cc = 0; s < eLen;) {
|
||||
// Copy next three bytes into lower 24 bits of int, paying attension to sign.
|
||||
int i = (sArr[s++] & 0xff) << 16 | (sArr[s++] & 0xff) << 8 | (sArr[s++] & 0xff);
|
||||
|
||||
// Encode the int into four chars
|
||||
dArr[d++] = CA[(i >>> 18) & 0x3f];
|
||||
dArr[d++] = CA[(i >>> 12) & 0x3f];
|
||||
dArr[d++] = CA[(i >>> 6) & 0x3f];
|
||||
dArr[d++] = CA[i & 0x3f];
|
||||
|
||||
// Add optional line separator
|
||||
if (lineSep && ++cc == 19 && d < dLen - 2) {
|
||||
dArr[d++] = '\r';
|
||||
dArr[d++] = '\n';
|
||||
cc = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Pad and encode last bits if source isn't even 24 bits.
|
||||
int left = sLen - eLen;// 0 - 2.
|
||||
if (left > 0) {
|
||||
// Prepare the int
|
||||
int i = ((sArr[eLen] & 0xff) << 10) | (left == 2 ? ((sArr[sLen - 1] & 0xff) << 2) : 0);
|
||||
|
||||
// Set last four chars
|
||||
dArr[dLen - 4] = CA[i >> 12];
|
||||
dArr[dLen - 3] = CA[(i >>> 6) & 0x3f];
|
||||
dArr[dLen - 2] = left == 2 ? CA[i & 0x3f] : '=';
|
||||
dArr[dLen - 1] = '=';
|
||||
}
|
||||
return dArr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes a BASE64 encoded char array. All illegal characters will be
|
||||
* ignored and can handle both arrays with and without line separators.
|
||||
*
|
||||
* @param sArr The source array. {@code null} or length 0 will return
|
||||
* an empty array.
|
||||
* @return The decoded array of bytes. May be of length 0. Will be
|
||||
* {@code null} if the legal characters (including '=') isn't
|
||||
* divideable by 4. (I.e. definitely corrupted).
|
||||
*/
|
||||
public static byte[] decode(char[] sArr) {
|
||||
// Check special case
|
||||
int sLen = sArr != null ? sArr.length : 0;
|
||||
if (sLen == 0) {
|
||||
return new byte[0];
|
||||
}
|
||||
|
||||
// Count illegal characters (including '\r', '\n') to know what size the returned array will be,
|
||||
// so we don't have to reallocate & copy it later.
|
||||
int sepCnt = 0;// Number of separator characters. (Actually illegal characters, but that's a bonus...)
|
||||
for (int i = 0; i < sLen; i++)// If input is "pure" (I.e. no line separators or illegal chars) base64 this loop can be commented out.
|
||||
{
|
||||
if (IA[sArr[i]] < 0) {
|
||||
sepCnt++;
|
||||
}
|
||||
}
|
||||
|
||||
// Check so that legal chars (including '=') are evenly divideable by 4 as specified in RFC 2045.
|
||||
if ((sLen - sepCnt) % 4 != 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int pad = 0;
|
||||
for (int i = sLen; i > 1 && IA[sArr[--i]] <= 0;) {
|
||||
if (sArr[i] == '=') {
|
||||
pad++;
|
||||
}
|
||||
}
|
||||
|
||||
int len = ((sLen - sepCnt) * 6 >> 3) - pad;
|
||||
|
||||
byte[] dArr = new byte[len];// Preallocate byte[] of exact length
|
||||
|
||||
for (int s = 0, d = 0; d < len;) {
|
||||
// Assemble three bytes into an int from four "valid" characters.
|
||||
int i = 0;
|
||||
for (int j = 0; j < 4; j++)
|
||||
{// j only increased if a valid char was found.
|
||||
int c = IA[sArr[s++]];
|
||||
if (c >= 0) {
|
||||
i |= c << (18 - j * 6);
|
||||
}
|
||||
else {
|
||||
j--;
|
||||
}
|
||||
}
|
||||
// Add the bytes
|
||||
dArr[d++] = (byte) (i >> 16);
|
||||
if (d < len) {
|
||||
dArr[d++] = (byte) (i >> 8);
|
||||
if (d < len) {
|
||||
dArr[d++] = (byte) i;
|
||||
}
|
||||
}
|
||||
}
|
||||
return dArr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes a BASE64 encoded char array that is known to be resonably well formatted. The method is about twice as
|
||||
* fast as {@link #decode(char[])}. The preconditions are:<br>
|
||||
* + The array must have a line length of 76 chars OR no line separators at all (one line).<br>
|
||||
* + Line separator must be "\r\n", as specified in RFC 2045
|
||||
* + The array must not contain illegal characters within the encoded string<br>
|
||||
* + The array CAN have illegal characters at the beginning and end, those will be dealt with appropriately.<br>
|
||||
*
|
||||
* @param sArr The source array. Length 0 will return an empty array. {@code null} will throw an exception.
|
||||
* @return The decoded array of bytes. May be of length 0.
|
||||
*/
|
||||
public static byte[] decodeFast(char[] sArr) {
|
||||
// Check special case
|
||||
int sLen = sArr.length;
|
||||
if (sLen == 0) {
|
||||
return new byte[0];
|
||||
}
|
||||
|
||||
int sIx = 0, eIx = sLen - 1;// Start and end index after trimming.
|
||||
|
||||
// Trim illegal chars from start
|
||||
while (sIx < eIx && IA[sArr[sIx]] < 0) {
|
||||
sIx++;
|
||||
}
|
||||
|
||||
// Trim illegal chars from end
|
||||
while (eIx > 0 && IA[sArr[eIx]] < 0) {
|
||||
eIx--;
|
||||
}
|
||||
|
||||
// get the padding count (=) (0, 1 or 2)
|
||||
int pad = sArr[eIx] == '=' ? (sArr[eIx - 1] == '=' ? 2 : 1) : 0;// Count '=' at end.
|
||||
int cCnt = eIx - sIx + 1;// Content count including possible separators
|
||||
int sepCnt = sLen > 76 ? (sArr[76] == '\r' ? cCnt / 78 : 0) << 1 : 0;
|
||||
|
||||
int len = ((cCnt - sepCnt) * 6 >> 3) - pad;// The number of decoded bytes
|
||||
byte[] dArr = new byte[len];// Preallocate byte[] of exact length
|
||||
|
||||
// Decode all but the last 0 - 2 bytes.
|
||||
int d = 0;
|
||||
for (int cc = 0, eLen = (len / 3) * 3; d < eLen;) {
|
||||
// Assemble three bytes into an int from four "valid" characters.
|
||||
int i = IA[sArr[sIx++]] << 18 | IA[sArr[sIx++]] << 12 | IA[sArr[sIx++]] << 6 | IA[sArr[sIx++]];
|
||||
|
||||
// Add the bytes
|
||||
dArr[d++] = (byte) (i >> 16);
|
||||
dArr[d++] = (byte) (i >> 8);
|
||||
dArr[d++] = (byte) i;
|
||||
|
||||
// If line separator, jump over it.
|
||||
if (sepCnt > 0 && ++cc == 19) {
|
||||
sIx += 2;
|
||||
cc = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (d < len) {
|
||||
// Decode last 1-3 bytes (incl '=') into 1-3 bytes
|
||||
int i = 0;
|
||||
for (int j = 0; sIx <= eIx - pad; j++) {
|
||||
i |= IA[sArr[sIx++]] << (18 - j * 6);
|
||||
}
|
||||
|
||||
for (int r = 16; d < len; r -= 8) {
|
||||
dArr[d++] = (byte) (i >> r);
|
||||
}
|
||||
}
|
||||
|
||||
return dArr;
|
||||
}
|
||||
|
||||
// ****************************************************************************************
|
||||
// * byte[] version
|
||||
// ****************************************************************************************
|
||||
|
||||
/**
|
||||
* Encodes a raw byte array into a BASE64 {@code byte[]} representation i accordance with RFC 2045.
|
||||
*
|
||||
* @param sArr The bytes to convert. If {@code null} or length 0 an empty array will be returned.
|
||||
* @param lineSep Optional "\r\n" after 76 characters, unless end of file.<br>
|
||||
* No line separator will be in breach of RFC 2045 which specifies max 76 per line but will be a
|
||||
* little faster.
|
||||
* @return A BASE64 encoded array. Never {@code null}.
|
||||
*/
|
||||
public static byte[] encodeToByte(byte[] sArr, boolean lineSep) {
|
||||
// Check special case
|
||||
int sLen = sArr != null ? sArr.length : 0;
|
||||
if (sLen == 0) {
|
||||
return new byte[0];
|
||||
}
|
||||
|
||||
int eLen = (sLen / 3) * 3;// Length of even 24-bits.
|
||||
int cCnt = ((sLen - 1) / 3 + 1) << 2;// Returned character count
|
||||
int dLen = cCnt + (lineSep ? (cCnt - 1) / 76 << 1 : 0);// Length of returned array
|
||||
byte[] dArr = new byte[dLen];
|
||||
|
||||
// Encode even 24-bits
|
||||
for (int s = 0, d = 0, cc = 0; s < eLen;) {
|
||||
// Copy next three bytes into lower 24 bits of int, paying attension to sign.
|
||||
int i = (sArr[s++] & 0xff) << 16 | (sArr[s++] & 0xff) << 8 | (sArr[s++] & 0xff);
|
||||
|
||||
// Encode the int into four chars
|
||||
dArr[d++] = (byte) CA[(i >>> 18) & 0x3f];
|
||||
dArr[d++] = (byte) CA[(i >>> 12) & 0x3f];
|
||||
dArr[d++] = (byte) CA[(i >>> 6) & 0x3f];
|
||||
dArr[d++] = (byte) CA[i & 0x3f];
|
||||
|
||||
// Add optional line separator
|
||||
if (lineSep && ++cc == 19 && d < dLen - 2) {
|
||||
dArr[d++] = '\r';
|
||||
dArr[d++] = '\n';
|
||||
cc = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Pad and encode last bits if source isn't an even 24 bits.
|
||||
int left = sLen - eLen;// 0 - 2.
|
||||
if (left > 0) {
|
||||
// Prepare the int
|
||||
int i = ((sArr[eLen] & 0xff) << 10) | (left == 2 ? ((sArr[sLen - 1] & 0xff) << 2) : 0);
|
||||
|
||||
// Set last four chars
|
||||
dArr[dLen - 4] = (byte) CA[i >> 12];
|
||||
dArr[dLen - 3] = (byte) CA[(i >>> 6) & 0x3f];
|
||||
dArr[dLen - 2] = left == 2 ? (byte) CA[i & 0x3f] : (byte) '=';
|
||||
dArr[dLen - 1] = '=';
|
||||
}
|
||||
return dArr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes a BASE64 encoded byte array. All illegal characters will be ignored and can handle both arrays with
|
||||
* and without line separators.
|
||||
*
|
||||
* @param sArr The source array. Length 0 will return an empty array. {@code null} will throw an exception.
|
||||
* @return The decoded array of bytes. May be of length 0. Will be {@code null} if the legal characters
|
||||
* (including '=') isn't divideable by 4. (I.e. definitely corrupted).
|
||||
*/
|
||||
public static byte[] decode(byte[] sArr) {
|
||||
// Check special case
|
||||
int sLen = sArr.length;
|
||||
|
||||
// Count illegal characters (including '\r', '\n') to know what size the returned array will be,
|
||||
// so we don't have to reallocate & copy it later.
|
||||
int sepCnt = 0;// Number of separator characters. (Actually illegal characters, but that's a bonus...)
|
||||
for (int i = 0; i < sLen; i++)// If input is "pure" (I.e. no line separators or illegal chars) base64 this loop can be commented out.
|
||||
{
|
||||
if (IA[sArr[i] & 0xff] < 0) {
|
||||
sepCnt++;
|
||||
}
|
||||
}
|
||||
|
||||
// Check so that legal chars (including '=') are evenly divideable by 4 as specified in RFC 2045.
|
||||
if ((sLen - sepCnt) % 4 != 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int pad = 0;
|
||||
for (int i = sLen; i > 1 && IA[sArr[--i] & 0xff] <= 0;) {
|
||||
if (sArr[i] == '=') {
|
||||
pad++;
|
||||
}
|
||||
}
|
||||
|
||||
int len = ((sLen - sepCnt) * 6 >> 3) - pad;
|
||||
|
||||
byte[] dArr = new byte[len];// Preallocate byte[] of exact length
|
||||
|
||||
for (int s = 0, d = 0; d < len;) {
|
||||
// Assemble three bytes into an int from four "valid" characters.
|
||||
int i = 0;
|
||||
for (int j = 0; j < 4; j++)
|
||||
{// j only increased if a valid char was found.
|
||||
int c = IA[sArr[s++] & 0xff];
|
||||
if (c >= 0) {
|
||||
i |= c << (18 - j * 6);
|
||||
}
|
||||
else {
|
||||
j--;
|
||||
}
|
||||
}
|
||||
|
||||
// Add the bytes
|
||||
dArr[d++] = (byte) (i >> 16);
|
||||
if (d < len) {
|
||||
dArr[d++] = (byte) (i >> 8);
|
||||
if (d < len) {
|
||||
dArr[d++] = (byte) i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return dArr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes a BASE64 encoded byte array that is known to be resonably well formatted. The method is about twice as
|
||||
* fast as {@link #decode(byte[])}. The preconditions are:<br>
|
||||
* + The array must have a line length of 76 chars OR no line separators at all (one line).<br>
|
||||
* + Line separator must be "\r\n", as specified in RFC 2045
|
||||
* + The array must not contain illegal characters within the encoded string<br>
|
||||
* + The array CAN have illegal characters at the beginning and end, those will be dealt with appropriately.<br>
|
||||
*
|
||||
* @param sArr The source array. Length 0 will return an empty array. {@code null} will throw an exception.
|
||||
* @return The decoded array of bytes. May be of length 0.
|
||||
*/
|
||||
public static byte[] decodeFast(byte[] sArr) {
|
||||
// Check special case
|
||||
int sLen = sArr.length;
|
||||
if (sLen == 0) {
|
||||
return new byte[0];
|
||||
}
|
||||
|
||||
int sIx = 0, eIx = sLen - 1;// Start and end index after trimming.
|
||||
|
||||
// Trim illegal chars from start
|
||||
while (sIx < eIx && IA[sArr[sIx] & 0xff] < 0) {
|
||||
sIx++;
|
||||
}
|
||||
|
||||
// Trim illegal chars from end
|
||||
while (eIx > 0 && IA[sArr[eIx] & 0xff] < 0) {
|
||||
eIx--;
|
||||
}
|
||||
|
||||
// get the padding count (=) (0, 1 or 2)
|
||||
int pad = sArr[eIx] == '=' ? (sArr[eIx - 1] == '=' ? 2 : 1) : 0;// Count '=' at end.
|
||||
int cCnt = eIx - sIx + 1;// Content count including possible separators
|
||||
int sepCnt = sLen > 76 ? (sArr[76] == '\r' ? cCnt / 78 : 0) << 1 : 0;
|
||||
|
||||
int len = ((cCnt - sepCnt) * 6 >> 3) - pad;// The number of decoded bytes
|
||||
byte[] dArr = new byte[len];// Preallocate byte[] of exact length
|
||||
|
||||
// Decode all but the last 0 - 2 bytes.
|
||||
int d = 0;
|
||||
for (int cc = 0, eLen = (len / 3) * 3; d < eLen;) {
|
||||
// Assemble three bytes into an int from four "valid" characters.
|
||||
int i = IA[sArr[sIx++]] << 18 | IA[sArr[sIx++]] << 12 | IA[sArr[sIx++]] << 6 | IA[sArr[sIx++]];
|
||||
|
||||
// Add the bytes
|
||||
dArr[d++] = (byte) (i >> 16);
|
||||
dArr[d++] = (byte) (i >> 8);
|
||||
dArr[d++] = (byte) i;
|
||||
|
||||
// If line separator, jump over it.
|
||||
if (sepCnt > 0 && ++cc == 19) {
|
||||
sIx += 2;
|
||||
cc = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (d < len) {
|
||||
// Decode last 1-3 bytes (incl '=') into 1-3 bytes
|
||||
int i = 0;
|
||||
for (int j = 0; sIx <= eIx - pad; j++) {
|
||||
i |= IA[sArr[sIx++]] << (18 - j * 6);
|
||||
}
|
||||
|
||||
for (int r = 16; d < len; r -= 8) {
|
||||
dArr[d++] = (byte) (i >> r);
|
||||
}
|
||||
}
|
||||
|
||||
return dArr;
|
||||
}
|
||||
|
||||
// ****************************************************************************************
|
||||
// * String version
|
||||
// ****************************************************************************************
|
||||
|
||||
/**
|
||||
* Encodes a raw byte array into a BASE64 {@code String} representation i accordance with RFC 2045.
|
||||
*
|
||||
* @param sArr The bytes to convert. If {@code null} or length 0 an empty array will be returned.
|
||||
* @param lineSep Optional "\r\n" after 76 characters, unless end of file.<br>
|
||||
* No line separator will be in breach of RFC 2045 which specifies max 76 per line but will be a
|
||||
* little faster.
|
||||
* @return A BASE64 encoded array. Never {@code null}.
|
||||
*/
|
||||
public static String encodeToString(byte[] sArr, boolean lineSep) {
|
||||
// Reuse char[] since we can't create a String incrementally anyway and StringBuffer/Builder would be slower.
|
||||
return new String(encodeToChar(sArr, lineSep));
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes a BASE64 encoded {@code String}. All illegal characters will be ignored and can handle both strings with
|
||||
* and without line separators.<br>
|
||||
* <b>Note!</b> It can be up to about 2x the speed to call {@code decode(str.toCharArray())} instead. That
|
||||
* will create a temporary array though. This version will use {@code str.charAt(i)} to iterate the string.
|
||||
*
|
||||
* @param str The source string. {@code null} or length 0 will return an empty array.
|
||||
* @return The decoded array of bytes. May be of length 0. Will be {@code null} if the legal characters
|
||||
* (including '=') isn't divideable by 4. (I.e. definitely corrupted).
|
||||
*/
|
||||
public static byte[] decode(String str) {
|
||||
// Check special case
|
||||
int sLen = str != null ? str.length() : 0;
|
||||
if (sLen == 0) {
|
||||
return new byte[0];
|
||||
}
|
||||
|
||||
// Count illegal characters (including '\r', '\n') to know what size the returned array will be,
|
||||
// so we don't have to reallocate & copy it later.
|
||||
int sepCnt = 0;// Number of separator characters. (Actually illegal characters, but that's a bonus...)
|
||||
for (int i = 0; i < sLen; i++)// If input is "pure" (I.e. no line separators or illegal chars) base64 this loop can be commented out.
|
||||
{
|
||||
if (IA[str.charAt(i)] < 0) {
|
||||
sepCnt++;
|
||||
}
|
||||
}
|
||||
|
||||
// Check so that legal chars (including '=') are evenly divideable by 4 as specified in RFC 2045.
|
||||
if ((sLen - sepCnt) % 4 != 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Count '=' at end
|
||||
int pad = 0;
|
||||
for (int i = sLen; i > 1 && IA[str.charAt(--i)] <= 0;) {
|
||||
if (str.charAt(i) == '=') {
|
||||
pad++;
|
||||
}
|
||||
}
|
||||
|
||||
int len = ((sLen - sepCnt) * 6 >> 3) - pad;
|
||||
|
||||
byte[] dArr = new byte[len];// Preallocate byte[] of exact length
|
||||
|
||||
for (int s = 0, d = 0; d < len;) {
|
||||
// Assemble three bytes into an int from four "valid" characters.
|
||||
int i = 0;
|
||||
for (int j = 0; j < 4; j++)
|
||||
{// j only increased if a valid char was found.
|
||||
int c = IA[str.charAt(s++)];
|
||||
if (c >= 0) {
|
||||
i |= c << (18 - j * 6);
|
||||
}
|
||||
else {
|
||||
j--;
|
||||
}
|
||||
}
|
||||
// Add the bytes
|
||||
dArr[d++] = (byte) (i >> 16);
|
||||
if (d < len) {
|
||||
dArr[d++] = (byte) (i >> 8);
|
||||
if (d < len) {
|
||||
dArr[d++] = (byte) i;
|
||||
}
|
||||
}
|
||||
}
|
||||
return dArr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes a BASE64 encoded string that is known to be resonably well formatted. The method is about twice as
|
||||
* fast as {@link #decode(String)}. The preconditions are:<br>
|
||||
* + The array must have a line length of 76 chars OR no line separators at all (one line).<br>
|
||||
* + Line separator must be "\r\n", as specified in RFC 2045
|
||||
* + The array must not contain illegal characters within the encoded string<br>
|
||||
* + The array CAN have illegal characters at the beginning and end, those will be dealt with appropriately.<br>
|
||||
*
|
||||
* @param s The source string. Length 0 will return an empty array. {@code null} will throw an exception.
|
||||
* @return The decoded array of bytes. May be of length 0.
|
||||
*/
|
||||
public static byte[] decodeFast(String s) {
|
||||
// Check special case
|
||||
int sLen = s.length();
|
||||
if (sLen == 0) {
|
||||
return new byte[0];
|
||||
}
|
||||
|
||||
int sIx = 0, eIx = sLen - 1;// Start and end index after trimming.
|
||||
|
||||
// Trim illegal chars from start
|
||||
while (sIx < eIx && IA[s.charAt(sIx) & 0xff] < 0) {
|
||||
sIx++;
|
||||
}
|
||||
|
||||
// Trim illegal chars from end
|
||||
while (eIx > 0 && IA[s.charAt(eIx) & 0xff] < 0) {
|
||||
eIx--;
|
||||
}
|
||||
|
||||
// get the padding count (=) (0, 1 or 2)
|
||||
int pad = s.charAt(eIx) == '=' ? (s.charAt(eIx - 1) == '=' ? 2 : 1) : 0;// Count '=' at end.
|
||||
int cCnt = eIx - sIx + 1;// Content count including possible separators
|
||||
int sepCnt = sLen > 76 ? (s.charAt(76) == '\r' ? cCnt / 78 : 0) << 1 : 0;
|
||||
|
||||
int len = ((cCnt - sepCnt) * 6 >> 3) - pad;// The number of decoded bytes
|
||||
byte[] dArr = new byte[len];// Preallocate byte[] of exact length
|
||||
|
||||
// Decode all but the last 0 - 2 bytes.
|
||||
int d = 0;
|
||||
for (int cc = 0, eLen = (len / 3) * 3; d < eLen;) {
|
||||
// Assemble three bytes into an int from four "valid" characters.
|
||||
int i = IA[s.charAt(sIx++)] << 18 | IA[s.charAt(sIx++)] << 12 | IA[s.charAt(sIx++)] << 6 | IA[s.charAt(sIx++)];
|
||||
|
||||
// Add the bytes
|
||||
dArr[d++] = (byte) (i >> 16);
|
||||
dArr[d++] = (byte) (i >> 8);
|
||||
dArr[d++] = (byte) i;
|
||||
|
||||
// If line separator, jump over it.
|
||||
if (sepCnt > 0 && ++cc == 19) {
|
||||
sIx += 2;
|
||||
cc = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (d < len) {
|
||||
// Decode last 1-3 bytes (incl '=') into 1-3 bytes
|
||||
int i = 0;
|
||||
for (int j = 0; sIx <= eIx - pad; j++) {
|
||||
i |= IA[s.charAt(sIx++)] << (18 - j * 6);
|
||||
}
|
||||
|
||||
for (int r = 16; d < len; r -= 8) {
|
||||
dArr[d++] = (byte) (i >> r);
|
||||
}
|
||||
}
|
||||
|
||||
return dArr;
|
||||
}
|
||||
}
|
||||
@@ -28,9 +28,9 @@
|
||||
|
||||
package com.twelvemonkeys.io.enc;
|
||||
|
||||
import com.twelvemonkeys.io.FastByteArrayOutputStream;
|
||||
|
||||
import java.io.*;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* {@code Decoder} implementation for standard base64 encoding.
|
||||
@@ -47,7 +47,7 @@ public final class Base64Decoder implements Decoder {
|
||||
/**
|
||||
* This array maps the characters to their 6 bit values
|
||||
*/
|
||||
final static char[] PEM_ARRAY = {
|
||||
final static byte[] PEM_ARRAY = {
|
||||
//0 1 2 3 4 5 6 7
|
||||
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', // 0
|
||||
'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', // 1
|
||||
@@ -62,8 +62,6 @@ public final class Base64Decoder implements Decoder {
|
||||
final static byte[] PEM_CONVERT_ARRAY;
|
||||
|
||||
private byte[] decodeBuffer = new byte[4];
|
||||
private ByteArrayOutputStream wrapped;
|
||||
private Object wrappedObject;
|
||||
|
||||
static {
|
||||
PEM_CONVERT_ARRAY = new byte[256];
|
||||
@@ -93,7 +91,7 @@ public final class Base64Decoder implements Decoder {
|
||||
return pLength;
|
||||
}
|
||||
|
||||
protected boolean decodeAtom(final InputStream pInput, final OutputStream pOutput, final int pLength)
|
||||
protected boolean decodeAtom(final InputStream pInput, final ByteBuffer pOutput, final int pLength)
|
||||
throws IOException {
|
||||
|
||||
byte byte0 = -1;
|
||||
@@ -147,16 +145,16 @@ public final class Base64Decoder implements Decoder {
|
||||
default:
|
||||
switch (length) {
|
||||
case 2:
|
||||
pOutput.write((byte) (byte0 << 2 & 252 | byte1 >>> 4 & 3));
|
||||
pOutput.put((byte) (byte0 << 2 & 252 | byte1 >>> 4 & 3));
|
||||
break;
|
||||
case 3:
|
||||
pOutput.write((byte) (byte0 << 2 & 252 | byte1 >>> 4 & 3));
|
||||
pOutput.write((byte) (byte1 << 4 & 240 | byte2 >>> 2 & 15));
|
||||
pOutput.put((byte) (byte0 << 2 & 252 | byte1 >>> 4 & 3));
|
||||
pOutput.put((byte) (byte1 << 4 & 240 | byte2 >>> 2 & 15));
|
||||
break;
|
||||
case 4:
|
||||
pOutput.write((byte) (byte0 << 2 & 252 | byte1 >>> 4 & 3));
|
||||
pOutput.write((byte) (byte1 << 4 & 240 | byte2 >>> 2 & 15));
|
||||
pOutput.write((byte) (byte2 << 6 & 192 | byte3 & 63));
|
||||
pOutput.put((byte) (byte0 << 2 & 252 | byte1 >>> 4 & 3));
|
||||
pOutput.put((byte) (byte1 << 4 & 240 | byte2 >>> 2 & 15));
|
||||
pOutput.put((byte) (byte2 << 6 & 192 | byte3 & 63));
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -166,34 +164,23 @@ public final class Base64Decoder implements Decoder {
|
||||
return true;
|
||||
}
|
||||
|
||||
void decodeBuffer(final InputStream pInput, final ByteArrayOutputStream pOutput, final int pLength) throws IOException {
|
||||
public int decode(final InputStream stream, final ByteBuffer buffer) throws IOException {
|
||||
do {
|
||||
int k = 72;
|
||||
int i;
|
||||
|
||||
for (i = 0; i + 4 < k; i += 4) {
|
||||
if(!decodeAtom(pInput, pOutput, 4)) {
|
||||
if(!decodeAtom(stream, buffer, 4)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!decodeAtom(pInput, pOutput, k - i)) {
|
||||
if (!decodeAtom(stream, buffer, k - i)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
while (pOutput.size() + 54 < pLength); // 72 char lines should produce no more than 54 bytes
|
||||
}
|
||||
while (buffer.remaining() > 54); // 72 char lines should produce no more than 54 bytes
|
||||
|
||||
public int decode(final InputStream pStream, final byte[] pBuffer) throws IOException {
|
||||
if (wrappedObject != pBuffer) {
|
||||
// NOTE: Array not cloned in FastByteArrayOutputStream
|
||||
wrapped = new FastByteArrayOutputStream(pBuffer);
|
||||
wrappedObject = pBuffer;
|
||||
}
|
||||
|
||||
wrapped.reset(); // NOTE: This only resets count to 0
|
||||
decodeBuffer(pStream, wrapped, pBuffer.length);
|
||||
|
||||
return wrapped.size();
|
||||
return buffer.position();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ package com.twelvemonkeys.io.enc;
|
||||
|
||||
import java.io.OutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* {@code Encoder} implementation for standard base64 encoding.
|
||||
@@ -44,15 +45,9 @@ import java.io.IOException;
|
||||
*/
|
||||
public class Base64Encoder implements Encoder {
|
||||
|
||||
public void encode(final OutputStream pStream, final byte[] pBuffer, final int pOffset, final int pLength)
|
||||
public void encode(final OutputStream stream, final ByteBuffer buffer)
|
||||
throws IOException
|
||||
{
|
||||
if (pOffset < 0 || pOffset > pLength || pOffset > pBuffer.length) {
|
||||
throw new IndexOutOfBoundsException("offset outside [0...length]");
|
||||
}
|
||||
else if (pLength > pBuffer.length) {
|
||||
throw new IndexOutOfBoundsException("length > buffer length");
|
||||
}
|
||||
|
||||
// TODO: Implement
|
||||
// NOTE: This is impossible, given the current spec, as we need to either:
|
||||
@@ -61,48 +56,47 @@ public class Base64Encoder implements Encoder {
|
||||
// to ensure proper end of stream handling
|
||||
|
||||
int length;
|
||||
int offset = pOffset;
|
||||
|
||||
// TODO: Temp impl, will only work for single writes
|
||||
while ((pBuffer.length - offset) > 0) {
|
||||
while (buffer.hasRemaining()) {
|
||||
byte a, b, c;
|
||||
|
||||
if ((pBuffer.length - offset) > 2) {
|
||||
length = 3;
|
||||
}
|
||||
else {
|
||||
length = pBuffer.length - offset;
|
||||
}
|
||||
// if ((buffer.remaining()) > 2) {
|
||||
// length = 3;
|
||||
// }
|
||||
// else {
|
||||
// length = buffer.remaining();
|
||||
// }
|
||||
length = Math.min(3, buffer.remaining());
|
||||
|
||||
switch (length) {
|
||||
case 1:
|
||||
a = pBuffer[offset];
|
||||
a = buffer.get();
|
||||
b = 0;
|
||||
pStream.write(Base64Decoder.PEM_ARRAY[(a >>> 2) & 0x3F]);
|
||||
pStream.write(Base64Decoder.PEM_ARRAY[((a << 4) & 0x30) + ((b >>> 4) & 0xf)]);
|
||||
pStream.write('=');
|
||||
pStream.write('=');
|
||||
offset++;
|
||||
stream.write(Base64Decoder.PEM_ARRAY[(a >>> 2) & 0x3F]);
|
||||
stream.write(Base64Decoder.PEM_ARRAY[((a << 4) & 0x30) + ((b >>> 4) & 0xf)]);
|
||||
stream.write('=');
|
||||
stream.write('=');
|
||||
break;
|
||||
|
||||
case 2:
|
||||
a = pBuffer[offset];
|
||||
b = pBuffer[offset + 1];
|
||||
a = buffer.get();
|
||||
b = buffer.get();
|
||||
c = 0;
|
||||
pStream.write(Base64Decoder.PEM_ARRAY[(a >>> 2) & 0x3F]);
|
||||
pStream.write(Base64Decoder.PEM_ARRAY[((a << 4) & 0x30) + ((b >>> 4) & 0xf)]);
|
||||
pStream.write(Base64Decoder.PEM_ARRAY[((b << 2) & 0x3c) + ((c >>> 6) & 0x3)]);
|
||||
pStream.write('=');
|
||||
offset += offset + 2; // ???
|
||||
stream.write(Base64Decoder.PEM_ARRAY[(a >>> 2) & 0x3F]);
|
||||
stream.write(Base64Decoder.PEM_ARRAY[((a << 4) & 0x30) + ((b >>> 4) & 0xf)]);
|
||||
stream.write(Base64Decoder.PEM_ARRAY[((b << 2) & 0x3c) + ((c >>> 6) & 0x3)]);
|
||||
stream.write('=');
|
||||
break;
|
||||
|
||||
default:
|
||||
a = pBuffer[offset];
|
||||
b = pBuffer[offset + 1];
|
||||
c = pBuffer[offset + 2];
|
||||
pStream.write(Base64Decoder.PEM_ARRAY[(a >>> 2) & 0x3F]);
|
||||
pStream.write(Base64Decoder.PEM_ARRAY[((a << 4) & 0x30) + ((b >>> 4) & 0xf)]);
|
||||
pStream.write(Base64Decoder.PEM_ARRAY[((b << 2) & 0x3c) + ((c >>> 6) & 0x3)]);
|
||||
pStream.write(Base64Decoder.PEM_ARRAY[c & 0x3F]);
|
||||
offset = offset + 3;
|
||||
a = buffer.get();
|
||||
b = buffer.get();
|
||||
c = buffer.get();
|
||||
stream.write(Base64Decoder.PEM_ARRAY[(a >>> 2) & 0x3F]);
|
||||
stream.write(Base64Decoder.PEM_ARRAY[((a << 4) & 0x30) + ((b >>> 4) & 0xf)]);
|
||||
stream.write(Base64Decoder.PEM_ARRAY[((b << 2) & 0x3c) + ((c >>> 6) & 0x3)]);
|
||||
stream.write(Base64Decoder.PEM_ARRAY[c & 0x3F]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,8 +28,9 @@
|
||||
|
||||
package com.twelvemonkeys.io.enc;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* Interface for decoders.
|
||||
@@ -47,11 +48,11 @@ import java.io.IOException;
|
||||
public interface Decoder {
|
||||
|
||||
/**
|
||||
* Decodes up to {@code pBuffer.length} bytes from the given input stream,
|
||||
* Decodes up to {@code buffer.length} bytes from the given input stream,
|
||||
* into the given buffer.
|
||||
*
|
||||
* @param pStream the input stream to decode data from
|
||||
* @param pBuffer buffer to store the read data
|
||||
* @param stream the input stream to decode data from
|
||||
* @param buffer buffer to store the read data
|
||||
*
|
||||
* @return the total number of bytes read into the buffer, or {@code 0}
|
||||
* if there is no more data because the end of the stream has been reached.
|
||||
@@ -60,5 +61,5 @@ public interface Decoder {
|
||||
* @throws IOException if an I/O error occurs
|
||||
* @throws java.io.EOFException if a premature end-of-file is encountered
|
||||
*/
|
||||
int decode(InputStream pStream, byte[] pBuffer) throws IOException;
|
||||
int decode(InputStream stream, ByteBuffer buffer) throws IOException;
|
||||
}
|
||||
|
||||
@@ -28,9 +28,10 @@
|
||||
|
||||
package com.twelvemonkeys.io.enc;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.FilterInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* An {@code InputStream} that provides on-the-fly decoding from an underlying
|
||||
@@ -43,10 +44,7 @@ import java.io.FilterInputStream;
|
||||
* @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/DecoderStream.java#2 $
|
||||
*/
|
||||
public final class DecoderStream extends FilterInputStream {
|
||||
|
||||
protected int bufferPos;
|
||||
protected int bufferLimit;
|
||||
protected final byte[] buffer;
|
||||
protected final ByteBuffer buffer;
|
||||
protected final Decoder decoder;
|
||||
|
||||
/**
|
||||
@@ -76,30 +74,24 @@ public final class DecoderStream extends FilterInputStream {
|
||||
*/
|
||||
public DecoderStream(final InputStream pStream, final Decoder pDecoder, final int pBufferSize) {
|
||||
super(pStream);
|
||||
|
||||
decoder = pDecoder;
|
||||
buffer = new byte[pBufferSize];
|
||||
bufferPos = 0;
|
||||
bufferLimit = 0;
|
||||
buffer = ByteBuffer.allocate(pBufferSize);
|
||||
buffer.flip();
|
||||
}
|
||||
|
||||
public int available() throws IOException {
|
||||
return bufferLimit - bufferPos + super.available();
|
||||
return buffer.remaining();
|
||||
}
|
||||
|
||||
public int read() throws IOException {
|
||||
if (bufferPos == bufferLimit) {
|
||||
bufferLimit = fill();
|
||||
if (!buffer.hasRemaining()) {
|
||||
if (fill() < 0) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (bufferLimit < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return buffer[bufferPos++] & 0xff;
|
||||
}
|
||||
|
||||
public int read(final byte pBytes[]) throws IOException {
|
||||
return read(pBytes, 0, pBytes.length);
|
||||
return buffer.get() & 0xff;
|
||||
}
|
||||
|
||||
public int read(final byte pBytes[], final int pOffset, final int pLength) throws IOException {
|
||||
@@ -115,8 +107,10 @@ public final class DecoderStream extends FilterInputStream {
|
||||
}
|
||||
|
||||
// End of file?
|
||||
if ((bufferLimit - bufferPos) < 0) {
|
||||
return -1;
|
||||
if (!buffer.hasRemaining()) {
|
||||
if (fill() < 0) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// Read until we have read pLength bytes, or have reached EOF
|
||||
@@ -124,21 +118,15 @@ public final class DecoderStream extends FilterInputStream {
|
||||
int off = pOffset;
|
||||
|
||||
while (pLength > count) {
|
||||
int avail = bufferLimit - bufferPos;
|
||||
|
||||
if (avail <= 0) {
|
||||
bufferLimit = fill();
|
||||
|
||||
if (bufferLimit < 0) {
|
||||
if (!buffer.hasRemaining()) {
|
||||
if (fill() < 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Copy as many bytes as possible
|
||||
int dstLen = Math.min(pLength - count, avail);
|
||||
System.arraycopy(buffer, bufferPos, pBytes, off, dstLen);
|
||||
|
||||
bufferPos += dstLen;
|
||||
int dstLen = Math.min(pLength - count, buffer.remaining());
|
||||
buffer.get(pBytes, off, dstLen);
|
||||
|
||||
// Update offset (rest)
|
||||
off += dstLen;
|
||||
@@ -152,29 +140,25 @@ public final class DecoderStream extends FilterInputStream {
|
||||
|
||||
public long skip(final long pLength) throws IOException {
|
||||
// End of file?
|
||||
if (bufferLimit - bufferPos < 0) {
|
||||
return 0;
|
||||
if (!buffer.hasRemaining()) {
|
||||
if (fill() < 0) {
|
||||
return 0; // Yes, 0, not -1
|
||||
}
|
||||
}
|
||||
|
||||
// Skip until we have skipped pLength bytes, or have reached EOF
|
||||
long total = 0;
|
||||
|
||||
while (total < pLength) {
|
||||
int avail = bufferLimit - bufferPos;
|
||||
|
||||
if (avail == 0) {
|
||||
bufferLimit = fill();
|
||||
|
||||
if (bufferLimit < 0) {
|
||||
if (!buffer.hasRemaining()) {
|
||||
if (fill() < 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: Skipped can never be more than avail, which is
|
||||
// an int, so the cast is safe
|
||||
int skipped = (int) Math.min(pLength - total, avail);
|
||||
|
||||
bufferPos += skipped; // Just skip these bytes
|
||||
int skipped = (int) Math.min(pLength - total, buffer.remaining());
|
||||
total += skipped;
|
||||
}
|
||||
|
||||
@@ -190,19 +174,20 @@ public final class DecoderStream extends FilterInputStream {
|
||||
* @throws IOException if an I/O error occurs
|
||||
*/
|
||||
protected int fill() throws IOException {
|
||||
buffer.clear();
|
||||
int read = decoder.decode(in, buffer);
|
||||
|
||||
// TODO: Enforce this in test case, leave here to aid debugging
|
||||
if (read > buffer.length) {
|
||||
if (read > buffer.capacity()) {
|
||||
throw new AssertionError(
|
||||
String.format(
|
||||
"Decode beyond buffer (%d): %d (using %s decoder)",
|
||||
buffer.length, read, decoder.getClass().getName()
|
||||
buffer.capacity(), read, decoder.getClass().getName()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
bufferPos = 0;
|
||||
buffer.flip();
|
||||
|
||||
if (read == 0) {
|
||||
return -1;
|
||||
|
||||
@@ -30,11 +30,12 @@ package com.twelvemonkeys.io.enc;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* Interface for endcoders.
|
||||
* Interface for encoders.
|
||||
* An {@code Encoder} may be used with an {@code EncoderStream}, to perform
|
||||
* on-the-fly enoding to an {@code OutputStream}.
|
||||
* on-the-fly encoding to an {@code OutputStream}.
|
||||
* <p/>
|
||||
* Important note: Encoder implementations are typically not synchronized.
|
||||
*
|
||||
@@ -47,17 +48,15 @@ import java.io.OutputStream;
|
||||
public interface Encoder {
|
||||
|
||||
/**
|
||||
* Encodes up to {@code pBuffer.length} bytes into the given input stream,
|
||||
* Encodes up to {@code buffer.remaining()} bytes into the given input stream,
|
||||
* from the given buffer.
|
||||
*
|
||||
* @param pStream the outputstream to encode data to
|
||||
* @param pBuffer buffer to read data from
|
||||
* @param pOffset offset into the buffer array
|
||||
* @param pLength length of data in the buffer
|
||||
* @param stream the output stream to encode data to
|
||||
* @param buffer buffer to read data from
|
||||
*
|
||||
* @throws java.io.IOException if an I/O error occurs
|
||||
*/
|
||||
void encode(OutputStream pStream, byte[] pBuffer, int pOffset, int pLength) throws IOException;
|
||||
void encode(OutputStream stream, ByteBuffer buffer) throws IOException;
|
||||
|
||||
//TODO: int requiredBufferSize(): -1 == any, otherwise, use this buffer size
|
||||
// void flush()?
|
||||
|
||||
@@ -29,8 +29,9 @@
|
||||
package com.twelvemonkeys.io.enc;
|
||||
|
||||
import java.io.FilterOutputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* An {@code OutputStream} that provides on-the-fly encoding to an underlying
|
||||
@@ -43,12 +44,12 @@ import java.io.IOException;
|
||||
* @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/EncoderStream.java#2 $
|
||||
*/
|
||||
public final class EncoderStream extends FilterOutputStream {
|
||||
// TODO: This class need a test case ASAP!!!
|
||||
|
||||
protected final Encoder encoder;
|
||||
private final boolean flushOnWrite;
|
||||
|
||||
protected int bufferPos;
|
||||
protected final byte[] buffer;
|
||||
protected final ByteBuffer buffer;
|
||||
|
||||
/**
|
||||
* Creates an output stream filter built on top of the specified
|
||||
@@ -76,8 +77,8 @@ public final class EncoderStream extends FilterOutputStream {
|
||||
encoder = pEncoder;
|
||||
flushOnWrite = pFlushOnWrite;
|
||||
|
||||
buffer = new byte[1024];
|
||||
bufferPos = 0;
|
||||
buffer = ByteBuffer.allocate(1024);
|
||||
buffer.flip();
|
||||
}
|
||||
|
||||
public void close() throws IOException {
|
||||
@@ -91,12 +92,14 @@ public final class EncoderStream extends FilterOutputStream {
|
||||
}
|
||||
|
||||
private void encodeBuffer() throws IOException {
|
||||
if (bufferPos != 0) {
|
||||
if (buffer.position() != 0) {
|
||||
buffer.flip();
|
||||
|
||||
// Make sure all remaining data in buffer is written to the stream
|
||||
encoder.encode(out, buffer, 0, bufferPos);
|
||||
encoder.encode(out, buffer);
|
||||
|
||||
// Reset buffer
|
||||
bufferPos = 0;
|
||||
buffer.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,25 +112,24 @@ public final class EncoderStream extends FilterOutputStream {
|
||||
// that the encoder can't buffer. In that case, the encoder should probably
|
||||
// tell the EncoderStream how large buffer it prefers...
|
||||
public void write(final byte[] pBytes, final int pOffset, final int pLength) throws IOException {
|
||||
if (!flushOnWrite && bufferPos + pLength < buffer.length) {
|
||||
if (!flushOnWrite && pLength < buffer.remaining()) {
|
||||
// Buffer data
|
||||
System.arraycopy(pBytes, pOffset, buffer, bufferPos, pLength);
|
||||
bufferPos += pLength;
|
||||
buffer.put(pBytes, pOffset, pLength);
|
||||
}
|
||||
else {
|
||||
// Encode data already in the buffer
|
||||
encodeBuffer();
|
||||
|
||||
// Encode rest without buffering
|
||||
encoder.encode(out, pBytes, pOffset, pLength);
|
||||
encoder.encode(out, ByteBuffer.wrap(pBytes, pOffset, pLength));
|
||||
}
|
||||
}
|
||||
|
||||
public void write(final int pByte) throws IOException {
|
||||
if (bufferPos >= buffer.length - 1) {
|
||||
if (!buffer.hasRemaining()) {
|
||||
encodeBuffer(); // Resets bufferPos to 0
|
||||
}
|
||||
|
||||
buffer[bufferPos++] = (byte) pByte;
|
||||
buffer.put((byte) pByte);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,9 +28,10 @@
|
||||
|
||||
package com.twelvemonkeys.io.enc;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* Decoder implementation for 16 bit-chunked Apple PackBits-like run-length
|
||||
@@ -77,20 +78,20 @@ public final class PackBits16Decoder implements Decoder {
|
||||
/**
|
||||
* Decodes bytes from the given input stream, to the given buffer.
|
||||
*
|
||||
* @param pStream the stream to decode from
|
||||
* @param pBuffer a byte array, minimum 128 (or 129 if no-op is disabled)
|
||||
* @param stream the stream to decode from
|
||||
* @param buffer a byte array, minimum 128 (or 129 if no-op is disabled)
|
||||
* bytes long
|
||||
* @return The number of bytes decoded
|
||||
*
|
||||
* @throws java.io.IOException
|
||||
*/
|
||||
public int decode(final InputStream pStream, final byte[] pBuffer) throws IOException {
|
||||
public int decode(final InputStream stream, final ByteBuffer buffer) throws IOException {
|
||||
if (reachedEOF) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int read = 0;
|
||||
final int max = pBuffer.length;
|
||||
final int max = buffer.capacity();
|
||||
|
||||
while (read < max) {
|
||||
int n;
|
||||
@@ -102,7 +103,7 @@ public final class PackBits16Decoder implements Decoder {
|
||||
}
|
||||
else {
|
||||
// Start new run
|
||||
int b = pStream.read();
|
||||
int b = stream.read();
|
||||
if (b < 0) {
|
||||
reachedEOF = true;
|
||||
break;
|
||||
@@ -126,18 +127,18 @@ public final class PackBits16Decoder implements Decoder {
|
||||
if (n >= 0) {
|
||||
// Copy next n + 1 shorts literally
|
||||
int len = 2 * (n + 1);
|
||||
readFully(pStream, pBuffer, read, len);
|
||||
readFully(stream, buffer, len);
|
||||
read += len;
|
||||
}
|
||||
// Allow -128 for compatibility, see above
|
||||
else if (disableNoop || n != -128) {
|
||||
// Replicate the next short -n + 1 times
|
||||
byte value1 = readByte(pStream);
|
||||
byte value2 = readByte(pStream);
|
||||
byte value1 = readByte(stream);
|
||||
byte value2 = readByte(stream);
|
||||
|
||||
for (int i = -n + 1; i > 0; i--) {
|
||||
pBuffer[read++] = value1;
|
||||
pBuffer[read++] = value2;
|
||||
buffer.put(value1);
|
||||
buffer.put(value2);
|
||||
}
|
||||
}
|
||||
// else NOOP (-128)
|
||||
@@ -160,7 +161,7 @@ public final class PackBits16Decoder implements Decoder {
|
||||
return (byte) read;
|
||||
}
|
||||
|
||||
private static void readFully(final InputStream pStream, final byte[] pBuffer, final int pOffset, final int pLength) throws IOException {
|
||||
private static void readFully(final InputStream pStream, final ByteBuffer pBuffer, final int pLength) throws IOException {
|
||||
if (pLength < 0) {
|
||||
throw new IndexOutOfBoundsException();
|
||||
}
|
||||
@@ -168,7 +169,7 @@ public final class PackBits16Decoder implements Decoder {
|
||||
int read = 0;
|
||||
|
||||
while (read < pLength) {
|
||||
int count = pStream.read(pBuffer, pOffset + read, pLength - read);
|
||||
int count = pStream.read(pBuffer.array(), pBuffer.arrayOffset() + pBuffer.position() + read, pLength - read);
|
||||
|
||||
if (count < 0) {
|
||||
throw new EOFException("Unexpected end of PackBits stream");
|
||||
|
||||
@@ -31,6 +31,7 @@ package com.twelvemonkeys.io.enc;
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* Decoder implementation for Apple PackBits run-length encoding.
|
||||
@@ -92,22 +93,19 @@ public final class PackBitsDecoder implements Decoder {
|
||||
/**
|
||||
* Decodes bytes from the given input stream, to the given buffer.
|
||||
*
|
||||
* @param pStream the stream to decode from
|
||||
* @param pBuffer a byte array, minimum 128 (or 129 if no-op is disabled) bytes long
|
||||
* @param stream the stream to decode from
|
||||
* @param buffer a byte array, minimum 128 (or 129 if no-op is disabled) bytes long
|
||||
* @return The number of bytes decoded
|
||||
*
|
||||
* @throws java.io.IOException
|
||||
*/
|
||||
public int decode(final InputStream pStream, final byte[] pBuffer) throws IOException {
|
||||
public int decode(final InputStream stream, final ByteBuffer buffer) throws IOException {
|
||||
if (reachedEOF) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int read = 0;
|
||||
final int max = pBuffer.length;
|
||||
|
||||
// TODO: Don't decode more than single runs, because some writers add pad bytes inside the stream...
|
||||
while (read < max) {
|
||||
while (buffer.hasRemaining()) {
|
||||
int n;
|
||||
|
||||
if (splitRun) {
|
||||
@@ -117,7 +115,7 @@ public final class PackBitsDecoder implements Decoder {
|
||||
}
|
||||
else {
|
||||
// Start new run
|
||||
int b = pStream.read();
|
||||
int b = stream.read();
|
||||
if (b < 0) {
|
||||
reachedEOF = true;
|
||||
break;
|
||||
@@ -126,12 +124,12 @@ public final class PackBitsDecoder implements Decoder {
|
||||
}
|
||||
|
||||
// Split run at or before max
|
||||
if (n >= 0 && n + 1 + read > max) {
|
||||
if (n >= 0 && n + 1 > buffer.remaining()) {
|
||||
leftOfRun = n;
|
||||
splitRun = true;
|
||||
break;
|
||||
}
|
||||
else if (n < 0 && -n + 1 + read > max) {
|
||||
else if (n < 0 && -n + 1 > buffer.remaining()) {
|
||||
leftOfRun = n;
|
||||
splitRun = true;
|
||||
break;
|
||||
@@ -140,17 +138,15 @@ public final class PackBitsDecoder implements Decoder {
|
||||
try {
|
||||
if (n >= 0) {
|
||||
// Copy next n + 1 bytes literally
|
||||
readFully(pStream, pBuffer, read, n + 1);
|
||||
|
||||
read += n + 1;
|
||||
readFully(stream, buffer, n + 1);
|
||||
}
|
||||
// Allow -128 for compatibility, see above
|
||||
else if (disableNoop || n != -128) {
|
||||
// Replicate the next byte -n + 1 times
|
||||
byte value = readByte(pStream);
|
||||
byte value = readByte(stream);
|
||||
|
||||
for (int i = -n + 1; i > 0; i--) {
|
||||
pBuffer[read++] = value;
|
||||
buffer.put(value);
|
||||
}
|
||||
}
|
||||
// else NOOP (-128)
|
||||
@@ -160,7 +156,7 @@ public final class PackBitsDecoder implements Decoder {
|
||||
}
|
||||
}
|
||||
|
||||
return read;
|
||||
return buffer.position();
|
||||
}
|
||||
|
||||
static byte readByte(final InputStream pStream) throws IOException {
|
||||
@@ -173,7 +169,7 @@ public final class PackBitsDecoder implements Decoder {
|
||||
return (byte) read;
|
||||
}
|
||||
|
||||
static void readFully(final InputStream pStream, final byte[] pBuffer, final int pOffset, final int pLength) throws IOException {
|
||||
static void readFully(final InputStream pStream, final ByteBuffer pBuffer, final int pLength) throws IOException {
|
||||
if (pLength < 0) {
|
||||
throw new IndexOutOfBoundsException(String.format("Negative length: %d", pLength));
|
||||
}
|
||||
@@ -181,7 +177,7 @@ public final class PackBitsDecoder implements Decoder {
|
||||
int total = 0;
|
||||
|
||||
while (total < pLength) {
|
||||
int count = pStream.read(pBuffer, pOffset + total, pLength - total);
|
||||
int count = pStream.read(pBuffer.array(), pBuffer.arrayOffset() + pBuffer.position() + total, pLength - total);
|
||||
|
||||
if (count < 0) {
|
||||
throw new EOFException("Unexpected end of PackBits stream");
|
||||
@@ -189,5 +185,7 @@ public final class PackBitsDecoder implements Decoder {
|
||||
|
||||
total += count;
|
||||
}
|
||||
|
||||
pBuffer.position(pBuffer.position() + total);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ package com.twelvemonkeys.io.enc;
|
||||
|
||||
import java.io.OutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* Encoder implementation for Apple PackBits run-length encoding.
|
||||
@@ -71,7 +72,12 @@ public final class PackBitsEncoder implements Encoder {
|
||||
public PackBitsEncoder() {
|
||||
}
|
||||
|
||||
public void encode(OutputStream pStream, byte[] pBuffer, int pOffset, int pLength) throws IOException {
|
||||
public void encode(final OutputStream stream, final ByteBuffer buffer) throws IOException {
|
||||
encode(stream, buffer.array(), buffer.arrayOffset() + buffer.position(), buffer.remaining());
|
||||
buffer.position(buffer.remaining());
|
||||
}
|
||||
|
||||
private void encode(OutputStream pStream, byte[] pBuffer, int pOffset, int pLength) throws IOException {
|
||||
// NOTE: It's best to encode a 2 byte repeat
|
||||
// run as a replicate run except when preceded and followed by a
|
||||
// literal run, in which case it's best to merge the three into one
|
||||
@@ -86,7 +92,7 @@ public final class PackBitsEncoder implements Encoder {
|
||||
// Compressed run
|
||||
int run = 1;
|
||||
byte replicate = pBuffer[offset];
|
||||
while(run < 127 && offset < max && pBuffer[offset] == pBuffer[offset + 1]) {
|
||||
while (run < 127 && offset < max && pBuffer[offset] == pBuffer[offset + 1]) {
|
||||
offset++;
|
||||
run++;
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ import java.io.InputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Implements 4 bit RLE decoding as specifed by in the Windows BMP (aka DIB) file format.
|
||||
* Implements 4 bit RLE decoding as specified by in the Windows BMP (aka DIB) file format.
|
||||
* <p/>
|
||||
*
|
||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||
@@ -41,7 +41,7 @@ import java.io.IOException;
|
||||
// TODO: Move to other package or make public
|
||||
final class RLE4Decoder extends AbstractRLEDecoder {
|
||||
|
||||
public RLE4Decoder(int pWidth, int pHeight) {
|
||||
public RLE4Decoder(final int pWidth, final int pHeight) {
|
||||
super((pWidth + 1) / 2, pHeight);
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ import java.io.InputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Implements 8 bit RLE decoding as specifed by in the Windows BMP (aka DIB) file format.
|
||||
* Implements 8 bit RLE decoding as specified by in the Windows BMP (aka DIB) file format.
|
||||
* <p/>
|
||||
*
|
||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||
@@ -41,7 +41,7 @@ import java.io.IOException;
|
||||
// TODO: Move to other package or make public
|
||||
final class RLE8Decoder extends AbstractRLEDecoder {
|
||||
|
||||
public RLE8Decoder(int pWidth, int pHeight) {
|
||||
public RLE8Decoder(final int pWidth, final int pHeight) {
|
||||
super(pWidth, pHeight);
|
||||
}
|
||||
|
||||
|
||||
@@ -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.net;
|
||||
|
||||
import com.twelvemonkeys.lang.DateUtil;
|
||||
import com.twelvemonkeys.lang.StringUtil;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
import java.util.TimeZone;
|
||||
|
||||
/**
|
||||
* HTTPUtil
|
||||
*
|
||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||
* @author last modified by $Author: haraldk$
|
||||
* @version $Id: HTTPUtil.java,v 1.0 08.09.13 13:57 haraldk Exp$
|
||||
*/
|
||||
public class HTTPUtil {
|
||||
/**
|
||||
* RFC 1123 date format, as recommended by RFC 2616 (HTTP/1.1), sec 3.3
|
||||
* NOTE: All date formats are private, to ensure synchronized access.
|
||||
*/
|
||||
private static final SimpleDateFormat HTTP_RFC1123_FORMAT = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US);
|
||||
static {
|
||||
HTTP_RFC1123_FORMAT.setTimeZone(TimeZone.getTimeZone("GMT"));
|
||||
}
|
||||
|
||||
/**
|
||||
* RFC 850 date format, (almost) as described in RFC 2616 (HTTP/1.1), sec 3.3
|
||||
* USE FOR PARSING ONLY (format is not 100% correct, to be more robust).
|
||||
*/
|
||||
private static final SimpleDateFormat HTTP_RFC850_FORMAT = new SimpleDateFormat("EEE, dd-MMM-yy HH:mm:ss z", Locale.US);
|
||||
/**
|
||||
* ANSI C asctime() date format, (almost) as described in RFC 2616 (HTTP/1.1), sec 3.3.
|
||||
* USE FOR PARSING ONLY (format is not 100% correct, to be more robust).
|
||||
*/
|
||||
private static final SimpleDateFormat HTTP_ASCTIME_FORMAT = new SimpleDateFormat("EEE MMM d HH:mm:ss yy", Locale.US);
|
||||
|
||||
private static long sNext50YearWindowChange = DateUtil.currentTimeDay();
|
||||
static {
|
||||
HTTP_RFC850_FORMAT.setTimeZone(TimeZone.getTimeZone("GMT"));
|
||||
HTTP_ASCTIME_FORMAT.setTimeZone(TimeZone.getTimeZone("GMT"));
|
||||
|
||||
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html#sec19.3:
|
||||
// - HTTP/1.1 clients and caches SHOULD assume that an RFC-850 date
|
||||
// which appears to be more than 50 years in the future is in fact
|
||||
// in the past (this helps solve the "year 2000" problem).
|
||||
update50YearWindowIfNeeded();
|
||||
}
|
||||
|
||||
private static void update50YearWindowIfNeeded() {
|
||||
// Avoid class synchronization
|
||||
long next = sNext50YearWindowChange;
|
||||
|
||||
if (next < System.currentTimeMillis()) {
|
||||
// Next check in one day
|
||||
next += DateUtil.DAY;
|
||||
sNext50YearWindowChange = next;
|
||||
|
||||
Date startDate = new Date(next - (50l * DateUtil.CALENDAR_YEAR));
|
||||
//System.out.println("next test: " + new Date(next) + ", 50 year start: " + startDate);
|
||||
synchronized (HTTP_RFC850_FORMAT) {
|
||||
HTTP_RFC850_FORMAT.set2DigitYearStart(startDate);
|
||||
}
|
||||
synchronized (HTTP_ASCTIME_FORMAT) {
|
||||
HTTP_ASCTIME_FORMAT.set2DigitYearStart(startDate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private HTTPUtil() {}
|
||||
|
||||
/**
|
||||
* Formats the time to a HTTP date, using the RFC 1123 format, as described
|
||||
* in <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3"
|
||||
* >RFC 2616 (HTTP/1.1), sec. 3.3</a>.
|
||||
*
|
||||
* @param pTime the time
|
||||
* @return a {@code String} representation of the time
|
||||
*/
|
||||
public static String formatHTTPDate(long pTime) {
|
||||
return formatHTTPDate(new Date(pTime));
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the time to a HTTP date, using the RFC 1123 format, as described
|
||||
* in <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3"
|
||||
* >RFC 2616 (HTTP/1.1), sec. 3.3</a>.
|
||||
*
|
||||
* @param pTime the time
|
||||
* @return a {@code String} representation of the time
|
||||
*/
|
||||
public static String formatHTTPDate(Date pTime) {
|
||||
synchronized (HTTP_RFC1123_FORMAT) {
|
||||
return HTTP_RFC1123_FORMAT.format(pTime);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a HTTP date string into a {@code long} representing milliseconds
|
||||
* since January 1, 1970 GMT.
|
||||
* <p>
|
||||
* Use this method with headers that contain dates, such as
|
||||
* {@code If-Modified-Since} or {@code Last-Modified}.
|
||||
* <p>
|
||||
* The date string may be in either RFC 1123, RFC 850 or ANSI C asctime()
|
||||
* format, as described in
|
||||
* <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3"
|
||||
* >RFC 2616 (HTTP/1.1), sec. 3.3</a>
|
||||
*
|
||||
* @param pDate the date to parse
|
||||
*
|
||||
* @return a {@code long} value representing the date, expressed as the
|
||||
* number of milliseconds since January 1, 1970 GMT,
|
||||
* @throws NumberFormatException if the date parameter is not parseable.
|
||||
* @throws IllegalArgumentException if the date paramter is {@code null}
|
||||
*/
|
||||
public static long parseHTTPDate(String pDate) throws NumberFormatException {
|
||||
return parseHTTPDateImpl(pDate).getTime();
|
||||
}
|
||||
|
||||
/**
|
||||
* ParseHTTPDate implementation
|
||||
*
|
||||
* @param pDate the date string to parse
|
||||
*
|
||||
* @return a {@code Date}
|
||||
* @throws NumberFormatException if the date parameter is not parseable.
|
||||
* @throws IllegalArgumentException if the date paramter is {@code null}
|
||||
*/
|
||||
private static Date parseHTTPDateImpl(final String pDate) throws NumberFormatException {
|
||||
if (pDate == null) {
|
||||
throw new IllegalArgumentException("date == null");
|
||||
}
|
||||
|
||||
if (StringUtil.isEmpty(pDate)) {
|
||||
throw new NumberFormatException("Invalid HTTP date: \"" + pDate + "\"");
|
||||
}
|
||||
|
||||
DateFormat format;
|
||||
|
||||
if (pDate.indexOf('-') >= 0) {
|
||||
format = HTTP_RFC850_FORMAT;
|
||||
update50YearWindowIfNeeded();
|
||||
}
|
||||
else if (pDate.indexOf(',') < 0) {
|
||||
format = HTTP_ASCTIME_FORMAT;
|
||||
update50YearWindowIfNeeded();
|
||||
}
|
||||
else {
|
||||
format = HTTP_RFC1123_FORMAT;
|
||||
// NOTE: RFC1123 always uses 4-digit years
|
||||
}
|
||||
|
||||
Date date;
|
||||
try {
|
||||
//noinspection SynchronizationOnLocalVariableOrMethodParameter
|
||||
synchronized (format) {
|
||||
date = format.parse(pDate);
|
||||
}
|
||||
}
|
||||
catch (ParseException e) {
|
||||
NumberFormatException nfe = new NumberFormatException("Invalid HTTP date: \"" + pDate + "\"");
|
||||
nfe.initCause(e);
|
||||
throw nfe;
|
||||
}
|
||||
|
||||
if (date == null) {
|
||||
throw new NumberFormatException("Invalid HTTP date: \"" + pDate + "\"");
|
||||
}
|
||||
|
||||
return date;
|
||||
}
|
||||
}
|
||||
@@ -69,12 +69,15 @@ public class XMLSerializer {
|
||||
context = new SerializationContext();
|
||||
}
|
||||
|
||||
public final void setIndentation(String pIndent) {
|
||||
public final XMLSerializer indentation(String pIndent) {
|
||||
// TODO: Verify that indent value is only whitespace?
|
||||
context.indent = pIndent != null ? pIndent : "\t";
|
||||
return this;
|
||||
}
|
||||
|
||||
public final void setStripComments(boolean pStrip) {
|
||||
public final XMLSerializer stripComments(boolean pStrip) {
|
||||
context.stripComments = pStrip;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,7 +2,9 @@ package com.twelvemonkeys.io.enc;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.*;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
@@ -23,19 +25,6 @@ public class Base64EncoderTestCase extends EncoderAbstractTestCase {
|
||||
return new Base64Decoder();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNegativeEncode() throws IOException {
|
||||
Encoder encoder = createEncoder();
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
|
||||
try {
|
||||
encoder.encode(bytes, new byte[1], 2, 1);
|
||||
fail("wrong index should throw IndexOutOfBoundsException");
|
||||
}
|
||||
catch (IndexOutOfBoundsException expected) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEmptyEncode() throws IOException {
|
||||
String data = "";
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.twelvemonkeys.lang.ObjectAbstractTestCase;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
@@ -39,7 +40,7 @@ public abstract class DecoderAbstractTestCase extends ObjectAbstractTestCase {
|
||||
ByteArrayInputStream bytes = new ByteArrayInputStream(new byte[0]);
|
||||
|
||||
try {
|
||||
int count = decoder.decode(bytes, new byte[128]);
|
||||
int count = decoder.decode(bytes, ByteBuffer.allocate(128));
|
||||
assertEquals("Should not be able to read any bytes", 0, count);
|
||||
}
|
||||
catch (EOFException allowed) {
|
||||
|
||||
@@ -35,7 +35,7 @@ public abstract class EncoderAbstractTestCase extends ObjectAbstractTestCase {
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
|
||||
try {
|
||||
encoder.encode(bytes, null, 0, 1);
|
||||
encoder.encode(bytes, null);
|
||||
fail("null should throw NullPointerException");
|
||||
}
|
||||
catch (NullPointerException expected) {
|
||||
@@ -54,7 +54,12 @@ public abstract class EncoderAbstractTestCase extends ObjectAbstractTestCase {
|
||||
OutputStream out = new EncoderStream(outBytes, createEncoder(), true);
|
||||
|
||||
try {
|
||||
out.write(data);
|
||||
// Provoke failure for encoders that doesn't take array offset properly into account
|
||||
int off = (data.length + 1) / 2;
|
||||
out.write(data, 0, off);
|
||||
if (data.length > off) {
|
||||
out.write(data, off, data.length - off);
|
||||
}
|
||||
}
|
||||
finally {
|
||||
out.close();
|
||||
@@ -127,4 +132,8 @@ public abstract class EncoderAbstractTestCase extends ObjectAbstractTestCase {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Test that the transition from byte[] to ByteBuffer didn't introduce bugs when writing to a wrapped array with offset.
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
package com.twelvemonkeys.io.enc;
|
||||
|
||||
import com.twelvemonkeys.io.enc.Decoder;
|
||||
import com.twelvemonkeys.io.enc.Encoder;
|
||||
import com.twelvemonkeys.io.enc.PackBitsDecoder;
|
||||
import com.twelvemonkeys.io.enc.PackBitsEncoder;
|
||||
|
||||
/**
|
||||
* PackBitsEncoderTest
|
||||
* <p/>
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* 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.net;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
/**
|
||||
* HTTPUtilTest
|
||||
*
|
||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||
* @author last modified by $Author: haraldk$
|
||||
* @version $Id: HTTPUtilTest.java,v 1.0 08.09.13 13:57 haraldk Exp$
|
||||
*/
|
||||
public class HTTPUtilTest {
|
||||
@Test
|
||||
public void testParseHTTPDateRFC1123() {
|
||||
long time = HTTPUtil.parseHTTPDate("Sun, 06 Nov 1994 08:49:37 GMT");
|
||||
assertEquals(784111777000l, time);
|
||||
|
||||
time = HTTPUtil.parseHTTPDate("Sunday, 06 Nov 1994 08:49:37 GMT");
|
||||
assertEquals(784111777000l, time);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseHTTPDateRFC850() {
|
||||
long time = HTTPUtil.parseHTTPDate("Sunday, 06-Nov-1994 08:49:37 GMT");
|
||||
assertEquals(784111777000l, time);
|
||||
|
||||
time = HTTPUtil.parseHTTPDate("Sun, 06-Nov-94 08:49:37 GMT");
|
||||
assertEquals(784111777000l, time);
|
||||
|
||||
// NOTE: This test will fail some time, around 2044,
|
||||
// as the 50 year window will slide...
|
||||
time = HTTPUtil.parseHTTPDate("Sunday, 06-Nov-94 08:49:37 GMT");
|
||||
assertEquals(784111777000l, time);
|
||||
|
||||
time = HTTPUtil.parseHTTPDate("Sun, 06-Nov-94 08:49:37 GMT");
|
||||
assertEquals(784111777000l, time);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseHTTPDateAsctime() {
|
||||
long time = HTTPUtil.parseHTTPDate("Sun Nov 6 08:49:37 1994");
|
||||
assertEquals(784111777000l, time);
|
||||
|
||||
time = HTTPUtil.parseHTTPDate("Sun Nov 6 08:49:37 94");
|
||||
assertEquals(784111777000l, time);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFormatHTTPDateRFC1123() {
|
||||
long time = 784111777000l;
|
||||
assertEquals("Sun, 06 Nov 1994 08:49:37 GMT", HTTPUtil.formatHTTPDate(time));
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
package com.twelvemonkeys.net;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
/**
|
||||
* NetUtilTestCase
|
||||
* <p/>
|
||||
* <!-- To change this template use Options | File Templates. -->
|
||||
* <!-- Created by IntelliJ IDEA. -->
|
||||
*
|
||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||
* @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/test/java/com/twelvemonkeys/net/NetUtilTestCase.java#1 $
|
||||
*/
|
||||
public class NetUtilTestCase extends TestCase {
|
||||
public void setUp() throws Exception {
|
||||
super.setUp();
|
||||
}
|
||||
|
||||
public void tearDown() throws Exception {
|
||||
super.tearDown();
|
||||
}
|
||||
|
||||
public void testParseHTTPDateRFC1123() {
|
||||
long time = NetUtil.parseHTTPDate("Sun, 06 Nov 1994 08:49:37 GMT");
|
||||
assertEquals(784111777000l, time);
|
||||
|
||||
time = NetUtil.parseHTTPDate("Sunday, 06 Nov 1994 08:49:37 GMT");
|
||||
assertEquals(784111777000l, time);
|
||||
}
|
||||
|
||||
public void testParseHTTPDateRFC850() {
|
||||
long time = NetUtil.parseHTTPDate("Sunday, 06-Nov-1994 08:49:37 GMT");
|
||||
assertEquals(784111777000l, time);
|
||||
|
||||
time = NetUtil.parseHTTPDate("Sun, 06-Nov-94 08:49:37 GMT");
|
||||
assertEquals(784111777000l, time);
|
||||
|
||||
// NOTE: This test will fail some time, around 2044,
|
||||
// as the 50 year window will slide...
|
||||
time = NetUtil.parseHTTPDate("Sunday, 06-Nov-94 08:49:37 GMT");
|
||||
assertEquals(784111777000l, time);
|
||||
|
||||
time = NetUtil.parseHTTPDate("Sun, 06-Nov-94 08:49:37 GMT");
|
||||
assertEquals(784111777000l, time);
|
||||
}
|
||||
|
||||
public void testParseHTTPDateAsctime() {
|
||||
long time = NetUtil.parseHTTPDate("Sun Nov 6 08:49:37 1994");
|
||||
assertEquals(784111777000l, time);
|
||||
|
||||
time = NetUtil.parseHTTPDate("Sun Nov 6 08:49:37 94");
|
||||
assertEquals(784111777000l, time);
|
||||
}
|
||||
|
||||
public void testFormatHTTPDateRFC1123() {
|
||||
long time = 784111777000l;
|
||||
assertEquals("Sun, 06 Nov 1994 08:49:37 GMT", NetUtil.formatHTTPDate(time));
|
||||
}
|
||||
}
|
||||
@@ -526,10 +526,11 @@ public class SVGImageReader extends ImageReaderBase {
|
||||
processImageProgress(99f);
|
||||
|
||||
return dest;
|
||||
//writeImage(dest, output);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new TranscoderException(ex.getMessage(), ex);
|
||||
TranscoderException exception = new TranscoderException(ex.getMessage());
|
||||
exception.initCause(ex);
|
||||
throw exception;
|
||||
}
|
||||
finally {
|
||||
if (mContext != null) {
|
||||
|
||||
@@ -65,7 +65,7 @@ public class SVGImageReaderSpi extends ImageReaderSpi {
|
||||
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
|
||||
ImageReaderSpi.STANDARD_INPUT_TYPE, // Output types
|
||||
new Class[] {ImageInputStream.class}, // Input types
|
||||
null, // Writer SPI names
|
||||
true, // Supports standard stream metadata format
|
||||
null, // Native stream metadata format name
|
||||
|
||||
@@ -65,8 +65,8 @@ public class WMFImageReaderSpi extends ImageReaderSpi {
|
||||
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
|
||||
WMFImageReader.class.getName(), // Reader class name..?
|
||||
ImageReaderSpi.STANDARD_INPUT_TYPE, // Output 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
|
||||
|
||||
@@ -32,6 +32,7 @@ import com.twelvemonkeys.imageio.util.ImageReaderAbstractTestCase;
|
||||
|
||||
import javax.imageio.spi.ImageReaderSpi;
|
||||
import java.awt.*;
|
||||
import java.awt.image.ImagingOpException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
@@ -75,4 +76,27 @@ public class SVGImageReaderTestCase extends ImageReaderAbstractTestCase<SVGImage
|
||||
protected List<String> getMIMETypes() {
|
||||
return Arrays.asList("image/svg+xml");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void testReadWithSizeParam() {
|
||||
try {
|
||||
super.testReadWithSizeParam();
|
||||
}
|
||||
catch (AssertionError failure) {
|
||||
Throwable cause = failure;
|
||||
|
||||
while (cause.getCause() != null) {
|
||||
cause = cause.getCause();
|
||||
}
|
||||
|
||||
if (cause instanceof ImagingOpException && cause.getMessage().equals("Unable to transform src image")) {
|
||||
// This is a very strange regression introduced by the later JDK/JRE (at least it's in 7u45)
|
||||
// Haven't found a workaround yet
|
||||
System.err.println("WARNING: Oracle JRE 7u45 broke my SVGImageReader (known issue): " + cause.getMessage());
|
||||
}
|
||||
else {
|
||||
throw failure;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -235,7 +235,7 @@ public abstract class ImageReaderBase extends ImageReader {
|
||||
|
||||
// If param is non-null, use it
|
||||
if (param != null) {
|
||||
// Try to get the explicit destinaton image
|
||||
// Try to get the explicit destination image
|
||||
BufferedImage dest = param.getDestination();
|
||||
|
||||
if (dest != null) {
|
||||
|
||||
@@ -86,6 +86,9 @@ public final class ColorSpaces {
|
||||
/** A best-effort "generic" CMYK color space. Either read from disk or built-in. */
|
||||
public static final int CS_GENERIC_CMYK = 5001;
|
||||
|
||||
/** Value used instead of 'XYZ ' in problematic Corbis RGB Profiles */
|
||||
private static final byte[] CORBIS_RGB_ALTERNATE_XYZ = new byte[] {0x17, (byte) 0xA5, 0x05, (byte) 0xB8};
|
||||
|
||||
// Weak references to hold the color spaces while cached
|
||||
private static WeakReference<ICC_Profile> adobeRGB1998 = new WeakReference<ICC_Profile>(null);
|
||||
private static WeakReference<ICC_Profile> genericCMYK = new WeakReference<ICC_Profile>(null);
|
||||
@@ -135,9 +138,42 @@ public final class ColorSpaces {
|
||||
}
|
||||
}
|
||||
|
||||
// Special handling to detect problematic Corbis RGB ICC Profile.
|
||||
// This makes sure tags that are expected to be of type 'XYZ ' really have this expected type.
|
||||
// Should leave other ICC profiles unchanged.
|
||||
if (fixProfileXYZTag(profile, ICC_Profile.icSigMediaWhitePointTag)) {
|
||||
fixProfileXYZTag(profile, ICC_Profile.icSigRedColorantTag);
|
||||
fixProfileXYZTag(profile, ICC_Profile.icSigGreenColorantTag);
|
||||
fixProfileXYZTag(profile, ICC_Profile.icSigBlueColorantTag);
|
||||
}
|
||||
|
||||
return getCachedOrCreateCS(profile, profileHeader);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fixes problematic 'XYZ ' tags in Corbis RGB profile.
|
||||
*
|
||||
* @return {@code true} if found and fixed, otherwise {@code false} for short-circuiting
|
||||
* to avoid unnecessary array copying.
|
||||
*/
|
||||
private static boolean fixProfileXYZTag(ICC_Profile profile, final int tagSignature) {
|
||||
byte[] data = profile.getData(tagSignature);
|
||||
|
||||
// The CMM expects 0x64 65 73 63 ('XYZ ') but is 0x17 A5 05 B8..?
|
||||
if (data != null && Arrays.equals(Arrays.copyOfRange(data, 0, 4), CORBIS_RGB_ALTERNATE_XYZ)) {
|
||||
data[0] = 'X';
|
||||
data[1] = 'Y';
|
||||
data[2] = 'Z';
|
||||
data[3] = ' ';
|
||||
|
||||
profile.setData(tagSignature, data);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static ICC_ColorSpace getInternalCS(final int profileCSType, final byte[] profileHeader) {
|
||||
if (profileCSType == ColorSpace.TYPE_RGB && Arrays.equals(profileHeader, sRGB.header)) {
|
||||
return (ICC_ColorSpace) ColorSpace.getInstance(ColorSpace.CS_sRGB);
|
||||
@@ -209,6 +245,7 @@ public final class ColorSpaces {
|
||||
// being 1 (01000000) - "Media Relative Colormetric" in the offending profiles,
|
||||
// and 0 (00000000) - "Perceptual" in the good profiles
|
||||
// (that is 1 single bit of difference right there.. ;-)
|
||||
// See http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=7064516
|
||||
|
||||
// This is particularly annoying, as the byte copying isn't really necessary,
|
||||
// except the getRenderingIntent method is package protected in java.awt.color
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package com.twelvemonkeys.imageio.stream;
|
||||
|
||||
import com.twelvemonkeys.lang.Validate;
|
||||
|
||||
import javax.imageio.stream.ImageInputStream;
|
||||
import javax.imageio.stream.ImageInputStreamImpl;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import static com.twelvemonkeys.lang.Validate.notNull;
|
||||
|
||||
/**
|
||||
* A buffered {@code ImageInputStream}.
|
||||
@@ -20,61 +21,67 @@ import java.io.IOException;
|
||||
// TODO: Create a provider for this (wrapping the FileIIS and FileCacheIIS classes), and disable the Sun built-in spis?
|
||||
// TODO: Test on other platforms, might be just an OS X issue
|
||||
public final class BufferedImageInputStream extends ImageInputStreamImpl implements ImageInputStream {
|
||||
|
||||
static final int DEFAULT_BUFFER_SIZE = 8192;
|
||||
|
||||
private ImageInputStream stream;
|
||||
|
||||
private byte[] buffer;
|
||||
private long bufferStart = 0;
|
||||
private int bufferPos = 0;
|
||||
private int bufferLength = 0;
|
||||
private ByteBuffer buffer;
|
||||
|
||||
public BufferedImageInputStream(final ImageInputStream pStream) throws IOException {
|
||||
this(pStream, DEFAULT_BUFFER_SIZE);
|
||||
}
|
||||
|
||||
private BufferedImageInputStream(final ImageInputStream pStream, final int pBufferSize) throws IOException {
|
||||
Validate.notNull(pStream, "stream");
|
||||
|
||||
stream = pStream;
|
||||
stream = notNull(pStream, "stream");
|
||||
streamPos = pStream.getStreamPosition();
|
||||
buffer = new byte[pBufferSize];
|
||||
buffer = ByteBuffer.allocate(pBufferSize);
|
||||
buffer.limit(0);
|
||||
}
|
||||
|
||||
private void fillBuffer() throws IOException {
|
||||
bufferStart = streamPos;
|
||||
bufferLength = stream.read(buffer, 0, buffer.length);
|
||||
bufferPos = 0;
|
||||
buffer.clear();
|
||||
|
||||
int length = stream.read(buffer.array(), 0, buffer.capacity());
|
||||
|
||||
if (length >= 0) {
|
||||
try {
|
||||
buffer.position(length);
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
System.err.println("length: " + length);
|
||||
throw e;
|
||||
}
|
||||
buffer.flip();
|
||||
}
|
||||
else {
|
||||
buffer.limit(0);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isBufferValid() throws IOException {
|
||||
return bufferPos < bufferLength && bufferStart == stream.getStreamPosition() - bufferLength;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
if (!isBufferValid()) {
|
||||
if (!buffer.hasRemaining()) {
|
||||
fillBuffer();
|
||||
}
|
||||
|
||||
if (bufferLength <= 0) {
|
||||
if (!buffer.hasRemaining()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
bitOffset = 0;
|
||||
streamPos++;
|
||||
|
||||
return buffer[bufferPos++] & 0xff;
|
||||
return buffer.get() & 0xff;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(final byte[] pBuffer, final int pOffset, final int pLength) throws IOException {
|
||||
bitOffset = 0;
|
||||
|
||||
if (!isBufferValid()) {
|
||||
if (!buffer.hasRemaining()) {
|
||||
// Bypass cache if cache is empty for reads longer than buffer
|
||||
if (pLength >= buffer.length) {
|
||||
if (pLength >= buffer.capacity()) {
|
||||
return readDirect(pBuffer, pOffset, pLength);
|
||||
}
|
||||
else {
|
||||
@@ -87,30 +94,29 @@ public final class BufferedImageInputStream extends ImageInputStreamImpl impleme
|
||||
|
||||
private int readDirect(final byte[] pBuffer, final int pOffset, final int pLength) throws IOException {
|
||||
// TODO: Figure out why reading more than the buffer length causes alignment issues...
|
||||
int read = stream.read(pBuffer, pOffset, Math.min(buffer.length, pLength));
|
||||
int read = stream.read(pBuffer, pOffset, Math.min(buffer.capacity(), pLength));
|
||||
|
||||
if (read > 0) {
|
||||
streamPos += read;
|
||||
}
|
||||
|
||||
bufferStart = stream.getStreamPosition();
|
||||
bufferLength = 0;
|
||||
|
||||
return read;
|
||||
}
|
||||
|
||||
|
||||
private int readBuffered(final byte[] pBuffer, final int pOffset, final int pLength) {
|
||||
if (bufferLength <= 0) {
|
||||
if (!buffer.hasRemaining()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Read as much as possible from buffer
|
||||
int length = Math.min(bufferLength - bufferPos, pLength);
|
||||
int length = Math.min(buffer.remaining(), pLength);
|
||||
|
||||
if (length > 0) {
|
||||
System.arraycopy(buffer, bufferPos, pBuffer, pOffset, length);
|
||||
bufferPos += length;
|
||||
int position = buffer.position();
|
||||
System.arraycopy(buffer.array(), position, pBuffer, pOffset, length);
|
||||
buffer.position(position + length);
|
||||
|
||||
}
|
||||
|
||||
streamPos += length;
|
||||
@@ -122,7 +128,7 @@ public final class BufferedImageInputStream extends ImageInputStreamImpl impleme
|
||||
public void seek(long pPosition) throws IOException {
|
||||
// TODO: Could probably be optimized to not invalidate buffer if new position is within current buffer
|
||||
stream.seek(pPosition);
|
||||
bufferLength = 0; // Will invalidate buffer
|
||||
buffer.limit(0); // Will invalidate buffer
|
||||
streamPos = stream.getStreamPosition();
|
||||
}
|
||||
|
||||
@@ -158,6 +164,7 @@ public final class BufferedImageInputStream extends ImageInputStreamImpl impleme
|
||||
stream = null;
|
||||
buffer = null;
|
||||
}
|
||||
|
||||
super.close();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package com.twelvemonkeys.imageio.stream;
|
||||
|
||||
import com.twelvemonkeys.lang.Validate;
|
||||
|
||||
import javax.imageio.stream.ImageInputStreamImpl;
|
||||
import java.io.IOException;
|
||||
|
||||
import static com.twelvemonkeys.lang.Validate.isTrue;
|
||||
import static com.twelvemonkeys.lang.Validate.notNull;
|
||||
|
||||
/**
|
||||
* Experimental
|
||||
*
|
||||
@@ -22,13 +23,13 @@ public final class ByteArrayImageInputStream extends ImageInputStreamImpl {
|
||||
}
|
||||
|
||||
public ByteArrayImageInputStream(final byte[] pData, int offset, int length) {
|
||||
Validate.notNull(pData, "data");
|
||||
Validate.isTrue(offset >= 0 && offset <= pData.length, offset, "offset out of range: %d");
|
||||
Validate.isTrue(length >= 0 && length <= pData.length - offset, length, "length out of range: %d");
|
||||
data = notNull(pData, "data");
|
||||
dataOffset = isBetween(0, pData.length, offset, "offset");
|
||||
dataLength = isBetween(0, pData.length - offset, length, "length");
|
||||
}
|
||||
|
||||
data = pData;
|
||||
dataOffset = offset;
|
||||
dataLength = length;
|
||||
private static int isBetween(final int low, final int high, final int value, final String name) {
|
||||
return isTrue(value >= low && value <= high, value, String.format("%s out of range [%d, %d]: %d", name, low, high, value));
|
||||
}
|
||||
|
||||
public int read() throws IOException {
|
||||
|
||||
@@ -33,6 +33,7 @@ import org.junit.Test;
|
||||
import java.awt.color.ColorSpace;
|
||||
import java.awt.color.ICC_ColorSpace;
|
||||
import java.awt.color.ICC_Profile;
|
||||
import java.io.IOException;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
@@ -184,4 +185,15 @@ public class ColorSpacesTest {
|
||||
public void testIsCS_sRGBNull() {
|
||||
ColorSpaces.isCS_sRGB(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCorbisRGBSpecialHandling() throws IOException {
|
||||
ICC_Profile corbisRGB = ICC_Profile.getInstance(getClass().getResourceAsStream("/profiles/Corbis RGB.icc"));
|
||||
ICC_Profile corbisRGBFixed = ICC_Profile.getInstance(getClass().getResourceAsStream("/profiles/Corbis RGB_fixed.icc"));
|
||||
|
||||
ICC_ColorSpace colorSpace = ColorSpaces.createColorSpace(corbisRGB);
|
||||
|
||||
assertNotNull(colorSpace);
|
||||
assertArrayEquals(colorSpace.getProfile().getData(), corbisRGBFixed.getData());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
|
||||
package com.twelvemonkeys.imageio.util;
|
||||
|
||||
import com.twelvemonkeys.image.ImageUtil;
|
||||
import com.twelvemonkeys.imageio.stream.URLImageInputStreamSpi;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
@@ -45,9 +46,7 @@ import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.RenderedImage;
|
||||
import java.awt.image.SampleModel;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
@@ -75,29 +74,6 @@ public abstract class ImageReaderAbstractTestCase<T extends ImageReader> {
|
||||
|
||||
protected abstract List<TestData> getTestData();
|
||||
|
||||
/**
|
||||
* Convenience method to get a list of test files from the classpath.
|
||||
* Currently only works for resources on the filesystem (not in jars or
|
||||
* archives).
|
||||
*
|
||||
* @param pResourceInFolder a resource in the correct classpath folder.
|
||||
* @return a list of files
|
||||
*/
|
||||
protected final List<File> getInputsFromClasspath(final String pResourceInFolder) {
|
||||
URL resource = getClass().getClassLoader().getResource(pResourceInFolder);
|
||||
assertNotNull(resource);
|
||||
File dir;
|
||||
try {
|
||||
dir = new File(resource.toURI()).getParentFile();
|
||||
}
|
||||
catch (URISyntaxException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
List<File> files = Arrays.asList(dir.listFiles());
|
||||
assertFalse(files.isEmpty());
|
||||
return files;
|
||||
}
|
||||
|
||||
protected abstract ImageReaderSpi createProvider();
|
||||
|
||||
protected abstract Class<T> getReaderClass();
|
||||
@@ -476,7 +452,7 @@ public abstract class ImageReaderAbstractTestCase<T extends ImageReader> {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadWithSubsampleParam() {
|
||||
public void testReadWithSubsampleParamDimensions() {
|
||||
ImageReader reader = createReader();
|
||||
TestData data = getTestData().get(0);
|
||||
reader.setInput(data.getInputStream());
|
||||
@@ -493,8 +469,61 @@ public abstract class ImageReaderAbstractTestCase<T extends ImageReader> {
|
||||
}
|
||||
|
||||
assertNotNull("Image was null!", image);
|
||||
assertEquals("Read image has wrong width: ", (double) data.getDimension(0).width / 5.0, image.getWidth(), 1.0);
|
||||
assertEquals("Read image has wrong height: ", (double) data.getDimension(0).height / 5.0, image.getHeight(), 1.0);
|
||||
assertEquals("Read image has wrong width: ", (data.getDimension(0).width + 4) / 5, image.getWidth());
|
||||
assertEquals("Read image has wrong height: ", (data.getDimension(0).height + 4) / 5, image.getHeight());
|
||||
}
|
||||
|
||||
@Ignore
|
||||
@Test
|
||||
public void testReadWithSubsampleParamPixels() throws IOException {
|
||||
ImageReader reader = createReader();
|
||||
TestData data = getTestData().get(0);
|
||||
reader.setInput(data.getInputStream());
|
||||
|
||||
ImageReadParam param = reader.getDefaultReadParam();
|
||||
param.setSourceRegion(new Rectangle(Math.min(100, reader.getWidth(0)), Math.min(100, reader.getHeight(0))));
|
||||
|
||||
BufferedImage image = null;
|
||||
BufferedImage subsampled = null;
|
||||
try {
|
||||
image = reader.read(0, param);
|
||||
param.setSourceSubsampling(2, 2, 1, 1); // Hmm.. Seems to be the offset the fake version (ReplicateScaleFilter) uses
|
||||
|
||||
subsampled = reader.read(0, param);
|
||||
}
|
||||
catch (IOException e) {
|
||||
failBecause("Image could not be read", e);
|
||||
}
|
||||
|
||||
BufferedImage expected = ImageUtil.toBuffered(IIOUtil.fakeSubsampling(image, param));
|
||||
|
||||
// JPanel panel = new JPanel();
|
||||
// panel.add(new JLabel("Expected", new BufferedImageIcon(expected, 300, 300), JLabel.CENTER));
|
||||
// panel.add(new JLabel("Actual", new BufferedImageIcon(subsampled, 300, 300), JLabel.CENTER));
|
||||
// JOptionPane.showConfirmDialog(null, panel);
|
||||
|
||||
assertImageDataEquals("Subsampled image data does not match expected", expected, subsampled);
|
||||
}
|
||||
|
||||
protected final void assertImageDataEquals(String message, BufferedImage expected, BufferedImage actual) {
|
||||
assertNotNull("Expected image was null", expected);
|
||||
assertNotNull("Actual image was null!", actual);
|
||||
|
||||
if (expected == actual) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (int y = 0; y < expected.getHeight(); y++) {
|
||||
for (int x = 0; x < expected.getWidth(); x++) {
|
||||
int expectedRGB = expected.getRGB(x, y);
|
||||
int actualRGB = actual.getRGB(x, y);
|
||||
|
||||
assertEquals(String.format("%s alpha at (%d, %d)", message, x, y), (expectedRGB >> 24) & 0xff, (actualRGB >> 24) & 0xff, 5);
|
||||
assertEquals(String.format("%s red at (%d, %d)", message, x, y), (expectedRGB >> 16) & 0xff, (actualRGB >> 16) & 0xff, 5);
|
||||
assertEquals(String.format("%s green at (%d, %d)", message, x, y), (expectedRGB >> 8) & 0xff, (actualRGB >> 8) & 0xff, 5);
|
||||
assertEquals(String.format("%s blue at (%d, %d)", message, x, y), expectedRGB & 0xff, actualRGB & 0xff, 5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -513,6 +542,7 @@ public abstract class ImageReaderAbstractTestCase<T extends ImageReader> {
|
||||
catch (IOException e) {
|
||||
failBecause("Image could not be read", e);
|
||||
}
|
||||
|
||||
assertNotNull("Image was null!", image);
|
||||
assertEquals("Read image has wrong width: " + image.getWidth(), 10, image.getWidth());
|
||||
assertEquals("Read image has wrong height: " + image.getHeight(), 10, image.getHeight());
|
||||
@@ -540,6 +570,7 @@ public abstract class ImageReaderAbstractTestCase<T extends ImageReader> {
|
||||
catch (IOException e) {
|
||||
failBecause("Image could not be read", e);
|
||||
}
|
||||
|
||||
assertNotNull("Image was null!", image);
|
||||
assertEquals("Read image has wrong width: " + image.getWidth(), 10, image.getWidth());
|
||||
assertEquals("Read image has wrong height: " + image.getHeight(), 10, image.getHeight());
|
||||
@@ -1291,13 +1322,14 @@ public abstract class ImageReaderAbstractTestCase<T extends ImageReader> {
|
||||
// TODO: This is thrown by ImageReader.getDestination. But are we happy with that?
|
||||
// The problem is that the checkReadParamBandSettings throws IllegalArgumentException, which seems more appropriate...
|
||||
String message = expected.getMessage().toLowerCase();
|
||||
assertTrue(
|
||||
"Wrong message: " + message + " for type " + destination.getType(),
|
||||
message.contains("destination") ||
|
||||
((destination.getType() == BufferedImage.TYPE_BYTE_BINARY ||
|
||||
destination.getType() == BufferedImage.TYPE_BYTE_INDEXED)
|
||||
&& message.contains("indexcolormodel"))
|
||||
);
|
||||
if (!(message.contains("destination") || message.contains("band size") || // For JDK classes
|
||||
((destination.getType() == BufferedImage.TYPE_BYTE_BINARY ||
|
||||
destination.getType() == BufferedImage.TYPE_BYTE_INDEXED) &&
|
||||
message.contains("indexcolormodel")))) {
|
||||
failBecause(
|
||||
"Wrong message: " + message + " for type " + destination.getType(), expected
|
||||
);
|
||||
}
|
||||
}
|
||||
catch (IllegalArgumentException expected) {
|
||||
String message = expected.getMessage().toLowerCase();
|
||||
@@ -1352,7 +1384,7 @@ public abstract class ImageReaderAbstractTestCase<T extends ImageReader> {
|
||||
boolean removed = illegalTypes.remove(valid);
|
||||
|
||||
// TODO: 4BYTE_ABGR (6) and 4BYTE_ABGR_PRE (7) is essentially the same type...
|
||||
// !#$#<23>%$! ImageTypeSpecifier.equals is not well-defined
|
||||
// #$@*%$! ImageTypeSpecifier.equals is not well-defined
|
||||
if (!removed) {
|
||||
for (Iterator<ImageTypeSpecifier> iterator = illegalTypes.iterator(); iterator.hasNext();) {
|
||||
ImageTypeSpecifier illegalType = iterator.next();
|
||||
@@ -1422,6 +1454,7 @@ public abstract class ImageReaderAbstractTestCase<T extends ImageReader> {
|
||||
failBecause("Could not read " + data.getInput() + " with explicit destination type " + type, e);
|
||||
}
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(type.getColorModel(), result.getColorModel());
|
||||
|
||||
// The following logically tests
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
|
||||
package com.twelvemonkeys.imageio.util;
|
||||
|
||||
import com.twelvemonkeys.imageio.stream.URLImageInputStreamSpi;
|
||||
import org.junit.Test;
|
||||
import org.mockito.InOrder;
|
||||
|
||||
@@ -35,12 +36,14 @@ import javax.imageio.ImageIO;
|
||||
import javax.imageio.ImageWriteParam;
|
||||
import javax.imageio.ImageWriter;
|
||||
import javax.imageio.event.IIOWriteProgressListener;
|
||||
import javax.imageio.spi.IIORegistry;
|
||||
import javax.imageio.stream.ImageOutputStream;
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.RenderedImage;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
@@ -56,6 +59,12 @@ import static org.mockito.Mockito.*;
|
||||
*/
|
||||
public abstract class ImageWriterAbstractTestCase {
|
||||
|
||||
// TODO: Move static block + getClassLoaderResource to common superclass for reader/writer test cases or delegate.
|
||||
|
||||
static {
|
||||
IIORegistry.getDefaultInstance().registerServiceProvider(new URLImageInputStreamSpi());
|
||||
}
|
||||
|
||||
protected abstract ImageWriter createImageWriter();
|
||||
|
||||
protected abstract List<? extends RenderedImage> getTestData();
|
||||
@@ -85,6 +94,10 @@ public abstract class ImageWriterAbstractTestCase {
|
||||
return getTestData().get(index);
|
||||
}
|
||||
|
||||
protected URL getClassLoaderResource(final String pName) {
|
||||
return getClass().getResource(pName);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetOutput() throws IOException {
|
||||
// Should just pass with no exceptions
|
||||
|
||||
BIN
imageio/imageio-core/src/test/resources/profiles/Corbis RGB.icc
Normal file
BIN
imageio/imageio-core/src/test/resources/profiles/Corbis RGB.icc
Normal file
Binary file not shown.
BIN
imageio/imageio-core/src/test/resources/profiles/Corbis RGB_fixed.icc
Executable file
BIN
imageio/imageio-core/src/test/resources/profiles/Corbis RGB_fixed.icc
Executable file
Binary file not shown.
@@ -59,7 +59,7 @@ public final class ICNSImageReaderSpi extends ImageReaderSpi{
|
||||
"image/x-apple-icons", // Common extension MIME
|
||||
},
|
||||
"com.twelvemonkeys.imageio.plugins.icns.ICNSImageReader",
|
||||
STANDARD_INPUT_TYPE,
|
||||
new Class[] {ImageInputStream.class},
|
||||
null,
|
||||
true, null, null, null, null,
|
||||
true,
|
||||
|
||||
@@ -29,10 +29,13 @@
|
||||
package com.twelvemonkeys.imageio.plugins.icns;
|
||||
|
||||
import com.twelvemonkeys.imageio.util.ImageReaderAbstractTestCase;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
|
||||
import javax.imageio.ImageReader;
|
||||
import javax.imageio.spi.ImageReaderSpi;
|
||||
import java.awt.*;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
@@ -61,7 +64,7 @@ public class ICNSImageReaderTest extends ImageReaderAbstractTestCase {
|
||||
new Dimension(32, 32), // 24 bit + 8 bit mask
|
||||
new Dimension(48, 48), // 24 bit + 8 bit mask
|
||||
new Dimension(128, 128), // 24 bit + 8 bit mask
|
||||
new Dimension(256, 256), // JPEG 2000 ic08
|
||||
new Dimension(256, 256), // JPEG 2000 ic08
|
||||
new Dimension(512, 512) // JPEG 2000 ic09
|
||||
),
|
||||
new TestData(
|
||||
@@ -69,7 +72,7 @@ public class ICNSImageReaderTest extends ImageReaderAbstractTestCase {
|
||||
new Dimension(16, 16), // 24 bit + 8 bit mask
|
||||
new Dimension(32, 32), // 24 bit + 8 bit mask
|
||||
new Dimension(128, 128), // 24 bit + 8 bit mask
|
||||
new Dimension(256, 256), // JPEG 2000 ic08
|
||||
new Dimension(256, 256), // JPEG 2000 ic08
|
||||
new Dimension(512, 512) // JPEG 2000 ic09
|
||||
),
|
||||
new TestData(
|
||||
@@ -128,4 +131,11 @@ public class ICNSImageReaderTest extends ImageReaderAbstractTestCase {
|
||||
protected List<String> getMIMETypes() {
|
||||
return Arrays.asList("image/x-apple-icons");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore("Known issue: Subsampled reading not supported")
|
||||
@Override
|
||||
public void testReadWithSubsampleParamPixels() throws IOException {
|
||||
super.testReadWithSubsampleParamPixels();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ public final class CURImageReaderSpi extends ImageReaderSpi {
|
||||
"image/cursor" // Unofficial, but common
|
||||
},
|
||||
"com.twelvemonkeys.imageio.plugins.ico.CURImageReader",
|
||||
STANDARD_INPUT_TYPE,
|
||||
new Class[] {ImageInputStream.class},
|
||||
null,
|
||||
true, null, null, null, null,
|
||||
true,
|
||||
|
||||
@@ -61,7 +61,7 @@ public final class ICOImageReaderSpi extends ImageReaderSpi {
|
||||
"image/ico" // Unofficial, but common
|
||||
},
|
||||
"com.twelvemonkeys.imageio.plugins.ico.ICOImageReader",
|
||||
STANDARD_INPUT_TYPE,
|
||||
new Class[] {ImageInputStream.class},
|
||||
null,
|
||||
true, null, null, null, null,
|
||||
true,
|
||||
|
||||
@@ -28,8 +28,6 @@
|
||||
|
||||
package com.twelvemonkeys.imageio.plugins.iff;
|
||||
|
||||
import com.twelvemonkeys.image.InverseColorMapIndexColorModel;
|
||||
|
||||
import javax.imageio.IIOException;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.IndexColorModel;
|
||||
@@ -159,7 +157,7 @@ final class CMAPChunk extends IFFChunk {
|
||||
// with alpha, where all colors above the original color is all transparent?
|
||||
// This is a waste of time and space, of course...
|
||||
int transparent = header.maskType == BMHDChunk.MASK_TRANSPARENT_COLOR ? header.transparentIndex : -1;
|
||||
model = new InverseColorMapIndexColorModel(header.bitplanes, reds.length, reds, greens, blues, transparent);
|
||||
model = new IndexColorModel(header.bitplanes, reds.length, reds, greens, blues, transparent); // https://github.com/haraldk/TwelveMonkeys/issues/15
|
||||
}
|
||||
|
||||
return model;
|
||||
|
||||
@@ -50,15 +50,15 @@ import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Reader for Amiga (Electronic Arts) IFF ILBM (InterLeaved BitMap) and PBM
|
||||
* Reader for Commodore Amiga (Electronic Arts) IFF ILBM (InterLeaved BitMap) and PBM
|
||||
* format (Packed BitMap).
|
||||
* The IFF format (Interchange File Format) is the standard file format
|
||||
* supported by allmost all image software for the Amiga computer.
|
||||
* <p/>
|
||||
* This reader supports the original palette-based 1-8 bit formats, including
|
||||
* EHB (Extra Halfbright), HAM (Hold and Modify), and the more recent "deep"
|
||||
* EHB (Extra Half-Bright), HAM (Hold and Modify), and the more recent "deep"
|
||||
* formats, 8 bit gray, 24 bit RGB and 32 bit ARGB.
|
||||
* Uncompressed and ByteRun1 compressed (run lenght encoding) files are
|
||||
* Uncompressed and ByteRun1 compressed (run length encoding) files are
|
||||
* supported.
|
||||
* <p/>
|
||||
* Palette based images are read as {@code BufferedImage} of
|
||||
@@ -613,12 +613,12 @@ public class IFFImageReader extends ImageReaderBase {
|
||||
}
|
||||
|
||||
// Skip rows outside AOI
|
||||
if (srcY < aoi.y || (srcY - aoi.y) % sourceYSubsampling != 0) {
|
||||
continue;
|
||||
}
|
||||
else if (srcY >= (aoi.y + aoi.height)) {
|
||||
if (srcY >= (aoi.y + aoi.height)) {
|
||||
return;
|
||||
}
|
||||
else if (srcY < aoi.y || (srcY - aoi.y) % sourceYSubsampling != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (formType == IFF.TYPE_ILBM) {
|
||||
// NOTE: Using (channels - c - 1) instead of just c,
|
||||
@@ -639,19 +639,21 @@ public class IFFImageReader extends ImageReaderBase {
|
||||
}
|
||||
}
|
||||
|
||||
int dstY = (srcY - aoi.y) / sourceYSubsampling;
|
||||
// TODO: Support conversion to INT (A)RGB rasters (maybe using ColorConvertOp?)
|
||||
// TODO: Avoid createChild if no region?
|
||||
if (sourceXSubsampling == 1) {
|
||||
destination.setRect(0, dstY, sourceRow);
|
||||
if (srcY >= aoi.y && (srcY - aoi.y) % sourceYSubsampling == 0) {
|
||||
int dstY = (srcY - aoi.y) / sourceYSubsampling;
|
||||
// TODO: Support conversion to INT (A)RGB rasters (maybe using ColorConvertOp?)
|
||||
// TODO: Avoid createChild if no region?
|
||||
if (sourceXSubsampling == 1) {
|
||||
destination.setRect(0, dstY, sourceRow);
|
||||
// dataElements = raster.getDataElements(aoi.x, 0, aoi.width, 1, dataElements);
|
||||
// destination.setDataElements(offset.x, offset.y + (srcY - aoi.y) / sourceYSubsampling, aoi.width, 1, dataElements);
|
||||
}
|
||||
else {
|
||||
for (int srcX = 0; srcX < sourceRow.getWidth(); srcX += sourceXSubsampling) {
|
||||
dataElements = sourceRow.getDataElements(srcX, 0, dataElements);
|
||||
int dstX = srcX / sourceXSubsampling;
|
||||
destination.setDataElements(dstX, dstY, dataElements);
|
||||
}
|
||||
else {
|
||||
for (int srcX = 0; srcX < sourceRow.getWidth(); srcX += sourceXSubsampling) {
|
||||
dataElements = sourceRow.getDataElements(srcX, 0, dataElements);
|
||||
int dstX = srcX / sourceXSubsampling;
|
||||
destination.setDataElements(dstX, dstY, dataElements);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ public class IFFImageReaderSpi extends ImageReaderSpi {
|
||||
new String[]{"iff", "lbm", "ham", "ham8", "ilbm"},
|
||||
new String[]{"image/iff", "image/x-iff"},
|
||||
"com.twelvemonkeys.imageio.plugins.iff.IFFImageReader",
|
||||
STANDARD_INPUT_TYPE,
|
||||
new Class[] {ImageInputStream.class},
|
||||
new String[]{"com.twelvemonkeys.imageio.plugins.iff.IFFImageWriterSpi"},
|
||||
true, null, null, null, null,
|
||||
true, null, null, null, null
|
||||
@@ -108,7 +108,7 @@ public class IFFImageReaderSpi extends ImageReaderSpi {
|
||||
}
|
||||
|
||||
public String getDescription(Locale pLocale) {
|
||||
return "Amiga (Electronic Arts) Image Interchange Format (IFF) image reader";
|
||||
return "Commodore Amiga/Electronic Arts Image Interchange Format (IFF) image reader";
|
||||
}
|
||||
|
||||
public static ImageReaderSpi sharedProvider() {
|
||||
|
||||
@@ -45,7 +45,7 @@ import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
/**
|
||||
* Writer for Amiga (Electronic Arts) IFF ILBM (InterLeaved BitMap) format.
|
||||
* Writer for Commodore Amiga (Electronic Arts) IFF ILBM (InterLeaved BitMap) format.
|
||||
* The IFF format (Interchange File Format) is the standard file format
|
||||
* supported by almost all image software for the Amiga computer.
|
||||
* <p/>
|
||||
|
||||
@@ -79,6 +79,6 @@ public class IFFImageWriterSpi extends ImageWriterSpi {
|
||||
}
|
||||
|
||||
public String getDescription(Locale pLocale) {
|
||||
return "Amiga (Electronic Arts) IFF image writer";
|
||||
return "Commodore Amiga/Electronic Arts Image Interchange Format (IFF) image writer";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ import java.util.Locale;
|
||||
*/
|
||||
abstract class JMagickImageReaderSpiSupport extends ImageReaderSpi {
|
||||
|
||||
final static boolean AVAILABLE = SystemUtil.isClassAvailable("com.twelvemonkeys.imageio.plugins.jmagick.JMagick");
|
||||
final static boolean AVAILABLE = SystemUtil.isClassAvailable("com.twelvemonkeys.imageio.plugins.jmagick.JMagick", JMagickImageReaderSpiSupport.class);
|
||||
|
||||
/**
|
||||
* Creates a JMagickImageReaderSpiSupport
|
||||
@@ -69,7 +69,7 @@ abstract class JMagickImageReaderSpiSupport extends ImageReaderSpi {
|
||||
AVAILABLE ? pSuffixes : null, // Suffixes
|
||||
AVAILABLE ? pMimeTypes : null, // Mime-types
|
||||
pReaderClassName, // Reader class name
|
||||
ImageReaderSpi.STANDARD_INPUT_TYPE, // Output types
|
||||
new Class[] {ImageInputStream.class}, // Input types
|
||||
pWriterSpiNames, // Writer SPI names
|
||||
true, // Supports standard stream metadata format
|
||||
null, // Native stream metadata format name
|
||||
|
||||
@@ -30,9 +30,9 @@ package com.twelvemonkeys.imageio.plugins.jmagick;
|
||||
|
||||
import javax.imageio.ImageTypeSpecifier;
|
||||
import javax.imageio.ImageWriter;
|
||||
import javax.imageio.spi.ImageReaderSpi;
|
||||
import javax.imageio.spi.ImageWriterSpi;
|
||||
import javax.imageio.spi.ServiceRegistry;
|
||||
import javax.imageio.stream.ImageOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Locale;
|
||||
|
||||
@@ -68,7 +68,7 @@ abstract class JMagickImageWriterSpiSupport extends ImageWriterSpi {
|
||||
AVAILABLE ? pSuffixes : null, // Suffixes
|
||||
AVAILABLE ? pMimeTypes : null, // Mime-types
|
||||
pWriterClassName, // Writer class name
|
||||
ImageReaderSpi.STANDARD_INPUT_TYPE, // Output types
|
||||
new Class[] {ImageOutputStream.class}, // Output types
|
||||
pReaderSpiNames, // Reader SPI names
|
||||
true, // Supports standard stream metadata format
|
||||
null, // Native stream metadata format name
|
||||
|
||||
@@ -93,11 +93,11 @@ abstract class JMagickReader extends ImageReaderBase {
|
||||
private static final ColorModel CM_GRAY_ALPHA =
|
||||
new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_GRAY), true, true, Transparency.TRANSLUCENT, DataBuffer.TYPE_BYTE);
|
||||
|
||||
private final boolean mUseTempFile;
|
||||
private File mTempFile;
|
||||
private final boolean useTempFile;
|
||||
private File tempFile;
|
||||
|
||||
private MagickImage mImage;
|
||||
private Dimension mSize;
|
||||
private MagickImage image;
|
||||
private Dimension size;
|
||||
|
||||
protected JMagickReader(final JMagickImageReaderSpiSupport pProvider) {
|
||||
this(pProvider, pProvider.useTempFile());
|
||||
@@ -105,28 +105,29 @@ abstract class JMagickReader extends ImageReaderBase {
|
||||
|
||||
protected JMagickReader(final ImageReaderSpi pProvider, final boolean pUseTemp) {
|
||||
super(pProvider);
|
||||
mUseTempFile = pUseTemp;
|
||||
useTempFile = pUseTemp;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void resetMembers() {
|
||||
if (mTempFile != null) {
|
||||
mTempFile.delete();
|
||||
if (tempFile != null&& !tempFile.delete()) {
|
||||
tempFile.deleteOnExit();
|
||||
}
|
||||
mTempFile = null;
|
||||
|
||||
if (mImage != null) {
|
||||
mImage.destroyImages();
|
||||
tempFile = null;
|
||||
|
||||
if (image != null) {
|
||||
image.destroyImages();
|
||||
}
|
||||
mImage = null;
|
||||
|
||||
mSize = null;
|
||||
image = null;
|
||||
size = null;
|
||||
}
|
||||
|
||||
// TODO: Handle multi-image formats
|
||||
// if (mImage.hasFrames()) {
|
||||
// int count = mImage.getNumFrames();
|
||||
// MagickImage[] images = mImage.breakFrames();
|
||||
// if (image.hasFrames()) {
|
||||
// int count = image.getNumFrames();
|
||||
// MagickImage[] images = image.breakFrames();
|
||||
// }
|
||||
|
||||
public Iterator<ImageTypeSpecifier> getImageTypes(int pIndex) throws IOException {
|
||||
@@ -140,7 +141,7 @@ abstract class JMagickReader extends ImageReaderBase {
|
||||
try {
|
||||
ColorModel cm;
|
||||
// NOTE: These are all fall-through by intention
|
||||
switch (mImage.getImageType()) {
|
||||
switch (image.getImageType()) {
|
||||
case ImageType.BilevelType:
|
||||
specs.add(IndexedImageTypeSpecifier.createFromIndexColorModel(MonochromeColorModel.getInstance()));
|
||||
case ImageType.GrayscaleType:
|
||||
@@ -158,11 +159,11 @@ abstract class JMagickReader extends ImageReaderBase {
|
||||
));
|
||||
case ImageType.PaletteType:
|
||||
specs.add(IndexedImageTypeSpecifier.createFromIndexColorModel(
|
||||
MagickUtil.createIndexColorModel(mImage.getColormap(), false)
|
||||
MagickUtil.createIndexColorModel(image.getColormap(), false)
|
||||
));
|
||||
case ImageType.PaletteMatteType:
|
||||
specs.add(IndexedImageTypeSpecifier.createFromIndexColorModel(
|
||||
MagickUtil.createIndexColorModel(mImage.getColormap(), true)
|
||||
MagickUtil.createIndexColorModel(image.getColormap(), true)
|
||||
));
|
||||
case ImageType.TrueColorType:
|
||||
// cm = MagickUtil.CM_COLOR_OPAQUE;
|
||||
@@ -183,7 +184,7 @@ abstract class JMagickReader extends ImageReaderBase {
|
||||
case ImageType.ColorSeparationMatteType:
|
||||
case ImageType.OptimizeType:
|
||||
default:
|
||||
throw new MagickException("Unknown JMagick image type: " + mImage.getImageType());
|
||||
throw new MagickException("Unknown JMagick image type: " + image.getImageType());
|
||||
}
|
||||
}
|
||||
catch (MagickException e) {
|
||||
@@ -196,19 +197,19 @@ abstract class JMagickReader extends ImageReaderBase {
|
||||
public int getWidth(int pIndex) throws IOException {
|
||||
checkBounds(pIndex);
|
||||
|
||||
if (mSize == null) {
|
||||
if (size == null) {
|
||||
init(0);
|
||||
}
|
||||
return mSize != null ? mSize.width : -1;
|
||||
return size != null ? size.width : -1;
|
||||
}
|
||||
|
||||
public int getHeight(int pIndex) throws IOException {
|
||||
checkBounds(pIndex);
|
||||
|
||||
if (mSize == null) {
|
||||
if (size == null) {
|
||||
init(0);
|
||||
}
|
||||
return mSize != null ? mSize.height : -1;
|
||||
return size != null ? size.height : -1;
|
||||
}
|
||||
|
||||
public BufferedImage read(int pIndex, ImageReadParam pParam) throws IOException {
|
||||
@@ -218,14 +219,14 @@ abstract class JMagickReader extends ImageReaderBase {
|
||||
processImageStarted(pIndex);
|
||||
|
||||
// Some more waste of time and space...
|
||||
Dimension size = mSize;
|
||||
Dimension size = this.size;
|
||||
|
||||
if (pParam != null) {
|
||||
// Source region
|
||||
// TODO: Maybe have to do some tests, to check if we are within bounds...
|
||||
Rectangle sourceRegion = pParam.getSourceRegion();
|
||||
if (sourceRegion != null) {
|
||||
mImage = mImage.cropImage(sourceRegion);
|
||||
image = image.cropImage(sourceRegion);
|
||||
size = sourceRegion.getSize();
|
||||
}
|
||||
|
||||
@@ -234,7 +235,7 @@ abstract class JMagickReader extends ImageReaderBase {
|
||||
int w = size.width / pParam.getSourceXSubsampling();
|
||||
int h = size.height / pParam.getSourceYSubsampling();
|
||||
|
||||
mImage = mImage.sampleImage(w, h);
|
||||
image = image.sampleImage(w, h);
|
||||
size = new Dimension(w, h);
|
||||
}
|
||||
}
|
||||
@@ -245,7 +246,7 @@ abstract class JMagickReader extends ImageReaderBase {
|
||||
}
|
||||
|
||||
processImageProgress(10f);
|
||||
BufferedImage buffered = MagickUtil.toBuffered(mImage);
|
||||
BufferedImage buffered = MagickUtil.toBuffered(image);
|
||||
processImageProgress(100f);
|
||||
|
||||
/**/
|
||||
@@ -260,12 +261,12 @@ abstract class JMagickReader extends ImageReaderBase {
|
||||
//*/
|
||||
|
||||
/**
|
||||
System.out.println("Colorspace: " + mImage.getColorspace());
|
||||
System.out.println("Depth: " + mImage.getDepth());
|
||||
System.out.println("Format: " + mImage.getImageFormat());
|
||||
System.out.println("Type: " + mImage.getImageType());
|
||||
System.out.println("IPTCProfile: " + StringUtil.deepToString(mImage.getIptcProfile()));
|
||||
System.out.println("StorageClass: " + mImage.getStorageClass());
|
||||
System.out.println("Colorspace: " + image.getColorspace());
|
||||
System.out.println("Depth: " + image.getDepth());
|
||||
System.out.println("Format: " + image.getImageFormat());
|
||||
System.out.println("Type: " + image.getImageType());
|
||||
System.out.println("IPTCProfile: " + StringUtil.deepToString(image.getIptcProfile()));
|
||||
System.out.println("StorageClass: " + image.getStorageClass());
|
||||
//*/
|
||||
|
||||
processImageComplete();
|
||||
@@ -282,11 +283,11 @@ abstract class JMagickReader extends ImageReaderBase {
|
||||
checkBounds(pIndex);
|
||||
|
||||
try {
|
||||
if (mImage == null) {
|
||||
if (image == null) {
|
||||
// TODO: If ImageInputStream is already file-backed, maybe we can peek into that file?
|
||||
// At the moment, the cache/file is not accessible, but we could create our own
|
||||
// FileImageInputStream provider that gives us this access.
|
||||
if (!mUseTempFile && imageInput.length() >= 0 && imageInput.length() <= Integer.MAX_VALUE) {
|
||||
if (!useTempFile && imageInput.length() >= 0 && imageInput.length() <= Integer.MAX_VALUE) {
|
||||
// This works for most file formats, as long as ImageMagick
|
||||
// uses the file magic to decide file format
|
||||
byte[] bytes = new byte[(int) imageInput.length()];
|
||||
@@ -294,17 +295,18 @@ abstract class JMagickReader extends ImageReaderBase {
|
||||
|
||||
// Unfortunately, this is a waste of space & time...
|
||||
ImageInfo info = new ImageInfo();
|
||||
mImage = new MagickImage(info);
|
||||
mImage.blobToImage(info, bytes);
|
||||
image = new MagickImage(info);
|
||||
image.blobToImage(info, bytes);
|
||||
}
|
||||
else {
|
||||
// Quirks mode: Use temp file to get correct file extension
|
||||
// (which is even more waste of space & time, but might save memory)
|
||||
String ext = getFormatName().toLowerCase();
|
||||
|
||||
mTempFile = File.createTempFile("jmagickreader", "." + ext);
|
||||
mTempFile.deleteOnExit();
|
||||
OutputStream out = new BufferedOutputStream(new FileOutputStream(mTempFile));
|
||||
tempFile = File.createTempFile("jmagickreader", "." + ext);
|
||||
tempFile.deleteOnExit();
|
||||
OutputStream out = new BufferedOutputStream(new FileOutputStream(tempFile));
|
||||
|
||||
try {
|
||||
byte[] buffer = new byte[FileUtil.BUF_SIZE];
|
||||
int count;
|
||||
@@ -320,11 +322,11 @@ abstract class JMagickReader extends ImageReaderBase {
|
||||
out.close();
|
||||
}
|
||||
|
||||
ImageInfo info = new ImageInfo(mTempFile.getAbsolutePath());
|
||||
mImage = new MagickImage(info);
|
||||
ImageInfo info = new ImageInfo(tempFile.getAbsolutePath());
|
||||
image = new MagickImage(info);
|
||||
}
|
||||
|
||||
mSize = mImage.getDimension();
|
||||
size = image.getDimension();
|
||||
}
|
||||
}
|
||||
catch (MagickException e) {
|
||||
|
||||
@@ -52,12 +52,13 @@ public class JPEGImageReaderSpi extends JMagickImageReaderSpiSupport {
|
||||
boolean canDecode(ImageInputStream pSource) throws IOException {
|
||||
// new byte[][] {new byte[] {(byte) 0xff, (byte) 0xd8, (byte) 0xff, (byte) 0xe0},
|
||||
// new byte[] {(byte) 0xff, (byte) 0xd8, (byte) 0xff, (byte) 0xe1}}, // JPEG
|
||||
// new byte[][] {new byte[] {(byte) 0xff, (byte) 0xd8, (byte) 0xff, (byte) 0xed}}, // PHOTOSHOP 3 JPEG
|
||||
// new byte[][] {new byte[] {(byte) 0xff, (byte) 0xd8, (byte) 0xff, (byte) 0xee}}, // JPG
|
||||
byte[] magic = new byte[4];
|
||||
pSource.readFully(magic);
|
||||
|
||||
return magic[0] == (byte) 0xFF && magic[1] == (byte) 0xD8 && magic[2] == (byte) 0xFF &&
|
||||
(magic[3] == (byte) 0xE0 || magic[0] == (byte) 0xE1 || magic[0] == (byte) 0xEE);
|
||||
(magic[3] == (byte) 0xE0 || magic[3] == (byte) 0xE1 || magic[3] == (byte) 0xED || magic[3] == (byte) 0xEE);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ public class TargaImageReaderSpi extends JMagickImageReaderSpiSupport {
|
||||
);
|
||||
}
|
||||
|
||||
boolean canDecode(ImageInputStream pSource) throws IOException {
|
||||
boolean canDecode(final ImageInputStream pSource) throws IOException {
|
||||
// // TODO: Targa 1989 signature look like (bytes 8-23 of 26 LAST bytes):
|
||||
// // 'T', 'R', 'U', 'E', 'V', 'I', 'S', 'I', 'O', 'N', '-', 'X', 'F', 'I', 'L', 'E'
|
||||
// // Targa 1987:
|
||||
@@ -63,6 +63,12 @@ public class TargaImageReaderSpi extends JMagickImageReaderSpiSupport {
|
||||
// new byte[] {-1, 0x01, 0x20}, // Type 31: Compressed CM
|
||||
// new byte[] {-1, 0x01, 0x21}, // Type 32: Compressed CM, 4 pass
|
||||
// },
|
||||
|
||||
// If we don't know the stream length, just give up, as the Targa format has trailing magic bytes...
|
||||
if (pSource.length() < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
pSource.seek(pSource.length() - 18);
|
||||
byte[] magic = new byte[18];
|
||||
pSource.readFully(magic);
|
||||
|
||||
@@ -54,7 +54,7 @@ public class WMFImageReaderSpi extends JMagickImageReaderSpiSupport {
|
||||
// (byte) 0x9a, (byte) 0x00, (byte) 0x00,}},
|
||||
byte[] magic = new byte[6];
|
||||
pSource.readFully(magic);
|
||||
return magic[0] == (byte) 0xD7 && magic[2] == (byte) 0xCD &&
|
||||
return magic[0] == (byte) 0xD7 && magic[1] == (byte) 0xCD &&
|
||||
magic[2] == (byte) 0xC6 && magic[3] == (byte) 0x9A &&
|
||||
magic[4] == (byte) 0x00 && magic[5] == (byte) 0x00;
|
||||
}
|
||||
|
||||
@@ -32,9 +32,12 @@ import com.twelvemonkeys.imageio.metadata.Directory;
|
||||
import com.twelvemonkeys.imageio.metadata.Entry;
|
||||
import com.twelvemonkeys.imageio.metadata.exif.TIFF;
|
||||
import com.twelvemonkeys.imageio.util.IIOUtil;
|
||||
import com.twelvemonkeys.lang.Validate;
|
||||
|
||||
import javax.imageio.IIOException;
|
||||
import javax.imageio.ImageReader;
|
||||
import javax.imageio.stream.ImageInputStream;
|
||||
import javax.imageio.stream.MemoryCacheImageInputStream;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
@@ -51,14 +54,16 @@ import java.util.Arrays;
|
||||
* @version $Id: EXIFThumbnail.java,v 1.0 18.04.12 12:19 haraldk Exp$
|
||||
*/
|
||||
final class EXIFThumbnailReader extends ThumbnailReader {
|
||||
private final ImageReader reader;
|
||||
private final Directory ifd;
|
||||
private final ImageInputStream stream;
|
||||
private final int compression;
|
||||
|
||||
private transient SoftReference<BufferedImage> cachedThumbnail;
|
||||
|
||||
public EXIFThumbnailReader(ThumbnailReadProgressListener progressListener, int imageIndex, int thumbnailIndex, Directory ifd, ImageInputStream stream) {
|
||||
public EXIFThumbnailReader(ThumbnailReadProgressListener progressListener, ImageReader jpegReader, int imageIndex, int thumbnailIndex, Directory ifd, ImageInputStream stream) {
|
||||
super(progressListener, imageIndex, thumbnailIndex);
|
||||
this.reader = Validate.notNull(jpegReader);
|
||||
this.ifd = ifd;
|
||||
this.stream = stream;
|
||||
|
||||
@@ -125,8 +130,16 @@ final class EXIFThumbnailReader extends ThumbnailReader {
|
||||
};
|
||||
|
||||
input = new SequenceInputStream(new ByteArrayInputStream(fakeEmptyExif), input);
|
||||
|
||||
try {
|
||||
return readJPEGThumbnail(input);
|
||||
MemoryCacheImageInputStream stream = new MemoryCacheImageInputStream(input);
|
||||
|
||||
try {
|
||||
return readJPEGThumbnail(reader, stream);
|
||||
}
|
||||
finally {
|
||||
stream.close();
|
||||
}
|
||||
}
|
||||
finally {
|
||||
input.close();
|
||||
@@ -202,7 +215,7 @@ final class EXIFThumbnailReader extends ThumbnailReader {
|
||||
Entry width = ifd.getEntryById(TIFF.TAG_IMAGE_WIDTH);
|
||||
|
||||
if (width == null) {
|
||||
throw new IIOException("Missing dimensions for unknown EXIF thumbnail");
|
||||
throw new IIOException("Missing dimensions for uncompressed EXIF thumbnail");
|
||||
}
|
||||
|
||||
return ((Number) width.getValue()).intValue();
|
||||
@@ -221,7 +234,7 @@ final class EXIFThumbnailReader extends ThumbnailReader {
|
||||
Entry height = ifd.getEntryById(TIFF.TAG_IMAGE_HEIGHT);
|
||||
|
||||
if (height == null) {
|
||||
throw new IIOException("Missing dimensions for unknown EXIF thumbnail");
|
||||
throw new IIOException("Missing dimensions for uncompressed EXIF thumbnail");
|
||||
}
|
||||
|
||||
return ((Number) height.getValue()).intValue();
|
||||
|
||||
@@ -144,35 +144,16 @@ class FastCMYKToRGB implements /*BufferedImageOp,*/ RasterOp {
|
||||
}
|
||||
|
||||
public WritableRaster createCompatibleDestRaster(final Raster src) {
|
||||
return src.createChild(0, 0, src.getWidth(), src.getHeight(), 0, 0, new int[]{0, 1, 2}).createCompatibleWritableRaster();
|
||||
// WHAT?? This code no longer work for JRE 7u45+... JRE bug?!
|
||||
// Raster child = src.createChild(0, 0, src.getWidth(), src.getHeight(), 0, 0, new int[] {0, 1, 2});
|
||||
// return child.createCompatibleWritableRaster(); // Throws an exception complaining about the scanline stride from the verify() method
|
||||
|
||||
// This is a workaround for the above code that no longer works.
|
||||
// It wil use 25% more memory, but it seems to work...
|
||||
WritableRaster raster = src.createCompatibleWritableRaster();
|
||||
return raster.createWritableChild(0, 0, src.getWidth(), src.getHeight(), 0, 0, new int[] {0, 1, 2});
|
||||
}
|
||||
|
||||
/*
|
||||
public BufferedImage filter(BufferedImage src, BufferedImage dest) {
|
||||
Validate.notNull(src, "src may not be null");
|
||||
// Validate.isTrue(src != dest, "src and dest image may not be same");
|
||||
|
||||
if (dest == null) {
|
||||
dest = createCompatibleDestImage(src, ColorModel.getRGBdefault());
|
||||
}
|
||||
|
||||
filter(src.getRaster(), dest.getRaster());
|
||||
|
||||
return dest;
|
||||
}
|
||||
|
||||
public Rectangle2D getBounds2D(BufferedImage src) {
|
||||
return getBounds2D(src.getRaster());
|
||||
}
|
||||
|
||||
public BufferedImage createCompatibleDestImage(BufferedImage src, ColorModel destCM) {
|
||||
// TODO: dest color model depends on bands...
|
||||
return destCM == null ?
|
||||
new BufferedImage(src.getWidth(), src.getHeight(), BufferedImage.TYPE_3BYTE_BGR) :
|
||||
new BufferedImage(destCM, destCM.createCompatibleWritableRaster(src.getWidth(), src.getHeight()), destCM.isAlphaPremultiplied(), null);
|
||||
}
|
||||
*/
|
||||
|
||||
public Point2D getPoint2D(Point2D srcPt, Point2D dstPt) {
|
||||
if (dstPt == null) {
|
||||
dstPt = new Point2D.Double(srcPt.getX(), srcPt.getY());
|
||||
|
||||
@@ -29,10 +29,14 @@
|
||||
package com.twelvemonkeys.imageio.plugins.jpeg;
|
||||
|
||||
import com.twelvemonkeys.image.InverseColorMapIndexColorModel;
|
||||
import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream;
|
||||
import com.twelvemonkeys.lang.Validate;
|
||||
|
||||
import javax.imageio.IIOException;
|
||||
import javax.imageio.ImageReader;
|
||||
import javax.imageio.metadata.IIOMetadata;
|
||||
import javax.imageio.stream.ImageInputStream;
|
||||
import java.awt.image.*;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.lang.ref.SoftReference;
|
||||
|
||||
@@ -45,12 +49,14 @@ import java.lang.ref.SoftReference;
|
||||
*/
|
||||
final class JFXXThumbnailReader extends ThumbnailReader {
|
||||
|
||||
private final ImageReader reader;
|
||||
private final JFXXSegment segment;
|
||||
|
||||
private transient SoftReference<BufferedImage> cachedThumbnail;
|
||||
|
||||
protected JFXXThumbnailReader(final ThumbnailReadProgressListener progressListener, final int imageIndex, final int thumbnailIndex, final JFXXSegment segment) {
|
||||
protected JFXXThumbnailReader(final ThumbnailReadProgressListener progressListener, ImageReader jpegReader, final int imageIndex, final int thumbnailIndex, final JFXXSegment segment) {
|
||||
super(progressListener, imageIndex, thumbnailIndex);
|
||||
this.reader = Validate.notNull(jpegReader);
|
||||
this.segment = segment;
|
||||
}
|
||||
|
||||
@@ -79,11 +85,30 @@ final class JFXXThumbnailReader extends ThumbnailReader {
|
||||
return thumbnail;
|
||||
}
|
||||
|
||||
public IIOMetadata readMetadata() throws IOException {
|
||||
ImageInputStream input = new ByteArrayImageInputStream(segment.thumbnail);
|
||||
|
||||
try {
|
||||
reader.setInput(input);
|
||||
|
||||
return reader.getImageMetadata(0);
|
||||
}
|
||||
finally {
|
||||
input.close();
|
||||
}
|
||||
}
|
||||
|
||||
private BufferedImage readJPEGCached(boolean pixelsExposed) throws IOException {
|
||||
BufferedImage thumbnail = cachedThumbnail != null ? cachedThumbnail.get() : null;
|
||||
|
||||
if (thumbnail == null) {
|
||||
thumbnail = readJPEGThumbnail(new ByteArrayInputStream(segment.thumbnail));
|
||||
ImageInputStream stream = new ByteArrayImageInputStream(segment.thumbnail);
|
||||
try {
|
||||
thumbnail = readJPEGThumbnail(reader, stream);
|
||||
}
|
||||
finally {
|
||||
stream.close();
|
||||
}
|
||||
}
|
||||
|
||||
cachedThumbnail = pixelsExposed ? null : new SoftReference<BufferedImage>(thumbnail);
|
||||
|
||||
@@ -0,0 +1,271 @@
|
||||
package com.twelvemonkeys.imageio.plugins.jpeg;
|
||||
|
||||
import com.twelvemonkeys.imageio.metadata.jpeg.JPEG;
|
||||
import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegment;
|
||||
import com.twelvemonkeys.xml.XMLSerializer;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
|
||||
import javax.imageio.metadata.IIOInvalidTreeException;
|
||||
import javax.imageio.metadata.IIOMetadata;
|
||||
import javax.imageio.metadata.IIOMetadataNode;
|
||||
import java.awt.color.ICC_Profile;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* JPEGImage10MetadataCleaner
|
||||
*
|
||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||
* @author last modified by $Author: haraldk$
|
||||
* @version $Id: JPEGImage10MetadataCleaner.java,v 1.0 22.10.13 14:41 haraldk Exp$
|
||||
*/
|
||||
final class JPEGImage10MetadataCleaner {
|
||||
|
||||
/**
|
||||
* Native metadata format name
|
||||
*/
|
||||
static final String JAVAX_IMAGEIO_JPEG_IMAGE_1_0 = "javax_imageio_jpeg_image_1.0";
|
||||
|
||||
private final JPEGImageReader reader;
|
||||
|
||||
JPEGImage10MetadataCleaner(final JPEGImageReader reader) {
|
||||
this.reader = reader;
|
||||
}
|
||||
|
||||
IIOMetadata cleanMetadata(final IIOMetadata imageMetadata) throws IOException {
|
||||
// We filter out pretty much everything from the stream..
|
||||
// Meaning we have to read get *all APP segments* and re-insert into metadata.
|
||||
List<JPEGSegment> appSegments = reader.getAppSegments(JPEGImageReader.ALL_APP_MARKERS, null);
|
||||
|
||||
// NOTE: There's a bug in the merging code in JPEGMetadata mergeUnknownNode that makes sure all "unknown" nodes are added twice in certain conditions.... ARGHBL...
|
||||
// DONE: 1: Work around
|
||||
// TODO: 2: REPORT BUG!
|
||||
// TODO: Report dht inconsistency bug (reads any amount of tables but only allows setting 4 tables)
|
||||
|
||||
// TODO: Allow EXIF (as app1EXIF) in the JPEGvariety (sic) node. Need new format, might as well create a completely new format...
|
||||
// As EXIF is (a subset of) TIFF, (and the EXIF data is a valid TIFF stream) probably use something like:
|
||||
// http://download.java.net/media/jai-imageio/javadoc/1.1/com/sun/media/imageio/plugins/tiff/package-summary.html#ImageMetadata
|
||||
/*
|
||||
from: http://docs.oracle.com/javase/6/docs/api/javax/imageio/metadata/doc-files/jpeg_metadata.html
|
||||
|
||||
In future versions of the JPEG metadata format, other varieties of JPEG metadata may be supported (e.g. Exif)
|
||||
by defining other types of nodes which may appear as a child of the JPEGvariety node.
|
||||
|
||||
(Note that an application wishing to interpret Exif metadata given a metadata tree structure in the
|
||||
javax_imageio_jpeg_image_1.0 format must check for an unknown marker segment with a tag indicating an
|
||||
APP1 marker and containing data identifying it as an Exif marker segment. Then it may use application-specific
|
||||
code to interpret the data in the marker segment. If such an application were to encounter a metadata tree
|
||||
formatted according to a future version of the JPEG metadata format, the Exif marker segment might not be
|
||||
unknown in that format - it might be structured as a child node of the JPEGvariety node.
|
||||
|
||||
Thus, it is important for an application to specify which version to use by passing the string identifying
|
||||
the version to the method/constructor used to obtain an IIOMetadata object.)
|
||||
*/
|
||||
|
||||
IIOMetadataNode tree = (IIOMetadataNode) imageMetadata.getAsTree(JAVAX_IMAGEIO_JPEG_IMAGE_1_0);
|
||||
IIOMetadataNode jpegVariety = (IIOMetadataNode) tree.getElementsByTagName("JPEGvariety").item(0);
|
||||
IIOMetadataNode markerSequence = (IIOMetadataNode) tree.getElementsByTagName("markerSequence").item(0);
|
||||
|
||||
JFIFSegment jfifSegment = reader.getJFIF();
|
||||
JFXXSegment jfxxSegment = reader.getJFXX();
|
||||
AdobeDCTSegment adobeDCT = reader.getAdobeDCT();
|
||||
ICC_Profile embeddedICCProfile = reader.getEmbeddedICCProfile(true);
|
||||
SOFSegment sof = reader.getSOF();
|
||||
|
||||
boolean hasRealJFIF = false;
|
||||
boolean hasRealJFXX = false;
|
||||
boolean hasRealICC = false;
|
||||
|
||||
if (jfifSegment != null) {
|
||||
// Normal case, conformant JFIF with 1 or 3 components
|
||||
// TODO: Test if we have CMY or other non-JFIF color space?
|
||||
if (sof.componentsInFrame() == 1 || sof.componentsInFrame() == 3) {
|
||||
IIOMetadataNode jfif = new IIOMetadataNode("app0JFIF");
|
||||
jfif.setAttribute("majorVersion", String.valueOf(jfifSegment.majorVersion));
|
||||
jfif.setAttribute("minorVersion", String.valueOf(jfifSegment.minorVersion));
|
||||
jfif.setAttribute("resUnits", String.valueOf(jfifSegment.units));
|
||||
jfif.setAttribute("Xdensity", String.valueOf(Math.max(1, jfifSegment.xDensity))); // Avoid 0 density
|
||||
jfif.setAttribute("Ydensity", String.valueOf(Math.max(1,jfifSegment.yDensity)));
|
||||
jfif.setAttribute("thumbWidth", String.valueOf(jfifSegment.xThumbnail));
|
||||
jfif.setAttribute("thumbHeight", String.valueOf(jfifSegment.yThumbnail));
|
||||
|
||||
jpegVariety.appendChild(jfif);
|
||||
hasRealJFIF = true;
|
||||
|
||||
// Add app2ICC and JFXX as proper nodes
|
||||
if (embeddedICCProfile != null) {
|
||||
IIOMetadataNode app2ICC = new IIOMetadataNode("app2ICC");
|
||||
app2ICC.setUserObject(embeddedICCProfile);
|
||||
jfif.appendChild(app2ICC);
|
||||
hasRealICC = true;
|
||||
}
|
||||
|
||||
if (jfxxSegment != null) {
|
||||
IIOMetadataNode JFXX = new IIOMetadataNode("JFXX");
|
||||
jfif.appendChild(JFXX);
|
||||
IIOMetadataNode app0JFXX = new IIOMetadataNode("app0JFXX");
|
||||
app0JFXX.setAttribute("extensionCode", String.valueOf(jfxxSegment.extensionCode));
|
||||
|
||||
JFXXThumbnailReader thumbnailReader = new JFXXThumbnailReader(null, reader.getThumbnailReader(), 0, 0, jfxxSegment);
|
||||
IIOMetadataNode jfifThumb;
|
||||
|
||||
switch (jfxxSegment.extensionCode) {
|
||||
case JFXXSegment.JPEG:
|
||||
jfifThumb = new IIOMetadataNode("JFIFthumbJPEG");
|
||||
// Contains it's own "markerSequence" with full DHT, DQT, SOF etc...
|
||||
IIOMetadata thumbMeta = thumbnailReader.readMetadata();
|
||||
Node thumbTree = thumbMeta.getAsTree(JAVAX_IMAGEIO_JPEG_IMAGE_1_0);
|
||||
jfifThumb.appendChild(thumbTree.getLastChild());
|
||||
app0JFXX.appendChild(jfifThumb);
|
||||
break;
|
||||
|
||||
case JFXXSegment.INDEXED:
|
||||
jfifThumb = new IIOMetadataNode("JFIFthumbPalette");
|
||||
jfifThumb.setAttribute("thumbWidth", String.valueOf(thumbnailReader.getWidth()));
|
||||
jfifThumb.setAttribute("thumbHeight", String.valueOf(thumbnailReader.getHeight()));
|
||||
app0JFXX.appendChild(jfifThumb);
|
||||
break;
|
||||
|
||||
case JFXXSegment.RGB:
|
||||
jfifThumb = new IIOMetadataNode("JFIFthumbRGB");
|
||||
jfifThumb.setAttribute("thumbWidth", String.valueOf(thumbnailReader.getWidth()));
|
||||
jfifThumb.setAttribute("thumbHeight", String.valueOf(thumbnailReader.getHeight()));
|
||||
app0JFXX.appendChild(jfifThumb);
|
||||
break;
|
||||
|
||||
default:
|
||||
reader.processWarningOccurred(String.format("Unknown JFXX extension code: %d", jfxxSegment.extensionCode));
|
||||
}
|
||||
|
||||
JFXX.appendChild(app0JFXX);
|
||||
hasRealJFXX = true;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Typically CMYK JPEG with JFIF segment (Adobe or similar).
|
||||
reader.processWarningOccurred(String.format(
|
||||
"Incompatible JFIF marker segment in stream. " +
|
||||
"SOF%d has %d color components, JFIF allows only 1 or 3 components. Ignoring JFIF marker.",
|
||||
sof.marker & 0xf, sof.componentsInFrame()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Special case: Broken AdobeDCT segment, inconsistent with SOF, use values from SOF
|
||||
if (adobeDCT != null && adobeDCT.getTransform() == AdobeDCTSegment.YCCK && sof.componentsInFrame() < 4) {
|
||||
reader.processWarningOccurred(String.format(
|
||||
"Invalid Adobe App14 marker. Indicates YCCK/CMYK data, but SOF%d has %d color components. " +
|
||||
"Ignoring Adobe App14 marker.",
|
||||
sof.marker & 0xf, sof.componentsInFrame()
|
||||
));
|
||||
|
||||
// Remove bad AdobeDCT
|
||||
NodeList app14Adobe = tree.getElementsByTagName("app14Adobe");
|
||||
for (int i = app14Adobe.getLength() - 1; i >= 0; i--) {
|
||||
Node item = app14Adobe.item(i);
|
||||
item.getParentNode().removeChild(item);
|
||||
}
|
||||
|
||||
// We don't add this as unknown marker, as we are certain it's bogus by now
|
||||
}
|
||||
|
||||
Node next = null;
|
||||
for (JPEGSegment segment : appSegments) {
|
||||
// Except real app0JFIF, app0JFXX, app2ICC and app14Adobe, add all the app segments that we filtered away as "unknown" markers
|
||||
if (segment.marker() == JPEG.APP0 && "JFIF".equals(segment.identifier()) && hasRealJFIF) {
|
||||
continue;
|
||||
}
|
||||
else if (segment.marker() == JPEG.APP0 && "JFXX".equals(segment.identifier()) && hasRealJFXX) {
|
||||
continue;
|
||||
}
|
||||
else if (segment.marker() == JPEG.APP1 && "Exif".equals(segment.identifier()) /* always inserted */) {
|
||||
continue;
|
||||
}
|
||||
else if (segment.marker() == JPEG.APP2 && "ICC_PROFILE".equals(segment.identifier()) && hasRealICC) {
|
||||
continue;
|
||||
}
|
||||
else if (segment.marker() == JPEG.APP14 && "Adobe".equals(segment.identifier()) /* always inserted */) {
|
||||
continue;
|
||||
}
|
||||
|
||||
IIOMetadataNode unknown = new IIOMetadataNode("unknown");
|
||||
unknown.setAttribute("MarkerTag", Integer.toString(segment.marker() & 0xff));
|
||||
|
||||
DataInputStream stream = new DataInputStream(segment.data());
|
||||
|
||||
try {
|
||||
String identifier = segment.identifier();
|
||||
int off = identifier != null ? identifier.length() + 1 : 0;
|
||||
|
||||
byte[] data = new byte[off + segment.length()];
|
||||
|
||||
if (identifier != null) {
|
||||
System.arraycopy(identifier.getBytes(Charset.forName("ASCII")), 0, data, 0, identifier.length());
|
||||
}
|
||||
|
||||
stream.readFully(data, off, segment.length());
|
||||
|
||||
unknown.setUserObject(data);
|
||||
}
|
||||
finally {
|
||||
stream.close();
|
||||
}
|
||||
|
||||
if (next == null) {
|
||||
// To be semi-compatible with the functionality in mergeTree,
|
||||
// let's insert after the last unknown tag, or before any other tag if no unknown tag exists
|
||||
NodeList unknowns = markerSequence.getElementsByTagName("unknown");
|
||||
|
||||
if (unknowns.getLength() > 0) {
|
||||
next = unknowns.item(unknowns.getLength() - 1).getNextSibling();
|
||||
}
|
||||
else {
|
||||
next = markerSequence.getFirstChild();
|
||||
}
|
||||
}
|
||||
|
||||
markerSequence.insertBefore(unknown, next);
|
||||
}
|
||||
|
||||
// Inconsistency issue in the com.sun classes, it can read metadata with dht containing
|
||||
// more than 4 children, but will not allow setting such a tree...
|
||||
// We'll split AC/DC tables into separate dht nodes.
|
||||
NodeList dhts = markerSequence.getElementsByTagName("dht");
|
||||
for (int j = 0; j < dhts.getLength(); j++) {
|
||||
Node dht = dhts.item(j);
|
||||
NodeList dhtables = dht.getChildNodes();
|
||||
|
||||
if (dhtables.getLength() > 4) {
|
||||
IIOMetadataNode acTables = new IIOMetadataNode("dht");
|
||||
dht.getParentNode().insertBefore(acTables, dht.getNextSibling());
|
||||
|
||||
// Split into 2 dht nodes, one for AC and one for DC
|
||||
for (int i = 0; i < dhtables.getLength(); i++) {
|
||||
Element dhtable = (Element) dhtables.item(i);
|
||||
String tableClass = dhtable.getAttribute("class");
|
||||
if ("1".equals(tableClass)) {
|
||||
dht.removeChild(dhtable);
|
||||
acTables.appendChild(dhtable);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
imageMetadata.setFromTree(JAVAX_IMAGEIO_JPEG_IMAGE_1_0, tree);
|
||||
}
|
||||
catch (IIOInvalidTreeException e) {
|
||||
if (JPEGImageReader.DEBUG) {
|
||||
new XMLSerializer(System.out, System.getProperty("file.encoding")).serialize(tree, false);
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
|
||||
return imageMetadata;
|
||||
}
|
||||
}
|
||||
@@ -41,13 +41,16 @@ import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegment;
|
||||
import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegmentUtil;
|
||||
import com.twelvemonkeys.imageio.util.ProgressListenerBase;
|
||||
import com.twelvemonkeys.lang.Validate;
|
||||
import com.twelvemonkeys.xml.XMLSerializer;
|
||||
|
||||
import javax.imageio.*;
|
||||
import javax.imageio.event.IIOReadUpdateListener;
|
||||
import javax.imageio.event.IIOReadWarningListener;
|
||||
import javax.imageio.metadata.IIOMetadata;
|
||||
import javax.imageio.metadata.IIOMetadataFormatImpl;
|
||||
import javax.imageio.spi.ImageReaderSpi;
|
||||
import javax.imageio.stream.ImageInputStream;
|
||||
import javax.imageio.stream.MemoryCacheImageInputStream;
|
||||
import java.awt.*;
|
||||
import java.awt.color.ColorSpace;
|
||||
import java.awt.color.ICC_ColorSpace;
|
||||
@@ -63,6 +66,7 @@ import java.util.List;
|
||||
* <p/>
|
||||
* Main features:
|
||||
* <ul>
|
||||
* <li>Support for YCbCr JPEGs without JFIF segment (converted to RGB, using the embedded ICC profile if applicable)</li>
|
||||
* <li>Support for CMYK JPEGs (converted to RGB by default or as CMYK, using the embedded ICC profile if applicable)</li>
|
||||
* <li>Support for Adobe YCCK JPEGs (converted to RGB by default or as CMYK, using the embedded ICC profile if applicable)</li>
|
||||
* <li>Support for JPEGs containing ICC profiles with interpretation other than 'Perceptual' (profile is assumed to be 'Perceptual' and used)</li>
|
||||
@@ -75,10 +79,17 @@ import java.util.List;
|
||||
* </ul>
|
||||
* Thumbnail support:
|
||||
* <ul>
|
||||
* <li>Support for JFIF thumbnails (even if stream contains "inconsistent metadata")</li>
|
||||
* <li>Support for JFIF thumbnails (even if stream contains inconsistent metadata)</li>
|
||||
* <li>Support for JFXX thumbnails (JPEG, Indexed and RGB)</li>
|
||||
* <li>Support for EXIF thumbnails (JPEG, RGB and YCbCr)</li>
|
||||
* </ul>
|
||||
* Metadata support:
|
||||
* <ul>
|
||||
* <li>Support for JPEG metadata in both standard and native formats (even if stream contains inconsistent metadata)</li>
|
||||
* <li>Support for {@code javax_imageio_jpeg_image_1.0} format (currently as native format, may change in the future)</li>
|
||||
* <li>Support for illegal combinations of JFIF, Exif and Adobe markers, using "unknown" segments in the
|
||||
* "MarkerSequence" tag for the unsupported segments (for {@code javax_imageio_jpeg_image_1.0} format)</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||
* @author LUT-based YCbCR conversion by Werner Randelshofer
|
||||
@@ -86,10 +97,13 @@ import java.util.List;
|
||||
* @version $Id: JPEGImageReader.java,v 1.0 24.01.11 16.37 haraldk Exp$
|
||||
*/
|
||||
public class JPEGImageReader extends ImageReaderBase {
|
||||
// TODO: Fix the (stream) metadata inconsistency issues.
|
||||
// - Sun JPEGMetadata class does not (and can not be made to) support CMYK data.. We need to create all new metadata classes.. :-/
|
||||
// TODO: Allow automatic rotation based on EXIF rotation field?
|
||||
// TODO: Create a simplified native metadata format that is closer to the actual JPEG stream AND supports EXIF in a sensible way
|
||||
|
||||
private final static boolean DEBUG = "true".equalsIgnoreCase(System.getProperty("com.twelvemonkeys.imageio.plugins.jpeg.debug"));
|
||||
final static boolean DEBUG = "true".equalsIgnoreCase(System.getProperty("com.twelvemonkeys.imageio.plugins.jpeg.debug"));
|
||||
|
||||
/** Internal constant for referring all APP segments */
|
||||
static final int ALL_APP_MARKERS = -1;
|
||||
|
||||
/** Segment identifiers for the JPEG segments we care about reading. */
|
||||
private static final Map<Integer, List<String>> SEGMENT_IDENTIFIERS = createSegmentIds();
|
||||
@@ -97,17 +111,10 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
private static Map<Integer, List<String>> createSegmentIds() {
|
||||
Map<Integer, List<String>> map = new LinkedHashMap<Integer, List<String>>();
|
||||
|
||||
// JFIF/JFXX APP0 markers
|
||||
map.put(JPEG.APP0, JPEGSegmentUtil.ALL_IDS);
|
||||
|
||||
// Exif metadata
|
||||
map.put(JPEG.APP1, Collections.singletonList("Exif"));
|
||||
|
||||
// ICC Color Profile
|
||||
map.put(JPEG.APP2, Collections.singletonList("ICC_PROFILE"));
|
||||
|
||||
// Adobe APP14 marker
|
||||
map.put(JPEG.APP14, Collections.singletonList("Adobe"));
|
||||
// Need all APP markers to be able to re-generate proper metadata later
|
||||
for (int appMarker = JPEG.APP0; appMarker <= JPEG.APP15; appMarker++) {
|
||||
map.put(appMarker, JPEGSegmentUtil.ALL_IDS);
|
||||
}
|
||||
|
||||
// SOFn markers
|
||||
map.put(JPEG.SOF0, null);
|
||||
@@ -133,11 +140,15 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
/** Listens to progress updates in the delegate, and delegates back to this instance */
|
||||
private final ProgressDelegator progressDelegator;
|
||||
|
||||
/** Cached JPEG app segments */
|
||||
private List<JPEGSegment> segments;
|
||||
|
||||
/** Extra delegate for reading JPEG encoded thumbnails */
|
||||
private ImageReader thumbnailReader;
|
||||
private List<ThumbnailReader> thumbnails;
|
||||
|
||||
private JPEGImage10MetadataCleaner metadataCleaner;
|
||||
|
||||
/** Cached list of JPEG segments we filter from the underlying stream */
|
||||
private List<JPEGSegment> segments;
|
||||
|
||||
JPEGImageReader(final ImageReaderSpi provider, final ImageReader delegate) {
|
||||
super(provider);
|
||||
this.delegate = Validate.notNull(delegate);
|
||||
@@ -157,6 +168,12 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
segments = null;
|
||||
thumbnails = null;
|
||||
|
||||
if (thumbnailReader != null) {
|
||||
thumbnailReader.reset();
|
||||
}
|
||||
|
||||
metadataCleaner = null;
|
||||
|
||||
installListeners();
|
||||
}
|
||||
|
||||
@@ -164,6 +181,11 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
public void dispose() {
|
||||
super.dispose();
|
||||
|
||||
if (thumbnailReader != null) {
|
||||
thumbnailReader.dispose();
|
||||
thumbnailReader = null;
|
||||
}
|
||||
|
||||
delegate.dispose();
|
||||
}
|
||||
|
||||
@@ -280,45 +302,52 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
assertInput();
|
||||
checkBounds(imageIndex);
|
||||
|
||||
// TODO: This test is not good enough for JDK7, which seems to have fixed some of the issues.
|
||||
// NOTE: We rely on the fact that unsupported images has no valid types. This is kind of hacky.
|
||||
// Might want to look into the metadata, to see if there's a better way to identify these.
|
||||
boolean unsupported = !delegate.getImageTypes(imageIndex).hasNext();
|
||||
// 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));
|
||||
// }
|
||||
// }
|
||||
|
||||
ICC_Profile profile = getEmbeddedICCProfile(false);
|
||||
AdobeDCTSegment adobeDCT = getAdobeDCT();
|
||||
SOFSegment sof = getSOF();
|
||||
JPEGColorSpace sourceCSType = getSourceCSType(adobeDCT, sof);
|
||||
|
||||
// TODO: Probably something bogus here, as ICC profile isn't applied if reading through the delegate any more...
|
||||
// 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() && (
|
||||
unsupported ||
|
||||
sourceCSType == JPEGColorSpace.CMYK ||
|
||||
sourceCSType == JPEGColorSpace.YCCK ||
|
||||
adobeDCT != null && adobeDCT.getTransform() == AdobeDCTSegment.YCCK ||
|
||||
profile != null && !ColorSpaces.isCS_sRGB(profile))) {
|
||||
// profile != null && (ColorSpaces.isOffendingColorProfile(profile) || profile.getColorSpaceType() == ColorSpace.TYPE_CMYK))) {
|
||||
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);
|
||||
}
|
||||
|
||||
return readImageAsRasterAndReplaceColorProfile(imageIndex, param, ensureDisplayProfile(profile));
|
||||
// TODO: Possible to optimize slightly, to avoid readAsRaster for non-CMyK and other good types?
|
||||
return readImageAsRasterAndReplaceColorProfile(imageIndex, param, sof, sourceCSType, adobeDCT, ensureDisplayProfile(profile));
|
||||
}
|
||||
|
||||
if (DEBUG) {
|
||||
System.out.println("Reading using delegate");
|
||||
}
|
||||
|
||||
|
||||
return delegate.read(imageIndex, param);
|
||||
}
|
||||
|
||||
private BufferedImage readImageAsRasterAndReplaceColorProfile(int imageIndex, ImageReadParam param, ICC_Profile profile) throws IOException {
|
||||
private BufferedImage readImageAsRasterAndReplaceColorProfile(int imageIndex, ImageReadParam param, SOFSegment startOfFrame, JPEGColorSpace csType, AdobeDCTSegment adobeDCT, ICC_Profile profile) throws IOException {
|
||||
int origWidth = getWidth(imageIndex);
|
||||
int origHeight = getHeight(imageIndex);
|
||||
|
||||
AdobeDCTSegment adobeDCT = getAdobeDCT();
|
||||
SOFSegment startOfFrame = getSOF();
|
||||
JPEGColorSpace csType = getSourceCSType(adobeDCT, startOfFrame);
|
||||
|
||||
Iterator<ImageTypeSpecifier> imageTypes = getImageTypes(imageIndex);
|
||||
BufferedImage image = getDestination(param, imageTypes, origWidth, origHeight);
|
||||
WritableRaster destination = image.getRaster();
|
||||
@@ -442,6 +471,8 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
// Apply further color conversion for explicit color space, or just copy the pixels into place
|
||||
if (convert != null) {
|
||||
convert.filter(src, dest);
|
||||
// WritableRaster filtered = convert.filter(src, null);
|
||||
// new AffineTransformOp(AffineTransform.getRotateInstance(2 * Math.PI, filtered.getWidth() / 2.0, filtered.getHeight() / 2.0), null).filter(filtered, dest);
|
||||
}
|
||||
else {
|
||||
dest.setRect(0, 0, src);
|
||||
@@ -474,16 +505,17 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
|
||||
When reading, the contents of the stream are interpreted by the usual JPEG conventions, as follows:
|
||||
|
||||
• If a JFIF APP0 marker segment is present, the colorspace is known to be either grayscale, YCbCr or CMYK.
|
||||
• If a JFIF APP0 marker segment is present, the colorspace should be either grayscale or YCbCr.
|
||||
If an APP2 marker segment containing an embedded ICC profile is also present, then YCbCr is converted to RGB according
|
||||
to the formulas given in the JFIF spec, and the ICC profile is assumed to refer to the resulting RGB space.
|
||||
CMYK data is read as is, and the ICC profile is assumed to refer to the resulting CMYK space.
|
||||
But, as software does not follow the spec, we can't really assume anything.
|
||||
|
||||
• If an Adobe APP14 marker segment is present, the colorspace is determined by consulting the transform flag.
|
||||
The transform flag takes one of three values:
|
||||
o 2 - The image is encoded as YCCK (implicitly converted from CMYK on encoding).
|
||||
o 1 - The image is encoded as YCbCr (implicitly converted from RGB on encoding).
|
||||
o 0 - Unknown. 3-channel images are assumed to be RGB, 4-channel images are assumed to be CMYK.
|
||||
o 0 - Unknown. 1-channel images are assumed to be Gray, 3-channel images are assumed to be RGB,
|
||||
4-channel images are assumed to be CMYK.
|
||||
|
||||
• If neither marker segment is present, the following procedure is followed: Single-channel images are assumed
|
||||
to be grayscale, and 2-channel images are assumed to be grayscale with an alpha channel. For 3- and 4-channel
|
||||
@@ -511,8 +543,10 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
if (adobeDCT != null) {
|
||||
switch (adobeDCT.getTransform()) {
|
||||
case AdobeDCTSegment.YCC:
|
||||
// TODO: Verify that startOfFrame has 3 components, otherwise issue warning and ignore adobeDCT
|
||||
return JPEGColorSpace.YCbCr;
|
||||
case AdobeDCTSegment.YCCK:
|
||||
// TODO: Verify that startOfFrame has 4 components, otherwise issue warning and ignore adobeDCT
|
||||
return JPEGColorSpace.YCCK;
|
||||
case AdobeDCTSegment.Unknown:
|
||||
if (startOfFrame.components.length == 1) {
|
||||
@@ -545,7 +579,7 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
return JPEGColorSpace.PhotoYCC;
|
||||
}
|
||||
else {
|
||||
// if subsampled, YCbCr else RGB
|
||||
// If subsampled, YCbCr else RGB
|
||||
for (SOFComponent component : startOfFrame.components) {
|
||||
if (component.hSub != 1 || component.vSub != 1) {
|
||||
return JPEGColorSpace.YCbCr;
|
||||
@@ -571,7 +605,8 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
return JPEGColorSpace.YCCK;
|
||||
}
|
||||
else {
|
||||
// if subsampled, YCCK else CMYK
|
||||
// TODO: JPEGMetadata (standard format) will report YCbCrA for 4 channel subsampled... :-/
|
||||
// If subsampled, YCCK else CMYK
|
||||
for (SOFComponent component : startOfFrame.components) {
|
||||
if (component.hSub != 1 || component.vSub != 1) {
|
||||
return JPEGColorSpace.YCCK;
|
||||
@@ -597,6 +632,8 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
byte[] profileData = profile.getData(); // Need to clone entire profile, due to a JDK 7 bug
|
||||
|
||||
if (profileData[ICC_Profile.icHdrRenderingIntent] == ICC_Profile.icPerceptual) {
|
||||
processWarningOccurred("ICC profile is Perceptual but Display class, treating as Display class");
|
||||
|
||||
intToBigEndian(ICC_Profile.icSigDisplayClass, profileData, ICC_Profile.icHdrDeviceClass); // Header is first
|
||||
|
||||
return ICC_Profile.getInstance(profileData);
|
||||
@@ -653,13 +690,14 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
}
|
||||
}
|
||||
|
||||
private List<JPEGSegment> getAppSegments(final int marker, final String identifier) throws IOException {
|
||||
List<JPEGSegment> getAppSegments(final int marker, final String identifier) throws IOException {
|
||||
initHeader();
|
||||
|
||||
List<JPEGSegment> appSegments = Collections.emptyList();
|
||||
|
||||
for (JPEGSegment segment : segments) {
|
||||
if (segment.marker() == marker && (identifier == null || identifier.equals(segment.identifier()))) {
|
||||
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<JPEGSegment>(segments.size());
|
||||
}
|
||||
@@ -671,7 +709,7 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
return appSegments;
|
||||
}
|
||||
|
||||
private SOFSegment getSOF() throws IOException {
|
||||
SOFSegment getSOF() throws IOException {
|
||||
for (JPEGSegment segment : segments) {
|
||||
if (JPEG.SOF0 >= segment.marker() && segment.marker() <= JPEG.SOF3 ||
|
||||
JPEG.SOF5 >= segment.marker() && segment.marker() <= JPEG.SOF7 ||
|
||||
@@ -707,7 +745,7 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
return null;
|
||||
}
|
||||
|
||||
private AdobeDCTSegment getAdobeDCT() throws IOException {
|
||||
AdobeDCTSegment getAdobeDCT() throws IOException {
|
||||
// TODO: Investigate http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6355567: 33/35 byte Adobe APP14 markers
|
||||
List<JPEGSegment> adobe = getAppSegments(JPEG.APP14, "Adobe");
|
||||
|
||||
@@ -726,18 +764,18 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
return null;
|
||||
}
|
||||
|
||||
private JFIFSegment getJFIF() throws IOException{
|
||||
JFIFSegment getJFIF() throws IOException{
|
||||
List<JPEGSegment> jfif = getAppSegments(JPEG.APP0, "JFIF");
|
||||
|
||||
|
||||
if (!jfif.isEmpty()) {
|
||||
JPEGSegment segment = jfif.get(0);
|
||||
return JFIFSegment.read(segment.data());
|
||||
}
|
||||
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private JFXXSegment getJFXX() throws IOException {
|
||||
JFXXSegment getJFXX() throws IOException {
|
||||
List<JPEGSegment> jfxx = getAppSegments(JPEG.APP0, "JFXX");
|
||||
|
||||
if (!jfxx.isEmpty()) {
|
||||
@@ -748,6 +786,27 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
return null;
|
||||
}
|
||||
|
||||
private CompoundDirectory getExif() throws IOException {
|
||||
List<JPEGSegment> exifSegments = getAppSegments(JPEG.APP1, "Exif");
|
||||
|
||||
if (!exifSegments.isEmpty()) {
|
||||
JPEGSegment exif = exifSegments.get(0);
|
||||
InputStream data = exif.data();
|
||||
|
||||
if (data.read() == -1) { // Read pad
|
||||
processWarningOccurred("Exif chunk has no data.");
|
||||
}
|
||||
else {
|
||||
ImageInputStream stream = ImageIO.createImageInputStream(data);
|
||||
return (CompoundDirectory) new EXIFReader().read(stream);
|
||||
|
||||
// TODO: Directory offset of thumbnail is wrong/relative to container stream, causing trouble for the EXIFReader...
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// TODO: Util method?
|
||||
static byte[] readFully(DataInput stream, int len) throws IOException {
|
||||
if (len == 0) {
|
||||
@@ -759,7 +818,7 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
return data;
|
||||
}
|
||||
|
||||
private ICC_Profile getEmbeddedICCProfile(final boolean allowBadIndexes) throws IOException {
|
||||
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)
|
||||
@@ -893,7 +952,7 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
case JFXXSegment.JPEG:
|
||||
case JFXXSegment.INDEXED:
|
||||
case JFXXSegment.RGB:
|
||||
thumbnails.add(new JFXXThumbnailReader(thumbnailProgressDelegator, imageIndex, thumbnails.size(), jfxx));
|
||||
thumbnails.add(new JFXXThumbnailReader(thumbnailProgressDelegator, getThumbnailReader(), imageIndex, thumbnails.size(), jfxx));
|
||||
break;
|
||||
default:
|
||||
processWarningOccurred("Unknown JFXX extension code: " + jfxx.extensionCode);
|
||||
@@ -911,17 +970,23 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
processWarningOccurred("Exif chunk has no data.");
|
||||
}
|
||||
else {
|
||||
ImageInputStream stream = ImageIO.createImageInputStream(data);
|
||||
ImageInputStream stream = new MemoryCacheImageInputStream(data);
|
||||
CompoundDirectory exifMetadata = (CompoundDirectory) new EXIFReader().read(stream);
|
||||
|
||||
if (exifMetadata.directoryCount() == 2) {
|
||||
Directory ifd1 = exifMetadata.getDirectory(1);
|
||||
|
||||
Entry compression = ifd1.getEntryById(TIFF.TAG_COMPRESSION);
|
||||
|
||||
// 1 = no compression, 6 = JPEG compression (default)
|
||||
if (compression == null || compression.getValue().equals(1) || compression.getValue().equals(6)) {
|
||||
thumbnails.add(new EXIFThumbnailReader(thumbnailProgressDelegator, 0, thumbnails.size(), ifd1, stream));
|
||||
Entry jpegLength = ifd1.getEntryById(TIFF.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH);
|
||||
|
||||
if ((jpegLength == null || ((Number) jpegLength.getValue()).longValue() > 0)) {
|
||||
thumbnails.add(new EXIFThumbnailReader(thumbnailProgressDelegator, getThumbnailReader(), 0, thumbnails.size(), ifd1, stream));
|
||||
}
|
||||
else {
|
||||
processWarningOccurred("EXIF IFD with empty (zero-length) thumbnail");
|
||||
}
|
||||
}
|
||||
else {
|
||||
processWarningOccurred("EXIF IFD with unknown compression (expected 1 or 6): " + compression.getValue());
|
||||
@@ -932,6 +997,14 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
}
|
||||
}
|
||||
|
||||
ImageReader getThumbnailReader() throws IOException {
|
||||
if (thumbnailReader == null) {
|
||||
thumbnailReader = delegate.getOriginatingProvider().createReaderInstance();
|
||||
}
|
||||
|
||||
return thumbnailReader;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getNumThumbnails(final int imageIndex) throws IOException {
|
||||
readThumbnailMetadata(imageIndex);
|
||||
@@ -965,44 +1038,21 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
return thumbnails.get(thumbnailIndex).read();
|
||||
}
|
||||
|
||||
|
||||
// Metadata
|
||||
|
||||
@Override
|
||||
public IIOMetadata getImageMetadata(int imageIndex) throws IOException {
|
||||
// TODO: Nice try, but no cigar.. getAsTree does not return a "live" view, so any modifications are thrown away
|
||||
IIOMetadata metadata = delegate.getImageMetadata(imageIndex);
|
||||
IIOMetadata imageMetadata = delegate.getImageMetadata(imageIndex);
|
||||
|
||||
// IIOMetadataNode tree = (IIOMetadataNode) metadata.getAsTree(metadata.getNativeMetadataFormatName());
|
||||
// Node jpegVariety = tree.getElementsByTagName("JPEGvariety").item(0);
|
||||
if (imageMetadata != null && Arrays.asList(imageMetadata.getMetadataFormatNames()).contains(JPEGImage10MetadataCleaner.JAVAX_IMAGEIO_JPEG_IMAGE_1_0)) {
|
||||
if (metadataCleaner == null) {
|
||||
metadataCleaner = new JPEGImage10MetadataCleaner(this);
|
||||
}
|
||||
|
||||
// TODO: Allow EXIF (as app1EXIF) in the JPEGvariety (sic) node.
|
||||
// As EXIF is (a subset of) TIFF, (and the EXIF data is a valid TIFF stream) probably use something like:
|
||||
// http://download.java.net/media/jai-imageio/javadoc/1.1/com/sun/media/imageio/plugins/tiff/package-summary.html#ImageMetadata
|
||||
/*
|
||||
from: http://docs.oracle.com/javase/6/docs/api/javax/imageio/metadata/doc-files/jpeg_metadata.html
|
||||
return metadataCleaner.cleanMetadata(imageMetadata);
|
||||
}
|
||||
|
||||
In future versions of the JPEG metadata format, other varieties of JPEG metadata may be supported (e.g. Exif)
|
||||
by defining other types of nodes which may appear as a child of the JPEGvariety node.
|
||||
|
||||
(Note that an application wishing to interpret Exif metadata given a metadata tree structure in the
|
||||
javax_imageio_jpeg_image_1.0 format must check for an unknown marker segment with a tag indicating an
|
||||
APP1 marker and containing data identifying it as an Exif marker segment. Then it may use application-specific
|
||||
code to interpret the data in the marker segment. If such an application were to encounter a metadata tree
|
||||
formatted according to a future version of the JPEG metadata format, the Exif marker segment might not be
|
||||
unknown in that format - it might be structured as a child node of the JPEGvariety node.
|
||||
|
||||
Thus, it is important for an application to specify which version to use by passing the string identifying
|
||||
the version to the method/constructor used to obtain an IIOMetadata object.)
|
||||
*/
|
||||
|
||||
// IIOMetadataNode app2ICC = new IIOMetadataNode("app2ICC");
|
||||
// app2ICC.setUserObject(getEmbeddedICCProfile());
|
||||
// jpegVariety.getFirstChild().appendChild(app2ICC);
|
||||
|
||||
// new XMLSerializer(System.err, System.getProperty("file.encoding")).serialize(tree, false);
|
||||
|
||||
return metadata;
|
||||
return imageMetadata;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1010,6 +1060,11 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
return delegate.getStreamMetadata();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processWarningOccurred(String warning) {
|
||||
super.processWarningOccurred(warning);
|
||||
}
|
||||
|
||||
private static void invertCMYK(final Raster raster) {
|
||||
byte[] data = ((DataBufferByte) raster.getDataBuffer()).getData();
|
||||
|
||||
@@ -1321,10 +1376,10 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
// start = System.currentTimeMillis();
|
||||
float aspect = reader.getAspectRatio(0);
|
||||
if (aspect >= 1f) {
|
||||
image = ImageUtil.createResampled(image, maxW, Math.round(maxW / aspect), Image.SCALE_DEFAULT);
|
||||
image = ImageUtil.createResampled(image, maxW, Math.round(maxW / aspect), Image.SCALE_SMOOTH);
|
||||
}
|
||||
else {
|
||||
image = ImageUtil.createResampled(image, Math.round(maxH * aspect), maxH, Image.SCALE_DEFAULT);
|
||||
image = ImageUtil.createResampled(image, Math.round(maxH * aspect), maxH, Image.SCALE_SMOOTH);
|
||||
}
|
||||
// System.err.println("Scale time: " + (System.currentTimeMillis() - start) + " ms");
|
||||
}
|
||||
@@ -1332,6 +1387,14 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
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();
|
||||
|
||||
int numThumbnails = reader.getNumThumbnails(0);
|
||||
for (int i = 0; i < numThumbnails; i++) {
|
||||
BufferedImage thumbnail = reader.readThumbnail(0, i);
|
||||
@@ -1340,7 +1403,7 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
}
|
||||
}
|
||||
catch (IIOException e) {
|
||||
System.err.println("Could not read thumbnails: " + e.getMessage());
|
||||
System.err.println("Could not read thumbnails: " + arg + ": " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,9 @@ import javax.imageio.ImageReader;
|
||||
import javax.imageio.metadata.IIOMetadataFormat;
|
||||
import javax.imageio.spi.ImageReaderSpi;
|
||||
import javax.imageio.spi.ServiceRegistry;
|
||||
import javax.imageio.stream.ImageInputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Iterator;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
@@ -65,7 +67,7 @@ public class JPEGImageReaderSpi extends ImageReaderSpi {
|
||||
new String[]{"jpg", "jpeg"},
|
||||
new String[]{"image/jpeg"},
|
||||
"com.twelvemonkeys.imageio.plugins.jpeg.JPEGImageReader",
|
||||
STANDARD_INPUT_TYPE,
|
||||
new Class[] {ImageInputStream.class},
|
||||
new String[] {"com.twelvemonkeys.imageio.plugins.jpeg.JPEGImageWriterSpi"},
|
||||
true, null, null, null, null,
|
||||
true, null, null, null, null
|
||||
@@ -84,14 +86,14 @@ public class JPEGImageReaderSpi extends ImageReaderSpi {
|
||||
}
|
||||
|
||||
static ImageReaderSpi lookupDelegateProvider(final ServiceRegistry registry) {
|
||||
// Should be safe to lookup now, as the bundled providers are hardcoded usually
|
||||
try {
|
||||
return (ImageReaderSpi) registry.getServiceProviderByClass(Class.forName("com.sun.imageio.plugins.jpeg.JPEGImageReaderSpi"));
|
||||
}
|
||||
catch (ClassNotFoundException ignore) {
|
||||
}
|
||||
catch (SecurityException e) {
|
||||
e.printStackTrace();
|
||||
Iterator<ImageReaderSpi> providers = registry.getServiceProviders(ImageReaderSpi.class, true);
|
||||
|
||||
while (providers.hasNext()) {
|
||||
ImageReaderSpi provider = providers.next();
|
||||
|
||||
if (provider.getClass().getName().equals("com.sun.imageio.plugins.jpeg.JPEGImageReaderSpi")) {
|
||||
return provider;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -37,8 +37,10 @@ import javax.imageio.ImageWriter;
|
||||
import javax.imageio.metadata.IIOMetadataFormat;
|
||||
import javax.imageio.spi.ImageWriterSpi;
|
||||
import javax.imageio.spi.ServiceRegistry;
|
||||
import javax.imageio.stream.ImageOutputStream;
|
||||
import java.awt.image.RenderedImage;
|
||||
import java.io.IOException;
|
||||
import java.util.Iterator;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
@@ -66,9 +68,9 @@ public class JPEGImageWriterSpi extends ImageWriterSpi {
|
||||
new String[]{"JPEG", "jpeg", "JPG", "jpg"},
|
||||
new String[]{"jpg", "jpeg"},
|
||||
new String[]{"image/jpeg"},
|
||||
"twelvemonkeys.imageio.plugins.jpeg.JPEGImageWriter",
|
||||
STANDARD_OUTPUT_TYPE,
|
||||
new String[] {"twelvemonkeys.imageio.plugins.jpeg.JPEGImageReaderSpi"},
|
||||
"com.twelvemonkeys.imageio.plugins.jpeg.JPEGImageWriter",
|
||||
new Class[] { ImageOutputStream.class },
|
||||
new String[] {"com.twelvemonkeys.imageio.plugins.jpeg.JPEGImageReaderSpi"},
|
||||
true, null, null, null, null,
|
||||
true, null, null, null, null
|
||||
);
|
||||
@@ -86,14 +88,14 @@ public class JPEGImageWriterSpi extends ImageWriterSpi {
|
||||
}
|
||||
|
||||
static ImageWriterSpi lookupDelegateProvider(final ServiceRegistry registry) {
|
||||
// Should be safe to lookup now, as the bundled providers are hardcoded usually
|
||||
try {
|
||||
return (ImageWriterSpi) registry.getServiceProviderByClass(Class.forName("com.sun.imageio.plugins.jpeg.JPEGImageWriterSpi"));
|
||||
}
|
||||
catch (ClassNotFoundException ignore) {
|
||||
}
|
||||
catch (SecurityException e) {
|
||||
e.printStackTrace();
|
||||
Iterator<ImageWriterSpi> providers = registry.getServiceProviders(ImageWriterSpi.class, true);
|
||||
|
||||
while (providers.hasNext()) {
|
||||
ImageWriterSpi provider = providers.next();
|
||||
|
||||
if (provider.getClass().getName().equals("com.sun.imageio.plugins.jpeg.JPEGImageWriterSpi")) {
|
||||
return provider;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -91,16 +91,29 @@ final class JPEGSegmentImageInputStream extends ImageInputStreamImpl {
|
||||
long realPosition = stream.getStreamPosition();
|
||||
int marker = stream.readUnsignedShort();
|
||||
|
||||
// Skip over weird 0x00 padding, but leave in stream, read seems to handle it well with a warning
|
||||
int trash = 0;
|
||||
while (marker == 0) {
|
||||
marker = stream.readUnsignedShort();
|
||||
trash += 2;
|
||||
}
|
||||
|
||||
if (marker == 0x00ff) {
|
||||
trash++;
|
||||
marker = 0xff00 | stream.readUnsignedByte();
|
||||
}
|
||||
|
||||
// Skip over 0xff padding between markers
|
||||
while (marker == 0xffff) {
|
||||
realPosition++;
|
||||
marker = 0xff00 | stream.readUnsignedByte();
|
||||
}
|
||||
|
||||
// TODO: Optionally skip JFIF only for non-JFIF conformant streams
|
||||
// TODO: Refactor to make various segments optional, we probably only want the "Adobe" APP14 segment, 'Exif' APP1 and very few others
|
||||
if (isAppSegmentMarker(marker) && marker != JPEG.APP0 && !(marker == JPEG.APP1 && isAppSegmentWithId("Exif", stream)) && marker != JPEG.APP14) {
|
||||
if (isAppSegmentMarker(marker) && !(marker == JPEG.APP1 && isAppSegmentWithId("Exif", stream)) && marker != JPEG.APP14) {
|
||||
int length = stream.readUnsignedShort(); // Length including length field itself
|
||||
stream.seek(realPosition + 2 + length); // Skip marker (2) + length
|
||||
stream.seek(realPosition + trash + 2 + length); // Skip marker (2) + length
|
||||
}
|
||||
else {
|
||||
if (marker == JPEG.EOI) {
|
||||
@@ -116,7 +129,7 @@ final class JPEGSegmentImageInputStream extends ImageInputStreamImpl {
|
||||
}
|
||||
else {
|
||||
// Length including length field itself
|
||||
length = stream.readUnsignedShort() + 2;
|
||||
length = trash + stream.readUnsignedShort() + 2;
|
||||
}
|
||||
|
||||
segment = new Segment(marker, realPosition, segment.end(), length);
|
||||
|
||||
@@ -28,12 +28,12 @@
|
||||
|
||||
package com.twelvemonkeys.imageio.plugins.jpeg;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.imageio.ImageReader;
|
||||
import javax.imageio.stream.ImageInputStream;
|
||||
import java.awt.*;
|
||||
import java.awt.color.ColorSpace;
|
||||
import java.awt.image.*;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* ThumbnailReader
|
||||
@@ -42,6 +42,7 @@ import java.io.InputStream;
|
||||
* @author last modified by $Author: haraldk$
|
||||
* @version $Id: ThumbnailReader.java,v 1.0 18.04.12 12:22 haraldk Exp$
|
||||
*/
|
||||
// TODO: Get rid of the com.sun import!!
|
||||
abstract class ThumbnailReader {
|
||||
|
||||
private final ThumbnailReadProgressListener progressListener;
|
||||
@@ -49,10 +50,11 @@ abstract class ThumbnailReader {
|
||||
protected final int thumbnailIndex;
|
||||
|
||||
protected ThumbnailReader(final ThumbnailReadProgressListener progressListener, final int imageIndex, final int thumbnailIndex) {
|
||||
this.progressListener = progressListener;
|
||||
this.progressListener = progressListener != null ? progressListener : new NullProgressListener();
|
||||
this.imageIndex = imageIndex;
|
||||
this.thumbnailIndex = thumbnailIndex;
|
||||
}
|
||||
|
||||
protected final void processThumbnailStarted() {
|
||||
progressListener.processThumbnailStarted(imageIndex, thumbnailIndex);
|
||||
}
|
||||
@@ -65,8 +67,20 @@ abstract class ThumbnailReader {
|
||||
progressListener.processThumbnailComplete();
|
||||
}
|
||||
|
||||
static protected BufferedImage readJPEGThumbnail(InputStream stream) throws IOException {
|
||||
return ImageIO.read(stream);
|
||||
static protected BufferedImage readJPEGThumbnail(final ImageReader reader, final ImageInputStream stream) throws IOException {
|
||||
// try {
|
||||
// try {
|
||||
reader.setInput(stream);
|
||||
|
||||
return reader.read(0);
|
||||
// }
|
||||
// finally {
|
||||
// input.close();
|
||||
// }
|
||||
// }
|
||||
// finally {
|
||||
// reader.dispose();
|
||||
// }
|
||||
}
|
||||
|
||||
static protected BufferedImage readRawThumbnail(final byte[] thumbnail, final int size, final int offset, int w, int h) {
|
||||
@@ -82,4 +96,15 @@ abstract class ThumbnailReader {
|
||||
public abstract int getWidth() throws IOException;
|
||||
|
||||
public abstract int getHeight() throws IOException;
|
||||
|
||||
private static class NullProgressListener implements ThumbnailReadProgressListener {
|
||||
public void processThumbnailStarted(int imageIndex, int thumbnailIndex) {
|
||||
}
|
||||
|
||||
public void processThumbnailProgress(float percentageDone) {
|
||||
}
|
||||
|
||||
public void processThumbnailComplete() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ public class EXIFThumbnailReaderTest extends AbstractThumbnailReaderTest {
|
||||
|
||||
assertEquals(2, ifds.directoryCount());
|
||||
|
||||
return new EXIFThumbnailReader(progressListener, imageIndex, thumbnailIndex, ifds.getDirectory(1), exifStream);
|
||||
return new EXIFThumbnailReader(progressListener, ImageIO.getImageReadersByFormatName("JPEG").next(), imageIndex, thumbnailIndex, ifds.getDirectory(1), exifStream);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -34,6 +34,7 @@ import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegmentUtil;
|
||||
import org.junit.Test;
|
||||
import org.mockito.InOrder;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.imageio.stream.ImageInputStream;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.IOException;
|
||||
@@ -63,7 +64,7 @@ public class JFXXThumbnailReaderTest extends AbstractThumbnailReaderTest {
|
||||
assertFalse(segments.isEmpty());
|
||||
|
||||
JPEGSegment jfxx = segments.get(0);
|
||||
return new JFXXThumbnailReader(progressListener, imageIndex, thumbnailIndex, JFXXSegment.read(jfxx.data(), jfxx.length()));
|
||||
return new JFXXThumbnailReader(progressListener, ImageIO.getImageReadersByFormatName("jpeg").next(), imageIndex, thumbnailIndex, JFXXSegment.read(jfxx.data(), jfxx.length()));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -29,23 +29,31 @@
|
||||
package com.twelvemonkeys.imageio.plugins.jpeg;
|
||||
|
||||
import com.twelvemonkeys.imageio.util.ImageReaderAbstractTestCase;
|
||||
import org.hamcrest.core.IsInstanceOf;
|
||||
import org.junit.Test;
|
||||
import org.mockito.internal.matchers.GreaterThan;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.NamedNodeMap;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
|
||||
import javax.imageio.IIOException;
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.imageio.ImageReadParam;
|
||||
import javax.imageio.ImageTypeSpecifier;
|
||||
import javax.imageio.*;
|
||||
import javax.imageio.event.IIOReadWarningListener;
|
||||
import javax.imageio.metadata.IIOMetadata;
|
||||
import javax.imageio.metadata.IIOMetadataFormatImpl;
|
||||
import javax.imageio.metadata.IIOMetadataNode;
|
||||
import javax.imageio.plugins.jpeg.JPEGHuffmanTable;
|
||||
import javax.imageio.plugins.jpeg.JPEGQTable;
|
||||
import javax.imageio.spi.IIORegistry;
|
||||
import javax.imageio.spi.ImageReaderSpi;
|
||||
import javax.imageio.stream.ImageInputStream;
|
||||
import java.awt.*;
|
||||
import java.awt.color.ColorSpace;
|
||||
import java.awt.color.ICC_Profile;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.DataBufferByte;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
@@ -321,6 +329,25 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTestCase<JPEGImageRe
|
||||
verify(warningListener).warningOccurred(eq(reader), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCorbisRGB() throws IOException {
|
||||
// Special case, throws exception below without special treatment
|
||||
// java.awt.color.CMMException: General CMM error517
|
||||
JPEGImageReader reader = createReader();
|
||||
reader.setInput(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/cmm-exception-corbis-rgb.jpg")));
|
||||
|
||||
assertEquals(512, reader.getWidth(0));
|
||||
assertEquals(384, reader.getHeight(0));
|
||||
|
||||
BufferedImage image = reader.read(0);
|
||||
|
||||
assertNotNull(image);
|
||||
assertEquals(512, image.getWidth());
|
||||
assertEquals(384, image.getHeight());
|
||||
|
||||
reader.dispose();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHasThumbnailNoIFD1() throws IOException {
|
||||
JPEGImageReader reader = createReader();
|
||||
@@ -600,10 +627,59 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTestCase<JPEGImageRe
|
||||
reader.dispose();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadNoJFIFYCbCr() throws IOException {
|
||||
// Basically the same issue as http://stackoverflow.com/questions/9340569/jpeg-image-with-wrong-colors
|
||||
JPEGImageReader reader = createReader();
|
||||
reader.setInput(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/no-jfif-ycbcr.jpg")));
|
||||
|
||||
assertEquals(310, reader.getWidth(0));
|
||||
assertEquals(206, reader.getHeight(0));
|
||||
|
||||
ImageReadParam param = reader.getDefaultReadParam();
|
||||
param.setSourceRegion(new Rectangle(0, 0, 310, 8));
|
||||
BufferedImage image = reader.read(0, param);
|
||||
assertNotNull(image);
|
||||
assertEquals(310, image.getWidth());
|
||||
assertEquals(8, image.getHeight());
|
||||
|
||||
int[] expectedRGB = new int[] {
|
||||
0xff3c1b14, 0xff35140b, 0xff4b2920, 0xff3b160e, 0xff49231a, 0xff874e3d, 0xff563d27, 0xff926c61,
|
||||
0xff350005, 0xff84432d, 0xff754f46, 0xff2c2223, 0xff422016, 0xff220f0b, 0xff251812, 0xff1c1209,
|
||||
0xff483429, 0xff1b140c, 0xff231c16, 0xff2f261f, 0xff2e2923, 0xff170c08, 0xff383025, 0xff443b34,
|
||||
0xff574a39, 0xff3b322b, 0xffeee1d0, 0xffebdecd, 0xffe9dccb, 0xffe8dbca, 0xffe7dcca,
|
||||
};
|
||||
|
||||
// Validate strip colors
|
||||
for (int i = 0; i < image.getWidth() / 10; i++) {
|
||||
int actualRGB = image.getRGB(i * 10, 7);
|
||||
assertEquals((actualRGB >> 16) & 0xff, (expectedRGB[i] >> 16) & 0xff, 5);
|
||||
assertEquals((actualRGB >> 8) & 0xff, (expectedRGB[i] >> 8) & 0xff, 5);
|
||||
assertEquals((actualRGB ) & 0xff, (expectedRGB[i] ) & 0xff, 5);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testXDensityOutOfRangeIssue() throws IOException {
|
||||
// Image has JFIF with x/y density 0
|
||||
JPEGImageReader reader = createReader();
|
||||
reader.setInput(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/xdensity-out-of-range-zero.jpg")));
|
||||
|
||||
IIOMetadata imageMetadata = reader.getImageMetadata(0);
|
||||
assertNotNull(imageMetadata);
|
||||
|
||||
// Assume that the aspect ratio is 1 if both x/y density is 0.
|
||||
IIOMetadataNode tree = (IIOMetadataNode) imageMetadata.getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName);
|
||||
NodeList dimensions = tree.getElementsByTagName("Dimension");
|
||||
assertEquals(1, dimensions.getLength());
|
||||
assertEquals("PixelAspectRatio", dimensions.item(0).getFirstChild().getNodeName());
|
||||
assertEquals("1.0", ((Element) dimensions.item(0).getFirstChild()).getAttribute("value"));
|
||||
}
|
||||
|
||||
// TODO: Test RGBA/YCbCrA handling
|
||||
|
||||
@Test
|
||||
public void testReadMetadataMaybeNull() throws IOException {
|
||||
public void testReadMetadata() throws IOException {
|
||||
// Just test that we can read the metadata without exceptions
|
||||
JPEGImageReader reader = createReader();
|
||||
|
||||
@@ -614,11 +690,276 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTestCase<JPEGImageRe
|
||||
try {
|
||||
IIOMetadata metadata = reader.getImageMetadata(i);
|
||||
assertNotNull(String.format("Image metadata null for %s image %s", testData, i), metadata);
|
||||
|
||||
Node tree = metadata.getAsTree(metadata.getNativeMetadataFormatName());
|
||||
assertNotNull(tree);
|
||||
assertThat(tree, new IsInstanceOf(IIOMetadataNode.class));
|
||||
|
||||
IIOMetadataNode iioTree = (IIOMetadataNode) tree;
|
||||
assertEquals(1, iioTree.getElementsByTagName("JPEGvariety").getLength());
|
||||
Node jpegVariety = iioTree.getElementsByTagName("JPEGvariety").item(0);
|
||||
assertNotNull(jpegVariety);
|
||||
|
||||
Node app0JFIF = jpegVariety.getFirstChild();
|
||||
if (app0JFIF != null) {
|
||||
assertEquals("app0JFIF", app0JFIF.getLocalName());
|
||||
}
|
||||
|
||||
NodeList markerSequences = iioTree.getElementsByTagName("markerSequence");
|
||||
assertTrue(markerSequences.getLength() == 1 || markerSequences.getLength() == 2); // In case of JPEG encoded thumbnail, there will be 2
|
||||
IIOMetadataNode markerSequence = (IIOMetadataNode) markerSequences.item(0);
|
||||
assertNotNull(markerSequence);
|
||||
assertThat(markerSequence.getChildNodes().getLength(), new GreaterThan<Integer>(0));
|
||||
|
||||
NodeList unknowns = markerSequence.getElementsByTagName("unknown");
|
||||
for (int j = 0; j < unknowns.getLength(); j++) {
|
||||
IIOMetadataNode unknown = (IIOMetadataNode) unknowns.item(j);
|
||||
assertNotNull(unknown.getUserObject()); // All unknowns must have user object (data array)
|
||||
}
|
||||
}
|
||||
catch (IIOException e) {
|
||||
System.err.println(String.format("WARNING: Reading metadata failed for %s image %s: %s", testData, i, e.getMessage()));
|
||||
fail(String.format("Reading metadata failed for %s image %s: %s", testData, i, e.getMessage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadInconsistentMetadata() throws IOException {
|
||||
// A collection of JPEG files that makes the JPEGImageReader throw exception "Inconsistent metadata read from stream"...
|
||||
List<String> resources = Arrays.asList(
|
||||
"/jpeg/jfif-jfxx-thumbnail-olympus-d320l.jpg", // Ok
|
||||
"/jpeg/gray-sample.jpg", // Ok
|
||||
"/jpeg/cmyk-sample.jpg",
|
||||
"/jpeg/cmyk-sample-multiple-chunk-icc.jpg",
|
||||
"/jpeg/invalid-icc-duplicate-sequence-numbers-rgb-xerox-dc250-heavyweight-1-progressive-jfif.jpg",
|
||||
"/jpeg/no-image-types-rgb-us-web-coated-v2-ms-photogallery-exif.jpg"
|
||||
);
|
||||
|
||||
for (String resource : resources) {
|
||||
// Just test that we can read the metadata without exceptions
|
||||
JPEGImageReader reader = createReader();
|
||||
ImageInputStream stream = ImageIO.createImageInputStream(getClassLoaderResource(resource));
|
||||
|
||||
try {
|
||||
reader.setInput(stream);
|
||||
IIOMetadata metadata = reader.getImageMetadata(0);
|
||||
assertNotNull(String.format("%s: null metadata", resource), metadata);
|
||||
|
||||
Node tree = metadata.getAsTree(metadata.getNativeMetadataFormatName());
|
||||
assertNotNull(tree);
|
||||
// new XMLSerializer(System.err, System.getProperty("file.encoding")).serialize(tree, false);
|
||||
|
||||
}
|
||||
catch (IIOException e) {
|
||||
AssertionError fail = new AssertionError(String.format("Reading metadata failed for %ss: %s", resource, e.getMessage()));
|
||||
fail.initCause(e);
|
||||
throw fail;
|
||||
}
|
||||
finally {
|
||||
stream.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadMetadataEqualReference() throws IOException {
|
||||
// Compares the metadata for JFIF-conformant files with metadata from com.sun...JPEGImageReader
|
||||
JPEGImageReader reader = createReader();
|
||||
ImageReader referenceReader;
|
||||
|
||||
try {
|
||||
@SuppressWarnings("unchecked")
|
||||
Class<ImageReaderSpi> spiClass = (Class<ImageReaderSpi>) Class.forName("com.sun.imageio.plugins.jpeg.JPEGImageReaderSpi");
|
||||
ImageReaderSpi provider = spiClass.newInstance();
|
||||
referenceReader = provider.createReaderInstance();
|
||||
}
|
||||
catch (Throwable t) {
|
||||
System.err.println("WARNING: Could not create ImageReader for reference (missing dependency): " + t.getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
for (TestData testData : getTestData()) {
|
||||
reader.setInput(testData.getInputStream());
|
||||
referenceReader.setInput(testData.getInputStream());
|
||||
|
||||
for (int i = 0; i < reader.getNumImages(true); i++) {
|
||||
try {
|
||||
IIOMetadata reference = referenceReader.getImageMetadata(i);
|
||||
|
||||
try {
|
||||
IIOMetadata metadata = reader.getImageMetadata(i);
|
||||
|
||||
String[] formatNames = reference.getMetadataFormatNames();
|
||||
for (String formatName : formatNames) {
|
||||
Node referenceTree = reference.getAsTree(formatName);
|
||||
Node actualTree = metadata.getAsTree(formatName);
|
||||
|
||||
// new XMLSerializer(System.out, System.getProperty("file.encoding")).serialize(actualTree, false);
|
||||
assertTreesEquals(String.format("Metadata differs for %s image %s ", testData, i), referenceTree, actualTree);
|
||||
}
|
||||
}
|
||||
catch (IIOException e) {
|
||||
AssertionError fail = new AssertionError(String.format("Reading metadata failed for %s image %s: %s", testData, i, e.getMessage()));
|
||||
fail.initCause(e);
|
||||
throw fail;
|
||||
}
|
||||
}
|
||||
catch (IIOException ignore) {
|
||||
// The reference reader will fail on certain images, we'll just ignore that
|
||||
System.err.println(String.format("WARNING: Reading reference metadata failed for %s image %s: %s", testData, i, ignore.getMessage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void assertTreesEquals(String message, Node expectedTree, Node actualTree) {
|
||||
if (expectedTree == actualTree) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (expectedTree == null) {
|
||||
assertNull(actualTree);
|
||||
}
|
||||
|
||||
assertEquals(String.format("%s: Node names differ", message), expectedTree.getNodeName(), actualTree.getNodeName());
|
||||
|
||||
NamedNodeMap expectedAttributes = expectedTree.getAttributes();
|
||||
NamedNodeMap actualAttributes = actualTree.getAttributes();
|
||||
assertEquals(String.format("%s: Number of attributes for <%s> differ", message, expectedTree.getNodeName()), expectedAttributes.getLength(), actualAttributes.getLength());
|
||||
for (int i = 0; i < expectedAttributes.getLength(); i++) {
|
||||
Node item = expectedAttributes.item(i);
|
||||
assertEquals(String.format("%s: \"%s\" attribute for <%s> differ", message, item.getNodeName(), expectedTree.getNodeName()), item.getNodeValue(), actualAttributes.getNamedItem(item.getNodeName()).getNodeValue());
|
||||
}
|
||||
|
||||
// Test for equal user objects.
|
||||
// - array equals or reflective equality... Most user objects does not have a decent equals method.. :-P
|
||||
if (expectedTree instanceof IIOMetadataNode) {
|
||||
assertTrue(String.format("%s: %s not an IIOMetadataNode", message, expectedTree.getNodeName()), actualTree instanceof IIOMetadataNode);
|
||||
|
||||
Object expectedUserObject = ((IIOMetadataNode) expectedTree).getUserObject();
|
||||
|
||||
if (expectedUserObject != null) {
|
||||
Object actualUserObject = ((IIOMetadataNode) actualTree).getUserObject();
|
||||
assertNotNull(String.format("%s: User object missing for <%s>", message, expectedTree.getNodeName()), actualUserObject);
|
||||
assertEqualUserObjects(String.format("%s: User objects for <%s MarkerTag\"%s\"> differ", message, expectedTree.getNodeName(), ((IIOMetadataNode) expectedTree).getAttribute("MarkerTag")), expectedUserObject, actualUserObject);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort nodes to make sure that sequence of equally named tags does not matter
|
||||
List<IIOMetadataNode> expectedChildren = sortNodes(expectedTree.getChildNodes());
|
||||
List<IIOMetadataNode> actualChildren = sortNodes(actualTree.getChildNodes());
|
||||
|
||||
assertEquals(String.format("%s: Number of child nodes for %s differ", message, expectedTree.getNodeName()), expectedChildren.size(), actualChildren.size());
|
||||
|
||||
for (int i = 0; i < expectedChildren.size(); i++) {
|
||||
assertTreesEquals(message + "<" + expectedTree.getNodeName() + ">", expectedChildren.get(i), actualChildren.get(i));
|
||||
}
|
||||
}
|
||||
|
||||
private void assertEqualUserObjects(String message, Object expectedUserObject, Object actualUserObject) {
|
||||
if (expectedUserObject.equals(actualUserObject)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (expectedUserObject instanceof ICC_Profile) {
|
||||
if (actualUserObject instanceof ICC_Profile) {
|
||||
assertArrayEquals(message, ((ICC_Profile) expectedUserObject).getData(), ((ICC_Profile) actualUserObject).getData());
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (expectedUserObject instanceof byte[]) {
|
||||
if (actualUserObject instanceof byte[]) {
|
||||
assertArrayEquals(message, (byte[]) expectedUserObject, (byte[]) actualUserObject);
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (expectedUserObject instanceof JPEGHuffmanTable) {
|
||||
if (actualUserObject instanceof JPEGHuffmanTable) {
|
||||
assertArrayEquals(message, ((JPEGHuffmanTable) expectedUserObject).getLengths(), ((JPEGHuffmanTable) actualUserObject).getLengths());
|
||||
assertArrayEquals(message, ((JPEGHuffmanTable) expectedUserObject).getValues(), ((JPEGHuffmanTable) actualUserObject).getValues());
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (expectedUserObject instanceof JPEGQTable) {
|
||||
if (actualUserObject instanceof JPEGQTable) {
|
||||
assertArrayEquals(message, ((JPEGQTable) expectedUserObject).getTable(), ((JPEGQTable) actualUserObject).getTable());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
fail(expectedUserObject.getClass().getName());
|
||||
}
|
||||
|
||||
private List<IIOMetadataNode> sortNodes(final NodeList nodes) {
|
||||
ArrayList<IIOMetadataNode> sortedNodes = new ArrayList<IIOMetadataNode>(new AbstractList<IIOMetadataNode>() {
|
||||
@Override
|
||||
public IIOMetadataNode get(int index) {
|
||||
return (IIOMetadataNode) nodes.item(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return nodes.getLength();
|
||||
}
|
||||
});
|
||||
|
||||
Collections.sort(
|
||||
sortedNodes,
|
||||
new Comparator<IIOMetadataNode>() {
|
||||
public int compare(IIOMetadataNode left, IIOMetadataNode right) {
|
||||
int res = left.getNodeName().compareTo(right.getNodeName());
|
||||
if (res != 0) {
|
||||
return res;
|
||||
}
|
||||
|
||||
// Compare attribute values
|
||||
NamedNodeMap leftAttributes = left.getAttributes(); // TODO: We should sort left's attributes as well, for stable sorting + handle diffs in attributes
|
||||
NamedNodeMap rightAttributes = right.getAttributes();
|
||||
|
||||
for (int i = 0; i < leftAttributes.getLength(); i++) {
|
||||
Node leftAttribute = leftAttributes.item(i);
|
||||
Node rightAttribute = rightAttributes.getNamedItem(leftAttribute.getNodeName());
|
||||
|
||||
if (rightAttribute == null) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
res = leftAttribute.getNodeValue().compareTo(rightAttribute.getNodeValue());
|
||||
if (res != 0) {
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
if (left.getUserObject() instanceof byte[] && right.getUserObject() instanceof byte[]) {
|
||||
byte[] leftBytes = (byte[]) left.getUserObject();
|
||||
byte[] rightBytes = (byte[]) right.getUserObject();
|
||||
|
||||
if (leftBytes.length < rightBytes.length) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (leftBytes.length > rightBytes.length) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (leftBytes.length > 0) {
|
||||
for (int i = 0; i < leftBytes.length; i++) {
|
||||
if (leftBytes[i] < rightBytes[i]) {
|
||||
return -1;
|
||||
}
|
||||
if (leftBytes[i] > rightBytes[i]) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return sortedNodes;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ public class JPEGSegmentImageInputStreamTest {
|
||||
public void testStreamRealData() throws IOException {
|
||||
ImageInputStream stream = new JPEGSegmentImageInputStream(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/invalid-icc-duplicate-sequence-numbers-rgb-internal-kodak-srgb-jfif.jpg")));
|
||||
assertEquals(JPEG.SOI, stream.readUnsignedShort());
|
||||
assertEquals(JPEG.APP0, stream.readUnsignedShort());
|
||||
assertEquals(JPEG.DQT, stream.readUnsignedShort());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -88,7 +88,7 @@ public class JPEGSegmentImageInputStreamTest {
|
||||
// NOTE: read(byte[], int, int) must always read len bytes (or until EOF), due to known bug in Sun code
|
||||
assertEquals(20, stream.read(bytes, 0, 20));
|
||||
|
||||
assertArrayEquals(new byte[] {(byte) 0xFF, (byte) 0xD8, (byte) 0xFF, (byte) 0xE0, 0x0, 0x10, 'J', 'F', 'I', 'F', 0x0, 0x1, 0x1, 0x1, 0x1, (byte) 0xCC, 0x1, (byte) 0xCC, 0, 0}, bytes);
|
||||
assertArrayEquals(new byte[] {(byte) 0xFF, (byte) 0xD8, (byte) 0xFF, (byte) 0xDB, 0x0, 0x43, 0x0, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1}, bytes);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -102,7 +102,7 @@ public class JPEGSegmentImageInputStreamTest {
|
||||
|
||||
assertThat(length, new LessOrEqual<Long>(10203l)); // In no case should length increase
|
||||
|
||||
assertEquals(9625l, length); // May change, if more chunks are passed to reader...
|
||||
assertEquals(9607L, length); // May change, if more chunks are passed to reader...
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -110,18 +110,15 @@ public class JPEGSegmentImageInputStreamTest {
|
||||
ImageInputStream stream = new JPEGSegmentImageInputStream(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/no-image-types-rgb-us-web-coated-v2-ms-photogallery-exif.jpg")));
|
||||
List<JPEGSegment> appSegments = JPEGSegmentUtil.readSegments(stream, JPEGSegmentUtil.APP_SEGMENTS);
|
||||
|
||||
assertEquals(3, appSegments.size());
|
||||
assertEquals(2, appSegments.size());
|
||||
|
||||
assertEquals(JPEG.APP0, appSegments.get(0).marker());
|
||||
assertEquals("JFIF", appSegments.get(0).identifier());
|
||||
assertEquals(JPEG.APP1, appSegments.get(0).marker());
|
||||
assertEquals("Exif", appSegments.get(0).identifier());
|
||||
|
||||
assertEquals(JPEG.APP1, appSegments.get(1).marker());
|
||||
assertEquals("Exif", appSegments.get(1).identifier());
|
||||
assertEquals(JPEG.APP14, appSegments.get(1).marker());
|
||||
assertEquals("Adobe", appSegments.get(1).identifier());
|
||||
|
||||
assertEquals(JPEG.APP14, appSegments.get(2).marker());
|
||||
assertEquals("Adobe", appSegments.get(2).identifier());
|
||||
|
||||
// And thus, no XMP, no ICC_PROFILE or other segments
|
||||
// And thus, no JFIF, no XMP, no ICC_PROFILE or other segments
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -133,7 +130,7 @@ public class JPEGSegmentImageInputStreamTest {
|
||||
length++;
|
||||
}
|
||||
|
||||
assertEquals(9299l, length); // Sanity check: same as file size
|
||||
assertEquals(9281L, length); // Sanity check: same as file size
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -141,13 +138,10 @@ public class JPEGSegmentImageInputStreamTest {
|
||||
ImageInputStream stream = new JPEGSegmentImageInputStream(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/jfif-padded-segments.jpg")));
|
||||
|
||||
List<JPEGSegment> appSegments = JPEGSegmentUtil.readSegments(stream, JPEGSegmentUtil.APP_SEGMENTS);
|
||||
assertEquals(2, appSegments.size());
|
||||
assertEquals(1, appSegments.size());
|
||||
|
||||
assertEquals(JPEG.APP0, appSegments.get(0).marker());
|
||||
assertEquals("JFIF", appSegments.get(0).identifier());
|
||||
|
||||
assertEquals(JPEG.APP1, appSegments.get(1).marker());
|
||||
assertEquals("Exif", appSegments.get(1).identifier());
|
||||
assertEquals(JPEG.APP1, appSegments.get(0).marker());
|
||||
assertEquals("Exif", appSegments.get(0).identifier());
|
||||
|
||||
stream.seek(0l);
|
||||
|
||||
@@ -156,6 +150,6 @@ public class JPEGSegmentImageInputStreamTest {
|
||||
length++;
|
||||
}
|
||||
|
||||
assertEquals(1079L, length); // Sanity check: same as file size, except padding and the filtered ICC_PROFILE segment
|
||||
assertEquals(1061L, length); // Sanity check: same as file size, except padding and the filtered ICC_PROFILE segment
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 52 KiB |
BIN
imageio/imageio-jpeg/src/test/resources/jpeg/no-jfif-ycbcr.jpg
Normal file
BIN
imageio/imageio-jpeg/src/test/resources/jpeg/no-jfif-ycbcr.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 228 KiB |
@@ -44,7 +44,7 @@ import java.util.Arrays;
|
||||
public abstract class AbstractEntry implements Entry {
|
||||
|
||||
private final Object identifier;
|
||||
private final Object value; // TODO: Might need to be mutable..
|
||||
private final Object value; // Entries are immutable, directories can be mutated
|
||||
|
||||
protected AbstractEntry(final Object identifier, final Object value) {
|
||||
Validate.notNull(identifier, "identifier");
|
||||
@@ -181,10 +181,10 @@ public abstract class AbstractEntry implements Entry {
|
||||
@Override
|
||||
public String toString() {
|
||||
String name = getFieldName();
|
||||
String nameStr = name != null ? "/" + name + "" : "";
|
||||
String nameStr = name != null ? String.format("/%s", name) : "";
|
||||
|
||||
String type = getTypeName();
|
||||
String typeStr = type != null ? " (" + type + ")" : "";
|
||||
String typeStr = type != null ? String.format(" (%s)", type) : "";
|
||||
|
||||
return String.format("%s%s: %s%s", getNativeIdentifier(), nameStr, getValueAsString(), typeStr);
|
||||
}
|
||||
|
||||
@@ -46,6 +46,8 @@ final class EXIFEntry extends AbstractEntry {
|
||||
if (type < 1 || type > TIFF.TYPE_NAMES.length) {
|
||||
throw new IllegalArgumentException(String.format("Illegal EXIF type: %s", type));
|
||||
}
|
||||
|
||||
// TODO: Validate that type is applicable to value?
|
||||
|
||||
this.type = type;
|
||||
}
|
||||
@@ -92,6 +94,8 @@ final class EXIFEntry extends AbstractEntry {
|
||||
return "SamplesPerPixels";
|
||||
case TIFF.TAG_ROWS_PER_STRIP:
|
||||
return "RowsPerStrip";
|
||||
case TIFF.TAG_STRIP_BYTE_COUNTS:
|
||||
return "StripByteCounts";
|
||||
case TIFF.TAG_X_RESOLUTION:
|
||||
return "XResolution";
|
||||
case TIFF.TAG_Y_RESOLUTION:
|
||||
|
||||
@@ -110,7 +110,6 @@ public final class EXIFReader extends MetadataReader {
|
||||
|
||||
// Read linked IFDs
|
||||
if (nextOffset != 0) {
|
||||
// TODO: This is probably not okay anymore.. Replace recursion with while loop
|
||||
AbstractCompoundDirectory next = (AbstractCompoundDirectory) readDirectory(pInput, nextOffset);
|
||||
for (int i = 0; i < next.directoryCount(); i++) {
|
||||
ifds.add((IFD) next.getDirectory(i));
|
||||
@@ -298,7 +297,7 @@ public final class EXIFReader extends MetadataReader {
|
||||
long pos = pInput.getStreamPosition();
|
||||
|
||||
switch (pType) {
|
||||
case 2: // ASCII
|
||||
case TIFF.TYPE_ASCII:
|
||||
// TODO: This might be UTF-8 or ISO-8859-x, even though spec says NULL-terminated 7 bit ASCII
|
||||
// TODO: Fail if unknown chars, try parsing with ISO-8859-1 or file.encoding
|
||||
if (pCount == 0) {
|
||||
@@ -308,17 +307,17 @@ public final class EXIFReader extends MetadataReader {
|
||||
pInput.readFully(ascii);
|
||||
int len = ascii[ascii.length - 1] == 0 ? ascii.length - 1 : ascii.length;
|
||||
return StringUtil.decode(ascii, 0, len, "UTF-8"); // UTF-8 is ASCII compatible
|
||||
case 1: // BYTE
|
||||
case TIFF.TYPE_BYTE:
|
||||
if (pCount == 1) {
|
||||
return pInput.readUnsignedByte();
|
||||
}
|
||||
// else fall through
|
||||
case 6: // SBYTE
|
||||
case TIFF.TYPE_SBYTE:
|
||||
if (pCount == 1) {
|
||||
return pInput.readByte();
|
||||
}
|
||||
// else fall through
|
||||
case 7: // UNDEFINED
|
||||
case TIFF.TYPE_UNDEFINED:
|
||||
byte[] bytes = new byte[pCount];
|
||||
pInput.readFully(bytes);
|
||||
|
||||
@@ -326,11 +325,11 @@ public final class EXIFReader extends MetadataReader {
|
||||
// binary data and we want to keep that as a byte array for clients to parse futher
|
||||
|
||||
return bytes;
|
||||
case 3: // SHORT
|
||||
case TIFF.TYPE_SHORT:
|
||||
if (pCount == 1) {
|
||||
return pInput.readUnsignedShort();
|
||||
}
|
||||
case 8: // SSHORT
|
||||
case TIFF.TYPE_SSHORT:
|
||||
if (pCount == 1) {
|
||||
return pInput.readShort();
|
||||
}
|
||||
@@ -338,21 +337,22 @@ public final class EXIFReader extends MetadataReader {
|
||||
short[] shorts = new short[pCount];
|
||||
pInput.readFully(shorts, 0, shorts.length);
|
||||
|
||||
if (pType == 3) {
|
||||
if (pType == TIFF.TYPE_SHORT) {
|
||||
int[] ints = new int[pCount];
|
||||
for (int i = 0; i < pCount; i++) {
|
||||
ints[i] = shorts[i] & 0xffff;
|
||||
}
|
||||
|
||||
return ints;
|
||||
}
|
||||
|
||||
return shorts;
|
||||
case 13: // IFD
|
||||
case 4: // LONG
|
||||
case TIFF.TYPE_IFD:
|
||||
case TIFF.TYPE_LONG:
|
||||
if (pCount == 1) {
|
||||
return pInput.readUnsignedInt();
|
||||
}
|
||||
case 9: // SLONG
|
||||
case TIFF.TYPE_SLONG:
|
||||
if (pCount == 1) {
|
||||
return pInput.readInt();
|
||||
}
|
||||
@@ -360,16 +360,17 @@ public final class EXIFReader extends MetadataReader {
|
||||
int[] ints = new int[pCount];
|
||||
pInput.readFully(ints, 0, ints.length);
|
||||
|
||||
if (pType == 4 || pType == 13) {
|
||||
if (pType == TIFF.TYPE_LONG || pType == TIFF.TYPE_IFD) {
|
||||
long[] longs = new long[pCount];
|
||||
for (int i = 0; i < pCount; i++) {
|
||||
longs[i] = ints[i] & 0xffffffffL;
|
||||
}
|
||||
|
||||
return longs;
|
||||
}
|
||||
|
||||
return ints;
|
||||
case 11: // FLOAT
|
||||
case TIFF.TYPE_FLOAT:
|
||||
if (pCount == 1) {
|
||||
return pInput.readFloat();
|
||||
}
|
||||
@@ -377,7 +378,7 @@ public final class EXIFReader extends MetadataReader {
|
||||
float[] floats = new float[pCount];
|
||||
pInput.readFully(floats, 0, floats.length);
|
||||
return floats;
|
||||
case 12: // DOUBLE
|
||||
case TIFF.TYPE_DOUBLE:
|
||||
if (pCount == 1) {
|
||||
return pInput.readDouble();
|
||||
}
|
||||
@@ -386,7 +387,7 @@ public final class EXIFReader extends MetadataReader {
|
||||
pInput.readFully(doubles, 0, doubles.length);
|
||||
return doubles;
|
||||
|
||||
case 5: // RATIONAL
|
||||
case TIFF.TYPE_RATIONAL:
|
||||
if (pCount == 1) {
|
||||
return createSafeRational(pInput.readUnsignedInt(), pInput.readUnsignedInt());
|
||||
}
|
||||
@@ -397,7 +398,7 @@ public final class EXIFReader extends MetadataReader {
|
||||
}
|
||||
|
||||
return rationals;
|
||||
case 10: // SRATIONAL
|
||||
case TIFF.TYPE_SRATIONAL:
|
||||
if (pCount == 1) {
|
||||
return createSafeRational(pInput.readInt(), pInput.readInt());
|
||||
}
|
||||
@@ -445,7 +446,7 @@ public final class EXIFReader extends MetadataReader {
|
||||
return new Rational(numerator, denominator);
|
||||
}
|
||||
|
||||
private int getValueLength(final int pType, final int pCount) {
|
||||
static int getValueLength(final int pType, final int pCount) {
|
||||
if (pType > 0 && pType <= TIFF.TYPE_LENGTHS.length) {
|
||||
return TIFF.TYPE_LENGTHS[pType - 1] * pCount;
|
||||
}
|
||||
|
||||
@@ -37,8 +37,25 @@ package com.twelvemonkeys.imageio.metadata.exif;
|
||||
*/
|
||||
@SuppressWarnings("UnusedDeclaration")
|
||||
public interface TIFF {
|
||||
short BYTE_ORDER_MARK_BIG_ENDIAN = ('M' << 8) | 'M';
|
||||
short BYTE_ORDER_MARK_LITTLE_ENDIAN = ('I' << 8) | 'I';
|
||||
|
||||
int TIFF_MAGIC = 42;
|
||||
|
||||
short TYPE_BYTE = 1;
|
||||
short TYPE_ASCII = 2;
|
||||
short TYPE_SHORT = 3;
|
||||
short TYPE_LONG = 4;
|
||||
short TYPE_RATIONAL = 5;
|
||||
|
||||
short TYPE_SBYTE = 6;
|
||||
short TYPE_UNDEFINED = 7;
|
||||
short TYPE_SSHORT = 8;
|
||||
short TYPE_SLONG = 9;
|
||||
short TYPE_SRATIONAL = 10;
|
||||
short TYPE_FLOAT = 11;
|
||||
short TYPE_DOUBLE = 12;
|
||||
short TYPE_IFD = 13;
|
||||
/*
|
||||
1 = BYTE 8-bit unsigned integer.
|
||||
2 = ASCII 8-bit byte that contains a 7-bit ASCII code; the last byte
|
||||
|
||||
@@ -77,6 +77,8 @@ public final class JPEGSegment implements Serializable {
|
||||
return marker >= 0xFFE0 && marker <= 0xFFEF;
|
||||
}
|
||||
|
||||
// TODO: Consider returning an ImageInputStream and use ByteArrayImageInputStream directly, for less wrapping and better performance
|
||||
// TODO: BUT: Must find a way to skip padding in/after segment identifier (eg: Exif has null-term + null-pad, ICC_PROFILE has only null-term). Is data always word-aligned?
|
||||
public InputStream data() {
|
||||
return data != null ? new ByteArrayInputStream(data, offset(), length()) : null;
|
||||
}
|
||||
@@ -85,7 +87,7 @@ public final class JPEGSegment implements Serializable {
|
||||
return data != null ? data.length - offset() : 0;
|
||||
}
|
||||
|
||||
private int offset() {
|
||||
int offset() {
|
||||
String identifier = identifier();
|
||||
|
||||
return identifier == null ? 0 : identifier.length() + 1;
|
||||
|
||||
@@ -33,6 +33,7 @@ import com.twelvemonkeys.imageio.metadata.exif.EXIFReader;
|
||||
import com.twelvemonkeys.imageio.metadata.psd.PSDReader;
|
||||
import com.twelvemonkeys.imageio.metadata.xmp.XMP;
|
||||
import com.twelvemonkeys.imageio.metadata.xmp.XMPReader;
|
||||
import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream;
|
||||
|
||||
import javax.imageio.IIOException;
|
||||
import javax.imageio.ImageIO;
|
||||
@@ -156,6 +157,22 @@ public final class JPEGSegmentUtil {
|
||||
static JPEGSegment readSegment(final ImageInputStream stream, final Map<Integer, List<String>> segmentIdentifiers) throws IOException {
|
||||
int marker = stream.readUnsignedShort();
|
||||
|
||||
// Skip over weird 0x00 padding...?
|
||||
int bad = 0;
|
||||
while (marker == 0) {
|
||||
marker = stream.readUnsignedShort();
|
||||
bad += 2;
|
||||
}
|
||||
|
||||
if (marker == 0x00ff) {
|
||||
bad++;
|
||||
marker = 0xff00 | stream.readUnsignedByte();
|
||||
}
|
||||
|
||||
if (bad != 0) {
|
||||
// System.err.println("bad: " + bad);
|
||||
}
|
||||
|
||||
// Skip over 0xff padding between markers
|
||||
while (marker == 0xffff) {
|
||||
marker = 0xff00 | stream.readUnsignedByte();
|
||||
@@ -245,38 +262,48 @@ public final class JPEGSegmentUtil {
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
List<JPEGSegment> segments = readSegments(ImageIO.createImageInputStream(new File(args[0])), ALL_SEGMENTS);
|
||||
|
||||
for (JPEGSegment segment : segments) {
|
||||
System.err.println("segment: " + segment);
|
||||
|
||||
if ("Exif".equals(segment.identifier())) {
|
||||
InputStream data = segment.data();
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
data.read(); // Pad
|
||||
|
||||
ImageInputStream stream = ImageIO.createImageInputStream(data);
|
||||
|
||||
// Root entry is TIFF, that contains the EXIF sub-IFD
|
||||
Directory tiff = new EXIFReader().read(stream);
|
||||
System.err.println("EXIF: " + tiff);
|
||||
for (String arg : args) {
|
||||
if (args.length > 1) {
|
||||
System.out.println("File: " + arg);
|
||||
System.out.println("------");
|
||||
}
|
||||
else if (XMP.NS_XAP.equals(segment.identifier())) {
|
||||
Directory xmp = new XMPReader().read(ImageIO.createImageInputStream(segment.data()));
|
||||
System.err.println("XMP: " + xmp);
|
||||
|
||||
List<JPEGSegment> segments = readSegments(ImageIO.createImageInputStream(new File(arg)), ALL_SEGMENTS);
|
||||
|
||||
for (JPEGSegment segment : segments) {
|
||||
System.err.println("segment: " + segment);
|
||||
|
||||
if ("Exif".equals(segment.identifier())) {
|
||||
ImageInputStream stream = new ByteArrayImageInputStream(segment.data, segment.offset() + 1, segment.length() - 1);
|
||||
|
||||
// Root entry is TIFF, that contains the EXIF sub-IFD
|
||||
Directory tiff = new EXIFReader().read(stream);
|
||||
System.err.println("EXIF: " + tiff);
|
||||
}
|
||||
else if (XMP.NS_XAP.equals(segment.identifier())) {
|
||||
Directory xmp = new XMPReader().read(new ByteArrayImageInputStream(segment.data, segment.offset(), segment.length()));
|
||||
System.err.println("XMP: " + xmp);
|
||||
System.err.println(EXIFReader.HexDump.dump(segment.data));
|
||||
}
|
||||
else if ("Photoshop 3.0".equals(segment.identifier())) {
|
||||
// TODO: The "Photoshop 3.0" segment contains several image resources, of which one might contain
|
||||
// IPTC metadata. Probably duplicated in the XMP though...
|
||||
ImageInputStream stream = new ByteArrayImageInputStream(segment.data, segment.offset(), segment.length());
|
||||
Directory psd = new PSDReader().read(stream);
|
||||
System.err.println("PSD: " + psd);
|
||||
System.err.println(EXIFReader.HexDump.dump(segment.data));
|
||||
}
|
||||
else if ("ICC_PROFILE".equals(segment.identifier())) {
|
||||
// Skip
|
||||
}
|
||||
else {
|
||||
System.err.println(EXIFReader.HexDump.dump(segment.data));
|
||||
}
|
||||
}
|
||||
else if ("Photoshop 3.0".equals(segment.identifier())) {
|
||||
// TODO: The "Photoshop 3.0" segment contains several image resources, of which one might contain
|
||||
// IPTC metadata. Probably duplicated in the XMP though...
|
||||
ImageInputStream stream = ImageIO.createImageInputStream(segment.data());
|
||||
Directory psd = new PSDReader().read(stream);
|
||||
System.err.println("PSD: " + psd);
|
||||
}
|
||||
else if ("ICC_PROFILE".equals(segment.identifier())) {
|
||||
// Skip
|
||||
}
|
||||
else {
|
||||
System.err.println(EXIFReader.HexDump.dump(segment.data));
|
||||
|
||||
if (args.length > 1) {
|
||||
System.out.println("------");
|
||||
System.out.println();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ package com.twelvemonkeys.imageio.metadata.psd;
|
||||
* @author last modified by $Author: haraldk$
|
||||
* @version $Id: PSD.java,v 1.0 24.01.12 16:51 haraldk Exp$
|
||||
*/
|
||||
interface PSD {
|
||||
public interface PSD {
|
||||
static final int RESOURCE_TYPE = ('8' << 24) + ('B' << 16) + ('I' << 8) + 'M';
|
||||
|
||||
static final int RES_IPTC_NAA = 0x0404;
|
||||
|
||||
@@ -128,20 +128,8 @@ public final class XMPReader extends MetadataReader {
|
||||
|
||||
Object value;
|
||||
|
||||
Node parseType = node.getAttributes().getNamedItemNS(XMP.NS_RDF, "parseType");
|
||||
if (parseType != null && "Resource".equals(parseType.getNodeValue())) {
|
||||
// See: http://www.w3.org/TR/REC-rdf-syntax/#section-Syntax-parsetype-resource
|
||||
List<Entry> entries = new ArrayList<Entry>();
|
||||
|
||||
for (Node child : asIterable(node.getChildNodes())) {
|
||||
if (child.getNodeType() != Node.ELEMENT_NODE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
entries.add(new XMPEntry(child.getNamespaceURI() + child.getLocalName(), child.getLocalName(), getChildTextValue(child)));
|
||||
}
|
||||
|
||||
value = new RDFDescription(entries);
|
||||
if (isResourceType(node)) {
|
||||
value = parseAsResource(node);
|
||||
}
|
||||
else {
|
||||
// TODO: This method contains loads of duplication an should be cleaned up...
|
||||
@@ -178,6 +166,27 @@ public final class XMPReader extends MetadataReader {
|
||||
return new XMPDirectory(entries, toolkit);
|
||||
}
|
||||
|
||||
private boolean isResourceType(Node node) {
|
||||
Node parseType = node.getAttributes().getNamedItemNS(XMP.NS_RDF, "parseType");
|
||||
|
||||
return parseType != null && "Resource".equals(parseType.getNodeValue());
|
||||
}
|
||||
|
||||
private RDFDescription parseAsResource(Node node) {
|
||||
// See: http://www.w3.org/TR/REC-rdf-syntax/#section-Syntax-parsetype-resource
|
||||
List<Entry> entries = new ArrayList<Entry>();
|
||||
|
||||
for (Node child : asIterable(node.getChildNodes())) {
|
||||
if (child.getNodeType() != Node.ELEMENT_NODE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
entries.add(new XMPEntry(child.getNamespaceURI() + child.getLocalName(), child.getLocalName(), getChildTextValue(child)));
|
||||
}
|
||||
|
||||
return new RDFDescription(entries);
|
||||
}
|
||||
|
||||
private void parseAttributesForKnownElements(Map<String, List<Entry>> subdirs, Node desc) {
|
||||
// NOTE: NamedNodeMap does not have any particular order...
|
||||
NamedNodeMap attributes = desc.getAttributes();
|
||||
@@ -201,15 +210,13 @@ public final class XMPReader extends MetadataReader {
|
||||
private Object getChildTextValue(final Node node) {
|
||||
for (Node child : asIterable(node.getChildNodes())) {
|
||||
if (XMP.NS_RDF.equals(child.getNamespaceURI()) && "Alt".equals(child.getLocalName())) {
|
||||
// Support for <rdf:Alt><rdf:li> -> return a Map<String, Object> (keyed on xml:lang?)
|
||||
// Support for <rdf:Alt><rdf:li> -> return a Map<String, Object> keyed on xml:lang
|
||||
Map<String, Object> alternatives = new LinkedHashMap<String, Object>();
|
||||
for (Node alternative : asIterable(child.getChildNodes())) {
|
||||
if (XMP.NS_RDF.equals(alternative.getNamespaceURI()) && "li".equals(alternative.getLocalName())) {
|
||||
//return getChildTextValue(alternative);
|
||||
NamedNodeMap attributes = alternative.getAttributes();
|
||||
Node key = attributes.getNamedItem("xml:lang");
|
||||
|
||||
alternatives.put(key.getTextContent(), getChildTextValue(alternative));
|
||||
alternatives.put(key == null ? null : key.getTextContent(), getChildTextValue(alternative));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,9 +242,13 @@ public final class XMPReader extends MetadataReader {
|
||||
}
|
||||
}
|
||||
|
||||
// Need to support rdf:parseType="Resource" here as well...
|
||||
if (isResourceType(node)) {
|
||||
return parseAsResource(node);
|
||||
}
|
||||
|
||||
Node child = node.getFirstChild();
|
||||
String strVal = child != null ? child.getNodeValue() : null;
|
||||
|
||||
return strVal != null ? strVal.trim() : "";
|
||||
}
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ public class PICTImageReaderSpi extends ImageReaderSpi {
|
||||
new String[]{"pct", "pict"},
|
||||
new String[]{"image/pict", "image/x-pict"},
|
||||
"com.twelvemkonkeys.imageio.plugins.pict.PICTImageReader",
|
||||
STANDARD_INPUT_TYPE,
|
||||
new Class[] {ImageInputStream.class},
|
||||
new String[]{"com.twelvemkonkeys.imageio.plugins.pict.PICTImageWriterSpi"},
|
||||
true, null, null, null, null,
|
||||
true, null, null, null, null
|
||||
|
||||
@@ -64,7 +64,7 @@ public class PSDImageReaderSpi extends ImageReaderSpi {
|
||||
"image/x-psd", "application/x-photoshop", "image/x-photoshop"
|
||||
},
|
||||
"com.twelvemkonkeys.imageio.plugins.psd.PSDImageReader",
|
||||
STANDARD_INPUT_TYPE,
|
||||
new Class[] {ImageInputStream.class},
|
||||
// new String[]{"com.twelvemkonkeys.imageio.plugins.psd.PSDImageWriterSpi"},
|
||||
null,
|
||||
true, // supports standard stream metadata
|
||||
|
||||
@@ -27,6 +27,7 @@ public final class PSDMetadata extends AbstractMetadata {
|
||||
// TODO: Decide on image/stream metadata...
|
||||
static final String NATIVE_METADATA_FORMAT_NAME = "com_twelvemonkeys_imageio_psd_image_1.0";
|
||||
static final String NATIVE_METADATA_FORMAT_CLASS_NAME = "com.twelvemonkeys.imageio.plugins.psd.PSDMetadataFormat";
|
||||
// TODO: Support TIFF metadata, based on EXIF/XMP + merge in PSD specifics
|
||||
|
||||
PSDHeader header;
|
||||
PSDColorData colorData;
|
||||
@@ -626,7 +627,6 @@ public final class PSDMetadata extends AbstractMetadata {
|
||||
}
|
||||
|
||||
IIOMetadataNode text = new IIOMetadataNode("Text");
|
||||
IIOMetadataNode node;
|
||||
|
||||
// TODO: Alpha channel names? (PSDAlphaChannelInfo/PSDUnicodeAlphaNames)
|
||||
// TODO: Reader/writer (PSDVersionInfo)
|
||||
@@ -691,7 +691,7 @@ public final class PSDMetadata extends AbstractMetadata {
|
||||
String fieldName = entry.getFieldName();
|
||||
|
||||
if (fieldName != null) {
|
||||
tag.setAttribute("keyword", String.format("%s", fieldName));
|
||||
tag.setAttribute("keyword", fieldName);
|
||||
}
|
||||
else {
|
||||
// TODO: This should never happen, as we filter out only specific nodes
|
||||
@@ -722,8 +722,8 @@ public final class PSDMetadata extends AbstractMetadata {
|
||||
}
|
||||
|
||||
private boolean hasAlpha() {
|
||||
return header.mode == PSD.COLOR_MODE_RGB && header.channels >= 4 ||
|
||||
header.mode == PSD.COLOR_MODE_CMYK & header.channels >= 5;
|
||||
return header.mode == PSD.COLOR_MODE_RGB && header.channels > 3 ||
|
||||
header.mode == PSD.COLOR_MODE_CMYK & header.channels > 4;
|
||||
}
|
||||
|
||||
<T extends PSDImageResource> Iterator<T> getResources(final Class<T> resourceType) {
|
||||
|
||||
@@ -65,8 +65,8 @@ public class ThumbsDBImageReaderSpi extends ImageReaderSpi {
|
||||
new String[]{"thumbs", "THUMBS", "Thumbs DB"},
|
||||
new String[]{"db"},
|
||||
new String[]{"image/x-thumbs-db", "application/octet-stream"}, // TODO: Check IANA et al...
|
||||
ThumbsDBImageReader.class.getName(),
|
||||
STANDARD_INPUT_TYPE,
|
||||
"com.twelvemonkeys.imageio.plugins.thumbsdb.ThumbsDBImageReader",
|
||||
new Class[] {ImageInputStream.class},
|
||||
null,
|
||||
true, null, null, null, null,
|
||||
true, null, null, null, null
|
||||
|
||||
@@ -31,10 +31,12 @@ package com.twelvemonkeys.imageio.plugins.tiff;
|
||||
import com.twelvemonkeys.lang.Validate;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.io.FilterInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.channels.Channels;
|
||||
import java.nio.channels.ReadableByteChannel;
|
||||
|
||||
/**
|
||||
* A decoder for data converted using "horizontal differencing predictor".
|
||||
@@ -43,29 +45,26 @@ import java.nio.ByteOrder;
|
||||
* @author last modified by $Author: haraldk$
|
||||
* @version $Id: HorizontalDeDifferencingStream.java,v 1.0 11.03.13 14:20 haraldk Exp$
|
||||
*/
|
||||
final class HorizontalDeDifferencingStream extends FilterInputStream {
|
||||
final class HorizontalDeDifferencingStream extends InputStream {
|
||||
// See TIFF 6.0 Specification, Section 14: "Differencing Predictor", page 64.
|
||||
|
||||
private final int columns;
|
||||
// NOTE: PlanarConfiguration == 2 may be treated as samplesPerPixel == 1
|
||||
private final int samplesPerPixel;
|
||||
private final int bitsPerSample;
|
||||
private final ByteOrder byteOrder;
|
||||
|
||||
int decodedLength;
|
||||
int decodedPos;
|
||||
|
||||
private final byte[] buffer;
|
||||
private final ReadableByteChannel channel;
|
||||
private final ByteBuffer buffer;
|
||||
|
||||
public HorizontalDeDifferencingStream(final InputStream stream, final int columns, final int samplesPerPixel, final int bitsPerSample, final ByteOrder byteOrder) {
|
||||
super(Validate.notNull(stream, "stream"));
|
||||
channel = Channels.newChannel(Validate.notNull(stream, "stream"));
|
||||
|
||||
this.columns = Validate.isTrue(columns > 0, columns, "width must be greater than 0");
|
||||
this.samplesPerPixel = Validate.isTrue(bitsPerSample >= 8 || samplesPerPixel == 1, samplesPerPixel, "Unsupported samples per pixel for < 8 bit samples: %s");
|
||||
this.bitsPerSample = Validate.isTrue(isValidBPS(bitsPerSample), bitsPerSample, "Unsupported bits per sample value: %s");
|
||||
this.byteOrder = byteOrder;
|
||||
|
||||
buffer = new byte[(columns * samplesPerPixel * bitsPerSample + 7) / 8];
|
||||
buffer = ByteBuffer.allocate((columns * samplesPerPixel * bitsPerSample + 7) / 8).order(byteOrder);
|
||||
buffer.flip();
|
||||
}
|
||||
|
||||
private boolean isValidBPS(final int bitsPerSample) {
|
||||
@@ -83,75 +82,81 @@ final class HorizontalDeDifferencingStream extends FilterInputStream {
|
||||
}
|
||||
}
|
||||
|
||||
private void fetch() throws IOException {
|
||||
int pos = 0;
|
||||
int read;
|
||||
@SuppressWarnings("StatementWithEmptyBody")
|
||||
private boolean fetch() throws IOException {
|
||||
buffer.clear();
|
||||
|
||||
// This *SHOULD* read an entire row of pixels (or nothing at all) into the buffer, otherwise we will throw EOFException below
|
||||
while (pos < buffer.length && (read = in.read(buffer, pos, buffer.length - pos)) > 0) {
|
||||
pos += read;
|
||||
}
|
||||
// This *SHOULD* read an entire row of pixels (or nothing at all) into the buffer,
|
||||
// otherwise we will throw EOFException below
|
||||
while (channel.read(buffer) > 0);
|
||||
|
||||
if (pos > 0) {
|
||||
if (buffer.length > pos) {
|
||||
if (buffer.position() > 0) {
|
||||
if (buffer.hasRemaining()) {
|
||||
throw new EOFException("Unexpected end of stream");
|
||||
}
|
||||
|
||||
decodeRow();
|
||||
buffer.flip();
|
||||
|
||||
decodedLength = buffer.length;
|
||||
decodedPos = 0;
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
decodedLength = -1;
|
||||
buffer.position(buffer.capacity());
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void decodeRow() throws EOFException {
|
||||
// Un-apply horizontal predictor
|
||||
byte original;
|
||||
int sample = 0;
|
||||
byte temp;
|
||||
|
||||
switch (bitsPerSample) {
|
||||
case 1:
|
||||
for (int b = 0; b < (columns + 7) / 8; b++) {
|
||||
sample += (buffer[b] >> 7) & 0x1;
|
||||
original = buffer.get(b);
|
||||
sample += (original >> 7) & 0x1;
|
||||
temp = (byte) ((sample << 7) & 0x80);
|
||||
sample += (buffer[b] >> 6) & 0x1;
|
||||
sample += (original >> 6) & 0x1;
|
||||
temp |= (byte) ((sample << 6) & 0x40);
|
||||
sample += (buffer[b] >> 5) & 0x1;
|
||||
sample += (original >> 5) & 0x1;
|
||||
temp |= (byte) ((sample << 5) & 0x20);
|
||||
sample += (buffer[b] >> 4) & 0x1;
|
||||
sample += (original >> 4) & 0x1;
|
||||
temp |= (byte) ((sample << 4) & 0x10);
|
||||
sample += (buffer[b] >> 3) & 0x1;
|
||||
sample += (original >> 3) & 0x1;
|
||||
temp |= (byte) ((sample << 3) & 0x08);
|
||||
sample += (buffer[b] >> 2) & 0x1;
|
||||
sample += (original >> 2) & 0x1;
|
||||
temp |= (byte) ((sample << 2) & 0x04);
|
||||
sample += (buffer[b] >> 1) & 0x1;
|
||||
sample += (original >> 1) & 0x1;
|
||||
temp |= (byte) ((sample << 1) & 0x02);
|
||||
sample += buffer[b] & 0x1;
|
||||
buffer[b] = (byte) (temp | sample & 0x1);
|
||||
sample += original & 0x1;
|
||||
buffer.put(b, (byte) (temp | sample & 0x1));
|
||||
}
|
||||
break;
|
||||
|
||||
case 2:
|
||||
for (int b = 0; b < (columns + 3) / 4; b++) {
|
||||
sample += (buffer[b] >> 6) & 0x3;
|
||||
original = buffer.get(b);
|
||||
sample += (original >> 6) & 0x3;
|
||||
temp = (byte) ((sample << 6) & 0xc0);
|
||||
sample += (buffer[b] >> 4) & 0x3;
|
||||
sample += (original >> 4) & 0x3;
|
||||
temp |= (byte) ((sample << 4) & 0x30);
|
||||
sample += (buffer[b] >> 2) & 0x3;
|
||||
sample += (original >> 2) & 0x3;
|
||||
temp |= (byte) ((sample << 2) & 0x0c);
|
||||
sample += buffer[b] & 0x3;
|
||||
buffer[b] = (byte) (temp | sample & 0x3);
|
||||
sample += original & 0x3;
|
||||
buffer.put(b, (byte) (temp | sample & 0x3));
|
||||
}
|
||||
break;
|
||||
|
||||
case 4:
|
||||
for (int b = 0; b < (columns + 1) / 2; b++) {
|
||||
sample += (buffer[b] >> 4) & 0xf;
|
||||
original = buffer.get(b);
|
||||
sample += (original >> 4) & 0xf;
|
||||
temp = (byte) ((sample << 4) & 0xf0);
|
||||
sample += buffer[b] & 0x0f;
|
||||
buffer[b] = (byte) (temp | sample & 0xf);
|
||||
sample += original & 0x0f;
|
||||
buffer.put(b, (byte) (temp | sample & 0xf));
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -159,7 +164,7 @@ final class HorizontalDeDifferencingStream extends FilterInputStream {
|
||||
for (int x = 1; x < columns; x++) {
|
||||
for (int b = 0; b < samplesPerPixel; b++) {
|
||||
int off = x * samplesPerPixel + b;
|
||||
buffer[off] = (byte) (buffer[off - samplesPerPixel] + buffer[off]);
|
||||
buffer.put(off, (byte) (buffer.get(off - samplesPerPixel) + buffer.get(off)));
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -168,7 +173,7 @@ final class HorizontalDeDifferencingStream extends FilterInputStream {
|
||||
for (int x = 1; x < columns; x++) {
|
||||
for (int b = 0; b < samplesPerPixel; b++) {
|
||||
int off = x * samplesPerPixel + b;
|
||||
putShort(off, asShort(off - samplesPerPixel) + asShort(off));
|
||||
buffer.putShort(2 * off, (short) (buffer.getShort(2 * (off - samplesPerPixel)) + buffer.getShort(2 * off)));
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -177,7 +182,7 @@ final class HorizontalDeDifferencingStream extends FilterInputStream {
|
||||
for (int x = 1; x < columns; x++) {
|
||||
for (int b = 0; b < samplesPerPixel; b++) {
|
||||
int off = x * samplesPerPixel + b;
|
||||
putInt(off, asInt(off - samplesPerPixel) + asInt(off));
|
||||
buffer.putInt(4 * off, buffer.getInt(4 * (off - samplesPerPixel)) + buffer.getInt(4 * off));
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -186,7 +191,7 @@ final class HorizontalDeDifferencingStream extends FilterInputStream {
|
||||
for (int x = 1; x < columns; x++) {
|
||||
for (int b = 0; b < samplesPerPixel; b++) {
|
||||
int off = x * samplesPerPixel + b;
|
||||
putLong(off, asLong(off - samplesPerPixel) + asLong(off));
|
||||
buffer.putLong(8 * off, buffer.getLong(8 * (off - samplesPerPixel)) + buffer.getLong(8 * off));
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -196,145 +201,58 @@ final class HorizontalDeDifferencingStream extends FilterInputStream {
|
||||
}
|
||||
}
|
||||
|
||||
private void putLong(final int index, final long value) {
|
||||
if (byteOrder == ByteOrder.BIG_ENDIAN) {
|
||||
buffer[index * 8 ] = (byte) ((value >> 56) & 0xff);
|
||||
buffer[index * 8 + 1] = (byte) ((value >> 48) & 0xff);
|
||||
buffer[index * 8 + 2] = (byte) ((value >> 40) & 0xff);
|
||||
buffer[index * 8 + 3] = (byte) ((value >> 32) & 0xff);
|
||||
buffer[index * 8 + 4] = (byte) ((value >> 24) & 0xff);
|
||||
buffer[index * 8 + 5] = (byte) ((value >> 16) & 0xff);
|
||||
buffer[index * 8 + 6] = (byte) ((value >> 8) & 0xff);
|
||||
buffer[index * 8 + 7] = (byte) ((value) & 0xff);
|
||||
}
|
||||
else {
|
||||
buffer[index * 8 + 7] = (byte) ((value >> 56) & 0xff);
|
||||
buffer[index * 8 + 6] = (byte) ((value >> 48) & 0xff);
|
||||
buffer[index * 8 + 5] = (byte) ((value >> 40) & 0xff);
|
||||
buffer[index * 8 + 4] = (byte) ((value >> 32) & 0xff);
|
||||
buffer[index * 8 + 3] = (byte) ((value >> 24) & 0xff);
|
||||
buffer[index * 8 + 2] = (byte) ((value >> 16) & 0xff);
|
||||
buffer[index * 8 + 1] = (byte) ((value >> 8) & 0xff);
|
||||
buffer[index * 8 ] = (byte) ((value) & 0xff);
|
||||
}
|
||||
}
|
||||
|
||||
private long asLong(final int index) {
|
||||
if (byteOrder == ByteOrder.BIG_ENDIAN) {
|
||||
return (buffer[index * 8 ] & 0xffl) << 56l | (buffer[index * 8 + 1] & 0xffl) << 48l |
|
||||
(buffer[index * 8 + 2] & 0xffl) << 40l | (buffer[index * 8 + 3] & 0xffl) << 32l |
|
||||
(buffer[index * 8 + 4] & 0xffl) << 24 | (buffer[index * 8 + 5] & 0xffl) << 16 |
|
||||
(buffer[index * 8 + 6] & 0xffl) << 8 | buffer[index * 8 + 7] & 0xffl;
|
||||
}
|
||||
else {
|
||||
return (buffer[index * 8 + 7] & 0xffl) << 56l | (buffer[index * 8 + 6] & 0xffl) << 48l |
|
||||
(buffer[index * 8 + 5] & 0xffl) << 40l | (buffer[index * 8 + 4] & 0xffl) << 32l |
|
||||
(buffer[index * 8 + 3] & 0xffl) << 24 | (buffer[index * 8 + 2] & 0xffl) << 16 |
|
||||
(buffer[index * 8 + 1] & 0xffl) << 8 | buffer[index * 8] & 0xffl;
|
||||
}
|
||||
}
|
||||
|
||||
private void putInt(final int index, final int value) {
|
||||
if (byteOrder == ByteOrder.BIG_ENDIAN) {
|
||||
buffer[index * 4 ] = (byte) ((value >> 24) & 0xff);
|
||||
buffer[index * 4 + 1] = (byte) ((value >> 16) & 0xff);
|
||||
buffer[index * 4 + 2] = (byte) ((value >> 8) & 0xff);
|
||||
buffer[index * 4 + 3] = (byte) ((value) & 0xff);
|
||||
}
|
||||
else {
|
||||
buffer[index * 4 + 3] = (byte) ((value >> 24) & 0xff);
|
||||
buffer[index * 4 + 2] = (byte) ((value >> 16) & 0xff);
|
||||
buffer[index * 4 + 1] = (byte) ((value >> 8) & 0xff);
|
||||
buffer[index * 4 ] = (byte) ((value) & 0xff);
|
||||
}
|
||||
}
|
||||
|
||||
private int asInt(final int index) {
|
||||
if (byteOrder == ByteOrder.BIG_ENDIAN) {
|
||||
return (buffer[index * 4] & 0xff) << 24 | (buffer[index * 4 + 1] & 0xff) << 16 |
|
||||
(buffer[index * 4 + 2] & 0xff) << 8 | buffer[index * 4 + 3] & 0xff;
|
||||
}
|
||||
else {
|
||||
return (buffer[index * 4 + 3] & 0xff) << 24 | (buffer[index * 4 + 2] & 0xff) << 16 |
|
||||
(buffer[index * 4 + 1] & 0xff) << 8 | buffer[index * 4] & 0xff;
|
||||
}
|
||||
}
|
||||
|
||||
private void putShort(final int index, final int value) {
|
||||
if (byteOrder == ByteOrder.BIG_ENDIAN) {
|
||||
buffer[index * 2 ] = (byte) ((value >> 8) & 0xff);
|
||||
buffer[index * 2 + 1] = (byte) ((value) & 0xff);
|
||||
}
|
||||
else {
|
||||
buffer[index * 2 + 1] = (byte) ((value >> 8) & 0xff);
|
||||
buffer[index * 2 ] = (byte) ((value) & 0xff);
|
||||
}
|
||||
}
|
||||
|
||||
private short asShort(final int index) {
|
||||
if (byteOrder == ByteOrder.BIG_ENDIAN) {
|
||||
return (short) ((buffer[index * 2] & 0xff) << 8 | buffer[index * 2 + 1] & 0xff);
|
||||
}
|
||||
else {
|
||||
return (short) ((buffer[index * 2 + 1] & 0xff) << 8 | buffer[index * 2] & 0xff);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
if (decodedLength < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (decodedPos >= decodedLength) {
|
||||
fetch();
|
||||
|
||||
if (decodedLength < 0) {
|
||||
if (!buffer.hasRemaining()) {
|
||||
if (!fetch()) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
return buffer[decodedPos++] & 0xff;
|
||||
return buffer.get() & 0xff;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] b, int off, int len) throws IOException {
|
||||
if (decodedLength < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (decodedPos >= decodedLength) {
|
||||
fetch();
|
||||
|
||||
if (decodedLength < 0) {
|
||||
if (!buffer.hasRemaining()) {
|
||||
if (!fetch()) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
int read = Math.min(decodedLength - decodedPos, len);
|
||||
System.arraycopy(buffer, decodedPos, b, off, read);
|
||||
decodedPos += read;
|
||||
int read = Math.min(buffer.remaining(), len);
|
||||
buffer.get(b, off, read);
|
||||
|
||||
return read;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long skip(long n) throws IOException {
|
||||
if (decodedLength < 0) {
|
||||
return -1;
|
||||
if (n < 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (decodedPos >= decodedLength) {
|
||||
fetch();
|
||||
|
||||
if (decodedLength < 0) {
|
||||
return -1;
|
||||
if (!buffer.hasRemaining()) {
|
||||
if (!fetch()) {
|
||||
return 0; // SIC
|
||||
}
|
||||
}
|
||||
|
||||
int skipped = (int) Math.min(decodedLength - decodedPos, n);
|
||||
decodedPos += skipped;
|
||||
int skipped = (int) Math.min(buffer.remaining(), n);
|
||||
buffer.position(buffer.position() + skipped);
|
||||
|
||||
return skipped;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
try {
|
||||
super.close();
|
||||
}
|
||||
finally {
|
||||
if (channel.isOpen()) {
|
||||
channel.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ import com.twelvemonkeys.io.enc.Decoder;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* Lempel–Ziv–Welch (LZW) decompression. LZW is a universal loss-less data compression algorithm
|
||||
@@ -94,10 +95,9 @@ abstract class LZWDecoder implements Decoder {
|
||||
maxString = 1;
|
||||
}
|
||||
|
||||
public int decode(final InputStream stream, final byte[] buffer) throws IOException {
|
||||
public int decode(final InputStream stream, final ByteBuffer buffer) throws IOException {
|
||||
// Adapted from the pseudo-code example found in the TIFF 6.0 Specification, 1992.
|
||||
// See Section 13: "LZW Compression"/"LZW Decoding", page 61+
|
||||
int bufferPos = 0;
|
||||
int code;
|
||||
|
||||
while ((code = getNextCode(stream)) != EOI_CODE) {
|
||||
@@ -109,30 +109,30 @@ abstract class LZWDecoder implements Decoder {
|
||||
break;
|
||||
}
|
||||
|
||||
bufferPos += table[code].writeTo(buffer, bufferPos);
|
||||
table[code].writeTo(buffer);
|
||||
}
|
||||
else {
|
||||
if (isInTable(code)) {
|
||||
bufferPos += table[code].writeTo(buffer, bufferPos);
|
||||
table[code].writeTo(buffer);
|
||||
addStringToTable(table[oldCode].concatenate(table[code].firstChar));
|
||||
}
|
||||
else {
|
||||
String outString = table[oldCode].concatenate(table[oldCode].firstChar);
|
||||
|
||||
bufferPos += outString.writeTo(buffer, bufferPos);
|
||||
outString.writeTo(buffer);
|
||||
addStringToTable(outString);
|
||||
}
|
||||
}
|
||||
|
||||
oldCode = code;
|
||||
|
||||
if (bufferPos >= buffer.length - maxString - 1) {
|
||||
if (buffer.remaining() < maxString + 1) {
|
||||
// Buffer full, stop decoding for now
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return bufferPos;
|
||||
return buffer.position();
|
||||
}
|
||||
|
||||
private void addStringToTable(final String string) throws IOException {
|
||||
@@ -301,24 +301,24 @@ abstract class LZWDecoder implements Decoder {
|
||||
return new String(firstChar, this.firstChar, length + 1, this);
|
||||
}
|
||||
|
||||
public final int writeTo(final byte[] buffer, final int offset) {
|
||||
public final void writeTo(final ByteBuffer buffer) {
|
||||
if (length == 0) {
|
||||
return 0;
|
||||
return;
|
||||
}
|
||||
else if (length == 1) {
|
||||
buffer[offset] = value;
|
||||
|
||||
return 1;
|
||||
if (length == 1) {
|
||||
buffer.put(value);
|
||||
}
|
||||
else {
|
||||
String e = this;
|
||||
final int offset = buffer.position();
|
||||
|
||||
for (int i = length - 1; i >= 0; i--) {
|
||||
buffer[offset + i] = e.value;
|
||||
buffer.put(offset + i, e.value);
|
||||
e = e.previous;
|
||||
}
|
||||
|
||||
return length;
|
||||
buffer.position(offset + length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,9 +47,11 @@ 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;
|
||||
import javax.imageio.metadata.IIOMetadata;
|
||||
import javax.imageio.plugins.jpeg.JPEGImageReadParam;
|
||||
import javax.imageio.spi.IIORegistry;
|
||||
import javax.imageio.spi.ImageReaderSpi;
|
||||
@@ -62,7 +64,6 @@ import java.awt.image.*;
|
||||
import java.io.*;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
import java.util.zip.Inflater;
|
||||
import java.util.zip.InflaterInputStream;
|
||||
|
||||
@@ -80,6 +81,7 @@ import java.util.zip.InflaterInputStream;
|
||||
* <ul>
|
||||
* <li>Tiling</li>
|
||||
* <li>LZW Compression (type 5)</li>
|
||||
* <li>"Old-style" JPEG Compression (type 6), as a best effort, as the spec is not well-defined</li>
|
||||
* <li>JPEG Compression (type 7)</li>
|
||||
* <li>ZLib (aka Adobe-style Deflate) Compression (type 8)</li>
|
||||
* <li>Deflate Compression (type 32946)</li>
|
||||
@@ -90,6 +92,7 @@ import java.util.zip.InflaterInputStream;
|
||||
* <li>Planar data (PlanarConfiguration type 2/Planar)</li>
|
||||
* <li>ICC profiles (ICCProfile)</li>
|
||||
* <li>BitsPerSample values up to 16 for most PhotometricInterpretations</li>
|
||||
* <li>Multiple images (pages) in one file</li>
|
||||
* </ul>
|
||||
*
|
||||
* @see <a href="http://partners.adobe.com/public/developer/tiff/index.html">Adobe TIFF developer resources</a>
|
||||
@@ -104,6 +107,7 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
// TODOs ImageIO basic functionality:
|
||||
// TODO: Subsampling (*tests should be failing*)
|
||||
// TODO: Source region (*tests should be failing*)
|
||||
// TODO: Thumbnail support
|
||||
// TODO: TIFFImageWriter + Spi
|
||||
|
||||
// TODOs Full BaseLine support:
|
||||
@@ -111,7 +115,8 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
// (0: Unspecified (not alpha), 1: Associated Alpha (pre-multiplied), 2: Unassociated Alpha (non-multiplied)
|
||||
|
||||
// TODOs ImageIO advanced functionality:
|
||||
// TODO: Implement readAsRenderedImage to allow tiled renderImage?
|
||||
// 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)
|
||||
@@ -119,6 +124,7 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
|
||||
// TODOs Extension support
|
||||
// TODO: Support PlanarConfiguration 2
|
||||
// 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
|
||||
@@ -290,7 +296,7 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
}
|
||||
case 4:
|
||||
if (bitsPerSample == 8 || bitsPerSample == 16) {
|
||||
// ExtraSamples 0=unspecified, 1=associated (premultiplied), 2=unassociated (TODO: Support unspecified, not alpha)
|
||||
// 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) {
|
||||
@@ -356,7 +362,7 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
}
|
||||
case 5:
|
||||
if (bitsPerSample == 8 || bitsPerSample == 16) {
|
||||
// ExtraSamples 0=unspecified, 1=associated (premultiplied), 2=unassociated (TODO: Support unspecified, not alpha)
|
||||
// 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) {
|
||||
@@ -428,19 +434,19 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
readIFD(imageIndex);
|
||||
|
||||
ImageTypeSpecifier rawType = getRawImageType(imageIndex);
|
||||
List<ImageTypeSpecifier> specs = new ArrayList<ImageTypeSpecifier>();
|
||||
Set<ImageTypeSpecifier> specs = new LinkedHashSet<ImageTypeSpecifier>(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().isCS_sRGB() && rawType.getColorModel().getColorSpace().getType() == ColorSpace.TYPE_RGB) {
|
||||
if (rawType.getColorModel().getColorSpace().getType() == ColorSpace.TYPE_RGB) {
|
||||
if (rawType.getNumBands() == 3 && rawType.getBitsPerBand(0) == 8) {
|
||||
specs.add(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_3BYTE_BGR));
|
||||
specs.add(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_INT_BGR));
|
||||
specs.add(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_INT_RGB));
|
||||
// specs.add(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_INT_BGR));
|
||||
// specs.add(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_INT_RGB));
|
||||
}
|
||||
else if (rawType.getNumBands() == 4 && rawType.getBitsPerBand(0) == 8) {
|
||||
specs.add(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_4BYTE_ABGR));
|
||||
specs.add(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_INT_ARGB));
|
||||
// specs.add(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_INT_ARGB));
|
||||
specs.add(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_4BYTE_ABGR_PRE));
|
||||
}
|
||||
}
|
||||
@@ -666,53 +672,10 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
|
||||
jpegReader.setInput(new ByteArrayImageInputStream(tablesValue));
|
||||
|
||||
// NOTE: This initializes the tables AND MORE secret internal settings for the reader (as if by magic).
|
||||
// This is probably a bug, as later setInput calls should clear/override the tables.
|
||||
// However, it would be extremely convenient, not having to actually fiddle with the stream meta data (as below)
|
||||
// NOTE: This initializes the tables and other internal settings for the reader (as if by magic).
|
||||
// This is actually a feature of JPEG,
|
||||
// see: http://docs.oracle.com/javase/6/docs/api/javax/imageio/metadata/doc-files/jpeg_metadata.html#abbrev
|
||||
/*IIOMetadata streamMetadata = */jpegReader.getStreamMetadata();
|
||||
|
||||
/*
|
||||
IIOMetadataNode root = (IIOMetadataNode) streamMetadata.getAsTree(streamMetadata.getNativeMetadataFormatName());
|
||||
NodeList dqt = root.getElementsByTagName("dqt");
|
||||
NodeList dqtables = ((IIOMetadataNode) dqt.item(0)).getElementsByTagName("dqtable");
|
||||
JPEGQTable[] qTables = new JPEGQTable[dqtables.getLength()];
|
||||
for (int i = 0; i < dqtables.getLength(); i++) {
|
||||
qTables[i] = (JPEGQTable) ((IIOMetadataNode) dqtables.item(i)).getUserObject();
|
||||
System.err.println("qTables: " + qTables[i]);
|
||||
}
|
||||
|
||||
List<JPEGHuffmanTable> acHTables = new ArrayList<JPEGHuffmanTable>();
|
||||
List<JPEGHuffmanTable> dcHTables = new ArrayList<JPEGHuffmanTable>();
|
||||
|
||||
NodeList dht = root.getElementsByTagName("dht");
|
||||
for (int i = 0; i < dht.getLength(); i++) {
|
||||
NodeList dhtables = ((IIOMetadataNode) dht.item(i)).getElementsByTagName("dhtable");
|
||||
for (int j = 0; j < dhtables.getLength(); j++) {
|
||||
System.err.println("dhtables.getLength(): " + dhtables.getLength());
|
||||
IIOMetadataNode dhtable = (IIOMetadataNode) dhtables.item(j);
|
||||
JPEGHuffmanTable userObject = (JPEGHuffmanTable) dhtable.getUserObject();
|
||||
if ("0".equals(dhtable.getAttribute("class"))) {
|
||||
dcHTables.add(userObject);
|
||||
}
|
||||
else {
|
||||
acHTables.add(userObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
JPEGHuffmanTable[] dcTables = dcHTables.toArray(new JPEGHuffmanTable[dcHTables.size()]);
|
||||
JPEGHuffmanTable[] acTables = acHTables.toArray(new JPEGHuffmanTable[acHTables.size()]);
|
||||
*/
|
||||
// JPEGTables tables = new JPEGTables(new ByteArrayImageInputStream(tablesValue));
|
||||
// JPEGQTable[] qTables = tables.getQTables();
|
||||
// JPEGHuffmanTable[] dcTables = tables.getDCHuffmanTables();
|
||||
// JPEGHuffmanTable[] acTables = tables.getACHuffmanTables();
|
||||
|
||||
// System.err.println("qTables: " + Arrays.toString(qTables));
|
||||
// System.err.println("dcTables: " + Arrays.toString(dcTables));
|
||||
// System.err.println("acTables: " + Arrays.toString(acTables));
|
||||
|
||||
// jpegParam.setDecodeTables(qTables, dcTables, acTables);
|
||||
}
|
||||
else {
|
||||
processWarningOccurred("Missing JPEGTables for TIFF with compression: 7 (JPEG)");
|
||||
@@ -732,6 +695,7 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
|
||||
imageInput.seek(stripTileOffsets[i]);
|
||||
ImageInputStream subStream = new SubImageInputStream(imageInput, stripTileByteCounts != null ? (int) stripTileByteCounts[i] : Short.MAX_VALUE);
|
||||
|
||||
try {
|
||||
jpegReader.setInput(subStream);
|
||||
jpegParam.setSourceRegion(new Rectangle(0, 0, colsInTile, rowsInTile));
|
||||
@@ -1236,6 +1200,15 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
return ICC_Profile.getInstance(value);
|
||||
}
|
||||
|
||||
// TODO: Tiling support
|
||||
// isImageTiled
|
||||
// getTileWidth
|
||||
// getTileHeight
|
||||
// readTile
|
||||
// readTileRaster
|
||||
|
||||
// TODO: Thumbnail support
|
||||
|
||||
public static void main(final String[] args) throws IOException {
|
||||
for (final String arg : args) {
|
||||
File file = new File(arg);
|
||||
@@ -1312,6 +1285,12 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
// param.setSourceSubsampling(2, 2, 0, 0);
|
||||
BufferedImage image = reader.read(imageNo, param);
|
||||
System.err.println("Read time: " + (System.currentTimeMillis() - start) + " ms");
|
||||
|
||||
IIOMetadata metadata = reader.getImageMetadata(0);
|
||||
if (metadata != null) {
|
||||
new XMLSerializer(System.out, "UTF-8").serialize(metadata.getAsTree(metadata.getNativeMetadataFormatName()), false);
|
||||
}
|
||||
|
||||
// System.err.println("image: " + image);
|
||||
|
||||
// File tempFile = File.createTempFile("lzw-", ".bin");
|
||||
@@ -1371,6 +1350,10 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
}
|
||||
}
|
||||
|
||||
protected static void showIt(BufferedImage image, String title) {
|
||||
ImageReaderBase.showIt(image, title);
|
||||
}
|
||||
|
||||
private static void deregisterOSXTIFFImageReaderSpi() {
|
||||
IIORegistry registry = IIORegistry.getDefaultInstance();
|
||||
Iterator<ImageReaderSpi> providers = registry.getServiceProviders(ImageReaderSpi.class, new ServiceRegistry.Filter() {
|
||||
|
||||
@@ -33,6 +33,7 @@ import com.twelvemonkeys.imageio.spi.ProviderInfo;
|
||||
import com.twelvemonkeys.imageio.util.IIOUtil;
|
||||
|
||||
import javax.imageio.spi.ImageReaderSpi;
|
||||
import javax.imageio.spi.ServiceRegistry;
|
||||
import javax.imageio.stream.ImageInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteOrder;
|
||||
@@ -46,7 +47,6 @@ import java.util.Locale;
|
||||
* @version $Id: TIFFImageReaderSpi.java,v 1.0 08.05.12 15:14 haraldk Exp$
|
||||
*/
|
||||
public class TIFFImageReaderSpi extends ImageReaderSpi {
|
||||
// TODO: Should we make sure we register (order) before the com.sun.imageio thing (that isn't what is says) provided by Apple?
|
||||
/**
|
||||
* Creates a {@code TIFFImageReaderSpi}.
|
||||
*/
|
||||
@@ -64,7 +64,7 @@ public class TIFFImageReaderSpi extends ImageReaderSpi {
|
||||
"image/tiff", "image/x-tiff"
|
||||
},
|
||||
"com.twelvemkonkeys.imageio.plugins.tiff.TIFFImageReader",
|
||||
STANDARD_INPUT_TYPE,
|
||||
new Class[] {ImageInputStream.class},
|
||||
// new String[]{"com.twelvemkonkeys.imageio.plugins.tif.TIFFImageWriterSpi"},
|
||||
null,
|
||||
true, // supports standard stream metadata
|
||||
@@ -76,6 +76,23 @@ public class TIFFImageReaderSpi extends ImageReaderSpi {
|
||||
);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public void onRegistration(final ServiceRegistry registry, final Class<?> category) {
|
||||
// Make sure we're ordered before the Apple-provided TIFF reader on OS X
|
||||
try {
|
||||
Class<ImageReaderSpi> providerClass = (Class<ImageReaderSpi>) Class.forName("com.sun.imageio.plugins.tiff.TIFFImageReaderSpi");
|
||||
ImageReaderSpi appleSpi = registry.getServiceProviderByClass(providerClass);
|
||||
|
||||
if (appleSpi != null && appleSpi.getVendorName() != null && appleSpi.getVendorName().startsWith("Apple")) {
|
||||
registry.setOrdering((Class<ImageReaderSpi>) category, this, appleSpi);
|
||||
}
|
||||
}
|
||||
catch (ClassNotFoundException ignore) {
|
||||
// This is actually OK, now we don't have to do anything
|
||||
}
|
||||
}
|
||||
|
||||
public boolean canDecodeInput(final Object pSource) throws IOException {
|
||||
if (!(pSource instanceof ImageInputStream)) {
|
||||
return false;
|
||||
|
||||
@@ -69,6 +69,9 @@ final class YCbCrUpsamplerStream extends FilterInputStream {
|
||||
public YCbCrUpsamplerStream(final InputStream stream, final int[] chromaSub, final int yCbCrPos, final int columns, final double[] coefficients) {
|
||||
super(Validate.notNull(stream, "stream"));
|
||||
|
||||
Validate.notNull(chromaSub, "chromaSub");
|
||||
Validate.isTrue(chromaSub.length == 2, "chromaSub.length != 2");
|
||||
|
||||
this.horizChromaSub = chromaSub[0];
|
||||
this.vertChromaSub = chromaSub[1];
|
||||
this.yCbCrPos = yCbCrPos;
|
||||
|
||||
@@ -28,11 +28,13 @@
|
||||
|
||||
package com.twelvemonkeys.imageio.plugins.tiff;
|
||||
|
||||
import com.twelvemonkeys.io.InputStreamAbstractTestCase;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
/**
|
||||
* YCbCrUpsamplerStreamTest
|
||||
@@ -41,11 +43,77 @@ import java.io.InputStream;
|
||||
* @author last modified by $Author: haraldk$
|
||||
* @version $Id: YCbCrUpsamplerStreamTest.java,v 1.0 31.01.13 14:35 haraldk Exp$
|
||||
*/
|
||||
@Ignore
|
||||
public class YCbCrUpsamplerStreamTest extends InputStreamAbstractTestCase {
|
||||
// TODO: Implement + add @Ignore for all tests that makes no sense for this class.
|
||||
@Override
|
||||
protected InputStream makeInputStream(byte[] pBytes) {
|
||||
return new YCbCrUpsamplerStream(new ByteArrayInputStream(pBytes), new int[] {2, 2}, TIFFExtension.YCBCR_POSITIONING_CENTERED, pBytes.length / 4, null);
|
||||
public class YCbCrUpsamplerStreamTest {
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testCreateNullStream() {
|
||||
new YCbCrUpsamplerStream(null, new int[2], 7, 5, null);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testCreateNullChroma() {
|
||||
new YCbCrUpsamplerStream(new ByteArrayInputStream(new byte[0]), new int[3], 7, 5, null);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testCreateShortChroma() {
|
||||
new YCbCrUpsamplerStream(new ByteArrayInputStream(new byte[0]), new int[1], 7, 5, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpsample22() throws IOException {
|
||||
byte[] bytes = new byte[] {
|
||||
1, 2, 3, 4, 5, 6, 7, 8, 42, 96,
|
||||
108, 109, 110, 111, 112, 113, 114, 115, 43, 97
|
||||
};
|
||||
YCbCrUpsamplerStream stream = new YCbCrUpsamplerStream(new ByteArrayInputStream(bytes), new int[] {2, 2}, TIFFExtension.YCBCR_POSITIONING_CENTERED, 8, null);
|
||||
|
||||
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
|
||||
};
|
||||
byte[] upsampled = new byte[expected.length];
|
||||
|
||||
new DataInputStream(stream).readFully(upsampled);
|
||||
|
||||
assertArrayEquals(expected, upsampled);
|
||||
assertEquals(-1, stream.read());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpsample21() throws IOException {
|
||||
byte[] bytes = new byte[] {
|
||||
1, 2, 3, 4, 42, 96, 77,
|
||||
112, 113, 114, 115, 43, 97, 43
|
||||
};
|
||||
YCbCrUpsamplerStream stream = new YCbCrUpsamplerStream(new ByteArrayInputStream(bytes), new int[] {2, 1}, TIFFExtension.YCBCR_POSITIONING_CENTERED, 4, null);
|
||||
|
||||
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
|
||||
};
|
||||
byte[] upsampled = new byte[expected.length];
|
||||
|
||||
new DataInputStream(stream).readFully(upsampled);
|
||||
|
||||
assertArrayEquals(expected, upsampled);
|
||||
assertEquals(-1, stream.read());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpsample12() throws IOException {
|
||||
byte[] bytes = new byte[] {
|
||||
1, 2, 3, 4, 42, 96, 77,
|
||||
112, 113, 114, 115, 43, 97, 43
|
||||
};
|
||||
YCbCrUpsamplerStream stream = new YCbCrUpsamplerStream(new ByteArrayInputStream(bytes), new int[] {1, 2}, TIFFExtension.YCBCR_POSITIONING_CENTERED, 4, null);
|
||||
|
||||
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
|
||||
};
|
||||
byte[] upsampled = new byte[expected.length];
|
||||
|
||||
new DataInputStream(stream).readFully(upsampled);
|
||||
|
||||
assertArrayEquals(expected, upsampled);
|
||||
assertEquals(-1, stream.read());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
- FileChannelImageInputStream/MappedByteBufferImageInputStream
|
||||
- FileChannelCacheImageInputStream
|
||||
- FileChannelImageOutputStream
|
||||
- FileChannelCacheImageOutputStream
|
||||
|
||||
- Consider creating a raw ImageReader (or util class?) that can read raw bitmaps:
|
||||
o Interleaved (A)RGB (as in BMP, PICT, IFF PBM etc) -> A1R1G1B1, A2R2G2B2, ..., AnRnGnNn
|
||||
o Channeled (A)RGB (as in Photoshop) -> A1A2...An, R1R2...Rn, G1G2...Gn, B1B2...Bn
|
||||
|
||||
18
pom.xml
18
pom.xml
@@ -40,6 +40,10 @@
|
||||
</contributor>
|
||||
</contributors>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<build>
|
||||
<finalName>twelvemonkeys-${project.artifactId}-${project.version}</finalName>
|
||||
<plugins>
|
||||
@@ -64,6 +68,7 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-resources-plugin</artifactId>
|
||||
<version>2.5</version>
|
||||
<configuration>
|
||||
<encoding>UTF-8</encoding>
|
||||
</configuration>
|
||||
@@ -71,7 +76,7 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<version>2.2</version>
|
||||
<version>2.4</version>
|
||||
<inherited>true</inherited>
|
||||
<executions>
|
||||
<execution>
|
||||
@@ -86,6 +91,7 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-source-plugin</artifactId>
|
||||
<version>2.2.1</version>
|
||||
<inherited>true</inherited>
|
||||
<!-- THIS DOES NOT WORK..?! -->
|
||||
<executions>
|
||||
@@ -102,11 +108,13 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>2.3.2</version>
|
||||
<inherited>true</inherited>
|
||||
<configuration>
|
||||
<source>1.5</source>
|
||||
<target>1.5</target>
|
||||
<showDeprecation>true</showDeprecation>
|
||||
<showDeprecation>false</showDeprecation>
|
||||
<compilerArgument>-g:lines</compilerArgument>
|
||||
<compilerArguments>
|
||||
<encoding>iso-8859-1</encoding>
|
||||
</compilerArguments>
|
||||
@@ -116,6 +124,7 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>2.10</version>
|
||||
<configuration>
|
||||
<systemProperties>
|
||||
<property>
|
||||
@@ -156,18 +165,22 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-javadoc-plugin</artifactId>
|
||||
<version>2.9.1</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-report-plugin</artifactId>
|
||||
<version>2.16</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>cobertura-maven-plugin</artifactId>
|
||||
<version>2.6</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-pmd-plugin</artifactId>
|
||||
<version>3.0.1</version>
|
||||
<configuration>
|
||||
<targetJdk>1.5</targetJdk>
|
||||
</configuration>
|
||||
@@ -175,6 +188,7 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-checkstyle-plugin</artifactId>
|
||||
<version>2.10</version>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</reporting>
|
||||
|
||||
@@ -79,6 +79,12 @@
|
||||
<version>${project.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.twelvemonkeys.sandbox</groupId>
|
||||
<artifactId>sandbox-common</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.twelvemonkeys.common</groupId>
|
||||
@@ -120,7 +126,7 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<version>2.2</version>
|
||||
<version>2.4</version>
|
||||
<configuration>
|
||||
<archive>
|
||||
<manifestEntries>
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* 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.image;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.KeyEvent;
|
||||
import java.awt.event.WindowAdapter;
|
||||
import java.awt.event.WindowEvent;
|
||||
import java.awt.geom.Point2D;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.BufferedImageOp;
|
||||
import java.awt.image.ColorModel;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* AbstractFilter
|
||||
*
|
||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||
* @author last modified by $Author: haraldk$
|
||||
* @version $Id: AbstractFilter.java,v 1.0 18.06.12 16:55 haraldk Exp$
|
||||
*/
|
||||
public abstract class AbstractFilter implements BufferedImageOp {
|
||||
public abstract BufferedImage filter(BufferedImage src, BufferedImage dest);
|
||||
|
||||
public BufferedImage createCompatibleDestImage(BufferedImage src, ColorModel destCM) {
|
||||
throw new UnsupportedOperationException("Method createCompatibleDestImage not implemented"); // TODO: Implement
|
||||
}
|
||||
|
||||
public Rectangle2D getBounds2D(BufferedImage src) {
|
||||
return new Rectangle2D.Double(0, 0, src.getWidth(), src.getHeight());
|
||||
}
|
||||
|
||||
public Point2D getPoint2D(Point2D srcPt, Point2D dstPt) {
|
||||
if (dstPt == null) {
|
||||
dstPt = new Point2D.Double();
|
||||
}
|
||||
|
||||
dstPt.setLocation(srcPt);
|
||||
|
||||
return dstPt;
|
||||
}
|
||||
|
||||
public RenderingHints getRenderingHints() {
|
||||
return null;
|
||||
}
|
||||
|
||||
protected static void exercise(final String[] args, final BufferedImageOp filter, final Color background) throws IOException {
|
||||
boolean original = false;
|
||||
|
||||
for (String arg : args) {
|
||||
if (arg.startsWith("-")) {
|
||||
if (arg.equals("-o") || arg.equals("--original")) {
|
||||
original = true;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
final File file = new File(arg);
|
||||
BufferedImage image = ImageIO.read(file);
|
||||
|
||||
if (image.getWidth() > 640) {
|
||||
image = new ResampleOp(640, Math.round(image.getHeight() * (640f / image.getWidth())), null).filter(image, null);
|
||||
}
|
||||
|
||||
if (!original) {
|
||||
filter.filter(image, image);
|
||||
}
|
||||
|
||||
final Color bg = original ? Color.BLACK : background;
|
||||
final BufferedImage img = image;
|
||||
|
||||
SwingUtilities.invokeLater(new Runnable() {
|
||||
public void run() {
|
||||
JFrame frame = new JFrame(filter.getClass().getSimpleName().replace("Filter", "") + "Test: " + file.getName());
|
||||
frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
|
||||
frame.addWindowListener(new WindowAdapter() {
|
||||
@Override
|
||||
public void windowClosed(final WindowEvent e) {
|
||||
Window[] windows = Window.getWindows();
|
||||
if (windows == null || windows.length == 0) {
|
||||
System.exit(0);
|
||||
}
|
||||
}
|
||||
});
|
||||
frame.getRootPane().getActionMap().put("window-close", new AbstractAction() {
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
Window window = SwingUtilities.getWindowAncestor((Component) e.getSource());
|
||||
window.setVisible(false);
|
||||
window.dispose();
|
||||
}
|
||||
});
|
||||
frame.getRootPane().getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_W, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()), "window-close");
|
||||
|
||||
JLabel label = new JLabel(new BufferedImageIcon(img));
|
||||
if (bg != null) {
|
||||
label.setOpaque(true);
|
||||
label.setBackground(bg);
|
||||
}
|
||||
label.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
|
||||
JScrollPane scrollPane = new JScrollPane(label);
|
||||
scrollPane.setBorder(BorderFactory.createEmptyBorder());
|
||||
frame.add(scrollPane);
|
||||
|
||||
frame.pack();
|
||||
frame.setLocationByPlatform(true);
|
||||
frame.setVisible(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
/*
|
||||
* 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.image;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.color.ColorSpace;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.ColorConvertOp;
|
||||
import java.awt.image.RescaleOp;
|
||||
import java.io.IOException;
|
||||
import java.util.Random;
|
||||
|
||||
/**
|
||||
* InstaCRTFilter
|
||||
*
|
||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||
* @author last modified by $Author: haraldk$
|
||||
* @version $Id: InstaCRTFilter.java,v 1.0 15.06.12 13:24 haraldk Exp$
|
||||
*/
|
||||
public class InstaCRTFilter extends AbstractFilter {
|
||||
|
||||
// NOTE: This is a PoC, and not good code...
|
||||
public BufferedImage filter(BufferedImage src, BufferedImage dest) {
|
||||
if (dest == null) {
|
||||
dest = createCompatibleDestImage(src, null);
|
||||
}
|
||||
|
||||
// Make grayscale
|
||||
BufferedImage image = new ColorConvertOp(ColorSpace.getInstance(ColorSpace.CS_GRAY), getRenderingHints()).filter(src, null);
|
||||
|
||||
// Make image faded/too bright
|
||||
image = new RescaleOp(1.2f, 120f, getRenderingHints()).filter(image, image);
|
||||
|
||||
// Blur
|
||||
image = ImageUtil.blur(image, 2.5f);
|
||||
|
||||
Graphics2D g = dest.createGraphics();
|
||||
try {
|
||||
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
g.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
|
||||
g.drawImage(image, 0, 0, null);
|
||||
|
||||
// Rotate it slightly for a more analogue feeling
|
||||
double angle = .0055;
|
||||
g.rotate(angle);
|
||||
|
||||
// Apply fake green-ish h-sync line at random position
|
||||
Random random = new Random();
|
||||
int lineStart = random.nextInt(image.getHeight() - 80);
|
||||
int lineHeight = random.nextInt(10) + 20;
|
||||
|
||||
g.setComposite(AlphaComposite.SrcOver.derive(.3f));
|
||||
g.setPaint(new LinearGradientPaint(
|
||||
0, lineStart, 0, lineStart + lineHeight,
|
||||
new float[] {0, .3f, .9f, 1},
|
||||
new Color[] {new Color(0, true), new Color(0x90AF66), new Color(0x99606F33, true), new Color(0, true)}
|
||||
));
|
||||
g.fillRect(0, lineStart, image.getWidth(), lineHeight);
|
||||
|
||||
// Apply fake large dot-pitch (black lines w/transparency)
|
||||
g.setComposite(AlphaComposite.SrcOver.derive(.55f));
|
||||
g.setColor(Color.BLACK);
|
||||
|
||||
for (int y = 0; y < image.getHeight(); y += 3) {
|
||||
g.setStroke(new BasicStroke(random.nextFloat() / 3 + .8f));
|
||||
g.drawLine(0, y, image.getWidth(), y);
|
||||
}
|
||||
|
||||
// Vignette/border
|
||||
g.setComposite(AlphaComposite.SrcOver.derive(.75f));
|
||||
int focus = Math.min(image.getWidth() / 8, image.getHeight() / 8);
|
||||
g.setPaint(new RadialGradientPaint(
|
||||
new Point(image.getWidth() / 2, image.getHeight() / 2),
|
||||
Math.max(image.getWidth(), image.getHeight()) / 1.6f,
|
||||
new Point(focus, focus),
|
||||
new float[] {0, .3f, .9f, 1f},
|
||||
new Color[] {new Color(0x99FFFFFF, true), new Color(0x00FFFFFF, true), new Color(0x0, true), Color.BLACK},
|
||||
MultipleGradientPaint.CycleMethod.NO_CYCLE
|
||||
));
|
||||
g.fillRect(-2, -2, image.getWidth() + 4, image.getHeight() + 4);
|
||||
|
||||
g.rotate(-angle);
|
||||
|
||||
g.setComposite(AlphaComposite.SrcOver.derive(.35f));
|
||||
g.setPaint(new RadialGradientPaint(
|
||||
new Point(image.getWidth() / 2, image.getHeight() / 2),
|
||||
Math.max(image.getWidth(), image.getHeight()) / 1.65f,
|
||||
new Point(image.getWidth() / 2, image.getHeight() / 2),
|
||||
new float[] {0, .85f, 1f},
|
||||
new Color[] {new Color(0x0, true), new Color(0x0, true), Color.BLACK},
|
||||
MultipleGradientPaint.CycleMethod.NO_CYCLE
|
||||
));
|
||||
g.fillRect(0, 0, image.getWidth(), image.getHeight());
|
||||
|
||||
// Highlight
|
||||
g.setComposite(AlphaComposite.SrcOver.derive(.55f));
|
||||
g.setPaint(new RadialGradientPaint(
|
||||
new Point(image.getWidth(), image.getHeight()),
|
||||
Math.max(image.getWidth(), image.getHeight()) * 1.1f,
|
||||
new Point(image.getWidth() / 2, image.getHeight() / 2),
|
||||
new float[] {0, .75f, 1f},
|
||||
new Color[] {new Color(0x00FFFFFF, true), new Color(0x00FFFFFF, true), Color.WHITE},
|
||||
MultipleGradientPaint.CycleMethod.NO_CYCLE
|
||||
));
|
||||
g.fillRect(0, 0, image.getWidth(), image.getHeight());
|
||||
}
|
||||
finally {
|
||||
g.dispose();
|
||||
}
|
||||
|
||||
// Round corners
|
||||
BufferedImage foo = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D graphics = foo.createGraphics();
|
||||
try {
|
||||
graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
graphics.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
|
||||
graphics.setColor(Color.WHITE);
|
||||
double angle = -0.04;
|
||||
g.rotate(angle);
|
||||
graphics.fillRoundRect(1, 1, image.getWidth() - 2, image.getHeight() - 2, 20, 20);
|
||||
}
|
||||
finally {
|
||||
graphics.dispose();
|
||||
}
|
||||
|
||||
foo = ImageUtil.blur(foo, 4.5f);
|
||||
|
||||
// Compose image into rounded corners
|
||||
graphics = foo.createGraphics();
|
||||
try {
|
||||
graphics.setComposite(AlphaComposite.SrcIn);
|
||||
graphics.drawImage(dest, 0, 0, null);
|
||||
}
|
||||
finally {
|
||||
graphics.dispose();
|
||||
}
|
||||
|
||||
// Draw it all back to dest
|
||||
g = dest.createGraphics();
|
||||
try {
|
||||
g.setComposite(AlphaComposite.SrcOver);
|
||||
g.setColor(Color.BLACK);
|
||||
g.fillRect(0, 0, image.getWidth(), image.getHeight());
|
||||
g.drawImage(foo, 0, 0, null);
|
||||
}
|
||||
finally {
|
||||
g.dispose();
|
||||
}
|
||||
|
||||
return dest;
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
exercise(args, new InstaCRTFilter(), Color.BLACK);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
/*
|
||||
* 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.image;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.RescaleOp;
|
||||
import java.io.IOException;
|
||||
import java.util.Random;
|
||||
|
||||
/**
|
||||
* InstaLomoFilter
|
||||
*
|
||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||
* @author last modified by $Author: haraldk$
|
||||
* @version $Id: InstaLomoFilter.java,v 1.0 15.06.12 13:24 haraldk Exp$
|
||||
*/
|
||||
public class InstaLomoFilter extends AbstractFilter {
|
||||
final private Random random = new Random();
|
||||
|
||||
// NOTE: This is a PoC, and not good code...
|
||||
public BufferedImage filter(BufferedImage src, BufferedImage dest) {
|
||||
if (dest == null) {
|
||||
dest = createCompatibleDestImage(src, null);
|
||||
}
|
||||
|
||||
// Make image faded/washed out/red-ish
|
||||
// DARK WARM
|
||||
float[] scales = new float[] { 2.2f, 2.0f, 1.55f};
|
||||
float[] offsets = new float[] {-20.0f, -90.0f, -110.0f};
|
||||
|
||||
// BRIGHT NATURAL
|
||||
// float[] scales = new float[] { 1.1f, .9f, .7f};
|
||||
// float[] offsets = new float[] {20, 30, 80};
|
||||
|
||||
// Faded, old-style
|
||||
// float[] scales = new float[] { 1.1f, .7f, .3f};
|
||||
// float[] offsets = new float[] {20, 30, 80};
|
||||
|
||||
// float[] scales = new float[] { 1.2f, .4f, .4f};
|
||||
// float[] offsets = new float[] {0, 120, 120};
|
||||
|
||||
// BRIGHT WARM
|
||||
// float[] scales = new float[] {1.1f, .8f, 1.6f};
|
||||
// float[] offsets = new float[] {60, 70, -80};
|
||||
BufferedImage image = new RescaleOp(scales, offsets, getRenderingHints()).filter(src, null);
|
||||
|
||||
// Blur
|
||||
image = ImageUtil.blur(image, 2.5f);
|
||||
|
||||
Graphics2D g = dest.createGraphics();
|
||||
try {
|
||||
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
g.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
|
||||
g.drawImage(image, 0, 0, null);
|
||||
|
||||
// Rotate it slightly for a more analogue feeling
|
||||
double angle = .0055;
|
||||
g.rotate(angle);
|
||||
|
||||
// Scratches
|
||||
g.setComposite(AlphaComposite.SrcOver.derive(.025f));
|
||||
for (int i = 0; i < 100; i++) {
|
||||
g.setColor(random.nextBoolean() ? Color.WHITE : Color.BLACK);
|
||||
g.setStroke(new BasicStroke(random.nextFloat() * 2f));
|
||||
int x = random.nextInt(image.getWidth());
|
||||
|
||||
int off = random.nextInt(100);
|
||||
for (int j = random.nextInt(3); j > 0; j--) {
|
||||
g.drawLine(x + j, 0, x + off - 50 + j, image.getHeight());
|
||||
}
|
||||
}
|
||||
|
||||
// Vignette/border
|
||||
g.setComposite(AlphaComposite.SrcOver.derive(.75f));
|
||||
int focus = Math.min(image.getWidth() / 8, image.getHeight() / 8);
|
||||
g.setPaint(new RadialGradientPaint(
|
||||
new Point(image.getWidth() / 2, image.getHeight() / 2),
|
||||
Math.max(image.getWidth(), image.getHeight()) / 1.6f,
|
||||
new Point(focus, focus),
|
||||
new float[] {0, .3f, .9f, 1f},
|
||||
new Color[] {new Color(0x99FFFFFF, true), new Color(0x00FFFFFF, true), new Color(0x0, true), Color.BLACK},
|
||||
MultipleGradientPaint.CycleMethod.NO_CYCLE
|
||||
));
|
||||
g.fillRect(-2, -2, image.getWidth() + 4, image.getHeight() + 4);
|
||||
|
||||
g.rotate(-angle);
|
||||
|
||||
g.setComposite(AlphaComposite.SrcOver.derive(.35f));
|
||||
g.setPaint(new RadialGradientPaint(
|
||||
new Point(image.getWidth() / 2, image.getHeight() / 2),
|
||||
Math.max(image.getWidth(), image.getHeight()) / 1.65f,
|
||||
new Point(image.getWidth() / 2, image.getHeight() / 2),
|
||||
new float[] {0, .85f, 1f},
|
||||
new Color[] {new Color(0x0, true), new Color(0x0, true), Color.BLACK},
|
||||
MultipleGradientPaint.CycleMethod.NO_CYCLE
|
||||
));
|
||||
g.fillRect(0, 0, image.getWidth(), image.getHeight());
|
||||
|
||||
// Highlight
|
||||
g.setComposite(AlphaComposite.SrcOver.derive(.35f));
|
||||
g.setPaint(new RadialGradientPaint(
|
||||
new Point(image.getWidth(), image.getHeight()),
|
||||
Math.max(image.getWidth(), image.getHeight()) * 1.1f,
|
||||
new Point(image.getWidth() / 2, image.getHeight() / 2),
|
||||
new float[] {0, .75f, 1f},
|
||||
new Color[] {new Color(0x00FFFFFF, true), new Color(0x00FFFFFF, true), Color.PINK},
|
||||
MultipleGradientPaint.CycleMethod.NO_CYCLE
|
||||
));
|
||||
g.fillRect(0, 0, image.getWidth(), image.getHeight());
|
||||
}
|
||||
finally {
|
||||
g.dispose();
|
||||
}
|
||||
|
||||
// Noise
|
||||
NoiseFilter noise = new NoiseFilter();
|
||||
noise.setAmount(10);
|
||||
noise.setDensity(2);
|
||||
dest = noise.filter(dest, dest);
|
||||
|
||||
// Round corners
|
||||
BufferedImage foo = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D graphics = foo.createGraphics();
|
||||
try {
|
||||
graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
graphics.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
|
||||
graphics.setColor(Color.WHITE);
|
||||
double angle = (random.nextDouble() * .01) - .005;
|
||||
graphics.rotate(angle);
|
||||
graphics.fillRoundRect(4, 4, image.getWidth() - 8, image.getHeight() - 8, 20, 20);
|
||||
}
|
||||
finally {
|
||||
graphics.dispose();
|
||||
}
|
||||
|
||||
noise.setAmount(20);
|
||||
noise.setDensity(1);
|
||||
noise.setMonochrome(true);
|
||||
foo = noise.filter(foo, foo);
|
||||
|
||||
foo = ImageUtil.blur(foo, 4.5f);
|
||||
|
||||
// Compose image into rounded corners
|
||||
graphics = foo.createGraphics();
|
||||
try {
|
||||
graphics.setComposite(AlphaComposite.SrcIn);
|
||||
graphics.drawImage(dest, 0, 0, null);
|
||||
}
|
||||
finally {
|
||||
graphics.dispose();
|
||||
}
|
||||
|
||||
// Draw it all back to dest
|
||||
g = dest.createGraphics();
|
||||
try {
|
||||
if (dest.getTransparency() != Transparency.OPAQUE) {
|
||||
g.setComposite(AlphaComposite.Clear);
|
||||
}
|
||||
g.setColor(Color.WHITE);
|
||||
g.fillRect(0, 0, image.getWidth(), image.getHeight());
|
||||
g.setComposite(AlphaComposite.SrcOver);
|
||||
g.drawImage(foo, 0, 0, null);
|
||||
}
|
||||
finally {
|
||||
g.dispose();
|
||||
}
|
||||
|
||||
return dest;
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
exercise(args, new InstaLomoFilter(), Color.WHITE);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
* 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.image;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.color.ColorSpace;
|
||||
import java.awt.image.*;
|
||||
import java.io.IOException;
|
||||
import java.util.Random;
|
||||
|
||||
/**
|
||||
* InstaLomoFilter
|
||||
*
|
||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||
* @author last modified by $Author: haraldk$
|
||||
* @version $Id: InstaLomoFilter.java,v 1.0 15.06.12 13:24 haraldk Exp$
|
||||
*/
|
||||
public class InstaSepiaFilter extends AbstractFilter {
|
||||
final private Random random = new Random();
|
||||
|
||||
// NOTE: This is a PoC, and not good code...
|
||||
@Override
|
||||
public BufferedImage filter(BufferedImage src, BufferedImage dest) {
|
||||
if (dest == null) {
|
||||
dest = createCompatibleDestImage(src, null);
|
||||
}
|
||||
|
||||
BufferedImage image = new ColorConvertOp(ColorSpace.getInstance(ColorSpace.CS_GRAY), getRenderingHints()).filter(src, dest);
|
||||
|
||||
Graphics2D g2d = dest.createGraphics();
|
||||
try {
|
||||
g2d.drawImage(image, 0, 0, null);
|
||||
}
|
||||
finally {
|
||||
g2d.dispose();
|
||||
}
|
||||
|
||||
// Blur
|
||||
image = ImageUtil.blur(image, 2.5f);
|
||||
|
||||
Graphics2D g = dest.createGraphics();
|
||||
try {
|
||||
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
g.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
|
||||
g.drawImage(image, 0, 0, null);
|
||||
|
||||
// Rotate it slightly for a more analogue feeling
|
||||
double angle = -.0055;
|
||||
g.rotate(angle);
|
||||
|
||||
// Vignette/border
|
||||
g.setComposite(AlphaComposite.SrcOver.derive(.35f));
|
||||
g.setPaint(new RadialGradientPaint(
|
||||
new Point(image.getWidth() / 2, image.getHeight() / 2),
|
||||
Math.max(image.getWidth(), image.getHeight()) / 1.65f,
|
||||
new Point(image.getWidth() / 2, image.getHeight() / 2),
|
||||
new float[] {0, .85f, 1f},
|
||||
new Color[] {new Color(0x0, true), new Color(0x0, true), Color.BLACK},
|
||||
MultipleGradientPaint.CycleMethod.NO_CYCLE
|
||||
));
|
||||
g.fillRect(0, 0, image.getWidth(), image.getHeight());
|
||||
|
||||
}
|
||||
finally {
|
||||
g.dispose();
|
||||
}
|
||||
|
||||
// Round corners
|
||||
BufferedImage foo = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D graphics = foo.createGraphics();
|
||||
try {
|
||||
graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
graphics.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
|
||||
graphics.setColor(Color.WHITE);
|
||||
double angle = (random.nextDouble() * .01) - .005;
|
||||
graphics.rotate(angle);
|
||||
graphics.fillRoundRect(4, 4, image.getWidth() - 8, image.getHeight() - 8, 20, 20);
|
||||
}
|
||||
finally {
|
||||
graphics.dispose();
|
||||
}
|
||||
|
||||
// Noise
|
||||
NoiseFilter noise = new NoiseFilter();
|
||||
noise.setAmount(20);
|
||||
noise.setDensity(1);
|
||||
noise.setMonochrome(true);
|
||||
foo = noise.filter(foo, foo);
|
||||
|
||||
foo = ImageUtil.blur(foo, 4.5f);
|
||||
|
||||
// Compose image into rounded corners
|
||||
graphics = foo.createGraphics();
|
||||
try {
|
||||
graphics.setComposite(AlphaComposite.SrcIn);
|
||||
graphics.drawImage(dest, 0, 0, null);
|
||||
}
|
||||
finally {
|
||||
graphics.dispose();
|
||||
}
|
||||
|
||||
float[] scales = new float[] {1, 1, 1, 1};
|
||||
float[] offsets = new float[] {80, 40, 0, 0};
|
||||
foo = new RescaleOp(scales, offsets, getRenderingHints()).filter(foo, foo);
|
||||
|
||||
// Draw it all back to dest
|
||||
g = dest.createGraphics();
|
||||
try {
|
||||
g.setComposite(AlphaComposite.SrcOver);
|
||||
g.setColor(Color.WHITE);
|
||||
g.fillRect(0, 0, image.getWidth(), image.getHeight());
|
||||
g.drawImage(foo, 0, 0, null);
|
||||
}
|
||||
finally {
|
||||
g.dispose();
|
||||
}
|
||||
|
||||
return dest;
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
exercise(args, new InstaSepiaFilter(), null);
|
||||
}
|
||||
}
|
||||
@@ -61,6 +61,7 @@ import java.util.concurrent.*;
|
||||
public class MappedBufferImage {
|
||||
private static int threads = Runtime.getRuntime().availableProcessors();
|
||||
private static ExecutorService executorService = Executors.newFixedThreadPool(threads * 4);
|
||||
private static ExecutorService executorService2 = Executors.newFixedThreadPool(2);
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
int argIndex = 0;
|
||||
@@ -553,15 +554,15 @@ public class MappedBufferImage {
|
||||
}
|
||||
}
|
||||
|
||||
public void drawTo(Graphics2D g) {
|
||||
public boolean drawTo(Graphics2D g) {
|
||||
BufferedImage img = data.get();
|
||||
|
||||
if (img != null) {
|
||||
g.drawImage(img, x, y, null);
|
||||
return true;
|
||||
}
|
||||
|
||||
// g.setPaint(Color.GREEN);
|
||||
// g.drawString(String.format("[%d, %d]", x, y), x + 20, y + 20);
|
||||
return false;
|
||||
}
|
||||
|
||||
public int getX() {
|
||||
@@ -622,6 +623,7 @@ public class MappedBufferImage {
|
||||
}
|
||||
|
||||
// TODO: Consider a fixed size (mem) LRUCache instead
|
||||
// TODO: Better yet, re-use tiles
|
||||
Map<Point, Tile> tiles = createTileCache();
|
||||
|
||||
private void repaintImage(final Rectangle rect, final Graphics2D g2) {
|
||||
@@ -634,6 +636,15 @@ public class MappedBufferImage {
|
||||
// Paint tiles of the image, to preserve memory
|
||||
final int tileSize = 200;
|
||||
|
||||
// Calculate relative to image(0,0), rather than rect(x, y)
|
||||
int xOff = rect.x % tileSize;
|
||||
int yOff = rect.y % tileSize;
|
||||
|
||||
rect.x -= xOff;
|
||||
rect.y -= yOff;
|
||||
rect.width += xOff;
|
||||
rect.height += yOff;
|
||||
|
||||
int tilesW = 1 + rect.width / tileSize;
|
||||
int tilesH = 1 + rect.height / tileSize;
|
||||
|
||||
@@ -658,10 +669,10 @@ public class MappedBufferImage {
|
||||
// TODO: Could we use ImageProducer/ImageConsumer/ImageObserver interface??
|
||||
|
||||
// Destination (display) coordinates
|
||||
int dstX = (int) Math.round(x * zoom);
|
||||
int dstY = (int) Math.round(y * zoom);
|
||||
int dstW = (int) Math.round(w * zoom);
|
||||
int dstH = (int) Math.round(h * zoom);
|
||||
int dstX = (int) Math.floor(x * zoom);
|
||||
int dstY = (int) Math.floor(y * zoom);
|
||||
int dstW = (int) Math.ceil(w * zoom);
|
||||
int dstH = (int) Math.ceil(h * zoom);
|
||||
|
||||
if (dstW == 0 || dstH == 0) {
|
||||
continue;
|
||||
@@ -678,8 +689,8 @@ public class MappedBufferImage {
|
||||
// final int tileSrcH = Math.min(tileSize, image.getHeight() - tileSrcY);
|
||||
|
||||
// Destination (display) coordinates
|
||||
int tileDstX = (int) Math.round(tileSrcX * zoom);
|
||||
int tileDstY = (int) Math.round(tileSrcY * zoom);
|
||||
int tileDstX = (int) Math.floor(tileSrcX * zoom);
|
||||
int tileDstY = (int) Math.floor(tileSrcY * zoom);
|
||||
// final int tileDstW = (int) Math.round(tileSrcW * zoom);
|
||||
// final int tileDstH = (int) Math.round(tileSrcH * zoom);
|
||||
|
||||
@@ -699,9 +710,7 @@ public class MappedBufferImage {
|
||||
Tile tile = tiles.get(point);
|
||||
|
||||
if (tile != null) {
|
||||
Reference<BufferedImage> img = tile.data;
|
||||
if (img != null) {
|
||||
tile.drawTo(g2);
|
||||
if (tile.drawTo(g2)) {
|
||||
continue;
|
||||
}
|
||||
else {
|
||||
@@ -713,9 +722,8 @@ public class MappedBufferImage {
|
||||
|
||||
// Dispatch to off-thread worker
|
||||
final Map<Point, Tile> localTiles = tiles;
|
||||
executorService.submit(new Runnable() {
|
||||
executorService2.submit(new Runnable() {
|
||||
public void run() {
|
||||
// TODO: Fix rounding issues... Problem is that sometimes the srcW/srcH is 1 pixel off filling the tile...
|
||||
int tileSrcX = (int) Math.round(point.x / zoom);
|
||||
int tileSrcY = (int) Math.round(point.y / zoom);
|
||||
int tileSrcW = Math.min(tileSize, image.getWidth() - tileSrcX);
|
||||
@@ -735,8 +743,14 @@ public class MappedBufferImage {
|
||||
}
|
||||
|
||||
// Test against current view rect, to avoid computing tiles that will be thrown away immediately
|
||||
// TODO: EDT safe?
|
||||
if (!getVisibleRect().intersects(new Rectangle(point.x, point.y, tileDstW, tileDstH))) {
|
||||
final Rectangle visibleRect = new Rectangle();
|
||||
SwingUtilities.invokeAndWait(new Runnable() {
|
||||
public void run() {
|
||||
visibleRect.setBounds(getVisibleRect());
|
||||
}
|
||||
});
|
||||
|
||||
if (!visibleRect.intersects(new Rectangle(point.x, point.y, tileDstW, tileDstH))) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -30,11 +30,12 @@ package com.twelvemonkeys.image;
|
||||
|
||||
import javax.imageio.ImageTypeSpecifier;
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.ColorModel;
|
||||
import java.awt.image.DataBuffer;
|
||||
import java.awt.image.SampleModel;
|
||||
import java.awt.image.*;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.lang.reflect.UndeclaredThrowableException;
|
||||
|
||||
/**
|
||||
* A factory for creating {@link BufferedImage}s backed by memory mapped files.
|
||||
@@ -50,6 +51,9 @@ public final class MappedImageFactory {
|
||||
// TODO: Create a way to do ColorConvertOp (or other color space conversion) on these images.
|
||||
// - Current implementation of CCOp delegates to internal sun.awt classes that assumes java.awt.DataBufferByte for type byte buffers :-/
|
||||
|
||||
private static final boolean DEBUG = "true".equalsIgnoreCase(System.getProperty("com.twelvemonkeys.image.mapped.debug"));
|
||||
static final RasterFactory RASTER_FACTORY = createRasterFactory();
|
||||
|
||||
private MappedImageFactory() {}
|
||||
|
||||
public static BufferedImage createCompatibleMappedImage(int width, int height, int type) throws IOException {
|
||||
@@ -58,7 +62,8 @@ public final class MappedImageFactory {
|
||||
}
|
||||
|
||||
public static BufferedImage createCompatibleMappedImage(int width, int height, GraphicsConfiguration configuration, int transparency) throws IOException {
|
||||
// TODO: Should we also use the sample model?
|
||||
// BufferedImage temp = configuration.createCompatibleImage(1, 1, transparency);
|
||||
// return createCompatibleMappedImage(width, height, temp.getSampleModel().createCompatibleSampleModel(width, height), temp.getColorModel());
|
||||
return createCompatibleMappedImage(width, height, configuration.getColorModel(transparency));
|
||||
}
|
||||
|
||||
@@ -73,6 +78,88 @@ public final class MappedImageFactory {
|
||||
static BufferedImage createCompatibleMappedImage(int width, int height, SampleModel sm, ColorModel cm) throws IOException {
|
||||
DataBuffer buffer = MappedFileBuffer.create(sm.getTransferType(), width * height * sm.getNumDataElements(), 1);
|
||||
|
||||
return new BufferedImage(cm, new GenericWritableRaster(sm, buffer, new Point()), cm.isAlphaPremultiplied(), null);
|
||||
return new BufferedImage(cm, RASTER_FACTORY.createRaster(sm, buffer, new Point()), cm.isAlphaPremultiplied(), null);
|
||||
}
|
||||
|
||||
private static RasterFactory createRasterFactory() {
|
||||
try {
|
||||
// Try to instantiate, will throw LinkageError if it fails
|
||||
return new SunRasterFactory();
|
||||
}
|
||||
catch (LinkageError e) {
|
||||
if (DEBUG) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
System.err.println("Could not instantiate SunWritableRaster, falling back to GenericWritableRaster.");
|
||||
}
|
||||
|
||||
// Fall back
|
||||
return new GenericRasterFactory();
|
||||
}
|
||||
|
||||
static interface RasterFactory {
|
||||
WritableRaster createRaster(SampleModel model, DataBuffer buffer, Point origin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic implementation that should work for any JRE, and creates a custom subclass of {@link WritableRaster}.
|
||||
*/
|
||||
static final class GenericRasterFactory implements RasterFactory {
|
||||
public WritableRaster createRaster(final SampleModel model, final DataBuffer buffer, final Point origin) {
|
||||
return new GenericWritableRaster(model, buffer, origin);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sun/Oracle JRE-specific implementation that creates {@code sun.awt.image.SunWritableRaster}.
|
||||
* Callers must catch {@link LinkageError}.
|
||||
*/
|
||||
static final class SunRasterFactory implements RasterFactory {
|
||||
final private Constructor<WritableRaster> factoryMethod = getFactoryMethod();
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static Constructor<WritableRaster> getFactoryMethod() {
|
||||
try {
|
||||
Class<?> cls = Class.forName("sun.awt.image.SunWritableRaster");
|
||||
|
||||
if (Modifier.isAbstract(cls.getModifiers())) {
|
||||
throw new IncompatibleClassChangeError("sun.awt.image.SunWritableRaster has become abstract and can't be instantiated");
|
||||
}
|
||||
|
||||
return (Constructor<WritableRaster>) cls.getConstructor(SampleModel.class, DataBuffer.class, Point.class);
|
||||
}
|
||||
catch (ClassNotFoundException e) {
|
||||
throw new NoClassDefFoundError(e.getMessage());
|
||||
}
|
||||
catch (NoSuchMethodException e) {
|
||||
throw new NoSuchMethodError(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public WritableRaster createRaster(final SampleModel model, final DataBuffer buffer, final Point origin) {
|
||||
try {
|
||||
return factoryMethod.newInstance(model, buffer, origin);
|
||||
}
|
||||
catch (InstantiationException e) {
|
||||
throw new Error("Could not create SunWritableRaster: ", e); // Should never happen, as we test for abstract class
|
||||
}
|
||||
catch (IllegalAccessException e) {
|
||||
throw new Error("Could not create SunWritableRaster: ", e); // Should never happen, only public constructors are reflected
|
||||
}
|
||||
catch (InvocationTargetException e) {
|
||||
// Unwrap to allow normal exception flow
|
||||
Throwable cause = e.getCause();
|
||||
|
||||
if (cause instanceof RuntimeException) {
|
||||
throw (RuntimeException) cause;
|
||||
}
|
||||
else if (cause instanceof Error) {
|
||||
throw (Error) cause;
|
||||
}
|
||||
|
||||
throw new UndeclaredThrowableException(cause);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
/*
|
||||
Copyright 2006 Jerry Huxtable
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
package com.twelvemonkeys.image;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.WritableRaster;
|
||||
import java.util.Random;
|
||||
|
||||
/**
|
||||
* NoiseFilter
|
||||
*
|
||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||
* @author last modified by $Author: haraldk$
|
||||
* @version $Id: NoiseFilter.java,v 1.0 15.06.12 22:59 haraldk Exp$
|
||||
*/
|
||||
public class NoiseFilter extends AbstractFilter {
|
||||
|
||||
/**
|
||||
* Gaussian distribution for the noise.
|
||||
*/
|
||||
public final static int GAUSSIAN = 0;
|
||||
|
||||
/**
|
||||
* Uniform distribution for the noise.
|
||||
*/
|
||||
public final static int UNIFORM = 1;
|
||||
|
||||
private int amount = 25;
|
||||
private int distribution = UNIFORM;
|
||||
private boolean monochrome = false;
|
||||
private float density = 1;
|
||||
private Random randomNumbers = new Random();
|
||||
|
||||
public NoiseFilter() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the amount of effect.
|
||||
*
|
||||
* @param amount the amount
|
||||
* @min-value 0
|
||||
* @max-value 1
|
||||
* @see #getAmount
|
||||
*/
|
||||
public void setAmount(int amount) {
|
||||
this.amount = amount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the amount of noise.
|
||||
*
|
||||
* @return the amount
|
||||
* @see #setAmount
|
||||
*/
|
||||
public int getAmount() {
|
||||
return amount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the distribution of the noise.
|
||||
*
|
||||
* @param distribution the distribution
|
||||
* @see #getDistribution
|
||||
*/
|
||||
public void setDistribution(int distribution) {
|
||||
this.distribution = distribution;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the distribution of the noise.
|
||||
*
|
||||
* @return the distribution
|
||||
* @see #setDistribution
|
||||
*/
|
||||
public int getDistribution() {
|
||||
return distribution;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether to use monochrome noise.
|
||||
*
|
||||
* @param monochrome true for monochrome noise
|
||||
* @see #getMonochrome
|
||||
*/
|
||||
public void setMonochrome(boolean monochrome) {
|
||||
this.monochrome = monochrome;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get whether to use monochrome noise.
|
||||
*
|
||||
* @return true for monochrome noise
|
||||
* @see #setMonochrome
|
||||
*/
|
||||
public boolean getMonochrome() {
|
||||
return monochrome;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the density of the noise.
|
||||
*
|
||||
* @param density the density
|
||||
* @see #getDensity
|
||||
*/
|
||||
public void setDensity(float density) {
|
||||
this.density = density;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the density of the noise.
|
||||
*
|
||||
* @return the density
|
||||
* @see #setDensity
|
||||
*/
|
||||
public float getDensity() {
|
||||
return density;
|
||||
}
|
||||
|
||||
private int random() {
|
||||
return (int) (((distribution == GAUSSIAN ? randomNumbers.nextGaussian() : 2 * randomNumbers.nextFloat() - 1)) * amount);
|
||||
}
|
||||
|
||||
private static int clamp(int x) {
|
||||
if (x < 0) {
|
||||
return 0;
|
||||
}
|
||||
else if (x > 0xff) {
|
||||
return 0xff;
|
||||
}
|
||||
return x;
|
||||
}
|
||||
|
||||
public int filterRGB(int x, int y, int rgb) {
|
||||
if (randomNumbers.nextFloat() <= density) {
|
||||
int a = rgb & 0xff000000;
|
||||
int r = (rgb >> 16) & 0xff;
|
||||
int g = (rgb >> 8) & 0xff;
|
||||
int b = rgb & 0xff;
|
||||
|
||||
if (monochrome) {
|
||||
int n = random();
|
||||
r = clamp(r + n);
|
||||
g = clamp(g + n);
|
||||
b = clamp(b + n);
|
||||
}
|
||||
else {
|
||||
r = clamp(r + random());
|
||||
g = clamp(g + random());
|
||||
b = clamp(b + random());
|
||||
}
|
||||
return a | (r << 16) | (g << 8) | b;
|
||||
}
|
||||
return rgb;
|
||||
}
|
||||
|
||||
public BufferedImage filter(BufferedImage src, BufferedImage dst) {
|
||||
int width = src.getWidth();
|
||||
int height = src.getHeight();
|
||||
int type = src.getType();
|
||||
WritableRaster srcRaster = src.getRaster();
|
||||
|
||||
if (dst == null) {
|
||||
dst = createCompatibleDestImage(src, null);
|
||||
}
|
||||
WritableRaster dstRaster = dst.getRaster();
|
||||
|
||||
int[] inPixels = new int[width];
|
||||
for (int y = 0; y < height; y++) {
|
||||
// We try to avoid calling getRGB on images as it causes them to become unmanaged, causing horrible performance problems.
|
||||
if (type == BufferedImage.TYPE_INT_ARGB) {
|
||||
srcRaster.getDataElements(0, y, width, 1, inPixels);
|
||||
for (int x = 0; x < width; x++) {
|
||||
inPixels[x] = filterRGB(x, y, inPixels[x]);
|
||||
}
|
||||
dstRaster.setDataElements(0, y, width, 1, inPixels);
|
||||
}
|
||||
else {
|
||||
src.getRGB(0, y, width, 1, inPixels, 0, width);
|
||||
for (int x = 0; x < width; x++) {
|
||||
inPixels[x] = filterRGB(x, y, inPixels[x]);
|
||||
}
|
||||
dst.setRGB(0, y, width, 1, inPixels, 0, width);
|
||||
}
|
||||
}
|
||||
|
||||
return dst;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user