ranger-compiler 3.5.0 → 3.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (338) hide show
  1. package/CHANGELOG.md +1160 -19
  2. package/LICENSE +3 -1
  3. package/README.md +124 -29
  4. package/dist/Lang.rgr +1188 -290
  5. package/dist/api.d.ts +2343 -879
  6. package/dist/api.js +79924 -56922
  7. package/dist/lib/JSON.rgr +102 -91
  8. package/dist/lib/Shell.rgr +4 -4
  9. package/dist/lib/apple/AppleToolchain.rgr +4 -4
  10. package/dist/lib/apple/README.md +5 -4
  11. package/dist/lib/apple/apple_test.rgr +1 -1
  12. package/dist/lib/core/README.md +1 -1
  13. package/dist/lib/evg/EVG.rgr +12 -0
  14. package/dist/lib/evg/EVGA11yFromTree.rgr +302 -0
  15. package/dist/lib/evg/EVGA11yTree.rgr +894 -0
  16. package/dist/lib/evg/EVGBox.rgr +267 -0
  17. package/dist/lib/evg/EVGBoxShorthandTest.rgr +220 -0
  18. package/dist/lib/evg/EVGCodepoint.rgr +316 -0
  19. package/dist/lib/evg/EVGColor.rgr +700 -0
  20. package/dist/lib/evg/EVGCommands.rgr +177 -0
  21. package/dist/lib/evg/EVGComponent.rgr +331 -0
  22. package/dist/lib/evg/EVGComponentTest.rgr +387 -0
  23. package/dist/lib/evg/EVGConnector.rgr +541 -0
  24. package/dist/lib/evg/EVGConnectorTest.rgr +306 -0
  25. package/dist/lib/evg/EVGDisplayList.rgr +4186 -0
  26. package/dist/lib/evg/EVGEasing.rgr +370 -0
  27. package/dist/lib/evg/EVGEffectTest.rgr +253 -0
  28. package/dist/lib/evg/EVGElement.rgr +4914 -0
  29. package/dist/lib/evg/EVGFixedTest.rgr +289 -0
  30. package/dist/lib/evg/EVGFlexRulesTest.rgr +317 -0
  31. package/dist/lib/evg/EVGFlexWrapTest.rgr +246 -0
  32. package/dist/lib/evg/EVGFling.rgr +244 -0
  33. package/dist/lib/evg/EVGFocus.rgr +400 -0
  34. package/dist/lib/evg/EVGFocusTest.rgr +399 -0
  35. package/dist/lib/evg/EVGGradient.rgr +322 -0
  36. package/dist/lib/evg/EVGGrapheme.rgr +190 -0
  37. package/dist/lib/evg/EVGGrid.rgr +975 -0
  38. package/dist/lib/evg/EVGHitTest.rgr +208 -0
  39. package/dist/lib/evg/EVGHoles.rgr +294 -0
  40. package/dist/lib/evg/EVGHostMeasurerTest.rgr +281 -0
  41. package/dist/lib/evg/EVGHostTextMeasurer.rgr +331 -0
  42. package/dist/lib/evg/EVGHostTree.rgr +834 -0
  43. package/dist/lib/evg/EVGHostTreeTest.rgr +447 -0
  44. package/dist/lib/evg/EVGImageDecode.rgr +117 -0
  45. package/dist/lib/evg/EVGImageMeasurer.rgr +86 -0
  46. package/dist/lib/evg/EVGInspect.rgr +883 -0
  47. package/dist/lib/evg/EVGInvalidateTest.rgr +432 -0
  48. package/dist/lib/evg/EVGJsonTest.rgr +535 -0
  49. package/dist/lib/evg/EVGLayout.rgr +4220 -0
  50. package/dist/lib/evg/EVGMeasure.rgr +1267 -0
  51. package/dist/lib/evg/EVGOverlayTest.rgr +626 -0
  52. package/dist/lib/evg/EVGPatch.rgr +1721 -0
  53. package/dist/lib/evg/EVGPatchTest.rgr +692 -0
  54. package/dist/lib/evg/EVGPopoverTest.rgr +395 -0
  55. package/dist/lib/evg/EVGReconcile.rgr +226 -0
  56. package/dist/lib/evg/EVGReconcileTest.rgr +421 -0
  57. package/dist/lib/evg/EVGReject.rgr +126 -0
  58. package/dist/lib/evg/EVGRelayoutTest.rgr +375 -0
  59. package/dist/lib/evg/EVGRtlLayoutTest.rgr +319 -0
  60. package/dist/lib/evg/EVGRuler.rgr +232 -0
  61. package/dist/lib/evg/EVGRulerTest.rgr +172 -0
  62. package/dist/lib/evg/EVGSelectChrome.rgr +178 -0
  63. package/dist/lib/evg/EVGStyleCacheTest.rgr +419 -0
  64. package/dist/lib/evg/EVGStyleSheet.rgr +2148 -0
  65. package/dist/lib/evg/EVGStyleStateTest.rgr +407 -0
  66. package/dist/lib/evg/EVGStyleVarTest.rgr +471 -0
  67. package/dist/lib/evg/EVGText.rgr +105 -0
  68. package/dist/lib/evg/EVGTextEngine.rgr +536 -0
  69. package/dist/lib/evg/EVGTextMeasurer.rgr +720 -0
  70. package/dist/lib/evg/EVGTimingTest.rgr +1417 -0
  71. package/dist/lib/evg/EVGToolbar.rgr +1369 -0
  72. package/dist/lib/evg/EVGTransition.rgr +728 -0
  73. package/dist/lib/evg/EVGTreeJson.rgr +721 -0
  74. package/dist/lib/evg/EVGUnit.rgr +554 -0
  75. package/dist/lib/evg/EVGViewportUnitTest.rgr +236 -0
  76. package/dist/lib/evg/EvgApp.rgr +189 -0
  77. package/dist/lib/evg/EvgBitmapTracer.rgr +4277 -0
  78. package/dist/lib/evg/EvgBitmapTracerTest.rgr +2119 -0
  79. package/dist/lib/evg/EvgHost.rgr +451 -0
  80. package/dist/lib/evg/EvgTest.rgr +91 -0
  81. package/dist/lib/evg/EvgTraceColor.rgr +236 -0
  82. package/dist/lib/evg/EvgTraceCurve.rgr +648 -0
  83. package/dist/lib/evg/EvgTraceFit.rgr +767 -0
  84. package/dist/lib/evg/EvgTracePath.rgr +435 -0
  85. package/dist/lib/evg/EvgTraceTypes.rgr +559 -0
  86. package/dist/lib/evg/EvgViewport.rgr +313 -0
  87. package/dist/lib/evg/FxDemoDoc.rgr +185 -0
  88. package/dist/lib/evg/HOSTS.md +227 -0
  89. package/dist/lib/evg/ISSUES.md +835 -0
  90. package/dist/lib/evg/PLAN_ACCESSIBILITY.md +650 -0
  91. package/dist/lib/evg/PLAN_CSS_LAYOUT_AND_FONTS.md +1124 -0
  92. package/dist/lib/evg/PLAN_EFFECTS.md +431 -0
  93. package/dist/lib/evg/PLAN_EVG.md +518 -0
  94. package/dist/lib/evg/PLAN_EVG_RENDERER.md +1674 -0
  95. package/dist/lib/evg/PLAN_INSPECTOR.md +788 -0
  96. package/dist/lib/evg/PLAN_LINKS_AND_FORMS.md +157 -0
  97. package/dist/lib/evg/PLAN_NATIVE_HOSTS.md +671 -0
  98. package/dist/lib/evg/PLAN_VECTOR_IR.md +664 -0
  99. package/dist/lib/evg/PLAN_VIEW_TRANSFORM.md +322 -0
  100. package/dist/lib/evg/PathBuilder.rgr +333 -0
  101. package/dist/lib/evg/README.md +1236 -0
  102. package/dist/lib/evg/SPEC.md +1252 -0
  103. package/dist/lib/evg/SVGPathParser.rgr +1199 -0
  104. package/dist/lib/evg/SvgParser.rgr +1736 -0
  105. package/dist/lib/evg/TODO_EVG_RENDERER.md +595 -0
  106. package/dist/lib/evg/VectorShapes.rgr +331 -0
  107. package/dist/lib/evg/VectorStroke.rgr +107 -0
  108. package/dist/lib/evg/VectorViewBox.rgr +380 -0
  109. package/dist/lib/evg/agent/README.md +290 -0
  110. package/dist/lib/evg/agent/evg_agent.rgr +899 -0
  111. package/dist/lib/evg/agent/fixtures/broken.evg.json +7 -0
  112. package/dist/lib/evg/agent/fixtures/card.evg.json +9 -0
  113. package/dist/lib/evg/agent/fixtures/connector.css +63 -0
  114. package/dist/lib/evg/agent/fixtures/connector.evg.json +14 -0
  115. package/dist/lib/evg/agent/fixtures/drawn.evg.json +129 -0
  116. package/dist/lib/evg/agent/fixtures/gradient.evg.json +4 -0
  117. package/dist/lib/evg/agent/fixtures/popover.css +99 -0
  118. package/dist/lib/evg/agent/fixtures/popover.evg.json +30 -0
  119. package/dist/lib/evg/agent/roundtrip.sh +74 -0
  120. package/dist/lib/evg/agent/smoke.sh +230 -0
  121. package/dist/lib/evg/android/README.md +97 -0
  122. package/dist/lib/evg/android/androidstubs/AndroidStubs.kt +175 -0
  123. package/dist/lib/evg/android/androidstubs/Annotation.kt +10 -0
  124. package/dist/lib/evg/android/androidstubs/App.kt +30 -0
  125. package/dist/lib/evg/android/androidstubs/Content.kt +41 -0
  126. package/dist/lib/evg/android/androidstubs/ContentRes.kt +12 -0
  127. package/dist/lib/evg/android/androidstubs/InputMethod.kt +46 -0
  128. package/dist/lib/evg/android/androidstubs/Net.kt +6 -0
  129. package/dist/lib/evg/android/androidstubs/Os.kt +30 -0
  130. package/dist/lib/evg/android/androidstubs/Util.kt +15 -0
  131. package/dist/lib/evg/android/androidstubs/View.kt +113 -0
  132. package/dist/lib/evg/android/androidstubs/Widget.kt +16 -0
  133. package/dist/lib/evg/android/src/android/kotlin/fi/ranger/evg/AndroidEvgSurface.kt +323 -0
  134. package/dist/lib/evg/android/src/android/kotlin/fi/ranger/evg/AndroidTextMeasurer.kt +56 -0
  135. package/dist/lib/evg/android/src/android/kotlin/fi/ranger/evg/RippleEffect.kt +262 -0
  136. package/dist/lib/evg/android/src/awt/kotlin/fi/ranger/evg/AwtEvgSurface.kt +238 -0
  137. package/dist/lib/evg/android/src/awt/kotlin/fi/ranger/evg/AwtTextMeasurer.kt +85 -0
  138. package/dist/lib/evg/android/src/main/kotlin/fi/ranger/evg/EvgEngineThread.kt +115 -0
  139. package/dist/lib/evg/android/src/main/kotlin/fi/ranger/evg/EvgPainter.kt +231 -0
  140. package/dist/lib/evg/android/src/main/kotlin/fi/ranger/evg/EvgSurface.kt +117 -0
  141. package/dist/lib/evg/android/src/main/kotlin/fi/ranger/evg/RecordingSurface.kt +94 -0
  142. package/dist/lib/evg/apple/README.md +128 -0
  143. package/dist/lib/evg/apple/Sources/CoreGraphicsEvgSurface.swift +329 -0
  144. package/dist/lib/evg/apple/Sources/CoreTextMeasurer.swift +70 -0
  145. package/dist/lib/evg/apple/Sources/EvgEngineQueue.swift +105 -0
  146. package/dist/lib/evg/apple/Sources/EvgPainter.swift +225 -0
  147. package/dist/lib/evg/apple/Sources/EvgSurface.swift +163 -0
  148. package/dist/lib/evg/apple/Sources/RecordingSurface.swift +110 -0
  149. package/dist/lib/evg/bench/EvgLayoutBench.rgr +34 -0
  150. package/dist/lib/evg/bench/README.md +64 -0
  151. package/dist/lib/evg/bench/layout-bench.mjs +316 -0
  152. package/dist/lib/evg/bench/layout-cases.mjs +510 -0
  153. package/dist/lib/evg/bench/layout-conformance.mjs +254 -0
  154. package/dist/lib/evg/bin/.gitignore +17 -0
  155. package/dist/lib/evg/evg_test.rgr +313 -0
  156. package/dist/lib/evg/gl/README.md +198 -0
  157. package/dist/lib/evg/gl/a11y-paint-check.mjs +73 -0
  158. package/dist/lib/evg/gl/blur-check.mjs +399 -0
  159. package/dist/lib/evg/gl/boxmodel.json +1 -0
  160. package/dist/lib/evg/gl/demo.html +46 -0
  161. package/dist/lib/evg/gl/effect-presets.css +285 -0
  162. package/dist/lib/evg/gl/effect-presets.js +63 -0
  163. package/dist/lib/evg/gl/effect-shots.mjs +135 -0
  164. package/dist/lib/evg/gl/evg-a11y.js +566 -0
  165. package/dist/lib/evg/gl/evg-binary.js +160 -0
  166. package/dist/lib/evg/gl/evg-engine.js +296 -0
  167. package/dist/lib/evg/gl/evg-fx.js +237 -0
  168. package/dist/lib/evg/gl/evg-gestures.js +209 -0
  169. package/dist/lib/evg/gl/evg-list.js +167 -0
  170. package/dist/lib/evg/gl/evg-measure.js +186 -0
  171. package/dist/lib/evg/gl/evg-textinput.js +303 -0
  172. package/dist/lib/evg/gl/evg-view.js +144 -0
  173. package/dist/lib/evg/gl/evg-webgl.js +3942 -0
  174. package/dist/lib/evg/gl/fx-check.mjs +998 -0
  175. package/dist/lib/evg/gl/fx-demo.html +166 -0
  176. package/dist/lib/evg/gl/fx-demo.js +2 -0
  177. package/dist/lib/evg/gl/fx-serve.mjs +47 -0
  178. package/dist/lib/evg/gl/gestures-check.mjs +189 -0
  179. package/dist/lib/evg/gl/list-binary-check.mjs +152 -0
  180. package/dist/lib/evg/gl/measure-check.mjs +135 -0
  181. package/dist/lib/evg/gl/rotation-check.mjs +207 -0
  182. package/dist/lib/evg/gl/shift-check.mjs +113 -0
  183. package/dist/lib/evg/gl/stroke-check.mjs +168 -0
  184. package/dist/lib/evg/gl/text-snap-check.mjs +139 -0
  185. package/dist/lib/evg/gl/view-check.mjs +343 -0
  186. package/dist/lib/evg/gl/view-policy-check.mjs +184 -0
  187. package/dist/lib/evg/html/evg-dom.js +318 -0
  188. package/dist/lib/evg/html/evg-html.js +600 -0
  189. package/dist/lib/evg/inspect/README.md +415 -0
  190. package/dist/lib/evg/inspect/browser-smoke.mjs +115 -0
  191. package/dist/lib/evg/inspect/evg-inspect.js +947 -0
  192. package/dist/lib/evg/inspect/shots/css.png +0 -0
  193. package/dist/lib/evg/inspect/shots/dashboard.png +0 -0
  194. package/dist/lib/evg/inspect/shots/pptx-slide.png +0 -0
  195. package/dist/lib/evg/inspect/shots/state.png +0 -0
  196. package/dist/lib/evg/inspect/shots.mjs +226 -0
  197. package/dist/lib/evg/oracle/css-blur.json +576 -0
  198. package/dist/lib/evg/oracle/css-box.json +157 -0
  199. package/dist/lib/evg/oracle/css-timing.json +709 -0
  200. package/dist/lib/evg/oracle/css_blur_oracle.mjs +529 -0
  201. package/dist/lib/evg/oracle/css_box_oracle.mjs +106 -0
  202. package/dist/lib/evg/oracle/css_timing_oracle.mjs +389 -0
  203. package/dist/lib/evg/original/EVGColor.clj +310 -0
  204. package/dist/lib/evg/original/EVGColorContext.rgr +178 -0
  205. package/dist/lib/evg/original/SVGPath.rgr +627 -0
  206. package/dist/lib/evg/original/Vec2.crgr +103 -0
  207. package/dist/lib/evg/ranger.json +9 -0
  208. package/dist/lib/evg/showcase/README.md +246 -0
  209. package/dist/lib/evg/showcase/assets/emblem.svg +30 -0
  210. package/dist/lib/evg/showcase/assets/rosette.svg +22 -0
  211. package/dist/lib/evg/showcase/build.mjs +651 -0
  212. package/dist/lib/evg/showcase/pages/album.tsx +30 -0
  213. package/dist/lib/evg/showcase/pages/boxmodel.tsx +37 -0
  214. package/dist/lib/evg/showcase/pages/cards.tsx +41 -0
  215. package/dist/lib/evg/showcase/pages/chart_api.tsx +170 -0
  216. package/dist/lib/evg/showcase/pages/charts.tsx +219 -0
  217. package/dist/lib/evg/showcase/pages/drawing.tsx +160 -0
  218. package/dist/lib/evg/showcase/pages/emoji.tsx +91 -0
  219. package/dist/lib/evg/showcase/pages/flex.tsx +46 -0
  220. package/dist/lib/evg/showcase/pages/more.tsx +332 -0
  221. package/dist/lib/evg/showcase/pages/plots.tsx +262 -0
  222. package/dist/lib/evg/showcase/pages/svg.tsx +63 -0
  223. package/dist/lib/evg/showcase/pages/tables.tsx +209 -0
  224. package/dist/lib/evg/showcase/pages/typography.tsx +43 -0
  225. package/dist/lib/evg/showcase/pages/units.tsx +35 -0
  226. package/dist/lib/evg/showcase/pages/variants.tsx +233 -0
  227. package/dist/lib/evg/showcase/pages/vector.tsx +75 -0
  228. package/dist/lib/evg/showcase/pages/views.tsx +223 -0
  229. package/dist/lib/evg/showcase/tests/chart_api_smoke.mjs +339 -0
  230. package/dist/lib/evg/showcase/tests/gl_smoke.mjs +125 -0
  231. package/dist/lib/evg/showcase/themes/chart_api-default.css +15 -0
  232. package/dist/lib/evg/showcase/themes/charts-default.css +29 -0
  233. package/dist/lib/evg/showcase/themes/drawing-default.css +35 -0
  234. package/dist/lib/evg/showcase/themes/more-default.css +128 -0
  235. package/dist/lib/evg/showcase/themes/plots-default.css +18 -0
  236. package/dist/lib/evg/showcase/themes/showcase.css +508 -0
  237. package/dist/lib/evg/showcase/themes/tables-default.css +123 -0
  238. package/dist/lib/evg/showcase/themes/variants-default.css +15 -0
  239. package/dist/lib/evg/showcase/themes/views-default.css +15 -0
  240. package/dist/lib/evg/tools/bench_vs_potrace.mjs +290 -0
  241. package/dist/lib/evg/tools/evg_image_tool.rgr +488 -0
  242. package/dist/lib/evg/tools/evg_trace_bench.rgr +114 -0
  243. package/dist/lib/evg/tools/evg_trace_cli.rgr +662 -0
  244. package/dist/lib/evg/tools/evg_trace_cpp_bench.rgr +83 -0
  245. package/dist/lib/evg/tools/run_trace_bench.sh +74 -0
  246. package/dist/lib/evg/tools/run_trace_cli_smoke.sh +128 -0
  247. package/dist/lib/evg/web/responsive/EvgResponsiveCheck.rgr +271 -0
  248. package/dist/lib/evg/web/responsive/EvgResponsiveDemo.rgr +531 -0
  249. package/dist/lib/evg/web/responsive/README.md +97 -0
  250. package/dist/lib/evg/web/responsive/build.mjs +90 -0
  251. package/dist/lib/evg/web/responsive/dom-check.mjs +169 -0
  252. package/dist/lib/evg/web/responsive/index.html +177 -0
  253. package/dist/lib/evg/web/responsive/smoke.mjs +221 -0
  254. package/dist/lib/evg/web/tools/assets-client.mjs +95 -0
  255. package/dist/lib/evg/web/tools/boot-bench.mjs +189 -0
  256. package/dist/lib/evg/web/tools/inline-assets.mjs +119 -0
  257. package/dist/lib/evg/web/tools/minify.mjs +60 -0
  258. package/dist/lib/evg/web/tracer/build.mjs +87 -0
  259. package/dist/lib/evg/web/tracer/index.html +2805 -0
  260. package/dist/lib/evg/web/tracer/sample.png +0 -0
  261. package/dist/lib/evg/web/tracer/smoke.mjs +1162 -0
  262. package/dist/lib/evgr/Cargo.lock +21 -0
  263. package/dist/lib/evgr/Cargo.toml +19 -0
  264. package/dist/lib/evgr/README.md +140 -0
  265. package/dist/lib/evgr/bench/NativeBench.rgr +246 -0
  266. package/dist/lib/evgr/bench/compare.mjs +252 -0
  267. package/dist/lib/evgr/bench/speed.mjs +277 -0
  268. package/dist/lib/evgr/bin/.gitignore +3 -0
  269. package/dist/lib/evgr/src/bin/bench.rs +106 -0
  270. package/dist/lib/evgr/src/bin/smoke.rs +13 -0
  271. package/dist/lib/evgr/src/grid.rs +730 -0
  272. package/dist/lib/evgr/src/lib.rs +1003 -0
  273. package/dist/lib/evgr/src/style.rs +468 -0
  274. package/dist/lib/evgr/src/text.rs +83 -0
  275. package/dist/lib/image/BitReader.rgr +171 -0
  276. package/dist/lib/image/Buffer.rgr +173 -0
  277. package/dist/lib/image/DCT.rgr +283 -0
  278. package/dist/lib/image/Deflate.rgr +339 -0
  279. package/dist/lib/image/HuffmanDecoder.rgr +181 -0
  280. package/dist/lib/image/ImageBuffer.rgr +706 -0
  281. package/dist/lib/image/JPEGDecoder.rgr +808 -0
  282. package/dist/lib/image/PNGDecoder.rgr +544 -0
  283. package/dist/lib/image/PNGEncoder.rgr +388 -0
  284. package/dist/lib/image/PPMImage.rgr +224 -0
  285. package/dist/lib/image/ProgressiveJPEGDecoder.rgr +1154 -0
  286. package/dist/lib/image/README.md +33 -0
  287. package/dist/lib/image/RasterBuffer.rgr +294 -0
  288. package/dist/lib/image/VP8BoolDecoder.rgr +163 -0
  289. package/dist/lib/image/WebPDecoder.rgr +433 -0
  290. package/dist/lib/image/WebPLossless.rgr +1035 -0
  291. package/dist/lib/image/WebPLossy.rgr +1929 -0
  292. package/dist/lib/image/ranger.json +9 -0
  293. package/dist/lib/image/testdata/webp/anim_2f_24x16.webp +0 -0
  294. package/dist/lib/image/testdata/webp/fixtures.txt +33 -0
  295. package/dist/lib/image/testdata/webp/gen_fixtures.py +261 -0
  296. package/dist/lib/image/testdata/webp/ll_1x1.webp +0 -0
  297. package/dist/lib/image/testdata/webp/ll_alpha_29x21.webp +0 -0
  298. package/dist/lib/image/testdata/webp/ll_gradient_64x64.webp +0 -0
  299. package/dist/lib/image/testdata/webp/ll_meta_32x32.webp +0 -0
  300. package/dist/lib/image/testdata/webp/ll_noise_17x9.webp +0 -0
  301. package/dist/lib/image/testdata/webp/ll_pal11_23x11.webp +0 -0
  302. package/dist/lib/image/testdata/webp/ll_pal2_19x7.webp +0 -0
  303. package/dist/lib/image/testdata/webp/ll_pal40_16x16.webp +0 -0
  304. package/dist/lib/image/testdata/webp/ll_pal4_13x5.webp +0 -0
  305. package/dist/lib/image/testdata/webp/ll_photo_40x30.webp +0 -0
  306. package/dist/lib/image/testdata/webp/ll_predictors_64x32.webp +0 -0
  307. package/dist/lib/image/testdata/webp/ly_1x1.webp +0 -0
  308. package/dist/lib/image/testdata/webp/ly_alph_raw_f0_21x13.webp +0 -0
  309. package/dist/lib/image/testdata/webp/ly_alph_raw_f1_21x13.webp +0 -0
  310. package/dist/lib/image/testdata/webp/ly_alph_raw_f2_21x13.webp +0 -0
  311. package/dist/lib/image/testdata/webp/ly_alph_raw_f3_21x13.webp +0 -0
  312. package/dist/lib/image/testdata/webp/ly_alph_vp8l_f0_21x13.webp +0 -0
  313. package/dist/lib/image/testdata/webp/ly_alph_vp8l_f1_21x13.webp +0 -0
  314. package/dist/lib/image/testdata/webp/ly_alph_vp8l_f2_21x13.webp +0 -0
  315. package/dist/lib/image/testdata/webp/ly_alph_vp8l_f3_21x13.webp +0 -0
  316. package/dist/lib/image/testdata/webp/ly_alpha_33x17.webp +0 -0
  317. package/dist/lib/image/testdata/webp/ly_exif_15x11.webp +0 -0
  318. package/dist/lib/image/testdata/webp/ly_i16_96x80.webp +0 -0
  319. package/dist/lib/image/testdata/webp/ly_nofilter_24x20.webp +0 -0
  320. package/dist/lib/image/testdata/webp/ly_odd_17x9.webp +0 -0
  321. package/dist/lib/image/testdata/webp/ly_photo_48x40.webp +0 -0
  322. package/dist/lib/image/testdata/webp/ly_q100_20x18.webp +0 -0
  323. package/dist/lib/image/testdata/webp/ly_sharp_40x36.webp +0 -0
  324. package/dist/lib/image/testdata/webp/ly_simple_filter_33x31.webp +0 -0
  325. package/dist/lib/image/testdata/webp/ly_vpx_parts8_37x45.webp +0 -0
  326. package/dist/lib/image/testdata/webp/ly_vpx_skip_96x80.webp +0 -0
  327. package/dist/lib/image/tests/WebPDecodeTool.rgr +81 -0
  328. package/dist/lib/image/tests/WebPDecoderTest.rgr +1162 -0
  329. package/dist/lib/image/tests/run_webp_tests.sh +39 -0
  330. package/dist/lib/rust/RsJson.rgr +468 -0
  331. package/dist/lib/rust/RsPrelude.rgr +1663 -0
  332. package/dist/lib/shell_test.rgr +7 -7
  333. package/dist/lib/stdlib.rgr +21 -5
  334. package/dist/lib/zip/ranger.json +6 -0
  335. package/dist/rgrc.js +87318 -67610
  336. package/package.json +1010 -1040
  337. package/dist/README.md +0 -117
  338. 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
+ }