ocdkit 0.0.5__tar.gz → 0.0.7__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 (251) hide show
  1. {ocdkit-0.0.5/src/ocdkit.egg-info → ocdkit-0.0.7}/PKG-INFO +4 -1
  2. ocdkit-0.0.7/badges/coverage.svg +1 -0
  3. ocdkit-0.0.7/badges/tests.svg +1 -0
  4. ocdkit-0.0.7/jupyter-config/jupyter_server_config.d/ocdkit_tileserve.json +7 -0
  5. {ocdkit-0.0.5 → ocdkit-0.0.7}/pyproject.toml +14 -0
  6. ocdkit-0.0.7/scripts/check_image_grid_auto_color.py +111 -0
  7. ocdkit-0.0.7/scripts/hdr_harness.py +69 -0
  8. ocdkit-0.0.7/scripts/make_chromaticity_p3.py +397 -0
  9. ocdkit-0.0.7/src/ocdkit/io/fig_export.py +549 -0
  10. ocdkit-0.0.7/src/ocdkit/io/figure.py +5591 -0
  11. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/io/figure_server.py +132 -259
  12. ocdkit-0.0.7/src/ocdkit/io/live_figure.py +72 -0
  13. ocdkit-0.0.7/src/ocdkit/io/mpl_axes_cropper.py +302 -0
  14. ocdkit-0.0.7/src/ocdkit/io/pptx.py +1333 -0
  15. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/plot/__init__.py +11 -14
  16. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/plot/composite_grid.py +64 -6
  17. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/plot/contour.py +90 -3
  18. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/plot/display.py +115 -12
  19. ocdkit-0.0.7/src/ocdkit/plot/image_grid.py +1608 -0
  20. ocdkit-0.0.7/src/ocdkit/plot/luts.py +59 -0
  21. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/plot/ncolor.py +24 -0
  22. ocdkit-0.0.7/src/ocdkit/plot/pyramid.py +68 -0
  23. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/plot/svg.py +41 -34
  24. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/plot/text_metrics.py +16 -1
  25. ocdkit-0.0.7/src/ocdkit/plot/web/colormap_image.js +168 -0
  26. ocdkit-0.0.7/src/ocdkit/plot/web/hdr_colormap.js +432 -0
  27. ocdkit-0.0.7/src/ocdkit/plot/web/hdr_headroom.js +89 -0
  28. ocdkit-0.0.7/src/ocdkit/plot/web/label_gl.js +529 -0
  29. ocdkit-0.0.7/src/ocdkit/plot/web/scatter_gl.js +161 -0
  30. ocdkit-0.0.7/src/ocdkit/plot/web/spectra_density_element.js +123 -0
  31. ocdkit-0.0.7/src/ocdkit/plot/web/spectra_density_gl.js +796 -0
  32. ocdkit-0.0.7/src/ocdkit/plot/web/tile_grid_element.js +95 -0
  33. ocdkit-0.0.7/src/ocdkit/tileserve/__init__.py +21 -0
  34. ocdkit-0.0.7/src/ocdkit/tileserve/headless.py +356 -0
  35. ocdkit-0.0.7/src/ocdkit/tileserve/jupyter_ext.py +117 -0
  36. ocdkit-0.0.7/src/ocdkit/tileserve/layout.py +148 -0
  37. ocdkit-0.0.7/src/ocdkit/tileserve/server.py +520 -0
  38. ocdkit-0.0.7/src/ocdkit/tileserve/shaders.py +100 -0
  39. ocdkit-0.0.7/src/ocdkit/tileserve/viewer.py +2387 -0
  40. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/app.py +13 -0
  41. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/assets.py +18 -0
  42. ocdkit-0.0.7/src/ocdkit/viewer/edr_bridge.py +74 -0
  43. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/app.js +47 -4
  44. ocdkit-0.0.7/src/ocdkit/viewer/web/hdr_cmap_prototype.html +129 -0
  45. ocdkit-0.0.7/src/ocdkit/viewer/web/hdr_cmap_uhdr.html +32 -0
  46. ocdkit-0.0.7/src/ocdkit/viewer/web/hdr_css_test.html +16 -0
  47. ocdkit-0.0.7/src/ocdkit/viewer/web/html/_attic_omnipose-panel.html +130 -0
  48. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/js/colormap.js +231 -0
  49. ocdkit-0.0.7/src/ocdkit/viewer/web/js/hdr_image_layer.js +118 -0
  50. ocdkit-0.0.7/src/ocdkit/viewer/web/js/hdr_ui.js +165 -0
  51. {ocdkit-0.0.5 → ocdkit-0.0.7/src/ocdkit.egg-info}/PKG-INFO +4 -1
  52. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit.egg-info/SOURCES.txt +35 -0
  53. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit.egg-info/requires.txt +3 -0
  54. ocdkit-0.0.7/tests/test_io_live_figure.py +66 -0
  55. ocdkit-0.0.7/tests/test_io_pptx.py +55 -0
  56. ocdkit-0.0.7/tests/test_tileserve_pick_level.py +51 -0
  57. ocdkit-0.0.5/badges/coverage.svg +0 -1
  58. ocdkit-0.0.5/badges/tests.svg +0 -1
  59. ocdkit-0.0.5/src/ocdkit/io/figure.py +0 -2897
  60. ocdkit-0.0.5/src/ocdkit/plot/image_grid.py +0 -877
  61. {ocdkit-0.0.5 → ocdkit-0.0.7}/.github/workflows/test_and_deploy.yml +0 -0
  62. {ocdkit-0.0.5 → ocdkit-0.0.7}/.gitignore +0 -0
  63. {ocdkit-0.0.5 → ocdkit-0.0.7}/LICENSE +0 -0
  64. {ocdkit-0.0.5 → ocdkit-0.0.7}/MANIFEST.in +0 -0
  65. {ocdkit-0.0.5 → ocdkit-0.0.7}/README.md +0 -0
  66. {ocdkit-0.0.5 → ocdkit-0.0.7}/docs/plot-backend-roadmap.md +0 -0
  67. {ocdkit-0.0.5 → ocdkit-0.0.7}/docs/plugin-authoring.md +0 -0
  68. {ocdkit-0.0.5 → ocdkit-0.0.7}/docs/pywebview-desktop-integration.md +0 -0
  69. {ocdkit-0.0.5 → ocdkit-0.0.7}/scripts/bench_colorize.py +0 -0
  70. {ocdkit-0.0.5 → ocdkit-0.0.7}/scripts/bench_contour_alignment.py +0 -0
  71. {ocdkit-0.0.5 → ocdkit-0.0.7}/scripts/bench_hdr_cmap.py +0 -0
  72. {ocdkit-0.0.5 → ocdkit-0.0.7}/scripts/bench_vector_contours.py +0 -0
  73. {ocdkit-0.0.5 → ocdkit-0.0.7}/scripts/bench_vector_contours_tier2.py +0 -0
  74. {ocdkit-0.0.5 → ocdkit-0.0.7}/scripts/check_compare_pixels.py +0 -0
  75. {ocdkit-0.0.5 → ocdkit-0.0.7}/scripts/check_hdr_cmap.py +0 -0
  76. {ocdkit-0.0.5 → ocdkit-0.0.7}/scripts/check_jxl_p3_bytes.py +0 -0
  77. {ocdkit-0.0.5 → ocdkit-0.0.7}/scripts/check_playwright_render.py +0 -0
  78. {ocdkit-0.0.5 → ocdkit-0.0.7}/scripts/check_uhdr_sdr_base.py +0 -0
  79. {ocdkit-0.0.5 → ocdkit-0.0.7}/scripts/coverage_cross_device.env.example +0 -0
  80. {ocdkit-0.0.5 → ocdkit-0.0.7}/scripts/coverage_cross_device.sh +0 -0
  81. {ocdkit-0.0.5 → ocdkit-0.0.7}/setup.cfg +0 -0
  82. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/__init__.py +0 -0
  83. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/__main__.py +0 -0
  84. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/array/__init__.py +0 -0
  85. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/array/convert.py +0 -0
  86. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/array/filters.py +0 -0
  87. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/array/imports.py +0 -0
  88. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/array/index.py +0 -0
  89. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/array/morphology.py +0 -0
  90. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/array/normalize.py +0 -0
  91. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/array/ops.py +0 -0
  92. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/array/parallel.py +0 -0
  93. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/array/spatial.py +0 -0
  94. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/array/transform.py +0 -0
  95. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/array/union_find.py +0 -0
  96. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/array/warp.py +0 -0
  97. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/cli/__init__.py +0 -0
  98. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/cli/__main__.py +0 -0
  99. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/cli/main.py +0 -0
  100. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/cli/migrate.py +0 -0
  101. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/cli/paths.py +0 -0
  102. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/desktop/__init__.py +0 -0
  103. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/desktop/pinning.py +0 -0
  104. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/imports.py +0 -0
  105. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/io/__init__.py +0 -0
  106. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/io/files.py +0 -0
  107. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/io/image.py +0 -0
  108. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/io/imports.py +0 -0
  109. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/io/path.py +0 -0
  110. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/io/result.py +0 -0
  111. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/load/__init__.py +0 -0
  112. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/load/module.py +0 -0
  113. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/load/object.py +0 -0
  114. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/logging/__init__.py +0 -0
  115. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/logging/handler.py +0 -0
  116. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/measure/__init__.py +0 -0
  117. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/measure/bbox.py +0 -0
  118. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/measure/diameter.py +0 -0
  119. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/measure/imports.py +0 -0
  120. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/measure/medoid.py +0 -0
  121. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/measure/metrics.py +0 -0
  122. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/plot/bench.py +0 -0
  123. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/plot/color.py +0 -0
  124. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/plot/composite.py +0 -0
  125. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/plot/defaults.py +0 -0
  126. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/plot/export.py +0 -0
  127. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/plot/figure.py +0 -0
  128. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/plot/grid.py +0 -0
  129. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/plot/hdr_cmap.py +0 -0
  130. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/plot/imports.py +0 -0
  131. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/plot/label.py +0 -0
  132. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/plot/layout.py +0 -0
  133. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/plot/style.py +0 -0
  134. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/testing/__init__.py +0 -0
  135. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/testing/collisions.py +0 -0
  136. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/testing/imports.py +0 -0
  137. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/tls/__init__.py +0 -0
  138. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/tls/external_ca.py +0 -0
  139. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/tls/hostnames.py +0 -0
  140. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/tls/imports.py +0 -0
  141. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/tls/local_ca.py +0 -0
  142. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/tls/paths.py +0 -0
  143. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/tls/trust.py +0 -0
  144. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/utils/__init__.py +0 -0
  145. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/utils/collections.py +0 -0
  146. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/utils/gpu.py +0 -0
  147. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/utils/kwargs.py +0 -0
  148. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/utils/paths.py +0 -0
  149. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/__init__.py +0 -0
  150. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/__main__.py +0 -0
  151. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/cli.py +0 -0
  152. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/demo.html +0 -0
  153. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/dependencies.py +0 -0
  154. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/exceptions.py +0 -0
  155. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/masks.py +0 -0
  156. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/middleware.py +0 -0
  157. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/model_registry.py +0 -0
  158. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/plugins/__init__.py +0 -0
  159. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/plugins/base.py +0 -0
  160. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/plugins/registry.py +0 -0
  161. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/plugins/schema.py +0 -0
  162. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/plugins/threshold.py +0 -0
  163. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/routers/__init__.py +0 -0
  164. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/routers/index.py +0 -0
  165. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/routers/log.py +0 -0
  166. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/routers/mask.py +0 -0
  167. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/routers/plugin.py +0 -0
  168. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/routers/segment.py +0 -0
  169. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/routers/session_routes.py +0 -0
  170. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/routers/system.py +0 -0
  171. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/routers/trust.py +0 -0
  172. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/routes.py +0 -0
  173. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/sample_image.py +0 -0
  174. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/schemas.py +0 -0
  175. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/segmentation.py +0 -0
  176. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/session.py +0 -0
  177. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/system.py +0 -0
  178. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/css/controls.css +0 -0
  179. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/css/layout.css +0 -0
  180. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/css/tools.css +0 -0
  181. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/css/viewer.css +0 -0
  182. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/html/left-panel.html +0 -0
  183. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/html/sidebar.html +0 -0
  184. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/html/viewer.html +0 -0
  185. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/icons/affinity.svg +0 -0
  186. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/icons/arrow-back-up.svg +0 -0
  187. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/icons/arrow-forward-up.svg +0 -0
  188. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/icons/dbscan-nested-arcs.svg +0 -0
  189. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/icons/download.svg +0 -0
  190. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/icons/droplet-half-2.svg +0 -0
  191. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/icons/eraser.svg +0 -0
  192. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/icons/home-2.svg +0 -0
  193. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/icons/minus.svg +0 -0
  194. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/icons/palette.svg +0 -0
  195. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/icons/pencil.svg +0 -0
  196. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/icons/plus.svg +0 -0
  197. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/icons/rotate-rectangle.svg +0 -0
  198. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/icons/topology-star.svg +0 -0
  199. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/index.html +0 -0
  200. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/js/brush.js +0 -0
  201. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/js/debug-apple-material.js +0 -0
  202. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/js/file-navigation.js +0 -0
  203. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/js/history.js +0 -0
  204. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/js/interactions.js +0 -0
  205. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/js/logging.js +0 -0
  206. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/js/mask-pipeline.js +0 -0
  207. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/js/painting.js +0 -0
  208. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/js/plugin-panel.js +0 -0
  209. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/js/pointer-state.js +0 -0
  210. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/js/state-persistence.js +0 -0
  211. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/js/tooltip-editor.js +0 -0
  212. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/js/ui-utils.js +0 -0
  213. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/js/wasm_fill.c +0 -0
  214. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit.egg-info/dependency_links.txt +0 -0
  215. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit.egg-info/entry_points.txt +0 -0
  216. {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit.egg-info/top_level.txt +0 -0
  217. {ocdkit-0.0.5 → ocdkit-0.0.7}/tests/e2e/__init__.py +0 -0
  218. {ocdkit-0.0.5 → ocdkit-0.0.7}/tests/e2e/conftest.py +0 -0
  219. {ocdkit-0.0.5 → ocdkit-0.0.7}/tests/e2e/test_browser_smoke.py +0 -0
  220. {ocdkit-0.0.5 → ocdkit-0.0.7}/tests/e2e/test_pywebview_snapshot.py +0 -0
  221. {ocdkit-0.0.5 → ocdkit-0.0.7}/tests/fixtures/multichan_3c_4x4.czi +0 -0
  222. {ocdkit-0.0.5 → ocdkit-0.0.7}/tests/fixtures/tiny_8x8.czi +0 -0
  223. {ocdkit-0.0.5 → ocdkit-0.0.7}/tests/test_array.py +0 -0
  224. {ocdkit-0.0.5 → ocdkit-0.0.7}/tests/test_gpu.py +0 -0
  225. {ocdkit-0.0.5 → ocdkit-0.0.7}/tests/test_import_cycles.py +0 -0
  226. {ocdkit-0.0.5 → ocdkit-0.0.7}/tests/test_io.py +0 -0
  227. {ocdkit-0.0.5 → ocdkit-0.0.7}/tests/test_measure.py +0 -0
  228. {ocdkit-0.0.5 → ocdkit-0.0.7}/tests/test_module_collisions.py +0 -0
  229. {ocdkit-0.0.5 → ocdkit-0.0.7}/tests/test_module_discovery.py +0 -0
  230. {ocdkit-0.0.5 → ocdkit-0.0.7}/tests/test_morphology.py +0 -0
  231. {ocdkit-0.0.5 → ocdkit-0.0.7}/tests/test_paths_migration.py +0 -0
  232. {ocdkit-0.0.5 → ocdkit-0.0.7}/tests/test_plot_color.py +0 -0
  233. {ocdkit-0.0.5 → ocdkit-0.0.7}/tests/test_plot_contour.py +0 -0
  234. {ocdkit-0.0.5 → ocdkit-0.0.7}/tests/test_plot_display.py +0 -0
  235. {ocdkit-0.0.5 → ocdkit-0.0.7}/tests/test_plot_export.py +0 -0
  236. {ocdkit-0.0.5 → ocdkit-0.0.7}/tests/test_plot_figure.py +0 -0
  237. {ocdkit-0.0.5 → ocdkit-0.0.7}/tests/test_plot_grid.py +0 -0
  238. {ocdkit-0.0.5 → ocdkit-0.0.7}/tests/test_plot_label.py +0 -0
  239. {ocdkit-0.0.5 → ocdkit-0.0.7}/tests/test_plot_notebook.py +0 -0
  240. {ocdkit-0.0.5 → ocdkit-0.0.7}/tests/test_registration.py +0 -0
  241. {ocdkit-0.0.5 → ocdkit-0.0.7}/tests/test_slice.py +0 -0
  242. {ocdkit-0.0.5 → ocdkit-0.0.7}/tests/test_spatial.py +0 -0
  243. {ocdkit-0.0.5 → ocdkit-0.0.7}/tests/viewer/__init__.py +0 -0
  244. {ocdkit-0.0.5 → ocdkit-0.0.7}/tests/viewer/test_active_plugin_cache.py +0 -0
  245. {ocdkit-0.0.5 → ocdkit-0.0.7}/tests/viewer/test_app.py +0 -0
  246. {ocdkit-0.0.5 → ocdkit-0.0.7}/tests/viewer/test_async_dispatch.py +0 -0
  247. {ocdkit-0.0.5 → ocdkit-0.0.7}/tests/viewer/test_envelope_and_middleware.py +0 -0
  248. {ocdkit-0.0.5 → ocdkit-0.0.7}/tests/viewer/test_plugin_contract.py +0 -0
  249. {ocdkit-0.0.5 → ocdkit-0.0.7}/tests/viewer/test_session_eviction.py +0 -0
  250. {ocdkit-0.0.5 → ocdkit-0.0.7}/tests/viewer/test_title_config.py +0 -0
  251. {ocdkit-0.0.5 → ocdkit-0.0.7}/tests/viewer/test_ui_mode.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ocdkit
3
- Version: 0.0.5
3
+ Version: 0.0.7
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
@@ -26,6 +26,9 @@ Requires-Dist: tqdm
26
26
  Requires-Dist: platformdirs
27
27
  Requires-Dist: cryptography
28
28
  Requires-Dist: lxml
29
+ Requires-Dist: python-pptx
30
+ Requires-Dist: freetype-py
31
+ Requires-Dist: rich
29
32
  Provides-Extra: viewer
30
33
  Requires-Dist: fastapi; extra == "viewer"
31
34
  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: 32.58%"><title>coverage: 32.58%</title><filter id="blur"><feGaussianBlur stdDeviation="16"/></filter><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"/></clipPath><g clip-path="url(#r)"><rect width="61" height="20" fill="#555"/><rect x="61" width="53" height="20" fill="#dd4343"/><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"><g transform="scale(.1)"><g aria-hidden="true" fill="#010101"><text x="315" y="150" fill-opacity=".8" filter="url(#blur)" textLength="510">coverage</text><text x="315" y="150" fill-opacity=".3" textLength="510">coverage</text></g><text x="315" y="140" textLength="510">coverage</text></g><g transform="scale(.1)"><g aria-hidden="true" fill="#010101"><text x="865" y="150" fill-opacity=".8" filter="url(#blur)" textLength="430">32.58%</text><text x="865" y="150" fill-opacity=".3" textLength="430">32.58%</text></g><text x="865" y="140" textLength="430">32.58%</text></g></g></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="68" height="20" role="img" aria-label="tests: 423"><title>tests: 423</title><filter id="blur"><feGaussianBlur stdDeviation="16"/></filter><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"/></clipPath><g clip-path="url(#r)"><rect width="37" height="20" fill="#555"/><rect x="37" width="31" height="20" fill="#4b0"/><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"><g transform="scale(.1)"><g aria-hidden="true" fill="#010101"><text x="195" y="150" fill-opacity=".8" filter="url(#blur)" textLength="270">tests</text><text x="195" y="150" fill-opacity=".3" textLength="270">tests</text></g><text x="195" y="140" textLength="270">tests</text></g><g transform="scale(.1)"><g aria-hidden="true" fill="#010101"><text x="515" y="150" fill-opacity=".8" filter="url(#blur)" textLength="210">423</text><text x="515" y="150" fill-opacity=".3" textLength="210">423</text></g><text x="515" y="140" textLength="210">423</text></g></g></svg>
@@ -0,0 +1,7 @@
1
+ {
2
+ "ServerApp": {
3
+ "jpserver_extensions": {
4
+ "ocdkit.tileserve.jupyter_ext": true
5
+ }
6
+ }
7
+ }
@@ -37,6 +37,12 @@ dependencies = [
37
37
  "platformdirs",
38
38
  "cryptography",
39
39
  "lxml",
40
+ # python-pptx: figs_to_deck PPTX export (ocdkit.io.pptx)
41
+ "python-pptx",
42
+ # freetype-py: glyph metrics for ocdkit.plot.text_metrics (measure_text etc.)
43
+ "freetype-py",
44
+ # rich: used by ocdkit.logging.handler (lazy import) for the rich logging handler
45
+ "rich",
40
46
  ]
41
47
 
42
48
  [project.optional-dependencies]
@@ -68,6 +74,14 @@ ocdkit-viewer-gui = "ocdkit.viewer.cli:main"
68
74
  [tool.setuptools.packages.find]
69
75
  where = ["src"]
70
76
 
77
+ # Auto-enable the tileserve Jupyter proxy extension on a REGULAR install (data
78
+ # files don't land for editable installs — there, enable once with:
79
+ # jupyter server extension enable ocdkit.tileserve.jupyter_ext
80
+ [tool.setuptools.data-files]
81
+ "etc/jupyter/jupyter_server_config.d" = [
82
+ "jupyter-config/jupyter_server_config.d/ocdkit_tileserve.json",
83
+ ]
84
+
71
85
  [tool.setuptools.package-data]
72
86
  "ocdkit.viewer" = [
73
87
  "*.html",
@@ -0,0 +1,111 @@
1
+ """Sanity check for ``image_grid(fontcolor='auto')``.
2
+
3
+ Renders two side-by-side 4x4 grids of uniform-gray tiles:
4
+
5
+ * **SDR grid** — uint8 tiles, displayed at their literal sRGB brightness.
6
+ Labels flip at ~50% (linear ~0.5) — light on dark cells, dark on light.
7
+
8
+ * **HDR grid** — linear-light float tiles encoded as UHDR JPEG. On an
9
+ HDR-capable browser the cells render up to ``sdr_white_nits`` (1600 nits
10
+ by default), so a "low" linear value (~0.06) already displays at the
11
+ same brightness as the SDR-white label. Labels flip *earlier* than 50%
12
+ to keep contrast against the perceived display brightness.
13
+
14
+ Outputs:
15
+
16
+ * ``figures/image_grid_auto_color.html`` — interactive HTML shell.
17
+ Open in a browser; toggle OS / browser dark mode to verify the page
18
+ swaps but the per-cell label colours stay content-locked.
19
+ * ``figures/image_grid_auto_color.svg`` — plain SVG of the SDR grid.
20
+ """
21
+ from __future__ import annotations
22
+
23
+ import sys
24
+ from pathlib import Path
25
+
26
+ import numpy as np
27
+
28
+ REPO_ROOT = Path(__file__).resolve().parent.parent
29
+ sys.path.insert(0, str(REPO_ROOT / 'src'))
30
+
31
+ from ocdkit.plot.image_grid import image_grid # noqa: E402
32
+
33
+
34
+ N = 4 # 4x4 = 16 shades from 0 → 1
35
+ TILE_PX = 96
36
+
37
+
38
+ def _make_grid(*, hdr: bool):
39
+ shades = np.linspace(0.0, 1.0, N * N)
40
+ tiles = []
41
+ labels = []
42
+ for v in shades:
43
+ if hdr:
44
+ arr = np.full((TILE_PX, TILE_PX, 3), float(v), dtype=np.float32)
45
+ else:
46
+ arr = np.full((TILE_PX, TILE_PX, 3), int(round(v * 255)),
47
+ dtype=np.uint8)
48
+ tiles.append(arr)
49
+ labels.append(f'{v:.2f}')
50
+ return image_grid(
51
+ tiles, ncol=N, plot_labels=labels,
52
+ fontcolor='auto',
53
+ fontsize=10, figsize=N, lpos='top_middle',
54
+ )
55
+
56
+
57
+ def main() -> None:
58
+ sdr_fig = _make_grid(hdr=False)
59
+ hdr_fig = _make_grid(hdr=True)
60
+
61
+ fig_dir = REPO_ROOT / 'figures'
62
+ fig_dir.mkdir(exist_ok=True)
63
+ html_path = fig_dir / 'image_grid_auto_color.html'
64
+ svg_path = fig_dir / 'image_grid_auto_color.svg'
65
+
66
+ sdr_html = sdr_fig._repr_mimebundle_()['text/html']
67
+ hdr_html = hdr_fig._repr_mimebundle_()['text/html']
68
+ html_body = (
69
+ '<div style="display:flex;gap:32px;align-items:flex-start;flex-wrap:wrap">'
70
+ f'<div><h3>SDR (uint8 tiles)</h3>{sdr_html}</div>'
71
+ f'<div><h3>HDR (linear-light float, encoded as UHDR)</h3>{hdr_html}</div>'
72
+ '</div>'
73
+ )
74
+ # Standalone HTML wrapper: honour the host's color scheme but also
75
+ # let the user force a background to verify contrast either way.
76
+ full_html = (
77
+ '<!DOCTYPE html>\n'
78
+ '<html><head><meta charset="utf-8">'
79
+ '<title>image_grid auto color check</title>'
80
+ '<style>'
81
+ 'html { color-scheme: light dark; }'
82
+ 'body { font-family: system-ui, sans-serif; '
83
+ ' margin: 24px; '
84
+ ' background: light-dark(#ffffff, #1e1e1e); '
85
+ ' color: light-dark(#1a1a1a, #f0f0f0); }'
86
+ '.swatch { display: flex; gap: 16px; margin-bottom: 12px; }'
87
+ '.swatch button { padding: 6px 10px; cursor: pointer; }'
88
+ '</style></head><body>'
89
+ '<h2>image_grid(fontcolor=\'auto\') — adaptive label check</h2>'
90
+ '<p>Labels should stay readable against every tile shade.'
91
+ ' Switch your OS / browser theme to flip light↔dark.</p>'
92
+ '<div class="swatch">'
93
+ '<button onclick="document.documentElement.style.colorScheme=\'light\'">'
94
+ 'Force light</button>'
95
+ '<button onclick="document.documentElement.style.colorScheme=\'dark\'">'
96
+ 'Force dark</button>'
97
+ '<button onclick="document.documentElement.style.colorScheme=\'\'">'
98
+ 'Follow system</button>'
99
+ '</div>'
100
+ f'{html_body}'
101
+ '</body></html>'
102
+ )
103
+ html_path.write_text(full_html)
104
+ sdr_fig.savefig(svg_path)
105
+
106
+ print(f'HTML: {html_path}')
107
+ print(f'SVG: {svg_path}')
108
+
109
+
110
+ if __name__ == '__main__':
111
+ main()
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env python
2
+ """Native-webview harness for the HDR colormap prototype.
3
+
4
+ Opens ``hdr_cmap_prototype.html`` in pywebview's macOS WKWebView and pumps the
5
+ LIVE EDR headroom — ``NSScreen.maximumExtendedDynamicRangeColorComponentValue``,
6
+ the value the browser deliberately hides from JS — into the page as
7
+ ``window.__edrHeadroom`` every 0.5 s. The page's ``detectHeadroom()`` already
8
+ consumes that global, so the colormap re-adapts to your real display headroom and
9
+ follows brightness changes, never clipping.
10
+
11
+ This also answers the open question: does WKWebView actually EDR-COMPOSITE the
12
+ WebGPU canvas inside a third-party app? If the lifted panel glows brighter than
13
+ SDR white here, it does. If it looks flat/SDR, the host window needs an EDR
14
+ opt-in (deeper PyObjC work) even though the headroom number reads fine.
15
+
16
+ Run: python scripts/hdr_harness.py
17
+ (needs: pip install pywebview pyobjc-framework-Cocoa)
18
+ """
19
+ import functools
20
+ import http.server
21
+ import os
22
+ import socketserver
23
+ import sys
24
+ import threading
25
+ import time
26
+
27
+ # Make the in-repo package importable so we dogfood the shared bridge helper.
28
+ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")))
29
+ from ocdkit.viewer.edr_bridge import start_edr_pump # noqa: E402
30
+
31
+ # Serve the package root so BOTH /viewer/web/ and the shared /plot/web/ modules
32
+ # resolve over HTTP.
33
+ WEB_DIR = os.path.abspath(
34
+ os.path.join(os.path.dirname(__file__), "..", "src", "ocdkit"))
35
+ PORT = 8013
36
+ URL = f"http://127.0.0.1:{PORT}/viewer/web/hdr_cmap_prototype.html"
37
+
38
+
39
+ def _serve():
40
+ """Serve the package root so /viewer/web/ and /plot/web/ both resolve
41
+ (self-contained — no dependency on a separately-running server)."""
42
+ handler = functools.partial(http.server.SimpleHTTPRequestHandler, directory=WEB_DIR)
43
+ socketserver.TCPServer.allow_reuse_address = True
44
+ with socketserver.TCPServer(("127.0.0.1", PORT), handler) as httpd:
45
+ httpd.serve_forever()
46
+
47
+
48
+ def main():
49
+ try:
50
+ import webview
51
+ except ImportError:
52
+ raise SystemExit("pywebview not installed — run: pip install pywebview")
53
+
54
+ threading.Thread(target=_serve, daemon=True).start()
55
+ time.sleep(0.3)
56
+ print(f"[harness] serving {WEB_DIR}\n[harness] opening {URL}")
57
+ print("[harness] move the window onto your HDR display; change screen "
58
+ "brightness and watch the in-page 'reported headroom' track it.")
59
+ win = webview.create_window(
60
+ "ocdkit HDR colormap — EDR-pumped", URL, width=1120, height=860)
61
+ # debug=True enables the WKWebView inspector + context menu (right-click →
62
+ # Reload, or ⌘R) so edits to the served files reload in place.
63
+ webview.start(
64
+ lambda: start_edr_pump(win, on_value=lambda hr: print(f"[edr] headroom = {hr:.3f}x -> window.__edrHeadroom")),
65
+ debug=True)
66
+
67
+
68
+ if __name__ == "__main__":
69
+ main()
@@ -0,0 +1,397 @@
1
+ """CIE 1931 chromaticity diagram with Display-P3-accurate fill.
2
+
3
+ - Transparent background, gray axes / labels (dark-mode rcParams convention).
4
+ - Region inside Display-P3 gamut is colored with the actual P3 RGB triplet
5
+ for each chromaticity (max-normalized per pixel). On a P3 display, colors
6
+ *between* the sRGB and P3 triangles render with full P3 saturation; sRGB
7
+ displays narrow them to their gamut.
8
+ - Everything outside the P3 triangle is left transparent — those
9
+ chromaticities cannot be displayed on a P3 monitor.
10
+
11
+ Outputs:
12
+ - figures/chromaticity_p3.svg — JXL raster tagged display-p3; best in Safari.
13
+ - figures/chromaticity_p3.png — flat PNG with an embedded Display P3 ICC
14
+ profile (iCCP chunk). PowerPoint / Keynote / Preview honor the profile
15
+ and render the wide gamut on a P3 display.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import sys
20
+ from pathlib import Path
21
+
22
+ import numpy as np
23
+
24
+ REPO = Path(__file__).resolve().parents[1]
25
+ sys.path.insert(0, str(REPO / "src"))
26
+
27
+ from ocdkit.plot.svg import SVG, jxl_data_url # noqa: E402
28
+
29
+
30
+ # Linear Display-P3 ← XYZ (D65) — same matrix used in ocdkit.plot.hdr_cmap
31
+ _P3_FROM_XYZ = np.array([
32
+ [ 2.4934969119, -0.9313836179, -0.4027107845],
33
+ [-0.8294889696, 1.7626640603, 0.0236246858],
34
+ [ 0.0358458302, -0.0761723893, 0.9568845240],
35
+ ])
36
+
37
+ # Gamut primaries (xy chromaticities, D65)
38
+ SRGB_PRIMARIES = np.array([(0.6400, 0.3300), (0.3000, 0.6000), (0.1500, 0.0600)])
39
+ P3_PRIMARIES = np.array([(0.6800, 0.3200), (0.2650, 0.6900), (0.1500, 0.0600)])
40
+ D65 = (0.3127, 0.3290)
41
+
42
+ # CIE 1931 2° observer color-matching functions (10 nm samples).
43
+ _CMF = np.array([
44
+ [380, 0.001368, 0.000039, 0.006450],
45
+ [390, 0.004243, 0.000120, 0.020050],
46
+ [400, 0.014310, 0.000396, 0.067850],
47
+ [410, 0.043510, 0.001210, 0.207400],
48
+ [420, 0.134380, 0.004000, 0.645600],
49
+ [430, 0.283900, 0.011600, 1.385600],
50
+ [440, 0.348280, 0.023000, 1.747060],
51
+ [450, 0.336200, 0.038000, 1.772110],
52
+ [460, 0.290800, 0.060000, 1.669200],
53
+ [470, 0.195360, 0.090980, 1.287640],
54
+ [480, 0.095640, 0.139020, 0.812950],
55
+ [490, 0.032010, 0.208020, 0.465180],
56
+ [500, 0.004900, 0.323000, 0.272000],
57
+ [510, 0.009300, 0.503000, 0.158200],
58
+ [520, 0.063270, 0.710000, 0.078250],
59
+ [530, 0.165500, 0.862000, 0.042160],
60
+ [540, 0.290400, 0.954000, 0.020300],
61
+ [550, 0.433450, 0.994950, 0.008750],
62
+ [560, 0.594500, 0.995000, 0.003900],
63
+ [570, 0.762100, 0.952000, 0.002100],
64
+ [580, 0.916300, 0.870000, 0.001650],
65
+ [590, 1.026300, 0.757000, 0.001100],
66
+ [600, 1.062200, 0.631000, 0.000800],
67
+ [610, 1.002600, 0.503000, 0.000340],
68
+ [620, 0.854450, 0.381000, 0.000190],
69
+ [630, 0.642400, 0.265000, 0.000050],
70
+ [640, 0.447900, 0.175000, 0.000020],
71
+ [650, 0.283500, 0.107000, 0.000000],
72
+ [660, 0.164900, 0.061000, 0.000000],
73
+ [670, 0.087400, 0.032000, 0.000000],
74
+ [680, 0.046770, 0.017000, 0.000000],
75
+ [690, 0.022700, 0.008210, 0.000000],
76
+ [700, 0.011360, 0.004102, 0.000000],
77
+ [710, 0.005790, 0.002091, 0.000000],
78
+ [720, 0.002899, 0.001047, 0.000000],
79
+ [730, 0.001440, 0.000520, 0.000000],
80
+ ])
81
+ def _densify_locus(cmf: np.ndarray, step_nm: float = 1.0):
82
+ """Upsample the 10 nm CMF to a fine spectral-locus polyline.
83
+
84
+ A cubic spline on each tristimulus channel keeps the locus visually
85
+ smooth at the sharp 380-440 nm bend and along the 460-540 nm shoulder
86
+ where 10 nm sampling looks polygonal. xy is computed AFTER the spline
87
+ so the locus stays consistent with the densified CMF.
88
+ """
89
+ from scipy.interpolate import CubicSpline
90
+
91
+ nm_dense = np.arange(cmf[0, 0], cmf[-1, 0] + step_nm * 0.5, step_nm)
92
+ xyz_dense = np.column_stack([
93
+ CubicSpline(cmf[:, 0], cmf[:, k])(nm_dense)
94
+ for k in (1, 2, 3)
95
+ ])
96
+ xyz_dense = np.maximum(xyz_dense, 0.0)
97
+ s = xyz_dense.sum(axis=1)
98
+ return nm_dense, xyz_dense[:, 0] / s, xyz_dense[:, 1] / s
99
+
100
+
101
+ LOCUS_NM, LOCUS_X, LOCUS_Y = _densify_locus(_CMF, step_nm=1.0)
102
+
103
+
104
+ def srgb_oetf(x: np.ndarray) -> np.ndarray:
105
+ """Apply the sRGB / Display-P3 transfer curve (linear → encoded)."""
106
+ x = np.clip(x, 0.0, 1.0)
107
+ return np.where(
108
+ x <= 0.0031308,
109
+ 12.92 * x,
110
+ 1.055 * np.power(np.maximum(x, 1e-12), 1.0 / 2.4) - 0.055,
111
+ )
112
+
113
+
114
+ def make_chromaticity_raster(res: int, x_max: float, y_max: float) -> np.ndarray:
115
+ """(res, res, 4) uint8 RGBA — top-left origin, y_max at top.
116
+
117
+ For each pixel (chromaticity x, y) we set Y=1, compute XYZ → linear-P3,
118
+ and (if all components are ≥ 0) normalize so the max channel = 1 — the
119
+ most saturated P3 color at that chromaticity. sRGB OETF for encoding,
120
+ then quantize to uint8. Pixels outside the P3 triangle get alpha = 0.
121
+ """
122
+ xs = np.linspace(0.0, x_max, res)
123
+ ys = np.linspace(y_max, 0.0, res)
124
+ XX, YY = np.meshgrid(xs, ys)
125
+ safe_y = np.where(YY > 1e-4, YY, 1.0)
126
+ X = XX / safe_y
127
+ Y = np.ones_like(XX)
128
+ Z = (1.0 - XX - YY) / safe_y
129
+ XYZ = np.stack([X, Y, Z], axis=-1)
130
+ lin = XYZ @ _P3_FROM_XYZ.T
131
+ # Soft alpha at the gamut boundary: 1 deep inside, ramps to 0 just past
132
+ # the boundary where the most-negative channel exceeds ``feather``.
133
+ feather = 0.004
134
+ neg = -np.minimum(lin.min(axis=-1), 0.0)
135
+ alpha = np.clip(1.0 - neg / feather, 0.0, 1.0)
136
+ alpha[YY <= 1e-3] = 0.0
137
+ # Per-pixel max normalize the (clipped-positive) linear-P3 values.
138
+ lin_pos = np.clip(lin, 0.0, None)
139
+ mx = lin_pos.max(axis=-1, keepdims=True)
140
+ mx_safe = np.where(mx > 1e-9, mx, 1.0)
141
+ rgb = srgb_oetf(lin_pos / mx_safe)
142
+ rgba = np.concatenate([rgb, alpha[..., None]], axis=-1)
143
+ return (rgba * 255.0).round().clip(0, 255).astype(np.uint8)
144
+
145
+
146
+ X_MAX, Y_MAX = 0.8, 0.9
147
+
148
+
149
+ def build_svg(raster: np.ndarray) -> str:
150
+ PLOT_W, PLOT_H = 640, 720
151
+ M_L, M_R, M_T, M_B = 70, 40, 50, 60
152
+ W = PLOT_W + M_L + M_R
153
+ H = PLOT_H + M_T + M_B
154
+ svg = SVG(W, H)
155
+
156
+ def xpx(x: float) -> float:
157
+ return M_L + (x / X_MAX) * PLOT_W
158
+
159
+ def ypx(y: float) -> float:
160
+ return M_T + PLOT_H - (y / Y_MAX) * PLOT_H
161
+
162
+ # --- 1. Display-P3-tagged raster of the colored gamut --------------
163
+ raster_url = jxl_data_url(raster, color="display-p3")
164
+ svg.add(
165
+ f'<image x="{xpx(0):.2f}" y="{ypx(Y_MAX):.2f}" '
166
+ f'width="{PLOT_W:.2f}" height="{PLOT_H:.2f}" '
167
+ f'href="{raster_url}" preserveAspectRatio="none" '
168
+ f'image-rendering="auto"/>'
169
+ )
170
+
171
+ # --- 2. spectral locus + line of purples ---------------------------
172
+ locus_pts = list(zip(LOCUS_X, LOCUS_Y))
173
+ d_locus = "M " + " L ".join(f"{xpx(x):.2f},{ypx(y):.2f}" for x, y in locus_pts)
174
+ svg.path(d_locus, stroke="gray", stroke_width=1.0, fill="none")
175
+ # line of purples (last sample → first sample), dashed
176
+ x0, y0 = locus_pts[0]
177
+ x1, y1 = locus_pts[-1]
178
+ svg.line(xpx(x1), ypx(y1), xpx(x0), ypx(y0),
179
+ stroke="gray", stroke_width=1.0, dasharray="4,3")
180
+
181
+ # --- 3. wavelength tick marks + labels -----------------------------
182
+ # Tick at every entry; label only the well-separated ones (the locus
183
+ # bunches near 460-480 nm and 510-520 nm, so we'd otherwise overlap).
184
+ tick_nm = [400, 460, 470, 480, 490, 500, 510, 520,
185
+ 540, 560, 580, 600, 620, 700]
186
+ label_nm = {400, 460, 480, 500, 520, 540, 560, 580, 600, 620, 700}
187
+ for nm in tick_nm:
188
+ idx = int(np.argmin(np.abs(LOCUS_NM - nm)))
189
+ lx, ly = LOCUS_X[idx], LOCUS_Y[idx]
190
+ dx, dy = lx - D65[0], ly - D65[1]
191
+ n = float(np.hypot(dx, dy)) or 1.0
192
+ ux, uy = dx / n, dy / n
193
+ x0p, y0p = xpx(lx), ypx(ly)
194
+ svg.line(x0p, y0p,
195
+ x0p + ux * 6.0, y0p - uy * 6.0,
196
+ stroke="gray", stroke_width=1)
197
+ if nm in label_nm:
198
+ svg.text(x0p + ux * 24.0, y0p - uy * 24.0,
199
+ str(nm), size=10, fill="gray",
200
+ anchor="middle", baseline="middle")
201
+
202
+ # --- 4. gamut triangles --------------------------------------------
203
+ tri = lambda P: [(xpx(x), ypx(y)) for x, y in P]
204
+ # sRGB: solid stroke
205
+ svg.polygon(tri(SRGB_PRIMARIES), fill="none",
206
+ stroke="gray", stroke_width=1.6)
207
+ # P3: dashed stroke (so the two outlines never alias to the same line)
208
+ p3_d = ("M " + " L ".join(f"{x:.2f},{y:.2f}" for x, y in tri(P3_PRIMARIES))
209
+ + " Z")
210
+ svg.path(p3_d, stroke="gray", stroke_width=1.6, fill="none",
211
+ dasharray="6,3")
212
+
213
+ # --- 5. D65 white point --------------------------------------------
214
+ svg.add(
215
+ f'<circle cx="{xpx(D65[0]):.2f}" cy="{ypx(D65[1]):.2f}" '
216
+ f'r="3" fill="gray" stroke="none"/>'
217
+ )
218
+ svg.text(xpx(D65[0]) + 6, ypx(D65[1]) - 6, "D65",
219
+ size=10, fill="gray", anchor="start")
220
+
221
+ # --- 6. axes -------------------------------------------------------
222
+ svg.line(xpx(0), ypx(0), xpx(X_MAX), ypx(0),
223
+ stroke="gray", stroke_width=1)
224
+ svg.line(xpx(0), ypx(0), xpx(0), ypx(Y_MAX),
225
+ stroke="gray", stroke_width=1)
226
+ for tx in np.arange(0.0, X_MAX + 1e-9, 0.1):
227
+ px = xpx(tx)
228
+ svg.line(px, ypx(0), px, ypx(0) + 5,
229
+ stroke="gray", stroke_width=1)
230
+ svg.text(px, ypx(0) + 18, f"{tx:.1f}",
231
+ size=10, fill="gray", anchor="middle")
232
+ for ty in np.arange(0.0, Y_MAX + 1e-9, 0.1):
233
+ py = ypx(ty)
234
+ svg.line(xpx(0) - 5, py, xpx(0), py,
235
+ stroke="gray", stroke_width=1)
236
+ svg.text(xpx(0) - 8, py + 4, f"{ty:.1f}",
237
+ size=10, fill="gray", anchor="end")
238
+ svg.text((xpx(0) + xpx(X_MAX)) / 2, ypx(0) + 38, "x",
239
+ size=14, fill="gray", anchor="middle",
240
+ baseline="middle")
241
+ svg.text(xpx(0) - 42, (ypx(0) + ypx(Y_MAX)) / 2, "y",
242
+ size=14, fill="gray", anchor="middle",
243
+ baseline="middle")
244
+
245
+ # --- 7. legend (gamut key) -----------------------------------------
246
+ lg_x = xpx(0.50)
247
+ lg_y = ypx(0.85)
248
+ svg.line(lg_x, lg_y, lg_x + 32, lg_y,
249
+ stroke="gray", stroke_width=1.6)
250
+ svg.text(lg_x + 38, lg_y + 4, "sRGB",
251
+ size=11, fill="gray", anchor="start")
252
+ svg.line(lg_x, lg_y + 18, lg_x + 32, lg_y + 18,
253
+ stroke="gray", stroke_width=1.6,
254
+ dasharray="6,3")
255
+ svg.text(lg_x + 38, lg_y + 22, "Display P3",
256
+ size=11, fill="gray", anchor="start")
257
+
258
+ # title
259
+ svg.text(W / 2, 24,
260
+ "CIE 1931 chromaticity — Display-P3 fill",
261
+ size=13, fill="gray",
262
+ anchor="middle", baseline="middle")
263
+
264
+ return svg.finalize()
265
+
266
+
267
+ def build_png_rgba(raster: np.ndarray, fig_in: float = 8.0,
268
+ dpi: int = 300) -> np.ndarray:
269
+ """Render the figure to an (H, W, 4) uint8 RGBA array via matplotlib Agg.
270
+
271
+ The chromaticity raster is laid into the axes via ``imshow`` as raw
272
+ uint8 — matplotlib + Agg do not color-transform pre-encoded RGBA, so
273
+ the Display-P3-encoded bytes pass through to the final composite. The
274
+ gray overlays (axes, ticks, labels, gamut triangles, locus) are
275
+ neutral, so their byte values are identical under sRGB and P3
276
+ interpretation — the composite remains a valid P3 buffer.
277
+ """
278
+ import matplotlib
279
+ matplotlib.use("Agg")
280
+ import matplotlib.pyplot as plt
281
+ plt.rcParams.update({
282
+ "figure.facecolor": "none",
283
+ "axes.facecolor": "none",
284
+ "savefig.facecolor": "none",
285
+ "axes.edgecolor": "gray",
286
+ "axes.labelcolor": "gray",
287
+ "xtick.color": "gray",
288
+ "ytick.color": "gray",
289
+ "text.color": "gray",
290
+ "axes.titlecolor": "gray",
291
+ })
292
+
293
+ fig, ax = plt.subplots(figsize=(fig_in, fig_in), dpi=dpi)
294
+
295
+ # 1. Display-P3-encoded raster — interpolation='bilinear' is fine
296
+ # because blending neighboring P3-encoded samples on a smooth
297
+ # chromaticity gradient just gives the in-between P3 value.
298
+ ax.imshow(raster, extent=(0.0, X_MAX, 0.0, Y_MAX), origin="upper",
299
+ interpolation="bilinear", zorder=1)
300
+
301
+ # 2. spectral locus
302
+ ax.plot(LOCUS_X, LOCUS_Y, color="gray", lw=1.0, zorder=2)
303
+ # line of purples (last locus point → first), dashed
304
+ ax.plot([LOCUS_X[-1], LOCUS_X[0]], [LOCUS_Y[-1], LOCUS_Y[0]],
305
+ color="gray", lw=1.0, dashes=(4, 3), zorder=2)
306
+
307
+ # 3. wavelength ticks + labels
308
+ tick_nm = [400, 460, 470, 480, 490, 500, 510, 520,
309
+ 540, 560, 580, 600, 620, 700]
310
+ label_nm = {400, 460, 480, 500, 520, 540, 560, 580, 600, 620, 700}
311
+ tick_pt = 5 # tick length, screen points
312
+ label_pt = 14 # label offset, screen points
313
+ for nm in tick_nm:
314
+ idx = int(np.argmin(np.abs(LOCUS_NM - nm)))
315
+ lx, ly = float(LOCUS_X[idx]), float(LOCUS_Y[idx])
316
+ dx, dy = lx - D65[0], ly - D65[1]
317
+ n = float(np.hypot(dx, dy)) or 1.0
318
+ ux, uy = dx / n, dy / n
319
+ # tick: use annotate with offset transform so length is in points
320
+ ax.annotate("", xy=(lx, ly), xycoords="data",
321
+ xytext=(ux * tick_pt, uy * tick_pt),
322
+ textcoords="offset points",
323
+ arrowprops=dict(arrowstyle="-", color="gray", lw=1.0))
324
+ if nm in label_nm:
325
+ ax.annotate(str(nm), xy=(lx, ly),
326
+ xytext=(ux * label_pt, uy * label_pt),
327
+ textcoords="offset points",
328
+ color="gray", fontsize=8,
329
+ ha="center", va="center")
330
+
331
+ # 4. gamut triangles
332
+ def plot_tri(P, **kw):
333
+ Pc = np.vstack([P, P[:1]])
334
+ ax.plot(Pc[:, 0], Pc[:, 1], **kw)
335
+
336
+ plot_tri(SRGB_PRIMARIES, color="gray", lw=1.6, label="sRGB", zorder=3)
337
+ plot_tri(P3_PRIMARIES, color="gray", lw=1.6,
338
+ dashes=(6, 3), label="Display P3", zorder=3)
339
+
340
+ # 5. white point
341
+ ax.plot(D65[0], D65[1], "o", color="gray", markersize=4, zorder=4)
342
+ ax.annotate("D65", xy=D65, xytext=(6, 6),
343
+ textcoords="offset points", color="gray", fontsize=8)
344
+
345
+ ax.set_xlim(0.0, X_MAX)
346
+ ax.set_ylim(0.0, Y_MAX)
347
+ ax.set_xlabel("x")
348
+ ax.set_ylabel("y")
349
+ ax.set_aspect("equal")
350
+ ax.set_title("CIE 1931 chromaticity — Display-P3 fill")
351
+ leg = ax.legend(loc="upper right", frameon=False)
352
+ for txt in leg.get_texts():
353
+ txt.set_color("gray")
354
+
355
+ fig.tight_layout()
356
+ fig.canvas.draw()
357
+ rgba = np.asarray(fig.canvas.buffer_rgba()).copy()
358
+ plt.close(fig)
359
+ return rgba
360
+
361
+
362
+ def encode_png_display_p3(rgba: np.ndarray) -> bytes:
363
+ """Encode RGBA → PNG bytes with an embedded Display P3 ICC profile.
364
+
365
+ The ``iCCP`` chunk is what tells PowerPoint / Keynote / Preview etc.
366
+ that the pixel values are Display-P3-encoded — without it they fall
367
+ back to sRGB and the wide-gamut extension between sRGB and P3 gets
368
+ narrowed.
369
+ """
370
+ from opencodecs._png_codec import PngCodec
371
+ icc_path = Path("/System/Library/ColorSync/Profiles/Display P3.icc")
372
+ icc_bytes = icc_path.read_bytes()
373
+ return PngCodec().encode(
374
+ np.ascontiguousarray(rgba),
375
+ iccprofile=icc_bytes,
376
+ iccprofile_name="Display P3",
377
+ )
378
+
379
+
380
+ def main() -> None:
381
+ figures = REPO / "figures"
382
+ figures.mkdir(exist_ok=True)
383
+
384
+ raster = make_chromaticity_raster(res=2048, x_max=X_MAX, y_max=Y_MAX)
385
+
386
+ svg_out = figures / "chromaticity_p3.svg"
387
+ svg_out.write_text(build_svg(raster))
388
+ print(f"wrote {svg_out}")
389
+
390
+ png_out = figures / "chromaticity_p3.png"
391
+ rgba = build_png_rgba(raster)
392
+ png_out.write_bytes(encode_png_display_p3(rgba))
393
+ print(f"wrote {png_out}")
394
+
395
+
396
+ if __name__ == "__main__":
397
+ main()