From c86302df5809d31b977a2e68695e29822a54d36c Mon Sep 17 00:00:00 2001 From: Glavo Date: Thu, 2 Feb 2023 21:26:27 +0800 Subject: [PATCH] Update NormalizedSkin (#2036) * Update NormalizedSkin * Add tests * update * update --- .../hmcl/ui/account/AccountListItem.java | 14 +++---- .../org/jackhuang/hmcl/JavaFXLauncher.java | 22 ++++++++-- .../hmcl/util/skin/NormalizedSkin.java | 40 ++++++++++--------- .../org/jackhuang/hmcl/JavaFXLauncher.java | 22 ++++++++-- .../org/jackhuang/hmcl/util/TaskTest.java | 5 ++- .../hmcl/util/skin/NormalizedSkinTest.java | 30 ++++++++++++++ 6 files changed, 100 insertions(+), 33 deletions(-) create mode 100644 HMCLCore/src/test/java/org/jackhuang/hmcl/util/skin/NormalizedSkinTest.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListItem.java index ee5eed0ff..f8b2c5293 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListItem.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListItem.java @@ -25,6 +25,7 @@ import javafx.beans.property.StringProperty; import javafx.beans.value.ObservableBooleanValue; import javafx.scene.control.RadioButton; import javafx.scene.control.Skin; +import javafx.scene.image.Image; import javafx.stage.FileChooser; import org.jackhuang.hmcl.auth.Account; import org.jackhuang.hmcl.auth.AuthenticationException; @@ -47,9 +48,8 @@ import org.jackhuang.hmcl.util.skin.InvalidSkinException; import org.jackhuang.hmcl.util.skin.NormalizedSkin; import org.jetbrains.annotations.Nullable; -import javax.imageio.ImageIO; -import java.awt.image.BufferedImage; import java.io.File; +import java.io.FileInputStream; import java.io.IOException; import java.util.Optional; import java.util.Set; @@ -167,14 +167,14 @@ public class AccountListItem extends RadioButton { return refreshAsync() .thenRunAsync(() -> { - BufferedImage skinImg; - try { - skinImg = ImageIO.read(selectedFile); + Image skinImg; + try (FileInputStream input = new FileInputStream(selectedFile)) { + skinImg = new Image(input); } catch (IOException e) { throw new InvalidSkinException("Failed to read skin image", e); } - if (skinImg == null) { - throw new InvalidSkinException("Failed to read skin image"); + if (skinImg.isError()) { + throw new InvalidSkinException("Failed to read skin image", skinImg.getException()); } NormalizedSkin skin = new NormalizedSkin(skinImg); String model = skin.isSlim() ? "slim" : ""; diff --git a/HMCL/src/test/java/org/jackhuang/hmcl/JavaFXLauncher.java b/HMCL/src/test/java/org/jackhuang/hmcl/JavaFXLauncher.java index 577401046..a4cbcb924 100644 --- a/HMCL/src/test/java/org/jackhuang/hmcl/JavaFXLauncher.java +++ b/HMCL/src/test/java/org/jackhuang/hmcl/JavaFXLauncher.java @@ -26,7 +26,9 @@ public final class JavaFXLauncher { private JavaFXLauncher() { } - public static void start() { + private static boolean started = false; + + static { // init JavaFX Toolkit try { // Java 9 or Latter @@ -35,12 +37,26 @@ public final class JavaFXLauncher { javafx.application.Platform.class, "startup", MethodType.methodType(void.class, Runnable.class)); startup.invokeExact((Runnable) () -> { }); - } catch (Throwable e) { + started = true; + } catch (NoSuchMethodException e) { // Java 8 try { Class.forName("javafx.embed.swing.JFXPanel").getDeclaredConstructor().newInstance(); - } catch (Throwable ignored) { + started = true; + } catch (Throwable e0) { + e0.printStackTrace(); } + } catch (IllegalStateException e) { + started = true; + } catch (Throwable e) { + e.printStackTrace(); } } + + public static void start() { + } + + public static boolean isStarted() { + return started; + } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/skin/NormalizedSkin.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/skin/NormalizedSkin.java index 92f82dc06..250d9e804 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/skin/NormalizedSkin.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/skin/NormalizedSkin.java @@ -17,7 +17,10 @@ */ package org.jackhuang.hmcl.util.skin; -import java.awt.image.BufferedImage; +import javafx.scene.image.Image; +import javafx.scene.image.PixelReader; +import javafx.scene.image.PixelWriter; +import javafx.scene.image.WritableImage; /** * Describes a Minecraft 1.8+ skin (64x64). @@ -27,26 +30,29 @@ import java.awt.image.BufferedImage; */ public class NormalizedSkin { - private static void copyImage(BufferedImage src, BufferedImage dst, int sx, int sy, int dx, int dy, int w, int h, boolean flipHorizontal) { + private static void copyImage(Image src, WritableImage dst, int sx, int sy, int dx, int dy, int w, int h, boolean flipHorizontal) { + PixelReader reader = src.getPixelReader(); + PixelWriter writer = dst.getPixelWriter(); + for (int y = 0; y < h; y++) { for (int x = 0; x < w; x++) { - int pixel = src.getRGB(sx + x, sy + y); - dst.setRGB(dx + (flipHorizontal ? w - x - 1 : x), dy + y, pixel); + int pixel = reader.getArgb(sx + x, sy + y); + writer.setArgb(dx + (flipHorizontal ? w - x - 1 : x), dy + y, pixel); } } } - private final BufferedImage texture; - private final BufferedImage normalizedTexture; + private final Image texture; + private final WritableImage normalizedTexture; private final int scale; private final boolean oldFormat; - public NormalizedSkin(BufferedImage texture) throws InvalidSkinException { + public NormalizedSkin(Image texture) throws InvalidSkinException { this.texture = texture; // check format - int w = texture.getWidth(); - int h = texture.getHeight(); + int w = (int) texture.getWidth(); + int h = (int) texture.getHeight(); if (w % 64 != 0) { throw new InvalidSkinException("Invalid size " + w + "x" + h); } @@ -61,7 +67,7 @@ public class NormalizedSkin { // compute scale scale = w / 64; - normalizedTexture = new BufferedImage(w, w, BufferedImage.TYPE_INT_ARGB); + normalizedTexture = new WritableImage(w, w); copyImage(texture, normalizedTexture, 0, 0, 0, 0, w, h, false); if (oldFormat) { convertOldSkin(); @@ -87,11 +93,11 @@ public class NormalizedSkin { copyImage(normalizedTexture, normalizedTexture, sx * scale, sy * scale, dx * scale, dy * scale, w * scale, h * scale, flipHorizontal); } - public BufferedImage getOriginalTexture() { + public Image getOriginalTexture() { return texture; } - public BufferedImage getNormalizedTexture() { + public Image getNormalizedTexture() { return normalizedTexture; } @@ -103,10 +109,6 @@ public class NormalizedSkin { return oldFormat; } - /** - * Tests whether the skin is slim. - * Note that this method doesn't guarantee the result is correct. - */ public boolean isSlim() { return (hasTransparencyRelative(50, 16, 2, 4) || hasTransparencyRelative(54, 20, 2, 12) || @@ -119,13 +121,14 @@ public class NormalizedSkin { } private boolean hasTransparencyRelative(int x0, int y0, int w, int h) { + PixelReader reader = normalizedTexture.getPixelReader(); x0 *= scale; y0 *= scale; w *= scale; h *= scale; for (int y = 0; y < h; y++) { for (int x = 0; x < w; x++) { - int pixel = normalizedTexture.getRGB(x0 + x, y0 + y); + int pixel = reader.getArgb(x0 + x, y0 + y); if (pixel >>> 24 != 0xff) { return true; } @@ -135,13 +138,14 @@ public class NormalizedSkin { } private boolean isAreaBlackRelative(int x0, int y0, int w, int h) { + PixelReader reader = normalizedTexture.getPixelReader(); x0 *= scale; y0 *= scale; w *= scale; h *= scale; for (int y = 0; y < h; y++) { for (int x = 0; x < w; x++) { - int pixel = normalizedTexture.getRGB(x0 + x, y0 + y); + int pixel = reader.getArgb(x0 + x, y0 + y); if (pixel != 0xff000000) { return false; } diff --git a/HMCLCore/src/test/java/org/jackhuang/hmcl/JavaFXLauncher.java b/HMCLCore/src/test/java/org/jackhuang/hmcl/JavaFXLauncher.java index 577401046..a4cbcb924 100644 --- a/HMCLCore/src/test/java/org/jackhuang/hmcl/JavaFXLauncher.java +++ b/HMCLCore/src/test/java/org/jackhuang/hmcl/JavaFXLauncher.java @@ -26,7 +26,9 @@ public final class JavaFXLauncher { private JavaFXLauncher() { } - public static void start() { + private static boolean started = false; + + static { // init JavaFX Toolkit try { // Java 9 or Latter @@ -35,12 +37,26 @@ public final class JavaFXLauncher { javafx.application.Platform.class, "startup", MethodType.methodType(void.class, Runnable.class)); startup.invokeExact((Runnable) () -> { }); - } catch (Throwable e) { + started = true; + } catch (NoSuchMethodException e) { // Java 8 try { Class.forName("javafx.embed.swing.JFXPanel").getDeclaredConstructor().newInstance(); - } catch (Throwable ignored) { + started = true; + } catch (Throwable e0) { + e0.printStackTrace(); } + } catch (IllegalStateException e) { + started = true; + } catch (Throwable e) { + e.printStackTrace(); } } + + public static void start() { + } + + public static boolean isStarted() { + return started; + } } diff --git a/HMCLCore/src/test/java/org/jackhuang/hmcl/util/TaskTest.java b/HMCLCore/src/test/java/org/jackhuang/hmcl/util/TaskTest.java index 0494d0764..1babd41a1 100644 --- a/HMCLCore/src/test/java/org/jackhuang/hmcl/util/TaskTest.java +++ b/HMCLCore/src/test/java/org/jackhuang/hmcl/util/TaskTest.java @@ -17,13 +17,13 @@ */ package org.jackhuang.hmcl.util; -import org.jackhuang.hmcl.JavaFXLauncher; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.task.TaskExecutor; import org.jackhuang.hmcl.util.platform.JavaVersion; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIf; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicBoolean; @@ -75,8 +75,9 @@ public class TaskTest { assertTrue(bool.get(), "withRunAsync should be executed"); } + @Test + @EnabledIf("org.jackhuang.hmcl.JavaFXLauncher#isStarted") public void testThenAccept() { - JavaFXLauncher.start(); AtomicBoolean flag = new AtomicBoolean(); boolean result = Task.supplyAsync(JavaVersion::fromCurrentEnvironment) .thenAcceptAsync(Schedulers.javafx(), javaVersion -> { diff --git a/HMCLCore/src/test/java/org/jackhuang/hmcl/util/skin/NormalizedSkinTest.java b/HMCLCore/src/test/java/org/jackhuang/hmcl/util/skin/NormalizedSkinTest.java new file mode 100644 index 000000000..f1153b810 --- /dev/null +++ b/HMCLCore/src/test/java/org/jackhuang/hmcl/util/skin/NormalizedSkinTest.java @@ -0,0 +1,30 @@ +package org.jackhuang.hmcl.util.skin; + +import javafx.scene.image.Image; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIf; + +import java.nio.file.Paths; + +import static org.junit.jupiter.api.Assertions.*; + +public class NormalizedSkinTest { + private static NormalizedSkin getSkin(String name) throws InvalidSkinException { + String path = Paths.get("../HMCL/src/main/resources/assets/img/skin/" + name + ".png").normalize().toAbsolutePath().toUri().toString(); + return new NormalizedSkin(new Image(path)); + } + + @Test + @EnabledIf("org.jackhuang.hmcl.JavaFXLauncher#isStarted") + public void testIsSlim() throws Exception { + assertFalse(getSkin("steve").isSlim()); + assertTrue(getSkin("alex").isSlim()); + assertTrue(getSkin("noor").isSlim()); + assertFalse(getSkin("sunny").isSlim()); + assertFalse(getSkin("ari").isSlim()); + assertFalse(getSkin("zuri").isSlim()); + assertTrue(getSkin("makena").isSlim()); + assertFalse(getSkin("kai").isSlim()); + assertTrue(getSkin("efe").isSlim()); + } +}