/* Lector y escritor de PNG (usa zlib solo para DEFLATE/CRC). */ #include #include "io_internal.h" static uint32_t get_u32(const uint8_t *p) { return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) | ((uint32_t)p[2] << 8) | p[3]; } void png_put_u32(uint8_t *p, uint32_t v) { p[0] = (uint8_t)(v >> 24); p[1] = (uint8_t)(v >> 16); p[2] = (uint8_t)(v >> 8); p[3] = (uint8_t)v; } static const uint8_t SIG[8] = {0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A}; void png_put_signature(tgb_sb *sb) { sb_append(sb, SIG, 8); } void png_put_chunk(tgb_sb *sb, const char type[4], const uint8_t *data, size_t len) { uint8_t hdr[8]; png_put_u32(hdr, (uint32_t)len); memcpy(hdr + 4, type, 4); sb_append(sb, hdr, 8); if (len) sb_append(sb, data, len); uLong crc = crc32(0, (const Bytef *)type, 4); if (len) crc = crc32(crc, data, (uInt)len); uint8_t c[4]; png_put_u32(c, (uint32_t)crc); sb_append(sb, c, 4); } /* ================= decodificación ================= */ static int paeth(int a, int b, int c) { int p = a + b - c, pa = abs(p - a), pb = abs(p - b), pc = abs(p - c); return (pa <= pb && pa <= pc) ? a : (pb <= pc ? b : c); } static tgb_status bad(const char *msg) { return tgb_fail(TGB_ERR_CORRUPT, "PNG dañado: %s", msg); } tgb_status tgb_png_decode_memory(const uint8_t *data, size_t len, tgb_canvas **out) { if (!data || !out) return tgb_fail(TGB_ERR_INVALID_ARG, "tgb_png_decode_memory: argumento nulo"); *out = NULL; if (len < 8 || memcmp(data, SIG, 8) != 0) return tgb_fail(TGB_ERR_FORMAT, "No es una imagen PNG (firma incorrecta)"); size_t pos = 8; uint32_t w = 0, h = 0; int depth = 0, ctype = 0; bool have_ihdr = false, have_iend = false; uint8_t palette[256 * 3]; uint8_t pal_alpha[256]; size_t pal_n = 0; bool have_trns = false; uint16_t key[3] = {0, 0, 0}; uint8_t *idat = NULL; size_t idat_len = 0, idat_cap = 0; memset(pal_alpha, 255, sizeof pal_alpha); tgb_status s = TGB_OK; while (pos < len && !have_iend) { if (len - pos < 12) { s = bad("chunk incompleto al final del archivo"); goto done; } uint32_t clen = get_u32(data + pos); const uint8_t *type = data + pos + 4; if (clen > 0x7FFFFFFFu || (size_t)clen > len - pos - 12) { s = bad("longitud de chunk mayor que el archivo (¿truncado?)"); goto done; } const uint8_t *cdata = data + pos + 8; uLong crc = crc32(crc32(0, type, 4), cdata, clen); if ((uint32_t)crc != get_u32(cdata + clen)) { s = bad("CRC incorrecto"); goto done; } if (!have_ihdr && memcmp(type, "IHDR", 4) != 0) { s = bad("el primer chunk no es IHDR"); goto done; } if (!memcmp(type, "IHDR", 4)) { if (have_ihdr || clen != 13) { s = bad("IHDR no válido"); goto done; } w = get_u32(cdata); h = get_u32(cdata + 4); depth = cdata[8]; ctype = cdata[9]; if (cdata[10] != 0 || cdata[11] != 0) { s = tgb_fail(TGB_ERR_UNSUPPORTED, "PNG con método de compresión o filtro desconocido"); goto done; } if (cdata[12] != 0) { s = tgb_fail(TGB_ERR_UNSUPPORTED, "PNG entrelazado (Adam7) no soportado todavía"); goto done; } if (w == 0 || h == 0 || w > TGB_MAX_DIMENSION || h > TGB_MAX_DIMENSION) { s = tgb_fail(TGB_ERR_LIMIT, "Imagen PNG de %ux%u fuera de los límites (máximo %d)", (unsigned)w, (unsigned)h, TGB_MAX_DIMENSION); goto done; } bool ok = (ctype == 0 && (depth == 1 || depth == 2 || depth == 4 || depth == 8 || depth == 16)) || (ctype == 2 && (depth == 8 || depth == 16)) || (ctype == 3 && (depth == 1 || depth == 2 || depth == 4 || depth == 8)) || (ctype == 4 && (depth == 8 || depth == 16)) || (ctype == 6 && (depth == 8 || depth == 16)); if (!ok) { s = bad("combinación de tipo de color y profundidad no válida"); goto done; } have_ihdr = true; } else if (!memcmp(type, "PLTE", 4)) { if (clen % 3 != 0 || clen == 0 || clen > 768) { s = bad("paleta no válida"); goto done; } pal_n = clen / 3; memcpy(palette, cdata, clen); } else if (!memcmp(type, "tRNS", 4)) { have_trns = true; if (ctype == 3) { if (clen > 256) { s = bad("tRNS demasiado largo"); goto done; } memcpy(pal_alpha, cdata, clen); } else if (ctype == 0 && clen == 2) key[0] = (uint16_t)((cdata[0] << 8) | cdata[1]); else if (ctype == 2 && clen == 6) for (int i = 0; i < 3; i++) key[i] = (uint16_t)((cdata[i * 2] << 8) | cdata[i * 2 + 1]); else if (ctype == 0 || ctype == 2) { s = bad("tRNS no válido"); goto done; } } else if (!memcmp(type, "IDAT", 4)) { size_t need = idat_len + clen; if (need > TGB_MAX_FILE_BYTES) { s = tgb_fail(TGB_ERR_LIMIT, "Datos de imagen PNG demasiado grandes"); goto done; } if (need > idat_cap) { size_t nc = idat_cap ? idat_cap * 2 : 65536; while (nc < need) nc *= 2; uint8_t *ni = realloc(idat, nc); if (!ni) { s = tgb_fail(TGB_ERR_NOMEM, "Sin memoria para decodificar PNG"); goto done; } idat = ni; idat_cap = nc; } memcpy(idat + idat_len, cdata, clen); idat_len += clen; } else if (!memcmp(type, "IEND", 4)) { have_iend = true; } else if ((type[0] & 0x20) == 0) { /* chunk crítico desconocido */ s = tgb_fail(TGB_ERR_UNSUPPORTED, "PNG con un chunk crítico desconocido '%.4s'", (const char *)type); goto done; } pos += 12 + clen; } if (!have_ihdr) { s = bad("falta IHDR"); goto done; } if (!have_iend) { s = bad("falta IEND: el archivo está truncado"); goto done; } if (idat_len == 0) { s = bad("no contiene datos de imagen"); goto done; } if (ctype == 3 && pal_n == 0) { s = bad("falta la paleta (PLTE)"); goto done; } { int samples = ctype == 0 ? 1 : ctype == 2 ? 3 : ctype == 3 ? 1 : ctype == 4 ? 2 : 4; size_t bits_pp = (size_t)samples * (size_t)depth; size_t rowb = ((size_t)w * bits_pp + 7) / 8; size_t bpp = bits_pp < 8 ? 1 : bits_pp / 8; size_t rawsz; if (!tgb_mul_ok(rowb + 1, h, &rawsz)) { s = tgb_fail(TGB_ERR_LIMIT, "Imagen PNG demasiado grande"); goto done; } uint8_t *raw = malloc(rawsz); if (!raw) { s = tgb_fail(TGB_ERR_NOMEM, "Sin memoria para decodificar PNG"); goto done; } uLongf dest = (uLongf)rawsz; int zr = uncompress(raw, &dest, idat, (uLong)idat_len); if (zr == Z_BUF_ERROR) { free(raw); s = bad("datos comprimidos incompletos o con más contenido del esperado"); goto done; } if (zr != Z_OK) { free(raw); s = bad("datos comprimidos (zlib) corruptos"); goto done; } if (dest != rawsz) { free(raw); s = bad("los datos de imagen son más cortos de lo esperado"); goto done; } for (uint32_t y = 0; y < h; y++) { uint8_t *row = raw + (size_t)y * (rowb + 1) + 1; uint8_t ft = row[-1]; const uint8_t *up = y ? row - (rowb + 1) : NULL; if (ft > 4) { free(raw); s = bad("tipo de filtro de fila desconocido"); goto done; } for (size_t i = 0; i < rowb; i++) { int a = i >= bpp ? row[i - bpp] : 0, b = up ? up[i] : 0, c = (up && i >= bpp) ? up[i - bpp] : 0; switch (ft) { case 1: row[i] = (uint8_t)(row[i] + a); break; case 2: row[i] = (uint8_t)(row[i] + b); break; case 3: row[i] = (uint8_t)(row[i] + (a + b) / 2); break; case 4: row[i] = (uint8_t)(row[i] + paeth(a, b, c)); break; default: break; } } } tgb_canvas *cv = tgb_canvas_create((int)w, (int)h); if (!cv) { free(raw); s = TGB_ERR_NOMEM; goto done; } uint8_t *px = tgb_canvas_pixels_mut(cv); unsigned maxv = (1u << (depth > 8 ? 8 : depth)) - 1u; for (uint32_t y = 0; y < h; y++) { const uint8_t *row = raw + (size_t)y * (rowb + 1) + 1; uint8_t *o = px + (size_t)y * w * 4; for (uint32_t x = 0; x < w; x++, o += 4) { uint8_t r, g, b, a = 255; if (ctype == 6) { size_t k = (size_t)x * 4 * (depth / 8) ; if (depth == 8) { r = row[k]; g = row[k + 1]; b = row[k + 2]; a = row[k + 3]; } else { r = row[k]; g = row[k + 2]; b = row[k + 4]; a = row[k + 6]; } } else if (ctype == 2) { size_t k = (size_t)x * 3 * (depth / 8); uint16_t vr, vg, vb; if (depth == 8) { vr = row[k]; vg = row[k + 1]; vb = row[k + 2]; r = row[k]; g = row[k + 1]; b = row[k + 2]; } else { vr = (uint16_t)((row[k] << 8) | row[k + 1]); vg = (uint16_t)((row[k + 2] << 8) | row[k + 3]); vb = (uint16_t)((row[k + 4] << 8) | row[k + 5]); r = row[k]; g = row[k + 2]; b = row[k + 4]; } if (have_trns && vr == key[0] && vg == key[1] && vb == key[2]) a = 0; } else if (ctype == 4) { size_t k = (size_t)x * 2 * (depth / 8); if (depth == 8) { r = g = b = row[k]; a = row[k + 1]; } else { r = g = b = row[k]; a = row[k + 2]; } } else { /* tipos 0 (gris) y 3 (paleta), cualquier profundidad */ unsigned v; if (depth == 16) v = (unsigned)((row[x * 2] << 8) | row[x * 2 + 1]); else if (depth == 8) v = row[x]; else { size_t bit = (size_t)x * (size_t)depth; v = (row[bit / 8] >> (8 - depth - (int)(bit % 8))) & maxv; } if (ctype == 3) { if (v >= pal_n) { tgb_canvas_release(cv); free(raw); s = bad("índice de paleta fuera de rango"); goto done; } r = palette[v * 3]; g = palette[v * 3 + 1]; b = palette[v * 3 + 2]; a = pal_alpha[v]; } else { unsigned g8 = depth == 16 ? (v >> 8) : (v * 255u / maxv); r = g = b = (uint8_t)g8; if (have_trns && v == key[0]) a = 0; } } if (a == 0) { o[0] = o[1] = o[2] = o[3] = 0; } /* forma canónica */ else { o[0] = r; o[1] = g; o[2] = b; o[3] = a; } } } free(raw); *out = cv; } done: free(idat); return s; } /* ================= codificación ================= */ static unsigned filter_cost(const uint8_t *row, size_t n) { unsigned sum = 0; for (size_t i = 0; i < n; i++) { int v = (int8_t)row[i]; sum += (unsigned)(v < 0 ? -v : v); } return sum; } tgb_status png_compress_image(const tgb_canvas *c, uint8_t **out, size_t *len) { size_t w = (size_t)tgb_canvas_width(c), h = (size_t)tgb_canvas_height(c), rowb = w * 4; const uint8_t *px = tgb_canvas_pixels(c); uint8_t *raw = malloc((rowb + 1) * h); uint8_t *cand = malloc(rowb * 5); if (!raw || !cand) { free(raw); free(cand); return tgb_fail(TGB_ERR_NOMEM, "Sin memoria para codificar PNG"); } for (size_t y = 0; y < h; y++) { const uint8_t *cur = px + y * rowb, *up = y ? px + (y - 1) * rowb : NULL; for (size_t i = 0; i < rowb; i++) { int a = i >= 4 ? cur[i - 4] : 0, b = up ? up[i] : 0, cc = (up && i >= 4) ? up[i - 4] : 0; cand[i] = cur[i]; cand[rowb + i] = (uint8_t)(cur[i] - a); cand[2 * rowb + i] = (uint8_t)(cur[i] - b); cand[3 * rowb + i] = (uint8_t)(cur[i] - (a + b) / 2); cand[4 * rowb + i] = (uint8_t)(cur[i] - paeth(a, b, cc)); } int best = 0; unsigned best_cost = filter_cost(cand, rowb); for (int f = 1; f < 5; f++) { unsigned cost = filter_cost(cand + (size_t)f * rowb, rowb); if (cost < best_cost) { best_cost = cost; best = f; } } raw[y * (rowb + 1)] = (uint8_t)best; memcpy(raw + y * (rowb + 1) + 1, cand + (size_t)best * rowb, rowb); } free(cand); uLongf zlen = compressBound((uLong)((rowb + 1) * h)); uint8_t *z = malloc(zlen); if (!z) { free(raw); return tgb_fail(TGB_ERR_NOMEM, "Sin memoria para codificar PNG"); } int r = compress2(z, &zlen, raw, (uLong)((rowb + 1) * h), 6); free(raw); if (r != Z_OK) { free(z); return tgb_fail(TGB_ERR_IO, "zlib no pudo comprimir la imagen (código %d)", r); } *out = z; *len = zlen; return TGB_OK; } tgb_status tgb_png_encode_memory(const tgb_canvas *canvas, uint8_t **out_data, size_t *out_len) { if (!canvas || !out_data || !out_len) return tgb_fail(TGB_ERR_INVALID_ARG, "tgb_png_encode_memory: argumento nulo"); uint8_t *z; size_t zl; tgb_status s = png_compress_image(canvas, &z, &zl); if (TGB_FAILED(s)) return s; tgb_sb sb; sb_init(&sb); uint8_t ihdr[13]; png_put_u32(ihdr, (uint32_t)tgb_canvas_width(canvas)); png_put_u32(ihdr + 4, (uint32_t)tgb_canvas_height(canvas)); ihdr[8] = 8; ihdr[9] = 6; ihdr[10] = 0; ihdr[11] = 0; ihdr[12] = 0; png_put_signature(&sb); png_put_chunk(&sb, "IHDR", ihdr, 13); png_put_chunk(&sb, "IDAT", z, zl); png_put_chunk(&sb, "IEND", NULL, 0); free(z); if (sb.oom) { sb_free(&sb); return tgb_fail(TGB_ERR_NOMEM, "Sin memoria para codificar PNG"); } *out_data = (uint8_t *)sb_take(&sb, out_len); return TGB_OK; } void tgb_free(void *p) { free(p); }