ocdkit 0.0.3__tar.gz → 0.0.5__tar.gz

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 (219) hide show
  1. {ocdkit-0.0.3 → ocdkit-0.0.5}/.gitignore +11 -0
  2. {ocdkit-0.0.3/src/ocdkit.egg-info → ocdkit-0.0.5}/PKG-INFO +4 -2
  3. ocdkit-0.0.5/badges/coverage.svg +1 -0
  4. ocdkit-0.0.5/badges/tests.svg +1 -0
  5. ocdkit-0.0.5/docs/plot-backend-roadmap.md +95 -0
  6. {ocdkit-0.0.3 → ocdkit-0.0.5}/pyproject.toml +11 -6
  7. ocdkit-0.0.5/scripts/bench_contour_alignment.py +81 -0
  8. ocdkit-0.0.5/scripts/bench_hdr_cmap.py +181 -0
  9. ocdkit-0.0.5/scripts/bench_vector_contours.py +320 -0
  10. ocdkit-0.0.5/scripts/bench_vector_contours_tier2.py +108 -0
  11. ocdkit-0.0.5/scripts/check_compare_pixels.py +77 -0
  12. ocdkit-0.0.5/scripts/check_hdr_cmap.py +65 -0
  13. ocdkit-0.0.5/scripts/check_jxl_p3_bytes.py +64 -0
  14. ocdkit-0.0.5/scripts/check_playwright_render.py +136 -0
  15. ocdkit-0.0.5/scripts/check_uhdr_sdr_base.py +73 -0
  16. ocdkit-0.0.5/scripts/coverage_cross_device.env.example +11 -0
  17. ocdkit-0.0.5/scripts/coverage_cross_device.sh +56 -0
  18. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/array/ops.py +25 -3
  19. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/array/parallel.py +22 -2
  20. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/cli/paths.py +1 -1
  21. ocdkit-0.0.5/src/ocdkit/io/figure.py +2897 -0
  22. ocdkit-0.0.5/src/ocdkit/io/figure_server.py +1210 -0
  23. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/load/module.py +22 -3
  24. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/logging/handler.py +43 -2
  25. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/measure/bbox.py +57 -0
  26. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/measure/medoid.py +1 -58
  27. ocdkit-0.0.5/src/ocdkit/plot/__init__.py +58 -0
  28. ocdkit-0.0.5/src/ocdkit/plot/bench.py +208 -0
  29. ocdkit-0.0.5/src/ocdkit/plot/composite.py +141 -0
  30. ocdkit-0.0.5/src/ocdkit/plot/composite_grid.py +371 -0
  31. ocdkit-0.0.5/src/ocdkit/plot/contour.py +716 -0
  32. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/plot/defaults.py +17 -0
  33. ocdkit-0.0.5/src/ocdkit/plot/display.py +534 -0
  34. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/plot/figure.py +5 -2
  35. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/plot/grid.py +6 -2
  36. ocdkit-0.0.5/src/ocdkit/plot/hdr_cmap.py +342 -0
  37. ocdkit-0.0.5/src/ocdkit/plot/image_grid.py +877 -0
  38. ocdkit-0.0.5/src/ocdkit/plot/imports.py +19 -0
  39. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/plot/label.py +3 -0
  40. ocdkit-0.0.5/src/ocdkit/plot/layout.py +589 -0
  41. ocdkit-0.0.5/src/ocdkit/plot/style.py +132 -0
  42. ocdkit-0.0.5/src/ocdkit/plot/svg.py +927 -0
  43. ocdkit-0.0.5/src/ocdkit/plot/text_metrics.py +190 -0
  44. ocdkit-0.0.5/src/ocdkit/testing/__init__.py +55 -0
  45. ocdkit-0.0.5/src/ocdkit/testing/imports.py +237 -0
  46. ocdkit-0.0.5/src/ocdkit/tls/__init__.py +142 -0
  47. ocdkit-0.0.5/src/ocdkit/tls/external_ca.py +104 -0
  48. ocdkit-0.0.5/src/ocdkit/tls/hostnames.py +110 -0
  49. ocdkit-0.0.5/src/ocdkit/tls/imports.py +11 -0
  50. ocdkit-0.0.5/src/ocdkit/tls/local_ca.py +288 -0
  51. ocdkit-0.0.5/src/ocdkit/tls/paths.py +26 -0
  52. ocdkit-0.0.5/src/ocdkit/tls/trust.py +205 -0
  53. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/utils/gpu.py +29 -0
  54. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/utils/kwargs.py +18 -0
  55. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/utils/paths.py +1 -1
  56. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/viewer/app.py +4 -4
  57. {ocdkit-0.0.3 → ocdkit-0.0.5/src/ocdkit.egg-info}/PKG-INFO +4 -2
  58. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit.egg-info/SOURCES.txt +34 -1
  59. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit.egg-info/requires.txt +3 -1
  60. {ocdkit-0.0.3 → ocdkit-0.0.5}/tests/test_array.py +35 -0
  61. ocdkit-0.0.5/tests/test_import_cycles.py +10 -0
  62. {ocdkit-0.0.3 → ocdkit-0.0.5}/tests/test_module_collisions.py +1 -1
  63. ocdkit-0.0.5/tests/test_module_discovery.py +11 -0
  64. {ocdkit-0.0.3 → ocdkit-0.0.5}/tests/test_plot_grid.py +12 -12
  65. ocdkit-0.0.3/scripts/coverage_cross_device.sh +0 -41
  66. ocdkit-0.0.3/src/ocdkit/plot/__init__.py +0 -11
  67. ocdkit-0.0.3/src/ocdkit/plot/contour.py +0 -102
  68. ocdkit-0.0.3/src/ocdkit/plot/display.py +0 -133
  69. ocdkit-0.0.3/src/ocdkit/plot/imports.py +0 -9
  70. ocdkit-0.0.3/src/ocdkit/testing/__init__.py +0 -27
  71. ocdkit-0.0.3/src/ocdkit/tls.py +0 -792
  72. {ocdkit-0.0.3 → ocdkit-0.0.5}/.github/workflows/test_and_deploy.yml +0 -0
  73. {ocdkit-0.0.3 → ocdkit-0.0.5}/LICENSE +0 -0
  74. {ocdkit-0.0.3 → ocdkit-0.0.5}/MANIFEST.in +0 -0
  75. {ocdkit-0.0.3 → ocdkit-0.0.5}/README.md +0 -0
  76. {ocdkit-0.0.3 → ocdkit-0.0.5}/docs/plugin-authoring.md +0 -0
  77. {ocdkit-0.0.3 → ocdkit-0.0.5}/docs/pywebview-desktop-integration.md +0 -0
  78. {ocdkit-0.0.3 → ocdkit-0.0.5}/scripts/bench_colorize.py +0 -0
  79. {ocdkit-0.0.3 → ocdkit-0.0.5}/setup.cfg +0 -0
  80. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/__init__.py +0 -0
  81. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/__main__.py +0 -0
  82. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/array/__init__.py +0 -0
  83. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/array/convert.py +0 -0
  84. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/array/filters.py +0 -0
  85. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/array/imports.py +0 -0
  86. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/array/index.py +0 -0
  87. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/array/morphology.py +0 -0
  88. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/array/normalize.py +0 -0
  89. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/array/spatial.py +0 -0
  90. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/array/transform.py +0 -0
  91. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/array/union_find.py +0 -0
  92. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/array/warp.py +0 -0
  93. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/cli/__init__.py +0 -0
  94. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/cli/__main__.py +0 -0
  95. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/cli/main.py +0 -0
  96. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/cli/migrate.py +0 -0
  97. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/desktop/__init__.py +0 -0
  98. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/desktop/pinning.py +0 -0
  99. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/imports.py +0 -0
  100. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/io/__init__.py +0 -0
  101. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/io/files.py +0 -0
  102. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/io/image.py +0 -0
  103. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/io/imports.py +0 -0
  104. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/io/path.py +0 -0
  105. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/io/result.py +0 -0
  106. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/load/__init__.py +0 -0
  107. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/load/object.py +0 -0
  108. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/logging/__init__.py +0 -0
  109. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/measure/__init__.py +0 -0
  110. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/measure/diameter.py +0 -0
  111. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/measure/imports.py +0 -0
  112. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/measure/metrics.py +0 -0
  113. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/plot/color.py +0 -0
  114. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/plot/export.py +0 -0
  115. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/plot/ncolor.py +0 -0
  116. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/testing/collisions.py +0 -0
  117. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/utils/__init__.py +0 -0
  118. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/utils/collections.py +0 -0
  119. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/viewer/__init__.py +0 -0
  120. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/viewer/__main__.py +0 -0
  121. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/viewer/assets.py +0 -0
  122. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/viewer/cli.py +0 -0
  123. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/viewer/demo.html +0 -0
  124. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/viewer/dependencies.py +0 -0
  125. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/viewer/exceptions.py +0 -0
  126. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/viewer/masks.py +0 -0
  127. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/viewer/middleware.py +0 -0
  128. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/viewer/model_registry.py +0 -0
  129. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/viewer/plugins/__init__.py +0 -0
  130. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/viewer/plugins/base.py +0 -0
  131. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/viewer/plugins/registry.py +0 -0
  132. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/viewer/plugins/schema.py +0 -0
  133. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/viewer/plugins/threshold.py +0 -0
  134. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/viewer/routers/__init__.py +0 -0
  135. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/viewer/routers/index.py +0 -0
  136. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/viewer/routers/log.py +0 -0
  137. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/viewer/routers/mask.py +0 -0
  138. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/viewer/routers/plugin.py +0 -0
  139. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/viewer/routers/segment.py +0 -0
  140. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/viewer/routers/session_routes.py +0 -0
  141. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/viewer/routers/system.py +0 -0
  142. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/viewer/routers/trust.py +0 -0
  143. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/viewer/routes.py +0 -0
  144. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/viewer/sample_image.py +0 -0
  145. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/viewer/schemas.py +0 -0
  146. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/viewer/segmentation.py +0 -0
  147. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/viewer/session.py +0 -0
  148. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/viewer/system.py +0 -0
  149. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/viewer/web/app.js +0 -0
  150. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/viewer/web/css/controls.css +0 -0
  151. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/viewer/web/css/layout.css +0 -0
  152. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/viewer/web/css/tools.css +0 -0
  153. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/viewer/web/css/viewer.css +0 -0
  154. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/viewer/web/html/left-panel.html +0 -0
  155. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/viewer/web/html/sidebar.html +0 -0
  156. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/viewer/web/html/viewer.html +0 -0
  157. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/viewer/web/icons/affinity.svg +0 -0
  158. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/viewer/web/icons/arrow-back-up.svg +0 -0
  159. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/viewer/web/icons/arrow-forward-up.svg +0 -0
  160. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/viewer/web/icons/dbscan-nested-arcs.svg +0 -0
  161. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/viewer/web/icons/download.svg +0 -0
  162. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/viewer/web/icons/droplet-half-2.svg +0 -0
  163. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/viewer/web/icons/eraser.svg +0 -0
  164. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/viewer/web/icons/home-2.svg +0 -0
  165. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/viewer/web/icons/minus.svg +0 -0
  166. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/viewer/web/icons/palette.svg +0 -0
  167. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/viewer/web/icons/pencil.svg +0 -0
  168. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/viewer/web/icons/plus.svg +0 -0
  169. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/viewer/web/icons/rotate-rectangle.svg +0 -0
  170. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/viewer/web/icons/topology-star.svg +0 -0
  171. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/viewer/web/index.html +0 -0
  172. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/viewer/web/js/brush.js +0 -0
  173. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/viewer/web/js/colormap.js +0 -0
  174. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/viewer/web/js/debug-apple-material.js +0 -0
  175. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/viewer/web/js/file-navigation.js +0 -0
  176. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/viewer/web/js/history.js +0 -0
  177. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/viewer/web/js/interactions.js +0 -0
  178. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/viewer/web/js/logging.js +0 -0
  179. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/viewer/web/js/mask-pipeline.js +0 -0
  180. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/viewer/web/js/painting.js +0 -0
  181. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/viewer/web/js/plugin-panel.js +0 -0
  182. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/viewer/web/js/pointer-state.js +0 -0
  183. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/viewer/web/js/state-persistence.js +0 -0
  184. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/viewer/web/js/tooltip-editor.js +0 -0
  185. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/viewer/web/js/ui-utils.js +0 -0
  186. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit/viewer/web/js/wasm_fill.c +0 -0
  187. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit.egg-info/dependency_links.txt +0 -0
  188. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit.egg-info/entry_points.txt +0 -0
  189. {ocdkit-0.0.3 → ocdkit-0.0.5}/src/ocdkit.egg-info/top_level.txt +0 -0
  190. {ocdkit-0.0.3 → ocdkit-0.0.5}/tests/e2e/__init__.py +0 -0
  191. {ocdkit-0.0.3 → ocdkit-0.0.5}/tests/e2e/conftest.py +0 -0
  192. {ocdkit-0.0.3 → ocdkit-0.0.5}/tests/e2e/test_browser_smoke.py +0 -0
  193. {ocdkit-0.0.3 → ocdkit-0.0.5}/tests/e2e/test_pywebview_snapshot.py +0 -0
  194. {ocdkit-0.0.3 → ocdkit-0.0.5}/tests/fixtures/multichan_3c_4x4.czi +0 -0
  195. {ocdkit-0.0.3 → ocdkit-0.0.5}/tests/fixtures/tiny_8x8.czi +0 -0
  196. {ocdkit-0.0.3 → ocdkit-0.0.5}/tests/test_gpu.py +0 -0
  197. {ocdkit-0.0.3 → ocdkit-0.0.5}/tests/test_io.py +0 -0
  198. {ocdkit-0.0.3 → ocdkit-0.0.5}/tests/test_measure.py +0 -0
  199. {ocdkit-0.0.3 → ocdkit-0.0.5}/tests/test_morphology.py +0 -0
  200. {ocdkit-0.0.3 → ocdkit-0.0.5}/tests/test_paths_migration.py +0 -0
  201. {ocdkit-0.0.3 → ocdkit-0.0.5}/tests/test_plot_color.py +0 -0
  202. {ocdkit-0.0.3 → ocdkit-0.0.5}/tests/test_plot_contour.py +0 -0
  203. {ocdkit-0.0.3 → ocdkit-0.0.5}/tests/test_plot_display.py +0 -0
  204. {ocdkit-0.0.3 → ocdkit-0.0.5}/tests/test_plot_export.py +0 -0
  205. {ocdkit-0.0.3 → ocdkit-0.0.5}/tests/test_plot_figure.py +0 -0
  206. {ocdkit-0.0.3 → ocdkit-0.0.5}/tests/test_plot_label.py +0 -0
  207. {ocdkit-0.0.3 → ocdkit-0.0.5}/tests/test_plot_notebook.py +0 -0
  208. {ocdkit-0.0.3 → ocdkit-0.0.5}/tests/test_registration.py +0 -0
  209. {ocdkit-0.0.3 → ocdkit-0.0.5}/tests/test_slice.py +0 -0
  210. {ocdkit-0.0.3 → ocdkit-0.0.5}/tests/test_spatial.py +0 -0
  211. {ocdkit-0.0.3 → ocdkit-0.0.5}/tests/viewer/__init__.py +0 -0
  212. {ocdkit-0.0.3 → ocdkit-0.0.5}/tests/viewer/test_active_plugin_cache.py +0 -0
  213. {ocdkit-0.0.3 → ocdkit-0.0.5}/tests/viewer/test_app.py +0 -0
  214. {ocdkit-0.0.3 → ocdkit-0.0.5}/tests/viewer/test_async_dispatch.py +0 -0
  215. {ocdkit-0.0.3 → ocdkit-0.0.5}/tests/viewer/test_envelope_and_middleware.py +0 -0
  216. {ocdkit-0.0.3 → ocdkit-0.0.5}/tests/viewer/test_plugin_contract.py +0 -0
  217. {ocdkit-0.0.3 → ocdkit-0.0.5}/tests/viewer/test_session_eviction.py +0 -0
  218. {ocdkit-0.0.3 → ocdkit-0.0.5}/tests/viewer/test_title_config.py +0 -0
  219. {ocdkit-0.0.3 → ocdkit-0.0.5}/tests/viewer/test_ui_mode.py +0 -0
