Cache resized and rotated images

RotatedResizedImageCache and MultiRotatedResizedImageCache contain the caching
machinery.

A cache of rotated and resized images is added to ImageCache, and is used by
the resizing functions there.

All the resizing and rotation functions in ImageHelper are redirected to the
ones in ImageCache.

This is slightly inefficient because it will cache some calls that are never
repeated, but it prevents developers from mistakenly using uncached functions
when calls are repeated, seriously impacting performance.

Also resizing functions that only take a width or an height have been removed,
and their calls fixed to provide the other dimension. It's still possible to
specify -1 as width or height to ignore constraints in that dimension, though.

Greatly speeds up UI performance.
This commit is contained in:
draxdyn 2016-06-01 17:05:28 +02:00
parent 1999dfe5c0
commit e3d84ca212
9 changed files with 179 additions and 157 deletions

View file

@ -54,6 +54,7 @@ import mage.client.plugins.impl.Plugins;
import mage.client.util.ImageHelper;
import mage.constants.EnlargeMode;
import org.jdesktop.swingx.JXPanel;
import mage.client.util.TransformedImageCache;
/**
* Class for displaying big image of the card
@ -103,7 +104,13 @@ public class BigCard extends JComponent {
}
public void setCard(UUID cardId, EnlargeMode enlargeMode, Image image, List<String> strings) {
public void setCard(UUID cardId, EnlargeMode enlargeMode, Image image, List<String> strings, boolean rotate) {
if (rotate && getWidth() > getHeight()) {
image = TransformedImageCache.getRotatedResizedImage((BufferedImage)image, getHeight(), getWidth(), Math.toRadians(90.0));
} else {
image = TransformedImageCache.getResizedImage((BufferedImage)image, getWidth(), getHeight());
}
if (this.cardId == null || !enlargeMode.equals(this.enlargeMode) || !this.cardId.equals(cardId)) {
if (this.panel != null) {
remove(this.panel);

View file

@ -374,7 +374,7 @@ public class Card extends MagePermanent implements MouseMotionListener, MouseLis
@Override
public void mouseMoved(MouseEvent arg0) {
this.bigCard.showTextComponent();
this.bigCard.setCard(card.getId(), EnlargeMode.NORMAL, image, getRules());
this.bigCard.setCard(card.getId(), EnlargeMode.NORMAL, image, getRules(), false);
}
@Override

View file

@ -56,6 +56,8 @@ import mage.client.util.ImageHelper;
import mage.constants.CardType;
import mage.view.CounterView;
import mage.view.PermanentView;
import org.mage.plugins.card.images.ImageCache;
import mage.client.util.TransformedImageCache;
/**
*
@ -215,7 +217,7 @@ public class Permanent extends Card {
Graphics2D g = (Graphics2D) tappedImage.getGraphics();
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g.drawImage(this.createImage(ImageHelper.rotate(small, dimension)), 0, 0, this);
g.drawImage(TransformedImageCache.getRotatedResizedImage(small, dimension.frameWidth, dimension.frameHeight, Math.toRadians(90.0)), 0, 0, this);
g.dispose();
}

View file

@ -409,8 +409,7 @@ public class TableModel extends AbstractTableModel implements ICardGrid {
Image image = Plugins.getInstance().getOriginalImage(card);
if (image != null && image instanceof BufferedImage) {
// XXX: scaled to fit width
image = ImageHelper.getResizedImage((BufferedImage) image, bigCard.getWidth());
bigCard.setCard(card.getId(), EnlargeMode.NORMAL, image, new ArrayList<String>());
bigCard.setCard(card.getId(), EnlargeMode.NORMAL, image, new ArrayList<String>(), false);
} else {
drawCardText(card);
}

View file

@ -228,7 +228,7 @@ public class DraftPanel extends javax.swing.JPanel {
int height = left * 18;
lblTableImage.setSize(new Dimension(lblTableImage.getWidth(), height));
Image tableImage = ImageHelper.getImageFromResources(draftView.getBoosterNum() == 2 ? "/draft/table_left.png" : "/draft/table_right.png");
BufferedImage resizedTable = ImageHelper.getResizedImage(BufferedImageBuilder.bufferImage(tableImage, BufferedImage.TYPE_INT_ARGB), lblTableImage.getWidth());
BufferedImage resizedTable = ImageHelper.getResizedImage(BufferedImageBuilder.bufferImage(tableImage, BufferedImage.TYPE_INT_ARGB), lblTableImage.getWidth(), lblTableImage.getHeight());
lblTableImage.setIcon(new ImageIcon(resizedTable));
int count = 0;

View file

@ -598,13 +598,7 @@ public class MageActionCallback implements ActionCallback {
private void displayCardInfo(MageCard mageCard, Image image, BigCard bigCard) {
if (image != null && image instanceof BufferedImage) {
// XXX: scaled to fit width
if (mageCard.getOriginal().isToRotate() && bigCard.getWidth() > bigCard.getHeight()) {
image = ImageHelper.getResizedImage((BufferedImage) image, bigCard.getHeight());
image = ImageHelper.rotate((BufferedImage) image, Math.toRadians(90));
} else {
image = ImageHelper.getResizedImage((BufferedImage) image, bigCard.getWidth());
}
bigCard.setCard(mageCard.getOriginal().getId(), enlargeMode, image, mageCard.getOriginal().getRules());
bigCard.setCard(mageCard.getOriginal().getId(), enlargeMode, image, mageCard.getOriginal().getRules(), mageCard.getOriginal().isToRotate());
// if it's an ability, show only the ability text as overlay
if (mageCard.getOriginal().isAbility() && enlargeMode.equals(EnlargeMode.NORMAL)) {
bigCard.showTextComponent();

View file

@ -52,6 +52,7 @@ import static mage.client.constants.Constants.FRAME_MAX_WIDTH;
import static mage.client.constants.Constants.SYMBOL_MAX_SPACE;
import mage.view.CardView;
import org.mage.card.arcane.UI;
import org.mage.plugins.card.images.ImageCache;
/**
*
@ -70,21 +71,6 @@ public class ImageHelper {
return null;
}
/**
*
* @param ref - image name
* @param height - height after scaling
* @return a scaled image that preserves the original aspect ratio, with a
* specified height
*/
public static BufferedImage loadImage(String ref, int height) {
BufferedImage image = loadImage(ref);
if (image != null) {
return scaleImage(image, height);
}
return null;
}
public static BufferedImage loadImage(String ref) {
if (!images.containsKey(ref)) {
try {
@ -107,67 +93,7 @@ public class ImageHelper {
}
public static BufferedImage scaleImage(BufferedImage image, int width, int height) {
BufferedImage scaledImage = image;
int w = image.getWidth();
int h = image.getHeight();
do {
w /= 2;
h /= 2;
if (w < width || h < height) {
w = width;
h = height;
}
BufferedImage newImage = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
Graphics2D graphics2D = newImage.createGraphics();
graphics2D.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
graphics2D.drawImage(scaledImage, 0, 0, w, h, null);
graphics2D.dispose();
scaledImage = newImage;
} while (w != width || h != height);
return scaledImage;
}
public static BufferedImage scaleImage(BufferedImage image, int height) {
double ratio = height / (double) image.getHeight();
int width = (int) (image.getWidth() * ratio);
return scaleImage(image, width, height);
}
public static MemoryImageSource rotate(Image image, CardDimensions dimensions) {
int buffer[] = new int[dimensions.frameWidth * dimensions.frameHeight];
int rotate[] = new int[dimensions.frameHeight * dimensions.frameWidth];
PixelGrabber grabber = new PixelGrabber(image, 0, 0, dimensions.frameWidth, dimensions.frameHeight, buffer, 0, dimensions.frameWidth);
try {
grabber.grabPixels();
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int y = 0; y < dimensions.frameHeight; y++) {
for (int x = 0; x < dimensions.frameWidth; x++) {
rotate[((dimensions.frameWidth - x - 1) * dimensions.frameHeight) + y] = buffer[(y * dimensions.frameWidth) + x];
}
}
return new MemoryImageSource(dimensions.frameHeight, dimensions.frameWidth, rotate, 0, dimensions.frameHeight);
}
public static BufferedImage rotate(BufferedImage image, double angle) {
double sin = Math.abs(Math.sin(angle)), cos = Math.abs(Math.cos(angle));
int w = image.getWidth(), h = image.getHeight();
int neww = (int) Math.floor(w * cos + h * sin), newh = (int) Math.floor(h * cos + w * sin);
GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
GraphicsDevice gs = ge.getDefaultScreenDevice();
GraphicsConfiguration gc = gs.getDefaultConfiguration();
BufferedImage result = gc.createCompatibleImage(neww, newh, Transparency.TRANSLUCENT);
Graphics2D g = result.createGraphics();
g.translate((neww - w) / 2, (newh - h) / 2);
g.rotate(angle, w / 2, h / 2);
g.drawRenderedImage(image, null);
g.dispose();
return result;
return TransformedImageCache.getResizedImage(image, width, height);
}
public static void drawCosts(List<String> costs, Graphics2D g, int xOffset, int yOffset, ImageObserver o) {
@ -191,26 +117,7 @@ public class ImageHelper {
* @return
*/
public static BufferedImage getResizedImage(BufferedImage original, int width, int height) {
ResampleOp resampleOp = new ResampleOp(width, height);
BufferedImage image = resampleOp.filter(original, null);
return image;
}
/**
* Returns an image scaled to fit width panel
*
* @param original
* @param width
* @return
*/
public static BufferedImage getResizedImage(BufferedImage original, int width) {
if (width != original.getWidth()) {
double ratio = width / (double) original.getWidth();
int height = (int) (original.getHeight() * ratio);
return getResizedImage(original, width, height);
} else {
return original;
}
return TransformedImageCache.getResizedImage(original, width, height);
}
/**
@ -223,17 +130,7 @@ public class ImageHelper {
* @return scaled image
*/
public static BufferedImage scale(BufferedImage sbi, int imageType, int dWidth, int dHeight) {
BufferedImage dbi = null;
if (sbi != null) {
double fWidth = dWidth / sbi.getWidth();
double fHeight = dHeight / sbi.getHeight();
dbi = new BufferedImage(dWidth, dHeight, imageType);
Graphics2D g = dbi.createGraphics();
AffineTransform at = AffineTransform.getScaleInstance(fWidth, fHeight);
g.drawRenderedImage(sbi, at);
g.dispose();
}
return dbi;
return TransformedImageCache.getResizedImage(sbi, dWidth, dHeight);
}
/**
@ -244,9 +141,7 @@ public class ImageHelper {
* @return
*/
public static BufferedImage getResizedImage(BufferedImage original, Rectangle sizeNeed) {
ResampleOp resampleOp = new ResampleOp(sizeNeed.width, sizeNeed.height);
BufferedImage image = resampleOp.filter(original, null);
return image;
return TransformedImageCache.getResizedImage(original, sizeNeed.width, sizeNeed.height);
}
/**

View file

@ -0,0 +1,153 @@
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package mage.client.util;
import com.google.common.base.Function;
import com.google.common.collect.MapMaker;
import com.mortennobel.imagescaling.ResampleOp;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.Transparency;
import java.awt.image.BufferedImage;
import java.util.Map;
import mage.client.util.ImageHelper;
/**
*
* @author user
*/
public class TransformedImageCache {
private final static class Key
{
final int width;
final int height;
final double angle;
public Key(int width, int height, double angle) {
this.width = width;
this.height = height;
this.angle = angle;
}
@Override
public int hashCode() {
int hash = 3;
hash = 53 * hash + this.width;
hash = 53 * hash + this.height;
hash = 53 * hash + (int) (Double.doubleToLongBits(this.angle) ^ (Double.doubleToLongBits(this.angle) >>> 32));
return hash;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final Key other = (Key) obj;
if (this.width != other.width) {
return false;
}
if (this.height != other.height) {
return false;
}
if (Double.doubleToLongBits(this.angle) != Double.doubleToLongBits(other.angle)) {
return false;
}
return true;
}
}
static Map<Key, Map<BufferedImage, BufferedImage>> IMAGE_CACHE;
static
{
// TODO: can we use a single map?
IMAGE_CACHE = ImageCaches.register(new MapMaker().softValues().makeComputingMap(new Function<Key, Map<BufferedImage, BufferedImage>>() {
@Override
public Map<BufferedImage, BufferedImage> apply(final Key key) {
return new MapMaker().weakKeys().softValues().makeComputingMap(new Function<BufferedImage, BufferedImage>() {
@Override
public BufferedImage apply(BufferedImage image) {
if(key.width != image.getWidth() || key.height != image.getHeight())
image = resizeImage(image, key.width, key.height);
if(key.angle != 0.0)
image = rotateImage(image, key.angle);
return image;
}
});
}
}));
}
private static BufferedImage rotateImage(BufferedImage image, double angle) {
double sin = Math.abs(Math.sin(angle)), cos = Math.abs(Math.cos(angle));
int w = image.getWidth(), h = image.getHeight();
int neww = (int) Math.floor(w * cos + h * sin), newh = (int) Math.floor(h * cos + w * sin);
GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
GraphicsDevice gs = ge.getDefaultScreenDevice();
GraphicsConfiguration gc = gs.getDefaultConfiguration();
BufferedImage result = gc.createCompatibleImage(neww, newh, Transparency.TRANSLUCENT);
Graphics2D g = result.createGraphics();
g.translate((neww - w) / 2, (newh - h) / 2);
g.rotate(angle, w / 2, h / 2);
g.drawRenderedImage(image, null);
g.dispose();
return result;
}
private static BufferedImage resizeImage(BufferedImage original, int width, int height) {
ResampleOp resampleOp = new ResampleOp(width, height);
BufferedImage image = resampleOp.filter(original, null);
return image;
}
public static BufferedImage getResizedImage(BufferedImage image, int width, int height)
{
return getRotatedResizedImage(image, width, height, 0.0);
}
public static BufferedImage getRotatedImage(BufferedImage image, double angle)
{
return getRotatedResizedImage(image, -1, -1, angle);
}
public static BufferedImage getRotatedResizedImage(BufferedImage image, int width, int height, double angle)
{
int imageWidth = image.getWidth();
int imageHeight = image.getHeight();
if(angle == 0.0 && (width < 0 || imageWidth == width) && (height < 0 || imageHeight == height))
return image;
int resWidth;
int resHeight;
if(width < 0 && height < 0) {
resWidth = imageWidth;
resHeight = imageHeight;
} else if((height < 0) || (width >= 0 && imageHeight * width <= imageWidth * height)) {
resWidth = width;
resHeight = imageHeight * width / imageWidth;
} else {
resWidth = imageWidth * height / imageHeight;
resHeight = height;
}
if(angle == 0.0 && imageWidth == resWidth && imageHeight == resHeight)
return image;
return IMAGE_CACHE.get(new Key(resWidth, resHeight, angle)).get(image);
}
}

View file

@ -1,5 +1,6 @@
package org.mage.plugins.card.images;
import mage.client.util.TransformedImageCache;
import com.google.common.base.Function;
import com.google.common.collect.ComputationException;
import com.google.common.collect.MapMaker;
@ -274,7 +275,7 @@ public class ImageCache {
}
public static BufferedImage makeThumbnail(BufferedImage original, String path) {
BufferedImage image = getResizedImage(original, Constants.THUMBNAIL_SIZE_FULL);
BufferedImage image = TransformedImageCache.getResizedImage(original, Constants.THUMBNAIL_SIZE_FULL.width, Constants.THUMBNAIL_SIZE_FULL.height);
TFile imageFile = getTFile(path);
if (imageFile == null) {
return null;
@ -312,36 +313,7 @@ public class ImageCache {
return original;
}
ResampleOp resampleOp = new ResampleOp(tgtWidth, tgtHeight);
BufferedImage image = resampleOp.filter(original, null);
return image;
}
/**
* Returns an image scaled to the size appropriate for the card picture
* panel For future use.
*/
private static BufferedImage getFullSizeImage(BufferedImage original, double scale) {
if (scale == 1) {
return original;
}
ResampleOp resampleOp = new ResampleOp((int) (original.getWidth() * scale), (int) (original.getHeight() * scale));
BufferedImage image = resampleOp.filter(original, null);
return image;
}
/**
* Returns an image scaled to the size appropriate for the card picture
* panel
*
* @param original
* @param sizeNeed
* @return
*/
public static BufferedImage getResizedImage(BufferedImage original, Rectangle sizeNeed) {
ResampleOp resampleOp = new ResampleOp(sizeNeed.width, sizeNeed.height);
BufferedImage image = resampleOp.filter(original, null);
return image;
return TransformedImageCache.getResizedImage(original, tgtWidth, tgtHeight);
}
/**
@ -364,11 +336,11 @@ public class ImageCache {
}
double scale = Math.min((double) width / original.getWidth(), (double) height / original.getHeight());
if (scale > 1) {
scale = 1;
if (scale >= 1) {
return original;
}
return getFullSizeImage(original, scale);
return TransformedImageCache.getResizedImage(original, (int)(original.getWidth() * scale), (int)(original.getHeight() * scale));
}
public static TFile getTFile(String path) {