jpeg_header.nx source
↩ module page · 156 lines · 5570 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
36import "syscalls.nx"
37
38const JPEG_ERR_FORMAT: i64 = -1
39const JPEG_ERR_SHORT: i64 = -2
40const JPEG_ERR_NOSOF: i64 = -3
41
42struct JpegInfo {
43 precision: i64, // bits per sample
44 width: i64, // pixels
45 height: i64, // pixels
46 num_components: i64, // 1/3/4
47}
48
49func jpeg_read_u16_be(buf: *u8, off: i64) -> i64 {
50 return (buf[off] << 8) | buf[off + 1]
51}
52
53// Test whether a marker byte in range 0xC0..0xCF is actually a SOF.
54// DHT (0xC4), reserved (0xC8), DAC (0xCC) are exceptions.
55func jpeg_is_sof(m: i64) -> i64 {
56 if m < 0xC0 { return 0 }
57 if m > 0xCF { return 0 }
58 if m == 0xC4 { return 0 }
59 if m == 0xC8 { return 0 }
60 if m == 0xCC { return 0 }
61 return 1
62}
63
64// Test whether a marker is a length-less one (SOI, EOI, RSTn).
65func jpeg_no_length(m: i64) -> i64 {
66 if m == 0xD8 { return 1 }
67 if m == 0xD9 { return 1 }
68 if m >= 0xD0 {
69 if m <= 0xD7 { return 1 }
70 }
71 return 0
72}
73
74// Parse JPEG headers until we find the SOF marker. Returns 0 on
75// success with info populated; negative on error.
76func jpeg_parse_header(buf: *u8, n: i64, info: *JpegInfo) -> i64 {
77 if n < 4 { return JPEG_ERR_SHORT }
78 // SOI must be 0xFF 0xD8.
79 if buf[0] != 0xFF { return JPEG_ERR_FORMAT }
80 if buf[1] != 0xD8 { return JPEG_ERR_FORMAT }
81
82 var off: i64 = 2
83 while off + 4 <= n {
84 // Consume any 0xFF fill bytes.
85 while off < n {
86 if buf[off] != 0xFF { return JPEG_ERR_FORMAT }
87 if off + 1 < n {
88 if buf[off + 1] != 0xFF { break }
89 }
90 off = off + 1
91 }
92 if off + 2 > n { return JPEG_ERR_SHORT }
93 let m: i64 = buf[off + 1]
94 off = off + 2
95
96 // SOS means we've passed the headers without finding SOF.
97 if m == 0xDA { return JPEG_ERR_NOSOF }
98 // EOI end of file.
99 if m == 0xD9 { return JPEG_ERR_NOSOF }
100 // Length-less markers (SOI, RSTn); ignore.
101 if jpeg_no_length(m) == 1 { continue }
102
103 if off + 2 > n { return JPEG_ERR_SHORT }
104 let seg_len: i64 = jpeg_read_u16_be(buf, off)
105 if seg_len < 2 { return JPEG_ERR_FORMAT }
106 if off + seg_len > n { return JPEG_ERR_SHORT }
107
108 if jpeg_is_sof(m) == 1 {
109 // SOF payload: precision(1) + height(2) + width(2) + ncomp(1).
110 if seg_len < 8 { return JPEG_ERR_FORMAT }
111 info.precision = buf[off + 2]
112 info.height = jpeg_read_u16_be(buf, off + 3)
113 info.width = jpeg_read_u16_be(buf, off + 5)
114 info.num_components = buf[off + 7]
115 return 0
116 }
117
118 off = off + seg_len
119 }
120 return JPEG_ERR_NOSOF
121}
122
123// Compile-only smoke -- forge a minimal JPEG with SOI, APP0,
124// SOF0, SOS. Only SOI + SOF0 matter for this test.
125func main() -> i64 {
126 let raw: *u8 = sys_mmap(64)
127 var i: i64 = 0
128 while i < 64 { raw[i] = 0; i = i + 1 }
129
130 // SOI
131 raw[0] = 0xFF; raw[1] = 0xD8
132 // SOF0 (0xFF 0xC0) with length 17 (big-endian), precision=8,
133 // height=200 (0x00C8), width=320 (0x0140), num_components=3
134 raw[2] = 0xFF; raw[3] = 0xC0
135 raw[4] = 0; raw[5] = 17
136 raw[6] = 8 // precision
137 raw[7] = 0x00; raw[8] = 0xC8 // height = 200
138 raw[9] = 0x01; raw[10] = 0x40 // width = 320
139 raw[11] = 3 // num_components
140 // pad out seg_len-6 = 11 bytes of dummy component specs
141 i = 12
142 while i < 19 { raw[i] = 0; i = i + 1 }
143
144 let info_raw: *u8 = sys_mmap(64)
145 let info: *JpegInfo = info_raw as *JpegInfo
146 if jpeg_parse_header(raw, 64, info) != 0 { return 1 }
147 if info.width != 320 { return 2 }
148 if info.height != 200 { return 3 }
149 if info.num_components != 3 { return 4 }
150 if info.precision != 8 { return 5 }
151
152 // Bad SOI
153 raw[1] = 0
154 if jpeg_parse_header(raw, 64, info) != JPEG_ERR_FORMAT { return 6 }
155 return 0
156}