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 8d69ffce..2954364c 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 @@ -61,6 +61,7 @@ import java.util.concurrent.*; public class MappedBufferImage { private static int threads = Runtime.getRuntime().availableProcessors(); private static ExecutorService executorService = Executors.newFixedThreadPool(threads * 4); + private static ExecutorService executorService2 = Executors.newFixedThreadPool(2); public static void main(String[] args) throws IOException { int argIndex = 0; @@ -553,15 +554,15 @@ public class MappedBufferImage { } } - public void drawTo(Graphics2D g) { + public boolean drawTo(Graphics2D g) { BufferedImage img = data.get(); if (img != null) { g.drawImage(img, x, y, null); + return true; } -// g.setPaint(Color.GREEN); -// g.drawString(String.format("[%d, %d]", x, y), x + 20, y + 20); + return false; } public int getX() { @@ -622,6 +623,7 @@ public class MappedBufferImage { } // TODO: Consider a fixed size (mem) LRUCache instead + // TODO: Better yet, re-use tiles Map tiles = createTileCache(); private void repaintImage(final Rectangle rect, final Graphics2D g2) { @@ -634,6 +636,15 @@ public class MappedBufferImage { // Paint tiles of the image, to preserve memory final int tileSize = 200; + // Calculate relative to image(0,0), rather than rect(x, y) + int xOff = rect.x % tileSize; + int yOff = rect.y % tileSize; + + rect.x -= xOff; + rect.y -= yOff; + rect.width += xOff; + rect.height += yOff; + int tilesW = 1 + rect.width / tileSize; int tilesH = 1 + rect.height / tileSize; @@ -658,10 +669,10 @@ public class MappedBufferImage { // 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); + int dstX = (int) Math.floor(x * zoom); + int dstY = (int) Math.floor(y * zoom); + int dstW = (int) Math.ceil(w * zoom); + int dstH = (int) Math.ceil(h * zoom); if (dstW == 0 || dstH == 0) { continue; @@ -678,8 +689,8 @@ public class MappedBufferImage { // 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); + int tileDstX = (int) Math.floor(tileSrcX * zoom); + int tileDstY = (int) Math.floor(tileSrcY * zoom); // final int tileDstW = (int) Math.round(tileSrcW * zoom); // final int tileDstH = (int) Math.round(tileSrcH * zoom); @@ -699,9 +710,7 @@ public class MappedBufferImage { Tile tile = tiles.get(point); if (tile != null) { - Reference img = tile.data; - if (img != null) { - tile.drawTo(g2); + if (tile.drawTo(g2)) { continue; } else { @@ -713,9 +722,8 @@ public class MappedBufferImage { // Dispatch to off-thread worker final Map localTiles = tiles; - executorService.submit(new Runnable() { + executorService2.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); @@ -735,8 +743,14 @@ public class MappedBufferImage { } // 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))) { + final Rectangle visibleRect = new Rectangle(); + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + visibleRect.setBounds(getVisibleRect()); + } + }); + + if (!visibleRect.intersects(new Rectangle(point.x, point.y, tileDstW, tileDstH))) { return; } diff --git a/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/MappedImageFactory.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/MappedImageFactory.java index 3347dd38..fb72da96 100644 --- a/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/MappedImageFactory.java +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/MappedImageFactory.java @@ -30,11 +30,12 @@ package com.twelvemonkeys.image; import javax.imageio.ImageTypeSpecifier; import java.awt.*; -import java.awt.image.BufferedImage; -import java.awt.image.ColorModel; -import java.awt.image.DataBuffer; -import java.awt.image.SampleModel; +import java.awt.image.*; import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Modifier; +import java.lang.reflect.UndeclaredThrowableException; /** * A factory for creating {@link BufferedImage}s backed by memory mapped files. @@ -50,6 +51,9 @@ public final class MappedImageFactory { // TODO: Create a way to do ColorConvertOp (or other color space conversion) on these images. // - Current implementation of CCOp delegates to internal sun.awt classes that assumes java.awt.DataBufferByte for type byte buffers :-/ + private static final boolean DEBUG = "true".equalsIgnoreCase(System.getProperty("com.twelvemonkeys.image.mapped.debug")); + static final RasterFactory RASTER_FACTORY = createRasterFactory(); + private MappedImageFactory() {} public static BufferedImage createCompatibleMappedImage(int width, int height, int type) throws IOException { @@ -58,7 +62,8 @@ public final class MappedImageFactory { } public static BufferedImage createCompatibleMappedImage(int width, int height, GraphicsConfiguration configuration, int transparency) throws IOException { - // TODO: Should we also use the sample model? +// BufferedImage temp = configuration.createCompatibleImage(1, 1, transparency); +// return createCompatibleMappedImage(width, height, temp.getSampleModel().createCompatibleSampleModel(width, height), temp.getColorModel()); return createCompatibleMappedImage(width, height, configuration.getColorModel(transparency)); } @@ -73,6 +78,88 @@ public final class MappedImageFactory { static BufferedImage createCompatibleMappedImage(int width, int height, SampleModel sm, ColorModel cm) throws IOException { DataBuffer buffer = MappedFileBuffer.create(sm.getTransferType(), width * height * sm.getNumDataElements(), 1); - return new BufferedImage(cm, new GenericWritableRaster(sm, buffer, new Point()), cm.isAlphaPremultiplied(), null); + return new BufferedImage(cm, RASTER_FACTORY.createRaster(sm, buffer, new Point()), cm.isAlphaPremultiplied(), null); + } + + private static RasterFactory createRasterFactory() { + try { + // Try to instantiate, will throw LinkageError if it fails + return new SunRasterFactory(); + } + catch (LinkageError e) { + if (DEBUG) { + e.printStackTrace(); + } + + System.err.println("Could not instantiate SunWritableRaster, falling back to GenericWritableRaster."); + } + + // Fall back + return new GenericRasterFactory(); + } + + static interface RasterFactory { + WritableRaster createRaster(SampleModel model, DataBuffer buffer, Point origin); + } + + /** + * Generic implementation that should work for any JRE, and creates a custom subclass of {@link WritableRaster}. + */ + static final class GenericRasterFactory implements RasterFactory { + public WritableRaster createRaster(final SampleModel model, final DataBuffer buffer, final Point origin) { + return new GenericWritableRaster(model, buffer, origin); + } + } + + /** + * Sun/Oracle JRE-specific implementation that creates {@code sun.awt.image.SunWritableRaster}. + * Callers must catch {@link LinkageError}. + */ + static final class SunRasterFactory implements RasterFactory { + final private Constructor factoryMethod = getFactoryMethod(); + + @SuppressWarnings("unchecked") + private static Constructor getFactoryMethod() { + try { + Class cls = Class.forName("sun.awt.image.SunWritableRaster"); + + if (Modifier.isAbstract(cls.getModifiers())) { + throw new IncompatibleClassChangeError("sun.awt.image.SunWritableRaster has become abstract and can't be instantiated"); + } + + return (Constructor) cls.getConstructor(SampleModel.class, DataBuffer.class, Point.class); + } + catch (ClassNotFoundException e) { + throw new NoClassDefFoundError(e.getMessage()); + } + catch (NoSuchMethodException e) { + throw new NoSuchMethodError(e.getMessage()); + } + } + + public WritableRaster createRaster(final SampleModel model, final DataBuffer buffer, final Point origin) { + try { + return factoryMethod.newInstance(model, buffer, origin); + } + catch (InstantiationException e) { + throw new Error("Could not create SunWritableRaster: ", e); // Should never happen, as we test for abstract class + } + catch (IllegalAccessException e) { + throw new Error("Could not create SunWritableRaster: ", e); // Should never happen, only public constructors are reflected + } + catch (InvocationTargetException e) { + // Unwrap to allow normal exception flow + Throwable cause = e.getCause(); + + if (cause instanceof RuntimeException) { + throw (RuntimeException) cause; + } + else if (cause instanceof Error) { + throw (Error) cause; + } + + throw new UndeclaredThrowableException(cause); + } + } } }