From e0b7d30fd437292c88141fb08d60681870b86c6e Mon Sep 17 00:00:00 2001 From: Tomi Valkeinen Date: Fri, 8 May 2026 17:22:58 +0300 Subject: Squashed 'subprojects/pixpat/' content from commit d444626 git-subtree-dir: subprojects/pixpat git-subtree-split: d444626e6ba988ec6d487800721e447f94b1eaf5 --- pixpat-python/pixpat/__init__.py | 489 +++++++++++++++++++++++++++++++++++++ pixpat-python/pixpat/_lib/.gitkeep | 3 + pixpat-python/pixpat/_native.py | 203 +++++++++++++++ 3 files changed, 695 insertions(+) create mode 100644 pixpat-python/pixpat/__init__.py create mode 100644 pixpat-python/pixpat/_lib/.gitkeep create mode 100644 pixpat-python/pixpat/_native.py (limited to 'pixpat-python/pixpat') diff --git a/pixpat-python/pixpat/__init__.py b/pixpat-python/pixpat/__init__.py new file mode 100644 index 0000000..6531662 --- /dev/null +++ b/pixpat-python/pixpat/__init__.py @@ -0,0 +1,489 @@ +"""Python bindings for libpixpat. + +Loads the bundled shared library via ctypes — no CPython extension. Works on +any CPython >= 3.9 for the wheel's target architecture. + +The library draws test patterns and converts pixel data directly into +caller-owned pixel buffers, in a wide range of pixel formats +(planar/semi-planar/packed YUV, RGB, raw, …). The caller is responsible for +allocating each plane with the correct size and stride for the chosen format. + +Pixel data is described by :class:`Buffer`, a passive struct mirroring the +C ``pixpat_buffer``: format name, width, height, one writable buffer per +plane, and one row stride per plane. Use :func:`draw_pattern` to fill a +buffer with a test pattern, and :func:`convert` to convert pixel data +between formats. + +Format names follow the convention used by `kms++` and `pixutils` +(e.g. ``"XRGB8888"``, ``"NV12"``, ``"YUYV"``) — not the DRM/V4L2 +four-character codes (``"XR24"``, etc.). + +Example: + >>> import pixpat + >>> width, height = 1920, 1080 + >>> stride = width * 4 # XRGB8888: 4 bytes per pixel + >>> data = bytearray(stride * height) + >>> buf = pixpat.Buffer(planes=[data], fmt="XRGB8888", + ... width=width, height=height, strides=[stride]) + >>> pixpat.draw_pattern(buf, "smpte") + +Use :func:`supported_formats` to enumerate the format names accepted by +``Buffer.fmt``, and :func:`is_supported` to test a single format. + +Using numpy buffers +------------------- + +pixpat does not import numpy and does not depend on it, but any +C-contiguous ``numpy.ndarray`` works as a plane via Python's buffer +protocol. The caller is responsible for matching the array's dtype and +shape to the pixel format and for passing the correct row stride: + + >>> import numpy as np + >>> arr = np.zeros((height, width, 4), dtype=np.uint8) + >>> stride = arr.strides[0] # bytes per row + >>> buf = pixpat.Buffer(planes=[arr], fmt="XRGB8888", + ... width=width, height=height, strides=[stride]) + >>> pixpat.draw_pattern(buf, "smpte") + +For multi-plane formats like ``"NV12"`` pass one ndarray per plane (e.g. +``(h, w)`` uint8 for Y and ``(h//2, w)`` uint8 for the interleaved UV +plane). + +If you already hold all planes in a single contiguous buffer — the layout +OpenCV uses for NV12, an ``(h * 3 // 2, w)`` uint8 array with Y on top +and the interleaved UV plane below — slice it into per-plane views and +pass those:: + + >>> nv12 = np.zeros((h * 3 // 2, w), dtype=np.uint8) + >>> y, uv = nv12[:h], nv12[h:].reshape(h // 2, w) + >>> pixpat.Buffer([y, uv], "NV12", w, h, + ... [y.strides[0], uv.strides[0]]) + +The source side of :func:`convert` accepts read-only buffers (``bytes``, +``arr.view()`` with ``writeable=False``, mmap'd files, …). The +destination side must always be writable. +""" + +import ctypes +from dataclasses import dataclass, field +from enum import IntEnum +from typing import Mapping, Optional, Sequence, Union + +from ._native import ( + _Buffer, + _ConvertOpts, + _PatternOpts, + _PinnedBuffers, + _fill_buffer, + _lib, +) + +_VALID_PATTERNS = frozenset( + { + 'kmstest', + 'smpte', + 'plain', + 'checker', + 'hramp', + 'vramp', + 'hbar', + 'vbar', + 'dramp', + 'zoneplate', + } +) + + +class Rec(IntEnum): + """YCbCr color encoding standard used for YUV output formats. + + Selects the matrix used to convert from internal RGB to YUV. Has no + effect when drawing into an RGB or raw format. + + Attributes: + BT601: ITU-R BT.601 (standard definition). + BT709: ITU-R BT.709 (HD). + BT2020: ITU-R BT.2020 (UHD / HDR, non-constant luminance). + """ + + BT601 = 0 + BT709 = 1 + BT2020 = 2 + + +class Range(IntEnum): + """Quantization range used for YUV output formats. + + Attributes: + LIMITED: "TV" / studio range — Y in [16, 235], C in [16, 240] + (scaled to bit depth). What most video pipelines expect. + FULL: "PC" / full range — every component uses the full code range + (e.g. [0, 255] for 8-bit). + """ + + LIMITED = 0 + FULL = 1 + + +class PixpatError(RuntimeError): + """Raised when the underlying ``pixpat_*`` call fails. + + Most commonly indicates an unknown format name or buffer dimensions / + plane layout that the format does not allow (e.g. odd width for a + horizontally-subsampled YUV format). + """ + + +@dataclass +class Buffer: + """Plane data + format + dimensions, mirroring the C ``pixpat_buffer``. + + A passive struct: no allocation, no format knowledge, no + numpy/cv2 awareness. The caller decides plane shapes and strides + based on its own format knowledge (e.g. via ``pixutils``). + + Attributes: + planes: One buffer-protocol object per plane (e.g. ``bytearray``, + ``array.array``, ``numpy.ndarray``, ``mmap.mmap``, + ``memoryview``). Up to 4 planes are supported. Each plane + must hold at least ``strides[i] * plane_height`` bytes, + where ``plane_height`` is ``height`` for the main plane and + ``height // vertical_subsampling`` for chroma planes (e.g. + ``height // 2`` for the UV plane of ``NV12``). For + destination buffers each plane must be writable; for the + source side of :func:`convert` read-only objects (such as + ``bytes``) are accepted. + fmt: Pixel-format name; see :func:`supported_formats`. + width: Image width in pixels. Some formats constrain this — for + chroma-subsampled YUV formats it must be a multiple of the + horizontal subsampling factor (e.g. 2 for ``NV12``), and + packed formats add a further multiple from their + pixel-group size (e.g. 6 for ``P030``, 4 for ``SBGGR10P``). + height: Image height in pixels. For vertically-subsampled + formats (e.g. ``NV12``, ``YUV420``) it must be a multiple + of the vertical subsampling factor. + strides: Per-plane row stride in bytes. Must have the same + length as ``planes``. Strides larger than the minimum row + size are allowed. + """ + + planes: Sequence + fmt: str + width: int + height: int + strides: Sequence[int] = field(default_factory=list) + + +def supported_formats() -> list[str]: + """Return the list of pixel-format names accepted by :class:`Buffer`. + + Format names follow the convention used by `kms++` and `pixutils` + (e.g. ``"XRGB8888"``, ``"NV12"``, ``"YUYV"``) — not the DRM/V4L2 + four-character codes. + """ + n = _lib.pixpat_format_count() + return [_lib.pixpat_format_name(i).decode('ascii') for i in range(n)] + + +def is_supported(fmt: str) -> bool: + """Return whether ``fmt`` is a known pixel-format name. + + Equivalent to ``fmt in supported_formats()`` but cheaper — it does not + materialize the full list. + """ + return bool(_lib.pixpat_format_supported(fmt.encode('ascii'))) + + +def _serialize_pattern_params( + params: Optional[Union[str, Mapping[str, object]]], +) -> Optional[bytes]: + """Encode `params` for the C ABI. + + Accepts a ready-made string (passed through verbatim) or a mapping of + string keys to stringifiable values, which is serialized to the + ``"key=val,key=val"`` form pixpat parses on the C side. Returns None + if there is nothing to pass. + """ + if params is None: + return None + if isinstance(params, str): + return params.encode('ascii') + if isinstance(params, Mapping): + items = [] + for k, v in params.items(): + if not isinstance(k, str): + raise TypeError(f'pattern params keys must be str, got {type(k).__name__}') + sv = str(v) + if ',' in k or '=' in k: + raise ValueError(f'pattern params key contains , or =: {k!r}') + if ',' in sv or '=' in sv: + raise ValueError(f'pattern params value contains , or =: {sv!r}') + items.append(f'{k}={sv}') + return ','.join(items).encode('ascii') + raise TypeError(f'pattern params must be None, str, or a Mapping, got {type(params).__name__}') + + +def draw_pattern( + dst: Buffer, + pattern: Optional[str] = None, + *, + rec: Rec = Rec.BT601, + color_range: Range = Range.LIMITED, + num_threads: int = 0, + params: Optional[Union[str, Mapping[str, object]]] = None, +) -> None: + """Draw a test pattern into ``dst``. + + Args: + dst: Destination buffer; all planes must be writable. + pattern: Name of the pattern to draw. ``None`` selects the default + (equivalent to ``"kmstest"``). Recognized values: + + * ``"kmstest"`` — color gradients with ramps, in the style of + the original ``kmstest`` tool. The default. + * ``"smpte"`` — SMPTE RP 219-1 color bars (with PLUGE). + * ``"plain"`` — solid color fill from ``params["color"]``. + See `params` below. + * ``"checker"`` — black/white checkerboard. Optional + ``params["cell"]`` (decimal positive integer, default 8) + sets the cell size in pixels. ``"cell": "1"`` is a + 1-pixel chroma-subsampling stress test. + * ``"hramp"`` — four horizontal stripes (R, G, B, gray), + each a 0..max ramp along x. Combined per-channel and + luma quantization check. + * ``"vramp"`` — same as ``"hramp"`` rotated 90°: four + vertical columns ramping along y. + * ``"hbar"`` — horizontal bar (full image width, narrow + along y) over a black background. Required + ``params["pos"]`` (signed integer, top edge in pixels); + optional ``params["width"]`` (positive integer, default + 32). The bar is split into seven equal-width regions + colored white/red/white/green/white/blue/white. + * ``"vbar"`` — same as ``"hbar"`` rotated 90°: vertical + bar with ``pos`` measured along x. + * ``"dramp"`` — diagonal RGB ramp (R on x, G on y, + B on x+y). + * ``"zoneplate"`` — centered radial cosine pattern, + frequency ramping from DC at the center to Nyquist at + the longer edge. Useful for spotting scaling/aliasing. + + ``"smpte"`` is defined by the spec in BT.709 / Limited. + Pass ``rec=Rec.BT709, color_range=Range.LIMITED`` for + spec-correct output; other settings produce visibly-wrong + colors when drawing into RGB sinks (the caller's matrix is + applied to BT.709-encoded values). Callers are trusted — + pixpat does not silently override the spec. + rec: YCbCr matrix for YUV formats. Ignored for RGB / raw formats. + Defaults to :attr:`Rec.BT601`. + color_range: Quantization range for YUV formats. Ignored for + RGB / raw formats. Defaults to :attr:`Range.LIMITED`. + num_threads: Worker-thread count. ``0`` selects a sensible + default (one per online CPU, capped to a sane maximum); + ``1`` runs single-threaded with no thread-spawn overhead; + ``N > 1`` uses exactly ``N`` workers. Defaults to ``0``. + Output is bit-identical regardless of the chosen count. + params: Optional pattern-specific parameters. Either a mapping + (``{"color": "ff0000"}``) — serialized to ``"color=ff0000"`` + for the C ABI — or a raw ``"key=val,key=val"`` string. + Unknown keys are silently ignored; patterns that don't read + params (``kmstest``, ``smpte``) ignore this entirely. + Per-pattern keys: + + * ``"plain"`` reads ``"color"`` as a hex RGB(A) string with + an optional ``"0x"`` prefix. The hex-digit count selects + the layout: 6 → 8-bit ``RRGGBB``, 8 → 8-bit ``AARRGGBB`` + (alpha first), 12 → 16-bit ``RRRRGGGGBBBB``, 16 → 16-bit + ``AAAARRRRGGGGBBBB``. Missing or malformed ``"color"`` + raises :class:`PixpatError`. + * ``"checker"`` reads optional ``"cell"`` as a positive + decimal integer; default 8. A non-positive or non-numeric + value raises :class:`PixpatError`. + * ``"hbar"`` / ``"vbar"`` read required ``"pos"`` (signed + decimal integer, top/left edge of the bar in pixels; + negative values clip at the edge) and optional ``"width"`` + (positive decimal integer, bar thickness; default 32). + Missing/non-numeric ``pos`` or non-positive ``width`` + raises :class:`PixpatError`. + + Raises: + ValueError: ``dst.planes`` exceeds the maximum plane count, + ``dst.strides`` length does not match ``dst.planes``, + ``pattern`` is not one of the recognized names, or a + ``params`` mapping value contains a forbidden ``,`` or ``=``. + TypeError: A plane is read-only (e.g. ``bytes``, a read-only + ``memoryview``), or ``params`` has an unsupported type. + PixpatError: The underlying C call failed — typically an unknown + ``fmt``, dimensions / strides incompatible with the format, + or pattern parameters that the pattern rejected. + + Example: + >>> w, h = 640, 480 + >>> stride = w * 4 + >>> data = bytearray(stride * h) + >>> dst = Buffer([data], "XRGB8888", w, h, [stride]) + >>> draw_pattern(dst, "smpte") + >>> draw_pattern(dst, "plain", params={"color": "ff0000"}) + """ + if pattern is None: + pattern = 'kmstest' + elif pattern not in _VALID_PATTERNS: + raise ValueError( + f'unknown pattern {pattern!r}; expected one of {sorted(_VALID_PATTERNS)} or None' + ) + if num_threads < 0: + raise ValueError(f'num_threads must be >= 0, got {num_threads}') + + params_bytes = _serialize_pattern_params(params) + + c_buf = _Buffer() + opts = _PatternOpts() + opts.rec = int(rec) + opts.range = int(color_range) + opts.num_threads = num_threads + opts.params = params_bytes + + with _PinnedBuffers() as pins: + _fill_buffer( + c_buf, + pins, + dst.planes, + dst.fmt, + dst.width, + dst.height, + dst.strides, + writable=True, + ) + rc = _lib.pixpat_draw_pattern( + ctypes.byref(c_buf), pattern.encode('ascii'), ctypes.byref(opts) + ) + + if rc != 0: + raise PixpatError(f'pixpat_draw_pattern failed (rc={rc}); check format name and dimensions') + + +def convert( + dst: Buffer, + src: Buffer, + *, + rec: Rec = Rec.BT601, + color_range: Range = Range.LIMITED, + num_threads: int = 0, +) -> None: + """Convert pixel data from ``src`` into ``dst``. + + Both buffers must describe an image of the same width and height; + only the pixel format may differ. Any format accepted by + :func:`supported_formats` works as both source and destination in + the default build (custom build profiles can mark individual formats + read-only or write-only). Conversion is routed internally through a + 16-bit normalized RGB or YUV intermediate, so format-to-format + conversions in either direction (e.g. ``NV12`` -> ``YUV420``, + ``XRGB8888`` -> ``NV12``, ``SRGGB10`` -> ``BGR888``) are a single + call. Bayer sources are decoded with a 3x3 bilinear demosaic. + + ``src.planes`` may hold read-only buffers (e.g. ``bytes``, mmap'd + files, numpy arrays with ``writeable=False``); ``dst.planes`` must + be writable. + + Args: + dst: Destination buffer. + src: Source buffer. + rec: YCbCr matrix used when the conversion crosses the RGB/YUV + boundary. Ignored when ``src.fmt`` and ``dst.fmt`` share + the same color kind. Defaults to :attr:`Rec.BT601`. + color_range: Quantization range used when the conversion crosses + the RGB/YUV boundary. Ignored when ``src.fmt`` and + ``dst.fmt`` share the same color kind. Defaults to + :attr:`Range.LIMITED`. + num_threads: Worker-thread count. ``0`` selects a sensible + default (one per online CPU, capped to a sane maximum); + ``1`` runs single-threaded with no thread-spawn overhead; + ``N > 1`` uses exactly ``N`` workers. Defaults to ``0``. + Output is bit-identical regardless of the chosen count. + + Raises: + ValueError: ``dst`` and ``src`` have mismatched dimensions, a + plane sequence exceeds the maximum plane count, or a + strides length does not match its planes length. + TypeError: A ``dst`` plane is read-only. + PixpatError: The underlying C call failed — typically an + unknown format name, a format disabled in the current build + (or disabled in the requested direction), or dimensions / + strides incompatible with one of the formats. + + Example: + Cross-color-kind, multi-plane on both sides — paint an NV12 + source via :func:`draw_pattern`, then convert it to planar + YUV420: + + >>> w, h = 64, 32 + >>> y_src = bytearray(w * h) + >>> uv_src = bytearray(w * h // 2) + >>> draw_pattern(Buffer([y_src, uv_src], "NV12", w, h, [w, w]), + ... "smpte") + >>> y_dst = bytearray(w * h) + >>> u_dst = bytearray(w * h // 4) + >>> v_dst = bytearray(w * h // 4) + >>> convert( + ... Buffer([y_dst, u_dst, v_dst], "YUV420", w, h, + ... [w, w // 2, w // 2]), + ... Buffer([y_src, uv_src], "NV12", w, h, [w, w]), + ... ) + """ + if dst.width != src.width or dst.height != src.height: + raise ValueError( + f'dst dimensions {dst.width}x{dst.height} do not match ' + f'src dimensions {src.width}x{src.height}' + ) + if num_threads < 0: + raise ValueError(f'num_threads must be >= 0, got {num_threads}') + + c_dst = _Buffer() + c_src = _Buffer() + opts = _ConvertOpts() + opts.rec = int(rec) + opts.range = int(color_range) + opts.num_threads = num_threads + + with _PinnedBuffers() as pins: + _fill_buffer( + c_dst, + pins, + dst.planes, + dst.fmt, + dst.width, + dst.height, + dst.strides, + writable=True, + role='dst', + ) + _fill_buffer( + c_src, + pins, + src.planes, + src.fmt, + src.width, + src.height, + src.strides, + writable=False, + role='src', + ) + rc = _lib.pixpat_convert(ctypes.byref(c_dst), ctypes.byref(c_src), ctypes.byref(opts)) + + if rc != 0: + raise PixpatError( + f'pixpat_convert failed (rc={rc}); check format names, dimensions, ' + f'and that src.fmt is a supported source format' + ) + + +__all__ = [ + 'Buffer', + 'Rec', + 'Range', + 'PixpatError', + 'supported_formats', + 'is_supported', + 'draw_pattern', + 'convert', +] diff --git a/pixpat-python/pixpat/_lib/.gitkeep b/pixpat-python/pixpat/_lib/.gitkeep new file mode 100644 index 0000000..7319712 --- /dev/null +++ b/pixpat-python/pixpat/_lib/.gitkeep @@ -0,0 +1,3 @@ +# Placeholder so the _lib/ directory exists in source control. +# `pip install .` and `pip install -e .` populate libpixpat.so.0 here +# (copy and symlink respectively); see setup.py. diff --git a/pixpat-python/pixpat/_native.py b/pixpat-python/pixpat/_native.py new file mode 100644 index 0000000..46fe452 --- /dev/null +++ b/pixpat-python/pixpat/_native.py @@ -0,0 +1,203 @@ +"""ctypes plumbing for libpixpat. + +Private module — public API lives in :mod:`pixpat`. Loads the bundled +shared library, declares the C structs and function signatures, and +provides :func:`_fill_buffer` to translate Python plane sequences into +the C ``pixpat_buffer`` layout while pinning the underlying memory via +the buffer protocol. + +The buffer-protocol path uses ``PyObject_GetBuffer`` / ``PyBuffer_Release`` +directly (rather than ctypes ``from_buffer``) so that read-only inputs +work for the ``src`` side of :func:`pixpat.convert`. ``from_buffer`` +unconditionally requires a writable buffer. +""" + +import ctypes +import os +import pathlib +from typing import Sequence + +_PIXPAT_MAX_PLANES = 4 + +_lib_override = os.environ.get('PIXPAT_LIB') +if _lib_override: + if not pathlib.Path(_lib_override).exists(): + raise ImportError(f'pixpat: PIXPAT_LIB={_lib_override} does not exist') + _lib = ctypes.CDLL(_lib_override) +else: + _lib_dir = pathlib.Path(__file__).parent / '_lib' + _so_candidates = sorted(_lib_dir.glob('libpixpat.so*')) + if not _so_candidates: + raise ImportError(f'pixpat: no libpixpat.so* found in {_lib_dir}') + _lib = ctypes.CDLL(str(_so_candidates[0])) + + +class _Buffer(ctypes.Structure): + _fields_ = [ + ('format', ctypes.c_char_p), + ('width', ctypes.c_uint32), + ('height', ctypes.c_uint32), + ('num_planes', ctypes.c_uint32), + ('planes', ctypes.c_void_p * _PIXPAT_MAX_PLANES), + ('strides', ctypes.c_uint32 * _PIXPAT_MAX_PLANES), + ] + + +class _PatternOpts(ctypes.Structure): + _fields_ = [ + ('rec', ctypes.c_int), + ('range', ctypes.c_int), + ('num_threads', ctypes.c_int), + ('params', ctypes.c_char_p), + ] + + +class _ConvertOpts(ctypes.Structure): + _fields_ = [ + ('rec', ctypes.c_int), + ('range', ctypes.c_int), + ('num_threads', ctypes.c_int), + ] + + +_lib.pixpat_draw_pattern.argtypes = [ + ctypes.POINTER(_Buffer), + ctypes.c_char_p, + ctypes.POINTER(_PatternOpts), +] +_lib.pixpat_draw_pattern.restype = ctypes.c_int + +_lib.pixpat_convert.argtypes = [ + ctypes.POINTER(_Buffer), + ctypes.POINTER(_Buffer), + ctypes.POINTER(_ConvertOpts), +] +_lib.pixpat_convert.restype = ctypes.c_int + +_lib.pixpat_format_supported.argtypes = [ctypes.c_char_p] +_lib.pixpat_format_supported.restype = ctypes.c_int + +_lib.pixpat_format_count.argtypes = [] +_lib.pixpat_format_count.restype = ctypes.c_size_t + +_lib.pixpat_format_name.argtypes = [ctypes.c_size_t] +_lib.pixpat_format_name.restype = ctypes.c_char_p + + +class _Py_buffer(ctypes.Structure): + _fields_ = [ + ('buf', ctypes.c_void_p), + ('obj', ctypes.py_object), + ('len', ctypes.c_ssize_t), + ('itemsize', ctypes.c_ssize_t), + ('readonly', ctypes.c_int), + ('ndim', ctypes.c_int), + ('format', ctypes.c_char_p), + ('shape', ctypes.POINTER(ctypes.c_ssize_t)), + ('strides', ctypes.POINTER(ctypes.c_ssize_t)), + ('suboffsets', ctypes.POINTER(ctypes.c_ssize_t)), + ('internal', ctypes.c_void_p), + ] + + +_PyObject_GetBuffer = ctypes.pythonapi.PyObject_GetBuffer +_PyObject_GetBuffer.argtypes = [ + ctypes.py_object, + ctypes.POINTER(_Py_buffer), + ctypes.c_int, +] +_PyObject_GetBuffer.restype = ctypes.c_int + +_PyBuffer_Release = ctypes.pythonapi.PyBuffer_Release +_PyBuffer_Release.argtypes = [ctypes.POINTER(_Py_buffer)] +_PyBuffer_Release.restype = None + +_PyBUF_SIMPLE = 0 +_PyBUF_WRITABLE = 0x0001 + + +class _PinnedBuffers: + """Holds Py_buffer views for the lifetime of a pixpat call. + + Releases each view in ``__exit__`` (or when garbage-collected) so + the underlying objects' buffer-export count drops back to zero. + """ + + def __init__(self) -> None: + self._views: list[_Py_buffer] = [] + + def acquire(self, obj, *, writable: bool) -> int: + view = _Py_buffer() + flags = _PyBUF_WRITABLE if writable else _PyBUF_SIMPLE + # ctypes.pythonapi propagates the set exception (BufferError / + # TypeError) automatically on failure, so no rc check is needed. + _PyObject_GetBuffer(obj, ctypes.byref(view), flags) + self._views.append(view) + return view.buf or 0 + + def release(self) -> None: + while self._views: + view = self._views.pop() + _PyBuffer_Release(ctypes.byref(view)) + + def __enter__(self) -> '_PinnedBuffers': + return self + + def __exit__(self, exc_type, exc, tb) -> None: + self.release() + + def __del__(self) -> None: + self.release() + + +def _fill_buffer( + buf: _Buffer, + pins: _PinnedBuffers, + planes: Sequence, + fmt: str, + width: int, + height: int, + strides: Sequence[int], + *, + writable: bool, + role: str = '', +) -> None: + """Populate ``buf`` from a Python plane sequence, pinning each plane. + + ``writable`` selects whether each plane must be writable: True for + destination buffers (the C library writes into them), False for + source buffers (the C library only reads). + """ + label = f'{role} ' if role else '' + if len(planes) > _PIXPAT_MAX_PLANES: + raise ValueError(f'too many {label}planes: {len(planes)} (max {_PIXPAT_MAX_PLANES})') + if len(strides) != len(planes): + raise ValueError(f'{label}strides has {len(strides)} entries, expected {len(planes)}') + + plane_ptrs = (ctypes.c_void_p * _PIXPAT_MAX_PLANES)() + stride_arr = (ctypes.c_uint32 * _PIXPAT_MAX_PLANES)() + for i, plane in enumerate(planes): + try: + addr = pins.acquire(plane, writable=writable) + except BufferError as e: + kind = 'writable' if writable else 'buffer-protocol' + raise TypeError(f'{label}plane {i} does not support the {kind} interface: {e}') from e + plane_ptrs[i] = addr + stride_arr[i] = strides[i] + + buf.format = fmt.encode('ascii') + buf.width = width + buf.height = height + buf.num_planes = len(planes) + buf.planes = plane_ptrs + buf.strides = stride_arr + + +__all__ = [ + '_Buffer', + '_PatternOpts', + '_ConvertOpts', + '_PinnedBuffers', + '_lib', + '_fill_buffer', +] -- cgit v1.2.3