File: test_bidirectional.py

package info (click to toggle)
fpdf2 2.8.7-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 114,352 kB
  • sloc: python: 50,410; sh: 133; makefile: 12
file content (276 lines) | stat: -rw-r--r-- 12,152 bytes parent folder | download
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
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
from pathlib import Path
import lzma

from fpdf import FPDF
from fpdf.bidi import BidiParagraph, auto_detect_base_direction
from fpdf.enums import TextDirection
from test.conftest import assert_pdf_equal

HERE = Path(__file__).resolve().parent
FONTS_DIR = HERE / ".." / "fonts"

# The BidiTest.txt file lists the input string as a list of character classes.
# CHAR_MAPPING is mapping one character of each class so we can test the algorithm with a string.
# i.e. translate L ET CS ON to "a$,!"
CHAR_MAPPING = {
    "AL": "\u0608",  # Arabic Letter
    "AN": "\u0600",  # Arabic Number
    "B": "\u2029",  # Paragraph Separator
    "BN": "\U000e007a",  # Boundary Neutral
    "CS": ",",  # Common Separator
    "EN": "0",  # European Number
    "ES": "+",  # European Separator
    "ET": "$",  # European Terminator
    "FSI": "\u2068",  # First Strong Isolate
    "L": "a",  # Left-to-right
    "NSM": "\u0303",  # Nonspacing Mark
    "R": "\u05be",  # Right-to-left
    "LRE": "\u202a",  # Left-to-Right Embedding
    "LRI": "\u2066",  # Left-to-Right Isolate
    "LRO": "\u202d",  # Left-to-Right Override
    "ON": "!",  # Other Neutrals
    "PDF": "\u202c",  # Pop Directional Format
    "PDI": "\u2069",  # Pop Directional Isolate
    "RLE": "\u202b",  # Right-to-Left Embedding
    "RLI": "\u2067",  # Right-to-Left Isolate
    "RLO": "\u202e",  # Right-to-Left Override
    "S": "\t",  # Segment Separator
    "WS": " ",  # White Space
}


def test_bidi_conformance():
    """
    The file BidiTest.txt comprises exhaustive test sequences of bidirectional types
    https://www.unicode.org/reports/tr41/tr41-32.html#Tests9
    This file contains 770,241 tests

    The file BidiTest.txt is available from:
    https://www.unicode.org/Public/17.0.0/ucd/BidiTest.txt

    The local copy is compressed with xz to save space (the original file is 7.7 MB)
    """

    def check_result(string, base_direction, levels, reorder):
        characters, reordered_characters = BidiParagraph(
            text=string, base_direction=base_direction
        ).get_all()
        levels = [i for i in levels if i != "x"]
        if not levels:
            len_levels = 0
        else:
            len_levels = len(levels)
        if len(characters) != len_levels:
            return False
        for index, char in enumerate(characters):
            if levels[index] != "x" and levels[index] != str(char.embedding_level):
                return False
        return not any(
            reorder[index] != str(char.character_index)
            for (index, char) in enumerate(reordered_characters)
        )

    with lzma.open(HERE / "BidiTest.txt.xz", "rb") as compressed_file:
        data = compressed_file.read().decode("utf-8").split("\n")

    levels = []
    reorder = []
    test_count = 0
    for line in data:
        if len(line) == 0:  # ignore blank line
            continue
        if line[0] == "#":  # ignore comment line
            continue
        if line.startswith("@Levels:"):
            levels = line[9:].split(" ")
            continue
        if line.startswith("@Reorder:"):
            reorder = line[10:].split(" ")
            continue
        test_data = line.split(";")
        assert len(test_data) == 2

        string = ""
        for char_type in test_data[0].split(" "):
            string += CHAR_MAPPING[char_type]

        bitset = test_data[1]
        if int(bitset) & 1 > 0:  # auto-detect
            assert check_result(string, None, levels, reorder)
            test_count += 1
        if int(bitset) & 2 > 0:  # force LTR
            assert check_result(string, TextDirection.LTR, levels, reorder)
            test_count += 1
        if int(bitset) & 4 > 0:  # force RTL
            assert check_result(string, TextDirection.RTL, levels, reorder)
            test_count += 1
    assert test_count == 770241


