leanjpeg 0.1.0__py3-none-any.whl

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/__init__.py ADDED
@@ -0,0 +1,188 @@
1
+ """leanjpeg: lean JPEG and JPEG XL encoding/decoding for NumPy.
2
+
3
+ Two independently installable backends live behind this package:
4
+
5
+ ``leanjpeg.simple``
6
+ The *fast path*: a fork of simplejpeg (https://github.com/jfolz/simplejpeg,
7
+ libjpeg-turbo) with free-threaded CPython (3.13t/3.14t) support and pooled
8
+ codec handles / output buffers so that repeated calls do not allocate.
9
+ Distribution: ``leanjpeg-simple`` (``pip install "leanjpeg[simple]"``).
10
+
11
+ ``leanjpeg.xl``
12
+ The *offline path*: a light binding over libjxl (https://github.com/libjxl/libjxl)
13
+ mirroring the simplejpeg API, with lossy/lossless encoding, decoding and
14
+ bit-exact lossless JPEG -> JPEG XL recompression.
15
+ Distribution: ``leanjpeg-xl`` (``pip install "leanjpeg[xl]"``).
16
+
17
+ This root package only contains pure-Python glue: backend discovery,
18
+ format sniffing and a tiny format-agnostic ``decode``/``encode`` dispatcher.
19
+ """
20
+ from __future__ import annotations
21
+
22
+ import importlib
23
+ import importlib.util
24
+
25
+ __version__ = "0.1.0"
26
+ __all__ = [
27
+ "__version__",
28
+ "BackendNotInstalled",
29
+ "backends",
30
+ "available_backends",
31
+ "require_backend",
32
+ "is_jpeg",
33
+ "is_jxl",
34
+ "sniff",
35
+ "decode",
36
+ "decode_header",
37
+ "encode",
38
+ ]
39
+
40
+ #: backend name -> importable distribution module
41
+ _BACKEND_MODULES = {"simple": "leanjpeg_simple", "xl": "leanjpeg_xl"}
42
+ _INSTALL_HINTS = {
43
+ "simple": 'pip install "leanjpeg[simple]" (distribution: leanjpeg-simple)',
44
+ "xl": 'pip install "leanjpeg[xl]" (distribution: leanjpeg-xl)',
45
+ }
46
+
47
+ JPEG_SOI = b"\xff\xd8"
48
+ JPEG_EOI = b"\xff\xd9"
49
+ JXL_CODESTREAM_SIGNATURE = b"\xff\x0a"
50
+ JXL_CONTAINER_SIGNATURE = b"\x00\x00\x00\x0cJXL \x0d\x0a\x87\x0a"
51
+
52
+
53
+ class BackendNotInstalled(ImportError):
54
+ """Raised when a leanjpeg backend is used but its distribution is missing."""
55
+
56
+ def __init__(self, backend: str, cause: BaseException | None = None):
57
+ hint = _INSTALL_HINTS.get(backend, f"pip install leanjpeg-{backend}")
58
+ msg = f"leanjpeg.{backend} is not installed. Install it with: {hint}"
59
+ if cause is not None:
60
+ msg += f" (import failed with: {cause!r})"
61
+ super().__init__(msg)
62
+ self.backend = backend
63
+
64
+
65
+ def backends() -> dict[str, bool]:
66
+ """Return ``{'simple': bool, 'xl': bool}`` describing installed backends.
67
+
68
+ Only checks importability of the distribution modules (cheap, no import).
69
+ """
70
+ return {
71
+ name: importlib.util.find_spec(module) is not None
72
+ for name, module in _BACKEND_MODULES.items()
73
+ }
74
+
75
+
76
+ def available_backends() -> list[str]:
77
+ """Names of the installed backends, in preference order (``simple`` first)."""
78
+ return [name for name, ok in backends().items() if ok]
79
+
80
+
81
+ def require_backend(name: str):
82
+ """Import and return the backend module ``leanjpeg.<name>``.
83
+
84
+ Raises :class:`BackendNotInstalled` with an install hint when missing.
85
+ """
86
+ if name not in _BACKEND_MODULES:
87
+ raise ValueError(f"unknown backend {name!r}; expected one of {sorted(_BACKEND_MODULES)}")
88
+ try:
89
+ return importlib.import_module(f"leanjpeg.{name}")
90
+ except ImportError as e: # pragma: no cover - depends on installation
91
+ if isinstance(e, BackendNotInstalled):
92
+ raise
93
+ raise BackendNotInstalled(name, e) from e
94
+
95
+
96
+ def _head_tail(data, n: int = 12) -> tuple[bytes, bytes]:
97
+ """First ``n`` and last 2 bytes of any bytes-like / buffer-protocol object."""
98
+ if isinstance(data, (bytes, bytearray)):
99
+ return bytes(data[:n]), bytes(data[-2:])
100
+ mv = memoryview(data)
101
+ if mv.ndim != 1 or mv.itemsize != 1:
102
+ mv = mv.cast("B")
103
+ return bytes(mv[:n]), bytes(mv[-2:])
104
+
105
+
106
+ def is_jpeg(data) -> bool:
107
+ """True if ``data`` looks like a complete JPEG (JFIF/Exif) file.
108
+
109
+ Mirrors ``simplejpeg.is_jpeg``: checks the SOI marker at the start and
110
+ the EOI marker at the end, so truncated files return False.
111
+ """
112
+ try:
113
+ head, tail = _head_tail(data)
114
+ except TypeError:
115
+ return False
116
+ return head[:2] == JPEG_SOI and tail == JPEG_EOI
117
+
118
+
119
+ def is_jxl(data) -> bool:
120
+ """True if ``data`` starts with a JPEG XL codestream or container signature."""
121
+ try:
122
+ head, _ = _head_tail(data)
123
+ except TypeError:
124
+ return False
125
+ return head.startswith(JXL_CODESTREAM_SIGNATURE) or head.startswith(JXL_CONTAINER_SIGNATURE)
126
+
127
+
128
+ def sniff(data) -> str | None:
129
+ """Return ``'jpeg'``, ``'jxl'`` or ``None`` based on the signature bytes."""
130
+ try:
131
+ head, _ = _head_tail(data)
132
+ except TypeError:
133
+ return None
134
+ if head[:2] == JPEG_SOI:
135
+ return "jpeg"
136
+ if head.startswith(JXL_CODESTREAM_SIGNATURE) or head.startswith(JXL_CONTAINER_SIGNATURE):
137
+ return "jxl"
138
+ return None
139
+
140
+
141
+ def decode(data, colorspace: str = "RGB", **kwargs):
142
+ """Decode JPEG or JPEG XL bytes to a ``(height, width, channels)`` array.
143
+
144
+ The format is sniffed from the signature and dispatched to
145
+ ``leanjpeg.simple.decode_jpeg`` or ``leanjpeg.xl.decode_jxl``.
146
+ Keyword arguments are passed through to the backend function.
147
+ """
148
+ kind = sniff(data)
149
+ if kind == "jpeg":
150
+ return require_backend("simple").decode_jpeg(data, colorspace, **kwargs)
151
+ if kind == "jxl":
152
+ return require_backend("xl").decode_jxl(data, colorspace, **kwargs)
153
+ raise ValueError("data is neither JPEG nor JPEG XL (unknown signature)")
154
+
155
+
156
+ def decode_header(data, **kwargs):
157
+ """Header-only decode; returns ``(height, width, colorspace, ...)`` tuples.
158
+
159
+ Dispatches to ``decode_jpeg_header`` / ``decode_jxl_header``. The first
160
+ three tuple elements have the same meaning for both backends.
161
+ """
162
+ kind = sniff(data)
163
+ if kind == "jpeg":
164
+ return require_backend("simple").decode_jpeg_header(data, **kwargs)
165
+ if kind == "jxl":
166
+ return require_backend("xl").decode_jxl_header(data, **kwargs)
167
+ raise ValueError("data is neither JPEG nor JPEG XL (unknown signature)")
168
+
169
+
170
+ def encode(image, format: str = "jpeg", **kwargs) -> bytes:
171
+ """Encode an array with the backend selected by ``format`` (``'jpeg'``/``'jxl'``)."""
172
+ fmt = format.lower()
173
+ if fmt in ("jpeg", "jpg"):
174
+ return require_backend("simple").encode_jpeg(image, **kwargs)
175
+ if fmt in ("jxl", "jpegxl", "jpeg-xl", "jpeg xl"):
176
+ return require_backend("xl").encode_jxl(image, **kwargs)
177
+ raise ValueError(f"unknown format {format!r}; expected 'jpeg' or 'jxl'")
178
+
179
+
180
+ def __getattr__(name: str):
181
+ # Lazy access: ``import leanjpeg; leanjpeg.simple`` / ``leanjpeg.xl``
182
+ if name in _BACKEND_MODULES:
183
+ return require_backend(name)
184
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
185
+
186
+
187
+ def __dir__():
188
+ return sorted(set(globals()) | set(_BACKEND_MODULES))
@@ -0,0 +1,13 @@
1
+ """PyInstaller hooks shipped with the ``leanjpeg`` distribution.
2
+
3
+ PyInstaller finds this directory through the ``pyinstaller40`` entry point in
4
+ ``pyproject.toml``, so freezing an application that uses leanjpeg needs no
5
+ configuration and no hook from pyinstaller-hooks-contrib.
6
+
7
+ Nothing here is imported at runtime; it only runs inside PyInstaller.
8
+ """
9
+ import os.path as pt
10
+
11
+
12
+ def get_hook_dirs():
13
+ return [pt.dirname(__file__)]
@@ -0,0 +1,29 @@
1
+ """PyInstaller hook for the ``leanjpeg`` root package.
2
+
3
+ The root package resolves its backends dynamically: ``backends()`` asks
4
+ ``importlib.util.find_spec`` and ``require_backend()`` calls
5
+ ``importlib.import_module('leanjpeg.<name>')``. Neither spelling is visible to
6
+ a static analysis, so an unhooked build collects neither shim, every backend
7
+ reports as missing, and the first call raises ``BackendNotInstalled`` -- on a
8
+ machine where both backends are installed.
9
+
10
+ Only installed backends are added. A hidden import for a distribution that is
11
+ not there is a build warning, and a frozen app reporting ``{'xl': False}`` for
12
+ a backend the developer never installed is the right answer rather than a bug.
13
+ To leave an installed backend out of a bundle on purpose, exclude its shim:
14
+
15
+ pyinstaller --exclude-module leanjpeg.xl app.py
16
+
17
+ The backends' own hooks (shipped with ``leanjpeg-simple`` / ``leanjpeg-xl``)
18
+ take it from there.
19
+ """
20
+ from PyInstaller.utils.hooks import can_import_module
21
+
22
+ hiddenimports = [
23
+ shim
24
+ for shim, distribution in (
25
+ ('leanjpeg.simple', 'leanjpeg_simple'),
26
+ ('leanjpeg.xl', 'leanjpeg_xl'),
27
+ )
28
+ if can_import_module(distribution)
29
+ ]
leanjpeg/py.typed ADDED
File without changes
@@ -0,0 +1,16 @@
1
+ """leanjpeg.simple: the fast path (libjpeg-turbo via the simplejpeg fork).
2
+
3
+ This module re-exports everything from the ``leanjpeg_simple`` distribution.
4
+ Install it with ``pip install "leanjpeg[simple]"``.
5
+ """
6
+ try:
7
+ import leanjpeg_simple as _backend
8
+ except ImportError as _e: # pragma: no cover - depends on installation
9
+ from leanjpeg import BackendNotInstalled as _BackendNotInstalled
10
+
11
+ raise _BackendNotInstalled("simple", _e) from _e
12
+
13
+ from leanjpeg_simple import * # noqa: F401,F403
14
+ from leanjpeg_simple import __all__, __version__, __upstream_version__ # noqa: F401
15
+
16
+ __backend__ = _backend
@@ -0,0 +1,16 @@
1
+ """leanjpeg.xl: the offline path (JPEG XL via libjxl).
2
+
3
+ This module re-exports everything from the ``leanjpeg_xl`` distribution.
4
+ Install it with ``pip install "leanjpeg[xl]"``.
5
+ """
6
+ try:
7
+ import leanjpeg_xl as _backend
8
+ except ImportError as _e: # pragma: no cover - depends on installation
9
+ from leanjpeg import BackendNotInstalled as _BackendNotInstalled
10
+
11
+ raise _BackendNotInstalled("xl", _e) from _e
12
+
13
+ from leanjpeg_xl import * # noqa: F401,F403
14
+ from leanjpeg_xl import __all__, __version__, __libjxl_version__ # noqa: F401
15
+
16
+ __backend__ = _backend
@@ -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
+ ![Fast path vs offline path](https://raw.githubusercontent.com/vxlk/leanjpeg/main/docs/charts/fast_vs_offline.png)
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
+ ![JPEG XL encoder effort](https://raw.githubusercontent.com/vxlk/leanjpeg/main/docs/charts/jxl_effort.png)
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
+ ![JPEG throughput, single thread](https://raw.githubusercontent.com/vxlk/leanjpeg/main/docs/charts/jpeg_single_thread.png)
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
+ ![JPEG throughput vs Python threads](https://raw.githubusercontent.com/vxlk/leanjpeg/main/docs/charts/jpeg_threads.png)
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
+ ![JPEG XL threading](https://raw.githubusercontent.com/vxlk/leanjpeg/main/docs/charts/jxl_threads.png)
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
+ ![JPEG vs JPEG XL crops](https://raw.githubusercontent.com/vxlk/leanjpeg/main/docs/quality/broadcast_news_720p_crops.png)
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
+ ![JPEG to JPEG XL recompression](https://raw.githubusercontent.com/vxlk/leanjpeg/main/docs/charts/jpeg_recompress.png)
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.
@@ -0,0 +1,12 @@
1
+ leanjpeg/__init__.py,sha256=Idg4z1n6elmOo_2-dIOLqLk4cV2dnFFgvwdWnNG02UI,6703
2
+ leanjpeg/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ leanjpeg/__pyinstaller/__init__.py,sha256=78ZF3kg7XsNUofdnQfgnIev8MH1GHNm6PZUuJ41lqNk,430
4
+ leanjpeg/__pyinstaller/hook-leanjpeg.py,sha256=z6LfqT2N4STmQqVN98GF3ArpdWv4pW5ARNDTL69a_ow,1183
5
+ leanjpeg/simple/__init__.py,sha256=mbL1QpsinG5yIsKPiH67kurnkNaj8eZci2pzeaBsvJM,607
6
+ leanjpeg/xl/__init__.py,sha256=7cf_2SJdW7-eyTSUdyqOBlurd4csmh5v2wh2UXJYqwM,561
7
+ leanjpeg-0.1.0.dist-info/licenses/LICENSE,sha256=27SbVUnd2DwRPMFi-IBHBIuZ4aqab2kaRdvcEAFr6-I,1703
8
+ leanjpeg-0.1.0.dist-info/METADATA,sha256=z--OLnN1n0rkmeubIQmvWI3ZzFyr3596IF8khLntvqk,28053
9
+ leanjpeg-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
10
+ leanjpeg-0.1.0.dist-info/entry_points.txt,sha256=_u6JVer4A5G6NRzmNgMc9nhxmi7NTUf4vYmCa2CTowI,65
11
+ leanjpeg-0.1.0.dist-info/top_level.txt,sha256=WCtsTq_kPcgN_4XciKychpysEE6BCMxNGedJ-J3ok1w,9
12
+ leanjpeg-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [pyinstaller40]
2
+ hook-dirs = leanjpeg.__pyinstaller:get_hook_dirs
@@ -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 @@
1
+ leanjpeg