diff --git a/README b/README deleted file mode 100644 index cb72f39a..00000000 --- a/README +++ /dev/null @@ -1 +0,0 @@ -We did it \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 00000000..6d5cbc1c --- /dev/null +++ b/README.md @@ -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 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 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. + + + + ... + + + ImageIO service provider loader/unloader + com.twelvemonkeys.servlet.image.IIOProviderContextListener + + + ... + + + + +#### 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 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: + + ... + + ... + + com.twelvemonkeys.imageio + imageio-jpeg + 3.0-SNAPSHOT + + + com.twelvemonkeys.imageio + imageio-tiff + 3.0-SNAPSHOT + + + +#### 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 diff --git a/common/common-image/src/main/java/com/twelvemonkeys/image/BrightnessContrastFilter.java b/common/common-image/src/main/java/com/twelvemonkeys/image/BrightnessContrastFilter.java index 2b4fb736..0dbb7598 100755 --- a/common/common-image/src/main/java/com/twelvemonkeys/image/BrightnessContrastFilter.java +++ b/common/common-image/src/main/java/com/twelvemonkeys/image/BrightnessContrastFilter.java @@ -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 { diff --git a/common/common-image/src/main/java/com/twelvemonkeys/image/DiffusionDither.java b/common/common-image/src/main/java/com/twelvemonkeys/image/DiffusionDither.java index 48664fcc..d530d077 100755 --- a/common/common-image/src/main/java/com/twelvemonkeys/image/DiffusionDither.java +++ b/common/common-image/src/main/java/com/twelvemonkeys/image/DiffusionDither.java @@ -17,7 +17,7 @@ import java.util.Random; * This {@code BufferedImageOp/RasterOp} implements basic * Floyd-Steinberg error-diffusion algorithm for dithering. *

- * 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: * - * - * - * @author Harald Kuhr - * @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)); - } -} diff --git a/imageio/imageio-batik/src/main/java/com/twelvemonkeys/imageio/plugins/svg/SVGImageReader.java b/imageio/imageio-batik/src/main/java/com/twelvemonkeys/imageio/plugins/svg/SVGImageReader.java index 43d57219..9071994b 100755 --- a/imageio/imageio-batik/src/main/java/com/twelvemonkeys/imageio/plugins/svg/SVGImageReader.java +++ b/imageio/imageio-batik/src/main/java/com/twelvemonkeys/imageio/plugins/svg/SVGImageReader.java @@ -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) { diff --git a/imageio/imageio-batik/src/main/java/com/twelvemonkeys/imageio/plugins/svg/SVGImageReaderSpi.java b/imageio/imageio-batik/src/main/java/com/twelvemonkeys/imageio/plugins/svg/SVGImageReaderSpi.java index 2d946d61..3c1744d8 100755 --- a/imageio/imageio-batik/src/main/java/com/twelvemonkeys/imageio/plugins/svg/SVGImageReaderSpi.java +++ b/imageio/imageio-batik/src/main/java/com/twelvemonkeys/imageio/plugins/svg/SVGImageReaderSpi.java @@ -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 diff --git a/imageio/imageio-batik/src/main/java/com/twelvemonkeys/imageio/plugins/wmf/WMFImageReaderSpi.java b/imageio/imageio-batik/src/main/java/com/twelvemonkeys/imageio/plugins/wmf/WMFImageReaderSpi.java index f857aae0..d9aecc2e 100755 --- a/imageio/imageio-batik/src/main/java/com/twelvemonkeys/imageio/plugins/wmf/WMFImageReaderSpi.java +++ b/imageio/imageio-batik/src/main/java/com/twelvemonkeys/imageio/plugins/wmf/WMFImageReaderSpi.java @@ -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 diff --git a/imageio/imageio-batik/src/test/java/com/twelvemonkeys/imageio/plugins/svg/SVGImageReaderTestCase.java b/imageio/imageio-batik/src/test/java/com/twelvemonkeys/imageio/plugins/svg/SVGImageReaderTestCase.java index a3772238..26875bac 100755 --- a/imageio/imageio-batik/src/test/java/com/twelvemonkeys/imageio/plugins/svg/SVGImageReaderTestCase.java +++ b/imageio/imageio-batik/src/test/java/com/twelvemonkeys/imageio/plugins/svg/SVGImageReaderTestCase.java @@ -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 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; + } + } + } } \ No newline at end of file diff --git a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/ImageReaderBase.java b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/ImageReaderBase.java index 4a0d7243..ca0129b2 100644 --- a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/ImageReaderBase.java +++ b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/ImageReaderBase.java @@ -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) { diff --git a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/color/ColorSpaces.java b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/color/ColorSpaces.java index 767e6b8e..ba622ab4 100644 --- a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/color/ColorSpaces.java +++ b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/color/ColorSpaces.java @@ -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 adobeRGB1998 = new WeakReference(null); private static WeakReference genericCMYK = new WeakReference(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 diff --git a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/BufferedImageInputStream.java b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/BufferedImageInputStream.java index 221a22ba..3bc4be58 100644 --- a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/BufferedImageInputStream.java +++ b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/BufferedImageInputStream.java @@ -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(); } diff --git a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/ByteArrayImageInputStream.java b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/ByteArrayImageInputStream.java index 18ae9cef..92aee63d 100755 --- a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/ByteArrayImageInputStream.java +++ b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/ByteArrayImageInputStream.java @@ -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 { diff --git a/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/color/ColorSpacesTest.java b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/color/ColorSpacesTest.java index ac154765..6f7cd126 100644 --- a/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/color/ColorSpacesTest.java +++ b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/color/ColorSpacesTest.java @@ -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()); + } } diff --git a/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/ImageReaderAbstractTestCase.java b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/ImageReaderAbstractTestCase.java index cf15f1f0..1b08bd96 100644 --- a/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/ImageReaderAbstractTestCase.java +++ b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/ImageReaderAbstractTestCase.java @@ -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 { protected abstract List 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 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 files = Arrays.asList(dir.listFiles()); - assertFalse(files.isEmpty()); - return files; - } - protected abstract ImageReaderSpi createProvider(); protected abstract Class getReaderClass(); @@ -476,7 +452,7 @@ public abstract class ImageReaderAbstractTestCase { } @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 { } 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 { 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 { 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 { // 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 { boolean removed = illegalTypes.remove(valid); // TODO: 4BYTE_ABGR (6) and 4BYTE_ABGR_PRE (7) is essentially the same type... - // !#$#�%$! ImageTypeSpecifier.equals is not well-defined + // #$@*%$! ImageTypeSpecifier.equals is not well-defined if (!removed) { for (Iterator iterator = illegalTypes.iterator(); iterator.hasNext();) { ImageTypeSpecifier illegalType = iterator.next(); @@ -1422,6 +1454,7 @@ public abstract class ImageReaderAbstractTestCase { failBecause("Could not read " + data.getInput() + " with explicit destination type " + type, e); } + assertNotNull(result); assertEquals(type.getColorModel(), result.getColorModel()); // The following logically tests diff --git a/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/ImageWriterAbstractTestCase.java b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/ImageWriterAbstractTestCase.java index e6fa78ea..67ecd60f 100755 --- a/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/ImageWriterAbstractTestCase.java +++ b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/ImageWriterAbstractTestCase.java @@ -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 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 diff --git a/imageio/imageio-core/src/test/resources/profiles/Corbis RGB.icc b/imageio/imageio-core/src/test/resources/profiles/Corbis RGB.icc new file mode 100644 index 00000000..487f03b1 Binary files /dev/null and b/imageio/imageio-core/src/test/resources/profiles/Corbis RGB.icc differ diff --git a/imageio/imageio-core/src/test/resources/profiles/Corbis RGB_fixed.icc b/imageio/imageio-core/src/test/resources/profiles/Corbis RGB_fixed.icc new file mode 100755 index 00000000..f6293449 Binary files /dev/null and b/imageio/imageio-core/src/test/resources/profiles/Corbis RGB_fixed.icc differ diff --git a/imageio/imageio-icns/src/main/java/com/twelvemonkeys/imageio/plugins/icns/ICNSImageReaderSpi.java b/imageio/imageio-icns/src/main/java/com/twelvemonkeys/imageio/plugins/icns/ICNSImageReaderSpi.java index 5b023276..0dd66e30 100644 --- a/imageio/imageio-icns/src/main/java/com/twelvemonkeys/imageio/plugins/icns/ICNSImageReaderSpi.java +++ b/imageio/imageio-icns/src/main/java/com/twelvemonkeys/imageio/plugins/icns/ICNSImageReaderSpi.java @@ -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, diff --git a/imageio/imageio-icns/src/test/java/com/twelvemonkeys/imageio/plugins/icns/ICNSImageReaderTest.java b/imageio/imageio-icns/src/test/java/com/twelvemonkeys/imageio/plugins/icns/ICNSImageReaderTest.java index 527a0ba0..bd8acd6a 100644 --- a/imageio/imageio-icns/src/test/java/com/twelvemonkeys/imageio/plugins/icns/ICNSImageReaderTest.java +++ b/imageio/imageio-icns/src/test/java/com/twelvemonkeys/imageio/plugins/icns/ICNSImageReaderTest.java @@ -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 getMIMETypes() { return Arrays.asList("image/x-apple-icons"); } + + @Test + @Ignore("Known issue: Subsampled reading not supported") + @Override + public void testReadWithSubsampleParamPixels() throws IOException { + super.testReadWithSubsampleParamPixels(); + } } diff --git a/imageio/imageio-ico/src/main/java/com/twelvemonkeys/imageio/plugins/ico/CURImageReaderSpi.java b/imageio/imageio-ico/src/main/java/com/twelvemonkeys/imageio/plugins/ico/CURImageReaderSpi.java index 43f127e8..af4c9dc8 100755 --- a/imageio/imageio-ico/src/main/java/com/twelvemonkeys/imageio/plugins/ico/CURImageReaderSpi.java +++ b/imageio/imageio-ico/src/main/java/com/twelvemonkeys/imageio/plugins/ico/CURImageReaderSpi.java @@ -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, diff --git a/imageio/imageio-ico/src/main/java/com/twelvemonkeys/imageio/plugins/ico/ICOImageReaderSpi.java b/imageio/imageio-ico/src/main/java/com/twelvemonkeys/imageio/plugins/ico/ICOImageReaderSpi.java index eebeda17..2e5adfb0 100755 --- a/imageio/imageio-ico/src/main/java/com/twelvemonkeys/imageio/plugins/ico/ICOImageReaderSpi.java +++ b/imageio/imageio-ico/src/main/java/com/twelvemonkeys/imageio/plugins/ico/ICOImageReaderSpi.java @@ -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, diff --git a/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/CMAPChunk.java b/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/CMAPChunk.java index ed79b5bf..b1546149 100755 --- a/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/CMAPChunk.java +++ b/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/CMAPChunk.java @@ -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; diff --git a/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/IFFImageReader.java b/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/IFFImageReader.java index 53f2fc8c..530e95c9 100755 --- a/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/IFFImageReader.java +++ b/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/IFFImageReader.java @@ -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. *

* 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. *

* 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); + } } } diff --git a/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/IFFImageReaderSpi.java b/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/IFFImageReaderSpi.java index 05835d21..8b92907c 100755 --- a/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/IFFImageReaderSpi.java +++ b/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/IFFImageReaderSpi.java @@ -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() { diff --git a/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/IFFImageWriter.java b/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/IFFImageWriter.java index bd3350d0..d567113a 100755 --- a/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/IFFImageWriter.java +++ b/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/IFFImageWriter.java @@ -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. *

diff --git a/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/IFFImageWriterSpi.java b/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/IFFImageWriterSpi.java index 4704236d..902a3dd9 100755 --- a/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/IFFImageWriterSpi.java +++ b/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/IFFImageWriterSpi.java @@ -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"; } } diff --git a/imageio/imageio-jmagick/src/main/java/com/twelvemonkeys/imageio/plugins/jmagick/JMagickImageReaderSpiSupport.java b/imageio/imageio-jmagick/src/main/java/com/twelvemonkeys/imageio/plugins/jmagick/JMagickImageReaderSpiSupport.java index 528ff26e..cb4a4d45 100755 --- a/imageio/imageio-jmagick/src/main/java/com/twelvemonkeys/imageio/plugins/jmagick/JMagickImageReaderSpiSupport.java +++ b/imageio/imageio-jmagick/src/main/java/com/twelvemonkeys/imageio/plugins/jmagick/JMagickImageReaderSpiSupport.java @@ -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 diff --git a/imageio/imageio-jmagick/src/main/java/com/twelvemonkeys/imageio/plugins/jmagick/JMagickImageWriterSpiSupport.java b/imageio/imageio-jmagick/src/main/java/com/twelvemonkeys/imageio/plugins/jmagick/JMagickImageWriterSpiSupport.java index f738cfd2..6ef71dd2 100755 --- a/imageio/imageio-jmagick/src/main/java/com/twelvemonkeys/imageio/plugins/jmagick/JMagickImageWriterSpiSupport.java +++ b/imageio/imageio-jmagick/src/main/java/com/twelvemonkeys/imageio/plugins/jmagick/JMagickImageWriterSpiSupport.java @@ -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 diff --git a/imageio/imageio-jmagick/src/main/java/com/twelvemonkeys/imageio/plugins/jmagick/JMagickReader.java b/imageio/imageio-jmagick/src/main/java/com/twelvemonkeys/imageio/plugins/jmagick/JMagickReader.java index 7d51b6c1..c9ff4f7b 100755 --- a/imageio/imageio-jmagick/src/main/java/com/twelvemonkeys/imageio/plugins/jmagick/JMagickReader.java +++ b/imageio/imageio-jmagick/src/main/java/com/twelvemonkeys/imageio/plugins/jmagick/JMagickReader.java @@ -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 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) { diff --git a/imageio/imageio-jmagick/src/main/java/com/twelvemonkeys/imageio/plugins/jmagick/JPEGImageReaderSpi.java b/imageio/imageio-jmagick/src/main/java/com/twelvemonkeys/imageio/plugins/jmagick/JPEGImageReaderSpi.java index 797714ca..ad93a67e 100755 --- a/imageio/imageio-jmagick/src/main/java/com/twelvemonkeys/imageio/plugins/jmagick/JPEGImageReaderSpi.java +++ b/imageio/imageio-jmagick/src/main/java/com/twelvemonkeys/imageio/plugins/jmagick/JPEGImageReaderSpi.java @@ -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); } diff --git a/imageio/imageio-jmagick/src/main/java/com/twelvemonkeys/imageio/plugins/jmagick/TargaImageReaderSpi.java b/imageio/imageio-jmagick/src/main/java/com/twelvemonkeys/imageio/plugins/jmagick/TargaImageReaderSpi.java index 6d838b1c..db8455d4 100755 --- a/imageio/imageio-jmagick/src/main/java/com/twelvemonkeys/imageio/plugins/jmagick/TargaImageReaderSpi.java +++ b/imageio/imageio-jmagick/src/main/java/com/twelvemonkeys/imageio/plugins/jmagick/TargaImageReaderSpi.java @@ -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); diff --git a/imageio/imageio-jmagick/src/main/java/com/twelvemonkeys/imageio/plugins/jmagick/WMFImageReaderSpi.java b/imageio/imageio-jmagick/src/main/java/com/twelvemonkeys/imageio/plugins/jmagick/WMFImageReaderSpi.java index 88f1cd9f..748d0c9b 100755 --- a/imageio/imageio-jmagick/src/main/java/com/twelvemonkeys/imageio/plugins/jmagick/WMFImageReaderSpi.java +++ b/imageio/imageio-jmagick/src/main/java/com/twelvemonkeys/imageio/plugins/jmagick/WMFImageReaderSpi.java @@ -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; } diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnailReader.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnailReader.java index 1eaba702..704dc1c9 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnailReader.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnailReader.java @@ -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 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(); diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/FastCMYKToRGB.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/FastCMYKToRGB.java index 58a96211..58a25744 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/FastCMYKToRGB.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/FastCMYKToRGB.java @@ -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()); diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JFXXThumbnailReader.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JFXXThumbnailReader.java index cce8b3bb..8de14dcb 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JFXXThumbnailReader.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JFXXThumbnailReader.java @@ -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 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(thumbnail); diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImage10MetadataCleaner.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImage10MetadataCleaner.java new file mode 100644 index 00000000..292a6bc8 --- /dev/null +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImage10MetadataCleaner.java @@ -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 Harald Kuhr + * @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 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; + } +} diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java index 81b7269e..e8be131d 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java @@ -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; *

* Main features: *

    + *
  • Support for YCbCr JPEGs without JFIF segment (converted to RGB, using the embedded ICC profile if applicable)
  • *
  • Support for CMYK JPEGs (converted to RGB by default or as CMYK, using the embedded ICC profile if applicable)
  • *
  • Support for Adobe YCCK JPEGs (converted to RGB by default or as CMYK, using the embedded ICC profile if applicable)
  • *
  • Support for JPEGs containing ICC profiles with interpretation other than 'Perceptual' (profile is assumed to be 'Perceptual' and used)
  • @@ -75,10 +79,17 @@ import java.util.List; *
* Thumbnail support: *
    - *
  • Support for JFIF thumbnails (even if stream contains "inconsistent metadata")
  • + *
  • Support for JFIF thumbnails (even if stream contains inconsistent metadata)
  • *
  • Support for JFXX thumbnails (JPEG, Indexed and RGB)
  • *
  • Support for EXIF thumbnails (JPEG, RGB and YCbCr)
  • *
+ * Metadata support: + *
    + *
  • Support for JPEG metadata in both standard and native formats (even if stream contains inconsistent metadata)
  • + *
  • Support for {@code javax_imageio_jpeg_image_1.0} format (currently as native format, may change in the future)
  • + *
  • 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)
  • + *
