forked from MinecraftUtilities/Backend
add isometric head renderer
This commit is contained in:
parent
557c0facb7
commit
8e5adf337a
@ -13,5 +13,8 @@ RUN mvn package -q -DskipTests
|
|||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
ENV PORT=80
|
ENV PORT=80
|
||||||
|
|
||||||
|
# Indicate that we're running in production
|
||||||
|
ENV ENVIRONMENT=production
|
||||||
|
|
||||||
# Run the jar file
|
# Run the jar file
|
||||||
CMD ["java", "-jar", "target/Minecraft-Utilities.jar"]
|
CMD ["java", "-jar", "target/Minecraft-Utilities.jar"]
|
@ -8,7 +8,6 @@ import lombok.extern.log4j.Log4j2;
|
|||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.web.cors.CorsConfiguration;
|
|
||||||
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
|
|
||||||
|
25
src/main/java/cc.fascinated/common/ImageUtils.java
Normal file
25
src/main/java/cc.fascinated/common/ImageUtils.java
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
package cc.fascinated.common;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
import java.awt.*;
|
||||||
|
import java.awt.geom.AffineTransform;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
|
||||||
|
public class ImageUtils {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resize an image.
|
||||||
|
*
|
||||||
|
* @param src the source image
|
||||||
|
* @param scale the scale factor
|
||||||
|
* @return the scaled image
|
||||||
|
*/
|
||||||
|
public static BufferedImage resize(@NotNull final BufferedImage src, final double scale) {
|
||||||
|
BufferedImage scaled = new BufferedImage((int) (src.getWidth() * scale), (int) (src.getHeight() * scale), BufferedImage.TYPE_INT_ARGB);
|
||||||
|
Graphics2D graphics = scaled.createGraphics();
|
||||||
|
graphics.drawImage(src, AffineTransform.getScaleInstance(scale, scale), null);
|
||||||
|
graphics.dispose();
|
||||||
|
return scaled;
|
||||||
|
}
|
||||||
|
}
|
@ -2,17 +2,11 @@ package cc.fascinated.common;
|
|||||||
|
|
||||||
import cc.fascinated.Main;
|
import cc.fascinated.Main;
|
||||||
import cc.fascinated.exception.impl.BadRequestException;
|
import cc.fascinated.exception.impl.BadRequestException;
|
||||||
import cc.fascinated.model.player.Skin;
|
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
import lombok.SneakyThrows;
|
import lombok.SneakyThrows;
|
||||||
import lombok.experimental.UtilityClass;
|
import lombok.experimental.UtilityClass;
|
||||||
import lombok.extern.log4j.Log4j2;
|
import lombok.extern.log4j.Log4j2;
|
||||||
|
|
||||||
import javax.imageio.ImageIO;
|
|
||||||
import java.awt.*;
|
|
||||||
import java.awt.image.BufferedImage;
|
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.http.HttpRequest;
|
import java.net.http.HttpRequest;
|
||||||
import java.net.http.HttpResponse;
|
import java.net.http.HttpResponse;
|
||||||
@ -53,38 +47,4 @@ public class PlayerUtils {
|
|||||||
HttpResponse.BodyHandlers.ofByteArray());
|
HttpResponse.BodyHandlers.ofByteArray());
|
||||||
return response.body();
|
return response.body();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the part data from the skin.
|
|
||||||
*
|
|
||||||
* @return the part data
|
|
||||||
*/
|
|
||||||
public static byte[] getSkinPartBytes(Skin skin, Skin.Parts part, int size) {
|
|
||||||
if (size <= 0) {
|
|
||||||
size = part.getDefaultSize();
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
BufferedImage image = ImageIO.read(new ByteArrayInputStream(skin.getSkinImage()));
|
|
||||||
if (image == null) {
|
|
||||||
image = ImageIO.read(new ByteArrayInputStream(Skin.DEFAULT_SKIN.getSkinImage())); // Fallback to the default skin
|
|
||||||
}
|
|
||||||
// Get the part of the image (e.g. the head)
|
|
||||||
BufferedImage partImage = image.getSubimage(part.getX(), part.getY(), part.getWidth(), part.getHeight());
|
|
||||||
|
|
||||||
// Scale the image
|
|
||||||
BufferedImage scaledImage = new BufferedImage(size, size, partImage.getType());
|
|
||||||
Graphics2D graphics2D = scaledImage.createGraphics();
|
|
||||||
graphics2D.drawImage(partImage, 0, 0, size, size, null);
|
|
||||||
graphics2D.dispose();
|
|
||||||
partImage = scaledImage;
|
|
||||||
|
|
||||||
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
|
|
||||||
ImageIO.write(partImage, "png", byteArrayOutputStream);
|
|
||||||
return byteArrayOutputStream.toByteArray();
|
|
||||||
} catch (Exception ex) {
|
|
||||||
log.error("Failed to get {} part bytes for {}", part.name(), skin.getUrl(), ex);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -2,19 +2,33 @@ package cc.fascinated.config;
|
|||||||
|
|
||||||
import jakarta.annotation.PostConstruct;
|
import jakarta.annotation.PostConstruct;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
import lombok.extern.log4j.Log4j2;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.core.env.Environment;
|
||||||
|
|
||||||
@Getter
|
@Getter @Log4j2
|
||||||
@Configuration
|
@Configuration
|
||||||
public class Config {
|
public class Config {
|
||||||
public static Config INSTANCE;
|
public static Config INSTANCE;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private Environment environment;
|
||||||
|
|
||||||
@Value("${public-url}")
|
@Value("${public-url}")
|
||||||
private String webPublicUrl;
|
private String webPublicUrl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the server is in production mode.
|
||||||
|
*/
|
||||||
|
private boolean production = false;
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
public void onInitialize() {
|
public void onInitialize() {
|
||||||
INSTANCE = this;
|
INSTANCE = this;
|
||||||
|
String environmentProperty = environment.getProperty("ENVIRONMENT", "development");
|
||||||
|
production = environmentProperty.equalsIgnoreCase("production"); // Set the production mode
|
||||||
|
log.info("Server is running in {} mode", production ? "production" : "development");
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -13,7 +13,6 @@ import org.springframework.http.MediaType;
|
|||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@ -50,6 +49,7 @@ public class PlayerController {
|
|||||||
@Parameter(description = "The part of the skin", example = "head") @PathVariable String part,
|
@Parameter(description = "The part of the skin", example = "head") @PathVariable String part,
|
||||||
@Parameter(description = "The UUID or Username of the player", example = "ImFascinated") @PathVariable String id,
|
@Parameter(description = "The UUID or Username of the player", example = "ImFascinated") @PathVariable String id,
|
||||||
@Parameter(description = "The size of the image", example = "256") @RequestParam(required = false, defaultValue = "256") int size,
|
@Parameter(description = "The size of the image", example = "256") @RequestParam(required = false, defaultValue = "256") int size,
|
||||||
|
@Parameter(description = "Whether to render the skin overlay (skin layers)", example = "false") @RequestParam(required = false, defaultValue = "false") boolean renderOverlay,
|
||||||
@Parameter(description = "Whether to download the image") @RequestParam(required = false, defaultValue = "false") boolean download) {
|
@Parameter(description = "Whether to download the image") @RequestParam(required = false, defaultValue = "false") boolean download) {
|
||||||
CachedPlayer player = playerService.getPlayer(id);
|
CachedPlayer player = playerService.getPlayer(id);
|
||||||
Skin.Parts skinPart = Skin.Parts.fromName(part);
|
Skin.Parts skinPart = Skin.Parts.fromName(part);
|
||||||
@ -60,6 +60,6 @@ public class PlayerController {
|
|||||||
.cacheControl(cacheControl)
|
.cacheControl(cacheControl)
|
||||||
.contentType(MediaType.IMAGE_PNG)
|
.contentType(MediaType.IMAGE_PNG)
|
||||||
.header(HttpHeaders.CONTENT_DISPOSITION, dispositionHeader.formatted(player.getUsername()))
|
.header(HttpHeaders.CONTENT_DISPOSITION, dispositionHeader.formatted(player.getUsername()))
|
||||||
.body(playerService.getSkinPart(player, skinPart, size).getBytes());
|
.body(playerService.getSkinPart(player, skinPart, renderOverlay, size).getBytes());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -41,10 +41,7 @@ public class MojangProfile {
|
|||||||
if (textureProperty == null) {
|
if (textureProperty == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
JsonObject texturesJson = textureProperty.getDecodedValue().getAsJsonObject("textures"); // Parse the decoded JSON and get the texture object
|
||||||
JsonObject json = Main.GSON.fromJson(textureProperty.getDecodedValue(), JsonObject.class); // Decode the texture property
|
|
||||||
JsonObject texturesJson = json.getAsJsonObject("textures"); // Parse the decoded JSON and get the textures object
|
|
||||||
|
|
||||||
return new Tuple<>(Skin.fromJson(texturesJson.getAsJsonObject("SKIN")).populatePartUrls(this.getFormattedUuid()),
|
return new Tuple<>(Skin.fromJson(texturesJson.getAsJsonObject("SKIN")).populatePartUrls(this.getFormattedUuid()),
|
||||||
Cape.fromJson(texturesJson.getAsJsonObject("CAPE")));
|
Cape.fromJson(texturesJson.getAsJsonObject("CAPE")));
|
||||||
}
|
}
|
||||||
@ -95,8 +92,8 @@ public class MojangProfile {
|
|||||||
* @return the decoded value
|
* @return the decoded value
|
||||||
*/
|
*/
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
public String getDecodedValue() {
|
public JsonObject getDecodedValue() {
|
||||||
return new String(Base64.getDecoder().decode(this.value));
|
return Main.GSON.fromJson(new String(Base64.getDecoder().decode(this.value)), JsonObject.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -3,6 +3,9 @@ package cc.fascinated.model.player;
|
|||||||
import cc.fascinated.common.PlayerUtils;
|
import cc.fascinated.common.PlayerUtils;
|
||||||
import cc.fascinated.config.Config;
|
import cc.fascinated.config.Config;
|
||||||
import cc.fascinated.exception.impl.BadRequestException;
|
import cc.fascinated.exception.impl.BadRequestException;
|
||||||
|
import cc.fascinated.service.skin.SkinPartParser;
|
||||||
|
import cc.fascinated.service.skin.impl.FlatParser;
|
||||||
|
import cc.fascinated.service.skin.impl.IsometricHeadParser;
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
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;
|
||||||
@ -77,7 +80,7 @@ public class Skin {
|
|||||||
public Skin populatePartUrls(String playerUuid) {
|
public Skin populatePartUrls(String playerUuid) {
|
||||||
for (Parts part : Parts.values()) {
|
for (Parts part : Parts.values()) {
|
||||||
String partName = part.name().toLowerCase();
|
String partName = part.name().toLowerCase();
|
||||||
this.partUrls.put(partName, Config.INSTANCE.getWebPublicUrl() + "/player/" + partName + "/" + playerUuid + "?size=" + part.getDefaultSize());
|
this.partUrls.put(partName, Config.INSTANCE.getWebPublicUrl() + "/player/" + partName + "/" + playerUuid);
|
||||||
}
|
}
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
@ -89,22 +92,16 @@ public class Skin {
|
|||||||
@Getter @AllArgsConstructor
|
@Getter @AllArgsConstructor
|
||||||
public enum Parts {
|
public enum Parts {
|
||||||
|
|
||||||
HEAD(8, 8, 8, 8, 256);
|
/**
|
||||||
|
* Head parts
|
||||||
|
*/
|
||||||
|
HEAD(new FlatParser(8, 8, 8)),
|
||||||
|
HEAD_ISOMETRIC(new IsometricHeadParser());
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The x and y position of the part.
|
* The skin part parser for the part.
|
||||||
*/
|
*/
|
||||||
private final int x, y;
|
private final SkinPartParser skinPartParser;
|
||||||
|
|
||||||
/**
|
|
||||||
* The width and height of the part.
|
|
||||||
*/
|
|
||||||
private final int width, height;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The scale of the part.
|
|
||||||
*/
|
|
||||||
private final int defaultSize;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the name of the part.
|
* Gets the name of the part.
|
||||||
|
@ -3,6 +3,7 @@ package cc.fascinated.service;
|
|||||||
import cc.fascinated.common.PlayerUtils;
|
import cc.fascinated.common.PlayerUtils;
|
||||||
import cc.fascinated.common.Tuple;
|
import cc.fascinated.common.Tuple;
|
||||||
import cc.fascinated.common.UUIDUtils;
|
import cc.fascinated.common.UUIDUtils;
|
||||||
|
import cc.fascinated.config.Config;
|
||||||
import cc.fascinated.exception.impl.MojangAPIRateLimitException;
|
import cc.fascinated.exception.impl.MojangAPIRateLimitException;
|
||||||
import cc.fascinated.exception.impl.RateLimitException;
|
import cc.fascinated.exception.impl.RateLimitException;
|
||||||
import cc.fascinated.exception.impl.ResourceNotFoundException;
|
import cc.fascinated.exception.impl.ResourceNotFoundException;
|
||||||
@ -58,7 +59,7 @@ public class PlayerService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Optional<CachedPlayer> cachedPlayer = playerCacheRepository.findById(uuid);
|
Optional<CachedPlayer> cachedPlayer = playerCacheRepository.findById(uuid);
|
||||||
if (cachedPlayer.isPresent()) { // Return the cached player if it exists
|
if (cachedPlayer.isPresent() && Config.INSTANCE.isProduction()) { // Return the cached player if it exists
|
||||||
log.info("Player {} is cached", originalId);
|
log.info("Player {} is cached", originalId);
|
||||||
return cachedPlayer.get();
|
return cachedPlayer.get();
|
||||||
}
|
}
|
||||||
@ -94,7 +95,7 @@ public class PlayerService {
|
|||||||
public CachedPlayerName usernameToUuid(String username) {
|
public CachedPlayerName usernameToUuid(String username) {
|
||||||
log.info("Getting UUID from username: {}", username);
|
log.info("Getting UUID from username: {}", username);
|
||||||
Optional<CachedPlayerName> cachedPlayerName = playerNameCacheRepository.findById(username.toUpperCase());
|
Optional<CachedPlayerName> cachedPlayerName = playerNameCacheRepository.findById(username.toUpperCase());
|
||||||
if (cachedPlayerName.isPresent()) {
|
if (cachedPlayerName.isPresent() && Config.INSTANCE.isProduction()) {
|
||||||
return cachedPlayerName.get();
|
return cachedPlayerName.get();
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@ -118,20 +119,21 @@ public class PlayerService {
|
|||||||
*
|
*
|
||||||
* @param player the player
|
* @param player the player
|
||||||
* @param part the part of the skin
|
* @param part the part of the skin
|
||||||
|
* @param renderOverlay whether to render the overlay
|
||||||
* @return the skin part
|
* @return the skin part
|
||||||
*/
|
*/
|
||||||
public CachedPlayerSkinPart getSkinPart(Player player, Skin.Parts part, int size) {
|
public CachedPlayerSkinPart getSkinPart(Player player, Skin.Parts part, boolean renderOverlay, int size) {
|
||||||
log.info("Getting skin part: {} for player: {}", part.getName(), player.getUniqueId());
|
log.info("Getting skin part: {} for player: {}", part.getName(), player.getUniqueId());
|
||||||
String key = "%s-%s-%s".formatted(player.getUniqueId(), part.getName(), size);
|
String key = "%s-%s-%s".formatted(player.getUniqueId(), part.getName(), size);
|
||||||
Optional<CachedPlayerSkinPart> cache = playerSkinPartCacheRepository.findById(key);
|
Optional<CachedPlayerSkinPart> cache = playerSkinPartCacheRepository.findById(key);
|
||||||
|
|
||||||
// The skin part is cached
|
// The skin part is cached
|
||||||
if (cache.isPresent()) {
|
if (cache.isPresent() && Config.INSTANCE.isProduction()) {
|
||||||
log.info("Skin part {} for player {} is cached", part.getName(), player.getUniqueId());
|
log.info("Skin part {} for player {} is cached", part.getName(), player.getUniqueId());
|
||||||
return cache.get();
|
return cache.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
byte[] skinPartBytes = PlayerUtils.getSkinPartBytes(player.getSkin(), part, size);
|
byte[] skinPartBytes = part.getSkinPartParser().getPart(player.getSkin(), part.getName(), renderOverlay, size);
|
||||||
CachedPlayerSkinPart skinPart = new CachedPlayerSkinPart(
|
CachedPlayerSkinPart skinPart = new CachedPlayerSkinPart(
|
||||||
key,
|
key,
|
||||||
skinPartBytes
|
skinPartBytes
|
||||||
|
@ -2,6 +2,7 @@ package cc.fascinated.service;
|
|||||||
|
|
||||||
import cc.fascinated.common.DNSUtils;
|
import cc.fascinated.common.DNSUtils;
|
||||||
import cc.fascinated.common.EnumUtils;
|
import cc.fascinated.common.EnumUtils;
|
||||||
|
import cc.fascinated.config.Config;
|
||||||
import cc.fascinated.exception.impl.BadRequestException;
|
import cc.fascinated.exception.impl.BadRequestException;
|
||||||
import cc.fascinated.exception.impl.ResourceNotFoundException;
|
import cc.fascinated.exception.impl.ResourceNotFoundException;
|
||||||
import cc.fascinated.model.cache.CachedMinecraftServer;
|
import cc.fascinated.model.cache.CachedMinecraftServer;
|
||||||
@ -63,7 +64,7 @@ public class ServerService {
|
|||||||
|
|
||||||
// Check if the server is cached
|
// Check if the server is cached
|
||||||
Optional<CachedMinecraftServer> cached = serverCacheRepository.findById(key);
|
Optional<CachedMinecraftServer> cached = serverCacheRepository.findById(key);
|
||||||
if (cached.isPresent()) {
|
if (cached.isPresent() && Config.INSTANCE.isProduction()) {
|
||||||
log.info("Server {}:{} is cached", hostname, port);
|
log.info("Server {}:{} is cached", hostname, port);
|
||||||
return cached.get();
|
return cached.get();
|
||||||
}
|
}
|
||||||
|
48
src/main/java/cc.fascinated/service/skin/SkinPartParser.java
Normal file
48
src/main/java/cc.fascinated/service/skin/SkinPartParser.java
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
package cc.fascinated.service.skin;
|
||||||
|
|
||||||
|
import cc.fascinated.common.ImageUtils;
|
||||||
|
import cc.fascinated.model.player.Skin;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.SneakyThrows;
|
||||||
|
|
||||||
|
import javax.imageio.ImageIO;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
|
||||||
|
@AllArgsConstructor @Getter
|
||||||
|
public abstract class SkinPartParser {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the skin part image.
|
||||||
|
*
|
||||||
|
* @param skin the skin
|
||||||
|
* @param x the x position
|
||||||
|
* @param y the y position
|
||||||
|
* @param width the width
|
||||||
|
* @param height the height
|
||||||
|
* @param scale the scale
|
||||||
|
* @return the skin part image
|
||||||
|
*/
|
||||||
|
public BufferedImage getSkinPart(Skin skin, int x, int y, int width, int height, double scale) {
|
||||||
|
try {
|
||||||
|
BufferedImage skinImage = ImageIO.read(new ByteArrayInputStream(skin.getSkinImage()));
|
||||||
|
BufferedImage part = skinImage.getSubimage(x, y, width, height);
|
||||||
|
return ImageUtils.resize(part, scale);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the skin part image.
|
||||||
|
*
|
||||||
|
* @param skin the skin
|
||||||
|
* @param partName the skin part name
|
||||||
|
* @param renderOverlay whether to render the overlay
|
||||||
|
* @param size the output size
|
||||||
|
* @return the skin part image
|
||||||
|
*/
|
||||||
|
public abstract byte[] getPart(Skin skin, String partName, boolean renderOverlay, int size);
|
||||||
|
}
|
@ -0,0 +1,66 @@
|
|||||||
|
package cc.fascinated.service.skin.impl;
|
||||||
|
|
||||||
|
import cc.fascinated.common.ImageUtils;
|
||||||
|
import cc.fascinated.model.player.Skin;
|
||||||
|
import cc.fascinated.service.skin.SkinPartParser;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.extern.log4j.Log4j2;
|
||||||
|
|
||||||
|
import javax.imageio.ImageIO;
|
||||||
|
import java.awt.*;
|
||||||
|
import java.awt.geom.AffineTransform;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
|
||||||
|
@Getter @Log4j2
|
||||||
|
public class FlatParser extends SkinPartParser {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The x and y position of the part.
|
||||||
|
*/
|
||||||
|
private final int x, y;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The width and height of the part.
|
||||||
|
*/
|
||||||
|
private final int widthAndHeight;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new {@link FlatParser}.
|
||||||
|
*
|
||||||
|
* @param x the x position of the part
|
||||||
|
* @param y the y position of the part
|
||||||
|
* @param widthAndHeight the width and height of the part
|
||||||
|
*/
|
||||||
|
public FlatParser(int x, int y, int widthAndHeight) {
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
this.widthAndHeight = widthAndHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] getPart(Skin skin, String partName, boolean renderOverlay, int size) {
|
||||||
|
double scale = (double) size / this.widthAndHeight;
|
||||||
|
log.info("Getting {} part bytes for {} with size {} and scale {}", partName, skin.getUrl(), size, scale);
|
||||||
|
|
||||||
|
try {
|
||||||
|
BufferedImage outputImage = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB);
|
||||||
|
Graphics2D graphics = outputImage.createGraphics();
|
||||||
|
|
||||||
|
graphics.setTransform(AffineTransform.getScaleInstance(scale, scale));
|
||||||
|
graphics.drawImage(this.getSkinPart(skin, this.x, this.y, this.widthAndHeight, this.widthAndHeight, 1), 0, 0, null);
|
||||||
|
|
||||||
|
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
|
||||||
|
ImageIO.write(outputImage, "png", outputStream);
|
||||||
|
// Cleanup
|
||||||
|
outputStream.flush();
|
||||||
|
graphics.dispose();
|
||||||
|
log.info("Successfully got {} part bytes for {}", partName, skin.getUrl());
|
||||||
|
return outputStream.toByteArray();
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.error("Failed to get {} part bytes for {}", partName, skin.getUrl(), ex);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,102 @@
|
|||||||
|
package cc.fascinated.service.skin.impl;
|
||||||
|
|
||||||
|
import cc.fascinated.common.ImageUtils;
|
||||||
|
import cc.fascinated.model.player.Skin;
|
||||||
|
import cc.fascinated.service.skin.SkinPartParser;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.extern.log4j.Log4j2;
|
||||||
|
|
||||||
|
import javax.imageio.ImageIO;
|
||||||
|
import java.awt.*;
|
||||||
|
import java.awt.geom.AffineTransform;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
|
||||||
|
@Getter @Log4j2
|
||||||
|
public class IsometricHeadParser extends SkinPartParser {
|
||||||
|
|
||||||
|
private static final double SKEW_A = 26d / 45d; // 0.57777777
|
||||||
|
private static final double SKEW_B = SKEW_A * 2d; // 1.15555555
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] getPart(Skin skin, String partName, boolean renderOverlay, int size) {
|
||||||
|
double scale = (size / 8d) / 2.5;
|
||||||
|
log.info("Getting {} part bytes for {} with size {} and scale {}", partName, skin.getUrl(), size, scale);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final BufferedImage outputImage = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB);
|
||||||
|
|
||||||
|
// Get all the required head parts
|
||||||
|
final BufferedImage headTop = ImageUtils.resize(this.getSkinPart(skin, 8, 0, 8, 8, 1), scale);
|
||||||
|
final BufferedImage headFront = ImageUtils.resize(this.getSkinPart(skin, 8, 8, 8, 8, 1), scale);
|
||||||
|
final BufferedImage headRight = ImageUtils.resize(this.getSkinPart(skin, 0, 8, 8, 8, 1), scale);
|
||||||
|
|
||||||
|
if (renderOverlay) {
|
||||||
|
// Draw the overlay on top of the gathered skin parts
|
||||||
|
|
||||||
|
// Top overlay
|
||||||
|
Graphics2D g = headTop.createGraphics();
|
||||||
|
g.drawImage(this.getSkinPart(skin, 40, 0, 8, 8, 1), 0, 0, null);
|
||||||
|
g.dispose();
|
||||||
|
|
||||||
|
// Front overlay
|
||||||
|
g = headFront.createGraphics();
|
||||||
|
g.drawImage(this.getSkinPart(skin, 16, 8, 8, 8, 1), 0, 0, null);
|
||||||
|
g.dispose();
|
||||||
|
|
||||||
|
// Right side overlay
|
||||||
|
g = headRight.createGraphics();
|
||||||
|
g.drawImage(this.getSkinPart(skin, 32, 8, 8, 8, 1), 0, 0, null);
|
||||||
|
g.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Declare pos
|
||||||
|
double x;
|
||||||
|
double y;
|
||||||
|
double z;
|
||||||
|
|
||||||
|
// Declare offsets
|
||||||
|
final double z_offset = scale * 3.5d;
|
||||||
|
final double x_offset = scale * 2d;
|
||||||
|
|
||||||
|
// Create graphics
|
||||||
|
final Graphics2D outGraphics = outputImage.createGraphics();
|
||||||
|
|
||||||
|
// head top
|
||||||
|
x = x_offset;
|
||||||
|
y = -0.5;
|
||||||
|
z = z_offset;
|
||||||
|
outGraphics.setTransform(new AffineTransform(1d, -SKEW_A, 1, SKEW_A, 0, 0));
|
||||||
|
outGraphics.drawImage(headTop, (int) (y - z), (int) (x + z), headTop.getWidth(), headTop.getHeight() + 1, null);
|
||||||
|
|
||||||
|
// head front
|
||||||
|
x = x_offset + 8 * scale;
|
||||||
|
y = 0;
|
||||||
|
z = z_offset - 0.5;
|
||||||
|
outGraphics.setTransform(new AffineTransform(1d, -SKEW_A, 0d, SKEW_B, 0d, SKEW_A));
|
||||||
|
outGraphics.drawImage(headFront, (int) (y + x), (int) (x + z), headFront.getWidth(), headFront.getHeight(), null);
|
||||||
|
|
||||||
|
// head right
|
||||||
|
x = x_offset;
|
||||||
|
y = 0;
|
||||||
|
z = z_offset;
|
||||||
|
outGraphics.setTransform(new AffineTransform(1d, SKEW_A, 0d, SKEW_B, 0d, 0d));
|
||||||
|
outGraphics.drawImage(headRight, (int) (x + y + 1), (int) (z - y - 0.5), headRight.getWidth(), headRight.getHeight() + 1, null);
|
||||||
|
|
||||||
|
// Cleanup and return
|
||||||
|
outGraphics.dispose();
|
||||||
|
|
||||||
|
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
|
||||||
|
ImageIO.write(outputImage, "png", outputStream);
|
||||||
|
// Cleanup
|
||||||
|
outputStream.flush();
|
||||||
|
outGraphics.dispose();
|
||||||
|
log.info("Successfully got {} part bytes for {}", partName, skin.getUrl());
|
||||||
|
return outputStream.toByteArray();
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.error("Failed to get {} part bytes for {}", partName, skin.getUrl(), ex);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user