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 997bbe624..16d1ad049 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java @@ -60,6 +60,7 @@ import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.ui.animation.AnimationUtils; import org.jackhuang.hmcl.util.*; +import org.jackhuang.hmcl.util.io.DataUri; import org.jackhuang.hmcl.util.io.FileUtils; import org.jackhuang.hmcl.util.io.NetworkUtils; import org.jackhuang.hmcl.util.javafx.ExtendedProperties; @@ -838,6 +839,18 @@ public final class FXUtils { public static Image loadImage(String url) throws Exception { URI uri = NetworkUtils.toURI(url); + if (DataUri.isDataUri(uri)) { + DataUri dataUri = new DataUri(uri); + if ("image/webp".equalsIgnoreCase(dataUri.getMediaType())) { + return loadWebPImage(new ByteArrayInputStream(dataUri.readBytes())); + } else { + Image image = new Image(new ByteArrayInputStream(dataUri.readBytes())); + if (image.isError()) + throw image.getException(); + return image; + } + } + URLConnection connection = NetworkUtils.createConnection(uri); if (connection instanceof HttpURLConnection) { connection = NetworkUtils.resolveConnection((HttpURLConnection) connection); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/DataUri.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/DataUri.java new file mode 100644 index 000000000..5ace39847 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/DataUri.java @@ -0,0 +1,112 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2025 huangyuhui and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.jackhuang.hmcl.util.io; + +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.net.URI; +import java.nio.charset.Charset; +import java.util.Base64; + +public final class DataUri { + public static final String SCHEME = "data"; + + private static IllegalArgumentException invalidUri(URI uri) { + return new IllegalArgumentException("Invalid data URI: " + uri); + } + + public static boolean isDataUri(URI uri) { + return uri != null && SCHEME.equals(uri.getScheme()); + } + + private final @NotNull String mediaType; + private final @NotNull Charset charset; + private final boolean base64; + private final @NotNull String rawData; + + public DataUri(URI uri) { + if (!uri.getScheme().equals(SCHEME)) { + throw new IllegalArgumentException("URI scheme must be " + SCHEME); + } + + String schemeSpecificPart = uri.getSchemeSpecificPart(); + if (schemeSpecificPart == null) + throw invalidUri(uri); + + int comma = schemeSpecificPart.indexOf(','); + if (comma < 0) + throw invalidUri(uri); + + String mediaType = schemeSpecificPart.substring(0, comma); + boolean base64 = mediaType.endsWith(";base64"); + if (base64) + mediaType = mediaType.substring(0, mediaType.length() - ";base64".length()); + + + this.mediaType = mediaType.trim(); + this.charset = NetworkUtils.getCharsetFromContentType(mediaType); + this.base64 = base64; + this.rawData = schemeSpecificPart.substring(comma + 1); + } + + public @NotNull String getMediaType() { + return mediaType; + } + + public @NotNull Charset getCharset() { + return charset; + } + + public boolean isBase64() { + return base64; + } + + public @NotNull String getRawData() { + return rawData; + } + + public byte[] readBytes() throws IOException { + if (base64) { + try { + return Base64.getDecoder().decode(rawData); + } catch (IllegalArgumentException e) { + throw new IOException(e); + } + } else { + return rawData.getBytes(charset); + } + } + + public String readString() throws IOException { + if (base64) { + try { + return new String(Base64.getDecoder().decode(rawData), charset); + } catch (IllegalArgumentException e) { + throw new IOException(e); + } + } else { + return rawData; + } + } + + @Override + public String toString() { + return String.format("DataUri{mediaType='%s', charset=%s, base64=%s, body='%s'}", mediaType, charset, base64, rawData); + } +} diff --git a/HMCLCore/src/test/java/org/jackhuang/hmcl/util/io/DataUriTest.java b/HMCLCore/src/test/java/org/jackhuang/hmcl/util/io/DataUriTest.java new file mode 100644 index 000000000..7b43ccbdd --- /dev/null +++ b/HMCLCore/src/test/java/org/jackhuang/hmcl/util/io/DataUriTest.java @@ -0,0 +1,44 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2025 huangyuhui and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.jackhuang.hmcl.util.io; + +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.net.URI; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public final class DataUriTest { + + private static String readString(String uri) throws IOException { + return new DataUri(URI.create(uri)).readString(); + } + + private static byte[] readBytes(String uri) throws IOException { + return new DataUri(URI.create(uri)).readBytes(); + } + + @Test + public void testReadString() throws IOException { + assertEquals("Hello, World!", readString("data:,Hello%2C%20World%21")); + assertEquals("Hello, World!", readString("data:text/plain;base64,SGVsbG8sIFdvcmxkIQ==")); + assertEquals("

Hello, World!

", readString("data:text/html,%3Ch1%3EHello%2C%20World%21%3C%2Fh1%3E")); + assertEquals("", readString("data:text/html,%3Cscript%3Ealert%28%27hi%27%29%3B%3C%2Fscript%3E")); + } +}