def test_bidi_character():
    """
    The other test file, BidiCharacterTest.txt, contains test sequences of explicit code points, including, for example, bracket pairs.
    There are 91,707 tests on this file

    The file BidiCharacterTest.txt is available from:
    https://www.unicode.org/Public/17.0.0/ucd/BidiCharacterTest.txt

    The local copy is compressed with xz to save space (the original file is 6.7 MB)
    """

    with lzma.open(HERE / "BidiCharacterTest.txt.xz", "rb") as compressed_file:
        data = compressed_file.read().decode("utf-8").split("\n")

    test_count = 0
    for line in data:
        if len(line) == 0:  # ignore blank line
            continue
        if line[0] == "#":  # ignore comment line
            continue
        test_data = line.split(";")
        assert len(test_data) == 5
        test_count += 1

        string = ""
        for char in test_data[0].split(" "):
            string += chr(int(char, 16))
        if test_data[1] == "0":
            base_direction = TextDirection.LTR
        elif test_data[1] == "1":
            base_direction = TextDirection.RTL
        elif test_data[1] == "2":
            base_direction = None  # auto
        else:
            raise ValueError(f"Invalid base direction {test_data[1]}")

        if not base_direction:
            # test the auto detect direction algorithm
            assert (
                auto_detect_base_direction(string) == TextDirection.LTR
                and test_data[2] == "0"
            ) or (
                auto_detect_base_direction(string) == TextDirection.RTL
                and test_data[2] == "1"
            )

        characters = BidiParagraph(
            text=string, base_direction=base_direction
        ).get_characters_with_embedding_level()

        result_index = 0
        for level in test_data[3].split(" "):
            if level == "x":
                continue
            assert int(level) == characters[result_index].embedding_level
            result_index += 1

    assert test_count == 91707


def test_bidi_lorem_ipsum(tmp_path):
    # taken from https://ar.lipsum.com/ - contains Arabic (RTL) with portions in english (LTR) within the text
    ARABIC_LOREM_IPSUM = """
ما فائدته ؟
هناك حقيقة مثبتة منذ زمن طويل وهي أن المحتوى المقروء لصفحة ما سيلهي القارئ عن التركيز على الشكل الخارجي للنص أو شكل توضع الفقرات في الصفحة التي يقرأها. ولذلك يتم استخدام طريقة لوريم إيبسوم لأنها تعطي توزيعاَ طبيعياَ -إلى حد ما- للأحرف عوضاً عن استخدام "هنا يوجد محتوى نصي، هنا يوجد محتوى نصي" فتجعلها تبدو (أي الأحرف) وكأنها نص مقروء. العديد من برامح النشر المكتبي وبرامح تحرير صفحات الويب تستخدم لوريم إيبسوم بشكل إفتراضي كنموذج عن النص، وإذا قمت بإدخال "lorem ipsum" في أي محرك بحث ستظهر العديد من المواقع الحديثة العهد في نتائج البحث. على مدى السنين ظهرت نسخ جديدة ومختلفة من نص لوريم إيبسوم، أحياناً عن طريق الصدفة، وأحياناً عن عمد كإدخال بعض العبارات الفكاهية إليها.

ما أصله ؟
خلافاَ للإعتقاد السائد فإن لوريم إيبسوم ليس نصاَ عشوائياً، بل إن له جذور في الأدب اللاتيني الكلاسيكي منذ العام 45 قبل الميلاد، مما يجعله أكثر من 2000 عام في القدم. قام البروفيسور "ريتشارد ماك لينتوك" (Richard McClintock) وهو بروفيسور اللغة اللاتينية في جامعة هامبدن-سيدني في فيرجينيا بالبحث عن أصول كلمة لاتينية غامضة في نص لوريم إيبسوم وهي "consectetur"، وخلال تتبعه لهذه الكلمة في الأدب اللاتيني اكتشف المصدر الغير قابل للشك. فلقد اتضح أن كلمات نص لوريم إيبسوم تأتي من الأقسام 1.10.32 و 1.10.33 من كتاب "حول أقاصي الخير والشر" (de Finibus Bonorum et Malorum) للمفكر شيشيرون (Cicero) والذي كتبه في عام 45 قبل الميلاد. هذا الكتاب هو بمثابة مقالة علمية مطولة في نظرية الأخلاق، وكان له شعبية كبيرة في عصر النهضة. السطر الأول من لوريم إيبسوم "Lorem ipsum dolor sit amet.." يأتي من سطر في القسم 1.20.32 من هذا الكتاب.

للمهتمين قمنا بوضع نص لوريم إبسوم القياسي والمُستخدم منذ القرن الخامس عشر في الأسفل. وتم أيضاً توفير الأقسام 1.10.32 و 1.10.33 من "حول أقاصي الخير والشر" (de Finibus Bonorum et Malorum) لمؤلفه شيشيرون (Cicero) بصيغها الأصلية، مرفقة بالنسخ الإنكليزية لها والتي قام بترجمتها هـ.راكهام (H. Rackham) في عام 1914.

"""
    pdf = FPDF()
    pdf.add_page()
    pdf.add_font(family="NotoArabic", fname=HERE / "NotoNaskhArabic-Regular.ttf")
    pdf.add_font(family="NotoSans", fname=FONTS_DIR / "NotoSans-Regular.ttf")
    pdf.set_fallback_fonts(["NotoSans"])
    pdf.set_font("NotoArabic", size=20)
    pdf.set_text_shaping(True)
    pdf.multi_cell(
        text=ARABIC_LOREM_IPSUM, w=pdf.epw, h=10, new_x="LEFT", new_y="NEXT", align="R"
    )

    assert_pdf_equal(
        pdf,
        HERE / "bidi_arabic_lorem_ipsum.pdf",
        tmp_path,
    )


