Merge remote-tracking branch 'upstream/master'

Conflicts:
	servlet/src/main/java/com/twelvemonkeys/servlet/ServletUtil.java
	servlet/src/main/java/com/twelvemonkeys/servlet/cache/HTTPCache.java
	servlet/src/test/java/com/twelvemonkeys/servlet/ServletConfigMapAdapterTest.java
	servlet/src/test/java/com/twelvemonkeys/servlet/ServletHeadersMapAdapterTest.java
	servlet/src/test/java/com/twelvemonkeys/servlet/ServletParametersMapAdapterTest.java
This commit is contained in:
Rune Bremnes
2013-08-14 09:45:32 +02:00
85 changed files with 5283 additions and 978 deletions

View File

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

View File

@@ -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
* <em>higher 16 bits</em> 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:
* <blockquote>{@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();
}
*/
}

View File

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

View File

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

View File

@@ -39,7 +39,7 @@ import java.util.Hashtable;
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @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 <em>same</em> {@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 {
* <p/>
* 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)}.
* <p/>
* 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)}
* <p/>
@@ -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:
* <TABLE border="1" cellspacing="0">
* <TR><TD>0.0</TD><TD>-{@code pAmmount}</TD><TD>0.0</TD></TR>
* <TR><TD>-{@code pAmmount}</TD>
* <TD>4.0 * {@code pAmmount} + 1.0</TD>
* <TD>-{@code pAmmount}</TD></TR>
* <TR><TD>0.0</TD><TD>-{@code pAmmount}</TD><TD>0.0</TD></TR>
* <TR><TD>0.0</TD><TD>-{@code pAmount}</TD><TD>0.0</TD></TR>
* <TR><TD>-{@code pAmount}</TD>
* <TD>4.0 * {@code pAmount} + 1.0</TD>
* <TD>-{@code pAmount}</TD></TR>
* <TR><TD>0.0</TD><TD>-{@code pAmount}</TD><TD>0.0</TD></TR>
* </TABLE>
*
* @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}.
* <p/>
* 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();
}
}
}

View File

@@ -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.
* <p/>
@@ -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.
* <p/>
* 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.
* <p/>
@@ -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.
* <p/>
@@ -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.
* <p/>
@@ -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.
* <p/>
@@ -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.
* <p/>
@@ -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.
* <p/>
@@ -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)) {

View File

@@ -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 <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @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()];

View File

