oxedyne/fe2o3/fe2o3_austenite/web/pearl-reader/README.md
4.8 KiB, 52 runs
created by r1870400018:38476, which is this file's identity for as long as the history lasts, whatever it is later renamed to
download · who wrote it · its history
| 1 | # Pearl web reader |
| 2 | |
| 3 | A vanilla-JS browser reader for the Pearl (`.prl`) document format. It fetches a real `.prl`, parses its |
| 4 | text jdat directly in the browser, and renders every page to inline SVG from the format's own data model |
| 5 | -- glyph outlines stored once and placed per leaf, plus fills, strokes, rules and rasters -- reproducing |
| 6 | the transform the Austenite SVG arm applies, so the page is pixel-identical to Austenite's own SVG output |
| 7 | (which already matches the PDF). |
| 8 | |
| 9 | No framework, no build step, no wasm, and **no JSON projection**. Just `index.html` + `jdat.js` + |
| 10 | `pearl.js`, reading the `.prl` the engine writes. |
| 11 | |
| 12 | ## Run |
| 13 | |
| 14 | ```bash |
| 15 | cd fe2o3_austenite/web/pearl-reader |
| 16 | cargo run -p oxedyne_fe2o3_net --no-default-features --bin localserve -- . --port 8137 |
| 17 | # then open http://127.0.0.1:8137/index.html |
| 18 | ``` |
| 19 | |
| 20 | A static server is needed because the reader `fetch()`es the `.prl`; a bare `file://` open is blocked by |
| 21 | the browser's local-file CORS rule. |
| 22 | |
| 23 | ## How it reads the format |
| 24 | |
| 25 | The `.prl` is text jdat: an ordered map of ordered maps, lists, strings and typed scalar atoms. |
| 26 | `jdat.js` is a minimal recursive-descent parser for exactly the subset a v1 `.prl` uses: |
| 27 | |
| 28 | | jdat text | shape | JS value | |
| 29 | |--------------------------|--------------|-----------------------------| |
| 30 | | `(omap\|{ "k": v, ... })` | ordered map | object, insertion order kept | |
| 31 | | `[ v, v, ... ]` | list | array | |
| 32 | | `"..."` | string | string (RFC 8259 escapes) | |
| 33 | | `(u32\|N)` `(i32\|N)` `(u8\|N)` | integer atom | Number | |
| 34 | | `(f32\|X.YeZ)` | float atom | Number | |
| 35 | |
| 36 | There are no byte-strings in a v1 `.prl`: a raster's PNG rides as a **base64 string** (the writer stores |
| 37 | `base64::encode(png)`), so the whole file is these five shapes. Stripping the type tag yields the same |
| 38 | plain value the old JSON projection did, so `pearl.js`'s renderer consumes the parsed document directly, |
| 39 | with no further translation step. Unknown map keys and type tags are tolerated, so a parallel lane adding |
| 40 | fields does not break the reader. |
| 41 | |
| 42 | A `text` leaf's fields past its rigid geometry and outline glyphs -- point size, the selectable spans |
| 43 | behind the `.tsel` layer below, and the optional fill colour -- ride in one keyed object (`leaf[7]`) |
| 44 | rather than further positional list elements, so a future field never shifts an index an old reader still |
| 45 | expects; see `emit/pearl.rs`'s own comment on the v0 leaf-shape fault this replaced. |
| 46 | |
| 47 | ### Selectable text |
| 48 | |
| 49 | Every page carries an invisible, selectable `.tsel` layer over its glyph outlines -- the same answer |
| 50 | Typst.ts gives for the same problem (outline-only SVG has nothing a browser can select or search). One |
| 51 | `<text class="tsel">` spans the whole page, its `<tspan>`s built from each `text` leaf's `spans` (a |
| 52 | glyph's cluster mapped to its source text, spaces included, straight from `ShapedText::glyph_text` on the |
| 53 | Rust side): a single page-wide element, not one per run, because Chromium's `window.find`/Ctrl+F was |
| 54 | found not to search across sibling `<text>` elements once their tspans carry per-glyph `x`/`y`. A leading |
| 55 | space precedes every run but the page's first, standing in for the interword gap the line breaker leaves |
| 56 | as pure position rather than a glyph. |
| 57 | |
| 58 | Regenerate the samples with the engine: |
| 59 | |
| 60 | ```bash |
| 61 | T=~/.cache/cargo-targets/$RC_SLOT/austenite-pearl/debug # or your target dir |
| 62 | $T/austenite --pearl samples/keystone.typ out/ # writes out/document.prl |
| 63 | cp out/document.prl samples/keystone.prl |
| 64 | ``` |
| 65 | |
| 66 | ## Verify (pixel parity) |
| 67 | |
| 68 | Rasterise the reader's SVG and Austenite's reference `page-001.svg` through the same rasteriser |
| 69 | (inkscape) and pixel-diff them. Differing pixels of 500,990 against the SVG-arm reference: |
| 70 | |
| 71 | | Document | exact-match diff | at 12% fuzz | |
| 72 | |-----------|------------------|-------------| |
| 73 | | keystone | 560 (0.112%) | 0 | |
| 74 | | maths | 40 (0.008%) | 0 | |
| 75 | | raster | 45 (0.009%) | 0 | |
| 76 | |
| 77 | Zero differing pixels at 12% fuzz means no glyph, path or raster is misplaced: the residual is sub-pixel |
| 78 | antialiasing on glyph and hairline edges, from placing a transformed outline versus a baked one. The |
| 79 | raster page (`raster.prl`, a PNG carried as base64) diffs to zero at fuzz -- the `<image>` leaf renders |
| 80 | identically. |
| 81 | |
| 82 | ## Byte parity |
| 83 | |
| 84 | Not met, and the reason is architectural rather than number formatting. `pearl_render` **bakes** each |
| 85 | translate/matrix transform into the path `d` coordinates and emits a bare `<path d="...">`; the reader |
| 86 | keeps the stored `d` and applies a `transform` attribute. Same pixels, different SVG text. Closing it |
| 87 | would mean porting `fe2o3_graphics`'s `Path::transform` + `write_path_data` float formatting into JS -- |
| 88 | worthwhile only if a byte-identical SVG is itself a requirement; pixel parity above is the shippable |
| 89 | metric. |