diff --git a/README.md b/README.md index a012148b..90854840 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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) --- @@ -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. @@ -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 diff --git a/negpy/desktop/view/keyboard_shortcuts.py b/negpy/desktop/view/keyboard_shortcuts.py index d351094f..35785f39 100644 --- a/negpy/desktop/view/keyboard_shortcuts.py +++ b/negpy/desktop/view/keyboard_shortcuts.py @@ -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), diff --git a/negpy/desktop/view/shortcut_registry.py b/negpy/desktop/view/shortcut_registry.py index 0e3cf50c..b3b5319a 100644 --- a/negpy/desktop/view/shortcut_registry.py +++ b/negpy/desktop/view/shortcut_registry.py @@ -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"), diff --git a/negpy/desktop/view/sidebar/controls_panel.py b/negpy/desktop/view/sidebar/controls_panel.py index 9986bc5f..a0342f7c 100644 --- a/negpy/desktop/view/sidebar/controls_panel.py +++ b/negpy/desktop/view/sidebar/controls_panel.py @@ -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( diff --git a/negpy/desktop/view/sidebar/export.py b/negpy/desktop/view/sidebar/export.py index 7835f16a..0e8961e1 100644 --- a/negpy/desktop/view/sidebar/export.py +++ b/negpy/desktop/view/sidebar/export.py @@ -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)