@@ -65,7 +65,8 @@ import java.io.*;
* @see java.io.DataOutput
*
* @author Elliotte Rusty Harold
* @version 1.0.3, 28 December 2002
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @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);
}
/**

View File

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

View File

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

View File

@@ -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 <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @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());
}
}

View File

@@ -41,8 +41,7 @@ import java.util.Arrays;
/**
* A utility class with some useful bean-related functions.
* <p/>
* <em>NOTE: This class is not considered part of the public API and may be
* changed without notice</em>
* <em>NOTE: This class is not considered part of the public API and may be changed without notice</em>
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @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> T createInstance(Class<T> 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> T createInstance(Class<T> 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<T> 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);
}

View File

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

View File

@@ -205,6 +205,7 @@ public class LRUHashMap<K, V> extends LinkedHashMap<K, V> implements ExpiringMap
*/
public void removeLRU() {
int removeCount = (int) Math.max((size() * trimFactor), 1);
Iterator<Map.Entry<K, V>> entries = entrySet().iterator();
while ((removeCount--) > 0 && entries.hasNext()) {
entries.next();

View File

@@ -45,7 +45,7 @@ import java.util.Map;
* </ul>
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @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<K, V> extends LinkedMap<K, V> implements ExpiringMap<K, V> {
@@ -222,8 +222,9 @@ public class LRUMap<K, V> extends LinkedMap<K, V> implements ExpiringMap<K, V> {
*/
public void removeLRU() {
int removeCount = (int) Math.max((size() * trimFactor), 1);
while ((removeCount--) > 0) {
removeEntry(head.mNext);
removeEntry(head.next);
}
}
}

View File

@@ -181,19 +181,19 @@ public class LinkedMap<K, V> extends AbstractDecoratedMap<K, V> 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<K, V> extends AbstractDecoratedMap<K, V> implements Seria
}
private abstract class LinkedMapIterator<E> implements Iterator<E> {
LinkedEntry<K, V> mNextEntry = head.mNext;
LinkedEntry<K, V> mNextEntry = head.next;
LinkedEntry<K, V> mLastReturned = null;
/**
@@ -254,7 +254,7 @@ public class LinkedMap<K, V> extends AbstractDecoratedMap<K, V> implements Seria
}
LinkedEntry<K, V> e = mLastReturned = mNextEntry;
mNextEntry = e.mNext;
mNextEntry = e.next;
return e;
}
@@ -309,7 +309,7 @@ public class LinkedMap<K, V> extends AbstractDecoratedMap<K, V> implements Seria
oldValue = null;
// Remove eldest entry if instructed, else grow capacity if appropriate
LinkedEntry<K, V> eldest = head.mNext;
LinkedEntry<K, V> eldest = head.next;
if (removeEldestEntry(eldest)) {
removeEntry(eldest);
}
@@ -407,13 +407,13 @@ public class LinkedMap<K, V> extends AbstractDecoratedMap<K, V> implements Seria
* Linked list implementation of {@code Map.Entry}.
*/
protected static class LinkedEntry<K, V> extends BasicEntry<K, V> implements Serializable {
LinkedEntry<K, V> mPrevious;
LinkedEntry<K, V> mNext;
LinkedEntry<K, V> previous;
LinkedEntry<K, V> next;
LinkedEntry(K pKey, V pValue, LinkedEntry<K, V> pNext) {
super(pKey, pValue);
mNext = pNext;
next = pNext;
}
/**
@@ -423,19 +423,19 @@ public class LinkedMap<K, V> extends AbstractDecoratedMap<K, V> implements Seria
* @param pExisting the entry to add before
*/
void addBefore(LinkedEntry<K, V> 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<K, V> extends AbstractDecoratedMap<K, V> 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<K, V> pMap) {
// TODO: Is this REALLY correct?

View File

@@ -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.
* <p/>
* 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<Class, PropertyConverter> converters = new Hashtable<Class, PropertyConverter>();
// 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);
}

View File

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

View File

@@ -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<String, String> map = new HashMap<String, String>();
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<String, Integer> map = new HashMap<String, Integer>();
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<String, Double> map = new HashMap<String, Double>();
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;
}
}
}

View File

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

View File

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

View File

@@ -73,8 +73,10 @@ public final class ColorSpaces {
private final static boolean DEBUG = "true".equalsIgnoreCase(System.getProperty("com.twelvemonkeys.imageio.color.debug"));
// JDK 7 seems to handle non-perceptual rendering intents gracefully, so we don't need to fiddle with the profiles
private final static boolean JDK_HANDLES_RENDERING_INTENTS = SystemUtil.isClassAvailable("java.lang.invoke.CallSite");
// OpenJDK 7 seems to handle non-perceptual rendering intents gracefully, so we don't need to fiddle with the profiles.
// However, the later Oracle distribute JDK seems to include the color management code that has the known bugs...
private final static boolean JDK_HANDLES_RENDERING_INTENTS =
SystemUtil.isClassAvailable("java.lang.invoke.CallSite") && !SystemUtil.isClassAvailable("sun.java2d.cmm.kcms.CMM");
// NOTE: java.awt.color.ColorSpace.CS_* uses 1000-1004, we'll use 5000+ to not interfere with future additions
@@ -171,6 +173,21 @@ public final class ColorSpaces {
}
}
/**
* Tests whether an ICC color profile is equal to the default sRGB profile.
*
* @param profile the ICC profile to test. May not be {@code null}.
* @return {@code true} if {@code profile} is equal to the default sRGB profile.
* @throws IllegalArgumentException if {@code profile} is {@code null}
*
* @see java.awt.color.ColorSpace#isCS_sRGB()
*/
public static boolean isCS_sRGB(final ICC_Profile profile) {
Validate.notNull(profile, "profile");
return profile.getColorSpaceType() == ColorSpace.TYPE_RGB && Arrays.equals(profile.getData(ICC_Profile.icSigHead), sRGB.header);
}
/**
* Tests whether an ICC color profile is known to cause problems for {@link java.awt.image.ColorConvertOp}.
* <p />
@@ -227,7 +244,7 @@ public final class ColorSpaces {
if (profile == null) {
// Fall back to the bundled ClayRGB1998 public domain Adobe RGB 1998 compatible profile,
// identical for all practical purposes
// which is identical for all practical purposes
profile = readProfileFromClasspathResource("/profiles/ClayRGB1998.icc");
if (profile == null) {
@@ -337,15 +354,19 @@ public final class ColorSpaces {
private static class sRGB {
private static final byte[] header = ICC_Profile.getInstance(ColorSpace.CS_sRGB).getData(ICC_Profile.icSigHead);
}
private static class CIEXYZ {
private static final byte[] header = ICC_Profile.getInstance(ColorSpace.CS_CIEXYZ).getData(ICC_Profile.icSigHead);
}
private static class PYCC {
private static final byte[] header = ICC_Profile.getInstance(ColorSpace.CS_PYCC).getData(ICC_Profile.icSigHead);
}
private static class GRAY {
private static final byte[] header = ICC_Profile.getInstance(ColorSpace.CS_GRAY).getData(ICC_Profile.icSigHead);
}
private static class LINEAR_RGB {
private static final byte[] header = ICC_Profile.getInstance(ColorSpace.CS_LINEAR_RGB).getData(ICC_Profile.icSigHead);
}
@@ -359,7 +380,14 @@ public final class ColorSpaces {
systemDefaults = SystemUtil.loadProperties(ColorSpaces.class, "com/twelvemonkeys/imageio/color/icc_profiles_" + os.id());
}
catch (IOException ignore) {
ignore.printStackTrace();
System.err.printf(
"Warning: Could not load system default ICC profile locations from %s, will use bundled fallback profiles.\n",
ignore.getMessage()
);
if (DEBUG) {
ignore.printStackTrace();
}
systemDefaults = null;
}

View File

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

View File

@@ -26,4 +26,4 @@
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
GENERIC_CMYK=/C:/Windows/System32/spool/drivers/color/RSWOP.icm
#ADOBE_RGB_1998=use built in for now
#ADOBE_RGB_1998=unknown, use built in for now

View File

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

View File

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

View File

@@ -0,0 +1,25 @@
Copyright (c) 2013, Harald Kuhr
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name "TwelveMonkeys" nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -29,13 +29,13 @@
package com.twelvemonkeys.imageio.plugins.jpeg;
/**
* AdobeDCT
* AdobeDCTSegment
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: AdobeDCT.java,v 1.0 23.04.12 16:55 haraldk Exp$
* @version $Id: AdobeDCTSegment.java,v 1.0 23.04.12 16:55 haraldk Exp$
*/
class AdobeDCT {
class AdobeDCTSegment {
public static final int Unknown = 0;
public static final int YCC = 1;
public static final int YCCK = 2;
@@ -45,7 +45,7 @@ class AdobeDCT {
final int flags1;
final int transform;
public AdobeDCT(int version, int flags0, int flags1, int transform) {
AdobeDCTSegment(int version, int flags0, int flags1, int transform) {
this.version = version; // 100 or 101
this.flags0 = flags0;
this.flags1 = flags1;

View File

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

View File

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

View File

@@ -45,6 +45,7 @@ import com.twelvemonkeys.lang.Validate;
import javax.imageio.*;
import javax.imageio.event.IIOReadUpdateListener;
import javax.imageio.event.IIOReadWarningListener;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream;
import java.awt.*;
@@ -58,8 +59,26 @@ import java.util.List;
/**
* A JPEG {@code ImageReader} implementation based on the JRE {@code JPEGImageReader},
* with support for CMYK/YCCK JPEGs, non-standard color spaces,broken ICC profiles
* and more.
* that adds support and properly handles cases where the JRE version throws exceptions.
* <p/>
* Main features:
* <ul>
* <li>Support for CMYK JPEGs (converted to RGB by default or as CMYK, using the embedded ICC profile if applicable)</li>
* <li>Support for Adobe YCCK JPEGs (converted to RGB by default or as CMYK, using the embedded ICC profile if applicable)</li>
* <li>Support for JPEGs containing ICC profiles with interpretation other than 'Perceptual' (profile is assumed to be 'Perceptual' and used)</li>
* <li>Support for JPEGs containing ICC profiles with class other than 'Display' (profile is assumed to have class 'Display' and used)</li>
* <li>Support for JPEGs containing ICC profiles that are incompatible with stream data (image data is read, profile is ignored)</li>
* <li>Support for JPEGs with corrupted ICC profiles (image data is read, profile is ignored)</li>
* <li>Support for JPEGs with corrupted {@code ICC_PROFILE} segments (image data is read, profile is ignored)</li>
* <li>Support for JPEGs using non-standard color spaces, unsupported by Java 2D (image data is read, profile is ignored)</li>
* <li>Issues warnings instead of throwing exceptions in cases of corrupted data where ever the image data can still be read in a reasonable way</li>
* </ul>
* Thumbnail support:
* <ul>
* <li>Support for JFIF thumbnails (even if stream contains "inconsistent metadata")</li>
* <li>Support for JFXX thumbnails (JPEG, Indexed and RGB)</li>
* <li>Support for EXIF thumbnails (JPEG, RGB and YCbCr)</li>
* </ul>
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author LUT-based YCbCR conversion by Werner Randelshofer
@@ -76,7 +95,7 @@ public class JPEGImageReader extends ImageReaderBase {
private static final Map<Integer, List<String>> SEGMENT_IDENTIFIERS = createSegmentIds();
private static Map<Integer, List<String>> createSegmentIds() {
Map<Integer, List<String>> map = new HashMap<Integer, List<String>>();
Map<Integer, List<String>> map = new LinkedHashMap<Integer, List<String>>();
// JFIF/JFXX APP0 markers
map.put(JPEG.APP0, JPEGSegmentUtil.ALL_IDS);
@@ -181,7 +200,7 @@ public class JPEGImageReader extends ImageReaderBase {
typeList.add(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_INT_BGR));
// We also read and return CMYK if the source image is CMYK/YCCK + original color profile if present
ICC_Profile profile = getEmbeddedICCProfile();
ICC_Profile profile = getEmbeddedICCProfile(false);
if (csType == JPEGColorSpace.CMYK || csType == JPEGColorSpace.YCCK) {
if (profile != null) {
@@ -216,8 +235,7 @@ public class JPEGImageReader extends ImageReaderBase {
}
@Override
public
ImageTypeSpecifier getRawImageType(int imageIndex) throws IOException {
public ImageTypeSpecifier getRawImageType(int imageIndex) throws IOException {
// If delegate can determine the spec, we'll just go with that
ImageTypeSpecifier rawType = delegate.getRawImageType(imageIndex);
@@ -231,7 +249,7 @@ public class JPEGImageReader extends ImageReaderBase {
switch (csType) {
case CMYK:
// Create based on embedded profile if exists, or create from "Generic CMYK"
ICC_Profile profile = getEmbeddedICCProfile();
ICC_Profile profile = getEmbeddedICCProfile(false);
if (profile != null) {
return ImageTypeSpecifier.createInterleaved(ColorSpaces.createColorSpace(profile), new int[] {3, 2, 1, 0}, DataBuffer.TYPE_BYTE, false, false);
@@ -267,16 +285,17 @@ public class JPEGImageReader extends ImageReaderBase {
// Might want to look into the metadata, to see if there's a better way to identify these.
boolean unsupported = !delegate.getImageTypes(imageIndex).hasNext();
ICC_Profile profile = getEmbeddedICCProfile();
AdobeDCT adobeDCT = getAdobeDCT();
ICC_Profile profile = getEmbeddedICCProfile(false);
AdobeDCTSegment adobeDCT = getAdobeDCT();
// TODO: Probably something bogus here, as ICC profile isn't applied if reading through the delegate any more...
// We need to apply ICC profile unless the profile is sRGB/default gray (whatever that is)
// - or only filter out the bad ICC profiles in the JPEGSegmentImageInputStream.
if (delegate.canReadRaster() && (
unsupported ||
adobeDCT != null && adobeDCT.getTransform() == AdobeDCT.YCCK ||
profile != null && (ColorSpaces.isOffendingColorProfile(profile) || profile.getColorSpaceType() == ColorSpace.TYPE_CMYK))) {
adobeDCT != null && adobeDCT.getTransform() == AdobeDCTSegment.YCCK ||
profile != null && !ColorSpaces.isCS_sRGB(profile))) {
// profile != null && (ColorSpaces.isOffendingColorProfile(profile) || profile.getColorSpaceType() == ColorSpace.TYPE_CMYK))) {
if (DEBUG) {
System.out.println("Reading using raster and extra conversion");
System.out.println("ICC color profile: " + profile);
@@ -296,8 +315,8 @@ public class JPEGImageReader extends ImageReaderBase {
int origWidth = getWidth(imageIndex);
int origHeight = getHeight(imageIndex);
AdobeDCT adobeDCT = getAdobeDCT();
SOF startOfFrame = getSOF();
AdobeDCTSegment adobeDCT = getAdobeDCT();
SOFSegment startOfFrame = getSOF();
JPEGColorSpace csType = getSourceCSType(adobeDCT, startOfFrame);
Iterator<ImageTypeSpecifier> imageTypes = getImageTypes(imageIndex);
@@ -316,12 +335,12 @@ public class JPEGImageReader extends ImageReaderBase {
}
else if (intendedCS != null) {
// Handle inconsistencies
if (startOfFrame.componentsInFrame != intendedCS.getNumComponents()) {
if (startOfFrame.componentsInFrame < 4 && (csType == JPEGColorSpace.CMYK || csType == JPEGColorSpace.YCCK)) {
if (startOfFrame.componentsInFrame() != intendedCS.getNumComponents()) {
if (startOfFrame.componentsInFrame() < 4 && (csType == JPEGColorSpace.CMYK || csType == JPEGColorSpace.YCCK)) {
processWarningOccurred(String.format(
"Invalid Adobe App14 marker. Indicates YCCK/CMYK data, but SOF%d has %d color components. " +
"Ignoring Adobe App14 marker, assuming YCbCr/RGB data.",
startOfFrame.marker & 0xf, startOfFrame.componentsInFrame
startOfFrame.marker & 0xf, startOfFrame.componentsInFrame()
));
csType = JPEGColorSpace.YCbCr;
@@ -332,12 +351,15 @@ public class JPEGImageReader extends ImageReaderBase {
"Embedded ICC color profile is incompatible with image data. " +
"Profile indicates %d components, but SOF%d has %d color components. " +
"Ignoring ICC profile, assuming source color space %s.",
intendedCS.getNumComponents(), startOfFrame.marker & 0xf, startOfFrame.componentsInFrame, csType
intendedCS.getNumComponents(), startOfFrame.marker & 0xf, startOfFrame.componentsInFrame(), csType
));
}
}
// NOTE: Avoid using CCOp if same color space, as it's more compatible that way
else if (intendedCS != image.getColorModel().getColorSpace()) {
if (DEBUG) {
System.err.println("Converting from " + intendedCS + " to " + (image.getColorModel().getColorSpace().isCS_sRGB() ? "sRGB" : image.getColorModel().getColorSpace()));
}
convert = new ColorConvertOp(intendedCS, image.getColorModel().getColorSpace(), null);
}
// Else, pass through with no conversion
@@ -346,10 +368,20 @@ public class JPEGImageReader extends ImageReaderBase {
ColorSpace cmykCS = ColorSpaces.getColorSpace(ColorSpaces.CS_GENERIC_CMYK);
if (cmykCS instanceof ICC_ColorSpace) {
processWarningOccurred(
"No embedded ICC color profile, defaulting to \"generic\" CMYK ICC profile. " +
"Colors may look incorrect."
);
convert = new ColorConvertOp(cmykCS, image.getColorModel().getColorSpace(), null);
}
else {
// ColorConvertOp using non-ICC CS is deadly slow, fall back to fast conversion instead
processWarningOccurred(
"No embedded ICC color profile, will convert using inaccurate CMYK to RGB conversion. " +
"Colors may look incorrect."
);
convert = new FastCMYKToRGB();
}
}
@@ -436,7 +468,7 @@ public class JPEGImageReader extends ImageReaderBase {
return image;
}
static JPEGColorSpace getSourceCSType(AdobeDCT adobeDCT, final SOF startOfFrame) throws IIOException {
static JPEGColorSpace getSourceCSType(AdobeDCTSegment adobeDCT, final SOFSegment startOfFrame) throws IIOException {
/*
ADAPTED from http://download.oracle.com/javase/6/docs/api/javax/imageio/metadata/doc-files/jpeg_metadata.html:
@@ -478,11 +510,11 @@ public class JPEGImageReader extends ImageReaderBase {
if (adobeDCT != null) {
switch (adobeDCT.getTransform()) {
case AdobeDCT.YCC:
case AdobeDCTSegment.YCC:
return JPEGColorSpace.YCbCr;
case AdobeDCT.YCCK:
case AdobeDCTSegment.YCCK:
return JPEGColorSpace.YCCK;
case AdobeDCT.Unknown:
case AdobeDCTSegment.Unknown:
if (startOfFrame.components.length == 1) {
return JPEGColorSpace.Gray;
}
@@ -601,14 +633,24 @@ public class JPEGImageReader extends ImageReaderBase {
segments = JPEGSegmentUtil.readSegments(imageInput, SEGMENT_IDENTIFIERS);
}
catch (IOException ignore) {
catch (IIOException ignore) {
if (DEBUG) {
ignore.printStackTrace();
}
}
catch (IllegalArgumentException foo) {
foo.printStackTrace();
if (DEBUG) {
foo.printStackTrace();
}
}
finally {
imageInput.reset();
}
// In case of an exception, avoid NPE when referencing segments later
if (segments == null) {
segments = Collections.emptyList();
}
}
private List<JPEGSegment> getAppSegments(final int marker, final String identifier) throws IOException {
@@ -629,7 +671,7 @@ public class JPEGImageReader extends ImageReaderBase {
return appSegments;
}
private SOF getSOF() throws IOException {
private SOFSegment getSOF() throws IOException {
for (JPEGSegment segment : segments) {
if (JPEG.SOF0 >= segment.marker() && segment.marker() <= JPEG.SOF3 ||
JPEG.SOF5 >= segment.marker() && segment.marker() <= JPEG.SOF7 ||
@@ -654,7 +696,7 @@ public class JPEGImageReader extends ImageReaderBase {
components[i] = new SOFComponent(id, ((sub & 0xF0) >> 4), (sub & 0xF), qtSel);
}
return new SOF(segment.marker(), samplePrecision, lines, samplesPerLine, componentsInFrame, components);
return new SOFSegment(segment.marker(), samplePrecision, lines, samplesPerLine, components);
}
finally {
data.close();
@@ -665,7 +707,7 @@ public class JPEGImageReader extends ImageReaderBase {
return null;
}
private AdobeDCT getAdobeDCT() throws IOException {
private AdobeDCTSegment getAdobeDCT() throws IOException {
// TODO: Investigate http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6355567: 33/35 byte Adobe APP14 markers
List<JPEGSegment> adobe = getAppSegments(JPEG.APP14, "Adobe");
@@ -673,7 +715,7 @@ public class JPEGImageReader extends ImageReaderBase {
// version (byte), flags (4bytes), color transform (byte: 0=unknown, 1=YCC, 2=YCCK)
DataInputStream stream = new DataInputStream(adobe.get(0).data());
return new AdobeDCT(
return new AdobeDCTSegment(
stream.readUnsignedByte(),
stream.readUnsignedShort(),
stream.readUnsignedShort(),
@@ -717,10 +759,14 @@ public class JPEGImageReader extends ImageReaderBase {
return data;
}
private ICC_Profile getEmbeddedICCProfile() throws IOException {
private ICC_Profile getEmbeddedICCProfile(final boolean allowBadIndexes) throws IOException {
// ICC v 1.42 (2006) annex B:
// APP2 marker (0xFFE2) + 2 byte length + ASCII 'ICC_PROFILE' + 0 (termination)
// + 1 byte chunk number + 1 byte chunk count (allows ICC profiles chunked in multiple APP2 segments)
// TODO: Allow metadata to contain the wrongly indexed profiles, if readable
// NOTE: We ignore any profile with wrong index for reading and image types, just to be on the safe side
List<JPEGSegment> segments = getAppSegments(JPEG.APP2, "ICC_PROFILE");
if (segments.size() == 1) {
@@ -731,7 +777,8 @@ public class JPEGImageReader extends ImageReaderBase {
int chunkCount = stream.readUnsignedByte();
if (chunkNumber != 1 && chunkCount != 1) {
processWarningOccurred(String.format("Bad number of 'ICC_PROFILE' chunks: %d of %d. Assuming single chunk.", chunkNumber, chunkCount));
processWarningOccurred(String.format("Unexpected number of 'ICC_PROFILE' chunks: %d of %d. Ignoring ICC profile.", chunkNumber, chunkCount));
return null;
}
return readICCProfileSafe(stream);
@@ -742,19 +789,27 @@ public class JPEGImageReader extends ImageReaderBase {
int chunkNumber = stream.readUnsignedByte();
int chunkCount = stream.readUnsignedByte();
// TODO: Most of the time the ICC profiles are readable and should be obtainable from metadata...
boolean badICC = false;
if (chunkCount != segments.size()) {
// Some weird JPEGs use 0-based indexes... count == 0 and all numbers == 0.
// Others use count == 1, and all numbers == 1.
// Handle these by issuing warning
processWarningOccurred(String.format("Bad 'ICC_PROFILE' chunk count: %d. Ignoring ICC profile.", chunkCount));
badICC = true;
processWarningOccurred(String.format("Unexpected 'ICC_PROFILE' chunk count: %d. Ignoring count, assuming %d chunks in sequence.", chunkCount, segments.size()));
if (!allowBadIndexes) {
return null;
}
}
if (!badICC && chunkNumber < 1) {
// Anything else is just ignored
processWarningOccurred(String.format("Invalid 'ICC_PROFILE' chunk index: %d. Ignoring ICC profile.", chunkNumber));
return null;
if (!allowBadIndexes) {
return null;
}
}
int count = badICC ? segments.size() : chunkCount;
@@ -910,6 +965,51 @@ public class JPEGImageReader extends ImageReaderBase {
return thumbnails.get(thumbnailIndex).read();
}
// Metadata
@Override
public IIOMetadata getImageMetadata(int imageIndex) throws IOException {
// TODO: Nice try, but no cigar.. getAsTree does not return a "live" view, so any modifications are thrown away
IIOMetadata metadata = delegate.getImageMetadata(imageIndex);
// IIOMetadataNode tree = (IIOMetadataNode) metadata.getAsTree(metadata.getNativeMetadataFormatName());
// Node jpegVariety = tree.getElementsByTagName("JPEGvariety").item(0);
// TODO: Allow EXIF (as app1EXIF) in the JPEGvariety (sic) node.
// As EXIF is (a subset of) TIFF, (and the EXIF data is a valid TIFF stream) probably use something like:
// http://download.java.net/media/jai-imageio/javadoc/1.1/com/sun/media/imageio/plugins/tiff/package-summary.html#ImageMetadata
/*
from: http://docs.oracle.com/javase/6/docs/api/javax/imageio/metadata/doc-files/jpeg_metadata.html
In future versions of the JPEG metadata format, other varieties of JPEG metadata may be supported (e.g. Exif)
by defining other types of nodes which may appear as a child of the JPEGvariety node.
(Note that an application wishing to interpret Exif metadata given a metadata tree structure in the
javax_imageio_jpeg_image_1.0 format must check for an unknown marker segment with a tag indicating an
APP1 marker and containing data identifying it as an Exif marker segment. Then it may use application-specific
code to interpret the data in the marker segment. If such an application were to encounter a metadata tree
formatted according to a future version of the JPEG metadata format, the Exif marker segment might not be
unknown in that format - it might be structured as a child node of the JPEGvariety node.
Thus, it is important for an application to specify which version to use by passing the string identifying
the version to the method/constructor used to obtain an IIOMetadata object.)
*/
// IIOMetadataNode app2ICC = new IIOMetadataNode("app2ICC");
// app2ICC.setUserObject(getEmbeddedICCProfile());
// jpegVariety.getFirstChild().appendChild(app2ICC);
// new XMLSerializer(System.err, System.getProperty("file.encoding")).serialize(tree, false);
return metadata;
}
@Override
public IIOMetadata getStreamMetadata() throws IOException {
return delegate.getStreamMetadata();
}
private static void invertCMYK(final Raster raster) {
byte[] data = ((DataBufferByte) raster.getDataBuffer()).getData();
@@ -1135,73 +1235,6 @@ public class JPEGImageReader extends ImageReaderBase {
}
}
private static class SOF {
private final int marker;
private final int samplePrecision;
private final int lines; // height
private final int samplesPerLine; // width
private final int componentsInFrame;
final SOFComponent[] components;
public SOF(int marker, int samplePrecision, int lines, int samplesPerLine, int componentsInFrame, SOFComponent[] components) {
this.marker = marker;
this.samplePrecision = samplePrecision;
this.lines = lines;
this.samplesPerLine = samplesPerLine;
this.componentsInFrame = componentsInFrame;
this.components = components;
}
public int getMarker() {
return marker;
}
public int getSamplePrecision() {
return samplePrecision;
}
public int getLines() {
return lines;
}
public int getSamplesPerLine() {
return samplesPerLine;
}
public int getComponentsInFrame() {
return componentsInFrame;
}
@Override
public String toString() {
return String.format(
"SOF%d[%04x, precision: %d, lines: %d, samples/line: %d, components: %s]",
marker & 0xf, marker, samplePrecision, lines, samplesPerLine, Arrays.toString(components)
);
}
}
private static class SOFComponent {
final int id;
final int hSub;
final int vSub;
final int qtSel;
public SOFComponent(int id, int hSub, int vSub, int qtSel) {
this.id = id;
this.hSub = hSub;
this.vSub = vSub;
this.qtSel = qtSel;
}
@Override
public String toString() {
// Use id either as component number or component name, based on value
Serializable idStr = (id >= 'a' && id <= 'z' || id >= 'A' && id <= 'Z') ? "'" + (char) id + "'" : id;
return String.format("id: %s, sub: %d/%d, sel: %d", idStr, hSub, vSub, qtSel);
}
}
protected static void showIt(final BufferedImage pImage, final String pTitle) {
ImageReaderBase.showIt(pImage, pTitle);
}
@@ -1272,7 +1305,7 @@ public class JPEGImageReader extends ImageReaderBase {
// param.setSourceSubsampling(sub, sub, 0, 0);
// }
long start = System.currentTimeMillis();
// long start = System.currentTimeMillis();
BufferedImage image = reader.read(0, param);
// System.err.println("Read time: " + (System.currentTimeMillis() - start) + " ms");
// System.err.println("image: " + image);
@@ -1280,12 +1313,12 @@ public class JPEGImageReader extends ImageReaderBase {
// image = new ResampleOp(reader.getWidth(0) / 4, reader.getHeight(0) / 4, ResampleOp.FILTER_LANCZOS).filter(image, null);
// int maxW = 1280;
// int maxH = 800;
int maxW = 400;
int maxH = 400;
int maxW = 1280;
int maxH = 800;
// int maxW = 400;
// int maxH = 400;
if (image.getWidth() > maxW || image.getHeight() > maxH) {
start = System.currentTimeMillis();
// start = System.currentTimeMillis();
float aspect = reader.getAspectRatio(0);
if (aspect >= 1f) {
image = ImageUtil.createResampled(image, maxW, Math.round(maxW / aspect), Image.SCALE_DEFAULT);

View File

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

View File

@@ -0,0 +1,59 @@
/*
* Copyright (c) 2013, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name "TwelveMonkeys" nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.imageio.plugins.jpeg;
import java.io.Serializable;
/**
* SOFComponent
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: SOFComponent.java,v 1.0 22.04.13 16:40 haraldk Exp$
*/
final class SOFComponent {
final int id;
final int hSub;
final int vSub;
final int qtSel;
SOFComponent(int id, int hSub, int vSub, int qtSel) {
this.id = id;
this.hSub = hSub;
this.vSub = vSub;
this.qtSel = qtSel;
}
@Override
public String toString() {
// Use id either as component number or component name, based on value
Serializable idStr = (id >= 'a' && id <= 'z' || id >= 'A' && id <= 'Z') ? "'" + (char) id + "'" : id;
return String.format("id: %s, sub: %d/%d, sel: %d", idStr, hSub, vSub, qtSel);
}
}

View File

@@ -0,0 +1,66 @@
/*
* Copyright (c) 2013, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name "TwelveMonkeys" nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.imageio.plugins.jpeg;
import java.util.Arrays;
/**
* SOFSegment
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: SOFSegment.java,v 1.0 22.04.13 16:40 haraldk Exp$
*/
final class SOFSegment {
final int marker;
final int samplePrecision;
final int lines; // height
final int samplesPerLine; // width
final SOFComponent[] components;
SOFSegment(int marker, int samplePrecision, int lines, int samplesPerLine, SOFComponent[] components) {
this.marker = marker;
this.samplePrecision = samplePrecision;
this.lines = lines;
this.samplesPerLine = samplesPerLine;
this.components = components;
}
final int componentsInFrame() {
return components.length;
}
@Override
public String toString() {
return String.format(
"SOF%d[%04x, precision: %d, lines: %d, samples/line: %d, components: %s]",
marker & 0xff - 0xc0, marker, samplePrecision, lines, samplesPerLine, Arrays.toString(components)
);
}
}

View File

@@ -31,10 +31,12 @@ package com.twelvemonkeys.imageio.plugins.jpeg;
import com.twelvemonkeys.imageio.util.ImageReaderAbstractTestCase;
import org.junit.Test;
import javax.imageio.IIOException;
import javax.imageio.ImageIO;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.event.IIOReadWarningListener;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.spi.IIORegistry;
import javax.imageio.spi.ImageReaderSpi;
import java.awt.*;
@@ -75,7 +77,8 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTestCase<JPEGImageRe
new TestData(getClassLoaderResource("/jpeg/gray-sample.jpg"), new Dimension(386, 396)),
new TestData(getClassLoaderResource("/jpeg/cmyk-sample.jpg"), new Dimension(160, 227)),
new TestData(getClassLoaderResource("/jpeg/cmyk-sample-multiple-chunk-icc.jpg"), new Dimension(2707, 3804)),
new TestData(getClassLoaderResource("/jpeg/jfif-jfxx-thumbnail-olympus-d320l.jpg"), new Dimension(640, 480))
new TestData(getClassLoaderResource("/jpeg/jfif-jfxx-thumbnail-olympus-d320l.jpg"), new Dimension(640, 480)),
new TestData(getClassLoaderResource("/jpeg/jfif-padded-segments.jpg"), new Dimension(20, 45))
);
// More test data in specific tests below
@@ -255,7 +258,7 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTestCase<JPEGImageRe
assertEquals(16, image.getHeight());
// TODO: Need to test colors!
assertTrue(reader.hasThumbnails(0)); // Should not blow up!
}
@@ -371,7 +374,7 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTestCase<JPEGImageRe
// JFIF with JFXX JPEG encoded thumbnail
JPEGImageReader reader = createReader();
reader.setInput(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/jfif-jfxx-thumbnail-olympus-d320l.jpg")));
assertTrue(reader.hasThumbnails(0));
assertEquals(1, reader.getNumThumbnails(0));
assertEquals(80, reader.getThumbnailWidth(0, 0));
@@ -457,8 +460,8 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTestCase<JPEGImageRe
for (int i = 0; i < strip.getWidth() / 128; i++) {
int actualRGB = strip.getRGB(i * 128, 4);
assertEquals((actualRGB >> 16) & 0xff, (expectedRGB[i] >> 16) & 0xff, 5);
assertEquals((actualRGB >> 8) & 0xff, (expectedRGB[i] >> 8) & 0xff, 5);
assertEquals((actualRGB) & 0xff, (expectedRGB[i]) & 0xff, 5);
assertEquals((actualRGB >> 8) & 0xff, (expectedRGB[i] >> 8) & 0xff, 5);
assertEquals((actualRGB) & 0xff, (expectedRGB[i]) & 0xff, 5);
}
}
@@ -487,8 +490,8 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTestCase<JPEGImageRe
for (int i = 0; i < thumbnail.getWidth() / 8; i++) {
int actualRGB = thumbnail.getRGB(i * 8, 4);
assertEquals((actualRGB >> 16) & 0xff, (expectedRGB[i] >> 16) & 0xff, 5);
assertEquals((actualRGB >> 8) & 0xff, (expectedRGB[i] >> 8) & 0xff, 5);
assertEquals((actualRGB) & 0xff, (expectedRGB[i]) & 0xff, 5);
assertEquals((actualRGB >> 8) & 0xff, (expectedRGB[i] >> 8) & 0xff, 5);
assertEquals((actualRGB) & 0xff, (expectedRGB[i]) & 0xff, 5);
}
}
@@ -549,7 +552,6 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTestCase<JPEGImageRe
new TestData(getClassLoaderResource("/jpeg/cmyk-sample-no-icc.jpg"), new Dimension(100, 100))
);
for (TestData data : cmykData) {
reader.setInput(data.getInputStream());
@@ -599,4 +601,24 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTestCase<JPEGImageRe
}
// TODO: Test RGBA/YCbCrA handling
@Test
public void testReadMetadataMaybeNull() throws IOException {
// Just test that we can read the metadata without exceptions
JPEGImageReader reader = createReader();
for (TestData testData : getTestData()) {
reader.setInput(testData.getInputStream());
for (int i = 0; i < reader.getNumImages(true); i++) {
try {
IIOMetadata metadata = reader.getImageMetadata(i);
assertNotNull(String.format("Image metadata null for %s image %s", testData, i), metadata);
}
catch (IIOException e) {
System.err.println(String.format("WARNING: Reading metadata failed for %s image %s: %s", testData, i, e.getMessage()));
}
}
}
}
}

View File

@@ -135,4 +135,27 @@ public class JPEGSegmentImageInputStreamTest {
assertEquals(9299l, length); // Sanity check: same as file size
}
@Test
public void testReadPaddedSegmentsBug() throws IOException {
ImageInputStream stream = new JPEGSegmentImageInputStream(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/jfif-padded-segments.jpg")));
List<JPEGSegment> appSegments = JPEGSegmentUtil.readSegments(stream, JPEGSegmentUtil.APP_SEGMENTS);
assertEquals(2, appSegments.size());
assertEquals(JPEG.APP0, appSegments.get(0).marker());
assertEquals("JFIF", appSegments.get(0).identifier());
assertEquals(JPEG.APP1, appSegments.get(1).marker());
assertEquals("Exif", appSegments.get(1).identifier());
stream.seek(0l);
long length = 0;
while (stream.read() != -1) {
length++;
}
assertEquals(1079L, length); // Sanity check: same as file size, except padding and the filtered ICC_PROFILE segment
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -0,0 +1,25 @@
Copyright (c) 2012, Harald Kuhr
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name "TwelveMonkeys" nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -95,7 +95,8 @@ public final class JPEGSegmentUtil {
JPEGSegment segment;
try {
while (!isImageDone(segment = readSegment(stream, segmentIdentifiers))) {
do {
segment = readSegment(stream, segmentIdentifiers);
// System.err.println("segment: " + segment);
if (isRequested(segment, segmentIdentifiers)) {
@@ -106,6 +107,7 @@ public final class JPEGSegmentUtil {
segments.add(segment);
}
}
while (!isImageDone(segment));
}
catch (EOFException ignore) {
// Just end here, in case of malformed stream
@@ -151,8 +153,18 @@ public final class JPEGSegmentUtil {
}
}
static JPEGSegment readSegment(final ImageInputStream stream, Map<Integer, List<String>> segmentIdentifiers) throws IOException {
static JPEGSegment readSegment(final ImageInputStream stream, final Map<Integer, List<String>> segmentIdentifiers) throws IOException {
int marker = stream.readUnsignedShort();
// Skip over 0xff padding between markers
while (marker == 0xffff) {
marker = 0xff00 | stream.readUnsignedByte();
}
if ((marker >> 8 & 0xff) != 0xff) {
throw new IIOException(String.format("Bad marker: %04x", marker));
}
int length = stream.readUnsignedShort(); // Length including length field itself
byte[] data;
@@ -191,7 +203,7 @@ public final class JPEGSegmentUtil {
}
@Override
public boolean contains(Object o) {
public boolean contains(final Object o) {
return true;
}
}
@@ -203,13 +215,13 @@ public final class JPEGSegmentUtil {
}
@Override
public List<String> get(Object key) {
public List<String> get(final Object key) {
return key instanceof Integer && JPEGSegment.isAppSegmentMarker((Integer) key) ? ALL_IDS : null;
}
@Override
public boolean containsKey(Object key) {
public boolean containsKey(final Object key) {
return true;
}
}
@@ -221,7 +233,7 @@ public final class JPEGSegmentUtil {
}
@Override
public List<String> get(Object key) {
public List<String> get(final Object key) {
return containsKey(key) ? ALL_IDS : null;
}

View File

@@ -151,19 +151,21 @@ public class JPEGSegmentUtilTest {
@Test
public void testReadAll() throws IOException {
List<JPEGSegment> segments = JPEGSegmentUtil.readSegments(getData("/jpeg/9788245605525.jpg"), JPEGSegmentUtil.ALL_SEGMENTS);
assertEquals(6, segments.size());
assertEquals(7, segments.size());
assertEquals(segments.toString(), JPEG.SOF0, segments.get(3).marker());
assertEquals(segments.toString(), null, segments.get(3).identifier());
assertEquals(segments.toString(), JPEG.SOS, segments.get(segments.size() - 1).marker());
}
@Test
public void testReadAllAlt() throws IOException {
List<JPEGSegment> segments = JPEGSegmentUtil.readSegments(getData("/jpeg/ts_open_300dpi.jpg"), JPEGSegmentUtil.ALL_SEGMENTS);
assertEquals(26, segments.size());
assertEquals(27, segments.size());
assertEquals(segments.toString(), JPEG.SOF0, segments.get(23).marker());
assertEquals(segments.toString(), null, segments.get(23).identifier());
assertEquals(segments.toString(), JPEG.SOS, segments.get(segments.size() - 1).marker());
}
@Test
@@ -194,4 +196,17 @@ public class JPEGSegmentUtilTest {
assertEquals(JPEG.APP14, segments.get(21).marker());
assertEquals("Adobe", segments.get(21).identifier());
}
@Test
public void testReadPaddedSegments() throws IOException {
List<JPEGSegment> segments = JPEGSegmentUtil.readSegments(getData("/jpeg/jfif-padded-segments.jpg"), JPEGSegmentUtil.APP_SEGMENTS);
assertEquals(3, segments.size());
assertEquals(JPEG.APP0, segments.get(0).marker());
assertEquals("JFIF", segments.get(0).identifier());
assertEquals(JPEG.APP2, segments.get(1).marker());
assertEquals("ICC_PROFILE", segments.get(1).identifier());
assertEquals(JPEG.APP1, segments.get(2).marker());
assertEquals("Exif", segments.get(2).identifier());
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -0,0 +1,25 @@
Copyright (c) 2013, Harald Kuhr
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name "TwelveMonkeys" nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -0,0 +1,452 @@
/*
* Copyright (c) 2012, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name "TwelveMonkeys" nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.imageio.plugins.tiff;
import com.twelvemonkeys.lang.Validate;
import java.io.EOFException;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* CCITT Modified Huffman RLE, Group 3 (T4) and Group 4 (T6) fax compression.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: CCITTFaxDecoderStream.java,v 1.0 23.05.12 15:55 haraldk Exp$
*/
final class CCITTFaxDecoderStream extends FilterInputStream {
// See TIFF 6.0 Specification, Section 10: "Modified Huffman Compression", page 43.
private final int columns;
private final byte[] decodedRow;
private int decodedLength;
private int decodedPos;
private int bitBuffer;
private int bitBufferLength;
// Need to take fill order into account (?) (use flip table?)
private final int fillOrder;
private final int type;
private final int[] changes;
private int changesCount;
private static final int EOL_CODE = 0x01; // 12 bit
public CCITTFaxDecoderStream(final InputStream stream, final int columns, final int type, final int fillOrder) {
super(Validate.notNull(stream, "stream"));
this.columns = Validate.isTrue(columns > 0, columns, "width must be greater than 0");
// We know this is only used for b/w (1 bit)
this.decodedRow = new byte[(columns + 7) / 8];
this.type = Validate.isTrue(type == TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE, type, "Only CCITT Modified Huffman RLE compression (2) supported: %s"); // TODO: Implement group 3 and 4
this.fillOrder = Validate.isTrue(fillOrder == 1, fillOrder, "Only fill order 1 supported: %s"); // TODO: Implement fillOrder == 2
this.changes = new int[columns];
}
// IDEA: Would it be faster to keep all bit combos of each length (>=2) that is NOT a code, to find bit length, then look up value in table?
// -- If white run, start at 4 bits to determine length, if black, start at 2 bits
private void fetch() throws IOException {
if (decodedPos >= decodedLength) {
decodedLength = 0;
try {
decodeRow();
}
catch (EOFException e) {
// TODO: Rewrite to avoid throw/catch for normal flow...
if (decodedLength != 0) {
throw e;
}
// ..otherwise, just client code trying to read past the end of stream
decodedLength = -1;
}
decodedPos = 0;
}
}
private void decodeRow() throws IOException {
resetBuffer();
boolean literalRun = true;
/*
if (type == TIFFExtension.COMPRESSION_CCITT_T4) {
int eol = readBits(12);
System.err.println("eol: " + eol);
while (eol != EOL_CODE) {
eol = readBits(1);
System.err.println("eol: " + eol);
// throw new IOException("Missing EOL");
}
literalRun = readBits(1) == 1;
}
System.err.println("literalRun: " + literalRun);
*/
int index = 0;
if (literalRun) {
changesCount = 0;
boolean white = true;
do {
int completeRun = 0;
int run;
do {
if (white) {
run = decodeRun(WHITE_CODES, WHITE_RUN_LENGTHS, 4);
}
else {
run = decodeRun(BLACK_CODES, BLACK_RUN_LENGTHS, 2);
}
completeRun += run;
}
while (run >= 64); // Additional makeup codes are packed into both b/w codes, terminating codes are < 64 bytes
changes[changesCount++] = index + completeRun;
// System.err.printf("%s run: %d\n", white ? "white" : "black", run);
// TODO: Optimize with lookup for 0-7 bits?
// Fill bits to byte boundary...
while (index % 8 != 0 && completeRun-- > 0) {
decodedRow[index++ / 8] |= (white ? 1 << 8 - (index % 8) : 0);
}
// ...then fill complete bytes to either 0xff or 0x00...
if (index % 8 == 0) {
final byte value = (byte) (white ? 0xff : 0x00);
while (completeRun > 7) {
decodedRow[index / 8] = value;
completeRun -= 8;
index += 8;
}
}
// ...finally fill any remaining bits
while (completeRun-- > 0) {
decodedRow[index++ / 8] |= (white ? 1 << 8 - (index % 8) : 0);
}
// Flip color for next run
white = !white;
}
while (index < columns);
}
else {
// non-literal run
}
if (type == TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE && index != columns) {
throw new IOException("Sum of run-lengths does not equal scan line width: " + index + " > " + columns);
}
decodedLength = (index / 8) + 1;
}
private int decodeRun(short[][] codes, short[][] runLengths, int minCodeSize) throws IOException {
// TODO: Optimize...
// Looping and comparing is the most straight-forward, but probably not the most effective way...
int code = readBits(minCodeSize);
for (int bits = 0; bits < codes.length; bits++) {
short[] bitCodes = codes[bits];
for (int i = 0; i < bitCodes.length; i++) {
if (bitCodes[i] == code) {
// System.err.println("code: " + code);
// Code found, return matching run length
return runLengths[bits][i];
}
}
// No code found, read one more bit and try again
code = fillOrder == 1 ? (code << 1) | readBits(1) : readBits(1) << (bits + minCodeSize) | code;
}
throw new IOException("Unknown code in Huffman RLE stream");
}
private void resetBuffer() {
for (int i = 0; i < decodedRow.length; i++) {
decodedRow[i] = 0;
}
bitBuffer = 0;
bitBufferLength = 0;
}
private int readBits(int bitCount) throws IOException {
while (bitBufferLength < bitCount) {
int read = in.read();
if (read == -1) {
throw new EOFException("Unexpected end of Huffman RLE stream");
}
int bits = read & 0xff;
bitBuffer = (bitBuffer << 8) | bits;
bitBufferLength += 8;
}
// TODO: Take fill order into account
bitBufferLength -= bitCount;
int result = bitBuffer >> bitBufferLength;
bitBuffer &= (1 << bitBufferLength) - 1;
return result;
}
@Override
public int read() throws IOException {
if (decodedLength < 0) {
return -1;
}
if (decodedPos >= decodedLength) {
fetch();
if (decodedLength < 0) {
return -1;
}
}
return decodedRow[decodedPos++] & 0xff;
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
if (decodedLength < 0) {
return -1;
}
if (decodedPos >= decodedLength) {
fetch();
if (decodedLength < 0) {
return -1;
}
}
int read = Math.min(decodedLength - decodedPos, len);
System.arraycopy(decodedRow, decodedPos, b, off, read);
decodedPos += read;
return read;
}
@Override
public long skip(long n) throws IOException {
if (decodedLength < 0) {
return -1;
}
if (decodedPos >= decodedLength) {
fetch();
if (decodedLength < 0) {
return -1;
}
}
int skipped = (int) Math.min(decodedLength - decodedPos, n);
decodedPos += skipped;
return skipped;
}
@Override
public boolean markSupported() {
return false;
}
@Override
public synchronized void reset() throws IOException {
throw new IOException("mark/reset not supported");
}
static final short[][] BLACK_CODES = {
{ // 2 bits
0x2, 0x3,
},
{ // 3 bits
0x2, 0x3,
},
{ // 4 bits
0x2, 0x3,
},
{ // 5 bits
0x3,
},
{ // 6 bits
0x4, 0x5,
},
{ // 7 bits
0x4, 0x5, 0x7,
},
{ // 8 bits
0x4, 0x7,
},
{ // 9 bits
0x18,
},
{ // 10 bits
0x17, 0x18, 0x37, 0x8, 0xf,
},
{ // 11 bits
0x17, 0x18, 0x28, 0x37, 0x67, 0x68, 0x6c, 0x8, 0xc, 0xd,
},
{ // 12 bits
0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x1c, 0x1d, 0x1e, 0x1f, 0x24, 0x27, 0x28, 0x2b, 0x2c, 0x33,
0x34, 0x35, 0x37, 0x38, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x5b, 0x64, 0x65,
0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0xc8, 0xc9, 0xca, 0xcb, 0xcc, 0xcd, 0xd2, 0xd3,
0xd4, 0xd5, 0xd6, 0xd7, 0xda, 0xdb,
},
{ // 13 bits
0x4a, 0x4b, 0x4c, 0x4d, 0x52, 0x53, 0x54, 0x55, 0x5a, 0x5b, 0x64, 0x65, 0x6c, 0x6d, 0x72, 0x73,
0x74, 0x75, 0x76, 0x77,
}
};
static final short[][] BLACK_RUN_LENGTHS = {
{ // 2 bits
3, 2,
},
{ // 3 bits
1, 4,
},
{ // 4 bits
6, 5,
},
{ // 5 bits
7,
},
{ // 6 bits
9, 8,
},
{ // 7 bits
10, 11, 12,
},
{ // 8 bits
13, 14,
},
{ // 9 bits
15,
},
{ // 10 bits
16, 17, 0, 18, 64,
},
{ // 11 bits
24, 25, 23, 22, 19, 20, 21, 1792, 1856, 1920,
},
{ // 12 bits
1984, 2048, 2112, 2176, 2240, 2304, 2368, 2432, 2496, 2560, 52, 55, 56, 59, 60, 320,
384, 448, 53, 54, 50, 51, 44, 45, 46, 47, 57, 58, 61, 256, 48, 49,
62, 63, 30, 31, 32, 33, 40, 41, 128, 192, 26, 27, 28, 29, 34, 35,
36, 37, 38, 39, 42, 43,
},
{ // 13 bits
640, 704, 768, 832, 1280, 1344, 1408, 1472, 1536, 1600, 1664, 1728, 512, 576, 896, 960,
1024, 1088, 1152, 1216,
}
};
public static final short[][] WHITE_CODES = {
{ // 4 bits
0x7, 0x8, 0xb, 0xc, 0xe, 0xf,
},
{ // 5 bits
0x12, 0x13, 0x14, 0x1b, 0x7, 0x8,
},
{ // 6 bits
0x17, 0x18, 0x2a, 0x2b, 0x3, 0x34, 0x35, 0x7, 0x8,
},
{ // 7 bits
0x13, 0x17, 0x18, 0x24, 0x27, 0x28, 0x2b, 0x3, 0x37, 0x4, 0x8, 0xc,
},
{ // 8 bits
0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x1a, 0x1b, 0x2, 0x24, 0x25, 0x28, 0x29, 0x2a, 0x2b, 0x2c,
0x2d, 0x3, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x4, 0x4a, 0x4b, 0x5, 0x52, 0x53, 0x54, 0x55,
0x58, 0x59, 0x5a, 0x5b, 0x64, 0x65, 0x67, 0x68, 0xa, 0xb,
},
{ // 9 bits
0x98, 0x99, 0x9a, 0x9b, 0xcc, 0xcd, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xdb,
},
{ // 10 bits
},
{ // 11 bits
0x8, 0xc, 0xd,
},
{ // 12 bits
0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x1c, 0x1d, 0x1e, 0x1f,
}
};
public static final short[][] WHITE_RUN_LENGTHS = {
{ // 4 bits
2, 3, 4, 5, 6, 7,
},
{ // 5 bits
128, 8, 9, 64, 10, 11,
},
{ // 6 bits
192, 1664, 16, 17, 13, 14, 15, 1, 12,
},
{ // 7 bits
26, 21, 28, 27, 18, 24, 25, 22, 256, 23, 20, 19,
},
{ // 8 bits
33, 34, 35, 36, 37, 38, 31, 32, 29, 53, 54, 39, 40, 41, 42, 43,
44, 30, 61, 62, 63, 0, 320, 384, 45, 59, 60, 46, 49, 50, 51,
52, 55, 56, 57, 58, 448, 512, 640, 576, 47, 48,
},
{ // 9 bits
1472, 1536, 1600, 1728, 704, 768, 832, 896, 960, 1024, 1088, 1152, 1216, 1280, 1344, 1408,
},
{ // 10 bits
},
{ // 11 bits
1792, 1856, 1920,
},
{ // 12 bits
1984, 2048, 2112, 2176, 2240, 2304, 2368, 2432, 2496, 2560,
}
};
}

View File

@@ -0,0 +1,340 @@
/*
* Copyright (c) 2013, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name "TwelveMonkeys" nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.imageio.plugins.tiff;
import com.twelvemonkeys.lang.Validate;
import java.io.EOFException;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteOrder;
/**
* A decoder for data converted using "horizontal differencing predictor".
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: HorizontalDeDifferencingStream.java,v 1.0 11.03.13 14:20 haraldk Exp$
*/
final class HorizontalDeDifferencingStream extends FilterInputStream {
// See TIFF 6.0 Specification, Section 14: "Differencing Predictor", page 64.
private final int columns;
// NOTE: PlanarConfiguration == 2 may be treated as samplesPerPixel == 1
private final int samplesPerPixel;
private final int bitsPerSample;
private final ByteOrder byteOrder;
int decodedLength;
int decodedPos;
private final byte[] buffer;
public HorizontalDeDifferencingStream(final InputStream stream, final int columns, final int samplesPerPixel, final int bitsPerSample, final ByteOrder byteOrder) {
super(Validate.notNull(stream, "stream"));
this.columns = Validate.isTrue(columns > 0, columns, "width must be greater than 0");
this.samplesPerPixel = Validate.isTrue(bitsPerSample >= 8 || samplesPerPixel == 1, samplesPerPixel, "Unsupported samples per pixel for < 8 bit samples: %s");
this.bitsPerSample = Validate.isTrue(isValidBPS(bitsPerSample), bitsPerSample, "Unsupported bits per sample value: %s");
this.byteOrder = byteOrder;
buffer = new byte[(columns * samplesPerPixel * bitsPerSample + 7) / 8];
}
private boolean isValidBPS(final int bitsPerSample) {
switch (bitsPerSample) {
case 1:
case 2:
case 4:
case 8:
case 16:
case 32:
case 64:
return true;
default:
return false;
}
}
private void fetch() throws IOException {
int pos = 0;
int read;
// This *SHOULD* read an entire row of pixels (or nothing at all) into the buffer, otherwise we will throw EOFException below
while (pos < buffer.length && (read = in.read(buffer, pos, buffer.length - pos)) > 0) {
pos += read;
}
if (pos > 0) {
if (buffer.length > pos) {
throw new EOFException("Unexpected end of stream");
}
decodeRow();
decodedLength = buffer.length;
decodedPos = 0;
}
else {
decodedLength = -1;
}
}
private void decodeRow() throws EOFException {
// Un-apply horizontal predictor
int sample = 0;
byte temp;
switch (bitsPerSample) {
case 1:
for (int b = 0; b < (columns + 7) / 8; b++) {
sample += (buffer[b] >> 7) & 0x1;
temp = (byte) ((sample << 7) & 0x80);
sample += (buffer[b] >> 6) & 0x1;
temp |= (byte) ((sample << 6) & 0x40);
sample += (buffer[b] >> 5) & 0x1;
temp |= (byte) ((sample << 5) & 0x20);
sample += (buffer[b] >> 4) & 0x1;
temp |= (byte) ((sample << 4) & 0x10);
sample += (buffer[b] >> 3) & 0x1;
temp |= (byte) ((sample << 3) & 0x08);
sample += (buffer[b] >> 2) & 0x1;
temp |= (byte) ((sample << 2) & 0x04);
sample += (buffer[b] >> 1) & 0x1;
temp |= (byte) ((sample << 1) & 0x02);
sample += buffer[b] & 0x1;
buffer[b] = (byte) (temp | sample & 0x1);
}
break;
case 2:
for (int b = 0; b < (columns + 3) / 4; b++) {
sample += (buffer[b] >> 6) & 0x3;
temp = (byte) ((sample << 6) & 0xc0);
sample += (buffer[b] >> 4) & 0x3;
temp |= (byte) ((sample << 4) & 0x30);
sample += (buffer[b] >> 2) & 0x3;
temp |= (byte) ((sample << 2) & 0x0c);
sample += buffer[b] & 0x3;
buffer[b] = (byte) (temp | sample & 0x3);
}
break;
case 4:
for (int b = 0; b < (columns + 1) / 2; b++) {
sample += (buffer[b] >> 4) & 0xf;
temp = (byte) ((sample << 4) & 0xf0);
sample += buffer[b] & 0x0f;
buffer[b] = (byte) (temp | sample & 0xf);
}
break;
case 8:
for (int x = 1; x < columns; x++) {
for (int b = 0; b < samplesPerPixel; b++) {
int off = x * samplesPerPixel + b;
buffer[off] = (byte) (buffer[off - samplesPerPixel] + buffer[off]);
}
}
break;
case 16:
for (int x = 1; x < columns; x++) {
for (int b = 0; b < samplesPerPixel; b++) {
int off = x * samplesPerPixel + b;
putShort(off, asShort(off - samplesPerPixel) + asShort(off));
}
}
break;
case 32:
for (int x = 1; x < columns; x++) {
for (int b = 0; b < samplesPerPixel; b++) {
int off = x * samplesPerPixel + b;
putInt(off, asInt(off - samplesPerPixel) + asInt(off));
}
}
break;
case 64:
for (int x = 1; x < columns; x++) {
for (int b = 0; b < samplesPerPixel; b++) {
int off = x * samplesPerPixel + b;
putLong(off, asLong(off - samplesPerPixel) + asLong(off));
}
}
break;
default:
throw new AssertionError(String.format("Unsupported bits per sample value: %d", bitsPerSample));
}
}
private void putLong(final int index, final long value) {
if (byteOrder == ByteOrder.BIG_ENDIAN) {
buffer[index * 8 ] = (byte) ((value >> 56) & 0xff);
buffer[index * 8 + 1] = (byte) ((value >> 48) & 0xff);
buffer[index * 8 + 2] = (byte) ((value >> 40) & 0xff);
buffer[index * 8 + 3] = (byte) ((value >> 32) & 0xff);
buffer[index * 8 + 4] = (byte) ((value >> 24) & 0xff);
buffer[index * 8 + 5] = (byte) ((value >> 16) & 0xff);
buffer[index * 8 + 6] = (byte) ((value >> 8) & 0xff);
buffer[index * 8 + 7] = (byte) ((value) & 0xff);
}
else {
buffer[index * 8 + 7] = (byte) ((value >> 56) & 0xff);
buffer[index * 8 + 6] = (byte) ((value >> 48) & 0xff);
buffer[index * 8 + 5] = (byte) ((value >> 40) & 0xff);
buffer[index * 8 + 4] = (byte) ((value >> 32) & 0xff);
buffer[index * 8 + 3] = (byte) ((value >> 24) & 0xff);
buffer[index * 8 + 2] = (byte) ((value >> 16) & 0xff);
buffer[index * 8 + 1] = (byte) ((value >> 8) & 0xff);
buffer[index * 8 ] = (byte) ((value) & 0xff);
}
}
private long asLong(final int index) {
if (byteOrder == ByteOrder.BIG_ENDIAN) {
return (buffer[index * 8 ] & 0xffl) << 56l | (buffer[index * 8 + 1] & 0xffl) << 48l |
(buffer[index * 8 + 2] & 0xffl) << 40l | (buffer[index * 8 + 3] & 0xffl) << 32l |
(buffer[index * 8 + 4] & 0xffl) << 24 | (buffer[index * 8 + 5] & 0xffl) << 16 |
(buffer[index * 8 + 6] & 0xffl) << 8 | buffer[index * 8 + 7] & 0xffl;
}
else {
return (buffer[index * 8 + 7] & 0xffl) << 56l | (buffer[index * 8 + 6] & 0xffl) << 48l |
(buffer[index * 8 + 5] & 0xffl) << 40l | (buffer[index * 8 + 4] & 0xffl) << 32l |
(buffer[index * 8 + 3] & 0xffl) << 24 | (buffer[index * 8 + 2] & 0xffl) << 16 |
(buffer[index * 8 + 1] & 0xffl) << 8 | buffer[index * 8] & 0xffl;
}
}
private void putInt(final int index, final int value) {
if (byteOrder == ByteOrder.BIG_ENDIAN) {
buffer[index * 4 ] = (byte) ((value >> 24) & 0xff);
buffer[index * 4 + 1] = (byte) ((value >> 16) & 0xff);
buffer[index * 4 + 2] = (byte) ((value >> 8) & 0xff);
buffer[index * 4 + 3] = (byte) ((value) & 0xff);
}
else {
buffer[index * 4 + 3] = (byte) ((value >> 24) & 0xff);
buffer[index * 4 + 2] = (byte) ((value >> 16) & 0xff);
buffer[index * 4 + 1] = (byte) ((value >> 8) & 0xff);
buffer[index * 4 ] = (byte) ((value) & 0xff);
}
}
private int asInt(final int index) {
if (byteOrder == ByteOrder.BIG_ENDIAN) {
return (buffer[index * 4] & 0xff) << 24 | (buffer[index * 4 + 1] & 0xff) << 16 |
(buffer[index * 4 + 2] & 0xff) << 8 | buffer[index * 4 + 3] & 0xff;
}
else {
return (buffer[index * 4 + 3] & 0xff) << 24 | (buffer[index * 4 + 2] & 0xff) << 16 |
(buffer[index * 4 + 1] & 0xff) << 8 | buffer[index * 4] & 0xff;
}
}
private void putShort(final int index, final int value) {
if (byteOrder == ByteOrder.BIG_ENDIAN) {
buffer[index * 2 ] = (byte) ((value >> 8) & 0xff);
buffer[index * 2 + 1] = (byte) ((value) & 0xff);
}
else {
buffer[index * 2 + 1] = (byte) ((value >> 8) & 0xff);
buffer[index * 2 ] = (byte) ((value) & 0xff);
}
}
private short asShort(final int index) {
if (byteOrder == ByteOrder.BIG_ENDIAN) {
return (short) ((buffer[index * 2] & 0xff) << 8 | buffer[index * 2 + 1] & 0xff);
}
else {
return (short) ((buffer[index * 2 + 1] & 0xff) << 8 | buffer[index * 2] & 0xff);
}
}
@Override
public int read() throws IOException {
if (decodedLength < 0) {
return -1;
}
if (decodedPos >= decodedLength) {
fetch();
if (decodedLength < 0) {
return -1;
}
}
return buffer[decodedPos++] & 0xff;
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
if (decodedLength < 0) {
return -1;
}
if (decodedPos >= decodedLength) {
fetch();
if (decodedLength < 0) {
return -1;
}
}
int read = Math.min(decodedLength - decodedPos, len);
System.arraycopy(buffer, decodedPos, b, off, read);
decodedPos += read;
return read;
}
@Override
public long skip(long n) throws IOException {
if (decodedLength < 0) {
return -1;
}
if (decodedPos >= decodedLength) {
fetch();
if (decodedLength < 0) {
return -1;
}
}
int skipped = (int) Math.min(decodedLength - decodedPos, n);
decodedPos += skipped;
return skipped;
}
}

View File

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

View File

@@ -33,16 +33,18 @@ import com.twelvemonkeys.io.enc.Decoder;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
/**
* LZWDecoder
* LempelZivWelch (LZW) decompression. LZW is a universal loss-less data compression algorithm
* created by Abraham Lempel, Jacob Ziv, and Terry Welch.
* Inspired by libTiff's LZW decompression.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: LZWDecoder.java,v 1.0 08.05.12 21:11 haraldk Exp$
* @see <a href="http://en.wikipedia.org/wiki/Lempel%E2%80%93Ziv%E2%80%93Welch">LZW (Wikipedia)</a>
*/
final class LZWDecoder implements Decoder {
abstract class LZWDecoder implements Decoder {
/** Clear: Re-initialize tables. */
static final int CLEAR_CODE = 256;
/** End of Information. */
@@ -51,44 +53,44 @@ final class LZWDecoder implements Decoder {
private static final int MIN_BITS = 9;
private static final int MAX_BITS = 12;
private final boolean reverseBitOrder;
private static final int TABLE_SIZE = 1 << MAX_BITS;
private int currentByte = -1;
private int bitPos;
private final boolean compatibilityMode;
// TODO: Consider speeding things up with a "string" type (instead of the inner byte[]),
// that uses variable size/dynamic allocation, to avoid the excessive array copying?
// private final byte[][] table = new byte[4096][0]; // libTiff adds another 1024 "for compatibility"...
private final byte[][] table = new byte[4096 + 1024][0]; // libTiff adds another 1024 "for compatibility"...
private final String[] table;
private int tableLength;
private int bitsPerCode;
int bitsPerCode;
private int oldCode = CLEAR_CODE;
private int maxCode;
int bitMask;
private int maxString;
private boolean eofReached;
boolean eofReached;
int nextData;
int nextBits;
LZWDecoder(final boolean reverseBitOrder) {
this.reverseBitOrder = reverseBitOrder;
protected LZWDecoder(final boolean compatibilityMode) {
this.compatibilityMode = compatibilityMode;
table = new String[compatibilityMode ? TABLE_SIZE + 1024 : TABLE_SIZE]; // libTiff adds 1024 "for compatibility"...
// First 258 entries of table is always fixed
for (int i = 0; i < 256; i++) {
table[i] = new byte[] {(byte) i};
table[i] = new String((byte) i);
}
init();
}
LZWDecoder() {
this(false);
}
private int maxCodeFor(final int bits) {
return reverseBitOrder ? (1 << bits) - 2 : (1 << bits) - 1;
private static int bitmaskFor(final int bits) {
return (1 << bits) - 1;
}
private void init() {
tableLength = 258;
bitsPerCode = MIN_BITS;
maxCode = maxCodeFor(bitsPerCode);
bitMask = bitmaskFor(bitsPerCode);
maxCode = maxCode();
maxString = 1;
}
@@ -107,25 +109,17 @@ final class LZWDecoder implements Decoder {
break;
}
bufferPos += writeString(table[code], buffer, bufferPos);
bufferPos += table[code].writeTo(buffer, bufferPos);
}
else {
if (code > tableLength + 1 || oldCode >= tableLength) {
// TODO: FixMe for old, borked streams
System.err.println("code: " + code);
System.err.println("oldCode: " + oldCode);
System.err.println("tableLength: " + tableLength);
throw new DecodeException("Corrupted LZW table");
}
if (isInTable(code)) {
bufferPos += writeString(table[code], buffer, bufferPos);
addStringToTable(concatenate(table[oldCode], table[code][0]));
bufferPos += table[code].writeTo(buffer, bufferPos);
addStringToTable(table[oldCode].concatenate(table[code].firstChar));
}
else {
byte[] outString = concatenate(table[oldCode], table[oldCode][0]);
String outString = table[oldCode].concatenate(table[oldCode].firstChar);
bufferPos += writeString(outString, buffer, bufferPos);
bufferPos += outString.writeTo(buffer, bufferPos);
addStringToTable(outString);
}
}
@@ -141,29 +135,23 @@ final class LZWDecoder implements Decoder {
return bufferPos;
}
private static byte[] concatenate(final byte[] string, final byte firstChar) {
byte[] result = Arrays.copyOf(string, string.length + 1);
result[string.length] = firstChar;
return result;
}
private void addStringToTable(final byte[] string) throws IOException {
private void addStringToTable(final String string) throws IOException {
table[tableLength++] = string;
if (tableLength >= maxCode) {
if (tableLength > maxCode) {
bitsPerCode++;
if (bitsPerCode > MAX_BITS) {
if (reverseBitOrder) {
if (compatibilityMode) {
bitsPerCode--;
}
else {
throw new DecodeException(String.format("TIFF LZW with more than %d bits per code encountered (table overflow)", MAX_BITS));
throw new DecodeException(java.lang.String.format("TIFF LZW with more than %d bits per code encountered (table overflow)", MAX_BITS));
}
}
maxCode = maxCodeFor(bitsPerCode);
bitMask = bitmaskFor(bitsPerCode);
maxCode = maxCode();
}
if (string.length > maxString) {
@@ -171,89 +159,14 @@ final class LZWDecoder implements Decoder {
}
}
private static int writeString(final byte[] string, final byte[] buffer, final int bufferPos) {
if (string.length == 0) {
return 0;
}
else if (string.length == 1) {
buffer[bufferPos] = string[0];
return 1;
}
else {
System.arraycopy(string, 0, buffer, bufferPos, string.length);
return string.length;
}
}
protected abstract int maxCode();
private boolean isInTable(int code) {
return code < tableLength;
}
private int getNextCode(final InputStream stream) throws IOException {
if (eofReached) {
return EOI_CODE;
}
protected abstract int getNextCode(final InputStream stream) throws IOException;
int bitsToFill = bitsPerCode;
int value = 0;
while (bitsToFill > 0) {
int nextBits;
if (bitPos == 0) {
nextBits = stream.read();
if (nextBits == -1) {
// This is really a bad stream, but should be safe to handle this way, rather than throwing an EOFException.
// An EOFException will be thrown by the decoder stream later, if further reading is attempted.
eofReached = true;
return EOI_CODE;
}
}
else {
nextBits = currentByte;
}
int bitsFromHere = 8 - bitPos;
if (bitsFromHere > bitsToFill) {
bitsFromHere = bitsToFill;
}
if (reverseBitOrder) {
// NOTE: This is a spec violation. However, libTiff reads such files.
// TIFF 6.0 Specification, Section 13: "LZW Compression"/"The Algorithm", page 61, says:
// "LZW compression codes are stored into bytes in high-to-low-order fashion, i.e., FillOrder
// is assumed to be 1. The compressed codes are written as bytes (not words) so that the
// compressed data will be identical whether it is an II or MM file."
// Fill bytes from right-to-left
for (int i = 0; i < bitsFromHere; i++) {
int destBitPos = bitsPerCode - bitsToFill + i;
int srcBitPos = bitPos + i;
value |= ((nextBits & (1 << srcBitPos)) >> srcBitPos) << destBitPos;
}
}
else {
value |= (nextBits >> 8 - bitPos - bitsFromHere & 0xff >> 8 - bitsFromHere) << bitsToFill - bitsFromHere;
}
bitsToFill -= bitsFromHere;
bitPos += bitsFromHere;
if (bitPos >= 8) {
bitPos = 0;
}
currentByte = nextBits;
}
if (value == EOI_CODE) {
eofReached = true;
}
return value;
}
static boolean isOldBitReversedStream(final InputStream stream) throws IOException {
stream.mark(2);
@@ -267,5 +180,147 @@ final class LZWDecoder implements Decoder {
stream.reset();
}
}
public static LZWDecoder create(boolean oldBitReversedStream) {
return oldBitReversedStream ? new LZWCompatibilityDecoder() : new LZWSpecDecoder();
}
private static final class LZWSpecDecoder extends LZWDecoder {
protected LZWSpecDecoder() {
super(false);
}
@Override
protected int maxCode() {
return bitMask - 1;
}
protected final int getNextCode(final InputStream stream) throws IOException {
if (eofReached) {
return EOI_CODE;
}
int code;
int read = stream.read();
if (read < 0) {
eofReached = true;
return EOI_CODE;
}
nextData = (nextData << 8) | read;
nextBits += 8;
if (nextBits < bitsPerCode) {
read = stream.read();
if (read < 0) {
eofReached = true;
return EOI_CODE;
}
nextData = (nextData << 8) | read;
nextBits += 8;
}
code = ((nextData >> (nextBits - bitsPerCode)) & bitMask);
nextBits -= bitsPerCode;
return code;
}
}
private static final class LZWCompatibilityDecoder extends LZWDecoder {
// NOTE: This is a spec violation. However, libTiff reads such files.
// TIFF 6.0 Specification, Section 13: "LZW Compression"/"The Algorithm", page 61, says:
// "LZW compression codes are stored into bytes in high-to-low-order fashion, i.e., FillOrder
// is assumed to be 1. The compressed codes are written as bytes (not words) so that the
// compressed data will be identical whether it is an II or MM file."
protected LZWCompatibilityDecoder() {
super(true);
}
@Override
protected int maxCode() {
return bitMask;
}
protected final int getNextCode(final InputStream stream) throws IOException {
if (eofReached) {
return EOI_CODE;
}
int code;
int read = stream.read();
if (read < 0) {
eofReached = true;
return EOI_CODE;
}
nextData |= read << nextBits;
nextBits += 8;
if (nextBits < bitsPerCode) {
read = stream.read();
if (read < 0) {
eofReached = true;
return EOI_CODE;
}
nextData |= read << nextBits;
nextBits += 8;
}
code = (nextData & bitMask);
nextData >>= bitsPerCode;
nextBits -= bitsPerCode;
return code;
}
}
private static final class String {
final String previous;
final int length;
final byte value;
final byte firstChar; // Copied forward for fast access
public String(final byte code) {
this(code, code, 1, null);
}
private String(final byte value, final byte firstChar, final int length, final String previous) {
this.value = value;
this.firstChar = firstChar;
this.length = length;
this.previous = previous;
}
public final String concatenate(final byte firstChar) {
return new String(firstChar, this.firstChar, length + 1, this);
}
public final int writeTo(final byte[] buffer, final int offset) {
if (length == 0) {
return 0;
}
else if (length == 1) {
buffer[offset] = value;
return 1;
}
else {
String e = this;
for (int i = length - 1; i >= 0; i--) {
buffer[offset + i] = e.value;
e = e.previous;
}
return length;
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,348 @@
/*
* Copyright (c) 2013, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name "TwelveMonkeys" nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.imageio.plugins.tiff;
import com.twelvemonkeys.lang.Validate;
import java.awt.image.DataBufferByte;
import java.awt.image.Raster;
import java.io.EOFException;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
/**
* Input stream that provides on-the-fly conversion and upsampling of TIFF susampled YCbCr samples to (raw) RGB samples.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: YCbCrUpsamplerStream.java,v 1.0 31.01.13 09:25 haraldk Exp$
*/
final class YCbCrUpsamplerStream extends FilterInputStream {
// NOTE: DO NOT MODIFY OR EXPOSE THIS ARRAY OUTSIDE PACKAGE!
static final double[] CCIR_601_1_COEFFICIENTS = new double[] {299.0 / 1000.0, 587.0 / 1000.0, 114.0 / 1000.0};
private final int horizChromaSub;
private final int vertChromaSub;
private final int yCbCrPos;
private final int columns;
private final double[] coefficients;
private final int units;
private final int unitSize;
private final int padding;
private final byte[] decodedRows;
int decodedLength;
int decodedPos;
private final byte[] buffer;
int bufferLength;
int bufferPos;
public YCbCrUpsamplerStream(final InputStream stream, final int[] chromaSub, final int yCbCrPos, final int columns, final double[] coefficients) {
super(Validate.notNull(stream, "stream"));
this.horizChromaSub = chromaSub[0];
this.vertChromaSub = chromaSub[1];
this.yCbCrPos = yCbCrPos;
this.columns = columns;
this.coefficients = Arrays.equals(CCIR_601_1_COEFFICIENTS, coefficients) ? null : coefficients;
// In TIFF, subsampled streams are stored in "units" of horiz * vert pixels.
// For a 4:2 subsampled stream like this:
//
// Y0 Y1 Y2 Y3 Cb0 Cr0 Y8 Y9 Y10 Y11 Cb1 Cr1
// Y4 Y5 Y6 Y7 Y12Y13Y14 Y15
//
// In the stream, the order is: Y0,Y1,Y2..Y7,Cb0,Cr0, Y8...Y15,Cb1,Cr1, Y16...
unitSize = horizChromaSub * vertChromaSub + 2;
units = (columns + horizChromaSub - 1) / horizChromaSub; // If columns % horizChromasSub != 0...
padding = units * horizChromaSub - columns; // ...each coded row will be padded to fill unit
decodedRows = new byte[columns * vertChromaSub * 3];
buffer = new byte[unitSize * units];
}
private void fetch() throws IOException {
if (bufferPos >= bufferLength) {
int pos = 0;
int read;
// This *SHOULD* read an entire row of units into the buffer, otherwise decodeRows will throw EOFException
while (pos < buffer.length && (read = in.read(buffer, pos, buffer.length - pos)) > 0) {
pos += read;
}
bufferLength = pos;
bufferPos = 0;
}
if (bufferLength > 0) {
decodeRows();
}
else {
decodedLength = -1;
}
}
private void decodeRows() throws EOFException {
decodedLength = decodedRows.length;
for (int u = 0; u < units; u++) {
if (bufferPos >= bufferLength) {
throw new EOFException("Unexpected end of stream");
}
// Decode one unit
byte cb = buffer[bufferPos + unitSize - 2];
byte cr = buffer[bufferPos + unitSize - 1];
for (int y = 0; y < vertChromaSub; y++) {
for (int x = 0; x < horizChromaSub; x++) {
// Skip padding at end of row
int column = horizChromaSub * u + x;
if (column >= columns) {
bufferPos += padding;
break;
}
int pixelOff = 3 * (column + columns * y);
decodedRows[pixelOff] = buffer[bufferPos++];
decodedRows[pixelOff + 1] = cb;
decodedRows[pixelOff + 2] = cr;
// Convert to RGB
if (coefficients == null) {
YCbCrConverter.convertYCbCr2RGB(decodedRows, decodedRows, pixelOff);
}
else {
convertYCbCr2RGB(decodedRows, decodedRows, coefficients, pixelOff);
}
}
}
bufferPos += 2; // Skip CbCr bytes at end of unit
}
bufferPos = bufferLength;
decodedPos = 0;
}
@Override
public int read() throws IOException {
if (decodedLength < 0) {
return -1;
}
if (decodedPos >= decodedLength) {
fetch();
if (decodedLength < 0) {
return -1;
}
}
return decodedRows[decodedPos++] & 0xff;
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
if (decodedLength < 0) {
return -1;
}
if (decodedPos >= decodedLength) {
fetch();
if (decodedLength < 0) {
return -1;
}
}
int read = Math.min(decodedLength - decodedPos, len);
System.arraycopy(decodedRows, decodedPos, b, off, read);
decodedPos += read;
return read;
}
@Override
public long skip(long n) throws IOException {
if (decodedLength < 0) {
return -1;
}
if (decodedPos >= decodedLength) {
fetch();
if (decodedLength < 0) {
return -1;
}
}
int skipped = (int) Math.min(decodedLength - decodedPos, n);
decodedPos += skipped;
return skipped;
}
@Override
public boolean markSupported() {
return false;
}
@Override
public synchronized void reset() throws IOException {
throw new IOException("mark/reset not supported");
}
private void convertYCbCr2RGB(final byte[] yCbCr, final byte[] rgb, final double[] coefficients, final int offset) {
double y = (yCbCr[offset ] & 0xff);
double cb = (yCbCr[offset + 1] & 0xff) - 128; // TODO: The -128 part seems bogus... Consult ReferenceBlackWhite??? But default to these values?
double cr = (yCbCr[offset + 2] & 0xff) - 128;
double lumaRed = coefficients[0];
double lumaGreen = coefficients[1];
double lumaBlue = coefficients[2];
int red = (int) Math.round(cr * (2 - 2 * lumaRed) + y);
int blue = (int) Math.round(cb * (2 - 2 * lumaBlue) + y);
int green = (int) Math.round((y - lumaRed * (rgb[offset] & 0xff) - lumaBlue * (rgb[offset + 2] & 0xff)) / lumaGreen);
rgb[offset ] = clamp(red);
rgb[offset + 2] = clamp(blue);
rgb[offset + 1] = clamp(green);
}
private static byte clamp(int val) {
return (byte) Math.max(0, Math.min(255, val));
}
// TODO: This code is copied from JPEG package, make it "more" public: com.tm.imageio.color package?
/**
* Static inner class for lazy-loading of conversion tables.
*/
static final class YCbCrConverter {
/** Define tables for YCC->RGB color space conversion. */
private final static int SCALEBITS = 16;
private final static int MAXJSAMPLE = 255;
private final static int CENTERJSAMPLE = 128;
private final static int ONE_HALF = 1 << (SCALEBITS - 1);
private final static int[] Cr_R_LUT = new int[MAXJSAMPLE + 1];
private final static int[] Cb_B_LUT = new int[MAXJSAMPLE + 1];
private final static int[] Cr_G_LUT = new int[MAXJSAMPLE + 1];
private final static int[] Cb_G_LUT = new int[MAXJSAMPLE + 1];
/**
* Initializes tables for YCC->RGB color space conversion.
*/
private static void buildYCCtoRGBtable() {
if (TIFFImageReader.DEBUG) {
System.err.println("Building YCC conversion table");
}
for (int i = 0, x = -CENTERJSAMPLE; i <= MAXJSAMPLE; i++, x++) {
// i is the actual input pixel value, in the range 0..MAXJSAMPLE
// The Cb or Cr value we are thinking of is x = i - CENTERJSAMPLE
// Cr=>R value is nearest int to 1.40200 * x
Cr_R_LUT[i] = (int) ((1.40200 * (1 << SCALEBITS) + 0.5) * x + ONE_HALF) >> SCALEBITS;
// Cb=>B value is nearest int to 1.77200 * x
Cb_B_LUT[i] = (int) ((1.77200 * (1 << SCALEBITS) + 0.5) * x + ONE_HALF) >> SCALEBITS;
// Cr=>G value is scaled-up -0.71414 * x
Cr_G_LUT[i] = -(int) (0.71414 * (1 << SCALEBITS) + 0.5) * x;
// Cb=>G value is scaled-up -0.34414 * x
// We also add in ONE_HALF so that need not do it in inner loop
Cb_G_LUT[i] = -(int) ((0.34414) * (1 << SCALEBITS) + 0.5) * x + ONE_HALF;
}
}
static {
buildYCCtoRGBtable();
}
static void convertYCbCr2RGB(final Raster raster) {
final int height = raster.getHeight();
final int width = raster.getWidth();
final byte[] data = ((DataBufferByte) raster.getDataBuffer()).getData();
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
convertYCbCr2RGB(data, data, (x + y * width) * 3);
}
}
}
static void convertYCbCr2RGB(final byte[] yCbCr, final byte[] rgb, final int offset) {
int y = yCbCr[offset ] & 0xff;
int cr = yCbCr[offset + 2] & 0xff;
int cb = yCbCr[offset + 1] & 0xff;
rgb[offset ] = clamp(y + Cr_R_LUT[cr]);
rgb[offset + 1] = clamp(y + (Cb_G_LUT[cb] + Cr_G_LUT[cr] >> SCALEBITS));
rgb[offset + 2] = clamp(y + Cb_B_LUT[cb]);
}
static void convertYCCK2CMYK(final Raster raster) {
final int height = raster.getHeight();
final int width = raster.getWidth();
final byte[] data = ((DataBufferByte) raster.getDataBuffer()).getData();
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
convertYCCK2CMYK(data, data, (x + y * width) * 4);
}
}
}
private static void convertYCCK2CMYK(byte[] ycck, byte[] cmyk, int offset) {
// Inverted
int y = 255 - ycck[offset ] & 0xff;
int cb = 255 - ycck[offset + 1] & 0xff;
int cr = 255 - ycck[offset + 2] & 0xff;
int k = 255 - ycck[offset + 3] & 0xff;
int cmykC = MAXJSAMPLE - (y + Cr_R_LUT[cr]);
int cmykM = MAXJSAMPLE - (y + (Cb_G_LUT[cb] + Cr_G_LUT[cr] >> SCALEBITS));
int cmykY = MAXJSAMPLE - (y + Cb_B_LUT[cb]);
cmyk[offset ] = clamp(cmykC);
cmyk[offset + 1] = clamp(cmykM);
cmyk[offset + 2] = clamp(cmykY);
cmyk[offset + 3] = (byte) k; // K passes through unchanged
}
// private static byte clamp(int val) {
// return (byte) Math.max(0, Math.min(255, val));
// }
}
}

View File

@@ -0,0 +1,137 @@
<!DOCTYPE com_sun_media_imageio_plugins_tiff_image_1.0 [
<!ELEMENT com_sun_media_imageio_plugins_tiff_image_1.0 (TIFFIFD)*>
<!ELEMENT TIFFIFD (TIFFField | TIFFIFD)*>
<!-- An IFD (directory) containing fields -->
<!ATTLIST TIFFIFD "tagSets" CDATA #REQUIRED>
<!-- Data type: String -->
<!ATTLIST TIFFIFD "parentTagNumber" CDATA #IMPLIED>
<!-- The tag number of the field pointing to this IFD -->
<!-- Data type: Integer -->
<!ATTLIST TIFFIFD "parentTagName" CDATA #IMPLIED>
<!-- A mnemonic name for the field pointing to this IFD, if known
-->
<!-- Data type: String -->
<!ELEMENT TIFFField (TIFFBytes | TIFFAsciis | TIFFShorts | TIFFSShorts | TIFFLongs | TIFFSLongs | TIFFRationals | TIFFSRationals | TIFFFloats | TIFFDoubles | TIFFUndefined)>
<!-- A field containing data -->
<!ATTLIST TIFFField "number" CDATA #REQUIRED>
<!-- The tag number asociated with the field -->
<!-- Data type: String -->
<!ATTLIST TIFFField "name" CDATA #IMPLIED>
<!-- A mnemonic name associated with the field, if known -->
<!-- Data type: String -->
<!ELEMENT TIFFBytes (TIFFByte)*>
<!-- A sequence of TIFFByte nodes -->
<!ELEMENT TIFFByte EMPTY>
<!-- An integral value between 0 and 255 -->
<!ATTLIST TIFFByte "value" CDATA #IMPLIED>
<!-- The value -->
<!-- Data type: String -->
<!ATTLIST TIFFByte "description" CDATA #IMPLIED>
<!-- A description, if available -->
<!-- Data type: String -->
<!ELEMENT TIFFAsciis (TIFFAscii)*>
<!-- A sequence of TIFFAscii nodes -->
<!ELEMENT TIFFAscii EMPTY>
<!-- A String value -->
<!ATTLIST TIFFAscii "value" CDATA #IMPLIED>
<!-- The value -->
<!-- Data type: String -->
<!ELEMENT TIFFShorts (TIFFShort)*>
<!-- A sequence of TIFFShort nodes -->
<!ELEMENT TIFFShort EMPTY>
<!-- An integral value between 0 and 65535 -->
<!ATTLIST TIFFShort "value" CDATA #IMPLIED>
<!-- The value -->
<!-- Data type: String -->
<!ATTLIST TIFFShort "description" CDATA #IMPLIED>
<!-- A description, if available -->
<!-- Data type: String -->
<!ELEMENT TIFFSShorts (TIFFSShort)*>
<!-- A sequence of TIFFSShort nodes -->
<!ELEMENT TIFFSShort EMPTY>
<!-- An integral value between -32768 and 32767 -->
<!ATTLIST TIFFSShort "value" CDATA #IMPLIED>
<!-- The value -->
<!-- Data type: String -->
<!ATTLIST TIFFSShort "description" CDATA #IMPLIED>
<!-- A description, if available -->
<!-- Data type: String -->
<!ELEMENT TIFFLongs (TIFFLong)*>
<!-- A sequence of TIFFLong nodes -->
<!ELEMENT TIFFLong EMPTY>
<!-- An integral value between 0 and 4294967295 -->
<!ATTLIST TIFFLong "value" CDATA #IMPLIED>
<!-- The value -->
<!-- Data type: String -->
<!ATTLIST TIFFLong "description" CDATA #IMPLIED>
<!-- A description, if available -->
<!-- Data type: String -->
<!ELEMENT TIFFSLongs (TIFFSLong)*>
<!-- A sequence of TIFFSLong nodes -->
<!ELEMENT TIFFSLong EMPTY>
<!-- An integral value between -2147483648 and 2147482647 -->
<!ATTLIST TIFFSLong "value" CDATA #IMPLIED>
<!-- The value -->
<!-- Data type: String -->
<!ATTLIST TIFFSLong "description" CDATA #IMPLIED>
<!-- A description, if available -->
<!-- Data type: String -->
<!ELEMENT TIFFRationals (TIFFRational)*>
<!-- A sequence of TIFFRational nodes -->
<!ELEMENT TIFFRational EMPTY>
<!-- A rational value consisting of an unsigned numerator and
denominator -->
<!ATTLIST TIFFRational "value" CDATA #IMPLIED>
<!-- The numerator and denominator, separated by a slash -->
<!-- Data type: String -->
<!ELEMENT TIFFSRationals (TIFFSRational)*>
<!-- A sequence of TIFFSRational nodes -->
<!ELEMENT TIFFSRational EMPTY>
<!-- A rational value consisting of a signed numerator and
denominator -->
<!ATTLIST TIFFSRational "value" CDATA #IMPLIED>
<!-- The numerator and denominator, separated by a slash -->
<!-- Data type: String -->
<!ELEMENT TIFFFloats (TIFFFloat)*>
<!-- A sequence of TIFFFloat nodes -->
<!ELEMENT TIFFFloat EMPTY>
<!-- A single-precision floating-point value -->
<!ATTLIST TIFFFloat "value" CDATA #IMPLIED>
<!-- The value -->
<!-- Data type: String -->
<!ELEMENT TIFFDoubles (TIFFDouble)*>
<!-- A sequence of TIFFDouble nodes -->
<!ELEMENT TIFFDouble EMPTY>
<!-- A double-precision floating-point value -->
<!ATTLIST TIFFDouble "value" CDATA #IMPLIED>
<!-- The value -->
<!-- Data type: String -->
<!ELEMENT TIFFUndefined EMPTY>
<!-- Uninterpreted byte data -->
<!ATTLIST TIFFUndefined "value" CDATA #IMPLIED>
<!-- A list of comma-separated byte values -->
<!-- Data type: String -->
]>

View File

@@ -0,0 +1,9 @@
<!DOCTYPE com_sun_media_imageio_plugins_tiff_stream_1.0 [
<!ELEMENT com_sun_media_imageio_plugins_tiff_stream_1.0 (ByteOrder)>
<!ELEMENT ByteOrder EMPTY>
<!-- The stream byte order -->
<!ATTLIST ByteOrder "value" CDATA #REQUIRED>
<!-- One of "BIG_ENDIAN" or "LITTLE_ENDIAN" -->
<!-- Data type: String -->
]>

View File

@@ -0,0 +1,166 @@
/*
* Copyright (c) 2013, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name "TwelveMonkeys" nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.imageio.plugins.tiff;
import org.junit.Before;
import org.junit.Test;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferByte;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import static org.junit.Assert.*;
/**
* CCITTFaxDecoderStreamTest
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: CCITTFaxDecoderStreamTest.java,v 1.0 09.03.13 14:44 haraldk Exp$
*/
public class CCITTFaxDecoderStreamTest {
// TODO: Better tests (full A4 width scan lines?)
// From http://www.mikekohn.net/file_formats/tiff.php
static final byte[] DATA_TYPE_2 = {
(byte) 0x84, (byte) 0xe0, // 10000100 11100000
(byte) 0x84, (byte) 0xe0, // 10000100 11100000
(byte) 0x84, (byte) 0xe0, // 10000100 11100000
(byte) 0x7d, (byte) 0xc0, // 01111101 11000000
};
static final byte[] DATA_TYPE_3 = {
0x00, 0x01, (byte) 0xc2, 0x70,
0x00, 0x01, 0x70,
0x01,
};
static final byte[] DATA_TYPE_4 = {
0x26, (byte) 0xb0, 95, (byte) 0xfa, (byte) 0xc0
};
// Image should be (6 x 4):
// 1 1 1 0 1 1 x x
// 1 1 1 0 1 1 x x
// 1 1 1 0 1 1 x x
// 1 1 0 0 1 1 x x
BufferedImage image;
@Before
public void init() {
image = new BufferedImage(6, 4, BufferedImage.TYPE_BYTE_BINARY);
for (int y = 0; y < 4; y++) {
for (int x = 0; x < 6; x++) {
image.setRGB(x, y, x == 3 ? 0xff000000 : 0xffffffff);
}
}
image.setRGB(2, 3, 0xff000000);
}
@Test
public void testReadCountType2() throws IOException {
InputStream stream = new CCITTFaxDecoderStream(new ByteArrayInputStream(DATA_TYPE_2), 6, TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE, 1);
int count = 0;
int read;
while ((read = stream.read()) >= 0) {
count++;
}
// Just make sure we'll have 4 bytes
assertEquals(4, count);
// Verify that we don't return arbitrary values
assertEquals(-1, read);
}
@Test
public void testDecodeType2() throws IOException {
InputStream stream = new CCITTFaxDecoderStream(new ByteArrayInputStream(DATA_TYPE_2), 6, TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE, 1);
byte[] imageData = ((DataBufferByte) image.getData().getDataBuffer()).getData();
byte[] bytes = new byte[imageData.length];
new DataInputStream(stream).readFully(bytes);
// JPanel panel = new JPanel();
// panel.add(new JLabel("Expected", new BufferedImageIcon(image, 300, 300, true), JLabel.CENTER));
// panel.add(new JLabel("Actual", new BufferedImageIcon(new BufferedImage(image.getColorModel(), Raster.createPackedRaster(new DataBufferByte(bytes, bytes.length), 6, 4, 1, null), false, null), 300, 300, true), JLabel.CENTER));
// JOptionPane.showConfirmDialog(null, panel);
assertArrayEquals(imageData, bytes);
}
@Test(expected = IllegalArgumentException.class)
public void testDecodeType3() throws IOException {
InputStream stream = new CCITTFaxDecoderStream(new ByteArrayInputStream(DATA_TYPE_3), 6, TIFFExtension.COMPRESSION_CCITT_T4, 1);
byte[] imageData = ((DataBufferByte) image.getData().getDataBuffer()).getData();
byte[] bytes = new byte[imageData.length];
DataInputStream dataInput = new DataInputStream(stream);
for (int y = 0; y < image.getHeight(); y++) {
System.err.println("y: " + y);
dataInput.readFully(bytes, y * image.getWidth(), image.getWidth());
}
// JPanel panel = new JPanel();
// panel.add(new JLabel("Expected", new BufferedImageIcon(image, 300, 300, true), JLabel.CENTER));
// panel.add(new JLabel("Actual", new BufferedImageIcon(new BufferedImage(image.getColorModel(), Raster.createPackedRaster(new DataBufferByte(bytes, bytes.length), 6, 4, 1, null), false, null), 300, 300, true), JLabel.CENTER));
// JOptionPane.showConfirmDialog(null, panel);
assertArrayEquals(imageData, bytes);
}
@Test(expected = IllegalArgumentException.class)
public void testDecodeType4() throws IOException {
InputStream stream = new CCITTFaxDecoderStream(new ByteArrayInputStream(DATA_TYPE_4), 6, TIFFExtension.COMPRESSION_CCITT_T6, 1);
byte[] imageData = ((DataBufferByte) image.getData().getDataBuffer()).getData();
byte[] bytes = new byte[imageData.length];
DataInputStream dataInput = new DataInputStream(stream);
for (int y = 0; y < image.getHeight(); y++) {
System.err.println("y: " + y);
dataInput.readFully(bytes, y * image.getWidth(), image.getWidth());
}
// JPanel panel = new JPanel();
// panel.add(new JLabel("Expected", new BufferedImageIcon(image, 300, 300, true), JLabel.CENTER));
// panel.add(new JLabel("Actual", new BufferedImageIcon(new BufferedImage(image.getColorModel(), Raster.createPackedRaster(new DataBufferByte(bytes, bytes.length), 6, 4, 1, null), false, null), 300, 300, true), JLabel.CENTER));
// JOptionPane.showConfirmDialog(null, panel);
assertArrayEquals(imageData, bytes);
}
}

View File

@@ -0,0 +1,569 @@
/*
* Copyright (c) 2013, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name "TwelveMonkeys" nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.imageio.plugins.tiff;
import com.twelvemonkeys.io.FastByteArrayOutputStream;
import com.twelvemonkeys.io.LittleEndianDataInputStream;
import com.twelvemonkeys.io.LittleEndianDataOutputStream;
import org.junit.Test;
import java.io.*;
import java.nio.ByteOrder;
import static org.junit.Assert.*;
/**
* HorizontalDeDifferencingStreamTest
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: HorizontalDeDifferencingStreamTest.java,v 1.0 13.03.13 12:46 haraldk Exp$
*/
public class HorizontalDeDifferencingStreamTest {
@Test
public void testRead1SPP1BPS() throws IOException {
// 1 sample per pixel, 1 bits per sample (mono/indexed)
byte[] data = {
(byte) 0x80, 0x00, 0x00,
0x71, 0x11, 0x44,
};
InputStream stream = new HorizontalDeDifferencingStream(new ByteArrayInputStream(data), 24, 1, 1, ByteOrder.BIG_ENDIAN);
// Row 1
assertEquals(0xff, stream.read());
assertEquals(0xff, stream.read());
assertEquals(0xff, stream.read());
// Row 2
assertEquals(0x5e, stream.read());
assertEquals(0x1e, stream.read());
assertEquals(0x78, stream.read());
// EOF
assertEquals(-1, stream.read());
}
@Test
public void testRead1SPP2BPS() throws IOException {
// 1 sample per pixel, 2 bits per sample (gray/indexed)
byte[] data = {
(byte) 0xc0, 0x00, 0x00, 0x00,
0x71, 0x11, 0x44, (byte) 0xcc,
};
InputStream stream = new HorizontalDeDifferencingStream(new ByteArrayInputStream(data), 16, 1, 2, ByteOrder.BIG_ENDIAN);
// Row 1
assertEquals(0xff, stream.read());
assertEquals(0xff, stream.read());
assertEquals(0xff, stream.read());
assertEquals(0xff, stream.read());
// Row 2
assertEquals(0x41, stream.read());
assertEquals(0x6b, stream.read());
assertEquals(0x05, stream.read());
assertEquals(0x0f, stream.read());
// EOF
assertEquals(-1, stream.read());
}
@Test
public void testRead1SPP4BPS() throws IOException {
// 1 sample per pixel, 4 bits per sample (gray/indexed)
byte[] data = {
(byte) 0xf0, 0x00, 0x00, 0x00,
0x70, 0x11, 0x44, (byte) 0xcc,
0x00, 0x01, 0x10, (byte) 0xe0
};
InputStream stream = new HorizontalDeDifferencingStream(new ByteArrayInputStream(data), 8, 1, 4, ByteOrder.BIG_ENDIAN);
// Row 1
assertEquals(0xff, stream.read());
assertEquals(0xff, stream.read());
assertEquals(0xff, stream.read());
assertEquals(0xff, stream.read());
// Row 2
assertEquals(0x77, stream.read());
assertEquals(0x89, stream.read());
assertEquals(0xd1, stream.read());
assertEquals(0xd9, stream.read());
// Row 3
assertEquals(0x00, stream.read());
assertEquals(0x01, stream.read());
assertEquals(0x22, stream.read());
assertEquals(0x00, stream.read());
// EOF
assertEquals(-1, stream.read());
}
@Test
public void testRead1SPP8BPS() throws IOException {
// 1 sample per pixel, 8 bits per sample (gray/indexed)
byte[] data = {
(byte) 0xff, 0, 0, 0,
0x7f, 1, 4, -4,
0x00, 127, 127, -127
};
InputStream stream = new HorizontalDeDifferencingStream(new ByteArrayInputStream(data), 4, 1, 8, ByteOrder.BIG_ENDIAN);
// Row 1
assertEquals(0xff, stream.read());
assertEquals(0xff, stream.read());
assertEquals(0xff, stream.read());
assertEquals(0xff, stream.read());
// Row 2
assertEquals(0x7f, stream.read());
assertEquals(0x80, stream.read());
assertEquals(0x84, stream.read());
assertEquals(0x80, stream.read());
// Row 3
assertEquals(0x00, stream.read());
assertEquals(0x7f, stream.read());
assertEquals(0xfe, stream.read());
assertEquals(0x7f, stream.read());
// EOF
assertEquals(-1, stream.read());
}
@Test
public void testReadArray1SPP8BPS() throws IOException {
// 1 sample per pixel, 8 bits per sample (gray/indexed)
byte[] data = {
(byte) 0xff, 0, 0, 0,
0x7f, 1, 4, -4,
0x00, 127, 127, -127
};
InputStream stream = new HorizontalDeDifferencingStream(new ByteArrayInputStream(data), 4, 1, 8, ByteOrder.BIG_ENDIAN);
byte[] result = new byte[data.length];
new DataInputStream(stream).readFully(result);
assertArrayEquals(
new byte[] {
(byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
0x7f, (byte) 0x80, (byte) 0x84, (byte) 0x80,
0x00, 0x7f, (byte) 0xfe, 0x7f,
},
result
);
// EOF
assertEquals(-1, stream.read(new byte[16]));
assertEquals(-1, stream.read());
}
@Test
public void testRead1SPP32BPS() throws IOException {
// 1 sample per pixel, 32 bits per sample (gray)
FastByteArrayOutputStream out = new FastByteArrayOutputStream(16);
DataOutput dataOut = new DataOutputStream(out);
dataOut.writeInt(0x00000000);
dataOut.writeInt(305419896);
dataOut.writeInt(305419896);
dataOut.writeInt(-610839792);
InputStream in = new HorizontalDeDifferencingStream(out.createInputStream(), 4, 1, 32, ByteOrder.BIG_ENDIAN);
DataInput dataIn = new DataInputStream(in);
// Row 1
assertEquals(0, dataIn.readInt());
assertEquals(305419896, dataIn.readInt());
assertEquals(610839792, dataIn.readInt());
assertEquals(0, dataIn.readInt());
// EOF
assertEquals(-1, in.read());
}
@Test
public void testRead1SPP32BPSLittleEndian() throws IOException {
// 1 sample per pixel, 32 bits per sample (gray)
FastByteArrayOutputStream out = new FastByteArrayOutputStream(16);
DataOutput dataOut = new LittleEndianDataOutputStream(out);
dataOut.writeInt(0x00000000);
dataOut.writeInt(305419896);
dataOut.writeInt(305419896);
dataOut.writeInt(-610839792);
InputStream in = new HorizontalDeDifferencingStream(out.createInputStream(), 4, 1, 32, ByteOrder.LITTLE_ENDIAN);
DataInput dataIn = new LittleEndianDataInputStream(in);
// Row 1
assertEquals(0, dataIn.readInt());
assertEquals(305419896, dataIn.readInt());
assertEquals(610839792, dataIn.readInt());
assertEquals(0, dataIn.readInt());
// EOF
assertEquals(-1, in.read());
}
@Test
public void testRead1SPP64BPS() throws IOException {
// 1 sample per pixel, 64 bits per sample (gray)
FastByteArrayOutputStream out = new FastByteArrayOutputStream(32);
DataOutput dataOut = new DataOutputStream(out);
dataOut.writeLong(0x00000000);
dataOut.writeLong(81985529216486895L);
dataOut.writeLong(81985529216486895L);
dataOut.writeLong(-163971058432973790L);
InputStream in = new HorizontalDeDifferencingStream(out.createInputStream(), 4, 1, 64, ByteOrder.BIG_ENDIAN);
DataInput dataIn = new DataInputStream(in);
// Row 1
assertEquals(0, dataIn.readLong());
assertEquals(81985529216486895L, dataIn.readLong());
assertEquals(163971058432973790L, dataIn.readLong());
assertEquals(0, dataIn.readLong());
// EOF
assertEquals(-1, in.read());
}
@Test
public void testRead1SPP64BPSLittleEndian() throws IOException {
// 1 sample per pixel, 64 bits per sample (gray)
FastByteArrayOutputStream out = new FastByteArrayOutputStream(32);
DataOutput dataOut = new LittleEndianDataOutputStream(out);
dataOut.writeLong(0x00000000);
dataOut.writeLong(81985529216486895L);
dataOut.writeLong(81985529216486895L);
dataOut.writeLong(-163971058432973790L);
InputStream in = new HorizontalDeDifferencingStream(out.createInputStream(), 4, 1, 64, ByteOrder.LITTLE_ENDIAN);
DataInput dataIn = new LittleEndianDataInputStream(in);
// Row 1
assertEquals(0, dataIn.readLong());
assertEquals(81985529216486895L, dataIn.readLong());
assertEquals(163971058432973790L, dataIn.readLong());
assertEquals(0, dataIn.readLong());
// EOF
assertEquals(-1, in.read());
}
@Test
public void testRead3SPP8BPS() throws IOException {
// 3 samples per pixel, 8 bits per sample (RGB)
byte[] data = {
(byte) 0xff, (byte) 0x00, (byte) 0x7f, -1, -1, -1, -4, -4, -4, 4, 4, 4,
0x7f, 0x7f, 0x7f, 1, 1, 1, 4, 4, 4, -4, -4, -4,
0x00, 0x00, 0x00, 127, -127, 0, -127, 127, 0, 0, 0, 127,
};
InputStream stream = new HorizontalDeDifferencingStream(new ByteArrayInputStream(data), 4, 3, 8, ByteOrder.BIG_ENDIAN);
// Row 1
assertEquals(0xff, stream.read());
assertEquals(0x00, stream.read());
assertEquals(0x7f, stream.read());
assertEquals(0xfe, stream.read());
assertEquals(0xff, stream.read());
assertEquals(0x7e, stream.read());
assertEquals(0xfa, stream.read());
assertEquals(0xfb, stream.read());
assertEquals(0x7a, stream.read());
assertEquals(0xfe, stream.read());
assertEquals(0xff, stream.read());
assertEquals(0x7e, stream.read());
// Row 2
assertEquals(0x7f, stream.read());
assertEquals(0x7f, stream.read());
assertEquals(0x7f, stream.read());
assertEquals(0x80, stream.read());
assertEquals(0x80, stream.read());
assertEquals(0x80, stream.read());
assertEquals(0x84, stream.read());
assertEquals(0x84, stream.read());
assertEquals(0x84, stream.read());
assertEquals(0x80, stream.read());
assertEquals(0x80, stream.read());
assertEquals(0x80, stream.read());
// Row 3
assertEquals(0x00, stream.read());
assertEquals(0x00, stream.read());
assertEquals(0x00, stream.read());
assertEquals(0x7f, stream.read());
assertEquals(0x81, stream.read());
assertEquals(0x00, stream.read());
assertEquals(0x00, stream.read());
assertEquals(0x00, stream.read());
assertEquals(0x00, stream.read());
assertEquals(0x00, stream.read());
assertEquals(0x00, stream.read());
assertEquals(0x7f, stream.read());
// EOF
assertEquals(-1, stream.read());
}
@Test
public void testRead3SPP16BPS() throws IOException {
FastByteArrayOutputStream out = new FastByteArrayOutputStream(24);
DataOutput dataOut = new DataOutputStream(out);
dataOut.writeShort(0x0000);
dataOut.writeShort(0x0000);
dataOut.writeShort(0x0000);
dataOut.writeShort(4660);
dataOut.writeShort(30292);
dataOut.writeShort(4660);
dataOut.writeShort(4660);
dataOut.writeShort(30292);
dataOut.writeShort(4660);
dataOut.writeShort(-9320);
dataOut.writeShort(-60584);
dataOut.writeShort(-9320);
dataOut.writeShort(0x0000);
dataOut.writeShort(0x0000);
dataOut.writeShort(0x0000);
dataOut.writeShort(30292);
dataOut.writeShort(30292);
dataOut.writeShort(30292);
dataOut.writeShort(30292);
dataOut.writeShort(30292);
dataOut.writeShort(30292);
dataOut.writeShort(-60584);
dataOut.writeShort(-60584);
dataOut.writeShort(-60584);
InputStream in = new HorizontalDeDifferencingStream(out.createInputStream(), 4, 3, 16, ByteOrder.BIG_ENDIAN);
DataInput dataIn = new DataInputStream(in);
// Row 1
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(4660, dataIn.readUnsignedShort());
assertEquals(30292, dataIn.readUnsignedShort());
assertEquals(4660, dataIn.readUnsignedShort());
assertEquals(9320, dataIn.readUnsignedShort());
assertEquals(60584, dataIn.readUnsignedShort());
assertEquals(9320, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
// Row 2
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(30292, dataIn.readUnsignedShort());
assertEquals(30292, dataIn.readUnsignedShort());
assertEquals(30292, dataIn.readUnsignedShort());
assertEquals(60584, dataIn.readUnsignedShort());
assertEquals(60584, dataIn.readUnsignedShort());
assertEquals(60584, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
// EOF
assertEquals(-1, in.read());
}
@Test
public void testRead3SPP16BPSLittleEndian() throws IOException {
FastByteArrayOutputStream out = new FastByteArrayOutputStream(24);
DataOutput dataOut = new LittleEndianDataOutputStream(out);
dataOut.writeShort(0x0000);
dataOut.writeShort(0x0000);
dataOut.writeShort(0x0000);
dataOut.writeShort(4660);
dataOut.writeShort(30292);
dataOut.writeShort(4660);
dataOut.writeShort(4660);
dataOut.writeShort(30292);
dataOut.writeShort(4660);
dataOut.writeShort(-9320);
dataOut.writeShort(-60584);
dataOut.writeShort(-9320);
dataOut.writeShort(0x0000);
dataOut.writeShort(0x0000);
dataOut.writeShort(0x0000);
dataOut.writeShort(30292);
dataOut.writeShort(30292);
dataOut.writeShort(30292);
dataOut.writeShort(30292);
dataOut.writeShort(30292);
dataOut.writeShort(30292);
dataOut.writeShort(-60584);
dataOut.writeShort(-60584);
dataOut.writeShort(-60584);
InputStream in = new HorizontalDeDifferencingStream(out.createInputStream(), 4, 3, 16, ByteOrder.LITTLE_ENDIAN);
DataInput dataIn = new LittleEndianDataInputStream(in);
// Row 1
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(4660, dataIn.readUnsignedShort());
assertEquals(30292, dataIn.readUnsignedShort());
assertEquals(4660, dataIn.readUnsignedShort());
assertEquals(9320, dataIn.readUnsignedShort());
assertEquals(60584, dataIn.readUnsignedShort());
assertEquals(9320, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
// Row 2
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(30292, dataIn.readUnsignedShort());
assertEquals(30292, dataIn.readUnsignedShort());
assertEquals(30292, dataIn.readUnsignedShort());
assertEquals(60584, dataIn.readUnsignedShort());
assertEquals(60584, dataIn.readUnsignedShort());
assertEquals(60584, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
// EOF
assertEquals(-1, in.read());
}
@Test
public void testRead4SPP8BPS() throws IOException {
// 4 samples per pixel, 8 bits per sample (RGBA)
byte[] data = {
(byte) 0xff, (byte) 0x00, (byte) 0x7f, 0x00, -1, -1, -1, -1, -4, -4, -4, -4, 4, 4, 4, 4,
0x7f, 0x7f, 0x7f, 0x7f, 1, 1, 1, 1, 4, 4, 4, 4, -4, -4, -4, -4,
};
InputStream stream = new HorizontalDeDifferencingStream(new ByteArrayInputStream(data), 4, 4, 8, ByteOrder.BIG_ENDIAN);
// Row 1
assertEquals(0xff, stream.read());
assertEquals(0x00, stream.read());
assertEquals(0x7f, stream.read());
assertEquals(0x00, stream.read());
assertEquals(0xfe, stream.read());
assertEquals(0xff, stream.read());
assertEquals(0x7e, stream.read());
assertEquals(0xff, stream.read());
assertEquals(0xfa, stream.read());
assertEquals(0xfb, stream.read());
assertEquals(0x7a, stream.read());
assertEquals(0xfb, stream.read());
assertEquals(0xfe, stream.read());
assertEquals(0xff, stream.read());
assertEquals(0x7e, stream.read());
assertEquals(0xff, stream.read());
// Row 2
assertEquals(0x7f, stream.read());
assertEquals(0x7f, stream.read());
assertEquals(0x7f, stream.read());
assertEquals(0x7f, stream.read());
assertEquals(0x80, stream.read());
assertEquals(0x80, stream.read());
assertEquals(0x80, stream.read());
assertEquals(0x80, stream.read());
assertEquals(0x84, stream.read());
assertEquals(0x84, stream.read());
assertEquals(0x84, stream.read());
assertEquals(0x84, stream.read());
assertEquals(0x80, stream.read());
assertEquals(0x80, stream.read());
assertEquals(0x80, stream.read());
assertEquals(0x80, stream.read());
// EOF
assertEquals(-1, stream.read());
}
@Test
public void testReadArray4SPP8BPS() throws IOException {
// 4 samples per pixel, 8 bits per sample (RGBA)
byte[] data = {
(byte) 0xff, (byte) 0x00, (byte) 0x7f, 0x00, -1, -1, -1, -1, -4, -4, -4, -4, 4, 4, 4, 4,
0x7f, 0x7f, 0x7f, 0x7f, 1, 1, 1, 1, 4, 4, 4, 4, -4, -4, -4, -4,
};
InputStream stream = new HorizontalDeDifferencingStream(new ByteArrayInputStream(data), 4, 4, 8, ByteOrder.BIG_ENDIAN);
byte[] result = new byte[data.length];
new DataInputStream(stream).readFully(result);
assertArrayEquals(
new byte[] {
(byte) 0xff, 0x00, 0x7f, 0x00,
(byte) 0xfe, (byte) 0xff, 0x7e, (byte) 0xff,
(byte) 0xfa, (byte) 0xfb, 0x7a, (byte) 0xfb,
(byte) 0xfe, (byte) 0xff, 0x7e, (byte) 0xff,
0x7f, 0x7f, 0x7f, 0x7f,
(byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80,
(byte) 0x84, (byte) 0x84, (byte) 0x84, (byte) 0x84,
(byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80,
},
result
);
// EOF
assertEquals(-1, stream.read(new byte[16]));
assertEquals(-1, stream.read());
}
}

View File

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

View File

@@ -52,10 +52,15 @@ public class TIFFImageReaderTest extends ImageReaderAbstractTestCase<TIFFImageRe
new TestData(getClassLoaderResource("/tiff/sm_colors_tile.tif"), new Dimension(64, 64)), // RGB, uncompressed, tiled
new TestData(getClassLoaderResource("/tiff/sm_colors_pb_tile.tif"), new Dimension(64, 64)), // RGB, PackBits compressed, tiled
new TestData(getClassLoaderResource("/tiff/galaxy.tif"), new Dimension(965, 965)), // RGB, LZW compressed
new TestData(getClassLoaderResource("/tiff/quad-lzw.tif"), new Dimension(512, 384)), // RGB, Old spec (reversed) LZW compressed, tiled
new TestData(getClassLoaderResource("/tiff/bali.tif"), new Dimension(725, 489)), // Palette-based, LZW compressed
new TestData(getClassLoaderResource("/tiff/f14.tif"), new Dimension(640, 480)), // Gray, uncompressed
new TestData(getClassLoaderResource("/tiff/marbles.tif"), new Dimension(1419, 1001)), // RGB, LZW compressed w/predictor
new TestData(getClassLoaderResource("/tiff/chifley_logo.tif"), new Dimension(591, 177)) // CMYK, uncompressed
new TestData(getClassLoaderResource("/tiff/chifley_logo.tif"), new Dimension(591, 177)), // CMYK, uncompressed
new TestData(getClassLoaderResource("/tiff/ycbcr-cat.tif"), new Dimension(250, 325)), // YCbCr, LZW compressed
new TestData(getClassLoaderResource("/tiff/quad-jpeg.tif"), new Dimension(512, 384)), // YCbCr, JPEG compressed, striped
new TestData(getClassLoaderResource("/tiff/smallliz.tif"), new Dimension(160, 160)), // YCbCr, Old-Style JPEG compressed (full JFIF stream)
new TestData(getClassLoaderResource("/tiff/zackthecat.tif"), new Dimension(234, 213)) // YCbCr, Old-Style JPEG compressed (tables, no JFIF stream)
);
}
@@ -88,4 +93,6 @@ public class TIFFImageReaderTest extends ImageReaderAbstractTestCase<TIFFImageRe
protected List<String> getMIMETypes() {
return Arrays.asList("image/tiff");
}
// TODO: Test YCbCr colors
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2012, Harald Kuhr
* Copyright (c) 2013, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -28,20 +28,24 @@
package com.twelvemonkeys.imageio.plugins.tiff;
import com.twelvemonkeys.io.enc.Decoder;
import com.twelvemonkeys.io.InputStreamAbstractTestCase;
import org.junit.Ignore;
import java.io.IOException;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
/**
* CCITT Group 3 One-Dimensional (G31D) "No EOLs" Decoder.
* YCbCrUpsamplerStreamTest
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: G31DDecoder.java,v 1.0 23.05.12 15:55 haraldk Exp$
* @version $Id: YCbCrUpsamplerStreamTest.java,v 1.0 31.01.13 14:35 haraldk Exp$
*/
final class G31DDecoder implements Decoder {
public int decode(final InputStream stream, final byte[] buffer) throws IOException {
throw new UnsupportedOperationException("Method decode not implemented"); // TODO: Implement
@Ignore
public class YCbCrUpsamplerStreamTest extends InputStreamAbstractTestCase {
// TODO: Implement + add @Ignore for all tests that makes no sense for this class.
@Override
protected InputStream makeInputStream(byte[] pBytes) {
return new YCbCrUpsamplerStream(new ByteArrayInputStream(pBytes), new int[] {2, 2}, TIFFExtension.YCBCR_POSITIONING_CENTERED, pBytes.length / 4, null);
}
}

Binary file not shown.

Binary file not shown.

View File

@@ -45,7 +45,7 @@
<module>imageio-jmagick</module>
<!-- Test cases for the JRE provided ImageIO plugins -->
<module>imageio-reference</module>
<module>imageio-reference</module>
</modules>
<properties>
@@ -90,6 +90,7 @@
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<version>1.8.5</version>
<scope>test</scope>
</dependency>
</dependencies>
@@ -115,5 +116,5 @@
<scope>test</scope>
</dependency>
</dependencies>
</dependencyManagement>
</dependencyManagement>
</project>

View File

@@ -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<Point, Tile> createTileCache() {
return Collections.synchronizedMap(new SizedLRUMap<Point, Tile>(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<BufferedImage> data;
private final BufferedImage hardRef;
Tile(int x, int y, BufferedImage data) {
this.x = x;
this.y = y;
this.data = new SoftReference<BufferedImage>(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<Point, Tile> 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<Point> points = new ArrayList<Point>(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<BufferedImage> 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<Point, Tile> 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<K, V> extends LRUHashMap<K, V> {
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<K, V> 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 {

View File

@@ -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<K extends Serializable, V extends Serializable> extends AbstractMap<K, V>{
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<K, UUID> index = new LinkedHashMap<K, UUID>();
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<File>() {
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<Entry<K, V>> entrySet() {
return new AbstractSet<Entry<K, V>>() {
@Override
public Iterator<Entry<K, V>> iterator() {
return new Iterator<Entry<K, V>>() {
Iterator<Entry<K, UUID>> indexIter = index.entrySet().iterator();
public boolean hasNext() {
return indexIter.hasNext();
}
public Entry<K, V> next() {
return new Entry<K, V>() {
final Entry<K, UUID> 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
*/

View File

@@ -29,7 +29,7 @@
<artifactId>common-image</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.twelvemonkeys.common</groupId>
<artifactId>common-lang</artifactId>
@@ -65,7 +65,7 @@
<version>1.2.14</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
@@ -84,6 +84,7 @@
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<version>1.8.5</version>
<scope>test</scope>
</dependency>
</dependencies>
@@ -117,5 +118,5 @@
</plugin>
</plugins>
</build>
</project>

View File

@@ -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<String, List<String>> {
// TODO: This map is now a little too lazy.. Should cache entries too (instead?) !
private final static List<String> NULL_LIST = new ArrayList<String>();
private transient Map<String, List<String>> cache = new HashMap<String, List<String>>();
private transient int size = -1;
private transient AbstractSet<Entry<String, List<String>>> entries;
abstract class AbstractServletMapAdapter<T> extends AbstractMap<String, T> {
// TODO: This map is now a little too lazy.. Should cache entries!
private transient Set<Entry<String, T>> entries;
protected abstract Iterator<String> keysImpl();
protected abstract Iterator<String> valuesImpl(String pName);
protected abstract T valueImpl(String pName);
@Override
public List<String> 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<String> getValues(final String pName) {
List<String> values = cache.get(pName);
if (values == null) {
//noinspection unchecked
Iterator<String> 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<String> toList(final Iterator<String> pValues) {
List<String> list = new ArrayList<String>();
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<String> names = keysImpl(); names.hasNext(); names.next()) {
size++;
}
return size;
}
private void computeSize() {
size = 0;
for (Iterator<String> names = keysImpl(); names.hasNext(); names.next()) {
size++;
}
}
public Set<Entry<String, List<String>>> entrySet() {
public Set<Entry<String, T>> entrySet() {
if (entries == null) {
entries = new AbstractSet<Entry<String, List<String>>>() {
public Iterator<Entry<String, List<String>>> iterator() {
return new Iterator<Entry<String, List<String>>>() {
Iterator<String> headerNames = keysImpl();
entries = new AbstractSet<Entry<String, T>>() {
public Iterator<Entry<String, T>> iterator() {
return new Iterator<Entry<String, T>>() {
Iterator<String> keys = keysImpl();
public boolean hasNext() {
return headerNames.hasNext();
return keys.hasNext();
}
public Entry<String, List<String>> next() {
public Entry<String, T> 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, List<String
return entries;
}
private class HeaderEntry implements Entry<String, List<String>> {
String headerName;
private class HeaderEntry implements Entry<String, T> {
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<String> getValue() {
return get(headerName);
public T getValue() {
return get(key);
}
public List<String> setValue(List<String> pValue) {
throw new UnsupportedOperationException();
public T setValue(final T pValue) {
// Write-through if supported
return put(key, pValue);
}
@Override
public int hashCode() {
List<String> 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;
}

View File

@@ -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 <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: ServletAttributesMap.java,v 1.0 01.03.13 10:34 haraldk Exp$
*/
class ServletAttributesMapAdapter extends AbstractServletMapAdapter<Object> {
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<String> 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<String> keysImpl() {
final Enumeration<String> keys = getAttributeNames();
return new Iterator<String>() {
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;
}
}

View File

@@ -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<List<String>> {
protected final HttpServletRequest request;
public ServletHeadersMapAdapter(HttpServletRequest pRequest) {
request = Validate.notNull(pRequest, "request");
public ServletHeadersMapAdapter(final HttpServletRequest pRequest) {
request = notNull(pRequest, "request");
}
protected Iterator<String> valuesImpl(String pName) {
//noinspection unchecked
protected List<String> valueImpl(final String pName) {
@SuppressWarnings("unchecked")
Enumeration<String> headers = request.getHeaders(pName);
return headers == null ? null : CollectionUtil.iterator(headers);
return headers == null ? null : toList(CollectionUtil.iterator(headers));
}
private static List<String> toList(final Iterator<String> pValues) {
List<String> list = new ArrayList<String>();
CollectionUtil.addAll(list, pValues);
return Collections.unmodifiableList(list);
}
protected Iterator<String> keysImpl() {
//noinspection unchecked
@SuppressWarnings("unchecked")
Enumeration<String> headerNames = request.getHeaderNames();
return headerNames == null ? null : CollectionUtil.iterator(headerNames);
}
}

View File

@@ -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<List<String>> {
// 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<String> valuesImpl(String pName) {
protected List<String> valueImpl(String pName) {
String[] values = request.getParameterValues(pName);
return values == null ? null : CollectionUtil.iterator(values);
return values == null ? null : Arrays.asList(values);
}
protected Iterator<String> keysImpl() {
//noinspection unchecked
@SuppressWarnings("unchecked")
Enumeration<String> names = request.getParameterNames();
return names == null ? null : CollectionUtil.iterator(names);
}
}

View File

@@ -50,7 +50,7 @@ import java.util.Map;
/**
* Various servlet related helper methods.
*
* @author Harald Kuhr
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @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<String, ?> attributesAsMap(final ServletContext pContext) {
// }
//
// public static Map<String, ?> attributesAsMap(final ServletRequest pRequest) {
// }
//
/**
* Creates an <em>modifiable</em> {@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<String, Object> attributesAsMap(final ServletContext pContext) {
return new ServletAttributesMapAdapter(pContext);
}
/**
* Creates an <em>modifiable</em> {@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<String, Object> 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<String, List<String>> parametersAsMap(final HttpServletRequest pRequest) {
public static Map<String, List<String>> parametersAsMap(final ServletRequest pRequest) {
return new ServletParametersMapAdapter(pRequest);
}

View File

@@ -1089,13 +1089,13 @@ public class HTTPCache {
// TODO: Extract and make public?
final static class SizedLRUMap<K, V> extends LRUHashMap<K, V> {
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<K, V> 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();
}
}
}
}
}

View File

@@ -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
* <p/>
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @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<Integer> 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<String, Object> createAttributes(boolean initialValues) {
Map<String, Object> map = new ConcurrentHashMap<String, Object>();
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<String, Object> 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);
}
}
}

View File

@@ -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
* <p/>
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @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<Integer> 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<String, Object> createAttributes(boolean initialValues) {
Map<String, Object> map = new ConcurrentHashMap<String, Object>();
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<String, Object> 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);
}
}
}

View File

@@ -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 <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @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();

View File

@@ -17,7 +17,7 @@ import static org.mockito.Mockito.when;
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @version $Id: ServletHeadersMapAdapterTestCase.java#1 $
*/
public class ServletHeadersMapAdapterTestCase extends MapAbstractTestCase {
public class ServletHeadersMapAdapterTest extends MapAbstractTestCase {
private static final List<String> HEADER_VALUE_ETAG = Arrays.asList("\"1234567890abcdef\"");
private static final List<String> HEADER_VALUE_DATE = Arrays.asList(new Date().toString());
private static final List<String> HEADER_VALUE_FOO = Arrays.asList("one", "two");

View File

@@ -17,7 +17,7 @@ import static org.mockito.Mockito.when;
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @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<String> PARAM_VALUE_ETAG = Arrays.asList("\"1234567890abcdef\"");
private static final List<String> PARAM_VALUE_DATE = Arrays.asList(new Date().toString());
private static final List<String> PARAM_VALUE_FOO = Arrays.asList("one", "two");
@@ -93,4 +93,4 @@ public class ServletParametersMapAdapterTestCase extends MapAbstractTestCase {
return Collections.enumeration(collection);
}
}
}
}