File: test_general.py

package info (click to toggle)
pymupdf 1.25.4%2Bds1-3
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 98,632 kB
  • sloc: python: 43,379; ansic: 75; makefile: 6
file content (1672 lines) | stat: -rw-r--r-- 58,503 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
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
# encoding utf-8
"""
* Confirm sample doc has no links and no annots.
* Confirm proper release of file handles via Document.close()
* Confirm properly raising exceptions in document creation
"""
import io
import os

import pymupdf
import pathlib
import pickle
import platform
import re
import subprocess
import sys
import textwrap
import time

import gentle_compare

scriptdir = os.path.abspath(os.path.dirname(__file__))
filename = os.path.join(scriptdir, "resources", "001003ED.pdf")


def test_haslinks():
    doc = pymupdf.open(filename)
    assert doc.has_links() == False


def test_hasannots():
    doc = pymupdf.open(filename)
    assert doc.has_annots() == False


def test_haswidgets():
    doc = pymupdf.open(filename)
    assert doc.is_form_pdf == False


def test_isrepaired():
    doc = pymupdf.open(filename)
    assert doc.is_repaired == False
    pymupdf.TOOLS.mupdf_warnings()


def test_isdirty():
    doc = pymupdf.open(filename)
    assert doc.is_dirty == False


def test_cansaveincrementally():
    doc = pymupdf.open(filename)
    assert doc.can_save_incrementally() == True


def test_iswrapped():
    doc = pymupdf.open(filename)
    page = doc[0]
    assert page.is_wrapped


def test_wrapcontents():
    doc = pymupdf.open(filename)
    page = doc[0]
    page.wrap_contents()
    xref = page.get_contents()[0]
    cont = page.read_contents()
    doc.update_stream(xref, cont)
    page.set_contents(xref)
    assert len(page.get_contents()) == 1
    page.clean_contents()
    rebased = hasattr(pymupdf, 'mupdf')
    if rebased:
        wt = pymupdf.TOOLS.mupdf_warnings()
        assert wt == 'PDF stream Length incorrect'


def test_page_clean_contents():
    """Assert that page contents cleaning actually is invoked."""
    doc = pymupdf.open()
    page = doc.new_page()

    # draw two rectangles - will lead to two /Contents objects
    page.draw_rect((10, 10, 20, 20))
    page.draw_rect((20, 20, 30, 30))
    assert len(page.get_contents()) == 2
    assert page.read_contents().startswith(b"q") == False

    # clean / consolidate into one /Contents object
    page.clean_contents()
    assert len(page.get_contents()) == 1
    assert page.read_contents().startswith(b"q") == True


def test_annot_clean_contents():
    """Assert that annot contents cleaning actually is invoked."""
    doc = pymupdf.open()
    page = doc.new_page()
    annot = page.add_highlight_annot((10, 10, 20, 20))

    # the annotation appearance will not start with command b"q"


    # invoke appearance stream cleaning and reformatting
    annot.clean_contents()

    # appearance stream should now indeed start with command b"q"
    assert annot._getAP().startswith(b"q") == True


def test_config():
    assert pymupdf.TOOLS.fitz_config["py-memory"] in (True, False)


def test_glyphnames():
    name = "INFINITY"
    infinity = pymupdf.glyph_name_to_unicode(name)
    assert pymupdf.unicode_to_glyph_name(infinity) == name


def test_rgbcodes():
    sRGB = 0xFFFFFF
    assert pymupdf.sRGB_to_pdf(sRGB) == (1, 1, 1)
    assert pymupdf.sRGB_to_rgb(sRGB) == (255, 255, 255)


def test_pdfstring():
    pymupdf.get_pdf_now()
    pymupdf.get_pdf_str("Beijing, chinesisch 北京")
    pymupdf.get_text_length("Beijing, chinesisch 北京", fontname="china-s")
    pymupdf.get_pdf_str("Latin characters êßöäü")


def test_open_exceptions():
    try:
        pymupdf.open(filename, filetype="xps")
    except RuntimeError as e:
        assert repr(e).startswith("FileDataError")
    else:
        assert 0

    try:
        pymupdf.open(filename, filetype="xxx")
    except Exception as e:
        assert repr(e).startswith("ValueError")
    else:
        assert 0

    try:
        pymupdf.open("x.y")
    except Exception as e:
        assert repr(e).startswith("FileNotFoundError")
    else:
        assert 0

    try:
        pymupdf.open(stream=b"", filetype="pdf")
    except RuntimeError as e:
        assert repr(e).startswith("EmptyFileError")
    else:
        assert 0


def test_bug1945():
    pdf = pymupdf.open(f'{scriptdir}/resources/bug1945.pdf')
    buffer_ = io.BytesIO()
    pdf.save(buffer_, clean=True)


def test_bug1971():
    for _ in range(2):
        doc = pymupdf.Document(f'{scriptdir}/resources/bug1971.pdf')
        page = next(doc.pages())
        page.get_drawings()
        doc.close()
        assert doc.is_closed

def test_default_font():
    f = pymupdf.Font()
    assert str(f) == "Font('Noto Serif Regular')"
    assert repr(f) == "Font('Noto Serif Regular')"

def test_add_ink_annot():
    import math
    document = pymupdf.Document()
    page = document.new_page()
    line1 = []
    line2 = []
    for a in range( 0, 360*2, 15):
        x = a
        c = 300 + 200 * math.cos( a * math.pi/180)
        s = 300 + 100 * math.sin( a * math.pi/180)
        line1.append( (x, c))
        line2.append( (x, s))
    page.add_ink_annot( [line1, line2])
    page.insert_text((100, 72), 'Hello world')
    page.add_text_annot((200,200), "Some Text")
    page.get_bboxlog()
    path = f'{scriptdir}/resources/test_add_ink_annot.pdf'
    document.save( path)
    print( f'Have saved to: path={path!r}')

def test_techwriter_append():
    print(pymupdf.__doc__)
    doc = pymupdf.open()
    page = doc.new_page()
    tw = pymupdf.TextWriter(page.rect)
    text = "Red rectangle = TextWriter.text_rect, blue circle = .last_point"
    r = tw.append((100, 100), text)
    print(f'r={r!r}')
    tw.write_text(page)
    page.draw_rect(tw.text_rect, color=pymupdf.pdfcolor["red"])
    page.draw_circle(tw.last_point, 2, color=pymupdf.pdfcolor["blue"])
    path = f"{scriptdir}/resources/test_techwriter_append.pdf"
    doc.ez_save(path)
    print( f'Have saved to: {path}')

def test_opacity():
    doc = pymupdf.open()
    page = doc.new_page()

    annot1 = page.add_circle_annot((50, 50, 100, 100))
    annot1.set_colors(fill=(1, 0, 0), stroke=(1, 0, 0))
    annot1.set_opacity(2 / 3)
    annot1.update(blend_mode="Multiply")

    annot2 = page.add_circle_annot((75, 75, 125, 125))
    annot2.set_colors(fill=(0, 0, 1), stroke=(0, 0, 1))
    annot2.set_opacity(1 / 3)
    annot2.update(blend_mode="Multiply")
    outfile = f'{scriptdir}/resources/opacity.pdf'
    doc.save(outfile, expand=True, pretty=True)
    print("saved", outfile)

def test_get_text_dict():
    import json
    doc=pymupdf.open(f'{scriptdir}/resources/v110-changes.pdf')
    page=doc[0]
    blocks=page.get_text("dict")["blocks"]
    # Check no opaque types in `blocks`.
    json.dumps( blocks, indent=4)

def test_font():
    font = pymupdf.Font()
    print(repr(font))
    bbox = font.glyph_bbox( 65)
    print( f'bbox={bbox!r}')

def test_insert_font():
    doc=pymupdf.open(f'{scriptdir}/resources/v110-changes.pdf')
    page = doc[0]
    i = page.insert_font()
    print( f'page.insert_font() => {i}')

