From e3d84ca2120f8407d4d05add4cf4733bd988b6b8 Mon Sep 17 00:00:00 2001 From: draxdyn Date: Wed, 1 Jun 2016 17:05:28 +0200 Subject: [PATCH] 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. --- .../main/java/mage/client/cards/BigCard.java | 9 +- .../src/main/java/mage/client/cards/Card.java | 2 +- .../java/mage/client/cards/Permanent.java | 4 +- .../client/deckeditor/table/TableModel.java | 3 +- .../java/mage/client/draft/DraftPanel.java | 2 +- .../plugins/adapters/MageActionCallback.java | 8 +- .../java/mage/client/util/ImageHelper.java | 115 +------------ .../client/util/TransformedImageCache.java | 153 ++++++++++++++++++ .../mage/plugins/card/images/ImageCache.java | 40 +---- 9 files changed, 179 insertions(+), 157 deletions(-) create mode 100644 Mage.Client/src/main/java/mage/client/util/TransformedImageCache.java diff --git a/Mage.Client/src/main/java/mage/client/cards/BigCard.java b/Mage.Client/src/main/java/mage/client/cards/BigCard.java index 75fa550878..cc3417c1b4 100644 --- a/Mage.Client/src/main/java/mage/client/cards/BigCard.java +++ b/Mage.Client/src/main/java/mage/client/cards/BigCard.java @@ -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 strings) { + public void setCard(UUID cardId, EnlargeMode enlargeMode, Image image, List 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); diff --git a/Mage.Client/src/main/java/mage/client/cards/Card.java b/Mage.Client/src/main/java/mage/client/cards/Card.java index 8d6235a08a..1a43fca155 100644 --- a/Mage.Client/src/main/java/mage/client/cards/Card.java +++ b/Mage.Client/src/main/java/mage/client/cards/Card.java @@ -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 diff --git a/Mage.Client/src/main/java/mage/client/cards/Permanent.java b/Mage.Client/src/main/java/mage/client/cards/Permanent.java index a01e368641..0db10e5887 100644 --- a/Mage.Client/src/main/java/mage/client/cards/Permanent.java +++ b/Mage.Client/src/main/java/mage/client/cards/Permanent.java @@ -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(); } diff --git a/Mage.Client/src/main/java/mage/client/deckeditor/table/TableModel.java b/Mage.Client/src/main/java/mage/client/deckeditor/table/TableModel.java index baa4cf5f85..65c5ebd027 100644 --- a/Mage.Client/src/main/java/mage/client/deckeditor/table/TableModel.java +++ b/Mage.Client/src/main/java/mage/client/deckeditor/table/TableModel.java @@ -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()); + bigCard.setCard(card.getId(), EnlargeMode.NORMAL, image, new ArrayList(), false); } else { drawCardText(card); } diff --git a/Mage.Client/src/main/java/mage/client/draft/DraftPanel.java b/Mage.Client/src/main/java/mage/client/draft/DraftPanel.java index 109e3c9e91..e55588b743 100644 --- a/Mage.Client/src/main/java/mage/client/draft/DraftPanel.java +++ b/Mage.Client/src/main/java/mage/client/draft/DraftPanel.java @@ -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; diff --git a/Mage.Client/src/main/java/mage/client/plugins/adapters/MageActionCallback.java b/Mage.Client/src/main/java/mage/client/plugins/adapters/MageActionCallback.java index 8a0bee2366..4c18ca7fb2 100644 --- a/Mage.Client/src/main/java/mage/client/plugins/adapters/MageActionCallback.java +++ b/Mage.Client/src/main/java/mage/client/plugins/adapters/MageActionCallback.java @@ -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(); diff --git a/Mage.Client/src/main/java/mage/client/util/ImageHelper.java b/Mage.Client/src/main/java/mage/client/util/ImageHelper.java index e4d55b0df4..1eddc28e0d 100644 --- a/Mage.Client/src/main/java/mage/client/util/ImageHelper.java +++ b/Mage.Client/src/main/java/mage/client/util/ImageHelper.java @@ -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 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); } /** diff --git a/Mage.Client/src/main/java/mage/client/util/TransformedImageCache.java b/Mage.Client/src/main/java/mage/client/util/TransformedImageCache.java new file mode 100644 index 0000000000..9a2cf205dc --- /dev/null +++ b/Mage.Client/src/main/java/mage/client/util/TransformedImageCache.java @@ -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> IMAGE_CACHE; + + static + { + // TODO: can we use a single map? + IMAGE_CACHE = ImageCaches.register(new MapMaker().softValues().makeComputingMap(new Function>() { + @Override + public Map apply(final Key key) { + return new MapMaker().weakKeys().softValues().makeComputingMap(new Function() { + @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); + } +} diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/images/ImageCache.java b/Mage.Client/src/main/java/org/mage/plugins/card/images/ImageCache.java index 86f7b6c5d0..3234bcc028 100644 --- a/Mage.Client/src/main/java/org/mage/plugins/card/images/ImageCache.java +++ b/Mage.Client/src/main/java/org/mage/plugins/card/images/ImageCache.java @@ -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) {