spatialdata-js-util 0.1.0__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 (61) hide show
  1. spatialdata_js_util-0.1.0/PKG-INFO +412 -0
  2. spatialdata_js_util-0.1.0/README.md +379 -0
  3. spatialdata_js_util-0.1.0/pyproject.toml +88 -0
  4. spatialdata_js_util-0.1.0/setup.cfg +4 -0
  5. spatialdata_js_util-0.1.0/src/spatialdata_js_util/__init__.py +107 -0
  6. spatialdata_js_util-0.1.0/src/spatialdata_js_util/cli.py +803 -0
  7. spatialdata_js_util-0.1.0/src/spatialdata_js_util/codecs/__init__.py +67 -0
  8. spatialdata_js_util-0.1.0/src/spatialdata_js_util/codecs/backends.py +250 -0
  9. spatialdata_js_util-0.1.0/src/spatialdata_js_util/codecs/chunks.py +40 -0
  10. spatialdata_js_util-0.1.0/src/spatialdata_js_util/codecs/encoding.py +154 -0
  11. spatialdata_js_util-0.1.0/src/spatialdata_js_util/codecs/htj2k_wasm.py +266 -0
  12. spatialdata_js_util-0.1.0/src/spatialdata_js_util/codecs/names.py +26 -0
  13. spatialdata_js_util-0.1.0/src/spatialdata_js_util/codecs/probe/multicomponent.j2c +0 -0
  14. spatialdata_js_util-0.1.0/src/spatialdata_js_util/codecs/probe/multicomponent.json +3 -0
  15. spatialdata_js_util-0.1.0/src/spatialdata_js_util/codecs/probe/multicomponent.npy +0 -0
  16. spatialdata_js_util-0.1.0/src/spatialdata_js_util/codecs/vendor/__init__.py +1 -0
  17. spatialdata_js_util-0.1.0/src/spatialdata_js_util/codecs/vendor/encode-plane.mjs +164 -0
  18. spatialdata_js_util-0.1.0/src/spatialdata_js_util/codecs/vendor/openjph/index.mjs +131 -0
  19. spatialdata_js_util-0.1.0/src/spatialdata_js_util/codecs/vendor/openjph/wasm/libopenjph.mjs +2 -0
  20. spatialdata_js_util-0.1.0/src/spatialdata_js_util/codecs/vendor/openjph/wasm/libopenjph.wasm +0 -0
  21. spatialdata_js_util-0.1.0/src/spatialdata_js_util/codecs/vendor/openjph/wasm/libopenjph_simd.mjs +2 -0
  22. spatialdata_js_util-0.1.0/src/spatialdata_js_util/codecs/vendor/openjph/wasm/libopenjph_simd.wasm +0 -0
  23. spatialdata_js_util-0.1.0/src/spatialdata_js_util/codecs/zarr_codec.py +149 -0
  24. spatialdata_js_util-0.1.0/src/spatialdata_js_util/errors.py +5 -0
  25. spatialdata_js_util-0.1.0/src/spatialdata_js_util/images.py +849 -0
  26. spatialdata_js_util-0.1.0/src/spatialdata_js_util/index_permutations.py +311 -0
  27. spatialdata_js_util-0.1.0/src/spatialdata_js_util/points.py +445 -0
  28. spatialdata_js_util-0.1.0/src/spatialdata_js_util/points_zarr.py +799 -0
  29. spatialdata_js_util-0.1.0/src/spatialdata_js_util/provenance.py +17 -0
  30. spatialdata_js_util-0.1.0/src/spatialdata_js_util/pyramids.py +318 -0
  31. spatialdata_js_util-0.1.0/src/spatialdata_js_util/runners.py +458 -0
  32. spatialdata_js_util-0.1.0/src/spatialdata_js_util/store.py +305 -0
  33. spatialdata_js_util-0.1.0/src/spatialdata_js_util/tables.py +310 -0
  34. spatialdata_js_util-0.1.0/src/spatialdata_js_util/tui/__init__.py +0 -0
  35. spatialdata_js_util-0.1.0/src/spatialdata_js_util/tui/app.py +112 -0
  36. spatialdata_js_util-0.1.0/src/spatialdata_js_util/tui/models.py +45 -0
  37. spatialdata_js_util-0.1.0/src/spatialdata_js_util/tui/screens.py +1255 -0
  38. spatialdata_js_util-0.1.0/src/spatialdata_js_util/tui/widgets.py +50 -0
  39. spatialdata_js_util-0.1.0/src/spatialdata_js_util/verify.py +338 -0
  40. spatialdata_js_util-0.1.0/src/spatialdata_js_util.egg-info/PKG-INFO +412 -0
  41. spatialdata_js_util-0.1.0/src/spatialdata_js_util.egg-info/SOURCES.txt +59 -0
  42. spatialdata_js_util-0.1.0/src/spatialdata_js_util.egg-info/dependency_links.txt +1 -0
  43. spatialdata_js_util-0.1.0/src/spatialdata_js_util.egg-info/entry_points.txt +7 -0
  44. spatialdata_js_util-0.1.0/src/spatialdata_js_util.egg-info/requires.txt +19 -0
  45. spatialdata_js_util-0.1.0/src/spatialdata_js_util.egg-info/top_level.txt +1 -0
  46. spatialdata_js_util-0.1.0/tests/test_htj2k_encode.py +76 -0
  47. spatialdata_js_util-0.1.0/tests/test_htj2k_encode_demo.py +37 -0
  48. spatialdata_js_util-0.1.0/tests/test_htj2k_quality.py +37 -0
  49. spatialdata_js_util-0.1.0/tests/test_integration.py +236 -0
  50. spatialdata_js_util-0.1.0/tests/test_points.py +324 -0
  51. spatialdata_js_util-0.1.0/tests/test_points_zarr.py +347 -0
  52. spatialdata_js_util-0.1.0/tests/test_pyramids.py +272 -0
  53. spatialdata_js_util-0.1.0/tests/test_recompress.py +631 -0
  54. spatialdata_js_util-0.1.0/tests/test_synthetic_images.py +65 -0
  55. spatialdata_js_util-0.1.0/tests/test_tables.py +505 -0
  56. spatialdata_js_util-0.1.0/tests/test_tui.py +464 -0
  57. spatialdata_js_util-0.1.0/tests/test_verify.py +163 -0
  58. spatialdata_js_util-0.1.0/tests/test_write_synthetic.py +43 -0
  59. spatialdata_js_util-0.1.0/tests/test_writer.py +150 -0
  60. spatialdata_js_util-0.1.0/tests/test_zarr.py +113 -0
  61. spatialdata_js_util-0.1.0/tests/test_zarr_codec.py +221 -0
