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}