Fix garbled Arabic/Devanagari text by using HarfBuzz text shaping

encode_text() maps unicode characters one-by-one to glyph IDs without
any text shaping, producing incorrect output for complex scripts:
Arabic glyphs in wrong order without joining forms, Devanagari conjuncts
broken apart. Replace with shape_text() which runs HarfBuzz for proper
BiDi reordering, Arabic shaping, and Devanagari conjunct formation.
This commit is contained in:
James R. Barlow
2026-02-11 01:30:15 -08:00
parent 716ce6324c
commit 2b32d35a03
+22 -1
View File
@@ -691,7 +691,7 @@ class Fpdf2PdfRenderer:
render_tz = word_tz
ops.append(f'{render_tz:.2f} Tz')
ops.append(pdf.current_font.encode_text(text_to_render))
ops.append(self._encode_shaped_text(pdf, text_to_render))
prev_x_baseline = x_baseline
@@ -707,6 +707,27 @@ class Fpdf2PdfRenderer:
# don't think Tz is still set from our raw operators
pdf.font_stretching = 100
def _encode_shaped_text(self, pdf: FPDF, text: str) -> str:
"""Encode text using HarfBuzz text shaping for complex script support.
Unlike font.encode_text() which maps unicode characters one-by-one to
glyph IDs, this uses HarfBuzz to handle BiDi reordering, Arabic joining
forms, Devanagari conjuncts, and other complex script shaping. Falls
back to encode_text() when text shaping is not enabled.
"""
font = pdf.current_font
if pdf.text_shaping and pdf.text_shaping.get("use_shaping_engine"):
shaped = font.shape_text(text, pdf.font_size_pt, pdf.text_shaping)
if shaped:
mapped = "".join(
chr(ti["mapped_char"])
for ti in shaped
if ti["mapped_char"] is not None
)
if mapped:
return f"({font.escape_text(mapped)}) Tj"
return font.encode_text(text)
def _is_cjk_only(self, text: str) -> bool:
"""Check if text contains only CJK characters.