def test_bidi_paragraph_direction(tmp_path):
    pdf = FPDF()
    pdf.add_page()
    pdf.add_font(family="SBL_Hbrw", fname=HERE / "SBL_Hbrw.ttf")
    pdf.set_font("SBL_Hbrw", size=18)
    pdf.set_text_shaping(True)

    text1 = "אנגלית (באנגלית: English)"  # first char is RTL
    text2 = "The test is: אנגלית (באנגלית: English)"  # first char is LTR

    pdf.cell(text="No text shaping (not bidirectional)", new_x="left", new_y="next")
    pdf.cell(text=text1, new_x="left", new_y="next")
    pdf.cell(text=text2, new_x="left", new_y="next")
    pdf.ln()

    pdf.set_text_shaping(use_shaping_engine=True)
    pdf.cell(
        text="Text shaping-Automatic paragraph direction", new_x="left", new_y="next"
    )
    pdf.cell(text=text1, new_x="left", new_y="next")
    pdf.cell(text=text2, new_x="left", new_y="next")
    pdf.ln()

    pdf.set_text_shaping(use_shaping_engine=True, direction="rtl")
    pdf.cell(
        text="Text shaping-Force paragraph direction RTL", new_x="left", new_y="next"
    )
    pdf.cell(text=text1, new_x="left", new_y="next")
    pdf.cell(text=text2, new_x="left", new_y="next")
    pdf.ln()

    pdf.set_text_shaping(use_shaping_engine=True, direction="ltr")
    pdf.cell(
        text="Text shaping-Force paragraph direction LTR", new_x="left", new_y="next"
    )
    pdf.cell(text=text1, new_x="left", new_y="next")
    pdf.cell(text=text2, new_x="left", new_y="next")
    pdf.ln()

    assert_pdf_equal(
        pdf,
        HERE / "bidi_paragraph_direction.pdf",
        tmp_path,
    )


def test_bidi_get_string_width(tmp_path):
    # issue 1231
    teststrings = [
        "الملوك",
        "الملوك",
        "test",
        "الملوك",
        "الملوك",
        "الملوك",
        "test",
        "الملوك",
        "test",
    ]
    pdf = FPDF()
    pdf.add_page()
    pdf.c_margin = 0
    pdf.add_font("noto", style="", fname=HERE / "NotoNaskhArabic-Regular.ttf")
    pdf.set_text_shaping(use_shaping_engine=True)

    pdf.set_font("noto", size=24)
    pdf.set_draw_color(160)
    pdf.set_line_width(0.3)
    for string in teststrings:
        pdf.set_x(110 - pdf.get_string_width(string))
        pdf.rect(
            pdf.get_x(), pdf.get_y() + 2, pdf.get_string_width(string), 13, style="D"
        )
        pdf.cell(h=17, text=string)
        pdf.ln()
    pdf.ln()
    assert_pdf_equal(pdf, HERE / "bidi_get_string_width.pdf", tmp_path)