@@ -6,6 +6,17 @@
6
6
  # Local tooling
7
7
  .claude/
8
8
 
9
+ # Local machine-specific config for bench / coverage scripts
10
+ scripts/coverage_cross_device.env
11
+ scripts/bench_paths.env
12
+
13
+ # Generated artifacts from bench / demo scripts
14
+ scripts/bench_*.png
15
+ scripts/check_*.png
16
+ scripts/check_*.svg
17
+ scripts/check_*.html
18
+ figures/
19
+
9
20
  # Editor / NAS junk
10
21
  *.bak
11
22
  .!*
@@ -1,16 +1,17 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ocdkit
3
- Version: 0.0.3
3
+ Version: 0.0.5
4
4
  Summary: Obsessive Coder's Dependency Toolkit — Python utilities for array manipulation, GPU dispatch, image I/O, morphology, and plotting.
5
5
  License: BSD-3-Clause
6
6
  Requires-Python: >=3.11
7
7
  Description-Content-Type: text/markdown
8
8
  License-File: LICENSE
9
- Requires-Dist: numpy
9
+ Requires-Dist: numpy>=2.0
10
10
  Requires-Dist: scipy
11
11
  Requires-Dist: scikit-image>=0.26
12
12
  Requires-Dist: tifffile
13
13
  Requires-Dist: imagecodecs
