nx_jpeg_header.nx source
↩ module page · 162 lines · 5668 B
1// jpeg_header.nx -- parse JPEG JFIF markers enough to extract
2// width + height + component count without decoding the scan.
3//
4// JPEG streams are a sequence of markers, each:
5// - 0xFF prefix byte (occasionally padded with more 0xFFs)
6// - 1 marker byte
7// - For most markers: 2-byte big-endian length (includes its own
8// 2 bytes) + (length-2) bytes of data.
9// - SOI (0xD8) and EOI (0xD9) and RSTn have no length.
10//
11// The markers we care about:
12// 0xD8 SOI start of image (must be first)
13// 0xE0 APP0 JFIF identifier, density info
14// 0xE1 APP1 often EXIF
15// 0xC0..0xCF SOF start of frame -- contains width + height +
16// component count. 0xC4 is DHT, 0xC8 is
17// reserved, 0xCC is DAC -- these are NOT SOF
18// even though they're in the range; we skip.
19// 0xDA SOS start of scan -- data follows; we stop here.
20// 0xD9 EOI end of image.
21//
22// SOF data layout:
23// 1 byte precision (bits per sample, typically 8)
24// 2 bytes height (BE)
25// 2 bytes width (BE)
26// 1 byte num_components (1 grayscale, 3 YCbCr, 4 CMYK)
27//
28// Invariants:
29// J1 First two bytes must be 0xFF 0xD8 (SOI).
30// J2 Parser advances until it finds a SOF marker; returns its
31// decoded dims. If SOS is hit first we return ERR_NOSOF
32// (malformed file / truncated header).
33// J3 We ignore restart markers (0xD0..0xD7) which have no
34// length field.
35
36// nx_safety_envelope:
37// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
38// sil_target: SIL1
39// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
40// verdict: NOT_YET_EVALUATED
41
42import "nx_syscalls.nx"
43
44const JPEG_ERR_FORMAT: i64 = -1
45const JPEG_ERR_SHORT: i64 = -2
46const JPEG_ERR_NOSOF: i64 = -3
47
48struct JpegInfo {
49 precision: i64, // bits per sample
50 width: i64, // pixels
51 height: i64, // pixels
52 num_components: i64, // 1/3/4
53}
54
55func jpeg_read_u16_be(buf: *u8, off: i64) -> i64 {
56 return (buf[off] << 8) | buf[off + 1]
57}
58
59// Test whether a marker byte in range 0xC0..0xCF is actually a SOF.
60// DHT (0xC4), reserved (0xC8), DAC (0xCC) are exceptions.
61func jpeg_is_sof(m: i64) -> i64 {
62 if m < 0xC0 { return 0 }
63 if m > 0xCF { return 0 }
64 if m == 0xC4 { return 0 }
65 if m == 0xC8 { return 0 }
66 if m == 0xCC { return 0 }
67 return 1
68}
69
70// Test whether a marker is a length-less one (SOI, EOI, RSTn).
71func jpeg_no_length(m: i64) -> i64 {
72 if m == 0xD8 { return 1 }
73 if m == 0xD9 { return 1 }
74 if m >= 0xD0 {
75 if m <= 0xD7 { return 1 }
76 }
77 return 0
78}
79
80// Parse JPEG headers until we find the SOF marker. Returns 0 on
81// success with info populated; negative on error.
82func jpeg_parse_header(buf: *u8, n: i64, info: *JpegInfo) -> i64 {
83 if n < 4 { return JPEG_ERR_SHORT }
84 // SOI must be 0xFF 0xD8.
85 if buf[0] != 0xFF { return JPEG_ERR_FORMAT }
86 if buf[1] != 0xD8 { return JPEG_ERR_FORMAT }
87
88 var off: i64 = 2
89 while off + 4 <= n {
90 // Consume any 0xFF fill bytes.
91 while off < n {
92 if buf[off] != 0xFF { return JPEG_ERR_FORMAT }
93 if off + 1 < n {
94 if buf[off + 1] != 0xFF { break }
95 }
96 off = off + 1
97 }
98 if off + 2 > n { return JPEG_ERR_SHORT }
99 let m: i64 = buf[off + 1]
100 off = off + 2
101
102 // SOS means we've passed the headers without finding SOF.
103 if m == 0xDA { return JPEG_ERR_NOSOF }
104 // EOI end of file.
105 if m == 0xD9 { return JPEG_ERR_NOSOF }
106 // Length-less markers (SOI, RSTn); ignore.
107 if jpeg_no_length(m) == 1 { continue }
108
109 if off + 2 > n { return JPEG_ERR_SHORT }
110 let seg_len: i64 = jpeg_read_u16_be(buf, off)
111 if seg_len < 2 { return JPEG_ERR_FORMAT }
112 if off + seg_len > n { return JPEG_ERR_SHORT }
113
114 if jpeg_is_sof(m) == 1 {
115 // SOF payload: precision(1) + height(2) + width(2) + ncomp(1).
116 if seg_len < 8 { return JPEG_ERR_FORMAT }
117 info.precision = buf[off + 2]
118 info.height = jpeg_read_u16_be(buf, off + 3)
119 info.width = jpeg_read_u16_be(buf, off + 5)
120 info.num_components = buf[off + 7]
121 return 0
122 }
123
124 off = off + seg_len
125 }
126 return JPEG_ERR_NOSOF
127}
128
129// Compile-only smoke -- forge a minimal JPEG with SOI, APP0,
130// SOF0, SOS. Only SOI + SOF0 matter for this test.
131func main() -> i64 {
132 let raw: *u8 = sys_mmap(64)
133 var i: i64 = 0
134 while i < 64 { raw[i] = 0; i = i + 1 }
135
136 // SOI
137 raw[0] = 0xFF; raw[1] = 0xD8
138 // SOF0 (0xFF 0xC0) with length 17 (big-endian), precision=8,
139 // height=200 (0x00C8), width=320 (0x0140), num_components=3
140 raw[2] = 0xFF; raw[3] = 0xC0
141 raw[4] = 0; raw[5] = 17
142 raw[6] = 8 // precision
143 raw[7] = 0x00; raw[8] = 0xC8 // height = 200
144 raw[9] = 0x01; raw[10] = 0x40 // width = 320
145 raw[11] = 3 // num_components
146 // pad out seg_len-6 = 11 bytes of dummy component specs
147 i = 12
148 while i < 19 { raw[i] = 0; i = i + 1 }
149
150 let info_raw: *u8 = sys_mmap(64)
151 let info: *JpegInfo = info_raw as *JpegInfo
152 if jpeg_parse_header(raw, 64, info) != 0 { return 1 }
153 if info.width != 320 { return 2 }
154 if info.height != 200 { return 3 }
155 if info.num_components != 3 { return 4 }
156 if info.precision != 8 { return 5 }
157
158 // Bad SOI
159 raw[1] = 0
160 if jpeg_parse_header(raw, 64, info) != JPEG_ERR_FORMAT { return 6 }
161 return 0
162}