modm_data.pdf2html.convert

  1# Copyright 2022, Niklas Hauser
  2# SPDX-License-Identifier: MPL-2.0
  3
  4import gc
  5import os
  6import hashlib
  7from anytree import RenderTree, PreOrderIter
  8from lxml import etree
  9from typing import Iterable
 10
 11from .html import format_document, write_html
 12from .render import annotate_debug_info
 13from ..utils import pkg_apply_patch, pkg_file_exists, apply_patch
 14from .ast import merge_area
 15from pathlib import Path
 16import pypdfium2 as pp
 17
 18
 19def _write_figures(root, html_file: Path, folder: Path, names: set):
 20    """
 21    Writes the vector graphics of all figures as SVG files and their bitmap
 22    images in their embedded format into the folder, so that the HTML only
 23    contains references to them.
 24
 25    The folder is shared by all HTML files of a document, which may be written
 26    by separate processes that each convert their own pages. The files are
 27    therefore named after the figure number and page, and the images after
 28    their content, which also saves identical images only once, like the
 29    hundreds of tiles of a pattern.
 30    """
 31    html_file, folder = Path(html_file), Path(folder)
 32    for node in PreOrderIter(root, filter_=lambda n: n.name == "figure"):
 33        # The page keeps the name unique, if the figure numbers start over
 34        name = "figure_" + (f"{node.number}_" if node.number >= 0 else "") + f"p{node.obj._page.number}"
 35        # Figures without caption and side-by-side figures share their name
 36        name = next(n for n in [name] + [f"{name}_{ii}" for ii in range(1, 100)] if n not in names)
 37        names.add(name)
 38
 39        files = []
 40        for image in node.obj.images:
 41            suffix, data = image.encode()
 42            files.append(f"image_{hashlib.sha1(data).hexdigest()[:16]}{suffix}")
 43            folder.mkdir(parents=True, exist_ok=True)
 44            (folder / files[-1]).write_bytes(data)
 45        if (svg := node.obj.as_svg(files)) is not None:
 46            folder.mkdir(parents=True, exist_ok=True)
 47            etree.ElementTree(svg).write(folder / f"{name}.svg", pretty_print=True)
 48            # The SVG draws the images itself, so the HTML only references the SVG
 49            files = [f"{name}.svg"]
 50        relative = Path(os.path.relpath(folder, html_file.parent))
 51        node._srcs = [(relative / file).as_posix() for file in dict.fromkeys(files)]
 52
 53
 54def convert(
 55    doc: pp.PdfDocument,
 56    page_range: Iterable[int],
 57    output_path: Path,
 58    figures_path: Path = None,
 59    format_chapters: bool = False,
 60    pretty: bool = True,
 61    render_html: bool = True,
 62    render_pdf: bool = False,
 63    render_all: bool = False,
 64    show_ast: bool = False,
 65    show_tree: bool = False,
 66    show_tags: bool = False,
 67) -> bool:
 68    document = None
 69    debug_doc = None
 70    debug_index = 0
 71    for page in doc.pages(page_range):
 72        # The previous pages create millions of long-lived objects, which the
 73        # garbage collector would otherwise traverse again in every collection
 74        # of the oldest generation, taking a third of the conversion time.
 75        gc.freeze()
 76        if not render_all and not page.is_relevant:
 77            continue
 78        print(f"\n\n=== {page.top} #{page.number} ===\n")
 79
 80        if show_tags:
 81            for struct in page.structures:
 82                print(struct.descr())
 83
 84        if show_tree or render_html or show_ast:
 85            areas = page.content_ast
 86            if show_ast:
 87                print()
 88                for area in areas:
 89                    print(RenderTree(area))
 90            if show_tree or render_html:
 91                for area in areas:
 92                    document = merge_area(document, area)
 93
 94        if render_pdf:
 95            debug_doc = annotate_debug_info(page, debug_doc, debug_index)
 96            debug_index += 1
 97
 98    if render_pdf:
 99        with open(f"debug_{output_path.stem}.pdf", "wb") as file_handle:
100            pp.PdfDocument(debug_doc).save(file_handle)
101
102    if show_tree or render_html:
103        if document is None:
104            print("No pages parsed, empty document!")
105            return True
106
107        document = doc._normalize(document)
108        if show_tree:
109            print(RenderTree(document))
110
111        if render_html:
112            names = set()
113            if format_chapters:
114                for chapter in document.children:
115                    if chapter.name == "chapter":
116                        print(f"\nFormatting HTML for '{chapter.title}'")
117                        output_file = f"{output_path}/chapter_{chapter._filename}.html"
118                        _write_figures(chapter, output_file, figures_path or output_path / "figures", names)
119                        html = format_document(chapter)
120                        print(f"\nWriting HTML '{output_file}'")
121                        write_html(html, output_file, pretty=pretty)
122            else:
123                print("\nFormatting HTML")
124                _write_figures(document, output_path, figures_path or output_path.with_suffix("") / "figures", names)
125                html = format_document(document)
126                print(f"\nWriting HTML '{str(output_path)}'")
127                write_html(html, str(output_path), pretty=pretty)
128
129    return True
130
131
132def patch(doc, data_module, output_path: Path, patch_file: Path = None) -> bool:
133    if patch_file is None:
134        # First try the patch file for the specific version
135        patch_file = f"{doc.name}.patch"
136        if not pkg_file_exists(data_module, patch_file):
137            # Then try the patch file shared between versions
138            patch_file = f"{doc.name.split('-')[0]}.patch"
139            if not pkg_file_exists(data_module, patch_file):
140                return True
141        return pkg_apply_patch(data_module, patch_file, output_path)
142    return apply_patch(patch_file, output_path)
def convert( doc: pypdfium2._helpers.document.PdfDocument, page_range: Iterable[int], output_path: pathlib.Path, figures_path: pathlib.Path = None, format_chapters: bool = False, pretty: bool = True, render_html: bool = True, render_pdf: bool = False, render_all: bool = False, show_ast: bool = False, show_tree: bool = False, show_tags: bool = False) -> bool:
 55def convert(
 56    doc: pp.PdfDocument,
 57    page_range: Iterable[int],
 58    output_path: Path,
 59    figures_path: Path = None,
 60    format_chapters: bool = False,
 61    pretty: bool = True,
 62    render_html: bool = True,
 63    render_pdf: bool = False,
 64    render_all: bool = False,
 65    show_ast: bool = False,
 66    show_tree: bool = False,
 67    show_tags: bool = False,
 68) -> bool:
 69    document = None
 70    debug_doc = None
 71    debug_index = 0
 72    for page in doc.pages(page_range):
 73        # The previous pages create millions of long-lived objects, which the
 74        # garbage collector would otherwise traverse again in every collection
 75        # of the oldest generation, taking a third of the conversion time.
 76        gc.freeze()
 77        if not render_all and not page.is_relevant:
 78            continue
 79        print(f"\n\n=== {page.top} #{page.number} ===\n")
 80
 81        if show_tags:
 82            for struct in page.structures:
 83                print(struct.descr())
 84
 85        if show_tree or render_html or show_ast:
 86            areas = page.content_ast
 87            if show_ast:
 88                print()
 89                for area in areas:
 90                    print(RenderTree(area))
 91            if show_tree or render_html:
 92                for area in areas:
 93                    document = merge_area(document, area)
 94
 95        if render_pdf:
 96            debug_doc = annotate_debug_info(page, debug_doc, debug_index)
 97            debug_index += 1
 98
 99    if render_pdf:
100        with open(f"debug_{output_path.stem}.pdf", "wb") as file_handle:
101            pp.PdfDocument(debug_doc).save(file_handle)
102
103    if show_tree or render_html:
104        if document is None:
105            print("No pages parsed, empty document!")
106            return True
107
108        document = doc._normalize(document)
109        if show_tree:
110            print(RenderTree(document))
111
112        if render_html:
113            names = set()
114            if format_chapters:
115                for chapter in document.children:
116                    if chapter.name == "chapter":
117                        print(f"\nFormatting HTML for '{chapter.title}'")
118                        output_file = f"{output_path}/chapter_{chapter._filename}.html"
119                        _write_figures(chapter, output_file, figures_path or output_path / "figures", names)
120                        html = format_document(chapter)
121                        print(f"\nWriting HTML '{output_file}'")
122                        write_html(html, output_file, pretty=pretty)
123            else:
124                print("\nFormatting HTML")
125                _write_figures(document, output_path, figures_path or output_path.with_suffix("") / "figures", names)
126                html = format_document(document)
127                print(f"\nWriting HTML '{str(output_path)}'")
128                write_html(html, str(output_path), pretty=pretty)
129
130    return True
def patch( doc, data_module, output_path: pathlib.Path, patch_file: pathlib.Path = None) -> bool:
133def patch(doc, data_module, output_path: Path, patch_file: Path = None) -> bool:
134    if patch_file is None:
135        # First try the patch file for the specific version
136        patch_file = f"{doc.name}.patch"
137        if not pkg_file_exists(data_module, patch_file):
138            # Then try the patch file shared between versions
139            patch_file = f"{doc.name.split('-')[0]}.patch"
140            if not pkg_file_exists(data_module, patch_file):
141                return True
142        return pkg_apply_patch(data_module, patch_file, output_path)
143    return apply_patch(patch_file, output_path)