#!/usr/bin/env python3
"""Measure unchanged inpainting files; no image is written or modified.

Requires Python 3, Pillow with LittleCMS support, and NumPy.
Download coffee.png, rocket.jpg and the two *-output.png files beside this
script, then run python3 measure-artifacts.py. The repository layout also works.
"""
import argparse
import hashlib
from io import BytesIO
import json
import os
from pathlib import Path
import platform

import numpy as np
import PIL
from PIL import Image, ImageCms

parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--directory", type=Path, default=Path(__file__).resolve().parent)
parser.add_argument("--sources-dir", type=Path)
args = parser.parse_args()
directory = args.directory.resolve()
sources = args.sources_dir or (directory if (directory / "coffee.png").exists() else directory / "../../images")
sources = sources.resolve()


def fingerprint(path):
    data = path.read_bytes()
    return {"file": os.path.relpath(path, directory), "bytes": len(data), "sha256": hashlib.sha256(data).hexdigest()}


def read(path):
    with Image.open(path) as image:
        image.load()
        raw = np.asarray(image.convert("RGB"), dtype=np.int16)
        rgba = np.asarray(image.convert("RGBA"), dtype=np.uint8)
        icc = image.info.get("icc_profile")
        profile_name = None
        if icc:
            profile = ImageCms.ImageCmsProfile(BytesIO(icc))
            profile_name = ImageCms.getProfileName(profile).strip()
            display_rgb = np.asarray(ImageCms.profileToProfile(image, profile, ImageCms.createProfile("sRGB"), renderingIntent=0, outputMode="RGB"), dtype=np.int16)
        else:
            display_rgb = raw
        metadata = {**fingerprint(path), "width": image.width, "height": image.height, "mode": image.mode,
                    "iccProfileName": profile_name, "iccProfileBytes": len(icc) if icc else 0,
                    "fullyOpaquePixels": int((rgba[:, :, 3] == 255).sum())}
        return raw, display_rgb, metadata


def compare(source, output):
    difference = np.abs(output - source)
    changed = (difference >= 16).any(axis=2)
    ys, xs = np.where(changed)
    return {
        "meanAbsoluteRgbCodeDifference": round(float(difference.mean()), 6),
        "pixelsWithAnyChannelDifferenceAtLeast16": int(changed.sum()),
        "differenceBoundsX0Y0X1Y1Exclusive": [int(xs.min()), int(ys.min()), int(xs.max()) + 1, int(ys.max()) + 1] if len(xs) else None,
    }


cases = []
artifact_paths = []
for name, extension in [("coffee", "png"), ("rocket", "jpg")]:
    source = sources / f"{name}.{extension}"
    output = directory / f"{name}-output.png"
    raw_source, converted_source, source_meta = read(source)
    raw_output, converted_output, output_meta = read(output)
    if raw_source.shape != raw_output.shape:
        raise ValueError(f"Dimensions differ for {name}; no alignment/resize is performed.")
    cases.append({"id": name, "input": source_meta, "output": output_meta,
                  "rawEncodedRgbComparison": compare(raw_source, raw_output),
                  "profileAwareRgbComparison": compare(converted_source, converted_output)})
    artifact_paths.extend([source, output])

report = {
    "id": "INPAINT-2026-09-07-TWO-MASKS",
    "environment": {"python": platform.python_version(), "pillow": PIL.__version__, "numpy": np.__version__, "littlecms": ImageCms.core.littlecms_version},
    "method": {
        "threshold": "A pixel is counted when at least one 8-bit RGB code value differs by 16 or more. This localizes visible-scale changes; it is not mask coverage, reconstruction accuracy or a quality score.",
        "profileAware": "The rocket JPEG embeds Adobe RGB (1998). Convert the source in memory to an sRGB profile with Pillow ImageCms/LittleCMS, renderingIntent=0. Untagged exported PNGs and the untagged coffee source are treated as sRGB. The script writes JSON only, never image files.",
        "comparisonBoundary": "Profile conversion and JPEG decoding can differ slightly between this measurement runtime and a browser. Even the profile-aware differences are not an exact recovered mask. Raw and profile-aware numbers are retained separately so the profile issue is visible.",
        "coordinates": "Difference bounds are pixel boxes [x0,y0,x1,y1], excluding x1 and y1, on same-size source/output pairs.",
        "scope": "Two completed browser exports measured after the runs. No unmasked ground-truth scene, pixel mask, model benchmark or processing-time measurement is available.",
    },
    "cases": cases,
}
(directory / "measurements.json").write_text(json.dumps(report, indent=2) + "\n")
artifact_paths.extend([directory / "run.json", Path(__file__).resolve(), directory / "measurements.json"])
manifest = {"id": report["id"], "description": "Unchanged source images and browser exports, method, run record and measurements. Manifest excludes itself.",
            "artifacts": [fingerprint(path) for path in artifact_paths if path.exists()]}
(directory / "manifest.json").write_text(json.dumps(manifest, indent=2) + "\n")
print(json.dumps({"cases": [{"id": case["id"], **case["profileAwareRgbComparison"]} for case in cases], "artifacts": len(manifest["artifacts"])}, indent=2))
