pypixelpack 0.2.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 (36) hide show
  1. pypixelpack-0.2.0/.flywheel.yml +7 -0
  2. pypixelpack-0.2.0/.github/workflows/ci.yml +48 -0
  3. pypixelpack-0.2.0/.github/workflows/flywheel-pr.yml +26 -0
  4. pypixelpack-0.2.0/.github/workflows/flywheel-push.yml +22 -0
  5. pypixelpack-0.2.0/.github/workflows/governance-lint.yml +11 -0
  6. pypixelpack-0.2.0/.github/workflows/quality.yml +55 -0
  7. pypixelpack-0.2.0/.gitignore +10 -0
  8. pypixelpack-0.2.0/.markdownlint-cli2.jsonc +21 -0
  9. pypixelpack-0.2.0/.markdownlint.json +1 -0
  10. pypixelpack-0.2.0/.python-version +1 -0
  11. pypixelpack-0.2.0/.vale.ini +8 -0
  12. pypixelpack-0.2.0/CHANGELOG.md +6 -0
  13. pypixelpack-0.2.0/LICENSE +28 -0
  14. pypixelpack-0.2.0/PKG-INFO +138 -0
  15. pypixelpack-0.2.0/README.md +111 -0
  16. pypixelpack-0.2.0/REQUIREMENTS.md +96 -0
  17. pypixelpack-0.2.0/ROADMAP.md +7 -0
  18. pypixelpack-0.2.0/SPEC.md +178 -0
  19. pypixelpack-0.2.0/pypixelpack/__init__.py +29 -0
  20. pypixelpack-0.2.0/pypixelpack/_backend.py +72 -0
  21. pypixelpack-0.2.0/pypixelpack/encoding.py +287 -0
  22. pypixelpack-0.2.0/pypixelpack/layouts.py +333 -0
  23. pypixelpack-0.2.0/pypixelpack/py.typed +0 -0
  24. pypixelpack-0.2.0/pyproject.toml +101 -0
  25. pypixelpack-0.2.0/styles/Requirements/FillerPhrases.yml +12 -0
  26. pypixelpack-0.2.0/styles/Requirements/MustDeprecated.yml +7 -0
  27. pypixelpack-0.2.0/styles/Requirements/OrdinalHeadings.yml +7 -0
  28. pypixelpack-0.2.0/styles/Requirements/WillDeprecated.yml +7 -0
  29. pypixelpack-0.2.0/tests/conftest.py +35 -0
  30. pypixelpack-0.2.0/tests/test_backend.py +43 -0
  31. pypixelpack-0.2.0/tests/test_backend_parity.py +148 -0
  32. pypixelpack-0.2.0/tests/test_encoding.py +287 -0
  33. pypixelpack-0.2.0/tests/test_layouts.py +319 -0
  34. pypixelpack-0.2.0/tests/test_package.py +30 -0
  35. pypixelpack-0.2.0/tools/check_core_install.py +66 -0
  36. pypixelpack-0.2.0/uv.lock +420 -0
