#!/usr/bin/env python3
"""Inspect retained image exports with Python's standard library; never edit them.

Usage: python3 inspect-image-export.py matting-before.png matting-after.png bg-after.png
Supported: baseline JFIF headers and non-interlaced 8-bit PNG types 2, 3 and 6.
JPEG alpha absence is established from its baseline three-component JFIF header.
PNG alpha is decoded and counted. This is not a general image validator.
"""
import argparse
import hashlib
import json
import struct
import zlib
from pathlib import Path


def paeth(a, b, c):
    p = a + b - c
    distances = (abs(p - a), abs(p - b), abs(p - c))
    return (a, b, c)[distances.index(min(distances))]


def inspect_jpeg(path, data):
    offset, jfif, frame = 2, False, None
    markers = []
    while offset < len(data):
        if data[offset] != 255:
            raise ValueError(f"{path.name}: expected JPEG marker")
        while offset < len(data) and data[offset] == 255:
            offset += 1
        marker = data[offset]
        offset += 1
        if marker in (0xd9, 0xda):
            break
        length = struct.unpack_from(">H", data, offset)[0]
        payload = data[offset + 2:offset + length]
        if length < 2 or offset + length > len(data):
            raise ValueError(f"{path.name}: truncated JPEG header")
        markers.append(f"FF{marker:02X}")
        if marker == 0xe0 and payload.startswith(b"JFIF\0"):
            jfif = True
        if marker == 0xc0:
            precision, height, width, components = struct.unpack(">BHHB", payload[:6])
            frame = (precision, height, width, components)
        offset += length
    if not jfif or frame is None or frame[0] != 8 or frame[3] != 3:
        raise ValueError(f"{path.name}: expected baseline 8-bit, three-component JFIF")
    return {
        "detected_format": "JPEG/JFIF",
        "filename_matches_format": path.suffix.lower() in (".jpg", ".jpeg", ".jfif"),
        "width": frame[2], "height": frame[1], "bit_depth": frame[0],
        "jpeg_frame_marker": "FFC0", "jpeg_components": frame[3],
        "header_markers_before_first_scan": markers,
        "contains_transparent_pixels": False,
        "transparency_check": "Baseline three-component JFIF carries no alpha channel. JPEG pixels were not decoded or counted by this script.",
    }


