Mercurial > hgrepos > Python2 > PyMuPDF
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­sen­schafts­publizist. Eine Sammlung seiner Einwürfe ist 2019 als Buch unter dem Titel <b>»Lauter Überraschungen. Was die Wis­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­ro­wis­sen­schaft ist die komplette Kartierung des menschlichen Gehirns – die ge­treue Ab­bildung des Ge­strüpps der Nervenzellen mit den baum­för­mi­gen Ver­ästel­ungen der aus ihnen sprie­ßen­den Den­dri­ten und den viel län­ge­ren Axo­nen, wel­che oft der Sig­nal­über­tragung von einem Sin­nes­or­gan oder zu einer Mus­kel­fa­ser die­nen. Zum Gesamtbild gehören die winzigen Knötchen auf den Dendriten; dort sitzen die Synapsen. Das sind Kontakt- und Schalt­stel­len, leb­haf­te Ver­bin­dungen zu anderen Neuronen.</p> | |
| 86 | |
| 87 <p>Dieses Dickicht bis zur Ebene einzelner Zel­len zu durchforsten und es räumlich dar­zu­stel­len, ist eine gigantische Aufgabe, die bis vor Kurzem utopisch anmuten musste. Neu­er­dings vermag der junge For­schungs­zweig der Konnektomik (von Englisch: con­nect für ver­bin­den) das Zusammenspiel der Neurone immer besser zu verstehen. Das gelingt mit dem Einsatz dreidimensionaler Elek­tro­nen­mik­ros­ko­pie. Aus Dünn­schicht­auf­nah­men von zerebralen Ge­we­be­pro­ben lassen sich plastische Bil­der ganzer Zellverbände zu­sam­men­setzen.</p> | |
| 88 | |
| 89 <p>Da frisches menschliches Hirn­ge­we­be nicht ohne Wei­te­res zu­gäng­lich ist – in der Regel nur nach chirurgischen Eingriffen an Epi­lep­sie­pa­tien­ten –, hält die Maus als Mo­dell­or­ga­nis­mus her. Die evolutionäre Ver­wandt­schaft von Mensch und Nager macht die Wahl plau­sibel. Vor allem das Team um Moritz Helmstaedter am Max-Planck-Institut (MPI) für Hirnforschung in Frankfurt hat in den ver­gangenen Jahren Expertise bei der kon­nek­tomischen Analyse entwickelt.</p> | |
| 90 | |
| 91 <p>Aber steckt in unserem Kopf bloß ein auf die tausendfache Neu­ro­nen­an­zahl auf­ge­bläh­tes Mäu­se­hirn? Oder ist menschliches Ner­ven­ge­we­be viel­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­dern von zwei wegen Hirntumoren operierten Patienten. Die For­scher wollten damit vermeiden, dass die oft jahrelange Behandlung mit An­ti­epi­lep­ti­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­ge­se­hen von den ganz of­fen­sicht­li­chen quan­titativen Unterschieden wie Hirngröße und Neu­ro­nen­anzahl – recht gute Über­ein­stim­mun­gen, die somit den Gebrauch von Tier­modellen recht­fer­ti­gen. Doch in einem Punkt erlebte das MPI-Team eine echte Über­raschung.</p> | |
| 96 | |
| 97 <p>Gewisse Nervenzellen, die so genannten In­ter­neurone, zeichnen sich dadurch aus, dass sie aus­schließ­lich mit anderen Ner­ven­zel­len in­ter­agieren. Solche »Zwi­schen­neu­rone« mit meist kurzen Axonen sind nicht primär für das Verarbeiten externer Reize oder das Aus­lösen körperlicher Reaktionen zuständig; sie be­schäf­ti­gen sich bloß mit der Ver­stär­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­son­ders intensiv untereinander ver­flochten. Die meisten Interneurone kop­peln sich fast ausschließlich an ihresgleichen. Dadurch wirkt sich ihr konnektomisches Ge­wicht ver­gleichs­weise zehnmal so stark aus.</p> | |
| 100 | |
| 101 <p>Vermutlich ist eine derart mit sich selbst be­schäf­tigte Sig­nal­ver­ar­beitung die Vor­be­ding­ung für ge­stei­gerte Hirn­leis­tungen. Um einen Ver­gleich mit verhältnismäßig pri­mi­ti­ver Tech­nik zu wagen: Bei küns­tli­chen neu­ro­na­len Netzen – Algorithmen nach dem Vor­bild verknüpfter Nervenzellen – ge­nü­gen schon ein, zwei so genannte ver­bor­ge­ne Schich­ten von selbst­be­züg­li­chen Schaltstellen zwischen Input und Output-Ebene, um die ver­blüf­fen­den Erfolge der künstlichen Intel­ligenz her­vor­zu­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 |
