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,66 @@
|
||||
"""PDF -> PDF passthrough (.pdf uploads).
|
||||
|
||||
A valid, unencrypted PDF is validated with pypdf and passed through to the
|
||||
output with the standard naming/collision contract, re-stamping /Title from
|
||||
the uploaded filename stem. It reuses common.finalize_pdf so the atomic
|
||||
write path (.tmp + os.replace) and metadata handling are identical to every
|
||||
other converter.
|
||||
|
||||
Rejections (all ValueError -> surfaced verbatim as a job error / CLI error):
|
||||
- corrupt / unparseable -> "Invalid PDF: ... could not be parsed"
|
||||
- encrypted / password -> "PDF rejected: ... encrypted or password-protected"
|
||||
- more than MAX_PAGES -> "PDF too large: ... pages (max N)"
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
from pathlib import Path
|
||||
|
||||
from pypdf import PdfReader
|
||||
|
||||
from . import common
|
||||
|
||||
# req 1 (pdf passthrough): upper bound on page count for a pass-through PDF.
|
||||
MAX_PAGES = 500
|
||||
|
||||
|
||||
def convert(path: Path, job_id: str, progress_cb=None, output_dir=None,
|
||||
source_name=None) -> str:
|
||||
"""Validate a PDF and pass it through to a same-named output PDF.
|
||||
|
||||
Satisfies: req 1 (pdf passthrough — validate, stamp /Title, standard
|
||||
naming/collision, atomic write).
|
||||
"""
|
||||
name = source_name or path.name
|
||||
stem = Path(name).stem or "book"
|
||||
raw = path.read_bytes()
|
||||
|
||||
# req 1: validate with pypdf. Construction raises on header/body corruption.
|
||||
try:
|
||||
reader = PdfReader(io.BytesIO(raw))
|
||||
except Exception as exc: # noqa: BLE001 - pypdf raises many corrupt-file types
|
||||
raise ValueError(f"Invalid PDF: {name} could not be parsed ({exc})") from exc
|
||||
|
||||
# req 1: reject encrypted / password-protected BEFORE touching .pages
|
||||
# (accessing pages on an encrypted reader raises, not a clean error).
|
||||
if reader.is_encrypted:
|
||||
raise ValueError(f"PDF rejected: {name} is encrypted or password-protected")
|
||||
|
||||
# req 1: count pages; a corrupt body beyond a valid header fails here.
|
||||
try:
|
||||
page_count = len(reader.pages)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
raise ValueError(f"Invalid PDF: {name} could not be parsed ({exc})") from exc
|
||||
|
||||
# req 1: reject oversized documents (clear error, distinct from corrupt).
|
||||
if page_count > MAX_PAGES:
|
||||
raise ValueError(
|
||||
f"PDF too large: {name} has {page_count} pages (max {MAX_PAGES})"
|
||||
)
|
||||
|
||||
# req 1: stamp /Title from the uploaded filename stem; standard naming/collision.
|
||||
common.report(progress_cb, 30.0, f"Passing through {name} (1/1)")
|
||||
out_dir = common.default_output_dir(output_dir)
|
||||
filename = common.derive_export_name(name, out_dir)
|
||||
return common.finalize_pdf(raw, out_dir, filename, job_id,
|
||||
progress_cb=progress_cb, title=stem)
|
||||
Reference in New Issue
Block a user