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
1 change: 1 addition & 0 deletions build.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
"--add-data=icc:icc",
"--add-data=media:media",
"--add-data=crosstalk:crosstalk",
"--add-data=gear:gear",
"--add-data=VERSION:.",
]

Expand Down
44 changes: 29 additions & 15 deletions docs/USER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,27 +210,41 @@ ICC profile for soft-proofing in the preview and (optionally) embedding in the e
---

## 11. Metadata Panel
Custom film/process metadata plus per-image overrides of EXIF inherited from the source file.
Archival metadata for the **original analog capture** — gear, process, and scanning — written into exported JPEG, TIFF, and PNG files as EXIF and embedded XMP.

### Custom
### Original analog gear

These fields are written into the exported file's metadata (XMP/EXIF as supported by the format).
Pick gear from your library (or a saved preset). Use **Manage…** to edit cameras, lenses, film stocks, and gear presets. Starter data is seeded into `~/NegPy/gear/` on first launch.

* **Film**: Film stock name — e.g. `Portra 400`, `HP5+`.
* **Format**: Film format dropdown — `35mm`, `120`, `4×5`, `8×10`, or `Other`.
* **Format (Custom)**: Free-text field that appears only when Format = `Other` (e.g. `6×7`).
* **Developer**: Developer name and dilution, e.g. `D-76 1+1`.
* **Push / Pull**: Process push or pull from `Push +3` down to `Pull -3`, with `Normal` in the middle.
* **Scanning**: Scanner or scanning method, e.g. `Noritsu HS-1800`.
* **Sync custom metadata to all files in batch export**: When on, all files in a batch export receive these custom metadata values.
* **Preset**: One-click camera + lens + film combination.
* **Camera / Lens / Film stock**: Dropdowns from the gear library. Changing any item clears the preset selection.
* **Clear**: Clears gear preset and library selections for this frame.

### Inherited (from source)
Structured fields for the **original capture** (camera, lens, film ISO) are written to **standard EXIF** when you set gear in the Metadata tab — so Lightroom and other DAMs show your film camera and lens. Scan-only tags from the source file (`FocalLengthIn35mmFormat`, scan exposure/ISO, etc.) are stripped so they do not mix with capture data. The **digitization rig** (DSLR, film scanner, copy-stand setup) is preserved in `negpy:Scan*` XMP tags only.

Camera, Lens, and Exposure are populated read-only from the source file's EXIF. Each has a small lock-button on the right — click it to unlock the field and override the inherited value for this specific edit. Clicking again re-locks to the EXIF source.
If you have **not** set capture gear, standard EXIF is left as-is (your scanner or DSLR remains visible in Lightroom) and scan data is mirrored to `negpy:Scan*` XMP. Process-only fields (developer, format) never overwrite camera/lens EXIF.

* **Camera**: `Make` + `Model` from the source EXIF, or your override.
* **Lens**: `LensModel` from EXIF, or override.
* **Exposure**: Formatted exposure string (e.g. `1/125s f/2.0 ISO 400`), or override.
Capture fields are also written to `negpy:Capture*` in XMP for structured archival access.

### Process

* **Format**: Film format — `35mm`, `120`, `4×5`, `8×10`, `110`, or `Other`.
* **Format (Custom)**: Shown when Format = `Other` (e.g. `6×7`).
* **Developer**: Developer and dilution, e.g. `D-76 1+1`.
* **Push / Pull**: `Push +3` … `Pull -3`, with `Normal` in the middle.

### Scanning

* **Scanning**: Scan method or notes (written to `negpy:ScanMethod` in XMP). EXIF `Software` is always `NegPy`.
* **Sync custom metadata to all files in batch export**: When on, batch export uses the current metadata tab values for every file.

### Exposure

Optional **original capture** exposure (shutter, aperture, film ISO). Click the lock icon to edit a free-text string (e.g. `1/125s f/2.8 ISO 400`). Scan exposure from the source file appears in the preview under **Scan**, not here.

### Metadata preview

Collapsible live preview grouped by **Original capture**, **Scan**, **Process**, and **File** — the tag values embedded on export.

---

