From 36da64f796d55938656745d1f4df873606979826 Mon Sep 17 00:00:00 2001 From: Glavo Date: Thu, 28 Nov 2024 00:35:08 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=8A=A0=E8=BD=BD=20WebP=20?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E7=9A=84=E8=83=8C=E6=99=AF=E5=9B=BE=E7=89=87?= =?UTF-8?q?=20(#3457)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 支持加载 WebP 格式的背景图片 * update * update * update * update --- HMCL/build.gradle.kts | 4 + .../hmcl/game/HMCLGameRepository.java | 42 ++---- .../java/org/jackhuang/hmcl/ui/FXUtils.java | 67 +++++++++- .../hmcl/ui/construct/MultiFileItem.java | 14 +- .../ui/decorator/DecoratorController.java | 83 ++++-------- .../hmcl/ui/export/ExportWizardProvider.java | 17 +-- .../hmcl/ui/main/PersonalizationPage.java | 4 +- .../hmcl/ui/versions/VersionIconDialog.java | 2 +- .../hmcl/ui/versions/VersionSettingsPage.java | 2 +- .../org/jackhuang/hmcl/util/SwingFXUtils.java | 124 ++++++++++++++++++ .../resources/assets/lang/I18N.properties | 3 +- .../resources/assets/lang/I18N_es.properties | 3 +- .../resources/assets/lang/I18N_ja.properties | 3 +- .../resources/assets/lang/I18N_ru.properties | 3 +- .../resources/assets/lang/I18N_zh.properties | 3 +- .../assets/lang/I18N_zh_CN.properties | 3 +- 16 files changed, 264 insertions(+), 113 deletions(-) create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/util/SwingFXUtils.java diff --git a/HMCL/build.gradle.kts b/HMCL/build.gradle.kts index ee46d63e8..86bda3f8f 100644 --- a/HMCL/build.gradle.kts +++ b/HMCL/build.gradle.kts @@ -37,6 +37,7 @@ version = "$versionRoot.$buildNumber" dependencies { implementation(project(":HMCLCore")) implementation("libs:JFoenix") + implementation("com.twelvemonkeys.imageio:imageio-webp:3.12.0") } fun digest(algorithm: String, bytes: ByteArray): ByteArray = MessageDigest.getInstance(algorithm).digest(bytes) @@ -110,6 +111,9 @@ tasks.getByName("sha exclude("**/package-info.class") exclude("META-INF/maven/**") + exclude("META-INF/services/javax.imageio.spi.ImageReaderSpi") + exclude("META-INF/services/javax.imageio.spi.ImageInputStreamSpi") + minimize { exclude(dependency("com.google.code.gson:.*:.*")) exclude(dependency("libs:JFoenix:.*")) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java index db71cb536..1d34d9fd3 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java @@ -33,6 +33,7 @@ import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.setting.ProxyManager; import org.jackhuang.hmcl.setting.VersionIconType; import org.jackhuang.hmcl.setting.VersionSetting; +import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.gson.JsonUtils; @@ -43,9 +44,7 @@ import org.jackhuang.hmcl.util.versioning.VersionNumber; import org.jetbrains.annotations.Nullable; import java.io.File; -import java.io.FileInputStream; import java.io.IOException; -import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.time.Instant; @@ -261,24 +260,11 @@ public class HMCLGameRepository extends DefaultGameRepository { public Optional getVersionIconFile(String id) { File root = getVersionRoot(id); - File iconFile = new File(root, "icon.png"); - if (iconFile.exists()) { - return Optional.of(iconFile); - } - - iconFile = new File(root, "icon.jpg"); - if (iconFile.exists()) { - return Optional.of(iconFile); - } - - iconFile = new File(root, "icon.bmp"); - if (iconFile.exists()) { - return Optional.of(iconFile); - } - - iconFile = new File(root, "icon.gif"); - if (iconFile.exists()) { - return Optional.of(iconFile); + for (String extension : FXUtils.IMAGE_EXTENSIONS) { + File file = new File(root, "icon." + extension); + if (file.exists()) { + return Optional.of(file); + } } return Optional.empty(); @@ -286,7 +272,7 @@ public class HMCLGameRepository extends DefaultGameRepository { public void setVersionIconFile(String id, File iconFile) throws IOException { String ext = FileUtils.getExtension(iconFile).toLowerCase(Locale.ROOT); - if (!ext.equals("png") && !ext.equals("jpg") && !ext.equals("bmp") && !ext.equals("gif")) { + if (!FXUtils.IMAGE_EXTENSIONS.contains(ext)) { throw new IllegalArgumentException("Unsupported icon file: " + ext); } @@ -297,11 +283,9 @@ public class HMCLGameRepository extends DefaultGameRepository { public void deleteIconFile(String id) { File root = getVersionRoot(id); - - new File(root, "icon.png").delete(); - new File(root, "icon.jpg").delete(); - new File(root, "icon.bmp").delete(); - new File(root, "icon.gif").delete(); + for (String extension : FXUtils.IMAGE_EXTENSIONS) { + new File(root, "icon." + extension).delete(); + } } public Image getVersionIconImage(String id) { @@ -315,9 +299,9 @@ public class HMCLGameRepository extends DefaultGameRepository { Version version = getVersion(id).resolve(this); Optional iconFile = getVersionIconFile(id); if (iconFile.isPresent()) { - try (InputStream inputStream = new FileInputStream(iconFile.get())) { - return new Image(inputStream); - } catch (IOException e) { + try { + return FXUtils.loadImage(iconFile.get().toPath()); + } catch (Exception e) { LOG.warning("Failed to load version icon of " + id, e); } } 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 5d0d68473..d42a5913b 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java @@ -18,6 +18,7 @@ package org.jackhuang.hmcl.ui; import com.jfoenix.controls.*; +import com.twelvemonkeys.imageio.plugins.webp.WebPImageReaderSpi; import javafx.animation.*; import javafx.application.Platform; import javafx.beans.InvalidationListener; @@ -44,6 +45,7 @@ import javafx.scene.paint.Color; import javafx.scene.shape.Rectangle; import javafx.scene.text.Text; import javafx.scene.text.TextFlow; +import javafx.stage.FileChooser; import javafx.stage.Stage; import javafx.util.Callback; import javafx.util.Duration; @@ -53,11 +55,9 @@ import org.glavo.png.PNGWriter; import org.glavo.png.javafx.PNGJavaFXUtils; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.ui.animation.AnimationUtils; -import org.jackhuang.hmcl.util.Holder; -import org.jackhuang.hmcl.util.Lang; -import org.jackhuang.hmcl.util.ResourceNotFoundError; -import org.jackhuang.hmcl.util.StringUtils; +import org.jackhuang.hmcl.util.*; import org.jackhuang.hmcl.util.io.FileUtils; +import org.jackhuang.hmcl.util.io.NetworkUtils; import org.jackhuang.hmcl.util.javafx.ExtendedProperties; import org.jackhuang.hmcl.util.javafx.SafeStringConverter; import org.jackhuang.hmcl.util.platform.OperatingSystem; @@ -68,13 +68,15 @@ import org.w3c.dom.NodeList; import org.xml.sax.InputSource; import org.xml.sax.SAXException; +import javax.imageio.ImageIO; +import javax.imageio.ImageReader; +import javax.imageio.stream.ImageInputStream; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import java.io.*; import java.lang.ref.WeakReference; -import java.net.URI; -import java.net.URISyntaxException; +import java.net.*; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -113,6 +115,10 @@ public final class FXUtils { public static final String DEFAULT_MONOSPACE_FONT = OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS ? "Consolas" : "Monospace"; + public static final List IMAGE_EXTENSIONS = Lang.immutableListOf( + "png", "jpg", "jpeg", "bmp", "gif", "webp" + ); + private static final Map builtinImageCache = new ConcurrentHashMap<>(); private static final Map remoteImageCache = new ConcurrentHashMap<>(); @@ -713,6 +719,50 @@ public final class FXUtils { stage.getIcons().add(newBuiltinImage(icon)); } + private static Image loadWebPImage(InputStream input) throws IOException { + WebPImageReaderSpi spi = new WebPImageReaderSpi(); + ImageReader reader = spi.createReaderInstance(null); + + try (ImageInputStream imageInput = ImageIO.createImageInputStream(input)) { + reader.setInput(imageInput, true, true); + return SwingFXUtils.toFXImage(reader.read(0, reader.getDefaultReadParam()), null); + } finally { + reader.dispose(); + } + } + + public static Image loadImage(Path path) throws Exception { + try (InputStream input = Files.newInputStream(path)) { + if ("webp".equalsIgnoreCase(FileUtils.getExtension(path))) + return loadWebPImage(input); + else { + Image image = new Image(input); + if (image.isError()) + throw image.getException(); + return image; + } + } + } + + public static Image loadImage(URL url) throws Exception { + URLConnection connection = NetworkUtils.createConnection(url); + if (connection instanceof HttpURLConnection) { + connection = NetworkUtils.resolveConnection((HttpURLConnection) connection); + } + + try (InputStream input = connection.getInputStream()) { + String path = url.getPath(); + if (path != null && "webp".equalsIgnoreCase(StringUtils.substringAfterLast(path, '.'))) + return loadWebPImage(input); + else { + Image image = new Image(input); + if (image.isError()) + throw image.getException(); + return image; + } + } + } + /** * Suppress IllegalArgumentException since the url is supposed to be correct definitely. * @@ -1034,4 +1084,9 @@ public final class FXUtils { return String.format("#%02x%02x%02x", r, g, b); } + + public static FileChooser.ExtensionFilter getImageExtensionFilter() { + return new FileChooser.ExtensionFilter(i18n("extension.png"), + IMAGE_EXTENSIONS.stream().map(ext -> "*." + ext).toArray(String[]::new)); + } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/MultiFileItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/MultiFileItem.java index d7e224baa..70b8c2247 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/MultiFileItem.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/MultiFileItem.java @@ -21,7 +21,6 @@ import com.jfoenix.controls.JFXRadioButton; import com.jfoenix.controls.JFXTextField; import com.jfoenix.validation.base.ValidatorBase; import javafx.beans.property.*; -import javafx.collections.ObservableList; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Node; @@ -114,6 +113,7 @@ public final class MultiFileItem extends VBox { public static class Option { protected final String title; protected String subtitle; + protected String tooltip; protected final T data; protected final BooleanProperty selected = new SimpleBooleanProperty(); protected final JFXRadioButton left = new JFXRadioButton(); @@ -140,6 +140,11 @@ public final class MultiFileItem extends VBox { return this; } + public Option setTooltip(String tooltip) { + this.tooltip = tooltip; + return this; + } + public boolean isSelected() { return left.isSelected(); } @@ -161,6 +166,8 @@ public final class MultiFileItem extends VBox { BorderPane.setAlignment(left, Pos.CENTER_LEFT); left.setToggleGroup(group); left.setUserData(data); + if (StringUtils.isNotBlank(tooltip)) + FXUtils.installFastTooltip(left, tooltip); pane.setLeft(left); if (StringUtils.isNotBlank(subtitle)) { @@ -268,8 +275,9 @@ public final class MultiFileItem extends VBox { return this; } - public ObservableList getExtensionFilters() { - return selector.getExtensionFilters(); + public FileOption addExtensionFilter(FileChooser.ExtensionFilter filter) { + selector.getExtensionFilters().add(filter); + return this; } @Override diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorController.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorController.java index 7b7b55229..2d0d555a0 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorController.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorController.java @@ -49,13 +49,10 @@ import org.jackhuang.hmcl.ui.construct.Navigator; import org.jackhuang.hmcl.ui.construct.StackContainerPane; import org.jackhuang.hmcl.ui.wizard.Refreshable; import org.jackhuang.hmcl.ui.wizard.WizardProvider; -import org.jackhuang.hmcl.util.io.NetworkUtils; +import org.jetbrains.annotations.Nullable; import java.io.IOException; -import java.io.InputStream; -import java.net.HttpURLConnection; import java.net.URL; -import java.net.URLConnection; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -180,23 +177,13 @@ public class DecoratorController { case CUSTOM: String backgroundImage = config().getBackgroundImage(); if (backgroundImage != null) - image = tryLoadImage(Paths.get(backgroundImage)).orElse(null); + image = tryLoadImage(Paths.get(backgroundImage)); break; case NETWORK: String backgroundImageUrl = config().getBackgroundImageUrl(); if (backgroundImageUrl != null) { try { - URLConnection connection = NetworkUtils.createConnection(new URL(backgroundImageUrl)); - if (connection instanceof HttpURLConnection) { - connection = NetworkUtils.resolveConnection((HttpURLConnection) connection); - } - - try (InputStream input = connection.getInputStream()) { - image = new Image(input); - if (image.isError()) { - throw image.getException(); - } - } + image = FXUtils.loadImage(new URL(backgroundImageUrl)); } catch (Exception e) { LOG.warning("Couldn't load background image", e); } @@ -218,73 +205,57 @@ public class DecoratorController { * Load background image from bg/, background.png, background.jpg, background.gif */ private Image loadDefaultBackgroundImage() { - Optional image = randomImageIn(Paths.get("bg")); - if (!image.isPresent()) { - image = tryLoadImage(Paths.get("background.png")); + Image image = randomImageIn(Paths.get("bg")); + if (image != null) + return image; + + for (String extension : FXUtils.IMAGE_EXTENSIONS) { + image = tryLoadImage(Paths.get("background." + extension)); + if (image != null) + return image; } - if (!image.isPresent()) { - image = tryLoadImage(Paths.get("background.jpg")); - } - if (!image.isPresent()) { - image = tryLoadImage(Paths.get("background.gif")); - } - return image.orElseGet(() -> newBuiltinImage("/assets/img/background.jpg")); + + return newBuiltinImage("/assets/img/background.jpg"); } - private Optional randomImageIn(Path imageDir) { + private @Nullable Image randomImageIn(Path imageDir) { if (!Files.isDirectory(imageDir)) { - return Optional.empty(); + return null; } List candidates; try (Stream stream = Files.list(imageDir)) { candidates = stream + .filter(it -> FXUtils.IMAGE_EXTENSIONS.contains(getExtension(it).toLowerCase(Locale.ROOT))) .filter(Files::isReadable) - .filter(it -> { - String ext = getExtension(it).toLowerCase(Locale.ROOT); - return ext.equals("png") || ext.equals("jpg") || ext.equals("gif"); - }) .collect(toList()); } catch (IOException e) { LOG.warning("Failed to list files in ./bg", e); - return Optional.empty(); + return null; } Random rnd = new Random(); - while (candidates.size() > 0) { + while (!candidates.isEmpty()) { int selected = rnd.nextInt(candidates.size()); - Optional loaded = tryLoadImage(candidates.get(selected)); - if (loaded.isPresent()) { + Image loaded = tryLoadImage(candidates.get(selected)); + if (loaded != null) return loaded; - } else { + else candidates.remove(selected); - } } - return Optional.empty(); + return null; } - private Optional tryLoadImage(Path path) { + private @Nullable Image tryLoadImage(Path path) { if (!Files.isReadable(path)) - return Optional.empty(); + return null; - return tryLoadImage(path.toAbsolutePath().toUri().toString()); - } - - private Optional tryLoadImage(String url) { - Image img; try { - img = new Image(url); - } catch (IllegalArgumentException e) { + return FXUtils.loadImage(path); + } catch (Exception e) { LOG.warning("Couldn't load background image", e); - return Optional.empty(); + return null; } - - if (img.getException() != null) { - LOG.warning("Couldn't load background image", img.getException()); - return Optional.empty(); - } - - return Optional.of(img); } // ==== Navigation ==== diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/export/ExportWizardProvider.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/export/ExportWizardProvider.java index c4087f038..12ee29fc5 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/export/ExportWizardProvider.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/export/ExportWizardProvider.java @@ -29,6 +29,7 @@ import org.jackhuang.hmcl.setting.ConfigHolder; import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.setting.VersionSetting; import org.jackhuang.hmcl.task.Task; +import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.wizard.WizardController; import org.jackhuang.hmcl.ui.wizard.WizardProvider; import org.jackhuang.hmcl.util.Lang; @@ -129,17 +130,13 @@ public final class ExportWizardProvider implements WizardProvider { if (bg.isDirectory()) zip.putDirectory(bg.toPath(), "bg"); - File background_png = new File("background.png").getAbsoluteFile(); - if (background_png.isFile()) - zip.putFile(background_png, "background.png"); + for (String extension : FXUtils.IMAGE_EXTENSIONS) { + String fileName = "background." + extension; - File background_jpg = new File("background.jpg").getAbsoluteFile(); - if (background_jpg.isFile()) - zip.putFile(background_jpg, "background.jpg"); - - File background_gif = new File("background.gif").getAbsoluteFile(); - if (background_gif.isFile()) - zip.putFile(background_gif, "background.gif"); + File background = new File(fileName).getAbsoluteFile(); + if (background.isFile()) + zip.putFile(background, "background.png"); + } zip.putFile(launcherJar, launcherJar.getFileName().toString()); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/PersonalizationPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/PersonalizationPage.java index f1938c8ef..0f3aac4a4 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/PersonalizationPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/PersonalizationPage.java @@ -106,11 +106,13 @@ public class PersonalizationPage extends StackPane { backgroundSublist.setHasSubtitle(true); backgroundItem.loadChildren(Arrays.asList( - new MultiFileItem.Option<>(i18n("launcher.background.default"), EnumBackgroundImage.DEFAULT), + new MultiFileItem.Option<>(i18n("launcher.background.default"), EnumBackgroundImage.DEFAULT) + .setTooltip(i18n("launcher.background.default.tooltip")), new MultiFileItem.Option<>(i18n("launcher.background.classic"), EnumBackgroundImage.CLASSIC), new MultiFileItem.Option<>(i18n("launcher.background.translucent"), EnumBackgroundImage.TRANSLUCENT), new MultiFileItem.FileOption<>(i18n("settings.custom"), EnumBackgroundImage.CUSTOM) .setChooserTitle(i18n("launcher.background.choose")) + .addExtensionFilter(FXUtils.getImageExtensionFilter()) .bindBidirectional(config().backgroundImageProperty()), new MultiFileItem.StringOption<>(i18n("launcher.background.network"), EnumBackgroundImage.NETWORK) .setValidators(new URLValidator(true)) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionIconDialog.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionIconDialog.java index a19ab6ac5..44b2ada09 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionIconDialog.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionIconDialog.java @@ -72,7 +72,7 @@ public class VersionIconDialog extends DialogPane { private void exploreIcon() { FileChooser chooser = new FileChooser(); - chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("extension.png"), "*.png", "*.jpg", "*.bmp", "*.gif")); + chooser.getExtensionFilters().add(FXUtils.getImageExtensionFilter()); File selectedFile = chooser.showOpenDialog(Controllers.getStage()); if (selectedFile != null) { try { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionSettingsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionSettingsPage.java index cd025c5c6..b756ec685 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionSettingsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionSettingsPage.java @@ -185,7 +185,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag javaCustomOption = new MultiFileItem.FileOption>(i18n("settings.custom"), pair(JavaVersionType.CUSTOM, null)) .setChooserTitle(i18n("settings.game.java_directory.choose")); if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) - javaCustomOption.getExtensionFilters().add(new FileChooser.ExtensionFilter("Java", "java.exe")); + javaCustomOption.addExtensionFilter(new FileChooser.ExtensionFilter("Java", "java.exe")); javaListChangeListener = FXUtils.onWeakChangeAndOperate(JavaManager.getAllJavaProperty(), allJava -> { List>> options = new ArrayList<>(); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/util/SwingFXUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/util/SwingFXUtils.java new file mode 100644 index 000000000..1163c0e8c --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/util/SwingFXUtils.java @@ -0,0 +1,124 @@ +// Copy from javafx.swing +/* + * Copyright (c) 2012, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code 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 + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package org.jackhuang.hmcl.util; + +import javafx.scene.image.*; +import javafx.scene.image.Image; + +import java.awt.*; +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferInt; +import java.awt.image.SampleModel; +import java.awt.image.SinglePixelPackedSampleModel; +import java.nio.IntBuffer; + +/** + * This class provides utility methods for converting data types between + * Swing/AWT and JavaFX formats. + * + * @since JavaFX 2.2 + */ +public final class SwingFXUtils { + private SwingFXUtils() { + } + + /** + * Snapshots the specified {@link BufferedImage} and stores a copy of + * its pixels into a JavaFX {@link Image} object, creating a new + * object if needed. + * The returned {@code Image} will be a static snapshot of the state + * of the pixels in the {@code BufferedImage} at the time the method + * completes. Further changes to the {@code BufferedImage} will not + * be reflected in the {@code Image}. + *

+ * The optional JavaFX {@link WritableImage} parameter may be reused + * to store the copy of the pixels. + * A new {@code Image} will be created if the supplied object is null, + * is too small or of a type which the image pixels cannot be easily + * converted into. + * + * @param bimg the {@code BufferedImage} object to be converted + * @param wimg an optional {@code WritableImage} object that can be + * used to store the returned pixel data + * @return an {@code Image} object representing a snapshot of the + * current pixels in the {@code BufferedImage}. + * @since JavaFX 2.2 + */ + public static WritableImage toFXImage(BufferedImage bimg, WritableImage wimg) { + int bw = bimg.getWidth(); + int bh = bimg.getHeight(); + switch (bimg.getType()) { + case BufferedImage.TYPE_INT_ARGB: + case BufferedImage.TYPE_INT_ARGB_PRE: + break; + default: + BufferedImage converted = + new BufferedImage(bw, bh, BufferedImage.TYPE_INT_ARGB_PRE); + Graphics2D g2d = converted.createGraphics(); + g2d.drawImage(bimg, 0, 0, null); + g2d.dispose(); + bimg = converted; + break; + } + // assert(bimg.getType == TYPE_INT_ARGB[_PRE]); + if (wimg != null) { + int iw = (int) wimg.getWidth(); + int ih = (int) wimg.getHeight(); + if (iw < bw || ih < bh) { + wimg = null; + } else if (bw < iw || bh < ih) { + int[] empty = new int[iw]; + PixelWriter pw = wimg.getPixelWriter(); + PixelFormat pf = PixelFormat.getIntArgbPreInstance(); + if (bw < iw) { + pw.setPixels(bw, 0, iw - bw, bh, pf, empty, 0, 0); + } + if (bh < ih) { + pw.setPixels(0, bh, iw, ih - bh, pf, empty, 0, 0); + } + } + } + if (wimg == null) { + wimg = new WritableImage(bw, bh); + } + PixelWriter pw = wimg.getPixelWriter(); + DataBufferInt db = (DataBufferInt) bimg.getRaster().getDataBuffer(); + int[] data = db.getData(); + int offset = bimg.getRaster().getDataBuffer().getOffset(); + int scan = 0; + SampleModel sm = bimg.getRaster().getSampleModel(); + if (sm instanceof SinglePixelPackedSampleModel) { + scan = ((SinglePixelPackedSampleModel) sm).getScanlineStride(); + } + + PixelFormat pf = (bimg.isAlphaPremultiplied() ? + PixelFormat.getIntArgbPreInstance() : + PixelFormat.getIntArgbInstance()); + pw.setPixels(0, 0, bw, bh, pf, data, offset, scan); + return wimg; + } +} + diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 62a8348e3..cdcd6a1be 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -787,7 +787,8 @@ launcher.agreement.hint=You must agree to the EULA to use this software. launcher.background=Background Image launcher.background.choose=Choose background image launcher.background.classic=Classic -launcher.background.default=Default (Or "background.png/.jpg/.gif" and the images in the "bg" directory) +launcher.background.default=Default +launcher.background.default.tooltip=Or "background.png/.jpg/.gif/.webp" and the images in the "bg" directory launcher.background.network=From URL launcher.background.translucent=Translucent launcher.cache_directory=Cache Directory diff --git a/HMCL/src/main/resources/assets/lang/I18N_es.properties b/HMCL/src/main/resources/assets/lang/I18N_es.properties index 551a0c4dd..20122a125 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_es.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_es.properties @@ -682,7 +682,8 @@ launcher.agreement.hint=Debe aceptar el EULA para utilizar este software. launcher.background=Imagen de fondo launcher.background.choose=Elige una imagen de fondo launcher.background.classic=Clásico -launcher.background.default=Por defecto (o background.png/jpg/gif, o imágenes en la carpeta bg) +launcher.background.default=Por defecto +launcher.background.default.tooltip=o background.png/.jpg/.gif/.webp, o imágenes en la carpeta bg launcher.background.network=Desde la URL launcher.background.translucent=Translúcido launcher.cache_directory=Directorio de la caché diff --git a/HMCL/src/main/resources/assets/lang/I18N_ja.properties b/HMCL/src/main/resources/assets/lang/I18N_ja.properties index a630f26e8..170d0c691 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ja.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ja.properties @@ -535,7 +535,8 @@ launcher.agreement.hint=このソフトウェアを使用するには、EULAに launcher.background=背景画像 launcher.background.classic=クラシック launcher.background.choose=背景画像ファイルを選択してください -launcher.background.default=標準(ランチャーと同じディレクトリにある background.png/jpg/gif と bg フォルダから自動的に画像を取得します。) +launcher.background.default=標準 +launcher.background.default.tooltip=ランチャーと同じディレクトリにある background.png/.jpg/.gif/.webp と bg フォルダから自動的に画像を取得します。 launcher.background.network=ネットワーク launcher.background.translucent=半透明 launcher.cache_directory=キャッシュ用のディレクトリ diff --git a/HMCL/src/main/resources/assets/lang/I18N_ru.properties b/HMCL/src/main/resources/assets/lang/I18N_ru.properties index e0d58deb4..15f88033d 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ru.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ru.properties @@ -557,7 +557,8 @@ launcher.agreement.hint=Нужно принять пользовательско launcher.background=Фоновое изображение launcher.background.choose=Выберите фоновое изображение launcher.background.classic=Классическое -launcher.background.default=По умолчанию (или background.png/jpg/gif, или папку с изображениями) +launcher.background.default=По умолчанию +launcher.background.default.tooltip=или background.png/.jpg/.gif/.webp, или папку с изображениями launcher.background.network=По ссылке launcher.background.translucent=Полупрозрачный launcher.cache_directory=Папка с кэшем diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index ce564a8e6..0eef2f5f3 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -602,7 +602,8 @@ launcher.agreement.hint=同意本軟體的使用者協議與免責宣告以使 launcher.background=背景圖片 launcher.background.choose=選取背景圖片 launcher.background.classic=經典 -launcher.background.default=預設 (自動尋找啟動器同目錄下的「background.png/.jpg/.gif」及「bg」目錄內的圖片) +launcher.background.default=預設 +launcher.background.default.tooltip=自動尋找啟動器同目錄下的「background.png/.jpg/.gif/.webp」及「bg」目錄內的圖片 launcher.background.network=網路 launcher.background.translucent=半透明 launcher.cache_directory=檔案下載快取目錄 diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties index 652debe8a..b39e8a2ef 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -613,7 +613,8 @@ launcher.agreement.hint=同意本软件的用户协议与免责声明以使用 launcher.background=背景图片 launcher.background.choose=选择背景图片 launcher.background.classic=经典 -launcher.background.default=默认 (自动检索启动器同文件夹下的“background.png/.jpg/.gif”及“bg”文件夹内的图片) +launcher.background.default=默认 +launcher.background.default.tooltip=自动检索启动器同文件夹下的“background.png/.jpg/.gif/.webp”及“bg”文件夹内的图片 launcher.background.network=网络 launcher.background.translucent=半透明 launcher.cache_directory=文件下载缓存文件夹