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,85 @@
|
||||
"""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",
|
||||
}
|
||||
|
||||
# .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)
|
||||
Reference in New Issue
Block a user