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.
- opencodecs/__init__.py +121 -0
- opencodecs/_avif_codec.py +53 -0
- opencodecs/_blosc2_codec.py +60 -0
- opencodecs/_bmp_codec.py +357 -0
- opencodecs/_brotli_codec.py +57 -0
- opencodecs/_czi_codec.py +88 -0
- opencodecs/_czi_reader.py +606 -0
- opencodecs/_deflate_codec.py +53 -0
- opencodecs/_hdf5_codec.py +149 -0
- opencodecs/_heif_codec.py +53 -0
- opencodecs/_jpeg2k_codec.py +54 -0
- opencodecs/_jpeg_codec.py +53 -0
- opencodecs/_jxl_codec.py +95 -0
- opencodecs/_lz4_codec.py +57 -0
- opencodecs/_png_codec.py +56 -0
- opencodecs/_qoi_codec.py +54 -0
- opencodecs/_webp_codec.py +52 -0
- opencodecs/_zarr_codecs.py +127 -0
- opencodecs/_zstd_codec.py +58 -0
- opencodecs/codecs/__init__.py +151 -0
- opencodecs/codecs/_avif.c +33518 -0
- opencodecs/codecs/_avif.cp313-win_amd64.pyd +0 -0
- opencodecs/codecs/_blosc2.c +29369 -0
- opencodecs/codecs/_blosc2.cp313-win_amd64.pyd +0 -0
- opencodecs/codecs/_brotli.c +29019 -0
- opencodecs/codecs/_brotli.cp313-win_amd64.pyd +0 -0
- opencodecs/codecs/_bytetools.c +27649 -0
- opencodecs/codecs/_bytetools.cp313-win_amd64.pyd +0 -0
- opencodecs/codecs/_deflate.c +28980 -0
- opencodecs/codecs/_deflate.cp313-win_amd64.pyd +0 -0
- opencodecs/codecs/_heif.c +33990 -0
- opencodecs/codecs/_heif.cp313-win_amd64.pyd +0 -0
- opencodecs/codecs/_jpeg.c +32482 -0
- opencodecs/codecs/_jpeg.cp313-win_amd64.pyd +0 -0
- opencodecs/codecs/_jpeg2k.c +35647 -0
- opencodecs/codecs/_jpeg2k.cp313-win_amd64.pyd +0 -0
- opencodecs/codecs/_jxl.c +54499 -0
- opencodecs/codecs/_jxl.cp313-win_amd64.pyd +0 -0
- opencodecs/codecs/_lz4.c +29557 -0
- opencodecs/codecs/_lz4.cp313-win_amd64.pyd +0 -0
- opencodecs/codecs/_png.c +33785 -0
- opencodecs/codecs/_png.cp313-win_amd64.pyd +0 -0
- opencodecs/codecs/_qoi.c +32198 -0
- opencodecs/codecs/_qoi.cp313-win_amd64.pyd +0 -0
- opencodecs/codecs/_registry.py +215 -0
- opencodecs/codecs/_webp.c +32477 -0
- opencodecs/codecs/_webp.cp313-win_amd64.pyd +0 -0
- opencodecs/codecs/_zstd.c +29352 -0
- opencodecs/codecs/_zstd.cp313-win_amd64.pyd +0 -0
- opencodecs/codecs/heif_shim.h +35 -0
- opencodecs/core/__init__.py +6 -0
- opencodecs/core/_io_helpers.py +55 -0
- opencodecs/core/_optional_backend.py +67 -0
- opencodecs/core/codec.py +359 -0
- opencodecs/core/color.py +118 -0
- opencodecs/core/errors.py +15 -0
- opencodecs/core/io.py +200 -0
- opencodecs/jxl.py +204 -0
- opencodecs/parallel.py +338 -0
- opencodecs/tiff_reader.py +288 -0
- opencodecs/tifffile_patch.py +258 -0
- opencodecs/zarr.py +148 -0
- opencodecs-0.1.0.dist-info/DELVEWHEEL +2 -0
- opencodecs-0.1.0.dist-info/METADATA +280 -0
- opencodecs-0.1.0.dist-info/RECORD +92 -0
- opencodecs-0.1.0.dist-info/WHEEL +5 -0
- opencodecs-0.1.0.dist-info/top_level.txt +1 -0
- opencodecs.libs/SvtAv1Enc-16fcbe129aca8462076b97bd52db6d93.dll +0 -0
- opencodecs.libs/aom-4a33db18ba51c7170bbd716cd8974f0b.dll +0 -0
- opencodecs.libs/avif-1b96b6df3ad5fcd319afaa6b02e03035.dll +0 -0
- opencodecs.libs/brotlicommon-6a2e79bd63994b569e2f94f172b3ae15.dll +0 -0
- opencodecs.libs/brotlidec-9d6351d922ea7f11becd20a5594e40fb.dll +0 -0
- opencodecs.libs/brotlienc-be3334cb03f24e9acdb6ffd04b31c366.dll +0 -0
- opencodecs.libs/dav1d-2c1c1900d0fa2fe69bc400e4f7a32cb4.dll +0 -0
- opencodecs.libs/heif-576dab975873acab853fe6eee03aa412.dll +0 -0
- opencodecs.libs/hwy-1f9837ee531e693b53ef6a7e7f8a91ff.dll +0 -0
- opencodecs.libs/jxl-8745db77665a24f9cb1e1ad017684cb4.dll +0 -0
- opencodecs.libs/jxl_cms-06f5158e3d3646fc94318878672c7af3.dll +0 -0
- opencodecs.libs/jxl_threads-11883bafbcd12fa6214d912ab4d53cb9.dll +0 -0
- opencodecs.libs/libblosc2-b4c519844e417bc911fe47b0bd2c7192.dll +0 -0
- opencodecs.libs/libde265-6ecc1f17c58a0ce677046858d830d36a.dll +0 -0
- opencodecs.libs/libsharpyuv-c910eeed9d8f2bbf76ba6edf2e292ff7.dll +0 -0
- opencodecs.libs/libwebp-d04c2bb4fae02ef22f9f33389e8fa295.dll +0 -0
- opencodecs.libs/libx265-bbb03b09e9892d9b22869c6b631dded8.dll +0 -0
- opencodecs.libs/lz4-6dc4c9d99b472a733962763c89a20ca1.dll +0 -0
- opencodecs.libs/msvcp140-8f141b4454fa78db34bc1f28c571b4da.dll +0 -0
- opencodecs.libs/openjp2-274e6a200cd564b014389546f6472d38.dll +0 -0
- opencodecs.libs/rav1e-c49bc3de5a4d43c796903c558484373a.dll +0 -0
- opencodecs.libs/turbojpeg-02fa4c0b0954b5d98f5c7a5e64682f4e.dll +0 -0
- opencodecs.libs/zlib-85c711c83f96bed93184bede15be6b5a.dll +0 -0
- opencodecs.libs/zlib-ng2-f57947fffdf36cbecc829a61001247e1.dll +0 -0
- 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"]
|
opencodecs/_bmp_codec.py
ADDED
|
@@ -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"]
|