cft-zarr 0.0.4__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.
@@ -0,0 +1,101 @@
1
+ Metadata-Version: 2.3
2
+ Name: cft-zarr
3
+ Version: 0.0.4
4
+ Summary: CFT Zarr codecs for 12-bit fluorescence and RGB compression
5
+ Author: Eli White
6
+ Author-email: eliwhite@gmail.com
7
+ Requires-Python: >=3.11,<=3.14
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.11
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: Programming Language :: Python :: 3.13
12
+ Requires-Dist: imagecodecs (>=2024.1.1)
13
+ Requires-Dist: numpy (>=1.26.4,<2.0.0)
14
+ Requires-Dist: zarr (>=3.0.0)
15
+ Description-Content-Type: text/markdown
16
+
17
+ # CFT Zarr Custom Codecs
18
+
19
+ Custom codecs for Zarr v3 optimized for CFT (Cryo-Fluorescence Tomography) data storage.
20
+
21
+ ## Codecs
22
+
23
+ This package provides custom Zarr v3 codecs:
24
+
25
+ - **`cft_zarr.jpeg_compressor`**: JPEG compressor for RGB images (BytesBytesCodec - supports incremental updates)
26
+ - **`cft_zarr.shift12jls_compressor`**: JPEG-LS compressor for 12-bit fluorescent data (BytesBytesCodec - supports incremental updates)
27
+ - **`cft_zarr.jpeg`**: JPEG codec for RGB images (ArrayBytesCodec)
28
+ - **`cft_zarr.shift12jls`**: JPEG-LS codec for 12-bit fluorescent data (ArrayBytesCodec)
29
+ - **`cft_zarr.jpegxl`**: JPEG XL codec (ArrayBytesCodec)
30
+
31
+ ## Installation
32
+
33
+ ```bash
34
+ pip install cft-zarr
35
+ ```
36
+
37
+ ## Usage
38
+
39
+ ### Reading Zarr Files
40
+
41
+ **Important**: You must import `cft_zarr` before opening Zarr files that use these codecs. This registers the codecs with Zarr.
42
+
43
+ ```python
44
+ import cft_zarr # This registers the codecs
45
+ import zarr
46
+
47
+ # Now you can open Zarr files that use custom codecs
48
+ arr = zarr.open('rgb.zarr', mode='r')
49
+ ```
50
+
51
+ ### Using with napari
52
+
53
+ When opening Zarr files in napari, `cft_zarr` can be auto-registered via the
54
+ napari plugin system (installed in the same environment as napari). If you
55
+ still have trouble, import `cft_zarr` first:
56
+
57
+ ```python
58
+ import cft_zarr # Register codecs before opening files
59
+ import napari
60
+
61
+ # Now napari can read Zarr files with custom codecs
62
+ viewer = napari.Viewer()
63
+ viewer.open('path/to/file.zarr') # Will work with custom codecs
64
+ ```
65
+
66
+ Or in a Python script before launching napari:
67
+
68
+ ```python
69
+ import cft_zarr # Must import before opening Zarr files
70
+ import napari
71
+
72
+ viewer = napari.Viewer()
73
+ viewer.open('rgb.zarr')
74
+ napari.run()
75
+ ```
76
+
77
+ ### Creating Zarr Arrays with Custom Codecs
78
+
79
+ ```python
80
+ import cft_zarr
81
+ from cft_zarr import JPEGCompressor, Shift12JLSCompressor
82
+ import zarr
83
+
84
+ # Create RGB array with JPEG compression
85
+ rgb_array = zarr.create(
86
+ shape=(100, 512, 512, 3),
87
+ chunks=(4, 512, 512, 3),
88
+ dtype='uint8',
89
+ compressors=[JPEGCompressor(level=85)]
90
+ )
91
+
92
+ # Create fluorescent array with Shift12JLS compression
93
+ fl_array = zarr.create(
94
+ shape=(100, 512, 512),
95
+ chunks=(4, 512, 512),
96
+ dtype='uint16',
97
+ compressors=[Shift12JLSCompressor()]
98
+ )
99
+ ```
100
+
101
+
@@ -0,0 +1,84 @@
1
+ # CFT Zarr Custom Codecs
2
+
3
+ Custom codecs for Zarr v3 optimized for CFT (Cryo-Fluorescence Tomography) data storage.
4
+
5
+ ## Codecs
6
+
7
+ This package provides custom Zarr v3 codecs:
8
+
9
+ - **`cft_zarr.jpeg_compressor`**: JPEG compressor for RGB images (BytesBytesCodec - supports incremental updates)
10
+ - **`cft_zarr.shift12jls_compressor`**: JPEG-LS compressor for 12-bit fluorescent data (BytesBytesCodec - supports incremental updates)
11
+ - **`cft_zarr.jpeg`**: JPEG codec for RGB images (ArrayBytesCodec)
12
+ - **`cft_zarr.shift12jls`**: JPEG-LS codec for 12-bit fluorescent data (ArrayBytesCodec)
13
+ - **`cft_zarr.jpegxl`**: JPEG XL codec (ArrayBytesCodec)
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ pip install cft-zarr
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ ### Reading Zarr Files
24
+
25
+ **Important**: You must import `cft_zarr` before opening Zarr files that use these codecs. This registers the codecs with Zarr.
26
+
27
+ ```python
28
+ import cft_zarr # This registers the codecs
29
+ import zarr
30
+
31
+ # Now you can open Zarr files that use custom codecs
32
+ arr = zarr.open('rgb.zarr', mode='r')
33
+ ```
34
+
35
+ ### Using with napari
36
+
37
+ When opening Zarr files in napari, `cft_zarr` can be auto-registered via the
38
+ napari plugin system (installed in the same environment as napari). If you
39
+ still have trouble, import `cft_zarr` first:
40
+
41
+ ```python
42
+ import cft_zarr # Register codecs before opening files
43
+ import napari
44
+
45
+ # Now napari can read Zarr files with custom codecs
46
+ viewer = napari.Viewer()
47
+ viewer.open('path/to/file.zarr') # Will work with custom codecs
48
+ ```
49
+
50
+ Or in a Python script before launching napari:
51
+
52
+ ```python
53
+ import cft_zarr # Must import before opening Zarr files
54
+ import napari
55
+
56
+ viewer = napari.Viewer()
57
+ viewer.open('rgb.zarr')
58
+ napari.run()
59
+ ```
60
+
61
+ ### Creating Zarr Arrays with Custom Codecs
62
+
63
+ ```python
64
+ import cft_zarr
65
+ from cft_zarr import JPEGCompressor, Shift12JLSCompressor
66
+ import zarr
67
+
68
+ # Create RGB array with JPEG compression
69
+ rgb_array = zarr.create(
70
+ shape=(100, 512, 512, 3),
71
+ chunks=(4, 512, 512, 3),
72
+ dtype='uint8',
73
+ compressors=[JPEGCompressor(level=85)]
74
+ )
75
+
76
+ # Create fluorescent array with Shift12JLS compression
77
+ fl_array = zarr.create(
78
+ shape=(100, 512, 512),
79
+ chunks=(4, 512, 512),
80
+ dtype='uint16',
81
+ compressors=[Shift12JLSCompressor()]
82
+ )
83
+ ```
84
+
@@ -0,0 +1,184 @@
1
+ """cft_zarr package - runtime registration of custom Zarr v3 codecs.
2
+
3
+ This module registers available codecs so Zarr can resolve them by name:
4
+ - cft_zarr.shift12jls (ArrayBytesCodec serializer - no incremental updates)
5
+ - cft_zarr.jpeg (ArrayBytesCodec serializer - no incremental updates)
6
+ - cft_zarr.shift12jls_compressor (BytesBytesCodec compressor - supports incremental updates)
7
+ - cft_zarr.jpeg_compressor (BytesBytesCodec compressor - supports incremental updates)
8
+ - cft_zarr.jpegxl
9
+
10
+ Registration is attempted against the Zarr v3 registry API. If unavailable,
11
+ we try a legacy-style register function as a best effort.
12
+ """
13
+
14
+ from typing import Any
15
+
16
+ __all__ = [
17
+ "Shift12JLSCodec",
18
+ "JPEGCodec",
19
+ "Shift12JLSCompressor",
20
+ "JPEGCompressor",
21
+ ]
22
+
23
+ # Import codecs if available
24
+ Shift12JLSCodec: Any = None
25
+ JPEGCodec: Any = None
26
+ Shift12JLSCompressor: Any = None
27
+ JPEGCompressor: Any = None
28
+ JPEGXLCodec: Any = None
29
+
30
+ try:
31
+ from .shift12jls import Shift12JLSCodec as _Shift12JLSCodec
32
+ Shift12JLSCodec = _Shift12JLSCodec
33
+ except Exception:
34
+ Shift12JLSCodec = None
35
+
36
+ try:
37
+ from .jpeg import JPEGCodec as _JPEGCodec
38
+ JPEGCodec = _JPEGCodec
39
+ except Exception:
40
+ JPEGCodec = None
41
+
42
+ try:
43
+ from .shift12jls_compressor import Shift12JLSCompressor as _Shift12JLSCompressor
44
+ Shift12JLSCompressor = _Shift12JLSCompressor
45
+ except Exception:
46
+ Shift12JLSCompressor = None
47
+
48
+ try:
49
+ from .jpeg_compressor import JPEGCompressor as _JPEGCompressor
50
+ JPEGCompressor = _JPEGCompressor
51
+ except Exception:
52
+ JPEGCompressor = None
53
+
54
+ try:
55
+ from .jpegxl import JPEGXLCodec as _JPEGXLCodec
56
+ JPEGXLCodec = _JPEGXLCodec
57
+ except Exception:
58
+ JPEGXLCodec = None
59
+
60
+
61
+ def _register_if_possible(cls: Any) -> None:
62
+ if cls is None:
63
+ return
64
+ # Use Zarr v3 registry API (zarr.registry.register_codec)
65
+ # Signature: register_codec(key: str, codec_cls: type[Codec])
66
+ try:
67
+ from zarr import registry as zregistry # type: ignore
68
+ codec_id = getattr(cls, 'codec_id', None)
69
+ if codec_id:
70
+ zregistry.register_codec(codec_id, cls) # type: ignore[attr-defined]
71
+ except Exception as e:
72
+ # Log error for debugging but don't fail
73
+ import warnings
74
+ warnings.warn(f"Failed to register codec {cls}: {e}", UserWarning)
75
+
76
+
77
+ def _force_map(cls: Any) -> None:
78
+ """Best-effort: insert codec class into known registry maps for immediate availability."""
79
+ if cls is None:
80
+ return
81
+ cid = getattr(cls, 'codec_id', None)
82
+ cname = getattr(cls, '__name__', None)
83
+ if not cid and not cname:
84
+ return
85
+ # zarr.registry private maps - try all possible registry attribute names
86
+ try:
87
+ from zarr import registry as zregistry # type: ignore
88
+ for attr_name in ('_CODECS', 'CODECS', '_codecs', 'codecs', '_registry', 'registry'):
89
+ reg = getattr(zregistry, attr_name, None)
90
+ if isinstance(reg, dict):
91
+ if cid:
92
+ reg[cid] = cls
93
+ if cname:
94
+ reg[cname] = cls
95
+ # Also try name lookup (Zarr might look up by class name)
96
+ if cname and cname != cid:
97
+ reg[cname] = cls
98
+ except Exception:
99
+ pass
100
+ # zarr.codecs.registry private maps
101
+ try:
102
+ from zarr.codecs import registry as cregistry # type: ignore
103
+ for attr_name in ('_CODECS', 'CODECS', '_codecs', 'codecs', '_registry', 'registry'):
104
+ reg = getattr(cregistry, attr_name, None)
105
+ if isinstance(reg, dict):
106
+ if cid:
107
+ reg[cid] = cls
108
+ if cname:
109
+ reg[cname] = cls
110
+ # Also try name lookup
111
+ if cname and cname != cid:
112
+ reg[cname] = cls
113
+ except Exception:
114
+ pass
115
+ # Try registering via get_codec if it exists (Zarr v3 resolver)
116
+ try:
117
+ from zarr.codecs import get_codec # type: ignore
118
+ # This might register it
119
+ except Exception:
120
+ pass
121
+ # Force register with any resolver functions we can find
122
+ try:
123
+ from zarr import registry as zregistry # type: ignore
124
+ # Try to find and call a resolver or register function
125
+ for attr_name in ('resolve_codec', 'get_codec', 'register', 'add'):
126
+ resolver = getattr(zregistry, attr_name, None)
127
+ if callable(resolver):
128
+ try:
129
+ if cid:
130
+ resolver(cid, cls) # type: ignore
131
+ except Exception:
132
+ try:
133
+ resolver(cls) # type: ignore
134
+ except Exception:
135
+ pass
136
+ except Exception:
137
+ pass
138
+
139
+
140
+ # Runtime registration
141
+ _register_if_possible(Shift12JLSCodec)
142
+ _register_if_possible(JPEGCodec)
143
+ _register_if_possible(Shift12JLSCompressor)
144
+ _register_if_possible(JPEGCompressor)
145
+ _register_if_possible(JPEGXLCodec)
146
+
147
+ # Force-map as a last resort so reopened arrays can resolve codecs immediately
148
+ _force_map(Shift12JLSCodec)
149
+ _force_map(JPEGCodec)
150
+ _force_map(Shift12JLSCompressor)
151
+ _force_map(JPEGCompressor)
152
+ _force_map(JPEGXLCodec)
153
+
154
+ from .shift12jls import Shift12JLSCodec
155
+ from .jpeg import JPEGCodec
156
+ from .shift12jls_compressor import Shift12JLSCompressor
157
+ from .jpeg_compressor import JPEGCompressor
158
+
159
+ __all__ = ["Shift12JLSCodec", "JPEGCodec", "Shift12JLSCompressor", "JPEGCompressor"]
160
+
161
+
162
+ def _register_for_napari():
163
+ """
164
+ Napari plugin hook - ensures codecs are registered when napari starts.
165
+
166
+ This function is called by napari when the plugin is loaded, ensuring
167
+ that custom Zarr codecs are registered before any Zarr files are opened.
168
+
169
+ The codecs are already registered during module import (see above),
170
+ but this function provides an explicit hook for napari.
171
+ """
172
+ # Re-register in case napari loads us before any other import side effects.
173
+ _register_if_possible(Shift12JLSCodec)
174
+ _register_if_possible(JPEGCodec)
175
+ _register_if_possible(Shift12JLSCompressor)
176
+ _register_if_possible(JPEGCompressor)
177
+ _register_if_possible(JPEGXLCodec)
178
+
179
+ # Force-map as a last resort so reopened arrays can resolve codecs immediately
180
+ _force_map(Shift12JLSCodec)
181
+ _force_map(JPEGCodec)
182
+ _force_map(Shift12JLSCompressor)
183
+ _force_map(JPEGCompressor)
184
+ _force_map(JPEGXLCodec)
@@ -0,0 +1,228 @@
1
+ """Chunked JPEG codec for RGB images in Zarr v3.
2
+
3
+ This codec handles RGB images in chunks of N slices (default: 4) with JPEG compression,
4
+ optimizing for spatial locality and efficient storage of photographic images.
5
+ """
6
+
7
+ import io
8
+ import numpy as np
9
+ from typing import Any, Dict, Optional
10
+
11
+ try:
12
+ import cv2
13
+ CV2_AVAILABLE = True
14
+ except ImportError:
15
+ CV2_AVAILABLE = False
16
+
17
+ try:
18
+ from numcodecs import Codec
19
+ from numcodecs.registry import register_codec
20
+ except ImportError:
21
+ # Fallback for Zarr v3 direct codec interface
22
+ Codec = object
23
+ register_codec = lambda *args, **kwargs: None
24
+
25
+
26
+ class ChunkedJPEGCodec(Codec):
27
+ """Codec for RGB images using chunked JPEG compression.
28
+
29
+ Encodes N×512×512×3 numpy arrays (RGB images) as JPEG-compressed bytes.
30
+ Default chunk shape is (4, 512, 512, 3) but is configurable.
31
+
32
+ Configuration:
33
+ quality: JPEG quality (0-100, default: 85)
34
+ chunk_shape: Tuple of (N, height, width, channels) or (N, height, width) for grayscale
35
+ Default: (4, 512, 512, 3) for RGB, (4, 512, 512) for grayscale
36
+ """
37
+
38
+ codec_id = "cft_zarr.chunked_jpeg"
39
+
40
+ def __init__(
41
+ self,
42
+ quality: int = 85,
43
+ chunk_shape: Optional[tuple] = None,
44
+ ):
45
+ """Initialize chunked JPEG codec.
46
+
47
+ Args:
48
+ quality: JPEG quality (0-100), default 85
49
+ chunk_shape: Chunk shape tuple, default (4, 512, 512, 3) for RGB
50
+ """
51
+ self.quality = max(0, min(100, quality))
52
+ self.chunk_shape = chunk_shape or (4, 512, 512, 3)
53
+
54
+ # Validate chunk shape
55
+ if len(self.chunk_shape) not in (3, 4):
56
+ raise ValueError(f"chunk_shape must be 3D (grayscale) or 4D (RGB), got {len(self.chunk_shape)}D")
57
+
58
+ self.is_rgb = len(self.chunk_shape) == 4
59
+ if self.is_rgb:
60
+ self.n_slices, self.height, self.width, self.channels = self.chunk_shape
61
+ else:
62
+ self.n_slices, self.height, self.width = self.chunk_shape
63
+ self.channels = 1
64
+
65
+ def encode(self, buf: np.ndarray) -> bytes:
66
+ """Encode numpy array to JPEG-compressed bytes.
67
+
68
+ Args:
69
+ buf: Numpy array of shape (N, height, width, 3) or (N, height, width)
70
+
71
+ Returns:
72
+ Compressed bytes containing JPEG-encoded slices
73
+ """
74
+ if not CV2_AVAILABLE:
75
+ raise ImportError("cv2 is required for chunked JPEG codec")
76
+
77
+ if not isinstance(buf, np.ndarray):
78
+ buf = np.asarray(buf)
79
+
80
+ # Validate shape
81
+ expected_shape = self.chunk_shape
82
+ if buf.shape != expected_shape:
83
+ # Handle partial chunks (last chunk may have fewer slices)
84
+ if buf.shape[1:] == expected_shape[1:]:
85
+ # Same spatial dimensions, just fewer slices
86
+ pass
87
+ else:
88
+ raise ValueError(
89
+ f"Expected shape {expected_shape}, got {buf.shape}. "
90
+ "Partial chunks (fewer slices) are allowed, but spatial dimensions must match."
91
+ )
92
+
93
+ # Convert to uint8 if needed
94
+ if buf.dtype != np.uint8:
95
+ buf = buf.astype(np.uint8)
96
+
97
+ # Encode each slice as JPEG using cv2
98
+ jpeg_bytes_list = []
99
+ n_slices = buf.shape[0]
100
+
101
+ # JPEG encoding parameters for cv2
102
+ encode_params = [cv2.IMWRITE_JPEG_QUALITY, self.quality, cv2.IMWRITE_JPEG_OPTIMIZE, 1]
103
+
104
+ for i in range(n_slices):
105
+ slice_data = buf[i]
106
+
107
+ # Handle RGB vs grayscale
108
+ if self.is_rgb:
109
+ # RGB: shape is (height, width, 3)
110
+ if slice_data.shape != (self.height, self.width, 3):
111
+ # Resize if needed
112
+ slice_data = cv2.resize(slice_data, (self.width, self.height), interpolation=cv2.INTER_LANCZOS4)
113
+ # cv2.imencode expects BGR for color images
114
+ slice_data_bgr = cv2.cvtColor(slice_data, cv2.COLOR_RGB2BGR)
115
+ else:
116
+ # Grayscale: shape is (height, width)
117
+ if slice_data.shape != (self.height, self.width):
118
+ slice_data = cv2.resize(slice_data, (self.width, self.height), interpolation=cv2.INTER_LANCZOS4)
119
+ slice_data_bgr = slice_data
120
+
121
+ # Encode as JPEG using cv2
122
+ success, jpeg_bytes = cv2.imencode('.jpg', slice_data_bgr, encode_params)
123
+ if not success:
124
+ raise RuntimeError(f"Failed to encode slice {i} as JPEG")
125
+
126
+ jpeg_bytes_list.append(jpeg_bytes.tobytes())
127
+
128
+ # Combine all JPEG bytes with a simple format:
129
+ # [n_slices (4 bytes)][slice_0_len (4 bytes)][slice_0_data][slice_1_len (4 bytes)][slice_1_data]...
130
+ result = io.BytesIO()
131
+ result.write(n_slices.to_bytes(4, byteorder='big'))
132
+ for jpeg_bytes in jpeg_bytes_list:
133
+ result.write(len(jpeg_bytes).to_bytes(4, byteorder='big'))
134
+ result.write(jpeg_bytes)
135
+
136
+ return result.getvalue()
137
+
138
+ def decode(self, buf: bytes, out: Optional[np.ndarray] = None) -> np.ndarray:
139
+ """Decode JPEG-compressed bytes to numpy array.
140
+
141
+ Args:
142
+ buf: Compressed bytes containing JPEG-encoded slices
143
+ out: Optional output array (not used, but required by Codec interface)
144
+
145
+ Returns:
146
+ Numpy array of shape (N, height, width, 3) or (N, height, width)
147
+ """
148
+ if not CV2_AVAILABLE:
149
+ raise ImportError("cv2 is required for chunked JPEG codec")
150
+
151
+ buffer = io.BytesIO(buf)
152
+
153
+ # Read number of slices
154
+ n_slices_bytes = buffer.read(4)
155
+ if len(n_slices_bytes) < 4:
156
+ raise ValueError("Invalid chunked JPEG format: missing slice count")
157
+ n_slices = int.from_bytes(n_slices_bytes, byteorder='big')
158
+
159
+ # Decode each slice
160
+ slices = []
161
+ for i in range(n_slices):
162
+ # Read slice length
163
+ len_bytes = buffer.read(4)
164
+ if len(len_bytes) < 4:
165
+ raise ValueError(f"Invalid chunked JPEG format: missing length for slice {i}")
166
+ slice_len = int.from_bytes(len_bytes, byteorder='big')
167
+
168
+ # Read slice data
169
+ slice_bytes = buffer.read(slice_len)
170
+ if len(slice_bytes) < slice_len:
171
+ raise ValueError(f"Invalid chunked JPEG format: incomplete data for slice {i}")
172
+
173
+ # Decode JPEG using cv2
174
+ slice_data = cv2.imdecode(np.frombuffer(slice_bytes, dtype=np.uint8), cv2.IMREAD_UNCHANGED)
175
+ if slice_data is None:
176
+ raise RuntimeError(f"Failed to decode JPEG slice {i}")
177
+
178
+ # Convert BGR to RGB if needed
179
+ if self.is_rgb and slice_data.ndim == 3:
180
+ slice_data = cv2.cvtColor(slice_data, cv2.COLOR_BGR2RGB)
181
+
182
+ # Ensure correct shape
183
+ if self.is_rgb:
184
+ if slice_data.shape != (self.height, self.width, 3):
185
+ slice_data = cv2.resize(slice_data, (self.width, self.height), interpolation=cv2.INTER_LANCZOS4)
186
+ else:
187
+ if slice_data.shape != (self.height, self.width):
188
+ slice_data = cv2.resize(slice_data, (self.width, self.height), interpolation=cv2.INTER_LANCZOS4)
189
+
190
+ slices.append(slice_data.astype(np.uint8))
191
+
192
+ # Stack slices
193
+ result = np.stack(slices, axis=0)
194
+
195
+ # Pad to expected chunk shape if needed (for partial chunks)
196
+ if result.shape[0] < self.n_slices:
197
+ if self.is_rgb:
198
+ padding_shape = (self.n_slices - result.shape[0], self.height, self.width, 3)
199
+ else:
200
+ padding_shape = (self.n_slices - result.shape[0], self.height, self.width)
201
+ padding = np.zeros(padding_shape, dtype=np.uint8)
202
+ result = np.concatenate([result, padding], axis=0)
203
+
204
+ return result
205
+
206
+ def get_config(self) -> Dict[str, Any]:
207
+ """Get codec configuration."""
208
+ return {
209
+ "id": self.codec_id,
210
+ "quality": self.quality,
211
+ "chunk_shape": list(self.chunk_shape),
212
+ }
213
+
214
+ @classmethod
215
+ def from_config(cls, config: Dict[str, Any]) -> "ChunkedJPEGCodec":
216
+ """Create codec from configuration."""
217
+ quality = config.get("quality", 85)
218
+ chunk_shape = config.get("chunk_shape")
219
+ if chunk_shape:
220
+ chunk_shape = tuple(chunk_shape)
221
+ return cls(quality=quality, chunk_shape=chunk_shape)
222
+
223
+
224
+ # Register codec with numcodecs (for Zarr v2 compatibility)
225
+ try:
226
+ register_codec(ChunkedJPEGCodec)
227
+ except Exception:
228
+ pass