opencodecs 0.1.0__cp313-cp313-win_amd64.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.
Files changed (92) hide show
  1. opencodecs/__init__.py +121 -0
  2. opencodecs/_avif_codec.py +53 -0
  3. opencodecs/_blosc2_codec.py +60 -0
  4. opencodecs/_bmp_codec.py +357 -0
  5. opencodecs/_brotli_codec.py +57 -0
  6. opencodecs/_czi_codec.py +88 -0
  7. opencodecs/_czi_reader.py +606 -0
  8. opencodecs/_deflate_codec.py +53 -0
  9. opencodecs/_hdf5_codec.py +149 -0
  10. opencodecs/_heif_codec.py +53 -0
  11. opencodecs/_jpeg2k_codec.py +54 -0
  12. opencodecs/_jpeg_codec.py +53 -0
  13. opencodecs/_jxl_codec.py +95 -0
  14. opencodecs/_lz4_codec.py +57 -0
  15. opencodecs/_png_codec.py +56 -0
  16. opencodecs/_qoi_codec.py +54 -0
  17. opencodecs/_webp_codec.py +52 -0
  18. opencodecs/_zarr_codecs.py +127 -0
  19. opencodecs/_zstd_codec.py +58 -0
  20. opencodecs/codecs/__init__.py +151 -0
  21. opencodecs/codecs/_avif.c +33518 -0
  22. opencodecs/codecs/_avif.cp313-win_amd64.pyd +0 -0
  23. opencodecs/codecs/_blosc2.c +29369 -0
  24. opencodecs/codecs/_blosc2.cp313-win_amd64.pyd +0 -0
  25. opencodecs/codecs/_brotli.c +29019 -0
  26. opencodecs/codecs/_brotli.cp313-win_amd64.pyd +0 -0
  27. opencodecs/codecs/_bytetools.c +27649 -0
  28. opencodecs/codecs/_bytetools.cp313-win_amd64.pyd +0 -0
  29. opencodecs/codecs/_deflate.c +28980 -0
  30. opencodecs/codecs/_deflate.cp313-win_amd64.pyd +0 -0
  31. opencodecs/codecs/_heif.c +33990 -0
  32. opencodecs/codecs/_heif.cp313-win_amd64.pyd +0 -0
  33. opencodecs/codecs/_jpeg.c +32482 -0
  34. opencodecs/codecs/_jpeg.cp313-win_amd64.pyd +0 -0
  35. opencodecs/codecs/_jpeg2k.c +35647 -0
  36. opencodecs/codecs/_jpeg2k.cp313-win_amd64.pyd +0 -0
  37. opencodecs/codecs/_jxl.c +54499 -0
  38. opencodecs/codecs/_jxl.cp313-win_amd64.pyd +0 -0
  39. opencodecs/codecs/_lz4.c +29557 -0
  40. opencodecs/codecs/_lz4.cp313-win_amd64.pyd +0 -0
  41. opencodecs/codecs/_png.c +33785 -0
  42. opencodecs/codecs/_png.cp313-win_amd64.pyd +0 -0
  43. opencodecs/codecs/_qoi.c +32198 -0
  44. opencodecs/codecs/_qoi.cp313-win_amd64.pyd +0 -0
  45. opencodecs/codecs/_registry.py +215 -0
  46. opencodecs/codecs/_webp.c +32477 -0
  47. opencodecs/codecs/_webp.cp313-win_amd64.pyd +0 -0
  48. opencodecs/codecs/_zstd.c +29352 -0
  49. opencodecs/codecs/_zstd.cp313-win_amd64.pyd +0 -0
  50. opencodecs/codecs/heif_shim.h +35 -0
  51. opencodecs/core/__init__.py +6 -0
  52. opencodecs/core/_io_helpers.py +55 -0
  53. opencodecs/core/_optional_backend.py +67 -0
  54. opencodecs/core/codec.py +359 -0
  55. opencodecs/core/color.py +118 -0
  56. opencodecs/core/errors.py +15 -0
  57. opencodecs/core/io.py +200 -0
  58. opencodecs/jxl.py +204 -0
  59. opencodecs/parallel.py +338 -0
  60. opencodecs/tiff_reader.py +288 -0
  61. opencodecs/tifffile_patch.py +258 -0
  62. opencodecs/zarr.py +148 -0
  63. opencodecs-0.1.0.dist-info/DELVEWHEEL +2 -0
  64. opencodecs-0.1.0.dist-info/METADATA +280 -0
  65. opencodecs-0.1.0.dist-info/RECORD +92 -0
  66. opencodecs-0.1.0.dist-info/WHEEL +5 -0
  67. opencodecs-0.1.0.dist-info/top_level.txt +1 -0
  68. opencodecs.libs/SvtAv1Enc-16fcbe129aca8462076b97bd52db6d93.dll +0 -0
  69. opencodecs.libs/aom-4a33db18ba51c7170bbd716cd8974f0b.dll +0 -0
  70. opencodecs.libs/avif-1b96b6df3ad5fcd319afaa6b02e03035.dll +0 -0
  71. opencodecs.libs/brotlicommon-6a2e79bd63994b569e2f94f172b3ae15.dll +0 -0
  72. opencodecs.libs/brotlidec-9d6351d922ea7f11becd20a5594e40fb.dll +0 -0
  73. opencodecs.libs/brotlienc-be3334cb03f24e9acdb6ffd04b31c366.dll +0 -0
  74. opencodecs.libs/dav1d-2c1c1900d0fa2fe69bc400e4f7a32cb4.dll +0 -0
  75. opencodecs.libs/heif-576dab975873acab853fe6eee03aa412.dll +0 -0
  76. opencodecs.libs/hwy-1f9837ee531e693b53ef6a7e7f8a91ff.dll +0 -0
  77. opencodecs.libs/jxl-8745db77665a24f9cb1e1ad017684cb4.dll +0 -0
  78. opencodecs.libs/jxl_cms-06f5158e3d3646fc94318878672c7af3.dll +0 -0
  79. opencodecs.libs/jxl_threads-11883bafbcd12fa6214d912ab4d53cb9.dll +0 -0
  80. opencodecs.libs/libblosc2-b4c519844e417bc911fe47b0bd2c7192.dll +0 -0
  81. opencodecs.libs/libde265-6ecc1f17c58a0ce677046858d830d36a.dll +0 -0
  82. opencodecs.libs/libsharpyuv-c910eeed9d8f2bbf76ba6edf2e292ff7.dll +0 -0
  83. opencodecs.libs/libwebp-d04c2bb4fae02ef22f9f33389e8fa295.dll +0 -0
  84. opencodecs.libs/libx265-bbb03b09e9892d9b22869c6b631dded8.dll +0 -0
  85. opencodecs.libs/lz4-6dc4c9d99b472a733962763c89a20ca1.dll +0 -0
  86. opencodecs.libs/msvcp140-8f141b4454fa78db34bc1f28c571b4da.dll +0 -0
  87. opencodecs.libs/openjp2-274e6a200cd564b014389546f6472d38.dll +0 -0
  88. opencodecs.libs/rav1e-c49bc3de5a4d43c796903c558484373a.dll +0 -0
  89. opencodecs.libs/turbojpeg-02fa4c0b0954b5d98f5c7a5e64682f4e.dll +0 -0
  90. opencodecs.libs/zlib-85c711c83f96bed93184bede15be6b5a.dll +0 -0
  91. opencodecs.libs/zlib-ng2-f57947fffdf36cbecc829a61001247e1.dll +0 -0
  92. opencodecs.libs/zstd-ca228f0f33b8296d6650b4694bb38ba9.dll +0 -0
