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}