micro-reader 0.0.1__py3-none-any.whl
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.
- micro_reader/__init__.py +23 -0
- micro_reader/_axes.py +196 -0
- micro_reader/_base.py +268 -0
- micro_reader/_codecs.py +439 -0
- micro_reader/_errors.py +14 -0
- micro_reader/_index.py +53 -0
- micro_reader/_io.py +83 -0
- micro_reader/_jpeg_restarts.py +174 -0
- micro_reader/_memory.py +161 -0
- micro_reader/_metadata.py +163 -0
- micro_reader/_pool.py +85 -0
- micro_reader/_registry.py +104 -0
- micro_reader/_stream.py +317 -0
- micro_reader/backends/__init__.py +1 -0
- micro_reader/backends/_czi_metadata.py +188 -0
- micro_reader/backends/_lif_metadata.py +125 -0
- micro_reader/backends/_nd2_legacy.py +370 -0
- micro_reader/backends/_nd2_metadata.py +224 -0
- micro_reader/backends/_nd2_structure.py +280 -0
- micro_reader/backends/_tiff_metadata.py +287 -0
- micro_reader/backends/czi.py +948 -0
- micro_reader/backends/ims.py +409 -0
- micro_reader/backends/lif.py +713 -0
- micro_reader/backends/mrc.py +184 -0
- micro_reader/backends/nd2.py +264 -0
- micro_reader/backends/oir.py +439 -0
- micro_reader/backends/raster.py +568 -0
- micro_reader/backends/tiff.py +858 -0
- micro_reader-0.0.1.dist-info/METADATA +741 -0
- micro_reader-0.0.1.dist-info/RECORD +32 -0
- micro_reader-0.0.1.dist-info/WHEEL +4 -0
- micro_reader-0.0.1.dist-info/licenses/LICENSE +21 -0
micro_reader/_codecs.py
ADDED
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
"""Decoders: the native package when it is installed, a reference path otherwise.
|
|
2
|
+
|
|
3
|
+
Native: ``micro_reader_codecs`` -- the separate package micro-reader-codecs
|
|
4
|
+
(Rust), a direct dependency -- decoding straight into the caller's buffer
|
|
5
|
+
without the GIL. Reference: an established independent decoder -- Python
|
|
6
|
+
3.14's ``compression.zstd``, else ``zstandard``, else ``imagecodecs``. It is
|
|
7
|
+
used only where the native package is missing (e.g. a development checkout)
|
|
8
|
+
or lacks a codec (JPEG XR), and by the tests, which compare native and
|
|
9
|
+
reference bit for bit. ``MICRO_READER_NO_NATIVE=1`` forces the reference path.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
|
|
17
|
+
from ._errors import CorruptFileError, MicroReaderError
|
|
18
|
+
|
|
19
|
+
if os.environ.get("MICRO_READER_NO_NATIVE"):
|
|
20
|
+
_native = None
|
|
21
|
+
else:
|
|
22
|
+
try:
|
|
23
|
+
import micro_reader_codecs as _native
|
|
24
|
+
except ImportError: # not installed (e.g. a source checkout)
|
|
25
|
+
_native = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def native_available() -> bool:
|
|
29
|
+
return _native is not None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _reference_zstd(data) -> bytes:
|
|
33
|
+
try:
|
|
34
|
+
from compression import zstd # Python >= 3.14
|
|
35
|
+
return zstd.decompress(bytes(data))
|
|
36
|
+
except ImportError:
|
|
37
|
+
pass
|
|
38
|
+
try:
|
|
39
|
+
import zstandard
|
|
40
|
+
return zstandard.ZstdDecompressor().decompress(bytes(data))
|
|
41
|
+
except ImportError:
|
|
42
|
+
pass
|
|
43
|
+
try:
|
|
44
|
+
import imagecodecs
|
|
45
|
+
return bytes(imagecodecs.zstd_decode(bytes(data)))
|
|
46
|
+
except ImportError:
|
|
47
|
+
pass
|
|
48
|
+
raise MicroReaderError(
|
|
49
|
+
"zstd-compressed data needs micro-reader's native module (install a wheel), "
|
|
50
|
+
"Python >= 3.14, or the 'zstandard' package")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def zstd_decompress_into(data, out: np.ndarray, *, native: bool | None = None) -> None:
|
|
54
|
+
"""Decompress one zstd frame into the C-contiguous array *out*, which
|
|
55
|
+
must be exactly the decompressed size."""
|
|
56
|
+
view = memoryview(out).cast("B")
|
|
57
|
+
use_native = _native is not None if native is None else native
|
|
58
|
+
if use_native:
|
|
59
|
+
if _native is None:
|
|
60
|
+
raise MicroReaderError("micro-reader's native module is not available")
|
|
61
|
+
try:
|
|
62
|
+
written = _native.zstd_decompress_into(data, view)
|
|
63
|
+
except ValueError as exc:
|
|
64
|
+
raise CorruptFileError(f"corrupt zstd data: {exc}") from exc
|
|
65
|
+
else:
|
|
66
|
+
try:
|
|
67
|
+
raw = _reference_zstd(data)
|
|
68
|
+
except Exception as exc: # noqa: BLE001 - decoder specific
|
|
69
|
+
if isinstance(exc, MicroReaderError):
|
|
70
|
+
raise
|
|
71
|
+
raise CorruptFileError(f"corrupt zstd data: {exc}") from exc
|
|
72
|
+
written = len(raw)
|
|
73
|
+
if written == len(view):
|
|
74
|
+
view[:] = raw
|
|
75
|
+
if written != len(view):
|
|
76
|
+
raise CorruptFileError(f"zstd data decodes to {written} bytes, expected {len(view)}")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _imagecodecs(what: str):
|
|
80
|
+
try:
|
|
81
|
+
import imagecodecs
|
|
82
|
+
except ImportError as exc:
|
|
83
|
+
raise MicroReaderError(
|
|
84
|
+
f"{what} needs micro-reader's native module or the [codecs] extra (imagecodecs)"
|
|
85
|
+
) from exc
|
|
86
|
+
return imagecodecs
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _use_native(native, name: str) -> bool:
|
|
90
|
+
if native is None:
|
|
91
|
+
return _native is not None and hasattr(_native, name)
|
|
92
|
+
if native and (_native is None or not hasattr(_native, name)):
|
|
93
|
+
raise MicroReaderError(f"micro-reader's native module has no {name}")
|
|
94
|
+
return native
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _fill(out: np.ndarray, decoded, what: str) -> None:
|
|
98
|
+
decoded = np.asarray(decoded)
|
|
99
|
+
view = memoryview(out).cast("B")
|
|
100
|
+
raw = decoded.astype(decoded.dtype.newbyteorder("<"), copy=False).tobytes()
|
|
101
|
+
if len(raw) != len(view):
|
|
102
|
+
raise CorruptFileError(f"{what} data decodes to {len(raw)} bytes, expected {len(view)}")
|
|
103
|
+
view[:] = raw
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def lz4_block_decompress_into(data, out: np.ndarray, *, native: bool | None = None) -> None:
|
|
107
|
+
"""One LZ4 block (no frame header) -> *out*, exactly its size."""
|
|
108
|
+
if _use_native(native, "lz4_block_decompress_into"):
|
|
109
|
+
try:
|
|
110
|
+
written = _native.lz4_block_decompress_into(data, memoryview(out).cast("B"))
|
|
111
|
+
except ValueError as exc:
|
|
112
|
+
raise CorruptFileError(f"corrupt LZ4 data: {exc}") from exc
|
|
113
|
+
else:
|
|
114
|
+
codecs = _imagecodecs("LZ4 data")
|
|
115
|
+
try:
|
|
116
|
+
raw = codecs.lz4_decode(bytes(data), header=False, out=out.nbytes)
|
|
117
|
+
except Exception as exc: # noqa: BLE001
|
|
118
|
+
raise CorruptFileError(f"corrupt LZ4 data: {exc}") from exc
|
|
119
|
+
_fill(out.reshape(-1).view(np.uint8), np.frombuffer(raw, np.uint8), "LZ4")
|
|
120
|
+
return
|
|
121
|
+
if written != out.nbytes:
|
|
122
|
+
raise CorruptFileError(f"LZ4 data decodes to {written} bytes, expected {out.nbytes}")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _check_written(written: int, out: np.ndarray, what: str, partial: bool) -> int:
|
|
126
|
+
if written > out.nbytes or (written < out.nbytes and not partial):
|
|
127
|
+
raise CorruptFileError(f"{what} data decodes to {written} bytes, expected {out.nbytes}")
|
|
128
|
+
return written
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _fill_partial(out: np.ndarray, raw, what: str, partial: bool) -> int:
|
|
132
|
+
"""A reference decoder's bytes -> *out*. With *partial*, a shorter
|
|
133
|
+
result fills the start of *out* (TIFF readers accept short segments)
|
|
134
|
+
and a longer one is cut; returns the bytes written."""
|
|
135
|
+
raw = memoryview(bytes(raw)).cast("B")
|
|
136
|
+
view = memoryview(out).cast("B")
|
|
137
|
+
if not partial:
|
|
138
|
+
_check_written(len(raw), out, what, partial)
|
|
139
|
+
n = min(len(raw), len(view))
|
|
140
|
+
view[:n] = raw[:n]
|
|
141
|
+
return n
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def lzw_decode_into(data, out: np.ndarray, *, partial: bool = False,
|
|
145
|
+
native: bool | None = None) -> int:
|
|
146
|
+
"""TIFF-style LZW -> *out*, which it must fill unless *partial*;
|
|
147
|
+
returns the bytes written."""
|
|
148
|
+
if _use_native(native, "lzw_decode_into"):
|
|
149
|
+
try:
|
|
150
|
+
written = _native.lzw_decode_into(data, memoryview(out).cast("B"))
|
|
151
|
+
except ValueError as exc:
|
|
152
|
+
raise CorruptFileError(f"corrupt LZW data: {exc}") from exc
|
|
153
|
+
return _check_written(written, out, "LZW", partial)
|
|
154
|
+
codecs = _imagecodecs("LZW data")
|
|
155
|
+
try:
|
|
156
|
+
raw = codecs.lzw_decode(bytes(data), out=out.nbytes)
|
|
157
|
+
except Exception as exc: # noqa: BLE001
|
|
158
|
+
raise CorruptFileError(f"corrupt LZW data: {exc}") from exc
|
|
159
|
+
return _fill_partial(out, raw, "LZW", partial)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def zlib_decompress_into(data, out: np.ndarray, *, partial: bool = False,
|
|
163
|
+
native: bool | None = None) -> int:
|
|
164
|
+
"""A zlib stream (TIFF / HDF5 deflate) -> *out*; returns the bytes
|
|
165
|
+
written. Reference: the standard library."""
|
|
166
|
+
if _use_native(native, "zlib_decompress_into"):
|
|
167
|
+
try:
|
|
168
|
+
written = _native.zlib_decompress_into(data, memoryview(out).cast("B"))
|
|
169
|
+
except ValueError as exc:
|
|
170
|
+
raise CorruptFileError(f"corrupt deflate data: {exc}") from exc
|
|
171
|
+
return _check_written(written, out, "deflate", partial)
|
|
172
|
+
import zlib
|
|
173
|
+
try:
|
|
174
|
+
raw = zlib.decompress(bytes(data))
|
|
175
|
+
except zlib.error as exc:
|
|
176
|
+
raise CorruptFileError(f"corrupt deflate data: {exc}") from exc
|
|
177
|
+
return _fill_partial(out, raw, "deflate", partial)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def packbits_decode_into(data, out: np.ndarray, *, partial: bool = False,
|
|
181
|
+
native: bool | None = None) -> int:
|
|
182
|
+
"""PackBits -> *out*; returns the bytes written. Reference: imagecodecs,
|
|
183
|
+
else a pure-Python decoder (slow; only without the native module)."""
|
|
184
|
+
if _use_native(native, "packbits_decode_into"):
|
|
185
|
+
written = _native.packbits_decode_into(data, memoryview(out).cast("B"))
|
|
186
|
+
return _check_written(written, out, "PackBits", partial)
|
|
187
|
+
try:
|
|
188
|
+
import imagecodecs
|
|
189
|
+
decoded = imagecodecs.packbits_decode(bytes(data))
|
|
190
|
+
except ImportError:
|
|
191
|
+
decoded = _packbits_decode(bytes(data), out.nbytes)
|
|
192
|
+
return _fill_partial(out, decoded, "PackBits", partial)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _packbits_decode(data: bytes, limit: int) -> bytes:
|
|
196
|
+
"""PackBits in pure Python: a header byte n, then n + 1 literal bytes
|
|
197
|
+
(n >= 0) or the next byte 1 - n times (n < 0; -128 is a no-op); stops
|
|
198
|
+
at *limit* bytes, as TIFF readers do."""
|
|
199
|
+
out = bytearray()
|
|
200
|
+
i = 0
|
|
201
|
+
while i < len(data) and len(out) < limit:
|
|
202
|
+
n = data[i] - 256 if data[i] > 127 else data[i]
|
|
203
|
+
i += 1
|
|
204
|
+
if n >= 0:
|
|
205
|
+
out += data[i:i + n + 1]
|
|
206
|
+
i += n + 1
|
|
207
|
+
elif n != -128 and i < len(data):
|
|
208
|
+
out += bytes([data[i]]) * (1 - n)
|
|
209
|
+
i += 1
|
|
210
|
+
return bytes(out[:limit])
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def lzma_decompress_into(data, out: np.ndarray, *, partial: bool = False) -> int:
|
|
214
|
+
"""An LZMA / xz stream -> *out*, with the standard library (rare in
|
|
215
|
+
TIFF; not worth a native decoder)."""
|
|
216
|
+
import lzma
|
|
217
|
+
try:
|
|
218
|
+
raw = lzma.decompress(bytes(data))
|
|
219
|
+
except lzma.LZMAError as exc:
|
|
220
|
+
raise CorruptFileError(f"corrupt LZMA data: {exc}") from exc
|
|
221
|
+
return _fill_partial(out, raw, "LZMA", partial)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _image_codec(name: str, what: str, data, out, native, reference: str):
|
|
225
|
+
"""The image codecs' common shape: native straight into *out*, else the
|
|
226
|
+
imagecodecs *reference* function's array copied in."""
|
|
227
|
+
if _use_native(native, name):
|
|
228
|
+
try:
|
|
229
|
+
return getattr(_native, name)(data, memoryview(out).cast("B"))
|
|
230
|
+
except ValueError as exc:
|
|
231
|
+
raise CorruptFileError(f"corrupt {what} data: {exc}") from exc
|
|
232
|
+
codecs = _imagecodecs(f"{what} data")
|
|
233
|
+
try:
|
|
234
|
+
image = np.asarray(getattr(codecs, reference)(bytes(data)))
|
|
235
|
+
except Exception as exc: # noqa: BLE001
|
|
236
|
+
raise CorruptFileError(f"corrupt {what} data: {exc}") from exc
|
|
237
|
+
_fill(out, image, what)
|
|
238
|
+
return (image.shape[1], image.shape[0], image.shape[2] if image.ndim == 3 else 1,
|
|
239
|
+
image.dtype.itemsize * 8)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def png_decode_into(data, out: np.ndarray, *, native: bool | None = None):
|
|
243
|
+
"""A PNG (8 or 16 bit, no palette) -> *out*. Returns (width, height,
|
|
244
|
+
samples, bits)."""
|
|
245
|
+
return _image_codec("png_decode_into", "PNG", data, out, native, "png_decode")
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def webp_decode_into(data, out: np.ndarray, *, native: bool | None = None):
|
|
249
|
+
"""A WebP -> *out* as 8-bit R, G, B (A). Returns (width, height, samples, 8)."""
|
|
250
|
+
return _image_codec("webp_decode_into", "WebP", data, out, native, "webp_decode")
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def jpeg2k_decode_into(data, out: np.ndarray, *, native: bool | None = None):
|
|
254
|
+
"""A JPEG 2000 codestream or JP2 -> *out*, components interleaved.
|
|
255
|
+
Returns (width, height, components, bits). The native decoder is
|
|
256
|
+
OpenJPEG, like imagecodecs', so lossy data decodes identically."""
|
|
257
|
+
return _image_codec("jpeg2k_decode_into", "JPEG 2000", data, out, native,
|
|
258
|
+
"jpeg2k_decode")[:4]
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def jpeg2k_decode_area_into(data, out: np.ndarray, x0: int, y0: int, x1: int, y1: int, *,
|
|
262
|
+
native: bool | None = None):
|
|
263
|
+
"""The rectangle x0 <= x < x1, y0 <= y < y1 of a JPEG 2000 codestream or
|
|
264
|
+
JP2 -> *out*, components interleaved. Native: OpenJPEG decodes only the
|
|
265
|
+
code-blocks covering it. Reference: the whole image decoded, then cut
|
|
266
|
+
(memory of the whole image). Returns (width, height, components, bits)
|
|
267
|
+
of the rectangle."""
|
|
268
|
+
if _use_native(native, "jpeg2k_decode_area_into"):
|
|
269
|
+
try:
|
|
270
|
+
return _native.jpeg2k_decode_area_into(data, memoryview(out).cast("B"),
|
|
271
|
+
x0, y0, x1, y1)[:4]
|
|
272
|
+
except ValueError as exc:
|
|
273
|
+
raise CorruptFileError(f"corrupt JPEG 2000 data: {exc}") from exc
|
|
274
|
+
codecs = _imagecodecs("JPEG 2000 data")
|
|
275
|
+
try:
|
|
276
|
+
image = np.asarray(codecs.jpeg2k_decode(bytes(data)))[y0:y1, x0:x1]
|
|
277
|
+
except Exception as exc: # noqa: BLE001
|
|
278
|
+
raise CorruptFileError(f"corrupt JPEG 2000 data: {exc}") from exc
|
|
279
|
+
_fill(out, image, "JPEG 2000")
|
|
280
|
+
return (image.shape[1], image.shape[0], image.shape[2] if image.ndim == 3 else 1,
|
|
281
|
+
image.dtype.itemsize * 8)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def jxl_decode_area_into(data, out: np.ndarray, x0: int, y0: int, x1: int, y1: int, *,
|
|
285
|
+
native: bool | None = None):
|
|
286
|
+
"""The rectangle x0 <= x < x1, y0 <= y < y1 of a JPEG XL codestream ->
|
|
287
|
+
*out*, like ``jxl_decode_into``. Native: jxl-oxide renders only the
|
|
288
|
+
groups covering it. Reference: the whole image decoded, then cut.
|
|
289
|
+
Returns (width, height, channels, bits) of the rectangle."""
|
|
290
|
+
if _use_native(native, "jxl_decode_area_into"):
|
|
291
|
+
try:
|
|
292
|
+
return _native.jxl_decode_area_into(data, memoryview(out).cast("B"), x0, y0, x1, y1)
|
|
293
|
+
except ValueError as exc:
|
|
294
|
+
raise CorruptFileError(f"corrupt JPEG XL data: {exc}") from exc
|
|
295
|
+
codecs = _imagecodecs("JPEG XL data")
|
|
296
|
+
try:
|
|
297
|
+
image = np.asarray(codecs.jpegxl_decode(bytes(data)))[y0:y1, x0:x1]
|
|
298
|
+
except Exception as exc: # noqa: BLE001
|
|
299
|
+
raise CorruptFileError(f"corrupt JPEG XL data: {exc}") from exc
|
|
300
|
+
_fill(out, image, "JPEG XL")
|
|
301
|
+
return (image.shape[1], image.shape[0], image.shape[2] if image.ndim == 3 else 1,
|
|
302
|
+
image.dtype.itemsize * 8)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _is_lossless_jpeg(data) -> bool:
|
|
306
|
+
return b"\xff\xc3" in bytes(data[:4096]) # SOF3 marker
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _is_12bit_lossy_jpeg(data) -> bool:
|
|
310
|
+
"""A lossy JPEG with 12-bit samples (SOF0/1/2 precision 12), which the
|
|
311
|
+
native decoder does not handle."""
|
|
312
|
+
head = bytes(data[:4096])
|
|
313
|
+
for marker in (b"\xff\xc0", b"\xff\xc1", b"\xff\xc2"):
|
|
314
|
+
at = head.find(marker)
|
|
315
|
+
if 0 <= at and at + 4 < len(head):
|
|
316
|
+
return head[at + 4] == 12
|
|
317
|
+
return False
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def jpeg_decode_into(data, out: np.ndarray, *, colour: str = "auto",
|
|
321
|
+
native: bool | None = None):
|
|
322
|
+
"""A JPEG (baseline, progressive or lossless) -> *out* as (rows, cols[,
|
|
323
|
+
samples]) little-endian, colour in R, G, B order. Returns (width,
|
|
324
|
+
height, samples, bits). *colour*: "auto" (the JPEG's own markers),
|
|
325
|
+
"ycbcr", or "rgb" (stored without a colour transform: TIFF Photometric
|
|
326
|
+
RGB). Lossy JPEG may differ from other decoders by a few grey levels
|
|
327
|
+
(allowed by the JPEG standard); lossless is exact. 12-bit lossy JPEG
|
|
328
|
+
goes to imagecodecs."""
|
|
329
|
+
if _use_native(native, "jpeg_decode_into") and not _is_12bit_lossy_jpeg(data):
|
|
330
|
+
try:
|
|
331
|
+
return _native.jpeg_decode_into(data, memoryview(out).cast("B"), colour)
|
|
332
|
+
except ValueError as exc:
|
|
333
|
+
raise CorruptFileError(f"corrupt JPEG data: {exc}") from exc
|
|
334
|
+
codecs = _imagecodecs("JPEG data")
|
|
335
|
+
try:
|
|
336
|
+
if _is_lossless_jpeg(data):
|
|
337
|
+
image = codecs.ljpeg_decode(bytes(data))
|
|
338
|
+
elif colour == "auto":
|
|
339
|
+
image = codecs.jpeg8_decode(bytes(data))
|
|
340
|
+
else:
|
|
341
|
+
image = codecs.jpeg8_decode(bytes(data), colorspace=colour, outcolorspace="rgb")
|
|
342
|
+
except Exception as exc: # noqa: BLE001
|
|
343
|
+
raise CorruptFileError(f"corrupt JPEG data: {exc}") from exc
|
|
344
|
+
_fill(out, image, "JPEG")
|
|
345
|
+
return (image.shape[1], image.shape[0], image.shape[2] if image.ndim == 3 else 1,
|
|
346
|
+
image.dtype.itemsize * 8)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def jxl_decode_into(data, out: np.ndarray, *, native: bool | None = None):
|
|
350
|
+
"""A JPEG XL codestream -> *out* (colour R, G, B). Returns (width,
|
|
351
|
+
height, channels, bits)."""
|
|
352
|
+
if _use_native(native, "jxl_decode_into"):
|
|
353
|
+
try:
|
|
354
|
+
return _native.jxl_decode_into(data, memoryview(out).cast("B"))
|
|
355
|
+
except ValueError as exc:
|
|
356
|
+
raise CorruptFileError(f"corrupt JPEG XL data: {exc}") from exc
|
|
357
|
+
codecs = _imagecodecs("JPEG XL data")
|
|
358
|
+
try:
|
|
359
|
+
image = codecs.jpegxl_decode(bytes(data))
|
|
360
|
+
except Exception as exc: # noqa: BLE001
|
|
361
|
+
raise CorruptFileError(f"corrupt JPEG XL data: {exc}") from exc
|
|
362
|
+
_fill(out, image, "JPEG XL")
|
|
363
|
+
return (image.shape[1], image.shape[0], image.shape[2] if image.ndim == 3 else 1,
|
|
364
|
+
image.dtype.itemsize * 8)
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def jpegxr_decode_into(data, out: np.ndarray):
|
|
368
|
+
"""A JPEG XR codestream -> *out* (colour R, G, B). Through imagecodecs
|
|
369
|
+
until the native jxrlib binding is decided (it needs libclang to build)."""
|
|
370
|
+
codecs = _imagecodecs("JPEG XR data")
|
|
371
|
+
try:
|
|
372
|
+
image = codecs.jpegxr_decode(bytes(data))
|
|
373
|
+
except Exception as exc: # noqa: BLE001
|
|
374
|
+
raise CorruptFileError(f"corrupt JPEG XR data: {exc}") from exc
|
|
375
|
+
_fill(out, image, "JPEG XR")
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def eer_decode_into(data, out: np.ndarray, skipbits: int, horzbits: int, vertbits: int,
|
|
379
|
+
superres: int = 0, *, native: bool | None = None) -> int:
|
|
380
|
+
"""An EER frame (Electron Event Representation, Falcon cameras) ->
|
|
381
|
+
electron counts in *out* (rows, cols; bool, uint8, uint16 or uint32),
|
|
382
|
+
super-resolved when *superres* > 0. Returns the number of electrons.
|
|
383
|
+
Reference: imagecodecs."""
|
|
384
|
+
rows, cols = out.shape[:2]
|
|
385
|
+
if _use_native(native, "eer_decode_into"):
|
|
386
|
+
try:
|
|
387
|
+
return _native.eer_decode_into(data, memoryview(out).cast("B"), rows, cols, skipbits,
|
|
388
|
+
horzbits, vertbits, superres, out.dtype.itemsize)
|
|
389
|
+
except ValueError as exc:
|
|
390
|
+
raise CorruptFileError(f"corrupt EER data: {exc}") from exc
|
|
391
|
+
codecs = _imagecodecs("EER data")
|
|
392
|
+
try:
|
|
393
|
+
image = codecs.eer_decode(bytes(data), (rows, cols), skipbits, horzbits, vertbits,
|
|
394
|
+
superres=superres)
|
|
395
|
+
except Exception as exc: # noqa: BLE001
|
|
396
|
+
raise CorruptFileError(f"corrupt EER data: {exc}") from exc
|
|
397
|
+
out.reshape(rows, cols)[...] = image
|
|
398
|
+
return int(np.count_nonzero(image)) if image.dtype == bool else int(image.sum())
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
#: every byte with its bits in reverse order (TIFF FillOrder 2)
|
|
402
|
+
REVERSED_BITS = np.array([int(f"{b:08b}"[::-1], 2) for b in range(256)], np.uint8)
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def reverse_bits(data) -> bytes:
|
|
406
|
+
"""Bytes with the bit order of each reversed (TIFF FillOrder 2)."""
|
|
407
|
+
return REVERSED_BITS[np.frombuffer(data, np.uint8)].tobytes()
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def unpack_bits_into(packed, out: np.ndarray, bits: int, runlen: int, *,
|
|
411
|
+
native: bool | None = None) -> int:
|
|
412
|
+
"""Unsigned samples packed at *bits* bits (most significant bit first,
|
|
413
|
+
every *runlen* samples -- a TIFF row -- starting on a byte boundary) ->
|
|
414
|
+
*out* (C-contiguous, 1-, 2- or 4-byte unsigned or bool). Returns the
|
|
415
|
+
samples written (whole runs). Reference: numpy bit arithmetic."""
|
|
416
|
+
item = out.dtype.itemsize
|
|
417
|
+
if _use_native(native, "unpack_bits_into"):
|
|
418
|
+
try:
|
|
419
|
+
return _native.unpack_bits_into(packed, memoryview(out).cast("B"), bits, runlen, item)
|
|
420
|
+
except ValueError as exc:
|
|
421
|
+
raise CorruptFileError(f"packed samples: {exc}") from exc
|
|
422
|
+
run_bytes = (runlen * bits + 7) // 8
|
|
423
|
+
data = np.frombuffer(packed, np.uint8)
|
|
424
|
+
runs = min(data.size // run_bytes, out.size // runlen)
|
|
425
|
+
b = np.unpackbits(data[:runs * run_bytes].reshape(runs, run_bytes), axis=1)
|
|
426
|
+
b = b[:, :runlen * bits].reshape(runs, runlen, bits)
|
|
427
|
+
weights = np.left_shift(np.uint64(1), np.arange(bits - 1, -1, -1, dtype=np.uint64))
|
|
428
|
+
values = (b.astype(np.uint64) * weights).sum(axis=-1, dtype=np.uint64)
|
|
429
|
+
out.reshape(-1)[:runs * runlen] = values.reshape(-1).astype(out.dtype)
|
|
430
|
+
return runs * runlen
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def hilo_unpack(packed: np.ndarray, out: np.ndarray) -> None:
|
|
434
|
+
"""Undo CZI's hi-lo byte packing of 16-bit data: all low bytes first,
|
|
435
|
+
then all high bytes -> interleaved (little-endian) samples in *out*."""
|
|
436
|
+
n = packed.size // 2
|
|
437
|
+
raw = out.view(np.uint8).reshape(-1)
|
|
438
|
+
raw[0::2] = packed[:n]
|
|
439
|
+
raw[1::2] = packed[n:2 * n]
|
micro_reader/_errors.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Exceptions. Plain Python exceptions only: they must survive pickling across
|
|
2
|
+
processes, which native exception objects (e.g. JPype's Java errors) do not."""
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class MicroReaderError(Exception):
|
|
6
|
+
"""Base class for all micro-reader errors."""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class UnsupportedFormatError(MicroReaderError):
|
|
10
|
+
"""No backend recognises the file, or it uses a feature not supported yet."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CorruptFileError(MicroReaderError):
|
|
14
|
+
"""The file is recognised but inconsistent, e.g. truncated."""
|
micro_reader/_index.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""numpy basic indexing, reduced to what a storage backend can serve.
|
|
2
|
+
|
|
3
|
+
Backends only ever read contiguous, unit-step spans. Integer indices and
|
|
4
|
+
steps are applied to the result in memory afterwards, so every backend gets
|
|
5
|
+
numpy semantics for free.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Tuple
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
Spans = Tuple[Tuple[int, int], ...]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def normalise_index(key, shape) -> tuple[Spans, tuple[int, ...], tuple[int, ...]]:
|
|
17
|
+
"""numpy basic-index *key* -> (per-axis (start, stop), axes to squeeze, steps).
|
|
18
|
+
|
|
19
|
+
Supports ints (negative too), slices with positive steps, and a single
|
|
20
|
+
Ellipsis. ``None`` / newaxis, negative steps and fancy indexing raise:
|
|
21
|
+
they have no meaning at the storage layer.
|
|
22
|
+
"""
|
|
23
|
+
if not isinstance(key, tuple):
|
|
24
|
+
key = (key,)
|
|
25
|
+
if sum(k is Ellipsis for k in key) > 1:
|
|
26
|
+
raise IndexError("an index can only have a single ellipsis ('...')")
|
|
27
|
+
if any(k is Ellipsis for k in key):
|
|
28
|
+
at = next(i for i, k in enumerate(key) if k is Ellipsis)
|
|
29
|
+
key = key[:at] + (slice(None),) * (len(shape) - len(key) + 1) + key[at + 1:]
|
|
30
|
+
if len(key) > len(shape):
|
|
31
|
+
raise IndexError(f"too many indices: array is {len(shape)}-D, got {len(key)}")
|
|
32
|
+
key = key + (slice(None),) * (len(shape) - len(key))
|
|
33
|
+
|
|
34
|
+
spans, squeeze, steps = [], [], []
|
|
35
|
+
for axis, (k, size) in enumerate(zip(key, shape)):
|
|
36
|
+
if isinstance(k, (int, np.integer)) and not isinstance(k, bool):
|
|
37
|
+
index = int(k) + size if k < 0 else int(k)
|
|
38
|
+
if not 0 <= index < size:
|
|
39
|
+
raise IndexError(
|
|
40
|
+
f"index {k} is out of bounds for axis {axis} with size {size}")
|
|
41
|
+
spans.append((index, index + 1))
|
|
42
|
+
squeeze.append(axis)
|
|
43
|
+
steps.append(1)
|
|
44
|
+
elif isinstance(k, slice):
|
|
45
|
+
start, stop, step = k.indices(size)
|
|
46
|
+
if step < 0:
|
|
47
|
+
raise IndexError("negative steps are not supported")
|
|
48
|
+
spans.append((start, max(start, stop)))
|
|
49
|
+
steps.append(step)
|
|
50
|
+
else:
|
|
51
|
+
raise IndexError(f"unsupported index type {type(k).__name__}; "
|
|
52
|
+
"use integers, slices or '...'")
|
|
53
|
+
return tuple(spans), tuple(squeeze), tuple(steps)
|
micro_reader/_io.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Pieces every backend that reads segments on the decode pool needs."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import builtins
|
|
5
|
+
import threading
|
|
6
|
+
from collections import OrderedDict
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
from ._errors import CorruptFileError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ThreadHandles:
|
|
14
|
+
"""One read-only file handle per thread for one file (seek + read on a
|
|
15
|
+
shared handle is not thread-safe), kept open until :meth:`close`."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, path: str):
|
|
18
|
+
self.path = path
|
|
19
|
+
self._local = threading.local()
|
|
20
|
+
self._lock = threading.Lock()
|
|
21
|
+
self._all: list = []
|
|
22
|
+
|
|
23
|
+
def get(self):
|
|
24
|
+
fh = getattr(self._local, "fh", None)
|
|
25
|
+
if fh is None:
|
|
26
|
+
fh = self._local.fh = builtins.open(self.path, "rb", buffering=0)
|
|
27
|
+
with self._lock:
|
|
28
|
+
self._all.append(fh)
|
|
29
|
+
return fh
|
|
30
|
+
|
|
31
|
+
def read(self, offset: int, size: int) -> bytes:
|
|
32
|
+
fh = self.get()
|
|
33
|
+
fh.seek(offset)
|
|
34
|
+
data = fh.read(size)
|
|
35
|
+
if len(data) != size:
|
|
36
|
+
raise CorruptFileError(f"{self.path!r}: data at byte {offset} ends early "
|
|
37
|
+
"(truncated file?)")
|
|
38
|
+
return data
|
|
39
|
+
|
|
40
|
+
def close(self) -> None:
|
|
41
|
+
with self._lock:
|
|
42
|
+
handles, self._all = self._all, []
|
|
43
|
+
for fh in handles:
|
|
44
|
+
fh.close()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class DecodeCache:
|
|
48
|
+
"""Decoded segments (tiles, strips, chunks), least recently used out.
|
|
49
|
+
|
|
50
|
+
Keeps a segment shared by neighbouring reads from being decoded twice.
|
|
51
|
+
Segments above a quarter of the budget are not kept (a whole plane from
|
|
52
|
+
a single-strip file would evict everything else)."""
|
|
53
|
+
|
|
54
|
+
def __init__(self, max_bytes: int):
|
|
55
|
+
self.max_bytes = max_bytes
|
|
56
|
+
self._items: OrderedDict = OrderedDict()
|
|
57
|
+
self._bytes = 0
|
|
58
|
+
self._lock = threading.Lock()
|
|
59
|
+
|
|
60
|
+
def get(self, key):
|
|
61
|
+
with self._lock:
|
|
62
|
+
seg = self._items.get(key)
|
|
63
|
+
if seg is not None:
|
|
64
|
+
self._items.move_to_end(key)
|
|
65
|
+
return seg
|
|
66
|
+
|
|
67
|
+
def put(self, key, seg: np.ndarray) -> None:
|
|
68
|
+
if seg.nbytes > self.max_bytes // 4:
|
|
69
|
+
return
|
|
70
|
+
seg.setflags(write=False)
|
|
71
|
+
with self._lock:
|
|
72
|
+
if key in self._items:
|
|
73
|
+
return
|
|
74
|
+
self._items[key] = seg
|
|
75
|
+
self._bytes += seg.nbytes
|
|
76
|
+
while self._bytes > self.max_bytes:
|
|
77
|
+
_, old = self._items.popitem(last=False)
|
|
78
|
+
self._bytes -= old.nbytes
|
|
79
|
+
|
|
80
|
+
def clear(self) -> None:
|
|
81
|
+
with self._lock:
|
|
82
|
+
self._items.clear()
|
|
83
|
+
self._bytes = 0
|