comparison tests/test_story.py @ 1:1d09e1dec1d9 upstream

ADD: PyMuPDF v1.26.4: the original sdist. It does not yet contain MuPDF. This normally will be downloaded when building PyMuPDF.
author Franz Glasner <fzglas.hg@dom66.de>
date Mon, 15 Sep 2025 11:37:51 +0200
parents
children
comparison
equal deleted inserted replaced
-1:000000000000 1:1d09e1dec1d9
1 import pymupdf
2 import os
3 import textwrap
4
5
6 def test_story():
7 otf = os.path.abspath(f'{__file__}/../resources/PragmaticaC.otf')
8 # 2023-12-06: latest mupdf throws exception if path uses back-slashes.
9 otf = otf.replace('\\', '/')
10 CSS = f"""
11 @font-face {{font-family: test; src: url({otf});}}
12 """
13
14 HTML = """
15 <p style="font-family: test;color: blue">We shall meet again at a place where there is no darkness.</p>
16 """
17
18 MEDIABOX = pymupdf.paper_rect("letter")
19 WHERE = MEDIABOX + (36, 36, -36, -36)
20 # the font files are located in /home/chinese
21 arch = pymupdf.Archive(".")
22 # if not specified user_css, the output pdf has content
23 story = pymupdf.Story(HTML, user_css=CSS, archive=arch)
24
25 writer = pymupdf.DocumentWriter("output.pdf")
26
27 more = 1
28
29 while more:
30 device = writer.begin_page(MEDIABOX)
31 more, _ = story.place(WHERE)
32 story.draw(device)
33 writer.end_page()
34
35 writer.close()
36
37
38 def test_2753():
39
40 def rectfn(rect_num, filled):
41 return pymupdf.Rect(0, 0, 200, 200), pymupdf.Rect(50, 50, 100, 150), None
42
43 def make_pdf(html, path_out):
44 story = pymupdf.Story(html=html)
45 document = story.write_with_links(rectfn)
46 print(f'test_2753(): Writing to: {path_out=}.')
47 document.save(path_out)
48 return document
49
50 doc_before = make_pdf(
51 textwrap.dedent('''
52 <p>Before</p>
53 <p style="page-break-before: always;"></p>
54 <p>After</p>
55 '''),
56 os.path.abspath(f'{__file__}/../../tests/test_2753-out-before.pdf'),
57 )
58
59 doc_after = make_pdf(
60 textwrap.dedent('''
61 <p>Before</p>
62 <p style="page-break-after: always;"></p>
63 <p>After</p>
64 '''),
65 os.path.abspath(f'{__file__}/../../tests/test_2753-out-after.pdf'),
66 )
67
68 path = os.path.normpath(f'{__file__}/../../tests/test_2753_out')
69 doc_before.save(f'{path}_before.pdf')
70 doc_after.save(f'{path}_after.pdf')
71 assert len(doc_before) == 2
72 assert len(doc_after) == 2
73
74 # codespell:ignore-begin
75 springer_html = '''
76 <article>
77 <aside>
78 <img src="springer.jpg">
79 <br><i>Michael Springer ist Schriftsteller und Wis&#173;sen&#173;schafts&#173;publizist. Eine Sammlung seiner Einwürfe ist 2019 als Buch unter dem Titel <b>»Lauter Überraschungen. Was die Wis&#173;senschaft weitertreibt«</b> erschienen.<br><a>www.spektrum.de/artikel/2040277</a></i>
80 </aside>
81 <h1>SPRINGERS EINWÜRFE: INTIME VERBINDUNGEN</h1>
82
83 <h2>Wieso kann unsereins so vieles, was eine Maus nicht kann? Unser Gehirn ist nicht bloß größer, sondern vor allem überraschend vertrackt verdrahtet.</h2>
84
85 <p>Der Heilige Gral der Neu&#173;ro&#173;wis&#173;sen&#173;schaft ist die komplette Kartierung des menschlichen Gehirns – die ge&#173;treue Ab&#173;bildung des Ge&#173;strüpps der Nervenzellen mit den baum&#173;för&#173;mi&#173;gen Ver&#173;ästel&#173;ungen der aus ihnen sprie&#173;ßen&#173;den Den&#173;dri&#173;ten und den viel län&#173;ge&#173;ren Axo&#173;nen, wel&#173;che oft der Sig&#173;nal&#173;über&#173;tragung von einem Sin&#173;nes&#173;or&#173;gan oder zu einer Mus&#173;kel&#173;fa&#173;ser die&#173;nen. Zum Gesamtbild gehören die winzigen Knötchen auf den Dendriten; dort sitzen die Synapsen. Das sind Kontakt- und Schalt&#173;stel&#173;len, leb&#173;haf&#173;te Ver&#173;bin&#173;dungen zu anderen Neuronen.</p>
86
87 <p>Dieses Dickicht bis zur Ebene einzelner Zel&#173;len zu durchforsten und es räumlich dar&#173;zu&#173;stel&#173;len, ist eine gigantische Aufgabe, die bis vor Kurzem utopisch anmuten musste. Neu&#173;er&#173;dings vermag der junge For&#173;schungs&#173;zweig der Konnektomik (von Englisch: con&#173;nect für ver&#173;bin&#173;den) das Zusammenspiel der Neurone immer besser zu verstehen. Das gelingt mit dem Einsatz dreidimensionaler Elek&#173;tro&#173;nen&#173;mik&#173;ros&#173;ko&#173;pie. Aus Dünn&#173;schicht&#173;auf&#173;nah&#173;men von zerebralen Ge&#173;we&#173;be&#173;pro&#173;ben lassen sich plastische Bil&#173;der ganzer Zellverbände zu&#173;sam&#173;men&#173;setzen.</p>
88
89 <p>Da frisches menschliches Hirn&#173;ge&#173;we&#173;be nicht ohne Wei&#173;te&#173;res zu&#173;gäng&#173;lich ist – in der Regel nur nach chirurgischen Eingriffen an Epi&#173;lep&#173;sie&#173;pa&#173;tien&#173;ten –, hält die Maus als Mo&#173;dell&#173;or&#173;ga&#173;nis&#173;mus her. Die evolutionäre Ver&#173;wandt&#173;schaft von Mensch und Nager macht die Wahl plau&#173;sibel. Vor allem das Team um Moritz Helmstaedter am Max-Planck-Institut (MPI) für Hirnforschung in Frankfurt hat in den ver&#173;gangenen Jahren Expertise bei der kon&#173;nek&#173;tomischen Analyse entwickelt.</p>
90
91 <p>Aber steckt in unserem Kopf bloß ein auf die tausendfache Neu&#173;ro&#173;nen&#173;an&#173;zahl auf&#173;ge&#173;bläh&#173;tes Mäu&#173;se&#173;hirn? Oder ist menschliches Ner&#173;ven&#173;ge&#173;we&#173;be viel&#173;leicht doch anders gestrickt? Zur Beantwortung dieser Frage unternahm die MPI-Gruppe einen detaillierten Vergleich von Maus, Makake und Mensch (Science 377, abo0924, 2022).</p>
92
93 <p>Menschliches Gewebe stammte diesmal nicht von Epileptikern, son&#173;dern von zwei wegen Hirntumoren operierten Patienten. Die For&#173;scher wollten damit vermeiden, dass die oft jahrelange Behandlung mit An&#173;ti&#173;epi&#173;lep&#173;ti&#173;ka das Bild der synaptischen Verknüpfungen trübte. Sie verglichen die Proben mit denen eines Makaken und von fünf Mäusen.</p>
94
95 <p>Einerseits ergaben sich – einmal ab&#173;ge&#173;se&#173;hen von den ganz of&#173;fen&#173;sicht&#173;li&#173;chen quan&#173;titativen Unterschieden wie Hirngröße und Neu&#173;ro&#173;nen&#173;anzahl – recht gute Über&#173;ein&#173;stim&#173;mun&#173;gen, die somit den Gebrauch von Tier&#173;modellen recht&#173;fer&#173;ti&#173;gen. Doch in einem Punkt erlebte das MPI-Team eine echte Über&#173;raschung.</p>
96
97 <p>Gewisse Nervenzellen, die so genannten In&#173;ter&#173;neurone, zeichnen sich dadurch aus, dass sie aus&#173;schließ&#173;lich mit anderen Ner&#173;ven&#173;zel&#173;len in&#173;ter&#173;agieren. Solche »Zwi&#173;schen&#173;neu&#173;rone« mit meist kurzen Axonen sind nicht primär für das Verarbeiten externer Reize oder das Aus&#173;lösen körperlicher Reaktionen zuständig; sie be&#173;schäf&#173;ti&#173;gen sich bloß mit der Ver&#173;stär&#173;kung oder Dämpfung interner Signale.</p>
98
99 <p>Just dieser Neuronentyp ist nun bei Makaken und Menschen nicht nur mehr als doppelt so häufig wie bei Mäusen, sondern obendrein be&#173;son&#173;ders intensiv untereinander ver&#173;flochten. Die meisten Interneurone kop&#173;peln sich fast ausschließlich an ihresgleichen. Dadurch wirkt sich ihr konnektomisches Ge&#173;wicht ver&#173;gleichs&#173;weise zehnmal so stark aus.</p>
100
101 <p>Vermutlich ist eine derart mit sich selbst be&#173;schäf&#173;tigte Sig&#173;nal&#173;ver&#173;ar&#173;beitung die Vor&#173;be&#173;ding&#173;ung für ge&#173;stei&#173;gerte Hirn&#173;leis&#173;tungen. Um einen Ver&#173;gleich mit verhältnismäßig pri&#173;mi&#173;ti&#173;ver Tech&#173;nik zu wagen: Bei küns&#173;tli&#173;chen neu&#173;ro&#173;na&#173;len Netzen – Algorithmen nach dem Vor&#173;bild verknüpfter Nervenzellen – ge&#173;nü&#173;gen schon ein, zwei so genannte ver&#173;bor&#173;ge&#173;ne Schich&#173;ten von selbst&#173;be&#173;züg&#173;li&#173;chen Schaltstellen zwischen Input und Output-Ebene, um die ver&#173;blüf&#173;fen&#173;den Erfolge der künstlichen Intel&#173;ligenz her&#173;vor&#173;zu&#173;bringen.</p>
102 </article>
103 '''
104 #codespell:ignore-end
105
106 def test_fit_springer():
107
108 if not hasattr(pymupdf, 'mupdf'):
109 print(f'test_fit_springer(): not running on classic.')
110 return
111
112 verbose = 0
113 story = pymupdf.Story(springer_html)
114
115 def check(call, expected):
116 '''
117 Checks that eval(call) returned parameter=expected. Also creates PDF
118 using path that contains `call` in its leafname,
119 '''
120 fit_result = eval(call)
121
122 print(f'test_fit_springer(): {call=} => {fit_result=}.')
123 if expected is None:
124 assert not fit_result.big_enough
125 else:
126 document = story.write_with_links(lambda rectnum, filled: (fit_result.rect, fit_result.rect, None))
127 path = os.path.abspath(f'{__file__}/../../tests/test_fit_springer_{call}_{fit_result.parameter=}_{fit_result.rect=}.pdf')
128 document.save(path)
129 print(f'Have saved document to {path}.')
130 assert abs(fit_result.parameter-expected) < 0.001, f'{expected=} {fit_result.parameter=}'
131
132 check(f'story.fit_scale(pymupdf.Rect(0, 0, 200, 200), scale_min=1, verbose={verbose})', 3.685728073120117)
133 check(f'story.fit_scale(pymupdf.Rect(0, 0, 595, 842), scale_min=1, verbose={verbose})', 1.0174560546875)
134 check(f'story.fit_scale(pymupdf.Rect(0, 0, 300, 421), scale_min=1, verbose={verbose})', 2.02752685546875)
135 check(f'story.fit_scale(pymupdf.Rect(0, 0, 600, 900), scale_min=1, scale_max=1, verbose={verbose})', 1)
136
137 check(f'story.fit_height(20, verbose={verbose})', 10782.3291015625)
138 check(f'story.fit_height(200, verbose={verbose})', 2437.4990234375)
139 check(f'story.fit_height(2000, verbose={verbose})', 450.2998046875)
140 check(f'story.fit_height(5000, verbose={verbose})', 378.2998046875)
141 check(f'story.fit_height(5500, verbose={verbose})', 378.2998046875)
142
143 check(f'story.fit_width(3000, verbose={verbose})', 167.30859375)
144 check(f'story.fit_width(2000, verbose={verbose})', 239.595703125)
145 check(f'story.fit_width(1000, verbose={verbose})', 510.85546875)
146 check(f'story.fit_width(500, verbose={verbose})', 1622.1272945404053)
147 check(f'story.fit_width(400, verbose={verbose})', 2837.507724761963)
148 check(f'story.fit_width(300, width_max=200000, verbose={verbose})', None)
149 check(f'story.fit_width(200, width_max=200000, verbose={verbose})', None)
150
151 # Run without verbose to check no calls to log() - checked by assert.
152 check('story.fit_scale(pymupdf.Rect(0, 0, 600, 900), scale_min=1, scale_max=1, verbose=0)', 1)
153 check('story.fit_scale(pymupdf.Rect(0, 0, 300, 421), scale_min=1, verbose=0)', 2.02752685546875)
154
155
156 def test_write_stabilized_with_links():
157
158 def rectfn(rect_num, filled):
159 '''
160 We return one rect per page.
161 '''
162 rect = pymupdf.Rect(10, 20, 290, 380)
163 mediabox = pymupdf.Rect(0, 0, 300, 400)
164 #print(f'rectfn(): rect_num={rect_num} filled={filled}')
165 return mediabox, rect, None
166
167 def contentfn(positions):
168 ret = ''
169 ret += textwrap.dedent('''
170 <!DOCTYPE html>
171 <body>
172 <h2>Contents</h2>
173 <ul>
174 ''')
175 for position in positions:
176 if position.heading and (position.open_close & 1):
177 text = position.text if position.text else ''
178 if position.id:
179 ret += f' <li><a href="#{position.id}">{text}</a>'
180 else:
181 ret += f' <li>{text}'
182 ret += f' page={position.page_num}\n'
183 ret += '</ul>\n'
184 ret += textwrap.dedent(f'''
185 <h1>First section</h1>
186 <p>Contents of first section.
187 <ul>
188 <li>External <a href="https://artifex.com/">link to https://artifex.com/</a>.
189 <li><a href="#idtest">Link to IDTEST</a>.
190 <li><a href="#nametest">Link to NAMETEST</a>.
191 </ul>
192
193 <h1>Second section</h1>
194 <p>Contents of second section.
195 <h2>Second section first subsection</h2>
196
197 <p>Contents of second section first subsection.
198 <p id="idtest">IDTEST
199
200 <h1>Third section</h1>
201 <p>Contents of third section.
202 <p><a name="nametest">NAMETEST</a>.
203
204 </body>
205 ''')
206 return ret.strip()
207
208 document = pymupdf.Story.write_stabilized_with_links(contentfn, rectfn)
209
210 # Check links.
211 links = list()
212 for page in document:
213 links += page.get_links()
214 print(f'{len(links)=}.')
215 external_links = dict()
216 for i, link in enumerate(links):
217 print(f' {i}: {link=}')
218 if link.get('kind') == pymupdf.LINK_URI:
219 uri = link['uri']
220 external_links.setdefault(uri, 0)
221 external_links[uri] += 1
222
223 # Check there is one external link.
224 print(f'{external_links=}')
225 if hasattr(pymupdf, 'mupdf'):
226 assert len(external_links) == 1
227 assert 'https://artifex.com/' in external_links
228
229 out_path = __file__.replace('.py', '.pdf')
230 document.save(out_path)
231
232 def test_archive_creation():
233 s = pymupdf.Story(archive=pymupdf.Archive('.'))
234 s = pymupdf.Story(archive='.')
235
236
237 def test_3813():
238 import pymupdf
239
240 HTML = """
241 <p>Count is fine:</p>
242 <ol>
243 <li>Lorem
244 <ol>
245 <li>Sub Lorem</li>
246 <li>Sub Lorem</li>
247 </ol>
248 </li>
249 <li>Lorem</li>
250 <li>Lorem</li>
251 </ol>
252
253 <p>Broken count:</p>
254 <ol>
255 <li>Lorem
256 <ul>
257 <li>Sub Lorem</li>
258 <li>Sub Lorem</li>
259 </ul>
260 </li>
261 <li>Lorem</li>
262 <li>Lorem</li>
263 </ol>
264 """
265 MEDIABOX = pymupdf.paper_rect("A4")
266 WHERE = MEDIABOX + (36, 36, -36, -36)
267
268 story = pymupdf.Story(html=HTML)
269 path = os.path.normpath(f'{__file__}/../../tests/test_3813_out.pdf')
270 writer = pymupdf.DocumentWriter(path)
271
272 more = 1
273
274 while more:
275 device = writer.begin_page(MEDIABOX)
276 more, _ = story.place(WHERE)
277 story.draw(device)
278 writer.end_page()
279
280 writer.close()
281
282 with pymupdf.open(path) as document:
283 page = document[0]
284 text = page.get_text()
285 text_utf8 = text.encode()
286
287 text_expected_utf8 = b'Count is \xef\xac\x81ne:\n1. Lorem\n1. Sub Lorem\n2. Sub Lorem\n2. Lorem\n3. Lorem\nBroken count:\n1. Lorem\n\xe2\x80\xa2 Sub Lorem\n\xe2\x80\xa2 Sub Lorem\n2. Lorem\n3. Lorem\n'
288 text_expected = text_expected_utf8.decode()
289
290 print(f'text_utf8:\n {text_utf8!r}')
291 print(f'text_expected_utf8:\n {text_expected_utf8!r}')
292 print(f'text:\n {textwrap.indent(text, " ")}')
293 print(f'text_expected:\n {textwrap.indent(text_expected, " ")}')
294
295 assert text == text_expected