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.
- pixflex-0.1.0/.github/workflows/ci.yml +33 -0
- pixflex-0.1.0/.github/workflows/wheels.yml +80 -0
- pixflex-0.1.0/.gitignore +8 -0
- pixflex-0.1.0/LICENSE +21 -0
- pixflex-0.1.0/PKG-INFO +150 -0
- pixflex-0.1.0/README.md +131 -0
- pixflex-0.1.0/docs/GALLERY.md +448 -0
- pixflex-0.1.0/docs/HANDOVER.md +78 -0
- pixflex-0.1.0/docs/gallery/absolute.png +0 -0
- pixflex-0.1.0/docs/gallery/align_items.png +0 -0
- pixflex-0.1.0/docs/gallery/before_after.png +0 -0
- pixflex-0.1.0/docs/gallery/collage.png +0 -0
- pixflex-0.1.0/docs/gallery/contact_sheet.png +0 -0
- pixflex-0.1.0/docs/gallery/feature_maps.png +0 -0
- pixflex-0.1.0/docs/gallery/flex_direction.png +0 -0
- pixflex-0.1.0/docs/gallery/flex_wrap.png +0 -0
- pixflex-0.1.0/docs/gallery/gap.png +0 -0
- pixflex-0.1.0/docs/gallery/homepage.png +0 -0
- pixflex-0.1.0/docs/gallery/justified.png +0 -0
- pixflex-0.1.0/docs/gallery/justify_content.png +0 -0
- pixflex-0.1.0/docs/gallery/responsive.png +0 -0
- pixflex-0.1.0/docs/gallery/sizing.png +0 -0
- pixflex-0.1.0/examples/batch_torch.py +25 -0
- pixflex-0.1.0/examples/figure_panel.py +54 -0
- pixflex-0.1.0/examples/gallery/__init__.py +0 -0
- pixflex-0.1.0/examples/gallery/_kit.py +105 -0
- pixflex-0.1.0/examples/gallery/examples.py +428 -0
- pixflex-0.1.0/examples/gallery.py +71 -0
- pixflex-0.1.0/examples/video_overlay.py +124 -0
- pixflex-0.1.0/pyproject.toml +45 -0
- pixflex-0.1.0/src/pixflex/__init__.py +39 -0
- pixflex-0.1.0/src/pixflex/_backend.py +40 -0
- pixflex-0.1.0/src/pixflex/canvas.py +464 -0
- pixflex-0.1.0/src/pixflex/dsl.py +163 -0
- pixflex-0.1.0/src/pixflex/errors.py +37 -0
- pixflex-0.1.0/src/pixflex/fit.py +95 -0
- pixflex-0.1.0/src/pixflex/plan.py +240 -0
- pixflex-0.1.0/src/pixflex/resize.py +111 -0
- pixflex-0.1.0/tests/__init__.py +0 -0
- pixflex-0.1.0/tests/conftest.py +60 -0
- pixflex-0.1.0/tests/test_canvas.py +388 -0
- pixflex-0.1.0/tests/test_gallery.py +20 -0
- pixflex-0.1.0/tests/test_key_typing.py +83 -0
- 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/
|
pixflex-0.1.0/.gitignore
ADDED
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.
|
pixflex-0.1.0/README.md
ADDED
|
@@ -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.
|