fix(webpage): downscale large full-size ptycho PNGs before upload
CI for csaxs_bec / test (push) Failing after 2m19s

omny.psi.ch/upload.php rejected the full-size probe/spectrum PNGs of
stitched reconstructions with HTTP 413. Full-size copies above 900 kB are
now saved as downscaled PNGs until they fit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ALDnH16dC6iyHjhe5Ey4w9
This commit is contained in:
x12saandClaude Opus 5.5 committed 2026-10-15 16:10:46 +02:00
1 parent de14963f3e
commit e5c5d64307
2 files changed
+57 -1

No files matched your search

@@ -136,6 +136,10 @@ _MISMATCH_LOCAL_PORT = 8081
# Subfolders follow the pattern dset_NNNNN and contain S#####_*_phase.png etc.
_PTYCHO_ONLINE_DIR = "~/data/raw/analysis/online/ptycho"
_PTYCHO_THUMB_PX = 200 # longest edge of generated thumbnail in pixels
# The upload server (omny.psi.ch/upload.php) rejects large POSTs with HTTP 413;
# stitched reconstructions produce full-size PNGs above that. Full-size copies
# larger than this are downscaled until they fit.
_PTYCHO_FULL_MAX_BYTES = 900_000
VERBOSITY_SILENT = 0
VERBOSITY_NORMAL = 1
@@ -1324,7 +1328,7 @@ class WebpageGeneratorBase:
img.save(thumb_path, "JPEG", quality=85)
else:
shutil.copy2(src, thumb_path)
shutil.copy2(src, full_path)
self._copy_full_capped(src, full_path)
images.append({
"role": role,
"thumb": thumb_name,
@@ -1339,6 +1343,26 @@ class WebpageGeneratorBase:
self._last_ptycho_scan = scan_id
return {"scan_id": scan_id, "images": images, "folder": str(latest_dir)}
@staticmethod
def _copy_full_capped(src: Path, dst: Path, max_bytes: int = _PTYCHO_FULL_MAX_BYTES) -> None:
"""Copy src to dst; if src is larger than max_bytes, save a downscaled
PNG instead (halving the pixel area each step) so the upload server
accepts it. Without Pillow, or if it can't get small enough, the
plain/smallest copy is kept."""
if src.stat().st_size <= max_bytes or not _PIL_AVAILABLE:
shutil.copy2(src, dst)
return
img = _PILImage.open(src)
img.load()
w, h = img.size
scale = 1.0
while True:
scale *= 0.7071
size = (max(1, int(w * scale)), max(1, int(h * scale)))
img.resize(size, _PILImage.LANCZOS).save(dst, "PNG", optimize=True)
if dst.stat().st_size <= max_bytes or min(size) <= 64:
return
# ------------------------------------------------------------------
# Subclass hooks
# ------------------------------------------------------------------
@@ -0,0 +1,32 @@
"""Full-size ptycho PNGs copied for upload are capped in size: the upload
server rejected large (stitched) probe/spectrum images with HTTP 413."""
import numpy as np
import pytest
PIL = pytest.importorskip("PIL.Image")
from csaxs_bec.bec_ipython_client.plugins.OMNY_shared.webpage_generator_base import (
WebpageGeneratorBase,
)
def test_large_png_is_downscaled_below_cap(tmp_path):
src, dst = tmp_path / "S00085-S00093_probe.png", tmp_path / "out.png"
PIL.fromarray((np.random.default_rng(0).random((800, 1200, 3)) * 255).astype("uint8")).save(src)
cap = 300_000
assert src.stat().st_size > cap
WebpageGeneratorBase._copy_full_capped(src, dst, max_bytes=cap)
assert dst.stat().st_size <= cap
assert PIL.open(dst).size[0] < 1200
def test_small_png_is_copied_unchanged(tmp_path):
src, dst = tmp_path / "S00001_phase.png", tmp_path / "out.png"
PIL.new("RGB", (50, 50)).save(src)
WebpageGeneratorBase._copy_full_capped(src, dst, max_bytes=300_000)
assert dst.read_bytes() == src.read_bytes()