Update NormalizedSkin (#2036)

* Update NormalizedSkin

* Add tests

* update

* update
This commit is contained in:
Glavo 2023-02-02 21:26:27 +08:00 committed by GitHub
parent 3d9c9689e3
commit c86302df58
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 100 additions and 33 deletions

View File

@ -25,6 +25,7 @@ import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableBooleanValue; import javafx.beans.value.ObservableBooleanValue;
import javafx.scene.control.RadioButton; import javafx.scene.control.RadioButton;
import javafx.scene.control.Skin; import javafx.scene.control.Skin;
import javafx.scene.image.Image;
import javafx.stage.FileChooser; import javafx.stage.FileChooser;
import org.jackhuang.hmcl.auth.Account; import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.AuthenticationException; 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.jackhuang.hmcl.util.skin.NormalizedSkin;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File; import java.io.File;
import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
@ -167,14 +167,14 @@ public class AccountListItem extends RadioButton {
return refreshAsync() return refreshAsync()
.thenRunAsync(() -> { .thenRunAsync(() -> {
BufferedImage skinImg; Image skinImg;
try { try (FileInputStream input = new FileInputStream(selectedFile)) {
skinImg = ImageIO.read(selectedFile); skinImg = new Image(input);
} catch (IOException e) { } catch (IOException e) {
throw new InvalidSkinException("Failed to read skin image", e); throw new InvalidSkinException("Failed to read skin image", e);
} }
if (skinImg == null) { if (skinImg.isError()) {
throw new InvalidSkinException("Failed to read skin image"); throw new InvalidSkinException("Failed to read skin image", skinImg.getException());
} }
NormalizedSkin skin = new NormalizedSkin(skinImg); NormalizedSkin skin = new NormalizedSkin(skinImg);
String model = skin.isSlim() ? "slim" : ""; String model = skin.isSlim() ? "slim" : "";

View File

@ -26,7 +26,9 @@ public final class JavaFXLauncher {
private JavaFXLauncher() { private JavaFXLauncher() {
} }
public static void start() { private static boolean started = false;
static {
// init JavaFX Toolkit // init JavaFX Toolkit
try { try {
// Java 9 or Latter // Java 9 or Latter
@ -35,12 +37,26 @@ public final class JavaFXLauncher {
javafx.application.Platform.class, "startup", MethodType.methodType(void.class, Runnable.class)); javafx.application.Platform.class, "startup", MethodType.methodType(void.class, Runnable.class));
startup.invokeExact((Runnable) () -> { startup.invokeExact((Runnable) () -> {
}); });
} catch (Throwable e) { started = true;
} catch (NoSuchMethodException e) {
// Java 8 // Java 8
try { try {
Class.forName("javafx.embed.swing.JFXPanel").getDeclaredConstructor().newInstance(); 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;
} }
} }

View File

@ -17,7 +17,10 @@
*/ */
package org.jackhuang.hmcl.util.skin; 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). * Describes a Minecraft 1.8+ skin (64x64).
@ -27,26 +30,29 @@ import java.awt.image.BufferedImage;
*/ */
public class NormalizedSkin { 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 y = 0; y < h; y++) {
for (int x = 0; x < w; x++) { for (int x = 0; x < w; x++) {
int pixel = src.getRGB(sx + x, sy + y); int pixel = reader.getArgb(sx + x, sy + y);
dst.setRGB(dx + (flipHorizontal ? w - x - 1 : x), dy + y, pixel); writer.setArgb(dx + (flipHorizontal ? w - x - 1 : x), dy + y, pixel);
} }
} }
} }
private final BufferedImage texture; private final Image texture;
private final BufferedImage normalizedTexture; private final WritableImage normalizedTexture;
private final int scale; private final int scale;
private final boolean oldFormat; private final boolean oldFormat;
public NormalizedSkin(BufferedImage texture) throws InvalidSkinException { public NormalizedSkin(Image texture) throws InvalidSkinException {
this.texture = texture; this.texture = texture;
// check format // check format
int w = texture.getWidth(); int w = (int) texture.getWidth();
int h = texture.getHeight(); int h = (int) texture.getHeight();
if (w % 64 != 0) { if (w % 64 != 0) {
throw new InvalidSkinException("Invalid size " + w + "x" + h); throw new InvalidSkinException("Invalid size " + w + "x" + h);
} }
@ -61,7 +67,7 @@ public class NormalizedSkin {
// compute scale // compute scale
scale = w / 64; 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); copyImage(texture, normalizedTexture, 0, 0, 0, 0, w, h, false);
if (oldFormat) { if (oldFormat) {
convertOldSkin(); convertOldSkin();
@ -87,11 +93,11 @@ public class NormalizedSkin {
copyImage(normalizedTexture, normalizedTexture, sx * scale, sy * scale, dx * scale, dy * scale, w * scale, h * scale, flipHorizontal); copyImage(normalizedTexture, normalizedTexture, sx * scale, sy * scale, dx * scale, dy * scale, w * scale, h * scale, flipHorizontal);
} }
public BufferedImage getOriginalTexture() { public Image getOriginalTexture() {
return texture; return texture;
} }
public BufferedImage getNormalizedTexture() { public Image getNormalizedTexture() {
return normalizedTexture; return normalizedTexture;
} }
@ -103,10 +109,6 @@ public class NormalizedSkin {
return oldFormat; return oldFormat;
} }
/**
* Tests whether the skin is slim.
* Note that this method doesn't guarantee the result is correct.
*/
public boolean isSlim() { public boolean isSlim() {
return (hasTransparencyRelative(50, 16, 2, 4) || return (hasTransparencyRelative(50, 16, 2, 4) ||
hasTransparencyRelative(54, 20, 2, 12) || hasTransparencyRelative(54, 20, 2, 12) ||
@ -119,13 +121,14 @@ public class NormalizedSkin {
} }
private boolean hasTransparencyRelative(int x0, int y0, int w, int h) { private boolean hasTransparencyRelative(int x0, int y0, int w, int h) {
PixelReader reader = normalizedTexture.getPixelReader();
x0 *= scale; x0 *= scale;
y0 *= scale; y0 *= scale;
w *= scale; w *= scale;
h *= scale; h *= scale;
for (int y = 0; y < h; y++) { for (int y = 0; y < h; y++) {
for (int x = 0; x < w; x++) { 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) { if (pixel >>> 24 != 0xff) {
return true; return true;
} }
@ -135,13 +138,14 @@ public class NormalizedSkin {
} }
private boolean isAreaBlackRelative(int x0, int y0, int w, int h) { private boolean isAreaBlackRelative(int x0, int y0, int w, int h) {
PixelReader reader = normalizedTexture.getPixelReader();
x0 *= scale; x0 *= scale;
y0 *= scale; y0 *= scale;
w *= scale; w *= scale;
h *= scale; h *= scale;
for (int y = 0; y < h; y++) { for (int y = 0; y < h; y++) {
for (int x = 0; x < w; x++) { 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) { if (pixel != 0xff000000) {
return false; return false;
} }

View File

@ -26,7 +26,9 @@ public final class JavaFXLauncher {
private JavaFXLauncher() { private JavaFXLauncher() {
} }
public static void start() { private static boolean started = false;
static {
// init JavaFX Toolkit // init JavaFX Toolkit
try { try {
// Java 9 or Latter // Java 9 or Latter
@ -35,12 +37,26 @@ public final class JavaFXLauncher {
javafx.application.Platform.class, "startup", MethodType.methodType(void.class, Runnable.class)); javafx.application.Platform.class, "startup", MethodType.methodType(void.class, Runnable.class));
startup.invokeExact((Runnable) () -> { startup.invokeExact((Runnable) () -> {
}); });
} catch (Throwable e) { started = true;
} catch (NoSuchMethodException e) {
// Java 8 // Java 8
try { try {
Class.forName("javafx.embed.swing.JFXPanel").getDeclaredConstructor().newInstance(); 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;
} }
} }

View File

@ -17,13 +17,13 @@
*/ */
package org.jackhuang.hmcl.util; package org.jackhuang.hmcl.util;
import org.jackhuang.hmcl.JavaFXLauncher;
import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor; import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.util.platform.JavaVersion; import org.jackhuang.hmcl.util.platform.JavaVersion;
import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIf;
import java.util.concurrent.*; import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
@ -75,8 +75,9 @@ public class TaskTest {
assertTrue(bool.get(), "withRunAsync should be executed"); assertTrue(bool.get(), "withRunAsync should be executed");
} }
@Test
@EnabledIf("org.jackhuang.hmcl.JavaFXLauncher#isStarted")
public void testThenAccept() { public void testThenAccept() {
JavaFXLauncher.start();
AtomicBoolean flag = new AtomicBoolean(); AtomicBoolean flag = new AtomicBoolean();
boolean result = Task.supplyAsync(JavaVersion::fromCurrentEnvironment) boolean result = Task.supplyAsync(JavaVersion::fromCurrentEnvironment)
.thenAcceptAsync(Schedulers.javafx(), javaVersion -> { .thenAcceptAsync(Schedulers.javafx(), javaVersion -> {

View File

@ -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());
}
}