Cyanotype printing - #802
Merged
Merged
Conversation
A cyanotype is not a silver print with a blue curve on it. The sensitiser is iron, the image substance is Prussian blue, there is no development stage to time, and the whole thing is fixed by how much UV got through the negative and by how long a density range the paper can hold. Rename the Lith collapsible to Alternative Processes and put a None / Lith / Cyanotype selector at the top of it, showing only the selected process's controls. The two are mutually exclusive - you cannot lith-develop a cyanotype - so LithConfig is replaced by one AltProcessConfig carrying an alt_process enum rather than a bool per process, which would have made "both on" loadable from a sidecar. lith_enabled migrates in migrate_flat_config; the three lith sliders keep their names and their values. The controls come from Ware's measurements. Exposure Scale is the negative density range the sensitiser prints, in log D: he puts the traditional Herschel formula at about 1.0 to 1.2 against 2.4 for his own, and his Simple Cyanotype ships as three variants at 1.8, 2.3 and 2.7. Note the direction - a longer scale prints *softer*, because a negative that does not fill the scale prints flat. Measured on a real scan, gamma over a fixed negative band runs 0.63 at scale 1.0 down to 0.30 at 2.6, with the blocked-at-Dmax fraction falling 23% to 0%. Within the scale the midtones compress, which is the reason a cyanotype digital-negative correction curve exists at all. Modelled as a reverse-S written v*|v| rather than a power, so both engines evaluate it identically, blended against a straight line so the slope at the centre is 0.55 instead of zero - a perfectly flat midtone posterizes a gradient. Colour follows lith's construction: an (a*, b*) path indexed on output density with L* taken from the density itself. Prussian blue absorbs around 700nm, so the print never goes black, it goes blue; the highlight knot is green because Ware describes the residual yellow sensitiser colour-mixing with the blue on both formulas. Sensitiser picks Dmax and the path - Classic 0.95 and greener, New 1.40 and deeper, matching his densitometry. Bleach then tannin is the standard toning pair, so it gets two sliders rather than one: washing soda strips pigment highlights-first, tannic acid re-develops the iron as a brown tannate that covers more than the pigment it replaced. A partial bleach leaves a split blue-brown. There is no silver anywhere in a cyanotype, so the six chemical baths are skipped entirely on both engines while it is selected and greyed out in the Toning panel. Split toning is a dye stain and still applies. toning.wgsl's lith_active flag becomes a three-state alt_mode. Not modelled: solarisation. It is real and it is what everyone photographs mid-process, but the reversed shadows regain their density on drying as Prussian white oxidises back to Prussian blue, so the finished print does not carry it. Bronzing is a surface sheen rather than a colour, and is also left out. CPU and GPU agree across every cyanotype case.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Extends the alternative-processes work from #798. The Lith collapsible becomes Alternative Processes with a
None / Lith / Cyanotypeselector; only the selected process's controls are shown.Why it is its own stage
A cyanotype is not a silver print with a blue curve on it. Iron sensitiser, Prussian blue image substance, no development stage to time. It is fixed by how much UV got through the negative and by how long a density range the paper can hold.
Data model
LithConfigis replaced by oneAltProcessConfigwith analt_processenum instead of a bool per process — two booleans would have made "both on" loadable from a sidecar or preset.lith_enabledmigrates inmigrate_flat_config; the three lith sliders keep their names and values, so existing edits open unchanged.features/altprocess/holds only the config, because the panel is shared; the logic and shaders stay infeatures/lith/andfeatures/cyanotype/. Noted in CLAUDE.md as the one exception to the feature pattern.Controls, and where the numbers come from
Exposure Scale runs the opposite way to intuition: a longer scale prints softer, because a negative that doesn't fill the scale prints flat. That is what drives new-cyanotype printers to make denser negatives. Measured on a real scan, gamma over a fixed band of the untouched print:
The transfer
Within the scale the midtones compress — the reason a cyanotype digital-negative correction curve exists at all. Written as a reverse-S using
v·|v|(v = 2t−1) rather than a power, so both engines evaluate it identically, blended against a straight line so mid-slope is 0.55 instead of zero; a perfectly flat midtone posterizes a gradient.Colour follows lith's construction — an (a*, b*) path indexed on output density with L* from the density itself. Prussian blue absorbs around 700nm, so the print never goes black, it goes blue. The highlight knot is green because Ware describes the residual yellow sensitiser colour-mixing with the blue on both formulas.
Toning
No silver anywhere in a cyanotype, so the six chemical baths are skipped entirely on both engines while it is selected, and greyed out in the panel. Split toning is a dye stain and still applies.
toning.wgsl'slith_activeflag becomes a three-statealt_mode.Deliberately not modelled
Solarisation. It is real, and it is what everyone photographs mid-process, but Ware is explicit that the reversed shadows regain their density on drying as Prussian white oxidises back to Prussian blue — the finished print does not carry it. Bronzing is a surface sheen rather than a colour, so it is out too.
Verification
Headless drive of the real app on a B&W scan:
CPU and GPU agree across every cyanotype case. New tests:
test_cyanotype_logic.py,test_altprocess_config.py(thelith_enabledmigration),test_altprocess_sidebar.py,TestCyanotypeParity, cyanotype rows in the bit-identity stage-skip sweep, and inert-toner tests.make allgreen.One fix worth calling out: the sensitiser combo stores plain strings as item data while the config holds a
StrEnum, sofindData(enum)missed andsync_uisnapped the dropdown back to Classic on every render. Same QVariant gotcha the Lab sharpen-method combo already has a test for; pinned with one here too.Known gap
PrintService._toned_paper_whitederives the mat / filed-carrier colour from the toning stack only, so it ignores cyanotype — as it already ignores lith. The cyanotype paper base is near-white, so the mismatch is small. Left for a follow-up.