"""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}`, 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 ''}'. " 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)