Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 64 additions & 51 deletions CLAUDE.md

Large diffs are not rendered by default.

142 changes: 62 additions & 80 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,28 +1,25 @@
# Contributing to NegPy

Thank you for your interest in contributing to **NegPy**!
## Claiming an Issue

## 🙋 Claiming an Issue
Contributors with read access cannot use GitHub's assignee UI, so comment on the issue:

Contributors with read access can't use GitHub's assignee UI, so NegPy lets you
self-assign by commenting on an issue:
- `/assign`: assign the issue to yourself
- `/unassign`: remove yourself from the issue

- `/assign` — assign the issue to yourself
- `/unassign` — remove yourself from the issue
A 👀 reaction on your comment confirms that the command ran.

A 👀 reaction on your comment confirms the command ran.
## Development Setup

## 🛠️ Development Setup

NegPy requires **Python 3.13+**. We use **uv** for environment and dependency management.
NegPy requires Python 3.13+ and `uv`.

### 1. Prerequisites
Install [uv](https://docs.astral.sh/uv/getting-started/installation/) if you haven't already.
Install [uv](https://docs.astral.sh/uv/getting-started/installation/).

**Scanner support (optional):**
Scanner support (optional):

- **SANE** (Linux/macOS) — Coolscans and other SANE film scanners. Install the system library, then the `sane` group (`uv sync --group sane` or `pip install negpy[sane]`):
- **Linux** (Debian/Ubuntu):
- **SANE** (Linux/macOS): Coolscans and other SANE film scanners. Install the system library, then the `sane` group (`uv sync --group sane` or `pip install negpy[sane]`):
- **Linux**:
```bash
sudo pacman -S sane # arch
sudo apt install libsane-dev # debian/ubuntu
Expand All @@ -32,13 +29,12 @@ Install [uv](https://docs.astral.sh/uv/getting-started/installation/) if you hav
brew install sane-backends
```

- **Plustek USB** (Windows/macOS/Linux) — optional [pyopticfilm](https://github.com/jboneng/pyopticfilm) driver (8200i SE and 8100 V2): `uv sync --group plustek` or `pip install negpy[plustek]`. Windows installs `libusb-package` via pyopticfilm (bundled in release builds). On Windows, bind WinUSB with Zadig for the scanner's USB id (`07b3:1825` 8200i SE, `07b3:1824` 8100 V2) before scanning (vendor/SilverFast drivers conflict). See [docs/PLUSTEK_WINDOWS.md](docs/PLUSTEK_WINDOWS.md).
- **Plustek USB** (Windows/macOS/Linux): the [pyopticfilm](https://github.com/jboneng/pyopticfilm) driver: `uv sync --group plustek` or `pip install negpy[plustek]`. On Windows, pyopticfilm installs `libusb-package`, and the scanner needs WinUSB bound with Zadig in place of the vendor/SilverFast driver (`07b3:1825` 8200i SE, `07b3:1824` 8100 V2). See [docs/PLUSTEK_WINDOWS.md](docs/PLUSTEK_WINDOWS.md).

- **Nikon Coolscan** (Windows/macOS/Linux) — [nkscan](https://github.com/activexray/nkscan) 0.12 or newer, shipped as wheels: `uv sync --group nkscan` or `pip install negpy[nkscan]`. On Linux, USB needs a udev rule for vendor `04b0`; FireWire/SCSI needs the `sg` module.
- **Nikon Coolscan** (Windows/macOS/Linux): [nkscan](https://github.com/activexray/nkscan) 0.12 or newer, shipped as wheels: `uv sync --group nkscan` or `pip install negpy[nkscan]`. On Linux, USB needs a udev rule for vendor `04b0`, and FireWire/SCSI needs the `sg` module.


### 2. Python Environment
The `Makefile` handles synchronization via `uv`. Run this to set up your environment:

```bash
make install
Expand All @@ -54,43 +50,37 @@ make run
#### A separate user directory for development

NegPy keeps its databases, caches, presets, logs and `override.toml` in one user
directory — `Documents/NegPy` by default. If you also use NegPy for real work, point
your development builds at a different directory, so that test edits and stale caches
cannot touch the real one.

Set `NEGPY_USER_DIR` to an absolute path. The `Makefile` reads an optional, gitignored
`.env.local`:
directory, `Documents/NegPy` by default. If you also use NegPy for real work, give your
development builds their own directory: set `NEGPY_USER_DIR` to an absolute path in an
optional, gitignored `.env.local`, which the `Makefile` reads:

```make
# .env.local
NEGPY_USER_DIR = $(HOME)/negpy-devhome
```

`make run` then uses that directory, and creates it if it is missing. Without the file,
nothing changes.
`make run` then uses (and creates) that directory.

Write `$(HOME)`, not `~` or `$HOME`: make leaves `~` alone, and reads `$HOME` as the
variable `H` followed by `OME`. Either one resolves to a path inside the checkout, so
make stops and tells you when the value is not absolute. Note that the path must also
be right for the platform — `/home/you/...` is a Linux path, and macOS puts your home
under `/Users/you`.
Write `$(HOME)`, not `~` or `$HOME`: make leaves `~` alone and reads `$HOME` as `$H`
followed by `OME`. make stops when the value is not absolute. On macOS your home is under
`/Users/you`, not `/home/you/...`.

To start again with no saved edits and no caches:
To reset it:

```bash
make clear-devhome
```

The target deletes the whole directory after a confirmation (`FORCE=1` skips it), and
refuses to run if `NEGPY_USER_DIR` is unset or points at the default directory. An
`override.toml` you rely on lives in there too, so keep a copy to put back.
This deletes the whole directory after a confirmation (`FORCE=1` skips it). It refuses to
run if `NEGPY_USER_DIR` is unset or points at the default directory. Keep a copy of any
`override.toml` you rely on.

Two things stay outside the user directory:

- `.negpy` sidecars, which are written next to the source images. A sidecar written by a
development build is promoted into the database of whichever install opens that image
next. Sidecar export is off by default; if you turn it on, test against copies.
- Exports, wherever you send them.
- `.negpy` sidecars next to the source images. The next install that opens an image
promotes its sidecar into its database. Sidecar export is off by default; if you turn it
on, test against copies.
- Exports.

#### Running without scanning hardware

Expand All @@ -100,56 +90,50 @@ make run-sim

Starts the app with a simulated camera, Scanlight and film scanners
(`NEGPY_SIMULATE_HARDWARE=1`). Only the gphoto2 module, the serial port and one scanner
backend are fake; the drivers, workers and panels above them are the real code.
backend are fake.

- **Camera**: frames are exposed from a synthetic negative under the Scanlight's color and
the body's shutter and ISO, and saved as Bayer DNGs. Put the calibration ROI on the clear
base band around the picture.
- **Camera**: exposes frames from a synthetic negative under the Scanlight's color and the
body's shutter and ISO, saved as Bayer DNGs. Put the calibration ROI on the clear base
band around the picture.
- **Scanlight**: a v4b that answers the firmware query and reports LED temperature.
- **Film Scanner**: the "Simulated" backend has a feeder (frames by index, IR, Eject), a
prescan device (full-window preview, then crop) and a roll device (frames found on the
strip).

## 🏗️ Project Structure

The codebase follows a modular architecture:
## Project Structure

- `negpy/domain/`: Core data models, types, and interfaces.
- `negpy/features/`: Image processing logic implementations (Exposure, Geometry, Lab, etc.).
- `negpy/infrastructure/`: Low-level system implementations (GPU resources, file loaders).
- `negpy/kernel/`: Core system services (Logging, Config, caching).
- `negpy/services/`: High-level orchestration (Rendering engine, Export service).
- `negpy/desktop/`: PyQt6 UI implementation (View, Controller, Workers).
- `tests/`: Unit and integration tests.
- `negpy/domain/`: core data models, types and interfaces.
- `negpy/features/`: image processing logic (Exposure, Geometry, Lab, etc.).
- `negpy/infrastructure/`: low-level system code (GPU resources, file loaders).
- `negpy/kernel/`: core system services (Logging, Config, caching).
- `negpy/services/`: high-level orchestration (Rendering engine, Export service).
- `negpy/desktop/`: PyQt6 UI (View, Controller, Workers).
- `tests/`: unit and integration tests.

## 📐 Coding Standards
## Coding Standards

**Always run `make format` before committing.**
Always run `make format` before committing.

### 1. Style & Formatting
- **Ruff**: Used for both linting and formatting.
- **Type Hints**: Required for all new function definitions (`ty` is enforced). Using `cast` to get around it is frowned upon.
- **Docstrings**: Use clear, concise docstrings for classes and public methods.
- **Style**: Use double quotes for strings, snake_case for variables and functions, and PascalCase for classes.
- Ruff lints and formats.
- Type hints are required on every new function definition (`ty` is enforced). Do not use `cast` to get around it.
- Clear, short docstrings on classes and public methods.
- Double quotes for strings, snake_case for variables and functions, PascalCase for classes.

### 2. Testing
We use `pytest`. New features should include unit tests in the `tests/` directory.
New features include unit tests (`pytest`) in `tests/`.

```bash
make test
```

`make test` skips tests marked `slow` by default (see `addopts` in `pyproject.toml`). This includes the performance metrics suite in `tests/metrics/`.

To run the metrics tests and write a JSON results file:
`make test` skips tests marked `slow` (see `addopts` in `pyproject.toml`), including the performance metrics suite in `tests/metrics/`. To run it and write a JSON results file:

```bash
NEGPY_METRICS_OUT=metrics.json uv run pytest tests/metrics/ -m "slow" -q
```

Fixtures (Canon CR2, Nikon NEF, Sony ARW, Fuji RAF, Leica DNG) are downloaded automatically from rawsamples.ch on first run (~20–30 MB each) and cached in `~/.cache/negpy-metrics/`. Tests skip gracefully if a download fails.

To point a test at a local file instead of downloading, set the per-format env var:
The fixtures (Canon CR2, Nikon NEF, Sony ARW, Fuji RAF, Leica DNG) download from rawsamples.ch on the first run (~20–30 MB each) and are cached in `~/.cache/negpy-metrics/`. A test skips if its download fails. To use a local file, set the per-format env var:

```bash
NEGPY_PERF_RAW_CR2=/path/to/file.CR2 \
Expand All @@ -160,29 +144,27 @@ uv run pytest tests/metrics/ -m "slow" -q
Available overrides: `NEGPY_PERF_RAW_CR2`, `NEGPY_PERF_RAW_NEF`, `NEGPY_PERF_RAW_ARW`, `NEGPY_PERF_RAW_RAF`, `NEGPY_PERF_RAW_DNG`.

### 3. Workflow (The Makefile)
The `Makefile` is the central source of truth for developer commands and executes everything via `uv run`:
- `make install`: Set up environment and sync dependencies.
- `make lint`: Run Ruff checks.
- `make type`: Run `ty` type checks.
- `make test`: Run all unit tests.
- `make format`: Auto-format code with Ruff.
- `make all`: Run lint, type, and test in sequence.
- `make clean`: Removes cache and build artifacts.
- `make run-sim`: Runs the app with simulated scanning hardware (see [above](#running-without-scanning-hardware)).
- `make clear-devhome`: Deletes the development user directory (see [above](#a-separate-user-directory-for-development)).
Every developer command runs through `uv run`:
- `make install`: set up and sync the environment.
- `make lint`: Ruff checks.
- `make type`: `ty` type checks.
- `make test`: unit tests.
- `make format`: auto-format with Ruff.
- `make all`: lint, type and test in sequence.
- `make clean`: remove cache and build artifacts.
- `make run-sim`: run with simulated scanning hardware (see [above](#running-without-scanning-hardware)).
- `make clear-devhome`: delete the development user directory (see [above](#a-separate-user-directory-for-development)).


## 📦 Building and Packaging
## Building and Packaging

To build the standalone application for your current OS:
To build the standalone application for your current OS with PyInstaller:

```bash
make build
```
This will trigger the Python backend build via PyInstaller.

On macOS, you can choose the target architecture for the DMG build with `NEGPY_MACOS_ARCH`.
For an Intel build from a compatible macOS environment, run:
On macOS, `NEGPY_MACOS_ARCH` sets the DMG's target architecture. For an Intel build:

```bash
NEGPY_MACOS_ARCH=x86_64 make build
Expand Down
Loading
Loading