Skip to content

Commit a28509d

Browse files
authored
支持加载 SVG 图片 (#5484)
1 parent e45601e commit a28509d

6 files changed

Lines changed: 73 additions & 3 deletions

File tree

HMCL/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ dependencies {
5757
implementation(project(":HMCLBoot"))
5858
implementation("libs:JFoenix")
5959
implementation(libs.twelvemonkeys.imageio.webp)
60+
implementation(libs.fxsvgimage)
6061
implementation(libs.java.info)
6162
implementation(libs.monet.fx)
6263
implementation(libs.nayuki.qrcodegen)

HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,7 @@ public Image getVersionIconImage(String id) {
310310
Optional<Path> iconFile = getVersionIconFile(id);
311311
if (iconFile.isPresent()) {
312312
try {
313-
return FXUtils.loadImage(iconFile.get());
313+
return FXUtils.loadImage(iconFile.get(), 64, 64, true, true);
314314
} catch (Exception e) {
315315
LOG.warning("Failed to load version icon of " + id, e);
316316
}

HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -235,8 +235,8 @@ private FXUtils() {
235235

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

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

242242
private static final Map<String, Image> builtinImageCache = new ConcurrentHashMap<>();

HMCL/src/main/java/org/jackhuang/hmcl/ui/image/ImageUtils.java

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,17 @@
1919

2020
import com.twelvemonkeys.imageio.plugins.webp.WebPImageReaderSpi;
2121
import javafx.animation.Timeline;
22+
import javafx.application.Platform;
23+
import javafx.scene.SnapshotParameters;
2224
import javafx.scene.image.Image;
2325
import javafx.scene.image.PixelFormat;
2426
import javafx.scene.image.WritableImage;
27+
import javafx.scene.paint.Color;
28+
import org.girod.javafx.svgimage.LoaderParameters;
29+
import org.girod.javafx.svgimage.SVGImage;
30+
import org.girod.javafx.svgimage.SVGLoader;
31+
import org.girod.javafx.svgimage.ScaleQuality;
32+
import org.jackhuang.hmcl.task.Schedulers;
2533
import org.jackhuang.hmcl.ui.image.apng.Png;
2634
import org.jackhuang.hmcl.ui.image.apng.argb8888.Argb8888Bitmap;
2735
import org.jackhuang.hmcl.ui.image.apng.argb8888.Argb8888BitmapSequence;
@@ -39,7 +47,9 @@
3947
import java.awt.image.BufferedImage;
4048
import java.io.IOException;
4149
import java.nio.ByteBuffer;
50+
import java.nio.charset.StandardCharsets;
4251
import java.util.*;
52+
import java.util.concurrent.CompletableFuture;
4353
import java.util.regex.Pattern;
4454

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

86+
public static final ImageLoader SVG = (input, requestedWidth, requestedHeight, preserveRatio, smooth) -> {
87+
String content = new String(input.readAllBytes(), StandardCharsets.UTF_8);
88+
89+
LoaderParameters parameters = new LoaderParameters();
90+
parameters.autoStartAnimations = false;
91+
92+
SVGImage image;
93+
94+
if (Platform.isFxApplicationThread()) {
95+
image = SVGLoader.load(content, parameters);
96+
} else {
97+
// TODO: Currently, SVGLoader.load(...) requires the javafx.swing module if it operates on a non-JavaFX thread.
98+
image = CompletableFuture.supplyAsync(
99+
() -> SVGLoader.load(content, parameters),
100+
Schedulers.javafx()
101+
).get();
102+
}
103+
104+
if (image == null)
105+
throw new IOException("Failed to load SVG image");
106+
107+
var snapshotParameters = new SnapshotParameters();
108+
snapshotParameters.setFill(Color.TRANSPARENT);
109+
110+
if (requestedWidth <= 0. || requestedHeight <= 0.) {
111+
return image.toImage(snapshotParameters);
112+
}
113+
114+
double scaleX = requestedWidth / image.getScaledWidth();
115+
double scaleY = requestedHeight / image.getScaledHeight();
116+
117+
if (preserveRatio || scaleX == scaleY) {
118+
double scale = Math.min(scaleX, scaleY);
119+
return image.scale(scale).toImage(snapshotParameters);
120+
} else {
121+
// FIXME: Use DEFAULT_SVG_SNAPSHOT_PARAMS
122+
return image.toImageScaled(ScaleQuality.RENDER_QUALITY, scaleX, scaleY);
123+
}
124+
};
125+
76126
public static final ImageLoader APNG = (input, requestedWidth, requestedHeight, preserveRatio, smooth) -> {
77127
if (!"true".equals(System.getProperty("hmcl.experimental.apng", "true")))
78128
return DEFAULT.load(input, requestedWidth, requestedHeight, preserveRatio, smooth);
@@ -136,11 +186,13 @@ public final class ImageUtils {
136186

137187
public static final Map<String, ImageLoader> EXT_TO_LOADER = Map.of(
138188
"webp", WEBP,
189+
"svg", SVG,
139190
"apng", APNG
140191
);
141192

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

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

220+
private static final byte[] SVG_HEADER = "<svg".getBytes(StandardCharsets.US_ASCII);
221+
222+
// This is currently a simple check, more complex checks can be considered in the future
223+
public static boolean isSVG(byte[] headerBuffer) {
224+
return headerBuffer.length > SVG_HEADER.length
225+
&& Arrays.equals(headerBuffer, 0, SVG_HEADER.length, SVG_HEADER, 0, SVG_HEADER.length);
226+
}
227+
168228
private static final byte[] PNG_HEADER = {
169229
(byte) 0x89, (byte) 0x50, (byte) 0x4e, (byte) 0x47,
170230
(byte) 0x0d, (byte) 0x0a, (byte) 0x1a, (byte) 0x0a,
@@ -232,6 +292,8 @@ public static boolean isApng(byte[] headerBuffer) {
232292
return WEBP;
233293
if (isApng(headerBuffer))
234294
return APNG;
295+
if (isSVG(headerBuffer))
296+
return SVG;
235297
return null;
236298
}
237299

HMCL/src/main/resources/assets/about/deps.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@
6969
"subtitle" : "Copyright © 2015 Andrew Ellerton.\nLicensed under the Apache 2.0 License.",
7070
"externalLink" : "https://github.com/aellerton/japng"
7171
},
72+
{
73+
"title" : "fxsvgimage",
74+
"subtitle" : "Copyright (c) 2021, 2022, 2025 Hervé Girod.\nLicensed under the BSD 3-clause License.",
75+
"externalLink" : "https://github.com/hervegirod/fxsvgimage"
76+
},
7277
{
7378
"title": "Terracotta",
7479
"subtitle": "Copyright © 2025 Burning_TNT.\nLicensed under the AGPL 3.0 License.",

gradle/libs.versions.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ nanohttpd = "2.3.1"
1414
jsoup = "1.21.2"
1515
chardet = "2.5.0"
1616
twelvemonkeys = "3.13.1"
17+
fxsvgimage = "1.3"
1718
jna = "5.18.1"
1819
pci-ids = "0.4.0"
1920
java-info = "1.0"
@@ -47,6 +48,7 @@ nanohttpd = { module = "org.nanohttpd:nanohttpd", version.ref = "nanohttpd" }
4748
jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" }
4849
chardet = { module = "org.glavo:chardet", version.ref = "chardet" }
4950
twelvemonkeys-imageio-webp = { module = "com.twelvemonkeys.imageio:imageio-webp", version.ref = "twelvemonkeys" }
51+
fxsvgimage = { module = "com.github.hervegirod:fxsvgimage", version.ref = "fxsvgimage" }
5052
jna = { module = "net.java.dev.jna:jna", version.ref = "jna" }
5153
jna-platform = { module = "net.java.dev.jna:jna-platform", version.ref = "jna" }
5254
pci-ids = { module = "org.glavo:pci-ids", version.ref = "pci-ids" }

0 commit comments

Comments
 (0)