code wiki / (root) / ppm_writer.nx

ppm_writer.nx source

↩ module page · 151 lines · 5023 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 32import "syscalls.nx" 33 34const PPM_ERR_SHORT: i64 = -1 35 36// Write one byte at out[off]; return off + 1. 37func pwr_put(out: *u8, off: i64, b: i64) -> i64 { 38 out[off] = b 39 return off + 1 40} 41 42// Write a decimal i64 at out[off]; returns new offset. Supports 43// non-negative values only (dimensions + max are always >= 0). 44func pwr_put_dec(out: *u8, off: i64, v: i64) -> i64 { 45 if v == 0 { 46 out[off] = 0x30 47 return off + 1 48 } 49 // Build digits in reverse. 50 let digits_raw: *u8 = sys_mmap(32) 51 var tmp: i64 = v 52 var n: i64 = 0 53 while tmp > 0 { 54 digits_raw[n] = 0x30 + (tmp % 10) 55 tmp = tmp / 10 56 n = n + 1 57 } 58 // Emit in forward order. 59 var i: i64 = n - 1 60 var cur: i64 = off 61 while i >= 0 { 62 out[cur] = digits_raw[i] 63 cur = cur + 1 64 i = i - 1 65 } 66 return cur 67} 68 69// Write "P6\nWIDTH HEIGHT\n255\n" header. Returns new offset or 70// PPM_ERR_SHORT. Caller sizes the buffer to accommodate. 71func ppm_write_p6_header(out: *u8, cap: i64, width: i64, height: i64) -> i64 { 72 // Max header length: "P6\n" + 20 + " " + 20 + "\n255\n" = 47ish. 73 if cap < 64 { return PPM_ERR_SHORT } 74 var off: i64 = 0 75 off = pwr_put(out, off, 0x50) // 'P' 76 off = pwr_put(out, off, 0x36) // '6' 77 off = pwr_put(out, off, 0x0A) // '\n' 78 off = pwr_put_dec(out, off, width) 79 off = pwr_put(out, off, 0x20) // ' ' 80 off = pwr_put_dec(out, off, height) 81 off = pwr_put(out, off, 0x0A) 82 off = pwr_put(out, off, 0x32) // '2' 83 off = pwr_put(out, off, 0x35) // '5' 84 off = pwr_put(out, off, 0x35) // '5' 85 off = pwr_put(out, off, 0x0A) 86 return off 87} 88 89// Write P5 (grayscale) header: "P5\nWIDTH HEIGHT\n255\n". 90func ppm_write_p5_header(out: *u8, cap: i64, width: i64, height: i64) -> i64 { 91 if cap < 64 { return PPM_ERR_SHORT } 92 var off: i64 = 0 93 off = pwr_put(out, off, 0x50) 94 off = pwr_put(out, off, 0x35) // '5' 95 off = pwr_put(out, off, 0x0A) 96 off = pwr_put_dec(out, off, width) 97 off = pwr_put(out, off, 0x20) 98 off = pwr_put_dec(out, off, height) 99 off = pwr_put(out, off, 0x0A) 100 off = pwr_put(out, off, 0x32) 101 off = pwr_put(out, off, 0x35) 102 off = pwr_put(out, off, 0x35) 103 off = pwr_put(out, off, 0x0A) 104 return off 105} 106 107// Write the full P6 file: header + RGB block from `pixels` 108// (caller-owned u8 array of size width*height*3, interleaved 109// [R,G,B,R,G,B,...] row-major top-to-bottom). Returns total 110// bytes written. 111func ppm_write_p6(out: *u8, cap: i64, width: i64, height: i64, 112 pixels: *u8) -> i64 { 113 let data_bytes: i64 = width * height * 3 114 if cap < 64 + data_bytes { return PPM_ERR_SHORT } 115 var off: i64 = ppm_write_p6_header(out, cap, width, height) 116 if off < 0 { return off } 117 118 var i: i64 = 0 119 while i < data_bytes { 120 out[off + i] = pixels[i] 121 i = i + 1 122 } 123 return off + data_bytes 124} 125 126// Compile-only smoke: write a 2x1 red-green P6 image. 127func main() -> i64 { 128 let out: *u8 = sys_mmap(128) 129 let px: *u8 = sys_mmap(16) 130 // (R,G,B) = (255,0,0), (0,255,0) 131 px[0] = 255; px[1] = 0; px[2] = 0 132 px[3] = 0; px[4] = 255; px[5] = 0 133 134 let n: i64 = ppm_write_p6(out, 128, 2, 1, px) 135 if n <= 0 { return 1 } 136 137 // Header "P6\n2 1\n255\n" = 11 bytes; data = 6 bytes; total 17. 138 if n != 17 { return 2 } 139 140 // Magic bytes 141 if out[0] != 0x50 { return 3 } // 'P' 142 if out[1] != 0x36 { return 4 } // '6' 143 if out[2] != 0x0A { return 5 } // '\n' 144 145 // Width '2' at offset 3. 146 if out[3] != 0x32 { return 6 } 147 148 // After header at offset 11, pixel byte is 255. 149 if out[11] != 255 { return 7 } 150 return 0 151}