code wiki / (root) / nx_jpeg_header.nx

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}