fix: Use fixed-point fractional x-advance and kerning for better text layout (#1168)
## Summary **What is the goal of this PR?** Hopefully fixes #1182. _Note: I think letterforms got a "heavier" appearance after #1098, which makes this more noticeable. The current version of this PR reverts the change to add `--force-autohint` for Bookerly, which to me seems to bring the font back to a more aesthetic and consistent weight._ #### Problem Character spacing was uneven in certain words. The word "drew" in Bookerly was the clearest example: a visible gap between `d` and `r`, while `e` and `w` appeared tightly condensed. The root cause was twofold: 1. **Integer-only glyph advances.** `advanceX` was stored as a `uint8_t` of whole pixels, sourced from FreeType's hinted `advance.x` (which grid-fits to integers). A glyph whose true advance is 15.56px was stored as 16px -- an error of +0.44px per character that compounds across a line. 2. **Floor-rounded kerning.** Kern adjustments were converted with `math.floor()`, which systematically over-tightened negative kerns. A kern of -0.3px became -1px -- a 0.7px over-correction that visibly closed gaps. Combined, these produced the classic symptom: some pairs too wide, others too tight, with the imbalance varying per word. #### Solution: fixed-point accumulation with 1/16-pixel resolution, for sub-pixel precision during text layout All font metrics now use a "fixed-point 4" format -- 4 fractional bits giving 1/16-pixel (0.0625px) resolution. This is implemented with plain integer arithmetic (shifts and adds), requiring no floating-point on the ESP32. **How it works:** A value like 15.56px is stored as the integer `249`: ``` 249 = 15 * 16 + 9 (where 9/16 = 0.5625, closest to 0.56) ``` Two storage widths share the same 4 fractional bits: | Field | Type | Format | Range | Use | |-------|------|--------|-------|-----| | `advanceX` | `uint16_t` | 12.4 | 0 -- 4095.9375 px | Glyph advance width | | `kernMatrix` | `int8_t` | 4.4 | -8.0 -- +7.9375 px | Kerning adjustment | Because both have 4 fractional bits, they add directly into a single `int32_t` accumulator during layout. The accumulator is only snapped to the nearest whole pixel at the moment each glyph is rendered: ```cpp int32_t xFP = fp4::fromPixel(startX); // pixel to 12.4: startX << 4 for each character: xFP += kernFP; // add 4.4 kern (sign-extends into int32_t) int xPx = fp4::toPixel(xFP); // snap to nearest pixel: (xFP + 8) >> 4 render glyph at xPx; xFP += glyph->advanceX; // add 12.4 advance ``` Fractional remainders carry forward indefinitely. Rounding errors stay below +/- 0.5px and never compound. #### Concrete example: "drew" in Bookerly **Before** (integer advances, floor-rounded kerning): | Char | Advance | Kern | Cursor | Snap | Gap from prev | |------|---------|------|--------|------|---------------| | d | 16 px | -- | 33 | 33 | -- | | r | 12 px | 0 | 49 | 49 | ~2px | | e | 13 px | -1 | 60 | 60 | ~0px | | w | 22 px | -1 | 72 | 72 | ~0px | The d-to-r gap was visibly wider than the tightly packed `rew`. **After** (12.4 advances, 4.4 kerning, fractional accumulation): | Char | Advance (FP) | Kern (FP) | Accumulator | Snap | Ink start | Gap from prev | |------|-------------|-----------|-------------|------|-----------|---------------| | d | 249 (15.56px) | -- | 528 | 33 | 34 | -- | | r | 184 (11.50px) | 0 | 777 | 49 | 49 | 0px | | e | 208 (13.00px) | -8 (-0.50px) | 953 | 60 | 61 | 1px | | w | 356 (22.25px) | -4 (-0.25px) | 1157 | 72 | 72 | 0px | Spacing is now `0, 1, 0` pixels -- nearly uniform. Verified on-device: all 5 copies of "drew" in the test EPUB produce identical spacing, confirming zero accumulator drift. #### Changes **Font conversion (`fontconvert.py`)** - Use `linearHoriAdvance` (FreeType 16.16, unhinted) instead of `advance.x` (26.6, grid-fitted to integers) for glyph advances - Encode kern values as 4.4 fixed-point with `round()` instead of `floor()` - Add `fp4_from_ft16_16()` and `fp4_from_design_units()` helper functions - Add module-level documentation of fixed-point conventions **Font data structures (`EpdFontData.h`)** - `EpdGlyph::advanceX`: `uint8_t` to `uint16_t` (no memory cost due to existing struct padding) - Add `fp4` namespace with `constexpr` helpers: `fromPixel()`, `toPixel()`, `toFloat()` - Document fixed-point conventions **Font API (`EpdFont.h/cpp`, `EpdFontFamily.h/cpp`)** - `getKerning()` return type: `int8_t` to `int` (to avoid truncation of the 4.4 value) **Rendering (`GfxRenderer.cpp`)** - `drawText()`: replace integer cursor with `int32_t` fixed-point accumulator - `drawTextRotated90CW()`: same accumulator treatment for vertical layout - `getTextAdvanceX()`, `getSpaceWidth()`, `getSpaceKernAdjust()`, `getKerning()`: convert from fixed-point to pixel at API boundary **Regenerated all built-in font headers** with new 12.4 advances and 4.4 kern values. #### Memory impact Zero additional RAM. The `advanceX` field grew from `uint8_t` to `uint16_t`, but the `EpdGlyph` struct already had 1 byte of padding at that position, so the struct size is unchanged. The fixed-point accumulator is a single `int32_t` on the stack. #### Test plan - [ ] Verify "drew" spacing in Bookerly at small, medium, and large sizes - [ ] Verify uppercase kerning pairs: AVERY, WAVE, VALUE - [ ] Verify ligature words: coffee, waffle, office - [ ] Verify all built-in fonts render correctly at each size - [ ] Verify rotated text (progress bar percentage) renders correctly - [ ] Verify combining marks (accented characters) still position correctly - [ ] Spot-check a full-length book for any layout regressions --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _**YES, Claude Opus 4.6 helped figure out a non-floating point approach for sub-pixel error accumulation**_
This commit is contained in:
+9
-10
@@ -15,10 +15,9 @@ void EpdFont::getTextBounds(const char* string, const int startX, const int star
|
||||
return;
|
||||
}
|
||||
|
||||
int cursorX = startX;
|
||||
const int cursorY = startY;
|
||||
int32_t cursorXFP = fp4::fromPixel(startX); // 12.4 fixed-point accumulator
|
||||
int lastBaseX = startX;
|
||||
int lastBaseAdvance = 0;
|
||||
int lastBaseAdvanceFP = 0; // 12.4 fixed-point
|
||||
int lastBaseTop = 0;
|
||||
constexpr int MIN_COMBINING_GAP_PX = 1;
|
||||
uint32_t cp;
|
||||
@@ -32,7 +31,6 @@ void EpdFont::getTextBounds(const char* string, const int startX, const int star
|
||||
|
||||
const EpdGlyph* glyph = getGlyph(cp);
|
||||
if (!glyph) {
|
||||
// TODO: Better handle this?
|
||||
prevCp = 0;
|
||||
continue;
|
||||
}
|
||||
@@ -46,11 +44,12 @@ void EpdFont::getTextBounds(const char* string, const int startX, const int star
|
||||
}
|
||||
|
||||
if (!isCombining && prevCp != 0) {
|
||||
cursorX += getKerning(prevCp, cp);
|
||||
cursorXFP += getKerning(prevCp, cp); // 4.4 fixed-point kern
|
||||
}
|
||||
|
||||
const int glyphBaseX = isCombining ? (lastBaseX + lastBaseAdvance / 2) : cursorX;
|
||||
const int glyphBaseY = cursorY - raiseBy;
|
||||
const int cursorXPixels = fp4::toPixel(cursorXFP); // snap 12.4 fixed-point to nearest pixel
|
||||
const int glyphBaseX = isCombining ? (lastBaseX + fp4::toPixel(lastBaseAdvanceFP / 2)) : cursorXPixels;
|
||||
const int glyphBaseY = startY - raiseBy;
|
||||
|
||||
*minX = std::min(*minX, glyphBaseX + glyph->left);
|
||||
*maxX = std::max(*maxX, glyphBaseX + glyph->left + glyph->width);
|
||||
@@ -58,10 +57,10 @@ void EpdFont::getTextBounds(const char* string, const int startX, const int star
|
||||
*maxY = std::max(*maxY, glyphBaseY + glyph->top);
|
||||
|
||||
if (!isCombining) {
|
||||
lastBaseX = cursorX;
|
||||
lastBaseAdvance = glyph->advanceX;
|
||||
lastBaseX = cursorXPixels;
|
||||
lastBaseAdvanceFP = glyph->advanceX; // 12.4 fixed-point
|
||||
lastBaseTop = glyph->top;
|
||||
cursorX += glyph->advanceX;
|
||||
cursorXFP += glyph->advanceX; // 12.4 fixed-point advance
|
||||
prevCp = cp;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ class EpdFont {
|
||||
|
||||
const EpdGlyph* getGlyph(uint32_t cp) const;
|
||||
|
||||
/// Returns the kerning adjustment (in pixels) between two codepoints.
|
||||
/// Returns the kerning adjustment (4.4 fixed-point in pixels) between two codepoints.
|
||||
/// Returns 0 if no kerning data exists for the pair.
|
||||
int8_t getKerning(uint32_t leftCp, uint32_t rightCp) const;
|
||||
|
||||
|
||||
@@ -4,11 +4,40 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
/// Font metrics use "fixed-point 4" (4 fractional bits, i.e. 1/16-pixel
|
||||
/// resolution). Both the 12.4 glyph advances (uint16_t) and the 4.4 kern
|
||||
/// values (int8_t) share the same 4 fractional bits, so they can be freely
|
||||
/// added into a single int32_t accumulator during text layout. The
|
||||
/// accumulator is snapped to the nearest whole pixel only at render time,
|
||||
/// which avoids the per-character rounding errors that plagued integer-only
|
||||
/// layout.
|
||||
///
|
||||
/// The helpers below eliminate the raw bit-shifts that would otherwise be
|
||||
/// scattered across every layout / measurement call site.
|
||||
namespace fp4 {
|
||||
constexpr int FRAC_BITS = 4;
|
||||
constexpr int32_t HALF = 1 << (FRAC_BITS - 1); // 8, added before shift for round-to-nearest
|
||||
|
||||
/// Convert an integer pixel value to 12.4 fixed-point.
|
||||
constexpr int32_t fromPixel(int px) { return static_cast<int32_t>(px) << FRAC_BITS; }
|
||||
|
||||
/// Snap a fixed-point value to the nearest integer pixel.
|
||||
constexpr int toPixel(int32_t fp) { return static_cast<int>((fp + HALF) >> FRAC_BITS); }
|
||||
|
||||
/// Convert a fixed-point value to float (mainly useful for debug logging).
|
||||
constexpr float toFloat(int32_t fp) { return fp / static_cast<float>(1 << FRAC_BITS); }
|
||||
} // namespace fp4
|
||||
|
||||
/// Fixed-point conventions used by EpdGlyph and EpdFontData:
|
||||
/// advanceX: 12.4 unsigned fixed-point in uint16_t (use fp4::toPixel)
|
||||
/// kernMatrix: 4.4 signed fixed-point in int8_t (use fp4::toPixel)
|
||||
/// Both share 4 fractional bits so they combine directly in an accumulator.
|
||||
|
||||
/// Font data stored PER GLYPH
|
||||
typedef struct {
|
||||
uint8_t width; ///< Bitmap dimensions in pixels
|
||||
uint8_t height; ///< Bitmap dimensions in pixels
|
||||
uint8_t advanceX; ///< Distance to advance cursor (x axis)
|
||||
uint16_t advanceX; ///< Distance to advance cursor (x axis), 12.4 fixed-point in pixels
|
||||
int16_t left; ///< X dist from cursor pos to UL corner
|
||||
int16_t top; ///< Y dist from cursor pos to UL corner
|
||||
uint16_t dataLength; ///< Size of the font data.
|
||||
@@ -59,11 +88,11 @@ typedef struct {
|
||||
uint16_t groupCount; ///< 0 for uncompressed fonts
|
||||
const EpdKernClassEntry* kernLeftClasses; ///< Sorted left-side class map (nullptr if none)
|
||||
const EpdKernClassEntry* kernRightClasses; ///< Sorted right-side class map (nullptr if none)
|
||||
const int8_t* kernMatrix; ///< Flat leftClassCount x rightClassCount matrix
|
||||
uint16_t kernLeftEntryCount; ///< Entries in kernLeftClasses
|
||||
uint16_t kernRightEntryCount; ///< Entries in kernRightClasses
|
||||
uint8_t kernLeftClassCount; ///< Number of distinct left classes (matrix rows)
|
||||
uint8_t kernRightClassCount; ///< Number of distinct right classes (matrix cols)
|
||||
const EpdLigaturePair* ligaturePairs; ///< Sorted ligature pair table (nullptr if none)
|
||||
uint32_t ligaturePairCount; ///< Number of entries in ligaturePairs
|
||||
const int8_t* kernMatrix; ///< Flat leftClassCount x rightClassCount matrix, 4.4 fixed-point in pixels
|
||||
uint16_t kernLeftEntryCount; ///< Entries in kernLeftClasses
|
||||
uint16_t kernRightEntryCount; ///< Entries in kernRightClasses
|
||||
uint8_t kernLeftClassCount; ///< Number of distinct left classes (matrix rows)
|
||||
uint8_t kernRightClassCount; ///< Number of distinct right classes (matrix cols)
|
||||
const EpdLigaturePair* ligaturePairs; ///< Sorted ligature pair table (nullptr if none)
|
||||
uint32_t ligaturePairCount; ///< Number of entries in ligaturePairs
|
||||
} EpdFontData;
|
||||
|
||||
+4317
-4296
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+4789
-4729
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+4578
-4787
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+5285
-5158
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+1760
-1734
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+1760
-1733
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+1760
-1747
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+1760
-1748
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+1813
-1790
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+1813
-1793
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -14,7 +14,7 @@ for size in ${BOOKERLY_FONT_SIZES[@]}; do
|
||||
font_name="bookerly_${size}_$(echo $style | tr '[:upper:]' '[:lower:]')"
|
||||
font_path="../builtinFonts/source/Bookerly/Bookerly-${style}.ttf"
|
||||
output_path="../builtinFonts/${font_name}.h"
|
||||
python fontconvert.py $font_name $size $font_path --2bit --compress --force-autohint > $output_path
|
||||
python fontconvert.py $font_name $size $font_path --2bit --compress > $output_path
|
||||
echo "Generated $output_path"
|
||||
done
|
||||
done
|
||||
|
||||
@@ -137,6 +137,33 @@ def norm_floor(val):
|
||||
def norm_ceil(val):
|
||||
return int(math.ceil(val / (1 << 6)))
|
||||
|
||||
# Fixed-point (fp4) output conventions (must match EpdFontData.h / fp4 namespace):
|
||||
#
|
||||
# advanceX 12.4 unsigned fixed-point (uint16_t).
|
||||
# 12 integer bits, 4 fractional bits = 1/16-pixel resolution.
|
||||
# Encoded from FreeType's 16.16 linearHoriAdvance.
|
||||
#
|
||||
# kernMatrix 4.4 signed fixed-point (int8_t).
|
||||
# 4 integer bits, 4 fractional bits = 1/16-pixel resolution.
|
||||
# Range: -8.0 to +7.9375 pixels.
|
||||
# Encoded from font design-unit kerning values.
|
||||
#
|
||||
# Both share 4 fractional bits so the renderer can add them directly into a
|
||||
# single int32_t accumulator and defer rounding until pixel placement.
|
||||
|
||||
def fp4_from_ft16_16(val):
|
||||
"""Convert FreeType 16.16 fixed-point to 12.4 fixed-point with rounding."""
|
||||
return (val + (1 << 11)) >> 12
|
||||
|
||||
def fp4_from_design_units(du, scale):
|
||||
"""Convert a font design-unit value to 4.4 fixed-point, clamped to int8_t.
|
||||
|
||||
Multiplies by scale (ppem / units_per_em) and shifts into 4 fractional
|
||||
bits. The result is rounded to nearest and clamped to [-128, 127].
|
||||
"""
|
||||
raw = round(du * scale * 16)
|
||||
return max(-128, min(127, raw))
|
||||
|
||||
def chunks(l, n):
|
||||
for i in range(0, len(l), n):
|
||||
yield l[i:i + n]
|
||||
@@ -270,7 +297,9 @@ for i_start, i_end in intervals:
|
||||
glyph = GlyphProps(
|
||||
width = bitmap.width,
|
||||
height = bitmap.rows,
|
||||
advance_x = norm_floor(face.glyph.advance.x),
|
||||
# We use linearHoriAdvance (16.16 fixed-point, unhinted) instead of
|
||||
# advance.x (26.6 fixed-point, grid-fitted to whole pixels by hinter)
|
||||
advance_x = fp4_from_ft16_16(face.glyph.linearHoriAdvance),
|
||||
left = face.glyph.bitmap_left,
|
||||
top = face.glyph.bitmap_top,
|
||||
data_length = len(packed),
|
||||
@@ -406,15 +435,14 @@ def extract_kerning_fonttools(font_path, codepoints, ppem):
|
||||
|
||||
font.close()
|
||||
|
||||
# Scale design-unit values to pixels
|
||||
# Scale design-unit kerning values to 4.4 fixed-point pixels.
|
||||
scale = ppem / units_per_em
|
||||
result = {} # (leftCp, rightCp) -> adjust
|
||||
result = {} # (leftCp, rightCp) -> 4.4 fixed-point adjust
|
||||
for (lg, rg), du in raw_kern.items():
|
||||
lcp = glyph_to_cp[lg]
|
||||
rcp = glyph_to_cp[rg]
|
||||
adjust = int(math.floor(du * scale))
|
||||
adjust = fp4_from_design_units(du, scale)
|
||||
if adjust != 0:
|
||||
adjust = max(-128, min(127, adjust))
|
||||
result[(lcp, rcp)] = adjust
|
||||
return result
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ enum class TextRotation { None, Rotated90CW };
|
||||
// Coordinate mapping and cursor advance direction are selected at compile time via the template parameter.
|
||||
template <TextRotation rotation>
|
||||
static void renderCharImpl(const GfxRenderer& renderer, GfxRenderer::RenderMode renderMode,
|
||||
const EpdFontFamily& fontFamily, const uint32_t cp, int* cursorX, int* cursorY,
|
||||
const EpdFontFamily& fontFamily, const uint32_t cp, int cursorX, int cursorY,
|
||||
const bool pixelState, const EpdFontFamily::Style style) {
|
||||
const EpdGlyph* glyph = fontFamily.getGlyph(cp, style);
|
||||
if (!glyph) {
|
||||
@@ -87,11 +87,11 @@ static void renderCharImpl(const GfxRenderer& renderer, GfxRenderer::RenderMode
|
||||
// For Rotated: outer loop advances screenX, inner loop advances screenY (in reverse)
|
||||
int outerBase, innerBase;
|
||||
if constexpr (rotation == TextRotation::Rotated90CW) {
|
||||
outerBase = *cursorX + fontData->ascender - top; // screenX = outerBase + glyphY
|
||||
innerBase = *cursorY - left; // screenY = innerBase - glyphX
|
||||
outerBase = cursorX + fontData->ascender - top; // screenX = outerBase + glyphY
|
||||
innerBase = cursorY - left; // screenY = innerBase - glyphX
|
||||
} else {
|
||||
outerBase = *cursorY - top; // screenY = outerBase + glyphY
|
||||
innerBase = *cursorX + left; // screenX = innerBase + glyphX
|
||||
outerBase = cursorY - top; // screenY = outerBase + glyphY
|
||||
innerBase = cursorX + left; // screenX = innerBase + glyphX
|
||||
}
|
||||
|
||||
if (is2Bit) {
|
||||
@@ -152,12 +152,6 @@ static void renderCharImpl(const GfxRenderer& renderer, GfxRenderer::RenderMode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if constexpr (rotation == TextRotation::Rotated90CW) {
|
||||
*cursorY -= glyph->advanceX;
|
||||
} else {
|
||||
*cursorX += glyph->advanceX;
|
||||
}
|
||||
}
|
||||
|
||||
// IMPORTANT: This function is in critical rendering path and is called for every pixel. Please keep it as simple and
|
||||
@@ -206,11 +200,10 @@ void GfxRenderer::drawCenteredText(const int fontId, const int y, const char* te
|
||||
|
||||
void GfxRenderer::drawText(const int fontId, const int x, const int y, const char* text, const bool black,
|
||||
const EpdFontFamily::Style style) const {
|
||||
int yPos = y + getFontAscenderSize(fontId);
|
||||
int xPos = x;
|
||||
const int yPos = y + getFontAscenderSize(fontId);
|
||||
int32_t xPosFP = fp4::fromPixel(x); // 12.4 fixed-point accumulator
|
||||
int lastBaseX = x;
|
||||
int lastBaseY = yPos;
|
||||
int lastBaseAdvance = 0;
|
||||
int lastBaseAdvanceFP = 0; // 12.4 fixed-point
|
||||
int lastBaseTop = 0;
|
||||
|
||||
// cannot draw a NULL / empty string
|
||||
@@ -239,25 +232,26 @@ void GfxRenderer::drawText(const int fontId, const int x, const int y, const cha
|
||||
}
|
||||
}
|
||||
|
||||
int combiningX = lastBaseX + lastBaseAdvance / 2;
|
||||
int combiningY = lastBaseY - raiseBy;
|
||||
renderChar(font, cp, &combiningX, &combiningY, black, style);
|
||||
const int combiningX = lastBaseX + fp4::toPixel(lastBaseAdvanceFP / 2);
|
||||
const int combiningY = yPos - raiseBy;
|
||||
renderCharImpl<TextRotation::None>(*this, renderMode, font, cp, combiningX, combiningY, black, style);
|
||||
continue;
|
||||
}
|
||||
|
||||
cp = font.applyLigatures(cp, text, style);
|
||||
if (prevCp != 0) {
|
||||
xPos += font.getKerning(prevCp, cp, style);
|
||||
}
|
||||
const int kernFP = (prevCp != 0) ? font.getKerning(prevCp, cp, style) : 0; // 4.4 fixed-point kern
|
||||
xPosFP += kernFP;
|
||||
|
||||
lastBaseX = fp4::toPixel(xPosFP); // snap 12.4 fixed-point to nearest pixel
|
||||
const EpdGlyph* glyph = font.getGlyph(cp, style);
|
||||
|
||||
lastBaseX = xPos;
|
||||
lastBaseY = yPos;
|
||||
lastBaseAdvance = glyph ? glyph->advanceX : 0;
|
||||
lastBaseAdvanceFP = glyph ? glyph->advanceX : 0;
|
||||
lastBaseTop = glyph ? glyph->top : 0;
|
||||
|
||||
renderChar(font, cp, &xPos, &yPos, black, style);
|
||||
renderCharImpl<TextRotation::None>(*this, renderMode, font, cp, lastBaseX, yPos, black, style);
|
||||
if (glyph) {
|
||||
xPosFP += glyph->advanceX; // 12.4 fixed-point advance
|
||||
}
|
||||
prevCp = cp;
|
||||
}
|
||||
}
|
||||
@@ -946,7 +940,7 @@ int GfxRenderer::getSpaceWidth(const int fontId, const EpdFontFamily::Style styl
|
||||
}
|
||||
|
||||
const EpdGlyph* spaceGlyph = fontIt->second.getGlyph(' ', style);
|
||||
return spaceGlyph ? spaceGlyph->advanceX : 0;
|
||||
return spaceGlyph ? fp4::toPixel(spaceGlyph->advanceX) : 0; // snap 12.4 fixed-point to nearest pixel
|
||||
}
|
||||
|
||||
int GfxRenderer::getSpaceKernAdjust(const int fontId, const uint32_t leftCp, const uint32_t rightCp,
|
||||
@@ -954,14 +948,16 @@ int GfxRenderer::getSpaceKernAdjust(const int fontId, const uint32_t leftCp, con
|
||||
const auto fontIt = fontMap.find(fontId);
|
||||
if (fontIt == fontMap.end()) return 0;
|
||||
const auto& font = fontIt->second;
|
||||
return font.getKerning(leftCp, ' ', style) + font.getKerning(' ', rightCp, style);
|
||||
const int kernFP = font.getKerning(leftCp, ' ', style) + font.getKerning(' ', rightCp, style); // 4.4 fixed-point
|
||||
return fp4::toPixel(kernFP); // snap 4.4 fixed-point to nearest pixel
|
||||
}
|
||||
|
||||
int GfxRenderer::getKerning(const int fontId, const uint32_t leftCp, const uint32_t rightCp,
|
||||
const EpdFontFamily::Style style) const {
|
||||
const auto fontIt = fontMap.find(fontId);
|
||||
if (fontIt == fontMap.end()) return 0;
|
||||
return fontIt->second.getKerning(leftCp, rightCp, style);
|
||||
const int kernFP = fontIt->second.getKerning(leftCp, rightCp, style); // 4.4 fixed-point
|
||||
return fp4::toPixel(kernFP); // snap 4.4 fixed-point to nearest pixel
|
||||
}
|
||||
|
||||
int GfxRenderer::getTextAdvanceX(const int fontId, const char* text, EpdFontFamily::Style style) const {
|
||||
@@ -973,7 +969,7 @@ int GfxRenderer::getTextAdvanceX(const int fontId, const char* text, EpdFontFami
|
||||
|
||||
uint32_t cp;
|
||||
uint32_t prevCp = 0;
|
||||
int width = 0;
|
||||
int32_t widthFP = 0; // 12.4 fixed-point accumulator
|
||||
const auto& font = fontIt->second;
|
||||
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text)))) {
|
||||
if (utf8IsCombiningMark(cp)) {
|
||||
@@ -981,13 +977,13 @@ int GfxRenderer::getTextAdvanceX(const int fontId, const char* text, EpdFontFami
|
||||
}
|
||||
cp = font.applyLigatures(cp, text, style);
|
||||
if (prevCp != 0) {
|
||||
width += font.getKerning(prevCp, cp, style);
|
||||
widthFP += font.getKerning(prevCp, cp, style); // 4.4 fixed-point kern
|
||||
}
|
||||
const EpdGlyph* glyph = font.getGlyph(cp, style);
|
||||
if (glyph) width += glyph->advanceX;
|
||||
if (glyph) widthFP += glyph->advanceX; // 12.4 fixed-point advance
|
||||
prevCp = cp;
|
||||
}
|
||||
return width;
|
||||
return fp4::toPixel(widthFP); // snap 12.4 fixed-point to nearest pixel
|
||||
}
|
||||
|
||||
int GfxRenderer::getFontAscenderSize(const int fontId) const {
|
||||
@@ -1034,11 +1030,9 @@ void GfxRenderer::drawTextRotated90CW(const int fontId, const int x, const int y
|
||||
|
||||
const auto& font = fontIt->second;
|
||||
|
||||
int xPos = x;
|
||||
int yPos = y;
|
||||
int lastBaseX = x;
|
||||
int32_t yPosFP = fp4::fromPixel(y); // 12.4 fixed-point accumulator
|
||||
int lastBaseY = y;
|
||||
int lastBaseAdvance = 0;
|
||||
int lastBaseAdvanceFP = 0; // 12.4 fixed-point
|
||||
int lastBaseTop = 0;
|
||||
constexpr int MIN_COMBINING_GAP_PX = 1;
|
||||
|
||||
@@ -1055,25 +1049,27 @@ void GfxRenderer::drawTextRotated90CW(const int fontId, const int x, const int y
|
||||
}
|
||||
}
|
||||
|
||||
int combiningX = lastBaseX - raiseBy;
|
||||
int combiningY = lastBaseY - lastBaseAdvance / 2;
|
||||
renderCharImpl<TextRotation::Rotated90CW>(*this, renderMode, font, cp, &combiningX, &combiningY, black, style);
|
||||
const int combiningX = x - raiseBy;
|
||||
const int combiningY = lastBaseY - fp4::toPixel(lastBaseAdvanceFP / 2);
|
||||
renderCharImpl<TextRotation::Rotated90CW>(*this, renderMode, font, cp, combiningX, combiningY, black, style);
|
||||
continue;
|
||||
}
|
||||
|
||||
cp = font.applyLigatures(cp, text, style);
|
||||
if (prevCp != 0) {
|
||||
yPos -= font.getKerning(prevCp, cp, style);
|
||||
yPosFP -= font.getKerning(prevCp, cp, style); // 4.4 fixed-point kern (subtract for rotated)
|
||||
}
|
||||
|
||||
lastBaseY = fp4::toPixel(yPosFP); // snap 12.4 fixed-point to nearest pixel
|
||||
const EpdGlyph* glyph = font.getGlyph(cp, style);
|
||||
|
||||
lastBaseX = xPos;
|
||||
lastBaseY = yPos;
|
||||
lastBaseAdvance = glyph ? glyph->advanceX : 0;
|
||||
lastBaseAdvanceFP = glyph ? glyph->advanceX : 0; // 12.4 fixed-point
|
||||
lastBaseTop = glyph ? glyph->top : 0;
|
||||
|
||||
renderCharImpl<TextRotation::Rotated90CW>(*this, renderMode, font, cp, &xPos, &yPos, black, style);
|
||||
renderCharImpl<TextRotation::Rotated90CW>(*this, renderMode, font, cp, x, lastBaseY, black, style);
|
||||
if (glyph) {
|
||||
yPosFP -= glyph->advanceX; // 12.4 fixed-point advance (subtract for rotated)
|
||||
}
|
||||
prevCp = cp;
|
||||
}
|
||||
}
|
||||
@@ -1174,11 +1170,6 @@ void GfxRenderer::cleanupGrayscaleWithFrameBuffer() const {
|
||||
}
|
||||
}
|
||||
|
||||
void GfxRenderer::renderChar(const EpdFontFamily& fontFamily, uint32_t cp, int* x, int* y, bool pixelState,
|
||||
EpdFontFamily::Style style) const {
|
||||
renderCharImpl<TextRotation::None>(*this, renderMode, fontFamily, cp, x, y, pixelState, style);
|
||||
}
|
||||
|
||||
void GfxRenderer::getOrientedViewableTRBL(int* outTop, int* outRight, int* outBottom, int* outLeft) const {
|
||||
switch (orientation) {
|
||||
case Portrait:
|
||||
|
||||
@@ -40,8 +40,6 @@ class GfxRenderer {
|
||||
uint8_t* bwBufferChunks[BW_BUFFER_NUM_CHUNKS] = {nullptr};
|
||||
std::map<int, EpdFontFamily> fontMap;
|
||||
FontDecompressor* fontDecompressor = nullptr;
|
||||
void renderChar(const EpdFontFamily& fontFamily, uint32_t cp, int* x, int* y, bool pixelState,
|
||||
EpdFontFamily::Style style) const;
|
||||
void freeBwBufferChunks();
|
||||
template <Color color>
|
||||
void drawPixelDither(int x, int y) const;
|
||||
|
||||
Reference in New Issue
Block a user