def test_2173():
    from pymupdf import IRect, Pixmap, CS_RGB, Colorspace
    for i in range( 100):
        #print( f'i={i!r}')
        image = Pixmap(Colorspace(CS_RGB), IRect(0, 0, 13, 37))
    print( 'test_2173() finished')

def test_texttrace():
    import time
    document = pymupdf.Document( f'{scriptdir}/resources/joined.pdf')
    t = time.time()
    for page in document:
        tt = page.get_texttrace()
    t = time.time() - t
    print( f'test_texttrace(): t={t!r}')

    # Repeat, this time writing data to file.
    import json
    path = f'{scriptdir}/resources/test_texttrace.txt'
    print( f'test_texttrace(): Writing to: {path}')
    with open( path, 'w') as f:
        for i, page in enumerate(document):
            tt = page.get_texttrace()
            print( f'page {i} json:\n{json.dumps(tt, indent="    ")}', file=f)


def test_2533():
    """Assert correct char bbox in page.get_texttrace().

    Search for a unique char on page and confirm that page.get_texttrace()
    returns the same bbox as the search method.
    """
    if hasattr(pymupdf, 'mupdf') and not pymupdf.g_use_extra:
        print('Not running test_2533() because rebased with use_extra=0 known to fail')
        return
    pymupdf.TOOLS.set_small_glyph_heights(True)
    try:
        doc = pymupdf.open(os.path.join(scriptdir, "resources", "test_2533.pdf"))
        page = doc[0]
        NEEDLE = "民"
        ord_NEEDLE = ord(NEEDLE)
        for span in page.get_texttrace():
            for char in span["chars"]:
                if char[0] == ord_NEEDLE:
                    bbox = pymupdf.Rect(char[3])
                    break
        bbox2 = page.search_for(NEEDLE)[0]
        assert bbox2 == bbox, f'{bbox=} {bbox2=} {bbox2-bbox=}.'
    finally:
        pymupdf.TOOLS.set_small_glyph_heights(False)


def test_2645():
    """Assert same font size calculation in corner cases.
    """
    folder = os.path.join(scriptdir, "resources")
    files = ("test_2645_1.pdf", "test_2645_2.pdf", "test_2645_3.pdf")
    for f in files:
        doc = pymupdf.open(os.path.join(folder, f))
        page = doc[0]
        fontsize0 = page.get_texttrace()[0]["size"]
        fontsize1 = page.get_text("dict", flags=pymupdf.TEXTFLAGS_TEXT)["blocks"][0]["lines"][
            0
        ]["spans"][0]["size"]
        assert abs(fontsize0 - fontsize1) < 1e-5


def test_2506():
    """Ensure expected font size across text writing angles."""
    doc = pymupdf.open()
    page = doc.new_page()
    point = pymupdf.Point(100, 300)  # insertion point
    fontsize = 11  # fontsize
    text = "Hello"  # text
    angles = (0, 30, 60, 90, 120)  # some angles

    # write text with different angles
    for angle in angles:
        page.insert_text(
            point, text, fontsize=fontsize, morph=(point, pymupdf.Matrix(angle))
        )

    # ensure correct fontsize for get_texttrace() - forgiving rounding problems
    for span in page.get_texttrace():
        print(span["dir"])
        assert round(span["size"]) == fontsize

    # ensure correct fontsize for get_text() - forgiving rounding problems
    for block in page.get_text("dict")["blocks"]:
        for line in block["lines"]:
            print(line["dir"])
            for span in line["spans"]:
                print(span["size"])
                assert round(span["size"]) == fontsize


def test_2108():
    doc = pymupdf.open(f'{scriptdir}/resources/test_2108.pdf')
    page = doc[0]
    areas = page.search_for("{sig}")
    rect = areas[0]
    page.add_redact_annot(rect)
    page.apply_redactions()
    text = page.get_text()

    text_expected = b'Frau\nClaire Dunphy\nTeststra\xc3\x9fe 5\n12345 Stadt\nVertragsnummer:  12345\nSehr geehrte Frau Dunphy,\nText\nMit freundlichen Gr\xc3\xbc\xc3\x9fen\nTestfirma\nVertrag:\n  12345\nAnsprechpartner:\nJay Pritchet\nTelefon:\n123456\nE-Mail:\ntest@test.de\nDatum:\n07.12.2022\n'.decode('utf8')

    if 1:
        # Verbose info.
        print(f'test_2108(): text is:\n{text}')
        print(f'')
        print(f'test_2108(): repr(text) is:\n{text!r}')
        print(f'')
        print(f'test_2108(): repr(text.encode("utf8")) is:\n{text.encode("utf8")!r}')
        print(f'')
        print(f'test_2108(): text_expected is:\n{text_expected}')
        print(f'')
        print(f'test_2108(): repr(text_expected) is:\n{text_expected!r}')
        print(f'')
        print(f'test_2108(): repr(text_expected.encode("utf8")) is:\n{text_expected.encode("utf8")!r}')

        ok1 = (text == text_expected)
        ok2 = (text.encode("utf8") == text_expected.encode("utf8"))
        ok3 = (repr(text.encode("utf8")) == repr(text_expected.encode("utf8")))

        print(f'')
        print(f'ok1={ok1}')
        print(f'ok2={ok2}')
        print(f'ok3={ok3}')

        print(f'')

    print(f'{pymupdf.mupdf_version_tuple=}')
    if pymupdf.mupdf_version_tuple >= (1, 21, 2):
        print('Asserting text==text_expected')
        assert text == text_expected
    else:
        print('Asserting text!=text_expected')
        assert text != text_expected


def test_2238():
    filepath = f'{scriptdir}/resources/test2238.pdf'
    doc = pymupdf.open(filepath)
    rebased = hasattr(pymupdf, 'mupdf')
    if rebased:
        wt = pymupdf.TOOLS.mupdf_warnings()
        wt_expected = ''
        if pymupdf.mupdf_version_tuple >= (1, 26):
            wt_expected += 'garbage bytes before version marker\n'
            wt_expected += 'syntax error: expected \'obj\' keyword (6 0 ?)\n'
        else:
            wt_expected += 'format error: cannot recognize version marker\n'
        wt_expected += 'trying to repair broken xref\n'
        wt_expected += 'repairing PDF document'
        assert wt == wt_expected, f'{wt=}'
    first_page = doc.load_page(0).get_text('text', pymupdf.INFINITE_RECT())
    last_page = doc.load_page(-1).get_text('text', pymupdf.INFINITE_RECT())

    print(f'first_page={first_page!r}')
    print(f'last_page={last_page!r}')
    assert first_page == 'Hello World\n'
    assert last_page == 'Hello World\n'

    first_page = doc.load_page(0).get_text('text')
    last_page = doc.load_page(-1).get_text('text')

    print(f'first_page={first_page!r}')
    print(f'last_page={last_page!r}')
    assert first_page == 'Hello World\n'
    assert last_page == 'Hello World\n'


