File size: 3,193 Bytes
844c9ea
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
394e793
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
844c9ea
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
"""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