Files

56 lines
2.1 KiB
Python

"""SVG -> PDF (vector render via WeasyPrint).
SVG is XML by spec, so the document is first well-formedness-checked with
lxml — an unbalanced file (e.g. "<svg><g></path>") is a *job* error with
"Invalid SVG: <reason>", never a silent blank render (HTML5 foreign-content
parsing would otherwise swallow the mismatch and succeed with an empty page).
The SVG is then embedded verbatim in a minimal A4 HTML document; WeasyPrint
renders SVG natively as a vector (text stays sharp, no rasterization). A
forced page break guarantees at least one PDF page even for a degenerate
document. A WeasyPrint render failure is also reported as "Invalid SVG: ...".
"""
from __future__ import annotations
from pathlib import Path
from lxml import etree
from . import common
_DOC = (
"<!DOCTYPE html><html><head><style>"
"@page { size: A4; margin: 20mm; }"
"body { margin: 0; font-kerning: none; }"
"svg { max-width: 100%; height: auto; }"
"</style></head><body>{svg}<div style='page-break-after: always'></div>"
"</body></html>"
)
def convert(path: Path, job_id: str, progress_cb=None, output_dir=None,
source_name=None) -> str:
"""Render an SVG file to a vector PDF."""
raw = path.read_bytes()
if not raw.strip():
raise ValueError("empty file: nothing to convert")
svg_text = common.decode_text(raw)
try:
etree.fromstring(svg_text.encode("utf-8"))
except etree.XMLSyntaxError as exc:
raise ValueError(f"Invalid SVG: {exc}") from exc
doc = _DOC.replace("{svg}", svg_text)
out_dir = common.default_output_dir(output_dir)
common.report(progress_cb, 30.0, f"Rendering {path.name} (1/1)")
try:
pdf_bytes = common.html_to_pdf_bytes(doc)
except ValueError:
raise # empty-file errors keep their own message
except Exception as exc: # noqa: BLE001 - render failure is a job error
raise ValueError(f"Invalid SVG: {exc}") from exc
filename = common.derive_export_name(source_name or path.name, out_dir)
return common.finalize_pdf(pdf_bytes, out_dir, filename, job_id,
progress_cb=progress_cb)