add server preview renderer

This commit is contained in:
Lee 2024-04-20 19:37:58 +01:00
parent ff58b1756a
commit d2ae4b4cc5
17 changed files with 344 additions and 17 deletions

@ -3,6 +3,7 @@ package xyz.mcutils.backend.common;
import lombok.NonNull; import lombok.NonNull;
import lombok.experimental.UtilityClass; import lombok.experimental.UtilityClass;
import java.awt.*;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@ -93,4 +94,20 @@ public final class ColorUtils {
return builder.toString(); return builder.toString();
} }
/**
* Gets a {@link Color} from a Minecraft color code.
*
* @param colorCode the color code to get the color from
* @return the color
*/
public static Color getMinecraftColor(char colorCode) {
String color = COLOR_MAP.getOrDefault(colorCode, null);
if (color == null) {
System.out.println("Unknown color code: " + colorCode);
return Color.WHITE;
}
return Color.decode(color);
}
} }

@ -0,0 +1,23 @@
package xyz.mcutils.backend.common;
import xyz.mcutils.backend.Main;
import java.awt.*;
import java.io.IOException;
import java.io.InputStream;
public class Fonts {
public static final Font MINECRAFT;
public static final Font MINECRAFT_BOLD;
static {
InputStream stream = Main.class.getResourceAsStream("/fonts/minecraft-font.ttf");
try {
MINECRAFT = Font.createFont(Font.TRUETYPE_FONT, stream).deriveFont(18f);
MINECRAFT_BOLD = MINECRAFT.deriveFont(Font.BOLD);
} catch (FontFormatException | IOException e) {
throw new RuntimeException(e);
}
}
}

@ -8,21 +8,23 @@ 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.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.util.Base64;
@Log4j2(topic = "Image Utils") @Log4j2(topic = "Image Utils")
public class ImageUtils { public class ImageUtils {
/** /**
* Scale the given image to the provided size. * Scale the given image to the provided scale.
* *
* @param image the image to scale * @param image the image to scale
* @param size the size to scale the image to * @param scale the scale to scale the image to
* @return the scaled image * @return the scaled image
*/ */
public static BufferedImage resize(BufferedImage image, double size) { public static BufferedImage resize(BufferedImage image, double scale) {
BufferedImage scaled = new BufferedImage((int) (image.getWidth() * size), (int) (image.getHeight() * size), BufferedImage.TYPE_INT_ARGB); BufferedImage scaled = new BufferedImage((int) (image.getWidth() * scale), (int) (image.getHeight() * scale), BufferedImage.TYPE_INT_ARGB);
Graphics2D graphics = scaled.createGraphics(); Graphics2D graphics = scaled.createGraphics();
graphics.drawImage(image, AffineTransform.getScaleInstance(size, size), null); graphics.drawImage(image, AffineTransform.getScaleInstance(scale, scale), null);
graphics.dispose(); graphics.dispose();
return scaled; return scaled;
} }
@ -56,4 +58,21 @@ public class ImageUtils {
throw new Exception("Failed to convert image to bytes", e); throw new Exception("Failed to convert image to bytes", e);
} }
} }
/**
* Convert a base64 string to an image.
*
* @param base64 the base64 string to convert
* @return the image
*/
@SneakyThrows
public static BufferedImage base64ToImage(String base64) {
String favicon = base64.contains("data:image/png;base64,") ? base64.split(",")[1] : base64;
try {
return ImageIO.read(new ByteArrayInputStream(Base64.getDecoder().decode(favicon)));
} catch (Exception e) {
throw new Exception("Failed to convert base64 to image", e);
}
}
} }

@ -0,0 +1,14 @@
package xyz.mcutils.backend.common.renderer;
import java.awt.image.BufferedImage;
public abstract class Renderer<T> {
/**
* Renders the object to the specified size.
*
* @param input The object to render.
* @param size The size to render the object to.
*/
public abstract BufferedImage render(T input, int size);
}