@@ -0,0 +1,7 @@
1
+ flywheel:
2
+ streams:
3
+ - name: main-line
4
+ branches:
5
+ - name: main
6
+ release: production
7
+ auto_merge: [fix, chore, docs]
@@ -0,0 +1,48 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ # ruff and pyright are pinned to py312 in pyproject.toml, so one leg
10
+ # carries the full lint signal; only pytest is version-sensitive.
11
+ lint:
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v5
15
+ - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
16
+ with:
17
+ python-version: "3.12"
18
+ - run: uv sync
19
+ - run: uv run ruff format --check .
20
+ - run: uv run ruff check .
21
+ - run: uv run pyright
22
+
23
+ test:
24
+ runs-on: ubuntu-latest
25
+ strategy:
26
+ matrix:
27
+ python-version: ["3.12", "3.13", "3.14"]
28
+ steps:
29
+ - uses: actions/checkout@v5
30
+ - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
31
+ with:
32
+ python-version: ${{ matrix.python-version }}
33
+ - run: uv sync
34
+ - run: uv run pytest
35
+
36
+ # Prove the numpy-only core: install the built wheel into a clean
37
+ # environment and run the core probe (§spec:package-shape).
38
+ core-install:
39
+ runs-on: ubuntu-latest
40
+ steps:
41
+ - uses: actions/checkout@v5
42
+ with:
43
+ fetch-depth: 0 # hatch-vcs derives the version from tag history
44
+ - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
45
+ with:
46
+ python-version: "3.12"
47
+ - run: uv build
48
+ - run: uv run --isolated --no-project --with dist/*.whl python tools/check_core_install.py
@@ -0,0 +1,26 @@
1
+ name: Flywheel — PR
2
+ on:
3
+ pull_request:
4
+ types: [opened, synchronize, reopened, ready_for_review, edited]
5
+ concurrency:
6
+ group: flywheel-pr-${{ github.event.pull_request.number }}
7
+ cancel-in-progress: true
8
+ jobs:
9
+ conduct:
10
+ # Skip drafts; ignore `edited` events from bots (only a human editing the
11
+ # title/body should retrigger the conventional-commit rewrite). Skips
12
+ # cleanly until the flywheel GitHub App is installed on the org and its
13
+ # credentials are configured.
14
+ if: |
15
+ vars.FLYWHEEL_GH_APP_ID != '' &&
16
+ github.event.pull_request.draft == false &&
17
+ (github.event.action != 'edited' || github.event.sender.type == 'User')
18
+ runs-on: ubuntu-latest
19
+ steps:
20
+ # SHA-pinned (no floating tags for third-party actions); the trailing
21
+ # # vX.Y.Z comment is for human review. Mirrors pydecklink's setup.
22
+ - uses: point-source/flywheel@59758d1940ebe054ce9fdd4b7a7dc79a0cf235e0 # v2.1.0
23
+ with:
24
+ event: pull_request
25
+ app-id: ${{ vars.FLYWHEEL_GH_APP_ID }}
26
+ app-private-key: ${{ secrets.FLYWHEEL_GH_APP_PRIVATE_KEY }}
@@ -0,0 +1,22 @@
1
+ name: Flywheel — Push
2
+ on:
3
+ push:
4
+ branches: ["**"]
5
+ concurrency:
6
+ group: flywheel-push-${{ github.ref_name }}
7
+ cancel-in-progress: false
8
+ jobs:
9
+ release:
10
+ # Skips cleanly until the flywheel GitHub App is installed on the org
11
+ # and its credentials are configured (vars.FLYWHEEL_GH_APP_ID,
12
+ # secrets.FLYWHEEL_GH_APP_PRIVATE_KEY).
13
+ if: vars.FLYWHEEL_GH_APP_ID != ''
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ # SHA-pinned (no floating tags for third-party actions); the trailing
17
+ # # vX.Y.Z comment is for human review. Mirrors pydecklink's setup.
18
+ - uses: point-source/flywheel@59758d1940ebe054ce9fdd4b7a7dc79a0cf235e0 # v2.1.0
19
+ with:
20
+ event: push
21
+ app-id: ${{ vars.FLYWHEEL_GH_APP_ID }}
22
+ app-private-key: ${{ secrets.FLYWHEEL_GH_APP_PRIVATE_KEY }}
@@ -0,0 +1,11 @@
1
+ name: Governance Lint
2
+ on:
3
+ push:
4
+ branches: [main]
5
+ pull_request:
6
+
7
+ jobs:
8
+ governance:
9
+ uses: repentsinner/symphonize/.github/workflows/governance-lint.yml@notation--v0
10
+ with:
11
+ readme-type: "library"
@@ -0,0 +1,55 @@
1
+ name: quality
2
+
3
+ # The two checks the `Flywheel - review` ruleset requires by name —
4
+ # `markdownlint` and `shellcheck` — and which nothing in this repository
5
+ # emitted, so every pull request into the default branch sat at "Expected —
6
+ # Waiting for status to be reported" and could not merge. The job keys below
7
+ # ARE the check names the ruleset matches; renaming one silently re-blocks
8
+ # the branch.
9
+ #
10
+ # Neither ran here under those names: markdownlint only as a step inside the
11
+ # reusable governance-lint workflow, and shellcheck not at all — this
12
+ # repository ships no shell scripts, so that job passes on an empty set and
13
+ # exists to satisfy the context. A step's name is not a check's name, which is
14
+ # why the ruleset never saw them.
15
+ on:
16
+ pull_request:
17
+ merge_group:
18
+
19
+ permissions:
20
+ contents: read
21
+
22
+ jobs:
23
+ # Skip on Flywheel's own release and back-merge commits, which carry
24
+ # generated content rather than reviewable changes. The guard is per job so
25
+ # each required check still resolves rather than hanging — a skipped job
26
+ # satisfies a required context, an absent one does not.
27
+ markdownlint:
28
+ if: >-
29
+ !contains(github.event.pull_request.title || '', ': promote ') &&
30
+ !startsWith(github.event.merge_group.head_commit.message || '', 'chore(release):') &&
31
+ !startsWith(github.event.merge_group.head_commit.message || '', 'chore: back-merge')
32
+ runs-on: ubuntu-latest
33
+ steps:
34
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
35
+ # Root governance and landing documents, matching the sibling
36
+ # repositories. Config is .markdownlint.json plus the ignore list in
37
+ # .markdownlint-cli2.jsonc.
38
+ - uses: DavidAnson/markdownlint-cli2-action@21c1be1b93ad9ed58fa840aacc3f279cde2a72ff # v24
39
+ with:
40
+ globs: "*.md"
41
+
42
+ shellcheck:
43
+ if: >-
44
+ !contains(github.event.pull_request.title || '', ': promote ') &&
45
+ !startsWith(github.event.merge_group.head_commit.message || '', 'chore(release):') &&
46
+ !startsWith(github.event.merge_group.head_commit.message || '', 'chore: back-merge')
47
+ runs-on: ubuntu-latest
48
+ steps:
49
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
50
+ # `severity: error` is the enforced baseline, as in the sibling
51
+ # repositories: the floor keeps an info-level finding — SC1091 from a
52
+ # source this action does not follow, say — out of a required gate.
53
+ - uses: ludeeus/action-shellcheck@00cae500b08a931fb5698e11e79bfbd38e612a38 # v2.0.0
54
+ with:
55
+ severity: error
@@ -0,0 +1,10 @@
1
+ # Agent worktrees and session state
2
+ .claude/
3
+
4
+ # Python
5
+ __pycache__/
6
+ *.pyc
7
+ .venv/
8
+ dist/
9
+ .pytest_cache/
10
+ .ruff_cache/
@@ -0,0 +1,21 @@
1
+ {
2
+ // CHANGELOG.md is generated by Flywheel/semantic-release in
3
+ // conventional-changelog format, which opens each release with an h1 and
4
+ // then jumps to h3 for the section headings — MD001, and not ours to fix:
5
+ // the file is rewritten from the commit history on every release, so a
6
+ // correction would be overwritten by the next one and a release would be
7
+ // able to fail the branch's own required checks.
8
+ //
9
+ // The rest are directories a local `markdownlint-cli2` run would otherwise
10
+ // walk: .venv/ is the environment uv creates and .claude/ the worktrees this
11
+ // repository's own workflow makes. CI lints the root *.md alone
12
+ // (.github/workflows/quality.yml), so these matter only to a developer
13
+ // running the tool by hand — which is exactly when an ignore list earns its
14
+ // place.
15
+ "ignores": [
16
+ "CHANGELOG.md",
17
+ "node_modules/**",
18
+ ".venv/**",
19
+ ".claude/**"
20
+ ]
21
+ }
@@ -0,0 +1 @@
1
+ {"MD013": false, "MD024": false, "MD036": false}
@@ -0,0 +1 @@
1
+ 3.12
@@ -0,0 +1,8 @@
1
+ StylesPath = styles
2
+ MinAlertLevel = warning
3
+
4
+ [SPEC.md]
5
+ BasedOnStyles = Requirements
6
+
7
+ [REQUIREMENTS.md]
8
+ BasedOnStyles = Requirements
@@ -0,0 +1,6 @@
1
+ # [0.2.0](https://github.com/Fuse-Technical-Group/pypixelpack/compare/v0.1.0...v0.2.0) (2026-08-30)
2
+
3
+
4
+ ### Features
5
+
6
+ * **encoding:** expose the achromatic chroma code ([b39317c](https://github.com/Fuse-Technical-Group/pypixelpack/commit/b39317cad4034e78ae7bda17905e10ed27f6b15f))
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026 Fuse Technical Group
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,138 @@
1
+ Metadata-Version: 2.5
2
+ Name: pypixelpack
3
+ Version: 0.2.0
4
+ Summary: Pixel layouts and wire encodings for video I/O, device-free
5
+ Project-URL: Homepage, https://github.com/Fuse-Technical-Group/pypixelpack
6
+ Project-URL: Repository, https://github.com/Fuse-Technical-Group/pypixelpack.git
7
+ Project-URL: Issues, https://github.com/Fuse-Technical-Group/pypixelpack/issues
8
+ Project-URL: Changelog, https://github.com/Fuse-Technical-Group/pypixelpack/blob/main/CHANGELOG.md
9
+ Author-email: Ritchie Argue <4462072+repentsinner@users.noreply.github.com>
10
+ License-Expression: BSD-3-Clause
11
+ License-File: LICENSE
12
+ Keywords: decklink,pixel-format,r210,sdi,st2110,v210,ycbcr
13
+ Classifier: Development Status :: 2 - Pre-Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: MacOS
16
+ Classifier: Operating System :: Microsoft :: Windows
17
+ Classifier: Operating System :: POSIX :: Linux
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
22
+ Classifier: Topic :: Multimedia :: Video
23
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
+ Requires-Python: >=3.12
25
+ Requires-Dist: numpy>=2.3.1
26
+ Description-Content-Type: text/markdown
27
+
28
+ # pypixelpack
29
+
30
+ **Pixel layouts and wire encodings for video I/O, device-free.**
31
+
32
+ The bytes a video frame becomes on a wire — v210, r210, the ST 2110-20
33
+ pgroup — and the encoding between a frame's RGB and those bytes: colour
34
+ matrix, range, chroma subsampling. Written once against a caller-supplied
35
+ array namespace, so the same source packs on numpy for a host frame and
36
+ on torch for a frame that never leaves a GPU.
37
+
38
+ Extracted from [pydecklink](https://github.com/Fuse-Technical-Group/pydecklink)'s
39
+ packing module and a GPU render pipeline's colorspace node, which held
40
+ the same layout twice. This repository's [SPEC.md](SPEC.md) and
41
+ [ROADMAP.md](ROADMAP.md) govern the package.
42
+
43
+ ## Installation
44
+
45
+ ```sh
46
+ uv add pypixelpack
47
+ ```
48
+
49
+ numpy is the only dependency. torch is a namespace the caller supplies,
50
+ never a dependency of this package (`§spec:backend`).
51
+
52
+ ## Usage
53
+
54
+ ```python
55
+ import numpy as np
56
+ from pypixelpack import decode, encode, pack, unpack
57
+
58
+ rgb = np.zeros((1080, 1920, 3), dtype=np.float32) # [R, G, B] in [0, 1]
59
+ codes = encode(rgb, subsampling="422") # (H, W, 3) uint16 [Y, Cb, Cr]
60
+ data = pack(codes, "v210", row_bytes=5120) # 1-D uint8, DMA-ready
61
+ back = unpack(data, "v210", width=1920, height=1080, row_bytes=5120)
62
+ rgb_again = decode(back, subsampling="422") # float32 [R, G, B] in [0, 1]
63
+ ```
64
+
65
+ On a GPU host, pass the namespace and the arrays stay resident:
66
+
67
+ ```python
68
+ import torch
69
+
70
+ codes = encode(rgb_on_cuda, subsampling="422", xp=torch)
71
+ data = pack(codes, "v210", row_bytes=5120, xp=torch)
72
+ ```
73
+
74
+ ## API
75
+
76
+ - `pack(pixels, layout, row_bytes, *, xp=numpy)` — `(H, W, 3)` integer
77
+ samples to a 1-D `uint8` buffer of `H × row_bytes` in `layout`, on the
78
+ input's device (`§spec:layouts`). RGB layouts take `[R, G, B]`; `v210`
79
+ and `2vuy` take `[Y, Cb, Cr]` with chroma read from even columns.
80
+ - `unpack(data, layout, width, height, row_bytes, *, xp=numpy)` — the
81
+ inverse; `unpack(pack(x)) == x` for every layout. Returns `uint8` for
82
+ 8-bit layouts and `uint16` otherwise.
83
+ - `row_bytes(layout, width)` — the smallest `row_bytes` that holds a line.
84
+ - `LAYOUTS` — the layout table, `name → (pixels per group, bytes per
85
+ group, bit depth)`: `argb`, `bgra`, `r210`, `r10b`, `r10l`, `v210`,
86
+ `2vuy`, `r12b`, `r12l`.
87
+
88
+ `pack` raises `ValueError` for an unknown layout, a `row_bytes` shorter
89
+ than the packed line, or a sample above the layout's bit depth; the
90
+ last check is skipped under `torch.compile`, where a compiled caller
91
+ trusts its own inputs (`§spec:backend`).
92
+
93
+ ### Encoding
94
+
95
+ - `encode(rgb, *, matrix="bt709", levels="narrow", layout=None,
96
+ bits=None, subsampling=None, xp=numpy)` — `(H, W, 3)` float RGB in
97
+ `[0, 1]` to `(H, W, 3)` `uint16` `[Y, Cb, Cr]` (`§spec:encoding`).
98
+ `matrix` is `bt709` or `bt2020`; `levels` is `narrow` (luma 16–235,
99
+ chroma 16–240 at 8 bits, shifted up by `bits - 8`) or `full`.
100
+ `layout="v210"` selects the depth and subsampling that wire format
101
+ expects — 10-bit 4:2:2; `2vuy` is 8-bit 4:2:2 — and a `bits` or
102
+ `subsampling` that contradicts it is refused; without a layout they
103
+ default to 10 and `444`. `422` averages chroma over each horizontal
104
+ pair and writes it to both pixels, the shape `pack` reads. Rounding is
105
+ half to even, arithmetic is float32 on every backend, and codes clamp
106
+ to the level range's span.
107
+ - `decode(ycbcr, *, matrix, levels, layout=None, bits=None, xp=numpy)`
108
+ — the inverse, per pixel, to within half a code per component;
109
+ returns `float32` RGB clamped to `[0, 1]`.
110
+ - `encoding_for(layout)` — the `(bits, subsampling)` a layout carries;
111
+ `ENCODINGS` is the table behind it.
112
+ - `legal_codes(*, levels="narrow", bits=10)` — the `(luma, chroma)` code
113
+ spans as `range` objects; `len(legal_codes().luma)` is 877, the levels
114
+ 10-bit narrow range can represent.
115
+ - `MATRICES` — `name → (KR, KB)`: `bt709`, `bt2020`.
116
+
117
+ `encode` and `decode` raise `ValueError` for an unknown matrix, levels or
118
+ subsampling, a `bits` outside 8–16, or a layout with no component
119
+ encoding.
120
+
121
+ ## Development
122
+
123
+ ```sh
124
+ uv sync
125
+ uv run ruff format --check . && uv run ruff check . && uv run pyright && uv run pytest
126
+ ```
127
+
128
+ `uv sync` installs torch (CPU) into the dev group so the parity suite
129
+ runs; without it those tests skip. To prove the wheel on numpy alone:
130
+
131
+ ```sh
132
+ uv build
133
+ uv run --isolated --no-project --with dist/*.whl python tools/check_core_install.py
134
+ ```
135
+
136
+ ## License
137
+
138
+ BSD-3-Clause. See [LICENSE](LICENSE).
@@ -0,0 +1,111 @@
1
+ # pypixelpack
2
+
3
+ **Pixel layouts and wire encodings for video I/O, device-free.**
4
+
5
+ The bytes a video frame becomes on a wire — v210, r210, the ST 2110-20
6
+ pgroup — and the encoding between a frame's RGB and those bytes: colour
7
+ matrix, range, chroma subsampling. Written once against a caller-supplied
8
+ array namespace, so the same source packs on numpy for a host frame and
9
+ on torch for a frame that never leaves a GPU.
10
+
11
+ Extracted from [pydecklink](https://github.com/Fuse-Technical-Group/pydecklink)'s
12
+ packing module and a GPU render pipeline's colorspace node, which held
13
+ the same layout twice. This repository's [SPEC.md](SPEC.md) and
14
+ [ROADMAP.md](ROADMAP.md) govern the package.
15
+
16
+ ## Installation
17
+
18
+ ```sh
19
+ uv add pypixelpack
20
+ ```
21
+
22
+ numpy is the only dependency. torch is a namespace the caller supplies,
23
+ never a dependency of this package (`§spec:backend`).
24
+
25
+ ## Usage
26
+
27
+ ```python
28
+ import numpy as np
29
+ from pypixelpack import decode, encode, pack, unpack
30
+
31
+ rgb = np.zeros((1080, 1920, 3), dtype=np.float32) # [R, G, B] in [0, 1]
32
+ codes = encode(rgb, subsampling="422") # (H, W, 3) uint16 [Y, Cb, Cr]
33
+ data = pack(codes, "v210", row_bytes=5120) # 1-D uint8, DMA-ready
34
+ back = unpack(data, "v210", width=1920, height=1080, row_bytes=5120)
35
+ rgb_again = decode(back, subsampling="422") # float32 [R, G, B] in [0, 1]
36
+ ```
37
+
38
+ On a GPU host, pass the namespace and the arrays stay resident:
39
+
40
+ ```python
41
+ import torch
42
+
43
+ codes = encode(rgb_on_cuda, subsampling="422", xp=torch)
44
+ data = pack(codes, "v210", row_bytes=5120, xp=torch)
45
+ ```
46
+
47
+ ## API
48
+
49
+ - `pack(pixels, layout, row_bytes, *, xp=numpy)` — `(H, W, 3)` integer
50
+ samples to a 1-D `uint8` buffer of `H × row_bytes` in `layout`, on the
51
+ input's device (`§spec:layouts`). RGB layouts take `[R, G, B]`; `v210`
52
+ and `2vuy` take `[Y, Cb, Cr]` with chroma read from even columns.
53
+ - `unpack(data, layout, width, height, row_bytes, *, xp=numpy)` — the
54
+ inverse; `unpack(pack(x)) == x` for every layout. Returns `uint8` for
55
+ 8-bit layouts and `uint16` otherwise.
56
+ - `row_bytes(layout, width)` — the smallest `row_bytes` that holds a line.
57
+ - `LAYOUTS` — the layout table, `name → (pixels per group, bytes per
58
+ group, bit depth)`: `argb`, `bgra`, `r210`, `r10b`, `r10l`, `v210`,
59
+ `2vuy`, `r12b`, `r12l`.
60
+
61
+ `pack` raises `ValueError` for an unknown layout, a `row_bytes` shorter
62
+ than the packed line, or a sample above the layout's bit depth; the
63
+ last check is skipped under `torch.compile`, where a compiled caller
64
+ trusts its own inputs (`§spec:backend`).
65
+
66
+ ### Encoding
67
+
68
+ - `encode(rgb, *, matrix="bt709", levels="narrow", layout=None,
69
+ bits=None, subsampling=None, xp=numpy)` — `(H, W, 3)` float RGB in
70
+ `[0, 1]` to `(H, W, 3)` `uint16` `[Y, Cb, Cr]` (`§spec:encoding`).
71
+ `matrix` is `bt709` or `bt2020`; `levels` is `narrow` (luma 16–235,
72
+ chroma 16–240 at 8 bits, shifted up by `bits - 8`) or `full`.
73
+ `layout="v210"` selects the depth and subsampling that wire format
74
+ expects — 10-bit 4:2:2; `2vuy` is 8-bit 4:2:2 — and a `bits` or
75
+ `subsampling` that contradicts it is refused; without a layout they
76
+ default to 10 and `444`. `422` averages chroma over each horizontal
77
+ pair and writes it to both pixels, the shape `pack` reads. Rounding is
78
+ half to even, arithmetic is float32 on every backend, and codes clamp
79
+ to the level range's span.
80
+ - `decode(ycbcr, *, matrix, levels, layout=None, bits=None, xp=numpy)`
81
+ — the inverse, per pixel, to within half a code per component;
82
+ returns `float32` RGB clamped to `[0, 1]`.
83
+ - `encoding_for(layout)` — the `(bits, subsampling)` a layout carries;
84
+ `ENCODINGS` is the table behind it.
85
+ - `legal_codes(*, levels="narrow", bits=10)` — the `(luma, chroma)` code
86
+ spans as `range` objects; `len(legal_codes().luma)` is 877, the levels
87
+ 10-bit narrow range can represent.
88
+ - `MATRICES` — `name → (KR, KB)`: `bt709`, `bt2020`.
89
+
90
+ `encode` and `decode` raise `ValueError` for an unknown matrix, levels or
91
+ subsampling, a `bits` outside 8–16, or a layout with no component
92
+ encoding.
93
+
94
+ ## Development
95
+
96
+ ```sh
97
+ uv sync
98
+ uv run ruff format --check . && uv run ruff check . && uv run pyright && uv run pytest
99
+ ```
100
+
101
+ `uv sync` installs torch (CPU) into the dev group so the parity suite
102
+ runs; without it those tests skip. To prove the wheel on numpy alone:
103
+
104
+ ```sh
105
+ uv build
106
+ uv run --isolated --no-project --with dist/*.whl python tools/check_core_install.py
107
+ ```
108
+
109
+ ## License
110
+
111
+ BSD-3-Clause. See [LICENSE](LICENSE).
@@ -0,0 +1,96 @@
1
+ # Requirements
2
+
3
+ Problem-space document for pypixelpack: pixel layouts and wire encodings
4
+ as a device-free library. Extracted from
5
+ [pydecklink](https://github.com/Fuse-Technical-Group/pydecklink)
6
+ (`§spec:pixel-packing` there) and a GPU render pipeline that outputs to
7
+ SDI and ST 2110.
8
+
9
+ ## Problem statement §req:problem-statement
10
+
11
+ Target users are the maintainers of video I/O tools in this organization
12
+ and the display-measurement tools that drive them: a DeckLink binding, a
13
+ GPU render pipeline that outputs to SDI and ST 2110, a signal generator,
14
+ and a measurement session that puts exact code values on a wire.
15
+
16
+ The same v210 layout exists twice. pydecklink packs it in numpy on the
17
+ host for a DMA buffer; a GPU render pipeline packs it in torch on the
18
+ device and DMAs the result, because a round trip through host memory costs a full
19
+ uncompressed frame across PCIe every frame. Each is correct for its
20
+ caller and neither serves the other, so the next wire format — r210 for
21
+ RGB 4:4:4 over SDI — recurs the same way.
22
+
23
+ The colour encoding between RGB and those samples lives in one of the two
24
+ and is hard-bound to one matrix. A measurement session on SDI has to
25
+ declare that encoding and cannot implement it without a second copy,
26
+ and a wrong matrix or range offsets every code in a way nothing
27
+ downstream can detect.
28
+
29
+ ## Success criteria §req:success-criteria
30
+
31
+ - pydecklink consumes this library for its layouts, keeps only the map
32
+ from its SDK enum to a layout name, and `pack`/`unpack` output is
33
+ byte-identical before and after.
34
+ - The GPU render pipeline consumes this library on torch, on the
35
+ device, with no host round trip, and a test proves the compiled path is taken.
36
+ - The same source packs a frame on numpy and on torch to identical
37
+ bytes.
38
+ - Every layout round-trips: `unpack(pack(x)) == x`.
39
+ - display-measure declares an SDI encoding by name and matrix and never
40
+ carries a conversion of its own.
41
+
42
+ ## User stories §req:user-stories
43
+
44
+ - As the pydecklink maintainer, I delete the layout implementations and
45
+ keep the enum map, so the transport binding holds nothing but the
46
+ vendor's surface.
47
+ - As a render-pipeline developer, I pack v210 and r210 on the GPU and
48
+ DMA the bytes, so the frame never crosses the bus uncompressed.
49
+ - As a measurement-session author, I declare "10-bit BT.709 narrow-range
50
+ 4:2:2 in v210" and drive RGB patches through it, so the encoding on
51
+ the wire is the one the artifact records.
52
+ - As a UHD integrator, I select BT.2020, so the encoding follows the
53
+ signal rather than a constant.
54
+
55
+ ## Quality attributes §req:quality-attributes
56
+
57
+ - **Exactness.** Layouts are bit-exact against the format's own
58
+ reference tables; encodings state their rounding and honor it.
59
+ - **Determinism.** Same inputs, same bytes, on every backend.
60
+ - **Footprint.** numpy is the only dependency; torch is supplied by the
61
+ caller.
62
+ - **Device residency.** No function forces a transfer to host memory.
63
+ - **Fusion.** The torch path stays traceable by `torch.compile`; a
64
+ change that silently drops it to eager fails a test.
65
+
66
+ ## Constraints §req:constraints
67
+
68
+ - Public repository under Fuse-Technical-Group; BSD-3-Clause;
69
+ published to PyPI as `pypixelpack`.
70
+ - Seeded by extraction: pydecklink's layouts and the GPU pipeline's
71
+ encoding move verbatim before any reshaping; adoption is behind one
72
+ release each.
73
+ - No transport and no device: what a byte buffer is *for* — a DeckLink
74
+ frame, an RTP payload — belongs to the consumer.
75
+ - No resizing, no colour management beyond the stated matrix and range:
76
+ the library encodes what it is given.
77
+ - Renders into a caller-supplied array namespace on the same contract
78
+ display-patterns established.
79
+
80
+ ## Priorities §req:priorities
81
+
82
+ Essential, in adoption order:
83
+
84
+ 1. The eight layouts pydecklink holds, namespace-generic, with
85
+ reference vectors and numpy/torch byte-identity.
86
+ 2. pydecklink adoption; first release.
87
+ 3. BT.709 and BT.2020 encoding at narrow and full range, chroma
88
+ subsampling by pair average.
89
+ 4. GPU render pipeline adoption on the device, with fusion asserted.
90
+
91
+ Nice-to-have, after adoption:
92
+
93
+ - `2vuy`, which no current implementation packs.
94
+ - Provenance for `r10b`, `r10l`, `r12b`, `r12l` against FFmpeg's
95
+ format list.
96
+ - The ST 2110-20 pgroup, if a second consumer appears.
@@ -0,0 +1,7 @@
1
+ # Roadmap
2
+
3
+ Work remaining to reach [SPEC.md](SPEC.md). Sections in build order;
4
+ completed work leaves from the head. A slug in backticks resolves in the
5
+ named repository.
6
+
7
+ No work remains; every section here has shipped.