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}