package org.mage.plugins.card.images;
import com.google.common.base.Function;
import com.google.common.collect.ComputationException;
import com.google.common.collect.MapMaker;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.imageio.ImageIO;
import mage.client.dialog.PreferencesDialog;
import mage.client.util.TransformedImageCache;
import mage.view.CardView;
import net.java.truevfs.access.TFile;
import net.java.truevfs.access.TFileInputStream;
import net.java.truevfs.access.TFileOutputStream;
import org.apache.log4j.Logger;
import org.mage.plugins.card.constants.Constants;
import org.mage.plugins.card.dl.sources.DirectLinksForDownload;
import org.mage.plugins.card.utils.CardImageUtils;
/**
* This class stores ALL card images in a cache with soft values. this means
* that the images may be garbage collected when they are not needed any more,
* but will be kept as long as possible.
*
* Key format: "[cardname]#[setname]#[type]#[collectorID]#[param]"
*
* where param is:
* <ul>
* <li>size of image</li>
*
* <li>#Normal: request for unrotated image</li>
* <li>#Tapped: request for rotated image</li>
* <li>#Cropped: request for cropped image that is used for Shandalar like card
* look</li>
* </ul>
*/
public final class ImageCache {
private static final Logger LOGGER = Logger.getLogger(ImageCache.class);
private static final Map<String, BufferedImage> IMAGE_CACHE;
/**
* Common pattern for keys. Format: "<cardname>#<setname>#<collectorID>"
*/
private static final Pattern KEY_PATTERN = Pattern.compile("(.*)#(.*)#(.*)#(.*)#(.*)#(.*)");
static {
IMAGE_CACHE = new MapMaker().softValues().makeComputingMap(new Function<String, BufferedImage>() {
@Override
public BufferedImage apply(String key) {
try {
boolean usesVariousArt = false;
if (key.matches(".*#usesVariousArt.*")) {
usesVariousArt = true;
key = key.replace("#usesVariousArt", "");
}
boolean thumbnail = false;
if (key.matches(".*#thumb.*")) {
thumbnail = true;
key = key.replace("#thumb", "");
}
Matcher m = KEY_PATTERN.matcher(key);
if (m.matches()) {
String name = m.group(1);
String set = m.group(2);
Integer type = Integer.parseInt(m.group(3));
String collectorId = m.group(4);
if (collectorId.equals("null")) {
collectorId = "0";
}
String tokenSetCode = m.group(5);
String tokenDescriptor = m.group(6);
CardDownloadData info = new CardDownloadData(name, set, collectorId, usesVariousArt, type, tokenSetCode, tokenDescriptor);
String path;
if (collectorId.isEmpty() || "0".equals(collectorId)) {
info.setToken(true);
path = CardImageUtils.generateTokenImagePath(info);
if (path == null) {
path = DirectLinksForDownload.outDir + File.separator + DirectLinksForDownload.cardbackFilename;
}
} else {
path = CardImageUtils.generateImagePath(info);
}
if (path == null) {
return null;
}
TFile file = getTFile(path);
if (file == null) {
return null;
}
if (thumbnail && path.endsWith(".jpg")) {
String thumbnailPath = buildThumbnailPath(path);
TFile thumbnailFile = null;
try {
thumbnailFile = new TFile(thumbnailPath);
} catch (Exception ex) {
}
boolean exists = false;
if (thumbnailFile != null) {
try {
exists = thumbnailFile.exists();
} catch (Exception ex) {
exists = false;
}
}
if (exists) {
LOGGER.debug("loading thumbnail for " + key + ", path=" + thumbnailPath);
BufferedImage thumbnailImage = loadImage(thumbnailFile);
if (thumbnailImage == null) { // thumbnail exists but broken for some reason
LOGGER.warn("failed loading thumbnail for " + key + ", path=" + thumbnailPath
+ ", thumbnail file is probably broken, attempting to recreate it...");
thumbnailImage = makeThumbnailByFile(key, file, thumbnailPath);
}
return thumbnailImage;
} else {
return makeThumbnailByFile(key, file, thumbnailPath);
}
} else {
BufferedImage image = loadImage(file);
image = getWizardsCard(image);
return image;
}
} else {
throw new RuntimeException(
"Requested image doesn't fit the requirement for key (<cardname>#<setname>#<collectorID>): " + key);
}
} catch (Exception ex) {
if (ex instanceof ComputationException) {
throw (ComputationException) ex;
} else {
throw new ComputationException(ex);
}
}
}
public BufferedImage makeThumbnailByFile(String key, TFile file, String thumbnailPath) {
BufferedImage image = loadImage(file);
image = getWizardsCard(image);
if (image == null) {
return null;
}
LOGGER.debug("creating thumbnail for " + key);
return makeThumbnail(image, thumbnailPath);
}
});
}
private ImageCache() {
}
public static BufferedImage getMorphImage() {
CardDownloadData info = new CardDownloadData("Morph", "KTK", "0", false, 0, "KTK", "");
info.setToken(true);
String path = CardImageUtils.generateTokenImagePath(info);
if (path == null) {
return null;
}
TFile file = getTFile(path);
return loadImage(file);
}
public static BufferedImage getManifestImage() {
CardDownloadData info = new CardDownloadData("Manifest", "FRF", "0", false, 0, "FRF", "");
info.setToken(true);
String path = CardImageUtils.generateTokenImagePath(info);
if (path == null) {
return null;
}
TFile file = getTFile(path);
return loadImage(file);
}
private static String buildThumbnailPath(String path) {
String thumbnailPath;
if (PreferencesDialog.isSaveImagesToZip()) {
thumbnailPath = path.replace(".zip", ".thumb.zip");
} else {
thumbnailPath = path.replace(".jpg", ".thumb.jpg");
}
return thumbnailPath;
}
public static BufferedImage getWizardsCard(BufferedImage image) {
if (image != null && image.getWidth() == 265 && image.getHeight() == 370) {
BufferedImage crop = new BufferedImage(256, 360, BufferedImage.TYPE_INT_RGB);
Graphics2D graphics2D = crop.createGraphics();
graphics2D.drawImage(image, 0, 0, 255, 360, 5, 5, 261, 365, null);
graphics2D.dispose();
return crop;
} else {
return image;
}
}
public static BufferedImage getThumbnail(CardView card) {
return getImage(getKey(card, card.getName(), "#thumb"));
}
public static BufferedImage tryGetThumbnail(CardView card) {
return tryGetImage(getKey(card, card.getName(), "#thumb"));
}
public static BufferedImage getImageOriginal(CardView card) {
return getImage(getKey(card, card.getName(), ""));
}
public static BufferedImage getImageOriginalAlternateName(CardView card) {
return getImage(getKey(card, card.getAlternateName(), ""));
}
/**
* Returns the Image corresponding to the key
*/
private static BufferedImage getImage(String key) {
try {
return IMAGE_CACHE.get(key);
} catch (NullPointerException ex) {
// unfortunately NullOutputException, thrown when apply() returns
// null, is not public
// NullOutputException is a subclass of NullPointerException
// legitimate, happens when a card has no image
return null;
} catch (ComputationException ex) {
if (ex.getCause() instanceof NullPointerException) {
return null;
}
LOGGER.error(ex, ex);
return null;
}
}
/**
* Returns the Image corresponding to the key only if it already exists in
* the cache.
*/
private static BufferedImage tryGetImage(String key) {
return IMAGE_CACHE.containsKey(key) ? IMAGE_CACHE.get(key) : null;
}
/**
* Returns the map key for a card, without any suffixes for the image size.
*/
private static String getKey(CardView card, String name, String suffix) {
return name + '#' + card.getExpansionSetCode() + '#' + card.getType() + '#' + card.getCardNumber() + '#'
+ (card.getTokenSetCode() == null ? "" : card.getTokenSetCode())
+ suffix
+ (card.getUsesVariousArt() ? "#usesVariousArt" : "")
+ (card.getTokenDescriptor() != null ? '#' + card.getTokenDescriptor() : "#");
}
// /**
// * Returns the map key for the flip image of a card, without any suffixes for the image size.
// */
// private static String getKeyAlternateName(CardView card, String alternateName) {
// return alternateName + "#" + card.getExpansionSetCode() + "#" +card.getType()+ "#" + card.getCardNumber() + "#"
// + (card.getTokenSetCode() == null ? "":card.getTokenSetCode());
// }
/**
* Load image from file
*
* @param file file to load image from
* @return {@link BufferedImage}
*/
public static BufferedImage loadImage(TFile file) {
if (file == null) {
return null;
}
if (!file.exists()) {
LOGGER.debug("File does not exist: " + file.toString());
return null;
}
BufferedImage image = null;
try {
try (TFileInputStream inputStream = new TFileInputStream(file)) {
image = ImageIO.read(inputStream);
}
} catch (Exception e) {
LOGGER.error(e, e);
}
return image;
}
public static BufferedImage makeThumbnail(BufferedImage original, String path) {
BufferedImage image = TransformedImageCache.getResizedImage(original, Constants.THUMBNAIL_SIZE_FULL.width, Constants.THUMBNAIL_SIZE_FULL.height);
TFile imageFile = getTFile(path);
if (imageFile == null) {
return null;
}
try {
try (TFileOutputStream outputStream = new TFileOutputStream(imageFile)) {
String format = image.getColorModel().getNumComponents() > 3 ? "png" : "jpg";
ImageIO.write(image, format, outputStream);
}
} catch (IOException e) {
LOGGER.error(e, e);
imageFile.delete();
}
return image;
}
/**
* Returns an image scaled to the size given
*
* @param original
* @return
*/
public static BufferedImage getNormalSizeImage(BufferedImage original) {
if (original == null) {
return null;
}
int srcWidth = original.getWidth();
int srcHeight = original.getHeight();
int tgtWidth = Constants.CARD_SIZE_FULL.width;
int tgtHeight = Constants.CARD_SIZE_FULL.height;
if (srcWidth == tgtWidth && srcHeight == tgtHeight) {
return original;
}
return TransformedImageCache.getResizedImage(original, tgtWidth, tgtHeight);
}
/**
* Returns the image appropriate to display the card in the picture panel
*
* @param card
* @param width
* @param height
* @return
*/
public static BufferedImage getImage(CardView card, int width, int height) {
if (Constants.THUMBNAIL_SIZE_FULL.width + 10 > width) {
return getThumbnail(card);
}
String key = getKey(card, card.getName(), Integer.toString(width));
BufferedImage original = getImage(key);
if (original == null) {
LOGGER.debug(key + " not found");
return null;
}
double scale = Math.min((double) width / original.getWidth(), (double) height / original.getHeight());
if (scale >= 1) {
return original;
}
return TransformedImageCache.getResizedImage(original, (int) (original.getWidth() * scale), (int) (original.getHeight() * scale));
}
/**
* Returns the image appropriate to display for a card in a picture panel,
* but only it was ALREADY LOADED. That is, the call is immediate and will
* not block on file IO.
*
* @param card
* @param width
* @param height
* @return
*/
public static BufferedImage tryGetImage(CardView card, int width, int height) {
if (Constants.THUMBNAIL_SIZE_FULL.width + 10 > width) {
return tryGetThumbnail(card);
}
String key = getKey(card, card.getName(), Integer.toString(width));
BufferedImage original = tryGetImage(key);
if (original == null) {
LOGGER.debug(key + " not found");
return null;
}
double scale = Math.min((double) width / original.getWidth(), (double) height / original.getHeight());
if (scale >= 1) {
return original;
}
return TransformedImageCache.getResizedImage(original, (int) (original.getWidth() * scale), (int) (original.getHeight() * scale));
}
public static TFile getTFile(String path) {
try {
return new TFile(path);
} catch (NullPointerException ex) {
LOGGER.warn("Imagefile does not exist: " + path);
}
return null;
}
}