def test_2093():
    doc = pymupdf.open(f'{scriptdir}/resources/test2093.pdf')

    def average_color(page):
        pixmap = page.get_pixmap()
        p_average = [0] * pixmap.n
        for y in range(pixmap.height):
            for x in range(pixmap.width):
                p = pixmap.pixel(x, y)
                for i in range(pixmap.n):
                    p_average[i] += p[i]
        for i in range(pixmap.n):
            p_average[i] /= (pixmap.height * pixmap.width)
        return p_average

    page = doc.load_page(0)
    pixel_average_before = average_color(page)

    rx=135.123
    ry=123.56878
    rw=69.8409
    rh=9.46397

    x0 = rx
    y0 = ry
    x1 = rx + rw
    y1 = ry + rh

    rect = pymupdf.Rect(x0, y0, x1, y1)

    font = pymupdf.Font("Helvetica")
    fill_color=(0,0,0)
    page.add_redact_annot(
        quad=rect,
        #text="null",
        fontname=font.name,
        fontsize=12,
        align=pymupdf.TEXT_ALIGN_CENTER,
        fill=fill_color,
        text_color=(1,1,1),
    )

    page.apply_redactions()
    pixel_average_after = average_color(page)

    print(f'pixel_average_before={pixel_average_before!r}')
    print(f'pixel_average_after={pixel_average_after!r}')

    # Before this bug was fixed (MuPDF-1.22):
    #   pixel_average_before=[130.864323120088, 115.23577810900859, 92.9268559996174]
    #   pixel_average_after=[138.68844553555772, 123.05687162237561, 100.74275056194105]
    # After fix:
    #   pixel_average_before=[130.864323120088, 115.23577810900859, 92.9268559996174]
    #   pixel_average_after=[130.8889209934799, 115.25722751837269, 92.94327384463327]
    #
    for i in range(len(pixel_average_before)):
        diff = pixel_average_before[i] - pixel_average_after[i]
        assert abs(diff) < 0.1

    out = f'{scriptdir}/resources/test2093-out.pdf'
    doc.save(out)
    print(f'Have written to: {out}')


def test_2182():
    print(f'test_2182() started')
    doc = pymupdf.open(f'{scriptdir}/resources/test2182.pdf')
    page = doc[0]
    for annot in page.annots():
        print(annot)
    print(f'test_2182() finished')


def test_2246():
    """
    Test / confirm identical text positions generated by
    * page.insert_text()
    versus
    * TextWriter.write_text()

    ... under varying situations as follows:

    1. MediaBox does not start at (0, 0)
    2. CropBox origin is different from that of MediaBox
    3. Check for all 4 possible page rotations

    The test writes the same text at the same positions using `page.insert_text()`,
    respectively `TextWriter.write_text()`.
    Then extracts the text spans and confirms that they all occupy the same bbox.
    This ensures coincidence of text positions of page.of insert_text()
    (which is assumed correct) and TextWriter.write_text().
    """
    def bbox_count(rot):
        """Make a page and insert identical text via different methods.

        Desired page rotation is a parameter. MediaBox and CropBox are chosen
        to be "awkward": MediaBox does not start at (0,0) and CropBox is a
        true subset of MediaBox.
        """
        # bboxes of spans on page: same text positions are represented by ONE bbox
        bboxes = set()
        doc = pymupdf.open()
        # prepare a page with desired MediaBox / CropBox peculiarities
        mediabox = pymupdf.paper_rect("letter")
        page = doc.new_page(width=mediabox.width, height=mediabox.height)
        xref = page.xref
        newmbox = list(map(float, doc.xref_get_key(xref, "MediaBox")[1][1:-1].split()))
        newmbox = pymupdf.Rect(newmbox)
        mbox = newmbox + (10, 20, 10, 20)
        cbox = mbox + (10, 10, -10, -10)
        doc.xref_set_key(xref, "MediaBox", "[%g %g %g %g]" % tuple(mbox))
        doc.xref_set_key(xref, "CrobBox", "[%g %g %g %g]" % tuple(cbox))
        # set page to desired rotation
        page.set_rotation(rot)
        page.insert_text((50, 50), "Text inserted at (50,50)")
        tw = pymupdf.TextWriter(page.rect)
        tw.append((50, 50), "Text inserted at (50,50)")
        tw.write_text(page)
        blocks = page.get_text("dict")["blocks"]
        for b in blocks:
            for l in b["lines"]:
                for s in l["spans"]:
                    # store bbox rounded to 3 decimal places
                    bboxes.add(pymupdf.Rect(pymupdf.JM_TUPLE3(s["bbox"])))
        return len(bboxes)  # should be 1!

    # the following tests must all pass
    assert bbox_count(0) == 1
    assert bbox_count(90) == 1
    assert bbox_count(180) == 1
    assert bbox_count(270) == 1


def test_2430():
    """Confirm that multiple font property checks will not destroy Py_None."""
    font = pymupdf.Font("helv")
    for i in range(1000):
        _ = font.flags

def test_2692():
    document = pymupdf.Document(f'{scriptdir}/resources/2.pdf')
    for page in document:
        pix = page.get_pixmap(clip=pymupdf.Rect(0,0,10,10))
        dl = page.get_displaylist(annots=True)
        pix = dl.get_pixmap(
                matrix=pymupdf.Identity,
                colorspace=pymupdf.csRGB,
                alpha=False,
                clip=pymupdf.Rect(0,0,10,10),
                )
        pix = dl.get_pixmap(
                matrix=pymupdf.Identity,
                #colorspace=pymupdf.csRGB,
                alpha=False,
                clip=pymupdf.Rect(0,0,10,10),
                )
    

def test_2596():
    """Confirm correctly abandoning cache when reloading a page."""
    doc = pymupdf.Document(f"{scriptdir}/resources/test_2596.pdf")
    page = doc[0]
    pix0 = page.get_pixmap()  # render the page
    _ = doc.tobytes(garbage=3)  # save with garbage collection

    # Note this will invalidate cache content for this page.
    # Reloading the page now empties the cache, so rendering
    # will deliver the same pixmap
    page = doc.reload_page(page)
    pix1 = page.get_pixmap()
    assert pix1.samples == pix0.samples
    rebased = hasattr(pymupdf, 'mupdf')
    if rebased:
        wt = pymupdf.TOOLS.mupdf_warnings()
        assert wt == 'too many indirections (possible indirection cycle involving 24 0 R)'


def test_2730():
    """Ensure identical output across text extractions."""
    doc = pymupdf.open(f"{scriptdir}/resources/test_2730.pdf")
    page = doc[0]
    s1 = set(page.get_text())  # plain text extraction
    s2 = set(page.get_text(sort=True))  # uses "blocks" extraction
    s3 = set(page.get_textbox(page.rect))
    assert s1 == s2
    assert s1 == s3


def test_2553():
    """Ensure identical output across text extractions."""
    verbose = 0
    doc = pymupdf.open(f"{scriptdir}/resources/test_2553.pdf")
    page = doc[0]

    # extract plain text, build set of all characters
    list1 = page.get_text()
    set1 = set(list1)

    # extract text blocks, build set of all characters
    list2 = page.get_text(sort=True)  # internally uses "blocks"
    set2 = set(list2)
    
    # extract textbox content, build set of all characters
    list3 = page.get_textbox(page.rect)
    set3 = set(list3)
    
    def show(l):
        ret = f'len={len(l)}\n'
        for c in l:
            cc = ord(c)
            if (cc >= 32 and cc < 127) or c == '\n':
                ret += c
            else:
                ret += f' [0x{hex(cc)}]'
        return ret
    
    if verbose:
        print(f'list1:\n{show(list1)}')
        print(f'list2:\n{show(list2)}')
        print(f'list3:\n{show(list3)}')
    
    # all sets must be equal
    assert set1 == set2
    assert set1 == set3

    # With mupdf later than 1.23.4, this special page contains no invalid
    # Unicodes.
    #
    print(f'Checking no occurrence of 0xFFFD, {pymupdf.mupdf_version_tuple=}.')
    assert chr(0xFFFD) not in set1

def test_2553_2():
    doc = pymupdf.open(f"{scriptdir}/resources/test_2553-2.pdf")
    page = doc[0]

    # extract plain text, ensure that there are no 0xFFFD characters
    text = page.get_text()
    assert chr(0xfffd) not in text

def test_2635():
    """Rendering a page before and after cleaning it should yield the same pixmap."""
    doc = pymupdf.open(f"{scriptdir}/resources/test_2635.pdf")
    page = doc[0]
    pix1 = page.get_pixmap()  # pixmap before cleaning

    page.clean_contents()  # clean page
    pix2 = page.get_pixmap()  # pixmap after cleaning
    assert pix1.samples == pix2.samples  # assert equality


