Files
epub2pdf/converters/__init__.py
T

96 lines
3.6 KiB
Python

"""converters — PHASE 1 multi-format dispatch (any common file -> PDF).
Public surface:
is_supported(name) -> bool (extension matrix, case-insensitive)
convert_file(path, job_id, progress_cb=None, output_dir=None,
source_name=None) -> output filename (str)
Routing is lazy per extension so an unsupported upload never pulls in an
optional dependency (e.g. PIL) it would not need. .epub routes to the
existing EPUB pipeline (app.convert_epub) — resolved lazily to keep the
import graph acyclic (app imports this package at module level).
Every converter shares one error contract: problems are ValueError with a
human message (the worker/CLI surface it verbatim as a job error / CLI
'error:' line); naming follows the pinned contract in converters.common.
"""
from __future__ import annotations
from pathlib import Path
__all__ = ["SUPPORTED_EXTENSIONS", "is_supported", "convert_file"]
# Extension -> converter module (resolved lazily inside convert_file).
_ROUTING: dict[str, str] = {
".html": "html_converter",
".htm": "html_converter",
".txt": "text_converter",
".log": "text_converter",
".md": "md_converter",
".markdown": "md_converter",
".csv": "csv_converter",
".tsv": "csv_converter",
".json": "json_converter",
".xml": "xml_converter",
".svg": "svg_converter",
".png": "image_converter",
".jpg": "image_converter",
".jpeg": "image_converter",
".webp": "image_converter",
".tiff": "image_converter",
".bmp": "image_converter",
".pdf": "pdf_passthrough",
# PHASE 2 (Chunk A): office / LaTeX / structured text via Pandoc ->
# standalone HTML5 -> the shared WeasyPrint render (pandoc_converter).
".docx": "pandoc_converter",
".odt": "pandoc_converter",
".rtf": "pandoc_converter",
".tex": "pandoc_converter",
".latex": "pandoc_converter",
".org": "pandoc_converter",
".adoc": "pandoc_converter",
".rst": "pandoc_converter",
}
# .epub is supported (existing pipeline) but routes differently.
SUPPORTED_EXTENSIONS = frozenset(_ROUTING) | {".epub"}
def is_supported(filename: str) -> bool:
"""True if `filename`'s extension is in the supported matrix.
Case-insensitive; extensionless names are unsupported.
"""
if not filename:
return False
ext = Path(filename).suffix.lower()
return ext in SUPPORTED_EXTENSIONS
def convert_file(path: Path, job_id: str, progress_cb=None, output_dir=None,
source_name=None) -> str:
"""Convert `path` to a PDF; return the output file name (no dir).
`source_name` is the original upload/CLI file name — the on-disk name
for web uploads is `{job_id}<ext>`, so the export name must be derived
from the original name, not the temp path.
Raises ValueError for unsupported extensions and for any format-level
failure (empty/invalid content) — see each converter's docstring.
"""
ext = Path(path).suffix.lower()
if ext not in SUPPORTED_EXTENSIONS:
raise ValueError(
f"Unsupported file type: '{ext or '<none>'}'. "
f"Supported: {', '.join(sorted(SUPPORTED_EXTENSIONS))}"
)
if ext == ".epub":
import app # lazy: the EPUB pipeline lives in app.py (avoid cycle)
return app.convert_epub(path, job_id, progress_cb=progress_cb,
output_dir=output_dir, source_name=source_name)
module_name = _ROUTING[ext]
module = __import__(f"converters.{module_name}", fromlist=["convert"])
return module.convert(Path(path), job_id, progress_cb=progress_cb,
output_dir=output_dir, source_name=source_name)