diff --git a/imageio/imageio-jpeg/pom.xml b/imageio/imageio-jpeg/pom.xml
new file mode 100644
index 00000000..7e6a16e3
--- /dev/null
+++ b/imageio/imageio-jpeg/pom.xml
@@ -0,0 +1,32 @@
+
+
+ 4.0.0
+
+ com.twelvemonkeys.imageio
+ imageio
+ 3.0-SNAPSHOT
+
+ imageio-jpeg
+ TwelveMonkeys :: ImageIO :: JPEG plugin
+
+ ImageIO plugin for Joint Photographer Expert Group images (JPEG/JFIF).
+
+
+
+
+ com.twelvemonkeys.imageio
+ imageio-core
+
+
+ com.twelvemonkeys.imageio
+ imageio-core
+ tests
+
+
+ com.twelvemonkeys.imageio
+ imageio-metadata
+
+
+
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
new file mode 100644
index 00000000..1b24e819
--- /dev/null
+++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java
@@ -0,0 +1,1046 @@
+/*
+ * Copyright (c) 2011, Harald Kuhr
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * Neither the name "TwelveMonkeys" nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.twelvemonkeys.imageio.plugins.jpeg;
+
+import com.twelvemonkeys.image.ImageUtil;
+import com.twelvemonkeys.imageio.ImageReaderBase;
+import com.twelvemonkeys.imageio.color.ColorSpaces;
+import com.twelvemonkeys.imageio.metadata.jpeg.JPEG;
+import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegmentUtil;
+import com.twelvemonkeys.imageio.util.ProgressListenerBase;
+import com.twelvemonkeys.lang.Validate;
+
+import javax.imageio.*;
+import javax.imageio.event.IIOReadUpdateListener;
+import javax.imageio.event.IIOReadWarningListener;
+import javax.imageio.spi.ImageReaderSpi;
+import javax.imageio.stream.ImageInputStream;
+import java.awt.*;
+import java.awt.color.ColorSpace;
+import java.awt.color.ICC_ColorSpace;
+import java.awt.color.ICC_Profile;
+import java.awt.image.*;
+import java.io.*;
+import java.lang.reflect.Field;
+import java.util.*;
+import java.util.List;
+
+/**
+ * JPEGImageReader
+ *
+ * @author Harald Kuhr
+ * @author LUT-based YCbCR conversion by Werner Randelshofer
+ * @author last modified by $Author: haraldk$
+ * @version $Id: JPEGImageReader.java,v 1.0 24.01.11 16.37 haraldk Exp$
+ */
+public class JPEGImageReader extends ImageReaderBase {
+
+ private final static boolean DEBUG = "true".equalsIgnoreCase(System.getProperty("com.twelvemonkeys.imageio.plugins.jpeg.debug"));
+
+ /** Segment identifiers for the JPEG app segments we care about reading. */
+ private static final Map> SEGMENT_IDENTIFIERS = createSegmentIds();
+
+ private static Map> createSegmentIds() {
+ Map> map = new HashMap>();
+
+ // JFIF APP0 markers
+ map.put(JPEG.APP0, Arrays.asList("JFIF", "JFXX"));
+
+ // ICC Color Profile
+ map.put(JPEG.APP2, Collections.singletonList("ICC_PROFILE"));
+
+ // Adobe APP14 marker
+ map.put(JPEG.APP14, Collections.singletonList("Adobe"));
+
+ // SOFn markers
+ map.put(JPEG.SOF0, null);
+ map.put(JPEG.SOF1, null);
+ map.put(JPEG.SOF2, null);
+ map.put(JPEG.SOF3, null);
+ map.put(JPEG.SOF5, null);
+ map.put(JPEG.SOF6, null);
+ map.put(JPEG.SOF7, null);
+ map.put(JPEG.SOF9, null);
+ map.put(JPEG.SOF10, null);
+ map.put(JPEG.SOF11, null);
+ map.put(JPEG.SOF13, null);
+ map.put(JPEG.SOF14, null);
+ map.put(JPEG.SOF15, null);
+
+ return Collections.unmodifiableMap(map);
+ }
+
+ /** Our JPEG reading delegate */
+ private final ImageReader delegate;
+
+ private final ICCSpaceInterceptor iccSpaceInterceptor;
+ private final ProgressDelegator progressDelegator;
+
+ /** Cached JFIF app segments */
+ private List segments;
+
+ private static Field getFieldSafely(final Class> cl, final String fieldName) {
+ try {
+ Field field = cl.getDeclaredField(fieldName);
+ field.setAccessible(true);
+ return field;
+ }
+ catch (NoSuchFieldException ignore) {
+ }
+ catch (SecurityException ignore) {
+ }
+
+ return null;
+ }
+
+ JPEGImageReader(final ImageReaderSpi provider, final ImageReader delegate) {
+ super(provider);
+ this.delegate = Validate.notNull(delegate);
+
+ Field iccCS = getFieldSafely(delegate.getClass(), "iccCS");
+ iccSpaceInterceptor = iccCS != null ? new ICCSpaceInterceptor(iccCS) : null;
+
+ progressDelegator = new ProgressDelegator();
+ }
+
+ public static void main(String[] args) throws IOException {
+ File file = new File(args[0]);
+
+ ImageInputStream input = ImageIO.createImageInputStream(file);
+ Iterator readers = ImageIO.getImageReaders(input);
+ if (!readers.hasNext()) {
+ System.err.println("No reader for: " + file);
+ System.exit(1);
+ }
+ ImageReader myReader = readers.next();
+ System.err.println("Reading using: " + myReader);
+ myReader.addIIOReadWarningListener(new IIOReadWarningListener() {
+ public void warningOccurred(ImageReader source, String warning) {
+ System.err.println("warning: " + warning);
+ }
+ });
+ myReader.addIIOReadProgressListener(new ProgressListenerBase() {
+ private static final int MAX_W = 78;
+ int lastProgress = 0;
+
+ @Override
+ public void imageStarted(ImageReader source, int imageIndex) {
+ System.out.print("[");
+ }
+
+ @Override
+ public void imageProgress(ImageReader source, float percentageDone) {
+ int steps = ((int) (percentageDone * MAX_W) / 100);
+// System.err.println("percentageDone: " + percentageDone);
+ for (int i = lastProgress; i < steps; i++) {
+ System.out.print(".");
+ }
+ System.out.flush();
+ lastProgress = steps;
+ }
+
+ @Override
+ public void imageComplete(ImageReader source) {
+ for (int i = lastProgress; i < MAX_W; i++) {
+ System.out.print(".");
+ }
+ System.out.println("]");
+ }
+ });
+
+ myReader.setInput(input);
+
+ try {
+ ImageReadParam param = myReader.getDefaultReadParam();
+ if (args.length > 1) {
+ int sub = Integer.parseInt(args[1]);
+ param.setSourceSubsampling(sub, sub, 1, 1);
+ }
+
+ long start = System.currentTimeMillis();
+ BufferedImage image = myReader.read(0, param);
+ System.err.println("Time: " + (System.currentTimeMillis() - start) + " ms");
+ System.err.println("image: " + image);
+
+// image = new ResampleOp(myReader.getWidth(0) / 4, myReader.getHeight(0) / 4, ResampleOp.FILTER_BLACKMAN_SINC).filter(image, null);
+
+ if (image.getWidth() > 1600 || image.getHeight() > 1000) {
+ float aspect = myReader.getAspectRatio(0);
+ int height = Math.round(1600 / aspect);
+ if (height <= 1000) {
+ image = ImageUtil.createResampled(image, 1600, height, Image.SCALE_DEFAULT);
+ }
+ else {
+ image = ImageUtil.createResampled(image, Math.round(1000 * aspect), 1000, Image.SCALE_DEFAULT);
+ }
+ }
+
+ showIt(image, String.format("Image: %s [%d x %d]", file.getName(), myReader.getWidth(0), myReader.getHeight(0)));
+ }
+ finally {
+ input.close();
+ }
+ }
+
+ private void installListeners() {
+ if (iccSpaceInterceptor != null) {
+ delegate.addIIOReadProgressListener(iccSpaceInterceptor);
+ }
+
+ delegate.addIIOReadProgressListener(progressDelegator);
+ delegate.addIIOReadUpdateListener(progressDelegator);
+ delegate.addIIOReadWarningListener(progressDelegator);
+ }
+
+ // TODO: Delegate all methods?!
+
+ @Override
+ protected void resetMembers() {
+ delegate.reset();
+ segments = null;
+
+ installListeners();
+ }
+
+ @Override
+ public void dispose() {
+ super.dispose();
+
+ delegate.dispose();
+ }
+
+ @Override
+ public String getFormatName() throws IOException {
+ return delegate.getFormatName();
+ }
+
+ @Override
+ public int getNumImages(boolean allowSearch) throws IOException {
+ return delegate.getNumImages(allowSearch);
+ }
+
+ @Override
+ public int getWidth(int imageIndex) throws IOException {
+ return delegate.getWidth(imageIndex);
+ }
+
+ @Override
+ public int getHeight(int imageIndex) throws IOException {
+ return delegate.getHeight(imageIndex);
+ }
+
+ @Override
+ public Iterator getImageTypes(int imageIndex) throws IOException {
+ // TODO: Read header, and make sure we return valid types for the images we can now read
+
+ Iterator types = delegate.getImageTypes(imageIndex);
+ if (iccSpaceInterceptor != null) {
+ iccSpaceInterceptor.replaceCS(delegate);
+ }
+ return types;
+ }
+
+ @Override
+ public ImageTypeSpecifier getRawImageType(int imageIndex) throws IOException {
+ // TODO: Implement something better, so we don't return null for CMYK images + fixes the "Inconsistent metadata" issue
+ return delegate.getRawImageType(imageIndex);
+ }
+
+ @Override
+ public void setInput(Object input, boolean seekForwardOnly, boolean ignoreMetadata) {
+ super.setInput(input, seekForwardOnly, ignoreMetadata);
+
+ delegate.setInput(input, seekForwardOnly, ignoreMetadata);
+ }
+
+ @Override
+ public boolean isRandomAccessEasy(int imageIndex) throws IOException {
+ return delegate.isRandomAccessEasy(imageIndex);
+ }
+
+ @Override
+ public BufferedImage read(int imageIndex, ImageReadParam param) throws IOException {
+ assertInput();
+ checkBounds(imageIndex);
+
+ // 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();
+
+ ICC_Profile profile = getEmbeddedICCProfile();
+ if (delegate.canReadRaster() && (unsupported || profile != null && ColorSpaces.isOffendingColorProfile(profile))) {
+ if (DEBUG) {
+ System.out.println("Reading using raster and extra conversion");
+ System.out.println("ICC color profile = " + profile);
+ }
+
+ return readImageAsRasterAndReplaceColorProfile(imageIndex, param, profile);
+ }
+
+ if (DEBUG) {
+ System.out.println("Reading using " + (iccSpaceInterceptor != null ? "intercepted " : "") + "delegate");
+ }
+ return delegate.read(imageIndex, param);
+ }
+
+ private BufferedImage readImageAsRasterAndReplaceColorProfile(int imageIndex, ImageReadParam param, ICC_Profile profile) throws IOException {
+
+ int origWidth = getWidth(imageIndex);
+ int origHeight = getHeight(imageIndex);
+
+ ColorSpace srcCs = null;
+ int xform = AdobeDCT.Unknown;
+
+ /*--------------------------------------------------------------------------------------------------------------
+
+ From http://download.oracle.com/javase/6/docs/api/javax/imageio/metadata/doc-files/jpeg_metadata.html:
+
+ "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 or YCbCr. If an APP2
+ marker segment containing an embedded ICC profile is also present, then the 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.
+
+ • 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.
+
+ • 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
+ images, the component ids are consulted. If these values are 1-3 for a 3-channel image, then the image is
+ assumed to be YCbCr. If these values are 1-4 for a 4-channel image, then the image is assumed to be YCbCrA. If
+ these values are > 4, they are checked against the ASCII codes for 'R', 'G', 'B', 'A', 'C', 'c'.
+ These can encode the following colorspaces:
+
+ RGB
+ RGBA
+ YCC (as 'Y','C','c'), assumed to be PhotoYCC
+ YCCA (as 'Y','C','c','A'), assumed to be PhotoYCCA
+
+ Otherwise, 3-channel subsampled images are assumed to be YCbCr, 3-channel non-subsampled images are assumed to
+ be RGB, 4-channel subsampled images are assumed to be YCCK, and 4-channel, non-subsampled images are assumed to
+ be CMYK.
+
+ • All other images are declared uninterpretable and an exception is thrown if an attempt is made to read one as
+ a BufferedImage. Such an image may be read only as a Raster. If an image is interpretable but there is no Java
+ ColorSpace available corresponding to the encoded colorspace (e.g. YCbCr), then ImageReader.getRawImageType
+ will return null."
+
+ --------------------------------------------------------------------------------------------------------------*/
+
+ // TODO: Fix this algorithm to behave like above, except for the presence of JFIF APP0 dictating YCbCr or gray,
+ // as it might just as well be CMYK...
+ // AdobeApp14 with transform either 1 or 2 can be trusted to be YCC/YCCK respectively, transform 0 means 1 component gray, 3 comp rgb, 4 comp cmyk
+ //
+
+ // 9788245605525.jpg: JFIF App0 + Adobe App14 transform 0, channel Id's C, M, Y, K
+ // lund-logo-cmyk.jpg: Adobe App14 transform 0 (+ flag?), channel Id's 1-4
+ // teastar_300dpi_cmyk.jpg: Adobe App14 transform 2 (+ flag), channel Id's 1-4
+
+
+// System.err.println("----> isAPP0Present(): " + isJFIFAPP0Present());
+ SOF startOfFrame = getSOF();
+
+// System.err.println("startOfFrame: " + startOfFrame);
+
+ Iterator imageTypes = delegate.getImageTypes(imageIndex);
+
+ // CMYK Support, assuming the delegate reader can't decode, and any 4 component image is CMYK
+ if (!imageTypes.hasNext() && startOfFrame.componentsInFrame == 4) {
+ // NOTE: Reading the metadata here chokes on some images. Instead, parse the Adobe App14 segment and read transform directly
+ AdobeDCT adobeDCT = getAdobeDCT();
+// System.err.println("adobeDCT: " + adobeDCT);
+ xform = adobeDCT != null ? adobeDCT.getTransform() : AdobeDCT.Unknown;
+
+ srcCs = ColorSpaces.getColorSpace(ColorSpaces.CS_GENERIC_CMYK);
+ imageTypes = Arrays.asList(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_3BYTE_BGR)).iterator();
+ }
+
+ BufferedImage image = getDestination(param, imageTypes, origWidth, origHeight);
+
+ WritableRaster destination = image.getRaster();
+
+ // TODO: checkReadParamBandSettings(param, );
+
+ ColorConvertOp convert = null;
+ ICC_ColorSpace replacement = profile != null ? ColorSpaces.createColorSpace(profile) : null;
+
+ if (profile.getColorSpaceType() == ColorSpace.TYPE_GRAY && image.getColorModel().getColorSpace().getType() == ColorSpace.CS_GRAY) {
+ // com.sun. reader does not do ColorConvertOp for CS_GRAY, even if embedded ICC profile,
+ // probably because IJG native part does it already...? If applied, color looks wrong (too dark)...
+ }
+ else if (replacement != null) {
+ // NOTE: CCO is not really necessary if replacement == image.getCM().getCS(),
+ // but in practice it's as fast/faster than raster.setRect() (see below)
+ convert = new ColorConvertOp(replacement, image.getColorModel().getColorSpace(), null);
+ }
+ else if (srcCs != null) {
+ convert = new ColorConvertOp(srcCs, image.getColorModel().getColorSpace(), null);
+ }
+// else if (!image.getColorModel().getColorSpace().isCS_sRGB()) {
+ // TODO: Need to handle case where src and dest differ still
+// convert = new ColorConvertOp(ColorSpace.getInstance(ColorSpace.CS_sRGB), image.getColorModel().getColorSpace(), null);
+// }
+ else if (profile != null) {
+ processWarningOccurred("Image contains an ICC color profile that is incompatible with Java 2D, color profile ignored.");
+ }
+
+ // We'll need a read param
+ if (param == null) {
+ param = delegate.getDefaultReadParam();
+ }
+
+ Rectangle srcRegion = new Rectangle();
+ Rectangle dstRegion = new Rectangle();
+
+ computeRegions(param, origWidth, origHeight, image, srcRegion, dstRegion);
+
+ // We're ready to go
+ processImageStarted(imageIndex);
+
+ // Unfortunately looping is slower than reading all at once, but
+ // that requires 2 x memory, so a few steps is an ok compromise I guess
+ try {
+ int csType = srcCs != null ? srcCs.getType() : image.getColorModel().getColorSpace().getType();
+ int step = Math.max(1024, srcRegion.height / 10);
+
+ for (int y = srcRegion.y; y < srcRegion.height; y += step) {
+ int scan = Math.min(step, srcRegion.height - y);
+ // Let the progress delegator handle progress, using corrected range
+ progressDelegator.updateProgressRange(100f * (y + scan) / srcRegion.height);
+// try {
+ param.setSourceRegion(new Rectangle(srcRegion.x, y, srcRegion.width, scan));
+ Raster raster = delegate.readRaster(imageIndex, param); // non-converted
+
+ // Apply source color conversion form implicit color space
+ if ((xform == AdobeDCT.YCC || xform == AdobeDCT.Unknown) && csType == ColorSpace.TYPE_RGB) {
+ YCbCrConverter.convertYCbCr2RGB(raster);
+ }
+ else if (xform == AdobeDCT.YCCK && csType == ColorSpace.TYPE_CMYK) {
+ YCbCrConverter.convertYCCK2CMYK(raster);
+ }
+ else if (xform == AdobeDCT.Unknown && csType == ColorSpace.TYPE_CMYK) {
+ invertCMYK(raster);
+ }
+
+ // TODO: Subsampling
+
+ Raster src = raster.createChild(0, 0, raster.getWidth(), raster.getHeight(), 0, 0, param.getSourceBands());
+ WritableRaster dest = destination.createWritableChild(dstRegion.x, dstRegion.y + y - srcRegion.y, dstRegion.width, raster.getHeight(), 0, 0, param.getDestinationBands());
+
+ // Apply further color conversion for explicit color space, or just copy the pixels into place
+ if (convert != null) {
+ convert.filter(src, dest);
+ }
+ else {
+ dest.setRect(0, 0, src);
+ }
+
+ if (abortRequested()) {
+ processReadAborted();
+ break;
+ }
+// }
+// catch (RasterFormatException e) {
+// System.err.println("y: " + y);
+// System.err.println("step: " + step);
+// System.err.println("scan: " + scan);
+// System.err.println("srcRegion: " + srcRegion);
+// System.err.println("dstRegion: " + dstRegion);
+//
+// throw e;
+// }
+ }
+ }
+ finally {
+ // NOTE: Would be cleaner to clone the param, unfortunately it can't be done easily...
+ param.setSourceRegion(srcRegion);
+
+ // Restore normal read progress processing
+ progressDelegator.resetProgressRange();
+ }
+
+ processImageComplete();
+
+ return image;
+ }
+
+ private void initHeader() throws IOException {
+ if (segments == null) {
+ long start = DEBUG ? System.currentTimeMillis() : 0;
+
+ readSegments();
+
+ if (DEBUG) {
+ System.out.println("Read metadata in " + (System.currentTimeMillis() - start) + " ms");
+ }
+ }
+ }
+
+ private void readSegments() throws IOException {
+ long pos = mImageInput.getStreamPosition();
+
+ try {
+ mImageInput.seek(0); // TODO: Seek to wanted image
+
+ segments = JPEGSegmentUtil.readSegments(mImageInput, SEGMENT_IDENTIFIERS);
+ }
+ catch (IOException ignore) {
+ }
+ catch (IllegalArgumentException foo) {
+ foo.printStackTrace();
+ }
+ finally {
+ mImageInput.seek(pos);
+ }
+ }
+
+ private List getAppSegments(final int marker, final String identifier) throws IOException {
+ initHeader();
+
+ List appSegments = Collections.emptyList();
+
+ for (JPEGSegmentUtil.Segment segment : segments) {
+ if (segment.marker() == marker && identifier.equals(segment.identifier())) {
+ if (appSegments == Collections.EMPTY_LIST) {
+ appSegments = new ArrayList(segments.size());
+ }
+
+ appSegments.add(segment);
+ }
+ }
+
+ return appSegments;
+ }
+
+ public boolean isJFIFAPP0Present() throws IOException {
+ return !(getAppSegments(JPEG.APP0, "JFIF").isEmpty() && getAppSegments(JPEG.APP0, "JFXX").isEmpty());
+ }
+
+ private SOF getSOF() throws IOException {
+ for (JPEGSegmentUtil.Segment segment : segments) {
+ if (JPEG.SOF0 <= segment.marker() && segment.marker() <= JPEG.SOF3 ||
+ JPEG.SOF5 <= segment.marker() && segment.marker() <= JPEG.SOF7 ||
+ JPEG.SOF9 <= segment.marker() && segment.marker() <= JPEG.SOF11 ||
+ JPEG.SOF13 <= segment.marker() && segment.marker() <= JPEG.SOF15) {
+
+ DataInputStream data = new DataInputStream(segment.data());
+ try {
+ int samplePrecision = data.readUnsignedByte();
+ int lines = data.readUnsignedShort();
+ int samplesPerLine = data.readUnsignedShort();
+ int componentsInFrame = data.readUnsignedByte();
+
+ /**
+ // Might not need this
+ for (int i = 0; i < componentsInFrame; i++) {
+ int comp = i + 1;
+ int id = data.readUnsignedByte();
+ System.err.println(comp + " id: " + id + " '" + (char) id + "'"); // typically 1-4, but may be 'C'/'M'/'Y'/'K' or similar...
+ int hv = data.readUnsignedByte();
+ System.err.println(comp + " horiz sub: " + ((hv & 0xF0) >> 4));
+ System.err.println(comp + " vertical sub: " + ((hv & 0xF)));
+ System.err.println(comp + " qt sel: " + data.readUnsignedByte());
+ }
+ //*/
+
+ return new SOF(segment.marker(), samplePrecision, lines, samplesPerLine, componentsInFrame);
+ }
+ finally {
+ data.close();
+ }
+ }
+ }
+
+ return null;
+ }
+
+ private AdobeDCT getAdobeDCT() throws IOException {
+ List adobe = getAppSegments(JPEG.APP14, "Adobe");
+
+ if (!adobe.isEmpty()) {
+ // version (byte), flags (4bytes), color transform (byte: 0=unknown, 1=YCC, 2=YCCK)
+ DataInputStream stream = new DataInputStream(adobe.get(0).data());
+
+ return new AdobeDCT(
+ stream.readUnsignedByte(),
+ stream.readUnsignedShort(),
+ stream.readUnsignedShort(),
+ stream.readUnsignedByte()
+ );
+ }
+
+ return null;
+ }
+
+ private ICC_Profile getEmbeddedICCProfile() 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)
+ List segments = getAppSegments(JPEG.APP2, "ICC_PROFILE");
+
+ if (segments.size() == 1) {
+ // Faster code for the common case
+ JPEGSegmentUtil.Segment segment = segments.get(0);
+ DataInputStream stream = new DataInputStream(segment.data());
+ int chunkNumber = stream.readUnsignedByte();
+ int chunkCount = stream.readUnsignedByte();
+
+ if (chunkNumber != 1 && chunkCount != 1) {
+ throw new IIOException(String.format("Bad number of 'ICC_PROFILE' chunks."));
+ }
+
+ return ICC_Profile.getInstance(stream);
+ }
+ else if (!segments.isEmpty()) {
+ // NOTE: This is probably over-complicated, as I've never encountered ICC_PROFILE chunks out of order...
+ DataInputStream stream = new DataInputStream(segments.get(0).data());
+ int chunkNumber = stream.readUnsignedByte();
+ int chunkCount = stream.readUnsignedByte();
+
+ InputStream[] streams = new InputStream[chunkCount];
+ streams[chunkNumber - 1] = stream;
+
+ for (int i = 1; i < chunkCount; i++) {
+ stream = new DataInputStream(segments.get(i).data());
+
+ chunkNumber = stream.readUnsignedByte();
+ if (stream.readUnsignedByte() != chunkCount) {
+ throw new IIOException(String.format("Bad number of 'ICC_PROFILE' chunks."));
+ }
+
+ streams[chunkNumber - 1] = stream;
+ }
+
+ return ICC_Profile.getInstance(new SequenceInputStream(Collections.enumeration(Arrays.asList(streams))));
+ }
+
+ return null;
+ }
+
+ @Override
+ public boolean canReadRaster() {
+ return delegate.canReadRaster();
+ }
+
+ @Override
+ public Raster readRaster(int imageIndex, ImageReadParam param) throws IOException {
+ return delegate.readRaster(imageIndex, param);
+ }
+
+ @Override
+ public RenderedImage readAsRenderedImage(int imageIndex, ImageReadParam param) throws IOException {
+ return read(imageIndex, param);
+ }
+
+ @Override
+ public void abort() {
+ super.abort();
+
+ delegate.abort();
+ }
+
+ @Override
+ public boolean readerSupportsThumbnails() {
+ return delegate.readerSupportsThumbnails();
+ }
+
+ @Override
+ public boolean hasThumbnails(int imageIndex) throws IOException {
+ return delegate.hasThumbnails(imageIndex);
+ }
+
+ @Override
+ public int getNumThumbnails(int imageIndex) throws IOException {
+ return delegate.getNumThumbnails(imageIndex);
+ }
+
+ @Override
+ public int getThumbnailWidth(int imageIndex, int thumbnailIndex) throws IOException {
+ return delegate.getThumbnailWidth(imageIndex, thumbnailIndex);
+ }
+
+ @Override
+ public int getThumbnailHeight(int imageIndex, int thumbnailIndex) throws IOException {
+ return delegate.getThumbnailHeight(imageIndex, thumbnailIndex);
+ }
+
+ @Override
+ public BufferedImage readThumbnail(int imageIndex, int thumbnailIndex) throws IOException {
+ return delegate.readThumbnail(imageIndex, thumbnailIndex);
+ }
+
+ private static void invertCMYK(final Raster raster) {
+ byte[] data = ((DataBufferByte) raster.getDataBuffer()).getData();
+ for (int i = 0, dataLength = data.length; i < dataLength; i++) {
+ data[i] = (byte) (255 - data[i] & 0xff);
+ }
+ }
+
+ /**
+ * Static inner class for lazy-loading of conversion tables.
+ */
+ static final class YCbCrConverter {
+ /** Define tables for YCC->RGB color space conversion. */
+ private final static int SCALEBITS = 16;
+ private final static int MAXJSAMPLE = 255;
+ private final static int CENTERJSAMPLE = 128;
+ private final static int ONE_HALF = 1 << (SCALEBITS - 1);
+ private final static int[] Cr_R_LUT = new int[MAXJSAMPLE + 1];
+ private final static int[] Cb_B_LUT = new int[MAXJSAMPLE + 1];
+ private final static int[] Cr_G_LUT = new int[MAXJSAMPLE + 1];
+ private final static int[] Cb_G_LUT = new int[MAXJSAMPLE + 1];
+
+ /**
+ * Initializes tables for YCC->RGB color space conversion.
+ */
+ private static void buildYCCtoRGBtable() {
+ if (DEBUG) {
+ System.err.println("Building YCC conversion table");
+ }
+
+ for (int i = 0, x = -CENTERJSAMPLE; i <= MAXJSAMPLE; i++, x++) {
+ // i is the actual input pixel value, in the range 0..MAXJSAMPLE
+ // The Cb or Cr value we are thinking of is x = i - CENTERJSAMPLE
+ // Cr=>R value is nearest int to 1.40200 * x
+ Cr_R_LUT[i] = (int) ((1.40200 * (1 << SCALEBITS) + 0.5) * x + ONE_HALF) >> SCALEBITS;
+ // Cb=>B value is nearest int to 1.77200 * x
+ Cb_B_LUT[i] = (int) ((1.77200 * (1 << SCALEBITS) + 0.5) * x + ONE_HALF) >> SCALEBITS;
+ // Cr=>G value is scaled-up -0.71414 * x
+ Cr_G_LUT[i] = -(int) (0.71414 * (1 << SCALEBITS) + 0.5) * x;
+ // Cb=>G value is scaled-up -0.34414 * x
+ // We also add in ONE_HALF so that need not do it in inner loop
+ Cb_G_LUT[i] = -(int) ((0.34414) * (1 << SCALEBITS) + 0.5) * x + ONE_HALF;
+ }
+ }
+
+ static {
+ buildYCCtoRGBtable();
+ }
+
+ static void convertYCbCr2RGB(final Raster raster) {
+ final int height = raster.getHeight();
+ final int width = raster.getWidth();
+
+ final byte[] data = ((DataBufferByte) raster.getDataBuffer()).getData();
+ for (int y = 0; y < height; y++) {
+ for (int x = 0; x < width; x++) {
+ convertYCbCr2RGB(data, data, (x + y * width) * 3);
+ }
+ }
+ }
+
+ private static void convertYCbCr2RGB(final byte[] yCbCr, final byte[] rgb, final int offset) {
+ int y = yCbCr[offset ] & 0xff;
+ int cr = yCbCr[offset + 2] & 0xff;
+ int cb = yCbCr[offset + 1] & 0xff;
+
+ rgb[offset ] = clamp(y + Cr_R_LUT[cr]);
+ rgb[offset + 1] = clamp(y + (Cb_G_LUT[cb] + Cr_G_LUT[cr] >> SCALEBITS));
+ rgb[offset + 2] = clamp(y + Cb_B_LUT[cb]);
+ }
+
+ static void convertYCCK2CMYK(final Raster raster) {
+ final int height = raster.getHeight();
+ final int width = raster.getWidth();
+
+ final byte[] data = ((DataBufferByte) raster.getDataBuffer()).getData();
+ for (int y = 0; y < height; y++) {
+ for (int x = 0; x < width; x++) {
+ convertYCCK2CMYK(data, data, (x + y * width) * 4);
+ }
+ }
+ }
+
+ private static void convertYCCK2CMYK(byte[] ycck, byte[] cmyk, int offset) {
+ // Inverted
+ int y = 255 - ycck[offset ] & 0xff;
+ int cb = 255 - ycck[offset + 1] & 0xff;
+ int cr = 255 - ycck[offset + 2] & 0xff;
+ int k = 255 - ycck[offset + 3] & 0xff;
+
+ int cmykC = MAXJSAMPLE - (y + Cr_R_LUT[cr]);
+ int cmykM = MAXJSAMPLE - (y + (Cb_G_LUT[cb] + Cr_G_LUT[cr] >> SCALEBITS));
+ int cmykY = MAXJSAMPLE - (y + Cb_B_LUT[cb]);
+
+ cmyk[offset ] = clamp(cmykC);
+ cmyk[offset + 1] = clamp(cmykM);
+ cmyk[offset + 2] = clamp(cmykY);
+ cmyk[offset + 3] = (byte) k; // K passes through unchanged
+ }
+
+ private static byte clamp(int val) {
+ return (byte) Math.max(0, Math.min(255, val));
+ }
+ }
+
+ private class ProgressDelegator extends ProgressListenerBase implements IIOReadUpdateListener, IIOReadWarningListener {
+ float readProgressStart = -1;
+ float readProgressStop = -1;
+
+ void resetProgressRange() {
+ readProgressStart = -1;
+ readProgressStop = -1;
+ }
+
+ private boolean isProgressRangeCorrected() {
+ return readProgressStart == -1 && readProgressStop == -1;
+ }
+
+ void updateProgressRange(float limit) {
+ Validate.isTrue(limit >= 0, limit, "Negative range limit");
+
+ readProgressStart = readProgressStop != -1 ? readProgressStop : 0;
+ readProgressStop = limit;
+ }
+
+ @Override
+ public void imageComplete(ImageReader source) {
+ if (isProgressRangeCorrected()) {
+ processImageComplete();
+ }
+ }
+
+ @Override
+ public void imageProgress(ImageReader source, float percentageDone) {
+ if (isProgressRangeCorrected()) {
+ processImageProgress(percentageDone);
+ }
+ else {
+ processImageProgress(readProgressStart + (percentageDone * (readProgressStop - readProgressStart) / 100f));
+ }
+ }
+
+ @Override
+ public void imageStarted(ImageReader source, int imageIndex) {
+ if (isProgressRangeCorrected()) {
+ processImageStarted(imageIndex);
+ }
+ }
+
+ @Override
+ public void readAborted(ImageReader source) {
+ if (isProgressRangeCorrected()) {
+ processReadAborted();
+ }
+ }
+
+ @Override
+ public void sequenceComplete(ImageReader source) {
+ processSequenceComplete();
+ }
+
+ @Override
+ public void sequenceStarted(ImageReader source, int minIndex) {
+ processSequenceStarted(minIndex);
+ }
+
+ @Override
+ public void thumbnailComplete(ImageReader source) {
+ processThumbnailComplete();
+ }
+
+ @Override
+ public void thumbnailProgress(ImageReader source, float percentageDone) {
+ processThumbnailProgress(percentageDone);
+ }
+
+ @Override
+ public void thumbnailStarted(ImageReader source, int imageIndex, int thumbnailIndex) {
+ processThumbnailStarted(imageIndex, thumbnailIndex);
+ }
+
+ @Override
+ public void imageComplete(ImageWriter source) {
+ processImageComplete();
+ }
+
+ public void passStarted(ImageReader source, BufferedImage theImage, int pass, int minPass, int maxPass, int minX, int minY, int periodX, int periodY, int[] bands) {
+ processPassStarted(theImage, pass, minPass, maxPass, minX, minY, periodX, periodY, bands);
+ }
+
+ public void imageUpdate(ImageReader source, BufferedImage theImage, int minX, int minY, int width, int height, int periodX, int periodY, int[] bands) {
+ processImageUpdate(theImage, minX, minY, width, height, periodX, periodY, bands);
+ }
+
+ public void passComplete(ImageReader source, BufferedImage theImage) {
+ processPassComplete(theImage);
+ }
+
+ public void thumbnailPassStarted(ImageReader source, BufferedImage theThumbnail, int pass, int minPass, int maxPass, int minX, int minY, int periodX, int periodY, int[] bands) {
+ processThumbnailPassStarted(theThumbnail, pass, minPass, maxPass, minX, minY, periodX, periodY, bands);
+ }
+
+ public void thumbnailUpdate(ImageReader source, BufferedImage theThumbnail, int minX, int minY, int width, int height, int periodX, int periodY, int[] bands) {
+ processThumbnailUpdate(theThumbnail, minX, minY, width, height, periodX, periodY, bands);
+ }
+
+ public void thumbnailPassComplete(ImageReader source, BufferedImage theThumbnail) {
+ processThumbnailPassComplete(theThumbnail);
+ }
+
+ public void warningOccurred(ImageReader source, String warning) {
+ processWarningOccurred(warning);
+ }
+
+ }
+
+ private static class ICCSpaceInterceptor extends ProgressListenerBase {
+ final Field iccCS;
+
+ public ICCSpaceInterceptor(final Field iccField) {
+ iccCS = iccField;
+ }
+
+ @Override
+ public void imageStarted(final ImageReader source, final int imageIndex) {
+ replaceCS(source);
+ }
+
+ private void replaceCS(final ImageReader source) {
+ // Intercept and modify the ICC color profile just before the read starts
+ if (iccCS != null) {
+ try {
+ ICC_ColorSpace cs = (ICC_ColorSpace) iccCS.get(source);
+
+ if (cs != null) {
+ ICC_Profile profile = cs.getProfile();
+
+ byte[] header = profile.getData(ICC_Profile.icSigHead);
+
+ // ColorConvertOp (or the sun.awt.color.CMM class, really) seems to choke on
+ // rendering intent other than perceptual (0), so we'll change the intent
+ // NOTE: Rendering intent is really a 4 byte value,
+ // but legal values are 0-3 (see ICC1v42_2006_05_1.pdf, 7.2.15, p. 19)
+ if (header[ICC_Profile.icHdrRenderingIntent] != 0) {
+ header[ICC_Profile.icHdrRenderingIntent] = 0;
+
+ // NOTE: We are mutating the current profile in place,
+ // so we don't need to write the iccCS field back..
+ profile.setData(ICC_Profile.icSigHead, header);
+
+ // But do it anyway, just in case...
+ iccCS.set(source, ColorSpaces.createColorSpace(profile));
+ }
+ }
+ }
+ catch (IllegalAccessException ignore) {
+ }
+ }
+ }
+ }
+
+ private static class SOF {
+ private final int marker;
+ private final int samplePrecision;
+ private final int lines; // height
+ private final int samplesPerLine; // width
+ private final int componentsInFrame;
+
+ public SOF(int marker, int samplePrecision, int lines, int samplesPerLine, int componentsInFrame) {
+ this.marker = marker;
+ this.samplePrecision = samplePrecision;
+ this.lines = lines;
+ this.samplesPerLine = samplesPerLine;
+ this.componentsInFrame = componentsInFrame;
+ }
+
+ public int getMarker() {
+ return marker;
+ }
+
+ public int getSamplePrecision() {
+ return samplePrecision;
+ }
+
+ public int getLines() {
+ return lines;
+ }
+
+ public int getSamplesPerLine() {
+ return samplesPerLine;
+ }
+
+ public int getComponentsInFrame() {
+ return componentsInFrame;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ "SOF[marker: %04x, preciscion: %d, lines: %d, samples/line: %d, components: %d]",
+ marker, samplePrecision, lines, samplesPerLine, componentsInFrame
+ );
+ }
+ }
+
+ private static class AdobeDCT {
+ public static final int Unknown = 0;
+ public static final int YCC = 1;
+ public static final int YCCK = 2;
+
+ private final int version;
+ private final int flags0;
+ private final int flags1;
+ private final int transform;
+
+ public AdobeDCT(int version, int flags0, int flags1, int transform) {
+ this.version = version;
+ this.flags0 = flags0;
+ this.flags1 = flags1;
+ this.transform = transform;
+ }
+
+ public int getVersion() {
+ return version;
+ }
+
+ public int getFlags0() {
+ return flags0;
+ }
+
+ public int getFlags1() {
+ return flags1;
+ }
+
+ public int getTransform() {
+ return transform;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ "AdobeDCT[ver: %d, flags: %s %s, transform: %d]",
+ getVersion(), Integer.toBinaryString(getFlags0()), Integer.toBinaryString(getFlags1()), getTransform()
+ );
+ }
+ }
+}
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
new file mode 100644
index 00000000..86ec5246
--- /dev/null
+++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReaderSpi.java
@@ -0,0 +1,213 @@
+/*
+ * Copyright (c) 2011, Harald Kuhr
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * Neither the name "TwelveMonkeys" nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.twelvemonkeys.imageio.plugins.jpeg;
+
+import com.twelvemonkeys.imageio.spi.ProviderInfo;
+import com.twelvemonkeys.imageio.util.IIOUtil;
+import com.twelvemonkeys.lang.Validate;
+
+import javax.imageio.ImageReader;
+import javax.imageio.metadata.IIOMetadataFormat;
+import javax.imageio.spi.ImageReaderSpi;
+import javax.imageio.spi.ServiceRegistry;
+import java.io.IOException;
+import java.util.Locale;
+
+/**
+ * JPEGImageReaderSpi
+ *
+ * @author Harald Kuhr
+ * @author last modified by $Author: haraldk$
+ * @version $Id: JPEGImageReaderSpi.java,v 1.0 24.01.11 22.12 haraldk Exp$
+ */
+public class JPEGImageReaderSpi extends ImageReaderSpi {
+ private ImageReaderSpi delegateProvider;
+
+ /**
+ * Constructor for use by {@link javax.imageio.spi.IIORegistry} only.
+ * The instance created will not work without being properly registered.
+ */
+ public JPEGImageReaderSpi() {
+ this(IIOUtil.getProviderInfo(JPEGImageReaderSpi.class));
+ }
+
+ private JPEGImageReaderSpi(final ProviderInfo providerInfo) {
+ super(
+ providerInfo.getVendorName(),
+ providerInfo.getVersion(),
+ new String[]{"JPEG", "jpeg", "JPG", "jpg"},
+ new String[]{"jpg", "jpeg"},
+ new String[]{"image/jpeg"},
+ JPEGImageReader.class.getName(),
+ STANDARD_INPUT_TYPE,
+ null,
+ true, null, null, null, null,
+ true, null, null, null, null
+ );
+ }
+
+ /**
+ * Creates a {@code JPEGImageReaderSpi} with the given delegate.
+ *
+ * @param delegateProvider a {@code ImageReaderSpi} that can read JPEG.
+ */
+ protected JPEGImageReaderSpi(final ImageReaderSpi delegateProvider) {
+ this(IIOUtil.getProviderInfo(JPEGImageReaderSpi.class));
+
+ this.delegateProvider = Validate.notNull(delegateProvider);
+ }
+
+ ImageReaderSpi lookupDelegate(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();
+ }
+
+ return null;
+ }
+
+ @SuppressWarnings({"unchecked"})
+ @Override
+ public void onRegistration(final ServiceRegistry registry, final Class> category) {
+ if (delegateProvider == null) {
+ // Install delegate now
+ delegateProvider = lookupDelegate(registry);
+ }
+
+ if (delegateProvider == null) {
+ IIOUtil.deregisterProvider(registry, this, category);
+ }
+ else {
+ // Order before com.sun provider, to aid ImageIO in selecting our reader
+ registry.setOrdering((Class) category, this, delegateProvider);
+ }
+ }
+
+ @Override
+ public String getVendorName() {
+ return String.format("%s/%s", super.getVendorName(), delegateProvider.getVendorName());
+ }
+
+ @Override
+ public String getVersion() {
+ return String.format("%s/%s", super.getVersion(), delegateProvider.getVersion());
+ }
+
+ @Override
+ public ImageReader createReaderInstance(Object extension) throws IOException {
+ return new JPEGImageReader(this, delegateProvider.createReaderInstance(extension));
+ }
+
+ @Override
+ public boolean canDecodeInput(Object source) throws IOException {
+ return delegateProvider.canDecodeInput(source);
+ }
+
+ @Override
+ public String[] getImageWriterSpiNames() {
+ // TODO: The WriterSpi will have a similar method, and it should return the name of this class...
+ return delegateProvider.getImageWriterSpiNames();
+ }
+
+ @Override
+ public String[] getFormatNames() {
+ return delegateProvider.getFormatNames();
+ }
+
+ @Override
+ public String[] getFileSuffixes() {
+ return delegateProvider.getFileSuffixes();
+ }
+
+ @Override
+ public String[] getMIMETypes() {
+ return delegateProvider.getMIMETypes();
+ }
+
+ @Override
+ public String getPluginClassName() {
+ return "com.twelvemonkeys.imageio.plugins.jpeg.JPEGImageReader";
+// return delegateProvider.getPluginClassName();
+ }
+
+ @Override
+ public boolean isStandardStreamMetadataFormatSupported() {
+ return delegateProvider.isStandardStreamMetadataFormatSupported();
+ }
+
+ @Override
+ public String getNativeStreamMetadataFormatName() {
+ return delegateProvider.getNativeStreamMetadataFormatName();
+ }
+
+ @Override
+ public String[] getExtraStreamMetadataFormatNames() {
+ return delegateProvider.getExtraStreamMetadataFormatNames();
+ }
+
+ @Override
+ public boolean isStandardImageMetadataFormatSupported() {
+ return delegateProvider.isStandardImageMetadataFormatSupported();
+ }
+
+ @Override
+ public String getNativeImageMetadataFormatName() {
+ return delegateProvider.getNativeImageMetadataFormatName();
+ }
+
+ @Override
+ public String[] getExtraImageMetadataFormatNames() {
+ return delegateProvider.getExtraImageMetadataFormatNames();
+ }
+
+ @Override
+ public IIOMetadataFormat getStreamMetadataFormat(String formatName) {
+ return delegateProvider.getStreamMetadataFormat(formatName);
+ }
+
+ @Override
+ public IIOMetadataFormat getImageMetadataFormat(String formatName) {
+ return delegateProvider.getImageMetadataFormat(formatName);
+ }
+
+ @Override
+ public String getDescription(Locale locale) {
+ return delegateProvider.getDescription(locale);
+ }
+
+ @Override
+ public Class[] getInputTypes() {
+ return delegateProvider.getInputTypes();
+ }
+}
diff --git a/imageio/imageio-jpeg/src/main/resources/META-INF/services/javax.imageio.spi.ImageReaderSpi b/imageio/imageio-jpeg/src/main/resources/META-INF/services/javax.imageio.spi.ImageReaderSpi
new file mode 100644
index 00000000..90624014
--- /dev/null
+++ b/imageio/imageio-jpeg/src/main/resources/META-INF/services/javax.imageio.spi.ImageReaderSpi
@@ -0,0 +1 @@
+com.twelvemonkeys.imageio.plugins.jpeg.JPEGImageReaderSpi
\ No newline at end of file
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
new file mode 100644
index 00000000..2f6fca4a
--- /dev/null
+++ b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReaderTest.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (c) 2011, Harald Kuhr
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * Neither the name "TwelveMonkeys" nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.twelvemonkeys.imageio.plugins.jpeg;
+
+import com.twelvemonkeys.imageio.util.ImageReaderAbstractTestCase;
+
+import javax.imageio.spi.IIORegistry;
+import javax.imageio.spi.ImageReaderSpi;
+import java.awt.*;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * JPEGImageReaderTest
+ *
+ * @author Harald Kuhr
+ * @author last modified by $Author: haraldk$
+ * @version $Id: JPEGImageReaderTest.java,v 1.0 24.01.11 22.04 haraldk Exp$
+ */
+public class JPEGImageReaderTest extends ImageReaderAbstractTestCase {
+
+ private static final JPEGImageReaderSpi SPI = new JPEGImageReaderSpi();
+
+ static {
+ IIORegistry.getDefaultInstance().registerServiceProvider(SPI);
+ }
+
+ @Override
+ protected List getTestData() {
+ return Arrays.asList(
+ // TODO: Create JPEG with broken profile from Anders' photos..
+ new TestData(getClassLoaderResource("/jpeg/cmm-exception-adobe-rgb.jpg"), new Dimension(626, 76)),
+ new TestData(getClassLoaderResource("/jpeg/cmm-exception-srgb.jpg"), new Dimension(1800, 1200)),
+ new TestData(getClassLoaderResource("/jpeg/gray-sample.jpg"), new Dimension(386, 396)),
+ new TestData(getClassLoaderResource("/jpeg/cmyk-sample.jpg"), new Dimension(160, 227)),
+ new TestData(getClassLoaderResource("/jpeg/cmyk-sample-multiple-chunk-icc.jpg"), new Dimension(2707, 3804))
+ );
+ }
+
+ @Override
+ protected ImageReaderSpi createProvider() {
+ return SPI;
+ }
+
+ @Override
+ protected JPEGImageReader createReader() {
+ try {
+ return (JPEGImageReader) SPI.createReaderInstance();
+ }
+ catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @SuppressWarnings({"unchecked"})
+ @Override
+ protected Class getReaderClass() {
+ return JPEGImageReader.class;
+ }
+
+ @Override
+ protected boolean allowsNullRawImageType() {
+ return true;
+ }
+
+ @Override
+ protected List getFormatNames() {
+ return Arrays.asList("JPEG", "jpeg", "JPG", "jpg");
+ }
+
+ @Override
+ protected List getSuffixes() {
+ return Arrays.asList("jpeg", "jpg");
+ }
+
+ @Override
+ protected List getMIMETypes() {
+ return Arrays.asList("image/jpeg");
+ }
+
+ @Override
+ public void testSetDestinationType() throws IOException {
+ // TODO: This method currently fails, fix it
+ super.testSetDestinationType();
+ }
+}
diff --git a/imageio/imageio-jpeg/src/test/resources/jpeg/cmm-exception-adobe-rgb.jpg b/imageio/imageio-jpeg/src/test/resources/jpeg/cmm-exception-adobe-rgb.jpg
new file mode 100644
index 00000000..72dcdc69
Binary files /dev/null and b/imageio/imageio-jpeg/src/test/resources/jpeg/cmm-exception-adobe-rgb.jpg differ
diff --git a/imageio/imageio-jpeg/src/test/resources/jpeg/cmm-exception-srgb.jpg b/imageio/imageio-jpeg/src/test/resources/jpeg/cmm-exception-srgb.jpg
new file mode 100644
index 00000000..0ea63d8b
Binary files /dev/null and b/imageio/imageio-jpeg/src/test/resources/jpeg/cmm-exception-srgb.jpg differ
diff --git a/imageio/imageio-jpeg/src/test/resources/jpeg/cmyk-sample-multiple-chunk-icc.jpg b/imageio/imageio-jpeg/src/test/resources/jpeg/cmyk-sample-multiple-chunk-icc.jpg
new file mode 100644
index 00000000..1e35e3b8
Binary files /dev/null and b/imageio/imageio-jpeg/src/test/resources/jpeg/cmyk-sample-multiple-chunk-icc.jpg differ
diff --git a/imageio/imageio-jpeg/src/test/resources/jpeg/cmyk-sample.jpg b/imageio/imageio-jpeg/src/test/resources/jpeg/cmyk-sample.jpg
new file mode 100644
index 00000000..295234be
Binary files /dev/null and b/imageio/imageio-jpeg/src/test/resources/jpeg/cmyk-sample.jpg differ
diff --git a/imageio/imageio-jpeg/src/test/resources/jpeg/gray-sample.jpg b/imageio/imageio-jpeg/src/test/resources/jpeg/gray-sample.jpg
new file mode 100644
index 00000000..a20a5899
Binary files /dev/null and b/imageio/imageio-jpeg/src/test/resources/jpeg/gray-sample.jpg differ
diff --git a/imageio/pom.xml b/imageio/pom.xml
index 7918a451..05e6c56c 100644
--- a/imageio/pom.xml
+++ b/imageio/pom.xml
@@ -14,7 +14,6 @@
pom
TwelveMonkeys :: ImageIO
-
Harald Kuhr
@@ -34,7 +33,8 @@
imageio-ico
imageio-iff
- imageio-pdf
+ imageio-jpeg
+ imageio-pdf
imageio-pict
imageio-psd
imageio-thumbsdb
@@ -100,7 +100,8 @@
imageio-core
${project.version}
-
+
+
${project.groupId}
imageio-metadata
${project.version}