def test_resolve_names():
    """Test PDF name resolution."""
    # guard against wrong PyMuPDF architecture version
    if not hasattr(pymupdf.Document, "resolve_names"):
        print("PyMuPDF version does not support resolving PDF names")
        return
    pickle_in = open(f"{scriptdir}/resources/cython.pickle", "rb")
    old_names = pickle.load(pickle_in)
    doc = pymupdf.open(f"{scriptdir}/resources/cython.pdf")
    new_names = doc.resolve_names()
    assert new_names == old_names

def test_2777():
    document = pymupdf.Document()
    page = document.new_page()
    print(page.mediabox.width)

def test_2710():
    doc = pymupdf.open(f'{scriptdir}/resources/test_2710.pdf')
    page = doc.load_page(0)
    
    print(f'test_2710(): {page.cropbox=}')
    print(f'test_2710(): {page.mediabox=}')
    print(f'test_2710(): {page.rect=}')
    
    def numbers_approx_eq(a, b):
        return abs(a-b) < 0.001
    def points_approx_eq(a, b):
        return numbers_approx_eq(a.x, b.x) and numbers_approx_eq(a.y, b.y)
    def rects_approx_eq(a, b):
        return points_approx_eq(a.bottom_left, b.bottom_left) and points_approx_eq(a.top_right, b.top_right)
    def assert_rects_approx_eq(a, b):
        assert rects_approx_eq(a, b), f'Not nearly identical: {a=} {b=}'
                
    blocks = page.get_text('blocks')
    print(f'test_2710(): {blocks=}')
    assert len(blocks) == 2
    block = blocks[1]
    rect = pymupdf.Rect(block[:4])
    text = block[4]
    print(f'test_2710(): {rect=}')
    print(f'test_2710(): {text=}')
    assert text == 'Text at left page border\n'
    
    assert_rects_approx_eq(page.cropbox, pymupdf.Rect(30.0, 30.0, 565.3200073242188, 811.9199829101562))
    assert_rects_approx_eq(page.mediabox, pymupdf.Rect(0.0, 0.0, 595.3200073242188, 841.9199829101562))
    print(f'test_2710(): {pymupdf.mupdf_version_tuple=}')
    # 2023-11-05: Currently broken in mupdf master.
    print(f'test_2710(): Not Checking page.rect and rect.')
    rebased = hasattr(pymupdf, 'mupdf')
    if rebased:
        wt = pymupdf.TOOLS.mupdf_warnings()
        assert wt == (
                "syntax error: cannot find ExtGState resource 'GS7'\n"
                "syntax error: cannot find ExtGState resource 'GS8'\n"
                "encountered syntax errors; page may not be correct"
                )


def test_2736():
    """Check handling of CropBox changes vis-a-vis a MediaBox with
       negative coordinates."""
    doc = pymupdf.open()
    page = doc.new_page()

    # fake a MediaBox for demo purposes
    doc.xref_set_key(page.xref, "MediaBox", "[-30 -20 595 842]")

    assert page.cropbox == pymupdf.Rect(-30, 0, 595, 862)
    assert page.rect == pymupdf.Rect(0, 0, 625, 862)

    # change the CropBox: shift by (10, 10) in both dimensions. Please note:
    # To achieve this, 10 must be subtracted from 862! yo must never be negative!
    page.set_cropbox(pymupdf.Rect(-20, 0, 595, 852))

    # get CropBox from the page definition
    assert doc.xref_get_key(page.xref, "CropBox")[1] == "[-20 -10 595 842]"
    assert page.rect == pymupdf.Rect(0, 0, 615, 852)

    error = False
    text = ""
    try:  # check error detection
        page.set_cropbox((-35, -10, 595, 842))
    except Exception as e:
        text = str(e)
        error = True
    assert error == True
    assert text == "CropBox not in MediaBox"


def test_subset_fonts():
    """Confirm subset_fonts is working."""
    if not hasattr(pymupdf, "mupdf"):
        print("Not testing 'test_subset_fonts' in classic.")
        return
    text = "Just some arbitrary text."
    arch = pymupdf.Archive()
    css = pymupdf.css_for_pymupdf_font("ubuntu", archive=arch)
    css += "* {font-family: ubuntu;}"
    doc = pymupdf.open()
    page = doc.new_page()
    page.insert_htmlbox(page.rect, text, css=css, archive=arch)
    doc.subset_fonts(verbose=True)
    found = False
    for xref in range(1, doc.xref_length()):
        if "+Ubuntu#20Regular" in doc.xref_object(xref):
            found = True
            break
    assert found is True


def test_2957_1():
    """Text following a redaction must not change coordinates."""
    # test file with redactions
    doc = pymupdf.open(os.path.join(scriptdir, "resources", "test_2957_1.pdf"))
    page = doc[0]
    # search for string that must not move by redactions
    rects0 = page.search_for("6e9f73dfb4384a2b8af6ebba")
    # sort rectangles vertically
    rects0 = sorted(rects0, key=lambda r: r.y1)
    assert len(rects0) == 2  # must be 2 redactions
    page.apply_redactions()

    # reload page to finalize updates
    page = doc.reload_page(page)

    # the two string must retain their positions (except rounding errors)
    rects1 = page.search_for("6e9f73dfb4384a2b8af6ebba")
    rects1 = sorted(rects1, key=lambda r: r.y1)

    assert page.first_annot is None  # make sure annotations have disappeared
    for i in range(2):
        r0 = rects0[i].irect  # take rounded rects
        r1 = rects1[i].irect
        assert r0 == r1


def test_2957_2():
    """Redacted text must not change positions of remaining text."""
    doc = pymupdf.open(os.path.join(scriptdir, "resources", "test_2957_2.pdf"))
    page = doc[0]
    words0 = page.get_text("words")  # all words before redacting
    page.apply_redactions()  # remove/redact the word "longer"
    words1 = page.get_text("words")  # extract words again
    assert len(words1) == len(words0) - 1  # must be one word less
    assert words0[3][4] == "longer"  # just confirm test file is correct one
    del words0[3]  # remove the redacted word from first list
    for i in range(len(words1)):  # compare words
        w1 = words1[i]  # word after redaction
        bbox1 = pymupdf.Rect(w1[:4]).irect  # its IRect coordinates
        w0 = words0[i]  # word before redaction
        bbox0 = pymupdf.Rect(w0[:4]).irect  # its IRect coordinates
        assert bbox0 == bbox1  # must be same coordinates


def test_707560():
    """https://bugs.ghostscript.com/show_bug.cgi?id=707560
    Ensure that redactions also remove characters with an empty width bbox.
    """
    # Make text that will contain characters with an empty bbox.

    greetings = (
        "Hello, World!",  # english
        "Hallo, Welt!",  # german
        "سلام دنیا!",  # persian
        "வணக்கம், உலகம்!",  # tamil
        "สวัสดีชาวโลก!",  # thai
        "Привіт Світ!",  # ucranian
        "שלום עולם!",  # hebrew
        "ওহে বিশ্ব!",  # bengali
        "你好世界!",  # chinese
        "こんにちは世界!",  # japanese
        "안녕하세요, 월드!",  # korean
        "नमस्कार, विश्व !",  # sanskrit
        "हैलो वर्ल्ड!",  # hindi
    )
    text = " ... ".join([g for g in greetings])
    where = (50, 50, 400, 500)
    story = pymupdf.Story(text)
    bio = io.BytesIO()
    writer = pymupdf.DocumentWriter(bio)
    more = True
    while more:
        dev = writer.begin_page(pymupdf.paper_rect("a4"))
        more, _ = story.place(where)
        story.draw(dev)
        writer.end_page()
    writer.close()
    doc = pymupdf.open("pdf", bio)
    page = doc[0]
    text = page.get_text()
    assert text, "Unexpected: test page has no text."
    page.add_redact_annot(page.rect)
    page.apply_redactions()
    assert not page.get_text(), "Unexpected: text not fully redacted."


