Merge branch 'add-mobile-art-download' into 'master'

Add option to download missing card art on the fly

See merge request core-developers/forge!1049
This commit is contained in:
Michael Kamensky
2018-11-03 16:23:35 +00:00
15 changed files with 277 additions and 75 deletions

View File

@@ -9,6 +9,8 @@ import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Uri;
@@ -28,6 +30,8 @@ import forge.util.FileUtil;
import forge.util.ThreadUtil;
import java.io.File;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.concurrent.Callable;
public class Main extends AndroidApplication {
@@ -243,5 +247,11 @@ public class Main extends AndroidApplication {
}
});
}
@Override
public void convertToJPEG(InputStream input, OutputStream output) {
Bitmap bmp = BitmapFactory.decodeStream(input);
bmp.compress(Bitmap.CompressFormat.JPEG, 100, output);
}
}
}

View File

@@ -4,6 +4,9 @@ import java.awt.image.BufferedImage;
import forge.game.card.CardView;
import forge.game.player.PlayerView;
import forge.util.ImageFetcher;
import forge.util.SwingImageFetcher;
public abstract class CachedCardImage implements ImageFetcher.Callback {
final CardView card;
@@ -11,6 +14,8 @@ public abstract class CachedCardImage implements ImageFetcher.Callback {
final int width;
final int height;
static final SwingImageFetcher fetcher = new SwingImageFetcher();
public CachedCardImage(final CardView card, final Iterable<PlayerView> viewers, final int width, final int height) {
this.card = card;
this.viewers = viewers;
@@ -19,7 +24,7 @@ public abstract class CachedCardImage implements ImageFetcher.Callback {
BufferedImage image = ImageCache.getImageNoDefault(card, viewers, width, height);
if (image == null) {
String key = card.getCurrentState().getImageKey(viewers);
ImageFetcher.fetchImage(card, key, this);
fetcher.fetchImage(key, this);
}
}

View File

@@ -23,10 +23,7 @@ import forge.sound.*;
import forge.toolbox.FOptionPane;
import forge.toolbox.FSkin;
import forge.toolbox.FSkin.SkinImage;
import forge.util.BuildInfo;
import forge.util.Callback;
import forge.util.FileUtil;
import forge.util.OperatingSystem;
import forge.util.*;
import org.apache.commons.lang3.StringUtils;
import javax.swing.*;
@@ -42,6 +39,8 @@ import java.util.Collection;
import java.util.List;
public class GuiDesktop implements IGuiBase {
private ImageFetcher imageFetcher = new SwingImageFetcher();
@Override
public boolean isRunningOnDesktop() {
return true;
@@ -63,6 +62,11 @@ public class GuiDesktop implements IGuiBase {
"../forge-gui/" : "";
}
@Override
public ImageFetcher getImageFetcher() {
return imageFetcher;
}
@Override
public void invokeInEdtNow(final Runnable proc) {
proc.run();

View File

@@ -24,8 +24,9 @@ import java.awt.image.WritableRaster;
import javax.swing.JPanel;
import forge.GuiBase;
import forge.ImageCache;
import forge.ImageFetcher;
import forge.util.ImageFetcher;
import forge.ImageKeys;
import forge.game.card.CardView.CardStateView;
import forge.item.InventoryItem;
@@ -115,7 +116,7 @@ public final class CardPicturePanel extends JPanel implements ImageFetcher.Callb
CardStateView card = (CardStateView) displayed;
BufferedImage image = ImageCache.getOriginalImage(card.getImageKey(), false);
if (image == null) {
ImageFetcher.fetchImage(card.getCard(), card.getImageKey(), this);
GuiBase.getInterface().getImageFetcher().fetchImage(card.getImageKey(), this);
}
return FImageUtil.getImage((CardStateView) displayed);
}

View File

@@ -0,0 +1,57 @@
package forge.util;
import javax.imageio.ImageIO;
import javax.swing.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.net.URL;
public class SwingImageFetcher extends ImageFetcher {
@Override
protected Runnable getDownloadTask(String[] downloadUrls, String destPath, Runnable notifyObservers) {
return new SwingDownloadTask(downloadUrls, destPath, notifyObservers);
}
private static class SwingDownloadTask implements Runnable {
private final String[] downloadUrls;
private final String destPath;
private final Runnable notifyObservers;
public SwingDownloadTask(String[] downloadUrls, String destPath, Runnable notifyObservers) {
this.downloadUrls = downloadUrls;
this.destPath = destPath;
this.notifyObservers = notifyObservers;
}
private void doFetch(String urlToDownload) throws IOException {
URL url = new URL(urlToDownload);
System.out.println("Attempting to fetch: " + url);
java.net.URLConnection c = url.openConnection();
c.setRequestProperty("User-Agent", "");
BufferedImage image = ImageIO.read(c.getInputStream());
// First, save to a temporary file so that nothing tries to read
// a partial download.
File destFile = new File(destPath + ".tmp");
destFile.mkdirs();
ImageIO.write(image, "jpg", destFile);
// Now, rename it to the correct name.
destFile.renameTo(new File(destPath));
System.out.println("Saved image to " + destPath);
SwingUtilities.invokeLater(notifyObservers);
}
public void run() {
for (String urlToDownload : downloadUrls) {
try {
doFetch(urlToDownload);
break;
} catch (IOException e) {
System.out.println("Failed to download card [" + destPath + "] image: " + e.getMessage());
}
}
}
}
}

View File

@@ -13,6 +13,10 @@ import org.robovm.apple.foundation.NSAutoreleasePool;
import org.robovm.apple.uikit.UIApplication;
import org.robovm.apple.uikit.UIPasteboard;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class Main extends IOSApplication.Delegate {
@Override
@@ -94,5 +98,10 @@ public class Main extends IOSApplication.Delegate {
public void exit() {
// Not possible on iOS
}
@Override
public void convertToJPEG(InputStream input, OutputStream output) throws IOException {
}
}
}

View File

@@ -13,9 +13,10 @@ import forge.util.RestartUtil;
import forge.util.Utils;
import org.apache.commons.cli.*;
import javax.imageio.ImageIO;
import java.awt.*;
import java.io.File;
import java.io.IOException;
import java.awt.image.BufferedImage;
import java.io.*;
public class Main {
public static void main(String[] args) {
@@ -162,5 +163,11 @@ public class Main {
public void preventSystemSleep(boolean preventSleep) {
OperatingSystem.preventSystemSleep(preventSleep);
}
@Override
public void convertToJPEG(InputStream input, OutputStream output) throws IOException {
BufferedImage image = ImageIO.read(input);
ImageIO.write(image, "jpg", output);
}
}
}

View File

@@ -0,0 +1,40 @@
package forge;
import com.badlogic.gdx.graphics.Texture;
import forge.assets.ImageCache;
import forge.game.card.CardView;
import forge.item.InventoryItem;
import forge.util.ImageFetcher;
public abstract class CachedCardImage implements ImageFetcher.Callback {
protected final String key;
static final ImageFetcher fetcher = GuiBase.getInterface().getImageFetcher();
public CachedCardImage(final CardView card) {
key = card.getCurrentState().getImageKey();
fetch();
}
public CachedCardImage(final InventoryItem ii) {
key = ii.getImageKey(false);
fetch();
}
public CachedCardImage(String key) {
this.key = key;
fetch();
}
public void fetch() {
Texture image = ImageCache.getImage(key, false);
if (image == null) {
fetcher.fetchImage(key, this);
}
}
public Texture getImage() {
return ImageCache.getImage(key, true);
}
public abstract void onImageFetched();
}

View File

@@ -37,14 +37,11 @@ import forge.sound.IAudioClip;
import forge.sound.IAudioMusic;
import forge.toolbox.FOptionPane;
import forge.toolbox.GuiChoose;
import forge.util.Callback;
import forge.util.FileUtil;
import forge.util.ThreadUtil;
import forge.util.WaitCallback;
import forge.util.WaitRunnable;
import forge.util.*;
public class GuiMobile implements IGuiBase {
private final String assetsDir;
private ImageFetcher imageFetcher = new LibGDXImageFetcher();
public GuiMobile(final String assetsDir0) {
assetsDir = assetsDir0;
@@ -70,6 +67,11 @@ public class GuiMobile implements IGuiBase {
return assetsDir;
}
@Override
public ImageFetcher getImageFetcher() {
return imageFetcher;
}
@Override
public void invokeInEdtNow(final Runnable proc) {
proc.run();

View File

@@ -15,6 +15,7 @@ import com.badlogic.gdx.graphics.g2d.freetype.FreeTypeFontGenerator;
import com.badlogic.gdx.graphics.g2d.freetype.FreeTypeFontGenerator.FreeTypeFontParameter;
import com.badlogic.gdx.graphics.glutils.PixmapTextureData;
import com.badlogic.gdx.utils.Array;
import forge.CachedCardImage;
import forge.FThreads;
import forge.Graphics;
import forge.StaticData;
@@ -28,6 +29,7 @@ import forge.game.card.CardView.CardStateView;
import forge.game.keyword.Keyword;
import forge.game.card.CounterType;
import forge.item.IPaperCard;
import forge.item.InventoryItem;
import forge.item.PaperCard;
import forge.model.FModel;
import forge.properties.ForgeConstants;
@@ -51,6 +53,34 @@ public class CardRenderer {
BehindVert
}
// class that simplifies the callback logic of CachedCardImage
static class RendererCachedCardImage extends CachedCardImage {
boolean clearCardArtCache = false;
public RendererCachedCardImage(CardView card, boolean clearArtCache) {
super(card);
this.clearCardArtCache = clearArtCache;
}
public RendererCachedCardImage(InventoryItem ii, boolean clearArtCache) {
super(ii);
this.clearCardArtCache = clearArtCache;
}
public RendererCachedCardImage(String key, boolean clearArtCache) {
super(key);
this.clearCardArtCache = clearArtCache;
}
@Override
public void onImageFetched() {
ImageCache.clear();
if (clearCardArtCache) {
cardArtCache.remove(key);
}
}
}
private static final FSkinFont NAME_FONT = FSkinFont.get(16);
public static final float NAME_BOX_TINT = 0.2f;
public static final float TEXT_BOX_TINT = 0.1f;
@@ -124,7 +154,7 @@ public class CardRenderer {
public static FImageComplex getCardArt(String imageKey, boolean isSplitCard, boolean isHorizontalCard, boolean isAftermathCard) {
FImageComplex cardArt = cardArtCache.get(imageKey);
if (cardArt == null) {
Texture image = ImageCache.getImage(imageKey, true);
Texture image = new RendererCachedCardImage(imageKey, true).getImage();
if (image != null) {
if (image == ImageCache.defaultImage) {
cardArt = CardImageRenderer.forgeArt;
@@ -186,7 +216,13 @@ public class CardRenderer {
public static FImageComplex getAftermathSecondCardArt(String imageKey) {
FImageComplex cardArt = cardArtCache.get("Aftermath_second_"+imageKey);
if (cardArt == null) {
Texture image = ImageCache.getImage(imageKey, true);
Texture image = new CachedCardImage(imageKey) {
@Override
public void onImageFetched() {
ImageCache.clear();
cardArtCache.remove("Aftermath_second_" + imageKey);
}
}.getImage();
if (image != null) {
if (image == ImageCache.defaultImage) {
cardArt = CardImageRenderer.forgeArt;
@@ -347,7 +383,8 @@ public class CardRenderer {
}
public static void drawCard(Graphics g, IPaperCard pc, float x, float y, float w, float h, CardStackPosition pos) {
Texture image = ImageCache.getImage(pc);
Texture image = new RendererCachedCardImage(pc, false).getImage();
if (image != null) {
if (image == ImageCache.defaultImage) {
CardImageRenderer.drawCardImage(g, CardView.getCardForUi(pc), false, x, y, w, h, pos);
@@ -369,7 +406,8 @@ public class CardRenderer {
}
public static void drawCard(Graphics g, CardView card, float x, float y, float w, float h, CardStackPosition pos, boolean rotate) {
Texture image = ImageCache.getImage(card);
Texture image = new RendererCachedCardImage(card, false).getImage();;
if (image != null) {
if (image == ImageCache.defaultImage) {
CardImageRenderer.drawCardImage(g, card, false, x, y, w, h, pos);

View File

@@ -204,6 +204,10 @@ public class SettingsPage extends TabPage<SettingsScreen> {
"If turned on, Forge will load all historic format definitions, this may take slightly longer to load at startup."), 3);
//Graphic Options
lstSettings.addItem(new BooleanSetting(FPref.UI_ENABLE_ONLINE_IMAGE_FETCHER,
"Download missing card art",
"Automatically download missing card art"),
4);
lstSettings.addItem(new BooleanSetting(FPref.UI_OVERLAY_FOIL_EFFECT,
"Display Foil Overlay",
"Displays foil cards with the visual foil overlay effect."),

View File

@@ -0,0 +1,62 @@
package forge.util;
import com.badlogic.gdx.files.FileHandle;
import forge.Forge;
import forge.GuiBase;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
public class LibGDXImageFetcher extends ImageFetcher {
@Override
protected Runnable getDownloadTask(String[] downloadUrls, String destPath, Runnable notifyObservers) {
return new LibGDXDownloadTask(downloadUrls, destPath, notifyObservers);
}
private static class LibGDXDownloadTask implements Runnable {
private final String[] downloadUrls;
private final String destPath;
private final Runnable notifyObservers;
LibGDXDownloadTask(String[] downloadUrls, String destPath, Runnable notifyObservers) {
this.downloadUrls = downloadUrls;
this.destPath = destPath;
this.notifyObservers = notifyObservers;
}
private void doFetch(String urlToDownload) throws IOException {
URL url = new URL(urlToDownload);
System.out.println("Attempting to fetch: " + url);
java.net.URLConnection c = url.openConnection();
c.setRequestProperty("User-Agent", "");
InputStream is = c.getInputStream();
// First, save to a temporary file so that nothing tries to read
// a partial download.
FileHandle destFile = new FileHandle(destPath + ".tmp");
System.out.println(destPath);
destFile.parent().mkdirs();
// Conversion to JPEG will be handled differently depending on the platform
Forge.getDeviceAdapter().convertToJPEG(is, new FileOutputStream(destFile.file()));
destFile.moveTo(new FileHandle(destPath));
System.out.println("Saved image to " + destPath);
GuiBase.getInterface().invokeInEdtLater(notifyObservers);
}
public void run() {
for (String urlToDownload : downloadUrls) {
try {
doFetch(urlToDownload);
break;
} catch (IOException e) {
System.out.println("Failed to download card [" + destPath + "] image: " + e.getMessage());
}
}
}
}
}

View File

@@ -1,5 +1,9 @@
package forge.interfaces;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public interface IDeviceAdapter {
boolean isConnectedToInternet();
boolean isConnectedToWifi();
@@ -10,4 +14,5 @@ public interface IDeviceAdapter {
void preventSystemSleep(boolean preventSleep);
void restart();
void exit();
void convertToJPEG(InputStream input, OutputStream output) throws IOException;
}

View File

@@ -16,12 +16,14 @@ import forge.match.HostedMatch;
import forge.sound.IAudioClip;
import forge.sound.IAudioMusic;
import forge.util.Callback;
import forge.util.ImageFetcher;
public interface IGuiBase {
boolean isRunningOnDesktop();
boolean isLibgdxPort();
String getCurrentVersion();
String getAssetsDir();
ImageFetcher getImageFetcher();
void invokeInEdtNow(Runnable runnable);
void invokeInEdtLater(Runnable runnable);
void invokeInEdtAndWait(Runnable proc);

View File

@@ -1,34 +1,28 @@
package forge;
package forge.util;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javax.imageio.ImageIO;
import javax.swing.SwingUtilities;
import forge.FThreads;
import forge.ImageKeys;
import forge.StaticData;
import org.apache.commons.lang3.tuple.Pair;
import forge.game.card.CardView;
import forge.item.PaperCard;
import forge.model.FModel;
import forge.properties.ForgeConstants;
import forge.properties.ForgePreferences;
import forge.util.FileUtil;
import forge.util.ImageUtil;
public class ImageFetcher {
public abstract class ImageFetcher {
private static final ExecutorService threadPool = Executors.newCachedThreadPool();
private static HashMap<String, HashSet<Callback>> currentFetches = new HashMap<>();
private static HashMap<String, String> tokenImages;
private HashMap<String, HashSet<Callback>> currentFetches = new HashMap<>();
private HashMap<String, String> tokenImages;
public static void fetchImage(final CardView card, final String imageKey, final Callback callback) {
public void fetchImage(final String imageKey, final Callback callback) {
FThreads.assertExecutedByEdt(true);
if (!FModel.getPreferences().getPrefBoolean(ForgePreferences.FPref.UI_ENABLE_ONLINE_IMAGE_FETCHER))
@@ -119,50 +113,12 @@ public class ImageFetcher {
currentFetches.remove(destPath);
}
};
threadPool.submit(new DownloadTask(downloadUrls.toArray(new String[0]), destPath, notifyObservers));
threadPool.submit(getDownloadTask(downloadUrls.toArray(new String[0]), destPath, notifyObservers));
}
protected abstract Runnable getDownloadTask(String[] toArray, String destPath, Runnable notifyObservers);
public static interface Callback {
public void onImageFetched();
}
private static class DownloadTask implements Runnable {
private final String[] downloadUrls;
private final String destPath;
private final Runnable notifyObservers;
public DownloadTask(String[] downloadUrls, String destPath, Runnable notifyObservers) {
this.downloadUrls = downloadUrls;
this.destPath = destPath;
this.notifyObservers = notifyObservers;
}
private void doFetch(String urlToDownload) throws IOException {
URL url = new URL(urlToDownload);
System.out.println("Attempting to fetch: " + url);
java.net.URLConnection c = url.openConnection();
c.setRequestProperty("User-Agent", "");
BufferedImage image = ImageIO.read(c.getInputStream());
// First, save to a temporary file so that nothing tries to read
// a partial download.
File destFile = new File(destPath + ".tmp");
destFile.mkdirs();
ImageIO.write(image, "jpg", destFile);
// Now, rename it to the correct name.
destFile.renameTo(new File(destPath));
System.out.println("Saved image to " + destPath);
SwingUtilities.invokeLater(notifyObservers);
}
public void run() {
for (String urlToDownload : downloadUrls) {
try {
doFetch(urlToDownload);
break;
} catch (IOException e) {
System.out.println("Failed to download card [" + destPath + "] image: " + e.getMessage());
}
}
}
}
}