Skip to content

Commit

Permalink
Implement custom fonts (#2) (3.4.0)
Browse files Browse the repository at this point in the history
Co-authored-by: Lukas Schulte Pelkum <kbrt@protonmail.com>
  • Loading branch information
cerus and lus committed Sep 4, 2022
1 parent ef42d32 commit e3c75c2
Show file tree
Hide file tree
Showing 15 changed files with 420 additions and 86 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ See [FAQ](#FAQ)
<dependency>
<groupId>dev.cerus.maps</groupId>
<artifactId>common</artifactId>
<version>3.3.0</version>
<version>3.4.0</version>
<scope>provided</scope> <!-- "provided" if the maps plugin is on the server, "compile" if not -->
</dependency>

Expand All @@ -59,7 +59,7 @@ See [FAQ](#FAQ)
<dependency>
<groupId>dev.cerus.maps</groupId>
<artifactId>plugin</artifactId>
<version>3.3.0</version>
<version>3.4.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
Expand Down
2 changes: 1 addition & 1 deletion bukkit-16_R3/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<parent>
<artifactId>parent</artifactId>
<groupId>dev.cerus.maps</groupId>
<version>3.3.0</version>
<version>3.4.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>

Expand Down
2 changes: 1 addition & 1 deletion bukkit-17_R1/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<parent>
<artifactId>parent</artifactId>
<groupId>dev.cerus.maps</groupId>
<version>3.3.0</version>
<version>3.4.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>

Expand Down
2 changes: 1 addition & 1 deletion bukkit-18_R1/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<parent>
<artifactId>parent</artifactId>
<groupId>dev.cerus.maps</groupId>
<version>3.3.0</version>
<version>3.4.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>

Expand Down
2 changes: 1 addition & 1 deletion bukkit-18_R2/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<parent>
<artifactId>parent</artifactId>
<groupId>dev.cerus.maps</groupId>
<version>3.3.0</version>
<version>3.4.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>

Expand Down
2 changes: 1 addition & 1 deletion bukkit-19_R1/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<parent>
<artifactId>parent</artifactId>
<groupId>dev.cerus.maps</groupId>
<version>3.3.0</version>
<version>3.4.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>

Expand Down
2 changes: 1 addition & 1 deletion common/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<parent>
<artifactId>parent</artifactId>
<groupId>dev.cerus.maps</groupId>
<version>3.3.0</version>
<version>3.4.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>

Expand Down
87 changes: 67 additions & 20 deletions common/src/main/java/dev/cerus/maps/api/font/FontConverter.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,66 +7,113 @@
import java.awt.image.BufferedImage;
import java.util.List;
import java.util.stream.IntStream;
import org.bukkit.map.MapFont;

/**
* Converts regular Java fonts into MapFonts
*/
public class FontConverter {

public static final List<Character> ASCII = List.copyOf(IntStream.range(26, 127)
/**
* All Ascii chars
*/
public static final String ASCII = IntStream.range(26, 127)
.mapToObj(v -> (char) v)
.toList());
public static final List<Character> UNICODE = List.copyOf(IntStream.range(0, Character.MAX_VALUE)
.filter(value -> Character.isDefined((char) value))
.mapToObj(v -> (char) v)
.toList());
.collect(StringBuilder::new, StringBuilder::append, StringBuilder::append)
.toString();

/**
* All German umlauts
*/
public static final String UMLAUTS = "ÄäÖöÜü";

/**
* Sharp s ("Eszett", "scharfes S")
*/
public static final String SHARP_S = "ẞß";

private FontConverter() {
throw new UnsupportedOperationException();
}

public static MapFont convert(final Font font, final List<Character> charsToCheck) {
final List<Character> supportedChars = charsToCheck.stream()
/**
* Convert the specified font into a MapFont. Since we can't get a list of supported codepoints from
* the font object you have to specify each individual character that you want us to convert.
* <p>
* Unsupported codepoints will be ignored.
*
* @param font The Java font
* @param textToCheck The characters that you want us to convert
*
* @return The converted font
*/
public static MapFont convert(final Font font, final String textToCheck) {
final List<Integer> supportedCodepoints = textToCheck.codePoints()
.filter(font::canDisplay)
.boxed()
.toList();

final MapFont mapFont = new MapFont();
for (final char c : supportedChars) {
final BufferedImage img = toImage(font, c);
for (final int cp : supportedCodepoints) {
final BufferedImage img = toImage(font, cp);
if (img == null) {
continue;
}
final MapFont.CharacterSprite sprite = makeSprite(img);
mapFont.setChar(c, sprite);
final Sprite sprite = makeSprite(img);
mapFont.set(cp, sprite);
}
return mapFont;
}

private static BufferedImage toImage(final Font font, final char c) {
/**
* Draw a single codepoint on an image
*
* @param font The parent font
* @param cp The codepoint
*
* @return The image
*/
private static BufferedImage toImage(final Font font, final int cp) {
// Get bounds of the codepoint (why does this have to be so complicated)
BufferedImage image = newImg(1, 1);
Graphics2D graphics = image.createGraphics();
final Rectangle2D bounds = font.getStringBounds(String.valueOf(c), graphics.getFontMetrics().getFontRenderContext());
final Rectangle2D bounds = font.getStringBounds(new String(Character.toChars(cp)), graphics.getFontMetrics().getFontRenderContext());
graphics.dispose();
if (bounds.getWidth() <= 0 || bounds.getHeight() <= 0) {
return null;
}

// Create image with correct size
image = newImg((int) Math.ceil(bounds.getWidth()), (int) Math.ceil(bounds.getHeight()));
graphics = image.createGraphics();
graphics.setColor(new Color(0, 0, 0, 0));
graphics.fillRect(0, 0, image.getWidth(), image.getHeight());
graphics.setColor(Color.BLACK);
graphics.setFont(font);
graphics.drawString(String.valueOf(c), 0, graphics.getFontMetrics().getAscent());
graphics.drawString(new String(Character.toChars(cp)), 0, graphics.getFontMetrics().getAscent());
graphics.dispose();
return image;
}

/**
* Create a new image with the specified bounds
*
* @param w The width of the image
* @param h The height of the image
*
* @return A new image
*/
private static BufferedImage newImg(final int w, final int h) {
return new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
}

private static MapFont.CharacterSprite makeSprite(final BufferedImage image) {
/**
* Convert an image into a MapFont sprite
*
* @param image The image
*
* @return The sprite
*/
private static Sprite makeSprite(final BufferedImage image) {
final boolean[] data = new boolean[image.getWidth() * image.getHeight()];
final MapFont.CharacterSprite sprite = new MapFont.CharacterSprite(image.getWidth(), image.getHeight(), data);
final Sprite sprite = new Sprite(image.getWidth(), image.getHeight(), data);

for (int x = 0; x < image.getWidth(); x++) {
for (int y = 0; y < image.getHeight(); y++) {
Expand Down
163 changes: 163 additions & 0 deletions common/src/main/java/dev/cerus/maps/api/font/MapFont.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
package dev.cerus.maps.api.font;

import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.bukkit.map.MinecraftFont;

/**
* Represents a font
*/
public class MapFont {

public static final MapFont MINECRAFT_FONT = fromBukkit(MinecraftFont.Font);

private final Map<Integer, Sprite> codePointMap = new HashMap<>();
private int maxHeight;

/**
* Convert Bukkit font to maps font
*
* @param bukkitFont The Bukkit font
*
* @return The converted font
*/
public static MapFont fromBukkit(final org.bukkit.map.MapFont bukkitFont) {
final MapFont mapFont = new MapFont();
try {
final Field charsField = org.bukkit.map.MapFont.class.getDeclaredField("chars");
charsField.setAccessible(true);
final Map<Character, org.bukkit.map.MapFont.CharacterSprite> spriteMap
= (Map<Character, org.bukkit.map.MapFont.CharacterSprite>) charsField.get(bukkitFont);
spriteMap.forEach((character, characterSprite) ->
mapFont.set(String.valueOf(character).codePointAt(0), Sprite.fromBukkit(characterSprite)));
} catch (final NoSuchFieldException | IllegalAccessException e) {
throw new RuntimeException("Failed to convert Bukkit font into maps font", e);
}
return mapFont;
}

/**
* Register a codepoint
*
* @param codePoint The codepoint
* @param sprite The sprite
*/
public void set(final int codePoint, final Sprite sprite) {
this.codePointMap.put(codePoint, sprite);
if (sprite.getHeight() > this.maxHeight) {
this.maxHeight = sprite.getHeight();
}
}

/**
* Get a registered sprite
*
* @param codePoint The codepoint registered to the sprite
*
* @return The sprite
*
* @throws IllegalArgumentException if the codepoint was not registered
*/
public Sprite get(final int codePoint) {
if (!this.isValid(codePoint)) {
throw new IllegalArgumentException("Invalid code point");
}
return this.codePointMap.get(codePoint);
}

/**
* Get all sprites for the specified text
*
* @param text The text
*
* @return A list of sprites
*
* @throws IllegalArgumentException if the text is invalid
*/
public List<Sprite> getSprites(final String text) {
if (!this.isValid(text)) {
throw new IllegalArgumentException("Unsupported chars");
}
return text.codePoints().mapToObj(this::get).toList();
}

/**
* Get the width of a string
*
* @param text The string
*
* @return The width of the string
*
* @throws IllegalArgumentException if the text is invalid
*/
public int getWidth(final String text) {
if (text == null || text.length() == 0 || !this.isValid(text)) {
throw new IllegalArgumentException("Invalid text");
}
return text.codePoints().map(cp -> this.codePointMap.get(cp).getWidth()).sum() + text.length() - 1;
}

/**
* Get the maximum height of this font
*
* @return The max height
*/
public int getHeight() {
return this.getHeight(null);
}

/**
* Get the height for a string
*
* @param text The string
*
* @return The height of the string
*
* @throws IllegalArgumentException if the text is invalid
*/
public int getHeight(final String text) {
if (text == null) {
return this.maxHeight;
}
if (!this.isValid(text)) {
throw new IllegalArgumentException("Unsupported characters");
}
return text.codePoints().map(cp -> this.codePointMap.get(cp).getHeight()).max().orElse(0);
}

/**
* Checks if a codepoint was registered
*
* @param codePoint The codepoint
*
* @return True if valid
*/
public boolean isValid(final int codePoint) {
return this.codePointMap.containsKey(codePoint);
}

/**
* Checks if a character was registered
*
* @param c The character
*
* @return True if valid
*/
public boolean isValid(final char c) {
return this.isValid(String.valueOf(c).codePointAt(0));
}

/**
* Checks if a string is valid
*
* @param text The string
*
* @return True if valid
*/
public boolean isValid(final String text) {
return text.codePoints().allMatch(this::isValid);
}

}
Loading

0 comments on commit e3c75c2

Please sign in to comment.