Expand Down
37 changes: 37 additions & 0 deletions gear/cameras.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
[
{
"id": "cam-canon-ae1p",
"make": "Canon",
"model": "AE-1 Program",
"displayName": "Canon AE-1 Program",
"notes": "35mm SLR, 1981-1987, FD mount."
},
{
"id": "cam-nikon-fm2",
"make": "Nikon",
"model": "FM2",
"displayName": "Nikon FM2",
"notes": "35mm fully mechanical SLR, F-mount."
},
{
"id": "cam-pentax-k1000",
"make": "Pentax",
"model": "K1000",
"displayName": "Pentax K1000",
"notes": "35mm mechanical SLR, K-mount."
},
{
"id": "cam-olympus-om1",
"make": "Olympus",
"model": "OM-1",
"displayName": "Olympus OM-1",
"notes": "35mm compact mechanical SLR, OM-mount."
},
{
"id": "cam-mamiya-645",
"make": "Mamiya",
"model": "M645 1000s",
"displayName": "Mamiya 645 1000s",
"notes": "Medium-format 6x4.5 SLR."
}
]
67 changes: 67 additions & 0 deletions gear/film_stocks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
[
{
"id": "film-portra-400",
"displayName": "Kodak Portra 400",
"manufacturer": "Kodak",
"stockName": "Portra 400",
"iso": 400,
"format": "35mm",
"colorType": "ColorNegative",
"notes": "Professional color negative, warm and gentle skin tones."
},
{
"id": "film-portra-160",
"displayName": "Kodak Portra 160",
"manufacturer": "Kodak",
"stockName": "Portra 160",
"iso": 160,
"format": "35mm",
"colorType": "ColorNegative"
},
{
"id": "film-gold-200",
"displayName": "Kodak Gold 200",
"manufacturer": "Kodak",
"stockName": "Gold 200",
"iso": 200,
"format": "35mm",
"colorType": "ColorNegative"
},
{
"id": "film-tri-x-400",
"displayName": "Kodak Tri-X 400",
"manufacturer": "Kodak",
"stockName": "Tri-X 400",
"iso": 400,
"format": "35mm",
"colorType": "B&W Negative"
},
{
"id": "film-hp5-400",
"displayName": "Ilford HP5 Plus",
"manufacturer": "Ilford",
"stockName": "HP5 Plus 400",
"iso": 400,
"format": "35mm",
"colorType": "B&W Negative"
},
{
"id": "film-ektachrome-e100",
"displayName": "Kodak Ektachrome E100",
"manufacturer": "Kodak",
"stockName": "Ektachrome E100",
"iso": 100,
"format": "35mm",
"colorType": "ColorSlide"
},
{
"id": "film-cinestill-800t",
"displayName": "CineStill 800T",
"manufacturer": "CineStill",
"stockName": "800T",
"iso": 800,
"format": "35mm",
"colorType": "ColorNegative",
"notes": "Tungsten-balanced color negative with characteristic halation."
}
]
16 changes: 16 additions & 0 deletions gear/gear_presets.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[
{
"id": "preset-ae1p-fd50-portra400",
"displayName": "AE-1P / FD 50 f/1.4 / Portra 400",
"cameraId": "cam-canon-ae1p",
"lensId": "lens-canon-fd-50-1.4",
"filmStockId": "film-portra-400"
},
{
"id": "preset-fm2-50-trix",
"displayName": "FM2 / Nikkor 50 f/1.8 / Tri-X 400",
"cameraId": "cam-nikon-fm2",
"lensId": "lens-nikkor-50-1.8",
"filmStockId": "film-tri-x-400"
}
]
42 changes: 42 additions & 0 deletions gear/lenses.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
[
{
"id": "lens-canon-fd-50-1.4",
"displayName": "Canon FD 50mm f/1.4",
"lensModel": "FD 50mm f/1.4 S.S.C.",
"make": "Canon",
"focalLength": 50,
"maxAperture": 1.4
},
{
"id": "lens-canon-fd-28-2.8",
"displayName": "Canon FD 28mm f/2.8",
"lensModel": "FD 28mm f/2.8",
"make": "Canon",
"focalLength": 28,
"maxAperture": 2.8
},
{
"id": "lens-nikkor-50-1.8",
"displayName": "Nikkor 50mm f/1.8 AI-S",
"lensModel": "Nikkor 50mm f/1.8 AI-S",
"make": "Nikon",
"focalLength": 50,
"maxAperture": 1.8
},
{
"id": "lens-pentax-smc-50-1.7",
"displayName": "SMC Pentax-M 50mm f/1.7",
"lensModel": "SMC Pentax-M 50mm f/1.7",
"make": "Pentax",
"focalLength": 50,
"maxAperture": 1.7
},
{
"id": "lens-olympus-zuiko-50-1.8",
"displayName": "Olympus Zuiko 50mm f/1.8",
"lensModel": "Zuiko Auto-S 50mm f/1.8",
"make": "Olympus",
"focalLength": 50,
"maxAperture": 1.8
}
]
10 changes: 10 additions & 0 deletions negpy/desktop/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -1083,6 +1083,16 @@ def delete_flatfield_profile(self, name: str) -> None:
if self.session.repo.get_global_setting("flatfield_active_profile") == name:
self.set_active_flatfield_profile("")

def load_gear_library(self):
from negpy.services.assets.gear import GearProfiles

return GearProfiles.load_library()

def save_gear_library(self, library) -> None:
from negpy.services.assets.gear import GearProfiles

GearProfiles.save_library(library)

def set_flatfield_enabled(self, enabled: bool) -> None:
"""
Per-image toggle to enable/disable flat-field correction for the current frame.
Expand Down
3 changes: 3 additions & 0 deletions negpy/desktop/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from negpy.desktop.view.main_window import MainWindow
from negpy.infrastructure.storage.repository import StorageRepository
from negpy.services.assets.crosstalk import CrosstalkProfiles
from negpy.services.assets.gear import GearProfiles
from negpy.kernel.system.config import APP_CONFIG, BASE_USER_DIR
from negpy.kernel.system.logging import get_logger, setup_logging
from negpy.kernel.system.override import apply as apply_override
Expand Down Expand Up @@ -50,12 +51,14 @@ def _bootstrap_environment() -> None:
APP_CONFIG.cache_dir,
APP_CONFIG.user_icc_dir,
APP_CONFIG.crosstalk_dir,
APP_CONFIG.gear_dir,
APP_CONFIG.contact_sheet_templates_dir,
APP_CONFIG.default_export_dir,
]
for d in dirs:
os.makedirs(d, exist_ok=True)
CrosstalkProfiles.seed_example()
GearProfiles.seed_example()


def main() -> None:
Expand Down
Loading
Loading