# scripts/make-metadata-test-photos.py — from tinytinytools.com # Served verbatim from the working tree. Method: https://tinytinytools.com/guides """ Builds the test photographs used by /guides/what-your-photos-reveal. Pillow writes the EXIF; the site's own parser in src/lib/images/metadata.ts reads it back. Two independent implementations on either side of the file is the point — a study where the same code writes and reads the test data proves only that it is self-consistent. Usage: python3 scripts/make-metadata-test-photos.py Needs: pip install pillow """ import sys from pathlib import Path from PIL import Image from PIL.TiffImagePlugin import IFDRational # Tag numbers, so the intent is legible without a Pillow constants lookup. MAKE, MODEL, SOFTWARE, DATETIME = 0x010F, 0x0110, 0x0131, 0x0132 EXIF_IFD, GPS_IFD = 0x8769, 0x8825 DATETIME_ORIGINAL, LENS_MODEL, BODY_SERIAL = 0x9003, 0xA434, 0xA431 GPS_LAT_REF, GPS_LAT, GPS_LON_REF, GPS_LON = 1, 2, 3, 4 # 27°42'12.34"N, 85°19'43.21"E — Kupondole, Lalitpur. A real, checkable location. LATITUDE = (IFDRational(27), IFDRational(42), IFDRational(1234, 100)) LONGITUDE = (IFDRational(85), IFDRational(19), IFDRational(4321, 100)) def canvas(width=1024, height=768): """ A plausible photograph: a tonal sweep with detail at three scales, deterministic. The first version of this was a smooth gradient with no texture, and it quietly understated everything the guide measures. A gradient is trivially easy to encode, so re-encoding one costs almost nothing — mean channel drift came out at 0.008, which would have supported "converting barely touches your photo". That is a fact about the test image, not about converting. Real photographs carry detail at several scales, and that detail is what a second lossy pass damages. """ import math import random rng = random.Random(1) octaves = [(8, 46.0), (32, 22.0), (128, 10.0)] fields = [[[rng.random() for _ in range(size + 2)] for _ in range(size + 2)] for size, _ in octaves] def sample(field, size, x, y): fx, fy = x / width * size, y / height * size x0, y0 = int(fx), int(fy) tx, ty = fx - x0, fy - y0 a = field[y0][x0] * (1 - tx) + field[y0][x0 + 1] * tx b = field[y0 + 1][x0] * (1 - tx) + field[y0 + 1][x0 + 1] * tx return a * (1 - ty) + b * ty image = Image.new("RGB", (width, height)) pixels = image.load() for y in range(height): for x in range(width): detail = 0.0 for index, (size, amplitude) in enumerate(octaves): detail += (sample(fields[index], size, x, y) - 0.5) * amplitude grain = (rng.random() - 0.5) * 5 sweep = math.sin(x / 150) * math.cos(y / 120) * 28 value = detail + grain + sweep pixels[x, y] = ( max(0, min(255, int(120 + (x * 90) // width + value))), max(0, min(255, int(118 + (y * 70) // height + value))), max(0, min(255, int(142 + value))), ) return image def full_exif(): """What an unmodified phone photo typically carries.""" exif = Image.Exif() exif[MAKE] = "Acme" exif[MODEL] = "Acme Phone 14 Pro" exif[SOFTWARE] = "AcmeOS 19.2" exif[DATETIME] = "2026:06:14 09:31:07" sub = exif.get_ifd(EXIF_IFD) sub[DATETIME_ORIGINAL] = "2026:06:14 09:31:07" sub[LENS_MODEL] = "Acme 26mm f/1.8" sub[BODY_SERIAL] = "SN-TEST-0001" gps = exif.get_ifd(GPS_IFD) gps[GPS_LAT_REF] = "N" gps[GPS_LAT] = LATITUDE gps[GPS_LON_REF] = "E" gps[GPS_LON] = LONGITUDE return exif def camera_only_exif(): """A photo whose location was never recorded: camera details, no GPS.""" exif = Image.Exif() exif[MAKE] = "Acme" exif[MODEL] = "Acme Phone 14 Pro" exif[DATETIME] = "2026:06:14 09:31:07" exif.get_ifd(EXIF_IFD)[DATETIME_ORIGINAL] = "2026:06:14 09:31:07" return exif def main(out_dir): out = Path(out_dir) out.mkdir(parents=True, exist_ok=True) image = canvas() written = [] for name, exif in [("phone-photo.jpg", full_exif()), ("camera-only.jpg", camera_only_exif())]: path = out / name image.save(path, "JPEG", quality=88, exif=exif.tobytes()) written.append(path) # The control: identical pixels, saved with no EXIF at all. path = out / "no-metadata.jpg" image.save(path, "JPEG", quality=88) written.append(path) for path in written: with Image.open(path) as check: read_back = check.getexif() gps = read_back.get_ifd(GPS_IFD) print(f"{path.name:18} {path.stat().st_size:>7} bytes " f"exif_tags={len(read_back)} gps_tags={len(gps)}") if __name__ == "__main__": main(sys.argv[1] if len(sys.argv) > 1 else "tmp/metadata-test-photos")