Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a99996bd9c |
@@ -34,7 +34,7 @@ def dispatch(path, quality, maxsize, maxzoom, data=None):
|
||||
backend = "unknown"
|
||||
try:
|
||||
if data:
|
||||
backend = "pyvips"
|
||||
backend = "vips"
|
||||
return process_image_buffer(
|
||||
data, quality=quality, maxsize=maxsize, maxzoom=maxzoom
|
||||
)
|
||||
@@ -47,11 +47,11 @@ def dispatch(path, quality, maxsize, maxzoom, data=None):
|
||||
backend = "video"
|
||||
return process_video(path, quality=quality, maxsize=maxsize)
|
||||
if mime_type and mime_type.startswith("image/"):
|
||||
backend = "pyvips"
|
||||
backend = "vips"
|
||||
return process_image(path, quality=quality, maxsize=maxsize)
|
||||
except PreviewError:
|
||||
# Already structured (e.g. a stage of a combined pipeline like
|
||||
# pdf+pyvips) — keep the original backend/stage identity.
|
||||
# Already structured (e.g. a failing stage of a combined pipeline
|
||||
# like pdf+vips) — keep the original backend identity.
|
||||
raise
|
||||
except ValueError as e:
|
||||
raise backend_error(backend, str(e)) from e
|
||||
|
||||
@@ -148,7 +148,7 @@ def process_image_pyvips(path, *, maxsize, quality):
|
||||
)
|
||||
except pyvips.error.Error as e:
|
||||
raise ValueError(f"cannot decode image: {e}") from e
|
||||
backend = "pyvips"
|
||||
backend = "vips"
|
||||
t_end = perf_counter()
|
||||
|
||||
return ret, PreviewResponse(
|
||||
@@ -181,7 +181,7 @@ def process_image_buffer(data: bytes, *, quality, maxsize, maxzoom):
|
||||
return ret, PreviewResponse(
|
||||
ok=True,
|
||||
mime="image/avif",
|
||||
backend="pyvips",
|
||||
backend="vips",
|
||||
timings=[round((t_end - t_start) * 1000, 1)],
|
||||
width=orig_w,
|
||||
height=orig_h,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""PDF/XPS/EPUB preview conversion via PyMuPDF + pyvips."""
|
||||
"""PDF/XPS/EPUB preview conversion via PyMuPDF + vips."""
|
||||
|
||||
from time import perf_counter
|
||||
|
||||
@@ -13,7 +13,7 @@ try:
|
||||
except ImportError: # pragma: no cover - optional pdf extra
|
||||
pymupdf = None
|
||||
|
||||
BACKEND = "pdf+pyvips"
|
||||
BACKEND = "pdf+vips"
|
||||
|
||||
|
||||
def process_pdf(path, *, maxsize, maxzoom, quality, page_number=0):
|
||||
@@ -31,7 +31,8 @@ def process_pdf(path, *, maxsize, maxzoom, quality, page_number=0):
|
||||
pix = page.get_pixmap(matrix=mat)
|
||||
samples, width, height, n = pix.samples_mv, pix.width, pix.height, pix.n
|
||||
except Exception as e:
|
||||
raise backend_error(BACKEND, str(e), stage="pdf") from e
|
||||
# vips was never reached — this is a plain pdf error.
|
||||
raise backend_error("pdf", str(e)) from e
|
||||
t_load_end = perf_counter()
|
||||
|
||||
t_save_start = perf_counter()
|
||||
@@ -39,7 +40,7 @@ def process_pdf(path, *, maxsize, maxzoom, quality, page_number=0):
|
||||
img = pyvips.Image.new_from_memory(samples, width, height, n, "uchar")
|
||||
ret = img.write_to_buffer(".avif", Q=quality, effort=AVIF_FAST_EFFORT, keep="none")
|
||||
except Exception as e:
|
||||
raise backend_error(BACKEND, str(e), stage="pyvips") from e
|
||||
raise backend_error(BACKEND, str(e)) from e
|
||||
t_save_end = perf_counter()
|
||||
|
||||
return ret, PreviewResponse(
|
||||
|
||||
@@ -17,9 +17,10 @@ The hierarchy is intentionally small:
|
||||
|
||||
- ``OnlyOfficeError`` covers all OnlyOffice failures; optional fields
|
||||
(``code``, ``status``, ``url``, ``snippet``) describe the specific failure.
|
||||
- ``PreviewBackendError`` covers backend conversion failures (ffmpeg, pyvips,
|
||||
pdf, etc.); ``stage`` identifies the failing step of a combined pipeline
|
||||
(e.g. "pdf" vs "pyvips" in the "pdf+pyvips" backend).
|
||||
- ``PreviewBackendError`` covers backend conversion failures (ffmpeg, vips,
|
||||
pdf, etc.). Combined pipelines report the failing step in ``backend``
|
||||
(e.g. "pdf" if pdf reading failed before vips was reached, "pdf+vips"
|
||||
for a vips write failure).
|
||||
- ``PreviewTimeoutError`` covers timeouts for any backend.
|
||||
- ``PreviewCancelledError`` covers cancellations (e.g. pool shutdown).
|
||||
|
||||
@@ -80,17 +81,6 @@ class OnlyOfficeError(PreviewError):
|
||||
class PreviewBackendError(PreviewError):
|
||||
"""Backend conversion failure (image/video/pdf/etc)."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message: str = "preview failed",
|
||||
short: str = "error",
|
||||
*,
|
||||
stage: str | None = None,
|
||||
backend: str | None = None,
|
||||
):
|
||||
super().__init__(message, short, backend=backend)
|
||||
self.stage = stage
|
||||
|
||||
|
||||
class PreviewTimeoutError(PreviewError):
|
||||
"""Preview conversion exceeded its timeout for a given backend."""
|
||||
@@ -163,13 +153,12 @@ def onlyoffice_no_fileurl_error(snippet: str | None = None) -> OnlyOfficeError:
|
||||
return OnlyOfficeError(log, "no-fileurl error", snippet=snippet)
|
||||
|
||||
|
||||
def backend_error(backend: str, message: str, *, stage: str | None = None) -> PreviewBackendError:
|
||||
def backend_error(backend: str, message: str) -> PreviewBackendError:
|
||||
short = message.splitlines()[0][:60]
|
||||
return PreviewBackendError(
|
||||
f"[{backend}] preview failed: {message}",
|
||||
short,
|
||||
backend=backend,
|
||||
stage=stage,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -90,5 +90,5 @@ def expected_backend(path: Path) -> str:
|
||||
if mime_type and mime_type.startswith("video/"):
|
||||
return "video"
|
||||
if mime_type and mime_type.startswith("image/"):
|
||||
return "pyvips"
|
||||
return "vips"
|
||||
return "preview"
|
||||
|
||||
@@ -477,7 +477,7 @@ async def generate_office_preview(
|
||||
img, resp = await run_preview(filepath, quality, maxsize, maxzoom, data=png_bytes)
|
||||
|
||||
if resp is not None:
|
||||
resp.backend = "onlyoffice+" + (resp.backend or "pyvips")
|
||||
resp.backend = "onlyoffice+" + (resp.backend or "vips")
|
||||
if resp.timings:
|
||||
resp.timings = [round((t_oo_end - t_oo_start) * 1000, 1), *resp.timings]
|
||||
return img, resp
|
||||
|
||||
@@ -38,6 +38,7 @@ except ImportError: # pragma: no cover - optional worker extra
|
||||
sys.exit(1)
|
||||
|
||||
from mediapreview.backends import dispatch
|
||||
from mediapreview.exceptions import PreviewError
|
||||
from mediapreview.protocol import PreviewRequest, PreviewResponse
|
||||
from mediapreview.util.logformat import format_level_prefix
|
||||
|
||||
@@ -140,7 +141,13 @@ def _run_loop() -> None:
|
||||
)
|
||||
_write_response(resp, result or b"")
|
||||
except Exception as e:
|
||||
logger.exception("Preview worker error for %s", req.path)
|
||||
# PreviewError is an expected failure (broken input, missing
|
||||
# extra, backend error) — a warning suffices. Tracebacks are
|
||||
# reserved for internal errors we did not anticipate.
|
||||
if isinstance(e, PreviewError):
|
||||
logger.warning("Preview failed for %s: %s", req.path, e)
|
||||
else:
|
||||
logger.exception("Preview worker error for %s", req.path)
|
||||
captured = stderr_capture.getvalue().strip()
|
||||
_write_response(
|
||||
PreviewResponse(
|
||||
|
||||
@@ -64,11 +64,10 @@ def test_onlyoffice_no_fileurl_error():
|
||||
assert err.short == "no-fileurl error"
|
||||
|
||||
|
||||
def test_backend_error_stage():
|
||||
"""Combined pipelines tag the failing stage."""
|
||||
err = backend_error("pdf+pyvips", "cannot read document", stage="pdf")
|
||||
assert err.backend == "pdf+pyvips"
|
||||
assert err.stage == "pdf"
|
||||
def test_backend_error_pipeline_backend():
|
||||
"""Combined pipelines report the failing step in the backend name."""
|
||||
err = backend_error("pdf", "cannot read document")
|
||||
assert err.backend == "pdf"
|
||||
assert err.short == "cannot read document"
|
||||
assert isinstance(err, PreviewBackendError)
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ def _assert_ok(data, resp, backend: str | None = None) -> None:
|
||||
def test_process_image_exif_orientations(path: Path) -> None:
|
||||
"""Every EXIF orientation fixture must produce a valid preview."""
|
||||
data, resp = process_image(path, maxsize=512, quality=60)
|
||||
_assert_ok(data, resp, backend="pyvips")
|
||||
_assert_ok(data, resp, backend="vips")
|
||||
assert resp.width in (1200, 1800)
|
||||
assert resp.height in (1200, 1800)
|
||||
|
||||
@@ -74,7 +74,7 @@ def test_process_image_pyvips() -> None:
|
||||
"""The pyvips-only image backend works on a plain JPEG."""
|
||||
path = FILES / "Landscape_1.jpg"
|
||||
data, resp = process_image_pyvips(path, maxsize=512, quality=60)
|
||||
_assert_ok(data, resp, backend="pyvips")
|
||||
_assert_ok(data, resp, backend="vips")
|
||||
|
||||
|
||||
def test_process_image_buffer() -> None:
|
||||
@@ -83,7 +83,7 @@ def test_process_image_buffer() -> None:
|
||||
data, resp = process_image_buffer(
|
||||
path.read_bytes(), maxsize=512, quality=60, maxzoom=2.0
|
||||
)
|
||||
_assert_ok(data, resp, backend="pyvips")
|
||||
_assert_ok(data, resp, backend="vips")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -127,7 +127,7 @@ def test_process_pdf() -> None:
|
||||
maxzoom=2.0,
|
||||
quality=60,
|
||||
)
|
||||
_assert_ok(data, resp, backend="pdf+pyvips")
|
||||
_assert_ok(data, resp, backend="pdf+vips")
|
||||
assert resp.width == 595
|
||||
assert resp.height == 842
|
||||
|
||||
@@ -138,9 +138,9 @@ def test_process_pdf() -> None:
|
||||
|
||||
|
||||
DISPATCH_FIXTURES = [
|
||||
("Landscape_1.jpg", "pyvips", 1800, 1200),
|
||||
("Landscape_1.jpg", "vips", 1800, 1200),
|
||||
("sample-1mb.mp4", "video", 854, 480),
|
||||
("sample.pdf", "pdf+pyvips", 595, 842),
|
||||
("sample.pdf", "pdf+vips", 595, 842),
|
||||
]
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user