mirror of
https://github.com/KAMKEEL/CustomNPC-Plus.git
synced 2026-11-13 05:27:37 +00:00
Improved font rendering for ITALIC and BOLD to remove glyph artifacts
This commit is contained in:
1 parent
f8a7241747
commit
f6f19c0e48
2 files changed
+93
-10
No files matched your search
@@ -78,13 +78,13 @@ public class GlyphCache {
|
||||
private static final int GLYPH_PADDING = 2;
|
||||
|
||||
/**
|
||||
* Spacing (in pixels) inserted between adjacent glyphs in the temporary string image during rasterization.
|
||||
* Each glyph is shifted right by GLYPH_SPACING * glyphIndex to prevent kerning from merging adjacent glyphs.
|
||||
* This must be >= 2 * GLYPH_PADDING + 1 so that when padded bounding boxes are extracted for adjacent glyphs,
|
||||
* they never overlap. Without sufficient spacing, bold/italic glyphs (which have wider ink bounds) can bleed
|
||||
* fragments of neighbouring glyphs into each other's cached texture regions.
|
||||
* Minimum spacing (in pixels) inserted between adjacent glyphs in the temporary string image during
|
||||
* rasterization. Each glyph is shifted right by glyphSpacing * glyphIndex to prevent kerning from
|
||||
* merging adjacent glyphs. This base value is used for plain/bold styles. For italic fonts, the
|
||||
* spacing is increased dynamically by {@link #computeGlyphSpacing(Font)} to account for the
|
||||
* horizontal overhang caused by the italic slant.
|
||||
*/
|
||||
private static final int GLYPH_SPACING = 2 * GLYPH_PADDING + 1;
|
||||
private static final int GLYPH_SPACING_BASE = 2 * GLYPH_PADDING + 1;
|
||||
|
||||
/**
|
||||
* Transparent (alpha zero) white background color for use with BufferedImage.clearRect().
|
||||
@@ -158,6 +158,36 @@ public class GlyphCache {
|
||||
return GLYPH_PADDING;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the inter-glyph spacing needed for a given font when rasterizing into the temporary
|
||||
* string image. For plain and bold fonts, the base spacing (2*GLYPH_PADDING+1) is sufficient.
|
||||
* For italic fonts, the slant causes the top of each glyph to lean horizontally, and at large
|
||||
* raster sizes (e.g. 54pt at scale 3) this overhang can be 8+ pixels — far exceeding the base
|
||||
* spacing's 1-pixel effective gap. Without adequate spacing, adjacent italic glyphs' rendered
|
||||
* pixels overlap in the string image, and narrow glyphs like 'I'/'i' capture neighboring
|
||||
* fragments as visible dot artifacts in their cached texture.
|
||||
*
|
||||
* The italic overhang is computed as: lineHeight * |italicAngle|. Java2D's standard italic
|
||||
* angle is ~0.2 (approx 12°). A safety factor of 1.5× is applied since antialiasing can
|
||||
* produce faint alpha slightly beyond the geometric slant boundary.
|
||||
*/
|
||||
private int computeGlyphSpacing(Font font) {
|
||||
float italicAngle = font.getItalicAngle();
|
||||
if (italicAngle == 0 && font.isItalic()) {
|
||||
italicAngle = -0.2f; // Java2D synthetic italic default shear
|
||||
}
|
||||
|
||||
if (italicAngle == 0) {
|
||||
return GLYPH_SPACING_BASE;
|
||||
}
|
||||
|
||||
java.awt.font.LineMetrics lm = font.getLineMetrics("I", fontRenderContext);
|
||||
float lineHeight = lm.getAscent() + lm.getDescent();
|
||||
int italicOverhang = (int) Math.ceil(lineHeight * Math.abs(italicAngle) * 1.5f);
|
||||
|
||||
return Math.max(GLYPH_SPACING_BASE, 2 * GLYPH_PADDING + italicOverhang + 1);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Intermediate data array for use with textureImage.getRgb().
|
||||
@@ -481,6 +511,7 @@ public class GlyphCache {
|
||||
int numGlyphs = vector.getNumGlyphs(); /* Length of the GlyphVector */
|
||||
Rectangle dirty = null; /* Total area within texture that needs to be updated with glTexSubImage2D() */
|
||||
boolean vectorRendered = false; /* True if entire GlyphVector was rendered into stringImage */
|
||||
int glyphSpacing = computeGlyphSpacing(font);
|
||||
|
||||
for (int index = 0; index < numGlyphs; index++) {
|
||||
int charIndex = start + vector.getGlyphCharIndex(index);
|
||||
@@ -497,6 +528,20 @@ public class GlyphCache {
|
||||
continue;
|
||||
}
|
||||
|
||||
/*
|
||||
* Skip whitespace glyphs that have no visible ink (spaces, tabs, etc.). These characters
|
||||
* contribute only advance width, not visible pixels. Without this check, GLYPH_PADDING
|
||||
* expansion would create a small non-zero rect for the empty glyph, and the blit from
|
||||
* stringImage could capture stray antialiased fringe pixels from neighboring glyphs,
|
||||
* causing whitespace to render as visible dots.
|
||||
*/
|
||||
{
|
||||
Rectangle glyphBounds = vector.getGlyphPixelBounds(index, fontRenderContext, 0, 0);
|
||||
if (glyphBounds.width == 0 || glyphBounds.height == 0) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* The only way to get glyph shapes with font hinting is to draw the entire glyph vector into a
|
||||
* temporary BufferedImage, and then bit blit the individual glyphs based on their bounding boxes
|
||||
@@ -515,7 +560,7 @@ public class GlyphCache {
|
||||
*/
|
||||
for (int i = 0; i < numGlyphs; i++) {
|
||||
Point2D pos = vector.getGlyphPosition(i);
|
||||
pos.setLocation(pos.getX() + GLYPH_SPACING * i, pos.getY());
|
||||
pos.setLocation(pos.getX() + glyphSpacing * i, pos.getY());
|
||||
vector.setGlyphPosition(i, pos);
|
||||
}
|
||||
|
||||
@@ -537,8 +582,16 @@ public class GlyphCache {
|
||||
allocateStringImage(width, height);
|
||||
}
|
||||
|
||||
/* Erase the region where the string will get drawn (expanded for hinting overflow) */
|
||||
stringGraphics.clearRect(0, 0, vectorBounds.width, vectorBounds.height);
|
||||
/*
|
||||
* Clear the ENTIRE stringImage, not just the vectorBounds area. The stringImage is reused
|
||||
* across calls and may be larger than the current vectorBounds. Individual glyph pixel bounds
|
||||
* (from getGlyphPixelBounds()) can differ slightly from the aggregate vector bounds
|
||||
* (from getPixelBounds()) due to integer rounding. After GLYPH_PADDING expansion, a glyph's
|
||||
* padded rect could extend 1-2 pixels beyond the vectorBounds area, capturing stale pixels
|
||||
* from a previous render. This caused tiny dot artifacts above capital letters whose ink
|
||||
* extends to the top of the line bounds. Clearing the full image eliminates this class of bugs.
|
||||
*/
|
||||
stringGraphics.clearRect(0, 0, stringImage.getWidth(), stringImage.getHeight());
|
||||
|
||||
/* Draw string with opaque white color and baseline adjustment so the upper-left corner of the image is at (0,0) */
|
||||
stringGraphics.drawGlyphVector(vector, -vectorBounds.x, -vectorBounds.y);
|
||||
@@ -555,6 +608,30 @@ public class GlyphCache {
|
||||
Rectangle rect = vector.getGlyphPixelBounds(index, fontRenderContext, -vectorBounds.x, -vectorBounds.y);
|
||||
rect.grow(GLYPH_PADDING, GLYPH_PADDING);
|
||||
|
||||
/*
|
||||
* Clamp the padded rect to the stringImage bounds. Due to rounding differences between
|
||||
* getPixelBounds() and getGlyphPixelBounds(), a padded glyph rect can occasionally extend
|
||||
* beyond the image edge. Reading pixels outside the image would cause an exception or
|
||||
* capture garbage data.
|
||||
*/
|
||||
if (rect.x < 0) {
|
||||
rect.width += rect.x; // shrink width by the overshoot
|
||||
rect.x = 0;
|
||||
}
|
||||
if (rect.y < 0) {
|
||||
rect.height += rect.y; // shrink height by the overshoot
|
||||
rect.y = 0;
|
||||
}
|
||||
if (rect.x + rect.width > stringImage.getWidth()) {
|
||||
rect.width = stringImage.getWidth() - rect.x;
|
||||
}
|
||||
if (rect.y + rect.height > stringImage.getHeight()) {
|
||||
rect.height = stringImage.getHeight() - rect.y;
|
||||
}
|
||||
if (rect.width <= 0 || rect.height <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/* If the current line in cache image is full, then advance to the next line */
|
||||
if (cachePosX + rect.width + GLYPH_BORDER > TEXTURE_WIDTH) {
|
||||
cachePosX = GLYPH_BORDER;
|
||||
|
||||
@@ -611,6 +611,12 @@ public class StringCache {
|
||||
/* Select the current glyph's texture information and horizontal layout position within this string */
|
||||
Glyph glyph = entry.glyphs[glyphIndex];
|
||||
GlyphCache.Entry texture = glyph.texture;
|
||||
|
||||
/* Skip glyphs with no cached texture (whitespace characters like spaces and tabs) */
|
||||
if (texture == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int glyphX = glyph.x + glyph.inkOffsetX;
|
||||
|
||||
/*
|
||||
@@ -698,7 +704,7 @@ public class StringCache {
|
||||
Glyph glyph = entry.glyphs[glyphIndex];
|
||||
|
||||
/* The strike/underlines are drawn beyond the glyph's width to include the extra space between glyphs */
|
||||
int glyphSpace = glyph.advance - glyph.texture.width;
|
||||
int glyphSpace = glyph.texture != null ? glyph.advance - glyph.texture.width : 0;
|
||||
|
||||
/* Draw underline under glyph if the style is enabled */
|
||||
if ((renderStyle & ColorCode.UNDERLINE) != 0) {
|
||||
|
||||
Reference in new issue
Block a user