@@ -0,0 +1,412 @@
1
+ Metadata-Version: 2.4
2
+ Name: spatialdata-js-util
3
+ Version: 0.1.0
4
+ Summary: Read/write utilities for browser-oriented SpatialData stores (image codecs, Morton-indexed points, CSC tables)
5
+ Author: SpatialData.js contributors
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/Taylor-CCB-Group/SpatialData.js
8
+ Project-URL: Repository, https://github.com/Taylor-CCB-Group/SpatialData.js
9
+ Keywords: spatialdata,zarr,ome-zarr,htj2k,jpeg2000,spatial-omics
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
15
+ Requires-Python: >=3.12
16
+ Description-Content-Type: text/markdown
17
+ Requires-Dist: imagecodecs>=2025.8.2
18
+ Requires-Dist: numcodecs>=0.16
19
+ Requires-Dist: numpy>=2.0
20
+ Requires-Dist: pandas>=2.2
21
+ Requires-Dist: pyarrow>=18
22
+ Requires-Dist: zarr>=3.3.0
23
+ Provides-Extra: write
24
+ Requires-Dist: anndata>=0.12; extra == "write"
25
+ Requires-Dist: dask[array]>=2026.7.1; extra == "write"
26
+ Requires-Dist: ome-zarr>=0.16; extra == "write"
27
+ Requires-Dist: scipy>=1.11; extra == "write"
28
+ Requires-Dist: spatialdata>=0.8.0; extra == "write"
29
+ Provides-Extra: tui
30
+ Requires-Dist: textual>=1.0; extra == "tui"
31
+ Provides-Extra: all
32
+ Requires-Dist: spatialdata-js-util[tui,write]; extra == "all"
33
+
34
+ # spatialdata-js-util
35
+
36
+ Python utilities for producing — and reading back — SpatialData stores tuned for
37
+ incremental reading in the browser by
38
+ [SpatialData.js](https://github.com/Taylor-CCB-Group/SpatialData.js).
39
+
40
+ Three optimizations, all aimed at making a viewport cost a few small ranged reads
41
+ instead of a whole-element download:
42
+
43
+ | Area | What it does | Why |
44
+ |------|--------------|-----|
45
+ | **images** | Recompress rasters as JPEG 2000 or HTJ2K, and add [multiscale pyramids](#multiscale-pyramids) where they are missing | Tiles decode in the browser; HTJ2K is markedly cheaper to decode than JPEG 2000. Without a pyramid, a zoomed-out view still costs full-resolution chunks |
46
+ | **points** | Morton-sort Points Parquet with sentinel bbox rows and controlled row groups | A viewport maps to a few Parquet row-group reads |
47
+ | **tables** | Convert `X`/layers from CSR to CSC | Reading one gene becomes one contiguous range read, not a scan of every row |
48
+
49
+ Installing the package also **registers the image codecs with zarr**, so stores
50
+ written here open in ordinary `spatialdata.read_zarr` — see
51
+ [Reading stores back in Python](#reading-stores-back-in-python).
52
+
53
+ Every command is available three ways: an [interactive TUI](#interactive-tui)
54
+ (start here), the `spatialdata-js-util` CLI, and the [Python API](#python-api).
55
+
56
+ > **Status: experimental.** The HTJ2K codec identifier is not a standard
57
+ > OME-Zarr codec. Stores using it are readable by the `SpatialData.js` runtime
58
+ > and by Python with this package installed. It is published in the hope that,
59
+ > if useful, the approach can be adopted more broadly.
60
+
61
+ ## Install
62
+
63
+ ```bash
64
+ pip install spatialdata-js-util
65
+ ```
66
+
67
+ That base install is enough to **read** stores. To **write** them, add the
68
+ `write` extra, which pulls in `spatialdata`, `anndata`, `dask` and `scipy`:
69
+
70
+ ```bash
71
+ pip install 'spatialdata-js-util[write]'
72
+ ```
73
+
74
+ Requires Python 3.12+ and `spatialdata >= 0.8.0`.
75
+
76
+ ## Interactive TUI
77
+
78
+ If you would rather not memorise flags, everything below is also available as a
79
+ terminal UI. It is the easiest way to get started.
80
+
81
+ ```bash
82
+ pip install 'spatialdata-js-util[tui]'
83
+ spatialdata-js-util tui
84
+
85
+ # Or pre-fill the store path so you skip the first prompt
86
+ spatialdata-js-util tui ~/data/xenium.zarr
87
+ ```
88
+
89
+ Pick a command from the home menu and the UI walks you through it:
90
+
91
+ ```text
92
+ IMAGES Recompress rasters (JPEG 2000 / HTJ2K)
93
+ POINTS List Points elements in a Zarr store
94
+ POINTS Morton-sort Points from Zarr
95
+ POINTS Morton-sort CSV/Parquet file
96
+ POINTS Write multiscale Points Parquet
97
+ POINTS Write index permutations derivative store
98
+ TABLES List table elements in a Zarr store
99
+ TABLES Convert table matrices to CSC
100
+ CODECS Show HTJ2K backend availability
101
+ ```
102
+
103
+ The flow is the same for every command: **guided form → confirm → run → report.**
104
+
105
+ - **Forms** are pre-filled with sensible defaults, and with the store path you
106
+ already used this session. `Enter` advances between fields and submits on the
107
+ last one; `Escape` goes back; `q` quits.
108
+ - **Anything that overwrites asks first.** Rewriting `points/<key>/points.parquet`
109
+ in place, converting tables in place, or replacing an existing output store all
110
+ stop on a confirmation screen naming the exact path before touching anything.
111
+ - **Runs stream their JSON output** to a log pane, so a long recompression shows
112
+ progress rather than appearing to hang.
113
+ - The recompress form carries the [pyramid](#multiscale-pyramids) controls, so
114
+ single-resolution input can be made browser-ready without leaving the UI.
115
+ - **Writes are verified afterwards** and the report screen shows a pass/fail
116
+ table. For Morton writes that means:
117
+
118
+ | Check | Meaning |
119
+ |-------|---------|
120
+ | `column_present` | `morton_code_2d` column exists |
121
+ | `sentinel_prefix` | First 2–4 rows have `morton_code_2d == 0` |
122
+ | `sentinel_bbox` | Sentinel rows encode the full dataset x/y bounds |
123
+ | `morton_monotonic` | Morton codes non-decreasing after sentinels |
124
+ | `row_group_sentinels` | Row group 0 contains only sentinel rows |
125
+ | `no_uint_intermediates` | No persisted `*_uint` staging columns |
126
+
127
+ Multiscale and index-permutation runs show schema/manifest checks instead.
128
+
129
+ `CODECS → Show HTJ2K backend availability` is a good first stop: it tells you
130
+ which codec backend your environment resolved to, and whether it passed the
131
+ multi-component probe described [below](#htj2k-backends-and-why-there-is-a-probe).
132
+
133
+ ### Reading stores back in Python
134
+
135
+ ```python
136
+ import spatialdata as sd
137
+
138
+ sdata = sd.read_zarr("store-htj2k.zarr") # HTJ2K/JP2K images decode transparently
139
+ ```
140
+
141
+ No import of this package and no `register_codecs()` call is needed — the codecs
142
+ are advertised through `zarr.codecs` entry points, so zarr finds them as soon as
143
+ the distribution is installed. (`spatialdata_js_util.codecs.register_codecs()`
144
+ exists only for running from a source tree with no installed metadata.)
145
+
146
+ ### HTJ2K backends, and why there is a probe
147
+
148
+ HTJ2K encode/decode can go through either of two OpenJPH builds:
149
+
150
+ - **`imagecodecs`** — native OpenJPH, pure `pip install`, no Node.js. Preferred.
151
+ - **`openjph-wasm`** — the vendored WASM the JS reader uses, driven by Node.js
152
+ worker processes. Requires `node` on `PATH`.
153
+
154
+ A WASM build previously used in this project silently decoded *every* component
155
+ of a multi-component codestream as component 0 — losing volumetric planes with
156
+ no error (see [`docs/multi-component-codec-findings.md`](docs/multi-component-codec-findings.md)).
157
+ Because that failure is silent, a backend is not trusted on the strength of its
158
+ library name. `imagecodecs` is admitted only if it decodes a committed
159
+ multi-component codestream — produced by the WASM encoder — to exactly the
160
+ expected samples. Any mismatch, and it is rejected in favour of the WASM path.
161
+
162
+ Check what your environment resolved to:
163
+
164
+ ```bash
165
+ spatialdata-js-util codecs info
166
+ ```
167
+
168
+ Pin a backend explicitly with `SPATIALDATA_JS_UTIL_HTJ2K_BACKEND=imagecodecs`
169
+ or `=openjph-wasm` (the probe still gates `imagecodecs`).
170
+
171
+ ## Images
172
+
173
+ Recompress a store, preserving everything that is not an image. Path sources are
174
+ copied first, so tables, shapes, points and unconfigured rasters survive without
175
+ the whole object being loaded.
176
+
177
+ ```bash
178
+ spatialdata-js-util images recompress input.zarr output-htj2k.zarr \
179
+ --codec experimental.openjph_htj2k --preset balanced --chunks auto --overwrite
180
+ ```
181
+
182
+ With `--sibling`, each original image is kept and a compressed copy added
183
+ alongside it (`morphology_focus:htj2k_balanced`), so tools without the codec keep
184
+ working.
185
+
186
+ ### Presets and `--quality`
187
+
188
+ `--quality` is the OpenJPH quantization step: **lower means higher fidelity and a
189
+ larger codestream** — not a JPEG-style 0–100 scale. The step is relative to the
190
+ dtype's full dynamic range, so one input LSB is `1/256` for 8-bit data and
191
+ `1/65536` for 16-bit.
192
+
193
+ HTJ2K presets are therefore expressed as a **multiple of the input's LSB**, and
194
+ resolve to a concrete step from the raster's dtype:
195
+
196
+ | Preset | JPEG 2000 | HTJ2K | `uint8` | `uint16` |
197
+ |--------|-----------|-------|---------|----------|
198
+ | `lossless` | reversible, round-trip verified | `reversible=True` | — | — |
199
+ | `balanced` | `level=100` | 2 LSB | `0.0078125` | `0.000030518` |
200
+ | `small` | `level=75` | 5 LSB | `0.01953125` | `0.000076294` |
201
+
202
+ A step **below one input LSB is strictly worse than lossless**: it cannot buy
203
+ fidelity the data does not have, so the irreversible transform returns a
204
+ bit-identical image while encoding *larger* than `--preset lossless`. Passing
205
+ such a `--quality` warns and names the floor. The resolved step is recorded in
206
+ the manifest under `encode_options`.
207
+
208
+ An explicit `--quality` is an absolute step, not an LSB multiple, so pick it for
209
+ the raster's dtype — `0.0005` below is ~33 LSB of `uint16` morphology, but would
210
+ be 0.13 LSB (worse than lossless) on 8-bit input:
211
+
212
+ ```bash
213
+ spatialdata-js-util images recompress input.zarr out.zarr \
214
+ --codec experimental.openjph_htj2k --quality 0.0005 --sibling --overwrite
215
+ ```
216
+
217
+ ### Multiscale pyramids
218
+
219
+ Acquisition output is often written at a single resolution. A browser then has no
220
+ choice but to fetch full-resolution chunks however far out the user is zoomed —
221
+ which defeats the image codecs rather than complementing them. `--pyramid` gives
222
+ any single-level image a pyramid before recompressing it, so one command turns
223
+ single-resolution input into a browser-ready store:
224
+
225
+ ```bash
226
+ spatialdata-js-util images recompress input.zarr output.zarr \
227
+ --codec experimental.openjph_htj2k --preset balanced --pyramid --overwrite
228
+ ```
229
+
230
+ To add pyramids without touching codecs:
231
+
232
+ ```bash
233
+ spatialdata-js-util images add-pyramid input.zarr output.zarr --overwrite
234
+ ```
235
+
236
+ By default levels are chosen automatically: keep halving until the largest
237
+ spatial axis is `--pyramid-min-size` (1024) or smaller, so the coarsest level is
238
+ roughly one chunk. Override with `--pyramid-levels N` (a *total* count including
239
+ full resolution) and `--pyramid-downscale K`.
240
+
241
+ Images that already have more than one level are **skipped**, and the manifest
242
+ says so; `--pyramid-force` rebuilds them anyway. Pass `--labels` to
243
+ `add-pyramid` to include label elements — they are downsampled by the label
244
+ model, so ids are never averaged into values that identify no object.
245
+
246
+ Full resolution is copied through unchanged; only the added levels are new.
247
+ Levels are generated through SpatialData's own parsers, which get the per-level
248
+ scale *and* half-pixel translation right — hand-written `multiscales` metadata
249
+ tends to misalign every level against the full-resolution image.
250
+
251
+ Pyramids are always written to a **different** store than the source. SpatialData
252
+ refuses to delete the files backing a live element, so there is no in-place mode.
253
+
254
+ ### Other image notes
255
+
256
+ Labels are written with Blosc/zstd. Browser image codecs support `uint8`,
257
+ `int8`, `uint16` and `int16` only. A sidecar manifest records the expanded
258
+ config, per-raster stats, the codec backend used, and decoded checksums:
259
+
260
+ ```bash
261
+ spatialdata-js-util images inspect output-htj2k.manifest.json
262
+ ```
263
+
264
+ For repeatable multi-image runs, use a JSON config:
265
+
266
+ ```json
267
+ {
268
+ "default_image": { "codec": "imagecodecs_jpeg2k", "preset": "lossless", "chunks": "auto" },
269
+ "images": {
270
+ "morphology_focus": { "preset": "balanced" },
271
+ "he_image": { "codec": "experimental.openjph_htj2k", "quality": 0.001 }
272
+ },
273
+ "default_labels": { "codec": "blosc", "clevel": 5 }
274
+ }
275
+ ```
276
+
277
+ ## Points
278
+
279
+ Morton-sorted Points Parquet, Vitessce-compatible:
280
+
281
+ - `morton_code_2d` added using 16 bits per axis
282
+ - the first 2–4 rows are sentinels with `morton_code_2d == 0` encoding the full
283
+ point bounding box, so readers can get bounds without a scan
284
+ - `{feature_key}_codes` added when the element declares a `feature_key`
285
+ - string/categorical columns moved to the right of the table
286
+ - row-group size controlled, so a viewport maps to a few row-group reads
287
+
288
+ ```bash
289
+ # List Points elements
290
+ spatialdata-js-util points list ~/data/xenium.zarr
291
+
292
+ # Morton-sort transcripts in place on points/<key>/points.parquet
293
+ spatialdata-js-util points morton-from-zarr ~/data/xenium.zarr --points-key transcripts
294
+
295
+ # Morton-sort a CSV or Parquet file
296
+ spatialdata-js-util points morton input.csv output.parquet --feature-key feature_name
297
+ ```
298
+
299
+ Morton v1 belongs on the **canonical** element path `points/<key>/points.parquet`.
300
+ Use `--experimental` only for layouts standard readers cannot consume (see
301
+ [ADR 0002](../../docs/adr/0002-spatially-aware-vector-loading.md)).
302
+
303
+ `points index-permutations` builds a derivative store with several sort layouts
304
+ side by side for benchmarking. All of these are also available from the
305
+ [TUI](#interactive-tui), which verifies the output after each write.
306
+
307
+ ### Multiscale Zarr points (experimental)
308
+
309
+ `points zarr` writes the layout of merfisheyes'
310
+ [points-zarr spec draft 0.1](https://github.com/kresnajenie/merfisheyes/blob/main/docs/points-zarr-spec.md):
311
+ levels of uniform random samples (each a quarter of the one below), each stored in
312
+ tile order and in feature order, with a row index. A viewer can then draw any zoom
313
+ from a bounded read, and read only the rows of the genes selected.
314
+
315
+ ```bash
316
+ # A new store: a standard points element, with the copy beside it
317
+ spatialdata-js-util points zarr transcripts.parquet transcripts.zarr \
318
+ --feature-key feature_name --coordinates x_location,y_location
319
+
320
+ # Add the copy to an existing store's points element, from its own points.parquet
321
+ spatialdata-js-util points add-zarr ~/data/xenium.zarr --points-key transcripts
322
+ ```
323
+
324
+ The copy is the element's `experimental.points-zarr` subgroup; the element itself stays
325
+ an ordinary SpatialData points element, so spatialdata and other standard readers read
326
+ the store as before ([ADR 0006](../../docs/adr/0006-experimental-formats-beside-the-standard.md)).
327
+ Writing works out of core: Parquet input is read in batches, spilled to temporary files
328
+ beside the output (about 18 bytes a point, plus one level's sorted copy) and sorted one
329
+ band of tile rows at a time.
330
+
331
+ On exFAT (and other filesystems without extended attributes) macOS writes a `._name`
332
+ file beside each file that carries one, which doubles the file count and breaks
333
+ readers that list a directory (pyarrow reads `._part.0.parquet` as a part). Delete
334
+ them after writing: `find STORE -name '._*' -delete`.
335
+
336
+ ## Tables
337
+
338
+ ```bash
339
+ # Rewrite every table's X (and named layers) as CSC, in place
340
+ spatialdata-js-util tables to-csc ~/data/xenium.zarr
341
+
342
+ # Or write a converted copy, leaving the source alone
343
+ spatialdata-js-util tables to-csc input.zarr output-csc.zarr --overwrite
344
+ ```
345
+
346
+ The conversion changes only the sparse layout, not the values, and the result
347
+ stays readable by any AnnData client. Dense matrices are left alone unless you
348
+ pass `--densify`, since sparsifying a dense matrix can make it bigger.
349
+
350
+ Only the matrices are rewritten — `obs`, `var`, `uns` and the rest are left
351
+ byte-for-byte alone, and what is written is pinned to the store's existing
352
+ encodings (no sharding, no `nullable-string-array` index). Re-serialising a whole
353
+ table would otherwise silently re-encode it with whatever conventions the
354
+ installed AnnData currently prefers, which can leave readers unable to find the
355
+ variable names even though every value is still present.
356
+
357
+ ```python
358
+ from spatialdata_js_util import convert_store_tables_to_csc, to_csc
359
+
360
+ convert_store_tables_to_csc("store.zarr") # whole store
361
+ csc = to_csc(adata.X) # or just a matrix
362
+ ```
363
+
364
+ ## Python API
365
+
366
+ ```python
367
+ from spatialdata_js_util import (
368
+ recompress_spatialdata,
369
+ add_pyramids,
370
+ has_pyramid,
371
+ write_morton_points_parquet,
372
+ convert_store_tables_to_csc,
373
+ backend_report,
374
+ )
375
+
376
+ result = recompress_spatialdata(
377
+ "input.zarr", "output-htj2k.zarr",
378
+ codec="experimental.openjph_htj2k",
379
+ preset="balanced",
380
+ pyramid=True, # add levels to single-resolution images first
381
+ overwrite=True,
382
+ )
383
+ print(result.manifest_path)
384
+
385
+ # Or pyramids on their own
386
+ has_pyramid("input.zarr", "images", "morphology") # -> False
387
+ add_pyramids("input.zarr", "output.zarr", levels=4)
388
+ ```
389
+
390
+ ## Monorepo development
391
+
392
+ From the repository root:
393
+
394
+ ```bash
395
+ pnpm test:python
396
+ ```
397
+
398
+ The vendored `openjph-wasm` assets under
399
+ `src/spatialdata_js_util/codecs/vendor/openjph/` are gitignored and produced by
400
+ `node scripts/vendor-openjph-for-python.mjs`, which `pnpm test:python` runs
401
+ first. They are only needed for the WASM backend; the `imagecodecs` backend works
402
+ without them.
403
+
404
+ Fixture-generation scripts under `scripts/` are repo-local and not shipped in the
405
+ wheel. To regenerate the backend probe fixture (requires Node.js):
406
+
407
+ ```bash
408
+ uv run --directory python/spatialdata-js-util python scripts/build_htj2k_probe.py
409
+ ```
410
+
411
+ TypeScript callers can use `encodeHtj2kPlane()` from `zarrextra` directly; see
412
+ [`docs/htj2k-wasm-encode-design.md`](docs/htj2k-wasm-encode-design.md).