#!/usr/bin/env python3 """Split an image into two lossless, random-looking PNG fixture shares.""" from __future__ import annotations import argparse from io import BytesIO from pathlib import Path import secrets from PIL import Image def split_image(input_path: Path, output_dir: Path, index: int | None = None) -> tuple[Path, Path]: if index is None: index = 1 while any((output_dir / f"image_{index}{part}.png").exists() for part in ("a", "b")): index += 1 if index < 1: raise ValueError("Image index must be a positive integer") paths = ( output_dir / f"image_{index}a.png", output_dir / f"image_{index}b.png", ) for path in paths: if path.exists(): raise FileExistsError(f"Refusing to overwrite {path}; choose another --index") # RGBA shares preserve every decoded pixel, including transparency. For animated # inputs, use the first frame, matching the dashboard's static image upload behavior. with Image.open(input_path) as source: image = source.convert("RGBA") pixels = image.tobytes() first = secrets.token_bytes(len(pixels)) second = bytes(pixel ^ mask for pixel, mask in zip(pixels, first)) output_dir.mkdir(parents=True, exist_ok=True) for path, share in zip(paths, (first, second)): # Exclusive creation also prevents overwriting a file created during splitting. encoded = BytesIO() Image.frombytes("RGBA", image.size, share).save(encoded, format="PNG") with path.open("xb") as output: output.write(encoded.getvalue()) return paths def main() -> None: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("input_image", type=Path) parser.add_argument("--output-dir", type=Path, default=Path(".")) parser.add_argument("--index", type=int, help="Fixture number (default: first unused positive index)") args = parser.parse_args() try: paths = split_image(args.input_image, args.output_dir, args.index) except (OSError, ValueError) as error: parser.exit(1, f"Could not split image: {error}\n") for path in paths: print(path) if __name__ == "__main__": main()