ScanSlicer/AGENTS.md
dan 5f71b61596 docs: finalize README, attribute fork, sync AGENTS.md
- Rewrite README for fork context, add changes-from-upstream section,
  prune sections duplicated by docs/, surface AppImage as preferred run
- Add fork maintainer alongside upstream author in pyproject.toml
- Sync AGENTS.md with completed patch and deliberate supply-chain decisions
2026-09-11 23:24:54 +02:00

152 lines
7.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ScanSlicer (fork) — Project Guide
Purpose of this file: give any LLM working in this repo enough context to be
effective without re-reading everything. Keep it current as the code changes.
## What this project is
ScanSlicer is a desktop Qt application (Python/PySide6) that detects multiple
photos in a flatbed scan image, lets the user correct each box manually, then
exports perspective-corrected crops. Inspired by
[PhotoSlicer](https://github.com/francescovannini/photoslicer) (Francesco
Vannini). Runs on Linux over Wayland or X11.
This repo is a **local fork** of
[macrodev8/ScanSlicer](https://github.com/macrodev8/ScanSlicer) (upstream owner
inactive). License: GNU GPL v3 (must be preserved). Attribution: keep upstream
credit and the "inspired by PhotoSlicer" note; `pyproject.toml` lists both the
fork maintainer and the upstream author, and `README.md` states the fork
relationship and the changes made.
## Goals of this fork
1. Fix the mirrored-export orientation bug and add an explicit left/right
orientation workflow. Implemented, tested in the GUI, and committed — see
"The orientation fix" below. This was the primary patch.
2. Keep the app usable on the maintainer's own photo scans; afterwards only
minimal maintenance is expected.
3. Push to the maintainer's Forgejo instance at `code.dgerdesmann.de` (no
GitHub fork). Keep licensing, attribution, and repository description proper.
The maintainer is primarily an R user and new to Python/Git. Changes should be
small, self-checked, clean, and secure; do not assume the maintainer can vet
Python code.
## The orientation fix (implemented)
The bug: exported slices could come out mirrored (left/right swapped). Root
cause is `PhotoSlice.normalize_top_left()` (in `slices.py`), which re-derives
"top" from geometry — it picks a top-left corner and resolves winding so edge
0→1 is the "most horizontal" top edge. That inference is ambiguous, and because
`squarize()` and `reset_rotation()` both re-run `normalize_top_left()` at the
end, any manual correction was silently overwritten.
The fix adds an explicit left/right orientation workflow:
- `PhotoSlice.swapped` — persistent flag (default `False`): "user has explicitly
mirrored left/right".
- `PhotoSlice.swap_left_right()` — toggles `swapped` and reindexes the corners
`[TL,TR,BR,BL] -> [TR,TL,BL,BR]` (i.e. `bbox[[1,0,3,2]]`), a pure left/right
mirror that keeps the top (edge 0→1) and bottom edges in place. NOTE: the
earlier `[0,3,2,1]` reindex was wrong — it rotated the quad 90°.
- `normalize_top_left()` applies the same `[1,0,3,2]` reindex when `swapped` is
true (after its geometric inference), so manual orientation survives
`squarize()` / `reset_rotation()`.
- "Swap L/R" toolbar button (`ui_builders_preview.py`) ->
`CanvasStateMixin.swap_active_slice_left_right()` ->
`ScanSlicerCanvasActionsMixin.swap_active_box_left_right()`.
- Visual indication in `CanvasRenderMixin.paintEvent()` (`canvas_render.py`):
corners color-coded left = orange, right = cyan; edges colored red = top
(0→1), green = right (1→2), orange = bottom (2→3), and the left edge (3→0)
follows lock state (blue locked / lightgreen unlocked).
## Key implementation facts
- `PhotoSlice.bbox` is a 4×2 numpy array in the order
`[top-left, top-right, bottom-right, bottom-left]`.
- `Autoslicer.save_slice()` (in `autoslicer.py`) builds the perspective
transform assuming the `[TL,TR,BR,BL]` order. Do NOT change that contract
without updating every consumer.
- `PhotoSlice.squarize()` and `reset_rotation()` call `normalize_top_left()` at
the end; the `swapped` flag makes that safe for manual orientation.
## Repository structure
```
scanslicer/ application source (package)
entry.py CLI parsing, Wayland/X11 backend selection
main.py QtScanSlicer main window, theme, app bootstrap
controller*.py UI action handlers (status, canvas, detection,
image I/O, params)
ui_builders*.py widget construction (controls, preview, shared)
canvas*.py interactive canvas (interaction/render/state mixins)
slices.py PhotoSlice geometry model + polys_iou
autoslicer.py detection/export engine wrapper (save_slice)
autoslicer_detection.py detection pipelines, contour→box extraction
autoslicer_feedback.py auto-tune profiles, scoring, feedback persistence
params.py parameter model, defaults, presets
packaging/appimage/ AppImage recipe + build scripts (preferred deploy)
Dockerfile container runtime image
docker-compose.yml GUI container wiring
pyproject.toml package metadata + dependencies (uv.lock pinned)
docs/ user-guide, architecture, parameters, autotune,
runtime-config, appimage, development
```
## Architecture in brief
- `QtScanSlicer` (`main.py`) is a `QMainWindow` composed from UI builder mixins
and controller mixins.
- `QtSlicingCanvas` (`canvas.py`) = interaction + render + state mixins; holds
the image, zoom/pan state, and the list of `PhotoSlice` objects.
- Background work (detection, auto-tune) runs on a single-worker `QThreadPool`;
results are delivered back to the UI via Qt signals.
- Export flow: `save_all()` (`controller_image_io.py`) loops over locked slices
and calls `Autoslicer.save_slice(bbox, path, params)` for each.
## Run / build
```bash
uv sync --frozen
uv run scanslicer /path/to/scans # or: python -m scanslicer
```
- AppImage (preferred): `bash packaging/appimage/build-appimage.sh`
- AppImage via Docker: `bash packaging/appimage/build-appimage-docker.sh`
- Docker runtime: `docker compose up --build`
- Dependencies (from `pyproject.toml`): PySide6, opencv-python-headless, numpy.
- Python >= 3.10; dev env is Python 3.12 in `.venv`.
Dependency / supply-chain decisions (deliberate — keep them):
- `opencv-python-headless` is intentional: the app uses no OpenCV GUI/highgui.
- Pillow was removed (it was unused). Don't re-add without a concrete need.
- The AppImage build exports hashes from `uv.lock` and installs with
`pip install --require-hashes`.
### AppImage build: Docker context gotcha
The maintainer's Docker points at Docker Desktop (`desktop-linux` context). Its
bind-mount filesystem forbids `chown`, but `appimage-builder` needs root
ownership while extracting `.deb` packages, so the build fails with
`dpkg-deb ... Cannot change ownership to uid 0, gid 0: Operation not permitted`.
Workaround: run the Dockerized build against the native daemon (or switch
context with `docker context use default`):
```bash
DOCKER_HOST=unix:///var/run/docker.sock bash packaging/appimage/build-appimage-docker.sh
```
Output lands in `appimage/dist/ScanSlicer-<version>-x86_64.AppImage` (the
`appimage/` dir is git-ignored).
## Config / persistence
Runtime files live in `~/.config/scanslicer/` (or `$XDG_CONFIG_HOME/scanslicer/`)
by default: `defaults.json`, `presets.json`, `autotune-feedback.json`,
`status.log`. Paths are overridable via `SCANSLICER_*_PATH` env vars. In the
Docker setup these land in `./.scanslicer/` on the host.
## Notes
- No automated test suite exists; validate changes by running the app on sample scans (startup, detection, save, auto-tune).