1 Commits
Author SHA1 Message Date
LeoVasanko a99996bd9c Quiet expected preview failures; rename pyvips to vips
Worker logs expected PreviewError failures as warnings; tracebacks are
reserved for unanticipated internal errors. pdf read errors report
backend "pdf" (vips not yet reached); the stage field is dropped as the
backend name now encodes the failing pipeline step. Backend label
"pyvips" renamed to "vips" everywhere.
2026-08-13 04:46:08 +00:00
9 changed files with 36 additions and 40 deletions
+4 -4
View File
@@ -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
+2 -2
View File
@@ -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,
+5 -4
View File
@@ -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(
+5 -16
View File
@@ -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,
)
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -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
+8 -1
View File
@@ -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(
+4 -5
View File
@@ -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)
+6 -6
View File
@@ -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),
]