* * @author Harald Kuhr * @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> SEGMENT_IDENTIFIERS = createSegmentIds(); @@ -97,17 +111,10 @@ public class JPEGImageReader extends ImageReaderBase { private static Map> createSegmentIds() { Map> map = new LinkedHashMap>(); - // 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 segments; - + /** Extra delegate for reading JPEG encoded thumbnails */ + private ImageReader thumbnailReader; private List thumbnails; + private JPEGImage10MetadataCleaner metadataCleaner; + + /** Cached list of JPEG segments we filter from the underlying stream */ + private List 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 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 getAppSegments(final int marker, final String identifier) throws IOException { + List getAppSegments(final int marker, final String identifier) throws IOException { initHeader(); List 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(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 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 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 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 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(); } } diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReaderSpi.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReaderSpi.java index d3814c0c..3fe841e0 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReaderSpi.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReaderSpi.java @@ -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 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; diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageWriterSpi.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageWriterSpi.java index 4201be18..a0dcf30f 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageWriterSpi.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageWriterSpi.java @@ -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 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; diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGSegmentImageInputStream.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGSegmentImageInputStream.java index 23ab8f8b..817dc645 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGSegmentImageInputStream.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGSegmentImageInputStream.java @@ -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); diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/ThumbnailReader.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/ThumbnailReader.java index 6354ef93..370022c6 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/ThumbnailReader.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/ThumbnailReader.java @@ -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() { + } + } } diff --git a/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnailReaderTest.java b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnailReaderTest.java index f1b92e27..9a13a17f 100644 --- a/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnailReaderTest.java +++ b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnailReaderTest.java @@ -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 diff --git a/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JFXXThumbnailReaderTest.java b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JFXXThumbnailReaderTest.java index 4f8db3ff..0b8a6402 100644 --- a/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JFXXThumbnailReaderTest.java +++ b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JFXXThumbnailReaderTest.java @@ -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 diff --git a/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReaderTest.java b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReaderTest.java index 37a34f73..aa5042ee 100644 --- a/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReaderTest.java +++ b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReaderTest.java @@ -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> 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(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 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 spiClass = (Class) 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 expectedChildren = sortNodes(expectedTree.getChildNodes()); + List 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 sortNodes(final NodeList nodes) { + ArrayList sortedNodes = new ArrayList(new AbstractList() { + @Override + public IIOMetadataNode get(int index) { + return (IIOMetadataNode) nodes.item(index); + } + + @Override + public int size() { + return nodes.getLength(); + } + }); + + Collections.sort( + sortedNodes, + new Comparator() { + 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; + } } diff --git a/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGSegmentImageInputStreamTest.java b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGSegmentImageInputStreamTest.java index 4027a56d..b42fa9d2 100644 --- a/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGSegmentImageInputStreamTest.java +++ b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGSegmentImageInputStreamTest.java @@ -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(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 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 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 } } diff --git a/imageio/imageio-jpeg/src/test/resources/jpeg/cmm-exception-corbis-rgb.jpg b/imageio/imageio-jpeg/src/test/resources/jpeg/cmm-exception-corbis-rgb.jpg new file mode 100644 index 00000000..30fa40d6 Binary files /dev/null and b/imageio/imageio-jpeg/src/test/resources/jpeg/cmm-exception-corbis-rgb.jpg differ diff --git a/imageio/imageio-jpeg/src/test/resources/jpeg/no-jfif-ycbcr.jpg b/imageio/imageio-jpeg/src/test/resources/jpeg/no-jfif-ycbcr.jpg new file mode 100644 index 00000000..7e31ea76 Binary files /dev/null and b/imageio/imageio-jpeg/src/test/resources/jpeg/no-jfif-ycbcr.jpg differ diff --git a/imageio/imageio-jpeg/src/test/resources/jpeg/xdensity-out-of-range-zero.jpg b/imageio/imageio-jpeg/src/test/resources/jpeg/xdensity-out-of-range-zero.jpg new file mode 100644 index 00000000..6fcf07ab Binary files /dev/null and b/imageio/imageio-jpeg/src/test/resources/jpeg/xdensity-out-of-range-zero.jpg differ diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/AbstractEntry.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/AbstractEntry.java index 9b5170ab..99f6b524 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/AbstractEntry.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/AbstractEntry.java @@ -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); } diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFEntry.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFEntry.java index cf10da19..ca18a1ca 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFEntry.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFEntry.java @@ -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: diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFReader.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFReader.java index 8497da35..02a8fab6 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFReader.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFReader.java @@ -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; } diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/TIFF.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/TIFF.java index 574bfb4d..375fec62 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/TIFF.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/TIFF.java @@ -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 diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGSegment.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGSegment.java index 2d667b6c..d206f438 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGSegment.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGSegment.java @@ -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; diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGSegmentUtil.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGSegmentUtil.java index 10db0713..2aec4e53 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGSegmentUtil.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGSegmentUtil.java @@ -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> 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 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 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(); } } } diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/psd/PSD.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/psd/PSD.java index 4a1b95dc..94e8edac 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/psd/PSD.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/psd/PSD.java @@ -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; diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/xmp/XMPReader.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/xmp/XMPReader.java index e43d7557..642ecf7f 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/xmp/XMPReader.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/xmp/XMPReader.java @@ -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 entries = new ArrayList(); - - 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 entries = new ArrayList(); + + 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> 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 -> return a Map (keyed on xml:lang?) + // Support for -> return a Map keyed on xml:lang Map alternatives = new LinkedHashMap(); 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() : ""; } diff --git a/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PICTImageReaderSpi.java b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PICTImageReaderSpi.java index 5f19f0ae..bfa92b0c 100755 --- a/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PICTImageReaderSpi.java +++ b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PICTImageReaderSpi.java @@ -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 diff --git a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDImageReaderSpi.java b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDImageReaderSpi.java index 754b355e..1abab894 100755 --- a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDImageReaderSpi.java +++ b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDImageReaderSpi.java @@ -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 diff --git a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDMetadata.java b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDMetadata.java index 5f0b59fc..4bbb542c 100644 --- a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDMetadata.java +++ b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDMetadata.java @@ -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; } Iterator getResources(final Class resourceType) { diff --git a/imageio/imageio-thumbsdb/src/main/java/com/twelvemonkeys/imageio/plugins/thumbsdb/ThumbsDBImageReaderSpi.java b/imageio/imageio-thumbsdb/src/main/java/com/twelvemonkeys/imageio/plugins/thumbsdb/ThumbsDBImageReaderSpi.java index 1c704285..a8ad5fb2 100755 --- a/imageio/imageio-thumbsdb/src/main/java/com/twelvemonkeys/imageio/plugins/thumbsdb/ThumbsDBImageReaderSpi.java +++ b/imageio/imageio-thumbsdb/src/main/java/com/twelvemonkeys/imageio/plugins/thumbsdb/ThumbsDBImageReaderSpi.java @@ -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 diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/HorizontalDeDifferencingStream.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/HorizontalDeDifferencingStream.java index 4adaf9ed..04b86dc0 100644 --- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/HorizontalDeDifferencingStream.java +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/HorizontalDeDifferencingStream.java @@ -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(); + } + } + } } diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/LZWDecoder.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/LZWDecoder.java index bf573fa9..789190d6 100644 --- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/LZWDecoder.java +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/LZWDecoder.java @@ -33,6 +33,7 @@ import com.twelvemonkeys.io.enc.Decoder; import java.io.IOException; import java.io.InputStream; +import java.nio.ByteBuffer; /** * Lempel–Ziv–Welch (LZW) decompression. LZW is a universal loss-less data compression algorithm @@ -94,10 +95,9 @@ abstract class LZWDecoder implements Decoder { maxString = 1; } - public int decode(final InputStream stream, final byte[] buffer) throws IOException { + public int decode(final InputStream stream, final ByteBuffer buffer) throws IOException { // Adapted from the pseudo-code example found in the TIFF 6.0 Specification, 1992. // See Section 13: "LZW Compression"/"LZW Decoding", page 61+ - int bufferPos = 0; int code; while ((code = getNextCode(stream)) != EOI_CODE) { @@ -109,30 +109,30 @@ abstract class LZWDecoder implements Decoder { break; } - bufferPos += table[code].writeTo(buffer, bufferPos); + table[code].writeTo(buffer); } else { if (isInTable(code)) { - bufferPos += table[code].writeTo(buffer, bufferPos); + table[code].writeTo(buffer); addStringToTable(table[oldCode].concatenate(table[code].firstChar)); } else { String outString = table[oldCode].concatenate(table[oldCode].firstChar); - bufferPos += outString.writeTo(buffer, bufferPos); + outString.writeTo(buffer); addStringToTable(outString); } } oldCode = code; - if (bufferPos >= buffer.length - maxString - 1) { + if (buffer.remaining() < maxString + 1) { // Buffer full, stop decoding for now break; } } - return bufferPos; + return buffer.position(); } private void addStringToTable(final String string) throws IOException { @@ -301,24 +301,24 @@ abstract class LZWDecoder implements Decoder { return new String(firstChar, this.firstChar, length + 1, this); } - public final int writeTo(final byte[] buffer, final int offset) { + public final void writeTo(final ByteBuffer buffer) { if (length == 0) { - return 0; + return; } - else if (length == 1) { - buffer[offset] = value; - return 1; + if (length == 1) { + buffer.put(value); } else { String e = this; + final int offset = buffer.position(); for (int i = length - 1; i >= 0; i--) { - buffer[offset + i] = e.value; + buffer.put(offset + i, e.value); e = e.previous; } - return length; + buffer.position(offset + length); } } } diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReader.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReader.java index 87a6efaf..77f58aba 100644 --- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReader.java +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReader.java @@ -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; *
    *
  • 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)
  • @@ -90,6 +92,7 @@ import java.util.zip.InflaterInputStream; *
  • Planar data (PlanarConfiguration type 2/Planar)
  • *
  • ICC profiles (ICCProfile)
  • *
  • BitsPerSample values up to 16 for most PhotometricInterpretations
  • + *
  • Multiple images (pages) in one file
  • *
* * @see Adobe TIFF developer resources @@ -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 specs = new ArrayList(); + Set specs = new LinkedHashSet(5); // TODO: Based on raw type, we can probably convert to most RGB types at least, maybe gray etc // TODO: Planar to chunky by default - if (!rawType.getColorModel().getColorSpace().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 acHTables = new ArrayList(); - List dcHTables = new ArrayList(); - - 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 providers = registry.getServiceProviders(ImageReaderSpi.class, new ServiceRegistry.Filter() { diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReaderSpi.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReaderSpi.java index c4254176..451993f6 100644 --- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReaderSpi.java +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReaderSpi.java @@ -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 providerClass = (Class) 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) 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; diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/YCbCrUpsamplerStream.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/YCbCrUpsamplerStream.java index dc193438..c0826c4c 100644 --- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/YCbCrUpsamplerStream.java +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/YCbCrUpsamplerStream.java @@ -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; diff --git a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/YCbCrUpsamplerStreamTest.java b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/YCbCrUpsamplerStreamTest.java index 836816aa..ebb4a236 100644 --- a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/YCbCrUpsamplerStreamTest.java +++ b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/YCbCrUpsamplerStreamTest.java @@ -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()); } } diff --git a/imageio/todo.txt b/imageio/todo.txt index d3fbf082..97c37228 100755 --- a/imageio/todo.txt +++ b/imageio/todo.txt @@ -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 diff --git a/pom.xml b/pom.xml index 7a805421..6cf590a1 100755 --- a/pom.xml +++ b/pom.xml @@ -40,6 +40,10 @@ + + UTF-8 + + twelvemonkeys-${project.artifactId}-${project.version} @@ -64,6 +68,7 @@ org.apache.maven.plugins maven-resources-plugin + 2.5 UTF-8 @@ -71,7 +76,7 @@ org.apache.maven.plugins maven-jar-plugin - 2.2 + 2.4 true @@ -86,6 +91,7 @@ org.apache.maven.plugins maven-source-plugin + 2.2.1 true @@ -102,11 +108,13 @@ org.apache.maven.plugins maven-compiler-plugin + 2.3.2 true 1.5 1.5 - true + false + -g:lines iso-8859-1 @@ -116,6 +124,7 @@ org.apache.maven.plugins maven-surefire-plugin + 2.10 @@ -156,18 +165,22 @@ org.apache.maven.plugins maven-javadoc-plugin + 2.9.1 org.apache.maven.plugins maven-surefire-report-plugin + 2.16 org.codehaus.mojo cobertura-maven-plugin + 2.6 org.apache.maven.plugins maven-pmd-plugin + 3.0.1 1.5 @@ -175,6 +188,7 @@ org.apache.maven.plugins maven-checkstyle-plugin + 2.10 diff --git a/sandbox/pom.xml b/sandbox/pom.xml index 20decb8b..2a022f46 100644 --- a/sandbox/pom.xml +++ b/sandbox/pom.xml @@ -79,6 +79,12 @@ ${project.version} provided + + com.twelvemonkeys.sandbox + sandbox-common + ${project.version} + compile + com.twelvemonkeys.common @@ -120,7 +126,7 @@ org.apache.maven.plugins maven-jar-plugin - 2.2 + 2.4 diff --git a/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/AbstractFilter.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/AbstractFilter.java new file mode 100644 index 00000000..064fae79 --- /dev/null +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/AbstractFilter.java @@ -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 Harald Kuhr + * @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); + } + }); + } + } +} diff --git a/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/InstaCRTFilter.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/InstaCRTFilter.java new file mode 100644 index 00000000..1929d7fe --- /dev/null +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/InstaCRTFilter.java @@ -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 Harald Kuhr + * @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); + } +} diff --git a/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/InstaLomoFilter.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/InstaLomoFilter.java new file mode 100644 index 00000000..6d277717 --- /dev/null +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/InstaLomoFilter.java @@ -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 Harald Kuhr + * @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); + } +} diff --git a/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/InstaSepiaFilter.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/InstaSepiaFilter.java new file mode 100644 index 00000000..b82c6b9c --- /dev/null +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/InstaSepiaFilter.java @@ -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 Harald Kuhr + * @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); + } +} diff --git a/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/MappedBufferImage.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/MappedBufferImage.java index 8d69ffce..2954364c 100644 --- a/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/MappedBufferImage.java +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/MappedBufferImage.java @@ -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 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 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 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; } diff --git a/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/MappedImageFactory.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/MappedImageFactory.java index 3347dd38..fb72da96 100644 --- a/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/MappedImageFactory.java +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/MappedImageFactory.java @@ -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 factoryMethod = getFactoryMethod(); + + @SuppressWarnings("unchecked") + private static Constructor 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) 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); + } + } } } diff --git a/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/NoiseFilter.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/NoiseFilter.java new file mode 100644 index 00000000..73c8d50b --- /dev/null +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/NoiseFilter.java @@ -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 Harald Kuhr + * @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; + } +} diff --git a/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/io/StringInputStream.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/io/StringInputStream.java new file mode 100644 index 00000000..61e0fb50 --- /dev/null +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/io/StringInputStream.java @@ -0,0 +1,87 @@ +package com.twelvemonkeys.io; + +import com.twelvemonkeys.lang.Validate; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.nio.charset.CharsetEncoder; + +/** + * An {@code InputStream} that reads bytes from a {@code String}. + * + * This class properly converts characters into bytes using a {@code Charset}, + * unlike the deprecated {@link java.io.StringBufferInputStream}. + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: StringInputStream.java,v 1.0 03.09.13 10:19 haraldk Exp$ + */ +public final class StringInputStream extends InputStream { + + private final CharBuffer chars; + private final CharsetEncoder encoder; + private final ByteBuffer buffer; + + public StringInputStream(final String string, final Charset charset) { + this(Validate.notNull(string, "string"), 0, string.length(), charset); + } + + public StringInputStream(final String string, int offset, int length, final Charset charset) { + chars = CharBuffer.wrap(Validate.notNull(string, "string"), offset, offset + length); + encoder = Validate.notNull(charset, "charset").newEncoder(); + buffer = ByteBuffer.allocate(256); + buffer.flip(); + } + + private boolean fillBuffer() { + buffer.clear(); + encoder.encode(chars, buffer, chars.hasRemaining()); // TODO: Do we have to care about the result? + buffer.flip(); + + return buffer.hasRemaining(); + } + + private boolean ensureBuffer() { + return buffer.hasRemaining() || (chars.hasRemaining() && fillBuffer()); + } + + @Override + public int read() throws IOException { + if (!ensureBuffer()) { + return -1; + } + + return buffer.get() & 0xff; + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + if (!ensureBuffer()) { + return -1; + } + + int count = Math.min(buffer.remaining(), len); + buffer.get(b, off, count); + return count; + } + + @Override + public long skip(long len) throws IOException { + if (!ensureBuffer()) { + return -1; + } + + int count = (int) Math.min(buffer.remaining(), len); + int position = buffer.position(); + buffer.position(position + count); + return count; + } + + @Override + public int available() throws IOException { + return buffer.remaining(); + } +} diff --git a/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/io/enc/DeflateEncoder.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/io/enc/DeflateEncoder.java index c457a85d..5e11f5b9 100644 --- a/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/io/enc/DeflateEncoder.java +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/io/enc/DeflateEncoder.java @@ -30,6 +30,7 @@ package com.twelvemonkeys.io.enc; import java.io.OutputStream; import java.io.IOException; +import java.nio.ByteBuffer; import java.util.zip.Deflater; /** @@ -50,7 +51,6 @@ final class DeflateEncoder implements Encoder { private final byte[] buffer = new byte[1024]; public DeflateEncoder() { -// this(new Deflater()); this(new Deflater(Deflater.DEFAULT_COMPRESSION, true)); // TODO: Should we use "no wrap"? } @@ -62,12 +62,12 @@ final class DeflateEncoder implements Encoder { deflater = pDeflater; } - public void encode(final OutputStream pStream, final byte[] pBuffer, final int pOffset, final int pLength) + public void encode(final OutputStream stream, ByteBuffer buffer) throws IOException { System.out.println("DeflateEncoder.encode"); - deflater.setInput(pBuffer, pOffset, pLength); - flushInputToStream(pStream); + deflater.setInput(buffer.array(), buffer.arrayOffset() + buffer.position(), buffer.remaining()); + flushInputToStream(stream); } private void flushInputToStream(final OutputStream pStream) throws IOException { diff --git a/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/io/enc/InflateDecoder.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/io/enc/InflateDecoder.java index 438bdbba..eaeac33e 100644 --- a/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/io/enc/InflateDecoder.java +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/io/enc/InflateDecoder.java @@ -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; import java.util.zip.DataFormatException; import java.util.zip.Inflater; @@ -75,17 +76,17 @@ final class InflateDecoder implements Decoder { buffer = new byte[1024]; } - public int decode(final InputStream pStream, final byte[] pBuffer) throws IOException { + public int decode(final InputStream stream, final ByteBuffer buffer) throws IOException { try { int decoded; - while ((decoded = inflater.inflate(pBuffer, 0, pBuffer.length)) == 0) { + while ((decoded = inflater.inflate(buffer.array(), buffer.arrayOffset(), buffer.capacity())) == 0) { if (inflater.finished() || inflater.needsDictionary()) { return 0; } if (inflater.needsInput()) { - fill(pStream); + fill(stream); } } diff --git a/common/common-io/src/main/java/com/twelvemonkeys/net/AuthenticatorFilter.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/net/AuthenticatorFilter.java similarity index 97% rename from common/common-io/src/main/java/com/twelvemonkeys/net/AuthenticatorFilter.java rename to sandbox/sandbox-common/src/main/java/com/twelvemonkeys/net/AuthenticatorFilter.java index 6a3f993f..21171d22 100755 --- a/common/common-io/src/main/java/com/twelvemonkeys/net/AuthenticatorFilter.java +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/net/AuthenticatorFilter.java @@ -1,46 +1,45 @@ -/* - * 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. - */ - -package com.twelvemonkeys.net; - -import java.net.*; - -/** - * Interface for filtering Authenticator requests, used by the - * SimpleAuthenticator. - * - * @see SimpleAuthenticator - * @see java.net.Authenticator - * - * @author Harald Kuhr - * @version 1.0 - */ -public interface AuthenticatorFilter { - public boolean accept(InetAddress pAddress, int pPort, String pProtocol, String pPrompt, String pScheme); - -} +/* + * 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. + */ + +package com.twelvemonkeys.net; + +import java.net.*; + +/** + * Interface for filtering Authenticator requests, used by the + * SimpleAuthenticator. + * + * @see SimpleAuthenticator + * @see java.net.Authenticator + * + * @author Harald Kuhr + * @version 1.0 + */ +public interface AuthenticatorFilter { + public boolean accept(InetAddress pAddress, int pPort, String pProtocol, String pPrompt, String pScheme); +} diff --git a/common/common-io/src/main/java/com/twelvemonkeys/net/BASE64.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/net/BASE64.java similarity index 97% rename from common/common-io/src/main/java/com/twelvemonkeys/net/BASE64.java rename to sandbox/sandbox-common/src/main/java/com/twelvemonkeys/net/BASE64.java index 0ebbd67d..4d4346f8 100755 --- a/common/common-io/src/main/java/com/twelvemonkeys/net/BASE64.java +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/net/BASE64.java @@ -1,144 +1,143 @@ -/* - * 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. - */ - -package com.twelvemonkeys.net; - -import com.twelvemonkeys.io.*; -import com.twelvemonkeys.io.enc.Base64Decoder; -import com.twelvemonkeys.io.enc.DecoderStream; - -import java.io.*; - - -/** - * This class does BASE64 encoding (and decoding). - * - * @author unascribed - * @author last modified by $Author: haku $ - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/BASE64.java#1 $ - * @deprecated Use {@link com.twelvemonkeys.io.enc.Base64Encoder}/{@link Base64Decoder} instead - */ -class BASE64 { - - /** - * This array maps the characters to their 6 bit values - */ - private final static char[] 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 - 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', // 2 - 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', // 3 - 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', // 4 - 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', // 5 - 'w', 'x', 'y', 'z', '0', '1', '2', '3', // 6 - '4', '5', '6', '7', '8', '9', '+', '/' // 7 - }; - - /** - * Encodes the input data using the standard base64 encoding scheme. - * - * @param pData the bytes to encode to base64 - * @return a string with base64 encoded data - */ - public static String encode(byte[] pData) { - int offset = 0; - int len; - StringBuilder buf = new StringBuilder(); - - while ((pData.length - offset) > 0) { - byte a, b, c; - if ((pData.length - offset) > 2) { - len = 3; - } - else { - len = pData.length - offset; - } - - switch (len) { - case 1: - a = pData[offset]; - b = 0; - buf.append(PEM_ARRAY[(a >>> 2) & 0x3F]); - buf.append(PEM_ARRAY[((a << 4) & 0x30) + ((b >>> 4) & 0xf)]); - buf.append('='); - buf.append('='); - offset++; - break; - case 2: - a = pData[offset]; - b = pData[offset + 1]; - c = 0; - buf.append(PEM_ARRAY[(a >>> 2) & 0x3F]); - buf.append(PEM_ARRAY[((a << 4) & 0x30) + ((b >>> 4) & 0xf)]); - buf.append(PEM_ARRAY[((b << 2) & 0x3c) + ((c >>> 6) & 0x3)]); - buf.append('='); - offset += offset + 2; // ??? - break; - default: - a = pData[offset]; - b = pData[offset + 1]; - c = pData[offset + 2]; - buf.append(PEM_ARRAY[(a >>> 2) & 0x3F]); - buf.append(PEM_ARRAY[((a << 4) & 0x30) + ((b >>> 4) & 0xf)]); - buf.append(PEM_ARRAY[((b << 2) & 0x3c) + ((c >>> 6) & 0x3)]); - buf.append(PEM_ARRAY[c & 0x3F]); - offset = offset + 3; - break; - } - - } - return buf.toString(); - } - - public static byte[] decode(String pData) throws IOException { - InputStream in = new DecoderStream(new ByteArrayInputStream(pData.getBytes()), new Base64Decoder()); - ByteArrayOutputStream bytes = new FastByteArrayOutputStream(pData.length() * 3); - FileUtil.copy(in, bytes); - - return bytes.toByteArray(); - } - - //private final static sun.misc.BASE64Decoder DECODER = new sun.misc.BASE64Decoder(); - - public static void main(String[] pArgs) throws IOException { - if (pArgs.length == 1) { - System.out.println(encode(pArgs[0].getBytes())); - } - else - if (pArgs.length == 2 && ("-d".equals(pArgs[0]) || "--decode".equals(pArgs[0]))) - { - System.out.println(new String(decode(pArgs[1]))); - } - else { - System.err.println("BASE64 [ -d | --decode ] arg"); - System.err.println("Encodes or decodes a given string"); - System.exit(5); - } - } +/* + * 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. + */ + +package com.twelvemonkeys.net; + +import com.twelvemonkeys.io.*; +import com.twelvemonkeys.io.enc.Base64Decoder; +import com.twelvemonkeys.io.enc.DecoderStream; + +import java.io.*; + + +/** + * This class does BASE64 encoding (and decoding). + * + * @author unascribed + * @author last modified by $Author: haku $ + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/BASE64.java#1 $ + * @deprecated Use {@link com.twelvemonkeys.io.enc.Base64Encoder}/{@link Base64Decoder} instead + */ +class BASE64 { + /** + * This array maps the characters to their 6 bit values + */ + private final static char[] 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 + 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', // 2 + 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', // 3 + 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', // 4 + 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', // 5 + 'w', 'x', 'y', 'z', '0', '1', '2', '3', // 6 + '4', '5', '6', '7', '8', '9', '+', '/' // 7 + }; + + /** + * Encodes the input data using the standard base64 encoding scheme. + * + * @param pData the bytes to encode to base64 + * @return a string with base64 encoded data + */ + public static String encode(byte[] pData) { + int offset = 0; + int len; + StringBuilder buf = new StringBuilder(); + + while ((pData.length - offset) > 0) { + byte a, b, c; + if ((pData.length - offset) > 2) { + len = 3; + } + else { + len = pData.length - offset; + } + + switch (len) { + case 1: + a = pData[offset]; + b = 0; + buf.append(PEM_ARRAY[(a >>> 2) & 0x3F]); + buf.append(PEM_ARRAY[((a << 4) & 0x30) + ((b >>> 4) & 0xf)]); + buf.append('='); + buf.append('='); + offset++; + break; + case 2: + a = pData[offset]; + b = pData[offset + 1]; + c = 0; + buf.append(PEM_ARRAY[(a >>> 2) & 0x3F]); + buf.append(PEM_ARRAY[((a << 4) & 0x30) + ((b >>> 4) & 0xf)]); + buf.append(PEM_ARRAY[((b << 2) & 0x3c) + ((c >>> 6) & 0x3)]); + buf.append('='); + offset += offset + 2; // ??? + break; + default: + a = pData[offset]; + b = pData[offset + 1]; + c = pData[offset + 2]; + buf.append(PEM_ARRAY[(a >>> 2) & 0x3F]); + buf.append(PEM_ARRAY[((a << 4) & 0x30) + ((b >>> 4) & 0xf)]); + buf.append(PEM_ARRAY[((b << 2) & 0x3c) + ((c >>> 6) & 0x3)]); + buf.append(PEM_ARRAY[c & 0x3F]); + offset = offset + 3; + break; + } + + } + return buf.toString(); + } + + public static byte[] decode(String pData) throws IOException { + InputStream in = new DecoderStream(new ByteArrayInputStream(pData.getBytes()), new Base64Decoder()); + ByteArrayOutputStream bytes = new FastByteArrayOutputStream(pData.length() * 3); + FileUtil.copy(in, bytes); + + return bytes.toByteArray(); + } + + //private final static sun.misc.BASE64Decoder DECODER = new sun.misc.BASE64Decoder(); + + public static void main(String[] pArgs) throws IOException { + if (pArgs.length == 1) { + System.out.println(encode(pArgs[0].getBytes())); + } + else + if (pArgs.length == 2 && ("-d".equals(pArgs[0]) || "--decode".equals(pArgs[0]))) + { + System.out.println(new String(decode(pArgs[1]))); + } + else { + System.err.println("BASE64 [ -d | --decode ] arg"); + System.err.println("Encodes or decodes a given string"); + System.exit(5); + } + } } \ No newline at end of file diff --git a/common/common-io/src/main/java/com/twelvemonkeys/net/HttpURLConnection.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/net/HttpURLConnection.java similarity index 97% rename from common/common-io/src/main/java/com/twelvemonkeys/net/HttpURLConnection.java rename to sandbox/sandbox-common/src/main/java/com/twelvemonkeys/net/HttpURLConnection.java index bd337235..62119c2b 100755 --- a/common/common-io/src/main/java/com/twelvemonkeys/net/HttpURLConnection.java +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/net/HttpURLConnection.java @@ -1,1102 +1,1101 @@ -/* - * 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. - */ - -package com.twelvemonkeys.net; - -import com.twelvemonkeys.lang.StringUtil; - -import java.io.*; -import java.net.*; -import java.util.*; - -/** - * A URLConnection with support for HTTP-specific features. See - * the spec for details. - * This version also supports read and connect timeouts, making it more useful - * for clients with limitted time. - *

- * Note that the timeouts are created on the socket level, and that - *

- * Note: This class should now work as expected, but it need more testing before - * it can enter production release. - *
- * --.k - * - * @author Harald Kuhr - * @author last modified by $Author: haku $ - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/net/HttpURLConnection.java#1 $ - * @todo Write JUnit TestCase - * @todo ConnectionMananger! - * @see RFC 2616 - */ -public class HttpURLConnection extends java.net.HttpURLConnection { - - /** - * HTTP Status-Code 307: Temporary Redirect - */ - public final static int HTTP_REDIRECT = 307; - private final static int HTTP_DEFAULT_PORT = 80; - private final static String HTTP_HEADER_END = "\r\n\r\n"; - private static final String HEADER_WWW_AUTH = "WWW-Authenticate"; - private final static int BUF_SIZE = 8192; - private int maxRedirects = (System.getProperty("http.maxRedirects") != null) - ? Integer.parseInt(System.getProperty("http.maxRedirects")) - : 20; - protected int timeout = -1; - protected int connectTimeout = -1; - private Socket socket = null; - protected InputStream errorStream = null; - protected InputStream inputStream = null; - protected OutputStream outputStream = null; - private String[] responseHeaders = null; - protected Properties responseHeaderFields = null; - protected Properties requestProperties = new Properties(); - - /** - * Creates a HttpURLConnection. - * - * @param pURL the URL to connect to. - */ - protected HttpURLConnection(URL pURL) { - this(pURL, 0, 0); - } - - /** - * Creates a HttpURLConnection with a given read and connect timeout. - * A timeout value of zero is interpreted as an - * infinite timeout. - * - * @param pURL the URL to connect to. - * @param pTimeout the maximum time the socket will block for read - * and connect operations. - */ - protected HttpURLConnection(URL pURL, int pTimeout) { - this(pURL, pTimeout, pTimeout); - } - - /** - * Creates a HttpURLConnection with a given read and connect timeout. - * A timeout value of zero is interpreted as an - * infinite timeout. - * - * @param pURL the URL to connect to. - * @param pTimeout the maximum time the socket will block for read - * operations. - * @param pConnectTimeout the maximum time the socket will block for - * connection. - */ - protected HttpURLConnection(URL pURL, int pTimeout, int pConnectTimeout) { - super(pURL); - setTimeout(pTimeout); - connectTimeout = pConnectTimeout; - } - - /** - * Sets the general request property. If a property with the key already - * exists, overwrite its value with the new value. - *

- *

NOTE: HTTP requires all request properties which can - * legally have multiple instances with the same key - * to use a comma-seperated list syntax which enables multiple - * properties to be appended into a single property. - * - * @param pKey the keyword by which the request is known - * (e.g., "{@code accept}"). - * @param pValue the value associated with it. - * @see #getRequestProperty(java.lang.String) - */ - public void setRequestProperty(String pKey, String pValue) { - if (connected) { - throw new IllegalAccessError("Already connected"); - } - String oldValue = requestProperties.getProperty(pKey); - - if (oldValue == null) { - requestProperties.setProperty(pKey, pValue); - } - else { - requestProperties.setProperty(pKey, oldValue + ", " + pValue); - } - } - - /** - * Returns the value of the named general request property for this - * connection. - * - * @param pKey the keyword by which the request is known (e.g., "accept"). - * @return the value of the named general request property for this - * connection. - * @see #setRequestProperty(java.lang.String, java.lang.String) - */ - public String getRequestProperty(String pKey) { - if (connected) { - throw new IllegalAccessError("Already connected"); - } - return requestProperties.getProperty(pKey); - } - - /** - * Gets HTTP response status from responses like: - *

-     * HTTP/1.0 200 OK
-     * HTTP/1.0 401 Unauthorized
-     * 
- * Extracts the ints 200 and 401 respectively. - * Returns -1 if none can be discerned - * from the response (i.e., the response is not valid HTTP). - *

- * - * - * @return the HTTP Status-Code - * @throws IOException if an error occurred connecting to the server. - */ - public int getResponseCode() throws IOException { - if (responseCode != -1) { - return responseCode; - } - - // Make sure we've gotten the headers - getInputStream(); - String resp = getHeaderField(0); - - // should have no leading/trailing LWS - // expedite the typical case by assuming it has the - // form "HTTP/1.x 2XX " - int ind; - - try { - ind = resp.indexOf(' '); - while (resp.charAt(ind) == ' ') { - ind++; - } - responseCode = Integer.parseInt(resp.substring(ind, ind + 3)); - responseMessage = resp.substring(ind + 4).trim(); - return responseCode; - } - catch (Exception e) { - return responseCode; - } - } - - /** - * Returns the name of the specified header field. - * - * @param pName the name of a header field. - * @return the value of the named header field, or {@code null} - * if there is no such field in the header. - */ - public String getHeaderField(String pName) { - return responseHeaderFields.getProperty(StringUtil.toLowerCase(pName)); - } - - /** - * Returns the value for the {@code n}th header field. - * It returns {@code null} if there are fewer than - * {@code n} fields. - *

- * This method can be used in conjunction with the - * {@code getHeaderFieldKey} method to iterate through all - * the headers in the message. - * - * @param pIndex an index. - * @return the value of the {@code n}th header field. - * @see java.net.URLConnection#getHeaderFieldKey(int) - */ - public String getHeaderField(int pIndex) { - // TODO: getInputStream() first, to make sure we have header fields - if (pIndex >= responseHeaders.length) { - return null; - } - String field = responseHeaders[pIndex]; - - // pIndex == 0, means the response code etc (i.e. "HTTP/1.1 200 OK"). - if ((pIndex == 0) || (field == null)) { - return field; - } - int idx = field.indexOf(':'); - - return ((idx > 0) - ? field.substring(idx).trim() - : ""); // TODO: "" or null? - } - - /** - * Returns the key for the {@code n}th header field. - * - * @param pIndex an index. - * @return the key for the {@code n}th header field, - * or {@code null} if there are fewer than {@code n} - * fields. - */ - public String getHeaderFieldKey(int pIndex) { - // TODO: getInputStream() first, to make sure we have header fields - if (pIndex >= responseHeaders.length) { - return null; - } - String field = responseHeaders[pIndex]; - - if (StringUtil.isEmpty(field)) { - return null; - } - int idx = field.indexOf(':'); - - return StringUtil.toLowerCase(((idx > 0) - ? field.substring(0, idx) - : field)); - } - - /** - * Sets the read timeout for the undelying socket. - * A timeout of zero is interpreted as an - * infinite timeout. - * - * @param pTimeout the maximum time the socket will block for read - * operations, in milliseconds. - */ - public void setTimeout(int pTimeout) { - if (pTimeout < 0) { // Must be positive - throw new IllegalArgumentException("Timeout must be positive."); - } - timeout = pTimeout; - if (socket != null) { - try { - socket.setSoTimeout(pTimeout); - } - catch (SocketException se) { - // Not much to do about that... - } - } - } - - /** - * Gets the read timeout for the undelying socket. - * - * @return the maximum time the socket will block for read operations, in - * milliseconds. - * The default value is zero, which is interpreted as an - * infinite timeout. - */ - public int getTimeout() { - - try { - return ((socket != null) - ? socket.getSoTimeout() - : timeout); - } - catch (SocketException se) { - return timeout; - } - } - - /** - * Returns an input stream that reads from this open connection. - * - * @return an input stream that reads from this open connection. - * @throws IOException if an I/O error occurs while - * creating the input stream. - */ - public synchronized InputStream getInputStream() throws IOException { - if (!connected) { - connect(); - } - - // Nothing to return - if (responseCode == HTTP_NOT_FOUND) { - throw new FileNotFoundException(url.toString()); - } - int length; - - if (inputStream == null) { - return null; - } - - // "De-chunk" the output stream - else if ("chunked".equalsIgnoreCase(getHeaderField("Transfer-Encoding"))) { - if (!(inputStream instanceof ChunkedInputStream)) { - inputStream = new ChunkedInputStream(inputStream); - } - } - - // Make sure we don't wait forever, if the content-length is known - else if ((length = getHeaderFieldInt("Content-Length", -1)) >= 0) { - if (!(inputStream instanceof FixedLengthInputStream)) { - inputStream = new FixedLengthInputStream(inputStream, length); - } - } - return inputStream; - } - - /** - * Returns an output stream that writes to this connection. - * - * @return an output stream that writes to this connection. - * @throws IOException if an I/O error occurs while - * creating the output stream. - */ - public synchronized OutputStream getOutputStream() throws IOException { - - if (!connected) { - connect(); - } - return outputStream; - } - - /** - * Indicates that other requests to the server - * are unlikely in the near future. Calling disconnect() - * should not imply that this HttpURLConnection - * instance can be reused for other requests. - */ - public void disconnect() { - if (socket != null) { - try { - socket.close(); - } - catch (IOException ioe) { - - // Does not matter, I guess. - } - socket = null; - } - connected = false; - } - - /** - * Internal connect method. - */ - private void connect(final URL pURL, PasswordAuthentication pAuth, String pAuthType, int pRetries) throws IOException { - // Find correct port - final int port = (pURL.getPort() > 0) - ? pURL.getPort() - : HTTP_DEFAULT_PORT; - - // Create socket if we don't have one - if (socket == null) { - //socket = new Socket(pURL.getHost(), port); // Blocks... - socket = createSocket(pURL, port, connectTimeout); - socket.setSoTimeout(timeout); - } - - // Get Socket output stream - OutputStream os = socket.getOutputStream(); - - // Connect using HTTP - writeRequestHeaders(os, pURL, method, requestProperties, usingProxy(), pAuth, pAuthType); - - // Get response input stream - InputStream sis = socket.getInputStream(); - BufferedInputStream is = new BufferedInputStream(sis); - - // Detatch reponse headers from reponse input stream - InputStream header = detatchResponseHeader(is); - - // Parse headers and set response code/message - responseHeaders = parseResponseHeader(header); - responseHeaderFields = parseHeaderFields(responseHeaders); - - //System.err.println("Headers fields:"); - //responseHeaderFields.list(System.err); - // Test HTTP response code, to see if further action is needed - switch (getResponseCode()) { - case HTTP_OK: - // 200 OK - inputStream = is; - errorStream = null; - break; - - /* - case HTTP_PROXY_AUTH: - // 407 Proxy Authentication Required - */ - case HTTP_UNAUTHORIZED: - // 401 Unauthorized - // Set authorization and try again.. Slightly more compatible - responseCode = -1; - - // IS THIS REDIRECTION?? - //if (instanceFollowRedirects) { ??? - String auth = getHeaderField(HEADER_WWW_AUTH); - - // Missing WWW-Authenticate header for 401 response is an error - if (StringUtil.isEmpty(auth)) { - throw new ProtocolException("Missing \"" + HEADER_WWW_AUTH + "\" header for response: 401 " + responseMessage); - } - - // Get real mehtod from WWW-Authenticate header - int SP = auth.indexOf(" "); - String method; - String realm = null; - - if (SP >= 0) { - method = auth.substring(0, SP); - if (auth.length() >= SP + 7) { - realm = auth.substring(SP + 7); // " realm=".lenght() == 7 - } - - // else no realm - } - else { - // Default mehtod is Basic - method = SimpleAuthenticator.BASIC; - } - - // Get PasswordAuthentication - PasswordAuthentication pa = Authenticator.requestPasswordAuthentication(NetUtil.createInetAddressFromURL(pURL), port, - pURL.getProtocol(), realm, method); - - // Avoid infinite loop - if (pRetries++ <= 0) { - throw new ProtocolException("Server redirected too many times (" + maxRedirects + ") (Authentication required: " + auth + ")"); // This is what sun.net.www.protocol.http.HttpURLConnection does - } - else if (pa != null) { - connect(pURL, pa, method, pRetries); - } - break; - case HTTP_MOVED_PERM: - // 301 Moved Permanently - case HTTP_MOVED_TEMP: - // 302 Found - case HTTP_SEE_OTHER: - // 303 See Other - /* - case HTTP_USE_PROXY: - // 305 Use Proxy - // How do we handle this? - */ - case HTTP_REDIRECT: - // 307 Temporary Redirect - //System.err.println("Redirecting " + getResponseCode()); - if (instanceFollowRedirects) { - // Redirect - responseCode = -1; // Because of the java.net.URLConnection - - // getResponseCode implementation... - // --- - // I think redirects must be get? - //setRequestMethod("GET"); - // --- - String location = getHeaderField("Location"); - URL newLoc = new URL(pURL, location); - - // Test if we can reuse the Socket - if (!(newLoc.getAuthority().equals(pURL.getAuthority()) && (newLoc.getPort() == pURL.getPort()))) { - socket.close(); // Close the socket, won't need it anymore - socket = null; - } - if (location != null) { - //System.err.println("Redirecting to " + location); - // Avoid infinite loop - if (--pRetries <= 0) { - throw new ProtocolException("Server redirected too many times (5)"); - } - else { - connect(newLoc, pAuth, pAuthType, pRetries); - } - } - break; - } - - // ...else, fall through default (if no Location: header) - default : - // Not 200 OK, or any of the redirect responses - // Probably an error... - errorStream = is; - inputStream = null; - } - - // --- Need rethinking... - // No further questions, let the Socket wait forever (until the server - // closes the connection) - //socket.setSoTimeout(0); - // Probably not... The timeout should only kick if the read BLOCKS. - // Shutdown output, meaning any writes to the outputstream below will - // probably fail... - //socket.shutdownOutput(); - // Not a good idea at all... POSTs need the outputstream to send the - // form-data. - // --- /Need rethinking. - outputStream = os; - } - - private static interface SocketConnector extends Runnable { - - /** - * Method getSocket - * - * @return the socket - * @throws IOException - */ - public Socket getSocket() throws IOException; - } - - /** - * Creates a socket to the given URL and port, with the given connect - * timeout. If the socket waits more than the given timout to connect, - * an ConnectException is thrown. - * - * @param pURL the URL to connect to - * @param pPort the port to connect to - * @param pConnectTimeout the connect timeout - * @return the created Socket. - * @throws ConnectException if the connection is refused or otherwise - * times out. - * @throws UnknownHostException if the IP address of the host could not be - * determined. - * @throws IOException if an I/O error occurs when creating the socket. - * @todo Move this code to a SocetImpl or similar? - * @see Socket#Socket(String,int) - */ - private Socket createSocket(final URL pURL, final int pPort, int pConnectTimeout) throws IOException { - Socket socket; - final Object current = this; - SocketConnector connector; - Thread t = new Thread(connector = new SocketConnector() { - - private IOException mConnectException = null; - private Socket mLocalSocket = null; - - public Socket getSocket() throws IOException { - - if (mConnectException != null) { - throw mConnectException; - } - return mLocalSocket; - } - - // Run method - public void run() { - - try { - mLocalSocket = new Socket(pURL.getHost(), pPort); // Blocks... - } - catch (IOException ioe) { - - // Store this exception for later - mConnectException = ioe; - } - - // Signal that we are done - synchronized (current) { - current.notify(); - } - } - }); - - t.start(); - - // Wait for connect - synchronized (this) { - try { - - /// Only wait if thread is alive! - if (t.isAlive()) { - if (pConnectTimeout > 0) { - wait(pConnectTimeout); - } - else { - wait(); - } - } - } - catch (InterruptedException ie) { - - // Continue excecution on interrupt? Hmmm.. - } - } - - // Throw exception if the socket didn't connect fast enough - if ((socket = connector.getSocket()) == null) { - throw new ConnectException("Socket connect timed out!"); - } - return socket; - } - - /** - * Opens a communications link to the resource referenced by this - * URL, if such a connection has not already been established. - *

- * If the {@code connect} method is called when the connection - * has already been opened (indicated by the {@code connected} - * field having the value {@code true}), the call is ignored. - *

- * URLConnection objects go through two phases: first they are - * created, then they are connected. After being created, and - * before being connected, various options can be specified - * (e.g., doInput and UseCaches). After connecting, it is an - * error to try to set them. Operations that depend on being - * connected, like getContentLength, will implicitly perform the - * connection, if necessary. - * - * @throws IOException if an I/O error occurs while opening the - * connection. - * @see java.net.URLConnection#connected - * @see RFC 2616 - */ - public void connect() throws IOException { - if (connected) { - return; // Ignore - } - connected = true; - connect(url, null, null, maxRedirects); - } - - /** - * TODO: Proxy support is still missing. - * - * @return this method returns false, as proxy suport is not implemented. - */ - public boolean usingProxy() { - return false; - } - - /** - * Writes the HTTP request headers, for HTTP GET method. - * - * @see RFC 2616 - */ - private static void writeRequestHeaders(OutputStream pOut, URL pURL, String pMethod, Properties pProps, boolean pUsingProxy, - PasswordAuthentication pAuth, String pAuthType) { - PrintWriter out = new PrintWriter(pOut, true); // autoFlush - - if (!pUsingProxy) { - out.println(pMethod + " " + (!StringUtil.isEmpty(pURL.getPath()) - ? pURL.getPath() - : "/") + ((pURL.getQuery() != null) - ? "?" + pURL.getQuery() - : "") + " HTTP/1.1"); // HTTP/1.1 - - // out.println("Connection: close"); // No persistent connections yet - - /* - System.err.println(pMethod + " " - + (!StringUtil.isEmpty(pURL.getPath()) ? pURL.getPath() : "/") - + (pURL.getQuery() != null ? "?" + pURL.getQuery() : "") - + " HTTP/1.1"); // HTTP/1.1 - */ - - // Authority (Host: HTTP/1.1 field, but seems to work for HTTP/1.0) - out.println("Host: " + pURL.getHost() + ((pURL.getPort() != -1) - ? ":" + pURL.getPort() - : "")); - - /* - System.err.println("Host: " + pURL.getHost() - + (pURL.getPort() != -1 ? ":" + pURL.getPort() : "")); - */ - } - else { - - ////-- PROXY (absolute) VERSION - out.println(pMethod + " " + pURL.getProtocol() + "://" + pURL.getHost() + ((pURL.getPort() != -1) - ? ":" + pURL.getPort() - : "") + pURL.getPath() + ((pURL.getQuery() != null) - ? "?" + pURL.getQuery() - : "") + " HTTP/1.1"); - } - - // Check if we have authentication - if (pAuth != null) { - - // If found, set Authorization header - byte[] userPass = (pAuth.getUserName() + ":" + new String(pAuth.getPassword())).getBytes(); - - // "Authorization" ":" credentials - out.println("Authorization: " + pAuthType + " " + BASE64.encode(userPass)); - - /* - System.err.println("Authorization: " + pAuthType + " " - + BASE64.encode(userPass)); - */ - } - - // Iterate over properties - - for (Map.Entry property : pProps.entrySet()) { - out.println(property.getKey() + ": " + property.getValue()); - - //System.err.println(property.getKey() + ": " + property.getValue()); - } - out.println(); // Empty line, marks end of request-header - } - - /** - * Finds the end of the HTTP response header in an array of bytes. - * - * @todo This one's a little dirty... - */ - private static int findEndOfHeader(byte[] pBytes, int pEnd) { - byte[] header = HTTP_HEADER_END.getBytes(); - - // Normal condition, check all bytes - for (int i = 0; i < pEnd - 4; i++) { // Need 4 bytes to match - if ((pBytes[i] == header[0]) && (pBytes[i + 1] == header[1]) && (pBytes[i + 2] == header[2]) && (pBytes[i + 3] == header[3])) { - - //System.err.println("FOUND END OF HEADER!"); - return i + 4; - } - } - - // Check last 3 bytes, to check if we have a partial match - if ((pEnd - 1 >= 0) && (pBytes[pEnd - 1] == header[0])) { - - //System.err.println("FOUND LAST BYTE"); - return -2; // LAST BYTE - } - else if ((pEnd - 2 >= 0) && (pBytes[pEnd - 2] == header[0]) && (pBytes[pEnd - 1] == header[1])) { - - //System.err.println("FOUND LAST TWO BYTES"); - return -3; // LAST TWO BYTES - } - else if ((pEnd - 3 >= 0) && (pBytes[pEnd - 3] == header[0]) && (pBytes[pEnd - 2] == header[1]) && (pBytes[pEnd - 1] == header[2])) { - - //System.err.println("FOUND LAST THREE BYTES"); - return -4; // LAST THREE BYTES - } - return -1; // NO BYTES MATCH - } - - /** - * Reads the header part of the response, and copies it to a different - * InputStream. - */ - private static InputStream detatchResponseHeader(BufferedInputStream pIS) throws IOException { - // Store header in byte array - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - - pIS.mark(BUF_SIZE); - byte[] buffer = new byte[BUF_SIZE]; - int length; - int headerEnd; - - // Read from iput, store in bytes - while ((length = pIS.read(buffer)) != -1) { - - // End of header? - headerEnd = findEndOfHeader(buffer, length); - if (headerEnd >= 0) { - - // Write rest - bytes.write(buffer, 0, headerEnd); - - // Go back to last mark - pIS.reset(); - - // Position stream to right after header, and exit loop - pIS.skip(headerEnd); - break; - } - else if (headerEnd < -1) { - - // Write partial (except matching header bytes) - bytes.write(buffer, 0, length - 4); - - // Go back to last mark - pIS.reset(); - - // Position stream to right before potential header end - pIS.skip(length - 4); - } - else { - - // Write all - bytes.write(buffer, 0, length); - } - - // Can't read more than BUF_SIZE ahead anyway - pIS.mark(BUF_SIZE); - } - return new ByteArrayInputStream(bytes.toByteArray()); - } - - /** - * Pareses the response header fields. - */ - private static Properties parseHeaderFields(String[] pHeaders) { - Properties headers = new Properties(); - - // Get header information - int split; - String field; - String value; - - for (String header : pHeaders) { - //System.err.println(pHeaders[i]); - if ((split = header.indexOf(":")) > 0) { - - // Read & parse..? - field = header.substring(0, split); - value = header.substring(split + 1); - - //System.err.println(field + ": " + value.trim()); - headers.setProperty(StringUtil.toLowerCase(field), value.trim()); - } - } - return headers; - } - - /** - * Parses the response headers. - */ - private static String[] parseResponseHeader(InputStream pIS) throws IOException { - List headers = new ArrayList(); - - // Wrap Stream in Reader - BufferedReader in = new BufferedReader(new InputStreamReader(pIS)); - - // Get response status - String header; - - while ((header = in.readLine()) != null) { - //System.err.println(header); - headers.add(header); - } - return headers.toArray(new String[headers.size()]); - } - - /** - * A FilterInputStream that wraps HTTP streams, with given content-length. - */ - protected static class FixedLengthInputStream extends FilterInputStream { - - private int mBytesLeft = 0; - - protected FixedLengthInputStream(InputStream pIS, int pLength) { - super(pIS); - mBytesLeft = pLength; - } - - public int available() throws IOException { - int available = in.available(); - - return ((available < mBytesLeft) - ? available - : mBytesLeft); - } - - public int read() throws IOException { - if (mBytesLeft-- > 0) { - return in.read(); - } - return -1; - } - - public int read(byte[] pBytes, int pOffset, int pLength) throws IOException { - int read; - - if (mBytesLeft <= 0) { - return -1; // EOF - } - else if (mBytesLeft < pLength) { - - // Read all available - read = in.read(pBytes, pOffset, mBytesLeft); - - //System.err.println("Reading partial: " + read); - mBytesLeft -= read; - return read; - } - - // Just read - read = in.read(pBytes, pOffset, pLength); - - //System.err.println("Reading all avail: " + read); - mBytesLeft -= read; - return read; - } - } - - /** - * A FilterInputStream that wraps HTTP 1.1 "chunked" transfer mode. - */ - protected static class ChunkedInputStream extends FilterInputStream { - - private int mAvailableInCurrentChunk = 0; - - /** - * Creates an input streams that removes the "chunk-headers" and - * makes it look like any other input stream. - */ - protected ChunkedInputStream(InputStream pIS) { - - super(pIS); - if (pIS == null) { - throw new IllegalArgumentException("InputStream may not be null!"); - } - } - - /** - * Returns the number of bytes that can be read from this input stream - * without blocking. - *

- * This version returns whatever is less of in.available() and the - * length of the current chunk. - * - * @return the number of bytes that can be read from the input stream - * without blocking. - * @throws IOException if an I/O error occurs. - * @see #in - */ - public int available() throws IOException { - - if (mAvailableInCurrentChunk == 0) { - mAvailableInCurrentChunk = parseChunkSize(); - } - int realAvail = in.available(); - - return (mAvailableInCurrentChunk < realAvail) - ? mAvailableInCurrentChunk - : realAvail; - } - - /** - * Reads up to len bytes of data from this input stream into an array - * of bytes. This method blocks until some input is available. - *

- * This version will read up to len bytes of data, or as much as is - * available in the current chunk. If there is no more data in the - * curernt chunk, the method will read the size of the next chunk, and - * read from that, until the last chunk is read (a chunk with a size of - * 0). - * - * @param pBytes the buffer into which the data is read. - * @param pOffset the start offset of the data. - * @param pLength the maximum number of bytes read. - * @return the total number of bytes read into the buffer, or -1 if - * there is no more data because the end of the stream has been - * reached. - * @throws IOException if an I/O error occurs. - * @see #in - */ - public int read(byte[] pBytes, int pOffset, int pLength) throws IOException { - - //System.err.println("Avail: " + mAvailableInCurrentChunk - // + " length: " + pLength); - int read; - - if (mAvailableInCurrentChunk == -1) { - return -1; // EOF - } - if (mAvailableInCurrentChunk == 0) { - - //System.err.println("Nothing to read, parsing size!"); - // If nothing is read so far, read chunk header - mAvailableInCurrentChunk = parseChunkSize(); - return read(pBytes, pOffset, pLength); - } - else if (mAvailableInCurrentChunk < pLength) { - - // Read all available - read = in.read(pBytes, pOffset, mAvailableInCurrentChunk); - - //System.err.println("Reading partial: " + read); - mAvailableInCurrentChunk -= read; - return read; - } - - // Just read - read = in.read(pBytes, pOffset, pLength); - - //System.err.println("Reading all avail: " + read); - mAvailableInCurrentChunk -= read; - return read; - } - - /** - * Reads the next byte of data from this input stream. The value byte - * is returned as an int in the range 0 to 255. If no byte is available - * because the end of the stream has been reached, the value -1 is - * returned. This method blocks until input data is available, the end - * of the stream is detected, or an exception is thrown. - *

- * This version reads one byte of data from the current chunk as long - * as there is more data in the chunk. If there is no more data in the - * curernt chunk, the method will read the size of the next chunk, and - * read from that, until the last chunk is read (a chunk with a size of - * 0). - * - * @return the next byte of data, or -1 if the end of the stream is - * reached. - * @see #in - */ - public int read() throws IOException { - - // We have no data, parse chunk header - if (mAvailableInCurrentChunk == -1) { - return -1; - } - else if (mAvailableInCurrentChunk == 0) { - - // Next chunk! - mAvailableInCurrentChunk = parseChunkSize(); - return read(); - } - mAvailableInCurrentChunk--; - return in.read(); - } - - /** - * Reads the chunk size from the chunk header - * {@code chunk-size [SP chunk-extension] CRLF}. - * The chunk-extension is simply discarded. - * - * @return the length of the current chunk, or -1 if the current chunk - * is the last-chunk (a chunk with the size of 0). - */ - protected int parseChunkSize() throws IOException { - - StringBuilder buf = new StringBuilder(); - int b; - - // read chunk-size, chunk-extension (if any) and CRLF - while ((b = in.read()) > 0) { - if ((b == '\r') && (in.read() == '\n')) { // Should be no CR or LF - break; // except for this one... - } - buf.append((char) b); - } - String line = buf.toString(); - - // Happens, as we don't read CRLF off the end of the chunk data... - if (line.length() == 0) { - return 0; - } - - // Discard any chunk-extensions, and read size (HEX). - int spIdx = line.indexOf(' '); - int size = Integer.parseInt(((spIdx >= 0) - ? line.substring(0, spIdx) - : line), 16); - - // This is the last chunk (=EOF) - if (size == 0) { - return -1; - } - return size; - } - } -} +/* + * 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. + */ + +package com.twelvemonkeys.net; + +import com.twelvemonkeys.lang.StringUtil; + +import java.io.*; +import java.net.*; +import java.util.*; + +/** + * A URLConnection with support for HTTP-specific features. See + * the spec for details. + * This version also supports read and connect timeouts, making it more useful + * for clients with limitted time. + *

+ * Note that the timeouts are created on the socket level, and that + *

+ * Note: This class should now work as expected, but it needs more testing before + * it can enter production release. + *
+ * --.k + * + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/net/HttpURLConnection.java#1 $ + * @todo Write JUnit TestCase + * @todo ConnectionMananger! + * @see RFC 2616 + */ +public class HttpURLConnection extends java.net.HttpURLConnection { + /** + * HTTP Status-Code 307: Temporary Redirect + */ + public final static int HTTP_REDIRECT = 307; + private final static int HTTP_DEFAULT_PORT = 80; + private final static String HTTP_HEADER_END = "\r\n\r\n"; + private static final String HEADER_WWW_AUTH = "WWW-Authenticate"; + private final static int BUF_SIZE = 8192; + private int maxRedirects = (System.getProperty("http.maxRedirects") != null) + ? Integer.parseInt(System.getProperty("http.maxRedirects")) + : 20; + protected int timeout = -1; + protected int connectTimeout = -1; + private Socket socket = null; + protected InputStream errorStream = null; + protected InputStream inputStream = null; + protected OutputStream outputStream = null; + private String[] responseHeaders = null; + protected Properties responseHeaderFields = null; + protected Properties requestProperties = new Properties(); + + /** + * Creates a HttpURLConnection. + * + * @param pURL the URL to connect to. + */ + protected HttpURLConnection(URL pURL) { + this(pURL, 0, 0); + } + + /** + * Creates a HttpURLConnection with a given read and connect timeout. + * A timeout value of zero is interpreted as an + * infinite timeout. + * + * @param pURL the URL to connect to. + * @param pTimeout the maximum time the socket will block for read + * and connect operations. + */ + protected HttpURLConnection(URL pURL, int pTimeout) { + this(pURL, pTimeout, pTimeout); + } + + /** + * Creates a HttpURLConnection with a given read and connect timeout. + * A timeout value of zero is interpreted as an + * infinite timeout. + * + * @param pURL the URL to connect to. + * @param pTimeout the maximum time the socket will block for read + * operations. + * @param pConnectTimeout the maximum time the socket will block for + * connection. + */ + protected HttpURLConnection(URL pURL, int pTimeout, int pConnectTimeout) { + super(pURL); + setTimeout(pTimeout); + connectTimeout = pConnectTimeout; + } + + /** + * Sets the general request property. If a property with the key already + * exists, overwrite its value with the new value. + *

+ *

NOTE: HTTP requires all request properties which can + * legally have multiple instances with the same key + * to use a comma-seperated list syntax which enables multiple + * properties to be appended into a single property. + * + * @param pKey the keyword by which the request is known + * (e.g., "{@code accept}"). + * @param pValue the value associated with it. + * @see #getRequestProperty(java.lang.String) + */ + public void setRequestProperty(String pKey, String pValue) { + if (connected) { + throw new IllegalAccessError("Already connected"); + } + String oldValue = requestProperties.getProperty(pKey); + + if (oldValue == null) { + requestProperties.setProperty(pKey, pValue); + } + else { + requestProperties.setProperty(pKey, oldValue + ", " + pValue); + } + } + + /** + * Returns the value of the named general request property for this + * connection. + * + * @param pKey the keyword by which the request is known (e.g., "accept"). + * @return the value of the named general request property for this + * connection. + * @see #setRequestProperty(java.lang.String, java.lang.String) + */ + public String getRequestProperty(String pKey) { + if (connected) { + throw new IllegalAccessError("Already connected"); + } + return requestProperties.getProperty(pKey); + } + + /** + * Gets HTTP response status from responses like: + *

+     * HTTP/1.0 200 OK
+     * HTTP/1.0 401 Unauthorized
+     * 
+ * Extracts the ints 200 and 401 respectively. + * Returns -1 if none can be discerned + * from the response (i.e., the response is not valid HTTP). + *

+ * + * + * @return the HTTP Status-Code + * @throws IOException if an error occurred connecting to the server. + */ + public int getResponseCode() throws IOException { + if (responseCode != -1) { + return responseCode; + } + + // Make sure we've gotten the headers + getInputStream(); + String resp = getHeaderField(0); + + // should have no leading/trailing LWS + // expedite the typical case by assuming it has the + // form "HTTP/1.x 2XX " + int ind; + + try { + ind = resp.indexOf(' '); + while (resp.charAt(ind) == ' ') { + ind++; + } + responseCode = Integer.parseInt(resp.substring(ind, ind + 3)); + responseMessage = resp.substring(ind + 4).trim(); + return responseCode; + } + catch (Exception e) { + return responseCode; + } + } + + /** + * Returns the name of the specified header field. + * + * @param pName the name of a header field. + * @return the value of the named header field, or {@code null} + * if there is no such field in the header. + */ + public String getHeaderField(String pName) { + return responseHeaderFields.getProperty(StringUtil.toLowerCase(pName)); + } + + /** + * Returns the value for the {@code n}th header field. + * It returns {@code null} if there are fewer than + * {@code n} fields. + *

+ * This method can be used in conjunction with the + * {@code getHeaderFieldKey} method to iterate through all + * the headers in the message. + * + * @param pIndex an index. + * @return the value of the {@code n}th header field. + * @see java.net.URLConnection#getHeaderFieldKey(int) + */ + public String getHeaderField(int pIndex) { + // TODO: getInputStream() first, to make sure we have header fields + if (pIndex >= responseHeaders.length) { + return null; + } + String field = responseHeaders[pIndex]; + + // pIndex == 0, means the response code etc (i.e. "HTTP/1.1 200 OK"). + if ((pIndex == 0) || (field == null)) { + return field; + } + int idx = field.indexOf(':'); + + return ((idx > 0) + ? field.substring(idx).trim() + : ""); // TODO: "" or null? + } + + /** + * Returns the key for the {@code n}th header field. + * + * @param pIndex an index. + * @return the key for the {@code n}th header field, + * or {@code null} if there are fewer than {@code n} + * fields. + */ + public String getHeaderFieldKey(int pIndex) { + // TODO: getInputStream() first, to make sure we have header fields + if (pIndex >= responseHeaders.length) { + return null; + } + String field = responseHeaders[pIndex]; + + if (StringUtil.isEmpty(field)) { + return null; + } + int idx = field.indexOf(':'); + + return StringUtil.toLowerCase(((idx > 0) + ? field.substring(0, idx) + : field)); + } + + /** + * Sets the read timeout for the undelying socket. + * A timeout of zero is interpreted as an + * infinite timeout. + * + * @param pTimeout the maximum time the socket will block for read + * operations, in milliseconds. + */ + public void setTimeout(int pTimeout) { + if (pTimeout < 0) { // Must be positive + throw new IllegalArgumentException("Timeout must be positive."); + } + timeout = pTimeout; + if (socket != null) { + try { + socket.setSoTimeout(pTimeout); + } + catch (SocketException se) { + // Not much to do about that... + } + } + } + + /** + * Gets the read timeout for the undelying socket. + * + * @return the maximum time the socket will block for read operations, in + * milliseconds. + * The default value is zero, which is interpreted as an + * infinite timeout. + */ + public int getTimeout() { + + try { + return ((socket != null) + ? socket.getSoTimeout() + : timeout); + } + catch (SocketException se) { + return timeout; + } + } + + /** + * Returns an input stream that reads from this open connection. + * + * @return an input stream that reads from this open connection. + * @throws IOException if an I/O error occurs while + * creating the input stream. + */ + public synchronized InputStream getInputStream() throws IOException { + if (!connected) { + connect(); + } + + // Nothing to return + if (responseCode == HTTP_NOT_FOUND) { + throw new FileNotFoundException(url.toString()); + } + int length; + + if (inputStream == null) { + return null; + } + + // "De-chunk" the output stream + else if ("chunked".equalsIgnoreCase(getHeaderField("Transfer-Encoding"))) { + if (!(inputStream instanceof ChunkedInputStream)) { + inputStream = new ChunkedInputStream(inputStream); + } + } + + // Make sure we don't wait forever, if the content-length is known + else if ((length = getHeaderFieldInt("Content-Length", -1)) >= 0) { + if (!(inputStream instanceof FixedLengthInputStream)) { + inputStream = new FixedLengthInputStream(inputStream, length); + } + } + return inputStream; + } + + /** + * Returns an output stream that writes to this connection. + * + * @return an output stream that writes to this connection. + * @throws IOException if an I/O error occurs while + * creating the output stream. + */ + public synchronized OutputStream getOutputStream() throws IOException { + + if (!connected) { + connect(); + } + return outputStream; + } + + /** + * Indicates that other requests to the server + * are unlikely in the near future. Calling disconnect() + * should not imply that this HttpURLConnection + * instance can be reused for other requests. + */ + public void disconnect() { + if (socket != null) { + try { + socket.close(); + } + catch (IOException ioe) { + + // Does not matter, I guess. + } + socket = null; + } + connected = false; + } + + /** + * Internal connect method. + */ + private void connect(final URL pURL, PasswordAuthentication pAuth, String pAuthType, int pRetries) throws IOException { + // Find correct port + final int port = (pURL.getPort() > 0) + ? pURL.getPort() + : HTTP_DEFAULT_PORT; + + // Create socket if we don't have one + if (socket == null) { + //socket = new Socket(pURL.getHost(), port); // Blocks... + socket = createSocket(pURL, port, connectTimeout); + socket.setSoTimeout(timeout); + } + + // Get Socket output stream + OutputStream os = socket.getOutputStream(); + + // Connect using HTTP + writeRequestHeaders(os, pURL, method, requestProperties, usingProxy(), pAuth, pAuthType); + + // Get response input stream + InputStream sis = socket.getInputStream(); + BufferedInputStream is = new BufferedInputStream(sis); + + // Detatch reponse headers from reponse input stream + InputStream header = detatchResponseHeader(is); + + // Parse headers and set response code/message + responseHeaders = parseResponseHeader(header); + responseHeaderFields = parseHeaderFields(responseHeaders); + + //System.err.println("Headers fields:"); + //responseHeaderFields.list(System.err); + // Test HTTP response code, to see if further action is needed + switch (getResponseCode()) { + case HTTP_OK: + // 200 OK + inputStream = is; + errorStream = null; + break; + + /* + case HTTP_PROXY_AUTH: + // 407 Proxy Authentication Required + */ + case HTTP_UNAUTHORIZED: + // 401 Unauthorized + // Set authorization and try again.. Slightly more compatible + responseCode = -1; + + // IS THIS REDIRECTION?? + //if (instanceFollowRedirects) { ??? + String auth = getHeaderField(HEADER_WWW_AUTH); + + // Missing WWW-Authenticate header for 401 response is an error + if (StringUtil.isEmpty(auth)) { + throw new ProtocolException("Missing \"" + HEADER_WWW_AUTH + "\" header for response: 401 " + responseMessage); + } + + // Get real mehtod from WWW-Authenticate header + int SP = auth.indexOf(" "); + String method; + String realm = null; + + if (SP >= 0) { + method = auth.substring(0, SP); + if (auth.length() >= SP + 7) { + realm = auth.substring(SP + 7); // " realm=".lenght() == 7 + } + + // else no realm + } + else { + // Default mehtod is Basic + method = SimpleAuthenticator.BASIC; + } + + // Get PasswordAuthentication + PasswordAuthentication pa = Authenticator.requestPasswordAuthentication(NetUtil.createInetAddressFromURL(pURL), port, + pURL.getProtocol(), realm, method); + + // Avoid infinite loop + if (pRetries++ <= 0) { + throw new ProtocolException("Server redirected too many times (" + maxRedirects + ") (Authentication required: " + auth + ")"); // This is what sun.net.www.protocol.http.HttpURLConnection does + } + else if (pa != null) { + connect(pURL, pa, method, pRetries); + } + break; + case HTTP_MOVED_PERM: + // 301 Moved Permanently + case HTTP_MOVED_TEMP: + // 302 Found + case HTTP_SEE_OTHER: + // 303 See Other + /* + case HTTP_USE_PROXY: + // 305 Use Proxy + // How do we handle this? + */ + case HTTP_REDIRECT: + // 307 Temporary Redirect + //System.err.println("Redirecting " + getResponseCode()); + if (instanceFollowRedirects) { + // Redirect + responseCode = -1; // Because of the java.net.URLConnection + + // getResponseCode implementation... + // --- + // I think redirects must be get? + //setRequestMethod("GET"); + // --- + String location = getHeaderField("Location"); + URL newLoc = new URL(pURL, location); + + // Test if we can reuse the Socket + if (!(newLoc.getAuthority().equals(pURL.getAuthority()) && (newLoc.getPort() == pURL.getPort()))) { + socket.close(); // Close the socket, won't need it anymore + socket = null; + } + if (location != null) { + //System.err.println("Redirecting to " + location); + // Avoid infinite loop + if (--pRetries <= 0) { + throw new ProtocolException("Server redirected too many times (5)"); + } + else { + connect(newLoc, pAuth, pAuthType, pRetries); + } + } + break; + } + + // ...else, fall through default (if no Location: header) + default : + // Not 200 OK, or any of the redirect responses + // Probably an error... + errorStream = is; + inputStream = null; + } + + // --- Need rethinking... + // No further questions, let the Socket wait forever (until the server + // closes the connection) + //socket.setSoTimeout(0); + // Probably not... The timeout should only kick if the read BLOCKS. + // Shutdown output, meaning any writes to the outputstream below will + // probably fail... + //socket.shutdownOutput(); + // Not a good idea at all... POSTs need the outputstream to send the + // form-data. + // --- /Need rethinking. + outputStream = os; + } + + private static interface SocketConnector extends Runnable { + + /** + * Method getSocket + * + * @return the socket + * @throws IOException + */ + public Socket getSocket() throws IOException; + } + + /** + * Creates a socket to the given URL and port, with the given connect + * timeout. If the socket waits more than the given timout to connect, + * an ConnectException is thrown. + * + * @param pURL the URL to connect to + * @param pPort the port to connect to + * @param pConnectTimeout the connect timeout + * @return the created Socket. + * @throws ConnectException if the connection is refused or otherwise + * times out. + * @throws UnknownHostException if the IP address of the host could not be + * determined. + * @throws IOException if an I/O error occurs when creating the socket. + * @todo Move this code to a SocetImpl or similar? + * @see Socket#Socket(String,int) + */ + private Socket createSocket(final URL pURL, final int pPort, int pConnectTimeout) throws IOException { + Socket socket; + final Object current = this; + SocketConnector connector; + Thread t = new Thread(connector = new SocketConnector() { + + private IOException mConnectException = null; + private Socket mLocalSocket = null; + + public Socket getSocket() throws IOException { + + if (mConnectException != null) { + throw mConnectException; + } + return mLocalSocket; + } + + // Run method + public void run() { + + try { + mLocalSocket = new Socket(pURL.getHost(), pPort); // Blocks... + } + catch (IOException ioe) { + + // Store this exception for later + mConnectException = ioe; + } + + // Signal that we are done + synchronized (current) { + current.notify(); + } + } + }); + + t.start(); + + // Wait for connect + synchronized (this) { + try { + + /// Only wait if thread is alive! + if (t.isAlive()) { + if (pConnectTimeout > 0) { + wait(pConnectTimeout); + } + else { + wait(); + } + } + } + catch (InterruptedException ie) { + + // Continue excecution on interrupt? Hmmm.. + } + } + + // Throw exception if the socket didn't connect fast enough + if ((socket = connector.getSocket()) == null) { + throw new ConnectException("Socket connect timed out!"); + } + return socket; + } + + /** + * Opens a communications link to the resource referenced by this + * URL, if such a connection has not already been established. + *

+ * If the {@code connect} method is called when the connection + * has already been opened (indicated by the {@code connected} + * field having the value {@code true}), the call is ignored. + *

+ * URLConnection objects go through two phases: first they are + * created, then they are connected. After being created, and + * before being connected, various options can be specified + * (e.g., doInput and UseCaches). After connecting, it is an + * error to try to set them. Operations that depend on being + * connected, like getContentLength, will implicitly perform the + * connection, if necessary. + * + * @throws IOException if an I/O error occurs while opening the + * connection. + * @see java.net.URLConnection#connected + * @see RFC 2616 + */ + public void connect() throws IOException { + if (connected) { + return; // Ignore + } + connected = true; + connect(url, null, null, maxRedirects); + } + + /** + * TODO: Proxy support is still missing. + * + * @return this method returns false, as proxy suport is not implemented. + */ + public boolean usingProxy() { + return false; + } + + /** + * Writes the HTTP request headers, for HTTP GET method. + * + * @see RFC 2616 + */ + private static void writeRequestHeaders(OutputStream pOut, URL pURL, String pMethod, Properties pProps, boolean pUsingProxy, + PasswordAuthentication pAuth, String pAuthType) { + PrintWriter out = new PrintWriter(pOut, true); // autoFlush + + if (!pUsingProxy) { + out.println(pMethod + " " + (!StringUtil.isEmpty(pURL.getPath()) + ? pURL.getPath() + : "/") + ((pURL.getQuery() != null) + ? "?" + pURL.getQuery() + : "") + " HTTP/1.1"); // HTTP/1.1 + + // out.println("Connection: close"); // No persistent connections yet + + /* + System.err.println(pMethod + " " + + (!StringUtil.isEmpty(pURL.getPath()) ? pURL.getPath() : "/") + + (pURL.getQuery() != null ? "?" + pURL.getQuery() : "") + + " HTTP/1.1"); // HTTP/1.1 + */ + + // Authority (Host: HTTP/1.1 field, but seems to work for HTTP/1.0) + out.println("Host: " + pURL.getHost() + ((pURL.getPort() != -1) + ? ":" + pURL.getPort() + : "")); + + /* + System.err.println("Host: " + pURL.getHost() + + (pURL.getPort() != -1 ? ":" + pURL.getPort() : "")); + */ + } + else { + + ////-- PROXY (absolute) VERSION + out.println(pMethod + " " + pURL.getProtocol() + "://" + pURL.getHost() + ((pURL.getPort() != -1) + ? ":" + pURL.getPort() + : "") + pURL.getPath() + ((pURL.getQuery() != null) + ? "?" + pURL.getQuery() + : "") + " HTTP/1.1"); + } + + // Check if we have authentication + if (pAuth != null) { + + // If found, set Authorization header + byte[] userPass = (pAuth.getUserName() + ":" + new String(pAuth.getPassword())).getBytes(); + + // "Authorization" ":" credentials + out.println("Authorization: " + pAuthType + " " + BASE64.encode(userPass)); + + /* + System.err.println("Authorization: " + pAuthType + " " + + BASE64.encode(userPass)); + */ + } + + // Iterate over properties + + for (Map.Entry property : pProps.entrySet()) { + out.println(property.getKey() + ": " + property.getValue()); + + //System.err.println(property.getKey() + ": " + property.getValue()); + } + out.println(); // Empty line, marks end of request-header + } + + /** + * Finds the end of the HTTP response header in an array of bytes. + * + * @todo This one's a little dirty... + */ + private static int findEndOfHeader(byte[] pBytes, int pEnd) { + byte[] header = HTTP_HEADER_END.getBytes(); + + // Normal condition, check all bytes + for (int i = 0; i < pEnd - 4; i++) { // Need 4 bytes to match + if ((pBytes[i] == header[0]) && (pBytes[i + 1] == header[1]) && (pBytes[i + 2] == header[2]) && (pBytes[i + 3] == header[3])) { + + //System.err.println("FOUND END OF HEADER!"); + return i + 4; + } + } + + // Check last 3 bytes, to check if we have a partial match + if ((pEnd - 1 >= 0) && (pBytes[pEnd - 1] == header[0])) { + + //System.err.println("FOUND LAST BYTE"); + return -2; // LAST BYTE + } + else if ((pEnd - 2 >= 0) && (pBytes[pEnd - 2] == header[0]) && (pBytes[pEnd - 1] == header[1])) { + + //System.err.println("FOUND LAST TWO BYTES"); + return -3; // LAST TWO BYTES + } + else if ((pEnd - 3 >= 0) && (pBytes[pEnd - 3] == header[0]) && (pBytes[pEnd - 2] == header[1]) && (pBytes[pEnd - 1] == header[2])) { + + //System.err.println("FOUND LAST THREE BYTES"); + return -4; // LAST THREE BYTES + } + return -1; // NO BYTES MATCH + } + + /** + * Reads the header part of the response, and copies it to a different + * InputStream. + */ + private static InputStream detatchResponseHeader(BufferedInputStream pIS) throws IOException { + // Store header in byte array + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + + pIS.mark(BUF_SIZE); + byte[] buffer = new byte[BUF_SIZE]; + int length; + int headerEnd; + + // Read from iput, store in bytes + while ((length = pIS.read(buffer)) != -1) { + + // End of header? + headerEnd = findEndOfHeader(buffer, length); + if (headerEnd >= 0) { + + // Write rest + bytes.write(buffer, 0, headerEnd); + + // Go back to last mark + pIS.reset(); + + // Position stream to right after header, and exit loop + pIS.skip(headerEnd); + break; + } + else if (headerEnd < -1) { + + // Write partial (except matching header bytes) + bytes.write(buffer, 0, length - 4); + + // Go back to last mark + pIS.reset(); + + // Position stream to right before potential header end + pIS.skip(length - 4); + } + else { + + // Write all + bytes.write(buffer, 0, length); + } + + // Can't read more than BUF_SIZE ahead anyway + pIS.mark(BUF_SIZE); + } + return new ByteArrayInputStream(bytes.toByteArray()); + } + + /** + * Pareses the response header fields. + */ + private static Properties parseHeaderFields(String[] pHeaders) { + Properties headers = new Properties(); + + // Get header information + int split; + String field; + String value; + + for (String header : pHeaders) { + //System.err.println(pHeaders[i]); + if ((split = header.indexOf(":")) > 0) { + + // Read & parse..? + field = header.substring(0, split); + value = header.substring(split + 1); + + //System.err.println(field + ": " + value.trim()); + headers.setProperty(StringUtil.toLowerCase(field), value.trim()); + } + } + return headers; + } + + /** + * Parses the response headers. + */ + private static String[] parseResponseHeader(InputStream pIS) throws IOException { + List headers = new ArrayList(); + + // Wrap Stream in Reader + BufferedReader in = new BufferedReader(new InputStreamReader(pIS)); + + // Get response status + String header; + + while ((header = in.readLine()) != null) { + //System.err.println(header); + headers.add(header); + } + return headers.toArray(new String[headers.size()]); + } + + /** + * A FilterInputStream that wraps HTTP streams, with given content-length. + */ + protected static class FixedLengthInputStream extends FilterInputStream { + + private int mBytesLeft = 0; + + protected FixedLengthInputStream(InputStream pIS, int pLength) { + super(pIS); + mBytesLeft = pLength; + } + + public int available() throws IOException { + int available = in.available(); + + return ((available < mBytesLeft) + ? available + : mBytesLeft); + } + + public int read() throws IOException { + if (mBytesLeft-- > 0) { + return in.read(); + } + return -1; + } + + public int read(byte[] pBytes, int pOffset, int pLength) throws IOException { + int read; + + if (mBytesLeft <= 0) { + return -1; // EOF + } + else if (mBytesLeft < pLength) { + + // Read all available + read = in.read(pBytes, pOffset, mBytesLeft); + + //System.err.println("Reading partial: " + read); + mBytesLeft -= read; + return read; + } + + // Just read + read = in.read(pBytes, pOffset, pLength); + + //System.err.println("Reading all avail: " + read); + mBytesLeft -= read; + return read; + } + } + + /** + * A FilterInputStream that wraps HTTP 1.1 "chunked" transfer mode. + */ + protected static class ChunkedInputStream extends FilterInputStream { + + private int mAvailableInCurrentChunk = 0; + + /** + * Creates an input streams that removes the "chunk-headers" and + * makes it look like any other input stream. + */ + protected ChunkedInputStream(InputStream pIS) { + + super(pIS); + if (pIS == null) { + throw new IllegalArgumentException("InputStream may not be null!"); + } + } + + /** + * Returns the number of bytes that can be read from this input stream + * without blocking. + *

+ * This version returns whatever is less of in.available() and the + * length of the current chunk. + * + * @return the number of bytes that can be read from the input stream + * without blocking. + * @throws IOException if an I/O error occurs. + * @see #in + */ + public int available() throws IOException { + + if (mAvailableInCurrentChunk == 0) { + mAvailableInCurrentChunk = parseChunkSize(); + } + int realAvail = in.available(); + + return (mAvailableInCurrentChunk < realAvail) + ? mAvailableInCurrentChunk + : realAvail; + } + + /** + * Reads up to len bytes of data from this input stream into an array + * of bytes. This method blocks until some input is available. + *

+ * This version will read up to len bytes of data, or as much as is + * available in the current chunk. If there is no more data in the + * curernt chunk, the method will read the size of the next chunk, and + * read from that, until the last chunk is read (a chunk with a size of + * 0). + * + * @param pBytes the buffer into which the data is read. + * @param pOffset the start offset of the data. + * @param pLength the maximum number of bytes read. + * @return the total number of bytes read into the buffer, or -1 if + * there is no more data because the end of the stream has been + * reached. + * @throws IOException if an I/O error occurs. + * @see #in + */ + public int read(byte[] pBytes, int pOffset, int pLength) throws IOException { + + //System.err.println("Avail: " + mAvailableInCurrentChunk + // + " length: " + pLength); + int read; + + if (mAvailableInCurrentChunk == -1) { + return -1; // EOF + } + if (mAvailableInCurrentChunk == 0) { + + //System.err.println("Nothing to read, parsing size!"); + // If nothing is read so far, read chunk header + mAvailableInCurrentChunk = parseChunkSize(); + return read(pBytes, pOffset, pLength); + } + else if (mAvailableInCurrentChunk < pLength) { + + // Read all available + read = in.read(pBytes, pOffset, mAvailableInCurrentChunk); + + //System.err.println("Reading partial: " + read); + mAvailableInCurrentChunk -= read; + return read; + } + + // Just read + read = in.read(pBytes, pOffset, pLength); + + //System.err.println("Reading all avail: " + read); + mAvailableInCurrentChunk -= read; + return read; + } + + /** + * Reads the next byte of data from this input stream. The value byte + * is returned as an int in the range 0 to 255. If no byte is available + * because the end of the stream has been reached, the value -1 is + * returned. This method blocks until input data is available, the end + * of the stream is detected, or an exception is thrown. + *

+ * This version reads one byte of data from the current chunk as long + * as there is more data in the chunk. If there is no more data in the + * curernt chunk, the method will read the size of the next chunk, and + * read from that, until the last chunk is read (a chunk with a size of + * 0). + * + * @return the next byte of data, or -1 if the end of the stream is + * reached. + * @see #in + */ + public int read() throws IOException { + + // We have no data, parse chunk header + if (mAvailableInCurrentChunk == -1) { + return -1; + } + else if (mAvailableInCurrentChunk == 0) { + + // Next chunk! + mAvailableInCurrentChunk = parseChunkSize(); + return read(); + } + mAvailableInCurrentChunk--; + return in.read(); + } + + /** + * Reads the chunk size from the chunk header + * {@code chunk-size [SP chunk-extension] CRLF}. + * The chunk-extension is simply discarded. + * + * @return the length of the current chunk, or -1 if the current chunk + * is the last-chunk (a chunk with the size of 0). + */ + protected int parseChunkSize() throws IOException { + + StringBuilder buf = new StringBuilder(); + int b; + + // read chunk-size, chunk-extension (if any) and CRLF + while ((b = in.read()) > 0) { + if ((b == '\r') && (in.read() == '\n')) { // Should be no CR or LF + break; // except for this one... + } + buf.append((char) b); + } + String line = buf.toString(); + + // Happens, as we don't read CRLF off the end of the chunk data... + if (line.length() == 0) { + return 0; + } + + // Discard any chunk-extensions, and read size (HEX). + int spIdx = line.indexOf(' '); + int size = Integer.parseInt(((spIdx >= 0) + ? line.substring(0, spIdx) + : line), 16); + + // This is the last chunk (=EOF) + if (size == 0) { + return -1; + } + return size; + } + } +} diff --git a/common/common-io/src/main/java/com/twelvemonkeys/net/NetUtil.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/net/NetUtil.java similarity index 85% rename from common/common-io/src/main/java/com/twelvemonkeys/net/NetUtil.java rename to sandbox/sandbox-common/src/main/java/com/twelvemonkeys/net/NetUtil.java index ae7faefd..09d94938 100755 --- a/common/common-io/src/main/java/com/twelvemonkeys/net/NetUtil.java +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/net/NetUtil.java @@ -1,1422 +1,1258 @@ -package com.twelvemonkeys.net; - -import com.twelvemonkeys.io.FileUtil; -import com.twelvemonkeys.lang.StringUtil; -import com.twelvemonkeys.lang.DateUtil; -import com.twelvemonkeys.util.CollectionUtil; - -import java.io.*; -import java.net.*; -import java.text.DateFormat; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.*; - -/** - * Utility class with network related methods. - * - * @author Harald Kuhr - * @author last modified by $Author: haku $ - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/net/NetUtil.java#2 $ - */ -public final class NetUtil { - - private final static String VERSION_ID = "NetUtil/2.1"; - - private static Authenticator sAuthenticator = null; - - private final static int BUF_SIZE = 8192; - private final static String HTTP = "http://"; - private final static String HTTPS = "https://"; - - /** - * Field HTTP_PROTOCOL - */ - public final static String HTTP_PROTOCOL = "http"; - - /** - * Field HTTPS_PROTOCOL - */ - public final static String HTTPS_PROTOCOL = "https"; - - /** - * Field HTTP_GET - */ - public final static String HTTP_GET = "GET"; - - /** - * Field HTTP_POST - */ - public final static String HTTP_POST = "POST"; - - /** - * Field HTTP_HEAD - */ - public final static String HTTP_HEAD = "HEAD"; - - /** - * Field HTTP_OPTIONS - */ - public final static String HTTP_OPTIONS = "OPTIONS"; - - /** - * Field HTTP_PUT - */ - public final static String HTTP_PUT = "PUT"; - - /** - * Field HTTP_DELETE - */ - public final static String HTTP_DELETE = "DELETE"; - - /** - * Field HTTP_TRACE - */ - public final static String HTTP_TRACE = "TRACE"; - - /** - * RFC 1123 date format, as reccomended 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, (allmost) 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, (allmost) 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); - } - } - } - - /** - * Creates a NetUtil. - * This class has only static methods and members, and should not be - * instantiated. - */ - private NetUtil() { - } - - public static void main1(String[] args) { - String timeStr = (args.length > 0 && !StringUtil.isNumber(args[0])) ? args[0] : null; - - long time = args.length > 0 ? - (timeStr != null ? parseHTTPDate(timeStr) : Long.parseLong(args[0])) - : System.currentTimeMillis(); - System.out.println(timeStr + " --> " + time + " --> " + formatHTTPDate(time)); - } - - /** - * Main method, reads data from a URL and, optionally, writes it to stdout or a file. - * @param pArgs command line arguemnts - * @throws java.io.IOException if an I/O exception occurs - */ - public static void main(String[] pArgs) throws IOException { - // params: - int timeout = 0; - boolean followRedirects = true; - boolean debugHeaders = false; - String requestPropertiesFile = null; - String requestHeaders = null; - String postData = null; - File putData = null; - int argIdx = 0; - boolean errArgs = false; - boolean writeToFile = false; - boolean writeToStdOut = false; - String outFileName = null; - - while ((argIdx < pArgs.length) && (pArgs[argIdx].charAt(0) == '-') && (pArgs[argIdx].length() >= 2)) { - if ((pArgs[argIdx].charAt(1) == 't') || pArgs[argIdx].equals("--timeout")) { - argIdx++; - try { - timeout = Integer.parseInt(pArgs[argIdx++]); - } - catch (NumberFormatException nfe) { - errArgs = true; - break; - } - } - else if ((pArgs[argIdx].charAt(1) == 'd') || pArgs[argIdx].equals("--debugheaders")) { - debugHeaders = true; - argIdx++; - } - else if ((pArgs[argIdx].charAt(1) == 'n') || pArgs[argIdx].equals("--nofollowredirects")) { - followRedirects = false; - argIdx++; - } - else if ((pArgs[argIdx].charAt(1) == 'r') || pArgs[argIdx].equals("--requestproperties")) { - argIdx++; - requestPropertiesFile = pArgs[argIdx++]; - } - else if ((pArgs[argIdx].charAt(1) == 'p') || pArgs[argIdx].equals("--postdata")) { - argIdx++; - postData = pArgs[argIdx++]; - } - else if ((pArgs[argIdx].charAt(1) == 'u') || pArgs[argIdx].equals("--putdata")) { - argIdx++; - putData = new File(pArgs[argIdx++]); - if (!putData.exists()) { - errArgs = true; - break; - } - } - else if ((pArgs[argIdx].charAt(1) == 'h') || pArgs[argIdx].equals("--header")) { - argIdx++; - requestHeaders = pArgs[argIdx++]; - } - else if ((pArgs[argIdx].charAt(1) == 'f') || pArgs[argIdx].equals("--file")) { - argIdx++; - writeToFile = true; - - // Get optional file name - if (!((argIdx >= (pArgs.length - 1)) || (pArgs[argIdx].charAt(0) == '-'))) { - outFileName = pArgs[argIdx++]; - } - } - else if ((pArgs[argIdx].charAt(1) == 'o') || pArgs[argIdx].equals("--output")) { - argIdx++; - writeToStdOut = true; - } - else { - System.err.println("Unknown option \"" + pArgs[argIdx++] + "\""); - } - } - if (errArgs || (pArgs.length < (argIdx + 1))) { - System.err.println("Usage: java NetUtil [-f|--file []] [-d|--debugheaders] [-h|--header

] [-p|--postdata ] [-u|--putdata ] [-r|--requestProperties ] [-t|--timeout ] [-n|--nofollowredirects] fromUrl"); - System.exit(5); - } - String url = pArgs[argIdx/*++*/]; - - // DONE ARGS - // Get request properties - Properties requestProperties = new Properties(); - - if (requestPropertiesFile != null) { - - // Just read, no exception handling... - requestProperties.load(new FileInputStream(new File(requestPropertiesFile))); - } - if (requestHeaders != null) { - - // Get request headers - String[] headerPairs = StringUtil.toStringArray(requestHeaders, ","); - - for (String headerPair : headerPairs) { - String[] pair = StringUtil.toStringArray(headerPair, ":"); - String key = (pair.length > 0) - ? pair[0].trim() - : null; - String value = (pair.length > 1) - ? pair[1].trim() - : ""; - - if (key != null) { - requestProperties.setProperty(key, value); - } - } - } - java.net.HttpURLConnection conn; - - // Create connection - URL reqURL = getURLAndSetAuthorization(url, requestProperties); - - conn = createHttpURLConnection(reqURL, requestProperties, followRedirects, timeout); - - // POST - if (postData != null) { - // HTTP POST method - conn.setRequestMethod(HTTP_POST); - - // Set entity headers - conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); - conn.setRequestProperty("Content-Length", String.valueOf(postData.length())); - conn.setRequestProperty("Content-Encoding", "ISO-8859-1"); - - // Get outputstream (this is where the connect actually happens) - OutputStream os = conn.getOutputStream(); - - System.err.println("OutputStream: " + os.getClass().getName() + "@" + System.identityHashCode(os)); - OutputStreamWriter writer = new OutputStreamWriter(os, "ISO-8859-1"); - - // Write post data to the stream - writer.write(postData); - writer.write("\r\n"); - - //writer.flush(); - writer.close(); // Does this close the underlying stream? - } - // PUT - else if (putData != null) { - // HTTP PUT method - conn.setRequestMethod(HTTP_PUT); - - // Set entity headers - //conn.setRequestProperty("Content-Type", "???"); - // TODO: Set Content-Type to correct type? - // TODO: Set content-encoding? Or can binary data be sent directly? - conn.setRequestProperty("Content-Length", String.valueOf(putData.length())); - - // Get outputstream (this is where the connect actually happens) - OutputStream os = conn.getOutputStream(); - - System.err.println("OutputStream: " + os.getClass().getName() + "@" + System.identityHashCode(os)); - - // Write put data to the stream - FileUtil.copy(new FileInputStream(putData), os); - - os.close(); - } - - // - InputStream is; - - if (conn.getResponseCode() == 200) { - - // Connect and get stream - is = conn.getInputStream(); - } - else { - is = conn.getErrorStream(); - } - - // - if (debugHeaders) { - System.err.println("Request (debug):"); - System.err.println(conn.getClass()); - System.err.println("Response (debug):"); - - // Headerfield 0 is response code - System.err.println(conn.getHeaderField(0)); - - // Loop from 1, as headerFieldKey(0) == null... - for (int i = 1; ; i++) { - String key = conn.getHeaderFieldKey(i); - - // Seems to be the way to loop through them all... - if (key == null) { - break; - } - System.err.println(key + ": " + conn.getHeaderField(key)); - } - } - - // Create output file if specified - OutputStream os; - - if (writeToFile) { - if (outFileName == null) { - outFileName = reqURL.getFile(); - if (StringUtil.isEmpty(outFileName)) { - outFileName = conn.getHeaderField("Location"); - if (StringUtil.isEmpty(outFileName)) { - outFileName = "index"; - - // Find a suitable extension - // TODO: Replace with MIME-type util with MIME/file ext mapping - String ext = conn.getContentType(); - - if (!StringUtil.isEmpty(ext)) { - int idx = ext.lastIndexOf('/'); - - if (idx >= 0) { - ext = ext.substring(idx + 1); - } - idx = ext.indexOf(';'); - if (idx >= 0) { - ext = ext.substring(0, idx); - } - outFileName += "." + ext; - } - } - } - int idx = outFileName.lastIndexOf('/'); - - if (idx >= 0) { - outFileName = outFileName.substring(idx + 1); - } - idx = outFileName.indexOf('?'); - if (idx >= 0) { - outFileName = outFileName.substring(0, idx); - } - } - File outFile = new File(outFileName); - - if (!outFile.createNewFile()) { - if (outFile.exists()) { - System.err.println("Cannot write to file " + outFile.getAbsolutePath() + ", file allready exists."); - } - else { - System.err.println("Cannot write to file " + outFile.getAbsolutePath() + ", check write permissions."); - } - System.exit(5); - } - os = new FileOutputStream(outFile); - } - else if (writeToStdOut) { - os = System.out; - } - else { - os = null; - } - - // Get data. - if ((writeToFile || writeToStdOut) && is != null) { - FileUtil.copy(is, os); - } - - /* - Hashtable postData = new Hashtable(); - postData.put("SearchText", "condition"); - - try { - InputStream in = getInputStreamHttpPost(pArgs[argIdx], postData, - props, true, 0); - out = new FileOutputStream(file); - FileUtil.copy(in, out); - } - catch (Exception e) { - System.err.println("Error: " + e); - e.printStackTrace(System.err); - continue; - } - */ - } - - /* - public static class Cookie { - String mName = null; - String mValue = null; - - public Cookie(String pName, String pValue) { - mName = pName; - mValue = pValue; - } - - public String toString() { - return mName + "=" + mValue; - } - */ - - /* - // Just a way to set cookies.. - if (pCookies != null) { - String cookieStr = ""; - for (int i = 0; i < pCookies.length; i++) - cookieStr += ((i == pCookies.length) ? pCookies[i].toString() - : pCookies[i].toString() + ";"); - - // System.out.println("Cookie: " + cookieStr); - - conn.setRequestProperty("Cookie", cookieStr); - } - */ - - /* - } - */ - - /** - * Test if the given URL is using HTTP protocol. - * - * @param pURL the url to condition - * @return true if the protocol is HTTP. - */ - public static boolean isHttpURL(String pURL) { - return ((pURL != null) && pURL.startsWith(HTTP)); - } - - /** - * Test if the given URL is using HTTP protocol. - * - * @param pURL the url to condition - * @return true if the protocol is HTTP. - */ - public static boolean isHttpURL(URL pURL) { - return ((pURL != null) && pURL.getProtocol().equals("http")); - } - - /** - * Gets the content from a given URL, and returns it as a byte array. - * Supports basic HTTP - * authentication, using a URL string similar to most browsers. - *

- * NOTE: If you supply a username and password for HTTP - * authentication, this method uses the java.net.Authenticator's static - * {@code setDefault()} method, that can only be set ONCE. This - * means that if the default Authenticator is allready set, this method - * will fail. - * It also means if any other piece of code tries to register a new default - * Authenticator within the current VM, it will fail. - * - * @param pURL A String containing the URL, on the form - * [http://][:@]servername[/file.ext] - * where everything in brackets are optional. - * @return a byte array with the URL contents. If an error occurs, the - * returned array may be zero-length, but not null. - * @throws MalformedURLException if the urlName parameter is not a valid - * URL. Note that the protocol cannot be anything but HTTP. - * @throws FileNotFoundException if there is no file at the given URL. - * @throws IOException if an error occurs during transfer. - * @see java.net.Authenticator - * @see SimpleAuthenticator - */ - public static byte[] getBytesHttp(String pURL) throws IOException { - return getBytesHttp(pURL, 0); - } - - /** - * Gets the content from a given URL, and returns it as a byte array. - * - * @param pURL the URL to get. - * @return a byte array with the URL contents. If an error occurs, the - * returned array may be zero-length, but not null. - * @throws FileNotFoundException if there is no file at the given URL. - * @throws IOException if an error occurs during transfer. - * @see #getBytesHttp(String) - */ - public static byte[] getBytesHttp(URL pURL) throws IOException { - return getBytesHttp(pURL, 0); - } - - /** - * Gets the InputStream from a given URL. Supports basic HTTP - * authentication, using a URL string similar to most browsers. - *

- * NOTE: If you supply a username and password for HTTP - * authentication, this method uses the java.net.Authenticator's static - * {@code setDefault()} method, that can only be set ONCE. This - * means that if the default Authenticator is allready set, this method - * will fail. - * It also means if any other piece of code tries to register a new default - * Authenticator within the current VM, it will fail. - * - * @param pURL A String containing the URL, on the form - * [http://][:@]servername[/file.ext] - * where everything in brackets are optional. - * @return an input stream that reads from the connection created by the - * given URL. - * @throws MalformedURLException if the urlName parameter specifies an - * unknown protocol, or does not form a valid URL. - * Note that the protocol cannot be anything but HTTP. - * @throws FileNotFoundException if there is no file at the given URL. - * @throws IOException if an error occurs during transfer. - * @see java.net.Authenticator - * @see SimpleAuthenticator - */ - public static InputStream getInputStreamHttp(String pURL) throws IOException { - return getInputStreamHttp(pURL, 0); - } - - /** - * Gets the InputStream from a given URL. - * - * @param pURL the URL to get. - * @return an input stream that reads from the connection created by the - * given URL. - * @throws FileNotFoundException if there is no file at the given URL. - * @throws IOException if an error occurs during transfer. - * @see #getInputStreamHttp(String) - */ - public static InputStream getInputStreamHttp(URL pURL) throws IOException { - return getInputStreamHttp(pURL, 0); - } - - /** - * Gets the InputStream from a given URL, with the given timeout. - * The timeout must be > 0. A timeout of zero is interpreted as an - * infinite timeout. Supports basic HTTP - * authentication, using a URL string similar to most browsers. - *

- * Implementation note: If the timeout parameter is greater than 0, - * this method uses my own implementation of - * java.net.HttpURLConnection, that uses plain sockets, to create an - * HTTP connection to the given URL. The {@code read} methods called - * on the returned InputStream, will block only for the specified timeout. - * If the timeout expires, a java.io.InterruptedIOException is raised. This - * might happen BEFORE OR AFTER this method returns, as the HTTP headers - * will be read and parsed from the InputStream before this method returns, - * while further read operations on the returned InputStream might be - * performed at a later stage. - *
- *
- * - * @param pURL the URL to get. - * @param pTimeout the specified timeout, in milliseconds. - * @return an input stream that reads from the socket connection, created - * from the given URL. - * @throws MalformedURLException if the url parameter specifies an - * unknown protocol, or does not form a valid URL. - * @throws UnknownHostException if the IP address for the given URL cannot - * be resolved. - * @throws FileNotFoundException if there is no file at the given URL. - * @throws IOException if an error occurs during transfer. - * @see #getInputStreamHttp(URL,int) - * @see java.net.Socket - * @see java.net.Socket#setSoTimeout(int) setSoTimeout - * @see java.io.InterruptedIOException - * @see RFC 2616 - */ - public static InputStream getInputStreamHttp(String pURL, int pTimeout) throws IOException { - return getInputStreamHttp(pURL, null, true, pTimeout); - } - - /** - * Gets the InputStream from a given URL, with the given timeout. - * The timeout must be > 0. A timeout of zero is interpreted as an - * infinite timeout. Supports basic HTTP - * authentication, using a URL string similar to most browsers. - *

- * Implementation note: If the timeout parameter is greater than 0, - * this method uses my own implementation of - * java.net.HttpURLConnection, that uses plain sockets, to create an - * HTTP connection to the given URL. The {@code read} methods called - * on the returned InputStream, will block only for the specified timeout. - * If the timeout expires, a java.io.InterruptedIOException is raised. This - * might happen BEFORE OR AFTER this method returns, as the HTTP headers - * will be read and parsed from the InputStream before this method returns, - * while further read operations on the returned InputStream might be - * performed at a later stage. - *
- *
- * - * @param pURL the URL to get. - * @param pProperties the request header properties. - * @param pFollowRedirects specifying wether redirects should be followed. - * @param pTimeout the specified timeout, in milliseconds. - * @return an input stream that reads from the socket connection, created - * from the given URL. - * @throws MalformedURLException if the url parameter specifies an - * unknown protocol, or does not form a valid URL. - * @throws UnknownHostException if the IP address for the given URL cannot - * be resolved. - * @throws FileNotFoundException if there is no file at the given URL. - * @throws IOException if an error occurs during transfer. - * @see #getInputStreamHttp(URL,int) - * @see java.net.Socket - * @see java.net.Socket#setSoTimeout(int) setSoTimeout - * @see java.io.InterruptedIOException - * @see RFC 2616 - */ - public static InputStream getInputStreamHttp(final String pURL, final Properties pProperties, final boolean pFollowRedirects, final int pTimeout) - throws IOException { - - // Make sure we have properties - Properties properties = pProperties != null ? pProperties : new Properties(); - - //URL url = getURLAndRegisterPassword(pURL); - URL url = getURLAndSetAuthorization(pURL, properties); - - //unregisterPassword(url); - return getInputStreamHttp(url, properties, pFollowRedirects, pTimeout); - } - - /** - * Registers the password from the URL string, and returns the URL object. - * - * @param pURL the string representation of the URL, possibly including authorization part - * @param pProperties the - * @return the URL created from {@code pURL}. - * @throws java.net.MalformedURLException if there's a syntax error in {@code pURL} - */ - private static URL getURLAndSetAuthorization(final String pURL, final Properties pProperties) throws MalformedURLException { - String url = pURL; - // Split user/password away from url - String userPass = null; - String protocolPrefix = HTTP; - int httpIdx = url.indexOf(HTTPS); - - if (httpIdx >= 0) { - protocolPrefix = HTTPS; - url = url.substring(httpIdx + HTTPS.length()); - } - else { - httpIdx = url.indexOf(HTTP); - if (httpIdx >= 0) { - url = url.substring(httpIdx + HTTP.length()); - } - } - - // Get authorization part - int atIdx = url.indexOf("@"); - - if (atIdx >= 0) { - userPass = url.substring(0, atIdx); - url = url.substring(atIdx + 1); - } - - // Set authorization if user/password is present - if (userPass != null) { - // System.out.println("Setting password ("+ userPass + ")!"); - pProperties.setProperty("Authorization", "Basic " + BASE64.encode(userPass.getBytes())); - } - - // Return URL - return new URL(protocolPrefix + url); - } - - /** - * Gets the InputStream from a given URL, with the given timeout. - * The timeout must be > 0. A timeout of zero is interpreted as an - * infinite timeout. - *

- * Implementation note: If the timeout parameter is greater than 0, - * this method uses my own implementation of - * java.net.HttpURLConnection, that uses plain sockets, to create an - * HTTP connection to the given URL. The {@code read} methods called - * on the returned InputStream, will block only for the specified timeout. - * If the timeout expires, a java.io.InterruptedIOException is raised. This - * might happen BEFORE OR AFTER this method returns, as the HTTP headers - * will be read and parsed from the InputStream before this method returns, - * while further read operations on the returned InputStream might be - * performed at a later stage. - *
- *
- * - * @param pURL the URL to get. - * @param pTimeout the specified timeout, in milliseconds. - * @return an input stream that reads from the socket connection, created - * from the given URL. - * @throws UnknownHostException if the IP address for the given URL cannot - * be resolved. - * @throws FileNotFoundException if there is no file at the given URL. - * @throws IOException if an error occurs during transfer. - * @see com.twelvemonkeys.net.HttpURLConnection - * @see java.net.Socket - * @see java.net.Socket#setSoTimeout(int) setSoTimeout - * @see java.net.HttpURLConnection - * @see java.io.InterruptedIOException - * @see RFC 2616 - */ - public static InputStream getInputStreamHttp(URL pURL, int pTimeout) throws IOException { - return getInputStreamHttp(pURL, null, true, pTimeout); - } - - /** - * Gets the InputStream from a given URL, with the given timeout. - * The timeout must be > 0. A timeout of zero is interpreted as an - * infinite timeout. Supports basic HTTP - * authentication, using a URL string similar to most browsers. - *

- * Implementation note: If the timeout parameter is greater than 0, - * this method uses my own implementation of - * java.net.HttpURLConnection, that uses plain sockets, to create an - * HTTP connection to the given URL. The {@code read} methods called - * on the returned InputStream, will block only for the specified timeout. - * If the timeout expires, a java.io.InterruptedIOException is raised. This - * might happen BEFORE OR AFTER this method returns, as the HTTP headers - * will be read and parsed from the InputStream before this method returns, - * while further read operations on the returned InputStream might be - * performed at a later stage. - *
- *
- * - * @param pURL the URL to get. - * @param pProperties the request header properties. - * @param pFollowRedirects specifying wether redirects should be followed. - * @param pTimeout the specified timeout, in milliseconds. - * @return an input stream that reads from the socket connection, created - * from the given URL. - * @throws UnknownHostException if the IP address for the given URL cannot - * be resolved. - * @throws FileNotFoundException if there is no file at the given URL. - * @throws IOException if an error occurs during transfer. - * @see #getInputStreamHttp(URL,int) - * @see java.net.Socket - * @see java.net.Socket#setSoTimeout(int) setSoTimeout - * @see java.io.InterruptedIOException - * @see RFC 2616 - */ - public static InputStream getInputStreamHttp(URL pURL, Properties pProperties, boolean pFollowRedirects, int pTimeout) - throws IOException { - - // Open the connection, and get the stream - java.net.HttpURLConnection conn = createHttpURLConnection(pURL, pProperties, pFollowRedirects, pTimeout); - - // HTTP GET method - conn.setRequestMethod(HTTP_GET); - - // This is where the connect happens - InputStream is = conn.getInputStream(); - - // We only accept the 200 OK message - if (conn.getResponseCode() != HttpURLConnection.HTTP_OK) { - throw new IOException("The request gave the response: " + conn.getResponseCode() + ": " + conn.getResponseMessage()); - } - return is; - } - - /** - * Gets the InputStream from a given URL, with the given timeout. - * The timeout must be > 0. A timeout of zero is interpreted as an - * infinite timeout. Supports basic HTTP - * authentication, using a URL string similar to most browsers. - *

- * Implementation note: If the timeout parameter is greater than 0, - * this method uses my own implementation of - * java.net.HttpURLConnection, that uses plain sockets, to create an - * HTTP connection to the given URL. The {@code read} methods called - * on the returned InputStream, will block only for the specified timeout. - * If the timeout expires, a java.io.InterruptedIOException is raised. This - * might happen BEFORE OR AFTER this method returns, as the HTTP headers - * will be read and parsed from the InputStream before this method returns, - * while further read operations on the returned InputStream might be - * performed at a later stage. - *
- *
- * - * @param pURL the URL to get. - * @param pPostData the post data. - * @param pProperties the request header properties. - * @param pFollowRedirects specifying wether redirects should be followed. - * @param pTimeout the specified timeout, in milliseconds. - * @return an input stream that reads from the socket connection, created - * from the given URL. - * @throws MalformedURLException if the url parameter specifies an - * unknown protocol, or does not form a valid URL. - * @throws UnknownHostException if the IP address for the given URL cannot - * be resolved. - * @throws FileNotFoundException if there is no file at the given URL. - * @throws IOException if an error occurs during transfer. - */ - public static InputStream getInputStreamHttpPost(String pURL, Map pPostData, Properties pProperties, boolean pFollowRedirects, int pTimeout) - throws IOException { - - pProperties = pProperties != null ? pProperties : new Properties(); - - //URL url = getURLAndRegisterPassword(pURL); - URL url = getURLAndSetAuthorization(pURL, pProperties); - - //unregisterPassword(url); - return getInputStreamHttpPost(url, pPostData, pProperties, pFollowRedirects, pTimeout); - } - - /** - * Gets the InputStream from a given URL, with the given timeout. - * The timeout must be > 0. A timeout of zero is interpreted as an - * infinite timeout. Supports basic HTTP - * authentication, using a URL string similar to most browsers. - *

- * Implementation note: If the timeout parameter is greater than 0, - * this method uses my own implementation of - * java.net.HttpURLConnection, that uses plain sockets, to create an - * HTTP connection to the given URL. The {@code read} methods called - * on the returned InputStream, will block only for the specified timeout. - * If the timeout expires, a java.io.InterruptedIOException is raised. This - * might happen BEFORE OR AFTER this method returns, as the HTTP headers - * will be read and parsed from the InputStream before this method returns, - * while further read operations on the returned InputStream might be - * performed at a later stage. - *
- *
- * - * @param pURL the URL to get. - * @param pPostData the post data. - * @param pProperties the request header properties. - * @param pFollowRedirects specifying wether redirects should be followed. - * @param pTimeout the specified timeout, in milliseconds. - * @return an input stream that reads from the socket connection, created - * from the given URL. - * @throws UnknownHostException if the IP address for the given URL cannot - * be resolved. - * @throws FileNotFoundException if there is no file at the given URL. - * @throws IOException if an error occurs during transfer. - */ - public static InputStream getInputStreamHttpPost(URL pURL, Map pPostData, Properties pProperties, boolean pFollowRedirects, int pTimeout) - throws IOException { - // Open the connection, and get the stream - java.net.HttpURLConnection conn = createHttpURLConnection(pURL, pProperties, pFollowRedirects, pTimeout); - - // HTTP POST method - conn.setRequestMethod(HTTP_POST); - - // Iterate over and create post data string - StringBuilder postStr = new StringBuilder(); - - if (pPostData != null) { - Iterator data = pPostData.entrySet().iterator(); - - while (data.hasNext()) { - Map.Entry entry = (Map.Entry) data.next(); - - // Properties key/values can be safely cast to strings - // Encode the string - postStr.append(URLEncoder.encode((String) entry.getKey(), "UTF-8")); - postStr.append('='); - postStr.append(URLEncoder.encode(entry.getValue().toString(), "UTF-8")); - - if (data.hasNext()) { - postStr.append('&'); - } - } - } - - // Set entity headers - String encoding = conn.getRequestProperty("Content-Encoding"); - if (StringUtil.isEmpty(encoding)) { - encoding = "UTF-8"; - } - conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); - conn.setRequestProperty("Content-Length", String.valueOf(postStr.length())); - conn.setRequestProperty("Content-Encoding", encoding); - - // Get outputstream (this is where the connect actually happens) - OutputStream os = conn.getOutputStream(); - OutputStreamWriter writer = new OutputStreamWriter(os, encoding); - - // Write post data to the stream - writer.write(postStr.toString()); - writer.write("\r\n"); - writer.close(); // Does this close the underlying stream? - - // Get the inputstream - InputStream is = conn.getInputStream(); - - // We only accept the 200 OK message - // TODO: Accept all 200 messages, like ACCEPTED, CREATED or NO_CONTENT? - if (conn.getResponseCode() != HttpURLConnection.HTTP_OK) { - throw new IOException("The request gave the response: " + conn.getResponseCode() + ": " + conn.getResponseMessage()); - } - return is; - } - - /** - * Creates a HTTP connection to the given URL. - * - * @param pURL the URL to get. - * @param pProperties connection properties. - * @param pFollowRedirects specifies whether we should follow redirects. - * @param pTimeout the specified timeout, in milliseconds. - * @return a HttpURLConnection - * @throws UnknownHostException if the hostname in the URL cannot be found. - * @throws IOException if an I/O exception occurs. - */ - public static java.net.HttpURLConnection createHttpURLConnection(URL pURL, Properties pProperties, boolean pFollowRedirects, int pTimeout) - throws IOException { - - // Open the connection, and get the stream - java.net.HttpURLConnection conn; - - if (pTimeout > 0) { - // Supports timeout - conn = new com.twelvemonkeys.net.HttpURLConnection(pURL, pTimeout); - } - else { - // Faster, more compatible - conn = (java.net.HttpURLConnection) pURL.openConnection(); - } - - // Set user agent - if ((pProperties == null) || !pProperties.containsKey("User-Agent")) { - conn.setRequestProperty("User-Agent", - VERSION_ID - + " (" + System.getProperty("os.name") + "/" + System.getProperty("os.version") + "; " - + System.getProperty("os.arch") + "; " - + System.getProperty("java.vm.name") + "/" + System.getProperty("java.vm.version") + ")"); - } - - // Set request properties - if (pProperties != null) { - for (Map.Entry entry : pProperties.entrySet()) { - // Properties key/values can be safely cast to strings - conn.setRequestProperty((String) entry.getKey(), entry.getValue().toString()); - } - } - - try { - // Breaks with JRE1.2? - conn.setInstanceFollowRedirects(pFollowRedirects); - } - catch (LinkageError le) { - // This is the best we can do... - java.net.HttpURLConnection.setFollowRedirects(pFollowRedirects); - System.err.println("You are using an old Java Spec, consider upgrading."); - System.err.println("java.net.HttpURLConnection.setInstanceFollowRedirects(" + pFollowRedirects + ") failed."); - - //le.printStackTrace(System.err); - } - - conn.setDoInput(true); - conn.setDoOutput(true); - - //conn.setUseCaches(true); - return conn; - } - - /** - * This is a hack to get around the protected constructors in - * HttpURLConnection, should maybe consider registering and do things - * properly... - */ - - /* - private static class TimedHttpURLConnection - extends com.twelvemonkeys.net.HttpURLConnection { - TimedHttpURLConnection(URL pURL, int pTimeout) { - super(pURL, pTimeout); - } - } - */ - - /** - * Gets the content from a given URL, with the given timeout. - * The timeout must be > 0. A timeout of zero is interpreted as an - * infinite timeout. Supports basic HTTP - * authentication, using a URL string similar to most browsers. - *

- * Implementation note: If the timeout parameter is greater than 0, - * this method uses my own implementation of - * java.net.HttpURLConnection, that uses plain sockets, to create an - * HTTP connection to the given URL. The {@code read} methods called - * on the returned InputStream, will block only for the specified timeout. - * If the timeout expires, a java.io.InterruptedIOException is raised. - *
- *
- * - * @param pURL the URL to get. - * @param pTimeout the specified timeout, in milliseconds. - * @return a byte array that is read from the socket connection, created - * from the given URL. - * @throws MalformedURLException if the url parameter specifies an - * unknown protocol, or does not form a valid URL. - * @throws UnknownHostException if the IP address for the given URL cannot - * be resolved. - * @throws FileNotFoundException if there is no file at the given URL. - * @throws IOException if an error occurs during transfer. - * @see #getBytesHttp(URL,int) - * @see java.net.Socket - * @see java.net.Socket#setSoTimeout(int) setSoTimeout - * @see java.io.InterruptedIOException - * @see RFC 2616 - */ - public static byte[] getBytesHttp(String pURL, int pTimeout) throws IOException { - // Get the input stream from the url - InputStream in = new BufferedInputStream(getInputStreamHttp(pURL, pTimeout), BUF_SIZE * 2); - - // Get all the bytes in loop - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - int count; - byte[] buffer = new byte[BUF_SIZE]; - - try { - while ((count = in.read(buffer)) != -1) { - // NOTE: According to the J2SE API doc, read(byte[]) will read - // at least 1 byte, or return -1, if end-of-file is reached. - bytes.write(buffer, 0, count); - } - } - finally { - - // Close the buffer - in.close(); - } - return bytes.toByteArray(); - } - - /** - * Gets the content from a given URL, with the given timeout. - * The timeout must be > 0. A timeout of zero is interpreted as an - * infinite timeout. - *

- * Implementation note: If the timeout parameter is greater than 0, - * this method uses my own implementation of - * java.net.HttpURLConnection, that uses plain sockets, to create an - * HTTP connection to the given URL. The {@code read} methods called - * on the returned InputStream, will block only for the specified timeout. - * If the timeout expires, a java.io.InterruptedIOException is raised. - *
- *
- * - * @param pURL the URL to get. - * @param pTimeout the specified timeout, in milliseconds. - * @return an input stream that reads from the socket connection, created - * from the given URL. - * @throws UnknownHostException if the IP address for the given URL cannot - * be resolved. - * @throws FileNotFoundException if there is no file at the given URL. - * @throws IOException if an error occurs during transfer. - * @see #getInputStreamHttp(URL,int) - * @see com.twelvemonkeys.net.HttpURLConnection - * @see java.net.Socket - * @see java.net.Socket#setSoTimeout(int) setSoTimeout - * @see java.net.HttpURLConnection - * @see java.io.InterruptedIOException - * @see RFC 2616 - */ - public static byte[] getBytesHttp(URL pURL, int pTimeout) throws IOException { - // Get the input stream from the url - InputStream in = new BufferedInputStream(getInputStreamHttp(pURL, pTimeout), BUF_SIZE * 2); - - // Get all the bytes in loop - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - int count; - byte[] buffer = new byte[BUF_SIZE]; - - try { - while ((count = in.read(buffer)) != -1) { - // NOTE: According to the J2SE API doc, read(byte[]) will read - // at least 1 byte, or return -1, if end-of-file is reached. - bytes.write(buffer, 0, count); - } - } - finally { - - // Close the buffer - in.close(); - } - return bytes.toByteArray(); - } - - /** - * Unregisters the password asscociated with this URL - */ - - /* - private static void unregisterPassword(URL pURL) { - Authenticator auth = registerAuthenticator(); - if (auth != null && auth instanceof SimpleAuthenticator) - ((SimpleAuthenticator) auth) - .unregisterPasswordAuthentication(pURL); - } - */ - - /** - * Registers the password from the URL string, and returns the URL object. - */ - - /* - private static URL getURLAndRegisterPassword(String pURL) - throws MalformedURLException - { - // Split user/password away from url - String userPass = null; - String protocolPrefix = HTTP; - - int httpIdx = pURL.indexOf(HTTPS); - if (httpIdx >= 0) { - protocolPrefix = HTTPS; - pURL = pURL.substring(httpIdx + HTTPS.length()); - } - else { - httpIdx = pURL.indexOf(HTTP); - if (httpIdx >= 0) - pURL = pURL.substring(httpIdx + HTTP.length()); - } - - int atIdx = pURL.indexOf("@"); - if (atIdx >= 0) { - userPass = pURL.substring(0, atIdx); - pURL = pURL.substring(atIdx + 1); - } - - // Set URL - URL url = new URL(protocolPrefix + pURL); - - // Set Authenticator if user/password is present - if (userPass != null) { - // System.out.println("Setting password ("+ userPass + ")!"); - - int colIdx = userPass.indexOf(":"); - if (colIdx < 0) - throw new MalformedURLException("Error in username/password!"); - - String userName = userPass.substring(0, colIdx); - String passWord = userPass.substring(colIdx + 1); - - // Try to register the authenticator - // System.out.println("Trying to register authenticator!"); - Authenticator auth = registerAuthenticator(); - - // System.out.println("Got authenticator " + auth + "."); - - // Register our username/password with it - if (auth != null && auth instanceof SimpleAuthenticator) { - ((SimpleAuthenticator) auth) - .registerPasswordAuthentication(url, - new PasswordAuthentication(userName, - passWord.toCharArray())); - } - else { - // Not supported! - throw new RuntimeException("Could not register PasswordAuthentication"); - } - } - - return url; - } - */ - - /** - * Registers the Authenticator given in the system property - * {@code java.net.Authenticator}, or the default implementation - * ({@code com.twelvemonkeys.net.SimpleAuthenticator}). - *

- * BUG: What if authenticator has allready been set outside this class? - * - * @return The Authenticator created and set as default, or null, if it - * was not set as the default. However, there is no (clean) way to - * be sure the authenticator was set (the SimpleAuthenticator uses - * a hack to get around this), so it might be possible that the - * returned authenticator was not set as default... - * @see Authenticator#setDefault(Authenticator) - * @see SimpleAuthenticator - */ - public synchronized static Authenticator registerAuthenticator() { - if (sAuthenticator != null) { - return sAuthenticator; - } - - // Get the system property - String authenticatorName = System.getProperty("java.net.Authenticator"); - - // Try to get the Authenticator from the system property - if (authenticatorName != null) { - try { - Class authenticatorClass = Class.forName(authenticatorName); - - sAuthenticator = (Authenticator) authenticatorClass.newInstance(); - } - catch (ClassNotFoundException cnfe) { - // We should maybe rethrow this? - } - catch (InstantiationException ie) { - // Ignore - } - catch (IllegalAccessException iae) { - // Ignore - } - } - - // Get the default authenticator - if (sAuthenticator == null) { - sAuthenticator = SimpleAuthenticator.getInstance(); - } - - // Register authenticator as default - Authenticator.setDefault(sAuthenticator); - return sAuthenticator; - } - - /** - * Creates the InetAddress object from the given URL. - * Equivalent to calling {@code InetAddress.getByName(URL.getHost())} - * except that it returns null, instead of throwing UnknownHostException. - * - * @param pURL the URL to look up. - * @return the createad InetAddress, or null if the host was unknown. - * @see java.net.InetAddress - * @see java.net.URL - */ - public static InetAddress createInetAddressFromURL(URL pURL) { - try { - return InetAddress.getByName(pURL.getHost()); - } - catch (UnknownHostException e) { - return null; - } - } - - /** - * Creates an URL from the given InetAddress object, using the given - * protocol. - * Equivalent to calling - * {@code new URL(protocol, InetAddress.getHostName(), "")} - * except that it returns null, instead of throwing MalformedURLException. - * - * @param pIP the IP address to look up - * @param pProtocol the protocol to use in the new URL - * @return the created URL or null, if the URL could not be created. - * @see java.net.URL - * @see java.net.InetAddress - */ - public static URL createURLFromInetAddress(InetAddress pIP, String pProtocol) { - try { - return new URL(pProtocol, pIP.getHostName(), ""); - } - catch (MalformedURLException e) { - return null; - } - } - - /** - * Creates an URL from the given InetAddress object, using HTTP protocol. - * Equivalent to calling - * {@code new URL("http", InetAddress.getHostName(), "")} - * except that it returns null, instead of throwing MalformedURLException. - * - * @param pIP the IP address to look up - * @return the created URL or null, if the URL could not be created. - * @see java.net.URL - * @see java.net.InetAddress - */ - public static URL createURLFromInetAddress(InetAddress pIP) { - return createURLFromInetAddress(pIP, HTTP); - } - - /* - * TODO: Benchmark! - */ - static byte[] getBytesHttpOld(String pURL) throws IOException { - // Get the input stream from the url - InputStream in = new BufferedInputStream(getInputStreamHttp(pURL), BUF_SIZE * 2); - - // Get all the bytes in loop - byte[] bytes = new byte[0]; - int count; - byte[] buffer = new byte[BUF_SIZE]; - - try { - while ((count = in.read(buffer)) != -1) { - - // NOTE: According to the J2SE API doc, read(byte[]) will read - // at least 1 byte, or return -1, if end-of-file is reached. - bytes = (byte[]) CollectionUtil.mergeArrays(bytes, 0, bytes.length, buffer, 0, count); - } - } - finally { - - // Close the buffer - in.close(); - } - return bytes; - } - - /** - * Formats the time to a HTTP date, using the RFC 1123 format, as described - * in RFC 2616 (HTTP/1.1), sec. 3.3. - * - * @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 RFC 2616 (HTTP/1.1), sec. 3.3. - * - * @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. - *

- * Use this method with headers that contain dates, such as - * {@code If-Modified-Since} or {@code Last-Modified}. - *

- * The date string may be in either RFC 1123, RFC 850 or ANSI C asctime() - * format, as described in - * RFC 2616 (HTTP/1.1), sec. 3.3 - * - * @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; - } +package com.twelvemonkeys.net; + +import com.twelvemonkeys.io.FileUtil; +import com.twelvemonkeys.lang.StringUtil; +import com.twelvemonkeys.util.CollectionUtil; + +import java.io.*; +import java.net.*; +import java.net.HttpURLConnection; +import java.util.Iterator; +import java.util.Map; +import java.util.Properties; + +/** + * Utility class with network related methods. + * + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/net/NetUtil.java#2 $ + */ +public final class NetUtil { + + private final static String VERSION_ID = "NetUtil/2.1"; + + private static Authenticator sAuthenticator = null; + + private final static int BUF_SIZE = 8192; + private final static String HTTP = "http://"; + private final static String HTTPS = "https://"; + + /** + * Field HTTP_PROTOCOL + */ + public final static String HTTP_PROTOCOL = "http"; + + /** + * Field HTTPS_PROTOCOL + */ + public final static String HTTPS_PROTOCOL = "https"; + + /** + * Field HTTP_GET + */ + public final static String HTTP_GET = "GET"; + + /** + * Field HTTP_POST + */ + public final static String HTTP_POST = "POST"; + + /** + * Field HTTP_HEAD + */ + public final static String HTTP_HEAD = "HEAD"; + + /** + * Field HTTP_OPTIONS + */ + public final static String HTTP_OPTIONS = "OPTIONS"; + + /** + * Field HTTP_PUT + */ + public final static String HTTP_PUT = "PUT"; + + /** + * Field HTTP_DELETE + */ + public final static String HTTP_DELETE = "DELETE"; + + /** + * Field HTTP_TRACE + */ + public final static String HTTP_TRACE = "TRACE"; + + /** + * Creates a NetUtil. + * This class has only static methods and members, and should not be + * instantiated. + */ + private NetUtil() { + } + + /** + * Main method, reads data from a URL and, optionally, writes it to stdout or a file. + * @param pArgs command line arguemnts + * @throws java.io.IOException if an I/O exception occurs + */ + public static void main(String[] pArgs) throws IOException { + // params: + int timeout = 0; + boolean followRedirects = true; + boolean debugHeaders = false; + String requestPropertiesFile = null; + String requestHeaders = null; + String postData = null; + File putData = null; + int argIdx = 0; + boolean errArgs = false; + boolean writeToFile = false; + boolean writeToStdOut = false; + String outFileName = null; + + while ((argIdx < pArgs.length) && (pArgs[argIdx].charAt(0) == '-') && (pArgs[argIdx].length() >= 2)) { + if ((pArgs[argIdx].charAt(1) == 't') || pArgs[argIdx].equals("--timeout")) { + argIdx++; + try { + timeout = Integer.parseInt(pArgs[argIdx++]); + } + catch (NumberFormatException nfe) { + errArgs = true; + break; + } + } + else if ((pArgs[argIdx].charAt(1) == 'd') || pArgs[argIdx].equals("--debugheaders")) { + debugHeaders = true; + argIdx++; + } + else if ((pArgs[argIdx].charAt(1) == 'n') || pArgs[argIdx].equals("--nofollowredirects")) { + followRedirects = false; + argIdx++; + } + else if ((pArgs[argIdx].charAt(1) == 'r') || pArgs[argIdx].equals("--requestproperties")) { + argIdx++; + requestPropertiesFile = pArgs[argIdx++]; + } + else if ((pArgs[argIdx].charAt(1) == 'p') || pArgs[argIdx].equals("--postdata")) { + argIdx++; + postData = pArgs[argIdx++]; + } + else if ((pArgs[argIdx].charAt(1) == 'u') || pArgs[argIdx].equals("--putdata")) { + argIdx++; + putData = new File(pArgs[argIdx++]); + if (!putData.exists()) { + errArgs = true; + break; + } + } + else if ((pArgs[argIdx].charAt(1) == 'h') || pArgs[argIdx].equals("--header")) { + argIdx++; + requestHeaders = pArgs[argIdx++]; + } + else if ((pArgs[argIdx].charAt(1) == 'f') || pArgs[argIdx].equals("--file")) { + argIdx++; + writeToFile = true; + + // Get optional file name + if (!((argIdx >= (pArgs.length - 1)) || (pArgs[argIdx].charAt(0) == '-'))) { + outFileName = pArgs[argIdx++]; + } + } + else if ((pArgs[argIdx].charAt(1) == 'o') || pArgs[argIdx].equals("--output")) { + argIdx++; + writeToStdOut = true; + } + else { + System.err.println("Unknown option \"" + pArgs[argIdx++] + "\""); + } + } + if (errArgs || (pArgs.length < (argIdx + 1))) { + System.err.println("Usage: java NetUtil [-f|--file []] [-d|--debugheaders] [-h|--header

] [-p|--postdata ] [-u|--putdata ] [-r|--requestProperties ] [-t|--timeout ] [-n|--nofollowredirects] fromUrl"); + System.exit(5); + } + String url = pArgs[argIdx/*++*/]; + + // DONE ARGS + // Get request properties + Properties requestProperties = new Properties(); + + if (requestPropertiesFile != null) { + + // Just read, no exception handling... + requestProperties.load(new FileInputStream(new File(requestPropertiesFile))); + } + if (requestHeaders != null) { + + // Get request headers + String[] headerPairs = StringUtil.toStringArray(requestHeaders, ","); + + for (String headerPair : headerPairs) { + String[] pair = StringUtil.toStringArray(headerPair, ":"); + String key = (pair.length > 0) + ? pair[0].trim() + : null; + String value = (pair.length > 1) + ? pair[1].trim() + : ""; + + if (key != null) { + requestProperties.setProperty(key, value); + } + } + } + HttpURLConnection conn; + + // Create connection + URL reqURL = getURLAndSetAuthorization(url, requestProperties); + + conn = createHttpURLConnection(reqURL, requestProperties, followRedirects, timeout); + + // POST + if (postData != null) { + // HTTP POST method + conn.setRequestMethod(HTTP_POST); + + // Set entity headers + conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); + conn.setRequestProperty("Content-Length", String.valueOf(postData.length())); + conn.setRequestProperty("Content-Encoding", "ISO-8859-1"); + + // Get outputstream (this is where the connect actually happens) + OutputStream os = conn.getOutputStream(); + + System.err.println("OutputStream: " + os.getClass().getName() + "@" + System.identityHashCode(os)); + OutputStreamWriter writer = new OutputStreamWriter(os, "ISO-8859-1"); + + // Write post data to the stream + writer.write(postData); + writer.write("\r\n"); + + //writer.flush(); + writer.close(); // Does this close the underlying stream? + } + // PUT + else if (putData != null) { + // HTTP PUT method + conn.setRequestMethod(HTTP_PUT); + + // Set entity headers + //conn.setRequestProperty("Content-Type", "???"); + // TODO: Set Content-Type to correct type? + // TODO: Set content-encoding? Or can binary data be sent directly? + conn.setRequestProperty("Content-Length", String.valueOf(putData.length())); + + // Get outputstream (this is where the connect actually happens) + OutputStream os = conn.getOutputStream(); + + System.err.println("OutputStream: " + os.getClass().getName() + "@" + System.identityHashCode(os)); + + // Write put data to the stream + FileUtil.copy(new FileInputStream(putData), os); + + os.close(); + } + + // + InputStream is; + + if (conn.getResponseCode() == 200) { + + // Connect and get stream + is = conn.getInputStream(); + } + else { + is = conn.getErrorStream(); + } + + // + if (debugHeaders) { + System.err.println("Request (debug):"); + System.err.println(conn.getClass()); + System.err.println("Response (debug):"); + + // Headerfield 0 is response code + System.err.println(conn.getHeaderField(0)); + + // Loop from 1, as headerFieldKey(0) == null... + for (int i = 1; ; i++) { + String key = conn.getHeaderFieldKey(i); + + // Seems to be the way to loop through them all... + if (key == null) { + break; + } + System.err.println(key + ": " + conn.getHeaderField(key)); + } + } + + // Create output file if specified + OutputStream os; + + if (writeToFile) { + if (outFileName == null) { + outFileName = reqURL.getFile(); + if (StringUtil.isEmpty(outFileName)) { + outFileName = conn.getHeaderField("Location"); + if (StringUtil.isEmpty(outFileName)) { + outFileName = "index"; + + // Find a suitable extension + // TODO: Replace with MIME-type util with MIME/file ext mapping + String ext = conn.getContentType(); + + if (!StringUtil.isEmpty(ext)) { + int idx = ext.lastIndexOf('/'); + + if (idx >= 0) { + ext = ext.substring(idx + 1); + } + idx = ext.indexOf(';'); + if (idx >= 0) { + ext = ext.substring(0, idx); + } + outFileName += "." + ext; + } + } + } + int idx = outFileName.lastIndexOf('/'); + + if (idx >= 0) { + outFileName = outFileName.substring(idx + 1); + } + idx = outFileName.indexOf('?'); + if (idx >= 0) { + outFileName = outFileName.substring(0, idx); + } + } + File outFile = new File(outFileName); + + if (!outFile.createNewFile()) { + if (outFile.exists()) { + System.err.println("Cannot write to file " + outFile.getAbsolutePath() + ", file allready exists."); + } + else { + System.err.println("Cannot write to file " + outFile.getAbsolutePath() + ", check write permissions."); + } + System.exit(5); + } + os = new FileOutputStream(outFile); + } + else if (writeToStdOut) { + os = System.out; + } + else { + os = null; + } + + // Get data. + if ((writeToFile || writeToStdOut) && is != null) { + FileUtil.copy(is, os); + } + + /* + Hashtable postData = new Hashtable(); + postData.put("SearchText", "condition"); + + try { + InputStream in = getInputStreamHttpPost(pArgs[argIdx], postData, + props, true, 0); + out = new FileOutputStream(file); + FileUtil.copy(in, out); + } + catch (Exception e) { + System.err.println("Error: " + e); + e.printStackTrace(System.err); + continue; + } + */ + } + + /* + public static class Cookie { + String mName = null; + String mValue = null; + + public Cookie(String pName, String pValue) { + mName = pName; + mValue = pValue; + } + + public String toString() { + return mName + "=" + mValue; + } + */ + + /* + // Just a way to set cookies.. + if (pCookies != null) { + String cookieStr = ""; + for (int i = 0; i < pCookies.length; i++) + cookieStr += ((i == pCookies.length) ? pCookies[i].toString() + : pCookies[i].toString() + ";"); + + // System.out.println("Cookie: " + cookieStr); + + conn.setRequestProperty("Cookie", cookieStr); + } + */ + + /* + } + */ + + /** + * Test if the given URL is using HTTP protocol. + * + * @param pURL the url to condition + * @return true if the protocol is HTTP. + */ + public static boolean isHttpURL(String pURL) { + return ((pURL != null) && pURL.startsWith(HTTP)); + } + + /** + * Test if the given URL is using HTTP protocol. + * + * @param pURL the url to condition + * @return true if the protocol is HTTP. + */ + public static boolean isHttpURL(URL pURL) { + return ((pURL != null) && pURL.getProtocol().equals("http")); + } + + /** + * Gets the content from a given URL, and returns it as a byte array. + * Supports basic HTTP + * authentication, using a URL string similar to most browsers. + *

+ * NOTE: If you supply a username and password for HTTP + * authentication, this method uses the java.net.Authenticator's static + * {@code setDefault()} method, that can only be set ONCE. This + * means that if the default Authenticator is allready set, this method + * will fail. + * It also means if any other piece of code tries to register a new default + * Authenticator within the current VM, it will fail. + * + * @param pURL A String containing the URL, on the form + * [http://][:@]servername[/file.ext] + * where everything in brackets are optional. + * @return a byte array with the URL contents. If an error occurs, the + * returned array may be zero-length, but not null. + * @throws MalformedURLException if the urlName parameter is not a valid + * URL. Note that the protocol cannot be anything but HTTP. + * @throws FileNotFoundException if there is no file at the given URL. + * @throws IOException if an error occurs during transfer. + * @see java.net.Authenticator + * @see SimpleAuthenticator + */ + public static byte[] getBytesHttp(String pURL) throws IOException { + return getBytesHttp(pURL, 0); + } + + /** + * Gets the content from a given URL, and returns it as a byte array. + * + * @param pURL the URL to get. + * @return a byte array with the URL contents. If an error occurs, the + * returned array may be zero-length, but not null. + * @throws FileNotFoundException if there is no file at the given URL. + * @throws IOException if an error occurs during transfer. + * @see #getBytesHttp(String) + */ + public static byte[] getBytesHttp(URL pURL) throws IOException { + return getBytesHttp(pURL, 0); + } + + /** + * Gets the InputStream from a given URL. Supports basic HTTP + * authentication, using a URL string similar to most browsers. + *

+ * NOTE: If you supply a username and password for HTTP + * authentication, this method uses the java.net.Authenticator's static + * {@code setDefault()} method, that can only be set ONCE. This + * means that if the default Authenticator is allready set, this method + * will fail. + * It also means if any other piece of code tries to register a new default + * Authenticator within the current VM, it will fail. + * + * @param pURL A String containing the URL, on the form + * [http://][:@]servername[/file.ext] + * where everything in brackets are optional. + * @return an input stream that reads from the connection created by the + * given URL. + * @throws MalformedURLException if the urlName parameter specifies an + * unknown protocol, or does not form a valid URL. + * Note that the protocol cannot be anything but HTTP. + * @throws FileNotFoundException if there is no file at the given URL. + * @throws IOException if an error occurs during transfer. + * @see java.net.Authenticator + * @see SimpleAuthenticator + */ + public static InputStream getInputStreamHttp(String pURL) throws IOException { + return getInputStreamHttp(pURL, 0); + } + + /** + * Gets the InputStream from a given URL. + * + * @param pURL the URL to get. + * @return an input stream that reads from the connection created by the + * given URL. + * @throws FileNotFoundException if there is no file at the given URL. + * @throws IOException if an error occurs during transfer. + * @see #getInputStreamHttp(String) + */ + public static InputStream getInputStreamHttp(URL pURL) throws IOException { + return getInputStreamHttp(pURL, 0); + } + + /** + * Gets the InputStream from a given URL, with the given timeout. + * The timeout must be > 0. A timeout of zero is interpreted as an + * infinite timeout. Supports basic HTTP + * authentication, using a URL string similar to most browsers. + *

+ * Implementation note: If the timeout parameter is greater than 0, + * this method uses my own implementation of + * java.net.HttpURLConnection, that uses plain sockets, to create an + * HTTP connection to the given URL. The {@code read} methods called + * on the returned InputStream, will block only for the specified timeout. + * If the timeout expires, a java.io.InterruptedIOException is raised. This + * might happen BEFORE OR AFTER this method returns, as the HTTP headers + * will be read and parsed from the InputStream before this method returns, + * while further read operations on the returned InputStream might be + * performed at a later stage. + *
+ *
+ * + * @param pURL the URL to get. + * @param pTimeout the specified timeout, in milliseconds. + * @return an input stream that reads from the socket connection, created + * from the given URL. + * @throws MalformedURLException if the url parameter specifies an + * unknown protocol, or does not form a valid URL. + * @throws UnknownHostException if the IP address for the given URL cannot + * be resolved. + * @throws FileNotFoundException if there is no file at the given URL. + * @throws IOException if an error occurs during transfer. + * @see #getInputStreamHttp(URL,int) + * @see java.net.Socket + * @see java.net.Socket#setSoTimeout(int) setSoTimeout + * @see java.io.InterruptedIOException + * @see RFC 2616 + */ + public static InputStream getInputStreamHttp(String pURL, int pTimeout) throws IOException { + return getInputStreamHttp(pURL, null, true, pTimeout); + } + + /** + * Gets the InputStream from a given URL, with the given timeout. + * The timeout must be > 0. A timeout of zero is interpreted as an + * infinite timeout. Supports basic HTTP + * authentication, using a URL string similar to most browsers. + *

+ * Implementation note: If the timeout parameter is greater than 0, + * this method uses my own implementation of + * java.net.HttpURLConnection, that uses plain sockets, to create an + * HTTP connection to the given URL. The {@code read} methods called + * on the returned InputStream, will block only for the specified timeout. + * If the timeout expires, a java.io.InterruptedIOException is raised. This + * might happen BEFORE OR AFTER this method returns, as the HTTP headers + * will be read and parsed from the InputStream before this method returns, + * while further read operations on the returned InputStream might be + * performed at a later stage. + *
+ *
+ * + * @param pURL the URL to get. + * @param pProperties the request header properties. + * @param pFollowRedirects specifying wether redirects should be followed. + * @param pTimeout the specified timeout, in milliseconds. + * @return an input stream that reads from the socket connection, created + * from the given URL. + * @throws MalformedURLException if the url parameter specifies an + * unknown protocol, or does not form a valid URL. + * @throws UnknownHostException if the IP address for the given URL cannot + * be resolved. + * @throws FileNotFoundException if there is no file at the given URL. + * @throws IOException if an error occurs during transfer. + * @see #getInputStreamHttp(URL,int) + * @see java.net.Socket + * @see java.net.Socket#setSoTimeout(int) setSoTimeout + * @see java.io.InterruptedIOException + * @see RFC 2616 + */ + public static InputStream getInputStreamHttp(final String pURL, final Properties pProperties, final boolean pFollowRedirects, final int pTimeout) + throws IOException { + + // Make sure we have properties + Properties properties = pProperties != null ? pProperties : new Properties(); + + //URL url = getURLAndRegisterPassword(pURL); + URL url = getURLAndSetAuthorization(pURL, properties); + + //unregisterPassword(url); + return getInputStreamHttp(url, properties, pFollowRedirects, pTimeout); + } + + /** + * Registers the password from the URL string, and returns the URL object. + * + * @param pURL the string representation of the URL, possibly including authorization part + * @param pProperties the + * @return the URL created from {@code pURL}. + * @throws java.net.MalformedURLException if there's a syntax error in {@code pURL} + */ + private static URL getURLAndSetAuthorization(final String pURL, final Properties pProperties) throws MalformedURLException { + String url = pURL; + // Split user/password away from url + String userPass = null; + String protocolPrefix = HTTP; + int httpIdx = url.indexOf(HTTPS); + + if (httpIdx >= 0) { + protocolPrefix = HTTPS; + url = url.substring(httpIdx + HTTPS.length()); + } + else { + httpIdx = url.indexOf(HTTP); + if (httpIdx >= 0) { + url = url.substring(httpIdx + HTTP.length()); + } + } + + // Get authorization part + int atIdx = url.indexOf("@"); + + if (atIdx >= 0) { + userPass = url.substring(0, atIdx); + url = url.substring(atIdx + 1); + } + + // Set authorization if user/password is present + if (userPass != null) { + // System.out.println("Setting password ("+ userPass + ")!"); + pProperties.setProperty("Authorization", "Basic " + BASE64.encode(userPass.getBytes())); + } + + // Return URL + return new URL(protocolPrefix + url); + } + + /** + * Gets the InputStream from a given URL, with the given timeout. + * The timeout must be > 0. A timeout of zero is interpreted as an + * infinite timeout. + *

+ * Implementation note: If the timeout parameter is greater than 0, + * this method uses my own implementation of + * java.net.HttpURLConnection, that uses plain sockets, to create an + * HTTP connection to the given URL. The {@code read} methods called + * on the returned InputStream, will block only for the specified timeout. + * If the timeout expires, a java.io.InterruptedIOException is raised. This + * might happen BEFORE OR AFTER this method returns, as the HTTP headers + * will be read and parsed from the InputStream before this method returns, + * while further read operations on the returned InputStream might be + * performed at a later stage. + *
+ *
+ * + * @param pURL the URL to get. + * @param pTimeout the specified timeout, in milliseconds. + * @return an input stream that reads from the socket connection, created + * from the given URL. + * @throws UnknownHostException if the IP address for the given URL cannot + * be resolved. + * @throws FileNotFoundException if there is no file at the given URL. + * @throws IOException if an error occurs during transfer. + * @see com.twelvemonkeys.net.HttpURLConnection + * @see java.net.Socket + * @see java.net.Socket#setSoTimeout(int) setSoTimeout + * @see HttpURLConnection + * @see java.io.InterruptedIOException + * @see RFC 2616 + */ + public static InputStream getInputStreamHttp(URL pURL, int pTimeout) throws IOException { + return getInputStreamHttp(pURL, null, true, pTimeout); + } + + /** + * Gets the InputStream from a given URL, with the given timeout. + * The timeout must be > 0. A timeout of zero is interpreted as an + * infinite timeout. Supports basic HTTP + * authentication, using a URL string similar to most browsers. + *

+ * Implementation note: If the timeout parameter is greater than 0, + * this method uses my own implementation of + * java.net.HttpURLConnection, that uses plain sockets, to create an + * HTTP connection to the given URL. The {@code read} methods called + * on the returned InputStream, will block only for the specified timeout. + * If the timeout expires, a java.io.InterruptedIOException is raised. This + * might happen BEFORE OR AFTER this method returns, as the HTTP headers + * will be read and parsed from the InputStream before this method returns, + * while further read operations on the returned InputStream might be + * performed at a later stage. + *
+ *
+ * + * @param pURL the URL to get. + * @param pProperties the request header properties. + * @param pFollowRedirects specifying wether redirects should be followed. + * @param pTimeout the specified timeout, in milliseconds. + * @return an input stream that reads from the socket connection, created + * from the given URL. + * @throws UnknownHostException if the IP address for the given URL cannot + * be resolved. + * @throws FileNotFoundException if there is no file at the given URL. + * @throws IOException if an error occurs during transfer. + * @see #getInputStreamHttp(URL,int) + * @see java.net.Socket + * @see java.net.Socket#setSoTimeout(int) setSoTimeout + * @see java.io.InterruptedIOException + * @see RFC 2616 + */ + public static InputStream getInputStreamHttp(URL pURL, Properties pProperties, boolean pFollowRedirects, int pTimeout) + throws IOException { + + // Open the connection, and get the stream + HttpURLConnection conn = createHttpURLConnection(pURL, pProperties, pFollowRedirects, pTimeout); + + // HTTP GET method + conn.setRequestMethod(HTTP_GET); + + // This is where the connect happens + InputStream is = conn.getInputStream(); + + // We only accept the 200 OK message + if (conn.getResponseCode() != HttpURLConnection.HTTP_OK) { + throw new IOException("The request gave the response: " + conn.getResponseCode() + ": " + conn.getResponseMessage()); + } + return is; + } + + /** + * Gets the InputStream from a given URL, with the given timeout. + * The timeout must be > 0. A timeout of zero is interpreted as an + * infinite timeout. Supports basic HTTP + * authentication, using a URL string similar to most browsers. + *

+ * Implementation note: If the timeout parameter is greater than 0, + * this method uses my own implementation of + * java.net.HttpURLConnection, that uses plain sockets, to create an + * HTTP connection to the given URL. The {@code read} methods called + * on the returned InputStream, will block only for the specified timeout. + * If the timeout expires, a java.io.InterruptedIOException is raised. This + * might happen BEFORE OR AFTER this method returns, as the HTTP headers + * will be read and parsed from the InputStream before this method returns, + * while further read operations on the returned InputStream might be + * performed at a later stage. + *
+ *
+ * + * @param pURL the URL to get. + * @param pPostData the post data. + * @param pProperties the request header properties. + * @param pFollowRedirects specifying wether redirects should be followed. + * @param pTimeout the specified timeout, in milliseconds. + * @return an input stream that reads from the socket connection, created + * from the given URL. + * @throws MalformedURLException if the url parameter specifies an + * unknown protocol, or does not form a valid URL. + * @throws UnknownHostException if the IP address for the given URL cannot + * be resolved. + * @throws FileNotFoundException if there is no file at the given URL. + * @throws IOException if an error occurs during transfer. + */ + public static InputStream getInputStreamHttpPost(String pURL, Map pPostData, Properties pProperties, boolean pFollowRedirects, int pTimeout) + throws IOException { + + pProperties = pProperties != null ? pProperties : new Properties(); + + //URL url = getURLAndRegisterPassword(pURL); + URL url = getURLAndSetAuthorization(pURL, pProperties); + + //unregisterPassword(url); + return getInputStreamHttpPost(url, pPostData, pProperties, pFollowRedirects, pTimeout); + } + + /** + * Gets the InputStream from a given URL, with the given timeout. + * The timeout must be > 0. A timeout of zero is interpreted as an + * infinite timeout. Supports basic HTTP + * authentication, using a URL string similar to most browsers. + *

+ * Implementation note: If the timeout parameter is greater than 0, + * this method uses my own implementation of + * java.net.HttpURLConnection, that uses plain sockets, to create an + * HTTP connection to the given URL. The {@code read} methods called + * on the returned InputStream, will block only for the specified timeout. + * If the timeout expires, a java.io.InterruptedIOException is raised. This + * might happen BEFORE OR AFTER this method returns, as the HTTP headers + * will be read and parsed from the InputStream before this method returns, + * while further read operations on the returned InputStream might be + * performed at a later stage. + *
+ *
+ * + * @param pURL the URL to get. + * @param pPostData the post data. + * @param pProperties the request header properties. + * @param pFollowRedirects specifying wether redirects should be followed. + * @param pTimeout the specified timeout, in milliseconds. + * @return an input stream that reads from the socket connection, created + * from the given URL. + * @throws UnknownHostException if the IP address for the given URL cannot + * be resolved. + * @throws FileNotFoundException if there is no file at the given URL. + * @throws IOException if an error occurs during transfer. + */ + public static InputStream getInputStreamHttpPost(URL pURL, Map pPostData, Properties pProperties, boolean pFollowRedirects, int pTimeout) + throws IOException { + // Open the connection, and get the stream + HttpURLConnection conn = createHttpURLConnection(pURL, pProperties, pFollowRedirects, pTimeout); + + // HTTP POST method + conn.setRequestMethod(HTTP_POST); + + // Iterate over and create post data string + StringBuilder postStr = new StringBuilder(); + + if (pPostData != null) { + Iterator data = pPostData.entrySet().iterator(); + + while (data.hasNext()) { + Map.Entry entry = (Map.Entry) data.next(); + + // Properties key/values can be safely cast to strings + // Encode the string + postStr.append(URLEncoder.encode((String) entry.getKey(), "UTF-8")); + postStr.append('='); + postStr.append(URLEncoder.encode(entry.getValue().toString(), "UTF-8")); + + if (data.hasNext()) { + postStr.append('&'); + } + } + } + + // Set entity headers + String encoding = conn.getRequestProperty("Content-Encoding"); + if (StringUtil.isEmpty(encoding)) { + encoding = "UTF-8"; + } + conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); + conn.setRequestProperty("Content-Length", String.valueOf(postStr.length())); + conn.setRequestProperty("Content-Encoding", encoding); + + // Get outputstream (this is where the connect actually happens) + OutputStream os = conn.getOutputStream(); + OutputStreamWriter writer = new OutputStreamWriter(os, encoding); + + // Write post data to the stream + writer.write(postStr.toString()); + writer.write("\r\n"); + writer.close(); // Does this close the underlying stream? + + // Get the inputstream + InputStream is = conn.getInputStream(); + + // We only accept the 200 OK message + // TODO: Accept all 200 messages, like ACCEPTED, CREATED or NO_CONTENT? + if (conn.getResponseCode() != HttpURLConnection.HTTP_OK) { + throw new IOException("The request gave the response: " + conn.getResponseCode() + ": " + conn.getResponseMessage()); + } + return is; + } + + /** + * Creates a HTTP connection to the given URL. + * + * @param pURL the URL to get. + * @param pProperties connection properties. + * @param pFollowRedirects specifies whether we should follow redirects. + * @param pTimeout the specified timeout, in milliseconds. + * @return a HttpURLConnection + * @throws UnknownHostException if the hostname in the URL cannot be found. + * @throws IOException if an I/O exception occurs. + */ + public static HttpURLConnection createHttpURLConnection(URL pURL, Properties pProperties, boolean pFollowRedirects, int pTimeout) + throws IOException { + + // Open the connection, and get the stream + HttpURLConnection conn; + + if (pTimeout > 0) { + // Supports timeout + conn = new com.twelvemonkeys.net.HttpURLConnection(pURL, pTimeout); + } + else { + // Faster, more compatible + conn = (HttpURLConnection) pURL.openConnection(); + } + + // Set user agent + if ((pProperties == null) || !pProperties.containsKey("User-Agent")) { + conn.setRequestProperty("User-Agent", + VERSION_ID + + " (" + System.getProperty("os.name") + "/" + System.getProperty("os.version") + "; " + + System.getProperty("os.arch") + "; " + + System.getProperty("java.vm.name") + "/" + System.getProperty("java.vm.version") + ")"); + } + + // Set request properties + if (pProperties != null) { + for (Map.Entry entry : pProperties.entrySet()) { + // Properties key/values can be safely cast to strings + conn.setRequestProperty((String) entry.getKey(), entry.getValue().toString()); + } + } + + try { + // Breaks with JRE1.2? + conn.setInstanceFollowRedirects(pFollowRedirects); + } + catch (LinkageError le) { + // This is the best we can do... + HttpURLConnection.setFollowRedirects(pFollowRedirects); + System.err.println("You are using an old Java Spec, consider upgrading."); + System.err.println("java.net.HttpURLConnection.setInstanceFollowRedirects(" + pFollowRedirects + ") failed."); + + //le.printStackTrace(System.err); + } + + conn.setDoInput(true); + conn.setDoOutput(true); + + //conn.setUseCaches(true); + return conn; + } + + /** + * This is a hack to get around the protected constructors in + * HttpURLConnection, should maybe consider registering and do things + * properly... + */ + + /* + private static class TimedHttpURLConnection + extends com.twelvemonkeys.net.HttpURLConnection { + TimedHttpURLConnection(URL pURL, int pTimeout) { + super(pURL, pTimeout); + } + } + */ + + /** + * Gets the content from a given URL, with the given timeout. + * The timeout must be > 0. A timeout of zero is interpreted as an + * infinite timeout. Supports basic HTTP + * authentication, using a URL string similar to most browsers. + *

+ * Implementation note: If the timeout parameter is greater than 0, + * this method uses my own implementation of + * java.net.HttpURLConnection, that uses plain sockets, to create an + * HTTP connection to the given URL. The {@code read} methods called + * on the returned InputStream, will block only for the specified timeout. + * If the timeout expires, a java.io.InterruptedIOException is raised. + *
+ *
+ * + * @param pURL the URL to get. + * @param pTimeout the specified timeout, in milliseconds. + * @return a byte array that is read from the socket connection, created + * from the given URL. + * @throws MalformedURLException if the url parameter specifies an + * unknown protocol, or does not form a valid URL. + * @throws UnknownHostException if the IP address for the given URL cannot + * be resolved. + * @throws FileNotFoundException if there is no file at the given URL. + * @throws IOException if an error occurs during transfer. + * @see #getBytesHttp(URL,int) + * @see java.net.Socket + * @see java.net.Socket#setSoTimeout(int) setSoTimeout + * @see java.io.InterruptedIOException + * @see RFC 2616 + */ + public static byte[] getBytesHttp(String pURL, int pTimeout) throws IOException { + // Get the input stream from the url + InputStream in = new BufferedInputStream(getInputStreamHttp(pURL, pTimeout), BUF_SIZE * 2); + + // Get all the bytes in loop + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + int count; + byte[] buffer = new byte[BUF_SIZE]; + + try { + while ((count = in.read(buffer)) != -1) { + // NOTE: According to the J2SE API doc, read(byte[]) will read + // at least 1 byte, or return -1, if end-of-file is reached. + bytes.write(buffer, 0, count); + } + } + finally { + + // Close the buffer + in.close(); + } + return bytes.toByteArray(); + } + + /** + * Gets the content from a given URL, with the given timeout. + * The timeout must be > 0. A timeout of zero is interpreted as an + * infinite timeout. + *

+ * Implementation note: If the timeout parameter is greater than 0, + * this method uses my own implementation of + * java.net.HttpURLConnection, that uses plain sockets, to create an + * HTTP connection to the given URL. The {@code read} methods called + * on the returned InputStream, will block only for the specified timeout. + * If the timeout expires, a java.io.InterruptedIOException is raised. + *
+ *
+ * + * @param pURL the URL to get. + * @param pTimeout the specified timeout, in milliseconds. + * @return an input stream that reads from the socket connection, created + * from the given URL. + * @throws UnknownHostException if the IP address for the given URL cannot + * be resolved. + * @throws FileNotFoundException if there is no file at the given URL. + * @throws IOException if an error occurs during transfer. + * @see #getInputStreamHttp(URL,int) + * @see com.twelvemonkeys.net.HttpURLConnection + * @see java.net.Socket + * @see java.net.Socket#setSoTimeout(int) setSoTimeout + * @see HttpURLConnection + * @see java.io.InterruptedIOException + * @see RFC 2616 + */ + public static byte[] getBytesHttp(URL pURL, int pTimeout) throws IOException { + // Get the input stream from the url + InputStream in = new BufferedInputStream(getInputStreamHttp(pURL, pTimeout), BUF_SIZE * 2); + + // Get all the bytes in loop + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + int count; + byte[] buffer = new byte[BUF_SIZE]; + + try { + while ((count = in.read(buffer)) != -1) { + // NOTE: According to the J2SE API doc, read(byte[]) will read + // at least 1 byte, or return -1, if end-of-file is reached. + bytes.write(buffer, 0, count); + } + } + finally { + + // Close the buffer + in.close(); + } + return bytes.toByteArray(); + } + + /** + * Unregisters the password asscociated with this URL + */ + + /* + private static void unregisterPassword(URL pURL) { + Authenticator auth = registerAuthenticator(); + if (auth != null && auth instanceof SimpleAuthenticator) + ((SimpleAuthenticator) auth) + .unregisterPasswordAuthentication(pURL); + } + */ + + /** + * Registers the password from the URL string, and returns the URL object. + */ + + /* + private static URL getURLAndRegisterPassword(String pURL) + throws MalformedURLException + { + // Split user/password away from url + String userPass = null; + String protocolPrefix = HTTP; + + int httpIdx = pURL.indexOf(HTTPS); + if (httpIdx >= 0) { + protocolPrefix = HTTPS; + pURL = pURL.substring(httpIdx + HTTPS.length()); + } + else { + httpIdx = pURL.indexOf(HTTP); + if (httpIdx >= 0) + pURL = pURL.substring(httpIdx + HTTP.length()); + } + + int atIdx = pURL.indexOf("@"); + if (atIdx >= 0) { + userPass = pURL.substring(0, atIdx); + pURL = pURL.substring(atIdx + 1); + } + + // Set URL + URL url = new URL(protocolPrefix + pURL); + + // Set Authenticator if user/password is present + if (userPass != null) { + // System.out.println("Setting password ("+ userPass + ")!"); + + int colIdx = userPass.indexOf(":"); + if (colIdx < 0) + throw new MalformedURLException("Error in username/password!"); + + String userName = userPass.substring(0, colIdx); + String passWord = userPass.substring(colIdx + 1); + + // Try to register the authenticator + // System.out.println("Trying to register authenticator!"); + Authenticator auth = registerAuthenticator(); + + // System.out.println("Got authenticator " + auth + "."); + + // Register our username/password with it + if (auth != null && auth instanceof SimpleAuthenticator) { + ((SimpleAuthenticator) auth) + .registerPasswordAuthentication(url, + new PasswordAuthentication(userName, + passWord.toCharArray())); + } + else { + // Not supported! + throw new RuntimeException("Could not register PasswordAuthentication"); + } + } + + return url; + } + */ + + /** + * Registers the Authenticator given in the system property + * {@code java.net.Authenticator}, or the default implementation + * ({@code com.twelvemonkeys.net.SimpleAuthenticator}). + *

+ * BUG: What if authenticator has allready been set outside this class? + * + * @return The Authenticator created and set as default, or null, if it + * was not set as the default. However, there is no (clean) way to + * be sure the authenticator was set (the SimpleAuthenticator uses + * a hack to get around this), so it might be possible that the + * returned authenticator was not set as default... + * @see Authenticator#setDefault(Authenticator) + * @see SimpleAuthenticator + */ + public synchronized static Authenticator registerAuthenticator() { + if (sAuthenticator != null) { + return sAuthenticator; + } + + // Get the system property + String authenticatorName = System.getProperty("java.net.Authenticator"); + + // Try to get the Authenticator from the system property + if (authenticatorName != null) { + try { + Class authenticatorClass = Class.forName(authenticatorName); + + sAuthenticator = (Authenticator) authenticatorClass.newInstance(); + } + catch (ClassNotFoundException cnfe) { + // We should maybe rethrow this? + } + catch (InstantiationException ie) { + // Ignore + } + catch (IllegalAccessException iae) { + // Ignore + } + } + + // Get the default authenticator + if (sAuthenticator == null) { + sAuthenticator = SimpleAuthenticator.getInstance(); + } + + // Register authenticator as default + Authenticator.setDefault(sAuthenticator); + return sAuthenticator; + } + + /** + * Creates the InetAddress object from the given URL. + * Equivalent to calling {@code InetAddress.getByName(URL.getHost())} + * except that it returns null, instead of throwing UnknownHostException. + * + * @param pURL the URL to look up. + * @return the createad InetAddress, or null if the host was unknown. + * @see java.net.InetAddress + * @see java.net.URL + */ + public static InetAddress createInetAddressFromURL(URL pURL) { + try { + return InetAddress.getByName(pURL.getHost()); + } + catch (UnknownHostException e) { + return null; + } + } + + /** + * Creates an URL from the given InetAddress object, using the given + * protocol. + * Equivalent to calling + * {@code new URL(protocol, InetAddress.getHostName(), "")} + * except that it returns null, instead of throwing MalformedURLException. + * + * @param pIP the IP address to look up + * @param pProtocol the protocol to use in the new URL + * @return the created URL or null, if the URL could not be created. + * @see java.net.URL + * @see java.net.InetAddress + */ + public static URL createURLFromInetAddress(InetAddress pIP, String pProtocol) { + try { + return new URL(pProtocol, pIP.getHostName(), ""); + } + catch (MalformedURLException e) { + return null; + } + } + + /** + * Creates an URL from the given InetAddress object, using HTTP protocol. + * Equivalent to calling + * {@code new URL("http", InetAddress.getHostName(), "")} + * except that it returns null, instead of throwing MalformedURLException. + * + * @param pIP the IP address to look up + * @return the created URL or null, if the URL could not be created. + * @see java.net.URL + * @see java.net.InetAddress + */ + public static URL createURLFromInetAddress(InetAddress pIP) { + return createURLFromInetAddress(pIP, HTTP); + } + + /* + * TODO: Benchmark! + */ + static byte[] getBytesHttpOld(String pURL) throws IOException { + // Get the input stream from the url + InputStream in = new BufferedInputStream(getInputStreamHttp(pURL), BUF_SIZE * 2); + + // Get all the bytes in loop + byte[] bytes = new byte[0]; + int count; + byte[] buffer = new byte[BUF_SIZE]; + + try { + while ((count = in.read(buffer)) != -1) { + + // NOTE: According to the J2SE API doc, read(byte[]) will read + // at least 1 byte, or return -1, if end-of-file is reached. + bytes = (byte[]) CollectionUtil.mergeArrays(bytes, 0, bytes.length, buffer, 0, count); + } + } + finally { + + // Close the buffer + in.close(); + } + return bytes; + } } \ No newline at end of file diff --git a/common/common-io/src/main/java/com/twelvemonkeys/net/PasswordAuthenticator.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/net/PasswordAuthenticator.java similarity index 95% rename from common/common-io/src/main/java/com/twelvemonkeys/net/PasswordAuthenticator.java rename to sandbox/sandbox-common/src/main/java/com/twelvemonkeys/net/PasswordAuthenticator.java index 3bbebcbb..81c68f66 100755 --- a/common/common-io/src/main/java/com/twelvemonkeys/net/PasswordAuthenticator.java +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/net/PasswordAuthenticator.java @@ -1,45 +1,45 @@ -/* - * 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. - */ - -package com.twelvemonkeys.net; - -import java.net.*; - -/** - * Interface fro PasswordAuthenticators used by SimpleAuthenticator. - * - * @see SimpleAuthenticator - * @see java.net.Authenticator - * - * @author Harald Kuhr (haraldk@iconmedialab.no) - * - * @version 1.0 - */ -public interface PasswordAuthenticator { - public PasswordAuthentication requestPasswordAuthentication(InetAddress addr, int port, String protocol, String prompt, String scheme); -} +/* + * 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. + */ + +package com.twelvemonkeys.net; + +import java.net.*; + +/** + * Interface fro PasswordAuthenticators used by SimpleAuthenticator. + * + * @see SimpleAuthenticator + * @see java.net.Authenticator + * + * @author Harald Kuhr + * + * @version 1.0 + */ +public interface PasswordAuthenticator { + public PasswordAuthentication requestPasswordAuthentication(InetAddress addr, int port, String protocol, String prompt, String scheme); +} diff --git a/common/common-io/src/main/java/com/twelvemonkeys/net/SimpleAuthenticator.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/net/SimpleAuthenticator.java similarity index 97% rename from common/common-io/src/main/java/com/twelvemonkeys/net/SimpleAuthenticator.java rename to sandbox/sandbox-common/src/main/java/com/twelvemonkeys/net/SimpleAuthenticator.java index d036fb2b..a7830581 100755 --- a/common/common-io/src/main/java/com/twelvemonkeys/net/SimpleAuthenticator.java +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/net/SimpleAuthenticator.java @@ -1,270 +1,270 @@ -/* - * 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. - */ - -package com.twelvemonkeys.net; - -import com.twelvemonkeys.lang.Validate; - -import java.net.Authenticator; -import java.net.InetAddress; -import java.net.PasswordAuthentication; -import java.net.URL; -import java.util.HashMap; -import java.util.Map; - -/** - * A simple Authenticator implementation. - * Singleton class, obtain reference through the static - * {@code getInstance} method. - *

- * After swearing, sweating, pulling my hair, banging my head repeatedly - * into the walls and reading the java.net.Authenticator API documentation - * once more, an idea came to my mind. This is the result. I hope you find it - * useful. -- Harald K. - * - * @author Harald Kuhr (haraldk@iconmedialab.no) - * @version 1.0 - * @see java.net.Authenticator - */ -public class SimpleAuthenticator extends Authenticator { - - /** The reference to the single instance of this class. */ - private static SimpleAuthenticator sInstance = null; - /** Keeps track of the state of this class. */ - private static boolean sInitialized = false; - - // These are used for the identification hack. - private final static String MAGIC = "magic"; - private final static int FOURTYTWO = 42; - - /** Basic authentication scheme. */ - public final static String BASIC = "Basic"; - - /** The hastable that keeps track of the PasswordAuthentications. */ - protected Map passwordAuthentications = null; - - /** The hastable that keeps track of the Authenticators. */ - protected Map authenticators = null; - - /** Creates a SimpleAuthenticator. */ - private SimpleAuthenticator() { - passwordAuthentications = new HashMap(); - authenticators = new HashMap(); - } - - /** - * Gets the SimpleAuthenticator instance and registers it through the - * Authenticator.setDefault(). If there is no current instance - * of the SimpleAuthenticator in the VM, one is created. This method will - * try to figure out if the setDefault() succeeded (a hack), and will - * return null if it was not able to register the instance as default. - * - * @return The single instance of this class, or null, if another - * Authenticator is allready registered as default. - */ - public static synchronized SimpleAuthenticator getInstance() { - if (!sInitialized) { - // Create an instance - sInstance = new SimpleAuthenticator(); - - // Try to set default (this may quietly fail...) - Authenticator.setDefault(sInstance); - - // A hack to figure out if we really did set the authenticator - PasswordAuthentication pa = Authenticator.requestPasswordAuthentication(null, FOURTYTWO, null, null, MAGIC); - - // If this test returns false, we didn't succeed, so we set the - // instance back to null. - if (pa == null || !MAGIC.equals(pa.getUserName()) || !("" + FOURTYTWO).equals(new String(pa.getPassword()))) { - sInstance = null; - } - - // Done - sInitialized = true; - } - - return sInstance; - } - - /** - * Gets the PasswordAuthentication for the request. Called when password - * authorization is needed. - * - * @return The PasswordAuthentication collected from the user, or null if - * none is provided. - */ - protected PasswordAuthentication getPasswordAuthentication() { - // Don't worry, this is just a hack to figure out if we were able - // to set this Authenticator through the setDefault method. - if (!sInitialized && MAGIC.equals(getRequestingScheme()) && getRequestingPort() == FOURTYTWO) { - return new PasswordAuthentication(MAGIC, ("" + FOURTYTWO).toCharArray()); - } - /* - System.err.println("getPasswordAuthentication"); - System.err.println(getRequestingSite()); - System.err.println(getRequestingPort()); - System.err.println(getRequestingProtocol()); - System.err.println(getRequestingPrompt()); - System.err.println(getRequestingScheme()); - */ - - // TODO: - // Look for a more specific PasswordAuthenticatior before using - // Default: - // - // if (...) - // return pa.requestPasswordAuthentication(getRequestingSite(), - // getRequestingPort(), - // getRequestingProtocol(), - // getRequestingPrompt(), - // getRequestingScheme()); - - return passwordAuthentications.get(new AuthKey(getRequestingSite(), - getRequestingPort(), - getRequestingProtocol(), - getRequestingPrompt(), - getRequestingScheme())); - } - - /** Registers a PasswordAuthentication with a given URL address. */ - public PasswordAuthentication registerPasswordAuthentication(URL pURL, PasswordAuthentication pPA) { - return registerPasswordAuthentication(NetUtil.createInetAddressFromURL(pURL), - pURL.getPort(), - pURL.getProtocol(), - null, // Prompt/Realm - BASIC, - pPA); - } - - /** Registers a PasswordAuthentication with a given net address. */ - public PasswordAuthentication registerPasswordAuthentication(InetAddress pAddress, int pPort, String pProtocol, String pPrompt, String pScheme, PasswordAuthentication pPA) { - /* - System.err.println("registerPasswordAuthentication"); - System.err.println(pAddress); - System.err.println(pPort); - System.err.println(pProtocol); - System.err.println(pPrompt); - System.err.println(pScheme); - */ - - return passwordAuthentications.put(new AuthKey(pAddress, pPort, pProtocol, pPrompt, pScheme), pPA); - } - - /** Unregisters a PasswordAuthentication with a given URL address. */ - public PasswordAuthentication unregisterPasswordAuthentication(URL pURL) { - return unregisterPasswordAuthentication(NetUtil.createInetAddressFromURL(pURL), pURL.getPort(), pURL.getProtocol(), null, BASIC); - } - - /** Unregisters a PasswordAuthentication with a given net address. */ - public PasswordAuthentication unregisterPasswordAuthentication(InetAddress pAddress, int pPort, String pProtocol, String pPrompt, String pScheme) { - return passwordAuthentications.remove(new AuthKey(pAddress, pPort, pProtocol, pPrompt, pScheme)); - } - - /** - * TODO: Registers a PasswordAuthenticator that can answer authentication - * requests. - * - * @see PasswordAuthenticator - */ - public void registerPasswordAuthenticator(PasswordAuthenticator pPA, AuthenticatorFilter pFilter) { - authenticators.put(pPA, pFilter); - } - - /** - * TODO: Unregisters a PasswordAuthenticator that can answer authentication - * requests. - * - * @see PasswordAuthenticator - */ - public void unregisterPasswordAuthenticator(PasswordAuthenticator pPA) { - authenticators.remove(pPA); - } -} - -/** - * Utility class, used for caching the PasswordAuthentication objects. - * Everything but address may be null - */ -class AuthKey { - - InetAddress address = null; - int port = -1; - String protocol = null; - String prompt = null; - String scheme = null; - - AuthKey(InetAddress pAddress, int pPort, String pProtocol, String pPrompt, String pScheme) { - Validate.notNull(pAddress, "address"); - - address = pAddress; - port = pPort; - protocol = pProtocol; - prompt = pPrompt; - scheme = pScheme; - - // System.out.println("Created: " + this); - } - - /** Creates a string representation of this object. */ - - public String toString() { - return "AuthKey[" + address + ":" + port + "/" + protocol + " \"" + prompt + "\" (" + scheme + ")]"; - } - - public boolean equals(Object pObj) { - return (pObj instanceof AuthKey && equals((AuthKey) pObj)); - } - - // Ahem.. Breaks the rule from Object.equals(Object): - // It is transitive: for any reference values x, y, and z, if x.equals(y) - // returns true and y.equals(z) returns true, then x.equals(z) - // should return true. - - public boolean equals(AuthKey pKey) { - // Maybe allow nulls, and still be equal? - return (address.equals(pKey.address) - && (port == -1 - || pKey.port == -1 - || port == pKey.port) - && (protocol == null - || pKey.protocol == null - || protocol.equals(pKey.protocol)) - && (prompt == null - || pKey.prompt == null - || prompt.equals(pKey.prompt)) - && (scheme == null - || pKey.scheme == null - || scheme.equalsIgnoreCase(pKey.scheme))); - } - - public int hashCode() { - // There won't be too many pr address, will it? ;-) - return address.hashCode(); - } -} - +/* + * 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. + */ + +package com.twelvemonkeys.net; + +import com.twelvemonkeys.lang.Validate; + +import java.net.Authenticator; +import java.net.InetAddress; +import java.net.PasswordAuthentication; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; + +/** + * A simple Authenticator implementation. + * Singleton class, obtain reference through the static + * {@code getInstance} method. + *

+ * After swearing, sweating, pulling my hair, banging my head repeatedly + * into the walls and reading the java.net.Authenticator API documentation + * once more, an idea came to my mind. This is the result. I hope you find it + * useful. -- Harald K. + * + * @author Harald Kuhr + * @version 1.0 + * @see java.net.Authenticator + */ +public class SimpleAuthenticator extends Authenticator { + /** The reference to the single instance of this class. */ + private static SimpleAuthenticator sInstance = null; + /** Keeps track of the state of this class. */ + private static boolean sInitialized = false; + + // These are used for the identification hack. + private final static String MAGIC = "magic"; + private final static int FOURTYTWO = 42; + + /** Basic authentication scheme. */ + public final static String BASIC = "Basic"; + + /** The hastable that keeps track of the PasswordAuthentications. */ + protected Map passwordAuthentications = null; + + /** The hastable that keeps track of the Authenticators. */ + protected Map authenticators = null; + + /** Creates a SimpleAuthenticator. */ + private SimpleAuthenticator() { + passwordAuthentications = new HashMap(); + authenticators = new HashMap(); + } + + /** + * Gets the SimpleAuthenticator instance and registers it through the + * Authenticator.setDefault(). If there is no current instance + * of the SimpleAuthenticator in the VM, one is created. This method will + * try to figure out if the setDefault() succeeded (a hack), and will + * return null if it was not able to register the instance as default. + * + * @return The single instance of this class, or null, if another + * Authenticator is allready registered as default. + */ + public static synchronized SimpleAuthenticator getInstance() { + if (!sInitialized) { + // Create an instance + sInstance = new SimpleAuthenticator(); + + // Try to set default (this may quietly fail...) + Authenticator.setDefault(sInstance); + + // A hack to figure out if we really did set the authenticator + PasswordAuthentication pa = Authenticator.requestPasswordAuthentication(null, FOURTYTWO, null, null, MAGIC); + + // If this test returns false, we didn't succeed, so we set the + // instance back to null. + if (pa == null || !MAGIC.equals(pa.getUserName()) || !("" + FOURTYTWO).equals(new String(pa.getPassword()))) { + sInstance = null; + } + + // Done + sInitialized = true; + } + + return sInstance; + } + + /** + * Gets the PasswordAuthentication for the request. Called when password + * authorization is needed. + * + * @return The PasswordAuthentication collected from the user, or null if + * none is provided. + */ + protected PasswordAuthentication getPasswordAuthentication() { + // Don't worry, this is just a hack to figure out if we were able + // to set this Authenticator through the setDefault method. + if (!sInitialized && MAGIC.equals(getRequestingScheme()) && getRequestingPort() == FOURTYTWO) { + return new PasswordAuthentication(MAGIC, ("" + FOURTYTWO).toCharArray()); + } + /* + System.err.println("getPasswordAuthentication"); + System.err.println(getRequestingSite()); + System.err.println(getRequestingPort()); + System.err.println(getRequestingProtocol()); + System.err.println(getRequestingPrompt()); + System.err.println(getRequestingScheme()); + */ + + // TODO: + // Look for a more specific PasswordAuthenticatior before using + // Default: + // + // if (...) + // return pa.requestPasswordAuthentication(getRequestingSite(), + // getRequestingPort(), + // getRequestingProtocol(), + // getRequestingPrompt(), + // getRequestingScheme()); + + return passwordAuthentications.get(new AuthKey(getRequestingSite(), + getRequestingPort(), + getRequestingProtocol(), + getRequestingPrompt(), + getRequestingScheme())); + } + + /** Registers a PasswordAuthentication with a given URL address. */ + public PasswordAuthentication registerPasswordAuthentication(URL pURL, PasswordAuthentication pPA) { + return registerPasswordAuthentication(NetUtil.createInetAddressFromURL(pURL), + pURL.getPort(), + pURL.getProtocol(), + null, // Prompt/Realm + BASIC, + pPA); + } + + /** Registers a PasswordAuthentication with a given net address. */ + public PasswordAuthentication registerPasswordAuthentication(InetAddress pAddress, int pPort, String pProtocol, String pPrompt, String pScheme, PasswordAuthentication pPA) { + /* + System.err.println("registerPasswordAuthentication"); + System.err.println(pAddress); + System.err.println(pPort); + System.err.println(pProtocol); + System.err.println(pPrompt); + System.err.println(pScheme); + */ + + return passwordAuthentications.put(new AuthKey(pAddress, pPort, pProtocol, pPrompt, pScheme), pPA); + } + + /** Unregisters a PasswordAuthentication with a given URL address. */ + public PasswordAuthentication unregisterPasswordAuthentication(URL pURL) { + return unregisterPasswordAuthentication(NetUtil.createInetAddressFromURL(pURL), pURL.getPort(), pURL.getProtocol(), null, BASIC); + } + + /** Unregisters a PasswordAuthentication with a given net address. */ + public PasswordAuthentication unregisterPasswordAuthentication(InetAddress pAddress, int pPort, String pProtocol, String pPrompt, String pScheme) { + return passwordAuthentications.remove(new AuthKey(pAddress, pPort, pProtocol, pPrompt, pScheme)); + } + + /** + * TODO: Registers a PasswordAuthenticator that can answer authentication + * requests. + * + * @see PasswordAuthenticator + */ + public void registerPasswordAuthenticator(PasswordAuthenticator pPA, AuthenticatorFilter pFilter) { + authenticators.put(pPA, pFilter); + } + + /** + * TODO: Unregisters a PasswordAuthenticator that can answer authentication + * requests. + * + * @see PasswordAuthenticator + */ + public void unregisterPasswordAuthenticator(PasswordAuthenticator pPA) { + authenticators.remove(pPA); + } +} + +/** + * Utility class, used for caching the PasswordAuthentication objects. + * Everything but address may be null + */ +class AuthKey { + // TODO: Move this class to sandbox? + + InetAddress address = null; + int port = -1; + String protocol = null; + String prompt = null; + String scheme = null; + + AuthKey(InetAddress pAddress, int pPort, String pProtocol, String pPrompt, String pScheme) { + Validate.notNull(pAddress, "address"); + + address = pAddress; + port = pPort; + protocol = pProtocol; + prompt = pPrompt; + scheme = pScheme; + + // System.out.println("Created: " + this); + } + + /** Creates a string representation of this object. */ + + public String toString() { + return "AuthKey[" + address + ":" + port + "/" + protocol + " \"" + prompt + "\" (" + scheme + ")]"; + } + + public boolean equals(Object pObj) { + return (pObj instanceof AuthKey && equals((AuthKey) pObj)); + } + + // Ahem.. Breaks the rule from Object.equals(Object): + // It is transitive: for any reference values x, y, and z, if x.equals(y) + // returns true and y.equals(z) returns true, then x.equals(z) + // should return true. + + public boolean equals(AuthKey pKey) { + // Maybe allow nulls, and still be equal? + return (address.equals(pKey.address) + && (port == -1 + || pKey.port == -1 + || port == pKey.port) + && (protocol == null + || pKey.protocol == null + || protocol.equals(pKey.protocol)) + && (prompt == null + || pKey.prompt == null + || prompt.equals(pKey.prompt)) + && (scheme == null + || pKey.scheme == null + || scheme.equalsIgnoreCase(pKey.scheme))); + } + + public int hashCode() { + // There won't be too many pr address, will it? ;-) + return address.hashCode(); + } +} + diff --git a/sandbox/sandbox-common/src/test/java/com/twelvemonkeys/io/StringInputStreamTest.java b/sandbox/sandbox-common/src/test/java/com/twelvemonkeys/io/StringInputStreamTest.java new file mode 100644 index 00000000..d515e8bd --- /dev/null +++ b/sandbox/sandbox-common/src/test/java/com/twelvemonkeys/io/StringInputStreamTest.java @@ -0,0 +1,126 @@ +package com.twelvemonkeys.io; + +import org.junit.Test; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.Arrays; + +import static org.junit.Assert.*; + +/** + * StringInputStreamTest + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: StringInputStreamTest.java,v 1.0 03.09.13 10:40 haraldk Exp$ + */ +public class StringInputStreamTest { + + static final Charset UTF8 = Charset.forName("UTF-8"); + static final String LONG_STRING = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse id est lobortis, elementum nisi id, mollis urna. Morbi lorem nulla, vehicula ut ultricies ut, blandit sit amet metus. Praesent ut urna et arcu commodo tempus. Aenean dapibus commodo ligula, non vehicula leo dictum a. Aenean at leo ut eros hendrerit pellentesque. Phasellus sagittis arcu non faucibus faucibus. Sed volutpat vulputate metus sed consequat. Aenean auctor sapien sit amet erat dictum laoreet. Nullam libero felis, rutrum scelerisque elit eu, porta mollis nisi. Vestibulum vel ultricies turpis, vel dignissim arcu.\n" + + "Ut convallis erat et dapibus feugiat. Pellentesque eu dictum ligula, et interdum nibh. Sed rutrum justo a leo faucibus eleifend. Proin est justo, porttitor vel nulla egestas, faucibus scelerisque lacus. Vivamus sit amet gravida nibh. Praesent odio diam, ornare vitae mi nec, pretium ultrices tellus. Pellentesque vitae felis consequat mauris lacinia condimentum in ut nibh. In odio quam, laoreet luctus velit vel, suscipit mollis leo. Etiam justo nulla, posuere et massa non, pretium vehicula diam. Sed porta molestie mauris quis condimentum. Sed quis gravida ipsum, eget porttitor felis. Vivamus volutpat velit vitae dolor convallis, nec malesuada est porttitor. Proin sed purus vel leo pretium suscipit. Morbi ut nibh quis tortor vehicula porttitor non sit amet lorem. Proin tempor vel sem sit amet accumsan.\n" + + "Cras vulputate orci a lorem luctus, vel egestas leo porttitor. Duis venenatis odio et mauris molestie rutrum. Mauris gravida volutpat odio at consequat. Mauris eros purus, bibendum in vulputate vitae, laoreet quis libero. Quisque lacinia, neque sed semper fringilla, elit dolor sagittis est, nec tincidunt ipsum risus ut sem. Maecenas consectetur aliquam augue. Etiam neque mi, euismod eget metus quis, molestie lacinia odio. Sed eget sollicitudin metus. Phasellus facilisis augue et sem facilisis, consequat mollis augue ultricies.\n" + + "Vivamus in porta massa. Sed eget lorem non lectus viverra pretium. Curabitur convallis posuere est vestibulum vulputate. Maecenas placerat risus ut dui hendrerit, sed suscipit magna tincidunt. Etiam ut mattis dolor, quis dictum velit. Donec ut dui sit amet libero convallis euismod. Phasellus dapibus dolor in nibh volutpat, eu scelerisque neque tempus. Maecenas a rhoncus velit. Etiam sollicitudin, leo non euismod vehicula, lectus risus aliquet metus, quis cursus purus orci non turpis. Nulla vel enim tortor. Quisque nec mi vulputate, convallis orci vel, suscipit nibh. Sed sed tellus id elit commodo laoreet ut euismod ligula. Mauris suscipit commodo interdum. Phasellus scelerisque arcu nec nibh porta, et semper massa rutrum. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos.\n" + + "Praesent cursus, sapien ut venenatis malesuada, turpis nulla venenatis velit, nec tristique leo turpis auctor purus. Curabitur non porta urna. Sed vitae felis massa. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Phasellus scelerisque id dolor nec fermentum. Etiam suscipit tincidunt odio, sed molestie elit fringilla in. Phasellus nec euismod lacus. Suspendisse bibendum vulputate viverra. Fusce mollis pharetra imperdiet. Phasellus tortor eros, rhoncus volutpat diam in, scelerisque viverra felis. Ut ornare urna commodo, pretium mauris eget, eleifend ipsum."; + static final String SHORT_STRING = "Java"; + + @Test + public void testReadShortString() throws IOException { + StringInputStream stream = new StringInputStream(SHORT_STRING, UTF8); + + byte[] value = SHORT_STRING.getBytes(UTF8); + for (int i = 0; i < value.length; i++) { + int read = stream.read(); + assertEquals(String.format("Wrong value at offset %s: '%s' != '%s'", i, String.valueOf((char) value[i]), String.valueOf((char) (byte) read)), value[i] &0xff, read); + } + + assertEquals(-1, stream.read()); + } + + @Test + public void testReadSubString() throws IOException { + StringInputStream stream = new StringInputStream("foo bar xyzzy", 4, 3, UTF8); + + byte[] value = "bar".getBytes(UTF8); + for (int i = 0; i < value.length; i++) { + int read = stream.read(); + assertEquals(String.format("Wrong value at offset %s: '%s' != '%s'", i, String.valueOf((char) value[i]), String.valueOf((char) (byte) read)), value[i] &0xff, read); + } + + assertEquals(-1, stream.read()); + } + + @Test + public void testReadNonAsciiString() throws IOException { + String string = "\u00c6\u00d8\u00c5\u00e6\u00f8\u00e5\u00e1\u00e9\u00c0\u00c8\u00fc\u00dc\u00df"; + StringInputStream stream = new StringInputStream(string, UTF8); + + byte[] value = string.getBytes(UTF8); + for (int i = 0; i < value.length; i++) { + int read = stream.read(); + assertEquals(String.format("Wrong value at offset %s: '%s' != '%s'", i, String.valueOf((char) value[i]), String.valueOf((char) (byte) read)), value[i] &0xff, read); + } + + assertEquals(-1, stream.read()); + } + + @Test + public void testReadLongString() throws IOException { + StringInputStream stream = new StringInputStream(LONG_STRING, UTF8); + + byte[] value = LONG_STRING.getBytes(UTF8); + for (int i = 0; i < value.length; i++) { + int read = stream.read(); + assertEquals(String.format("Wrong value at offset %s: '%s' != '%s'", i, String.valueOf((char) value[i]), String.valueOf((char) (byte) read)), value[i] &0xff, read); + } + + assertEquals(-1, stream.read()); + } + + @Test + public void testReadArrayLongString() throws IOException { + StringInputStream stream = new StringInputStream(LONG_STRING, UTF8); + + byte[] value = LONG_STRING.getBytes(UTF8); + byte[] buffer = new byte[17]; + int count; + for (int i = 0; i < value.length; i += count) { + count = stream.read(buffer); + assertArrayEquals(String.format("Wrong value at offset %s", i), Arrays.copyOfRange(value, i, i + count), Arrays.copyOfRange(buffer, 0, count)); + } + + assertEquals(-1, stream.read()); + } + + @Test + public void testReadArraySkipLongString() throws IOException { + StringInputStream stream = new StringInputStream(LONG_STRING, UTF8); + + byte[] value = LONG_STRING.getBytes(UTF8); + byte[] buffer = new byte[17]; + int count; + for (int i = 0; i < value.length; i += count) { + if (i % 2 == 0) { + count = (int) stream.skip(buffer.length); + } + else { + count = stream.read(buffer); + assertArrayEquals(String.format("Wrong value at offset %s", i), Arrays.copyOfRange(value, i, i + count), Arrays.copyOfRange(buffer, 0, count)); + } + } + + assertEquals(-1, stream.read()); + } + + /*@Test + public */void testPerformance() throws IOException { + for (int i = 0; i < 100000; i++) { + StringInputStream stream = new StringInputStream(LONG_STRING, UTF8); + while(stream.read() != -1) { + stream.available(); + } + } + + } +} diff --git a/sandbox/sandbox-imageio/pom.xml b/sandbox/sandbox-imageio/pom.xml index c574bd69..4974cd80 100644 --- a/sandbox/sandbox-imageio/pom.xml +++ b/sandbox/sandbox-imageio/pom.xml @@ -40,7 +40,7 @@ jar TwelveMonkeys :: Sandbox :: ImageIO - The TwelveMonkeys ImageIO Sandbox. Experimental stuff. Old retired stuff. + The TwelveMonkeys ImageIO Sandbox. New experimental stuff. Old retired stuff. @@ -62,6 +62,12 @@ compile + + com.twelvemonkeys.sandbox + sandbox-common + compile + + com.twelvemonkeys.common common-io diff --git a/sandbox/sandbox-imageio/src/main/java/com/twelvemonkeys/imageio/stream/Base64DataURLImageInputStreamSpi.java b/sandbox/sandbox-imageio/src/main/java/com/twelvemonkeys/imageio/stream/Base64DataURLImageInputStreamSpi.java new file mode 100644 index 00000000..eab14369 --- /dev/null +++ b/sandbox/sandbox-imageio/src/main/java/com/twelvemonkeys/imageio/stream/Base64DataURLImageInputStreamSpi.java @@ -0,0 +1,65 @@ +package com.twelvemonkeys.imageio.stream; + +import com.twelvemonkeys.io.StringInputStream; +import com.twelvemonkeys.io.enc.Base64Decoder; +import com.twelvemonkeys.io.enc.DecoderStream; + +import javax.imageio.spi.ImageInputStreamSpi; +import javax.imageio.stream.FileCacheImageInputStream; +import javax.imageio.stream.ImageInputStream; +import javax.imageio.stream.MemoryCacheImageInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; +import java.util.Locale; + +/** + * Base64DataURLImageInputStreamSpi + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: Base64DataURLImageInputStreamSpi.java,v 1.0 03.09.13 09:35 haraldk Exp$ + */ +public class Base64DataURLImageInputStreamSpi extends ImageInputStreamSpi { + // This is generally a bad idea, because: + // - It is bound to String.class, and not all strings are base64 encoded data URLs. + // - It's better to just create a decoder stream from the base64 stream, and use what's already in ImageIO.... + + public Base64DataURLImageInputStreamSpi() { + super("TwelveMonkeys", "0.1-BETA", String.class); + } + + @Override + public ImageInputStream createInputStreamInstance(final Object input, final boolean useCache, final File cacheDir) throws IOException { + String string = (String) input; + + InputStream stream = createStreamFromBase64(string); + + return useCache && cacheDir != null ? new FileCacheImageInputStream(stream, cacheDir) : new MemoryCacheImageInputStream(stream); + } + + private InputStream createStreamFromBase64(String string) { + if (!string.startsWith("data:")) { + throw new IllegalArgumentException(String.format("Not a data URL: %s", string)); + } + + int index = string.indexOf(';'); + if (index < 0 || !string.regionMatches(index + 1, "base64,", 0, "base64,".length())) { + throw new IllegalArgumentException(String.format("Not base64 encoded: %s", string)); + } + + int offset = index + "base64,".length() + 1; + return new DecoderStream(new StringInputStream(string.substring(offset), Charset.forName("UTF-8")), new Base64Decoder()); + } + + @Override + public boolean canUseCacheFile() { + return true; + } + + @Override + public String getDescription(Locale locale) { + return "Service provider that instantiates a FileCacheImageInputStream or MemoryCacheImageInputStream from a Base64 encoded data string"; + } +} diff --git a/sandbox/sandbox-imageio/src/test/java/com/twelvemonkeys/imageio/stream/Base64DataURLImageInputStreamTest.java b/sandbox/sandbox-imageio/src/test/java/com/twelvemonkeys/imageio/stream/Base64DataURLImageInputStreamTest.java new file mode 100644 index 00000000..60a8e18f --- /dev/null +++ b/sandbox/sandbox-imageio/src/test/java/com/twelvemonkeys/imageio/stream/Base64DataURLImageInputStreamTest.java @@ -0,0 +1,44 @@ +package com.twelvemonkeys.imageio.stream; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import javax.imageio.ImageIO; +import javax.imageio.spi.IIORegistry; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; + +import static org.junit.Assert.*; + +/** + * Base64DataURLImageInputStreamTest + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: Base64DataURLImageInputStreamTest.java,v 1.0 04.09.13 13:47 haraldk Exp$ + */ +public class Base64DataURLImageInputStreamTest { + static final String DATA = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADgAAAAiCAYAAAAKyxrjAAAKMWlDQ1BJQ0MgUHJvZmlsZQAASImdlndUU9kWh8+9N71QkhCKlNBraFICSA29SJEuKjEJEErAkAAiNkRUcERRkaYIMijggKNDkbEiioUBUbHrBBlE1HFwFBuWSWStGd+8ee/Nm98f935rn73P3Wfvfda6AJD8gwXCTFgJgAyhWBTh58WIjYtnYAcBDPAAA2wA4HCzs0IW+EYCmQJ82IxsmRP4F726DiD5+yrTP4zBAP+flLlZIjEAUJiM5/L42VwZF8k4PVecJbdPyZi2NE3OMErOIlmCMlaTc/IsW3z2mWUPOfMyhDwZy3PO4mXw5Nwn4405Er6MkWAZF+cI+LkyviZjg3RJhkDGb+SxGXxONgAoktwu5nNTZGwtY5IoMoIt43kA4EjJX/DSL1jMzxPLD8XOzFouEiSniBkmXFOGjZMTi+HPz03ni8XMMA43jSPiMdiZGVkc4XIAZs/8WRR5bRmyIjvYODk4MG0tbb4o1H9d/JuS93aWXoR/7hlEH/jD9ld+mQ0AsKZltdn6h21pFQBd6wFQu/2HzWAvAIqyvnUOfXEeunxeUsTiLGcrq9zcXEsBn2spL+jv+p8Of0NffM9Svt3v5WF485M4knQxQ143bmZ6pkTEyM7icPkM5p+H+B8H/nUeFhH8JL6IL5RFRMumTCBMlrVbyBOIBZlChkD4n5r4D8P+pNm5lona+BHQllgCpSEaQH4eACgqESAJe2Qr0O99C8ZHA/nNi9GZmJ37z4L+fVe4TP7IFiR/jmNHRDK4ElHO7Jr8WgI0IABFQAPqQBvoAxPABLbAEbgAD+ADAkEoiARxYDHgghSQAUQgFxSAtaAYlIKtYCeoBnWgETSDNnAYdIFj4DQ4By6By2AE3AFSMA6egCnwCsxAEISFyBAVUod0IEPIHLKFWJAb5AMFQxFQHJQIJUNCSAIVQOugUqgcqobqoWboW+godBq6AA1Dt6BRaBL6FXoHIzAJpsFasBFsBbNgTzgIjoQXwcnwMjgfLoK3wJVwA3wQ7oRPw5fgEVgKP4GnEYAQETqiizARFsJGQpF4JAkRIauQEqQCaUDakB6kH7mKSJGnyFsUBkVFMVBMlAvKHxWF4qKWoVahNqOqUQdQnag+1FXUKGoK9RFNRmuizdHO6AB0LDoZnYsuRlegm9Ad6LPoEfQ4+hUGg6FjjDGOGH9MHCYVswKzGbMb0445hRnGjGGmsVisOtYc64oNxXKwYmwxtgp7EHsSewU7jn2DI+J0cLY4X1w8TogrxFXgWnAncFdwE7gZvBLeEO+MD8Xz8MvxZfhGfA9+CD+OnyEoE4wJroRIQiphLaGS0EY4S7hLeEEkEvWITsRwooC4hlhJPEQ8TxwlviVRSGYkNimBJCFtIe0nnSLdIr0gk8lGZA9yPFlM3kJuJp8h3ye/UaAqWCoEKPAUVivUKHQqXFF4pohXNFT0VFysmK9YoXhEcUjxqRJeyUiJrcRRWqVUo3RU6YbStDJV2UY5VDlDebNyi/IF5UcULMWI4kPhUYoo+yhnKGNUhKpPZVO51HXURupZ6jgNQzOmBdBSaaW0b2iDtCkVioqdSrRKnkqNynEVKR2hG9ED6On0Mvph+nX6O1UtVU9Vvuom1TbVK6qv1eaoeajx1UrU2tVG1N6pM9R91NPUt6l3qd/TQGmYaYRr5Grs0Tir8XQObY7LHO6ckjmH59zWhDXNNCM0V2ju0xzQnNbS1vLTytKq0jqj9VSbru2hnaq9Q/uE9qQOVcdNR6CzQ+ekzmOGCsOTkc6oZPQxpnQ1df11Jbr1uoO6M3rGelF6hXrtevf0Cfos/ST9Hfq9+lMGOgYhBgUGrQa3DfGGLMMUw12G/YavjYyNYow2GHUZPTJWMw4wzjduNb5rQjZxN1lm0mByzRRjyjJNM91tetkMNrM3SzGrMRsyh80dzAXmu82HLdAWThZCiwaLG0wS05OZw2xljlrSLYMtCy27LJ9ZGVjFW22z6rf6aG1vnW7daH3HhmITaFNo02Pzq62ZLde2xvbaXPJc37mr53bPfW5nbse322N3055qH2K/wb7X/oODo4PIoc1h0tHAMdGx1vEGi8YKY21mnXdCO3k5rXY65vTW2cFZ7HzY+RcXpkuaS4vLo3nG8/jzGueNueq5clzrXaVuDLdEt71uUnddd457g/sDD30PnkeTx4SnqWeq50HPZ17WXiKvDq/XbGf2SvYpb8Tbz7vEe9CH4hPlU+1z31fPN9m31XfKz95vhd8pf7R/kP82/xsBWgHcgOaAqUDHwJWBfUGkoAVB1UEPgs2CRcE9IXBIYMj2kLvzDecL53eFgtCA0O2h98KMw5aFfR+OCQ8Lrwl/GGETURDRv4C6YMmClgWvIr0iyyLvRJlESaJ6oxWjE6Kbo1/HeMeUx0hjrWJXxl6K04gTxHXHY+Oj45vipxf6LNy5cDzBPqE44foi40V5iy4s1licvvj4EsUlnCVHEtGJMYktie85oZwGzvTSgKW1S6e4bO4u7hOeB28Hb5Lvyi/nTyS5JpUnPUp2Td6ePJninlKR8lTAFlQLnqf6p9alvk4LTduf9ik9Jr09A5eRmHFUSBGmCfsytTPzMoezzLOKs6TLnJftXDYlChI1ZUPZi7K7xTTZz9SAxESyXjKa45ZTk/MmNzr3SJ5ynjBvYLnZ8k3LJ/J9879egVrBXdFboFuwtmB0pefK+lXQqqWrelfrry5aPb7Gb82BtYS1aWt/KLQuLC98uS5mXU+RVtGaorH1futbixWKRcU3NrhsqNuI2ijYOLhp7qaqTR9LeCUXS61LK0rfb+ZuvviVzVeVX33akrRlsMyhbM9WzFbh1uvb3LcdKFcuzy8f2x6yvXMHY0fJjpc7l+y8UGFXUbeLsEuyS1oZXNldZVC1tep9dUr1SI1XTXutZu2m2te7ebuv7PHY01anVVda926vYO/Ner/6zgajhop9mH05+x42Rjf2f836urlJo6m06cN+4X7pgYgDfc2Ozc0tmi1lrXCrpHXyYMLBy994f9Pdxmyrb6e3lx4ChySHHn+b+O31w0GHe4+wjrR9Z/hdbQe1o6QT6lzeOdWV0iXtjusePhp4tLfHpafje8vv9x/TPVZzXOV42QnCiaITn07mn5w+lXXq6enk02O9S3rvnIk9c60vvG/wbNDZ8+d8z53p9+w/ed71/LELzheOXmRd7LrkcKlzwH6g4wf7HzoGHQY7hxyHui87Xe4Znjd84or7ldNXva+euxZw7dLI/JHh61HXb95IuCG9ybv56Fb6ree3c27P3FlzF3235J7SvYr7mvcbfjT9sV3qID0+6j068GDBgztj3LEnP2X/9H686CH5YcWEzkTzI9tHxyZ9Jy8/Xvh4/EnWk5mnxT8r/1z7zOTZd794/DIwFTs1/lz0/NOvm1+ov9j/0u5l73TY9P1XGa9mXpe8UX9z4C3rbf+7mHcTM7nvse8rP5h+6PkY9PHup4xPn34D94Tz++xtAWsAAAAJcEhZcwAACxMAAAsTAQCanBgAAAuQSURBVFiF5ZlbjF3nVcd/69u3c84+x8fjcSYxqZO6xW7r4HpkQ0ByokIVCUrVPvTBRaoqQCpCICFeeADeeOEFCcRLeQhCLRLiovICCEGEUkHaNCSkNk7jtkkd13Xi+DLjuZzbvn1r8bC/c2Ym47apyAMKS9o6+2jO7O/7f+v2X/+NmfFOXb83HFbv5PPeiUvMjLfaw0lSPRRFrEQRHRESIBEhDvelGaUZM2CkykiV83nOb62vp/se9iPYqTSths6RiXDQuX1rR4ADFDCgMsMDtRkV0Jhx23ueKYrFPvYBPJtl1c9kGe+JIlKRe25Ew8Pm5oCec6x5jw9/r832fO4/xr02UmUKbHnPiSRh2bk969cBjMIOKDNqM2raQ9dd+/le0/BMUaQLgKtZVv1qngPwpemU3z8wpCeC2wXKFqdlbJox27VIA8Q/BMQPsruqjMxY856zaeuAZtfaALrrsJrgtTrc7wb6kysNL99JuFRVO3v6TJ7zH2XJl4uCk0nC8W6XeGUFSWOsasB7fFFgRUFdlqw1DXdMmZqxpcpUldthIWjDaiDCS3XNmvfcVeWuKt0QdgAiwvyAFxsF/mo65WyaMhThwSjibJahZjQixEAsQmPGNHgVESozygDwpTsJH1xRmltJC3A1y6rP5DkvVRX/fvEinzpzBgA/mcC2x+oazJA4xuqaqbYbOSiOf5iOudw0DERIRTiXZQAUIR9c8LqjDcPX75Hz97IXq2pxf9173hfHHIkiFDgSRTgR5ommtGEpZkQhrI/3Ha/cDlF1vtfjubJkZMZHV1dZTVPKpsFvbC4e0HFCVZQosKWeJydjrjYNN0+sEscZd4HNtRs8tT3j1OwOnw3hvhJFnElTvlqW9ES47j29cOJN2FgEe/LtkHMsRxH3OYcL0QAwCx5+rWn4UJKQ0OZjY4YLRagOz1grPX0XE69mWXW+1+OlqiIW4akLF/j8uXMUatSmi0VHjS0S/MnJmBdOnAX2511XSv67qlDgsHN8vNvl4SjiWK/HphlNCGnl3mbAhio1cECEXIROALil7X954Lb3HIkiIhF8CNXKjMIMNWNcGzkgfzAcVkfjmD8ZjRiKUAO/MxiQS1teBPC0D0gRvjidLMBpU+ObBvOKmTHauMPho8e4efUyk801AB7PMv7iwfewNplQhMI0o83VMmxqXu71LeFb0YZ3GfLtjveMzHjTe+54zwNRxM93u4v8G6si4VOBbbMdBzyeZXyy06XEGNmO9yJgFHLombLkhRNnF8C08WT9Pkma0slytJmS5wPiOEXCqReAFgWHooipGglKpkZJC3RKqNBmIIKa8a2mYabKDNhW5fkjHyBqClwUI+yE8ncAbrzCySTha1XFy3XNqSThsVAHDoeixMyMk0nCDW2r3YYqZYj3eb+5q8rr3lPXJXgl6XSJ4pg0Sen1BuQHcjZuJvQPDInTncDtAC7LkCgiLwpowIm27cegoV1nHrJjM641Dc/mh4izjCiO6YqHTreNJ1XMFMxQg5cP/hi/VG9xOk33kIAtVdZUW4ATM8aq9J2jNONQYBG1GbEIY1X+aTZj/eRPt0ymHO8Dt3T/UbLXvsnB++4jS7MFwFgESRJctwtRRD4aYepQUWpv7Tohr9SMi3XNvxYFwwMOoW0lOEiyBHCYKuoVVY9rGh7aeIMrnQ5fLgou1W2JWU0Sfn0wYH0OcE2VYSi7nUCVygAuAqa7yi9AnCakScry/Q/QHx4k6w5I8mXGowmbd+5QVuWeXHJJgsu7SByDGfl4jKqjEaMxyDC8CNuqvNm07d28QqRgbR2IXIRIhIrHBJwHVePWgx/g6q0r/MZgAMAhETSkBoArgKEIcw6oZnylKHi+LHm6KEhEuNY0vP6hRxcbTrtdxlsbXPnGpQU4l/XwZoy3t2iqZvHb2gxJUyTLkE6KS9ur49q+6QC3q/nPN2YYLk6wTh9Ne5QqzLwy9Uo5nTAbb1PMRpTTtl1B2y4+vb7OH49GZCF6YoCha8Nhnn+PdTokQC5CQdto5xbFMUmckGVd6rrkwjNP4WuPqlJsbzMZjmianSY9NaNZX+fbr1/npuoeTlmbLWjXvJ99rNPhiU6Hohqj1Rg2b+2rrvssy3i1rllybeV/ta7ph+iLoa2UiQjH4pj3h/sMcAi1KYejiGuAiyLiKMY5h4gjy/r08yHiHEmWce1b00WLmFfRTIRoMOD06dOcvX+FaGkJt7TE5he+iFUVWlUUapSmVAK1tr1szZQvTaf8W1HwuwcOkInwL7MZz1UVvz0Y0AGeLkteCN/fG8eU4SA2zUgRenMPEkD1EGKBCFmwixph7r8oiojjhChqAypyQtbrkWYJvQMH+e5lz3RrfQEO9hKB+vp1qivfwaqKg7/yy2w8+eft4aB4dXhr+1cqwhLCJ7pdfi7LyJ2jMeMXul0+kmX0g6c+mmU8lmUMw/furnUHztGWpXe5LQ64NmMq4AwSgQxorG3yUfiN955GaswUEYdqBNMpVeEoiwpxEb3h8p4QbXYtlhw9uidEMcNUqdVoTPEC3lp2s2HGP85mbztEh84x25Wro0D3YtjpQbe8Z0OV44HI5tKG51ooMuo9jQgignNQVTO2NyaLIjPenvDAsZPcvHqZ6dY60I4vfjTi8nNfe1tFxtMS5sKM98Yxn+v3uR3WfyRJeCRJGKsyBj6cJHw4fN9dZA6KUNHSuxjarj90jkPOkYvwlaLAaCvgL3a7HA1FBsA3DZhSzWY0Vc2jT3x80Sae/us/XVC1ufVEiJeXOXXwGD9RVejWCD8eU1YV296zrcqElm++6T1/M53yYlUxWD5C1suhe4A4jnEuwgCvHpuN8XWNqUcb5aGNN/jNwWChMhxPEsbhsOIOsGUGquQiOBEe63QoA/+szXg4jvnuN59f9MJqNqM/XGL5/gcoZ6M2/IBIZB9VS0SwqsLKEitrdFflrMKEroEWQkvtAARBm5qoGOPSlKzXD43eaHo55hWtG7z3HBu168Ui/O3y8qLRN3MPHnaOsSqJCKUqvTCi1IEA98JIMremqqnqivVbN5mNJoGqQX+Q76NqAFrXMJmhRYGOx0zUKNBW7mBnquiIcCSOoaqQyIE4EBaegzZnLVA1U8/9b3ybY50OfzYa7aNqzHMwF6EXRfREFtLCW8n2ShRRXf5Pbh5fxcURvmmoAKajAOM65XS6j6o1Zlhdo6poUbTgTBc6ThGe34R8XE0SRp0Oz3rFxWHKUKjLmnuR7e8tPcj76y1+vN/fQ7bXvMfmALsi/FdV8clOl2XnFuMR7B+X1pMMlZq6mFE2Ht/v0/iGqqqo63ofVSsALVvAUzUm6C7ZcUc4mheenggPxzErs01ms13jUjHbNy4BPLJ5g0vfZ1xKdzf6Z8qSS1V1z4E3lzZhz3d7zF55kRdOnMXFCdq0QKvJhLHdZTKa0J20VG0uJnUA1+n8wIG32ZWLAB+M223NB95H16/+0IH3Y90uj2fZvoFXTqdpdb7X4wvjMTPgn7/+dT5/7hyfywd7JIu5vvlWyWK3ba7doBhtMtlc41Sa7pEsgHdUsshEvq9kMVbl8OGatbWE+GJZpn+4tFSdSlOeLcuF6NRxQqKtFxU4GAmVtiB/Le/DKy/uEZ3mNrOM02nKZ/OcDVVmZlz1nq+WJeve/69Ep26YHSPgaOCePhxaw47sWAD9RLhGyMG/m06Zy4Z/f+ECnzpzhiyOkTwHvyMbJnGMFgW+CSCBv3ztGwvZ8PhcNuz12FBlW5UrqrzRNEzMuNo0bUv6Ee1ns4z3xTGHnCOjlQ3novPcAXPtpqbt34eztjPEABfLMv2jQ4eqU2nKR1ZXOZkEMp3n+4Rf8Z6eKlOFO6Y80e3yUwHMZpA2CKe+EkXcCu1HzRg4x8rbEH5fD+r2PuGXHW6pu8QqQnjOhd/SjFfHSmPCu166///z8mW3vZten72jLxv/L74A/R9e1ZBJD0Hq0AAAAABJRU5ErkJggg=="; + static final Base64DataURLImageInputStreamSpi provider = new Base64DataURLImageInputStreamSpi(); + + @Before + public void init() { + IIORegistry.getDefaultInstance().registerServiceProvider(provider); + } + + @After + public void destroy() { + IIORegistry.getDefaultInstance().deregisterServiceProvider(provider); + } + + @Test + public void testRead() throws IOException, InvocationTargetException, InterruptedException { + BufferedImage image = ImageIO.read(ImageIO.createImageInputStream(DATA)); + + assertNotNull(image); + assertEquals(56, image.getWidth()); + assertEquals(34, image.getHeight()); + } +} diff --git a/servlet/pom.xml b/servlet/pom.xml index a59056e0..8e46c060 100644 --- a/servlet/pom.xml +++ b/servlet/pom.xml @@ -104,7 +104,7 @@ org.apache.maven.plugins maven-jar-plugin - 2.2 + 2.4 diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/GenericFilter.java b/servlet/src/main/java/com/twelvemonkeys/servlet/GenericFilter.java index c7cc82d6..29541b16 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/GenericFilter.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/GenericFilter.java @@ -50,10 +50,10 @@ import java.util.Enumeration; *

* To write a generic filter, you need only override the abstract * {@link #doFilterImpl(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain)} doFilterImpl} method. @@ -67,6 +67,7 @@ import java.util.Enumeration; * @see FilterConfig */ public abstract class GenericFilter implements Filter, FilterConfig, Serializable { + // TODO: Rewrite to use ServletConfigurator instead of BeanUtil /** * The filter config. @@ -76,32 +77,29 @@ public abstract class GenericFilter implements Filter, FilterConfig, Serializabl /** * Makes sure the filter runs once per request *

- * see #isRunOnce - * + * @see #isRunOnce + * @see #ATTRIB_RUN_ONCE_VALUE * @see #oncePerRequest - * see #ATTRIB_RUN_ONCE_VALUE */ private final static String ATTRIB_RUN_ONCE_EXT = ".REQUEST_HANDLED"; /** * Makes sure the filter runs once per request. * Must be configured through init method, as the filter name is not - * available before we have a FitlerConfig object. + * available before we have a {@code FilterConfig} object. *

- * see #isRunOnce - * + * @see #isRunOnce + * @see #ATTRIB_RUN_ONCE_VALUE * @see #oncePerRequest - * see #ATTRIB_RUN_ONCE_VALUE */ private String attribRunOnce = null; /** * Makes sure the filter runs once per request *

- * see #isRunOnce - * + * @see #isRunOnce + * @see #ATTRIB_RUN_ONCE_EXT * @see #oncePerRequest - * see #ATTRIB_RUN_ONCE_EXT */ private static final Object ATTRIB_RUN_ONCE_VALUE = new Object(); @@ -213,16 +211,16 @@ public abstract class GenericFilter implements Filter, FilterConfig, Serializabl * and returns false. * A return value of false, indicates that the filter has not yet run. * A return value of true, indicates that the filter has run for this - * request, and processing should not contine. + * request, and processing should not continue. *

* Note that the method will mark the request as filtered on first * invocation. *

- * see #ATTRIB_RUN_ONCE_EXT - * see #ATTRIB_RUN_ONCE_VALUE + * @see #ATTRIB_RUN_ONCE_EXT + * @see #ATTRIB_RUN_ONCE_VALUE * * @param pRequest the servlet request - * @return {@code true} if the request is allready filtered, otherwise + * @return {@code true} if the request is already filtered, otherwise * {@code false}. */ private boolean isRunOnce(final ServletRequest pRequest) { @@ -233,6 +231,7 @@ public abstract class GenericFilter implements Filter, FilterConfig, Serializabl // Set attribute and return false (continue) pRequest.setAttribute(attribRunOnce, ATTRIB_RUN_ONCE_VALUE); + return false; } @@ -286,7 +285,6 @@ public abstract class GenericFilter implements Filter, FilterConfig, Serializabl * @see ServletContext */ public ServletContext getServletContext() { - // TODO: Create a servlet context wrapper that lets you log to a log4j appender? return filterConfig.getServletContext(); } @@ -347,6 +345,7 @@ public abstract class GenericFilter implements Filter, FilterConfig, Serializabl * * @deprecated For compatibility only, use {@link #init init} instead. */ + @SuppressWarnings("UnusedDeclaration") public void setFilterConfig(final FilterConfig pFilterConfig) { try { init(pFilterConfig); diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/GenericServlet.java b/servlet/src/main/java/com/twelvemonkeys/servlet/GenericServlet.java index 60535841..56641a05 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/GenericServlet.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/GenericServlet.java @@ -39,7 +39,7 @@ import java.lang.reflect.InvocationTargetException; *

* {@code GenericServlet} has an auto-init system, that automatically invokes * the method matching the signature {@code void setX(<Type>)}, - * for every init-parameter {@code x}. Both camelCase and lisp-style paramter + * for every init-parameter {@code x}. Both camelCase and lisp-style parameter * naming is supported, lisp-style names will be converted to camelCase. * Parameter values are automatically converted from string representation to * most basic types, if necessary. @@ -50,6 +50,7 @@ import java.lang.reflect.InvocationTargetException; * @version $Id: GenericServlet.java#1 $ */ public abstract class GenericServlet extends javax.servlet.GenericServlet { + // TODO: Rewrite to use ServletConfigurator instead of BeanUtil /** * Called by the web container to indicate to a servlet that it is being diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/HttpServlet.java b/servlet/src/main/java/com/twelvemonkeys/servlet/HttpServlet.java index da91650c..f1ab6060 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/HttpServlet.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/HttpServlet.java @@ -39,7 +39,7 @@ import java.lang.reflect.InvocationTargetException; *

* {@code HttpServlet} has an auto-init system, that automatically invokes * the method matching the signature {@code void setX(<Type>)}, - * for every init-parameter {@code x}. Both camelCase and lisp-style paramter + * for every init-parameter {@code x}. Both camelCase and lisp-style parameter * naming is supported, lisp-style names will be converted to camelCase. * Parameter values are automatically converted from string representation to * most basic types, if necessary. @@ -50,6 +50,7 @@ import java.lang.reflect.InvocationTargetException; * @version $Id: HttpServlet.java#1 $ */ public abstract class HttpServlet extends javax.servlet.http.HttpServlet { + // TODO: Rewrite to use ServletConfigurator instead of BeanUtil /** * Called by the web container to indicate to a servlet that it is being diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/ServletResponseStreamDelegate.java b/servlet/src/main/java/com/twelvemonkeys/servlet/ServletResponseStreamDelegate.java index 853c5a6c..36493c83 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/ServletResponseStreamDelegate.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/ServletResponseStreamDelegate.java @@ -28,17 +28,20 @@ package com.twelvemonkeys.servlet; -import com.twelvemonkeys.lang.Validate; - import javax.servlet.ServletOutputStream; import javax.servlet.ServletResponse; import java.io.IOException; +import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.PrintWriter; -import java.io.OutputStream; + +import static com.twelvemonkeys.lang.Validate.notNull; /** * A delegate for handling stream support in wrapped servlet responses. + *

+ * Client code should delegate {@code getOutputStream}, {@code getWriter}, + * {@code flushBuffer} and {@code resetBuffer} methods from the servlet response. * * @author Harald Kuhr * @author last modified by $Author: haku $ @@ -48,35 +51,33 @@ public class ServletResponseStreamDelegate { private Object out = null; protected final ServletResponse response; - public ServletResponseStreamDelegate(ServletResponse pResponse) { - response = Validate.notNull(pResponse, "response"); + public ServletResponseStreamDelegate(final ServletResponse pResponse) { + response = notNull(pResponse, "response"); } - // NOTE: Intentionally NOT threadsafe, as one request/response should be - // handled by one thread ONLY. + // NOTE: Intentionally NOT thread safe, as one request/response should be handled by one thread ONLY. public final ServletOutputStream getOutputStream() throws IOException { if (out == null) { OutputStream out = createOutputStream(); this.out = out instanceof ServletOutputStream ? out : new OutputStreamAdapter(out); } else if (out instanceof PrintWriter) { - throw new IllegalStateException("getWriter() allready called."); + throw new IllegalStateException("getWriter() already called."); } return (ServletOutputStream) out; } - // NOTE: Intentionally NOT threadsafe, as one request/response should be - // handled by one thread ONLY. + // NOTE: Intentionally NOT thread safe, as one request/response should be handled by one thread ONLY. public final PrintWriter getWriter() throws IOException { if (out == null) { - // NOTE: getCharacterEncoding may should not return null + // NOTE: getCharacterEncoding may/should not return null OutputStream out = createOutputStream(); String charEncoding = response.getCharacterEncoding(); this.out = new PrintWriter(charEncoding != null ? new OutputStreamWriter(out, charEncoding) : new OutputStreamWriter(out)); } else if (out instanceof ServletOutputStream) { - throw new IllegalStateException("getOutputStream() allready called."); + throw new IllegalStateException("getOutputStream() already called."); } return (PrintWriter) out; @@ -84,8 +85,9 @@ public class ServletResponseStreamDelegate { /** * Returns the {@code OutputStream}. - * Override this method to provide a decoreated outputstream. - * This method is guaranteed to be invoked only once for a request/response. + * Subclasses should override this method to provide a decorated output stream. + * This method is guaranteed to be invoked only once for a request/response + * (unless {@code resetBuffer} is invoked). *

* This implementation simply returns the output stream from the wrapped * response. @@ -107,7 +109,6 @@ public class ServletResponseStreamDelegate { } public void resetBuffer() { - // TODO: Is this okay? Probably not... :-) out = null; } } diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/cache/CacheResponseWrapper.java b/servlet/src/main/java/com/twelvemonkeys/servlet/cache/CacheResponseWrapper.java index 1db91f9f..a666d68e 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/cache/CacheResponseWrapper.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/cache/CacheResponseWrapper.java @@ -29,7 +29,7 @@ package com.twelvemonkeys.servlet.cache; import com.twelvemonkeys.lang.StringUtil; -import com.twelvemonkeys.net.NetUtil; +import com.twelvemonkeys.net.HTTPUtil; import com.twelvemonkeys.servlet.ServletResponseStreamDelegate; import javax.servlet.ServletOutputStream; @@ -212,7 +212,7 @@ class CacheResponseWrapper extends HttpServletResponseWrapper { if (Boolean.FALSE.equals(cacheable)) { super.setDateHeader(pName, pValue); } - cachedResponse.setHeader(pName, NetUtil.formatHTTPDate(pValue)); + cachedResponse.setHeader(pName, HTTPUtil.formatHTTPDate(pValue)); } public void addDateHeader(String pName, long pValue) { @@ -220,7 +220,7 @@ class CacheResponseWrapper extends HttpServletResponseWrapper { if (Boolean.FALSE.equals(cacheable)) { super.addDateHeader(pName, pValue); } - cachedResponse.addHeader(pName, NetUtil.formatHTTPDate(pValue)); + cachedResponse.addHeader(pName, HTTPUtil.formatHTTPDate(pValue)); } public void setHeader(String pName, String pValue) { diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/cache/HTTPCache.java b/servlet/src/main/java/com/twelvemonkeys/servlet/cache/HTTPCache.java index d6cce8bd..fc655de0 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/cache/HTTPCache.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/cache/HTTPCache.java @@ -32,7 +32,7 @@ import com.twelvemonkeys.io.FileUtil; import com.twelvemonkeys.lang.StringUtil; import com.twelvemonkeys.lang.Validate; import com.twelvemonkeys.net.MIMEUtil; -import com.twelvemonkeys.net.NetUtil; +import com.twelvemonkeys.net.HTTPUtil; import com.twelvemonkeys.util.LRUHashMap; import com.twelvemonkeys.util.NullMap; @@ -972,7 +972,7 @@ public class HTTPCache { File cached = getCachedFile(pCacheURI, pRequest); if (cached != null && cached.exists()) { lastModified = cached.lastModified(); - //// System.out.println(" ## HTTPCache ## Last-Modified is " + NetUtil.formatHTTPDate(lastModified) + ", using cachedFile.lastModified()"); + //// System.out.println(" ## HTTPCache ## Last-Modified is " + HTTPUtil.formatHTTPDate(lastModified) + ", using cachedFile.lastModified()"); } } */ @@ -981,11 +981,11 @@ public class HTTPCache { int maxAge = getIntHeader(response, HEADER_CACHE_CONTROL, "max-age"); if (maxAge == -1) { expires = lastModified + defaultExpiryTime; - //// System.out.println(" ## HTTPCache ## Expires is " + NetUtil.formatHTTPDate(expires) + ", using lastModified + defaultExpiry"); + //// System.out.println(" ## HTTPCache ## Expires is " + HTTPUtil.formatHTTPDate(expires) + ", using lastModified + defaultExpiry"); } else { expires = lastModified + (maxAge * 1000L); // max-age is seconds - //// System.out.println(" ## HTTPCache ## Expires is " + NetUtil.formatHTTPDate(expires) + ", using lastModified + maxAge"); + //// System.out.println(" ## HTTPCache ## Expires is " + HTTPUtil.formatHTTPDate(expires) + ", using lastModified + maxAge"); } } /* @@ -997,7 +997,7 @@ public class HTTPCache { // Expired? if (expires < now) { // System.out.println(" ## HTTPCache ## Content is stale (content expired: " - // + NetUtil.formatHTTPDate(expires) + " before " + NetUtil.formatHTTPDate(now) + ")."); + // + HTTPUtil.formatHTTPDate(expires) + " before " + HTTPUtil.formatHTTPDate(now) + ")."); return true; } @@ -1008,7 +1008,7 @@ public class HTTPCache { File cached = getCachedFile(pCacheURI, pRequest); if (cached != null && cached.exists()) { lastModified = cached.lastModified(); - //// System.out.println(" ## HTTPCache ## Last-Modified is " + NetUtil.formatHTTPDate(lastModified) + ", using cachedFile.lastModified()"); + //// System.out.println(" ## HTTPCache ## Last-Modified is " + HTTPUtil.formatHTTPDate(lastModified) + ", using cachedFile.lastModified()"); } } */ @@ -1018,7 +1018,7 @@ public class HTTPCache { //noinspection RedundantIfStatement if (real != null && real.exists() && real.lastModified() > lastModified) { // System.out.println(" ## HTTPCache ## Content is stale (new content" - // + NetUtil.formatHTTPDate(lastModified) + " before " + NetUtil.formatHTTPDate(real.lastModified()) + ")."); + // + HTTPUtil.formatHTTPDate(lastModified) + " before " + HTTPUtil.formatHTTPDate(real.lastModified()) + ")."); return true; } @@ -1082,7 +1082,7 @@ public class HTTPCache { static long getDateHeader(final String pHeaderValue) { long date = -1L; if (pHeaderValue != null) { - date = NetUtil.parseHTTPDate(pHeaderValue); + date = HTTPUtil.parseHTTPDate(pHeaderValue); } return date; } @@ -1147,4 +1147,4 @@ public class HTTPCache { } } -} +} \ No newline at end of file diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/cache/SerlvetCacheResponseWrapper.java b/servlet/src/main/java/com/twelvemonkeys/servlet/cache/SerlvetCacheResponseWrapper.java index 0b616ab4..1576cc38 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/cache/SerlvetCacheResponseWrapper.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/cache/SerlvetCacheResponseWrapper.java @@ -29,7 +29,7 @@ package com.twelvemonkeys.servlet.cache; import com.twelvemonkeys.lang.StringUtil; -import com.twelvemonkeys.net.NetUtil; +import com.twelvemonkeys.net.HTTPUtil; import com.twelvemonkeys.servlet.ServletResponseStreamDelegate; import javax.servlet.ServletOutputStream; @@ -224,7 +224,7 @@ class SerlvetCacheResponseWrapper extends HttpServletResponseWrapper { if (Boolean.FALSE.equals(cacheable)) { super.setDateHeader(pName, pValue); } - cacheResponse.setHeader(pName, NetUtil.formatHTTPDate(pValue)); + cacheResponse.setHeader(pName, HTTPUtil.formatHTTPDate(pValue)); } public void addDateHeader(String pName, long pValue) { @@ -232,7 +232,7 @@ class SerlvetCacheResponseWrapper extends HttpServletResponseWrapper { if (Boolean.FALSE.equals(cacheable)) { super.addDateHeader(pName, pValue); } - cacheResponse.addHeader(pName, NetUtil.formatHTTPDate(pValue)); + cacheResponse.addHeader(pName, HTTPUtil.formatHTTPDate(pValue)); } public void setHeader(String pName, String pValue) { diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/cache/WritableCachedResponseImpl.java b/servlet/src/main/java/com/twelvemonkeys/servlet/cache/WritableCachedResponseImpl.java index 4565e593..19085b9c 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/cache/WritableCachedResponseImpl.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/cache/WritableCachedResponseImpl.java @@ -29,7 +29,7 @@ package com.twelvemonkeys.servlet.cache; import com.twelvemonkeys.io.FastByteArrayOutputStream; -import com.twelvemonkeys.net.NetUtil; +import com.twelvemonkeys.net.HTTPUtil; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -53,7 +53,7 @@ class WritableCachedResponseImpl implements WritableCachedResponse { protected WritableCachedResponseImpl() { cachedResponse = new CachedResponseImpl(); // Hmmm.. - setHeader(HTTPCache.HEADER_CACHED_TIME, NetUtil.formatHTTPDate(System.currentTimeMillis())); + setHeader(HTTPCache.HEADER_CACHED_TIME, HTTPUtil.formatHTTPDate(System.currentTimeMillis())); } public CachedResponse getCachedResponse() { diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/image/IIOProviderContextListener.java b/servlet/src/main/java/com/twelvemonkeys/servlet/image/IIOProviderContextListener.java index 8a30e8ba..cee1f7d5 100644 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/image/IIOProviderContextListener.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/image/IIOProviderContextListener.java @@ -33,7 +33,9 @@ import javax.imageio.spi.IIORegistry; import javax.imageio.spi.ServiceRegistry; import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; +import java.util.ArrayList; import java.util.Iterator; +import java.util.List; /** * Takes care of registering and de-registering local ImageIO plugins (service providers) for the servlet context. @@ -66,8 +68,13 @@ public final class IIOProviderContextListener implements ServletContextListener Class category = categories.next(); Iterator providers = registry.getServiceProviders(category, localFilter, false); + // Copy the providers, as de-registering while iterating over providers will lead to ConcurrentModificationExceptions. + List providersCopy = new ArrayList(); while (providers.hasNext()) { - Object provider = providers.next(); + providersCopy.add(providers.next()); + } + + for (Object provider : providersCopy) { registry.deregisterServiceProvider(provider); event.getServletContext().log(String.format("Unregistered locally installed provider class: %s", provider.getClass())); } diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/image/ImageServletResponseImpl.java b/servlet/src/main/java/com/twelvemonkeys/servlet/image/ImageServletResponseImpl.java index c4c013b5..9e352828 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/image/ImageServletResponseImpl.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/image/ImageServletResponseImpl.java @@ -63,17 +63,15 @@ import java.util.Iterator; * The response also automatically handles writing the image back to the underlying response stream * in the preferred format, when the response is flushed. *

- * * @author Harald Kuhr * @version $Id: ImageServletResponseImpl.java#10 $ - * */ // TODO: Refactor out HTTP specifics (if possible). // TODO: Is it a good ide to throw IIOException? // TODO: This implementation has a problem if two filters does scaling, as the second will overwrite the SIZE attribute // TODO: Allow different scaling algorithm based on input image (use case: IndexColorModel does not scale well using default, smooth may be slow for large images) +// TODO: Support pluggable pre- and post-processing steps class ImageServletResponseImpl extends HttpServletResponseWrapper implements ImageServletResponse { - private ServletRequest originalRequest; private final ServletContext context; private final ServletResponseStreamDelegate streamDelegate; @@ -223,6 +221,9 @@ class ImageServletResponseImpl extends HttpServletResponseWrapper implements Ima // The default JPEG quality is not good enough, so always adjust compression/quality if ((requestQuality != null || "jpeg".equalsIgnoreCase(getFormatNameSafe(writer))) && param.canWriteCompressed()) { + // TODO: See http://blog.apokalyptik.com/2009/09/16/quality-time-with-your-jpegs/ for better adjusting the (default) JPEG quality + // OR: Use the metadata of the original image + param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); // WORKAROUND: Known bug in GIFImageWriter in certain JDK versions, compression type is not set by default @@ -234,10 +235,13 @@ class ImageServletResponseImpl extends HttpServletResponseWrapper implements Ima } if ("gif".equalsIgnoreCase(getFormatNameSafe(writer)) && !(image.getColorModel() instanceof IndexColorModel) - && image.getColorModel().getTransparency() != Transparency.OPAQUE) { + /*&& image.getColorModel().getTransparency() != Transparency.OPAQUE*/) { // WORKAROUND: Bug in GIFImageWriter may throw NPE if transparent pixels // See: http://bugs.sun.com/view_bug.do?bug_id=6287936 - image = ImageUtil.createIndexed(ImageUtil.toBuffered(image), 256, null, ImageUtil.TRANSPARENCY_BITMASK | ImageUtil.DITHER_DIFFUSION_ALTSCANS); + image = ImageUtil.createIndexed( + ImageUtil.toBuffered(image), 256, null, + (image.getColorModel().getTransparency() == Transparency.OPAQUE ? ImageUtil.TRANSPARENCY_OPAQUE : ImageUtil.TRANSPARENCY_BITMASK) | ImageUtil.DITHER_DIFFUSION_ALTSCANS + ); } ////////////////// ImageOutputStream stream = ImageIO.createImageOutputStream(out); @@ -425,18 +429,29 @@ class ImageServletResponseImpl extends HttpServletResponseWrapper implements Ima if (image != null && size != null && (image.getWidth() != size.width || image.getHeight() != size.height)) { int resampleAlgorithm = getResampleAlgorithmFromRequest(); + // TODO: One possibility is to NOT handle index color here, and only handle it later, IF NEEDED (read: GIF, + // possibly also for PNG) when we know the output format (flush method). + // This will make the filter faster (and better quality, possibly at the expense of more bytes being sent + // over the wire) in the general case. Who uses GIF nowadays anyway? + // Also, this means we could either keep the original IndexColorModel in the filter, or go through the + // expensive operation of re-calculating the optimal palette for the new image (the latter might improve quality). + // NOTE: Only use createScaled if IndexColorModel, as it's more expensive due to color conversion - if (image.getColorModel() instanceof IndexColorModel) { - return ImageUtil.createScaled(image, size.width, size.height, resampleAlgorithm); +/* if (image.getColorModel() instanceof IndexColorModel) { +// return ImageUtil.createScaled(image, size.width, size.height, resampleAlgorithm); + BufferedImage resampled = ImageUtil.createResampled(image, size.width, size.height, resampleAlgorithm); + return ImageUtil.createIndexed(resampled, (IndexColorModel) image.getColorModel(), null, ImageUtil.DITHER_NONE | ImageUtil.TRANSPARENCY_BITMASK); +// return ImageUtil.createIndexed(resampled, 256, null, ImageUtil.COLOR_SELECTION_QUALITY | ImageUtil.DITHER_NONE | ImageUtil.TRANSPARENCY_BITMASK); } else { + */ return ImageUtil.createResampled(image, size.width, size.height, resampleAlgorithm); - } +// } } return image; } - private int getResampleAlgorithmFromRequest() { + int getResampleAlgorithmFromRequest() { Object algorithm = originalRequest.getAttribute(ATTRIB_IMAGE_RESAMPLE_ALGORITHM); if (algorithm instanceof Integer && ((Integer) algorithm == Image.SCALE_SMOOTH || (Integer) algorithm == Image.SCALE_FAST || (Integer) algorithm == Image.SCALE_DEFAULT)) { return (Integer) algorithm; @@ -445,6 +460,7 @@ class ImageServletResponseImpl extends HttpServletResponseWrapper implements Ima if (algorithm != null) { context.log("WARN: Illegal image resampling algorithm: " + algorithm); } + return BufferedImage.SCALE_DEFAULT; } } diff --git a/servlet/src/test/java/com/twelvemonkeys/servlet/cache/HTTPCacheTestCase.java b/servlet/src/test/java/com/twelvemonkeys/servlet/cache/HTTPCacheTestCase.java index b5b69f45..dbe759aa 100755 --- a/servlet/src/test/java/com/twelvemonkeys/servlet/cache/HTTPCacheTestCase.java +++ b/servlet/src/test/java/com/twelvemonkeys/servlet/cache/HTTPCacheTestCase.java @@ -1,6 +1,6 @@ package com.twelvemonkeys.servlet.cache; -import com.twelvemonkeys.net.NetUtil; +import com.twelvemonkeys.net.HTTPUtil; import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; @@ -644,7 +644,7 @@ public class HTTPCacheTestCase { CacheResponse res = (CacheResponse) invocation.getArguments()[1]; res.setStatus(HttpServletResponse.SC_OK); - res.setHeader("Date", NetUtil.formatHTTPDate(System.currentTimeMillis())); + res.setHeader("Date", HTTPUtil.formatHTTPDate(System.currentTimeMillis())); res.setHeader("Cache-Control", "public"); res.addHeader("X-Custom", "FOO"); res.addHeader("X-Custom", "BAR"); @@ -1126,7 +1126,7 @@ public class HTTPCacheTestCase { CacheResponse res = (CacheResponse) invocation.getArguments()[1]; res.setStatus(status); - res.setHeader("Date", NetUtil.formatHTTPDate(System.currentTimeMillis())); + res.setHeader("Date", HTTPUtil.formatHTTPDate(System.currentTimeMillis())); for (Map.Entry> header : headers.entrySet()) { for (String value : header.getValue()) { diff --git a/servlet/src/test/java/com/twelvemonkeys/servlet/image/IIOProviderContextListenerTest.java b/servlet/src/test/java/com/twelvemonkeys/servlet/image/IIOProviderContextListenerTest.java new file mode 100644 index 00000000..02f96a0c --- /dev/null +++ b/servlet/src/test/java/com/twelvemonkeys/servlet/image/IIOProviderContextListenerTest.java @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2014, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.servlet.image; + +import org.junit.Test; + +import javax.imageio.ImageReader; +import javax.imageio.spi.IIORegistry; +import javax.imageio.spi.ImageReaderSpi; +import javax.servlet.ServletContext; +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; +import java.io.IOException; +import java.util.Locale; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * IIOProviderContextListenerTest + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: IIOProviderContextListenerTest.java,v 1.0 02.01.14 12:33 haraldk Exp$ + */ +public class IIOProviderContextListenerTest { + @Test + public void testContextInitialized() { + ServletContextListener listener = new IIOProviderContextListener(); + listener.contextInitialized(mock(ServletContextEvent.class)); + } + + @Test + public void testContextDestroyed() { + ServletContext context = mock(ServletContext.class); + ServletContextEvent destroyed = mock(ServletContextEvent.class); + when(destroyed.getServletContext()).thenReturn(context); + + ServletContextListener listener = new IIOProviderContextListener(); + listener.contextInitialized(mock(ServletContextEvent.class)); + listener.contextDestroyed(destroyed); + } + + // Regression test for issue #29 + @Test + public void testDestroyConcurrentModRegression() { + ServletContext context = mock(ServletContext.class); + ServletContextEvent destroyed = mock(ServletContextEvent.class); + when(destroyed.getServletContext()).thenReturn(context); + + ServletContextListener listener = new IIOProviderContextListener(); + listener.contextInitialized(mock(ServletContextEvent.class)); + + ImageReaderSpi provider1 = new MockImageReaderSpiOne(); + ImageReaderSpi provider2 = new MockImageReaderSpiToo(); + + // NOTE: Fake registering for simplicity, but it still exposes the original problem with de-registering + IIORegistry registry = IIORegistry.getDefaultInstance(); + registry.registerServiceProvider(provider1); + registry.registerServiceProvider(provider2); + assertTrue(registry.contains(provider1)); + assertTrue(registry.contains(provider2)); + + listener.contextDestroyed(destroyed); + + assertFalse(registry.contains(provider1)); + assertFalse(registry.contains(provider2)); + } + + private static abstract class MockImageReaderSpiBase extends ImageReaderSpi { + @Override + public boolean canDecodeInput(Object source) throws IOException { + return false; + } + + @Override + public ImageReader createReaderInstance(Object extension) throws IOException { + return null; + } + + @Override + public String getDescription(Locale locale) { + return "I'm a mock. So don't mock me."; + } + } + + private static final class MockImageReaderSpiOne extends MockImageReaderSpiBase { + } + + private static final class MockImageReaderSpiToo extends MockImageReaderSpiBase { + } +} diff --git a/servlet/src/test/java/com/twelvemonkeys/servlet/image/ImageServletResponseImplTestCase.java b/servlet/src/test/java/com/twelvemonkeys/servlet/image/ImageServletResponseImplTestCase.java index 5210151d..c42f316d 100755 --- a/servlet/src/test/java/com/twelvemonkeys/servlet/image/ImageServletResponseImplTestCase.java +++ b/servlet/src/test/java/com/twelvemonkeys/servlet/image/ImageServletResponseImplTestCase.java @@ -2,10 +2,13 @@ package com.twelvemonkeys.servlet.image; import com.twelvemonkeys.image.BufferedImageIcon; import com.twelvemonkeys.image.ImageUtil; +import com.twelvemonkeys.io.FastByteArrayOutputStream; import com.twelvemonkeys.io.FileUtil; import com.twelvemonkeys.servlet.OutputStreamAdapter; import org.junit.Before; import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; import javax.imageio.ImageIO; import javax.servlet.ServletContext; @@ -15,8 +18,8 @@ import javax.servlet.http.HttpServletResponse; import javax.swing.*; import java.awt.*; import java.awt.image.BufferedImage; +import java.awt.image.IndexColorModel; import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.InvocationTargetException; @@ -30,7 +33,7 @@ import static org.mockito.Mockito.*; * * @author Harald Kuhr * @author last modified by $Author: haku $ - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/test/java/com/twelvemonkeys/servlet/image/ImageServletResponseImplTestCase.java#6 $ + * @version $Id: twelvemonkeys-servlet/src/test/java/com/twelvemonkeys/servlet/image/ImageServletResponseImplTestCase.java#6 $ */ public class ImageServletResponseImplTestCase { private static final String CONTENT_TYPE_BMP = "image/bmp"; @@ -42,9 +45,13 @@ public class ImageServletResponseImplTestCase { private static final String IMAGE_NAME_PNG = "12monkeys-splash.png"; private static final String IMAGE_NAME_GIF = "tux.gif"; + private static final String IMAGE_NAME_PNG_INDEXED = "star.png"; private static final Dimension IMAGE_DIMENSION_PNG = new Dimension(300, 410); private static final Dimension IMAGE_DIMENSION_GIF = new Dimension(250, 250); + private static final Dimension IMAGE_DIMENSION_PNG_INDEXED = new Dimension(199, 192); + + private static final int STREAM_DEFAULT_SIZE = 2000; private HttpServletRequest request; private ServletContext context; @@ -58,12 +65,19 @@ public class ImageServletResponseImplTestCase { context = mock(ServletContext.class); when(context.getResource("/" + IMAGE_NAME_PNG)).thenReturn(getClass().getResource(IMAGE_NAME_PNG)); when(context.getResource("/" + IMAGE_NAME_GIF)).thenReturn(getClass().getResource(IMAGE_NAME_GIF)); + when(context.getResource("/" + IMAGE_NAME_PNG_INDEXED)).thenReturn(getClass().getResource(IMAGE_NAME_PNG_INDEXED)); when(context.getMimeType("file.bmp")).thenReturn(CONTENT_TYPE_BMP); when(context.getMimeType("file.foo")).thenReturn(CONTENT_TYPE_FOO); when(context.getMimeType("file.gif")).thenReturn(CONTENT_TYPE_GIF); when(context.getMimeType("file.jpeg")).thenReturn(CONTENT_TYPE_JPEG); when(context.getMimeType("file.png")).thenReturn(CONTENT_TYPE_PNG); when(context.getMimeType("file.txt")).thenReturn(CONTENT_TYPE_TEXT); + + MockLogger mockLogger = new MockLogger(); + doAnswer(mockLogger).when(context).log(anyString()); + doAnswer(mockLogger).when(context).log(anyString(), any(Throwable.class)); + //noinspection deprecation + doAnswer(mockLogger).when(context).log(any(Exception.class), anyString()); } private void fakeResponse(HttpServletRequest pRequest, ImageServletResponseImpl pImageResponse) throws IOException { @@ -98,7 +112,7 @@ public class ImageServletResponseImplTestCase { @Test public void testBasicResponse() throws IOException { - ByteArrayOutputStream out = new ByteArrayOutputStream(); + FastByteArrayOutputStream out = new FastByteArrayOutputStream(STREAM_DEFAULT_SIZE); HttpServletResponse response = mock(HttpServletResponse.class); when(response.getOutputStream()).thenReturn(new OutputStreamAdapter(out)); @@ -118,7 +132,7 @@ public class ImageServletResponseImplTestCase { assertTrue("Content has no data", out.size() > 0); // Test that image data is still readable - BufferedImage outImage = ImageIO.read(new ByteArrayInputStream(out.toByteArray())); + BufferedImage outImage = ImageIO.read(out.createInputStream()); assertNotNull(outImage); assertEquals(image.getWidth(), outImage.getWidth()); assertEquals(image.getHeight(), outImage.getHeight()); @@ -133,7 +147,7 @@ public class ImageServletResponseImplTestCase { @Test public void testNoOpResponse() throws IOException { - ByteArrayOutputStream out = new ByteArrayOutputStream(); + FastByteArrayOutputStream out = new FastByteArrayOutputStream(STREAM_DEFAULT_SIZE); HttpServletResponse response = mock(HttpServletResponse.class); when(response.getOutputStream()).thenReturn(new OutputStreamAdapter(out)); @@ -157,7 +171,7 @@ public class ImageServletResponseImplTestCase { // Transcode original PNG to JPEG with no other changes @Test public void testTranscodeResponsePNGToJPEG() throws IOException { - ByteArrayOutputStream out = new ByteArrayOutputStream(); + FastByteArrayOutputStream out = new FastByteArrayOutputStream(STREAM_DEFAULT_SIZE); HttpServletResponse response = mock(HttpServletResponse.class); when(response.getOutputStream()).thenReturn(new OutputStreamAdapter(out)); @@ -173,6 +187,12 @@ public class ImageServletResponseImplTestCase { assertTrue("Content has no data", out.size() > 0); + // Assert JPEG + ByteArrayInputStream input = out.createInputStream(); + assertEquals(0xFF, input.read()); + assertEquals(0xD8, input.read()); + assertEquals(0xFF, input.read()); + // Test that image data is still readable /* File tempFile = File.createTempFile("imageservlet-test-", ".jpeg"); @@ -182,7 +202,7 @@ public class ImageServletResponseImplTestCase { System.err.println("open " + tempFile); */ - BufferedImage outImage = ImageIO.read(new ByteArrayInputStream(out.toByteArray())); + BufferedImage outImage = ImageIO.read(out.createInputStream()); assertNotNull(outImage); assertEquals(IMAGE_DIMENSION_PNG.width, outImage.getWidth()); assertEquals(IMAGE_DIMENSION_PNG.height, outImage.getHeight()); @@ -208,43 +228,53 @@ public class ImageServletResponseImplTestCase { // (even if there's only one possible compression mode/type combo; MODE_EXPLICIT/"LZW") @Test public void testTranscodeResponsePNGToGIFWithQuality() throws IOException { - ByteArrayOutputStream out = new ByteArrayOutputStream(); + FastByteArrayOutputStream out = new FastByteArrayOutputStream(STREAM_DEFAULT_SIZE); - HttpServletResponse response = mock(HttpServletResponse.class); - when(response.getOutputStream()).thenReturn(new OutputStreamAdapter(out)); - when(request.getAttribute(ImageServletResponse.ATTRIB_OUTPUT_QUALITY)).thenReturn(.5f); // Force quality setting in param + HttpServletResponse response = mock(HttpServletResponse.class); + when(response.getOutputStream()).thenReturn(new OutputStreamAdapter(out)); + when(request.getAttribute(ImageServletResponse.ATTRIB_OUTPUT_QUALITY)).thenReturn(.5f); // Force quality setting in param - ImageServletResponseImpl imageResponse = new ImageServletResponseImpl(request, response, context); - fakeResponse(request, imageResponse); + ImageServletResponseImpl imageResponse = new ImageServletResponseImpl(request, response, context); + fakeResponse(request, imageResponse); - // Force transcode to GIF - imageResponse.setOutputContentType("image/gif"); + // Force transcode to GIF + imageResponse.setOutputContentType("image/gif"); - // Flush image to wrapped response - imageResponse.flush(); + // Flush image to wrapped response + imageResponse.flush(); - assertTrue("Content has no data", out.size() > 0); + assertTrue("Content has no data", out.size() > 0); - // Test that image data is still readable - BufferedImage outImage = ImageIO.read(new ByteArrayInputStream(out.toByteArray())); - assertNotNull(outImage); - assertEquals(IMAGE_DIMENSION_PNG.width, outImage.getWidth()); - assertEquals(IMAGE_DIMENSION_PNG.height, outImage.getHeight()); + // Assert GIF + ByteArrayInputStream stream = out.createInputStream(); + assertEquals('G', stream.read()); + assertEquals('I', stream.read()); + assertEquals('F', stream.read()); + assertEquals('8', stream.read()); + assertEquals('9', stream.read()); + assertEquals('a', stream.read()); - BufferedImage image = ImageIO.read(context.getResource("/" + IMAGE_NAME_PNG)); + // Test that image data is still readable + BufferedImage outImage = ImageIO.read(out.createInputStream()); + assertNotNull(outImage); + assertEquals(IMAGE_DIMENSION_PNG.width, outImage.getWidth()); + assertEquals(IMAGE_DIMENSION_PNG.height, outImage.getHeight()); - // Should keep transparency, but is now binary - assertSimilarImageTransparent(image, outImage, 120f); + BufferedImage image = ImageIO.read(context.getResource("/" + IMAGE_NAME_PNG)); - verify(response).setContentType(CONTENT_TYPE_GIF); - verify(response).getOutputStream(); - } + // Should keep transparency, but is now binary +// showIt(image, outImage, null); + assertSimilarImageTransparent(image, outImage, 50f); + + verify(response).setContentType(CONTENT_TYPE_GIF); + verify(response).getOutputStream(); + } // WORKAROUND: Bug in GIFImageWriter may throw NPE if transparent pixels // See: http://bugs.sun.com/view_bug.do?bug_id=6287936 @Test public void testTranscodeResponsePNGToGIF() throws IOException { - ByteArrayOutputStream out = new ByteArrayOutputStream(); + FastByteArrayOutputStream out = new FastByteArrayOutputStream(STREAM_DEFAULT_SIZE); HttpServletResponse response = mock(HttpServletResponse.class); when(response.getOutputStream()).thenReturn(new OutputStreamAdapter(out)); @@ -260,7 +290,7 @@ public class ImageServletResponseImplTestCase { assertTrue("Content has no data", out.size() > 0); // Test that image data is still readable - BufferedImage outImage = ImageIO.read(new ByteArrayInputStream(out.toByteArray())); + BufferedImage outImage = ImageIO.read(out.createInputStream()); assertNotNull(outImage); assertEquals(IMAGE_DIMENSION_PNG.width, outImage.getWidth()); assertEquals(IMAGE_DIMENSION_PNG.height, outImage.getHeight()); @@ -268,7 +298,8 @@ public class ImageServletResponseImplTestCase { BufferedImage image = ImageIO.read(context.getResource("/" + IMAGE_NAME_PNG)); // Should keep transparency, but is now binary - assertSimilarImageTransparent(image, outImage, 120f); +// showIt(image, outImage, null); + assertSimilarImageTransparent(image, outImage, 50f); verify(response).setContentType(CONTENT_TYPE_GIF); verify(response).getOutputStream(); @@ -297,13 +328,13 @@ public class ImageServletResponseImplTestCase { } @Test - public void testTranscodeResponseIndexedCM() throws IOException { + public void testTranscodeResponseIndexColorModelGIFToJPEG() throws IOException { // Custom setup HttpServletRequest request = mock(HttpServletRequest.class); when(request.getContextPath()).thenReturn("/ape"); when(request.getRequestURI()).thenReturn("/ape/" + IMAGE_NAME_GIF); - ByteArrayOutputStream out = new ByteArrayOutputStream(); + FastByteArrayOutputStream out = new FastByteArrayOutputStream(STREAM_DEFAULT_SIZE); HttpServletResponse response = mock(HttpServletResponse.class); when(response.getOutputStream()).thenReturn(new OutputStreamAdapter(out)); @@ -318,8 +349,14 @@ public class ImageServletResponseImplTestCase { assertTrue("Content has no data", out.size() > 0); + // Assert JPEG + ByteArrayInputStream stream = out.createInputStream(); + assertEquals(0xFF, stream.read()); + assertEquals(0xD8, stream.read()); + assertEquals(0xFF, stream.read()); + // Test that image data is still readable - BufferedImage outImage = ImageIO.read(new ByteArrayInputStream(out.toByteArray())); + BufferedImage outImage = ImageIO.read(out.createInputStream()); assertNotNull(outImage); assertEquals(IMAGE_DIMENSION_GIF.width, outImage.getWidth()); assertEquals(IMAGE_DIMENSION_GIF.height, outImage.getHeight()); @@ -329,6 +366,59 @@ public class ImageServletResponseImplTestCase { assertSimilarImage(image, outImage, 96f); } + @Test + // TODO: Insert bug id/reference here for regression tracking + public void testIndexedColorModelResizePNG() throws IOException { + // Results differ with algorithm, so we test each algorithm by itself + int[] algorithms = new int[] {Image.SCALE_DEFAULT, Image.SCALE_FAST, Image.SCALE_SMOOTH, Image.SCALE_REPLICATE, Image.SCALE_AREA_AVERAGING, 77}; + + for (int algorithm : algorithms) { + Dimension size = new Dimension(100, 100); + + // Custom setup + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getAttribute(ImageServletResponse.ATTRIB_SIZE)).thenReturn(size); + when(request.getAttribute(ImageServletResponse.ATTRIB_SIZE_UNIFORM)).thenReturn(false); + when(request.getAttribute(ImageServletResponse.ATTRIB_IMAGE_RESAMPLE_ALGORITHM)).thenReturn(algorithm); + when(request.getContextPath()).thenReturn("/ape"); + when(request.getRequestURI()).thenReturn("/ape/" + IMAGE_NAME_PNG_INDEXED); + + FastByteArrayOutputStream out = new FastByteArrayOutputStream(STREAM_DEFAULT_SIZE); + HttpServletResponse response = mock(HttpServletResponse.class); + when(response.getOutputStream()).thenReturn(new OutputStreamAdapter(out)); + + ImageServletResponseImpl imageResponse = new ImageServletResponseImpl(request, response, context); + fakeResponse(request, imageResponse); + + imageResponse.getImage(); + + // Flush image to wrapped response + imageResponse.flush(); + + assertTrue("Content has no data", out.size() > 0); + + // Assert format is still PNG + ByteArrayInputStream inputStream = out.createInputStream(); + assertEquals(0x89, inputStream.read()); + assertEquals('P', inputStream.read()); + assertEquals('N', inputStream.read()); + assertEquals('G', inputStream.read()); + + // Test that image data is still readable + BufferedImage outImage = ImageIO.read(out.createInputStream()); + assertNotNull(outImage); + assertEquals(size.width, outImage.getWidth()); + assertEquals(size.height, outImage.getHeight()); + + BufferedImage read = ImageIO.read(context.getResource("/" + IMAGE_NAME_PNG_INDEXED)); + BufferedImage image = ImageUtil.createResampled(read, size.width, size.height, imageResponse.getResampleAlgorithmFromRequest()); + +// showIt(image, outImage, null); + + assertSimilarImageTransparent(image, outImage, 10f); + } + } + private static BufferedImage flatten(final BufferedImage pImage, final Color pBackgroundColor) { BufferedImage image = ImageUtil.toBuffered(pImage, BufferedImage.TYPE_INT_ARGB); @@ -369,32 +459,64 @@ public class ImageServletResponseImplTestCase { } private void assertSimilarImageTransparent(final BufferedImage pExpected, final BufferedImage pActual, final float pArtifactThreshold) { + IndexColorModel icm = pActual.getColorModel() instanceof IndexColorModel ? (IndexColorModel) pActual.getColorModel() : null; + Object pixel = null; + for (int y = 0; y < pExpected.getHeight(); y++) { for (int x = 0; x < pExpected.getWidth(); x++) { int expected = pExpected.getRGB(x, y); int actual = pActual.getRGB(x, y); - int alpha = (expected >> 24) & 0xff; + if (icm != null) { + // Look up, using ICM - boolean transparent = alpha < 0x40; + int alpha = (expected >> 24) & 0xff; + boolean transparent = alpha < 0x40; - // Multiply out alpha for each component - int expectedR = (int) ((((expected >> 16) & 0xff) * alpha) / 255f); - int expectedG = (int) ((((expected >> 8 ) & 0xff) * alpha) / 255f); - int expectedB = (int) ((( expected & 0xff) * alpha) / 255f); + if (transparent) { + int expectedLookedUp = icm.getRGB(icm.getTransparentPixel()); + assertRGBEquals(x, y, expectedLookedUp & 0xff000000, actual & 0xff000000, 0); + } + else { + pixel = icm.getDataElements(expected, pixel); + int expectedLookedUp = icm.getRGB(pixel); + assertRGBEquals(x, y, expectedLookedUp & 0xffffff, actual & 0xffffff, pArtifactThreshold); + } + } + else { + // Multiply out alpha for each component if pre-multiplied +// int expectedR = (int) ((((expected >> 16) & 0xff) * alpha) / 255f); +// int expectedG = (int) ((((expected >> 8) & 0xff) * alpha) / 255f); +// int expectedB = (int) (((expected & 0xff) * alpha) / 255f); - - assertEquals("a(" + x + "," + y + ")", transparent ? 0 : 0xff, (actual >> 24) & 0xff); - assertEquals("R(" + x + "," + y + ")", expectedR, (actual >> 16) & 0xff, pArtifactThreshold); - assertEquals("G(" + x + "," + y + ")", expectedG, (actual >> 8) & 0xff, pArtifactThreshold); - assertEquals("B(" + x + "," + y + ")", expectedB, actual & 0xff, pArtifactThreshold); + assertRGBEquals(x, y, expected, actual, pArtifactThreshold); + } } } } + private void assertRGBEquals(int x, int y, int expected, int actual, float pArtifactThreshold) { + int expectedA = (expected >> 24) & 0xff; + int expectedR = (expected >> 16) & 0xff; + int expectedG = (expected >> 8) & 0xff; + int expectedB = expected & 0xff; + + try { + assertEquals("Alpha", expectedA, (actual >> 24) & 0xff, pArtifactThreshold); + assertEquals("RGB", 0, (Math.abs(expectedR - ((actual >> 16) & 0xff)) + + Math.abs(expectedG - ((actual >> 8) & 0xff)) + + Math.abs(expectedB - ((actual) & 0xff))) / 3.0, pArtifactThreshold); + } + catch (AssertionError e) { + AssertionError assertionError = new AssertionError(String.format("@[%d,%d] expected: 0x%08x but was: 0x%08x", x, y, expected, actual)); + assertionError.initCause(e); + throw assertionError; + } + } + @Test public void testReplaceResponse() throws IOException { - ByteArrayOutputStream out = new ByteArrayOutputStream(); + FastByteArrayOutputStream out = new FastByteArrayOutputStream(STREAM_DEFAULT_SIZE); HttpServletResponse response = mock(HttpServletResponse.class); when(response.getOutputStream()).thenReturn(new OutputStreamAdapter(out)); @@ -418,7 +540,7 @@ public class ImageServletResponseImplTestCase { assertTrue("Content has no data", out.size() > 0); // Test that image data is still readable - BufferedImage outImage = ImageIO.read(new ByteArrayInputStream(out.toByteArray())); + BufferedImage outImage = ImageIO.read(out.createInputStream()); assertNotNull(outImage); assertEquals(image.getWidth(), outImage.getWidth()); assertEquals(image.getHeight(), outImage.getHeight()); @@ -527,7 +649,7 @@ public class ImageServletResponseImplTestCase { when(request.getContextPath()).thenReturn("/ape"); when(request.getRequestURI()).thenReturn("/ape/" + IMAGE_NAME_PNG); - ByteArrayOutputStream out = new ByteArrayOutputStream(); + FastByteArrayOutputStream out = new FastByteArrayOutputStream(STREAM_DEFAULT_SIZE); HttpServletResponse response = mock(HttpServletResponse.class); when(response.getOutputStream()).thenReturn(new OutputStreamAdapter(out)); @@ -546,7 +668,7 @@ public class ImageServletResponseImplTestCase { assertTrue("Content has no data", out.size() > 0); // Test that image data is still readable - BufferedImage outImage = ImageIO.read(new ByteArrayInputStream(out.toByteArray())); + BufferedImage outImage = ImageIO.read(out.createInputStream()); assertNotNull(outImage); assertEquals(image.getWidth(), outImage.getWidth()); assertEquals(image.getHeight(), outImage.getHeight()); @@ -564,7 +686,7 @@ public class ImageServletResponseImplTestCase { when(request.getContextPath()).thenReturn("/ape"); when(request.getRequestURI()).thenReturn("/ape/" + IMAGE_NAME_PNG); - ByteArrayOutputStream out = new ByteArrayOutputStream(); + FastByteArrayOutputStream out = new FastByteArrayOutputStream(STREAM_DEFAULT_SIZE); HttpServletResponse response = mock(HttpServletResponse.class); when(response.getOutputStream()).thenReturn(new OutputStreamAdapter(out)); @@ -584,7 +706,7 @@ public class ImageServletResponseImplTestCase { assertTrue("Content has no data", out.size() > 0); // Test that image data is still readable - BufferedImage outImage = ImageIO.read(new ByteArrayInputStream(out.toByteArray())); + BufferedImage outImage = ImageIO.read(out.createInputStream()); assertNotNull(outImage); assertEquals(image.getWidth(), outImage.getWidth()); assertEquals(image.getHeight(), outImage.getHeight()); @@ -605,7 +727,7 @@ public class ImageServletResponseImplTestCase { when(request.getContextPath()).thenReturn("/ape"); when(request.getRequestURI()).thenReturn("/ape/" + IMAGE_NAME_PNG); - ByteArrayOutputStream out = new ByteArrayOutputStream(); + FastByteArrayOutputStream out = new FastByteArrayOutputStream(STREAM_DEFAULT_SIZE); HttpServletResponse response = mock(HttpServletResponse.class); when(response.getOutputStream()).thenReturn(new OutputStreamAdapter(out)); @@ -645,7 +767,7 @@ public class ImageServletResponseImplTestCase { assertTrue("Content has no data", out.size() > 0); // Test that image data is still readable - BufferedImage outImage = ImageIO.read(new ByteArrayInputStream(out.toByteArray())); + BufferedImage outImage = ImageIO.read(out.createInputStream()); assertNotNull(outImage); assertEquals(image.getWidth(), outImage.getWidth()); assertEquals(image.getHeight(), outImage.getHeight()); @@ -666,7 +788,7 @@ public class ImageServletResponseImplTestCase { when(request.getContextPath()).thenReturn("/ape"); when(request.getRequestURI()).thenReturn("/ape/" + IMAGE_NAME_PNG); - ByteArrayOutputStream out = new ByteArrayOutputStream(); + FastByteArrayOutputStream out = new FastByteArrayOutputStream(STREAM_DEFAULT_SIZE); HttpServletResponse response = mock(HttpServletResponse.class); when(response.getOutputStream()).thenReturn(new OutputStreamAdapter(out)); @@ -729,7 +851,7 @@ public class ImageServletResponseImplTestCase { assertTrue("Content has no data", out.size() > 0); // Test that image data is still readable - BufferedImage outImage = ImageIO.read(new ByteArrayInputStream(out.toByteArray())); + BufferedImage outImage = ImageIO.read(out.createInputStream()); assertNotNull(outImage); assertEquals(image.getWidth(), outImage.getWidth()); assertEquals(image.getHeight(), outImage.getHeight()); @@ -748,7 +870,7 @@ public class ImageServletResponseImplTestCase { when(request.getContextPath()).thenReturn("/ape"); when(request.getRequestURI()).thenReturn("/ape/" + IMAGE_NAME_PNG); - ByteArrayOutputStream out = new ByteArrayOutputStream(); + FastByteArrayOutputStream out = new FastByteArrayOutputStream(STREAM_DEFAULT_SIZE); HttpServletResponse response = mock(HttpServletResponse.class); when(response.getOutputStream()).thenReturn(new OutputStreamAdapter(out)); @@ -777,7 +899,7 @@ public class ImageServletResponseImplTestCase { assertTrue("Content has no data", out.size() > 0); // Test that image data is still readable - BufferedImage outImage = ImageIO.read(new ByteArrayInputStream(out.toByteArray())); + BufferedImage outImage = ImageIO.read(out.createInputStream()); assertNotNull(outImage); assertEquals(image.getWidth(), outImage.getWidth()); assertEquals(image.getHeight(), outImage.getHeight()); @@ -796,7 +918,7 @@ public class ImageServletResponseImplTestCase { when(request.getContextPath()).thenReturn("/ape"); when(request.getRequestURI()).thenReturn("/ape/" + IMAGE_NAME_PNG); - ByteArrayOutputStream out = new ByteArrayOutputStream(); + FastByteArrayOutputStream out = new FastByteArrayOutputStream(STREAM_DEFAULT_SIZE); HttpServletResponse response = mock(HttpServletResponse.class); when(response.getOutputStream()).thenReturn(new OutputStreamAdapter(out)); @@ -815,7 +937,7 @@ public class ImageServletResponseImplTestCase { assertTrue("Content has no data", out.size() > 0); // Test that image data is still readable - BufferedImage outImage = ImageIO.read(new ByteArrayInputStream(out.toByteArray())); + BufferedImage outImage = ImageIO.read(out.createInputStream()); assertNotNull(outImage); assertEquals(image.getWidth(), outImage.getWidth()); assertEquals(image.getHeight(), outImage.getHeight()); @@ -836,7 +958,7 @@ public class ImageServletResponseImplTestCase { when(request.getContextPath()).thenReturn("/ape"); when(request.getRequestURI()).thenReturn("/ape/" + IMAGE_NAME_PNG); - ByteArrayOutputStream out = new ByteArrayOutputStream(); + FastByteArrayOutputStream out = new FastByteArrayOutputStream(STREAM_DEFAULT_SIZE); HttpServletResponse response = mock(HttpServletResponse.class); when(response.getOutputStream()).thenReturn(new OutputStreamAdapter(out)); @@ -865,7 +987,7 @@ public class ImageServletResponseImplTestCase { assertTrue("Content has no data", out.size() > 0); // Test that image data is still readable - BufferedImage outImage = ImageIO.read(new ByteArrayInputStream(out.toByteArray())); + BufferedImage outImage = ImageIO.read(out.createInputStream()); assertNotNull(outImage); assertEquals(image.getWidth(), outImage.getWidth()); assertEquals(image.getHeight(), outImage.getHeight()); @@ -886,7 +1008,7 @@ public class ImageServletResponseImplTestCase { when(request.getContextPath()).thenReturn("/ape"); when(request.getRequestURI()).thenReturn("/ape/" + IMAGE_NAME_PNG); - ByteArrayOutputStream out = new ByteArrayOutputStream(); + FastByteArrayOutputStream out = new FastByteArrayOutputStream(STREAM_DEFAULT_SIZE); HttpServletResponse response = mock(HttpServletResponse.class); when(response.getOutputStream()).thenReturn(new OutputStreamAdapter(out)); @@ -905,7 +1027,7 @@ public class ImageServletResponseImplTestCase { assertTrue("Content has no data", out.size() > 0); // Test that image data is still readable - BufferedImage outImage = ImageIO.read(new ByteArrayInputStream(out.toByteArray())); + BufferedImage outImage = ImageIO.read(out.createInputStream()); assertNotNull(outImage); assertEquals(image.getWidth(), outImage.getWidth()); assertEquals(image.getHeight(), outImage.getHeight()); @@ -927,7 +1049,7 @@ public class ImageServletResponseImplTestCase { when(request.getContextPath()).thenReturn("/ape"); when(request.getRequestURI()).thenReturn("/ape/" + IMAGE_NAME_PNG); - ByteArrayOutputStream out = new ByteArrayOutputStream(); + FastByteArrayOutputStream out = new FastByteArrayOutputStream(STREAM_DEFAULT_SIZE); HttpServletResponse response = mock(HttpServletResponse.class); when(response.getOutputStream()).thenReturn(new OutputStreamAdapter(out)); @@ -964,7 +1086,7 @@ public class ImageServletResponseImplTestCase { assertTrue("Content has no data", out.size() > 0); // Test that image data is still readable - BufferedImage outImage = ImageIO.read(new ByteArrayInputStream(out.toByteArray())); + BufferedImage outImage = ImageIO.read(out.createInputStream()); assertNotNull(outImage); assertEquals(image.getWidth(), outImage.getWidth()); assertEquals(image.getHeight(), outImage.getHeight()); @@ -986,7 +1108,7 @@ public class ImageServletResponseImplTestCase { when(request.getContextPath()).thenReturn("/ape"); when(request.getRequestURI()).thenReturn("/ape/" + IMAGE_NAME_PNG); - ByteArrayOutputStream out = new ByteArrayOutputStream(); + FastByteArrayOutputStream out = new FastByteArrayOutputStream(STREAM_DEFAULT_SIZE); HttpServletResponse response = mock(HttpServletResponse.class); when(response.getOutputStream()).thenReturn(new OutputStreamAdapter(out)); @@ -1027,7 +1149,7 @@ public class ImageServletResponseImplTestCase { assertTrue("Content has no data", out.size() > 0); // Test that image data is still readable - BufferedImage outImage = ImageIO.read(new ByteArrayInputStream(out.toByteArray())); + BufferedImage outImage = ImageIO.read(out.createInputStream()); assertNotNull(outImage); assertEquals(image.getWidth(), outImage.getWidth()); assertEquals(image.getHeight(), outImage.getHeight()); @@ -1049,7 +1171,7 @@ public class ImageServletResponseImplTestCase { when(request.getContextPath()).thenReturn("/ape"); when(request.getRequestURI()).thenReturn("/ape/" + IMAGE_NAME_PNG); - ByteArrayOutputStream out = new ByteArrayOutputStream(); + FastByteArrayOutputStream out = new FastByteArrayOutputStream(STREAM_DEFAULT_SIZE); HttpServletResponse response = mock(HttpServletResponse.class); when(response.getOutputStream()).thenReturn(new OutputStreamAdapter(out)); @@ -1070,7 +1192,7 @@ public class ImageServletResponseImplTestCase { assertTrue("Content has no data", out.size() > 0); // Test that image data is still readable - BufferedImage outImage = ImageIO.read(new ByteArrayInputStream(out.toByteArray())); + BufferedImage outImage = ImageIO.read(out.createInputStream()); assertNotNull(outImage); assertEquals(image.getWidth(), outImage.getWidth()); assertEquals(image.getHeight(), outImage.getHeight()); @@ -1401,6 +1523,9 @@ public class ImageServletResponseImplTestCase { // TODO: Test getSize()... private static class BlackLabel extends JLabel { + private final Paint checkeredBG; + private boolean opaque = true; + public BlackLabel(final String text, final BufferedImage outImage) { super(text, new BufferedImageIcon(outImage), JLabel.CENTER); setOpaque(true); @@ -1409,6 +1534,71 @@ public class ImageServletResponseImplTestCase { setVerticalAlignment(JLabel.CENTER); setVerticalTextPosition(JLabel.BOTTOM); setHorizontalTextPosition(JLabel.CENTER); + + checkeredBG = createTexture(); + } + + @Override + public boolean isOpaque() { + return opaque && super.isOpaque(); + } + + @Override + protected void paintComponent(Graphics graphics) { + Graphics2D g = (Graphics2D) graphics; + + int iconHeight = getIcon() == null ? 0 : getIcon().getIconHeight() + getIconTextGap(); + + // Paint checkered bg behind icon + g.setPaint(checkeredBG); + g.fillRect(0, 0, getWidth(), getHeight()); + + // Paint black bg behind text + g.setColor(getBackground()); + g.fillRect(0, iconHeight, getWidth(), getHeight() - iconHeight); + + try { + opaque = false; + super.paintComponent(g); + } + finally { + opaque = true; + } + } + + private static Paint createTexture() { + GraphicsConfiguration graphicsConfiguration = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration(); + BufferedImage pattern = graphicsConfiguration.createCompatibleImage(20, 20); + Graphics2D g = pattern.createGraphics(); + try { + g.setColor(Color.LIGHT_GRAY); + g.fillRect(0, 0, pattern.getWidth(), pattern.getHeight()); + g.setColor(Color.GRAY); + g.fillRect(0, 0, pattern.getWidth() / 2, pattern.getHeight() / 2); + g.fillRect(pattern.getWidth() / 2, pattern.getHeight() / 2, pattern.getWidth() / 2, pattern.getHeight() / 2); + } + finally { + g.dispose(); + } + + return new TexturePaint(pattern, new Rectangle(pattern.getWidth(), pattern.getHeight())); + } + } + + private static class MockLogger implements Answer { + public Void answer(InvocationOnMock invocation) throws Throwable { + // either log(String), log(String, Throwable) or log(Exception, String) + Object[] arguments = invocation.getArguments(); + + String msg = (String) (arguments[0] instanceof String ? arguments[0] : arguments[1]); + Throwable t = (Throwable) (arguments[0] instanceof Exception ? arguments[0] : arguments.length > 1 ? arguments[1] : null); + + System.out.println("mock-context: " + msg); + if (t != null) { + t.printStackTrace(System.out); + } + + return null; } } } diff --git a/servlet/src/test/resources/com/twelvemonkeys/servlet/image/star.png b/servlet/src/test/resources/com/twelvemonkeys/servlet/image/star.png new file mode 100644 index 00000000..8b0730e9 Binary files /dev/null and b/servlet/src/test/resources/com/twelvemonkeys/servlet/image/star.png differ