def inspect_png(path, data):
    offset, chunks, compressed, transparency, palette = 8, [], [], None, None
    header = None
    ended = False
    while offset < len(data):
        if offset + 12 > len(data):
            raise ValueError(f"{path.name}: truncated chunk")
        length = struct.unpack_from(">I", data, offset)[0]
        end = offset + 12 + length
        if end > len(data):
            raise ValueError(f"{path.name}: truncated payload")
        kind = data[offset + 4:offset + 8]
        payload = data[offset + 8:end - 4]
        crc = struct.unpack_from(">I", data, end - 4)[0]
        if zlib.crc32(kind + payload) & 0xffffffff != crc:
            raise ValueError(f"{path.name}: CRC mismatch in {kind!r}")
        chunks.append(kind.decode("ascii"))
        if kind == b"IHDR":
            if header is not None or len(chunks) != 1 or length != 13:
                raise ValueError(f"{path.name}: unexpected IHDR")
            header = struct.unpack(">IIBBBBB", payload)
        elif kind == b"IDAT":
            compressed.append(payload)
        elif kind == b"tRNS":
            transparency = payload
        elif kind == b"PLTE":
            palette = payload
        elif kind == b"IEND":
            if length != 0 or end != len(data):
                raise ValueError(f"{path.name}: unexpected bytes at IEND")
            ended = True
            break
        offset = end
    if not ended or header is None or not compressed:
        raise ValueError(f"{path.name}: missing required chunk")
    width, height, depth, color, compression, filtering, interlace = header
    if depth != 8 or color not in (2, 3, 6) or any((compression, filtering, interlace)):
        raise ValueError(f"{path.name}: this inspector supports only non-interlaced 8-bit RGB/indexed/RGBA PNG")
    if not 0 < width * height <= 10_000_000:
        raise ValueError(f"{path.name}: pixel count outside this inspector's limit")
    if transparency is not None and ((color == 2 and len(transparency) != 6) or color == 6):
        raise ValueError(f"{path.name}: unexpected tRNS for this supported format")
    if color == 3 and (palette is None or len(palette) % 3 != 0):
        raise ValueError(f"{path.name}: missing or invalid palette")
    channels = {2: 3, 3: 1, 6: 4}[color]
    stride = width * channels
    expected_bytes = (stride + 1) * height
    decoder = zlib.decompressobj()
    raw = decoder.decompress(b"".join(compressed), expected_bytes + 1)
    if len(raw) != expected_bytes or not decoder.eof or decoder.unused_data:
        raise ValueError(f"{path.name}: decompressed size or zlib stream mismatch")
    previous = bytearray(stride)
    counts = {"fully_transparent": 0, "partially_transparent": 0, "fully_opaque": 0}
    transparent_rgb = struct.unpack(">HHH", transparency) if transparency is not None and color == 2 else None
    samples = []
    for y in range(height):
        row_offset = y * (stride + 1)
        filter_type = raw[row_offset]
        if filter_type > 4:
            raise ValueError(f"{path.name}: unsupported PNG filter")
        row = bytearray(raw[row_offset + 1:row_offset + 1 + stride])
        for i in range(stride):
            left = row[i - channels] if i >= channels else 0
            upper = previous[i]
            diagonal = previous[i - channels] if i >= channels else 0
            predictor = (0, left, upper, (left + upper) // 2, paeth(left, upper, diagonal))[filter_type]
            row[i] = (row[i] + predictor) & 255
        for x in range(width):
            i = x * channels
            if color == 3:
                index = row[i]
                rgb = tuple(palette[index * 3:index * 3 + 3])
                if len(rgb) != 3:
                    raise ValueError(f"{path.name}: palette index outside PLTE")
                alpha = transparency[index] if transparency is not None and index < len(transparency) else 255
            else:
                rgb = tuple(row[i:i + 3])
                alpha = row[i + 3] if color == 6 else 0 if rgb == transparent_rgb else 255
            key = "fully_transparent" if alpha == 0 else "fully_opaque" if alpha == 255 else "partially_transparent"
            counts[key] += 1
            if (x, y) in ((0, 0), (width - 1, 0), (0, height - 1), (width - 1, height - 1)):
                samples.append({"x": x, "y": y, "rgba": list(rgb) + [alpha]})
        previous = row
    pixels = width * height
    return {
        "detected_format": "PNG",
        "filename_matches_format": path.suffix.lower() == ".png",
        "width": width,
        "height": height,
        "bit_depth": depth,
        "png_color_type": color,
        "stored_channels": {2: "RGB", 3: "palette indices", 6: "RGBA"}[color],
        "tRNS_present": transparency is not None,
        "chunks_in_order": chunks,
        "all_chunk_crcs_valid": True,
        "pixel_count": pixels,
        "alpha_counts": counts,
        "alpha_percentages": {key: round(value * 100 / pixels, 4) for key, value in counts.items()},
        "corner_samples": samples,
        "contains_transparent_pixels": counts["fully_transparent"] + counts["partially_transparent"] > 0,
    }


def inspect(path):
    data = path.read_bytes()
    result = {"filename": path.name, "sha256": hashlib.sha256(data).hexdigest(), "bytes": len(data), "first_16_bytes_hex": data[:16].hex()}
    if data[:8] == b"\x89PNG\r\n\x1a\n":
        result.update(inspect_png(path, data))
    elif data[:2] == b"\xff\xd8":
        result.update(inspect_jpeg(path, data))
    else:
        raise ValueError(f"{path.name}: unsupported file signature")
    return result


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("images", nargs="+", type=Path)
    args = parser.parse_args()
    print(json.dumps({
        "inspector": "image-export-inspection-v1",
        "method": "Original-file SHA-256 and signature; JPEG JFIF/SOF header inspection or PNG chunk CRCs, IHDR/tRNS and decoded alpha counts; no model inference or image editing.",
        "files": [inspect(path) for path in args.images],
    }, indent=2))
