Files
epub2pdf/converters/html_converter.py

68 lines
2.3 KiB
Python

"""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,
)