code wiki / _hdl_build / nx_render_html.nx
nx_render_html.nx source
↩ module page · 767 lines · 40769 B
1// nx_render_html.nx -- REUSABLE: render an HTML byte buffer to a styled 24-bit BMP. The full
2// styled pipeline (extracted from nx_render_wiki_styled so the file-driver AND nx_browse compose it,
3// not duplicate it): combined [HTML][UA-CSS] buffer -> nx_layout_from_dom -> box-indexed CssElement[]
4// -> nx_css_apply -> font-size/spacing-aware layout -> HTML-entity decode -> inherited-color +
5// font-size-scaled paint -> 24-bit BMP. UA sheet = heading bars + box-model spacing + blue links +
6// font-size hierarchy. 100% sovereign (no gcc/V8/Chrome). This organ has NO main (it's a library).
7import "nx_syscalls.nx"
8import "nx_css_vars.nx" // native var(--token) resolution (fleet --nx-color-* design tokens) -- 3rd-party-free
9import "nx_css_tokenize.nx"
10import "nx_css_parse.nx"
11import "nx_css_apply.nx"
12import "nx_css_color_decode.nx"
13import "nx_css_selector_match.nx"
14import "nx_html_tokenizer.nx"
15import "nx_html_entities.nx"
16import "nx_dom_query.nx"
17import "nx_layout_box.nx"
18import "nx_layout_default_display.nx"
19import "nx_layout_from_dom.nx"
20import "nx_layout_block.nx"
21import "nx_paint_solid_rect.nx"
22import "nx_paint_text.nx"
23import "nx_font_bitmap_5x7.nx"
24import "nx_paint_walk_layout.nx"
25import "nx_png_write.nx"
26const K_MAGIC_4096: i64 = 4096
27const K_MAGIC_1000000: i64 = 1000000
28const K_MAGIC_262144: i64 = 262144
29const K_MAGIC_16384: i64 = 16384
30const K_MAGIC_8192: i64 = 8192
31const K_MAGIC_32768: i64 = 32768
32const K_MAGIC_5000: i64 = 5000
33
34func rh_slen(s: *u8) -> i64 { var k: i64=0; while s[k]!=(0 as u8){k=k+1} return k }
35func rh_u32(fd: i64, v: i64) -> i64 { let b: *u8=sys_mmap(4); b[0]=(v&255) as u8; b[1]=((v>>8)&255) as u8; b[2]=((v>>16)&255) as u8; b[3]=((v>>24)&255) as u8; sys_write(fd,b,4); return 0 }
36func rh_u16(fd: i64, v: i64) -> i64 { let b: *u8=sys_mmap(2); b[0]=(v&255) as u8; b[1]=((v>>8)&255) as u8; sys_write(fd,b,2); return 0 }
37func rh_save(path: *u8, fb: *Framebuffer, w: i64, y0: i64, y1: i64) -> i64 {
38 let h: i64 = y1 - y0
39 let row3: i64=w*3; let pad: i64=(4-(row3%4))%4; let rowstride: i64=row3+pad; let datasize: i64=rowstride*h
40 let fd: i64=sys_openat_wr(path,420)
41 if fd<0 { return 0-1 }
42 sys_write(fd,"BM\x00" as *u8,2); rh_u32(fd,54+datasize); rh_u32(fd,0); rh_u32(fd,54)
43 rh_u32(fd,40); rh_u32(fd,w); rh_u32(fd,h); rh_u16(fd,1); rh_u16(fd,24); rh_u32(fd,0); rh_u32(fd,datasize); rh_u32(fd,0); rh_u32(fd,0); rh_u32(fd,0); rh_u32(fd,0)
44 let rowbuf: *u8=sys_mmap(rowstride+4); let px: *CssColor=(sys_mmap(NX_CSS_COLOR_BYTES as nx_size)) as *CssColor
45 var y: i64=y1-1
46 while y>=y0 {
47 var x: i64=0
48 while x<w {
49 nx_framebuffer_get_pixel(fb,x,y,px)
50 if px.a==0 { rowbuf[x*3]=255 as u8; rowbuf[x*3+1]=255 as u8; rowbuf[x*3+2]=255 as u8 }
51 else { rowbuf[x*3]=px.b as u8; rowbuf[x*3+1]=px.g as u8; rowbuf[x*3+2]=px.r as u8 }
52 x=x+1
53 }
54 var p: i64=0
55 while p<pad { rowbuf[row3+p]=0 as u8; p=p+1 }
56 sys_write(fd,rowbuf,rowstride); y=y-1
57 }
58 sys_close(fd); return 0
59}
60func rh_extract(html: *u8, hlen: i64, elements: *CssElement, maxn: i64) -> i64 {
61 let c: *HtmlCursor=sys_mmap(24) as *HtmlCursor; nx_html_cursor_init(c,html,hlen)
62 let tok: *HtmlToken=sys_mmap(56) as *HtmlToken
63 let ao: *i64=sys_mmap(8) as *i64; let al: *i64=sys_mmap(8) as *i64; let bo: *i64=sys_mmap(8) as *i64; let bl: *i64=sys_mmap(8) as *i64
64 var n: i64=0
65 var cskip: i64=0
66 while 1==1 {
67 nx_html_next_token(c,tok)
68 if tok.kind==NX_HTML_TOK_EOF { return n }
69 if n>=maxn { return n }
70 var ie: i64=0
71 // mirror nx_layout_from_dom's DEPTH-counted <nav>/<aside>/role=navigation chrome-skip EXACTLY
72 // so element order == box order (else the box-indexed cascade + <main> detection misalign).
73 if cskip > 0 {
74 if tok.kind==NX_HTML_TOK_START_TAG { cskip = cskip + 1 }
75 if tok.kind==NX_HTML_TOK_END_TAG { cskip = cskip - 1 }
76 } else {
77 if tok.kind==NX_HTML_TOK_START_TAG {
78 if _lfd_is_chrome_start(html, tok.name_off, tok.name_len, tok.src_off, tok.src_len) == 1 { cskip = 1 }
79 else { ie = 1 }
80 }
81 if tok.kind==NX_HTML_TOK_SELF_CLOSING { ie = 1 }
82 }
83 if ie==1 {
84 let e: *CssElement=((elements as i64)+n*NX_CSS_ELEMENT_BYTES) as *CssElement
85 e.src=html; e.tag_off=tok.name_off; e.tag_len=tok.name_len
86 if nx_dom_find_attr(html,tok.src_off,tok.src_len,"id\x00" as *u8,ao,al)==1 { e.id_off=ao[0]; e.id_len=al[0] } else { e.id_off=0; e.id_len=0 }
87 if nx_dom_find_attr(html,tok.src_off,tok.src_len,"class\x00" as *u8,bo,bl)==1 { e.class_off=bo[0]; e.class_len=bl[0] } else { e.class_off=0; e.class_len=0 }
88 n=n+1
89 }
90 }
91 return n
92}
93func rh_is_elem(k: i64) -> i64 { if k==NX_LAYOUT_BOX_BLOCK {return 1} if k==NX_LAYOUT_BOX_INLINE {return 1} if k==NX_LAYOUT_BOX_INLINE_BLOCK {return 1} return 0 }
94func rh_digit(c: i64) -> i64 { if c >= 48 { if c <= 57 { return 1 } } return 0 }
95// find NUL-terminated `pat` in src[off..end); offset or -1.
96func rh_find(src: *u8, off: i64, end: i64, pat: *u8) -> i64 {
97 var pl: i64 = 0
98 while pat[pl] != (0 as u8) { pl = pl + 1 }
99 if pl == 0 { return off }
100 var i: i64 = off
101 while i + pl <= end {
102 var j: i64 = 0
103 var ok: i64 = 1
104 while j < pl { if (src[i+j]&0xff) != (pat[j]&0xff) { ok = 0; j = pl } else { j = j + 1 } }
105 if ok == 1 { return i }
106 i = i + 1
107 }
108 return 0 - 1
109}
110// integer value of the first digit run at/after p (within end); -1 if none.
111func rh_num_after(src: *u8, p: i64, end: i64) -> i64 {
112 var i: i64 = p
113 var st: i64 = 0
114 while st == 0 { if i >= end { st = 2 } else { if rh_digit(src[i]&0xff)==1 { st = 1 } else { i = i + 1 } } }
115 if st == 2 { return 0 - 1 }
116 var v: i64 = 0
117 while i < end { if rh_digit(src[i]&0xff)==1 { v = v*10 + ((src[i]&0xff)-48); i = i + 1 } else { i = end } }
118 return v
119}
120// Does the @media condition src[off..end) match viewport width vw? Handles max-width/min-width
121// (the responsive breakpoints); unknown features default to MATCH (apply the rules).
122func rh_media_matches(src: *u8, off: i64, end: i64, vw: i64) -> i64 {
123 var result: i64 = 1
124 let mx: i64 = rh_find(src, off, end, "max-width\x00" as *u8)
125 if mx >= 0 { let n: i64 = rh_num_after(src, mx+9, end); if n >= 0 { if vw > n { result = 0 } } }
126 let mn: i64 = rh_find(src, off, end, "min-width\x00" as *u8)
127 if mn >= 0 { let n: i64 = rh_num_after(src, mn+9, end); if n >= 0 { if vw < n { result = 0 } } }
128 // ---- NON-WIDTH FEATURES (2026-07-27) ----------------------------------------------
129 // Everything used to fall through as MATCHED, because only the two width features were
130 // understood. That is fine for `screen`/`hover` but CATASTROPHIC for the two below, and
131 // it is why feeding real author CSS turned stackoverflow into a solid black rectangle:
132 // the site's `@media (prefers-color-scheme: dark)` block was flattened in unconditionally,
133 // so the page got the dark background while the text kept its light-theme dark colour --
134 // dark-on-dark, 100% dark pixels, OCR 0. MEASURED as a pixel histogram, not guessed.
135 // We render a LIGHT surface, so dark-scheme blocks must NOT match; `light` still does
136 // (result stays 1). Print stylesheets must not apply to a screen render either -- they
137 // routinely `display:none` the whole nav/chrome.
138 let pcs: i64 = rh_find(src, off, end, "prefers-color-scheme\x00" as *u8)
139 if pcs >= 0 { if rh_find(src, pcs, end, "dark\x00" as *u8) >= 0 { result = 0 } }
140 if rh_find(src, off, end, "print\x00" as *u8) >= 0 { result = 0 }
141 return result
142}
143// Filter CSS by viewport: keep `@media (cond){...}` blocks whose cond matches vw (flattened, wrapper
144// removed), drop non-matching ones; pass everything else through. So the parser only sees rules that
145// actually apply at our width -- the gate to applying a page's responsive CSS without conflicts.
146// @media-flatten to our viewport, THEN resolve var(--token) custom properties into `out` (so the native
147// engine renders the fleet's --nx-color-* design tokens; a var-less page expands byte-identical -> no gate
148// regression). Filtering lands in an internal scratch; cx_expand copies+substitutes into `out`.
149func rh_filter_media(src: *u8, off: i64, len: i64, out: *u8, out_cap: i64, vw: i64) -> i64 {
150 let scratch: *u8 = sys_mmap(out_cap + 16)
151 let end: i64 = off + len
152 var i: i64 = off
153 var o: i64 = 0
154 while i < end {
155 var is_media: i64 = 0
156 if (src[i]&0xff) == 64 { if i + 6 <= end { if rh_find(src, i, i+6, "@media\x00" as *u8) == i { is_media = 1 } } }
157 if is_media == 1 {
158 var brace: i64 = 0 - 1
159 var j: i64 = i + 6
160 while j < end { if (src[j]&0xff)==123 { brace = j; j = end } else { j = j + 1 } }
161 if brace < 0 { i = end }
162 else {
163 let matches: i64 = rh_media_matches(src, i+6, brace, vw)
164 var depth: i64 = 1
165 var k: i64 = brace + 1
166 var closeb: i64 = 0 - 1
167 while k < end { if closeb >= 0 { k = end } else {
168 let ck: i64 = src[k]&0xff
169 if ck == 123 { depth = depth + 1 }
170 if ck == 125 { depth = depth - 1; if depth == 0 { closeb = k } }
171 k = k + 1
172 } }
173 if closeb < 0 { i = end }
174 else {
175 if matches == 1 {
176 var m: i64 = brace + 1
177 while m < closeb { if o < out_cap - 1 { scratch[o] = src[m]; o = o + 1 } m = m + 1 }
178 }
179 i = closeb + 1
180 }
181 }
182 } else {
183 if o < out_cap - 1 { scratch[o] = src[i]; o = o + 1 }
184 i = i + 1
185 }
186 }
187 // resolve var(--token) then clamp() (fluid type) at THIS viewport -> concrete values the parser understands.
188 // Both are byte-identical no-ops when absent, so var/clamp-less pages (all existing gates) are unchanged.
189 let scratch2: *u8 = sys_mmap(out_cap + 16)
190 let ex: i64 = cx_expand(scratch, o, scratch2, out_cap)
191 return clamp_resolve(scratch2, ex, out, out_cap, vw)
192}
193// Extract the concatenated text of all inline <style> blocks (the page's own CSS) into `out`. So we
194// can parse + cascade the page's REAL styles (incl. display:none) alongside our UA reader stylesheet.
195// Copy CSS bytes html[off..off+len) into out[*op..], STRIPPING `<![CDATA[` / `]]>` markers. XHTML
196// stylesheets wrap their CSS in a CDATA section; those markers are not CSS and (left in) corrupt the first
197// rule's selector. Only the literal 9-byte open / 3-byte close sequences are removed (single ']' or '>'
198// from child/attr selectors are untouched).
199func _rh_copy_css(html: *u8, off: i64, len: i64, out: *u8, op: *i64, cap: i64) -> i64 {
200 var i: i64 = 0
201 while i < len {
202 var skip: i64 = 0
203 if (html[off+i]&0xff) == 60 { if i + 9 <= len {
204 if (html[off+i+1]&0xff)==33 { if (html[off+i+2]&0xff)==91 { if (html[off+i+3]&0xff)==67 { if (html[off+i+4]&0xff)==68 { if (html[off+i+5]&0xff)==65 { if (html[off+i+6]&0xff)==84 { if (html[off+i+7]&0xff)==65 { if (html[off+i+8]&0xff)==91 {
205 i = i + 9; skip = 1
206 } } } } } } } }
207 } }
208 if skip == 0 { if (html[off+i]&0xff) == 93 { if i + 3 <= len {
209 if (html[off+i+1]&0xff)==93 { if (html[off+i+2]&0xff)==62 { i = i + 3; skip = 1 } }
210 } } }
211 if skip == 0 {
212 let oo: i64 = op[0]
213 if oo < cap - 2 { out[oo] = html[off+i]; op[0] = oo + 1 }
214 i = i + 1
215 }
216 }
217 return 0
218}
219
220func rh_extract_styles(html: *u8, hlen: i64, out: *u8, out_cap: i64) -> i64 {
221 let c: *HtmlCursor = sys_mmap(24) as *HtmlCursor
222 nx_html_cursor_init(c, html, hlen)
223 let tok: *HtmlToken = sys_mmap(56) as *HtmlToken
224 let raw: *HtmlToken = sys_mmap(56) as *HtmlToken
225 let op: *i64 = sys_mmap(8) as *i64
226 op[0] = 0
227 var keep: i64 = 1
228 while keep == 1 {
229 nx_html_next_token(c, tok)
230 if tok.kind == NX_HTML_TOK_EOF { keep = 0 }
231 else {
232 if tok.kind == NX_HTML_TOK_START_TAG {
233 // Raw-text tags (style/script/textarea/title) need raw-body consumption so a '<' inside the
234 // body (notably `<![CDATA[`) isn't mis-tokenized as markup. We only KEEP <style> bodies.
235 if nx_html_is_raw_text_tag(html, tok.name_off, tok.name_len) == 1 {
236 nx_html_consume_raw_text(c, (html as i64 + tok.name_off) as *u8, tok.name_len, raw)
237 if _lfd_name_eq(html, tok.name_off, tok.name_len, "style\x00" as *u8) == 1 {
238 _rh_copy_css(html, raw.body_off, raw.body_len, out, op, out_cap)
239 if op[0] < out_cap - 2 { out[op[0]] = 10 as u8; op[0] = op[0] + 1 }
240 }
241 }
242 }
243 }
244 }
245 return op[0]
246}
247
248// ---- complex CSS selector matching (compound / descendant / child / comma-list) ----
249func _rh_namechar(c: i64) -> i64 {
250 if c >= 48 { if c <= 57 { return 1 } }
251 if c >= 65 { if c <= 90 { return 1 } }
252 if c >= 97 { if c <= 122 { return 1 } }
253 if c == 45 { return 1 }
254 if c == 95 { return 1 }
255 return 0
256}
257// End of the namechar run starting at s (the first non-namechar position, or end).
258func _rh_name_end(buf: *u8, s: i64, end: i64) -> i64 {
259 var e: i64 = s
260 var d: i64 = 0
261 while d == 0 { if e >= end { d = 1 } else { if _rh_namechar(buf[e]&0xff)==1 { e=e+1 } else { d=1 } } }
262 return e
263}
264// Match a COMPOUND selector buf[off..off+len) (adjacent simple selectors, no combinator) against el.
265// Handles tag / .class / #id / * ; SKIPS :pseudo and [attr] (we can't evaluate those -> ignored).
266func _rh_match_compound(buf: *u8, off: i64, len: i64, el: *CssElement) -> i64 {
267 let end: i64 = off + len
268 var i: i64 = off
269 var any: i64 = 0
270 while i < end {
271 let c: i64 = buf[i] & 0xff
272 if c == 46 {
273 let s: i64 = i + 1
274 let e: i64 = _rh_name_end(buf, s, end)
275 if _css_class_list_contains(buf, el.class_off, el.class_len, s, e - s) != 1 { return 0 }
276 any = 1; i = e
277 } else { if c == 35 {
278 let s: i64 = i + 1
279 let e: i64 = _rh_name_end(buf, s, end)
280 if _css_bytes_equal(buf, el.id_off, el.id_len, s, e - s) != 1 { return 0 }
281 any = 1; i = e
282 } else { if c == 42 {
283 any = 1; i = i + 1
284 } else { if c == 58 {
285 i = _rh_name_end(buf, i + 1, end)
286 } else { if c == 91 {
287 var jj: i64 = i + 1
288 var dn: i64 = 0
289 while dn == 0 { if jj >= end { dn = 1 } else { if (buf[jj]&0xff)==93 { jj=jj+1; dn=1 } else { jj=jj+1 } } }
290 i = jj
291 } else { if _rh_namechar(c) == 1 {
292 let s: i64 = i
293 let e: i64 = _rh_name_end(buf, s, end)
294 if _css_bytes_equal(buf, el.tag_off, el.tag_len, s, e - s) != 1 { return 0 }
295 any = 1; i = e
296 } else { i = i + 1 } } } } } }
297 }
298 return any
299}
300// Match a single (comma-free) selector against box_idx, walking ancestors for descendant/child combs.
301func _rh_match_one(buf: *u8, off: i64, len: i64, box_idx: i64, tree: *LayoutTree, boxel: *CssElement, coff: *i64, clen: *i64, ccmb: *i64) -> i64 {
302 // coff/clen/ccmb (>=32 i64 each) are caller-provided scratch -- HOISTED out of this fn: the cascade
303 // calls it O(n_boxes x n_rules) times and a per-call sys_mmap (no free) OOMs on real pages (a 422KB
304 // Wikipedia page x 8192 rules = ~134M calls x 768B). ccmb = combinator BEFORE this compound (1=desc 2=child).
305 var nc: i64 = 0
306 let end: i64 = off + len
307 var i: i64 = off
308 var pend: i64 = 0 // pending combinator for the next compound
309 while i < end {
310 let c: i64 = buf[i] & 0xff
311 if c == 32 { if pend == 0 { pend = 1 } i = i + 1 }
312 else { if c == 9 { if pend == 0 { pend = 1 } i = i + 1 }
313 else { if c == 62 { pend = 2; i = i + 1 }
314 else {
315 // start of a compound: read until ws / '>' / end
316 let s: i64 = i
317 var te: i64 = s
318 var done: i64 = 0
319 while done == 0 {
320 if te >= end { done = 1 }
321 else { let cc: i64 = buf[te]&0xff; if cc==32 { done=1 } else { if cc==9 { done=1 } else { if cc==62 { done=1 } else { te=te+1 } } } }
322 }
323 if nc < 32 { coff[nc]=s; clen[nc]=te - s; ccmb[nc]=pend; nc=nc+1 }
324 pend = 0
325 i = te
326 } } }
327 }
328 if nc == 0 { return 0 }
329 // match rightmost compound against the box
330 if _rh_match_compound(buf, coff[nc-1], clen[nc-1], ((boxel as i64)+box_idx*NX_CSS_ELEMENT_BYTES) as *CssElement) != 1 { return 0 }
331 var cur: i64 = box_idx
332 var ci: i64 = nc - 2
333 while ci >= 0 {
334 let comb: i64 = ccmb[ci+1]
335 if comb == 2 {
336 let cb: *LayoutBox = ((tree.boxes as i64)+cur*NX_LAYOUT_BOX_BYTES) as *LayoutBox
337 cur = cb.parent_idx
338 if cur < 0 { return 0 }
339 if _rh_match_compound(buf, coff[ci], clen[ci], ((boxel as i64)+cur*NX_CSS_ELEMENT_BYTES) as *CssElement) != 1 { return 0 }
340 } else {
341 var found: i64 = 0
342 let cb2: *LayoutBox = ((tree.boxes as i64)+cur*NX_LAYOUT_BOX_BYTES) as *LayoutBox
343 cur = cb2.parent_idx
344 var ag: i64 = 0
345 while found == 0 {
346 if cur < 0 { found = 2 }
347 else { if ag >= K_MAGIC_4096 { found = 2 }
348 else {
349 if _rh_match_compound(buf, coff[ci], clen[ci], ((boxel as i64)+cur*NX_CSS_ELEMENT_BYTES) as *CssElement) == 1 { found = 1 }
350 else { let cb3: *LayoutBox = ((tree.boxes as i64)+cur*NX_LAYOUT_BOX_BYTES) as *LayoutBox; cur = cb3.parent_idx; ag = ag + 1 }
351 }
352 }
353 }
354 if found != 1 { return 0 }
355 }
356 ci = ci - 1
357 }
358 return 1
359}
360// Match a full selector (possibly comma-separated) buf[off..off+len) against box_idx. 1 if ANY matches.
361func rh_match_complex(buf: *u8, off: i64, len: i64, box_idx: i64, tree: *LayoutTree, boxel: *CssElement, coff: *i64, clen: *i64, ccmb: *i64) -> i64 {
362 let end: i64 = off + len
363 var seg: i64 = off
364 var i: i64 = off
365 var matched: i64 = 0
366 var keep: i64 = 1
367 while keep == 1 {
368 var atend: i64 = 0
369 if i >= end { atend = 1 }
370 var iscomma: i64 = 0
371 if atend == 0 { if (buf[i]&0xff) == 44 { iscomma = 1 } }
372 if atend == 1 { if _rh_match_one(buf, seg, i - seg, box_idx, tree, boxel, coff, clen, ccmb) == 1 { matched = 1 } keep = 0 }
373 else { if iscomma == 1 { if _rh_match_one(buf, seg, i - seg, box_idx, tree, boxel, coff, clen, ccmb) == 1 { matched = 1 } seg = i + 1; i = i + 1 }
374 else { i = i + 1 } }
375 }
376 return matched
377}
378
379// Per-MEMBER specificity of a (possibly comma-separated) selector against box_idx: the MAX specificity
380// among the members that MATCH, or -1 when none does. MEASURED 2026-09-02 (nx_inline_vec_measure_gate
381// T15/T17): the whole-list count let a class member leak its specificity onto the type members of its
382// list, so wikipedia's `.mw-heading,h1,h2{font-weight:bold}` beat its own later `.mw-heading1,h1{font-
383// weight:normal}` and every title painted bold. CSS 2.1 6.4.3: each member is its own selector.
384func rh_match_complex_spec(buf: *u8, off: i64, len: i64, box_idx: i64, tree: *LayoutTree, boxel: *CssElement, coff: *i64, clen: *i64, ccmb: *i64) -> i64 {
385 let end: i64 = off + len
386 var seg: i64 = off
387 var i: i64 = off
388 var best: i64 = 0 - 1
389 var keep: i64 = 1
390 while keep == 1 {
391 var atend: i64 = 0
392 if i >= end { atend = 1 }
393 var iscomma: i64 = 0
394 if atend == 0 { if (buf[i]&0xff) == 44 { iscomma = 1 } }
395 if atend == 1 {
396 if _rh_match_one(buf, seg, i - seg, box_idx, tree, boxel, coff, clen, ccmb) == 1 { let sp: i64 = rh_specificity(buf, seg, i - seg); if sp > best { best = sp } }
397 keep = 0
398 } else { if iscomma == 1 {
399 if _rh_match_one(buf, seg, i - seg, box_idx, tree, boxel, coff, clen, ccmb) == 1 { let sp2: i64 = rh_specificity(buf, seg, i - seg); if sp2 > best { best = sp2 } }
400 seg = i + 1; i = i + 1
401 } else { i = i + 1 } }
402 }
403 return best
404}
405
406func rh_namechar2(c: i64) -> i64 {
407 if c>=48 { if c<=57 { return 1 } }
408 if c>=65 { if c<=90 { return 1 } }
409 if c>=97 { if c<=122 { return 1 } }
410 if c==45 { return 1 } // -
411 if c==95 { return 1 } // _
412 return 0
413}
414// CSS SPECIFICITY of selector buf[off..off+len) as a single comparable int: id*1e6 + (class+attr+pseudo)
415// *1e3 + type. (W3C cascade ยง6.4.3 a-b-c packed.) Approximate but correct for the common forms (tag,
416// .class, #id, p.class, .a .b, div>p). '*' = 0. Comma-lists use the whole-selector count (good enough;
417// per-matching-compound specificity is a later refinement).
418func rh_specificity(buf: *u8, off: i64, len: i64) -> i64 {
419 let end: i64 = off + len
420 var nid: i64 = 0
421 var ncls: i64 = 0
422 var ntype: i64 = 0
423 var i: i64 = off
424 while i < end {
425 let c: i64 = buf[i] & 0xff
426 if c == 35 { // '#' id
427 nid = nid + 1; i = i + 1
428 var s: i64 = 1
429 while s == 1 { if i < end { if rh_namechar2(buf[i]&0xff)==1 { i=i+1 } else { s=0 } } else { s=0 } }
430 } else { if c == 46 { // '.' class
431 ncls = ncls + 1; i = i + 1
432 var s2: i64 = 1
433 while s2 == 1 { if i < end { if rh_namechar2(buf[i]&0xff)==1 { i=i+1 } else { s2=0 } } else { s2=0 } }
434 } else { if c == 91 { // '[' attr
435 ncls = ncls + 1
436 var s3: i64 = 1
437 while s3 == 1 { if i < end { if (buf[i]&0xff)==93 { i=i+1; s3=0 } else { i=i+1 } } else { s3=0 } }
438 } else { if c == 58 { // ':' pseudo
439 ncls = ncls + 1; i = i + 1
440 if i < end { if (buf[i]&0xff)==58 { i=i+1 } }
441 var s4: i64 = 1
442 while s4 == 1 { if i < end { if rh_namechar2(buf[i]&0xff)==1 { i=i+1 } else { s4=0 } } else { s4=0 } }
443 } else { if c == 42 { // '*' universal -> 0
444 i = i + 1
445 } else { if rh_namechar2(c) == 1 { // bare type selector
446 ntype = ntype + 1
447 var s5: i64 = 1
448 while s5 == 1 { if i < end { if rh_namechar2(buf[i]&0xff)==1 { i=i+1 } else { s5=0 } } else { s5=0 } }
449 } else { // whitespace / > / + / ~ / , -> skip
450 i = i + 1
451 } } } } } }
452 }
453 return nid*K_MAGIC_1000000 + ncls*1000 + ntype
454}
455
456// Promote INLINE boxes that carry an explicit block-level `display` (flex/grid/block/table) to BLOCK kind, so
457// width/height apply (nx_layout_block block path) AND background/border paint (both gated on kind==BLOCK).
458// Box kind is otherwise assigned from the TAG name only (nx_layout_from_dom), ignoring CSS `display` -- so
459// `<span>` with `display:flex` (e.g. the office .fico kind-badges) never got a painted, sized box. SAFE BY
460// CONSTRUCTION: only promotes INLINE *up*, never demotes a BLOCK box -> pinned block-level layouts are
461// byte-identical; a page with NO display-overridden inline element is unchanged. Runs after cascade, before
462// layout. (inline-block/inline-flex intentionally NOT promoted here -- INLINE_BLOCK bg paint is a separate rung.)
463func rh_promote_display_kinds(tree: *LayoutTree, computed: *CssComputedDecl, ncomp: i64, src: *u8) -> i64 {
464 let dprop: *u8 = "display\x00" as *u8
465 var bi: i64 = 0
466 while bi < tree.count {
467 let b: *LayoutBox = ((tree.boxes as i64) + bi * NX_LAYOUT_BOX_BYTES) as *LayoutBox
468 if b.kind == NX_LAYOUT_BOX_INLINE {
469 var promote: i64 = 0
470 var j: i64 = ncomp - 1
471 while j >= 0 {
472 let cd: *CssComputedDecl = ((computed as i64) + j * NX_CSS_COMPUTED_DECL_BYTES) as *CssComputedDecl
473 if cd.element_idx == bi { if cd.prop_len == 7 {
474 var sp: i64 = 1
475 var k: i64 = 0
476 while k < 7 { if (src[cd.prop_off+k]&0xff) != (dprop[k]&0xff) { sp = 0; k = 7 } else { k = k + 1 } }
477 if sp == 1 {
478 let vl: i64 = cd.val_len
479 let v0: i64 = src[cd.val_off]&0xff
480 // Block-level & "blockified" display values -> promote INLINE box to BLOCK so it gets a
481 // sized box AND paints. last-wins: first match stops the reverse scan.
482 // vl==4: flex(f)/grid(g) -- NOT none(n)
483 // vl==5: block(b)/table(t)
484 // vl==11: inline-flex / inline-grid (buttons/pills: <a class=btn display:inline-flex>)
485 // vl==12: inline-block / inline-table (badges/tags/chips: <a class=chip display:inline-block>)
486 // inline-* are spec-inline-level -> INLINE_BLOCK (promote=2): they now flow ATOMICALLY on
487 // the line (padding/border/width honored + bg painted) instead of each taking a full-width
488 // block row. Fixes chip/tag/nav-pill lists (the atlas Family-Tree page = the driver).
489 if vl == 4 { if v0 == 102 { promote = 1 } if v0 == 103 { promote = 1 } }
490 if vl == 5 { if v0 == 98 { promote = 1 } if v0 == 116 { promote = 1 } }
491 if vl == 11 { promote = 2 }
492 if vl == 12 { promote = 2 }
493 j = 0
494 }
495 } }
496 j = j - 1
497 }
498 if promote == 1 { b.kind = NX_LAYOUT_BOX_BLOCK }
499 if promote == 2 { b.kind = NX_LAYOUT_BOX_INLINE_BLOCK }
500 }
501 bi = bi + 1
502 }
503 return 0
504}
505
506// Cascade using the COMPLEX matcher (compound/descendant/child) + the box tree (for ancestor walks).
507// Output = CssComputedDecl[] consumed by last-match-wins readers. Per box, matched rules are emitted in
508// ASCENDING-SPECIFICITY order (stable: ties keep source order), so the existing reverse-scan-last-wins
509// naturally yields SPECIFICITY-then-source-order -- correct CSS cascade -- with NO reader change.
510func rh_cascade(buf: *u8, rules: *CssRule, n_rules: i64, decls: *CssDeclaration, n_decls: i64,
511 boxel: *CssElement, n_boxes: i64, tree: *LayoutTree,
512 computed: *CssComputedDecl, max_computed: i64, out_count: *i64) -> i64 {
513 // selector-matcher scratch allocated ONCE (not per match call) -- the per-call mmap was the cascade's
514 // OOM on real pages. Reused across every box x rule. >=32 i64 each (the compound-selector cap).
515 let mc_coff: *i64 = sys_mmap(8*32) as *i64
516 let mc_clen: *i64 = sys_mmap(8*32) as *i64
517 let mc_ccmb: *i64 = sys_mmap(8*32) as *i64
518 // specificity is computed PER MATCHING MEMBER at match time (rh_match_complex_spec), never per rule:
519 // a precomputed whole-list count is the measured defect (2026-09-02, T15/T17 of the measure gate).
520 let matched: *i64 = sys_mmap(8 * (n_rules + 1)) as *i64
521 let mspec: *i64 = sys_mmap(8 * (n_rules + 1)) as *i64
522 var written: i64 = 0
523 var e: i64 = 0
524 while e < n_boxes {
525 // gather matched rule indices for this box, in source order
526 var nm: i64 = 0
527 var r: i64 = 0
528 while r < n_rules {
529 let rule: *CssRule = (rules as *u8 + (r as nx_size) * (NX_CSS_RULE_BYTES as nx_size)) as *CssRule
530 if rule.sel_full_len > 0 {
531 let msp: i64 = rh_match_complex_spec(buf, rule.sel_full_off, rule.sel_full_len, e, tree, boxel, mc_coff, mc_clen, mc_ccmb)
532 if msp >= 0 { matched[nm] = r; mspec[nm] = msp; nm = nm + 1 }
533 }
534 r = r + 1
535 }
536 // STABLE insertion sort matched[0..nm) by specificity ASCENDING (equal spec keeps source order):
537 // shift only strictly-greater, so the existing reverse-scan-last-wins yields specificity-then-source.
538 var a: i64 = 1
539 while a < nm {
540 let key: i64 = matched[a]
541 let ks: i64 = mspec[a]
542 var b: i64 = a - 1
543 var done: i64 = 0
544 while done == 0 {
545 if b < 0 { done = 1 }
546 else { if mspec[b] > ks { matched[b+1] = matched[b]; mspec[b+1] = mspec[b]; b = b - 1 } else { done = 1 } }
547 }
548 matched[b+1] = key
549 mspec[b+1] = ks
550 a = a + 1
551 }
552 // emit declarations in ascending-specificity order
553 var m: i64 = 0
554 while m < nm {
555 let rule: *CssRule = (rules as *u8 + (matched[m] as nx_size) * (NX_CSS_RULE_BYTES as nx_size)) as *CssRule
556 var d: i64 = 0
557 while d < rule.decl_count {
558 let sdi: i64 = rule.decl_first + d
559 if sdi >= n_decls { d = rule.decl_count }
560 else {
561 if written >= max_computed { out_count[0] = written; return 0 }
562 let sd: *CssDeclaration = (decls as *u8 + (sdi as nx_size) * (NX_CSS_DECLARATION_BYTES as nx_size)) as *CssDeclaration
563 let cd: *CssComputedDecl = (computed as *u8 + (written as nx_size) * (NX_CSS_COMPUTED_DECL_BYTES as nx_size)) as *CssComputedDecl
564 cd.element_idx = e
565 cd.prop_off = sd.prop_off; cd.prop_len = sd.prop_len
566 cd.val_kind = sd.val_kind; cd.val_off = sd.val_off; cd.val_len = sd.val_len
567 cd.unit_off = sd.unit_off; cd.unit_len = sd.unit_len
568 written = written + 1; d = d + 1
569 }
570 }
571 m = m + 1
572 }
573 e = e + 1
574 }
575 out_count[0] = written
576 return 1
577}
578
579// Render `raw` (hlen bytes of HTML) into a freshly-allocated framebuffer. out3 receives [VW, VH,
580// box_count]; returns the framebuffer (0 if no content). The render core; the to-bmp/to-png writers
581// below share it (one render path, two output formats).
582func _rh_to_fb(raw: *u8, hlen: i64, out3: *i64) -> *Framebuffer {
583 out3[0] = 0; out3[1] = 0; out3[2] = 0
584 if hlen <= 0 { return 0 as *Framebuffer }
585 // UA stylesheet: heading bars + box-model SPACING + BLUE LINKS + FONT-SIZE hierarchy. nx_layout_block
586 // honors margin/padding/font-size and nx_paint_walk_layout honors color/font-size from the cascade.
587 let css: *u8 = "h1{background-color:#a8c8ff;margin-top:10px;margin-bottom:4px;font-size:40px}h2{background-color:#d0d0d0;margin-top:10px;margin-bottom:3px;font-size:28px}h3{background-color:#e4e4e4;margin-top:6px;margin-bottom:2px;font-size:24px}body{background-color:#ffffff;padding-left:8px;padding-top:6px}p{margin-top:6px;margin-bottom:5px}ul{padding-left:16px;margin-top:3px;margin-bottom:3px}ol{padding-left:24px;margin-top:3px;margin-bottom:3px}li{margin-bottom:2px}tr{display:flex}th{font-weight:bold;padding-left:4px;padding-right:4px}td{padding-left:4px;padding-right:4px}a{color:#3366cc}b{font-weight:bold}strong{font-weight:bold}i{font-style:italic}em{font-style:italic}.vector-menu-content-list{display:none}.vector-dropdown{display:none}.mw-interlanguage-selector{display:none}.wbc-editpage{display:none}.infobox{float:right;width:300px;margin-left:8px;background-color:#f8f8f8}\x00"
588 let clen: i64 = rh_slen(css)
589 let STYCAP: i64 = K_MAGIC_262144
590 let buf: *u8 = sys_mmap(hlen + STYCAP + clen + 16)
591 var i: i64 = 0
592 while i < hlen { buf[i] = raw[i]; i = i + 1 }
593 // [HTML][page's own inline <style> CSS, @media-FILTERED to our viewport][UA reader stylesheet].
594 // Now that flex (R1) + @media filtering (R2) exist, apply the page's inline CSS but ONLY the rules
595 // matching our 800px width (rh_filter_media) -- so conflicting narrow/wide @media rules no longer
596 // all fire at once. (Linked load.php stylesheets, which hold the vector-menu chrome rules, are a
597 // later rung: fetch them too.)
598 // [HTML][page's @media-filtered inline CSS][UA reader stylesheet LAST]. UA goes LAST so our reader
599 // rules WIN the cascade (last-in-source-order) for the elements we target (chrome display:none, heading
600 // bars, blue links, tables, bold/italic, .infobox). Now SAFE: declaration-level parser recovery means
601 // a malformed page declaration no longer aborts the sheet, so UA-last is reached. (The first attempt at
602 // this broke because the page CSS aborted at a multi-value decl before UA parsed -- recovery fixed it.)
603 let rawsty: *u8 = sys_mmap(STYCAP)
604 let rawstylen: i64 = rh_extract_styles(raw, hlen, rawsty, STYCAP)
605 let stylen: i64 = rh_filter_media(rawsty, 0, rawstylen, ((buf as i64)+hlen) as *u8, STYCAP, 800)
606 i = 0
607 while i < clen { buf[hlen + stylen + i] = css[i]; i = i + 1 }
608 let total: i64 = hlen + stylen + clen
609
610 let VW: i64 = 800
611 let boxes: *LayoutBox = (sys_mmap(NX_LAYOUT_BOX_BYTES*K_MAGIC_16384+16)) as *LayoutBox
612 let tree: *LayoutTree = (sys_mmap(64)) as *LayoutTree
613 nx_layout_tree_init(tree, boxes, K_MAGIC_16384)
614 let stk: *LayoutFromDomStack = (sys_mmap(NX_LAYOUT_FROM_DOM_STACK_BYTES+8)) as *LayoutFromDomStack
615 let sidx: *i64 = (sys_mmap(8*128)) as *i64
616 nx_layout_from_dom_stack_init(stk, sidx, 128)
617 nx_layout_from_dom(buf, hlen, tree, stk)
618
619 let ext: *CssElement = (sys_mmap(NX_CSS_ELEMENT_BYTES*K_MAGIC_16384 as nx_size)) as *CssElement
620 let n_ext: i64 = rh_extract(buf, hlen, ext, K_MAGIC_16384)
621 let boxel: *CssElement = (sys_mmap((NX_CSS_ELEMENT_BYTES*(tree.count+1)) as nx_size)) as *CssElement
622 var k: i64 = 0
623 i = 0
624 while i < tree.count {
625 let b: *LayoutBox = ((tree.boxes as i64)+i*NX_LAYOUT_BOX_BYTES) as *LayoutBox
626 let e: *CssElement = ((boxel as i64)+i*NX_CSS_ELEMENT_BYTES) as *CssElement
627 e.src = buf
628 var mapped: i64 = 0
629 if rh_is_elem(b.kind)==1 { if b.parent_idx != (0-1) { if k < n_ext {
630 let s: *CssElement = ((ext as i64)+k*NX_CSS_ELEMENT_BYTES) as *CssElement
631 e.tag_off=s.tag_off; e.tag_len=s.tag_len; e.id_off=s.id_off; e.id_len=s.id_len; e.class_off=s.class_off; e.class_len=s.class_len
632 k=k+1; mapped=1
633 } } }
634 if mapped==0 { e.tag_off=0; e.tag_len=0; e.id_off=0; e.id_len=0; e.class_off=0; e.class_len=0 }
635 i = i + 1
636 }
637
638 let cur: *CssCursor = (sys_mmap(NX_CSS_CURSOR_BYTES) as nx_size) as *CssCursor
639 nx_css_cursor_init(cur, buf, total); cur.pos = hlen
640 let tok: *CssToken = (sys_mmap(NX_CSS_TOKEN_BYTES) as nx_size) as *CssToken
641 let rules: *CssRule = (sys_mmap(NX_CSS_RULE_BYTES*K_MAGIC_8192 as nx_size)) as *CssRule
642 let decls: *CssDeclaration = (sys_mmap(NX_CSS_DECLARATION_BYTES*K_MAGIC_32768 as nx_size)) as *CssDeclaration
643 let st: *CssParseState = (sys_mmap(128) as nx_size) as *CssParseState
644 nx_css_parse_state_init(st, cur, tok, rules, K_MAGIC_8192, decls, K_MAGIC_32768)
645 nx_css_parse(st)
646 let computed: *CssComputedDecl = (sys_mmap(NX_CSS_COMPUTED_DECL_BYTES*K_MAGIC_262144 as nx_size)) as *CssComputedDecl
647 let cb: *i64 = (sys_mmap(8)) as *i64
648 rh_cascade(buf, rules, st.rule_count, decls, st.decl_count, boxel, tree.count, tree, computed, K_MAGIC_262144, cb)
649 let ncomp: i64 = cb[0]
650 rh_promote_display_kinds(tree, computed, ncomp, buf) // inline + display:flex/grid/block -> BLOCK (sized + painted)
651
652 let pbuf: *u8 = sys_mmap(256)
653 let ltable: *LayoutPropTable = (sys_mmap(NX_LAYOUT_PROP_TABLE_BYTES)) as *LayoutPropTable
654 nx_layout_prop_table_init(ltable, pbuf)
655 let resolve: *CssResolveCtx = (sys_mmap(NX_CSS_RESOLVE_CTX_BYTES)) as *CssResolveCtx
656 resolve.root_font_size_px = 16; resolve.parent_font_size_px = 16; resolve.parent_dimension_px = VW
657 let lctx: *LayoutCtx = (sys_mmap(NX_LAYOUT_CTX_BYTES)) as *LayoutCtx
658 nx_layout_ctx_init(lctx, tree, computed, ncomp, buf, VW, resolve)
659 let page_h: i64 = nx_layout_block_layout(lctx, ltable, 0)
660 var VH: i64 = page_h
661 if VH < 64 { VH = 64 }
662 if VH > K_MAGIC_5000 { VH = K_MAGIC_5000 }
663
664 let fbpx: *u8 = (sys_mmap((VW*VH*4) as nx_size)) as *u8
665 let fb: *Framebuffer = (sys_mmap(NX_FRAMEBUFFER_BYTES as nx_size)) as *Framebuffer
666 nx_framebuffer_init(fb, fbpx, VW, VH); nx_framebuffer_clear(fb)
667 let ptbuf: *u8 = (sys_mmap(64)) as *u8
668 let ptable: *PaintPropTable = (sys_mmap(NX_PAINT_PROP_TABLE_BYTES as nx_size)) as *PaintPropTable
669 nx_paint_prop_table_init(ptable, ptbuf)
670 let pctx: *PaintCtx = (sys_mmap(NX_PAINT_CTX_BYTES as nx_size)) as *PaintCtx
671 nx_paint_ctx_init(pctx, fb, tree, computed, ncomp, buf, ptable)
672 let ink: *CssColor = (sys_mmap(NX_CSS_COLOR_BYTES as nx_size)) as *CssColor
673 ink.r=0; ink.g=0; ink.b=0; ink.a=255
674 // Text prep per TEXT box: HTML-entity decode -> UTF-8-decode + ASCII-fold (accented Latin ->
675 // base letter so language names/diacritics are READABLE in the ASCII 5x7 font, non-Latin skipped),
676 // accumulated into one scratch buffer the paint reads from.
677 let dtxt: *u8 = sys_mmap(hlen + 16)
678 let dtmp: *u8 = sys_mmap(hlen + 16)
679 var dpos: i64 = 0
680 var di: i64 = 0
681 while di < tree.count {
682 let dbx: *LayoutBox = ((tree.boxes as i64) + di*NX_LAYOUT_BOX_BYTES) as *LayoutBox
683 if dbx.kind == NX_LAYOUT_BOX_TEXT {
684 let denc: i64 = nx_html_decode_entities(buf, dbx.text_off, dbx.text_len, dtmp, hlen + 16)
685 let dnl: i64 = nx_fold_ascii(dtmp, 0, denc, ((dtxt as i64)+dpos) as *u8, (hlen+16)-dpos)
686 dbx.text_off = dpos
687 dbx.text_len = dnl
688 dpos = dpos + dnl
689 }
690 di = di + 1
691 }
692 nx_paint_ctx_set_text(pctx, dtxt, ink)
693 nx_paint_ctx_set_boxel(pctx, boxel) // enable <ul><li> disc bullets
694 nx_paint_walk_layout(pctx, 0)
695
696 // CONTENT FOCUS: if the page has an HTML5 <main> (the article container), crop the output to it so
697 // the header/sidebar nav + footer chrome don't bury the article. General (HTML5 semantic element).
698 var cy0: i64 = 0
699 var cy1: i64 = VH
700 var mi: i64 = 0
701 var fnd: i64 = 0
702 while mi < tree.count {
703 if fnd == 0 {
704 let mel: *CssElement = ((boxel as i64)+mi*NX_CSS_ELEMENT_BYTES) as *CssElement
705 if mel.tag_len == 4 {
706 if (buf[mel.tag_off]&0xff)==0x6D { if (buf[mel.tag_off+1]&0xff)==0x61 { if (buf[mel.tag_off+2]&0xff)==0x69 { if (buf[mel.tag_off+3]&0xff)==0x6E {
707 let mbx: *LayoutBox = ((tree.boxes as i64)+mi*NX_LAYOUT_BOX_BYTES) as *LayoutBox
708 cy0 = mbx.y
709 cy1 = mbx.y + mbx.h
710 fnd = 1
711 } } } }
712 }
713 }
714 mi = mi + 1
715 }
716 if cy1 > VH { cy1 = VH }
717 if cy0 < 0 { cy0 = 0 }
718 if cy0 >= cy1 { cy0 = 0; cy1 = VH }
719
720 out3[0] = VW
721 out3[1] = VH
722 out3[2] = tree.count
723 out3[3] = cy0
724 out3[4] = cy1
725 return fb
726}
727
728// Convert the rendered framebuffer (top-down RGBA, transparent->white) to a PNG via the sovereign
729// nx_png_write_rgb -- so the browse output is DIRECTLY viewable (Read/Photos show PNG, not BMP).
730func rh_save_png(path: *u8, fb: *Framebuffer, w: i64, y0: i64, y1: i64) -> i64 {
731 let h: i64 = y1 - y0
732 let rgb: *u8 = sys_mmap(w * h * 3 + 16)
733 let px: *CssColor = (sys_mmap(NX_CSS_COLOR_BYTES as nx_size)) as *CssColor
734 var yy: i64 = 0
735 while yy < h {
736 let fy: i64 = y0 + yy
737 var x: i64 = 0
738 while x < w {
739 nx_framebuffer_get_pixel(fb, x, fy, px)
740 let d: i64 = (yy * w + x) * 3
741 if px.a == 0 { rgb[d]=255 as u8; rgb[d+1]=255 as u8; rgb[d+2]=255 as u8 }
742 else { rgb[d]=px.r as u8; rgb[d+1]=px.g as u8; rgb[d+2]=px.b as u8 }
743 x = x + 1
744 }
745 yy = yy + 1
746 }
747 nx_png_write_rgb(path, rgb, w, h)
748 return 0
749}
750
751// Render `raw` (hlen bytes of HTML) to a styled 24-bit BMP at `bmp_path`. Returns the box count.
752func nx_render_html_to_bmp(raw: *u8, hlen: i64, bmp_path: *u8) -> i64 {
753 let o3: *i64 = sys_mmap(64) as *i64
754 let fb: *Framebuffer = _rh_to_fb(raw, hlen, o3)
755 if o3[2] <= 0 { return 0 }
756 rh_save(bmp_path, fb, o3[0], o3[3], o3[4])
757 return o3[2]
758}
759
760// Render `raw` to a styled PNG at `png_path` (directly viewable, no BMP->PNG hop). Returns box count.
761func nx_render_html_to_png(raw: *u8, hlen: i64, png_path: *u8) -> i64 {
762 let o3: *i64 = sys_mmap(64) as *i64
763 let fb: *Framebuffer = _rh_to_fb(raw, hlen, o3)
764 if o3[2] <= 0 { return 0 }
765 rh_save_png(png_path, fb, o3[0], o3[3], o3[4])
766 return o3[2]
767}