NegClone is a Python CLI tool that analyzes your film photography archive (on Flickr or locally), extracts a grain and color fingerprint per film stock, and auto-generates Darktable .dtstyle and Lightroom/ACR .xmp preset files. The result is a publishable preset pack derived from real scanned negatives — not synthetic emulations.
- Sell your own preset pack. If you shoot film and scan your negatives, NegClone can extract the look of each stock you shoot and turn it into a distributable preset pack (Darktable + Lightroom) that you can sell on Gumroad, Etsy, or your own site.
- Match your digital edits to your film scans. Use NegClone to fingerprint your favorite stocks, then apply those presets to your digital photos for a consistent look across both formats.
- Compare film stocks objectively. Use
negclone compareto see exactly how Portra 400 differs from Ektar 100 in your scans — grain, color bias, tonal response, all measured. - Preserve your film look. If you're switching from film to digital (or hybrid shooting), fingerprint your go-to stocks before you stop buying them. The presets capture the characteristics of your actual scans.
- Build presets from a specific body of work. Point NegClone at a tagged subset of your Flickr archive (e.g.,
--tags desert) or a local folder to generate presets that capture the look of a specific project or series. - Generate visual reports. Use
negclone reportto create an HTML page with fingerprint cards, color swatches, and a similarity matrix — useful for blog posts, Gumroad listings, or your own reference.
- Python 3.12+
- uv (recommended) or pip
- Film photos either uploaded to Flickr or in a local directory
Only needed if you're pulling photos from Flickr (not needed for local scans):
- Go to https://www.flickr.com/services/apps/create/
- Click "Apply for a Non-Commercial Key"
- Fill in the application details
- Copy your API Key and API Secret
From PyPI:
pip install negcloneOr with uv:
uv pip install negcloneFor development:
git clone https://github.com/pfrederiksen/negclone.git
cd negclone
uv pip install -e ".[dev]"For Flickr mode, set your credentials as environment variables:
export FLICKR_API_KEY=your_api_key_here
export FLICKR_API_SECRET=your_api_secret_hereFor local mode, no configuration is needed.
# 1. Authenticate with Flickr (opens browser for OAuth)
negclone auth
# 2. Build an inventory of your film photos
negclone inventory --output inventory.json
# 3. Download original images for a specific stock
negclone download inventory.json --stock portra400 --sample-size 30
# 4. Analyze and fingerprint the stock
negclone fingerprint inventory.json --stock portra400 --sample-size 30
# 5. Generate Darktable + Lightroom presets
negclone generate fingerprint_portra400.json --format both --output-dir ./output
# 6. Package everything into a distributable zip
negclone pack --output-dir ./output --name "Desert Paul Film Presets"# Organize scans in folders by stock name:
# scans/portra400/IMG_001.tif
# scans/hp5/scan_042.jpg
# 1. Build inventory from local directory
negclone inventory --local ./scans --output inventory.json
# 2. Link local files into the cache
negclone download inventory.json
# 3. Fingerprint, generate, and pack — same as Flickr workflow
negclone fingerprint inventory.json
negclone generate fingerprint_portra400.json --output-dir ./output
negclone pack --output-dir ./outputAuthenticates with Flickr via OAuth 1.0a. Opens your browser to authorize the app, then prompts for the verifier code. Tokens are stored securely at ~/.negclone/flickr_tokens.json (file permissions 600). You only need to do this once.
negclone auth
negclone auth --verifier 123-456-789 # skip interactive promptScans your Flickr photostream or a local directory and groups photos by detected film stock. Outputs an inventory.json with photo IDs, URLs/paths, detected stock names, and metadata.
# Flickr: scan your own photostream
negclone inventory
# Flickr: scan a specific user with filters
negclone inventory --user paul-frederiksen --tags film --min-date 2023-01-01
# Local: scan a directory of scans
negclone inventory --local ./scans
# Use a custom tag-to-stock mapping
negclone inventory --tag-map my_tags.jsonFor local mode, stock detection works by checking:
- Parent folder name (e.g.,
scans/portra400/) - Filename (e.g.,
portra400_001.jpg) - EXIF metadata (ImageDescription, UserComment)
The --tag-map option accepts a JSON file like {"myportra": "portra400", "bw": "hp5"} for custom aliases.
Downloads original-resolution images from Flickr to a local cache, or creates symlinks for local scans. Skips already-cached files.
negclone download inventory.json --stock portra400
negclone download inventory.json --sample-size 30 # all stocks, 30 eachAnalyzes cached images and computes a fingerprint per film stock. Each fingerprint includes:
- Grain profile — intensity, FFT-derived size (peak spatial frequency), clumping factor, spectral slope
- Color bias — R/G/B channel shifts in shadows, midtones, and highlights (with optional scanner compensation)
- Tonal rolloff — shadow lift, highlight compression, midtone contrast, plus a PCHIP monotone spline tone curve
- Confidence score — how consistent the measurements are across samples
- Scanner model — auto-detected from EXIF, used to compensate for scanner color bias
negclone fingerprint inventory.json --stock portra400
negclone fingerprint inventory.json # all stocks
negclone fingerprint inventory.json --sample-size 40 # more samplesGenerates preset files from a fingerprint JSON. Supports Darktable .dtstyle and Lightroom/ACR .xmp formats. Lightroom presets include spline-based tone curves when available.
negclone generate fingerprint_portra400.json --format both
negclone generate fingerprint_portra400.json --format darktable
negclone generate fingerprint_portra400.json --dry-run
negclone generate fingerprint_portra400.json --forceCompare two fingerprints side-by-side with a detailed diff table showing grain, color, tonal differences, and an overall similarity score.
negclone compare fingerprint_portra400.json fingerprint_ektar100.jsonGenerate an HTML report with fingerprint cards, color swatches, and a similarity matrix across all provided fingerprints.
negclone report fingerprint_portra400.json fingerprint_ektar100.json fingerprint_hp5.json
negclone report fingerprint_*.json -o my_stocks.htmlBundles all generated presets into a zip archive with a README.txt containing import instructions. Prints a Gumroad upload checklist.
negclone pack --output-dir ./output --name "Desert Paul Film Presets"
negclone pack --dry-run # preview contentsNegClone samples patches across each scanned image, applies a 2D Hann window, and computes the FFT power spectrum. The radially averaged spectrum gives a frequency profile of the grain texture. From this it extracts:
- Peak frequency — dominant spatial frequency in cycles/pixel (inverted to get grain size in pixels)
- Spectral slope — steepness of the log-log power spectrum (steep = soft/large grain, shallow = sharp/fine)
- Spectral centroid — weighted mean frequency for robust size estimation
Local standard deviation and spatial autocorrelation are also computed for backward-compatible intensity and clumping metrics.
For each image, pixel luminance is bucketed into shadows (bottom 25%), midtones (25-75%), and highlights (top 25%). Within each bucket, the mean R, G, B channel values are compared to the per-bucket neutral mean, producing a color shift vector per region.
When scanner model detection finds a known scanner (Epson V600/V700/V850, Noritsu, Frontier, Plustek, etc.), the scanner's inherent color bias is subtracted from the measurements, so the fingerprint reflects the film stock rather than the scanner.
Tonal analysis measures the luminance histogram shape: shadow lift (deep vs. near shadow density), highlight compression, and midtone contrast. A PCHIP (Piecewise Cubic Hermite Interpolating Polynomial) monotone spline is fitted to the luminance CDF, producing a smooth, non-inverting tone curve that is exported directly into Lightroom presets as a multi-point ToneCurvePV2012.
NegClone auto-detects these film stocks from tags, titles, descriptions, folder names, and filenames:
| Stock | Type | Key Characteristics |
|---|---|---|
| Portra 160 | Color Negative | Fine grain, neutral-warm tones |
| Portra 400 | Color Negative | Medium grain, warm midtones, lifted shadows |
| Portra 800 | Color Negative | Visible grain, warm tones, good in low light |
| Ektar 100 | Color Negative | Fine grain, saturated, warm shadows |
| Gold 200 | Color Negative | Moderate grain, warm/yellow bias |
| HP5 Plus | B&W Negative | Pronounced grain, high contrast |
| Delta 100 | B&W Negative | Fine grain, smooth tones |
| Tri-X 400 | B&W Negative | Classic grain, rich midtones |
| Fomapan 100 | B&W Negative | Fine grain, soft tones |
| CineStill 800T | Color Negative (Tungsten) | Halation, cool tones, visible grain |
Custom tag-to-stock mappings:
{"portra": "portra400", "bw_street": "hp5", "cine": "cinestill800t"}- Open Darktable and switch to the lighttable view
- Find the styles module in the right panel
- Click import and select the
.dtstylefile(s) - Apply: select a photo, then click the style name
Note: Darktable module params use approximated binary encoding in v1. Grain, tone, and color grading are functional, but exact C struct encoding for all Darktable versions is a v2 feature.
- Copy
.xmpfile(s) to your presets directory:- macOS:
~/Library/Application Support/Adobe/CameraRaw/Settings/ - Windows:
C:\Users\<username>\AppData\Roaming\Adobe\CameraRaw\Settings\
- macOS:
- Restart Lightroom
- Find presets in the Develop module preset panel
- More samples = better fingerprints.
--sample-size 40gives higher confidence and more stable results. - Local scans are best. TIFFs from your scanner give better fingerprints than re-compressed Flickr JPEGs.
- Organize by folder. For local mode, put scans in folders named after the stock (e.g.,
portra400/) — this is the most reliable detection method. - Use
--dry-runongenerateandpackto preview before writing. - Compare stocks with
negclone compareto understand how they actually differ in your workflow. - Generate reports with
negclone reportfor blog posts or preset pack marketing pages.
uv pip install -e ".[dev]"
pytest # 65 tests
ruff check . && ruff format .
mypy negcloneMIT