ScanSlicer/MAINTENANCE.md
dan c8194742d1 docs: add maintenance guide; fix AppImage version selection; bump to 1.1.0
- Add MAINTENANCE.md (dependency updates, EOL watch, build/smoke-test
  workflows, orientation-fix invariants)
- Remove AGENTS.md; fold its unique context into MAINTENANCE.md
- Add .venv/.posit/appimage to .dockerignore to shrink build context
- Fix build-appimage.sh to select the wheel matching pyproject.toml
  version instead of alphabetically-first (bundled stale 1.0.0 code)
- Bump version 1.0.0 -> 1.1.0 in pyproject.toml, uv.lock, AppImage recipe
2026-09-12 15:11:07 +02:00

10 KiB
Raw Permalink Blame History

Maintaining ScanSlicer

This file is a how-to for keeping the fork alive over time. The goals are narrow and deliberate:

  1. Keep the app functional for its one real job (detect photos in a flatbed scan, correct the boxes, export straightened crops).
  2. Avoid security issues and breakage from end-of-life (EOL) components.

There is no ambition to develop new features. Maintenance is manual: when something drifts out of date, you bump a version, re-verify the app, and commit. Do not add features, refactor, or "improve" things while doing maintenance — small, focused changes only.

Scope guard: this repo is a local fork of macrodev8/ScanSlicer, published at https://code.dgerdesmann.de/dan/ScanSlicer.git. License is GPL v3. Keep the upstream credit and the "inspired by PhotoSlicer" note intact. See README.md and docs/ for project context.


Where things live

Component File(s)
Package metadata + dependency constraints pyproject.toml
Pinned dependency versions (source of truth) uv.lock
Runtime container image Dockerfile (ubuntu:24.04 + uv)
AppImage recipe + build scripts packaging/appimage/
Bundled third-party license notice THIRD_PARTY_NOTICES
Dev env (Python 3.12) .venv/ (managed by uv)

Dependency management is done with uv. pyproject.toml declares the requirements (mostly loose), and uv.lock pins the exact versions that get installed and shipped. uv.lock is committed — do not edit it by hand; let uv rewrite it.


Code invariants (orientation fix + bbox order)

This fork's one real code change is the mirrored-export orientation fix. Keep these invariants in mind when touching geometry code.

  • PhotoSlice.bbox is a 4×2 numpy array in the order [top-left, top-right, bottom-right, bottom-left]. Autoslicer.save_slice() (autoslicer.py) builds the perspective transform assuming that order. Do NOT change this contract without updating every consumer.
  • PhotoSlice.swapped (default False) is the persistent "user explicitly mirrored left/right" flag. swap_left_right() toggles it and reindexes corners [TL,TR,BR,BL] -> [TR,TL,BL,BR] (bbox[[1,0,3,2]]) — a pure left/right mirror that keeps the top and bottom edges in place.
  • Root cause of the original bug: normalize_top_left() (slices.py) re-derives "top" from geometry, which is ambiguous, and squarize() / reset_rotation() both re-run it at the end, silently overwriting manual corrections. The fix makes normalize_top_left() apply the same [1,0,3,2] reindex when swapped is true, so manual orientation survives. (Historical: an earlier [0,3,2,1] reindex was wrong — it rotated the quad 90°.)
  • UI wiring: "Swap L/R" button (ui_builders_preview.py) → CanvasStateMixin.swap_active_slice_left_right() (canvas_state.py) → ScanSlicerCanvasActionsMixin.swap_active_box_left_right() (controller_canvas_actions.py) → PhotoSlice.swap_left_right().
  • Visual color coding (canvas_render.py): corners — left = orange, right = cyan; edges — red = top (0→1), green = right (1→2), orange = bottom (2→3), left edge (3→0) follows lock state (blue locked / lightgreen unlocked).

Current pinned versions (snapshot)

Update this table whenever you bump something, so the next session has a starting point.

Component Pinned version Where
PySide6 / PySide6-Addons / -Essentials / shiboken6 6.10.2 uv.lock
opencv-python-headless 4.13.0.92 uv.lock (constraint ~=4.13.0 in pyproject.toml)
numpy 2.2.6 (Py<3.11) / 2.4.2 (Py≥3.11) uv.lock
Python (dev env) 3.12 .venv
Python floor >=3.10 pyproject.toml
uv (both Dockerfiles) 0.10.2 Dockerfile, packaging/appimage/Dockerfile
appimage-builder 1.1.0 packaging/appimage/Dockerfile
AppImage base OS Ubuntu noble (24.04) packaging/appimage/AppImageBuilder.yml
Runtime image base Ubuntu 24.04 Dockerfile

What can go stale, and the priority order

