ranger-compiler 3.5.1 → 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 +1064 -20
- package/LICENSE +3 -1
- package/README.md +123 -28
- package/dist/Lang.rgr +1188 -290
- package/dist/api.d.ts +2343 -879
- package/dist/api.js +79923 -56921
- 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,2119 @@
|
|
|
1
|
+
; SPDX-License-Identifier: MIT
|
|
2
|
+
|
|
3
|
+
; ==============================================================================
|
|
4
|
+
; EvgBitmapTracerTest.rgr — bitmap → EVG path
|
|
5
|
+
; ==============================================================================
|
|
6
|
+
;
|
|
7
|
+
; npm run evg:trace:test
|
|
8
|
+
; ==============================================================================
|
|
9
|
+
|
|
10
|
+
Import "EvgBitmapTracer.rgr"
|
|
11
|
+
Import "EvgTest.rgr"
|
|
12
|
+
|
|
13
|
+
class EvgBitmapTracerTest {
|
|
14
|
+
|
|
15
|
+
sfn m@(main):void () {
|
|
16
|
+
def test (new EvgBitmapTracerTest())
|
|
17
|
+
def t:EvgTest (EvgTest.forSuite("evg/bitmap_tracer"))
|
|
18
|
+
test.testEmpty(t)
|
|
19
|
+
test.testFilledRect(t)
|
|
20
|
+
test.testHole(t)
|
|
21
|
+
test.testTurdsize(t)
|
|
22
|
+
test.testPolygonMode(t)
|
|
23
|
+
test.testImageBuffer(t)
|
|
24
|
+
test.testEVGElement(t)
|
|
25
|
+
test.testOptimalRectPolygon(t)
|
|
26
|
+
test.testCurveModeRectCompact(t)
|
|
27
|
+
test.testCheckerTopology(t)
|
|
28
|
+
test.testMonoFillHex(t)
|
|
29
|
+
test.testPosterizeTwoColors(t)
|
|
30
|
+
test.testThinInkSurvivesManyColors(t)
|
|
31
|
+
test.testExtraColorsDoNotSplitOneRegion(t)
|
|
32
|
+
test.testAutoBackgroundKeepsInteriorWhite(t)
|
|
33
|
+
test.testFixedPalette(t)
|
|
34
|
+
test.testSeededPaletteKeepsThePin(t)
|
|
35
|
+
test.testPaletteBiasDefaultIsUnchanged(t)
|
|
36
|
+
test.testSmoothRemovesSpeckle(t)
|
|
37
|
+
test.testStackedLayersLeaveNoSeam(t)
|
|
38
|
+
test.testNamedBackgroundColor(t)
|
|
39
|
+
test.testContourSmoothing(t)
|
|
40
|
+
test.testGradientFills(t)
|
|
41
|
+
test.testOverlayRecoversASwallowedShape(t)
|
|
42
|
+
test.testEdgeFilterSeparatesNoiseFromBoundary(t)
|
|
43
|
+
test.testGradientFillObeysItsControls(t)
|
|
44
|
+
test.testOverlayStaysWithinItsPalette(t)
|
|
45
|
+
test.testOverlaySimilarityDecidesHowManyStack(t)
|
|
46
|
+
test.testDetailMaskFindsTheEyeNotTheRamp(t)
|
|
47
|
+
test.testDetailNeighbourhoodGetsItsOwnPalette(t)
|
|
48
|
+
test.testCompactNumberFormatting(t)
|
|
49
|
+
test.testCompactEncodingDrawsTheSamePath(t)
|
|
50
|
+
test.testOklabIsOnTheSameScale(t)
|
|
51
|
+
test.testAbsorbContrastKeepsAStandoutSpeck(t)
|
|
52
|
+
test.testPresetsAreStartingPoints(t)
|
|
53
|
+
test.testOklabRoundTripsThroughSrgb(t)
|
|
54
|
+
test.testDistinctBiasSurvivesTheLloydPasses(t)
|
|
55
|
+
test.testPaletteDialsMoveWhereTheySay(t)
|
|
56
|
+
t.summary()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
; Build an n×n checkerboard with cell×cell tiles (black on even cells).
|
|
60
|
+
fn makeChecker:EvgBinaryBitmap (n:int cell:int) {
|
|
61
|
+
def bm:EvgBinaryBitmap (EvgBinaryBitmap.create(n n))
|
|
62
|
+
def y:int 0
|
|
63
|
+
while (y < n) {
|
|
64
|
+
def x:int 0
|
|
65
|
+
while (x < n) {
|
|
66
|
+
def cx:int (idiv x cell)
|
|
67
|
+
def cy:int (idiv y cell)
|
|
68
|
+
def cellSum:int (cx + cy)
|
|
69
|
+
if (((cellSum - ((idiv cellSum 2) * 2)) == 0)) {
|
|
70
|
+
bm.setBit(x y true)
|
|
71
|
+
}
|
|
72
|
+
x = (x + 1)
|
|
73
|
+
}
|
|
74
|
+
y = (y + 1)
|
|
75
|
+
}
|
|
76
|
+
return bm
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
fn filledRect:EvgBinaryBitmap (w:int h:int x0:int y0:int rw:int rh:int) {
|
|
80
|
+
def bm:EvgBinaryBitmap (EvgBinaryBitmap.create(w h))
|
|
81
|
+
def y:int y0
|
|
82
|
+
while (y < (y0 + rh)) {
|
|
83
|
+
def x:int x0
|
|
84
|
+
while (x < (x0 + rw)) {
|
|
85
|
+
bm.setBit(x y true)
|
|
86
|
+
x = (x + 1)
|
|
87
|
+
}
|
|
88
|
+
y = (y + 1)
|
|
89
|
+
}
|
|
90
|
+
return bm
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
fn countType:int (cmds:[PathCommand] kind:string) {
|
|
94
|
+
def n:int 0
|
|
95
|
+
def i:int 0
|
|
96
|
+
while (i < (array_length cmds)) {
|
|
97
|
+
def c:PathCommand (itemAt cmds i)
|
|
98
|
+
if (c.type == kind) {
|
|
99
|
+
n = (n + 1)
|
|
100
|
+
}
|
|
101
|
+
i = (i + 1)
|
|
102
|
+
}
|
|
103
|
+
return n
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
fn testEmpty:void (t:EvgTest) {
|
|
107
|
+
def bm:EvgBinaryBitmap (EvgBinaryBitmap.create(8 8))
|
|
108
|
+
def opts:EvgTraceOptions (EvgTraceOptions.defaults())
|
|
109
|
+
def tr:EvgBitmapTracer (EvgBitmapTracer.fromBinary(bm opts))
|
|
110
|
+
tr.trace()
|
|
111
|
+
t.eqInt("empty bitmap yields no rings" (tr.ringCount()) 0)
|
|
112
|
+
t.eqInt("and no commands" (tr.commandCount()) 0)
|
|
113
|
+
t.eqStr("empty path data" (tr.getPathData()) "")
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
fn testFilledRect:void (t:EvgTest) {
|
|
117
|
+
def bm:EvgBinaryBitmap (this.filledRect(32 32 4 4 16 12))
|
|
118
|
+
def opts:EvgTraceOptions (EvgTraceOptions.defaults())
|
|
119
|
+
opts.turdsize = 2
|
|
120
|
+
opts.opttolerance = 0.5
|
|
121
|
+
def tr:EvgBitmapTracer (EvgBitmapTracer.fromBinary(bm opts))
|
|
122
|
+
tr.trace()
|
|
123
|
+
t.ok("a solid rect produces at least one ring" ((tr.ringCount()) >= 1))
|
|
124
|
+
t.ok("and some path commands" ((tr.commandCount()) >= 4))
|
|
125
|
+
t.ok("path data is non-empty" ((strlen (tr.getPathData())) > 0))
|
|
126
|
+
def cmds:[PathCommand] (tr.getCommands())
|
|
127
|
+
t.eqInt("path opens with a moveto" (this.countType(cmds "M")) 1)
|
|
128
|
+
t.ok("path closes" ((this.countType(cmds "Z")) >= 1))
|
|
129
|
+
def svg:string (tr.toSVG())
|
|
130
|
+
t.ok("SVG mentions evenodd" ((indexOf svg "evenodd") >= 0))
|
|
131
|
+
t.ok("SVG embeds the path" ((indexOf svg "d=\"") >= 0))
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
fn testHole:void (t:EvgTest) {
|
|
135
|
+
; Outer 20x20 block with a 6x6 hole punched in the middle.
|
|
136
|
+
def bm:EvgBinaryBitmap (this.filledRect(40 40 5 5 20 20))
|
|
137
|
+
def y:int 12
|
|
138
|
+
while (y < 18) {
|
|
139
|
+
def x:int 12
|
|
140
|
+
while (x < 18) {
|
|
141
|
+
bm.setBit(x y false)
|
|
142
|
+
x = (x + 1)
|
|
143
|
+
}
|
|
144
|
+
y = (y + 1)
|
|
145
|
+
}
|
|
146
|
+
def opts:EvgTraceOptions (EvgTraceOptions.defaults())
|
|
147
|
+
opts.turdsize = 2
|
|
148
|
+
def tr:EvgBitmapTracer (EvgBitmapTracer.fromBinary(bm opts))
|
|
149
|
+
tr.trace()
|
|
150
|
+
t.ok("a ring with a hole yields at least two rings" ((tr.ringCount()) >= 2))
|
|
151
|
+
def el:EVGElement (tr.toEVGElement())
|
|
152
|
+
t.eqStr("EVG path uses evenodd for holes" el.fillRule "evenodd")
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
fn testTurdsize:void (t:EvgTest) {
|
|
156
|
+
def bm:EvgBinaryBitmap (EvgBinaryBitmap.create(16 16))
|
|
157
|
+
; One-pixel speckles — area 1, below turdsize 2.
|
|
158
|
+
bm.setBit(2 2 true)
|
|
159
|
+
bm.setBit(10 10 true)
|
|
160
|
+
def opts:EvgTraceOptions (EvgTraceOptions.defaults())
|
|
161
|
+
opts.turdsize = 2
|
|
162
|
+
def tr:EvgBitmapTracer (EvgBitmapTracer.fromBinary(bm opts))
|
|
163
|
+
tr.trace()
|
|
164
|
+
t.eqInt("turdsize drops single-pixel speckles" (tr.ringCount()) 0)
|
|
165
|
+
|
|
166
|
+
def bm2:EvgBinaryBitmap (this.filledRect(16 16 2 2 4 4))
|
|
167
|
+
def tr2:EvgBitmapTracer (EvgBitmapTracer.fromBinary(bm2 opts))
|
|
168
|
+
tr2.trace()
|
|
169
|
+
t.ok("a 4x4 block survives turdsize 2" ((tr2.ringCount()) >= 1))
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
fn testPolygonMode:void (t:EvgTest) {
|
|
173
|
+
def bm:EvgBinaryBitmap (this.filledRect(24 24 3 3 10 10))
|
|
174
|
+
def opts:EvgTraceOptions (EvgTraceOptions.defaults())
|
|
175
|
+
opts.optcurve = false
|
|
176
|
+
opts.opttolerance = 0.01
|
|
177
|
+
def tr:EvgBitmapTracer (EvgBitmapTracer.fromBinary(bm opts))
|
|
178
|
+
tr.trace()
|
|
179
|
+
def cmds:[PathCommand] (tr.getCommands())
|
|
180
|
+
t.eqInt("polygon mode emits no cubics" (this.countType(cmds "C")) 0)
|
|
181
|
+
t.ok("but still has linetos" ((this.countType(cmds "L")) >= 3))
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
fn testImageBuffer:void (t:EvgTest) {
|
|
185
|
+
def img (new ImageBuffer())
|
|
186
|
+
img.init(20 20)
|
|
187
|
+
def y:int 4
|
|
188
|
+
while (y < 16) {
|
|
189
|
+
def x:int 4
|
|
190
|
+
while (x < 16) {
|
|
191
|
+
img.setPixelRGB(x y 0 0 0)
|
|
192
|
+
x = (x + 1)
|
|
193
|
+
}
|
|
194
|
+
y = (y + 1)
|
|
195
|
+
}
|
|
196
|
+
def opts:EvgTraceOptions (EvgTraceOptions.defaults())
|
|
197
|
+
opts.threshold = 128
|
|
198
|
+
def tr:EvgBitmapTracer (EvgBitmapTracer.fromImageBuffer(img opts))
|
|
199
|
+
tr.trace()
|
|
200
|
+
t.ok("ImageBuffer threshold traces a black square" ((tr.ringCount()) >= 1))
|
|
201
|
+
t.eqInt("width preserved" tr.width 20)
|
|
202
|
+
t.eqInt("height preserved" tr.height 20)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
fn testEVGElement:void (t:EvgTest) {
|
|
206
|
+
def bm:EvgBinaryBitmap (this.filledRect(16 16 2 2 8 8))
|
|
207
|
+
def opts:EvgTraceOptions (EvgTraceOptions.defaults())
|
|
208
|
+
def tr:EvgBitmapTracer (EvgBitmapTracer.fromBinary(bm opts))
|
|
209
|
+
tr.trace()
|
|
210
|
+
def el:EVGElement (tr.toEVGElement())
|
|
211
|
+
t.eqStr("tag is path" el.tagName "path")
|
|
212
|
+
t.ok("svgPath filled" ((strlen el.svgPath) > 0))
|
|
213
|
+
t.ok("viewBox set" ((strlen el.viewBox) > 0))
|
|
214
|
+
def b:PathBuilder (tr.toPathBuilder())
|
|
215
|
+
t.ok("PathBuilder is not empty" ((b.isEmpty()) == false))
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
; Quality gate for the hard polygon stage: an axis-aligned filled rect
|
|
219
|
+
; must collapse to four corners (Potrace's optimal polygon), not a noisy
|
|
220
|
+
; RDP polyline. Polygon mode makes the vertex count visible as L commands.
|
|
221
|
+
fn testOptimalRectPolygon:void (t:EvgTest) {
|
|
222
|
+
def bm:EvgBinaryBitmap (this.filledRect(40 40 8 8 20 16))
|
|
223
|
+
def opts:EvgTraceOptions (EvgTraceOptions.defaults())
|
|
224
|
+
opts.optcurve = false
|
|
225
|
+
def tr:EvgBitmapTracer (EvgBitmapTracer.fromBinary(bm opts))
|
|
226
|
+
tr.trace()
|
|
227
|
+
def cmds:[PathCommand] (tr.getCommands())
|
|
228
|
+
t.eqInt("optimal rect: one moveto" (this.countType(cmds "M")) 1)
|
|
229
|
+
; Four vertices → moveto + three linetos + closepath (Z returns to start).
|
|
230
|
+
t.eqInt("optimal rect: four corners → three linetos" (this.countType(cmds "L")) 3)
|
|
231
|
+
t.eqInt("optimal rect: one closepath" (this.countType(cmds "Z")) 1)
|
|
232
|
+
t.eqInt("optimal rect: command budget" (array_length cmds) 5)
|
|
233
|
+
; Path char budget should be in the same league as CLI potrace for a rect.
|
|
234
|
+
t.ok("path data stays compact" ((strlen (tr.getPathData())) < 120))
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
; Default optcurve=true runs alphamax + optiCurve. For an axis-aligned rect
|
|
238
|
+
; every vertex is a CORNER, so emit collapses to the same compact polygon
|
|
239
|
+
; the Potrace CLI SVG produces (no midpoint staging).
|
|
240
|
+
fn testCurveModeRectCompact:void (t:EvgTest) {
|
|
241
|
+
def bm:EvgBinaryBitmap (this.filledRect(40 40 8 8 20 16))
|
|
242
|
+
def opts:EvgTraceOptions (EvgTraceOptions.defaults())
|
|
243
|
+
opts.optcurve = true
|
|
244
|
+
opts.alphamax = 1.0
|
|
245
|
+
opts.opttolerance = 0.2
|
|
246
|
+
def tr:EvgBitmapTracer (EvgBitmapTracer.fromBinary(bm opts))
|
|
247
|
+
tr.trace()
|
|
248
|
+
def cmds:[PathCommand] (tr.getCommands())
|
|
249
|
+
t.eqInt("curve-mode rect: one moveto" (this.countType(cmds "M")) 1)
|
|
250
|
+
t.eqInt("curve-mode rect: three linetos" (this.countType(cmds "L")) 3)
|
|
251
|
+
t.eqInt("curve-mode rect: no cubics on a box" (this.countType(cmds "C")) 0)
|
|
252
|
+
t.eqInt("curve-mode rect: command budget" (array_length cmds) 5)
|
|
253
|
+
t.ok("curve-mode path stays compact" ((strlen (tr.getPathData())) < 120))
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
; Checker topology must match libpotrace pathlist length (not a mistaken
|
|
257
|
+
; "always 2 subpaths" reading of the first SVG <path> only). Coarse 4×4
|
|
258
|
+
; cells → 2 paths; the bench's 8×8 cells on 128² → 8 paths.
|
|
259
|
+
fn testCheckerTopology:void (t:EvgTest) {
|
|
260
|
+
def opts:EvgTraceOptions (EvgTraceOptions.defaults())
|
|
261
|
+
opts.turdsize = 2
|
|
262
|
+
opts.optcurve = true
|
|
263
|
+
def coarse:EvgBinaryBitmap (this.makeChecker(128 32))
|
|
264
|
+
def tr1:EvgBitmapTracer (EvgBitmapTracer.fromBinary(coarse opts))
|
|
265
|
+
tr1.trace()
|
|
266
|
+
t.eqInt("coarse checker (cell 32) → 2 rings" (tr1.ringCount()) 2)
|
|
267
|
+
def fine:EvgBinaryBitmap (this.makeChecker(128 8))
|
|
268
|
+
def tr2:EvgBitmapTracer (EvgBitmapTracer.fromBinary(fine opts))
|
|
269
|
+
tr2.trace()
|
|
270
|
+
t.eqInt("fine checker (cell 8) → 8 rings" (tr2.ringCount()) 8)
|
|
271
|
+
t.ok("fine checker path stays under fair CLI budget" ((strlen (tr2.getPathData())) < 5500))
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
; Mono traces honor options.fillHex in SVG / EVG output.
|
|
275
|
+
fn testMonoFillHex:void (t:EvgTest) {
|
|
276
|
+
def bm:EvgBinaryBitmap (this.filledRect(24 24 4 4 12 12))
|
|
277
|
+
def opts:EvgTraceOptions (EvgTraceOptions.defaults())
|
|
278
|
+
opts.fillHex = "#CC3300"
|
|
279
|
+
def tr:EvgBitmapTracer (EvgBitmapTracer.fromBinary(bm opts))
|
|
280
|
+
tr.trace()
|
|
281
|
+
def svg:string (tr.toSVG())
|
|
282
|
+
t.ok("SVG uses the requested fill hex" ((indexOf svg "#CC3300") >= 0))
|
|
283
|
+
t.eqInt("mono still yields one layer" (tr.layerCount()) 1)
|
|
284
|
+
def el:EVGElement (tr.toEVGElement())
|
|
285
|
+
t.ok("EVG fill picks up fillHex" ((el.fillColor.toHexString()) == "#CC3300"))
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
; Two solid colored blocks → posterize into separate colored path layers.
|
|
289
|
+
fn testPosterizeTwoColors:void (t:EvgTest) {
|
|
290
|
+
def img (new ImageBuffer())
|
|
291
|
+
img.init(40 24)
|
|
292
|
+
def y:int 0
|
|
293
|
+
while (y < 24) {
|
|
294
|
+
def x:int 0
|
|
295
|
+
while (x < 40) {
|
|
296
|
+
img.setPixelRGB(x y 255 255 255)
|
|
297
|
+
x = (x + 1)
|
|
298
|
+
}
|
|
299
|
+
y = (y + 1)
|
|
300
|
+
}
|
|
301
|
+
y = 4
|
|
302
|
+
while (y < 20) {
|
|
303
|
+
def x2:int 4
|
|
304
|
+
while (x2 < 18) {
|
|
305
|
+
img.setPixelRGB(x2 y 200 30 30)
|
|
306
|
+
x2 = (x2 + 1)
|
|
307
|
+
}
|
|
308
|
+
def x3:int 22
|
|
309
|
+
while (x3 < 36) {
|
|
310
|
+
img.setPixelRGB(x3 y 30 60 200)
|
|
311
|
+
x3 = (x3 + 1)
|
|
312
|
+
}
|
|
313
|
+
y = (y + 1)
|
|
314
|
+
}
|
|
315
|
+
def opts:EvgTraceOptions (EvgTraceOptions.defaults())
|
|
316
|
+
opts.colorCount = 2
|
|
317
|
+
opts.turdsize = 2
|
|
318
|
+
opts.skipLuma = 250
|
|
319
|
+
def tr:EvgBitmapTracer (EvgBitmapTracer.fromImageBuffer(img opts))
|
|
320
|
+
tr.trace()
|
|
321
|
+
t.eqInt("posterize yields two color layers" (tr.layerCount()) 2)
|
|
322
|
+
def svg:string (tr.toSVG())
|
|
323
|
+
t.ok("SVG has two path elements" ((indexOf svg "<path") >= 0))
|
|
324
|
+
; Count <path occurrences roughly via split length.
|
|
325
|
+
def els:[EVGElement] (tr.toEVGElements())
|
|
326
|
+
t.eqInt("toEVGElements matches layer count" (array_length els) 2)
|
|
327
|
+
def layer0:EvgTraceLayer (itemAt tr.layers 0)
|
|
328
|
+
def layer1:EvgTraceLayer (itemAt tr.layers 1)
|
|
329
|
+
def h0:string layer0.fillHex
|
|
330
|
+
def h1:string layer1.fillHex
|
|
331
|
+
t.ok("layer fills differ" (h0 != h1))
|
|
332
|
+
t.ok("SVG embeds first layer fill" ((indexOf svg h0) >= 0))
|
|
333
|
+
t.ok("SVG embeds second layer fill" ((indexOf svg h1) >= 0))
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
; Fill a rect, anti-aliasing every boundary the way a rasterizer would.
|
|
337
|
+
fn blend:int (a:int b:int t:int) {
|
|
338
|
+
return (idiv (((a * (100 - t)) + (b * t))) 100)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
; A dark hairline on a colored field. Asking for many colors used to spend
|
|
342
|
+
; the extra swatches on the halo around the line and then lose the line
|
|
343
|
+
; itself to turdsize — the regression this guards.
|
|
344
|
+
fn makeInkLine:ImageBuffer () {
|
|
345
|
+
def img (new ImageBuffer())
|
|
346
|
+
img.init(60 40)
|
|
347
|
+
def y:int 0
|
|
348
|
+
while (y < 40) {
|
|
349
|
+
def x:int 0
|
|
350
|
+
while (x < 60) {
|
|
351
|
+
img.setPixelRGB(x y 220 150 60)
|
|
352
|
+
x = (x + 1)
|
|
353
|
+
}
|
|
354
|
+
y = (y + 1)
|
|
355
|
+
}
|
|
356
|
+
; A 3px ink stroke with one blended pixel on each side.
|
|
357
|
+
y = 0
|
|
358
|
+
while (y < 40) {
|
|
359
|
+
img.setPixelRGB(28 y (this.blend(220 20 60)) (this.blend(150 20 60)) (this.blend(60 20 60)))
|
|
360
|
+
img.setPixelRGB(29 y 20 20 20)
|
|
361
|
+
img.setPixelRGB(30 y 20 20 20)
|
|
362
|
+
img.setPixelRGB(31 y (this.blend(220 20 60)) (this.blend(150 20 60)) (this.blend(60 20 60)))
|
|
363
|
+
y = (y + 1)
|
|
364
|
+
}
|
|
365
|
+
return img
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
fn countInkPixels:int (tr:EvgBitmapTracer) {
|
|
369
|
+
def n:int 0
|
|
370
|
+
def i:int 0
|
|
371
|
+
while (i < (array_length tr.labels)) {
|
|
372
|
+
def lab:int (itemAt tr.labels i)
|
|
373
|
+
if (lab == 0) {
|
|
374
|
+
n = (n + 1)
|
|
375
|
+
}
|
|
376
|
+
i = (i + 1)
|
|
377
|
+
}
|
|
378
|
+
return n
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
fn testThinInkSurvivesManyColors:void (t:EvgTest) {
|
|
382
|
+
def counts:[int]
|
|
383
|
+
push counts 2
|
|
384
|
+
push counts 6
|
|
385
|
+
push counts 12
|
|
386
|
+
def ci:int 0
|
|
387
|
+
while (ci < (array_length counts)) {
|
|
388
|
+
def want:int (itemAt counts ci)
|
|
389
|
+
def opts:EvgTraceOptions (EvgTraceOptions.defaults())
|
|
390
|
+
opts.colorCount = want
|
|
391
|
+
def tr:EvgBitmapTracer (EvgBitmapTracer.fromImageBuffer((this.makeInkLine()) opts))
|
|
392
|
+
tr.trace()
|
|
393
|
+
def darkest:EvgTraceLayer (itemAt tr.layers 0)
|
|
394
|
+
t.ok(("ink layer survives at " + (to_string want) + " colors") ((darkest.ringCount) > 0))
|
|
395
|
+
; The stroke is 40px tall and at least 2px wide.
|
|
396
|
+
def ink:int (this.countInkPixels(tr))
|
|
397
|
+
t.ok(("ink keeps its width at " + (to_string want) + " colors") (ink >= 80))
|
|
398
|
+
ci = (ci + 1)
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
; The image holds two colors; asking for twelve must not invent ten more
|
|
403
|
+
; near-identical swatches that then split every region.
|
|
404
|
+
fn testExtraColorsDoNotSplitOneRegion:void (t:EvgTest) {
|
|
405
|
+
def opts:EvgTraceOptions (EvgTraceOptions.defaults())
|
|
406
|
+
opts.colorCount = 12
|
|
407
|
+
def tr:EvgBitmapTracer (EvgBitmapTracer.fromImageBuffer((this.makeInkLine()) opts))
|
|
408
|
+
tr.trace()
|
|
409
|
+
t.ok("palette collapses to what the image holds" ((tr.layerCount()) <= 4))
|
|
410
|
+
t.ok("palette is not empty" ((tr.layerCount()) >= 2))
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
; A white page around a shape, and a white patch inside it. The page goes;
|
|
414
|
+
; the patch stays — skipLuma alone could not tell them apart.
|
|
415
|
+
fn testAutoBackgroundKeepsInteriorWhite:void (t:EvgTest) {
|
|
416
|
+
def img (new ImageBuffer())
|
|
417
|
+
img.init(40 40)
|
|
418
|
+
def y:int 0
|
|
419
|
+
while (y < 40) {
|
|
420
|
+
def x:int 0
|
|
421
|
+
while (x < 40) {
|
|
422
|
+
img.setPixelRGB(x y 255 255 255)
|
|
423
|
+
x = (x + 1)
|
|
424
|
+
}
|
|
425
|
+
y = (y + 1)
|
|
426
|
+
}
|
|
427
|
+
y = 6
|
|
428
|
+
while (y < 34) {
|
|
429
|
+
def x2:int 6
|
|
430
|
+
while (x2 < 34) {
|
|
431
|
+
img.setPixelRGB(x2 y 40 90 200)
|
|
432
|
+
x2 = (x2 + 1)
|
|
433
|
+
}
|
|
434
|
+
y = (y + 1)
|
|
435
|
+
}
|
|
436
|
+
y = 14
|
|
437
|
+
while (y < 26) {
|
|
438
|
+
def x3:int 14
|
|
439
|
+
while (x3 < 26) {
|
|
440
|
+
img.setPixelRGB(x3 y 255 255 255)
|
|
441
|
+
x3 = (x3 + 1)
|
|
442
|
+
}
|
|
443
|
+
y = (y + 1)
|
|
444
|
+
}
|
|
445
|
+
def opts:EvgTraceOptions (EvgTraceOptions.defaults())
|
|
446
|
+
opts.colorCount = 2
|
|
447
|
+
def tr:EvgBitmapTracer (EvgBitmapTracer.fromImageBuffer(img opts))
|
|
448
|
+
tr.trace()
|
|
449
|
+
t.eqInt("auto background yields both swatches" (tr.layerCount()) 2)
|
|
450
|
+
; Count what each swatch painted: the patch is 12×12, the page 40×40-28×28.
|
|
451
|
+
def white:int 0
|
|
452
|
+
def blue:int 0
|
|
453
|
+
def i:int 0
|
|
454
|
+
while (i < (array_length tr.labels)) {
|
|
455
|
+
def lab:int (itemAt tr.labels i)
|
|
456
|
+
if (lab == 0) {
|
|
457
|
+
blue = (blue + 1)
|
|
458
|
+
}
|
|
459
|
+
if (lab == 1) {
|
|
460
|
+
white = (white + 1)
|
|
461
|
+
}
|
|
462
|
+
i = (i + 1)
|
|
463
|
+
}
|
|
464
|
+
t.eqInt("interior white patch is painted" white 144)
|
|
465
|
+
t.eqInt("only the shape around it is painted" blue 640)
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
; A big slow gray gradient — lots of area spread over many histogram bins —
|
|
469
|
+
; and one small vivid patch. This is the ball-pit case in miniature: the
|
|
470
|
+
; dull background is what a population-weighted quantizer wants to spend
|
|
471
|
+
; swatches on, and the colorful thing is what a person wants to keep.
|
|
472
|
+
fn makeGradientWithSpot:ImageBuffer () {
|
|
473
|
+
def img (new ImageBuffer())
|
|
474
|
+
img.init(60 40)
|
|
475
|
+
def y:int 0
|
|
476
|
+
while (y < 40) {
|
|
477
|
+
def x:int 0
|
|
478
|
+
while (x < 60) {
|
|
479
|
+
def g:int (90 + (idiv x 2))
|
|
480
|
+
img.setPixelRGB(x y g g (g - 2))
|
|
481
|
+
x = (x + 1)
|
|
482
|
+
}
|
|
483
|
+
y = (y + 1)
|
|
484
|
+
}
|
|
485
|
+
y = 6
|
|
486
|
+
while (y < 12) {
|
|
487
|
+
def x2:int 6
|
|
488
|
+
while (x2 < 12) {
|
|
489
|
+
img.setPixelRGB(x2 y 225 25 35)
|
|
490
|
+
x2 = (x2 + 1)
|
|
491
|
+
}
|
|
492
|
+
y = (y + 1)
|
|
493
|
+
}
|
|
494
|
+
return img
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
; Largest channel spread of a hex swatch — a cheap stand-in for saturation.
|
|
498
|
+
fn spreadOf:int (hex:string) {
|
|
499
|
+
def c:EVGColor (EVGColor.parse(hex))
|
|
500
|
+
def r:int (to_int c.r)
|
|
501
|
+
def g:int (to_int c.g)
|
|
502
|
+
def b:int (to_int c.b)
|
|
503
|
+
def hi:int r
|
|
504
|
+
def lo:int r
|
|
505
|
+
if (g > hi) { hi = g }
|
|
506
|
+
if (b > hi) { hi = b }
|
|
507
|
+
if (g < lo) { lo = g }
|
|
508
|
+
if (b < lo) { lo = b }
|
|
509
|
+
return (hi - lo)
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
fn vividCount:int (tr:EvgBitmapTracer) {
|
|
513
|
+
def n:int 0
|
|
514
|
+
def i:int 0
|
|
515
|
+
while (i < (tr.layerCount())) {
|
|
516
|
+
def layer:EvgTraceLayer (itemAt tr.layers i)
|
|
517
|
+
if ((this.spreadOf(layer.fillHex)) > 60) {
|
|
518
|
+
n = (n + 1)
|
|
519
|
+
}
|
|
520
|
+
i = (i + 1)
|
|
521
|
+
}
|
|
522
|
+
return n
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
; "fixed" uses exactly the colors it is given, and the colorCount slider has
|
|
526
|
+
; nothing to do with it.
|
|
527
|
+
fn testFixedPalette:void (t:EvgTest) {
|
|
528
|
+
def opts:EvgTraceOptions (EvgTraceOptions.defaults())
|
|
529
|
+
opts.colorCount = 9
|
|
530
|
+
opts.paletteMode = "fixed"
|
|
531
|
+
opts.bgMode = "none"
|
|
532
|
+
push opts.paletteHex "#000000"
|
|
533
|
+
push opts.paletteHex "#ffffff"
|
|
534
|
+
push opts.paletteHex "#ffd400"
|
|
535
|
+
def tr:EvgBitmapTracer (EvgBitmapTracer.fromImageBuffer((this.makeGradientWithSpot()) opts))
|
|
536
|
+
tr.trace()
|
|
537
|
+
t.ok("fixed palette ignores colorCount" ((tr.layerCount()) <= 3))
|
|
538
|
+
t.ok("fixed palette still paints something" ((tr.layerCount()) >= 1))
|
|
539
|
+
def i:int 0
|
|
540
|
+
def foreign:int 0
|
|
541
|
+
while (i < (tr.layerCount())) {
|
|
542
|
+
def layer:EvgTraceLayer (itemAt tr.layers i)
|
|
543
|
+
def h:string layer.fillHex
|
|
544
|
+
if ((h != "#000000") && (h != "#FFFFFF") && (h != "#FFD400")) {
|
|
545
|
+
foreign = (foreign + 1)
|
|
546
|
+
}
|
|
547
|
+
i = (i + 1)
|
|
548
|
+
}
|
|
549
|
+
t.eqInt("no swatch outside the fixed palette" foreign 0)
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
; "seeded" pins what it is given and quantizes the rest around it. The pin
|
|
553
|
+
; must survive both the Lloyd passes and the near-swatch merge — an
|
|
554
|
+
; unpinned swatch this close to its neighbour would be merged away.
|
|
555
|
+
fn testSeededPaletteKeepsThePin:void (t:EvgTest) {
|
|
556
|
+
def opts:EvgTraceOptions (EvgTraceOptions.defaults())
|
|
557
|
+
opts.colorCount = 3
|
|
558
|
+
opts.paletteMode = "seeded"
|
|
559
|
+
opts.bgMode = "none"
|
|
560
|
+
push opts.paletteHex "#6A6A68"
|
|
561
|
+
def tr:EvgBitmapTracer (EvgBitmapTracer.fromImageBuffer((this.makeGradientWithSpot()) opts))
|
|
562
|
+
tr.trace()
|
|
563
|
+
def pinned:int 0
|
|
564
|
+
def i:int 0
|
|
565
|
+
while (i < (tr.layerCount())) {
|
|
566
|
+
def layer:EvgTraceLayer (itemAt tr.layers i)
|
|
567
|
+
if ((layer.fillHex) == "#6A6A68") {
|
|
568
|
+
pinned = (pinned + 1)
|
|
569
|
+
}
|
|
570
|
+
i = (i + 1)
|
|
571
|
+
}
|
|
572
|
+
t.eqInt("the pinned swatch survives" pinned 1)
|
|
573
|
+
t.ok("the rest of the palette is still filled in" ((tr.layerCount()) >= 2))
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
; The defaults must reproduce the quantizer exactly as it behaved before the
|
|
577
|
+
; palette controls existed — the muted, area-driven palette is a choice, not
|
|
578
|
+
; an accident, and it stays the default.
|
|
579
|
+
fn testPaletteBiasDefaultIsUnchanged:void (t:EvgTest) {
|
|
580
|
+
def a:EvgTraceOptions (EvgTraceOptions.defaults())
|
|
581
|
+
t.ok("palette mode defaults to auto" ((a.paletteMode) == "auto"))
|
|
582
|
+
t.ok("palette bias defaults to area" ((a.paletteBias) == "area"))
|
|
583
|
+
t.eqInt("no palette colors by default" (array_length a.paletteHex) 0)
|
|
584
|
+
; With two swatches to spend, area buys two steps of the gradient and
|
|
585
|
+
; loses the red spot; balanced buys the spot. That trade is the point.
|
|
586
|
+
def optA:EvgTraceOptions (EvgTraceOptions.defaults())
|
|
587
|
+
optA.colorCount = 2
|
|
588
|
+
optA.bgMode = "none"
|
|
589
|
+
def trA:EvgBitmapTracer (EvgBitmapTracer.fromImageBuffer((this.makeGradientWithSpot()) optA))
|
|
590
|
+
trA.trace()
|
|
591
|
+
t.eqInt("area spends both swatches on the big region" (this.vividCount(trA)) 0)
|
|
592
|
+
def optB:EvgTraceOptions (EvgTraceOptions.defaults())
|
|
593
|
+
optB.colorCount = 2
|
|
594
|
+
optB.bgMode = "none"
|
|
595
|
+
optB.paletteBias = "balanced"
|
|
596
|
+
def trB:EvgBitmapTracer (EvgBitmapTracer.fromImageBuffer((this.makeGradientWithSpot()) optB))
|
|
597
|
+
trB.trace()
|
|
598
|
+
t.ok("balanced keeps the small vivid patch" ((this.vividCount(trB)) >= 1))
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
; Two flat halves with salt-and-pepper sprayed over them. Quantizing this
|
|
602
|
+
; without smoothing turns every speck into its own ring, which is what a
|
|
603
|
+
; photograph does at a high colorCount.
|
|
604
|
+
fn makeSpeckled:ImageBuffer () {
|
|
605
|
+
def img (new ImageBuffer())
|
|
606
|
+
img.init(60 40)
|
|
607
|
+
def seed:int 12345
|
|
608
|
+
def y:int 0
|
|
609
|
+
while (y < 40) {
|
|
610
|
+
def x:int 0
|
|
611
|
+
while (x < 60) {
|
|
612
|
+
def left:boolean (x < 30)
|
|
613
|
+
; A small LCG — every target can hold these numbers.
|
|
614
|
+
seed = (((seed * 75) + 74) % 65537)
|
|
615
|
+
if ((seed % 100) < 15) {
|
|
616
|
+
left = (left == false)
|
|
617
|
+
}
|
|
618
|
+
if left {
|
|
619
|
+
img.setPixelRGB(x y 60 90 160)
|
|
620
|
+
} {
|
|
621
|
+
img.setPixelRGB(x y 200 180 120)
|
|
622
|
+
}
|
|
623
|
+
x = (x + 1)
|
|
624
|
+
}
|
|
625
|
+
y = (y + 1)
|
|
626
|
+
}
|
|
627
|
+
return img
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
fn totalRings:int (tr:EvgBitmapTracer) {
|
|
631
|
+
def n:int 0
|
|
632
|
+
def i:int 0
|
|
633
|
+
while (i < (tr.layerCount())) {
|
|
634
|
+
def layer:EvgTraceLayer (itemAt tr.layers i)
|
|
635
|
+
n = (n + layer.ringCount)
|
|
636
|
+
i = (i + 1)
|
|
637
|
+
}
|
|
638
|
+
return n
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
fn testSmoothRemovesSpeckle:void (t:EvgTest) {
|
|
642
|
+
def a:EvgTraceOptions (EvgTraceOptions.defaults())
|
|
643
|
+
t.eqInt("smoothing is off by default" (a.smooth) 0)
|
|
644
|
+
|
|
645
|
+
def rawOpts:EvgTraceOptions (EvgTraceOptions.defaults())
|
|
646
|
+
rawOpts.colorCount = 2
|
|
647
|
+
rawOpts.bgMode = "none"
|
|
648
|
+
rawOpts.minRegion = 0
|
|
649
|
+
def raw:EvgBitmapTracer (EvgBitmapTracer.fromImageBuffer((this.makeSpeckled()) rawOpts))
|
|
650
|
+
raw.trace()
|
|
651
|
+
def rawRings:int (this.totalRings(raw))
|
|
652
|
+
|
|
653
|
+
def smOpts:EvgTraceOptions (EvgTraceOptions.defaults())
|
|
654
|
+
smOpts.colorCount = 2
|
|
655
|
+
smOpts.bgMode = "none"
|
|
656
|
+
smOpts.minRegion = 0
|
|
657
|
+
smOpts.smooth = 2
|
|
658
|
+
def sm:EvgBitmapTracer (EvgBitmapTracer.fromImageBuffer((this.makeSpeckled()) smOpts))
|
|
659
|
+
sm.trace()
|
|
660
|
+
def smRings:int (this.totalRings(sm))
|
|
661
|
+
|
|
662
|
+
; 15 rather than something larger: in the default stacked mode the
|
|
663
|
+
; bottom layer is one ring covering everything, so only the layer above
|
|
664
|
+
; it carries the speckle.
|
|
665
|
+
t.ok("the speckle really is there without smoothing" (rawRings > 15))
|
|
666
|
+
t.ok("smoothing collapses it" (smRings < (idiv rawRings 4)))
|
|
667
|
+
; The edge between the halves must survive: both colors still painted.
|
|
668
|
+
t.eqInt("both regions survive the median" (sm.layerCount()) 2)
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
; A light square inside a dark field. Traced as disjoint regions the dark
|
|
672
|
+
; layer needs a hole for the square, and the two layers then meet along one
|
|
673
|
+
; pixel boundary that each of them curve-fits on its own — the fits differ,
|
|
674
|
+
; and the page shows through the difference as a hairline. Stacked, the
|
|
675
|
+
; dark layer simply runs under the square: no hole, and nothing to show
|
|
676
|
+
; through.
|
|
677
|
+
fn makeInsetSquare:ImageBuffer () {
|
|
678
|
+
def img (new ImageBuffer())
|
|
679
|
+
img.init(40 40)
|
|
680
|
+
def y:int 0
|
|
681
|
+
while (y < 40) {
|
|
682
|
+
def x:int 0
|
|
683
|
+
while (x < 40) {
|
|
684
|
+
img.setPixelRGB(x y 30 40 60)
|
|
685
|
+
x = (x + 1)
|
|
686
|
+
}
|
|
687
|
+
y = (y + 1)
|
|
688
|
+
}
|
|
689
|
+
y = 12
|
|
690
|
+
while (y < 28) {
|
|
691
|
+
def x2:int 12
|
|
692
|
+
while (x2 < 28) {
|
|
693
|
+
img.setPixelRGB(x2 y 230 220 200)
|
|
694
|
+
x2 = (x2 + 1)
|
|
695
|
+
}
|
|
696
|
+
y = (y + 1)
|
|
697
|
+
}
|
|
698
|
+
return img
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
fn traceInset:EvgBitmapTracer (mode:string) {
|
|
702
|
+
def opts:EvgTraceOptions (EvgTraceOptions.defaults())
|
|
703
|
+
opts.colorCount = 2
|
|
704
|
+
opts.bgMode = "none"
|
|
705
|
+
opts.layerMode = mode
|
|
706
|
+
def tr:EvgBitmapTracer (EvgBitmapTracer.fromImageBuffer((this.makeInsetSquare()) opts))
|
|
707
|
+
tr.trace()
|
|
708
|
+
return tr
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
fn testStackedLayersLeaveNoSeam:void (t:EvgTest) {
|
|
712
|
+
def d:EvgTraceOptions (EvgTraceOptions.defaults())
|
|
713
|
+
t.ok("layers are stacked by default" ((d.layerMode) == "stacked"))
|
|
714
|
+
|
|
715
|
+
def flat:EvgBitmapTracer (this.traceInset("flat"))
|
|
716
|
+
def stacked:EvgBitmapTracer (this.traceInset("stacked"))
|
|
717
|
+
t.eqInt("flat still yields both swatches" (flat.layerCount()) 2)
|
|
718
|
+
t.eqInt("stacked still yields both swatches" (stacked.layerCount()) 2)
|
|
719
|
+
|
|
720
|
+
def flatDark:EvgTraceLayer (itemAt flat.layers 0)
|
|
721
|
+
def stackedDark:EvgTraceLayer (itemAt stacked.layers 0)
|
|
722
|
+
; The hole is the seam: disjoint needs one, stacked does not.
|
|
723
|
+
t.eqInt("the disjoint dark layer is cut open for the square" (flatDark.ringCount) 2)
|
|
724
|
+
t.eqInt("the stacked dark layer runs under it whole" (stackedDark.ringCount) 1)
|
|
725
|
+
|
|
726
|
+
def flatLight:EvgTraceLayer (itemAt flat.layers 1)
|
|
727
|
+
def stackedLight:EvgTraceLayer (itemAt stacked.layers 1)
|
|
728
|
+
t.eqInt("the square itself is unchanged" (stackedLight.ringCount) (flatLight.ringCount))
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
; The same shape, but a band along the top edge breaks up the border. That
|
|
732
|
+
; is enough for "auto" to refuse — which is correct, it cannot tell a page
|
|
733
|
+
; from a photograph — and is where naming the color takes over.
|
|
734
|
+
fn makeBustedBorder:ImageBuffer () {
|
|
735
|
+
def img (new ImageBuffer())
|
|
736
|
+
img.init(40 40)
|
|
737
|
+
def y:int 0
|
|
738
|
+
while (y < 40) {
|
|
739
|
+
def x:int 0
|
|
740
|
+
while (x < 40) {
|
|
741
|
+
img.setPixelRGB(x y 255 255 255)
|
|
742
|
+
x = (x + 1)
|
|
743
|
+
}
|
|
744
|
+
y = (y + 1)
|
|
745
|
+
}
|
|
746
|
+
; A band across the top: the border is no longer one flat color.
|
|
747
|
+
y = 0
|
|
748
|
+
while (y < 4) {
|
|
749
|
+
def xb:int 0
|
|
750
|
+
while (xb < 40) {
|
|
751
|
+
img.setPixelRGB(xb y 40 70 110)
|
|
752
|
+
xb = (xb + 1)
|
|
753
|
+
}
|
|
754
|
+
y = (y + 1)
|
|
755
|
+
}
|
|
756
|
+
y = 8
|
|
757
|
+
while (y < 34) {
|
|
758
|
+
def x2:int 6
|
|
759
|
+
while (x2 < 34) {
|
|
760
|
+
img.setPixelRGB(x2 y 40 90 200)
|
|
761
|
+
x2 = (x2 + 1)
|
|
762
|
+
}
|
|
763
|
+
y = (y + 1)
|
|
764
|
+
}
|
|
765
|
+
; White inside the shape — this must survive, it is not the page.
|
|
766
|
+
y = 16
|
|
767
|
+
while (y < 26) {
|
|
768
|
+
def x3:int 14
|
|
769
|
+
while (x3 < 26) {
|
|
770
|
+
img.setPixelRGB(x3 y 255 255 255)
|
|
771
|
+
x3 = (x3 + 1)
|
|
772
|
+
}
|
|
773
|
+
y = (y + 1)
|
|
774
|
+
}
|
|
775
|
+
return img
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
fn countLabel:int (tr:EvgBitmapTracer lab:int) {
|
|
779
|
+
def n:int 0
|
|
780
|
+
def i:int 0
|
|
781
|
+
while (i < (array_length tr.labels)) {
|
|
782
|
+
if ((itemAt tr.labels i) == lab) {
|
|
783
|
+
n = (n + 1)
|
|
784
|
+
}
|
|
785
|
+
i = (i + 1)
|
|
786
|
+
}
|
|
787
|
+
return n
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
fn testNamedBackgroundColor:void (t:EvgTest) {
|
|
791
|
+
def d:EvgTraceOptions (EvgTraceOptions.defaults())
|
|
792
|
+
t.ok("background color defaults to white" ((d.bgColor) == "#ffffff"))
|
|
793
|
+
|
|
794
|
+
def autoOpts:EvgTraceOptions (EvgTraceOptions.defaults())
|
|
795
|
+
autoOpts.colorCount = 3
|
|
796
|
+
def au:EvgBitmapTracer (EvgBitmapTracer.fromImageBuffer((this.makeBustedBorder()) autoOpts))
|
|
797
|
+
au.trace()
|
|
798
|
+
; The border is not one flat color here, so the modal-color flood stands
|
|
799
|
+
; down — and what used to happen then was that every bright pixel in the
|
|
800
|
+
; picture was deleted wherever it sat, white belly and all. Auto now
|
|
801
|
+
; falls back to flooding *brightness* in from the border, which removes
|
|
802
|
+
; the same page and cannot reach an interior.
|
|
803
|
+
t.ok("auto still removes a page whose border is not one flat color" (au.backgroundRemoved()))
|
|
804
|
+
def autoWhite:int 0
|
|
805
|
+
def ai:int 0
|
|
806
|
+
while (ai < (au.layerCount())) {
|
|
807
|
+
def al:EvgTraceLayer (itemAt au.layers ai)
|
|
808
|
+
def ac:EVGColor (EVGColor.parse(al.fillHex))
|
|
809
|
+
if ((EvgBitmapTracer.lumaOf((to_int ac.r) (to_int ac.g) (to_int ac.b))) > 200) {
|
|
810
|
+
autoWhite = (autoWhite + (this.countLabel(au ai)))
|
|
811
|
+
}
|
|
812
|
+
ai = (ai + 1)
|
|
813
|
+
}
|
|
814
|
+
t.eqInt("and keeps the white inside the shape, which is not the page" autoWhite 120)
|
|
815
|
+
|
|
816
|
+
def namedOpts:EvgTraceOptions (EvgTraceOptions.defaults())
|
|
817
|
+
namedOpts.colorCount = 3
|
|
818
|
+
namedOpts.bgMode = "color"
|
|
819
|
+
namedOpts.bgColor = "#ffffff"
|
|
820
|
+
def nm:EvgBitmapTracer (EvgBitmapTracer.fromImageBuffer((this.makeBustedBorder()) namedOpts))
|
|
821
|
+
nm.trace()
|
|
822
|
+
t.ok("naming the color removes it anyway" (nm.backgroundRemoved()))
|
|
823
|
+
|
|
824
|
+
; 40×40 = 1600 px: a 4×40 band, a 28×26 square with a 12×10 white
|
|
825
|
+
; patch punched out of it, and the rest is page. Everything is hard
|
|
826
|
+
; edged, so these are exact.
|
|
827
|
+
t.eqInt("the page is gone" (this.countLabel(nm (0 - 1))) 712)
|
|
828
|
+
def keptWhite:int 0
|
|
829
|
+
def li:int 0
|
|
830
|
+
while (li < (nm.layerCount())) {
|
|
831
|
+
def layer:EvgTraceLayer (itemAt nm.layers li)
|
|
832
|
+
def c:EVGColor (EVGColor.parse(layer.fillHex))
|
|
833
|
+
def lum:int (EvgBitmapTracer.lumaOf((to_int c.r) (to_int c.g) (to_int c.b)))
|
|
834
|
+
if (lum > 200) {
|
|
835
|
+
keptWhite = (keptWhite + (this.countLabel(nm li)))
|
|
836
|
+
}
|
|
837
|
+
li = (li + 1)
|
|
838
|
+
}
|
|
839
|
+
t.eqInt("the white inside the shape is kept, and only that" keptWhite 120)
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
; A gentle left-to-right ramp with one hard-edged block sitting in it. The
|
|
843
|
+
; ramp has no edge in it at all; the block has a real one. A quantizer cuts
|
|
844
|
+
; the ramp into bands and draws contours the picture does not have.
|
|
845
|
+
fn makeRampWithBlock:ImageBuffer () {
|
|
846
|
+
def img (new ImageBuffer())
|
|
847
|
+
img.init(60 40)
|
|
848
|
+
def y:int 0
|
|
849
|
+
while (y < 40) {
|
|
850
|
+
def x:int 0
|
|
851
|
+
while (x < 60) {
|
|
852
|
+
def g:int (80 + (idiv (x * 110) 60))
|
|
853
|
+
img.setPixelRGB(x y g g g)
|
|
854
|
+
x = (x + 1)
|
|
855
|
+
}
|
|
856
|
+
y = (y + 1)
|
|
857
|
+
}
|
|
858
|
+
y = 20
|
|
859
|
+
while (y < 30) {
|
|
860
|
+
def x2:int 20
|
|
861
|
+
while (x2 < 30) {
|
|
862
|
+
img.setPixelRGB(x2 y 30 60 200)
|
|
863
|
+
x2 = (x2 + 1)
|
|
864
|
+
}
|
|
865
|
+
y = (y + 1)
|
|
866
|
+
}
|
|
867
|
+
return img
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
; How many times the label changes along one row — a band boundary in the
|
|
871
|
+
; ramp is exactly such a change, and row 5 is above the block, so on this
|
|
872
|
+
; image every change it counts is a false contour.
|
|
873
|
+
fn labelChangesInRow:int (tr:EvgBitmapTracer row:int) {
|
|
874
|
+
def n:int 0
|
|
875
|
+
def x:int 1
|
|
876
|
+
while (x < 60) {
|
|
877
|
+
def a:int (itemAt tr.labels ((row * 60) + x - 1))
|
|
878
|
+
def b:int (itemAt tr.labels ((row * 60) + x))
|
|
879
|
+
if (a != b) {
|
|
880
|
+
n = (n + 1)
|
|
881
|
+
}
|
|
882
|
+
x = (x + 1)
|
|
883
|
+
}
|
|
884
|
+
return n
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
fn traceRamp:EvgBitmapTracer (mode:string spread:int) {
|
|
888
|
+
def opts:EvgTraceOptions (EvgTraceOptions.defaults())
|
|
889
|
+
opts.colorCount = 4
|
|
890
|
+
opts.bgMode = "none"
|
|
891
|
+
opts.minRegion = 0
|
|
892
|
+
opts.contourMode = mode
|
|
893
|
+
opts.contourSpread = spread
|
|
894
|
+
def tr:EvgBitmapTracer (EvgBitmapTracer.fromImageBuffer((this.makeRampWithBlock()) opts))
|
|
895
|
+
tr.trace()
|
|
896
|
+
return tr
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
fn testContourSmoothing:void (t:EvgTest) {
|
|
900
|
+
def d:EvgTraceOptions (EvgTraceOptions.defaults())
|
|
901
|
+
t.ok("contour smoothing is off by default" ((d.contourMode) == "off"))
|
|
902
|
+
|
|
903
|
+
def off:EvgBitmapTracer (this.traceRamp("off" 48))
|
|
904
|
+
def on:EvgBitmapTracer (this.traceRamp("smooth" 48))
|
|
905
|
+
; The ramp spans 110 levels, more than the default leash of 48, so the
|
|
906
|
+
; region stops growing once and one seam is left. That is the leash
|
|
907
|
+
; working, and the price of not letting a gradient walk off the picture.
|
|
908
|
+
def loose:EvgBitmapTracer (this.traceRamp("smooth" 200))
|
|
909
|
+
|
|
910
|
+
def bandsOff:int (this.labelChangesInRow(off 5))
|
|
911
|
+
def bandsOn:int (this.labelChangesInRow(on 5))
|
|
912
|
+
def bandsLoose:int (this.labelChangesInRow(loose 5))
|
|
913
|
+
t.ok("the ramp really is banded without it" (bandsOff >= 2))
|
|
914
|
+
t.ok("smoothing cuts the banding" (bandsOn < bandsOff))
|
|
915
|
+
t.eqInt("and removes it outright once the leash is long enough" bandsLoose 0)
|
|
916
|
+
|
|
917
|
+
; And the block, whose edge is real, must still be its own region.
|
|
918
|
+
def inside:int (itemAt loose.labels ((25 * 60) + 25))
|
|
919
|
+
def outside:int (itemAt loose.labels ((25 * 60) + 5))
|
|
920
|
+
t.ok("the hard edge is still an edge" (inside != outside))
|
|
921
|
+
; Its whole 10×10 body kept one label, so nothing leaked into it.
|
|
922
|
+
def body:int 0
|
|
923
|
+
def y:int 20
|
|
924
|
+
while (y < 30) {
|
|
925
|
+
def x:int 20
|
|
926
|
+
while (x < 30) {
|
|
927
|
+
if ((itemAt loose.labels ((y * 60) + x)) == inside) {
|
|
928
|
+
body = (body + 1)
|
|
929
|
+
}
|
|
930
|
+
x = (x + 1)
|
|
931
|
+
}
|
|
932
|
+
y = (y + 1)
|
|
933
|
+
}
|
|
934
|
+
; Even with the leash off, the hard edge holds it together.
|
|
935
|
+
t.eqInt("the block is whole" body 100)
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
; Three fields side by side: one even, one that ramps in a straight line,
|
|
939
|
+
; and one that is a bright spot falling off in every direction. Only flat
|
|
940
|
+
; and linear are emitted — a radial fit is held back because away from a
|
|
941
|
+
; genuinely round region its cone infers structure the picture does not
|
|
942
|
+
; contain — so the spot is expected to come out flat or as a ramp, not as
|
|
943
|
+
; a radial gradient.
|
|
944
|
+
fn makeThreeFields:ImageBuffer () {
|
|
945
|
+
def img (new ImageBuffer())
|
|
946
|
+
img.init(150 50)
|
|
947
|
+
def y:int 0
|
|
948
|
+
while (y < 50) {
|
|
949
|
+
def x:int 0
|
|
950
|
+
while (x < 150) {
|
|
951
|
+
def v:int 0
|
|
952
|
+
if (x < 50) {
|
|
953
|
+
v = 60
|
|
954
|
+
} {
|
|
955
|
+
if (x < 100) {
|
|
956
|
+
; Two levels per pixel: a real gradient is gentle, and
|
|
957
|
+
; a step above contourEdge would break the field into
|
|
958
|
+
; stripes before any of this got to look at it.
|
|
959
|
+
v = (30 + ((x - 50) * 2))
|
|
960
|
+
} {
|
|
961
|
+
def dx:int (x - 125)
|
|
962
|
+
def dy:int (y - 25)
|
|
963
|
+
def r2:int ((dx * dx) + (dy * dy))
|
|
964
|
+
def r:int (to_int (sqrt (to_double r2)))
|
|
965
|
+
v = (240 - (r * 2))
|
|
966
|
+
if (v < 20) {
|
|
967
|
+
v = 20
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
img.setPixelRGB(x y v v v)
|
|
972
|
+
x = (x + 1)
|
|
973
|
+
}
|
|
974
|
+
y = (y + 1)
|
|
975
|
+
}
|
|
976
|
+
return img
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
fn countKind:int (tr:EvgBitmapTracer kind:string) {
|
|
980
|
+
def n:int 0
|
|
981
|
+
def i:int 0
|
|
982
|
+
while (i < (tr.layerCount())) {
|
|
983
|
+
def layer:EvgTraceLayer (itemAt tr.layers i)
|
|
984
|
+
if ((layer.fillKind) == kind) {
|
|
985
|
+
n = (n + 1)
|
|
986
|
+
}
|
|
987
|
+
i = (i + 1)
|
|
988
|
+
}
|
|
989
|
+
return n
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
fn testGradientFills:void (t:EvgTest) {
|
|
993
|
+
def d:EvgTraceOptions (EvgTraceOptions.defaults())
|
|
994
|
+
t.ok("gradient fills are off by default" ((d.gradientFill) == false))
|
|
995
|
+
|
|
996
|
+
def opts:EvgTraceOptions (EvgTraceOptions.defaults())
|
|
997
|
+
opts.colorCount = 6
|
|
998
|
+
opts.bgMode = "none"
|
|
999
|
+
opts.minRegion = 40
|
|
1000
|
+
opts.gradientFill = true
|
|
1001
|
+
def tr:EvgBitmapTracer (EvgBitmapTracer.fromImageBuffer((this.makeThreeFields()) opts))
|
|
1002
|
+
tr.trace()
|
|
1003
|
+
|
|
1004
|
+
; Each model is picked where it belongs, and the flat field stays flat —
|
|
1005
|
+
; a gradient with two equal stops is just a bigger way of saying one
|
|
1006
|
+
; color, so gradientGain has to refuse it.
|
|
1007
|
+
t.ok("the straight ramp gets a linear fill" ((this.countKind(tr "linear")) >= 1))
|
|
1008
|
+
t.ok("the even field stays flat" ((this.countKind(tr "flat")) >= 1))
|
|
1009
|
+
t.eqInt("no radial fill is ever emitted" (this.countKind(tr "radial")) 0)
|
|
1010
|
+
|
|
1011
|
+
; A linear layer must carry an axis with length, a radial one a radius.
|
|
1012
|
+
def i:int 0
|
|
1013
|
+
def checked:int 0
|
|
1014
|
+
while (i < (tr.layerCount())) {
|
|
1015
|
+
def layer:EvgTraceLayer (itemAt tr.layers i)
|
|
1016
|
+
if ((layer.fillKind) == "linear") {
|
|
1017
|
+
def dx:double (layer.gx1 - layer.gx0)
|
|
1018
|
+
def dy:double (layer.gy1 - layer.gy0)
|
|
1019
|
+
t.ok("the linear axis is not a point" (((dx * dx) + (dy * dy)) > 1.0))
|
|
1020
|
+
t.ok("its two stops differ" ((layer.stopA) != (layer.stopB)))
|
|
1021
|
+
checked = (checked + 1)
|
|
1022
|
+
}
|
|
1023
|
+
i = (i + 1)
|
|
1024
|
+
}
|
|
1025
|
+
t.ok("at least one linear fill was checked" (checked >= 1))
|
|
1026
|
+
|
|
1027
|
+
; The SVG has to declare what it references.
|
|
1028
|
+
def svg:string (tr.toSVG())
|
|
1029
|
+
t.ok("the SVG declares its gradients" ((indexOf svg "<defs>") >= 0))
|
|
1030
|
+
t.ok("and a linear one among them" ((indexOf svg "<linearGradient") >= 0))
|
|
1031
|
+
t.eqInt("and no radial one" (indexOf svg "<radialGradient") (0 - 1))
|
|
1032
|
+
t.ok("paths reference them by url()" ((indexOf svg "fill=\"url(#g") >= 0))
|
|
1033
|
+
|
|
1034
|
+
; Off, the same image comes back as flat swatch layers and no defs.
|
|
1035
|
+
def plain:EvgTraceOptions (EvgTraceOptions.defaults())
|
|
1036
|
+
plain.colorCount = 6
|
|
1037
|
+
plain.bgMode = "none"
|
|
1038
|
+
plain.minRegion = 40
|
|
1039
|
+
def flatTr:EvgBitmapTracer (EvgBitmapTracer.fromImageBuffer((this.makeThreeFields()) plain))
|
|
1040
|
+
flatTr.trace()
|
|
1041
|
+
t.eqInt("nothing but flat fills when it is off" (this.countKind(flatTr "flat")) (flatTr.layerCount()))
|
|
1042
|
+
t.ok("and no defs block" ((indexOf (flatTr.toSVG()) "<defs>") < 0))
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
; A ramp with a block sitting on it whose own tone ramps too, so the
|
|
1046
|
+
; contrast between them passes through zero somewhere along the boundary.
|
|
1047
|
+
; A partitioning flood walks through that one weak spot and the block is
|
|
1048
|
+
; gone — it is inside the surface now, and nothing later can tell.
|
|
1049
|
+
fn makeVanishingBoundary:ImageBuffer () {
|
|
1050
|
+
def img (new ImageBuffer())
|
|
1051
|
+
img.init(120 80)
|
|
1052
|
+
def y:int 0
|
|
1053
|
+
while (y < 80) {
|
|
1054
|
+
def x:int 0
|
|
1055
|
+
while (x < 120) {
|
|
1056
|
+
def v:int (150 + (idiv (x * 60) 120))
|
|
1057
|
+
if ((y >= 24) && (y < 56) && (x >= 32) && (x < 88)) {
|
|
1058
|
+
v = (v + (idiv ((x - 60) * 9) 10))
|
|
1059
|
+
}
|
|
1060
|
+
img.setPixelRGB(x y v v v)
|
|
1061
|
+
x = (x + 1)
|
|
1062
|
+
}
|
|
1063
|
+
y = (y + 1)
|
|
1064
|
+
}
|
|
1065
|
+
return img
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
fn traceVanishing:EvgBitmapTracer (mode:string grad:boolean gain:int) {
|
|
1069
|
+
def opts:EvgTraceOptions (EvgTraceOptions.defaults())
|
|
1070
|
+
opts.colorCount = 6
|
|
1071
|
+
opts.bgMode = "none"
|
|
1072
|
+
opts.minRegion = 20
|
|
1073
|
+
opts.contourMode = mode
|
|
1074
|
+
opts.gradientFill = grad
|
|
1075
|
+
opts.gradientGain = gain
|
|
1076
|
+
def tr:EvgBitmapTracer (EvgBitmapTracer.fromImageBuffer((this.makeVanishingBoundary()) opts))
|
|
1077
|
+
tr.trace()
|
|
1078
|
+
return tr
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
fn testOverlayRecoversASwallowedShape:void (t:EvgTest) {
|
|
1082
|
+
def d:EvgTraceOptions (EvgTraceOptions.defaults())
|
|
1083
|
+
t.ok("overlay is not the default" ((d.contourMode) != "overlay"))
|
|
1084
|
+
t.eqInt("the similarity stop has a default" (d.overlaySimilar) 8)
|
|
1085
|
+
|
|
1086
|
+
def smooth:EvgBitmapTracer (this.traceVanishing("smooth" false 15))
|
|
1087
|
+
def overlay:EvgBitmapTracer (this.traceVanishing("overlay" true 15))
|
|
1088
|
+
; Partitioned, the block is swallowed and the picture collapses to the
|
|
1089
|
+
; surface that ate it. Stacked, it is drawn again on top.
|
|
1090
|
+
t.ok("the block is lost when the shapes partition" ((smooth.layerCount()) <= 2))
|
|
1091
|
+
t.ok("and comes back when they stack" ((overlay.layerCount()) > (smooth.layerCount())))
|
|
1092
|
+
|
|
1093
|
+
; Layer 0 is the base surface the stack is drawn onto, so the shapes
|
|
1094
|
+
; that matter start at 1 — and at least one of them has to say something
|
|
1095
|
+
; the base does not, or the stack is decoration.
|
|
1096
|
+
def base:EvgTraceLayer (itemAt overlay.layers 0)
|
|
1097
|
+
def differs:int 0
|
|
1098
|
+
def li:int 1
|
|
1099
|
+
while (li < (overlay.layerCount())) {
|
|
1100
|
+
def l:EvgTraceLayer (itemAt overlay.layers li)
|
|
1101
|
+
if ((l.fillHex) != (base.fillHex)) {
|
|
1102
|
+
differs = (differs + 1)
|
|
1103
|
+
}
|
|
1104
|
+
li = (li + 1)
|
|
1105
|
+
}
|
|
1106
|
+
t.ok("a stacked shape says something the base does not" (differs >= 1))
|
|
1107
|
+
|
|
1108
|
+
; gradientGain is the restraint: at 100% nothing can out-argue flat.
|
|
1109
|
+
def strict:EvgBitmapTracer (this.traceVanishing("overlay" true 100))
|
|
1110
|
+
def i:int 0
|
|
1111
|
+
def shaped:int 0
|
|
1112
|
+
while (i < (strict.layerCount())) {
|
|
1113
|
+
def layer:EvgTraceLayer (itemAt strict.layers i)
|
|
1114
|
+
if ((layer.fillKind) != "flat") {
|
|
1115
|
+
shaped = (shaped + 1)
|
|
1116
|
+
}
|
|
1117
|
+
i = (i + 1)
|
|
1118
|
+
}
|
|
1119
|
+
t.eqInt("a gain of 100 leaves every fill flat" shaped 0)
|
|
1120
|
+
|
|
1121
|
+
; And no overlay shape gets a radial fill either.
|
|
1122
|
+
def rad:int 0
|
|
1123
|
+
def j:int 0
|
|
1124
|
+
while (j < (strict.layerCount())) {
|
|
1125
|
+
def l2:EvgTraceLayer (itemAt strict.layers j)
|
|
1126
|
+
if ((l2.fillKind) == "radial") {
|
|
1127
|
+
rad = (rad + 1)
|
|
1128
|
+
}
|
|
1129
|
+
j = (j + 1)
|
|
1130
|
+
}
|
|
1131
|
+
t.eqInt("no radial fill among the overlay shapes" rad 0)
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
; One straight boundary and five specks well apart from each other. Both
|
|
1135
|
+
; are steps; only one is a boundary. The filter has to say which.
|
|
1136
|
+
fn makeLineAndSpecks:ImageBuffer () {
|
|
1137
|
+
def img (new ImageBuffer())
|
|
1138
|
+
img.init(60 40)
|
|
1139
|
+
def y:int 0
|
|
1140
|
+
while (y < 40) {
|
|
1141
|
+
def x:int 0
|
|
1142
|
+
while (x < 60) {
|
|
1143
|
+
def v:int 110
|
|
1144
|
+
if (x >= 30) {
|
|
1145
|
+
v = 190
|
|
1146
|
+
}
|
|
1147
|
+
img.setPixelRGB(x y v v v)
|
|
1148
|
+
x = (x + 1)
|
|
1149
|
+
}
|
|
1150
|
+
y = (y + 1)
|
|
1151
|
+
}
|
|
1152
|
+
img.setPixelRGB(8 8 160 160 160)
|
|
1153
|
+
img.setPixelRGB(14 20 160 160 160)
|
|
1154
|
+
img.setPixelRGB(20 32 160 160 160)
|
|
1155
|
+
img.setPixelRGB(44 10 160 160 160)
|
|
1156
|
+
img.setPixelRGB(50 26 160 160 160)
|
|
1157
|
+
return img
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
fn traceLineAndSpecks:EvgBitmapTracer (minRun:int) {
|
|
1161
|
+
def opts:EvgTraceOptions (EvgTraceOptions.defaults())
|
|
1162
|
+
opts.colorCount = 2
|
|
1163
|
+
opts.bgMode = "none"
|
|
1164
|
+
opts.minRegion = 0
|
|
1165
|
+
opts.contourMode = "smooth"
|
|
1166
|
+
opts.edgeMinRun = minRun
|
|
1167
|
+
def tr:EvgBitmapTracer (EvgBitmapTracer.fromImageBuffer((this.makeLineAndSpecks()) opts))
|
|
1168
|
+
tr.trace()
|
|
1169
|
+
return tr
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
; A noisy ramp: the shape of a photograph as far as the overlay tracer is
|
|
1173
|
+
; concerned. Every pixel differs a little from its neighbour, so there is no
|
|
1174
|
+
; flat surface anywhere and no clean edge either — which is precisely the
|
|
1175
|
+
; input that used to shatter into thousands of one-color shapes.
|
|
1176
|
+
fn makeNoisyRamp:ImageBuffer () {
|
|
1177
|
+
def img (new ImageBuffer())
|
|
1178
|
+
img.init(60 40)
|
|
1179
|
+
def seed:int 987
|
|
1180
|
+
def y:int 0
|
|
1181
|
+
while (y < 40) {
|
|
1182
|
+
def x:int 0
|
|
1183
|
+
while (x < 60) {
|
|
1184
|
+
seed = (((seed * 75) + 74) % 65537)
|
|
1185
|
+
def jitter:int ((seed % 25) - 12)
|
|
1186
|
+
def v:int ((idiv (x * 200) 60) + jitter)
|
|
1187
|
+
if (v < 0) {
|
|
1188
|
+
v = 0
|
|
1189
|
+
}
|
|
1190
|
+
if (v > 255) {
|
|
1191
|
+
v = 255
|
|
1192
|
+
}
|
|
1193
|
+
img.setPixelRGB(x y v (idiv (v * 3) 4) (idiv v 2))
|
|
1194
|
+
x = (x + 1)
|
|
1195
|
+
}
|
|
1196
|
+
y = (y + 1)
|
|
1197
|
+
}
|
|
1198
|
+
return img
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
fn traceNoisyOverlay:EvgBitmapTracer (colors:int minRegion:int) {
|
|
1202
|
+
def opts:EvgTraceOptions (EvgTraceOptions.defaults())
|
|
1203
|
+
opts.colorCount = colors
|
|
1204
|
+
opts.bgMode = "none"
|
|
1205
|
+
opts.minRegion = minRegion
|
|
1206
|
+
opts.contourMode = "overlay"
|
|
1207
|
+
def tr:EvgBitmapTracer (EvgBitmapTracer.fromImageBuffer((this.makeNoisyRamp()) opts))
|
|
1208
|
+
tr.trace()
|
|
1209
|
+
return tr
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
fn distinctFills:int (tr:EvgBitmapTracer) {
|
|
1213
|
+
def seen:[string]
|
|
1214
|
+
def i:int 0
|
|
1215
|
+
while (i < (tr.layerCount())) {
|
|
1216
|
+
def layer:EvgTraceLayer (itemAt tr.layers i)
|
|
1217
|
+
def hex:string layer.fillHex
|
|
1218
|
+
def found:boolean false
|
|
1219
|
+
def j:int 0
|
|
1220
|
+
while (j < (array_length seen)) {
|
|
1221
|
+
if ((itemAt seen j) == hex) {
|
|
1222
|
+
found = true
|
|
1223
|
+
}
|
|
1224
|
+
j = (j + 1)
|
|
1225
|
+
}
|
|
1226
|
+
if (found == false) {
|
|
1227
|
+
push seen hex
|
|
1228
|
+
}
|
|
1229
|
+
i = (i + 1)
|
|
1230
|
+
}
|
|
1231
|
+
return (array_length seen)
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
; Overlay used to paint every shape its own mean color and queue the same
|
|
1235
|
+
; pixel as a seed once per neighbour that touched it. Together those made a
|
|
1236
|
+
; photograph come back as thousands of swatches under a slider that said
|
|
1237
|
+
; four, and cost ten seconds of CPU to do it.
|
|
1238
|
+
fn gradientTrace:EvgBitmapTracer (minRegion:int) {
|
|
1239
|
+
def o:EvgTraceOptions (EvgTraceOptions.defaults())
|
|
1240
|
+
o.colorCount = 4
|
|
1241
|
+
o.bgMode = "none"
|
|
1242
|
+
o.gradientFill = true
|
|
1243
|
+
o.minRegion = minRegion
|
|
1244
|
+
def tr:EvgBitmapTracer (EvgBitmapTracer.fromImageBuffer((this.makeRampWithBlock()) o))
|
|
1245
|
+
tr.trace()
|
|
1246
|
+
return tr
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
; Gradient fill had two faults that came from the same place. Every region
|
|
1250
|
+
; was painted its own measured mean, never quantized — a portrait asked for
|
|
1251
|
+
; ten swatches came back with 1128 — and the smallest-area control did
|
|
1252
|
+
; nothing, because absorbing ran a single pass and a small region can only
|
|
1253
|
+
; join a big-enough neighbour. On a photograph a small region's neighbours
|
|
1254
|
+
; are small too, so nothing moved: minRegion from 6 to 4000 shifted the
|
|
1255
|
+
; count by 7%.
|
|
1256
|
+
fn testGradientFillObeysItsControls:void (t:EvgTest) {
|
|
1257
|
+
def fine:EvgBitmapTracer (this.gradientTrace(6))
|
|
1258
|
+
t.ok("a gradient trace stays inside the color count" ((this.distinctFills(fine)) <= 4))
|
|
1259
|
+
|
|
1260
|
+
def coarse:EvgBitmapTracer (this.gradientTrace(400))
|
|
1261
|
+
t.ok("and the smallest area actually makes it draw fewer regions"
|
|
1262
|
+
((coarse.layerCount()) < (fine.layerCount())))
|
|
1263
|
+
t.ok("without inventing colors either" ((this.distinctFills(coarse)) <= 4))
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
fn testOverlayStaysWithinItsPalette:void (t:EvgTest) {
|
|
1267
|
+
def four:EvgBitmapTracer (this.traceNoisyOverlay(4 6))
|
|
1268
|
+
; The color count is the ceiling on the colors, not a suggestion. The
|
|
1269
|
+
; base surface under everything is drawn from the same palette, so it
|
|
1270
|
+
; adds no swatch of its own.
|
|
1271
|
+
t.ok("overlay never invents a color the palette does not have" ((this.distinctFills(four)) <= 4))
|
|
1272
|
+
|
|
1273
|
+
def twelve:EvgBitmapTracer (this.traceNoisyOverlay(12 6))
|
|
1274
|
+
t.ok("and the ceiling follows the count that was asked for" ((this.distinctFills(twelve)) <= 12))
|
|
1275
|
+
|
|
1276
|
+
; 2400 pixels of pure noise. A shape per handful of pixels is the
|
|
1277
|
+
; failure this guards: it is what made the SVG megabytes wide.
|
|
1278
|
+
t.ok("a noisy image does not become a shape per pixel" ((four.layerCount()) < 300))
|
|
1279
|
+
|
|
1280
|
+
; Overlay stands on the plain picture, so its floor is the plain result.
|
|
1281
|
+
; It used to stand on a single flat rectangle in the average color of
|
|
1282
|
+
; the frame, and when the shapes above failed to cover that rectangle —
|
|
1283
|
+
; on a portrait they reached the face only as slivers — the frame *was*
|
|
1284
|
+
; the result. Standing on the real thing, an overlay that adds nothing
|
|
1285
|
+
; costs nothing and one that is wrong is wrong only inside its own
|
|
1286
|
+
; outline.
|
|
1287
|
+
def plain:EvgTraceOptions (EvgTraceOptions.defaults())
|
|
1288
|
+
plain.colorCount = 4
|
|
1289
|
+
plain.bgMode = "none"
|
|
1290
|
+
plain.minRegion = 6
|
|
1291
|
+
def flat:EvgBitmapTracer (EvgBitmapTracer.fromImageBuffer((this.makeNoisyRamp()) plain))
|
|
1292
|
+
flat.trace()
|
|
1293
|
+
t.ok("overlay draws at least the whole plain picture" ((four.layerCount()) >= (flat.layerCount())))
|
|
1294
|
+
def sameStart:boolean true
|
|
1295
|
+
def li:int 0
|
|
1296
|
+
while (li < (flat.layerCount())) {
|
|
1297
|
+
def a:EvgTraceLayer (itemAt flat.layers li)
|
|
1298
|
+
def b:EvgTraceLayer (itemAt four.layers li)
|
|
1299
|
+
if ((a.fillHex) != (b.fillHex)) {
|
|
1300
|
+
sameStart = false
|
|
1301
|
+
}
|
|
1302
|
+
li = (li + 1)
|
|
1303
|
+
}
|
|
1304
|
+
t.ok("and the plain picture is what it starts from, swatch for swatch" sameStart)
|
|
1305
|
+
|
|
1306
|
+
; A ramp of noise gives an overlay nothing to say — the plain picture
|
|
1307
|
+
; already describes it — and the mode correctly adds no shape at all to
|
|
1308
|
+
; it. That is the point of standing on the plain picture, and it is why
|
|
1309
|
+
; the similarity control is exercised on the image where overlays do
|
|
1310
|
+
; fire, below.
|
|
1311
|
+
t.eqInt("noise alone earns no overlay at all" (four.layerCount()) (flat.layerCount()))
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
; An overlay earns its place by differing from what it sits on, so the
|
|
1315
|
+
; similarity tolerance decides how many there are. Asked against a blank
|
|
1316
|
+
; paint map, as it used to be, every overlay "showed" and the control did
|
|
1317
|
+
; nothing whatever — the mode drew thousands of them on any photograph and
|
|
1318
|
+
; the same thousands whatever this was set to.
|
|
1319
|
+
fn rampLayers:int (colors:int mode:string sim:int) {
|
|
1320
|
+
def o:EvgTraceOptions (EvgTraceOptions.defaults())
|
|
1321
|
+
o.colorCount = colors
|
|
1322
|
+
o.bgMode = "none"
|
|
1323
|
+
o.minRegion = 6
|
|
1324
|
+
o.contourMode = mode
|
|
1325
|
+
o.overlaySimilar = sim
|
|
1326
|
+
def tr:EvgBitmapTracer (EvgBitmapTracer.fromImageBuffer((this.makeNoisyRamp()) o))
|
|
1327
|
+
tr.trace()
|
|
1328
|
+
return (tr.layerCount())
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
; Left half: a smooth ramp — busy, but busy in the way a gradient is busy.
|
|
1332
|
+
; Right half: a flat field with a small patch where black, white and a mid
|
|
1333
|
+
; tone meet inside a few pixels, the way they do in an eye.
|
|
1334
|
+
fn makeRampAndEye:ImageBuffer () {
|
|
1335
|
+
def img (new ImageBuffer())
|
|
1336
|
+
img.init(80 40)
|
|
1337
|
+
def y:int 0
|
|
1338
|
+
while (y < 40) {
|
|
1339
|
+
def x:int 0
|
|
1340
|
+
while (x < 80) {
|
|
1341
|
+
if (x < 40) {
|
|
1342
|
+
def v:int (idiv (x * 250) 40)
|
|
1343
|
+
img.setPixelRGB(x y v v v)
|
|
1344
|
+
} {
|
|
1345
|
+
img.setPixelRGB(x y 130 130 130)
|
|
1346
|
+
}
|
|
1347
|
+
x = (x + 1)
|
|
1348
|
+
}
|
|
1349
|
+
y = (y + 1)
|
|
1350
|
+
}
|
|
1351
|
+
; the "eye": white sclera, black pupil, mid lash, inside 10x8 pixels
|
|
1352
|
+
def ey:int 16
|
|
1353
|
+
while (ey < 24) {
|
|
1354
|
+
def ex:int 55
|
|
1355
|
+
while (ex < 65) {
|
|
1356
|
+
img.setPixelRGB(ex ey 255 255 255)
|
|
1357
|
+
ex = (ex + 1)
|
|
1358
|
+
}
|
|
1359
|
+
ey = (ey + 1)
|
|
1360
|
+
}
|
|
1361
|
+
ey = 18
|
|
1362
|
+
while (ey < 22) {
|
|
1363
|
+
def ex2:int 58
|
|
1364
|
+
while (ex2 < 62) {
|
|
1365
|
+
img.setPixelRGB(ex2 ey 0 0 0)
|
|
1366
|
+
ex2 = (ex2 + 1)
|
|
1367
|
+
}
|
|
1368
|
+
ey = (ey + 1)
|
|
1369
|
+
}
|
|
1370
|
+
def ex3:int 55
|
|
1371
|
+
while (ex3 < 65) {
|
|
1372
|
+
img.setPixelRGB(ex3 15 60 40 40)
|
|
1373
|
+
ex3 = (ex3 + 1)
|
|
1374
|
+
}
|
|
1375
|
+
return img
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
; The mode was asked to adapt: spend precision where several plainly
|
|
1379
|
+
; different colors meet in a few pixels, and not where the picture merely
|
|
1380
|
+
; changes smoothly. Both halves of the test matter — a gradient passes a
|
|
1381
|
+
; contrast test and a patch of noise inside one flat color passes a variety
|
|
1382
|
+
; test, and neither is detail.
|
|
1383
|
+
; A detail neighbourhood quantizes itself with its own palette, which is
|
|
1384
|
+
; what "zoom in on it" means done in place — and it is the only way the eye
|
|
1385
|
+
; of a portrait keeps its lid and its opening instead of flattening into one
|
|
1386
|
+
; dark almond. The global palette cannot do it: it is counting a whole frame
|
|
1387
|
+
; and a face is mostly cheek, so an eye gets two swatches out of ten.
|
|
1388
|
+
fn testDetailNeighbourhoodGetsItsOwnPalette:void (t:EvgTest) {
|
|
1389
|
+
def d:EvgTraceOptions (EvgTraceOptions.defaults())
|
|
1390
|
+
t.eqInt("a detail neighbourhood quantizes itself in four" (d.detailColors) 4)
|
|
1391
|
+
t.eqInt("and neighbourhoods share one palette by default" (d.detailColorMerge) 16)
|
|
1392
|
+
t.eqInt("with a ceiling on how much it may grow" (d.detailColorMax) 12)
|
|
1393
|
+
|
|
1394
|
+
def opts:EvgTraceOptions (EvgTraceOptions.defaults())
|
|
1395
|
+
opts.colorCount = 6
|
|
1396
|
+
opts.bgMode = "none"
|
|
1397
|
+
opts.contourMode = "overlay"
|
|
1398
|
+
opts.detailMinShare = 0
|
|
1399
|
+
def tr:EvgBitmapTracer (EvgBitmapTracer.fromImageBuffer((this.makeRampAndEye()) opts))
|
|
1400
|
+
tr.trace()
|
|
1401
|
+
|
|
1402
|
+
def off:EvgTraceOptions (EvgTraceOptions.defaults())
|
|
1403
|
+
off.colorCount = 6
|
|
1404
|
+
off.bgMode = "none"
|
|
1405
|
+
off.contourMode = "overlay"
|
|
1406
|
+
off.detailColors = 0
|
|
1407
|
+
def bare:EvgBitmapTracer (EvgBitmapTracer.fromImageBuffer((this.makeRampAndEye()) off))
|
|
1408
|
+
bare.trace()
|
|
1409
|
+
|
|
1410
|
+
t.ok("the eye earns shapes the global palette did not pay for" ((tr.layerCount()) > (bare.layerCount())))
|
|
1411
|
+
|
|
1412
|
+
; And they are the eye's own colors. The point is the extremes: six
|
|
1413
|
+
; swatches spread over a ramp leave nothing near white or near black for
|
|
1414
|
+
; an eye, so both its sclera and its pupil round to something mid-grey.
|
|
1415
|
+
def darkest:int 256
|
|
1416
|
+
def lightest:int (0 - 1)
|
|
1417
|
+
def li:int 0
|
|
1418
|
+
while (li < (tr.layerCount())) {
|
|
1419
|
+
def l:EvgTraceLayer (itemAt tr.layers li)
|
|
1420
|
+
def col:EVGColor (EVGColor.parse(l.fillHex))
|
|
1421
|
+
def lv:int (EvgBitmapTracer.lumaOf((to_int col.r) (to_int col.g) (to_int col.b)))
|
|
1422
|
+
if (lv < darkest) {
|
|
1423
|
+
darkest = lv
|
|
1424
|
+
}
|
|
1425
|
+
if (lv > lightest) {
|
|
1426
|
+
lightest = lv
|
|
1427
|
+
}
|
|
1428
|
+
li = (li + 1)
|
|
1429
|
+
}
|
|
1430
|
+
t.ok("with a darker shape than the global palette held" (darkest < 40))
|
|
1431
|
+
t.ok("and a lighter one" (lightest > 200))
|
|
1432
|
+
|
|
1433
|
+
; Neighbourhoods share one palette rather than each inventing its own.
|
|
1434
|
+
; Derived in ignorance of one another they produce near-duplicates by
|
|
1435
|
+
; the dozen — on a portrait, 456 fills for 561 shapes — and merging what
|
|
1436
|
+
; is within a few levels costs nothing anyone can see.
|
|
1437
|
+
def wide:EvgTraceOptions (EvgTraceOptions.defaults())
|
|
1438
|
+
wide.colorCount = 6
|
|
1439
|
+
wide.bgMode = "none"
|
|
1440
|
+
wide.contourMode = "overlay"
|
|
1441
|
+
wide.detailMinShare = 0
|
|
1442
|
+
wide.detailColorMerge = 0
|
|
1443
|
+
def unmerged:EvgBitmapTracer (EvgBitmapTracer.fromImageBuffer((this.makeRampAndEye()) wide))
|
|
1444
|
+
unmerged.trace()
|
|
1445
|
+
t.ok("merging never adds colors" ((this.distinctFills(tr)) <= (this.distinctFills(unmerged))))
|
|
1446
|
+
|
|
1447
|
+
; Merging bounds nothing on its own: it folds together colors that are
|
|
1448
|
+
; nearly the same, and colors that are genuinely different keep arriving
|
|
1449
|
+
; one per neighbourhood per band. A busy picture has hundreds of
|
|
1450
|
+
; neighbourhoods, and a portrait asked for six swatches came back with
|
|
1451
|
+
; 103. Past the ceiling a band takes the closest color already in hand,
|
|
1452
|
+
; however far away that is.
|
|
1453
|
+
def capped:EvgTraceOptions (EvgTraceOptions.defaults())
|
|
1454
|
+
capped.colorCount = 6
|
|
1455
|
+
capped.bgMode = "none"
|
|
1456
|
+
capped.contourMode = "overlay"
|
|
1457
|
+
capped.detailMinShare = 0
|
|
1458
|
+
capped.detailColorMax = 2
|
|
1459
|
+
def few:EvgBitmapTracer (EvgBitmapTracer.fromImageBuffer((this.makeRampAndEye()) capped))
|
|
1460
|
+
few.trace()
|
|
1461
|
+
t.ok("the ceiling holds the color count to what was asked plus it" ((this.distinctFills(few)) <= 8))
|
|
1462
|
+
t.ok("and never costs shapes, only the colors they are painted"
|
|
1463
|
+
((few.layerCount()) >= (tr.layerCount())))
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
fn testDetailMaskFindsTheEyeNotTheRamp:void (t:EvgTest) {
|
|
1467
|
+
def d:EvgTraceOptions (EvgTraceOptions.defaults())
|
|
1468
|
+
t.eqInt("the detail window wants several swatches" (d.detailSwatches) 4)
|
|
1469
|
+
t.ok("and true color inside it is opt-in" ((d.detailTrueColor) == false))
|
|
1470
|
+
|
|
1471
|
+
def opts:EvgTraceOptions (EvgTraceOptions.defaults())
|
|
1472
|
+
opts.colorCount = 6
|
|
1473
|
+
opts.bgMode = "none"
|
|
1474
|
+
opts.contourMode = "overlay"
|
|
1475
|
+
def tr:EvgBitmapTracer (EvgBitmapTracer.fromImageBuffer((this.makeRampAndEye()) opts))
|
|
1476
|
+
tr.trace()
|
|
1477
|
+
|
|
1478
|
+
; What the mask marks is where the different colors *meet* — the band
|
|
1479
|
+
; around the eye rather than the flat middle of the pupil, which on its
|
|
1480
|
+
; own is as plain as a cheek. That is the part a coarse pass rounds away.
|
|
1481
|
+
def onEye:int 0
|
|
1482
|
+
def y:int 12
|
|
1483
|
+
while (y < 28) {
|
|
1484
|
+
def x:int 50
|
|
1485
|
+
while (x < 70) {
|
|
1486
|
+
if ((itemAt tr.detailMask ((y * 80) + x)) == 1) {
|
|
1487
|
+
onEye = (onEye + 1)
|
|
1488
|
+
}
|
|
1489
|
+
x = (x + 1)
|
|
1490
|
+
}
|
|
1491
|
+
y = (y + 1)
|
|
1492
|
+
}
|
|
1493
|
+
t.ok("the eye is marked as needing fine work" (onEye >= 8))
|
|
1494
|
+
|
|
1495
|
+
; And nothing else in the picture is — not one pixel. The ramp is the
|
|
1496
|
+
; case that matters: it is the steepest thing here and it is still not
|
|
1497
|
+
; detail, because a gradient is one or two swatches wide at any window.
|
|
1498
|
+
def total:int 0
|
|
1499
|
+
def i:int 0
|
|
1500
|
+
while (i < (array_length tr.detailMask)) {
|
|
1501
|
+
if ((itemAt tr.detailMask i) == 1) {
|
|
1502
|
+
total = (total + 1)
|
|
1503
|
+
}
|
|
1504
|
+
i = (i + 1)
|
|
1505
|
+
}
|
|
1506
|
+
t.eqInt("and nowhere else in the picture is, the ramp included" total onEye)
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
fn testOverlaySimilarityDecidesHowManyStack:void (t:EvgTest) {
|
|
1510
|
+
def plain:int (this.rampLayers(6 "off" 8))
|
|
1511
|
+
def deflt:int (this.rampLayers(6 "overlay" 8))
|
|
1512
|
+
def loose:int (this.rampLayers(6 "overlay" 40))
|
|
1513
|
+
|
|
1514
|
+
; Six swatches leave one stretch of this ramp the plain picture cannot
|
|
1515
|
+
; describe, and an overlay goes on top of it.
|
|
1516
|
+
t.ok("an overlay is drawn where the plain picture falls short" (deflt > plain))
|
|
1517
|
+
; Told that a wider difference still counts as the same color, the same
|
|
1518
|
+
; overlay stops being worth drawing and is gone. Against the blank paint
|
|
1519
|
+
; map this control had no effect whatever, because the comparison it
|
|
1520
|
+
; makes was being made against nothing.
|
|
1521
|
+
t.ok("and a loose similarity limit drops it again" (loose < deflt))
|
|
1522
|
+
t.eqInt("leaving exactly the plain picture" loose plain)
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
fn testEdgeFilterSeparatesNoiseFromBoundary:void (t:EvgTest) {
|
|
1526
|
+
def d:EvgTraceOptions (EvgTraceOptions.defaults())
|
|
1527
|
+
t.eqInt("the edge filter is on by default" (d.edgeMinRun) 3)
|
|
1528
|
+
|
|
1529
|
+
def tr:EvgBitmapTracer (this.traceLineAndSpecks(3))
|
|
1530
|
+
; The boundary is marked the whole way down.
|
|
1531
|
+
def onLine:int 0
|
|
1532
|
+
def y:int 2
|
|
1533
|
+
while (y < 38) {
|
|
1534
|
+
if (((itemAt tr.edgeMask ((y * 60) + 29)) == 1) || ((itemAt tr.edgeMask ((y * 60) + 30)) == 1)) {
|
|
1535
|
+
onLine = (onLine + 1)
|
|
1536
|
+
}
|
|
1537
|
+
y = (y + 1)
|
|
1538
|
+
}
|
|
1539
|
+
t.eqInt("the straight boundary is marked all the way down" onLine 36)
|
|
1540
|
+
|
|
1541
|
+
; The specks are steps of the same size and are not marked at all: each
|
|
1542
|
+
; is alone, so it fails the continuity test however strong it is.
|
|
1543
|
+
def onSpeck:int 0
|
|
1544
|
+
if ((itemAt tr.edgeMask ((8 * 60) + 8)) == 1) {
|
|
1545
|
+
onSpeck = (onSpeck + 1)
|
|
1546
|
+
}
|
|
1547
|
+
if ((itemAt tr.edgeMask ((20 * 60) + 14)) == 1) {
|
|
1548
|
+
onSpeck = (onSpeck + 1)
|
|
1549
|
+
}
|
|
1550
|
+
if ((itemAt tr.edgeMask ((32 * 60) + 20)) == 1) {
|
|
1551
|
+
onSpeck = (onSpeck + 1)
|
|
1552
|
+
}
|
|
1553
|
+
if ((itemAt tr.edgeMask ((10 * 60) + 44)) == 1) {
|
|
1554
|
+
onSpeck = (onSpeck + 1)
|
|
1555
|
+
}
|
|
1556
|
+
if ((itemAt tr.edgeMask ((26 * 60) + 50)) == 1) {
|
|
1557
|
+
onSpeck = (onSpeck + 1)
|
|
1558
|
+
}
|
|
1559
|
+
t.eqInt("and not one isolated speck is" onSpeck 0)
|
|
1560
|
+
|
|
1561
|
+
; At 0 the filter is off and every step is taken at face value again,
|
|
1562
|
+
; which is what the old behaviour was.
|
|
1563
|
+
def off:EvgBitmapTracer (this.traceLineAndSpecks(0))
|
|
1564
|
+
def marked:int 0
|
|
1565
|
+
def i:int 0
|
|
1566
|
+
while (i < (array_length off.edgeMask)) {
|
|
1567
|
+
if ((itemAt off.edgeMask i) == 1) {
|
|
1568
|
+
marked = (marked + 1)
|
|
1569
|
+
}
|
|
1570
|
+
i = (i + 1)
|
|
1571
|
+
}
|
|
1572
|
+
t.eqInt("nothing is filtered when it is switched off" marked 0)
|
|
1573
|
+
|
|
1574
|
+
; And the default path is untouched by any of this.
|
|
1575
|
+
def plain:EvgTraceOptions (EvgTraceOptions.defaults())
|
|
1576
|
+
t.ok("contour smoothing is still off by default" ((plain.contourMode) == "off"))
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
; ----------------------------------------------------------------------
|
|
1580
|
+
; Compact path encoding (EvgTracePath)
|
|
1581
|
+
; ----------------------------------------------------------------------
|
|
1582
|
+
|
|
1583
|
+
fn testCompactNumberFormatting:void (t:EvgTest) {
|
|
1584
|
+
t.eqStr("a whole number keeps no point" (EvgTracePath.num(12.0 2)) "12")
|
|
1585
|
+
t.eqStr("trailing zeros go" (EvgTracePath.num(3.50 2)) "3.5")
|
|
1586
|
+
t.eqStr("interior zeros stay" (EvgTracePath.num(3.05 2)) "3.05")
|
|
1587
|
+
t.eqStr("a pure fraction loses its leading zero" (EvgTracePath.num(0.5 2)) ".5")
|
|
1588
|
+
t.eqStr("and so does a negative one" (EvgTracePath.num((0.0 - 0.5) 2)) "-.5")
|
|
1589
|
+
t.eqStr("rounding is to the asked-for place" (EvgTracePath.num(1.2345 2)) "1.23")
|
|
1590
|
+
t.eqStr("and one place is one place" (EvgTracePath.num(1.2345 1)) "1.2")
|
|
1591
|
+
t.eqStr("a value that rounds away is plain zero" (EvgTracePath.num((0.0 - 0.001) 2)) "0")
|
|
1592
|
+
|
|
1593
|
+
; The separator rule, which is where this went wrong: a minus sign
|
|
1594
|
+
; STARTS a number. Reading it as an interior character makes ".2-1"
|
|
1595
|
+
; look like one number that has already spent its dot, and the ".44"
|
|
1596
|
+
; after it is then written with no separator — ".2-1.44" is three
|
|
1597
|
+
; numbers where four were meant, and the path silently breaks there.
|
|
1598
|
+
t.eqStr("the last number is the last number" (EvgTracePath.lastNum("s.2-1")) "-1")
|
|
1599
|
+
t.eqStr("a dot is interior to it" (EvgTracePath.lastNum("c1.5")) "1.5")
|
|
1600
|
+
t.eqStr("nothing follows a command letter" (EvgTracePath.lastNum("M")) "")
|
|
1601
|
+
t.eqStr("a number may run onto one that spent its dot" (EvgTracePath.joinNum("c1.5" "1.5" ".25")) "c1.5.25")
|
|
1602
|
+
t.eqStr("but not onto one that did not" (EvgTracePath.joinNum("c-1" "-1" ".44")) "c-1 .44")
|
|
1603
|
+
t.eqStr("a minus never needs a separator" (EvgTracePath.joinNum("c5" "5" "-1")) "c5-1")
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
; Every command letter and the count of numbers it must be given.
|
|
1607
|
+
sfn arityOf:int (letter:string) {
|
|
1608
|
+
if ((indexOf "MmLl" letter) >= 0) {
|
|
1609
|
+
return 2
|
|
1610
|
+
}
|
|
1611
|
+
if ((indexOf "HhVv" letter) >= 0) {
|
|
1612
|
+
return 1
|
|
1613
|
+
}
|
|
1614
|
+
if ((indexOf "Cc" letter) >= 0) {
|
|
1615
|
+
return 6
|
|
1616
|
+
}
|
|
1617
|
+
if ((indexOf "SsQq" letter) >= 0) {
|
|
1618
|
+
return 4
|
|
1619
|
+
}
|
|
1620
|
+
if ((indexOf "Zz" letter) >= 0) {
|
|
1621
|
+
return 0
|
|
1622
|
+
}
|
|
1623
|
+
return (0 - 1)
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
; Count the numbers a real SVG parser would read out of `d`, and check each
|
|
1627
|
+
; command got exactly as many as it takes. A run-on that swallows a number
|
|
1628
|
+
; shows up here as an arity that is one short.
|
|
1629
|
+
fn miscountedCommands:int (d:string) {
|
|
1630
|
+
def bad:int 0
|
|
1631
|
+
def n:int (strlen d)
|
|
1632
|
+
def i:int 0
|
|
1633
|
+
def want:int 0
|
|
1634
|
+
def have:int 0
|
|
1635
|
+
def inNum:boolean false
|
|
1636
|
+
def hadDot:boolean false
|
|
1637
|
+
while (i < n) {
|
|
1638
|
+
def ch:string (substring d i (i + 1))
|
|
1639
|
+
def isDigit:boolean ((indexOf "0123456789" ch) >= 0)
|
|
1640
|
+
if (isDigit) {
|
|
1641
|
+
if (inNum == false) {
|
|
1642
|
+
inNum = true
|
|
1643
|
+
hadDot = false
|
|
1644
|
+
have = (have + 1)
|
|
1645
|
+
}
|
|
1646
|
+
} {
|
|
1647
|
+
if (ch == ".") {
|
|
1648
|
+
; A second dot cannot continue the number, so it opens one.
|
|
1649
|
+
if ((inNum == false) || hadDot) {
|
|
1650
|
+
inNum = true
|
|
1651
|
+
have = (have + 1)
|
|
1652
|
+
}
|
|
1653
|
+
hadDot = true
|
|
1654
|
+
} {
|
|
1655
|
+
if (ch == "-") {
|
|
1656
|
+
inNum = true
|
|
1657
|
+
hadDot = false
|
|
1658
|
+
have = (have + 1)
|
|
1659
|
+
} {
|
|
1660
|
+
inNum = false
|
|
1661
|
+
hadDot = false
|
|
1662
|
+
def a:int (EvgBitmapTracerTest.arityOf(ch))
|
|
1663
|
+
if (a >= 0) {
|
|
1664
|
+
if (have != want) {
|
|
1665
|
+
bad = (bad + 1)
|
|
1666
|
+
}
|
|
1667
|
+
want = a
|
|
1668
|
+
have = 0
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
i = (i + 1)
|
|
1674
|
+
}
|
|
1675
|
+
if (have != want) {
|
|
1676
|
+
bad = (bad + 1)
|
|
1677
|
+
}
|
|
1678
|
+
return bad
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
fn testCompactEncodingDrawsTheSamePath:void (t:EvgTest) {
|
|
1682
|
+
; A checkerboard gives axis-aligned runs (H/V), a ring gives long
|
|
1683
|
+
; smooth curves (S), and both give plenty of negative deltas.
|
|
1684
|
+
def shapes:[EvgBinaryBitmap]
|
|
1685
|
+
push shapes (this.makeChecker(64 8))
|
|
1686
|
+
push shapes (this.makeRing(64))
|
|
1687
|
+
|
|
1688
|
+
def si:int 0
|
|
1689
|
+
while (si < (array_length shapes)) {
|
|
1690
|
+
def bm:EvgBinaryBitmap (itemAt shapes si)
|
|
1691
|
+
|
|
1692
|
+
def cOpts:EvgTraceOptions (EvgTraceOptions.defaults())
|
|
1693
|
+
def compact:EvgBitmapTracer (EvgBitmapTracer.fromBinary(bm cOpts))
|
|
1694
|
+
compact.trace()
|
|
1695
|
+
|
|
1696
|
+
def pOpts:EvgTraceOptions (EvgTraceOptions.defaults())
|
|
1697
|
+
pOpts.pathFormat = "plain"
|
|
1698
|
+
def plain:EvgBitmapTracer (EvgBitmapTracer.fromBinary(bm pOpts))
|
|
1699
|
+
plain.trace()
|
|
1700
|
+
|
|
1701
|
+
t.eqInt("the same rings either way" (compact.ringCount()) (plain.ringCount()))
|
|
1702
|
+
t.eqInt("and the same commands" (compact.commandCount()) (plain.commandCount()))
|
|
1703
|
+
def cd:string (compact.getPathData())
|
|
1704
|
+
def pd:string (plain.getPathData())
|
|
1705
|
+
def miscount:int (this.miscountedCommands(cd))
|
|
1706
|
+
t.eqInt("every compact command is given the numbers it takes" miscount 0)
|
|
1707
|
+
def clen:int (strlen cd)
|
|
1708
|
+
def plen:int (strlen pd)
|
|
1709
|
+
t.ok("and it is the shorter of the two" (clen < plen))
|
|
1710
|
+
si = (si + 1)
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
; A filled disc, for curves that actually curve.
|
|
1715
|
+
fn makeRing:EvgBinaryBitmap (n:int) {
|
|
1716
|
+
def bm:EvgBinaryBitmap (EvgBinaryBitmap.create(n n))
|
|
1717
|
+
def c:int (idiv n 2)
|
|
1718
|
+
def r2:int (((idiv n 3) * (idiv n 3)))
|
|
1719
|
+
def y:int 0
|
|
1720
|
+
while (y < n) {
|
|
1721
|
+
def x:int 0
|
|
1722
|
+
while (x < n) {
|
|
1723
|
+
def dx:int (x - c)
|
|
1724
|
+
def dy:int (y - c)
|
|
1725
|
+
if ((((dx * dx) + (dy * dy))) <= r2) {
|
|
1726
|
+
bm.setBit(x y true)
|
|
1727
|
+
}
|
|
1728
|
+
x = (x + 1)
|
|
1729
|
+
}
|
|
1730
|
+
y = (y + 1)
|
|
1731
|
+
}
|
|
1732
|
+
return bm
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
; ----------------------------------------------------------------------
|
|
1736
|
+
; OKLab color distance (EvgTraceColor)
|
|
1737
|
+
; ----------------------------------------------------------------------
|
|
1738
|
+
|
|
1739
|
+
fn testOklabIsOnTheSameScale:void (t:EvgTest) {
|
|
1740
|
+
def lin:[double] (EvgTraceColor.buildLinearTable())
|
|
1741
|
+
t.eqInt("the transfer table has one entry per channel value" (array_length lin) 256)
|
|
1742
|
+
def black:double (itemAt lin 0)
|
|
1743
|
+
def white:double (itemAt lin 255)
|
|
1744
|
+
t.ok("black is 0 in linear light" (black < 0.000001))
|
|
1745
|
+
def whiteOk:boolean (((white > 0.9999) && (white < 1.0001)))
|
|
1746
|
+
t.ok("and white is 1" whiteOk)
|
|
1747
|
+
|
|
1748
|
+
def p (new EvgOklab())
|
|
1749
|
+
def q (new EvgOklab())
|
|
1750
|
+
p.setFrom(lin 0 0 0)
|
|
1751
|
+
q.setFrom(lin 255 255 255)
|
|
1752
|
+
def bw:double (EvgTraceColor.dist2(p q))
|
|
1753
|
+
def scaled:boolean (((bw > 390149.0) && (bw < 390151.0)))
|
|
1754
|
+
t.ok("black against white reads the same as it does in RGB" scaled)
|
|
1755
|
+
|
|
1756
|
+
q.setFrom(lin 0 0 0)
|
|
1757
|
+
t.ok("a color is no distance from itself" ((EvgTraceColor.dist2(p q)) < 0.000001))
|
|
1758
|
+
|
|
1759
|
+
; What the space is for: RGB puts a dark navy and a dark green the same
|
|
1760
|
+
; distance apart as two mid greys that a viewer separates far more
|
|
1761
|
+
; easily. OKLab does not.
|
|
1762
|
+
def a (new EvgOklab())
|
|
1763
|
+
def b (new EvgOklab())
|
|
1764
|
+
a.setFrom(lin 20 20 90)
|
|
1765
|
+
b.setFrom(lin 20 90 20)
|
|
1766
|
+
def okDark:double (EvgTraceColor.dist2(a b))
|
|
1767
|
+
a.setFrom(lin 130 130 130)
|
|
1768
|
+
b.setFrom(lin 200 200 200)
|
|
1769
|
+
def okGrey:double (EvgTraceColor.dist2(a b))
|
|
1770
|
+
def rgbDark:double (EvgBitmapTracer.colorDist2(20 20 90 20 90 20 3))
|
|
1771
|
+
def rgbGrey:double (EvgBitmapTracer.colorDist2(130 130 130 200 200 200 3))
|
|
1772
|
+
t.ok("RGB calls the grey step the larger one" (rgbGrey > rgbDark))
|
|
1773
|
+
t.ok("OKLab calls the hue step the larger one" (okDark > okGrey))
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
; ----------------------------------------------------------------------
|
|
1777
|
+
; Contrast-aware speckle absorb (options.absorbContrast)
|
|
1778
|
+
; ----------------------------------------------------------------------
|
|
1779
|
+
|
|
1780
|
+
; A dark field with one small bright mark in it — a speck that is the
|
|
1781
|
+
; subject, not noise, and one that is not.
|
|
1782
|
+
fn makeSpeckField:ImageBuffer (speckR:int speckG:int speckB:int) {
|
|
1783
|
+
def img (new ImageBuffer())
|
|
1784
|
+
img.init(40 40)
|
|
1785
|
+
def y:int 0
|
|
1786
|
+
while (y < 40) {
|
|
1787
|
+
def x:int 0
|
|
1788
|
+
while (x < 40) {
|
|
1789
|
+
img.setPixelRGB(x y 30 40 120)
|
|
1790
|
+
x = (x + 1)
|
|
1791
|
+
}
|
|
1792
|
+
y = (y + 1)
|
|
1793
|
+
}
|
|
1794
|
+
; Five across, so the mark has interior pixels of its own: only those
|
|
1795
|
+
; vote for a swatch (buildFlatMask keeps the anti-aliased rim out), and
|
|
1796
|
+
; a mark with no interior never earns one to be kept or absorbed.
|
|
1797
|
+
y = 18
|
|
1798
|
+
while (y < 23) {
|
|
1799
|
+
def x2:int 18
|
|
1800
|
+
while (x2 < 23) {
|
|
1801
|
+
img.setPixelRGB(x2 y speckR speckG speckB)
|
|
1802
|
+
x2 = (x2 + 1)
|
|
1803
|
+
}
|
|
1804
|
+
y = (y + 1)
|
|
1805
|
+
}
|
|
1806
|
+
return img
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
fn speckSurvives:boolean (o:EvgTraceOptions speckR:int speckG:int speckB:int) {
|
|
1810
|
+
def img:ImageBuffer (this.makeSpeckField(speckR speckG speckB))
|
|
1811
|
+
def tr:EvgBitmapTracer (EvgBitmapTracer.fromImageBuffer(img o))
|
|
1812
|
+
tr.trace()
|
|
1813
|
+
def i:int 0
|
|
1814
|
+
while (i < (array_length tr.labels)) {
|
|
1815
|
+
if ((itemAt tr.labels i) != (itemAt tr.labels 0)) {
|
|
1816
|
+
return true
|
|
1817
|
+
}
|
|
1818
|
+
i = (i + 1)
|
|
1819
|
+
}
|
|
1820
|
+
return false
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
fn testAbsorbContrastKeepsAStandoutSpeck:void (t:EvgTest) {
|
|
1824
|
+
; Twenty-five pixels against a minRegion of forty, so size alone
|
|
1825
|
+
; condemns the mark either way. What decides is whether it stands out
|
|
1826
|
+
; from the field around it.
|
|
1827
|
+
def keep:EvgTraceOptions (EvgTraceOptions.defaults())
|
|
1828
|
+
keep.colorCount = 4
|
|
1829
|
+
keep.bgMode = "none"
|
|
1830
|
+
keep.minRegion = 40
|
|
1831
|
+
t.ok("a bright mark on a dark field survives minRegion"
|
|
1832
|
+
(this.speckSurvives(keep 240 240 230)))
|
|
1833
|
+
|
|
1834
|
+
def off:EvgTraceOptions (EvgTraceOptions.defaults())
|
|
1835
|
+
off.colorCount = 4
|
|
1836
|
+
off.bgMode = "none"
|
|
1837
|
+
off.minRegion = 40
|
|
1838
|
+
off.absorbContrast = 0
|
|
1839
|
+
t.ok("and is absorbed once the contrast test is switched off"
|
|
1840
|
+
((this.speckSurvives(off 240 240 230)) == false))
|
|
1841
|
+
|
|
1842
|
+
; A mark the quantizer cannot tell from its field is not a mark.
|
|
1843
|
+
t.ok("a mark that matches its field is absorbed either way"
|
|
1844
|
+
((this.speckSurvives(keep 32 42 122)) == false))
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
fn testPresetsAreStartingPoints:void (t:EvgTest) {
|
|
1848
|
+
def line:EvgTraceOptions (EvgTraceOptions.preset("lineart"))
|
|
1849
|
+
t.eqInt("line art is one color" (line.colorCount) 1)
|
|
1850
|
+
t.ok("and lets Otsu pick the threshold" ((line.threshold) < 0))
|
|
1851
|
+
t.eqInt("and never medians a thin stroke away" (line.smooth) 0)
|
|
1852
|
+
|
|
1853
|
+
def photo:EvgTraceOptions (EvgTraceOptions.preset("photo"))
|
|
1854
|
+
t.ok("a photograph is medianed first" ((photo.smooth) > 0))
|
|
1855
|
+
def print:EvgTraceOptions (EvgTraceOptions.preset("print"))
|
|
1856
|
+
t.ok("and absorbs more speckle than a print does"
|
|
1857
|
+
((photo.absorbContrast) > (print.absorbContrast)))
|
|
1858
|
+
|
|
1859
|
+
def unknown:EvgTraceOptions (EvgTraceOptions.preset("no-such-preset"))
|
|
1860
|
+
def plain:EvgTraceOptions (EvgTraceOptions.defaults())
|
|
1861
|
+
t.eqInt("an unknown name is just the defaults" (unknown.colorCount) (plain.colorCount))
|
|
1862
|
+
t.eqStr("compact path data is the default" (plain.pathFormat) "compact")
|
|
1863
|
+
t.eqStr("and RGB is still the default space" (plain.colorSpace) "rgb")
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
|
|
1867
|
+
; ----------------------------------------------------------------------
|
|
1868
|
+
; The palette objective
|
|
1869
|
+
; ----------------------------------------------------------------------
|
|
1870
|
+
|
|
1871
|
+
fn testOklabRoundTripsThroughSrgb:void (t:EvgTest) {
|
|
1872
|
+
def lin:[double] (EvgTraceColor.buildLinearTable())
|
|
1873
|
+
def o (new EvgOklab())
|
|
1874
|
+
def worst:int 0
|
|
1875
|
+
def k:int 0
|
|
1876
|
+
while (k < 400) {
|
|
1877
|
+
def r:int (k - ((idiv k 256) * 256))
|
|
1878
|
+
def g:int ((k * 7) - ((idiv (k * 7) 256) * 256))
|
|
1879
|
+
def b:int ((k * 13) - ((idiv (k * 13) 256) * 256))
|
|
1880
|
+
o.setFrom(lin r g b)
|
|
1881
|
+
def ol:double o.l
|
|
1882
|
+
def oa:double o.a
|
|
1883
|
+
def ob:double o.b
|
|
1884
|
+
def back:[int] (EvgTraceColor.srgbOf(ol oa ob))
|
|
1885
|
+
def dr:int ((itemAt back 0) - r)
|
|
1886
|
+
def dg:int ((itemAt back 1) - g)
|
|
1887
|
+
def db:int ((itemAt back 2) - b)
|
|
1888
|
+
def e:int (EvgBitmapTracer.absI(dr))
|
|
1889
|
+
def e2:int (EvgBitmapTracer.absI(dg))
|
|
1890
|
+
def e3:int (EvgBitmapTracer.absI(db))
|
|
1891
|
+
if (e2 > e) {
|
|
1892
|
+
e = e2
|
|
1893
|
+
}
|
|
1894
|
+
if (e3 > e) {
|
|
1895
|
+
e = e3
|
|
1896
|
+
}
|
|
1897
|
+
if (e > worst) {
|
|
1898
|
+
worst = e
|
|
1899
|
+
}
|
|
1900
|
+
k = (k + 1)
|
|
1901
|
+
}
|
|
1902
|
+
t.eqInt("a color survives the trip into OKLab and back" worst 0)
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
; A large flat field with one small saturated mark in it. Weighted by area
|
|
1906
|
+
; the mark is nothing; the question is whether the quantizer keeps a swatch
|
|
1907
|
+
; for it anyway.
|
|
1908
|
+
fn makeMarkOnField:ImageBuffer () {
|
|
1909
|
+
def img (new ImageBuffer())
|
|
1910
|
+
img.init(80 80)
|
|
1911
|
+
; Four grey bands, so four swatches are already spoken for by area.
|
|
1912
|
+
def y:int 0
|
|
1913
|
+
while (y < 80) {
|
|
1914
|
+
def band:int (idiv y 20)
|
|
1915
|
+
def v:int 60
|
|
1916
|
+
if (band == 1) {
|
|
1917
|
+
v = 110
|
|
1918
|
+
}
|
|
1919
|
+
if (band == 2) {
|
|
1920
|
+
v = 160
|
|
1921
|
+
}
|
|
1922
|
+
if (band == 3) {
|
|
1923
|
+
v = 215
|
|
1924
|
+
}
|
|
1925
|
+
def x:int 0
|
|
1926
|
+
while (x < 80) {
|
|
1927
|
+
img.setPixelRGB(x y v (v - 2) (v - 6))
|
|
1928
|
+
x = (x + 1)
|
|
1929
|
+
}
|
|
1930
|
+
y = (y + 1)
|
|
1931
|
+
}
|
|
1932
|
+
; And one small ochre mark: 121 pixels, under two percent of the frame.
|
|
1933
|
+
y = 34
|
|
1934
|
+
while (y < 45) {
|
|
1935
|
+
def x2:int 34
|
|
1936
|
+
while (x2 < 45) {
|
|
1937
|
+
img.setPixelRGB(x2 y 178 158 101)
|
|
1938
|
+
x2 = (x2 + 1)
|
|
1939
|
+
}
|
|
1940
|
+
y = (y + 1)
|
|
1941
|
+
}
|
|
1942
|
+
return img
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
; The r/g/b of a layer's fill, 0..255.
|
|
1946
|
+
fn fillRgb:[int] (tr:EvgBitmapTracer i:int) {
|
|
1947
|
+
def layers:[EvgTraceLayer] (tr.getLayers())
|
|
1948
|
+
def layer:EvgTraceLayer (itemAt layers i)
|
|
1949
|
+
def hex:string layer.fillHex
|
|
1950
|
+
; EVGColor keeps r/g/b in 0..255 already.
|
|
1951
|
+
def c:EVGColor (EVGColor.parseHex(hex))
|
|
1952
|
+
def out:[int]
|
|
1953
|
+
push out (to_int ((c.r) + 0.5))
|
|
1954
|
+
push out (to_int ((c.g) + 0.5))
|
|
1955
|
+
push out (to_int ((c.b) + 0.5))
|
|
1956
|
+
return out
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
; How far the nearest palette entry sits from a color, largest per-channel.
|
|
1960
|
+
fn nearestFillDistance:int (tr:EvgBitmapTracer r:int g:int b:int) {
|
|
1961
|
+
def best:int 999
|
|
1962
|
+
def i:int 0
|
|
1963
|
+
def layers:[EvgTraceLayer] (tr.getLayers())
|
|
1964
|
+
while (i < (array_length layers)) {
|
|
1965
|
+
def rgb:[int] (this.fillRgb(tr i))
|
|
1966
|
+
def cr:int (itemAt rgb 0)
|
|
1967
|
+
def cg:int (itemAt rgb 1)
|
|
1968
|
+
def cb:int (itemAt rgb 2)
|
|
1969
|
+
def d1:int (EvgBitmapTracer.absI((cr - r)))
|
|
1970
|
+
def d2:int (EvgBitmapTracer.absI((cg - g)))
|
|
1971
|
+
def d3:int (EvgBitmapTracer.absI((cb - b)))
|
|
1972
|
+
def d:int d1
|
|
1973
|
+
if (d2 > d) {
|
|
1974
|
+
d = d2
|
|
1975
|
+
}
|
|
1976
|
+
if (d3 > d) {
|
|
1977
|
+
d = d3
|
|
1978
|
+
}
|
|
1979
|
+
if (d < best) {
|
|
1980
|
+
best = d
|
|
1981
|
+
}
|
|
1982
|
+
i = (i + 1)
|
|
1983
|
+
}
|
|
1984
|
+
return best
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
fn traceMark:EvgBitmapTracer (bias:string chroma:int) {
|
|
1988
|
+
def o:EvgTraceOptions (EvgTraceOptions.defaults())
|
|
1989
|
+
o.colorCount = 4
|
|
1990
|
+
o.bgMode = "none"
|
|
1991
|
+
o.paletteBias = bias
|
|
1992
|
+
o.paletteChroma = chroma
|
|
1993
|
+
def img:ImageBuffer (this.makeMarkOnField())
|
|
1994
|
+
def tr:EvgBitmapTracer (EvgBitmapTracer.fromImageBuffer(img o))
|
|
1995
|
+
tr.trace()
|
|
1996
|
+
return tr
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
fn testDistinctBiasSurvivesTheLloydPasses:void (t:EvgTest) {
|
|
2000
|
+
; Steering only the seeds is the same as not steering: the Lloyd passes
|
|
2001
|
+
; that follow are what decides where a swatch settles, and run on
|
|
2002
|
+
; population they walk every seed back onto the mass it was chosen to
|
|
2003
|
+
; get away from. This is that regression.
|
|
2004
|
+
def area:EvgBitmapTracer (this.traceMark("area" 0))
|
|
2005
|
+
def distinct:EvgBitmapTracer (this.traceMark("distinct" 0))
|
|
2006
|
+
def dArea:int (this.nearestFillDistance(area 178 158 101))
|
|
2007
|
+
def dDist:int (this.nearestFillDistance(distinct 178 158 101))
|
|
2008
|
+
t.ok("area spends every swatch on the four big bands" (dArea > 40))
|
|
2009
|
+
t.ok("distinct keeps one for the mark" (dDist < 32))
|
|
2010
|
+
t.ok("and it is the closer of the two" (dDist < dArea))
|
|
2011
|
+
|
|
2012
|
+
; Colorfulness counts on top of whichever bias is in force.
|
|
2013
|
+
def plain:EvgBitmapTracer (this.traceMark("area" 0))
|
|
2014
|
+
def chroma:EvgBitmapTracer (this.traceMark("area" 300))
|
|
2015
|
+
def dPlain:int (this.nearestFillDistance(plain 178 158 101))
|
|
2016
|
+
def dChroma:int (this.nearestFillDistance(chroma 178 158 101))
|
|
2017
|
+
t.ok("weighting colorfulness moves the palette toward the mark" (dChroma <= dPlain))
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
; OKLab chroma of a palette entry, in thousandths.
|
|
2021
|
+
fn fillChroma:int (tr:EvgBitmapTracer i:int) {
|
|
2022
|
+
def lin:[double] (EvgTraceColor.buildLinearTable())
|
|
2023
|
+
def rgb:[int] (this.fillRgb(tr i))
|
|
2024
|
+
def cr:int (itemAt rgb 0)
|
|
2025
|
+
def cg:int (itemAt rgb 1)
|
|
2026
|
+
def cb:int (itemAt rgb 2)
|
|
2027
|
+
def o (new EvgOklab())
|
|
2028
|
+
o.setFrom(lin cr cg cb)
|
|
2029
|
+
def oa:double o.a
|
|
2030
|
+
def ob:double o.b
|
|
2031
|
+
def mag:double (sqrt ((oa * oa) + (ob * ob)))
|
|
2032
|
+
return (to_int ((mag * 1000.0) + 0.5))
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
fn traceGraded:EvgBitmapTracer (mute:int tint:int warm:int contrast:int) {
|
|
2036
|
+
def o:EvgTraceOptions (EvgTraceOptions.defaults())
|
|
2037
|
+
o.colorCount = 4
|
|
2038
|
+
o.bgMode = "none"
|
|
2039
|
+
o.paletteMute = mute
|
|
2040
|
+
o.paletteTint = tint
|
|
2041
|
+
o.paletteWarm = warm
|
|
2042
|
+
o.paletteContrast = contrast
|
|
2043
|
+
def img:ImageBuffer (this.makeMarkOnField())
|
|
2044
|
+
def tr:EvgBitmapTracer (EvgBitmapTracer.fromImageBuffer(img o))
|
|
2045
|
+
tr.trace()
|
|
2046
|
+
return tr
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
fn lowestChroma:int (tr:EvgBitmapTracer) {
|
|
2050
|
+
def best:int 9999
|
|
2051
|
+
def i:int 0
|
|
2052
|
+
def layers:[EvgTraceLayer] (tr.getLayers())
|
|
2053
|
+
while (i < (array_length layers)) {
|
|
2054
|
+
def c:int (this.fillChroma(tr i))
|
|
2055
|
+
if (c < best) {
|
|
2056
|
+
best = c
|
|
2057
|
+
}
|
|
2058
|
+
i = (i + 1)
|
|
2059
|
+
}
|
|
2060
|
+
return best
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
fn highestChroma:int (tr:EvgBitmapTracer) {
|
|
2064
|
+
def best:int 0
|
|
2065
|
+
def i:int 0
|
|
2066
|
+
def layers:[EvgTraceLayer] (tr.getLayers())
|
|
2067
|
+
while (i < (array_length layers)) {
|
|
2068
|
+
def c:int (this.fillChroma(tr i))
|
|
2069
|
+
if (c > best) {
|
|
2070
|
+
best = c
|
|
2071
|
+
}
|
|
2072
|
+
i = (i + 1)
|
|
2073
|
+
}
|
|
2074
|
+
return best
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
fn testPaletteDialsMoveWhereTheySay:void (t:EvgTest) {
|
|
2078
|
+
def plain:EvgBitmapTracer (this.traceGraded(100 0 0 100))
|
|
2079
|
+
def muted:EvgBitmapTracer (this.traceGraded(50 0 0 100))
|
|
2080
|
+
def rich:EvgBitmapTracer (this.traceGraded(180 0 0 100))
|
|
2081
|
+
def hi1:int (this.highestChroma(plain))
|
|
2082
|
+
def hi2:int (this.highestChroma(muted))
|
|
2083
|
+
def hi3:int (this.highestChroma(rich))
|
|
2084
|
+
t.ok("mute below 100 breaks the palette toward grey" (hi2 < hi1))
|
|
2085
|
+
t.ok("and above 100 enriches it" (hi3 > hi1))
|
|
2086
|
+
|
|
2087
|
+
; The floor is what keeps a broken palette off dead grey.
|
|
2088
|
+
def dead:int (this.lowestChroma(plain))
|
|
2089
|
+
def tinted:EvgBitmapTracer (this.traceGraded(50 40 0 100))
|
|
2090
|
+
def lifted:int (this.lowestChroma(tinted))
|
|
2091
|
+
t.ok("the picture holds a near-neutral" (dead < 40))
|
|
2092
|
+
t.ok("and the tint floor lifts it off grey" (lifted >= 38))
|
|
2093
|
+
|
|
2094
|
+
; Identity at the defaults: an untouched trace is byte for byte what it
|
|
2095
|
+
; was before the dials existed.
|
|
2096
|
+
def a:EvgBitmapTracer (this.traceGraded(100 0 0 100))
|
|
2097
|
+
def o:EvgTraceOptions (EvgTraceOptions.defaults())
|
|
2098
|
+
o.colorCount = 4
|
|
2099
|
+
o.bgMode = "none"
|
|
2100
|
+
def img:ImageBuffer (this.makeMarkOnField())
|
|
2101
|
+
def b:EvgBitmapTracer (EvgBitmapTracer.fromImageBuffer(img o))
|
|
2102
|
+
b.trace()
|
|
2103
|
+
t.eqStr("the dials are identity where they are left alone" (a.getPathData()) (b.getPathData()))
|
|
2104
|
+
def la:[EvgTraceLayer] (a.getLayers())
|
|
2105
|
+
def lb:[EvgTraceLayer] (b.getLayers())
|
|
2106
|
+
def sameFills:boolean true
|
|
2107
|
+
def i:int 0
|
|
2108
|
+
while (i < (array_length la)) {
|
|
2109
|
+
def x:EvgTraceLayer (itemAt la i)
|
|
2110
|
+
def y:EvgTraceLayer (itemAt lb i)
|
|
2111
|
+
if ((x.fillHex) != (y.fillHex)) {
|
|
2112
|
+
sameFills = false
|
|
2113
|
+
}
|
|
2114
|
+
i = (i + 1)
|
|
2115
|
+
}
|
|
2116
|
+
t.ok("including every color in the palette" sameFills)
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
}
|