96 lines
3.6 KiB
Python
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)
|