def test_3070():
    with pymupdf.open(os.path.abspath(f'{__file__}/../../tests/resources/test_3070.pdf')) as pdf:
      links = pdf[0].get_links()
      links[0]['uri'] = "https://www.ddg.gg"
      pdf[0].update_link(links[0])
      pdf.save(os.path.abspath(f'{__file__}/../../tests/test_3070_out.pdf'))

def test_bboxlog_2885():
    doc = pymupdf.open(os.path.abspath(f'{__file__}/../../tests/resources/test_2885.pdf'))
    page=doc[0]
    
    bbl = page.get_bboxlog()
    if pymupdf.mupdf_version_tuple >= (1, 24, 9):
        wt = pymupdf.TOOLS.mupdf_warnings()
        assert wt == 'invalid marked content and clip nesting'
    
    bbl = page.get_bboxlog(layers=True)
    if pymupdf.mupdf_version_tuple >= (1, 24, 9):
        wt = pymupdf.TOOLS.mupdf_warnings()
        assert wt == 'invalid marked content and clip nesting'

def test_3081():
    '''
    Check Document.close() closes file handles, even if a Page instance exists.
    '''
    path1 = os.path.abspath(f'{__file__}/../../tests/resources/1.pdf')
    path2 = os.path.abspath(f'{__file__}/../../tests/test_3081-2.pdf')
    
    rebased = hasattr(pymupdf, 'mupdf')
    
    import shutil
    import sys
    import traceback
    shutil.copy2(path1, path2)
    
    # Find next two available fds.
    next_fd_1 = os.open(path2, os.O_RDONLY)
    next_fd_2 = os.open(path2, os.O_RDONLY)
    os.close(next_fd_1)
    os.close(next_fd_2)

    def next_fd():
        fd = os.open(path2, os.O_RDONLY)
        os.close(fd)
        return fd
    
    fd1 = next_fd()
    document = pymupdf.open(path2)
    page = document[0]
    fd2 = next_fd()
    document.close()
    if rebased:
        assert document.this is None
        assert page.this is None
    try:
        document.page_count()
    except Exception as e:
        print(f'Received expected exception: {e}')
        #traceback.print_exc(file=sys.stdout)
        assert str(e) == 'document closed'
    else:
        assert 0, 'Did not receive expected exception.'
    fd3 = next_fd()
    try:
        page.bound()
    except Exception as e:
        print(f'Received expected exception: {e}')
        #traceback.print_exc(file=sys.stdout)
        if rebased:
            assert str(e) == 'page is None'
        else:
            assert str(e) == 'orphaned object: parent is None'
    else:
        assert 0, 'Did not receive expected exception.'
    page = None
    fd4 = next_fd()
    print(f'{next_fd_1=} {next_fd_2=}')
    print(f'{fd1=} {fd2=} {fd3=} {fd4=}')
    print(f'{document=}')
    assert fd1 == next_fd_1
    assert fd2 == next_fd_2 # Checks document only uses one fd.
    assert fd3 == next_fd_1 # Checks no leaked fds after document close.
    assert fd4 == next_fd_1 # Checks no leaked fds after failed page access.

def test_xml():
    path = os.path.abspath(f'{__file__}/../../tests/resources/2.pdf')
    with pymupdf.open(path) as document:
        document.get_xml_metadata()

def test_3112_set_xml_metadata():
    document = pymupdf.Document()
    document.set_xml_metadata('hello world')

def test_archive_3126():
    if not hasattr(pymupdf, 'mupdf'):
        print(f'Not running because known to fail with classic.')
        return
    p = os.path.abspath(f'{__file__}/../../tests/resources')
    p = pathlib.Path(p)
    archive = pymupdf.Archive(p)
    
def test_3140():
    if not hasattr(pymupdf, 'mupdf'):
        print(f'Not running test_3140 on classic, because Page.insert_htmlbox() not available.')
        return
    css2 = ''
    path = os.path.abspath(f'{__file__}/../../tests/resources/2.pdf')
    oldfile = os.path.abspath(f'{__file__}/../../tests/test_3140_old.pdf')
    newfile = os.path.abspath(f'{__file__}/../../tests/test_3140_new.pdf')
    import shutil
    shutil.copy2(path, oldfile)
    def next_fd():
        fd = os.open(path, os.O_RDONLY)
        os.close(fd)
        return fd
    fd1 = next_fd()
    with pymupdf.open(oldfile) as doc:  # open document
        page = doc[0]
        rect = pymupdf.Rect(130, 400, 430, 600)
        CELLS = pymupdf.make_table(rect, cols=3, rows=5)
        shape = page.new_shape()  # create Shape
        for i in range(5):
            for j in range(3):
                qtext = "<b>" + "Ques #" + str(i*3+j+1) + ": " + "</b>" # codespell:ignore
                atext = "<b>" + "Ans:" + "</b>" # codespell:ignore
                qtext = qtext + '<br>' + atext
                shape.draw_rect(CELLS[i][j])  # draw rectangle
                page.insert_htmlbox(CELLS[i][j], qtext, css=css2, scale_low=0)
        shape.finish(width=2.5, color=pymupdf.pdfcolor["blue"], )
        shape.commit()  # write all stuff to the page
        doc.subset_fonts()
        doc.ez_save(newfile)
    fd2 = next_fd()
    assert fd2 == fd1, f'{fd1=} {fd2=}'
    os.remove(oldfile)

def test_cli():
    if not hasattr(pymupdf, 'mupdf'):
        print('test_cli(): Not running on classic because of fitz_old.')
        return
    import subprocess
    subprocess.run(f'pymupdf -h', shell=1, check=1)


def check_lines(expected_regexes, actual):
    '''
    Checks lines in <actual> match regexes in <expected_regexes>.
    '''
    print(f'check_lines():', flush=1)
    print(f'{expected_regexes=}', flush=1)
    print(f'{actual=}', flush=1)
    def str_to_list(s):
        if isinstance(s, str):
            return s.split('\n') if s else list()
        return s
    expected_regexes = str_to_list(expected_regexes)
    actual = str_to_list(actual)
    if expected_regexes and expected_regexes[-1]:
        expected_regexes.append('') # Always expect a trailing empty line.
    # Remove `None` regexes and make all regexes match entire lines.
    expected_regexes = [f'^{i}$' for i in expected_regexes if i is not None]
    print(f'{expected_regexes=}', flush=1)
    for expected_regex_line, actual_line in zip(expected_regexes, actual):
        print(f'    {expected_regex_line=}', flush=1)
        print(f'            {actual_line=}', flush=1)
        assert re.match(expected_regex_line, actual_line)
    assert len(expected_regexes) == len(actual), \
            f'expected/actual lines mismatch: {len(expected_regexes)=} {len(actual)=}.'