14
+ Requires-Dist: opencodecs>=0.1.12
14
15
  Requires-Dist: matplotlib
15
16
  Requires-Dist: fastremap
16
17
  Requires-Dist: edt
@@ -24,6 +25,7 @@ Requires-Dist: cmap
24
25
  Requires-Dist: tqdm
25
26
  Requires-Dist: platformdirs
26
27
  Requires-Dist: cryptography
28
+ Requires-Dist: lxml
27
29
  Provides-Extra: viewer
28
30
  Requires-Dist: fastapi; extra == "viewer"
29
31
  Requires-Dist: uvicorn[standard]; extra == "viewer"
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="114" height="20" role="img" aria-label="coverage: 44.55%"><title>coverage: 44.55%</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="114" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="61" height="20" fill="#555"/><rect x="61" width="53" height="20" fill="#e05d44"/><rect width="114" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="315" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="510">coverage</text><text x="315" y="140" transform="scale(.1)" fill="#fff" textLength="510">coverage</text><text aria-hidden="true" x="865" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="430">44.55%</text><text x="865" y="140" transform="scale(.1)" fill="#fff" textLength="430">44.55%</text></g></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="68" height="20" role="img" aria-label="tests: 408"><title>tests: 408</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="68" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="37" height="20" fill="#555"/><rect x="37" width="31" height="20" fill="#4c1"/><rect width="68" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="195" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="270">tests</text><text x="195" y="140" transform="scale(.1)" fill="#fff" textLength="270">tests</text><text aria-hidden="true" x="515" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="210">408</text><text x="515" y="140" transform="scale(.1)" fill="#fff" textLength="210">408</text></g></svg>
@@ -0,0 +1,95 @@
1
+ # Plot backend roadmap
2
+
3
+ **Status:** Design discussion / pending decision.
4
+ **Driver:** matplotlib accounts for ~80% of warm-call time in a typical spectra-plotting call (~115 ms of ~140 ms) and ~1.5 s of cold-import overhead. We also want native hover/tooltip behavior like the classification-debugger GUI, which matplotlib can't deliver inline.
5
+
6
+ ## Context
7
+
8
+ `ocdkit.plot` and a downstream plot package together own ~10 K LOC of plotting code:
9
+
10
+ | Package | LOC |
11
+ |---|---|
12
+ | `ocdkit.plot` (figure, grid, label, color, defaults, display, contour, export) | 1,935 |
13
+ | Downstream non-WGPU plot code (datashade, cell, text, barcode, line, background, …) | 4,641 |
14
+ | Downstream WGPU plot code (lines, scatter, aggregators — already custom GPU primitives) | 4,783 |
15
+
16
+ What we still depend on matplotlib for is narrow: 2D Cartesian axes, ticks, labels, legends, image display, save-to-PNG/PDF/SVG, inline-in-Jupyter. **Not** 3D, animation, multi-backend abstraction (Qt/GTK/MacOSX), most chart types, complex tick locators/formatters, or any of the other ~180 K LOC of matplotlib's surface.
17
+
18
+ The driver question: replace matplotlib with what?
19
+
20
+ ## Library survey (2026-05-07)
21
+
22
+ Shallow-cloned each candidate, counted Python LOC excluding tests, sphinx, sample data, and auto-generated validators:
23
+
24
+ | Library | Repo | Relevant Py LOC | Native code | Notes |
25
+ |---|---|---|---|---|
26
+ | matplotlib | — | 186 K | C extensions | What we're replacing |
27
+ | bokeh | 120 MB | 64 K | 150 K TypeScript | UI lives in TS frontend; Py is scene graph + serialization |
28
+ | plotly | 66 MB | 13 K relevant (+ ~600 K autogen) | JS bundle as data | Most LOC is auto-generated trait validators |
29
+ | datoviz | 137 MB | 8 K Py wrapper | 140 K C++ Vulkan engine | Heaviest binary footprint, fastest renderer |
30
+ | vispy | 11 MB | 32 K | None — pure Py + OpenGL | OpenGL stack, not WGPU |
31
+ | pygfx | 84 MB | 20 K | None — uses `wgpu-py` | Same backend ocdkit already uses |
32
+
33
+ ## Tradeoff summary
34
+
35
+ **Bokeh** — toolbar/logo *can* be removed (`figure(toolbar_location=None)`, `fig.toolbar.logo = None`), but the interaction model lives in a separate TypeScript frontend. To customize hover/zoom UX we'd be writing or forking TypeScript, not Python. The 150 K-LOC TS frontend is essentially the part we'd want to control, and it's the part we can't easily touch.
36
+
37
+ **Plotly** — same shape. Most LOC is auto-generated validators; the actual rendering and interaction is in the JS bundle. `displayModeBar=False` removes the toolbar but customizing hover beyond what their config exposes means reaching into JS.
38
+
39
+ **Datoviz** — Vulkan engine is fast and well-architected, but adds a 137 MB binary dependency and a different graphics stack from our existing wgpu-py code. Two graphics backends to maintain instead of one.
40
+
41
+ **Vispy** — pure Python and has scene + visuals modules that overlap heavily with what we want. But it's OpenGL via PyOpenGL, not WebGPU. We'd be running a second graphics stack alongside our wgpu-py code.
42
+
43
+ **pygfx** — uses *exactly* the `wgpu-py` we already depend on. Its `gfx.Lines`, `gfx.Mesh`, `gfx.Text`, `gfx.OrthographicCamera` primitives compose directly with our `DensityLineRenderer`. The most architecturally compatible third-party option.
44
+
45
+ **Roll our own** — given that we already have ~10 K LOC of plotting infrastructure, including a working WGPU line/scatter rasterizer, the increment to "complete 2D plotting library covering our actual needs" is roughly 3–5 K LOC. The scope is well-defined: axes, ticks, labels, legend, image display, hover, export.
46
+
47
+ ## Recommendation
48
+
49
+ **Two-step.**
50
+
51
+ ### Step 1: pygfx prototype (1 day)
52
+
53
+ Spike a `plot_spectra_pygfx` that uses `pygfx.Lines` + `pygfx.Text` + an `OrthographicCamera` to reproduce the current spectra layout. This is cheap because pygfx and our existing WGPU code share the same `wgpu-py` device — they can literally run in the same process without backend conflict.
54
+
55
+ What to evaluate:
56
+ - Visual quality vs. our current WGPU-rendered density lines (especially anti-aliasing)
57
+ - Text rendering quality (pygfx uses FreeType-rendered SDFs)
58
+ - Hover/picking — does pygfx's built-in picking suffice for our tooltip needs?
59
+ - Export — pygfx renders to a wgpu canvas; PNG export is straightforward, PDF/SVG would need our own rasterize-and-vectorize path
60
+
61
+ If pygfx covers ≥80% of our needs at ≥80% of the visual quality, **adopt pygfx**. We get hover, zoom, pan, picking essentially for free, plus the `pygfx` ecosystem (geometries, materials, post-processing).
62
+
63
+ ### Step 2 (only if pygfx is insufficient): roll our own
64
+
65
+ Estimated scope, building on the existing WGPU line rasterizer:
66
+
67
+ | Module | LOC est. | Notes |
68
+ |---|---|---|
69
+ | `ocdkit.plot.figure_v2` | 400 | New `Figure` class owning the wgpu canvas + axes layout; coexists with current matplotlib `figure()` so migration is piecewise |
70
+ | `ocdkit.plot.axis` | 800 | `LinearAxis`, `LogAxis` — tick locator + formatter + render-time placement. The matplotlib equivalent is ~3 K LOC; we don't need most of it |
71
+ | `ocdkit.plot.wgpu.text` | 600 | FreeType-rendered glyph atlas + WGSL textured-quad shader. The one piece of genuinely new rasterization work |
72
+ | `ocdkit.plot.legend` | 200 | Boxed layout: text + marker swatches |
73
+ | `ocdkit.plot.hover` | 300 | Mouse → data-coord lookup → DOM/Jupyter tooltip overlay. Generalize the classification-debugger pattern |
74
+ | `ocdkit.plot.export` | 400 | PNG (numpy→PIL), SVG (string templates), maybe PDF (skip if PIL→PNG covers science exports) |
75
+ | Migration: rewrite `plot_spectra`, `plot_image_grid`, `key_slice_grid`, `label_axes` against new primitives | ~600 | Mostly drop-in replacements at the call sites |
76
+ | **Total** | **~3.3 K** | New code, all in our control |
77
+
78
+ Plus: deletion of matplotlib-specific code paths in the downstream plot package (~1 K LOC saved) and removal of matplotlib runtime dep.
79
+
80
+ ### Why not pygfx + own axes?
81
+
82
+ A hybrid path is also viable: use pygfx for the rasterization layer (lines/text/transform/camera) and write our own axis/tick/legend/hover layer on top. That'd be the best of both — pygfx handles the rendering substrate, we own the plotting semantics. Estimated ~1.5 K LOC of new code on top of pygfx.
83
+
84
+ ## Open questions
85
+
86
+ 1. **Hover semantics** — does the classification-debugger tooltip pattern generalize cleanly, or do different plots need different hover content models?
87
+ 2. **Export fidelity** — do we need true vector PDF, or is high-DPI PNG enough? Vector requires re-rasterizing axes/text on the CPU side, which is non-trivial.
88
+ 3. **Notebook + standalone parity** — pygfx renders to a wgpu canvas that displays inline in Jupyter via `wgpu_jupyter`. Does that path work in VS Code's Jupyter extension and in standalone scripts (`savefig`-equivalent)?
89
+ 4. **Remoting** — if we ever want a server-side render pipeline (for cloud GUIs), pygfx's WGPU backend can render headlessly; matplotlib `Agg` does the same. Bokeh/plotly's JS-frontend assumption is harder to remote.
90
+
91
+ ## Concrete next action
92
+
93
+ Spike `plot_spectra_pygfx` against a representative spectra notebook (e.g. `scope.mixed_spectra[-1]` from any existing demo). Compare visual output side-by-side with `plot_spectra_wgpu` and `plot_spectra_cpu`. Decide pygfx-vs-roll-own from that single comparison.
94
+
95
+ If the answer ends up being "pygfx + own axis layer", the new module structure would live in `ocdkit.plot.gfx_*` (parallel to the current matplotlib-based modules) so the migration is a per-call-site flip rather than a big-bang rewrite.
@@ -10,11 +10,19 @@ readme = "README.md"
10
10
  license = {text = "BSD-3-Clause"}
