fix: Fix hyphenation and rendering of decomposed characters (#1037)
## Summary * This PR fixes decomposed diacritic handling end-to-end: - Hyphenation: normalize common Latin base+combining sequences to precomposed codepoints before Liang pattern matching, so decomposed words hyphenate correctly - Rendering: correct combining-mark placement logic so non-spacing marks are attached to the preceding base glyph in normal and rotated text rendering paths, with corresponding text-bounds consistency updates. - Hyphenation around non breaking space variants have been fixed (and extended) - Hyphenation of terms that already included of hyphens were fixed to include Liang pattern application (eg "US-Satellitensystem" was *exclusively* broken at the existing hyphen) ## Additional Context * Before <img width="800" height="480" alt="2" src="https://github.com/user-attachments/assets/b9c515c4-ab75-45cc-8b52-f4d86bce519d" /> * After <img width="480" height="800" alt="fix1" src="https://github.com/user-attachments/assets/4999f6a8-f51c-4c0a-b144-f153f77ddb57" /> <img width="800" height="480" alt="fix2" src="https://github.com/user-attachments/assets/7355126b-80c7-441f-b390-4e0897ee3fb6" /> * Note 1: the hyphenation fix is not a 100% bullet proof implementation. It adds composition of *common* base+combining sequences (e.g. O + U+0308 -> Ö) during codepoint collection. A complete solution would require implementing proper Unicode normalization (at least NFC, possibly NFKC in specific cases) before hyphenation and rendering, instead of hand-mapping a few combining marks. That was beyond the scope of this fix. * Note 2: the render fix should be universal and not limited to the constraints outlined above: it properly x-centers the compund glyph over the previous one, and it uses at least 1pt of visual distance in y. Before: <img width="478" height="167" alt="Image" src="https://github.com/user-attachments/assets/f8db60d5-35b1-4477-96d0-5003b4e4a2a1" /> After: <img width="479" height="180" alt="Image" src="https://github.com/user-attachments/assets/1b48ef97-3a77-475a-8522-23f4aca8e904" /> * This should resolve the issues described in #998 --- ### 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? _**PARTIALLY**_
This commit is contained in:
@@ -157,10 +157,12 @@ static void renderCharImpl(const GfxRenderer& renderer, GfxRenderer::RenderMode
|
||||
}
|
||||
}
|
||||
|
||||
if constexpr (rotation == TextRotation::Rotated90CW) {
|
||||
*cursorY -= glyph->advanceX;
|
||||
} else {
|
||||
*cursorX += glyph->advanceX;
|
||||
if (!utf8IsCombiningMark(cp)) {
|
||||
if constexpr (rotation == TextRotation::Rotated90CW) {
|
||||
*cursorY -= glyph->advanceX;
|
||||
} else {
|
||||
*cursorX += glyph->advanceX;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,6 +214,11 @@ void GfxRenderer::drawText(const int fontId, const int x, const int y, const cha
|
||||
const EpdFontFamily::Style style) const {
|
||||
int yPos = y + getFontAscenderSize(fontId);
|
||||
int xpos = x;
|
||||
int lastBaseX = x;
|
||||
int lastBaseY = yPos;
|
||||
int lastBaseAdvance = 0;
|
||||
int lastBaseTop = 0;
|
||||
bool hasBaseGlyph = false;
|
||||
|
||||
// cannot draw a NULL / empty string
|
||||
if (text == nullptr || *text == '\0') {
|
||||
@@ -224,9 +231,43 @@ void GfxRenderer::drawText(const int fontId, const int x, const int y, const cha
|
||||
return;
|
||||
}
|
||||
const auto& font = fontIt->second;
|
||||
constexpr int MIN_COMBINING_GAP_PX = 1;
|
||||
|
||||
uint32_t cp;
|
||||
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text)))) {
|
||||
if (utf8IsCombiningMark(cp) && hasBaseGlyph) {
|
||||
const EpdGlyph* combiningGlyph = font.getGlyph(cp, style);
|
||||
if (!combiningGlyph) {
|
||||
combiningGlyph = font.getGlyph(REPLACEMENT_GLYPH, style);
|
||||
}
|
||||
|
||||
int raiseBy = 0;
|
||||
if (combiningGlyph) {
|
||||
const int currentGap = combiningGlyph->top - combiningGlyph->height - lastBaseTop;
|
||||
if (currentGap < MIN_COMBINING_GAP_PX) {
|
||||
raiseBy = MIN_COMBINING_GAP_PX - currentGap;
|
||||
}
|
||||
}
|
||||
|
||||
int combiningX = lastBaseX + lastBaseAdvance / 2;
|
||||
int combiningY = lastBaseY - raiseBy;
|
||||
renderChar(font, cp, &combiningX, &combiningY, black, style);
|
||||
continue;
|
||||
}
|
||||
|
||||
const EpdGlyph* glyph = font.getGlyph(cp, style);
|
||||
if (!glyph) {
|
||||
glyph = font.getGlyph(REPLACEMENT_GLYPH, style);
|
||||
}
|
||||
|
||||
if (!utf8IsCombiningMark(cp)) {
|
||||
lastBaseX = xpos;
|
||||
lastBaseY = yPos;
|
||||
lastBaseAdvance = glyph ? glyph->advanceX : 0;
|
||||
lastBaseTop = glyph ? glyph->top : 0;
|
||||
hasBaseGlyph = true;
|
||||
}
|
||||
|
||||
renderChar(font, cp, &xpos, &yPos, black, style);
|
||||
}
|
||||
}
|
||||
@@ -864,6 +905,9 @@ int GfxRenderer::getTextAdvanceX(const int fontId, const char* text, const EpdFo
|
||||
int width = 0;
|
||||
const auto& font = fontIt->second;
|
||||
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text)))) {
|
||||
if (utf8IsCombiningMark(cp)) {
|
||||
continue;
|
||||
}
|
||||
const EpdGlyph* glyph = font.getGlyph(cp, style);
|
||||
if (!glyph) glyph = font.getGlyph(REPLACEMENT_GLYPH, style);
|
||||
if (glyph) width += glyph->advanceX;
|
||||
@@ -917,9 +961,48 @@ void GfxRenderer::drawTextRotated90CW(const int fontId, const int x, const int y
|
||||
|
||||
int xPos = x;
|
||||
int yPos = y;
|
||||
int lastBaseX = x;
|
||||
int lastBaseY = y;
|
||||
int lastBaseAdvance = 0;
|
||||
int lastBaseTop = 0;
|
||||
bool hasBaseGlyph = false;
|
||||
constexpr int MIN_COMBINING_GAP_PX = 1;
|
||||
|
||||
uint32_t cp;
|
||||
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text)))) {
|
||||
if (utf8IsCombiningMark(cp) && hasBaseGlyph) {
|
||||
const EpdGlyph* combiningGlyph = font.getGlyph(cp, style);
|
||||
if (!combiningGlyph) {
|
||||
combiningGlyph = font.getGlyph(REPLACEMENT_GLYPH, style);
|
||||
}
|
||||
|
||||
int raiseBy = 0;
|
||||
if (combiningGlyph) {
|
||||
const int currentGap = combiningGlyph->top - combiningGlyph->height - lastBaseTop;
|
||||
if (currentGap < MIN_COMBINING_GAP_PX) {
|
||||
raiseBy = MIN_COMBINING_GAP_PX - currentGap;
|
||||
}
|
||||
}
|
||||
|
||||
int combiningX = lastBaseX - raiseBy;
|
||||
int combiningY = lastBaseY - lastBaseAdvance / 2;
|
||||
renderCharImpl<TextRotation::Rotated90CW>(*this, renderMode, font, cp, &combiningX, &combiningY, black, style);
|
||||
continue;
|
||||
}
|
||||
|
||||
const EpdGlyph* glyph = font.getGlyph(cp, style);
|
||||
if (!glyph) {
|
||||
glyph = font.getGlyph(REPLACEMENT_GLYPH, style);
|
||||
}
|
||||
|
||||
if (!utf8IsCombiningMark(cp)) {
|
||||
lastBaseX = xPos;
|
||||
lastBaseY = yPos;
|
||||
lastBaseAdvance = glyph ? glyph->advanceX : 0;
|
||||
lastBaseTop = glyph ? glyph->top : 0;
|
||||
hasBaseGlyph = true;
|
||||
}
|
||||
|
||||
renderCharImpl<TextRotation::Rotated90CW>(*this, renderMode, font, cp, &xPos, &yPos, black, style);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user