def test_cli_out():
    '''
    Check redirection of messages and log diagnostics with environment
    variables PYMUPDF_LOG and PYMUPDF_MESSAGE.
    '''
    if not hasattr(pymupdf, 'mupdf'):
        print('test_cli(): Not running on classic because of fitz_old.')
        return
    import platform
    import re
    import subprocess
    log_prefix = None
    if os.environ.get('PYMUPDF_USE_EXTRA') == '0':
        log_prefix = f'.+Using non-default setting from PYMUPDF_USE_EXTRA: \'0\''
    
    def check(
            expect_out,
            expect_err,
            message=None,
            log=None,
            verbose=0,
            ):
        '''
        Sets PYMUPDF_MESSAGE to `message` and PYMUPDF_LOG to `log`, runs
        `pymupdf internal`, and checks lines stdout and stderr match regexes in
        `expect_out` and `expect_err`. Note that we enclose regexes in `^...$`.
        '''
        env = dict()
        if log:
            env['PYMUPDF_LOG'] = log
        if message:
            env['PYMUPDF_MESSAGE'] = message
        env = os.environ | env
        print(f'Running with {env=}: pymupdf internal', flush=1)
        cp = subprocess.run(f'pymupdf internal', shell=1, check=1, capture_output=1, env=env, text=True)
        
        if verbose:
            #print(f'{cp.stdout=}.', flush=1)
            #print(f'{cp.stderr=}.', flush=1)
            sys.stdout.write(f'stdout:\n{textwrap.indent(cp.stdout, "    ")}')
            sys.stdout.write(f'stderr:\n{textwrap.indent(cp.stderr, "    ")}')
        check_lines(expect_out, cp.stdout)
        check_lines(expect_err, cp.stderr)
    
    #
    print(f'Checking default, all output to stdout.')
    check(
            [
                log_prefix,
                'This is from PyMuPDF message[(][)][.]',
                '.+This is from PyMuPDF log[(][)].',
            ],
            '',
            )
    
    #
    if platform.system() != 'Windows':
        print(f'Checking redirection of everything to /dev/null.')
        check('', '', 'path:/dev/null', 'path:/dev/null')
    
    #
    print(f'Checking redirection to files.')
    path_out = os.path.abspath(f'{__file__}/../../tests/test_cli_out.out')
    path_err = os.path.abspath(f'{__file__}/../../tests/test_cli_out.err')
    check('', '', f'path:{path_out}', f'path:{path_err}')
    def read(path):
        with open(path) as f:
            return f.read()
    out = read(path_out)
    err = read(path_err)
    check_lines(['This is from PyMuPDF message[(][)][.]'], out)
    check_lines([log_prefix, '.+This is from PyMuPDF log[(][)][.]'], err)
    
    #
    print(f'Checking redirection to fds.')
    check(
            [
                'This is from PyMuPDF message[(][)][.]',
            ],
            [
                log_prefix,
                '.+This is from PyMuPDF log[(][)].',
            ],
            'fd:1',
            'fd:2',
            )


def test_use_python_logging():
    '''
    Checks pymupdf.use_python_logging().
    '''
    log_prefix = None
    if os.environ.get('PYMUPDF_USE_EXTRA') == '0':
        log_prefix = f'.+Using non-default setting from PYMUPDF_USE_EXTRA: \'0\''
    
    if os.path.basename(__file__).startswith(f'test_fitz_'):
        # Do nothing, because command `pymupdf` outputs diagnostics containing
        # `pymupdf` which are not renamed to `fitz`, which breaks our checking.
        print(f'Not testing with fitz alias.')
        return
    
    def check(
            code,
            regexes_stdout,
            regexes_stderr,
            env = None,
            ):
        code = textwrap.dedent(code)
        path = os.path.abspath(f'{__file__}/../../tests/resources_test_logging.py')
        with open(path, 'w') as f:
            f.write(code)
        command = f'{sys.executable} {path}'
        if env:
            print(f'{env=}.')
            env = os.environ | env
        print(f'Running: {command}', flush=1)
        try:
            cp = subprocess.run(command, shell=1, check=1, capture_output=1, text=True, env=env)
        except Exception as e:
            print(f'Command failed: {command}.', flush=1)
            print(f'Stdout\n{textwrap.indent(e.stdout, "    ")}', flush=1)
            print(f'Stderr\n{textwrap.indent(e.stderr, "    ")}', flush=1)
            raise
        check_lines(regexes_stdout, cp.stdout)
        check_lines(regexes_stderr, cp.stderr)
    
    print(f'## Basic use of `logging` sends output to stderr instead of default stdout.')
    check(
            '''
            import pymupdf
            pymupdf.message('this is pymupdf.message()')
            pymupdf.log('this is pymupdf.log()')
            pymupdf.set_messages(pylogging=1)
            pymupdf.set_log(pylogging=1)
            pymupdf.message('this is pymupdf.message() 2')
            pymupdf.log('this is pymupdf.log() 2')
            ''',
            [
                log_prefix,
                'this is pymupdf.message[(][)]',
                '.+this is pymupdf.log[(][)]',
            ],
            [
                'this is pymupdf.message[(][)] 2',
                '.+this is pymupdf.log[(][)] 2',
            ],
            )
    
    print(f'## Calling logging.basicConfig() makes logging output contain <LEVEL>:<name> prefixes.')
    check(
            '''
            import pymupdf
            
            import logging
            logging.basicConfig()
            pymupdf.set_messages(pylogging=1)
            pymupdf.set_log(pylogging=1)
            
            pymupdf.message('this is pymupdf.message()')
            pymupdf.log('this is pymupdf.log()')
            ''',
            [
                log_prefix,
            ],
            [
                'WARNING:pymupdf:this is pymupdf.message[(][)]',
                'WARNING:pymupdf:.+this is pymupdf.log[(][)]',
            ],
            )
    
    print(f'## Setting PYMUPDF_USE_PYTHON_LOGGING=1 makes PyMuPDF use logging on startup.')
    check(
            '''
            import pymupdf
            pymupdf.message('this is pymupdf.message()')
            pymupdf.log('this is pymupdf.log()')
            ''',
            '',
            [
                log_prefix,
                'this is pymupdf.message[(][)]',
                '.+this is pymupdf.log[(][)]',
            ],
            env = dict(
                    PYMUPDF_MESSAGE='logging:',
                    PYMUPDF_LOG='logging:',
                    ),
            )
    
    print(f'## Pass explicit logger to pymupdf.use_python_logging() with logging.basicConfig().')
    check(
            '''
            import pymupdf
            
            import logging
            logging.basicConfig()
            
            logger = logging.getLogger('foo')
            pymupdf.set_messages(pylogging_logger=logger, pylogging_level=logging.WARNING)
            pymupdf.set_log(pylogging_logger=logger, pylogging_level=logging.ERROR)
            
            pymupdf.message('this is pymupdf.message()')
            pymupdf.log('this is pymupdf.log()')
            ''',
            [
                log_prefix,
            ],
            [
                'WARNING:foo:this is pymupdf.message[(][)]',
                'ERROR:foo:.+this is pymupdf.log[(][)]',
            ],
            )
    
    print(f'## Check pymupdf.set_messages() pylogging_level args.')
    check(
            '''
            import pymupdf
            
            import logging
            logging.basicConfig(level=logging.DEBUG)
            logger = logging.getLogger('pymupdf')
            
            pymupdf.set_messages(pylogging_level=logging.CRITICAL)
            pymupdf.set_log(pylogging_level=logging.INFO)
            
            pymupdf.message('this is pymupdf.message()')
            pymupdf.log('this is pymupdf.log()')
            ''',
            [
                log_prefix,
            ],
            [
                'CRITICAL:pymupdf:this is pymupdf.message[(][)]',
                'INFO:pymupdf:.+this is pymupdf.log[(][)]',
            ],
            )
    
    print(f'## Check messages() with sys.stdout=None.')
    check(
            '''
            import sys
            sys.stdout = None
            import pymupdf
            
            pymupdf.message('this is pymupdf.message()')
            pymupdf.log('this is pymupdf.log()')
            ''',
            [],
            [],
            )
    

def relpath(path, start=None):
    '''
    A 'safe' alternative to os.path.relpath(). Avoids an exception on Windows
    if the drive needs to change - in this case we use os.path.abspath().
    '''
    try:
        return os.path.relpath(path, start)
    except ValueError:
        # os.path.relpath() fails if trying to change drives.
        assert platform.system() == 'Windows'
        return os.path.abspath(path)


