code wiki / (root) / nx_ppm_writer.nx

nx_ppm_writer.nx source

↩ module page · 157 lines · 5126 B

1// ppm_writer.nx -- serialise Netpbm images (PBM / PGM / PPM). 2// 3// The Netpbm family is the simplest lossless image format alive: 4// ASCII magic + dimensions + max-value + raw binary pixels. Used 5// as a debug output target by virtually every graphics engine 6// during bring-up (ImageMagick, pixman, cairo, mesa) precisely 7// because you can emit it with printf. 8// 9// Magic codes: 10// P1 ASCII PBM 1-bit bitmap (0/1) 11// P2 ASCII PGM grayscale 12// P3 ASCII PPM RGB 13// P4 binary PBM 14// P5 binary PGM 15// P6 binary PPM 16// 17// We emit P5 (binary grayscale) + P6 (binary RGB) -- the two 18// forms useful for rendering output. ASCII forms are larger and 19// slower. Binary PBM (P4) packs 8 pixels per byte and is 20// rarely worth the complexity. 21// 22// Use cases: Nishi game engine screenshot dump, procgen debug 23// visualisations, DDS/PNG precursor, comparing frame buffers 24// byte-exact in regression tests. 25// 26// Invariants: 27// P1 Header is plain ASCII, LF-terminated fields. 28// P2 max_value literal: we hardcode 255 (8-bit per channel). 29// P3 Binary pixel block: P5 width*height bytes, P6 30// width*height*3 bytes, row-major, top-to-bottom. 31 32// nx_safety_envelope: 33// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 34// sil_target: SIL1 35// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 36// verdict: NOT_YET_EVALUATED 37 38import "nx_syscalls.nx" 39 40const PPM_ERR_SHORT: i64 = -1 41 42// Write one byte at out[off]; return off + 1. 43func pwr_put(out: *u8, off: i64, b: i64) -> i64 { 44 out[off] = b 45 return off + 1 46} 47 48// Write a decimal i64 at out[off]; returns new offset. Supports 49// non-negative values only (dimensions + max are always >= 0). 50func pwr_put_dec(out: *u8, off: i64, v: i64) -> i64 { 51 if v == 0 { 52 out[off] = 0x30 53 return off + 1 54 } 55 // Build digits in reverse. 56 let digits_raw: *u8 = sys_mmap(32) 57 var tmp: i64 = v 58 var n: i64 = 0 59 while tmp > 0 { 60 digits_raw[n] = 0x30 + (tmp % 10) 61 tmp = tmp / 10 62 n = n + 1 63 } 64 // Emit in forward order. 65 var i: i64 = n - 1 66 var cur: i64 = off 67 while i >= 0 { 68 out[cur] = digits_raw[i] 69 cur = cur + 1 70 i = i - 1 71 } 72 return cur 73} 74 75// Write "P6\nWIDTH HEIGHT\n255\n" header. Returns new offset or 76// PPM_ERR_SHORT. Caller sizes the buffer to accommodate. 77func ppm_write_p6_header(out: *u8, cap: i64, width: i64, height: i64) -> i64 { 78 // Max header length: "P6\n" + 20 + " " + 20 + "\n255\n" = 47ish. 79 if cap < 64 { return PPM_ERR_SHORT } 80 var off: i64 = 0 81 off = pwr_put(out, off, 0x50) // 'P' 82 off = pwr_put(out, off, 0x36) // '6' 83 off = pwr_put(out, off, 0x0A) // '\n' 84 off = pwr_put_dec(out, off, width) 85 off = pwr_put(out, off, 0x20) // ' ' 86 off = pwr_put_dec(out, off, height) 87 off = pwr_put(out, off, 0x0A) 88 off = pwr_put(out, off, 0x32) // '2' 89 off = pwr_put(out, off, 0x35) // '5' 90 off = pwr_put(out, off, 0x35) // '5' 91 off = pwr_put(out, off, 0x0A) 92 return off 93} 94 95// Write P5 (grayscale) header: "P5\nWIDTH HEIGHT\n255\n". 96func ppm_write_p5_header(out: *u8, cap: i64, width: i64, height: i64) -> i64 { 97 if cap < 64 { return PPM_ERR_SHORT } 98 var off: i64 = 0 99 off = pwr_put(out, off, 0x50) 100 off = pwr_put(out, off, 0x35) // '5' 101 off = pwr_put(out, off, 0x0A) 102 off = pwr_put_dec(out, off, width) 103 off = pwr_put(out, off, 0x20) 104 off = pwr_put_dec(out, off, height) 105 off = pwr_put(out, off, 0x0A) 106 off = pwr_put(out, off, 0x32) 107 off = pwr_put(out, off, 0x35) 108 off = pwr_put(out, off, 0x35) 109 off = pwr_put(out, off, 0x0A) 110 return off 111} 112 113// Write the full P6 file: header + RGB block from `pixels` 114// (caller-owned u8 array of size width*height*3, interleaved 115// [R,G,B,R,G,B,...] row-major top-to-bottom). Returns total 116// bytes written. 117func ppm_write_p6(out: *u8, cap: i64, width: i64, height: i64, 118 pixels: *u8) -> i64 { 119 let data_bytes: i64 = width * height * 3 120 if cap < 64 + data_bytes { return PPM_ERR_SHORT } 121 var off: i64 = ppm_write_p6_header(out, cap, width, height) 122 if off < 0 { return off } 123 124 var i: i64 = 0 125 while i < data_bytes { 126 out[off + i] = pixels[i] 127 i = i + 1 128 } 129 return off + data_bytes 130} 131 132// Compile-only smoke: write a 2x1 red-green P6 image. 133func main() -> i64 { 134 let out: *u8 = sys_mmap(128) 135 let px: *u8 = sys_mmap(16) 136 // (R,G,B) = (255,0,0), (0,255,0) 137 px[0] = 255; px[1] = 0; px[2] = 0 138 px[3] = 0; px[4] = 255; px[5] = 0 139 140 let n: i64 = ppm_write_p6(out, 128, 2, 1, px) 141 if n <= 0 { return 1 } 142 143 // Header "P6\n2 1\n255\n" = 11 bytes; data = 6 bytes; total 17. 144 if n != 17 { return 2 } 145 146 // Magic bytes 147 if out[0] != 0x50 { return 3 } // 'P' 148 if out[1] != 0x36 { return 4 } // '6' 149 if out[2] != 0x0A { return 5 } // '\n' 150 151 // Width '2' at offset 3. 152 if out[3] != 0x32 { return 6 } 153 154 // After header at offset 11, pixel byte is 255. 155 if out[11] != 255 { return 7 } 156 return 0 157}