diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java index 2d12de7d4..3b0407633 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java @@ -29,6 +29,7 @@ import javafx.beans.binding.Bindings; import javafx.beans.property.BooleanProperty; import javafx.beans.property.Property; import javafx.beans.property.ReadOnlyObjectProperty; +import javafx.beans.property.SimpleObjectProperty; import javafx.beans.value.*; import javafx.collections.ObservableMap; import javafx.event.Event; @@ -60,6 +61,8 @@ import javafx.util.StringConverter; import org.glavo.png.PNGType; import org.glavo.png.PNGWriter; import org.glavo.png.javafx.PNGJavaFXUtils; +import org.jackhuang.hmcl.task.CacheFileTask; +import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.ui.animation.AnimationUtils; import org.jackhuang.hmcl.util.*; @@ -87,8 +90,12 @@ import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; import java.lang.ref.WeakReference; import java.net.*; +import java.nio.ByteBuffer; +import java.nio.channels.Channels; +import java.nio.channels.FileChannel; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.StandardOpenOption; import java.util.List; import java.util.*; import java.util.concurrent.ConcurrentHashMap; @@ -978,6 +985,55 @@ public final class FXUtils { return image; } + public static Task getRemoteImageTask(String url, double requestedWidth, double requestedHeight, boolean preserveRatio, boolean smooth) { + return new CacheFileTask(URI.create(url)) + .thenApplyAsync(file -> { + try (var channel = FileChannel.open(file, StandardOpenOption.READ)) { + var header = new byte[12]; + var buffer = ByteBuffer.wrap(header); + + //noinspection StatementWithEmptyBody + while (channel.read(buffer) > 0) { + } + + channel.position(0L); + if (!buffer.hasRemaining()) { + // WebP File + if (header[0] == 'R' && header[1] == 'I' && header[2] == 'F' && header[3] == 'F' && + header[8] == 'W' && header[9] == 'E' && header[10] == 'B' && header[11] == 'P') { + + WebPImageReaderSpi spi = new WebPImageReaderSpi(); + ImageReader reader = spi.createReaderInstance(null); + + try (ImageInputStream imageInput = ImageIO.createImageInputStream(Channels.newInputStream(channel))) { + reader.setInput(imageInput, true, true); + return SwingFXUtils.toFXImage(reader.read(0, reader.getDefaultReadParam()), + requestedWidth, requestedHeight, preserveRatio, smooth); + } finally { + reader.dispose(); + } + } + } + + return new Image(Channels.newInputStream(channel), requestedWidth, requestedHeight, preserveRatio, smooth); + } + }); + } + + public static ObservableValue newRemoteImage(String url, double requestedWidth, double requestedHeight, boolean preserveRatio, boolean smooth) { + var image = new SimpleObjectProperty(); + getRemoteImageTask(url, requestedWidth, requestedHeight, preserveRatio, smooth) + .whenComplete(Schedulers.javafx(), (result, exception) -> { + if (exception == null) { + image.set(result); + } else { + LOG.warning("An exception encountered while loading remote image: " + url, exception); + } + }) + .start(); + return image; + } + public static JFXButton newRaisedButton(String text) { JFXButton button = new JFXButton(text); button.getStyleClass().add("jfx-button-raised"); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java index 8f8556113..35244fe20 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java @@ -22,6 +22,7 @@ import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.text.Text; import javafx.scene.text.TextFlow; +import org.jackhuang.hmcl.task.Task; import org.jsoup.nodes.Node; import org.jsoup.nodes.TextNode; @@ -190,11 +191,11 @@ public final class HTMLRenderer { } } - Image image = FXUtils.newRemoteImage(uri.toString(), width, height, true, true, false); - if (image.isError()) { - LOG.warning("Failed to load image: " + uri, image.getException()); - } else { - ImageView imageView = new ImageView(image); + Task task = FXUtils.getRemoteImageTask(uri.toString(), width, height, true, true); + task.start(); + + try { + ImageView imageView = new ImageView(task.getResult()); if (hyperlink != null) { URI target = resolveLink(hyperlink); if (target != null) { @@ -204,6 +205,8 @@ public final class HTMLRenderer { } children.add(imageView); return; + } catch (Throwable e) { + LOG.warning("Failed to load image: " + uri, e); } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadListPage.java index ee99f4f85..49a897f85 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadListPage.java @@ -35,7 +35,6 @@ import javafx.scene.control.Control; import javafx.scene.control.Label; import javafx.scene.control.Skin; import javafx.scene.control.SkinBase; -import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; @@ -45,7 +44,6 @@ import org.jackhuang.hmcl.mod.RemoteMod; import org.jackhuang.hmcl.mod.RemoteModRepository; import org.jackhuang.hmcl.mod.modrinth.ModrinthRemoteModRepository; import org.jackhuang.hmcl.setting.Profile; -import org.jackhuang.hmcl.task.GetTask; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.ui.Controllers; @@ -63,8 +61,6 @@ import org.jackhuang.hmcl.util.i18n.I18n; import org.jackhuang.hmcl.util.javafx.BindingMapping; import org.jackhuang.hmcl.util.versioning.GameVersionNumber; -import java.net.URI; -import java.net.URISyntaxException; import java.util.*; import java.util.stream.Collectors; @@ -546,7 +542,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP if (StringUtils.isNotBlank(dataItem.getIconUrl())) { LOG.debug("Icon: " + dataItem.getIconUrl()); - imageView.setImage(FXUtils.newRemoteImage(dataItem.getIconUrl(), 40, 40, true, true, true)); + imageView.imageProperty().bind(FXUtils.newRemoteImage(dataItem.getIconUrl(), 40, 40, true, true)); } } }); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java index 6de0971de..2a8e5d1ab 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java @@ -220,7 +220,7 @@ public class DownloadPage extends Control implements DecoratorPage { { ImageView imageView = new ImageView(); if (StringUtils.isNotBlank(getSkinnable().addon.getIconUrl())) { - imageView.setImage(FXUtils.newRemoteImage(getSkinnable().addon.getIconUrl(), 40, 40, true, true, true)); + imageView.imageProperty().bind(FXUtils.newRemoteImage(getSkinnable().addon.getIconUrl(), 40, 40, true, true)); } descriptionPane.getChildren().add(FXUtils.limitingSize(imageView, 40, 40)); @@ -359,7 +359,7 @@ public class DownloadPage extends Control implements DecoratorPage { .collect(Collectors.toList())); if (StringUtils.isNotBlank(addon.getIconUrl())) { - imageView.setImage(FXUtils.newRemoteImage(addon.getIconUrl(), 40, 40, true, true, true)); + imageView.imageProperty().bind(FXUtils.newRemoteImage(addon.getIconUrl(), 40, 40, true, true)); } } else { content.setTitle(i18n("mods.broken_dependency.title")); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/util/SwingFXUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/util/SwingFXUtils.java index 1163c0e8c..4f9d28c15 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/util/SwingFXUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/util/SwingFXUtils.java @@ -120,5 +120,40 @@ public final class SwingFXUtils { pw.setPixels(0, 0, bw, bh, pf, data, offset, scan); return wimg; } + + public static WritableImage toFXImage(BufferedImage bimg, double requestedWidth, double requestedHeight, boolean preserveRatio, boolean smooth) { + int width = (int) requestedWidth; + int height = (int) requestedHeight; + + assert width > 0 && height > 0; + + // Calculate actual dimensions if preserveRatio is true + if (preserveRatio) { + double originalWidth = bimg.getWidth(); + double originalHeight = bimg.getHeight(); + double scaleX = requestedWidth / originalWidth; + double scaleY = requestedHeight / originalHeight; + double scale = Math.min(scaleX, scaleY); + + width = (int) (originalWidth * scale); + height = (int) (originalHeight * scale); + } + + // Create scaled BufferedImage + BufferedImage scaledImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB_PRE); + Graphics2D g2d = scaledImage.createGraphics(); + + if (smooth) { + g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); + g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); + g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + } + + g2d.drawImage(bimg, 0, 0, width, height, null); + g2d.dispose(); + + // Convert to JavaFX Image using the existing method + return toFXImage(scaledImage, null); + } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/CacheFileTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/CacheFileTask.java index 389c4bbac..6f67c7c7a 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/CacheFileTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/CacheFileTask.java @@ -22,7 +22,6 @@ import org.jetbrains.annotations.NotNull; import java.io.IOException; import java.io.OutputStream; import java.net.URI; -import java.net.URISyntaxException; import java.net.URLConnection; import java.nio.file.Files; import java.nio.file.Path; @@ -94,10 +93,7 @@ public final class CacheFileTask extends FetchTask { } try { - repository.cacheRemoteFile(connection, temp); - setResult(repository.getCachedRemoteFile(connection.getURL().toURI())); - } catch (URISyntaxException e) { - throw new IOException(e); + setResult(repository.cacheRemoteFile(connection, temp)); } finally { try { Files.deleteIfExists(temp); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/CacheRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/CacheRepository.java index 8049f8d81..0e7fae9c7 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/CacheRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/CacheRepository.java @@ -200,20 +200,20 @@ public class CacheRepository { // conn.setRequestProperty("If-Modified-Since", eTagItem.getRemoteLastModified()); } - public void cacheRemoteFile(URLConnection connection, Path downloaded) throws IOException { - cacheData(connection, () -> { + public Path cacheRemoteFile(URLConnection connection, Path downloaded) throws IOException { + return cacheData(connection, () -> { String hash = DigestUtils.digestToString(SHA1, downloaded); Path cached = cacheFile(downloaded, SHA1, hash); return new CacheResult(hash, cached); }); } - public void cacheText(URLConnection connection, String text) throws IOException { - cacheBytes(connection, text.getBytes(UTF_8)); + public Path cacheText(URLConnection connection, String text) throws IOException { + return cacheBytes(connection, text.getBytes(UTF_8)); } - public void cacheBytes(URLConnection connection, byte[] bytes) throws IOException { - cacheData(connection, () -> { + public Path cacheBytes(URLConnection connection, byte[] bytes) throws IOException { + return cacheData(connection, () -> { String hash = DigestUtils.digestToString(SHA1, bytes); Path cached = getFile(SHA1, hash); FileUtils.writeBytes(cached, bytes); @@ -221,9 +221,9 @@ public class CacheRepository { }); } - private void cacheData(URLConnection connection, ExceptionalSupplier cacheSupplier) throws IOException { + private Path cacheData(URLConnection connection, ExceptionalSupplier cacheSupplier) throws IOException { String eTag = connection.getHeaderField("ETag"); - if (StringUtils.isBlank(eTag)) return; + if (StringUtils.isBlank(eTag)) return null; URI uri; try { uri = NetworkUtils.dropQuery(connection.getURL().toURI()); @@ -240,6 +240,7 @@ public class CacheRepository { } finally { lock.writeLock().unlock(); } + return cacheResult.cachedFile; } private static final class CacheResult {