#!/usr/bin/env python3 """Re-encode one image safely: verify decode, strip metadata, bound dimensions. Fixed argv contract only: reencode_image.py --in SOURCE --out TARGET [--max-bytes N] [--max-dimension N] Reads SOURCE, decodes defensively (decompression-bomb guarded), strips all metadata by re-encoding to baseline JPEG, and writes TARGET. Prints one JSON line on success. Never prints image bytes or base64 to stdout/stderr. """ import argparse import io import json import os import sys Image = None # populated in main() so import errors fail closed def parser() -> argparse.ArgumentParser: p = argparse.ArgumentParser() p.add_argument("--in", dest="source", required=True) p.add_argument("--out", dest="target", required=True) p.add_argument("--max-bytes", type=int, default=4 * 1024 * 1024) p.add_argument("--max-dimension", type=int, default=4096) return p def main() -> int: args = parser().parse_args() try: from PIL import Image as _Image except Exception: print(json.dumps({"ok": False, "error": "image processing is unavailable"})) return 3 source_size = os.path.getsize(args.source) if source_size <= 0 or source_size > args.max_bytes: print(json.dumps({"ok": False, "error": "rejected"})) return 2 try: # Fail closed on decompression bombs before full pixel load. with _Image.open(args.source) as probe: try: probe.load() except _Image.DecompressionBombError: print(json.dumps({"ok": False, "error": "dimensions"})) return 2 width, height = probe.size if width > args.max_dimension or height > args.max_dimension: print(json.dumps({"ok": False, "error": "dimensions"})) return 2 image = probe.convert("RGB") except Exception: print(json.dumps({"ok": False, "error": "malformed"})) return 2 buffer = io.BytesIO() try: # Baseline JPEG re-encode drops EXIF/GPS/XMP/tEXt entirely. image.save(buffer, "JPEG", quality=85, optimize=True, progressive=False) except Exception: print(json.dumps({"ok": False, "error": "encode"})) return 2 data = buffer.getvalue() if not data or data[:3] != b"\xff\xd8\xff" or len(data) > args.max_bytes: print(json.dumps({"ok": False, "error": "encode"})) return 2 with open(args.target, "wb") as handle: handle.write(data) print(json.dumps({ "ok": True, "format": "jpeg", "width": image.size[0], "height": image.size[1], "bytes": len(data), "metadataStripped": True, })) return 0 if __name__ == "__main__": sys.exit(main())