pixflex 0.1.0__tar.gz

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 (44) hide show
  1. pixflex-0.1.0/.github/workflows/ci.yml +33 -0
  2. pixflex-0.1.0/.github/workflows/wheels.yml +80 -0
  3. pixflex-0.1.0/.gitignore +8 -0
  4. pixflex-0.1.0/LICENSE +21 -0
  5. pixflex-0.1.0/PKG-INFO +150 -0
  6. pixflex-0.1.0/README.md +131 -0
  7. pixflex-0.1.0/docs/GALLERY.md +448 -0
  8. pixflex-0.1.0/docs/HANDOVER.md +78 -0
  9. pixflex-0.1.0/docs/gallery/absolute.png +0 -0
  10. pixflex-0.1.0/docs/gallery/align_items.png +0 -0
  11. pixflex-0.1.0/docs/gallery/before_after.png +0 -0
  12. pixflex-0.1.0/docs/gallery/collage.png +0 -0
  13. pixflex-0.1.0/docs/gallery/contact_sheet.png +0 -0
  14. pixflex-0.1.0/docs/gallery/feature_maps.png +0 -0
  15. pixflex-0.1.0/docs/gallery/flex_direction.png +0 -0
  16. pixflex-0.1.0/docs/gallery/flex_wrap.png +0 -0
  17. pixflex-0.1.0/docs/gallery/gap.png +0 -0
  18. pixflex-0.1.0/docs/gallery/homepage.png +0 -0
  19. pixflex-0.1.0/docs/gallery/justified.png +0 -0
  20. pixflex-0.1.0/docs/gallery/justify_content.png +0 -0
  21. pixflex-0.1.0/docs/gallery/responsive.png +0 -0
  22. pixflex-0.1.0/docs/gallery/sizing.png +0 -0
  23. pixflex-0.1.0/examples/batch_torch.py +25 -0
  24. pixflex-0.1.0/examples/figure_panel.py +54 -0
  25. pixflex-0.1.0/examples/gallery/__init__.py +0 -0
  26. pixflex-0.1.0/examples/gallery/_kit.py +105 -0
  27. pixflex-0.1.0/examples/gallery/examples.py +428 -0
  28. pixflex-0.1.0/examples/gallery.py +71 -0
  29. pixflex-0.1.0/examples/video_overlay.py +124 -0
  30. pixflex-0.1.0/pyproject.toml +45 -0
  31. pixflex-0.1.0/src/pixflex/__init__.py +39 -0
  32. pixflex-0.1.0/src/pixflex/_backend.py +40 -0
  33. pixflex-0.1.0/src/pixflex/canvas.py +464 -0
  34. pixflex-0.1.0/src/pixflex/dsl.py +163 -0
  35. pixflex-0.1.0/src/pixflex/errors.py +37 -0
  36. pixflex-0.1.0/src/pixflex/fit.py +95 -0
  37. pixflex-0.1.0/src/pixflex/plan.py +240 -0
  38. pixflex-0.1.0/src/pixflex/resize.py +111 -0
  39. pixflex-0.1.0/tests/__init__.py +0 -0
  40. pixflex-0.1.0/tests/conftest.py +60 -0
  41. pixflex-0.1.0/tests/test_canvas.py +388 -0
  42. pixflex-0.1.0/tests/test_gallery.py +20 -0
  43. pixflex-0.1.0/tests/test_key_typing.py +83 -0
  44. pixflex-0.1.0/tests/test_layout.py +128 -0
