V2.0.0
This commit is contained in:
1 parent
7f7fcd545d
commit
4d9a339421
114 files changed
+11383
-1837
No files matched your search
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Busca dependencias circulares entre cabeceras públicas y entre módulos internos (por #include "...")."""
|
||||
import os, re, sys
|
||||
root = os.path.join(os.path.dirname(__file__), "..")
|
||||
|
||||
def includes(path):
|
||||
out = []
|
||||
for line in open(path, encoding="utf-8"):
|
||||
m = re.match(r'\s*#\s*include\s+"([^"]+)"', line)
|
||||
if m: out.append(m.group(1))
|
||||
return out
|
||||
|
||||
def find_cycle(graph):
|
||||
state, stack = {}, []
|
||||
def dfs(n):
|
||||
state[n] = 1; stack.append(n)
|
||||
for m in graph.get(n, []):
|
||||
if state.get(m) == 1: return stack[stack.index(m):] + [m]
|
||||
if state.get(m) is None:
|
||||
c = dfs(m)
|
||||
if c: return c
|
||||
stack.pop(); state[n] = 2
|
||||
return None
|
||||
for n in list(graph):
|
||||
if state.get(n) is None:
|
||||
c = dfs(n)
|
||||
if c: return c
|
||||
return None
|
||||
|
||||
# 1) cabeceras públicas
|
||||
pub = os.path.join(root, "include", "tahgibu")
|
||||
g = {}
|
||||
for f in sorted(os.listdir(pub)):
|
||||
g[f] = [i.split("/")[-1] for i in includes(os.path.join(pub, f)) if i.startswith("tahgibu/")]
|
||||
c = find_cycle(g)
|
||||
print("Cabeceras públicas:", "CICLO " + " -> ".join(c) if c else "sin ciclos (%d cabeceras)" % len(g))
|
||||
|
||||
# 2) módulos internos (carpetas de src/, sin ui/ ni platform/) por dependencias de archivos .c/.h
|
||||
src = os.path.join(root, "src")
|
||||
mods = {}
|
||||
for d, _, files in os.walk(src):
|
||||
for f in files:
|
||||
if not f.endswith((".c", ".h")): continue
|
||||
p = os.path.join(d, f)
|
||||
mod = os.path.relpath(d, src).split(os.sep)[0]
|
||||
for inc in includes(p):
|
||||
tgt = os.path.normpath(os.path.join(d, inc))
|
||||
if tgt.startswith(src) and os.path.exists(tgt):
|
||||
tmod = os.path.relpath(os.path.dirname(tgt), src).split(os.sep)[0]
|
||||
if tmod != mod: mods.setdefault(mod, set()).add(tmod)
|
||||
for m, deps in sorted(mods.items()): print(" %-12s -> %s" % (m, ", ".join(sorted(deps))))
|
||||
c = find_cycle({k: sorted(v) for k, v in mods.items()})
|
||||
print("Módulos internos:", "CICLO " + " -> ".join(c) if c else "sin ciclos")
|
||||
sys.exit(0)
|
||||
Executable
+10
@@ -0,0 +1,10 @@
|
||||
#!/bin/sh
|
||||
# Comprueba SOLO la sintaxis y los tipos de src/ui y src/main.c contra una cabecera mínima de raylib
|
||||
# (tools/dev/raylib_stub). NO compila ni enlaza la aplicación: para eso hace falta raylib real (CMake).
|
||||
set -eu
|
||||
ROOT=$(cd "$(dirname "$0")/.." && pwd)
|
||||
for f in "$ROOT"/src/ui/*.c "$ROOT"/src/main.c; do
|
||||
echo "syntax-check $(basename "$f")"
|
||||
gcc -std=c11 -fsyntax-only -Wall -Wextra -Wshadow -Wpedantic -Werror -D_GNU_SOURCE -I"$ROOT/include" -I"$ROOT/tools/dev/raylib_stub" -I"$ROOT/src" "$f"
|
||||
done
|
||||
echo "OK: la interfaz es sintacticamente correcta frente a la cabecera de prueba (NO verificada contra raylib real)"
|
||||
Executable
+25
@@ -0,0 +1,25 @@
|
||||
#!/bin/sh
|
||||
# Mide qué líneas del núcleo ejecutan las pruebas (gcc + gcov). Resultado en /tmp/tgb-cov/resumen.txt
|
||||
set -eu
|
||||
ROOT=$(cd "$(dirname "$0")/.." && pwd)
|
||||
OUT=${OUT:-/tmp/tgb-cov}
|
||||
rm -rf "$OUT"; mkdir -p "$OUT/obj" "$OUT/tests"
|
||||
FLAGS="-std=c11 -g -O0 --coverage -D_GNU_SOURCE -I$ROOT/include"
|
||||
for f in $(ls "$ROOT"/src/core/*.c "$ROOT"/src/document/*.c "$ROOT"/src/history/*.c "$ROOT"/src/editor/*.c "$ROOT"/src/editor/tools/*.c \
|
||||
"$ROOT"/src/animation/*.c "$ROOT"/src/rendering/*.c "$ROOT"/src/io/*.c "$ROOT"/src/recovery/*.c "$ROOT"/src/extensions/*.c "$ROOT"/src/platform/linux/*.c); do
|
||||
gcc $FLAGS -c "$f" -o "$OUT/obj/$(echo "${f#$ROOT/src/}" | tr '/' '_').o"
|
||||
done
|
||||
for f in "$ROOT"/tests/plugins/*.c; do gcc -std=c11 -fPIC -shared -I"$ROOT/include" "$f" -o "$OUT/tests/$(basename "$f" .c).so"; done
|
||||
for t in "$ROOT"/tests/test_*.c; do
|
||||
n=$(basename "$t" .c)
|
||||
gcc $FLAGS -DTGB_TEST_PLUGIN_DIR="\"$OUT/tests\"" "$t" "$OUT"/obj/*.o -o "$OUT/tests/$n" -lz -lm -ldl --coverage
|
||||
(cd "$OUT/obj" && "$OUT/tests/$n" > /dev/null 2>&1) || echo "AVISO: $n falló"
|
||||
done
|
||||
cd "$OUT/obj"
|
||||
: > "$OUT/resumen.txt"
|
||||
for o in *.o; do
|
||||
src=$(echo "$o" | sed 's/\.o$//')
|
||||
gcov -n -o . "$o" 2>/dev/null | awk -v file="$src" "/^File '.*\/src\//{f=\$2} /Lines executed/{ if (f != \"\") { split(\$2,a,\":\"); sub(\"%\",\"\",a[2]); print f, a[2], \$4; f=\"\" } }" >> "$OUT/resumen.txt" || true
|
||||
done
|
||||
sed -e "s|'||g" -e "s|$ROOT/||" "$OUT/resumen.txt" | sort -k2 -n > "$OUT/resumen2.txt"
|
||||
cat "$OUT/resumen2.txt"
|
||||
Executable
+45
@@ -0,0 +1,45 @@
|
||||
#!/bin/sh
|
||||
# Compilación de DESARROLLO sin CMake: construye la biblioteca y las pruebas
|
||||
# con gcc directamente. Sirve para verificar el núcleo en máquinas sin CMake.
|
||||
# La compilación oficial del proyecto es CMake (ver docs/BUILD.md).
|
||||
# tools/dev-build.sh -> compila y ejecuta todas las pruebas
|
||||
# SAN=1 tools/dev-build.sh -> con AddressSanitizer + UBSan
|
||||
# OUT=/ruta tools/dev-build.sh -> carpeta de compilación (por defecto /tmp/tgb-dev)
|
||||
set -eu
|
||||
ROOT=$(cd "$(dirname "$0")/.." && pwd)
|
||||
OUT=${OUT:-/tmp/tgb-dev}
|
||||
CC=${CC:-gcc}
|
||||
FLAGS="-std=c11 -g -O1 -Wall -Wextra -Wshadow -Wpedantic -Werror -D_GNU_SOURCE -I$ROOT/include"
|
||||
[ "${SAN:-0}" = "1" ] && FLAGS="$FLAGS -fsanitize=address,undefined -fno-omit-frame-pointer -fno-sanitize-recover=all"
|
||||
mkdir -p "$OUT/obj" "$OUT/tests"
|
||||
echo "== Biblioteca (libtahgibu.a) =="
|
||||
OBJS=""
|
||||
for f in $(ls "$ROOT"/src/core/*.c "$ROOT"/src/document/*.c "$ROOT"/src/history/*.c "$ROOT"/src/editor/*.c \
|
||||
"$ROOT"/src/editor/tools/*.c "$ROOT"/src/animation/*.c "$ROOT"/src/rendering/*.c "$ROOT"/src/io/*.c \
|
||||
"$ROOT"/src/recovery/*.c "$ROOT"/src/extensions/*.c "$ROOT"/src/platform/linux/*.c 2>/dev/null); do
|
||||
o="$OUT/obj/$(echo "${f#$ROOT/src/}" | tr '/' '_').o"
|
||||
$CC $FLAGS -c "$f" -o "$o"
|
||||
OBJS="$OBJS $o"
|
||||
done
|
||||
rm -f "$OUT/libtahgibu.a"; ar rcs "$OUT/libtahgibu.a" $OBJS
|
||||
echo "== Extensiones de prueba =="
|
||||
for f in "$ROOT"/tests/plugins/*.c; do
|
||||
[ -e "$f" ] || continue
|
||||
n=$(basename "$f" .c)
|
||||
$CC $FLAGS -fPIC -shared "$f" -o "$OUT/tests/$n.so"
|
||||
done
|
||||
echo "== Pruebas =="
|
||||
FAIL=0
|
||||
for t in "$ROOT"/tests/test_*.c; do
|
||||
n=$(basename "$t" .c)
|
||||
$CC $FLAGS -DTGB_TEST_PLUGIN_DIR="\"$OUT/tests\"" "$t" "$OUT/libtahgibu.a" -o "$OUT/tests/$n" -lz -lm -ldl -lpthread
|
||||
echo "--- $n"
|
||||
if ! "$OUT/tests/$n"; then FAIL=1; fi
|
||||
done
|
||||
echo "--- ui_smoke (interfaz con raylib FALSO: no prueba raylib real)"
|
||||
if $CC $FLAGS -I"$ROOT/tools/dev/raylib_stub" -I"$ROOT/src" -I"$ROOT/tests" \
|
||||
"$ROOT"/tests/ui_smoke/ui_smoke.c "$ROOT"/tests/ui_smoke/mock_raylib.c "$ROOT"/src/ui/*.c \
|
||||
"$OUT/libtahgibu.a" -o "$OUT/tests/ui_smoke" -lz -lm -ldl -lpthread; then
|
||||
"$OUT/tests/ui_smoke" || FAIL=1
|
||||
else FAIL=1; fi
|
||||
[ $FAIL = 0 ] && echo "TODAS LAS PRUEBAS PASARON" || { echo "HAY PRUEBAS FALLIDAS"; exit 1; }
|
||||
@@ -0,0 +1,80 @@
|
||||
/* NO ES RAYLIB. Cabecera mínima escrita a mano con SOLO las declaraciones de la API de raylib 5.x
|
||||
* que usa la interfaz de Tahgibu. Sirve únicamente para comprobar la sintaxis y los tipos del
|
||||
* código de src/ui/ en máquinas sin raylib (gcc -fsyntax-only). No genera nada ejecutable.
|
||||
* Las firmas están escritas de memoria: la compilación REAL contra raylib sigue pendiente. */
|
||||
#ifndef RAYLIB_STUB_H
|
||||
#define RAYLIB_STUB_H
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
typedef struct Vector2 { float x, y; } Vector2;
|
||||
typedef struct Rectangle { float x, y, width, height; } Rectangle;
|
||||
typedef struct Color { unsigned char r, g, b, a; } Color;
|
||||
typedef struct Image { void *data; int width, height, mipmaps, format; } Image;
|
||||
typedef struct Texture { unsigned int id; int width, height, mipmaps, format; } Texture;
|
||||
typedef Texture Texture2D;
|
||||
typedef struct GlyphInfo { int value, offsetX, offsetY, advanceX; Image image; } GlyphInfo;
|
||||
typedef struct Font { int baseSize, glyphCount, glyphPadding; Texture2D texture; Rectangle *recs; GlyphInfo *glyphs; } Font;
|
||||
|
||||
enum { FLAG_VSYNC_HINT = 0x40, FLAG_WINDOW_RESIZABLE = 0x04 };
|
||||
enum { MOUSE_BUTTON_LEFT = 0, MOUSE_BUTTON_RIGHT = 1, MOUSE_BUTTON_MIDDLE = 2 };
|
||||
enum { TEXTURE_FILTER_POINT = 0 };
|
||||
enum { PIXELFORMAT_UNCOMPRESSED_R8G8B8A8 = 7 };
|
||||
enum {
|
||||
KEY_NULL = 0, KEY_SPACE = 32, KEY_COMMA = 44, KEY_PERIOD = 46,
|
||||
KEY_B = 66, KEY_C = 67, KEY_E = 69, KEY_F = 70, KEY_I = 73, KEY_L = 76, KEY_N = 78, KEY_O = 79, KEY_P = 80,
|
||||
KEY_R = 82, KEY_S = 83, KEY_V = 86, KEY_Y = 89, KEY_Z = 90,
|
||||
KEY_LEFT_BRACKET = 91, KEY_RIGHT_BRACKET = 93,
|
||||
KEY_ESCAPE = 256, KEY_ENTER = 257, KEY_BACKSPACE = 259, KEY_DELETE = 261,
|
||||
KEY_LEFT_SHIFT = 340, KEY_LEFT_CONTROL = 341, KEY_RIGHT_CONTROL = 345, KEY_RIGHT_SHIFT = 344
|
||||
};
|
||||
|
||||
void SetConfigFlags(unsigned int flags);
|
||||
void InitWindow(int width, int height, const char *title);
|
||||
void CloseWindow(void);
|
||||
bool WindowShouldClose(void);
|
||||
void SetWindowMinSize(int width, int height);
|
||||
void SetTargetFPS(int fps);
|
||||
void SetExitKey(int key);
|
||||
int GetScreenWidth(void);
|
||||
int GetScreenHeight(void);
|
||||
float GetFrameTime(void);
|
||||
double GetTime(void);
|
||||
void BeginDrawing(void);
|
||||
void EndDrawing(void);
|
||||
void ClearBackground(Color color);
|
||||
void BeginScissorMode(int x, int y, int width, int height);
|
||||
void EndScissorMode(void);
|
||||
|
||||
void DrawRectangle(int posX, int posY, int width, int height, Color color);
|
||||
void DrawRectangleRec(Rectangle rec, Color color);
|
||||
void DrawRectangleLines(int posX, int posY, int width, int height, Color color);
|
||||
void DrawRectangleLinesEx(Rectangle rec, float lineThick, Color color);
|
||||
void DrawCircleLines(int centerX, int centerY, float radius, Color color);
|
||||
void DrawText(const char *text, int posX, int posY, int fontSize, Color color);
|
||||
int MeasureText(const char *text, int fontSize);
|
||||
Font GetFontDefault(void);
|
||||
Font LoadFontEx(const char *fileName, int fontSize, int *codepoints, int codepointCount);
|
||||
void UnloadFont(Font font);
|
||||
void DrawTextEx(Font font, const char *text, Vector2 position, float fontSize, float spacing, Color tint);
|
||||
Vector2 MeasureTextEx(Font font, const char *text, float fontSize, float spacing);
|
||||
|
||||
Texture2D LoadTextureFromImage(Image image);
|
||||
void UpdateTexture(Texture2D texture, const void *pixels);
|
||||
void UnloadTexture(Texture2D texture);
|
||||
void SetTextureFilter(Texture2D texture, int filter);
|
||||
void DrawTexturePro(Texture2D texture, Rectangle source, Rectangle dest, Vector2 origin, float rotation, Color tint);
|
||||
|
||||
Vector2 GetMousePosition(void);
|
||||
bool IsMouseButtonPressed(int button);
|
||||
bool IsMouseButtonDown(int button);
|
||||
bool IsMouseButtonReleased(int button);
|
||||
float GetMouseWheelMove(void);
|
||||
bool IsKeyPressed(int key);
|
||||
bool IsKeyPressedRepeat(int key);
|
||||
bool IsKeyDown(int key);
|
||||
int GetCharPressed(void);
|
||||
const char *GetClipboardText(void);
|
||||
bool CheckCollisionPointRec(Vector2 point, Rectangle rec);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Genera tests/fixtures_png.h con PNG producidos por una implementación
|
||||
INDEPENDIENTE (Pillow) y los píxeles RGBA esperados, para comprobar que nuestro
|
||||
decodificador entiende PNG reales y no solo los que escribe Tahgibu.
|
||||
Uso: python3 tools/gen_png_fixtures.py > tests/fixtures_png.h"""
|
||||
import io, struct, zlib
|
||||
from PIL import Image
|
||||
|
||||
W, H = 4, 3
|
||||
|
||||
def png_bytes(img, **kw):
|
||||
b = io.BytesIO(); img.save(b, "PNG", **kw); return b.getvalue()
|
||||
|
||||
def expected(img):
|
||||
rgba = img.convert("RGBA")
|
||||
out = []
|
||||
for y in range(H):
|
||||
for x in range(W):
|
||||
r, g, b, a = rgba.getpixel((x, y))
|
||||
if a == 0: r = g = b = 0 # forma canónica de Tahgibu
|
||||
out += [r, g, b, a]
|
||||
return out
|
||||
|
||||
def pat(f):
|
||||
return [f(x, y) for y in range(H) for x in range(W)]
|
||||
|
||||
fixtures = []
|
||||
# RGBA 8
|
||||
im = Image.new("RGBA", (W, H)); im.putdata(pat(lambda x, y: (x * 60, y * 100, 200 - x * 10, 255 if (x + y) % 3 else (x + y) * 20)))
|
||||
fixtures.append(("rgba8", png_bytes(im), expected(im)))
|
||||
# RGB 8
|
||||
im = Image.new("RGB", (W, H)); im.putdata(pat(lambda x, y: (x * 50, y * 90, 17 * (x + y))))
|
||||
fixtures.append(("rgb8", png_bytes(im), expected(im)))
|
||||
# Gris 8
|
||||
im = Image.new("L", (W, H)); im.putdata(pat(lambda x, y: x * 40 + y * 10))
|
||||
fixtures.append(("gray8", png_bytes(im), expected(im)))
|
||||
# Gris + alfa
|
||||
im = Image.new("LA", (W, H)); im.putdata(pat(lambda x, y: (x * 30 + 5, 255 - y * 100)))
|
||||
fixtures.append(("graya8", png_bytes(im), expected(im)))
|
||||
# Paleta con transparencia (tRNS)
|
||||
im = Image.new("P", (W, H)); pal = []
|
||||
for i in range(4): pal += [i * 60, 255 - i * 60, i * 20]
|
||||
im.putpalette(pal + [0] * (768 - len(pal))); im.putdata(pat(lambda x, y: (x + y) % 4))
|
||||
fixtures.append(("palette_trns", png_bytes(im, transparency=bytes([255, 128, 0, 255])), None))
|
||||
pm = Image.open(io.BytesIO(fixtures[-1][1])).convert("RGBA")
|
||||
fixtures[-1] = (fixtures[-1][0], fixtures[-1][1], [v for y in range(H) for x in range(W) for v in ((0,0,0,0) if pm.getpixel((x,y))[3]==0 else pm.getpixel((x,y)))])
|
||||
# 1 bit
|
||||
im = Image.new("1", (W, H)); im.putdata(pat(lambda x, y: 255 if (x + y) % 2 else 0))
|
||||
fixtures.append(("bilevel", png_bytes(im), expected(im)))
|
||||
# Paleta de 2 bits (4 colores) con filtros variados
|
||||
im = Image.new("P", (W, H)); im.putpalette([255,0,0, 0,255,0, 0,0,255, 255,255,0] + [0]*756); im.putdata(pat(lambda x, y: (x * 2 + y) % 4))
|
||||
fixtures.append(("palette2bit", png_bytes(im, bits=2), None))
|
||||
pm = Image.open(io.BytesIO(fixtures[-1][1])).convert("RGBA")
|
||||
fixtures[-1] = (fixtures[-1][0], fixtures[-1][1], [v for y in range(H) for x in range(W) for v in pm.getpixel((x,y))])
|
||||
# Gris 16 bits
|
||||
im = Image.new("I;16", (W, H)); im.putdata(pat(lambda x, y: x * 4000 + y * 9000))
|
||||
data = png_bytes(im)
|
||||
exp = []
|
||||
for y in range(H):
|
||||
for x in range(W):
|
||||
v = (x * 4000 + y * 9000) >> 8
|
||||
exp += [v, v, v, 255]
|
||||
fixtures.append(("gray16", data, exp))
|
||||
|
||||
print("/* GENERADO por tools/gen_png_fixtures.py con Pillow: no editar a mano. */")
|
||||
print("#ifndef TGB_FIXTURES_PNG_H\n#define TGB_FIXTURES_PNG_H\n#include <stddef.h>\n#include <stdint.h>")
|
||||
print("#define FIX_W %d\n#define FIX_H %d" % (W, H))
|
||||
for name, data, exp in fixtures:
|
||||
print("static const uint8_t fix_%s[] = {%s};" % (name, ",".join(str(b) for b in data)))
|
||||
print("static const uint8_t fix_%s_px[] = {%s};" % (name, ",".join(str(v) for v in exp)))
|
||||
print("typedef struct { const char *name; const uint8_t *png; size_t len; const uint8_t *px; } png_fixture;")
|
||||
print("static const png_fixture png_fixtures[] = {")
|
||||
for name, data, exp in fixtures:
|
||||
print(' {"%s", fix_%s, sizeof fix_%s, fix_%s_px},' % (name, name, name, name))
|
||||
print("};\n#endif")
|
||||
@@ -0,0 +1,48 @@
|
||||
#!/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 <ruta>/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)
|
||||
Reference in new issue
Block a user