mirror of
https://github.com/haraldk/TwelveMonkeys.git
synced 2025-10-03 23:53:15 -04:00
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:
@@ -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;
|
||||
|
@@ -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();
|
||||
}
|
||||
*/
|
||||
}
|
@@ -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() {
|
||||
|
@@ -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) {
|
||||
|
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@@ -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)) {
|
||||
|
@@ -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()];
|
||||
|
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -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
|
||||
|
@@ -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...
|
||||
|
@@ -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());
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
|
||||
|
@@ -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? :-)
|
||||
|
||||
|
@@ -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();
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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?
|
||||
|
@@ -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);
|
||||
}
|
||||
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -418,6 +418,10 @@ public abstract class ImageReaderBase extends ImageReader {
|
||||
}
|
||||
|
||||
private static class ImageLabel extends JLabel {
|
||||
static final String ZOOM_IN = "zoom-in";
|
||||
static final String ZOOM_OUT = "zoom-out";
|
||||
static final String ZOOM_ACTUAL = "zoom-actual";
|
||||
|
||||
Paint backgroundPaint;
|
||||
|
||||
final Paint checkeredBG;
|
||||
@@ -435,9 +439,8 @@ public abstract class ImageReaderBase extends ImageReader {
|
||||
|
||||
backgroundPaint = defaultBG != null ? defaultBG : checkeredBG;
|
||||
|
||||
JPopupMenu popup = createBackgroundPopup();
|
||||
|
||||
setComponentPopupMenu(popup);
|
||||
setupActions(pImage);
|
||||
setComponentPopupMenu(createPopupMenu());
|
||||
addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
public void mouseClicked(MouseEvent e) {
|
||||
@@ -448,24 +451,52 @@ public abstract class ImageReaderBase extends ImageReader {
|
||||
});
|
||||
}
|
||||
|
||||
private JPopupMenu createBackgroundPopup() {
|
||||
private void setupActions(final BufferedImage pImage) {
|
||||
// Mac weirdness... VK_MINUS/VK_PLUS seems to map to english key map always...
|
||||
bindAction(new ZoomAction("Zoom in", pImage, 2), ZOOM_IN, KeyStroke.getKeyStroke('+'), KeyStroke.getKeyStroke(KeyEvent.VK_ADD, 0));
|
||||
bindAction(new ZoomAction("Zoom out", pImage, .5), ZOOM_OUT, KeyStroke.getKeyStroke('-'), KeyStroke.getKeyStroke(KeyEvent.VK_SUBTRACT, 0));
|
||||
bindAction(new ZoomAction("Zoom actual", pImage), ZOOM_ACTUAL, KeyStroke.getKeyStroke('0'), KeyStroke.getKeyStroke(KeyEvent.VK_0, 0));
|
||||
}
|
||||
|
||||
private void bindAction(final AbstractAction action, final String key, final KeyStroke... keyStrokes) {
|
||||
for (KeyStroke keyStroke : keyStrokes) {
|
||||
getInputMap(WHEN_IN_FOCUSED_WINDOW).put(keyStroke, key);
|
||||
}
|
||||
|
||||
getActionMap().put(key, action);
|
||||
}
|
||||
|
||||
private JPopupMenu createPopupMenu() {
|
||||
JPopupMenu popup = new JPopupMenu();
|
||||
|
||||
popup.add(getActionMap().get(ZOOM_ACTUAL));
|
||||
popup.add(getActionMap().get(ZOOM_IN));
|
||||
popup.add(getActionMap().get(ZOOM_OUT));
|
||||
popup.addSeparator();
|
||||
|
||||
ButtonGroup group = new ButtonGroup();
|
||||
|
||||
addCheckBoxItem(new ChangeBackgroundAction("Checkered", checkeredBG), popup, group);
|
||||
popup.addSeparator();
|
||||
addCheckBoxItem(new ChangeBackgroundAction("White", Color.WHITE), popup, group);
|
||||
addCheckBoxItem(new ChangeBackgroundAction("Light", Color.LIGHT_GRAY), popup, group);
|
||||
addCheckBoxItem(new ChangeBackgroundAction("Gray", Color.GRAY), popup, group);
|
||||
addCheckBoxItem(new ChangeBackgroundAction("Dark", Color.DARK_GRAY), popup, group);
|
||||
addCheckBoxItem(new ChangeBackgroundAction("Black", Color.BLACK), popup, group);
|
||||
popup.addSeparator();
|
||||
addCheckBoxItem(new ChooseBackgroundAction("Choose...", defaultBG != null ? defaultBG : Color.BLUE), popup, group);
|
||||
JMenu background = new JMenu("Background");
|
||||
popup.add(background);
|
||||
|
||||
ChangeBackgroundAction checkered = new ChangeBackgroundAction("Checkered", checkeredBG);
|
||||
checkered.putValue(Action.SELECTED_KEY, backgroundPaint == checkeredBG);
|
||||
addCheckBoxItem(checkered, background, group);
|
||||
background.addSeparator();
|
||||
addCheckBoxItem(new ChangeBackgroundAction("White", Color.WHITE), background, group);
|
||||
addCheckBoxItem(new ChangeBackgroundAction("Light", Color.LIGHT_GRAY), background, group);
|
||||
addCheckBoxItem(new ChangeBackgroundAction("Gray", Color.GRAY), background, group);
|
||||
addCheckBoxItem(new ChangeBackgroundAction("Dark", Color.DARK_GRAY), background, group);
|
||||
addCheckBoxItem(new ChangeBackgroundAction("Black", Color.BLACK), background, group);
|
||||
background.addSeparator();
|
||||
ChooseBackgroundAction chooseBackgroundAction = new ChooseBackgroundAction("Choose...", defaultBG != null ? defaultBG : Color.BLUE);
|
||||
chooseBackgroundAction.putValue(Action.SELECTED_KEY, backgroundPaint == defaultBG);
|
||||
addCheckBoxItem(chooseBackgroundAction, background, group);
|
||||
|
||||
return popup;
|
||||
}
|
||||
|
||||
private void addCheckBoxItem(final Action pAction, final JPopupMenu pPopup, final ButtonGroup pGroup) {
|
||||
private void addCheckBoxItem(final Action pAction, final JMenu pPopup, final ButtonGroup pGroup) {
|
||||
JCheckBoxMenuItem item = new JCheckBoxMenuItem(pAction);
|
||||
pGroup.add(item);
|
||||
pPopup.add(item);
|
||||
@@ -553,6 +584,34 @@ public abstract class ImageReaderBase extends ImageReader {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ZoomAction extends AbstractAction {
|
||||
private final BufferedImage image;
|
||||
private final double zoomFactor;
|
||||
|
||||
public ZoomAction(final String name, final BufferedImage image, final double zoomFactor) {
|
||||
super(name);
|
||||
this.image = image;
|
||||
this.zoomFactor = zoomFactor;
|
||||
}
|
||||
|
||||
public ZoomAction(final String name, final BufferedImage image) {
|
||||
this(name, image, 0);
|
||||
}
|
||||
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
if (zoomFactor <= 0) {
|
||||
setIcon(new BufferedImageIcon(image));
|
||||
}
|
||||
else {
|
||||
Icon current = getIcon();
|
||||
int w = (int) Math.max(Math.min(current.getIconWidth() * zoomFactor, image.getWidth() * 16), image.getWidth() / 16);
|
||||
int h = (int) Math.max(Math.min(current.getIconHeight() * zoomFactor, image.getHeight() * 16), image.getHeight() / 16);
|
||||
|
||||
setIcon(new BufferedImageIcon(image, Math.max(w, 2), Math.max(h, 2), w > image.getWidth() || h > image.getHeight()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class ExitIfNoWindowPresentHandler extends WindowAdapter {
|
||||
|
@@ -73,8 +73,10 @@ public final class ColorSpaces {
|
||||
|
||||
private final static boolean DEBUG = "true".equalsIgnoreCase(System.getProperty("com.twelvemonkeys.imageio.color.debug"));
|
||||
|
||||
// JDK 7 seems to handle non-perceptual rendering intents gracefully, so we don't need to fiddle with the profiles
|
||||
private final static boolean JDK_HANDLES_RENDERING_INTENTS = SystemUtil.isClassAvailable("java.lang.invoke.CallSite");
|
||||
// OpenJDK 7 seems to handle non-perceptual rendering intents gracefully, so we don't need to fiddle with the profiles.
|
||||
// However, the later Oracle distribute JDK seems to include the color management code that has the known bugs...
|
||||
private final static boolean JDK_HANDLES_RENDERING_INTENTS =
|
||||
SystemUtil.isClassAvailable("java.lang.invoke.CallSite") && !SystemUtil.isClassAvailable("sun.java2d.cmm.kcms.CMM");
|
||||
|
||||
// NOTE: java.awt.color.ColorSpace.CS_* uses 1000-1004, we'll use 5000+ to not interfere with future additions
|
||||
|
||||
@@ -171,6 +173,21 @@ public final class ColorSpaces {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests whether an ICC color profile is equal to the default sRGB profile.
|
||||
*
|
||||
* @param profile the ICC profile to test. May not be {@code null}.
|
||||
* @return {@code true} if {@code profile} is equal to the default sRGB profile.
|
||||
* @throws IllegalArgumentException if {@code profile} is {@code null}
|
||||
*
|
||||
* @see java.awt.color.ColorSpace#isCS_sRGB()
|
||||
*/
|
||||
public static boolean isCS_sRGB(final ICC_Profile profile) {
|
||||
Validate.notNull(profile, "profile");
|
||||
|
||||
return profile.getColorSpaceType() == ColorSpace.TYPE_RGB && Arrays.equals(profile.getData(ICC_Profile.icSigHead), sRGB.header);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests whether an ICC color profile is known to cause problems for {@link java.awt.image.ColorConvertOp}.
|
||||
* <p />
|
||||
@@ -227,7 +244,7 @@ public final class ColorSpaces {
|
||||
|
||||
if (profile == null) {
|
||||
// Fall back to the bundled ClayRGB1998 public domain Adobe RGB 1998 compatible profile,
|
||||
// identical for all practical purposes
|
||||
// which is identical for all practical purposes
|
||||
profile = readProfileFromClasspathResource("/profiles/ClayRGB1998.icc");
|
||||
|
||||
if (profile == null) {
|
||||
@@ -337,15 +354,19 @@ public final class ColorSpaces {
|
||||
private static class sRGB {
|
||||
private static final byte[] header = ICC_Profile.getInstance(ColorSpace.CS_sRGB).getData(ICC_Profile.icSigHead);
|
||||
}
|
||||
|
||||
private static class CIEXYZ {
|
||||
private static final byte[] header = ICC_Profile.getInstance(ColorSpace.CS_CIEXYZ).getData(ICC_Profile.icSigHead);
|
||||
}
|
||||
|
||||
private static class PYCC {
|
||||
private static final byte[] header = ICC_Profile.getInstance(ColorSpace.CS_PYCC).getData(ICC_Profile.icSigHead);
|
||||
}
|
||||
|
||||
private static class GRAY {
|
||||
private static final byte[] header = ICC_Profile.getInstance(ColorSpace.CS_GRAY).getData(ICC_Profile.icSigHead);
|
||||
}
|
||||
|
||||
private static class LINEAR_RGB {
|
||||
private static final byte[] header = ICC_Profile.getInstance(ColorSpace.CS_LINEAR_RGB).getData(ICC_Profile.icSigHead);
|
||||
}
|
||||
@@ -359,7 +380,14 @@ public final class ColorSpaces {
|
||||
systemDefaults = SystemUtil.loadProperties(ColorSpaces.class, "com/twelvemonkeys/imageio/color/icc_profiles_" + os.id());
|
||||
}
|
||||
catch (IOException ignore) {
|
||||
ignore.printStackTrace();
|
||||
System.err.printf(
|
||||
"Warning: Could not load system default ICC profile locations from %s, will use bundled fallback profiles.\n",
|
||||
ignore.getMessage()
|
||||
);
|
||||
if (DEBUG) {
|
||||
ignore.printStackTrace();
|
||||
}
|
||||
|
||||
systemDefaults = null;
|
||||
}
|
||||
|
||||
|
@@ -0,0 +1,29 @@
|
||||
#
|
||||
# Copyright (c) 2013, Harald Kuhr
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
# * Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
# * Neither the name "TwelveMonkeys" nor the
|
||||
# names of its contributors may be used to endorse or promote products
|
||||
# derived from this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
||||
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
#
|
||||
#GENERIC_CMYK=unknown, use built in for now
|
||||
#ADOBE_RGB_1998=unknown, use built in for now
|
@@ -26,4 +26,4 @@
|
||||
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
#
|
||||
GENERIC_CMYK=/C:/Windows/System32/spool/drivers/color/RSWOP.icm
|
||||
#ADOBE_RGB_1998=use built in for now
|
||||
#ADOBE_RGB_1998=unknown, use built in for now
|
@@ -139,7 +139,7 @@ public class ColorSpacesTest {
|
||||
assertSame(cs, ColorSpaces.createColorSpace(iccCs.getProfile()));
|
||||
}
|
||||
else {
|
||||
System.err.println("Not an ICC_ColorSpace: " + cs);
|
||||
System.err.println("WARNING: Not an ICC_ColorSpace: " + cs);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,7 +163,25 @@ public class ColorSpacesTest {
|
||||
assertSame(cs, ColorSpaces.createColorSpace(iccCs.getProfile()));
|
||||
}
|
||||
else {
|
||||
System.err.println("Not an ICC_ColorSpace: " + cs);
|
||||
System.err.println("Warning: Not an ICC_ColorSpace: " + cs);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIsCS_sRGBTrue() {
|
||||
assertTrue(ColorSpaces.isCS_sRGB(ICC_Profile.getInstance(ColorSpace.CS_sRGB)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIsCS_sRGBFalse() {
|
||||
assertFalse(ColorSpaces.isCS_sRGB(ICC_Profile.getInstance(ColorSpace.CS_LINEAR_RGB)));
|
||||
assertFalse(ColorSpaces.isCS_sRGB(ICC_Profile.getInstance(ColorSpace.CS_CIEXYZ)));
|
||||
assertFalse(ColorSpaces.isCS_sRGB(ICC_Profile.getInstance(ColorSpace.CS_GRAY)));
|
||||
assertFalse(ColorSpaces.isCS_sRGB(ICC_Profile.getInstance(ColorSpace.CS_PYCC)));
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testIsCS_sRGBNull() {
|
||||
ColorSpaces.isCS_sRGB(null);
|
||||
}
|
||||
}
|
||||
|
@@ -54,12 +54,16 @@ final class SipsJP2Reader {
|
||||
|
||||
private static final File SIPS_COMMAND = new File("/usr/bin/sips");
|
||||
private static final boolean SIPS_EXISTS_AND_EXECUTES = existsAndExecutes(SIPS_COMMAND);
|
||||
private static final boolean DEBUG = "true".equalsIgnoreCase(System.getProperty("com.twelvemonkeys.imageio.plugins.icns.debug"));
|
||||
|
||||
private static boolean existsAndExecutes(final File cmd) {
|
||||
try {
|
||||
return cmd.exists() && cmd.canExecute();
|
||||
}
|
||||
catch (SecurityException ignore) {
|
||||
if (DEBUG) {
|
||||
ignore.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
|
25
imageio/imageio-jpeg/license.txt
Normal file
25
imageio/imageio-jpeg/license.txt
Normal file
@@ -0,0 +1,25 @@
|
||||
Copyright (c) 2013, Harald Kuhr
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
* Neither the name "TwelveMonkeys" nor the names of its contributors
|
||||
may be used to endorse or promote products derived from this software
|
||||
without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
@@ -29,13 +29,13 @@
|
||||
package com.twelvemonkeys.imageio.plugins.jpeg;
|
||||
|
||||
/**
|
||||
* AdobeDCT
|
||||
* AdobeDCTSegment
|
||||
*
|
||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||
* @author last modified by $Author: haraldk$
|
||||
* @version $Id: AdobeDCT.java,v 1.0 23.04.12 16:55 haraldk Exp$
|
||||
* @version $Id: AdobeDCTSegment.java,v 1.0 23.04.12 16:55 haraldk Exp$
|
||||
*/
|
||||
class AdobeDCT {
|
||||
class AdobeDCTSegment {
|
||||
public static final int Unknown = 0;
|
||||
public static final int YCC = 1;
|
||||
public static final int YCCK = 2;
|
||||
@@ -45,7 +45,7 @@ class AdobeDCT {
|
||||
final int flags1;
|
||||
final int transform;
|
||||
|
||||
public AdobeDCT(int version, int flags0, int flags1, int transform) {
|
||||
AdobeDCTSegment(int version, int flags0, int flags1, int transform) {
|
||||
this.version = version; // 100 or 101
|
||||
this.flags0 = flags0;
|
||||
this.flags1 = flags1;
|
@@ -147,7 +147,7 @@ final class EXIFThumbnailReader extends ThumbnailReader {
|
||||
}
|
||||
|
||||
Entry bitsPerSample = ifd.getEntryById(TIFF.TAG_BITS_PER_SAMPLE);
|
||||
Entry samplesPerPixel = ifd.getEntryById(TIFF.TAG_SAMPLES_PER_PIXELS);
|
||||
Entry samplesPerPixel = ifd.getEntryById(TIFF.TAG_SAMPLES_PER_PIXEL);
|
||||
Entry photometricInterpretation = ifd.getEntryById(TIFF.TAG_PHOTOMETRIC_INTERPRETATION);
|
||||
|
||||
// Required
|
||||
|
@@ -131,11 +131,12 @@ class FastCMYKToRGB implements /*BufferedImageOp,*/ RasterOp {
|
||||
return dest;
|
||||
}
|
||||
|
||||
@SuppressWarnings({"PointlessArithmeticExpression"})
|
||||
private void convertCMYKToRGB(byte[] cmyk, byte[] rgb) {
|
||||
rgb[0] = (byte) (((255 - cmyk[0] & 0xFF) * (255 - cmyk[3] & 0xFF)) / 255);
|
||||
rgb[1] = (byte) (((255 - cmyk[1] & 0xFF) * (255 - cmyk[3] & 0xFF)) / 255);
|
||||
rgb[2] = (byte) (((255 - cmyk[2] & 0xFF) * (255 - cmyk[3] & 0xFF)) / 255);
|
||||
// Adapted from http://www.easyrgb.com/index.php?X=MATH
|
||||
final int k = cmyk[3] & 0xFF;
|
||||
rgb[0] = (byte) (255 - (((cmyk[0] & 0xFF) * (255 - k) / 255) + k));
|
||||
rgb[1] = (byte) (255 - (((cmyk[1] & 0xFF) * (255 - k) / 255) + k));
|
||||
rgb[2] = (byte) (255 - (((cmyk[2] & 0xFF) * (255 - k) / 255) + k));
|
||||
}
|
||||
|
||||
public Rectangle2D getBounds2D(Raster src) {
|
||||
|
@@ -45,6 +45,7 @@ import com.twelvemonkeys.lang.Validate;
|
||||
import javax.imageio.*;
|
||||
import javax.imageio.event.IIOReadUpdateListener;
|
||||
import javax.imageio.event.IIOReadWarningListener;
|
||||
import javax.imageio.metadata.IIOMetadata;
|
||||
import javax.imageio.spi.ImageReaderSpi;
|
||||
import javax.imageio.stream.ImageInputStream;
|
||||
import java.awt.*;
|
||||
@@ -58,8 +59,26 @@ import java.util.List;
|
||||
|
||||
/**
|
||||
* A JPEG {@code ImageReader} implementation based on the JRE {@code JPEGImageReader},
|
||||
* with support for CMYK/YCCK JPEGs, non-standard color spaces,broken ICC profiles
|
||||
* and more.
|
||||
* that adds support and properly handles cases where the JRE version throws exceptions.
|
||||
* <p/>
|
||||
* Main features:
|
||||
* <ul>
|
||||
* <li>Support for CMYK JPEGs (converted to RGB by default or as CMYK, using the embedded ICC profile if applicable)</li>
|
||||
* <li>Support for Adobe YCCK JPEGs (converted to RGB by default or as CMYK, using the embedded ICC profile if applicable)</li>
|
||||
* <li>Support for JPEGs containing ICC profiles with interpretation other than 'Perceptual' (profile is assumed to be 'Perceptual' and used)</li>
|
||||
* <li>Support for JPEGs containing ICC profiles with class other than 'Display' (profile is assumed to have class 'Display' and used)</li>
|
||||
* <li>Support for JPEGs containing ICC profiles that are incompatible with stream data (image data is read, profile is ignored)</li>
|
||||
* <li>Support for JPEGs with corrupted ICC profiles (image data is read, profile is ignored)</li>
|
||||
* <li>Support for JPEGs with corrupted {@code ICC_PROFILE} segments (image data is read, profile is ignored)</li>
|
||||
* <li>Support for JPEGs using non-standard color spaces, unsupported by Java 2D (image data is read, profile is ignored)</li>
|
||||
* <li>Issues warnings instead of throwing exceptions in cases of corrupted data where ever the image data can still be read in a reasonable way</li>
|
||||
* </ul>
|
||||
* Thumbnail support:
|
||||
* <ul>
|
||||
* <li>Support for JFIF thumbnails (even if stream contains "inconsistent metadata")</li>
|
||||
* <li>Support for JFXX thumbnails (JPEG, Indexed and RGB)</li>
|
||||
* <li>Support for EXIF thumbnails (JPEG, RGB and YCbCr)</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||
* @author LUT-based YCbCR conversion by Werner Randelshofer
|
||||
@@ -76,7 +95,7 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
private static final Map<Integer, List<String>> SEGMENT_IDENTIFIERS = createSegmentIds();
|
||||
|
||||
private static Map<Integer, List<String>> createSegmentIds() {
|
||||
Map<Integer, List<String>> map = new HashMap<Integer, List<String>>();
|
||||
Map<Integer, List<String>> map = new LinkedHashMap<Integer, List<String>>();
|
||||
|
||||
// JFIF/JFXX APP0 markers
|
||||
map.put(JPEG.APP0, JPEGSegmentUtil.ALL_IDS);
|
||||
@@ -181,7 +200,7 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
typeList.add(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_INT_BGR));
|
||||
|
||||
// We also read and return CMYK if the source image is CMYK/YCCK + original color profile if present
|
||||
ICC_Profile profile = getEmbeddedICCProfile();
|
||||
ICC_Profile profile = getEmbeddedICCProfile(false);
|
||||
|
||||
if (csType == JPEGColorSpace.CMYK || csType == JPEGColorSpace.YCCK) {
|
||||
if (profile != null) {
|
||||
@@ -216,8 +235,7 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
ImageTypeSpecifier getRawImageType(int imageIndex) throws IOException {
|
||||
public ImageTypeSpecifier getRawImageType(int imageIndex) throws IOException {
|
||||
// If delegate can determine the spec, we'll just go with that
|
||||
ImageTypeSpecifier rawType = delegate.getRawImageType(imageIndex);
|
||||
|
||||
@@ -231,7 +249,7 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
switch (csType) {
|
||||
case CMYK:
|
||||
// Create based on embedded profile if exists, or create from "Generic CMYK"
|
||||
ICC_Profile profile = getEmbeddedICCProfile();
|
||||
ICC_Profile profile = getEmbeddedICCProfile(false);
|
||||
|
||||
if (profile != null) {
|
||||
return ImageTypeSpecifier.createInterleaved(ColorSpaces.createColorSpace(profile), new int[] {3, 2, 1, 0}, DataBuffer.TYPE_BYTE, false, false);
|
||||
@@ -267,16 +285,17 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
// Might want to look into the metadata, to see if there's a better way to identify these.
|
||||
boolean unsupported = !delegate.getImageTypes(imageIndex).hasNext();
|
||||
|
||||
ICC_Profile profile = getEmbeddedICCProfile();
|
||||
AdobeDCT adobeDCT = getAdobeDCT();
|
||||
ICC_Profile profile = getEmbeddedICCProfile(false);
|
||||
AdobeDCTSegment adobeDCT = getAdobeDCT();
|
||||
|
||||
// TODO: Probably something bogus here, as ICC profile isn't applied if reading through the delegate any more...
|
||||
// We need to apply ICC profile unless the profile is sRGB/default gray (whatever that is)
|
||||
// - or only filter out the bad ICC profiles in the JPEGSegmentImageInputStream.
|
||||
if (delegate.canReadRaster() && (
|
||||
unsupported ||
|
||||
adobeDCT != null && adobeDCT.getTransform() == AdobeDCT.YCCK ||
|
||||
profile != null && (ColorSpaces.isOffendingColorProfile(profile) || profile.getColorSpaceType() == ColorSpace.TYPE_CMYK))) {
|
||||
adobeDCT != null && adobeDCT.getTransform() == AdobeDCTSegment.YCCK ||
|
||||
profile != null && !ColorSpaces.isCS_sRGB(profile))) {
|
||||
// profile != null && (ColorSpaces.isOffendingColorProfile(profile) || profile.getColorSpaceType() == ColorSpace.TYPE_CMYK))) {
|
||||
if (DEBUG) {
|
||||
System.out.println("Reading using raster and extra conversion");
|
||||
System.out.println("ICC color profile: " + profile);
|
||||
@@ -296,8 +315,8 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
int origWidth = getWidth(imageIndex);
|
||||
int origHeight = getHeight(imageIndex);
|
||||
|
||||
AdobeDCT adobeDCT = getAdobeDCT();
|
||||
SOF startOfFrame = getSOF();
|
||||
AdobeDCTSegment adobeDCT = getAdobeDCT();
|
||||
SOFSegment startOfFrame = getSOF();
|
||||
JPEGColorSpace csType = getSourceCSType(adobeDCT, startOfFrame);
|
||||
|
||||
Iterator<ImageTypeSpecifier> imageTypes = getImageTypes(imageIndex);
|
||||
@@ -316,12 +335,12 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
}
|
||||
else if (intendedCS != null) {
|
||||
// Handle inconsistencies
|
||||
if (startOfFrame.componentsInFrame != intendedCS.getNumComponents()) {
|
||||
if (startOfFrame.componentsInFrame < 4 && (csType == JPEGColorSpace.CMYK || csType == JPEGColorSpace.YCCK)) {
|
||||
if (startOfFrame.componentsInFrame() != intendedCS.getNumComponents()) {
|
||||
if (startOfFrame.componentsInFrame() < 4 && (csType == JPEGColorSpace.CMYK || csType == JPEGColorSpace.YCCK)) {
|
||||
processWarningOccurred(String.format(
|
||||
"Invalid Adobe App14 marker. Indicates YCCK/CMYK data, but SOF%d has %d color components. " +
|
||||
"Ignoring Adobe App14 marker, assuming YCbCr/RGB data.",
|
||||
startOfFrame.marker & 0xf, startOfFrame.componentsInFrame
|
||||
startOfFrame.marker & 0xf, startOfFrame.componentsInFrame()
|
||||
));
|
||||
|
||||
csType = JPEGColorSpace.YCbCr;
|
||||
@@ -332,12 +351,15 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
"Embedded ICC color profile is incompatible with image data. " +
|
||||
"Profile indicates %d components, but SOF%d has %d color components. " +
|
||||
"Ignoring ICC profile, assuming source color space %s.",
|
||||
intendedCS.getNumComponents(), startOfFrame.marker & 0xf, startOfFrame.componentsInFrame, csType
|
||||
intendedCS.getNumComponents(), startOfFrame.marker & 0xf, startOfFrame.componentsInFrame(), csType
|
||||
));
|
||||
}
|
||||
}
|
||||
// NOTE: Avoid using CCOp if same color space, as it's more compatible that way
|
||||
else if (intendedCS != image.getColorModel().getColorSpace()) {
|
||||
if (DEBUG) {
|
||||
System.err.println("Converting from " + intendedCS + " to " + (image.getColorModel().getColorSpace().isCS_sRGB() ? "sRGB" : image.getColorModel().getColorSpace()));
|
||||
}
|
||||
convert = new ColorConvertOp(intendedCS, image.getColorModel().getColorSpace(), null);
|
||||
}
|
||||
// Else, pass through with no conversion
|
||||
@@ -346,10 +368,20 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
ColorSpace cmykCS = ColorSpaces.getColorSpace(ColorSpaces.CS_GENERIC_CMYK);
|
||||
|
||||
if (cmykCS instanceof ICC_ColorSpace) {
|
||||
processWarningOccurred(
|
||||
"No embedded ICC color profile, defaulting to \"generic\" CMYK ICC profile. " +
|
||||
"Colors may look incorrect."
|
||||
);
|
||||
|
||||
convert = new ColorConvertOp(cmykCS, image.getColorModel().getColorSpace(), null);
|
||||
}
|
||||
else {
|
||||
// ColorConvertOp using non-ICC CS is deadly slow, fall back to fast conversion instead
|
||||
processWarningOccurred(
|
||||
"No embedded ICC color profile, will convert using inaccurate CMYK to RGB conversion. " +
|
||||
"Colors may look incorrect."
|
||||
);
|
||||
|
||||
convert = new FastCMYKToRGB();
|
||||
}
|
||||
}
|
||||
@@ -436,7 +468,7 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
return image;
|
||||
}
|
||||
|
||||
static JPEGColorSpace getSourceCSType(AdobeDCT adobeDCT, final SOF startOfFrame) throws IIOException {
|
||||
static JPEGColorSpace getSourceCSType(AdobeDCTSegment adobeDCT, final SOFSegment startOfFrame) throws IIOException {
|
||||
/*
|
||||
ADAPTED from http://download.oracle.com/javase/6/docs/api/javax/imageio/metadata/doc-files/jpeg_metadata.html:
|
||||
|
||||
@@ -478,11 +510,11 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
|
||||
if (adobeDCT != null) {
|
||||
switch (adobeDCT.getTransform()) {
|
||||
case AdobeDCT.YCC:
|
||||
case AdobeDCTSegment.YCC:
|
||||
return JPEGColorSpace.YCbCr;
|
||||
case AdobeDCT.YCCK:
|
||||
case AdobeDCTSegment.YCCK:
|
||||
return JPEGColorSpace.YCCK;
|
||||
case AdobeDCT.Unknown:
|
||||
case AdobeDCTSegment.Unknown:
|
||||
if (startOfFrame.components.length == 1) {
|
||||
return JPEGColorSpace.Gray;
|
||||
}
|
||||
@@ -601,14 +633,24 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
|
||||
segments = JPEGSegmentUtil.readSegments(imageInput, SEGMENT_IDENTIFIERS);
|
||||
}
|
||||
catch (IOException ignore) {
|
||||
catch (IIOException ignore) {
|
||||
if (DEBUG) {
|
||||
ignore.printStackTrace();
|
||||
}
|
||||
}
|
||||
catch (IllegalArgumentException foo) {
|
||||
foo.printStackTrace();
|
||||
if (DEBUG) {
|
||||
foo.printStackTrace();
|
||||
}
|
||||
}
|
||||
finally {
|
||||
imageInput.reset();
|
||||
}
|
||||
|
||||
// In case of an exception, avoid NPE when referencing segments later
|
||||
if (segments == null) {
|
||||
segments = Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
private List<JPEGSegment> getAppSegments(final int marker, final String identifier) throws IOException {
|
||||
@@ -629,7 +671,7 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
return appSegments;
|
||||
}
|
||||
|
||||
private SOF getSOF() throws IOException {
|
||||
private SOFSegment getSOF() throws IOException {
|
||||
for (JPEGSegment segment : segments) {
|
||||
if (JPEG.SOF0 >= segment.marker() && segment.marker() <= JPEG.SOF3 ||
|
||||
JPEG.SOF5 >= segment.marker() && segment.marker() <= JPEG.SOF7 ||
|
||||
@@ -654,7 +696,7 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
components[i] = new SOFComponent(id, ((sub & 0xF0) >> 4), (sub & 0xF), qtSel);
|
||||
}
|
||||
|
||||
return new SOF(segment.marker(), samplePrecision, lines, samplesPerLine, componentsInFrame, components);
|
||||
return new SOFSegment(segment.marker(), samplePrecision, lines, samplesPerLine, components);
|
||||
}
|
||||
finally {
|
||||
data.close();
|
||||
@@ -665,7 +707,7 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
return null;
|
||||
}
|
||||
|
||||
private AdobeDCT getAdobeDCT() throws IOException {
|
||||
private AdobeDCTSegment getAdobeDCT() throws IOException {
|
||||
// TODO: Investigate http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6355567: 33/35 byte Adobe APP14 markers
|
||||
List<JPEGSegment> adobe = getAppSegments(JPEG.APP14, "Adobe");
|
||||
|
||||
@@ -673,7 +715,7 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
// version (byte), flags (4bytes), color transform (byte: 0=unknown, 1=YCC, 2=YCCK)
|
||||
DataInputStream stream = new DataInputStream(adobe.get(0).data());
|
||||
|
||||
return new AdobeDCT(
|
||||
return new AdobeDCTSegment(
|
||||
stream.readUnsignedByte(),
|
||||
stream.readUnsignedShort(),
|
||||
stream.readUnsignedShort(),
|
||||
@@ -717,10 +759,14 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
return data;
|
||||
}
|
||||
|
||||
private ICC_Profile getEmbeddedICCProfile() throws IOException {
|
||||
private ICC_Profile getEmbeddedICCProfile(final boolean allowBadIndexes) throws IOException {
|
||||
// ICC v 1.42 (2006) annex B:
|
||||
// APP2 marker (0xFFE2) + 2 byte length + ASCII 'ICC_PROFILE' + 0 (termination)
|
||||
// + 1 byte chunk number + 1 byte chunk count (allows ICC profiles chunked in multiple APP2 segments)
|
||||
|
||||
// TODO: Allow metadata to contain the wrongly indexed profiles, if readable
|
||||
// NOTE: We ignore any profile with wrong index for reading and image types, just to be on the safe side
|
||||
|
||||
List<JPEGSegment> segments = getAppSegments(JPEG.APP2, "ICC_PROFILE");
|
||||
|
||||
if (segments.size() == 1) {
|
||||
@@ -731,7 +777,8 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
int chunkCount = stream.readUnsignedByte();
|
||||
|
||||
if (chunkNumber != 1 && chunkCount != 1) {
|
||||
processWarningOccurred(String.format("Bad number of 'ICC_PROFILE' chunks: %d of %d. Assuming single chunk.", chunkNumber, chunkCount));
|
||||
processWarningOccurred(String.format("Unexpected number of 'ICC_PROFILE' chunks: %d of %d. Ignoring ICC profile.", chunkNumber, chunkCount));
|
||||
return null;
|
||||
}
|
||||
|
||||
return readICCProfileSafe(stream);
|
||||
@@ -742,19 +789,27 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
int chunkNumber = stream.readUnsignedByte();
|
||||
int chunkCount = stream.readUnsignedByte();
|
||||
|
||||
// TODO: Most of the time the ICC profiles are readable and should be obtainable from metadata...
|
||||
boolean badICC = false;
|
||||
if (chunkCount != segments.size()) {
|
||||
// Some weird JPEGs use 0-based indexes... count == 0 and all numbers == 0.
|
||||
// Others use count == 1, and all numbers == 1.
|
||||
// Handle these by issuing warning
|
||||
processWarningOccurred(String.format("Bad 'ICC_PROFILE' chunk count: %d. Ignoring ICC profile.", chunkCount));
|
||||
badICC = true;
|
||||
processWarningOccurred(String.format("Unexpected 'ICC_PROFILE' chunk count: %d. Ignoring count, assuming %d chunks in sequence.", chunkCount, segments.size()));
|
||||
|
||||
if (!allowBadIndexes) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (!badICC && chunkNumber < 1) {
|
||||
// Anything else is just ignored
|
||||
processWarningOccurred(String.format("Invalid 'ICC_PROFILE' chunk index: %d. Ignoring ICC profile.", chunkNumber));
|
||||
return null;
|
||||
|
||||
if (!allowBadIndexes) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
int count = badICC ? segments.size() : chunkCount;
|
||||
@@ -910,6 +965,51 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
return thumbnails.get(thumbnailIndex).read();
|
||||
}
|
||||
|
||||
|
||||
// Metadata
|
||||
|
||||
@Override
|
||||
public IIOMetadata getImageMetadata(int imageIndex) throws IOException {
|
||||
// TODO: Nice try, but no cigar.. getAsTree does not return a "live" view, so any modifications are thrown away
|
||||
IIOMetadata metadata = delegate.getImageMetadata(imageIndex);
|
||||
|
||||
// IIOMetadataNode tree = (IIOMetadataNode) metadata.getAsTree(metadata.getNativeMetadataFormatName());
|
||||
// Node jpegVariety = tree.getElementsByTagName("JPEGvariety").item(0);
|
||||
|
||||
// TODO: Allow EXIF (as app1EXIF) in the JPEGvariety (sic) node.
|
||||
// As EXIF is (a subset of) TIFF, (and the EXIF data is a valid TIFF stream) probably use something like:
|
||||
// http://download.java.net/media/jai-imageio/javadoc/1.1/com/sun/media/imageio/plugins/tiff/package-summary.html#ImageMetadata
|
||||
/*
|
||||
from: http://docs.oracle.com/javase/6/docs/api/javax/imageio/metadata/doc-files/jpeg_metadata.html
|
||||
|
||||
In future versions of the JPEG metadata format, other varieties of JPEG metadata may be supported (e.g. Exif)
|
||||
by defining other types of nodes which may appear as a child of the JPEGvariety node.
|
||||
|
||||
(Note that an application wishing to interpret Exif metadata given a metadata tree structure in the
|
||||
javax_imageio_jpeg_image_1.0 format must check for an unknown marker segment with a tag indicating an
|
||||
APP1 marker and containing data identifying it as an Exif marker segment. Then it may use application-specific
|
||||
code to interpret the data in the marker segment. If such an application were to encounter a metadata tree
|
||||
formatted according to a future version of the JPEG metadata format, the Exif marker segment might not be
|
||||
unknown in that format - it might be structured as a child node of the JPEGvariety node.
|
||||
|
||||
Thus, it is important for an application to specify which version to use by passing the string identifying
|
||||
the version to the method/constructor used to obtain an IIOMetadata object.)
|
||||
*/
|
||||
|
||||
// IIOMetadataNode app2ICC = new IIOMetadataNode("app2ICC");
|
||||
// app2ICC.setUserObject(getEmbeddedICCProfile());
|
||||
// jpegVariety.getFirstChild().appendChild(app2ICC);
|
||||
|
||||
// new XMLSerializer(System.err, System.getProperty("file.encoding")).serialize(tree, false);
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IIOMetadata getStreamMetadata() throws IOException {
|
||||
return delegate.getStreamMetadata();
|
||||
}
|
||||
|
||||
private static void invertCMYK(final Raster raster) {
|
||||
byte[] data = ((DataBufferByte) raster.getDataBuffer()).getData();
|
||||
|
||||
@@ -1135,73 +1235,6 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
}
|
||||
}
|
||||
|
||||
private static class SOF {
|
||||
private final int marker;
|
||||
private final int samplePrecision;
|
||||
private final int lines; // height
|
||||
private final int samplesPerLine; // width
|
||||
private final int componentsInFrame;
|
||||
final SOFComponent[] components;
|
||||
|
||||
public SOF(int marker, int samplePrecision, int lines, int samplesPerLine, int componentsInFrame, SOFComponent[] components) {
|
||||
this.marker = marker;
|
||||
this.samplePrecision = samplePrecision;
|
||||
this.lines = lines;
|
||||
this.samplesPerLine = samplesPerLine;
|
||||
this.componentsInFrame = componentsInFrame;
|
||||
this.components = components;
|
||||
}
|
||||
|
||||
public int getMarker() {
|
||||
return marker;
|
||||
}
|
||||
|
||||
public int getSamplePrecision() {
|
||||
return samplePrecision;
|
||||
}
|
||||
|
||||
public int getLines() {
|
||||
return lines;
|
||||
}
|
||||
|
||||
public int getSamplesPerLine() {
|
||||
return samplesPerLine;
|
||||
}
|
||||
|
||||
public int getComponentsInFrame() {
|
||||
return componentsInFrame;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format(
|
||||
"SOF%d[%04x, precision: %d, lines: %d, samples/line: %d, components: %s]",
|
||||
marker & 0xf, marker, samplePrecision, lines, samplesPerLine, Arrays.toString(components)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static class SOFComponent {
|
||||
final int id;
|
||||
final int hSub;
|
||||
final int vSub;
|
||||
final int qtSel;
|
||||
|
||||
public SOFComponent(int id, int hSub, int vSub, int qtSel) {
|
||||
this.id = id;
|
||||
this.hSub = hSub;
|
||||
this.vSub = vSub;
|
||||
this.qtSel = qtSel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
// Use id either as component number or component name, based on value
|
||||
Serializable idStr = (id >= 'a' && id <= 'z' || id >= 'A' && id <= 'Z') ? "'" + (char) id + "'" : id;
|
||||
return String.format("id: %s, sub: %d/%d, sel: %d", idStr, hSub, vSub, qtSel);
|
||||
}
|
||||
}
|
||||
|
||||
protected static void showIt(final BufferedImage pImage, final String pTitle) {
|
||||
ImageReaderBase.showIt(pImage, pTitle);
|
||||
}
|
||||
@@ -1272,7 +1305,7 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
// param.setSourceSubsampling(sub, sub, 0, 0);
|
||||
// }
|
||||
|
||||
long start = System.currentTimeMillis();
|
||||
// long start = System.currentTimeMillis();
|
||||
BufferedImage image = reader.read(0, param);
|
||||
// System.err.println("Read time: " + (System.currentTimeMillis() - start) + " ms");
|
||||
// System.err.println("image: " + image);
|
||||
@@ -1280,12 +1313,12 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
|
||||
// image = new ResampleOp(reader.getWidth(0) / 4, reader.getHeight(0) / 4, ResampleOp.FILTER_LANCZOS).filter(image, null);
|
||||
|
||||
// int maxW = 1280;
|
||||
// int maxH = 800;
|
||||
int maxW = 400;
|
||||
int maxH = 400;
|
||||
int maxW = 1280;
|
||||
int maxH = 800;
|
||||
// int maxW = 400;
|
||||
// int maxH = 400;
|
||||
if (image.getWidth() > maxW || image.getHeight() > maxH) {
|
||||
start = System.currentTimeMillis();
|
||||
// start = System.currentTimeMillis();
|
||||
float aspect = reader.getAspectRatio(0);
|
||||
if (aspect >= 1f) {
|
||||
image = ImageUtil.createResampled(image, maxW, Math.round(maxW / aspect), Image.SCALE_DEFAULT);
|
||||
|
@@ -49,8 +49,9 @@ import static com.twelvemonkeys.lang.Validate.notNull;
|
||||
*/
|
||||
final class JPEGSegmentImageInputStream extends ImageInputStreamImpl {
|
||||
// TODO: Rewrite JPEGSegment (from metadata) to store stream pos/length, and be able to replay data, and use instead of Segment?
|
||||
// TODO: Change order of segments, to make sure APP0/JFIF is always before APP14/Adobe?
|
||||
// TODO: Change order of segments, to make sure APP0/JFIF is always before APP14/Adobe? What about EXIF?
|
||||
// TODO: Insert fake APP0/JFIF if needed by the reader?
|
||||
// TODO: Sort out ICC_PROFILE issues (duplicate sequence numbers etc)?
|
||||
|
||||
final private ImageInputStream stream;
|
||||
|
||||
@@ -90,6 +91,12 @@ final class JPEGSegmentImageInputStream extends ImageInputStreamImpl {
|
||||
long realPosition = stream.getStreamPosition();
|
||||
int marker = stream.readUnsignedShort();
|
||||
|
||||
// Skip over 0xff padding between markers
|
||||
while (marker == 0xffff) {
|
||||
realPosition++;
|
||||
marker = 0xff00 | stream.readUnsignedByte();
|
||||
}
|
||||
|
||||
// TODO: Refactor to make various segments optional, we probably only want the "Adobe" APP14 segment, 'Exif' APP1 and very few others
|
||||
if (isAppSegmentMarker(marker) && marker != JPEG.APP0 && !(marker == JPEG.APP1 && isAppSegmentWithId("Exif", stream)) && marker != JPEG.APP14) {
|
||||
int length = stream.readUnsignedShort(); // Length including length field itself
|
||||
@@ -149,7 +156,7 @@ final class JPEGSegmentImageInputStream extends ImageInputStreamImpl {
|
||||
return segment;
|
||||
}
|
||||
|
||||
private static boolean isAppSegmentWithId(String segmentId, ImageInputStream stream) throws IOException {
|
||||
private static boolean isAppSegmentWithId(final String segmentId, final ImageInputStream stream) throws IOException {
|
||||
notNull(segmentId, "segmentId");
|
||||
|
||||
stream.mark();
|
||||
@@ -222,7 +229,7 @@ final class JPEGSegmentImageInputStream extends ImageInputStreamImpl {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] b, int off, int len) throws IOException {
|
||||
public int read(final byte[] b, final int off, final int len) throws IOException {
|
||||
bitOffset = 0;
|
||||
|
||||
// NOTE: There is a bug in the JPEGMetadata constructor (JPEGBuffer.loadBuf() method) that expects read to
|
||||
@@ -264,7 +271,7 @@ final class JPEGSegmentImageInputStream extends ImageInputStreamImpl {
|
||||
final long start;
|
||||
final long length;
|
||||
|
||||
Segment(int marker, long realStart, long start, long length) {
|
||||
Segment(final int marker, final long realStart, final long start, final long length) {
|
||||
this.marker = marker;
|
||||
this.realStart = realStart;
|
||||
this.start = start;
|
||||
|
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright (c) 2013, Harald Kuhr
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
* * Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
* * Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* * Neither the name "TwelveMonkeys" nor the
|
||||
* names of its contributors may be used to endorse or promote products
|
||||
* derived from this software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
||||
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
package com.twelvemonkeys.imageio.plugins.jpeg;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* SOFComponent
|
||||
*
|
||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||
* @author last modified by $Author: haraldk$
|
||||
* @version $Id: SOFComponent.java,v 1.0 22.04.13 16:40 haraldk Exp$
|
||||
*/
|
||||
final class SOFComponent {
|
||||
final int id;
|
||||
final int hSub;
|
||||
final int vSub;
|
||||
final int qtSel;
|
||||
|
||||
SOFComponent(int id, int hSub, int vSub, int qtSel) {
|
||||
this.id = id;
|
||||
this.hSub = hSub;
|
||||
this.vSub = vSub;
|
||||
this.qtSel = qtSel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
// Use id either as component number or component name, based on value
|
||||
Serializable idStr = (id >= 'a' && id <= 'z' || id >= 'A' && id <= 'Z') ? "'" + (char) id + "'" : id;
|
||||
return String.format("id: %s, sub: %d/%d, sel: %d", idStr, hSub, vSub, qtSel);
|
||||
}
|
||||
}
|
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright (c) 2013, Harald Kuhr
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
* * Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
* * Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* * Neither the name "TwelveMonkeys" nor the
|
||||
* names of its contributors may be used to endorse or promote products
|
||||
* derived from this software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
||||
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
package com.twelvemonkeys.imageio.plugins.jpeg;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* SOFSegment
|
||||
*
|
||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||
* @author last modified by $Author: haraldk$
|
||||
* @version $Id: SOFSegment.java,v 1.0 22.04.13 16:40 haraldk Exp$
|
||||
*/
|
||||
final class SOFSegment {
|
||||
final int marker;
|
||||
final int samplePrecision;
|
||||
final int lines; // height
|
||||
final int samplesPerLine; // width
|
||||
final SOFComponent[] components;
|
||||
|
||||
SOFSegment(int marker, int samplePrecision, int lines, int samplesPerLine, SOFComponent[] components) {
|
||||
this.marker = marker;
|
||||
this.samplePrecision = samplePrecision;
|
||||
this.lines = lines;
|
||||
this.samplesPerLine = samplesPerLine;
|
||||
this.components = components;
|
||||
}
|
||||
|
||||
final int componentsInFrame() {
|
||||
return components.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format(
|
||||
"SOF%d[%04x, precision: %d, lines: %d, samples/line: %d, components: %s]",
|
||||
marker & 0xff - 0xc0, marker, samplePrecision, lines, samplesPerLine, Arrays.toString(components)
|
||||
);
|
||||
}
|
||||
}
|
@@ -31,10 +31,12 @@ package com.twelvemonkeys.imageio.plugins.jpeg;
|
||||
import com.twelvemonkeys.imageio.util.ImageReaderAbstractTestCase;
|
||||
import org.junit.Test;
|
||||
|
||||
import javax.imageio.IIOException;
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.imageio.ImageReadParam;
|
||||
import javax.imageio.ImageTypeSpecifier;
|
||||
import javax.imageio.event.IIOReadWarningListener;
|
||||
import javax.imageio.metadata.IIOMetadata;
|
||||
import javax.imageio.spi.IIORegistry;
|
||||
import javax.imageio.spi.ImageReaderSpi;
|
||||
import java.awt.*;
|
||||
@@ -75,7 +77,8 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTestCase<JPEGImageRe
|
||||
new TestData(getClassLoaderResource("/jpeg/gray-sample.jpg"), new Dimension(386, 396)),
|
||||
new TestData(getClassLoaderResource("/jpeg/cmyk-sample.jpg"), new Dimension(160, 227)),
|
||||
new TestData(getClassLoaderResource("/jpeg/cmyk-sample-multiple-chunk-icc.jpg"), new Dimension(2707, 3804)),
|
||||
new TestData(getClassLoaderResource("/jpeg/jfif-jfxx-thumbnail-olympus-d320l.jpg"), new Dimension(640, 480))
|
||||
new TestData(getClassLoaderResource("/jpeg/jfif-jfxx-thumbnail-olympus-d320l.jpg"), new Dimension(640, 480)),
|
||||
new TestData(getClassLoaderResource("/jpeg/jfif-padded-segments.jpg"), new Dimension(20, 45))
|
||||
);
|
||||
|
||||
// More test data in specific tests below
|
||||
@@ -255,7 +258,7 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTestCase<JPEGImageRe
|
||||
assertEquals(16, image.getHeight());
|
||||
|
||||
// TODO: Need to test colors!
|
||||
|
||||
|
||||
assertTrue(reader.hasThumbnails(0)); // Should not blow up!
|
||||
}
|
||||
|
||||
@@ -371,7 +374,7 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTestCase<JPEGImageRe
|
||||
// JFIF with JFXX JPEG encoded thumbnail
|
||||
JPEGImageReader reader = createReader();
|
||||
reader.setInput(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/jfif-jfxx-thumbnail-olympus-d320l.jpg")));
|
||||
|
||||
|
||||
assertTrue(reader.hasThumbnails(0));
|
||||
assertEquals(1, reader.getNumThumbnails(0));
|
||||
assertEquals(80, reader.getThumbnailWidth(0, 0));
|
||||
@@ -457,8 +460,8 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTestCase<JPEGImageRe
|
||||
for (int i = 0; i < strip.getWidth() / 128; i++) {
|
||||
int actualRGB = strip.getRGB(i * 128, 4);
|
||||
assertEquals((actualRGB >> 16) & 0xff, (expectedRGB[i] >> 16) & 0xff, 5);
|
||||
assertEquals((actualRGB >> 8) & 0xff, (expectedRGB[i] >> 8) & 0xff, 5);
|
||||
assertEquals((actualRGB) & 0xff, (expectedRGB[i]) & 0xff, 5);
|
||||
assertEquals((actualRGB >> 8) & 0xff, (expectedRGB[i] >> 8) & 0xff, 5);
|
||||
assertEquals((actualRGB) & 0xff, (expectedRGB[i]) & 0xff, 5);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -487,8 +490,8 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTestCase<JPEGImageRe
|
||||
for (int i = 0; i < thumbnail.getWidth() / 8; i++) {
|
||||
int actualRGB = thumbnail.getRGB(i * 8, 4);
|
||||
assertEquals((actualRGB >> 16) & 0xff, (expectedRGB[i] >> 16) & 0xff, 5);
|
||||
assertEquals((actualRGB >> 8) & 0xff, (expectedRGB[i] >> 8) & 0xff, 5);
|
||||
assertEquals((actualRGB) & 0xff, (expectedRGB[i]) & 0xff, 5);
|
||||
assertEquals((actualRGB >> 8) & 0xff, (expectedRGB[i] >> 8) & 0xff, 5);
|
||||
assertEquals((actualRGB) & 0xff, (expectedRGB[i]) & 0xff, 5);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -549,7 +552,6 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTestCase<JPEGImageRe
|
||||
new TestData(getClassLoaderResource("/jpeg/cmyk-sample-no-icc.jpg"), new Dimension(100, 100))
|
||||
);
|
||||
|
||||
|
||||
for (TestData data : cmykData) {
|
||||
reader.setInput(data.getInputStream());
|
||||
|
||||
@@ -599,4 +601,24 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTestCase<JPEGImageRe
|
||||
}
|
||||
|
||||
// TODO: Test RGBA/YCbCrA handling
|
||||
|
||||
@Test
|
||||
public void testReadMetadataMaybeNull() throws IOException {
|
||||
// Just test that we can read the metadata without exceptions
|
||||
JPEGImageReader reader = createReader();
|
||||
|
||||
for (TestData testData : getTestData()) {
|
||||
reader.setInput(testData.getInputStream());
|
||||
|
||||
for (int i = 0; i < reader.getNumImages(true); i++) {
|
||||
try {
|
||||
IIOMetadata metadata = reader.getImageMetadata(i);
|
||||
assertNotNull(String.format("Image metadata null for %s image %s", testData, i), metadata);
|
||||
}
|
||||
catch (IIOException e) {
|
||||
System.err.println(String.format("WARNING: Reading metadata failed for %s image %s: %s", testData, i, e.getMessage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -135,4 +135,27 @@ public class JPEGSegmentImageInputStreamTest {
|
||||
|
||||
assertEquals(9299l, length); // Sanity check: same as file size
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadPaddedSegmentsBug() throws IOException {
|
||||
ImageInputStream stream = new JPEGSegmentImageInputStream(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/jfif-padded-segments.jpg")));
|
||||
|
||||
List<JPEGSegment> appSegments = JPEGSegmentUtil.readSegments(stream, JPEGSegmentUtil.APP_SEGMENTS);
|
||||
assertEquals(2, appSegments.size());
|
||||
|
||||
assertEquals(JPEG.APP0, appSegments.get(0).marker());
|
||||
assertEquals("JFIF", appSegments.get(0).identifier());
|
||||
|
||||
assertEquals(JPEG.APP1, appSegments.get(1).marker());
|
||||
assertEquals("Exif", appSegments.get(1).identifier());
|
||||
|
||||
stream.seek(0l);
|
||||
|
||||
long length = 0;
|
||||
while (stream.read() != -1) {
|
||||
length++;
|
||||
}
|
||||
|
||||
assertEquals(1079L, length); // Sanity check: same as file size, except padding and the filtered ICC_PROFILE segment
|
||||
}
|
||||
}
|
||||
|
BIN
imageio/imageio-jpeg/src/test/resources/jpeg/jfif-padded-segments.jpg
Executable file
BIN
imageio/imageio-jpeg/src/test/resources/jpeg/jfif-padded-segments.jpg
Executable file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
25
imageio/imageio-metadata/license.txt
Normal file
25
imageio/imageio-metadata/license.txt
Normal file
@@ -0,0 +1,25 @@
|
||||
Copyright (c) 2012, Harald Kuhr
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
* Neither the name "TwelveMonkeys" nor the names of its contributors
|
||||
may be used to endorse or promote products derived from this software
|
||||
without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
@@ -35,6 +35,7 @@ package com.twelvemonkeys.imageio.metadata.exif;
|
||||
* @author last modified by $Author: haraldk$
|
||||
* @version $Id: EXIF.java,v 1.0 Nov 11, 2009 5:36:04 PM haraldk Exp$
|
||||
*/
|
||||
@SuppressWarnings("UnusedDeclaration")
|
||||
public interface EXIF {
|
||||
// See http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html
|
||||
int TAG_EXPOSURE_TIME = 33434;
|
||||
|
@@ -88,7 +88,7 @@ final class EXIFEntry extends AbstractEntry {
|
||||
return "StripOffsets";
|
||||
case TIFF.TAG_ORIENTATION:
|
||||
return "Orientation";
|
||||
case TIFF.TAG_SAMPLES_PER_PIXELS:
|
||||
case TIFF.TAG_SAMPLES_PER_PIXEL:
|
||||
return "SamplesPerPixels";
|
||||
case TIFF.TAG_ROWS_PER_STRIP:
|
||||
return "RowsPerStrip";
|
||||
@@ -209,6 +209,28 @@ final class EXIFEntry extends AbstractEntry {
|
||||
return "PixelYDimension";
|
||||
|
||||
// TODO: More field names
|
||||
/*
|
||||
default:
|
||||
Class[] classes = new Class[] {TIFF.class, EXIF.class};
|
||||
|
||||
for (Class cl : classes) {
|
||||
Field[] fields = cl.getFields();
|
||||
|
||||
for (Field field : fields) {
|
||||
try {
|
||||
if (field.getType() == Integer.TYPE && field.getName().startsWith("TAG_")) {
|
||||
if (field.get(null).equals(getIdentifier())) {
|
||||
return StringUtil.lispToCamel(field.getName().substring(4).replace("_", "-").toLowerCase(), true);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (IllegalAccessException e) {
|
||||
// Should never happen, but in case, abort
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
return null;
|
||||
|
@@ -35,6 +35,7 @@ package com.twelvemonkeys.imageio.metadata.exif;
|
||||
* @author last modified by $Author: haraldk$
|
||||
* @version $Id: TIFF.java,v 1.0 Nov 15, 2009 3:02:24 PM haraldk Exp$
|
||||
*/
|
||||
@SuppressWarnings("UnusedDeclaration")
|
||||
public interface TIFF {
|
||||
int TIFF_MAGIC = 42;
|
||||
|
||||
@@ -98,8 +99,9 @@ public interface TIFF {
|
||||
int TAG_BITS_PER_SAMPLE = 258;
|
||||
int TAG_COMPRESSION = 259;
|
||||
int TAG_PHOTOMETRIC_INTERPRETATION = 262;
|
||||
int TAG_FILL_ORDER = 266;
|
||||
int TAG_ORIENTATION = 274;
|
||||
int TAG_SAMPLES_PER_PIXELS = 277;
|
||||
int TAG_SAMPLES_PER_PIXEL = 277;
|
||||
int TAG_PLANAR_CONFIGURATION = 284;
|
||||
int TAG_SAMPLE_FORMAT = 339;
|
||||
int TAG_YCBCR_SUB_SAMPLING = 530;
|
||||
@@ -113,6 +115,7 @@ public interface TIFF {
|
||||
int TAG_STRIP_OFFSETS = 273;
|
||||
int TAG_ROWS_PER_STRIP = 278;
|
||||
int TAG_STRIP_BYTE_COUNTS = 279;
|
||||
// "Old-style" JPEG (still used as EXIF thumbnail)
|
||||
int TAG_JPEG_INTERCHANGE_FORMAT = 513;
|
||||
int TAG_JPEG_INTERCHANGE_FORMAT_LENGTH = 514;
|
||||
|
||||
@@ -124,6 +127,7 @@ public interface TIFF {
|
||||
int TAG_PRIMARY_CHROMATICITIES = 319;
|
||||
int TAG_COLOR_MAP = 320;
|
||||
int TAG_EXTRA_SAMPLES = 338;
|
||||
int TAG_TRANSFER_RANGE = 342;
|
||||
int TAG_YCBCR_COEFFICIENTS = 529;
|
||||
int TAG_REFERENCE_BLACK_WHITE = 532;
|
||||
|
||||
@@ -133,6 +137,7 @@ public interface TIFF {
|
||||
int TAG_IMAGE_DESCRIPTION = 270;
|
||||
int TAG_MAKE = 271;
|
||||
int TAG_MODEL = 272;
|
||||
int TAG_PAGE_NUMBER = 297;
|
||||
int TAG_SOFTWARE = 305;
|
||||
int TAG_ARTIST = 315;
|
||||
int TAG_HOST_COMPUTER = 316;
|
||||
@@ -161,5 +166,12 @@ public interface TIFF {
|
||||
int TAG_TILE_OFFSETS = 324;
|
||||
int TAG_TILE_BYTE_COUNTS = 325;
|
||||
|
||||
// JPEG
|
||||
int TAG_JPEG_TABLES = 347;
|
||||
|
||||
// "Old-style" JPEG (Obsolete) DO NOT WRITE!
|
||||
int TAG_OLD_JPEG_PROC = 512;
|
||||
int TAG_OLD_JPEG_Q_TABLES = 519;
|
||||
int TAG_OLD_JPEG_DC_TABLES = 520;
|
||||
int TAG_OLD_JPEG_AC_TABLES = 521;
|
||||
}
|
||||
|
@@ -40,7 +40,8 @@ public interface JPEG {
|
||||
int SOI = 0xFFD8;
|
||||
/** End of Image segment marker (EOI). */
|
||||
int EOI = 0xFFD9;
|
||||
/** Start of Stream segment marker (SOS). */
|
||||
|
||||
/** Start of Scan segment marker (SOS). */
|
||||
int SOS = 0xFFDA;
|
||||
|
||||
/** Define Quantization Tables segment marker (DQT). */
|
||||
@@ -81,6 +82,10 @@ public interface JPEG {
|
||||
int SOF14 = 0xFFCE;
|
||||
int SOF15 = 0xFFCF;
|
||||
|
||||
// JPEG-LS markers
|
||||
int SOF55 = 0xFFF7; // NOTE: Equal to a normal SOF segment
|
||||
int LSE = 0xFFF8; // JPEG-LS Preset Parameter marker
|
||||
|
||||
// TODO: Known/Important APPn marker identifiers
|
||||
// "JFIF" APP0
|
||||
// "JFXX" APP0
|
||||
@@ -89,6 +94,6 @@ public interface JPEG {
|
||||
// "Adobe" APP14
|
||||
|
||||
// Possibly
|
||||
// "http://ns.adobe.com/xap/1.0/" (XMP)
|
||||
// "Photoshop 3.0" (Contains IPTC)
|
||||
// "http://ns.adobe.com/xap/1.0/" (XMP) APP1
|
||||
// "Photoshop 3.0" (may contain IPTC) APP13
|
||||
}
|
||||
|
@@ -90,10 +90,7 @@ public final class JPEGQuality {
|
||||
private static int getJPEGQuality(final int[][] quantizationTables) throws IOException {
|
||||
// System.err.println("tables: " + Arrays.deepToString(tables));
|
||||
|
||||
// TODO: Determine lossless JPEG
|
||||
// if (lossless) {
|
||||
// return 100; // TODO: Sums can be 100... Is lossless not 100?
|
||||
// }
|
||||
// TODO: Determine lossless JPEG, it's an entirely different algorithm
|
||||
|
||||
int qvalue;
|
||||
|
||||
|
@@ -95,7 +95,8 @@ public final class JPEGSegmentUtil {
|
||||
|
||||
JPEGSegment segment;
|
||||
try {
|
||||
while (!isImageDone(segment = readSegment(stream, segmentIdentifiers))) {
|
||||
do {
|
||||
segment = readSegment(stream, segmentIdentifiers);
|
||||
// System.err.println("segment: " + segment);
|
||||
|
||||
if (isRequested(segment, segmentIdentifiers)) {
|
||||
@@ -106,6 +107,7 @@ public final class JPEGSegmentUtil {
|
||||
segments.add(segment);
|
||||
}
|
||||
}
|
||||
while (!isImageDone(segment));
|
||||
}
|
||||
catch (EOFException ignore) {
|
||||
// Just end here, in case of malformed stream
|
||||
@@ -151,8 +153,18 @@ public final class JPEGSegmentUtil {
|
||||
}
|
||||
}
|
||||
|
||||
static JPEGSegment readSegment(final ImageInputStream stream, Map<Integer, List<String>> segmentIdentifiers) throws IOException {
|
||||
static JPEGSegment readSegment(final ImageInputStream stream, final Map<Integer, List<String>> segmentIdentifiers) throws IOException {
|
||||
int marker = stream.readUnsignedShort();
|
||||
|
||||
// Skip over 0xff padding between markers
|
||||
while (marker == 0xffff) {
|
||||
marker = 0xff00 | stream.readUnsignedByte();
|
||||
}
|
||||
|
||||
if ((marker >> 8 & 0xff) != 0xff) {
|
||||
throw new IIOException(String.format("Bad marker: %04x", marker));
|
||||
}
|
||||
|
||||
int length = stream.readUnsignedShort(); // Length including length field itself
|
||||
|
||||
byte[] data;
|
||||
@@ -191,7 +203,7 @@ public final class JPEGSegmentUtil {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean contains(Object o) {
|
||||
public boolean contains(final Object o) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -203,13 +215,13 @@ public final class JPEGSegmentUtil {
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> get(Object key) {
|
||||
public List<String> get(final Object key) {
|
||||
return key instanceof Integer && JPEGSegment.isAppSegmentMarker((Integer) key) ? ALL_IDS : null;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsKey(Object key) {
|
||||
public boolean containsKey(final Object key) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -221,7 +233,7 @@ public final class JPEGSegmentUtil {
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> get(Object key) {
|
||||
public List<String> get(final Object key) {
|
||||
return containsKey(key) ? ALL_IDS : null;
|
||||
|
||||
}
|
||||
|
@@ -151,19 +151,21 @@ public class JPEGSegmentUtilTest {
|
||||
@Test
|
||||
public void testReadAll() throws IOException {
|
||||
List<JPEGSegment> segments = JPEGSegmentUtil.readSegments(getData("/jpeg/9788245605525.jpg"), JPEGSegmentUtil.ALL_SEGMENTS);
|
||||
assertEquals(6, segments.size());
|
||||
assertEquals(7, segments.size());
|
||||
|
||||
assertEquals(segments.toString(), JPEG.SOF0, segments.get(3).marker());
|
||||
assertEquals(segments.toString(), null, segments.get(3).identifier());
|
||||
assertEquals(segments.toString(), JPEG.SOS, segments.get(segments.size() - 1).marker());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadAllAlt() throws IOException {
|
||||
List<JPEGSegment> segments = JPEGSegmentUtil.readSegments(getData("/jpeg/ts_open_300dpi.jpg"), JPEGSegmentUtil.ALL_SEGMENTS);
|
||||
assertEquals(26, segments.size());
|
||||
assertEquals(27, segments.size());
|
||||
|
||||
assertEquals(segments.toString(), JPEG.SOF0, segments.get(23).marker());
|
||||
assertEquals(segments.toString(), null, segments.get(23).identifier());
|
||||
assertEquals(segments.toString(), JPEG.SOS, segments.get(segments.size() - 1).marker());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -194,4 +196,17 @@ public class JPEGSegmentUtilTest {
|
||||
assertEquals(JPEG.APP14, segments.get(21).marker());
|
||||
assertEquals("Adobe", segments.get(21).identifier());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadPaddedSegments() throws IOException {
|
||||
List<JPEGSegment> segments = JPEGSegmentUtil.readSegments(getData("/jpeg/jfif-padded-segments.jpg"), JPEGSegmentUtil.APP_SEGMENTS);
|
||||
assertEquals(3, segments.size());
|
||||
|
||||
assertEquals(JPEG.APP0, segments.get(0).marker());
|
||||
assertEquals("JFIF", segments.get(0).identifier());
|
||||
assertEquals(JPEG.APP2, segments.get(1).marker());
|
||||
assertEquals("ICC_PROFILE", segments.get(1).identifier());
|
||||
assertEquals(JPEG.APP1, segments.get(2).marker());
|
||||
assertEquals("Exif", segments.get(2).identifier());
|
||||
}
|
||||
}
|
||||
|
BIN
imageio/imageio-metadata/src/test/resources/jpeg/jfif-padded-segments.jpg
Executable file
BIN
imageio/imageio-metadata/src/test/resources/jpeg/jfif-padded-segments.jpg
Executable file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
25
imageio/imageio-tiff/license.txt
Normal file
25
imageio/imageio-tiff/license.txt
Normal file
@@ -0,0 +1,25 @@
|
||||
Copyright (c) 2013, Harald Kuhr
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
* Neither the name "TwelveMonkeys" nor the names of its contributors
|
||||
may be used to endorse or promote products derived from this software
|
||||
without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
@@ -0,0 +1,452 @@
|
||||
/*
|
||||
* Copyright (c) 2012, Harald Kuhr
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
* * Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
* * Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* * Neither the name "TwelveMonkeys" nor the
|
||||
* names of its contributors may be used to endorse or promote products
|
||||
* derived from this software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
||||
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
package com.twelvemonkeys.imageio.plugins.tiff;
|
||||
|
||||
import com.twelvemonkeys.lang.Validate;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.io.FilterInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* CCITT Modified Huffman RLE, Group 3 (T4) and Group 4 (T6) fax compression.
|
||||
*
|
||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||
* @author last modified by $Author: haraldk$
|
||||
* @version $Id: CCITTFaxDecoderStream.java,v 1.0 23.05.12 15:55 haraldk Exp$
|
||||
*/
|
||||
final class CCITTFaxDecoderStream extends FilterInputStream {
|
||||
// See TIFF 6.0 Specification, Section 10: "Modified Huffman Compression", page 43.
|
||||
|
||||
private final int columns;
|
||||
private final byte[] decodedRow;
|
||||
|
||||
private int decodedLength;
|
||||
private int decodedPos;
|
||||
|
||||
private int bitBuffer;
|
||||
private int bitBufferLength;
|
||||
|
||||
// Need to take fill order into account (?) (use flip table?)
|
||||
private final int fillOrder;
|
||||
private final int type;
|
||||
|
||||
private final int[] changes;
|
||||
private int changesCount;
|
||||
|
||||
private static final int EOL_CODE = 0x01; // 12 bit
|
||||
|
||||
public CCITTFaxDecoderStream(final InputStream stream, final int columns, final int type, final int fillOrder) {
|
||||
super(Validate.notNull(stream, "stream"));
|
||||
|
||||
this.columns = Validate.isTrue(columns > 0, columns, "width must be greater than 0");
|
||||
// We know this is only used for b/w (1 bit)
|
||||
this.decodedRow = new byte[(columns + 7) / 8];
|
||||
this.type = Validate.isTrue(type == TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE, type, "Only CCITT Modified Huffman RLE compression (2) supported: %s"); // TODO: Implement group 3 and 4
|
||||
this.fillOrder = Validate.isTrue(fillOrder == 1, fillOrder, "Only fill order 1 supported: %s"); // TODO: Implement fillOrder == 2
|
||||
|
||||
this.changes = new int[columns];
|
||||
}
|
||||
|
||||
// IDEA: Would it be faster to keep all bit combos of each length (>=2) that is NOT a code, to find bit length, then look up value in table?
|
||||
// -- If white run, start at 4 bits to determine length, if black, start at 2 bits
|
||||
|
||||
private void fetch() throws IOException {
|
||||
if (decodedPos >= decodedLength) {
|
||||
decodedLength = 0;
|
||||
try {
|
||||
decodeRow();
|
||||
}
|
||||
catch (EOFException e) {
|
||||
// TODO: Rewrite to avoid throw/catch for normal flow...
|
||||
if (decodedLength != 0) {
|
||||
throw e;
|
||||
}
|
||||
|
||||
// ..otherwise, just client code trying to read past the end of stream
|
||||
decodedLength = -1;
|
||||
}
|
||||
|
||||
decodedPos = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private void decodeRow() throws IOException {
|
||||
resetBuffer();
|
||||
|
||||
boolean literalRun = true;
|
||||
|
||||
/*
|
||||
if (type == TIFFExtension.COMPRESSION_CCITT_T4) {
|
||||
int eol = readBits(12);
|
||||
System.err.println("eol: " + eol);
|
||||
while (eol != EOL_CODE) {
|
||||
eol = readBits(1);
|
||||
System.err.println("eol: " + eol);
|
||||
// throw new IOException("Missing EOL");
|
||||
}
|
||||
|
||||
literalRun = readBits(1) == 1;
|
||||
}
|
||||
|
||||
System.err.println("literalRun: " + literalRun);
|
||||
*/
|
||||
int index = 0;
|
||||
|
||||
if (literalRun) {
|
||||
changesCount = 0;
|
||||
boolean white = true;
|
||||
|
||||
do {
|
||||
int completeRun = 0;
|
||||
|
||||
int run;
|
||||
do {
|
||||
if (white) {
|
||||
run = decodeRun(WHITE_CODES, WHITE_RUN_LENGTHS, 4);
|
||||
}
|
||||
else {
|
||||
run = decodeRun(BLACK_CODES, BLACK_RUN_LENGTHS, 2);
|
||||
}
|
||||
|
||||
completeRun += run;
|
||||
}
|
||||
while (run >= 64); // Additional makeup codes are packed into both b/w codes, terminating codes are < 64 bytes
|
||||
|
||||
changes[changesCount++] = index + completeRun;
|
||||
|
||||
// System.err.printf("%s run: %d\n", white ? "white" : "black", run);
|
||||
|
||||
// TODO: Optimize with lookup for 0-7 bits?
|
||||
// Fill bits to byte boundary...
|
||||
while (index % 8 != 0 && completeRun-- > 0) {
|
||||
decodedRow[index++ / 8] |= (white ? 1 << 8 - (index % 8) : 0);
|
||||
}
|
||||
|
||||
// ...then fill complete bytes to either 0xff or 0x00...
|
||||
if (index % 8 == 0) {
|
||||
final byte value = (byte) (white ? 0xff : 0x00);
|
||||
|
||||
while (completeRun > 7) {
|
||||
decodedRow[index / 8] = value;
|
||||
completeRun -= 8;
|
||||
index += 8;
|
||||
}
|
||||
}
|
||||
|
||||
// ...finally fill any remaining bits
|
||||
while (completeRun-- > 0) {
|
||||
decodedRow[index++ / 8] |= (white ? 1 << 8 - (index % 8) : 0);
|
||||
}
|
||||
|
||||
// Flip color for next run
|
||||
white = !white;
|
||||
}
|
||||
while (index < columns);
|
||||
}
|
||||
else {
|
||||
// non-literal run
|
||||
}
|
||||
|
||||
if (type == TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE && index != columns) {
|
||||
throw new IOException("Sum of run-lengths does not equal scan line width: " + index + " > " + columns);
|
||||
}
|
||||
|
||||
decodedLength = (index / 8) + 1;
|
||||
}
|
||||
|
||||
private int decodeRun(short[][] codes, short[][] runLengths, int minCodeSize) throws IOException {
|
||||
// TODO: Optimize...
|
||||
// Looping and comparing is the most straight-forward, but probably not the most effective way...
|
||||
int code = readBits(minCodeSize);
|
||||
|
||||
for (int bits = 0; bits < codes.length; bits++) {
|
||||
short[] bitCodes = codes[bits];
|
||||
|
||||
for (int i = 0; i < bitCodes.length; i++) {
|
||||
if (bitCodes[i] == code) {
|
||||
// System.err.println("code: " + code);
|
||||
|
||||
// Code found, return matching run length
|
||||
return runLengths[bits][i];
|
||||
}
|
||||
}
|
||||
|
||||
// No code found, read one more bit and try again
|
||||
code = fillOrder == 1 ? (code << 1) | readBits(1) : readBits(1) << (bits + minCodeSize) | code;
|
||||
}
|
||||
|
||||
throw new IOException("Unknown code in Huffman RLE stream");
|
||||
}
|
||||
|
||||
private void resetBuffer() {
|
||||
for (int i = 0; i < decodedRow.length; i++) {
|
||||
decodedRow[i] = 0;
|
||||
}
|
||||
|
||||
bitBuffer = 0;
|
||||
bitBufferLength = 0;
|
||||
}
|
||||
|
||||
private int readBits(int bitCount) throws IOException {
|
||||
while (bitBufferLength < bitCount) {
|
||||
int read = in.read();
|
||||
if (read == -1) {
|
||||
throw new EOFException("Unexpected end of Huffman RLE stream");
|
||||
}
|
||||
|
||||
int bits = read & 0xff;
|
||||
bitBuffer = (bitBuffer << 8) | bits;
|
||||
bitBufferLength += 8;
|
||||
}
|
||||
|
||||
// TODO: Take fill order into account
|
||||
bitBufferLength -= bitCount;
|
||||
int result = bitBuffer >> bitBufferLength;
|
||||
bitBuffer &= (1 << bitBufferLength) - 1;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
if (decodedLength < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (decodedPos >= decodedLength) {
|
||||
fetch();
|
||||
|
||||
if (decodedLength < 0) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
return decodedRow[decodedPos++] & 0xff;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] b, int off, int len) throws IOException {
|
||||
if (decodedLength < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (decodedPos >= decodedLength) {
|
||||
fetch();
|
||||
|
||||
if (decodedLength < 0) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
int read = Math.min(decodedLength - decodedPos, len);
|
||||
System.arraycopy(decodedRow, decodedPos, b, off, read);
|
||||
decodedPos += read;
|
||||
|
||||
return read;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long skip(long n) throws IOException {
|
||||
if (decodedLength < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (decodedPos >= decodedLength) {
|
||||
fetch();
|
||||
|
||||
if (decodedLength < 0) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
int skipped = (int) Math.min(decodedLength - decodedPos, n);
|
||||
decodedPos += skipped;
|
||||
|
||||
return skipped;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean markSupported() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void reset() throws IOException {
|
||||
throw new IOException("mark/reset not supported");
|
||||
}
|
||||
|
||||
static final short[][] BLACK_CODES = {
|
||||
{ // 2 bits
|
||||
0x2, 0x3,
|
||||
},
|
||||
{ // 3 bits
|
||||
0x2, 0x3,
|
||||
},
|
||||
{ // 4 bits
|
||||
0x2, 0x3,
|
||||
},
|
||||
{ // 5 bits
|
||||
0x3,
|
||||
},
|
||||
{ // 6 bits
|
||||
0x4, 0x5,
|
||||
},
|
||||
{ // 7 bits
|
||||
0x4, 0x5, 0x7,
|
||||
},
|
||||
{ // 8 bits
|
||||
0x4, 0x7,
|
||||
},
|
||||
{ // 9 bits
|
||||
0x18,
|
||||
},
|
||||
{ // 10 bits
|
||||
0x17, 0x18, 0x37, 0x8, 0xf,
|
||||
},
|
||||
{ // 11 bits
|
||||
0x17, 0x18, 0x28, 0x37, 0x67, 0x68, 0x6c, 0x8, 0xc, 0xd,
|
||||
},
|
||||
{ // 12 bits
|
||||
0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x1c, 0x1d, 0x1e, 0x1f, 0x24, 0x27, 0x28, 0x2b, 0x2c, 0x33,
|
||||
0x34, 0x35, 0x37, 0x38, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x5b, 0x64, 0x65,
|
||||
0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0xc8, 0xc9, 0xca, 0xcb, 0xcc, 0xcd, 0xd2, 0xd3,
|
||||
0xd4, 0xd5, 0xd6, 0xd7, 0xda, 0xdb,
|
||||
},
|
||||
{ // 13 bits
|
||||
0x4a, 0x4b, 0x4c, 0x4d, 0x52, 0x53, 0x54, 0x55, 0x5a, 0x5b, 0x64, 0x65, 0x6c, 0x6d, 0x72, 0x73,
|
||||
0x74, 0x75, 0x76, 0x77,
|
||||
}
|
||||
};
|
||||
static final short[][] BLACK_RUN_LENGTHS = {
|
||||
{ // 2 bits
|
||||
3, 2,
|
||||
},
|
||||
{ // 3 bits
|
||||
1, 4,
|
||||
},
|
||||
{ // 4 bits
|
||||
6, 5,
|
||||
},
|
||||
{ // 5 bits
|
||||
7,
|
||||
},
|
||||
{ // 6 bits
|
||||
9, 8,
|
||||
},
|
||||
{ // 7 bits
|
||||
10, 11, 12,
|
||||
},
|
||||
{ // 8 bits
|
||||
13, 14,
|
||||
},
|
||||
{ // 9 bits
|
||||
15,
|
||||
},
|
||||
{ // 10 bits
|
||||
16, 17, 0, 18, 64,
|
||||
},
|
||||
{ // 11 bits
|
||||
24, 25, 23, 22, 19, 20, 21, 1792, 1856, 1920,
|
||||
},
|
||||
{ // 12 bits
|
||||
1984, 2048, 2112, 2176, 2240, 2304, 2368, 2432, 2496, 2560, 52, 55, 56, 59, 60, 320,
|
||||
384, 448, 53, 54, 50, 51, 44, 45, 46, 47, 57, 58, 61, 256, 48, 49,
|
||||
62, 63, 30, 31, 32, 33, 40, 41, 128, 192, 26, 27, 28, 29, 34, 35,
|
||||
36, 37, 38, 39, 42, 43,
|
||||
},
|
||||
{ // 13 bits
|
||||
640, 704, 768, 832, 1280, 1344, 1408, 1472, 1536, 1600, 1664, 1728, 512, 576, 896, 960,
|
||||
1024, 1088, 1152, 1216,
|
||||
}
|
||||
};
|
||||
|
||||
public static final short[][] WHITE_CODES = {
|
||||
{ // 4 bits
|
||||
0x7, 0x8, 0xb, 0xc, 0xe, 0xf,
|
||||
},
|
||||
{ // 5 bits
|
||||
0x12, 0x13, 0x14, 0x1b, 0x7, 0x8,
|
||||
},
|
||||
{ // 6 bits
|
||||
0x17, 0x18, 0x2a, 0x2b, 0x3, 0x34, 0x35, 0x7, 0x8,
|
||||
},
|
||||
{ // 7 bits
|
||||
0x13, 0x17, 0x18, 0x24, 0x27, 0x28, 0x2b, 0x3, 0x37, 0x4, 0x8, 0xc,
|
||||
},
|
||||
{ // 8 bits
|
||||
0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x1a, 0x1b, 0x2, 0x24, 0x25, 0x28, 0x29, 0x2a, 0x2b, 0x2c,
|
||||
0x2d, 0x3, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x4, 0x4a, 0x4b, 0x5, 0x52, 0x53, 0x54, 0x55,
|
||||
0x58, 0x59, 0x5a, 0x5b, 0x64, 0x65, 0x67, 0x68, 0xa, 0xb,
|
||||
},
|
||||
{ // 9 bits
|
||||
0x98, 0x99, 0x9a, 0x9b, 0xcc, 0xcd, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xdb,
|
||||
},
|
||||
{ // 10 bits
|
||||
},
|
||||
{ // 11 bits
|
||||
0x8, 0xc, 0xd,
|
||||
},
|
||||
{ // 12 bits
|
||||
0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x1c, 0x1d, 0x1e, 0x1f,
|
||||
}
|
||||
};
|
||||
|
||||
public static final short[][] WHITE_RUN_LENGTHS = {
|
||||
{ // 4 bits
|
||||
2, 3, 4, 5, 6, 7,
|
||||
},
|
||||
{ // 5 bits
|
||||
128, 8, 9, 64, 10, 11,
|
||||
},
|
||||
{ // 6 bits
|
||||
192, 1664, 16, 17, 13, 14, 15, 1, 12,
|
||||
},
|
||||
{ // 7 bits
|
||||
26, 21, 28, 27, 18, 24, 25, 22, 256, 23, 20, 19,
|
||||
},
|
||||
{ // 8 bits
|
||||
33, 34, 35, 36, 37, 38, 31, 32, 29, 53, 54, 39, 40, 41, 42, 43,
|
||||
44, 30, 61, 62, 63, 0, 320, 384, 45, 59, 60, 46, 49, 50, 51,
|
||||
52, 55, 56, 57, 58, 448, 512, 640, 576, 47, 48,
|
||||
},
|
||||
{ // 9 bits
|
||||
1472, 1536, 1600, 1728, 704, 768, 832, 896, 960, 1024, 1088, 1152, 1216, 1280, 1344, 1408,
|
||||
},
|
||||
{ // 10 bits
|
||||
},
|
||||
{ // 11 bits
|
||||
1792, 1856, 1920,
|
||||
},
|
||||
{ // 12 bits
|
||||
1984, 2048, 2112, 2176, 2240, 2304, 2368, 2432, 2496, 2560,
|
||||
}
|
||||
};
|
||||
}
|
@@ -0,0 +1,340 @@
|
||||
/*
|
||||
* Copyright (c) 2013, Harald Kuhr
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
* * Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
* * Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* * Neither the name "TwelveMonkeys" nor the
|
||||
* names of its contributors may be used to endorse or promote products
|
||||
* derived from this software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
||||
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
package com.twelvemonkeys.imageio.plugins.tiff;
|
||||
|
||||
import com.twelvemonkeys.lang.Validate;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.io.FilterInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
/**
|
||||
* A decoder for data converted using "horizontal differencing predictor".
|
||||
*
|
||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||
* @author last modified by $Author: haraldk$
|
||||
* @version $Id: HorizontalDeDifferencingStream.java,v 1.0 11.03.13 14:20 haraldk Exp$
|
||||
*/
|
||||
final class HorizontalDeDifferencingStream extends FilterInputStream {
|
||||
// See TIFF 6.0 Specification, Section 14: "Differencing Predictor", page 64.
|
||||
|
||||
private final int columns;
|
||||
// NOTE: PlanarConfiguration == 2 may be treated as samplesPerPixel == 1
|
||||
private final int samplesPerPixel;
|
||||
private final int bitsPerSample;
|
||||
private final ByteOrder byteOrder;
|
||||
|
||||
int decodedLength;
|
||||
int decodedPos;
|
||||
|
||||
private final byte[] buffer;
|
||||
|
||||
public HorizontalDeDifferencingStream(final InputStream stream, final int columns, final int samplesPerPixel, final int bitsPerSample, final ByteOrder byteOrder) {
|
||||
super(Validate.notNull(stream, "stream"));
|
||||
|
||||
this.columns = Validate.isTrue(columns > 0, columns, "width must be greater than 0");
|
||||
this.samplesPerPixel = Validate.isTrue(bitsPerSample >= 8 || samplesPerPixel == 1, samplesPerPixel, "Unsupported samples per pixel for < 8 bit samples: %s");
|
||||
this.bitsPerSample = Validate.isTrue(isValidBPS(bitsPerSample), bitsPerSample, "Unsupported bits per sample value: %s");
|
||||
this.byteOrder = byteOrder;
|
||||
|
||||
buffer = new byte[(columns * samplesPerPixel * bitsPerSample + 7) / 8];
|
||||
}
|
||||
|
||||
private boolean isValidBPS(final int bitsPerSample) {
|
||||
switch (bitsPerSample) {
|
||||
case 1:
|
||||
case 2:
|
||||
case 4:
|
||||
case 8:
|
||||
case 16:
|
||||
case 32:
|
||||
case 64:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void fetch() throws IOException {
|
||||
int pos = 0;
|
||||
int read;
|
||||
|
||||
// This *SHOULD* read an entire row of pixels (or nothing at all) into the buffer, otherwise we will throw EOFException below
|
||||
while (pos < buffer.length && (read = in.read(buffer, pos, buffer.length - pos)) > 0) {
|
||||
pos += read;
|
||||
}
|
||||
|
||||
if (pos > 0) {
|
||||
if (buffer.length > pos) {
|
||||
throw new EOFException("Unexpected end of stream");
|
||||
}
|
||||
|
||||
decodeRow();
|
||||
|
||||
decodedLength = buffer.length;
|
||||
decodedPos = 0;
|
||||
}
|
||||
else {
|
||||
decodedLength = -1;
|
||||
}
|
||||
}
|
||||
|
||||
private void decodeRow() throws EOFException {
|
||||
// Un-apply horizontal predictor
|
||||
int sample = 0;
|
||||
byte temp;
|
||||
|
||||
switch (bitsPerSample) {
|
||||
case 1:
|
||||
for (int b = 0; b < (columns + 7) / 8; b++) {
|
||||
sample += (buffer[b] >> 7) & 0x1;
|
||||
temp = (byte) ((sample << 7) & 0x80);
|
||||
sample += (buffer[b] >> 6) & 0x1;
|
||||
temp |= (byte) ((sample << 6) & 0x40);
|
||||
sample += (buffer[b] >> 5) & 0x1;
|
||||
temp |= (byte) ((sample << 5) & 0x20);
|
||||
sample += (buffer[b] >> 4) & 0x1;
|
||||
temp |= (byte) ((sample << 4) & 0x10);
|
||||
sample += (buffer[b] >> 3) & 0x1;
|
||||
temp |= (byte) ((sample << 3) & 0x08);
|
||||
sample += (buffer[b] >> 2) & 0x1;
|
||||
temp |= (byte) ((sample << 2) & 0x04);
|
||||
sample += (buffer[b] >> 1) & 0x1;
|
||||
temp |= (byte) ((sample << 1) & 0x02);
|
||||
sample += buffer[b] & 0x1;
|
||||
buffer[b] = (byte) (temp | sample & 0x1);
|
||||
}
|
||||
break;
|
||||
case 2:
|
||||
for (int b = 0; b < (columns + 3) / 4; b++) {
|
||||
sample += (buffer[b] >> 6) & 0x3;
|
||||
temp = (byte) ((sample << 6) & 0xc0);
|
||||
sample += (buffer[b] >> 4) & 0x3;
|
||||
temp |= (byte) ((sample << 4) & 0x30);
|
||||
sample += (buffer[b] >> 2) & 0x3;
|
||||
temp |= (byte) ((sample << 2) & 0x0c);
|
||||
sample += buffer[b] & 0x3;
|
||||
buffer[b] = (byte) (temp | sample & 0x3);
|
||||
}
|
||||
break;
|
||||
|
||||
case 4:
|
||||
for (int b = 0; b < (columns + 1) / 2; b++) {
|
||||
sample += (buffer[b] >> 4) & 0xf;
|
||||
temp = (byte) ((sample << 4) & 0xf0);
|
||||
sample += buffer[b] & 0x0f;
|
||||
buffer[b] = (byte) (temp | sample & 0xf);
|
||||
}
|
||||
break;
|
||||
|
||||
case 8:
|
||||
for (int x = 1; x < columns; x++) {
|
||||
for (int b = 0; b < samplesPerPixel; b++) {
|
||||
int off = x * samplesPerPixel + b;
|
||||
buffer[off] = (byte) (buffer[off - samplesPerPixel] + buffer[off]);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 16:
|
||||
for (int x = 1; x < columns; x++) {
|
||||
for (int b = 0; b < samplesPerPixel; b++) {
|
||||
int off = x * samplesPerPixel + b;
|
||||
putShort(off, asShort(off - samplesPerPixel) + asShort(off));
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 32:
|
||||
for (int x = 1; x < columns; x++) {
|
||||
for (int b = 0; b < samplesPerPixel; b++) {
|
||||
int off = x * samplesPerPixel + b;
|
||||
putInt(off, asInt(off - samplesPerPixel) + asInt(off));
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 64:
|
||||
for (int x = 1; x < columns; x++) {
|
||||
for (int b = 0; b < samplesPerPixel; b++) {
|
||||
int off = x * samplesPerPixel + b;
|
||||
putLong(off, asLong(off - samplesPerPixel) + asLong(off));
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new AssertionError(String.format("Unsupported bits per sample value: %d", bitsPerSample));
|
||||
}
|
||||
}
|
||||
|
||||
private void putLong(final int index, final long value) {
|
||||
if (byteOrder == ByteOrder.BIG_ENDIAN) {
|
||||
buffer[index * 8 ] = (byte) ((value >> 56) & 0xff);
|
||||
buffer[index * 8 + 1] = (byte) ((value >> 48) & 0xff);
|
||||
buffer[index * 8 + 2] = (byte) ((value >> 40) & 0xff);
|
||||
buffer[index * 8 + 3] = (byte) ((value >> 32) & 0xff);
|
||||
buffer[index * 8 + 4] = (byte) ((value >> 24) & 0xff);
|
||||
buffer[index * 8 + 5] = (byte) ((value >> 16) & 0xff);
|
||||
buffer[index * 8 + 6] = (byte) ((value >> 8) & 0xff);
|
||||
buffer[index * 8 + 7] = (byte) ((value) & 0xff);
|
||||
}
|
||||
else {
|
||||
buffer[index * 8 + 7] = (byte) ((value >> 56) & 0xff);
|
||||
buffer[index * 8 + 6] = (byte) ((value >> 48) & 0xff);
|
||||
buffer[index * 8 + 5] = (byte) ((value >> 40) & 0xff);
|
||||
buffer[index * 8 + 4] = (byte) ((value >> 32) & 0xff);
|
||||
buffer[index * 8 + 3] = (byte) ((value >> 24) & 0xff);
|
||||
buffer[index * 8 + 2] = (byte) ((value >> 16) & 0xff);
|
||||
buffer[index * 8 + 1] = (byte) ((value >> 8) & 0xff);
|
||||
buffer[index * 8 ] = (byte) ((value) & 0xff);
|
||||
}
|
||||
}
|
||||
|
||||
private long asLong(final int index) {
|
||||
if (byteOrder == ByteOrder.BIG_ENDIAN) {
|
||||
return (buffer[index * 8 ] & 0xffl) << 56l | (buffer[index * 8 + 1] & 0xffl) << 48l |
|
||||
(buffer[index * 8 + 2] & 0xffl) << 40l | (buffer[index * 8 + 3] & 0xffl) << 32l |
|
||||
(buffer[index * 8 + 4] & 0xffl) << 24 | (buffer[index * 8 + 5] & 0xffl) << 16 |
|
||||
(buffer[index * 8 + 6] & 0xffl) << 8 | buffer[index * 8 + 7] & 0xffl;
|
||||
}
|
||||
else {
|
||||
return (buffer[index * 8 + 7] & 0xffl) << 56l | (buffer[index * 8 + 6] & 0xffl) << 48l |
|
||||
(buffer[index * 8 + 5] & 0xffl) << 40l | (buffer[index * 8 + 4] & 0xffl) << 32l |
|
||||
(buffer[index * 8 + 3] & 0xffl) << 24 | (buffer[index * 8 + 2] & 0xffl) << 16 |
|
||||
(buffer[index * 8 + 1] & 0xffl) << 8 | buffer[index * 8] & 0xffl;
|
||||
}
|
||||
}
|
||||
|
||||
private void putInt(final int index, final int value) {
|
||||
if (byteOrder == ByteOrder.BIG_ENDIAN) {
|
||||
buffer[index * 4 ] = (byte) ((value >> 24) & 0xff);
|
||||
buffer[index * 4 + 1] = (byte) ((value >> 16) & 0xff);
|
||||
buffer[index * 4 + 2] = (byte) ((value >> 8) & 0xff);
|
||||
buffer[index * 4 + 3] = (byte) ((value) & 0xff);
|
||||
}
|
||||
else {
|
||||
buffer[index * 4 + 3] = (byte) ((value >> 24) & 0xff);
|
||||
buffer[index * 4 + 2] = (byte) ((value >> 16) & 0xff);
|
||||
buffer[index * 4 + 1] = (byte) ((value >> 8) & 0xff);
|
||||
buffer[index * 4 ] = (byte) ((value) & 0xff);
|
||||
}
|
||||
}
|
||||
|
||||
private int asInt(final int index) {
|
||||
if (byteOrder == ByteOrder.BIG_ENDIAN) {
|
||||
return (buffer[index * 4] & 0xff) << 24 | (buffer[index * 4 + 1] & 0xff) << 16 |
|
||||
(buffer[index * 4 + 2] & 0xff) << 8 | buffer[index * 4 + 3] & 0xff;
|
||||
}
|
||||
else {
|
||||
return (buffer[index * 4 + 3] & 0xff) << 24 | (buffer[index * 4 + 2] & 0xff) << 16 |
|
||||
(buffer[index * 4 + 1] & 0xff) << 8 | buffer[index * 4] & 0xff;
|
||||
}
|
||||
}
|
||||
|
||||
private void putShort(final int index, final int value) {
|
||||
if (byteOrder == ByteOrder.BIG_ENDIAN) {
|
||||
buffer[index * 2 ] = (byte) ((value >> 8) & 0xff);
|
||||
buffer[index * 2 + 1] = (byte) ((value) & 0xff);
|
||||
}
|
||||
else {
|
||||
buffer[index * 2 + 1] = (byte) ((value >> 8) & 0xff);
|
||||
buffer[index * 2 ] = (byte) ((value) & 0xff);
|
||||
}
|
||||
}
|
||||
|
||||
private short asShort(final int index) {
|
||||
if (byteOrder == ByteOrder.BIG_ENDIAN) {
|
||||
return (short) ((buffer[index * 2] & 0xff) << 8 | buffer[index * 2 + 1] & 0xff);
|
||||
}
|
||||
else {
|
||||
return (short) ((buffer[index * 2 + 1] & 0xff) << 8 | buffer[index * 2] & 0xff);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
if (decodedLength < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (decodedPos >= decodedLength) {
|
||||
fetch();
|
||||
|
||||
if (decodedLength < 0) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
return buffer[decodedPos++] & 0xff;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] b, int off, int len) throws IOException {
|
||||
if (decodedLength < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (decodedPos >= decodedLength) {
|
||||
fetch();
|
||||
|
||||
if (decodedLength < 0) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
int read = Math.min(decodedLength - decodedPos, len);
|
||||
System.arraycopy(buffer, decodedPos, b, off, read);
|
||||
decodedPos += read;
|
||||
|
||||
return read;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long skip(long n) throws IOException {
|
||||
if (decodedLength < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (decodedPos >= decodedLength) {
|
||||
fetch();
|
||||
|
||||
if (decodedLength < 0) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
int skipped = (int) Math.min(decodedLength - decodedPos, n);
|
||||
decodedPos += skipped;
|
||||
|
||||
return skipped;
|
||||
}
|
||||
}
|
@@ -112,7 +112,7 @@ class JPEGTables {
|
||||
|
||||
// Read lengths as short array
|
||||
short[] lengths = new short[DHT_LENGTH];
|
||||
for (int i = 0, lengthsLength = lengths.length; i < lengthsLength; i++) {
|
||||
for (int i = 0; i < DHT_LENGTH; i++) {
|
||||
lengths[i] = (short) data.readUnsignedByte();
|
||||
}
|
||||
read += lengths.length;
|
||||
|
@@ -33,16 +33,18 @@ import com.twelvemonkeys.io.enc.Decoder;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* LZWDecoder
|
||||
* Lempel–Ziv–Welch (LZW) decompression. LZW is a universal loss-less data compression algorithm
|
||||
* created by Abraham Lempel, Jacob Ziv, and Terry Welch.
|
||||
* Inspired by libTiff's LZW decompression.
|
||||
*
|
||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||
* @author last modified by $Author: haraldk$
|
||||
* @version $Id: LZWDecoder.java,v 1.0 08.05.12 21:11 haraldk Exp$
|
||||
* @see <a href="http://en.wikipedia.org/wiki/Lempel%E2%80%93Ziv%E2%80%93Welch">LZW (Wikipedia)</a>
|
||||
*/
|
||||
final class LZWDecoder implements Decoder {
|
||||
abstract class LZWDecoder implements Decoder {
|
||||
/** Clear: Re-initialize tables. */
|
||||
static final int CLEAR_CODE = 256;
|
||||
/** End of Information. */
|
||||
@@ -51,44 +53,44 @@ final class LZWDecoder implements Decoder {
|
||||
private static final int MIN_BITS = 9;
|
||||
private static final int MAX_BITS = 12;
|
||||
|
||||
private final boolean reverseBitOrder;
|
||||
private static final int TABLE_SIZE = 1 << MAX_BITS;
|
||||
|
||||
private int currentByte = -1;
|
||||
private int bitPos;
|
||||
private final boolean compatibilityMode;
|
||||
|
||||
// TODO: Consider speeding things up with a "string" type (instead of the inner byte[]),
|
||||
// that uses variable size/dynamic allocation, to avoid the excessive array copying?
|
||||
// private final byte[][] table = new byte[4096][0]; // libTiff adds another 1024 "for compatibility"...
|
||||
private final byte[][] table = new byte[4096 + 1024][0]; // libTiff adds another 1024 "for compatibility"...
|
||||
private final String[] table;
|
||||
private int tableLength;
|
||||
private int bitsPerCode;
|
||||
int bitsPerCode;
|
||||
private int oldCode = CLEAR_CODE;
|
||||
private int maxCode;
|
||||
int bitMask;
|
||||
private int maxString;
|
||||
private boolean eofReached;
|
||||
boolean eofReached;
|
||||
int nextData;
|
||||
int nextBits;
|
||||
|
||||
LZWDecoder(final boolean reverseBitOrder) {
|
||||
this.reverseBitOrder = reverseBitOrder;
|
||||
|
||||
protected LZWDecoder(final boolean compatibilityMode) {
|
||||
this.compatibilityMode = compatibilityMode;
|
||||
|
||||
table = new String[compatibilityMode ? TABLE_SIZE + 1024 : TABLE_SIZE]; // libTiff adds 1024 "for compatibility"...
|
||||
|
||||
// First 258 entries of table is always fixed
|
||||
for (int i = 0; i < 256; i++) {
|
||||
table[i] = new byte[] {(byte) i};
|
||||
table[i] = new String((byte) i);
|
||||
}
|
||||
|
||||
init();
|
||||
}
|
||||
|
||||
LZWDecoder() {
|
||||
this(false);
|
||||
}
|
||||
|
||||
private int maxCodeFor(final int bits) {
|
||||
return reverseBitOrder ? (1 << bits) - 2 : (1 << bits) - 1;
|
||||
private static int bitmaskFor(final int bits) {
|
||||
return (1 << bits) - 1;
|
||||
}
|
||||
|
||||
private void init() {
|
||||
tableLength = 258;
|
||||
bitsPerCode = MIN_BITS;
|
||||
maxCode = maxCodeFor(bitsPerCode);
|
||||
bitMask = bitmaskFor(bitsPerCode);
|
||||
maxCode = maxCode();
|
||||
maxString = 1;
|
||||
}
|
||||
|
||||
@@ -107,25 +109,17 @@ final class LZWDecoder implements Decoder {
|
||||
break;
|
||||
}
|
||||
|
||||
bufferPos += writeString(table[code], buffer, bufferPos);
|
||||
bufferPos += table[code].writeTo(buffer, bufferPos);
|
||||
}
|
||||
else {
|
||||
if (code > tableLength + 1 || oldCode >= tableLength) {
|
||||
// TODO: FixMe for old, borked streams
|
||||
System.err.println("code: " + code);
|
||||
System.err.println("oldCode: " + oldCode);
|
||||
System.err.println("tableLength: " + tableLength);
|
||||
throw new DecodeException("Corrupted LZW table");
|
||||
}
|
||||
|
||||
if (isInTable(code)) {
|
||||
bufferPos += writeString(table[code], buffer, bufferPos);
|
||||
addStringToTable(concatenate(table[oldCode], table[code][0]));
|
||||
bufferPos += table[code].writeTo(buffer, bufferPos);
|
||||
addStringToTable(table[oldCode].concatenate(table[code].firstChar));
|
||||
}
|
||||
else {
|
||||
byte[] outString = concatenate(table[oldCode], table[oldCode][0]);
|
||||
String outString = table[oldCode].concatenate(table[oldCode].firstChar);
|
||||
|
||||
bufferPos += writeString(outString, buffer, bufferPos);
|
||||
bufferPos += outString.writeTo(buffer, bufferPos);
|
||||
addStringToTable(outString);
|
||||
}
|
||||
}
|
||||
@@ -141,29 +135,23 @@ final class LZWDecoder implements Decoder {
|
||||
return bufferPos;
|
||||
}
|
||||
|
||||
private static byte[] concatenate(final byte[] string, final byte firstChar) {
|
||||
byte[] result = Arrays.copyOf(string, string.length + 1);
|
||||
result[string.length] = firstChar;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void addStringToTable(final byte[] string) throws IOException {
|
||||
private void addStringToTable(final String string) throws IOException {
|
||||
table[tableLength++] = string;
|
||||
|
||||
if (tableLength >= maxCode) {
|
||||
if (tableLength > maxCode) {
|
||||
bitsPerCode++;
|
||||
|
||||
if (bitsPerCode > MAX_BITS) {
|
||||
if (reverseBitOrder) {
|
||||
if (compatibilityMode) {
|
||||
bitsPerCode--;
|
||||
}
|
||||
else {
|
||||
throw new DecodeException(String.format("TIFF LZW with more than %d bits per code encountered (table overflow)", MAX_BITS));
|
||||
throw new DecodeException(java.lang.String.format("TIFF LZW with more than %d bits per code encountered (table overflow)", MAX_BITS));
|
||||
}
|
||||
}
|
||||
|
||||
maxCode = maxCodeFor(bitsPerCode);
|
||||
bitMask = bitmaskFor(bitsPerCode);
|
||||
maxCode = maxCode();
|
||||
}
|
||||
|
||||
if (string.length > maxString) {
|
||||
@@ -171,89 +159,14 @@ final class LZWDecoder implements Decoder {
|
||||
}
|
||||
}
|
||||
|
||||
private static int writeString(final byte[] string, final byte[] buffer, final int bufferPos) {
|
||||
if (string.length == 0) {
|
||||
return 0;
|
||||
}
|
||||
else if (string.length == 1) {
|
||||
buffer[bufferPos] = string[0];
|
||||
|
||||
return 1;
|
||||
}
|
||||
else {
|
||||
System.arraycopy(string, 0, buffer, bufferPos, string.length);
|
||||
|
||||
return string.length;
|
||||
}
|
||||
}
|
||||
protected abstract int maxCode();
|
||||
|
||||
private boolean isInTable(int code) {
|
||||
return code < tableLength;
|
||||
}
|
||||
|
||||
private int getNextCode(final InputStream stream) throws IOException {
|
||||
if (eofReached) {
|
||||
return EOI_CODE;
|
||||
}
|
||||
protected abstract int getNextCode(final InputStream stream) throws IOException;
|
||||
|
||||
int bitsToFill = bitsPerCode;
|
||||
int value = 0;
|
||||
|
||||
while (bitsToFill > 0) {
|
||||
int nextBits;
|
||||
if (bitPos == 0) {
|
||||
nextBits = stream.read();
|
||||
|
||||
if (nextBits == -1) {
|
||||
// This is really a bad stream, but should be safe to handle this way, rather than throwing an EOFException.
|
||||
// An EOFException will be thrown by the decoder stream later, if further reading is attempted.
|
||||
eofReached = true;
|
||||
return EOI_CODE;
|
||||
}
|
||||
}
|
||||
else {
|
||||
nextBits = currentByte;
|
||||
}
|
||||
|
||||
int bitsFromHere = 8 - bitPos;
|
||||
if (bitsFromHere > bitsToFill) {
|
||||
bitsFromHere = bitsToFill;
|
||||
}
|
||||
|
||||
if (reverseBitOrder) {
|
||||
// NOTE: This is a spec violation. However, libTiff reads such files.
|
||||
// TIFF 6.0 Specification, Section 13: "LZW Compression"/"The Algorithm", page 61, says:
|
||||
// "LZW compression codes are stored into bytes in high-to-low-order fashion, i.e., FillOrder
|
||||
// is assumed to be 1. The compressed codes are written as bytes (not words) so that the
|
||||
// compressed data will be identical whether it is an ‘II’ or ‘MM’ file."
|
||||
|
||||
// Fill bytes from right-to-left
|
||||
for (int i = 0; i < bitsFromHere; i++) {
|
||||
int destBitPos = bitsPerCode - bitsToFill + i;
|
||||
int srcBitPos = bitPos + i;
|
||||
value |= ((nextBits & (1 << srcBitPos)) >> srcBitPos) << destBitPos;
|
||||
}
|
||||
}
|
||||
else {
|
||||
value |= (nextBits >> 8 - bitPos - bitsFromHere & 0xff >> 8 - bitsFromHere) << bitsToFill - bitsFromHere;
|
||||
}
|
||||
|
||||
bitsToFill -= bitsFromHere;
|
||||
bitPos += bitsFromHere;
|
||||
|
||||
if (bitPos >= 8) {
|
||||
bitPos = 0;
|
||||
}
|
||||
|
||||
currentByte = nextBits;
|
||||
}
|
||||
|
||||
if (value == EOI_CODE) {
|
||||
eofReached = true;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
static boolean isOldBitReversedStream(final InputStream stream) throws IOException {
|
||||
stream.mark(2);
|
||||
@@ -267,5 +180,147 @@ final class LZWDecoder implements Decoder {
|
||||
stream.reset();
|
||||
}
|
||||
}
|
||||
|
||||
public static LZWDecoder create(boolean oldBitReversedStream) {
|
||||
return oldBitReversedStream ? new LZWCompatibilityDecoder() : new LZWSpecDecoder();
|
||||
}
|
||||
|
||||
private static final class LZWSpecDecoder extends LZWDecoder {
|
||||
|
||||
protected LZWSpecDecoder() {
|
||||
super(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int maxCode() {
|
||||
return bitMask - 1;
|
||||
}
|
||||
|
||||
protected final int getNextCode(final InputStream stream) throws IOException {
|
||||
if (eofReached) {
|
||||
return EOI_CODE;
|
||||
}
|
||||
|
||||
int code;
|
||||
int read = stream.read();
|
||||
if (read < 0) {
|
||||
eofReached = true;
|
||||
return EOI_CODE;
|
||||
}
|
||||
|
||||
nextData = (nextData << 8) | read;
|
||||
nextBits += 8;
|
||||
|
||||
if (nextBits < bitsPerCode) {
|
||||
read = stream.read();
|
||||
if (read < 0) {
|
||||
eofReached = true;
|
||||
return EOI_CODE;
|
||||
}
|
||||
|
||||
nextData = (nextData << 8) | read;
|
||||
nextBits += 8;
|
||||
}
|
||||
|
||||
code = ((nextData >> (nextBits - bitsPerCode)) & bitMask);
|
||||
nextBits -= bitsPerCode;
|
||||
|
||||
return code;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class LZWCompatibilityDecoder extends LZWDecoder {
|
||||
// NOTE: This is a spec violation. However, libTiff reads such files.
|
||||
// TIFF 6.0 Specification, Section 13: "LZW Compression"/"The Algorithm", page 61, says:
|
||||
// "LZW compression codes are stored into bytes in high-to-low-order fashion, i.e., FillOrder
|
||||
// is assumed to be 1. The compressed codes are written as bytes (not words) so that the
|
||||
// compressed data will be identical whether it is an ‘II’ or ‘MM’ file."
|
||||
|
||||
protected LZWCompatibilityDecoder() {
|
||||
super(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int maxCode() {
|
||||
return bitMask;
|
||||
}
|
||||
|
||||
protected final int getNextCode(final InputStream stream) throws IOException {
|
||||
if (eofReached) {
|
||||
return EOI_CODE;
|
||||
}
|
||||
|
||||
int code;
|
||||
int read = stream.read();
|
||||
if (read < 0) {
|
||||
eofReached = true;
|
||||
return EOI_CODE;
|
||||
}
|
||||
|
||||
nextData |= read << nextBits;
|
||||
nextBits += 8;
|
||||
|
||||
if (nextBits < bitsPerCode) {
|
||||
read = stream.read();
|
||||
if (read < 0) {
|
||||
eofReached = true;
|
||||
return EOI_CODE;
|
||||
}
|
||||
|
||||
nextData |= read << nextBits;
|
||||
nextBits += 8;
|
||||
}
|
||||
|
||||
code = (nextData & bitMask);
|
||||
nextData >>= bitsPerCode;
|
||||
nextBits -= bitsPerCode;
|
||||
|
||||
return code;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class String {
|
||||
final String previous;
|
||||
|
||||
final int length;
|
||||
final byte value;
|
||||
final byte firstChar; // Copied forward for fast access
|
||||
|
||||
public String(final byte code) {
|
||||
this(code, code, 1, null);
|
||||
}
|
||||
|
||||
private String(final byte value, final byte firstChar, final int length, final String previous) {
|
||||
this.value = value;
|
||||
this.firstChar = firstChar;
|
||||
this.length = length;
|
||||
this.previous = previous;
|
||||
}
|
||||
|
||||
public final String concatenate(final byte firstChar) {
|
||||
return new String(firstChar, this.firstChar, length + 1, this);
|
||||
}
|
||||
|
||||
public final int writeTo(final byte[] buffer, final int offset) {
|
||||
if (length == 0) {
|
||||
return 0;
|
||||
}
|
||||
else if (length == 1) {
|
||||
buffer[offset] = value;
|
||||
|
||||
return 1;
|
||||
}
|
||||
else {
|
||||
String e = this;
|
||||
|
||||
for (int i = length - 1; i >= 0; i--) {
|
||||
buffer[offset + i] = e.value;
|
||||
e = e.previous;
|
||||
}
|
||||
|
||||
return length;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -37,7 +37,7 @@ package com.twelvemonkeys.imageio.plugins.tiff;
|
||||
*/
|
||||
interface TIFFBaseline {
|
||||
int COMPRESSION_NONE = 1;
|
||||
int COMPRESSION_CCITT_HUFFMAN = 2;
|
||||
int COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE = 2;
|
||||
int COMPRESSION_PACKBITS = 32773;
|
||||
|
||||
int PHOTOMETRIC_WHITE_IS_ZERO = 0;
|
||||
|
@@ -49,7 +49,7 @@ interface TIFFCustom {
|
||||
int COMPRESSION_JBIG = 34661;
|
||||
int COMPRESSION_SGILOG = 34676;
|
||||
int COMPRESSION_SGILOG24 = 34677;
|
||||
int COMPRESSION_JP2000 = 34712;
|
||||
int COMPRESSION_JPEG2000 = 34712;
|
||||
|
||||
int PHOTOMETRIC_LOGL = 32844;
|
||||
int PHOTOMETRIC_LOGLUV = 32845;
|
||||
|
@@ -65,4 +65,11 @@ interface TIFFExtension {
|
||||
int SAMPLEFORMAT_INT = 2;
|
||||
int SAMPLEFORMAT_FP = 3;
|
||||
int SAMPLEFORMAT_UNDEFINED = 4;
|
||||
|
||||
int YCBCR_POSITIONING_CENTERED = 1;
|
||||
int YCBCR_POSITIONING_COSITED = 2;
|
||||
|
||||
// "Old-style" JPEG (obsolete)
|
||||
int JPEG_PROC_BASELINE = 1;
|
||||
int JPEG_PROC_LOSSLESS = 14;
|
||||
}
|
||||
|
@@ -35,12 +35,15 @@ import com.twelvemonkeys.imageio.metadata.CompoundDirectory;
|
||||
import com.twelvemonkeys.imageio.metadata.Directory;
|
||||
import com.twelvemonkeys.imageio.metadata.Entry;
|
||||
import com.twelvemonkeys.imageio.metadata.exif.EXIFReader;
|
||||
import com.twelvemonkeys.imageio.metadata.exif.Rational;
|
||||
import com.twelvemonkeys.imageio.metadata.exif.TIFF;
|
||||
import com.twelvemonkeys.imageio.metadata.jpeg.JPEG;
|
||||
import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream;
|
||||
import com.twelvemonkeys.imageio.stream.SubImageInputStream;
|
||||
import com.twelvemonkeys.imageio.util.IIOUtil;
|
||||
import com.twelvemonkeys.imageio.util.IndexedImageTypeSpecifier;
|
||||
import com.twelvemonkeys.imageio.util.ProgressListenerBase;
|
||||
import com.twelvemonkeys.io.FastByteArrayOutputStream;
|
||||
import com.twelvemonkeys.io.LittleEndianDataInputStream;
|
||||
import com.twelvemonkeys.io.enc.DecoderStream;
|
||||
import com.twelvemonkeys.io.enc.PackBitsDecoder;
|
||||
@@ -58,9 +61,7 @@ import java.awt.color.ICC_Profile;
|
||||
import java.awt.image.*;
|
||||
import java.io.*;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
import java.util.zip.Inflater;
|
||||
import java.util.zip.InflaterInputStream;
|
||||
@@ -105,28 +106,30 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
// TODO: Source region (*tests should be failing*)
|
||||
// TODO: TIFFImageWriter + Spi
|
||||
|
||||
// TODOs Full BaseLine support:
|
||||
// TODO: Support ExtraSamples (an array, if multiple extra samples!)
|
||||
// (0: Unspecified (not alpha), 1: Associated Alpha (pre-multiplied), 2: Unassociated Alpha (non-multiplied)
|
||||
|
||||
// TODOs ImageIO advanced functionality:
|
||||
// TODO: Implement readAsRenderedImage to allow tiled renderImage?
|
||||
// For some layouts, we could do reads super-fast with a memory mapped buffer.
|
||||
// TODO: Implement readAsRaster directly
|
||||
|
||||
// TODOs Full BaseLine support:
|
||||
// TODO: Support ExtraSamples (an array, if multiple extra samples!)
|
||||
// (0: Unspecified (not alpha), 1: Associated Alpha (pre-multiplied), 2: Unassociated Alpha (non-multiplied)
|
||||
// TODO: Support Compression 2 (CCITT Modified Huffman) for bi-level images
|
||||
// TODO: IIOMetadata (stay close to Sun's TIFF metadata)
|
||||
// http://download.java.net/media/jai-imageio/javadoc/1.1/com/sun/media/imageio/plugins/tiff/package-summary.html#ImageMetadata
|
||||
|
||||
// TODOs Extension support
|
||||
// TODO: Support PlanarConfiguration 2
|
||||
// TODO: Support ICCProfile (fully)
|
||||
// TODO: Support Compression 3 & 4 (CCITT T.4 & T.6)
|
||||
// TODO: Support Compression 6 ('Old-style' JPEG)
|
||||
// TODO: Support Compression 34712 (JPEG2000)? Depends on JPEG2000 ImageReader
|
||||
// TODO: Support Compression 34661 (JBIG)? Depends on JBIG ImageReader
|
||||
|
||||
// DONE:
|
||||
// Handle SampleFormat (and give up if not == 1)
|
||||
// Support Compression 6 ('Old-style' JPEG)
|
||||
// Support Compression 2 (CCITT Modified Huffman RLE) for bi-level images
|
||||
|
||||
private final static boolean DEBUG = "true".equalsIgnoreCase(System.getProperty("com.twelvemonkeys.imageio.plugins.tiff.debug"));
|
||||
final static boolean DEBUG = "true".equalsIgnoreCase(System.getProperty("com.twelvemonkeys.imageio.plugins.tiff.debug"));
|
||||
|
||||
private CompoundDirectory IFDs;
|
||||
private Directory currentIFD;
|
||||
@@ -150,12 +153,12 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
IFDs = (CompoundDirectory) new EXIFReader().read(imageInput); // NOTE: Sets byte order as a side effect
|
||||
|
||||
if (DEBUG) {
|
||||
for (int i = 0; i < IFDs.directoryCount(); i++) {
|
||||
System.err.printf("ifd[%d]: %s\n", i, IFDs.getDirectory(i));
|
||||
}
|
||||
|
||||
System.err.println("Byte order: " + imageInput.getByteOrder());
|
||||
System.err.println("numImages: " + IFDs.directoryCount());
|
||||
System.err.println("Number of images: " + IFDs.directoryCount());
|
||||
|
||||
for (int i = 0; i < IFDs.directoryCount(); i++) {
|
||||
System.err.printf("IFD %d: %s\n", i, IFDs.getDirectory(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -173,7 +176,7 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
return IFDs.directoryCount();
|
||||
}
|
||||
|
||||
private int getValueAsIntWithDefault(final int tag, String tagName, Integer defaultValue) throws IIOException {
|
||||
private Number getValueAsNumberWithDefault(final int tag, final String tagName, final Number defaultValue) throws IIOException {
|
||||
Entry entry = currentIFD.getEntryById(tag);
|
||||
|
||||
if (entry == null) {
|
||||
@@ -184,7 +187,19 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
throw new IIOException("Missing TIFF tag: " + (tagName != null ? tagName : tag));
|
||||
}
|
||||
|
||||
return ((Number) entry.getValue()).intValue();
|
||||
return (Number) entry.getValue();
|
||||
}
|
||||
|
||||
private long getValueAsLongWithDefault(final int tag, final String tagName, final Long defaultValue) throws IIOException {
|
||||
return getValueAsNumberWithDefault(tag, tagName, defaultValue).longValue();
|
||||
}
|
||||
|
||||
private long getValueAsLongWithDefault(final int tag, final Long defaultValue) throws IIOException {
|
||||
return getValueAsLongWithDefault(tag, null, defaultValue);
|
||||
}
|
||||
|
||||
private int getValueAsIntWithDefault(final int tag, final String tagName, final Integer defaultValue) throws IIOException {
|
||||
return getValueAsNumberWithDefault(tag, tagName, defaultValue).intValue();
|
||||
}
|
||||
|
||||
private int getValueAsIntWithDefault(final int tag, Integer defaultValue) throws IIOException {
|
||||
@@ -216,7 +231,7 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
getSampleFormat(); // We don't support anything but SAMPLEFORMAT_UINT at the moment, just sanity checking input
|
||||
int planarConfiguration = getValueAsIntWithDefault(TIFF.TAG_PLANAR_CONFIGURATION, TIFFExtension.PLANARCONFIG_PLANAR);
|
||||
int interpretation = getValueAsInt(TIFF.TAG_PHOTOMETRIC_INTERPRETATION, "PhotometricInterpretation");
|
||||
int samplesPerPixel = getValueAsIntWithDefault(TIFF.TAG_SAMPLES_PER_PIXELS, 1);
|
||||
int samplesPerPixel = getValueAsIntWithDefault(TIFF.TAG_SAMPLES_PER_PIXEL, 1);
|
||||
int bitsPerSample = getBitsPerSample();
|
||||
int dataType = bitsPerSample <= 8 ? DataBuffer.TYPE_BYTE : bitsPerSample <= 16 ? DataBuffer.TYPE_USHORT : DataBuffer.TYPE_INT;
|
||||
|
||||
@@ -252,8 +267,8 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
}
|
||||
|
||||
case TIFFExtension.PHOTOMETRIC_YCBCR:
|
||||
// JPEG reader will handle YCbCr to RGB for us, we'll have to do it ourselves if not JPEG...
|
||||
// TODO: Handle YCbCrSubsampling (up-scaler stream, or read data as-is + up-sample (sub-)raster after read? Apply smoothing?)
|
||||
// JPEG reader will handle YCbCr to RGB for us, otherwise we'll convert while reading
|
||||
// TODO: Sanity check that we have SamplesPerPixel == 3, BitsPerSample == [8,8,8] and Compression == 1 (none), 5 (LZW), or 6 (JPEG)
|
||||
case TIFFBaseline.PHOTOMETRIC_RGB:
|
||||
// RGB
|
||||
cs = profile == null ? ColorSpace.getInstance(ColorSpace.CS_sRGB) : ColorSpaces.createColorSpace(profile);
|
||||
@@ -274,23 +289,25 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
}
|
||||
}
|
||||
case 4:
|
||||
// TODO: Consult ExtraSamples!
|
||||
if (bitsPerSample == 8 || bitsPerSample == 16) {
|
||||
// ExtraSamples 0=unspecified, 1=associated (premultiplied), 2=unassociated (TODO: Support unspecified, not alpha)
|
||||
long[] extraSamples = getValueAsLongArray(TIFF.TAG_EXTRA_SAMPLES, "ExtraSamples", true);
|
||||
|
||||
switch (planarConfiguration) {
|
||||
case TIFFBaseline.PLANARCONFIG_CHUNKY:
|
||||
if (bitsPerSample == 8 && cs.isCS_sRGB()) {
|
||||
return ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_4BYTE_ABGR);
|
||||
}
|
||||
|
||||
return ImageTypeSpecifier.createInterleaved(cs, new int[] {0, 1, 2, 3}, dataType, false, false);
|
||||
return ImageTypeSpecifier.createInterleaved(cs, new int[] {0, 1, 2, 3}, dataType, true, extraSamples[0] == 1);
|
||||
|
||||
case TIFFExtension.PLANARCONFIG_PLANAR:
|
||||
return ImageTypeSpecifier.createBanded(cs, new int[] {0, 1, 2, 3}, new int[] {0, 0, 0, 0}, dataType, false, false);
|
||||
return ImageTypeSpecifier.createBanded(cs, new int[] {0, 1, 2, 3}, new int[] {0, 0, 0, 0}, dataType, true, extraSamples[0] == 1);
|
||||
}
|
||||
}
|
||||
// TODO: More samples might be ok, if multiple alpha or unknown samples
|
||||
default:
|
||||
throw new IIOException(String.format("Unsupported SamplesPerPixels/BitsPerSample combination for RGB TIF (expected 3/8, 4/8, 3/16 or 4/16): %d/%d", samplesPerPixel, bitsPerSample));
|
||||
throw new IIOException(String.format("Unsupported SamplesPerPixels/BitsPerSample combination for RGB TIFF (expected 3/8, 4/8, 3/16 or 4/16): %d/%d", samplesPerPixel, bitsPerSample));
|
||||
}
|
||||
case TIFFBaseline.PHOTOMETRIC_PALETTE:
|
||||
// Palette
|
||||
@@ -337,18 +354,29 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
return ImageTypeSpecifier.createBanded(cs, new int[] {0, 1, 2, 3}, new int[] {0, 0, 0, 0}, dataType, false, false);
|
||||
}
|
||||
}
|
||||
case 5:
|
||||
if (bitsPerSample == 8 || bitsPerSample == 16) {
|
||||
// ExtraSamples 0=unspecified, 1=associated (premultiplied), 2=unassociated (TODO: Support unspecified, not alpha)
|
||||
long[] extraSamples = getValueAsLongArray(TIFF.TAG_EXTRA_SAMPLES, "ExtraSamples", true);
|
||||
|
||||
switch (planarConfiguration) {
|
||||
case TIFFBaseline.PLANARCONFIG_CHUNKY:
|
||||
return ImageTypeSpecifier.createInterleaved(cs, new int[] {0, 1, 2, 3, 4}, dataType, true, extraSamples[0] == 1);
|
||||
case TIFFExtension.PLANARCONFIG_PLANAR:
|
||||
return ImageTypeSpecifier.createBanded(cs, new int[] {0, 1, 2, 3, 4}, new int[] {0, 0, 0, 0, 0}, dataType, true, extraSamples[0] == 1);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: More samples might be ok, if multiple alpha or unknown samples, consult ExtraSamples
|
||||
|
||||
default:
|
||||
throw new IIOException(
|
||||
String.format("Unsupported TIFF SamplesPerPixels/BitsPerSample combination for Separated TIFF (expected 4/8 or 4/16): %d/%s", samplesPerPixel, bitsPerSample)
|
||||
String.format("Unsupported TIFF SamplesPerPixels/BitsPerSample combination for Separated TIFF (expected 4/8, 4/16, 5/8 or 5/16): %d/%s", samplesPerPixel, bitsPerSample)
|
||||
);
|
||||
}
|
||||
case TIFFBaseline.PHOTOMETRIC_MASK:
|
||||
// Transparency mask
|
||||
|
||||
// TODO: Known extensions
|
||||
throw new IIOException("Unsupported TIFF PhotometricInterpretation value: " + interpretation);
|
||||
default:
|
||||
throw new IIOException("Unknown TIFF PhotometricInterpretation value: " + interpretation);
|
||||
@@ -448,7 +476,9 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
// NOTE: We handle strips as tiles of tileWidth == width by tileHeight == rowsPerStrip
|
||||
// Strips are top/down, tiles are left/right, top/down
|
||||
int stripTileWidth = width;
|
||||
int stripTileHeight = getValueAsIntWithDefault(TIFF.TAG_ROWS_PER_STRIP, height);
|
||||
long rowsPerStrip = getValueAsLongWithDefault(TIFF.TAG_ROWS_PER_STRIP, (1l << 32) - 1);
|
||||
int stripTileHeight = rowsPerStrip < height ? (int) rowsPerStrip : height;
|
||||
|
||||
long[] stripTileOffsets = getValueAsLongArray(TIFF.TAG_TILE_OFFSETS, "TileOffsets", false);
|
||||
long[] stripTileByteCounts;
|
||||
|
||||
@@ -479,9 +509,6 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
WritableRaster rowRaster = rawType.getColorModel().createCompatibleWritableRaster(stripTileWidth, 1);
|
||||
int row = 0;
|
||||
|
||||
// Read data
|
||||
processImageStarted(imageIndex);
|
||||
|
||||
switch (compression) {
|
||||
// TIFF Baseline
|
||||
case TIFFBaseline.COMPRESSION_NONE:
|
||||
@@ -494,9 +521,70 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
// LZW
|
||||
case TIFFExtension.COMPRESSION_ZLIB:
|
||||
// 'Adobe-style' Deflate
|
||||
case TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE:
|
||||
// CCITT modified Huffman
|
||||
// Additionally, the specification defines these values as part of the TIFF extensions:
|
||||
// case TIFFExtension.COMPRESSION_CCITT_T4:
|
||||
// CCITT Group 3 fax encoding
|
||||
// case TIFFExtension.COMPRESSION_CCITT_T6:
|
||||
// CCITT Group 4 fax encoding
|
||||
|
||||
int[] yCbCrSubsampling = null;
|
||||
int yCbCrPos = 1;
|
||||
double[] yCbCrCoefficients = null;
|
||||
if (interpretation == TIFFExtension.PHOTOMETRIC_YCBCR) {
|
||||
// getRawImageType does the lookup/conversion for these
|
||||
if (raster.getNumBands() != 3) {
|
||||
throw new IIOException("TIFF PhotometricInterpretation YCbCr requires SamplesPerPixel == 3: " + raster.getNumBands());
|
||||
}
|
||||
if (raster.getTransferType() != DataBuffer.TYPE_BYTE) {
|
||||
throw new IIOException("TIFF PhotometricInterpretation YCbCr requires BitsPerSample == [8,8,8]");
|
||||
}
|
||||
|
||||
yCbCrPos = getValueAsIntWithDefault(TIFF.TAG_YCBCR_POSITIONING, TIFFExtension.YCBCR_POSITIONING_CENTERED);
|
||||
if (yCbCrPos != TIFFExtension.YCBCR_POSITIONING_CENTERED && yCbCrPos != TIFFExtension.YCBCR_POSITIONING_COSITED) {
|
||||
processWarningOccurred("Uknown TIFF YCbCrPositioning value, expected 1 or 2: " + yCbCrPos);
|
||||
}
|
||||
|
||||
Entry subSampling = currentIFD.getEntryById(TIFF.TAG_YCBCR_SUB_SAMPLING);
|
||||
|
||||
if (subSampling != null) {
|
||||
try {
|
||||
yCbCrSubsampling = (int[]) subSampling.getValue();
|
||||
}
|
||||
catch (ClassCastException e) {
|
||||
throw new IIOException("Unknown TIFF YCbCrSubSampling value type: " + subSampling.getTypeName(), e);
|
||||
}
|
||||
|
||||
if (yCbCrSubsampling.length != 2 ||
|
||||
yCbCrSubsampling[0] != 1 && yCbCrSubsampling[0] != 2 && yCbCrSubsampling[0] != 4 ||
|
||||
yCbCrSubsampling[1] != 1 && yCbCrSubsampling[1] != 2 && yCbCrSubsampling[1] != 4) {
|
||||
throw new IIOException("Bad TIFF YCbCrSubSampling value: " + Arrays.toString(yCbCrSubsampling));
|
||||
}
|
||||
|
||||
if (yCbCrSubsampling[0] < yCbCrSubsampling[1]) {
|
||||
processWarningOccurred("TIFF PhotometricInterpretation YCbCr with bad subsampling, expected subHoriz >= subVert: " + Arrays.toString(yCbCrSubsampling));
|
||||
}
|
||||
}
|
||||
else {
|
||||
yCbCrSubsampling = new int[] {2, 2};
|
||||
}
|
||||
|
||||
Entry coefficients = currentIFD.getEntryById(TIFF.TAG_YCBCR_COEFFICIENTS);
|
||||
if (coefficients != null) {
|
||||
Rational[] value = (Rational[]) coefficients.getValue();
|
||||
yCbCrCoefficients = new double[] {value[0].doubleValue(), value[1].doubleValue(), value[2].doubleValue()};
|
||||
}
|
||||
else {
|
||||
// Default to y CCIR Recommendation 601-1 values
|
||||
yCbCrCoefficients = YCbCrUpsamplerStream.CCIR_601_1_COEFFICIENTS;
|
||||
}
|
||||
}
|
||||
|
||||
// Read data
|
||||
processImageStarted(imageIndex);
|
||||
|
||||
// TODO: Read only tiles that lies within region
|
||||
|
||||
// General uncompressed/compressed reading
|
||||
for (int y = 0; y < tilesDown; y++) {
|
||||
int col = 0;
|
||||
@@ -509,7 +597,8 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
imageInput.seek(stripTileOffsets[i]);
|
||||
|
||||
DataInput input;
|
||||
if (compression == TIFFBaseline.COMPRESSION_NONE) {
|
||||
if (compression == TIFFBaseline.COMPRESSION_NONE && interpretation != TIFFExtension.PHOTOMETRIC_YCBCR) {
|
||||
// No need for transformation, fast forward
|
||||
input = imageInput;
|
||||
}
|
||||
else {
|
||||
@@ -517,15 +606,21 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
? IIOUtil.createStreamAdapter(imageInput, stripTileByteCounts[i])
|
||||
: IIOUtil.createStreamAdapter(imageInput);
|
||||
|
||||
adapter = createDecompressorStream(compression, width, adapter);
|
||||
adapter = createUnpredictorStream(predictor, width, planarConfiguration == 2 ? 1 : raster.getNumBands(), getBitsPerSample(), adapter, imageInput.getByteOrder());
|
||||
|
||||
if (interpretation == TIFFExtension.PHOTOMETRIC_YCBCR) {
|
||||
adapter = new YCbCrUpsamplerStream(adapter, yCbCrSubsampling, yCbCrPos, colsInTile, yCbCrCoefficients);
|
||||
}
|
||||
|
||||
// According to the spec, short/long/etc should follow order of containing stream
|
||||
input = imageInput.getByteOrder() == ByteOrder.BIG_ENDIAN
|
||||
? new DataInputStream(createDecoderInputStream(compression, adapter))
|
||||
: new LittleEndianDataInputStream(createDecoderInputStream(compression, adapter));
|
||||
|
||||
? new DataInputStream(adapter)
|
||||
: new LittleEndianDataInputStream(adapter);
|
||||
}
|
||||
|
||||
// Read a full strip/tile
|
||||
readStripTileData(rowRaster, interpretation, predictor, raster, numBands, col, row, colsInTile, rowsInTile, input);
|
||||
readStripTileData(rowRaster, interpretation, raster, col, row, colsInTile, rowsInTile, input);
|
||||
|
||||
if (abortRequested()) {
|
||||
break;
|
||||
@@ -549,6 +644,7 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
case TIFFExtension.COMPRESSION_JPEG:
|
||||
// JPEG ('new-style' JPEG)
|
||||
// TODO: Refactor all JPEG reading out to separate JPEG support class?
|
||||
// TODO: Cache the JPEG reader for later use? Remember to reset to avoid resource leaks
|
||||
|
||||
// TIFF is strictly ISO JPEG, so we should probably stick to the standard reader
|
||||
ImageReader jpegReader = new JPEGImageReader(getOriginatingProvider());
|
||||
@@ -565,6 +661,9 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
// Might have something to do with subsampling?
|
||||
// How do we pass the chroma-subsampling parameter from the TIFF structure to the JPEG reader?
|
||||
|
||||
// TODO: Consider splicing the TAG_JPEG_TABLES into the streams for each tile, for a
|
||||
// (slightly slower for multiple images, but) more compatible approach..?
|
||||
|
||||
jpegReader.setInput(new ByteArrayImageInputStream(tablesValue));
|
||||
|
||||
// NOTE: This initializes the tables AND MORE secret internal settings for the reader (as if by magic).
|
||||
@@ -620,6 +719,9 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
// ...and the JPEG reader will probably choke on missing tables...
|
||||
}
|
||||
|
||||
// Read data
|
||||
processImageStarted(imageIndex);
|
||||
|
||||
for (int y = 0; y < tilesDown; y++) {
|
||||
int col = 0;
|
||||
int rowsInTile = Math.min(stripTileHeight, height - row);
|
||||
@@ -629,14 +731,14 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
int colsInTile = Math.min(stripTileWidth, width - col);
|
||||
|
||||
imageInput.seek(stripTileOffsets[i]);
|
||||
SubImageInputStream subStream = new SubImageInputStream(imageInput, stripTileByteCounts != null ? (int) stripTileByteCounts[i] : Short.MAX_VALUE);
|
||||
ImageInputStream subStream = new SubImageInputStream(imageInput, stripTileByteCounts != null ? (int) stripTileByteCounts[i] : Short.MAX_VALUE);
|
||||
try {
|
||||
jpegReader.setInput(subStream);
|
||||
jpegParam.setSourceRegion(new Rectangle(0, 0, colsInTile, rowsInTile));
|
||||
jpegParam.setDestinationOffset(new Point(col, row));
|
||||
jpegParam.setDestination(destination);
|
||||
// TODO: This works only if Gray/YCbCr/RGB, not CMYK/LAB/etc...
|
||||
// In the latter case we will have to use readAsRaster
|
||||
// In the latter case we will have to use readAsRaster and do color conversion ourselves
|
||||
jpegReader.read(0, jpegParam);
|
||||
}
|
||||
finally {
|
||||
@@ -662,15 +764,202 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
|
||||
break;
|
||||
|
||||
case TIFFBaseline.COMPRESSION_CCITT_HUFFMAN:
|
||||
// CCITT modified Huffman
|
||||
case TIFFExtension.COMPRESSION_OLD_JPEG:
|
||||
// JPEG ('old-style' JPEG, later overridden in Technote2)
|
||||
// http://www.remotesensing.org/libtiff/TIFFTechNote2.html
|
||||
|
||||
// 512/JPEGProc: 1=Baseline, 14=Lossless (with Huffman coding), no default, although 1 is assumed if absent
|
||||
int mode = getValueAsIntWithDefault(TIFF.TAG_OLD_JPEG_PROC, TIFFExtension.JPEG_PROC_BASELINE);
|
||||
switch (mode) {
|
||||
case TIFFExtension.JPEG_PROC_BASELINE:
|
||||
break; // Supported
|
||||
case TIFFExtension.JPEG_PROC_LOSSLESS:
|
||||
throw new IIOException("Unsupported TIFF JPEGProcessingMode: Lossless (14)");
|
||||
default:
|
||||
throw new IIOException("Unknown TIFF JPEGProcessingMode value: " + mode);
|
||||
}
|
||||
|
||||
// May use normal tiling??
|
||||
|
||||
// TIFF is strictly ISO JPEG, so we should probably stick to the standard reader
|
||||
jpegReader = new JPEGImageReader(getOriginatingProvider());
|
||||
jpegParam = (JPEGImageReadParam) jpegReader.getDefaultReadParam();
|
||||
|
||||
// 513/JPEGInterchangeFormat (may be absent...)
|
||||
int jpegOffset = getValueAsIntWithDefault(TIFF.TAG_JPEG_INTERCHANGE_FORMAT, -1);
|
||||
// 514/JPEGInterchangeFormatLength (may be absent...)
|
||||
int jpegLenght = getValueAsIntWithDefault(TIFF.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH, -1);
|
||||
// TODO: 515/JPEGRestartInterval (may be absent)
|
||||
|
||||
// Currently ignored
|
||||
// 517/JPEGLosslessPredictors
|
||||
// 518/JPEGPointTransforms
|
||||
|
||||
ImageInputStream stream;
|
||||
|
||||
if (jpegOffset != -1) {
|
||||
// Straight forward case: We're good to go! We'll disregard tiling and any tables tags
|
||||
|
||||
if (currentIFD.getEntryById(TIFF.TAG_OLD_JPEG_Q_TABLES) != null || currentIFD.getEntryById(TIFF.TAG_OLD_JPEG_DC_TABLES) != null || currentIFD.getEntryById(TIFF.TAG_OLD_JPEG_AC_TABLES) != null) {
|
||||
processWarningOccurred("Old-style JPEG compressed TIFF with JFIF stream encountered. Ignoring JPEG tables. Reading as single tile.");
|
||||
}
|
||||
else {
|
||||
processWarningOccurred("Old-style JPEG compressed TIFF with JFIF stream encountered. Reading as single tile.");
|
||||
}
|
||||
|
||||
imageInput.seek(jpegOffset);
|
||||
stream = new SubImageInputStream(imageInput, jpegLenght != -1 ? jpegLenght : Short.MAX_VALUE);
|
||||
jpegReader.setInput(stream);
|
||||
|
||||
// Read data
|
||||
processImageStarted(imageIndex);
|
||||
|
||||
try {
|
||||
jpegParam.setSourceRegion(new Rectangle(0, 0, width, height));
|
||||
jpegParam.setDestination(destination);
|
||||
// TODO: This works only if Gray/YCbCr/RGB, not CMYK/LAB/etc...
|
||||
// In the latter case we will have to use readAsRaster and do color conversion ourselves
|
||||
jpegReader.read(0, jpegParam);
|
||||
}
|
||||
finally {
|
||||
stream.close();
|
||||
}
|
||||
|
||||
processImageProgress(100f);
|
||||
|
||||
if (abortRequested()) {
|
||||
processReadAborted();
|
||||
}
|
||||
}
|
||||
else {
|
||||
// The hard way: Read tables and re-create a full JFIF stream
|
||||
|
||||
processWarningOccurred("Old-style JPEG compressed TIFF without JFIF stream encountered. Attempting to re-create JFIF stream.");
|
||||
|
||||
// 519/JPEGQTables
|
||||
// 520/JPEGDCTables
|
||||
// 521/JPEGACTables
|
||||
|
||||
// These fields were originally intended to point to a list of offsets to the quantization tables, one per
|
||||
// component. Each table consists of 64 BYTES (one for each DCT coefficient in the 8x8 block). The
|
||||
// quantization tables are stored in zigzag order, and are compatible with the quantization tables
|
||||
// usually found in a JPEG stream DQT marker.
|
||||
|
||||
// The original specification strongly recommended that, within the TIFF file, each component be
|
||||
// assigned separate tables, and labelled this field as mandatory whenever the JPEGProc field specifies
|
||||
// a DCT-based process.
|
||||
|
||||
// We've seen old-style JPEG in TIFF files where some or all Table offsets, contained the JPEGQTables,
|
||||
// JPEGDCTables, and JPEGACTables tags are incorrect values beyond EOF. However, these files do always
|
||||
// seem to contain a useful JPEGInterchangeFormat tag. Therefore, we recommend a careful attempt to read
|
||||
// the Tables tags only as a last resort, if no table data is found in a JPEGInterchangeFormat stream.
|
||||
|
||||
|
||||
// TODO: If any of the q/dc/ac tables are equal (or have same offset, even if "spec" violation),
|
||||
// use only the first occurrence, and update selectors in SOF0 and SOS
|
||||
|
||||
long[] qTablesOffsets = getValueAsLongArray(TIFF.TAG_OLD_JPEG_Q_TABLES, "JPEGQTables", true);
|
||||
byte[][] qTables = new byte[qTablesOffsets.length][(int) (qTablesOffsets[1] - qTablesOffsets[0])]; // TODO: Using the offsets is fragile.. Use fixed length??
|
||||
// byte[][] qTables = new byte[qTablesOffsets.length][64];
|
||||
// System.err.println("qTables: " + qTables[0].length);
|
||||
for (int j = 0; j < qTables.length; j++) {
|
||||
imageInput.seek(qTablesOffsets[j]);
|
||||
imageInput.readFully(qTables[j]);
|
||||
}
|
||||
|
||||
long[] dcTablesOffsets = getValueAsLongArray(TIFF.TAG_OLD_JPEG_DC_TABLES, "JPEGDCTables", true);
|
||||
byte[][] dcTables = new byte[dcTablesOffsets.length][(int) (dcTablesOffsets[1] - dcTablesOffsets[0])]; // TODO: Using the offsets is fragile.. Use fixed length??
|
||||
// byte[][] dcTables = new byte[dcTablesOffsets.length][28];
|
||||
// System.err.println("dcTables: " + dcTables[0].length);
|
||||
for (int j = 0; j < dcTables.length; j++) {
|
||||
imageInput.seek(dcTablesOffsets[j]);
|
||||
imageInput.readFully(dcTables[j]);
|
||||
}
|
||||
|
||||
long[] acTablesOffsets = getValueAsLongArray(TIFF.TAG_OLD_JPEG_AC_TABLES, "JPEGACTables", true);
|
||||
byte[][] acTables = new byte[acTablesOffsets.length][(int) (acTablesOffsets[1] - acTablesOffsets[0])]; // TODO: Using the offsets is fragile.. Use fixed length??
|
||||
// byte[][] acTables = new byte[acTablesOffsets.length][178];
|
||||
// System.err.println("acTables: " + acTables[0].length);
|
||||
for (int j = 0; j < acTables.length; j++) {
|
||||
imageInput.seek(acTablesOffsets[j]);
|
||||
imageInput.readFully(acTables[j]);
|
||||
}
|
||||
|
||||
// Read data
|
||||
processImageStarted(imageIndex);
|
||||
|
||||
for (int y = 0; y < tilesDown; y++) {
|
||||
int col = 0;
|
||||
int rowsInTile = Math.min(stripTileHeight, height - row);
|
||||
|
||||
for (int x = 0; x < tilesAcross; x++) {
|
||||
int colsInTile = Math.min(stripTileWidth, width - col);
|
||||
int i = y * tilesAcross + x;
|
||||
|
||||
imageInput.seek(stripTileOffsets[i]);
|
||||
stream = ImageIO.createImageInputStream(new SequenceInputStream(Collections.enumeration(
|
||||
Arrays.asList(
|
||||
createJFIFStream(raster, stripTileWidth, stripTileHeight, qTables, dcTables, acTables),
|
||||
IIOUtil.createStreamAdapter(imageInput, stripTileByteCounts != null ? (int) stripTileByteCounts[i] : Short.MAX_VALUE),
|
||||
new ByteArrayInputStream(new byte[] {(byte) 0xff, (byte) 0xd9}) // EOI
|
||||
)
|
||||
)));
|
||||
|
||||
jpegReader.setInput(stream);
|
||||
|
||||
try {
|
||||
jpegParam.setSourceRegion(new Rectangle(0, 0, colsInTile, rowsInTile));
|
||||
jpegParam.setDestinationOffset(new Point(col, row));
|
||||
jpegParam.setDestination(destination);
|
||||
// TODO: This works only if Gray/YCbCr/RGB, not CMYK/LAB/etc...
|
||||
// In the latter case we will have to use readAsRaster and do color conversion ourselves
|
||||
jpegReader.read(0, jpegParam);
|
||||
}
|
||||
finally {
|
||||
stream.close();
|
||||
}
|
||||
|
||||
if (abortRequested()) {
|
||||
break;
|
||||
}
|
||||
|
||||
col += colsInTile;
|
||||
}
|
||||
|
||||
processImageProgress(100f * row / (float) height);
|
||||
|
||||
if (abortRequested()) {
|
||||
processReadAborted();
|
||||
break;
|
||||
}
|
||||
|
||||
row += rowsInTile;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
// Additionally, the specification defines these values as part of the TIFF extensions:
|
||||
case TIFFExtension.COMPRESSION_CCITT_T4:
|
||||
// CCITT Group 3 fax encoding
|
||||
case TIFFExtension.COMPRESSION_CCITT_T6:
|
||||
// CCITT Group 4 fax encoding
|
||||
case TIFFExtension.COMPRESSION_OLD_JPEG:
|
||||
// JPEG ('old-style' JPEG, later overridden in Technote2)
|
||||
|
||||
// Known, but unsupported compression types
|
||||
case TIFFCustom.COMPRESSION_NEXT:
|
||||
case TIFFCustom.COMPRESSION_CCITTRLEW:
|
||||
case TIFFCustom.COMPRESSION_THUNDERSCAN:
|
||||
case TIFFCustom.COMPRESSION_IT8CTPAD:
|
||||
case TIFFCustom.COMPRESSION_IT8LW:
|
||||
case TIFFCustom.COMPRESSION_IT8MP:
|
||||
case TIFFCustom.COMPRESSION_IT8BL:
|
||||
case TIFFCustom.COMPRESSION_PIXARFILM:
|
||||
case TIFFCustom.COMPRESSION_PIXARLOG:
|
||||
case TIFFCustom.COMPRESSION_DCS:
|
||||
case TIFFCustom.COMPRESSION_JBIG: // Doable with JBIG plugin?
|
||||
case TIFFCustom.COMPRESSION_SGILOG:
|
||||
case TIFFCustom.COMPRESSION_SGILOG24:
|
||||
case TIFFCustom.COMPRESSION_JPEG2000: // Doable with JPEG2000 plugin?
|
||||
|
||||
throw new IIOException("Unsupported TIFF Compression value: " + compression);
|
||||
default:
|
||||
@@ -682,13 +971,83 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
return destination;
|
||||
}
|
||||
|
||||
private void readStripTileData(final WritableRaster rowRaster, final int interpretation, final int predictor,
|
||||
final WritableRaster raster, final int numBands, final int col, final int startRow,
|
||||
private static InputStream createJFIFStream(WritableRaster raster, int stripTileWidth, int stripTileHeight, byte[][] qTables, byte[][] dcTables, byte[][] acTables) throws IOException {
|
||||
FastByteArrayOutputStream stream = new FastByteArrayOutputStream(
|
||||
2 + 2 + 2 + 6 + 3 * raster.getNumBands() +
|
||||
5 * qTables.length + qTables.length * qTables[0].length +
|
||||
5 * dcTables.length + dcTables.length * dcTables[0].length +
|
||||
5 * acTables.length + acTables.length * acTables[0].length +
|
||||
8 + 2 * raster.getNumBands()
|
||||
);
|
||||
|
||||
DataOutputStream out = new DataOutputStream(stream);
|
||||
|
||||
out.writeShort(JPEG.SOI);
|
||||
out.writeShort(JPEG.SOF0);
|
||||
out.writeShort(2 + 6 + 3 * raster.getNumBands()); // SOF0 len
|
||||
out.writeByte(8); // bits TODO: Consult raster/transfer type or BitsPerSample for 12/16 bits support
|
||||
out.writeShort(stripTileHeight); // height
|
||||
out.writeShort(stripTileWidth); // width
|
||||
out.writeByte(raster.getNumBands()); // Number of components
|
||||
|
||||
for (int comp = 0; comp < raster.getNumBands(); comp++) {
|
||||
out.writeByte(comp); // Component id
|
||||
out.writeByte(comp == 0 ? 0x22 : 0x11); // h/v subsampling TODO: FixMe, consult YCbCrSubsampling
|
||||
out.writeByte(comp); // Q table selector TODO: Consider merging if tables are equal
|
||||
}
|
||||
|
||||
// TODO: Consider merging if tables are equal
|
||||
for (int tableIndex = 0; tableIndex < qTables.length; tableIndex++) {
|
||||
byte[] table = qTables[tableIndex];
|
||||
out.writeShort(JPEG.DQT);
|
||||
out.writeShort(3 + table.length); // DQT length
|
||||
out.writeByte(tableIndex); // Q table id
|
||||
out.write(table); // Table data
|
||||
}
|
||||
|
||||
// TODO: Consider merging if tables are equal
|
||||
for (int tableIndex = 0; tableIndex < dcTables.length; tableIndex++) {
|
||||
byte[] table = dcTables[tableIndex];
|
||||
out.writeShort(JPEG.DHT);
|
||||
out.writeShort(3 + table.length); // DHT length
|
||||
out.writeByte(tableIndex); // Huffman table id
|
||||
out.write(table); // Table data
|
||||
}
|
||||
|
||||
// TODO: Consider merging if tables are equal
|
||||
for (int tableIndex = 0; tableIndex < acTables.length; tableIndex++) {
|
||||
byte[] table = acTables[tableIndex];
|
||||
out.writeShort(JPEG.DHT);
|
||||
out.writeShort(3 + table.length); // DHT length
|
||||
out.writeByte(0x10 + (tableIndex & 0xf)); // Huffman table id
|
||||
out.write(table); // Table data
|
||||
}
|
||||
|
||||
out.writeShort(JPEG.SOS);
|
||||
out.writeShort(6 + 2 * raster.getNumBands()); // SOS length
|
||||
out.writeByte(raster.getNumBands()); // Num comp
|
||||
|
||||
for (int component = 0; component < raster.getNumBands(); component++) {
|
||||
out.writeByte(component); // Comp id
|
||||
out.writeByte(component == 0 ? component : 0x10 + (component & 0xf)); // dc/ac selector
|
||||
}
|
||||
|
||||
// Unknown 3 bytes pad... TODO: Figure out what the last 3 bytes are...
|
||||
out.writeByte(0);
|
||||
out.writeByte(0);
|
||||
out.writeByte(0);
|
||||
|
||||
return stream.createInputStream();
|
||||
}
|
||||
|
||||
private void readStripTileData(final WritableRaster rowRaster, final int interpretation,
|
||||
final WritableRaster raster, final int col, final int startRow,
|
||||
final int colsInStrip, final int rowsInStrip, final DataInput input)
|
||||
throws IOException {
|
||||
switch (rowRaster.getTransferType()) {
|
||||
case DataBuffer.TYPE_BYTE:
|
||||
byte[] rowData = ((DataBufferByte) rowRaster.getDataBuffer()).getData();
|
||||
|
||||
for (int j = 0; j < rowsInStrip; j++) {
|
||||
int row = startRow + j;
|
||||
|
||||
@@ -697,19 +1056,6 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
}
|
||||
|
||||
input.readFully(rowData);
|
||||
|
||||
// for (int k = 0; k < rowData.length; k++) {
|
||||
// try {
|
||||
// rowData[k] = input.readByte();
|
||||
// }
|
||||
// catch (IOException e) {
|
||||
// Arrays.fill(rowData, k, rowData.length, (byte) -1);
|
||||
// System.err.printf("Unexpected EOF or bad data at [%d %d]\n", col + k, row);
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
|
||||
unPredict(predictor, colsInStrip, 1, numBands, rowData);
|
||||
normalizeBlack(interpretation, rowData);
|
||||
|
||||
if (colsInStrip == rowRaster.getWidth() && col + colsInStrip <= raster.getWidth()) {
|
||||
@@ -724,6 +1070,7 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
break;
|
||||
case DataBuffer.TYPE_USHORT:
|
||||
short [] rowDataShort = ((DataBufferUShort) rowRaster.getDataBuffer()).getData();
|
||||
|
||||
for (int j = 0; j < rowsInStrip; j++) {
|
||||
int row = startRow + j;
|
||||
|
||||
@@ -735,7 +1082,6 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
rowDataShort[k] = input.readShort();
|
||||
}
|
||||
|
||||
unPredict(predictor, colsInStrip, 1, numBands, rowDataShort);
|
||||
normalizeBlack(interpretation, rowDataShort);
|
||||
|
||||
if (colsInStrip == rowRaster.getWidth() && col + colsInStrip <= raster.getWidth()) {
|
||||
@@ -750,6 +1096,7 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
break;
|
||||
case DataBuffer.TYPE_INT:
|
||||
int [] rowDataInt = ((DataBufferInt) rowRaster.getDataBuffer()).getData();
|
||||
|
||||
for (int j = 0; j < rowsInStrip; j++) {
|
||||
int row = startRow + j;
|
||||
|
||||
@@ -761,7 +1108,6 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
rowDataInt[k] = input.readInt();
|
||||
}
|
||||
|
||||
unPredict(predictor, colsInStrip, 1, numBands, rowDataInt);
|
||||
normalizeBlack(interpretation, rowDataInt);
|
||||
|
||||
if (colsInStrip == rowRaster.getWidth() && col + colsInStrip <= raster.getWidth()) {
|
||||
@@ -804,75 +1150,40 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("UnusedParameters")
|
||||
private void unPredict(final int predictor, int scanLine, int rows, int bands, int[] data) throws IIOException {
|
||||
// See TIFF 6.0 Specification, Section 14: "Differencing Predictor", page 64.
|
||||
switch (predictor) {
|
||||
case TIFFBaseline.PREDICTOR_NONE:
|
||||
break;
|
||||
case TIFFExtension.PREDICTOR_HORIZONTAL_DIFFERENCING:
|
||||
// TODO: Implement
|
||||
case TIFFExtension.PREDICTOR_HORIZONTAL_FLOATINGPOINT:
|
||||
throw new IIOException("Unsupported TIFF Predictor value: " + predictor);
|
||||
default:
|
||||
throw new IIOException("Unknown TIFF Predictor value: " + predictor);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("UnusedParameters")
|
||||
private void unPredict(final int predictor, int scanLine, int rows, int bands, short[] data) throws IIOException {
|
||||
// See TIFF 6.0 Specification, Section 14: "Differencing Predictor", page 64.
|
||||
switch (predictor) {
|
||||
case TIFFBaseline.PREDICTOR_NONE:
|
||||
break;
|
||||
case TIFFExtension.PREDICTOR_HORIZONTAL_DIFFERENCING:
|
||||
// TODO: Implement
|
||||
case TIFFExtension.PREDICTOR_HORIZONTAL_FLOATINGPOINT:
|
||||
throw new IIOException("Unsupported TIFF Predictor value: " + predictor);
|
||||
default:
|
||||
throw new IIOException("Unknown TIFF Predictor value: " + predictor);
|
||||
}
|
||||
}
|
||||
|
||||
private void unPredict(final int predictor, int scanLine, int rows, final int bands, byte[] data) throws IIOException {
|
||||
// See TIFF 6.0 Specification, Section 14: "Differencing Predictor", page 64.
|
||||
switch (predictor) {
|
||||
case TIFFBaseline.PREDICTOR_NONE:
|
||||
break;
|
||||
case TIFFExtension.PREDICTOR_HORIZONTAL_DIFFERENCING:
|
||||
for (int y = 0; y < rows; y++) {
|
||||
for (int x = 1; x < scanLine; x++) {
|
||||
// TODO: For planar data (PlanarConfiguration == 2), treat as bands == 1
|
||||
for (int b = 0; b < bands; b++) {
|
||||
int off = y * scanLine + x;
|
||||
data[off * bands + b] = (byte) (data[(off - 1) * bands + b] + data[off * bands + b]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case TIFFExtension.PREDICTOR_HORIZONTAL_FLOATINGPOINT:
|
||||
throw new IIOException("Unsupported TIFF Predictor value: " + predictor);
|
||||
default:
|
||||
throw new IIOException("Unknown TIFF Predictor value: " + predictor);
|
||||
}
|
||||
}
|
||||
|
||||
private InputStream createDecoderInputStream(final int compression, final InputStream stream) throws IOException {
|
||||
private InputStream createDecompressorStream(final int compression, final int width, final InputStream stream) throws IOException {
|
||||
switch (compression) {
|
||||
case TIFFBaseline.COMPRESSION_NONE:
|
||||
return stream;
|
||||
case TIFFBaseline.COMPRESSION_PACKBITS:
|
||||
return new DecoderStream(stream, new PackBitsDecoder(), 1024);
|
||||
case TIFFExtension.COMPRESSION_LZW:
|
||||
return new DecoderStream(stream, new LZWDecoder(LZWDecoder.isOldBitReversedStream(stream)), 1024);
|
||||
return new DecoderStream(stream, LZWDecoder.create(LZWDecoder.isOldBitReversedStream(stream)), 1024);
|
||||
case TIFFExtension.COMPRESSION_ZLIB:
|
||||
case TIFFExtension.COMPRESSION_DEFLATE:
|
||||
// TIFFphotoshop.pdf (aka TIFF specification, supplement 2) says ZLIB (8) and DEFLATE (32946) algorithms are identical
|
||||
case TIFFExtension.COMPRESSION_DEFLATE:
|
||||
return new InflaterInputStream(stream, new Inflater(), 1024);
|
||||
case TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE:
|
||||
case TIFFExtension.COMPRESSION_CCITT_T4:
|
||||
case TIFFExtension.COMPRESSION_CCITT_T6:
|
||||
return new CCITTFaxDecoderStream(stream, width, compression, getValueAsIntWithDefault(TIFF.TAG_FILL_ORDER, 1));
|
||||
default:
|
||||
throw new IllegalArgumentException("Unsupported TIFF compression: " + compression);
|
||||
}
|
||||
}
|
||||
|
||||
private InputStream createUnpredictorStream(final int predictor, final int width, final int samplesPerPixel, final int bitsPerSample, final InputStream stream, final ByteOrder byteOrder) throws IOException {
|
||||
switch (predictor) {
|
||||
case TIFFBaseline.PREDICTOR_NONE:
|
||||
return stream;
|
||||
case TIFFExtension.PREDICTOR_HORIZONTAL_DIFFERENCING:
|
||||
return new HorizontalDeDifferencingStream(stream, width, samplesPerPixel, bitsPerSample, byteOrder);
|
||||
case TIFFExtension.PREDICTOR_HORIZONTAL_FLOATINGPOINT:
|
||||
throw new IIOException("Unsupported TIFF Predictor value: " + predictor);
|
||||
default:
|
||||
throw new IIOException("Unknown TIFF Predictor value: " + predictor);
|
||||
}
|
||||
}
|
||||
|
||||
private long[] getValueAsLongArray(final int tag, final String tagName, boolean required) throws IIOException {
|
||||
Entry entry = currentIFD.getEntryById(tag);
|
||||
if (entry == null) {
|
||||
@@ -893,7 +1204,7 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
short[] shorts = (short[]) entry.getValue();
|
||||
value = new long[shorts.length];
|
||||
|
||||
for (int i = 0, stripOffsetsValueLength = value.length; i < stripOffsetsValueLength; i++) {
|
||||
for (int i = 0, length = value.length; i < length; i++) {
|
||||
value[i] = shorts[i];
|
||||
}
|
||||
}
|
||||
@@ -901,7 +1212,7 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
int[] ints = (int[]) entry.getValue();
|
||||
value = new long[ints.length];
|
||||
|
||||
for (int i = 0, stripOffsetsValueLength = value.length; i < stripOffsetsValueLength; i++) {
|
||||
for (int i = 0, length = value.length; i < length; i++) {
|
||||
value[i] = ints[i];
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,348 @@
|
||||
/*
|
||||
* Copyright (c) 2013, Harald Kuhr
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
* * Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
* * Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* * Neither the name "TwelveMonkeys" nor the
|
||||
* names of its contributors may be used to endorse or promote products
|
||||
* derived from this software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
||||
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
package com.twelvemonkeys.imageio.plugins.tiff;
|
||||
|
||||
import com.twelvemonkeys.lang.Validate;
|
||||
|
||||
import java.awt.image.DataBufferByte;
|
||||
import java.awt.image.Raster;
|
||||
import java.io.EOFException;
|
||||
import java.io.FilterInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Input stream that provides on-the-fly conversion and upsampling of TIFF susampled YCbCr samples to (raw) RGB samples.
|
||||
*
|
||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||
* @author last modified by $Author: haraldk$
|
||||
* @version $Id: YCbCrUpsamplerStream.java,v 1.0 31.01.13 09:25 haraldk Exp$
|
||||
*/
|
||||
final class YCbCrUpsamplerStream extends FilterInputStream {
|
||||
// NOTE: DO NOT MODIFY OR EXPOSE THIS ARRAY OUTSIDE PACKAGE!
|
||||
static final double[] CCIR_601_1_COEFFICIENTS = new double[] {299.0 / 1000.0, 587.0 / 1000.0, 114.0 / 1000.0};
|
||||
|
||||
private final int horizChromaSub;
|
||||
private final int vertChromaSub;
|
||||
private final int yCbCrPos;
|
||||
private final int columns;
|
||||
private final double[] coefficients;
|
||||
|
||||
private final int units;
|
||||
private final int unitSize;
|
||||
private final int padding;
|
||||
private final byte[] decodedRows;
|
||||
int decodedLength;
|
||||
int decodedPos;
|
||||
|
||||
private final byte[] buffer;
|
||||
int bufferLength;
|
||||
int bufferPos;
|
||||
|
||||
public YCbCrUpsamplerStream(final InputStream stream, final int[] chromaSub, final int yCbCrPos, final int columns, final double[] coefficients) {
|
||||
super(Validate.notNull(stream, "stream"));
|
||||
|
||||
this.horizChromaSub = chromaSub[0];
|
||||
this.vertChromaSub = chromaSub[1];
|
||||
this.yCbCrPos = yCbCrPos;
|
||||
this.columns = columns;
|
||||
this.coefficients = Arrays.equals(CCIR_601_1_COEFFICIENTS, coefficients) ? null : coefficients;
|
||||
|
||||
// In TIFF, subsampled streams are stored in "units" of horiz * vert pixels.
|
||||
// For a 4:2 subsampled stream like this:
|
||||
//
|
||||
// Y0 Y1 Y2 Y3 Cb0 Cr0 Y8 Y9 Y10 Y11 Cb1 Cr1
|
||||
// Y4 Y5 Y6 Y7 Y12Y13Y14 Y15
|
||||
//
|
||||
// In the stream, the order is: Y0,Y1,Y2..Y7,Cb0,Cr0, Y8...Y15,Cb1,Cr1, Y16...
|
||||
|
||||
unitSize = horizChromaSub * vertChromaSub + 2;
|
||||
units = (columns + horizChromaSub - 1) / horizChromaSub; // If columns % horizChromasSub != 0...
|
||||
padding = units * horizChromaSub - columns; // ...each coded row will be padded to fill unit
|
||||
decodedRows = new byte[columns * vertChromaSub * 3];
|
||||
buffer = new byte[unitSize * units];
|
||||
}
|
||||
|
||||
private void fetch() throws IOException {
|
||||
if (bufferPos >= bufferLength) {
|
||||
int pos = 0;
|
||||
int read;
|
||||
|
||||
// This *SHOULD* read an entire row of units into the buffer, otherwise decodeRows will throw EOFException
|
||||
while (pos < buffer.length && (read = in.read(buffer, pos, buffer.length - pos)) > 0) {
|
||||
pos += read;
|
||||
}
|
||||
|
||||
bufferLength = pos;
|
||||
bufferPos = 0;
|
||||
}
|
||||
|
||||
if (bufferLength > 0) {
|
||||
decodeRows();
|
||||
}
|
||||
else {
|
||||
decodedLength = -1;
|
||||
}
|
||||
}
|
||||
|
||||
private void decodeRows() throws EOFException {
|
||||
decodedLength = decodedRows.length;
|
||||
|
||||
for (int u = 0; u < units; u++) {
|
||||
if (bufferPos >= bufferLength) {
|
||||
throw new EOFException("Unexpected end of stream");
|
||||
}
|
||||
|
||||
// Decode one unit
|
||||
byte cb = buffer[bufferPos + unitSize - 2];
|
||||
byte cr = buffer[bufferPos + unitSize - 1];
|
||||
|
||||
for (int y = 0; y < vertChromaSub; y++) {
|
||||
for (int x = 0; x < horizChromaSub; x++) {
|
||||
// Skip padding at end of row
|
||||
int column = horizChromaSub * u + x;
|
||||
if (column >= columns) {
|
||||
bufferPos += padding;
|
||||
break;
|
||||
}
|
||||
|
||||
int pixelOff = 3 * (column + columns * y);
|
||||
|
||||
decodedRows[pixelOff] = buffer[bufferPos++];
|
||||
decodedRows[pixelOff + 1] = cb;
|
||||
decodedRows[pixelOff + 2] = cr;
|
||||
|
||||
// Convert to RGB
|
||||
if (coefficients == null) {
|
||||
YCbCrConverter.convertYCbCr2RGB(decodedRows, decodedRows, pixelOff);
|
||||
}
|
||||
else {
|
||||
convertYCbCr2RGB(decodedRows, decodedRows, coefficients, pixelOff);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bufferPos += 2; // Skip CbCr bytes at end of unit
|
||||
}
|
||||
|
||||
bufferPos = bufferLength;
|
||||
decodedPos = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
if (decodedLength < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (decodedPos >= decodedLength) {
|
||||
fetch();
|
||||
|
||||
if (decodedLength < 0) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
return decodedRows[decodedPos++] & 0xff;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] b, int off, int len) throws IOException {
|
||||
if (decodedLength < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (decodedPos >= decodedLength) {
|
||||
fetch();
|
||||
|
||||
if (decodedLength < 0) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
int read = Math.min(decodedLength - decodedPos, len);
|
||||
System.arraycopy(decodedRows, decodedPos, b, off, read);
|
||||
decodedPos += read;
|
||||
|
||||
return read;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long skip(long n) throws IOException {
|
||||
if (decodedLength < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (decodedPos >= decodedLength) {
|
||||
fetch();
|
||||
|
||||
if (decodedLength < 0) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
int skipped = (int) Math.min(decodedLength - decodedPos, n);
|
||||
decodedPos += skipped;
|
||||
|
||||
return skipped;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean markSupported() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void reset() throws IOException {
|
||||
throw new IOException("mark/reset not supported");
|
||||
}
|
||||
|
||||
private void convertYCbCr2RGB(final byte[] yCbCr, final byte[] rgb, final double[] coefficients, final int offset) {
|
||||
double y = (yCbCr[offset ] & 0xff);
|
||||
double cb = (yCbCr[offset + 1] & 0xff) - 128; // TODO: The -128 part seems bogus... Consult ReferenceBlackWhite??? But default to these values?
|
||||
double cr = (yCbCr[offset + 2] & 0xff) - 128;
|
||||
|
||||
double lumaRed = coefficients[0];
|
||||
double lumaGreen = coefficients[1];
|
||||
double lumaBlue = coefficients[2];
|
||||
|
||||
int red = (int) Math.round(cr * (2 - 2 * lumaRed) + y);
|
||||
int blue = (int) Math.round(cb * (2 - 2 * lumaBlue) + y);
|
||||
int green = (int) Math.round((y - lumaRed * (rgb[offset] & 0xff) - lumaBlue * (rgb[offset + 2] & 0xff)) / lumaGreen);
|
||||
|
||||
rgb[offset ] = clamp(red);
|
||||
rgb[offset + 2] = clamp(blue);
|
||||
rgb[offset + 1] = clamp(green);
|
||||
}
|
||||
|
||||
private static byte clamp(int val) {
|
||||
return (byte) Math.max(0, Math.min(255, val));
|
||||
}
|
||||
|
||||
// TODO: This code is copied from JPEG package, make it "more" public: com.tm.imageio.color package?
|
||||
/**
|
||||
* Static inner class for lazy-loading of conversion tables.
|
||||
*/
|
||||
static final class YCbCrConverter {
|
||||
/** Define tables for YCC->RGB color space conversion. */
|
||||
private final static int SCALEBITS = 16;
|
||||
private final static int MAXJSAMPLE = 255;
|
||||
private final static int CENTERJSAMPLE = 128;
|
||||
private final static int ONE_HALF = 1 << (SCALEBITS - 1);
|
||||
|
||||
private final static int[] Cr_R_LUT = new int[MAXJSAMPLE + 1];
|
||||
private final static int[] Cb_B_LUT = new int[MAXJSAMPLE + 1];
|
||||
private final static int[] Cr_G_LUT = new int[MAXJSAMPLE + 1];
|
||||
private final static int[] Cb_G_LUT = new int[MAXJSAMPLE + 1];
|
||||
|
||||
/**
|
||||
* Initializes tables for YCC->RGB color space conversion.
|
||||
*/
|
||||
private static void buildYCCtoRGBtable() {
|
||||
if (TIFFImageReader.DEBUG) {
|
||||
System.err.println("Building YCC conversion table");
|
||||
}
|
||||
|
||||
for (int i = 0, x = -CENTERJSAMPLE; i <= MAXJSAMPLE; i++, x++) {
|
||||
// i is the actual input pixel value, in the range 0..MAXJSAMPLE
|
||||
// The Cb or Cr value we are thinking of is x = i - CENTERJSAMPLE
|
||||
// Cr=>R value is nearest int to 1.40200 * x
|
||||
Cr_R_LUT[i] = (int) ((1.40200 * (1 << SCALEBITS) + 0.5) * x + ONE_HALF) >> SCALEBITS;
|
||||
// Cb=>B value is nearest int to 1.77200 * x
|
||||
Cb_B_LUT[i] = (int) ((1.77200 * (1 << SCALEBITS) + 0.5) * x + ONE_HALF) >> SCALEBITS;
|
||||
// Cr=>G value is scaled-up -0.71414 * x
|
||||
Cr_G_LUT[i] = -(int) (0.71414 * (1 << SCALEBITS) + 0.5) * x;
|
||||
// Cb=>G value is scaled-up -0.34414 * x
|
||||
// We also add in ONE_HALF so that need not do it in inner loop
|
||||
Cb_G_LUT[i] = -(int) ((0.34414) * (1 << SCALEBITS) + 0.5) * x + ONE_HALF;
|
||||
}
|
||||
}
|
||||
|
||||
static {
|
||||
buildYCCtoRGBtable();
|
||||
}
|
||||
|
||||
static void convertYCbCr2RGB(final Raster raster) {
|
||||
final int height = raster.getHeight();
|
||||
final int width = raster.getWidth();
|
||||
final byte[] data = ((DataBufferByte) raster.getDataBuffer()).getData();
|
||||
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
convertYCbCr2RGB(data, data, (x + y * width) * 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void convertYCbCr2RGB(final byte[] yCbCr, final byte[] rgb, final int offset) {
|
||||
int y = yCbCr[offset ] & 0xff;
|
||||
int cr = yCbCr[offset + 2] & 0xff;
|
||||
int cb = yCbCr[offset + 1] & 0xff;
|
||||
|
||||
rgb[offset ] = clamp(y + Cr_R_LUT[cr]);
|
||||
rgb[offset + 1] = clamp(y + (Cb_G_LUT[cb] + Cr_G_LUT[cr] >> SCALEBITS));
|
||||
rgb[offset + 2] = clamp(y + Cb_B_LUT[cb]);
|
||||
}
|
||||
|
||||
static void convertYCCK2CMYK(final Raster raster) {
|
||||
final int height = raster.getHeight();
|
||||
final int width = raster.getWidth();
|
||||
final byte[] data = ((DataBufferByte) raster.getDataBuffer()).getData();
|
||||
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
convertYCCK2CMYK(data, data, (x + y * width) * 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void convertYCCK2CMYK(byte[] ycck, byte[] cmyk, int offset) {
|
||||
// Inverted
|
||||
int y = 255 - ycck[offset ] & 0xff;
|
||||
int cb = 255 - ycck[offset + 1] & 0xff;
|
||||
int cr = 255 - ycck[offset + 2] & 0xff;
|
||||
int k = 255 - ycck[offset + 3] & 0xff;
|
||||
|
||||
int cmykC = MAXJSAMPLE - (y + Cr_R_LUT[cr]);
|
||||
int cmykM = MAXJSAMPLE - (y + (Cb_G_LUT[cb] + Cr_G_LUT[cr] >> SCALEBITS));
|
||||
int cmykY = MAXJSAMPLE - (y + Cb_B_LUT[cb]);
|
||||
|
||||
cmyk[offset ] = clamp(cmykC);
|
||||
cmyk[offset + 1] = clamp(cmykM);
|
||||
cmyk[offset + 2] = clamp(cmykY);
|
||||
cmyk[offset + 3] = (byte) k; // K passes through unchanged
|
||||
}
|
||||
|
||||
// private static byte clamp(int val) {
|
||||
// return (byte) Math.max(0, Math.min(255, val));
|
||||
// }
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,137 @@
|
||||
<!DOCTYPE com_sun_media_imageio_plugins_tiff_image_1.0 [
|
||||
<!ELEMENT com_sun_media_imageio_plugins_tiff_image_1.0 (TIFFIFD)*>
|
||||
|
||||
<!ELEMENT TIFFIFD (TIFFField | TIFFIFD)*>
|
||||
<!-- An IFD (directory) containing fields -->
|
||||
<!ATTLIST TIFFIFD "tagSets" CDATA #REQUIRED>
|
||||
<!-- Data type: String -->
|
||||
<!ATTLIST TIFFIFD "parentTagNumber" CDATA #IMPLIED>
|
||||
<!-- The tag number of the field pointing to this IFD -->
|
||||
<!-- Data type: Integer -->
|
||||
<!ATTLIST TIFFIFD "parentTagName" CDATA #IMPLIED>
|
||||
<!-- A mnemonic name for the field pointing to this IFD, if known
|
||||
-->
|
||||
<!-- Data type: String -->
|
||||
|
||||
<!ELEMENT TIFFField (TIFFBytes | TIFFAsciis | TIFFShorts | TIFFSShorts | TIFFLongs | TIFFSLongs | TIFFRationals | TIFFSRationals | TIFFFloats | TIFFDoubles | TIFFUndefined)>
|
||||
<!-- A field containing data -->
|
||||
<!ATTLIST TIFFField "number" CDATA #REQUIRED>
|
||||
<!-- The tag number asociated with the field -->
|
||||
<!-- Data type: String -->
|
||||
<!ATTLIST TIFFField "name" CDATA #IMPLIED>
|
||||
<!-- A mnemonic name associated with the field, if known -->
|
||||
<!-- Data type: String -->
|
||||
|
||||
<!ELEMENT TIFFBytes (TIFFByte)*>
|
||||
<!-- A sequence of TIFFByte nodes -->
|
||||
|
||||
<!ELEMENT TIFFByte EMPTY>
|
||||
<!-- An integral value between 0 and 255 -->
|
||||
<!ATTLIST TIFFByte "value" CDATA #IMPLIED>
|
||||
<!-- The value -->
|
||||
<!-- Data type: String -->
|
||||
<!ATTLIST TIFFByte "description" CDATA #IMPLIED>
|
||||
<!-- A description, if available -->
|
||||
<!-- Data type: String -->
|
||||
|
||||
<!ELEMENT TIFFAsciis (TIFFAscii)*>
|
||||
<!-- A sequence of TIFFAscii nodes -->
|
||||
|
||||
<!ELEMENT TIFFAscii EMPTY>
|
||||
<!-- A String value -->
|
||||
<!ATTLIST TIFFAscii "value" CDATA #IMPLIED>
|
||||
<!-- The value -->
|
||||
<!-- Data type: String -->
|
||||
|
||||
<!ELEMENT TIFFShorts (TIFFShort)*>
|
||||
<!-- A sequence of TIFFShort nodes -->
|
||||
|
||||
<!ELEMENT TIFFShort EMPTY>
|
||||
<!-- An integral value between 0 and 65535 -->
|
||||
<!ATTLIST TIFFShort "value" CDATA #IMPLIED>
|
||||
<!-- The value -->
|
||||
<!-- Data type: String -->
|
||||
<!ATTLIST TIFFShort "description" CDATA #IMPLIED>
|
||||
<!-- A description, if available -->
|
||||
<!-- Data type: String -->
|
||||
|
||||
<!ELEMENT TIFFSShorts (TIFFSShort)*>
|
||||
<!-- A sequence of TIFFSShort nodes -->
|
||||
|
||||
<!ELEMENT TIFFSShort EMPTY>
|
||||
<!-- An integral value between -32768 and 32767 -->
|
||||
<!ATTLIST TIFFSShort "value" CDATA #IMPLIED>
|
||||
<!-- The value -->
|
||||
<!-- Data type: String -->
|
||||
<!ATTLIST TIFFSShort "description" CDATA #IMPLIED>
|
||||
<!-- A description, if available -->
|
||||
<!-- Data type: String -->
|
||||
|
||||
<!ELEMENT TIFFLongs (TIFFLong)*>
|
||||
<!-- A sequence of TIFFLong nodes -->
|
||||
|
||||
<!ELEMENT TIFFLong EMPTY>
|
||||
<!-- An integral value between 0 and 4294967295 -->
|
||||
<!ATTLIST TIFFLong "value" CDATA #IMPLIED>
|
||||
<!-- The value -->
|
||||
<!-- Data type: String -->
|
||||
<!ATTLIST TIFFLong "description" CDATA #IMPLIED>
|
||||
<!-- A description, if available -->
|
||||
<!-- Data type: String -->
|
||||
|
||||
<!ELEMENT TIFFSLongs (TIFFSLong)*>
|
||||
<!-- A sequence of TIFFSLong nodes -->
|
||||
|
||||
<!ELEMENT TIFFSLong EMPTY>
|
||||
<!-- An integral value between -2147483648 and 2147482647 -->
|
||||
<!ATTLIST TIFFSLong "value" CDATA #IMPLIED>
|
||||
<!-- The value -->
|
||||
<!-- Data type: String -->
|
||||
<!ATTLIST TIFFSLong "description" CDATA #IMPLIED>
|
||||
<!-- A description, if available -->
|
||||
<!-- Data type: String -->
|
||||
|
||||
<!ELEMENT TIFFRationals (TIFFRational)*>
|
||||
<!-- A sequence of TIFFRational nodes -->
|
||||
|
||||
<!ELEMENT TIFFRational EMPTY>
|
||||
<!-- A rational value consisting of an unsigned numerator and
|
||||
denominator -->
|
||||
<!ATTLIST TIFFRational "value" CDATA #IMPLIED>
|
||||
<!-- The numerator and denominator, separated by a slash -->
|
||||
<!-- Data type: String -->
|
||||
|
||||
<!ELEMENT TIFFSRationals (TIFFSRational)*>
|
||||
<!-- A sequence of TIFFSRational nodes -->
|
||||
|
||||
<!ELEMENT TIFFSRational EMPTY>
|
||||
<!-- A rational value consisting of a signed numerator and
|
||||
denominator -->
|
||||
<!ATTLIST TIFFSRational "value" CDATA #IMPLIED>
|
||||
<!-- The numerator and denominator, separated by a slash -->
|
||||
<!-- Data type: String -->
|
||||
|
||||
<!ELEMENT TIFFFloats (TIFFFloat)*>
|
||||
<!-- A sequence of TIFFFloat nodes -->
|
||||
|
||||
<!ELEMENT TIFFFloat EMPTY>
|
||||
<!-- A single-precision floating-point value -->
|
||||
<!ATTLIST TIFFFloat "value" CDATA #IMPLIED>
|
||||
<!-- The value -->
|
||||
<!-- Data type: String -->
|
||||
|
||||
<!ELEMENT TIFFDoubles (TIFFDouble)*>
|
||||
<!-- A sequence of TIFFDouble nodes -->
|
||||
|
||||
<!ELEMENT TIFFDouble EMPTY>
|
||||
<!-- A double-precision floating-point value -->
|
||||
<!ATTLIST TIFFDouble "value" CDATA #IMPLIED>
|
||||
<!-- The value -->
|
||||
<!-- Data type: String -->
|
||||
|
||||
<!ELEMENT TIFFUndefined EMPTY>
|
||||
<!-- Uninterpreted byte data -->
|
||||
<!ATTLIST TIFFUndefined "value" CDATA #IMPLIED>
|
||||
<!-- A list of comma-separated byte values -->
|
||||
<!-- Data type: String -->
|
||||
]>
|
@@ -0,0 +1,9 @@
|
||||
<!DOCTYPE com_sun_media_imageio_plugins_tiff_stream_1.0 [
|
||||
<!ELEMENT com_sun_media_imageio_plugins_tiff_stream_1.0 (ByteOrder)>
|
||||
|
||||
<!ELEMENT ByteOrder EMPTY>
|
||||
<!-- The stream byte order -->
|
||||
<!ATTLIST ByteOrder "value" CDATA #REQUIRED>
|
||||
<!-- One of "BIG_ENDIAN" or "LITTLE_ENDIAN" -->
|
||||
<!-- Data type: String -->
|
||||
]>
|
@@ -0,0 +1,166 @@
|
||||
/*
|
||||
* Copyright (c) 2013, Harald Kuhr
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
* * Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
* * Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* * Neither the name "TwelveMonkeys" nor the
|
||||
* names of its contributors may be used to endorse or promote products
|
||||
* derived from this software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
||||
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
package com.twelvemonkeys.imageio.plugins.tiff;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.DataBufferByte;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
/**
|
||||
* CCITTFaxDecoderStreamTest
|
||||
*
|
||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||
* @author last modified by $Author: haraldk$
|
||||
* @version $Id: CCITTFaxDecoderStreamTest.java,v 1.0 09.03.13 14:44 haraldk Exp$
|
||||
*/
|
||||
public class CCITTFaxDecoderStreamTest {
|
||||
|
||||
// TODO: Better tests (full A4 width scan lines?)
|
||||
|
||||
// From http://www.mikekohn.net/file_formats/tiff.php
|
||||
static final byte[] DATA_TYPE_2 = {
|
||||
(byte) 0x84, (byte) 0xe0, // 10000100 11100000
|
||||
(byte) 0x84, (byte) 0xe0, // 10000100 11100000
|
||||
(byte) 0x84, (byte) 0xe0, // 10000100 11100000
|
||||
(byte) 0x7d, (byte) 0xc0, // 01111101 11000000
|
||||
};
|
||||
|
||||
static final byte[] DATA_TYPE_3 = {
|
||||
0x00, 0x01, (byte) 0xc2, 0x70,
|
||||
0x00, 0x01, 0x70,
|
||||
0x01,
|
||||
|
||||
};
|
||||
|
||||
static final byte[] DATA_TYPE_4 = {
|
||||
0x26, (byte) 0xb0, 95, (byte) 0xfa, (byte) 0xc0
|
||||
};
|
||||
|
||||
// Image should be (6 x 4):
|
||||
// 1 1 1 0 1 1 x x
|
||||
// 1 1 1 0 1 1 x x
|
||||
// 1 1 1 0 1 1 x x
|
||||
// 1 1 0 0 1 1 x x
|
||||
BufferedImage image;
|
||||
|
||||
@Before
|
||||
public void init() {
|
||||
image = new BufferedImage(6, 4, BufferedImage.TYPE_BYTE_BINARY);
|
||||
for (int y = 0; y < 4; y++) {
|
||||
for (int x = 0; x < 6; x++) {
|
||||
image.setRGB(x, y, x == 3 ? 0xff000000 : 0xffffffff);
|
||||
}
|
||||
}
|
||||
|
||||
image.setRGB(2, 3, 0xff000000);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadCountType2() throws IOException {
|
||||
InputStream stream = new CCITTFaxDecoderStream(new ByteArrayInputStream(DATA_TYPE_2), 6, TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE, 1);
|
||||
|
||||
int count = 0;
|
||||
int read;
|
||||
while ((read = stream.read()) >= 0) {
|
||||
count++;
|
||||
}
|
||||
|
||||
// Just make sure we'll have 4 bytes
|
||||
assertEquals(4, count);
|
||||
|
||||
// Verify that we don't return arbitrary values
|
||||
assertEquals(-1, read);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDecodeType2() throws IOException {
|
||||
InputStream stream = new CCITTFaxDecoderStream(new ByteArrayInputStream(DATA_TYPE_2), 6, TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE, 1);
|
||||
|
||||
byte[] imageData = ((DataBufferByte) image.getData().getDataBuffer()).getData();
|
||||
byte[] bytes = new byte[imageData.length];
|
||||
new DataInputStream(stream).readFully(bytes);
|
||||
|
||||
// JPanel panel = new JPanel();
|
||||
// panel.add(new JLabel("Expected", new BufferedImageIcon(image, 300, 300, true), JLabel.CENTER));
|
||||
// panel.add(new JLabel("Actual", new BufferedImageIcon(new BufferedImage(image.getColorModel(), Raster.createPackedRaster(new DataBufferByte(bytes, bytes.length), 6, 4, 1, null), false, null), 300, 300, true), JLabel.CENTER));
|
||||
// JOptionPane.showConfirmDialog(null, panel);
|
||||
|
||||
assertArrayEquals(imageData, bytes);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testDecodeType3() throws IOException {
|
||||
InputStream stream = new CCITTFaxDecoderStream(new ByteArrayInputStream(DATA_TYPE_3), 6, TIFFExtension.COMPRESSION_CCITT_T4, 1);
|
||||
|
||||
byte[] imageData = ((DataBufferByte) image.getData().getDataBuffer()).getData();
|
||||
byte[] bytes = new byte[imageData.length];
|
||||
DataInputStream dataInput = new DataInputStream(stream);
|
||||
|
||||
for (int y = 0; y < image.getHeight(); y++) {
|
||||
System.err.println("y: " + y);
|
||||
dataInput.readFully(bytes, y * image.getWidth(), image.getWidth());
|
||||
}
|
||||
|
||||
// JPanel panel = new JPanel();
|
||||
// panel.add(new JLabel("Expected", new BufferedImageIcon(image, 300, 300, true), JLabel.CENTER));
|
||||
// panel.add(new JLabel("Actual", new BufferedImageIcon(new BufferedImage(image.getColorModel(), Raster.createPackedRaster(new DataBufferByte(bytes, bytes.length), 6, 4, 1, null), false, null), 300, 300, true), JLabel.CENTER));
|
||||
// JOptionPane.showConfirmDialog(null, panel);
|
||||
|
||||
assertArrayEquals(imageData, bytes);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testDecodeType4() throws IOException {
|
||||
InputStream stream = new CCITTFaxDecoderStream(new ByteArrayInputStream(DATA_TYPE_4), 6, TIFFExtension.COMPRESSION_CCITT_T6, 1);
|
||||
|
||||
byte[] imageData = ((DataBufferByte) image.getData().getDataBuffer()).getData();
|
||||
byte[] bytes = new byte[imageData.length];
|
||||
DataInputStream dataInput = new DataInputStream(stream);
|
||||
|
||||
for (int y = 0; y < image.getHeight(); y++) {
|
||||
System.err.println("y: " + y);
|
||||
dataInput.readFully(bytes, y * image.getWidth(), image.getWidth());
|
||||
}
|
||||
|
||||
// JPanel panel = new JPanel();
|
||||
// panel.add(new JLabel("Expected", new BufferedImageIcon(image, 300, 300, true), JLabel.CENTER));
|
||||
// panel.add(new JLabel("Actual", new BufferedImageIcon(new BufferedImage(image.getColorModel(), Raster.createPackedRaster(new DataBufferByte(bytes, bytes.length), 6, 4, 1, null), false, null), 300, 300, true), JLabel.CENTER));
|
||||
// JOptionPane.showConfirmDialog(null, panel);
|
||||
|
||||
assertArrayEquals(imageData, bytes);
|
||||
}
|
||||
}
|
@@ -0,0 +1,569 @@
|
||||
/*
|
||||
* Copyright (c) 2013, Harald Kuhr
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
* * Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
* * Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* * Neither the name "TwelveMonkeys" nor the
|
||||
* names of its contributors may be used to endorse or promote products
|
||||
* derived from this software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
||||
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
package com.twelvemonkeys.imageio.plugins.tiff;
|
||||
|
||||
import com.twelvemonkeys.io.FastByteArrayOutputStream;
|
||||
import com.twelvemonkeys.io.LittleEndianDataInputStream;
|
||||
import com.twelvemonkeys.io.LittleEndianDataOutputStream;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
/**
|
||||
* HorizontalDeDifferencingStreamTest
|
||||
*
|
||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||
* @author last modified by $Author: haraldk$
|
||||
* @version $Id: HorizontalDeDifferencingStreamTest.java,v 1.0 13.03.13 12:46 haraldk Exp$
|
||||
*/
|
||||
public class HorizontalDeDifferencingStreamTest {
|
||||
@Test
|
||||
public void testRead1SPP1BPS() throws IOException {
|
||||
// 1 sample per pixel, 1 bits per sample (mono/indexed)
|
||||
byte[] data = {
|
||||
(byte) 0x80, 0x00, 0x00,
|
||||
0x71, 0x11, 0x44,
|
||||
};
|
||||
|
||||
InputStream stream = new HorizontalDeDifferencingStream(new ByteArrayInputStream(data), 24, 1, 1, ByteOrder.BIG_ENDIAN);
|
||||
|
||||
// Row 1
|
||||
assertEquals(0xff, stream.read());
|
||||
assertEquals(0xff, stream.read());
|
||||
assertEquals(0xff, stream.read());
|
||||
|
||||
// Row 2
|
||||
assertEquals(0x5e, stream.read());
|
||||
assertEquals(0x1e, stream.read());
|
||||
assertEquals(0x78, stream.read());
|
||||
|
||||
// EOF
|
||||
assertEquals(-1, stream.read());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRead1SPP2BPS() throws IOException {
|
||||
// 1 sample per pixel, 2 bits per sample (gray/indexed)
|
||||
byte[] data = {
|
||||
(byte) 0xc0, 0x00, 0x00, 0x00,
|
||||
0x71, 0x11, 0x44, (byte) 0xcc,
|
||||
};
|
||||
|
||||
InputStream stream = new HorizontalDeDifferencingStream(new ByteArrayInputStream(data), 16, 1, 2, ByteOrder.BIG_ENDIAN);
|
||||
|
||||
// Row 1
|
||||
assertEquals(0xff, stream.read());
|
||||
assertEquals(0xff, stream.read());
|
||||
assertEquals(0xff, stream.read());
|
||||
assertEquals(0xff, stream.read());
|
||||
|
||||
// Row 2
|
||||
assertEquals(0x41, stream.read());
|
||||
assertEquals(0x6b, stream.read());
|
||||
assertEquals(0x05, stream.read());
|
||||
assertEquals(0x0f, stream.read());
|
||||
|
||||
// EOF
|
||||
assertEquals(-1, stream.read());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRead1SPP4BPS() throws IOException {
|
||||
// 1 sample per pixel, 4 bits per sample (gray/indexed)
|
||||
byte[] data = {
|
||||
(byte) 0xf0, 0x00, 0x00, 0x00,
|
||||
0x70, 0x11, 0x44, (byte) 0xcc,
|
||||
0x00, 0x01, 0x10, (byte) 0xe0
|
||||
};
|
||||
|
||||
InputStream stream = new HorizontalDeDifferencingStream(new ByteArrayInputStream(data), 8, 1, 4, ByteOrder.BIG_ENDIAN);
|
||||
|
||||
// Row 1
|
||||
assertEquals(0xff, stream.read());
|
||||
assertEquals(0xff, stream.read());
|
||||
assertEquals(0xff, stream.read());
|
||||
assertEquals(0xff, stream.read());
|
||||
|
||||
// Row 2
|
||||
assertEquals(0x77, stream.read());
|
||||
assertEquals(0x89, stream.read());
|
||||
assertEquals(0xd1, stream.read());
|
||||
assertEquals(0xd9, stream.read());
|
||||
|
||||
// Row 3
|
||||
assertEquals(0x00, stream.read());
|
||||
assertEquals(0x01, stream.read());
|
||||
assertEquals(0x22, stream.read());
|
||||
assertEquals(0x00, stream.read());
|
||||
|
||||
// EOF
|
||||
assertEquals(-1, stream.read());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRead1SPP8BPS() throws IOException {
|
||||
// 1 sample per pixel, 8 bits per sample (gray/indexed)
|
||||
byte[] data = {
|
||||
(byte) 0xff, 0, 0, 0,
|
||||
0x7f, 1, 4, -4,
|
||||
0x00, 127, 127, -127
|
||||
};
|
||||
|
||||
InputStream stream = new HorizontalDeDifferencingStream(new ByteArrayInputStream(data), 4, 1, 8, ByteOrder.BIG_ENDIAN);
|
||||
|
||||
// Row 1
|
||||
assertEquals(0xff, stream.read());
|
||||
assertEquals(0xff, stream.read());
|
||||
assertEquals(0xff, stream.read());
|
||||
assertEquals(0xff, stream.read());
|
||||
|
||||
// Row 2
|
||||
assertEquals(0x7f, stream.read());
|
||||
assertEquals(0x80, stream.read());
|
||||
assertEquals(0x84, stream.read());
|
||||
assertEquals(0x80, stream.read());
|
||||
|
||||
// Row 3
|
||||
assertEquals(0x00, stream.read());
|
||||
assertEquals(0x7f, stream.read());
|
||||
assertEquals(0xfe, stream.read());
|
||||
assertEquals(0x7f, stream.read());
|
||||
|
||||
// EOF
|
||||
assertEquals(-1, stream.read());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadArray1SPP8BPS() throws IOException {
|
||||
// 1 sample per pixel, 8 bits per sample (gray/indexed)
|
||||
byte[] data = {
|
||||
(byte) 0xff, 0, 0, 0,
|
||||
0x7f, 1, 4, -4,
|
||||
0x00, 127, 127, -127
|
||||
};
|
||||
|
||||
InputStream stream = new HorizontalDeDifferencingStream(new ByteArrayInputStream(data), 4, 1, 8, ByteOrder.BIG_ENDIAN);
|
||||
|
||||
byte[] result = new byte[data.length];
|
||||
new DataInputStream(stream).readFully(result);
|
||||
|
||||
assertArrayEquals(
|
||||
new byte[] {
|
||||
(byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
|
||||
0x7f, (byte) 0x80, (byte) 0x84, (byte) 0x80,
|
||||
0x00, 0x7f, (byte) 0xfe, 0x7f,
|
||||
},
|
||||
result
|
||||
);
|
||||
|
||||
// EOF
|
||||
assertEquals(-1, stream.read(new byte[16]));
|
||||
assertEquals(-1, stream.read());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRead1SPP32BPS() throws IOException {
|
||||
// 1 sample per pixel, 32 bits per sample (gray)
|
||||
FastByteArrayOutputStream out = new FastByteArrayOutputStream(16);
|
||||
DataOutput dataOut = new DataOutputStream(out);
|
||||
dataOut.writeInt(0x00000000);
|
||||
dataOut.writeInt(305419896);
|
||||
dataOut.writeInt(305419896);
|
||||
dataOut.writeInt(-610839792);
|
||||
|
||||
InputStream in = new HorizontalDeDifferencingStream(out.createInputStream(), 4, 1, 32, ByteOrder.BIG_ENDIAN);
|
||||
DataInput dataIn = new DataInputStream(in);
|
||||
|
||||
// Row 1
|
||||
assertEquals(0, dataIn.readInt());
|
||||
assertEquals(305419896, dataIn.readInt());
|
||||
assertEquals(610839792, dataIn.readInt());
|
||||
assertEquals(0, dataIn.readInt());
|
||||
|
||||
// EOF
|
||||
assertEquals(-1, in.read());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRead1SPP32BPSLittleEndian() throws IOException {
|
||||
// 1 sample per pixel, 32 bits per sample (gray)
|
||||
FastByteArrayOutputStream out = new FastByteArrayOutputStream(16);
|
||||
DataOutput dataOut = new LittleEndianDataOutputStream(out);
|
||||
dataOut.writeInt(0x00000000);
|
||||
dataOut.writeInt(305419896);
|
||||
dataOut.writeInt(305419896);
|
||||
dataOut.writeInt(-610839792);
|
||||
|
||||
InputStream in = new HorizontalDeDifferencingStream(out.createInputStream(), 4, 1, 32, ByteOrder.LITTLE_ENDIAN);
|
||||
DataInput dataIn = new LittleEndianDataInputStream(in);
|
||||
|
||||
// Row 1
|
||||
assertEquals(0, dataIn.readInt());
|
||||
assertEquals(305419896, dataIn.readInt());
|
||||
assertEquals(610839792, dataIn.readInt());
|
||||
assertEquals(0, dataIn.readInt());
|
||||
|
||||
// EOF
|
||||
assertEquals(-1, in.read());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRead1SPP64BPS() throws IOException {
|
||||
// 1 sample per pixel, 64 bits per sample (gray)
|
||||
FastByteArrayOutputStream out = new FastByteArrayOutputStream(32);
|
||||
DataOutput dataOut = new DataOutputStream(out);
|
||||
dataOut.writeLong(0x00000000);
|
||||
dataOut.writeLong(81985529216486895L);
|
||||
dataOut.writeLong(81985529216486895L);
|
||||
dataOut.writeLong(-163971058432973790L);
|
||||
|
||||
InputStream in = new HorizontalDeDifferencingStream(out.createInputStream(), 4, 1, 64, ByteOrder.BIG_ENDIAN);
|
||||
DataInput dataIn = new DataInputStream(in);
|
||||
|
||||
// Row 1
|
||||
assertEquals(0, dataIn.readLong());
|
||||
assertEquals(81985529216486895L, dataIn.readLong());
|
||||
assertEquals(163971058432973790L, dataIn.readLong());
|
||||
assertEquals(0, dataIn.readLong());
|
||||
|
||||
// EOF
|
||||
assertEquals(-1, in.read());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRead1SPP64BPSLittleEndian() throws IOException {
|
||||
// 1 sample per pixel, 64 bits per sample (gray)
|
||||
FastByteArrayOutputStream out = new FastByteArrayOutputStream(32);
|
||||
DataOutput dataOut = new LittleEndianDataOutputStream(out);
|
||||
dataOut.writeLong(0x00000000);
|
||||
dataOut.writeLong(81985529216486895L);
|
||||
dataOut.writeLong(81985529216486895L);
|
||||
dataOut.writeLong(-163971058432973790L);
|
||||
|
||||
InputStream in = new HorizontalDeDifferencingStream(out.createInputStream(), 4, 1, 64, ByteOrder.LITTLE_ENDIAN);
|
||||
DataInput dataIn = new LittleEndianDataInputStream(in);
|
||||
|
||||
// Row 1
|
||||
assertEquals(0, dataIn.readLong());
|
||||
assertEquals(81985529216486895L, dataIn.readLong());
|
||||
assertEquals(163971058432973790L, dataIn.readLong());
|
||||
assertEquals(0, dataIn.readLong());
|
||||
|
||||
// EOF
|
||||
assertEquals(-1, in.read());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRead3SPP8BPS() throws IOException {
|
||||
// 3 samples per pixel, 8 bits per sample (RGB)
|
||||
byte[] data = {
|
||||
(byte) 0xff, (byte) 0x00, (byte) 0x7f, -1, -1, -1, -4, -4, -4, 4, 4, 4,
|
||||
0x7f, 0x7f, 0x7f, 1, 1, 1, 4, 4, 4, -4, -4, -4,
|
||||
0x00, 0x00, 0x00, 127, -127, 0, -127, 127, 0, 0, 0, 127,
|
||||
};
|
||||
|
||||
InputStream stream = new HorizontalDeDifferencingStream(new ByteArrayInputStream(data), 4, 3, 8, ByteOrder.BIG_ENDIAN);
|
||||
|
||||
// Row 1
|
||||
assertEquals(0xff, stream.read());
|
||||
assertEquals(0x00, stream.read());
|
||||
assertEquals(0x7f, stream.read());
|
||||
|
||||
assertEquals(0xfe, stream.read());
|
||||
assertEquals(0xff, stream.read());
|
||||
assertEquals(0x7e, stream.read());
|
||||
|
||||
assertEquals(0xfa, stream.read());
|
||||
assertEquals(0xfb, stream.read());
|
||||
assertEquals(0x7a, stream.read());
|
||||
|
||||
assertEquals(0xfe, stream.read());
|
||||
assertEquals(0xff, stream.read());
|
||||
assertEquals(0x7e, stream.read());
|
||||
|
||||
// Row 2
|
||||
assertEquals(0x7f, stream.read());
|
||||
assertEquals(0x7f, stream.read());
|
||||
assertEquals(0x7f, stream.read());
|
||||
|
||||
assertEquals(0x80, stream.read());
|
||||
assertEquals(0x80, stream.read());
|
||||
assertEquals(0x80, stream.read());
|
||||
|
||||
assertEquals(0x84, stream.read());
|
||||
assertEquals(0x84, stream.read());
|
||||
assertEquals(0x84, stream.read());
|
||||
|
||||
assertEquals(0x80, stream.read());
|
||||
assertEquals(0x80, stream.read());
|
||||
assertEquals(0x80, stream.read());
|
||||
|
||||
// Row 3
|
||||
assertEquals(0x00, stream.read());
|
||||
assertEquals(0x00, stream.read());
|
||||
assertEquals(0x00, stream.read());
|
||||
|
||||
assertEquals(0x7f, stream.read());
|
||||
assertEquals(0x81, stream.read());
|
||||
assertEquals(0x00, stream.read());
|
||||
|
||||
assertEquals(0x00, stream.read());
|
||||
assertEquals(0x00, stream.read());
|
||||
assertEquals(0x00, stream.read());
|
||||
|
||||
assertEquals(0x00, stream.read());
|
||||
assertEquals(0x00, stream.read());
|
||||
assertEquals(0x7f, stream.read());
|
||||
|
||||
// EOF
|
||||
assertEquals(-1, stream.read());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRead3SPP16BPS() throws IOException {
|
||||
FastByteArrayOutputStream out = new FastByteArrayOutputStream(24);
|
||||
DataOutput dataOut = new DataOutputStream(out);
|
||||
dataOut.writeShort(0x0000);
|
||||
dataOut.writeShort(0x0000);
|
||||
dataOut.writeShort(0x0000);
|
||||
dataOut.writeShort(4660);
|
||||
dataOut.writeShort(30292);
|
||||
dataOut.writeShort(4660);
|
||||
dataOut.writeShort(4660);
|
||||
dataOut.writeShort(30292);
|
||||
dataOut.writeShort(4660);
|
||||
dataOut.writeShort(-9320);
|
||||
dataOut.writeShort(-60584);
|
||||
dataOut.writeShort(-9320);
|
||||
|
||||
dataOut.writeShort(0x0000);
|
||||
dataOut.writeShort(0x0000);
|
||||
dataOut.writeShort(0x0000);
|
||||
dataOut.writeShort(30292);
|
||||
dataOut.writeShort(30292);
|
||||
dataOut.writeShort(30292);
|
||||
dataOut.writeShort(30292);
|
||||
dataOut.writeShort(30292);
|
||||
dataOut.writeShort(30292);
|
||||
dataOut.writeShort(-60584);
|
||||
dataOut.writeShort(-60584);
|
||||
dataOut.writeShort(-60584);
|
||||
|
||||
InputStream in = new HorizontalDeDifferencingStream(out.createInputStream(), 4, 3, 16, ByteOrder.BIG_ENDIAN);
|
||||
DataInput dataIn = new DataInputStream(in);
|
||||
|
||||
// Row 1
|
||||
assertEquals(0, dataIn.readUnsignedShort());
|
||||
assertEquals(0, dataIn.readUnsignedShort());
|
||||
assertEquals(0, dataIn.readUnsignedShort());
|
||||
assertEquals(4660, dataIn.readUnsignedShort());
|
||||
assertEquals(30292, dataIn.readUnsignedShort());
|
||||
assertEquals(4660, dataIn.readUnsignedShort());
|
||||
assertEquals(9320, dataIn.readUnsignedShort());
|
||||
assertEquals(60584, dataIn.readUnsignedShort());
|
||||
assertEquals(9320, dataIn.readUnsignedShort());
|
||||
assertEquals(0, dataIn.readUnsignedShort());
|
||||
assertEquals(0, dataIn.readUnsignedShort());
|
||||
assertEquals(0, dataIn.readUnsignedShort());
|
||||
|
||||
// Row 2
|
||||
assertEquals(0, dataIn.readUnsignedShort());
|
||||
assertEquals(0, dataIn.readUnsignedShort());
|
||||
assertEquals(0, dataIn.readUnsignedShort());
|
||||
assertEquals(30292, dataIn.readUnsignedShort());
|
||||
assertEquals(30292, dataIn.readUnsignedShort());
|
||||
assertEquals(30292, dataIn.readUnsignedShort());
|
||||
assertEquals(60584, dataIn.readUnsignedShort());
|
||||
assertEquals(60584, dataIn.readUnsignedShort());
|
||||
assertEquals(60584, dataIn.readUnsignedShort());
|
||||
assertEquals(0, dataIn.readUnsignedShort());
|
||||
assertEquals(0, dataIn.readUnsignedShort());
|
||||
assertEquals(0, dataIn.readUnsignedShort());
|
||||
|
||||
// EOF
|
||||
assertEquals(-1, in.read());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRead3SPP16BPSLittleEndian() throws IOException {
|
||||
FastByteArrayOutputStream out = new FastByteArrayOutputStream(24);
|
||||
DataOutput dataOut = new LittleEndianDataOutputStream(out);
|
||||
dataOut.writeShort(0x0000);
|
||||
dataOut.writeShort(0x0000);
|
||||
dataOut.writeShort(0x0000);
|
||||
dataOut.writeShort(4660);
|
||||
dataOut.writeShort(30292);
|
||||
dataOut.writeShort(4660);
|
||||
dataOut.writeShort(4660);
|
||||
dataOut.writeShort(30292);
|
||||
dataOut.writeShort(4660);
|
||||
dataOut.writeShort(-9320);
|
||||
dataOut.writeShort(-60584);
|
||||
dataOut.writeShort(-9320);
|
||||
|
||||
dataOut.writeShort(0x0000);
|
||||
dataOut.writeShort(0x0000);
|
||||
dataOut.writeShort(0x0000);
|
||||
dataOut.writeShort(30292);
|
||||
dataOut.writeShort(30292);
|
||||
dataOut.writeShort(30292);
|
||||
dataOut.writeShort(30292);
|
||||
dataOut.writeShort(30292);
|
||||
dataOut.writeShort(30292);
|
||||
dataOut.writeShort(-60584);
|
||||
dataOut.writeShort(-60584);
|
||||
dataOut.writeShort(-60584);
|
||||
|
||||
InputStream in = new HorizontalDeDifferencingStream(out.createInputStream(), 4, 3, 16, ByteOrder.LITTLE_ENDIAN);
|
||||
DataInput dataIn = new LittleEndianDataInputStream(in);
|
||||
|
||||
// Row 1
|
||||
assertEquals(0, dataIn.readUnsignedShort());
|
||||
assertEquals(0, dataIn.readUnsignedShort());
|
||||
assertEquals(0, dataIn.readUnsignedShort());
|
||||
assertEquals(4660, dataIn.readUnsignedShort());
|
||||
assertEquals(30292, dataIn.readUnsignedShort());
|
||||
assertEquals(4660, dataIn.readUnsignedShort());
|
||||
assertEquals(9320, dataIn.readUnsignedShort());
|
||||
assertEquals(60584, dataIn.readUnsignedShort());
|
||||
assertEquals(9320, dataIn.readUnsignedShort());
|
||||
assertEquals(0, dataIn.readUnsignedShort());
|
||||
assertEquals(0, dataIn.readUnsignedShort());
|
||||
assertEquals(0, dataIn.readUnsignedShort());
|
||||
|
||||
// Row 2
|
||||
assertEquals(0, dataIn.readUnsignedShort());
|
||||
assertEquals(0, dataIn.readUnsignedShort());
|
||||
assertEquals(0, dataIn.readUnsignedShort());
|
||||
assertEquals(30292, dataIn.readUnsignedShort());
|
||||
assertEquals(30292, dataIn.readUnsignedShort());
|
||||
assertEquals(30292, dataIn.readUnsignedShort());
|
||||
assertEquals(60584, dataIn.readUnsignedShort());
|
||||
assertEquals(60584, dataIn.readUnsignedShort());
|
||||
assertEquals(60584, dataIn.readUnsignedShort());
|
||||
assertEquals(0, dataIn.readUnsignedShort());
|
||||
assertEquals(0, dataIn.readUnsignedShort());
|
||||
assertEquals(0, dataIn.readUnsignedShort());
|
||||
|
||||
// EOF
|
||||
assertEquals(-1, in.read());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRead4SPP8BPS() throws IOException {
|
||||
// 4 samples per pixel, 8 bits per sample (RGBA)
|
||||
byte[] data = {
|
||||
(byte) 0xff, (byte) 0x00, (byte) 0x7f, 0x00, -1, -1, -1, -1, -4, -4, -4, -4, 4, 4, 4, 4,
|
||||
0x7f, 0x7f, 0x7f, 0x7f, 1, 1, 1, 1, 4, 4, 4, 4, -4, -4, -4, -4,
|
||||
};
|
||||
|
||||
InputStream stream = new HorizontalDeDifferencingStream(new ByteArrayInputStream(data), 4, 4, 8, ByteOrder.BIG_ENDIAN);
|
||||
|
||||
// Row 1
|
||||
assertEquals(0xff, stream.read());
|
||||
assertEquals(0x00, stream.read());
|
||||
assertEquals(0x7f, stream.read());
|
||||
assertEquals(0x00, stream.read());
|
||||
|
||||
assertEquals(0xfe, stream.read());
|
||||
assertEquals(0xff, stream.read());
|
||||
assertEquals(0x7e, stream.read());
|
||||
assertEquals(0xff, stream.read());
|
||||
|
||||
assertEquals(0xfa, stream.read());
|
||||
assertEquals(0xfb, stream.read());
|
||||
assertEquals(0x7a, stream.read());
|
||||
assertEquals(0xfb, stream.read());
|
||||
|
||||
assertEquals(0xfe, stream.read());
|
||||
assertEquals(0xff, stream.read());
|
||||
assertEquals(0x7e, stream.read());
|
||||
assertEquals(0xff, stream.read());
|
||||
|
||||
// Row 2
|
||||
assertEquals(0x7f, stream.read());
|
||||
assertEquals(0x7f, stream.read());
|
||||
assertEquals(0x7f, stream.read());
|
||||
assertEquals(0x7f, stream.read());
|
||||
|
||||
assertEquals(0x80, stream.read());
|
||||
assertEquals(0x80, stream.read());
|
||||
assertEquals(0x80, stream.read());
|
||||
assertEquals(0x80, stream.read());
|
||||
|
||||
assertEquals(0x84, stream.read());
|
||||
assertEquals(0x84, stream.read());
|
||||
assertEquals(0x84, stream.read());
|
||||
assertEquals(0x84, stream.read());
|
||||
|
||||
assertEquals(0x80, stream.read());
|
||||
assertEquals(0x80, stream.read());
|
||||
assertEquals(0x80, stream.read());
|
||||
assertEquals(0x80, stream.read());
|
||||
|
||||
// EOF
|
||||
assertEquals(-1, stream.read());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadArray4SPP8BPS() throws IOException {
|
||||
// 4 samples per pixel, 8 bits per sample (RGBA)
|
||||
byte[] data = {
|
||||
(byte) 0xff, (byte) 0x00, (byte) 0x7f, 0x00, -1, -1, -1, -1, -4, -4, -4, -4, 4, 4, 4, 4,
|
||||
0x7f, 0x7f, 0x7f, 0x7f, 1, 1, 1, 1, 4, 4, 4, 4, -4, -4, -4, -4,
|
||||
};
|
||||
|
||||
InputStream stream = new HorizontalDeDifferencingStream(new ByteArrayInputStream(data), 4, 4, 8, ByteOrder.BIG_ENDIAN);
|
||||
|
||||
byte[] result = new byte[data.length];
|
||||
new DataInputStream(stream).readFully(result);
|
||||
|
||||
assertArrayEquals(
|
||||
new byte[] {
|
||||
(byte) 0xff, 0x00, 0x7f, 0x00,
|
||||
(byte) 0xfe, (byte) 0xff, 0x7e, (byte) 0xff,
|
||||
(byte) 0xfa, (byte) 0xfb, 0x7a, (byte) 0xfb,
|
||||
(byte) 0xfe, (byte) 0xff, 0x7e, (byte) 0xff,
|
||||
|
||||
0x7f, 0x7f, 0x7f, 0x7f,
|
||||
(byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80,
|
||||
(byte) 0x84, (byte) 0x84, (byte) 0x84, (byte) 0x84,
|
||||
(byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80,
|
||||
},
|
||||
result
|
||||
);
|
||||
|
||||
// EOF
|
||||
assertEquals(-1, stream.read(new byte[16]));
|
||||
assertEquals(-1, stream.read());
|
||||
}
|
||||
}
|
@@ -30,7 +30,6 @@ import com.twelvemonkeys.io.enc.Decoder;
|
||||
import com.twelvemonkeys.io.enc.DecoderAbstractTestCase;
|
||||
import com.twelvemonkeys.io.enc.DecoderStream;
|
||||
import com.twelvemonkeys.io.enc.Encoder;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
@@ -60,24 +59,15 @@ public class LZWDecoderTest extends DecoderAbstractTestCase {
|
||||
|
||||
@Test
|
||||
public void testShortBitReversedStream() throws IOException {
|
||||
InputStream stream = new DecoderStream(getClass().getResourceAsStream("/lzw/lzw-short.bin"), new LZWDecoder(true), 128);
|
||||
InputStream stream = new DecoderStream(getClass().getResourceAsStream("/lzw/lzw-short.bin"), LZWDecoder.create(true), 128);
|
||||
InputStream unpacked = new ByteArrayInputStream(new byte[512 * 3 * 5]); // Should be all 0's
|
||||
|
||||
assertSameStreamContents(unpacked, stream);
|
||||
}
|
||||
|
||||
@Ignore("Known issue")
|
||||
@Test
|
||||
public void testShortBitReversedStreamLine45To49() throws IOException {
|
||||
InputStream stream = new DecoderStream(getClass().getResourceAsStream("/lzw/lzw-short-45-49.bin"), new LZWDecoder(true), 128);
|
||||
InputStream unpacked = getClass().getResourceAsStream("/lzw/unpacked-short-45-49.bin");
|
||||
|
||||
assertSameStreamContents(unpacked, stream);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLongStream() throws IOException {
|
||||
InputStream stream = new DecoderStream(getClass().getResourceAsStream("/lzw/lzw-long.bin"), new LZWDecoder(), 1024);
|
||||
InputStream stream = new DecoderStream(getClass().getResourceAsStream("/lzw/lzw-long.bin"), LZWDecoder.create(false), 1024);
|
||||
InputStream unpacked = getClass().getResourceAsStream("/lzw/unpacked-long.bin");
|
||||
|
||||
assertSameStreamContents(unpacked, stream);
|
||||
@@ -111,7 +101,7 @@ public class LZWDecoderTest extends DecoderAbstractTestCase {
|
||||
|
||||
@Override
|
||||
public Decoder createDecoder() {
|
||||
return new LZWDecoder();
|
||||
return LZWDecoder.create(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -52,10 +52,15 @@ public class TIFFImageReaderTest extends ImageReaderAbstractTestCase<TIFFImageRe
|
||||
new TestData(getClassLoaderResource("/tiff/sm_colors_tile.tif"), new Dimension(64, 64)), // RGB, uncompressed, tiled
|
||||
new TestData(getClassLoaderResource("/tiff/sm_colors_pb_tile.tif"), new Dimension(64, 64)), // RGB, PackBits compressed, tiled
|
||||
new TestData(getClassLoaderResource("/tiff/galaxy.tif"), new Dimension(965, 965)), // RGB, LZW compressed
|
||||
new TestData(getClassLoaderResource("/tiff/quad-lzw.tif"), new Dimension(512, 384)), // RGB, Old spec (reversed) LZW compressed, tiled
|
||||
new TestData(getClassLoaderResource("/tiff/bali.tif"), new Dimension(725, 489)), // Palette-based, LZW compressed
|
||||
new TestData(getClassLoaderResource("/tiff/f14.tif"), new Dimension(640, 480)), // Gray, uncompressed
|
||||
new TestData(getClassLoaderResource("/tiff/marbles.tif"), new Dimension(1419, 1001)), // RGB, LZW compressed w/predictor
|
||||
new TestData(getClassLoaderResource("/tiff/chifley_logo.tif"), new Dimension(591, 177)) // CMYK, uncompressed
|
||||
new TestData(getClassLoaderResource("/tiff/chifley_logo.tif"), new Dimension(591, 177)), // CMYK, uncompressed
|
||||
new TestData(getClassLoaderResource("/tiff/ycbcr-cat.tif"), new Dimension(250, 325)), // YCbCr, LZW compressed
|
||||
new TestData(getClassLoaderResource("/tiff/quad-jpeg.tif"), new Dimension(512, 384)), // YCbCr, JPEG compressed, striped
|
||||
new TestData(getClassLoaderResource("/tiff/smallliz.tif"), new Dimension(160, 160)), // YCbCr, Old-Style JPEG compressed (full JFIF stream)
|
||||
new TestData(getClassLoaderResource("/tiff/zackthecat.tif"), new Dimension(234, 213)) // YCbCr, Old-Style JPEG compressed (tables, no JFIF stream)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -88,4 +93,6 @@ public class TIFFImageReaderTest extends ImageReaderAbstractTestCase<TIFFImageRe
|
||||
protected List<String> getMIMETypes() {
|
||||
return Arrays.asList("image/tiff");
|
||||
}
|
||||
|
||||
// TODO: Test YCbCr colors
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2012, Harald Kuhr
|
||||
* Copyright (c) 2013, Harald Kuhr
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
@@ -28,20 +28,24 @@
|
||||
|
||||
package com.twelvemonkeys.imageio.plugins.tiff;
|
||||
|
||||
import com.twelvemonkeys.io.enc.Decoder;
|
||||
import com.twelvemonkeys.io.InputStreamAbstractTestCase;
|
||||
import org.junit.Ignore;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* CCITT Group 3 One-Dimensional (G31D) "No EOLs" Decoder.
|
||||
* YCbCrUpsamplerStreamTest
|
||||
*
|
||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||
* @author last modified by $Author: haraldk$
|
||||
* @version $Id: G31DDecoder.java,v 1.0 23.05.12 15:55 haraldk Exp$
|
||||
* @version $Id: YCbCrUpsamplerStreamTest.java,v 1.0 31.01.13 14:35 haraldk Exp$
|
||||
*/
|
||||
final class G31DDecoder implements Decoder {
|
||||
public int decode(final InputStream stream, final byte[] buffer) throws IOException {
|
||||
throw new UnsupportedOperationException("Method decode not implemented"); // TODO: Implement
|
||||
@Ignore
|
||||
public class YCbCrUpsamplerStreamTest extends InputStreamAbstractTestCase {
|
||||
// TODO: Implement + add @Ignore for all tests that makes no sense for this class.
|
||||
@Override
|
||||
protected InputStream makeInputStream(byte[] pBytes) {
|
||||
return new YCbCrUpsamplerStream(new ByteArrayInputStream(pBytes), new int[] {2, 2}, TIFFExtension.YCBCR_POSITIONING_CENTERED, pBytes.length / 4, null);
|
||||
}
|
||||
}
|
Binary file not shown.
Binary file not shown.
BIN
imageio/imageio-tiff/src/test/resources/tiff/quad-jpeg.tif
Executable file
BIN
imageio/imageio-tiff/src/test/resources/tiff/quad-jpeg.tif
Executable file
Binary file not shown.
BIN
imageio/imageio-tiff/src/test/resources/tiff/quad-lzw.tif
Normal file
BIN
imageio/imageio-tiff/src/test/resources/tiff/quad-lzw.tif
Normal file
Binary file not shown.
BIN
imageio/imageio-tiff/src/test/resources/tiff/smallliz.tif
Executable file
BIN
imageio/imageio-tiff/src/test/resources/tiff/smallliz.tif
Executable file
Binary file not shown.
BIN
imageio/imageio-tiff/src/test/resources/tiff/ycbcr-cat.tif
Normal file
BIN
imageio/imageio-tiff/src/test/resources/tiff/ycbcr-cat.tif
Normal file
Binary file not shown.
BIN
imageio/imageio-tiff/src/test/resources/tiff/zackthecat.tif
Normal file
BIN
imageio/imageio-tiff/src/test/resources/tiff/zackthecat.tif
Normal file
Binary file not shown.
@@ -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>
|
||||
|
@@ -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 {
|
||||
|
@@ -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
|
||||
|
||||
*/
|
@@ -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>
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
|
||||
}
|
@@ -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);
|
||||
}
|
||||
|
||||
|
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -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();
|
@@ -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");
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user