"""
Codec wire-format protocol for GZCM tile storage.
Defines the contract between codec encoder/decoder pairs and the GZCM container
writer/reader. This is the v5.1 foundation that prevents Bug 0.3
(writing-v4-file-with-lz4-codec-fails-to-decode because the writer prepended
an 8-byte shape header over LZ4's own 12-byte header) from recurring.
The protocol has two parts:
1. ``WireFormat`` enum — declares whether a codec's encoded payload is
``OPAQUE_PAYLOAD`` (writer prepends an 8-byte ``(rows, cols)`` shape header)
or ``SELF_DESCRIBING`` (encoder adds its own header; writer does NOT prepend).
2. ``Codec`` protocol — minimal interface every codec must satisfy:
``encode_tile``, ``decode_tile``, ``wire_format``.
Adding a new codec becomes: implement ``Codec``, register it via
``register_codec(...)``. No other code in the writer or reader changes.
See :mod:`gunz_cm.compressions` for the registry + auto-registration logic.
See ``docs/analysis/gzcm-v5-design-proposals.md`` §2 Proposal 1 for the
architectural rationale.
"""
from __future__ import annotations
import enum
from typing import TYPE_CHECKING, Protocol, runtime_checkable
if TYPE_CHECKING:
import numpy # noqa: F401 (used only for type annotations)
[docs]@runtime_checkable
class Codec(Protocol):
"""Minimal interface every GZCM tile codec must satisfy.
Implementations: :class:`CmcEncoder`, :class:`ZstdEncoder`,
:class:`CmcZstdEncoder`, :class:`BscEncoder`, :class:`BscCmcEncoder`,
:class:`Lz4Encoder`. Decoders are paired via the registry.
The :func:`runtime_checkable` decorator lets the registry assert
``isinstance(cls, Codec)`` at registration time without requiring
explicit subclassing.
"""
@property
def wire_format(self) -> WireFormat:
"""What the writer must do (or not do) around this codec's payload."""
...
[docs] def encode_tile(self, tile) -> bytes:
"""Encode a 2-D uint tile to bytes.
Parameters
----------
tile : numpy.ndarray
2-D array of shape ``(rows, cols)`` with integer dtype.
Returns
-------
bytes
The encoded payload. The wire-format contract
(``OPAQUE_PAYLOAD`` vs ``SELF_DESCRIBING``) determines whether
the writer prepends an 8-byte shape header before this payload.
"""
...
[docs] def decode_tile(self, payload: bytes, shape) -> "numpy.ndarray":
"""Decode bytes back to a 2-D uint tile of the given shape.
Parameters
----------
payload : bytes
For ``OPAQUE_PAYLOAD``: the raw encoded bytes (writer-prepended
8-byte shape header has already been stripped by the reader).
For ``SELF_DESCRIBING``: the full payload including the
encoder's own header.
shape : tuple[int, int]
``(rows, cols)``. For ``OPAQUE_PAYLOAD`` this comes from the
writer's 8-byte shape header. For ``SELF_DESCRIBING`` this
should match what the encoder embedded in its own header.
Returns
-------
numpy.ndarray
2-D array of shape ``shape`` with integer dtype.
"""
...