def test_open():

    if not hasattr(pymupdf, 'mupdf'):
        print('test_open(): not running on classic.')
        return
    
    import re
    import textwrap
    import traceback
    
    resources = relpath(os.path.abspath(f'{__file__}/../../tests/resources'))
    
    # We convert all strings to use `/` instead of os.sep, which avoids
    # problems with regex's on windows.
    resources = resources.replace(os.sep, '/')
    
    def check(filename=None, stream=None, filetype=None, exception=None):
        '''
        Checks we receive expected exception if specified.
        '''
        if isinstance(filename, str):
            filename = filename.replace(os.sep, '/')
        if exception:
            etype, eregex = exception
            if isinstance(eregex, (tuple, list)):
                # Treat as sequence of regexes to look for.
                eregex = '.*'.join(eregex)
            try:
                pymupdf.open(filename=filename, stream=stream, filetype=filetype)
            except etype as e:
                text = traceback.format_exc(limit=0)
                text = text.replace(os.sep, '/')
                text = textwrap.indent(text, '    ', lambda line: 1)
                assert re.search(eregex, text, re.DOTALL), \
                        f'Incorrect exception text, expected {eregex=}, received:\n{text}'
                print(f'Received expected exception for {filename=} {stream=} {filetype=}:\n{text}')
            except Exception as e:
                assert 0, \
                        f'Incorrect exception, expected {etype}, received {type(e)=}.'
            else:
                assert 0, f'Did not received exception, expected {etype=}.'
        else:
            document = pymupdf.open(filename=filename, stream=stream, filetype=filetype)
            return document
    
    check(f'{resources}/1.pdf')
    
    check(f'{resources}/Bezier.epub')
    
    path = 1234
    etype = TypeError
    eregex = re.escape(f'bad filename: type(filename)=<class \'int\'> filename={path}.')
    check(path, exception=(etype, eregex))
    
    path = 'test_open-this-file-will-not-exist'
    etype = pymupdf.FileNotFoundError
    eregex = f'no such file: \'{path}\''
    check(path, exception=(etype, eregex))
    
    path = resources
    etype = pymupdf.FileDataError
    eregex = re.escape(f'\'{path}\' is no file')
    check(path, exception=(etype, eregex))
    
    path = relpath(os.path.abspath(f'{resources}/../test_open_empty'))
    path = path.replace(os.sep, '/')
    with open(path, 'w') as f:
        pass
    etype = pymupdf.EmptyFileError
    eregex = re.escape(f'Cannot open empty file: filename={path!r}.')
    check(path, exception=(etype, eregex))
    
    path = f'{resources}/1.pdf'
    filetype = 'xps'
    etype = pymupdf.FileDataError
    # 2023-12-12: On OpenBSD, for some reason the SWIG catch code only catches
    # the exception as FzErrorBase.
    etype2 = 'FzErrorBase' if platform.system() == 'OpenBSD' else 'FzErrorFormat'
    eregex = (
            # With a sysinstall with separate MuPDF install, we get
            # `mupdf.FzErrorFormat` instead of `pymupdf.mupdf.FzErrorFormat`. So
            # we just search for the former.
            re.escape(f'mupdf.{etype2}: code=7: cannot recognize zip archive'),
            re.escape(f'pymupdf.FileDataError: Failed to open file {path!r} as type {filetype!r}.'),
            )
    check(path, filetype=filetype, exception=(etype, eregex))
    
    path = f'{resources}/chinese-tables.pickle'
    etype = pymupdf.FileDataError
    etype2 = 'FzErrorBase' if platform.system() == 'OpenBSD' else 'FzErrorUnsupported'
    etext = (
            re.escape(f'mupdf.{etype2}: code=6: cannot find document handler for file: {path}'),
            re.escape(f'pymupdf.FileDataError: Failed to open file {path!r}.'),
            )
    check(path, exception=(etype, etext))
    
    stream = 123
    etype = TypeError
    etext = re.escape('bad stream: type(stream)=<class \'int\'>.')
    check(stream=stream, exception=(etype, etext))
    
    check(stream=b'', exception=(pymupdf.EmptyFileError, re.escape('Cannot open empty stream.')))

def test_533():
    if not hasattr(pymupdf, 'mupdf'):
        print('test_533(): Not running on classic.')
        return
    path = os.path.abspath(f'{__file__}/../../tests/resources/2.pdf')
    doc = pymupdf.open(path)
    print()
    for p in doc:
        print(f'test_533(): for p in doc: {p=}.')
    for p in list(doc)[:]:
        print(f'test_533(): for p in list(doc)[:]: {p=}.')
    for p in doc[:]:
        print(f'test_533(): for p in doc[:]: {p=}.')

def test_3354():
    document = pymupdf.open(filename)
    v = dict(foo='bar')
    document.metadata = v
    assert document.metadata == v

def test_scientific_numbers():
    '''
    This is #3381.
    '''
    doc = pymupdf.open()
    page = doc.new_page(width=595, height=842)
    point = pymupdf.Point(1e-11, -1e-10)
    page.insert_text(point, "Test")
    contents = page.read_contents()
    print(f'{contents=}')
    if pymupdf.mupdf_version_tuple >= (1, 24, 2):
        assert b" 1e-" not in contents
    else:
        assert b" 1e-" in contents

def test_3615():
    print('')
    print(f'{pymupdf.pymupdf_version=}', flush=1)
    print(f'{pymupdf.VersionBind=}', flush=1)
    path = os.path.normpath(f'{__file__}/../../tests/resources/test_3615.epub')
    doc = pymupdf.open(path)
    print(doc.pagemode)
    print(doc.pagelayout)
    wt = pymupdf.TOOLS.mupdf_warnings()
    assert wt

def test_3654():
    path = os.path.normpath(f'{__file__}/../../tests/resources/test_3654.docx')
    content = ""
    with pymupdf.open(path) as document:
        for page in document:
            content += page.get_text() + '\n\n'
    content = content.strip()
    
    if pymupdf.mupdf_version_tuple < (1, 25):
        # As of 2024-07-04 we get a warning for this input file.
        wt = pymupdf.TOOLS.mupdf_warnings()
        assert wt == 'dropping unclosed output'

def test_3727():
    if pymupdf.mupdf_version_tuple < (1, 24, 9):
        print('test_3727(): not running because known to segv: {pymupdf.mupdf_version=}')
        return
    path = os.path.normpath(f'{__file__}/../../tests/resources/test_3727.pdf')
    doc = pymupdf.open(path)
    for page in doc:
        page.get_pixmap(matrix = pymupdf.Matrix(2,2))

def test_3569():
    path = os.path.normpath(f'{__file__}/../../tests/resources/test_3569.pdf')
    document = pymupdf.open(path)
    page = document[0]
    svg = page.get_svg_image(text_as_path=False)
    print(f'{svg=}')
    assert svg == (
            '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" version="1.1" width="3024" height="2160" viewBox="0 0 3024 2160">\n'
            '<defs>\n'
            '<clipPath id="clip_1">\n'
            '<path transform="matrix(0,-.06,-.06,-0,3024,2160)" d="M25432 10909H29692V15642H25432V10909"/>\n'
            '</clipPath>\n'
            '<clipPath id="clip_2">\n'
            '<path transform="matrix(0,-.06,-.06,-0,3024,2160)" d="M28526 38017 31807 40376V40379L31312 41314V42889H28202L25092 42888V42887L28524 38017H28526"/>\n'
            '</clipPath>\n'
            '</defs>\n'
            '<g clip-path="url(#clip_1)">\n'
            '<g inkscape:groupmode="layer" inkscape:label="CED - Text">\n'
            '<text xml:space="preserve" transform="matrix(.06 0 0 .06 3024 2160)" font-size="174.644" font-family="ArialMT"><tspan y="-28538" x="-14909 -14841.063 -14773.127 -14676.024 -14578.922 -14520.766 -14423.663">**L1-13</tspan></text>\n'
            '</g>\n'
            '</g>\n'
            '<g clip-path="url(#clip_2)">\n'
            '<g inkscape:groupmode="layer" inkscape:label="Level 03|S-COLS">\n'
            '<path transform="matrix(0,-.06,-.06,-0,3024,2160)" d="M31130 41483V42083L30530 41483ZM31130 42083 30530 41483V42083Z" fill="#7f7f7f"/>\n'
            '<path transform="matrix(0,-.06,-.06,-0,3024,2160)" stroke-width="0" stroke-linecap="butt" stroke-miterlimit="10" stroke-linejoin="miter" fill="none" stroke="#7f7f7f" d="M31130 41483V42083L30530 41483ZM31130 42083 30530 41483V42083Z"/>\n'
            '<path transform="matrix(0,-.06,-.06,-0,3024,2160)" stroke-width="9" stroke-linecap="round" stroke-linejoin="round" fill="none" stroke="#7f7f7f" d="M30530 41483H31130V42083H30530V41483"/>\n'
            '</g>\n'
            '</g>\n'
            '</svg>\n'
            )
    wt = pymupdf.TOOLS.mupdf_warnings()
    assert wt == 'unknown cid collection: PDFAUTOCAD-Indentity0\nnon-embedded font using identity encoding: ArialMT (mapping via )\ninvalid marked content and clip nesting'

