Compare commits

..
3 Commits
Author SHA1 Message Date
LeoVasanko 953ec628a0 Add -nostdin to ffmpeg preview conversions to suppress keyboard prompts 2026-08-11 05:32:27 +00:00
LeoVasanko e678c8c267 Put the failing ffmpeg command on its own line in error messages 2026-08-11 05:31:30 +00:00
LeoVasanko 69d58f99e3 Drop noisy ffmpeg fallback for non-HEIC images, quiet ffmpeg output
A corrupt TIFF in production produced a wall of ffmpeg error output:
pyvips could not decode it, the generic ffmpeg fallback was tried, and
ffmpeg's TIFF decoder failed just the same — with banner, configuration
and stream-mapping spam included.

- Non-HEIC images are now decoded by pyvips only; a pyvips failure
  raises a clean one-line ValueError ("cannot decode image: ...", a 422
  like any other undecodable file) instead of invoking ffmpeg. The
  ffmpeg path is kept for HEIC/HEIF, where pyvips genuinely falls short
  (tile assembly, HDR metadata).
- ffmpeg runs with -hide_banner -loglevel error -nostats: error output
  is still shown on failure, without the version/configuration/progress
  noise. The -s insertion index no longer depends on fixed positions.
2026-08-11 05:29:32 +00:00
+18 -11
View File
@@ -213,6 +213,13 @@ def _image_via_ffmpeg(path: Path, maxsize: int, quality: int) -> bytes:
tmp_path = tmp_f.name tmp_path = tmp_f.name
cmd = [ cmd = [
"ffmpeg", "ffmpeg",
# Keep error messages, drop the banner/config/stream-mapping spam.
"-hide_banner",
"-loglevel",
"error",
"-nostats",
# No interactive keyboard prompts ("Press [q] to stop ...").
"-nostdin",
"-y", "-y",
"-i", "-i",
str(path), str(path),
@@ -233,8 +240,9 @@ def _image_via_ffmpeg(path: Path, maxsize: int, quality: int) -> bytes:
new_w = int(w * scale) new_w = int(w * scale)
new_h = int(h * scale) new_h = int(h * scale)
# insert -s <wxh> right after the input file # insert -s <wxh> right after the input file
cmd.insert(4, "-s") input_index = cmd.index(str(path)) + 1
cmd.insert(5, f"{new_w}x{new_h}") cmd.insert(input_index, "-s")
cmd.insert(input_index + 1, f"{new_w}x{new_h}")
try: try:
try: try:
# stdin=DEVNULL is critical: ffmpeg must not inherit the worker's # stdin=DEVNULL is critical: ffmpeg must not inherit the worker's
@@ -254,10 +262,10 @@ def _image_via_ffmpeg(path: Path, maxsize: int, quality: int) -> bytes:
stderr = (e.stderr or b"").decode(errors="replace").strip() stderr = (e.stderr or b"").decode(errors="replace").strip()
if stderr: if stderr:
raise RuntimeError( raise RuntimeError(
f"ffmpeg failed (exit {e.returncode}): {shell_cmd}\n{stderr}" f"ffmpeg failed (exit {e.returncode}):\n{shell_cmd}\n{stderr}"
) from e ) from e
raise RuntimeError( raise RuntimeError(
f"ffmpeg failed (exit {e.returncode}): {shell_cmd}" f"ffmpeg failed (exit {e.returncode}):\n{shell_cmd}"
) from e ) from e
with Path(tmp_path).open("rb") as f: with Path(tmp_path).open("rb") as f:
return f.read() return f.read()
@@ -285,9 +293,10 @@ def process_image_pyvips(path, *, maxsize, quality):
height=height, height=height,
) )
# Other image formats: pyvips first, ffmpeg fallback. # Other image formats: pyvips only. ffmpeg is not a useful fallback
# here — when pyvips cannot decode a file, ffmpeg's image decoders
# cannot either, and their failure output is far noisier.
load_opts = {"access": "sequential"} load_opts = {"access": "sequential"}
orig_w = orig_h = None
try: try:
img = pyvips.Image.new_from_file(str(path), **load_opts) img = pyvips.Image.new_from_file(str(path), **load_opts)
img = img.autorot() img = img.autorot()
@@ -301,11 +310,9 @@ def process_image_pyvips(path, *, maxsize, quality):
effort=AVIF_FAST_EFFORT, effort=AVIF_FAST_EFFORT,
keep="none", keep="none",
) )
backend = "pyvips" except pyvips.error.Error as e:
except pyvips.error.Error: raise ValueError(f"cannot decode image: {e}") from e
orig_w, orig_h = None, None backend = "pyvips"
ret = _image_via_ffmpeg(path, maxsize, quality)
backend = "ffmpeg"
t_end = perf_counter() t_end = perf_counter()
return ret, PreviewResponse( return ret, PreviewResponse(