any-file to PDF (Phase 1): converters/ dispatcher, images/text/md/csv/json/xml/svg/pdf-passthrough, 112 tests green
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
"""HTML / HTM -> PDF.
|
||||
|
||||
A standalone .html upload is rendered as-is: the document's own <style>/
|
||||
<link> rules stay authoritative. app.BASE_CSS is injected at the TOP of
|
||||
<head> as a fallback layer (equal-specificity rules lose to the document's
|
||||
own later declarations, same layering as the EPUB path). Fragments without
|
||||
a <head> ("<p>...</p>") render bare.
|
||||
|
||||
Declared charset is honored (windows-1252/latin-1 <meta charset> or XML
|
||||
declaration) via the pinned regexes in app — no mojibake. The document
|
||||
<title> becomes the export name and PDF /Title when present, else the
|
||||
file stem.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from . import common
|
||||
|
||||
_TITLE_RE = re.compile(r"<title[^>]*>(.*?)</title>", re.IGNORECASE | re.DOTALL)
|
||||
|
||||
|
||||
def _extract_title(html_text: str) -> str | None:
|
||||
m = _TITLE_RE.search(html_text)
|
||||
if not m:
|
||||
return None
|
||||
title = re.sub(r"<[^>]+>", "", m.group(1))
|
||||
title = " ".join(title.split())
|
||||
return title or None
|
||||
|
||||
|
||||
def _inject_base_css(html_text: str) -> str:
|
||||
"""Insert BASE_CSS as a fallback <style> at the top of <head>.
|
||||
|
||||
Placed before the document's own styles so, at equal specificity,
|
||||
the document wins (WeasyPrint resolves source order within one
|
||||
stylesheet context like a browser). Fragments without <head> are
|
||||
returned unchanged (a lone <style> outside <head> is legal HTML5
|
||||
and still applies).
|
||||
"""
|
||||
import app # lazy: BASE_CSS lives with the EPUB pipeline
|
||||
|
||||
style = f"<style>\n{app.BASE_CSS}\n</style>"
|
||||
m = re.search(r"<head[^>]*>", html_text, re.IGNORECASE)
|
||||
if m:
|
||||
return html_text[: m.end()] + style + html_text[m.end():]
|
||||
return style + html_text
|
||||
|
||||
|
||||
def convert(path: Path, job_id: str, progress_cb=None, output_dir=None,
|
||||
source_name=None) -> str:
|
||||
"""Render a standalone HTML/HTM file to a typeset PDF."""
|
||||
raw = path.read_bytes()
|
||||
html_text = common.decode_declared(raw)
|
||||
if not html_text.strip():
|
||||
raise ValueError("empty file: nothing to convert")
|
||||
title = _extract_title(html_text)
|
||||
|
||||
def _build():
|
||||
return _inject_base_css(html_text), path.name
|
||||
|
||||
return common.convert_single(
|
||||
path, job_id, build_html=_build, title=title, css="",
|
||||
progress_cb=progress_cb, output_dir=output_dir,
|
||||
name_src=source_name or path.name,
|
||||
)
|
||||
Reference in New Issue
Block a user