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:
Rune Bremnes
2014-02-24 10:28:26 +01:00
128 changed files with 7361 additions and 4827 deletions

1
README
View File

@@ -1 +0,0 @@
We did it

498
README.md Normal file
View 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

View File

@@ -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
{

View File

@@ -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;

View File

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

View File

@@ -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:

View File

@@ -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

View File

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

View 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 (&lt 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;
}
}

View File

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

View File

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

View File

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

View File

@@ -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;

View File

@@ -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()?

View File

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

View File

@@ -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");

View File

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

View File

@@ -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++;
}

View File

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

View File

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

View File

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

View File

@@ -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;
}
/**

View File

@@ -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 = "";

View File

@@ -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) {

View File

@@ -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.
}

View File

@@ -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/>

View File

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

View File

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

View File

@@ -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) {

View File

@@ -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

View File

@@ -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

View File

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

View File

@@ -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) {

View File

@@ -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

View File

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

View File

@@ -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 {

View File

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

View File

@@ -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

View File

@@ -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

View File

@@ -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,

View File

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

View File

@@ -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,

View File

@@ -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,

View File

@@ -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;

View File

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

View File

@@ -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() {

View File

@@ -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/>

View File

@@ -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";
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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;

View File

@@ -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;

View File

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

View File

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

View File

@@ -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

View File

@@ -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

View File

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

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

View File

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

View File

@@ -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:

View File

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

View File

@@ -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

View File

@@ -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;

View File

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

View File

@@ -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;

View File

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

View File

@@ -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

View File

@@ -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

View File

@@ -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) {

View File

@@ -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

View File

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

View File

@@ -33,6 +33,7 @@ import com.twelvemonkeys.io.enc.Decoder;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
/**
* LempelZivWelch (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);
}
}
}

View File

@@ -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() {

View File

@@ -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;

View File

@@ -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;

View File

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

View File

@@ -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
View File

@@ -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>

View File

@@ -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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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