@ -0,0 +1,167 @@
package xyz.mcutils.backend.common.renderer.impl.misc;
import lombok.extern.log4j.Log4j2;
import xyz.mcutils.backend.Main;
import xyz.mcutils.backend.common.ColorUtils;
import xyz.mcutils.backend.common.Fonts;
import xyz.mcutils.backend.common.ImageUtils;
import xyz.mcutils.backend.common.renderer.Renderer;
import xyz.mcutils.backend.model.server.JavaMinecraftServer;
import xyz.mcutils.backend.model.server.MinecraftServer;
import xyz.mcutils.backend.service.ServerService;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
@Log4j2
public class ServerPreviewRenderer extends Renderer<MinecraftServer> {
public static final ServerPreviewRenderer INSTANCE = new ServerPreviewRenderer();
private static BufferedImage SERVER_BACKGROUND;
private static BufferedImage PING_ICON;
static {
try {
SERVER_BACKGROUND = ImageIO.read(new ByteArrayInputStream(Main.class.getResourceAsStream("/icons/server_background.png").readAllBytes()));
PING_ICON = ImageIO.read(new ByteArrayInputStream(Main.class.getResourceAsStream("/icons/ping.png").readAllBytes()));
} catch (Exception ex) {
log.error("Failed to load server preview assets", ex);
}
}
private final int fontSize = Fonts.MINECRAFT.getSize();
private final int width = 560;
private final int height = 64 + 3 + 3;
private final int padding = 3;
@Override
public BufferedImage render(MinecraftServer server, int size) {
BufferedImage texture = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); // The texture to return
BufferedImage favicon = getServerFavicon(server);
BufferedImage background = SERVER_BACKGROUND;
// Create the graphics for drawing
Graphics2D graphics = texture.createGraphics();
// Set up the font
graphics.setFont(Fonts.MINECRAFT);
// Draw the background
for (int backgroundX = 0; backgroundX < width + background.getWidth(); backgroundX += background.getWidth()) {
for (int backgroundY = 0; backgroundY < height + background.getHeight(); backgroundY += background.getHeight()) {
graphics.drawImage(background, backgroundX, backgroundY, null);
}
}
int y = fontSize + 1;
int x = 64 + 8;
int initialX = x; // Store the initial value of x
// Draw the favicon
graphics.drawImage(favicon, padding, padding, null);
// Draw the server hostname
graphics.setColor(Color.WHITE);
graphics.drawString(server.getHostname(), x, y);
// Draw the server motd
y += fontSize + (padding * 2);
for (String line : server.getMotd().getRaw()) {
int index = 0;
int colorIndex = line.indexOf("§");
while (colorIndex != -1) {
// Draw text before color code
String textBeforeColor = line.substring(index, colorIndex);
graphics.drawString(textBeforeColor, x, y);
// Calculate width of text before color code
int textWidth = graphics.getFontMetrics().stringWidth(textBeforeColor);
// Move x position to after the drawn text
x += textWidth;
// Set color based on color code
char colorCode = line.charAt(colorIndex + 1);
if (colorCode == 'l') {
graphics.setFont(Fonts.MINECRAFT_BOLD);
} else {
Color color = ColorUtils.getMinecraftColor(colorCode);
graphics.setColor(color);
graphics.setFont(Fonts.MINECRAFT);
}
// Move index to after the color code
index = colorIndex + 2;
// Find next color code
colorIndex = line.indexOf("§", index);
}
// Draw remaining text
String remainingText = line.substring(index);
graphics.drawString(remainingText, x, y);
// Move to the next line
y += fontSize + padding;
// Reset x position for the next line
x = initialX; // Reset x to its initial value
}
// Ensure the font is reset
graphics.setFont(Fonts.MINECRAFT);
// Render the ping
BufferedImage pingIcon = ImageUtils.resize(PING_ICON, 2);
x = width - pingIcon.getWidth() - padding;
graphics.drawImage(pingIcon, x, padding, null);
// Reset the y position
y = fontSize + 1;
// Render the player count
MinecraftServer.Players players = server.getPlayers();
String playersOnline = players.getOnline() + "";
String playersMax = players.getMax() + "";
// Calculate the width of each player count element
int maxWidth = graphics.getFontMetrics().stringWidth(playersMax);
int slashWidth = graphics.getFontMetrics().stringWidth("/");
int onlineWidth = graphics.getFontMetrics().stringWidth(playersOnline);
// Calculate the total width of the player count string
int totalWidth = maxWidth + slashWidth + onlineWidth;
// Calculate the starting x position
int startX = (width - totalWidth) - pingIcon.getWidth() - 6;
// Render the player count elements
graphics.setColor(Color.LIGHT_GRAY);
graphics.drawString(playersOnline, startX, y);
startX += onlineWidth;
graphics.setColor(Color.DARK_GRAY);
graphics.drawString("/", startX, y);
startX += slashWidth;
graphics.setColor(Color.LIGHT_GRAY);
graphics.drawString(playersMax, startX, y);
return ImageUtils.resize(texture, (double) size / width);
}
/**
* Get the favicon of a server.
*
* @param server the server to get the favicon of
* @return the server favicon
*/
public BufferedImage getServerFavicon(MinecraftServer server) {
String favicon = null;
// Get the server favicon
if (server instanceof JavaMinecraftServer javaServer) {
if (javaServer.getFavicon() != null) {
favicon = javaServer.getFavicon().getBase64();
}
}
// Fallback to the default server icon
if (favicon == null) {
favicon = ServerService.DEFAULT_SERVER_ICON;
}
return ImageUtils.base64ToImage(favicon);
}
}