opencodecs/__init__.py ADDED
@@ -0,0 +1,121 @@
1
+ """opencodecs — streaming, network-aware image codecs for scientific imaging.
2
+
3
+ Top-level API:
4
+
5
+ opencodecs.read(src, *, format=None, **opts) -> ndarray
6
+ opencodecs.write(dest, arr, *, format=None, **opts) -> bytes | None
7
+ opencodecs.open(src, *, format=None, **opts) -> Reader
8
+ opencodecs.list_codecs() -> [{name, native, encode, decode, ...}]
9
+ opencodecs.has_codec(name) -> bool
10
+
11
+ Format auto-detection: by file extension when the input is a path, by
12
+ magic bytes when it's bytes/file-like. Override with format="png".
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+
18
+ # start delvewheel patch
19
+ def _delvewheel_patch_1_12_1():
20
+ import os
21
+ if os.path.isdir(libs_dir := os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, 'opencodecs.libs'))):
22
+ os.add_dll_directory(libs_dir)
23
+
24
+
25
+ _delvewheel_patch_1_12_1()
26
+ del _delvewheel_patch_1_12_1
27
+ # end delvewheel patch
28
+
29
+ import os
30
+ from typing import Any
31
+
32
+ from .core.codec import (
33
+ Codec,
34
+ Reader,
35
+ Writer,
36
+ register_codec,
37
+ get_codec,
38
+ list_codecs,
39
+ has_codec,
40
+ codec_for_path,
41
+ codec_for_bytes,
42
+ _resolve_codec,
43
+ )
44
+ from .core.color import ColorSpec, parse_color
45
+ from .core.errors import OpenCodecsError
46
+
47
+ # Importing the codec subpackage triggers each format's
48
+ # register_codec(...) at module-init time, populating the registry.
49
+ from . import codecs as _codecs_pkg # noqa: F401
50
+
51
+ # Direct back-compat surface. Both modules below handle a missing libjxl
52
+ # backend internally: they import cleanly and only raise (with a
53
+ # helpful message) when a function is actually called. So
54
+ # ``import opencodecs`` succeeds on platforms without libjxl built.
55
+ from . import jxl, parallel
56
+ from .jxl import (
57
+ JxlReader,
58
+ JxlWriter,
59
+ encode as jxl_encode,
60
+ decode as jxl_decode,
61
+ iter_frames as jxl_iter_frames,
62
+ open as jxl_open,
63
+ )
64
+
65
+
66
+ def read(src: Any, *, format: str | None = None, **opts):
67
+ """Decode `src` to an ndarray. Codec auto-detected from path/bytes."""
68
+ return _resolve_codec(src, format=format).decode(src, **opts)
69
+
70
+
71
+ def write(
72
+ dest: Any,
73
+ arr,
74
+ *,
75
+ format: str | None = None,
76
+ **opts,
77
+ ):
78
+ """Encode `arr` to `dest`. Codec auto-detected from dest path or
79
+ `format=`.
80
+
81
+ `dest` may be a path, file-like, or None (in-memory: returns bytes).
82
+ """
83
+ if format is None:
84
+ if isinstance(dest, (str, os.PathLike)):
85
+ codec = codec_for_path(dest)
86
+ else:
87
+ raise ValueError(
88
+ "write() needs format=... when dest isn't a path"
89
+ )
90
+ else:
91
+ codec = get_codec(format)
92
+ return codec.encode(arr, dest=dest, **opts)
93
+
94
+
95
+ def open( # noqa: A001
96
+ src: Any,
97
+ *,
98
+ format: str | None = None,
99
+ **opts,
100
+ ) -> Reader:
101
+ """Open `src` for streaming / random-access reading."""
102
+ return _resolve_codec(src, format=format).open(src, **opts)
103
+
104
+
105
+ __all__ = [
106
+ # Top-level unified API
107
+ "read", "write", "open",
108
+ "list_codecs", "has_codec", "get_codec",
109
+ # Core types (subclassable)
110
+ "Codec", "Reader", "Writer", "register_codec",
111
+ # Color
112
+ "ColorSpec", "parse_color",
113
+ # Errors
114
+ "OpenCodecsError",
115
+ # Submodules + native JXL surface (back-compat)
116
+ "jxl", "parallel",
117
+ "JxlReader", "JxlWriter",
118
+ "jxl_encode", "jxl_decode", "jxl_iter_frames", "jxl_open",
119
+ ]
120
+
121
+ __version__ = "0.2.0.dev0"
@@ -0,0 +1,53 @@
1
+ """AvifCodec — Codec adapter wrapping the native _avif extension."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ import numpy as np
9
+
10
+ from .core.codec import Codec
11
+ from .core._io_helpers import read_src as _read_src, write_dest as _write_dest
12
+ from .core._optional_backend import import_or_stubs
13
+
14
+ _avif_encode, _avif_decode, _avif_check_signature, _HAVE_BACKEND = import_or_stubs(
15
+ "opencodecs.codecs._avif",
16
+ "encode", "decode", "check_signature",
17
+ )
18
+
19
+
20
+ class AvifCodec(Codec):
21
+ """Native AVIF codec via libavif."""
22
+
23
+ name = "avif"
24
+ file_extensions = (".avif",)
25
+
26
+ has_native = True
27
+ has_delegate = False
28
+ can_encode = True
29
+ can_decode = True
30
+ multi_frame = False
31
+ streaming_decode = False
32
+ parallel_decode = False
33
+
34
+ supported_dtypes = (np.uint8,)
35
+ supports_color = True
36
+
37
+ def signature(self, head: bytes) -> bool:
38
+ return _avif_check_signature(head)
39
+
40
+ def encode(self, data: Any, *, dest=None, level: int | None = None,
41
+ lossless: bool = False, speed: int = 6,
42
+ **opts) -> bytes | None:
43
+ if not isinstance(data, np.ndarray):
44
+ data = np.asarray(data)
45
+ encoded = _avif_encode(data, level=level, lossless=lossless, speed=speed)
46
+ return _write_dest(encoded, dest)
47
+
48
+ def decode(self, src: Any, **opts) -> np.ndarray:
49
+ return _avif_decode(_read_src(src))
50
+
51
+
52
+
53
+ __all__ = ["AvifCodec"]
@@ -0,0 +1,60 @@
1
+ """Blosc2Codec — Codec adapter wrapping the native _blosc2 extension."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ import numpy as np
9
+
10
+ from .core.codec import Codec
11
+ from .core._io_helpers import read_src as _read_src, write_dest as _write_dest
12
+ from .core._optional_backend import import_or_stubs
13
+
14
+ _blosc2_encode, _blosc2_decode, _blosc2_check_signature, _HAVE_BACKEND = import_or_stubs(
15
+ "opencodecs.codecs._blosc2",
16
+ "encode", "decode", "check_signature",
17
+ )
18
+
19
+
20
+ class Blosc2Codec(Codec):
21
+ """Native blosc2 meta-compressor (c-blosc2)."""
22
+
23
+ name = "blosc2"
24
+ file_extensions = (".b2",)
25
+
26
+ has_native = True
27
+ has_delegate = False
28
+ can_encode = True
29
+ can_decode = True
30
+ multi_frame = False
31
+ streaming_decode = False
32
+ parallel_decode = False
33
+
34
+ supported_dtypes = (np.uint8,)
35
+ supports_color = False
36
+
37
+ def signature(self, head: bytes) -> bool:
38
+ return _blosc2_check_signature(head)
39
+
40
+ def encode(self, data: Any, *, dest=None, level: int | None = None,
41
+ compressor: str | None = None,
42
+ typesize: int | None = None,
43
+ shuffle: bool | None = None,
44
+ **opts) -> bytes | None:
45
+ if isinstance(data, np.ndarray): # pragma: no cover - blosc2 is byte-oriented; ndarray-aware encode unused in tests
46
+ if typesize is None:
47
+ typesize = data.dtype.itemsize
48
+ data = data.tobytes()
49
+ compressed = _blosc2_encode(
50
+ data, level=level, compressor=compressor,
51
+ typesize=typesize, shuffle=shuffle,
52
+ )
53
+ return _write_dest(compressed, dest)
54
+
55
+ def decode(self, src: Any, **opts) -> bytes:
56
+ return _blosc2_decode(_read_src(src))
57
+
58
+
59
+
60
+ __all__ = ["Blosc2Codec"]
@@ -0,0 +1,357 @@
1
+ """BmpCodec — native BMP encode/decode (no external library).
2
+
3
+ BMP is a small, well-documented format. The heavy lifting is row-stride
4
+ arithmetic and channel reordering, both of which numpy handles at memory
5
+ bandwidth — no need for a Cython inner loop. Header parsing is in pure
6
+ Python via struct.
7
+
8
+ Encode parity with imagecodecs:
9
+ - 2D uint8 -> 8-bit paletted with identity grayscale palette
10
+ - (H, W, 3) uint8 -> 24-bit BI_RGB (BGR row order, 4-byte row padding)
11
+ - (H, W, 4) uint8 -> 32-bit BI_BITFIELDS BGRA via BITMAPV4HEADER
12
+
13
+ Decode supports the formats we actually encounter in the wild:
14
+ - 8-bit paletted (BI_RGB; grayscale-palette -> 2D, color-palette -> RGB)
15
+ - 24-bit BGR (BI_RGB)
16
+ - 32-bit BGRA/BGRX (BI_RGB and BI_BITFIELDS with explicit channel masks)
17
+ - 16-bit RGB555/RGB565 (BI_RGB / BI_BITFIELDS)
18
+ - bottom-up (positive height) and top-down (negative height) layouts
19
+
20
+ Not supported: BI_RLE4/BI_RLE8, BI_JPEG/BI_PNG, OS/2 BA/CI/CP variants,
21
+ 1- and 4-bit paletted. These are rare and not worth the parser surface.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import struct
27
+ from pathlib import Path
28
+ from typing import Any
29
+
30
+ import numpy as np
31
+
32
+ from .core.codec import Codec
33
+ from .core._io_helpers import read_src as _read_src, write_dest as _write_dest
34
+
35
+
36
+ class BmpError(RuntimeError):
37
+ """Raised on malformed or unsupported BMP files."""
38
+
39
+
40
+ _BI_RGB = 0
41
+ _BI_BITFIELDS = 3
42
+
43
+
44
+ def _row_stride(width: int, bits_per_pixel: int) -> int:
45
+ """BMP rows are padded to a multiple of 4 bytes."""
46
+ return ((width * bits_per_pixel + 31) // 32) * 4
47
+
48
+
49
+ def _encode(arr: np.ndarray) -> bytes:
50
+ if arr.dtype != np.uint8:
51
+ raise BmpError(f'BMP encode: unsupported dtype {arr.dtype}; need uint8')
52
+
53
+ # Pixel rows are written bottom-up; flip vertically.
54
+ if arr.ndim == 2:
55
+ return _encode_paletted8(arr)
56
+ if arr.ndim == 3 and arr.shape[2] == 3:
57
+ return _encode_bgr24(arr)
58
+ if arr.ndim == 3 and arr.shape[2] == 4:
59
+ return _encode_bgra32(arr)
60
+ raise BmpError(
61
+ f'BMP encode: unsupported array shape {arr.shape}; expected 2D or '
62
+ '(H, W, 3|4)')
63
+
64
+
65
+ def _encode_paletted8(arr: np.ndarray) -> bytes:
66
+ h, w = arr.shape
67
+ stride = _row_stride(w, 8)
68
+ pad = stride - w
69
+ rows = arr[::-1] # bottom-up
70
+ if pad:
71
+ rows = np.concatenate(
72
+ [rows, np.zeros((h, pad), dtype=np.uint8)], axis=1)
73
+ pixels = rows.tobytes()
74
+ palette_size = 256 * 4
75
+ palette = np.zeros((256, 4), dtype=np.uint8)
76
+ palette[:, 0] = palette[:, 1] = palette[:, 2] = np.arange(256) # BGRX
77
+ palette[:, 3] = 0xFF # reserved byte; imagecodecs sets 0xFF
78
+ palette_bytes = palette.tobytes()
79
+
80
+ info_size = 40
81
+ file_header_size = 14
82
+ pix_offset = file_header_size + info_size + palette_size
83
+ file_size = pix_offset + len(pixels)
84
+
85
+ file_hdr = struct.pack('<2sIHHI', b'BM', file_size, 0, 0, pix_offset)
86
+ info_hdr = struct.pack(
87
+ '<IiiHHIIiiII',
88
+ info_size, w, h, 1, 8, _BI_RGB, len(pixels), 3780, 3780, 0, 0)
89
+ return file_hdr + info_hdr + palette_bytes + pixels
90
+
91
+
92
+ def _encode_bgr24(arr: np.ndarray) -> bytes:
93
+ h, w, _ = arr.shape
94
+ stride = _row_stride(w, 24)
95
+ pad = stride - 3 * w
96
+ bgr = arr[::-1, :, ::-1] # bottom-up + RGB->BGR
97
+ if pad:
98
+ padded = np.zeros((h, stride), dtype=np.uint8)
99
+ padded[:, :3 * w] = bgr.reshape(h, 3 * w)
100
+ pixels = padded.tobytes()
101
+ else:
102
+ pixels = np.ascontiguousarray(bgr).tobytes()
103
+
104
+ info_size = 40
105
+ file_header_size = 14
106
+ pix_offset = file_header_size + info_size
107
+ file_size = pix_offset + len(pixels)
108
+
109
+ file_hdr = struct.pack('<2sIHHI', b'BM', file_size, 0, 0, pix_offset)
110
+ info_hdr = struct.pack(
111
+ '<IiiHHIIiiII',
112
+ info_size, w, h, 1, 24, _BI_RGB, len(pixels), 3780, 3780, 0, 0)
113
+ return file_hdr + info_hdr + pixels
114
+
115
+
116
+ def _encode_bgra32(arr: np.ndarray) -> bytes:
117
+ h, w, _ = arr.shape
118
+ # 32-bit rows are always 4-byte aligned; no padding needed.
119
+ bgra = arr[::-1, :, [2, 1, 0, 3]] # bottom-up, RGBA -> BGRA
120
+ pixels = np.ascontiguousarray(bgra).tobytes()
121
+
122
+ info_size = 108 # BITMAPV4HEADER
123
+ file_header_size = 14
124
+ pix_offset = file_header_size + info_size
125
+ file_size = pix_offset + len(pixels)
126
+
127
+ file_hdr = struct.pack('<2sIHHI', b'BM', file_size, 0, 0, pix_offset)
128
+ # BITMAPV4HEADER: 40 base bytes + 4 masks + 4 cs + 36 endpoints + 12 gamma
129
+ info_hdr = struct.pack(
130
+ '<IiiHHIIiiII',
131
+ info_size, w, h, 1, 32, _BI_BITFIELDS, len(pixels), 3780, 3780, 0, 0,
132
+ )
133
+ # Channel masks (BGRA layout in memory; little-endian DWORD pixel reads
134
+ # as 0xAA RR GG BB so masks reflect that).
135
+ masks = struct.pack(
136
+ '<IIII',
137
+ 0x00FF0000, # red
138
+ 0x0000FF00, # green
139
+ 0x000000FF, # blue
140
+ 0xFF000000, # alpha
141
+ )
142
+ cs = struct.pack('<I', 0) # LCS_CALIBRATED_RGB / unused
143
+ endpoints = b'\x00' * 36
144
+ gamma = struct.pack('<III', 0, 0, 0)
145
+ return file_hdr + info_hdr + masks + cs + endpoints + gamma + pixels
146
+
147
+
148
+ def _decode(data: bytes) -> np.ndarray:
149
+ if len(data) < 14 or data[:2] != b'BM':
150
+ raise BmpError('not a BMP file (missing BM magic)')
151
+ bf_size, _, _, pix_offset = struct.unpack('<IHHI', data[2:14])
152
+
153
+ if len(data) < 14 + 4:
154
+ raise BmpError('truncated BMP DIB header')
155
+ info_size = struct.unpack('<I', data[14:18])[0]
156
+ if info_size < 40:
157
+ raise BmpError(f'unsupported DIB header size {info_size} (need >= 40)')
158
+
159
+ if len(data) < 14 + info_size:
160
+ raise BmpError('truncated DIB header')
161
+ (
162
+ _info_size, width, height, planes, bpp, compression,
163
+ size_image, _xppm, _yppm, clr_used, _clr_important,
164
+ ) = struct.unpack('<IiiHHIIiiII', data[14:54])
165
+ del _info_size, planes, _xppm, _yppm, _clr_important
166
+
167
+ if compression not in (_BI_RGB, _BI_BITFIELDS):
168
+ raise BmpError(f'unsupported BMP compression {compression}')
169
+
170
+ top_down = height < 0
171
+ height = abs(height)
172
+ if width <= 0 or height <= 0:
173
+ raise BmpError(f'invalid BMP dimensions {width}x{height}')
174
+
175
+ # Pull channel masks (either from BITMAPV4HEADER region or from the
176
+ # 12 bytes that follow a BITMAPINFOHEADER when compression == BI_BITFIELDS).
177
+ masks = None
178
+ alpha_mask = 0
179
+ if compression == _BI_BITFIELDS:
180
+ if info_size >= 108:
181
+ r, g, b, a = struct.unpack('<IIII', data[54:70])
182
+ masks = (r, g, b)
183
+ alpha_mask = a
184
+ else:
185
+ mask_off = 14 + info_size
186
+ r, g, b = struct.unpack('<III', data[mask_off:mask_off + 12])
187
+ masks = (r, g, b)
188
+ # Some 32-bit BI_BITFIELDS files include a 4th alpha mask after.
189
+ if bpp == 32 and len(data) >= mask_off + 16:
190
+ alpha_mask = struct.unpack('<I', data[mask_off + 12:mask_off + 16])[0]
191
+
192
+ palette = None
193
+ if bpp <= 8:
194
+ n_colors = clr_used or (1 << bpp)
195
+ pal_off = 14 + info_size
196
+ if compression == _BI_BITFIELDS: # pragma: no cover - paletted+BI_BITFIELDS rare in wild
197
+ pal_off += 16 if bpp == 32 else 12
198
+ palette = np.frombuffer(
199
+ data, dtype=np.uint8, count=n_colors * 4, offset=pal_off,
200
+ ).reshape(n_colors, 4)
201
+
202
+ stride = _row_stride(width, bpp)
203
+ pix_data = data[pix_offset:pix_offset + stride * height]
204
+ if len(pix_data) < stride * height:
205
+ # Some encoders set size_image to 0; verify size from offset is enough.
206
+ if size_image and len(data) - pix_offset >= size_image: # pragma: no cover - rare encoder bug recovery
207
+ pix_data = data[pix_offset:pix_offset + size_image]
208
+ if len(pix_data) < stride * height:
209
+ raise BmpError('truncated BMP pixel data')
210
+
211
+ rows = np.frombuffer(pix_data, dtype=np.uint8).reshape(height, stride)
212
+
213
+ if bpp == 8:
214
+ idx = rows[:, :width]
215
+ # Flip vertically unless top-down.
216
+ if not top_down:
217
+ idx = idx[::-1]
218
+ # If palette is identity-grayscale (B==G==R==i), return 2D.
219
+ bgr = palette[:, :3]
220
+ is_gray = (
221
+ np.array_equal(bgr[:, 0], np.arange(len(bgr), dtype=np.uint8))
222
+ and np.array_equal(bgr[:, 1], bgr[:, 0])
223
+ and np.array_equal(bgr[:, 2], bgr[:, 0])
224
+ )
225
+ if is_gray:
226
+ return np.ascontiguousarray(idx)
227
+ rgb = np.empty((height, width, 3), dtype=np.uint8)
228
+ rgb[..., 0] = palette[idx, 2] # R from palette[B-channel]
229
+ rgb[..., 1] = palette[idx, 1]
230
+ rgb[..., 2] = palette[idx, 0]
231
+ return rgb
232
+
233
+ if bpp == 24:
234
+ bgr = rows[:, :3 * width].reshape(height, width, 3)
235
+ if not top_down:
236
+ bgr = bgr[::-1]
237
+ return np.ascontiguousarray(bgr[..., ::-1]) # BGR -> RGB
238
+
239
+ if bpp == 32:
240
+ px = rows[:, :4 * width].reshape(height, width, 4)
241
+ if not top_down:
242
+ px = px[::-1]
243
+ if compression == _BI_BITFIELDS and masks is not None:
244
+ return _unpack_32_bitfields(px, masks, alpha_mask)
245
+ # BI_RGB 32-bit: BGRX in memory; treat as RGB (drop X).
246
+ rgb = np.empty((height, width, 3), dtype=np.uint8)
247
+ rgb[..., 0] = px[..., 2]
248
+ rgb[..., 1] = px[..., 1]
249
+ rgb[..., 2] = px[..., 0]
250
+ return rgb
251
+
252
+ if bpp == 16:
253
+ # 16-bit pixels stored as little-endian uint16.
254
+ px = np.frombuffer(rows[:, :2 * width].tobytes(), dtype='<u2').reshape(
255
+ height, width)
256
+ if not top_down:
257
+ px = px[::-1]
258
+ if compression == _BI_BITFIELDS and masks is not None:
259
+ return _unpack_16_bitfields(px, masks, alpha_mask)
260
+ # BI_RGB 16-bit is RGB555 by spec.
261
+ return _unpack_16_bitfields(
262
+ px, (0x7C00, 0x03E0, 0x001F), 0)
263
+
264
+ raise BmpError(f'unsupported BMP bpp={bpp}')
265
+
266
+
267
+ def _shift_for_mask(mask: int) -> tuple[int, int]:
268
+ """Return (shift, width_in_bits) for a non-zero mask; (0,0) if mask==0."""
269
+ if mask == 0:
270
+ return 0, 0
271
+ shift = 0
272
+ m = mask
273
+ while m & 1 == 0:
274
+ m >>= 1
275
+ shift += 1
276
+ width = 0
277
+ while m & 1:
278
+ m >>= 1
279
+ width += 1
280
+ return shift, width
281
+
282
+
283
+ def _expand_channel(value: np.ndarray, width: int) -> np.ndarray:
284
+ """Expand a `width`-bit channel to 8 bits via top-bit replication."""
285
+ if width >= 8:
286
+ return (value >> (width - 8)).astype(np.uint8)
287
+ # Bit replication: 5-bit -> 8-bit by repeating top bits.
288
+ out = (value << (8 - width)).astype(np.uint8)
289
+ out |= (value >> (2 * width - 8)).astype(np.uint8) if 2 * width >= 8 else 0
290
+ return out
291
+
292
+
293
+ def _unpack_32_bitfields(
294
+ px: np.ndarray, masks: tuple[int, int, int], alpha_mask: int,
295
+ ) -> np.ndarray:
296
+ # px is (H, W, 4) uint8 in memory order. Reinterpret as little-endian DWORD.
297
+ h, w, _ = px.shape
298
+ dword = np.ascontiguousarray(px).view('<u4').reshape(h, w)
299
+ has_alpha = alpha_mask != 0
300
+ out = np.empty((h, w, 4 if has_alpha else 3), dtype=np.uint8)
301
+ for ch, mask in enumerate(masks):
302
+ shift, width = _shift_for_mask(mask)
303
+ out[..., ch] = _expand_channel((dword & mask) >> shift, width)
304
+ if has_alpha:
305
+ shift, width = _shift_for_mask(alpha_mask)
306
+ out[..., 3] = _expand_channel(
307
+ (dword & alpha_mask) >> shift, width)
308
+ return out
309
+
310
+
311
+ def _unpack_16_bitfields(
312
+ px: np.ndarray, masks: tuple[int, int, int], alpha_mask: int,
313
+ ) -> np.ndarray:
314
+ h, w = px.shape
315
+ has_alpha = alpha_mask != 0
316
+ out = np.empty((h, w, 4 if has_alpha else 3), dtype=np.uint8)
317
+ for ch, mask in enumerate(masks):
318
+ shift, width = _shift_for_mask(mask)
319
+ out[..., ch] = _expand_channel((px & mask) >> shift, width)
320
+ if has_alpha:
321
+ shift, width = _shift_for_mask(alpha_mask)
322
+ out[..., 3] = _expand_channel((px & alpha_mask) >> shift, width)
323
+ return out
324
+
325
+
326
+ class BmpCodec(Codec):
327
+ """Native BMP codec (no external library)."""
328
+
329
+ name = "bmp"
330
+ file_extensions = (".bmp", ".dib")
331
+
332
+ has_native = True
333
+ has_delegate = False
334
+ can_encode = True
335
+ can_decode = True
336
+ multi_frame = False
337
+ streaming_decode = False
338
+ parallel_decode = False
339
+
340
+ supported_dtypes = (np.uint8,)
341
+ supports_color = True
342
+
343
+ def signature(self, head: bytes) -> bool:
344
+ return len(head) >= 2 and head[:2] == b'BM'
345
+
346
+ def encode(self, data: Any, *, dest=None, **opts) -> bytes | None:
347
+ if not isinstance(data, np.ndarray):
348
+ data = np.asarray(data)
349
+ encoded = _encode(data)
350
+ return _write_dest(encoded, dest)
351
+
352
+ def decode(self, src: Any, **opts) -> np.ndarray:
353
+ return _decode(_read_src(src))
354
+
355
+
356
+
357
+ __all__ = ["BmpCodec", "BmpError"]
@@ -0,0 +1,57 @@
1
+ """BrotliCodec — Codec adapter wrapping the native _brotli extension."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ import numpy as np
9
+
10
+ from .core.codec import Codec
11
+ from .core._io_helpers import read_src as _read_src, write_dest as _write_dest
12
+ from .core._optional_backend import import_or_stubs
13
+
14
+ _brotli_encode, _brotli_decode, _brotli_check_signature, _HAVE_BACKEND = import_or_stubs(
15
+ "opencodecs.codecs._brotli",
16
+ "encode", "decode", "check_signature",
17
+ )
18
+
19
+
20
+ class BrotliCodec(Codec):
21
+ """Native brotli codec.
22
+
23
+ Bytes-in / bytes-out only. Brotli streams have no fixed magic header,
24
+ so signature-based dispatch always returns False; use ``format='brotli'``
25
+ or ``.br`` extension for routing.
26
+ """
27
+
28
+ name = "brotli"
29
+ file_extensions = (".br",)
30
+
31
+ has_native = True
32
+ has_delegate = False
33
+ can_encode = True
34
+ can_decode = True
35
+ multi_frame = False
36
+ streaming_decode = False
37
+ parallel_decode = False
38
+
39
+ supported_dtypes = (np.uint8,)
40
+ supports_color = False
41
+
42
+ def signature(self, head: bytes) -> bool:
43
+ return _brotli_check_signature(head)
44
+
45
+ def encode(self, data: Any, *, dest=None, level: int | None = None,
46
+ **opts) -> bytes | None:
47
+ if isinstance(data, np.ndarray):
48
+ data = data.tobytes()
49
+ compressed = _brotli_encode(data, level=level)
50
+ return _write_dest(compressed, dest)
51
+
52
+ def decode(self, src: Any, **opts) -> bytes:
53
+ return _brotli_decode(_read_src(src))
54
+
55
+
56
+
57
+ __all__ = ["BrotliCodec"]