# 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. ## Goals of this fork 1. Fix the mirrored-export orientation bug and add an explicit left/right orientation workflow (implemented, see below). This is 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`. ### 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--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).