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}