1"""Publish screenshot originals as public copies (Safari captures cropped to remove the browser chrome/bookmarks bar; Terminal captures unchanged),2write evidence/manifests/screenshots.csv and the LaTeX gallery appendix."""3import os, sys, csv, json, subprocess, hashlib4sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src")); sys.path.insert(0, os.path.dirname(__file__))5from common import REPO6from texutil import tex as T7from pgar.state import StateStore8from pgar.capture import publish9def sha(p): return hashlib.sha256(open(p, "rb").read()).hexdigest()10def main():11 store = StateStore(os.path.join(REPO, "runs", "state.sqlite"))12 rows = store.query("SELECT * FROM screenshots ORDER BY screenshot_id")13 pub_dir = os.path.join(REPO, "evidence", "public_screenshots"); os.makedirs(pub_dir, exist_ok=True); os.makedirs(os.path.join(REPO, "evidence", "manifests"), exist_ok=True)14 tex = [r"\section{Visual evidence gallery}", r"\label{app:gallery}", "Every image below is an unmodified or cropped capture of a real surface, recorded in the screenshot manifest (\\texttt{evidence/manifests/screenshots.csv}) with its capture mode, tool, UTC time, SHA-256 of the original and of the public copy, and the run and event it documents. Capture failures are listed with their reason and have no image. Hashes attest file integrity relative to the manifest; they do not independently prove the moment of capture, which is triangulated with the session and event timestamps in the state store."]15 man = []16 for r in rows:17 failed = "CAPTURE FAILURE" in (r.get("caption") or "")18 pub = None19 if not failed and r.get("original_path") and os.path.exists(os.path.join(REPO, r["original_path"])):20 name = os.path.basename(r["original_path"]); pub = os.path.join(pub_dir, name)21 if r["capture_mode"] in ("dashboard_live", "viewer_postrun") and "Safari" in (r.get("source_surface") or ""):22 # crop the top 150 logical px (browser chrome and bookmarks bar) → 2x retina = 300 px23 w, h = [int(x.split(":")[1]) for x in subprocess.run(["sips", "-g", "pixelWidth", "-g", "pixelHeight", os.path.join(REPO, r["original_path"])], capture_output=True, text=True).stdout.strip().splitlines()[1:3]]24 subprocess.run(["sips", "-c", str(h - 300), str(w), "--cropOffset", "300", "0", os.path.join(REPO, r["original_path"]), "--out", pub], capture_output=True, check=True)25 desc = "cropped: top 300 px (browser toolbar and bookmarks bar) removed; page content unmodified"26 else:27 subprocess.run(["cp", os.path.join(REPO, r["original_path"]), pub], check=True); desc = "copied without modification"28 with store.tx() as c:29 c.execute("UPDATE screenshots SET redacted_public_path=?, redacted_sha256=?, redaction_description=? WHERE screenshot_id=?", (os.path.relpath(pub, REPO), sha(pub), desc, r["screenshot_id"]))30 r2 = store.query("SELECT * FROM screenshots WHERE screenshot_id=?", (r["screenshot_id"],))[0]; man.append(r2)31 cap = T(r2.get("caption") or "")32 meta = f"\\textbf{{{r2['screenshot_id']}}} --- mode \\texttt{{{T(r2['capture_mode'])}}}; {r2['capture_timestamp_utc'][:19]} UTC; run {T(r2.get('run_id') or '--')}; tool {T(r2.get('capture_tool'))}; original SHA-256 {r2['original_sha256'][:16]}\\ldots; public SHA-256 {(r2.get('redacted_sha256') or '--')[:16]}\\ldots; {T(r2.get('redaction_description') or '')}. {cap}"33 if r2.get("redacted_public_path") and not failed:34 tex += [r"\begin{figure}[htbp]\centering", f"\\includegraphics[width=0.92\\textwidth]{{../{r2['redacted_public_path']}}}", f"\\caption{{{meta}}}", r"\end{figure}", r"\clearpage"]35 else:36 tex += [f"\\paragraph{{{r2['screenshot_id']} (no image)}} {meta}", ""]37 with open(os.path.join(REPO, "evidence", "manifests", "screenshots.csv"), "w", newline="") as fh:38 w = csv.DictWriter(fh, fieldnames=list(man[0].keys())); w.writeheader(); w.writerows(man)39 open(os.path.join(REPO, "paper", "appendices", "gallery.tex"), "w").write("\n".join(tex) + "\n"); print(len(man), "screenshots in manifest")40if __name__ == "__main__": main()41