1919
2020import com .twelvemonkeys .imageio .plugins .webp .WebPImageReaderSpi ;
2121import javafx .animation .Timeline ;
22+ import javafx .application .Platform ;
23+ import javafx .scene .SnapshotParameters ;
2224import javafx .scene .image .Image ;
2325import javafx .scene .image .PixelFormat ;
2426import 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 ;
2533import org .jackhuang .hmcl .ui .image .apng .Png ;
2634import org .jackhuang .hmcl .ui .image .apng .argb8888 .Argb8888Bitmap ;
2735import org .jackhuang .hmcl .ui .image .apng .argb8888 .Argb8888BitmapSequence ;
3947import java .awt .image .BufferedImage ;
4048import java .io .IOException ;
4149import java .nio .ByteBuffer ;
50+ import java .nio .charset .StandardCharsets ;
4251import java .util .*;
52+ import java .util .concurrent .CompletableFuture ;
4353import java .util .regex .Pattern ;
4454
4555import 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
0 commit comments