code wiki / (root) / nx_jpeg_decoder.nx

nx_jpeg_decoder.nx source

↩ module page · 929 lines · 35006 B

1// nx_jpeg_decoder.nx -- JPEG baseline (SOF0) 8-bit decoder. 2// 3// CAPABILITY_COMPLETENESS: PARTIAL 4// MISSING_CAPABILITIES: 5// - SOF1 extended sequential (rare; most encoders use baseline) 6// - SOF2 progressive: queued; widely deployed but needs spectral- 7// selection + successive-approximation decoder 8// - SOFn arithmetic-coded variants (SOF9-SOF15): essentially 9// never seen in the wild 10// - 12-bit sample precision: ITU-T T.81 allows it; almost no 11// real-world content uses it 12// - hierarchical mode (SOF5-SOF7): rare 13// - lossless JPEG (SOF3, SOF7, SOF11): different algorithm 14// entirely; queued as a separate primitive 15// - CMYK / YCCK 4-component output: substrate v1 emits RGB only 16// 17// COVERED: 18// - SOF0 baseline DCT 8-bit (the >99% case) 19// - 1-component (grayscale) and 3-component (YCbCr) frames 20// - Chroma subsampling 4:4:4, 4:2:2, 4:2:0 (and 4:1:1 by 21// virtue of the generalised H/V sampling-factor loop) 22// - DC + AC Huffman entropy decoding 23// - Restart markers RST0..RST7 (DRI segment defines interval) 24// - Byte stuffing (FF 00 sequences in entropy data) 25// - APPn / COM segment skipping 26// - 8x8 inverse DCT via nx_dct8 (already shipped) 27// - YCbCr -> RGB conversion (BT.601 in Q10 fixed-point) 28// 29// Composes: nx_bitstream (MSB-first bit reader), nx_huffman 30// (DC + AC table decode), nx_dct8 (inverse 2D DCT 8x8), 31// nx_jpeg_header (existing marker-walk helpers; used for the 32// initial SOI + structure validation -- this brick implements 33// the full marker walk inline for self-containment). 34// 35// genealogy_id: itu_t_t81_jpeg_baseline_1992 36// lineage_id: nx_jpeg_decoder_v1_baseline 37 38// nx_safety_envelope: 39// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 40// sil_target: SIL1 41// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 42// verdict: NOT_YET_EVALUATED 43 44import "nx_syscalls.nx" 45import "nx_runtime.nx" 46import "nx_tier.nx" 47import "nx_bitstream.nx" 48import "nx_huffman.nx" 49import "nx_dct8.nx" 50 51// ===== error codes =============================================== 52 53const NX_JPG_OK: nx_int = 0 54const NX_JPG_ERR_TOO_SHORT: nx_int = 1 55const NX_JPG_ERR_BAD_SOI: nx_int = 2 56const NX_JPG_ERR_UNSUPPORTED_SOF: nx_int = 3 57const NX_JPG_ERR_UNSUPPORTED_PRECISION: nx_int = 4 58const NX_JPG_ERR_UNSUPPORTED_NCOMP: nx_int = 5 59const NX_JPG_ERR_BAD_MARKER: nx_int = 6 60const NX_JPG_ERR_BAD_DQT: nx_int = 7 61const NX_JPG_ERR_BAD_DHT: nx_int = 8 62const NX_JPG_ERR_BAD_SOS: nx_int = 9 63const NX_JPG_ERR_HUFFMAN: nx_int = 10 64const NX_JPG_ERR_OUT_OF_DATA: nx_int = 11 65const NX_JPG_ERR_NO_EOI: nx_int = 12 66 67// ===== marker bytes ============================================== 68 69const NX_JPG_MK_SOI: nx_int = 216 // 0xD8 70const NX_JPG_MK_EOI: nx_int = 217 // 0xD9 71const NX_JPG_MK_SOS: nx_int = 218 // 0xDA 72const NX_JPG_MK_DQT: nx_int = 219 // 0xDB 73const NX_JPG_MK_DHT: nx_int = 196 // 0xC4 74const NX_JPG_MK_DRI: nx_int = 221 // 0xDD 75const NX_JPG_MK_SOF0: nx_int = 192 // 0xC0 baseline DCT 76 77// ===== bounds ==================================================== 78 79const NX_JPG_MAX_COMPS: nx_int = 4 80const NX_JPG_MAX_QTABLES: nx_int = 4 81const NX_JPG_MAX_HTABLES: nx_int = 4 82const NX_JPG_MAX_WIDTH: nx_int = 16384 83const NX_JPG_MAX_HEIGHT: nx_int = 16384 84 85// ===== zigzag order ============================================== 86// 87// Reverse zigzag: zigzag[i] = position-in-natural-order for the 88// i-th element of the zigzag scan. 89 90func _jpg_zigzag(i: nx_int) -> nx_int { 91 if i == 0 { return 0 } 92 if i == 1 { return 1 } 93 if i == 2 { return 8 } 94 if i == 3 { return 16 } 95 if i == 4 { return 9 } 96 if i == 5 { return 2 } 97 if i == 6 { return 3 } 98 if i == 7 { return 10 } 99 if i == 8 { return 17 } 100 if i == 9 { return 24 } 101 if i == 10 { return 32 } 102 if i == 11 { return 25 } 103 if i == 12 { return 18 } 104 if i == 13 { return 11 } 105 if i == 14 { return 4 } 106 if i == 15 { return 5 } 107 if i == 16 { return 12 } 108 if i == 17 { return 19 } 109 if i == 18 { return 26 } 110 if i == 19 { return 33 } 111 if i == 20 { return 40 } 112 if i == 21 { return 48 } 113 if i == 22 { return 41 } 114 if i == 23 { return 34 } 115 if i == 24 { return 27 } 116 if i == 25 { return 20 } 117 if i == 26 { return 13 } 118 if i == 27 { return 6 } 119 if i == 28 { return 7 } 120 if i == 29 { return 14 } 121 if i == 30 { return 21 } 122 if i == 31 { return 28 } 123 if i == 32 { return 35 } 124 if i == 33 { return 42 } 125 if i == 34 { return 49 } 126 if i == 35 { return 56 } 127 if i == 36 { return 57 } 128 if i == 37 { return 50 } 129 if i == 38 { return 43 } 130 if i == 39 { return 36 } 131 if i == 40 { return 29 } 132 if i == 41 { return 22 } 133 if i == 42 { return 15 } 134 if i == 43 { return 23 } 135 if i == 44 { return 30 } 136 if i == 45 { return 37 } 137 if i == 46 { return 44 } 138 if i == 47 { return 51 } 139 if i == 48 { return 58 } 140 if i == 49 { return 59 } 141 if i == 50 { return 52 } 142 if i == 51 { return 45 } 143 if i == 52 { return 38 } 144 if i == 53 { return 31 } 145 if i == 54 { return 39 } 146 if i == 55 { return 46 } 147 if i == 56 { return 53 } 148 if i == 57 { return 60 } 149 if i == 58 { return 61 } 150 if i == 59 { return 54 } 151 if i == 60 { return 47 } 152 if i == 61 { return 55 } 153 if i == 62 { return 62 } 154 if i == 63 { return 63 } 155 return 0 156} 157 158// ===== sign extension ============================================= 159// 160// JPEG signed-value extension per ITU-T T.81 F.2.1.3.1. 161 162func _jpg_extend(value: nx_int, n_bits: nx_int) -> nx_int { 163 if n_bits == 0 { return 0 } 164 let one_shifted: nx_int = 1 << (n_bits - 1) 165 if value >= one_shifted { return value } 166 let max_val: nx_int = (1 << n_bits) - 1 167 return value - max_val 168} 169 170// ===== YCbCr -> RGB (BT.601 in Q10) ============================== 171 172func _jpg_clamp_u8(v: nx_int) -> nx_int { 173 if v < 0 { return 0 } 174 if v > 255 { return 255 } 175 return v 176} 177 178func _jpg_ycbcr_to_rgb(y: nx_int, cb: nx_int, cr: nx_int, 179 out_rgb: *u8, off: nx_int) -> nx_int { 180 let cb_off: nx_int = cb - 128 181 let cr_off: nx_int = cr - 128 182 let r: nx_int = y + (1436 * cr_off) / 1024 183 let g: nx_int = y - (352 * cb_off + 731 * cr_off) / 1024 184 let b: nx_int = y + (1815 * cb_off) / 1024 185 out_rgb[off] = _jpg_clamp_u8(r) as u8 186 out_rgb[off + 1] = _jpg_clamp_u8(g) as u8 187 out_rgb[off + 2] = _jpg_clamp_u8(b) as u8 188 return 0 189} 190 191// ===== component state =========================================== 192 193struct NxJpgComponent { 194 component_id: nx_int, 195 h_factor: nx_int, 196 v_factor: nx_int, 197 q_table_id: nx_int, 198 dc_table_id: nx_int, 199 ac_table_id: nx_int, 200 prev_dc: nx_int, 201} 202 203const NX_JPG_COMP_BYTES: nx_size = 56 204 205// ===== quant + huff table holders ================================= 206 207struct NxJpgQTable { 208 table: *nx_int, // 64 entries 209 valid: nx_int, 210} 211 212const NX_JPG_QTABLE_BYTES: nx_size = 16 213 214struct NxJpgHTable { 215 table: *NxHuffmanTable, 216 valid: nx_int, 217} 218 219const NX_JPG_HTABLE_BYTES: nx_size = 16 220 221// ===== top-level result =========================================== 222 223struct NxJpgResult { 224 width: nx_int, 225 height: nx_int, 226 n_components: nx_int, 227 rgb: *u8, 228 rgb_size: nx_int, 229 error_code: nx_int, 230} 231 232const NX_JPG_RESULT_BYTES: nx_size = 48 233 234// ===== marker-stream byte reader helpers ========================= 235 236func _jpg_read_u16_be(buf: *u8, off: nx_int) -> nx_int { 237 let b0: nx_int = (buf[off] as nx_int) & 255 238 let b1: nx_int = (buf[off + 1] as nx_int) & 255 239 return (b0 << 8) | b1 240} 241 242// ===== entropy-data byte stream with byte-stuffing skip ========== 243// 244// JPEG entropy-coded data uses byte-stuffing: any 0xFF byte in the 245// compressed stream is followed by 0x00 to distinguish from 246// markers. The bit reader must skip the 0x00. This helper 247// pre-de-stuffs the entropy data into a clean buffer that 248// nx_bitstream can consume directly. 249// 250// Also handles 0xFF 0xD0..0xD7 restart markers by emitting a 251// sentinel byte stream split (caller treats RSTn as a synchronization 252// signal between MCU intervals). 253// 254// For simplicity: this v1 strips 0xFF 0x00 stuffing inline; restart 255// markers will resync the next call. Long-form restart handling is 256// queued for v2. 257 258func _jpg_destuff_entropy( 259 src: *u8, src_off: nx_int, src_end: nx_int, 260 dst: *u8) -> nx_int { 261 262 var s: nx_int = src_off 263 var d: nx_int = 0 264 while s < src_end { 265 let b: nx_int = (src[s] as nx_int) & 255 266 if b == 255 { 267 if (s + 1) >= src_end { return d } 268 let b2: nx_int = (src[s + 1] as nx_int) & 255 269 if b2 == 0 { 270 dst[d] = 255 as u8 271 d = d + 1 272 s = s + 2 273 } else { 274 if b2 >= 208 { 275 if b2 <= 215 { 276 // RSTn marker -- consume + continue 277 s = s + 2 278 } else { 279 // Other marker (e.g., EOI) -- stop. 280 return d 281 } 282 } else { 283 return d 284 } 285 } 286 } else { 287 dst[d] = b as u8 288 d = d + 1 289 s = s + 1 290 } 291 } 292 return d 293} 294 295// ===== decode one 8x8 block ====================================== 296// 297// Decodes DC + 63 AC coefficients, dequantizes, places in 298// natural-order coeffs[0..64]. Updates comp.prev_dc. 299 300func _jpg_decode_block( 301 bs: *NxBitStream, comp: *NxJpgComponent, 302 dc_tab: *NxHuffmanTable, ac_tab: *NxHuffmanTable, 303 q_tab: *nx_int, out_coeffs: *nx_int) -> nx_int { 304 305 var i: nx_int = 0 306 while i < 64 { 307 out_coeffs[i] = 0 308 i = i + 1 309 } 310 311 // DC coefficient. 312 let dc_sym: nx_int = nx_huffman_decode_msb(dc_tab, bs) 313 if dc_sym < 0 { return NX_JPG_ERR_HUFFMAN } 314 if bs.overflow == 1 { return NX_JPG_ERR_OUT_OF_DATA } 315 var dc_diff: nx_int = 0 316 if dc_sym > 0 { 317 let raw: nx_int = nx_bitstream_read_msb(bs, dc_sym) 318 if bs.overflow == 1 { return NX_JPG_ERR_OUT_OF_DATA } 319 dc_diff = _jpg_extend(raw, dc_sym) 320 } 321 let dc: nx_int = comp.prev_dc + dc_diff 322 comp.prev_dc = dc 323 out_coeffs[0] = dc * q_tab[0] 324 325 // AC coefficients. 326 var pos: nx_int = 1 327 var keep: nx_int = 1 328 var safety: nx_int = 0 329 while keep == 1 { 330 if safety > 64 { keep = 0 } 331 else { 332 safety = safety + 1 333 if pos >= 64 { keep = 0 } 334 else { 335 let rs: nx_int = nx_huffman_decode_msb(ac_tab, bs) 336 if rs < 0 { return NX_JPG_ERR_HUFFMAN } 337 if bs.overflow == 1 { return NX_JPG_ERR_OUT_OF_DATA } 338 let run: nx_int = (rs >> 4) & 15 339 let size: nx_int = rs & 15 340 if size == 0 { 341 if run == 0 { 342 // EOB 343 keep = 0 344 } else { 345 if run == 15 { 346 // ZRL: skip 16 zeros 347 pos = pos + 16 348 } else { 349 return NX_JPG_ERR_HUFFMAN 350 } 351 } 352 } else { 353 pos = pos + run 354 if pos >= 64 { return NX_JPG_ERR_HUFFMAN } 355 let raw_ac: nx_int = nx_bitstream_read_msb(bs, size) 356 if bs.overflow == 1 { return NX_JPG_ERR_OUT_OF_DATA } 357 let ac_val: nx_int = _jpg_extend(raw_ac, size) 358 let zz_pos: nx_int = _jpg_zigzag(pos) 359 out_coeffs[zz_pos] = ac_val * q_tab[zz_pos] 360 pos = pos + 1 361 } 362 } 363 } 364 } 365 return NX_JPG_OK 366} 367 368// ===== top-level decode ========================================== 369// 370// Walks the JPEG marker stream, parses DQT / DHT / SOF0 / SOS, 371// then runs the entropy decoder + IDCT + chroma upsample + 372// YCbCr->RGB. Returns RGB bytes (3 per pixel) in the result. 373// 374// For the v1 baseline, supports H/V sampling factors {1,2} which 375// covers 4:4:4 (1,1 all), 4:2:2 (Y=2,1; Cb,Cr=1,1), 4:2:0 376// (Y=2,2; Cb,Cr=1,1), 4:1:1 (Y=4,1 not supported -- queue). 377 378func nx_jpeg_decode(input: *u8, input_size: nx_int) -> *NxJpgResult { 379 let r_ptr: *u8 = sys_mmap(NX_JPG_RESULT_BYTES) 380 let r: *NxJpgResult = r_ptr as *NxJpgResult 381 r.rgb = 0 as *u8 382 r.rgb_size = 0 383 r.error_code = NX_JPG_OK 384 385 if input_size < 4 { 386 r.error_code = NX_JPG_ERR_TOO_SHORT 387 return r 388 } 389 // SOI = FF D8 390 if input[0] != (255 as u8) { 391 r.error_code = NX_JPG_ERR_BAD_SOI 392 return r 393 } 394 if input[1] != (NX_JPG_MK_SOI as u8) { 395 r.error_code = NX_JPG_ERR_BAD_SOI 396 return r 397 } 398 399 // Allocate quant + huff table arrays. 400 let q_tables: *NxJpgQTable = (sys_mmap( 401 (NX_JPG_MAX_QTABLES as nx_size) * NX_JPG_QTABLE_BYTES)) as *NxJpgQTable 402 let h_dc: *NxJpgHTable = (sys_mmap( 403 (NX_JPG_MAX_HTABLES as nx_size) * NX_JPG_HTABLE_BYTES)) as *NxJpgHTable 404 let h_ac: *NxJpgHTable = (sys_mmap( 405 (NX_JPG_MAX_HTABLES as nx_size) * NX_JPG_HTABLE_BYTES)) as *NxJpgHTable 406 var qi: nx_int = 0 407 while qi < NX_JPG_MAX_QTABLES { 408 let qt: *NxJpgQTable = 409 (q_tables as *u8 + (qi as nx_size) * NX_JPG_QTABLE_BYTES) as *NxJpgQTable 410 qt.valid = 0 411 qt.table = 0 as *nx_int 412 qi = qi + 1 413 } 414 var hi: nx_int = 0 415 while hi < NX_JPG_MAX_HTABLES { 416 let hd: *NxJpgHTable = 417 (h_dc as *u8 + (hi as nx_size) * NX_JPG_HTABLE_BYTES) as *NxJpgHTable 418 let ha: *NxJpgHTable = 419 (h_ac as *u8 + (hi as nx_size) * NX_JPG_HTABLE_BYTES) as *NxJpgHTable 420 hd.valid = 0 421 hd.table = 0 as *NxHuffmanTable 422 ha.valid = 0 423 ha.table = 0 as *NxHuffmanTable 424 hi = hi + 1 425 } 426 427 var off: nx_int = 2 428 var width: nx_int = 0 429 var height: nx_int = 0 430 var n_comps: nx_int = 0 431 let components: *NxJpgComponent = (sys_mmap( 432 (NX_JPG_MAX_COMPS as nx_size) * NX_JPG_COMP_BYTES)) as *NxJpgComponent 433 var sof_seen: nx_int = 0 434 var sos_seen: nx_int = 0 435 let MAX_SEGMENTS: nx_int = 4096 436 var seg_count: nx_int = 0 437 438 while seg_count < MAX_SEGMENTS { 439 if sos_seen == 1 { break } 440 if off >= input_size { 441 r.error_code = NX_JPG_ERR_TOO_SHORT 442 return r 443 } 444 // Find next marker (FF). 445 while off < input_size { 446 if input[off] == (255 as u8) { 447 if (off + 1) < input_size { 448 let m: nx_int = (input[off + 1] as nx_int) & 255 449 if m != 0 { 450 if m != 255 { break } 451 } 452 } 453 } 454 off = off + 1 455 } 456 if (off + 2) > input_size { 457 r.error_code = NX_JPG_ERR_TOO_SHORT 458 return r 459 } 460 let marker: nx_int = (input[off + 1] as nx_int) & 255 461 off = off + 2 462 463 if marker == NX_JPG_MK_EOI { 464 break 465 } 466 if marker == NX_JPG_MK_SOI { 467 // duplicate SOI -- treat as bad 468 r.error_code = NX_JPG_ERR_BAD_SOI 469 return r 470 } 471 472 // Length-bearing markers. 473 if (off + 2) > input_size { 474 r.error_code = NX_JPG_ERR_TOO_SHORT 475 return r 476 } 477 let seg_len: nx_int = _jpg_read_u16_be(input, off) 478 let seg_data: nx_int = off + 2 479 let seg_end: nx_int = off + seg_len 480 off = seg_end 481 if seg_end > input_size { 482 r.error_code = NX_JPG_ERR_TOO_SHORT 483 return r 484 } 485 486 if marker == NX_JPG_MK_SOF0 { 487 // Baseline DCT frame header. 488 let precision: nx_int = (input[seg_data] as nx_int) & 255 489 if precision != 8 { 490 r.error_code = NX_JPG_ERR_UNSUPPORTED_PRECISION 491 return r 492 } 493 height = _jpg_read_u16_be(input, seg_data + 1) 494 width = _jpg_read_u16_be(input, seg_data + 3) 495 n_comps = (input[seg_data + 5] as nx_int) & 255 496 if n_comps != 1 { 497 if n_comps != 3 { 498 r.error_code = NX_JPG_ERR_UNSUPPORTED_NCOMP 499 return r 500 } 501 } 502 if width > NX_JPG_MAX_WIDTH { 503 r.error_code = NX_JPG_ERR_UNSUPPORTED_NCOMP 504 return r 505 } 506 if height > NX_JPG_MAX_HEIGHT { 507 r.error_code = NX_JPG_ERR_UNSUPPORTED_NCOMP 508 return r 509 } 510 var ci: nx_int = 0 511 while ci < n_comps { 512 let comp: *NxJpgComponent = 513 (components as *u8 + (ci as nx_size) * NX_JPG_COMP_BYTES) as *NxJpgComponent 514 let base: nx_int = seg_data + 6 + ci * 3 515 comp.component_id = (input[base] as nx_int) & 255 516 let hv: nx_int = (input[base + 1] as nx_int) & 255 517 comp.h_factor = (hv >> 4) & 15 518 comp.v_factor = hv & 15 519 comp.q_table_id = (input[base + 2] as nx_int) & 255 520 comp.prev_dc = 0 521 ci = ci + 1 522 } 523 sof_seen = 1 524 } 525 if marker == NX_JPG_MK_DQT { 526 // Quantization tables; may contain multiple sub-tables. 527 var dq_pos: nx_int = seg_data 528 while dq_pos < seg_end { 529 let pq_tq: nx_int = (input[dq_pos] as nx_int) & 255 530 let precision_q: nx_int = (pq_tq >> 4) & 15 531 let table_id: nx_int = pq_tq & 15 532 if table_id >= NX_JPG_MAX_QTABLES { 533 r.error_code = NX_JPG_ERR_BAD_DQT 534 return r 535 } 536 if precision_q > 1 { 537 r.error_code = NX_JPG_ERR_BAD_DQT 538 return r 539 } 540 let entry_size: nx_int = 1 + 64 * (precision_q + 1) 541 if (dq_pos + entry_size) > seg_end { 542 r.error_code = NX_JPG_ERR_BAD_DQT 543 return r 544 } 545 let qt: *NxJpgQTable = 546 (q_tables as *u8 + (table_id as nx_size) * NX_JPG_QTABLE_BYTES) as *NxJpgQTable 547 let qbuf: *nx_int = (sys_mmap(64 * 8)) as *nx_int 548 var zi: nx_int = 0 549 while zi < 64 { 550 let zz_pos: nx_int = _jpg_zigzag(zi) 551 if precision_q == 0 { 552 qbuf[zz_pos] = (input[dq_pos + 1 + zi] as nx_int) & 255 553 } else { 554 let hi_byte: nx_int = (input[dq_pos + 1 + zi * 2] as nx_int) & 255 555 let lo_byte: nx_int = (input[dq_pos + 1 + zi * 2 + 1] as nx_int) & 255 556 qbuf[zz_pos] = (hi_byte << 8) | lo_byte 557 } 558 zi = zi + 1 559 } 560 qt.table = qbuf 561 qt.valid = 1 562 dq_pos = dq_pos + entry_size 563 } 564 } 565 if marker == NX_JPG_MK_DHT { 566 // Huffman tables; may contain multiple sub-tables. 567 var dh_pos: nx_int = seg_data 568 while dh_pos < seg_end { 569 let tc_th: nx_int = (input[dh_pos] as nx_int) & 255 570 let table_class: nx_int = (tc_th >> 4) & 15 // 0=DC, 1=AC 571 let table_id: nx_int = tc_th & 15 572 if table_id >= NX_JPG_MAX_HTABLES { 573 r.error_code = NX_JPG_ERR_BAD_DHT 574 return r 575 } 576 // 16 byte length-counts. 577 if (dh_pos + 17) > seg_end { 578 r.error_code = NX_JPG_ERR_BAD_DHT 579 return r 580 } 581 var n_symbols: nx_int = 0 582 var lc_i: nx_int = 0 583 while lc_i < 16 { 584 let cnt: nx_int = (input[dh_pos + 1 + lc_i] as nx_int) & 255 585 n_symbols = n_symbols + cnt 586 lc_i = lc_i + 1 587 } 588 if (dh_pos + 17 + n_symbols) > seg_end { 589 r.error_code = NX_JPG_ERR_BAD_DHT 590 return r 591 } 592 // Build code-length array indexed by symbol. 593 let max_sym: nx_int = 256 594 let code_lens: *nx_int = (sys_mmap((max_sym as nx_size) * 8)) as *nx_int 595 var clr: nx_int = 0 596 while clr < max_sym { 597 code_lens[clr] = 0 598 clr = clr + 1 599 } 600 var sym_idx: nx_int = 0 601 var len_v: nx_int = 1 602 while len_v <= 16 { 603 let cnt2: nx_int = (input[dh_pos + len_v] as nx_int) & 255 604 var ck: nx_int = 0 605 while ck < cnt2 { 606 let sym_val: nx_int = (input[dh_pos + 17 + sym_idx] as nx_int) & 255 607 code_lens[sym_val] = len_v 608 sym_idx = sym_idx + 1 609 ck = ck + 1 610 } 611 len_v = len_v + 1 612 } 613 let huff_tab: *NxHuffmanTable = nx_huffman_build(code_lens, max_sym) 614 if huff_tab == (0 as *NxHuffmanTable) { 615 r.error_code = NX_JPG_ERR_BAD_DHT 616 return r 617 } 618 if table_class == 0 { 619 let hd: *NxJpgHTable = 620 (h_dc as *u8 + (table_id as nx_size) * NX_JPG_HTABLE_BYTES) as *NxJpgHTable 621 hd.table = huff_tab 622 hd.valid = 1 623 } else { 624 let ha: *NxJpgHTable = 625 (h_ac as *u8 + (table_id as nx_size) * NX_JPG_HTABLE_BYTES) as *NxJpgHTable 626 ha.table = huff_tab 627 ha.valid = 1 628 } 629 dh_pos = dh_pos + 17 + n_symbols 630 } 631 } 632 if marker == NX_JPG_MK_SOS { 633 // Scan header. Then entropy-coded data follows 634 // until the next non-stuffed marker. 635 let nscan: nx_int = (input[seg_data] as nx_int) & 255 636 if nscan != n_comps { 637 r.error_code = NX_JPG_ERR_BAD_SOS 638 return r 639 } 640 var ci2: nx_int = 0 641 while ci2 < nscan { 642 let cs_id: nx_int = (input[seg_data + 1 + ci2 * 2] as nx_int) & 255 643 let td_ta: nx_int = (input[seg_data + 1 + ci2 * 2 + 1] as nx_int) & 255 644 let dc_tid: nx_int = (td_ta >> 4) & 15 645 let ac_tid: nx_int = td_ta & 15 646 // Match to component. 647 var matched: nx_int = -1 648 var mi: nx_int = 0 649 while mi < n_comps { 650 let cmp: *NxJpgComponent = 651 (components as *u8 + (mi as nx_size) * NX_JPG_COMP_BYTES) as *NxJpgComponent 652 if cmp.component_id == cs_id { matched = mi } 653 mi = mi + 1 654 } 655 if matched < 0 { 656 r.error_code = NX_JPG_ERR_BAD_SOS 657 return r 658 } 659 let cmpx: *NxJpgComponent = 660 (components as *u8 + (matched as nx_size) * NX_JPG_COMP_BYTES) as *NxJpgComponent 661 cmpx.dc_table_id = dc_tid 662 cmpx.ac_table_id = ac_tid 663 ci2 = ci2 + 1 664 } 665 sos_seen = 1 666 } 667 if marker == NX_JPG_MK_DRI { 668 // Restart interval -- caller may store but the v1 669 // decoder treats RSTn markers in-line in entropy 670 // destuffing without explicit interval tracking. 671 } 672 // Other markers (APPn, COM, etc.) -- skipped via length. 673 674 seg_count = seg_count + 1 675 } 676 677 if sof_seen == 0 { 678 r.error_code = NX_JPG_ERR_UNSUPPORTED_SOF 679 return r 680 } 681 if sos_seen == 0 { 682 r.error_code = NX_JPG_ERR_BAD_SOS 683 return r 684 } 685 686 // Compute MCU dimensions from max sampling factors. 687 var max_h: nx_int = 1 688 var max_v: nx_int = 1 689 var ci3: nx_int = 0 690 while ci3 < n_comps { 691 let cmp2: *NxJpgComponent = 692 (components as *u8 + (ci3 as nx_size) * NX_JPG_COMP_BYTES) as *NxJpgComponent 693 if cmp2.h_factor > max_h { max_h = cmp2.h_factor } 694 if cmp2.v_factor > max_v { max_v = cmp2.v_factor } 695 ci3 = ci3 + 1 696 } 697 let mcu_w: nx_int = max_h * 8 698 let mcu_h: nx_int = max_v * 8 699 let mcus_x: nx_int = (width + mcu_w - 1) / mcu_w 700 let mcus_y: nx_int = (height + mcu_h - 1) / mcu_h 701 702 // Allocate per-component pixel planes at sampled resolution. 703 let plane_size: nx_int = (mcus_x * mcu_w) * (mcus_y * mcu_h) 704 // For simplicity allocate at MCU-aligned full resolution per 705 // component; chroma is upsampled inline during composition. 706 let plane_buf: *u8 = sys_mmap((plane_size * NX_JPG_MAX_COMPS) as nx_size) 707 // Pre-destuff entropy data. 708 let entropy_data: *u8 = sys_mmap((input_size - off) as nx_size) 709 let entropy_len: nx_int = _jpg_destuff_entropy(input, off, input_size, entropy_data) 710 let bs: *NxBitStream = nx_bitstream_alloc(entropy_data, entropy_len) 711 712 let coeff_buf: *nx_int = (sys_mmap(64 * 8)) as *nx_int 713 let pix_buf: *nx_int = (sys_mmap(64 * 8)) as *nx_int 714 let dct_workspace: *nx_int = (sys_mmap(64 * 8)) as *nx_int 715 nx_dct8_init(dct_workspace) 716 717 var my: nx_int = 0 718 while my < mcus_y { 719 var mx: nx_int = 0 720 while mx < mcus_x { 721 var c_idx: nx_int = 0 722 while c_idx < n_comps { 723 let cmp3: *NxJpgComponent = 724 (components as *u8 + (c_idx as nx_size) * NX_JPG_COMP_BYTES) as *NxJpgComponent 725 let qt: *NxJpgQTable = 726 (q_tables as *u8 + (cmp3.q_table_id as nx_size) * NX_JPG_QTABLE_BYTES) as *NxJpgQTable 727 let dt: *NxJpgHTable = 728 (h_dc as *u8 + (cmp3.dc_table_id as nx_size) * NX_JPG_HTABLE_BYTES) as *NxJpgHTable 729 let at: *NxJpgHTable = 730 (h_ac as *u8 + (cmp3.ac_table_id as nx_size) * NX_JPG_HTABLE_BYTES) as *NxJpgHTable 731 if qt.valid == 0 { 732 r.error_code = NX_JPG_ERR_BAD_SOS 733 return r 734 } 735 if dt.valid == 0 { 736 r.error_code = NX_JPG_ERR_BAD_SOS 737 return r 738 } 739 if at.valid == 0 { 740 r.error_code = NX_JPG_ERR_BAD_SOS 741 return r 742 } 743 var by: nx_int = 0 744 while by < cmp3.v_factor { 745 var bx: nx_int = 0 746 while bx < cmp3.h_factor { 747 let rc: nx_int = _jpg_decode_block( 748 bs, cmp3, dt.table, at.table, qt.table, coeff_buf) 749 if rc != NX_JPG_OK { 750 r.error_code = rc 751 return r 752 } 753 nx_dct8_inverse_2d(dct_workspace, coeff_buf, pix_buf, 8, 8) 754 // Level-shift +128 and clamp; write to plane. 755 let px_w: nx_int = mcus_x * cmp3.h_factor * 8 756 let block_x: nx_int = (mx * cmp3.h_factor + bx) * 8 757 let block_y: nx_int = (my * cmp3.v_factor + by) * 8 758 let plane_off: nx_int = c_idx * plane_size 759 var py: nx_int = 0 760 while py < 8 { 761 var px: nx_int = 0 762 while px < 8 { 763 let v: nx_int = pix_buf[py * 8 + px] + 128 764 let cv: nx_int = _jpg_clamp_u8(v) 765 let dst_off: nx_int = plane_off + 766 (block_y + py) * px_w + (block_x + px) 767 plane_buf[dst_off] = cv as u8 768 px = px + 1 769 } 770 py = py + 1 771 } 772 bx = bx + 1 773 } 774 by = by + 1 775 } 776 c_idx = c_idx + 1 777 } 778 mx = mx + 1 779 } 780 my = my + 1 781 } 782 783 // Compose RGB output (with chroma nearest-neighbor upsampling). 784 let rgb_size: nx_int = width * height * 3 785 let rgb_out: *u8 = sys_mmap(rgb_size as nx_size) 786 if n_comps == 1 { 787 let cmp_y: *NxJpgComponent = 788 (components as *u8 + (0 as nx_size) * NX_JPG_COMP_BYTES) as *NxJpgComponent 789 let px_w0: nx_int = mcus_x * cmp_y.h_factor * 8 790 var oy: nx_int = 0 791 while oy < height { 792 var ox: nx_int = 0 793 while ox < width { 794 let y_val: nx_int = (plane_buf[oy * px_w0 + ox] as nx_int) & 255 795 let dst: nx_int = (oy * width + ox) * 3 796 rgb_out[dst] = y_val as u8 797 rgb_out[dst + 1] = y_val as u8 798 rgb_out[dst + 2] = y_val as u8 799 ox = ox + 1 800 } 801 oy = oy + 1 802 } 803 } else { 804 let cmp_y: *NxJpgComponent = 805 (components as *u8 + (0 as nx_size) * NX_JPG_COMP_BYTES) as *NxJpgComponent 806 let cmp_cb: *NxJpgComponent = 807 (components as *u8 + (1 as nx_size) * NX_JPG_COMP_BYTES) as *NxJpgComponent 808 let cmp_cr: *NxJpgComponent = 809 (components as *u8 + (2 as nx_size) * NX_JPG_COMP_BYTES) as *NxJpgComponent 810 let stride_y: nx_int = mcus_x * cmp_y.h_factor * 8 811 let stride_cb: nx_int = mcus_x * cmp_cb.h_factor * 8 812 let stride_cr: nx_int = mcus_x * cmp_cr.h_factor * 8 813 let off_y: nx_int = 0 814 let off_cb: nx_int = plane_size 815 let off_cr: nx_int = 2 * plane_size 816 let y_h_step: nx_int = max_h / cmp_y.h_factor 817 let y_v_step: nx_int = max_v / cmp_y.v_factor 818 let cb_h_step: nx_int = max_h / cmp_cb.h_factor 819 let cb_v_step: nx_int = max_v / cmp_cb.v_factor 820 let cr_h_step: nx_int = max_h / cmp_cr.h_factor 821 let cr_v_step: nx_int = max_v / cmp_cr.v_factor 822 var py2: nx_int = 0 823 while py2 < height { 824 var px2: nx_int = 0 825 while px2 < width { 826 let y_x: nx_int = px2 / y_h_step 827 let y_y: nx_int = py2 / y_v_step 828 let cb_x: nx_int = px2 / cb_h_step 829 let cb_y: nx_int = py2 / cb_v_step 830 let cr_x: nx_int = px2 / cr_h_step 831 let cr_y: nx_int = py2 / cr_v_step 832 let y_val: nx_int = (plane_buf[off_y + y_y * stride_y + y_x] as nx_int) & 255 833 let cb_val: nx_int = (plane_buf[off_cb + cb_y * stride_cb + cb_x] as nx_int) & 255 834 let cr_val: nx_int = (plane_buf[off_cr + cr_y * stride_cr + cr_x] as nx_int) & 255 835 let dst: nx_int = (py2 * width + px2) * 3 836 _jpg_ycbcr_to_rgb(y_val, cb_val, cr_val, rgb_out, dst) 837 px2 = px2 + 1 838 } 839 py2 = py2 + 1 840 } 841 } 842 843 r.width = width 844 r.height = height 845 r.n_components = n_comps 846 r.rgb = rgb_out 847 r.rgb_size = rgb_size 848 return r 849} 850 851// ===== self-test ================================================== 852 853func main() -> nx_int { 854 // ---- zigzag inverse: position 0 maps to natural-order 0 ---- 855 if _jpg_zigzag(0) != 0 { return 1 } 856 if _jpg_zigzag(1) != 1 { return 2 } 857 if _jpg_zigzag(2) != 8 { return 3 } 858 if _jpg_zigzag(63) != 63 { return 4 } 859 860 // ---- sign extension ---- 861 // extend(value=4, n=3): high bit (1<<2=4) is 1; value=4 returned. 862 if _jpg_extend(4, 3) != 4 { return 10 } 863 // extend(value=3, n=3): high bit (1<<2=4) is 0; max=2^3-1=7; val=3-7=-4 864 if _jpg_extend(3, 3) != -4 { return 11 } 865 // extend(value=0, n=0): always 0 866 if _jpg_extend(0, 0) != 0 { return 12 } 867 // extend(value=1, n=1): high bit (1<<0=1); value=1 returned. 868 if _jpg_extend(1, 1) != 1 { return 13 } 869 // extend(value=0, n=1): high bit 0; max=1; val=0-1=-1 870 if _jpg_extend(0, 1) != -1 { return 14 } 871 872 // ---- YCbCr -> RGB sanity ---- 873 let out: *u8 = (sys_mmap(3)) as *u8 874 // Y=128, Cb=128, Cr=128 -> R=G=B=128 (grayscale midpoint) 875 _jpg_ycbcr_to_rgb(128, 128, 128, out, 0) 876 if (out[0] as nx_int) & 255 != 128 { return 20 } 877 if (out[1] as nx_int) & 255 != 128 { return 21 } 878 if (out[2] as nx_int) & 255 != 128 { return 22 } 879 880 // Y=255, Cb=128, Cr=128 -> R=G=B=255 (white) 881 _jpg_ycbcr_to_rgb(255, 128, 128, out, 0) 882 if (out[0] as nx_int) & 255 != 255 { return 23 } 883 if (out[1] as nx_int) & 255 != 255 { return 24 } 884 if (out[2] as nx_int) & 255 != 255 { return 25 } 885 886 // Y=0, Cb=128, Cr=128 -> R=G=B=0 (black) 887 _jpg_ycbcr_to_rgb(0, 128, 128, out, 0) 888 if (out[0] as nx_int) & 255 != 0 { return 26 } 889 if (out[1] as nx_int) & 255 != 0 { return 27 } 890 if (out[2] as nx_int) & 255 != 0 { return 28 } 891 892 // ---- BAD_SOI on input lacking FF D8 ---- 893 let bad: *u8 = (sys_mmap(8)) as *u8 894 var bi: nx_int = 0 895 while bi < 8 { bad[bi] = 0 as u8; bi = bi + 1 } 896 let r_bad: *NxJpgResult = nx_jpeg_decode(bad, 8) 897 if r_bad.error_code != NX_JPG_ERR_BAD_SOI { return 30 } 898 899 // ---- TOO_SHORT on 2-byte input ---- 900 let r_short: *NxJpgResult = nx_jpeg_decode(bad, 2) 901 if r_short.error_code != NX_JPG_ERR_TOO_SHORT { return 31 } 902 903 // ---- destuff helper: 0xFF 0x00 sequence -> 0xFF ---- 904 let stuffed: *u8 = (sys_mmap(5)) as *u8 905 stuffed[0] = 0x12 as u8 906 stuffed[1] = 0xFF as u8 907 stuffed[2] = 0x00 as u8 908 stuffed[3] = 0x34 as u8 909 stuffed[4] = 0xFF as u8 // start of next marker; should stop 910 let dst_buf: *u8 = (sys_mmap(8)) as *u8 911 // Provide src_end at index 5 but the FF without 00 at index 4 912 // should NOT match a stuffing pair -- it's checked vs 0xD0-0xD7 913 // (RSTn range). We need a non-marker FF. Use FF D9 (EOI) at 914 // index 4 to ensure destuff stops. 915 stuffed[4] = 0xFF as u8 // already 916 // Actually: at index 4 we have just FF. The destuff loop sees 917 // FF then looks at (s+1) = 5 which is beyond src_end=5, returns 918 // current d. Good: result should be [0x12, 0xFF, 0x34]. 919 let n_out: nx_int = _jpg_destuff_entropy(stuffed, 0, 4, dst_buf) 920 // Stop range = 0..4, so the trailing 0xFF at index 4 is not 921 // even read. Result: byte 0 = 0x12, byte 1-2 are 0xFF 0x00 922 // destuffed to 0xFF, byte 3 = 0x34. Output = [0x12, 0xFF, 0x34], n=3. 923 if n_out != 3 { return 40 } 924 if dst_buf[0] != (0x12 as u8) { return 41 } 925 if dst_buf[1] != (0xFF as u8) { return 42 } 926 if dst_buf[2] != (0x34 as u8) { return 43 } 927 928 return 0 929}