Add a right-click copy link and share menu on gallery photos with stable per-photo permalinks
CI / build-and-scan (pull_request) Successful in 2m9s

This commit is contained in:
2026-09-23 15:46:35 +02:00
parent 6c0a169528
commit 88754bf907
8 changed files with 610 additions and 1 deletions
+76
View File
@@ -135,3 +135,79 @@ def test_generate_favicon_ico_no_favicon_skips(tmp_path):
output_path = tmp_path / "output" / "favicon.ico"
ip.generate_favicon_ico({}, tmp_path, output_path)
assert not output_path.exists()
def test_compute_photo_ids_hashes_original_files(tmp_path, make_image):
import hashlib
img_dir = tmp_path / "photos"
a = make_image(img_dir / "gallery" / "a.jpg", color=(1, 2, 3))
make_image(img_dir / "gallery" / "b.jpg", color=(200, 10, 10))
images = [{"src": "gallery/a.jpg"}, {"src": "gallery/b.jpg"}, {"src": "gallery/missing.jpg"}]
ip.compute_photo_ids(images, img_dir)
assert images[0]["photo_id"] == hashlib.sha256(a.read_bytes()).hexdigest()
assert len(images[1]["photo_id"]) == 64
assert images[0]["photo_id"] != images[1]["photo_id"]
assert "photo_id" not in images[2]
def test_compute_photo_ids_is_independent_of_order_and_name(tmp_path, make_image):
img_dir = tmp_path / "photos"
make_image(img_dir / "gallery" / "a.jpg", color=(1, 2, 3))
(img_dir / "gallery" / "renamed.jpg").write_bytes((img_dir / "gallery" / "a.jpg").read_bytes())
make_image(img_dir / "gallery" / "b.jpg", color=(9, 9, 9))
first = [{"src": "gallery/a.jpg"}, {"src": "gallery/b.jpg"}]
second = [{"src": "gallery/b.jpg"}, {"src": "gallery/renamed.jpg"}]
ip.compute_photo_ids(first, img_dir)
ip.compute_photo_ids(second, img_dir)
assert first[0]["photo_id"] == second[1]["photo_id"]
assert first[1]["photo_id"] == second[0]["photo_id"]
def test_compute_photo_ids_suffixes_duplicates(tmp_path, make_image):
img_dir = tmp_path / "photos"
make_image(img_dir / "gallery" / "a.jpg")
images = [{"src": "gallery/a.jpg"}, {"src": "gallery/a.jpg"}, {"src": "gallery/a.jpg"}]
ip.compute_photo_ids(images, img_dir)
base = images[0]["photo_id"]
assert images[1]["photo_id"] == f"{base}-2"
assert images[2]["photo_id"] == f"{base}-3"
def test_generate_share_preview_resizes_to_jpeg(tmp_path, make_image):
input_path = make_image(tmp_path / "in.png", size=(2400, 1600), fmt="PNG")
output_path = tmp_path / "share" / "x.jpg"
size = ip.generate_share_preview(input_path, output_path)
assert size == (1200, 800)
with Image.open(output_path) as img:
assert img.format == "JPEG"
assert img.size == (1200, 800)
def test_generate_share_preview_converts_embedded_profile_to_srgb(tmp_path):
from PIL import ImageCms
input_path = tmp_path / "in.jpg"
srgb_bytes = ImageCms.ImageCmsProfile(ImageCms.createProfile("sRGB")).tobytes()
Image.new("RGB", (300, 200), (10, 200, 30)).save(input_path, "JPEG", icc_profile=srgb_bytes)
output_path = tmp_path / "out.jpg"
size = ip.generate_share_preview(input_path, output_path)
assert size == (300, 200)
with Image.open(output_path) as img:
assert img.mode == "RGB"
def test_generate_share_preview_failure_returns_none(tmp_path):
bad = tmp_path / "bad.jpg"
bad.write_bytes(b"not an image")
assert ip.generate_share_preview(bad, tmp_path / "out.jpg") is None