#!/usr/bin/env python3 """Verifica con herramientas INDEPENDIENTES (Pillow, ffprobe) los archivos que escribe Tahgibu. Uso: mkdir -p /tmp/tgb-art && TGB_TEST_ARTIFACTS=/tmp/tgb-art /test_io_files && python3 tools/verify-external.py /tmp/tgb-art""" import base64, io, json, subprocess, sys, shutil from PIL import Image, ImageSequence d = sys.argv[1] ok = True def check(cond, msg): global ok print((" OK " if cond else " FALLO ") + msg) ok &= bool(cond) png0 = Image.open(f"{d}/muestra_f0.png"); png0.load() check(png0.format == "PNG" and png0.mode == "RGBA" and png0.size == (48, 32), f"PNG exportado lo abre Pillow: {png0.format} {png0.mode} {png0.size}") v2 = json.load(open(f"{d}/muestra_v2.tgb")) check(v2["format"] == "TAHGIBU" and v2["version"] == 2 and v2["fps"] == 12 and (v2["width"], v2["height"]) == (48, 32), "cabecera formato 2 coincide con el ejemplo histórico") v2_frames = [Image.open(io.BytesIO(base64.b64decode(f))).convert("RGBA") for f in v2["frames"]] check(len(v2_frames) == 3 and all(f.size == (48, 32) for f in v2_frames), "formato 2: 3 fotogramas PNG válidos en Base64 (decodificados con base64+Pillow)") check(list(png0.getdata()) == list(v2_frames[0].getdata()), "el PNG exportado == fotograma 0 del .tgb v2") v3 = json.load(open(f"{d}/muestra_v3.tgb")) check(v3["version"] == 3 and "tgb3" in v3 and len(v3["frames"]) == 3, "formato 3: 'frames' aplanados + bloque 'tgb3'") v3_flat = [Image.open(io.BytesIO(base64.b64decode(f))).convert("RGBA") for f in v3["frames"]] check(all(list(a.getdata()) == list(b.getdata()) for a, b in zip(v3_flat, v2_frames)), "formato 3: los fotogramas aplanados == formato 2") imgs = [Image.open(io.BytesIO(base64.b64decode(s))) for s in v3["tgb3"]["images"]] check(all(i.size == (48, 32) and i.mode == "RGBA" for i in imgs), f"formato 3: {len(imgs)} imágenes de celda válidas") check([f["duration_ms"] for f in v3["tgb3"]["frames"]] == [100, 250, 40], "formato 3: duraciones por fotograma") ap = Image.open(f"{d}/muestra.apng") check(getattr(ap, "is_animated", False) and ap.n_frames == 3, f"APNG: Pillow lo reconoce animado con {getattr(ap, 'n_frames', '?')} fotogramas") frames, durs = [], [] for fr in ImageSequence.Iterator(ap): frames.append(fr.convert("RGBA").copy()); durs.append(fr.info.get("duration")) check(durs == [100, 250, 40], f"APNG: duraciones {durs}") check(all(list(a.getdata()) == list(b.getdata()) for a, b in zip(frames, v2_frames)), "APNG: cada fotograma == fotograma del proyecto (píxel a píxel)") if shutil.which("ffprobe"): out = subprocess.run(["ffprobe", "-v", "error", "-count_frames", "-select_streams", "v:0", "-show_entries", "stream=nb_read_frames,width,height,codec_name", "-of", "json", f"{d}/muestra.apng"], capture_output=True, text=True) info = json.loads(out.stdout or "{}").get("streams", [{}])[0] check(out.returncode == 0 and info.get("codec_name") == "apng" and int(info.get("nb_read_frames", 0)) == 3, f"ffprobe: codec={info.get('codec_name')} fotogramas={info.get('nb_read_frames')} {info.get('width')}x{info.get('height')}") else: print(" (ffprobe no disponible: omitido)") print("RESULTADO:", "TODO CORRECTO" if ok else "HAY FALLOS") sys.exit(0 if ok else 1)