From d8867736b72a59e938ad5906043fbbd0dbf41d0b Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Fri, 15 Feb 2013 12:55:37 +0100 Subject: [PATCH] TMI-TIFF: Fixed several bugs in the LittleEndianDataInputStream needed for proper TIFF output (should affect other things as well...) --- .../io/LittleEndianDataInputStream.java | 16 +- .../io/LittleEndianDataInputStreamTest.java | 208 +++++++ .../image/MappedBufferImage.java | 521 ++++++++++++++++-- .../com/twelvemonkeys/util/PersistentMap.java | 299 +++++++++- 4 files changed, 978 insertions(+), 66 deletions(-) create mode 100644 common/common-io/src/test/java/com/twelvemonkeys/io/LittleEndianDataInputStreamTest.java diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/LittleEndianDataInputStream.java b/common/common-io/src/main/java/com/twelvemonkeys/io/LittleEndianDataInputStream.java index adceaead..c20d8000 100755 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/LittleEndianDataInputStream.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/io/LittleEndianDataInputStream.java @@ -158,7 +158,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 +198,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 +221,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 +248,10 @@ public class LittleEndianDataInputStream extends FilterInputStream implements Da throw new EOFException(); } - return (byte8 << 56) + ((byte7 << 56) >>> 8) - + ((byte6 << 56) >>> 16) + ((byte5 << 56) >>> 24) - + ((byte4 << 56) >>> 32) + ((byte3 << 56) >>> 40) - + ((byte2 << 56) >>> 48) + ((byte1 << 56) >>> 56); + return (byte8 << 56) | ((byte7 << 56) >>> 8) + | ((byte6 << 56) >>> 16) | ((byte5 << 56) >>> 24) + | ((byte4 << 56) >>> 32) | ((byte3 << 56) >>> 40) + | ((byte2 << 56) >>> 48) | ((byte1 << 56) >>> 56); } /** diff --git a/common/common-io/src/test/java/com/twelvemonkeys/io/LittleEndianDataInputStreamTest.java b/common/common-io/src/test/java/com/twelvemonkeys/io/LittleEndianDataInputStreamTest.java new file mode 100644 index 00000000..1f7c551b --- /dev/null +++ b/common/common-io/src/test/java/com/twelvemonkeys/io/LittleEndianDataInputStreamTest.java @@ -0,0 +1,208 @@ +/* + * Copyright (c) 2013, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.io; + +import org.junit.Test; + +import java.io.ByteArrayInputStream; +import java.io.IOException; + +import static org.junit.Assert.*; + +/** + * LittleEndianDataInputStreamTest + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: LittleEndianDataInputStreamTest.java,v 1.0 15.02.13 11:04 haraldk Exp$ + */ +public class LittleEndianDataInputStreamTest { + @Test + public void testReadBoolean() throws IOException { + LittleEndianDataInputStream data = new LittleEndianDataInputStream(new ByteArrayInputStream(new byte[] {0, 1, 0x7f, (byte) 0xff})); + assertFalse(data.readBoolean()); + assertTrue(data.readBoolean()); + assertTrue(data.readBoolean()); + assertTrue(data.readBoolean()); + } + + @Test + public void testReadByte() throws IOException { + LittleEndianDataInputStream data = new LittleEndianDataInputStream(new ByteArrayInputStream( + new byte[] { + (byte) 0x00, (byte) 0x00, + (byte) 0x01, (byte) 0x00, + (byte) 0xff, (byte) 0xff, + (byte) 0x00, (byte) 0x80, + (byte) 0xff, (byte) 0x7f, + (byte) 0x00, (byte) 0x01, + } + + )); + + assertEquals(0, data.readByte()); + assertEquals(0, data.readByte()); + assertEquals(1, data.readByte()); + assertEquals(0, data.readByte()); + assertEquals(-1, data.readByte()); + assertEquals(-1, data.readByte()); + assertEquals(0, data.readByte()); + assertEquals(Byte.MIN_VALUE, data.readByte()); + assertEquals(-1, data.readByte()); + assertEquals(Byte.MAX_VALUE, data.readByte()); + assertEquals(0, data.readByte()); + assertEquals(1, data.readByte()); + } + + @Test + public void testReadUnsignedByte() throws IOException { + LittleEndianDataInputStream data = new LittleEndianDataInputStream(new ByteArrayInputStream( + new byte[] { + (byte) 0x00, (byte) 0x00, + (byte) 0x01, (byte) 0x00, + (byte) 0xff, (byte) 0xff, + (byte) 0x00, (byte) 0x80, + (byte) 0xff, (byte) 0x7f, + (byte) 0x00, (byte) 0x01, + } + + )); + + assertEquals(0, data.readUnsignedByte()); + assertEquals(0, data.readUnsignedByte()); + assertEquals(1, data.readUnsignedByte()); + assertEquals(0, data.readUnsignedByte()); + assertEquals(255, data.readUnsignedByte()); + assertEquals(255, data.readUnsignedByte()); + assertEquals(0, data.readUnsignedByte()); + assertEquals(128, data.readUnsignedByte()); + assertEquals(255, data.readUnsignedByte()); + assertEquals(Byte.MAX_VALUE, data.readUnsignedByte()); + assertEquals(0, data.readUnsignedByte()); + assertEquals(1, data.readUnsignedByte()); + } + + @Test + public void testReadShort() throws IOException { + LittleEndianDataInputStream data = new LittleEndianDataInputStream(new ByteArrayInputStream( + new byte[] { + (byte) 0x00, (byte) 0x00, + (byte) 0x01, (byte) 0x00, + (byte) 0xff, (byte) 0xff, + (byte) 0x00, (byte) 0x80, + (byte) 0xff, (byte) 0x7f, + (byte) 0x00, (byte) 0x01, + } + + )); + + assertEquals(0, data.readShort()); + assertEquals(1, data.readShort()); + assertEquals(-1, data.readShort()); + assertEquals(Short.MIN_VALUE, data.readShort()); + assertEquals(Short.MAX_VALUE, data.readShort()); + assertEquals(256, data.readShort()); + } + + @Test + public void testReadUnsignedShort() throws IOException { + LittleEndianDataInputStream data = new LittleEndianDataInputStream(new ByteArrayInputStream( + new byte[] { + (byte) 0x00, (byte) 0x00, + (byte) 0x01, (byte) 0x00, + (byte) 0xff, (byte) 0xff, + (byte) 0x00, (byte) 0x80, + (byte) 0xff, (byte) 0x7f, + (byte) 0x00, (byte) 0x01, + } + + )); + + assertEquals(0, data.readUnsignedShort()); + assertEquals(1, data.readUnsignedShort()); + assertEquals(Short.MAX_VALUE * 2 + 1, data.readUnsignedShort()); + assertEquals(Short.MAX_VALUE + 1, data.readUnsignedShort()); + assertEquals(Short.MAX_VALUE, data.readUnsignedShort()); + assertEquals(256, data.readUnsignedShort()); + } + + @Test + public void testReadInt() throws IOException { + LittleEndianDataInputStream data = new LittleEndianDataInputStream(new ByteArrayInputStream( + new byte[] { + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x80, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x7f, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01, + (byte) 0xff, (byte) 0x00, (byte) 0xff, (byte) 0x00, + (byte) 0x00, (byte) 0xff, (byte) 0x00, (byte) 0xff, + (byte) 0xbe, (byte) 0xba, (byte) 0xfe, (byte) 0xca, + (byte) 0xca, (byte) 0xfe, (byte) 0xd0, (byte) 0x0d, + } + + )); + + assertEquals(0, data.readInt()); + assertEquals(1, data.readInt()); + assertEquals(-1, data.readInt()); + assertEquals(Integer.MIN_VALUE, data.readInt()); + assertEquals(Integer.MAX_VALUE, data.readInt()); + assertEquals(16777216, data.readInt()); + assertEquals(0xff00ff, data.readInt()); + assertEquals(0xff00ff00, data.readInt()); + assertEquals(0xCafeBabe, data.readInt()); + assertEquals(0x0dd0feca, data.readInt()); + } + + @Test + public void testReadLong() throws IOException { + LittleEndianDataInputStream data = new LittleEndianDataInputStream(new ByteArrayInputStream( + new byte[] { + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x80, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x7f, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01, + (byte) 0x0d, (byte) 0xd0, (byte) 0xfe, (byte) 0xca, (byte) 0xbe, (byte) 0xba, (byte) 0xfe, (byte) 0xca, + } + + )); + + assertEquals(0, data.readLong()); + assertEquals(1, data.readLong()); + assertEquals(-1, data.readLong()); + assertEquals(Long.MIN_VALUE, data.readLong()); + assertEquals(Long.MAX_VALUE, data.readLong()); + assertEquals(72057594037927936L, data.readLong()); + assertEquals(0xCafeBabeL << 32 | 0xCafeD00dL, data.readLong()); + } +} diff --git a/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/MappedBufferImage.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/MappedBufferImage.java index 7a31a119..8d69ffce 100644 --- a/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/MappedBufferImage.java +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/MappedBufferImage.java @@ -30,6 +30,7 @@ package com.twelvemonkeys.image; import com.twelvemonkeys.imageio.util.ProgressListenerBase; import com.twelvemonkeys.lang.StringUtil; +import com.twelvemonkeys.util.LRUHashMap; import javax.imageio.ImageIO; import javax.imageio.ImageReadParam; @@ -38,17 +39,17 @@ import javax.imageio.ImageTypeSpecifier; import javax.imageio.stream.ImageInputStream; import javax.swing.*; import java.awt.*; -import java.awt.geom.AffineTransform; +import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; import java.awt.image.BufferedImage; import java.awt.image.DataBuffer; import java.io.File; import java.io.IOException; -import java.util.Iterator; -import java.util.Random; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; +import java.lang.ref.Reference; +import java.lang.ref.SoftReference; +import java.util.*; +import java.util.List; +import java.util.concurrent.*; /** * MappedBufferImage @@ -59,7 +60,7 @@ import java.util.concurrent.TimeUnit; */ public class MappedBufferImage { private static int threads = Runtime.getRuntime().availableProcessors(); - private static ExecutorService executorService = Executors.newFixedThreadPool(threads); + private static ExecutorService executorService = Executors.newFixedThreadPool(threads * 4); public static void main(String[] args) throws IOException { int argIndex = 0; @@ -91,8 +92,9 @@ public class MappedBufferImage { // TODO: Negotiate best layout according to the GraphicsConfiguration. - w = reader.getWidth(0); - h = reader.getHeight(0); + int sub = 1; + w = reader.getWidth(0) / sub; + h = reader.getHeight(0) / sub; // GraphicsConfiguration configuration = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration(); // ColorModel cm2 = configuration.getColorModel(cm.getTransparency()); @@ -111,8 +113,11 @@ public class MappedBufferImage { System.out.println("image = " + image); + // TODO: Display image while reading + ImageReadParam param = reader.getDefaultReadParam(); param.setDestination(image); + param.setSourceSubsampling(sub, sub, 0, 0); reader.addIIOReadProgressListener(new ConsoleProgressListener()); reader.read(0, param); @@ -166,7 +171,7 @@ public class MappedBufferImage { return size; } }; - frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); JScrollPane scroll = new JScrollPane(new ImageComponent(image)); scroll.setBorder(BorderFactory.createEmptyBorder()); frame.add(scroll); @@ -184,13 +189,24 @@ public class MappedBufferImage { // NOTE: The createCompatibleDestImage takes the byte order/layout into account, unlike the cm.createCompatibleWritableRaster final BufferedImage output = new ResampleOp(width, height).createCompatibleDestImage(image, null); - final int inStep = (int) Math.ceil(image.getHeight() / (double) threads); - final int outStep = (int) Math.ceil(height / (double) threads); + final int steps = threads * height / 100; + final int inStep = (int) Math.ceil(image.getHeight() / (double) steps); + final int outStep = (int) Math.ceil(height / (double) steps); - final CountDownLatch latch = new CountDownLatch(threads); + final CountDownLatch latch = new CountDownLatch(steps); + + // System.out.println("Starting image scale on single thread, waiting for execution to complete..."); +// BufferedImage output = new ResampleOp(width, height, ResampleOp.FILTER_LANCZOS).filter(image, null); + System.out.printf("Started image scale on %d threads, waiting for execution to complete...\n", threads); + + System.out.print("["); + final int dotsPerStep = 78 / steps; + for (int j = 0; j < 78 - (steps * dotsPerStep); j++) { + System.out.print("."); + } // Resample image in slices - for (int i = 0; i < threads; i++) { + for (int i = 0; i < steps; i++) { final int inY = i * inStep; final int outY = i * outStep; final int inHeight = Math.min(inStep, image.getHeight() - inY); @@ -200,10 +216,12 @@ public class MappedBufferImage { try { BufferedImage in = image.getSubimage(0, inY, image.getWidth(), inHeight); BufferedImage out = output.getSubimage(0, outY, width, outHeight); - new ResampleOp(width, outHeight, ResampleOp.FILTER_LANCZOS).filter(in, out); -// new ResampleOp(width, outHeight, ResampleOp.FILTER_LANCZOS).resample(in, out, ResampleOp.createFilter(ResampleOp.FILTER_LANCZOS)); -// BufferedImage out = new ResampleOp(width, outHeight, ResampleOp.FILTER_LANCZOS).filter(in, null); -// ImageUtil.drawOnto(output.getSubimage(0, outY, width, outHeight), out); + new ResampleOp(width, outHeight, ResampleOp.FILTER_TRIANGLE).filter(in, out); +// new ResampleOp(width, outHeight, ResampleOp.FILTER_LANCZOS).filter(in, out); + + for (int j = 0; j < dotsPerStep; j++) { + System.out.print("."); + } } catch (RuntimeException e) { e.printStackTrace(); @@ -216,19 +234,17 @@ public class MappedBufferImage { }); } -// System.out.println("Starting image scale on single thread, waiting for execution to complete..."); -// BufferedImage output = new ResampleOp(width, height, ResampleOp.FILTER_LANCZOS).filter(image, null); - System.out.printf("Started image scale on %d threads, waiting for execution to complete...%n", threads); - Boolean done = null; try { done = latch.await(5L, TimeUnit.MINUTES); } catch (InterruptedException ignore) { } + System.out.println("]"); - System.out.printf("%s scaling image in %d ms%n", (done == null ? "Interrupted" : !done ? "Timed out" : "Done"), System.currentTimeMillis() - start); + System.out.printf("%s scaling image in %d ms\n", (done == null ? "Interrupted" : !done ? "Timed out" : "Done"), System.currentTimeMillis() - start); System.out.println("image = " + output); + return output; } @@ -358,10 +374,12 @@ public class MappedBufferImage { private static class ImageComponent extends JComponent implements Scrollable { private final BufferedImage image; private Paint texture; - double zoom = 1; + private double zoom = 1; public ImageComponent(final BufferedImage image) { - setOpaque(true); // Very important when subclassing JComponent... + setOpaque(true); // Very important when sub classing JComponent... + setDoubleBuffered(true); + this.image = image; } @@ -370,6 +388,68 @@ public class MappedBufferImage { super.addNotify(); texture = createTexture(); + + Rectangle bounds = GraphicsEnvironment.getLocalGraphicsEnvironment().getMaximumWindowBounds(); + zoom = Math.min(1.0, Math.min(bounds.getWidth() / (double) image.getWidth(), bounds.getHeight() / (double) image.getHeight())); + + // TODO: Take scroll pane into account when zooming (center around center point) + AbstractAction zoomIn = new AbstractAction() { + public void actionPerformed(ActionEvent e) { + System.err.println("ZOOM IN"); + setZoom(zoom * 2); + } + }; + + addAction(KeyStroke.getKeyStroke(KeyEvent.VK_PLUS, getToolkit().getMenuShortcutKeyMask()), zoomIn); + addAction(KeyStroke.getKeyStroke(KeyEvent.VK_ADD, getToolkit().getMenuShortcutKeyMask()), zoomIn); + addAction(KeyStroke.getKeyStroke(Character.valueOf('+'), 0), zoomIn); + addAction(KeyStroke.getKeyStroke(Character.valueOf('+'), getToolkit().getMenuShortcutKeyMask()), zoomIn); + AbstractAction zoomOut = new AbstractAction() { + public void actionPerformed(ActionEvent e) { + System.err.println("ZOOM OUT"); + setZoom(zoom / 2); + } + }; + addAction(KeyStroke.getKeyStroke(KeyEvent.VK_MINUS, getToolkit().getMenuShortcutKeyMask()), zoomOut); + addAction(KeyStroke.getKeyStroke(KeyEvent.VK_SUBTRACT, getToolkit().getMenuShortcutKeyMask()), zoomOut); + addAction(KeyStroke.getKeyStroke(Character.valueOf('-'), 0), zoomOut); + addAction(KeyStroke.getKeyStroke(Character.valueOf('-'), getToolkit().getMenuShortcutKeyMask()), zoomOut); + AbstractAction zoomFit = new AbstractAction() { + public void actionPerformed(ActionEvent e) { + System.err.println("ZOOM FIT"); +// Rectangle bounds = GraphicsEnvironment.getLocalGraphicsEnvironment().getMaximumWindowBounds(); + Rectangle bounds = getVisibleRect(); + setZoom(Math.min(1.0, Math.min(bounds.getWidth() / (double) image.getWidth(), bounds.getHeight() / (double) image.getHeight()))); + } + }; + addAction(KeyStroke.getKeyStroke(KeyEvent.VK_EQUALS, getToolkit().getMenuShortcutKeyMask()), zoomFit); + addAction(KeyStroke.getKeyStroke(KeyEvent.VK_9, getToolkit().getMenuShortcutKeyMask()), zoomFit); + addAction(KeyStroke.getKeyStroke(KeyEvent.VK_0, getToolkit().getMenuShortcutKeyMask()), new AbstractAction() { + public void actionPerformed(ActionEvent e) { + System.err.println("ZOOM ACTUAL"); + setZoom(1); + } + }); + } + + private void setZoom(final double newZoom) { + if (newZoom != zoom) { + zoom = newZoom; + // TODO: Add PCL support for zoom and discard tiles cache based on property change + tiles = createTileCache(); + revalidate(); + repaint(); + } + } + + private Map createTileCache() { + return Collections.synchronizedMap(new SizedLRUMap(16 * 1024 * 1024)); + } + + private void addAction(final KeyStroke keyStroke, final AbstractAction action) { + UUID key = UUID.randomUUID(); + getInputMap(WHEN_IN_FOCUSED_WINDOW).put(keyStroke, key); + getActionMap().put(key, action); } private Paint createTexture() { @@ -392,10 +472,17 @@ public class MappedBufferImage { @Override protected void paintComponent(Graphics g) { + // TODO: Java 7 kills the performance from our custom painting... :-( + // TODO: Figure out why mouse wheel/track pad scroll repaints entire component, // unlike using the scroll bars of the JScrollPane. // Consider creating a custom mouse wheel listener as a workaround. + // TODO: Cache visible rect content in buffered/volatile image (s) + visible rect (+ zoom) to speed up repaints + // - Blit the cahced image (possibly translated) (onto itself?) + // - Paint only the necessary parts outside the cached image + // - Async rendering into cached image + // We want to paint only the visible part of the image Rectangle visible = getVisibleRect(); Rectangle clip = g.getClipBounds(); @@ -405,9 +492,28 @@ public class MappedBufferImage { g2.setPaint(texture); g2.fillRect(rect.x, rect.y, rect.width, rect.height); + /* + // Center image (might not be the best way to cooperate with the scroll pane) + Rectangle imageSize = new Rectangle((int) Math.round(image.getWidth() * zoom), (int) Math.round(image.getHeight() * zoom)); + if (imageSize.width < getWidth()) { + g2.translate((getWidth() - imageSize.width) / 2, 0); + } + if (imageSize.height < getHeight()) { + g2.translate(0, (getHeight() - imageSize.height) / 2); + } + */ + + // Zoom if (zoom != 1) { - AffineTransform transform = AffineTransform.getScaleInstance(zoom, zoom); - g2.setTransform(transform); + // NOTE: This helps mostly when scaling up, or scaling down less than 50% + g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); + + rect = new Rectangle( + (int) Math.round(rect.x / zoom), (int) Math.round(rect.y / zoom), + (int) Math.round(rect.width / zoom), (int) Math.round(rect.height / zoom) + ); + + rect = rect.intersection(new Rectangle(image.getWidth(), image.getHeight())); } long start = System.currentTimeMillis(); @@ -415,39 +521,308 @@ public class MappedBufferImage { System.err.println("repaint: " + (System.currentTimeMillis() - start) + " ms"); } - private void repaintImage(Rectangle rect, Graphics2D g2) { + static class Tile { + private final int size; + + private final int x; + private final int y; + + private final Reference data; + private final BufferedImage hardRef; + + Tile(int x, int y, BufferedImage data) { + this.x = x; + this.y = y; + this.data = new SoftReference(data); + + hardRef = data; + + size = 16 + data.getWidth() * data.getHeight() * data.getRaster().getNumDataElements() * sizeOf(data.getRaster().getTransferType()); + } + + private static int sizeOf(final int transferType) { + switch (transferType) { + case DataBuffer.TYPE_INT: + return 4; + case DataBuffer.TYPE_SHORT: + return 2; + case DataBuffer.TYPE_BYTE: + return 1; + default: + throw new IllegalArgumentException("Unsupported transfer type: " + transferType); + } + } + + public void drawTo(Graphics2D g) { + BufferedImage img = data.get(); + + if (img != null) { + g.drawImage(img, x, y, null); + } + +// g.setPaint(Color.GREEN); +// g.drawString(String.format("[%d, %d]", x, y), x + 20, y + 20); + } + + public int getX() { + return x; + } + + public int getY() { + return y; + } + + public int getWidth() { + BufferedImage img = data.get(); + return img != null ? img.getWidth() : -1; + } + + public int getHeight() { + BufferedImage img = data.get(); + return img != null ? img.getHeight() : -1; + } + + public Rectangle getRect() { + BufferedImage img = data.get(); + return img != null ? new Rectangle(x, y, img.getWidth(), img.getHeight()) : null; + } + + public Point getLocation() { + return new Point(x, y); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + + if (other == null || getClass() != other.getClass()) { + return false; + } + + Tile tile = (Tile) other; + + return x == tile.x && y == tile.y; + } + + @Override + public int hashCode() { + return 997 * x + y; + } + + @Override + public String toString() { + return String.format("Tile[%d, %d, %d, %d]", x, y, getWidth(), getHeight()); + } + + public int size() { + return size; + } + } + + // TODO: Consider a fixed size (mem) LRUCache instead + Map tiles = createTileCache(); + + private void repaintImage(final Rectangle rect, final Graphics2D g2) { +// System.err.println("rect: " + rect); +// System.err.println("tiles: " + tiles.size()); + // TODO: Fix rounding errors + // FIx repaint bugs + try { // Paint tiles of the image, to preserve memory - int sliceSize = 200; + final int tileSize = 200; - int slicesW = rect.width / sliceSize; - int slicesH = rect.height / sliceSize; + int tilesW = 1 + rect.width / tileSize; + int tilesH = 1 + rect.height / tileSize; - for (int sliceY = 0; sliceY <= slicesH; sliceY++) { - for (int sliceX = 0; sliceX <= slicesW; sliceX++) { - int x = rect.x + sliceX * sliceSize; - int y = rect.y + sliceY * sliceSize; + for (int yTile = 0; yTile <= tilesH; yTile++) { + for (int xTile = 0; xTile <= tilesW; xTile++) { + // Image (source) coordinates + int x = rect.x + xTile * tileSize; + int y = rect.y + yTile * tileSize; - int w = sliceX == slicesW ? Math.min(sliceSize, rect.x + rect.width - x) : sliceSize; - int h = sliceY == slicesH ? Math.min(sliceSize, rect.y + rect.height - y) : sliceSize; + int w = xTile == tilesW ? Math.min(tileSize, rect.x + rect.width - x) : tileSize; + int h = yTile == tilesH ? Math.min(tileSize, rect.y + rect.height - y) : tileSize; if (w == 0 || h == 0) { continue; } // System.err.printf("%04d, %04d, %04d, %04d%n", x, y, w, h); - BufferedImage img = image.getSubimage(x, y, w, h); - g2.drawImage(img, x, y, null); + + // - Get tile from cache + // - If non-null, paint + // - If null, request data for later use, with callback, and return + // TODO: Could we use ImageProducer/ImageConsumer/ImageObserver interface?? + + // Destination (display) coordinates + int dstX = (int) Math.round(x * zoom); + int dstY = (int) Math.round(y * zoom); + int dstW = (int) Math.round(w * zoom); + int dstH = (int) Math.round(h * zoom); + + if (dstW == 0 || dstH == 0) { + continue; + } + + // Don't create overlapping/duplicate tiles... + // - Always start tile grid at 0,0 + // - Always occupy entire tile, unless edge + + // Source (original) coordinates + int tileSrcX = x - x % tileSize; + int tileSrcY = y - y % tileSize; +// final int tileSrcW = Math.min(tileSize, image.getWidth() - tileSrcX); +// final int tileSrcH = Math.min(tileSize, image.getHeight() - tileSrcY); + + // Destination (display) coordinates + int tileDstX = (int) Math.round(tileSrcX * zoom); + int tileDstY = (int) Math.round(tileSrcY * zoom); +// final int tileDstW = (int) Math.round(tileSrcW * zoom); +// final int tileDstH = (int) Math.round(tileSrcH * zoom); + + List points = new ArrayList(4); + points.add(new Point(tileDstX, tileDstY)); + if (tileDstX != dstX) { + points.add(new Point(tileDstX + tileSize, tileDstY)); + } + if (tileDstY != dstY) { + points.add(new Point(tileDstX, tileDstY + tileSize)); + } + if (tileDstX != dstX && tileDstY != dstY) { + points.add(new Point(tileDstX + tileSize, tileDstY + tileSize)); + } + + for (final Point point : points) { + Tile tile = tiles.get(point); + + if (tile != null) { + Reference img = tile.data; + if (img != null) { + tile.drawTo(g2); + continue; + } + else { + tiles.remove(point); + } + } + +// System.err.printf("Tile miss: [%d, %d]\n", dstX, dstY); + + // Dispatch to off-thread worker + final Map localTiles = tiles; + executorService.submit(new Runnable() { + public void run() { + // TODO: Fix rounding issues... Problem is that sometimes the srcW/srcH is 1 pixel off filling the tile... + int tileSrcX = (int) Math.round(point.x / zoom); + int tileSrcY = (int) Math.round(point.y / zoom); + int tileSrcW = Math.min(tileSize, image.getWidth() - tileSrcX); + int tileSrcH = Math.min(tileSize, image.getHeight() - tileSrcY); + int tileDstW = (int) Math.round(tileSrcW * zoom); + int tileDstH = (int) Math.round(tileSrcH * zoom); + + try { + // TODO: Consider comparing zoom/local zoom + if (localTiles != tiles) { + return; // Return early after re-zoom + } + + if (localTiles.containsKey(point)) { +// System.err.println("Skipping tile, already producing..."); + return; + } + + // Test against current view rect, to avoid computing tiles that will be thrown away immediately + // TODO: EDT safe? + if (!getVisibleRect().intersects(new Rectangle(point.x, point.y, tileDstW, tileDstH))) { + return; + } + +// System.err.printf("Creating tile: [%d, %d]\n", tileDstX, tileDstY); + + BufferedImage temp = getGraphicsConfiguration().createCompatibleImage(tileDstW, tileDstH); + final Tile tile = new Tile(point.x, point.y, temp); + localTiles.put(point, tile); + + Graphics2D graphics = temp.createGraphics(); + try { + Object hint = g2.getRenderingHint(RenderingHints.KEY_INTERPOLATION); + + if (hint != null) { + graphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION, hint); + } + + graphics.scale(zoom, zoom); + graphics.drawImage(image.getSubimage(tileSrcX, tileSrcY, tileSrcW, tileSrcH), 0, 0, null); + } + finally { + graphics.dispose(); + } + + SwingUtilities.invokeLater(new Runnable() { + public void run() { + repaint(10, tile.x, tile.y, tile.getWidth(), tile.getHeight()); + } + }); + } + catch (Throwable t) { + localTiles.remove(point); + System.err.println("Boooo: " + t.getMessage()); + } + } + }); + } + } } - -// BufferedImage img = image.getSubimage(rect.x, rect.y, rect.width, rect.height); -// g2.drawImage(img, rect.x, rect.y, null); } catch (NullPointerException e) { // e.printStackTrace(); - // Happens whenever apple.awt.OSXCachingSufraceManager runs out of memory + // Happens whenever apple.awt.OSXCachingSurfaceManager runs out of memory // TODO: Figure out why repaint(x,y,w,h) doesn't work any more..? + System.err.println("Full repaint due to NullPointerException (probably out of memory)."); + repaint(); // NOTE: Might cause a brief flash while the component is redrawn + } + } + + private void repaintImage0(final Rectangle rect, final Graphics2D g2) { + g2.scale(zoom, zoom); + + try { + // Paint tiles of the image, to preserve memory + final int tileSize = 200; + + int tilesW = rect.width / tileSize; + int tilesH = rect.height / tileSize; + + for (int yTile = 0; yTile <= tilesH; yTile++) { + for (int xTile = 0; xTile <= tilesW; xTile++) { + // Image (source) coordinates + final int x = rect.x + xTile * tileSize; + final int y = rect.y + yTile * tileSize; + + final int w = xTile == tilesW ? Math.min(tileSize, rect.x + rect.width - x) : tileSize; + final int h = yTile == tilesH ? Math.min(tileSize, rect.y + rect.height - y) : tileSize; + + if (w == 0 || h == 0) { + continue; + } + +// System.err.printf("%04d, %04d, %04d, %04d%n", x, y, w, h); + + BufferedImage img = image.getSubimage(x, y, w, h); + g2.drawImage(img, x, y, null); + + } + } + } + catch (NullPointerException e) { +// e.printStackTrace(); + // Happens whenever apple.awt.OSXCachingSurfaceManager runs out of memory + // TODO: Figure out why repaint(x,y,w,h) doesn't work any more..? + System.err.println("Full repaint due to NullPointerException (probably out of memory)."); repaint(); // NOTE: Might cause a brief flash while the component is redrawn } } @@ -476,12 +851,68 @@ public class MappedBufferImage { } public boolean getScrollableTracksViewportWidth() { - return false; + return getWidth() > getPreferredSize().width; } public boolean getScrollableTracksViewportHeight() { + return getHeight() > getPreferredSize().height; + } + } + + final static class SizedLRUMap extends LRUHashMap { + int currentSize; + int maxSize; + + public SizedLRUMap(int pMaxSize) { + super(); // Note: super.maxSize doesn't count... + maxSize = pMaxSize; + } + + + protected int sizeOf(final Object pValue) { + ImageComponent.Tile cached = (ImageComponent.Tile) pValue; + + if (cached == null) { + return 0; + } + + return cached.size(); + } + + @Override + public V put(K pKey, V pValue) { + currentSize += sizeOf(pValue); + + V old = super.put(pKey, pValue); + if (old != null) { + currentSize -= sizeOf(old); + } + return old; + } + + @Override + public V remove(Object pKey) { + V old = super.remove(pKey); + if (old != null) { + currentSize -= sizeOf(old); + } + return old; + } + + @Override + protected boolean removeEldestEntry(Map.Entry pEldest) { + if (maxSize <= currentSize) { // NOTE: maxSize here is mem size + removeLRU(); + } return false; } + + @Override + public void removeLRU() { + while (maxSize <= currentSize) { // NOTE: maxSize here is mem size + super.removeLRU(); + } + } } private static class PaintDotsTask implements Runnable { diff --git a/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/util/PersistentMap.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/util/PersistentMap.java index c35ece68..e87673b7 100755 --- a/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/util/PersistentMap.java +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/util/PersistentMap.java @@ -28,6 +28,13 @@ package com.twelvemonkeys.util; +import com.twelvemonkeys.io.FileUtil; + +import java.io.*; +import java.util.*; + +import static com.twelvemonkeys.lang.Validate.notNull; + /** * PersistentMap * @@ -35,27 +42,293 @@ package com.twelvemonkeys.util; * @author last modified by $Author: haraldk$ * @version $Id: PersistentMap.java,v 1.0 May 13, 2009 2:31:29 PM haraldk Exp$ */ -public class PersistentMap { - // TODO: Implement Map - // TODO: Delta synchronization (db?) +public class PersistentMap extends AbstractMap{ + public static final FileFilter DIRECTORIES = new FileFilter() { + public boolean accept(File file) { + return file.isDirectory(); + } + + @Override + public String toString() { + return "[All folders]"; + } + }; + private static final String INDEX = ".index"; + + private final File root; + private final Map index = new LinkedHashMap(); + + private boolean mutable = true; + + + // Idea 2.0: + // - Create directory per hashCode + // - Create file per object in that directory + // - Name file after serialized form of key? Base64? + // - Special case for String/Integer/Long etc? + // - Or create index file in directory with serialized objects + name (uuid) of file + + // TODO: Consider single index file? Or a few? In root directory instead of each directory + // Consider a RAF/FileChannel approach instead of streams - how do we discard portions of a RAF? + // - Need to keep track of used/unused parts of file, scan for gaps etc...? + // - Need to periodically truncate and re-build the index (always as startup, then at every N puts/removes?) + + /*public */PersistentMap(String id) { + this(new File(FileUtil.getTempDirFile(), id)); + } + + public PersistentMap(File root) { + this.root = notNull(root); + + init(); + } + + private void init() { + if (!root.exists() && !root.mkdirs()) { + throw new IllegalStateException(String.format("'%s' does not exist/could not be created", root.getAbsolutePath())); + } + else if (!root.isDirectory()) { + throw new IllegalStateException(String.format("'%s' exists but is not a directory", root.getAbsolutePath())); + } + + if (!root.canRead()) { + throw new IllegalStateException(String.format("'%s' is not readable", root.getAbsolutePath())); + } + + if (!root.canWrite()) { + mutable = false; + } + + FileUtil.visitFiles(root, DIRECTORIES, new Visitor() { + public void visit(File dir) { + // - Read .index file + // - Add entries to index + ObjectInputStream input = null; + try { + input = new ObjectInputStream(new FileInputStream(new File(dir, INDEX))); + while (true) { + @SuppressWarnings({"unchecked"}) + K key = (K) input.readObject(); + String fileName = (String) input.readObject(); + index.put(key, UUID.fromString(fileName)); + } + } + catch (EOFException eof) { + // break here + } + catch (IOException e) { + throw new RuntimeException(e); + } + catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + finally { + FileUtil.close(input); + } + } + }); + } + + @Override + public Set> entrySet() { + return new AbstractSet>() { + @Override + public Iterator> iterator() { + return new Iterator>() { + Iterator> indexIter = index.entrySet().iterator(); + + public boolean hasNext() { + return indexIter.hasNext(); + } + + public Entry next() { + return new Entry() { + final Entry entry = indexIter.next(); + + public K getKey() { + return entry.getKey(); + } + + public V getValue() { + K key = entry.getKey(); + int hash = key != null ? key.hashCode() : 0; + return readVal(hash, entry.getValue()); + } + + public V setValue(V value) { + K key = entry.getKey(); + int hash = key != null ? key.hashCode() : 0; + return writeVal(key, hash, entry.getValue(), value, getValue()); + } + }; + } + + public void remove() { + indexIter.remove(); + } + }; + } + + @Override + public int size() { + return index.size(); + } + }; + } + + @Override + public int size() { + return index.size(); + } + + @Override + public V put(K key, V value) { + V oldVal = null; + + UUID uuid = index.get(key); + int hash = key != null ? key.hashCode() : 0; + + if (uuid != null) { + oldVal = readVal(hash, uuid); + } + + return writeVal(key, hash, uuid, value, oldVal); + } + + private V writeVal(K key, int hash, UUID uuid, V value, V oldVal) { + if (!mutable) { + throw new UnsupportedOperationException(); + } + + File bucket = new File(root, hashToFileName(hash)); + if (!bucket.exists() && !bucket.mkdirs()) { + throw new IllegalStateException(String.format("Could not create bucket '%s'", bucket)); + } + + if (uuid == null) { + // No uuid means new entry + uuid = UUID.randomUUID(); + + File idx = new File(bucket, INDEX); + + ObjectOutputStream output = null; + try { + output = new ObjectOutputStream(new FileOutputStream(idx, true)); + output.writeObject(key); + output.writeObject(uuid.toString()); + + index.put(key, uuid); + } + catch (IOException e) { + throw new RuntimeException(e); + } + finally { + FileUtil.close(output); + } + } + + File entry = new File(bucket, uuid.toString()); + if (value != null) { + ObjectOutputStream output = null; + try { + output = new ObjectOutputStream(new FileOutputStream(entry)); + output.writeObject(value); + + } + catch (IOException e) { + throw new RuntimeException(e); + } + finally { + FileUtil.close(output); + } + } + else if (entry.exists()) { + if (!entry.delete()) { + throw new IllegalStateException(String.format("'%s' could not be deleted", entry)); + } + } + + return oldVal; + } + + private String hashToFileName(int hash) { + return Integer.toString(hash, 16); + } + + @Override + public V get(Object key) { + UUID uuid = index.get(key); + + if (uuid != null) { + int hash = key != null ? key.hashCode() : 0; + return readVal(hash, uuid); + } + + return null; + } + + private V readVal(final int hash, final UUID uuid) { + File bucket = new File(root, hashToFileName(hash)); + File entry = new File(bucket, uuid.toString()); + + if (entry.exists()) { + ObjectInputStream input = null; + try { + input = new ObjectInputStream(new FileInputStream(entry)); + //noinspection unchecked + return (V) input.readObject(); + } + catch (IOException e) { + throw new RuntimeException(e); + } + catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + finally { + FileUtil.close(input); + } + } + + return null; + } + + @Override + public V remove(Object key) { + // TODO!!! + return super.remove(key); + } + + // TODO: Should override size, put, get, remove, containsKey and containsValue + + + } + + /* +Memory mapped file? +Delta sync? + Persistent format Header File ID 4-8 bytes - Size + Size (entries) - Entry pointer array block - Size - Next entry pointer block address - Entry 1 address + PersistentEntry pointer array block (PersistentEntry 0) + Size (bytes) + Next entry pointer block address (0 if last) + PersistentEntry 1 address/offset + key ... - Entry n address + PersistentEntry n address/offset + key + + PersistentEntry 1 + Size (bytes)? + Serialized value or pointer array block + ... + PersistentEntry n + Size (bytes)? + Serialized value or pointer array block - Entry 1 - ... - Entry n - */ \ No newline at end of file