diff --git a/source/net/filebot/ResourceManager.java b/source/net/filebot/ResourceManager.java index d9a4bac3..c33f2bb1 100644 --- a/source/net/filebot/ResourceManager.java +++ b/source/net/filebot/ResourceManager.java @@ -64,17 +64,12 @@ public final class ResourceManager { image.add(ImageIO.read(r)); } - // Windows 10: use @2x image for non-integer scale factors 1.25 / 1.5 / 1.75 - if (PRIMARY_SCALE_FACTOR != 1 && PRIMARY_SCALE_FACTOR != 2) { - BufferedImage hidpi = image.get(image.size() - 1); - if (PRIMARY_SCALE_FACTOR < 2) { - image.add(1, scale(PRIMARY_SCALE_FACTOR, hidpi)); - } else { - image.add(scale(PRIMARY_SCALE_FACTOR, hidpi)); - } + // Windows 10: use down-scaled @2x image for non-integer scale factors 1.25 / 1.5 / 1.75 + if (PRIMARY_SCALE_FACTOR > 1 && PRIMARY_SCALE_FACTOR < 2 && image.size() >= 2) { + image.add(1, scale(PRIMARY_SCALE_FACTOR / 2, image.get(1))); } - return new BaseMultiResolutionImage(image.toArray(new Image[0])); + return new BaseMultiResolutionImage(image.toArray(Image[]::new)); } catch (Exception e) { throw new RuntimeException(e); } @@ -90,7 +85,7 @@ public final class ResourceManager { public static final double PRIMARY_SCALE_FACTOR = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration().getDefaultTransform().getScaleX(); - private static BufferedImage scale(double scale, BufferedImage image) { + public static BufferedImage scale(double scale, BufferedImage image) { int w = (int) (scale * image.getWidth()); int h = (int) (scale * image.getHeight()); return Scalr.resize(image, Method.ULTRA_QUALITY, Mode.FIT_TO_WIDTH, w, h, Scalr.OP_ANTIALIAS); diff --git a/source/net/filebot/ThumbnailServices.java b/source/net/filebot/ThumbnailServices.java index 515f71f2..60ccd41f 100644 --- a/source/net/filebot/ThumbnailServices.java +++ b/source/net/filebot/ThumbnailServices.java @@ -3,8 +3,13 @@ package net.filebot; import static java.nio.charset.StandardCharsets.*; import static java.util.stream.Collectors.*; import static net.filebot.Logging.*; +import static net.filebot.ResourceManager.*; import static net.filebot.util.RegularExpressions.*; +import java.awt.Image; +import java.awt.image.BaseMultiResolutionImage; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; import java.net.URI; import java.net.URL; import java.net.http.HttpClient; @@ -12,6 +17,7 @@ import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandlers; import java.nio.ByteBuffer; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -19,6 +25,7 @@ import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.logging.Level; +import javax.imageio.ImageIO; import javax.swing.Icon; import javax.swing.ImageIcon; @@ -35,12 +42,17 @@ public enum ThumbnailServices implements ThumbnailProvider { return "https://api.filebot.net/images/" + name().toLowerCase() + "/thumb/poster/" + file; } - protected Cache getCache() { - return Cache.getCache("thumbnail_" + ordinal(), CacheType.Persistent); + protected String getThumbnailResource(int id, ResolutionVariant variant) { + return variant == ResolutionVariant.NORMAL ? id + ".png" : id + "@2x.png"; + } + + protected Cache getCache(ResolutionVariant variant) { + return Cache.getCache("thumbnail_" + ordinal() + "_" + variant.ordinal(), CacheType.Persistent); } protected Set getIndex() throws Exception { - byte[] bytes = getCache().bytes("index.txt.xz", n -> new URL(getResource(n)), XZInputStream::new).expire(Cache.ONE_MONTH).get(); + Cache cache = getCache(ResolutionVariant.NORMAL); + byte[] bytes = cache.bytes("index.txt.xz", n -> new URL(getResource(n)), XZInputStream::new).expire(Cache.ONE_MONTH).get(); // all data files are UTF-8 encoded XZ compressed text files return NEWLINE.splitAsStream(UTF_8.decode(ByteBuffer.wrap(bytes))).filter(s -> s.length() > 0).map(Integer::parseInt).collect(toSet()); @@ -48,8 +60,11 @@ public enum ThumbnailServices implements ThumbnailProvider { private final Resource> index = Resource.lazy(this::getIndex); - public byte[][] getThumbnails(int[] ids) throws Exception { - Cache cache = getCache(); + // shared HTTP Client instance for all thumbnail requests + private static final Resource http = Resource.lazy(HttpClient::newHttpClient); + + public byte[][] getThumbnails(int[] ids, ResolutionVariant variant) throws Exception { + Cache cache = getCache(variant); byte[][] response = new byte[ids.length][]; synchronized (index) { @@ -63,10 +78,10 @@ public enum ThumbnailServices implements ThumbnailProvider { for (int i = 0; i < response.length; i++) { if (response[i] == null && index.get().contains(ids[i])) { - URI r = URI.create(getResource(ids[i] + ".png")); - request[i] = http.get().sendAsync(HttpRequest.newBuilder(r).build(), BodyHandlers.ofByteArray()); + String resource = getThumbnailResource(ids[i], variant); + request[i] = http.get().sendAsync(HttpRequest.newBuilder(URI.create(resource)).build(), BodyHandlers.ofByteArray()); - debug.fine(format("Request %s", r)); + debug.fine(format("Request %s", resource)); } } @@ -91,14 +106,16 @@ public enum ThumbnailServices implements ThumbnailProvider { @Override public Map getThumbnails(List keys) throws Exception { + ResolutionVariant variant = PRIMARY_SCALE_FACTOR > 1 ? ResolutionVariant.RETINA : ResolutionVariant.NORMAL; + int[] ids = keys.stream().mapToInt(SearchResult::getId).toArray(); - byte[][] thumbnails = getThumbnails(ids); + byte[][] thumbnails = getThumbnails(ids, variant); Map icons = new HashMap<>(thumbnails.length); for (int i = 0; i < thumbnails.length; i++) { if (thumbnails[i] != null && thumbnails[i].length > 0) { try { - icons.put(keys.get(i), new ImageIcon(thumbnails[i])); + icons.put(keys.get(i), getScaledIcon(thumbnails[i], variant)); } catch (Exception e) { debug.log(Level.SEVERE, e, e::toString); } @@ -108,7 +125,40 @@ public enum ThumbnailServices implements ThumbnailProvider { return icons; } - // shared HTTP Client instance for all thumbnail requests - private static final Resource http = Resource.lazy(HttpClient::newHttpClient); + protected Icon getScaledIcon(byte[] bytes, ResolutionVariant variant) throws Exception { + // Load multi-resolution images only if necessary + if (PRIMARY_SCALE_FACTOR == 1 && variant == ResolutionVariant.NORMAL) { + return new ImageIcon(bytes); + } + + BufferedImage baseImage = ImageIO.read(new ByteArrayInputStream(bytes)); + double baseScale = variant.scaleFactor; + + List image = new ArrayList(3); + image.add(baseImage); + + // use down-scaled @2x image as @1x base image + if (baseScale > 1) { + image.add(0, scale(1 / baseScale, baseImage)); + } + + // Windows 10: use down-scaled @2x image for non-integer scale factors 1.25 / 1.5 / 1.75 + if (PRIMARY_SCALE_FACTOR > 1 && PRIMARY_SCALE_FACTOR < 2 && image.size() >= 2) { + image.add(1, scale(PRIMARY_SCALE_FACTOR / 2, image.get(1))); + } + + return new ImageIcon(new BaseMultiResolutionImage(image.toArray(Image[]::new))); + } + + public enum ResolutionVariant { + + NORMAL(1), RETINA(2); + + public final int scaleFactor; + + private ResolutionVariant(int scaleFactor) { + this.scaleFactor = scaleFactor; + } + } }