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
22 changes: 21 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ It is built with **Python**, running natively on Linux, macOS, and Windows.
* **Positive/Slide Support**: Dedicated **E-6 mode** with optional normalization to save expired or faded film.

**Capture & Input**
* **Camera Scanning**: Capture negatives with a tethered camera straight into NegPy — a single RAW, or automated red/green/blue narrowband triplets driven by an RGB [Scanlight](https://github.com/jackw01/scanlight) that feed the RGB Scan merge. macOS/Linux, optional dependency. [Camera Scanning guide](docs/CAMERA_SCANNING.md)
* **Scanner Support**: Direct control of SANE-compatible scanners. [supported devices](http://www.sane-project.org/sane-supported-devices.html)
* **RGB Scan (Trichromatic Capture)**: Merge three narrowband red/green/blue exposures of one negative into a single low-noise colour scan, with automatic sub-pixel alignment to kill fringing.
* **Flat-Field Correction**: Correct illumination falloff / vignetting from your light source or scanner via a reference scan of the bare light. Named profiles, toggle per image.
Expand All @@ -47,6 +48,7 @@ It is built with **Python**, running natively on Linux, macOS, and Windows.
* **Non-destructive**: Original files never touched; edits stored as recipes.
* **Database**: Edits in a local SQLite db keyed by file hash — move or rename files without losing work.
* **Persistent Undo/Redo & History**: Up to 100 edits per file. **History panel** lists every step — jump to any state, branch, or export an earlier version. Survives restarts.
* **Metadata & Gear Library**: Archival metadata for the original analog capture — manage a library of cameras, lenses, and film stocks, apply gear presets per frame, and write real camera/lens/ISO EXIF (plus XMP scan tags) into exports so Lightroom shows your film gear. [see the guide](docs/USER_GUIDE.md#11-metadata-panel)
* **Keyboard Shortcuts**: [see here](docs/KEYBOARD.md)

---
Expand All @@ -72,7 +74,12 @@ sudo pacman -S sane # Arch
```
Or your distro's equivalent. The app launches fine without so you can ignore that if you don't plan to use a scanner.

You can also clone the repo and build it yourself, instruction here: [CONTRIBUTING.md](CONTRIBUTING.md)
**Camera scanning support** (optional) uses `python-gphoto2` for tethered capture, and may need the system `libgphoto2` installed:
```
sudo pacman -S libgphoto2 # Arch
```
Or look up your distro's equivalent package.


#### Unsigned Software Warning
Since this is a free hobby project, I don't pay Apple or Microsoft ransom for their developer certificates. You'll get a scary warning the first time you run it.
Expand All @@ -88,10 +95,23 @@ brew install sane-backends
```
The app launches fine without so you can ignore that if you don't plan to use a scanner.

**Camera scanning support** (optional) uses `python-gphoto2`, and may need `libgphoto2` from [Homebrew](https://brew.sh/):
```
brew install libgphoto2
```

**Windows**:
1. Run the installer (ignore the warnings)
2. Start the app and click through the warnings.

Scanner and camera scanning are **not available on Windows**. Both ride on Unix-first free-software libraries — SANE for scanners, libgphoto2 for cameras — that just don't build there. It's not really their fault: the open world spent decades writing generic, vendor-neutral drivers for hundreds of devices, while Windows stuck with closed per-vendor blobs and never grew an equivalent. So the free stack NegPy leans on has nowhere to stand on Windows. Everything else works fine.

Good news: you can install Linux on pretty much any Windows machine. 🐧

---

You can also clone the repo and build it yourself, instruction here: [CONTRIBUTING.md](CONTRIBUTING.md)

---

## Data Location
Expand Down
10 changes: 10 additions & 0 deletions negpy/desktop/view/keyboard_shortcuts.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,16 @@ def _build_actions(self) -> dict[str, Callable[[], None]]:
"shoulder_dec": (lambda: controls.tone_sidebar.sh_slider, -1.0),
"shoulder_width_inc": (lambda: controls.tone_sidebar.sh_w_slider, 1.0),
"shoulder_width_dec": (lambda: controls.tone_sidebar.sh_w_slider, -1.0),
"snap_inc": (lambda: controls.tone_sidebar.midtone_gamma_slider, 1.0),
"snap_dec": (lambda: controls.tone_sidebar.midtone_gamma_slider, -1.0),
"shadow_density_inc": (lambda: controls.tone_sidebar.shadow_density_slider, 1.0),
"shadow_density_dec": (lambda: controls.tone_sidebar.shadow_density_slider, -1.0),
"highlight_density_inc": (lambda: controls.tone_sidebar.highlight_density_slider, 1.0),
"highlight_density_dec": (lambda: controls.tone_sidebar.highlight_density_slider, -1.0),
"shadow_grade_inc": (lambda: controls.tone_sidebar.shadow_grade_slider, 1.0),
"shadow_grade_dec": (lambda: controls.tone_sidebar.shadow_grade_slider, -1.0),
"highlight_grade_inc": (lambda: controls.tone_sidebar.highlight_grade_slider, 1.0),
"highlight_grade_dec": (lambda: controls.tone_sidebar.highlight_grade_slider, -1.0),
"offset_inc": (lambda: controls.geometry_sidebar.offset_slider, 1.0),
"offset_dec": (lambda: controls.geometry_sidebar.offset_slider, -1.0),
"fine_rot_inc": (lambda: controls.geometry_sidebar.fine_rot_slider, 1.0),
Expand Down
10 changes: 10 additions & 0 deletions negpy/desktop/view/shortcut_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,16 @@ class ShortcutEntry:
"shoulder_inc": ShortcutEntry("Alt+U", "Shoulder up", "Exposure"),
"shoulder_width_dec": ShortcutEntry("Alt+Shift+I", "Shoulder width down", "Exposure"),
"shoulder_width_inc": ShortcutEntry("Alt+I", "Shoulder width up", "Exposure"),
"snap_dec": ShortcutEntry("", "Snap (midtone) down", "Exposure"),
"snap_inc": ShortcutEntry("", "Snap (midtone) up", "Exposure"),
"shadow_density_dec": ShortcutEntry("", "Shadows density down", "Exposure"),
"shadow_density_inc": ShortcutEntry("", "Shadows density up", "Exposure"),
"highlight_density_dec": ShortcutEntry("", "Highlights density down", "Exposure"),
"highlight_density_inc": ShortcutEntry("", "Highlights density up", "Exposure"),
"shadow_grade_dec": ShortcutEntry("", "Shadows grade down", "Exposure"),
"shadow_grade_inc": ShortcutEntry("", "Shadows grade up", "Exposure"),
"highlight_grade_dec": ShortcutEntry("", "Highlights grade down", "Exposure"),
"highlight_grade_inc": ShortcutEntry("", "Highlights grade up", "Exposure"),
"lock_bounds_toggle": ShortcutEntry("Alt+Q", "Toggle bounds lock", "Process"),
"analysis_buffer_dec": ShortcutEntry("Alt+Shift+B", "Analysis buffer down", "Process"),
"analysis_buffer_inc": ShortcutEntry("Alt+B", "Analysis buffer up", "Process"),
Expand Down
35 changes: 35 additions & 0 deletions negpy/desktop/view/sidebar/controls_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,41 @@ def apply_shortcut_tooltips(self) -> None:
["shoulder_width_inc", "shoulder_width_dec"],
)
)
exp.midtone_gamma_slider.setToolTip(
tooltip_with_shortcut(
"Snap — paper midtone gamma trim: steepens or flattens the S-curve around the reference "
"tone; paper white/black stay put. In R/G/B mode: this layer's Snap trim",
["snap_inc", "snap_dec"],
)
)
exp.shadow_density_slider.setToolTip(
tooltip_with_shortcut(
"Shadow zone density (ΔD): weighted to the deep shadows, bounded by paper black. "
"Positive darkens shadows; negative lifts them",
["shadow_density_inc", "shadow_density_dec"],
)
)
exp.highlight_density_slider.setToolTip(
tooltip_with_shortcut(
"Highlight zone density (ΔD): weighted to the highlights, bounded by paper white. "
"Positive burns highlights in; negative bleaches them",
["highlight_density_inc", "highlight_density_dec"],
)
)
exp.shadow_grade_slider.setToolTip(
tooltip_with_shortcut(
"Split grade — shadow zone contrast trim (ISO-R): rotates the curve locally in the deep "
"shadows. In R/G/B mode: this layer's shadow-grade trim",
["shadow_grade_inc", "shadow_grade_dec"],
)
)
exp.highlight_grade_slider.setToolTip(
tooltip_with_shortcut(
"Split grade — highlight zone contrast trim (ISO-R): rotates the curve locally in the "
"highlights. In R/G/B mode: this layer's highlight-grade trim",
["highlight_grade_inc", "highlight_grade_dec"],
)
)

geo.manual_crop_btn.setToolTip(
tooltip_with_shortcut(
Expand Down
1 change: 0 additions & 1 deletion negpy/desktop/view/sidebar/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ class ExportSidebar(BaseSidebar):
SIDE_MARGIN = THEME.space_xl

def _init_ui(self) -> None:

self.update_timer = QTimer()
self.update_timer.setSingleShot(True)
self.update_timer.setInterval(500)
Expand Down
Loading