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,4277 @@
|
|
|
1
|
+
; SPDX-License-Identifier: MIT
|
|
2
|
+
|
|
3
|
+
; ==============================================================================
|
|
4
|
+
; EvgBitmapTracer.rgr — bitmap → EVG path, potrace-style
|
|
5
|
+
; ==============================================================================
|
|
6
|
+
;
|
|
7
|
+
; Pipeline (Potrace paper):
|
|
8
|
+
; 1. decompose bitmap into closed edge paths
|
|
9
|
+
; 2. turdsize despeckle
|
|
10
|
+
; 3. optimal polygon (EvgTraceFit — calcLon / bestPolygon / adjustVertices)
|
|
11
|
+
; 4. alphamax corner/curve fit (+ optional optiCurve)
|
|
12
|
+
; 5. emit PathBuilder / EVGElement / SVG
|
|
13
|
+
;
|
|
14
|
+
; Color:
|
|
15
|
+
; options.fillHex — mono path fill (default #000000)
|
|
16
|
+
; options.colorCount — 1 = mono threshold; >1 = posterize ImageBuffer into
|
|
17
|
+
; N swatches and emit one evenodd <path> per layer
|
|
18
|
+
;
|
|
19
|
+
; Posterize (see the block comment above colorDist2 for why each step exists):
|
|
20
|
+
; detectBackground — flood the page in from the border (options.bgMode)
|
|
21
|
+
; buildFlatMask — region interior vs. anti-aliased transition
|
|
22
|
+
; buildPalette — count-weighted k-means++ over a 5-bit histogram of the
|
|
23
|
+
; interiors only, then merge indistinguishable swatches
|
|
24
|
+
; assignLabels — nearest swatch, with a guarded snap for transitions
|
|
25
|
+
; mergeTinyRegions — absorb runs below options.minRegion into their surround
|
|
26
|
+
; maskForLabel — one binary mask per swatch, traced like the mono path
|
|
27
|
+
;
|
|
28
|
+
; bash lib/evg/tools/run_trace_bench.sh
|
|
29
|
+
; ==============================================================================
|
|
30
|
+
|
|
31
|
+
Import "EvgTraceTypes.rgr"
|
|
32
|
+
Import "EvgTraceColor.rgr"
|
|
33
|
+
Import "EvgTracePath.rgr"
|
|
34
|
+
Import "EvgTraceFit.rgr"
|
|
35
|
+
Import "EvgTraceCurve.rgr"
|
|
36
|
+
Import "PathBuilder.rgr"
|
|
37
|
+
Import "EVGElement.rgr"
|
|
38
|
+
Import "EVGColor.rgr"
|
|
39
|
+
Import "pkg:image/ImageBuffer.rgr"
|
|
40
|
+
|
|
41
|
+
class EvgBitmapTracer {
|
|
42
|
+
def options:EvgTraceOptions
|
|
43
|
+
def bitmap:EvgBinaryBitmap
|
|
44
|
+
def rings:[EvgTraceRing]
|
|
45
|
+
def commands:[PathCommand]
|
|
46
|
+
def pathData:string ""
|
|
47
|
+
def width:int 0
|
|
48
|
+
def height:int 0
|
|
49
|
+
; Multi-color posterize planes (populated by fromImageBuffer when colorCount > 1).
|
|
50
|
+
def hasColorPlanes:boolean false
|
|
51
|
+
def planeR:[int]
|
|
52
|
+
def planeG:[int]
|
|
53
|
+
def planeB:[int]
|
|
54
|
+
def planeA:[int]
|
|
55
|
+
; 1 = region interior, 0 = anti-aliased transition (buildFlatMask).
|
|
56
|
+
def flat:[int]
|
|
57
|
+
; Palette index per pixel, -1 = not painted (assignLabels).
|
|
58
|
+
def labels:[int]
|
|
59
|
+
; 1 = a step that survived the uniqueness and continuity tests, i.e. a
|
|
60
|
+
; boundary rather than noise (buildEdgeMask).
|
|
61
|
+
def edgeMask:[int]
|
|
62
|
+
def detailMask:[int]
|
|
63
|
+
; Whether the two masks are in play at all. Both were being re-derived on
|
|
64
|
+
; every call — an options lookup and an array_length against width*height —
|
|
65
|
+
; and both are asked once per neighbour per pixel per shape, which on a
|
|
66
|
+
; photograph is millions of times for an answer that cannot change during a
|
|
67
|
+
; trace.
|
|
68
|
+
def detailOn:boolean false
|
|
69
|
+
def edgeMaskOn:boolean false
|
|
70
|
+
; OKLab scratch (options.colorSpace). `okLin` is the 256-entry sRGB →
|
|
71
|
+
; linear table, built on first use because a binary trace never asks.
|
|
72
|
+
def okOn:boolean false
|
|
73
|
+
def okLin:[double]
|
|
74
|
+
def okP:EvgOklab
|
|
75
|
+
def okQ:EvgOklab
|
|
76
|
+
def okC:EvgOklab
|
|
77
|
+
; Continuity region id per pixel, -1 = none (buildContourRegions).
|
|
78
|
+
def regionOf:[int]
|
|
79
|
+
def regionCount:int 0
|
|
80
|
+
; 1 = page background flooded in from the border (detectBackground).
|
|
81
|
+
def bgMask:[int]
|
|
82
|
+
def bgActive:boolean false
|
|
83
|
+
def layers:[EvgTraceLayer]
|
|
84
|
+
|
|
85
|
+
Constructor () {
|
|
86
|
+
options = (EvgTraceOptions.defaults())
|
|
87
|
+
bitmap = (EvgBinaryBitmap.create(0 0))
|
|
88
|
+
def r:[EvgTraceRing]
|
|
89
|
+
rings = r
|
|
90
|
+
def c:[PathCommand]
|
|
91
|
+
commands = c
|
|
92
|
+
def pr:[int]
|
|
93
|
+
planeR = pr
|
|
94
|
+
def pg:[int]
|
|
95
|
+
planeG = pg
|
|
96
|
+
def pb:[int]
|
|
97
|
+
planeB = pb
|
|
98
|
+
def pa:[int]
|
|
99
|
+
planeA = pa
|
|
100
|
+
def fl:[int]
|
|
101
|
+
flat = fl
|
|
102
|
+
def lb:[int]
|
|
103
|
+
labels = lb
|
|
104
|
+
def bg:[int]
|
|
105
|
+
bgMask = bg
|
|
106
|
+
def ro:[int]
|
|
107
|
+
regionOf = ro
|
|
108
|
+
def em:[int]
|
|
109
|
+
edgeMask = em
|
|
110
|
+
edgeMaskOn = ((options.edgeMinRun) >= 1)
|
|
111
|
+
def ok:[double]
|
|
112
|
+
okLin = ok
|
|
113
|
+
okP = (new EvgOklab())
|
|
114
|
+
okQ = (new EvgOklab())
|
|
115
|
+
okC = (new EvgOklab())
|
|
116
|
+
def ly:[EvgTraceLayer]
|
|
117
|
+
layers = ly
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
; Flags derived from options. The constructor runs before a caller has
|
|
121
|
+
; assigned `options` (fromBinary / fromImageBuffer set it afterwards), so
|
|
122
|
+
; anything cached off an option has to be refreshed once the real options
|
|
123
|
+
; are in place — trace() does that here.
|
|
124
|
+
fn syncOptions:void () {
|
|
125
|
+
edgeMaskOn = ((options.edgeMinRun) >= 1)
|
|
126
|
+
okOn = ((options.colorSpace) == "oklab")
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
sfn absD:double (v:double) {
|
|
130
|
+
if (v < 0.0) {
|
|
131
|
+
return (0.0 - v)
|
|
132
|
+
}
|
|
133
|
+
return v
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
sfn absI:int (v:int) {
|
|
137
|
+
if (v < 0) {
|
|
138
|
+
return (0 - v)
|
|
139
|
+
}
|
|
140
|
+
return v
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
; Truncating division toward zero for the half-cell neighbour lookups.
|
|
144
|
+
sfn idivTowardZero:int (a:int b:int) {
|
|
145
|
+
if (b == 0) {
|
|
146
|
+
return 0
|
|
147
|
+
}
|
|
148
|
+
if (a < 0) {
|
|
149
|
+
if (b > 0) {
|
|
150
|
+
return (0 - (idiv (0 - a) b))
|
|
151
|
+
}
|
|
152
|
+
return (idiv (0 - a) (0 - b))
|
|
153
|
+
}
|
|
154
|
+
if (b < 0) {
|
|
155
|
+
return (0 - (idiv a (0 - b)))
|
|
156
|
+
}
|
|
157
|
+
return (idiv a b)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
sfn fromBinary:EvgBitmapTracer (bm:EvgBinaryBitmap opts:EvgTraceOptions) {
|
|
161
|
+
def t (new EvgBitmapTracer())
|
|
162
|
+
t.options = opts
|
|
163
|
+
t.bitmap = bm
|
|
164
|
+
t.width = bm.w
|
|
165
|
+
t.height = bm.h
|
|
166
|
+
return t
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
; Threshold an RGBA ImageBuffer into a binary bitmap, then wrap a tracer.
|
|
170
|
+
; When options.colorCount > 1, keep RGB planes and posterize in trace().
|
|
171
|
+
sfn fromImageBuffer:EvgBitmapTracer (img:ImageBuffer opts:EvgTraceOptions) {
|
|
172
|
+
if (opts.colorCount > 1) {
|
|
173
|
+
def tCol (new EvgBitmapTracer())
|
|
174
|
+
tCol.options = opts
|
|
175
|
+
tCol.width = img.width
|
|
176
|
+
tCol.height = img.height
|
|
177
|
+
tCol.hasColorPlanes = true
|
|
178
|
+
tCol.bitmap = (EvgBinaryBitmap.create(img.width img.height))
|
|
179
|
+
def pr:[int]
|
|
180
|
+
def pg:[int]
|
|
181
|
+
def pb:[int]
|
|
182
|
+
def pa:[int]
|
|
183
|
+
def y:int 0
|
|
184
|
+
while (y < img.height) {
|
|
185
|
+
def x:int 0
|
|
186
|
+
while (x < img.width) {
|
|
187
|
+
def c:Color (img.getPixel(x y))
|
|
188
|
+
push pr c.r
|
|
189
|
+
push pg c.g
|
|
190
|
+
push pb c.b
|
|
191
|
+
push pa c.a
|
|
192
|
+
x = (x + 1)
|
|
193
|
+
}
|
|
194
|
+
y = (y + 1)
|
|
195
|
+
}
|
|
196
|
+
tCol.planeR = pr
|
|
197
|
+
tCol.planeG = pg
|
|
198
|
+
tCol.planeB = pb
|
|
199
|
+
tCol.planeA = pa
|
|
200
|
+
return tCol
|
|
201
|
+
}
|
|
202
|
+
def thr:int opts.threshold
|
|
203
|
+
if (thr < 0) {
|
|
204
|
+
thr = (EvgBitmapTracer.otsuThreshold(img))
|
|
205
|
+
}
|
|
206
|
+
def bm:EvgBinaryBitmap (EvgBinaryBitmap.create(img.width img.height))
|
|
207
|
+
def y2:int 0
|
|
208
|
+
while (y2 < img.height) {
|
|
209
|
+
def x2:int 0
|
|
210
|
+
while (x2 < img.width) {
|
|
211
|
+
def c2:Color (img.getPixel(x2 y2))
|
|
212
|
+
def g:int (c2.grayscale())
|
|
213
|
+
def on:boolean false
|
|
214
|
+
if opts.blackOnWhite {
|
|
215
|
+
if (g < thr) {
|
|
216
|
+
on = true
|
|
217
|
+
}
|
|
218
|
+
} {
|
|
219
|
+
if (g >= thr) {
|
|
220
|
+
on = true
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
; Transparent pixels never count as foreground.
|
|
224
|
+
if (c2.a < 16) {
|
|
225
|
+
on = false
|
|
226
|
+
}
|
|
227
|
+
bm.setBit(x2 y2 on)
|
|
228
|
+
x2 = (x2 + 1)
|
|
229
|
+
}
|
|
230
|
+
y2 = (y2 + 1)
|
|
231
|
+
}
|
|
232
|
+
return (EvgBitmapTracer.fromBinary(bm opts))
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
; Otsu threshold on luminance — used when options.threshold < 0.
|
|
236
|
+
sfn otsuThreshold:int (img:ImageBuffer) {
|
|
237
|
+
def hist:[int]
|
|
238
|
+
def i:int 0
|
|
239
|
+
while (i < 256) {
|
|
240
|
+
push hist 0
|
|
241
|
+
i = (i + 1)
|
|
242
|
+
}
|
|
243
|
+
def total:int (img.width * img.height)
|
|
244
|
+
if (total <= 0) {
|
|
245
|
+
return 128
|
|
246
|
+
}
|
|
247
|
+
def y:int 0
|
|
248
|
+
while (y < img.height) {
|
|
249
|
+
def x:int 0
|
|
250
|
+
while (x < img.width) {
|
|
251
|
+
def c:Color (img.getPixel(x y))
|
|
252
|
+
def g:int (c.grayscale())
|
|
253
|
+
def prev:int (itemAt hist g)
|
|
254
|
+
set hist g (prev + 1)
|
|
255
|
+
x = (x + 1)
|
|
256
|
+
}
|
|
257
|
+
y = (y + 1)
|
|
258
|
+
}
|
|
259
|
+
def sum:double 0.0
|
|
260
|
+
i = 0
|
|
261
|
+
while (i < 256) {
|
|
262
|
+
def cnt:int (itemAt hist i)
|
|
263
|
+
sum = (sum + ((to_double i) * (to_double cnt)))
|
|
264
|
+
i = (i + 1)
|
|
265
|
+
}
|
|
266
|
+
def sumB:double 0.0
|
|
267
|
+
def wB:int 0
|
|
268
|
+
def best:double (0.0 - 1.0)
|
|
269
|
+
def thr:int 128
|
|
270
|
+
i = 0
|
|
271
|
+
while (i < 256) {
|
|
272
|
+
def c2:int (itemAt hist i)
|
|
273
|
+
wB = (wB + c2)
|
|
274
|
+
if (wB > 0) {
|
|
275
|
+
def wF:int (total - wB)
|
|
276
|
+
if (wF > 0) {
|
|
277
|
+
sumB = (sumB + ((to_double i) * (to_double c2)))
|
|
278
|
+
def mB:double (sumB / (to_double wB))
|
|
279
|
+
def mF:double ((sum - sumB) / (to_double wF))
|
|
280
|
+
def diff:double (mB - mF)
|
|
281
|
+
def between:double ((diff * diff) * (to_double wB) * (to_double wF))
|
|
282
|
+
if (between > best) {
|
|
283
|
+
best = between
|
|
284
|
+
thr = i
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
i = (i + 1)
|
|
289
|
+
}
|
|
290
|
+
return thr
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
; ----------------------------------------------------------------------
|
|
294
|
+
; Public entry
|
|
295
|
+
; ----------------------------------------------------------------------
|
|
296
|
+
|
|
297
|
+
fn trace:void () {
|
|
298
|
+
this.syncOptions()
|
|
299
|
+
def emptyR:[EvgTraceRing]
|
|
300
|
+
rings = emptyR
|
|
301
|
+
def emptyC:[PathCommand]
|
|
302
|
+
commands = emptyC
|
|
303
|
+
def emptyL:[EvgTraceLayer]
|
|
304
|
+
layers = emptyL
|
|
305
|
+
pathData = ""
|
|
306
|
+
if hasColorPlanes {
|
|
307
|
+
this.traceColorLayers()
|
|
308
|
+
} {
|
|
309
|
+
this.decompose()
|
|
310
|
+
this.emitCommands()
|
|
311
|
+
pathData = (this.pathDataOf(commands))
|
|
312
|
+
; Mono also exposes a single layer so callers can always use layers.
|
|
313
|
+
def layer (new EvgTraceLayer())
|
|
314
|
+
layer.fillHex = options.fillHex
|
|
315
|
+
layer.pathData = pathData
|
|
316
|
+
layer.ringCount = (array_length rings)
|
|
317
|
+
layer.commandCount = (array_length commands)
|
|
318
|
+
push layers layer
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
fn getCommands:[PathCommand] () {
|
|
323
|
+
return commands
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
fn getPathData:string () {
|
|
327
|
+
return pathData
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
fn ringCount:int () {
|
|
331
|
+
return (array_length rings)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
fn commandCount:int () {
|
|
335
|
+
return (array_length commands)
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
; True when a page color was actually flooded away — the page can say so,
|
|
339
|
+
; because "auto" declining silently is indistinguishable from it failing.
|
|
340
|
+
fn backgroundRemoved:boolean () {
|
|
341
|
+
return bgActive
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
fn layerCount:int () {
|
|
345
|
+
return (array_length layers)
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
fn getLayers:[EvgTraceLayer] () {
|
|
349
|
+
return layers
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
fn toPathBuilder:PathBuilder () {
|
|
353
|
+
def b (new PathBuilder())
|
|
354
|
+
b.addCommands(commands)
|
|
355
|
+
return b
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
fn toEVGElement:EVGElement () {
|
|
359
|
+
def el:EVGElement (EVGElement.createPath())
|
|
360
|
+
el.svgPath = pathData
|
|
361
|
+
el.fillRule = "evenodd"
|
|
362
|
+
el.viewBox = ("0 0 " + (to_string width) + " " + (to_string height))
|
|
363
|
+
el.fillColor = (EVGColor.parse(options.fillHex))
|
|
364
|
+
if ((array_length layers) > 0) {
|
|
365
|
+
def layer0:EvgTraceLayer (itemAt layers 0)
|
|
366
|
+
el.svgPath = layer0.pathData
|
|
367
|
+
el.fillColor = (EVGColor.parse(layer0.fillHex))
|
|
368
|
+
}
|
|
369
|
+
el.width = (EVGUnit.px((to_double width)))
|
|
370
|
+
el.height = (EVGUnit.px((to_double height)))
|
|
371
|
+
return el
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
; One EVG path element per color layer (posterize / mono).
|
|
375
|
+
fn toEVGElements:[EVGElement] () {
|
|
376
|
+
def out:[EVGElement]
|
|
377
|
+
def i:int 0
|
|
378
|
+
while (i < (array_length layers)) {
|
|
379
|
+
def layer:EvgTraceLayer (itemAt layers i)
|
|
380
|
+
def el:EVGElement (EVGElement.createPath())
|
|
381
|
+
el.svgPath = layer.pathData
|
|
382
|
+
el.fillRule = "evenodd"
|
|
383
|
+
el.viewBox = ("0 0 " + (to_string width) + " " + (to_string height))
|
|
384
|
+
el.fillColor = (EVGColor.parse(layer.fillHex))
|
|
385
|
+
el.width = (EVGUnit.px((to_double width)))
|
|
386
|
+
el.height = (EVGUnit.px((to_double height)))
|
|
387
|
+
push out el
|
|
388
|
+
i = (i + 1)
|
|
389
|
+
}
|
|
390
|
+
return out
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
sfn num:string (v:double) {
|
|
394
|
+
; Two decimals is plenty for a gradient axis and keeps the file small.
|
|
395
|
+
def scaled:int (to_int ((v * 100.0) + 0.5))
|
|
396
|
+
def whole:int (idiv scaled 100)
|
|
397
|
+
def frac:int (scaled - (whole * 100))
|
|
398
|
+
if (frac < 0) {
|
|
399
|
+
frac = (0 - frac)
|
|
400
|
+
}
|
|
401
|
+
def fs:string (to_string frac)
|
|
402
|
+
if (frac < 10) {
|
|
403
|
+
fs = ("0" + fs)
|
|
404
|
+
}
|
|
405
|
+
return ((to_string whole) + "." + fs)
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
fn toSVG:string () {
|
|
409
|
+
def svg:string "<svg xmlns=\"http://www.w3.org/2000/svg\" "
|
|
410
|
+
svg = (svg + "width=\"" + (to_string width) + "\" ")
|
|
411
|
+
svg = (svg + "height=\"" + (to_string height) + "\" ")
|
|
412
|
+
svg = (svg + "viewBox=\"0 0 " + (to_string width) + " " + (to_string height) + "\">")
|
|
413
|
+
if ((array_length layers) > 0) {
|
|
414
|
+
; Gradients have to be declared before they are referenced.
|
|
415
|
+
def defs:string ""
|
|
416
|
+
def i:int 0
|
|
417
|
+
while (i < (array_length layers)) {
|
|
418
|
+
def layer:EvgTraceLayer (itemAt layers i)
|
|
419
|
+
def id:string ("g" + (to_string i))
|
|
420
|
+
if ((layer.fillKind) == "linear") {
|
|
421
|
+
defs = (defs + "<linearGradient id=\"" + id + "\" gradientUnits=\"userSpaceOnUse\"")
|
|
422
|
+
defs = (defs + " x1=\"" + (EvgBitmapTracer.num(layer.gx0)) + "\" y1=\"" + (EvgBitmapTracer.num(layer.gy0)) + "\"")
|
|
423
|
+
defs = (defs + " x2=\"" + (EvgBitmapTracer.num(layer.gx1)) + "\" y2=\"" + (EvgBitmapTracer.num(layer.gy1)) + "\">")
|
|
424
|
+
defs = (defs + "<stop offset=\"0\" stop-color=\"" + layer.stopA + "\"/>")
|
|
425
|
+
defs = (defs + "<stop offset=\"1\" stop-color=\"" + layer.stopB + "\"/></linearGradient>")
|
|
426
|
+
}
|
|
427
|
+
i = (i + 1)
|
|
428
|
+
}
|
|
429
|
+
if ((strlen defs) > 0) {
|
|
430
|
+
svg = (svg + "<defs>" + defs + "</defs>")
|
|
431
|
+
}
|
|
432
|
+
i = 0
|
|
433
|
+
while (i < (array_length layers)) {
|
|
434
|
+
def layer2:EvgTraceLayer (itemAt layers i)
|
|
435
|
+
def fill:string layer2.fillHex
|
|
436
|
+
if ((layer2.fillKind) != "flat") {
|
|
437
|
+
fill = ("url(#g" + (to_string i) + ")")
|
|
438
|
+
}
|
|
439
|
+
svg = (svg + "<path fill=\"" + fill + "\" fill-rule=\"evenodd\" d=\"" + layer2.pathData + "\"/>")
|
|
440
|
+
i = (i + 1)
|
|
441
|
+
}
|
|
442
|
+
} {
|
|
443
|
+
svg = (svg + "<path fill=\"" + options.fillHex + "\" fill-rule=\"evenodd\" d=\"" + pathData + "\"/>")
|
|
444
|
+
}
|
|
445
|
+
svg = (svg + "</svg>")
|
|
446
|
+
return svg
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
; ----------------------------------------------------------------------
|
|
450
|
+
; Multi-color posterize (quantize → per-swatch binary trace)
|
|
451
|
+
;
|
|
452
|
+
; The palette is built from *plateau* pixels only. Anti-aliased boundary
|
|
453
|
+
; pixels are blends of two real colors: if they are allowed to vote, the
|
|
454
|
+
; extra swatches are spent on halo tones instead of new regions, and every
|
|
455
|
+
; ink stroke is then eaten from both sides until turdsize deletes what is
|
|
456
|
+
; left. Transitional pixels are instead snapped to a neighbouring region.
|
|
457
|
+
; ----------------------------------------------------------------------
|
|
458
|
+
|
|
459
|
+
; Weighted Euclidean distance in RGB with an extra luma term, so a dark
|
|
460
|
+
; ink line never collapses into a mid-tone of the same hue.
|
|
461
|
+
sfn colorDist2:double (r0:int g0:int b0:int r1:int g1:int b1:int lw:int) {
|
|
462
|
+
def dr:double (to_double (r0 - r1))
|
|
463
|
+
def dg:double (to_double (g0 - g1))
|
|
464
|
+
def db:double (to_double (b0 - b1))
|
|
465
|
+
def l0:int (EvgBitmapTracer.lumaOf(r0 g0 b0))
|
|
466
|
+
def l1:int (EvgBitmapTracer.lumaOf(r1 g1 b1))
|
|
467
|
+
def dl:double (to_double (l0 - l1))
|
|
468
|
+
return ((((dr * dr) + (dg * dg)) + (db * db)) + ((dl * dl) * (to_double lw)))
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
sfn lumaOf:int (r:int g:int b:int) {
|
|
472
|
+
; Integer approx of 0.299R+0.587G+0.114B
|
|
473
|
+
return (idiv (((r * 299) + (g * 587)) + (b * 114)) 1000)
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
fn lumaWeight:int () {
|
|
477
|
+
def lw:int options.lumaWeight
|
|
478
|
+
if (lw < 1) {
|
|
479
|
+
return 1
|
|
480
|
+
}
|
|
481
|
+
return lw
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
; Every `d` attribute the tracer emits goes through here, so the encoding
|
|
485
|
+
; is one option rather than a choice repeated at each emit site.
|
|
486
|
+
fn pathDataOf:string (cmds:[PathCommand]) {
|
|
487
|
+
if ((options.pathFormat) == "plain") {
|
|
488
|
+
return (VectorShapes.asPathData(cmds))
|
|
489
|
+
}
|
|
490
|
+
def p:int options.pathPrecision
|
|
491
|
+
if (p < 0) {
|
|
492
|
+
p = 0
|
|
493
|
+
}
|
|
494
|
+
return (EvgTracePath.encode(cmds p true))
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
; The one place the posterizer asks how far apart two colors are. Which
|
|
498
|
+
; space answers is options.colorSpace; both answers are on the same scale,
|
|
499
|
+
; so a caller comparing against a threshold does not care which ran.
|
|
500
|
+
fn dist2:double (r0:int g0:int b0:int r1:int g1:int b1:int) {
|
|
501
|
+
if (okOn == false) {
|
|
502
|
+
def lw:int (this.lumaWeight())
|
|
503
|
+
return (EvgBitmapTracer.colorDist2(r0 g0 b0 r1 g1 b1 lw))
|
|
504
|
+
}
|
|
505
|
+
if ((array_length okLin) == 0) {
|
|
506
|
+
okLin = (EvgTraceColor.buildLinearTable())
|
|
507
|
+
}
|
|
508
|
+
okP.setFrom(okLin r0 g0 b0)
|
|
509
|
+
okQ.setFrom(okLin r1 g1 b1)
|
|
510
|
+
def p:EvgOklab (unwrap okP)
|
|
511
|
+
def q:EvgOklab (unwrap okQ)
|
|
512
|
+
return (EvgTraceColor.dist2(p q))
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
; What `dist2` returns for a gray step of `perChannel` in every channel —
|
|
516
|
+
; the bridge between a threshold written in channel units (minColorDelta)
|
|
517
|
+
; and the metric actually in use. Exact for "rgb"; for "oklab" the scale
|
|
518
|
+
; was chosen to make black-to-white agree, and a gray step agrees with it
|
|
519
|
+
; to within the transfer curve's own bend.
|
|
520
|
+
fn dist2Limit:double (perChannel:int) {
|
|
521
|
+
def d:double (to_double (perChannel * perChannel))
|
|
522
|
+
if okOn {
|
|
523
|
+
return (d * 6.0)
|
|
524
|
+
}
|
|
525
|
+
def lw:int (this.lumaWeight())
|
|
526
|
+
def w:double (to_double (3 + lw))
|
|
527
|
+
return (d * w)
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
; True if this opaque pixel may join the palette / masks.
|
|
531
|
+
fn pixelAllowed:boolean (i:int) {
|
|
532
|
+
def a:int (itemAt planeA i)
|
|
533
|
+
if (a < 16) {
|
|
534
|
+
return false
|
|
535
|
+
}
|
|
536
|
+
if bgActive {
|
|
537
|
+
return ((itemAt bgMask i) == 0)
|
|
538
|
+
}
|
|
539
|
+
; Nothing else drops a pixel. The bright-pixel rule that used to sit here
|
|
540
|
+
; had no idea where it was: it deleted the white of an eye and the white
|
|
541
|
+
; of a boot along with the white of the page, because all it asked was
|
|
542
|
+
; how bright the pixel is. Transparency belongs to what is outside the
|
|
543
|
+
; subject, so it is decided by the border floods in detectBackground and
|
|
544
|
+
; nowhere else.
|
|
545
|
+
return true
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
; ----------------------------------------------------------------------
|
|
549
|
+
; Background detection
|
|
550
|
+
;
|
|
551
|
+
; skipLuma is a blunt instrument: it deletes every bright pixel, so a white
|
|
552
|
+
; belly or a highlight inside the subject is thrown away along with the
|
|
553
|
+
; page. Instead, flood the border color inwards and drop only what is
|
|
554
|
+
; actually connected to the edge of the image. Bail out (and leave the
|
|
555
|
+
; skipLuma rule in charge) unless the border really is one flat color —
|
|
556
|
+
; a photograph has no page to remove.
|
|
557
|
+
; ----------------------------------------------------------------------
|
|
558
|
+
|
|
559
|
+
fn detectBackground:void () {
|
|
560
|
+
bgActive = false
|
|
561
|
+
def mode:string options.bgMode
|
|
562
|
+
def named:boolean (mode == "color")
|
|
563
|
+
if ((mode != "auto") && (named == false)) {
|
|
564
|
+
return
|
|
565
|
+
}
|
|
566
|
+
def n:int (width * height)
|
|
567
|
+
if (n < 4) {
|
|
568
|
+
return
|
|
569
|
+
}
|
|
570
|
+
; Modal border color, via the same 5-bit histogram as the palette.
|
|
571
|
+
def bins:int 32768
|
|
572
|
+
def cnt:[int]
|
|
573
|
+
def sr:[int]
|
|
574
|
+
def sg:[int]
|
|
575
|
+
def sb:[int]
|
|
576
|
+
def i:int 0
|
|
577
|
+
while (i < bins) {
|
|
578
|
+
push cnt 0
|
|
579
|
+
push sr 0
|
|
580
|
+
push sg 0
|
|
581
|
+
push sb 0
|
|
582
|
+
i = (i + 1)
|
|
583
|
+
}
|
|
584
|
+
def border:[int]
|
|
585
|
+
def x:int 0
|
|
586
|
+
while (x < width) {
|
|
587
|
+
push border x
|
|
588
|
+
push border (((height - 1) * width) + x)
|
|
589
|
+
x = (x + 1)
|
|
590
|
+
}
|
|
591
|
+
def y:int 1
|
|
592
|
+
while (y < (height - 1)) {
|
|
593
|
+
push border (y * width)
|
|
594
|
+
push border ((y * width) + (width - 1))
|
|
595
|
+
y = (y + 1)
|
|
596
|
+
}
|
|
597
|
+
def bn:int (array_length border)
|
|
598
|
+
if (bn == 0) {
|
|
599
|
+
return
|
|
600
|
+
}
|
|
601
|
+
def opaqueBorder:int 0
|
|
602
|
+
i = 0
|
|
603
|
+
while (i < bn) {
|
|
604
|
+
def idx:int (itemAt border i)
|
|
605
|
+
if ((itemAt planeA idx) >= 16) {
|
|
606
|
+
def r:int (itemAt planeR idx)
|
|
607
|
+
def g:int (itemAt planeG idx)
|
|
608
|
+
def b:int (itemAt planeB idx)
|
|
609
|
+
def key:int (((((idiv r 8) * 32) + (idiv g 8)) * 32) + (idiv b 8))
|
|
610
|
+
set cnt key ((itemAt cnt key) + 1)
|
|
611
|
+
set sr key ((itemAt sr key) + r)
|
|
612
|
+
set sg key ((itemAt sg key) + g)
|
|
613
|
+
set sb key ((itemAt sb key) + b)
|
|
614
|
+
opaqueBorder = (opaqueBorder + 1)
|
|
615
|
+
}
|
|
616
|
+
i = (i + 1)
|
|
617
|
+
}
|
|
618
|
+
; A fully transparent border is a cut-out: alpha already removed it.
|
|
619
|
+
if (opaqueBorder == 0) {
|
|
620
|
+
return
|
|
621
|
+
}
|
|
622
|
+
def topI:int (0 - 1)
|
|
623
|
+
def topC:int 0
|
|
624
|
+
i = 0
|
|
625
|
+
while (i < bins) {
|
|
626
|
+
if ((itemAt cnt i) > topC) {
|
|
627
|
+
topC = (itemAt cnt i)
|
|
628
|
+
topI = i
|
|
629
|
+
}
|
|
630
|
+
i = (i + 1)
|
|
631
|
+
}
|
|
632
|
+
if (topI < 0) {
|
|
633
|
+
return
|
|
634
|
+
}
|
|
635
|
+
def bgR:int (idiv (itemAt sr topI) topC)
|
|
636
|
+
def bgG:int (idiv (itemAt sg topI) topC)
|
|
637
|
+
def bgB:int (idiv (itemAt sb topI) topC)
|
|
638
|
+
def tol:int options.bgTolerance
|
|
639
|
+
if (tol < 0) {
|
|
640
|
+
tol = 0
|
|
641
|
+
}
|
|
642
|
+
if named {
|
|
643
|
+
; The caller named the color, so the border does not get a vote on
|
|
644
|
+
; what it is — or on whether the flood happens at all.
|
|
645
|
+
def c:EVGColor (EVGColor.parse(options.bgColor))
|
|
646
|
+
if (c.isSet == false) {
|
|
647
|
+
return
|
|
648
|
+
}
|
|
649
|
+
bgR = (to_int c.r)
|
|
650
|
+
bgG = (to_int c.g)
|
|
651
|
+
bgB = (to_int c.b)
|
|
652
|
+
} {
|
|
653
|
+
; How much of the border actually is that color?
|
|
654
|
+
def within:int 0
|
|
655
|
+
i = 0
|
|
656
|
+
while (i < bn) {
|
|
657
|
+
def idx2:int (itemAt border i)
|
|
658
|
+
if ((itemAt planeA idx2) >= 16) {
|
|
659
|
+
if (this.nearBg(idx2 bgR bgG bgB tol)) {
|
|
660
|
+
within = (within + 1)
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
i = (i + 1)
|
|
664
|
+
}
|
|
665
|
+
if ((idiv (within * 100) opaqueBorder) < 80) {
|
|
666
|
+
; The border is not one flat color — a gradient page, or a
|
|
667
|
+
; photograph with no page at all. Rather than fall back to
|
|
668
|
+
; deleting every bright pixel wherever it sits, try the same
|
|
669
|
+
; question the other way round: is there a *bright* area
|
|
670
|
+
; connected to the edge? That removes a white or near-white page
|
|
671
|
+
; and cannot touch a white belly, because a belly does not reach
|
|
672
|
+
; the border.
|
|
673
|
+
this.floodBrightFromBorder(border bn)
|
|
674
|
+
return
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
; Flood the page in from every matching border pixel.
|
|
678
|
+
def mask:[int]
|
|
679
|
+
i = 0
|
|
680
|
+
while (i < n) {
|
|
681
|
+
push mask 0
|
|
682
|
+
i = (i + 1)
|
|
683
|
+
}
|
|
684
|
+
def stack:[int]
|
|
685
|
+
i = 0
|
|
686
|
+
while (i < bn) {
|
|
687
|
+
def s0:int (itemAt border i)
|
|
688
|
+
if ((itemAt mask s0) == 0) {
|
|
689
|
+
if (this.nearBg(s0 bgR bgG bgB tol)) {
|
|
690
|
+
set mask s0 1
|
|
691
|
+
push stack s0
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
i = (i + 1)
|
|
695
|
+
}
|
|
696
|
+
def head:int 0
|
|
697
|
+
while (head < (array_length stack)) {
|
|
698
|
+
def cur:int (itemAt stack head)
|
|
699
|
+
head = (head + 1)
|
|
700
|
+
def cy:int (idiv cur width)
|
|
701
|
+
def cx:int (cur - (cy * width))
|
|
702
|
+
if (cx > 0) {
|
|
703
|
+
this.floodStep((cur - 1) mask stack bgR bgG bgB tol)
|
|
704
|
+
}
|
|
705
|
+
if (cx < (width - 1)) {
|
|
706
|
+
this.floodStep((cur + 1) mask stack bgR bgG bgB tol)
|
|
707
|
+
}
|
|
708
|
+
if (cy > 0) {
|
|
709
|
+
this.floodStep((cur - width) mask stack bgR bgG bgB tol)
|
|
710
|
+
}
|
|
711
|
+
if (cy < (height - 1)) {
|
|
712
|
+
this.floodStep((cur + width) mask stack bgR bgG bgB tol)
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
bgMask = mask
|
|
716
|
+
bgActive = true
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
; A page that is bright but not one flat color: flood inwards from every
|
|
720
|
+
; border pixel at least as bright as skipLuma. Interiors are unreachable by
|
|
721
|
+
; construction, which is the whole point — brightness alone was never a
|
|
722
|
+
; statement about being background.
|
|
723
|
+
fn floodBrightFromBorder:void (border:[int] bn:int) {
|
|
724
|
+
def lim:int options.skipLuma
|
|
725
|
+
if (lim > 255) {
|
|
726
|
+
return
|
|
727
|
+
}
|
|
728
|
+
def n:int (width * height)
|
|
729
|
+
def mask:[int]
|
|
730
|
+
def i:int 0
|
|
731
|
+
while (i < n) {
|
|
732
|
+
push mask 0
|
|
733
|
+
i = (i + 1)
|
|
734
|
+
}
|
|
735
|
+
def stack:[int]
|
|
736
|
+
i = 0
|
|
737
|
+
while (i < bn) {
|
|
738
|
+
def s0:int (itemAt border i)
|
|
739
|
+
if ((itemAt mask s0) == 0) {
|
|
740
|
+
if (this.brightEnough(s0 lim)) {
|
|
741
|
+
set mask s0 1
|
|
742
|
+
push stack s0
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
i = (i + 1)
|
|
746
|
+
}
|
|
747
|
+
if ((array_length stack) == 0) {
|
|
748
|
+
return
|
|
749
|
+
}
|
|
750
|
+
def head:int 0
|
|
751
|
+
while (head < (array_length stack)) {
|
|
752
|
+
def cur:int (itemAt stack head)
|
|
753
|
+
head = (head + 1)
|
|
754
|
+
def cy:int (idiv cur width)
|
|
755
|
+
def cx:int (cur - (cy * width))
|
|
756
|
+
if (cx > 0) {
|
|
757
|
+
this.brightStep((cur - 1) mask stack lim)
|
|
758
|
+
}
|
|
759
|
+
if (cx < (width - 1)) {
|
|
760
|
+
this.brightStep((cur + 1) mask stack lim)
|
|
761
|
+
}
|
|
762
|
+
if (cy > 0) {
|
|
763
|
+
this.brightStep((cur - width) mask stack lim)
|
|
764
|
+
}
|
|
765
|
+
if (cy < (height - 1)) {
|
|
766
|
+
this.brightStep((cur + width) mask stack lim)
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
bgMask = mask
|
|
770
|
+
bgActive = true
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
fn brightEnough:boolean (i:int lim:int) {
|
|
774
|
+
if ((itemAt planeA i) < 16) {
|
|
775
|
+
return true
|
|
776
|
+
}
|
|
777
|
+
return ((EvgBitmapTracer.lumaOf((itemAt planeR i) (itemAt planeG i) (itemAt planeB i))) >= lim)
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
fn brightStep:void (i:int mask:[int] stack:[int] lim:int) {
|
|
781
|
+
if ((itemAt mask i) == 1) {
|
|
782
|
+
return
|
|
783
|
+
}
|
|
784
|
+
if ((this.brightEnough(i lim)) == false) {
|
|
785
|
+
return
|
|
786
|
+
}
|
|
787
|
+
set mask i 1
|
|
788
|
+
push stack i
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
fn nearBg:boolean (i:int bgR:int bgG:int bgB:int tol:int) {
|
|
792
|
+
if ((itemAt planeA i) < 16) {
|
|
793
|
+
return true
|
|
794
|
+
}
|
|
795
|
+
if ((EvgBitmapTracer.absI((itemAt planeR i) - bgR)) > tol) {
|
|
796
|
+
return false
|
|
797
|
+
}
|
|
798
|
+
if ((EvgBitmapTracer.absI((itemAt planeG i) - bgG)) > tol) {
|
|
799
|
+
return false
|
|
800
|
+
}
|
|
801
|
+
if ((EvgBitmapTracer.absI((itemAt planeB i) - bgB)) > tol) {
|
|
802
|
+
return false
|
|
803
|
+
}
|
|
804
|
+
return true
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
fn floodStep:void (i:int mask:[int] stack:[int] bgR:int bgG:int bgB:int tol:int) {
|
|
808
|
+
if ((itemAt mask i) != 0) {
|
|
809
|
+
return
|
|
810
|
+
}
|
|
811
|
+
if (this.nearBg(i bgR bgG bgB tol)) {
|
|
812
|
+
set mask i 1
|
|
813
|
+
push stack i
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
; ----------------------------------------------------------------------
|
|
818
|
+
; Plateau / edge classification
|
|
819
|
+
; ----------------------------------------------------------------------
|
|
820
|
+
|
|
821
|
+
; Largest per-channel difference between two pixels (alpha included, so the
|
|
822
|
+
; feathered rim of a cut-out counts as a transition too).
|
|
823
|
+
fn pixelDelta:int (i:int j:int) {
|
|
824
|
+
def d:int (EvgBitmapTracer.absI((itemAt planeR i) - (itemAt planeR j)))
|
|
825
|
+
def dg:int (EvgBitmapTracer.absI((itemAt planeG i) - (itemAt planeG j)))
|
|
826
|
+
if (dg > d) {
|
|
827
|
+
d = dg
|
|
828
|
+
}
|
|
829
|
+
def db:int (EvgBitmapTracer.absI((itemAt planeB i) - (itemAt planeB j)))
|
|
830
|
+
if (db > d) {
|
|
831
|
+
d = db
|
|
832
|
+
}
|
|
833
|
+
def da:int (EvgBitmapTracer.absI((itemAt planeA i) - (itemAt planeA j)))
|
|
834
|
+
if (da > d) {
|
|
835
|
+
d = da
|
|
836
|
+
}
|
|
837
|
+
return d
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
; flat[i] = 1 when every 4-neighbour is within flatTolerance, i.e. the pixel
|
|
841
|
+
; sits inside a region rather than on an anti-aliased boundary.
|
|
842
|
+
fn buildFlatMask:void () {
|
|
843
|
+
def n:int (width * height)
|
|
844
|
+
def f:[int]
|
|
845
|
+
def i:int 0
|
|
846
|
+
while (i < n) {
|
|
847
|
+
push f 1
|
|
848
|
+
i = (i + 1)
|
|
849
|
+
}
|
|
850
|
+
def tol:int options.flatTolerance
|
|
851
|
+
if (tol < 0) {
|
|
852
|
+
tol = 0
|
|
853
|
+
}
|
|
854
|
+
def y:int 0
|
|
855
|
+
while (y < height) {
|
|
856
|
+
def x:int 0
|
|
857
|
+
while (x < width) {
|
|
858
|
+
def idx:int ((y * width) + x)
|
|
859
|
+
def isFlat:boolean true
|
|
860
|
+
if (x > 0) {
|
|
861
|
+
if ((this.pixelDelta(idx (idx - 1))) > tol) {
|
|
862
|
+
isFlat = false
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
if (x < (width - 1)) {
|
|
866
|
+
if ((this.pixelDelta(idx (idx + 1))) > tol) {
|
|
867
|
+
isFlat = false
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
if (y > 0) {
|
|
871
|
+
if ((this.pixelDelta(idx (idx - width))) > tol) {
|
|
872
|
+
isFlat = false
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
if (y < (height - 1)) {
|
|
876
|
+
if ((this.pixelDelta(idx (idx + width))) > tol) {
|
|
877
|
+
isFlat = false
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
if isFlat {
|
|
881
|
+
set f idx 1
|
|
882
|
+
} {
|
|
883
|
+
set f idx 0
|
|
884
|
+
}
|
|
885
|
+
x = (x + 1)
|
|
886
|
+
}
|
|
887
|
+
y = (y + 1)
|
|
888
|
+
}
|
|
889
|
+
flat = f
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
; ----------------------------------------------------------------------
|
|
893
|
+
; Color histogram (5 bits per channel) over the pixels that may vote
|
|
894
|
+
; ----------------------------------------------------------------------
|
|
895
|
+
|
|
896
|
+
fn buildHistogram:void (flatOnly:boolean binR:[int] binG:[int] binB:[int] binW:[int]) {
|
|
897
|
+
def bins:int 32768
|
|
898
|
+
def cnt:[int]
|
|
899
|
+
def sumR:[int]
|
|
900
|
+
def sumG:[int]
|
|
901
|
+
def sumB:[int]
|
|
902
|
+
def i:int 0
|
|
903
|
+
while (i < bins) {
|
|
904
|
+
push cnt 0
|
|
905
|
+
push sumR 0
|
|
906
|
+
push sumG 0
|
|
907
|
+
push sumB 0
|
|
908
|
+
i = (i + 1)
|
|
909
|
+
}
|
|
910
|
+
def n:int (array_length planeR)
|
|
911
|
+
def hasFlat:boolean ((array_length flat) == n)
|
|
912
|
+
i = 0
|
|
913
|
+
while (i < n) {
|
|
914
|
+
def take:boolean (this.pixelAllowed(i))
|
|
915
|
+
if (take && flatOnly && hasFlat) {
|
|
916
|
+
if ((itemAt flat i) == 0) {
|
|
917
|
+
take = false
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
if take {
|
|
921
|
+
def r:int (itemAt planeR i)
|
|
922
|
+
def g:int (itemAt planeG i)
|
|
923
|
+
def b:int (itemAt planeB i)
|
|
924
|
+
def key:int (((((idiv r 8) * 32) + (idiv g 8)) * 32) + (idiv b 8))
|
|
925
|
+
set cnt key ((itemAt cnt key) + 1)
|
|
926
|
+
set sumR key ((itemAt sumR key) + r)
|
|
927
|
+
set sumG key ((itemAt sumG key) + g)
|
|
928
|
+
set sumB key ((itemAt sumB key) + b)
|
|
929
|
+
}
|
|
930
|
+
i = (i + 1)
|
|
931
|
+
}
|
|
932
|
+
i = 0
|
|
933
|
+
while (i < bins) {
|
|
934
|
+
def c:int (itemAt cnt i)
|
|
935
|
+
if (c > 0) {
|
|
936
|
+
push binR (idiv (itemAt sumR i) c)
|
|
937
|
+
push binG (idiv (itemAt sumG i) c)
|
|
938
|
+
push binB (idiv (itemAt sumB i) c)
|
|
939
|
+
push binW c
|
|
940
|
+
}
|
|
941
|
+
i = (i + 1)
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
; ----------------------------------------------------------------------
|
|
946
|
+
; Palette: weighted k-means++ seeding + Lloyd passes over the histogram.
|
|
947
|
+
; Weighting by pixel count is what stops a handful of outlier pixels from
|
|
948
|
+
; claiming a swatch that then paints nothing — the old farthest-point seed
|
|
949
|
+
; was drawn to exactly those outliers.
|
|
950
|
+
; ----------------------------------------------------------------------
|
|
951
|
+
|
|
952
|
+
; Parse options.paletteHex into RGB triples, dropping anything EVGColor
|
|
953
|
+
; cannot read. Returns how many survived.
|
|
954
|
+
fn parsePaletteHex:int (outR:[int] outG:[int] outB:[int]) {
|
|
955
|
+
def n:int (array_length options.paletteHex)
|
|
956
|
+
def i:int 0
|
|
957
|
+
while (i < n) {
|
|
958
|
+
def c:EVGColor (EVGColor.parse((itemAt options.paletteHex i)))
|
|
959
|
+
if c.isSet {
|
|
960
|
+
push outR (to_int c.r)
|
|
961
|
+
push outG (to_int c.g)
|
|
962
|
+
push outB (to_int c.b)
|
|
963
|
+
}
|
|
964
|
+
i = (i + 1)
|
|
965
|
+
}
|
|
966
|
+
return (array_length outR)
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
; The seeding score for one histogram bin. "area" is population × distance²,
|
|
970
|
+
; which is why a photo's big flat background always takes the first swatches;
|
|
971
|
+
; the other two trade that away for colors that are merely distinct.
|
|
972
|
+
; How colorful a color is, 0 = neutral, 1 = as colorful as this picture is
|
|
973
|
+
; likely to get. OKLab chroma, normalized by 0.10 rather than by the 0.32
|
|
974
|
+
; a pure sRGB primary reaches: normalized against the primaries every
|
|
975
|
+
;色 an actual picture contains lands in the bottom fifth of the range and
|
|
976
|
+
; the dial does nothing. At 0.10 the ochre of a boat hull reads 0.79 and
|
|
977
|
+
; the grey of the sea 0.16, which is the separation the dial is for.
|
|
978
|
+
fn chromaOf:double (r:int g:int b:int) {
|
|
979
|
+
if ((array_length okLin) == 0) {
|
|
980
|
+
okLin = (EvgTraceColor.buildLinearTable())
|
|
981
|
+
}
|
|
982
|
+
okC.setFrom(okLin r g b)
|
|
983
|
+
def c:EvgOklab (unwrap okC)
|
|
984
|
+
def ca:double c.a
|
|
985
|
+
def cb:double c.b
|
|
986
|
+
def mag:double (sqrt ((ca * ca) + (cb * cb)))
|
|
987
|
+
def norm:double (mag / 0.10)
|
|
988
|
+
if (norm > 1.0) {
|
|
989
|
+
return 1.0
|
|
990
|
+
}
|
|
991
|
+
return norm
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
; ----------------------------------------------------------------------
|
|
995
|
+
; The palette objective (options.paletteMute / Tint / Warm / Contrast)
|
|
996
|
+
; ----------------------------------------------------------------------
|
|
997
|
+
;
|
|
998
|
+
; Quantization answers "which colors are in this picture". It does not
|
|
999
|
+
; answer "which colors should this picture be painted in", and the second
|
|
1000
|
+
; question has no exact match to snap to. A palette that lands on the
|
|
1001
|
+
; measured mean of every region is faithful and often dead: the mean of a
|
|
1002
|
+
; large region is a neutral, the mean of a small vivid one rarely survives
|
|
1003
|
+
; to be asked, and a picture built out of eight measured means has no color
|
|
1004
|
+
; in it that a painter would have chosen.
|
|
1005
|
+
;
|
|
1006
|
+
; So the palette gets its own objective, dialled rather than measured, in
|
|
1007
|
+
; OKLab where the dials mean what their names say. It runs AFTER
|
|
1008
|
+
; assignLabels: every decision about where an edge goes, which swatch a
|
|
1009
|
+
; pixel joins and which small region survives has already been made against
|
|
1010
|
+
; the measured colors, and what moves here is only what the picture is
|
|
1011
|
+
; painted with. Each dial is identity at its default, so an untouched
|
|
1012
|
+
; trace comes out exactly as before.
|
|
1013
|
+
fn gradePalette:void (palR:[int] palG:[int] palB:[int]) {
|
|
1014
|
+
def mute:int options.paletteMute
|
|
1015
|
+
def tint:int options.paletteTint
|
|
1016
|
+
def warm:int options.paletteWarm
|
|
1017
|
+
def contrast:int options.paletteContrast
|
|
1018
|
+
def on:boolean false
|
|
1019
|
+
if (mute != 100) {
|
|
1020
|
+
on = true
|
|
1021
|
+
}
|
|
1022
|
+
if (tint > 0) {
|
|
1023
|
+
on = true
|
|
1024
|
+
}
|
|
1025
|
+
if (warm != 0) {
|
|
1026
|
+
on = true
|
|
1027
|
+
}
|
|
1028
|
+
if (contrast != 100) {
|
|
1029
|
+
on = true
|
|
1030
|
+
}
|
|
1031
|
+
if (on == false) {
|
|
1032
|
+
return
|
|
1033
|
+
}
|
|
1034
|
+
def k:int (array_length palR)
|
|
1035
|
+
if (k == 0) {
|
|
1036
|
+
return
|
|
1037
|
+
}
|
|
1038
|
+
if ((array_length okLin) == 0) {
|
|
1039
|
+
okLin = (EvgTraceColor.buildLinearTable())
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
; Read the palette into OKLab. Two things about the palette as a whole
|
|
1043
|
+
; are needed: the lightness the contrast dial pivots about, and the hue
|
|
1044
|
+
; a swatch with no hue of its own is tinted in.
|
|
1045
|
+
def ls:[double]
|
|
1046
|
+
def cas:[double]
|
|
1047
|
+
def cbs:[double]
|
|
1048
|
+
def sumL:double 0.0
|
|
1049
|
+
def sumA:double 0.0
|
|
1050
|
+
def sumB:double 0.0
|
|
1051
|
+
def i:int 0
|
|
1052
|
+
while (i < k) {
|
|
1053
|
+
okC.setFrom(okLin (itemAt palR i) (itemAt palG i) (itemAt palB i))
|
|
1054
|
+
def c:EvgOklab (unwrap okC)
|
|
1055
|
+
def cl:double c.l
|
|
1056
|
+
def ca:double c.a
|
|
1057
|
+
def cb:double c.b
|
|
1058
|
+
push ls cl
|
|
1059
|
+
push cas ca
|
|
1060
|
+
push cbs cb
|
|
1061
|
+
sumL = (sumL + cl)
|
|
1062
|
+
sumA = (sumA + ca)
|
|
1063
|
+
sumB = (sumB + cb)
|
|
1064
|
+
i = (i + 1)
|
|
1065
|
+
}
|
|
1066
|
+
def kd:double (to_double k)
|
|
1067
|
+
def meanL:double (sumL / kd)
|
|
1068
|
+
def meanA:double (sumA / kd)
|
|
1069
|
+
def meanB:double (sumB / kd)
|
|
1070
|
+
def meanMag:double (sqrt ((meanA * meanA) + (meanB * meanB)))
|
|
1071
|
+
; A palette with no color at all still has to tint somewhere; yellow is
|
|
1072
|
+
; the direction paper and skin already lean, so it is the least
|
|
1073
|
+
; surprising default.
|
|
1074
|
+
def dirA:double 0.0
|
|
1075
|
+
def dirB:double 1.0
|
|
1076
|
+
if (meanMag > 0.000001) {
|
|
1077
|
+
dirA = (meanA / meanMag)
|
|
1078
|
+
dirB = (meanB / meanMag)
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
def wd:double ((to_double warm) / 1000.0)
|
|
1082
|
+
def floorC:double ((to_double tint) / 1000.0)
|
|
1083
|
+
def ms:double ((to_double mute) / 100.0)
|
|
1084
|
+
def cs:double ((to_double contrast) / 100.0)
|
|
1085
|
+
i = 0
|
|
1086
|
+
while (i < k) {
|
|
1087
|
+
; Warmth first, as a shift along the yellow–blue axis. That is what
|
|
1088
|
+
; a temperature change physically is, and unlike a hue rotation it
|
|
1089
|
+
; leaves a neutral neutral-ish instead of spinning it somewhere.
|
|
1090
|
+
def la:double (itemAt cas i)
|
|
1091
|
+
def lb:double ((itemAt cbs i) + wd)
|
|
1092
|
+
def mag:double (sqrt ((la * la) + (lb * lb)))
|
|
1093
|
+
def na:double dirA
|
|
1094
|
+
def nb:double dirB
|
|
1095
|
+
if (mag > 0.000001) {
|
|
1096
|
+
na = (la / mag)
|
|
1097
|
+
nb = (lb / mag)
|
|
1098
|
+
}
|
|
1099
|
+
; Chroma: scaled, then never below the tint floor, so the palette
|
|
1100
|
+
; can be broken toward grey without any swatch reaching dead grey.
|
|
1101
|
+
def chroma:double (mag * ms)
|
|
1102
|
+
if (chroma < floorC) {
|
|
1103
|
+
chroma = floorC
|
|
1104
|
+
}
|
|
1105
|
+
def lightness:double (meanL + (((itemAt ls i) - meanL) * cs))
|
|
1106
|
+
if (lightness < 0.0) {
|
|
1107
|
+
lightness = 0.0
|
|
1108
|
+
}
|
|
1109
|
+
if (lightness > 1.0) {
|
|
1110
|
+
lightness = 1.0
|
|
1111
|
+
}
|
|
1112
|
+
def outA:double (na * chroma)
|
|
1113
|
+
def outB:double (nb * chroma)
|
|
1114
|
+
def rgb:[int] (EvgTraceColor.srgbOf(lightness outA outB))
|
|
1115
|
+
set palR i (itemAt rgb 0)
|
|
1116
|
+
set palG i (itemAt rgb 1)
|
|
1117
|
+
set palB i (itemAt rgb 2)
|
|
1118
|
+
i = (i + 1)
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
; What one histogram bin is worth when the palette is decided.
|
|
1123
|
+
;
|
|
1124
|
+
; This is the objective, and it is not the same question as "reproduce the
|
|
1125
|
+
; picture". k-means over pixel counts minimizes total squared error, and a
|
|
1126
|
+
; color covering one percent of the frame contributes one percent of that
|
|
1127
|
+
; error — so dropping it is cheap by that measure and free by any measure
|
|
1128
|
+
; the quantizer can see. To a viewer it is the boats. options.paletteBias
|
|
1129
|
+
; chooses what a swatch is spent on and options.paletteChroma says how much
|
|
1130
|
+
; being colorful counts on top of it.
|
|
1131
|
+
;
|
|
1132
|
+
; The weight is used for the seeding, for the Lloyd passes, and for moving
|
|
1133
|
+
; a stranded swatch — every step that decides *where* a swatch sits. It is
|
|
1134
|
+
; deliberately not used for the noise floor, which stays on true population
|
|
1135
|
+
; so a single stray saturated pixel cannot buy a swatch, nor for the merge,
|
|
1136
|
+
; where the question is only which of two indistinguishable colors is the
|
|
1137
|
+
; representative and population is the right answer.
|
|
1138
|
+
fn binWeight:double (w:int r:int g:int b:int) {
|
|
1139
|
+
def bias:string options.paletteBias
|
|
1140
|
+
def base:double (to_double w)
|
|
1141
|
+
if (bias == "balanced") {
|
|
1142
|
+
base = (sqrt base)
|
|
1143
|
+
}
|
|
1144
|
+
if (bias == "distinct") {
|
|
1145
|
+
; Every color present counts once, however much of it there is.
|
|
1146
|
+
base = 1.0
|
|
1147
|
+
}
|
|
1148
|
+
def gain:int options.paletteChroma
|
|
1149
|
+
if (gain <= 0) {
|
|
1150
|
+
return base
|
|
1151
|
+
}
|
|
1152
|
+
def c:double (this.chromaOf(r g b))
|
|
1153
|
+
def g2:double (to_double gain)
|
|
1154
|
+
def mul:double (1.0 + ((g2 / 100.0) * c))
|
|
1155
|
+
return (base * mul)
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
fn seedScore:double (weight:double dist2:double) {
|
|
1159
|
+
return (weight * dist2)
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
; `outR/outG/outB` may arrive holding `locked` pinned swatches; those are
|
|
1163
|
+
; seeds the caller chose, so they are never moved by a Lloyd pass and never
|
|
1164
|
+
; dropped by the merge.
|
|
1165
|
+
fn buildPalette:void (want:int locked:int outR:[int] outG:[int] outB:[int]) {
|
|
1166
|
+
def binR:[int]
|
|
1167
|
+
def binG:[int]
|
|
1168
|
+
def binB:[int]
|
|
1169
|
+
def binW:[int]
|
|
1170
|
+
this.buildHistogram(true binR binG binB binW)
|
|
1171
|
+
; Line art can be thin enough to have no plateau at all — then let every
|
|
1172
|
+
; allowed pixel vote rather than emitting nothing.
|
|
1173
|
+
if ((array_length binR) < 2) {
|
|
1174
|
+
def allR:[int]
|
|
1175
|
+
def allG:[int]
|
|
1176
|
+
def allB:[int]
|
|
1177
|
+
def allW:[int]
|
|
1178
|
+
this.buildHistogram(false allR allG allB allW)
|
|
1179
|
+
binR = allR
|
|
1180
|
+
binG = allG
|
|
1181
|
+
binB = allB
|
|
1182
|
+
binW = allW
|
|
1183
|
+
}
|
|
1184
|
+
def m:int (array_length binR)
|
|
1185
|
+
if (m == 0) {
|
|
1186
|
+
return
|
|
1187
|
+
}
|
|
1188
|
+
; A bin has to clear a noise floor before it can win a swatch. Only the
|
|
1189
|
+
; "distinct" bias would otherwise hand one to a single stray pixel.
|
|
1190
|
+
def total:int 0
|
|
1191
|
+
def i:int 0
|
|
1192
|
+
while (i < m) {
|
|
1193
|
+
total = (total + (itemAt binW i))
|
|
1194
|
+
i = (i + 1)
|
|
1195
|
+
}
|
|
1196
|
+
def floorW:int (idiv total 2000)
|
|
1197
|
+
if (floorW < 2) {
|
|
1198
|
+
floorW = 2
|
|
1199
|
+
}
|
|
1200
|
+
if (locked == 0) {
|
|
1201
|
+
; Seed 1: the heaviest bin.
|
|
1202
|
+
def bestI:int 0
|
|
1203
|
+
def bestW:int (itemAt binW 0)
|
|
1204
|
+
i = 1
|
|
1205
|
+
while (i < m) {
|
|
1206
|
+
if ((itemAt binW i) > bestW) {
|
|
1207
|
+
bestW = (itemAt binW i)
|
|
1208
|
+
bestI = i
|
|
1209
|
+
}
|
|
1210
|
+
i = (i + 1)
|
|
1211
|
+
}
|
|
1212
|
+
push outR (itemAt binR bestI)
|
|
1213
|
+
push outG (itemAt binG bestI)
|
|
1214
|
+
push outB (itemAt binB bestI)
|
|
1215
|
+
}
|
|
1216
|
+
; Remaining seeds: maximize seedScore against the closest center so far.
|
|
1217
|
+
def dist:[double]
|
|
1218
|
+
i = 0
|
|
1219
|
+
while (i < m) {
|
|
1220
|
+
push dist (this.dist2((itemAt binR i) (itemAt binG i) (itemAt binB i) (itemAt outR 0) (itemAt outG 0) (itemAt outB 0)))
|
|
1221
|
+
i = (i + 1)
|
|
1222
|
+
}
|
|
1223
|
+
def seeded:int 1
|
|
1224
|
+
while (seeded < (array_length outR)) {
|
|
1225
|
+
i = 0
|
|
1226
|
+
while (i < m) {
|
|
1227
|
+
def d0:double (this.dist2((itemAt binR i) (itemAt binG i) (itemAt binB i) (itemAt outR seeded) (itemAt outG seeded) (itemAt outB seeded)))
|
|
1228
|
+
if (d0 < (itemAt dist i)) {
|
|
1229
|
+
set dist i d0
|
|
1230
|
+
}
|
|
1231
|
+
i = (i + 1)
|
|
1232
|
+
}
|
|
1233
|
+
seeded = (seeded + 1)
|
|
1234
|
+
}
|
|
1235
|
+
while ((array_length outR) < want) {
|
|
1236
|
+
def pickI:int (0 - 1)
|
|
1237
|
+
def pickScore:double 0.0
|
|
1238
|
+
i = 0
|
|
1239
|
+
while (i < m) {
|
|
1240
|
+
if ((itemAt binW i) >= floorW) {
|
|
1241
|
+
def bw:double (this.binWeight((itemAt binW i) (itemAt binR i) (itemAt binG i) (itemAt binB i)))
|
|
1242
|
+
def score:double (this.seedScore(bw (itemAt dist i)))
|
|
1243
|
+
if (score > pickScore) {
|
|
1244
|
+
pickScore = score
|
|
1245
|
+
pickI = i
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
i = (i + 1)
|
|
1249
|
+
}
|
|
1250
|
+
if (pickI < 0) {
|
|
1251
|
+
want = (array_length outR)
|
|
1252
|
+
} {
|
|
1253
|
+
def cr:int (itemAt binR pickI)
|
|
1254
|
+
def cg:int (itemAt binG pickI)
|
|
1255
|
+
def cb:int (itemAt binB pickI)
|
|
1256
|
+
push outR cr
|
|
1257
|
+
push outG cg
|
|
1258
|
+
push outB cb
|
|
1259
|
+
i = 0
|
|
1260
|
+
while (i < m) {
|
|
1261
|
+
def d:double (this.dist2((itemAt binR i) (itemAt binG i) (itemAt binB i) cr cg cb))
|
|
1262
|
+
if (d < (itemAt dist i)) {
|
|
1263
|
+
set dist i d
|
|
1264
|
+
}
|
|
1265
|
+
i = (i + 1)
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
; Lloyd passes, weighted by bin population.
|
|
1270
|
+
def k:int (array_length outR)
|
|
1271
|
+
def pass:int 0
|
|
1272
|
+
while (pass < 12) {
|
|
1273
|
+
def sumR:[double]
|
|
1274
|
+
def sumG:[double]
|
|
1275
|
+
def sumB:[double]
|
|
1276
|
+
def wgt:[double]
|
|
1277
|
+
def ki:int 0
|
|
1278
|
+
while (ki < k) {
|
|
1279
|
+
push sumR 0.0
|
|
1280
|
+
push sumG 0.0
|
|
1281
|
+
push sumB 0.0
|
|
1282
|
+
push wgt 0.0
|
|
1283
|
+
ki = (ki + 1)
|
|
1284
|
+
}
|
|
1285
|
+
i = 0
|
|
1286
|
+
while (i < m) {
|
|
1287
|
+
def br:int (itemAt binR i)
|
|
1288
|
+
def bg:int (itemAt binG i)
|
|
1289
|
+
def bb:int (itemAt binB i)
|
|
1290
|
+
def best:int (this.nearestIndex(br bg bb outR outG outB))
|
|
1291
|
+
def w:double (this.binWeight((itemAt binW i) br bg bb))
|
|
1292
|
+
set sumR best ((itemAt sumR best) + ((to_double br) * w))
|
|
1293
|
+
set sumG best ((itemAt sumG best) + ((to_double bg) * w))
|
|
1294
|
+
set sumB best ((itemAt sumB best) + ((to_double bb) * w))
|
|
1295
|
+
set wgt best ((itemAt wgt best) + w)
|
|
1296
|
+
set dist i 0.0
|
|
1297
|
+
i = (i + 1)
|
|
1298
|
+
}
|
|
1299
|
+
def moved:boolean false
|
|
1300
|
+
ki = locked
|
|
1301
|
+
while (ki < k) {
|
|
1302
|
+
def wk:double (itemAt wgt ki)
|
|
1303
|
+
if (wk > 0.0) {
|
|
1304
|
+
def nr:int (to_int ((itemAt sumR ki) / wk))
|
|
1305
|
+
def ng:int (to_int ((itemAt sumG ki) / wk))
|
|
1306
|
+
def nb:int (to_int ((itemAt sumB ki) / wk))
|
|
1307
|
+
if ((nr != (itemAt outR ki)) || (ng != (itemAt outG ki)) || (nb != (itemAt outB ki))) {
|
|
1308
|
+
moved = true
|
|
1309
|
+
}
|
|
1310
|
+
set outR ki nr
|
|
1311
|
+
set outG ki ng
|
|
1312
|
+
set outB ki nb
|
|
1313
|
+
} {
|
|
1314
|
+
; An empty swatch paints nothing — move it onto the bin that
|
|
1315
|
+
; is worst served right now instead of leaving it stranded.
|
|
1316
|
+
def farI:int (0 - 1)
|
|
1317
|
+
def farScore:double 0.0
|
|
1318
|
+
i = 0
|
|
1319
|
+
while (i < m) {
|
|
1320
|
+
def dd:double (this.dist2((itemAt binR i) (itemAt binG i) (itemAt binB i) (itemAt outR (this.nearestIndex((itemAt binR i) (itemAt binG i) (itemAt binB i) outR outG outB))) (itemAt outG (this.nearestIndex((itemAt binR i) (itemAt binG i) (itemAt binB i) outR outG outB))) (itemAt outB (this.nearestIndex((itemAt binR i) (itemAt binG i) (itemAt binB i) outR outG outB)))))
|
|
1321
|
+
def bw2:double (this.binWeight((itemAt binW i) (itemAt binR i) (itemAt binG i) (itemAt binB i)))
|
|
1322
|
+
def sc:double (dd * bw2)
|
|
1323
|
+
if (sc > farScore) {
|
|
1324
|
+
farScore = sc
|
|
1325
|
+
farI = i
|
|
1326
|
+
}
|
|
1327
|
+
i = (i + 1)
|
|
1328
|
+
}
|
|
1329
|
+
if (farI >= 0) {
|
|
1330
|
+
set outR ki (itemAt binR farI)
|
|
1331
|
+
set outG ki (itemAt binG farI)
|
|
1332
|
+
set outB ki (itemAt binB farI)
|
|
1333
|
+
moved = true
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
ki = (ki + 1)
|
|
1337
|
+
}
|
|
1338
|
+
if (moved) {
|
|
1339
|
+
pass = (pass + 1)
|
|
1340
|
+
} {
|
|
1341
|
+
pass = 12
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
; Collapse swatches the eye cannot tell apart. Without this, asking for
|
|
1345
|
+
; more colors than the image holds splits one region between two
|
|
1346
|
+
; near-identical swatches and turdsize eats both halves.
|
|
1347
|
+
this.mergeCloseSwatches(locked outR outG outB binR binG binB binW)
|
|
1348
|
+
; Sort palette dark → light (ascending luma). Masks are disjoint so
|
|
1349
|
+
; paint order is mostly cosmetic; lightest ends up last.
|
|
1350
|
+
def k2:int (array_length outR)
|
|
1351
|
+
def a:int 0
|
|
1352
|
+
while (a < k2) {
|
|
1353
|
+
def bIdx:int (a + 1)
|
|
1354
|
+
while (bIdx < k2) {
|
|
1355
|
+
def la:int (EvgBitmapTracer.lumaOf((itemAt outR a) (itemAt outG a) (itemAt outB a)))
|
|
1356
|
+
def lb:int (EvgBitmapTracer.lumaOf((itemAt outR bIdx) (itemAt outG bIdx) (itemAt outB bIdx)))
|
|
1357
|
+
if (la > lb) {
|
|
1358
|
+
def tr:int (itemAt outR a)
|
|
1359
|
+
def tg:int (itemAt outG a)
|
|
1360
|
+
def tb:int (itemAt outB a)
|
|
1361
|
+
set outR a (itemAt outR bIdx)
|
|
1362
|
+
set outG a (itemAt outG bIdx)
|
|
1363
|
+
set outB a (itemAt outB bIdx)
|
|
1364
|
+
set outR bIdx tr
|
|
1365
|
+
set outG bIdx tg
|
|
1366
|
+
set outB bIdx tb
|
|
1367
|
+
}
|
|
1368
|
+
bIdx = (bIdx + 1)
|
|
1369
|
+
}
|
|
1370
|
+
a = (a + 1)
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
; Merge palette entries closer than options.minColorDelta, keeping whichever
|
|
1375
|
+
; of the pair paints more pixels. The first `locked` entries were chosen by
|
|
1376
|
+
; the caller: a pair of them is left alone, and against an automatic swatch
|
|
1377
|
+
; the locked one is what survives.
|
|
1378
|
+
fn mergeCloseSwatches:void (locked:int outR:[int] outG:[int] outB:[int] binR:[int] binG:[int] binB:[int] binW:[int]) {
|
|
1379
|
+
def delta:int options.minColorDelta
|
|
1380
|
+
if (delta <= 0) {
|
|
1381
|
+
return
|
|
1382
|
+
}
|
|
1383
|
+
; Same metric the assignment uses, scaled so `delta` still reads as a
|
|
1384
|
+
; per-channel difference whichever space is measuring.
|
|
1385
|
+
def limit:double (this.dist2Limit(delta))
|
|
1386
|
+
def merged:boolean true
|
|
1387
|
+
while merged {
|
|
1388
|
+
merged = false
|
|
1389
|
+
def k:int (array_length outR)
|
|
1390
|
+
if (k < 2) {
|
|
1391
|
+
return
|
|
1392
|
+
}
|
|
1393
|
+
; Population of each swatch, so the survivor is the one that paints.
|
|
1394
|
+
def wgt:[int]
|
|
1395
|
+
def ki:int 0
|
|
1396
|
+
while (ki < k) {
|
|
1397
|
+
push wgt 0
|
|
1398
|
+
ki = (ki + 1)
|
|
1399
|
+
}
|
|
1400
|
+
def m:int (array_length binR)
|
|
1401
|
+
def i:int 0
|
|
1402
|
+
while (i < m) {
|
|
1403
|
+
def idx:int (this.nearestIndex((itemAt binR i) (itemAt binG i) (itemAt binB i) outR outG outB))
|
|
1404
|
+
set wgt idx ((itemAt wgt idx) + (itemAt binW i))
|
|
1405
|
+
i = (i + 1)
|
|
1406
|
+
}
|
|
1407
|
+
def dropAt:int (0 - 1)
|
|
1408
|
+
def a:int 0
|
|
1409
|
+
while ((a < k) && (dropAt < 0)) {
|
|
1410
|
+
def b:int (a + 1)
|
|
1411
|
+
while ((b < k) && (dropAt < 0)) {
|
|
1412
|
+
def d:double (this.dist2((itemAt outR a) (itemAt outG a) (itemAt outB a) (itemAt outR b) (itemAt outG b) (itemAt outB b)))
|
|
1413
|
+
if (d <= limit) {
|
|
1414
|
+
if (b < locked) {
|
|
1415
|
+
; both pinned — the caller asked for both.
|
|
1416
|
+
} {
|
|
1417
|
+
if (a < locked) {
|
|
1418
|
+
dropAt = b
|
|
1419
|
+
} {
|
|
1420
|
+
if ((itemAt wgt a) >= (itemAt wgt b)) {
|
|
1421
|
+
dropAt = b
|
|
1422
|
+
} {
|
|
1423
|
+
dropAt = a
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
b = (b + 1)
|
|
1429
|
+
}
|
|
1430
|
+
a = (a + 1)
|
|
1431
|
+
}
|
|
1432
|
+
if (dropAt >= 0) {
|
|
1433
|
+
remove outR dropAt
|
|
1434
|
+
remove outG dropAt
|
|
1435
|
+
remove outB dropAt
|
|
1436
|
+
merged = true
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
fn nearestIndex:int (r:int g:int b:int palR:[int] palG:[int] palB:[int]) {
|
|
1442
|
+
def k:int (array_length palR)
|
|
1443
|
+
if (k == 0) {
|
|
1444
|
+
return 0
|
|
1445
|
+
}
|
|
1446
|
+
def best:int 0
|
|
1447
|
+
def bestD:double (this.dist2(r g b (itemAt palR 0) (itemAt palG 0) (itemAt palB 0)))
|
|
1448
|
+
def j:int 1
|
|
1449
|
+
while (j < k) {
|
|
1450
|
+
def d:double (this.dist2(r g b (itemAt palR j) (itemAt palG j) (itemAt palB j)))
|
|
1451
|
+
if (d < bestD) {
|
|
1452
|
+
bestD = d
|
|
1453
|
+
best = j
|
|
1454
|
+
}
|
|
1455
|
+
j = (j + 1)
|
|
1456
|
+
}
|
|
1457
|
+
return best
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
; ----------------------------------------------------------------------
|
|
1461
|
+
; Pixel → swatch assignment
|
|
1462
|
+
; ----------------------------------------------------------------------
|
|
1463
|
+
|
|
1464
|
+
; Every allowed pixel takes its nearest swatch. A transitional pixel is
|
|
1465
|
+
; then re-checked against the regions actually touching it: it only moves
|
|
1466
|
+
; there when the neighbouring swatch fits about as well as the global best
|
|
1467
|
+
; (snapRatio). That guard matters — the one-pixel ink core of a thin
|
|
1468
|
+
; anti-aliased outline is transitional too, and an unguarded snap hands it
|
|
1469
|
+
; to the big region beside it, which is what turns outlines into dashes.
|
|
1470
|
+
fn assignLabels:void (palR:[int] palG:[int] palB:[int]) {
|
|
1471
|
+
def n:int (width * height)
|
|
1472
|
+
def base:[int]
|
|
1473
|
+
def dists:[double]
|
|
1474
|
+
def i:int 0
|
|
1475
|
+
while (i < n) {
|
|
1476
|
+
push base (0 - 1)
|
|
1477
|
+
push dists 0.0
|
|
1478
|
+
i = (i + 1)
|
|
1479
|
+
}
|
|
1480
|
+
i = 0
|
|
1481
|
+
while (i < n) {
|
|
1482
|
+
if (this.pixelAllowed(i)) {
|
|
1483
|
+
def lab:int (this.nearestIndex((itemAt planeR i) (itemAt planeG i) (itemAt planeB i) palR palG palB))
|
|
1484
|
+
set base i lab
|
|
1485
|
+
set dists i (this.dist2((itemAt planeR i) (itemAt planeG i) (itemAt planeB i) (itemAt palR lab) (itemAt palG lab) (itemAt palB lab)))
|
|
1486
|
+
}
|
|
1487
|
+
i = (i + 1)
|
|
1488
|
+
}
|
|
1489
|
+
def outL:[int]
|
|
1490
|
+
i = 0
|
|
1491
|
+
while (i < n) {
|
|
1492
|
+
push outL (itemAt base i)
|
|
1493
|
+
i = (i + 1)
|
|
1494
|
+
}
|
|
1495
|
+
def hasFlat:boolean ((array_length flat) == n)
|
|
1496
|
+
if (options.edgeSnap && hasFlat) {
|
|
1497
|
+
def ratio:double options.snapRatio
|
|
1498
|
+
def y:int 0
|
|
1499
|
+
while (y < height) {
|
|
1500
|
+
def x:int 0
|
|
1501
|
+
while (x < width) {
|
|
1502
|
+
def idx:int ((y * width) + x)
|
|
1503
|
+
def gLab:int (itemAt base idx)
|
|
1504
|
+
if ((gLab >= 0) && ((itemAt flat idx) == 0)) {
|
|
1505
|
+
def gD:double (itemAt dists idx)
|
|
1506
|
+
def pr:int (itemAt planeR idx)
|
|
1507
|
+
def pg:int (itemAt planeG idx)
|
|
1508
|
+
def pb:int (itemAt planeB idx)
|
|
1509
|
+
def nLab:int (0 - 1)
|
|
1510
|
+
def nD:double 0.0
|
|
1511
|
+
def dy:int (0 - 1)
|
|
1512
|
+
while (dy <= 1) {
|
|
1513
|
+
def yy:int (y + dy)
|
|
1514
|
+
if ((yy >= 0) && (yy < height)) {
|
|
1515
|
+
def dx:int (0 - 1)
|
|
1516
|
+
while (dx <= 1) {
|
|
1517
|
+
def xx:int (x + dx)
|
|
1518
|
+
if ((xx >= 0) && (xx < width)) {
|
|
1519
|
+
def nIdx:int ((yy * width) + xx)
|
|
1520
|
+
if ((itemAt flat nIdx) == 1) {
|
|
1521
|
+
def lab2:int (itemAt base nIdx)
|
|
1522
|
+
if (lab2 >= 0) {
|
|
1523
|
+
def d:double (this.dist2(pr pg pb (itemAt palR lab2) (itemAt palG lab2) (itemAt palB lab2)))
|
|
1524
|
+
if ((nLab < 0) || (d < nD)) {
|
|
1525
|
+
nLab = lab2
|
|
1526
|
+
nD = d
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
dx = (dx + 1)
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
dy = (dy + 1)
|
|
1535
|
+
}
|
|
1536
|
+
if ((nLab >= 0) && (nLab != gLab)) {
|
|
1537
|
+
if (nD <= ((gD * ratio) + 1.0)) {
|
|
1538
|
+
set outL idx nLab
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
x = (x + 1)
|
|
1543
|
+
}
|
|
1544
|
+
y = (y + 1)
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
labels = outL
|
|
1548
|
+
this.despeckleLabels()
|
|
1549
|
+
this.smoothContours(palR palG palB)
|
|
1550
|
+
this.mergeTinyRegions(palR palG palB)
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
; ----------------------------------------------------------------------
|
|
1554
|
+
; Banding suppression (options.contourMode)
|
|
1555
|
+
; ----------------------------------------------------------------------
|
|
1556
|
+
|
|
1557
|
+
; One step of the continuity walk. A neighbour joins when the step to it is
|
|
1558
|
+
; small enough to read as continuous AND it has not drifted too far from
|
|
1559
|
+
; what the region has averaged so far — the second test is what stops a
|
|
1560
|
+
; gradient from walking off across the whole picture.
|
|
1561
|
+
fn contourStep:void (i:int from:int seen:[int] stack:[int] acc:[int] edgeTol:int spread:int) {
|
|
1562
|
+
if ((itemAt seen i) != 0) {
|
|
1563
|
+
return
|
|
1564
|
+
}
|
|
1565
|
+
if ((itemAt labels i) < 0) {
|
|
1566
|
+
return
|
|
1567
|
+
}
|
|
1568
|
+
; A step stops the growth only when it is a boundary: strongest across
|
|
1569
|
+
; its own direction, and part of a run rather than a speck. On a
|
|
1570
|
+
; photograph nearly every neighbouring pair differs by a few levels, so
|
|
1571
|
+
; taking each one at face value stops the growth everywhere and the
|
|
1572
|
+
; picture comes apart into noise.
|
|
1573
|
+
if (this.isBoundary(from i edgeTol)) {
|
|
1574
|
+
return
|
|
1575
|
+
}
|
|
1576
|
+
def cnt:int (itemAt acc 3)
|
|
1577
|
+
if (cnt > 0) {
|
|
1578
|
+
def mr:int (idiv (itemAt acc 0) cnt)
|
|
1579
|
+
def mg:int (idiv (itemAt acc 1) cnt)
|
|
1580
|
+
def mb:int (idiv (itemAt acc 2) cnt)
|
|
1581
|
+
def d:int (EvgBitmapTracer.absI((itemAt planeR i) - mr))
|
|
1582
|
+
def dg:int (EvgBitmapTracer.absI((itemAt planeG i) - mg))
|
|
1583
|
+
if (dg > d) {
|
|
1584
|
+
d = dg
|
|
1585
|
+
}
|
|
1586
|
+
def db:int (EvgBitmapTracer.absI((itemAt planeB i) - mb))
|
|
1587
|
+
if (db > d) {
|
|
1588
|
+
d = db
|
|
1589
|
+
}
|
|
1590
|
+
if (d > spread) {
|
|
1591
|
+
return
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
set seen i 1
|
|
1595
|
+
push stack i
|
|
1596
|
+
set acc 0 ((itemAt acc 0) + (itemAt planeR i))
|
|
1597
|
+
set acc 1 ((itemAt acc 1) + (itemAt planeG i))
|
|
1598
|
+
set acc 2 ((itemAt acc 2) + (itemAt planeB i))
|
|
1599
|
+
set acc 3 (cnt + 1)
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
; ----------------------------------------------------------------------
|
|
1603
|
+
; Which steps are boundaries and which are noise
|
|
1604
|
+
; ----------------------------------------------------------------------
|
|
1605
|
+
|
|
1606
|
+
; Mark the steps that are worth treating as boundaries. Two tests, and a
|
|
1607
|
+
; step has to pass both:
|
|
1608
|
+
; unique — it is at least as strong as the steps either side of it
|
|
1609
|
+
; ACROSS its own direction. A ridge keeps its crest; a lone
|
|
1610
|
+
; noisy pixel loses to whatever it sits next to.
|
|
1611
|
+
; continuous — it belongs to a run of at least edgeMinRun such pixels.
|
|
1612
|
+
; A boundary is a line; noise is scattered.
|
|
1613
|
+
; Mark the neighbourhoods that need fine work.
|
|
1614
|
+
;
|
|
1615
|
+
; Not every busy patch of a picture is the same kind of busy. A gradient is
|
|
1616
|
+
; busy and wants to be left alone; an eye is busy because three or four
|
|
1617
|
+
; plainly different colors — the white, the iris, the lash, the skin — meet
|
|
1618
|
+
; inside a few pixels, and that is detail a coarse pass throws away. The
|
|
1619
|
+
; test is therefore both halves at once: several distinct swatches inside a
|
|
1620
|
+
; small window *and* a wide luma spread across it. A smooth ramp fails the
|
|
1621
|
+
; first (its window is one or two swatches), and a patch of noise in one
|
|
1622
|
+
; flat color fails the second.
|
|
1623
|
+
;
|
|
1624
|
+
; Overlay uses this to work at a finer grain where it finds it, and only
|
|
1625
|
+
; there, which is what lets the whites and the darks of an eye be resolved
|
|
1626
|
+
; without spending the same effort on a cheek.
|
|
1627
|
+
fn buildDetailMask:void () {
|
|
1628
|
+
def n:int (width * height)
|
|
1629
|
+
def dm:[int]
|
|
1630
|
+
def i:int 0
|
|
1631
|
+
while (i < n) {
|
|
1632
|
+
push dm 0
|
|
1633
|
+
i = (i + 1)
|
|
1634
|
+
}
|
|
1635
|
+
detailMask = dm
|
|
1636
|
+
detailOn = ((options.detailBoost) >= 2)
|
|
1637
|
+
def minSwatches:int options.detailSwatches
|
|
1638
|
+
if (minSwatches < 2) {
|
|
1639
|
+
return
|
|
1640
|
+
}
|
|
1641
|
+
def minSpread:int options.detailSpread
|
|
1642
|
+
def r:int options.detailRadius
|
|
1643
|
+
if (r < 1) {
|
|
1644
|
+
r = 1
|
|
1645
|
+
}
|
|
1646
|
+
def lum:[int]
|
|
1647
|
+
i = 0
|
|
1648
|
+
while (i < n) {
|
|
1649
|
+
push lum (EvgBitmapTracer.lumaOf((itemAt planeR i) (itemAt planeG i) (itemAt planeB i)))
|
|
1650
|
+
i = (i + 1)
|
|
1651
|
+
}
|
|
1652
|
+
; One slot per swatch, stamped with the window it was last seen in,
|
|
1653
|
+
; instead of a fresh list per pixel searched linearly. The old form
|
|
1654
|
+
; allocated an array for every pixel in the image and scanned it once
|
|
1655
|
+
; per neighbour — a quarter of a million allocations to answer "how many
|
|
1656
|
+
; different swatches are in this little square".
|
|
1657
|
+
def mark:[int]
|
|
1658
|
+
def mi:int 0
|
|
1659
|
+
def marks:int 256
|
|
1660
|
+
while (mi < marks) {
|
|
1661
|
+
push mark (0 - 1)
|
|
1662
|
+
mi = (mi + 1)
|
|
1663
|
+
}
|
|
1664
|
+
def visit:int 0
|
|
1665
|
+
def y:int 0
|
|
1666
|
+
while (y < height) {
|
|
1667
|
+
def x:int 0
|
|
1668
|
+
while (x < width) {
|
|
1669
|
+
visit = (visit + 1)
|
|
1670
|
+
def distinct:int 0
|
|
1671
|
+
def lo:int 256
|
|
1672
|
+
def hi:int (0 - 1)
|
|
1673
|
+
def dy:int (0 - r)
|
|
1674
|
+
while (dy <= r) {
|
|
1675
|
+
def yy:int (y + dy)
|
|
1676
|
+
if ((yy >= 0) && (yy < height)) {
|
|
1677
|
+
def dx:int (0 - r)
|
|
1678
|
+
while (dx <= r) {
|
|
1679
|
+
def xx:int (x + dx)
|
|
1680
|
+
if ((xx >= 0) && (xx < width)) {
|
|
1681
|
+
def q:int ((yy * width) + xx)
|
|
1682
|
+
def lab:int (itemAt labels q)
|
|
1683
|
+
if (lab >= 0) {
|
|
1684
|
+
if (lab < marks) {
|
|
1685
|
+
if ((itemAt mark lab) != visit) {
|
|
1686
|
+
set mark lab visit
|
|
1687
|
+
distinct = (distinct + 1)
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
def lv:int (itemAt lum q)
|
|
1691
|
+
if (lv < lo) {
|
|
1692
|
+
lo = lv
|
|
1693
|
+
}
|
|
1694
|
+
if (lv > hi) {
|
|
1695
|
+
hi = lv
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
dx = (dx + 1)
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
dy = (dy + 1)
|
|
1703
|
+
}
|
|
1704
|
+
if (distinct >= minSwatches) {
|
|
1705
|
+
if ((hi - lo) >= minSpread) {
|
|
1706
|
+
set dm ((y * width) + x) 1
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
x = (x + 1)
|
|
1710
|
+
}
|
|
1711
|
+
y = (y + 1)
|
|
1712
|
+
}
|
|
1713
|
+
; What the test marks is where the colors *meet* — a ring around an eye
|
|
1714
|
+
; rather than the eye. The flat middle of a pupil is as plain as a cheek
|
|
1715
|
+
; by this measure, and left out it would split the feature in two.
|
|
1716
|
+
; Growing the mark by the window radius closes the ring into the shape a
|
|
1717
|
+
; person would have drawn around the feature.
|
|
1718
|
+
def grown:[int]
|
|
1719
|
+
i = 0
|
|
1720
|
+
while (i < n) {
|
|
1721
|
+
push grown (itemAt dm i)
|
|
1722
|
+
i = (i + 1)
|
|
1723
|
+
}
|
|
1724
|
+
def gy:int 0
|
|
1725
|
+
while (gy < height) {
|
|
1726
|
+
def gx:int 0
|
|
1727
|
+
while (gx < width) {
|
|
1728
|
+
if ((itemAt dm ((gy * width) + gx)) == 1) {
|
|
1729
|
+
def dy2:int (0 - r)
|
|
1730
|
+
while (dy2 <= r) {
|
|
1731
|
+
def yy2:int (gy + dy2)
|
|
1732
|
+
if ((yy2 >= 0) && (yy2 < height)) {
|
|
1733
|
+
def dx2:int (0 - r)
|
|
1734
|
+
while (dx2 <= r) {
|
|
1735
|
+
def xx2:int (gx + dx2)
|
|
1736
|
+
if ((xx2 >= 0) && (xx2 < width)) {
|
|
1737
|
+
set grown ((yy2 * width) + xx2) 1
|
|
1738
|
+
}
|
|
1739
|
+
dx2 = (dx2 + 1)
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
dy2 = (dy2 + 1)
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
gx = (gx + 1)
|
|
1746
|
+
}
|
|
1747
|
+
gy = (gy + 1)
|
|
1748
|
+
}
|
|
1749
|
+
detailMask = grown
|
|
1750
|
+
detailOn = ((options.detailBoost) >= 2)
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
fn buildEdgeMask:void () {
|
|
1754
|
+
def n:int (width * height)
|
|
1755
|
+
def keep:[int]
|
|
1756
|
+
def mag:[int]
|
|
1757
|
+
def lum:[int]
|
|
1758
|
+
def i:int 0
|
|
1759
|
+
while (i < n) {
|
|
1760
|
+
push keep 0
|
|
1761
|
+
push mag 0
|
|
1762
|
+
push lum (EvgBitmapTracer.lumaOf((itemAt planeR i) (itemAt planeG i) (itemAt planeB i)))
|
|
1763
|
+
i = (i + 1)
|
|
1764
|
+
}
|
|
1765
|
+
if ((options.edgeMinRun) < 1) {
|
|
1766
|
+
edgeMask = keep
|
|
1767
|
+
edgeMaskOn = false
|
|
1768
|
+
return
|
|
1769
|
+
}
|
|
1770
|
+
def edgeTol:int options.contourEdge
|
|
1771
|
+
if (edgeTol < 1) {
|
|
1772
|
+
edgeTol = 1
|
|
1773
|
+
}
|
|
1774
|
+
; --- magnitude ---
|
|
1775
|
+
def y:int 1
|
|
1776
|
+
while (y < (height - 1)) {
|
|
1777
|
+
def x:int 1
|
|
1778
|
+
while (x < (width - 1)) {
|
|
1779
|
+
def idx:int ((y * width) + x)
|
|
1780
|
+
def gx:int ((itemAt lum (idx + 1)) - (itemAt lum (idx - 1)))
|
|
1781
|
+
def gy:int ((itemAt lum (idx + width)) - (itemAt lum (idx - width)))
|
|
1782
|
+
set mag idx ((EvgBitmapTracer.absI(gx)) + (EvgBitmapTracer.absI(gy)))
|
|
1783
|
+
x = (x + 1)
|
|
1784
|
+
}
|
|
1785
|
+
y = (y + 1)
|
|
1786
|
+
}
|
|
1787
|
+
; --- unique: strongest across its own direction ---
|
|
1788
|
+
y = 1
|
|
1789
|
+
while (y < (height - 1)) {
|
|
1790
|
+
def x2:int 1
|
|
1791
|
+
while (x2 < (width - 1)) {
|
|
1792
|
+
def idx2:int ((y * width) + x2)
|
|
1793
|
+
def m:int (itemAt mag idx2)
|
|
1794
|
+
if (m > edgeTol) {
|
|
1795
|
+
def gx2:int ((itemAt lum (idx2 + 1)) - (itemAt lum (idx2 - 1)))
|
|
1796
|
+
def gy2:int ((itemAt lum (idx2 + width)) - (itemAt lum (idx2 - width)))
|
|
1797
|
+
def ax:int (EvgBitmapTracer.absI(gx2))
|
|
1798
|
+
def ay:int (EvgBitmapTracer.absI(gy2))
|
|
1799
|
+
def a:int 0
|
|
1800
|
+
def b:int 0
|
|
1801
|
+
if (ax > (ay * 2)) {
|
|
1802
|
+
a = (idx2 - 1)
|
|
1803
|
+
b = (idx2 + 1)
|
|
1804
|
+
} {
|
|
1805
|
+
if (ay > (ax * 2)) {
|
|
1806
|
+
a = (idx2 - width)
|
|
1807
|
+
b = (idx2 + width)
|
|
1808
|
+
} {
|
|
1809
|
+
if ((gx2 * gy2) > 0) {
|
|
1810
|
+
a = ((idx2 - width) - 1)
|
|
1811
|
+
b = ((idx2 + width) + 1)
|
|
1812
|
+
} {
|
|
1813
|
+
a = ((idx2 - width) + 1)
|
|
1814
|
+
b = ((idx2 + width) - 1)
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
if ((m >= (itemAt mag a)) && (m >= (itemAt mag b))) {
|
|
1819
|
+
set keep idx2 1
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
x2 = (x2 + 1)
|
|
1823
|
+
}
|
|
1824
|
+
y = (y + 1)
|
|
1825
|
+
}
|
|
1826
|
+
; --- continuous: drop runs shorter than edgeMinRun ---
|
|
1827
|
+
def minRun:int options.edgeMinRun
|
|
1828
|
+
def seen:[int]
|
|
1829
|
+
i = 0
|
|
1830
|
+
while (i < n) {
|
|
1831
|
+
push seen 0
|
|
1832
|
+
i = (i + 1)
|
|
1833
|
+
}
|
|
1834
|
+
def start:int 0
|
|
1835
|
+
while (start < n) {
|
|
1836
|
+
if (((itemAt keep start) == 1) && ((itemAt seen start) == 0)) {
|
|
1837
|
+
def comp:[int]
|
|
1838
|
+
def stack:[int]
|
|
1839
|
+
set seen start 1
|
|
1840
|
+
push stack start
|
|
1841
|
+
def head:int 0
|
|
1842
|
+
while (head < (array_length stack)) {
|
|
1843
|
+
def cur:int (itemAt stack head)
|
|
1844
|
+
head = (head + 1)
|
|
1845
|
+
push comp cur
|
|
1846
|
+
def cy:int (idiv cur width)
|
|
1847
|
+
def cx:int (cur - (cy * width))
|
|
1848
|
+
def dy:int (0 - 1)
|
|
1849
|
+
while (dy <= 1) {
|
|
1850
|
+
def yy:int (cy + dy)
|
|
1851
|
+
if ((yy >= 0) && (yy < height)) {
|
|
1852
|
+
def dx:int (0 - 1)
|
|
1853
|
+
while (dx <= 1) {
|
|
1854
|
+
def xx:int (cx + dx)
|
|
1855
|
+
if ((xx >= 0) && (xx < width)) {
|
|
1856
|
+
def j:int ((yy * width) + xx)
|
|
1857
|
+
if (((itemAt keep j) == 1) && ((itemAt seen j) == 0)) {
|
|
1858
|
+
set seen j 1
|
|
1859
|
+
push stack j
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
dx = (dx + 1)
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
dy = (dy + 1)
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
if ((array_length comp) < minRun) {
|
|
1869
|
+
def c:int 0
|
|
1870
|
+
while (c < (array_length comp)) {
|
|
1871
|
+
set keep (itemAt comp c) 0
|
|
1872
|
+
c = (c + 1)
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
start = (start + 1)
|
|
1877
|
+
}
|
|
1878
|
+
edgeMask = keep
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
; True when the step from `a` to `b` should split two regions: big enough,
|
|
1882
|
+
; and landing on a boundary that passed both tests rather than on noise.
|
|
1883
|
+
fn isBoundary:boolean (a:int b:int edgeTol:int) {
|
|
1884
|
+
return (this.isBoundaryOf(a b edgeTol (this.pixelDelta(a b))))
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
; The same test for a caller that has already measured the step. The growth
|
|
1888
|
+
; loop had measured it, compared it to the tolerance, and then called in
|
|
1889
|
+
; here — which measured it again. pixelDelta was a fifth of the trace.
|
|
1890
|
+
fn isBoundaryOf:boolean (a:int b:int edgeTol:int delta:int) {
|
|
1891
|
+
if (delta <= edgeTol) {
|
|
1892
|
+
return false
|
|
1893
|
+
}
|
|
1894
|
+
if (edgeMaskOn == false) {
|
|
1895
|
+
return true
|
|
1896
|
+
}
|
|
1897
|
+
if ((itemAt edgeMask b) == 1) {
|
|
1898
|
+
return true
|
|
1899
|
+
}
|
|
1900
|
+
return ((itemAt edgeMask a) == 1)
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
; Grow every allowed pixel into a region of continuous color and record
|
|
1904
|
+
; which region it landed in. Both the banding fix and the gradient fills
|
|
1905
|
+
; read this; it is the segmentation, and everything downstream is a choice
|
|
1906
|
+
; of what to paint each region with.
|
|
1907
|
+
fn buildContourRegions:void () {
|
|
1908
|
+
this.buildEdgeMask()
|
|
1909
|
+
def edgeTol:int options.contourEdge
|
|
1910
|
+
if (edgeTol < 1) {
|
|
1911
|
+
edgeTol = 1
|
|
1912
|
+
}
|
|
1913
|
+
def spread:int options.contourSpread
|
|
1914
|
+
if (spread < 1) {
|
|
1915
|
+
spread = 1
|
|
1916
|
+
}
|
|
1917
|
+
def n:int (width * height)
|
|
1918
|
+
def ro:[int]
|
|
1919
|
+
def seen:[int]
|
|
1920
|
+
def i:int 0
|
|
1921
|
+
while (i < n) {
|
|
1922
|
+
push ro (0 - 1)
|
|
1923
|
+
push seen 0
|
|
1924
|
+
i = (i + 1)
|
|
1925
|
+
}
|
|
1926
|
+
def rid:int 0
|
|
1927
|
+
def start:int 0
|
|
1928
|
+
while (start < n) {
|
|
1929
|
+
if (((itemAt seen start) == 0) && ((itemAt labels start) >= 0)) {
|
|
1930
|
+
def stack:[int]
|
|
1931
|
+
def acc:[int]
|
|
1932
|
+
push acc (itemAt planeR start)
|
|
1933
|
+
push acc (itemAt planeG start)
|
|
1934
|
+
push acc (itemAt planeB start)
|
|
1935
|
+
push acc 1
|
|
1936
|
+
set seen start 1
|
|
1937
|
+
push stack start
|
|
1938
|
+
def head:int 0
|
|
1939
|
+
while (head < (array_length stack)) {
|
|
1940
|
+
def cur:int (itemAt stack head)
|
|
1941
|
+
head = (head + 1)
|
|
1942
|
+
set ro cur rid
|
|
1943
|
+
def cy:int (idiv cur width)
|
|
1944
|
+
def cx:int (cur - (cy * width))
|
|
1945
|
+
if (cx > 0) {
|
|
1946
|
+
this.contourStep((cur - 1) cur seen stack acc edgeTol spread)
|
|
1947
|
+
}
|
|
1948
|
+
if (cx < (width - 1)) {
|
|
1949
|
+
this.contourStep((cur + 1) cur seen stack acc edgeTol spread)
|
|
1950
|
+
}
|
|
1951
|
+
if (cy > 0) {
|
|
1952
|
+
this.contourStep((cur - width) cur seen stack acc edgeTol spread)
|
|
1953
|
+
}
|
|
1954
|
+
if (cy < (height - 1)) {
|
|
1955
|
+
this.contourStep((cur + width) cur seen stack acc edgeTol spread)
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
rid = (rid + 1)
|
|
1959
|
+
}
|
|
1960
|
+
start = (start + 1)
|
|
1961
|
+
}
|
|
1962
|
+
regionOf = ro
|
|
1963
|
+
regionCount = rid
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
; Give each continuity region the one swatch nearest its own mean, so a
|
|
1967
|
+
; ramp stops being cut into bands.
|
|
1968
|
+
fn smoothContours:void (palR:[int] palG:[int] palB:[int]) {
|
|
1969
|
+
if ((options.contourMode) != "smooth") {
|
|
1970
|
+
return
|
|
1971
|
+
}
|
|
1972
|
+
this.buildContourRegions()
|
|
1973
|
+
def k:int regionCount
|
|
1974
|
+
if (k == 0) {
|
|
1975
|
+
return
|
|
1976
|
+
}
|
|
1977
|
+
def sumR:[int]
|
|
1978
|
+
def sumG:[int]
|
|
1979
|
+
def sumB:[int]
|
|
1980
|
+
def cnt:[int]
|
|
1981
|
+
def i:int 0
|
|
1982
|
+
while (i < k) {
|
|
1983
|
+
push sumR 0
|
|
1984
|
+
push sumG 0
|
|
1985
|
+
push sumB 0
|
|
1986
|
+
push cnt 0
|
|
1987
|
+
i = (i + 1)
|
|
1988
|
+
}
|
|
1989
|
+
def n:int (width * height)
|
|
1990
|
+
i = 0
|
|
1991
|
+
while (i < n) {
|
|
1992
|
+
def r:int (itemAt regionOf i)
|
|
1993
|
+
if (r >= 0) {
|
|
1994
|
+
set sumR r ((itemAt sumR r) + (itemAt planeR i))
|
|
1995
|
+
set sumG r ((itemAt sumG r) + (itemAt planeG i))
|
|
1996
|
+
set sumB r ((itemAt sumB r) + (itemAt planeB i))
|
|
1997
|
+
set cnt r ((itemAt cnt r) + 1)
|
|
1998
|
+
}
|
|
1999
|
+
i = (i + 1)
|
|
2000
|
+
}
|
|
2001
|
+
def lab:[int]
|
|
2002
|
+
i = 0
|
|
2003
|
+
while (i < k) {
|
|
2004
|
+
def c:int (itemAt cnt i)
|
|
2005
|
+
if (c > 0) {
|
|
2006
|
+
push lab (this.nearestIndex((idiv (itemAt sumR i) c) (idiv (itemAt sumG i) c) (idiv (itemAt sumB i) c) palR palG palB))
|
|
2007
|
+
} {
|
|
2008
|
+
push lab 0
|
|
2009
|
+
}
|
|
2010
|
+
i = (i + 1)
|
|
2011
|
+
}
|
|
2012
|
+
i = 0
|
|
2013
|
+
while (i < n) {
|
|
2014
|
+
def r2:int (itemAt regionOf i)
|
|
2015
|
+
if (r2 >= 0) {
|
|
2016
|
+
set labels i (itemAt lab r2)
|
|
2017
|
+
}
|
|
2018
|
+
i = (i + 1)
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
; ----------------------------------------------------------------------
|
|
2023
|
+
; Gradient fills (options.gradientFill)
|
|
2024
|
+
;
|
|
2025
|
+
; Each continuity region is fitted with three models — one flat color, a
|
|
2026
|
+
; linear ramp — and keeps whichever leaves the least error. A radial model
|
|
2027
|
+
; is fitted alongside them but not used: away from a genuinely round region
|
|
2028
|
+
; its cone infers structure the picture does not contain.
|
|
2029
|
+
; ----------------------------------------------------------------------
|
|
2030
|
+
|
|
2031
|
+
sfn clamp255:int (v:double) {
|
|
2032
|
+
if (v < 0.0) {
|
|
2033
|
+
return 0
|
|
2034
|
+
}
|
|
2035
|
+
if (v > 255.0) {
|
|
2036
|
+
return 255
|
|
2037
|
+
}
|
|
2038
|
+
return (to_int v)
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
; Merge any region below minPx into whichever neighbour it shares the most
|
|
2042
|
+
; boundary with, then renumber what is left from zero.
|
|
2043
|
+
fn absorbTinyRegions:void (minPx:int) {
|
|
2044
|
+
def k:int regionCount
|
|
2045
|
+
if (k == 0) {
|
|
2046
|
+
return
|
|
2047
|
+
}
|
|
2048
|
+
def n:int (width * height)
|
|
2049
|
+
def size:[int]
|
|
2050
|
+
def i:int 0
|
|
2051
|
+
while (i < k) {
|
|
2052
|
+
push size 0
|
|
2053
|
+
i = (i + 1)
|
|
2054
|
+
}
|
|
2055
|
+
i = 0
|
|
2056
|
+
while (i < n) {
|
|
2057
|
+
def r:int (itemAt regionOf i)
|
|
2058
|
+
if (r >= 0) {
|
|
2059
|
+
set size r ((itemAt size r) + 1)
|
|
2060
|
+
}
|
|
2061
|
+
i = (i + 1)
|
|
2062
|
+
}
|
|
2063
|
+
; One pass: every small region takes the neighbour it touches most.
|
|
2064
|
+
def target:[int]
|
|
2065
|
+
i = 0
|
|
2066
|
+
while (i < k) {
|
|
2067
|
+
push target i
|
|
2068
|
+
i = (i + 1)
|
|
2069
|
+
}
|
|
2070
|
+
def bestN:[int]
|
|
2071
|
+
def bestR:[int]
|
|
2072
|
+
i = 0
|
|
2073
|
+
while (i < k) {
|
|
2074
|
+
push bestN 0
|
|
2075
|
+
push bestR (0 - 1)
|
|
2076
|
+
i = (i + 1)
|
|
2077
|
+
}
|
|
2078
|
+
def y:int 0
|
|
2079
|
+
while (y < height) {
|
|
2080
|
+
def x:int 0
|
|
2081
|
+
while (x < width) {
|
|
2082
|
+
def idx:int ((y * width) + x)
|
|
2083
|
+
def r0:int (itemAt regionOf idx)
|
|
2084
|
+
if ((r0 >= 0) && ((itemAt size r0) < minPx)) {
|
|
2085
|
+
if (x < (width - 1)) {
|
|
2086
|
+
this.tallyNeighbour(r0 (itemAt regionOf (idx + 1)) size minPx bestN bestR)
|
|
2087
|
+
}
|
|
2088
|
+
if (x > 0) {
|
|
2089
|
+
this.tallyNeighbour(r0 (itemAt regionOf (idx - 1)) size minPx bestN bestR)
|
|
2090
|
+
}
|
|
2091
|
+
if (y < (height - 1)) {
|
|
2092
|
+
this.tallyNeighbour(r0 (itemAt regionOf (idx + width)) size minPx bestN bestR)
|
|
2093
|
+
}
|
|
2094
|
+
if (y > 0) {
|
|
2095
|
+
this.tallyNeighbour(r0 (itemAt regionOf (idx - width)) size minPx bestN bestR)
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
x = (x + 1)
|
|
2099
|
+
}
|
|
2100
|
+
y = (y + 1)
|
|
2101
|
+
}
|
|
2102
|
+
i = 0
|
|
2103
|
+
while (i < k) {
|
|
2104
|
+
if ((itemAt bestR i) >= 0) {
|
|
2105
|
+
set target i (itemAt bestR i)
|
|
2106
|
+
}
|
|
2107
|
+
i = (i + 1)
|
|
2108
|
+
}
|
|
2109
|
+
; Renumber: only regions that still hold pixels get an id.
|
|
2110
|
+
def remap:[int]
|
|
2111
|
+
i = 0
|
|
2112
|
+
while (i < k) {
|
|
2113
|
+
push remap (0 - 1)
|
|
2114
|
+
i = (i + 1)
|
|
2115
|
+
}
|
|
2116
|
+
def next:int 0
|
|
2117
|
+
i = 0
|
|
2118
|
+
while (i < n) {
|
|
2119
|
+
def r2:int (itemAt regionOf i)
|
|
2120
|
+
if (r2 >= 0) {
|
|
2121
|
+
def t:int (itemAt target r2)
|
|
2122
|
+
if ((itemAt remap t) < 0) {
|
|
2123
|
+
set remap t next
|
|
2124
|
+
next = (next + 1)
|
|
2125
|
+
}
|
|
2126
|
+
set regionOf i (itemAt remap t)
|
|
2127
|
+
}
|
|
2128
|
+
i = (i + 1)
|
|
2129
|
+
}
|
|
2130
|
+
regionCount = next
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
; The biggest neighbour wins. Requiring it to clear minPx would strand a
|
|
2134
|
+
; chain of small regions that only ever touch each other — which is most of
|
|
2135
|
+
; a photograph — so any different neighbour will do, and repeated passes
|
|
2136
|
+
; then cascade the small ones into the large.
|
|
2137
|
+
fn tallyNeighbour:void (mine:int other:int size:[int] minPx:int bestN:[int] bestR:[int]) {
|
|
2138
|
+
if (other < 0) {
|
|
2139
|
+
return
|
|
2140
|
+
}
|
|
2141
|
+
if (other == mine) {
|
|
2142
|
+
return
|
|
2143
|
+
}
|
|
2144
|
+
def sz:int (itemAt size other)
|
|
2145
|
+
if ((itemAt bestR mine) < 0) {
|
|
2146
|
+
set bestR mine other
|
|
2147
|
+
set bestN mine sz
|
|
2148
|
+
return
|
|
2149
|
+
}
|
|
2150
|
+
if (sz > (itemAt bestN mine)) {
|
|
2151
|
+
set bestR mine other
|
|
2152
|
+
set bestN mine sz
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
|
|
2156
|
+
; Least squares for v = a + b·x + c·y over one region, by Cramer's rule
|
|
2157
|
+
; on the normal equations. out = [ok, a, b, c].
|
|
2158
|
+
sfn solveLinear:void (n:double sx:double sy:double sxx:double sxy:double syy:double v0:double v1:double v2:double out:[double]) {
|
|
2159
|
+
def det:double (((n * ((sxx * syy) - (sxy * sxy))) - (sx * ((sx * syy) - (sxy * sy)))) + (sy * ((sx * sxy) - (sxx * sy))))
|
|
2160
|
+
if ((EvgBitmapTracer.absD(det)) < 0.000001) {
|
|
2161
|
+
set out 0 0.0
|
|
2162
|
+
return
|
|
2163
|
+
}
|
|
2164
|
+
def da:double (((v0 * ((sxx * syy) - (sxy * sxy))) - (sx * ((v1 * syy) - (sxy * v2)))) + (sy * ((v1 * sxy) - (sxx * v2))))
|
|
2165
|
+
def db:double (((n * ((v1 * syy) - (v2 * sxy))) - (v0 * ((sx * syy) - (sxy * sy)))) + (sy * ((sx * v2) - (v1 * sy))))
|
|
2166
|
+
def dc:double (((n * ((sxx * v2) - (sxy * v1))) - (sx * ((sx * v2) - (v1 * sy)))) + (v0 * ((sx * sxy) - (sxx * sy))))
|
|
2167
|
+
set out 0 1.0
|
|
2168
|
+
set out 1 (da / det)
|
|
2169
|
+
set out 2 (db / det)
|
|
2170
|
+
set out 3 (dc / det)
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2173
|
+
fn hexOf:string (r:double g:double b:double) {
|
|
2174
|
+
return (this.hexFromRgb((EvgBitmapTracer.clamp255(r)) (EvgBitmapTracer.clamp255(g)) (EvgBitmapTracer.clamp255(b))))
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
; One layer per continuity region, filled with whichever of flat / linear /
|
|
2178
|
+
; a linear ramp explains it best. Regions are painted largest first, each mask
|
|
2179
|
+
; is grown by a pixel, so the smaller shape laid on top covers the join —
|
|
2180
|
+
; the same reason the swatch layers are stacked rather than butted together.
|
|
2181
|
+
fn traceGradientRegions:void (palR:[int] palG:[int] palB:[int]) {
|
|
2182
|
+
this.buildContourRegions()
|
|
2183
|
+
def floorPx:int options.minRegion
|
|
2184
|
+
if (floorPx < 8) {
|
|
2185
|
+
floorPx = 8
|
|
2186
|
+
}
|
|
2187
|
+
; Absorb until it settles, not once. A pass lets a region below the
|
|
2188
|
+
; floor join the largest big-enough neighbour it touches — and on a
|
|
2189
|
+
; photograph a small region's neighbours are mostly small too, so one
|
|
2190
|
+
; pass moves almost nothing and the floor never bites. Measured before
|
|
2191
|
+
; this: minRegion from 6 to 4000 moved the region count from 1131 to
|
|
2192
|
+
; 1052, which is a control that does nothing. Each pass grows the
|
|
2193
|
+
; survivors, so neighbours that were too small last time are eligible
|
|
2194
|
+
; this time, and repeating until the count stops falling is what makes
|
|
2195
|
+
; the number the user set the number they get.
|
|
2196
|
+
def prev:int (regionCount + 1)
|
|
2197
|
+
def guard:int 0
|
|
2198
|
+
while ((regionCount < prev) && (guard < 24)) {
|
|
2199
|
+
prev = regionCount
|
|
2200
|
+
this.absorbTinyRegions(floorPx)
|
|
2201
|
+
guard = (guard + 1)
|
|
2202
|
+
}
|
|
2203
|
+
; A photograph can still segment into tens of thousands of scraps; each
|
|
2204
|
+
; one would be its own traced path. Raise the floor until that is sane.
|
|
2205
|
+
guard = 0
|
|
2206
|
+
while ((regionCount > 2000) && (guard < 8)) {
|
|
2207
|
+
floorPx = (floorPx * 2)
|
|
2208
|
+
prev = (regionCount + 1)
|
|
2209
|
+
def inner:int 0
|
|
2210
|
+
while ((regionCount < prev) && (inner < 24)) {
|
|
2211
|
+
prev = regionCount
|
|
2212
|
+
this.absorbTinyRegions(floorPx)
|
|
2213
|
+
inner = (inner + 1)
|
|
2214
|
+
}
|
|
2215
|
+
guard = (guard + 1)
|
|
2216
|
+
}
|
|
2217
|
+
def k:int regionCount
|
|
2218
|
+
if (k == 0) {
|
|
2219
|
+
return
|
|
2220
|
+
}
|
|
2221
|
+
def n:int (width * height)
|
|
2222
|
+
; --- pass 1: geometry and the linear normal equations ---
|
|
2223
|
+
def rn:[double]
|
|
2224
|
+
def rsx:[double]
|
|
2225
|
+
def rsy:[double]
|
|
2226
|
+
def rsxx:[double]
|
|
2227
|
+
def rsyy:[double]
|
|
2228
|
+
def rsxy:[double]
|
|
2229
|
+
def sv:[double]
|
|
2230
|
+
def sxv:[double]
|
|
2231
|
+
def syv:[double]
|
|
2232
|
+
def svv:[double]
|
|
2233
|
+
def i:int 0
|
|
2234
|
+
while (i < k) {
|
|
2235
|
+
push rn 0.0
|
|
2236
|
+
push rsx 0.0
|
|
2237
|
+
push rsy 0.0
|
|
2238
|
+
push rsxx 0.0
|
|
2239
|
+
push rsyy 0.0
|
|
2240
|
+
push rsxy 0.0
|
|
2241
|
+
def c:int 0
|
|
2242
|
+
while (c < 3) {
|
|
2243
|
+
push sv 0.0
|
|
2244
|
+
push sxv 0.0
|
|
2245
|
+
push syv 0.0
|
|
2246
|
+
push svv 0.0
|
|
2247
|
+
c = (c + 1)
|
|
2248
|
+
}
|
|
2249
|
+
i = (i + 1)
|
|
2250
|
+
}
|
|
2251
|
+
def y:int 0
|
|
2252
|
+
while (y < height) {
|
|
2253
|
+
def x:int 0
|
|
2254
|
+
while (x < width) {
|
|
2255
|
+
def idx:int ((y * width) + x)
|
|
2256
|
+
def r:int (itemAt regionOf idx)
|
|
2257
|
+
if (r >= 0) {
|
|
2258
|
+
def dx:double (to_double x)
|
|
2259
|
+
def dy:double (to_double y)
|
|
2260
|
+
set rn r ((itemAt rn r) + 1.0)
|
|
2261
|
+
set rsx r ((itemAt rsx r) + dx)
|
|
2262
|
+
set rsy r ((itemAt rsy r) + dy)
|
|
2263
|
+
set rsxx r ((itemAt rsxx r) + (dx * dx))
|
|
2264
|
+
set rsyy r ((itemAt rsyy r) + (dy * dy))
|
|
2265
|
+
set rsxy r ((itemAt rsxy r) + (dx * dy))
|
|
2266
|
+
def c2:int 0
|
|
2267
|
+
while (c2 < 3) {
|
|
2268
|
+
def v:double (to_double (this.planeAt(c2 idx)))
|
|
2269
|
+
def j:int ((r * 3) + c2)
|
|
2270
|
+
set sv j ((itemAt sv j) + v)
|
|
2271
|
+
set sxv j ((itemAt sxv j) + (dx * v))
|
|
2272
|
+
set syv j ((itemAt syv j) + (dy * v))
|
|
2273
|
+
set svv j ((itemAt svv j) + (v * v))
|
|
2274
|
+
c2 = (c2 + 1)
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
x = (x + 1)
|
|
2278
|
+
}
|
|
2279
|
+
y = (y + 1)
|
|
2280
|
+
}
|
|
2281
|
+
; --- solve the linear model, per region per channel ---
|
|
2282
|
+
def la:[double]
|
|
2283
|
+
def lb:[double]
|
|
2284
|
+
def lc:[double]
|
|
2285
|
+
def lok:[double]
|
|
2286
|
+
i = 0
|
|
2287
|
+
while (i < k) {
|
|
2288
|
+
def sol:[double]
|
|
2289
|
+
push sol 0.0
|
|
2290
|
+
push sol 0.0
|
|
2291
|
+
push sol 0.0
|
|
2292
|
+
push sol 0.0
|
|
2293
|
+
def okAll:double 1.0
|
|
2294
|
+
def c3:int 0
|
|
2295
|
+
while (c3 < 3) {
|
|
2296
|
+
def j2:int ((i * 3) + c3)
|
|
2297
|
+
EvgBitmapTracer.solveLinear((itemAt rn i) (itemAt rsx i) (itemAt rsy i) (itemAt rsxx i) (itemAt rsxy i) (itemAt rsyy i) (itemAt sv j2) (itemAt sxv j2) (itemAt syv j2) sol)
|
|
2298
|
+
if ((itemAt sol 0) < 0.5) {
|
|
2299
|
+
okAll = 0.0
|
|
2300
|
+
}
|
|
2301
|
+
push la (itemAt sol 1)
|
|
2302
|
+
push lb (itemAt sol 2)
|
|
2303
|
+
push lc (itemAt sol 3)
|
|
2304
|
+
c3 = (c3 + 1)
|
|
2305
|
+
}
|
|
2306
|
+
push lok okAll
|
|
2307
|
+
i = (i + 1)
|
|
2308
|
+
}
|
|
2309
|
+
; --- pass 2: radial sums and the extents, both centroid-based ---
|
|
2310
|
+
def rsd:[double]
|
|
2311
|
+
def rsdd:[double]
|
|
2312
|
+
def rsdv:[double]
|
|
2313
|
+
def tmin:[double]
|
|
2314
|
+
def tmax:[double]
|
|
2315
|
+
def dmax:[double]
|
|
2316
|
+
def ux:[double]
|
|
2317
|
+
def uy:[double]
|
|
2318
|
+
i = 0
|
|
2319
|
+
while (i < k) {
|
|
2320
|
+
push rsd 0.0
|
|
2321
|
+
push rsdd 0.0
|
|
2322
|
+
push tmin 1000000.0
|
|
2323
|
+
push tmax (0.0 - 1000000.0)
|
|
2324
|
+
push dmax 0.0
|
|
2325
|
+
def c4:int 0
|
|
2326
|
+
while (c4 < 3) {
|
|
2327
|
+
push rsdv 0.0
|
|
2328
|
+
c4 = (c4 + 1)
|
|
2329
|
+
}
|
|
2330
|
+
; The gradient's direction is the one luma runs along — the three
|
|
2331
|
+
; channels each have their own slope, but the SVG has one axis.
|
|
2332
|
+
def bl:double (((0.299 * (itemAt lb ((i * 3) + 0))) + (0.587 * (itemAt lb ((i * 3) + 1)))) + (0.114 * (itemAt lb ((i * 3) + 2))))
|
|
2333
|
+
def cl:double (((0.299 * (itemAt lc ((i * 3) + 0))) + (0.587 * (itemAt lc ((i * 3) + 1)))) + (0.114 * (itemAt lc ((i * 3) + 2))))
|
|
2334
|
+
def mag:double (sqrt ((bl * bl) + (cl * cl)))
|
|
2335
|
+
if (mag < 0.000001) {
|
|
2336
|
+
push ux 1.0
|
|
2337
|
+
push uy 0.0
|
|
2338
|
+
} {
|
|
2339
|
+
push ux (bl / mag)
|
|
2340
|
+
push uy (cl / mag)
|
|
2341
|
+
}
|
|
2342
|
+
i = (i + 1)
|
|
2343
|
+
}
|
|
2344
|
+
y = 0
|
|
2345
|
+
while (y < height) {
|
|
2346
|
+
def x2:int 0
|
|
2347
|
+
while (x2 < width) {
|
|
2348
|
+
def idx2:int ((y * width) + x2)
|
|
2349
|
+
def r2:int (itemAt regionOf idx2)
|
|
2350
|
+
if (r2 >= 0) {
|
|
2351
|
+
def cnt:double (itemAt rn r2)
|
|
2352
|
+
def mx:double ((itemAt rsx r2) / cnt)
|
|
2353
|
+
def my:double ((itemAt rsy r2) / cnt)
|
|
2354
|
+
def ddx:double ((to_double x2) - mx)
|
|
2355
|
+
def ddy:double ((to_double y) - my)
|
|
2356
|
+
def dist:double (sqrt ((ddx * ddx) + (ddy * ddy)))
|
|
2357
|
+
set rsd r2 ((itemAt rsd r2) + dist)
|
|
2358
|
+
set rsdd r2 ((itemAt rsdd r2) + (dist * dist))
|
|
2359
|
+
if (dist > (itemAt dmax r2)) {
|
|
2360
|
+
set dmax r2 dist
|
|
2361
|
+
}
|
|
2362
|
+
def tproj:double ((ddx * (itemAt ux r2)) + (ddy * (itemAt uy r2)))
|
|
2363
|
+
if (tproj < (itemAt tmin r2)) {
|
|
2364
|
+
set tmin r2 tproj
|
|
2365
|
+
}
|
|
2366
|
+
if (tproj > (itemAt tmax r2)) {
|
|
2367
|
+
set tmax r2 tproj
|
|
2368
|
+
}
|
|
2369
|
+
def c5:int 0
|
|
2370
|
+
while (c5 < 3) {
|
|
2371
|
+
def j3:int ((r2 * 3) + c5)
|
|
2372
|
+
set rsdv j3 ((itemAt rsdv j3) + (dist * (to_double (this.planeAt(c5 idx2)))))
|
|
2373
|
+
c5 = (c5 + 1)
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
x2 = (x2 + 1)
|
|
2377
|
+
}
|
|
2378
|
+
y = (y + 1)
|
|
2379
|
+
}
|
|
2380
|
+
this.emitRegionLayers(palR palG palB k rn rsx rsy sv svv sxv syv la lb lc lok rsd rsdd rsdv tmin tmax dmax ux uy)
|
|
2381
|
+
}
|
|
2382
|
+
|
|
2383
|
+
fn planeAt:int (c:int i:int) {
|
|
2384
|
+
if (c == 0) {
|
|
2385
|
+
return (itemAt planeR i)
|
|
2386
|
+
}
|
|
2387
|
+
if (c == 1) {
|
|
2388
|
+
return (itemAt planeG i)
|
|
2389
|
+
}
|
|
2390
|
+
return (itemAt planeB i)
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2393
|
+
; Pick the model for each region and trace it. Sum of squared error comes
|
|
2394
|
+
; straight out of the sums already gathered, so choosing costs no extra
|
|
2395
|
+
; pass over the pixels.
|
|
2396
|
+
fn emitRegionLayers:void (palR:[int] palG:[int] palB:[int] k:int rn:[double] rsx:[double] rsy:[double] sv:[double] svv:[double] sxv:[double] syv:[double] la:[double] lb:[double] lc:[double] lok:[double] rsd:[double] rsdd:[double] rsdv:[double] tmin:[double] tmax:[double] dmax:[double] ux:[double] uy:[double]) {
|
|
2397
|
+
; Largest region first: a smaller shape drawn later covers the join.
|
|
2398
|
+
def order:[int]
|
|
2399
|
+
def i:int 0
|
|
2400
|
+
while (i < k) {
|
|
2401
|
+
push order i
|
|
2402
|
+
i = (i + 1)
|
|
2403
|
+
}
|
|
2404
|
+
def a:int 0
|
|
2405
|
+
while (a < k) {
|
|
2406
|
+
def b:int (a + 1)
|
|
2407
|
+
while (b < k) {
|
|
2408
|
+
if ((itemAt rn (itemAt order b)) > (itemAt rn (itemAt order a))) {
|
|
2409
|
+
def tmp:int (itemAt order a)
|
|
2410
|
+
set order a (itemAt order b)
|
|
2411
|
+
set order b tmp
|
|
2412
|
+
}
|
|
2413
|
+
b = (b + 1)
|
|
2414
|
+
}
|
|
2415
|
+
a = (a + 1)
|
|
2416
|
+
}
|
|
2417
|
+
def gain:double ((to_double options.gradientGain) / 100.0)
|
|
2418
|
+
def allCmds:[PathCommand]
|
|
2419
|
+
def oi:int 0
|
|
2420
|
+
while (oi < k) {
|
|
2421
|
+
def r:int (itemAt order oi)
|
|
2422
|
+
def cnt:double (itemAt rn r)
|
|
2423
|
+
if (cnt > 0.0) {
|
|
2424
|
+
def mx:double ((itemAt rsx r) / cnt)
|
|
2425
|
+
def my:double ((itemAt rsy r) / cnt)
|
|
2426
|
+
; --- error of each model, summed over the three channels ---
|
|
2427
|
+
def eFlat:double 0.0
|
|
2428
|
+
def eLin:double 0.0
|
|
2429
|
+
def eRad:double 0.0
|
|
2430
|
+
def ra:[double]
|
|
2431
|
+
def rb:[double]
|
|
2432
|
+
def c:int 0
|
|
2433
|
+
while (c < 3) {
|
|
2434
|
+
def j:int ((r * 3) + c)
|
|
2435
|
+
def n2:double cnt
|
|
2436
|
+
def s1:double (itemAt sv j)
|
|
2437
|
+
eFlat = (eFlat + ((itemAt svv j) - ((s1 * s1) / n2)))
|
|
2438
|
+
if ((itemAt lok r) > 0.5) {
|
|
2439
|
+
eLin = (eLin + ((itemAt svv j) - ((((itemAt la j) * s1) + ((itemAt lb j) * (itemAt sxv j))) + ((itemAt lc j) * (itemAt syv j)))))
|
|
2440
|
+
} {
|
|
2441
|
+
eLin = (eLin + 1000000000.0)
|
|
2442
|
+
}
|
|
2443
|
+
def den:double ((n2 * (itemAt rsdd r)) - ((itemAt rsd r) * (itemAt rsd r)))
|
|
2444
|
+
if ((EvgBitmapTracer.absD(den)) > 0.000001) {
|
|
2445
|
+
def bb:double (((n2 * (itemAt rsdv j)) - ((itemAt rsd r) * s1)) / den)
|
|
2446
|
+
def aa:double ((s1 - (bb * (itemAt rsd r))) / n2)
|
|
2447
|
+
push ra aa
|
|
2448
|
+
push rb bb
|
|
2449
|
+
eRad = (eRad + ((itemAt svv j) - ((aa * s1) + (bb * (itemAt rsdv j)))))
|
|
2450
|
+
} {
|
|
2451
|
+
push ra 0.0
|
|
2452
|
+
push rb 0.0
|
|
2453
|
+
eRad = (eRad + 1000000000.0)
|
|
2454
|
+
}
|
|
2455
|
+
c = (c + 1)
|
|
2456
|
+
}
|
|
2457
|
+
if (eFlat < 0.0) {
|
|
2458
|
+
eFlat = 0.0
|
|
2459
|
+
}
|
|
2460
|
+
; A thin or tiny region gives an ill-conditioned fit: the normal
|
|
2461
|
+
; equations are near-singular along the missing direction and
|
|
2462
|
+
; Cramer's rule answers with wild coefficients whose residual
|
|
2463
|
+
; looks excellent and whose stop colors are nonsense. Below a
|
|
2464
|
+
; useful size, and whenever the arithmetic returns something a
|
|
2465
|
+
; sum of squares cannot be, stay flat.
|
|
2466
|
+
if (cnt < 64.0) {
|
|
2467
|
+
eLin = 1000000000.0
|
|
2468
|
+
eRad = 1000000000.0
|
|
2469
|
+
}
|
|
2470
|
+
if (eLin < 0.0) {
|
|
2471
|
+
eLin = 1000000000.0
|
|
2472
|
+
}
|
|
2473
|
+
if (eRad < 0.0) {
|
|
2474
|
+
eRad = 1000000000.0
|
|
2475
|
+
}
|
|
2476
|
+
; --- choose ---
|
|
2477
|
+
def kind:string "flat"
|
|
2478
|
+
def best:double eFlat
|
|
2479
|
+
if (eLin < best) {
|
|
2480
|
+
best = eLin
|
|
2481
|
+
kind = "linear"
|
|
2482
|
+
}
|
|
2483
|
+
; Radial is fitted but never chosen: an isotropic cone
|
|
2484
|
+
; invents structure that is not in the picture wherever the
|
|
2485
|
+
; region is not actually round, and the shapes it infers are
|
|
2486
|
+
; strange rather than merely wrong. Kept out until it can be
|
|
2487
|
+
; fitted to the region's real geometry.
|
|
2488
|
+
; A gradient has to earn its place: a nearly flat region stays
|
|
2489
|
+
; flat. Both tests are needed — the relative one alone lets a
|
|
2490
|
+
; perfectly even region through, because zero is not 15% worse
|
|
2491
|
+
; than zero, and it would emit a gradient with two equal stops.
|
|
2492
|
+
if (kind != "flat") {
|
|
2493
|
+
if ((eFlat - best) < (eFlat * gain)) {
|
|
2494
|
+
kind = "flat"
|
|
2495
|
+
}
|
|
2496
|
+
if (eFlat < (cnt * 3.0)) {
|
|
2497
|
+
kind = "flat"
|
|
2498
|
+
}
|
|
2499
|
+
}
|
|
2500
|
+
def mask:EvgBinaryBitmap (this.maskForRegion(r))
|
|
2501
|
+
def layerOpts:EvgTraceOptions (EvgTraceOptions.defaults())
|
|
2502
|
+
layerOpts.turdsize = options.turdsize
|
|
2503
|
+
layerOpts.alphamax = options.alphamax
|
|
2504
|
+
layerOpts.turnpolicy = options.turnpolicy
|
|
2505
|
+
layerOpts.optcurve = options.optcurve
|
|
2506
|
+
layerOpts.opttolerance = options.opttolerance
|
|
2507
|
+
layerOpts.pathFormat = options.pathFormat
|
|
2508
|
+
layerOpts.pathPrecision = options.pathPrecision
|
|
2509
|
+
def sub:EvgBitmapTracer (EvgBitmapTracer.fromBinary(mask layerOpts))
|
|
2510
|
+
sub.trace()
|
|
2511
|
+
if ((sub.ringCount()) > 0) {
|
|
2512
|
+
def layer:EvgTraceLayer (itemAt sub.layers 0)
|
|
2513
|
+
def meanR:double ((itemAt sv ((r * 3) + 0)) / cnt)
|
|
2514
|
+
def meanG:double ((itemAt sv ((r * 3) + 1)) / cnt)
|
|
2515
|
+
def meanB:double ((itemAt sv ((r * 3) + 2)) / cnt)
|
|
2516
|
+
; Snap the region's own mean onto the palette. It was
|
|
2517
|
+
; written out raw, which meant one color per region and a
|
|
2518
|
+
; thousand regions: a portrait asked for ten swatches came
|
|
2519
|
+
; back with 1128, and nine in ten of those were flat fills —
|
|
2520
|
+
; a mosaic, not a gradient render. A gradient's stops stay
|
|
2521
|
+
; as measured, because describing a ramp is the whole point
|
|
2522
|
+
; of the mode; this is the color the region falls back to,
|
|
2523
|
+
; and the color the palette strip reads.
|
|
2524
|
+
def gi:int (this.nearestIndex((EvgBitmapTracer.clamp255(meanR)) (EvgBitmapTracer.clamp255(meanG)) (EvgBitmapTracer.clamp255(meanB)) palR palG palB))
|
|
2525
|
+
layer.fillHex = (this.hexFromRgb((itemAt palR gi) (itemAt palG gi) (itemAt palB gi)))
|
|
2526
|
+
layer.fillKind = kind
|
|
2527
|
+
if (kind == "linear") {
|
|
2528
|
+
def t0:double (itemAt tmin r)
|
|
2529
|
+
def t1:double (itemAt tmax r)
|
|
2530
|
+
; A region with no extent along its own gradient axis
|
|
2531
|
+
; would emit a zero-length linearGradient, which SVG
|
|
2532
|
+
; leaves undefined — and an axis a pixel or two long is
|
|
2533
|
+
; a flat color written the long way. One says it better.
|
|
2534
|
+
if ((t1 - t0) < 2.0) {
|
|
2535
|
+
kind = "flat"
|
|
2536
|
+
layer.fillKind = "flat"
|
|
2537
|
+
}
|
|
2538
|
+
layer.gx0 = (mx + ((itemAt ux r) * t0))
|
|
2539
|
+
layer.gy0 = (my + ((itemAt uy r) * t0))
|
|
2540
|
+
layer.gx1 = (mx + ((itemAt ux r) * t1))
|
|
2541
|
+
layer.gy1 = (my + ((itemAt uy r) * t1))
|
|
2542
|
+
layer.stopA = (this.hexOf((this.linAt(la lb lc r 0 layer.gx0 layer.gy0)) (this.linAt(la lb lc r 1 layer.gx0 layer.gy0)) (this.linAt(la lb lc r 2 layer.gx0 layer.gy0))))
|
|
2543
|
+
layer.stopB = (this.hexOf((this.linAt(la lb lc r 0 layer.gx1 layer.gy1)) (this.linAt(la lb lc r 1 layer.gx1 layer.gy1)) (this.linAt(la lb lc r 2 layer.gx1 layer.gy1))))
|
|
2544
|
+
}
|
|
2545
|
+
push layers layer
|
|
2546
|
+
def cmds:[PathCommand] (sub.getCommands())
|
|
2547
|
+
def ci:int 0
|
|
2548
|
+
while (ci < (array_length cmds)) {
|
|
2549
|
+
push allCmds (itemAt cmds ci)
|
|
2550
|
+
ci = (ci + 1)
|
|
2551
|
+
}
|
|
2552
|
+
pathData = layer.pathData
|
|
2553
|
+
rings = sub.rings
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
oi = (oi + 1)
|
|
2557
|
+
}
|
|
2558
|
+
commands = allCmds
|
|
2559
|
+
if ((array_length layers) > 0) {
|
|
2560
|
+
def first:EvgTraceLayer (itemAt layers 0)
|
|
2561
|
+
pathData = first.pathData
|
|
2562
|
+
}
|
|
2563
|
+
}
|
|
2564
|
+
|
|
2565
|
+
fn linAt:double (la:[double] lb:[double] lc:[double] r:int c:int x:double y:double) {
|
|
2566
|
+
def j:int ((r * 3) + c)
|
|
2567
|
+
return (((itemAt la j) + ((itemAt lb j) * x)) + ((itemAt lc j) * y))
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2570
|
+
; The region's own pixels, grown by one so the neighbour drawn on top of it
|
|
2571
|
+
; has something to cover rather than the page.
|
|
2572
|
+
fn maskForRegion:EvgBinaryBitmap (rid:int) {
|
|
2573
|
+
def bm:EvgBinaryBitmap (EvgBinaryBitmap.create(width height))
|
|
2574
|
+
def n:int (width * height)
|
|
2575
|
+
def i:int 0
|
|
2576
|
+
while (i < n) {
|
|
2577
|
+
if ((itemAt regionOf i) == rid) {
|
|
2578
|
+
def y:int (idiv i width)
|
|
2579
|
+
def x:int (i - (y * width))
|
|
2580
|
+
bm.setBit(x y true)
|
|
2581
|
+
if (x > 0) {
|
|
2582
|
+
bm.setBit((x - 1) y true)
|
|
2583
|
+
}
|
|
2584
|
+
if (x < (width - 1)) {
|
|
2585
|
+
bm.setBit((x + 1) y true)
|
|
2586
|
+
}
|
|
2587
|
+
if (y > 0) {
|
|
2588
|
+
bm.setBit(x (y - 1) true)
|
|
2589
|
+
}
|
|
2590
|
+
if (y < (height - 1)) {
|
|
2591
|
+
bm.setBit(x (y + 1) true)
|
|
2592
|
+
}
|
|
2593
|
+
}
|
|
2594
|
+
i = (i + 1)
|
|
2595
|
+
}
|
|
2596
|
+
return bm
|
|
2597
|
+
}
|
|
2598
|
+
|
|
2599
|
+
; ----------------------------------------------------------------------
|
|
2600
|
+
; Overlay shapes (contourMode "overlay")
|
|
2601
|
+
;
|
|
2602
|
+
; The partition's weakness is that one weak pixel on a boundary lets the
|
|
2603
|
+
; flood through, and the shape behind it is gone for good. Stacking removes
|
|
2604
|
+
; the consequence rather than the cause: the surface below keeps whatever it
|
|
2605
|
+
; took, and the shape it swallowed is drawn again on top of it.
|
|
2606
|
+
; ----------------------------------------------------------------------
|
|
2607
|
+
|
|
2608
|
+
; Is this pixel already covered by something it would not be told apart
|
|
2609
|
+
; from? Then an overlay has no reason to reach it.
|
|
2610
|
+
fn coveredAlready:boolean (i:int paintR:[int] paintG:[int] paintB:[int] painted:[int] tol:int) {
|
|
2611
|
+
if ((itemAt painted i) == 0) {
|
|
2612
|
+
return false
|
|
2613
|
+
}
|
|
2614
|
+
def d:int (EvgBitmapTracer.absI((itemAt planeR i) - (itemAt paintR i)))
|
|
2615
|
+
def dg:int (EvgBitmapTracer.absI((itemAt planeG i) - (itemAt paintG i)))
|
|
2616
|
+
if (dg > d) {
|
|
2617
|
+
d = dg
|
|
2618
|
+
}
|
|
2619
|
+
def db:int (EvgBitmapTracer.absI((itemAt planeB i) - (itemAt paintB i)))
|
|
2620
|
+
if (db > d) {
|
|
2621
|
+
d = db
|
|
2622
|
+
}
|
|
2623
|
+
return (d <= tol)
|
|
2624
|
+
}
|
|
2625
|
+
|
|
2626
|
+
; Fit one shape's pixel list with flat or linear, describe the
|
|
2627
|
+
; winner on the layer, and write what the model *predicts* into the paint
|
|
2628
|
+
; map. That last part is the whole reason the two features belong together:
|
|
2629
|
+
; the similarity stop asks "does an overlay show against what is under it",
|
|
2630
|
+
; and against a flat mean the answer is yes everywhere a ramp leaves the
|
|
2631
|
+
; mean — so every gradient shatters into bands. Against the model, it is
|
|
2632
|
+
; only yes where the model is actually wrong.
|
|
2633
|
+
fn fitAndPaintShape:void (comp:[int] layer:EvgTraceLayer paintR:[int] paintG:[int] paintB:[int]) {
|
|
2634
|
+
def n:int (array_length comp)
|
|
2635
|
+
if (n == 0) {
|
|
2636
|
+
return
|
|
2637
|
+
}
|
|
2638
|
+
def cnt:double (to_double n)
|
|
2639
|
+
def sx:double 0.0
|
|
2640
|
+
def sy:double 0.0
|
|
2641
|
+
def sxx:double 0.0
|
|
2642
|
+
def syy:double 0.0
|
|
2643
|
+
def sxy:double 0.0
|
|
2644
|
+
def sv:[double]
|
|
2645
|
+
def sxv:[double]
|
|
2646
|
+
def syv:[double]
|
|
2647
|
+
def svv:[double]
|
|
2648
|
+
def c:int 0
|
|
2649
|
+
while (c < 3) {
|
|
2650
|
+
push sv 0.0
|
|
2651
|
+
push sxv 0.0
|
|
2652
|
+
push syv 0.0
|
|
2653
|
+
push svv 0.0
|
|
2654
|
+
c = (c + 1)
|
|
2655
|
+
}
|
|
2656
|
+
def i:int 0
|
|
2657
|
+
while (i < n) {
|
|
2658
|
+
def idx:int (itemAt comp i)
|
|
2659
|
+
def yy:int (idiv idx width)
|
|
2660
|
+
def dx:double (to_double (idx - (yy * width)))
|
|
2661
|
+
def dy:double (to_double yy)
|
|
2662
|
+
sx = (sx + dx)
|
|
2663
|
+
sy = (sy + dy)
|
|
2664
|
+
sxx = (sxx + (dx * dx))
|
|
2665
|
+
syy = (syy + (dy * dy))
|
|
2666
|
+
sxy = (sxy + (dx * dy))
|
|
2667
|
+
c = 0
|
|
2668
|
+
while (c < 3) {
|
|
2669
|
+
def v:double (to_double (this.planeAt(c idx)))
|
|
2670
|
+
set sv c ((itemAt sv c) + v)
|
|
2671
|
+
set sxv c ((itemAt sxv c) + (dx * v))
|
|
2672
|
+
set syv c ((itemAt syv c) + (dy * v))
|
|
2673
|
+
set svv c ((itemAt svv c) + (v * v))
|
|
2674
|
+
c = (c + 1)
|
|
2675
|
+
}
|
|
2676
|
+
i = (i + 1)
|
|
2677
|
+
}
|
|
2678
|
+
def mx:double (sx / cnt)
|
|
2679
|
+
def my:double (sy / cnt)
|
|
2680
|
+
def la:[double]
|
|
2681
|
+
def lb:[double]
|
|
2682
|
+
def lc:[double]
|
|
2683
|
+
def eFlat:double 0.0
|
|
2684
|
+
def eLin:double 0.0
|
|
2685
|
+
def sol:[double]
|
|
2686
|
+
push sol 0.0
|
|
2687
|
+
push sol 0.0
|
|
2688
|
+
push sol 0.0
|
|
2689
|
+
push sol 0.0
|
|
2690
|
+
c = 0
|
|
2691
|
+
while (c < 3) {
|
|
2692
|
+
def s1:double (itemAt sv c)
|
|
2693
|
+
eFlat = (eFlat + ((itemAt svv c) - ((s1 * s1) / cnt)))
|
|
2694
|
+
EvgBitmapTracer.solveLinear(cnt sx sy sxx sxy syy s1 (itemAt sxv c) (itemAt syv c) sol)
|
|
2695
|
+
if ((itemAt sol 0) > 0.5) {
|
|
2696
|
+
push la (itemAt sol 1)
|
|
2697
|
+
push lb (itemAt sol 2)
|
|
2698
|
+
push lc (itemAt sol 3)
|
|
2699
|
+
eLin = (eLin + ((itemAt svv c) - ((((itemAt sol 1) * s1) + ((itemAt sol 2) * (itemAt sxv c))) + ((itemAt sol 3) * (itemAt syv c)))))
|
|
2700
|
+
} {
|
|
2701
|
+
push la (s1 / cnt)
|
|
2702
|
+
push lb 0.0
|
|
2703
|
+
push lc 0.0
|
|
2704
|
+
eLin = (eLin + 1000000000.0)
|
|
2705
|
+
}
|
|
2706
|
+
c = (c + 1)
|
|
2707
|
+
}
|
|
2708
|
+
if (eFlat < 0.0) {
|
|
2709
|
+
eFlat = 0.0
|
|
2710
|
+
}
|
|
2711
|
+
if ((eLin < 0.0) || (cnt < 64.0)) {
|
|
2712
|
+
eLin = 1000000000.0
|
|
2713
|
+
}
|
|
2714
|
+
def gain:double ((to_double options.gradientGain) / 100.0)
|
|
2715
|
+
def best:double eFlat
|
|
2716
|
+
def useLin:boolean false
|
|
2717
|
+
if (eLin < best) {
|
|
2718
|
+
best = eLin
|
|
2719
|
+
useLin = true
|
|
2720
|
+
}
|
|
2721
|
+
if ((eFlat - best) < (eFlat * gain)) {
|
|
2722
|
+
useLin = false
|
|
2723
|
+
}
|
|
2724
|
+
if (eFlat < (cnt * 3.0)) {
|
|
2725
|
+
useLin = false
|
|
2726
|
+
}
|
|
2727
|
+
def mR:int (EvgBitmapTracer.clamp255((itemAt sv 0) / cnt))
|
|
2728
|
+
def mG:int (EvgBitmapTracer.clamp255((itemAt sv 1) / cnt))
|
|
2729
|
+
def mB:int (EvgBitmapTracer.clamp255((itemAt sv 2) / cnt))
|
|
2730
|
+
layer.fillHex = (this.hexFromRgb(mR mG mB))
|
|
2731
|
+
if (useLin == false) {
|
|
2732
|
+
layer.fillKind = "flat"
|
|
2733
|
+
i = 0
|
|
2734
|
+
while (i < n) {
|
|
2735
|
+
def p0:int (itemAt comp i)
|
|
2736
|
+
set paintR p0 mR
|
|
2737
|
+
set paintG p0 mG
|
|
2738
|
+
set paintB p0 mB
|
|
2739
|
+
i = (i + 1)
|
|
2740
|
+
}
|
|
2741
|
+
return
|
|
2742
|
+
}
|
|
2743
|
+
; The axis is the direction luma runs along; the extent comes from the
|
|
2744
|
+
; shape's own pixels projected onto it.
|
|
2745
|
+
def bl:double (((0.299 * (itemAt lb 0)) + (0.587 * (itemAt lb 1))) + (0.114 * (itemAt lb 2)))
|
|
2746
|
+
def cl:double (((0.299 * (itemAt lc 0)) + (0.587 * (itemAt lc 1))) + (0.114 * (itemAt lc 2)))
|
|
2747
|
+
def mag:double (sqrt ((bl * bl) + (cl * cl)))
|
|
2748
|
+
def ux:double 1.0
|
|
2749
|
+
def uy:double 0.0
|
|
2750
|
+
if (mag > 0.000001) {
|
|
2751
|
+
ux = (bl / mag)
|
|
2752
|
+
uy = (cl / mag)
|
|
2753
|
+
}
|
|
2754
|
+
def tmin:double 1000000.0
|
|
2755
|
+
def tmax:double (0.0 - 1000000.0)
|
|
2756
|
+
i = 0
|
|
2757
|
+
while (i < n) {
|
|
2758
|
+
def p1:int (itemAt comp i)
|
|
2759
|
+
def y1:int (idiv p1 width)
|
|
2760
|
+
def t:double ((((to_double (p1 - (y1 * width))) - mx) * ux) + (((to_double y1) - my) * uy))
|
|
2761
|
+
if (t < tmin) {
|
|
2762
|
+
tmin = t
|
|
2763
|
+
}
|
|
2764
|
+
if (t > tmax) {
|
|
2765
|
+
tmax = t
|
|
2766
|
+
}
|
|
2767
|
+
; Paint the prediction, clamped, so the similarity test below sees
|
|
2768
|
+
; the ramp this shape actually describes.
|
|
2769
|
+
set paintR p1 (EvgBitmapTracer.clamp255((this.linAt(la lb lc 0 0 (to_double (p1 - (y1 * width))) (to_double y1)))))
|
|
2770
|
+
set paintG p1 (EvgBitmapTracer.clamp255((this.linAt(la lb lc 0 1 (to_double (p1 - (y1 * width))) (to_double y1)))))
|
|
2771
|
+
set paintB p1 (EvgBitmapTracer.clamp255((this.linAt(la lb lc 0 2 (to_double (p1 - (y1 * width))) (to_double y1)))))
|
|
2772
|
+
i = (i + 1)
|
|
2773
|
+
}
|
|
2774
|
+
if ((tmax - tmin) < 2.0) {
|
|
2775
|
+
layer.fillKind = "flat"
|
|
2776
|
+
return
|
|
2777
|
+
}
|
|
2778
|
+
layer.fillKind = "linear"
|
|
2779
|
+
layer.gx0 = (mx + (ux * tmin))
|
|
2780
|
+
layer.gy0 = (my + (uy * tmin))
|
|
2781
|
+
layer.gx1 = (mx + (ux * tmax))
|
|
2782
|
+
layer.gy1 = (my + (uy * tmax))
|
|
2783
|
+
layer.stopA = (this.hexOf((this.linAt(la lb lc 0 0 layer.gx0 layer.gy0)) (this.linAt(la lb lc 0 1 layer.gx0 layer.gy0)) (this.linAt(la lb lc 0 2 layer.gx0 layer.gy0))))
|
|
2784
|
+
layer.stopB = (this.hexOf((this.linAt(la lb lc 0 0 layer.gx1 layer.gy1)) (this.linAt(la lb lc 0 1 layer.gx1 layer.gy1)) (this.linAt(la lb lc 0 2 layer.gx1 layer.gy1))))
|
|
2785
|
+
}
|
|
2786
|
+
|
|
2787
|
+
; Re-quantize each detail neighbourhood using only its own pixels, and draw
|
|
2788
|
+
; the result on top of the picture the global palette produced.
|
|
2789
|
+
;
|
|
2790
|
+
; This answers the complaint that an eye traced on its own comes out
|
|
2791
|
+
; beautifully while the same eye inside a whole face comes out a smudge.
|
|
2792
|
+
; Nothing is wrong with the tracer in either case: zoomed in, the entire
|
|
2793
|
+
; color budget is spent on an eye; in a portrait the eye gets two swatches
|
|
2794
|
+
; out of ten, because the quantizer counts the whole frame and a face is
|
|
2795
|
+
; mostly cheek. What survives that is the eye's outline; what is lost is the
|
|
2796
|
+
; lid, the lash line and the opening — everything that makes it read as an
|
|
2797
|
+
; eye rather than a dark almond. Giving the neighbourhood its own palette is
|
|
2798
|
+
; what "zoom in on it" means, done in place, and the shapes come out the way
|
|
2799
|
+
; the plain algorithm makes shapes — whole connected regions of one color —
|
|
2800
|
+
; rather than grown on a leash and fragmented.
|
|
2801
|
+
fn traceDetailShapes:void (allCmds:[PathCommand] minPx:int) {
|
|
2802
|
+
def k:int options.detailColors
|
|
2803
|
+
if (k < 2) {
|
|
2804
|
+
return
|
|
2805
|
+
}
|
|
2806
|
+
def n:int (width * height)
|
|
2807
|
+
if ((array_length detailMask) != n) {
|
|
2808
|
+
return
|
|
2809
|
+
}
|
|
2810
|
+
def seen:[int]
|
|
2811
|
+
def i:int 0
|
|
2812
|
+
while (i < n) {
|
|
2813
|
+
push seen 0
|
|
2814
|
+
i = (i + 1)
|
|
2815
|
+
}
|
|
2816
|
+
def needPx:int minPx
|
|
2817
|
+
if ((options.detailBoost) >= 2) {
|
|
2818
|
+
needPx = (idiv minPx (options.detailBoost))
|
|
2819
|
+
}
|
|
2820
|
+
if (needPx < 4) {
|
|
2821
|
+
needPx = 4
|
|
2822
|
+
}
|
|
2823
|
+
; One palette shared by every neighbourhood. Each still derives its own
|
|
2824
|
+
; bands from its own pixels — that is the whole feature — but derived in
|
|
2825
|
+
; ignorance of one another, forty neighbourhoods invent forty nearly
|
|
2826
|
+
; identical browns, and a portrait came back with 456 fills for 561
|
|
2827
|
+
; shapes. A band that lands close to a color already in use takes that
|
|
2828
|
+
; one instead of adding another.
|
|
2829
|
+
def shR:[int]
|
|
2830
|
+
def shG:[int]
|
|
2831
|
+
def shB:[int]
|
|
2832
|
+
def start:int 0
|
|
2833
|
+
while (start < n) {
|
|
2834
|
+
if (((itemAt detailMask start) == 1) && ((itemAt seen start) == 0)) {
|
|
2835
|
+
def comp:[int]
|
|
2836
|
+
def stack:[int]
|
|
2837
|
+
set seen start 1
|
|
2838
|
+
push stack start
|
|
2839
|
+
def head:int 0
|
|
2840
|
+
while (head < (array_length stack)) {
|
|
2841
|
+
def cur:int (itemAt stack head)
|
|
2842
|
+
head = (head + 1)
|
|
2843
|
+
push comp cur
|
|
2844
|
+
def cy:int (idiv cur width)
|
|
2845
|
+
def cx:int (cur - (cy * width))
|
|
2846
|
+
if (cx > 0) {
|
|
2847
|
+
this.detailVisit((cur - 1) seen stack)
|
|
2848
|
+
}
|
|
2849
|
+
if (cx < (width - 1)) {
|
|
2850
|
+
this.detailVisit((cur + 1) seen stack)
|
|
2851
|
+
}
|
|
2852
|
+
if (cy > 0) {
|
|
2853
|
+
this.detailVisit((cur - width) seen stack)
|
|
2854
|
+
}
|
|
2855
|
+
if (cy < (height - 1)) {
|
|
2856
|
+
this.detailVisit((cur + width) seen stack)
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
2859
|
+
; A neighbourhood has to be able to hold the pieces it is being
|
|
2860
|
+
; split into, or the split is noise.
|
|
2861
|
+
if ((array_length comp) >= (needPx * k)) {
|
|
2862
|
+
this.paintDetailCluster(comp k needPx allCmds shR shG shB)
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2865
|
+
start = (start + 1)
|
|
2866
|
+
}
|
|
2867
|
+
}
|
|
2868
|
+
|
|
2869
|
+
fn detailVisit:void (i:int seen:[int] stack:[int]) {
|
|
2870
|
+
if ((itemAt seen i) == 1) {
|
|
2871
|
+
return
|
|
2872
|
+
}
|
|
2873
|
+
if ((itemAt detailMask i) == 0) {
|
|
2874
|
+
return
|
|
2875
|
+
}
|
|
2876
|
+
set seen i 1
|
|
2877
|
+
push stack i
|
|
2878
|
+
}
|
|
2879
|
+
|
|
2880
|
+
; Find this color in the shared detail palette, or add it. Greedy and in
|
|
2881
|
+
; order, which is enough: the question is only whether two neighbourhoods
|
|
2882
|
+
; are describing the same brown.
|
|
2883
|
+
fn shareColor:int (r:int g:int b:int tol:int shR:[int] shG:[int] shB:[int]) {
|
|
2884
|
+
def n:int (array_length shR)
|
|
2885
|
+
; Nearest entry as well as near-enough entry: the first answers "is this
|
|
2886
|
+
; the same brown as one we have", the second answers "which of the ones
|
|
2887
|
+
; we have is this closest to", and the ceiling needs the second.
|
|
2888
|
+
def best:int (0 - 1)
|
|
2889
|
+
def bestD:int 1000
|
|
2890
|
+
def i:int 0
|
|
2891
|
+
while (i < n) {
|
|
2892
|
+
def dr:int (EvgBitmapTracer.absI(r - (itemAt shR i)))
|
|
2893
|
+
def dg:int (EvgBitmapTracer.absI(g - (itemAt shG i)))
|
|
2894
|
+
def db:int (EvgBitmapTracer.absI(b - (itemAt shB i)))
|
|
2895
|
+
def d:int dr
|
|
2896
|
+
if (dg > d) {
|
|
2897
|
+
d = dg
|
|
2898
|
+
}
|
|
2899
|
+
if (db > d) {
|
|
2900
|
+
d = db
|
|
2901
|
+
}
|
|
2902
|
+
if (d < bestD) {
|
|
2903
|
+
bestD = d
|
|
2904
|
+
best = i
|
|
2905
|
+
}
|
|
2906
|
+
i = (i + 1)
|
|
2907
|
+
}
|
|
2908
|
+
if ((best >= 0) && (bestD <= tol)) {
|
|
2909
|
+
return best
|
|
2910
|
+
}
|
|
2911
|
+
; Merging near-duplicates bounds nothing on its own — colors that really
|
|
2912
|
+
; are different keep arriving, one per neighbourhood per band, and a busy
|
|
2913
|
+
; picture has hundreds of neighbourhoods. Past the ceiling a new band
|
|
2914
|
+
; takes the closest color already in hand, however far that is, so the
|
|
2915
|
+
; count the user asked for stays the count they roughly get.
|
|
2916
|
+
def cap:int options.detailColorMax
|
|
2917
|
+
if ((cap > 0) && (n >= cap)) {
|
|
2918
|
+
if (best >= 0) {
|
|
2919
|
+
return best
|
|
2920
|
+
}
|
|
2921
|
+
}
|
|
2922
|
+
push shR r
|
|
2923
|
+
push shG g
|
|
2924
|
+
push shB b
|
|
2925
|
+
return n
|
|
2926
|
+
}
|
|
2927
|
+
|
|
2928
|
+
; One neighbourhood: build a palette out of its own pixels, split it by that
|
|
2929
|
+
; palette, and emit each piece as a layer.
|
|
2930
|
+
fn paintDetailCluster:void (comp:[int] k:int needPx:int allCmds:[PathCommand] shR:[int] shG:[int] shB:[int]) {
|
|
2931
|
+
; The local palette comes from equal shares of the neighbourhood's own
|
|
2932
|
+
; luma range. Bands rather than a k-means because what has to be
|
|
2933
|
+
; separated here is exactly light from dark — the white of an eye from
|
|
2934
|
+
; its pupil — and a band split does that by construction, cheaply, and
|
|
2935
|
+
; with the same answer every run.
|
|
2936
|
+
def binN:[int]
|
|
2937
|
+
def binR:[int]
|
|
2938
|
+
def binG:[int]
|
|
2939
|
+
def binB:[int]
|
|
2940
|
+
def i:int 0
|
|
2941
|
+
while (i < 256) {
|
|
2942
|
+
push binN 0
|
|
2943
|
+
push binR 0
|
|
2944
|
+
push binG 0
|
|
2945
|
+
push binB 0
|
|
2946
|
+
i = (i + 1)
|
|
2947
|
+
}
|
|
2948
|
+
def c:int 0
|
|
2949
|
+
while (c < (array_length comp)) {
|
|
2950
|
+
def px:int (itemAt comp c)
|
|
2951
|
+
def r:int (itemAt planeR px)
|
|
2952
|
+
def g:int (itemAt planeG px)
|
|
2953
|
+
def b:int (itemAt planeB px)
|
|
2954
|
+
def l:int (EvgBitmapTracer.lumaOf(r g b))
|
|
2955
|
+
set binN l ((itemAt binN l) + 1)
|
|
2956
|
+
set binR l ((itemAt binR l) + r)
|
|
2957
|
+
set binG l ((itemAt binG l) + g)
|
|
2958
|
+
set binB l ((itemAt binB l) + b)
|
|
2959
|
+
c = (c + 1)
|
|
2960
|
+
}
|
|
2961
|
+
def total:int (array_length comp)
|
|
2962
|
+
def lpR:[int]
|
|
2963
|
+
def lpG:[int]
|
|
2964
|
+
def lpB:[int]
|
|
2965
|
+
def acc:int 0
|
|
2966
|
+
def sR:int 0
|
|
2967
|
+
def sG:int 0
|
|
2968
|
+
def sB:int 0
|
|
2969
|
+
def sN:int 0
|
|
2970
|
+
def band:int 1
|
|
2971
|
+
i = 0
|
|
2972
|
+
while (i < 256) {
|
|
2973
|
+
def cnt:int (itemAt binN i)
|
|
2974
|
+
if (cnt > 0) {
|
|
2975
|
+
sR = (sR + (itemAt binR i))
|
|
2976
|
+
sG = (sG + (itemAt binG i))
|
|
2977
|
+
sB = (sB + (itemAt binB i))
|
|
2978
|
+
sN = (sN + cnt)
|
|
2979
|
+
acc = (acc + cnt)
|
|
2980
|
+
}
|
|
2981
|
+
def edge:int (idiv (total * band) k)
|
|
2982
|
+
if ((acc >= edge) && (band < k)) {
|
|
2983
|
+
if (sN > 0) {
|
|
2984
|
+
push lpR (idiv sR sN)
|
|
2985
|
+
push lpG (idiv sG sN)
|
|
2986
|
+
push lpB (idiv sB sN)
|
|
2987
|
+
}
|
|
2988
|
+
sR = 0
|
|
2989
|
+
sG = 0
|
|
2990
|
+
sB = 0
|
|
2991
|
+
sN = 0
|
|
2992
|
+
band = (band + 1)
|
|
2993
|
+
}
|
|
2994
|
+
i = (i + 1)
|
|
2995
|
+
}
|
|
2996
|
+
if (sN > 0) {
|
|
2997
|
+
push lpR (idiv sR sN)
|
|
2998
|
+
push lpG (idiv sG sN)
|
|
2999
|
+
push lpB (idiv sB sN)
|
|
3000
|
+
}
|
|
3001
|
+
def lk:int (array_length lpR)
|
|
3002
|
+
if (lk < 2) {
|
|
3003
|
+
return
|
|
3004
|
+
}
|
|
3005
|
+
def merge:int options.detailColorMerge
|
|
3006
|
+
if (merge > 0) {
|
|
3007
|
+
def bi:int 0
|
|
3008
|
+
while (bi < lk) {
|
|
3009
|
+
def want:int (this.shareColor((itemAt lpR bi) (itemAt lpG bi) (itemAt lpB bi) merge shR shG shB))
|
|
3010
|
+
set lpR bi (itemAt shR want)
|
|
3011
|
+
set lpG bi (itemAt shG want)
|
|
3012
|
+
set lpB bi (itemAt shB want)
|
|
3013
|
+
bi = (bi + 1)
|
|
3014
|
+
}
|
|
3015
|
+
}
|
|
3016
|
+
; Speckle here is not whole bands that are too small — with equal-share
|
|
3017
|
+
; bands none ever is, each holds about 1/k of the neighbourhood by
|
|
3018
|
+
; construction, and gating on that was measured to do nothing until it
|
|
3019
|
+
; was high enough to delete real structure. It is the scatter *inside* a
|
|
3020
|
+
; band: a band traces to one big ring plus a spray of little ones. So
|
|
3021
|
+
; the share is spent where it bites, as the smallest ring a detail shape
|
|
3022
|
+
; may keep.
|
|
3023
|
+
def speck:int (idiv (total * (options.detailMinShare)) 100)
|
|
3024
|
+
def bx0:int width
|
|
3025
|
+
def by0:int height
|
|
3026
|
+
def bx1:int 0
|
|
3027
|
+
def by1:int 0
|
|
3028
|
+
c = 0
|
|
3029
|
+
while (c < (array_length comp)) {
|
|
3030
|
+
def q:int (itemAt comp c)
|
|
3031
|
+
def qy:int (idiv q width)
|
|
3032
|
+
def qx:int (q - (qy * width))
|
|
3033
|
+
if (qx < bx0) {
|
|
3034
|
+
bx0 = qx
|
|
3035
|
+
}
|
|
3036
|
+
if (qx > bx1) {
|
|
3037
|
+
bx1 = qx
|
|
3038
|
+
}
|
|
3039
|
+
if (qy < by0) {
|
|
3040
|
+
by0 = qy
|
|
3041
|
+
}
|
|
3042
|
+
if (qy > by1) {
|
|
3043
|
+
by1 = qy
|
|
3044
|
+
}
|
|
3045
|
+
c = (c + 1)
|
|
3046
|
+
}
|
|
3047
|
+
bx0 = (bx0 - 1)
|
|
3048
|
+
by0 = (by0 - 1)
|
|
3049
|
+
def bw:int ((bx1 - bx0) + 2)
|
|
3050
|
+
def bh:int ((by1 - by0) + 2)
|
|
3051
|
+
def li:int 0
|
|
3052
|
+
while (li < lk) {
|
|
3053
|
+
def mask:EvgBinaryBitmap (EvgBinaryBitmap.create(bw bh))
|
|
3054
|
+
def held:int 0
|
|
3055
|
+
c = 0
|
|
3056
|
+
while (c < (array_length comp)) {
|
|
3057
|
+
def p2:int (itemAt comp c)
|
|
3058
|
+
def py:int (idiv p2 width)
|
|
3059
|
+
def pxx:int (p2 - (py * width))
|
|
3060
|
+
def near:int (this.nearestIndex((itemAt planeR p2) (itemAt planeG p2) (itemAt planeB p2) lpR lpG lpB))
|
|
3061
|
+
if (near == li) {
|
|
3062
|
+
mask.setBit((pxx - bx0) (py - by0) true)
|
|
3063
|
+
held = (held + 1)
|
|
3064
|
+
}
|
|
3065
|
+
c = (c + 1)
|
|
3066
|
+
}
|
|
3067
|
+
if (held >= needPx) {
|
|
3068
|
+
def layerOpts:EvgTraceOptions (EvgTraceOptions.defaults())
|
|
3069
|
+
layerOpts.turdsize = options.turdsize
|
|
3070
|
+
if (speck > (options.turdsize)) {
|
|
3071
|
+
layerOpts.turdsize = speck
|
|
3072
|
+
}
|
|
3073
|
+
layerOpts.alphamax = options.alphamax
|
|
3074
|
+
layerOpts.turnpolicy = options.turnpolicy
|
|
3075
|
+
layerOpts.optcurve = options.optcurve
|
|
3076
|
+
layerOpts.opttolerance = options.opttolerance
|
|
3077
|
+
layerOpts.fillHex = (this.hexFromRgb((itemAt lpR li) (itemAt lpG li) (itemAt lpB li)))
|
|
3078
|
+
layerOpts.pathFormat = options.pathFormat
|
|
3079
|
+
layerOpts.pathPrecision = options.pathPrecision
|
|
3080
|
+
def sub:EvgBitmapTracer (EvgBitmapTracer.fromBinary(mask layerOpts))
|
|
3081
|
+
sub.trace()
|
|
3082
|
+
if ((sub.ringCount()) > 0) {
|
|
3083
|
+
def cmds:[PathCommand] (sub.getCommands())
|
|
3084
|
+
def ox:double (to_double bx0)
|
|
3085
|
+
def oy:double (to_double by0)
|
|
3086
|
+
def ci:int 0
|
|
3087
|
+
while (ci < (array_length cmds)) {
|
|
3088
|
+
def cm:PathCommand (itemAt cmds ci)
|
|
3089
|
+
cm.x = (cm.x + ox)
|
|
3090
|
+
cm.y = (cm.y + oy)
|
|
3091
|
+
cm.x1 = (cm.x1 + ox)
|
|
3092
|
+
cm.y1 = (cm.y1 + oy)
|
|
3093
|
+
cm.x2 = (cm.x2 + ox)
|
|
3094
|
+
cm.y2 = (cm.y2 + oy)
|
|
3095
|
+
push allCmds cm
|
|
3096
|
+
ci = (ci + 1)
|
|
3097
|
+
}
|
|
3098
|
+
def layer:EvgTraceLayer (itemAt sub.layers 0)
|
|
3099
|
+
layer.pathData = (this.pathDataOf(cmds))
|
|
3100
|
+
push layers layer
|
|
3101
|
+
rings = sub.rings
|
|
3102
|
+
}
|
|
3103
|
+
}
|
|
3104
|
+
li = (li + 1)
|
|
3105
|
+
}
|
|
3106
|
+
}
|
|
3107
|
+
|
|
3108
|
+
fn traceOverlayShapes:void (palR:[int] palG:[int] palB:[int]) {
|
|
3109
|
+
this.buildEdgeMask()
|
|
3110
|
+
this.buildDetailMask()
|
|
3111
|
+
def n:int (width * height)
|
|
3112
|
+
def edgeTol:int options.contourEdge
|
|
3113
|
+
if (edgeTol < 1) {
|
|
3114
|
+
edgeTol = 1
|
|
3115
|
+
}
|
|
3116
|
+
def spread:int options.contourSpread
|
|
3117
|
+
if (spread < 1) {
|
|
3118
|
+
spread = 1
|
|
3119
|
+
}
|
|
3120
|
+
def simTol:int options.overlaySimilar
|
|
3121
|
+
def minPx:int options.minRegion
|
|
3122
|
+
if (minPx < 4) {
|
|
3123
|
+
minPx = 4
|
|
3124
|
+
}
|
|
3125
|
+
def paintR:[int]
|
|
3126
|
+
def paintG:[int]
|
|
3127
|
+
def paintB:[int]
|
|
3128
|
+
def painted:[int]
|
|
3129
|
+
def stamp:[int]
|
|
3130
|
+
; The paint map starts as the base picture, because that is what is on
|
|
3131
|
+
; screen before a single overlay is drawn. It was started blank, which
|
|
3132
|
+
; made both of the stop rules lie: "would this overlay show against what
|
|
3133
|
+
; is under it" was asked against nothing, so every overlay showed and
|
|
3134
|
+
; every overlay was drawn. Answered against the real picture, the rule
|
|
3135
|
+
; means what it says — an overlay exists only where it differs from the
|
|
3136
|
+
; picture it sits on — and the shapes that merely repeat it fall out.
|
|
3137
|
+
def i:int 0
|
|
3138
|
+
while (i < n) {
|
|
3139
|
+
def lab:int (itemAt labels i)
|
|
3140
|
+
if ((lab >= 0) && (lab < (array_length palR))) {
|
|
3141
|
+
push paintR (itemAt palR lab)
|
|
3142
|
+
push paintG (itemAt palG lab)
|
|
3143
|
+
push paintB (itemAt palB lab)
|
|
3144
|
+
push painted 1
|
|
3145
|
+
} {
|
|
3146
|
+
push paintR 0
|
|
3147
|
+
push paintG 0
|
|
3148
|
+
push paintB 0
|
|
3149
|
+
push painted 0
|
|
3150
|
+
}
|
|
3151
|
+
push stamp (0 - 1)
|
|
3152
|
+
i = (i + 1)
|
|
3153
|
+
}
|
|
3154
|
+
; Seeds are held in 256 buckets by the strength of the edge that
|
|
3155
|
+
; produced them, and the steepest is always served first. Order is not
|
|
3156
|
+
; a detail here: a smooth surface started first spreads across weak
|
|
3157
|
+
; boundaries and claims what lies behind them, and the shape it took can
|
|
3158
|
+
; then only come back as whatever part of itself still differs — half a
|
|
3159
|
+
; rectangle, with the other half sitting inside the surface that ate it.
|
|
3160
|
+
; Started from the sharpest edge, the shapes with an identity are laid
|
|
3161
|
+
; down before the smooth ones get to move.
|
|
3162
|
+
def bucket:[int]
|
|
3163
|
+
def seedPixel:[int]
|
|
3164
|
+
def seedNext:[int]
|
|
3165
|
+
def seedBest:[int]
|
|
3166
|
+
i = 0
|
|
3167
|
+
while (i < 256) {
|
|
3168
|
+
push bucket (0 - 1)
|
|
3169
|
+
i = (i + 1)
|
|
3170
|
+
}
|
|
3171
|
+
i = 0
|
|
3172
|
+
while (i < n) {
|
|
3173
|
+
push seedBest (0 - 1)
|
|
3174
|
+
i = (i + 1)
|
|
3175
|
+
}
|
|
3176
|
+
; Every steep pixel goes in up front, not just the steepest one. A
|
|
3177
|
+
; figure can be star-shaped, or in several pieces, and growing outward
|
|
3178
|
+
; from one point does not necessarily reach the sharp corners of the
|
|
3179
|
+
; others. Queued together and served steepest-first, the question
|
|
3180
|
+
; "have all the steep points been dealt with" answers itself: the queue
|
|
3181
|
+
; is empty when they have.
|
|
3182
|
+
def y0:int 0
|
|
3183
|
+
while (y0 < height) {
|
|
3184
|
+
def x0:int 0
|
|
3185
|
+
while (x0 < width) {
|
|
3186
|
+
def idx0:int ((y0 * width) + x0)
|
|
3187
|
+
; Only a step that passed the uniqueness and continuity tests
|
|
3188
|
+
; earns a seed. Seeding every raw step means seeding every
|
|
3189
|
+
; speck, and the shape budget goes to noise.
|
|
3190
|
+
if ((itemAt labels idx0) >= 0) {
|
|
3191
|
+
def st:int 0
|
|
3192
|
+
if (x0 > 0) {
|
|
3193
|
+
if (this.isBoundary(idx0 (idx0 - 1) edgeTol)) {
|
|
3194
|
+
st = (this.pixelDelta(idx0 (idx0 - 1)))
|
|
3195
|
+
}
|
|
3196
|
+
}
|
|
3197
|
+
if (x0 < (width - 1)) {
|
|
3198
|
+
if (this.isBoundary(idx0 (idx0 + 1) edgeTol)) {
|
|
3199
|
+
def s1:int (this.pixelDelta(idx0 (idx0 + 1)))
|
|
3200
|
+
if (s1 > st) {
|
|
3201
|
+
st = s1
|
|
3202
|
+
}
|
|
3203
|
+
}
|
|
3204
|
+
}
|
|
3205
|
+
if (y0 > 0) {
|
|
3206
|
+
if (this.isBoundary(idx0 (idx0 - width) edgeTol)) {
|
|
3207
|
+
def s2:int (this.pixelDelta(idx0 (idx0 - width)))
|
|
3208
|
+
if (s2 > st) {
|
|
3209
|
+
st = s2
|
|
3210
|
+
}
|
|
3211
|
+
}
|
|
3212
|
+
}
|
|
3213
|
+
if (y0 < (height - 1)) {
|
|
3214
|
+
if (this.isBoundary(idx0 (idx0 + width) edgeTol)) {
|
|
3215
|
+
def s3:int (this.pixelDelta(idx0 (idx0 + width)))
|
|
3216
|
+
if (s3 > st) {
|
|
3217
|
+
st = s3
|
|
3218
|
+
}
|
|
3219
|
+
}
|
|
3220
|
+
}
|
|
3221
|
+
if (st > 0) {
|
|
3222
|
+
this.pushSeed(idx0 st bucket seedPixel seedNext seedBest)
|
|
3223
|
+
}
|
|
3224
|
+
}
|
|
3225
|
+
x0 = (x0 + 1)
|
|
3226
|
+
}
|
|
3227
|
+
y0 = (y0 + 1)
|
|
3228
|
+
}
|
|
3229
|
+
def allCmds:[PathCommand]
|
|
3230
|
+
; The picture the quantizer just produced goes down first, and the
|
|
3231
|
+
; overlay shapes stack on top of it.
|
|
3232
|
+
;
|
|
3233
|
+
; What used to be here was a single flat rectangle in the average color
|
|
3234
|
+
; of the frame, on the theory that the shapes above would cover it. They
|
|
3235
|
+
; do not: on a portrait the base was a dark brown sheet, the shapes on
|
|
3236
|
+
; top reached the face only as slivers, and the face read as that brown
|
|
3237
|
+
; — with its own skin shapes sitting in the file underneath, drawn and
|
|
3238
|
+
; invisible. Measured on the cheek pixel: layer 0 painted it #6C2D21 and
|
|
3239
|
+
; of the 1467 shapes above it exactly one touched it at all.
|
|
3240
|
+
;
|
|
3241
|
+
; Standing on the real picture makes the floor of this mode the ordinary
|
|
3242
|
+
; result rather than a flat sheet. An overlay that adds nothing now
|
|
3243
|
+
; costs nothing, and one that is wrong can only be as wrong as the
|
|
3244
|
+
; shape it draws — never the whole frame.
|
|
3245
|
+
this.paintLabelLayers(palR palG palB allCmds)
|
|
3246
|
+
; Then the neighbourhoods that need a palette of their own, before the
|
|
3247
|
+
; grown overlays — those refine the picture, these re-read it.
|
|
3248
|
+
this.traceDetailShapes(allCmds minPx)
|
|
3249
|
+
; How many shapes are worth making scales with the picture: a fixed
|
|
3250
|
+
; ceiling starves a large photograph of exactly the shapes it needs,
|
|
3251
|
+
; and the flag in one test came out as bare background because its
|
|
3252
|
+
; stripes were never reached.
|
|
3253
|
+
def budget:int (idiv n 4)
|
|
3254
|
+
if (budget < 500) {
|
|
3255
|
+
budget = 500
|
|
3256
|
+
}
|
|
3257
|
+
if (budget > 80000) {
|
|
3258
|
+
budget = 80000
|
|
3259
|
+
}
|
|
3260
|
+
def shapeNo:int 0
|
|
3261
|
+
def sweepFrom:int 0
|
|
3262
|
+
while (shapeNo < budget) {
|
|
3263
|
+
; Take the next seed; when they run out, sweep for anything still
|
|
3264
|
+
; unpainted so nothing is left as a hole.
|
|
3265
|
+
def seed:int (0 - 1)
|
|
3266
|
+
def empty:boolean false
|
|
3267
|
+
; Steepest-first alone starves the large smooth areas: a photograph
|
|
3268
|
+
; has thousands of sharp spots, they take the whole shape budget as
|
|
3269
|
+
; small overlays, and a face — which is nearly all low-contrast —
|
|
3270
|
+
; never reaches the front of the queue at all. Every fourth shape
|
|
3271
|
+
; is therefore taken from the plain sweep instead, so the big quiet
|
|
3272
|
+
; surfaces are laid down alongside the loud little ones.
|
|
3273
|
+
if (((shapeNo % 4) == 3) && (sweepFrom < n)) {
|
|
3274
|
+
while ((sweepFrom < n) && (seed < 0)) {
|
|
3275
|
+
if (((itemAt labels sweepFrom) >= 0) && ((itemAt painted sweepFrom) == 0)) {
|
|
3276
|
+
seed = sweepFrom
|
|
3277
|
+
}
|
|
3278
|
+
sweepFrom = (sweepFrom + 1)
|
|
3279
|
+
}
|
|
3280
|
+
}
|
|
3281
|
+
while ((seed < 0) && (empty == false)) {
|
|
3282
|
+
def cand:int (this.popSeed(bucket seedPixel seedNext))
|
|
3283
|
+
if (cand < 0) {
|
|
3284
|
+
empty = true
|
|
3285
|
+
} {
|
|
3286
|
+
if ((itemAt labels cand) >= 0) {
|
|
3287
|
+
def candSim:int simTol
|
|
3288
|
+
if (this.isDetail(cand)) {
|
|
3289
|
+
candSim = (idiv simTol (options.detailBoost))
|
|
3290
|
+
}
|
|
3291
|
+
if ((this.coveredAlready(cand paintR paintG paintB painted candSim)) == false) {
|
|
3292
|
+
seed = cand
|
|
3293
|
+
}
|
|
3294
|
+
}
|
|
3295
|
+
}
|
|
3296
|
+
}
|
|
3297
|
+
if (seed < 0) {
|
|
3298
|
+
while ((sweepFrom < n) && (seed < 0)) {
|
|
3299
|
+
if (((itemAt labels sweepFrom) >= 0) && ((itemAt painted sweepFrom) == 0)) {
|
|
3300
|
+
seed = sweepFrom
|
|
3301
|
+
}
|
|
3302
|
+
sweepFrom = (sweepFrom + 1)
|
|
3303
|
+
}
|
|
3304
|
+
}
|
|
3305
|
+
if (seed < 0) {
|
|
3306
|
+
shapeNo = budget
|
|
3307
|
+
} {
|
|
3308
|
+
; --- grow one surface to its end ---
|
|
3309
|
+
def comp:[int]
|
|
3310
|
+
def stack:[int]
|
|
3311
|
+
def acc:[int]
|
|
3312
|
+
push acc (itemAt planeR seed)
|
|
3313
|
+
push acc (itemAt planeG seed)
|
|
3314
|
+
push acc (itemAt planeB seed)
|
|
3315
|
+
push acc 1
|
|
3316
|
+
set stamp seed shapeNo
|
|
3317
|
+
push stack seed
|
|
3318
|
+
def h2:int 0
|
|
3319
|
+
while (h2 < (array_length stack)) {
|
|
3320
|
+
def cur:int (itemAt stack h2)
|
|
3321
|
+
h2 = (h2 + 1)
|
|
3322
|
+
push comp cur
|
|
3323
|
+
def cy:int (idiv cur width)
|
|
3324
|
+
def cx:int (cur - (cy * width))
|
|
3325
|
+
if (cx > 0) {
|
|
3326
|
+
this.overlayStep((cur - 1) cur stamp stack acc bucket seedPixel seedNext seedBest shapeNo edgeTol spread simTol paintR paintG paintB painted)
|
|
3327
|
+
}
|
|
3328
|
+
if (cx < (width - 1)) {
|
|
3329
|
+
this.overlayStep((cur + 1) cur stamp stack acc bucket seedPixel seedNext seedBest shapeNo edgeTol spread simTol paintR paintG paintB painted)
|
|
3330
|
+
}
|
|
3331
|
+
if (cy > 0) {
|
|
3332
|
+
this.overlayStep((cur - width) cur stamp stack acc bucket seedPixel seedNext seedBest shapeNo edgeTol spread simTol paintR paintG paintB painted)
|
|
3333
|
+
}
|
|
3334
|
+
if (cy < (height - 1)) {
|
|
3335
|
+
this.overlayStep((cur + width) cur stamp stack acc bucket seedPixel seedNext seedBest shapeNo edgeTol spread simTol paintR paintG paintB painted)
|
|
3336
|
+
}
|
|
3337
|
+
}
|
|
3338
|
+
def cnt:int (itemAt acc 3)
|
|
3339
|
+
def mr:int (idiv (itemAt acc 0) cnt)
|
|
3340
|
+
def mg:int (idiv (itemAt acc 1) cnt)
|
|
3341
|
+
def mb:int (idiv (itemAt acc 2) cnt)
|
|
3342
|
+
; Every shape used to be painted its own mean, which meant the
|
|
3343
|
+
; color count did nothing at all in this mode: a photograph
|
|
3344
|
+
; decomposes into thousands of surfaces, each one got a color of
|
|
3345
|
+
; its own, and the palette strip came back with five thousand
|
|
3346
|
+
; swatches under a slider that said four. The mean is snapped
|
|
3347
|
+
; onto the palette the quantizer already built, so "four colors"
|
|
3348
|
+
; means four colors here too — and a shape that lands on the
|
|
3349
|
+
; same swatch as the surface under it is then exactly, not
|
|
3350
|
+
; approximately, the same color, which is what lets it be
|
|
3351
|
+
; dropped rather than drawn.
|
|
3352
|
+
def pi:int (this.nearestIndex(mr mg mb palR palG palB))
|
|
3353
|
+
def keepTrue:boolean false
|
|
3354
|
+
if (options.detailTrueColor) {
|
|
3355
|
+
if (this.isDetail(seed)) {
|
|
3356
|
+
keepTrue = true
|
|
3357
|
+
}
|
|
3358
|
+
}
|
|
3359
|
+
; Giving the shape its *region's* swatch instead was tried and
|
|
3360
|
+
; is provably useless: the base picture already paints that
|
|
3361
|
+
; swatch there, so every overlay would add nothing by
|
|
3362
|
+
; construction and the mode collapses to the plain result. A
|
|
3363
|
+
; shape earns its place by differing from what it sits on, so
|
|
3364
|
+
; the mean is what decides its color.
|
|
3365
|
+
if (keepTrue == false) {
|
|
3366
|
+
mr = (itemAt palR pi)
|
|
3367
|
+
mg = (itemAt palG pi)
|
|
3368
|
+
mb = (itemAt palB pi)
|
|
3369
|
+
}
|
|
3370
|
+
; Every shape marks its ground, whatever its size — a scrap too
|
|
3371
|
+
; small to be worth its own path still has to stop later shapes
|
|
3372
|
+
; from growing over it again.
|
|
3373
|
+
def sameBelow:int 0
|
|
3374
|
+
def c:int 0
|
|
3375
|
+
while (c < (array_length comp)) {
|
|
3376
|
+
def px:int (itemAt comp c)
|
|
3377
|
+
if ((itemAt painted px) == 1) {
|
|
3378
|
+
if (((itemAt paintR px) == mr) && ((itemAt paintG px) == mg)) {
|
|
3379
|
+
if ((itemAt paintB px) == mb) {
|
|
3380
|
+
sameBelow = (sameBelow + 1)
|
|
3381
|
+
}
|
|
3382
|
+
}
|
|
3383
|
+
}
|
|
3384
|
+
set paintR px mr
|
|
3385
|
+
set paintG px mg
|
|
3386
|
+
set paintB px mb
|
|
3387
|
+
set painted px 1
|
|
3388
|
+
c = (c + 1)
|
|
3389
|
+
}
|
|
3390
|
+
; Every shape that marks the map has to be drawn, or it is a
|
|
3391
|
+
; hole: the shapes below it have already stopped at its border
|
|
3392
|
+
; and nothing else will cover it. Only specks smaller than a
|
|
3393
|
+
; traceable ring are skipped — turdsize deals with those.
|
|
3394
|
+
; A shape every pixel of which already carries this exact
|
|
3395
|
+
; swatch is a path that would change no pixel on screen. It can
|
|
3396
|
+
; be dropped without leaving a hole, and the argument is by
|
|
3397
|
+
; induction: a color reaches the paint map only from a shape
|
|
3398
|
+
; that was drawn, because the first shape to cover a pixel
|
|
3399
|
+
; always finds it unpainted and so is never dropped here.
|
|
3400
|
+
def addsNothing:boolean (sameBelow == (array_length comp))
|
|
3401
|
+
; And a shape that sits in detail is allowed to be small. A
|
|
3402
|
+
; pupil is a handful of pixels; the area that is right for a
|
|
3403
|
+
; cheek discards it.
|
|
3404
|
+
def needPx:int minPx
|
|
3405
|
+
if (this.isDetail(seed)) {
|
|
3406
|
+
needPx = (idiv minPx (options.detailBoost))
|
|
3407
|
+
if (needPx < 4) {
|
|
3408
|
+
needPx = 4
|
|
3409
|
+
}
|
|
3410
|
+
}
|
|
3411
|
+
if (((array_length comp) >= needPx) && (addsNothing == false)) {
|
|
3412
|
+
; The mask covers the shape's own bounding box, not the
|
|
3413
|
+
; frame. A full-size bitmap per shape is allocated, copied
|
|
3414
|
+
; and scanned once each, so a photograph — which is
|
|
3415
|
+
; thousands of shapes — was paying the whole image area
|
|
3416
|
+
; thousands of times over. The path is moved back
|
|
3417
|
+
; afterwards.
|
|
3418
|
+
def bx0:int width
|
|
3419
|
+
def by0:int height
|
|
3420
|
+
def bx1:int 0
|
|
3421
|
+
def by1:int 0
|
|
3422
|
+
c = 0
|
|
3423
|
+
while (c < (array_length comp)) {
|
|
3424
|
+
def q:int (itemAt comp c)
|
|
3425
|
+
def qy:int (idiv q width)
|
|
3426
|
+
def qx:int (q - (qy * width))
|
|
3427
|
+
if (qx < bx0) {
|
|
3428
|
+
bx0 = qx
|
|
3429
|
+
}
|
|
3430
|
+
if (qx > bx1) {
|
|
3431
|
+
bx1 = qx
|
|
3432
|
+
}
|
|
3433
|
+
if (qy < by0) {
|
|
3434
|
+
by0 = qy
|
|
3435
|
+
}
|
|
3436
|
+
if (qy > by1) {
|
|
3437
|
+
by1 = qy
|
|
3438
|
+
}
|
|
3439
|
+
c = (c + 1)
|
|
3440
|
+
}
|
|
3441
|
+
; One cell of margin so a shape touching the box edge still
|
|
3442
|
+
; closes a ring around itself.
|
|
3443
|
+
bx0 = (bx0 - 1)
|
|
3444
|
+
by0 = (by0 - 1)
|
|
3445
|
+
def bw:int ((bx1 - bx0) + 2)
|
|
3446
|
+
def bh:int ((by1 - by0) + 2)
|
|
3447
|
+
def bm:EvgBinaryBitmap (EvgBinaryBitmap.create(bw bh))
|
|
3448
|
+
c = 0
|
|
3449
|
+
while (c < (array_length comp)) {
|
|
3450
|
+
def p2:int (itemAt comp c)
|
|
3451
|
+
def yy:int (idiv p2 width)
|
|
3452
|
+
bm.setBit(((p2 - (yy * width)) - bx0) (yy - by0) true)
|
|
3453
|
+
c = (c + 1)
|
|
3454
|
+
}
|
|
3455
|
+
def layerOpts:EvgTraceOptions (EvgTraceOptions.defaults())
|
|
3456
|
+
layerOpts.turdsize = options.turdsize
|
|
3457
|
+
layerOpts.alphamax = options.alphamax
|
|
3458
|
+
layerOpts.turnpolicy = options.turnpolicy
|
|
3459
|
+
layerOpts.optcurve = options.optcurve
|
|
3460
|
+
layerOpts.opttolerance = options.opttolerance
|
|
3461
|
+
layerOpts.fillHex = (this.hexFromRgb(mr mg mb))
|
|
3462
|
+
layerOpts.pathFormat = options.pathFormat
|
|
3463
|
+
layerOpts.pathPrecision = options.pathPrecision
|
|
3464
|
+
def sub:EvgBitmapTracer (EvgBitmapTracer.fromBinary(bm layerOpts))
|
|
3465
|
+
sub.trace()
|
|
3466
|
+
if ((sub.ringCount()) > 0) {
|
|
3467
|
+
def cmds:[PathCommand] (sub.getCommands())
|
|
3468
|
+
def ox:double (to_double bx0)
|
|
3469
|
+
def oy:double (to_double by0)
|
|
3470
|
+
def ci:int 0
|
|
3471
|
+
while (ci < (array_length cmds)) {
|
|
3472
|
+
def cm:PathCommand (itemAt cmds ci)
|
|
3473
|
+
cm.x = (cm.x + ox)
|
|
3474
|
+
cm.y = (cm.y + oy)
|
|
3475
|
+
cm.x1 = (cm.x1 + ox)
|
|
3476
|
+
cm.y1 = (cm.y1 + oy)
|
|
3477
|
+
cm.x2 = (cm.x2 + ox)
|
|
3478
|
+
cm.y2 = (cm.y2 + oy)
|
|
3479
|
+
push allCmds cm
|
|
3480
|
+
ci = (ci + 1)
|
|
3481
|
+
}
|
|
3482
|
+
def layer:EvgTraceLayer (itemAt sub.layers 0)
|
|
3483
|
+
layer.pathData = (this.pathDataOf(cmds))
|
|
3484
|
+
if options.gradientFill {
|
|
3485
|
+
this.fitAndPaintShape(comp layer paintR paintG paintB)
|
|
3486
|
+
}
|
|
3487
|
+
push layers layer
|
|
3488
|
+
rings = sub.rings
|
|
3489
|
+
}
|
|
3490
|
+
}
|
|
3491
|
+
shapeNo = (shapeNo + 1)
|
|
3492
|
+
}
|
|
3493
|
+
}
|
|
3494
|
+
commands = allCmds
|
|
3495
|
+
if ((array_length layers) > 0) {
|
|
3496
|
+
def first:EvgTraceLayer (itemAt layers 0)
|
|
3497
|
+
pathData = first.pathData
|
|
3498
|
+
}
|
|
3499
|
+
}
|
|
3500
|
+
|
|
3501
|
+
; True where the picture asks for fine work — see buildDetailMask.
|
|
3502
|
+
fn isDetail:boolean (i:int) {
|
|
3503
|
+
if (detailOn == false) {
|
|
3504
|
+
return false
|
|
3505
|
+
}
|
|
3506
|
+
return ((itemAt detailMask i) == 1)
|
|
3507
|
+
}
|
|
3508
|
+
|
|
3509
|
+
; One step of an overlay's growth. Three things stop it: an edge, drifting
|
|
3510
|
+
; too far from its own mean, and arriving somewhere it would look the same
|
|
3511
|
+
; as what is already painted. An edge is not just a stop — it is where the
|
|
3512
|
+
; next shape begins, on the far side and above.
|
|
3513
|
+
; Push one seed into the bucket for its edge strength.
|
|
3514
|
+
;
|
|
3515
|
+
; A pixel is a point, and the queue's question is "has every steep point
|
|
3516
|
+
; been dealt with" — so queuing the same pixel twice answers nothing and
|
|
3517
|
+
; costs everything. Growth bumps into the same boundary from every pixel
|
|
3518
|
+
; along it, and each bump used to queue both sides again: on a photograph
|
|
3519
|
+
; that is millions of entries in three arrays that only ever grow, which is
|
|
3520
|
+
; why a 640-pixel-wide photo spent ten seconds here and half of it in the
|
|
3521
|
+
; collector. seedBest remembers the strength each pixel is already waiting
|
|
3522
|
+
; at, and only a steeper claim on the same pixel gets through — so a pixel
|
|
3523
|
+
; can still be promoted to the front, but never merely repeated.
|
|
3524
|
+
fn pushSeed:void (px:int strength:int bucket:[int] seedPixel:[int] seedNext:[int] seedBest:[int]) {
|
|
3525
|
+
def s:int strength
|
|
3526
|
+
if (s < 0) {
|
|
3527
|
+
s = 0
|
|
3528
|
+
}
|
|
3529
|
+
if (s > 255) {
|
|
3530
|
+
s = 255
|
|
3531
|
+
}
|
|
3532
|
+
if (s <= (itemAt seedBest px)) {
|
|
3533
|
+
return
|
|
3534
|
+
}
|
|
3535
|
+
set seedBest px s
|
|
3536
|
+
def idx:int (array_length seedPixel)
|
|
3537
|
+
push seedPixel px
|
|
3538
|
+
push seedNext (itemAt bucket s)
|
|
3539
|
+
set bucket s idx
|
|
3540
|
+
}
|
|
3541
|
+
|
|
3542
|
+
; Take the steepest seed still waiting, or -1 when there are none.
|
|
3543
|
+
fn popSeed:int (bucket:[int] seedPixel:[int] seedNext:[int]) {
|
|
3544
|
+
def s:int 255
|
|
3545
|
+
while (s >= 0) {
|
|
3546
|
+
def idx:int (itemAt bucket s)
|
|
3547
|
+
if (idx >= 0) {
|
|
3548
|
+
set bucket s (itemAt seedNext idx)
|
|
3549
|
+
return (itemAt seedPixel idx)
|
|
3550
|
+
}
|
|
3551
|
+
s = (s - 1)
|
|
3552
|
+
}
|
|
3553
|
+
return (0 - 1)
|
|
3554
|
+
}
|
|
3555
|
+
|
|
3556
|
+
fn overlayStep:void (i:int from:int stamp:[int] stack:[int] acc:[int] bucket:[int] seedPixel:[int] seedNext:[int] seedBest:[int] shapeNo:int edgeTol:int spread:int simTol:int paintR:[int] paintG:[int] paintB:[int] painted:[int]) {
|
|
3557
|
+
if ((itemAt stamp i) == shapeNo) {
|
|
3558
|
+
return
|
|
3559
|
+
}
|
|
3560
|
+
if ((itemAt labels i) < 0) {
|
|
3561
|
+
return
|
|
3562
|
+
}
|
|
3563
|
+
; An overlay shape may not cross a boundary of the base picture.
|
|
3564
|
+
;
|
|
3565
|
+
; Without this it is free to disagree with the quantizer that ran just
|
|
3566
|
+
; before it, and on a photograph it does: a surface grown by continuity
|
|
3567
|
+
; runs from a cheek out into the background behind it, takes one color
|
|
3568
|
+
; for the pair, and a face comes back as brown mush. Growth stopping at
|
|
3569
|
+
; the base boundary makes overlay a *refinement* of that picture rather
|
|
3570
|
+
; than a competitor to it — it may still split one region into several
|
|
3571
|
+
; shapes stacked in z, which is the whole point of it, but it can no
|
|
3572
|
+
; longer contradict where the regions are.
|
|
3573
|
+
;
|
|
3574
|
+
; Checking the pair rather than the seed is the same test: every step
|
|
3575
|
+
; preserves the label, so the whole component carries one. It is also
|
|
3576
|
+
; what bounds a shape's area, since no shape can outgrow its region.
|
|
3577
|
+
; — except where the picture asks for precision. The leash and the
|
|
3578
|
+
; point of the mode are in tension: a shape confined to one region has a
|
|
3579
|
+
; mean dominated by that region, so it snaps back to that region's own
|
|
3580
|
+
; swatch and repaints what is already there. Measured: with the leash on
|
|
3581
|
+
; everywhere, 42 overlay shapes changed the per-pixel error by nothing
|
|
3582
|
+
; at all, to two decimals. Off everywhere, a face turns to mush. So it
|
|
3583
|
+
; comes off exactly where buildDetailMask says several plainly different
|
|
3584
|
+
; colors meet in a few pixels — an eye, a set of teeth — and stays on
|
|
3585
|
+
; across the cheeks and the curtains, which is where letting go hurt.
|
|
3586
|
+
if (options.overlayFollowBase) {
|
|
3587
|
+
if ((this.isDetail(i)) == false) {
|
|
3588
|
+
if ((itemAt labels i) != (itemAt labels from)) {
|
|
3589
|
+
return
|
|
3590
|
+
}
|
|
3591
|
+
}
|
|
3592
|
+
}
|
|
3593
|
+
def step:int (this.pixelDelta(from i))
|
|
3594
|
+
if (step > edgeTol) {
|
|
3595
|
+
; Both sides of an edge are shapes of their own. Seeding only the
|
|
3596
|
+
; far side leaves the near side to whichever surface happens to
|
|
3597
|
+
; arrive there, and at a boundary that fades that is the surface
|
|
3598
|
+
; which already leaked through it — which is how half a shape goes
|
|
3599
|
+
; missing while the other half is drawn.
|
|
3600
|
+
;
|
|
3601
|
+
; A step still stops the growth whatever it is — crossing it would
|
|
3602
|
+
; only hand the region a color it cannot describe — but it earns a
|
|
3603
|
+
; new shape only when it is a boundary rather than a speck. That is
|
|
3604
|
+
; the difference between finding edges and finding noise.
|
|
3605
|
+
if (this.isBoundaryOf(from i edgeTol step)) {
|
|
3606
|
+
this.pushSeed(i step bucket seedPixel seedNext seedBest)
|
|
3607
|
+
this.pushSeed(from step bucket seedPixel seedNext seedBest)
|
|
3608
|
+
}
|
|
3609
|
+
return
|
|
3610
|
+
}
|
|
3611
|
+
; Inside a detail neighbourhood the bar for "this shows against what is
|
|
3612
|
+
; under it" is lower, so the small strong shapes an eye is made of are
|
|
3613
|
+
; drawn instead of being written off as near enough to the skin.
|
|
3614
|
+
def useSim:int simTol
|
|
3615
|
+
if (this.isDetail(i)) {
|
|
3616
|
+
useSim = (idiv simTol (options.detailBoost))
|
|
3617
|
+
}
|
|
3618
|
+
if (this.coveredAlready(i paintR paintG paintB painted useSim)) {
|
|
3619
|
+
return
|
|
3620
|
+
}
|
|
3621
|
+
def cnt:int (itemAt acc 3)
|
|
3622
|
+
def mr:int (idiv (itemAt acc 0) cnt)
|
|
3623
|
+
def mg:int (idiv (itemAt acc 1) cnt)
|
|
3624
|
+
def mb:int (idiv (itemAt acc 2) cnt)
|
|
3625
|
+
def d:int (EvgBitmapTracer.absI((itemAt planeR i) - mr))
|
|
3626
|
+
def dg:int (EvgBitmapTracer.absI((itemAt planeG i) - mg))
|
|
3627
|
+
if (dg > d) {
|
|
3628
|
+
d = dg
|
|
3629
|
+
}
|
|
3630
|
+
def db:int (EvgBitmapTracer.absI((itemAt planeB i) - mb))
|
|
3631
|
+
if (db > d) {
|
|
3632
|
+
d = db
|
|
3633
|
+
}
|
|
3634
|
+
if (d > spread) {
|
|
3635
|
+
return
|
|
3636
|
+
}
|
|
3637
|
+
set stamp i shapeNo
|
|
3638
|
+
push stack i
|
|
3639
|
+
set acc 0 ((itemAt acc 0) + (itemAt planeR i))
|
|
3640
|
+
set acc 1 ((itemAt acc 1) + (itemAt planeG i))
|
|
3641
|
+
set acc 2 ((itemAt acc 2) + (itemAt planeB i))
|
|
3642
|
+
set acc 3 (cnt + 1)
|
|
3643
|
+
}
|
|
3644
|
+
|
|
3645
|
+
; Absorb connected runs below options.minRegion into the region that
|
|
3646
|
+
; surrounds them. turdsize would otherwise drop those rings outright, which
|
|
3647
|
+
; is why the painted area used to shrink every time colorCount went up.
|
|
3648
|
+
;
|
|
3649
|
+
; Size alone is the wrong question, though. A four-pixel run is noise when
|
|
3650
|
+
; it looks like what is around it and it is the *subject* when it does not:
|
|
3651
|
+
; the spray off a wave, a boat against the sea, a highlight in an eye are
|
|
3652
|
+
; all a handful of pixels that no larger shape is going to put back.
|
|
3653
|
+
; options.absorbContrast is the second half of the test — a run whose
|
|
3654
|
+
; swatch is that far from the swatch it would join survives on its own.
|
|
3655
|
+
fn mergeTinyRegions:void (palR:[int] palG:[int] palB:[int]) {
|
|
3656
|
+
def k:int (array_length palR)
|
|
3657
|
+
def minPx:int options.minRegion
|
|
3658
|
+
if (minPx < 2) {
|
|
3659
|
+
return
|
|
3660
|
+
}
|
|
3661
|
+
def keepLimit:double 0.0
|
|
3662
|
+
def keepOn:boolean false
|
|
3663
|
+
if ((options.absorbContrast) > 0) {
|
|
3664
|
+
keepOn = true
|
|
3665
|
+
keepLimit = (this.dist2Limit(options.absorbContrast))
|
|
3666
|
+
}
|
|
3667
|
+
def n:int (width * height)
|
|
3668
|
+
def seen:[int]
|
|
3669
|
+
def i:int 0
|
|
3670
|
+
while (i < n) {
|
|
3671
|
+
push seen 0
|
|
3672
|
+
i = (i + 1)
|
|
3673
|
+
}
|
|
3674
|
+
def touch:[int]
|
|
3675
|
+
def ki:int 0
|
|
3676
|
+
while (ki < k) {
|
|
3677
|
+
push touch 0
|
|
3678
|
+
ki = (ki + 1)
|
|
3679
|
+
}
|
|
3680
|
+
def start:int 0
|
|
3681
|
+
while (start < n) {
|
|
3682
|
+
def lab:int (itemAt labels start)
|
|
3683
|
+
if ((lab >= 0) && ((itemAt seen start) == 0)) {
|
|
3684
|
+
def comp:[int]
|
|
3685
|
+
def stack:[int]
|
|
3686
|
+
def c:int 0
|
|
3687
|
+
ki = 0
|
|
3688
|
+
while (ki < k) {
|
|
3689
|
+
set touch ki 0
|
|
3690
|
+
ki = (ki + 1)
|
|
3691
|
+
}
|
|
3692
|
+
set seen start 1
|
|
3693
|
+
push stack start
|
|
3694
|
+
def head:int 0
|
|
3695
|
+
while (head < (array_length stack)) {
|
|
3696
|
+
def cur:int (itemAt stack head)
|
|
3697
|
+
head = (head + 1)
|
|
3698
|
+
push comp cur
|
|
3699
|
+
def cy:int (idiv cur width)
|
|
3700
|
+
def cx:int (cur - (cy * width))
|
|
3701
|
+
if (cx > 0) {
|
|
3702
|
+
this.compStep((cur - 1) lab seen stack touch)
|
|
3703
|
+
}
|
|
3704
|
+
if (cx < (width - 1)) {
|
|
3705
|
+
this.compStep((cur + 1) lab seen stack touch)
|
|
3706
|
+
}
|
|
3707
|
+
if (cy > 0) {
|
|
3708
|
+
this.compStep((cur - width) lab seen stack touch)
|
|
3709
|
+
}
|
|
3710
|
+
if (cy < (height - 1)) {
|
|
3711
|
+
this.compStep((cur + width) lab seen stack touch)
|
|
3712
|
+
}
|
|
3713
|
+
}
|
|
3714
|
+
if ((array_length comp) < minPx) {
|
|
3715
|
+
def bestLab:int (0 - 1)
|
|
3716
|
+
def bestN:int 0
|
|
3717
|
+
ki = 0
|
|
3718
|
+
while (ki < k) {
|
|
3719
|
+
if ((itemAt touch ki) > bestN) {
|
|
3720
|
+
bestN = (itemAt touch ki)
|
|
3721
|
+
bestLab = ki
|
|
3722
|
+
}
|
|
3723
|
+
ki = (ki + 1)
|
|
3724
|
+
}
|
|
3725
|
+
if (bestLab >= 0) {
|
|
3726
|
+
def absorb:boolean true
|
|
3727
|
+
if (keepOn && (lab >= 0)) {
|
|
3728
|
+
def gap:double (this.dist2((itemAt palR lab) (itemAt palG lab) (itemAt palB lab) (itemAt palR bestLab) (itemAt palG bestLab) (itemAt palB bestLab)))
|
|
3729
|
+
if (gap >= keepLimit) {
|
|
3730
|
+
absorb = false
|
|
3731
|
+
}
|
|
3732
|
+
}
|
|
3733
|
+
if absorb {
|
|
3734
|
+
c = 0
|
|
3735
|
+
while (c < (array_length comp)) {
|
|
3736
|
+
set labels (itemAt comp c) bestLab
|
|
3737
|
+
c = (c + 1)
|
|
3738
|
+
}
|
|
3739
|
+
}
|
|
3740
|
+
}
|
|
3741
|
+
}
|
|
3742
|
+
}
|
|
3743
|
+
start = (start + 1)
|
|
3744
|
+
}
|
|
3745
|
+
}
|
|
3746
|
+
|
|
3747
|
+
; Flood one step of the component walk; a differently-labelled neighbour is
|
|
3748
|
+
; recorded as a merge candidate instead.
|
|
3749
|
+
fn compStep:void (i:int lab:int seen:[int] stack:[int] touch:[int]) {
|
|
3750
|
+
def other:int (itemAt labels i)
|
|
3751
|
+
if (other == lab) {
|
|
3752
|
+
if ((itemAt seen i) == 0) {
|
|
3753
|
+
set seen i 1
|
|
3754
|
+
push stack i
|
|
3755
|
+
}
|
|
3756
|
+
return
|
|
3757
|
+
}
|
|
3758
|
+
if (other >= 0) {
|
|
3759
|
+
set touch other ((itemAt touch other) + 1)
|
|
3760
|
+
}
|
|
3761
|
+
}
|
|
3762
|
+
|
|
3763
|
+
; Drop single-pixel islands whose whole 4-neighbourhood agrees on another
|
|
3764
|
+
; swatch. Those specks only add rings that turdsize would delete anyway.
|
|
3765
|
+
fn despeckleLabels:void () {
|
|
3766
|
+
def n:int (width * height)
|
|
3767
|
+
def src:[int]
|
|
3768
|
+
def i:int 0
|
|
3769
|
+
while (i < n) {
|
|
3770
|
+
push src (itemAt labels i)
|
|
3771
|
+
i = (i + 1)
|
|
3772
|
+
}
|
|
3773
|
+
def y:int 0
|
|
3774
|
+
while (y < height) {
|
|
3775
|
+
def x:int 0
|
|
3776
|
+
while (x < width) {
|
|
3777
|
+
def idx:int ((y * width) + x)
|
|
3778
|
+
def me:int (itemAt src idx)
|
|
3779
|
+
if (me >= 0) {
|
|
3780
|
+
def agree:int (0 - 2)
|
|
3781
|
+
def same:boolean true
|
|
3782
|
+
def cnt:int 0
|
|
3783
|
+
if (x > 0) {
|
|
3784
|
+
def l:int (itemAt src (idx - 1))
|
|
3785
|
+
if (agree == (0 - 2)) {
|
|
3786
|
+
agree = l
|
|
3787
|
+
}
|
|
3788
|
+
if (l != agree) {
|
|
3789
|
+
same = false
|
|
3790
|
+
}
|
|
3791
|
+
cnt = (cnt + 1)
|
|
3792
|
+
}
|
|
3793
|
+
if (x < (width - 1)) {
|
|
3794
|
+
def r:int (itemAt src (idx + 1))
|
|
3795
|
+
if (agree == (0 - 2)) {
|
|
3796
|
+
agree = r
|
|
3797
|
+
}
|
|
3798
|
+
if (r != agree) {
|
|
3799
|
+
same = false
|
|
3800
|
+
}
|
|
3801
|
+
cnt = (cnt + 1)
|
|
3802
|
+
}
|
|
3803
|
+
if (y > 0) {
|
|
3804
|
+
def u:int (itemAt src (idx - width))
|
|
3805
|
+
if (agree == (0 - 2)) {
|
|
3806
|
+
agree = u
|
|
3807
|
+
}
|
|
3808
|
+
if (u != agree) {
|
|
3809
|
+
same = false
|
|
3810
|
+
}
|
|
3811
|
+
cnt = (cnt + 1)
|
|
3812
|
+
}
|
|
3813
|
+
if (y < (height - 1)) {
|
|
3814
|
+
def dn:int (itemAt src (idx + width))
|
|
3815
|
+
if (agree == (0 - 2)) {
|
|
3816
|
+
agree = dn
|
|
3817
|
+
}
|
|
3818
|
+
if (dn != agree) {
|
|
3819
|
+
same = false
|
|
3820
|
+
}
|
|
3821
|
+
cnt = (cnt + 1)
|
|
3822
|
+
}
|
|
3823
|
+
if (same && (cnt >= 3) && (agree != me)) {
|
|
3824
|
+
set labels idx agree
|
|
3825
|
+
}
|
|
3826
|
+
}
|
|
3827
|
+
x = (x + 1)
|
|
3828
|
+
}
|
|
3829
|
+
y = (y + 1)
|
|
3830
|
+
}
|
|
3831
|
+
}
|
|
3832
|
+
|
|
3833
|
+
fn hexFromRgb:string (r:int g:int b:int) {
|
|
3834
|
+
def c:EVGColor (EVGColor.rgb(r g b))
|
|
3835
|
+
return (c.toHexString())
|
|
3836
|
+
}
|
|
3837
|
+
|
|
3838
|
+
; In "stacked" mode a layer claims its own swatch and everything painted
|
|
3839
|
+
; after it, so each layer is drawn on top of a filled neighbour rather than
|
|
3840
|
+
; on the page. The palette is sorted dark → light, so what shows through a
|
|
3841
|
+
; hairline is the next darker color instead of white.
|
|
3842
|
+
fn maskForLabel:EvgBinaryBitmap (colorIndex:int) {
|
|
3843
|
+
def stacked:boolean ((options.layerMode) != "flat")
|
|
3844
|
+
def bm:EvgBinaryBitmap (EvgBinaryBitmap.create(width height))
|
|
3845
|
+
def n:int (array_length labels)
|
|
3846
|
+
def i:int 0
|
|
3847
|
+
while (i < n) {
|
|
3848
|
+
def lab:int (itemAt labels i)
|
|
3849
|
+
def take:boolean (lab == colorIndex)
|
|
3850
|
+
if (stacked && (lab >= colorIndex) && (lab >= 0)) {
|
|
3851
|
+
take = true
|
|
3852
|
+
}
|
|
3853
|
+
if take {
|
|
3854
|
+
def y:int (idiv i width)
|
|
3855
|
+
def x:int (i - (y * width))
|
|
3856
|
+
bm.setBit(x y true)
|
|
3857
|
+
}
|
|
3858
|
+
i = (i + 1)
|
|
3859
|
+
}
|
|
3860
|
+
return bm
|
|
3861
|
+
}
|
|
3862
|
+
|
|
3863
|
+
; A 3×3 median that moves whole pixels: of the nine neighbours, take the one
|
|
3864
|
+
; whose luma is the median and copy its RGB. Picking each channel's median
|
|
3865
|
+
; separately would invent colors that are nowhere in the image; this cannot,
|
|
3866
|
+
; and it removes speckle without softening an edge the way a blur does.
|
|
3867
|
+
fn smoothPlanes:void () {
|
|
3868
|
+
def passes:int options.smooth
|
|
3869
|
+
if (passes < 1) {
|
|
3870
|
+
return
|
|
3871
|
+
}
|
|
3872
|
+
if (passes > 4) {
|
|
3873
|
+
passes = 4
|
|
3874
|
+
}
|
|
3875
|
+
def n:int (width * height)
|
|
3876
|
+
def pass:int 0
|
|
3877
|
+
while (pass < passes) {
|
|
3878
|
+
def srcR:[int]
|
|
3879
|
+
def srcG:[int]
|
|
3880
|
+
def srcB:[int]
|
|
3881
|
+
def i:int 0
|
|
3882
|
+
while (i < n) {
|
|
3883
|
+
push srcR (itemAt planeR i)
|
|
3884
|
+
push srcG (itemAt planeG i)
|
|
3885
|
+
push srcB (itemAt planeB i)
|
|
3886
|
+
i = (i + 1)
|
|
3887
|
+
}
|
|
3888
|
+
def y:int 0
|
|
3889
|
+
while (y < height) {
|
|
3890
|
+
def x:int 0
|
|
3891
|
+
while (x < width) {
|
|
3892
|
+
def lum:[int]
|
|
3893
|
+
def idxs:[int]
|
|
3894
|
+
def dy:int (0 - 1)
|
|
3895
|
+
while (dy <= 1) {
|
|
3896
|
+
def yy:int (y + dy)
|
|
3897
|
+
if ((yy >= 0) && (yy < height)) {
|
|
3898
|
+
def dx:int (0 - 1)
|
|
3899
|
+
while (dx <= 1) {
|
|
3900
|
+
def xx:int (x + dx)
|
|
3901
|
+
if ((xx >= 0) && (xx < width)) {
|
|
3902
|
+
def j:int ((yy * width) + xx)
|
|
3903
|
+
push idxs j
|
|
3904
|
+
push lum (EvgBitmapTracer.lumaOf((itemAt srcR j) (itemAt srcG j) (itemAt srcB j)))
|
|
3905
|
+
}
|
|
3906
|
+
dx = (dx + 1)
|
|
3907
|
+
}
|
|
3908
|
+
}
|
|
3909
|
+
dy = (dy + 1)
|
|
3910
|
+
}
|
|
3911
|
+
; Insertion sort — at most nine entries.
|
|
3912
|
+
def k:int (array_length lum)
|
|
3913
|
+
def a:int 1
|
|
3914
|
+
while (a < k) {
|
|
3915
|
+
def lv:int (itemAt lum a)
|
|
3916
|
+
def iv:int (itemAt idxs a)
|
|
3917
|
+
def b:int (a - 1)
|
|
3918
|
+
while ((b >= 0) && ((itemAt lum b) > lv)) {
|
|
3919
|
+
set lum (b + 1) (itemAt lum b)
|
|
3920
|
+
set idxs (b + 1) (itemAt idxs b)
|
|
3921
|
+
b = (b - 1)
|
|
3922
|
+
}
|
|
3923
|
+
set lum (b + 1) lv
|
|
3924
|
+
set idxs (b + 1) iv
|
|
3925
|
+
a = (a + 1)
|
|
3926
|
+
}
|
|
3927
|
+
def pick:int (itemAt idxs (idiv k 2))
|
|
3928
|
+
def here:int ((y * width) + x)
|
|
3929
|
+
set planeR here (itemAt srcR pick)
|
|
3930
|
+
set planeG here (itemAt srcG pick)
|
|
3931
|
+
set planeB here (itemAt srcB pick)
|
|
3932
|
+
x = (x + 1)
|
|
3933
|
+
}
|
|
3934
|
+
y = (y + 1)
|
|
3935
|
+
}
|
|
3936
|
+
pass = (pass + 1)
|
|
3937
|
+
}
|
|
3938
|
+
}
|
|
3939
|
+
|
|
3940
|
+
; Paint the ordinary quantized picture — one evenodd layer per swatch —
|
|
3941
|
+
; appending to layers and to allCmds. This is what the tracer draws when no
|
|
3942
|
+
; extra algorithm is asked for, and it is also what the overlay tracer now
|
|
3943
|
+
; stands on.
|
|
3944
|
+
fn paintLabelLayers:void (palR:[int] palG:[int] palB:[int] allCmds:[PathCommand]) {
|
|
3945
|
+
def k:int (array_length palR)
|
|
3946
|
+
def li:int 0
|
|
3947
|
+
while (li < k) {
|
|
3948
|
+
def mask:EvgBinaryBitmap (this.maskForLabel(li))
|
|
3949
|
+
def layerOpts:EvgTraceOptions (EvgTraceOptions.defaults())
|
|
3950
|
+
layerOpts.turdsize = options.turdsize
|
|
3951
|
+
layerOpts.alphamax = options.alphamax
|
|
3952
|
+
layerOpts.turnpolicy = options.turnpolicy
|
|
3953
|
+
layerOpts.optcurve = options.optcurve
|
|
3954
|
+
layerOpts.opttolerance = options.opttolerance
|
|
3955
|
+
layerOpts.pathFormat = options.pathFormat
|
|
3956
|
+
layerOpts.pathPrecision = options.pathPrecision
|
|
3957
|
+
layerOpts.fillHex = (this.hexFromRgb((itemAt palR li) (itemAt palG li) (itemAt palB li)))
|
|
3958
|
+
def sub:EvgBitmapTracer (EvgBitmapTracer.fromBinary(mask layerOpts))
|
|
3959
|
+
sub.trace()
|
|
3960
|
+
if ((sub.ringCount()) > 0) {
|
|
3961
|
+
def layer:EvgTraceLayer (itemAt sub.layers 0)
|
|
3962
|
+
push layers layer
|
|
3963
|
+
def cmds:[PathCommand] (sub.getCommands())
|
|
3964
|
+
def ci:int 0
|
|
3965
|
+
while (ci < (array_length cmds)) {
|
|
3966
|
+
push allCmds (itemAt cmds ci)
|
|
3967
|
+
ci = (ci + 1)
|
|
3968
|
+
}
|
|
3969
|
+
; Keep mono-compatible aggregates: last non-empty path + total rings.
|
|
3970
|
+
pathData = layer.pathData
|
|
3971
|
+
rings = sub.rings
|
|
3972
|
+
}
|
|
3973
|
+
li = (li + 1)
|
|
3974
|
+
}
|
|
3975
|
+
}
|
|
3976
|
+
|
|
3977
|
+
fn traceColorLayers:void () {
|
|
3978
|
+
def want:int options.colorCount
|
|
3979
|
+
if (want < 2) {
|
|
3980
|
+
want = 2
|
|
3981
|
+
}
|
|
3982
|
+
this.smoothPlanes()
|
|
3983
|
+
this.detectBackground()
|
|
3984
|
+
this.buildFlatMask()
|
|
3985
|
+
def mode:string options.paletteMode
|
|
3986
|
+
def palR:[int]
|
|
3987
|
+
def palG:[int]
|
|
3988
|
+
def palB:[int]
|
|
3989
|
+
def given:int 0
|
|
3990
|
+
if ((mode == "fixed") || (mode == "seeded")) {
|
|
3991
|
+
given = (this.parsePaletteHex(palR palG palB))
|
|
3992
|
+
}
|
|
3993
|
+
; "fixed" with a usable list skips quantization entirely and keeps the
|
|
3994
|
+
; caller's order; anything else (including a list that parsed to
|
|
3995
|
+
; nothing) goes through the quantizer, with the given colors pinned.
|
|
3996
|
+
def useGivenOnly:boolean false
|
|
3997
|
+
if (mode == "fixed") {
|
|
3998
|
+
if (given > 0) {
|
|
3999
|
+
useGivenOnly = true
|
|
4000
|
+
}
|
|
4001
|
+
}
|
|
4002
|
+
if (useGivenOnly == false) {
|
|
4003
|
+
this.buildPalette(want given palR palG palB)
|
|
4004
|
+
}
|
|
4005
|
+
def k:int (array_length palR)
|
|
4006
|
+
if (k == 0) {
|
|
4007
|
+
return
|
|
4008
|
+
}
|
|
4009
|
+
this.assignLabels(palR palG palB)
|
|
4010
|
+
; Every decision above was made against the measured colors; from here
|
|
4011
|
+
; the palette is only what the picture gets painted with, so this is
|
|
4012
|
+
; where a dialled palette replaces a measured one.
|
|
4013
|
+
this.gradePalette(palR palG palB)
|
|
4014
|
+
; Overlay shapes carry their own fits, so it takes precedence when both
|
|
4015
|
+
; are asked for — the two compose rather than compete.
|
|
4016
|
+
if ((options.contourMode) == "overlay") {
|
|
4017
|
+
this.traceOverlayShapes(palR palG palB)
|
|
4018
|
+
return
|
|
4019
|
+
}
|
|
4020
|
+
if options.gradientFill {
|
|
4021
|
+
this.traceGradientRegions(palR palG palB)
|
|
4022
|
+
return
|
|
4023
|
+
}
|
|
4024
|
+
def allCmds:[PathCommand]
|
|
4025
|
+
this.paintLabelLayers(palR palG palB allCmds)
|
|
4026
|
+
commands = allCmds
|
|
4027
|
+
if ((array_length layers) > 0) {
|
|
4028
|
+
def first:EvgTraceLayer (itemAt layers 0)
|
|
4029
|
+
pathData = first.pathData
|
|
4030
|
+
}
|
|
4031
|
+
}
|
|
4032
|
+
|
|
4033
|
+
; ----------------------------------------------------------------------
|
|
4034
|
+
; 1. Path decomposition (Potrace-style edge walk)
|
|
4035
|
+
; ----------------------------------------------------------------------
|
|
4036
|
+
|
|
4037
|
+
fn decompose:void () {
|
|
4038
|
+
def work:EvgBinaryBitmap (bitmap.copy())
|
|
4039
|
+
def start:int 0
|
|
4040
|
+
while (start >= 0) {
|
|
4041
|
+
def idx:int (work.findNext(start))
|
|
4042
|
+
if (idx < 0) {
|
|
4043
|
+
start = (0 - 1)
|
|
4044
|
+
} {
|
|
4045
|
+
def y0:int (idiv idx work.w)
|
|
4046
|
+
def x0:int (idx - (y0 * work.w))
|
|
4047
|
+
def ring:EvgTraceRing (this.findPath(work x0 y0))
|
|
4048
|
+
this.xorPath(work ring)
|
|
4049
|
+
def a:int (EvgBitmapTracer.absI(ring.area))
|
|
4050
|
+
if (a > options.turdsize) {
|
|
4051
|
+
push rings ring
|
|
4052
|
+
}
|
|
4053
|
+
start = idx
|
|
4054
|
+
}
|
|
4055
|
+
}
|
|
4056
|
+
}
|
|
4057
|
+
|
|
4058
|
+
fn majorityAt:boolean (bm:EvgBinaryBitmap x:int y:int) {
|
|
4059
|
+
def i:int 2
|
|
4060
|
+
while (i < 5) {
|
|
4061
|
+
def ct:int 0
|
|
4062
|
+
def a:int (0 - i + 1)
|
|
4063
|
+
while (a <= (i - 1)) {
|
|
4064
|
+
if (bm.at((x + a) (y + i - 1))) {
|
|
4065
|
+
ct = (ct + 1)
|
|
4066
|
+
} {
|
|
4067
|
+
ct = (ct - 1)
|
|
4068
|
+
}
|
|
4069
|
+
if (bm.at((x + i - 1) (y + a - 1))) {
|
|
4070
|
+
ct = (ct + 1)
|
|
4071
|
+
} {
|
|
4072
|
+
ct = (ct - 1)
|
|
4073
|
+
}
|
|
4074
|
+
if (bm.at((x + a - 1) (y - i))) {
|
|
4075
|
+
ct = (ct + 1)
|
|
4076
|
+
} {
|
|
4077
|
+
ct = (ct - 1)
|
|
4078
|
+
}
|
|
4079
|
+
if (bm.at((x - i) (y + a))) {
|
|
4080
|
+
ct = (ct + 1)
|
|
4081
|
+
} {
|
|
4082
|
+
ct = (ct - 1)
|
|
4083
|
+
}
|
|
4084
|
+
a = (a + 1)
|
|
4085
|
+
}
|
|
4086
|
+
if (ct > 0) {
|
|
4087
|
+
return true
|
|
4088
|
+
}
|
|
4089
|
+
if (ct < 0) {
|
|
4090
|
+
return false
|
|
4091
|
+
}
|
|
4092
|
+
i = (i + 1)
|
|
4093
|
+
}
|
|
4094
|
+
return false
|
|
4095
|
+
}
|
|
4096
|
+
|
|
4097
|
+
fn shouldTurnRight:boolean (bm:EvgBinaryBitmap path:EvgTraceRing x:int y:int) {
|
|
4098
|
+
def pol:string options.turnpolicy
|
|
4099
|
+
if (pol == "right") {
|
|
4100
|
+
return true
|
|
4101
|
+
}
|
|
4102
|
+
if (pol == "left") {
|
|
4103
|
+
return false
|
|
4104
|
+
}
|
|
4105
|
+
if (pol == "black") {
|
|
4106
|
+
return (path.sign == "+")
|
|
4107
|
+
}
|
|
4108
|
+
if (pol == "white") {
|
|
4109
|
+
return (path.sign == "-")
|
|
4110
|
+
}
|
|
4111
|
+
if (pol == "majority") {
|
|
4112
|
+
return (this.majorityAt(bm x y))
|
|
4113
|
+
}
|
|
4114
|
+
; minority (default): turn right when majority is false
|
|
4115
|
+
def maj:boolean (this.majorityAt(bm x y))
|
|
4116
|
+
return (maj == false)
|
|
4117
|
+
}
|
|
4118
|
+
|
|
4119
|
+
fn findPath:EvgTraceRing (bm:EvgBinaryBitmap x0:int y0:int) {
|
|
4120
|
+
def path (new EvgTraceRing())
|
|
4121
|
+
path.minX = x0
|
|
4122
|
+
path.maxX = x0
|
|
4123
|
+
path.minY = y0
|
|
4124
|
+
path.maxY = y0
|
|
4125
|
+
if (bitmap.at(x0 y0)) {
|
|
4126
|
+
path.sign = "+"
|
|
4127
|
+
} {
|
|
4128
|
+
path.sign = "-"
|
|
4129
|
+
}
|
|
4130
|
+
def x:int x0
|
|
4131
|
+
def y:int y0
|
|
4132
|
+
def dirx:int 0
|
|
4133
|
+
def diry:int 1
|
|
4134
|
+
def done:boolean false
|
|
4135
|
+
def guard:int 0
|
|
4136
|
+
def limit:int ((bm.w * bm.h) * 8)
|
|
4137
|
+
if (limit < 64) {
|
|
4138
|
+
limit = 64
|
|
4139
|
+
}
|
|
4140
|
+
while (done == false) {
|
|
4141
|
+
if (guard >= limit) {
|
|
4142
|
+
done = true
|
|
4143
|
+
} {
|
|
4144
|
+
push path.pts (EvgTracePoint.ofInt(x y))
|
|
4145
|
+
if (x > path.maxX) {
|
|
4146
|
+
path.maxX = x
|
|
4147
|
+
}
|
|
4148
|
+
if (x < path.minX) {
|
|
4149
|
+
path.minX = x
|
|
4150
|
+
}
|
|
4151
|
+
if (y > path.maxY) {
|
|
4152
|
+
path.maxY = y
|
|
4153
|
+
}
|
|
4154
|
+
if (y < path.minY) {
|
|
4155
|
+
path.minY = y
|
|
4156
|
+
}
|
|
4157
|
+
x = (x + dirx)
|
|
4158
|
+
y = (y + diry)
|
|
4159
|
+
path.area = (path.area - (x * diry))
|
|
4160
|
+
if ((x == x0) && (y == y0)) {
|
|
4161
|
+
done = true
|
|
4162
|
+
} {
|
|
4163
|
+
; Left / right samples in the half-cell neighbours of the step.
|
|
4164
|
+
def lx:int (EvgBitmapTracer.idivTowardZero(((dirx + diry) - 1) 2))
|
|
4165
|
+
def ly:int (EvgBitmapTracer.idivTowardZero(((diry - dirx) - 1) 2))
|
|
4166
|
+
def rx:int (EvgBitmapTracer.idivTowardZero(((dirx - diry) - 1) 2))
|
|
4167
|
+
def ry:int (EvgBitmapTracer.idivTowardZero(((diry + dirx) - 1) 2))
|
|
4168
|
+
def l:boolean (bm.at((x + lx) (y + ly)))
|
|
4169
|
+
def r:boolean (bm.at((x + rx) (y + ry)))
|
|
4170
|
+
if r {
|
|
4171
|
+
if (l == false) {
|
|
4172
|
+
if (this.shouldTurnRight(bm path x y)) {
|
|
4173
|
+
def tmp:int dirx
|
|
4174
|
+
dirx = (0 - diry)
|
|
4175
|
+
diry = tmp
|
|
4176
|
+
} {
|
|
4177
|
+
def tmp2:int dirx
|
|
4178
|
+
dirx = diry
|
|
4179
|
+
diry = (0 - tmp2)
|
|
4180
|
+
}
|
|
4181
|
+
} {
|
|
4182
|
+
def tmp3:int dirx
|
|
4183
|
+
dirx = (0 - diry)
|
|
4184
|
+
diry = tmp3
|
|
4185
|
+
}
|
|
4186
|
+
} {
|
|
4187
|
+
if (l == false) {
|
|
4188
|
+
def tmp4:int dirx
|
|
4189
|
+
dirx = diry
|
|
4190
|
+
diry = (0 - tmp4)
|
|
4191
|
+
}
|
|
4192
|
+
}
|
|
4193
|
+
}
|
|
4194
|
+
guard = (guard + 1)
|
|
4195
|
+
}
|
|
4196
|
+
}
|
|
4197
|
+
return path
|
|
4198
|
+
}
|
|
4199
|
+
|
|
4200
|
+
fn xorPath:void (bm:EvgBinaryBitmap path:EvgTraceRing) {
|
|
4201
|
+
def n:int (path.len())
|
|
4202
|
+
if (n < 2) {
|
|
4203
|
+
return
|
|
4204
|
+
}
|
|
4205
|
+
def p0:EvgTracePoint (itemAt path.pts 0)
|
|
4206
|
+
def y1:int (to_int p0.y)
|
|
4207
|
+
def i:int 1
|
|
4208
|
+
while (i < n) {
|
|
4209
|
+
def p:EvgTracePoint (itemAt path.pts i)
|
|
4210
|
+
def x:int (to_int p.x)
|
|
4211
|
+
def y:int (to_int p.y)
|
|
4212
|
+
if (y != y1) {
|
|
4213
|
+
def minY:int y1
|
|
4214
|
+
if (y < y1) {
|
|
4215
|
+
minY = y
|
|
4216
|
+
}
|
|
4217
|
+
def j:int x
|
|
4218
|
+
while (j < path.maxX) {
|
|
4219
|
+
bm.flip(j minY)
|
|
4220
|
+
j = (j + 1)
|
|
4221
|
+
}
|
|
4222
|
+
y1 = y
|
|
4223
|
+
}
|
|
4224
|
+
i = (i + 1)
|
|
4225
|
+
}
|
|
4226
|
+
}
|
|
4227
|
+
|
|
4228
|
+
; ----------------------------------------------------------------------
|
|
4229
|
+
; 2–4. Optimal polygon + alphamax smooth + optiCurve → path commands
|
|
4230
|
+
; ----------------------------------------------------------------------
|
|
4231
|
+
|
|
4232
|
+
; When optcurve is false: emit the optimal polygon as straight edges.
|
|
4233
|
+
; When true: alphamax smooth followed by optiCurve (Potrace default).
|
|
4234
|
+
fn emitCommands:void () {
|
|
4235
|
+
def out:[PathCommand]
|
|
4236
|
+
def ri:int 0
|
|
4237
|
+
while (ri < (array_length rings)) {
|
|
4238
|
+
def ring:EvgTraceRing (itemAt rings ri)
|
|
4239
|
+
def poly:[EvgTracePoint] (EvgTraceFit.fitRing(ring))
|
|
4240
|
+
def n:int (array_length poly)
|
|
4241
|
+
if (n >= 3) {
|
|
4242
|
+
; Potrace reverses negative (hole) polygons so evenodd winding matches.
|
|
4243
|
+
if (ring.sign == "-") {
|
|
4244
|
+
def rev:[EvgTracePoint]
|
|
4245
|
+
def ri2:int (n - 1)
|
|
4246
|
+
while (ri2 >= 0) {
|
|
4247
|
+
push rev (itemAt poly ri2)
|
|
4248
|
+
ri2 = (ri2 - 1)
|
|
4249
|
+
}
|
|
4250
|
+
poly = rev
|
|
4251
|
+
}
|
|
4252
|
+
if options.optcurve {
|
|
4253
|
+
def curve:EvgTraceCurve (EvgTraceCurve.fromPolygon(poly options.alphamax))
|
|
4254
|
+
def opt:EvgTraceCurve (curve.optimize(options.opttolerance))
|
|
4255
|
+
opt.emit(out)
|
|
4256
|
+
} {
|
|
4257
|
+
this.appendPolygon(out poly)
|
|
4258
|
+
}
|
|
4259
|
+
}
|
|
4260
|
+
ri = (ri + 1)
|
|
4261
|
+
}
|
|
4262
|
+
commands = out
|
|
4263
|
+
}
|
|
4264
|
+
|
|
4265
|
+
fn appendPolygon:void (out:[PathCommand] poly:[EvgTracePoint]) {
|
|
4266
|
+
def n:int (array_length poly)
|
|
4267
|
+
def p0:EvgTracePoint (itemAt poly 0)
|
|
4268
|
+
push out (VectorShapes.moveTo(p0.x p0.y))
|
|
4269
|
+
def i:int 1
|
|
4270
|
+
while (i < n) {
|
|
4271
|
+
def p:EvgTracePoint (itemAt poly i)
|
|
4272
|
+
push out (VectorShapes.lineTo(p.x p.y))
|
|
4273
|
+
i = (i + 1)
|
|
4274
|
+
}
|
|
4275
|
+
push out (VectorShapes.closePath())
|
|
4276
|
+
}
|
|
4277
|
+
}
|