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