code wiki / (root) / nx_png_decoder.nx

nx_png_decoder.nx source

↩ module page · 822 lines · 32851 B

1// nx_png_decoder.nx -- RFC 2083 PNG decoder. 2// 3// CAPABILITY_COMPLETENESS: PARTIAL 4// MISSING_CAPABILITIES: 5// - color-type 3 (palette / PLTE chunk): IMPLEMENTED (PLTE walker + post-defilter expansion to 6// packed RGB; gate-proven by nx_boxtex_gate T1 on Khronos BoxTextured, 66 distinct colours). 7// This line read -queued, low-priority- until 2026-08-05. That stale claim was COPIED into 8// nx_image_gray.nx, nx_image_index.nx, nx_image_grade_v0.nx and a USER-VISIBLE error string in 9// nx_uiq_visual_lib.nx, so a shipped capability kept being re-scoped as unbuilt work. 10// A STALE COMMENT IS A DEFECT: it costs the same as a missing feature and hides longer. 11// (most modern PNGs use RGB/RGBA; palette is rare for new 12// content but common in legacy assets) 13// - Adam7 interlacing: IMPLEMENTED v4 2026-08-05 (all 7 passes, depths 14// 1/2/4/8, gate nx_png_adam7_gate 5/5; interlaced 16-bit stays sealed 15// as NX_PNG_ERR_UNSUPPORTED_INTERLACE -- rare, and no consumer of the 16// linear 16-bit two-byte contract is interlaced) 17// - 1 / 2 / 4 bit-depth sub-byte packing: IMPLEMENTED v3 2026-07-24 18// (gray scales to 0..255, palette indices stay raw for PLTE) 19// - 16-bit-depth high-byte handling: scanlines are processed 20// as bytes; 16-bit samples are filtered correctly per spec 21// but high/low byte split is NOT presented as native u16 -- 22// caller reads two bytes per sample. 23// - ancillary chunks (tEXt / zTXt / iTXt / pHYs / gAMA / 24// cHRM / sRGB / iCCP / etc.) are skipped (CRC validated, 25// contents discarded); queued as nx_png_ancillary.nx 26// 27// Per cardinal `feedback-no-skip-paths-as-error-codes`: this 28// primitive ships with COMPLETENESS=PARTIAL so callers know 29// exactly what's covered. Caller can test for 30// NX_PNG_ERR_UNSUPPORTED_* to detect when they hit one of the 31// queued capabilities. 32// 33// PNG file structure: 34// 8-byte signature: 89 50 4E 47 0D 0A 1A 0A 35// sequence of chunks; first must be IHDR, last must be IEND 36// each chunk: 37// 4 bytes BE data length 38// 4 bytes chunk type (ASCII) 39// N bytes data 40// 4 bytes BE CRC-32 of (type + data) 41// 42// Scanline filtering (per RFC 2083 section 9): 43// each scanline begins with a 1-byte filter type: 44// 0 = None, 1 = Sub, 2 = Up, 3 = Average, 4 = Paeth 45// filters operate on raw scanline bytes; bpp = bytes-per-pixel 46// (1 for grayscale 8-bit, 2 for grayscale 16-bit + 8-bit GA, 47// 3 for RGB 8-bit, 4 for RGB 16-bit + RGBA 8-bit + 8-bit GA, 48// 6 for RGB 16-bit, 8 for RGBA 16-bit) 49// 50// Composes: nx_zlib_wrap (IDAT decompression), nx_crc32 (chunk 51// CRC validation), nx_png_header (existing magic + signature). 52// 53// genealogy_id: rfc2083_png_1996 54// lineage_id: nx_png_decoder_v1_partial 55 56// nx_safety_envelope: 57// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 58// sil_target: SIL1 59// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 60// verdict: NOT_YET_EVALUATED 61 62import "nx_syscalls.nx" 63import "nx_runtime.nx" 64import "nx_tier.nx" 65import "nx_zlib_wrap.nx" 66import "nx_crc32.nx" 67const NX_MAGIC_2026: i64 = 2026 68const NX_MAGIC_67108864: i64 = 67108864 69const NX_MAGIC_262144: i64 = 262144 70 71// ===== error codes =============================================== 72 73const NX_PNG_OK: nx_int = 0 74const NX_PNG_ERR_TOO_SHORT: nx_int = 1 75const NX_PNG_ERR_BAD_SIGNATURE: nx_int = 2 76const NX_PNG_ERR_NO_IHDR: nx_int = 3 77const NX_PNG_ERR_BAD_IHDR: nx_int = 4 78const NX_PNG_ERR_BAD_CRC: nx_int = 5 79const NX_PNG_ERR_UNSUPPORTED_COLOR: nx_int = 6 // partial-capability 80const NX_PNG_ERR_UNSUPPORTED_DEPTH: nx_int = 7 // partial-capability 81const NX_PNG_ERR_UNSUPPORTED_INTERLACE: nx_int = 8 // partial-capability 82const NX_PNG_ERR_DECOMPRESS: nx_int = 9 83const NX_PNG_ERR_BAD_FILTER: nx_int = 10 84const NX_PNG_ERR_SCANLINE_LEN: nx_int = 11 85const NX_PNG_ERR_NO_IEND: nx_int = 12 86 87// ===== filter types ============================================== 88 89const NX_PNG_FILTER_NONE: nx_int = 0 90const NX_PNG_FILTER_SUB: nx_int = 1 91const NX_PNG_FILTER_UP: nx_int = 2 92const NX_PNG_FILTER_AVERAGE: nx_int = 3 93const NX_PNG_FILTER_PAETH: nx_int = 4 94 95// ===== color types =============================================== 96 97const NX_PNG_COLOR_GRAY: nx_int = 0 98const NX_PNG_COLOR_RGB: nx_int = 2 99const NX_PNG_COLOR_PALETTE: nx_int = 3 // unsupported v1 100const NX_PNG_COLOR_GRAY_ALPHA: nx_int = 4 101const NX_PNG_COLOR_RGBA: nx_int = 6 102 103// ===== IHDR struct =============================================== 104 105// FIELDS ARE i64 ON PURPOSE: nx_cc lays out nx_int struct fields as SIZE 0 106// (offset collapse -> garbage/SIGSEGV on rebuild) -- the banked 2026-07-21 107// codegen bug. i64 until the compiler resolves aliases before layout. 108struct NxPngHeader { 109 width: i64, 110 height: i64, 111 bit_depth: i64, 112 color_type: i64, 113 compression: i64, 114 filter: i64, 115 interlace: i64, 116} 117 118const NX_PNG_HEADER_BYTES: nx_size = 56 119 120// ===== result struct ============================================= 121 122// i64 fields for the same nx_cc layout bug (see NxPngHeader note above). 123struct NxPngResult { 124 header: *NxPngHeader, 125 pixels: *u8, 126 pixels_size: i64, 127 n_channels: i64, 128 bytes_per_pix: i64, 129 error_code: i64, 130} 131 132const NX_PNG_RESULT_BYTES: nx_size = 48 // 6 x 8B fields (was 40 = one field short; mmap page-rounding hid it) 133 134// ===== big-endian helpers ======================================== 135 136func _png_read_u32_be(buf: *u8, off: nx_int) -> nx_int { 137 let b0: nx_int = (buf[off] as nx_int) & 255 138 let b1: nx_int = (buf[off + 1] as nx_int) & 255 139 let b2: nx_int = (buf[off + 2] as nx_int) & 255 140 let b3: nx_int = (buf[off + 3] as nx_int) & 255 141 return (b0 << 24) | (b1 << 16) | (b2 << 8) | b3 142} 143 144// ===== signature check ============================================ 145 146func _png_check_signature(buf: *u8, n: nx_int) -> nx_int { 147 if n < 8 { return 0 } 148 if buf[0] != (137 as u8) { return 0 } // 0x89 149 if buf[1] != (80 as u8) { return 0 } // 'P' 150 if buf[2] != (78 as u8) { return 0 } // 'N' 151 if buf[3] != (71 as u8) { return 0 } // 'G' 152 if buf[4] != (13 as u8) { return 0 } // 0x0D 153 if buf[5] != (10 as u8) { return 0 } // 0x0A 154 if buf[6] != (26 as u8) { return 0 } // 0x1A 155 if buf[7] != (10 as u8) { return 0 } // 0x0A 156 return 1 157} 158 159// ===== channel + bpp lookup ====================================== 160 161func _png_n_channels(color_type: nx_int) -> nx_int { 162 if color_type == NX_PNG_COLOR_GRAY { return 1 } 163 if color_type == NX_PNG_COLOR_RGB { return 3 } 164 if color_type == NX_PNG_COLOR_GRAY_ALPHA { return 2 } 165 if color_type == NX_PNG_COLOR_RGBA { return 4 } 166 if color_type == NX_PNG_COLOR_PALETTE { return 1 } // v2 (NX_MAGIC_2026-07-11): one index byte per pixel (8-bit); expanded via PLTE post-defilter 167 return -1 168} 169// v2: PLTE palette store (populated by the chunk walker for color-type 3) 170static NXPNG_PAL: i64 // ptr to up to 256*3 bytes 171static NXPNG_PALN: i64 // entries 172 173func _png_bytes_per_pixel(color_type: nx_int, bit_depth: nx_int) -> nx_int { 174 let n_ch: nx_int = _png_n_channels(color_type) 175 if n_ch < 0 { return -1 } 176 let bits_per_pixel: nx_int = n_ch * bit_depth 177 let bpp: nx_int = (bits_per_pixel + 7) / 8 178 return bpp 179} 180 181// ===== Paeth predictor (RFC 2083 section 9.4) ==================== 182 183func _png_paeth(a: nx_int, b: nx_int, c: nx_int) -> nx_int { 184 let p: nx_int = a + b - c 185 var pa: nx_int = p - a 186 if pa < 0 { pa = 0 - pa } 187 var pb: nx_int = p - b 188 if pb < 0 { pb = 0 - pb } 189 var pc: nx_int = p - c 190 if pc < 0 { pc = 0 - pc } 191 if pa <= pb { 192 if pa <= pc { return a } 193 } 194 if pb <= pc { return b } 195 return c 196} 197 198// ===== scanline filter reverse =================================== 199// 200// Reverses one filtered scanline of length `width_bytes`. The 201// caller provides the previous (already-unfiltered) scanline OR 202// 0 for the first scanline. bpp = bytes-per-pixel; for filter 203// Sub / Paeth we need the byte `bpp` to the left in the CURRENT 204// scanline. The output is written into `out_line`. 205 206func _png_unfilter_scanline( 207 filt_type: nx_int, 208 filt_data: *u8, // filtered scanline bytes (length = width_bytes) 209 prev_line: *u8, // previous scanline (or 0 for first) 210 out_line: *u8, 211 width_bytes: nx_int, bpp: nx_int) -> nx_int { 212 213 if filt_type == NX_PNG_FILTER_NONE { 214 var i: nx_int = 0 215 while i < width_bytes { 216 out_line[i] = filt_data[i] 217 i = i + 1 218 } 219 return 0 220 } 221 if filt_type == NX_PNG_FILTER_SUB { 222 var i: nx_int = 0 223 while i < width_bytes { 224 var left: nx_int = 0 225 if i >= bpp { left = (out_line[i - bpp] as nx_int) & 255 } 226 let v: nx_int = (((filt_data[i] as nx_int) & 255) + left) & 255 227 out_line[i] = v as u8 228 i = i + 1 229 } 230 return 0 231 } 232 if filt_type == NX_PNG_FILTER_UP { 233 var i: nx_int = 0 234 while i < width_bytes { 235 var above: nx_int = 0 236 if prev_line != (0 as *u8) { 237 above = (prev_line[i] as nx_int) & 255 238 } 239 let v: nx_int = (((filt_data[i] as nx_int) & 255) + above) & 255 240 out_line[i] = v as u8 241 i = i + 1 242 } 243 return 0 244 } 245 if filt_type == NX_PNG_FILTER_AVERAGE { 246 var i: nx_int = 0 247 while i < width_bytes { 248 var left: nx_int = 0 249 if i >= bpp { left = (out_line[i - bpp] as nx_int) & 255 } 250 var above: nx_int = 0 251 if prev_line != (0 as *u8) { 252 above = (prev_line[i] as nx_int) & 255 253 } 254 let avg: nx_int = (left + above) / 2 255 let v: nx_int = (((filt_data[i] as nx_int) & 255) + avg) & 255 256 out_line[i] = v as u8 257 i = i + 1 258 } 259 return 0 260 } 261 if filt_type == NX_PNG_FILTER_PAETH { 262 var i: nx_int = 0 263 while i < width_bytes { 264 var left: nx_int = 0 265 if i >= bpp { left = (out_line[i - bpp] as nx_int) & 255 } 266 var above: nx_int = 0 267 if prev_line != (0 as *u8) { 268 above = (prev_line[i] as nx_int) & 255 269 } 270 var upper_left: nx_int = 0 271 if i >= bpp { 272 if prev_line != (0 as *u8) { 273 upper_left = (prev_line[i - bpp] as nx_int) & 255 274 } 275 } 276 let pred: nx_int = _png_paeth(left, above, upper_left) 277 let v: nx_int = (((filt_data[i] as nx_int) & 255) + pred) & 255 278 out_line[i] = v as u8 279 i = i + 1 280 } 281 return 0 282 } 283 return -1 284} 285 286// ===== Adam7 interlace (v4, 2026-08-05) ========================== 287// Canonical pass tables (RFC 2083 sec 2.6 / libpng png_pass_*): 288// pass: 1 2 3 4 5 6 7 289// start row: 0 0 4 0 2 0 1 290// start col: 0 4 0 2 0 1 0 291// row inc: 8 8 8 4 4 2 2 292// col inc: 8 8 4 4 2 2 1 293 294func _png_a7_row0(p: nx_int) -> nx_int { if p==0 {return 0} if p==1 {return 0} if p==2 {return 4} if p==3 {return 0} if p==4 {return 2} if p==5 {return 0} return 1 } 295func _png_a7_col0(p: nx_int) -> nx_int { if p==0 {return 0} if p==1 {return 4} if p==2 {return 0} if p==3 {return 2} if p==4 {return 0} if p==5 {return 1} return 0 } 296func _png_a7_rowi(p: nx_int) -> nx_int { if p==0 {return 8} if p==1 {return 8} if p==2 {return 8} if p==3 {return 4} if p==4 {return 4} if p==5 {return 2} return 2 } 297func _png_a7_coli(p: nx_int) -> nx_int { if p==0 {return 8} if p==1 {return 8} if p==2 {return 4} if p==3 {return 4} if p==4 {return 2} if p==5 {return 2} return 1 } 298 299// expected post-inflate byte count of an Adam7 stream: each non-empty pass 300// contributes ph scanlines of (1 filter byte + its own width_bytes). 301func _png_a7_expected(w: nx_int, h: nx_int, nch: nx_int, bd: nx_int) -> nx_int { 302 var total: nx_int = 0 303 var p: nx_int = 0 304 while p < 7 { 305 let pw: nx_int = (w - _png_a7_col0(p) + _png_a7_coli(p) - 1) / _png_a7_coli(p) 306 let ph: nx_int = (h - _png_a7_row0(p) + _png_a7_rowi(p) - 1) / _png_a7_rowi(p) 307 if pw > 0 { if ph > 0 { 308 let wb: nx_int = (pw * nch * bd + 7) / 8 309 total = total + ph * (1 + wb) 310 } } 311 p = p + 1 312 } 313 return total 314} 315 316// Decode all 7 passes into a full-size 8-bit-per-sample plane (w*nch bytes per 317// row). Each pass defilters against ITS OWN previous scanline (per spec), then 318// its samples scatter to (row0 + ry*rowi, col0 + sx*coli). Gray sub-byte 319// samples scale to 0..255; palette indices stay raw for the PLTE lookup. 320// Returns 0, or -1 on a bad filter byte. 321func _png_a7_deinterlace(zdata: *u8, w: nx_int, h: nx_int, nch: nx_int, bd: nx_int, 322 ct: nx_int, bpp: nx_int, out8: *u8) -> nx_int { 323 var zoff: nx_int = 0 324 let smask: nx_int = (1 << bd) - 1 325 var p: nx_int = 0 326 while p < 7 { 327 let c0: nx_int = _png_a7_col0(p) 328 let r0: nx_int = _png_a7_row0(p) 329 let ci: nx_int = _png_a7_coli(p) 330 let ri: nx_int = _png_a7_rowi(p) 331 let pw: nx_int = (w - c0 + ci - 1) / ci 332 let ph: nx_int = (h - r0 + ri - 1) / ri 333 if pw > 0 { if ph > 0 { 334 let wb: nx_int = (pw * nch * bd + 7) / 8 335 let pbuf: *u8 = sys_mmap((ph * wb) as nx_size) 336 var ry: nx_int = 0 337 while ry < ph { 338 let ft: nx_int = (zdata[zoff] as nx_int) & 255 339 if ft > 4 { return 0 - 1 } 340 let fl: *u8 = (zdata as nx_int + zoff + 1) as *u8 341 let ol: *u8 = (pbuf as nx_int + ry * wb) as *u8 342 var pl: *u8 = 0 as *u8 343 if ry > 0 { pl = (pbuf as nx_int + (ry - 1) * wb) as *u8 } 344 if _png_unfilter_scanline(ft, fl, pl, ol, wb, bpp) != 0 { return 0 - 1 } 345 zoff = zoff + 1 + wb 346 ry = ry + 1 347 } 348 var sy: nx_int = 0 349 while sy < ph { 350 let prow: *u8 = (pbuf as nx_int + sy * wb) as *u8 351 let dsty: nx_int = r0 + sy * ri 352 var sx: nx_int = 0 353 while sx < pw { 354 let dstx: nx_int = c0 + sx * ci 355 var chn: nx_int = 0 356 while chn < nch { 357 let sidx: nx_int = sx * nch + chn 358 var v: nx_int = 0 359 if bd == 8 { v = (prow[sidx] as nx_int) & 255 } else { 360 let bitpos: nx_int = sidx * bd 361 let byi: nx_int = bitpos / 8 362 let boff: nx_int = bitpos % 8 363 let shift: nx_int = 8 - boff - bd 364 v = ((prow[byi] as nx_int) >> shift) & smask 365 if ct == NX_PNG_COLOR_GRAY { v = v * 255 / smask } 366 } 367 out8[(dsty * w + dstx) * nch + chn] = v as u8 368 chn = chn + 1 369 } 370 sx = sx + 1 371 } 372 sy = sy + 1 373 } 374 } } 375 p = p + 1 376 } 377 return 0 378} 379 380// ===== top-level decode ========================================== 381 382func nx_png_decode(input: *u8, input_size: nx_int) -> *NxPngResult { 383 let r_ptr: *u8 = sys_mmap(NX_PNG_RESULT_BYTES) 384 let r: *NxPngResult = r_ptr as *NxPngResult 385 r.header = 0 as *NxPngHeader 386 r.pixels = 0 as *u8 387 r.pixels_size = 0 388 r.error_code = NX_PNG_OK 389 390 if input_size < 8 { 391 r.error_code = NX_PNG_ERR_TOO_SHORT 392 return r 393 } 394 if _png_check_signature(input, input_size) == 0 { 395 r.error_code = NX_PNG_ERR_BAD_SIGNATURE 396 return r 397 } 398 399 var off: nx_int = 8 400 var ihdr_seen: nx_int = 0 401 var iend_seen: nx_int = 0 402 let header_ptr: *u8 = sys_mmap(NX_PNG_HEADER_BYTES) 403 let header: *NxPngHeader = header_ptr as *NxPngHeader 404 405 // We accumulate IDAT data into a growing buffer. Cap at 64MiB 406 // for substrate (sufficient for any reasonable user image; larger 407 // assets get queued as nx_png_stream). 408 let MAX_IDAT: nx_int = NX_MAGIC_67108864 409 let idat_buf: *u8 = sys_mmap(MAX_IDAT as nx_size) 410 var idat_len: nx_int = 0 411 412 // JPL Rule 2: bounded chunk walk. 413 var safety: nx_int = 0 414 let MAX_CHUNKS: nx_int = NX_MAGIC_262144 415 while safety < MAX_CHUNKS { 416 if iend_seen == 1 { break } 417 if (off + 8) > input_size { 418 r.error_code = NX_PNG_ERR_TOO_SHORT 419 return r 420 } 421 let data_len: nx_int = _png_read_u32_be(input, off) 422 off = off + 4 423 let type_off: nx_int = off 424 let t0: nx_int = (input[type_off] as nx_int) & 255 425 let t1: nx_int = (input[type_off + 1] as nx_int) & 255 426 let t2: nx_int = (input[type_off + 2] as nx_int) & 255 427 let t3: nx_int = (input[type_off + 3] as nx_int) & 255 428 off = off + 4 429 let data_off: nx_int = off 430 if (off + data_len + 4) > input_size { 431 r.error_code = NX_PNG_ERR_TOO_SHORT 432 return r 433 } 434 off = off + data_len 435 436 // CRC-32 over type + data. 437 let crc_total_len: nx_int = 4 + data_len 438 let crc_buf: *u8 = (input as nx_int + type_off) as *u8 439 let crc_expected: nx_int = _png_read_u32_be(input, off) 440 let crc_computed: nx_int = nx_crc32(crc_buf, crc_total_len) 441 off = off + 4 442 if crc_computed != crc_expected { 443 r.error_code = NX_PNG_ERR_BAD_CRC 444 return r 445 } 446 447 // IHDR = "IHDR" = 73 72 68 82 448 if t0 == 73 { 449 if t1 == 72 { 450 if t2 == 68 { 451 if t3 == 82 { 452 if data_len != 13 { 453 r.error_code = NX_PNG_ERR_BAD_IHDR 454 return r 455 } 456 let w: nx_int = _png_read_u32_be(input, data_off) 457 let h: nx_int = _png_read_u32_be(input, data_off + 4) 458 let bd: nx_int = (input[data_off + 8] as nx_int) & 255 459 let ct: nx_int = (input[data_off + 9] as nx_int) & 255 460 let cm: nx_int = (input[data_off + 10] as nx_int) & 255 461 let fm: nx_int = (input[data_off + 11] as nx_int) & 255 462 let il: nx_int = (input[data_off + 12] as nx_int) & 255 463 header.width = w 464 header.height = h 465 header.bit_depth = bd 466 header.color_type = ct 467 header.compression = cm 468 header.filter = fm 469 header.interlace = il 470 ihdr_seen = 1 471 // v3 (2026-07-24): sub-byte depths 1/2/4 allowed for GRAYSCALE(0) + PALETTE(3) -- 472 // unpacked to 8-bit after defilter (the residual "thumb decode error" slice, seq624). 473 // RGB/RGBA/GRAY_ALPHA are never sub-byte -> stay 8/16 only. 474 if _png_n_channels(ct) < 0 { 475 r.header = header 476 r.error_code = NX_PNG_ERR_UNSUPPORTED_COLOR 477 return r 478 } 479 var subok: nx_int = 0 480 if ct == NX_PNG_COLOR_GRAY { subok = 1 } 481 if ct == NX_PNG_COLOR_PALETTE { subok = 1 } 482 var depthok: nx_int = 0 483 if bd == 8 { depthok = 1 } 484 if bd == 16 { depthok = 1 } 485 if subok == 1 { 486 if bd == 1 { depthok = 1 } 487 if bd == 2 { depthok = 1 } 488 if bd == 4 { depthok = 1 } 489 } 490 if depthok == 0 { 491 r.header = header 492 r.error_code = NX_PNG_ERR_UNSUPPORTED_DEPTH 493 return r 494 } 495 // v4 (2026-08-05): Adam7 (il==1) SUPPORTED for depths 1/2/4/8. 496 // il>1 is not a PNG; interlaced 16-bit stays refused (rare, 497 // and the linear 16-bit two-byte contract has no interlaced 498 // consumer yet) -- sealed, not silent. 499 if il > 1 { 500 r.header = header 501 r.error_code = NX_PNG_ERR_UNSUPPORTED_INTERLACE 502 return r 503 } 504 if il == 1 { if bd == 16 { 505 r.header = header 506 r.error_code = NX_PNG_ERR_UNSUPPORTED_INTERLACE 507 return r 508 } } 509 } 510 } 511 } 512 } 513 514 // IDAT = "IDAT" = 73 68 65 84 515 if t0 == 73 { 516 if t1 == 68 { 517 if t2 == 65 { 518 if t3 == 84 { 519 if ihdr_seen == 0 { 520 r.error_code = NX_PNG_ERR_NO_IHDR 521 return r 522 } 523 if (idat_len + data_len) > MAX_IDAT { 524 r.error_code = NX_PNG_ERR_DECOMPRESS 525 return r 526 } 527 var cp: nx_int = 0 528 while cp < data_len { 529 idat_buf[idat_len + cp] = input[data_off + cp] 530 cp = cp + 1 531 } 532 idat_len = idat_len + data_len 533 } 534 } 535 } 536 } 537 538 // PLTE = "PLTE" = 80 76 84 69 (v2: palette for color-type 3) 539 if t0 == 80 { 540 if t1 == 76 { 541 if t2 == 84 { 542 if t3 == 69 { 543 if NXPNG_PAL == 0 { NXPNG_PAL = sys_mmap(768) as i64 } 544 var pe: nx_int = data_len 545 if pe > 768 { pe = 768 } 546 let pal: *u8 = NXPNG_PAL as *u8 547 var pc: nx_int = 0 548 while pc < pe { pal[pc] = input[data_off + pc]; pc = pc + 1 } 549 NXPNG_PALN = pe / 3 550 } 551 } 552 } 553 } 554 555 // IEND = "IEND" = 73 69 78 68 556 if t0 == 73 { 557 if t1 == 69 { 558 if t2 == 78 { 559 if t3 == 68 { 560 iend_seen = 1 561 } 562 } 563 } 564 } 565 566 safety = safety + 1 567 } 568 569 if ihdr_seen == 0 { 570 r.error_code = NX_PNG_ERR_NO_IHDR 571 return r 572 } 573 if iend_seen == 0 { 574 r.error_code = NX_PNG_ERR_NO_IEND 575 return r 576 } 577 r.header = header 578 579 // Decompress IDAT (zlib-wrapped DEFLATE). 580 let bpp: nx_int = _png_bytes_per_pixel(header.color_type, header.bit_depth) 581 if bpp <= 0 { 582 r.error_code = NX_PNG_ERR_UNSUPPORTED_COLOR 583 return r 584 } 585 let nch0: nx_int = _png_n_channels(header.color_type) 586 // scanline bytes = ceil(width * channels * bit_depth / 8): correct for 1/2/4/8/16 bit. For 8/16 this 587 // equals bpp*width EXACTLY (working path unchanged). Filter distance stays bpp (=1 for sub-byte, per spec). 588 var width_bytes: nx_int = (header.width * nch0 * header.bit_depth + 7) / 8 589 var expected_raw: nx_int = header.height * (1 + width_bytes) 590 if header.interlace == 1 { expected_raw = _png_a7_expected(header.width, header.height, nch0, header.bit_depth) } 591 let max_out: nx_int = expected_raw + 64 592 593 let zlib_r: *NxZlibResult = nx_zlib_inflate(idat_buf, idat_len, max_out) 594 if zlib_r == (0 as *NxZlibResult) { 595 r.error_code = NX_PNG_ERR_DECOMPRESS 596 return r 597 } 598 if zlib_r.error_code != NX_ZLIB_OK { 599 r.error_code = NX_PNG_ERR_DECOMPRESS 600 return r 601 } 602 if zlib_r.output_size != expected_raw { 603 r.error_code = NX_PNG_ERR_SCANLINE_LEN 604 return r 605 } 606 607 // Allocate pixel output (without filter bytes). 608 var pixels_size: nx_int = header.height * width_bytes 609 var pixels: *u8 = 0 as *u8 610 if header.interlace == 1 { 611 // v4: Adam7 -- defilter each of the 7 passes independently, scatter the 612 // samples into a full-size 8-bit-per-sample plane, then fall through to 613 // the shared palette expansion / packaging exactly like the linear path. 614 pixels_size = header.width * header.height * nch0 615 pixels = sys_mmap(pixels_size as nx_size) 616 if _png_a7_deinterlace(zlib_r.output_data, header.width, header.height, nch0, 617 header.bit_depth, header.color_type, bpp, pixels) != 0 { 618 r.error_code = NX_PNG_ERR_BAD_FILTER 619 return r 620 } 621 width_bytes = header.width * nch0 622 header.bit_depth = 8 623 } else { 624 pixels = sys_mmap(pixels_size as nx_size) 625 626 // Reverse-filter each scanline. 627 var y: nx_int = 0 628 while y < header.height { 629 let src_off: nx_int = y * (1 + width_bytes) 630 let filt_type: nx_int = (zlib_r.output_data[src_off] as nx_int) & 255 631 if filt_type > 4 { 632 r.error_code = NX_PNG_ERR_BAD_FILTER 633 return r 634 } 635 let filt_line: *u8 = (zlib_r.output_data as nx_int + src_off + 1) as *u8 636 let out_line: *u8 = (pixels as nx_int + y * width_bytes) as *u8 637 var prev_line: *u8 = 0 as *u8 638 if y > 0 { 639 prev_line = (pixels as nx_int + (y - 1) * width_bytes) as *u8 640 } 641 let rc: nx_int = _png_unfilter_scanline( 642 filt_type, filt_line, prev_line, out_line, width_bytes, bpp) 643 if rc != 0 { 644 r.error_code = NX_PNG_ERR_BAD_FILTER 645 return r 646 } 647 y = y + 1 648 } 649 650 // v3: UNPACK sub-byte samples (1/2/4-bit) to ONE byte each -> palette/grayscale downstream sees 8-bit. 651 // Gray samples SCALE to 0-255 (val*255/max); palette samples stay as the raw index for the PLTE lookup. 652 if header.bit_depth < 8 { 653 let smask: nx_int = (1 << header.bit_depth) - 1 654 let spr: nx_int = header.width * nch0 655 let up: *u8 = sys_mmap((header.width * header.height * nch0) as nx_size) 656 var uy: nx_int = 0 657 while uy < header.height { 658 let inrow: *u8 = (pixels as nx_int + uy * width_bytes) as *u8 659 let outrow: *u8 = (up as nx_int + uy * spr) as *u8 660 var sx: nx_int = 0 661 while sx < spr { 662 let bitpos: nx_int = sx * header.bit_depth 663 let byi: nx_int = bitpos / 8 664 let boff: nx_int = bitpos % 8 665 let shift: nx_int = 8 - boff - header.bit_depth 666 var v: nx_int = ((inrow[byi] as nx_int) >> shift) & smask 667 if header.color_type == NX_PNG_COLOR_GRAY { v = v * 255 / smask } 668 outrow[sx] = v as u8 669 sx = sx + 1 670 } 671 uy = uy + 1 672 } 673 pixels = up 674 width_bytes = header.width * nch0 675 header.bit_depth = 8 676 } 677 } 678 679 // v2: PALETTE expansion (color-type 3): defiltered bytes are indices -> expand via PLTE to packed RGB. 680 if header.color_type == NX_PNG_COLOR_PALETTE { 681 if NXPNG_PALN <= 0 { 682 r.error_code = NX_PNG_ERR_UNSUPPORTED_COLOR 683 return r 684 } 685 let rgbsz: nx_int = header.width * header.height * 3 686 let rgbpx: *u8 = sys_mmap(rgbsz as nx_size) 687 let pal: *u8 = NXPNG_PAL as *u8 688 var pi: nx_int = 0 689 let npix: nx_int = header.width * header.height 690 while pi < npix { 691 var idx: nx_int = (pixels[pi] as nx_int) & 255 692 if idx >= NXPNG_PALN { idx = NXPNG_PALN - 1 } 693 rgbpx[pi*3] = pal[idx*3] 694 rgbpx[pi*3+1] = pal[idx*3+1] 695 rgbpx[pi*3+2] = pal[idx*3+2] 696 pi = pi + 1 697 } 698 r.pixels = rgbpx 699 r.pixels_size = rgbsz 700 r.n_channels = 3 701 r.bytes_per_pix = 3 702 return r 703 } 704 r.pixels = pixels 705 r.pixels_size = pixels_size 706 r.n_channels = _png_n_channels(header.color_type) 707 r.bytes_per_pix = bpp 708 return r 709} 710 711// ===== self-test ================================================= 712 713func main() -> nx_int { 714 // ---- signature check ---- 715 let bad_sig: *u8 = (sys_mmap(8)) as *u8 716 var i: nx_int = 0 717 while i < 8 { 718 bad_sig[i] = 0 as u8 719 i = i + 1 720 } 721 if _png_check_signature(bad_sig, 8) != 0 { return 1 } 722 723 let good_sig: *u8 = (sys_mmap(8)) as *u8 724 good_sig[0] = 137 as u8 725 good_sig[1] = 80 as u8 726 good_sig[2] = 78 as u8 727 good_sig[3] = 71 as u8 728 good_sig[4] = 13 as u8 729 good_sig[5] = 10 as u8 730 good_sig[6] = 26 as u8 731 good_sig[7] = 10 as u8 732 if _png_check_signature(good_sig, 8) != 1 { return 2 } 733 734 // ---- channel + bpp lookup ---- 735 if _png_n_channels(NX_PNG_COLOR_GRAY) != 1 { return 10 } 736 if _png_n_channels(NX_PNG_COLOR_RGB) != 3 { return 11 } 737 if _png_n_channels(NX_PNG_COLOR_GRAY_ALPHA) != 2 { return 12 } 738 if _png_n_channels(NX_PNG_COLOR_RGBA) != 4 { return 13 } 739 if _png_n_channels(NX_PNG_COLOR_PALETTE) != 1 { return 14 } // v2: palette = 1 index byte per pixel 740 741 if _png_bytes_per_pixel(NX_PNG_COLOR_RGB, 8) != 3 { return 20 } 742 if _png_bytes_per_pixel(NX_PNG_COLOR_RGBA, 8) != 4 { return 21 } 743 if _png_bytes_per_pixel(NX_PNG_COLOR_RGB, 16) != 6 { return 22 } 744 if _png_bytes_per_pixel(NX_PNG_COLOR_RGBA, 16) != 8 { return 23 } 745 if _png_bytes_per_pixel(NX_PNG_COLOR_GRAY, 8) != 1 { return 24 } 746 if _png_bytes_per_pixel(NX_PNG_COLOR_GRAY_ALPHA, 8) != 2 { return 25 } 747 748 // ---- Paeth predictor ---- 749 // 750 // From RFC 2083 worked examples: 751 // paeth(10, 20, 15): p = 15; pa = 5, pb = 5, pc = 0 752 // pa <= pb (true) but pa <= pc (5 > 0 false) -> first 753 // branch fails; pb <= pc (5 > 0 false); return c = 15. 754 if _png_paeth(10, 20, 15) != 15 { return 30 } 755 // paeth(255, 0, 128): p = 127; pa = 128, pb = 127, pc = 1 756 // pa <= pb? 128 > 127 false; pb <= pc? 127 > 1 false; 757 // return c = 128. 758 if _png_paeth(255, 0, 128) != 128 { return 31 } 759 // paeth(5, 10, 0): p = 15; pa = 10, pb = 5, pc = 15 760 // pa <= pb? 10 > 5 false; pb <= pc? 5 <= 15 true; return b = 10. 761 if _png_paeth(5, 10, 0) != 10 { return 32 } 762 // paeth(50, 60, 20): p = 90; pa = 40, pb = 30, pc = 70 763 // pa <= pb? 40 > 30 false; pb <= pc? 30 <= 70 true; return b = 60. 764 if _png_paeth(50, 60, 20) != 60 { return 33 } 765 766 // ---- filter reverse: FILTER_NONE ---- 767 let f_in: *u8 = (sys_mmap(4)) as *u8 768 let f_out: *u8 = (sys_mmap(4)) as *u8 769 f_in[0] = 10 as u8 770 f_in[1] = 20 as u8 771 f_in[2] = 30 as u8 772 f_in[3] = 40 as u8 773 _png_unfilter_scanline(NX_PNG_FILTER_NONE, f_in, 0 as *u8, f_out, 4, 1) 774 if f_out[0] != (10 as u8) { return 40 } 775 if f_out[3] != (40 as u8) { return 41 } 776 777 // ---- filter reverse: FILTER_SUB ---- 778 // filt: [5, 3, 7, 2] bpp=1 779 // out[0] = 5 + 0 = 5 780 // out[1] = 3 + 5 = 8 781 // out[2] = 7 + 8 = 15 782 // out[3] = 2 + 15 = 17 783 let s_in: *u8 = (sys_mmap(4)) as *u8 784 let s_out: *u8 = (sys_mmap(4)) as *u8 785 s_in[0] = 5 as u8 786 s_in[1] = 3 as u8 787 s_in[2] = 7 as u8 788 s_in[3] = 2 as u8 789 _png_unfilter_scanline(NX_PNG_FILTER_SUB, s_in, 0 as *u8, s_out, 4, 1) 790 if s_out[0] != (5 as u8) { return 50 } 791 if s_out[1] != (8 as u8) { return 51 } 792 if s_out[2] != (15 as u8) { return 52 } 793 if s_out[3] != (17 as u8) { return 53 } 794 795 // ---- filter reverse: FILTER_UP ---- 796 // prev: [10, 20, 30, 40]; filt: [1, 2, 3, 4] 797 // out = filt + prev = [11, 22, 33, 44] 798 let u_in: *u8 = (sys_mmap(4)) as *u8 799 let u_prev: *u8 = (sys_mmap(4)) as *u8 800 let u_out: *u8 = (sys_mmap(4)) as *u8 801 u_in[0] = 1 as u8 802 u_in[1] = 2 as u8 803 u_in[2] = 3 as u8 804 u_in[3] = 4 as u8 805 u_prev[0] = 10 as u8 806 u_prev[1] = 20 as u8 807 u_prev[2] = 30 as u8 808 u_prev[3] = 40 as u8 809 _png_unfilter_scanline(NX_PNG_FILTER_UP, u_in, u_prev, u_out, 4, 1) 810 if u_out[0] != (11 as u8) { return 60 } 811 if u_out[3] != (44 as u8) { return 61 } 812 813 // ---- BAD_SIGNATURE on real file path ---- 814 let r_bad: *NxPngResult = nx_png_decode(bad_sig, 8) 815 if r_bad.error_code != NX_PNG_ERR_BAD_SIGNATURE { return 70 } 816 817 // ---- TOO_SHORT ---- 818 let r_short: *NxPngResult = nx_png_decode(good_sig, 5) 819 if r_short.error_code != NX_PNG_ERR_TOO_SHORT { return 71 } 820 821 return 0 822}