def test_3450():
    # This issue is a slow-down, so we just show time taken - it's not safe
    # to fail if test takes too long because that can give spurious failures
    # depending on hardware etc.
    #
    # On a mac-mini, PyMuPDF-1.24.8 takes 60s, PyMuPDF-1.24.9 takes 4s.
    #
    if os.environ.get('PYMUPDF_RUNNING_ON_VALGRIND') == '1':
        print(f'test_3450(): not running on valgrind because very slow.', flush=1)
        return
    path = os.path.normpath(f'{__file__}/../../tests/resources/test_3450.pdf')
    pdf = pymupdf.open(path)
    page = pdf[0]
    t = time.time()
    pix = page.get_pixmap(alpha=False, dpi=150)
    t = time.time() - t
    print(f'test_3450(): {t=}')

def test_3859():
    if pymupdf.mupdf_version_tuple > (1, 24, 9):
        print(f'{pymupdf.mupdf.PDF_NULL=}.')
        print(f'{pymupdf.mupdf.PDF_TRUE=}.')
        print(f'{pymupdf.mupdf.PDF_FALSE=}.')
        for name in ('NULL', 'TRUE', 'FALSE'):
            name2 = f'PDF_{name}'
            v = getattr(pymupdf.mupdf, name2)
            print(f'{name=} {name2=} {v=} {type(v)=}')
            assert type(v)==pymupdf.mupdf.PdfObj, f'`v` is not a pymupdf.mupdf.PdfObj.'
    else:
        assert not hasattr(pymupdf.mupdf, 'PDF_TRUE')

def test_3905():
    data = b'A,B,C,D\r\n1,2,1,2\r\n2,2,1,2\r\n'
    try:
        document = pymupdf.open(stream=data)
    except pymupdf.FileDataError as e:
        pass
    else:
        assert 0
    wt = pymupdf.TOOLS.mupdf_warnings()
    if pymupdf.mupdf_version_tuple >= (1, 26):
        assert wt == 'format error: cannot find version marker\ntrying to repair broken xref\nrepairing PDF document'
    else:
        assert wt == 'format error: cannot recognize version marker\ntrying to repair broken xref\nrepairing PDF document'

def test_3624():
    path = os.path.normpath(f'{__file__}/../../tests/resources/test_3624.pdf')
    path_png_expected = os.path.normpath(f'{__file__}/../../tests/resources/test_3624_expected.png')
    path_png = os.path.normpath(f'{__file__}/../../tests/test_3624.png')
    with pymupdf.open(path) as document:
        page = document[0]
        pixmap = page.get_pixmap(matrix=pymupdf.Matrix(2, 2))
        print(f'Saving to {path_png=}.')
        pixmap.save(path_png)
        rms = gentle_compare.pixmaps_rms(path_png_expected, path_png)
        if pymupdf.mupdf_version_tuple < (1, 24, 10):
            assert rms > 12
        else:
            # We get small differences in sysinstall tests, where some
            # thirdparty libraries can differ.
            assert rms < 1


def test_4043():
    path = os.path.normpath(f'{__file__}/../../tests/resources/test_4043.pdf')
    doc = pymupdf.open(path)
    doc.fullcopy_page(1)


def test_4018():
    document = pymupdf.open()
    for page in document.pages(-1, -1):
        pass

def test_4034():
    # tests/resources/test_4034.pdf is first two pages of input file in
    # https://github.com/pymupdf/PyMuPDF/issues/4034.
    path = os.path.normpath(f'{__file__}/../../tests/resources/test_4034.pdf')
    path_clean = os.path.normpath(f'{__file__}/../../tests/test_4034_out.pdf')
    with pymupdf.open(path) as document:
        pixmap1 = document[0].get_pixmap()
        document.save(path_clean, clean=1)
    with pymupdf.open(path_clean) as document:
        page = document[0]
        pixmap2 = document[0].get_pixmap()
    rms = gentle_compare.pixmaps_rms(pixmap1, pixmap2)
    print(f'test_4034(): Comparison of original/cleaned page 0 pixmaps: {rms=}.')
    if pymupdf.mupdf_version_tuple < (1, 25, 2):
        assert 30 < rms < 50
    else:
        assert rms == 0

def test_4309():
    document = pymupdf.open()
    page = document.new_page()
    document.delete_page()

def test_4263():
    path = os.path.normpath(f'{__file__}/../../tests/resources/test_4263.pdf')
    path_out = f'{path}.linerarized.pdf'
    command = f'pymupdf clean -linear {path} {path_out}'
    print(f'Running: {command}')
    cp = subprocess.run(command, shell=1, check=0)
    if pymupdf.mupdf_version_tuple < (1, 26):
        assert cp.returncode == 0
    else:
        # Support for linerarisation dropped in MuPDF-1.26.
        assert cp.returncode

def test_4224():
    path = os.path.normpath(f'{__file__}/../../tests/resources/test_4224.pdf')
    with pymupdf.open(path) as document:
        for page in document.pages():
            pixmap = page.get_pixmap(dpi=150)
            path_pixmap = f'{path}.{page.number}.png'
            pixmap.save(path_pixmap)
            print(f'Have created: {path_pixmap}')
    if pymupdf.mupdf_version_tuple < (1, 25, 5):
        wt = pymupdf.TOOLS.mupdf_warnings()
        assert wt == 'format error: negative code in 1d faxd\npadding truncated image'

def test_4319():
    # Have not seen this test reproduce issue #4319, but keeping it anyway.
    path = os.path.normpath(f'{__file__}/../../tests/resources/test_4319.pdf')
    doc = pymupdf.open()
    page = doc.new_page()
    page.insert_text((10, 100), "some text")
    doc.save(path)
    doc.close()
    doc = pymupdf.open(path)
    page = doc[0]
    pc = doc.page_count
    doc.close()
    os.remove(path)
    print(f"removed {doc.name=}")

def test_3886():
    path = os.path.normpath(f'{__file__}/../../tests/resources/test_3886.pdf')
    path_clean0 = os.path.normpath(f'{__file__}/../../tests/resources/test_3886_clean0.pdf')
    path_clean1 = os.path.normpath(f'{__file__}/../../tests/resources/test_3886_clean1.pdf')
    
    with pymupdf.open(path) as document:
        pixmap = document[0].get_pixmap()
        document.save(path_clean0, clean=0)
    
    with pymupdf.open(path) as document:
        document.save(path_clean1, clean=1)
    
    with pymupdf.open(path_clean0) as document:
        pixmap_clean0 = document[0].get_pixmap()
    
    with pymupdf.open(path_clean1) as document:
        pixmap_clean1 = document[0].get_pixmap()
    
    rms_0 = gentle_compare.pixmaps_rms(pixmap, pixmap_clean0)
    rms_1 = gentle_compare.pixmaps_rms(pixmap, pixmap_clean1)
    print(f'test_3886(): {rms_0=} {rms_1=}')