Cache skin image data

This commit is contained in:
Braydon 2024-04-11 08:56:42 -04:00
parent 63121afe32
commit 655ee50a21
4 changed files with 59 additions and 19 deletions

@ -24,11 +24,14 @@
package me.braydon.mc.common; package me.braydon.mc.common;
import lombok.NonNull; import lombok.NonNull;
import lombok.SneakyThrows;
import lombok.experimental.UtilityClass; import lombok.experimental.UtilityClass;
import javax.imageio.ImageIO;
import java.awt.*; import java.awt.*;
import java.awt.geom.AffineTransform; import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
/** /**
* @author Braydon * @author Braydon
@ -65,4 +68,19 @@ public final class ImageUtils {
graphics.dispose(); graphics.dispose();
return flipped; return flipped;
} }
/**
* Get the byte array from the given image.
*
* @param image the image to extract from
* @return the byte array of the image
*/
@SneakyThrows
public static byte[] toByteArray(@NonNull BufferedImage image) {
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
ImageIO.write(image, "png", outputStream);
outputStream.flush();
return outputStream.toByteArray();
}
}
} }

@ -32,7 +32,7 @@ import me.braydon.mc.model.skin.Skin;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
import java.awt.*; import java.awt.*;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.net.URL; import java.io.ByteArrayInputStream;
/** /**
* A renderer for a {@link ISkinPart}. * A renderer for a {@link ISkinPart}.
@ -63,17 +63,17 @@ public abstract class SkinRenderer<T extends ISkinPart> {
*/ */
@SneakyThrows @SneakyThrows
protected final BufferedImage getVanillaSkinPart(@NonNull Skin skin, @NonNull ISkinPart.Vanilla part, double size) { protected final BufferedImage getVanillaSkinPart(@NonNull Skin skin, @NonNull ISkinPart.Vanilla part, double size) {
BufferedImage skinImage = ImageIO.read(new URL(skin.getUrl())); // The skin texture
ISkinPart.Vanilla.Coordinates coordinates = part.getCoordinates(); // The coordinates of the part ISkinPart.Vanilla.Coordinates coordinates = part.getCoordinates(); // The coordinates of the part
// The skin texture is legacy, use legacy coordinates // The skin texture is legacy, use legacy coordinates
if (skinImage.getHeight() == 32 && part.hasLegacyCoordinates()) { if (skin.isLegacy() && part.hasLegacyCoordinates()) {
coordinates = part.getLegacyCoordinates(); coordinates = part.getLegacyCoordinates();
} }
int width = part.getWidth(); // The width of the part int width = part.getWidth(); // The width of the part
if (skin.getModel() == Skin.Model.SLIM && part.isFrontArm()) { if (skin.getModel() == Skin.Model.SLIM && part.isFrontArm()) {
width--; width--;
} }
BufferedImage skinImage = ImageIO.read(new ByteArrayInputStream(skin.getSkinImage())); // The skin texture
BufferedImage partTexture = getSkinPartTexture(skinImage, coordinates.getX(), coordinates.getY(), width, part.getHeight(), size); BufferedImage partTexture = getSkinPartTexture(skinImage, coordinates.getX(), coordinates.getY(), width, part.getHeight(), size);
if (coordinates instanceof ISkinPart.Vanilla.LegacyCoordinates legacyCoordinates && legacyCoordinates.isFlipped()) { if (coordinates instanceof ISkinPart.Vanilla.LegacyCoordinates legacyCoordinates && legacyCoordinates.isFlipped()) {
partTexture = ImageUtils.flip(partTexture); partTexture = ImageUtils.flip(partTexture);

@ -23,12 +23,17 @@
*/ */
package me.braydon.mc.model.skin; package me.braydon.mc.model.skin;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import lombok.*; import lombok.*;
import me.braydon.mc.common.ImageUtils;
import me.braydon.mc.config.AppConfig; import me.braydon.mc.config.AppConfig;
import me.braydon.mc.model.Player; import me.braydon.mc.model.Player;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.net.URL;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
@ -37,9 +42,9 @@ import java.util.Map;
* *
* @author Braydon * @author Braydon
*/ */
@RequiredArgsConstructor(access = AccessLevel.PRIVATE) @Setter @Getter @ToString @AllArgsConstructor(access = AccessLevel.PRIVATE) @Getter @ToString
public final class Skin { public final class Skin {
public static final Skin DEFAULT_STEVE = new Skin("http://textures.minecraft.net/texture/60a5bd016b3c9a1b9272e4929e30827a67be4ebb219017adbbc4a4d22ebd5b1", Model.DEFAULT); public static final Skin DEFAULT_STEVE = create("http://textures.minecraft.net/texture/60a5bd016b3c9a1b9272e4929e30827a67be4ebb219017adbbc4a4d22ebd5b1", Model.DEFAULT);
/** /**
* The texture URL of this skin. * The texture URL of this skin.
@ -51,10 +56,20 @@ public final class Skin {
*/ */
@NonNull private final Model model; @NonNull private final Model model;
/**
* The image data of this skin.
*/
@JsonIgnore private final byte[] skinImage;
/**
* Is this skin legacy?
*/
private final boolean legacy;
/** /**
* URLs to the parts of this skin. * URLs to the parts of this skin.
*/ */
@NonNull @JsonProperty("parts") private Map<String, String> partUrls = new HashMap<>(); @NonNull @JsonProperty("parts") private final Map<String, String> partUrls;
/** /**
* Populate the part URLs for this skin. * Populate the part URLs for this skin.
@ -88,7 +103,22 @@ public final class Skin {
if (metadataJsonObject != null) { // Parse the skin model if (metadataJsonObject != null) { // Parse the skin model
model = Model.valueOf(metadataJsonObject.get("model").getAsString().toUpperCase()); model = Model.valueOf(metadataJsonObject.get("model").getAsString().toUpperCase());
} }
return new Skin(jsonObject.get("url").getAsString(), model); return create(jsonObject.get("url").getAsString(), model);
}
/**
* Create a skin from the given URL and model.
*
* @param url the skin url
* @param model the skin model
* @return the constructed skin
*/
@NonNull @SneakyThrows
private static Skin create(@NonNull String url, @NonNull Model model) {
BufferedImage image = ImageIO.read(new URL(url)); // Get the skin image
byte[] bytes = ImageUtils.toByteArray(image); // Convert the image into bytes
boolean legacy = image.getWidth() == 64 && image.getHeight() == 32; // Is the skin legacy?
return new Skin(url, model, bytes, legacy, new HashMap<>());
} }
/** /**

@ -61,9 +61,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.InputStream; import java.io.InputStream;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.net.URL; import java.net.URL;
@ -231,17 +229,11 @@ public final class MojangService {
BufferedImage texture = part.render(skin, overlays, size); // Render the skin part BufferedImage texture = part.render(skin, overlays, size); // Render the skin part
log.info("Render of skin part took {}ms: {}", System.currentTimeMillis() - before, id); log.info("Render of skin part took {}ms: {}", System.currentTimeMillis() - before, id);
// Convert BufferedImage to byte array byte[] bytes = ImageUtils.toByteArray(texture); // Convert the image into a byte array
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
ImageIO.write(texture, "png", outputStream);
outputStream.flush();
byte[] bytes = outputStream.toByteArray();
skinPartTextureCache.save(new CachedSkinPartTexture(id, bytes)); // Cache the texture skinPartTextureCache.save(new CachedSkinPartTexture(id, bytes)); // Cache the texture
log.info("Cached skin part texture: {}", id); log.info("Cached skin part texture: {}", id);
return bytes; return bytes;
} }
}
/** /**
* Get a player by their username or UUID. * Get a player by their username or UUID.