Mercurial > hgrepos > Python2 > PyMuPDF
diff 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 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test_story.py Mon Sep 15 11:37:51 2025 +0200 @@ -0,0 +1,295 @@ +import pymupdf +import os +import textwrap + + +def test_story(): + otf = os.path.abspath(f'{__file__}/../resources/PragmaticaC.otf') + # 2023-12-06: latest mupdf throws exception if path uses back-slashes. + otf = otf.replace('\\', '/') + CSS = f""" + @font-face {{font-family: test; src: url({otf});}} + """ + + HTML = """ + <p style="font-family: test;color: blue">We shall meet again at a place where there is no darkness.</p> + """ + + MEDIABOX = pymupdf.paper_rect("letter") + WHERE = MEDIABOX + (36, 36, -36, -36) + # the font files are located in /home/chinese + arch = pymupdf.Archive(".") + # if not specified user_css, the output pdf has content + story = pymupdf.Story(HTML, user_css=CSS, archive=arch) + + writer = pymupdf.DocumentWriter("output.pdf") + + more = 1 + + while more: + device = writer.begin_page(MEDIABOX) + more, _ = story.place(WHERE) + story.draw(device) + writer.end_page() + + writer.close() + + +def test_2753(): + + def rectfn(rect_num, filled): + return pymupdf.Rect(0, 0, 200, 200), pymupdf.Rect(50, 50, 100, 150), None + + def make_pdf(html, path_out): + story = pymupdf.Story(html=html) + document = story.write_with_links(rectfn) + print(f'test_2753(): Writing to: {path_out=}.') + document.save(path_out) + return document + + doc_before = make_pdf( + textwrap.dedent(''' + <p>Before</p> + <p style="page-break-before: always;"></p> + <p>After</p> + '''), + os.path.abspath(f'{__file__}/../../tests/test_2753-out-before.pdf'), + ) + + doc_after = make_pdf( + textwrap.dedent(''' + <p>Before</p> + <p style="page-break-after: always;"></p> + <p>After</p> + '''), + os.path.abspath(f'{__file__}/../../tests/test_2753-out-after.pdf'), + ) + + path = os.path.normpath(f'{__file__}/../../tests/test_2753_out') + doc_before.save(f'{path}_before.pdf') + doc_after.save(f'{path}_after.pdf') + assert len(doc_before) == 2 + assert len(doc_after) == 2 + +# codespell:ignore-begin +springer_html = ''' +<article> +<aside> +<img src="springer.jpg"> +<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> +</aside> +<h1>SPRINGERS EINWÜRFE: INTIME VERBINDUNGEN</h1> + +<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> + +<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> + +<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> + +<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> + +<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> + +<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> + +<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> + +<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> + +<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> + +<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> +</article> +''' +#codespell:ignore-end + +def test_fit_springer(): + + if not hasattr(pymupdf, 'mupdf'): + print(f'test_fit_springer(): not running on classic.') + return + + verbose = 0 + story = pymupdf.Story(springer_html) + + def check(call, expected): + ''' + Checks that eval(call) returned parameter=expected. Also creates PDF + using path that contains `call` in its leafname, + ''' + fit_result = eval(call) + + print(f'test_fit_springer(): {call=} => {fit_result=}.') + if expected is None: + assert not fit_result.big_enough + else: + document = story.write_with_links(lambda rectnum, filled: (fit_result.rect, fit_result.rect, None)) + path = os.path.abspath(f'{__file__}/../../tests/test_fit_springer_{call}_{fit_result.parameter=}_{fit_result.rect=}.pdf') + document.save(path) + print(f'Have saved document to {path}.') + assert abs(fit_result.parameter-expected) < 0.001, f'{expected=} {fit_result.parameter=}' + + check(f'story.fit_scale(pymupdf.Rect(0, 0, 200, 200), scale_min=1, verbose={verbose})', 3.685728073120117) + check(f'story.fit_scale(pymupdf.Rect(0, 0, 595, 842), scale_min=1, verbose={verbose})', 1.0174560546875) + check(f'story.fit_scale(pymupdf.Rect(0, 0, 300, 421), scale_min=1, verbose={verbose})', 2.02752685546875) + check(f'story.fit_scale(pymupdf.Rect(0, 0, 600, 900), scale_min=1, scale_max=1, verbose={verbose})', 1) + + check(f'story.fit_height(20, verbose={verbose})', 10782.3291015625) + check(f'story.fit_height(200, verbose={verbose})', 2437.4990234375) + check(f'story.fit_height(2000, verbose={verbose})', 450.2998046875) + check(f'story.fit_height(5000, verbose={verbose})', 378.2998046875) + check(f'story.fit_height(5500, verbose={verbose})', 378.2998046875) + + check(f'story.fit_width(3000, verbose={verbose})', 167.30859375) + check(f'story.fit_width(2000, verbose={verbose})', 239.595703125) + check(f'story.fit_width(1000, verbose={verbose})', 510.85546875) + check(f'story.fit_width(500, verbose={verbose})', 1622.1272945404053) + check(f'story.fit_width(400, verbose={verbose})', 2837.507724761963) + check(f'story.fit_width(300, width_max=200000, verbose={verbose})', None) + check(f'story.fit_width(200, width_max=200000, verbose={verbose})', None) + + # Run without verbose to check no calls to log() - checked by assert. + check('story.fit_scale(pymupdf.Rect(0, 0, 600, 900), scale_min=1, scale_max=1, verbose=0)', 1) + check('story.fit_scale(pymupdf.Rect(0, 0, 300, 421), scale_min=1, verbose=0)', 2.02752685546875) + + +def test_write_stabilized_with_links(): + + def rectfn(rect_num, filled): + ''' + We return one rect per page. + ''' + rect = pymupdf.Rect(10, 20, 290, 380) + mediabox = pymupdf.Rect(0, 0, 300, 400) + #print(f'rectfn(): rect_num={rect_num} filled={filled}') + return mediabox, rect, None + + def contentfn(positions): + ret = '' + ret += textwrap.dedent(''' + <!DOCTYPE html> + <body> + <h2>Contents</h2> + <ul> + ''') + for position in positions: + if position.heading and (position.open_close & 1): + text = position.text if position.text else '' + if position.id: + ret += f' <li><a href="#{position.id}">{text}</a>' + else: + ret += f' <li>{text}' + ret += f' page={position.page_num}\n' + ret += '</ul>\n' + ret += textwrap.dedent(f''' + <h1>First section</h1> + <p>Contents of first section. + <ul> + <li>External <a href="https://artifex.com/">link to https://artifex.com/</a>. + <li><a href="#idtest">Link to IDTEST</a>. + <li><a href="#nametest">Link to NAMETEST</a>. + </ul> + + <h1>Second section</h1> + <p>Contents of second section. + <h2>Second section first subsection</h2> + + <p>Contents of second section first subsection. + <p id="idtest">IDTEST + + <h1>Third section</h1> + <p>Contents of third section. + <p><a name="nametest">NAMETEST</a>. + + </body> + ''') + return ret.strip() + + document = pymupdf.Story.write_stabilized_with_links(contentfn, rectfn) + + # Check links. + links = list() + for page in document: + links += page.get_links() + print(f'{len(links)=}.') + external_links = dict() + for i, link in enumerate(links): + print(f' {i}: {link=}') + if link.get('kind') == pymupdf.LINK_URI: + uri = link['uri'] + external_links.setdefault(uri, 0) + external_links[uri] += 1 + + # Check there is one external link. + print(f'{external_links=}') + if hasattr(pymupdf, 'mupdf'): + assert len(external_links) == 1 + assert 'https://artifex.com/' in external_links + + out_path = __file__.replace('.py', '.pdf') + document.save(out_path) + +def test_archive_creation(): + s = pymupdf.Story(archive=pymupdf.Archive('.')) + s = pymupdf.Story(archive='.') + + +def test_3813(): + import pymupdf + + HTML = """ + <p>Count is fine:</p> + <ol> + <li>Lorem + <ol> + <li>Sub Lorem</li> + <li>Sub Lorem</li> + </ol> + </li> + <li>Lorem</li> + <li>Lorem</li> + </ol> + + <p>Broken count:</p> + <ol> + <li>Lorem + <ul> + <li>Sub Lorem</li> + <li>Sub Lorem</li> + </ul> + </li> + <li>Lorem</li> + <li>Lorem</li> + </ol> + """ + MEDIABOX = pymupdf.paper_rect("A4") + WHERE = MEDIABOX + (36, 36, -36, -36) + + story = pymupdf.Story(html=HTML) + path = os.path.normpath(f'{__file__}/../../tests/test_3813_out.pdf') + writer = pymupdf.DocumentWriter(path) + + more = 1 + + while more: + device = writer.begin_page(MEDIABOX) + more, _ = story.place(WHERE) + story.draw(device) + writer.end_page() + + writer.close() + + with pymupdf.open(path) as document: + page = document[0] + text = page.get_text() + text_utf8 = text.encode() + + 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' + text_expected = text_expected_utf8.decode() + + print(f'text_utf8:\n {text_utf8!r}') + print(f'text_expected_utf8:\n {text_expected_utf8!r}') + print(f'text:\n {textwrap.indent(text, " ")}') + print(f'text_expected:\n {textwrap.indent(text_expected, " ")}') + + assert text == text_expected
