Film simulation engine, Mac app, command-line tool, Resolve and Final Cut Pro plugins, and browser demo.
2
stars
266
commits
Swift
primary language
Sep 14, 2026
updated
Fotufilm is an open-source film simulation engine for photos and video. It models how light exposes photographic film, how the image develops into dye or silver, and how that film becomes a print, scan, or viewed transparency. Colour, contrast, grain, and halation follow the selected film's properties and the process used to render it.
This repository includes the shared engine, a command-line tool, a Mac app, plugins for DaVinci Resolve and Final Cut Pro, and a browser photo and video editor.
Download for Mac · User guide ·
The model follows the stages between scene light and a finished photograph:
flowchart LR
A[Scene light] --> B[Film exposure]
B --> C[Development and grain]
C --> D[Print, scan, or direct view]
D --> E[Output image]
These stages explain why the controls work together: exposure moves the image along the film's response curve, film format changes the scale of its texture, and the output medium helps determine the final colour and contrast.
Source Illuminant defaults to Stock Native for RAW, processed images and
video. RAW white balance still controls decoding; it no longer automatically
selects the simulated scene light. In the Mac app and plugins, choose a preset or
Custom temperature to simulate another source, independently of print viewing light. In the CLI, use
--scene-kelvin 5500 for an explicit source; omission follows the selected stock.
Film stocks are data-driven profiles describing spectral sensitivity, characteristic curves, dyes, grain, and spatial behaviour. The same engine reads these properties for colour negative, black-and-white, and reversal films. See Included films and Print media for the bundled data and its sources.
The model combines published measurements with physical and statistical approximations. RGB cannot uniquely recover the original scene spectrum, and clipped highlights cannot supply missing exposure. Digitised curves are limited by their source graphs and extrapolate beyond the published range. Grain models describe aggregate texture rather than individual crystals. Results therefore depend on the input, profile data, and viewing conditions as well as the model.
For a closer look at the implementation, start with the pipeline and controls, spectral model, and film profile structure.
On a Mac, install Xcode 26 or newer and Halide:
brew install halide
swift build
python3 tools/test-engine.py
The test command builds the release test bundle and runs every discovered test in
up to eight persistent workers. It checks that each test ran exactly once and
records worker logs and a timing report in build/engine-tests. Use
--workers 2 on a smaller machine. The previous report helps balance later runs;
it never substitutes for running tests.
On macOS, Swift package builds reuse compiled Halide CPU and Metal kernels across
processes. The test command stores them in .build/compiled-kernels; other package
clients use the macOS user cache directory. These files contain compiled code,
not photos or rendered results. Engine binaries, Halide, compiler, SDK, target
and compilation settings identify each cache entry, so first runs and runs after
relinking can take longer. Missing or invalid entries are rebuilt; unavailable
caches fall back to normal JIT compilation. Set FOTUFILM_COMPILED_CACHE=0 to
disable reuse, or remove
.build/compiled-kernels to clear the test cache.
The standard swift test -c release --parallel command remains supported for
testing with SwiftPM's process scheduling.
The engine/desktop test workflow runs manually; Apple AOT releases also run automatically when their build inputs change on main.
If Halide is installed elsewhere, set HALIDE_ROOT to its installation folder.
tools/generate-halide-aot.sh device (or simulator, macos, macos-intel)
first fetches matching precompiled kernels from the public engine's GitHub releases.
Downloads need no token or installed Halide compiler. SHA-256 checksums and a manifest
verify the source inputs, platform, compiler recipe and every archive/header before use.
If a release is unavailable, local builds can generate with HALIDE_ROOT instead.
Set FOTUFILM_AOT_REQUIRE_PREBUILT=1 in CI to require a published set, or
FOTUFILM_AOT_NO_FETCH=1 to test local generation. Schedule overrides do not substitute
default release kernels; select FOTUFILM_AOT_NO_FETCH=1 when testing another compiler.
The Apple AOT releases workflow runs on kernel/build-input changes merged to main,
and supports manual runs. It uses GitHub's standard macos-26 runner, builds the pinned
Halide source with LLVM 22.1.8 and Xcode 26.6, and publishes missing content-keyed releases
for all four targets. Only the publishing job uses GitHub's built-in token; no personal
access token is required. Archives include licence notices, exclude host tools and local
paths, and pass a full bridge-link check before publication. These are linkage and
archive checks, not GPU execution tests. AOT releases never become the app's Latest release.
The compiler contract is in tools/aot-toolchain.json; update it deliberately when
upgrading the toolchain. For a local publisher, install cmake, ninja, llvm@22,
lld@22 and flatbuffers with Homebrew, set LLVM_ROOT and LLD_ROOT to those versioned
formula prefixes, and build with tools/build-halide.sh. Then run
tools/publish-aot-release.sh from a clean, merged engine checkout.
List the included films or process an image from the command line:
swift run fotufilm --list-stocks
swift run fotufilm input.jpg output.jpg --stock gold200
The Mac app targets Apple silicon and macOS 14 or newer.
macos/build.sh --test
Open build/macos/Fotufilm.app. The build also includes the Resolve plugin.
To include the Final Cut plugin, install Apple's FxPlug SDK first. See the
Resolve guide and Final Cut guide
for separate builds and installation steps.
Mac, Resolve, and Final Cut rebuilds reuse compiled objects after checking source
and header contents, compiler, flags, and SDK dependencies. Linking, bundle assembly,
signing, audits, and requested tests still run. Set FOTUFILM_BUILD_CACHE=0 to
force recompilation, or remove build/macos/obj, build/resolve/obj-*, and
build/finalcut/obj-*.
After building, check camera-log conversion and the full-float video decode path:
build/macos/Fotufilm.app/Contents/MacOS/Fotufilm --verify-log-conversion
build/macos/Fotufilm.app/Contents/MacOS/Fotufilm --verify-preview-depth
build/macos/Fotufilm.app/Contents/MacOS/Fotufilm --verify-source-illuminant
These checks use synthetic ramps to compare CPU and Metal conversion and measure the precision retained through decoding, playback, and paused-frame rendering. The source-illuminant check also verifies saved selections and RAW decode-cache isolation.
Install Emscripten and Python 3.10 or newer. Set EMSDK_ROOT to your Emscripten
SDK folder, or install it at build/emsdk. Then run:
tools/build-wasm.sh
cd web
npm ci
npm run build
The output is in web/dist. Set FOTUFILM_BASE=/demo/ when running npm run build
to host the demo at fotufilm.com/demo.
Video supports the Mac app’s camera log inputs, frame previews, trimming, audio,
and MP4 / H.264 or WebM / VP9 export. It reads files in bounded slices and streams
exports to disk. HEVC Main / Main10 has a worker-based software decoder when
native decoding cannot expose its original pixels. Export is 8-bit SDR.
The editor previews adjustments automatically and
exports from the original image, up to 120 megapixels. Large images render in
tiles; the pack carries spatial parameters for a ladder of frame sizes so grain
and halation follow the emulsion's scale. The editor uses the CPU when
a WebGPU-compatible Halide toolchain is not available. To build one, install Homebrew's llvm and
lld and run tools/build-halide.sh --webgpu first; it fetches the Halide
pull request the browser runtime needs and applies the patches in tools/.
The reference CPU and browser WebGPU kernels use strict float32 evaluation. Rebuild an older WebGPU toolchain before generating the browser runtime; the build checks that its arithmetic patch matches the engine. The parity diagnostics compare output bits before display conversion, including native reference fixtures and per-stage reports.
The browser editor guide covers controls, shortcuts, export, and the features that still require the Mac app.
Default source builds include all 40 film profiles, free to use without activation.
The runtime JSON profiles are available in Sources/FotufilmCore/Stocks/ under
CC BY-SA 4.0. You may modify and redistribute them
with attribution and ShareAlike terms. This licence does not apply to rendered
photos or videos. The engine code uses Apache-2.0.
Thirty-three profiles carry sampled characteristic curves with smooth interpolation through every validated digitized point. Source tracing variations are retained; response outside each published range is extrapolated. These schema version 2 profiles require a build with sampled-curve support. Schema version 1 remains supported.
The CLI and tests also include synthetic films. The browser opens a generated scene-linear float32 EXR chart. See Build support for stock-pack setup.
The six negative-to-positive physical media below are digitised from the manufacturers' published datasheets, on the same 380-780 nm grid at 5 nm the film model uses. Ilfochrome/Cibachrome adds two positive-to-positive papers with approximate profiles.
| Medium | Source |
|---|---|
| Kodak Ektacolor Edge | Kodak E-7020 (April 2019) |
| Kodak Professional Endura Premier | Kodak E-4070 (March 2013) |
| Fujicolor Crystal Archive Type CA | Fujifilm AF3-0250U2 (November 2018) |
| Kodak Vision 2383 | Kodak H-1-2383 (March 2022) |
| Kodak Vision Premier 2393 | Kodak 2393 curve sheets |
| Fujifilm ETERNA-CP 3513DI | Fujifilm ETERNA-CP 3513DI brochure |
| Ilfochrome Classic CPS.1K (Cibachrome), normal contrast | Ilford TDS 307US (August 2003), tone specifications |
| Ilfochrome Classic CLM.1K (Cibachrome), medium contrast | Ilford TDS 307US (August 2003), tone specifications |
For slide film, choose Ilfochrome Classic CPS.1K or the softer CLM.1K in Output Medium.
The CLI accepts --paper ilfochrome-cps-1k or --paper ilfochrome-clm-1k;
ilfochrome and cibachrome both select CPS.1K. Match Film still defaults to direct viewing.
Instant sheets remain direct-view only, and negative films retain their existing media.
Positive paper supports viewing light and the simulated optical printer; increasing
printer exposure lightens the print. Paper output is SDR.
These profiles use the Ilford Deluxe media datasheet, p. 1: visual density ranges of 2.0/2.05 D and mid-tone contrasts of 1.40/1.15. The curves' endpoints are approximate. The publication does not supply spectral sensitivities or dye spectra; the current color model uses the Ektacolor receiver as a provisional basis. It does not reproduce measured Ilfochrome azo-dye color. Cibachrome aliases the same family; a separate historical coating is not simulated.
A sheet that publishes one characteristic curve develops all three records along it;
E-7020, E-4070, 2383, 2393 and ETERNA-CP publish three and are carried per record. The lab scan and
telecine are inversions rather than sheets, and are described in PrintPaperTables.swift.
Colour release prints time the reference negative to the published gross Status A LAD aims: 1.09/1.06/1.03 for Kodak 2383 and 2393, and 1.10/1.05/1.05 for ETERNA-CP. The solve preserves relative layer speeds and changes exposure through a non-negative, UV-blocked tungsten RGB additive head. These aims are not three equal densities above clear film. Reflection papers retain a visually balanced 0.744 D (18%) setup under D50. Viewing another lamp never changes the developed dyes. Optional digital channel-contrast correction is off by default.
The printer passbands and filtered 5400 K xenon reference are explicit approximations, not measured printer filters or the manufacturers' original calibration lamps. Output is relative to clear medium; no unmeasured print-base spectrum is invented. Reflection paper filtration remains an exposure-ratio approximation. ETERNA-CP's published Gray and dye sum remain inconsistent with a non-negative additive base, so its neutral spectral calibration remains uncertain.
SOURCE_ASSETS.json records where assets came from and their file hashes. Before
adding data or images, run python3 tools/check-source-boundary.py.
To convert a scan, choose File → Import Scanned Negative…, sample its clear film border and preview the positive. Import it to adjust all four crop corners independently. See the scan import guide for input requirements and the approximate conversion’s limits.
The engine, Mac app, and plugins use Apache-2.0. Film profiles have separate licences.
266 commits
Swift
67.4%
C++
13.0%
C
4.7%
JavaScript
4.5%
Objective-C++
3.9%
Metal
2.7%
Python
1.5%
Shell
1.5%
Film simulation engine, Mac app, command-line tool, Resolve and Final Cut Pro plugins, and browser demo.
2
stars
266
commits
Swift
primary language
Sep 14, 2026
updated
Fotufilm is an open-source film simulation engine for photos and video. It models how light exposes photographic film, how the image develops into dye or silver, and how that film becomes a print, scan, or viewed transparency. Colour, contrast, grain, and halation follow the selected film's properties and the process used to render it.
This repository includes the shared engine, a command-line tool, a Mac app, plugins for DaVinci Resolve and Final Cut Pro, and a browser photo and video editor.
Download for Mac · User guide ·
The model follows the stages between scene light and a finished photograph:
flowchart LR
A[Scene light] --> B[Film exposure]
B --> C[Development and grain]
C --> D[Print, scan, or direct view]
D --> E[Output image]
These stages explain why the controls work together: exposure moves the image along the film's response curve, film format changes the scale of its texture, and the output medium helps determine the final colour and contrast.
Source Illuminant defaults to Stock Native for RAW, processed images and
video. RAW white balance still controls decoding; it no longer automatically
selects the simulated scene light. In the Mac app and plugins, choose a preset or
Custom temperature to simulate another source, independently of print viewing light. In the CLI, use
--scene-kelvin 5500 for an explicit source; omission follows the selected stock.
Film stocks are data-driven profiles describing spectral sensitivity, characteristic curves, dyes, grain, and spatial behaviour. The same engine reads these properties for colour negative, black-and-white, and reversal films. See Included films and Print media for the bundled data and its sources.
The model combines published measurements with physical and statistical approximations. RGB cannot uniquely recover the original scene spectrum, and clipped highlights cannot supply missing exposure. Digitised curves are limited by their source graphs and extrapolate beyond the published range. Grain models describe aggregate texture rather than individual crystals. Results therefore depend on the input, profile data, and viewing conditions as well as the model.
For a closer look at the implementation, start with the pipeline and controls, spectral model, and film profile structure.
On a Mac, install Xcode 26 or newer and Halide:
brew install halide
swift build
python3 tools/test-engine.py
The test command builds the release test bundle and runs every discovered test in
up to eight persistent workers. It checks that each test ran exactly once and
records worker logs and a timing report in build/engine-tests. Use
--workers 2 on a smaller machine. The previous report helps balance later runs;
it never substitutes for running tests.
On macOS, Swift package builds reuse compiled Halide CPU and Metal kernels across
processes. The test command stores them in .build/compiled-kernels; other package
clients use the macOS user cache directory. These files contain compiled code,
not photos or rendered results. Engine binaries, Halide, compiler, SDK, target
and compilation settings identify each cache entry, so first runs and runs after
relinking can take longer. Missing or invalid entries are rebuilt; unavailable
caches fall back to normal JIT compilation. Set FOTUFILM_COMPILED_CACHE=0 to
disable reuse, or remove
.build/compiled-kernels to clear the test cache.
The standard swift test -c release --parallel command remains supported for
testing with SwiftPM's process scheduling.
The engine/desktop test workflow runs manually; Apple AOT releases also run automatically when their build inputs change on main.
If Halide is installed elsewhere, set HALIDE_ROOT to its installation folder.
tools/generate-halide-aot.sh device (or simulator, macos, macos-intel)
first fetches matching precompiled kernels from the public engine's GitHub releases.
Downloads need no token or installed Halide compiler. SHA-256 checksums and a manifest
verify the source inputs, platform, compiler recipe and every archive/header before use.
If a release is unavailable, local builds can generate with HALIDE_ROOT instead.
Set FOTUFILM_AOT_REQUIRE_PREBUILT=1 in CI to require a published set, or
FOTUFILM_AOT_NO_FETCH=1 to test local generation. Schedule overrides do not substitute
default release kernels; select FOTUFILM_AOT_NO_FETCH=1 when testing another compiler.
The Apple AOT releases workflow runs on kernel/build-input changes merged to main,
and supports manual runs. It uses GitHub's standard macos-26 runner, builds the pinned
Halide source with LLVM 22.1.8 and Xcode 26.6, and publishes missing content-keyed releases
for all four targets. Only the publishing job uses GitHub's built-in token; no personal
access token is required. Archives include licence notices, exclude host tools and local
paths, and pass a full bridge-link check before publication. These are linkage and
archive checks, not GPU execution tests. AOT releases never become the app's Latest release.
The compiler contract is in tools/aot-toolchain.json; update it deliberately when
upgrading the toolchain. For a local publisher, install cmake, ninja, llvm@22,
lld@22 and flatbuffers with Homebrew, set LLVM_ROOT and LLD_ROOT to those versioned
formula prefixes, and build with tools/build-halide.sh. Then run
tools/publish-aot-release.sh from a clean, merged engine checkout.
List the included films or process an image from the command line:
swift run fotufilm --list-stocks
swift run fotufilm input.jpg output.jpg --stock gold200
The Mac app targets Apple silicon and macOS 14 or newer.
macos/build.sh --test
Open build/macos/Fotufilm.app. The build also includes the Resolve plugin.
To include the Final Cut plugin, install Apple's FxPlug SDK first. See the
Resolve guide and Final Cut guide
for separate builds and installation steps.
Mac, Resolve, and Final Cut rebuilds reuse compiled objects after checking source
and header contents, compiler, flags, and SDK dependencies. Linking, bundle assembly,
signing, audits, and requested tests still run. Set FOTUFILM_BUILD_CACHE=0 to
force recompilation, or remove build/macos/obj, build/resolve/obj-*, and
build/finalcut/obj-*.
After building, check camera-log conversion and the full-float video decode path:
build/macos/Fotufilm.app/Contents/MacOS/Fotufilm --verify-log-conversion
build/macos/Fotufilm.app/Contents/MacOS/Fotufilm --verify-preview-depth
build/macos/Fotufilm.app/Contents/MacOS/Fotufilm --verify-source-illuminant
These checks use synthetic ramps to compare CPU and Metal conversion and measure the precision retained through decoding, playback, and paused-frame rendering. The source-illuminant check also verifies saved selections and RAW decode-cache isolation.
Install Emscripten and Python 3.10 or newer. Set EMSDK_ROOT to your Emscripten
SDK folder, or install it at build/emsdk. Then run:
tools/build-wasm.sh
cd web
npm ci
npm run build
The output is in web/dist. Set FOTUFILM_BASE=/demo/ when running npm run build
to host the demo at fotufilm.com/demo.
Video supports the Mac app’s camera log inputs, frame previews, trimming, audio,
and MP4 / H.264 or WebM / VP9 export. It reads files in bounded slices and streams
exports to disk. HEVC Main / Main10 has a worker-based software decoder when
native decoding cannot expose its original pixels. Export is 8-bit SDR.
The editor previews adjustments automatically and
exports from the original image, up to 120 megapixels. Large images render in
tiles; the pack carries spatial parameters for a ladder of frame sizes so grain
and halation follow the emulsion's scale. The editor uses the CPU when
a WebGPU-compatible Halide toolchain is not available. To build one, install Homebrew's llvm and
lld and run tools/build-halide.sh --webgpu first; it fetches the Halide
pull request the browser runtime needs and applies the patches in tools/.
The reference CPU and browser WebGPU kernels use strict float32 evaluation. Rebuild an older WebGPU toolchain before generating the browser runtime; the build checks that its arithmetic patch matches the engine. The parity diagnostics compare output bits before display conversion, including native reference fixtures and per-stage reports.
The browser editor guide covers controls, shortcuts, export, and the features that still require the Mac app.
Default source builds include all 40 film profiles, free to use without activation.
The runtime JSON profiles are available in Sources/FotufilmCore/Stocks/ under
CC BY-SA 4.0. You may modify and redistribute them
with attribution and ShareAlike terms. This licence does not apply to rendered
photos or videos. The engine code uses Apache-2.0.
Thirty-three profiles carry sampled characteristic curves with smooth interpolation through every validated digitized point. Source tracing variations are retained; response outside each published range is extrapolated. These schema version 2 profiles require a build with sampled-curve support. Schema version 1 remains supported.
The CLI and tests also include synthetic films. The browser opens a generated scene-linear float32 EXR chart. See Build support for stock-pack setup.
The six negative-to-positive physical media below are digitised from the manufacturers' published datasheets, on the same 380-780 nm grid at 5 nm the film model uses. Ilfochrome/Cibachrome adds two positive-to-positive papers with approximate profiles.
| Medium | Source |
|---|---|
| Kodak Ektacolor Edge | Kodak E-7020 (April 2019) |
| Kodak Professional Endura Premier | Kodak E-4070 (March 2013) |
| Fujicolor Crystal Archive Type CA | Fujifilm AF3-0250U2 (November 2018) |
| Kodak Vision 2383 | Kodak H-1-2383 (March 2022) |
| Kodak Vision Premier 2393 | Kodak 2393 curve sheets |
| Fujifilm ETERNA-CP 3513DI | Fujifilm ETERNA-CP 3513DI brochure |
| Ilfochrome Classic CPS.1K (Cibachrome), normal contrast | Ilford TDS 307US (August 2003), tone specifications |
| Ilfochrome Classic CLM.1K (Cibachrome), medium contrast | Ilford TDS 307US (August 2003), tone specifications |
For slide film, choose Ilfochrome Classic CPS.1K or the softer CLM.1K in Output Medium.
The CLI accepts --paper ilfochrome-cps-1k or --paper ilfochrome-clm-1k;
ilfochrome and cibachrome both select CPS.1K. Match Film still defaults to direct viewing.
Instant sheets remain direct-view only, and negative films retain their existing media.
Positive paper supports viewing light and the simulated optical printer; increasing
printer exposure lightens the print. Paper output is SDR.
These profiles use the Ilford Deluxe media datasheet, p. 1: visual density ranges of 2.0/2.05 D and mid-tone contrasts of 1.40/1.15. The curves' endpoints are approximate. The publication does not supply spectral sensitivities or dye spectra; the current color model uses the Ektacolor receiver as a provisional basis. It does not reproduce measured Ilfochrome azo-dye color. Cibachrome aliases the same family; a separate historical coating is not simulated.
A sheet that publishes one characteristic curve develops all three records along it;
E-7020, E-4070, 2383, 2393 and ETERNA-CP publish three and are carried per record. The lab scan and
telecine are inversions rather than sheets, and are described in PrintPaperTables.swift.
Colour release prints time the reference negative to the published gross Status A LAD aims: 1.09/1.06/1.03 for Kodak 2383 and 2393, and 1.10/1.05/1.05 for ETERNA-CP. The solve preserves relative layer speeds and changes exposure through a non-negative, UV-blocked tungsten RGB additive head. These aims are not three equal densities above clear film. Reflection papers retain a visually balanced 0.744 D (18%) setup under D50. Viewing another lamp never changes the developed dyes. Optional digital channel-contrast correction is off by default.
The printer passbands and filtered 5400 K xenon reference are explicit approximations, not measured printer filters or the manufacturers' original calibration lamps. Output is relative to clear medium; no unmeasured print-base spectrum is invented. Reflection paper filtration remains an exposure-ratio approximation. ETERNA-CP's published Gray and dye sum remain inconsistent with a non-negative additive base, so its neutral spectral calibration remains uncertain.
SOURCE_ASSETS.json records where assets came from and their file hashes. Before
adding data or images, run python3 tools/check-source-boundary.py.
To convert a scan, choose File → Import Scanned Negative…, sample its clear film border and preview the positive. Import it to adjust all four crop corners independently. See the scan import guide for input requirements and the approximate conversion’s limits.
The engine, Mac app, and plugins use Apache-2.0. Film profiles have separate licences.
266 commits
Swift
67.4%
C++
13.0%
C
4.7%
JavaScript
4.5%
Objective-C++
3.9%
Metal
2.7%
Python
1.5%
Shell
1.5%