1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
|
"""Compares a new post-processed PNG with the local baseline.
The baseline `.screenshots-cache/baseline/<id>.png` only counts when its
pixels hash equals the lock's `pixels_sha256`. It is never fetched from the CDN.
"""
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Optional
from PIL import Image, ImageChops
from postprocess import pixels_sha256
NEW = "new"
REPLACE = "replace"
UNCHANGED = "unchanged"
BASELINE_UNAVAILABLE = "baseline-unavailable"
@dataclass
class DiffResult:
result: str
changed_ratio: Optional[float] = None
detail: str = ""
def baseline_path(cache: Path, entry_id: str) -> Path:
return Path(cache) / "baseline" / f"{entry_id}.png"
def baseline_valid(cache: Path, entry_id: str, lock_record: Optional[dict[str, Any]]) -> bool:
if not lock_record:
return False
path = baseline_path(cache, entry_id)
if not path.is_file():
return False
try:
with Image.open(path) as img:
return pixels_sha256(img) == lock_record.get("pixels_sha256")
except OSError:
return False
def changed_ratio(new: Image.Image, old: Image.Image, tolerance: int) -> float:
"""Share of pixels whose largest per-channel delta exceeds `tolerance`."""
delta = ImageChops.difference(new.convert("RGBA"), old.convert("RGBA"))
bands = delta.split()
worst = bands[0]
for band in bands[1:]:
worst = ImageChops.lighter(worst, band)
changed = worst.point(lambda v: 255 if v > tolerance else 0).histogram()[255]
return changed / float(new.size[0] * new.size[1])
def compare(new_png: Path, cache: Path, entry_id: str, lock_record: Optional[dict[str, Any]],
settings: dict[str, Any]) -> DiffResult:
if not lock_record:
return DiffResult(NEW, detail="no lock record")
if not baseline_valid(cache, entry_id, lock_record):
path = baseline_path(cache, entry_id)
why = "baseline missing" if not path.exists() else "baseline pixels_sha256 differs from the lock"
return DiffResult(BASELINE_UNAVAILABLE, detail=why)
with Image.open(new_png) as new, Image.open(baseline_path(cache, entry_id)) as old:
if new.size != old.size:
return DiffResult(REPLACE, 1.0, f"size {old.size} -> {new.size}")
ratio = changed_ratio(new, old, int(settings["tolerance"]))
if ratio > float(settings["threshold"]):
return DiffResult(REPLACE, ratio, f"{ratio:.6f} of pixels changed")
return DiffResult(UNCHANGED, ratio, f"{ratio:.6f} of pixels changed")
|