Skip to content

Enhance BitmapFont and BitmapText Javadoc #2534

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
197 changes: 140 additions & 57 deletions jme3-core/src/main/java/com/jme3/font/BitmapFont.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2009-2021 jMonkeyEngine
* Copyright (c) 2009-2025 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
Expand Down Expand Up @@ -31,14 +31,22 @@
*/
package com.jme3.font;

import com.jme3.export.*;
import com.jme3.export.InputCapsule;
import com.jme3.export.JmeExporter;
import com.jme3.export.JmeImporter;
import com.jme3.export.OutputCapsule;
import com.jme3.export.Savable;
import com.jme3.material.Material;

import java.io.IOException;

/**
* Represents a font within jME that is generated with the AngelCode Bitmap Font Generator
* Represents a font loaded from a bitmap font definition
* (e.g., generated by <a href="https://libgdx.com/wiki/tools/hiero">AngelCode Bitmap Font Generator</a>).
* It manages character sets, font pages (textures), and provides utilities for text measurement and rendering.
*
* @author dhdd
* @author Yonghoon
*/
public class BitmapFont implements Savable {

Expand Down Expand Up @@ -87,61 +95,112 @@ public enum VAlign {
Bottom
}

// The character set containing definitions for each character (glyph) in the font.
private BitmapCharacterSet charSet;
// An array of materials, where each material corresponds to a font page (texture).
private Material[] pages;
// Indicates whether this font is designed for right-to-left (RTL) text rendering.
private boolean rightToLeft = false;
// For cursive bitmap fonts in which letter shape is determined by the adjacent glyphs.
private GlyphParser glyphParser;

/**
* @return true, if this is a right-to-left font, otherwise it will return false.
* Creates a new instance of `BitmapFont`.
* This constructor is primarily used for deserialization.
*/
public boolean isRightToLeft() {
return rightToLeft;
public BitmapFont() {
}

/**
* Specify if this is a right-to-left font. By default it is set to false.
* This can be "overwritten" in the BitmapText constructor.
* Creates a new {@link BitmapText} instance initialized with this font.
* The label's size will be set to the font's rendered size, and its text content
* will be set to the provided string.
*
* @param rightToLeft true &rarr; right-to-left, false &rarr; left-to-right
* (default=false)
* @param content The initial text content for the label.
* @return A new {@link BitmapText} instance.
*/
public void setRightToLeft(boolean rightToLeft) {
this.rightToLeft = rightToLeft;
}

public BitmapFont() {
}

public BitmapText createLabel(String content) {
BitmapText label = new BitmapText(this);
label.setSize(getCharSet().getRenderedSize());
label.setText(content);
return label;
}

/**
* Checks if this font is configured for right-to-left (RTL) text rendering.
*
* @return true if this is a right-to-left font, otherwise false (default is left-to-right).
*/
public boolean isRightToLeft() {
return rightToLeft;
}

/**
* Specifies whether this font should be rendered as right-to-left (RTL).
* By default, it is set to false (left-to-right).
*
* @param rightToLeft true to enable right-to-left rendering; false for left-to-right.
*/
public void setRightToLeft(boolean rightToLeft) {
this.rightToLeft = rightToLeft;
}

/**
* Returns the preferred size of the font, which is typically its rendered size.
*
* @return The preferred size of the font in font units.
*/
public float getPreferredSize() {
return getCharSet().getRenderedSize();
}

/**
* Sets the character set for this font. The character set contains
* information about individual glyphs, their positions, and kerning data.
*
* @param charSet The {@link BitmapCharacterSet} to associate with this font.
*/
public void setCharSet(BitmapCharacterSet charSet) {
this.charSet = charSet;
}

/**
* Sets the array of materials (font pages) for this font. Each material
* corresponds to a texture page containing character bitmaps.
* The character set's page size is also updated based on the number of pages.
*
* @param pages An array of {@link Material} objects representing the font pages.
*/
public void setPages(Material[] pages) {
this.pages = pages;
charSet.setPageSize(pages.length);
}

/**
* Retrieves a specific font page material by its index.
*
* @param index The index of the font page to retrieve.
* @return The {@link Material} for the specified font page.
* @throws IndexOutOfBoundsException if the index is out of bounds.
*/
public Material getPage(int index) {
return pages[index];
}

/**
* Returns the total number of font pages (materials) associated with this font.
*
* @return The number of font pages.
*/
public int getPageSize() {
return pages.length;
}

/**
* Retrieves the character set associated with this font.
*
* @return The {@link BitmapCharacterSet} of this font.
*/
public BitmapCharacterSet getCharSet() {
return charSet;
}
Expand Down Expand Up @@ -192,26 +251,19 @@ private int findKerningAmount(int newLineLastChar, int nextChar) {
return c.getKerning(nextChar);
}

@Override
public void write(JmeExporter ex) throws IOException {
OutputCapsule oc = ex.getCapsule(this);
oc.write(charSet, "charSet", null);
oc.write(pages, "pages", null);
oc.write(rightToLeft, "rightToLeft", false);
oc.write(glyphParser, "glyphParser", null);
}

@Override
public void read(JmeImporter im) throws IOException {
InputCapsule ic = im.getCapsule(this);
charSet = (BitmapCharacterSet) ic.readSavable("charSet", null);
Savable[] pagesSavable = ic.readSavableArray("pages", null);
pages = new Material[pagesSavable.length];
System.arraycopy(pagesSavable, 0, pages, 0, pages.length);
rightToLeft = ic.readBoolean("rightToLeft", false);
glyphParser = (GlyphParser) ic.readSavable("glyphParser", null);
}

/**
* Calculates the width of the given text in font units.
* This method accounts for character advances, kerning, and line breaks.
* It also attempts to skip custom color tags (e.g., "\#RRGGBB#" or "\#RRGGBBAA#")
* based on a specific format.
* <p>
* Note: This method calculates width in "font units" where the font's
* {@link BitmapCharacterSet#getRenderedSize() rendered size} is the base.
* Actual pixel scaling for display is typically handled by {@link BitmapText}.
*
* @param text The text to measure.
* @return The maximum line width of the text in font units.
*/
public float getLineWidth(CharSequence text) {
// This method will probably always be a bit of a maintenance
// nightmare since it bases its calculation on a different
Expand Down Expand Up @@ -252,29 +304,36 @@ public float getLineWidth(CharSequence text) {
boolean firstCharOfLine = true;
// float sizeScale = (float) block.getSize() / charSet.getRenderedSize();
float sizeScale = 1f;
CharSequence characters = glyphParser != null ? glyphParser.parse(text) : text;

for (int i = 0; i < characters.length(); i++) {
char theChar = characters.charAt(i);
if (theChar == '\n') {
// Use GlyphParser if available for complex script shaping (e.g., cursive fonts).
CharSequence processedText = glyphParser != null ? glyphParser.parse(text) : text;

for (int i = 0; i < processedText.length(); i++) {
char currChar = processedText.charAt(i);
if (currChar == '\n') {
maxLineWidth = Math.max(maxLineWidth, lineWidth);
lineWidth = 0f;
firstCharOfLine = true;
continue;
}
BitmapCharacter c = charSet.getCharacter(theChar);
BitmapCharacter c = charSet.getCharacter(currChar);
if (c != null) {
if (theChar == '\\' && i < characters.length() - 1 && characters.charAt(i + 1) == '#') {
if (i + 5 < characters.length() && characters.charAt(i + 5) == '#') {
// Custom color tag skipping logic:
// Assumes tags are of the form `\#RRGGBB#` (9 chars total) or `\#RRGGBBAA#` (12 chars total).
if (currChar == '\\' && i < processedText.length() - 1 && processedText.charAt(i + 1) == '#') {
// Check for `\#XXXXX#` (6 chars after '\', including final '#')
if (i + 5 < processedText.length() && processedText.charAt(i + 5) == '#') {
i += 5;
continue;
} else if (i + 8 < characters.length() && characters.charAt(i + 8) == '#') {
}
// Check for `\#XXXXXXXX#` (9 chars after '\', including final '#')
else if (i + 8 < processedText.length() && processedText.charAt(i + 8) == '#') {
i += 8;
continue;
}
}
if (!firstCharOfLine) {
lineWidth += findKerningAmount(lastChar, theChar) * sizeScale;
lineWidth += findKerningAmount(lastChar, currChar) * sizeScale;
} else {
if (rightToLeft) {
// Ignore offset, so it will be compatible with BitmapText.getLineWidth().
Expand All @@ -292,7 +351,7 @@ public float getLineWidth(CharSequence text) {
// If this is the last character of a line, then we really should
// have only added its width. The advance may include extra spacing
// that we don't care about.
if (i == characters.length() - 1 || characters.charAt(i + 1) == '\n') {
if (i == processedText.length() - 1 || processedText.charAt(i + 1) == '\n') {
if (rightToLeft) {
// In RTL text we move the letter x0 by its xAdvance, so
// we should add it to lineWidth.
Expand All @@ -315,30 +374,54 @@ public float getLineWidth(CharSequence text) {
return Math.max(maxLineWidth, lineWidth);
}


/**
* Merge two fonts.
* If two font have the same style, merge will fail.
* @param newFont Style must be assigned to this.
* author: Yonghoon
* Merges another {@link BitmapFont} into this one.
* This operation combines the character sets and font pages.
* If both fonts contain the same style, the merge will fail and throw a RuntimeException.
*
* @param newFont The {@link BitmapFont} to merge into this one. It must have a style assigned.
*/
public void merge(BitmapFont newFont) {
charSet.merge(newFont.charSet);
final int size1 = this.pages.length;
final int size2 = newFont.pages.length;

Material[] tmp = new Material[size1+size2];
Material[] tmp = new Material[size1 + size2];
System.arraycopy(this.pages, 0, tmp, 0, size1);
System.arraycopy(newFont.pages, 0, tmp, size1, size2);

this.pages = tmp;

// this.pages = Arrays.copyOf(this.pages, size1+size2);
// System.arraycopy(newFont.pages, 0, this.pages, size1, size2);
}

/**
* Sets the style for the font's character set.
* This method is typically used when a font file contains only one style
* but needs to be assigned a specific style identifier for merging
* with other multi-style fonts.
*
* @param style The integer style identifier to set.
*/
public void setStyle(int style) {
charSet.setStyle(style);
}

}
@Override
public void write(JmeExporter ex) throws IOException {
OutputCapsule oc = ex.getCapsule(this);
oc.write(charSet, "charSet", null);
oc.write(pages, "pages", null);
oc.write(rightToLeft, "rightToLeft", false);
oc.write(glyphParser, "glyphParser", null);
}

@Override
public void read(JmeImporter im) throws IOException {
InputCapsule ic = im.getCapsule(this);
charSet = (BitmapCharacterSet) ic.readSavable("charSet", null);
Savable[] pagesSavable = ic.readSavableArray("pages", null);
pages = new Material[pagesSavable.length];
System.arraycopy(pagesSavable, 0, pages, 0, pages.length);
rightToLeft = ic.readBoolean("rightToLeft", false);
glyphParser = (GlyphParser) ic.readSavable("glyphParser", null);
}
}
Loading