@ -1,4 +1,4 @@
package xyz.mcutils.backend.common.renderer.impl; package xyz.mcutils.backend.common.renderer.impl.skin;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Getter;

@ -1,4 +1,4 @@
package xyz.mcutils.backend.common.renderer.impl; package xyz.mcutils.backend.common.renderer.impl.skin;
import xyz.mcutils.backend.common.renderer.IsometricSkinRenderer; import xyz.mcutils.backend.common.renderer.IsometricSkinRenderer;
import xyz.mcutils.backend.model.skin.ISkinPart; import xyz.mcutils.backend.model.skin.ISkinPart;

@ -1,4 +1,4 @@
package xyz.mcutils.backend.common.renderer.impl; package xyz.mcutils.backend.common.renderer.impl.skin;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Getter;

@ -56,6 +56,23 @@ public class ServerController {
.body(favicon); .body(favicon);
} }
@ResponseBody
@GetMapping(value = "/preview/{platform}/{hostname}", produces = MediaType.IMAGE_PNG_VALUE)
public ResponseEntity<?> getServerPreview(
@Parameter(description = "The platform of the server", example = "java") @PathVariable String platform,
@Parameter(description = "The hostname and port of the server", example = "aetheria.cc") @PathVariable String hostname,
@Parameter(description = "Whether to download the image") @RequestParam(required = false, defaultValue = "false") boolean download,
@Parameter(description = "The size of the image", example = "1024") @RequestParam(required = false, defaultValue = "1024") int size) {
String dispositionHeader = download ? "attachment; filename=%s.png" : "inline; filename=%s.png";
CachedMinecraftServer server = serverService.getServer(platform, hostname);
return ResponseEntity.ok()
.cacheControl(CacheControl.maxAge(1, TimeUnit.HOURS).cachePublic())
.contentType(MediaType.IMAGE_PNG)
.header(HttpHeaders.CONTENT_DISPOSITION, dispositionHeader.formatted(hostname))
.body(serverService.getServerPreview(server, platform, size));
}
@ResponseBody @ResponseBody
@GetMapping(value = "/blocked/{hostname}", produces = MediaType.APPLICATION_JSON_VALUE) @GetMapping(value = "/blocked/{hostname}", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<?> getServerBlockedStatus( public ResponseEntity<?> getServerBlockedStatus(

@ -0,0 +1,21 @@
package xyz.mcutils.backend.model.cache;
import lombok.*;
import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;
@AllArgsConstructor
@Setter @Getter @EqualsAndHashCode
@RedisHash(value = "serverPreview", timeToLive = 60L * 5) // 5 minutes (in seconds)
public class CachedServerPreview {
/**
* The ID of the server preview
*/
@Id @NonNull private String id;
/**
* The server preview bytes
*/
private byte[] bytes;
}

@ -3,9 +3,9 @@ package xyz.mcutils.backend.model.skin;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Getter;
import xyz.mcutils.backend.common.renderer.SkinRenderer; import xyz.mcutils.backend.common.renderer.SkinRenderer;
import xyz.mcutils.backend.common.renderer.impl.BodyRenderer; import xyz.mcutils.backend.common.renderer.impl.skin.BodyRenderer;
import xyz.mcutils.backend.common.renderer.impl.IsometricHeadRenderer; import xyz.mcutils.backend.common.renderer.impl.skin.IsometricHeadRenderer;
import xyz.mcutils.backend.common.renderer.impl.SquareRenderer; import xyz.mcutils.backend.common.renderer.impl.skin.SquareRenderer;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;

@ -0,0 +1,9 @@
package xyz.mcutils.backend.repository.redis;
import org.springframework.data.repository.CrudRepository;
import xyz.mcutils.backend.model.cache.CachedServerPreview;
/**
* A cache repository for server previews.
*/
public interface ServerPreviewCacheRepository extends CrudRepository<CachedServerPreview, String> { }

@ -135,12 +135,10 @@ public class PlayerService {
*/ */
public CachedPlayerSkinPart getSkinPart(Player player, String partName, boolean renderOverlay, int size) { public CachedPlayerSkinPart getSkinPart(Player player, String partName, boolean renderOverlay, int size) {
if (size > 512) { if (size > 512) {
log.info("Size {} is too large, setting to 512", size); throw new BadRequestException("Size cannot be larger than 512");
size = 512;
} }
if (size < 32) { if (size < 32) {
log.info("Size {} is too small, setting to 32", size); throw new BadRequestException("Size cannot be smaller than 32");
size = 32;
} }
ISkinPart part = ISkinPart.getByName(partName); // The skin part to get ISkinPart part = ISkinPart.getByName(partName); // The skin part to get

@ -6,15 +6,19 @@ import org.springframework.stereotype.Service;
import xyz.mcutils.backend.common.AppConfig; import xyz.mcutils.backend.common.AppConfig;
import xyz.mcutils.backend.common.DNSUtils; import xyz.mcutils.backend.common.DNSUtils;
import xyz.mcutils.backend.common.EnumUtils; import xyz.mcutils.backend.common.EnumUtils;
import xyz.mcutils.backend.common.ImageUtils;
import xyz.mcutils.backend.common.renderer.impl.misc.ServerPreviewRenderer;
import xyz.mcutils.backend.exception.impl.BadRequestException; import xyz.mcutils.backend.exception.impl.BadRequestException;
import xyz.mcutils.backend.exception.impl.ResourceNotFoundException; import xyz.mcutils.backend.exception.impl.ResourceNotFoundException;
import xyz.mcutils.backend.model.cache.CachedMinecraftServer; import xyz.mcutils.backend.model.cache.CachedMinecraftServer;
import xyz.mcutils.backend.model.cache.CachedServerPreview;
import xyz.mcutils.backend.model.dns.DNSRecord; import xyz.mcutils.backend.model.dns.DNSRecord;
import xyz.mcutils.backend.model.dns.impl.ARecord; import xyz.mcutils.backend.model.dns.impl.ARecord;
import xyz.mcutils.backend.model.dns.impl.SRVRecord; import xyz.mcutils.backend.model.dns.impl.SRVRecord;
import xyz.mcutils.backend.model.server.JavaMinecraftServer; import xyz.mcutils.backend.model.server.JavaMinecraftServer;
import xyz.mcutils.backend.model.server.MinecraftServer; import xyz.mcutils.backend.model.server.MinecraftServer;
import xyz.mcutils.backend.repository.redis.MinecraftServerCacheRepository; import xyz.mcutils.backend.repository.redis.MinecraftServerCacheRepository;
import xyz.mcutils.backend.repository.redis.ServerPreviewCacheRepository;
import xyz.mcutils.backend.service.metric.metrics.UniqueServerLookupsMetric; import xyz.mcutils.backend.service.metric.metrics.UniqueServerLookupsMetric;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
@ -25,17 +29,20 @@ import java.util.Optional;
@Service @Log4j2(topic = "Server Service") @Service @Log4j2(topic = "Server Service")
public class ServerService { public class ServerService {
private static final String DEFAULT_SERVER_ICON = "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"; public static final String DEFAULT_SERVER_ICON = "iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAASFBMVEWwsLBBQUE9PT1JSUlFRUUuLi5MTEyzs7M0NDQ5OTlVVVVQUFAmJia5ubl+fn5zc3PFxcVdXV3AwMCJiYmUlJRmZmbQ0NCjo6OL5p+6AAAFVklEQVRYw+1W67K0KAzkJnIZdRAZ3/9NtzvgXM45dX7st1VbW7XBUVDSdEISRqn/5R+T82/+nsr/XZn/SHm/3x9/ArA/IP8qwPK433d44VubZ/XT6/cJy0L792VZfnDrcRznr86d748u92X5vtaxOe228zcCy+MSMpg/5SwRopsYMv8oigCwngbQhE/rzhwAYMpxnvMvHhgy/8AgByJolzb5pPqEbvtgMBBmtvkbgxKmaaIZ5TyPum6Viue6te241N+s+W6nOlucgjEx6Nay9zZta1XVxejW+Q5ZhhkDS31lgOTegjUBor33CQilbC2GYGy9y9bN8ytevjE4a2stajHDAgAcUkoYwzO6zQi8ZflC+XO0+exiuNa3OQtIJOCk13neUjv7VO7Asu/3LwDFeg37sQtQhy4lAQH6IR9ztca0E3oI5PtDAlJ1tHGplrJ12jjrrXPWYvXsU042Bl/qUr3B9qzPSKaovpvjgglYL2F1x+Zs7gIvpLYuq46wr3H5/RJxyvM6sXOY762oU4YZ3mAz1lpc9O3Y30VJUM/iWhBIib63II/LA4COEMxcSmrH4ddl/wTYe3RIO0vK2VI9wQy6AxRsJpb3AAALvXb6TxvUCYSdOQo5Mh0GySkJc7rB405GUEfzbbl/iFpPoNQVNUQAZG06nkI6RCABRqRA9IimH6Up5Mhybtu2IlewB2Sf6AmQ4ZU9rfBELvyA23Yub6LWWtUBgK3OB79L7FILLDKWd4wpxmMRAMoLQR1ItLoiWUmhFtjptab7LQDgRARliLITLrcBkHNp9VACUH1UDRQEYGuYxzyM9H0mBccQNnCkQ3Q1UHBaO6sNyw0CelEtBGXKSoE+fJWZh5GupyneMIkCOMESAniMAzMreLvuO+pnmBQSp4C+ELCiMSGVLPh7M023SSBAiAA5yPh2m0wigEbWKnw3qDrrscF00cciCATGwNQRAv2YGvyD4Y36QGhqOS4AcABAA88oGvBCRho5H2+UiW6EfyM1L5l8a56rqdvE6lFakc3ScVDOBNBUoFM8c1vgnhAG5VsAqMD6Q9IwwtAkR39iGEQF1ZBxgU+v9UGL6MBQYiTdJllIBtx5y0rixGdAZ1YysbS53TAVy3vf4aabEpt1T0HoB2Eg4Yv5OKNwyHgmNvPKaQAYLG3EIyIqcL6Fj5C2jhXL9EpCdRMROE5nCW3qm1vfR6wYh0HKGG3wY+JgLkUWQ/WMfI8oMvIWMY7aCncNxxpSmHRUCEzDdSR0+dRwIQaMWW1FE0AOGeKkx0OLwYanBK3qfC0BSmIlozkuFcvSkulckoIB2FbHWu0y9gMHsEapMMEoySNUA2RDrduxIqr5POQV2zZ++IBOwVrFO9THrtjU2uWsCMZjxXl88Hmeaz1rPdAqXyJl68F5RTtdvN1aIyYEAMAWJaCMHvon7s23jljlxoKBEgNv6LQ25/rZIQyOdwDO3jLsqE2nbVAil21LxqFpZ2xJ3CFuE33QCo7kfkfO8kpW6gdioxdzZDLOaMMwidzeKD0RxaD7cnHHsu0jVkW5oTwwMGI0lwwA36u2nMY8AKzErLW9JxFiteyzZsAAxY1vPe5Uf68lIDVjV8JZpPfjxbc/QuyRKdAQJaAdIA4tCTht+kQJ1I4nbdjfHxgpTSLyI19pb/iuK7+9YJaZCxEIKj79YZ6uDU8f97878teRN1FzA7OvquSrVKUgk+S6ROpJfA7GpN6RPkx4voshXgu91p7CGHeA+IY8dUUVXwT7PYw12Xsj0Lfh9X4ac9XgKW86cj8bPh8XmyDOD88FLoB+YPXp4YtyB3gBPXu98xeRI2zploVCBQAAAABJRU5ErkJggg==";
private final MojangService mojangService; private final MojangService mojangService;
private final MetricService metricService; private final MetricService metricService;
private final MinecraftServerCacheRepository serverCacheRepository; private final MinecraftServerCacheRepository serverCacheRepository;
private final ServerPreviewCacheRepository serverPreviewCacheRepository;
@Autowired @Autowired
public ServerService(MojangService mojangService, MetricService metricService, MinecraftServerCacheRepository serverCacheRepository) { public ServerService(MojangService mojangService, MetricService metricService, MinecraftServerCacheRepository serverCacheRepository,
ServerPreviewCacheRepository serverPreviewCacheRepository) {
this.mojangService = mojangService; this.mojangService = mojangService;
this.metricService = metricService; this.metricService = metricService;
this.serverCacheRepository = serverCacheRepository; this.serverCacheRepository = serverCacheRepository;
this.serverPreviewCacheRepository = serverPreviewCacheRepository;
} }
/** /**
@ -131,4 +138,39 @@ public class ServerService {
} }
return Base64.getDecoder().decode(icon); // Return the decoded favicon return Base64.getDecoder().decode(icon); // Return the decoded favicon
} }
/**
* Gets the server list preview image.
*
* @param cachedServer the server to get the preview of
* @param platform the platform of the server
* @param size the size of the preview
* @return the server preview
*/
public byte[] getServerPreview(CachedMinecraftServer cachedServer, String platform, int size) {
if (size > 2048) {
throw new BadRequestException("Size cannot be greater than 2048");
}
if (size < 256) {
throw new BadRequestException("Size cannot be smaller than 256");
}
MinecraftServer server = cachedServer.getServer();
log.info("Getting preview for server: {}:{} (size {})", server.getHostname(), server.getPort(), size);
String key = "%s-%s:%s".formatted(platform, server.getHostname(), server.getPort());
// Check if the server preview is cached
Optional<CachedServerPreview> cached = serverPreviewCacheRepository.findById(key);
if (cached.isPresent() && AppConfig.isProduction()) {
log.info("Server preview for {}:{} is cached", server.getHostname(), server.getPort());
return cached.get().getBytes();
}
long start = System.currentTimeMillis();
byte[] preview = ImageUtils.imageToBytes(ServerPreviewRenderer.INSTANCE.render(server, size));
log.info("Took {}ms to render preview for server: {}:{}", System.currentTimeMillis() - start, server.getHostname(), server.getPort());
CachedServerPreview serverPreview = new CachedServerPreview(key, preview);
serverPreviewCacheRepository.save(serverPreview);
return preview;
}
} }

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 593 B