1
0
mirror of https://github.com/GeyserMC/PackConverter.git synced 2025-12-19 14:59:21 +00:00

Polish up FontTransformer, makes it easier to understand and maintain

This commit is contained in:
Aurora
2025-07-27 18:49:13 +01:00
parent 9cdb48187a
commit c4ab6c3f49

View File

@@ -36,34 +36,39 @@ import team.unnamed.creative.font.*;
import team.unnamed.creative.font.Font; import team.unnamed.creative.font.Font;
import team.unnamed.creative.texture.Texture; import team.unnamed.creative.texture.Texture;
import javax.imageio.ImageIO;
import java.awt.*; import java.awt.*;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.*; import java.util.*;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
@AutoService(TextureTransformer.class) @AutoService(TextureTransformer.class)
public class FontTransformer implements TextureTransformer { public class FontTransformer implements TextureTransformer {
// Mappings for where characters should go in the default8.png file
private static final List<FontMapping> DEFAULT8_MAPPINGS = new ArrayList<>(); private static final List<FontMapping> DEFAULT8_MAPPINGS = new ArrayList<>();
// Data for the 3 common font images in java
private static final Map<String, FontData> FONT_DATA = Map.of( private static final Map<String, FontData> FONT_DATA = Map.of(
"ascii", new FontData(8, 8), "ascii", new FontData(8, 8),
"accented", new FontData(9, 12, 0.888f, 0.666f), "accented", new FontData(9, 12, 0.888f, 0.666f),
"nonlatin_european", new FontData(8, 8) "nonlatin_european", new FontData(8, 8)
); );
// A fallback for the above
private static final FontData DEFAULT_FONT_DATA = new FontData(8, 8); private static final FontData DEFAULT_FONT_DATA = new FontData(8, 8);
@Override @Override
public void transform(@NotNull TransformContext context) throws IOException { public void transform(@NotNull TransformContext context) throws IOException {
// We first do default8, since we may return if needed below
transformDefault8(context); transformDefault8(context);
List<UnicodeFontData> unicodeFontData = new ArrayList<>(); List<UnicodeFontData> unicodeFontData = new ArrayList<>();
// Currently, only the default font is converted, custom fonts are not supported on bedrock
for (Font font : context.javaResourcePack().fonts()) { for (Font font : context.javaResourcePack().fonts()) {
// TODO Can we register custom fonts to bedrock and use those instead?
if (!font.key().equals(Key.key(Key.MINECRAFT_NAMESPACE, "default"))) continue; if (!font.key().equals(Key.key(Key.MINECRAFT_NAMESPACE, "default"))) continue;
for (FontProvider fontProvider : font.providers()) { for (FontProvider fontProvider : font.providers()) {
@@ -80,7 +85,6 @@ public class FontTransformer implements TextureTransformer {
) return; ) return;
for (Font font : context.vanillaPack().fonts()) { for (Font font : context.vanillaPack().fonts()) {
// TODO Can we register custom fonts to bedrock and use those instead?
if (!font.key().equals(Key.key(Key.MINECRAFT_NAMESPACE, "default"))) continue; if (!font.key().equals(Key.key(Key.MINECRAFT_NAMESPACE, "default"))) continue;
for (FontProvider fontProvider : font.providers()) { for (FontProvider fontProvider : font.providers()) {
@@ -96,35 +100,32 @@ public class FontTransformer implements TextureTransformer {
for (UnicodeFontData fontData : unicodeFontData) { for (UnicodeFontData fontData : unicodeFontData) {
byte[] bytes = String.valueOf(fontData.character()).getBytes(StandardCharsets.UTF_16BE); byte[] bytes = String.valueOf(fontData.character()).getBytes(StandardCharsets.UTF_16BE);
byte upperData = bytes.length == 1 ? 0 : bytes[0]; byte upperData = bytes.length == 1 ? 0 : bytes[0]; // The first byte, determines which image this character will be written to
containedCharacters.computeIfAbsent(upperData, ignored -> new ArrayList<>()); containedCharacters.computeIfAbsent(upperData, ignored -> new ArrayList<>());
containedCharacters.get(upperData).add(fontData); containedCharacters.get(upperData).add(fontData);
if (fontData.filename() == null) continue; fontData.computeCache(context, images);
images.computeIfAbsent(fontData.filename(), filename -> {
Texture texture = context.pollOrPeekVanilla(filename);
try {
return texture == null ? null : this.readImage(texture);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
} }
// A formatter for our hex value, used when writing the glyph files
HexFormat hexFormat = HexFormat.of(); HexFormat hexFormat = HexFormat.of();
for (Map.Entry<Byte, List<UnicodeFontData>> data : containedCharacters.entrySet()) { for (Map.Entry<Byte, List<UnicodeFontData>> data : containedCharacters.entrySet()) {
int maxWidth = data.getValue().stream().mapToInt(fontData -> { if (data.getValue().isEmpty()) continue; // We have nothing to work with
if (fontData.filename() == null) return fontData.spaces();
return (int) (fontData.width() * FONT_DATA.getOrDefault(fontData.filename().value().substring(5, fontData.filename().value().length() - 4), DEFAULT_FONT_DATA).scaleX()); // Determine the size the image should be to fit all our characters
}).max().getAsInt(); // Better to default to something than an exception, so lets default to 1
int maxHeight = data.getValue().stream().mapToInt(fontData -> { int maxWidth = data.getValue().stream().mapToInt(
if (fontData.filename() == null) return 1; fontData ->
return (int) (fontData.height * FONT_DATA.getOrDefault(fontData.filename().value().substring(5, fontData.filename().value().length() - 4), DEFAULT_FONT_DATA).scaleY()); (int) (fontData.width() * fontData.fontData().scaleX())
}).max().getAsInt(); ).max().orElse(1);
int maxHeight = data.getValue().stream().mapToInt(
fontData ->
(int) (fontData.height() * fontData.fontData().scaleY())
).max().orElse(1);
int size = Math.max(maxWidth, maxHeight); int size = Math.max(maxWidth, maxHeight);
@@ -133,45 +134,32 @@ public class FontTransformer implements TextureTransformer {
Graphics g = bedrockImage.getGraphics(); Graphics g = bedrockImage.getGraphics();
for (UnicodeFontData fontData : data.getValue()) { for (UnicodeFontData fontData : data.getValue()) {
if (!fontData.shouldRead()) continue;
int dataWidth = fontData.width(); int dataWidth = fontData.width();
int dataHeight = fontData.height(); int dataHeight = fontData.height();
int dataX = fontData.x(); int dataX = fontData.x();
int dataY = fontData.y(); int dataY = fontData.y();
BufferedImage javaImage; BufferedImage javaImage = fontData.readJavaImage(images);
if (javaImage == null) {
if (fontData.filename() == null) { // This is a space character, treat it differently context.warn("Missing font file, unable to write character '%s'.".formatted(fontData.character()));
dataWidth = fontData.spaces(); continue;
dataHeight = 1;
dataX = 0;
dataY = 0;
if (dataWidth < 1) continue; // Skip, the character will just be blank in that case
javaImage = new BufferedImage(dataWidth, dataHeight, BufferedImage.TYPE_INT_ARGB);
Graphics javaGraphics = javaImage.getGraphics();
javaGraphics.setColor(new Color(255, 255, 255, 1));
javaGraphics.drawRect(0, 0, dataWidth, dataHeight);
} else {
javaImage = images.get(fontData.filename());
if (javaImage == null) {
context.warn("Missing %s, unable to write character.".formatted(fontData.filename().asString()));
continue;
}
} }
byte[] bytes = String.valueOf(fontData.character()).getBytes(StandardCharsets.UTF_16BE); byte[] bytes = String.valueOf(fontData.character()).getBytes(StandardCharsets.UTF_16BE);
int position = (bytes.length == 1 ? bytes[0] : bytes[1]) & 0xff; int position = bytes[bytes.length - 1] & 0xff; // The last byte of the character
// Now we can find where the character belongs in the bedrock image // Now we can find where the character belongs in the bedrock image
int desX = position % 16; int desX = position % 16;
int desY = position / 16; int desY = position / 16;
// Determine how to scale the image to ensure they're in line with every other character
float scaleX = (float) maxWidth / dataWidth; float scaleX = (float) maxWidth / dataWidth;
float scaleY = (float) maxHeight / dataHeight; float scaleY = (float) maxHeight / dataHeight;
float scale = Math.min(scaleX, scaleY); float scale = Math.min(scaleX, scaleY); // Prevent stretching, use the minimum one
// Since we don't stretch fully, we should offset to ensure the character appears correctly in bedrock
int xOffset = (size - dataWidth) / 2; int xOffset = (size - dataWidth) / 2;
int yOffset = (size - dataHeight) / 2; int yOffset = (size - dataHeight) / 2;
@@ -204,17 +192,12 @@ public class FontTransformer implements TextureTransformer {
List<UnicodeFontData> unicodeFontData = new ArrayList<>(); List<UnicodeFontData> unicodeFontData = new ArrayList<>();
if (fontProvider instanceof SpaceFontProvider spaceFontProvider) { if (fontProvider instanceof SpaceFontProvider spaceFontProvider) {
// Simple space fonts, easy to handle
for (Map.Entry<String, Integer> entry : spaceFontProvider.advances().entrySet()) { for (Map.Entry<String, Integer> entry : spaceFontProvider.advances().entrySet()) {
unicodeFontData.add(new UnicodeFontData( unicodeFontData.add(new SpaceFontData(entry.getKey().charAt(0), entry.getValue()));
null,
entry.getKey().charAt(0),
0, 0, 0, 0,
entry.getValue()
));
} }
} else if (fontProvider instanceof BitMapFontProvider bitMapFontProvider) { } else if (fontProvider instanceof BitMapFontProvider bitMapFontProvider) {
// First of all we need to determine the width and height of the characters // First of all we need to determine the width and height of the characters
Texture texture = context.peek(bitMapFontProvider.file()); Texture texture = context.peek(bitMapFontProvider.file());
if (texture == null) return unicodeFontData; // We don't have the texture, so we can't continue if (texture == null) return unicodeFontData; // We don't have the texture, so we can't continue
@@ -228,7 +211,7 @@ public class FontTransformer implements TextureTransformer {
for (String charLines : bitMapFontProvider.characters()) { for (String charLines : bitMapFontProvider.characters()) {
for (char character : charLines.toCharArray()) { for (char character : charLines.toCharArray()) {
unicodeFontData.add(new UnicodeFontData( unicodeFontData.add(new BitMapFontData(
bitMapFontProvider.file(), bitMapFontProvider.file(),
character, character,
x, y, width, height x, y, width, height
@@ -240,7 +223,18 @@ public class FontTransformer implements TextureTransformer {
y++; y++;
} }
} else if (fontProvider instanceof ReferenceFontProvider referenceFontProvider) { } else if (fontProvider instanceof ReferenceFontProvider referenceFontProvider) {
for (FontProvider fontProvider1 : context.javaResourcePack().font(referenceFontProvider.id()).providers()) { // Refers to other fonts, so we need to read those
Font font = context.javaResourcePack().font(referenceFontProvider.id());
if (font == null) { // Just maybe, the vanilla files are used
font = context.vanillaPack().font(referenceFontProvider.id());
}
if (font == null) {
context.warn("Unable to find font %s, continuing without.".formatted(referenceFontProvider.id().asString()));
return unicodeFontData;
}
for (FontProvider fontProvider1 : font.providers()) {
unicodeFontData.addAll(handleFont(context, fontProvider1)); unicodeFontData.addAll(handleFont(context, fontProvider1));
} }
} else if (fontProvider instanceof UnihexFontProvider unihexFontProvider) { } else if (fontProvider instanceof UnihexFontProvider unihexFontProvider) {
@@ -251,12 +245,14 @@ public class FontTransformer implements TextureTransformer {
} }
private void transformDefault8(@NotNull TransformContext context) throws IOException { private void transformDefault8(@NotNull TransformContext context) throws IOException {
// Don't attempt to write default8 if we have no data to pull from, otherwise it's vanilla to vanilla
if ( if (
!context.isTexturePresent(Key.key(Key.MINECRAFT_NAMESPACE, "font/ascii.png")) && !context.isTexturePresent(Key.key(Key.MINECRAFT_NAMESPACE, "font/ascii.png")) &&
!context.isTexturePresent(Key.key(Key.MINECRAFT_NAMESPACE, "font/accented.png")) && !context.isTexturePresent(Key.key(Key.MINECRAFT_NAMESPACE, "font/accented.png")) &&
!context.isTexturePresent(Key.key(Key.MINECRAFT_NAMESPACE, "font/nonlatin_european.png")) !context.isTexturePresent(Key.key(Key.MINECRAFT_NAMESPACE, "font/nonlatin_european.png"))
) return; ) return;
// Store the java images to prevent constant image reading
Map<String, BufferedImage> imgs = new HashMap<>(); Map<String, BufferedImage> imgs = new HashMap<>();
Map<String, Integer> scales = new HashMap<>(); Map<String, Integer> scales = new HashMap<>();
@@ -281,9 +277,11 @@ public class FontTransformer implements TextureTransformer {
scales.put("nonlatin_european", image.getWidth() / 128); scales.put("nonlatin_european", image.getWidth() / 128);
} }
// Use ASCII as a base, since bedrock's default8 has the same character size as ASCII
int charWidth = scales.get("ascii") * 8; int charWidth = scales.get("ascii") * 8;
int charHeight = scales.get("ascii") * 8; int charHeight = scales.get("ascii") * 8;
// default8 is 16 by 16 characters in size
BufferedImage bedrockImage = new BufferedImage(16 * charWidth, 16 * charHeight, BufferedImage.TYPE_INT_ARGB); BufferedImage bedrockImage = new BufferedImage(16 * charWidth, 16 * charHeight, BufferedImage.TYPE_INT_ARGB);
Graphics g = bedrockImage.getGraphics(); Graphics g = bedrockImage.getGraphics();
@@ -291,6 +289,7 @@ public class FontTransformer implements TextureTransformer {
for (FontMapping fontMapping : DEFAULT8_MAPPINGS) { for (FontMapping fontMapping : DEFAULT8_MAPPINGS) {
FontData fontData = FONT_DATA.get(fontMapping.javaTexture); FontData fontData = FONT_DATA.get(fontMapping.javaTexture);
// Determines the position in the java image, accounting for scale
int realCharX = fontData.charSizeX * scales.get(fontMapping.javaTexture); int realCharX = fontData.charSizeX * scales.get(fontMapping.javaTexture);
int realCharY = fontData.charSizeY * scales.get(fontMapping.javaTexture); int realCharY = fontData.charSizeY * scales.get(fontMapping.javaTexture);
@@ -476,9 +475,96 @@ public class FontTransformer implements TextureTransformer {
} }
} }
private record UnicodeFontData(Key filename, char character, int x, int y, int width, int height, int spaces) { // The base for our unicode fonts
public UnicodeFontData(Key filename, char character, int x, int y, int width, int height) { private interface UnicodeFontData {
this(filename, character, x, y, width, height, 0); BufferedImage readJavaImage(Map<Key, BufferedImage> imageCache);
default boolean shouldRead() {
return true;
}
default void computeCache(TransformContext context, Map<Key, BufferedImage> imageCache) {} // No caching if not needed
char character();
int x();
int y();
int width();
int height();
default FontData fontData() {
return DEFAULT_FONT_DATA;
}
}
// Bitmap implementation of our fonts, the simplest to read
private record BitMapFontData(Key textureName, char character, int x, int y, int width, int height) implements UnicodeFontData {
@Override
public BufferedImage readJavaImage(Map<Key, BufferedImage> imageCache) {
return imageCache.get(textureName);
}
@Override
public void computeCache(TransformContext context, Map<Key, BufferedImage> imageCache) {
Texture texture = context.pollOrPeekVanilla(textureName);
if (texture != null) {
try {
imageCache.put(
textureName,
ImageUtil.ensure32BitImage(
ImageIO.read(new ByteArrayInputStream(texture.data().toByteArray()))
)
);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
@Override
public FontData fontData() {
return FONT_DATA.getOrDefault(textureName.value().substring(5, textureName.value().length() - 4), DEFAULT_FONT_DATA);
}
}
// The simplest font, *empty*
private record SpaceFontData(char character, int spaces) implements UnicodeFontData {
@Override
public BufferedImage readJavaImage(Map<Key, BufferedImage> imageCache) {
BufferedImage javaImage = new BufferedImage(spaces, 1, BufferedImage.TYPE_INT_ARGB);
Graphics javaGraphics = javaImage.getGraphics();
javaGraphics.setColor(new Color(255, 255, 255, 1)); // Just so bedrock knows the width of our character, not noticable to the human eye
javaGraphics.drawRect(0, 0, spaces, 1);
return javaImage;
}
@Override
public boolean shouldRead() {
return spaces > 0;
}
@Override
public int x() {
return 0;
}
@Override
public int y() {
return 0;
}
@Override
public int width() {
return spaces;
}
@Override
public int height() {
return 1;
} }
} }
} }