Skip to content

visualdynamics.report.pdf

pdf

Print a rendered report page to a paginated PDF.

The PDF is the report page itself, printed by the web engine the report editor already uses (Qt WebEngine's printToPdf), so what is issued is what the page shows. The page is loaded with #print, which draws its figures at print resolution, and its print stylesheet keeps a figure off a page break. With the report feature report_identity on, the page's margins carry the laboratory, the report's number and revision, the title and page n of N, numbered by the print engine's own page counters.

The printing runs in the web engine's processes while the application's thread turns the event loop, so a window stays painted. Called from a worker thread, the print is handed to the application's thread and the worker waits for it. It needs a Qt application, and makes one when a script has none.

Functions:

Name Description
print_pdf

Print page (a report rendered by render_html) to a PDF at

Functions:

print_pdf

print_pdf(page: str, path: str | PathLike, *, page_size: str = 'letter', timeout: float = 300.0) -> bytes

Print page (a report rendered by render_html) to a PDF at path, and return the PDF's bytes.

page_size is 'letter' or 'a4'. The PDF is printed beside the page in a temporary folder and moved to path only once it is whole, so a failed print leaves path as it was.

Raises:

Type Description
ValueError

For a page size that is not one of PAGE_SIZES.

OSError

When the page does not load, or the print fails or does not finish within timeout seconds.

Source code in src/visualdynamics/report/pdf.py
def print_pdf(page: str, path: str | os.PathLike, *,
              page_size: str = 'letter', timeout: float = 300.0) -> bytes:
    """Print `page` (a report rendered by `render_html`) to a PDF at
    `path`, and return the PDF's bytes.

    `page_size` is 'letter' or 'a4'. The PDF is printed beside the page
    in a temporary folder and moved to `path` only once it is whole, so
    a failed print leaves `path` as it was.

    Raises
    ------
    ValueError
        For a page size that is not one of `PAGE_SIZES`.
    OSError
        When the page does not load, or the print fails or does not
        finish within `timeout` seconds.
    """
    if page_size not in PAGE_SIZES:
        raise ValueError(f'unknown page size {page_size!r}; the sizes are: '
                         f'{", ".join(PAGE_SIZES)}')
    from PySide6.QtCore import QMarginsF, QUrl
    from PySide6.QtGui import QPageLayout, QPageSize
    from PySide6.QtWebEngineCore import QWebEnginePage
    from PySide6.QtWebEngineWidgets import QWebEngineView

    app = _application()
    if not app.thread().isCurrentThread():
        return _on_gui_thread(app, lambda: print_pdf(
            page, path, page_size=page_size, timeout=timeout))
    target = os.path.expanduser(str(path))
    size = (QPageSize.PageSizeId.Letter if page_size == 'letter'
            else QPageSize.PageSizeId.A4)
    # the margins the identity rides in (`identity.MARGINS`, which the
    # page's own print stylesheet also states, for a page printed from
    # a browser)
    from .identity import MARGINS

    top, side = MARGINS
    layout = QPageLayout(QPageSize(size), QPageLayout.Orientation.Portrait,
                         QMarginsF(side, top, side, top),
                         QPageLayout.Unit.Inch)
    with tempfile.TemporaryDirectory(prefix='vd-print-') as folder:
        source = os.path.join(folder, 'report.html')
        printed = os.path.join(folder, 'report.pdf')
        with open(source, 'w', encoding='utf-8') as out:
            out.write(page)
        view = QWebEngineView()
        web = QWebEnginePage()
        view.setPage(web)
        view.resize(PRINT_WIDTH, 900)
        state: dict[str, Any] = {}
        web.loadFinished.connect(lambda ok: state.update(loaded=ok))
        web.pdfPrintingFinished.connect(
            lambda _file, ok: state.update(printed=ok))
        url = QUrl.fromLocalFile(source)
        url.setFragment('print')
        try:
            view.load(url)
            if not _wait(app, lambda: 'loaded' in state, timeout) \
                    or not state['loaded']:
                raise OSError(f'the report page did not load within '
                              f'{timeout:g} s, so nothing was printed')
            web.printToPdf(printed, layout)
            if not _wait(app, lambda: 'printed' in state, timeout):
                raise OSError(f'the PDF was not finished within {timeout:g} '
                              's; nothing was written')
            if not state['printed'] or not os.path.isfile(printed):
                raise OSError(f'the web engine could not print {target}')
            with open(printed, 'rb') as handle:
                data = handle.read()
            shutil.move(printed, target)
        finally:
            _stand_down(app, view, web)
    return data