coglite 0.1.0__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.
- coglite/__init__.py +58 -0
- coglite/cli.py +152 -0
- coglite/codecs.py +290 -0
- coglite/cog.py +591 -0
- coglite/errors.py +36 -0
- coglite/geo.py +303 -0
- coglite/io.py +484 -0
- coglite/png.py +80 -0
- coglite/py.typed +0 -0
- coglite/tiff.py +440 -0
- coglite/validate.py +189 -0
- coglite-0.1.0.dist-info/METADATA +262 -0
- coglite-0.1.0.dist-info/RECORD +17 -0
- coglite-0.1.0.dist-info/WHEEL +5 -0
- coglite-0.1.0.dist-info/entry_points.txt +2 -0
- coglite-0.1.0.dist-info/licenses/LICENSE +21 -0
- coglite-0.1.0.dist-info/top_level.txt +1 -0
coglite/__init__.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""coglite: read Cloud-Optimized GeoTIFFs over HTTP(S) or from local files without GDAL.
|
|
2
|
+
|
|
3
|
+
>>> import coglite
|
|
4
|
+
>>> cog = coglite.open("https://example.com/data/cog.tif") # doctest: +SKIP
|
|
5
|
+
>>> cog.profile, cog.bounds, cog.crs_epsg # doctest: +SKIP
|
|
6
|
+
>>> arr = cog.read(window=(0, 0, 512, 512), bands=[1, 2, 3]) # doctest: +SKIP
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
__version__ = "0.1.0"
|
|
12
|
+
|
|
13
|
+
from .codecs import available_codecs
|
|
14
|
+
from .cog import COG, Level, RasterResult, Window, open
|
|
15
|
+
from .errors import (
|
|
16
|
+
CodecError,
|
|
17
|
+
COGLiteError,
|
|
18
|
+
CRSError,
|
|
19
|
+
RangeRequestError,
|
|
20
|
+
TIFFError,
|
|
21
|
+
UnsupportedError,
|
|
22
|
+
)
|
|
23
|
+
from .geo import Affine
|
|
24
|
+
from .io import (
|
|
25
|
+
BytesReader,
|
|
26
|
+
CallableReader,
|
|
27
|
+
FileReader,
|
|
28
|
+
HTTPReader,
|
|
29
|
+
RangeReader,
|
|
30
|
+
ReaderStats,
|
|
31
|
+
register_scheme,
|
|
32
|
+
)
|
|
33
|
+
from .validate import ValidationReport
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"__version__",
|
|
37
|
+
"open",
|
|
38
|
+
"COG",
|
|
39
|
+
"Window",
|
|
40
|
+
"Level",
|
|
41
|
+
"RasterResult",
|
|
42
|
+
"Affine",
|
|
43
|
+
"ValidationReport",
|
|
44
|
+
"RangeReader",
|
|
45
|
+
"HTTPReader",
|
|
46
|
+
"FileReader",
|
|
47
|
+
"BytesReader",
|
|
48
|
+
"CallableReader",
|
|
49
|
+
"ReaderStats",
|
|
50
|
+
"register_scheme",
|
|
51
|
+
"available_codecs",
|
|
52
|
+
"COGLiteError",
|
|
53
|
+
"TIFFError",
|
|
54
|
+
"UnsupportedError",
|
|
55
|
+
"CodecError",
|
|
56
|
+
"RangeRequestError",
|
|
57
|
+
"CRSError",
|
|
58
|
+
]
|
coglite/cli.py
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Command line interface: ``coglite info | read | validate``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
from typing import Any, List, Optional, Sequence
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
|
|
12
|
+
from . import __version__
|
|
13
|
+
from .cog import COG, Window, open
|
|
14
|
+
from .errors import COGLiteError
|
|
15
|
+
from .png import write_png
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _open(args: argparse.Namespace) -> COG:
|
|
19
|
+
headers = {}
|
|
20
|
+
for h in args.header or []:
|
|
21
|
+
if ":" not in h:
|
|
22
|
+
raise SystemExit(f"--header expects 'Name: value', got {h!r}")
|
|
23
|
+
k, v = h.split(":", 1)
|
|
24
|
+
headers[k.strip()] = v.strip()
|
|
25
|
+
return open(args.source, headers=headers or None, timeout=args.timeout)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _bands(text: Optional[str]) -> Optional[List[int]]:
|
|
29
|
+
if not text:
|
|
30
|
+
return None
|
|
31
|
+
return [int(b) for b in text.split(",") if b.strip()]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _json_default(o: Any) -> Any:
|
|
35
|
+
if isinstance(o, (np.integer,)):
|
|
36
|
+
return int(o)
|
|
37
|
+
if isinstance(o, (np.floating,)):
|
|
38
|
+
return float(o)
|
|
39
|
+
return str(o)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def cmd_info(args: argparse.Namespace) -> int:
|
|
43
|
+
with _open(args) as cog:
|
|
44
|
+
info = cog.info()
|
|
45
|
+
if args.json:
|
|
46
|
+
print(json.dumps(info, indent=2, default=_json_default))
|
|
47
|
+
return 0
|
|
48
|
+
print(f"Source: {info['source']}")
|
|
49
|
+
print(
|
|
50
|
+
f"Size: {info['width']} x {info['height']} pixels, {info['count']} band(s), {info['dtype']}"
|
|
51
|
+
)
|
|
52
|
+
print(f"CRS: EPSG:{info['crs_epsg']}" if info["crs_epsg"] else "CRS: unknown")
|
|
53
|
+
print(f"Transform: {info['transform']}")
|
|
54
|
+
print(f"Bounds: {info['bounds']}")
|
|
55
|
+
nod = "nan" if info["nodata_is_nan"] else info["nodata"]
|
|
56
|
+
print(f"NoData: {nod}")
|
|
57
|
+
print(
|
|
58
|
+
f"Layout: {'tiled' if info['tiled'] else 'stripped'} {info['blockxsize']}x{info['blockysize']}, "
|
|
59
|
+
f"{info['compress']}, predictor {info['predictor']}, interleave {info['interleave']}"
|
|
60
|
+
)
|
|
61
|
+
print(f"Format: {'BigTIFF' if info['bigtiff'] else 'TIFF'}, {info['byteorder']}-endian")
|
|
62
|
+
print(f"Overviews: {', '.join(f'{w}x{h}' for w, h in info['overviews']) or 'none'}")
|
|
63
|
+
print(f"Mask: {'internal' if info['has_mask'] else 'none'}")
|
|
64
|
+
if any(s != 1.0 for s in info["scales"]) or any(o != 0.0 for o in info["offsets"]):
|
|
65
|
+
print(f"Scale/offset: {info['scales']} / {info['offsets']}")
|
|
66
|
+
st = cog.reader.stats
|
|
67
|
+
print(f"I/O: {st.requests} request(s), {st.bytes_fetched} bytes")
|
|
68
|
+
return 0
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def cmd_read(args: argparse.Namespace) -> int:
|
|
72
|
+
out = args.output
|
|
73
|
+
if not out.lower().endswith((".npy", ".png")):
|
|
74
|
+
raise SystemExit("--output must end with .npy or .png")
|
|
75
|
+
with _open(args) as cog:
|
|
76
|
+
bands = _bands(args.bands)
|
|
77
|
+
if args.bbox:
|
|
78
|
+
res = cog.read_bbox(args.bbox, crs=args.crs, max_size=args.max_size, bands=bands, masked=True)
|
|
79
|
+
data = res.data
|
|
80
|
+
msg = f"level {res.level}, window {tuple(res.window)}, transform {tuple(res.transform) if res.transform else None}"
|
|
81
|
+
else:
|
|
82
|
+
win = Window(*args.window) if args.window else None
|
|
83
|
+
data = cog.read(win, bands, args.overview, masked=True)
|
|
84
|
+
msg = f"window {tuple(win) if win else 'full'}, overview {args.overview}"
|
|
85
|
+
if out.lower().endswith(".npy"):
|
|
86
|
+
np.save(out, np.ma.getdata(data))
|
|
87
|
+
else:
|
|
88
|
+
write_png(out, data)
|
|
89
|
+
st = cog.reader.stats
|
|
90
|
+
print(
|
|
91
|
+
f"wrote {out}: shape {tuple(data.shape)} {data.dtype}; {msg}; "
|
|
92
|
+
f"{st.requests} request(s), {st.bytes_fetched} bytes"
|
|
93
|
+
)
|
|
94
|
+
return 0
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def cmd_validate(args: argparse.Namespace) -> int:
|
|
98
|
+
with _open(args) as cog:
|
|
99
|
+
report = cog.validate()
|
|
100
|
+
if args.json:
|
|
101
|
+
print(json.dumps(report.to_dict(), indent=2, default=_json_default))
|
|
102
|
+
else:
|
|
103
|
+
print(report)
|
|
104
|
+
return 0 if report.is_valid else 1
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
108
|
+
p = argparse.ArgumentParser(prog="coglite", description="Read Cloud-Optimized GeoTIFFs without GDAL.")
|
|
109
|
+
p.add_argument("--version", action="version", version=f"coglite {__version__}")
|
|
110
|
+
sub = p.add_subparsers(dest="command", required=True)
|
|
111
|
+
|
|
112
|
+
def common(sp: argparse.ArgumentParser) -> None:
|
|
113
|
+
sp.add_argument("source", help="local path or http(s):// URL")
|
|
114
|
+
sp.add_argument("--timeout", type=float, default=30.0, help="HTTP timeout in seconds")
|
|
115
|
+
sp.add_argument("--header", action="append", help="extra HTTP header 'Name: value' (repeatable)")
|
|
116
|
+
|
|
117
|
+
sp = sub.add_parser("info", help="print the profile of a COG")
|
|
118
|
+
common(sp)
|
|
119
|
+
sp.add_argument("--json", action="store_true", help="machine-readable output")
|
|
120
|
+
sp.set_defaults(func=cmd_info)
|
|
121
|
+
|
|
122
|
+
sp = sub.add_parser("read", help="read a bbox or window to .npy or .png")
|
|
123
|
+
common(sp)
|
|
124
|
+
g = sp.add_mutually_exclusive_group()
|
|
125
|
+
g.add_argument("--bbox", type=float, nargs=4, metavar=("LEFT", "BOTTOM", "RIGHT", "TOP"))
|
|
126
|
+
g.add_argument("--window", type=int, nargs=4, metavar=("COL", "ROW", "WIDTH", "HEIGHT"))
|
|
127
|
+
sp.add_argument("--crs", default=None, help="CRS of --bbox (default: raster CRS); EPSG:4326/3857 only")
|
|
128
|
+
sp.add_argument("--max-size", type=int, default=1024, help="longest output side for --bbox")
|
|
129
|
+
sp.add_argument("--overview", type=int, default=None, help="overview index for --window")
|
|
130
|
+
sp.add_argument("--bands", default=None, help="comma separated 1-based band list")
|
|
131
|
+
sp.add_argument("-o", "--output", required=True, help="output .npy or .png file")
|
|
132
|
+
sp.set_defaults(func=cmd_read)
|
|
133
|
+
|
|
134
|
+
sp = sub.add_parser("validate", help="check Cloud-Optimized GeoTIFF layout rules")
|
|
135
|
+
common(sp)
|
|
136
|
+
sp.add_argument("--json", action="store_true", help="machine-readable output")
|
|
137
|
+
sp.set_defaults(func=cmd_validate)
|
|
138
|
+
return p
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def main(argv: Optional[Sequence[str]] = None) -> int:
|
|
142
|
+
parser = build_parser()
|
|
143
|
+
args = parser.parse_args(argv)
|
|
144
|
+
try:
|
|
145
|
+
return int(args.func(args))
|
|
146
|
+
except (COGLiteError, OSError, ValueError, IndexError) as exc:
|
|
147
|
+
print(f"coglite: error: {exc}", file=sys.stderr)
|
|
148
|
+
return 2
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
if __name__ == "__main__": # pragma: no cover
|
|
152
|
+
sys.exit(main())
|
coglite/codecs.py
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
"""Decompression codecs and TIFF predictors.
|
|
2
|
+
|
|
3
|
+
Built in (standard library + numpy only):
|
|
4
|
+
|
|
5
|
+
* ``1`` no compression
|
|
6
|
+
* ``5`` LZW (pure-Python decoder, TIFF "early change" variant)
|
|
7
|
+
* ``8`` Deflate / zlib (also the obsolete code ``32946``)
|
|
8
|
+
* ``32773`` PackBits
|
|
9
|
+
* ``34925`` LZMA
|
|
10
|
+
|
|
11
|
+
Optional, through the ``imagecodecs`` package (``pip install coglite[imagecodecs]``):
|
|
12
|
+
|
|
13
|
+
* ``7`` JPEG
|
|
14
|
+
* ``50000`` ZSTD (also served by the standard library on Python 3.14+)
|
|
15
|
+
* ``50001`` WebP
|
|
16
|
+
* ``34887`` LERC (optionally wrapped in Deflate or ZSTD)
|
|
17
|
+
|
|
18
|
+
Predictors: ``1`` none, ``2`` horizontal differencing (integers) and ``3``
|
|
19
|
+
floating-point (byte-shuffled differencing).
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import zlib
|
|
25
|
+
from typing import Any, Callable, Dict, Optional, Sequence, Union
|
|
26
|
+
|
|
27
|
+
import numpy as np
|
|
28
|
+
|
|
29
|
+
from .errors import CodecError, TIFFError
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"COMPRESSION_NAMES",
|
|
33
|
+
"lzw_decode",
|
|
34
|
+
"packbits_decode",
|
|
35
|
+
"decompress",
|
|
36
|
+
"is_image_codec",
|
|
37
|
+
"undo_horizontal_predictor",
|
|
38
|
+
"undo_float_predictor",
|
|
39
|
+
"available_codecs",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
COMPRESSION_NAMES: Dict[int, str] = {
|
|
43
|
+
1: "none",
|
|
44
|
+
5: "lzw",
|
|
45
|
+
6: "ojpeg",
|
|
46
|
+
7: "jpeg",
|
|
47
|
+
8: "deflate",
|
|
48
|
+
32946: "deflate",
|
|
49
|
+
32773: "packbits",
|
|
50
|
+
34887: "lerc",
|
|
51
|
+
34925: "lzma",
|
|
52
|
+
50000: "zstd",
|
|
53
|
+
50001: "webp",
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
#: Codecs that return an image array instead of a byte stream.
|
|
57
|
+
_IMAGE_CODECS = {7, 50001, 34887}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def is_image_codec(compression: int) -> bool:
|
|
61
|
+
"""Return True if the codec decodes straight to pixels (predictors do not apply)."""
|
|
62
|
+
return compression in _IMAGE_CODECS
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _imagecodecs() -> Any:
|
|
66
|
+
try:
|
|
67
|
+
import imagecodecs # type: ignore[import-not-found,import-untyped,unused-ignore]
|
|
68
|
+
except ImportError: # pragma: no cover - depends on the environment
|
|
69
|
+
return None
|
|
70
|
+
return imagecodecs
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def available_codecs() -> Dict[str, bool]:
|
|
74
|
+
"""Report which compression schemes can be decoded in this environment."""
|
|
75
|
+
ic = _imagecodecs()
|
|
76
|
+
zstd_ok = _zstd_decoder() is not None
|
|
77
|
+
return {
|
|
78
|
+
"none": True,
|
|
79
|
+
"lzw": True,
|
|
80
|
+
"deflate": True,
|
|
81
|
+
"packbits": True,
|
|
82
|
+
"lzma": True,
|
|
83
|
+
"zstd": zstd_ok,
|
|
84
|
+
"jpeg": ic is not None,
|
|
85
|
+
"webp": ic is not None,
|
|
86
|
+
"lerc": ic is not None,
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# --------------------------------------------------------------------------- LZW
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def lzw_decode(data: bytes) -> bytes:
|
|
94
|
+
"""Decode TIFF LZW data (MSB-first codes, 9-12 bits, early change).
|
|
95
|
+
|
|
96
|
+
Raises :class:`~coglite.errors.TIFFError` for corrupt streams and for the
|
|
97
|
+
obsolete pre-TIFF-6 LSB-first variant.
|
|
98
|
+
"""
|
|
99
|
+
if len(data) >= 2 and data[0] == 0 and (data[1] & 0x01):
|
|
100
|
+
raise CodecError("old-style (LSB-first) LZW is not supported")
|
|
101
|
+
buf = bytes(data) + b"\x00\x00\x00"
|
|
102
|
+
nbits = len(data) * 8
|
|
103
|
+
out = bytearray()
|
|
104
|
+
table: list = [bytes((i,)) for i in range(256)]
|
|
105
|
+
table.append(b"") # 256 clear
|
|
106
|
+
table.append(b"") # 257 end of information
|
|
107
|
+
append = table.append
|
|
108
|
+
bitpos = 0
|
|
109
|
+
code_len = 9
|
|
110
|
+
mask = 0x1FF
|
|
111
|
+
prev: Optional[bytes] = None
|
|
112
|
+
while bitpos + code_len <= nbits:
|
|
113
|
+
p = bitpos >> 3
|
|
114
|
+
word = (buf[p] << 16) | (buf[p + 1] << 8) | buf[p + 2]
|
|
115
|
+
code = (word >> (24 - (bitpos & 7) - code_len)) & mask
|
|
116
|
+
bitpos += code_len
|
|
117
|
+
if code == 257:
|
|
118
|
+
break
|
|
119
|
+
if code == 256:
|
|
120
|
+
del table[258:]
|
|
121
|
+
code_len = 9
|
|
122
|
+
mask = 0x1FF
|
|
123
|
+
prev = None
|
|
124
|
+
continue
|
|
125
|
+
if prev is None:
|
|
126
|
+
if code > 255:
|
|
127
|
+
raise TIFFError("corrupt LZW stream: first code after clear is not a literal")
|
|
128
|
+
entry = table[code]
|
|
129
|
+
out += entry
|
|
130
|
+
prev = entry
|
|
131
|
+
continue
|
|
132
|
+
n = len(table)
|
|
133
|
+
if code < n:
|
|
134
|
+
entry = table[code]
|
|
135
|
+
append(prev + entry[:1])
|
|
136
|
+
elif code == n:
|
|
137
|
+
entry = prev + prev[:1]
|
|
138
|
+
append(entry)
|
|
139
|
+
else:
|
|
140
|
+
raise TIFFError(f"corrupt LZW stream: code {code} > table size {n}")
|
|
141
|
+
out += entry
|
|
142
|
+
prev = entry
|
|
143
|
+
if len(table) >= (1 << code_len) - 1 and code_len < 12:
|
|
144
|
+
code_len += 1
|
|
145
|
+
mask = (1 << code_len) - 1
|
|
146
|
+
return bytes(out)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# ---------------------------------------------------------------------- PackBits
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def packbits_decode(data: bytes) -> bytes:
|
|
153
|
+
"""Decode Apple PackBits run-length encoded data."""
|
|
154
|
+
out = bytearray()
|
|
155
|
+
i = 0
|
|
156
|
+
n = len(data)
|
|
157
|
+
while i < n:
|
|
158
|
+
h = data[i]
|
|
159
|
+
i += 1
|
|
160
|
+
if h < 128:
|
|
161
|
+
out += data[i : i + h + 1]
|
|
162
|
+
i += h + 1
|
|
163
|
+
elif h > 128:
|
|
164
|
+
if i < n:
|
|
165
|
+
out += bytes((data[i],)) * (257 - h)
|
|
166
|
+
i += 1
|
|
167
|
+
# h == 128 is a no-op
|
|
168
|
+
return bytes(out)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
# ---------------------------------------------------------------------- dispatch
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _zstd_decoder() -> Optional[Callable[[bytes], bytes]]:
|
|
175
|
+
try: # Python 3.14+
|
|
176
|
+
from compression import zstd
|
|
177
|
+
except ImportError:
|
|
178
|
+
pass
|
|
179
|
+
else:
|
|
180
|
+
decoder: Callable[[bytes], bytes] = zstd.decompress
|
|
181
|
+
return decoder
|
|
182
|
+
ic = _imagecodecs()
|
|
183
|
+
if ic is not None:
|
|
184
|
+
return ic.zstd_decode # type: ignore[no-any-return,unused-ignore]
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _need_imagecodecs(name: str) -> Any:
|
|
189
|
+
ic = _imagecodecs()
|
|
190
|
+
if ic is None:
|
|
191
|
+
raise CodecError(
|
|
192
|
+
f"{name} compression needs the optional 'imagecodecs' package: pip install 'coglite[imagecodecs]'"
|
|
193
|
+
)
|
|
194
|
+
return ic
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _jpeg_colorspaces(photometric: int, planar: int, extrasamples: Sequence[int]) -> tuple:
|
|
198
|
+
if photometric == 6: # YCbCr stored, return RGB
|
|
199
|
+
return 3, 2
|
|
200
|
+
if photometric == 2 and planar == 1 and not extrasamples:
|
|
201
|
+
return 2, 2 # RGB stored as RGB, no conversion
|
|
202
|
+
return None, None
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def decompress(
|
|
206
|
+
compression: int,
|
|
207
|
+
data: bytes,
|
|
208
|
+
*,
|
|
209
|
+
jpeg_tables: Optional[bytes] = None,
|
|
210
|
+
photometric: int = 1,
|
|
211
|
+
planar: int = 1,
|
|
212
|
+
extrasamples: Sequence[int] = (),
|
|
213
|
+
lerc_parameters: Sequence[int] = (),
|
|
214
|
+
) -> Union[bytes, np.ndarray]:
|
|
215
|
+
"""Decompress one tile or strip.
|
|
216
|
+
|
|
217
|
+
Byte-stream codecs return ``bytes``; image codecs (JPEG, WebP, LERC)
|
|
218
|
+
return a numpy array.
|
|
219
|
+
"""
|
|
220
|
+
if compression == 1:
|
|
221
|
+
return bytes(data)
|
|
222
|
+
if compression in (8, 32946):
|
|
223
|
+
try:
|
|
224
|
+
return zlib.decompress(data)
|
|
225
|
+
except zlib.error:
|
|
226
|
+
# Some writers emit truncated/raw streams; decompressobj is lenient.
|
|
227
|
+
try:
|
|
228
|
+
return zlib.decompressobj().decompress(data)
|
|
229
|
+
except zlib.error as exc:
|
|
230
|
+
raise TIFFError(f"corrupt Deflate data: {exc}") from exc
|
|
231
|
+
if compression == 5:
|
|
232
|
+
return lzw_decode(data)
|
|
233
|
+
if compression == 32773:
|
|
234
|
+
return packbits_decode(data)
|
|
235
|
+
if compression == 34925:
|
|
236
|
+
import lzma
|
|
237
|
+
|
|
238
|
+
return lzma.decompress(data)
|
|
239
|
+
if compression == 50000:
|
|
240
|
+
dec = _zstd_decoder()
|
|
241
|
+
if dec is None:
|
|
242
|
+
_need_imagecodecs("ZSTD")
|
|
243
|
+
assert dec is not None
|
|
244
|
+
return bytes(dec(data))
|
|
245
|
+
if compression == 7:
|
|
246
|
+
ic = _need_imagecodecs("JPEG")
|
|
247
|
+
cs, ocs = _jpeg_colorspaces(photometric, planar, extrasamples)
|
|
248
|
+
return np.asarray(ic.jpeg_decode(data, tables=jpeg_tables, colorspace=cs, outcolorspace=ocs))
|
|
249
|
+
if compression == 50001:
|
|
250
|
+
ic = _need_imagecodecs("WebP")
|
|
251
|
+
return np.asarray(ic.webp_decode(data))
|
|
252
|
+
if compression == 34887:
|
|
253
|
+
ic = _need_imagecodecs("LERC")
|
|
254
|
+
extra = lerc_parameters[1] if len(lerc_parameters) > 1 else 0
|
|
255
|
+
if extra == 1:
|
|
256
|
+
data = zlib.decompress(data)
|
|
257
|
+
elif extra == 2:
|
|
258
|
+
data = ic.zstd_decode(data)
|
|
259
|
+
return np.asarray(ic.lerc_decode(data))
|
|
260
|
+
name = COMPRESSION_NAMES.get(compression, "unknown")
|
|
261
|
+
raise CodecError(f"compression {compression} ({name}) is not supported")
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
# -------------------------------------------------------------------- predictors
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def undo_horizontal_predictor(arr: np.ndarray) -> np.ndarray:
|
|
268
|
+
"""Undo predictor 2 on an array shaped ``(rows, cols, samples)`` (native byte order)."""
|
|
269
|
+
if arr.dtype.kind == "f":
|
|
270
|
+
# libtiff differences the raw bit patterns as unsigned integers.
|
|
271
|
+
as_int = np.ascontiguousarray(arr).view(f"u{arr.dtype.itemsize}")
|
|
272
|
+
return np.cumsum(as_int, axis=1, dtype=as_int.dtype).view(arr.dtype)
|
|
273
|
+
return np.cumsum(arr, axis=1, dtype=arr.dtype)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def undo_float_predictor(raw: bytes, rows: int, cols: int, samples: int, itemsize: int) -> np.ndarray:
|
|
277
|
+
"""Undo predictor 3 and return float values shaped ``(rows, cols, samples)``.
|
|
278
|
+
|
|
279
|
+
The floating-point predictor first splits every row into byte planes (most
|
|
280
|
+
significant byte first) and then applies byte-wise horizontal differencing
|
|
281
|
+
with a stride of ``samples``; this function reverses both steps.
|
|
282
|
+
"""
|
|
283
|
+
nbytes = rows * cols * samples * itemsize
|
|
284
|
+
buf = np.frombuffer(raw, dtype=np.uint8, count=nbytes)
|
|
285
|
+
b = buf.reshape(rows, cols * itemsize, samples)
|
|
286
|
+
b = np.cumsum(b, axis=1, dtype=np.uint8)
|
|
287
|
+
planes = b.reshape(rows, itemsize, cols * samples)
|
|
288
|
+
shuffled = np.ascontiguousarray(planes.transpose(0, 2, 1))
|
|
289
|
+
values = shuffled.view(f">f{itemsize}").reshape(rows, cols, samples)
|
|
290
|
+
return values.astype(values.dtype.newbyteorder("="))
|