"""--split, --from-md, YAML-Kopf und --jpeg-quality.""" import zipfile import pymupdf import pytest import pdf2md as p from conftest import needs_pandoc from test_e2e import run BOOK = """# Das Buch ## Inhaltsverzeichnis Teil I 1 Anfang ## Teil I: Der Anfang Einleitung zum Teil. ### 1 Anfang Text eins. ``` # kein Kapitel, nur ein Kommentar ``` ### 2 Weiter Text zwei. ## Impressum Alle Rechte vorbehalten. """ def test_frontmatter_round_trip(): meta = {"title": "Ein \"Buch\": Teil 1", "author": "Max Muster", "chapter": 3, "leer": ""} text = p.make_frontmatter(meta) + "# Text\n" parsed, body = p.split_frontmatter(text) assert parsed == {"title": "Ein \"Buch\": Teil 1", "author": "Max Muster", "chapter": 3} assert body == "# Text\n" def test_text_without_frontmatter_is_unchanged(): assert p.split_frontmatter("---\n\nText\n") == ({}, "---\n\nText\n") def test_slugify(): assert p.slugify("2 Ein Brief – eine Liste – ein Vermächtnis") == "2-ein-brief-eine-liste-ein-vermaechtnis" assert p.slugify("Größe & Maß") == "groesse-mass" assert p.slugify("!!!") == "abschnitt" def test_split_by_chapters(): sections = p.split_markdown(BOOK, "kapitel") assert [s["title"] for s in sections] == [ "Vorspann", "Teil I: Der Anfang", "1 Anfang", "2 Weiter", "Impressum", ] assert sections[0]["body"].startswith("# Das Buch") and "## Inhaltsverzeichnis" in sections[0]["body"] assert sections[2]["part"] == "Teil I: Der Anfang" and sections[2]["chapter"] == 1 assert "# kein Kapitel" in sections[2]["body"] # Codeblock wird nicht geschnitten assert "".join(s["body"] for s in sections).replace("\n", "") == BOOK.replace("\n", "") def test_split_by_parts(): sections = p.split_markdown(BOOK, "teile") assert [s["title"] for s in sections] == ["Vorspann", "Teil I: Der Anfang", "Impressum"] assert "### 2 Weiter" in sections[1]["body"] def test_write_split_and_read_back(tmp_path): sections = p.split_markdown(BOOK, "kapitel") files = p.write_split(tmp_path / "buch", sections, {"title": "Das Buch", "author": "Max"}) assert [f.name for f in files][:3] == ["00_vorspann.md", "01_teil-i-der-anfang.md", "02_1-anfang.md"] meta, body, base = p.read_markdown_source(tmp_path / "buch") assert meta["title"] == "Das Buch" and base == tmp_path / "buch" assert body.replace("\n", "") == BOOK.replace("\n", "") head, _ = p.split_frontmatter(files[2].read_text()) assert head == {"title": "1 Anfang", "part": "Teil I: Der Anfang", "chapter": 1, "order": 2, "book": "Das Buch", "author": "Max"} def test_missing_index_entry_is_reported(tmp_path): folder = tmp_path / "buch" p.write_split(folder, p.split_markdown(BOOK, "teile"), {"title": "X"}) (folder / "01_teil-i-der-anfang.md").unlink() with pytest.raises(RuntimeError, match="nicht vorhanden"): p.read_markdown_source(folder) # --- Ende zu Ende ------------------------------------------------------------------ def test_cli_split_writes_folder(pdfs, tmp_path): run(pdfs["digital"], tmp_path / "buch.md", "--split", "kapitel", "--images") folder = tmp_path / "buch" names = sorted(f.name for f in folder.glob("*.md")) assert names[0] == "00_vorspann.md" and "index.md" in names assert any(n.endswith("_1-der-notar.md") for n in names) assert (folder / "images" / "cover.jpg").is_file() text = "".join(f.read_text() for f in folder.glob("*.md")) assert "](images/" in text and "_images/" not in text @needs_pandoc def test_from_md_folder_follows_index_order(pdfs, tmp_path): run(pdfs["digital"], tmp_path / "buch.md", "--split", "kapitel", "--epub") index = tmp_path / "buch" / "index.md" lines = index.read_text().split("\n") i = next(n for n, line in enumerate(lines) if "1-der-notar" in line) j = next(n for n, line in enumerate(lines) if "2-ein-brief" in line) lines[i], lines[j] = lines[j], lines[i] index.write_text("\n".join(lines)) run("--from-md", tmp_path / "buch", "--epub-file", tmp_path / "neu.epub") with zipfile.ZipFile(tmp_path / "neu.epub") as z: nav = z.read("EPUB/nav.xhtml").decode() opf = z.read("EPUB/content.opf").decode() assert nav.index("2 Ein Brief") < nav.index("1 Der Notar") assert "Erika Muster" in opf and "cover" in opf @needs_pandoc def test_from_md_file_uses_frontmatter_and_overrides(pdfs, tmp_path): run(pdfs["digital"], tmp_path / "buch.md", "--images") md = tmp_path / "buch.md" md.write_text(md.read_text().replace("Der Regen", "Der starke Regen")) run("--from-md", md, "--title", "Neuer Titel") with zipfile.ZipFile(tmp_path / "buch.epub") as z: opf = z.read("EPUB/content.opf").decode() text = "".join(z.read(n).decode() for n in z.namelist() if n.endswith(".xhtml")) assert ">Neuer Titel<" in opf and "Erika Muster" in opf and "cover" in opf assert "Der starke Regen" in text and "title:" not in text def test_jpeg_quality_changes_image_size(pdfs, tmp_path): def size(quality): out = tmp_path / f"q{quality}" / "buch.md" run(pdfs["digital"], out, "--images", "--jpeg-quality", str(quality)) return sum(f.stat().st_size for f in (out.parent / "buch_images").glob("*.jpg")) assert size(30) < size(95) # --- Seitenzahlen, Teilen nach Länge ----------------------------------------------- def _book_pdf(outline: bool): doc = pymupdf.open() texts = [ "Inhalt\n1 Anfang\n2 Weiter\n3 Schluss", "1 Anfang\nText eins.", "Noch mehr Text\nzum Anfang,\nüber mehrere\nZeilen.\n2 Weiter\nText zwei.", "3 Schluss\nText drei.", ] for text in texts: doc.new_page().insert_text((72, 72), text, fontsize=11) if outline: doc.set_toc([[1, "1 Anfang", 2], [1, "2 Weiter", 3], [1, "3 Schluss", 4]]) return doc @pytest.mark.parametrize("outline", [True, False]) def test_source_pages(outline): sections = [{"title": t} for t in ("Vorspann", "1 Anfang", "2 Weiter", "3 Schluss")] p.add_source_pages(_book_pdf(outline), sections) assert [s["source_pages"] for s in sections] == [[1, 1], [2, 3], [3, 3], [4, 4]] def test_split_without_headings_by_length(): paragraphs = [f"Absatz {n} " + " ".join(["Text"] * 40) for n in range(30)] code = "```\n" + "\n\n".join(f"zeile_{n} = {n}" for n in range(20)) + "\n```" markdown = "\n\n".join(paragraphs[:15] + [code] + paragraphs[15:]) + "\n" assert p.split_markdown(markdown, "kapitel")[0]["title"] == "Abschnitt 1" sections = p.split_by_length(markdown, size=1500) assert len(sections) > 3 assert [s["title"] for s in sections[:2]] == ["Abschnitt 1", "Abschnitt 2"] assert all(s["body"].count("```") in (0, 2) for s in sections) # Code bleibt ganz assert "".join(s["body"] for s in sections).replace("\n", "") == markdown.replace("\n", "") def test_cli_split_writes_source_pages(pdfs, tmp_path): run(pdfs["digital"], tmp_path / "buch.md", "--split", "kapitel") files = {f.name: p.split_frontmatter(f.read_text())[0] for f in (tmp_path / "buch").glob("0*.md")} notar = next(meta for name, meta in files.items() if name.endswith("_1-der-notar.md")) assert notar["source_pages"] == [2, 3] # --- Links mit eigenem Text -------------------------------------------------------- def _link_pdf(path): doc = pymupdf.open() page = doc.new_page() page.insert_text((72, 72), "Den ganzen Bericht finden Sie hier im Netz.", fontsize=11) page.insert_text((72, 90), "Siehe auch www.example.org/info zum Thema.", fontsize=11) for word, uri in (("hier", "https://beispiel.de/bericht?utm_source=x"), ("www.example.org/info", "https://www.example.org/info")): rect = page.search_for(word)[0] page.insert_link({"kind": pymupdf.LINK_URI, "from": rect, "uri": uri}) doc.save(path) return path def test_load_link_anchors_skips_url_texts(tmp_path): doc = pymupdf.open(_link_pdf(tmp_path / "links.pdf")) assert p.load_link_anchors(doc) == [ ("https://beispiel.de/bericht", ["Bericht", "finden", "Sie"], ["hier"]) ] def test_link_anchors_uses_context_and_skips_existing_links(): anchors = [("https://a.de/x", ["finden", "Sie"], ["hier"])] md = "Hier steht hier. Den Bericht finden Sie hier im Netz.\n" assert p.link_anchors(md, anchors) == ( "Hier steht hier. Den Bericht finden Sie [hier](https://a.de/x) im Netz.\n" ) # Fußnotenziffer und Trennung zwischen Kontext und Linktext stören nicht anchors = [("https://b.de/", ["überprüfen.81"], ["Landes-", "medienanstalten"])] md = "Die Landesmedienanstalten.\n\nzu überprüfen.[81 ] Die Landesmedienanstalten prüfen.\n" anchors = [("https://b.de/", ["überprüfen.81", "Die"], ["Landes-", "medienanstalten"])] assert "Die [Landesmedienanstalten](https://b.de/) prüfen" in p.link_anchors(md, anchors) def test_cli_sets_anchor_links(tmp_path): out = tmp_path / "links.md" run(_link_pdf(tmp_path / "links.pdf"), out) md = out.read_text() assert "[hier](https://beispiel.de/bericht)" in md assert "" in md or "[www.example.org/info]" in md