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}