11
11
  requires-python = ">=3.11"
12
12
  dependencies = [
13
- "numpy",
13
+ # numpy >= 2.0 — np.unique was reworked / sped up in the 2.0 cycle
14
+ # and matches fastremap.unique on label arrays. Helpers like
15
+ # array.ops.unique_nonzero / is_sequential rely on that parity to
16
+ # avoid hard-importing fastremap.
17
+ "numpy>=2.0",
14
18
  "scipy",
15
19
  "scikit-image>=0.26",
16
20
  "tifffile",
17
21
  "imagecodecs",
22
+ # opencodecs.uhdr.read_thumbnail_bytes (≥0.1.12) is the fast path
23
+ # in resolve_uhdr_thumb_bytes; ≥0.1.10 has the _fast_log2 fix
24
+ # required for correct gain-map encoding via encode_native.
25
+ "opencodecs>=0.1.12",
18
26
  "matplotlib",
19
27
  "fastremap",
20
28
  "edt",
@@ -28,6 +36,7 @@ dependencies = [
28
36
  "tqdm",
29
37
  "platformdirs",
30
38
  "cryptography",
39
+ "lxml",
31
40
  ]
32
41
 
33
42
  [project.optional-dependencies]
@@ -78,11 +87,7 @@ testpaths = ["tests"]
78
87
  source = ["ocdkit"]
79
88
 
80
89
  [tool.coverage.paths]
81
- source = [
82
- "src/ocdkit",
83
- "/Volumes/DataDrive/ocdkit/src/ocdkit",
84
- "/home/kcutler/DataDrive/ocdkit/src/ocdkit",
85
- ]
90
+ source = ["src/ocdkit"]
86
91
 
87
92
  [tool.coverage.report]
88
93
  show_missing = true
@@ -0,0 +1,81 @@
1
+ """Synthetic single-cell test for contour alignment.
2
+
3
+ Plots a small mask with grid lines at integer pixel boundaries so we can
4
+ see exactly where each pipeline puts its outline relative to:
5
+ - the pixels of the cell (filled gray squares spanning [j-.5, j+.5] x [i-.5, i+.5])
6
+ - the cell boundary (gridline at half-integer y and x at the cell edge)
7
+
8
+ The three pipelines are:
9
+ - current (B-spline through pixel centers)
10
+ - tier 1 (Gaussian-smoothed pixel-center walk)
11
+ - tier 2 (marching squares; geometric cell boundary)
12
+
13
+ Also tests the new `offset` knob in tier 2.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import sys
19
+ from pathlib import Path
20
+
21
+ import matplotlib.pyplot as plt
22
+ import numpy as np
23
+
24
+ sys.path.insert(0, str(Path(__file__).parent))
25
+
26
+ from bench_vector_contours import vector_contours_fast
27
+ from ocdkit.plot.contour import vector_contours, vector_contours_marching
28
+
29
+
30
+ def make_mask():
31
+ m = np.zeros((20, 20), dtype=np.int32)
32
+ m[5:11, 5:11] = 1 # 6x6 square cell
33
+ m[14:17, 13:18] = 2 # 3x5 rectangle cell
34
+ return m
35
+
36
+
37
+ def add_grid(ax, shape):
38
+ H, W = shape
39
+ for x in np.arange(-0.5, W, 1):
40
+ ax.axvline(x, color='cyan', linewidth=0.3, alpha=0.35, zorder=0)
41
+ for y in np.arange(-0.5, H, 1):
42
+ ax.axhline(y, color='cyan', linewidth=0.3, alpha=0.35, zorder=0)
43
+
44
+
45
+ def main():
46
+ mask = make_mask()
47
+
48
+ fig, axes = plt.subplots(2, 2, figsize=(12, 12))
49
+
50
+ titles = [
51
+ "Current vector_contours (splprep) — note up-left bias",
52
+ "Tier 1 (gaussian on pixel-center walk)",
53
+ "Tier 2 default offset=0 (geometric cell boundary)",
54
+ "Tier 2 offset=0.5 (through pixel centers, no bias)",
55
+ ]
56
+ for ax, title in zip(axes.flat, titles):
57
+ ax.imshow(mask, cmap='gray_r', interpolation='nearest', vmin=0, vmax=2)
58
+ add_grid(ax, mask.shape)
59
+ ax.set_title(title, fontsize=10)
60
+ ax.set_xticks(range(mask.shape[1]))
61
+ ax.set_yticks(range(mask.shape[0]))
62
+ ax.tick_params(labelsize=6)
63
+
64
+ vector_contours(fig, axes[0, 0], mask, smooth_factor=5,
65
+ color='red', linewidth=1.5)
66
+ vector_contours_fast(fig, axes[0, 1], mask, smooth_sigma=1.0,
67
+ color='red', linewidth=1.5)
68
+ vector_contours_marching(fig, axes[1, 0], mask, smooth_sigma=1.0,
69
+ color='red', linewidth=1.5)
70
+ # Tier 2 with offset (still need to add the knob — we'll do that next)
71
+ vector_contours_marching(fig, axes[1, 1], mask, smooth_sigma=1.0,
72
+ color='red', linewidth=1.5)
73
+
74
+ fig.tight_layout()
75
+ out = Path(__file__).resolve().parent / 'bench_contour_alignment.png'
76
+ fig.savefig(out, dpi=150, bbox_inches='tight')
77
+ print(f"saved: {out}")
78
+
79
+
80
+ if __name__ == '__main__':
81
+ main()
@@ -0,0 +1,181 @@
1
+ """Demo: HDR-lifted colormaps through the css-img / SVG image_grid pipe.
2
+
3
+ Renders a grayscale gradient with viridis / magma / inferno / rdbu in:
4
+ - SDR (uint8 P3, current default behavior)
5
+ - HDR (float linear-P3 with values > 1.0, peak ≈ 1600 nits)
6
+
7
+ Saves figures/hdr_cmap_demo.svg — open in Safari to see HDR on an EDR
8
+ display (Chrome on macOS works too). Also prints the lifted LUTs'
9
+ out-of-SDR-range fractions so you can confirm the lift actually used
10
+ the headroom.
11
+ """
12
+ import sys
13
+ import time
14
+ from pathlib import Path
15
+
16
+ import numpy as np
17
+
18
+ import matplotlib.pyplot as plt
19
+ plt.rcParams.update({
20
+ 'figure.facecolor': 'none',
21
+ 'axes.facecolor': 'none',
22
+ 'savefig.facecolor': 'none',
23
+ 'axes.edgecolor': 'gray',
24
+ 'axes.labelcolor': 'gray',
25
+ 'xtick.color': 'gray',
26
+ 'ytick.color': 'gray',
27
+ 'text.color': 'gray',
28
+ 'axes.titlecolor': 'gray',
29
+ })
30
+
31
+ REPO = Path(__file__).resolve().parents[1]
32
+ sys.path.insert(0, str(REPO / 'src'))
33
+
34
+ from ocdkit.plot.hdr_cmap import ( # noqa: E402
35
+ make_hdr_cmap_lut, apply_hdr_cmap,
36
+ SDR_WHITE_NITS, HDR_PEAK_NITS_DEFAULT,
37
+ )
38
+ from ocdkit.plot import imshow # noqa: E402
39
+
40
+ # linear Display-P3 → Y (relative luminance)
41
+ P3_Y_WEIGHTS = np.array([0.2289745641, 0.6917385218, 0.0792869141])
42
+
43
+
44
+ def lut_stats(name):
45
+ sdr = make_hdr_cmap_lut(name, hdr_jz=0.155,
46
+ hdr_peak_nits=SDR_WHITE_NITS) # SDR baseline
47
+ hdr = make_hdr_cmap_lut(name, hdr_peak_nits=HDR_PEAK_NITS_DEFAULT)
48
+ # both LUTs are linear-P3 normalized so 1.0 ≡ their respective peak.
49
+ # Convert to absolute nits via Y-weighted sum × peak-nits.
50
+ peak_sdr_nits = float((sdr @ P3_Y_WEIGHTS).max() * SDR_WHITE_NITS)
51
+ peak_hdr_nits = float((hdr @ P3_Y_WEIGHTS).max() * HDR_PEAK_NITS_DEFAULT)
52
+ print(
53
+ f" {name:12s} "
54
+ f"SDR peak Y={peak_sdr_nits:6.1f} nits "
55
+ f"HDR peak Y={peak_hdr_nits:6.1f} nits "
56
+ f"lift {peak_hdr_nits / max(peak_sdr_nits, 1e-3):4.2f}x"
57
+ )
58
+
59
+
60
+ def main():
61
+ print("Lift stats (peak_mult=7.88 ≈ 1600 nits):")
62
+ for name in ('viridis', 'magma', 'inferno', 'plasma',
63
+ 'cividis', 'RdBu', 'twilight'):
64
+ lut_stats(name)
65
+
66
+ H, W = 64, 1024
67
+ grad = np.tile(np.linspace(0, 1, W, dtype=np.float32), (H, 1))
68
+
69
+ figures_dir = REPO / 'figures'
70
+ figures_dir.mkdir(exist_ok=True)
71
+
72
+ print("\nFull pipeline (HDR + SDR side by side):")
73
+ cmaps = ['viridis', 'magma', 'inferno', 'plasma', 'cividis']
74
+ for name in cmaps:
75
+ t0 = time.perf_counter()
76
+ rgb_hdr = apply_hdr_cmap(grad, name)
77
+ dt = (time.perf_counter() - t0) * 1000
78
+ peak_nits = float((rgb_hdr @ P3_Y_WEIGHTS).max() * HDR_PEAK_NITS_DEFAULT)
79
+ print(f" apply_hdr_cmap({name}) {dt:6.1f} ms "
80
+ f"linear-P3 max={float(rgb_hdr.max()):.3f} "
81
+ f"peak Y={peak_nits:6.1f} nits dtype={rgb_hdr.dtype}")
82
+
83
+ # Two routes through the new `cmap=` kwarg on imshow:
84
+ out_hdr = figures_dir / 'hdr_cmap_demo_hdr.svg'
85
+ out_sdr = figures_dir / 'hdr_cmap_demo_sdr.svg'
86
+
87
+ # HDR route: pre-applied float linear-P3 tiles. (imshow's per-call
88
+ # cmap kwarg applies one cmap to all 2D items, so to show per-cmap
89
+ # tiles in a single grid we apply manually here.)
90
+ hdr_tiles = [apply_hdr_cmap(grad, c) for c in cmaps]
91
+ fig_hdr = imshow(hdr_tiles, figsize=2, titles=cmaps)
92
+ out_hdr.write_text(fig_hdr._inner.to_string())
93
+ print(f"\nSaved HDR demo: {out_hdr}")
94
+
95
+ # SDR baseline through the same `cmap=` kwarg, using hdr=False.
96
+ from cmap import Colormap
97
+ sdr_tiles = []
98
+ for c in cmaps:
99
+ cm = Colormap(c)
100
+ rgba = np.asarray(cm(grad))
101
+ sdr_tiles.append((rgba[..., :3] * 255).astype(np.uint8))
102
+ fig_sdr = imshow(sdr_tiles, figsize=2, titles=cmaps)
103
+ out_sdr.write_text(fig_sdr._inner.to_string())
104
+ print(f"Saved SDR demo: {out_sdr}")
105
+
106
+ # And one figure exercising the new imshow(cmap=, hdr=True) wiring
107
+ # directly on a single 2D array.
108
+ out_kw = figures_dir / 'hdr_cmap_demo_kwarg.svg'
109
+ fig_kw = imshow(grad, cmap='viridis', hdr=True, figsize=4,
110
+ titles='imshow(grad, cmap="viridis", hdr=True)')
111
+ out_kw.write_text(fig_kw._inner.to_string())
112
+ print(f"Saved kwarg demo: {out_kw}")
113
+
114
+ # ─────────────────────────────────────────────────────────────────
115
+ # SDR-fallback A/B test
116
+ # ─────────────────────────────────────────────────────────────────
117
+ # LEFT tile : jxl-p3, uint8 sRGB-curve viridis (native SDR cmap).
118
+ # RIGHT tile : Ultra-HDR JPEG (apply_hdr_cmap → linear-P3 float →
119
+ # libuhdr base + gain map). The SDR base layer of the
120
+ # UHDR JPEG is the *native non-lifted cmap* (via
121
+ # HdrCmapArray._sdr_base_p3_u8), not libuhdr's
122
+ # auto-tone-map — which used to desaturate bright
123
+ # stops (the original reason for switching off PQ-JXL).
124
+ #
125
+ # Expected on HDR display: RIGHT tile glows (~600 nits peak).
126
+ # Expected on SDR display (or HDR display toggled off): LEFT and
127
+ # RIGHT should look pixel-identical — both are the same SDR cmap.
128
+ bumps = np.tile(np.linspace(0, 1, 1024, dtype=np.float32), (192, 1))
129
+ out_sdr_only = figures_dir / 'hdr_cmap_compare_sdr.svg'
130
+ out_hdr_only = figures_dir / 'hdr_cmap_compare_hdr.svg'
131
+
132
+ fig_sdr_only = imshow(bumps, cmap='viridis', hdr=False, figsize=6,
133
+ titles='viridis · jxl-p3 (native SDR cmap)')
134
+ out_sdr_only.write_text(fig_sdr_only._inner.to_string())
135
+
136
+ fig_hdr_only = imshow(bumps, cmap='viridis', hdr=True, figsize=6,
137
+ titles='viridis · UHDR (HDR-lifted, SDR base = native cmap)')
138
+ out_hdr_only.write_text(fig_hdr_only._inner.to_string())
139
+
140
+ out_html = figures_dir / 'hdr_cmap_compare.html'
141
+ out_html.write_text(f"""<!doctype html>
142
+ <html><head><meta charset="utf-8">
143
+ <title>HDR vs SDR cmap A/B</title>
144
+ <style>
145
+ body {{
146
+ background: #111; color: #ccc; font-family: -apple-system, sans-serif;
147
+ margin: 20px; line-height: 1.45;
148
+ }}
149
+ .row {{ display: flex; gap: 16px; align-items: flex-start; flex-wrap: wrap; }}
150
+ .col {{ flex: 1 1 480px; min-width: 380px; }}
151
+ h2 {{ font-size: 14px; color: #888; margin: 6px 0; font-weight: normal; }}
152
+ p {{ font-size: 13px; color: #888; max-width: 80ch; }}
153
+ code {{ color: #ddc; }}
154
+ </style></head>
155
+ <body>
156
+ <h1 style="font-size:16px;color:#aaa">HDR vs SDR cmap A/B (Ultra-HDR + native SDR base)</h1>
157
+ <p>Toggle display HDR (macOS: System Settings → Displays → "High Dynamic
158
+ Range"). On HDR the right tile glows. On SDR the two should be
159
+ pixel-identical — the UHDR's SDR base layer is the same native viridis
160
+ the left tile uses.</p>
161
+ <div class="row">
162
+ <div class="col"><h2>SDR baseline (jxl-p3, uint8 viridis)</h2>
163
+ <object data="hdr_cmap_compare_sdr.svg" type="image/svg+xml"
164
+ style="width:100%"></object>
165
+ </div>
166
+ <div class="col"><h2>HDR UHDR (apply_hdr_cmap viridis)</h2>
167
+ <object data="hdr_cmap_compare_hdr.svg" type="image/svg+xml"
168
+ style="width:100%"></object>
169
+ </div>
170
+ </div>
171
+ </body></html>""")
172
+ print(f"Saved A/B page: {out_html}")
173
+ print(f" SDR JXL: {out_sdr_only}")
174
+ print(f" HDR UHDR: {out_hdr_only}")
175
+ print(" Open the HTML in Safari, then toggle Displays → HDR off.")
176
+ print(" Tiles should now look identical in SDR mode; HDR mode the")
177
+ print(" right tile gains brightness from the gain map.")
178
+
179
+
180
+ if __name__ == '__main__':
181
+ main()