ranger-compiler 3.5.0 → 3.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1160 -19
- package/LICENSE +3 -1
- package/README.md +124 -29
- package/dist/Lang.rgr +1188 -290
- package/dist/api.d.ts +2343 -879
- package/dist/api.js +79924 -56922
- package/dist/lib/JSON.rgr +102 -91
- package/dist/lib/Shell.rgr +4 -4
- package/dist/lib/apple/AppleToolchain.rgr +4 -4
- package/dist/lib/apple/README.md +5 -4
- package/dist/lib/apple/apple_test.rgr +1 -1
- package/dist/lib/core/README.md +1 -1
- package/dist/lib/evg/EVG.rgr +12 -0
- package/dist/lib/evg/EVGA11yFromTree.rgr +302 -0
- package/dist/lib/evg/EVGA11yTree.rgr +894 -0
- package/dist/lib/evg/EVGBox.rgr +267 -0
- package/dist/lib/evg/EVGBoxShorthandTest.rgr +220 -0
- package/dist/lib/evg/EVGCodepoint.rgr +316 -0
- package/dist/lib/evg/EVGColor.rgr +700 -0
- package/dist/lib/evg/EVGCommands.rgr +177 -0
- package/dist/lib/evg/EVGComponent.rgr +331 -0
- package/dist/lib/evg/EVGComponentTest.rgr +387 -0
- package/dist/lib/evg/EVGConnector.rgr +541 -0
- package/dist/lib/evg/EVGConnectorTest.rgr +306 -0
- package/dist/lib/evg/EVGDisplayList.rgr +4186 -0
- package/dist/lib/evg/EVGEasing.rgr +370 -0
- package/dist/lib/evg/EVGEffectTest.rgr +253 -0
- package/dist/lib/evg/EVGElement.rgr +4914 -0
- package/dist/lib/evg/EVGFixedTest.rgr +289 -0
- package/dist/lib/evg/EVGFlexRulesTest.rgr +317 -0
- package/dist/lib/evg/EVGFlexWrapTest.rgr +246 -0
- package/dist/lib/evg/EVGFling.rgr +244 -0
- package/dist/lib/evg/EVGFocus.rgr +400 -0
- package/dist/lib/evg/EVGFocusTest.rgr +399 -0
- package/dist/lib/evg/EVGGradient.rgr +322 -0
- package/dist/lib/evg/EVGGrapheme.rgr +190 -0
- package/dist/lib/evg/EVGGrid.rgr +975 -0
- package/dist/lib/evg/EVGHitTest.rgr +208 -0
- package/dist/lib/evg/EVGHoles.rgr +294 -0
- package/dist/lib/evg/EVGHostMeasurerTest.rgr +281 -0
- package/dist/lib/evg/EVGHostTextMeasurer.rgr +331 -0
- package/dist/lib/evg/EVGHostTree.rgr +834 -0
- package/dist/lib/evg/EVGHostTreeTest.rgr +447 -0
- package/dist/lib/evg/EVGImageDecode.rgr +117 -0
- package/dist/lib/evg/EVGImageMeasurer.rgr +86 -0
- package/dist/lib/evg/EVGInspect.rgr +883 -0
- package/dist/lib/evg/EVGInvalidateTest.rgr +432 -0
- package/dist/lib/evg/EVGJsonTest.rgr +535 -0
- package/dist/lib/evg/EVGLayout.rgr +4220 -0
- package/dist/lib/evg/EVGMeasure.rgr +1267 -0
- package/dist/lib/evg/EVGOverlayTest.rgr +626 -0
- package/dist/lib/evg/EVGPatch.rgr +1721 -0
- package/dist/lib/evg/EVGPatchTest.rgr +692 -0
- package/dist/lib/evg/EVGPopoverTest.rgr +395 -0
- package/dist/lib/evg/EVGReconcile.rgr +226 -0
- package/dist/lib/evg/EVGReconcileTest.rgr +421 -0
- package/dist/lib/evg/EVGReject.rgr +126 -0
- package/dist/lib/evg/EVGRelayoutTest.rgr +375 -0
- package/dist/lib/evg/EVGRtlLayoutTest.rgr +319 -0
- package/dist/lib/evg/EVGRuler.rgr +232 -0
- package/dist/lib/evg/EVGRulerTest.rgr +172 -0
- package/dist/lib/evg/EVGSelectChrome.rgr +178 -0
- package/dist/lib/evg/EVGStyleCacheTest.rgr +419 -0
- package/dist/lib/evg/EVGStyleSheet.rgr +2148 -0
- package/dist/lib/evg/EVGStyleStateTest.rgr +407 -0
- package/dist/lib/evg/EVGStyleVarTest.rgr +471 -0
- package/dist/lib/evg/EVGText.rgr +105 -0
- package/dist/lib/evg/EVGTextEngine.rgr +536 -0
- package/dist/lib/evg/EVGTextMeasurer.rgr +720 -0
- package/dist/lib/evg/EVGTimingTest.rgr +1417 -0
- package/dist/lib/evg/EVGToolbar.rgr +1369 -0
- package/dist/lib/evg/EVGTransition.rgr +728 -0
- package/dist/lib/evg/EVGTreeJson.rgr +721 -0
- package/dist/lib/evg/EVGUnit.rgr +554 -0
- package/dist/lib/evg/EVGViewportUnitTest.rgr +236 -0
- package/dist/lib/evg/EvgApp.rgr +189 -0
- package/dist/lib/evg/EvgBitmapTracer.rgr +4277 -0
- package/dist/lib/evg/EvgBitmapTracerTest.rgr +2119 -0
- package/dist/lib/evg/EvgHost.rgr +451 -0
- package/dist/lib/evg/EvgTest.rgr +91 -0
- package/dist/lib/evg/EvgTraceColor.rgr +236 -0
- package/dist/lib/evg/EvgTraceCurve.rgr +648 -0
- package/dist/lib/evg/EvgTraceFit.rgr +767 -0
- package/dist/lib/evg/EvgTracePath.rgr +435 -0
- package/dist/lib/evg/EvgTraceTypes.rgr +559 -0
- package/dist/lib/evg/EvgViewport.rgr +313 -0
- package/dist/lib/evg/FxDemoDoc.rgr +185 -0
- package/dist/lib/evg/HOSTS.md +227 -0
- package/dist/lib/evg/ISSUES.md +835 -0
- package/dist/lib/evg/PLAN_ACCESSIBILITY.md +650 -0
- package/dist/lib/evg/PLAN_CSS_LAYOUT_AND_FONTS.md +1124 -0
- package/dist/lib/evg/PLAN_EFFECTS.md +431 -0
- package/dist/lib/evg/PLAN_EVG.md +518 -0
- package/dist/lib/evg/PLAN_EVG_RENDERER.md +1674 -0
- package/dist/lib/evg/PLAN_INSPECTOR.md +788 -0
- package/dist/lib/evg/PLAN_LINKS_AND_FORMS.md +157 -0
- package/dist/lib/evg/PLAN_NATIVE_HOSTS.md +671 -0
- package/dist/lib/evg/PLAN_VECTOR_IR.md +664 -0
- package/dist/lib/evg/PLAN_VIEW_TRANSFORM.md +322 -0
- package/dist/lib/evg/PathBuilder.rgr +333 -0
- package/dist/lib/evg/README.md +1236 -0
- package/dist/lib/evg/SPEC.md +1252 -0
- package/dist/lib/evg/SVGPathParser.rgr +1199 -0
- package/dist/lib/evg/SvgParser.rgr +1736 -0
- package/dist/lib/evg/TODO_EVG_RENDERER.md +595 -0
- package/dist/lib/evg/VectorShapes.rgr +331 -0
- package/dist/lib/evg/VectorStroke.rgr +107 -0
- package/dist/lib/evg/VectorViewBox.rgr +380 -0
- package/dist/lib/evg/agent/README.md +290 -0
- package/dist/lib/evg/agent/evg_agent.rgr +899 -0
- package/dist/lib/evg/agent/fixtures/broken.evg.json +7 -0
- package/dist/lib/evg/agent/fixtures/card.evg.json +9 -0
- package/dist/lib/evg/agent/fixtures/connector.css +63 -0
- package/dist/lib/evg/agent/fixtures/connector.evg.json +14 -0
- package/dist/lib/evg/agent/fixtures/drawn.evg.json +129 -0
- package/dist/lib/evg/agent/fixtures/gradient.evg.json +4 -0
- package/dist/lib/evg/agent/fixtures/popover.css +99 -0
- package/dist/lib/evg/agent/fixtures/popover.evg.json +30 -0
- package/dist/lib/evg/agent/roundtrip.sh +74 -0
- package/dist/lib/evg/agent/smoke.sh +230 -0
- package/dist/lib/evg/android/README.md +97 -0
- package/dist/lib/evg/android/androidstubs/AndroidStubs.kt +175 -0
- package/dist/lib/evg/android/androidstubs/Annotation.kt +10 -0
- package/dist/lib/evg/android/androidstubs/App.kt +30 -0
- package/dist/lib/evg/android/androidstubs/Content.kt +41 -0
- package/dist/lib/evg/android/androidstubs/ContentRes.kt +12 -0
- package/dist/lib/evg/android/androidstubs/InputMethod.kt +46 -0
- package/dist/lib/evg/android/androidstubs/Net.kt +6 -0
- package/dist/lib/evg/android/androidstubs/Os.kt +30 -0
- package/dist/lib/evg/android/androidstubs/Util.kt +15 -0
- package/dist/lib/evg/android/androidstubs/View.kt +113 -0
- package/dist/lib/evg/android/androidstubs/Widget.kt +16 -0
- package/dist/lib/evg/android/src/android/kotlin/fi/ranger/evg/AndroidEvgSurface.kt +323 -0
- package/dist/lib/evg/android/src/android/kotlin/fi/ranger/evg/AndroidTextMeasurer.kt +56 -0
- package/dist/lib/evg/android/src/android/kotlin/fi/ranger/evg/RippleEffect.kt +262 -0
- package/dist/lib/evg/android/src/awt/kotlin/fi/ranger/evg/AwtEvgSurface.kt +238 -0
- package/dist/lib/evg/android/src/awt/kotlin/fi/ranger/evg/AwtTextMeasurer.kt +85 -0
- package/dist/lib/evg/android/src/main/kotlin/fi/ranger/evg/EvgEngineThread.kt +115 -0
- package/dist/lib/evg/android/src/main/kotlin/fi/ranger/evg/EvgPainter.kt +231 -0
- package/dist/lib/evg/android/src/main/kotlin/fi/ranger/evg/EvgSurface.kt +117 -0
- package/dist/lib/evg/android/src/main/kotlin/fi/ranger/evg/RecordingSurface.kt +94 -0
- package/dist/lib/evg/apple/README.md +128 -0
- package/dist/lib/evg/apple/Sources/CoreGraphicsEvgSurface.swift +329 -0
- package/dist/lib/evg/apple/Sources/CoreTextMeasurer.swift +70 -0
- package/dist/lib/evg/apple/Sources/EvgEngineQueue.swift +105 -0
- package/dist/lib/evg/apple/Sources/EvgPainter.swift +225 -0
- package/dist/lib/evg/apple/Sources/EvgSurface.swift +163 -0
- package/dist/lib/evg/apple/Sources/RecordingSurface.swift +110 -0
- package/dist/lib/evg/bench/EvgLayoutBench.rgr +34 -0
- package/dist/lib/evg/bench/README.md +64 -0
- package/dist/lib/evg/bench/layout-bench.mjs +316 -0
- package/dist/lib/evg/bench/layout-cases.mjs +510 -0
- package/dist/lib/evg/bench/layout-conformance.mjs +254 -0
- package/dist/lib/evg/bin/.gitignore +17 -0
- package/dist/lib/evg/evg_test.rgr +313 -0
- package/dist/lib/evg/gl/README.md +198 -0
- package/dist/lib/evg/gl/a11y-paint-check.mjs +73 -0
- package/dist/lib/evg/gl/blur-check.mjs +399 -0
- package/dist/lib/evg/gl/boxmodel.json +1 -0
- package/dist/lib/evg/gl/demo.html +46 -0
- package/dist/lib/evg/gl/effect-presets.css +285 -0
- package/dist/lib/evg/gl/effect-presets.js +63 -0
- package/dist/lib/evg/gl/effect-shots.mjs +135 -0
- package/dist/lib/evg/gl/evg-a11y.js +566 -0
- package/dist/lib/evg/gl/evg-binary.js +160 -0
- package/dist/lib/evg/gl/evg-engine.js +296 -0
- package/dist/lib/evg/gl/evg-fx.js +237 -0
- package/dist/lib/evg/gl/evg-gestures.js +209 -0
- package/dist/lib/evg/gl/evg-list.js +167 -0
- package/dist/lib/evg/gl/evg-measure.js +186 -0
- package/dist/lib/evg/gl/evg-textinput.js +303 -0
- package/dist/lib/evg/gl/evg-view.js +144 -0
- package/dist/lib/evg/gl/evg-webgl.js +3942 -0
- package/dist/lib/evg/gl/fx-check.mjs +998 -0
- package/dist/lib/evg/gl/fx-demo.html +166 -0
- package/dist/lib/evg/gl/fx-demo.js +2 -0
- package/dist/lib/evg/gl/fx-serve.mjs +47 -0
- package/dist/lib/evg/gl/gestures-check.mjs +189 -0
- package/dist/lib/evg/gl/list-binary-check.mjs +152 -0
- package/dist/lib/evg/gl/measure-check.mjs +135 -0
- package/dist/lib/evg/gl/rotation-check.mjs +207 -0
- package/dist/lib/evg/gl/shift-check.mjs +113 -0
- package/dist/lib/evg/gl/stroke-check.mjs +168 -0
- package/dist/lib/evg/gl/text-snap-check.mjs +139 -0
- package/dist/lib/evg/gl/view-check.mjs +343 -0
- package/dist/lib/evg/gl/view-policy-check.mjs +184 -0
- package/dist/lib/evg/html/evg-dom.js +318 -0
- package/dist/lib/evg/html/evg-html.js +600 -0
- package/dist/lib/evg/inspect/README.md +415 -0
- package/dist/lib/evg/inspect/browser-smoke.mjs +115 -0
- package/dist/lib/evg/inspect/evg-inspect.js +947 -0
- package/dist/lib/evg/inspect/shots/css.png +0 -0
- package/dist/lib/evg/inspect/shots/dashboard.png +0 -0
- package/dist/lib/evg/inspect/shots/pptx-slide.png +0 -0
- package/dist/lib/evg/inspect/shots/state.png +0 -0
- package/dist/lib/evg/inspect/shots.mjs +226 -0
- package/dist/lib/evg/oracle/css-blur.json +576 -0
- package/dist/lib/evg/oracle/css-box.json +157 -0
- package/dist/lib/evg/oracle/css-timing.json +709 -0
- package/dist/lib/evg/oracle/css_blur_oracle.mjs +529 -0
- package/dist/lib/evg/oracle/css_box_oracle.mjs +106 -0
- package/dist/lib/evg/oracle/css_timing_oracle.mjs +389 -0
- package/dist/lib/evg/original/EVGColor.clj +310 -0
- package/dist/lib/evg/original/EVGColorContext.rgr +178 -0
- package/dist/lib/evg/original/SVGPath.rgr +627 -0
- package/dist/lib/evg/original/Vec2.crgr +103 -0
- package/dist/lib/evg/ranger.json +9 -0
- package/dist/lib/evg/showcase/README.md +246 -0
- package/dist/lib/evg/showcase/assets/emblem.svg +30 -0
- package/dist/lib/evg/showcase/assets/rosette.svg +22 -0
- package/dist/lib/evg/showcase/build.mjs +651 -0
- package/dist/lib/evg/showcase/pages/album.tsx +30 -0
- package/dist/lib/evg/showcase/pages/boxmodel.tsx +37 -0
- package/dist/lib/evg/showcase/pages/cards.tsx +41 -0
- package/dist/lib/evg/showcase/pages/chart_api.tsx +170 -0
- package/dist/lib/evg/showcase/pages/charts.tsx +219 -0
- package/dist/lib/evg/showcase/pages/drawing.tsx +160 -0
- package/dist/lib/evg/showcase/pages/emoji.tsx +91 -0
- package/dist/lib/evg/showcase/pages/flex.tsx +46 -0
- package/dist/lib/evg/showcase/pages/more.tsx +332 -0
- package/dist/lib/evg/showcase/pages/plots.tsx +262 -0
- package/dist/lib/evg/showcase/pages/svg.tsx +63 -0
- package/dist/lib/evg/showcase/pages/tables.tsx +209 -0
- package/dist/lib/evg/showcase/pages/typography.tsx +43 -0
- package/dist/lib/evg/showcase/pages/units.tsx +35 -0
- package/dist/lib/evg/showcase/pages/variants.tsx +233 -0
- package/dist/lib/evg/showcase/pages/vector.tsx +75 -0
- package/dist/lib/evg/showcase/pages/views.tsx +223 -0
- package/dist/lib/evg/showcase/tests/chart_api_smoke.mjs +339 -0
- package/dist/lib/evg/showcase/tests/gl_smoke.mjs +125 -0
- package/dist/lib/evg/showcase/themes/chart_api-default.css +15 -0
- package/dist/lib/evg/showcase/themes/charts-default.css +29 -0
- package/dist/lib/evg/showcase/themes/drawing-default.css +35 -0
- package/dist/lib/evg/showcase/themes/more-default.css +128 -0
- package/dist/lib/evg/showcase/themes/plots-default.css +18 -0
- package/dist/lib/evg/showcase/themes/showcase.css +508 -0
- package/dist/lib/evg/showcase/themes/tables-default.css +123 -0
- package/dist/lib/evg/showcase/themes/variants-default.css +15 -0
- package/dist/lib/evg/showcase/themes/views-default.css +15 -0
- package/dist/lib/evg/tools/bench_vs_potrace.mjs +290 -0
- package/dist/lib/evg/tools/evg_image_tool.rgr +488 -0
- package/dist/lib/evg/tools/evg_trace_bench.rgr +114 -0
- package/dist/lib/evg/tools/evg_trace_cli.rgr +662 -0
- package/dist/lib/evg/tools/evg_trace_cpp_bench.rgr +83 -0
- package/dist/lib/evg/tools/run_trace_bench.sh +74 -0
- package/dist/lib/evg/tools/run_trace_cli_smoke.sh +128 -0
- package/dist/lib/evg/web/responsive/EvgResponsiveCheck.rgr +271 -0
- package/dist/lib/evg/web/responsive/EvgResponsiveDemo.rgr +531 -0
- package/dist/lib/evg/web/responsive/README.md +97 -0
- package/dist/lib/evg/web/responsive/build.mjs +90 -0
- package/dist/lib/evg/web/responsive/dom-check.mjs +169 -0
- package/dist/lib/evg/web/responsive/index.html +177 -0
- package/dist/lib/evg/web/responsive/smoke.mjs +221 -0
- package/dist/lib/evg/web/tools/assets-client.mjs +95 -0
- package/dist/lib/evg/web/tools/boot-bench.mjs +189 -0
- package/dist/lib/evg/web/tools/inline-assets.mjs +119 -0
- package/dist/lib/evg/web/tools/minify.mjs +60 -0
- package/dist/lib/evg/web/tracer/build.mjs +87 -0
- package/dist/lib/evg/web/tracer/index.html +2805 -0
- package/dist/lib/evg/web/tracer/sample.png +0 -0
- package/dist/lib/evg/web/tracer/smoke.mjs +1162 -0
- package/dist/lib/evgr/Cargo.lock +21 -0
- package/dist/lib/evgr/Cargo.toml +19 -0
- package/dist/lib/evgr/README.md +140 -0
- package/dist/lib/evgr/bench/NativeBench.rgr +246 -0
- package/dist/lib/evgr/bench/compare.mjs +252 -0
- package/dist/lib/evgr/bench/speed.mjs +277 -0
- package/dist/lib/evgr/bin/.gitignore +3 -0
- package/dist/lib/evgr/src/bin/bench.rs +106 -0
- package/dist/lib/evgr/src/bin/smoke.rs +13 -0
- package/dist/lib/evgr/src/grid.rs +730 -0
- package/dist/lib/evgr/src/lib.rs +1003 -0
- package/dist/lib/evgr/src/style.rs +468 -0
- package/dist/lib/evgr/src/text.rs +83 -0
- package/dist/lib/image/BitReader.rgr +171 -0
- package/dist/lib/image/Buffer.rgr +173 -0
- package/dist/lib/image/DCT.rgr +283 -0
- package/dist/lib/image/Deflate.rgr +339 -0
- package/dist/lib/image/HuffmanDecoder.rgr +181 -0
- package/dist/lib/image/ImageBuffer.rgr +706 -0
- package/dist/lib/image/JPEGDecoder.rgr +808 -0
- package/dist/lib/image/PNGDecoder.rgr +544 -0
- package/dist/lib/image/PNGEncoder.rgr +388 -0
- package/dist/lib/image/PPMImage.rgr +224 -0
- package/dist/lib/image/ProgressiveJPEGDecoder.rgr +1154 -0
- package/dist/lib/image/README.md +33 -0
- package/dist/lib/image/RasterBuffer.rgr +294 -0
- package/dist/lib/image/VP8BoolDecoder.rgr +163 -0
- package/dist/lib/image/WebPDecoder.rgr +433 -0
- package/dist/lib/image/WebPLossless.rgr +1035 -0
- package/dist/lib/image/WebPLossy.rgr +1929 -0
- package/dist/lib/image/ranger.json +9 -0
- package/dist/lib/image/testdata/webp/anim_2f_24x16.webp +0 -0
- package/dist/lib/image/testdata/webp/fixtures.txt +33 -0
- package/dist/lib/image/testdata/webp/gen_fixtures.py +261 -0
- package/dist/lib/image/testdata/webp/ll_1x1.webp +0 -0
- package/dist/lib/image/testdata/webp/ll_alpha_29x21.webp +0 -0
- package/dist/lib/image/testdata/webp/ll_gradient_64x64.webp +0 -0
- package/dist/lib/image/testdata/webp/ll_meta_32x32.webp +0 -0
- package/dist/lib/image/testdata/webp/ll_noise_17x9.webp +0 -0
- package/dist/lib/image/testdata/webp/ll_pal11_23x11.webp +0 -0
- package/dist/lib/image/testdata/webp/ll_pal2_19x7.webp +0 -0
- package/dist/lib/image/testdata/webp/ll_pal40_16x16.webp +0 -0
- package/dist/lib/image/testdata/webp/ll_pal4_13x5.webp +0 -0
- package/dist/lib/image/testdata/webp/ll_photo_40x30.webp +0 -0
- package/dist/lib/image/testdata/webp/ll_predictors_64x32.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_1x1.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_alph_raw_f0_21x13.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_alph_raw_f1_21x13.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_alph_raw_f2_21x13.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_alph_raw_f3_21x13.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_alph_vp8l_f0_21x13.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_alph_vp8l_f1_21x13.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_alph_vp8l_f2_21x13.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_alph_vp8l_f3_21x13.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_alpha_33x17.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_exif_15x11.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_i16_96x80.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_nofilter_24x20.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_odd_17x9.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_photo_48x40.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_q100_20x18.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_sharp_40x36.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_simple_filter_33x31.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_vpx_parts8_37x45.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_vpx_skip_96x80.webp +0 -0
- package/dist/lib/image/tests/WebPDecodeTool.rgr +81 -0
- package/dist/lib/image/tests/WebPDecoderTest.rgr +1162 -0
- package/dist/lib/image/tests/run_webp_tests.sh +39 -0
- package/dist/lib/rust/RsJson.rgr +468 -0
- package/dist/lib/rust/RsPrelude.rgr +1663 -0
- package/dist/lib/shell_test.rgr +7 -7
- package/dist/lib/stdlib.rgr +21 -5
- package/dist/lib/zip/ranger.json +6 -0
- package/dist/rgrc.js +87318 -67610
- package/package.json +1010 -1040
- package/dist/README.md +0 -117
- package/dist/package.json +0 -47
|
@@ -0,0 +1,1124 @@
|
|
|
1
|
+
# EVG: CSS-closer layout + font-correct rendering
|
|
2
|
+
|
|
3
|
+
**Status:** Phases 0-4 landed
|
|
4
|
+
**Date:** 2026-08-12, updated 2026-08-14
|
|
5
|
+
**Related:** `PLAN_EVG.md`, `SPEC.md`, `ISSUES.md`, `gallery/pdf_writer/TODO_PDF.md`
|
|
6
|
+
|
|
7
|
+
## 1. Goal
|
|
8
|
+
|
|
9
|
+
Move EVG closer to a practical HTML/CSS subset so photo-book and print layouts can:
|
|
10
|
+
|
|
11
|
+
- use familiar **flexbox** (more complete than today)
|
|
12
|
+
- use simple **CSS Grid** for album pages
|
|
13
|
+
- switch look quickly via **classes / themes** (not only inline props)
|
|
14
|
+
- keep **layout and font rendering correct and consistent** across HTML preview, PDF, and raster
|
|
15
|
+
|
|
16
|
+
The hard requirement is not “full browser CSS”. It is:
|
|
17
|
+
|
|
18
|
+
> **The same text, with the same font file, must measure and paint to the same box on every target.**
|
|
19
|
+
|
|
20
|
+
Without that, flex/grid improvements will still look wrong in print.
|
|
21
|
+
|
|
22
|
+
## 2. Non-goals (for this track)
|
|
23
|
+
|
|
24
|
+
- Full CSS cascade, specificity wars, or selector engine parity with browsers
|
|
25
|
+
- Media queries / responsive breakpoints as a first deliverable
|
|
26
|
+
- `:hover`, animations, or interactive pseudo-classes
|
|
27
|
+
- Replacing SwiftUI/AppKit for the macOS app chrome
|
|
28
|
+
- Perfect PDF vs browser visual identity for shadows/gradients (already known gaps)
|
|
29
|
+
|
|
30
|
+
## 3. Current state (baseline)
|
|
31
|
+
|
|
32
|
+
### Layout
|
|
33
|
+
|
|
34
|
+
| Area | Today | Gap |
|
|
35
|
+
| --- | --- | --- |
|
|
36
|
+
| Lengths | `px`, `%`, `em`, `rem`, `vw` / `vh`, and the absolute print units `pt` / `pc` / `in` / `mm` / `cm`, plus EVG's own `hp` and `fill` | `vmin` / `vmax`, `ch` / `ex`, `calc()` — all rejected rather than misread |
|
|
37
|
+
| Kerning | GPOS pair adjustments (formats 1 and 2, incl. Extension lookups) and the legacy `kern` table, in measurement and in paint | Ligatures and other GPOS features |
|
|
38
|
+
| Flex | Grow, per-item `flex-shrink`, `flex-basis` (as a size, growing or not), `flex` shorthand, min/max frozen and redistributed on BOTH sides of the distribution, `align-self` | Reversed directions (`row-reverse` / `column-reverse`) parse and warn |
|
|
39
|
+
| `gap` | Both axes, including between wrapped lines, with `row-gap` / `column-gap` overriding the shorthand per axis | — |
|
|
40
|
+
| `flex-wrap` | `wrap` (default) / `nowrap` / `wrap-reverse`, with the full `align-content` set including `stretch` | — |
|
|
41
|
+
| Alignment | `justifyContent`, `alignItems` (incl. `stretch` and `baseline`), plus legacy `align` / `verticalAlign` | Naming overlap between the CSS and legacy names |
|
|
42
|
+
| Text intrinsic size | Shrink-wraps to content measured from the real face | — |
|
|
43
|
+
| Grid | `display: grid` with fr/px/%/`auto`/`fit-content()`/repeat/`minmax()` tracks, gaps, spans, `grid-template-areas`, `grid-auto-flow: dense`, `subgrid` on both axes, named lines. 20 fixtures checked against Chromium | Intrinsic sizing of a container item (only definite widths and text leaves contribute) |
|
|
44
|
+
| Styles | Mostly inline JSX attributes | No class/theme stylesheet layer |
|
|
45
|
+
|
|
46
|
+
`min-width` / `max-width` / `min-height` / `max-height` parse, clamp, and are
|
|
47
|
+
ordered correctly: `max` first and `min` second, so the minimum wins a
|
|
48
|
+
contradiction, and inside a flex line a clamp freezes the item and hands the
|
|
49
|
+
space it refused to its siblings. Measured against Chromium in
|
|
50
|
+
[`bench/`](../evg/bench/); asserted in `EVGFlexRulesTest.rgr`.
|
|
51
|
+
|
|
52
|
+
ISSUES #1 (labels taking full parent width in a `row`) is resolved — see that
|
|
53
|
+
file for what the fix depended on.
|
|
54
|
+
|
|
55
|
+
### Fonts
|
|
56
|
+
|
|
57
|
+
| Path | Role | Risk |
|
|
58
|
+
| --- | --- | --- |
|
|
59
|
+
| `EVGTextMeasurer` default | Heuristic widths (`fontSize * 0.55`) | Declares `isFontAccurate() == false`; reported, and fatal under `-strict-fonts` |
|
|
60
|
+
| `TTFTextMeasurer` + `FontManager` + `TrueTypeFont` | Real advance widths, ascender/descender/lineGap | Installed by every tool via `EVGFontSetup` |
|
|
61
|
+
| HTML renderer | Same TTFs via `@font-face`, measured with `TTFTextMeasurer` | Parity checked against Chromium in CI |
|
|
62
|
+
| Raster (`RasterText`) | Glyph outlines from same TTF | Layout now measures with the same faces |
|
|
63
|
+
|
|
64
|
+
All four targets resolve fonts through `EVGFontSetup` and measure through
|
|
65
|
+
`EVGTextEngine`, so layout and paint cannot disagree. See Phase 0 below for what
|
|
66
|
+
this replaced.
|
|
67
|
+
|
|
68
|
+
## 4. Design principle: fonts drive layout
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
72
|
+
│ Authoring │
|
|
73
|
+
│ TSX + className / theme (+ optional inline overrides) │
|
|
74
|
+
└─────────────────────────────┬───────────────────────────────┘
|
|
75
|
+
▼
|
|
76
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
77
|
+
│ Style resolve │
|
|
78
|
+
│ stylesheet subset → computed style map on each node │
|
|
79
|
+
└─────────────────────────────┬───────────────────────────────┘
|
|
80
|
+
▼
|
|
81
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
82
|
+
│ Font resolve │
|
|
83
|
+
│ family + weight + style → concrete TTF face (FontManager) │
|
|
84
|
+
│ FAIL or explicit fallback if face missing │
|
|
85
|
+
│ (never silent guess for print metrics) │
|
|
86
|
+
└─────────────────────────────┬───────────────────────────────┘
|
|
87
|
+
▼
|
|
88
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
89
|
+
│ Text shaping / metrics (shared) │
|
|
90
|
+
│ width, ascent, descent, lineHeight, wrap breakpoints │
|
|
91
|
+
│ ONE implementation used by layout + all paint backends │
|
|
92
|
+
└─────────────────────────────┬───────────────────────────────┘
|
|
93
|
+
▼
|
|
94
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
95
|
+
│ EVGLayout (flex v2 + grid v1) │
|
|
96
|
+
│ uses only resolved metrics + box model │
|
|
97
|
+
└─────────────────────────────┬───────────────────────────────┘
|
|
98
|
+
▼
|
|
99
|
+
┌──────────────┬──────────────┬──────────────┐
|
|
100
|
+
▼ ▼ ▼
|
|
101
|
+
PDF paint Raster paint HTML preview
|
|
102
|
+
(embed TTF) (outline TTF) (@font-face same files)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### 4.1 Font correctness rules
|
|
106
|
+
|
|
107
|
+
1. **Same face file for measure and paint**
|
|
108
|
+
Layout never uses the heuristic measurer when a document declares custom fonts.
|
|
109
|
+
|
|
110
|
+
2. **Metrics come from TTF tables**
|
|
111
|
+
- advance widths from `hmtx` (via existing `TrueTypeFont.measureText`)
|
|
112
|
+
- `ascent` / `descent` / `lineGap` from `hhea` (already exposed)
|
|
113
|
+
- optional later: OS/2 `sTypo*` vs `hhea` policy documented and fixed
|
|
114
|
+
|
|
115
|
+
3. **`line-height` semantics**
|
|
116
|
+
Support CSS-like:
|
|
117
|
+
- unitless multiplier (`1.2`) → relative to font size / content box policy (pick one, document it)
|
|
118
|
+
- absolute (`18px`, `14pt`)
|
|
119
|
+
Default should match current print-friendly behavior, not browser quirks mode.
|
|
120
|
+
|
|
121
|
+
4. **Baseline alignment** ✅
|
|
122
|
+
`align-items: baseline` is implemented against real ascent metrics; text
|
|
123
|
+
nodes carry `calculatedBaseline` / `calculatedDescent`.
|
|
124
|
+
|
|
125
|
+
5. **Wrapping**
|
|
126
|
+
Word wrap must use the same width measurement as final paint. PDF `wrapText` and layout height calculation must call the shared shaper, not duplicate heuristics.
|
|
127
|
+
|
|
128
|
+
6. **Weight / style mapping**
|
|
129
|
+
`font-weight: 700` / `bold` resolves to a loaded face (e.g. `"Open Sans Bold"`), not a synthetic stroke in layout. If bold face is missing → warning + regular face (measurable), never “pretend bold” for width.
|
|
130
|
+
|
|
131
|
+
7. **HTML parity** ✅
|
|
132
|
+
Preview loads the same TTF files through `@font-face`, and the widths a
|
|
133
|
+
browser measures for a set of golden strings are recorded into
|
|
134
|
+
`test/fixtures/browser_parity.snapshot` so the check runs **without a
|
|
135
|
+
browser**. `--update-snapshot` re-records; `--verify-snapshot` confirms the
|
|
136
|
+
stored numbers still match a live Chromium.
|
|
137
|
+
|
|
138
|
+
8. **Encoding honesty** ✅
|
|
139
|
+
Source is decoded as UTF-8 so layout measures real glyphs. The encoder now
|
|
140
|
+
uses the actual WinAnsi repertoire (CP1252) rather than Latin-1, so the
|
|
141
|
+
0x80–0x9F band — em and en dashes, curly quotes, ellipsis, bullet, euro —
|
|
142
|
+
reaches the page as the right byte instead of being refused; see Phase 4.2.
|
|
143
|
+
What WinAnsi genuinely cannot hold is reported with its codepoint (fatal
|
|
144
|
+
under `-strict-fonts`) rather than silently substituted. Embedding a subset
|
|
145
|
+
font, which would lift the repertoire limit rather than report it, is still
|
|
146
|
+
open. (A `/ToUnicode` cmap is already written, and now covers that band too,
|
|
147
|
+
so text extracts correctly — but it does not widen what can be encoded.)
|
|
148
|
+
|
|
149
|
+
9. **Box model** ✅
|
|
150
|
+
Padding, margins, gaps, nesting and background colour are checked against
|
|
151
|
+
recorded browser geometry. Building it found two real divergences:
|
|
152
|
+
percentage padding/margin on the vertical axis resolved against the
|
|
153
|
+
containing block's *height* (CSS uses its **width** on all four sides — the
|
|
154
|
+
rule behind the padding-bottom aspect-ratio trick), and a box whose padding
|
|
155
|
+
exceeded its declared size kept that size and gave its children a negative
|
|
156
|
+
content box, where CSS grows the box instead. Both are fixed.
|
|
157
|
+
|
|
158
|
+
10. **Kerning** ✅
|
|
159
|
+
EVG reads GPOS pair adjustments — and the legacy `kern` table on a face with
|
|
160
|
+
no GPOS — and applies them in measurement AND in paint. The parity snapshot
|
|
161
|
+
now targets the browser's **kerned** width: worst delta **0.015px** across
|
|
162
|
+
24 fixtures, and **0.006px** against a live Chromium page. Before this, the
|
|
163
|
+
worst was 1.94px on "Helsinki, 2024" in Cinzel at 30px. See Phase 4.3.
|
|
164
|
+
|
|
165
|
+
## 5. Style layer (quick theme changes)
|
|
166
|
+
|
|
167
|
+
### 5.1 Authoring model
|
|
168
|
+
|
|
169
|
+
Keep JSX components; add:
|
|
170
|
+
|
|
171
|
+
```tsx
|
|
172
|
+
<Document theme="classic">
|
|
173
|
+
<Page className="spread">
|
|
174
|
+
<Label className="caption">Helsinki, 2024</Label>
|
|
175
|
+
<View className="grid-2">
|
|
176
|
+
<Image className="photo" src={a} />
|
|
177
|
+
<Image className="photo" src={b} />
|
|
178
|
+
</View>
|
|
179
|
+
</Page>
|
|
180
|
+
</Document>
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Stylesheet (CSS subset or JSON equivalent — CSS surface preferred for familiarity):
|
|
184
|
+
|
|
185
|
+
```css
|
|
186
|
+
.theme-classic .spread { padding: 24px; gap: 16px; }
|
|
187
|
+
.theme-classic .caption {
|
|
188
|
+
font-family: "Cinzel";
|
|
189
|
+
font-size: 14px;
|
|
190
|
+
line-height: 1.3;
|
|
191
|
+
color: #222;
|
|
192
|
+
}
|
|
193
|
+
.theme-classic .grid-2 {
|
|
194
|
+
display: grid;
|
|
195
|
+
grid-template-columns: 1fr 1fr;
|
|
196
|
+
gap: 12px;
|
|
197
|
+
}
|
|
198
|
+
.theme-classic .photo { object-fit: cover; width: 100%; height: 100%; }
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### 5.2 Supported CSS subset (v1)
|
|
202
|
+
|
|
203
|
+
**Selectors:** `.class`, `.theme-x .class`, element type optional later
|
|
204
|
+
**Properties (style resolve → existing EVG fields):**
|
|
205
|
+
|
|
206
|
+
- box: `width`, `height`, `min/max-width/height`, `margin`, `padding`, `border-*`, `border-radius`
|
|
207
|
+
- flex: `display: flex|block`, `flex-direction`, `flex` / `flex-grow|shrink|basis`, `justify-content`, `align-items`, `align-content`, `flex-wrap`, `gap`
|
|
208
|
+
- grid (v1): `display: grid`, `grid-template-columns`, `grid-template-rows`, `gap`, `grid-column`, `grid-row` (simple span)
|
|
209
|
+
- text/font: `font-family`, `font-size`, `font-weight`, `font-style`, `line-height`, `text-align`, `color`
|
|
210
|
+
- visual: `background`, `background-color`, `opacity` (where backend allows), `object-fit`
|
|
211
|
+
|
|
212
|
+
**Cascade v1:** theme defaults < class < inline attributes. No IDs, no `!important`.
|
|
213
|
+
|
|
214
|
+
### 5.3 Why not full CSS?
|
|
215
|
+
|
|
216
|
+
Print albums need predictable pages. A small stylesheet with themes gives fast visual iteration without implementing a browser. The resolve step outputs the same computed props PDF already understands.
|
|
217
|
+
|
|
218
|
+
## 6. Layout: Flexbox v2
|
|
219
|
+
|
|
220
|
+
Bring `EVGLayout` closer to CSS Flexbox without rewriting callers.
|
|
221
|
+
|
|
222
|
+
### 6.1 Must-have
|
|
223
|
+
|
|
224
|
+
| Feature | Notes |
|
|
225
|
+
| --- | --- |
|
|
226
|
+
| ~~`flex-grow` / `flex-basis`~~ ✅ | `flex` shorthand parses `1`, `1 1 auto`, `2 0 120px` |
|
|
227
|
+
| `flex-wrap` | Row/column wrap with correct line packing |
|
|
228
|
+
| `min-width` / `max-width` / `min-height` / `max-height` | Clamp before grow/shrink |
|
|
229
|
+
| Intrinsic text width | Fix ISSUES #1: content-sized labels in `row` |
|
|
230
|
+
| `gap` on both axes | Already partly present; define wrapping interaction |
|
|
231
|
+
| Shorthand `flex` | Parse `1`, `1 1 auto`, etc. |
|
|
232
|
+
|
|
233
|
+
### 6.2 Should-have soon after
|
|
234
|
+
|
|
235
|
+
- `align-content` for wrapped lines
|
|
236
|
+
- `align-items: baseline` (depends on font ascent)
|
|
237
|
+
- Deprecate dual naming: prefer CSS names; map legacy `align` / `verticalAlign`
|
|
238
|
+
|
|
239
|
+
### 6.3 Algorithm note
|
|
240
|
+
|
|
241
|
+
Keep one deterministic pass (or two-pass grow/shrink) in Ranger. Do not call into browser layout for PDF. HTML preview may either:
|
|
242
|
+
|
|
243
|
+
- **A (preferred for parity):** use precomputed EVG frames (absolute positions), or
|
|
244
|
+
- **B:** emit real CSS flex and **diff** against EVG frames in tests
|
|
245
|
+
|
|
246
|
+
For print trust, A or test-gated B is required; never “HTML looks fine so PDF must be fine”.
|
|
247
|
+
|
|
248
|
+
## 7. Layout: Grid v1
|
|
249
|
+
|
|
250
|
+
Target photo-book pages, not full CSS Grid Level 2.
|
|
251
|
+
|
|
252
|
+
### 7.1 v1 features
|
|
253
|
+
|
|
254
|
+
- `display: grid`
|
|
255
|
+
- `grid-template-columns` / `grid-template-rows` with:
|
|
256
|
+
- fixed (`120px`, `40%`)
|
|
257
|
+
- `fr`
|
|
258
|
+
- `repeat(n, 1fr)` (limited)
|
|
259
|
+
- `gap` / `row-gap` / `column-gap`
|
|
260
|
+
- child placement: auto flow row, plus `grid-column: span 2` / explicit line numbers (simple)
|
|
261
|
+
- stretch alignment default for images
|
|
262
|
+
|
|
263
|
+
### 7.2 Example (replaces nested HOC grids)
|
|
264
|
+
|
|
265
|
+
```tsx
|
|
266
|
+
<View className="page-grid">
|
|
267
|
+
<Image className="photo" src={a} />
|
|
268
|
+
<Image className="photo" src={b} />
|
|
269
|
+
<Image className="photo span-2" src={c} />
|
|
270
|
+
</View>
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
```css
|
|
274
|
+
.page-grid {
|
|
275
|
+
display: grid;
|
|
276
|
+
grid-template-columns: 1fr 1fr;
|
|
277
|
+
grid-template-rows: 1fr 1fr;
|
|
278
|
+
gap: 15px;
|
|
279
|
+
width: 100%;
|
|
280
|
+
height: 100%;
|
|
281
|
+
}
|
|
282
|
+
.span-2 { grid-column: span 2; }
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### 7.3 Beyond v1 — landed
|
|
286
|
+
|
|
287
|
+
- **`minmax(min, max)`** sizes as its max and clamps into the range. A track
|
|
288
|
+
that hits a bound is pinned and the space it did not take is offered to the
|
|
289
|
+
rest, the same freeze-and-redistribute the flex axis uses — so
|
|
290
|
+
`minmax(120px, 1fr)` holds its floor without silently starving a neighbour.
|
|
291
|
+
Works inside `repeat()`, which is how a responsive album grid is written.
|
|
292
|
+
- **`grid-template-areas`** draws the page as a picture of names, and
|
|
293
|
+
`grid-area` claims a region. Each name must form a rectangle; a ragged or
|
|
294
|
+
bent one is reported rather than guessed at. With no explicit column template
|
|
295
|
+
the column count comes from the picture.
|
|
296
|
+
- **`grid-auto-flow: row dense`** restarts the scan from the top for each item,
|
|
297
|
+
so a later small item backfills a hole a wider one left behind. The default
|
|
298
|
+
only moves forward, which keeps source order but can leave gaps.
|
|
299
|
+
- **`subgrid`, both axes** — `grid-template-columns: subgrid` and
|
|
300
|
+
`grid-template-rows: subgrid` adopt the enclosing grid's tracks for the span
|
|
301
|
+
the element occupies, so nested cards line their columns *and* their internal
|
|
302
|
+
rows up with each other instead of each dividing its own box. Declared with
|
|
303
|
+
nothing to inherit from, either falls back — one full-width column, or
|
|
304
|
+
content-sized rows — and says so. See Phase 4.5.
|
|
305
|
+
|
|
306
|
+
- **Named grid lines** — `[full-start] 1fr [main] 2fr [main-end]` labels the
|
|
307
|
+
lines between tracks, and `grid-column: main-start / main-end` places against
|
|
308
|
+
them. One line may carry several names (`[a b]`); a name is resolved against
|
|
309
|
+
the container's template, so the placement is parsed from the item and
|
|
310
|
+
pointed at real lines by the layout, which is the only place that sees both.
|
|
311
|
+
A name the template does not define leaves the item auto-placed and is
|
|
312
|
+
reported.
|
|
313
|
+
|
|
314
|
+
- **Intrinsic tracks** — `auto` sizes to the content of the items placed in it,
|
|
315
|
+
and `fit-content(limit)` caps that without ever going below min-content. See
|
|
316
|
+
Phase 4.4.
|
|
317
|
+
|
|
318
|
+
Still out of scope: the intrinsic size of a **container** item, which needs a
|
|
319
|
+
recursive pass — see the end of Phase 4.4.
|
|
320
|
+
|
|
321
|
+
## 8. Shared text engine API (contract)
|
|
322
|
+
|
|
323
|
+
Centralize in one module used by layout + PDF + raster (+ HTML measurement debug):
|
|
324
|
+
|
|
325
|
+
```text
|
|
326
|
+
resolveFace(family, weight, style) -> FontFace
|
|
327
|
+
measureRun(face, text, fontSize) -> { width, ascent, descent, lineHeight }
|
|
328
|
+
breakLines(face, text, fontSize, maxWidth, lineHeight) -> [Line]
|
|
329
|
+
Line: { text, width, ascent, descent }
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
Rules:
|
|
333
|
+
|
|
334
|
+
- Layout heights for text nodes come only from `breakLines`
|
|
335
|
+
- PDF draw uses the same lines (no second wrap pass with different widths)
|
|
336
|
+
- Raster draw uses the same face + fontSize scaling (`unitsPerEm`)
|
|
337
|
+
- Snapshot tests lock glyph advances for a fixture string/font
|
|
338
|
+
|
|
339
|
+
## 9. Target parity matrix (definition of done)
|
|
340
|
+
|
|
341
|
+
| Concern | HTML preview | PDF | Raster |
|
|
342
|
+
| --- | --- | --- | --- |
|
|
343
|
+
| Face files | `@font-face` same TTF | embedded TTF | parsed TTF outlines |
|
|
344
|
+
| Advance widths | measured via shared engine (or DOM diff ≤ tolerance) | shared engine | shared engine |
|
|
345
|
+
| Line breaks | same breaks as PDF | shared engine | shared engine |
|
|
346
|
+
| Flex/grid frames | same boxes (or asserted) | EVGLayout | EVGLayout |
|
|
347
|
+
| Theme swap | change stylesheet only | same | same |
|
|
348
|
+
|
|
349
|
+
Tolerance: integer pixel/pt rounding policy documented (e.g. round half-up to 1/100 pt for PDF).
|
|
350
|
+
|
|
351
|
+
## 10. Phased delivery
|
|
352
|
+
|
|
353
|
+
### Phase 0 — Font/layout correctness foundation ✅
|
|
354
|
+
|
|
355
|
+
Landed last, which is why the earlier phases carried a caveat. The starting
|
|
356
|
+
state was worse than this plan assumed: **no target was measuring with real font
|
|
357
|
+
metrics at all.**
|
|
358
|
+
|
|
359
|
+
- The PDF tool's fonts directory was `./gallery/pdf_writer/Fonts` — a path that
|
|
360
|
+
does not exist (the files are under `assets/fonts`) and resolved against the
|
|
361
|
+
process working directory rather than the document. Every `loadFont` failed,
|
|
362
|
+
so `FontManager.measureText` fell through to `strlen * fontSize * 0.5`.
|
|
363
|
+
- The HTML tool never constructed a `FontManager` at all and measured with
|
|
364
|
+
`fontSize * 0.55`.
|
|
365
|
+
- The PNG tool loaded fonts, painted real glyph outlines, and laid out with the
|
|
366
|
+
heuristic measurer.
|
|
367
|
+
|
|
368
|
+
So preview and print disagreed with each other *and* with the faces being
|
|
369
|
+
painted. Measured against Chromium loading the same TTF, the title in
|
|
370
|
+
`test_theme.tsx` was 8.31px (4.3%) too wide.
|
|
371
|
+
|
|
372
|
+
What landed:
|
|
373
|
+
|
|
374
|
+
- **`EVGTextEngine.rgr`** — the §8 contract (`measureRun`, `breakLines`,
|
|
375
|
+
`lineCount`, `maxLineWidth`) in one place. There were four wrap
|
|
376
|
+
implementations; layout's and the PDF's broke lines in different places, so
|
|
377
|
+
the height layout reserved did not match the lines paint drew. The PDF's
|
|
378
|
+
algorithm won (it measures the whole candidate line, including the real space
|
|
379
|
+
advance, instead of adding a guessed `fontSize * 0.3`) and everything else
|
|
380
|
+
calls it.
|
|
381
|
+
- **The element's own font family is threaded through.** Layout passed a
|
|
382
|
+
hardcoded `"Helvetica"` to the measurer for every string, so a document set in
|
|
383
|
+
Cinzel was laid out with Helvetica widths even once a TTF measurer was
|
|
384
|
+
installed.
|
|
385
|
+
- **`EVGFontSetup.rgr`** — one font-resolution path for all four tools,
|
|
386
|
+
anchored on the document's directory. `-fonts DIR` overrides it and is
|
|
387
|
+
authoritative rather than merely first, since falling back after a typo is
|
|
388
|
+
exactly the silent substitution this is meant to prevent.
|
|
389
|
+
- **Honesty instead of fallback.** `FontManager.hasFont` distinguishes a real
|
|
390
|
+
hit from `getFont`'s substitution chain; measurers declare `isFontAccurate`;
|
|
391
|
+
the engine reports each unbacked family once and `-strict-fonts` refuses to
|
|
392
|
+
write output rather than guessing.
|
|
393
|
+
- **ISSUES #1 closed**, with the family and measurement caveats fixed.
|
|
394
|
+
|
|
395
|
+
Verification:
|
|
396
|
+
|
|
397
|
+
```
|
|
398
|
+
bash gallery/pdf_writer/test/run_fonts.sh
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
`font_metrics_test.rgr` locks advance widths, vertical metrics and wrap
|
|
402
|
+
positions against the real TTFs (29 assertions). `font_parity.js` renders a page
|
|
403
|
+
in Chromium with the same faces via `@font-face` and compares EVG's boxes to the
|
|
404
|
+
browser's — the check that catches EVG agreeing with itself while both sides are
|
|
405
|
+
wrong. Worst delta is now **0.375px against 8.31px before**.
|
|
406
|
+
|
|
407
|
+
### Phase 1 — Flexbox v2
|
|
408
|
+
|
|
409
|
+
- ~~row/column grow, main-axis shrink, wrap gating, `gap`, `alignItems: stretch`~~
|
|
410
|
+
(landed with the engine unification — see §11.1)
|
|
411
|
+
- ~~`flex-basis` and the `flex` shorthand~~ — landed. `flex: 1` sets basis 0, so
|
|
412
|
+
an item shares the line even when it also carries a width; `flex: 2 1 120px`,
|
|
413
|
+
`flex: auto` and `flex: 120px` parse. This removed the wart both bundled
|
|
414
|
+
themes had to document.
|
|
415
|
+
- ~~Widen the JSX attribute surface~~ — see Phase 2.5.
|
|
416
|
+
- ~~Real per-item `flex-shrink` factors~~ — landed. Overflow is shared by
|
|
417
|
+
`flex-shrink x size`, so `flex-shrink: 0` holds an item's size while its
|
|
418
|
+
siblings absorb the whole overflow. With every factor at the CSS default of 1
|
|
419
|
+
this is the same uniform scale as before, which is why no existing page moved.
|
|
420
|
+
- ~~min/max clamped in the right order relative to grow/shrink~~ — landed.
|
|
421
|
+
Limits are resolved inside the distribution: an item that hits one is frozen
|
|
422
|
+
there and the space it did not use is offered back to the others, instead of
|
|
423
|
+
being clamped afterwards and leaving a hole in the row.
|
|
424
|
+
- ~~`align-content` for wrapped lines~~ — landed: `flex-start`, `flex-end`,
|
|
425
|
+
`center`, `space-between`, `space-around`, `space-evenly`. It applies only
|
|
426
|
+
when the content wrapped and the container height is definite, following the
|
|
427
|
+
same rule as the rest of the engine. `stretch` would have to grow each line
|
|
428
|
+
and re-lay its children out, so it currently behaves as `flex-start`.
|
|
429
|
+
- ~~`wrap-reverse`~~ — landed: the lines stack from the far edge, with the wrap
|
|
430
|
+
points unchanged.
|
|
431
|
+
- ~~`align-content: stretch`~~ — landed: each line grows by an equal share of
|
|
432
|
+
the spare space, and items that did not ask for a specific height grow with
|
|
433
|
+
their line and re-lay their own children inside the taller box.
|
|
434
|
+
- Update `PhotoLayouts` only where behavior changes
|
|
435
|
+
|
|
436
|
+
### Phase 2 — Style layer ✅
|
|
437
|
+
|
|
438
|
+
Landed. Open decision #1 was settled in favour of a **real CSS subset parser**
|
|
439
|
+
rather than JSON style maps: the property names were already CSS-shaped, and
|
|
440
|
+
`EVGElement.setAttribute` already accepted both `font-size` and `fontSize`, so
|
|
441
|
+
the parser only had to tokenize and dispatch — no second authoring vocabulary.
|
|
442
|
+
|
|
443
|
+
- `EVGStyleSheet.rgr` parses `.class`, `.theme-<name> .class`, selector lists
|
|
444
|
+
and `/* comments */`, and applies rules over a tree
|
|
445
|
+
- Cascade: unscoped class < theme-scoped class < inline attributes, with source
|
|
446
|
+
order breaking ties inside each group
|
|
447
|
+
- Inline precedence is explicit, not positional: front-ends call
|
|
448
|
+
`EVGElement.markInline()` for every authored attribute and the applier skips
|
|
449
|
+
those properties. `toKebab` normalizes so `fontSize` and `font-size` are one key
|
|
450
|
+
- `-css FILE` (repeatable) and `-theme NAME` on the PDF and HTML tools, via the
|
|
451
|
+
shared `EVGStyleLoader` so both targets resolve identically
|
|
452
|
+
- Unsupported selectors are collected and printed, not silently dropped
|
|
453
|
+
- `examples/themes/classic.css` + `minimal.css` declare the same class names, so
|
|
454
|
+
`examples/test_theme.tsx` swaps look with no TSX edit:
|
|
455
|
+
|
|
456
|
+
```
|
|
457
|
+
evg-html test_theme.tsx out.html -css themes/classic.css -theme classic
|
|
458
|
+
evg-html test_theme.tsx out.html -css themes/minimal.css -theme minimal
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
Not in this subset: element/ID selectors, multi-level descendants, pseudo-classes,
|
|
462
|
+
`!important`, shorthand expansion beyond what `setAttribute` already does.
|
|
463
|
+
|
|
464
|
+
### Phase 2.5 — Attribute surface ✅
|
|
465
|
+
|
|
466
|
+
Every phase turned up a property that parsed but never reached the engine,
|
|
467
|
+
because a property had to appear in **two** independent whitelists —
|
|
468
|
+
`parseAttributes` in `JSXToEVG.rgr` for JSX attributes, and
|
|
469
|
+
`EVGElement.setAttribute` for the stylesheet and `ComponentEngine` paths. Missing
|
|
470
|
+
from either meant silently dropped on that path only:
|
|
471
|
+
|
|
472
|
+
| Property | Was dropped from | Found during |
|
|
473
|
+
| --- | --- | --- |
|
|
474
|
+
| `gap` | JSX attributes | Phase 1 |
|
|
475
|
+
| `className` | JSX attributes (compared to `"className"`, arrives as `"class-name"`) | Phase 2 |
|
|
476
|
+
| `display` | `setAttribute` — so a stylesheet could set every grid property and still lay out as a block | Phase 3 |
|
|
477
|
+
| `justify-content`, `align-items`, `border-width`, `border-color` | JSX attributes | closing this gap |
|
|
478
|
+
|
|
479
|
+
`parseAttributes` now calls `setAttribute` first and the special cases refine
|
|
480
|
+
the result, so the two surfaces cannot drift: anything the element understands
|
|
481
|
+
is reachable from JSX. Unknown names are ignored, so component props like `key`
|
|
482
|
+
pass through harmlessly.
|
|
483
|
+
|
|
484
|
+
`gallery/pdf_writer/test/attrs_test.rgr` is the guard — it sets each property as
|
|
485
|
+
a JSX attribute and asserts it landed, so a refactor that reintroduces a
|
|
486
|
+
whitelist fails there rather than in someone's print run.
|
|
487
|
+
|
|
488
|
+
The examples show the cost of the old behaviour: `test_scandinavian`'s cover page
|
|
489
|
+
authored `justifyContent="center" alignItems="center"` and was never centred, and
|
|
490
|
+
`test_simple` authored a border that never drew.
|
|
491
|
+
|
|
492
|
+
### Phase 3 — Grid v1 ✅
|
|
493
|
+
|
|
494
|
+
Landed in `EVGGrid.rgr` (track lists + placement parsing) and
|
|
495
|
+
`EVGLayout.layoutGrid` (auto-flow placement). `layoutChildren` branches to it on
|
|
496
|
+
`display: grid`, keeping the same "returns content height" contract, so a grid
|
|
497
|
+
nests inside flex and vice versa.
|
|
498
|
+
|
|
499
|
+
- `grid-template-columns` / `grid-template-rows`: fixed px, `%`, `fr`,
|
|
500
|
+
`repeat(n, …)`. Fixed and percentage tracks are taken first, `fr` splits the
|
|
501
|
+
remainder in proportion
|
|
502
|
+
- `gap`, plus `row-gap` / `column-gap` overriding it per axis
|
|
503
|
+
- `grid-column` / `grid-row` accepting `span N`, `N`, `N / M`, `N / span M`
|
|
504
|
+
- Auto-flow row placement over an occupancy map, so column *and* row spans
|
|
505
|
+
reserve their cells and later items flow around them
|
|
506
|
+
- Items stretch to their cell by default (§7.1); an explicit `height` still wins,
|
|
507
|
+
because `layoutElement` only consults the stretch height when `height.isSet`
|
|
508
|
+
is false
|
|
509
|
+
- Row sizing mirrors the Phase 1 auto-height rule: `grid-template-rows` is only
|
|
510
|
+
used when the container's height is definite. Otherwise rows are
|
|
511
|
+
content-sized from a measuring pass, so a grid in normal document flow grows
|
|
512
|
+
to its content instead of being squeezed into a stale inner height
|
|
513
|
+
- A grid with no column template is a single full-width column, rather than
|
|
514
|
+
collapsing to zero
|
|
515
|
+
|
|
516
|
+
`components/PhotoLayouts.tsx` `FourPhotoGrid` is now a real 2×2 grid. The old
|
|
517
|
+
version faked it with `48%` widths and `4%` margins, which made the horizontal
|
|
518
|
+
and vertical gutters different sizes; one `gap` value now drives both axes.
|
|
519
|
+
|
|
520
|
+
`examples/themes/album.css` + `examples/test_album_grid.tsx` are the print
|
|
521
|
+
fixtures — one tree, three compositions:
|
|
522
|
+
|
|
523
|
+
```
|
|
524
|
+
evg-html test_album_grid.tsx a4.html -css themes/album.css -theme album -w 595 -h 842
|
|
525
|
+
evg-html test_album_grid.tsx land.html -css themes/album.css -theme album -w 842 -h 595
|
|
526
|
+
evg-html test_album_grid.tsx contact.html -css themes/album.css -theme contact -w 595 -h 842
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
`grid-template-areas`, `grid-auto-flow: dense`, `minmax()` and column `subgrid`
|
|
530
|
+
landed afterwards — see §7.3. Still out of scope: row `subgrid`, `fit-content()`
|
|
531
|
+
and named lines. `auto` in a track list is accepted but behaves as `1fr` —
|
|
532
|
+
sizing it properly needs per-track content measurement.
|
|
533
|
+
|
|
534
|
+
### Phase 4 — Baseline + polish ✅
|
|
535
|
+
|
|
536
|
+
**`align-items: baseline`.** Text nodes record `calculatedBaseline` (leading +
|
|
537
|
+
ascent from the real face) and `calculatedDescent`; containers inherit the first
|
|
538
|
+
in-flow child's baseline, so wrapping a label in a `View` does not break the
|
|
539
|
+
alignment. A box with no text baseline aligns on its bottom margin edge, which
|
|
540
|
+
is what CSS does and what makes an image sit on a caption's baseline. Row
|
|
541
|
+
alignment takes the max baseline offset and shifts each item down to meet it.
|
|
542
|
+
This is why the rule waited for Phase 0 — it is meaningless without real ascents.
|
|
543
|
+
|
|
544
|
+
**Encoding honesty (§4.1 rule 8).** The starting point was not the expected
|
|
545
|
+
"unsupported codepoint becomes `?`" — it was that `buffer_to_string` builds a
|
|
546
|
+
string with one character per *byte*, i.e. it reads UTF-8 source as Latin-1. So
|
|
547
|
+
`ä` arrived as two characters and `—` as three. Layout measured two or three
|
|
548
|
+
glyph advances for one character, and the PDF wrote each byte as its own WinAnsi
|
|
549
|
+
escape, printing `ä` where the source said `ä`. Nothing complained, because the
|
|
550
|
+
mangled bytes are all inside the byte range.
|
|
551
|
+
|
|
552
|
+
`Utf8.decode` now runs at every source-read boundary (TSX, components,
|
|
553
|
+
stylesheets). Text carries real codepoints, so measurement counts real glyphs,
|
|
554
|
+
Latin-1-range characters encode correctly, and characters genuinely outside
|
|
555
|
+
WinAnsi are reported with their codepoint and context instead of being hidden:
|
|
556
|
+
|
|
557
|
+
```
|
|
558
|
+
Encoding warning: U+2014 in "Hyvää yötä — Kaivopuisto" is outside WinAnsi and was written as '?'
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
`-strict-fonts` makes that fatal. Invalid byte sequences pass through unchanged,
|
|
562
|
+
so a file that really is Latin-1 keeps working.
|
|
563
|
+
|
|
564
|
+
**Bleed-aware page boxes.** `-bleed PT` grows the sheet by that much on every
|
|
565
|
+
side, translates the page content into the middle of it, and declares
|
|
566
|
+
`TrimBox`/`BleedBox` so a printer knows where the finished page is cut. Layout
|
|
567
|
+
keeps working in trim coordinates and knows nothing about bleed. With no bleed
|
|
568
|
+
the output is byte-identical to before — a single `MediaBox`, no translate.
|
|
569
|
+
|
|
570
|
+
```
|
|
571
|
+
evg-pdf album.tsx out.pdf -bleed 8.5 # 3mm trade bleed
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
Verification: `bash gallery/pdf_writer/test/run_print.sh` (23 assertions covering
|
|
575
|
+
UTF-8 decoding, WinAnsi detection and page boxes at both orientations), plus the
|
|
576
|
+
baseline cases in `evg_test`.
|
|
577
|
+
|
|
578
|
+
### Phase 4.1 — CSS length units ✅
|
|
579
|
+
|
|
580
|
+
Checking the units against a browser turned up three things that were wrong in
|
|
581
|
+
ways nothing would have complained about.
|
|
582
|
+
|
|
583
|
+
**`font-size` never inherited.** Every element was constructed carrying a 14px
|
|
584
|
+
font-size, so `inheritProperties` copied the parent's size in and the default
|
|
585
|
+
immediately overwrote it — a `font-size` only ever applied to the element that
|
|
586
|
+
declared it, never to anything below. The root had the opposite problem: it has
|
|
587
|
+
no parent, so `inheritProperties` never ran on it at all and a size set on a
|
|
588
|
+
`Page` or `Print` was dropped outright. `fontSize` now starts *unset*, layout
|
|
589
|
+
fills it in from the inherited size (remembering that it did, so a re-layout
|
|
590
|
+
does not mistake its own fill-in for an authored value), and `EVGLayout.layout`
|
|
591
|
+
applies the root's own size before anything descends. This is what `em` needs,
|
|
592
|
+
and it fixes inherited text size at the same time.
|
|
593
|
+
|
|
594
|
+
**`2rem` silently meant `2em`.** The parser tested the two-character suffix
|
|
595
|
+
first, so `rem` was chopped to `"2r"` — and `to_double` stops at the first
|
|
596
|
+
character it cannot use, so that reads as 2. `rem` is now a unit of its own,
|
|
597
|
+
resolved against the root's font size, which is threaded down the tree
|
|
598
|
+
alongside the inherited one (a unit cannot reach the root on its own, so
|
|
599
|
+
whoever resolves it hands the value over).
|
|
600
|
+
|
|
601
|
+
**Any unknown unit became pixels.** The same `to_double` behaviour meant `10vw`
|
|
602
|
+
resolved to 10px and `calc(100% - 20px)` to 100px. An unrecognised suffix now
|
|
603
|
+
leaves the length unset — i.e. `auto`, which is what a browser does with a
|
|
604
|
+
declaration it cannot parse. `ch`/`ex` (they need font metrics the unit layer
|
|
605
|
+
cannot reach) and `calc()` stay out of scope, but they now stay out loudly.
|
|
606
|
+
|
|
607
|
+
A suffix test alone was not enough, and a later pass found the hole: the
|
|
608
|
+
NUMBER has to be a number too. `to_double` stops at the first character it
|
|
609
|
+
cannot use, so `10vmin` — which ends in `in`, one of the absolute units — came
|
|
610
|
+
out as ten inches, 960 pixels, for a unit this layer does not support. Every
|
|
611
|
+
branch now checks that what precedes the suffix is digits before it believes
|
|
612
|
+
the suffix.
|
|
613
|
+
|
|
614
|
+
**`vw` and `vh` are in.** The line above used to say they were out because a
|
|
615
|
+
print page has no viewport, and that was the wrong way round: a print page has
|
|
616
|
+
a *page area*, which is exactly what CSS says viewport-percentage lengths mean
|
|
617
|
+
in paged media, and it is already what the print renderers hand `EVGLayout` as
|
|
618
|
+
its page size (the section less two margins). So the same rule reads correctly
|
|
619
|
+
on paper and on a screen with no print-specific code: `vw`/`vh` resolve against
|
|
620
|
+
`EVGLayout.pageWidth` / `pageHeight`, published onto the root and inherited the
|
|
621
|
+
way the root font size is.
|
|
622
|
+
|
|
623
|
+
What they buy over `%` is the case `%` cannot express: `height: 100%` needs a
|
|
624
|
+
chain of ancestors that all have a definite height, so a page that wants to be
|
|
625
|
+
as tall as its window has to be handed that number by its host. The ui
|
|
626
|
+
gallery's dashboard was: its page, sidebar and hairline stated a constant
|
|
627
|
+
`1420px`, which looked full in every window shorter than that and stopped two
|
|
628
|
+
thirds of the way down an Android tablet in portrait. They say `100vh` now.
|
|
629
|
+
|
|
630
|
+
Verification: `npm run evg:viewport:test` — 22 checks over the parse, the
|
|
631
|
+
arithmetic, a nested `100vh` that is the page rather than the 300-pixel box it
|
|
632
|
+
sits in, box lengths in `vh`, and a page area of A4 less two 40pt margins.
|
|
633
|
+
|
|
634
|
+
The absolute print units — `pt`, `pc`, `in`, `mm`, `cm` — landed in the same
|
|
635
|
+
pass. They are pinned to CSS's reference pixel (`1in = 96px`) and folded to px
|
|
636
|
+
at parse time, so nothing downstream has to know about them. `12pt` used to
|
|
637
|
+
measure 12px.
|
|
638
|
+
|
|
639
|
+
Verification: 12 unit fixtures in the box-model gate (121 boxes, up from 60),
|
|
640
|
+
covering `rem` against a root size that differs from the local one, `em` on
|
|
641
|
+
`font-size` itself chaining down a tree, the box model in `em`, gaps in `rem`,
|
|
642
|
+
and all five absolute units resolving to the same 96px. All 22 examples that
|
|
643
|
+
render still produce byte-identical HTML.
|
|
644
|
+
|
|
645
|
+
### Phase 4.2 — Saying so out loud ✅
|
|
646
|
+
|
|
647
|
+
Three things the engine could not do, each of which it had been doing quietly.
|
|
648
|
+
|
|
649
|
+
**WinAnsi is not Latin-1.** The encoder's repertoire check was `codepoint >
|
|
650
|
+
255`, which is the Latin-1 boundary. WinAnsi is CP1252, and the band Latin-1
|
|
651
|
+
leaves as control codes is exactly where CP1252 keeps the punctuation a book
|
|
652
|
+
sets: `—` `–` `“” ‘’` `…` `•` `€` `†` `‰`. Every one of them was refused,
|
|
653
|
+
written as `?`, and — under `-strict-fonts` — fatal, on text the format can
|
|
654
|
+
carry perfectly well. `Utf8.toWinAnsi` / `fromWinAnsi` now hold the real
|
|
655
|
+
mapping; the encoder uses it, the font's `/Widths` array looks each glyph up by
|
|
656
|
+
its true codepoint instead of by the byte, and the `/ToUnicode` cmap gained the
|
|
657
|
+
27 `bfchar` entries for the band so the text also extracts correctly.
|
|
658
|
+
|
|
659
|
+
Fixing that exposed a second bug behind it. The JSX parser hands text back one
|
|
660
|
+
token at a time, so `100% sure` arrives as three fragments and the joiner
|
|
661
|
+
decides what goes between them. It was guessing from a whitelist of characters
|
|
662
|
+
allowed to follow a word with no space — `, . ! ? : ; - ) ]` — which got
|
|
663
|
+
`Hello, world!` right and mangled everything else: `100 % sure`, `a / b`,
|
|
664
|
+
`a ( b) c`, `Kämp- hotellissa`, `Gallen- Kallelan`, `usePrintSettings ()`, and
|
|
665
|
+
every curly quote as `“ x ”`. The tokens carry source offsets, so adjacency is
|
|
666
|
+
a fact to read, not a guess: a space is written iff the next token starts past
|
|
667
|
+
where the previous one ended. Twelve of the 22 rendering examples changed, all
|
|
668
|
+
of them corrections.
|
|
669
|
+
|
|
670
|
+
**Grid rejections were invisible.** `fit-content()`, row `subgrid`, a bent
|
|
671
|
+
`grid-template-areas` and a missing `grid-area` name all set an error that was
|
|
672
|
+
handed to `this.log()` — a no-op unless `debug` is on. In a normal run the page
|
|
673
|
+
simply came out with the wrong number of columns. `EVGLayout` now collects
|
|
674
|
+
these, deduplicated, and every tool prints them:
|
|
675
|
+
|
|
676
|
+
```
|
|
677
|
+
Layout warning: grid-template-columns: Unsupported track size: fit-content(100px)
|
|
678
|
+
Layout warning: grid-template-rows: Unsupported track size: subgrid
|
|
679
|
+
```
|
|
680
|
+
|
|
681
|
+
**Named grid lines said nothing at all.** `grid-column: sidebar` went through
|
|
682
|
+
`to_double`, came back 0, and 0 is auto — indistinguishable from not having
|
|
683
|
+
written anything. `EVGGridPlacement` now rejects any token that is neither a
|
|
684
|
+
positive line number nor `span N` (which also catches negative line numbers,
|
|
685
|
+
CSS's count-from-the-end form, equally unsupported), keeps the auto placement,
|
|
686
|
+
and reports it.
|
|
687
|
+
|
|
688
|
+
The raster tool reported none of this before — not even font warnings — and now
|
|
689
|
+
reports both.
|
|
690
|
+
|
|
691
|
+
Verification: 12 new WinAnsi assertions plus a round-trip over all 27 assigned
|
|
692
|
+
codes, 12 text-spacing assertions, and 12 grid-warning assertions covering the
|
|
693
|
+
report, the dedup, and silence on a grid the engine fully understands.
|
|
694
|
+
|
|
695
|
+
> **Compiling is not a passing build.** `node dist/rgrc.js` prints
|
|
696
|
+
> `Compilation FAILED` and still exits 0, so `npm run <tool>:compile && echo OK`
|
|
697
|
+
> reports success on a broken tool. Grep the output for `Compilation FAILED`.
|
|
698
|
+
> This cost a round here: a tool that had not rebuilt looked like a feature
|
|
699
|
+
> that had not worked.
|
|
700
|
+
|
|
701
|
+
### Phase 4.3 — Kerning ✅
|
|
702
|
+
|
|
703
|
+
The last open rule in §4.1, and the one that needed the most care to not make
|
|
704
|
+
things worse.
|
|
705
|
+
|
|
706
|
+
**Reading it.** `TrueTypeFont` walks GPOS: FeatureList for a `kern` feature,
|
|
707
|
+
its lookups, and the pair-adjustment subtables underneath — including
|
|
708
|
+
LookupType 9 (Extension), which large faces use to reach past the 64K offset
|
|
709
|
+
limit, so skipping it would have missed kerning in exactly the fonts that need
|
|
710
|
+
it most. Both PairPos formats are read: format 1 (per-glyph pair sets, binary
|
|
711
|
+
searched) and format 2 (class pairs). Coverage and ClassDef tables are binary
|
|
712
|
+
searched rather than expanded — a class subtable is a few hundred bytes that
|
|
713
|
+
expands to tens of thousands of pairs, almost none of which a given page uses.
|
|
714
|
+
|
|
715
|
+
Two things the browser snapshot caught that reading the spec alone did not:
|
|
716
|
+
|
|
717
|
+
- **Subtables within one lookup are first-match-wins**, not additive. Summing
|
|
718
|
+
every subtable double-counted any pair listed in more than one, which made
|
|
719
|
+
Noto Sans measure narrower than Chromium draws it. Separate lookups still
|
|
720
|
+
accumulate.
|
|
721
|
+
- **A face with GPOS is positioned by GPOS alone.** Open Sans has a GPOS table
|
|
722
|
+
with no `kern` feature *and* 18694 legacy `kern` pairs. Falling back to those
|
|
723
|
+
made EVG kern a run a browser leaves alone. OpenType says GPOS wins outright;
|
|
724
|
+
the fallback now only applies when there is no GPOS table at all.
|
|
725
|
+
|
|
726
|
+
**Painting it.** Measuring kerned while painting unkerned would have been worse
|
|
727
|
+
than not kerning: the box right and the ink wrong. A PDF viewer advances by the
|
|
728
|
+
`/Widths` entry and kerns nothing itself, so the renderer emits a `TJ` array
|
|
729
|
+
instead of `Tj` when the face kerns — `[(H) 15 (e) 10 (lsinki, 20) 25 (2) 15
|
|
730
|
+
(4)] TJ`, which is 65/1000 em at 30px, exactly the 1.95px the measurement
|
|
731
|
+
moved. A run that does not kern still emits a plain `Tj`. The raster pen kerns
|
|
732
|
+
between glyphs the same way; A/B-ing the PNG shows the right edge of that
|
|
733
|
+
Cinzel line moving 2px left with the left edge unchanged, which is kerning
|
|
734
|
+
between glyphs and not a shifted origin.
|
|
735
|
+
|
|
736
|
+
Results:
|
|
737
|
+
|
|
738
|
+
| | before | after |
|
|
739
|
+
| --- | --- | --- |
|
|
740
|
+
| worst vs browser (24 snapshot fixtures) | 1.94px | **0.015px** |
|
|
741
|
+
| worst vs live Chromium page | 0.375px | **0.006px** |
|
|
742
|
+
|
|
743
|
+
All 19 rendering examples that contain text moved, every diff a width or a
|
|
744
|
+
position — no text and no structure changed.
|
|
745
|
+
|
|
746
|
+
### Phase 4.4 — Grid, against a browser ✅
|
|
747
|
+
|
|
748
|
+
The box-model parity gate learned `display: grid`, so the grid work stopped
|
|
749
|
+
being checked against hand-computed expectations and started being checked
|
|
750
|
+
against Chromium. Sixteen fixtures: fr tracks, mixed px/fr/%, gaps, `repeat()`,
|
|
751
|
+
spans, explicit lines, named lines, `minmax()` both clamped and not, percentage
|
|
752
|
+
tracks, and a padded container. The gate is now **894 assertions over 161
|
|
753
|
+
boxes**, and it needs no browser to run.
|
|
754
|
+
|
|
755
|
+
The existing grid passed all of it on the first recording. That is the useful
|
|
756
|
+
kind of result — it says the earlier phases were right, and it is what made the
|
|
757
|
+
two genuine gaps stand out.
|
|
758
|
+
|
|
759
|
+
**`auto` was a disguised `1fr`.** Predictable, documented, and not what CSS
|
|
760
|
+
does: an `auto` track sizes to the content of the items in it, and only then is
|
|
761
|
+
leftover space handed to the `fr` tracks. Column sizing is now deferred until
|
|
762
|
+
after placement — the tracks cannot be sized until it is known what lands in
|
|
763
|
+
them — and each intrinsic track takes the widest item placed in it.
|
|
764
|
+
|
|
765
|
+
**`fit-content(limit)`** is the same track with a ceiling on the max-content
|
|
766
|
+
side, floored at min-content. That floor is the whole subtlety: Chromium
|
|
767
|
+
measures `fit-content(100px)` around a 200px box as **200**, because a box with
|
|
768
|
+
a definite width cannot be squeezed and the clamp would push it out of its own
|
|
769
|
+
cell. The limit only bites on something that *can* be squeezed — text — which
|
|
770
|
+
is why `EVGTextEngine` gained `minLineWidth` (break at every opportunity, take
|
|
771
|
+
the widest line: CSS's min-content) to sit beside `maxLineWidth`.
|
|
772
|
+
|
|
773
|
+
What contributes to an intrinsic track is deliberately narrow: an item with a
|
|
774
|
+
definite width (which is both its min- and max-content size) and a text leaf.
|
|
775
|
+
A container item contributes nothing rather than a guess — sizing one to its
|
|
776
|
+
subtree needs a real recursive intrinsic pass, and a made-up number would
|
|
777
|
+
silently misplace every neighbour instead of merely leaving a track narrow.
|
|
778
|
+
Items spanning several tracks are left out for the same reason: there is no one
|
|
779
|
+
track to charge them to.
|
|
780
|
+
|
|
781
|
+
### Phase 4.5 — Row subgrid ✅
|
|
782
|
+
|
|
783
|
+
Columns landed in §7.3; rows were left out because "row sizes are only known
|
|
784
|
+
after the items are measured". True, but the conclusion was wrong: the tracks
|
|
785
|
+
are handed to a subgrid child at *final placement*, and by then the parent's
|
|
786
|
+
rows are settled — whether they came from its own template or from its content
|
|
787
|
+
pass. The handoff is the same code as columns, one axis over.
|
|
788
|
+
|
|
789
|
+
What actually made rows harder is that a row subgrid **always spans** several
|
|
790
|
+
of its parent's rows, and spanning items are excluded from content-based row
|
|
791
|
+
sizing (there is no one row to charge them to). With only subgrid cards in a
|
|
792
|
+
grid, every row measured zero and the cards collapsed. The fix is the thing
|
|
793
|
+
that defines subgrid: it is the *grandchildren* that size those rows. Rather
|
|
794
|
+
than see through the card, the parent measures it once — a subgrid with nothing
|
|
795
|
+
inherited falls back to content-sized rows, which is exactly the measurement
|
|
796
|
+
wanted — and adopts the rows it came up with, one for one, via
|
|
797
|
+
`computedRowSizes`.
|
|
798
|
+
|
|
799
|
+
Fixing this also turned up a bug in what column subgrid had been reporting. A
|
|
800
|
+
subgrid child is laid out during its parent's content-measuring pass, before
|
|
801
|
+
any tracks can exist, and it was announcing "subgrid has no enclosing grid to
|
|
802
|
+
inherit tracks from" every time — which was simply untrue. The enclosing grid
|
|
803
|
+
now claims the child (`subgridPending`) as soon as it collects it, well before
|
|
804
|
+
it can size anything, so a child that is merely early is told apart from one
|
|
805
|
+
that really is orphaned.
|
|
806
|
+
|
|
807
|
+
Verification: four subgrid fixtures against Chromium — rows over an explicit
|
|
808
|
+
parent template, rows over content-sized parent rows, columns, and both axes at
|
|
809
|
+
once — plus 10 assertions on the case subgrid exists for: two cards, spanning
|
|
810
|
+
the same rows, whose captions line up without either card knowing about the
|
|
811
|
+
other. The box-model gate is now **1019 assertions over 203 boxes**.
|
|
812
|
+
|
|
813
|
+
One harness note recorded in the fixtures: a card that subgrids only its rows
|
|
814
|
+
has a template-less column axis, which CSS sizes as a single `auto` track. EVG
|
|
815
|
+
defaults that to `1fr`. Under this harness's `justify-content: start` an auto
|
|
816
|
+
track does not stretch, so the two disagree; with the CSS default of `normal`
|
|
817
|
+
they coincide. The fixtures pin the column axis rather than paper over it.
|
|
818
|
+
|
|
819
|
+
### Phase 4.6 — The showcase, and the five bugs it found ✅
|
|
820
|
+
|
|
821
|
+
`lib/evg/showcase/` renders six example pages under two themes and to three
|
|
822
|
+
targets, and publishes them to `/evg/` on the project's Pages site
|
|
823
|
+
(`npm run showcase`). Nothing in `pages/*.tsx` carries a visual attribute:
|
|
824
|
+
the pages say what is on the page, one stylesheet says how it looks, and
|
|
825
|
+
swapping `-theme editorial` for `-theme studio` re-skins all six.
|
|
826
|
+
|
|
827
|
+
Rendering real pages found five bugs that every gate had missed, because each
|
|
828
|
+
one was silent:
|
|
829
|
+
|
|
830
|
+
- **Composite glyphs were never drawn.** `ä`, `ö`, `å`, `é` are a base letter
|
|
831
|
+
plus a diacritic, and the raster path skipped that entire glyph kind while
|
|
832
|
+
still reserving its advance — *päivää* rendered as *piv*. `RasterText` now
|
|
833
|
+
reads the component table, applies each component's 2×2 transform and offset,
|
|
834
|
+
and recurses for components that are themselves composite.
|
|
835
|
+
- **Bold was measured in the regular cut.** The renderers append `-Bold` at
|
|
836
|
+
paint time; layout never did, so a bold heading was measured narrow and drawn
|
|
837
|
+
wide, wrapping a line later than its box. Chromium lays "A Mysterious
|
|
838
|
+
Discovery" out at 18px as 196.02 regular and **209.25** bold; EVG reported
|
|
839
|
+
196.00 for both. `EVGElement.effectiveFontFamily()` is now the one resolver
|
|
840
|
+
layout, the PDF renderer and the raster pen all go through — which also means
|
|
841
|
+
`font-weight` finally does something in a PDF, since the bold face is now
|
|
842
|
+
embedded rather than silently replaced by the regular one.
|
|
843
|
+
- **A grid item resolved percentages against the grid, not its cell.** Every
|
|
844
|
+
`width: 100%` item in a spread was laid out at the full grid width and they
|
|
845
|
+
overlapped. A grid item's containing block is its grid area.
|
|
846
|
+
- **The raster target had no image support at all**, so a photo book rendered
|
|
847
|
+
to PNG came out with the text and none of the pictures. It draws them now,
|
|
848
|
+
with `object-fit: cover`, a decode cache, and the progressive-JPEG decoder
|
|
849
|
+
for files the baseline one rejects — `Example.jpg` is progressive, and used
|
|
850
|
+
to fail with nothing but a line in the log.
|
|
851
|
+
- **An explicit `grid-template-rows` was dropped** when the container had no
|
|
852
|
+
declared height, so `170px auto` on an auto-height deck sized every row from
|
|
853
|
+
content. Tracks that need no container height to resolve are applied now;
|
|
854
|
+
`%` and `fr` genuinely do need one and stay content-sized.
|
|
855
|
+
|
|
856
|
+
Each is covered by the gates: three new browser-verified fixtures (percentage
|
|
857
|
+
items in a cell, with and without padding; an explicit row template on an
|
|
858
|
+
auto-height container) and six assertions pinning the bold face to the width a
|
|
859
|
+
browser actually draws.
|
|
860
|
+
|
|
861
|
+
Known limit the gallery shows rather than hides: the raster target's JPEG
|
|
862
|
+
decode has visible block artefacts, and the PDF path — which embeds the
|
|
863
|
+
original file untouched — does not. Both are on the page, side by side.
|
|
864
|
+
|
|
865
|
+
### Phase 4.7 — Codepoints, and emoji that actually print ✅
|
|
866
|
+
|
|
867
|
+
**Text is stepped by codepoint.** `charAt` returns a UTF-16 code *unit*, so
|
|
868
|
+
everything outside the BMP — emoji, CJK extensions, most maths symbols — was
|
|
869
|
+
seen as two characters, and neither half is a real codepoint. On a page
|
|
870
|
+
containing `a😀b`:
|
|
871
|
+
|
|
872
|
+
| | before |
|
|
873
|
+
| --- | --- |
|
|
874
|
+
| measured width | 47.53px — the emoji was charged **two** `.notdef` advances |
|
|
875
|
+
| JSON display list | `ed a0 bd ed b8 80`, CESU-8 surrogate halves; a strict UTF-8 parser refuses the file |
|
|
876
|
+
| PDF | two encoding warnings, at U+D83D and U+DE00, which are not characters |
|
|
877
|
+
|
|
878
|
+
`EVGCodepoint` is now the one place that knows how to walk a string —
|
|
879
|
+
`codeAt`, `unitsAt`, `count`, `toArray`, `toStr`, `encodeUtf8` — and it is
|
|
880
|
+
threaded through every site that looks up a glyph or writes bytes.
|
|
881
|
+
|
|
882
|
+
**Emoji reach the page.** Three things had to be true at once, and each was
|
|
883
|
+
false:
|
|
884
|
+
|
|
885
|
+
- **A face that has the glyphs.** `Noto_Emoji/NotoEmoji-Regular.ttf` is loaded
|
|
886
|
+
last, so it is never picked as a substitute for a missing *text* face. It is
|
|
887
|
+
the monochrome `glyf` cut on purpose: the colour formats are bitmaps
|
|
888
|
+
(CBDT/sbix) or layered vectors (COLR/CPAL), and neither the outline
|
|
889
|
+
rasterizer nor the PDF font path can read those. Ordinary outlines mean the
|
|
890
|
+
same file works on all three targets with no new machinery.
|
|
891
|
+
- **A cmap that reaches past the BMP.** Format 4 is 16-bit and cannot address
|
|
892
|
+
U+1F600 at all. `TrueTypeFont` now prefers a format 12 subtable — `(3,10)`
|
|
893
|
+
or `(0,4)`/`(0,6)` — and binary-searches its groups.
|
|
894
|
+
- **A run that can cross faces.** A text face has no emoji and an emoji face
|
|
895
|
+
has no letters, so `Ready 🎉` cannot be measured or drawn from one file.
|
|
896
|
+
`FontManager.faceForCodepoint` resolves per codepoint, the primary family
|
|
897
|
+
winning whatever it can draw, so ordinary text takes exactly the same path it
|
|
898
|
+
did before. Kerning is applied only between two codepoints from the same
|
|
899
|
+
face — a pair spanning the boundary has no kern pair by definition.
|
|
900
|
+
|
|
901
|
+
Each target then does the one thing it has to:
|
|
902
|
+
|
|
903
|
+
- **PNG** — `RasterText` swaps face mid-run for a missing glyph and keeps the
|
|
904
|
+
primary face's baseline, so the line does not step where the face changes.
|
|
905
|
+
- **PDF** — WinAnsi is one byte wide and has no room for U+1F389 at any price,
|
|
906
|
+
so a fallback span is drawn through a **Type0 / Identity-H** resource
|
|
907
|
+
(`/E1..`, alongside the WinAnsi `/F1..`) whose strings are glyph ids, with a
|
|
908
|
+
`/W` array and a `/ToUnicode` cmap built from what was actually drawn. The
|
|
909
|
+
existing fonts are untouched: converting everything to Identity-H would
|
|
910
|
+
change the encoding, `/Widths` and cmap of text that is currently correct in
|
|
911
|
+
order to fix text that currently cannot be written at all.
|
|
912
|
+
- **HTML** — the fallback face is named in the `font-family` stack and
|
|
913
|
+
`@font-face`d, but *only when the document needs it*: without that the
|
|
914
|
+
browser substitutes its own emoji font, whose advances are not the ones EVG
|
|
915
|
+
measured with, and the line wraps where the PDF did not.
|
|
916
|
+
|
|
917
|
+
Emoji in a rendered PDF now extract as their real codepoints (`U+1F389`,
|
|
918
|
+
`U+1F600`, …) rather than as `?`.
|
|
919
|
+
|
|
920
|
+
One bug this surfaced, in code written the same afternoon: `unitsPerEm` is born
|
|
921
|
+
`1000`, so a *blank* `TrueTypeFont` — the "no face has this codepoint" answer —
|
|
922
|
+
passed a `unitsPerEm > 0` test and was used as if it were a font. `✓` (U+2713,
|
|
923
|
+
which nothing bundled actually has) came out as `.notdef` drawn from a face
|
|
924
|
+
that had never been opened, with no warning. `TrueTypeFont.isLoaded()` is the
|
|
925
|
+
test now, and the unencodable character is reported again.
|
|
926
|
+
|
|
927
|
+
### Phase 4.8 — Clusters, ligatures, and a font that is only as big as the page ✅
|
|
928
|
+
|
|
929
|
+
Two of Phase 4.7's three known limits, closed.
|
|
930
|
+
|
|
931
|
+
**The embedded font carries the glyphs the page used.** `TTFSubset` keeps head,
|
|
932
|
+
hhea, maxp, hmtx, loca and glyf, and drops everything else — `cmap` included,
|
|
933
|
+
because a Type0/Identity-H font never consults it, which takes GSUB, post,
|
|
934
|
+
name and vmtx with it. It deliberately **keeps glyph ids**: a dense
|
|
935
|
+
renumbering would invalidate every string already written, since Identity-H
|
|
936
|
+
puts glyph ids in the content stream. Composite glyphs pull their components
|
|
937
|
+
in transitively.
|
|
938
|
+
|
|
939
|
+
| | before | after |
|
|
940
|
+
| --- | --- | --- |
|
|
941
|
+
| a page with three emoji | 1 298 067 | 429 332 |
|
|
942
|
+
| `test_for_loop_simple` | 1 717 604 | 840 059 |
|
|
943
|
+
|
|
944
|
+
The remainder in each is the WinAnsi text face, which is **not** subset: a
|
|
945
|
+
simple TrueType font is read through its cmap, so that path needs a different
|
|
946
|
+
set of tables kept.
|
|
947
|
+
|
|
948
|
+
**Text is stepped by grapheme cluster.** A codepoint is not what a reader calls
|
|
949
|
+
a character: 🇫🇮 is two, 👍🏽 is two, 1️⃣ is three, 👨👩👧 is five, and each is one
|
|
950
|
+
glyph, one advance, and one place a line may not break. Stepping by codepoint
|
|
951
|
+
cost three separate things — the face was chosen per codepoint, so a keycap put
|
|
952
|
+
its digit in the text face and its box in the emoji face and the ligature never
|
|
953
|
+
saw all three parts; the width was the sum of the parts, so a family measured
|
|
954
|
+
four advances wide and drew one; and the `/ToUnicode` entry named one codepoint
|
|
955
|
+
for a glyph made of five.
|
|
956
|
+
|
|
957
|
+
`EVGGrapheme` is the cluster rule — a deliberate subset of UAX #29: the
|
|
958
|
+
emoji-relevant rules and the combining marks, not the full property tables.
|
|
959
|
+
`TrueTypeFont.shape()` is the shaper: drop the variation selectors, then take
|
|
960
|
+
the longest GSUB **LookupType 4** ligature, with the Extension (type 7) wrapper
|
|
961
|
+
unwrapped. That is not a general OpenType shaper — no contextual lookups, no
|
|
962
|
+
reordering — but it is exactly what emoji sequences need, and it runs only on a
|
|
963
|
+
run already known to belong to one face, so it can never disturb ordinary text.
|
|
964
|
+
Measured on Noto Emoji, it resolves every case that matters:
|
|
965
|
+
|
|
966
|
+
```
|
|
967
|
+
👨👩👧 5 codepoints -> 1 glyph 🇫🇮 2 -> 1 1️⃣ 3 -> 1
|
|
968
|
+
👍🏽 2 -> 1 🏳️🌈 4 -> 1
|
|
969
|
+
```
|
|
970
|
+
|
|
971
|
+
A cluster the primary face draws is measured and painted exactly as before —
|
|
972
|
+
per codepoint, with kerning — because those two must not part company. Only a
|
|
973
|
+
cluster handed to a fallback face goes through the shaper. All 28 example PDFs
|
|
974
|
+
re-render **byte-identical** across this change.
|
|
975
|
+
|
|
976
|
+
One bug it turned up in its own first draft: a fallback segment was *measured*
|
|
977
|
+
through the ordinary per-codepoint path and *drawn* shaped, so a joined family
|
|
978
|
+
was charged five advances and drew one, pushing everything after it on the line
|
|
979
|
+
to the right. A segment's width now comes from the same walk that draws it.
|
|
980
|
+
|
|
981
|
+
HTML needed one more thing. Chromium treats U+FE0F as an instruction to use its
|
|
982
|
+
own colour emoji font whatever the `font-family` stack says, so keycaps and the
|
|
983
|
+
rainbow flag came out of the system font, in colour, at advances that were not
|
|
984
|
+
the ones EVG measured with. The HTML renderer now writes the text as the engine
|
|
985
|
+
shaped it — selectors dropped — because the engine has already chosen the
|
|
986
|
+
presentation by choosing the face. All three targets agree glyph for glyph.
|
|
987
|
+
|
|
988
|
+
### Phase 4.9 — `emoji-color` ✅
|
|
989
|
+
|
|
990
|
+
A monochrome emoji face is outlines, so it takes whatever colour it is filled
|
|
991
|
+
with. Every target already tinted emoji with the element's `color` — the one
|
|
992
|
+
thing the engine could not do was give them a **different** colour from the
|
|
993
|
+
sentence they sit in, because EVG has no inline spans to hang a second colour
|
|
994
|
+
on.
|
|
995
|
+
|
|
996
|
+
```css
|
|
997
|
+
.caption { color: #1f2937; emoji-color: #e11d48 }
|
|
998
|
+
```
|
|
999
|
+
|
|
1000
|
+
Inherited like `color`, so a deck sets it once. Unset it is the text colour and
|
|
1001
|
+
every existing document is byte-identical — all 28 example PDFs confirm that.
|
|
1002
|
+
|
|
1003
|
+
- **PDF** — the fill colour is chosen per segment, and a fallback segment is
|
|
1004
|
+
already its own `BT`/`ET` block, so this is one `rg` operator.
|
|
1005
|
+
- **PNG** — the raster pen carries a second colour for fallback clusters.
|
|
1006
|
+
- **HTML** — fallback runs are wrapped in a `<span>` with their own colour,
|
|
1007
|
+
which is also the first thing in this engine that needs the renderer to know
|
|
1008
|
+
the faces rather than just their filenames.
|
|
1009
|
+
|
|
1010
|
+
### Multi-colour emoji — sized, deliberately not built
|
|
1011
|
+
|
|
1012
|
+
Noto Color Emoji is **COLRv1 with no v0 layer records at all**, so there is no
|
|
1013
|
+
simple layered-glyph path to take. Measured on the v40 face:
|
|
1014
|
+
|
|
1015
|
+
| | |
|
|
1016
|
+
| --- | --- |
|
|
1017
|
+
| file | 25 MB, of which the `SVG ` table is 19 MB |
|
|
1018
|
+
| base colour glyphs | 3 993 |
|
|
1019
|
+
| using a gradient | **2 278 (57%)** |
|
|
1020
|
+
| deepest glyph | 230 layers |
|
|
1021
|
+
| paint formats | `PaintGlyph` 64 637, `PaintSolid` 51 493, transforms 29 107, gradients 8 630, `PaintComposite` 314 |
|
|
1022
|
+
|
|
1023
|
+
Two facts decide it. Gradients are not a rounding error — flattening them to a
|
|
1024
|
+
single stop would visibly degrade more than half the set — and PDF has no COLR
|
|
1025
|
+
support at all, so colour glyphs must become vector artwork with invisible text
|
|
1026
|
+
behind them for extraction. That is a paint-graph interpreter, a glyph
|
|
1027
|
+
outline → PDF path converter, axial and radial shading patterns on both the PDF
|
|
1028
|
+
and the raster side, and a subsetter that follows COLR layer references. It is
|
|
1029
|
+
tractable and it is scoped here; it is not a variation on what `emoji-color`
|
|
1030
|
+
does.
|
|
1031
|
+
|
|
1032
|
+
## 11. File / module impact (expected)
|
|
1033
|
+
|
|
1034
|
+
| Area | Likely touch points |
|
|
1035
|
+
| --- | --- |
|
|
1036
|
+
| Layout | `lib/evg/EVGLayout.rgr`, `EVGElement.rgr`, `EVGText.rgr`, `EVGGrid.rgr` |
|
|
1037
|
+
| Fonts | `pdf_writer/src/fonts/FontManager.rgr`, `TrueTypeFont.rgr`, `EVGFontSetup.rgr`, `lib/evg/EVGTextEngine.rgr` |
|
|
1038
|
+
| JSX bridge | `pdf_writer/src/jsx/JSXToEVG.rgr`, component engine |
|
|
1039
|
+
| Style | `lib/evg/EVGStyleSheet.rgr` (parse/resolve), `pdf_writer/src/core/EVGStyleLoader.rgr` (CLI wiring) |
|
|
1040
|
+
| Renderers | `EVGPDFRenderer`, `EVGHTMLRenderer`, `EVGRasterRenderer` / `RasterText` |
|
|
1041
|
+
| Examples | `components/PhotoLayouts.tsx`, new theme CSS, album fixtures |
|
|
1042
|
+
| Docs | this plan → later SPEC sections for flex/grid/style |
|
|
1043
|
+
|
|
1044
|
+
### 11.1 One engine, one location
|
|
1045
|
+
|
|
1046
|
+
`lib/evg/` is the single EVG engine. It was previously forked — the game
|
|
1047
|
+
engine carried its own copy under `gallery/game_engine/v2/evg/` that had drifted
|
|
1048
|
+
~200 lines ahead (main-axis `gap`, column grow, shrink-to-fit, `alignItems:
|
|
1049
|
+
stretch`, `flexWrap`, `SVGPathParser.flatten`). That copy has been promoted into
|
|
1050
|
+
`lib/evg/` and deleted, so pdf_writer, the game engine (v1 and v2) and
|
|
1051
|
+
`watch_evg` all compile against the same files.
|
|
1052
|
+
|
|
1053
|
+
Only `EvElementToEVG.rgr` (which depends on the interpreter's `EvalValue`) and
|
|
1054
|
+
`evg_test.rgr` (which uses the v2 `RgTest` harness) remain under
|
|
1055
|
+
`game_engine/v2/evg/`. **Layout changes belong in `lib/evg/` only** — do not
|
|
1056
|
+
re-fork.
|
|
1057
|
+
|
|
1058
|
+
### 11.2 Gates
|
|
1059
|
+
|
|
1060
|
+
```
|
|
1061
|
+
npm run test:evg:all
|
|
1062
|
+
```
|
|
1063
|
+
|
|
1064
|
+
runs all three, or individually:
|
|
1065
|
+
|
|
1066
|
+
| Script | Suite | Covers |
|
|
1067
|
+
| --- | --- | --- |
|
|
1068
|
+
| `test:evg` | `gallery/game_engine/v2/evg/run.sh` | layout engine: units, box model, flex, grid, stylesheet, text engine, baseline |
|
|
1069
|
+
| `test:evg:fonts` | `gallery/pdf_writer/test/run_fonts.sh` | advance-width goldens against the real TTFs, plus EVG-vs-browser parity from a recorded snapshot |
|
|
1070
|
+
| `test:evg:layout` | `gallery/pdf_writer/test/run_layout.sh` | box-model parity against the browser: padding, margins, gaps, nesting, background colour |
|
|
1071
|
+
| `test:evg:frontend` | `gallery/pdf_writer/test/run_print.sh` | UTF-8 decoding, the WinAnsi repertoire, page boxes, JSX text spacing, and the JSX attribute surface |
|
|
1072
|
+
|
|
1073
|
+
**None of these need a browser.** The browser-measured widths live in
|
|
1074
|
+
`gallery/pdf_writer/test/fixtures/browser_parity.snapshot`, so the parity gate
|
|
1075
|
+
runs anywhere. Chromium is only needed to change that file:
|
|
1076
|
+
|
|
1077
|
+
```
|
|
1078
|
+
bash gallery/pdf_writer/test/run_fonts.sh --verify-snapshot # still matches a live browser?
|
|
1079
|
+
bash gallery/pdf_writer/test/run_fonts.sh --update-snapshot # re-record it
|
|
1080
|
+
bash gallery/pdf_writer/test/run_fonts.sh --page-parity # measure a whole rendered page
|
|
1081
|
+
bash gallery/pdf_writer/test/run_layout.sh --verify-snapshot # same, for the box model
|
|
1082
|
+
bash gallery/pdf_writer/test/run_layout.sh --update-snapshot
|
|
1083
|
+
```
|
|
1084
|
+
|
|
1085
|
+
The box-model gate compares padding, per-side padding, margins, per-side
|
|
1086
|
+
margins, row and column gaps, nesting, background colour, and every supported
|
|
1087
|
+
length unit across 121 boxes. Both sides build their tree from the same
|
|
1088
|
+
`fixtures/box_model.fixtures`, and the browser side is configured to EVG's model
|
|
1089
|
+
rather than CSS defaults — `box-sizing: border-box` because EVG's width includes
|
|
1090
|
+
padding, and `display: flex` because EVG's flow is flex (block flow would
|
|
1091
|
+
collapse adjacent vertical margins and the two would disagree for a reason that
|
|
1092
|
+
has nothing to do with EVG).
|
|
1093
|
+
|
|
1094
|
+
Re-record only when the font files change. A snapshot diff with unchanged fonts
|
|
1095
|
+
means the browser disagreed with itself, which is worth reading before
|
|
1096
|
+
committing.
|
|
1097
|
+
|
|
1098
|
+
## 12. Test plan (fonts first)
|
|
1099
|
+
|
|
1100
|
+
1. **Advance width fixtures** — fixed strings per font; assert widths stable across PDF measure API and raster measure API
|
|
1101
|
+
2. **Wrap fixtures** — paragraph + maxWidth → identical line arrays in layout and PDF
|
|
1102
|
+
3. **Flex intrinsic text** — `row` + Label + Image does not force-wrap incorrectly
|
|
1103
|
+
4. **Theme swap** — same TSX tree, two stylesheets → different fonts/spacing, still valid layout
|
|
1104
|
+
5. **Grid album page** — 2×2 + span row; no overlap; gaps respected
|
|
1105
|
+
6. **Cross-target visual** — HTML vs raster/PDF page render diff under tolerance for text blocks
|
|
1106
|
+
|
|
1107
|
+
## 13. Open decisions
|
|
1108
|
+
|
|
1109
|
+
1. ~~Stylesheet syntax: real CSS subset parser vs JSON style maps~~ — **decided:
|
|
1110
|
+
CSS subset parser** (`EVGStyleSheet.rgr`), see Phase 2
|
|
1111
|
+
2. HTML preview strategy: precomputed frames (parity) vs native CSS (speed) + CI diffs
|
|
1112
|
+
3. Default `line-height` policy when property omitted (font `lineGap` vs `1.2`)
|
|
1113
|
+
4. ~~Whether Grid v1 lands before or after theme CSS~~ — **done in that order**: flex → themes → grid
|
|
1114
|
+
|
|
1115
|
+
## 14. Summary
|
|
1116
|
+
|
|
1117
|
+
EVG should grow toward HTML/CSS **as a print-safe subset**, not as a browser. The order that protects quality is:
|
|
1118
|
+
|
|
1119
|
+
1. **Shared TTF metrics + wrap** (layout and paint cannot disagree)
|
|
1120
|
+
2. **Flexbox v2 + intrinsic text**
|
|
1121
|
+
3. **Class/theme styles** for fast visual iteration
|
|
1122
|
+
4. **Grid v1** for album pages
|
|
1123
|
+
|
|
1124
|
+
Fonts are not a side feature here: they are the constraint that makes flex/grid trustworthy on paper.
|