Merge remote-tracking branch 'upstream/master'

Conflicts:
	servlet/src/main/java/com/twelvemonkeys/servlet/ServletUtil.java
	servlet/src/main/java/com/twelvemonkeys/servlet/cache/HTTPCache.java
	servlet/src/test/java/com/twelvemonkeys/servlet/ServletConfigMapAdapterTest.java
	servlet/src/test/java/com/twelvemonkeys/servlet/ServletHeadersMapAdapterTest.java
	servlet/src/test/java/com/twelvemonkeys/servlet/ServletParametersMapAdapterTest.java
This commit is contained in:
Rune Bremnes
2013-08-14 09:45:32 +02:00
85 changed files with 5283 additions and 978 deletions
@@ -418,6 +418,10 @@ public abstract class ImageReaderBase extends ImageReader {
}
private static class ImageLabel extends JLabel {
static final String ZOOM_IN = "zoom-in";
static final String ZOOM_OUT = "zoom-out";
static final String ZOOM_ACTUAL = "zoom-actual";
Paint backgroundPaint;
final Paint checkeredBG;
@@ -435,9 +439,8 @@ public abstract class ImageReaderBase extends ImageReader {
backgroundPaint = defaultBG != null ? defaultBG : checkeredBG;
JPopupMenu popup = createBackgroundPopup();
setComponentPopupMenu(popup);
setupActions(pImage);
setComponentPopupMenu(createPopupMenu());
addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
@@ -448,24 +451,52 @@ public abstract class ImageReaderBase extends ImageReader {
});
}
private JPopupMenu createBackgroundPopup() {
private void setupActions(final BufferedImage pImage) {
// Mac weirdness... VK_MINUS/VK_PLUS seems to map to english key map always...
bindAction(new ZoomAction("Zoom in", pImage, 2), ZOOM_IN, KeyStroke.getKeyStroke('+'), KeyStroke.getKeyStroke(KeyEvent.VK_ADD, 0));
bindAction(new ZoomAction("Zoom out", pImage, .5), ZOOM_OUT, KeyStroke.getKeyStroke('-'), KeyStroke.getKeyStroke(KeyEvent.VK_SUBTRACT, 0));
bindAction(new ZoomAction("Zoom actual", pImage), ZOOM_ACTUAL, KeyStroke.getKeyStroke('0'), KeyStroke.getKeyStroke(KeyEvent.VK_0, 0));
}
private void bindAction(final AbstractAction action, final String key, final KeyStroke... keyStrokes) {
for (KeyStroke keyStroke : keyStrokes) {
getInputMap(WHEN_IN_FOCUSED_WINDOW).put(keyStroke, key);
}
getActionMap().put(key, action);
}
private JPopupMenu createPopupMenu() {
JPopupMenu popup = new JPopupMenu();
popup.add(getActionMap().get(ZOOM_ACTUAL));
popup.add(getActionMap().get(ZOOM_IN));
popup.add(getActionMap().get(ZOOM_OUT));
popup.addSeparator();
ButtonGroup group = new ButtonGroup();
addCheckBoxItem(new ChangeBackgroundAction("Checkered", checkeredBG), popup, group);
popup.addSeparator();
addCheckBoxItem(new ChangeBackgroundAction("White", Color.WHITE), popup, group);
addCheckBoxItem(new ChangeBackgroundAction("Light", Color.LIGHT_GRAY), popup, group);
addCheckBoxItem(new ChangeBackgroundAction("Gray", Color.GRAY), popup, group);
addCheckBoxItem(new ChangeBackgroundAction("Dark", Color.DARK_GRAY), popup, group);
addCheckBoxItem(new ChangeBackgroundAction("Black", Color.BLACK), popup, group);
popup.addSeparator();
addCheckBoxItem(new ChooseBackgroundAction("Choose...", defaultBG != null ? defaultBG : Color.BLUE), popup, group);
JMenu background = new JMenu("Background");
popup.add(background);
ChangeBackgroundAction checkered = new ChangeBackgroundAction("Checkered", checkeredBG);
checkered.putValue(Action.SELECTED_KEY, backgroundPaint == checkeredBG);
addCheckBoxItem(checkered, background, group);
background.addSeparator();
addCheckBoxItem(new ChangeBackgroundAction("White", Color.WHITE), background, group);
addCheckBoxItem(new ChangeBackgroundAction("Light", Color.LIGHT_GRAY), background, group);
addCheckBoxItem(new ChangeBackgroundAction("Gray", Color.GRAY), background, group);
addCheckBoxItem(new ChangeBackgroundAction("Dark", Color.DARK_GRAY), background, group);
addCheckBoxItem(new ChangeBackgroundAction("Black", Color.BLACK), background, group);
background.addSeparator();
ChooseBackgroundAction chooseBackgroundAction = new ChooseBackgroundAction("Choose...", defaultBG != null ? defaultBG : Color.BLUE);
chooseBackgroundAction.putValue(Action.SELECTED_KEY, backgroundPaint == defaultBG);
addCheckBoxItem(chooseBackgroundAction, background, group);
return popup;
}
private void addCheckBoxItem(final Action pAction, final JPopupMenu pPopup, final ButtonGroup pGroup) {
private void addCheckBoxItem(final Action pAction, final JMenu pPopup, final ButtonGroup pGroup) {
JCheckBoxMenuItem item = new JCheckBoxMenuItem(pAction);
pGroup.add(item);
pPopup.add(item);
@@ -553,6 +584,34 @@ public abstract class ImageReaderBase extends ImageReader {
}
}
}
private class ZoomAction extends AbstractAction {
private final BufferedImage image;
private final double zoomFactor;
public ZoomAction(final String name, final BufferedImage image, final double zoomFactor) {
super(name);
this.image = image;
this.zoomFactor = zoomFactor;
}
public ZoomAction(final String name, final BufferedImage image) {
this(name, image, 0);
}
public void actionPerformed(ActionEvent e) {
if (zoomFactor <= 0) {
setIcon(new BufferedImageIcon(image));
}
else {
Icon current = getIcon();
int w = (int) Math.max(Math.min(current.getIconWidth() * zoomFactor, image.getWidth() * 16), image.getWidth() / 16);
int h = (int) Math.max(Math.min(current.getIconHeight() * zoomFactor, image.getHeight() * 16), image.getHeight() / 16);
setIcon(new BufferedImageIcon(image, Math.max(w, 2), Math.max(h, 2), w > image.getWidth() || h > image.getHeight()));
}
}
}
}
private static class ExitIfNoWindowPresentHandler extends WindowAdapter {
@@ -73,8 +73,10 @@ public final class ColorSpaces {
private final static boolean DEBUG = "true".equalsIgnoreCase(System.getProperty("com.twelvemonkeys.imageio.color.debug"));
// JDK 7 seems to handle non-perceptual rendering intents gracefully, so we don't need to fiddle with the profiles
private final static boolean JDK_HANDLES_RENDERING_INTENTS = SystemUtil.isClassAvailable("java.lang.invoke.CallSite");
// OpenJDK 7 seems to handle non-perceptual rendering intents gracefully, so we don't need to fiddle with the profiles.
// However, the later Oracle distribute JDK seems to include the color management code that has the known bugs...
private final static boolean JDK_HANDLES_RENDERING_INTENTS =
SystemUtil.isClassAvailable("java.lang.invoke.CallSite") && !SystemUtil.isClassAvailable("sun.java2d.cmm.kcms.CMM");
// NOTE: java.awt.color.ColorSpace.CS_* uses 1000-1004, we'll use 5000+ to not interfere with future additions
@@ -171,6 +173,21 @@ public final class ColorSpaces {
}
}
/**
* Tests whether an ICC color profile is equal to the default sRGB profile.
*
* @param profile the ICC profile to test. May not be {@code null}.
* @return {@code true} if {@code profile} is equal to the default sRGB profile.
* @throws IllegalArgumentException if {@code profile} is {@code null}
*
* @see java.awt.color.ColorSpace#isCS_sRGB()
*/
public static boolean isCS_sRGB(final ICC_Profile profile) {
Validate.notNull(profile, "profile");
return profile.getColorSpaceType() == ColorSpace.TYPE_RGB && Arrays.equals(profile.getData(ICC_Profile.icSigHead), sRGB.header);
}
/**
* Tests whether an ICC color profile is known to cause problems for {@link java.awt.image.ColorConvertOp}.
* <p />
@@ -227,7 +244,7 @@ public final class ColorSpaces {
if (profile == null) {
// Fall back to the bundled ClayRGB1998 public domain Adobe RGB 1998 compatible profile,
// identical for all practical purposes
// which is identical for all practical purposes
profile = readProfileFromClasspathResource("/profiles/ClayRGB1998.icc");
if (profile == null) {
@@ -337,15 +354,19 @@ public final class ColorSpaces {
private static class sRGB {
private static final byte[] header = ICC_Profile.getInstance(ColorSpace.CS_sRGB).getData(ICC_Profile.icSigHead);
}
private static class CIEXYZ {
private static final byte[] header = ICC_Profile.getInstance(ColorSpace.CS_CIEXYZ).getData(ICC_Profile.icSigHead);
}
private static class PYCC {
private static final byte[] header = ICC_Profile.getInstance(ColorSpace.CS_PYCC).getData(ICC_Profile.icSigHead);
}
private static class GRAY {
private static final byte[] header = ICC_Profile.getInstance(ColorSpace.CS_GRAY).getData(ICC_Profile.icSigHead);
}
private static class LINEAR_RGB {
private static final byte[] header = ICC_Profile.getInstance(ColorSpace.CS_LINEAR_RGB).getData(ICC_Profile.icSigHead);
}
@@ -359,7 +380,14 @@ public final class ColorSpaces {
systemDefaults = SystemUtil.loadProperties(ColorSpaces.class, "com/twelvemonkeys/imageio/color/icc_profiles_" + os.id());
}
catch (IOException ignore) {
ignore.printStackTrace();
System.err.printf(
"Warning: Could not load system default ICC profile locations from %s, will use bundled fallback profiles.\n",
ignore.getMessage()
);
if (DEBUG) {
ignore.printStackTrace();
}
systemDefaults = null;
}
@@ -0,0 +1,29 @@
#
# Copyright (c) 2013, Harald Kuhr
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name "TwelveMonkeys" nor the
# names of its contributors may be used to endorse or promote products
# derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
#GENERIC_CMYK=unknown, use built in for now
#ADOBE_RGB_1998=unknown, use built in for now
@@ -26,4 +26,4 @@
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
GENERIC_CMYK=/C:/Windows/System32/spool/drivers/color/RSWOP.icm
#ADOBE_RGB_1998=use built in for now
#ADOBE_RGB_1998=unknown, use built in for now
@@ -139,7 +139,7 @@ public class ColorSpacesTest {
assertSame(cs, ColorSpaces.createColorSpace(iccCs.getProfile()));
}
else {
System.err.println("Not an ICC_ColorSpace: " + cs);
System.err.println("WARNING: Not an ICC_ColorSpace: " + cs);
}
}
@@ -163,7 +163,25 @@ public class ColorSpacesTest {
assertSame(cs, ColorSpaces.createColorSpace(iccCs.getProfile()));
}
else {
System.err.println("Not an ICC_ColorSpace: " + cs);
System.err.println("Warning: Not an ICC_ColorSpace: " + cs);
}
}
@Test
public void testIsCS_sRGBTrue() {
assertTrue(ColorSpaces.isCS_sRGB(ICC_Profile.getInstance(ColorSpace.CS_sRGB)));
}
@Test
public void testIsCS_sRGBFalse() {
assertFalse(ColorSpaces.isCS_sRGB(ICC_Profile.getInstance(ColorSpace.CS_LINEAR_RGB)));
assertFalse(ColorSpaces.isCS_sRGB(ICC_Profile.getInstance(ColorSpace.CS_CIEXYZ)));
assertFalse(ColorSpaces.isCS_sRGB(ICC_Profile.getInstance(ColorSpace.CS_GRAY)));
assertFalse(ColorSpaces.isCS_sRGB(ICC_Profile.getInstance(ColorSpace.CS_PYCC)));
}
@Test(expected = IllegalArgumentException.class)
public void testIsCS_sRGBNull() {
ColorSpaces.isCS_sRGB(null);
}
}
@@ -54,12 +54,16 @@ final class SipsJP2Reader {
private static final File SIPS_COMMAND = new File("/usr/bin/sips");
private static final boolean SIPS_EXISTS_AND_EXECUTES = existsAndExecutes(SIPS_COMMAND);
private static final boolean DEBUG = "true".equalsIgnoreCase(System.getProperty("com.twelvemonkeys.imageio.plugins.icns.debug"));
private static boolean existsAndExecutes(final File cmd) {
try {
return cmd.exists() && cmd.canExecute();
}
catch (SecurityException ignore) {
if (DEBUG) {
ignore.printStackTrace();
}
}
return false;
+25
View File
@@ -0,0 +1,25 @@
Copyright (c) 2013, Harald Kuhr
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name "TwelveMonkeys" nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -29,13 +29,13 @@
package com.twelvemonkeys.imageio.plugins.jpeg;
/**
* AdobeDCT
* AdobeDCTSegment
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: AdobeDCT.java,v 1.0 23.04.12 16:55 haraldk Exp$
* @version $Id: AdobeDCTSegment.java,v 1.0 23.04.12 16:55 haraldk Exp$
*/
class AdobeDCT {
class AdobeDCTSegment {
public static final int Unknown = 0;
public static final int YCC = 1;
public static final int YCCK = 2;
@@ -45,7 +45,7 @@ class AdobeDCT {
final int flags1;
final int transform;
public AdobeDCT(int version, int flags0, int flags1, int transform) {
AdobeDCTSegment(int version, int flags0, int flags1, int transform) {
this.version = version; // 100 or 101
this.flags0 = flags0;
this.flags1 = flags1;
@@ -147,7 +147,7 @@ final class EXIFThumbnailReader extends ThumbnailReader {
}
Entry bitsPerSample = ifd.getEntryById(TIFF.TAG_BITS_PER_SAMPLE);
Entry samplesPerPixel = ifd.getEntryById(TIFF.TAG_SAMPLES_PER_PIXELS);
Entry samplesPerPixel = ifd.getEntryById(TIFF.TAG_SAMPLES_PER_PIXEL);
Entry photometricInterpretation = ifd.getEntryById(TIFF.TAG_PHOTOMETRIC_INTERPRETATION);
// Required
@@ -131,11 +131,12 @@ class FastCMYKToRGB implements /*BufferedImageOp,*/ RasterOp {
return dest;
}
@SuppressWarnings({"PointlessArithmeticExpression"})
private void convertCMYKToRGB(byte[] cmyk, byte[] rgb) {
rgb[0] = (byte) (((255 - cmyk[0] & 0xFF) * (255 - cmyk[3] & 0xFF)) / 255);
rgb[1] = (byte) (((255 - cmyk[1] & 0xFF) * (255 - cmyk[3] & 0xFF)) / 255);
rgb[2] = (byte) (((255 - cmyk[2] & 0xFF) * (255 - cmyk[3] & 0xFF)) / 255);
// Adapted from http://www.easyrgb.com/index.php?X=MATH
final int k = cmyk[3] & 0xFF;
rgb[0] = (byte) (255 - (((cmyk[0] & 0xFF) * (255 - k) / 255) + k));
rgb[1] = (byte) (255 - (((cmyk[1] & 0xFF) * (255 - k) / 255) + k));
rgb[2] = (byte) (255 - (((cmyk[2] & 0xFF) * (255 - k) / 255) + k));
}
public Rectangle2D getBounds2D(Raster src) {
@@ -45,6 +45,7 @@ import com.twelvemonkeys.lang.Validate;
import javax.imageio.*;
import javax.imageio.event.IIOReadUpdateListener;
import javax.imageio.event.IIOReadWarningListener;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream;
import java.awt.*;
@@ -58,8 +59,26 @@ import java.util.List;
/**
* A JPEG {@code ImageReader} implementation based on the JRE {@code JPEGImageReader},
* with support for CMYK/YCCK JPEGs, non-standard color spaces,broken ICC profiles
* and more.
* that adds support and properly handles cases where the JRE version throws exceptions.
* <p/>
* Main features:
* <ul>
* <li>Support for CMYK JPEGs (converted to RGB by default or as CMYK, using the embedded ICC profile if applicable)</li>
* <li>Support for Adobe YCCK JPEGs (converted to RGB by default or as CMYK, using the embedded ICC profile if applicable)</li>
* <li>Support for JPEGs containing ICC profiles with interpretation other than 'Perceptual' (profile is assumed to be 'Perceptual' and used)</li>
* <li>Support for JPEGs containing ICC profiles with class other than 'Display' (profile is assumed to have class 'Display' and used)</li>
* <li>Support for JPEGs containing ICC profiles that are incompatible with stream data (image data is read, profile is ignored)</li>
* <li>Support for JPEGs with corrupted ICC profiles (image data is read, profile is ignored)</li>
* <li>Support for JPEGs with corrupted {@code ICC_PROFILE} segments (image data is read, profile is ignored)</li>
* <li>Support for JPEGs using non-standard color spaces, unsupported by Java 2D (image data is read, profile is ignored)</li>
* <li>Issues warnings instead of throwing exceptions in cases of corrupted data where ever the image data can still be read in a reasonable way</li>
* </ul>
* Thumbnail support:
* <ul>
* <li>Support for JFIF thumbnails (even if stream contains "inconsistent metadata")</li>
* <li>Support for JFXX thumbnails (JPEG, Indexed and RGB)</li>
* <li>Support for EXIF thumbnails (JPEG, RGB and YCbCr)</li>
* </ul>
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author LUT-based YCbCR conversion by Werner Randelshofer
@@ -76,7 +95,7 @@ public class JPEGImageReader extends ImageReaderBase {
private static final Map<Integer, List<String>> SEGMENT_IDENTIFIERS = createSegmentIds();
private static Map<Integer, List<String>> createSegmentIds() {
Map<Integer, List<String>> map = new HashMap<Integer, List<String>>();
Map<Integer, List<String>> map = new LinkedHashMap<Integer, List<String>>();
// JFIF/JFXX APP0 markers
map.put(JPEG.APP0, JPEGSegmentUtil.ALL_IDS);
@@ -181,7 +200,7 @@ public class JPEGImageReader extends ImageReaderBase {
typeList.add(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_INT_BGR));
// We also read and return CMYK if the source image is CMYK/YCCK + original color profile if present
ICC_Profile profile = getEmbeddedICCProfile();
ICC_Profile profile = getEmbeddedICCProfile(false);
if (csType == JPEGColorSpace.CMYK || csType == JPEGColorSpace.YCCK) {
if (profile != null) {
@@ -216,8 +235,7 @@ public class JPEGImageReader extends ImageReaderBase {
}
@Override
public
ImageTypeSpecifier getRawImageType(int imageIndex) throws IOException {
public ImageTypeSpecifier getRawImageType(int imageIndex) throws IOException {
// If delegate can determine the spec, we'll just go with that
ImageTypeSpecifier rawType = delegate.getRawImageType(imageIndex);
@@ -231,7 +249,7 @@ public class JPEGImageReader extends ImageReaderBase {
switch (csType) {
case CMYK:
// Create based on embedded profile if exists, or create from "Generic CMYK"
ICC_Profile profile = getEmbeddedICCProfile();
ICC_Profile profile = getEmbeddedICCProfile(false);
if (profile != null) {
return ImageTypeSpecifier.createInterleaved(ColorSpaces.createColorSpace(profile), new int[] {3, 2, 1, 0}, DataBuffer.TYPE_BYTE, false, false);
@@ -267,16 +285,17 @@ public class JPEGImageReader extends ImageReaderBase {
// 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();
AdobeDCT adobeDCT = getAdobeDCT();
ICC_Profile profile = getEmbeddedICCProfile(false);
AdobeDCTSegment adobeDCT = getAdobeDCT();
// 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 ||
adobeDCT != null && adobeDCT.getTransform() == AdobeDCT.YCCK ||
profile != null && (ColorSpaces.isOffendingColorProfile(profile) || profile.getColorSpaceType() == ColorSpace.TYPE_CMYK))) {
adobeDCT != null && adobeDCT.getTransform() == AdobeDCTSegment.YCCK ||
profile != null && !ColorSpaces.isCS_sRGB(profile))) {
// profile != null && (ColorSpaces.isOffendingColorProfile(profile) || profile.getColorSpaceType() == ColorSpace.TYPE_CMYK))) {
if (DEBUG) {
System.out.println("Reading using raster and extra conversion");
System.out.println("ICC color profile: " + profile);
@@ -296,8 +315,8 @@ public class JPEGImageReader extends ImageReaderBase {
int origWidth = getWidth(imageIndex);
int origHeight = getHeight(imageIndex);
AdobeDCT adobeDCT = getAdobeDCT();
SOF startOfFrame = getSOF();
AdobeDCTSegment adobeDCT = getAdobeDCT();
SOFSegment startOfFrame = getSOF();
JPEGColorSpace csType = getSourceCSType(adobeDCT, startOfFrame);
Iterator<ImageTypeSpecifier> imageTypes = getImageTypes(imageIndex);
@@ -316,12 +335,12 @@ public class JPEGImageReader extends ImageReaderBase {
}
else if (intendedCS != null) {
// Handle inconsistencies
if (startOfFrame.componentsInFrame != intendedCS.getNumComponents()) {
if (startOfFrame.componentsInFrame < 4 && (csType == JPEGColorSpace.CMYK || csType == JPEGColorSpace.YCCK)) {
if (startOfFrame.componentsInFrame() != intendedCS.getNumComponents()) {
if (startOfFrame.componentsInFrame() < 4 && (csType == JPEGColorSpace.CMYK || csType == JPEGColorSpace.YCCK)) {
processWarningOccurred(String.format(
"Invalid Adobe App14 marker. Indicates YCCK/CMYK data, but SOF%d has %d color components. " +
"Ignoring Adobe App14 marker, assuming YCbCr/RGB data.",
startOfFrame.marker & 0xf, startOfFrame.componentsInFrame
startOfFrame.marker & 0xf, startOfFrame.componentsInFrame()
));
csType = JPEGColorSpace.YCbCr;
@@ -332,12 +351,15 @@ public class JPEGImageReader extends ImageReaderBase {
"Embedded ICC color profile is incompatible with image data. " +
"Profile indicates %d components, but SOF%d has %d color components. " +
"Ignoring ICC profile, assuming source color space %s.",
intendedCS.getNumComponents(), startOfFrame.marker & 0xf, startOfFrame.componentsInFrame, csType
intendedCS.getNumComponents(), startOfFrame.marker & 0xf, startOfFrame.componentsInFrame(), csType
));
}
}
// NOTE: Avoid using CCOp if same color space, as it's more compatible that way
else if (intendedCS != image.getColorModel().getColorSpace()) {
if (DEBUG) {
System.err.println("Converting from " + intendedCS + " to " + (image.getColorModel().getColorSpace().isCS_sRGB() ? "sRGB" : image.getColorModel().getColorSpace()));
}
convert = new ColorConvertOp(intendedCS, image.getColorModel().getColorSpace(), null);
}
// Else, pass through with no conversion
@@ -346,10 +368,20 @@ public class JPEGImageReader extends ImageReaderBase {
ColorSpace cmykCS = ColorSpaces.getColorSpace(ColorSpaces.CS_GENERIC_CMYK);
if (cmykCS instanceof ICC_ColorSpace) {
processWarningOccurred(
"No embedded ICC color profile, defaulting to \"generic\" CMYK ICC profile. " +
"Colors may look incorrect."
);
convert = new ColorConvertOp(cmykCS, image.getColorModel().getColorSpace(), null);
}
else {
// ColorConvertOp using non-ICC CS is deadly slow, fall back to fast conversion instead
processWarningOccurred(
"No embedded ICC color profile, will convert using inaccurate CMYK to RGB conversion. " +
"Colors may look incorrect."
);
convert = new FastCMYKToRGB();
}
}
@@ -436,7 +468,7 @@ public class JPEGImageReader extends ImageReaderBase {
return image;
}
static JPEGColorSpace getSourceCSType(AdobeDCT adobeDCT, final SOF startOfFrame) throws IIOException {
static JPEGColorSpace getSourceCSType(AdobeDCTSegment adobeDCT, final SOFSegment startOfFrame) throws IIOException {
/*
ADAPTED from http://download.oracle.com/javase/6/docs/api/javax/imageio/metadata/doc-files/jpeg_metadata.html:
@@ -478,11 +510,11 @@ public class JPEGImageReader extends ImageReaderBase {
if (adobeDCT != null) {
switch (adobeDCT.getTransform()) {
case AdobeDCT.YCC:
case AdobeDCTSegment.YCC:
return JPEGColorSpace.YCbCr;
case AdobeDCT.YCCK:
case AdobeDCTSegment.YCCK:
return JPEGColorSpace.YCCK;
case AdobeDCT.Unknown:
case AdobeDCTSegment.Unknown:
if (startOfFrame.components.length == 1) {
return JPEGColorSpace.Gray;
}
@@ -601,14 +633,24 @@ public class JPEGImageReader extends ImageReaderBase {
segments = JPEGSegmentUtil.readSegments(imageInput, SEGMENT_IDENTIFIERS);
}
catch (IOException ignore) {
catch (IIOException ignore) {
if (DEBUG) {
ignore.printStackTrace();
}
}
catch (IllegalArgumentException foo) {
foo.printStackTrace();
if (DEBUG) {
foo.printStackTrace();
}
}
finally {
imageInput.reset();
}
// In case of an exception, avoid NPE when referencing segments later
if (segments == null) {
segments = Collections.emptyList();
}
}
private List<JPEGSegment> getAppSegments(final int marker, final String identifier) throws IOException {
@@ -629,7 +671,7 @@ public class JPEGImageReader extends ImageReaderBase {
return appSegments;
}
private SOF getSOF() throws IOException {
private 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 ||
@@ -654,7 +696,7 @@ public class JPEGImageReader extends ImageReaderBase {
components[i] = new SOFComponent(id, ((sub & 0xF0) >> 4), (sub & 0xF), qtSel);
}
return new SOF(segment.marker(), samplePrecision, lines, samplesPerLine, componentsInFrame, components);
return new SOFSegment(segment.marker(), samplePrecision, lines, samplesPerLine, components);
}
finally {
data.close();
@@ -665,7 +707,7 @@ public class JPEGImageReader extends ImageReaderBase {
return null;
}
private AdobeDCT getAdobeDCT() throws IOException {
private AdobeDCTSegment getAdobeDCT() throws IOException {
// TODO: Investigate http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6355567: 33/35 byte Adobe APP14 markers
List<JPEGSegment> adobe = getAppSegments(JPEG.APP14, "Adobe");
@@ -673,7 +715,7 @@ public class JPEGImageReader extends ImageReaderBase {
// version (byte), flags (4bytes), color transform (byte: 0=unknown, 1=YCC, 2=YCCK)
DataInputStream stream = new DataInputStream(adobe.get(0).data());
return new AdobeDCT(
return new AdobeDCTSegment(
stream.readUnsignedByte(),
stream.readUnsignedShort(),
stream.readUnsignedShort(),
@@ -717,10 +759,14 @@ public class JPEGImageReader extends ImageReaderBase {
return data;
}
private ICC_Profile getEmbeddedICCProfile() throws IOException {
private 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)
// TODO: Allow metadata to contain the wrongly indexed profiles, if readable
// NOTE: We ignore any profile with wrong index for reading and image types, just to be on the safe side
List<JPEGSegment> segments = getAppSegments(JPEG.APP2, "ICC_PROFILE");
if (segments.size() == 1) {
@@ -731,7 +777,8 @@ public class JPEGImageReader extends ImageReaderBase {
int chunkCount = stream.readUnsignedByte();
if (chunkNumber != 1 && chunkCount != 1) {
processWarningOccurred(String.format("Bad number of 'ICC_PROFILE' chunks: %d of %d. Assuming single chunk.", chunkNumber, chunkCount));
processWarningOccurred(String.format("Unexpected number of 'ICC_PROFILE' chunks: %d of %d. Ignoring ICC profile.", chunkNumber, chunkCount));
return null;
}
return readICCProfileSafe(stream);
@@ -742,19 +789,27 @@ public class JPEGImageReader extends ImageReaderBase {
int chunkNumber = stream.readUnsignedByte();
int chunkCount = stream.readUnsignedByte();
// TODO: Most of the time the ICC profiles are readable and should be obtainable from metadata...
boolean badICC = false;
if (chunkCount != segments.size()) {
// Some weird JPEGs use 0-based indexes... count == 0 and all numbers == 0.
// Others use count == 1, and all numbers == 1.
// Handle these by issuing warning
processWarningOccurred(String.format("Bad 'ICC_PROFILE' chunk count: %d. Ignoring ICC profile.", chunkCount));
badICC = true;
processWarningOccurred(String.format("Unexpected 'ICC_PROFILE' chunk count: %d. Ignoring count, assuming %d chunks in sequence.", chunkCount, segments.size()));
if (!allowBadIndexes) {
return null;
}
}
if (!badICC && chunkNumber < 1) {
// Anything else is just ignored
processWarningOccurred(String.format("Invalid 'ICC_PROFILE' chunk index: %d. Ignoring ICC profile.", chunkNumber));
return null;
if (!allowBadIndexes) {
return null;
}
}
int count = badICC ? segments.size() : chunkCount;
@@ -910,6 +965,51 @@ 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);
// IIOMetadataNode tree = (IIOMetadataNode) metadata.getAsTree(metadata.getNativeMetadataFormatName());
// Node jpegVariety = tree.getElementsByTagName("JPEGvariety").item(0);
// 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
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;
}
@Override
public IIOMetadata getStreamMetadata() throws IOException {
return delegate.getStreamMetadata();
}
private static void invertCMYK(final Raster raster) {
byte[] data = ((DataBufferByte) raster.getDataBuffer()).getData();
@@ -1135,73 +1235,6 @@ public class JPEGImageReader extends ImageReaderBase {
}
}
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;
final SOFComponent[] components;
public SOF(int marker, int samplePrecision, int lines, int samplesPerLine, int componentsInFrame, SOFComponent[] components) {
this.marker = marker;
this.samplePrecision = samplePrecision;
this.lines = lines;
this.samplesPerLine = samplesPerLine;
this.componentsInFrame = componentsInFrame;
this.components = components;
}
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%d[%04x, precision: %d, lines: %d, samples/line: %d, components: %s]",
marker & 0xf, marker, samplePrecision, lines, samplesPerLine, Arrays.toString(components)
);
}
}
private static class SOFComponent {
final int id;
final int hSub;
final int vSub;
final int qtSel;
public SOFComponent(int id, int hSub, int vSub, int qtSel) {
this.id = id;
this.hSub = hSub;
this.vSub = vSub;
this.qtSel = qtSel;
}
@Override
public String toString() {
// Use id either as component number or component name, based on value
Serializable idStr = (id >= 'a' && id <= 'z' || id >= 'A' && id <= 'Z') ? "'" + (char) id + "'" : id;
return String.format("id: %s, sub: %d/%d, sel: %d", idStr, hSub, vSub, qtSel);
}
}
protected static void showIt(final BufferedImage pImage, final String pTitle) {
ImageReaderBase.showIt(pImage, pTitle);
}
@@ -1272,7 +1305,7 @@ public class JPEGImageReader extends ImageReaderBase {
// param.setSourceSubsampling(sub, sub, 0, 0);
// }
long start = System.currentTimeMillis();
// long start = System.currentTimeMillis();
BufferedImage image = reader.read(0, param);
// System.err.println("Read time: " + (System.currentTimeMillis() - start) + " ms");
// System.err.println("image: " + image);
@@ -1280,12 +1313,12 @@ public class JPEGImageReader extends ImageReaderBase {
// image = new ResampleOp(reader.getWidth(0) / 4, reader.getHeight(0) / 4, ResampleOp.FILTER_LANCZOS).filter(image, null);
// int maxW = 1280;
// int maxH = 800;
int maxW = 400;
int maxH = 400;
int maxW = 1280;
int maxH = 800;
// int maxW = 400;
// int maxH = 400;
if (image.getWidth() > maxW || image.getHeight() > maxH) {
start = System.currentTimeMillis();
// start = System.currentTimeMillis();
float aspect = reader.getAspectRatio(0);
if (aspect >= 1f) {
image = ImageUtil.createResampled(image, maxW, Math.round(maxW / aspect), Image.SCALE_DEFAULT);
@@ -49,8 +49,9 @@ import static com.twelvemonkeys.lang.Validate.notNull;
*/
final class JPEGSegmentImageInputStream extends ImageInputStreamImpl {
// TODO: Rewrite JPEGSegment (from metadata) to store stream pos/length, and be able to replay data, and use instead of Segment?
// TODO: Change order of segments, to make sure APP0/JFIF is always before APP14/Adobe?
// TODO: Change order of segments, to make sure APP0/JFIF is always before APP14/Adobe? What about EXIF?
// TODO: Insert fake APP0/JFIF if needed by the reader?
// TODO: Sort out ICC_PROFILE issues (duplicate sequence numbers etc)?
final private ImageInputStream stream;
@@ -90,6 +91,12 @@ final class JPEGSegmentImageInputStream extends ImageInputStreamImpl {
long realPosition = stream.getStreamPosition();
int marker = stream.readUnsignedShort();
// Skip over 0xff padding between markers
while (marker == 0xffff) {
realPosition++;
marker = 0xff00 | stream.readUnsignedByte();
}
// 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) {
int length = stream.readUnsignedShort(); // Length including length field itself
@@ -149,7 +156,7 @@ final class JPEGSegmentImageInputStream extends ImageInputStreamImpl {
return segment;
}
private static boolean isAppSegmentWithId(String segmentId, ImageInputStream stream) throws IOException {
private static boolean isAppSegmentWithId(final String segmentId, final ImageInputStream stream) throws IOException {
notNull(segmentId, "segmentId");
stream.mark();
@@ -222,7 +229,7 @@ final class JPEGSegmentImageInputStream extends ImageInputStreamImpl {
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
public int read(final byte[] b, final int off, final int len) throws IOException {
bitOffset = 0;
// NOTE: There is a bug in the JPEGMetadata constructor (JPEGBuffer.loadBuf() method) that expects read to
@@ -264,7 +271,7 @@ final class JPEGSegmentImageInputStream extends ImageInputStreamImpl {
final long start;
final long length;
Segment(int marker, long realStart, long start, long length) {
Segment(final int marker, final long realStart, final long start, final long length) {
this.marker = marker;
this.realStart = realStart;
this.start = start;
@@ -0,0 +1,59 @@
/*
* Copyright (c) 2013, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name "TwelveMonkeys" nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.imageio.plugins.jpeg;
import java.io.Serializable;
/**
* SOFComponent
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: SOFComponent.java,v 1.0 22.04.13 16:40 haraldk Exp$
*/
final class SOFComponent {
final int id;
final int hSub;
final int vSub;
final int qtSel;
SOFComponent(int id, int hSub, int vSub, int qtSel) {
this.id = id;
this.hSub = hSub;
this.vSub = vSub;
this.qtSel = qtSel;
}
@Override
public String toString() {
// Use id either as component number or component name, based on value
Serializable idStr = (id >= 'a' && id <= 'z' || id >= 'A' && id <= 'Z') ? "'" + (char) id + "'" : id;
return String.format("id: %s, sub: %d/%d, sel: %d", idStr, hSub, vSub, qtSel);
}
}
@@ -0,0 +1,66 @@
/*
* Copyright (c) 2013, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name "TwelveMonkeys" nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.imageio.plugins.jpeg;
import java.util.Arrays;
/**
* SOFSegment
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: SOFSegment.java,v 1.0 22.04.13 16:40 haraldk Exp$
*/
final class SOFSegment {
final int marker;
final int samplePrecision;
final int lines; // height
final int samplesPerLine; // width
final SOFComponent[] components;
SOFSegment(int marker, int samplePrecision, int lines, int samplesPerLine, SOFComponent[] components) {
this.marker = marker;
this.samplePrecision = samplePrecision;
this.lines = lines;
this.samplesPerLine = samplesPerLine;
this.components = components;
}
final int componentsInFrame() {
return components.length;
}
@Override
public String toString() {
return String.format(
"SOF%d[%04x, precision: %d, lines: %d, samples/line: %d, components: %s]",
marker & 0xff - 0xc0, marker, samplePrecision, lines, samplesPerLine, Arrays.toString(components)
);
}
}
@@ -31,10 +31,12 @@ package com.twelvemonkeys.imageio.plugins.jpeg;
import com.twelvemonkeys.imageio.util.ImageReaderAbstractTestCase;
import org.junit.Test;
import javax.imageio.IIOException;
import javax.imageio.ImageIO;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.event.IIOReadWarningListener;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.spi.IIORegistry;
import javax.imageio.spi.ImageReaderSpi;
import java.awt.*;
@@ -75,7 +77,8 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTestCase<JPEGImageRe
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)),
new TestData(getClassLoaderResource("/jpeg/jfif-jfxx-thumbnail-olympus-d320l.jpg"), new Dimension(640, 480))
new TestData(getClassLoaderResource("/jpeg/jfif-jfxx-thumbnail-olympus-d320l.jpg"), new Dimension(640, 480)),
new TestData(getClassLoaderResource("/jpeg/jfif-padded-segments.jpg"), new Dimension(20, 45))
);
// More test data in specific tests below
@@ -255,7 +258,7 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTestCase<JPEGImageRe
assertEquals(16, image.getHeight());
// TODO: Need to test colors!
assertTrue(reader.hasThumbnails(0)); // Should not blow up!
}
@@ -371,7 +374,7 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTestCase<JPEGImageRe
// JFIF with JFXX JPEG encoded thumbnail
JPEGImageReader reader = createReader();
reader.setInput(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/jfif-jfxx-thumbnail-olympus-d320l.jpg")));
assertTrue(reader.hasThumbnails(0));
assertEquals(1, reader.getNumThumbnails(0));
assertEquals(80, reader.getThumbnailWidth(0, 0));
@@ -457,8 +460,8 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTestCase<JPEGImageRe
for (int i = 0; i < strip.getWidth() / 128; i++) {
int actualRGB = strip.getRGB(i * 128, 4);
assertEquals((actualRGB >> 16) & 0xff, (expectedRGB[i] >> 16) & 0xff, 5);
assertEquals((actualRGB >> 8) & 0xff, (expectedRGB[i] >> 8) & 0xff, 5);
assertEquals((actualRGB) & 0xff, (expectedRGB[i]) & 0xff, 5);
assertEquals((actualRGB >> 8) & 0xff, (expectedRGB[i] >> 8) & 0xff, 5);
assertEquals((actualRGB) & 0xff, (expectedRGB[i]) & 0xff, 5);
}
}
@@ -487,8 +490,8 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTestCase<JPEGImageRe
for (int i = 0; i < thumbnail.getWidth() / 8; i++) {
int actualRGB = thumbnail.getRGB(i * 8, 4);
assertEquals((actualRGB >> 16) & 0xff, (expectedRGB[i] >> 16) & 0xff, 5);
assertEquals((actualRGB >> 8) & 0xff, (expectedRGB[i] >> 8) & 0xff, 5);
assertEquals((actualRGB) & 0xff, (expectedRGB[i]) & 0xff, 5);
assertEquals((actualRGB >> 8) & 0xff, (expectedRGB[i] >> 8) & 0xff, 5);
assertEquals((actualRGB) & 0xff, (expectedRGB[i]) & 0xff, 5);
}
}
@@ -549,7 +552,6 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTestCase<JPEGImageRe
new TestData(getClassLoaderResource("/jpeg/cmyk-sample-no-icc.jpg"), new Dimension(100, 100))
);
for (TestData data : cmykData) {
reader.setInput(data.getInputStream());
@@ -599,4 +601,24 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTestCase<JPEGImageRe
}
// TODO: Test RGBA/YCbCrA handling
@Test
public void testReadMetadataMaybeNull() throws IOException {
// Just test that we can read the metadata without exceptions
JPEGImageReader reader = createReader();
for (TestData testData : getTestData()) {
reader.setInput(testData.getInputStream());
for (int i = 0; i < reader.getNumImages(true); i++) {
try {
IIOMetadata metadata = reader.getImageMetadata(i);
assertNotNull(String.format("Image metadata null for %s image %s", testData, i), metadata);
}
catch (IIOException e) {
System.err.println(String.format("WARNING: Reading metadata failed for %s image %s: %s", testData, i, e.getMessage()));
}
}
}
}
}
@@ -135,4 +135,27 @@ public class JPEGSegmentImageInputStreamTest {
assertEquals(9299l, length); // Sanity check: same as file size
}
@Test
public void testReadPaddedSegmentsBug() throws IOException {
ImageInputStream stream = new JPEGSegmentImageInputStream(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/jfif-padded-segments.jpg")));
List<JPEGSegment> appSegments = JPEGSegmentUtil.readSegments(stream, JPEGSegmentUtil.APP_SEGMENTS);
assertEquals(2, appSegments.size());
assertEquals(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());
stream.seek(0l);
long length = 0;
while (stream.read() != -1) {
length++;
}
assertEquals(1079L, length); // Sanity check: same as file size, except padding and the filtered ICC_PROFILE segment
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

+25
View File
@@ -0,0 +1,25 @@
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.
@@ -35,6 +35,7 @@ package com.twelvemonkeys.imageio.metadata.exif;
* @author last modified by $Author: haraldk$
* @version $Id: EXIF.java,v 1.0 Nov 11, 2009 5:36:04 PM haraldk Exp$
*/
@SuppressWarnings("UnusedDeclaration")
public interface EXIF {
// See http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html
int TAG_EXPOSURE_TIME = 33434;
@@ -88,7 +88,7 @@ final class EXIFEntry extends AbstractEntry {
return "StripOffsets";
case TIFF.TAG_ORIENTATION:
return "Orientation";
case TIFF.TAG_SAMPLES_PER_PIXELS:
case TIFF.TAG_SAMPLES_PER_PIXEL:
return "SamplesPerPixels";
case TIFF.TAG_ROWS_PER_STRIP:
return "RowsPerStrip";
@@ -209,6 +209,28 @@ final class EXIFEntry extends AbstractEntry {
return "PixelYDimension";
// TODO: More field names
/*
default:
Class[] classes = new Class[] {TIFF.class, EXIF.class};
for (Class cl : classes) {
Field[] fields = cl.getFields();
for (Field field : fields) {
try {
if (field.getType() == Integer.TYPE && field.getName().startsWith("TAG_")) {
if (field.get(null).equals(getIdentifier())) {
return StringUtil.lispToCamel(field.getName().substring(4).replace("_", "-").toLowerCase(), true);
}
}
}
catch (IllegalAccessException e) {
// Should never happen, but in case, abort
break;
}
}
}
*/
}
return null;
@@ -35,6 +35,7 @@ package com.twelvemonkeys.imageio.metadata.exif;
* @author last modified by $Author: haraldk$
* @version $Id: TIFF.java,v 1.0 Nov 15, 2009 3:02:24 PM haraldk Exp$
*/
@SuppressWarnings("UnusedDeclaration")
public interface TIFF {
int TIFF_MAGIC = 42;
@@ -98,8 +99,9 @@ public interface TIFF {
int TAG_BITS_PER_SAMPLE = 258;
int TAG_COMPRESSION = 259;
int TAG_PHOTOMETRIC_INTERPRETATION = 262;
int TAG_FILL_ORDER = 266;
int TAG_ORIENTATION = 274;
int TAG_SAMPLES_PER_PIXELS = 277;
int TAG_SAMPLES_PER_PIXEL = 277;
int TAG_PLANAR_CONFIGURATION = 284;
int TAG_SAMPLE_FORMAT = 339;
int TAG_YCBCR_SUB_SAMPLING = 530;
@@ -113,6 +115,7 @@ public interface TIFF {
int TAG_STRIP_OFFSETS = 273;
int TAG_ROWS_PER_STRIP = 278;
int TAG_STRIP_BYTE_COUNTS = 279;
// "Old-style" JPEG (still used as EXIF thumbnail)
int TAG_JPEG_INTERCHANGE_FORMAT = 513;
int TAG_JPEG_INTERCHANGE_FORMAT_LENGTH = 514;
@@ -124,6 +127,7 @@ public interface TIFF {
int TAG_PRIMARY_CHROMATICITIES = 319;
int TAG_COLOR_MAP = 320;
int TAG_EXTRA_SAMPLES = 338;
int TAG_TRANSFER_RANGE = 342;
int TAG_YCBCR_COEFFICIENTS = 529;
int TAG_REFERENCE_BLACK_WHITE = 532;
@@ -133,6 +137,7 @@ public interface TIFF {
int TAG_IMAGE_DESCRIPTION = 270;
int TAG_MAKE = 271;
int TAG_MODEL = 272;
int TAG_PAGE_NUMBER = 297;
int TAG_SOFTWARE = 305;
int TAG_ARTIST = 315;
int TAG_HOST_COMPUTER = 316;
@@ -161,5 +166,12 @@ public interface TIFF {
int TAG_TILE_OFFSETS = 324;
int TAG_TILE_BYTE_COUNTS = 325;
// JPEG
int TAG_JPEG_TABLES = 347;
// "Old-style" JPEG (Obsolete) DO NOT WRITE!
int TAG_OLD_JPEG_PROC = 512;
int TAG_OLD_JPEG_Q_TABLES = 519;
int TAG_OLD_JPEG_DC_TABLES = 520;
int TAG_OLD_JPEG_AC_TABLES = 521;
}
@@ -40,7 +40,8 @@ public interface JPEG {
int SOI = 0xFFD8;
/** End of Image segment marker (EOI). */
int EOI = 0xFFD9;
/** Start of Stream segment marker (SOS). */
/** Start of Scan segment marker (SOS). */
int SOS = 0xFFDA;
/** Define Quantization Tables segment marker (DQT). */
@@ -81,6 +82,10 @@ public interface JPEG {
int SOF14 = 0xFFCE;
int SOF15 = 0xFFCF;
// JPEG-LS markers
int SOF55 = 0xFFF7; // NOTE: Equal to a normal SOF segment
int LSE = 0xFFF8; // JPEG-LS Preset Parameter marker
// TODO: Known/Important APPn marker identifiers
// "JFIF" APP0
// "JFXX" APP0
@@ -89,6 +94,6 @@ public interface JPEG {
// "Adobe" APP14
// Possibly
// "http://ns.adobe.com/xap/1.0/" (XMP)
// "Photoshop 3.0" (Contains IPTC)
// "http://ns.adobe.com/xap/1.0/" (XMP) APP1
// "Photoshop 3.0" (may contain IPTC) APP13
}
@@ -90,10 +90,7 @@ public final class JPEGQuality {
private static int getJPEGQuality(final int[][] quantizationTables) throws IOException {
// System.err.println("tables: " + Arrays.deepToString(tables));
// TODO: Determine lossless JPEG
// if (lossless) {
// return 100; // TODO: Sums can be 100... Is lossless not 100?
// }
// TODO: Determine lossless JPEG, it's an entirely different algorithm
int qvalue;
@@ -95,7 +95,8 @@ public final class JPEGSegmentUtil {
JPEGSegment segment;
try {
while (!isImageDone(segment = readSegment(stream, segmentIdentifiers))) {
do {
segment = readSegment(stream, segmentIdentifiers);
// System.err.println("segment: " + segment);
if (isRequested(segment, segmentIdentifiers)) {
@@ -106,6 +107,7 @@ public final class JPEGSegmentUtil {
segments.add(segment);
}
}
while (!isImageDone(segment));
}
catch (EOFException ignore) {
// Just end here, in case of malformed stream
@@ -151,8 +153,18 @@ public final class JPEGSegmentUtil {
}
}
static JPEGSegment readSegment(final ImageInputStream stream, Map<Integer, List<String>> segmentIdentifiers) throws IOException {
static JPEGSegment readSegment(final ImageInputStream stream, final Map<Integer, List<String>> segmentIdentifiers) throws IOException {
int marker = stream.readUnsignedShort();
// Skip over 0xff padding between markers
while (marker == 0xffff) {
marker = 0xff00 | stream.readUnsignedByte();
}
if ((marker >> 8 & 0xff) != 0xff) {
throw new IIOException(String.format("Bad marker: %04x", marker));
}
int length = stream.readUnsignedShort(); // Length including length field itself
byte[] data;
@@ -191,7 +203,7 @@ public final class JPEGSegmentUtil {
}
@Override
public boolean contains(Object o) {
public boolean contains(final Object o) {
return true;
}
}
@@ -203,13 +215,13 @@ public final class JPEGSegmentUtil {
}
@Override
public List<String> get(Object key) {
public List<String> get(final Object key) {
return key instanceof Integer && JPEGSegment.isAppSegmentMarker((Integer) key) ? ALL_IDS : null;
}
@Override
public boolean containsKey(Object key) {
public boolean containsKey(final Object key) {
return true;
}
}
@@ -221,7 +233,7 @@ public final class JPEGSegmentUtil {
}
@Override
public List<String> get(Object key) {
public List<String> get(final Object key) {
return containsKey(key) ? ALL_IDS : null;
}
@@ -151,19 +151,21 @@ public class JPEGSegmentUtilTest {
@Test
public void testReadAll() throws IOException {
List<JPEGSegment> segments = JPEGSegmentUtil.readSegments(getData("/jpeg/9788245605525.jpg"), JPEGSegmentUtil.ALL_SEGMENTS);
assertEquals(6, segments.size());
assertEquals(7, segments.size());
assertEquals(segments.toString(), JPEG.SOF0, segments.get(3).marker());
assertEquals(segments.toString(), null, segments.get(3).identifier());
assertEquals(segments.toString(), JPEG.SOS, segments.get(segments.size() - 1).marker());
}
@Test
public void testReadAllAlt() throws IOException {
List<JPEGSegment> segments = JPEGSegmentUtil.readSegments(getData("/jpeg/ts_open_300dpi.jpg"), JPEGSegmentUtil.ALL_SEGMENTS);
assertEquals(26, segments.size());
assertEquals(27, segments.size());
assertEquals(segments.toString(), JPEG.SOF0, segments.get(23).marker());
assertEquals(segments.toString(), null, segments.get(23).identifier());
assertEquals(segments.toString(), JPEG.SOS, segments.get(segments.size() - 1).marker());
}
@Test
@@ -194,4 +196,17 @@ public class JPEGSegmentUtilTest {
assertEquals(JPEG.APP14, segments.get(21).marker());
assertEquals("Adobe", segments.get(21).identifier());
}
@Test
public void testReadPaddedSegments() throws IOException {
List<JPEGSegment> segments = JPEGSegmentUtil.readSegments(getData("/jpeg/jfif-padded-segments.jpg"), JPEGSegmentUtil.APP_SEGMENTS);
assertEquals(3, segments.size());
assertEquals(JPEG.APP0, segments.get(0).marker());
assertEquals("JFIF", segments.get(0).identifier());
assertEquals(JPEG.APP2, segments.get(1).marker());
assertEquals("ICC_PROFILE", segments.get(1).identifier());
assertEquals(JPEG.APP1, segments.get(2).marker());
assertEquals("Exif", segments.get(2).identifier());
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

+25
View File
@@ -0,0 +1,25 @@
Copyright (c) 2013, Harald Kuhr
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name "TwelveMonkeys" nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,452 @@
/*
* Copyright (c) 2012, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name "TwelveMonkeys" nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.imageio.plugins.tiff;
import com.twelvemonkeys.lang.Validate;
import java.io.EOFException;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* CCITT Modified Huffman RLE, Group 3 (T4) and Group 4 (T6) fax compression.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: CCITTFaxDecoderStream.java,v 1.0 23.05.12 15:55 haraldk Exp$
*/
final class CCITTFaxDecoderStream extends FilterInputStream {
// See TIFF 6.0 Specification, Section 10: "Modified Huffman Compression", page 43.
private final int columns;
private final byte[] decodedRow;
private int decodedLength;
private int decodedPos;
private int bitBuffer;
private int bitBufferLength;
// Need to take fill order into account (?) (use flip table?)
private final int fillOrder;
private final int type;
private final int[] changes;
private int changesCount;
private static final int EOL_CODE = 0x01; // 12 bit
public CCITTFaxDecoderStream(final InputStream stream, final int columns, final int type, final int fillOrder) {
super(Validate.notNull(stream, "stream"));
this.columns = Validate.isTrue(columns > 0, columns, "width must be greater than 0");
// We know this is only used for b/w (1 bit)
this.decodedRow = new byte[(columns + 7) / 8];
this.type = Validate.isTrue(type == TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE, type, "Only CCITT Modified Huffman RLE compression (2) supported: %s"); // TODO: Implement group 3 and 4
this.fillOrder = Validate.isTrue(fillOrder == 1, fillOrder, "Only fill order 1 supported: %s"); // TODO: Implement fillOrder == 2
this.changes = new int[columns];
}
// IDEA: Would it be faster to keep all bit combos of each length (>=2) that is NOT a code, to find bit length, then look up value in table?
// -- If white run, start at 4 bits to determine length, if black, start at 2 bits
private void fetch() throws IOException {
if (decodedPos >= decodedLength) {
decodedLength = 0;
try {
decodeRow();
}
catch (EOFException e) {
// TODO: Rewrite to avoid throw/catch for normal flow...
if (decodedLength != 0) {
throw e;
}
// ..otherwise, just client code trying to read past the end of stream
decodedLength = -1;
}
decodedPos = 0;
}
}
private void decodeRow() throws IOException {
resetBuffer();
boolean literalRun = true;
/*
if (type == TIFFExtension.COMPRESSION_CCITT_T4) {
int eol = readBits(12);
System.err.println("eol: " + eol);
while (eol != EOL_CODE) {
eol = readBits(1);
System.err.println("eol: " + eol);
// throw new IOException("Missing EOL");
}
literalRun = readBits(1) == 1;
}
System.err.println("literalRun: " + literalRun);
*/
int index = 0;
if (literalRun) {
changesCount = 0;
boolean white = true;
do {
int completeRun = 0;
int run;
do {
if (white) {
run = decodeRun(WHITE_CODES, WHITE_RUN_LENGTHS, 4);
}
else {
run = decodeRun(BLACK_CODES, BLACK_RUN_LENGTHS, 2);
}
completeRun += run;
}
while (run >= 64); // Additional makeup codes are packed into both b/w codes, terminating codes are < 64 bytes
changes[changesCount++] = index + completeRun;
// System.err.printf("%s run: %d\n", white ? "white" : "black", run);
// TODO: Optimize with lookup for 0-7 bits?
// Fill bits to byte boundary...
while (index % 8 != 0 && completeRun-- > 0) {
decodedRow[index++ / 8] |= (white ? 1 << 8 - (index % 8) : 0);
}
// ...then fill complete bytes to either 0xff or 0x00...
if (index % 8 == 0) {
final byte value = (byte) (white ? 0xff : 0x00);
while (completeRun > 7) {
decodedRow[index / 8] = value;
completeRun -= 8;
index += 8;
}
}
// ...finally fill any remaining bits
while (completeRun-- > 0) {
decodedRow[index++ / 8] |= (white ? 1 << 8 - (index % 8) : 0);
}
// Flip color for next run
white = !white;
}
while (index < columns);
}
else {
// non-literal run
}
if (type == TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE && index != columns) {
throw new IOException("Sum of run-lengths does not equal scan line width: " + index + " > " + columns);
}
decodedLength = (index / 8) + 1;
}
private int decodeRun(short[][] codes, short[][] runLengths, int minCodeSize) throws IOException {
// TODO: Optimize...
// Looping and comparing is the most straight-forward, but probably not the most effective way...
int code = readBits(minCodeSize);
for (int bits = 0; bits < codes.length; bits++) {
short[] bitCodes = codes[bits];
for (int i = 0; i < bitCodes.length; i++) {
if (bitCodes[i] == code) {
// System.err.println("code: " + code);
// Code found, return matching run length
return runLengths[bits][i];
}
}
// No code found, read one more bit and try again
code = fillOrder == 1 ? (code << 1) | readBits(1) : readBits(1) << (bits + minCodeSize) | code;
}
throw new IOException("Unknown code in Huffman RLE stream");
}
private void resetBuffer() {
for (int i = 0; i < decodedRow.length; i++) {
decodedRow[i] = 0;
}
bitBuffer = 0;
bitBufferLength = 0;
}
private int readBits(int bitCount) throws IOException {
while (bitBufferLength < bitCount) {
int read = in.read();
if (read == -1) {
throw new EOFException("Unexpected end of Huffman RLE stream");
}
int bits = read & 0xff;
bitBuffer = (bitBuffer << 8) | bits;
bitBufferLength += 8;
}
// TODO: Take fill order into account
bitBufferLength -= bitCount;
int result = bitBuffer >> bitBufferLength;
bitBuffer &= (1 << bitBufferLength) - 1;
return result;
}
@Override
public int read() throws IOException {
if (decodedLength < 0) {
return -1;
}
if (decodedPos >= decodedLength) {
fetch();
if (decodedLength < 0) {
return -1;
}
}
return decodedRow[decodedPos++] & 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) {
return -1;
}
}
int read = Math.min(decodedLength - decodedPos, len);
System.arraycopy(decodedRow, decodedPos, b, off, read);
decodedPos += read;
return read;
}
@Override
public long skip(long n) throws IOException {
if (decodedLength < 0) {
return -1;
}
if (decodedPos >= decodedLength) {
fetch();
if (decodedLength < 0) {
return -1;
}
}
int skipped = (int) Math.min(decodedLength - decodedPos, n);
decodedPos += skipped;
return skipped;
}
@Override
public boolean markSupported() {
return false;
}
@Override
public synchronized void reset() throws IOException {
throw new IOException("mark/reset not supported");
}
static final short[][] BLACK_CODES = {
{ // 2 bits
0x2, 0x3,
},
{ // 3 bits
0x2, 0x3,
},
{ // 4 bits
0x2, 0x3,
},
{ // 5 bits
0x3,
},
{ // 6 bits
0x4, 0x5,
},
{ // 7 bits
0x4, 0x5, 0x7,
},
{ // 8 bits
0x4, 0x7,
},
{ // 9 bits
0x18,
},
{ // 10 bits
0x17, 0x18, 0x37, 0x8, 0xf,
},
{ // 11 bits
0x17, 0x18, 0x28, 0x37, 0x67, 0x68, 0x6c, 0x8, 0xc, 0xd,
},
{ // 12 bits
0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x1c, 0x1d, 0x1e, 0x1f, 0x24, 0x27, 0x28, 0x2b, 0x2c, 0x33,
0x34, 0x35, 0x37, 0x38, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x5b, 0x64, 0x65,
0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0xc8, 0xc9, 0xca, 0xcb, 0xcc, 0xcd, 0xd2, 0xd3,
0xd4, 0xd5, 0xd6, 0xd7, 0xda, 0xdb,
},
{ // 13 bits
0x4a, 0x4b, 0x4c, 0x4d, 0x52, 0x53, 0x54, 0x55, 0x5a, 0x5b, 0x64, 0x65, 0x6c, 0x6d, 0x72, 0x73,
0x74, 0x75, 0x76, 0x77,
}
};
static final short[][] BLACK_RUN_LENGTHS = {
{ // 2 bits
3, 2,
},
{ // 3 bits
1, 4,
},
{ // 4 bits
6, 5,
},
{ // 5 bits
7,
},
{ // 6 bits
9, 8,
},
{ // 7 bits
10, 11, 12,
},
{ // 8 bits
13, 14,
},
{ // 9 bits
15,
},
{ // 10 bits
16, 17, 0, 18, 64,
},
{ // 11 bits
24, 25, 23, 22, 19, 20, 21, 1792, 1856, 1920,
},
{ // 12 bits
1984, 2048, 2112, 2176, 2240, 2304, 2368, 2432, 2496, 2560, 52, 55, 56, 59, 60, 320,
384, 448, 53, 54, 50, 51, 44, 45, 46, 47, 57, 58, 61, 256, 48, 49,
62, 63, 30, 31, 32, 33, 40, 41, 128, 192, 26, 27, 28, 29, 34, 35,
36, 37, 38, 39, 42, 43,
},
{ // 13 bits
640, 704, 768, 832, 1280, 1344, 1408, 1472, 1536, 1600, 1664, 1728, 512, 576, 896, 960,
1024, 1088, 1152, 1216,
}
};
public static final short[][] WHITE_CODES = {
{ // 4 bits
0x7, 0x8, 0xb, 0xc, 0xe, 0xf,
},
{ // 5 bits
0x12, 0x13, 0x14, 0x1b, 0x7, 0x8,
},
{ // 6 bits
0x17, 0x18, 0x2a, 0x2b, 0x3, 0x34, 0x35, 0x7, 0x8,
},
{ // 7 bits
0x13, 0x17, 0x18, 0x24, 0x27, 0x28, 0x2b, 0x3, 0x37, 0x4, 0x8, 0xc,
},
{ // 8 bits
0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x1a, 0x1b, 0x2, 0x24, 0x25, 0x28, 0x29, 0x2a, 0x2b, 0x2c,
0x2d, 0x3, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x4, 0x4a, 0x4b, 0x5, 0x52, 0x53, 0x54, 0x55,
0x58, 0x59, 0x5a, 0x5b, 0x64, 0x65, 0x67, 0x68, 0xa, 0xb,
},
{ // 9 bits
0x98, 0x99, 0x9a, 0x9b, 0xcc, 0xcd, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xdb,
},
{ // 10 bits
},
{ // 11 bits
0x8, 0xc, 0xd,
},
{ // 12 bits
0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x1c, 0x1d, 0x1e, 0x1f,
}
};
public static final short[][] WHITE_RUN_LENGTHS = {
{ // 4 bits
2, 3, 4, 5, 6, 7,
},
{ // 5 bits
128, 8, 9, 64, 10, 11,
},
{ // 6 bits
192, 1664, 16, 17, 13, 14, 15, 1, 12,
},
{ // 7 bits
26, 21, 28, 27, 18, 24, 25, 22, 256, 23, 20, 19,
},
{ // 8 bits
33, 34, 35, 36, 37, 38, 31, 32, 29, 53, 54, 39, 40, 41, 42, 43,
44, 30, 61, 62, 63, 0, 320, 384, 45, 59, 60, 46, 49, 50, 51,
52, 55, 56, 57, 58, 448, 512, 640, 576, 47, 48,
},
{ // 9 bits
1472, 1536, 1600, 1728, 704, 768, 832, 896, 960, 1024, 1088, 1152, 1216, 1280, 1344, 1408,
},
{ // 10 bits
},
{ // 11 bits
1792, 1856, 1920,
},
{ // 12 bits
1984, 2048, 2112, 2176, 2240, 2304, 2368, 2432, 2496, 2560,
}
};
}
@@ -0,0 +1,340 @@
/*
* Copyright (c) 2013, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name "TwelveMonkeys" nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.imageio.plugins.tiff;
import com.twelvemonkeys.lang.Validate;
import java.io.EOFException;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteOrder;
/**
* A decoder for data converted using "horizontal differencing predictor".
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @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 {
// 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;
public HorizontalDeDifferencingStream(final InputStream stream, final int columns, final int samplesPerPixel, final int bitsPerSample, final ByteOrder byteOrder) {
super(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];
}
private boolean isValidBPS(final int bitsPerSample) {
switch (bitsPerSample) {
case 1:
case 2:
case 4:
case 8:
case 16:
case 32:
case 64:
return true;
default:
return false;
}
}
private void fetch() throws IOException {
int pos = 0;
int read;
// 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;
}
if (pos > 0) {
if (buffer.length > pos) {
throw new EOFException("Unexpected end of stream");
}
decodeRow();
decodedLength = buffer.length;
decodedPos = 0;
}
else {
decodedLength = -1;
}
}
private void decodeRow() throws EOFException {
// Un-apply horizontal predictor
int sample = 0;
byte temp;
switch (bitsPerSample) {
case 1:
for (int b = 0; b < (columns + 7) / 8; b++) {
sample += (buffer[b] >> 7) & 0x1;
temp = (byte) ((sample << 7) & 0x80);
sample += (buffer[b] >> 6) & 0x1;
temp |= (byte) ((sample << 6) & 0x40);
sample += (buffer[b] >> 5) & 0x1;
temp |= (byte) ((sample << 5) & 0x20);
sample += (buffer[b] >> 4) & 0x1;
temp |= (byte) ((sample << 4) & 0x10);
sample += (buffer[b] >> 3) & 0x1;
temp |= (byte) ((sample << 3) & 0x08);
sample += (buffer[b] >> 2) & 0x1;
temp |= (byte) ((sample << 2) & 0x04);
sample += (buffer[b] >> 1) & 0x1;
temp |= (byte) ((sample << 1) & 0x02);
sample += buffer[b] & 0x1;
buffer[b] = (byte) (temp | sample & 0x1);
}
break;
case 2:
for (int b = 0; b < (columns + 3) / 4; b++) {
sample += (buffer[b] >> 6) & 0x3;
temp = (byte) ((sample << 6) & 0xc0);
sample += (buffer[b] >> 4) & 0x3;
temp |= (byte) ((sample << 4) & 0x30);
sample += (buffer[b] >> 2) & 0x3;
temp |= (byte) ((sample << 2) & 0x0c);
sample += buffer[b] & 0x3;
buffer[b] = (byte) (temp | sample & 0x3);
}
break;
case 4:
for (int b = 0; b < (columns + 1) / 2; b++) {
sample += (buffer[b] >> 4) & 0xf;
temp = (byte) ((sample << 4) & 0xf0);
sample += buffer[b] & 0x0f;
buffer[b] = (byte) (temp | sample & 0xf);
}
break;
case 8:
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]);
}
}
break;
case 16:
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));
}
}
break;
case 32:
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));
}
}
break;
case 64:
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));
}
}
break;
default:
throw new AssertionError(String.format("Unsupported bits per sample value: %d", bitsPerSample));
}
}
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) {
return -1;
}
}
return buffer[decodedPos++] & 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) {
return -1;
}
}
int read = Math.min(decodedLength - decodedPos, len);
System.arraycopy(buffer, decodedPos, b, off, read);
decodedPos += read;
return read;
}
@Override
public long skip(long n) throws IOException {
if (decodedLength < 0) {
return -1;
}
if (decodedPos >= decodedLength) {
fetch();
if (decodedLength < 0) {
return -1;
}
}
int skipped = (int) Math.min(decodedLength - decodedPos, n);
decodedPos += skipped;
return skipped;
}
}
@@ -112,7 +112,7 @@ class JPEGTables {
// Read lengths as short array
short[] lengths = new short[DHT_LENGTH];
for (int i = 0, lengthsLength = lengths.length; i < lengthsLength; i++) {
for (int i = 0; i < DHT_LENGTH; i++) {
lengths[i] = (short) data.readUnsignedByte();
}
read += lengths.length;
@@ -33,16 +33,18 @@ import com.twelvemonkeys.io.enc.Decoder;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
/**
* LZWDecoder
* LempelZivWelch (LZW) decompression. LZW is a universal loss-less data compression algorithm
* created by Abraham Lempel, Jacob Ziv, and Terry Welch.
* Inspired by libTiff's LZW decompression.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: LZWDecoder.java,v 1.0 08.05.12 21:11 haraldk Exp$
* @see <a href="http://en.wikipedia.org/wiki/Lempel%E2%80%93Ziv%E2%80%93Welch">LZW (Wikipedia)</a>
*/
final class LZWDecoder implements Decoder {
abstract class LZWDecoder implements Decoder {
/** Clear: Re-initialize tables. */
static final int CLEAR_CODE = 256;
/** End of Information. */
@@ -51,44 +53,44 @@ final class LZWDecoder implements Decoder {
private static final int MIN_BITS = 9;
private static final int MAX_BITS = 12;
private final boolean reverseBitOrder;
private static final int TABLE_SIZE = 1 << MAX_BITS;
private int currentByte = -1;
private int bitPos;
private final boolean compatibilityMode;
// TODO: Consider speeding things up with a "string" type (instead of the inner byte[]),
// that uses variable size/dynamic allocation, to avoid the excessive array copying?
// private final byte[][] table = new byte[4096][0]; // libTiff adds another 1024 "for compatibility"...
private final byte[][] table = new byte[4096 + 1024][0]; // libTiff adds another 1024 "for compatibility"...
private final String[] table;
private int tableLength;
private int bitsPerCode;
int bitsPerCode;
private int oldCode = CLEAR_CODE;
private int maxCode;
int bitMask;
private int maxString;
private boolean eofReached;
boolean eofReached;
int nextData;
int nextBits;
LZWDecoder(final boolean reverseBitOrder) {
this.reverseBitOrder = reverseBitOrder;
protected LZWDecoder(final boolean compatibilityMode) {
this.compatibilityMode = compatibilityMode;
table = new String[compatibilityMode ? TABLE_SIZE + 1024 : TABLE_SIZE]; // libTiff adds 1024 "for compatibility"...
// First 258 entries of table is always fixed
for (int i = 0; i < 256; i++) {
table[i] = new byte[] {(byte) i};
table[i] = new String((byte) i);
}
init();
}
LZWDecoder() {
this(false);
}
private int maxCodeFor(final int bits) {
return reverseBitOrder ? (1 << bits) - 2 : (1 << bits) - 1;
private static int bitmaskFor(final int bits) {
return (1 << bits) - 1;
}
private void init() {
tableLength = 258;
bitsPerCode = MIN_BITS;
maxCode = maxCodeFor(bitsPerCode);
bitMask = bitmaskFor(bitsPerCode);
maxCode = maxCode();
maxString = 1;
}
@@ -107,25 +109,17 @@ final class LZWDecoder implements Decoder {
break;
}
bufferPos += writeString(table[code], buffer, bufferPos);
bufferPos += table[code].writeTo(buffer, bufferPos);
}
else {
if (code > tableLength + 1 || oldCode >= tableLength) {
// TODO: FixMe for old, borked streams
System.err.println("code: " + code);
System.err.println("oldCode: " + oldCode);
System.err.println("tableLength: " + tableLength);
throw new DecodeException("Corrupted LZW table");
}
if (isInTable(code)) {
bufferPos += writeString(table[code], buffer, bufferPos);
addStringToTable(concatenate(table[oldCode], table[code][0]));
bufferPos += table[code].writeTo(buffer, bufferPos);
addStringToTable(table[oldCode].concatenate(table[code].firstChar));
}
else {
byte[] outString = concatenate(table[oldCode], table[oldCode][0]);
String outString = table[oldCode].concatenate(table[oldCode].firstChar);
bufferPos += writeString(outString, buffer, bufferPos);
bufferPos += outString.writeTo(buffer, bufferPos);
addStringToTable(outString);
}
}
@@ -141,29 +135,23 @@ final class LZWDecoder implements Decoder {
return bufferPos;
}
private static byte[] concatenate(final byte[] string, final byte firstChar) {
byte[] result = Arrays.copyOf(string, string.length + 1);
result[string.length] = firstChar;
return result;
}
private void addStringToTable(final byte[] string) throws IOException {
private void addStringToTable(final String string) throws IOException {
table[tableLength++] = string;
if (tableLength >= maxCode) {
if (tableLength > maxCode) {
bitsPerCode++;
if (bitsPerCode > MAX_BITS) {
if (reverseBitOrder) {
if (compatibilityMode) {
bitsPerCode--;
}
else {
throw new DecodeException(String.format("TIFF LZW with more than %d bits per code encountered (table overflow)", MAX_BITS));
throw new DecodeException(java.lang.String.format("TIFF LZW with more than %d bits per code encountered (table overflow)", MAX_BITS));
}
}
maxCode = maxCodeFor(bitsPerCode);
bitMask = bitmaskFor(bitsPerCode);
maxCode = maxCode();
}
if (string.length > maxString) {
@@ -171,89 +159,14 @@ final class LZWDecoder implements Decoder {
}
}
private static int writeString(final byte[] string, final byte[] buffer, final int bufferPos) {
if (string.length == 0) {
return 0;
}
else if (string.length == 1) {
buffer[bufferPos] = string[0];
return 1;
}
else {
System.arraycopy(string, 0, buffer, bufferPos, string.length);
return string.length;
}
}
protected abstract int maxCode();
private boolean isInTable(int code) {
return code < tableLength;
}
private int getNextCode(final InputStream stream) throws IOException {
if (eofReached) {
return EOI_CODE;
}
protected abstract int getNextCode(final InputStream stream) throws IOException;
int bitsToFill = bitsPerCode;
int value = 0;
while (bitsToFill > 0) {
int nextBits;
if (bitPos == 0) {
nextBits = stream.read();
if (nextBits == -1) {
// This is really a bad stream, but should be safe to handle this way, rather than throwing an EOFException.
// An EOFException will be thrown by the decoder stream later, if further reading is attempted.
eofReached = true;
return EOI_CODE;
}
}
else {
nextBits = currentByte;
}
int bitsFromHere = 8 - bitPos;
if (bitsFromHere > bitsToFill) {
bitsFromHere = bitsToFill;
}
if (reverseBitOrder) {
// NOTE: This is a spec violation. However, libTiff reads such files.
// TIFF 6.0 Specification, Section 13: "LZW Compression"/"The Algorithm", page 61, says:
// "LZW compression codes are stored into bytes in high-to-low-order fashion, i.e., FillOrder
// is assumed to be 1. The compressed codes are written as bytes (not words) so that the
// compressed data will be identical whether it is an II or MM file."
// Fill bytes from right-to-left
for (int i = 0; i < bitsFromHere; i++) {
int destBitPos = bitsPerCode - bitsToFill + i;
int srcBitPos = bitPos + i;
value |= ((nextBits & (1 << srcBitPos)) >> srcBitPos) << destBitPos;
}
}
else {
value |= (nextBits >> 8 - bitPos - bitsFromHere & 0xff >> 8 - bitsFromHere) << bitsToFill - bitsFromHere;
}
bitsToFill -= bitsFromHere;
bitPos += bitsFromHere;
if (bitPos >= 8) {
bitPos = 0;
}
currentByte = nextBits;
}
if (value == EOI_CODE) {
eofReached = true;
}
return value;
}
static boolean isOldBitReversedStream(final InputStream stream) throws IOException {
stream.mark(2);
@@ -267,5 +180,147 @@ final class LZWDecoder implements Decoder {
stream.reset();
}
}
public static LZWDecoder create(boolean oldBitReversedStream) {
return oldBitReversedStream ? new LZWCompatibilityDecoder() : new LZWSpecDecoder();
}
private static final class LZWSpecDecoder extends LZWDecoder {
protected LZWSpecDecoder() {
super(false);
}
@Override
protected int maxCode() {
return bitMask - 1;
}
protected final int getNextCode(final InputStream stream) throws IOException {
if (eofReached) {
return EOI_CODE;
}
int code;
int read = stream.read();
if (read < 0) {
eofReached = true;
return EOI_CODE;
}
nextData = (nextData << 8) | read;
nextBits += 8;
if (nextBits < bitsPerCode) {
read = stream.read();
if (read < 0) {
eofReached = true;
return EOI_CODE;
}
nextData = (nextData << 8) | read;
nextBits += 8;
}
code = ((nextData >> (nextBits - bitsPerCode)) & bitMask);
nextBits -= bitsPerCode;
return code;
}
}
private static final class LZWCompatibilityDecoder extends LZWDecoder {
// NOTE: This is a spec violation. However, libTiff reads such files.
// TIFF 6.0 Specification, Section 13: "LZW Compression"/"The Algorithm", page 61, says:
// "LZW compression codes are stored into bytes in high-to-low-order fashion, i.e., FillOrder
// is assumed to be 1. The compressed codes are written as bytes (not words) so that the
// compressed data will be identical whether it is an II or MM file."
protected LZWCompatibilityDecoder() {
super(true);
}
@Override
protected int maxCode() {
return bitMask;
}
protected final int getNextCode(final InputStream stream) throws IOException {
if (eofReached) {
return EOI_CODE;
}
int code;
int read = stream.read();
if (read < 0) {
eofReached = true;
return EOI_CODE;
}
nextData |= read << nextBits;
nextBits += 8;
if (nextBits < bitsPerCode) {
read = stream.read();
if (read < 0) {
eofReached = true;
return EOI_CODE;
}
nextData |= read << nextBits;
nextBits += 8;
}
code = (nextData & bitMask);
nextData >>= bitsPerCode;
nextBits -= bitsPerCode;
return code;
}
}
private static final class String {
final String previous;
final int length;
final byte value;
final byte firstChar; // Copied forward for fast access
public String(final byte code) {
this(code, code, 1, null);
}
private String(final byte value, final byte firstChar, final int length, final String previous) {
this.value = value;
this.firstChar = firstChar;
this.length = length;
this.previous = previous;
}
public final String concatenate(final byte firstChar) {
return new String(firstChar, this.firstChar, length + 1, this);
}
public final int writeTo(final byte[] buffer, final int offset) {
if (length == 0) {
return 0;
}
else if (length == 1) {
buffer[offset] = value;
return 1;
}
else {
String e = this;
for (int i = length - 1; i >= 0; i--) {
buffer[offset + i] = e.value;
e = e.previous;
}
return length;
}
}
}
}
@@ -37,7 +37,7 @@ package com.twelvemonkeys.imageio.plugins.tiff;
*/
interface TIFFBaseline {
int COMPRESSION_NONE = 1;
int COMPRESSION_CCITT_HUFFMAN = 2;
int COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE = 2;
int COMPRESSION_PACKBITS = 32773;
int PHOTOMETRIC_WHITE_IS_ZERO = 0;
@@ -49,7 +49,7 @@ interface TIFFCustom {
int COMPRESSION_JBIG = 34661;
int COMPRESSION_SGILOG = 34676;
int COMPRESSION_SGILOG24 = 34677;
int COMPRESSION_JP2000 = 34712;
int COMPRESSION_JPEG2000 = 34712;
int PHOTOMETRIC_LOGL = 32844;
int PHOTOMETRIC_LOGLUV = 32845;
@@ -65,4 +65,11 @@ interface TIFFExtension {
int SAMPLEFORMAT_INT = 2;
int SAMPLEFORMAT_FP = 3;
int SAMPLEFORMAT_UNDEFINED = 4;
int YCBCR_POSITIONING_CENTERED = 1;
int YCBCR_POSITIONING_COSITED = 2;
// "Old-style" JPEG (obsolete)
int JPEG_PROC_BASELINE = 1;
int JPEG_PROC_LOSSLESS = 14;
}
@@ -35,12 +35,15 @@ import com.twelvemonkeys.imageio.metadata.CompoundDirectory;
import com.twelvemonkeys.imageio.metadata.Directory;
import com.twelvemonkeys.imageio.metadata.Entry;
import com.twelvemonkeys.imageio.metadata.exif.EXIFReader;
import com.twelvemonkeys.imageio.metadata.exif.Rational;
import com.twelvemonkeys.imageio.metadata.exif.TIFF;
import com.twelvemonkeys.imageio.metadata.jpeg.JPEG;
import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream;
import com.twelvemonkeys.imageio.stream.SubImageInputStream;
import com.twelvemonkeys.imageio.util.IIOUtil;
import com.twelvemonkeys.imageio.util.IndexedImageTypeSpecifier;
import com.twelvemonkeys.imageio.util.ProgressListenerBase;
import com.twelvemonkeys.io.FastByteArrayOutputStream;
import com.twelvemonkeys.io.LittleEndianDataInputStream;
import com.twelvemonkeys.io.enc.DecoderStream;
import com.twelvemonkeys.io.enc.PackBitsDecoder;
@@ -58,9 +61,7 @@ import java.awt.color.ICC_Profile;
import java.awt.image.*;
import java.io.*;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.*;
import java.util.List;
import java.util.zip.Inflater;
import java.util.zip.InflaterInputStream;
@@ -105,28 +106,30 @@ public class TIFFImageReader extends ImageReaderBase {
// TODO: Source region (*tests should be failing*)
// TODO: TIFFImageWriter + Spi
// TODOs Full BaseLine support:
// TODO: Support ExtraSamples (an array, if multiple extra samples!)
// (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?
// For some layouts, we could do reads super-fast with a memory mapped buffer.
// TODO: Implement readAsRaster directly
// TODOs Full BaseLine support:
// TODO: Support ExtraSamples (an array, if multiple extra samples!)
// (0: Unspecified (not alpha), 1: Associated Alpha (pre-multiplied), 2: Unassociated Alpha (non-multiplied)
// TODO: Support Compression 2 (CCITT Modified Huffman) for bi-level images
// TODO: IIOMetadata (stay close to Sun's TIFF metadata)
// http://download.java.net/media/jai-imageio/javadoc/1.1/com/sun/media/imageio/plugins/tiff/package-summary.html#ImageMetadata
// TODOs Extension support
// TODO: Support PlanarConfiguration 2
// TODO: Support ICCProfile (fully)
// TODO: Support Compression 3 & 4 (CCITT T.4 & T.6)
// TODO: Support Compression 6 ('Old-style' JPEG)
// TODO: Support Compression 34712 (JPEG2000)? Depends on JPEG2000 ImageReader
// TODO: Support Compression 34661 (JBIG)? Depends on JBIG ImageReader
// DONE:
// Handle SampleFormat (and give up if not == 1)
// Support Compression 6 ('Old-style' JPEG)
// Support Compression 2 (CCITT Modified Huffman RLE) for bi-level images
private final static boolean DEBUG = "true".equalsIgnoreCase(System.getProperty("com.twelvemonkeys.imageio.plugins.tiff.debug"));
final static boolean DEBUG = "true".equalsIgnoreCase(System.getProperty("com.twelvemonkeys.imageio.plugins.tiff.debug"));
private CompoundDirectory IFDs;
private Directory currentIFD;
@@ -150,12 +153,12 @@ public class TIFFImageReader extends ImageReaderBase {
IFDs = (CompoundDirectory) new EXIFReader().read(imageInput); // NOTE: Sets byte order as a side effect
if (DEBUG) {
for (int i = 0; i < IFDs.directoryCount(); i++) {
System.err.printf("ifd[%d]: %s\n", i, IFDs.getDirectory(i));
}
System.err.println("Byte order: " + imageInput.getByteOrder());
System.err.println("numImages: " + IFDs.directoryCount());
System.err.println("Number of images: " + IFDs.directoryCount());
for (int i = 0; i < IFDs.directoryCount(); i++) {
System.err.printf("IFD %d: %s\n", i, IFDs.getDirectory(i));
}
}
}
}
@@ -173,7 +176,7 @@ public class TIFFImageReader extends ImageReaderBase {
return IFDs.directoryCount();
}
private int getValueAsIntWithDefault(final int tag, String tagName, Integer defaultValue) throws IIOException {
private Number getValueAsNumberWithDefault(final int tag, final String tagName, final Number defaultValue) throws IIOException {
Entry entry = currentIFD.getEntryById(tag);
if (entry == null) {
@@ -184,7 +187,19 @@ public class TIFFImageReader extends ImageReaderBase {
throw new IIOException("Missing TIFF tag: " + (tagName != null ? tagName : tag));
}
return ((Number) entry.getValue()).intValue();
return (Number) entry.getValue();
}
private long getValueAsLongWithDefault(final int tag, final String tagName, final Long defaultValue) throws IIOException {
return getValueAsNumberWithDefault(tag, tagName, defaultValue).longValue();
}
private long getValueAsLongWithDefault(final int tag, final Long defaultValue) throws IIOException {
return getValueAsLongWithDefault(tag, null, defaultValue);
}
private int getValueAsIntWithDefault(final int tag, final String tagName, final Integer defaultValue) throws IIOException {
return getValueAsNumberWithDefault(tag, tagName, defaultValue).intValue();
}
private int getValueAsIntWithDefault(final int tag, Integer defaultValue) throws IIOException {
@@ -216,7 +231,7 @@ public class TIFFImageReader extends ImageReaderBase {
getSampleFormat(); // We don't support anything but SAMPLEFORMAT_UINT at the moment, just sanity checking input
int planarConfiguration = getValueAsIntWithDefault(TIFF.TAG_PLANAR_CONFIGURATION, TIFFExtension.PLANARCONFIG_PLANAR);
int interpretation = getValueAsInt(TIFF.TAG_PHOTOMETRIC_INTERPRETATION, "PhotometricInterpretation");
int samplesPerPixel = getValueAsIntWithDefault(TIFF.TAG_SAMPLES_PER_PIXELS, 1);
int samplesPerPixel = getValueAsIntWithDefault(TIFF.TAG_SAMPLES_PER_PIXEL, 1);
int bitsPerSample = getBitsPerSample();
int dataType = bitsPerSample <= 8 ? DataBuffer.TYPE_BYTE : bitsPerSample <= 16 ? DataBuffer.TYPE_USHORT : DataBuffer.TYPE_INT;
@@ -252,8 +267,8 @@ public class TIFFImageReader extends ImageReaderBase {
}
case TIFFExtension.PHOTOMETRIC_YCBCR:
// JPEG reader will handle YCbCr to RGB for us, we'll have to do it ourselves if not JPEG...
// TODO: Handle YCbCrSubsampling (up-scaler stream, or read data as-is + up-sample (sub-)raster after read? Apply smoothing?)
// JPEG reader will handle YCbCr to RGB for us, otherwise we'll convert while reading
// TODO: Sanity check that we have SamplesPerPixel == 3, BitsPerSample == [8,8,8] and Compression == 1 (none), 5 (LZW), or 6 (JPEG)
case TIFFBaseline.PHOTOMETRIC_RGB:
// RGB
cs = profile == null ? ColorSpace.getInstance(ColorSpace.CS_sRGB) : ColorSpaces.createColorSpace(profile);
@@ -274,23 +289,25 @@ public class TIFFImageReader extends ImageReaderBase {
}
}
case 4:
// TODO: Consult ExtraSamples!
if (bitsPerSample == 8 || bitsPerSample == 16) {
// ExtraSamples 0=unspecified, 1=associated (premultiplied), 2=unassociated (TODO: Support unspecified, not alpha)
long[] extraSamples = getValueAsLongArray(TIFF.TAG_EXTRA_SAMPLES, "ExtraSamples", true);
switch (planarConfiguration) {
case TIFFBaseline.PLANARCONFIG_CHUNKY:
if (bitsPerSample == 8 && cs.isCS_sRGB()) {
return ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_4BYTE_ABGR);
}
return ImageTypeSpecifier.createInterleaved(cs, new int[] {0, 1, 2, 3}, dataType, false, false);
return ImageTypeSpecifier.createInterleaved(cs, new int[] {0, 1, 2, 3}, dataType, true, extraSamples[0] == 1);
case TIFFExtension.PLANARCONFIG_PLANAR:
return ImageTypeSpecifier.createBanded(cs, new int[] {0, 1, 2, 3}, new int[] {0, 0, 0, 0}, dataType, false, false);
return ImageTypeSpecifier.createBanded(cs, new int[] {0, 1, 2, 3}, new int[] {0, 0, 0, 0}, dataType, true, extraSamples[0] == 1);
}
}
// TODO: More samples might be ok, if multiple alpha or unknown samples
default:
throw new IIOException(String.format("Unsupported SamplesPerPixels/BitsPerSample combination for RGB TIF (expected 3/8, 4/8, 3/16 or 4/16): %d/%d", samplesPerPixel, bitsPerSample));
throw new IIOException(String.format("Unsupported SamplesPerPixels/BitsPerSample combination for RGB TIFF (expected 3/8, 4/8, 3/16 or 4/16): %d/%d", samplesPerPixel, bitsPerSample));
}
case TIFFBaseline.PHOTOMETRIC_PALETTE:
// Palette
@@ -337,18 +354,29 @@ public class TIFFImageReader extends ImageReaderBase {
return ImageTypeSpecifier.createBanded(cs, new int[] {0, 1, 2, 3}, new int[] {0, 0, 0, 0}, dataType, false, false);
}
}
case 5:
if (bitsPerSample == 8 || bitsPerSample == 16) {
// ExtraSamples 0=unspecified, 1=associated (premultiplied), 2=unassociated (TODO: Support unspecified, not alpha)
long[] extraSamples = getValueAsLongArray(TIFF.TAG_EXTRA_SAMPLES, "ExtraSamples", true);
switch (planarConfiguration) {
case TIFFBaseline.PLANARCONFIG_CHUNKY:
return ImageTypeSpecifier.createInterleaved(cs, new int[] {0, 1, 2, 3, 4}, dataType, true, extraSamples[0] == 1);
case TIFFExtension.PLANARCONFIG_PLANAR:
return ImageTypeSpecifier.createBanded(cs, new int[] {0, 1, 2, 3, 4}, new int[] {0, 0, 0, 0, 0}, dataType, true, extraSamples[0] == 1);
}
}
// TODO: More samples might be ok, if multiple alpha or unknown samples, consult ExtraSamples
default:
throw new IIOException(
String.format("Unsupported TIFF SamplesPerPixels/BitsPerSample combination for Separated TIFF (expected 4/8 or 4/16): %d/%s", samplesPerPixel, bitsPerSample)
String.format("Unsupported TIFF SamplesPerPixels/BitsPerSample combination for Separated TIFF (expected 4/8, 4/16, 5/8 or 5/16): %d/%s", samplesPerPixel, bitsPerSample)
);
}
case TIFFBaseline.PHOTOMETRIC_MASK:
// Transparency mask
// TODO: Known extensions
throw new IIOException("Unsupported TIFF PhotometricInterpretation value: " + interpretation);
default:
throw new IIOException("Unknown TIFF PhotometricInterpretation value: " + interpretation);
@@ -448,7 +476,9 @@ public class TIFFImageReader extends ImageReaderBase {
// NOTE: We handle strips as tiles of tileWidth == width by tileHeight == rowsPerStrip
// Strips are top/down, tiles are left/right, top/down
int stripTileWidth = width;
int stripTileHeight = getValueAsIntWithDefault(TIFF.TAG_ROWS_PER_STRIP, height);
long rowsPerStrip = getValueAsLongWithDefault(TIFF.TAG_ROWS_PER_STRIP, (1l << 32) - 1);
int stripTileHeight = rowsPerStrip < height ? (int) rowsPerStrip : height;
long[] stripTileOffsets = getValueAsLongArray(TIFF.TAG_TILE_OFFSETS, "TileOffsets", false);
long[] stripTileByteCounts;
@@ -479,9 +509,6 @@ public class TIFFImageReader extends ImageReaderBase {
WritableRaster rowRaster = rawType.getColorModel().createCompatibleWritableRaster(stripTileWidth, 1);
int row = 0;
// Read data
processImageStarted(imageIndex);
switch (compression) {
// TIFF Baseline
case TIFFBaseline.COMPRESSION_NONE:
@@ -494,9 +521,70 @@ public class TIFFImageReader extends ImageReaderBase {
// LZW
case TIFFExtension.COMPRESSION_ZLIB:
// 'Adobe-style' Deflate
case TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE:
// CCITT modified Huffman
// Additionally, the specification defines these values as part of the TIFF extensions:
// case TIFFExtension.COMPRESSION_CCITT_T4:
// CCITT Group 3 fax encoding
// case TIFFExtension.COMPRESSION_CCITT_T6:
// CCITT Group 4 fax encoding
int[] yCbCrSubsampling = null;
int yCbCrPos = 1;
double[] yCbCrCoefficients = null;
if (interpretation == TIFFExtension.PHOTOMETRIC_YCBCR) {
// getRawImageType does the lookup/conversion for these
if (raster.getNumBands() != 3) {
throw new IIOException("TIFF PhotometricInterpretation YCbCr requires SamplesPerPixel == 3: " + raster.getNumBands());
}
if (raster.getTransferType() != DataBuffer.TYPE_BYTE) {
throw new IIOException("TIFF PhotometricInterpretation YCbCr requires BitsPerSample == [8,8,8]");
}
yCbCrPos = getValueAsIntWithDefault(TIFF.TAG_YCBCR_POSITIONING, TIFFExtension.YCBCR_POSITIONING_CENTERED);
if (yCbCrPos != TIFFExtension.YCBCR_POSITIONING_CENTERED && yCbCrPos != TIFFExtension.YCBCR_POSITIONING_COSITED) {
processWarningOccurred("Uknown TIFF YCbCrPositioning value, expected 1 or 2: " + yCbCrPos);
}
Entry subSampling = currentIFD.getEntryById(TIFF.TAG_YCBCR_SUB_SAMPLING);
if (subSampling != null) {
try {
yCbCrSubsampling = (int[]) subSampling.getValue();
}
catch (ClassCastException e) {
throw new IIOException("Unknown TIFF YCbCrSubSampling value type: " + subSampling.getTypeName(), e);
}
if (yCbCrSubsampling.length != 2 ||
yCbCrSubsampling[0] != 1 && yCbCrSubsampling[0] != 2 && yCbCrSubsampling[0] != 4 ||
yCbCrSubsampling[1] != 1 && yCbCrSubsampling[1] != 2 && yCbCrSubsampling[1] != 4) {
throw new IIOException("Bad TIFF YCbCrSubSampling value: " + Arrays.toString(yCbCrSubsampling));
}
if (yCbCrSubsampling[0] < yCbCrSubsampling[1]) {
processWarningOccurred("TIFF PhotometricInterpretation YCbCr with bad subsampling, expected subHoriz >= subVert: " + Arrays.toString(yCbCrSubsampling));
}
}
else {
yCbCrSubsampling = new int[] {2, 2};
}
Entry coefficients = currentIFD.getEntryById(TIFF.TAG_YCBCR_COEFFICIENTS);
if (coefficients != null) {
Rational[] value = (Rational[]) coefficients.getValue();
yCbCrCoefficients = new double[] {value[0].doubleValue(), value[1].doubleValue(), value[2].doubleValue()};
}
else {
// Default to y CCIR Recommendation 601-1 values
yCbCrCoefficients = YCbCrUpsamplerStream.CCIR_601_1_COEFFICIENTS;
}
}
// Read data
processImageStarted(imageIndex);
// TODO: Read only tiles that lies within region
// General uncompressed/compressed reading
for (int y = 0; y < tilesDown; y++) {
int col = 0;
@@ -509,7 +597,8 @@ public class TIFFImageReader extends ImageReaderBase {
imageInput.seek(stripTileOffsets[i]);
DataInput input;
if (compression == TIFFBaseline.COMPRESSION_NONE) {
if (compression == TIFFBaseline.COMPRESSION_NONE && interpretation != TIFFExtension.PHOTOMETRIC_YCBCR) {
// No need for transformation, fast forward
input = imageInput;
}
else {
@@ -517,15 +606,21 @@ public class TIFFImageReader extends ImageReaderBase {
? IIOUtil.createStreamAdapter(imageInput, stripTileByteCounts[i])
: IIOUtil.createStreamAdapter(imageInput);
adapter = createDecompressorStream(compression, width, adapter);
adapter = createUnpredictorStream(predictor, width, planarConfiguration == 2 ? 1 : raster.getNumBands(), getBitsPerSample(), adapter, imageInput.getByteOrder());
if (interpretation == TIFFExtension.PHOTOMETRIC_YCBCR) {
adapter = new YCbCrUpsamplerStream(adapter, yCbCrSubsampling, yCbCrPos, colsInTile, yCbCrCoefficients);
}
// According to the spec, short/long/etc should follow order of containing stream
input = imageInput.getByteOrder() == ByteOrder.BIG_ENDIAN
? new DataInputStream(createDecoderInputStream(compression, adapter))
: new LittleEndianDataInputStream(createDecoderInputStream(compression, adapter));
? new DataInputStream(adapter)
: new LittleEndianDataInputStream(adapter);
}
// Read a full strip/tile
readStripTileData(rowRaster, interpretation, predictor, raster, numBands, col, row, colsInTile, rowsInTile, input);
readStripTileData(rowRaster, interpretation, raster, col, row, colsInTile, rowsInTile, input);
if (abortRequested()) {
break;
@@ -549,6 +644,7 @@ public class TIFFImageReader extends ImageReaderBase {
case TIFFExtension.COMPRESSION_JPEG:
// JPEG ('new-style' JPEG)
// TODO: Refactor all JPEG reading out to separate JPEG support class?
// TODO: Cache the JPEG reader for later use? Remember to reset to avoid resource leaks
// TIFF is strictly ISO JPEG, so we should probably stick to the standard reader
ImageReader jpegReader = new JPEGImageReader(getOriginatingProvider());
@@ -565,6 +661,9 @@ public class TIFFImageReader extends ImageReaderBase {
// Might have something to do with subsampling?
// How do we pass the chroma-subsampling parameter from the TIFF structure to the JPEG reader?
// TODO: Consider splicing the TAG_JPEG_TABLES into the streams for each tile, for a
// (slightly slower for multiple images, but) more compatible approach..?
jpegReader.setInput(new ByteArrayImageInputStream(tablesValue));
// NOTE: This initializes the tables AND MORE secret internal settings for the reader (as if by magic).
@@ -620,6 +719,9 @@ public class TIFFImageReader extends ImageReaderBase {
// ...and the JPEG reader will probably choke on missing tables...
}
// Read data
processImageStarted(imageIndex);
for (int y = 0; y < tilesDown; y++) {
int col = 0;
int rowsInTile = Math.min(stripTileHeight, height - row);
@@ -629,14 +731,14 @@ public class TIFFImageReader extends ImageReaderBase {
int colsInTile = Math.min(stripTileWidth, width - col);
imageInput.seek(stripTileOffsets[i]);
SubImageInputStream subStream = new SubImageInputStream(imageInput, stripTileByteCounts != null ? (int) stripTileByteCounts[i] : Short.MAX_VALUE);
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));
jpegParam.setDestinationOffset(new Point(col, row));
jpegParam.setDestination(destination);
// TODO: This works only if Gray/YCbCr/RGB, not CMYK/LAB/etc...
// In the latter case we will have to use readAsRaster
// In the latter case we will have to use readAsRaster and do color conversion ourselves
jpegReader.read(0, jpegParam);
}
finally {
@@ -662,15 +764,202 @@ public class TIFFImageReader extends ImageReaderBase {
break;
case TIFFBaseline.COMPRESSION_CCITT_HUFFMAN:
// CCITT modified Huffman
case TIFFExtension.COMPRESSION_OLD_JPEG:
// JPEG ('old-style' JPEG, later overridden in Technote2)
// http://www.remotesensing.org/libtiff/TIFFTechNote2.html
// 512/JPEGProc: 1=Baseline, 14=Lossless (with Huffman coding), no default, although 1 is assumed if absent
int mode = getValueAsIntWithDefault(TIFF.TAG_OLD_JPEG_PROC, TIFFExtension.JPEG_PROC_BASELINE);
switch (mode) {
case TIFFExtension.JPEG_PROC_BASELINE:
break; // Supported
case TIFFExtension.JPEG_PROC_LOSSLESS:
throw new IIOException("Unsupported TIFF JPEGProcessingMode: Lossless (14)");
default:
throw new IIOException("Unknown TIFF JPEGProcessingMode value: " + mode);
}
// May use normal tiling??
// TIFF is strictly ISO JPEG, so we should probably stick to the standard reader
jpegReader = new JPEGImageReader(getOriginatingProvider());
jpegParam = (JPEGImageReadParam) jpegReader.getDefaultReadParam();
// 513/JPEGInterchangeFormat (may be absent...)
int jpegOffset = getValueAsIntWithDefault(TIFF.TAG_JPEG_INTERCHANGE_FORMAT, -1);
// 514/JPEGInterchangeFormatLength (may be absent...)
int jpegLenght = getValueAsIntWithDefault(TIFF.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH, -1);
// TODO: 515/JPEGRestartInterval (may be absent)
// Currently ignored
// 517/JPEGLosslessPredictors
// 518/JPEGPointTransforms
ImageInputStream stream;
if (jpegOffset != -1) {
// Straight forward case: We're good to go! We'll disregard tiling and any tables tags
if (currentIFD.getEntryById(TIFF.TAG_OLD_JPEG_Q_TABLES) != null || currentIFD.getEntryById(TIFF.TAG_OLD_JPEG_DC_TABLES) != null || currentIFD.getEntryById(TIFF.TAG_OLD_JPEG_AC_TABLES) != null) {
processWarningOccurred("Old-style JPEG compressed TIFF with JFIF stream encountered. Ignoring JPEG tables. Reading as single tile.");
}
else {
processWarningOccurred("Old-style JPEG compressed TIFF with JFIF stream encountered. Reading as single tile.");
}
imageInput.seek(jpegOffset);
stream = new SubImageInputStream(imageInput, jpegLenght != -1 ? jpegLenght : Short.MAX_VALUE);
jpegReader.setInput(stream);
// Read data
processImageStarted(imageIndex);
try {
jpegParam.setSourceRegion(new Rectangle(0, 0, width, height));
jpegParam.setDestination(destination);
// TODO: This works only if Gray/YCbCr/RGB, not CMYK/LAB/etc...
// In the latter case we will have to use readAsRaster and do color conversion ourselves
jpegReader.read(0, jpegParam);
}
finally {
stream.close();
}
processImageProgress(100f);
if (abortRequested()) {
processReadAborted();
}
}
else {
// The hard way: Read tables and re-create a full JFIF stream
processWarningOccurred("Old-style JPEG compressed TIFF without JFIF stream encountered. Attempting to re-create JFIF stream.");
// 519/JPEGQTables
// 520/JPEGDCTables
// 521/JPEGACTables
// These fields were originally intended to point to a list of offsets to the quantization tables, one per
// component. Each table consists of 64 BYTES (one for each DCT coefficient in the 8x8 block). The
// quantization tables are stored in zigzag order, and are compatible with the quantization tables
// usually found in a JPEG stream DQT marker.
// The original specification strongly recommended that, within the TIFF file, each component be
// assigned separate tables, and labelled this field as mandatory whenever the JPEGProc field specifies
// a DCT-based process.
// We've seen old-style JPEG in TIFF files where some or all Table offsets, contained the JPEGQTables,
// JPEGDCTables, and JPEGACTables tags are incorrect values beyond EOF. However, these files do always
// seem to contain a useful JPEGInterchangeFormat tag. Therefore, we recommend a careful attempt to read
// the Tables tags only as a last resort, if no table data is found in a JPEGInterchangeFormat stream.
// TODO: If any of the q/dc/ac tables are equal (or have same offset, even if "spec" violation),
// use only the first occurrence, and update selectors in SOF0 and SOS
long[] qTablesOffsets = getValueAsLongArray(TIFF.TAG_OLD_JPEG_Q_TABLES, "JPEGQTables", true);
byte[][] qTables = new byte[qTablesOffsets.length][(int) (qTablesOffsets[1] - qTablesOffsets[0])]; // TODO: Using the offsets is fragile.. Use fixed length??
// byte[][] qTables = new byte[qTablesOffsets.length][64];
// System.err.println("qTables: " + qTables[0].length);
for (int j = 0; j < qTables.length; j++) {
imageInput.seek(qTablesOffsets[j]);
imageInput.readFully(qTables[j]);
}
long[] dcTablesOffsets = getValueAsLongArray(TIFF.TAG_OLD_JPEG_DC_TABLES, "JPEGDCTables", true);
byte[][] dcTables = new byte[dcTablesOffsets.length][(int) (dcTablesOffsets[1] - dcTablesOffsets[0])]; // TODO: Using the offsets is fragile.. Use fixed length??
// byte[][] dcTables = new byte[dcTablesOffsets.length][28];
// System.err.println("dcTables: " + dcTables[0].length);
for (int j = 0; j < dcTables.length; j++) {
imageInput.seek(dcTablesOffsets[j]);
imageInput.readFully(dcTables[j]);
}
long[] acTablesOffsets = getValueAsLongArray(TIFF.TAG_OLD_JPEG_AC_TABLES, "JPEGACTables", true);
byte[][] acTables = new byte[acTablesOffsets.length][(int) (acTablesOffsets[1] - acTablesOffsets[0])]; // TODO: Using the offsets is fragile.. Use fixed length??
// byte[][] acTables = new byte[acTablesOffsets.length][178];
// System.err.println("acTables: " + acTables[0].length);
for (int j = 0; j < acTables.length; j++) {
imageInput.seek(acTablesOffsets[j]);
imageInput.readFully(acTables[j]);
}
// Read data
processImageStarted(imageIndex);
for (int y = 0; y < tilesDown; y++) {
int col = 0;
int rowsInTile = Math.min(stripTileHeight, height - row);
for (int x = 0; x < tilesAcross; x++) {
int colsInTile = Math.min(stripTileWidth, width - col);
int i = y * tilesAcross + x;
imageInput.seek(stripTileOffsets[i]);
stream = ImageIO.createImageInputStream(new SequenceInputStream(Collections.enumeration(
Arrays.asList(
createJFIFStream(raster, stripTileWidth, stripTileHeight, qTables, dcTables, acTables),
IIOUtil.createStreamAdapter(imageInput, stripTileByteCounts != null ? (int) stripTileByteCounts[i] : Short.MAX_VALUE),
new ByteArrayInputStream(new byte[] {(byte) 0xff, (byte) 0xd9}) // EOI
)
)));
jpegReader.setInput(stream);
try {
jpegParam.setSourceRegion(new Rectangle(0, 0, colsInTile, rowsInTile));
jpegParam.setDestinationOffset(new Point(col, row));
jpegParam.setDestination(destination);
// TODO: This works only if Gray/YCbCr/RGB, not CMYK/LAB/etc...
// In the latter case we will have to use readAsRaster and do color conversion ourselves
jpegReader.read(0, jpegParam);
}
finally {
stream.close();
}
if (abortRequested()) {
break;
}
col += colsInTile;
}
processImageProgress(100f * row / (float) height);
if (abortRequested()) {
processReadAborted();
break;
}
row += rowsInTile;
}
}
break;
// Additionally, the specification defines these values as part of the TIFF extensions:
case TIFFExtension.COMPRESSION_CCITT_T4:
// CCITT Group 3 fax encoding
case TIFFExtension.COMPRESSION_CCITT_T6:
// CCITT Group 4 fax encoding
case TIFFExtension.COMPRESSION_OLD_JPEG:
// JPEG ('old-style' JPEG, later overridden in Technote2)
// Known, but unsupported compression types
case TIFFCustom.COMPRESSION_NEXT:
case TIFFCustom.COMPRESSION_CCITTRLEW:
case TIFFCustom.COMPRESSION_THUNDERSCAN:
case TIFFCustom.COMPRESSION_IT8CTPAD:
case TIFFCustom.COMPRESSION_IT8LW:
case TIFFCustom.COMPRESSION_IT8MP:
case TIFFCustom.COMPRESSION_IT8BL:
case TIFFCustom.COMPRESSION_PIXARFILM:
case TIFFCustom.COMPRESSION_PIXARLOG:
case TIFFCustom.COMPRESSION_DCS:
case TIFFCustom.COMPRESSION_JBIG: // Doable with JBIG plugin?
case TIFFCustom.COMPRESSION_SGILOG:
case TIFFCustom.COMPRESSION_SGILOG24:
case TIFFCustom.COMPRESSION_JPEG2000: // Doable with JPEG2000 plugin?
throw new IIOException("Unsupported TIFF Compression value: " + compression);
default:
@@ -682,13 +971,83 @@ public class TIFFImageReader extends ImageReaderBase {
return destination;
}
private void readStripTileData(final WritableRaster rowRaster, final int interpretation, final int predictor,
final WritableRaster raster, final int numBands, final int col, final int startRow,
private static InputStream createJFIFStream(WritableRaster raster, int stripTileWidth, int stripTileHeight, byte[][] qTables, byte[][] dcTables, byte[][] acTables) throws IOException {
FastByteArrayOutputStream stream = new FastByteArrayOutputStream(
2 + 2 + 2 + 6 + 3 * raster.getNumBands() +
5 * qTables.length + qTables.length * qTables[0].length +
5 * dcTables.length + dcTables.length * dcTables[0].length +
5 * acTables.length + acTables.length * acTables[0].length +
8 + 2 * raster.getNumBands()
);
DataOutputStream out = new DataOutputStream(stream);
out.writeShort(JPEG.SOI);
out.writeShort(JPEG.SOF0);
out.writeShort(2 + 6 + 3 * raster.getNumBands()); // SOF0 len
out.writeByte(8); // bits TODO: Consult raster/transfer type or BitsPerSample for 12/16 bits support
out.writeShort(stripTileHeight); // height
out.writeShort(stripTileWidth); // width
out.writeByte(raster.getNumBands()); // Number of components
for (int comp = 0; comp < raster.getNumBands(); comp++) {
out.writeByte(comp); // Component id
out.writeByte(comp == 0 ? 0x22 : 0x11); // h/v subsampling TODO: FixMe, consult YCbCrSubsampling
out.writeByte(comp); // Q table selector TODO: Consider merging if tables are equal
}
// TODO: Consider merging if tables are equal
for (int tableIndex = 0; tableIndex < qTables.length; tableIndex++) {
byte[] table = qTables[tableIndex];
out.writeShort(JPEG.DQT);
out.writeShort(3 + table.length); // DQT length
out.writeByte(tableIndex); // Q table id
out.write(table); // Table data
}
// TODO: Consider merging if tables are equal
for (int tableIndex = 0; tableIndex < dcTables.length; tableIndex++) {
byte[] table = dcTables[tableIndex];
out.writeShort(JPEG.DHT);
out.writeShort(3 + table.length); // DHT length
out.writeByte(tableIndex); // Huffman table id
out.write(table); // Table data
}
// TODO: Consider merging if tables are equal
for (int tableIndex = 0; tableIndex < acTables.length; tableIndex++) {
byte[] table = acTables[tableIndex];
out.writeShort(JPEG.DHT);
out.writeShort(3 + table.length); // DHT length
out.writeByte(0x10 + (tableIndex & 0xf)); // Huffman table id
out.write(table); // Table data
}
out.writeShort(JPEG.SOS);
out.writeShort(6 + 2 * raster.getNumBands()); // SOS length
out.writeByte(raster.getNumBands()); // Num comp
for (int component = 0; component < raster.getNumBands(); component++) {
out.writeByte(component); // Comp id
out.writeByte(component == 0 ? component : 0x10 + (component & 0xf)); // dc/ac selector
}
// Unknown 3 bytes pad... TODO: Figure out what the last 3 bytes are...
out.writeByte(0);
out.writeByte(0);
out.writeByte(0);
return stream.createInputStream();
}
private void readStripTileData(final WritableRaster rowRaster, final int interpretation,
final WritableRaster raster, final int col, final int startRow,
final int colsInStrip, final int rowsInStrip, final DataInput input)
throws IOException {
switch (rowRaster.getTransferType()) {
case DataBuffer.TYPE_BYTE:
byte[] rowData = ((DataBufferByte) rowRaster.getDataBuffer()).getData();
for (int j = 0; j < rowsInStrip; j++) {
int row = startRow + j;
@@ -697,19 +1056,6 @@ public class TIFFImageReader extends ImageReaderBase {
}
input.readFully(rowData);
// for (int k = 0; k < rowData.length; k++) {
// try {
// rowData[k] = input.readByte();
// }
// catch (IOException e) {
// Arrays.fill(rowData, k, rowData.length, (byte) -1);
// System.err.printf("Unexpected EOF or bad data at [%d %d]\n", col + k, row);
// break;
// }
// }
unPredict(predictor, colsInStrip, 1, numBands, rowData);
normalizeBlack(interpretation, rowData);
if (colsInStrip == rowRaster.getWidth() && col + colsInStrip <= raster.getWidth()) {
@@ -724,6 +1070,7 @@ public class TIFFImageReader extends ImageReaderBase {
break;
case DataBuffer.TYPE_USHORT:
short [] rowDataShort = ((DataBufferUShort) rowRaster.getDataBuffer()).getData();
for (int j = 0; j < rowsInStrip; j++) {
int row = startRow + j;
@@ -735,7 +1082,6 @@ public class TIFFImageReader extends ImageReaderBase {
rowDataShort[k] = input.readShort();
}
unPredict(predictor, colsInStrip, 1, numBands, rowDataShort);
normalizeBlack(interpretation, rowDataShort);
if (colsInStrip == rowRaster.getWidth() && col + colsInStrip <= raster.getWidth()) {
@@ -750,6 +1096,7 @@ public class TIFFImageReader extends ImageReaderBase {
break;
case DataBuffer.TYPE_INT:
int [] rowDataInt = ((DataBufferInt) rowRaster.getDataBuffer()).getData();
for (int j = 0; j < rowsInStrip; j++) {
int row = startRow + j;
@@ -761,7 +1108,6 @@ public class TIFFImageReader extends ImageReaderBase {
rowDataInt[k] = input.readInt();
}
unPredict(predictor, colsInStrip, 1, numBands, rowDataInt);
normalizeBlack(interpretation, rowDataInt);
if (colsInStrip == rowRaster.getWidth() && col + colsInStrip <= raster.getWidth()) {
@@ -804,75 +1150,40 @@ public class TIFFImageReader extends ImageReaderBase {
}
}
@SuppressWarnings("UnusedParameters")
private void unPredict(final int predictor, int scanLine, int rows, int bands, int[] data) throws IIOException {
// See TIFF 6.0 Specification, Section 14: "Differencing Predictor", page 64.
switch (predictor) {
case TIFFBaseline.PREDICTOR_NONE:
break;
case TIFFExtension.PREDICTOR_HORIZONTAL_DIFFERENCING:
// TODO: Implement
case TIFFExtension.PREDICTOR_HORIZONTAL_FLOATINGPOINT:
throw new IIOException("Unsupported TIFF Predictor value: " + predictor);
default:
throw new IIOException("Unknown TIFF Predictor value: " + predictor);
}
}
@SuppressWarnings("UnusedParameters")
private void unPredict(final int predictor, int scanLine, int rows, int bands, short[] data) throws IIOException {
// See TIFF 6.0 Specification, Section 14: "Differencing Predictor", page 64.
switch (predictor) {
case TIFFBaseline.PREDICTOR_NONE:
break;
case TIFFExtension.PREDICTOR_HORIZONTAL_DIFFERENCING:
// TODO: Implement
case TIFFExtension.PREDICTOR_HORIZONTAL_FLOATINGPOINT:
throw new IIOException("Unsupported TIFF Predictor value: " + predictor);
default:
throw new IIOException("Unknown TIFF Predictor value: " + predictor);
}
}
private void unPredict(final int predictor, int scanLine, int rows, final int bands, byte[] data) throws IIOException {
// See TIFF 6.0 Specification, Section 14: "Differencing Predictor", page 64.
switch (predictor) {
case TIFFBaseline.PREDICTOR_NONE:
break;
case TIFFExtension.PREDICTOR_HORIZONTAL_DIFFERENCING:
for (int y = 0; y < rows; y++) {
for (int x = 1; x < scanLine; x++) {
// TODO: For planar data (PlanarConfiguration == 2), treat as bands == 1
for (int b = 0; b < bands; b++) {
int off = y * scanLine + x;
data[off * bands + b] = (byte) (data[(off - 1) * bands + b] + data[off * bands + b]);
}
}
}
break;
case TIFFExtension.PREDICTOR_HORIZONTAL_FLOATINGPOINT:
throw new IIOException("Unsupported TIFF Predictor value: " + predictor);
default:
throw new IIOException("Unknown TIFF Predictor value: " + predictor);
}
}
private InputStream createDecoderInputStream(final int compression, final InputStream stream) throws IOException {
private InputStream createDecompressorStream(final int compression, final int width, final InputStream stream) throws IOException {
switch (compression) {
case TIFFBaseline.COMPRESSION_NONE:
return stream;
case TIFFBaseline.COMPRESSION_PACKBITS:
return new DecoderStream(stream, new PackBitsDecoder(), 1024);
case TIFFExtension.COMPRESSION_LZW:
return new DecoderStream(stream, new LZWDecoder(LZWDecoder.isOldBitReversedStream(stream)), 1024);
return new DecoderStream(stream, LZWDecoder.create(LZWDecoder.isOldBitReversedStream(stream)), 1024);
case TIFFExtension.COMPRESSION_ZLIB:
case TIFFExtension.COMPRESSION_DEFLATE:
// TIFFphotoshop.pdf (aka TIFF specification, supplement 2) says ZLIB (8) and DEFLATE (32946) algorithms are identical
case TIFFExtension.COMPRESSION_DEFLATE:
return new InflaterInputStream(stream, new Inflater(), 1024);
case TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE:
case TIFFExtension.COMPRESSION_CCITT_T4:
case TIFFExtension.COMPRESSION_CCITT_T6:
return new CCITTFaxDecoderStream(stream, width, compression, getValueAsIntWithDefault(TIFF.TAG_FILL_ORDER, 1));
default:
throw new IllegalArgumentException("Unsupported TIFF compression: " + compression);
}
}
private InputStream createUnpredictorStream(final int predictor, final int width, final int samplesPerPixel, final int bitsPerSample, final InputStream stream, final ByteOrder byteOrder) throws IOException {
switch (predictor) {
case TIFFBaseline.PREDICTOR_NONE:
return stream;
case TIFFExtension.PREDICTOR_HORIZONTAL_DIFFERENCING:
return new HorizontalDeDifferencingStream(stream, width, samplesPerPixel, bitsPerSample, byteOrder);
case TIFFExtension.PREDICTOR_HORIZONTAL_FLOATINGPOINT:
throw new IIOException("Unsupported TIFF Predictor value: " + predictor);
default:
throw new IIOException("Unknown TIFF Predictor value: " + predictor);
}
}
private long[] getValueAsLongArray(final int tag, final String tagName, boolean required) throws IIOException {
Entry entry = currentIFD.getEntryById(tag);
if (entry == null) {
@@ -893,7 +1204,7 @@ public class TIFFImageReader extends ImageReaderBase {
short[] shorts = (short[]) entry.getValue();
value = new long[shorts.length];
for (int i = 0, stripOffsetsValueLength = value.length; i < stripOffsetsValueLength; i++) {
for (int i = 0, length = value.length; i < length; i++) {
value[i] = shorts[i];
}
}
@@ -901,7 +1212,7 @@ public class TIFFImageReader extends ImageReaderBase {
int[] ints = (int[]) entry.getValue();
value = new long[ints.length];
for (int i = 0, stripOffsetsValueLength = value.length; i < stripOffsetsValueLength; i++) {
for (int i = 0, length = value.length; i < length; i++) {
value[i] = ints[i];
}
}
@@ -0,0 +1,348 @@
/*
* Copyright (c) 2013, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name "TwelveMonkeys" nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.imageio.plugins.tiff;
import com.twelvemonkeys.lang.Validate;
import java.awt.image.DataBufferByte;
import java.awt.image.Raster;
import java.io.EOFException;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
/**
* Input stream that provides on-the-fly conversion and upsampling of TIFF susampled YCbCr samples to (raw) RGB samples.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: YCbCrUpsamplerStream.java,v 1.0 31.01.13 09:25 haraldk Exp$
*/
final class YCbCrUpsamplerStream extends FilterInputStream {
// NOTE: DO NOT MODIFY OR EXPOSE THIS ARRAY OUTSIDE PACKAGE!
static final double[] CCIR_601_1_COEFFICIENTS = new double[] {299.0 / 1000.0, 587.0 / 1000.0, 114.0 / 1000.0};
private final int horizChromaSub;
private final int vertChromaSub;
private final int yCbCrPos;
private final int columns;
private final double[] coefficients;
private final int units;
private final int unitSize;
private final int padding;
private final byte[] decodedRows;
int decodedLength;
int decodedPos;
private final byte[] buffer;
int bufferLength;
int bufferPos;
public YCbCrUpsamplerStream(final InputStream stream, final int[] chromaSub, final int yCbCrPos, final int columns, final double[] coefficients) {
super(Validate.notNull(stream, "stream"));
this.horizChromaSub = chromaSub[0];
this.vertChromaSub = chromaSub[1];
this.yCbCrPos = yCbCrPos;
this.columns = columns;
this.coefficients = Arrays.equals(CCIR_601_1_COEFFICIENTS, coefficients) ? null : coefficients;
// In TIFF, subsampled streams are stored in "units" of horiz * vert pixels.
// For a 4:2 subsampled stream like this:
//
// Y0 Y1 Y2 Y3 Cb0 Cr0 Y8 Y9 Y10 Y11 Cb1 Cr1
// Y4 Y5 Y6 Y7 Y12Y13Y14 Y15
//
// In the stream, the order is: Y0,Y1,Y2..Y7,Cb0,Cr0, Y8...Y15,Cb1,Cr1, Y16...
unitSize = horizChromaSub * vertChromaSub + 2;
units = (columns + horizChromaSub - 1) / horizChromaSub; // If columns % horizChromasSub != 0...
padding = units * horizChromaSub - columns; // ...each coded row will be padded to fill unit
decodedRows = new byte[columns * vertChromaSub * 3];
buffer = new byte[unitSize * units];
}
private void fetch() throws IOException {
if (bufferPos >= bufferLength) {
int pos = 0;
int read;
// This *SHOULD* read an entire row of units into the buffer, otherwise decodeRows will throw EOFException
while (pos < buffer.length && (read = in.read(buffer, pos, buffer.length - pos)) > 0) {
pos += read;
}
bufferLength = pos;
bufferPos = 0;
}
if (bufferLength > 0) {
decodeRows();
}
else {
decodedLength = -1;
}
}
private void decodeRows() throws EOFException {
decodedLength = decodedRows.length;
for (int u = 0; u < units; u++) {
if (bufferPos >= bufferLength) {
throw new EOFException("Unexpected end of stream");
}
// Decode one unit
byte cb = buffer[bufferPos + unitSize - 2];
byte cr = buffer[bufferPos + unitSize - 1];
for (int y = 0; y < vertChromaSub; y++) {
for (int x = 0; x < horizChromaSub; x++) {
// Skip padding at end of row
int column = horizChromaSub * u + x;
if (column >= columns) {
bufferPos += padding;
break;
}
int pixelOff = 3 * (column + columns * y);
decodedRows[pixelOff] = buffer[bufferPos++];
decodedRows[pixelOff + 1] = cb;
decodedRows[pixelOff + 2] = cr;
// Convert to RGB
if (coefficients == null) {
YCbCrConverter.convertYCbCr2RGB(decodedRows, decodedRows, pixelOff);
}
else {
convertYCbCr2RGB(decodedRows, decodedRows, coefficients, pixelOff);
}
}
}
bufferPos += 2; // Skip CbCr bytes at end of unit
}
bufferPos = bufferLength;
decodedPos = 0;
}
@Override
public int read() throws IOException {
if (decodedLength < 0) {
return -1;
}
if (decodedPos >= decodedLength) {
fetch();
if (decodedLength < 0) {
return -1;
}
}
return decodedRows[decodedPos++] & 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) {
return -1;
}
}
int read = Math.min(decodedLength - decodedPos, len);
System.arraycopy(decodedRows, decodedPos, b, off, read);
decodedPos += read;
return read;
}
@Override
public long skip(long n) throws IOException {
if (decodedLength < 0) {
return -1;
}
if (decodedPos >= decodedLength) {
fetch();
if (decodedLength < 0) {
return -1;
}
}
int skipped = (int) Math.min(decodedLength - decodedPos, n);
decodedPos += skipped;
return skipped;
}
@Override
public boolean markSupported() {
return false;
}
@Override
public synchronized void reset() throws IOException {
throw new IOException("mark/reset not supported");
}
private void convertYCbCr2RGB(final byte[] yCbCr, final byte[] rgb, final double[] coefficients, final int offset) {
double y = (yCbCr[offset ] & 0xff);
double cb = (yCbCr[offset + 1] & 0xff) - 128; // TODO: The -128 part seems bogus... Consult ReferenceBlackWhite??? But default to these values?
double cr = (yCbCr[offset + 2] & 0xff) - 128;
double lumaRed = coefficients[0];
double lumaGreen = coefficients[1];
double lumaBlue = coefficients[2];
int red = (int) Math.round(cr * (2 - 2 * lumaRed) + y);
int blue = (int) Math.round(cb * (2 - 2 * lumaBlue) + y);
int green = (int) Math.round((y - lumaRed * (rgb[offset] & 0xff) - lumaBlue * (rgb[offset + 2] & 0xff)) / lumaGreen);
rgb[offset ] = clamp(red);
rgb[offset + 2] = clamp(blue);
rgb[offset + 1] = clamp(green);
}
private static byte clamp(int val) {
return (byte) Math.max(0, Math.min(255, val));
}
// TODO: This code is copied from JPEG package, make it "more" public: com.tm.imageio.color package?
/**
* Static inner class for lazy-loading of conversion tables.
*/
static final class YCbCrConverter {
/** Define tables for YCC->RGB color space conversion. */
private final static int SCALEBITS = 16;
private final static int MAXJSAMPLE = 255;
private final static int CENTERJSAMPLE = 128;
private final static int ONE_HALF = 1 << (SCALEBITS - 1);
private final static int[] Cr_R_LUT = new int[MAXJSAMPLE + 1];
private final static int[] Cb_B_LUT = new int[MAXJSAMPLE + 1];
private final static int[] Cr_G_LUT = new int[MAXJSAMPLE + 1];
private final static int[] Cb_G_LUT = new int[MAXJSAMPLE + 1];
/**
* Initializes tables for YCC->RGB color space conversion.
*/
private static void buildYCCtoRGBtable() {
if (TIFFImageReader.DEBUG) {
System.err.println("Building YCC conversion table");
}
for (int i = 0, x = -CENTERJSAMPLE; i <= MAXJSAMPLE; i++, x++) {
// i is the actual input pixel value, in the range 0..MAXJSAMPLE
// The Cb or Cr value we are thinking of is x = i - CENTERJSAMPLE
// Cr=>R value is nearest int to 1.40200 * x
Cr_R_LUT[i] = (int) ((1.40200 * (1 << SCALEBITS) + 0.5) * x + ONE_HALF) >> SCALEBITS;
// Cb=>B value is nearest int to 1.77200 * x
Cb_B_LUT[i] = (int) ((1.77200 * (1 << SCALEBITS) + 0.5) * x + ONE_HALF) >> SCALEBITS;
// Cr=>G value is scaled-up -0.71414 * x
Cr_G_LUT[i] = -(int) (0.71414 * (1 << SCALEBITS) + 0.5) * x;
// Cb=>G value is scaled-up -0.34414 * x
// We also add in ONE_HALF so that need not do it in inner loop
Cb_G_LUT[i] = -(int) ((0.34414) * (1 << SCALEBITS) + 0.5) * x + ONE_HALF;
}
}
static {
buildYCCtoRGBtable();
}
static void convertYCbCr2RGB(final Raster raster) {
final int height = raster.getHeight();
final int width = raster.getWidth();
final byte[] data = ((DataBufferByte) raster.getDataBuffer()).getData();
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
convertYCbCr2RGB(data, data, (x + y * width) * 3);
}
}
}
static void convertYCbCr2RGB(final byte[] yCbCr, final byte[] rgb, final int offset) {
int y = yCbCr[offset ] & 0xff;
int cr = yCbCr[offset + 2] & 0xff;
int cb = yCbCr[offset + 1] & 0xff;
rgb[offset ] = clamp(y + Cr_R_LUT[cr]);
rgb[offset + 1] = clamp(y + (Cb_G_LUT[cb] + Cr_G_LUT[cr] >> SCALEBITS));
rgb[offset + 2] = clamp(y + Cb_B_LUT[cb]);
}
static void convertYCCK2CMYK(final Raster raster) {
final int height = raster.getHeight();
final int width = raster.getWidth();
final byte[] data = ((DataBufferByte) raster.getDataBuffer()).getData();
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
convertYCCK2CMYK(data, data, (x + y * width) * 4);
}
}
}
private static void convertYCCK2CMYK(byte[] ycck, byte[] cmyk, int offset) {
// Inverted
int y = 255 - ycck[offset ] & 0xff;
int cb = 255 - ycck[offset + 1] & 0xff;
int cr = 255 - ycck[offset + 2] & 0xff;
int k = 255 - ycck[offset + 3] & 0xff;
int cmykC = MAXJSAMPLE - (y + Cr_R_LUT[cr]);
int cmykM = MAXJSAMPLE - (y + (Cb_G_LUT[cb] + Cr_G_LUT[cr] >> SCALEBITS));
int cmykY = MAXJSAMPLE - (y + Cb_B_LUT[cb]);
cmyk[offset ] = clamp(cmykC);
cmyk[offset + 1] = clamp(cmykM);
cmyk[offset + 2] = clamp(cmykY);
cmyk[offset + 3] = (byte) k; // K passes through unchanged
}
// private static byte clamp(int val) {
// return (byte) Math.max(0, Math.min(255, val));
// }
}
}
@@ -0,0 +1,137 @@
<!DOCTYPE com_sun_media_imageio_plugins_tiff_image_1.0 [
<!ELEMENT com_sun_media_imageio_plugins_tiff_image_1.0 (TIFFIFD)*>
<!ELEMENT TIFFIFD (TIFFField | TIFFIFD)*>
<!-- An IFD (directory) containing fields -->
<!ATTLIST TIFFIFD "tagSets" CDATA #REQUIRED>
<!-- Data type: String -->
<!ATTLIST TIFFIFD "parentTagNumber" CDATA #IMPLIED>
<!-- The tag number of the field pointing to this IFD -->
<!-- Data type: Integer -->
<!ATTLIST TIFFIFD "parentTagName" CDATA #IMPLIED>
<!-- A mnemonic name for the field pointing to this IFD, if known
-->
<!-- Data type: String -->
<!ELEMENT TIFFField (TIFFBytes | TIFFAsciis | TIFFShorts | TIFFSShorts | TIFFLongs | TIFFSLongs | TIFFRationals | TIFFSRationals | TIFFFloats | TIFFDoubles | TIFFUndefined)>
<!-- A field containing data -->
<!ATTLIST TIFFField "number" CDATA #REQUIRED>
<!-- The tag number asociated with the field -->
<!-- Data type: String -->
<!ATTLIST TIFFField "name" CDATA #IMPLIED>
<!-- A mnemonic name associated with the field, if known -->
<!-- Data type: String -->
<!ELEMENT TIFFBytes (TIFFByte)*>
<!-- A sequence of TIFFByte nodes -->
<!ELEMENT TIFFByte EMPTY>
<!-- An integral value between 0 and 255 -->
<!ATTLIST TIFFByte "value" CDATA #IMPLIED>
<!-- The value -->
<!-- Data type: String -->
<!ATTLIST TIFFByte "description" CDATA #IMPLIED>
<!-- A description, if available -->
<!-- Data type: String -->
<!ELEMENT TIFFAsciis (TIFFAscii)*>
<!-- A sequence of TIFFAscii nodes -->
<!ELEMENT TIFFAscii EMPTY>
<!-- A String value -->
<!ATTLIST TIFFAscii "value" CDATA #IMPLIED>
<!-- The value -->
<!-- Data type: String -->
<!ELEMENT TIFFShorts (TIFFShort)*>
<!-- A sequence of TIFFShort nodes -->
<!ELEMENT TIFFShort EMPTY>
<!-- An integral value between 0 and 65535 -->
<!ATTLIST TIFFShort "value" CDATA #IMPLIED>
<!-- The value -->
<!-- Data type: String -->
<!ATTLIST TIFFShort "description" CDATA #IMPLIED>
<!-- A description, if available -->
<!-- Data type: String -->
<!ELEMENT TIFFSShorts (TIFFSShort)*>
<!-- A sequence of TIFFSShort nodes -->
<!ELEMENT TIFFSShort EMPTY>
<!-- An integral value between -32768 and 32767 -->
<!ATTLIST TIFFSShort "value" CDATA #IMPLIED>
<!-- The value -->
<!-- Data type: String -->
<!ATTLIST TIFFSShort "description" CDATA #IMPLIED>
<!-- A description, if available -->
<!-- Data type: String -->
<!ELEMENT TIFFLongs (TIFFLong)*>
<!-- A sequence of TIFFLong nodes -->
<!ELEMENT TIFFLong EMPTY>
<!-- An integral value between 0 and 4294967295 -->
<!ATTLIST TIFFLong "value" CDATA #IMPLIED>
<!-- The value -->
<!-- Data type: String -->
<!ATTLIST TIFFLong "description" CDATA #IMPLIED>
<!-- A description, if available -->
<!-- Data type: String -->
<!ELEMENT TIFFSLongs (TIFFSLong)*>
<!-- A sequence of TIFFSLong nodes -->
<!ELEMENT TIFFSLong EMPTY>
<!-- An integral value between -2147483648 and 2147482647 -->
<!ATTLIST TIFFSLong "value" CDATA #IMPLIED>
<!-- The value -->
<!-- Data type: String -->
<!ATTLIST TIFFSLong "description" CDATA #IMPLIED>
<!-- A description, if available -->
<!-- Data type: String -->
<!ELEMENT TIFFRationals (TIFFRational)*>
<!-- A sequence of TIFFRational nodes -->
<!ELEMENT TIFFRational EMPTY>
<!-- A rational value consisting of an unsigned numerator and
denominator -->
<!ATTLIST TIFFRational "value" CDATA #IMPLIED>
<!-- The numerator and denominator, separated by a slash -->
<!-- Data type: String -->
<!ELEMENT TIFFSRationals (TIFFSRational)*>
<!-- A sequence of TIFFSRational nodes -->
<!ELEMENT TIFFSRational EMPTY>
<!-- A rational value consisting of a signed numerator and
denominator -->
<!ATTLIST TIFFSRational "value" CDATA #IMPLIED>
<!-- The numerator and denominator, separated by a slash -->
<!-- Data type: String -->
<!ELEMENT TIFFFloats (TIFFFloat)*>
<!-- A sequence of TIFFFloat nodes -->
<!ELEMENT TIFFFloat EMPTY>
<!-- A single-precision floating-point value -->
<!ATTLIST TIFFFloat "value" CDATA #IMPLIED>
<!-- The value -->
<!-- Data type: String -->
<!ELEMENT TIFFDoubles (TIFFDouble)*>
<!-- A sequence of TIFFDouble nodes -->
<!ELEMENT TIFFDouble EMPTY>
<!-- A double-precision floating-point value -->
<!ATTLIST TIFFDouble "value" CDATA #IMPLIED>
<!-- The value -->
<!-- Data type: String -->
<!ELEMENT TIFFUndefined EMPTY>
<!-- Uninterpreted byte data -->
<!ATTLIST TIFFUndefined "value" CDATA #IMPLIED>
<!-- A list of comma-separated byte values -->
<!-- Data type: String -->
]>
@@ -0,0 +1,9 @@
<!DOCTYPE com_sun_media_imageio_plugins_tiff_stream_1.0 [
<!ELEMENT com_sun_media_imageio_plugins_tiff_stream_1.0 (ByteOrder)>
<!ELEMENT ByteOrder EMPTY>
<!-- The stream byte order -->
<!ATTLIST ByteOrder "value" CDATA #REQUIRED>
<!-- One of "BIG_ENDIAN" or "LITTLE_ENDIAN" -->
<!-- Data type: String -->
]>
@@ -0,0 +1,166 @@
/*
* Copyright (c) 2013, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name "TwelveMonkeys" nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.imageio.plugins.tiff;
import org.junit.Before;
import org.junit.Test;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferByte;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import static org.junit.Assert.*;
/**
* CCITTFaxDecoderStreamTest
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: CCITTFaxDecoderStreamTest.java,v 1.0 09.03.13 14:44 haraldk Exp$
*/
public class CCITTFaxDecoderStreamTest {
// TODO: Better tests (full A4 width scan lines?)
// From http://www.mikekohn.net/file_formats/tiff.php
static final byte[] DATA_TYPE_2 = {
(byte) 0x84, (byte) 0xe0, // 10000100 11100000
(byte) 0x84, (byte) 0xe0, // 10000100 11100000
(byte) 0x84, (byte) 0xe0, // 10000100 11100000
(byte) 0x7d, (byte) 0xc0, // 01111101 11000000
};
static final byte[] DATA_TYPE_3 = {
0x00, 0x01, (byte) 0xc2, 0x70,
0x00, 0x01, 0x70,
0x01,
};
static final byte[] DATA_TYPE_4 = {
0x26, (byte) 0xb0, 95, (byte) 0xfa, (byte) 0xc0
};
// Image should be (6 x 4):
// 1 1 1 0 1 1 x x
// 1 1 1 0 1 1 x x
// 1 1 1 0 1 1 x x
// 1 1 0 0 1 1 x x
BufferedImage image;
@Before
public void init() {
image = new BufferedImage(6, 4, BufferedImage.TYPE_BYTE_BINARY);
for (int y = 0; y < 4; y++) {
for (int x = 0; x < 6; x++) {
image.setRGB(x, y, x == 3 ? 0xff000000 : 0xffffffff);
}
}
image.setRGB(2, 3, 0xff000000);
}
@Test
public void testReadCountType2() throws IOException {
InputStream stream = new CCITTFaxDecoderStream(new ByteArrayInputStream(DATA_TYPE_2), 6, TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE, 1);
int count = 0;
int read;
while ((read = stream.read()) >= 0) {
count++;
}
// Just make sure we'll have 4 bytes
assertEquals(4, count);
// Verify that we don't return arbitrary values
assertEquals(-1, read);
}
@Test
public void testDecodeType2() throws IOException {
InputStream stream = new CCITTFaxDecoderStream(new ByteArrayInputStream(DATA_TYPE_2), 6, TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE, 1);
byte[] imageData = ((DataBufferByte) image.getData().getDataBuffer()).getData();
byte[] bytes = new byte[imageData.length];
new DataInputStream(stream).readFully(bytes);
// JPanel panel = new JPanel();
// panel.add(new JLabel("Expected", new BufferedImageIcon(image, 300, 300, true), JLabel.CENTER));
// panel.add(new JLabel("Actual", new BufferedImageIcon(new BufferedImage(image.getColorModel(), Raster.createPackedRaster(new DataBufferByte(bytes, bytes.length), 6, 4, 1, null), false, null), 300, 300, true), JLabel.CENTER));
// JOptionPane.showConfirmDialog(null, panel);
assertArrayEquals(imageData, bytes);
}
@Test(expected = IllegalArgumentException.class)
public void testDecodeType3() throws IOException {
InputStream stream = new CCITTFaxDecoderStream(new ByteArrayInputStream(DATA_TYPE_3), 6, TIFFExtension.COMPRESSION_CCITT_T4, 1);
byte[] imageData = ((DataBufferByte) image.getData().getDataBuffer()).getData();
byte[] bytes = new byte[imageData.length];
DataInputStream dataInput = new DataInputStream(stream);
for (int y = 0; y < image.getHeight(); y++) {
System.err.println("y: " + y);
dataInput.readFully(bytes, y * image.getWidth(), image.getWidth());
}
// JPanel panel = new JPanel();
// panel.add(new JLabel("Expected", new BufferedImageIcon(image, 300, 300, true), JLabel.CENTER));
// panel.add(new JLabel("Actual", new BufferedImageIcon(new BufferedImage(image.getColorModel(), Raster.createPackedRaster(new DataBufferByte(bytes, bytes.length), 6, 4, 1, null), false, null), 300, 300, true), JLabel.CENTER));
// JOptionPane.showConfirmDialog(null, panel);
assertArrayEquals(imageData, bytes);
}
@Test(expected = IllegalArgumentException.class)
public void testDecodeType4() throws IOException {
InputStream stream = new CCITTFaxDecoderStream(new ByteArrayInputStream(DATA_TYPE_4), 6, TIFFExtension.COMPRESSION_CCITT_T6, 1);
byte[] imageData = ((DataBufferByte) image.getData().getDataBuffer()).getData();
byte[] bytes = new byte[imageData.length];
DataInputStream dataInput = new DataInputStream(stream);
for (int y = 0; y < image.getHeight(); y++) {
System.err.println("y: " + y);
dataInput.readFully(bytes, y * image.getWidth(), image.getWidth());
}
// JPanel panel = new JPanel();
// panel.add(new JLabel("Expected", new BufferedImageIcon(image, 300, 300, true), JLabel.CENTER));
// panel.add(new JLabel("Actual", new BufferedImageIcon(new BufferedImage(image.getColorModel(), Raster.createPackedRaster(new DataBufferByte(bytes, bytes.length), 6, 4, 1, null), false, null), 300, 300, true), JLabel.CENTER));
// JOptionPane.showConfirmDialog(null, panel);
assertArrayEquals(imageData, bytes);
}
}
@@ -0,0 +1,569 @@
/*
* Copyright (c) 2013, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name "TwelveMonkeys" nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.imageio.plugins.tiff;
import com.twelvemonkeys.io.FastByteArrayOutputStream;
import com.twelvemonkeys.io.LittleEndianDataInputStream;
import com.twelvemonkeys.io.LittleEndianDataOutputStream;
import org.junit.Test;
import java.io.*;
import java.nio.ByteOrder;
import static org.junit.Assert.*;
/**
* HorizontalDeDifferencingStreamTest
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: HorizontalDeDifferencingStreamTest.java,v 1.0 13.03.13 12:46 haraldk Exp$
*/
public class HorizontalDeDifferencingStreamTest {
@Test
public void testRead1SPP1BPS() throws IOException {
// 1 sample per pixel, 1 bits per sample (mono/indexed)
byte[] data = {
(byte) 0x80, 0x00, 0x00,
0x71, 0x11, 0x44,
};
InputStream stream = new HorizontalDeDifferencingStream(new ByteArrayInputStream(data), 24, 1, 1, ByteOrder.BIG_ENDIAN);
// Row 1
assertEquals(0xff, stream.read());
assertEquals(0xff, stream.read());
assertEquals(0xff, stream.read());
// Row 2
assertEquals(0x5e, stream.read());
assertEquals(0x1e, stream.read());
assertEquals(0x78, stream.read());
// EOF
assertEquals(-1, stream.read());
}
@Test
public void testRead1SPP2BPS() throws IOException {
// 1 sample per pixel, 2 bits per sample (gray/indexed)
byte[] data = {
(byte) 0xc0, 0x00, 0x00, 0x00,
0x71, 0x11, 0x44, (byte) 0xcc,
};
InputStream stream = new HorizontalDeDifferencingStream(new ByteArrayInputStream(data), 16, 1, 2, ByteOrder.BIG_ENDIAN);
// Row 1
assertEquals(0xff, stream.read());
assertEquals(0xff, stream.read());
assertEquals(0xff, stream.read());
assertEquals(0xff, stream.read());
// Row 2
assertEquals(0x41, stream.read());
assertEquals(0x6b, stream.read());
assertEquals(0x05, stream.read());
assertEquals(0x0f, stream.read());
// EOF
assertEquals(-1, stream.read());
}
@Test
public void testRead1SPP4BPS() throws IOException {
// 1 sample per pixel, 4 bits per sample (gray/indexed)
byte[] data = {
(byte) 0xf0, 0x00, 0x00, 0x00,
0x70, 0x11, 0x44, (byte) 0xcc,
0x00, 0x01, 0x10, (byte) 0xe0
};
InputStream stream = new HorizontalDeDifferencingStream(new ByteArrayInputStream(data), 8, 1, 4, ByteOrder.BIG_ENDIAN);
// Row 1
assertEquals(0xff, stream.read());
assertEquals(0xff, stream.read());
assertEquals(0xff, stream.read());
assertEquals(0xff, stream.read());
// Row 2
assertEquals(0x77, stream.read());
assertEquals(0x89, stream.read());
assertEquals(0xd1, stream.read());
assertEquals(0xd9, stream.read());
// Row 3
assertEquals(0x00, stream.read());
assertEquals(0x01, stream.read());
assertEquals(0x22, stream.read());
assertEquals(0x00, stream.read());
// EOF
assertEquals(-1, stream.read());
}
@Test
public void testRead1SPP8BPS() throws IOException {
// 1 sample per pixel, 8 bits per sample (gray/indexed)
byte[] data = {
(byte) 0xff, 0, 0, 0,
0x7f, 1, 4, -4,
0x00, 127, 127, -127
};
InputStream stream = new HorizontalDeDifferencingStream(new ByteArrayInputStream(data), 4, 1, 8, ByteOrder.BIG_ENDIAN);
// Row 1
assertEquals(0xff, stream.read());
assertEquals(0xff, stream.read());
assertEquals(0xff, stream.read());
assertEquals(0xff, stream.read());
// Row 2
assertEquals(0x7f, stream.read());
assertEquals(0x80, stream.read());
assertEquals(0x84, stream.read());
assertEquals(0x80, stream.read());
// Row 3
assertEquals(0x00, stream.read());
assertEquals(0x7f, stream.read());
assertEquals(0xfe, stream.read());
assertEquals(0x7f, stream.read());
// EOF
assertEquals(-1, stream.read());
}
@Test
public void testReadArray1SPP8BPS() throws IOException {
// 1 sample per pixel, 8 bits per sample (gray/indexed)
byte[] data = {
(byte) 0xff, 0, 0, 0,
0x7f, 1, 4, -4,
0x00, 127, 127, -127
};
InputStream stream = new HorizontalDeDifferencingStream(new ByteArrayInputStream(data), 4, 1, 8, ByteOrder.BIG_ENDIAN);
byte[] result = new byte[data.length];
new DataInputStream(stream).readFully(result);
assertArrayEquals(
new byte[] {
(byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
0x7f, (byte) 0x80, (byte) 0x84, (byte) 0x80,
0x00, 0x7f, (byte) 0xfe, 0x7f,
},
result
);
// EOF
assertEquals(-1, stream.read(new byte[16]));
assertEquals(-1, stream.read());
}
@Test
public void testRead1SPP32BPS() throws IOException {
// 1 sample per pixel, 32 bits per sample (gray)
FastByteArrayOutputStream out = new FastByteArrayOutputStream(16);
DataOutput dataOut = new DataOutputStream(out);
dataOut.writeInt(0x00000000);
dataOut.writeInt(305419896);
dataOut.writeInt(305419896);
dataOut.writeInt(-610839792);
InputStream in = new HorizontalDeDifferencingStream(out.createInputStream(), 4, 1, 32, ByteOrder.BIG_ENDIAN);
DataInput dataIn = new DataInputStream(in);
// Row 1
assertEquals(0, dataIn.readInt());
assertEquals(305419896, dataIn.readInt());
assertEquals(610839792, dataIn.readInt());
assertEquals(0, dataIn.readInt());
// EOF
assertEquals(-1, in.read());
}
@Test
public void testRead1SPP32BPSLittleEndian() throws IOException {
// 1 sample per pixel, 32 bits per sample (gray)
FastByteArrayOutputStream out = new FastByteArrayOutputStream(16);
DataOutput dataOut = new LittleEndianDataOutputStream(out);
dataOut.writeInt(0x00000000);
dataOut.writeInt(305419896);
dataOut.writeInt(305419896);
dataOut.writeInt(-610839792);
InputStream in = new HorizontalDeDifferencingStream(out.createInputStream(), 4, 1, 32, ByteOrder.LITTLE_ENDIAN);
DataInput dataIn = new LittleEndianDataInputStream(in);
// Row 1
assertEquals(0, dataIn.readInt());
assertEquals(305419896, dataIn.readInt());
assertEquals(610839792, dataIn.readInt());
assertEquals(0, dataIn.readInt());
// EOF
assertEquals(-1, in.read());
}
@Test
public void testRead1SPP64BPS() throws IOException {
// 1 sample per pixel, 64 bits per sample (gray)
FastByteArrayOutputStream out = new FastByteArrayOutputStream(32);
DataOutput dataOut = new DataOutputStream(out);
dataOut.writeLong(0x00000000);
dataOut.writeLong(81985529216486895L);
dataOut.writeLong(81985529216486895L);
dataOut.writeLong(-163971058432973790L);
InputStream in = new HorizontalDeDifferencingStream(out.createInputStream(), 4, 1, 64, ByteOrder.BIG_ENDIAN);
DataInput dataIn = new DataInputStream(in);
// Row 1
assertEquals(0, dataIn.readLong());
assertEquals(81985529216486895L, dataIn.readLong());
assertEquals(163971058432973790L, dataIn.readLong());
assertEquals(0, dataIn.readLong());
// EOF
assertEquals(-1, in.read());
}
@Test
public void testRead1SPP64BPSLittleEndian() throws IOException {
// 1 sample per pixel, 64 bits per sample (gray)
FastByteArrayOutputStream out = new FastByteArrayOutputStream(32);
DataOutput dataOut = new LittleEndianDataOutputStream(out);
dataOut.writeLong(0x00000000);
dataOut.writeLong(81985529216486895L);
dataOut.writeLong(81985529216486895L);
dataOut.writeLong(-163971058432973790L);
InputStream in = new HorizontalDeDifferencingStream(out.createInputStream(), 4, 1, 64, ByteOrder.LITTLE_ENDIAN);
DataInput dataIn = new LittleEndianDataInputStream(in);
// Row 1
assertEquals(0, dataIn.readLong());
assertEquals(81985529216486895L, dataIn.readLong());
assertEquals(163971058432973790L, dataIn.readLong());
assertEquals(0, dataIn.readLong());
// EOF
assertEquals(-1, in.read());
}
@Test
public void testRead3SPP8BPS() throws IOException {
// 3 samples per pixel, 8 bits per sample (RGB)
byte[] data = {
(byte) 0xff, (byte) 0x00, (byte) 0x7f, -1, -1, -1, -4, -4, -4, 4, 4, 4,
0x7f, 0x7f, 0x7f, 1, 1, 1, 4, 4, 4, -4, -4, -4,
0x00, 0x00, 0x00, 127, -127, 0, -127, 127, 0, 0, 0, 127,
};
InputStream stream = new HorizontalDeDifferencingStream(new ByteArrayInputStream(data), 4, 3, 8, ByteOrder.BIG_ENDIAN);
// Row 1
assertEquals(0xff, stream.read());
assertEquals(0x00, stream.read());
assertEquals(0x7f, stream.read());
assertEquals(0xfe, stream.read());
assertEquals(0xff, stream.read());
assertEquals(0x7e, stream.read());
assertEquals(0xfa, stream.read());
assertEquals(0xfb, stream.read());
assertEquals(0x7a, stream.read());
assertEquals(0xfe, stream.read());
assertEquals(0xff, stream.read());
assertEquals(0x7e, stream.read());
// Row 2
assertEquals(0x7f, stream.read());
assertEquals(0x7f, stream.read());
assertEquals(0x7f, stream.read());
assertEquals(0x80, stream.read());
assertEquals(0x80, stream.read());
assertEquals(0x80, stream.read());
assertEquals(0x84, stream.read());
assertEquals(0x84, stream.read());
assertEquals(0x84, stream.read());
assertEquals(0x80, stream.read());
assertEquals(0x80, stream.read());
assertEquals(0x80, stream.read());
// Row 3
assertEquals(0x00, stream.read());
assertEquals(0x00, stream.read());
assertEquals(0x00, stream.read());
assertEquals(0x7f, stream.read());
assertEquals(0x81, stream.read());
assertEquals(0x00, stream.read());
assertEquals(0x00, stream.read());
assertEquals(0x00, stream.read());
assertEquals(0x00, stream.read());
assertEquals(0x00, stream.read());
assertEquals(0x00, stream.read());
assertEquals(0x7f, stream.read());
// EOF
assertEquals(-1, stream.read());
}
@Test
public void testRead3SPP16BPS() throws IOException {
FastByteArrayOutputStream out = new FastByteArrayOutputStream(24);
DataOutput dataOut = new DataOutputStream(out);
dataOut.writeShort(0x0000);
dataOut.writeShort(0x0000);
dataOut.writeShort(0x0000);
dataOut.writeShort(4660);
dataOut.writeShort(30292);
dataOut.writeShort(4660);
dataOut.writeShort(4660);
dataOut.writeShort(30292);
dataOut.writeShort(4660);
dataOut.writeShort(-9320);
dataOut.writeShort(-60584);
dataOut.writeShort(-9320);
dataOut.writeShort(0x0000);
dataOut.writeShort(0x0000);
dataOut.writeShort(0x0000);
dataOut.writeShort(30292);
dataOut.writeShort(30292);
dataOut.writeShort(30292);
dataOut.writeShort(30292);
dataOut.writeShort(30292);
dataOut.writeShort(30292);
dataOut.writeShort(-60584);
dataOut.writeShort(-60584);
dataOut.writeShort(-60584);
InputStream in = new HorizontalDeDifferencingStream(out.createInputStream(), 4, 3, 16, ByteOrder.BIG_ENDIAN);
DataInput dataIn = new DataInputStream(in);
// Row 1
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(4660, dataIn.readUnsignedShort());
assertEquals(30292, dataIn.readUnsignedShort());
assertEquals(4660, dataIn.readUnsignedShort());
assertEquals(9320, dataIn.readUnsignedShort());
assertEquals(60584, dataIn.readUnsignedShort());
assertEquals(9320, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
// Row 2
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(30292, dataIn.readUnsignedShort());
assertEquals(30292, dataIn.readUnsignedShort());
assertEquals(30292, dataIn.readUnsignedShort());
assertEquals(60584, dataIn.readUnsignedShort());
assertEquals(60584, dataIn.readUnsignedShort());
assertEquals(60584, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
// EOF
assertEquals(-1, in.read());
}
@Test
public void testRead3SPP16BPSLittleEndian() throws IOException {
FastByteArrayOutputStream out = new FastByteArrayOutputStream(24);
DataOutput dataOut = new LittleEndianDataOutputStream(out);
dataOut.writeShort(0x0000);
dataOut.writeShort(0x0000);
dataOut.writeShort(0x0000);
dataOut.writeShort(4660);
dataOut.writeShort(30292);
dataOut.writeShort(4660);
dataOut.writeShort(4660);
dataOut.writeShort(30292);
dataOut.writeShort(4660);
dataOut.writeShort(-9320);
dataOut.writeShort(-60584);
dataOut.writeShort(-9320);
dataOut.writeShort(0x0000);
dataOut.writeShort(0x0000);
dataOut.writeShort(0x0000);
dataOut.writeShort(30292);
dataOut.writeShort(30292);
dataOut.writeShort(30292);
dataOut.writeShort(30292);
dataOut.writeShort(30292);
dataOut.writeShort(30292);
dataOut.writeShort(-60584);
dataOut.writeShort(-60584);
dataOut.writeShort(-60584);
InputStream in = new HorizontalDeDifferencingStream(out.createInputStream(), 4, 3, 16, ByteOrder.LITTLE_ENDIAN);
DataInput dataIn = new LittleEndianDataInputStream(in);
// Row 1
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(4660, dataIn.readUnsignedShort());
assertEquals(30292, dataIn.readUnsignedShort());
assertEquals(4660, dataIn.readUnsignedShort());
assertEquals(9320, dataIn.readUnsignedShort());
assertEquals(60584, dataIn.readUnsignedShort());
assertEquals(9320, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
// Row 2
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(30292, dataIn.readUnsignedShort());
assertEquals(30292, dataIn.readUnsignedShort());
assertEquals(30292, dataIn.readUnsignedShort());
assertEquals(60584, dataIn.readUnsignedShort());
assertEquals(60584, dataIn.readUnsignedShort());
assertEquals(60584, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
// EOF
assertEquals(-1, in.read());
}
@Test
public void testRead4SPP8BPS() throws IOException {
// 4 samples per pixel, 8 bits per sample (RGBA)
byte[] data = {
(byte) 0xff, (byte) 0x00, (byte) 0x7f, 0x00, -1, -1, -1, -1, -4, -4, -4, -4, 4, 4, 4, 4,
0x7f, 0x7f, 0x7f, 0x7f, 1, 1, 1, 1, 4, 4, 4, 4, -4, -4, -4, -4,
};
InputStream stream = new HorizontalDeDifferencingStream(new ByteArrayInputStream(data), 4, 4, 8, ByteOrder.BIG_ENDIAN);
// Row 1
assertEquals(0xff, stream.read());
assertEquals(0x00, stream.read());
assertEquals(0x7f, stream.read());
assertEquals(0x00, stream.read());
assertEquals(0xfe, stream.read());
assertEquals(0xff, stream.read());
assertEquals(0x7e, stream.read());
assertEquals(0xff, stream.read());
assertEquals(0xfa, stream.read());
assertEquals(0xfb, stream.read());
assertEquals(0x7a, stream.read());
assertEquals(0xfb, stream.read());
assertEquals(0xfe, stream.read());
assertEquals(0xff, stream.read());
assertEquals(0x7e, stream.read());
assertEquals(0xff, stream.read());
// Row 2
assertEquals(0x7f, stream.read());
assertEquals(0x7f, stream.read());
assertEquals(0x7f, stream.read());
assertEquals(0x7f, stream.read());
assertEquals(0x80, stream.read());
assertEquals(0x80, stream.read());
assertEquals(0x80, stream.read());
assertEquals(0x80, stream.read());
assertEquals(0x84, stream.read());
assertEquals(0x84, stream.read());
assertEquals(0x84, stream.read());
assertEquals(0x84, stream.read());
assertEquals(0x80, stream.read());
assertEquals(0x80, stream.read());
assertEquals(0x80, stream.read());
assertEquals(0x80, stream.read());
// EOF
assertEquals(-1, stream.read());
}
@Test
public void testReadArray4SPP8BPS() throws IOException {
// 4 samples per pixel, 8 bits per sample (RGBA)
byte[] data = {
(byte) 0xff, (byte) 0x00, (byte) 0x7f, 0x00, -1, -1, -1, -1, -4, -4, -4, -4, 4, 4, 4, 4,
0x7f, 0x7f, 0x7f, 0x7f, 1, 1, 1, 1, 4, 4, 4, 4, -4, -4, -4, -4,
};
InputStream stream = new HorizontalDeDifferencingStream(new ByteArrayInputStream(data), 4, 4, 8, ByteOrder.BIG_ENDIAN);
byte[] result = new byte[data.length];
new DataInputStream(stream).readFully(result);
assertArrayEquals(
new byte[] {
(byte) 0xff, 0x00, 0x7f, 0x00,
(byte) 0xfe, (byte) 0xff, 0x7e, (byte) 0xff,
(byte) 0xfa, (byte) 0xfb, 0x7a, (byte) 0xfb,
(byte) 0xfe, (byte) 0xff, 0x7e, (byte) 0xff,
0x7f, 0x7f, 0x7f, 0x7f,
(byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80,
(byte) 0x84, (byte) 0x84, (byte) 0x84, (byte) 0x84,
(byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80,
},
result
);
// EOF
assertEquals(-1, stream.read(new byte[16]));
assertEquals(-1, stream.read());
}
}
@@ -30,7 +30,6 @@ import com.twelvemonkeys.io.enc.Decoder;
import com.twelvemonkeys.io.enc.DecoderAbstractTestCase;
import com.twelvemonkeys.io.enc.DecoderStream;
import com.twelvemonkeys.io.enc.Encoder;
import org.junit.Ignore;
import org.junit.Test;
import java.io.ByteArrayInputStream;
@@ -60,24 +59,15 @@ public class LZWDecoderTest extends DecoderAbstractTestCase {
@Test
public void testShortBitReversedStream() throws IOException {
InputStream stream = new DecoderStream(getClass().getResourceAsStream("/lzw/lzw-short.bin"), new LZWDecoder(true), 128);
InputStream stream = new DecoderStream(getClass().getResourceAsStream("/lzw/lzw-short.bin"), LZWDecoder.create(true), 128);
InputStream unpacked = new ByteArrayInputStream(new byte[512 * 3 * 5]); // Should be all 0's
assertSameStreamContents(unpacked, stream);
}
@Ignore("Known issue")
@Test
public void testShortBitReversedStreamLine45To49() throws IOException {
InputStream stream = new DecoderStream(getClass().getResourceAsStream("/lzw/lzw-short-45-49.bin"), new LZWDecoder(true), 128);
InputStream unpacked = getClass().getResourceAsStream("/lzw/unpacked-short-45-49.bin");
assertSameStreamContents(unpacked, stream);
}
@Test
public void testLongStream() throws IOException {
InputStream stream = new DecoderStream(getClass().getResourceAsStream("/lzw/lzw-long.bin"), new LZWDecoder(), 1024);
InputStream stream = new DecoderStream(getClass().getResourceAsStream("/lzw/lzw-long.bin"), LZWDecoder.create(false), 1024);
InputStream unpacked = getClass().getResourceAsStream("/lzw/unpacked-long.bin");
assertSameStreamContents(unpacked, stream);
@@ -111,7 +101,7 @@ public class LZWDecoderTest extends DecoderAbstractTestCase {
@Override
public Decoder createDecoder() {
return new LZWDecoder();
return LZWDecoder.create(false);
}
@Override
@@ -52,10 +52,15 @@ public class TIFFImageReaderTest extends ImageReaderAbstractTestCase<TIFFImageRe
new TestData(getClassLoaderResource("/tiff/sm_colors_tile.tif"), new Dimension(64, 64)), // RGB, uncompressed, tiled
new TestData(getClassLoaderResource("/tiff/sm_colors_pb_tile.tif"), new Dimension(64, 64)), // RGB, PackBits compressed, tiled
new TestData(getClassLoaderResource("/tiff/galaxy.tif"), new Dimension(965, 965)), // RGB, LZW compressed
new TestData(getClassLoaderResource("/tiff/quad-lzw.tif"), new Dimension(512, 384)), // RGB, Old spec (reversed) LZW compressed, tiled
new TestData(getClassLoaderResource("/tiff/bali.tif"), new Dimension(725, 489)), // Palette-based, LZW compressed
new TestData(getClassLoaderResource("/tiff/f14.tif"), new Dimension(640, 480)), // Gray, uncompressed
new TestData(getClassLoaderResource("/tiff/marbles.tif"), new Dimension(1419, 1001)), // RGB, LZW compressed w/predictor
new TestData(getClassLoaderResource("/tiff/chifley_logo.tif"), new Dimension(591, 177)) // CMYK, uncompressed
new TestData(getClassLoaderResource("/tiff/chifley_logo.tif"), new Dimension(591, 177)), // CMYK, uncompressed
new TestData(getClassLoaderResource("/tiff/ycbcr-cat.tif"), new Dimension(250, 325)), // YCbCr, LZW compressed
new TestData(getClassLoaderResource("/tiff/quad-jpeg.tif"), new Dimension(512, 384)), // YCbCr, JPEG compressed, striped
new TestData(getClassLoaderResource("/tiff/smallliz.tif"), new Dimension(160, 160)), // YCbCr, Old-Style JPEG compressed (full JFIF stream)
new TestData(getClassLoaderResource("/tiff/zackthecat.tif"), new Dimension(234, 213)) // YCbCr, Old-Style JPEG compressed (tables, no JFIF stream)
);
}
@@ -88,4 +93,6 @@ public class TIFFImageReaderTest extends ImageReaderAbstractTestCase<TIFFImageRe
protected List<String> getMIMETypes() {
return Arrays.asList("image/tiff");
}
// TODO: Test YCbCr colors
}
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2012, Harald Kuhr
* Copyright (c) 2013, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -28,20 +28,24 @@
package com.twelvemonkeys.imageio.plugins.tiff;
import com.twelvemonkeys.io.enc.Decoder;
import com.twelvemonkeys.io.InputStreamAbstractTestCase;
import org.junit.Ignore;
import java.io.IOException;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
/**
* CCITT Group 3 One-Dimensional (G31D) "No EOLs" Decoder.
* YCbCrUpsamplerStreamTest
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: G31DDecoder.java,v 1.0 23.05.12 15:55 haraldk Exp$
* @version $Id: YCbCrUpsamplerStreamTest.java,v 1.0 31.01.13 14:35 haraldk Exp$
*/
final class G31DDecoder implements Decoder {
public int decode(final InputStream stream, final byte[] buffer) throws IOException {
throw new UnsupportedOperationException("Method decode not implemented"); // TODO: Implement
@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);
}
}
Binary file not shown.
Binary file not shown.
+3 -2
View File
@@ -45,7 +45,7 @@
<module>imageio-jmagick</module>
<!-- Test cases for the JRE provided ImageIO plugins -->
<module>imageio-reference</module>
<module>imageio-reference</module>
</modules>
<properties>
@@ -90,6 +90,7 @@
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<version>1.8.5</version>
<scope>test</scope>
</dependency>
</dependencies>
@@ -115,5 +116,5 @@
<scope>test</scope>
</dependency>
</dependencies>
</dependencyManagement>
</dependencyManagement>
</project>