mirror of
https://github.com/haraldk/TwelveMonkeys.git
synced 2026-05-27 00:00:02 -04:00
47425e2ca0
Better tiling in test app.
1020 lines
42 KiB
Java
1020 lines
42 KiB
Java
/*
|
|
* Copyright (c) 2010, 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.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;
|
|
import javax.imageio.ImageReader;
|
|
import javax.imageio.ImageTypeSpecifier;
|
|
import javax.imageio.stream.ImageInputStream;
|
|
import javax.swing.*;
|
|
import java.awt.*;
|
|
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.lang.ref.Reference;
|
|
import java.lang.ref.SoftReference;
|
|
import java.util.*;
|
|
import java.util.List;
|
|
import java.util.concurrent.*;
|
|
|
|
/**
|
|
* MappedBufferImage
|
|
*
|
|
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
|
* @author last modified by $Author: haraldk$
|
|
* @version $Id: MappedBufferImage.java,v 1.0 Jun 13, 2010 7:33:19 PM haraldk Exp$
|
|
*/
|
|
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;
|
|
File file = args.length > 0 ? new File(args[argIndex]) : null;
|
|
|
|
int w;
|
|
int h;
|
|
BufferedImage image;
|
|
|
|
if (file != null && file.exists()) {
|
|
argIndex++;
|
|
|
|
// Load image using ImageIO
|
|
ImageInputStream input = ImageIO.createImageInputStream(file);
|
|
Iterator<ImageReader> readers = ImageIO.getImageReaders(input);
|
|
|
|
if (!readers.hasNext()) {
|
|
System.err.println("No image reader found for input: " + file.getAbsolutePath());
|
|
System.exit(0);
|
|
return;
|
|
}
|
|
|
|
ImageReader reader = readers.next();
|
|
try {
|
|
reader.setInput(input);
|
|
|
|
Iterator<ImageTypeSpecifier> types = reader.getImageTypes(0);
|
|
ImageTypeSpecifier type = types.next();
|
|
|
|
// TODO: Negotiate best layout according to the GraphicsConfiguration.
|
|
|
|
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());
|
|
|
|
// image = MappedImageFactory.createCompatibleMappedImage(w, h, cm2);
|
|
// image = MappedImageFactory.createCompatibleMappedImage(w, h, cm);
|
|
// image = MappedImageFactory.createCompatibleMappedImage(w, h, BufferedImage.TYPE_4BYTE_ABGR);
|
|
// image = MappedImageFactory.createCompatibleMappedImage(w, h, BufferedImage.TYPE_INT_BGR);
|
|
// image = MappedImageFactory.createCompatibleMappedImage(w, h, type);
|
|
// if (w > 1024 || h > 1024) {
|
|
image = MappedImageFactory.createCompatibleMappedImage(w, h, type);
|
|
// }
|
|
// else {
|
|
// image = type.createBufferedImage(w, h);
|
|
// }
|
|
|
|
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);
|
|
}
|
|
finally {
|
|
reader.dispose();
|
|
}
|
|
}
|
|
else {
|
|
w = args.length > argIndex && StringUtil.isNumber(args[argIndex]) ? Integer.parseInt(args[argIndex++]) : 6000;
|
|
h = args.length > argIndex && StringUtil.isNumber(args[argIndex]) ? Integer.parseInt(args[argIndex++]) : w * 2 / 3;
|
|
|
|
GraphicsConfiguration configuration = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration();
|
|
image = MappedImageFactory.createCompatibleMappedImage(w, h, configuration, Transparency.TRANSLUCENT);
|
|
// image = MappedImageFactory.createCompatibleMappedImage(w, h, configuration, Transparency.OPAQUE);
|
|
// image = MappedImageFactory.createCompatibleMappedImage(w, h, BufferedImage.TYPE_4BYTE_ABGR);
|
|
|
|
System.out.println("image = " + image);
|
|
|
|
DataBuffer buffer = image.getRaster().getDataBuffer();
|
|
final boolean alpha = image.getColorModel().hasAlpha();
|
|
|
|
// Mix in some nice colors
|
|
createBackground(w, h, buffer, alpha);
|
|
|
|
// Add some random dots (get out the coffee)
|
|
paintDots(w, h, image);
|
|
}
|
|
|
|
// Resample down to some fixed size
|
|
if (args.length > argIndex && "-scale".equals(args[argIndex++])) {
|
|
image = resampleImage(image, 800);
|
|
}
|
|
|
|
int bytesPerPixel = image.getColorModel().getPixelSize() / 8; // Calculate first to avoid overflow
|
|
String size = toHumanReadableSize(w * h * bytesPerPixel);
|
|
showIt(w, h, image, size);
|
|
}
|
|
|
|
private static void showIt(final int w, final int h, BufferedImage image, final String size) {
|
|
JFrame frame = new JFrame(String.format("Test [%s x %s] (%s)", w, h, size)) {
|
|
@Override
|
|
public Dimension getPreferredSize() {
|
|
// TODO: This looks like a useful util method...
|
|
DisplayMode displayMode = getGraphicsConfiguration().getDevice().getDisplayMode();
|
|
Dimension size = super.getPreferredSize();
|
|
|
|
size.width = Math.min(size.width, displayMode.getWidth());
|
|
size.height = Math.min(size.height, displayMode.getHeight());
|
|
|
|
return size;
|
|
}
|
|
};
|
|
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
|
|
JScrollPane scroll = new JScrollPane(new ImageComponent(image));
|
|
scroll.setBorder(BorderFactory.createEmptyBorder());
|
|
frame.add(scroll);
|
|
frame.pack();
|
|
frame.setLocationRelativeTo(null);
|
|
frame.setVisible(true);
|
|
}
|
|
|
|
private static BufferedImage resampleImage(final BufferedImage image, final int width) {
|
|
long start = System.currentTimeMillis();
|
|
|
|
float aspect = image.getHeight() / (float) image.getWidth();
|
|
int height = Math.round(width * aspect);
|
|
|
|
// 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 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(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 < steps; i++) {
|
|
final int inY = i * inStep;
|
|
final int outY = i * outStep;
|
|
final int inHeight = Math.min(inStep, image.getHeight() - inY);
|
|
final int outHeight = Math.min(outStep, output.getHeight() - outY);
|
|
executorService.submit(new Runnable() {
|
|
public void run() {
|
|
try {
|
|
BufferedImage in = image.getSubimage(0, inY, image.getWidth(), inHeight);
|
|
BufferedImage out = output.getSubimage(0, outY, width, outHeight);
|
|
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();
|
|
throw e;
|
|
}
|
|
finally {
|
|
latch.countDown();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
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.println("image = " + output);
|
|
|
|
return output;
|
|
}
|
|
|
|
private static void paintDots(int width, int height, final BufferedImage image) {
|
|
long start = System.currentTimeMillis();
|
|
|
|
int s = 300;
|
|
int ws = width / s;
|
|
int hs = height / s;
|
|
|
|
Color[] colors = new Color[] {
|
|
Color.WHITE, Color.ORANGE, Color.BLUE, Color.MAGENTA, Color.BLACK, Color.RED, Color.CYAN,
|
|
Color.GRAY, Color.GREEN, Color.YELLOW, Color.PINK, Color.LIGHT_GRAY, Color.DARK_GRAY
|
|
};
|
|
|
|
CountDownLatch latch = new CountDownLatch(threads);
|
|
int step = (int) Math.ceil(hs / (double) threads);
|
|
Random r = new Random();
|
|
|
|
for (int i = 0; i < threads; i++) {
|
|
executorService.submit(new PaintDotsTask(image, s, ws, colors, r, i * step, i * step + step, latch));
|
|
}
|
|
|
|
System.err.printf("Started painting in %d threads, waiting for execution to complete...%n", threads);
|
|
|
|
Boolean done = null;
|
|
try {
|
|
done = latch.await(3L, TimeUnit.MINUTES);
|
|
}
|
|
catch (InterruptedException ignore) {
|
|
}
|
|
|
|
System.out.printf("%s painting %d dots in %d ms%n", (done == null ? "Interrupted" : !done ? "Timed out" : "Done"), Math.max(0, hs - 1) * Math.max(0, ws - 1), System.currentTimeMillis() - start);
|
|
}
|
|
|
|
private static void paintDots0(BufferedImage image, int s, int ws, Color[] colors, Random r, final int first, final int last) {
|
|
for (int y = first; y < last; y++) {
|
|
for (int x = 0; x < ws - 1; x++) {
|
|
BufferedImage tile = image.getSubimage(x * s, y * s, 2 * s, 2 * s);
|
|
Graphics2D g;
|
|
try {
|
|
g = tile.createGraphics();
|
|
}
|
|
catch (OutOfMemoryError e) {
|
|
System.gc();
|
|
System.err.println("Out of memory: " + e.getMessage());
|
|
g = tile.createGraphics(); // If this fails, give up
|
|
}
|
|
|
|
try {
|
|
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
|
g.setComposite(AlphaComposite.SrcOver.derive(r.nextFloat()));
|
|
g.setColor(colors[r.nextInt(colors.length)]);
|
|
int o = r.nextInt(s) + s / 10;
|
|
int c = (2 * s - o) / 2;
|
|
g.fillOval(c, c, o, o);
|
|
}
|
|
finally {
|
|
g.dispose();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void createBackground(int w, int h, DataBuffer buffer, boolean alpha) {
|
|
long start = System.currentTimeMillis();
|
|
|
|
int step = (int) Math.ceil(h / (double) threads);
|
|
|
|
CountDownLatch latch = new CountDownLatch(threads);
|
|
for (int i = 0; i < threads; i++) {
|
|
executorService.submit(new PaintBackgroundTask(w, h, buffer, alpha, i * step, i * step + step, latch));
|
|
}
|
|
System.err.printf("Started painting in %d threads, waiting for execution to complete...%n", threads);
|
|
|
|
Boolean done = null;
|
|
try {
|
|
done = latch.await(3L, TimeUnit.MINUTES);
|
|
}
|
|
catch (InterruptedException ignore) {
|
|
}
|
|
|
|
System.out.printf("%s creating background in %d ms%n", (done == null ? "Interrupted" : !done ? "Timed out" : "Done"), System.currentTimeMillis() - start);
|
|
}
|
|
|
|
private static void paintBackground0(int w, int h, DataBuffer buffer, boolean alpha, final int first, final int last) {
|
|
for (int y = first; y < last; y++) {
|
|
for (int x = 0; x < w; x++) {
|
|
int r = (int) ((x * y * 255.0) / (h * w));
|
|
int g = (int) (((w - x) * y * 255.0) / (h * w));
|
|
int b = (int) ((x * (h - y) * 255.0) / (h * w));
|
|
int a = alpha ? (int) (((w - x) * (h - y) * 255.0) / (h * w)) : 0;
|
|
|
|
switch (buffer.getDataType()) {
|
|
case DataBuffer.TYPE_BYTE:
|
|
int off = (y * w + x) * (alpha ? 4 : 3);
|
|
if (alpha) {
|
|
buffer.setElem(off++, 255 - a);
|
|
buffer.setElem(off++, b);
|
|
buffer.setElem(off++, g);
|
|
buffer.setElem(off, r);
|
|
}
|
|
else {
|
|
// TODO: Why the RGB / ABGR byte order inconsistency??
|
|
buffer.setElem(off++, r);
|
|
buffer.setElem(off++, g);
|
|
buffer.setElem(off, b);
|
|
}
|
|
break;
|
|
case DataBuffer.TYPE_INT:
|
|
buffer.setElem(y * w + x, (255 - a) << 24 | r << 16 | g << 8 | b);
|
|
break;
|
|
default:
|
|
System.err.println("Transfer type not supported: " + buffer.getDataType());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private static String toHumanReadableSize(long size) {
|
|
return String.format("%,d MB", (long) (size / (double) (1024L << 10)));
|
|
}
|
|
|
|
/**
|
|
* A fairly optimized component for displaying a BufferedImage
|
|
*/
|
|
private static class ImageComponent extends JComponent implements Scrollable {
|
|
private final BufferedImage image;
|
|
private Paint texture;
|
|
private double zoom = 1;
|
|
|
|
public ImageComponent(final BufferedImage image) {
|
|
setOpaque(true); // Very important when sub classing JComponent...
|
|
setDoubleBuffered(true);
|
|
|
|
this.image = image;
|
|
}
|
|
|
|
@Override
|
|
public void addNotify() {
|
|
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() {
|
|
BufferedImage pattern = getGraphicsConfiguration().createCompatibleImage(20, 20);
|
|
Graphics2D g = pattern.createGraphics();
|
|
|
|
try {
|
|
g.setColor(Color.LIGHT_GRAY);
|
|
g.fillRect(0, 0, pattern.getWidth(), pattern.getHeight());
|
|
g.setColor(Color.GRAY);
|
|
g.fillRect(0, 0, pattern.getWidth() / 2, pattern.getHeight() / 2);
|
|
g.fillRect(pattern.getWidth() / 2, pattern.getHeight() / 2, pattern.getWidth() / 2, pattern.getHeight() / 2);
|
|
}
|
|
finally {
|
|
g.dispose();
|
|
}
|
|
|
|
return new TexturePaint(pattern, new Rectangle(pattern.getWidth(), pattern.getHeight()));
|
|
}
|
|
|
|
@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();
|
|
Rectangle rect = clip == null ? visible : visible.intersection(clip);
|
|
|
|
Graphics2D g2 = (Graphics2D) g;
|
|
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) {
|
|
// 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();
|
|
repaintImage(rect, g2);
|
|
System.err.println("repaint: " + (System.currentTimeMillis() - start) + " ms");
|
|
}
|
|
|
|
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 boolean drawTo(Graphics2D g) {
|
|
BufferedImage img = data.get();
|
|
|
|
if (img != null) {
|
|
g.drawImage(img, x, y, null);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
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
|
|
// TODO: Better yet, re-use tiles
|
|
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
|
|
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;
|
|
|
|
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 = 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);
|
|
|
|
// - 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.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;
|
|
}
|
|
|
|
// 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.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);
|
|
|
|
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) {
|
|
if (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;
|
|
executorService2.submit(new Runnable() {
|
|
public void run() {
|
|
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
|
|
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;
|
|
}
|
|
|
|
// 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) {
|
|
// 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
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public Dimension getPreferredSize() {
|
|
return new Dimension((int) (image.getWidth() * zoom), (int) (image.getHeight() * zoom));
|
|
}
|
|
|
|
public Dimension getPreferredScrollableViewportSize() {
|
|
return getPreferredSize();
|
|
}
|
|
|
|
public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) {
|
|
return 10;
|
|
}
|
|
|
|
public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) {
|
|
switch (orientation) {
|
|
case SwingConstants.HORIZONTAL:
|
|
return visibleRect.width * 3 / 4;
|
|
case SwingConstants.VERTICAL:
|
|
default:
|
|
return visibleRect.height * 3 / 4;
|
|
}
|
|
}
|
|
|
|
public boolean getScrollableTracksViewportWidth() {
|
|
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 {
|
|
private final BufferedImage image;
|
|
private final int s;
|
|
private final int wstep;
|
|
private final Color[] colors;
|
|
private final Random random;
|
|
private final int last;
|
|
private final int first;
|
|
private final CountDownLatch latch;
|
|
|
|
public PaintDotsTask(BufferedImage image, int s, int wstep, Color[] colors, Random random, int first, int last, CountDownLatch latch) {
|
|
this.image = image;
|
|
this.s = s;
|
|
this.wstep = wstep;
|
|
this.colors = colors;
|
|
this.random = random;
|
|
this.last = last;
|
|
this.first = first;
|
|
this.latch = latch;
|
|
}
|
|
|
|
public void run() {
|
|
try {
|
|
paintDots0(image, s, wstep, colors, random, first, last);
|
|
}
|
|
finally {
|
|
latch.countDown();
|
|
}
|
|
}
|
|
}
|
|
|
|
private static class PaintBackgroundTask implements Runnable {
|
|
private final int w;
|
|
private final int h;
|
|
private final DataBuffer buffer;
|
|
private final boolean alpha;
|
|
private final int first;
|
|
private final int last;
|
|
private final CountDownLatch latch;
|
|
|
|
public PaintBackgroundTask(int w, int h, DataBuffer buffer, boolean alpha, int first, int last, CountDownLatch latch) {
|
|
this.w = w;
|
|
this.h = h;
|
|
this.buffer = buffer;
|
|
this.alpha = alpha;
|
|
this.first = first;
|
|
this.last = last;
|
|
this.latch = latch;
|
|
}
|
|
|
|
public void run() {
|
|
try {
|
|
paintBackground0(w, h, buffer, alpha, first, last);
|
|
}
|
|
finally {
|
|
latch.countDown();
|
|
}
|
|
}
|
|
}
|
|
|
|
private static class ConsoleProgressListener extends ProgressListenerBase {
|
|
static final int COLUMNS = System.getenv("COLUMNS") != null ? Integer.parseInt(System.getenv("COLUMNS")) - 2 : 78;
|
|
int left = COLUMNS;
|
|
|
|
@Override
|
|
public void imageComplete(ImageReader source) {
|
|
for (; left > 0; left--) {
|
|
System.out.print(".");
|
|
}
|
|
System.out.println("]");
|
|
}
|
|
|
|
@Override
|
|
public void imageProgress(ImageReader source, float percentageDone) {
|
|
int progress = COLUMNS - Math.round(COLUMNS * percentageDone / 100f);
|
|
if (progress < left) {
|
|
for (; left > progress; left--) {
|
|
System.out.print(".");
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void imageStarted(ImageReader source, int imageIndex) {
|
|
System.out.print("[");
|
|
}
|
|
}
|
|
}
|