from __future__ import annotations
import ctypes
import os
from pathlib import Path
__all__ = [
"FfiError",
"LibraryNotFoundError",
"find_cdylib",
"decode_digest",
"channels",
"dequant_zigzag",
"dequant",
"idct_islow",
"idct_oracle",
"STATUS_OK",
"STATUS_INVALID",
"STATUS_REJECTED",
"LAYOUT_GRAY8",
"LAYOUT_RGB8",
"LAYOUT_RGBA8",
"BLOCK",
]
STATUS_OK = 0
STATUS_INVALID = -1
STATUS_REJECTED = -2
LAYOUT_GRAY8 = 0
LAYOUT_RGB8 = 2
LAYOUT_RGBA8 = 4
BLOCK = 64
CDYLIB_NAMES = ("pith_jpeg.dll", "libpith_jpeg.so", "libpith_jpeg.dylib")
class LibraryNotFoundError(OSError):
class FfiError(Exception):
def __init__(self, op: str, status: int) -> None:
detail = {
STATUS_INVALID: "invalid argument",
STATUS_REJECTED: "input rejected",
}.get(status, "unknown failure")
super().__init__(f"{op} failed: {detail} (status {status})")
self.status = status
def find_cdylib() -> Path:
explicit = os.environ.get("PITH_CDYLIB")
if explicit:
p = Path(explicit)
if p.is_file():
return p
env_dir = os.environ.get("PITH_CDYLIB_DIR")
candidates: list[Path] = []
if env_dir:
env_dir_path = Path(env_dir)
candidates.append(env_dir_path)
if not env_dir_path.is_absolute():
candidates.append(Path.cwd() / env_dir_path)
candidates.append(Path(__file__).resolve().parents[3] / env_dir_path)
candidates.append(Path(__file__).resolve().parent) candidates.append(Path(__file__).resolve().parents[3] / "target" / "release")
for directory in candidates:
for name in CDYLIB_NAMES:
p = directory / name
if p.is_file():
return p
raise LibraryNotFoundError(
"no pith-jpeg cdylib found (searched PITH_CDYLIB, PITH_CDYLIB_DIR, "
"the package directory and <repo>/target/release); "
"run `cargo build --release` first"
)
_lib: ctypes.CDLL | None = None
def _load() -> ctypes.CDLL:
global _lib
if _lib is None:
lib = ctypes.CDLL(str(find_cdylib()))
lib.pith_jpeg_decode.argtypes = [
ctypes.c_void_p, ctypes.c_size_t, ctypes.c_uint32, ctypes.POINTER(ctypes.c_uint64), ]
lib.pith_jpeg_decode.restype = ctypes.c_int32
lib.pith_jpeg_channels.argtypes = [
ctypes.c_void_p, ctypes.c_size_t, ctypes.POINTER(ctypes.c_uint32), ]
lib.pith_jpeg_channels.restype = ctypes.c_int32
for op in (
"pith_jpeg_dequant_zigzag",
"pith_jpeg_dequant",
"pith_jpeg_idct_islow",
"pith_jpeg_idct_oracle",
):
fn = getattr(lib, op)
fn.argtypes = [
ctypes.c_void_p, ctypes.c_size_t, ctypes.c_void_p, ctypes.c_size_t, ]
fn.restype = ctypes.c_int32
_lib = lib
return _lib
def decode_digest(data: bytes, layout: int) -> int:
digest = ctypes.c_uint64()
status = _load().pith_jpeg_decode(data, len(data), layout, ctypes.byref(digest))
if status != STATUS_OK:
raise FfiError("pith_jpeg_decode", status)
return digest.value
def channels(data: bytes) -> int:
out = ctypes.c_uint32()
status = _load().pith_jpeg_channels(data, len(data), ctypes.byref(out))
if status != STATUS_OK:
raise FfiError("pith_jpeg_channels", status)
return out.value
def _replay(op: str, values: list[float], out_lanes: int) -> list[float]:
import struct
packed = b"".join(struct.pack("<d", v) for v in values)
out = (ctypes.c_ubyte * (out_lanes * 8))()
status = getattr(_load(), op)(
packed, len(packed), out, len(out)
)
if status != STATUS_OK:
raise FfiError(op, status)
raw = bytes(out)
return list(struct.unpack(f"<{out_lanes}d", raw))
def dequant_zigzag(payload: list[float]) -> list[float]:
return _replay("pith_jpeg_dequant_zigzag", payload, BLOCK)
def dequant(coefs: list[float], qt: list[float]) -> list[float]:
return _replay("pith_jpeg_dequant", coefs + qt, BLOCK)
def idct_islow(coefs: list[float], qt: list[float]) -> list[float]:
return _replay("pith_jpeg_idct_islow", coefs + qt, BLOCK)
def idct_oracle(block: list[float]) -> list[float]:
return _replay("pith_jpeg_idct_oracle", block, BLOCK)