TMI-TIFF: Fixed several bugs in the LittleEndianDataInputStream needed for proper TIFF output (should affect other things as well...)

This commit is contained in:
Harald Kuhr 2013-02-15 12:55:37 +01:00
parent ed6223fcab
commit d8867736b7
4 changed files with 978 additions and 66 deletions

View File

@ -158,7 +158,7 @@ public class LittleEndianDataInputStream extends FilterInputStream implements Da
throw new EOFException(); throw new EOFException();
} }
return (short) (((byte2 << 24) >>> 16) + (byte1 << 24) >>> 24); return (short) (((byte2 << 24) >>> 16) | (byte1 << 24) >>> 24);
} }
/** /**
@ -198,7 +198,7 @@ public class LittleEndianDataInputStream extends FilterInputStream implements Da
throw new EOFException(); throw new EOFException();
} }
return (char) (((byte2 << 24) >>> 16) + ((byte1 << 24) >>> 24)); return (char) (((byte2 << 24) >>> 16) | ((byte1 << 24) >>> 24));
} }
@ -221,8 +221,8 @@ public class LittleEndianDataInputStream extends FilterInputStream implements Da
throw new EOFException(); throw new EOFException();
} }
return (byte4 << 24) + ((byte3 << 24) >>> 8) return (byte4 << 24) | ((byte3 << 24) >>> 8)
+ ((byte2 << 24) >>> 16) + ((byte1 << 24) >>> 24); | ((byte2 << 24) >>> 16) | ((byte1 << 24) >>> 24);
} }
/** /**
@ -248,10 +248,10 @@ public class LittleEndianDataInputStream extends FilterInputStream implements Da
throw new EOFException(); throw new EOFException();
} }
return (byte8 << 56) + ((byte7 << 56) >>> 8) return (byte8 << 56) | ((byte7 << 56) >>> 8)
+ ((byte6 << 56) >>> 16) + ((byte5 << 56) >>> 24) | ((byte6 << 56) >>> 16) | ((byte5 << 56) >>> 24)
+ ((byte4 << 56) >>> 32) + ((byte3 << 56) >>> 40) | ((byte4 << 56) >>> 32) | ((byte3 << 56) >>> 40)
+ ((byte2 << 56) >>> 48) + ((byte1 << 56) >>> 56); | ((byte2 << 56) >>> 48) | ((byte1 << 56) >>> 56);
} }
/** /**

View File

@ -0,0 +1,208 @@
/*
* Copyright (c) 2013, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name "TwelveMonkeys" nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.io;
import org.junit.Test;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import static org.junit.Assert.*;
/**
* LittleEndianDataInputStreamTest
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: LittleEndianDataInputStreamTest.java,v 1.0 15.02.13 11:04 haraldk Exp$
*/
public class LittleEndianDataInputStreamTest {
@Test
public void testReadBoolean() throws IOException {
LittleEndianDataInputStream data = new LittleEndianDataInputStream(new ByteArrayInputStream(new byte[] {0, 1, 0x7f, (byte) 0xff}));
assertFalse(data.readBoolean());
assertTrue(data.readBoolean());
assertTrue(data.readBoolean());
assertTrue(data.readBoolean());
}
@Test
public void testReadByte() throws IOException {
LittleEndianDataInputStream data = new LittleEndianDataInputStream(new ByteArrayInputStream(
new byte[] {
(byte) 0x00, (byte) 0x00,
(byte) 0x01, (byte) 0x00,
(byte) 0xff, (byte) 0xff,
(byte) 0x00, (byte) 0x80,
(byte) 0xff, (byte) 0x7f,
(byte) 0x00, (byte) 0x01,
}
));
assertEquals(0, data.readByte());
assertEquals(0, data.readByte());
assertEquals(1, data.readByte());
assertEquals(0, data.readByte());
assertEquals(-1, data.readByte());
assertEquals(-1, data.readByte());
assertEquals(0, data.readByte());
assertEquals(Byte.MIN_VALUE, data.readByte());
assertEquals(-1, data.readByte());
assertEquals(Byte.MAX_VALUE, data.readByte());
assertEquals(0, data.readByte());
assertEquals(1, data.readByte());
}
@Test
public void testReadUnsignedByte() throws IOException {
LittleEndianDataInputStream data = new LittleEndianDataInputStream(new ByteArrayInputStream(
new byte[] {
(byte) 0x00, (byte) 0x00,
(byte) 0x01, (byte) 0x00,
(byte) 0xff, (byte) 0xff,
(byte) 0x00, (byte) 0x80,
(byte) 0xff, (byte) 0x7f,
(byte) 0x00, (byte) 0x01,
}
));
assertEquals(0, data.readUnsignedByte());
assertEquals(0, data.readUnsignedByte());
assertEquals(1, data.readUnsignedByte());
assertEquals(0, data.readUnsignedByte());
assertEquals(255, data.readUnsignedByte());
assertEquals(255, data.readUnsignedByte());
assertEquals(0, data.readUnsignedByte());
assertEquals(128, data.readUnsignedByte());
assertEquals(255, data.readUnsignedByte());
assertEquals(Byte.MAX_VALUE, data.readUnsignedByte());
assertEquals(0, data.readUnsignedByte());
assertEquals(1, data.readUnsignedByte());
}
@Test
public void testReadShort() throws IOException {
LittleEndianDataInputStream data = new LittleEndianDataInputStream(new ByteArrayInputStream(
new byte[] {
(byte) 0x00, (byte) 0x00,
(byte) 0x01, (byte) 0x00,
(byte) 0xff, (byte) 0xff,
(byte) 0x00, (byte) 0x80,
(byte) 0xff, (byte) 0x7f,
(byte) 0x00, (byte) 0x01,
}
));
assertEquals(0, data.readShort());
assertEquals(1, data.readShort());
assertEquals(-1, data.readShort());
assertEquals(Short.MIN_VALUE, data.readShort());
assertEquals(Short.MAX_VALUE, data.readShort());
assertEquals(256, data.readShort());
}
@Test
public void testReadUnsignedShort() throws IOException {
LittleEndianDataInputStream data = new LittleEndianDataInputStream(new ByteArrayInputStream(
new byte[] {
(byte) 0x00, (byte) 0x00,
(byte) 0x01, (byte) 0x00,
(byte) 0xff, (byte) 0xff,
(byte) 0x00, (byte) 0x80,
(byte) 0xff, (byte) 0x7f,
(byte) 0x00, (byte) 0x01,
}
));
assertEquals(0, data.readUnsignedShort());
assertEquals(1, data.readUnsignedShort());
assertEquals(Short.MAX_VALUE * 2 + 1, data.readUnsignedShort());
assertEquals(Short.MAX_VALUE + 1, data.readUnsignedShort());
assertEquals(Short.MAX_VALUE, data.readUnsignedShort());
assertEquals(256, data.readUnsignedShort());
}
@Test
public void testReadInt() throws IOException {
LittleEndianDataInputStream data = new LittleEndianDataInputStream(new ByteArrayInputStream(
new byte[] {
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
(byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00,
(byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x80,
(byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x7f,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01,
(byte) 0xff, (byte) 0x00, (byte) 0xff, (byte) 0x00,
(byte) 0x00, (byte) 0xff, (byte) 0x00, (byte) 0xff,
(byte) 0xbe, (byte) 0xba, (byte) 0xfe, (byte) 0xca,
(byte) 0xca, (byte) 0xfe, (byte) 0xd0, (byte) 0x0d,
}
));
assertEquals(0, data.readInt());
assertEquals(1, data.readInt());
assertEquals(-1, data.readInt());
assertEquals(Integer.MIN_VALUE, data.readInt());
assertEquals(Integer.MAX_VALUE, data.readInt());
assertEquals(16777216, data.readInt());
assertEquals(0xff00ff, data.readInt());
assertEquals(0xff00ff00, data.readInt());
assertEquals(0xCafeBabe, data.readInt());
assertEquals(0x0dd0feca, data.readInt());
}
@Test
public void testReadLong() throws IOException {
LittleEndianDataInputStream data = new LittleEndianDataInputStream(new ByteArrayInputStream(
new byte[] {
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
(byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
(byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x80,
(byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x7f,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01,
(byte) 0x0d, (byte) 0xd0, (byte) 0xfe, (byte) 0xca, (byte) 0xbe, (byte) 0xba, (byte) 0xfe, (byte) 0xca,
}
));
assertEquals(0, data.readLong());
assertEquals(1, data.readLong());
assertEquals(-1, data.readLong());
assertEquals(Long.MIN_VALUE, data.readLong());
assertEquals(Long.MAX_VALUE, data.readLong());
assertEquals(72057594037927936L, data.readLong());
assertEquals(0xCafeBabeL << 32 | 0xCafeD00dL, data.readLong());
}
}

View File

@ -30,6 +30,7 @@ package com.twelvemonkeys.image;
import com.twelvemonkeys.imageio.util.ProgressListenerBase; import com.twelvemonkeys.imageio.util.ProgressListenerBase;
import com.twelvemonkeys.lang.StringUtil; import com.twelvemonkeys.lang.StringUtil;
import com.twelvemonkeys.util.LRUHashMap;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
import javax.imageio.ImageReadParam; import javax.imageio.ImageReadParam;
@ -38,17 +39,17 @@ import javax.imageio.ImageTypeSpecifier;
import javax.imageio.stream.ImageInputStream; import javax.imageio.stream.ImageInputStream;
import javax.swing.*; import javax.swing.*;
import java.awt.*; 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.BufferedImage;
import java.awt.image.DataBuffer; import java.awt.image.DataBuffer;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.Iterator; import java.lang.ref.Reference;
import java.util.Random; import java.lang.ref.SoftReference;
import java.util.concurrent.CountDownLatch; import java.util.*;
import java.util.concurrent.ExecutorService; import java.util.List;
import java.util.concurrent.Executors; import java.util.concurrent.*;
import java.util.concurrent.TimeUnit;
/** /**
* MappedBufferImage * MappedBufferImage
@ -59,7 +60,7 @@ import java.util.concurrent.TimeUnit;
*/ */
public class MappedBufferImage { public class MappedBufferImage {
private static int threads = Runtime.getRuntime().availableProcessors(); 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 { public static void main(String[] args) throws IOException {
int argIndex = 0; int argIndex = 0;
@ -91,8 +92,9 @@ public class MappedBufferImage {
// TODO: Negotiate best layout according to the GraphicsConfiguration. // TODO: Negotiate best layout according to the GraphicsConfiguration.
w = reader.getWidth(0); int sub = 1;
h = reader.getHeight(0); w = reader.getWidth(0) / sub;
h = reader.getHeight(0) / sub;
// GraphicsConfiguration configuration = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration(); // GraphicsConfiguration configuration = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration();
// ColorModel cm2 = configuration.getColorModel(cm.getTransparency()); // ColorModel cm2 = configuration.getColorModel(cm.getTransparency());
@ -111,8 +113,11 @@ public class MappedBufferImage {
System.out.println("image = " + image); System.out.println("image = " + image);
// TODO: Display image while reading
ImageReadParam param = reader.getDefaultReadParam(); ImageReadParam param = reader.getDefaultReadParam();
param.setDestination(image); param.setDestination(image);
param.setSourceSubsampling(sub, sub, 0, 0);
reader.addIIOReadProgressListener(new ConsoleProgressListener()); reader.addIIOReadProgressListener(new ConsoleProgressListener());
reader.read(0, param); reader.read(0, param);
@ -166,7 +171,7 @@ public class MappedBufferImage {
return size; return size;
} }
}; };
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
JScrollPane scroll = new JScrollPane(new ImageComponent(image)); JScrollPane scroll = new JScrollPane(new ImageComponent(image));
scroll.setBorder(BorderFactory.createEmptyBorder()); scroll.setBorder(BorderFactory.createEmptyBorder());
frame.add(scroll); frame.add(scroll);
@ -184,13 +189,24 @@ public class MappedBufferImage {
// NOTE: The createCompatibleDestImage takes the byte order/layout into account, unlike the cm.createCompatibleWritableRaster // 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 BufferedImage output = new ResampleOp(width, height).createCompatibleDestImage(image, null);
final int inStep = (int) Math.ceil(image.getHeight() / (double) threads); final int steps = threads * height / 100;
final int outStep = (int) Math.ceil(height / (double) threads); 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 // 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 inY = i * inStep;
final int outY = i * outStep; final int outY = i * outStep;
final int inHeight = Math.min(inStep, image.getHeight() - inY); final int inHeight = Math.min(inStep, image.getHeight() - inY);
@ -200,10 +216,12 @@ public class MappedBufferImage {
try { try {
BufferedImage in = image.getSubimage(0, inY, image.getWidth(), inHeight); BufferedImage in = image.getSubimage(0, inY, image.getWidth(), inHeight);
BufferedImage out = output.getSubimage(0, outY, width, outHeight); BufferedImage out = output.getSubimage(0, outY, width, outHeight);
new ResampleOp(width, outHeight, ResampleOp.FILTER_LANCZOS).filter(in, out); new ResampleOp(width, outHeight, ResampleOp.FILTER_TRIANGLE).filter(in, out);
// new ResampleOp(width, outHeight, ResampleOp.FILTER_LANCZOS).resample(in, out, ResampleOp.createFilter(ResampleOp.FILTER_LANCZOS)); // new ResampleOp(width, outHeight, ResampleOp.FILTER_LANCZOS).filter(in, out);
// BufferedImage out = new ResampleOp(width, outHeight, ResampleOp.FILTER_LANCZOS).filter(in, null);
// ImageUtil.drawOnto(output.getSubimage(0, outY, width, outHeight), out); for (int j = 0; j < dotsPerStep; j++) {
System.out.print(".");
}
} }
catch (RuntimeException e) { catch (RuntimeException e) {
e.printStackTrace(); 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; Boolean done = null;
try { try {
done = latch.await(5L, TimeUnit.MINUTES); done = latch.await(5L, TimeUnit.MINUTES);
} }
catch (InterruptedException ignore) { 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); System.out.println("image = " + output);
return output; return output;
} }
@ -358,10 +374,12 @@ public class MappedBufferImage {
private static class ImageComponent extends JComponent implements Scrollable { private static class ImageComponent extends JComponent implements Scrollable {
private final BufferedImage image; private final BufferedImage image;
private Paint texture; private Paint texture;
double zoom = 1; private double zoom = 1;
public ImageComponent(final BufferedImage image) { 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; this.image = image;
} }
@ -370,6 +388,68 @@ public class MappedBufferImage {
super.addNotify(); super.addNotify();
texture = createTexture(); 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() { private Paint createTexture() {
@ -392,10 +472,17 @@ public class MappedBufferImage {
@Override @Override
protected void paintComponent(Graphics g) { 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, // TODO: Figure out why mouse wheel/track pad scroll repaints entire component,
// unlike using the scroll bars of the JScrollPane. // unlike using the scroll bars of the JScrollPane.
// Consider creating a custom mouse wheel listener as a workaround. // 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 // We want to paint only the visible part of the image
Rectangle visible = getVisibleRect(); Rectangle visible = getVisibleRect();
Rectangle clip = g.getClipBounds(); Rectangle clip = g.getClipBounds();
@ -405,9 +492,28 @@ public class MappedBufferImage {
g2.setPaint(texture); g2.setPaint(texture);
g2.fillRect(rect.x, rect.y, rect.width, rect.height); 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) { if (zoom != 1) {
AffineTransform transform = AffineTransform.getScaleInstance(zoom, zoom); // NOTE: This helps mostly when scaling up, or scaling down less than 50%
g2.setTransform(transform); 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(); long start = System.currentTimeMillis();
@ -415,39 +521,308 @@ public class MappedBufferImage {
System.err.println("repaint: " + (System.currentTimeMillis() - start) + " ms"); 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 { try {
// Paint tiles of the image, to preserve memory // Paint tiles of the image, to preserve memory
int sliceSize = 200; final int tileSize = 200;
int slicesW = rect.width / sliceSize; int tilesW = 1 + rect.width / tileSize;
int slicesH = rect.height / sliceSize; int tilesH = 1 + rect.height / tileSize;
for (int sliceY = 0; sliceY <= slicesH; sliceY++) { for (int yTile = 0; yTile <= tilesH; yTile++) {
for (int sliceX = 0; sliceX <= slicesW; sliceX++) { for (int xTile = 0; xTile <= tilesW; xTile++) {
int x = rect.x + sliceX * sliceSize; // Image (source) coordinates
int y = rect.y + sliceY * sliceSize; 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 w = xTile == tilesW ? Math.min(tileSize, rect.x + rect.width - x) : tileSize;
int h = sliceY == slicesH ? Math.min(sliceSize, rect.y + rect.height - y) : sliceSize; int h = yTile == tilesH ? Math.min(tileSize, rect.y + rect.height - y) : tileSize;
if (w == 0 || h == 0) { if (w == 0 || h == 0) {
continue; continue;
} }
// System.err.printf("%04d, %04d, %04d, %04d%n", x, y, w, h); // 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);
} }
} }
// BufferedImage img = image.getSubimage(rect.x, rect.y, rect.width, rect.height); // System.err.printf("Tile miss: [%d, %d]\n", dstX, dstY);
// g2.drawImage(img, rect.x, rect.y, null);
// 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());
}
}
});
}
}
}
} }
catch (NullPointerException e) { catch (NullPointerException e) {
// e.printStackTrace(); // 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..? // 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 repaint(); // NOTE: Might cause a brief flash while the component is redrawn
} }
} }
@ -476,12 +851,68 @@ public class MappedBufferImage {
} }
public boolean getScrollableTracksViewportWidth() { public boolean getScrollableTracksViewportWidth() {
return false; return getWidth() > getPreferredSize().width;
} }
public boolean getScrollableTracksViewportHeight() { 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; return false;
} }
@Override
public void removeLRU() {
while (maxSize <= currentSize) { // NOTE: maxSize here is mem size
super.removeLRU();
}
}
} }
private static class PaintDotsTask implements Runnable { private static class PaintDotsTask implements Runnable {

View File

@ -28,6 +28,13 @@
package com.twelvemonkeys.util; package com.twelvemonkeys.util;
import com.twelvemonkeys.io.FileUtil;
import java.io.*;
import java.util.*;
import static com.twelvemonkeys.lang.Validate.notNull;
/** /**
* PersistentMap * PersistentMap
* *
@ -35,27 +42,293 @@ package com.twelvemonkeys.util;
* @author last modified by $Author: haraldk$ * @author last modified by $Author: haraldk$
* @version $Id: PersistentMap.java,v 1.0 May 13, 2009 2:31:29 PM haraldk Exp$ * @version $Id: PersistentMap.java,v 1.0 May 13, 2009 2:31:29 PM haraldk Exp$
*/ */
public class PersistentMap { public class PersistentMap<K extends Serializable, V extends Serializable> extends AbstractMap<K, V>{
// TODO: Implement Map public static final FileFilter DIRECTORIES = new FileFilter() {
// TODO: Delta synchronization (db?) 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 Persistent format
Header Header
File ID 4-8 bytes File ID 4-8 bytes
Size Size (entries)
Entry pointer array block PersistentEntry pointer array block (PersistentEntry 0)
Size Size (bytes)
Next entry pointer block address Next entry pointer block address (0 if last)
Entry 1 address PersistentEntry 1 address/offset + key
... ...
Entry n address PersistentEntry n address/offset + key
Entry 1 PersistentEntry 1
Size (bytes)?
Serialized value or pointer array block
... ...
Entry n PersistentEntry n
Size (bytes)?
Serialized value or pointer array block
*/ */