TextPhantom-v4.0.0 / backend /render /text_utils.py
plan291037's picture
Update text_utils.py
8f81bcf
Raw
History Blame Contribute Delete
3.19 kB
"""Low-level text classification / sanitisation used by the renderer.
Kept separate from :mod:`backend.utils.text` because these helpers are tied
to *drawing* concerns (Thai-vs-Latin font selection, control-char stripping)
rather than general text munging.
"""
from __future__ import annotations
import unicodedata
# Thai Unicode block.
_THAI_START = 0x0E00
_THAI_END = 0x0E7F
# A run is a (text, is_thai) pair — used to switch fonts mid-string.
Run = tuple[str, bool | None]
def is_thai_char(ch: str) -> bool:
"""True if ``ch`` is in the Thai Unicode block."""
return bool(ch) and _THAI_START <= ord(ch) <= _THAI_END
def contains_thai(text: str) -> bool:
"""True if ``text`` contains at least one Thai character."""
return any(is_thai_char(ch) for ch in text or "")
# Right-to-left Unicode ranges: Hebrew, Arabic (+ supplements) and the
# Arabic presentation forms blocks.
_RTL_RANGES = (
(0x0590, 0x05FF), # Hebrew
(0x0600, 0x06FF), # Arabic
(0x0700, 0x074F), # Syriac
(0x0750, 0x077F), # Arabic Supplement
(0x0780, 0x07BF), # Thaana
(0x08A0, 0x08FF), # Arabic Extended-A
(0xFB1D, 0xFDFF), # Hebrew / Arabic presentation forms-A
(0xFE70, 0xFEFF), # Arabic presentation forms-B
)
def is_rtl_char(ch: str) -> bool:
"""True if ``ch`` belongs to a right-to-left script block."""
if not ch:
return False
o = ord(ch)
return any(lo <= o <= hi for lo, hi in _RTL_RANGES)
def contains_rtl(text: str) -> bool:
"""True if ``text`` contains at least one right-to-left character."""
return any(is_rtl_char(ch) for ch in text or "")
def sanitize_draw_text(s: str) -> str:
"""Strip characters Pillow cannot render.
Removes zero-width spaces / BOMs and all control characters except
newline, while normalising newline styles.
"""
t = (s or "").replace("\r\n", "\n").replace("\r", "\n")
t = t.replace("​", "").replace("", "")
return "".join(
ch for ch in t if ch == "\n" or unicodedata.category(ch)[0] != "C"
)
def split_runs_for_fallback(text: str) -> list[Run]:
"""Split ``text`` into maximal Thai / non-Thai runs.
Whitespace inherits the current run's script so a space between two Thai
words doesn't force a font switch. Newlines become their own ``("\\n",
None)`` run. The renderer draws each run with the matching font.
"""
runs: list[Run] = []
cur: list[str] = []
cur_is_thai: bool | None = None
for ch in text:
if ch == "\n":
if cur:
runs.append(("".join(cur), cur_is_thai))
cur = []
runs.append(("\n", None))
cur_is_thai = None
continue
is_thai = is_thai_char(ch)
if ch.isspace() and cur_is_thai is not None:
is_thai = cur_is_thai
if cur_is_thai is None:
cur_is_thai = is_thai
cur = [ch]
elif is_thai == cur_is_thai:
cur.append(ch)
else:
runs.append(("".join(cur), cur_is_thai))
cur = [ch]
cur_is_thai = is_thai
if cur:
runs.append(("".join(cur), cur_is_thai))
return runs