Files
pesco-ncr/backend/app/services/pdf.py

66 lines
2.1 KiB
Python
Raw Normal View History

"""Printable NCR PDF (WeasyPrint) — a clean single-document rendering of the
complete NCR for hard-copy travelers and audits.
WeasyPrint is imported lazily so environments without the Pango/Cairo system
libraries (e.g. unit tests) can still import the app.
"""
from functools import partial
from pathlib import Path
import anyio
from jinja2 import Environment, FileSystemLoader, select_autoescape
from app.domain import STAGE_LABELS, Stage
from app.models import Ncr
from app.models.base import utcnow
from app.services.storage import attachment_abs_path
_TEMPLATES_DIR = Path(__file__).resolve().parent.parent / "templates"
_env = Environment(
loader=FileSystemLoader(_TEMPLATES_DIR),
autoescape=select_autoescape(["html"]),
)
def _render_html(ncr: Ncr, related_ncr_numbers: list[str]) -> str:
images = []
other_files = []
for att in ncr.attachments:
entry = {
"filename": att.original_filename,
"uploaded_by": att.uploaded_by.display_name,
"uploaded_at": att.uploaded_at,
"size_kb": max(1, att.size_bytes // 1024),
}
path = attachment_abs_path(att.stored_path)
if att.is_image and path.is_file():
entry["src"] = path.as_uri()
images.append(entry)
else:
other_files.append(entry)
template = _env.get_template("ncr_pdf.html")
return template.render(
ncr=ncr,
stage_label=STAGE_LABELS[Stage(ncr.stage)],
stage_labels=STAGE_LABELS,
Stage=Stage,
images=images,
other_files=other_files,
related_ncr_numbers=related_ncr_numbers,
generated_at=utcnow(),
)
def _html_to_pdf(html: str) -> bytes:
from weasyprint import HTML # lazy: needs Pango/Cairo system libs
return HTML(string=html).write_pdf()
async def render_ncr_pdf(ncr: Ncr, related_ncr_numbers: list[str] | None = None) -> bytes:
html = _render_html(ncr, related_ncr_numbers or [])
# WeasyPrint rendering is CPU-bound; keep it off the event loop.
return await anyio.to_thread.run_sync(partial(_html_to_pdf, html))