@@ -0,0 +1,33 @@
1
+ name: CI
2
+ on:
3
+ push: { branches: [main] }
4
+ pull_request:
5
+
6
+ jobs:
7
+ test:
8
+ name: test (${{ matrix.os }}, ${{ matrix.python }})
9
+ strategy:
10
+ fail-fast: false
11
+ matrix:
12
+ os: [ubuntu-latest, macos-latest, windows-latest]
13
+ python: ["3.10", "3.14"]
14
+ runs-on: ${{ matrix.os }}
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - uses: astral-sh/setup-uv@v5
18
+ - run: uv sync --python ${{ matrix.python }} --group dev
19
+ - run: uv run ruff check .
20
+ - run: uv run ruff format --check .
21
+ - run: uv run pytest -q # torch tests and the gallery smoke test skip themselves here
22
+
23
+ torch:
24
+ name: test with torch (CPU)
25
+ runs-on: ubuntu-latest
26
+ steps:
27
+ - uses: actions/checkout@v4
28
+ - uses: astral-sh/setup-uv@v5
29
+ - run: uv sync --python 3.12 --group dev --group examples
30
+ # after `uv sync`, which would otherwise remove it
31
+ - run: uv pip install torch --index-url https://download.pytorch.org/whl/cpu
32
+ - run: uv run --no-sync pyright # needs torch and imageio to resolve every import
33
+ - run: uv run --no-sync pytest -q # runs every test on numpy and torch, plus the gallery
@@ -0,0 +1,80 @@
1
+ name: Wheels
2
+
3
+ # Builds the sdist + wheel on every PR, manual run and version tag. Publishing uses PyPI trusted
4
+ # publishing (OIDC, no tokens) and uploads PEP 740 attestations automatically.
5
+ # * push a tag `vX.Y.Z` (must equal the version in pyproject.toml) -> PyPI
6
+ # * manual run with `publish: testpypi` -> TestPyPI
7
+ on:
8
+ pull_request:
9
+ push:
10
+ tags: ["v*"]
11
+ workflow_dispatch:
12
+ inputs:
13
+ publish:
14
+ description: "Where to publish the built files"
15
+ type: choice
16
+ options: [none, testpypi]
17
+ default: none
18
+
19
+ permissions:
20
+ contents: read
21
+
22
+ jobs:
23
+ build:
24
+ runs-on: ubuntu-latest
25
+ steps:
26
+ - uses: actions/checkout@v4
27
+ - uses: astral-sh/setup-uv@v5
28
+ - run: uv build
29
+ - run: pipx run twine check --strict dist/*
30
+ - name: Smoke-test the wheel in a clean environment
31
+ run: |
32
+ uv venv --python 3.12 /tmp/smoke
33
+ uv pip install --python /tmp/smoke dist/*.whl numpy pillow # pillow: resizer for the contain fit
34
+ cd /tmp && /tmp/smoke/bin/python - <<'PY'
35
+ import numpy as np
36
+ from pixflex import Column, Row, Slot
37
+
38
+ with Column(width=40, height=20, background=0) as root:
39
+ with Row(flex=1, gap=2):
40
+ a = Slot(flex=1)
41
+ b = Slot(flex=1, fit="contain")
42
+ img = root.solve().compose({a: np.full((5, 5, 3), 200, np.uint8), b: np.full((5, 5, 3), 90, np.uint8)}, like=np.zeros((1, 1, 3), np.uint8))
43
+ assert img.shape == (20, 40, 3) and img.max() == 200, img.shape
44
+ print("ok", img.shape)
45
+ PY
46
+ - uses: actions/upload-artifact@v4
47
+ with:
48
+ name: dist
49
+ path: dist/*
50
+
51
+ publish-pypi:
52
+ if: startsWith(github.ref, 'refs/tags/v')
53
+ needs: build
54
+ runs-on: ubuntu-latest
55
+ environment: pypi
56
+ permissions:
57
+ id-token: write
58
+ steps:
59
+ - uses: actions/checkout@v4
60
+ - name: Tag must match the package version
61
+ run: |
62
+ version=$(python3 -c "import tomllib;print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
63
+ test "v$version" = "${GITHUB_REF_NAME}" || { echo "tag ${GITHUB_REF_NAME} != version v$version"; exit 1; }
64
+ - uses: actions/download-artifact@v4
65
+ with: { name: dist, path: dist }
66
+ - uses: pypa/gh-action-pypi-publish@release/v1
67
+
68
+ publish-testpypi:
69
+ if: github.event_name == 'workflow_dispatch' && inputs.publish == 'testpypi'
70
+ needs: build
71
+ runs-on: ubuntu-latest
72
+ environment: testpypi
73
+ permissions:
74
+ id-token: write
75
+ steps:
76
+ - uses: actions/download-artifact@v4
77
+ with: { name: dist, path: dist }
78
+ - uses: pypa/gh-action-pypi-publish@release/v1
79
+ with:
80
+ repository-url: https://test.pypi.org/legacy/
@@ -0,0 +1,8 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.egg-info/
4
+ dist/
5
+ *.mp4
6
+ *.gif
7
+ out/
8
+ uv.lock
pixflex-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Patrick Snape
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
pixflex-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,150 @@
1
+ Metadata-Version: 2.5
2
+ Name: pixflex
3
+ Version: 0.1.0
4
+ Summary: Flexbox layout for image arrays: solve a layout with Yoga, then place numpy/torch/... content into it
5
+ License-Expression: MIT
6
+ License-File: LICENSE
7
+ Classifier: Typing :: Typed
8
+ Requires-Python: >=3.10
9
+ Requires-Dist: array-api-compat>=1.9
10
+ Requires-Dist: yogalayout>=0.1
11
+ Provides-Extra: numpy
12
+ Requires-Dist: numpy>=1.26; extra == 'numpy'
13
+ Provides-Extra: resize
14
+ Requires-Dist: opencv-python-headless; extra == 'resize'
15
+ Requires-Dist: pillow; extra == 'resize'
16
+ Provides-Extra: torch
17
+ Requires-Dist: torch; extra == 'torch'
18
+ Description-Content-Type: text/markdown
19
+
20
+ # pixflex
21
+
22
+ Lay out images the way you lay out a web page. Describe the arrangement as a flexbox tree,
23
+ let [Yoga](https://www.yogalayout.dev/) work out every rectangle in whole pixels, then place
24
+ numpy / torch / any [array-API](https://data-apis.org/array-api/) arrays into it — with as few
25
+ copies as possible and no manual index shifting.
26
+
27
+ ```python
28
+ import numpy as np
29
+ from pixflex import Column, Row, Slot
30
+
31
+ with Column(width=1280, height=720, background=(16, 16, 20)) as root:
32
+ title = Slot(height=80)
33
+ with Row(flex=1, gap=8):
34
+ video = Slot(flex=3, fit="contain", background=(0, 0, 0))
35
+ plot = Slot(flex=1, fit="exact")
36
+ footer = Slot(height=40)
37
+
38
+ plan = root.solve() # no arrays involved
39
+ plan[plot].hw # (600, 318): render your plot at exactly this size
40
+ img = plan.compose({title: t, video: frame, plot: p, footer: f}) # one allocation
41
+ ```
42
+
43
+ ## Install
44
+
45
+ ```
46
+ pip install pixflex # numpy arrays; resizing needs one of the extras below
47
+ pip install "pixflex[numpy,resize]" # numpy + Pillow/OpenCV resizers
48
+ pip install torch # torch tensors need no extra
49
+ ```
50
+
51
+ ## Gallery
52
+
53
+ Every image here was produced by pixflex itself (Yoga solves the layout, arrays are composed
54
+ into one buffer). [**See the full gallery with code →**](docs/GALLERY.md)
55
+
56
+ <table>
57
+ <tr>
58
+ <td><a href="docs/GALLERY.md#the-yogalayoutdev-homepage-layout"><img src="docs/gallery/homepage.png" width="400" alt="The Yoga homepage layout, solved and filled"></a></td>
59
+ <td><a href="docs/GALLERY.md#justified-rows-flickr-style"><img src="docs/gallery/justified.png" width="480" alt="Justified photo rows"></a></td>
60
+ </tr>
61
+ <tr>
62
+ <td><a href="docs/GALLERY.md#contact-sheet-with-captions"><img src="docs/gallery/contact_sheet.png" width="400" alt="Contact sheet"></a></td>
63
+ <td><a href="docs/GALLERY.md#torch-feature-maps"><img src="docs/gallery/feature_maps.png" width="480" alt="Torch feature maps"></a></td>
64
+ </tr>
65
+ </table>
66
+
67
+ Also: photo collage, before/after, one layout at three widths, and recreations of the Yoga docs'
68
+ `justify-content`, `align-items`, `flex-direction`, `flex-wrap`, `gap`, `position: absolute`
69
+ and sizing playgrounds.
70
+
71
+ ## Concepts
72
+
73
+ - **Declare** with `Column`/`Row`/`Box` (containers, usable as `with` blocks or with
74
+ `children=[...]`) and `Slot` (leaves). Style keywords are CSS-like and come from
75
+ [`yogalayout`](../yogalayout): `flex=1`, `gap=8`, `padding=(10, 20)`, `margin_inline=10`,
76
+ `position="absolute"`, `"50%"`, ...
77
+ - **Solve**: `plan = root.solve()` gives absolute integer `Rect(y, x, h, w)`s, no arrays touched.
78
+ - **Place**: `plan.compose({...})`, or keep a reusable `Canvas` (below).
79
+ - **Fit** per slot or per write, following CSS `object-fit`: `contain` (default), `cover`
80
+ (crops first, so only kept pixels are resized), `fill`, `none`, `scale-down`, `exact` (raises
81
+ `ShapeMismatch` instead of resizing). `align` follows `object-position`
82
+ (`"top left"`, or an `(x, y)` fraction pair).
83
+ - **Writes never change the layout and never leave their slot's rectangle.**
84
+ - **Stacking follows tree order** (CSS painter's algorithm): later nodes draw on top of earlier
85
+ ones, whatever order you write content in. A slot absolutely positioned over another (a logo
86
+ over a video) is redrawn automatically, clipped to the overlap, whenever the slot below is
87
+ rewritten. (Pixels of overlaying slots are kept for this; mutate your array in place and the
88
+ next redraw shows the new data.)
89
+
90
+ ## Addressing slots
91
+
92
+ Plans and canvases are indexed by **any hashable**. Recommended, in order:
93
+
94
+ 1. **Handles** — the `Slot`/`Box` objects themselves (identity-hashed), bundled in a
95
+ `NamedTuple` if you build the layout in a function. Static checkers can follow them.
96
+ 2. **Enum members** (or tuples, ints...) via `Slot(key=Panel.VIDEO)`; with
97
+ `root.solve(key_type=Panel)` pyright rejects `cv["video"]` and typos.
98
+ 3. **Strings** for scripts and config-driven layouts. Unknown keys raise `KeyError` with a
99
+ "did you mean" hint; duplicates raise `DuplicateKeyError` at `solve()`.
100
+
101
+ `cv.untyped` / `plan.untyped` give a `Hashable`-keyed view when key types are mixed.
102
+
103
+ ## Reusing a canvas (video, dashboards)
104
+
105
+ ```python
106
+ cv = plan.canvas(like=header_img, static={title: header_img, footer: footer_img})
107
+ for frame in frames:
108
+ cv[video] = frame # only the video slot's rectangle is rewritten
109
+ writer.write_frame(cv.array) # live buffer, zero copy
110
+ keep.append(cv.snapshot()) # explicit copy when you need to keep a frame
111
+ ```
112
+
113
+ - Constant slots are drawn once. When a `contain`/`none` placement changes size, only the
114
+ stale padding strips are cleared.
115
+ - `cv.clear(k)`, `cv.update({a: x, b: y})`, `cv.sub(box)` (a view-backed canvas with box-relative
116
+ rects), `cv.view(k)` (no-copy view), `cv.to_numpy()` (the one device→host copy).
117
+ - `cv.ring(n)` cycles `n` pre-drawn buffers for async consumers (encoder threads).
118
+ - Leading axes: `plan.canvas(like, axes="THWC")` broadcasts static parts over time and writes a
119
+ whole clip with one batched resize. Axes may use `H W C T N` in any order (`"CHW"`, `"HW"`...).
120
+
121
+ ## Mismatched data
122
+
123
+ | Mismatch | Behaviour |
124
+ |---|---|
125
+ | Size ≠ slot | slot's `fit` applies; `exact` raises `ShapeMismatch` |
126
+ | 1 channel into C | broadcasts; missing channel axis is added |
127
+ | RGBA into RGB | raises unless `alpha="drop"` or `"over"` |
128
+ | dtype | cast only if lossless (`can_cast`); else `DTypeMismatch`; `convert=fn` overrides |
129
+ | device / array library | raises (`to_device=True` allows a transfer) |
130
+
131
+ ## Backends and resizing
132
+
133
+ Allocation and writes go through `array-api-compat`, so numpy and torch work today (both are
134
+ tested). Resizers are chosen lazily: torch → `F.interpolate(antialias=True)`; numpy → OpenCV, else
135
+ Pillow (uint8 / float32), else SciPy. Install one (`pip install pixflex[resize]`) or plug in
136
+ your own with `pixflex.register_resizer("numpy", fn)`. **Not yet supported:** immutable arrays
137
+ (JAX) and cupy are untested.
138
+
139
+ ## Development
140
+
141
+ ```
142
+ uv sync --group dev # (a venv with CPU torch installed by hand: use `uv pip install -e .` instead)
143
+ .venv/bin/python -m pytest
144
+ uv sync --group examples && python examples/video_overlay.py # writes out/video_overlay.mp4
145
+ ```
146
+
147
+ See [`docs/HANDOVER.md`](docs/HANDOVER.md) for the design, status
148
+ and next steps.
149
+
150
+ MIT licensed.
@@ -0,0 +1,131 @@
1
+ # pixflex
2
+
3
+ Lay out images the way you lay out a web page. Describe the arrangement as a flexbox tree,
4
+ let [Yoga](https://www.yogalayout.dev/) work out every rectangle in whole pixels, then place
5
+ numpy / torch / any [array-API](https://data-apis.org/array-api/) arrays into it — with as few
6
+ copies as possible and no manual index shifting.
7
+
8
+ ```python
9
+ import numpy as np
10
+ from pixflex import Column, Row, Slot
11
+
12
+ with Column(width=1280, height=720, background=(16, 16, 20)) as root:
13
+ title = Slot(height=80)
14
+ with Row(flex=1, gap=8):
15
+ video = Slot(flex=3, fit="contain", background=(0, 0, 0))
16
+ plot = Slot(flex=1, fit="exact")
17
+ footer = Slot(height=40)
18
+
19
+ plan = root.solve() # no arrays involved
20
+ plan[plot].hw # (600, 318): render your plot at exactly this size
21
+ img = plan.compose({title: t, video: frame, plot: p, footer: f}) # one allocation
22
+ ```
23
+
24
+ ## Install
25
+
26
+ ```
27
+ pip install pixflex # numpy arrays; resizing needs one of the extras below
28
+ pip install "pixflex[numpy,resize]" # numpy + Pillow/OpenCV resizers
29
+ pip install torch # torch tensors need no extra
30
+ ```
31
+
32
+ ## Gallery
33
+
34
+ Every image here was produced by pixflex itself (Yoga solves the layout, arrays are composed
35
+ into one buffer). [**See the full gallery with code →**](docs/GALLERY.md)
36
+
37
+ <table>
38
+ <tr>
39
+ <td><a href="docs/GALLERY.md#the-yogalayoutdev-homepage-layout"><img src="docs/gallery/homepage.png" width="400" alt="The Yoga homepage layout, solved and filled"></a></td>
40
+ <td><a href="docs/GALLERY.md#justified-rows-flickr-style"><img src="docs/gallery/justified.png" width="480" alt="Justified photo rows"></a></td>
41
+ </tr>
42
+ <tr>
43
+ <td><a href="docs/GALLERY.md#contact-sheet-with-captions"><img src="docs/gallery/contact_sheet.png" width="400" alt="Contact sheet"></a></td>
44
+ <td><a href="docs/GALLERY.md#torch-feature-maps"><img src="docs/gallery/feature_maps.png" width="480" alt="Torch feature maps"></a></td>
45
+ </tr>
46
+ </table>
47
+
48
+ Also: photo collage, before/after, one layout at three widths, and recreations of the Yoga docs'
49
+ `justify-content`, `align-items`, `flex-direction`, `flex-wrap`, `gap`, `position: absolute`
50
+ and sizing playgrounds.
51
+
52
+ ## Concepts
53
+
54
+ - **Declare** with `Column`/`Row`/`Box` (containers, usable as `with` blocks or with
55
+ `children=[...]`) and `Slot` (leaves). Style keywords are CSS-like and come from
56
+ [`yogalayout`](../yogalayout): `flex=1`, `gap=8`, `padding=(10, 20)`, `margin_inline=10`,
57
+ `position="absolute"`, `"50%"`, ...
58
+ - **Solve**: `plan = root.solve()` gives absolute integer `Rect(y, x, h, w)`s, no arrays touched.
59
+ - **Place**: `plan.compose({...})`, or keep a reusable `Canvas` (below).
60
+ - **Fit** per slot or per write, following CSS `object-fit`: `contain` (default), `cover`
61
+ (crops first, so only kept pixels are resized), `fill`, `none`, `scale-down`, `exact` (raises
62
+ `ShapeMismatch` instead of resizing). `align` follows `object-position`
63
+ (`"top left"`, or an `(x, y)` fraction pair).
64
+ - **Writes never change the layout and never leave their slot's rectangle.**
65
+ - **Stacking follows tree order** (CSS painter's algorithm): later nodes draw on top of earlier
66
+ ones, whatever order you write content in. A slot absolutely positioned over another (a logo
67
+ over a video) is redrawn automatically, clipped to the overlap, whenever the slot below is
68
+ rewritten. (Pixels of overlaying slots are kept for this; mutate your array in place and the
69
+ next redraw shows the new data.)
70
+
71
+ ## Addressing slots
72
+
73
+ Plans and canvases are indexed by **any hashable**. Recommended, in order:
74
+
75
+ 1. **Handles** — the `Slot`/`Box` objects themselves (identity-hashed), bundled in a
76
+ `NamedTuple` if you build the layout in a function. Static checkers can follow them.
77
+ 2. **Enum members** (or tuples, ints...) via `Slot(key=Panel.VIDEO)`; with
78
+ `root.solve(key_type=Panel)` pyright rejects `cv["video"]` and typos.
79
+ 3. **Strings** for scripts and config-driven layouts. Unknown keys raise `KeyError` with a
80
+ "did you mean" hint; duplicates raise `DuplicateKeyError` at `solve()`.
81
+
82
+ `cv.untyped` / `plan.untyped` give a `Hashable`-keyed view when key types are mixed.
83
+
84
+ ## Reusing a canvas (video, dashboards)
85
+
86
+ ```python
87
+ cv = plan.canvas(like=header_img, static={title: header_img, footer: footer_img})
88
+ for frame in frames:
89
+ cv[video] = frame # only the video slot's rectangle is rewritten
90
+ writer.write_frame(cv.array) # live buffer, zero copy
91
+ keep.append(cv.snapshot()) # explicit copy when you need to keep a frame
92
+ ```
93
+
94
+ - Constant slots are drawn once. When a `contain`/`none` placement changes size, only the
95
+ stale padding strips are cleared.
96
+ - `cv.clear(k)`, `cv.update({a: x, b: y})`, `cv.sub(box)` (a view-backed canvas with box-relative
97
+ rects), `cv.view(k)` (no-copy view), `cv.to_numpy()` (the one device→host copy).
98
+ - `cv.ring(n)` cycles `n` pre-drawn buffers for async consumers (encoder threads).
99
+ - Leading axes: `plan.canvas(like, axes="THWC")` broadcasts static parts over time and writes a
100
+ whole clip with one batched resize. Axes may use `H W C T N` in any order (`"CHW"`, `"HW"`...).
101
+
102
+ ## Mismatched data
103
+
104
+ | Mismatch | Behaviour |
105
+ |---|---|
106
+ | Size ≠ slot | slot's `fit` applies; `exact` raises `ShapeMismatch` |
107
+ | 1 channel into C | broadcasts; missing channel axis is added |
108
+ | RGBA into RGB | raises unless `alpha="drop"` or `"over"` |
109
+ | dtype | cast only if lossless (`can_cast`); else `DTypeMismatch`; `convert=fn` overrides |
110
+ | device / array library | raises (`to_device=True` allows a transfer) |
111
+
112
+ ## Backends and resizing
113
+
114
+ Allocation and writes go through `array-api-compat`, so numpy and torch work today (both are
115
+ tested). Resizers are chosen lazily: torch → `F.interpolate(antialias=True)`; numpy → OpenCV, else
116
+ Pillow (uint8 / float32), else SciPy. Install one (`pip install pixflex[resize]`) or plug in
117
+ your own with `pixflex.register_resizer("numpy", fn)`. **Not yet supported:** immutable arrays
118
+ (JAX) and cupy are untested.
119
+
120
+ ## Development
121
+
122
+ ```
123
+ uv sync --group dev # (a venv with CPU torch installed by hand: use `uv pip install -e .` instead)
124
+ .venv/bin/python -m pytest
125
+ uv sync --group examples && python examples/video_overlay.py # writes out/video_overlay.mp4
126
+ ```
127
+
128
+ See [`docs/HANDOVER.md`](docs/HANDOVER.md) for the design, status
129
+ and next steps.
130
+
131
+ MIT licensed.