Review these roughly in this order — the first items are the real security/EOL surface; the last ones are packaging plumbing that only matters at build time.

  1. PySide6 / Qt — the largest attack surface and the component with the most aggressive EOL cadence. Qt 6.x minor releases stop receiving updates quickly; PySide6 tracks Qt. Keep this on a recent 6.x.
  2. opencv-python-headless — image processing; bump to the latest 4.x.
  3. numpy — bump to the latest 2.x.
  4. Python — the shipped runtime is what matters. The AppImage bundles its own Python (from Ubuntu noble, currently 3.12), so the app does not run on the host's Python. The requires-python = ">=3.10" floor only affects development resolution. Watch Python 3.10's EOL: when it goes EOL, raise the floor (e.g. to >=3.11) so no EOL interpreter is ever considered valid.
  5. Packaging toolchain — uv and appimage-builder versions pinned in the two Dockerfiles. Only bump these when you have a reason (build breaks, or a notable security fix in the tool itself).
  6. Ubuntu base images — noble is an LTS (supported to ~2029). Revisit the apt source and GPG key in AppImageBuilder.yml and the FROM line in both Dockerfiles when noble approaches EOL.

Suggested cadence: check PyPI/upstream releases every ~3–6 months, or promptly when a security advisory is published for any of the above.


Updating Python dependencies

All commands run from the repo root. The dev env is .venv (Python 3.12).

1. Bring the environment up to the committed lock (baseline)

uv sync --frozen

2. Bump a single dependency to its latest

uv lock --upgrade-package PySide6
uv lock --upgrade-package opencv-python-headless
uv lock --upgrade-package numpy

--dry-run previews the change without touching anything:

uv lock --upgrade-package PySide6 --dry-run

To bump everything at once (use sparingly — harder to bisect if something breaks):

uv lock --upgrade

3. Install the updated set and verify

uv sync
uv run scanslicer /path/to/test/scans

Then run the smoke-test checklist below.

Special case: opencv major/minor bump

opencv-python-headless is the only dependency with a constraint in pyproject.toml (~=4.13.0), which pins it to the 4.13.x line. To move to 4.14+, edit that constraint in pyproject.toml first, then run uv lock --upgrade-package opencv-python-headless. PySide6 and numpy are unpinned in pyproject.toml, so uv can move them freely.

After any version change

  • Commit pyproject.toml (if edited) and uv.lock together.
  • Update the version table above.
  • Update THIRD_PARTY_NOTICES — it lists concrete PySide6/Qt, numpy, and opencv versions, and it ships inside every AppImage. Keep it aligned with uv.lock.
  • Rebuild and smoke-test the AppImage if the runtime stack (PySide6/Qt) changed.

Updating the AppImage packaging toolchain

The AppImage build pins its own tool versions in packaging/appimage/Dockerfile:

  • uv (COPY --from=ghcr.io/astral-sh/uv:0.10.2 ...)
  • appimage-builder==1.1.0 (an ARG near the top)

Bump either by editing that file. If you change appimage-builder, the AppImageBuilder.yml recipe may need adjusting for new versions (rare). The Ubuntu apt source and GPG key live in AppImageBuilder.yml — update those when noble goes EOL or the key rotates.

Hardcoded Python paths: AppImageBuilder.yml hardcodes python3.12 in QT_PLUGIN_PATH and LD_LIBRARY_PATH. If the AppImage base OS ever moves to a different Python minor, those two lines must be updated to match.


Updating the runtime Docker image

Dockerfile (repo root) pins the base (ubuntu:24.04) and uv 0.10.2. Bump these when needed, then rebuild and run the container smoke test:

docker compose up --build

Verifying a change (smoke-test checklist)

There is no automated test suite. After any dependency or packaging change, manually confirm the app still works:

  1. Startup on both Wayland and X11:
    uv run scanslicer --backend wayland
    uv run scanslicer --backend x11
    
  2. Detection on a representative sample scan (boxes appear on load/Detect).
  3. Save flow — lock a box, export, confirm the output is straight and not mirrored (this fork's orientation fix is the thing most likely to regress).
  4. Auto-tune — run a profile and confirm feedback is written/read without error.
  5. If the runtime stack changed: rebuild the AppImage and confirm it launches and saves.
  6. If the Docker image changed: docker compose up --build and confirm the app opens.

The Docker gotcha (recap — keep this in mind)

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 Dockerized AppImage build fails with:

dpkg-deb ... Cannot change ownership to uid 0, gid 0: Operation not permitted

Workaround: run the build against the native daemon instead:

DOCKER_HOST=unix:///var/run/docker.sock bash packaging/appimage/build-appimage-docker.sh

(or docker context use default first). Output lands in appimage/dist/ScanSlicer-<version>-x86_64.AppImage (the appimage/ dir is git-ignored).


Commit / release hygiene

  • Commit uv.lock and pyproject.toml (and THIRD_PARTY_NOTICES when applicable) together as a single change.
  • Write a short, factual commit message, e.g. Bump PySide6 to 6.10.x.
  • Push to the Forgejo remote: git push origin.
  • The AppImage build verifies package hashes (--require-hashes) from uv.lock; publishing the AppImage alongside the matching committed source is already handled by committing before building.

One-line summary

Bump PySide6/Qt first and most often; bump opencv and numpy when convenient; treat uv/appimage-builder/Ubuntu as build plumbing you only touch when necessary. After every change, run the smoke-test checklist, rebuild the AppImage if the runtime stack moved, and commit uv.lock + pyproject.toml together.