Files

112 lines
4.5 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.
# Office trio: the ONLY family gated by render_mode (rich->LibreOffice,
# standard->pandoc). All other extensions route via _ROUTING unchanged.
_OFFICE_EXTS = frozenset({".docx", ".odt", ".rtf"})
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, render_mode="rich") -> 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.
`render_mode` ("rich" default | "standard") gates ONLY the office trio
(.docx/.odt/.rtf): rich -> LibreOffice, standard -> pandoc. Every other
extension (incl. .tex/.latex/.org/.adoc/.rst, which stay pandoc-only) and
.epub route exactly as before. An omitted/invalid render_mode is "rich".
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)
# AC (matrix HAPPY_PATH_STANDARD / HAPPY_PATH_RICH): the office trio is the
# only family gated by render_mode. rich -> LibreOffice, standard -> pandoc.
# .tex/.latex/.org/.adoc/.rst stay pandoc-only (the pandoc module itself
# gates tex via \documentclass); every other extension routes as before.
if ext in _OFFICE_EXTS:
module_name = "office_converter" if render_mode == "rich" else "pandoc_converter"
else:
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)