diff --git a/common/common-image/src/main/java/com/twelvemonkeys/image/BrightnessContrastFilter.java b/common/common-image/src/main/java/com/twelvemonkeys/image/BrightnessContrastFilter.java index da7e8a48..2b4fb736 100755 --- a/common/common-image/src/main/java/com/twelvemonkeys/image/BrightnessContrastFilter.java +++ b/common/common-image/src/main/java/com/twelvemonkeys/image/BrightnessContrastFilter.java @@ -66,7 +66,7 @@ public class BrightnessContrastFilter extends RGBImageFilter { canFilterIndexColorModel = true; } - // Use a precalculated lookup table for performace + // Use a pre-calculated lookup table for performance private final int[] LUT; /** @@ -149,7 +149,6 @@ public class BrightnessContrastFilter extends RGBImageFilter { * * @return the filtered pixel value in the default color space */ - public int filterRGB(int pX, int pY, int pARGB) { // Get color components int r = pARGB >> 16 & 0xFF; diff --git a/common/common-image/src/main/java/com/twelvemonkeys/image/BufferedImageFactory.java b/common/common-image/src/main/java/com/twelvemonkeys/image/BufferedImageFactory.java index 69de39b3..d6b097ff 100755 --- a/common/common-image/src/main/java/com/twelvemonkeys/image/BufferedImageFactory.java +++ b/common/common-image/src/main/java/com/twelvemonkeys/image/BufferedImageFactory.java @@ -259,11 +259,9 @@ public final class BufferedImageFactory { sourceProperties = null; } - private void processProgress(int mScanline) { + private void processProgress(int scanline) { if (listeners != null) { - int percent = 100 * mScanline / height; - - //System.out.println("Progress: " + percent + "%"); + int percent = 100 * scanline / height; if (percent > percentageDone) { percentageDone = percent; @@ -323,7 +321,7 @@ public final class BufferedImageFactory { * pixels. The conversion is done, by masking out the * higher 16 bits of the {@code int}. * - * For eny given {@code int}, the {@code short} value is computed as + * For any given {@code int}, the {@code short} value is computed as * follows: *
{@code * short value = (short) (intValue & 0x0000ffff); @@ -334,9 +332,11 @@ public final class BufferedImageFactory { */ private static short[] toShortPixels(int[] pPixels) { short[] pixels = new short[pPixels.length]; + for (int i = 0; i < pixels.length; i++) { pixels[i] = (short) (pPixels[i] & 0xffff); } + return pixels; } @@ -507,24 +507,11 @@ public final class BufferedImageFactory { } public void setPixels(int pX, int pY, int pWidth, int pHeight, ColorModel pModel, byte[] pPixels, int pOffset, int pScanSize) { - /*if (pModel.getPixelSize() < 8) { - // Byte packed - setPixelsImpl(pX, pY, pWidth, pHeight, pModel, toBytePackedPixels(pPixels, pModel.getPixelSize()), pOffset, pScanSize); - } - /* - else if (pModel.getPixelSize() > 8) { - // Byte interleaved - setPixelsImpl(pX, pY, pWidth, pHeight, pModel, toByteInterleavedPixels(pPixels), pOffset, pScanSize); - } - */ - //else { - // Default, pixelSize == 8, one byte pr pixel - setPixelsImpl(pX, pY, pWidth, pHeight, pModel, pPixels, pOffset, pScanSize); - //} + setPixelsImpl(pX, pY, pWidth, pHeight, pModel, pPixels, pOffset, pScanSize); } public void setPixels(int pX, int pY, int pWeigth, int pHeight, ColorModel pModel, int[] pPixels, int pOffset, int pScanSize) { - if (ImageUtil.getTransferType(pModel) == DataBuffer.TYPE_USHORT) { + if (pModel.getTransferType() == DataBuffer.TYPE_USHORT) { // NOTE: Workaround for limitation in ImageConsumer API // Convert int[] to short[], to be compatible with the ColorModel setPixelsImpl(pX, pY, pWeigth, pHeight, pModel, toShortPixels(pPixels), pOffset, pScanSize); @@ -538,4 +525,86 @@ public final class BufferedImageFactory { sourceProperties = pProperties; } } + + /* + public static void main(String[] args) throws InterruptedException { + Image image = Toolkit.getDefaultToolkit().createImage(args[0]); + System.err.printf("image: %s (which is %sa buffered image)\n", image, image instanceof BufferedImage ? "" : "not "); + + int warmUpLoops = 500; + int testLoops = 100; + + for (int i = 0; i < warmUpLoops; i++) { + // Warm up... + convertUsingFactory(image); + convertUsingPixelGrabber(image); + convertUsingPixelGrabberNaive(image); + } + + BufferedImage bufferedImage = null; + long start = System.currentTimeMillis(); + for (int i = 0; i < testLoops; i++) { + bufferedImage = convertUsingFactory(image); + } + System.err.printf("Conversion time (factory): %f ms (image: %s)\n", (System.currentTimeMillis() - start) / (double) testLoops, bufferedImage); + + start = System.currentTimeMillis(); + for (int i = 0; i < testLoops; i++) { + bufferedImage = convertUsingPixelGrabber(image); + } + System.err.printf("Conversion time (grabber): %f ms (image: %s)\n", (System.currentTimeMillis() - start) / (double) testLoops, bufferedImage); + + start = System.currentTimeMillis(); + for (int i = 0; i < testLoops; i++) { + bufferedImage = convertUsingPixelGrabberNaive(image); + } + System.err.printf("Conversion time (naive g): %f ms (image: %s)\n", (System.currentTimeMillis() - start) / (double) testLoops, bufferedImage); + } + + private static BufferedImage convertUsingPixelGrabberNaive(Image image) throws InterruptedException { + // NOTE: It does not matter if we wait for the image or not, the time is about the same as it will only happen once + if ((image.getWidth(null) < 0 || image.getHeight(null) < 0) && !ImageUtil.waitForImage(image)) { + System.err.printf("Could not get image dimensions for image %s\n", image.getSource()); + } + + int w = image.getWidth(null); + int h = image.getHeight(null); + PixelGrabber grabber = new PixelGrabber(image, 0, 0, w, h, true); // force RGB + grabber.grabPixels(); + + // Following casts are safe, as we force RGB in the pixel grabber + int[] pixels = (int[]) grabber.getPixels(); + + BufferedImage bufferedImage = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); +// bufferedImage.setRGB(0, 0, w, h, pixels, 0, w); + bufferedImage.getRaster().setDataElements(0, 0, w, h, pixels); + + return bufferedImage; + } + + private static BufferedImage convertUsingPixelGrabber(Image image) throws InterruptedException { + // NOTE: It does not matter if we wait for the image or not, the time is about the same as it will only happen once + if ((image.getWidth(null) < 0 || image.getHeight(null) < 0) && !ImageUtil.waitForImage(image)) { + System.err.printf("Could not get image dimensions for image %s\n", image.getSource()); + } + + int w = image.getWidth(null); + int h = image.getHeight(null); + PixelGrabber grabber = new PixelGrabber(image, 0, 0, w, h, true); // force RGB + grabber.grabPixels(); + + // Following casts are safe, as we force RGB in the pixel grabber +// DirectColorModel cm = (DirectColorModel) grabber.getColorModel(); + DirectColorModel cm = (DirectColorModel) ColorModel.getRGBdefault(); + int[] pixels = (int[]) grabber.getPixels(); + + WritableRaster raster = Raster.createPackedRaster(new DataBufferInt(pixels, pixels.length), w, h, w, cm.getMasks(), null); + + return new BufferedImage(cm, raster, cm.isAlphaPremultiplied(), null); + } + + private static BufferedImage convertUsingFactory(Image image) { + return new BufferedImageFactory(image).getBufferedImage(); + } + */ } \ No newline at end of file diff --git a/common/common-image/src/main/java/com/twelvemonkeys/image/BufferedImageIcon.java b/common/common-image/src/main/java/com/twelvemonkeys/image/BufferedImageIcon.java index d4339b14..649666ba 100755 --- a/common/common-image/src/main/java/com/twelvemonkeys/image/BufferedImageIcon.java +++ b/common/common-image/src/main/java/com/twelvemonkeys/image/BufferedImageIcon.java @@ -53,11 +53,15 @@ public class BufferedImageIcon implements Icon { } public BufferedImageIcon(BufferedImage pImage, int pWidth, int pHeight) { + this(pImage, pWidth, pHeight, pImage.getWidth() == pWidth && pImage.getHeight() == pHeight); + } + + public BufferedImageIcon(BufferedImage pImage, int pWidth, int pHeight, boolean useFastRendering) { image = Validate.notNull(pImage, "image"); width = Validate.isTrue(pWidth > 0, pWidth, "width must be positive: %d"); height = Validate.isTrue(pHeight > 0, pHeight, "height must be positive: %d"); - fast = image.getWidth() == width && image.getHeight() == height; + fast = useFastRendering; } public int getIconHeight() { diff --git a/common/common-image/src/main/java/com/twelvemonkeys/image/DiffusionDither.java b/common/common-image/src/main/java/com/twelvemonkeys/image/DiffusionDither.java index 363b9542..48664fcc 100755 --- a/common/common-image/src/main/java/com/twelvemonkeys/image/DiffusionDither.java +++ b/common/common-image/src/main/java/com/twelvemonkeys/image/DiffusionDither.java @@ -292,20 +292,20 @@ public class DiffusionDither implements BufferedImageOp, RasterOp { // When reference for column, add 1 to reference as this buffer is // offset from actual column position by one to allow FS to not check // left/right edge conditions - int[][] mCurrErr = new int[width + 2][3]; - int[][] mNextErr = new int[width + 2][3]; + int[][] currErr = new int[width + 2][3]; + int[][] nextErr = new int[width + 2][3]; // Random errors in [-1 .. 1] - for first row for (int i = 0; i < width + 2; i++) { // Note: This is broken for the strange cases where nextInt returns Integer.MIN_VALUE /* - mCurrErr[i][0] = (Math.abs(RANDOM.nextInt()) % (FS_SCALE * 2)) - FS_SCALE; - mCurrErr[i][1] = (Math.abs(RANDOM.nextInt()) % (FS_SCALE * 2)) - FS_SCALE; - mCurrErr[i][2] = (Math.abs(RANDOM.nextInt()) % (FS_SCALE * 2)) - FS_SCALE; + currErr[i][0] = (Math.abs(RANDOM.nextInt()) % (FS_SCALE * 2)) - FS_SCALE; + currErr[i][1] = (Math.abs(RANDOM.nextInt()) % (FS_SCALE * 2)) - FS_SCALE; + currErr[i][2] = (Math.abs(RANDOM.nextInt()) % (FS_SCALE * 2)) - FS_SCALE; */ - mCurrErr[i][0] = RANDOM.nextInt(FS_SCALE * 2) - FS_SCALE; - mCurrErr[i][1] = RANDOM.nextInt(FS_SCALE * 2) - FS_SCALE; - mCurrErr[i][2] = RANDOM.nextInt(FS_SCALE * 2) - FS_SCALE; + currErr[i][0] = RANDOM.nextInt(FS_SCALE * 2) - FS_SCALE; + currErr[i][1] = RANDOM.nextInt(FS_SCALE * 2) - FS_SCALE; + currErr[i][2] = RANDOM.nextInt(FS_SCALE * 2) - FS_SCALE; } // Temp buffers @@ -318,10 +318,10 @@ public class DiffusionDither implements BufferedImageOp, RasterOp { // Loop through image data for (int y = 0; y < height; y++) { // Clear out next error rows for colour errors - for (int i = mNextErr.length; --i >= 0;) { - mNextErr[i][0] = 0; - mNextErr[i][1] = 0; - mNextErr[i][2] = 0; + for (int i = nextErr.length; --i >= 0;) { + nextErr[i][0] = 0; + nextErr[i][1] = 0; + nextErr[i][2] = 0; } // Set up start column and limit @@ -348,7 +348,7 @@ public class DiffusionDither implements BufferedImageOp, RasterOp { for (int i = 0; i < 3; i++) { // Make a 28.4 FP number, add Error (with fraction), // rounding and truncate to int - inRGB[i] = ((inRGB[i] << 4) + mCurrErr[x + 1][i] + 0x08) >> 4; + inRGB[i] = ((inRGB[i] << 4) + currErr[x + 1][i] + 0x08) >> 4; // Clamp if (inRGB[i] > 255) { @@ -384,26 +384,26 @@ public class DiffusionDither implements BufferedImageOp, RasterOp { if (forward) { // Row 1 (y) // Update error in this pixel (x + 1) - mCurrErr[x + 2][0] += diff[0] * 7; - mCurrErr[x + 2][1] += diff[1] * 7; - mCurrErr[x + 2][2] += diff[2] * 7; + currErr[x + 2][0] += diff[0] * 7; + currErr[x + 2][1] += diff[1] * 7; + currErr[x + 2][2] += diff[2] * 7; // Row 2 (y + 1) // Update error in this pixel (x - 1) - mNextErr[x][0] += diff[0] * 3; - mNextErr[x][1] += diff[1] * 3; - mNextErr[x][2] += diff[2] * 3; + nextErr[x][0] += diff[0] * 3; + nextErr[x][1] += diff[1] * 3; + nextErr[x][2] += diff[2] * 3; // Update error in this pixel (x) - mNextErr[x + 1][0] += diff[0] * 5; - mNextErr[x + 1][1] += diff[1] * 5; - mNextErr[x + 1][2] += diff[2] * 5; + nextErr[x + 1][0] += diff[0] * 5; + nextErr[x + 1][1] += diff[1] * 5; + nextErr[x + 1][2] += diff[2] * 5; // Update error in this pixel (x + 1) // TODO: Consider calculating this using // error term = error - sum(error terms 1, 2 and 3) // See Computer Graphics (Foley et al.), p. 573 - mNextErr[x + 2][0] += diff[0]; // * 1; - mNextErr[x + 2][1] += diff[1]; // * 1; - mNextErr[x + 2][2] += diff[2]; // * 1; + nextErr[x + 2][0] += diff[0]; // * 1; + nextErr[x + 2][1] += diff[1]; // * 1; + nextErr[x + 2][2] += diff[2]; // * 1; // Next x++; @@ -417,26 +417,26 @@ public class DiffusionDither implements BufferedImageOp, RasterOp { else { // Row 1 (y) // Update error in this pixel (x - 1) - mCurrErr[x][0] += diff[0] * 7; - mCurrErr[x][1] += diff[1] * 7; - mCurrErr[x][2] += diff[2] * 7; + currErr[x][0] += diff[0] * 7; + currErr[x][1] += diff[1] * 7; + currErr[x][2] += diff[2] * 7; // Row 2 (y + 1) // Update error in this pixel (x + 1) - mNextErr[x + 2][0] += diff[0] * 3; - mNextErr[x + 2][1] += diff[1] * 3; - mNextErr[x + 2][2] += diff[2] * 3; + nextErr[x + 2][0] += diff[0] * 3; + nextErr[x + 2][1] += diff[1] * 3; + nextErr[x + 2][2] += diff[2] * 3; // Update error in this pixel (x) - mNextErr[x + 1][0] += diff[0] * 5; - mNextErr[x + 1][1] += diff[1] * 5; - mNextErr[x + 1][2] += diff[2] * 5; + nextErr[x + 1][0] += diff[0] * 5; + nextErr[x + 1][1] += diff[1] * 5; + nextErr[x + 1][2] += diff[2] * 5; // Update error in this pixel (x - 1) // TODO: Consider calculating this using // error term = error - sum(error terms 1, 2 and 3) // See Computer Graphics (Foley et al.), p. 573 - mNextErr[x][0] += diff[0]; // * 1; - mNextErr[x][1] += diff[1]; // * 1; - mNextErr[x][2] += diff[2]; // * 1; + nextErr[x][0] += diff[0]; // * 1; + nextErr[x][1] += diff[1]; // * 1; + nextErr[x][2] += diff[2]; // * 1; // Previous x--; @@ -450,9 +450,9 @@ public class DiffusionDither implements BufferedImageOp, RasterOp { // Make next error info current for next iteration int[][] temperr; - temperr = mCurrErr; - mCurrErr = mNextErr; - mNextErr = temperr; + temperr = currErr; + currErr = nextErr; + nextErr = temperr; // Toggle direction if (alternateScans) { diff --git a/common/common-image/src/main/java/com/twelvemonkeys/image/ImageUtil.java b/common/common-image/src/main/java/com/twelvemonkeys/image/ImageUtil.java index 9f08f0fd..c3d2f87d 100644 --- a/common/common-image/src/main/java/com/twelvemonkeys/image/ImageUtil.java +++ b/common/common-image/src/main/java/com/twelvemonkeys/image/ImageUtil.java @@ -39,7 +39,7 @@ import java.util.Hashtable; * * @author Harald Kuhr * @author last modified by $Author: haku $ - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/ImageUtil.java#3 $ + * @version $Id: common/common-image/src/main/java/com/twelvemonkeys/image/ImageUtil.java#3 $ */ public final class ImageUtil { // TODO: Split palette generation out, into ColorModel classes (?) @@ -175,19 +175,12 @@ public final class ImageUtil { /** Our static image tracker */ private static MediaTracker sTracker = new MediaTracker(NULL_COMPONENT); - //private static Object sTrackerMutex = new Object(); - - /** Image id used by the image tracker */ - //private static int sTrackerId = 0; /** */ protected static final AffineTransform IDENTITY_TRANSFORM = new AffineTransform(); /** */ protected static final Point LOCATION_UPPER_LEFT = new Point(0, 0); - /** */ - private static final boolean COLORMODEL_TRANSFERTYPE_SUPPORTED = isColorModelTransferTypeSupported(); - /** */ private static final GraphicsConfiguration DEFAULT_CONFIGURATION = getDefaultGraphicsConfiguration(); @@ -209,22 +202,6 @@ public final class ImageUtil { private ImageUtil() { } - /** - * Tests if {@code ColorModel} has a {@code getTransferType} method. - * - * @return {@code true} if {@code ColorModel} has a - * {@code getTransferType} method - */ - private static boolean isColorModelTransferTypeSupported() { - try { - ColorModel.getRGBdefault().getTransferType(); - return true; - } - catch (Throwable t) { - return false; - } - } - /** * Converts the {@code RenderedImage} to a {@code BufferedImage}. * The new image will have the same {@code ColorModel}, @@ -382,7 +359,7 @@ public final class ImageUtil { /** * Creates a copy of the given image. The image will have the same - * colormodel and raster type, but will not share image (pixel) data. + * color model and raster type, but will not share image (pixel) data. * * @param pImage the image to clone. * @@ -412,11 +389,11 @@ public final class ImageUtil { *

* This method is optimized for the most common cases of {@code ColorModel} * and pixel data combinations. The raster's backing {@code DataBuffer} is - * created directly from the pixel data, as this is faster and with more + * created directly from the pixel data, as this is faster and more * resource-friendly than using * {@code ColorModel.createCompatibleWritableRaster(w, h)}. *

- * For unknown combinations, the method will fallback to using + * For uncommon combinations, the method will fallback to using * {@code ColorModel.createCompatibleWritableRaster(w, h)} and * {@code WritableRaster.setDataElements(w, h, pixels)} *

@@ -442,8 +419,8 @@ public final class ImageUtil { */ static WritableRaster createRaster(int pWidth, int pHeight, Object pPixels, ColorModel pColorModel) { // NOTE: This is optimized code for most common cases. - // We create a DataBuffer with the array from grabber.getPixels() - // directly, and creating a raster based on the ColorModel. + // We create a DataBuffer from the pixel array directly, + // and creating a raster based on the DataBuffer and ColorModel. // Creating rasters this way is faster and more resource-friendly, as // cm.createCompatibleWritableRaster allocates an // "empty" DataBuffer with a storage array of w*h. This array is @@ -457,14 +434,12 @@ public final class ImageUtil { if (pPixels instanceof int[]) { int[] data = (int[]) pPixels; buffer = new DataBufferInt(data, data.length); - //bands = data.length / (w * h); bands = pColorModel.getNumComponents(); } else if (pPixels instanceof short[]) { short[] data = (short[]) pPixels; buffer = new DataBufferUShort(data, data.length); bands = data.length / (pWidth * pHeight); - //bands = cm.getNumComponents(); } else if (pPixels instanceof byte[]) { byte[] data = (byte[]) pPixels; @@ -477,47 +452,30 @@ public final class ImageUtil { else { bands = data.length / (pWidth * pHeight); } - - //bands = pColorModel.getNumComponents(); - //System.out.println("Pixels: " + data.length + " (" + buffer.getSize() + ")"); - //System.out.println("w*h*bands: " + (pWidth * pHeight * bands)); - //System.out.println("Bands: " + bands); - //System.out.println("Numcomponents: " + pColorModel.getNumComponents()); } else { - //System.out.println("Fallback!"); // Fallback mode, slower & requires more memory, but compatible bands = -1; - // Create raster from colormodel, w and h + // Create raster from color model, w and h raster = pColorModel.createCompatibleWritableRaster(pWidth, pHeight); raster.setDataElements(0, 0, pWidth, pHeight, pPixels); // Note: This is known to throw ClassCastExceptions.. } - //System.out.println("Bands: " + bands); - //System.out.println("Pixels: " + pixels.getClass() + " length: " + buffer.getSize()); - //System.out.println("Needed Raster: " + cm.createCompatibleWritableRaster(1, 1)); - if (raster == null) { - //int bits = cm.getPixelSize(); - //if (bits > 4) { if (pColorModel instanceof IndexColorModel && isIndexedPacked((IndexColorModel) pColorModel)) { - //System.out.println("Creating packed indexed model"); raster = Raster.createPackedRaster(buffer, pWidth, pHeight, pColorModel.getPixelSize(), LOCATION_UPPER_LEFT); } else if (pColorModel instanceof PackedColorModel) { - //System.out.println("Creating packed model"); PackedColorModel pcm = (PackedColorModel) pColorModel; raster = Raster.createPackedRaster(buffer, pWidth, pHeight, pWidth, pcm.getMasks(), LOCATION_UPPER_LEFT); } else { - //System.out.println("Creating interleaved model"); // (A)BGR order... For TYPE_3BYTE_BGR/TYPE_4BYTE_ABGR/TYPE_4BYTE_ABGR_PRE. int[] bandsOffsets = new int[bands]; for (int i = 0; i < bands;) { bandsOffsets[i] = bands - (++i); } - //System.out.println("zzz Data array: " + buffer.getSize()); raster = Raster.createInterleavedRaster(buffer, pWidth, pHeight, pWidth * bands, bands, bandsOffsets, LOCATION_UPPER_LEFT); } @@ -849,11 +807,13 @@ public final class ImageUtil { BufferedImage temp = new BufferedImage(cm, raster, cm.isAlphaPremultiplied(), null); if (cm instanceof IndexColorModel && pHints == Image.SCALE_SMOOTH) { + // TODO: DiffusionDither does not support transparency at the moment, this will create bad results new DiffusionDither((IndexColorModel) cm).filter(scaled, temp); } else { drawOnto(temp, scaled); } + scaled = temp; //long end = System.currentTimeMillis(); //System.out.println("Time: " + (end - start) + " ms"); @@ -1140,26 +1100,26 @@ public final class ImageUtil { * Sharpens an image using a convolution matrix. * The sharpen kernel used, is defined by the following 3 by 3 matrix: * - * - * - * - * - * + * + * + * + * + * *
0.0-{@code pAmmount}0.0
-{@code pAmmount}4.0 * {@code pAmmount} + 1.0-{@code pAmmount}
0.0-{@code pAmmount}0.0
0.0-{@code pAmount}0.0
-{@code pAmount}4.0 * {@code pAmount} + 1.0-{@code pAmount}
0.0-{@code pAmount}0.0
* * @param pOriginal the BufferedImage to sharpen - * @param pAmmount the ammount of sharpening + * @param pAmount the amount of sharpening * * @return a BufferedImage, containing the sharpened image. */ - public static BufferedImage sharpen(BufferedImage pOriginal, float pAmmount) { - if (pAmmount == 0f) { + public static BufferedImage sharpen(BufferedImage pOriginal, float pAmount) { + if (pAmount == 0f) { return pOriginal; } // Create the convolution matrix float[] data = new float[] { - 0.0f, -pAmmount, 0.0f, -pAmmount, 4f * pAmmount + 1f, -pAmmount, 0.0f, -pAmmount, 0.0f + 0.0f, -pAmount, 0.0f, -pAmount, 4f * pAmount + 1f, -pAmount, 0.0f, -pAmount, 0.0f }; // Do the filtering @@ -1185,7 +1145,7 @@ public final class ImageUtil { * Creates a blurred version of the given image. * * @param pOriginal the original image - * @param pRadius the ammount to blur + * @param pRadius the amount to blur * * @return a new {@code BufferedImage} with a blurred version of the given image */ @@ -1198,18 +1158,18 @@ public final class ImageUtil { // See: http://en.wikipedia.org/wiki/Gaussian_blur#Implementation // Also see http://www.jhlabs.com/ip/blurring.html - // TODO: Rethink... Fixed ammount and scale matrix instead? -// pAmmount = 1f - pAmmount; -// float pAmmount = 1f - pRadius; + // TODO: Rethink... Fixed amount and scale matrix instead? +// pAmount = 1f - pAmount; +// float pAmount = 1f - pRadius; // -// // Normalize ammount -// float normAmt = (1f - pAmmount) / 24; +// // Normalize amount +// float normAmt = (1f - pAmount) / 24; // // // Create the convolution matrix // float[] data = new float[] { // normAmt / 2, normAmt, normAmt, normAmt, normAmt / 2, // normAmt, normAmt, normAmt * 2, normAmt, normAmt, -// normAmt, normAmt * 2, pAmmount, normAmt * 2, normAmt, +// normAmt, normAmt * 2, pAmount, normAmt * 2, normAmt, // normAmt, normAmt, normAmt * 2, normAmt, normAmt, // normAmt / 2, normAmt, normAmt, normAmt, normAmt / 2 // }; @@ -1391,18 +1351,18 @@ public final class ImageUtil { * Changes the contrast of the image * * @param pOriginal the {@code Image} to change - * @param pAmmount the ammount of contrast in the range [-1.0..1.0]. + * @param pAmount the amount of contrast in the range [-1.0..1.0]. * * @return an {@code Image}, containing the contrasted image. */ - public static Image contrast(Image pOriginal, float pAmmount) { + public static Image contrast(Image pOriginal, float pAmount) { // No change, return original - if (pAmmount == 0f) { + if (pAmount == 0f) { return pOriginal; } // Create filter - RGBImageFilter filter = new BrightnessContrastFilter(0f, pAmmount); + RGBImageFilter filter = new BrightnessContrastFilter(0f, pAmount); // Return contrast adjusted image return filter(pOriginal, filter); @@ -1413,18 +1373,18 @@ public final class ImageUtil { * Changes the brightness of the original image. * * @param pOriginal the {@code Image} to change - * @param pAmmount the ammount of brightness in the range [-2.0..2.0]. + * @param pAmount the amount of brightness in the range [-2.0..2.0]. * * @return an {@code Image} */ - public static Image brightness(Image pOriginal, float pAmmount) { + public static Image brightness(Image pOriginal, float pAmount) { // No change, return original - if (pAmmount == 0f) { + if (pAmount == 0f) { return pOriginal; } // Create filter - RGBImageFilter filter = new BrightnessContrastFilter(pAmmount, 0f); + RGBImageFilter filter = new BrightnessContrastFilter(pAmount, 0f); // Return brightness adjusted image return filter(pOriginal, filter); @@ -1465,7 +1425,7 @@ public final class ImageUtil { } /** - * Tries to use H/W-accellerated code for an image for display purposes. + * Tries to use H/W-accelerated code for an image for display purposes. * Note that transparent parts of the image might be replaced by solid * color. Additional image information not used by the current diplay * hardware may be discarded, like extra bith depth etc. @@ -1478,7 +1438,7 @@ public final class ImageUtil { } /** - * Tries to use H/W-accellerated code for an image for display purposes. + * Tries to use H/W-accelerated code for an image for display purposes. * Note that transparent parts of the image might be replaced by solid * color. Additional image information not used by the current diplay * hardware may be discarded, like extra bith depth etc. @@ -1494,7 +1454,7 @@ public final class ImageUtil { } /** - * Tries to use H/W-accellerated code for an image for display purposes. + * Tries to use H/W-accelerated code for an image for display purposes. * Note that transparent parts of the image will be replaced by solid * color. Additional image information not used by the current diplay * hardware may be discarded, like extra bith depth etc. @@ -1784,7 +1744,7 @@ public final class ImageUtil { * @param pTimeOut the time to wait, in milliseconds. * * @return true if the image was loaded successfully, false if an error - * occured, or the wait was interrupted. + * occurred, or the wait was interrupted. * * @see #waitForImages(Image[],long) */ @@ -1799,7 +1759,7 @@ public final class ImageUtil { * @param pImages an array of Image objects to wait for. * * @return true if the images was loaded successfully, false if an error - * occured, or the wait was interrupted. + * occurred, or the wait was interrupted. * * @see #waitForImages(Image[],long) */ @@ -1815,7 +1775,7 @@ public final class ImageUtil { * @param pTimeOut the time to wait, in milliseconds * * @return true if the images was loaded successfully, false if an error - * occured, or the wait was interrupted. + * occurred, or the wait was interrupted. */ public static boolean waitForImages(Image[] pImages, long pTimeOut) { // TODO: Need to make sure that we don't wait for the same image many times @@ -1825,13 +1785,6 @@ public final class ImageUtil { // Create a local id for use with the mediatracker int imageId; - // NOTE: The synchronization throws IllegalMonitorStateException if - // using JIT on J2SE 1.2 (tested version Sun JRE 1.2.2_017). - // Works perfectly interpreted... Hmmm... - //synchronized (sTrackerMutex) { - //imageId = ++sTrackerId; - //} - // NOTE: This is very experimental... imageId = pImages.length == 1 ? System.identityHashCode(pImages[0]) : System.identityHashCode(pImages); @@ -1877,7 +1830,7 @@ public final class ImageUtil { } /** - * Tests wether the image has any transparent or semi-transparent pixels. + * Tests whether the image has any transparent or semi-transparent pixels. * * @param pImage the image * @param pFast if {@code true}, the method tests maximum 10 x 10 pixels, @@ -1945,7 +1898,7 @@ public final class ImageUtil { } /** - * Blends two ARGB values half and half, to create a tone inbetween. + * Blends two ARGB values half and half, to create a tone in between. * * @param pRGB1 color 1 * @param pRGB2 color 2 @@ -1958,7 +1911,7 @@ public final class ImageUtil { } /** - * Blends two colors half and half, to create a tone inbetween. + * Blends two colors half and half, to create a tone in between. * * @param pColor color 1 * @param pOther color 2 @@ -1976,7 +1929,7 @@ public final class ImageUtil { } /** - * Blends two colors, controlled by the blendfactor. + * Blends two colors, controlled by the blending factor. * A factor of {@code 0.0} will return the first color, * a factor of {@code 1.0} will return the second. * @@ -1998,50 +1951,4 @@ public final class ImageUtil { private static int clamp(float f) { return (int) f; } - /** - * PixelGrabber subclass that stores any potential properties from an image. - */ - /* - private static class MyPixelGrabber extends PixelGrabber { - private Hashtable mProps = null; - - public MyPixelGrabber(Image pImage) { - // Simply grab all pixels, do not convert to default RGB space - super(pImage, 0, 0, -1, -1, false); - } - - // Default implementation does not store the properties... - public void setProperties(Hashtable pProps) { - super.setProperties(pProps); - mProps = pProps; - } - - public Hashtable getProperties() { - return mProps; - } - } - */ - - /** - * Gets the transfer type from the given {@code ColorModel}. - *

- * NOTE: This is a workaround for missing functionality in JDK 1.2. - * - * @param pModel the color model - * @return the transfer type - * - * @throws NullPointerException if {@code pModel} is {@code null}. - * - * @see java.awt.image.ColorModel#getTransferType() - */ - public static int getTransferType(ColorModel pModel) { - if (COLORMODEL_TRANSFERTYPE_SUPPORTED) { - return pModel.getTransferType(); - } - else { - // Stupid workaround - // TODO: Create something that performs better - return pModel.createCompatibleSampleModel(1, 1).getDataType(); - } - } } \ No newline at end of file diff --git a/common/common-image/src/main/java/com/twelvemonkeys/image/IndexImage.java b/common/common-image/src/main/java/com/twelvemonkeys/image/IndexImage.java index ef97bc5c..f7419204 100755 --- a/common/common-image/src/main/java/com/twelvemonkeys/image/IndexImage.java +++ b/common/common-image/src/main/java/com/twelvemonkeys/image/IndexImage.java @@ -96,7 +96,7 @@ import java.util.Iterator; import java.util.List; /** - * This class implements an adaptive pallete generator to reduce images + * This class implements an adaptive palette generator to reduce images * to a variable number of colors. * It can also render images into fixed color pallettes. *

@@ -589,7 +589,7 @@ class IndexImage { /** * Gets an {@code IndexColorModel} from the given image. If the image has an * {@code IndexColorModel}, this will be returned. Otherwise, an {@code IndexColorModel} - * is created, using an adaptive pallete. + * is created, using an adaptive palette. * * @param pImage the image to get {@code IndexColorModel} from * @param pNumberOfColors the number of colors for the {@code IndexColorModel} @@ -637,7 +637,7 @@ class IndexImage { // We now have at least a buffered image, create model from it if (icm == null) { icm = createIndexColorModel(ImageUtil.toBuffered(image), pNumberOfColors, pHints); - } + } else if (!(icm instanceof InverseColorMapIndexColorModel)) { // If possible, use faster code icm = new InverseColorMapIndexColorModel(icm); @@ -648,7 +648,7 @@ class IndexImage { /** * Creates an {@code IndexColorModel} from the given image, using an adaptive - * pallete. + * palette. * * @param pImage the image to get {@code IndexColorModel} from * @param pNumberOfColors the number of colors for the {@code IndexColorModel} @@ -821,7 +821,7 @@ class IndexImage { /** * Converts the input image (must be {@code TYPE_INT_RGB} or * {@code TYPE_INT_ARGB}) to an indexed image. Generating an adaptive - * pallete (8 bit) from the color data in the image, and uses default + * palette (8 bit) from the color data in the image, and uses default * dither. *

* The image returned is a new image, the input image is not modified. @@ -865,7 +865,7 @@ class IndexImage { * Converts the input image (must be {@code TYPE_INT_RGB} or * {@code TYPE_INT_ARGB}) to an indexed image. If the palette image * uses an {@code IndexColorModel}, this will be used. Otherwise, generating an - * adaptive pallete (8 bit) from the given palette image. + * adaptive palette (8 bit) from the given palette image. * Dithering, transparency and color selection is controlled with the * {@code pHints}parameter. *

@@ -875,7 +875,7 @@ class IndexImage { * @param pPalette the Image to read color information from * @param pMatte the background color, used where the original image was * transparent - * @param pHints mHints that control output quality and speed. + * @param pHints hints that control output quality and speed. * @return the indexed BufferedImage. The image will be of type * {@code BufferedImage.TYPE_BYTE_INDEXED} or * {@code BufferedImage.TYPE_BYTE_BINARY}, and use an @@ -900,7 +900,7 @@ class IndexImage { /** * Converts the input image (must be {@code TYPE_INT_RGB} or * {@code TYPE_INT_ARGB}) to an indexed image. Generating an adaptive - * pallete with the given number of colors. + * palette with the given number of colors. * Dithering, transparency and color selection is controlled with the * {@code pHints}parameter. *

@@ -910,7 +910,7 @@ class IndexImage { * @param pNumberOfColors the number of colors for the image * @param pMatte the background color, used where the original image was * transparent - * @param pHints mHints that control output quality and speed. + * @param pHints hints that control output quality and speed. * @return the indexed BufferedImage. The image will be of type * {@code BufferedImage.TYPE_BYTE_INDEXED} or * {@code BufferedImage.TYPE_BYTE_BINARY}, and use an @@ -947,7 +947,7 @@ class IndexImage { /** * Converts the input image (must be {@code TYPE_INT_RGB} or * {@code TYPE_INT_ARGB}) to an indexed image. Using the supplied - * {@code IndexColorModel}'s pallete. + * {@code IndexColorModel}'s palette. * Dithering, transparency and color selection is controlled with the * {@code pHints} parameter. *

@@ -1064,7 +1064,7 @@ class IndexImage { /** * Converts the input image (must be {@code TYPE_INT_RGB} or * {@code TYPE_INT_ARGB}) to an indexed image. Generating an adaptive - * pallete with the given number of colors. + * palette with the given number of colors. * Dithering, transparency and color selection is controlled with the * {@code pHints}parameter. *

@@ -1072,7 +1072,7 @@ class IndexImage { * * @param pImage the BufferedImage to index * @param pNumberOfColors the number of colors for the image - * @param pHints mHints that control output quality and speed. + * @param pHints hints that control output quality and speed. * @return the indexed BufferedImage. The image will be of type * {@code BufferedImage.TYPE_BYTE_INDEXED} or * {@code BufferedImage.TYPE_BYTE_BINARY}, and use an @@ -1094,7 +1094,7 @@ class IndexImage { /** * Converts the input image (must be {@code TYPE_INT_RGB} or * {@code TYPE_INT_ARGB}) to an indexed image. Using the supplied - * {@code IndexColorModel}'s pallete. + * {@code IndexColorModel}'s palette. * Dithering, transparency and color selection is controlled with the * {@code pHints}parameter. *

@@ -1125,7 +1125,7 @@ class IndexImage { * Converts the input image (must be {@code TYPE_INT_RGB} or * {@code TYPE_INT_ARGB}) to an indexed image. If the palette image * uses an {@code IndexColorModel}, this will be used. Otherwise, generating an - * adaptive pallete (8 bit) from the given palette image. + * adaptive palette (8 bit) from the given palette image. * Dithering, transparency and color selection is controlled with the * {@code pHints}parameter. *

@@ -1133,7 +1133,7 @@ class IndexImage { * * @param pImage the BufferedImage to index * @param pPalette the Image to read color information from - * @param pHints mHints that control output quality and speed. + * @param pHints hints that control output quality and speed. * @return the indexed BufferedImage. The image will be of type * {@code BufferedImage.TYPE_BYTE_INDEXED} or * {@code BufferedImage.TYPE_BYTE_BINARY}, and use an @@ -1393,7 +1393,7 @@ class IndexImage { System.exit(5); } - // Create mHints + // Create hints int hints = DITHER_DEFAULT; if ("DIFFUSION".equalsIgnoreCase(dither)) { diff --git a/common/common-image/src/main/java/com/twelvemonkeys/image/InverseColorMapIndexColorModel.java b/common/common-image/src/main/java/com/twelvemonkeys/image/InverseColorMapIndexColorModel.java index 8bba6e5e..640fa002 100755 --- a/common/common-image/src/main/java/com/twelvemonkeys/image/InverseColorMapIndexColorModel.java +++ b/common/common-image/src/main/java/com/twelvemonkeys/image/InverseColorMapIndexColorModel.java @@ -30,6 +30,7 @@ package com.twelvemonkeys.image; import com.twelvemonkeys.lang.StringUtil; +import com.twelvemonkeys.lang.Validate; import java.awt.*; import java.awt.image.DataBuffer; @@ -37,7 +38,7 @@ import java.awt.image.IndexColorModel; /** * A faster implementation of {@code IndexColorModel}, that is backed by an - * inverse color-map, for fast lookups. + * inverse color-map, for fast look-ups. * * @author Harald Kuhr * @author $Author: haku $ @@ -60,19 +61,17 @@ public class InverseColorMapIndexColorModel extends IndexColorModel { * Creates an {@code InverseColorMapIndexColorModel} from an existing * {@code IndexColorModel}. * - * @param pColorModel the colormodel to create from + * @param pColorModel the color model to create from. + * @throws IllegalArgumentException if {@code pColorModel} is {@code null} */ - public InverseColorMapIndexColorModel(IndexColorModel pColorModel) { - this(pColorModel, getRGBs(pColorModel)); + public InverseColorMapIndexColorModel(final IndexColorModel pColorModel) { + this(Validate.notNull(pColorModel, "color model"), getRGBs(pColorModel)); } // NOTE: The pRGBs parameter is used to get around invoking getRGBs two // times. What is wrong with protected?! private InverseColorMapIndexColorModel(IndexColorModel pColorModel, int[] pRGBs) { - super(pColorModel.getComponentSize()[0], pColorModel.getMapSize(), - pRGBs, 0, - ImageUtil.getTransferType(pColorModel), - pColorModel.getValidPixels()); + super(pColorModel.getComponentSize()[0], pColorModel.getMapSize(), pRGBs, 0, pColorModel.getTransferType(), pColorModel.getValidPixels()); rgbs = pRGBs; mapSize = rgbs.length; @@ -82,11 +81,11 @@ public class InverseColorMapIndexColorModel extends IndexColorModel { } /** - * Creates a defensive copy of the RGB colormap in the given + * Creates a defensive copy of the RGB color map in the given * {@code IndexColorModel}. * - * @param pColorModel the indec colormodel to get RGB values from - * @return the RGB colormap + * @param pColorModel the indexed color model to get RGB values from + * @return the RGB color map */ private static int[] getRGBs(IndexColorModel pColorModel) { int[] rgb = new int[pColorModel.getMapSize()]; diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/LittleEndianDataInputStream.java b/common/common-io/src/main/java/com/twelvemonkeys/io/LittleEndianDataInputStream.java index adceaead..7eb31923 100755 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/LittleEndianDataInputStream.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/io/LittleEndianDataInputStream.java @@ -65,7 +65,8 @@ import java.io.*; * @see java.io.DataOutput * * @author Elliotte Rusty Harold - * @version 1.0.3, 28 December 2002 + * @author Harald Kuhr + * @version 2 */ public class LittleEndianDataInputStream extends FilterInputStream implements DataInput { // TODO: Optimize by reading into a fixed size (8 bytes) buffer instead of individual read operations? @@ -158,7 +159,7 @@ public class LittleEndianDataInputStream extends FilterInputStream implements Da throw new EOFException(); } - return (short) (((byte2 << 24) >>> 16) + (byte1 << 24) >>> 24); + return (short) (((byte2 << 24) >>> 16) | (byte1 << 24) >>> 24); } /** @@ -198,7 +199,7 @@ public class LittleEndianDataInputStream extends FilterInputStream implements Da throw new EOFException(); } - return (char) (((byte2 << 24) >>> 16) + ((byte1 << 24) >>> 24)); + return (char) (((byte2 << 24) >>> 16) | ((byte1 << 24) >>> 24)); } @@ -221,8 +222,8 @@ public class LittleEndianDataInputStream extends FilterInputStream implements Da throw new EOFException(); } - return (byte4 << 24) + ((byte3 << 24) >>> 8) - + ((byte2 << 24) >>> 16) + ((byte1 << 24) >>> 24); + return (byte4 << 24) | ((byte3 << 24) >>> 8) + | ((byte2 << 24) >>> 16) | ((byte1 << 24) >>> 24); } /** @@ -248,10 +249,10 @@ public class LittleEndianDataInputStream extends FilterInputStream implements Da throw new EOFException(); } - return (byte8 << 56) + ((byte7 << 56) >>> 8) - + ((byte6 << 56) >>> 16) + ((byte5 << 56) >>> 24) - + ((byte4 << 56) >>> 32) + ((byte3 << 56) >>> 40) - + ((byte2 << 56) >>> 48) + ((byte1 << 56) >>> 56); + return (byte8 << 56) | ((byte7 << 56) >>> 8) + | ((byte6 << 56) >>> 16) | ((byte5 << 56) >>> 24) + | ((byte4 << 56) >>> 32) | ((byte3 << 56) >>> 40) + | ((byte2 << 56) >>> 48) | ((byte1 << 56) >>> 56); } /** diff --git a/common/common-io/src/main/resources/com/twelvemonkeys/net/MIMEUtil.properties b/common/common-io/src/main/resources/com/twelvemonkeys/net/MIMEUtil.properties index 9598cc47..566e0a16 100755 --- a/common/common-io/src/main/resources/com/twelvemonkeys/net/MIMEUtil.properties +++ b/common/common-io/src/main/resources/com/twelvemonkeys/net/MIMEUtil.properties @@ -60,7 +60,7 @@ iff,ilbm=image/x-iff;image/iff jpeg,jpg,jpe,jfif=image/jpeg;image/x-jpeg jpm=image/jpm png=image/png;image/x-png -# NOTE: image/svg-xml is an old reccomendation, should not be used +# NOTE: image/svg-xml is an old recommendation, should not be used svg,svgz=image/svg+xml;image/svg-xml;image/x-svg tga=image/targa;image/x-targa tif,tiff=image/tiff;image/x-tiff diff --git a/common/common-io/src/test/java/com/twelvemonkeys/io/InputStreamAbstractTestCase.java b/common/common-io/src/test/java/com/twelvemonkeys/io/InputStreamAbstractTestCase.java index 22f5defa..9f21a06d 100755 --- a/common/common-io/src/test/java/com/twelvemonkeys/io/InputStreamAbstractTestCase.java +++ b/common/common-io/src/test/java/com/twelvemonkeys/io/InputStreamAbstractTestCase.java @@ -167,7 +167,7 @@ public abstract class InputStreamAbstractTestCase extends ObjectAbstractTestCase input.mark(100); // Should be a no-op int read = input.read(); - assertEquals(0, read); + assertTrue(read >= 0); // TODO: According to InputStream#reset, it is allowed to do some // implementation specific reset, and still be correct... diff --git a/common/common-io/src/test/java/com/twelvemonkeys/io/LittleEndianDataInputStreamTest.java b/common/common-io/src/test/java/com/twelvemonkeys/io/LittleEndianDataInputStreamTest.java new file mode 100644 index 00000000..1f7c551b --- /dev/null +++ b/common/common-io/src/test/java/com/twelvemonkeys/io/LittleEndianDataInputStreamTest.java @@ -0,0 +1,208 @@ +/* + * 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.io; + +import org.junit.Test; + +import java.io.ByteArrayInputStream; +import java.io.IOException; + +import static org.junit.Assert.*; + +/** + * LittleEndianDataInputStreamTest + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: LittleEndianDataInputStreamTest.java,v 1.0 15.02.13 11:04 haraldk Exp$ + */ +public class LittleEndianDataInputStreamTest { + @Test + public void testReadBoolean() throws IOException { + LittleEndianDataInputStream data = new LittleEndianDataInputStream(new ByteArrayInputStream(new byte[] {0, 1, 0x7f, (byte) 0xff})); + assertFalse(data.readBoolean()); + assertTrue(data.readBoolean()); + assertTrue(data.readBoolean()); + assertTrue(data.readBoolean()); + } + + @Test + public void testReadByte() throws IOException { + LittleEndianDataInputStream data = new LittleEndianDataInputStream(new ByteArrayInputStream( + new byte[] { + (byte) 0x00, (byte) 0x00, + (byte) 0x01, (byte) 0x00, + (byte) 0xff, (byte) 0xff, + (byte) 0x00, (byte) 0x80, + (byte) 0xff, (byte) 0x7f, + (byte) 0x00, (byte) 0x01, + } + + )); + + assertEquals(0, data.readByte()); + assertEquals(0, data.readByte()); + assertEquals(1, data.readByte()); + assertEquals(0, data.readByte()); + assertEquals(-1, data.readByte()); + assertEquals(-1, data.readByte()); + assertEquals(0, data.readByte()); + assertEquals(Byte.MIN_VALUE, data.readByte()); + assertEquals(-1, data.readByte()); + assertEquals(Byte.MAX_VALUE, data.readByte()); + assertEquals(0, data.readByte()); + assertEquals(1, data.readByte()); + } + + @Test + public void testReadUnsignedByte() throws IOException { + LittleEndianDataInputStream data = new LittleEndianDataInputStream(new ByteArrayInputStream( + new byte[] { + (byte) 0x00, (byte) 0x00, + (byte) 0x01, (byte) 0x00, + (byte) 0xff, (byte) 0xff, + (byte) 0x00, (byte) 0x80, + (byte) 0xff, (byte) 0x7f, + (byte) 0x00, (byte) 0x01, + } + + )); + + assertEquals(0, data.readUnsignedByte()); + assertEquals(0, data.readUnsignedByte()); + assertEquals(1, data.readUnsignedByte()); + assertEquals(0, data.readUnsignedByte()); + assertEquals(255, data.readUnsignedByte()); + assertEquals(255, data.readUnsignedByte()); + assertEquals(0, data.readUnsignedByte()); + assertEquals(128, data.readUnsignedByte()); + assertEquals(255, data.readUnsignedByte()); + assertEquals(Byte.MAX_VALUE, data.readUnsignedByte()); + assertEquals(0, data.readUnsignedByte()); + assertEquals(1, data.readUnsignedByte()); + } + + @Test + public void testReadShort() throws IOException { + LittleEndianDataInputStream data = new LittleEndianDataInputStream(new ByteArrayInputStream( + new byte[] { + (byte) 0x00, (byte) 0x00, + (byte) 0x01, (byte) 0x00, + (byte) 0xff, (byte) 0xff, + (byte) 0x00, (byte) 0x80, + (byte) 0xff, (byte) 0x7f, + (byte) 0x00, (byte) 0x01, + } + + )); + + assertEquals(0, data.readShort()); + assertEquals(1, data.readShort()); + assertEquals(-1, data.readShort()); + assertEquals(Short.MIN_VALUE, data.readShort()); + assertEquals(Short.MAX_VALUE, data.readShort()); + assertEquals(256, data.readShort()); + } + + @Test + public void testReadUnsignedShort() throws IOException { + LittleEndianDataInputStream data = new LittleEndianDataInputStream(new ByteArrayInputStream( + new byte[] { + (byte) 0x00, (byte) 0x00, + (byte) 0x01, (byte) 0x00, + (byte) 0xff, (byte) 0xff, + (byte) 0x00, (byte) 0x80, + (byte) 0xff, (byte) 0x7f, + (byte) 0x00, (byte) 0x01, + } + + )); + + assertEquals(0, data.readUnsignedShort()); + assertEquals(1, data.readUnsignedShort()); + assertEquals(Short.MAX_VALUE * 2 + 1, data.readUnsignedShort()); + assertEquals(Short.MAX_VALUE + 1, data.readUnsignedShort()); + assertEquals(Short.MAX_VALUE, data.readUnsignedShort()); + assertEquals(256, data.readUnsignedShort()); + } + + @Test + public void testReadInt() throws IOException { + LittleEndianDataInputStream data = new LittleEndianDataInputStream(new ByteArrayInputStream( + new byte[] { + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x80, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x7f, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01, + (byte) 0xff, (byte) 0x00, (byte) 0xff, (byte) 0x00, + (byte) 0x00, (byte) 0xff, (byte) 0x00, (byte) 0xff, + (byte) 0xbe, (byte) 0xba, (byte) 0xfe, (byte) 0xca, + (byte) 0xca, (byte) 0xfe, (byte) 0xd0, (byte) 0x0d, + } + + )); + + assertEquals(0, data.readInt()); + assertEquals(1, data.readInt()); + assertEquals(-1, data.readInt()); + assertEquals(Integer.MIN_VALUE, data.readInt()); + assertEquals(Integer.MAX_VALUE, data.readInt()); + assertEquals(16777216, data.readInt()); + assertEquals(0xff00ff, data.readInt()); + assertEquals(0xff00ff00, data.readInt()); + assertEquals(0xCafeBabe, data.readInt()); + assertEquals(0x0dd0feca, data.readInt()); + } + + @Test + public void testReadLong() throws IOException { + LittleEndianDataInputStream data = new LittleEndianDataInputStream(new ByteArrayInputStream( + new byte[] { + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x80, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x7f, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01, + (byte) 0x0d, (byte) 0xd0, (byte) 0xfe, (byte) 0xca, (byte) 0xbe, (byte) 0xba, (byte) 0xfe, (byte) 0xca, + } + + )); + + assertEquals(0, data.readLong()); + assertEquals(1, data.readLong()); + assertEquals(-1, data.readLong()); + assertEquals(Long.MIN_VALUE, data.readLong()); + assertEquals(Long.MAX_VALUE, data.readLong()); + assertEquals(72057594037927936L, data.readLong()); + assertEquals(0xCafeBabeL << 32 | 0xCafeD00dL, data.readLong()); + } +} diff --git a/common/common-lang/src/main/java/com/twelvemonkeys/lang/BeanUtil.java b/common/common-lang/src/main/java/com/twelvemonkeys/lang/BeanUtil.java index 0c09176d..7eae35dd 100755 --- a/common/common-lang/src/main/java/com/twelvemonkeys/lang/BeanUtil.java +++ b/common/common-lang/src/main/java/com/twelvemonkeys/lang/BeanUtil.java @@ -41,8 +41,7 @@ import java.util.Arrays; /** * A utility class with some useful bean-related functions. *

- * NOTE: This class is not considered part of the public API and may be - * changed without notice + * NOTE: This class is not considered part of the public API and may be changed without notice * * @author Harald Kuhr * @author last modified by $Author: haku $ @@ -60,10 +59,10 @@ public final class BeanUtil { * Now supports getting values from properties of properties * (recursive). * - * @param pObject The object to get the property from + * @param pObject The object to get the property from * @param pProperty The name of the property * - * @return A string containing the value of the given property, or null + * @return A string containing the value of the given property, or {@code null} * if it can not be found. * @todo Remove System.err's... Create new Exception? Hmm.. */ @@ -77,7 +76,7 @@ public final class BeanUtil { return null; } - Class objClass = pObject.getClass(); + Class objClass = pObject.getClass(); Object result = pObject; @@ -154,9 +153,8 @@ public final class BeanUtil { catch (NoSuchMethodException e) { System.err.print("No method named \"" + methodName + "()\""); // The array might be of size 0... - if (paramClass != null && paramClass.length > 0) { - System.err.print(" with the parameter " - + paramClass[0].getName()); + if (paramClass.length > 0 && paramClass[0] != null) { + System.err.print(" with the parameter " + paramClass[0].getName()); } System.err.println(" in class " + objClass.getName() + "!"); @@ -177,8 +175,7 @@ public final class BeanUtil { result = method.invoke(result, param); } catch (InvocationTargetException e) { - System.err.println("property=" + pProperty + " & result=" - + result + " & param=" + Arrays.toString(param)); + System.err.println("property=" + pProperty + " & result=" + result + " & param=" + Arrays.toString(param)); e.getTargetException().printStackTrace(); e.printStackTrace(); return null; @@ -188,8 +185,7 @@ public final class BeanUtil { return null; } catch (NullPointerException e) { - System.err.println(objClass.getName() + "." + method.getName() - + "(" + ((paramClass != null && paramClass.length > 0) ? paramClass[0].getName() : "") + ")"); + System.err.println(objClass.getName() + "." + method.getName() + "(" + ((paramClass.length > 0 && paramClass[0] != null) ? paramClass[0].getName() : "") + ")"); e.printStackTrace(); return null; } @@ -221,10 +217,8 @@ public final class BeanUtil { * @throws IllegalAccessException if the caller class has no access to the * write method */ - public static void setPropertyValue(Object pObject, String pProperty, - Object pValue) - throws NoSuchMethodException, InvocationTargetException, - IllegalAccessException { + public static void setPropertyValue(Object pObject, String pProperty, Object pValue) + throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { // // TODO: Support set(Object, Object)/put(Object, Object) methods @@ -255,7 +249,8 @@ public final class BeanUtil { method.invoke(obj, params); } - private static Method getMethodMayModifyParams(Object pObject, String pName, Class[] pParams, Object[] pValues) throws NoSuchMethodException { + private static Method getMethodMayModifyParams(Object pObject, String pName, Class[] pParams, Object[] pValues) + throws NoSuchMethodException { // NOTE: This method assumes pParams.length == 1 && pValues.length == 1 Method method = null; @@ -307,10 +302,8 @@ public final class BeanUtil { if (method == null) { Method[] methods = pObject.getClass().getMethods(); for (Method candidate : methods) { - if (Modifier.isPublic(candidate.getModifiers()) - && candidate.getName().equals(pName) - && candidate.getReturnType() == Void.TYPE - && candidate.getParameterTypes().length == 1) { + if (Modifier.isPublic(candidate.getModifiers()) && candidate.getName().equals(pName) + && candidate.getReturnType() == Void.TYPE && candidate.getParameterTypes().length == 1) { // NOTE: Assumes paramTypes.length == 1 Class type = candidate.getParameterTypes()[0]; @@ -337,7 +330,7 @@ public final class BeanUtil { return method; } - private static Object convertValueToType(Object pValue, Class pType) throws ConversionException { + private static Object convertValueToType(Object pValue, Class pType) throws ConversionException { if (pType.isPrimitive()) { if (pType == Boolean.TYPE && pValue instanceof Boolean) { return pValue; @@ -395,7 +388,7 @@ public final class BeanUtil { * @throws InvocationTargetException if the constructor failed */ // TODO: Move to ReflectUtil - public static Object createInstance(Class pClass, Object pParam) + public static T createInstance(Class pClass, Object pParam) throws InvocationTargetException { return createInstance(pClass, new Object[] {pParam}); } @@ -414,9 +407,9 @@ public final class BeanUtil { * @throws InvocationTargetException if the constructor failed */ // TODO: Move to ReflectUtil - public static Object createInstance(Class pClass, Object... pParams) + public static T createInstance(Class pClass, Object... pParams) throws InvocationTargetException { - Object value; + T value; try { // Create param and argument arrays @@ -429,8 +422,7 @@ public final class BeanUtil { } // Get constructor - //Constructor constructor = pClass.getDeclaredConstructor(paramTypes); - Constructor constructor = pClass.getConstructor(paramTypes); + Constructor constructor = pClass.getConstructor(paramTypes); // Invoke and create instance value = constructor.newInstance(pParams); @@ -468,12 +460,11 @@ public final class BeanUtil { * If the return type of the method is void, null is returned. * If the method could not be invoked for any reason, null is returned. * - * @throws InvocationTargetException if the invocaton failed + * @throws InvocationTargetException if the invocation failed */ // TODO: Move to ReflectUtil // TODO: Rename to invokeStatic? - public static Object invokeStaticMethod(Class pClass, String pMethod, - Object pParam) + public static Object invokeStaticMethod(Class pClass, String pMethod, Object pParam) throws InvocationTargetException { return invokeStaticMethod(pClass, pMethod, new Object[] {pParam}); @@ -492,12 +483,11 @@ public final class BeanUtil { * If the return type of the method is void, null is returned. * If the method could not be invoked for any reason, null is returned. * - * @throws InvocationTargetException if the invocaton failed + * @throws InvocationTargetException if the invocation failed */ // TODO: Move to ReflectUtil // TODO: Rename to invokeStatic? - public static Object invokeStaticMethod(Class pClass, String pMethod, - Object[] pParams) + public static Object invokeStaticMethod(Class pClass, String pMethod, Object... pParams) throws InvocationTargetException { Object value = null; @@ -518,8 +508,7 @@ public final class BeanUtil { Method method = pClass.getMethod(pMethod, paramTypes); // Invoke public static method - if (Modifier.isPublic(method.getModifiers()) - && Modifier.isStatic(method.getModifiers())) { + if (Modifier.isPublic(method.getModifiers()) && Modifier.isStatic(method.getModifiers())) { value = method.invoke(null, pParams); } diff --git a/common/common-lang/src/main/java/com/twelvemonkeys/util/AbstractTokenIterator.java b/common/common-lang/src/main/java/com/twelvemonkeys/util/AbstractTokenIterator.java index edfa51bf..90e56161 100755 --- a/common/common-lang/src/main/java/com/twelvemonkeys/util/AbstractTokenIterator.java +++ b/common/common-lang/src/main/java/com/twelvemonkeys/util/AbstractTokenIterator.java @@ -46,7 +46,7 @@ public abstract class AbstractTokenIterator implements TokenIterator { public void remove() { // TODO: This is not difficult: // - Convert String to StringBuilder in constructor - // - delete(pos, mNext.lenght()) + // - delete(pos, next.lenght()) // - Add toString() method // BUT: Would it ever be useful? :-) diff --git a/common/common-lang/src/main/java/com/twelvemonkeys/util/LRUHashMap.java b/common/common-lang/src/main/java/com/twelvemonkeys/util/LRUHashMap.java index cbaa015f..eee68eeb 100755 --- a/common/common-lang/src/main/java/com/twelvemonkeys/util/LRUHashMap.java +++ b/common/common-lang/src/main/java/com/twelvemonkeys/util/LRUHashMap.java @@ -205,6 +205,7 @@ public class LRUHashMap extends LinkedHashMap implements ExpiringMap */ public void removeLRU() { int removeCount = (int) Math.max((size() * trimFactor), 1); + Iterator> entries = entrySet().iterator(); while ((removeCount--) > 0 && entries.hasNext()) { entries.next(); diff --git a/common/common-lang/src/main/java/com/twelvemonkeys/util/LRUMap.java b/common/common-lang/src/main/java/com/twelvemonkeys/util/LRUMap.java index d5057046..040ed014 100755 --- a/common/common-lang/src/main/java/com/twelvemonkeys/util/LRUMap.java +++ b/common/common-lang/src/main/java/com/twelvemonkeys/util/LRUMap.java @@ -45,7 +45,7 @@ import java.util.Map; * * * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/LRUMap.java#1 $ + * @version $Id: com/twelvemonkeys/util/LRUMap.java#1 $ */ public class LRUMap extends LinkedMap implements ExpiringMap { @@ -222,8 +222,9 @@ public class LRUMap extends LinkedMap implements ExpiringMap { */ public void removeLRU() { int removeCount = (int) Math.max((size() * trimFactor), 1); + while ((removeCount--) > 0) { - removeEntry(head.mNext); + removeEntry(head.next); } } } diff --git a/common/common-lang/src/main/java/com/twelvemonkeys/util/LinkedMap.java b/common/common-lang/src/main/java/com/twelvemonkeys/util/LinkedMap.java index b493ff9d..e9a071a0 100755 --- a/common/common-lang/src/main/java/com/twelvemonkeys/util/LinkedMap.java +++ b/common/common-lang/src/main/java/com/twelvemonkeys/util/LinkedMap.java @@ -181,19 +181,19 @@ public class LinkedMap extends AbstractDecoratedMap implements Seria return "head"; } }; - head.mPrevious = head.mNext = head; + head.previous = head.next = head; } public boolean containsValue(Object pValue) { // Overridden to take advantage of faster iterator if (pValue == null) { - for (LinkedEntry e = head.mNext; e != head; e = e.mNext) { + for (LinkedEntry e = head.next; e != head; e = e.next) { if (e.mValue == null) { return true; } } } else { - for (LinkedEntry e = head.mNext; e != head; e = e.mNext) { + for (LinkedEntry e = head.next; e != head; e = e.next) { if (pValue.equals(e.mValue)) { return true; } @@ -215,7 +215,7 @@ public class LinkedMap extends AbstractDecoratedMap implements Seria } private abstract class LinkedMapIterator implements Iterator { - LinkedEntry mNextEntry = head.mNext; + LinkedEntry mNextEntry = head.next; LinkedEntry mLastReturned = null; /** @@ -254,7 +254,7 @@ public class LinkedMap extends AbstractDecoratedMap implements Seria } LinkedEntry e = mLastReturned = mNextEntry; - mNextEntry = e.mNext; + mNextEntry = e.next; return e; } @@ -309,7 +309,7 @@ public class LinkedMap extends AbstractDecoratedMap implements Seria oldValue = null; // Remove eldest entry if instructed, else grow capacity if appropriate - LinkedEntry eldest = head.mNext; + LinkedEntry eldest = head.next; if (removeEldestEntry(eldest)) { removeEntry(eldest); } @@ -407,13 +407,13 @@ public class LinkedMap extends AbstractDecoratedMap implements Seria * Linked list implementation of {@code Map.Entry}. */ protected static class LinkedEntry extends BasicEntry implements Serializable { - LinkedEntry mPrevious; - LinkedEntry mNext; + LinkedEntry previous; + LinkedEntry next; LinkedEntry(K pKey, V pValue, LinkedEntry pNext) { super(pKey, pValue); - mNext = pNext; + next = pNext; } /** @@ -423,19 +423,19 @@ public class LinkedMap extends AbstractDecoratedMap implements Seria * @param pExisting the entry to add before */ void addBefore(LinkedEntry pExisting) { - mNext = pExisting; - mPrevious = pExisting.mPrevious; + next = pExisting; + previous = pExisting.previous; - mPrevious.mNext = this; - mNext.mPrevious = this; + previous.next = this; + next.previous = this; } /** * Removes this entry from the linked list. */ void remove() { - mPrevious.mNext = mNext; - mNext.mPrevious = mPrevious; + previous.next = next; + next.previous = previous; } /** @@ -456,7 +456,7 @@ public class LinkedMap extends AbstractDecoratedMap implements Seria /** * Removes this entry from the linked list. * - * @param pMap the map to record remoal from + * @param pMap the map to record removal from */ protected void recordRemoval(Map pMap) { // TODO: Is this REALLY correct? diff --git a/common/common-lang/src/main/java/com/twelvemonkeys/util/convert/Converter.java b/common/common-lang/src/main/java/com/twelvemonkeys/util/convert/Converter.java index aa4015ef..106b6164 100755 --- a/common/common-lang/src/main/java/com/twelvemonkeys/util/convert/Converter.java +++ b/common/common-lang/src/main/java/com/twelvemonkeys/util/convert/Converter.java @@ -36,7 +36,7 @@ import java.util.Map; /** * The converter (singleton). Converts strings to objects and back. - * This is the entrypoint to the converter framework. + * This is the entry point to the converter framework. *

* By default, converters for {@link com.twelvemonkeys.util.Time}, {@link Date} * and {@link Object} @@ -53,17 +53,17 @@ import java.util.Map; */ // TODO: Get rid of singleton stuff // Can probably be a pure static class, but is that a good idea? -// Maybe have BeanUtil act as a "proxy", and hide this class alltogheter? +// Maybe have BeanUtil act as a "proxy", and hide this class all together? // TODO: ServiceRegistry for registering 3rd party converters // TODO: URI scheme, for implicit typing? Is that a good idea? // TODO: Array converters? public abstract class Converter implements PropertyConverter { /** Our singleton instance */ - protected static Converter sInstance = new ConverterImpl(); // Thread safe & EASY + protected static final Converter sInstance = new ConverterImpl(); // Thread safe & EASY - /** The conveters Map */ - protected Map converters = new Hashtable(); + /** The converters Map */ + protected final Map converters = new Hashtable(); // Register our predefined converters static { @@ -115,20 +115,21 @@ public abstract class Converter implements PropertyConverter { * * @see #unregisterConverter(Class) */ - public static void registerConverter(Class pType, PropertyConverter pConverter) { + public static void registerConverter(final Class pType, final PropertyConverter pConverter) { getInstance().converters.put(pType, pConverter); } /** - * Unregisters a converter for a given type. That is, making it unavailable + * Un-registers a converter for a given type. That is, making it unavailable * for the converter framework, and making it (potentially) available for - * garbabe collection. + * garbage collection. * * @param pType the (super) type to remove converter for * * @see #registerConverter(Class,PropertyConverter) */ - public static void unregisterConverter(Class pType) { + @SuppressWarnings("UnusedDeclaration") + public static void unregisterConverter(final Class pType) { getInstance().converters.remove(pType); } @@ -143,8 +144,7 @@ public abstract class Converter implements PropertyConverter { * @throws ConversionException if the string cannot be converted for any * reason. */ - public Object toObject(String pString, Class pType) - throws ConversionException { + public Object toObject(final String pString, final Class pType) throws ConversionException { return toObject(pString, pType, null); } @@ -174,7 +174,7 @@ public abstract class Converter implements PropertyConverter { * @throws ConversionException if the object cannot be converted to a * string for any reason. */ - public String toString(Object pObject) throws ConversionException { + public String toString(final Object pObject) throws ConversionException { return toString(pObject, null); } diff --git a/common/common-lang/src/main/java/com/twelvemonkeys/util/convert/DefaultConverter.java b/common/common-lang/src/main/java/com/twelvemonkeys/util/convert/DefaultConverter.java index 81e4d589..59236d7c 100755 --- a/common/common-lang/src/main/java/com/twelvemonkeys/util/convert/DefaultConverter.java +++ b/common/common-lang/src/main/java/com/twelvemonkeys/util/convert/DefaultConverter.java @@ -67,9 +67,9 @@ public final class DefaultConverter implements PropertyConverter { * * @throws ConversionException if the type is null, or if the string cannot * be converted into the given type, using a string constructor or static - * {@code valueof} method. + * {@code valueOf} method. */ - public Object toObject(String pString, final Class pType, String pFormat) throws ConversionException { + public Object toObject(final String pString, final Class pType, final String pFormat) throws ConversionException { if (pString == null) { return null; } @@ -87,13 +87,7 @@ public final class DefaultConverter implements PropertyConverter { // But what about generic type?! It's erased... // Primitive -> wrapper - Class type; - if (pType == Boolean.TYPE) { - type = Boolean.class; - } - else { - type = pType; - } + Class type = unBoxType(pType); try { // Try to create instance from (String) @@ -101,13 +95,15 @@ public final class DefaultConverter implements PropertyConverter { if (value == null) { // createInstance failed for some reason - - // Try to invoke the static method valueof(String) + // Try to invoke the static method valueOf(String) value = BeanUtil.invokeStaticMethod(type, "valueOf", pString); if (value == null) { // If the value is still null, well, then I cannot help... - throw new ConversionException("Could not convert String to " + pType.getName() + ": No constructor " + type.getName() + "(String) or static " + type.getName() + ".valueof(String) method found!"); + throw new ConversionException(String.format( + "Could not convert String to %1$s: No constructor %1$s(String) or static %1$s.valueOf(String) method found!", + type.getName() + )); } } @@ -116,12 +112,15 @@ public final class DefaultConverter implements PropertyConverter { catch (InvocationTargetException ite) { throw new ConversionException(ite.getTargetException()); } + catch (ConversionException ce) { + throw ce; + } catch (RuntimeException rte) { throw new ConversionException(rte); } } - private Object toArray(String pString, Class pType, String pFormat) { + private Object toArray(final String pString, final Class pType, final String pFormat) { String[] strings = StringUtil.toStringArray(pString, pFormat != null ? pFormat : StringUtil.DELIMITER_STRING); Class type = pType.getComponentType(); if (type == String.class) { @@ -152,10 +151,9 @@ public final class DefaultConverter implements PropertyConverter { * @param pObject the object to convert. * @param pFormat ignored. * - * @return the string representation of the object, or {@code null} if - * {@code pObject == null} + * @return the string representation of the object, or {@code null} if {@code pObject == null} */ - public String toString(Object pObject, String pFormat) + public String toString(final Object pObject, final String pFormat) throws ConversionException { try { @@ -170,7 +168,7 @@ public final class DefaultConverter implements PropertyConverter { return pFormat == null ? StringUtil.toCSVString(pArray) : StringUtil.toCSVString(pArray, pFormat); } - private Object[] toObjectArray(Object pObject) { + private Object[] toObjectArray(final Object pObject) { // TODO: Extract util method for wrapping/unwrapping native arrays? Object[] array; Class componentType = pObject.getClass().getComponentType(); @@ -232,4 +230,37 @@ public final class DefaultConverter implements PropertyConverter { } return array; } + + private Class unBoxType(final Class pType) { + if (pType.isPrimitive()) { + if (pType == boolean.class) { + return Boolean.class; + } + if (pType == byte.class) { + return Byte.class; + } + if (pType == char.class) { + return Character.class; + } + if (pType == short.class) { + return Short.class; + } + if (pType == int.class) { + return Integer.class; + } + if (pType == float.class) { + return Float.class; + } + if (pType == long.class) { + return Long.class; + } + if (pType == double.class) { + return Double.class; + } + + throw new IllegalArgumentException("Unknown type: " + pType); + } + + return pType; + } } diff --git a/common/common-lang/src/test/java/com/twelvemonkeys/lang/BeanUtilTestCase.java b/common/common-lang/src/test/java/com/twelvemonkeys/lang/BeanUtilTestCase.java index 2b567ed6..59e69b97 100755 --- a/common/common-lang/src/test/java/com/twelvemonkeys/lang/BeanUtilTestCase.java +++ b/common/common-lang/src/test/java/com/twelvemonkeys/lang/BeanUtilTestCase.java @@ -108,13 +108,13 @@ public class BeanUtilTestCase extends TestCase { assertEquals(0.3, bean.getDoubleValue()); } - public void testConfigureAmbigious1() { + public void testConfigureAmbiguous1() { TestBean bean = new TestBean(); Map map = new HashMap(); String value = "one"; - map.put("ambigious", value); + map.put("ambiguous", value); try { BeanUtil.configure(bean, map); @@ -123,20 +123,20 @@ public class BeanUtilTestCase extends TestCase { fail(e.getMessage()); } - assertNotNull(bean.getAmbigious()); - assertEquals("String converted rather than invoking setAmbigiouos(String), ordering not predictable", - "one", bean.getAmbigious()); - assertSame("String converted rather than invoking setAmbigiouos(String), ordering not predictable", - value, bean.getAmbigious()); + assertNotNull(bean.getAmbiguous()); + assertEquals("String converted rather than invoking setAmbiguous(String), ordering not predictable", + "one", bean.getAmbiguous()); + assertSame("String converted rather than invoking setAmbiguous(String), ordering not predictable", + value, bean.getAmbiguous()); } - public void testConfigureAmbigious2() { + public void testConfigureAmbiguous2() { TestBean bean = new TestBean(); Map map = new HashMap(); Integer value = 2; - map.put("ambigious", value); + map.put("ambiguous", value); try { BeanUtil.configure(bean, map); @@ -145,20 +145,20 @@ public class BeanUtilTestCase extends TestCase { fail(e.getMessage()); } - assertNotNull(bean.getAmbigious()); - assertEquals("Integer converted rather than invoking setAmbigiouos(Integer), ordering not predictable", - 2, bean.getAmbigious()); - assertSame("Integer converted rather than invoking setAmbigiouos(Integer), ordering not predictable", - value, bean.getAmbigious()); + assertNotNull(bean.getAmbiguous()); + assertEquals("Integer converted rather than invoking setAmbiguous(Integer), ordering not predictable", + 2, bean.getAmbiguous()); + assertSame("Integer converted rather than invoking setAmbiguous(Integer), ordering not predictable", + value, bean.getAmbiguous()); } - public void testConfigureAmbigious3() { + public void testConfigureAmbiguous3() { TestBean bean = new TestBean(); Map map = new HashMap(); Double value = .3; - map.put("ambigious", value); + map.put("ambiguous", value); try { BeanUtil.configure(bean, map); @@ -167,11 +167,11 @@ public class BeanUtilTestCase extends TestCase { fail(e.getMessage()); } - assertNotNull(bean.getAmbigious()); - assertEquals("Object converted rather than invoking setAmbigious(Object), ordering not predictable", - value.getClass(), bean.getAmbigious().getClass()); - assertSame("Object converted rather than invoking setAmbigious(Object), ordering not predictable", - value, bean.getAmbigious()); + assertNotNull(bean.getAmbiguous()); + assertEquals("Object converted rather than invoking setAmbiguous(Object), ordering not predictable", + value.getClass(), bean.getAmbiguous().getClass()); + assertSame("Object converted rather than invoking setAmbiguous(Object), ordering not predictable", + value, bean.getAmbiguous()); } static class TestBean { @@ -179,7 +179,7 @@ public class BeanUtilTestCase extends TestCase { private int intVal; private Double doubleVal; - private Object ambigious; + private Object ambiguous; public Double getDoubleValue() { return doubleVal; @@ -193,36 +193,43 @@ public class BeanUtilTestCase extends TestCase { return stringVal; } + @SuppressWarnings("UnusedDeclaration") public void setStringValue(String pString) { stringVal = pString; } + @SuppressWarnings("UnusedDeclaration") public void setIntValue(int pInt) { intVal = pInt; } + @SuppressWarnings("UnusedDeclaration") public void setDoubleValue(Double pDouble) { doubleVal = pDouble; } - public void setAmbigious(String pString) { - ambigious = pString; + @SuppressWarnings("UnusedDeclaration") + public void setAmbiguous(String pString) { + ambiguous = pString; } - public void setAmbigious(Object pObject) { - ambigious = pObject; + @SuppressWarnings("UnusedDeclaration") + public void setAmbiguous(Object pObject) { + ambiguous = pObject; } - public void setAmbigious(Integer pInteger) { - ambigious = pInteger; + @SuppressWarnings("UnusedDeclaration") + public void setAmbiguous(Integer pInteger) { + ambiguous = pInteger; } - public void setAmbigious(int pInt) { - ambigious = (long) pInt; // Just to differentiate... + @SuppressWarnings("UnusedDeclaration") + public void setAmbiguous(int pInt) { + ambiguous = (long) pInt; // Just to differentiate... } - public Object getAmbigious() { - return ambigious; + public Object getAmbiguous() { + return ambiguous; } } } diff --git a/common/common-lang/src/test/java/com/twelvemonkeys/util/convert/DefaultConverterTestCase.java b/common/common-lang/src/test/java/com/twelvemonkeys/util/convert/DefaultConverterTestCase.java index 65c879bd..6f1f19e7 100755 --- a/common/common-lang/src/test/java/com/twelvemonkeys/util/convert/DefaultConverterTestCase.java +++ b/common/common-lang/src/test/java/com/twelvemonkeys/util/convert/DefaultConverterTestCase.java @@ -1,8 +1,14 @@ package com.twelvemonkeys.util.convert; +import com.twelvemonkeys.lang.Validate; +import org.junit.Ignore; +import org.junit.Test; + import java.io.File; import java.net.URI; +import static org.junit.Assert.*; + /** * DefaultConverterTestCase *

@@ -47,23 +53,76 @@ public class DefaultConverterTestCase extends PropertyConverterAbstractTestCase // Object array test new Conversion("foo, bar", new FooBar[] {new FooBar("foo"), new FooBar("bar")}), - new Conversion("/temp, /usr/local/bin", new File[] {new File("/temp"), new File("/usr/local/bin")}), + new Conversion("/temp, /usr/local/bin".replace('/', File.separatorChar), new File[] {new File("/temp"), new File("/usr/local/bin")}), new Conversion("file:/temp, http://java.net/", new URI[] {URI.create("file:/temp"), URI.create("http://java.net/")}), // TODO: More tests }; } - // TODO: Test boolean -> Boolean conversion + @Test + public void testConvertBooleanPrimitive() { + PropertyConverter converter = makePropertyConverter(); + assertTrue((Boolean) converter.toObject("true", boolean.class, null)); + assertFalse((Boolean) converter.toObject("FalsE", Boolean.TYPE, null)); + } + + @Test + public void testConvertShortPrimitive() { + PropertyConverter converter = makePropertyConverter(); + assertEquals(1, (short) (Short) converter.toObject("1", short.class, null)); + assertEquals(-2, (short) (Short) converter.toObject("-2", Short.TYPE, null)); + } + @Test + public void testConvertIntPrimitive() { + PropertyConverter converter = makePropertyConverter(); + assertEquals(1, (int) (Integer) converter.toObject("1", int.class, null)); + assertEquals(-2, (int) (Integer) converter.toObject("-2", Integer.TYPE, null)); + } + + @Test + public void testConvertLongPrimitive() { + PropertyConverter converter = makePropertyConverter(); + assertEquals(Long.MAX_VALUE, (long) (Long) converter.toObject("9223372036854775807", long.class, null)); + assertEquals(-2, (long) (Long) converter.toObject("-2", Long.TYPE, null)); + } + + @Test + public void testConvertBytePrimitive() { + PropertyConverter converter = makePropertyConverter(); + assertEquals(1, (byte) (Byte) converter.toObject("1", byte.class, null)); + assertEquals(-2, (byte) (Byte) converter.toObject("-2", Byte.TYPE, null)); + } + + @Test + public void testConvertFloatPrimitive() { + PropertyConverter converter = makePropertyConverter(); + assertEquals(1f, (Float) converter.toObject("1.0", float.class, null), 0); + assertEquals(-2.3456f, (Float) converter.toObject("-2.3456", Float.TYPE, null), 0); + } + + @Test + public void testConvertDoublePrimitive() { + PropertyConverter converter = makePropertyConverter(); + assertEquals(1d, (Double) converter.toObject("1.0", double.class, null), 0); + assertEquals(-2.3456, (Double) converter.toObject("-2.3456", Double.TYPE, null), 0); + } + + @Ignore("Known issue. Why would anyone do something like this?") + @Test + public void testConvertCharPrimitive() { + PropertyConverter converter = makePropertyConverter(); + assertEquals('A', (char) (Character) converter.toObject("A", char.class, null)); + assertEquals('Z', (char) (Character) converter.toObject("Z", Character.TYPE, null)); + } public static class FooBar { - private final String mBar; + private final String bar; public FooBar(String pFoo) { - if (pFoo == null) { - throw new IllegalArgumentException("pFoo == null"); - } - mBar = reverse(pFoo); + Validate.notNull(pFoo, "foo"); + + bar = reverse(pFoo); } private String reverse(String pFoo) { @@ -77,16 +136,15 @@ public class DefaultConverterTestCase extends PropertyConverterAbstractTestCase } public String toString() { - return reverse(mBar); + return reverse(bar); } public boolean equals(Object obj) { - return obj == this || (obj instanceof FooBar && ((FooBar) obj).mBar.equals(mBar)); + return obj == this || (obj != null && obj.getClass() == getClass() && ((FooBar) obj).bar.equals(bar)); } public int hashCode() { - return 7 * mBar.hashCode(); + return 7 * bar.hashCode(); } } - } diff --git a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/ImageReaderBase.java b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/ImageReaderBase.java index 4ca4c686..4a0d7243 100644 --- a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/ImageReaderBase.java +++ b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/ImageReaderBase.java @@ -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 { diff --git a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/color/ColorSpaces.java b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/color/ColorSpaces.java index 13fabe81..767e6b8e 100644 --- a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/color/ColorSpaces.java +++ b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/color/ColorSpaces.java @@ -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}. *

@@ -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; } diff --git a/imageio/imageio-core/src/main/resources/com/twelvemonkeys/imageio/color/icc_profiles_lnx.properties b/imageio/imageio-core/src/main/resources/com/twelvemonkeys/imageio/color/icc_profiles_lnx.properties new file mode 100644 index 00000000..706fe6d6 --- /dev/null +++ b/imageio/imageio-core/src/main/resources/com/twelvemonkeys/imageio/color/icc_profiles_lnx.properties @@ -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 \ No newline at end of file diff --git a/imageio/imageio-core/src/main/resources/com/twelvemonkeys/imageio/color/icc_profiles_win.properties b/imageio/imageio-core/src/main/resources/com/twelvemonkeys/imageio/color/icc_profiles_win.properties index bab2bf93..21fef9f9 100644 --- a/imageio/imageio-core/src/main/resources/com/twelvemonkeys/imageio/color/icc_profiles_win.properties +++ b/imageio/imageio-core/src/main/resources/com/twelvemonkeys/imageio/color/icc_profiles_win.properties @@ -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 \ No newline at end of file +#ADOBE_RGB_1998=unknown, use built in for now \ No newline at end of file diff --git a/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/color/ColorSpacesTest.java b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/color/ColorSpacesTest.java index 9dddc7aa..ac154765 100644 --- a/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/color/ColorSpacesTest.java +++ b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/color/ColorSpacesTest.java @@ -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); + } } diff --git a/imageio/imageio-icns/src/main/java/com/twelvemonkeys/imageio/plugins/icns/SipsJP2Reader.java b/imageio/imageio-icns/src/main/java/com/twelvemonkeys/imageio/plugins/icns/SipsJP2Reader.java index c5834abb..ba1704e6 100644 --- a/imageio/imageio-icns/src/main/java/com/twelvemonkeys/imageio/plugins/icns/SipsJP2Reader.java +++ b/imageio/imageio-icns/src/main/java/com/twelvemonkeys/imageio/plugins/icns/SipsJP2Reader.java @@ -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; diff --git a/imageio/imageio-jpeg/license.txt b/imageio/imageio-jpeg/license.txt new file mode 100644 index 00000000..4913b243 --- /dev/null +++ b/imageio/imageio-jpeg/license.txt @@ -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. diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/AdobeDCT.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/AdobeDCTSegment.java similarity index 93% rename from imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/AdobeDCT.java rename to imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/AdobeDCTSegment.java index c95a8355..1d0651b7 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/AdobeDCT.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/AdobeDCTSegment.java @@ -29,13 +29,13 @@ package com.twelvemonkeys.imageio.plugins.jpeg; /** - * AdobeDCT + * AdobeDCTSegment * * @author Harald Kuhr * @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; diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnailReader.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnailReader.java index d18dbdf6..1eaba702 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnailReader.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnailReader.java @@ -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 diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/FastCMYKToRGB.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/FastCMYKToRGB.java index 5897a7aa..58a96211 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/FastCMYKToRGB.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/FastCMYKToRGB.java @@ -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) { diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java index 6a674242..81b7269e 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java @@ -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. + *

+ * Main features: + *

+ * Thumbnail support: + * * * @author Harald Kuhr * @author LUT-based YCbCR conversion by Werner Randelshofer @@ -76,7 +95,7 @@ public class JPEGImageReader extends ImageReaderBase { private static final Map> SEGMENT_IDENTIFIERS = createSegmentIds(); private static Map> createSegmentIds() { - Map> map = new HashMap>(); + Map> map = new LinkedHashMap>(); // 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 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 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 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 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); diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGSegmentImageInputStream.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGSegmentImageInputStream.java index 87dd6004..23ab8f8b 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGSegmentImageInputStream.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGSegmentImageInputStream.java @@ -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; diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/SOFComponent.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/SOFComponent.java new file mode 100644 index 00000000..3416aef8 --- /dev/null +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/SOFComponent.java @@ -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 Harald Kuhr +* @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); + } +} diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/SOFSegment.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/SOFSegment.java new file mode 100644 index 00000000..526f0c00 --- /dev/null +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/SOFSegment.java @@ -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 Harald Kuhr +* @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) + ); + } +} diff --git a/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReaderTest.java b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReaderTest.java index 6fa34246..37a34f73 100644 --- a/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReaderTest.java +++ b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReaderTest.java @@ -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> 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> 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 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 + } } diff --git a/imageio/imageio-jpeg/src/test/resources/jpeg/jfif-padded-segments.jpg b/imageio/imageio-jpeg/src/test/resources/jpeg/jfif-padded-segments.jpg new file mode 100755 index 00000000..b7cfd8e6 Binary files /dev/null and b/imageio/imageio-jpeg/src/test/resources/jpeg/jfif-padded-segments.jpg differ diff --git a/imageio/imageio-metadata/license.txt b/imageio/imageio-metadata/license.txt new file mode 100644 index 00000000..f1d861fb --- /dev/null +++ b/imageio/imageio-metadata/license.txt @@ -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. diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIF.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIF.java index 9ebda2ab..7ad54e97 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIF.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIF.java @@ -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; diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFEntry.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFEntry.java index efa13180..cf10da19 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFEntry.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFEntry.java @@ -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; diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/TIFF.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/TIFF.java index b60e2955..574bfb4d 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/TIFF.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/TIFF.java @@ -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; } diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEG.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEG.java index 8753c26e..72df9263 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEG.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEG.java @@ -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 } diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGQuality.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGQuality.java index 77a107d0..f4d0ae29 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGQuality.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGQuality.java @@ -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; diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGSegmentUtil.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGSegmentUtil.java index 6372737b..10db0713 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGSegmentUtil.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGSegmentUtil.java @@ -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> segmentIdentifiers) throws IOException { + static JPEGSegment readSegment(final ImageInputStream stream, final Map> 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 get(Object key) { + public List 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 get(Object key) { + public List get(final Object key) { return containsKey(key) ? ALL_IDS : null; } diff --git a/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGSegmentUtilTest.java b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGSegmentUtilTest.java index 2a69d8b1..8d961e05 100644 --- a/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGSegmentUtilTest.java +++ b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGSegmentUtilTest.java @@ -151,19 +151,21 @@ public class JPEGSegmentUtilTest { @Test public void testReadAll() throws IOException { List 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 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 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()); + } } diff --git a/imageio/imageio-metadata/src/test/resources/jpeg/jfif-padded-segments.jpg b/imageio/imageio-metadata/src/test/resources/jpeg/jfif-padded-segments.jpg new file mode 100755 index 00000000..b7cfd8e6 Binary files /dev/null and b/imageio/imageio-metadata/src/test/resources/jpeg/jfif-padded-segments.jpg differ diff --git a/imageio/imageio-tiff/license.txt b/imageio/imageio-tiff/license.txt new file mode 100644 index 00000000..4913b243 --- /dev/null +++ b/imageio/imageio-tiff/license.txt @@ -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. diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxDecoderStream.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxDecoderStream.java new file mode 100644 index 00000000..cdb895f9 --- /dev/null +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxDecoderStream.java @@ -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 Harald Kuhr + * @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, + } + }; +} diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/HorizontalDeDifferencingStream.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/HorizontalDeDifferencingStream.java new file mode 100644 index 00000000..4adaf9ed --- /dev/null +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/HorizontalDeDifferencingStream.java @@ -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 Harald Kuhr + * @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; + } +} diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/JPEGTables.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/JPEGTables.java index a806ec11..b1aba18f 100644 --- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/JPEGTables.java +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/JPEGTables.java @@ -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; diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/LZWDecoder.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/LZWDecoder.java index a8cf4617..bf573fa9 100644 --- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/LZWDecoder.java +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/LZWDecoder.java @@ -33,16 +33,18 @@ import com.twelvemonkeys.io.enc.Decoder; import java.io.IOException; import java.io.InputStream; -import java.util.Arrays; /** - * LZWDecoder + * Lempel–Ziv–Welch (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 Harald Kuhr * @author last modified by $Author: haraldk$ * @version $Id: LZWDecoder.java,v 1.0 08.05.12 21:11 haraldk Exp$ + * @see LZW (Wikipedia) */ -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; + } + } + } } diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFBaseline.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFBaseline.java index 3cc3e5f6..1f76d655 100644 --- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFBaseline.java +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFBaseline.java @@ -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; diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFCustom.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFCustom.java index 5f452b57..d72b489d 100644 --- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFCustom.java +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFCustom.java @@ -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; diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFExtension.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFExtension.java index 4ee2b28e..be17c4c4 100644 --- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFExtension.java +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFExtension.java @@ -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; } diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReader.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReader.java index d45d73e5..87a6efaf 100644 --- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReader.java +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReader.java @@ -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]; } } diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/YCbCrUpsamplerStream.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/YCbCrUpsamplerStream.java new file mode 100644 index 00000000..dc193438 --- /dev/null +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/YCbCrUpsamplerStream.java @@ -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 Harald Kuhr + * @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)); +// } + } + +} diff --git a/imageio/imageio-tiff/src/main/resources/tiff-image-metadata-sun.dtd b/imageio/imageio-tiff/src/main/resources/tiff-image-metadata-sun.dtd new file mode 100644 index 00000000..74451957 --- /dev/null +++ b/imageio/imageio-tiff/src/main/resources/tiff-image-metadata-sun.dtd @@ -0,0 +1,137 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]> \ No newline at end of file diff --git a/imageio/imageio-tiff/src/main/resources/tiff-stream-metadata-sun.dtd b/imageio/imageio-tiff/src/main/resources/tiff-stream-metadata-sun.dtd new file mode 100644 index 00000000..862a0fc0 --- /dev/null +++ b/imageio/imageio-tiff/src/main/resources/tiff-stream-metadata-sun.dtd @@ -0,0 +1,9 @@ + + + + + + + +]> diff --git a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxDecoderStreamTest.java b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxDecoderStreamTest.java new file mode 100644 index 00000000..8c735d82 --- /dev/null +++ b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxDecoderStreamTest.java @@ -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 Harald Kuhr + * @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); + } +} diff --git a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/HorizontalDeDifferencingStreamTest.java b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/HorizontalDeDifferencingStreamTest.java new file mode 100644 index 00000000..2ee1692d --- /dev/null +++ b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/HorizontalDeDifferencingStreamTest.java @@ -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 Harald Kuhr + * @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()); + } +} diff --git a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/LZWDecoderTest.java b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/LZWDecoderTest.java index e2d8dc62..f826ec82 100644 --- a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/LZWDecoderTest.java +++ b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/LZWDecoderTest.java @@ -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 diff --git a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReaderTest.java b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReaderTest.java index ced889d5..88ff7e55 100644 --- a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReaderTest.java +++ b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReaderTest.java @@ -52,10 +52,15 @@ public class TIFFImageReaderTest extends ImageReaderAbstractTestCase getMIMETypes() { return Arrays.asList("image/tiff"); } + + // TODO: Test YCbCr colors } diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/G31DDecoder.java b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/YCbCrUpsamplerStreamTest.java similarity index 72% rename from imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/G31DDecoder.java rename to imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/YCbCrUpsamplerStreamTest.java index cb02dd9b..836816aa 100644 --- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/G31DDecoder.java +++ b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/YCbCrUpsamplerStreamTest.java @@ -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 Harald Kuhr * @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); } } diff --git a/imageio/imageio-tiff/src/test/resources/lzw/lzw-short-45-49.bin b/imageio/imageio-tiff/src/test/resources/lzw/lzw-short-45-49.bin deleted file mode 100755 index 17bd1fcd..00000000 Binary files a/imageio/imageio-tiff/src/test/resources/lzw/lzw-short-45-49.bin and /dev/null differ diff --git a/imageio/imageio-tiff/src/test/resources/lzw/unpacked-short-45-49.bin b/imageio/imageio-tiff/src/test/resources/lzw/unpacked-short-45-49.bin deleted file mode 100644 index 9fb859fe..00000000 Binary files a/imageio/imageio-tiff/src/test/resources/lzw/unpacked-short-45-49.bin and /dev/null differ diff --git a/imageio/imageio-tiff/src/test/resources/tiff/quad-jpeg.tif b/imageio/imageio-tiff/src/test/resources/tiff/quad-jpeg.tif new file mode 100755 index 00000000..e14f4c22 Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/quad-jpeg.tif differ diff --git a/imageio/imageio-tiff/src/test/resources/tiff/quad-lzw.tif b/imageio/imageio-tiff/src/test/resources/tiff/quad-lzw.tif new file mode 100644 index 00000000..ec614bf6 Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/quad-lzw.tif differ diff --git a/imageio/imageio-tiff/src/test/resources/tiff/smallliz.tif b/imageio/imageio-tiff/src/test/resources/tiff/smallliz.tif new file mode 100755 index 00000000..ee9dbb0e Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/smallliz.tif differ diff --git a/imageio/imageio-tiff/src/test/resources/tiff/ycbcr-cat.tif b/imageio/imageio-tiff/src/test/resources/tiff/ycbcr-cat.tif new file mode 100644 index 00000000..445e6ac6 Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/ycbcr-cat.tif differ diff --git a/imageio/imageio-tiff/src/test/resources/tiff/zackthecat.tif b/imageio/imageio-tiff/src/test/resources/tiff/zackthecat.tif new file mode 100644 index 00000000..15185b68 Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/zackthecat.tif differ diff --git a/imageio/pom.xml b/imageio/pom.xml index f91ba124..fc95d0f6 100644 --- a/imageio/pom.xml +++ b/imageio/pom.xml @@ -45,7 +45,7 @@ imageio-jmagick - imageio-reference + imageio-reference @@ -90,6 +90,7 @@ org.mockito mockito-all 1.8.5 + test @@ -115,5 +116,5 @@ test - + diff --git a/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/MappedBufferImage.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/MappedBufferImage.java index 7a31a119..8d69ffce 100644 --- a/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/MappedBufferImage.java +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/MappedBufferImage.java @@ -30,6 +30,7 @@ package com.twelvemonkeys.image; import com.twelvemonkeys.imageio.util.ProgressListenerBase; import com.twelvemonkeys.lang.StringUtil; +import com.twelvemonkeys.util.LRUHashMap; import javax.imageio.ImageIO; import javax.imageio.ImageReadParam; @@ -38,17 +39,17 @@ import javax.imageio.ImageTypeSpecifier; import javax.imageio.stream.ImageInputStream; import javax.swing.*; import java.awt.*; -import java.awt.geom.AffineTransform; +import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; import java.awt.image.BufferedImage; import java.awt.image.DataBuffer; import java.io.File; import java.io.IOException; -import java.util.Iterator; -import java.util.Random; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; +import java.lang.ref.Reference; +import java.lang.ref.SoftReference; +import java.util.*; +import java.util.List; +import java.util.concurrent.*; /** * MappedBufferImage @@ -59,7 +60,7 @@ import java.util.concurrent.TimeUnit; */ public class MappedBufferImage { private static int threads = Runtime.getRuntime().availableProcessors(); - private static ExecutorService executorService = Executors.newFixedThreadPool(threads); + private static ExecutorService executorService = Executors.newFixedThreadPool(threads * 4); public static void main(String[] args) throws IOException { int argIndex = 0; @@ -91,8 +92,9 @@ public class MappedBufferImage { // TODO: Negotiate best layout according to the GraphicsConfiguration. - w = reader.getWidth(0); - h = reader.getHeight(0); + int sub = 1; + w = reader.getWidth(0) / sub; + h = reader.getHeight(0) / sub; // GraphicsConfiguration configuration = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration(); // ColorModel cm2 = configuration.getColorModel(cm.getTransparency()); @@ -111,8 +113,11 @@ public class MappedBufferImage { System.out.println("image = " + image); + // TODO: Display image while reading + ImageReadParam param = reader.getDefaultReadParam(); param.setDestination(image); + param.setSourceSubsampling(sub, sub, 0, 0); reader.addIIOReadProgressListener(new ConsoleProgressListener()); reader.read(0, param); @@ -166,7 +171,7 @@ public class MappedBufferImage { return size; } }; - frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); JScrollPane scroll = new JScrollPane(new ImageComponent(image)); scroll.setBorder(BorderFactory.createEmptyBorder()); frame.add(scroll); @@ -184,13 +189,24 @@ public class MappedBufferImage { // NOTE: The createCompatibleDestImage takes the byte order/layout into account, unlike the cm.createCompatibleWritableRaster final BufferedImage output = new ResampleOp(width, height).createCompatibleDestImage(image, null); - final int inStep = (int) Math.ceil(image.getHeight() / (double) threads); - final int outStep = (int) Math.ceil(height / (double) threads); + final int steps = threads * height / 100; + final int inStep = (int) Math.ceil(image.getHeight() / (double) steps); + final int outStep = (int) Math.ceil(height / (double) steps); - final CountDownLatch latch = new CountDownLatch(threads); + final CountDownLatch latch = new CountDownLatch(steps); + + // System.out.println("Starting image scale on single thread, waiting for execution to complete..."); +// BufferedImage output = new ResampleOp(width, height, ResampleOp.FILTER_LANCZOS).filter(image, null); + System.out.printf("Started image scale on %d threads, waiting for execution to complete...\n", threads); + + System.out.print("["); + final int dotsPerStep = 78 / steps; + for (int j = 0; j < 78 - (steps * dotsPerStep); j++) { + System.out.print("."); + } // Resample image in slices - for (int i = 0; i < threads; i++) { + for (int i = 0; i < steps; i++) { final int inY = i * inStep; final int outY = i * outStep; final int inHeight = Math.min(inStep, image.getHeight() - inY); @@ -200,10 +216,12 @@ public class MappedBufferImage { try { BufferedImage in = image.getSubimage(0, inY, image.getWidth(), inHeight); BufferedImage out = output.getSubimage(0, outY, width, outHeight); - new ResampleOp(width, outHeight, ResampleOp.FILTER_LANCZOS).filter(in, out); -// new ResampleOp(width, outHeight, ResampleOp.FILTER_LANCZOS).resample(in, out, ResampleOp.createFilter(ResampleOp.FILTER_LANCZOS)); -// BufferedImage out = new ResampleOp(width, outHeight, ResampleOp.FILTER_LANCZOS).filter(in, null); -// ImageUtil.drawOnto(output.getSubimage(0, outY, width, outHeight), out); + new ResampleOp(width, outHeight, ResampleOp.FILTER_TRIANGLE).filter(in, out); +// new ResampleOp(width, outHeight, ResampleOp.FILTER_LANCZOS).filter(in, out); + + for (int j = 0; j < dotsPerStep; j++) { + System.out.print("."); + } } catch (RuntimeException e) { e.printStackTrace(); @@ -216,19 +234,17 @@ public class MappedBufferImage { }); } -// System.out.println("Starting image scale on single thread, waiting for execution to complete..."); -// BufferedImage output = new ResampleOp(width, height, ResampleOp.FILTER_LANCZOS).filter(image, null); - System.out.printf("Started image scale on %d threads, waiting for execution to complete...%n", threads); - Boolean done = null; try { done = latch.await(5L, TimeUnit.MINUTES); } catch (InterruptedException ignore) { } + System.out.println("]"); - System.out.printf("%s scaling image in %d ms%n", (done == null ? "Interrupted" : !done ? "Timed out" : "Done"), System.currentTimeMillis() - start); + System.out.printf("%s scaling image in %d ms\n", (done == null ? "Interrupted" : !done ? "Timed out" : "Done"), System.currentTimeMillis() - start); System.out.println("image = " + output); + return output; } @@ -358,10 +374,12 @@ public class MappedBufferImage { private static class ImageComponent extends JComponent implements Scrollable { private final BufferedImage image; private Paint texture; - double zoom = 1; + private double zoom = 1; public ImageComponent(final BufferedImage image) { - setOpaque(true); // Very important when subclassing JComponent... + setOpaque(true); // Very important when sub classing JComponent... + setDoubleBuffered(true); + this.image = image; } @@ -370,6 +388,68 @@ public class MappedBufferImage { super.addNotify(); texture = createTexture(); + + Rectangle bounds = GraphicsEnvironment.getLocalGraphicsEnvironment().getMaximumWindowBounds(); + zoom = Math.min(1.0, Math.min(bounds.getWidth() / (double) image.getWidth(), bounds.getHeight() / (double) image.getHeight())); + + // TODO: Take scroll pane into account when zooming (center around center point) + AbstractAction zoomIn = new AbstractAction() { + public void actionPerformed(ActionEvent e) { + System.err.println("ZOOM IN"); + setZoom(zoom * 2); + } + }; + + addAction(KeyStroke.getKeyStroke(KeyEvent.VK_PLUS, getToolkit().getMenuShortcutKeyMask()), zoomIn); + addAction(KeyStroke.getKeyStroke(KeyEvent.VK_ADD, getToolkit().getMenuShortcutKeyMask()), zoomIn); + addAction(KeyStroke.getKeyStroke(Character.valueOf('+'), 0), zoomIn); + addAction(KeyStroke.getKeyStroke(Character.valueOf('+'), getToolkit().getMenuShortcutKeyMask()), zoomIn); + AbstractAction zoomOut = new AbstractAction() { + public void actionPerformed(ActionEvent e) { + System.err.println("ZOOM OUT"); + setZoom(zoom / 2); + } + }; + addAction(KeyStroke.getKeyStroke(KeyEvent.VK_MINUS, getToolkit().getMenuShortcutKeyMask()), zoomOut); + addAction(KeyStroke.getKeyStroke(KeyEvent.VK_SUBTRACT, getToolkit().getMenuShortcutKeyMask()), zoomOut); + addAction(KeyStroke.getKeyStroke(Character.valueOf('-'), 0), zoomOut); + addAction(KeyStroke.getKeyStroke(Character.valueOf('-'), getToolkit().getMenuShortcutKeyMask()), zoomOut); + AbstractAction zoomFit = new AbstractAction() { + public void actionPerformed(ActionEvent e) { + System.err.println("ZOOM FIT"); +// Rectangle bounds = GraphicsEnvironment.getLocalGraphicsEnvironment().getMaximumWindowBounds(); + Rectangle bounds = getVisibleRect(); + setZoom(Math.min(1.0, Math.min(bounds.getWidth() / (double) image.getWidth(), bounds.getHeight() / (double) image.getHeight()))); + } + }; + addAction(KeyStroke.getKeyStroke(KeyEvent.VK_EQUALS, getToolkit().getMenuShortcutKeyMask()), zoomFit); + addAction(KeyStroke.getKeyStroke(KeyEvent.VK_9, getToolkit().getMenuShortcutKeyMask()), zoomFit); + addAction(KeyStroke.getKeyStroke(KeyEvent.VK_0, getToolkit().getMenuShortcutKeyMask()), new AbstractAction() { + public void actionPerformed(ActionEvent e) { + System.err.println("ZOOM ACTUAL"); + setZoom(1); + } + }); + } + + private void setZoom(final double newZoom) { + if (newZoom != zoom) { + zoom = newZoom; + // TODO: Add PCL support for zoom and discard tiles cache based on property change + tiles = createTileCache(); + revalidate(); + repaint(); + } + } + + private Map createTileCache() { + return Collections.synchronizedMap(new SizedLRUMap(16 * 1024 * 1024)); + } + + private void addAction(final KeyStroke keyStroke, final AbstractAction action) { + UUID key = UUID.randomUUID(); + getInputMap(WHEN_IN_FOCUSED_WINDOW).put(keyStroke, key); + getActionMap().put(key, action); } private Paint createTexture() { @@ -392,10 +472,17 @@ public class MappedBufferImage { @Override protected void paintComponent(Graphics g) { + // TODO: Java 7 kills the performance from our custom painting... :-( + // TODO: Figure out why mouse wheel/track pad scroll repaints entire component, // unlike using the scroll bars of the JScrollPane. // Consider creating a custom mouse wheel listener as a workaround. + // TODO: Cache visible rect content in buffered/volatile image (s) + visible rect (+ zoom) to speed up repaints + // - Blit the cahced image (possibly translated) (onto itself?) + // - Paint only the necessary parts outside the cached image + // - Async rendering into cached image + // We want to paint only the visible part of the image Rectangle visible = getVisibleRect(); Rectangle clip = g.getClipBounds(); @@ -405,9 +492,28 @@ public class MappedBufferImage { g2.setPaint(texture); g2.fillRect(rect.x, rect.y, rect.width, rect.height); + /* + // Center image (might not be the best way to cooperate with the scroll pane) + Rectangle imageSize = new Rectangle((int) Math.round(image.getWidth() * zoom), (int) Math.round(image.getHeight() * zoom)); + if (imageSize.width < getWidth()) { + g2.translate((getWidth() - imageSize.width) / 2, 0); + } + if (imageSize.height < getHeight()) { + g2.translate(0, (getHeight() - imageSize.height) / 2); + } + */ + + // Zoom if (zoom != 1) { - AffineTransform transform = AffineTransform.getScaleInstance(zoom, zoom); - g2.setTransform(transform); + // NOTE: This helps mostly when scaling up, or scaling down less than 50% + g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); + + rect = new Rectangle( + (int) Math.round(rect.x / zoom), (int) Math.round(rect.y / zoom), + (int) Math.round(rect.width / zoom), (int) Math.round(rect.height / zoom) + ); + + rect = rect.intersection(new Rectangle(image.getWidth(), image.getHeight())); } long start = System.currentTimeMillis(); @@ -415,39 +521,308 @@ public class MappedBufferImage { System.err.println("repaint: " + (System.currentTimeMillis() - start) + " ms"); } - private void repaintImage(Rectangle rect, Graphics2D g2) { + static class Tile { + private final int size; + + private final int x; + private final int y; + + private final Reference data; + private final BufferedImage hardRef; + + Tile(int x, int y, BufferedImage data) { + this.x = x; + this.y = y; + this.data = new SoftReference(data); + + hardRef = data; + + size = 16 + data.getWidth() * data.getHeight() * data.getRaster().getNumDataElements() * sizeOf(data.getRaster().getTransferType()); + } + + private static int sizeOf(final int transferType) { + switch (transferType) { + case DataBuffer.TYPE_INT: + return 4; + case DataBuffer.TYPE_SHORT: + return 2; + case DataBuffer.TYPE_BYTE: + return 1; + default: + throw new IllegalArgumentException("Unsupported transfer type: " + transferType); + } + } + + public void drawTo(Graphics2D g) { + BufferedImage img = data.get(); + + if (img != null) { + g.drawImage(img, x, y, null); + } + +// g.setPaint(Color.GREEN); +// g.drawString(String.format("[%d, %d]", x, y), x + 20, y + 20); + } + + public int getX() { + return x; + } + + public int getY() { + return y; + } + + public int getWidth() { + BufferedImage img = data.get(); + return img != null ? img.getWidth() : -1; + } + + public int getHeight() { + BufferedImage img = data.get(); + return img != null ? img.getHeight() : -1; + } + + public Rectangle getRect() { + BufferedImage img = data.get(); + return img != null ? new Rectangle(x, y, img.getWidth(), img.getHeight()) : null; + } + + public Point getLocation() { + return new Point(x, y); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + + if (other == null || getClass() != other.getClass()) { + return false; + } + + Tile tile = (Tile) other; + + return x == tile.x && y == tile.y; + } + + @Override + public int hashCode() { + return 997 * x + y; + } + + @Override + public String toString() { + return String.format("Tile[%d, %d, %d, %d]", x, y, getWidth(), getHeight()); + } + + public int size() { + return size; + } + } + + // TODO: Consider a fixed size (mem) LRUCache instead + Map tiles = createTileCache(); + + private void repaintImage(final Rectangle rect, final Graphics2D g2) { +// System.err.println("rect: " + rect); +// System.err.println("tiles: " + tiles.size()); + // TODO: Fix rounding errors + // FIx repaint bugs + try { // Paint tiles of the image, to preserve memory - int sliceSize = 200; + final int tileSize = 200; - int slicesW = rect.width / sliceSize; - int slicesH = rect.height / sliceSize; + int tilesW = 1 + rect.width / tileSize; + int tilesH = 1 + rect.height / tileSize; - for (int sliceY = 0; sliceY <= slicesH; sliceY++) { - for (int sliceX = 0; sliceX <= slicesW; sliceX++) { - int x = rect.x + sliceX * sliceSize; - int y = rect.y + sliceY * sliceSize; + for (int yTile = 0; yTile <= tilesH; yTile++) { + for (int xTile = 0; xTile <= tilesW; xTile++) { + // Image (source) coordinates + int x = rect.x + xTile * tileSize; + int y = rect.y + yTile * tileSize; - int w = sliceX == slicesW ? Math.min(sliceSize, rect.x + rect.width - x) : sliceSize; - int h = sliceY == slicesH ? Math.min(sliceSize, rect.y + rect.height - y) : sliceSize; + int w = xTile == tilesW ? Math.min(tileSize, rect.x + rect.width - x) : tileSize; + int h = yTile == tilesH ? Math.min(tileSize, rect.y + rect.height - y) : tileSize; if (w == 0 || h == 0) { continue; } // System.err.printf("%04d, %04d, %04d, %04d%n", x, y, w, h); - BufferedImage img = image.getSubimage(x, y, w, h); - g2.drawImage(img, x, y, null); + + // - Get tile from cache + // - If non-null, paint + // - If null, request data for later use, with callback, and return + // TODO: Could we use ImageProducer/ImageConsumer/ImageObserver interface?? + + // Destination (display) coordinates + int dstX = (int) Math.round(x * zoom); + int dstY = (int) Math.round(y * zoom); + int dstW = (int) Math.round(w * zoom); + int dstH = (int) Math.round(h * zoom); + + if (dstW == 0 || dstH == 0) { + continue; + } + + // Don't create overlapping/duplicate tiles... + // - Always start tile grid at 0,0 + // - Always occupy entire tile, unless edge + + // Source (original) coordinates + int tileSrcX = x - x % tileSize; + int tileSrcY = y - y % tileSize; +// final int tileSrcW = Math.min(tileSize, image.getWidth() - tileSrcX); +// final int tileSrcH = Math.min(tileSize, image.getHeight() - tileSrcY); + + // Destination (display) coordinates + int tileDstX = (int) Math.round(tileSrcX * zoom); + int tileDstY = (int) Math.round(tileSrcY * zoom); +// final int tileDstW = (int) Math.round(tileSrcW * zoom); +// final int tileDstH = (int) Math.round(tileSrcH * zoom); + + List points = new ArrayList(4); + points.add(new Point(tileDstX, tileDstY)); + if (tileDstX != dstX) { + points.add(new Point(tileDstX + tileSize, tileDstY)); + } + if (tileDstY != dstY) { + points.add(new Point(tileDstX, tileDstY + tileSize)); + } + if (tileDstX != dstX && tileDstY != dstY) { + points.add(new Point(tileDstX + tileSize, tileDstY + tileSize)); + } + + for (final Point point : points) { + Tile tile = tiles.get(point); + + if (tile != null) { + Reference img = tile.data; + if (img != null) { + tile.drawTo(g2); + continue; + } + else { + tiles.remove(point); + } + } + +// System.err.printf("Tile miss: [%d, %d]\n", dstX, dstY); + + // Dispatch to off-thread worker + final Map localTiles = tiles; + executorService.submit(new Runnable() { + public void run() { + // TODO: Fix rounding issues... Problem is that sometimes the srcW/srcH is 1 pixel off filling the tile... + int tileSrcX = (int) Math.round(point.x / zoom); + int tileSrcY = (int) Math.round(point.y / zoom); + int tileSrcW = Math.min(tileSize, image.getWidth() - tileSrcX); + int tileSrcH = Math.min(tileSize, image.getHeight() - tileSrcY); + int tileDstW = (int) Math.round(tileSrcW * zoom); + int tileDstH = (int) Math.round(tileSrcH * zoom); + + try { + // TODO: Consider comparing zoom/local zoom + if (localTiles != tiles) { + return; // Return early after re-zoom + } + + if (localTiles.containsKey(point)) { +// System.err.println("Skipping tile, already producing..."); + return; + } + + // Test against current view rect, to avoid computing tiles that will be thrown away immediately + // TODO: EDT safe? + if (!getVisibleRect().intersects(new Rectangle(point.x, point.y, tileDstW, tileDstH))) { + return; + } + +// System.err.printf("Creating tile: [%d, %d]\n", tileDstX, tileDstY); + + BufferedImage temp = getGraphicsConfiguration().createCompatibleImage(tileDstW, tileDstH); + final Tile tile = new Tile(point.x, point.y, temp); + localTiles.put(point, tile); + + Graphics2D graphics = temp.createGraphics(); + try { + Object hint = g2.getRenderingHint(RenderingHints.KEY_INTERPOLATION); + + if (hint != null) { + graphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION, hint); + } + + graphics.scale(zoom, zoom); + graphics.drawImage(image.getSubimage(tileSrcX, tileSrcY, tileSrcW, tileSrcH), 0, 0, null); + } + finally { + graphics.dispose(); + } + + SwingUtilities.invokeLater(new Runnable() { + public void run() { + repaint(10, tile.x, tile.y, tile.getWidth(), tile.getHeight()); + } + }); + } + catch (Throwable t) { + localTiles.remove(point); + System.err.println("Boooo: " + t.getMessage()); + } + } + }); + } + } } - -// BufferedImage img = image.getSubimage(rect.x, rect.y, rect.width, rect.height); -// g2.drawImage(img, rect.x, rect.y, null); } catch (NullPointerException e) { // e.printStackTrace(); - // Happens whenever apple.awt.OSXCachingSufraceManager runs out of memory + // Happens whenever apple.awt.OSXCachingSurfaceManager runs out of memory // TODO: Figure out why repaint(x,y,w,h) doesn't work any more..? + System.err.println("Full repaint due to NullPointerException (probably out of memory)."); + repaint(); // NOTE: Might cause a brief flash while the component is redrawn + } + } + + private void repaintImage0(final Rectangle rect, final Graphics2D g2) { + g2.scale(zoom, zoom); + + try { + // Paint tiles of the image, to preserve memory + final int tileSize = 200; + + int tilesW = rect.width / tileSize; + int tilesH = rect.height / tileSize; + + for (int yTile = 0; yTile <= tilesH; yTile++) { + for (int xTile = 0; xTile <= tilesW; xTile++) { + // Image (source) coordinates + final int x = rect.x + xTile * tileSize; + final int y = rect.y + yTile * tileSize; + + final int w = xTile == tilesW ? Math.min(tileSize, rect.x + rect.width - x) : tileSize; + final int h = yTile == tilesH ? Math.min(tileSize, rect.y + rect.height - y) : tileSize; + + if (w == 0 || h == 0) { + continue; + } + +// System.err.printf("%04d, %04d, %04d, %04d%n", x, y, w, h); + + BufferedImage img = image.getSubimage(x, y, w, h); + g2.drawImage(img, x, y, null); + + } + } + } + catch (NullPointerException e) { +// e.printStackTrace(); + // Happens whenever apple.awt.OSXCachingSurfaceManager runs out of memory + // TODO: Figure out why repaint(x,y,w,h) doesn't work any more..? + System.err.println("Full repaint due to NullPointerException (probably out of memory)."); repaint(); // NOTE: Might cause a brief flash while the component is redrawn } } @@ -476,12 +851,68 @@ public class MappedBufferImage { } public boolean getScrollableTracksViewportWidth() { - return false; + return getWidth() > getPreferredSize().width; } public boolean getScrollableTracksViewportHeight() { + return getHeight() > getPreferredSize().height; + } + } + + final static class SizedLRUMap extends LRUHashMap { + int currentSize; + int maxSize; + + public SizedLRUMap(int pMaxSize) { + super(); // Note: super.maxSize doesn't count... + maxSize = pMaxSize; + } + + + protected int sizeOf(final Object pValue) { + ImageComponent.Tile cached = (ImageComponent.Tile) pValue; + + if (cached == null) { + return 0; + } + + return cached.size(); + } + + @Override + public V put(K pKey, V pValue) { + currentSize += sizeOf(pValue); + + V old = super.put(pKey, pValue); + if (old != null) { + currentSize -= sizeOf(old); + } + return old; + } + + @Override + public V remove(Object pKey) { + V old = super.remove(pKey); + if (old != null) { + currentSize -= sizeOf(old); + } + return old; + } + + @Override + protected boolean removeEldestEntry(Map.Entry pEldest) { + if (maxSize <= currentSize) { // NOTE: maxSize here is mem size + removeLRU(); + } return false; } + + @Override + public void removeLRU() { + while (maxSize <= currentSize) { // NOTE: maxSize here is mem size + super.removeLRU(); + } + } } private static class PaintDotsTask implements Runnable { diff --git a/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/util/PersistentMap.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/util/PersistentMap.java index c35ece68..e87673b7 100755 --- a/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/util/PersistentMap.java +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/util/PersistentMap.java @@ -28,6 +28,13 @@ package com.twelvemonkeys.util; +import com.twelvemonkeys.io.FileUtil; + +import java.io.*; +import java.util.*; + +import static com.twelvemonkeys.lang.Validate.notNull; + /** * PersistentMap * @@ -35,27 +42,293 @@ package com.twelvemonkeys.util; * @author last modified by $Author: haraldk$ * @version $Id: PersistentMap.java,v 1.0 May 13, 2009 2:31:29 PM haraldk Exp$ */ -public class PersistentMap { - // TODO: Implement Map - // TODO: Delta synchronization (db?) +public class PersistentMap extends AbstractMap{ + public static final FileFilter DIRECTORIES = new FileFilter() { + public boolean accept(File file) { + return file.isDirectory(); + } + + @Override + public String toString() { + return "[All folders]"; + } + }; + private static final String INDEX = ".index"; + + private final File root; + private final Map index = new LinkedHashMap(); + + private boolean mutable = true; + + + // Idea 2.0: + // - Create directory per hashCode + // - Create file per object in that directory + // - Name file after serialized form of key? Base64? + // - Special case for String/Integer/Long etc? + // - Or create index file in directory with serialized objects + name (uuid) of file + + // TODO: Consider single index file? Or a few? In root directory instead of each directory + // Consider a RAF/FileChannel approach instead of streams - how do we discard portions of a RAF? + // - Need to keep track of used/unused parts of file, scan for gaps etc...? + // - Need to periodically truncate and re-build the index (always as startup, then at every N puts/removes?) + + /*public */PersistentMap(String id) { + this(new File(FileUtil.getTempDirFile(), id)); + } + + public PersistentMap(File root) { + this.root = notNull(root); + + init(); + } + + private void init() { + if (!root.exists() && !root.mkdirs()) { + throw new IllegalStateException(String.format("'%s' does not exist/could not be created", root.getAbsolutePath())); + } + else if (!root.isDirectory()) { + throw new IllegalStateException(String.format("'%s' exists but is not a directory", root.getAbsolutePath())); + } + + if (!root.canRead()) { + throw new IllegalStateException(String.format("'%s' is not readable", root.getAbsolutePath())); + } + + if (!root.canWrite()) { + mutable = false; + } + + FileUtil.visitFiles(root, DIRECTORIES, new Visitor() { + public void visit(File dir) { + // - Read .index file + // - Add entries to index + ObjectInputStream input = null; + try { + input = new ObjectInputStream(new FileInputStream(new File(dir, INDEX))); + while (true) { + @SuppressWarnings({"unchecked"}) + K key = (K) input.readObject(); + String fileName = (String) input.readObject(); + index.put(key, UUID.fromString(fileName)); + } + } + catch (EOFException eof) { + // break here + } + catch (IOException e) { + throw new RuntimeException(e); + } + catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + finally { + FileUtil.close(input); + } + } + }); + } + + @Override + public Set> entrySet() { + return new AbstractSet>() { + @Override + public Iterator> iterator() { + return new Iterator>() { + Iterator> indexIter = index.entrySet().iterator(); + + public boolean hasNext() { + return indexIter.hasNext(); + } + + public Entry next() { + return new Entry() { + final Entry entry = indexIter.next(); + + public K getKey() { + return entry.getKey(); + } + + public V getValue() { + K key = entry.getKey(); + int hash = key != null ? key.hashCode() : 0; + return readVal(hash, entry.getValue()); + } + + public V setValue(V value) { + K key = entry.getKey(); + int hash = key != null ? key.hashCode() : 0; + return writeVal(key, hash, entry.getValue(), value, getValue()); + } + }; + } + + public void remove() { + indexIter.remove(); + } + }; + } + + @Override + public int size() { + return index.size(); + } + }; + } + + @Override + public int size() { + return index.size(); + } + + @Override + public V put(K key, V value) { + V oldVal = null; + + UUID uuid = index.get(key); + int hash = key != null ? key.hashCode() : 0; + + if (uuid != null) { + oldVal = readVal(hash, uuid); + } + + return writeVal(key, hash, uuid, value, oldVal); + } + + private V writeVal(K key, int hash, UUID uuid, V value, V oldVal) { + if (!mutable) { + throw new UnsupportedOperationException(); + } + + File bucket = new File(root, hashToFileName(hash)); + if (!bucket.exists() && !bucket.mkdirs()) { + throw new IllegalStateException(String.format("Could not create bucket '%s'", bucket)); + } + + if (uuid == null) { + // No uuid means new entry + uuid = UUID.randomUUID(); + + File idx = new File(bucket, INDEX); + + ObjectOutputStream output = null; + try { + output = new ObjectOutputStream(new FileOutputStream(idx, true)); + output.writeObject(key); + output.writeObject(uuid.toString()); + + index.put(key, uuid); + } + catch (IOException e) { + throw new RuntimeException(e); + } + finally { + FileUtil.close(output); + } + } + + File entry = new File(bucket, uuid.toString()); + if (value != null) { + ObjectOutputStream output = null; + try { + output = new ObjectOutputStream(new FileOutputStream(entry)); + output.writeObject(value); + + } + catch (IOException e) { + throw new RuntimeException(e); + } + finally { + FileUtil.close(output); + } + } + else if (entry.exists()) { + if (!entry.delete()) { + throw new IllegalStateException(String.format("'%s' could not be deleted", entry)); + } + } + + return oldVal; + } + + private String hashToFileName(int hash) { + return Integer.toString(hash, 16); + } + + @Override + public V get(Object key) { + UUID uuid = index.get(key); + + if (uuid != null) { + int hash = key != null ? key.hashCode() : 0; + return readVal(hash, uuid); + } + + return null; + } + + private V readVal(final int hash, final UUID uuid) { + File bucket = new File(root, hashToFileName(hash)); + File entry = new File(bucket, uuid.toString()); + + if (entry.exists()) { + ObjectInputStream input = null; + try { + input = new ObjectInputStream(new FileInputStream(entry)); + //noinspection unchecked + return (V) input.readObject(); + } + catch (IOException e) { + throw new RuntimeException(e); + } + catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + finally { + FileUtil.close(input); + } + } + + return null; + } + + @Override + public V remove(Object key) { + // TODO!!! + return super.remove(key); + } + + // TODO: Should override size, put, get, remove, containsKey and containsValue + + + } + + /* +Memory mapped file? +Delta sync? + Persistent format Header File ID 4-8 bytes - Size + Size (entries) - Entry pointer array block - Size - Next entry pointer block address - Entry 1 address + PersistentEntry pointer array block (PersistentEntry 0) + Size (bytes) + Next entry pointer block address (0 if last) + PersistentEntry 1 address/offset + key ... - Entry n address + PersistentEntry n address/offset + key + + PersistentEntry 1 + Size (bytes)? + Serialized value or pointer array block + ... + PersistentEntry n + Size (bytes)? + Serialized value or pointer array block - Entry 1 - ... - Entry n - */ \ No newline at end of file diff --git a/servlet/pom.xml b/servlet/pom.xml index 28bb1982..a59056e0 100644 --- a/servlet/pom.xml +++ b/servlet/pom.xml @@ -29,7 +29,7 @@ common-image ${project.version} - + com.twelvemonkeys.common common-lang @@ -65,7 +65,7 @@ 1.2.14 provided - + commons-fileupload commons-fileupload @@ -84,6 +84,7 @@ org.mockito mockito-all 1.8.5 + test @@ -117,5 +118,5 @@ - + diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/AbstractServletMapAdapter.java b/servlet/src/main/java/com/twelvemonkeys/servlet/AbstractServletMapAdapter.java index 3d187231..7ee01471 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/AbstractServletMapAdapter.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/AbstractServletMapAdapter.java @@ -1,7 +1,5 @@ package com.twelvemonkeys.servlet; -import com.twelvemonkeys.util.CollectionUtil; - import java.util.*; /** @@ -11,88 +9,53 @@ import java.util.*; * @author last modified by $Author: haku $ * @version $Id: AbstractServletMapAdapter.java#1 $ */ -abstract class AbstractServletMapAdapter extends AbstractMap> { - // TODO: This map is now a little too lazy.. Should cache entries too (instead?) ! - - private final static List NULL_LIST = new ArrayList(); - - private transient Map> cache = new HashMap>(); - private transient int size = -1; - private transient AbstractSet>> entries; +abstract class AbstractServletMapAdapter extends AbstractMap { + // TODO: This map is now a little too lazy.. Should cache entries! + private transient Set> entries; protected abstract Iterator keysImpl(); - protected abstract Iterator valuesImpl(String pName); + protected abstract T valueImpl(String pName); @Override - public List get(final Object pKey) { + public T get(final Object pKey) { if (pKey instanceof String) { - return getValues((String) pKey); + return valueImpl((String) pKey); } return null; } - private List getValues(final String pName) { - List values = cache.get(pName); - - if (values == null) { - //noinspection unchecked - Iterator headers = valuesImpl(pName); - - if (headers == null) { - cache.put(pName, NULL_LIST); - } - else { - values = toList(headers); - cache.put(pName, values); - } - } - - return values == NULL_LIST ? null : values; - } - - private static List toList(final Iterator pValues) { - List list = new ArrayList(); - CollectionUtil.addAll(list, pValues); - return Collections.unmodifiableList(list); - } - @Override public int size() { - if (size == -1) { - computeSize(); + // Avoid creating expensive entry set for computing size + int size = 0; + + for (Iterator names = keysImpl(); names.hasNext(); names.next()) { + size++; } return size; } - private void computeSize() { - size = 0; - - for (Iterator names = keysImpl(); names.hasNext(); names.next()) { - size++; - } - } - - public Set>> entrySet() { + public Set> entrySet() { if (entries == null) { - entries = new AbstractSet>>() { - public Iterator>> iterator() { - return new Iterator>>() { - Iterator headerNames = keysImpl(); + entries = new AbstractSet>() { + public Iterator> iterator() { + return new Iterator>() { + Iterator keys = keysImpl(); public boolean hasNext() { - return headerNames.hasNext(); + return keys.hasNext(); } - public Entry> next() { + public Entry next() { // TODO: Replace with cached lookup - return new HeaderEntry(headerNames.next()); + return new HeaderEntry(keys.next()); } public void remove() { - throw new UnsupportedOperationException(); + keys.remove(); } }; } @@ -106,34 +69,35 @@ abstract class AbstractServletMapAdapter extends AbstractMap> { - String headerName; + private class HeaderEntry implements Entry { + final String key; - public HeaderEntry(String pHeaderName) { - headerName = pHeaderName; + public HeaderEntry(final String pKey) { + key = pKey; } public String getKey() { - return headerName; + return key; } - public List getValue() { - return get(headerName); + public T getValue() { + return get(key); } - public List setValue(List pValue) { - throw new UnsupportedOperationException(); + public T setValue(final T pValue) { + // Write-through if supported + return put(key, pValue); } @Override public int hashCode() { - List value; - return (headerName == null ? 0 : headerName.hashCode()) ^ - ((value = getValue()) == null ? 0 : value.hashCode()); + T value = getValue(); + return (key == null ? 0 : key.hashCode()) ^ + (value == null ? 0 : value.hashCode()); } @Override - public boolean equals(Object pOther) { + public boolean equals(final Object pOther) { if (pOther == this) { return true; } diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/ServletAttributesMapAdapter.java b/servlet/src/main/java/com/twelvemonkeys/servlet/ServletAttributesMapAdapter.java new file mode 100644 index 00000000..227f12c7 --- /dev/null +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/ServletAttributesMapAdapter.java @@ -0,0 +1,134 @@ +/* + * 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.servlet; + +import javax.servlet.ServletContext; +import javax.servlet.ServletRequest; +import java.util.Enumeration; +import java.util.Iterator; + +import static com.twelvemonkeys.lang.Validate.notNull; + +/** + * ServletAttributesMap + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: ServletAttributesMap.java,v 1.0 01.03.13 10:34 haraldk Exp$ + */ +class ServletAttributesMapAdapter extends AbstractServletMapAdapter { + private final ServletContext context; + private final ServletRequest request; + + ServletAttributesMapAdapter(final ServletContext context) { + this(notNull(context), null); + } + + ServletAttributesMapAdapter(final ServletRequest request) { + this(null, notNull(request)); + } + + private ServletAttributesMapAdapter(final ServletContext context, final ServletRequest request) { + this.context = context; + this.request = request; + } + + @SuppressWarnings("unchecked") + private Enumeration getAttributeNames() { + return context != null ? context.getAttributeNames() : request.getAttributeNames(); + } + + private Object getAttribute(final String name) { + return context != null ? context.getAttribute(name) : request.getAttribute(name); + } + + private Object setAttribute(String name, Object value) { + Object oldValue = getAttribute(name); + + if (context != null) { + context.setAttribute(name, value); + } + else { + request.setAttribute(name, value); + } + + return oldValue; + } + + private Object removeAttribute(String name) { + Object oldValue = getAttribute(name); + + if (context != null) { + context.removeAttribute(name); + } + else { + request.removeAttribute(name); + } + + return oldValue; + } + + @Override + protected Iterator keysImpl() { + final Enumeration keys = getAttributeNames(); + return new Iterator() { + private String key; + + public boolean hasNext() { + return keys.hasMoreElements(); + } + + public String next() { + key = keys.nextElement(); + return key; + } + + public void remove() { + // Support removal of attribute through key iterator + removeAttribute(key); + } + }; + + } + + @Override + protected Object valueImpl(String pName) { + return getAttribute(pName); + } + + @Override + public Object put(String key, Object value) { + return setAttribute(key, value); + } + + @Override + public Object remove(Object key) { + return key instanceof String ? removeAttribute((String) key) : null; + } +} diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/ServletHeadersMapAdapter.java b/servlet/src/main/java/com/twelvemonkeys/servlet/ServletHeadersMapAdapter.java index 76054e11..b547929f 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/ServletHeadersMapAdapter.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/ServletHeadersMapAdapter.java @@ -1,11 +1,11 @@ package com.twelvemonkeys.servlet; -import com.twelvemonkeys.lang.Validate; import com.twelvemonkeys.util.CollectionUtil; import javax.servlet.http.HttpServletRequest; -import java.util.Enumeration; -import java.util.Iterator; +import java.util.*; + +import static com.twelvemonkeys.lang.Validate.notNull; /** * ServletHeadersMapAdapter @@ -14,24 +14,29 @@ import java.util.Iterator; * @author last modified by $Author: haku $ * @version $Id: ServletHeadersMapAdapter.java#1 $ */ -class ServletHeadersMapAdapter extends AbstractServletMapAdapter { +class ServletHeadersMapAdapter extends AbstractServletMapAdapter> { protected final HttpServletRequest request; - public ServletHeadersMapAdapter(HttpServletRequest pRequest) { - request = Validate.notNull(pRequest, "request"); + public ServletHeadersMapAdapter(final HttpServletRequest pRequest) { + request = notNull(pRequest, "request"); } - protected Iterator valuesImpl(String pName) { - //noinspection unchecked + protected List valueImpl(final String pName) { + @SuppressWarnings("unchecked") Enumeration headers = request.getHeaders(pName); - return headers == null ? null : CollectionUtil.iterator(headers); + return headers == null ? null : toList(CollectionUtil.iterator(headers)); + } + + private static List toList(final Iterator pValues) { + List list = new ArrayList(); + CollectionUtil.addAll(list, pValues); + return Collections.unmodifiableList(list); } protected Iterator keysImpl() { - //noinspection unchecked + @SuppressWarnings("unchecked") Enumeration headerNames = request.getHeaderNames(); return headerNames == null ? null : CollectionUtil.iterator(headerNames); } - } diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/ServletParametersMapAdapter.java b/servlet/src/main/java/com/twelvemonkeys/servlet/ServletParametersMapAdapter.java index 01dca170..9324d2fb 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/ServletParametersMapAdapter.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/ServletParametersMapAdapter.java @@ -1,11 +1,14 @@ package com.twelvemonkeys.servlet; -import com.twelvemonkeys.lang.Validate; import com.twelvemonkeys.util.CollectionUtil; -import javax.servlet.http.HttpServletRequest; +import javax.servlet.ServletRequest; +import java.util.Arrays; import java.util.Enumeration; import java.util.Iterator; +import java.util.List; + +import static com.twelvemonkeys.lang.Validate.notNull; /** * ServletParametersMapAdapter @@ -14,23 +17,23 @@ import java.util.Iterator; * @author last modified by $Author: haku $ * @version $Id: ServletParametersMapAdapter.java#1 $ */ -class ServletParametersMapAdapter extends AbstractServletMapAdapter { +class ServletParametersMapAdapter extends AbstractServletMapAdapter> { + // TODO: Be able to piggyback on HttpServletRequest.getParameterMap when available? - protected final HttpServletRequest request; + protected final ServletRequest request; - public ServletParametersMapAdapter(HttpServletRequest pRequest) { - request = Validate.notNull(pRequest, "request"); + public ServletParametersMapAdapter(final ServletRequest pRequest) { + request = notNull(pRequest, "request"); } - protected Iterator valuesImpl(String pName) { + protected List valueImpl(String pName) { String[] values = request.getParameterValues(pName); - return values == null ? null : CollectionUtil.iterator(values); + return values == null ? null : Arrays.asList(values); } protected Iterator keysImpl() { - //noinspection unchecked + @SuppressWarnings("unchecked") Enumeration names = request.getParameterNames(); return names == null ? null : CollectionUtil.iterator(names); } - } \ No newline at end of file diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/ServletUtil.java b/servlet/src/main/java/com/twelvemonkeys/servlet/ServletUtil.java index 0892075e..b2019707 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/ServletUtil.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/ServletUtil.java @@ -50,7 +50,7 @@ import java.util.Map; /** * Various servlet related helper methods. * - * @author Harald Kuhr + * @author Harald Kuhr * @author Eirik Torske * @author last modified by $Author: haku $ * @version $Id: ServletUtil.java#3 $ @@ -544,7 +544,7 @@ public final class ServletUtil { /** * Returns a {@code URL} containing the real path for a given virtual * path, on URL form. - * Note that this mehtod will return {@code null} for all the same reasons + * Note that this method will return {@code null} for all the same reasons * as {@code ServletContext.getRealPath(java.lang.String)} does. * * @param pContext the servlet context @@ -566,7 +566,7 @@ public final class ServletUtil { } /** - * Gets the temp directory for the given {@code ServletContext} (webapp). + * Gets the temp directory for the given {@code ServletContext} (web app). * * @param pContext the servlet context * @return the temp directory @@ -634,13 +634,30 @@ public final class ServletUtil { return new ServletConfigMapAdapter(pContext); } - // TODO? -// public static Map attributesAsMap(final ServletContext pContext) { -// } -// -// public static Map attributesAsMap(final ServletRequest pRequest) { -// } -// + /** + * Creates an modifiable {@code Map} view of the given + * {@code ServletContext}s attributes. + * + * @param pContext the servlet context + * @return a {@code Map} view of the attributes + * @throws IllegalArgumentException if {@code pContext} is {@code null} + */ + public static Map attributesAsMap(final ServletContext pContext) { + return new ServletAttributesMapAdapter(pContext); + } + + /** + * Creates an modifiable {@code Map} view of the given + * {@code ServletRequest}s attributes. + * + * @param pRequest the servlet request + * @return a {@code Map} view of the attributes + * @throws IllegalArgumentException if {@code pContext} is {@code null} + */ + public static Map attributesAsMap(final ServletRequest pRequest) { + return new ServletAttributesMapAdapter(pRequest); + } + /** * Creates an unmodifiable {@code Map} view of the given * {@code HttpServletRequest}s request parameters. @@ -649,7 +666,7 @@ public final class ServletUtil { * @return a {@code Map} view of the request parameters * @throws IllegalArgumentException if {@code pRequest} is {@code null} */ - public static Map> parametersAsMap(final HttpServletRequest pRequest) { + public static Map> parametersAsMap(final ServletRequest pRequest) { return new ServletParametersMapAdapter(pRequest); } diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/cache/HTTPCache.java b/servlet/src/main/java/com/twelvemonkeys/servlet/cache/HTTPCache.java index 0591821b..d6cce8bd 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/cache/HTTPCache.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/cache/HTTPCache.java @@ -1089,13 +1089,13 @@ public class HTTPCache { // TODO: Extract and make public? final static class SizedLRUMap extends LRUHashMap { - int mSize; - int mMaxSize; + int currentSize; + int maxSize; public SizedLRUMap(int pMaxSize) { //super(true); - super(); // Note: super.mMaxSize doesn't count... - mMaxSize = pMaxSize; + super(); // Note: super.maxSize doesn't count... + maxSize = pMaxSize; } @@ -1113,11 +1113,11 @@ public class HTTPCache { @Override public V put(K pKey, V pValue) { - mSize += sizeOf(pValue); + currentSize += sizeOf(pValue); V old = super.put(pKey, pValue); if (old != null) { - mSize -= sizeOf(old); + currentSize -= sizeOf(old); } return old; } @@ -1126,14 +1126,14 @@ public class HTTPCache { public V remove(Object pKey) { V old = super.remove(pKey); if (old != null) { - mSize -= sizeOf(old); + currentSize -= sizeOf(old); } return old; } @Override protected boolean removeEldestEntry(Map.Entry pEldest) { - if (mMaxSize <= mSize) { // NOTE: mMaxSize here is mem size + if (maxSize <= currentSize) { // NOTE: maxSize here is mem size removeLRU(); } return false; @@ -1141,10 +1141,10 @@ public class HTTPCache { @Override public void removeLRU() { - while (mMaxSize <= mSize) { // NOTE: mMaxSize here is mem size + while (maxSize <= currentSize) { // NOTE: maxSize here is mem size super.removeLRU(); } } } -} \ No newline at end of file +} diff --git a/servlet/src/test/java/com/twelvemonkeys/servlet/ServletAttributesMapAdapterContextTest.java b/servlet/src/test/java/com/twelvemonkeys/servlet/ServletAttributesMapAdapterContextTest.java new file mode 100755 index 00000000..759714e4 --- /dev/null +++ b/servlet/src/test/java/com/twelvemonkeys/servlet/ServletAttributesMapAdapterContextTest.java @@ -0,0 +1,156 @@ +/* + * 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.servlet; + +import com.twelvemonkeys.util.MapAbstractTestCase; +import org.mockito.Mockito; + +import javax.servlet.ServletContext; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +import static org.mockito.Mockito.mock; + +/** + * ServletConfigMapAdapterTestCase + *

+ * + * @author Harald Kuhr + * @version $Id: ServletAttributesMapAdapterTestCase.java#1 $ + */ +public class ServletAttributesMapAdapterContextTest extends MapAbstractTestCase { + private static final String ATTRIB_VALUE_ETAG = "\"1234567890abcdef\""; + private static final Date ATTRIB_VALUE_DATE = new Date(); + private static final List ATTRIB_VALUE_FOO = Arrays.asList(1, 2); + + @Override + public boolean isTestSerialization() { + return false; + } + + @Override + public boolean isAllowNullKey() { + return false; // Makes no sense... + } + + @Override + public boolean isAllowNullValue() { + return false; // Should be allowed, but the tests don't handle the put(foo, null) == remove(foo) semantics + } + + public Map makeEmptyMap() { + MockServletContextImpl context = mock(MockServletContextImpl.class, Mockito.CALLS_REAL_METHODS); + context.attributes = createAttributes(false); + + return new ServletAttributesMapAdapter(context); + } + + @Override + public Map makeFullMap() { + MockServletContextImpl context = mock(MockServletContextImpl.class, Mockito.CALLS_REAL_METHODS); + context.attributes = createAttributes(true); + + return new ServletAttributesMapAdapter(context); + } + + private Map createAttributes(boolean initialValues) { + Map map = new ConcurrentHashMap(); + + if (initialValues) { + String[] sampleKeys = (String[]) getSampleKeys(); + for (int i = 0; i < sampleKeys.length; i++) { + map.put(sampleKeys[i], getSampleValues()[i]); + } + } + + return map; + } + + @Override + public Object[] getSampleKeys() { + return new String[] {"Date", "ETag", "X-Foo"}; + } + + @Override + public Object[] getSampleValues() { + return new Object[] {ATTRIB_VALUE_DATE, ATTRIB_VALUE_ETAG, ATTRIB_VALUE_FOO}; + } + + @Override + public Object[] getNewSampleValues() { + // Needs to be same length but different values + return new Object[] {new Date(-1l), "foo/bar", Arrays.asList(2, 3, 4)}; + } + + @SuppressWarnings("unchecked") + @Override + public void testMapPutNullValue() { + // Special null semantics + resetFull(); + + int size = map.size(); + String key = getClass().getName() + ".someNewKey"; + map.put(key, null); + assertEquals(size, map.size()); + assertFalse(map.containsKey(key)); + + map.put(getSampleKeys()[0], null); + assertEquals(size - 1, map.size()); + assertFalse(map.containsKey(getSampleKeys()[0])); + + map.remove(getSampleKeys()[1]); + assertEquals(size - 2, map.size()); + assertFalse(map.containsKey(getSampleKeys()[1])); + } + + private static abstract class MockServletContextImpl implements ServletContext { + Map attributes; + + public Object getAttribute(String name) { + return attributes.get(name); + } + + public Enumeration getAttributeNames() { + return Collections.enumeration(attributes.keySet()); + } + + public void setAttribute(String name, Object o) { + if (o == null) { + attributes.remove(name); + } + else { + attributes.put(name, o); + } + } + + public void removeAttribute(String name) { + attributes.remove(name); + } + } +} diff --git a/servlet/src/test/java/com/twelvemonkeys/servlet/ServletAttributesMapAdapterRequestTest.java b/servlet/src/test/java/com/twelvemonkeys/servlet/ServletAttributesMapAdapterRequestTest.java new file mode 100755 index 00000000..ee461352 --- /dev/null +++ b/servlet/src/test/java/com/twelvemonkeys/servlet/ServletAttributesMapAdapterRequestTest.java @@ -0,0 +1,156 @@ +/* + * 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.servlet; + +import com.twelvemonkeys.util.MapAbstractTestCase; +import org.mockito.Mockito; + +import javax.servlet.ServletRequest; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +import static org.mockito.Mockito.mock; + +/** + * ServletConfigMapAdapterTestCase + *

+ * + * @author Harald Kuhr + * @version $Id: ServletAttributesMapAdapterTestCase.java#1 $ + */ +public class ServletAttributesMapAdapterRequestTest extends MapAbstractTestCase { + private static final String ATTRIB_VALUE_ETAG = "\"1234567890abcdef\""; + private static final Date ATTRIB_VALUE_DATE = new Date(); + private static final List ATTRIB_VALUE_FOO = Arrays.asList(1, 2); + + @Override + public boolean isTestSerialization() { + return false; + } + + @Override + public boolean isAllowNullKey() { + return false; // Makes no sense... + } + + @Override + public boolean isAllowNullValue() { + return false; // Should be allowed, but the tests don't handle the put(foo, null) == remove(foo) semantics + } + + public Map makeEmptyMap() { + MockServletRequestImpl request = mock(MockServletRequestImpl.class, Mockito.CALLS_REAL_METHODS); + request.attributes = createAttributes(false); + + return new ServletAttributesMapAdapter(request); + } + + @Override + public Map makeFullMap() { + MockServletRequestImpl request = mock(MockServletRequestImpl.class, Mockito.CALLS_REAL_METHODS); + request.attributes = createAttributes(true); + + return new ServletAttributesMapAdapter(request); + } + + private Map createAttributes(boolean initialValues) { + Map map = new ConcurrentHashMap(); + + if (initialValues) { + String[] sampleKeys = (String[]) getSampleKeys(); + for (int i = 0; i < sampleKeys.length; i++) { + map.put(sampleKeys[i], getSampleValues()[i]); + } + } + + return map; + } + + @Override + public Object[] getSampleKeys() { + return new String[] {"Date", "ETag", "X-Foo"}; + } + + @Override + public Object[] getSampleValues() { + return new Object[] {ATTRIB_VALUE_DATE, ATTRIB_VALUE_ETAG, ATTRIB_VALUE_FOO}; + } + + @Override + public Object[] getNewSampleValues() { + // Needs to be same length but different values + return new Object[] {new Date(-1l), "foo/bar", Arrays.asList(2, 3, 4)}; + } + + @SuppressWarnings("unchecked") + @Override + public void testMapPutNullValue() { + // Special null semantics + resetFull(); + + int size = map.size(); + String key = getClass().getName() + ".someNewKey"; + map.put(key, null); + assertEquals(size, map.size()); + assertFalse(map.containsKey(key)); + + map.put(getSampleKeys()[0], null); + assertEquals(size - 1, map.size()); + assertFalse(map.containsKey(getSampleKeys()[0])); + + map.remove(getSampleKeys()[1]); + assertEquals(size - 2, map.size()); + assertFalse(map.containsKey(getSampleKeys()[1])); + } + + private static abstract class MockServletRequestImpl implements ServletRequest { + Map attributes; + + public Object getAttribute(String name) { + return attributes.get(name); + } + + public Enumeration getAttributeNames() { + return Collections.enumeration(attributes.keySet()); + } + + public void setAttribute(String name, Object o) { + if (o == null) { + attributes.remove(name); + } + else { + attributes.put(name, o); + } + } + + public void removeAttribute(String name) { + attributes.remove(name); + } + } +} diff --git a/servlet/src/test/java/com/twelvemonkeys/servlet/ServletConfigMapAdapterTestCase.java b/servlet/src/test/java/com/twelvemonkeys/servlet/ServletConfigMapAdapterTest.java similarity index 89% rename from servlet/src/test/java/com/twelvemonkeys/servlet/ServletConfigMapAdapterTestCase.java rename to servlet/src/test/java/com/twelvemonkeys/servlet/ServletConfigMapAdapterTest.java index 4b60c01c..2d594264 100755 --- a/servlet/src/test/java/com/twelvemonkeys/servlet/ServletConfigMapAdapterTestCase.java +++ b/servlet/src/test/java/com/twelvemonkeys/servlet/ServletConfigMapAdapterTest.java @@ -1,13 +1,15 @@ package com.twelvemonkeys.servlet; import com.twelvemonkeys.util.MapAbstractTestCase; +import org.junit.runner.RunWith; +import org.junit.runners.Suite; import javax.servlet.*; -import java.util.*; -import java.io.Serializable; import java.io.InputStream; -import java.net.URL; +import java.io.Serializable; import java.net.MalformedURLException; +import java.net.URL; +import java.util.*; /** * ServletConfigMapAdapterTestCase @@ -16,7 +18,12 @@ import java.net.MalformedURLException; * @author Harald Kuhr * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/test/java/com/twelvemonkeys/servlet/ServletConfigMapAdapterTestCase.java#3 $ */ -public abstract class ServletConfigMapAdapterTestCase extends MapAbstractTestCase { +@RunWith(Suite.class) +@Suite.SuiteClasses({AbstractServletConfigMapAdapterTest.ServletConfigMapTest.class, AbstractServletConfigMapAdapterTest.FilterConfigMapTest.class, AbstractServletConfigMapAdapterTest.ServletContextMapTest.class}) +public final class ServletConfigMapAdapterTest { +} + +abstract class AbstractServletConfigMapAdapterTest extends MapAbstractTestCase { public boolean isPutAddSupported() { return false; @@ -148,7 +155,7 @@ public abstract class ServletConfigMapAdapterTestCase extends MapAbstractTestCas } } - public static final class ServletConfigMapTestCase extends ServletConfigMapAdapterTestCase { + public static final class ServletConfigMapTest extends AbstractServletConfigMapAdapterTest { public Map makeEmptyMap() { ServletConfig config = new TestConfig(); @@ -162,7 +169,7 @@ public abstract class ServletConfigMapAdapterTestCase extends MapAbstractTestCas } } - public static final class FilterConfigMapTestCase extends ServletConfigMapAdapterTestCase { + public static final class FilterConfigMapTest extends AbstractServletConfigMapAdapterTest { public Map makeEmptyMap() { FilterConfig config = new TestConfig(); @@ -176,7 +183,7 @@ public abstract class ServletConfigMapAdapterTestCase extends MapAbstractTestCas } } - public static final class ServletContextMapTestCase extends ServletConfigMapAdapterTestCase { + public static final class ServletContextMapTest extends AbstractServletConfigMapAdapterTest { public Map makeEmptyMap() { ServletContext config = new TestConfig(); diff --git a/servlet/src/test/java/com/twelvemonkeys/servlet/ServletHeadersMapAdapterTestCase.java b/servlet/src/test/java/com/twelvemonkeys/servlet/ServletHeadersMapAdapterTest.java similarity index 97% rename from servlet/src/test/java/com/twelvemonkeys/servlet/ServletHeadersMapAdapterTestCase.java rename to servlet/src/test/java/com/twelvemonkeys/servlet/ServletHeadersMapAdapterTest.java index 2a3c0861..5c97d226 100755 --- a/servlet/src/test/java/com/twelvemonkeys/servlet/ServletHeadersMapAdapterTestCase.java +++ b/servlet/src/test/java/com/twelvemonkeys/servlet/ServletHeadersMapAdapterTest.java @@ -17,7 +17,7 @@ import static org.mockito.Mockito.when; * @author Harald Kuhr * @version $Id: ServletHeadersMapAdapterTestCase.java#1 $ */ -public class ServletHeadersMapAdapterTestCase extends MapAbstractTestCase { +public class ServletHeadersMapAdapterTest extends MapAbstractTestCase { private static final List HEADER_VALUE_ETAG = Arrays.asList("\"1234567890abcdef\""); private static final List HEADER_VALUE_DATE = Arrays.asList(new Date().toString()); private static final List HEADER_VALUE_FOO = Arrays.asList("one", "two"); diff --git a/servlet/src/test/java/com/twelvemonkeys/servlet/ServletParametersMapAdapterTestCase.java b/servlet/src/test/java/com/twelvemonkeys/servlet/ServletParametersMapAdapterTest.java similarity index 97% rename from servlet/src/test/java/com/twelvemonkeys/servlet/ServletParametersMapAdapterTestCase.java rename to servlet/src/test/java/com/twelvemonkeys/servlet/ServletParametersMapAdapterTest.java index 883c1ac6..9d412216 100755 --- a/servlet/src/test/java/com/twelvemonkeys/servlet/ServletParametersMapAdapterTestCase.java +++ b/servlet/src/test/java/com/twelvemonkeys/servlet/ServletParametersMapAdapterTest.java @@ -17,7 +17,7 @@ import static org.mockito.Mockito.when; * @author Harald Kuhr * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/test/java/com/twelvemonkeys/servlet/ServletParametersMapAdapterTestCase.java#1 $ */ -public class ServletParametersMapAdapterTestCase extends MapAbstractTestCase { +public class ServletParametersMapAdapterTest extends MapAbstractTestCase { private static final List PARAM_VALUE_ETAG = Arrays.asList("\"1234567890abcdef\""); private static final List PARAM_VALUE_DATE = Arrays.asList(new Date().toString()); private static final List PARAM_VALUE_FOO = Arrays.asList("one", "two"); @@ -93,4 +93,4 @@ public class ServletParametersMapAdapterTestCase extends MapAbstractTestCase { return Collections.enumeration(collection); } } -} \ No newline at end of file +}