Metadata-Version: 2.4
Name: ll-descr
Version: 0.1.0
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Rust
Classifier: Topic :: Multimedia :: Graphics
License-File: LICENSE
Summary: Lossless JPEG descrambling via DCT coefficient block permutation
Keywords: jpeg,lossless,descramble,dct
License: GPL-3.0-or-later
Requires-Python: >=3.9
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM

# ll-descr

Lossless JPEG descrambling via DCT coefficient block permutation. Moves 8x8
DCT blocks without re-encoding pixels, so untouched regions stay bit-exact.
An optional lossless crop drops out-of-range MCUs and rewrites the SOF
dimensions — a `jpegtran`-style transform with no re-encoding.

License: GPL-3.0-or-later. See `LICENSE`.

## Install

```bash
pip install ll-descr
```

From source (requires a Rust toolchain):

```bash
pip install maturin
maturin build --release
pip install target/wheels/*.whl
```

Requires Python >= 3.9.

## Usage

```python
from ll_descr import descramble, descramble_file, jpeg_info

raw = open("scrambled.jpg", "rb").read()
print(jpeg_info(raw))  # {"width": ..., "height": ..., "num_components": ..., "samplings": [...]}

# One convention for moves: output[src] = input[dest], all values in pixels.
moves = [
    {"srcX": 0, "srcY": 0, "destX": 128, "destY": 0, "width": 64, "height": 64},
    {"srcX": 128, "srcY": 0, "destX": 0, "destY": 0, "width": 64, "height": 64},
]

out = descramble(raw, moves=moves)                      # permute only
out = descramble(raw, moves=moves, width=800, height=1200)  # permute + lossless crop
out = descramble(raw, width=800, height=1200)           # crop only
out = descramble(raw)                                   # passthrough copy

descramble_file("scrambled.jpg", "out.jpg", moves=moves, width=800, height=1200)
```

Accepted move forms (mix freely):

* mappings with `srcX/srcY/destX/destY/width/height` (`snake_case` aliases work)
* 6-tuples `(src_x, src_y, dest_x, dest_y, width, height)`
* attribute objects (`namedtuple`, dataclass, `SimpleNamespace`, ...)
* a JSON string encoding a list of any of the above

Other notes:

* `moves=None` or `[]` with no `width`/`height` returns a copy of the input
  without parsing it.
* `width` and `height` must both be given or both be `None`; targets must not
  exceed the JPEG dimensions.
* Out-of-bounds blocks are skipped by default; pass `strict=True` to raise.
* Errors raise `ValueError`.
* The GIL is released during processing, so concurrent calls scale across threads.

## Developing

```bash
pip install maturin Pillow pytest
maturin develop --release   # builds and installs into the active venv
pytest -q
```

## Publishing a release

Versions live in three places — keep them in sync:

1. `Cargo.toml` (`version = "..."`)
2. `pyproject.toml` (`version = "..."`)
3. `python/ll_descr/__init__.py` (`__version__ = "..."`)

Then:

```bash
git tag v0.1.0 && git push --tags   # or cut a GitHub Release
```

Pushing a tag / publishing a GitHub Release triggers the `CI` workflow, which
runs the test suite, builds wheels for CPython 3.9–3.13 on Linux (manylinux),
Windows, and macOS (arm64 + x86_64), builds the sdist, and uploads everything
to PyPI. First-time setup requires configuring a trusted publisher for the
package on PyPI (or, alternatively, a `PYPI_API_TOKEN` secret — see
`.github/workflows/ci.yml`).

Manual publish (e.g. to TestPyPI first):

```bash
maturin build --release
maturin sdist
twine check target/wheels/*
twine upload --repository testpypi target/wheels/*
twine upload target/wheels/*
```

## Building your own scheme on top

Compute tile coordinates however you like (grids, seeds, PRNGs, config files),
translate them into move dicts following `output[src] = input[dest]`, and pass
them in. Use `jpeg_info` to size tiles from the real frame dimensions and
sampling factors.

