nx_cdmap_raster_lib.nx source
↩ module page · 940 lines · 49430 B
1// nx_cdmap_raster_lib.nx -- THE SOVEREIGN RASTERISER FOR THE POSITION MAP (datavis DV6 export, 2026-09-15).
2// A compare page's position map is an inline SVG the emitter writes from nx_swcompare_lib (cdmap_pass). This library
3// turns that SVG subset into ink: it reads the page's :root colour tokens, walks the figure's elements in document order
4// (rect, circle, line, polyline with the momentum arrow marker, polygon, text incl. the -90 rotated axis title and the
5// note groups), paints them into an i64 framebuffer (r + g*256 + b*65536, the packing nx_png writes) at an integer scale,
6// draws every glyph as coverage through the estate's own TrueType engine (nx_ttf_fontlib, the same engine the pixel
7// referee nx_cdmap_pixjudge already trusts), and writes the PNG through nx_png. It also writes the STANDALONE SVG twin:
8// the figure's bytes with the svg namespace declared and every var(--nx-color-*) resolved to the rgb the page defined,
9// so the file renders identically outside the page's stylesheet.
10// WHY A SUBSET AND NOT A GENERAL SVG ENGINE: the emitter is the ONLY producer of these bytes, so the set of constructs is
11// closed and known (cdp_* in nx_swcompare_lib), and every construct outside it is COUNTED and NAMED in the stats, never
12// silently dropped: a render that cannot say what it skipped is a picture of the renderer's blind spots.
13// SCOPE STATED: the face is Liberation Sans (SIL-OFL data, licence beside the bytes) where the page asks for system-ui,
14// letter-spacing and rounded rect corners are not drawn, and a text rotated by anything but -90 degrees is skipped by
15// name. The pixel referee (DV2) and this rasteriser share the font engine, so a label the referee measures as ink is the
16// same ink this file paints. license_tier: ORIGINAL
17import "nx_syscalls.nx"
18import "nx_cdmap_referee_lib.nx" // cr_find / cr_attr / cr_int_at / cr_tag_end: the ONE tag reader the referees use; brings nx_ttf_fontlib
19import "nx_png.nx"
20
21const RS_I64_BYTES: i64 = 8
22const RS_ABSENT: i64 = 0 - 1
23const RS_SCALE_DEFAULT: i64 = 2
24const RS_SCALE_MAX: i64 = 8 // 760x520 at 8 is a 24 MiB framebuffer; past this the export is refused by name
25const RS_CANVAS_MAX: i64 = 4096 // a viewBox axis past this is refused (the pixel referee's bound)
26const RS_VB_W_DEFAULT: i64 = 760 // the emitter's CDP_SVG_W when the tag carries no viewBox
27const RS_VB_H_DEFAULT: i64 = 520
28const RS_TOK_MAX: i64 = 48 // :root colour tokens read (the page defines under twenty)
29const RS_TOK_NAME_W: i64 = 32
30const RS_TOK_REC: i64 = 4 // r, g, b, name-slot
31const RS_TOK_R: i64 = 0
32const RS_TOK_G: i64 = 1
33const RS_TOK_B: i64 = 2
34const RS_PERMIL: i64 = 1000
35const RS_COV_MAX: i64 = 255
36const RS_INK_MIN: i64 = 8 // coverage below this paints nothing (fringe)
37const RS_STROKE_DEFAULT: i64 = 1 // canvas px when an element carries no stroke-width
38const RS_HALO_PX: i64 = 1 // the label halo (paint-order:stroke, 3px) reads as one canvas px each side of the ink
39const RS_ARROW_LEN: i64 = 7 // the emitter's marker: 7 px, an 8x8 viewBox with its tip at (8,4)
40const RS_ARROW_HALF: i64 = 3
41const RS_WALK_DIV: i64 = 2 // line walk step = scaled px / this
42const RS_DEG_ROT: i64 = 0 - 90 // the one rotation the emitter uses (the y-axis title)
43const RS_VAL_W: i64 = 64 // an attribute value copy (fill='..', transform='..')
44const RS_TEXT_W: i64 = 256 // a text element's content copy
45const RS_PTS_MAX: i64 = 64 // points on one polyline or polygon (a momentum path has two)
46const RS_XS_MAX: i64 = 64 // scanline crossings per row
47const RS_MAX_DASH: i64 = 4096 // scaled px of one dash pattern, bounds the walker
48const RS_STAMP: i64 = 1 // announce on the png publish
49const RS_SVG_HEAD: *u8 = "<?xml version='1.0' encoding='UTF-8'?>\n"
50const RS_SVG_NS: *u8 = " xmlns='http://www.w3.org/2000/svg'"
51const RS_VAR_OPEN: *u8 = "var(--nx-color-"
52const RS_SVG_OPEN: *u8 = "<svg class='cdmap'"
53const RS_SVG_CLOSE: *u8 = "</svg>"
54const RS_ROOT_OPEN: *u8 = ":root{"
55const RS_TOK_PREFIX: *u8 = "--nx-color-"
56const RS_RGB_OPEN: *u8 = "rgb("
57const RS_TAG_TEXT_CLOSE: *u8 = "</text>"
58const RS_TAG_TITLE_CLOSE: *u8 = "</title>"
59const RS_TAG_DESC_CLOSE: *u8 = "</desc>"
60const RS_TAG_DEFS_CLOSE: *u8 = "</defs>"
61const RS_TAG_STYLE_CLOSE: *u8 = "</style>"
62const RS_NOTE_CLASS: *u8 = "class='cdnote'"
63const RS_CH_LT: i64 = 60
64const RS_CH_GT: i64 = 62
65const RS_CH_SLASH: i64 = 47
66const RS_CH_QUOTE: i64 = 39
67const RS_CH_SPACE: i64 = 32
68const RS_CH_COMMA: i64 = 44
69const RS_CH_DOT: i64 = 46
70const RS_CH_MINUS: i64 = 45
71const RS_CH_RPAREN: i64 = 41
72const RS_CH_SEMI: i64 = 59
73const RS_CH_COLON: i64 = 58
74const RS_CH_RBRACE: i64 = 125
75const RS_CH_0: i64 = 48
76const RS_CH_9: i64 = 57
77const RS_CH_A: i64 = 65
78const RS_CH_Z: i64 = 90
79const RS_CH_a: i64 = 97
80const RS_CH_z: i64 = 122
81const RS_CH_AMP: i64 = 38
82const RS_CH_NL: i64 = 10
83const RS_CASE_GAP: i64 = 32
84const RS_BASE10: i64 = 10
85const RS_BYTE: i64 = 256
86const RS_BYTE2: i64 = 65536
87const RS_TWO: i64 = 2
88const RS_MODE_0644: i64 = 420
89
90// ---- the stats record: every count a caller or a gate reads ----
91const RS_S_RECT: i64 = 0
92const RS_S_CIRCLE: i64 = 1
93const RS_S_LINE: i64 = 2
94const RS_S_POLYLINE: i64 = 3
95const RS_S_POLYGON: i64 = 4
96const RS_S_TEXT: i64 = 5
97const RS_S_SKIPPED: i64 = 6 // elements outside the subset (named on stderr-free stdout by the caller through rs_skip_name)
98const RS_S_UNRESOLVED: i64 = 7 // colour values no token or literal resolved (painted fg)
99const RS_S_GLYPHS: i64 = 8 // glyphs drawn
100const RS_S_ROTATED: i64 = 9 // rotated texts drawn
101const RS_S_ROTSKIP: i64 = 10 // rotated texts skipped (an angle the subset does not draw)
102const RS_S_NOTES: i64 = 11 // cdnote groups seen
103const RS_S_ARROWS: i64 = 12 // marker-end arrows drawn
104const RS_S_INK: i64 = 13 // pixels painted by text ink (a scale field: 0 means no glyph landed)
105const RS_S_W: i64 = 14
106const RS_S_TOKENS: i64 = 15 // colour tokens read from :root
107const RS_S_SLOTS: i64 = 16
108
109// ---- outcomes of rs_export ----
110const RS_OK: i64 = 0
111const RS_E_NOPAGE: i64 = 0 - 1
112const RS_E_NOSVG: i64 = 0 - 2
113const RS_E_NOFONT: i64 = 0 - 3
114const RS_E_CANVAS: i64 = 0 - 4
115const RS_E_PNG: i64 = 0 - 5
116const RS_E_SVG: i64 = 0 - 6
117const RS_E_SCALE: i64 = 0 - 7
118const RS_E_NOTOKENS: i64 = 0 - 8
119
120func rs_i64(n: i64) -> *i64 { return sys_mmap(n * RS_I64_BYTES) as *i64 }
121func rs_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
122func rs_abs(v: i64) -> i64 { if v < 0 { return 0 - v } return v }
123func rs_max(a: i64, b: i64) -> i64 { if a > b { return a } return b }
124func rs_min(a: i64, b: i64) -> i64 { if a < b { return a } return b }
125func rs_isqrt(v: i64) -> i64 { if v <= 0 { return 0 } var r: i64 = 1; while r * r <= v { r = r + 1 } return r - 1 }
126func rs_rc_name(rc: i64) -> *u8 {
127 if rc == RS_OK { return "OK" as *u8 }
128 if rc == RS_E_NOPAGE { return "NO-PAGE" as *u8 }
129 if rc == RS_E_NOSVG { return "NO-CDMAP-SVG" as *u8 }
130 if rc == RS_E_NOFONT { return "NO-FONT" as *u8 }
131 if rc == RS_E_CANVAS { return "CANVAS-UNREADABLE" as *u8 }
132 if rc == RS_E_PNG { return "PNG-WRITE-FAILED" as *u8 }
133 if rc == RS_E_SVG { return "SVG-WRITE-FAILED" as *u8 }
134 if rc == RS_E_SCALE { return "SCALE-OUT-OF-RANGE" as *u8 }
135 if rc == RS_E_NOTOKENS { return "NO-ROOT-TOKENS" as *u8 }
136 return "UNKNOWN" as *u8
137}
138
139// ---- the colour token table: name slot i at names + i*RS_TOK_NAME_W, rgb at rec + i*RS_TOK_REC ----
140func rs_tok_rec(recs: *i64, i: i64) -> *i64 { return ((recs as i64) + i * RS_TOK_REC * RS_I64_BYTES) as *i64 }
141func rs_tok_name(names: *u8, i: i64) -> *u8 { return ((names as i64) + i * RS_TOK_NAME_W) as *u8 }
142func rs_is_ident(c: i64) -> i64 {
143 if c >= RS_CH_a { if c <= RS_CH_z { return 1 } }
144 if c >= RS_CH_A { if c <= RS_CH_Z { return 1 } }
145 if c >= RS_CH_0 { if c <= RS_CH_9 { return 1 } }
146 if c == RS_CH_MINUS { return 1 }
147 return 0
148}
149// parse "rgb(r,g,b" at h[p..to): 1 and the triple, 0 when the bytes are not that shape
150func rs_rgb_at(h: *u8, p: i64, to: i64, rgb: *i64) -> i64 {
151 let key: *u8 = RS_RGB_OPEN
152 if cr_find(h, p, rs_min(to, p + rs_slen(key)), key) != p { return 0 }
153 var q: i64 = p + rs_slen(key)
154 var k: i64 = 0
155 while k < 3 {
156 while q < to { if (h[q] as i64) == RS_CH_SPACE { q = q + 1 } else { q = to + q } }
157 if q > to { q = q - to }
158 let v: i64 = cr_int_at(h, q, to)
159 if v == CR_ABSENT { return 0 }
160 rgb[k] = v
161 while q < to { let c: i64 = h[q] as i64; if c >= RS_CH_0 { if c <= RS_CH_9 { q = q + 1 } else { q = to + q } } else { q = to + q } }
162 if q > to { q = q - to }
163 if k < 2 { if q < to { if (h[q] as i64) == RS_CH_COMMA { q = q + 1 } else { return 0 } } else { return 0 } }
164 k = k + 1
165 }
166 return 1
167}
168// read the FIRST :root{...} block's --nx-color-* tokens (the light theme, the one the page paints by default). Returns the
169// token count, 0 when the page has no :root block.
170func rs_theme_load(h: *u8, n: i64, names: *u8, recs: *i64) -> i64 {
171 let r0: i64 = cr_find(h, 0, n, RS_ROOT_OPEN)
172 if r0 == CR_ABSENT { return 0 }
173 var r1: i64 = r0
174 while r1 < n { if (h[r1] as i64) == RS_CH_RBRACE { r1 = n + r1 } else { r1 = r1 + 1 } }
175 if r1 > n { r1 = r1 - n } else { r1 = n }
176 var ntok: i64 = 0
177 var p: i64 = r0
178 var go: i64 = 1
179 while go == 1 {
180 let t0: i64 = cr_find(h, p, r1, RS_TOK_PREFIX)
181 if t0 == CR_ABSENT { go = 0 } else {
182 var q: i64 = t0 + rs_slen(RS_TOK_PREFIX)
183 let nm: *u8 = rs_tok_name(names, ntok)
184 var k: i64 = 0
185 while q < r1 { if rs_is_ident(h[q] as i64) == 1 { if k < RS_TOK_NAME_W - 1 { nm[k] = h[q]; k = k + 1 } q = q + 1 } else { q = r1 + q } }
186 if q > r1 { q = q - r1 }
187 nm[k] = 0 as u8
188 // ':' then spaces then rgb(
189 if q < r1 { if (h[q] as i64) == RS_CH_COLON { q = q + 1 } }
190 while q < r1 { if (h[q] as i64) == RS_CH_SPACE { q = q + 1 } else { q = r1 + q } }
191 if q > r1 { q = q - r1 }
192 let rec: *i64 = rs_tok_rec(recs, ntok)
193 if rs_rgb_at(h, q, r1, rec) == 1 { if ntok < RS_TOK_MAX - 1 { ntok = ntok + 1 } }
194 p = q
195 }
196 }
197 return ntok
198}
199func rs_tok_find(names: *u8, ntok: i64, want: *u8, wl: i64) -> i64 {
200 var i: i64 = 0
201 while i < ntok {
202 let nm: *u8 = rs_tok_name(names, i)
203 var k: i64 = 0
204 var same: i64 = 1
205 while k < wl { if nm[k] != want[k] { same = 0; k = wl } else { k = k + 1 } }
206 if same == 1 { if nm[wl] == (0 as u8) { return i } }
207 i = i + 1
208 }
209 return RS_ABSENT
210}
211// the rgb of a token by name (NUL-terminated); 1 found, 0 absent
212func rs_tok_rgb(names: *u8, recs: *i64, ntok: i64, name: *u8, rgb: *i64) -> i64 {
213 let i: i64 = rs_tok_find(names, ntok, name, rs_slen(name))
214 if i == RS_ABSENT { return 0 }
215 let rec: *i64 = rs_tok_rec(recs, i)
216 rgb[0] = rec[RS_TOK_R]; rgb[1] = rec[RS_TOK_G]; rgb[2] = rec[RS_TOK_B]
217 return 1
218}
219
220// ---- attribute readers over one tag [t0, t1) ----
221// copy an attribute's quoted value (needle spelled WITH its leading space, name, = and quote) into out; the length, or RS_ABSENT
222func rs_attr_str(h: *u8, t0: i64, t1: i64, needle: *u8, out: *u8, cap: i64) -> i64 {
223 let at: i64 = cr_find(h, t0, t1, needle)
224 if at == CR_ABSENT { return RS_ABSENT }
225 var q: i64 = at + rs_slen(needle)
226 var k: i64 = 0
227 while q < t1 { if (h[q] as i64) == RS_CH_QUOTE { q = t1 + q } else { if k < cap - 1 { out[k] = h[q]; k = k + 1 } q = q + 1 } }
228 out[k] = 0 as u8
229 return k
230}
231// a fraction attribute ("0.82") as permil; RS_PERMIL when absent, 0..1000 otherwise
232func rs_attr_permil(h: *u8, t0: i64, t1: i64, needle: *u8) -> i64 {
233 let at: i64 = cr_find(h, t0, t1, needle)
234 if at == CR_ABSENT { return RS_PERMIL }
235 var q: i64 = at + rs_slen(needle)
236 let whole: i64 = cr_int_at(h, q, t1)
237 if whole == CR_ABSENT { return RS_PERMIL }
238 if whole >= 1 { return RS_PERMIL }
239 while q < t1 { let c: i64 = h[q] as i64; if c >= RS_CH_0 { if c <= RS_CH_9 { q = q + 1 } else { q = t1 + q } } else { q = t1 + q } }
240 if q > t1 { q = q - t1 }
241 if q >= t1 { return 0 }
242 if (h[q] as i64) != RS_CH_DOT { return 0 }
243 q = q + 1
244 var v: i64 = 0
245 var nd: i64 = 0
246 while q < t1 { let c: i64 = h[q] as i64; if c >= RS_CH_0 { if c <= RS_CH_9 { if nd < 3 { v = v * RS_BASE10 + (c - RS_CH_0); nd = nd + 1 } q = q + 1 } else { q = t1 + q } } else { q = t1 + q } }
247 while nd < 3 { v = v * RS_BASE10; nd = nd + 1 }
248 return v
249}
250// resolve a colour value copied from an attribute: 1 = rgb filled, 0 = none, RS_ABSENT = unresolved (rgb = fg, counted)
251func rs_color_value(v: *u8, names: *u8, recs: *i64, ntok: i64, fg: *i64, rgb: *i64, stats: *i64) -> i64 {
252 if v[0] == (0 as u8) { return 0 }
253 if cr_streq(v, "none" as *u8) == 1 { return 0 }
254 if cr_streq(v, "currentColor" as *u8) == 1 { rgb[0] = fg[0]; rgb[1] = fg[1]; rgb[2] = fg[2]; return 1 }
255 let vl: i64 = rs_slen(v)
256 if rs_rgb_at(v, 0, vl, rgb) == 1 { return 1 }
257 let vo: i64 = rs_slen(RS_VAR_OPEN)
258 if cr_find(v, 0, rs_min(vl, vo), RS_VAR_OPEN) == 0 {
259 var e: i64 = vo
260 while e < vl { if (v[e] as i64) == RS_CH_RPAREN { e = vl + e } else { e = e + 1 } }
261 if e > vl { e = e - vl }
262 let i: i64 = rs_tok_find(names, ntok, ((v as i64) + vo) as *u8, e - vo)
263 if i != RS_ABSENT { let rec: *i64 = rs_tok_rec(recs, i); rgb[0] = rec[RS_TOK_R]; rgb[1] = rec[RS_TOK_G]; rgb[2] = rec[RS_TOK_B]; return 1 }
264 }
265 stats[RS_S_UNRESOLVED] = stats[RS_S_UNRESOLVED] + 1
266 rgb[0] = fg[0]; rgb[1] = fg[1]; rgb[2] = fg[2]
267 return RS_ABSENT
268}
269// the colour of an attribute (" fill='" / " stroke='"): 1 rgb, 0 none/absent, RS_ABSENT unresolved-painted-fg
270func rs_attr_color(h: *u8, t0: i64, t1: i64, needle: *u8, names: *u8, recs: *i64, ntok: i64, fg: *i64, rgb: *i64, stats: *i64, present: *i64) -> i64 {
271 let v: *u8 = sys_mmap(RS_VAL_W)
272 let l: i64 = rs_attr_str(h, t0, t1, needle, v, RS_VAL_W)
273 present[0] = 0
274 if l == RS_ABSENT { sys_munmap(v, RS_VAL_W); return 0 }
275 present[0] = 1
276 let r: i64 = rs_color_value(v, names, recs, ntok, fg, rgb, stats)
277 sys_munmap(v, RS_VAL_W)
278 return r
279}
280// "6 4" -> on, off (scaled px); absent -> 0,0 (solid)
281func rs_attr_dash(h: *u8, t0: i64, t1: i64, scale: i64, dash: *i64) -> i64 {
282 dash[0] = 0; dash[1] = 0
283 let key: *u8 = " stroke-dasharray='" as *u8
284 let at: i64 = cr_find(h, t0, t1, key)
285 if at == CR_ABSENT { return 0 }
286 var q: i64 = at + rs_slen(key)
287 let a: i64 = cr_int_at(h, q, t1)
288 if a == CR_ABSENT { return 0 }
289 while q < t1 { let c: i64 = h[q] as i64; if c >= RS_CH_0 { if c <= RS_CH_9 { q = q + 1 } else { q = t1 + q } } else { q = t1 + q } }
290 if q > t1 { q = q - t1 }
291 while q < t1 { if (h[q] as i64) == RS_CH_SPACE { q = q + 1 } else { q = t1 + q } }
292 if q > t1 { q = q - t1 }
293 var b: i64 = cr_int_at(h, q, t1)
294 if b == CR_ABSENT { b = a }
295 dash[0] = a * scale; dash[1] = b * scale
296 return 1
297}
298// "x,y x,y ..." points into xs/ys (scaled); the count
299func rs_attr_points(h: *u8, t0: i64, t1: i64, scale: i64, xs: *i64, ys: *i64) -> i64 {
300 let key: *u8 = " points='" as *u8
301 let at: i64 = cr_find(h, t0, t1, key)
302 if at == CR_ABSENT { return 0 }
303 var q: i64 = at + rs_slen(key)
304 var np: i64 = 0
305 var go: i64 = 1
306 while go == 1 {
307 while q < t1 { if (h[q] as i64) == RS_CH_SPACE { q = q + 1 } else { q = t1 + q } }
308 if q > t1 { q = q - t1 }
309 if q >= t1 { go = 0 } else { if (h[q] as i64) == RS_CH_QUOTE { go = 0 } else {
310 let x: i64 = cr_int_at(h, q, t1)
311 if x == CR_ABSENT { go = 0 } else {
312 if (h[q] as i64) == RS_CH_MINUS { q = q + 1 }
313 while q < t1 { let c: i64 = h[q] as i64; if c >= RS_CH_0 { if c <= RS_CH_9 { q = q + 1 } else { q = t1 + q } } else { q = t1 + q } }
314 if q > t1 { q = q - t1 }
315 if q < t1 { if (h[q] as i64) == RS_CH_COMMA { q = q + 1 } }
316 let y: i64 = cr_int_at(h, q, t1)
317 if y == CR_ABSENT { go = 0 } else {
318 if (h[q] as i64) == RS_CH_MINUS { q = q + 1 }
319 while q < t1 { let c: i64 = h[q] as i64; if c >= RS_CH_0 { if c <= RS_CH_9 { q = q + 1 } else { q = t1 + q } } else { q = t1 + q } }
320 if q > t1 { q = q - t1 }
321 if np < RS_PTS_MAX { xs[np] = x * scale; ys[np] = y * scale; np = np + 1 }
322 }
323 }
324 } }
325 }
326 return np
327}
328
329// ---- the framebuffer ----
330func rs_fb_get(fb: *i64, W: i64, x: i64, y: i64, rgb: *i64) -> i64 { let v: i64 = fb[y * W + x]; rgb[0] = v % RS_BYTE; rgb[1] = (v / RS_BYTE) % RS_BYTE; rgb[2] = (v / RS_BYTE2) % RS_BYTE; return 0 }
331func rs_pack(r: i64, g: i64, b: i64) -> i64 { return r + g * RS_BYTE + b * RS_BYTE2 }
332// paint one pixel with alpha in permil (clipped)
333func rs_px(fb: *i64, W: i64, H: i64, x: i64, y: i64, rgb: *i64, a: i64) -> i64 {
334 if x < 0 { return 0 }
335 if y < 0 { return 0 }
336 if x >= W { return 0 }
337 if y >= H { return 0 }
338 if a >= RS_PERMIL { fb[y * W + x] = rs_pack(rgb[0], rgb[1], rgb[2]); return 1 }
339 if a <= 0 { return 0 }
340 let v: i64 = fb[y * W + x]
341 let r0: i64 = v % RS_BYTE
342 let g0: i64 = (v / RS_BYTE) % RS_BYTE
343 let b0: i64 = (v / RS_BYTE2) % RS_BYTE
344 let r: i64 = (r0 * (RS_PERMIL - a) + rgb[0] * a) / RS_PERMIL
345 let g: i64 = (g0 * (RS_PERMIL - a) + rgb[1] * a) / RS_PERMIL
346 let b: i64 = (b0 * (RS_PERMIL - a) + rgb[2] * a) / RS_PERMIL
347 fb[y * W + x] = rs_pack(r, g, b)
348 return 1
349}
350func rs_fill_all(fb: *i64, W: i64, H: i64, rgb: *i64) -> i64 { let v: i64 = rs_pack(rgb[0], rgb[1], rgb[2]); var i: i64 = 0; while i < W * H { fb[i] = v; i = i + 1 } return 0 }
351func rs_fill_rect(fb: *i64, W: i64, H: i64, x0: i64, y0: i64, w: i64, h: i64, rgb: *i64, a: i64) -> i64 {
352 var y: i64 = y0
353 while y < y0 + h { var x: i64 = x0; while x < x0 + w { rs_px(fb, W, H, x, y, rgb, a); x = x + 1 } y = y + 1 }
354 return 0
355}
356// a filled disk; radius in scaled px
357func rs_fill_disk(fb: *i64, W: i64, H: i64, cx: i64, cy: i64, r: i64, rgb: *i64, a: i64) -> i64 {
358 var y: i64 = cy - r
359 while y <= cy + r {
360 var x: i64 = cx - r
361 while x <= cx + r { if (x - cx) * (x - cx) + (y - cy) * (y - cy) <= r * r { rs_px(fb, W, H, x, y, rgb, a) } x = x + 1 }
362 y = y + 1
363 }
364 return 0
365}
366// a ring of width wd centred on radius r
367func rs_ring(fb: *i64, W: i64, H: i64, cx: i64, cy: i64, r: i64, wd: i64, rgb: *i64, a: i64) -> i64 {
368 let ro: i64 = r + (wd + 1) / RS_TWO
369 var ri: i64 = r - wd / RS_TWO
370 if ri < 0 { ri = 0 }
371 var y: i64 = cy - ro
372 while y <= cy + ro {
373 var x: i64 = cx - ro
374 while x <= cx + ro {
375 let d2: i64 = (x - cx) * (x - cx) + (y - cy) * (y - cy)
376 if d2 <= ro * ro { if d2 >= ri * ri { rs_px(fb, W, H, x, y, rgb, a) } }
377 x = x + 1
378 }
379 y = y + 1
380 }
381 return 0
382}
383// a stroked segment of width wd with an optional dash (on/off in scaled px, 0 = solid): the walker steps along the
384// segment in sub-pixel increments and stamps a disk of radius wd/2 wherever the dash phase is ON
385func rs_seg(fb: *i64, W: i64, H: i64, x1: i64, y1: i64, x2: i64, y2: i64, wd: i64, rgb: *i64, a: i64, dash: *i64, phase0: i64) -> i64 {
386 let dx: i64 = x2 - x1
387 let dy: i64 = y2 - y1
388 let len: i64 = rs_isqrt(dx * dx + dy * dy)
389 if len == 0 { rs_fill_disk(fb, W, H, x1, y1, wd / RS_TWO, rgb, a); return phase0 }
390 let steps: i64 = len * RS_WALK_DIV + 1
391 let period: i64 = dash[0] + dash[1]
392 var s: i64 = 0
393 var phase: i64 = phase0
394 while s <= steps {
395 let px: i64 = x1 + (dx * s) / steps
396 let py: i64 = y1 + (dy * s) / steps
397 var on: i64 = 1
398 if period > 0 { phase = phase0 + (len * s) / steps; if phase % period >= dash[0] { on = 0 } }
399 if on == 1 { if wd <= 1 { rs_px(fb, W, H, px, py, rgb, a) } else { rs_fill_disk(fb, W, H, px, py, wd / RS_TWO, rgb, a) } }
400 s = s + 1
401 }
402 if period > 0 { return (phase0 + len) % period }
403 return 0
404}
405// even-odd scanline fill of a closed polygon (scaled coords)
406func rs_fill_poly(fb: *i64, W: i64, H: i64, xs: *i64, ys: *i64, np: i64, rgb: *i64, a: i64) -> i64 {
407 if np < 3 { return 0 }
408 var ymin: i64 = ys[0]
409 var ymax: i64 = ys[0]
410 var i: i64 = 1
411 while i < np { if ys[i] < ymin { ymin = ys[i] } if ys[i] > ymax { ymax = ys[i] } i = i + 1 }
412 if ymin < 0 { ymin = 0 }
413 if ymax >= H { ymax = H - 1 }
414 let xcross: *i64 = rs_i64(RS_XS_MAX)
415 var y: i64 = ymin
416 while y <= ymax {
417 var nx: i64 = 0
418 var k: i64 = 0
419 while k < np {
420 let j: i64 = (k + 1) % np
421 let ya: i64 = ys[k]
422 let yb: i64 = ys[j]
423 if ya != yb {
424 var lo: i64 = ya
425 var hi: i64 = yb
426 if lo > hi { lo = yb; hi = ya }
427 if y >= lo { if y < hi {
428 let x: i64 = xs[k] + ((xs[j] - xs[k]) * (y - ya)) / (yb - ya)
429 if nx < RS_XS_MAX { xcross[nx] = x; nx = nx + 1 }
430 } }
431 }
432 k = k + 1
433 }
434 // sort the crossings (tiny n)
435 var p: i64 = 1
436 while p < nx { var q: i64 = p; while q > 0 { if xcross[q - 1] > xcross[q] { let t: i64 = xcross[q]; xcross[q] = xcross[q - 1]; xcross[q - 1] = t; q = q - 1 } else { q = 0 } } p = p + 1 }
437 var c: i64 = 0
438 while c + 1 < nx {
439 var x: i64 = xcross[c]
440 while x <= xcross[c + 1] { rs_px(fb, W, H, x, y, rgb, a); x = x + 1 }
441 c = c + RS_TWO
442 }
443 y = y + 1
444 }
445 sys_munmap(xcross as *u8, RS_XS_MAX * RS_I64_BYTES)
446 return 0
447}
448// the momentum arrow: a filled triangle whose tip is (x2,y2), pointing along (x1,y1)->(x2,y2)
449func rs_arrow(fb: *i64, W: i64, H: i64, x1: i64, y1: i64, x2: i64, y2: i64, scale: i64, rgb: *i64, a: i64) -> i64 {
450 let dx: i64 = x2 - x1
451 let dy: i64 = y2 - y1
452 let len: i64 = rs_isqrt(dx * dx + dy * dy)
453 if len == 0 { return 0 }
454 let al: i64 = RS_ARROW_LEN * scale
455 let ah: i64 = RS_ARROW_HALF * scale
456 let bx: i64 = x2 - (dx * al) / len
457 let by: i64 = y2 - (dy * al) / len
458 let xs: *i64 = rs_i64(3)
459 let ys: *i64 = rs_i64(3)
460 xs[0] = x2; ys[0] = y2
461 xs[1] = bx + (0 - dy) * ah / len; ys[1] = by + dx * ah / len
462 xs[2] = bx - (0 - dy) * ah / len; ys[2] = by - dx * ah / len
463 rs_fill_poly(fb, W, H, xs, ys, 3, rgb, a)
464 sys_munmap(xs as *u8, 3 * RS_I64_BYTES)
465 sys_munmap(ys as *u8, 3 * RS_I64_BYTES)
466 return 0
467}
468
469// ---- text ----
470// ---- entities ----
471// The emitter writes the page's text with HTML entities, and the figure carries five of them inside its svg (measured on
472// the live search page 2026-09-15: – in both axis titles, · and → in the mark titles, — once,
473// & in names). Two consequences: the RASTERISER draws ASCII faces only, so each entity maps to its nearest ASCII
474// (an en or em dash to a hyphen, a middle dot to a period, an arrow to the two bytes "->", a no-break space to a space),
475// and the STANDALONE SVG is XML, where a NAMED HTML entity is a parse error -- so the twin rewrites every named entity
476// the emitter uses to its NUMERIC character reference, which every XML reader accepts. The table is ONE list read by
477// both functions, so an entity added to one cannot be missed by the other.
478const RS_ENT_N: i64 = 10
479const RS_ENT_MAX: i64 = 8 // bytes of the longest entity name incl. & and ;
480func rs_ent_name(i: i64) -> *u8 {
481 if i == 0 { return "&" as *u8 }
482 if i == 1 { return "<" as *u8 }
483 if i == 2 { return ">" as *u8 }
484 if i == 3 { return """ as *u8 }
485 if i == 4 { return "'" as *u8 }
486 if i == 5 { return "–" as *u8 }
487 if i == 6 { return "—" as *u8 }
488 if i == 7 { return "·" as *u8 }
489 if i == 8 { return "→" as *u8 }
490 return " " as *u8
491}
492// the ASCII the rasteriser draws for entity i (two bytes for the arrow, NUL-terminated)
493func rs_ent_ascii(i: i64) -> *u8 {
494 if i == 0 { return "&" as *u8 }
495 if i == 1 { return "<" as *u8 }
496 if i == 2 { return ">" as *u8 }
497 if i == 3 { return "\"" as *u8 }
498 if i == 4 { return "'" as *u8 }
499 if i == 5 { return "-" as *u8 }
500 if i == 6 { return "-" as *u8 }
501 if i == 7 { return "." as *u8 }
502 if i == 8 { return "->" as *u8 }
503 return " " as *u8
504}
505// the XML-safe spelling the standalone twin writes for entity i (the five XML built-ins stay themselves)
506func rs_ent_xml(i: i64) -> *u8 {
507 if i == 0 { return "&" as *u8 }
508 if i == 1 { return "<" as *u8 }
509 if i == 2 { return ">" as *u8 }
510 if i == 3 { return """ as *u8 }
511 if i == 4 { return "'" as *u8 }
512 if i == 5 { return "–" as *u8 }
513 if i == 6 { return "—" as *u8 }
514 if i == 7 { return "·" as *u8 }
515 if i == 8 { return "→" as *u8 }
516 return " " as *u8
517}
518// the entity at h[q] (h[q] == '&'), or RS_ABSENT
519func rs_ent_at(h: *u8, q: i64, to: i64) -> i64 {
520 var i: i64 = 0
521 while i < RS_ENT_N {
522 let nm: *u8 = rs_ent_name(i)
523 let nl: i64 = rs_slen(nm)
524 if q + nl <= to { if cr_find(h, q, q + nl, nm) == q { return i } }
525 i = i + 1
526 }
527 return RS_ABSENT
528}
529// unescape the emitter's entities into out (NUL-terminated); returns the byte count
530func rs_unescape(h: *u8, from: i64, to: i64, out: *u8, cap: i64) -> i64 {
531 var q: i64 = from
532 var k: i64 = 0
533 while q < to {
534 var c: i64 = h[q] as i64
535 var adv: i64 = 1
536 if c == RS_CH_AMP {
537 let e: i64 = rs_ent_at(h, q, to)
538 if e != RS_ABSENT {
539 let a: *u8 = rs_ent_ascii(e)
540 var j: i64 = 0
541 while a[j] != (0 as u8) { if k < cap - 1 { out[k] = a[j]; k = k + 1 } j = j + 1 }
542 q = q + rs_slen(rs_ent_name(e))
543 c = RS_ABSENT
544 }
545 }
546 if c != RS_ABSENT {
547 if c == RS_CH_NL { c = RS_CH_SPACE }
548 if k < cap - 1 { out[k] = c as u8; k = k + 1 }
549 q = q + adv
550 }
551 }
552 out[k] = 0 as u8
553 return k
554}
555func rs_text_w(fh: *i64, s: *u8, ph: i64) -> i64 { var i: i64 = 0; var wpx: i64 = 0; while s[i] != (0 as u8) { wpx = wpx + tf_char_adv(fh, s[i] as i64, ph); i = i + 1 } return wpx }
556// map a canvas point through rotate(-90 cx cy): (px,py) -> (cx + (py-cy), cy - (px-cx)); rot=0 is the identity
557func rs_rot_x(px: i64, py: i64, cx: i64, cy: i64, rot: i64) -> i64 { if rot == 0 { return px } return cx + (py - cy) }
558func rs_rot_y(px: i64, py: i64, cx: i64, cy: i64, rot: i64) -> i64 { if rot == 0 { return py } return cy - (px - cx) }
559// draw s at pixel size ph with its baseline at (x, ybase); anchor 0 start / 1 middle / 2 end; halo paints halo_rgb one
560// scaled-px ring around the ink FIRST (the emitter's paint-order:stroke); rot=1 rotates -90 about (cx,cy). Returns ink px.
561func rs_text(fb: *i64, W: i64, H: i64, fh: *i64, s: *u8, ph: i64, x: i64, ybase: i64, anchor: i64, rgb: *i64, a: i64, halo: i64, halo_rgb: *i64, rot: i64, cx: i64, cy: i64, scale: i64, stats: *i64) -> i64 {
562 let wpx: i64 = rs_text_w(fh, s, ph)
563 var x0: i64 = x
564 if anchor == 1 { x0 = x - wpx / RS_TWO }
565 if anchor == 2 { x0 = x - wpx }
566 let y0: i64 = ybase - tf_asc_px(fh, ph)
567 var ink: i64 = 0
568 var pass: i64 = 0
569 let hr: i64 = RS_HALO_PX * scale
570 while pass < RS_TWO {
571 if pass == 0 { if halo == 0 { pass = 1 } }
572 if pass < RS_TWO {
573 var pen: i64 = x0
574 var i: i64 = 0
575 while s[i] != (0 as u8) {
576 let ch: i64 = s[i] as i64
577 let rec: *i64 = tf_glyph(fh, ch, ph)
578 if (rec as i64) == 0 { pen = pen + tf_char_adv(fh, ch, ph) } else {
579 let cov: *u8 = rec[TF_R_COV] as *u8
580 let cw: i64 = rec[TF_R_CW]
581 let chh: i64 = rec[TF_R_CHH]
582 let gx0: i64 = pen - rec[TF_R_LM]
583 var gy: i64 = 0
584 while gy < chh {
585 var gx: i64 = 0
586 while gx < cw {
587 let cv: i64 = cov[gy * cw + gx] as i64
588 if cv >= RS_INK_MIN {
589 let fx: i64 = gx0 + gx
590 let fy: i64 = y0 + gy
591 if pass == 0 {
592 var hy: i64 = fy - hr
593 while hy <= fy + hr { var hx: i64 = fx - hr; while hx <= fx + hr { rs_px(fb, W, H, rs_rot_x(hx, hy, cx, cy, rot), rs_rot_y(hx, hy, cx, cy, rot), halo_rgb, (cv * a) / RS_COV_MAX); hx = hx + 1 } hy = hy + 1 }
594 } else {
595 ink = ink + rs_px(fb, W, H, rs_rot_x(fx, fy, cx, cy, rot), rs_rot_y(fx, fy, cx, cy, rot), rgb, (cv * a) / RS_COV_MAX)
596 }
597 }
598 gx = gx + 1
599 }
600 gy = gy + 1
601 }
602 pen = pen + rec[TF_R_ADV]
603 if pass == 1 { stats[RS_S_GLYPHS] = stats[RS_S_GLYPHS] + 1 }
604 }
605 i = i + 1
606 }
607 pass = pass + 1
608 }
609 }
610 stats[RS_S_INK] = stats[RS_S_INK] + ink
611 return ink
612}
613func rs_upper(s: *u8) -> i64 { var i: i64 = 0; while s[i] != (0 as u8) { let c: i64 = s[i] as i64; if c >= RS_CH_a { if c <= RS_CH_z { s[i] = (c - RS_CASE_GAP) as u8 } } i = i + 1 } return 0 }
614
615// ---- the tag walker: the figure between s0 (at "<svg") and s1 (at "</svg>") ----
616// the element name at h[p] where h[p] == '<': copies it into name (NUL-terminated), returns its length (0 = not a tag)
617func rs_tag_name(h: *u8, p: i64, to: i64, name: *u8, cap: i64) -> i64 {
618 var q: i64 = p + 1
619 var k: i64 = 0
620 if q < to { if (h[q] as i64) == RS_CH_SLASH { name[k] = h[q]; k = k + 1; q = q + 1 } }
621 while q < to { let c: i64 = h[q] as i64; if rs_is_ident(c) == 1 { if c != RS_CH_MINUS { if k < cap - 1 { name[k] = h[q]; k = k + 1 } q = q + 1 } else { q = to + q } } else { q = to + q } }
622 name[k] = 0 as u8
623 return k
624}
625func rs_skip_past(h: *u8, p: i64, to: i64, closer: *u8) -> i64 { let e: i64 = cr_find(h, p, to, closer); if e == CR_ABSENT { return to } return e + rs_slen(closer) }
626// the viewBox width and height (canvas px) into wh; 1 read, 0 defaulted, RS_ABSENT unreadable or out of bounds
627func rs_viewbox(h: *u8, s0: i64, s1: i64, wh: *i64) -> i64 {
628 wh[0] = RS_VB_W_DEFAULT; wh[1] = RS_VB_H_DEFAULT
629 let t1: i64 = cr_tag_end(h, s0, s1)
630 if t1 == CR_ABSENT { return RS_ABSENT }
631 let key: *u8 = " viewBox='" as *u8
632 let vb: i64 = cr_find(h, s0, t1, key)
633 if vb == CR_ABSENT { return 0 }
634 var p: i64 = vb + rs_slen(key)
635 var k: i64 = 0
636 let v: *i64 = rs_i64(4)
637 while k < 4 {
638 while p < t1 { if (h[p] as i64) == RS_CH_SPACE { p = p + 1 } else { p = t1 + p } }
639 if p > t1 { p = p - t1 }
640 v[k] = cr_int_at(h, p, t1)
641 if v[k] == CR_ABSENT { return RS_ABSENT }
642 if (h[p] as i64) == RS_CH_MINUS { p = p + 1 }
643 while p < t1 { let c: i64 = h[p] as i64; if c >= RS_CH_0 { if c <= RS_CH_9 { p = p + 1 } else { p = t1 + p } } else { p = t1 + p } }
644 if p > t1 { p = p - t1 }
645 k = k + 1
646 }
647 if v[2] <= 0 { return RS_ABSENT }
648 if v[3] <= 0 { return RS_ABSENT }
649 if v[2] > RS_CANVAS_MAX { return RS_ABSENT }
650 if v[3] > RS_CANVAS_MAX { return RS_ABSENT }
651 wh[0] = v[2]; wh[1] = v[3]
652 sys_munmap(v as *u8, 4 * RS_I64_BYTES)
653 return 1
654}
655// render the figure into fb (W x H = canvas * scale). fg/bg from the tokens; fhr/fhb the regular and bold faces.
656func rs_render(h: *u8, s0: i64, s1: i64, names: *u8, recs: *i64, ntok: i64, fhr: *i64, fhb: *i64, scale: i64, fb: *i64, W: i64, H: i64, stats: *i64) -> i64 {
657 let fg: *i64 = rs_i64(3)
658 let bg: *i64 = rs_i64(3)
659 let rgb: *i64 = rs_i64(3)
660 let rgb2: *i64 = rs_i64(3)
661 let present: *i64 = rs_i64(1)
662 let dash: *i64 = rs_i64(2)
663 let xs: *i64 = rs_i64(RS_PTS_MAX)
664 let ys: *i64 = rs_i64(RS_PTS_MAX)
665 let name: *u8 = sys_mmap(RS_VAL_W)
666 let val: *u8 = sys_mmap(RS_VAL_W)
667 let txt: *u8 = sys_mmap(RS_TEXT_W)
668 if rs_tok_rgb(names, recs, ntok, "fg" as *u8, fg) == 0 { fg[0] = 0; fg[1] = 0; fg[2] = 0 }
669 if rs_tok_rgb(names, recs, ntok, "bg" as *u8, bg) == 0 { bg[0] = RS_COV_MAX; bg[1] = RS_COV_MAX; bg[2] = RS_COV_MAX }
670 rs_fill_all(fb, W, H, bg)
671 var p: i64 = s0
672 var go: i64 = 1
673 while go == 1 {
674 if p >= s1 { go = 0 } else {
675 if (h[p] as i64) != RS_CH_LT { p = p + 1 } else {
676 let nl: i64 = rs_tag_name(h, p, s1, name, RS_VAL_W)
677 let t1: i64 = cr_tag_end(h, p, s1)
678 if t1 == CR_ABSENT { go = 0 } else {
679 let t0: i64 = p
680 p = t1 + 1
681 if nl == 0 { stats[RS_S_SKIPPED] = stats[RS_S_SKIPPED] + 1 }
682 else { if cr_streq(name, "title" as *u8) == 1 { p = rs_skip_past(h, t1, s1, RS_TAG_TITLE_CLOSE) }
683 else { if cr_streq(name, "desc" as *u8) == 1 { p = rs_skip_past(h, t1, s1, RS_TAG_DESC_CLOSE) }
684 else { if cr_streq(name, "defs" as *u8) == 1 { p = rs_skip_past(h, t1, s1, RS_TAG_DEFS_CLOSE) }
685 else { if cr_streq(name, "style" as *u8) == 1 { p = rs_skip_past(h, t1, s1, RS_TAG_STYLE_CLOSE) }
686 else { if cr_streq(name, "svg" as *u8) == 1 { p = t1 + 1 }
687 else { if cr_streq(name, "g" as *u8) == 1 { if cr_find(h, t0, t1, RS_NOTE_CLASS) != CR_ABSENT { stats[RS_S_NOTES] = stats[RS_S_NOTES] + 1 } }
688 else { if (name[0] as i64) == RS_CH_SLASH { p = t1 + 1 }
689 else { if cr_streq(name, "rect" as *u8) == 1 {
690 stats[RS_S_RECT] = stats[RS_S_RECT] + 1
691 let x: i64 = cr_attr(h, t0, t1, " x='" as *u8) * scale
692 let y: i64 = cr_attr(h, t0, t1, " y='" as *u8) * scale
693 let wd: i64 = cr_attr(h, t0, t1, " width='" as *u8) * scale
694 let ht: i64 = cr_attr(h, t0, t1, " height='" as *u8) * scale
695 let fa: i64 = rs_attr_permil(h, t0, t1, " fill-opacity='" as *u8)
696 if rs_attr_color(h, t0, t1, " fill='" as *u8, names, recs, ntok, fg, rgb, stats, present) != 0 { rs_fill_rect(fb, W, H, x, y, wd, ht, rgb, fa) }
697 if rs_attr_color(h, t0, t1, " stroke='" as *u8, names, recs, ntok, fg, rgb2, stats, present) != 0 {
698 var sw: i64 = cr_attr(h, t0, t1, " stroke-width='" as *u8)
699 if sw == CR_ABSENT { sw = RS_STROKE_DEFAULT }
700 sw = sw * scale
701 rs_attr_dash(h, t0, t1, scale, dash)
702 var ph0: i64 = rs_seg(fb, W, H, x, y, x + wd, y, sw, rgb2, RS_PERMIL, dash, 0)
703 ph0 = rs_seg(fb, W, H, x + wd, y, x + wd, y + ht, sw, rgb2, RS_PERMIL, dash, ph0)
704 ph0 = rs_seg(fb, W, H, x + wd, y + ht, x, y + ht, sw, rgb2, RS_PERMIL, dash, ph0)
705 rs_seg(fb, W, H, x, y + ht, x, y, sw, rgb2, RS_PERMIL, dash, ph0)
706 }
707 }
708 else { if cr_streq(name, "circle" as *u8) == 1 {
709 stats[RS_S_CIRCLE] = stats[RS_S_CIRCLE] + 1
710 let cx: i64 = cr_attr(h, t0, t1, " cx='" as *u8) * scale
711 let cy: i64 = cr_attr(h, t0, t1, " cy='" as *u8) * scale
712 let r: i64 = cr_attr(h, t0, t1, " r='" as *u8) * scale
713 let fa: i64 = rs_attr_permil(h, t0, t1, " fill-opacity='" as *u8)
714 if rs_attr_color(h, t0, t1, " fill='" as *u8, names, recs, ntok, fg, rgb, stats, present) != 0 { rs_fill_disk(fb, W, H, cx, cy, r, rgb, fa) }
715 if rs_attr_color(h, t0, t1, " stroke='" as *u8, names, recs, ntok, fg, rgb2, stats, present) != 0 {
716 var sw: i64 = cr_attr(h, t0, t1, " stroke-width='" as *u8)
717 if sw == CR_ABSENT { sw = RS_STROKE_DEFAULT }
718 rs_ring(fb, W, H, cx, cy, r, sw * scale, rgb2, RS_PERMIL)
719 }
720 // a <title> child and the </circle> closer follow: the walker skips both as tags of their own
721 }
722 else { if cr_streq(name, "line" as *u8) == 1 {
723 stats[RS_S_LINE] = stats[RS_S_LINE] + 1
724 let x1: i64 = cr_attr(h, t0, t1, " x1='" as *u8) * scale
725 let y1: i64 = cr_attr(h, t0, t1, " y1='" as *u8) * scale
726 let x2: i64 = cr_attr(h, t0, t1, " x2='" as *u8) * scale
727 let y2: i64 = cr_attr(h, t0, t1, " y2='" as *u8) * scale
728 if rs_attr_color(h, t0, t1, " stroke='" as *u8, names, recs, ntok, fg, rgb, stats, present) != 0 {
729 var sw: i64 = cr_attr(h, t0, t1, " stroke-width='" as *u8)
730 if sw == CR_ABSENT { sw = RS_STROKE_DEFAULT }
731 rs_attr_dash(h, t0, t1, scale, dash)
732 rs_seg(fb, W, H, x1, y1, x2, y2, sw * scale, rgb, RS_PERMIL, dash, 0)
733 }
734 }
735 else { if cr_streq(name, "polyline" as *u8) == 1 {
736 stats[RS_S_POLYLINE] = stats[RS_S_POLYLINE] + 1
737 let np: i64 = rs_attr_points(h, t0, t1, scale, xs, ys)
738 if rs_attr_color(h, t0, t1, " stroke='" as *u8, names, recs, ntok, fg, rgb, stats, present) != 0 {
739 var sw: i64 = cr_attr(h, t0, t1, " stroke-width='" as *u8)
740 if sw == CR_ABSENT { sw = RS_STROKE_DEFAULT }
741 rs_attr_dash(h, t0, t1, scale, dash)
742 var ph1: i64 = 0
743 var k: i64 = 1
744 while k < np { ph1 = rs_seg(fb, W, H, xs[k - 1], ys[k - 1], xs[k], ys[k], sw * scale, rgb, RS_PERMIL, dash, ph1); k = k + 1 }
745 if np >= RS_TWO { if cr_find(h, t0, t1, " marker-end='" as *u8) != CR_ABSENT { rs_arrow(fb, W, H, xs[np - RS_TWO], ys[np - RS_TWO], xs[np - 1], ys[np - 1], scale, rgb, RS_PERMIL); stats[RS_S_ARROWS] = stats[RS_S_ARROWS] + 1 } }
746 }
747 }
748 else { if cr_streq(name, "polygon" as *u8) == 1 {
749 stats[RS_S_POLYGON] = stats[RS_S_POLYGON] + 1
750 let np: i64 = rs_attr_points(h, t0, t1, scale, xs, ys)
751 let fa: i64 = rs_attr_permil(h, t0, t1, " fill-opacity='" as *u8)
752 if rs_attr_color(h, t0, t1, " fill='" as *u8, names, recs, ntok, fg, rgb, stats, present) != 0 { rs_fill_poly(fb, W, H, xs, ys, np, rgb, fa) }
753 if rs_attr_color(h, t0, t1, " stroke='" as *u8, names, recs, ntok, fg, rgb2, stats, present) != 0 {
754 var sw: i64 = cr_attr(h, t0, t1, " stroke-width='" as *u8)
755 if sw == CR_ABSENT { sw = RS_STROKE_DEFAULT }
756 rs_attr_dash(h, t0, t1, scale, dash)
757 var ph2: i64 = 0
758 var k: i64 = 0
759 while k < np { let j: i64 = (k + 1) % np; ph2 = rs_seg(fb, W, H, xs[k], ys[k], xs[j], ys[j], sw * scale, rgb2, RS_PERMIL, dash, ph2); k = k + 1 }
760 }
761 }
762 else { if cr_streq(name, "text" as *u8) == 1 {
763 stats[RS_S_TEXT] = stats[RS_S_TEXT] + 1
764 let te: i64 = cr_find(h, t1, s1, RS_TAG_TEXT_CLOSE)
765 var tend: i64 = s1
766 if te != CR_ABSENT { tend = te }
767 rs_unescape(h, t1 + 1, tend, txt, RS_TEXT_W)
768 let x: i64 = cr_attr(h, t0, t1, " x='" as *u8) * scale
769 let y: i64 = cr_attr(h, t0, t1, " y='" as *u8) * scale
770 var fs: i64 = cr_attr(h, t0, t1, " font-size='" as *u8)
771 if fs == CR_ABSENT { fs = 12 }
772 var fh: *i64 = fhr
773 if cr_find(h, t0, t1, " font-weight='700'" as *u8) != CR_ABSENT { if (fhb as i64) != 0 { fh = fhb } }
774 var anchor: i64 = 0
775 if cr_find(h, t0, t1, "text-anchor='middle'" as *u8) != CR_ABSENT { anchor = 1 }
776 if cr_find(h, t0, t1, "text-anchor='end'" as *u8) != CR_ABSENT { anchor = 2 }
777 if cr_find(h, t0, t1, "text-transform:uppercase" as *u8) != CR_ABSENT { rs_upper(txt) }
778 let op: i64 = rs_attr_permil(h, t0, t1, " opacity='" as *u8)
779 var halo: i64 = 0
780 if cr_find(h, t0, t1, "paint-order:stroke" as *u8) != CR_ABSENT { halo = 1 }
781 var rot: i64 = 0
782 var rcx: i64 = 0
783 var rcy: i64 = 0
784 var drawable: i64 = 1
785 let rk: *u8 = " transform='rotate(" as *u8
786 let ra: i64 = cr_find(h, t0, t1, rk)
787 if ra != CR_ABSENT {
788 var q: i64 = ra + rs_slen(rk)
789 let ang: i64 = cr_int_at(h, q, t1)
790 if (h[q] as i64) == RS_CH_MINUS { q = q + 1 }
791 while q < t1 { let c: i64 = h[q] as i64; if c >= RS_CH_0 { if c <= RS_CH_9 { q = q + 1 } else { q = t1 + q } } else { q = t1 + q } }
792 if q > t1 { q = q - t1 }
793 while q < t1 { if (h[q] as i64) == RS_CH_SPACE { q = q + 1 } else { q = t1 + q } }
794 if q > t1 { q = q - t1 }
795 rcx = cr_int_at(h, q, t1) * scale
796 if (h[q] as i64) == RS_CH_MINUS { q = q + 1 }
797 while q < t1 { let c: i64 = h[q] as i64; if c >= RS_CH_0 { if c <= RS_CH_9 { q = q + 1 } else { q = t1 + q } } else { q = t1 + q } }
798 if q > t1 { q = q - t1 }
799 while q < t1 { if (h[q] as i64) == RS_CH_SPACE { q = q + 1 } else { q = t1 + q } }
800 if q > t1 { q = q - t1 }
801 rcy = cr_int_at(h, q, t1) * scale
802 if ang == RS_DEG_ROT { rot = 1; stats[RS_S_ROTATED] = stats[RS_S_ROTATED] + 1 } else { drawable = 0; stats[RS_S_ROTSKIP] = stats[RS_S_ROTSKIP] + 1 }
803 }
804 if drawable == 1 {
805 let fr: i64 = rs_attr_color(h, t0, t1, " fill='" as *u8, names, recs, ntok, fg, rgb, stats, present)
806 if fr == 0 { if present[0] == 0 { rgb[0] = fg[0]; rgb[1] = fg[1]; rgb[2] = fg[2] } }
807 if fr != 0 { rs_text(fb, W, H, fh, txt, fs * scale, x, y, anchor, rgb, op, halo, bg, rot, rcx, rcy, scale, stats) }
808 else { if present[0] == 0 { rs_text(fb, W, H, fh, txt, fs * scale, x, y, anchor, rgb, op, halo, bg, rot, rcx, rcy, scale, stats) } }
809 }
810 p = tend + rs_slen(RS_TAG_TEXT_CLOSE)
811 }
812 else { stats[RS_S_SKIPPED] = stats[RS_S_SKIPPED] + 1 } } } } } } } } } } } } } }
813 }
814 }
815 }
816 }
817 sys_munmap(name, RS_VAL_W); sys_munmap(val, RS_VAL_W); sys_munmap(txt, RS_TEXT_W)
818 return 0
819}
820
821// ---- the standalone SVG twin ----
822// write the figure with the namespace declared and every colour variable resolved; returns bytes written or RS_E_SVG
823func rs_svg_standalone(h: *u8, s0: i64, s1: i64, names: *u8, recs: *i64, ntok: i64, path: *u8) -> i64 {
824 let fd: i64 = sys_openat_wr(path, RS_MODE_0644)
825 if fd < 0 { return RS_E_SVG }
826 var total: i64 = 0
827 let head: *u8 = RS_SVG_HEAD
828 total = total + sys_write(fd, head, rs_slen(head))
829 let svg_open: i64 = rs_slen("<svg" as *u8)
830 total = total + sys_write(fd, ((h as i64) + s0) as *u8, svg_open)
831 let ns: *u8 = RS_SVG_NS
832 total = total + sys_write(fd, ns, rs_slen(ns))
833 let end: i64 = s1 + rs_slen(RS_SVG_CLOSE)
834 let fg: *i64 = rs_i64(3)
835 if rs_tok_rgb(names, recs, ntok, "fg" as *u8, fg) == 0 { fg[0] = 0; fg[1] = 0; fg[2] = 0 }
836 let num: *u8 = sys_mmap(RS_VAL_W)
837 var p: i64 = s0 + svg_open
838 var run0: i64 = p
839 let vo: i64 = rs_slen(RS_VAR_OPEN)
840 // one pass over the figure: colour variables become rgb literals, named HTML entities become numeric references
841 while p < end {
842 let c: i64 = h[p] as i64
843 var consumed: i64 = 0
844 if c == RS_CH_AMP {
845 let e: i64 = rs_ent_at(h, p, end)
846 if e != RS_ABSENT {
847 total = total + sys_write(fd, ((h as i64) + run0) as *u8, p - run0)
848 let xs: *u8 = rs_ent_xml(e)
849 total = total + sys_write(fd, xs, rs_slen(xs))
850 p = p + rs_slen(rs_ent_name(e))
851 run0 = p
852 consumed = 1
853 }
854 }
855 if consumed == 0 { if cr_find(h, p, rs_min(end, p + vo), RS_VAR_OPEN) == p {
856 var e2: i64 = p + vo
857 while e2 < end { if (h[e2] as i64) == RS_CH_RPAREN { e2 = end + e2 } else { e2 = e2 + 1 } }
858 if e2 > end { e2 = e2 - end }
859 let i: i64 = rs_tok_find(names, ntok, ((h as i64) + p + vo) as *u8, e2 - p - vo)
860 if i != RS_ABSENT {
861 total = total + sys_write(fd, ((h as i64) + run0) as *u8, p - run0)
862 let rec: *i64 = rs_tok_rec(recs, i)
863 var o: i64 = 0
864 let rk: *u8 = RS_RGB_OPEN
865 var k: i64 = 0
866 while rk[k] != (0 as u8) { num[o] = rk[k]; o = o + 1; k = k + 1 }
867 o = rs_dec_into(num, o, rec[RS_TOK_R]); num[o] = RS_CH_COMMA as u8; o = o + 1
868 o = rs_dec_into(num, o, rec[RS_TOK_G]); num[o] = RS_CH_COMMA as u8; o = o + 1
869 o = rs_dec_into(num, o, rec[RS_TOK_B]); num[o] = RS_CH_RPAREN as u8; o = o + 1
870 total = total + sys_write(fd, num, o)
871 p = e2 + 1
872 run0 = p
873 consumed = 1
874 }
875 } }
876 if consumed == 0 { p = p + 1 }
877 }
878 total = total + sys_write(fd, ((h as i64) + run0) as *u8, end - run0)
879 num[0] = RS_CH_NL as u8
880 total = total + sys_write(fd, num, 1)
881 sys_close(fd)
882 sys_munmap(num, RS_VAL_W)
883 return total
884}
885func rs_dec_into(dst: *u8, off: i64, v: i64) -> i64 {
886 var o: i64 = off
887 var x: i64 = v
888 if x < 0 { dst[o] = RS_CH_MINUS as u8; o = o + 1; x = 0 - x }
889 let t: *u8 = sys_mmap(24)
890 var n: i64 = 0
891 if x == 0 { t[0] = RS_CH_0 as u8; n = 1 }
892 while x > 0 { t[n] = (RS_CH_0 + x % RS_BASE10) as u8; x = x / RS_BASE10; n = n + 1 }
893 while n > 0 { n = n - 1; dst[o] = t[n]; o = o + 1 }
894 sys_munmap(t, 24)
895 return o
896}
897
898// ---- the one-call export: page -> cdmap.svg + cdmap.png ----
899// stats receives the render counts plus RS_S_W (the png width); svg_bytes/png_bytes land in out[0], out[1].
900// fonts: regular and bold TrueType paths (bold may be the empty string to reuse regular). Returns RS_OK or a named code.
901func rs_export(page: *u8, svg_path: *u8, png_path: *u8, freg: *u8, fbold: *u8, scale: i64, stats: *i64, out: *i64) -> i64 {
902 out[0] = 0; out[1] = 0
903 var k: i64 = 0
904 while k < RS_S_SLOTS { stats[k] = 0; k = k + 1 }
905 if scale < 1 { return RS_E_SCALE }
906 if scale > RS_SCALE_MAX { return RS_E_SCALE }
907 let nb: *i64 = rs_i64(2)
908 nb[0] = 0
909 let h: *u8 = sys_read_file(page, nb)
910 let n: i64 = nb[0]
911 if (h as i64) == 0 { return RS_E_NOPAGE }
912 if n <= 0 { return RS_E_NOPAGE }
913 let s0: i64 = cr_find(h, 0, n, RS_SVG_OPEN)
914 if s0 == CR_ABSENT { return RS_E_NOSVG }
915 let s1: i64 = cr_find(h, s0, n, RS_SVG_CLOSE)
916 if s1 == CR_ABSENT { return RS_E_NOSVG }
917 let names: *u8 = sys_mmap(RS_TOK_MAX * RS_TOK_NAME_W)
918 let recs: *i64 = rs_i64(RS_TOK_MAX * RS_TOK_REC)
919 let ntok: i64 = rs_theme_load(h, n, names, recs)
920 stats[RS_S_TOKENS] = ntok
921 if ntok == 0 { return RS_E_NOTOKENS }
922 let wh: *i64 = rs_i64(2)
923 if rs_viewbox(h, s0, s1, wh) == RS_ABSENT { return RS_E_CANVAS }
924 let fhr: *i64 = tf_load(freg)
925 if (fhr as i64) == 0 { return RS_E_NOFONT }
926 var fhb: *i64 = fhr
927 if fbold[0] != (0 as u8) { fhb = tf_load(fbold); if (fhb as i64) == 0 { fhb = fhr } }
928 let W: i64 = wh[0] * scale
929 let H: i64 = wh[1] * scale
930 let fb: *i64 = rs_i64(W * H)
931 rs_render(h, s0, s1, names, recs, ntok, fhr, fhb, scale, fb, W, H, stats)
932 stats[RS_S_W] = W
933 let sb: i64 = rs_svg_standalone(h, s0, s1, names, recs, ntok, svg_path)
934 if sb < 0 { return RS_E_SVG }
935 out[0] = sb
936 let pb: i64 = png_publish(fb, W, H, png_path, RS_STAMP)
937 if pb <= 0 { return RS_E_PNG }
938 out[1] = pb
939 return RS_OK
940}