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.
- {ocdkit-0.0.5/src/ocdkit.egg-info → ocdkit-0.0.7}/PKG-INFO +4 -1
- ocdkit-0.0.7/badges/coverage.svg +1 -0
- ocdkit-0.0.7/badges/tests.svg +1 -0
- ocdkit-0.0.7/jupyter-config/jupyter_server_config.d/ocdkit_tileserve.json +7 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/pyproject.toml +14 -0
- ocdkit-0.0.7/scripts/check_image_grid_auto_color.py +111 -0
- ocdkit-0.0.7/scripts/hdr_harness.py +69 -0
- ocdkit-0.0.7/scripts/make_chromaticity_p3.py +397 -0
- ocdkit-0.0.7/src/ocdkit/io/fig_export.py +549 -0
- ocdkit-0.0.7/src/ocdkit/io/figure.py +5591 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/io/figure_server.py +132 -259
- ocdkit-0.0.7/src/ocdkit/io/live_figure.py +72 -0
- ocdkit-0.0.7/src/ocdkit/io/mpl_axes_cropper.py +302 -0
- ocdkit-0.0.7/src/ocdkit/io/pptx.py +1333 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/plot/__init__.py +11 -14
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/plot/composite_grid.py +64 -6
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/plot/contour.py +90 -3
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/plot/display.py +115 -12
- ocdkit-0.0.7/src/ocdkit/plot/image_grid.py +1608 -0
- ocdkit-0.0.7/src/ocdkit/plot/luts.py +59 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/plot/ncolor.py +24 -0
- ocdkit-0.0.7/src/ocdkit/plot/pyramid.py +68 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/plot/svg.py +41 -34
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/plot/text_metrics.py +16 -1
- ocdkit-0.0.7/src/ocdkit/plot/web/colormap_image.js +168 -0
- ocdkit-0.0.7/src/ocdkit/plot/web/hdr_colormap.js +432 -0
- ocdkit-0.0.7/src/ocdkit/plot/web/hdr_headroom.js +89 -0
- ocdkit-0.0.7/src/ocdkit/plot/web/label_gl.js +529 -0
- ocdkit-0.0.7/src/ocdkit/plot/web/scatter_gl.js +161 -0
- ocdkit-0.0.7/src/ocdkit/plot/web/spectra_density_element.js +123 -0
- ocdkit-0.0.7/src/ocdkit/plot/web/spectra_density_gl.js +796 -0
- ocdkit-0.0.7/src/ocdkit/plot/web/tile_grid_element.js +95 -0
- ocdkit-0.0.7/src/ocdkit/tileserve/__init__.py +21 -0
- ocdkit-0.0.7/src/ocdkit/tileserve/headless.py +356 -0
- ocdkit-0.0.7/src/ocdkit/tileserve/jupyter_ext.py +117 -0
- ocdkit-0.0.7/src/ocdkit/tileserve/layout.py +148 -0
- ocdkit-0.0.7/src/ocdkit/tileserve/server.py +520 -0
- ocdkit-0.0.7/src/ocdkit/tileserve/shaders.py +100 -0
- ocdkit-0.0.7/src/ocdkit/tileserve/viewer.py +2387 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/app.py +13 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/assets.py +18 -0
- ocdkit-0.0.7/src/ocdkit/viewer/edr_bridge.py +74 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/app.js +47 -4
- ocdkit-0.0.7/src/ocdkit/viewer/web/hdr_cmap_prototype.html +129 -0
- ocdkit-0.0.7/src/ocdkit/viewer/web/hdr_cmap_uhdr.html +32 -0
- ocdkit-0.0.7/src/ocdkit/viewer/web/hdr_css_test.html +16 -0
- ocdkit-0.0.7/src/ocdkit/viewer/web/html/_attic_omnipose-panel.html +130 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/js/colormap.js +231 -0
- ocdkit-0.0.7/src/ocdkit/viewer/web/js/hdr_image_layer.js +118 -0
- ocdkit-0.0.7/src/ocdkit/viewer/web/js/hdr_ui.js +165 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7/src/ocdkit.egg-info}/PKG-INFO +4 -1
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit.egg-info/SOURCES.txt +35 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit.egg-info/requires.txt +3 -0
- ocdkit-0.0.7/tests/test_io_live_figure.py +66 -0
- ocdkit-0.0.7/tests/test_io_pptx.py +55 -0
- ocdkit-0.0.7/tests/test_tileserve_pick_level.py +51 -0
- ocdkit-0.0.5/badges/coverage.svg +0 -1
- ocdkit-0.0.5/badges/tests.svg +0 -1
- ocdkit-0.0.5/src/ocdkit/io/figure.py +0 -2897
- ocdkit-0.0.5/src/ocdkit/plot/image_grid.py +0 -877
- {ocdkit-0.0.5 → ocdkit-0.0.7}/.github/workflows/test_and_deploy.yml +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/.gitignore +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/LICENSE +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/MANIFEST.in +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/README.md +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/docs/plot-backend-roadmap.md +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/docs/plugin-authoring.md +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/docs/pywebview-desktop-integration.md +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/scripts/bench_colorize.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/scripts/bench_contour_alignment.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/scripts/bench_hdr_cmap.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/scripts/bench_vector_contours.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/scripts/bench_vector_contours_tier2.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/scripts/check_compare_pixels.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/scripts/check_hdr_cmap.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/scripts/check_jxl_p3_bytes.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/scripts/check_playwright_render.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/scripts/check_uhdr_sdr_base.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/scripts/coverage_cross_device.env.example +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/scripts/coverage_cross_device.sh +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/setup.cfg +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/__init__.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/__main__.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/array/__init__.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/array/convert.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/array/filters.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/array/imports.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/array/index.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/array/morphology.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/array/normalize.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/array/ops.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/array/parallel.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/array/spatial.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/array/transform.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/array/union_find.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/array/warp.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/cli/__init__.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/cli/__main__.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/cli/main.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/cli/migrate.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/cli/paths.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/desktop/__init__.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/desktop/pinning.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/imports.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/io/__init__.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/io/files.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/io/image.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/io/imports.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/io/path.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/io/result.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/load/__init__.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/load/module.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/load/object.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/logging/__init__.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/logging/handler.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/measure/__init__.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/measure/bbox.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/measure/diameter.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/measure/imports.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/measure/medoid.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/measure/metrics.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/plot/bench.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/plot/color.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/plot/composite.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/plot/defaults.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/plot/export.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/plot/figure.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/plot/grid.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/plot/hdr_cmap.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/plot/imports.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/plot/label.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/plot/layout.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/plot/style.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/testing/__init__.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/testing/collisions.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/testing/imports.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/tls/__init__.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/tls/external_ca.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/tls/hostnames.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/tls/imports.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/tls/local_ca.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/tls/paths.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/tls/trust.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/utils/__init__.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/utils/collections.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/utils/gpu.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/utils/kwargs.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/utils/paths.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/__init__.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/__main__.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/cli.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/demo.html +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/dependencies.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/exceptions.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/masks.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/middleware.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/model_registry.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/plugins/__init__.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/plugins/base.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/plugins/registry.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/plugins/schema.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/plugins/threshold.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/routers/__init__.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/routers/index.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/routers/log.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/routers/mask.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/routers/plugin.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/routers/segment.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/routers/session_routes.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/routers/system.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/routers/trust.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/routes.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/sample_image.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/schemas.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/segmentation.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/session.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/system.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/css/controls.css +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/css/layout.css +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/css/tools.css +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/css/viewer.css +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/html/left-panel.html +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/html/sidebar.html +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/html/viewer.html +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/icons/affinity.svg +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/icons/arrow-back-up.svg +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/icons/arrow-forward-up.svg +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/icons/dbscan-nested-arcs.svg +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/icons/download.svg +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/icons/droplet-half-2.svg +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/icons/eraser.svg +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/icons/home-2.svg +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/icons/minus.svg +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/icons/palette.svg +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/icons/pencil.svg +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/icons/plus.svg +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/icons/rotate-rectangle.svg +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/icons/topology-star.svg +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/index.html +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/js/brush.js +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/js/debug-apple-material.js +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/js/file-navigation.js +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/js/history.js +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/js/interactions.js +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/js/logging.js +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/js/mask-pipeline.js +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/js/painting.js +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/js/plugin-panel.js +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/js/pointer-state.js +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/js/state-persistence.js +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/js/tooltip-editor.js +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/js/ui-utils.js +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit/viewer/web/js/wasm_fill.c +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit.egg-info/dependency_links.txt +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit.egg-info/entry_points.txt +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/src/ocdkit.egg-info/top_level.txt +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/tests/e2e/__init__.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/tests/e2e/conftest.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/tests/e2e/test_browser_smoke.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/tests/e2e/test_pywebview_snapshot.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/tests/fixtures/multichan_3c_4x4.czi +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/tests/fixtures/tiny_8x8.czi +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/tests/test_array.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/tests/test_gpu.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/tests/test_import_cycles.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/tests/test_io.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/tests/test_measure.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/tests/test_module_collisions.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/tests/test_module_discovery.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/tests/test_morphology.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/tests/test_paths_migration.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/tests/test_plot_color.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/tests/test_plot_contour.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/tests/test_plot_display.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/tests/test_plot_export.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/tests/test_plot_figure.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/tests/test_plot_grid.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/tests/test_plot_label.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/tests/test_plot_notebook.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/tests/test_registration.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/tests/test_slice.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/tests/test_spatial.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/tests/viewer/__init__.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/tests/viewer/test_active_plugin_cache.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/tests/viewer/test_app.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/tests/viewer/test_async_dispatch.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/tests/viewer/test_envelope_and_middleware.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/tests/viewer/test_plugin_contract.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/tests/viewer/test_session_eviction.py +0 -0
- {ocdkit-0.0.5 → ocdkit-0.0.7}/tests/viewer/test_title_config.py +0 -0
- {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.
|
|
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>
|
|
@@ -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()
|