pith-jpeg 0.1.1__py3-none-win_amd64.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.
pith_jpeg/__init__.py ADDED
@@ -0,0 +1,233 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2026 pith-hash
3
+ """pith-jpeg SDK: JPEG decoding and kernel replay through ctypes.
4
+
5
+ Decodes a native JPEG (baseline SOF0/SOF1 and progressive SOF2,
6
+ byte-exact with libjpeg ``-dct int -nosmooth``) through the Rust
7
+ cdylib. The cdylib computes the ``fnv1a64`` digest of the decoded
8
+ sample bytes internally, so this package stays zero-dependency: one
9
+ call per fixture returns the exact digest the ``reference.json``
10
+ ``digests`` section pins. The four numeric kernels the ``vectors``
11
+ section pins (de-zigzag, dequantize, the fixed-point islow IDCT and
12
+ the orthonormal ``f64`` oracle) replay through the same boundary over
13
+ little-endian ``f64`` bit-pattern buffers.
14
+
15
+ The cdylib is located through the suite's discovery chain:
16
+
17
+ 1. ``PITH_CDYLIB`` — an explicit cdylib *file* path;
18
+ 2. ``PITH_CDYLIB_DIR`` — a *directory* scanned for the cdylib names
19
+ (the CD pipeline points this at ``target/release``);
20
+ 3. the package directory itself (the built wheel ships the cdylib as
21
+ package data);
22
+ 4. ``<repo root>/target/release`` — the repository working-tree layout,
23
+ so a source checkout runs against a local cargo build with no
24
+ configuration.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import ctypes
30
+ import os
31
+ from pathlib import Path
32
+
33
+ __all__ = [
34
+ "FfiError",
35
+ "LibraryNotFoundError",
36
+ "find_cdylib",
37
+ "decode_digest",
38
+ "channels",
39
+ "dequant_zigzag",
40
+ "dequant",
41
+ "idct_islow",
42
+ "idct_oracle",
43
+ "STATUS_OK",
44
+ "STATUS_INVALID",
45
+ "STATUS_REJECTED",
46
+ "LAYOUT_GRAY8",
47
+ "LAYOUT_RGB8",
48
+ "LAYOUT_RGBA8",
49
+ "BLOCK",
50
+ ]
51
+
52
+ #: Status: success.
53
+ STATUS_OK = 0
54
+ #: Status: a caller argument is invalid — null pointer, unknown layout,
55
+ #: wrong buffer length.
56
+ STATUS_INVALID = -1
57
+ #: Status: the decoder refused the input (malformed JPEG), or the
58
+ #: requested conversion is not offered (RGB→gray).
59
+ STATUS_REJECTED = -2
60
+
61
+ #: Output layout: 8-bit grayscale (the ``pith-image`` code).
62
+ LAYOUT_GRAY8 = 0
63
+ #: Output layout: 8-bit RGB, channel-interleaved (the ``pith-image``
64
+ #: code).
65
+ LAYOUT_RGB8 = 2
66
+ #: Output layout: 8-bit RGBA, channel-interleaved (the ``pith-image``
67
+ #: code).
68
+ LAYOUT_RGBA8 = 4
69
+
70
+ #: One 8×8 kernel block: 64 ``f64`` values.
71
+ BLOCK = 64
72
+
73
+ #: Every cdylib file name cargo may drop into the build directory, per
74
+ #: platform (windows / linux / macOS).
75
+ CDYLIB_NAMES = ("pith_jpeg.dll", "libpith_jpeg.so", "libpith_jpeg.dylib")
76
+
77
+
78
+ class LibraryNotFoundError(OSError):
79
+ """No cdylib was found through the discovery chain."""
80
+
81
+
82
+ class FfiError(Exception):
83
+ """A non-zero status code came back from the cdylib."""
84
+
85
+ def __init__(self, op: str, status: int) -> None:
86
+ detail = {
87
+ STATUS_INVALID: "invalid argument",
88
+ STATUS_REJECTED: "input rejected",
89
+ }.get(status, "unknown failure")
90
+ super().__init__(f"{op} failed: {detail} (status {status})")
91
+ #: The raw status code the FFI returned.
92
+ self.status = status
93
+
94
+
95
+ def find_cdylib() -> Path:
96
+ """Locates the cdylib through the suite's discovery chain."""
97
+ explicit = os.environ.get("PITH_CDYLIB")
98
+ if explicit:
99
+ p = Path(explicit)
100
+ if p.is_file():
101
+ return p
102
+ env_dir = os.environ.get("PITH_CDYLIB_DIR")
103
+ candidates: list[Path] = []
104
+ if env_dir:
105
+ env_dir_path = Path(env_dir)
106
+ candidates.append(env_dir_path)
107
+ if not env_dir_path.is_absolute():
108
+ # CD and local runs invoke tools from the repository root or
109
+ # from sdk/<lang>; resolve the env value against both.
110
+ candidates.append(Path.cwd() / env_dir_path)
111
+ candidates.append(Path(__file__).resolve().parents[3] / env_dir_path)
112
+ candidates.append(Path(__file__).resolve().parent) # packaged wheel
113
+ candidates.append(Path(__file__).resolve().parents[3] / "target" / "release")
114
+ for directory in candidates:
115
+ for name in CDYLIB_NAMES:
116
+ p = directory / name
117
+ if p.is_file():
118
+ return p
119
+ raise LibraryNotFoundError(
120
+ "no pith-jpeg cdylib found (searched PITH_CDYLIB, PITH_CDYLIB_DIR, "
121
+ "the package directory and <repo>/target/release); "
122
+ "run `cargo build --release` first"
123
+ )
124
+
125
+
126
+ _lib: ctypes.CDLL | None = None
127
+
128
+
129
+ def _load() -> ctypes.CDLL:
130
+ global _lib
131
+ if _lib is None:
132
+ lib = ctypes.CDLL(str(find_cdylib()))
133
+ lib.pith_jpeg_decode.argtypes = [
134
+ ctypes.c_void_p, # data
135
+ ctypes.c_size_t, # len
136
+ ctypes.c_uint32, # layout
137
+ ctypes.POINTER(ctypes.c_uint64), # out digest
138
+ ]
139
+ lib.pith_jpeg_decode.restype = ctypes.c_int32
140
+ lib.pith_jpeg_channels.argtypes = [
141
+ ctypes.c_void_p, # data
142
+ ctypes.c_size_t, # len
143
+ ctypes.POINTER(ctypes.c_uint32), # out channels
144
+ ]
145
+ lib.pith_jpeg_channels.restype = ctypes.c_int32
146
+ for op in (
147
+ "pith_jpeg_dequant_zigzag",
148
+ "pith_jpeg_dequant",
149
+ "pith_jpeg_idct_islow",
150
+ "pith_jpeg_idct_oracle",
151
+ ):
152
+ fn = getattr(lib, op)
153
+ fn.argtypes = [
154
+ ctypes.c_void_p, # input buffer (LE f64 lanes)
155
+ ctypes.c_size_t, # input length in bytes
156
+ ctypes.c_void_p, # output buffer (LE f64 lanes)
157
+ ctypes.c_size_t, # output length in bytes
158
+ ]
159
+ fn.restype = ctypes.c_int32
160
+ _lib = lib
161
+ return _lib
162
+
163
+
164
+ def decode_digest(data: bytes, layout: int) -> int:
165
+ """Decodes a JPEG and returns the ``fnv1a64`` digest of the sample
166
+ bytes in ``layout`` encoding: row-major, channel-interleaved — the
167
+ exact value ``reference.json`` pins for the native layout.
168
+
169
+ Raises :class:`FfiError` with ``status == STATUS_REJECTED`` for
170
+ malformed input and for the refused RGB→gray conversion, and with
171
+ ``status == STATUS_INVALID`` for an unknown layout code.
172
+ """
173
+ digest = ctypes.c_uint64()
174
+ status = _load().pith_jpeg_decode(data, len(data), layout, ctypes.byref(digest))
175
+ if status != STATUS_OK:
176
+ raise FfiError("pith_jpeg_decode", status)
177
+ return digest.value
178
+
179
+
180
+ def channels(data: bytes) -> int:
181
+ """Returns the frame's native channel count (1 or 3) — the digest-
182
+ exact layout is :data:`LAYOUT_GRAY8` for 1 and :data:`LAYOUT_RGB8`
183
+ for 3."""
184
+ out = ctypes.c_uint32()
185
+ status = _load().pith_jpeg_channels(data, len(data), ctypes.byref(out))
186
+ if status != STATUS_OK:
187
+ raise FfiError("pith_jpeg_channels", status)
188
+ return out.value
189
+
190
+
191
+ def _replay(op: str, values: list[float], out_lanes: int) -> list[float]:
192
+ """Feeds ``values`` to one kernel op over little-endian ``f64``
193
+ bit-pattern buffers and returns the decoded output lanes."""
194
+ import struct
195
+
196
+ packed = b"".join(struct.pack("<d", v) for v in values)
197
+ out = (ctypes.c_ubyte * (out_lanes * 8))()
198
+ status = getattr(_load(), op)(
199
+ packed, len(packed), out, len(out)
200
+ )
201
+ if status != STATUS_OK:
202
+ raise FfiError(op, status)
203
+ raw = bytes(out)
204
+ return list(struct.unpack(f"<{out_lanes}d", raw))
205
+
206
+
207
+ def dequant_zigzag(payload: list[float]) -> list[float]:
208
+ """Replays the ``dequant.zigzag.8x8`` kernel: 64 DQT-payload
209
+ values in scan order in, the same values in natural (row-major)
210
+ order out (``natural[ZIGZAG[k]] = payload[k]``)."""
211
+ return _replay("pith_jpeg_dequant_zigzag", payload, BLOCK)
212
+
213
+
214
+ def dequant(coefs: list[float], qt: list[float]) -> list[float]:
215
+ """Replays the ``dequant.8x8`` kernel: 64 natural-order
216
+ coefficients and 64 quantizer values in, the 64 element-wise
217
+ products out."""
218
+ return _replay("pith_jpeg_dequant", coefs + qt, BLOCK)
219
+
220
+
221
+ def idct_islow(coefs: list[float], qt: list[float]) -> list[float]:
222
+ """Replays the ``idct.islow.8x8`` kernel: the same input layout as
223
+ :func:`dequant`, the 64 ``u8`` samples of the fixed-point islow
224
+ transcription out as ``f64`` lanes."""
225
+ return _replay("pith_jpeg_idct_islow", coefs + qt, BLOCK)
226
+
227
+
228
+ def idct_oracle(block: list[float]) -> list[float]:
229
+ """Replays the ``idct.oracle.8x8`` kernel: 64 dequantized
230
+ coefficients in, the raw orthonormal ``f64`` DCT-III oracle result
231
+ out. Platform-``libm``-shaped: compare within the recorded
232
+ tolerance, never bit-for-bit."""
233
+ return _replay("pith_jpeg_idct_oracle", block, BLOCK)
Binary file
@@ -0,0 +1,137 @@
1
+ Metadata-Version: 2.4
2
+ Name: pith-jpeg
3
+ Version: 0.1.1
4
+ Summary: pith-jpeg SDK: JPEG decoding and kernel replay through the pith-hash Rust cdylib
5
+ Author: pith-hash
6
+ License-Expression: MIT
7
+ Project-URL: Repository, https://github.com/pith-hash/pith-jpeg
8
+ Keywords: jpeg,phash,perceptual-hash,pith-hash,ffi,ctypes
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Operating System :: Microsoft :: Windows
12
+ Classifier: Operating System :: POSIX :: Linux
13
+ Classifier: Operating System :: MacOS
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Topic :: Multimedia :: Graphics
16
+ Classifier: Topic :: Security :: Cryptography
17
+ Requires-Python: >=3.9
18
+ Description-Content-Type: text/markdown
19
+
20
+ <p align="center">
21
+ <img src="https://pith-jpeg.n24q02m.com/logo.svg" alt="pith-jpeg" width="120">
22
+ </p>
23
+
24
+ <h1 align="center">pith-jpeg</h1>
25
+
26
+ <p align="center">
27
+ <strong>pith jpeg lane: baseline and progressive JPEG decoding, byte-exact with libjpeg islow (zero-dep Rust)</strong>
28
+ </p>
29
+
30
+ <p align="center">
31
+ <a href="https://github.com/pith-hash/pith-jpeg/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/pith-hash/pith-jpeg/actions/workflows/ci.yml/badge.svg"></a>
32
+ <a href="https://github.com/pith-hash/pith-jpeg/actions/workflows/cd.yml"><img alt="CD" src="https://github.com/pith-hash/pith-jpeg/actions/workflows/cd.yml/badge.svg"></a>
33
+ <a href="https://github.com/pith-hash/pith-jpeg/releases/latest"><img alt="Latest release" src="https://img.shields.io/github/v/release/pith-hash/pith-jpeg?display_name=tag&sort=semver"></a>
34
+ <a href="https://github.com/n24q02m/better-semantic-release"><img alt="semantic-release" src="https://img.shields.io/badge/semantic--release-e10079?logo=semantic-release&logoColor=white"></a>
35
+ <a href="LICENSE"><img alt="License: MIT" src="https://img.shields.io/badge/License-MIT-blue.svg"></a>
36
+ </p>
37
+
38
+ <p align="center">
39
+ <a href="#install">Install</a> ·
40
+ <a href="#quick-start">Quick start</a> ·
41
+ <a href="#the-pith-suite-contract">Suite contract</a>
42
+ </p>
43
+
44
+ <!-- BEGIN: AUTO-GENERATED-CROSS-PROMO -->
45
+ <!-- END: AUTO-GENERATED-CROSS-PROMO -->
46
+
47
+ ## The pith suite contract
48
+
49
+ pith-jpeg is part of the **pith** suite (pith-hash). Every suite repository
50
+ follows the same rules; CI enforces them mechanically:
51
+
52
+ - **Naming**: a library is always `pith-<domain>` (`pith-image`, `pith-audio`,
53
+ `pith-zip`, ...). The curator/repository of repositories is the bare
54
+ `pith-hash`. Never invent a second naming scheme inside the suite.
55
+ - **Version pinning**: cross-library dependencies pin `~0.1` (e.g.
56
+ `pith-image = { version = "~0.1", path = "../pith-image" }`). The whole suite
57
+ moves together inside 0.1.x; breaking changes require a suite-wide version
58
+ bump, never a silent minor drift.
59
+ - **Zero third-party dependencies**: every crate depends only on other
60
+ `pith-*` crates plus `std`. `scripts/check-zero-deps.py` (run in CI) fails
61
+ the build on any other crate, for normal, build and dev dependencies alike.
62
+ - **No unsafe**: every crate root carries `#![forbid(unsafe_code)]`.
63
+ - **Hex-exact vectors**: `reference.json` at the repo root is the
64
+ cross-language source of truth. The `gen-reference` binary regenerates it;
65
+ CI verifies the committed copy is current (`gen-reference verify`), and CD
66
+ ships the regenerated file with every SDK artifact. Python, Node and Go SDKs
67
+ MUST test against the same bytes.
68
+
69
+ ## Repository layout
70
+
71
+ ```
72
+ crates/ one published crate per suite lib (pith-<domain>)
73
+ tools/gen-reference the vector generator binary (bin name: gen-reference)
74
+ sdk/python ctypes wheel; build backend reads PITH_CDYLIB_DIR
75
+ sdk/node koffi-based package; prebuilds/<os-arch>/ carry the cdylib
76
+ sdk/go cgo binding; go.mod carries the module's cgo flags
77
+ fuzz/corpus fuzz inputs, replayed by tests/fuzz_corpus.rs (parser crates)
78
+ reference.json hex-exact cross-SDK test vectors
79
+ ```
80
+
81
+ ## Overview
82
+
83
+ JPEG decoding per ITU T.81: baseline (SOF0), extended sequential (SOF1)
84
+ and progressive (SOF2) Huffman-coded frames — spectral selection,
85
+ successive approximation and EOBRUN included — with restart markers,
86
+ 4:4:4/4:2:2/4:4:0/4:2:0 and other integral sampling factors, and 1- or
87
+ 3-component output as `pith_image::raster::Image` (`Gray`/`Rgb`, 8-bit).
88
+
89
+ The pixel pipeline is a byte-exact transcription of libjpeg-turbo's
90
+ `islow` integer IDCT, `fancy` triangle chroma upsampling and
91
+ fixed-point YCbCr→RGB, validated against Pillow 12.3.0 fixtures byte
92
+ for byte (see `tests/fixtures/PROVENANCE.md`). Arithmetic-coded,
93
+ lossless, differential, hierarchical, 12-bit and CMYK/YCCK streams are
94
+ refused with `Error::Unsupported` naming the coding process.
95
+
96
+ ## Install
97
+
98
+ Rust (the core library):
99
+
100
+ ```bash
101
+ cargo add pith-jpeg
102
+ ```
103
+
104
+ Python / Node / Go SDKs are published from the same cdylib on every release;
105
+ see the release assets or the package registries for the matching version.
106
+
107
+ ## Quick start
108
+
109
+ Rust (the core library):
110
+
111
+ ```rust
112
+ let img = pith_jpeg::decode(&bytes)?;
113
+
114
+ match img {
115
+ pith_jpeg::Jpeg::Gray(g) => println!("{}x{}", g.width(), g.height()),
116
+ pith_jpeg::Jpeg::Rgb(rgb) => println!("{}x{}", rgb.width(), rgb.height()),
117
+ }
118
+ ```
119
+
120
+ `decode` never panics on untrusted input: malformed structure surfaces
121
+ as `Error::Truncated` / `Error::BadValue`, refusals as
122
+ `Error::Unsupported`. Hex-exact cross-SDK vectors — decode digests for
123
+ every committed fixture plus the DCT/dequant numerics — live in
124
+ `reference.json` (regenerate with `cargo run --bin gen-reference`,
125
+ verify with `cargo run --bin gen-reference -- verify`).
126
+
127
+ ## Contributing
128
+
129
+ See [CONTRIBUTING.md](CONTRIBUTING.md).
130
+
131
+ ## Security
132
+
133
+ See [SECURITY.md](SECURITY.md).
134
+
135
+ ## License
136
+
137
+ [MIT](LICENSE) © pith-hash
@@ -0,0 +1,6 @@
1
+ pith_jpeg/__init__.py,sha256=9x9V6F47bUciIiEaJ1z-R94WnWWaKURb12FPDGha3JY,8337
2
+ pith_jpeg/pith_jpeg.dll,sha256=6yvwQQZOu9j2oI0HrElBem67VfRtwfFdCqmfku6QkPU,184832
3
+ pith_jpeg-0.1.1.dist-info/METADATA,sha256=rBNaKHTpPvoADYK3PSAdWXb9apPdDrbddb1sc3Fe58I,5851
4
+ pith_jpeg-0.1.1.dist-info/WHEEL,sha256=Zx98gwb_dQKckJK3HEOVKnxxD52EfU_PXDG_usMJ2ng,98
5
+ pith_jpeg-0.1.1.dist-info/top_level.txt,sha256=kbWDcEQWIvue9olhdeuUxmjIDsNM4oFqmK4xpcm8gCc,10
6
+ pith_jpeg-0.1.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: false
4
+ Tag: py3-none-win_amd64
5
+
@@ -0,0 +1 @@
1
+ pith_jpeg