Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions HMCL/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ dependencies {
implementation(project(":HMCLBoot"))
implementation("libs:JFoenix")
implementation(libs.twelvemonkeys.imageio.webp)
implementation(libs.fxsvgimage)
implementation(libs.java.info)
implementation(libs.monet.fx)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ public Image getVersionIconImage(String id) {
Optional<Path> iconFile = getVersionIconFile(id);
if (iconFile.isPresent()) {
try {
return FXUtils.loadImage(iconFile.get());
return FXUtils.loadImage(iconFile.get(), 64, 64, true, true);
} catch (Exception e) {
LOG.warning("Failed to load version icon of " + id, e);
}
Expand Down
4 changes: 2 additions & 2 deletions HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -235,8 +235,8 @@ private FXUtils() {

public static final String DEFAULT_MONOSPACE_FONT = OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS ? "Consolas" : "Monospace";

public static final List<String> IMAGE_EXTENSIONS = Lang.immutableListOf(
"png", "jpg", "jpeg", "bmp", "gif", "webp", "apng"
public static final List<String> IMAGE_EXTENSIONS = List.of(
"png", "jpg", "jpeg", "bmp", "gif", "webp", "svg", "apng"
);

private static final Map<String, Image> builtinImageCache = new ConcurrentHashMap<>();
Expand Down
62 changes: 62 additions & 0 deletions HMCL/src/main/java/org/jackhuang/hmcl/ui/image/ImageUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,17 @@

import com.twelvemonkeys.imageio.plugins.webp.WebPImageReaderSpi;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.scene.SnapshotParameters;
import javafx.scene.image.Image;
import javafx.scene.image.PixelFormat;
import javafx.scene.image.WritableImage;
import javafx.scene.paint.Color;
import org.girod.javafx.svgimage.LoaderParameters;
import org.girod.javafx.svgimage.SVGImage;
import org.girod.javafx.svgimage.SVGLoader;
import org.girod.javafx.svgimage.ScaleQuality;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.image.apng.Png;
import org.jackhuang.hmcl.ui.image.apng.argb8888.Argb8888Bitmap;
import org.jackhuang.hmcl.ui.image.apng.argb8888.Argb8888BitmapSequence;
Expand All @@ -39,7 +47,9 @@
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.regex.Pattern;

import static org.jackhuang.hmcl.util.logging.Logger.LOG;
Expand Down Expand Up @@ -73,6 +83,46 @@ public final class ImageUtils {
return SwingFXUtils.toFXImage(bufferedImage, requestedWidth, requestedHeight, preserveRatio, smooth);
};

public static final ImageLoader SVG = (input, requestedWidth, requestedHeight, preserveRatio, smooth) -> {
String content = new String(input.readAllBytes(), StandardCharsets.UTF_8);

LoaderParameters parameters = new LoaderParameters();
parameters.autoStartAnimations = false;

SVGImage image;

if (Platform.isFxApplicationThread()) {
image = SVGLoader.load(content, parameters);
} else {
// TODO: Currently, SVGLoader.load(...) requires the javafx.swing module if it operates on a non-JavaFX thread.
image = CompletableFuture.supplyAsync(
() -> SVGLoader.load(content, parameters),
Schedulers.javafx()
).get();
}

if (image == null)
throw new IOException("Failed to load SVG image");

var snapshotParameters = new SnapshotParameters();
snapshotParameters.setFill(Color.TRANSPARENT);

if (requestedWidth <= 0. || requestedHeight <= 0.) {
return image.toImage(snapshotParameters);
}

double scaleX = requestedWidth / image.getScaledWidth();
double scaleY = requestedHeight / image.getScaledHeight();

if (preserveRatio || scaleX == scaleY) {
double scale = Math.min(scaleX, scaleY);
return image.scale(scale).toImage(snapshotParameters);
} else {
// FIXME: Use DEFAULT_SVG_SNAPSHOT_PARAMS
return image.toImageScaled(ScaleQuality.RENDER_QUALITY, scaleX, scaleY);
}
};

public static final ImageLoader APNG = (input, requestedWidth, requestedHeight, preserveRatio, smooth) -> {
if (!"true".equals(System.getProperty("hmcl.experimental.apng", "true")))
return DEFAULT.load(input, requestedWidth, requestedHeight, preserveRatio, smooth);
Expand Down Expand Up @@ -136,11 +186,13 @@ public final class ImageUtils {

public static final Map<String, ImageLoader> EXT_TO_LOADER = Map.of(
"webp", WEBP,
"svg", SVG,
"apng", APNG
);

public static final Map<String, ImageLoader> CONTENT_TYPE_TO_LOADER = Map.of(
"image/webp", WEBP,
"image/svg+xml", SVG,
"image/apng", APNG
);

Expand All @@ -165,6 +217,14 @@ public static boolean isWebP(byte[] headerBuffer) {
&& Arrays.equals(headerBuffer, 8, 12, WEBP_HEADER, 0, 4);
}

private static final byte[] SVG_HEADER = "<svg".getBytes(StandardCharsets.US_ASCII);

// This is currently a simple check, more complex checks can be considered in the future
public static boolean isSVG(byte[] headerBuffer) {
return headerBuffer.length > SVG_HEADER.length
&& Arrays.equals(headerBuffer, 0, SVG_HEADER.length, SVG_HEADER, 0, SVG_HEADER.length);
}

private static final byte[] PNG_HEADER = {
(byte) 0x89, (byte) 0x50, (byte) 0x4e, (byte) 0x47,
(byte) 0x0d, (byte) 0x0a, (byte) 0x1a, (byte) 0x0a,
Expand Down Expand Up @@ -232,6 +292,8 @@ public static boolean isApng(byte[] headerBuffer) {
return WEBP;
if (isApng(headerBuffer))
return APNG;
if (isSVG(headerBuffer))
return SVG;
return null;
}

Expand Down
5 changes: 5 additions & 0 deletions HMCL/src/main/resources/assets/about/deps.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@
"subtitle" : "Copyright (C) 2015 Andrew Ellerton.\nLicensed under the Apache 2.0 License.",
"externalLink" : "https://github.com/aellerton/japng"
},
{
"title" : "fxsvgimage",
"subtitle" : "Copyright (c) 2021, 2022, 2025 Hervé Girod.\nLicensed under the BSD 3-clause License.",
"externalLink" : "https://github.com/hervegirod/fxsvgimage"
},
{
"title": "Terracotta",
"subtitle": "Copyright (C) 2025 Burning_TNT.\nAll rights reserved.",
Expand Down
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ nanohttpd = "2.3.1"
jsoup = "1.21.2"
chardet = "2.5.0"
twelvemonkeys = "3.13.0"
fxsvgimage = "1.3"
jna = "5.18.1"
pci-ids = "0.4.0"
java-info = "1.0"
Expand Down Expand Up @@ -43,6 +44,7 @@ nanohttpd = { module = "org.nanohttpd:nanohttpd", version.ref = "nanohttpd" }
jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" }
chardet = { module = "org.glavo:chardet", version.ref = "chardet" }
twelvemonkeys-imageio-webp = { module = "com.twelvemonkeys.imageio:imageio-webp", version.ref = "twelvemonkeys" }
fxsvgimage = { module = "com.github.hervegirod:fxsvgimage", version.ref = "fxsvgimage" }
jna = { module = "net.java.dev.jna:jna", version.ref = "jna" }
jna-platform = { module = "net.java.dev.jna:jna-platform", version.ref = "jna" }
pci-ids = { module = "org.glavo:pci-ids", version.ref = "pci-ids" }
Expand Down