leanjpeg 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.
- leanjpeg-0.1.0/CHANGELOG.md +70 -0
- leanjpeg-0.1.0/LICENSE +33 -0
- leanjpeg-0.1.0/MANIFEST.in +14 -0
- leanjpeg-0.1.0/PKG-INFO +549 -0
- leanjpeg-0.1.0/README.md +494 -0
- leanjpeg-0.1.0/pyproject.toml +82 -0
- leanjpeg-0.1.0/setup.cfg +4 -0
- leanjpeg-0.1.0/src/leanjpeg/__init__.py +188 -0
- leanjpeg-0.1.0/src/leanjpeg/__pyinstaller/__init__.py +13 -0
- leanjpeg-0.1.0/src/leanjpeg/__pyinstaller/hook-leanjpeg.py +29 -0
- leanjpeg-0.1.0/src/leanjpeg/py.typed +0 -0
- leanjpeg-0.1.0/src/leanjpeg/simple/__init__.py +16 -0
- leanjpeg-0.1.0/src/leanjpeg/xl/__init__.py +16 -0
- leanjpeg-0.1.0/src/leanjpeg.egg-info/PKG-INFO +549 -0
- leanjpeg-0.1.0/src/leanjpeg.egg-info/SOURCES.txt +28 -0
- leanjpeg-0.1.0/src/leanjpeg.egg-info/dependency_links.txt +1 -0
- leanjpeg-0.1.0/src/leanjpeg.egg-info/entry_points.txt +2 -0
- leanjpeg-0.1.0/src/leanjpeg.egg-info/requires.txt +30 -0
- leanjpeg-0.1.0/src/leanjpeg.egg-info/top_level.txt +1 -0
- leanjpeg-0.1.0/tests/conftest.py +92 -0
- leanjpeg-0.1.0/tests/test_ffmpeg_workflow.py +104 -0
- leanjpeg-0.1.0/tests/test_meta.py +150 -0
- leanjpeg-0.1.0/tests/test_packaging.py +202 -0
- leanjpeg-0.1.0/tests/test_parity.py +139 -0
- leanjpeg-0.1.0/tests/test_pyinstaller.py +119 -0
- leanjpeg-0.1.0/tests/test_threading.py +141 -0
- leanjpeg-0.1.0/tools/build_dists.py +133 -0
- leanjpeg-0.1.0/tools/bump_version.py +123 -0
- leanjpeg-0.1.0/tools/ffmpeg_frames.py +162 -0
- leanjpeg-0.1.0/tools/upstream_sync.py +220 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to the three `leanjpeg` distributions. They share one
|
|
4
|
+
version number: `leanjpeg` pins its backends exactly, and
|
|
5
|
+
`tools/bump_version.py` keeps every location in step.
|
|
6
|
+
|
|
7
|
+
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
|
|
8
|
+
and the project uses [semantic versioning](https://semver.org/spec/v2.0.0.html).
|
|
9
|
+
|
|
10
|
+
## [Unreleased]
|
|
11
|
+
|
|
12
|
+
## [0.1.0] - 2026-09-07
|
|
13
|
+
|
|
14
|
+
First release. Three distributions:
|
|
15
|
+
|
|
16
|
+
### `leanjpeg`
|
|
17
|
+
|
|
18
|
+
* Pure-Python dispatcher: `sniff`, `is_jpeg`, `is_jxl`, and format-agnostic
|
|
19
|
+
`decode` / `decode_header` / `encode` that route to whichever backend is
|
|
20
|
+
installed, with `BackendNotInstalled` carrying the install command.
|
|
21
|
+
* Backends are optional dependencies, so a deployment pulls in only what it
|
|
22
|
+
uses: `leanjpeg[simple]`, `leanjpeg[xl]`, `leanjpeg[all]`.
|
|
23
|
+
|
|
24
|
+
### `leanjpeg-simple` (fast path)
|
|
25
|
+
|
|
26
|
+
* Fork of [simplejpeg](https://github.com/jfolz/simplejpeg) 1.9.0 over
|
|
27
|
+
libjpeg-turbo 3.2.0, statically linked, with an unchanged API.
|
|
28
|
+
* **Free-threaded CPython support** (3.13t / 3.14t): the extension declares
|
|
29
|
+
`Py_MOD_GIL_NOT_USED`, has no unsynchronised global state and releases the
|
|
30
|
+
GIL around all codec work. Upstream simplejpeg re-enables the GIL on import.
|
|
31
|
+
* **Reduced allocations for repeated calls**: pooled TurboJPEG handles and a
|
|
32
|
+
high-water-mark output buffer, so steady-state calls allocate only the object
|
|
33
|
+
they return. 6-30 % faster encode than upstream in an interleaved
|
|
34
|
+
single-process A/B, the margin growing as images get smaller.
|
|
35
|
+
* Added: `handle_pool_stats()`, `get_handle_pool_size()`,
|
|
36
|
+
`set_handle_pool_size()`, `clear_handle_pool()`, `libjpeg_turbo_version()`.
|
|
37
|
+
* Two fixes on top of the unchanged upstream API (see `UPSTREAM.md`):
|
|
38
|
+
`decode_jpeg_header` names the subsamplings libjpeg-turbo 3.x added after
|
|
39
|
+
4:1:1 instead of raising `KeyError`, and empty inputs raise a `ValueError`
|
|
40
|
+
that names the problem instead of being indexed out of range.
|
|
41
|
+
|
|
42
|
+
### `leanjpeg-xl` (offline path)
|
|
43
|
+
|
|
44
|
+
* Light binding over [libjxl](https://github.com/libjxl/libjxl) 0.12.0,
|
|
45
|
+
statically linked, mirroring the simplejpeg API: `encode_jxl`, `decode_jxl`,
|
|
46
|
+
`decode_jxl_header`, `is_jxl`, including colorspace names and `buffer=`.
|
|
47
|
+
* Lossy (`quality` / `distance` / `effort`) and mathematically lossless modes;
|
|
48
|
+
`uint8`, `uint16` and `float32` samples; alpha.
|
|
49
|
+
* **Bit-exact lossless JPEG recompression**: `recompress_jpeg()` /
|
|
50
|
+
`reconstruct_jpeg()` store the `jbrd` reconstruction box, about 20 % smaller
|
|
51
|
+
and byte-for-byte reversible.
|
|
52
|
+
* Explicit threading: `num_threads=None` (libjxl's own suggestion, roughly one
|
|
53
|
+
thread per 256x256 group), `1`, or a fixed count, plus `set_max_threads()`.
|
|
54
|
+
* Pooled codecs, thread pools and buffers; GIL released around all libjxl work;
|
|
55
|
+
free-threading compatible.
|
|
56
|
+
|
|
57
|
+
### Packaging
|
|
58
|
+
|
|
59
|
+
* **PyInstaller support out of the box**: each distribution ships a hook and
|
|
60
|
+
registers it through the `pyinstaller40` entry point, so `pyinstaller app.py`
|
|
61
|
+
bundles a working application with no flags. Without them a frozen app finds
|
|
62
|
+
no backends at all -- they are resolved through `importlib` -- and the
|
|
63
|
+
compiled extensions' NumPy import is invisible to static analysis.
|
|
64
|
+
* Wheels for CPython 3.13, 3.13t, 3.14 and 3.14t on manylinux and musllinux
|
|
65
|
+
(x86_64, aarch64), macOS (x86_64, arm64) and Windows (AMD64, ARM64).
|
|
66
|
+
* Source distributions carry the vendored libjpeg-turbo and libjxl sources, so
|
|
67
|
+
they build without network access or a git checkout.
|
|
68
|
+
|
|
69
|
+
[Unreleased]: https://github.com/vxlk/leanjpeg/compare/v0.1.0...HEAD
|
|
70
|
+
[0.1.0]: https://github.com/vxlk/leanjpeg/releases/tag/v0.1.0
|
leanjpeg-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 leanjpeg contributors
|
|
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.
|
|
22
|
+
|
|
23
|
+
--------------------------------------------------------------------------------
|
|
24
|
+
Third-party components
|
|
25
|
+
--------------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
leanjpeg-simple is a fork of simplejpeg (MIT License, Copyright (c) 2019
|
|
28
|
+
Joachim Folz) and statically links libjpeg-turbo (IJG License, Modified BSD
|
|
29
|
+
License, and zlib License). See packages/leanjpeg-simple/LICENSE.
|
|
30
|
+
|
|
31
|
+
leanjpeg-xl statically links libjxl (BSD 3-Clause License, Copyright (c) the
|
|
32
|
+
JPEG XL Project Authors), highway (Apache License 2.0), brotli (MIT License)
|
|
33
|
+
and skcms (BSD 3-Clause License). See packages/leanjpeg-xl/LICENSE.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# The root distribution is the pure-Python dispatcher. Its tests exercise both
|
|
2
|
+
# backends, so they are shipped together with the small helpers they import;
|
|
3
|
+
# everything that is repository-only (benchmarks, generated charts, vendored
|
|
4
|
+
# submodules, the two backend packages) stays out of the sdist.
|
|
5
|
+
include LICENSE README.md CHANGELOG.md
|
|
6
|
+
recursive-include src/leanjpeg *.py py.typed
|
|
7
|
+
recursive-include tests *.py
|
|
8
|
+
recursive-include tools *.py
|
|
9
|
+
prune docs
|
|
10
|
+
prune bench
|
|
11
|
+
prune packages
|
|
12
|
+
prune third_party
|
|
13
|
+
prune .github
|
|
14
|
+
global-exclude __pycache__ *.py[cod]
|
leanjpeg-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: leanjpeg
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Lean, allocation-conscious JPEG and JPEG XL encoding/decoding for NumPy: a simplejpeg fork (fast path) and a libjxl binding (offline path), free-threading ready.
|
|
5
|
+
Author: leanjpeg contributors
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/vxlk/leanjpeg
|
|
8
|
+
Project-URL: Source, https://github.com/vxlk/leanjpeg
|
|
9
|
+
Project-URL: Issues, https://github.com/vxlk/leanjpeg/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/vxlk/leanjpeg/blob/main/CHANGELOG.md
|
|
11
|
+
Keywords: jpeg,jpeg xl,jxl,libjpeg-turbo,libjxl,numpy,free-threading
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
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 :: Only
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
22
|
+
Classifier: Programming Language :: Python :: Free Threading :: 3 - Stable
|
|
23
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
24
|
+
Classifier: Topic :: Multimedia :: Graphics
|
|
25
|
+
Classifier: Topic :: Multimedia :: Graphics :: Graphics Conversion
|
|
26
|
+
Classifier: Typing :: Typed
|
|
27
|
+
Requires-Python: >=3.13
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
License-File: LICENSE
|
|
30
|
+
Provides-Extra: simple
|
|
31
|
+
Requires-Dist: leanjpeg-simple==0.1.0; extra == "simple"
|
|
32
|
+
Provides-Extra: xl
|
|
33
|
+
Requires-Dist: leanjpeg-xl==0.1.0; extra == "xl"
|
|
34
|
+
Provides-Extra: all
|
|
35
|
+
Requires-Dist: leanjpeg-simple==0.1.0; extra == "all"
|
|
36
|
+
Requires-Dist: leanjpeg-xl==0.1.0; extra == "all"
|
|
37
|
+
Provides-Extra: test
|
|
38
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
39
|
+
Requires-Dist: numpy>=2.1; extra == "test"
|
|
40
|
+
Requires-Dist: pillow>=11; extra == "test"
|
|
41
|
+
Provides-Extra: bench
|
|
42
|
+
Requires-Dist: numpy>=2.1; extra == "bench"
|
|
43
|
+
Requires-Dist: pillow>=11; extra == "bench"
|
|
44
|
+
Requires-Dist: matplotlib>=3.9; extra == "bench"
|
|
45
|
+
Provides-Extra: dev
|
|
46
|
+
Requires-Dist: leanjpeg[bench,test]; extra == "dev"
|
|
47
|
+
Requires-Dist: cython>=3.1; extra == "dev"
|
|
48
|
+
Requires-Dist: setuptools>=77; extra == "dev"
|
|
49
|
+
Requires-Dist: wheel; extra == "dev"
|
|
50
|
+
Requires-Dist: build; extra == "dev"
|
|
51
|
+
Requires-Dist: cibuildwheel>=3.2; extra == "dev"
|
|
52
|
+
Requires-Dist: twine>=6; extra == "dev"
|
|
53
|
+
Requires-Dist: validate-pyproject[all]; extra == "dev"
|
|
54
|
+
Dynamic: license-file
|
|
55
|
+
|
|
56
|
+
# leanjpeg
|
|
57
|
+
|
|
58
|
+
JPEG and JPEG XL for NumPy arrays, in two independently installable backends:
|
|
59
|
+
a **fast path** for real-time work and an **offline path** for archival.
|
|
60
|
+
|
|
61
|
+
| backend | codec | distribution | built for |
|
|
62
|
+
|---|---|---|---|
|
|
63
|
+
| `leanjpeg.simple` | libjpeg-turbo 3.2.0 | `leanjpeg-simple` | decode/encode in the hot loop: video frames, dataloaders, servers |
|
|
64
|
+
| `leanjpeg.xl` | libjxl 0.12.0 | `leanjpeg-xl` | smaller files offline: lossy at higher quality-per-byte, lossless, and **bit-exact JPEG recompression** |
|
|
65
|
+
|
|
66
|
+
`leanjpeg.simple` is a fork of [simplejpeg](https://github.com/jfolz/simplejpeg)
|
|
67
|
+
with two changes and nothing else: it supports **free-threaded CPython**
|
|
68
|
+
(3.13t / 3.14t), and it **stops allocating** in steady state by pooling codec
|
|
69
|
+
handles and output buffers. `leanjpeg.xl` is a new binding that mirrors the
|
|
70
|
+
same API over libjxl. Both statically link their codec, release the GIL around
|
|
71
|
+
all codec work, and are free-threading safe.
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
import leanjpeg
|
|
75
|
+
|
|
76
|
+
leanjpeg.available_backends() # ['simple', 'xl']
|
|
77
|
+
img = leanjpeg.decode(data) # sniffs JPEG vs JPEG XL, dispatches
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Install
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
pip install "leanjpeg[simple]" # fast path only
|
|
86
|
+
pip install "leanjpeg[xl]" # offline path only
|
|
87
|
+
pip install "leanjpeg[all]" # both
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`leanjpeg` itself is pure Python (backend discovery, format sniffing, a small
|
|
91
|
+
dispatcher); the extras pull in the compiled distributions. Each backend also
|
|
92
|
+
installs on its own as `leanjpeg-simple` / `leanjpeg-xl` and imports as
|
|
93
|
+
`leanjpeg_simple` / `leanjpeg_xl` without the umbrella package.
|
|
94
|
+
|
|
95
|
+
Wheels are built for **CPython 3.13+**, free-threaded builds included, on
|
|
96
|
+
manylinux and musllinux (x86_64, aarch64), macOS (x86_64, arm64) and Windows
|
|
97
|
+
(AMD64, ARM64) - `cp313`, `cp313t`, `cp314` and `cp314t` for each. Nothing is
|
|
98
|
+
dynamically linked beyond libc, so there is no codec to install alongside.
|
|
99
|
+
Building from source needs CMake >= 3.16, a C/C++17 compiler, and NASM for
|
|
100
|
+
libjpeg-turbo's x86 SIMD kernels; the codecs are git submodules, compiled and
|
|
101
|
+
linked statically. See
|
|
102
|
+
[docs/PACKAGING.md](https://github.com/vxlk/leanjpeg/blob/main/docs/PACKAGING.md)
|
|
103
|
+
for the full matrix and for building or releasing it yourself.
|
|
104
|
+
|
|
105
|
+
A missing backend fails with an actionable error rather than an ImportError
|
|
106
|
+
traceback:
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
>>> leanjpeg.xl.encode_jxl(img)
|
|
110
|
+
leanjpeg.BackendNotInstalled: leanjpeg.xl is not installed.
|
|
111
|
+
Install it with: pip install "leanjpeg[xl]" (distribution: leanjpeg-xl)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Quick start
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
import numpy as np
|
|
118
|
+
from leanjpeg import simple as sj, xl
|
|
119
|
+
|
|
120
|
+
img = np.zeros((1080, 1920, 3), np.uint8)
|
|
121
|
+
|
|
122
|
+
# --- fast path -----------------------------------------------------------
|
|
123
|
+
jpg = sj.encode_jpeg(img, quality=85, colorsubsampling='420')
|
|
124
|
+
out = sj.decode_jpeg(jpg, colorspace='RGB')
|
|
125
|
+
h, w, colorspace, subsampling = sj.decode_jpeg_header(jpg) # ~3 us
|
|
126
|
+
sj.decode_jpeg(jpg, buffer=out) # decode into your array
|
|
127
|
+
|
|
128
|
+
# --- offline path --------------------------------------------------------
|
|
129
|
+
jxl_lossy = xl.encode_jxl(img, quality=90) # -> distance 1.0
|
|
130
|
+
jxl_lossy = xl.encode_jxl(img, distance=1.5, effort=7)
|
|
131
|
+
jxl_exact = xl.encode_jxl(img, lossless=True)
|
|
132
|
+
out = xl.decode_jxl(jxl_lossy, colorspace='RGB', num_threads=4)
|
|
133
|
+
hdr = xl.decode_jxl_header(jxl_lossy) # height, width, colorspace, ...
|
|
134
|
+
|
|
135
|
+
# --- recompress an existing JPEG, reversibly -----------------------------
|
|
136
|
+
smaller = xl.recompress_jpeg(jpg) # ~20-30 % smaller
|
|
137
|
+
assert xl.reconstruct_jpeg(smaller) == jpg # byte for byte
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
The format-agnostic helpers on the root package sniff the signature and
|
|
141
|
+
dispatch: `leanjpeg.decode`, `leanjpeg.decode_header`, `leanjpeg.encode(img,
|
|
142
|
+
format='jxl')`, `leanjpeg.is_jpeg`, `leanjpeg.is_jxl`, `leanjpeg.sniff`.
|
|
143
|
+
|
|
144
|
+
## API
|
|
145
|
+
|
|
146
|
+
`leanjpeg.simple` is API-identical to simplejpeg 1.9.0 — `decode_jpeg`,
|
|
147
|
+
`decode_jpeg_header`, `encode_jpeg`, `encode_jpeg_yuv_planes`, `is_jpeg`, with
|
|
148
|
+
the same arguments, defaults, return types and error messages — so it is a
|
|
149
|
+
drop-in replacement. The fork adds `handle_pool_stats()`,
|
|
150
|
+
`get_handle_pool_size()`, `set_handle_pool_size(n)`, `clear_handle_pool()` and
|
|
151
|
+
`libjpeg_turbo_version()`, and fixes two upstream error paths (an unnamed
|
|
152
|
+
subsampling and empty input both used to raise the wrong thing — see
|
|
153
|
+
[UPSTREAM.md](https://github.com/vxlk/leanjpeg/blob/main/packages/leanjpeg-simple/UPSTREAM.md)).
|
|
154
|
+
|
|
155
|
+
`leanjpeg.xl` mirrors that shape:
|
|
156
|
+
|
|
157
|
+
| `leanjpeg.simple` | `leanjpeg.xl` | differences |
|
|
158
|
+
|---|---|---|
|
|
159
|
+
| `decode_jpeg(data, colorspace, fastdct, fastupsample, min_height, min_width, min_factor, buffer, strict)` | `decode_jxl(data, colorspace, *, dtype, num_threads, buffer, keep_orientation, unpremultiply_alpha, unscaled)` | same colorspace names and `buffer=` semantics; no DCT-scaled decoding (that is a JPEG-only trick) |
|
|
160
|
+
| `decode_jpeg_header(data)` -> `(h, w, colorspace, subsampling)` | `decode_jxl_header(data)` -> `JxlHeader(height, width, colorspace, bit_depth, has_alpha, has_jpeg_reconstruction, has_container, has_animation, orientation)` | first three fields agree |
|
|
161
|
+
| `encode_jpeg(image, quality, colorspace, colorsubsampling, fastdct)` | `encode_jxl(image, quality, *, distance, lossless, effort, colorspace, decoding_speed, num_threads, use_container, modular, premultiplied_alpha, bits_per_sample)` | `quality` is translated to a libjxl distance (90 -> 1.0); `uint8`, `uint16` and `float32` input |
|
|
162
|
+
| `encode_jpeg_yuv_planes(Y, U, V, ...)` | — | libjxl takes interleaved RGB/gray only |
|
|
163
|
+
| `is_jpeg(data)` | `is_jxl(data)` | |
|
|
164
|
+
| — | `recompress_jpeg`, `reconstruct_jpeg`, `jpeg_dimensions` | lossless JPEG transcoding |
|
|
165
|
+
|
|
166
|
+
Colorspaces on both sides: `RGB`, `BGR`, `RGBX`, `BGRX`, `XBGR`, `XRGB`,
|
|
167
|
+
`RGBA`, `BGRA`, `ABGR`, `ARGB`, `GRAY`, and `GRAYA` on the JPEG XL side.
|
|
168
|
+
|
|
169
|
+
## JPEG XL feature coverage
|
|
170
|
+
|
|
171
|
+
The scope right now is encode/decode parity with the fast path, plus lossless
|
|
172
|
+
and JPEG recompression. Everything below the line is a deliberate omission,
|
|
173
|
+
not a limitation of the design — each is a small addition to
|
|
174
|
+
`_jxl_core.cpp` plus arguments on the existing functions.
|
|
175
|
+
|
|
176
|
+
| feature | status | notes |
|
|
177
|
+
|---|---|---|
|
|
178
|
+
| Lossy encode (`distance` / `quality`, `effort` 1-10) | **yes** | `JxlEncoderDistanceFromQuality`; effort maps to `JXL_ENC_FRAME_SETTING_EFFORT` |
|
|
179
|
+
| Lossless encode | **yes** | `lossless=True`, modular mode |
|
|
180
|
+
| Lossless JPEG recompression + bit-exact reconstruction | **yes** | `recompress_jpeg` / `reconstruct_jpeg`, `jbrd` box |
|
|
181
|
+
| Decode to RGB/BGR/RGBA/.../GRAY | **yes** | in-place swizzle, Rec.601 luma for `GRAY` |
|
|
182
|
+
| `uint8` / `uint16` / `float32` samples | **yes** | `dtype=` on decode, dtype-driven on encode |
|
|
183
|
+
| Custom bit depth (10/12/16-bit) | **yes** | `bits_per_sample=` on encode, `unscaled=` on decode |
|
|
184
|
+
| Alpha, premultiplied alpha | **yes** | `premultiplied_alpha=`, `unpremultiply_alpha=` |
|
|
185
|
+
| Output buffer reuse (`buffer=`) | **yes** | zero-allocation decode into your array |
|
|
186
|
+
| Container vs bare codestream | **yes** | `use_container=`; header reports `has_container` |
|
|
187
|
+
| EXIF orientation | **yes** | applied by default, `keep_orientation=True` to skip |
|
|
188
|
+
| Decoding-speed tier, modular toggle | **yes** | `decoding_speed=0..4`, `modular=` |
|
|
189
|
+
| Explicit thread control | **yes** | see [Threading](#threading) |
|
|
190
|
+
| — | | |
|
|
191
|
+
| Progressive / responsive decoding | *not yet* | `JxlDecoderSetProgressiveDetail` + `JxlDecoderFlushImage`; would add a callback or DC-preview API |
|
|
192
|
+
| Downscaled / DC-only decode | *not yet* | 1:8 preview from the DC groups |
|
|
193
|
+
| Region-of-interest decode | *not yet* | `JxlDecoderSetImageOutBuffer` on a crop |
|
|
194
|
+
| Animation (multi-frame) | *not yet* | header already reports `has_animation`; decoding one frame of an animation is unsupported |
|
|
195
|
+
| Extra channels (depth, spot, thermal) | *not yet* | `JxlEncoderSetExtraChannelInfo` |
|
|
196
|
+
| ICC profiles / colour management | *not yet* | currently sRGB in, sRGB out; `JxlEncoderSetICCProfile` / `JxlDecoderGetColorAsICCProfile` |
|
|
197
|
+
| HDR transfer functions (PQ / HLG), gain maps | *not yet* | needs the colour-encoding plumbing above |
|
|
198
|
+
| EXIF / XMP / JUMBF metadata passthrough | *not yet* | boxes are compiled in (`JPEGXL_ENABLE_BOXES=ON`), just not exposed |
|
|
199
|
+
| Streaming / chunked encode | *not yet* | `JXL_ENC_FRAME_SETTING_BUFFERING`, output-mode knobs |
|
|
200
|
+
| CMYK, >4 channels | *not yet* | |
|
|
201
|
+
|
|
202
|
+
JPEG (fast path) is feature-complete against simplejpeg; there is no roadmap
|
|
203
|
+
gap there.
|
|
204
|
+
|
|
205
|
+
## Performance
|
|
206
|
+
|
|
207
|
+
All numbers below come from `bench/` on **Python 3.14.3t (free-threaded)**,
|
|
208
|
+
Windows 10, a 4-core / 8-thread Intel mobile CPU, frames decoded from the
|
|
209
|
+
video fixtures with ffmpeg (nothing vendored). This is a thermally limited
|
|
210
|
+
laptop: read the *ratios*, not the absolute frame rates, and expect run-to-run
|
|
211
|
+
spread of a few tens of percent on the multi-second measurements. See
|
|
212
|
+
[bench/README.md](https://github.com/vxlk/leanjpeg/blob/main/bench/README.md) to reproduce.
|
|
213
|
+
|
|
214
|
+
### Fast path vs offline path
|
|
215
|
+
|
|
216
|
+

|
|
217
|
+
|
|
218
|
+
The two paths are two orders of magnitude apart in encode throughput, which is
|
|
219
|
+
the whole reason there are two of them. At 1080p the fast path encodes at
|
|
220
|
+
**120 fps** and decodes at **81 fps**; libjxl at its cheapest effort encodes at
|
|
221
|
+
**6.6 fps** and decodes at **20 fps**, and buys 0.74-0.81 bpp against JPEG's
|
|
222
|
+
0.78 bpp at the same nominal quality — i.e. at *equal effort settings* the
|
|
223
|
+
sizes are close, and JPEG XL's real advantage shows up as quality per byte
|
|
224
|
+
(below) rather than as raw compression at a fixed quality number.
|
|
225
|
+
|
|
226
|
+
| 1080p, single call | encode | decode | header |
|
|
227
|
+
|---|---|---|---|
|
|
228
|
+
| `leanjpeg.simple`, q85 4:2:0 | 120 fps | 81 fps (96 fps into a reused buffer) | 3.4 µs |
|
|
229
|
+
| `leanjpeg.xl`, effort 1 | 6.6 fps | 20.2 fps | — |
|
|
230
|
+
| `leanjpeg.xl`, effort 3 | 8.5 fps | 19.3 fps | — |
|
|
231
|
+
| `leanjpeg.xl`, effort 5 | 2.4 fps | 20.5 fps | — |
|
|
232
|
+
| `leanjpeg.xl`, effort 7 | 1.2 fps | 18.1 fps | — |
|
|
233
|
+
| `leanjpeg.xl`, lossless effort 5 | 1.4 fps | 5.7 fps | — |
|
|
234
|
+
|
|
235
|
+

|
|
236
|
+
|
|
237
|
+
Effort 3 is the best default for batch work: 7-9 % smaller than effort 1 for
|
|
238
|
+
about 20 % more encode time. Above that the returns stop: effort 5 is another
|
|
239
|
+
0.2-2 % smaller for three to four times the time, and at a *fixed distance*
|
|
240
|
+
effort 7 produced **larger** files than effort 5 on every image tested here —
|
|
241
|
+
+9 to +10 % on the fixtures, +0.4 to +4 % on photographic test images.
|
|
242
|
+
`distance` is a quality target rather than a size target, so this is not
|
|
243
|
+
"effort 7 compresses worse"; a slower encode can spend its bits differently at
|
|
244
|
+
the same nominal quality. It does mean the usual assumption that higher effort
|
|
245
|
+
is strictly smaller does not hold, so measure efforts on your own content
|
|
246
|
+
before paying for them.
|
|
247
|
+
|
|
248
|
+
### The fork against upstream simplejpeg
|
|
249
|
+
|
|
250
|
+
Comparing two separate benchmark processes on a laptop is meaningless — the
|
|
251
|
+
run-to-run spread is larger than the effect. `bench/ab_fork_vs_upstream.py`
|
|
252
|
+
therefore loads **both libraries into one process** and interleaves them pass
|
|
253
|
+
by pass, so drift hits both equally:
|
|
254
|
+
|
|
255
|
+

|
|
256
|
+
|
|
257
|
+
| single thread | encode | decode |
|
|
258
|
+
|---|---|---|
|
|
259
|
+
| 64×64 | **+30 %** (12063 vs 9276 fps) | **+20 %** (15106 vs 12610 fps) |
|
|
260
|
+
| 256×256 | **+13 %** (1218 vs 1075 fps) | +6 % (1172 vs 1105 fps) |
|
|
261
|
+
| 720p | +9 % (108 vs 99 fps) | −2 % (88 vs 91 fps) |
|
|
262
|
+
| 1080p | +21 % (37 vs 30 fps) | −7 % (25 vs 27 fps) |
|
|
263
|
+
| 2160p | +7 % (12.2 vs 11.4 fps) | +6 % (10.2 vs 9.6 fps) |
|
|
264
|
+
|
|
265
|
+
That is exactly the shape the change predicts. Upstream creates and destroys a
|
|
266
|
+
TurboJPEG handle on *every* call and lets TurboJPEG allocate and free the
|
|
267
|
+
output buffer on every encode; the fork keeps both in a pool. The saving is a
|
|
268
|
+
fixed per-call cost, so it dominates on small images (+30 % at 64×64, where
|
|
269
|
+
thumbnails, tiles and patch pipelines live) and fades into the pixel work on
|
|
270
|
+
large ones. Decode saves only the handle, so it sits at parity within noise.
|
|
271
|
+
|
|
272
|
+
Multi-threaded, both scale the same way — the codec releases the GIL either
|
|
273
|
+
way:
|
|
274
|
+
|
|
275
|
+

|
|
276
|
+
|
|
277
|
+
The difference on a free-threaded build is not throughput, it is that
|
|
278
|
+
**importing upstream simplejpeg re-enables the GIL for the entire process**:
|
|
279
|
+
|
|
280
|
+
```
|
|
281
|
+
RuntimeWarning: The global interpreter lock (GIL) has been enabled to load
|
|
282
|
+
module 'simplejpeg._jpeg', which has not declared that it can run safely
|
|
283
|
+
without the GIL.
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Your JPEG calls still scale, because they release the GIL. Everything *else*
|
|
287
|
+
in your program stops scaling. leanjpeg's backends declare
|
|
288
|
+
`Py_MOD_GIL_NOT_USED` and leave the GIL disabled.
|
|
289
|
+
|
|
290
|
+
### Threading
|
|
291
|
+
|
|
292
|
+
`leanjpeg.simple` has no threading of its own: libjpeg-turbo is single
|
|
293
|
+
threaded per call, and you scale by calling it from several Python threads
|
|
294
|
+
(above) or processes.
|
|
295
|
+
|
|
296
|
+
`leanjpeg.xl` drives libjxl's `JxlResizableParallelRunner` and exposes one
|
|
297
|
+
argument, `num_threads`, on every call:
|
|
298
|
+
|
|
299
|
+
| `num_threads` | behaviour |
|
|
300
|
+
|---|---|
|
|
301
|
+
| `None` / `0` (default) | automatic: `min(SuggestThreads(w, h), get_max_threads())`, at least 1. libjxl suggests about one thread per 256×256 group, capped by the hardware concurrency |
|
|
302
|
+
| `1` | no worker threads at all; everything runs on the calling thread |
|
|
303
|
+
| `N` | exactly `N` threads *including* the caller (`N-1` workers) |
|
|
304
|
+
|
|
305
|
+

|
|
306
|
+
|
|
307
|
+
Encoding scales well inside one call (0.73 → 2.27 fps from 1 to 8 threads at
|
|
308
|
+
1080p, effort 5). Decoding saturates around 4 threads inside one call
|
|
309
|
+
(6.3 → 17.2 fps), and past that you get more from **Python-level** parallelism:
|
|
310
|
+
8 Python threads each calling with `num_threads=1` reach 27.8 fps aggregate
|
|
311
|
+
versus 18.3 fps for one call with `num_threads=8`. The rule of thumb:
|
|
312
|
+
|
|
313
|
+
* one image at a time (interactive, a single large file) → leave `num_threads`
|
|
314
|
+
automatic;
|
|
315
|
+
* many images (a batch, a dataloader, a server) → `num_threads=1` and
|
|
316
|
+
parallelise in Python, otherwise `N` Python threads × `M` libjxl workers
|
|
317
|
+
oversubscribes the machine.
|
|
318
|
+
|
|
319
|
+
`set_max_threads(n)` caps the automatic mode process-wide;
|
|
320
|
+
`suggest_num_threads(h, w)` and `effective_num_threads(h, w, n)` report what
|
|
321
|
+
the decision would be. Encoded bytes never depend on the thread count (there
|
|
322
|
+
is a test for that), so this is purely a performance knob.
|
|
323
|
+
|
|
324
|
+
Related knobs that interact with threading: `effort` (higher efforts add
|
|
325
|
+
sequential phases and gain less from threads) and `modular` (lossless mode
|
|
326
|
+
parallelises per modular group). Deliberately not exposed yet:
|
|
327
|
+
`MODULAR_GROUP_SIZE`, the buffering/output-mode streaming settings, and
|
|
328
|
+
`JxlThreadParallelRunner` (no advantage over the resizable runner here).
|
|
329
|
+
|
|
330
|
+
### Reduced allocations
|
|
331
|
+
|
|
332
|
+
Both backends pool their codec state, so a steady-state call allocates only
|
|
333
|
+
the object it returns. The counters are public — this is from the 1080p
|
|
334
|
+
benchmark run:
|
|
335
|
+
|
|
336
|
+
```python
|
|
337
|
+
>>> leanjpeg_simple.handle_pool_stats()
|
|
338
|
+
{'acquired': 2904, 'created': 16, 'destroyed': 0, 'scratch_reallocs': 10,
|
|
339
|
+
'cached_compress': 8, 'cached_decompress': 8, 'max_cached': 16,
|
|
340
|
+
'scratch_bytes': 68767744}
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
2904 encode/decode calls created **16** TurboJPEG handles (one per concurrent
|
|
344
|
+
caller, then reused) and grew the encoder's output buffer **10** times, after
|
|
345
|
+
which the high-water mark held. `created` and `scratch_reallocs` going flat
|
|
346
|
+
while `acquired` keeps climbing is the property the tests assert.
|
|
347
|
+
`leanjpeg_xl.codec_pool_stats()` reports the same for the JPEG XL codecs
|
|
348
|
+
(encoder + decoder + thread pool + buffers per pooled entry).
|
|
349
|
+
|
|
350
|
+
Pool sizes are tunable — `set_handle_pool_size(n)` / `set_codec_pool_size(n)`,
|
|
351
|
+
and `clear_handle_pool()` / `clear_codec_pool()` to release everything (for
|
|
352
|
+
example before forking or when a long-lived process goes idle).
|
|
353
|
+
|
|
354
|
+
## Image quality
|
|
355
|
+
|
|
356
|
+
Same frame, encoded to the *same file size* by both codecs, so the comparison
|
|
357
|
+
is quality-at-a-budget rather than two different points. libjxl's distance is
|
|
358
|
+
found by bisection until it matches the JPEG's byte count (±2 %); PSNR is on
|
|
359
|
+
RGB, SSIM on luma. Full-resolution originals and the other clips are in
|
|
360
|
+
[`docs/quality/`](https://github.com/vxlk/leanjpeg/tree/main/docs/quality/).
|
|
361
|
+
|
|
362
|
+

|
|
363
|
+
|
|
364
|
+
| clip | JPEG quality | JPEG size | JPEG PSNR / SSIM | JPEG XL distance (same size) | JPEG XL PSNR / SSIM |
|
|
365
|
+
|---|---|---|---|---|---|
|
|
366
|
+
| broadcast_news_720p | 50 | 57.8 KiB (0.51 bpp) | 28.5 dB / 0.976 | 3.00 (57.5 KiB) | **30.4 dB / 0.984** |
|
|
367
|
+
| broadcast_news_720p | 75 | 85.1 KiB (0.76 bpp) | 31.1 dB / 0.987 | 1.67 (85.1 KiB) | **32.6 dB / 0.990** |
|
|
368
|
+
| broadcast_news_720p | 90 | 141.8 KiB (1.26 bpp) | 33.0 dB / 0.994 | 0.76 (139.0 KiB) | **36.5 dB / 0.994** |
|
|
369
|
+
| dense_text_1080p | 50 | 136.8 KiB (0.54 bpp) | 28.0 dB / 0.974 | 3.22 (135.2 KiB) | **29.8 dB / 0.982** |
|
|
370
|
+
| dense_text_1080p | 75 | 202.6 KiB (0.80 bpp) | 30.6 dB / 0.986 | 1.75 (204.5 KiB) | **32.3 dB / 0.990** |
|
|
371
|
+
| dense_text_1080p | 90 | 335.9 KiB (1.33 bpp) | 32.6 dB / 0.994 | 0.79 (335.7 KiB) | **36.3 dB / 0.994** |
|
|
372
|
+
| dashcam_720p | 50 | 58.2 KiB (0.52 bpp) | 28.7 dB / 0.975 | 2.93 (58.1 KiB) | **30.3 dB / 0.984** |
|
|
373
|
+
| dashcam_720p | 75 | 85.9 KiB (0.76 bpp) | 31.2 dB / 0.987 | 1.60 (86.6 KiB) | **32.4 dB / 0.990** |
|
|
374
|
+
| dashcam_720p | 90 | 145.4 KiB (1.29 bpp) | 33.3 dB / 0.994 | 0.69 (144.1 KiB) | **36.2 dB / 0.994** |
|
|
375
|
+
|
|
376
|
+
JPEG XL wins everywhere here, by 1.5-3.5 dB PSNR at the same bytes, with the
|
|
377
|
+
gap widest at high quality. Read that with one caveat: **the fixtures are
|
|
378
|
+
synthetic**. They are OCR test clips — a Mandelbrot render under broadcast,
|
|
379
|
+
dashcam and telemetry text overlays — so they are all hard edges, saturated
|
|
380
|
+
colours and smooth gradients, which is friendly territory for JPEG XL's
|
|
381
|
+
modular tools and hostile to 4:2:0 chroma subsampling. The margin on your
|
|
382
|
+
content will be different.
|
|
383
|
+
|
|
384
|
+
There is no photographic corpus with true (non-JPEG) originals in this
|
|
385
|
+
repository to quote instead, and re-encoding existing JPEGs is not a valid
|
|
386
|
+
substitute: re-encoding a JPEG with JPEG at a matching quality is close to an
|
|
387
|
+
identity operation — measured that way, JPEG scores 54-72 dB PSNR on several
|
|
388
|
+
of simplejpeg's test photos, for reasons that have nothing to do with codec
|
|
389
|
+
quality. Compare on your own originals:
|
|
390
|
+
|
|
391
|
+
```bash
|
|
392
|
+
python bench/quality_compare.py --images a.png,b.png --out docs/quality
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
### Lossless
|
|
396
|
+
|
|
397
|
+
| clip | frame | PNG (Pillow, optimised) | JPEG XL lossless e7 | JPEG q90 | JPEG q90 → JPEG XL |
|
|
398
|
+
|---|---|---|---|---|---|
|
|
399
|
+
| broadcast_news_720p | 1280×720 | 622.1 KiB | 372.9 KiB (60 % of PNG) | 141.8 KiB | 113.9 KiB (**19.6 % smaller**) |
|
|
400
|
+
| dense_text_1080p | 1920×1080 | 1.40 MiB | 863.2 KiB (60 % of PNG) | 335.9 KiB | 269.9 KiB (**19.6 % smaller**) |
|
|
401
|
+
| dashcam_720p | 1280×720 | 709.0 KiB | 408.7 KiB (58 % of PNG) | 145.4 KiB | 116.8 KiB (**19.7 % smaller**) |
|
|
402
|
+
|
|
403
|
+
## Lossless JPEG recompression
|
|
404
|
+
|
|
405
|
+
The last column above is the feature to reach for if you have a JPEG archive.
|
|
406
|
+
`recompress_jpeg` re-entropy-codes the existing DCT coefficients — the image
|
|
407
|
+
is never decoded to pixels and never re-quantised — and stores a `jbrd` box
|
|
408
|
+
with everything needed to rebuild the original container. `reconstruct_jpeg`
|
|
409
|
+
returns the original file, byte for byte.
|
|
410
|
+
|
|
411
|
+

|
|
412
|
+
|
|
413
|
+
On 720p MJPEG frames (ffmpeg `-q:v 20`): **32.4 % smaller**, 41 fps to
|
|
414
|
+
recompress, 126 fps to reconstruct, and 128 fps to decode straight to pixels
|
|
415
|
+
without materialising the JPEG. Savings depend on how the original was
|
|
416
|
+
encoded — 32 % on those MJPEG frames, ~20 % on Pillow's q90 stills above.
|
|
417
|
+
|
|
418
|
+
```python
|
|
419
|
+
jxl = xl.recompress_jpeg(jpeg_bytes)
|
|
420
|
+
assert xl.reconstruct_jpeg(jxl) == jpeg_bytes # bit-exact
|
|
421
|
+
assert xl.decode_jxl_header(jxl).has_jpeg_reconstruction # tells you it is reversible
|
|
422
|
+
pixels = xl.decode_jxl(jxl) # or go straight to pixels
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
Round trips are verified bit-exact in the test suite for progressive,
|
|
426
|
+
optimised, 4:4:4 / 4:2:2 / 4:2:0 and grayscale JPEGs, for ffmpeg's MJPEG
|
|
427
|
+
output, and for the 21 photos in simplejpeg's own test corpus when that
|
|
428
|
+
submodule is checked out.
|
|
429
|
+
|
|
430
|
+
## Freezing with PyInstaller
|
|
431
|
+
|
|
432
|
+
All three distributions ship their own PyInstaller hook and advertise it
|
|
433
|
+
through the `pyinstaller40` entry point, so `pyinstaller app.py` just works --
|
|
434
|
+
no `--hidden-import`, no `--collect-all`, nothing from
|
|
435
|
+
pyinstaller-hooks-contrib.
|
|
436
|
+
|
|
437
|
+
The hooks earn their place. Backends are resolved through `importlib`, which
|
|
438
|
+
static analysis cannot follow, so an unhooked bundle silently reports every
|
|
439
|
+
backend as missing:
|
|
440
|
+
|
|
441
|
+
```python
|
|
442
|
+
>>> leanjpeg.backends() # frozen without the hook
|
|
443
|
+
{'simple': False, 'xl': False}
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
and the compiled extensions import NumPy from machine code, which PyInstaller
|
|
447
|
+
only notices today because it parses the type stub sitting next to each
|
|
448
|
+
extension. The hooks state both outright.
|
|
449
|
+
|
|
450
|
+
To leave an installed backend out of a bundle, exclude its shim -- `backends()`
|
|
451
|
+
then honestly reports it as absent and `BackendNotInstalled` names it:
|
|
452
|
+
|
|
453
|
+
```bash
|
|
454
|
+
pyinstaller --exclude-module leanjpeg.xl app.py
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
`tests/test_pyinstaller.py` freezes and runs an application for each entry
|
|
458
|
+
point; it is marked `slow` and skips itself when PyInstaller is absent.
|
|
459
|
+
|
|
460
|
+
## Free-threaded CPython
|
|
461
|
+
|
|
462
|
+
Both extensions declare `Py_MOD_GIL_NOT_USED` (Cython's
|
|
463
|
+
`freethreading_compatible=True`), hold no unprotected global state — the pools
|
|
464
|
+
are lock-protected free lists — and release the GIL around every codec call.
|
|
465
|
+
Importing them on 3.13t / 3.14t leaves `sys._is_gil_enabled()` `False`.
|
|
466
|
+
|
|
467
|
+
```python
|
|
468
|
+
import sys, leanjpeg_simple, leanjpeg_xl
|
|
469
|
+
assert not sys._is_gil_enabled() # on a free-threaded build
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
The test suite runs on both build kinds; the free-threading-specific tests
|
|
473
|
+
(concurrent encode/decode from many threads, pool behaviour under contention)
|
|
474
|
+
skip themselves on a GIL build.
|
|
475
|
+
|
|
476
|
+
## Building from source
|
|
477
|
+
|
|
478
|
+
```bash
|
|
479
|
+
git clone --recurse-submodules https://github.com/vxlk/leanjpeg
|
|
480
|
+
cd leanjpeg
|
|
481
|
+
pip install -e packages/leanjpeg-simple # needs CMake, a C compiler, NASM
|
|
482
|
+
pip install -e packages/leanjpeg-xl # needs CMake >= 3.16, C++17
|
|
483
|
+
pip install -e .
|
|
484
|
+
pytest
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
Each package builds its codec out of tree into
|
|
488
|
+
`packages/<name>/build/<codec>_<os>_<arch>/prefix` and links it statically, so
|
|
489
|
+
a rebuild of the Python extension does not rebuild the codec. Source
|
|
490
|
+
distributions carry the vendored codec sources, so `pip install` from an sdist
|
|
491
|
+
needs no network and no git checkout; if the submodule is missing from a git
|
|
492
|
+
tree, `leanjpeg-simple` falls back to downloading a pinned, checksummed
|
|
493
|
+
libjpeg-turbo tarball. On Windows, build from a short path - past 260
|
|
494
|
+
characters cmake fails to detect the compiler.
|
|
495
|
+
|
|
496
|
+
`python tools/build_dists.py --all` builds every distribution for the current
|
|
497
|
+
platform; `.github/workflows/wheels.yml` builds all 64 wheels.
|
|
498
|
+
|
|
499
|
+
## Keeping up with upstream
|
|
500
|
+
|
|
501
|
+
Both codecs are pinned git submodules, and the simplejpeg fork is kept
|
|
502
|
+
mergeable on purpose. `packages/leanjpeg-simple/UPSTREAM.json` records the
|
|
503
|
+
upstream commit and a file-by-file map; `UPSTREAM.md` lists every intentional
|
|
504
|
+
difference. The helper does the routine work:
|
|
505
|
+
|
|
506
|
+
```bash
|
|
507
|
+
python tools/upstream_sync.py status # are we behind upstream?
|
|
508
|
+
python tools/upstream_sync.py diff # what did we change, per file?
|
|
509
|
+
python tools/upstream_sync.py merge # 3-way merge upstream changes into the fork
|
|
510
|
+
python tools/upstream_sync.py pin # record a new upstream commit
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
Files the fork did not touch (`_color.c`, `_color.h`, the custom build
|
|
514
|
+
backend) are reported as identical, so a sync is only ever about the handful
|
|
515
|
+
of files that carry the two changes. Upgrading libjpeg-turbo or libjxl is a
|
|
516
|
+
submodule bump plus a version constant.
|
|
517
|
+
|
|
518
|
+
## Tests
|
|
519
|
+
|
|
520
|
+
```bash
|
|
521
|
+
pytest # everything that is installed
|
|
522
|
+
pytest -m "not ffmpeg" # skip the tests that shell out to ffmpeg
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
| suite | covers |
|
|
526
|
+
|---|---|
|
|
527
|
+
| `packages/leanjpeg-simple/tests` | simplejpeg's own decode/encode/YUV/util tests, ported unchanged |
|
|
528
|
+
| `packages/leanjpeg-xl/tests` | codec round trips across colorspaces, dtypes, bit depths, alpha; JPEG recompression; threading invariants |
|
|
529
|
+
| `tests/` | backend discovery and error messages, cross-backend parity, threading, ffmpeg-driven workflows, packaging metadata, PyInstaller freezes |
|
|
530
|
+
|
|
531
|
+
Suites skip themselves when their backend is not installed, so a
|
|
532
|
+
`leanjpeg[simple]`-only install still has a green run. The PyInstaller tests
|
|
533
|
+
build and run real executables, so they are marked `slow` and skip themselves
|
|
534
|
+
where PyInstaller is absent (`pytest -m "not slow"` skips them explicitly). The ffmpeg-marked tests
|
|
535
|
+
locate ffmpeg and the video fixtures via `LEANJPEG_FFMPEG` and
|
|
536
|
+
`LEANJPEG_VIDEOS`, a sibling `video-overlay-ocr` checkout, or `PATH`, and skip
|
|
537
|
+
when none is found.
|
|
538
|
+
|
|
539
|
+
CI runs that suite on Linux, macOS and Windows for 3.13, 3.13t, 3.14 and 3.14t,
|
|
540
|
+
and the release workflow re-runs each backend's suite against every built wheel
|
|
541
|
+
and against both source distributions unpacked outside the git checkout.
|
|
542
|
+
|
|
543
|
+
## Licences
|
|
544
|
+
|
|
545
|
+
leanjpeg is MIT. `leanjpeg-simple` also carries simplejpeg's MIT licence
|
|
546
|
+
(`LICENSE.simplejpeg`) and libjpeg-turbo's two BSD-style licences;
|
|
547
|
+
`leanjpeg-xl` statically links libjxl (BSD-3-Clause) and its dependencies —
|
|
548
|
+
highway (Apache-2.0), brotli (MIT) and skcms (BSD-3-Clause). ffmpeg is **not** vendored — the benchmarks call whatever
|
|
549
|
+
ffmpeg you point them at.
|