Skip to main content

pfx_post/
parse.rs

1use crate::bloom::{Bloom, NEUTRAL_GAINS, NEUTRAL_RADII};
2use crate::chain::{Chain, Pass};
3use crate::color;
4use crate::comic::ModernComic;
5use crate::fx::{self, NoiseKind};
6use crate::grade::{Contrast, Dither, Grain, Saturation, Vignette, Warmth};
7use crate::preset::{self, Style};
8use crate::tone::Tone;
9use toml::{Table, Value};
10
11pub const KEYS: &[&str] = &[
12    "aberration",
13    "angle",
14    "black",
15    "black_threshold",
16    "bloom",
17    "cavity",
18    "cavity_distance",
19    "cel_bands",
20    "cel_shadow",
21    "cel_softness",
22    "cel_spec",
23    "cel_spec_roughness",
24    "cel_spec_threshold",
25    "cel_threshold",
26    "contrast",
27    "crease_angle",
28    "crush",
29    "curvature",
30    "darkening",
31    "dither",
32    "dither_kind",
33    "distortion",
34    "dot",
35    "dust",
36    "encode",
37    "exposure",
38    "flicker",
39    "focus",
40    "frame",
41    "gain",
42    "grain",
43    "halation",
44    "highlight",
45    "highlight_threshold",
46    "ink",
47    "interior_line_strength",
48    "keep",
49    "keep_range",
50    "levels",
51    "line_weight",
52    "lut",
53    "mask",
54    "outline",
55    "outline_alpha",
56    "outline_color",
57    "outline_thickness",
58    "palette",
59    "paper_grain",
60    "pixel",
61    "posterize",
62    "radius",
63    "rim",
64    "rim_color",
65    "rim_width",
66    "saturation",
67    "saturation_lift",
68    "scale",
69    "scanlines",
70    "scratches",
71    "seed",
72    "sepia",
73    "shadow",
74    "style",
75    "tape",
76    "threshold",
77    "tilt_range",
78    "tone",
79    "tone_steps",
80    "vignette",
81    "warmth",
82    "weave",
83    "weights",
84];
85
86const BLOOM_KEYS: &[&str] = &[
87    "clamp",
88    "down",
89    "gains",
90    "kind",
91    "knee",
92    "knee_width",
93    "linear_taps",
94    "radii",
95    "radius",
96    "sigma",
97    "soft",
98    "strength",
99    "threshold",
100    "unit",
101];
102
103const VIGNETTE_KEYS: &[&str] = &[
104    "aspect", "inner", "kind", "outer", "power", "scale", "strength",
105];
106
107const GRAIN_KEYS: &[&str] = &["clamp", "response", "strength"];
108
109const LUT_KEYS: &[&str] = &["size", "values"];
110
111const OUTLINE_KEYS: &[&str] = &[
112    "alpha",
113    "color",
114    "crease_angle",
115    "enabled",
116    "local_color",
117    "thickness",
118];
119
120const SCRATCH_KEYS: &[&str] = &["count", "strength"];
121
122const KINDS: &[&str] = &["box", "rings", "tent"];
123
124const VIGNETTE_KINDS: &[&str] = &["power", "smoothstep"];
125
126const WARMTH_KEYS: &[&str] = &["amount", "high", "highlight", "kind", "low", "shadow"];
127
128const TONE_KEYS: &[&str] = &["clamp", "kind", "start"];
129
130const TONES: &[&str] = &["aces", "agx", "neutral"];
131
132const DITHER_KINDS: &[&str] = &["blue", "ordered"];
133
134#[derive(Clone, Debug)]
135pub struct ParseError {
136    message: String,
137}
138
139impl ParseError {
140    fn msg(message: impl Into<String>) -> Self {
141        Self {
142            message: message.into(),
143        }
144    }
145}
146
147impl std::fmt::Display for ParseError {
148    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149        f.write_str(&self.message)
150    }
151}
152
153impl std::error::Error for ParseError {}
154
155pub fn parse_str(text: &str) -> Result<Chain, ParseError> {
156    let table: Table = text
157        .parse()
158        .map_err(|err| ParseError::msg(format!("toml: {err}")))?;
159    parse(&table)
160}
161
162pub fn parse(table: &Table) -> Result<Chain, ParseError> {
163    if let Some(bad) = table.keys().find(|key| !KEYS.contains(&key.as_str())) {
164        return Err(ParseError::msg(format!(
165            "unknown key {bad}; allowed: {}",
166            KEYS.join(", ")
167        )));
168    }
169    let mut chain = match table.get("style") {
170        None => Chain::new(),
171        Some(value) => {
172            let name = value
173                .as_str()
174                .ok_or_else(|| ParseError::msg("style is a string"))?;
175            Style::parse(name)
176                .ok_or_else(|| {
177                    ParseError::msg(format!(
178                        "unknown style {name}; allowed: {}",
179                        preset::names().join(", ")
180                    ))
181                })?
182                .chain()
183        }
184    };
185    if let Some(value) = table.get("seed") {
186        chain.seed = integer(value, "seed")?;
187    }
188    if let Some(value) = table.get("frame") {
189        chain.frame = integer(value, "frame")?;
190    }
191    for (key, value) in table {
192        match key.as_str() {
193            "style" | "seed" | "frame" => {}
194            "exposure" => set_exposure(&mut chain, number(value, key)?),
195            "black" => set_black(&mut chain, number(value, key)?),
196            "black_threshold" => {
197                let n = number(value, key)?;
198                set_modern_comic(&mut chain, |p| p.black_threshold = n);
199            }
200            "saturation" => set_saturation(&mut chain, number(value, key)?),
201            "saturation_lift" => {
202                let n = number(value, key)?;
203                set_modern_comic(&mut chain, |p| p.saturation_lift = n);
204            }
205            "contrast" => set_contrast(&mut chain, number(value, key)?),
206            "warmth" => set_warmth(&mut chain, value)?,
207            "gain" => set_gain(&mut chain, number(value, key)?),
208            "tone" => set_tone(&mut chain, value)?,
209            "tone_steps" => {
210                let n = number_u(value, key)?;
211                if !(3..=5).contains(&n) {
212                    return Err(ParseError::msg("tone_steps is 3 to 5"));
213                }
214                set_modern_comic(&mut chain, |p| p.tone_steps = n);
215            }
216            "highlight_threshold" => {
217                let n = number(value, key)?;
218                set_modern_comic(&mut chain, |p| p.highlight_threshold = n);
219            }
220            "line_weight" => {
221                let n = number(value, key)?;
222                set_modern_comic(&mut chain, |p| p.line_weight = n);
223            }
224            "interior_line_strength" => {
225                let n = number(value, key)?;
226                set_modern_comic(&mut chain, |p| p.interior_line_strength = n);
227            }
228            "threshold" => set_threshold(&mut chain, number(value, key)?),
229            "bloom" => set_bloom(&mut chain, value)?,
230            "vignette" => set_vignette(&mut chain, value)?,
231            "grain" => set_grain(&mut chain, value)?,
232            "dither" => set_dither(&mut chain, value)?,
233            "dither_kind" => set_dither_kind(&mut chain, value)?,
234            "encode" => {
235                if boolean(value, key)? {
236                    push_missing(
237                        &mut chain,
238                        |pass| matches!(pass, Pass::Encode),
239                        Pass::Encode,
240                    );
241                }
242            }
243            "lut" => set_lut(&mut chain, value)?,
244            "weights" => set_weights(&mut chain, value)?,
245            "cel_bands" => {
246                let n = number_u(value, key)?;
247                set_cel(&mut chain, |cel| cel.bands = n);
248            }
249            "cel_shadow" => {
250                let n = number(value, key)?;
251                set_cel(&mut chain, |cel| cel.shadow = n);
252            }
253            "cel_threshold" => {
254                let n = number(value, key)?;
255                set_cel(&mut chain, |cel| cel.threshold = n);
256            }
257            "cel_softness" => {
258                let n = number(value, key)?;
259                set_cel(&mut chain, |cel| cel.softness = n);
260            }
261            "cel_spec" => {
262                let n = number(value, key)?;
263                set_cel(&mut chain, |cel| cel.spec = n);
264            }
265            "cel_spec_roughness" => {
266                let n = number(value, key)?;
267                set_cel(&mut chain, |cel| cel.spec_roughness = n);
268            }
269            "cel_spec_threshold" => {
270                let n = number(value, key)?;
271                set_cel(&mut chain, |cel| cel.spec_threshold = n);
272            }
273            "cavity" => {
274                let n = number(value, key)?;
275                set_cavity(&mut chain, |c| c.strength = n);
276            }
277            "cavity_distance" => {
278                let n = number(value, key)?;
279                set_cavity(&mut chain, |c| c.distance = n);
280            }
281            "rim" => {
282                let n = number(value, key)?;
283                set_rim(&mut chain, |r| r.strength = n);
284            }
285            "rim_width" => {
286                let n = number(value, key)?;
287                set_rim(&mut chain, |r| r.width = n);
288            }
289            "rim_color" => {
290                let c = color(value, key)?;
291                set_rim(&mut chain, |r| r.color = c);
292            }
293            "outline" => set_outline(&mut chain, value)?,
294            "outline_thickness" => {
295                let n = number(value, key)?;
296                set_outline_field(&mut chain, |o| o.thickness = n);
297            }
298            "outline_alpha" => {
299                let n = number(value, key)?;
300                set_outline_field(&mut chain, |o| o.alpha = n);
301            }
302            "outline_color" => {
303                let c = color(value, key)?;
304                set_outline_field(&mut chain, |o| o.color = c);
305            }
306            "crease_angle" => {
307                let n = number(value, key)?;
308                set_outline_field(&mut chain, |o| o.crease_angle = n);
309            }
310            "keep" => {
311                let c = color(value, key)?;
312                set_noir(&mut chain, |n| n.keep = Some(c));
313            }
314            "keep_range" => {
315                let n = number(value, key)?;
316                set_noir(&mut chain, |noir| noir.keep_range = n);
317            }
318            "crush" => {
319                let n = number(value, key)?;
320                set_noir(&mut chain, |noir| noir.crush = n);
321            }
322            "sepia" => set_sepia(&mut chain, number(value, key)?),
323            "posterize" | "levels" => set_levels(&mut chain, number(value, key)?),
324            "flicker" => set_f32(
325                &mut chain,
326                number(value, key)?,
327                |p| matches!(p, Pass::Flicker(_)),
328                Pass::Flicker,
329            ),
330            "weave" => set_f32(
331                &mut chain,
332                number(value, key)?,
333                |p| matches!(p, Pass::Weave(_)),
334                Pass::Weave,
335            ),
336            "dust" => set_f32(
337                &mut chain,
338                number(value, key)?,
339                |p| matches!(p, Pass::Dust(_)),
340                Pass::Dust,
341            ),
342            "scratches" => set_scratches(&mut chain, value)?,
343            "halation" => set_halation(&mut chain, number(value, key)?),
344            "aberration" => set_f32(
345                &mut chain,
346                number(value, key)?,
347                |p| matches!(p, Pass::Aberration(_)),
348                Pass::Aberration,
349            ),
350            "distortion" => set_f32(
351                &mut chain,
352                number(value, key)?,
353                |p| matches!(p, Pass::Distortion(_)),
354                Pass::Distortion,
355            ),
356            "curvature" => set_f32(
357                &mut chain,
358                number(value, key)?,
359                |p| matches!(p, Pass::Curvature(_)),
360                Pass::Curvature,
361            ),
362            "scanlines" => set_scanlines(&mut chain, number(value, key)?),
363            "mask" => set_f32(
364                &mut chain,
365                number(value, key)?,
366                |p| matches!(p, Pass::Aperture(_)),
367                Pass::Aperture,
368            ),
369            "dot" => {
370                let n = number(value, key)?;
371                set_halftone(&mut chain, |h| h.cell = n);
372            }
373            "angle" => {
374                let n = number(value, key)?;
375                set_halftone(&mut chain, |h| h.angle = n);
376            }
377            "ink" => set_ink(&mut chain, value)?,
378            "darkening" => {
379                let n = number(value, key)?;
380                set_water(&mut chain, |w| w.darkening = n);
381            }
382            "paper_grain" => set_paper_grain(&mut chain, number(value, key)?),
383            "scale" => set_scale(&mut chain, number(value, key)?),
384            "pixel" => set_pixel_size(&mut chain, integer(value, key)?),
385            "palette" => set_palette(&mut chain, boolean(value, key)?),
386            "shadow" => {
387                let c = color(value, key)?;
388                set_duotone(&mut chain, |d| d.shadow = c);
389            }
390            "highlight" => {
391                let c = color(value, key)?;
392                set_duotone(&mut chain, |d| d.highlight = c);
393            }
394            "radius" => set_radius(&mut chain, number(value, key)?),
395            "tape" => {
396                let tape = crate::tape::read(value, chain.tape()).map_err(ParseError::msg)?;
397                chain.set_tape(tape);
398            }
399            "focus" => {
400                let n = number(value, key)?;
401                set_tilt(&mut chain, |t| t.focus = n);
402            }
403            "tilt_range" => {
404                let n = number(value, key)?;
405                set_tilt(&mut chain, |t| t.range = n);
406            }
407            other => {
408                return Err(ParseError::msg(format!(
409                    "unknown key {other}; allowed: {}",
410                    KEYS.join(", ")
411                )));
412            }
413        }
414    }
415    Ok(chain)
416}
417
418fn number_u(value: &Value, key: &str) -> Result<u32, ParseError> {
419    let n = number(value, key)?;
420    if n < 0.0 {
421        return Err(ParseError::msg(format!("{key} is a positive number")));
422    }
423    Ok(n.round() as u32)
424}
425
426fn number(value: &Value, key: &str) -> Result<f32, ParseError> {
427    let n = if let Some(n) = value.as_float() {
428        n
429    } else if let Some(n) = value.as_integer() {
430        n as f64
431    } else {
432        return Err(ParseError::msg(format!("{key} is a number")));
433    };
434    if !n.is_finite() {
435        return Err(ParseError::msg(format!("{key} is a number")));
436    }
437    Ok(n as f32)
438}
439
440fn integer(value: &Value, key: &str) -> Result<u32, ParseError> {
441    let Some(n) = value.as_integer() else {
442        return Err(ParseError::msg(format!("{key} is an integer")));
443    };
444    u32::try_from(n).map_err(|_| ParseError::msg(format!("{key} is an integer")))
445}
446
447fn boolean(value: &Value, key: &str) -> Result<bool, ParseError> {
448    value
449        .as_bool()
450        .ok_or_else(|| ParseError::msg(format!("{key} is a bool")))
451}
452
453fn color(value: &Value, key: &str) -> Result<[f32; 3], ParseError> {
454    if let Some(text) = value.as_str() {
455        return color::parse_hex(text)
456            .ok_or_else(|| ParseError::msg(format!("{key} is #rrggbb or three numbers")));
457    }
458    let Some(items) = value.as_array() else {
459        return Err(ParseError::msg(format!(
460            "{key} is #rrggbb or three numbers"
461        )));
462    };
463    if items.len() != 3 {
464        return Err(ParseError::msg(format!(
465            "{key} is #rrggbb or three numbers"
466        )));
467    }
468    Ok([
469        number(&items[0], key)?,
470        number(&items[1], key)?,
471        number(&items[2], key)?,
472    ])
473}
474
475fn floats(value: &Value, key: &str, len: usize) -> Result<Vec<f32>, ParseError> {
476    let Some(items) = value.as_array() else {
477        return Err(ParseError::msg(format!("{key} is {len} numbers")));
478    };
479    if items.len() != len {
480        return Err(ParseError::msg(format!("{key} is {len} numbers")));
481    }
482    items.iter().map(|item| number(item, key)).collect()
483}
484
485fn unknown(prefix: &str, key: &str, allowed: &[&str]) -> ParseError {
486    ParseError::msg(format!(
487        "unknown key {prefix}.{key}; allowed: {}",
488        allowed.join(", ")
489    ))
490}
491
492fn push_missing(chain: &mut Chain, found: impl Fn(&Pass) -> bool, pass: Pass) {
493    if !chain.passes.iter().any(found) {
494        chain.passes.push(pass);
495    }
496}
497
498fn set_exposure(chain: &mut Chain, value: f32) {
499    for pass in &mut chain.passes {
500        if let Pass::Exposure(slot) = pass {
501            *slot = value;
502            return;
503        }
504    }
505    chain.passes.insert(0, Pass::Exposure(value));
506}
507
508fn set_black(chain: &mut Chain, value: f32) {
509    for pass in &mut chain.passes {
510        if let Pass::Black(slot) = pass {
511            *slot = value;
512            return;
513        }
514    }
515    chain.passes.push(Pass::Black(value));
516}
517
518fn set_saturation(chain: &mut Chain, value: f32) {
519    for pass in &mut chain.passes {
520        if let Pass::Saturation(slot) = pass {
521            slot.amount = value;
522            return;
523        }
524    }
525    chain
526        .passes
527        .push(Pass::Saturation(Saturation::rec709(value)));
528}
529
530fn set_contrast(chain: &mut Chain, value: f32) {
531    for pass in &mut chain.passes {
532        match pass {
533            Pass::Contrast(Contrast::Power { power, .. }) => {
534                *power = value;
535                return;
536            }
537            Pass::Contrast(Contrast::Pivot { amount, .. }) => {
538                *amount = value;
539                return;
540            }
541            Pass::Noir(noir) => {
542                noir.contrast = value;
543                return;
544            }
545            _ => {}
546        }
547    }
548    chain.passes.push(Pass::Contrast(Contrast::Pivot {
549        amount: value,
550        pivot: 0.5,
551    }));
552}
553
554fn set_warmth(chain: &mut Chain, value: &Value) -> Result<(), ParseError> {
555    if let Some(table) = value.as_table() {
556        if let Some(bad) = table
557            .keys()
558            .find(|key| !WARMTH_KEYS.contains(&key.as_str()))
559        {
560            return Err(unknown("warmth", bad, WARMTH_KEYS));
561        }
562        if let Some(kind) = table.get("kind") {
563            let name = kind
564                .as_str()
565                .ok_or_else(|| ParseError::msg("warmth.kind is a string"))?;
566            let amount = warmth_amount(chain);
567            let next = match name {
568                "linear" => Warmth::linear(amount),
569                "luma_curve" => Warmth::luma_curve(amount, [1.0; 2], [1.0; 2], 0.0, 1.0),
570                other => {
571                    return Err(ParseError::msg(format!(
572                        "unknown warmth.kind {other}; allowed: linear, luma_curve"
573                    )));
574                }
575            };
576            replace_warmth(chain, next);
577        }
578        if let Some(v) = table.get("amount") {
579            warmth_mut(chain).amount = number(v, "warmth.amount")?;
580        }
581        if let Some(v) = table.get("low") {
582            let items = floats(v, "warmth.low", 2)?;
583            warmth_mut(chain).low = [items[0], items[1]];
584        }
585        if let Some(v) = table.get("high") {
586            let items = floats(v, "warmth.high", 2)?;
587            warmth_mut(chain).high = [items[0], items[1]];
588        }
589        if let Some(v) = table.get("shadow") {
590            warmth_mut(chain).shadow = number(v, "warmth.shadow")?;
591        }
592        if let Some(v) = table.get("highlight") {
593            warmth_mut(chain).highlight = number(v, "warmth.highlight")?;
594        }
595        if !chain
596            .passes
597            .iter()
598            .any(|pass| matches!(pass, Pass::Warmth(_)))
599        {
600            chain.passes.push(Pass::Warmth(Warmth::linear(0.0)));
601        }
602        return Ok(());
603    }
604    let n = number(value, "warmth")?;
605    if chain
606        .passes
607        .iter()
608        .any(|pass| matches!(pass, Pass::Warmth(_)))
609    {
610        warmth_mut(chain).amount = n;
611    } else {
612        chain.passes.push(Pass::Warmth(Warmth::linear(n)));
613    }
614    Ok(())
615}
616
617fn warmth_amount(chain: &Chain) -> f32 {
618    chain
619        .passes
620        .iter()
621        .find_map(|pass| match pass {
622            Pass::Warmth(warmth) => Some(warmth.amount),
623            _ => None,
624        })
625        .unwrap_or(0.0)
626}
627
628fn replace_warmth(chain: &mut Chain, warmth: Warmth) {
629    for pass in &mut chain.passes {
630        if let Pass::Warmth(slot) = pass {
631            *slot = warmth;
632            return;
633        }
634    }
635    chain.passes.push(Pass::Warmth(warmth));
636}
637
638fn warmth_mut(chain: &mut Chain) -> &mut Warmth {
639    if !chain
640        .passes
641        .iter()
642        .any(|pass| matches!(pass, Pass::Warmth(_)))
643    {
644        chain.passes.push(Pass::Warmth(Warmth::linear(0.0)));
645    }
646    let Some(Pass::Warmth(slot)) = chain
647        .passes
648        .iter_mut()
649        .find(|pass| matches!(pass, Pass::Warmth(_)))
650    else {
651        unreachable!("warmth was inserted");
652    };
653    slot
654}
655
656fn set_gain(chain: &mut Chain, value: f32) {
657    for pass in &mut chain.passes {
658        if let Pass::Tone(Tone::Aces { gain }) = pass {
659            *gain = value;
660            return;
661        }
662    }
663    chain.passes.push(Pass::Tone(Tone::Aces { gain: value }));
664}
665
666fn set_tone(chain: &mut Chain, value: &Value) -> Result<(), ParseError> {
667    let tone = if let Some(name) = value.as_str() {
668        match name {
669            "aces" => Tone::aces(),
670            "agx" => Tone::Agx,
671            "neutral" => existing_neutral(chain).unwrap_or_else(|| Tone::neutral(1.0, false)),
672            other => {
673                return Err(ParseError::msg(format!(
674                    "unknown tone {other}; allowed: {}",
675                    TONES.join(", ")
676                )));
677            }
678        }
679    } else if let Some(table) = value.as_table() {
680        tone_from_table(table)?
681    } else {
682        return Err(ParseError::msg("tone is aces, agx, neutral, or a table"));
683    };
684    for pass in &mut chain.passes {
685        if let Pass::Tone(slot) = pass {
686            if matches!(
687                (&*slot, &tone),
688                (Tone::Neutral { .. }, Tone::Neutral { .. })
689            ) {
690                return Ok(());
691            }
692            *slot = tone;
693            return Ok(());
694        }
695    }
696    chain.passes.push(Pass::Tone(tone));
697    Ok(())
698}
699
700fn tone_from_table(table: &Table) -> Result<Tone, ParseError> {
701    if let Some(bad) = table.keys().find(|key| !TONE_KEYS.contains(&key.as_str())) {
702        return Err(unknown("tone", bad, TONE_KEYS));
703    }
704    let kind = table
705        .get("kind")
706        .and_then(Value::as_str)
707        .ok_or_else(|| ParseError::msg("tone.kind is a string"))?;
708    match kind {
709        "aces" | "agx" => {
710            if table.contains_key("start") || table.contains_key("clamp") {
711                return Err(ParseError::msg("tone.start and tone.clamp are for neutral"));
712            }
713            Ok(if kind == "aces" {
714                Tone::aces()
715            } else {
716                Tone::Agx
717            })
718        }
719        "neutral" => {
720            let start = match table.get("start") {
721                Some(value) => number(value, "tone.start")?,
722                None => 1.0,
723            };
724            let clamp = match table.get("clamp") {
725                Some(value) => boolean(value, "tone.clamp")?,
726                None => false,
727            };
728            Ok(Tone::neutral(start, clamp))
729        }
730        other => Err(ParseError::msg(format!(
731            "unknown tone.kind {other}; allowed: {}",
732            TONES.join(", ")
733        ))),
734    }
735}
736
737fn existing_neutral(chain: &Chain) -> Option<Tone> {
738    chain.passes.iter().find_map(|pass| match pass {
739        Pass::Tone(tone @ Tone::Neutral { .. }) => Some(*tone),
740        _ => None,
741    })
742}
743
744fn ensure_bloom(chain: &mut Chain) -> &mut Bloom {
745    if !chain
746        .passes
747        .iter()
748        .any(|pass| matches!(pass, Pass::Bloom(_)))
749    {
750        let index = chain
751            .passes
752            .iter()
753            .position(|pass| matches!(pass, Pass::Exposure(_)))
754            .map(|i| i + 1)
755            .unwrap_or(0);
756        chain.passes.insert(index, Pass::Bloom(Bloom::neutral(0.0)));
757    }
758    let Some(Pass::Bloom(bloom)) = chain
759        .passes
760        .iter_mut()
761        .find(|pass| matches!(pass, Pass::Bloom(_)))
762    else {
763        unreachable!("bloom was inserted");
764    };
765    bloom
766}
767
768fn set_threshold(chain: &mut Chain, value: f32) {
769    ensure_bloom(chain).threshold = value;
770}
771
772fn set_bloom(chain: &mut Chain, value: &Value) -> Result<(), ParseError> {
773    if let Some(table) = value.as_table() {
774        if let Some(bad) = table.keys().find(|key| !BLOOM_KEYS.contains(&key.as_str())) {
775            return Err(unknown("bloom", bad, BLOOM_KEYS));
776        }
777        let bloom = ensure_bloom(chain);
778        if let Some(kind) = table.get("kind") {
779            let name = kind
780                .as_str()
781                .ok_or_else(|| ParseError::msg("bloom.kind is a string"))?;
782            *bloom = match name {
783                "box" => Bloom::neutral(bloom.strength),
784                "tent" => Bloom::tent(bloom.strength, 1.0, 4, 16.0, 2),
785                "rings" => Bloom::rings(bloom.strength, 1.0, 4, NEUTRAL_RADII, NEUTRAL_GAINS),
786                other => {
787                    return Err(ParseError::msg(format!(
788                        "unknown bloom.kind {other}; allowed: {}",
789                        KINDS.join(", ")
790                    )));
791                }
792            };
793        }
794        let bloom = ensure_bloom(chain);
795        if let Some(v) = table.get("strength") {
796            bloom.strength = number(v, "bloom.strength")?;
797        }
798        if let Some(v) = table.get("threshold") {
799            bloom.threshold = number(v, "bloom.threshold")?;
800        }
801        if let Some(v) = table.get("knee") {
802            bloom.knee = number(v, "bloom.knee")?;
803        }
804        if let Some(v) = table.get("knee_width") {
805            bloom.knee_width = number(v, "bloom.knee_width")?;
806        }
807        if let Some(v) = table.get("soft") {
808            bloom.soft = number(v, "bloom.soft")?;
809        }
810        if let Some(v) = table.get("radius") {
811            bloom.radius = number_u(v, "bloom.radius")?;
812        }
813        if let Some(v) = table.get("sigma") {
814            bloom.spread = number(v, "bloom.sigma")?;
815        }
816        if let Some(v) = table.get("down") {
817            bloom.down = number_u(v, "bloom.down")?.max(1);
818        }
819        if let Some(v) = table.get("clamp") {
820            bloom.clamp = number(v, "bloom.clamp")?;
821        }
822        if let Some(v) = table.get("unit") {
823            bloom.unit = number(v, "bloom.unit")?;
824        }
825        if let Some(v) = table.get("linear_taps") {
826            bloom.linear_taps = boolean(v, "bloom.linear_taps")?;
827        }
828        if let Some(v) = table.get("radii") {
829            let items = floats(v, "bloom.radii", 4)?;
830            bloom.radii = [items[0], items[1], items[2], items[3]];
831        }
832        if let Some(v) = table.get("gains") {
833            let items = floats(v, "bloom.gains", 4)?;
834            bloom.gains = [items[0], items[1], items[2], items[3]];
835        }
836        return Ok(());
837    }
838    ensure_bloom(chain).strength = number(value, "bloom")?;
839    Ok(())
840}
841
842fn set_vignette(chain: &mut Chain, value: &Value) -> Result<(), ParseError> {
843    if let Some(table) = value.as_table() {
844        if let Some(bad) = table
845            .keys()
846            .find(|key| !VIGNETTE_KEYS.contains(&key.as_str()))
847        {
848            return Err(unknown("vignette", bad, VIGNETTE_KEYS));
849        }
850        ensure_vignette(chain);
851        if let Some(kind) = table.get("kind") {
852            let name = kind
853                .as_str()
854                .ok_or_else(|| ParseError::msg("vignette.kind is a string"))?;
855            let strength = vignette_strength(chain);
856            let next = match name {
857                "power" => Vignette::power(strength, 2.0, 1.0, 1.0),
858                "smoothstep" => Vignette::smoothstep(strength, 1.0, 0.0, 1.0),
859                other => {
860                    return Err(ParseError::msg(format!(
861                        "unknown vignette.kind {other}; allowed: {}",
862                        VIGNETTE_KINDS.join(", ")
863                    )));
864                }
865            };
866            replace_vignette(chain, next);
867        }
868        if let Some(v) = table.get("strength") {
869            let n = number(v, "vignette.strength")?;
870            vignette_mut(chain).strength = n;
871        }
872        if let Some(v) = table.get("power") {
873            let n = number(v, "vignette.power")?;
874            vignette_mut(chain).power = n;
875        }
876        if let Some(v) = table.get("aspect") {
877            let n = number(v, "vignette.aspect")?;
878            vignette_mut(chain).aspect = n;
879        }
880        if let Some(v) = table.get("scale") {
881            let n = number(v, "vignette.scale")?;
882            vignette_mut(chain).scale = n;
883        }
884        if let Some(v) = table.get("inner") {
885            let n = number(v, "vignette.inner")?;
886            vignette_mut(chain).inner = n;
887        }
888        if let Some(v) = table.get("outer") {
889            let n = number(v, "vignette.outer")?;
890            vignette_mut(chain).outer = n;
891        }
892        return Ok(());
893    }
894    let n = number(value, "vignette")?;
895    if chain
896        .passes
897        .iter()
898        .any(|pass| matches!(pass, Pass::Vignette(_)))
899    {
900        vignette_mut(chain).strength = n;
901    } else {
902        chain
903            .passes
904            .push(Pass::Vignette(Vignette::power(n, 2.0, 1.0, 1.0)));
905    }
906    Ok(())
907}
908
909fn ensure_vignette(chain: &mut Chain) {
910    if !chain
911        .passes
912        .iter()
913        .any(|pass| matches!(pass, Pass::Vignette(_)))
914    {
915        chain
916            .passes
917            .push(Pass::Vignette(Vignette::power(0.0, 2.0, 1.0, 1.0)));
918    }
919}
920
921fn vignette_strength(chain: &Chain) -> f32 {
922    chain
923        .passes
924        .iter()
925        .find_map(|pass| match pass {
926            Pass::Vignette(v) => Some(v.strength),
927            _ => None,
928        })
929        .unwrap_or(0.0)
930}
931
932fn replace_vignette(chain: &mut Chain, vignette: Vignette) {
933    for pass in &mut chain.passes {
934        if let Pass::Vignette(slot) = pass {
935            *slot = vignette;
936            return;
937        }
938    }
939}
940
941fn vignette_mut(chain: &mut Chain) -> &mut Vignette {
942    ensure_vignette(chain);
943    let Some(Pass::Vignette(slot)) = chain
944        .passes
945        .iter_mut()
946        .find(|pass| matches!(pass, Pass::Vignette(_)))
947    else {
948        unreachable!("vignette was inserted");
949    };
950    slot
951}
952
953fn set_grain(chain: &mut Chain, value: &Value) -> Result<(), ParseError> {
954    if let Some(table) = value.as_table() {
955        if let Some(bad) = table.keys().find(|key| !GRAIN_KEYS.contains(&key.as_str())) {
956            return Err(unknown("grain", bad, GRAIN_KEYS));
957        }
958        ensure_grain(chain);
959        if let Some(v) = table.get("strength") {
960            let n = number(v, "grain.strength")?;
961            grain_mut(chain).strength = n;
962        }
963        if let Some(v) = table.get("response") {
964            let n = number(v, "grain.response")?;
965            grain_mut(chain).response = n;
966        }
967        if let Some(v) = table.get("clamp") {
968            let n = boolean(v, "grain.clamp")?;
969            grain_mut(chain).clamp = n;
970        }
971        return Ok(());
972    }
973    let n = number(value, "grain")?;
974    if chain
975        .passes
976        .iter()
977        .any(|pass| matches!(pass, Pass::Grain(_)))
978    {
979        grain_mut(chain).strength = n;
980    } else {
981        chain.passes.push(Pass::Grain(Grain {
982            strength: n,
983            response: 0.0,
984            clamp: false,
985        }));
986    }
987    Ok(())
988}
989
990fn ensure_grain(chain: &mut Chain) {
991    if !chain
992        .passes
993        .iter()
994        .any(|pass| matches!(pass, Pass::Grain(_)))
995    {
996        chain.passes.push(Pass::Grain(Grain {
997            strength: 0.0,
998            response: 0.0,
999            clamp: false,
1000        }));
1001    }
1002}
1003
1004fn grain_mut(chain: &mut Chain) -> &mut Grain {
1005    ensure_grain(chain);
1006    let Some(Pass::Grain(slot)) = chain
1007        .passes
1008        .iter_mut()
1009        .find(|pass| matches!(pass, Pass::Grain(_)))
1010    else {
1011        unreachable!("grain was inserted");
1012    };
1013    slot
1014}
1015
1016fn set_dither(chain: &mut Chain, value: &Value) -> Result<(), ParseError> {
1017    let amplitude = if let Some(on) = value.as_bool() {
1018        if on { 1.0 } else { 0.0 }
1019    } else {
1020        number(value, "dither")?
1021    };
1022    for pass in &mut chain.passes {
1023        if let Pass::Dither(slot) = pass {
1024            slot.amplitude = amplitude;
1025            return Ok(());
1026        }
1027    }
1028    chain.passes.push(Pass::Dither(Dither { amplitude }));
1029    Ok(())
1030}
1031
1032fn set_dither_kind(chain: &mut Chain, value: &Value) -> Result<(), ParseError> {
1033    let name = value
1034        .as_str()
1035        .ok_or_else(|| ParseError::msg("dither_kind is a string"))?;
1036    let kind = match name {
1037        "ordered" => NoiseKind::Ordered,
1038        "blue" => NoiseKind::Blue,
1039        other => {
1040            return Err(ParseError::msg(format!(
1041                "unknown dither_kind {other}; allowed: {}",
1042                DITHER_KINDS.join(", ")
1043            )));
1044        }
1045    };
1046    for pass in &mut chain.passes {
1047        if let Pass::OneBit(slot) = pass {
1048            slot.kind = kind;
1049            return Ok(());
1050        }
1051    }
1052    chain.passes.push(Pass::OneBit(fx::OneBit { kind }));
1053    Ok(())
1054}
1055
1056fn set_lut(chain: &mut Chain, value: &Value) -> Result<(), ParseError> {
1057    let Some(table) = value.as_table() else {
1058        return Err(ParseError::msg("lut is a table"));
1059    };
1060    if let Some(bad) = table.keys().find(|key| !LUT_KEYS.contains(&key.as_str())) {
1061        return Err(unknown("lut", bad, LUT_KEYS));
1062    }
1063    let size = table
1064        .get("size")
1065        .ok_or_else(|| ParseError::msg("lut.size is an integer"))
1066        .and_then(|v| integer(v, "lut.size"))?;
1067    if !(2..=32).contains(&size) {
1068        return Err(ParseError::msg("lut.size is from 2 to 32"));
1069    }
1070    let values = table
1071        .get("values")
1072        .ok_or_else(|| ParseError::msg("lut.values is a list"))?;
1073    let Some(items) = values.as_array() else {
1074        return Err(ParseError::msg("lut.values is a list"));
1075    };
1076    let need = (size as usize) * (size as usize) * (size as usize) * 3;
1077    if items.len() != need {
1078        return Err(ParseError::msg(format!("lut.values has {need} numbers")));
1079    }
1080    let mut rgb = Vec::with_capacity(need / 3);
1081    for chunk in items.chunks(3) {
1082        rgb.push([
1083            number(&chunk[0], "lut.values")?,
1084            number(&chunk[1], "lut.values")?,
1085            number(&chunk[2], "lut.values")?,
1086        ]);
1087    }
1088    let lut = crate::grade::Lut { size, values: rgb };
1089    for pass in &mut chain.passes {
1090        if let Pass::Lut(slot) = pass {
1091            *slot = lut;
1092            return Ok(());
1093        }
1094    }
1095    let index = chain
1096        .passes
1097        .iter()
1098        .position(|pass| matches!(pass, Pass::Tone(_)))
1099        .unwrap_or(chain.passes.len());
1100    chain.passes.insert(index, Pass::Lut(lut));
1101    Ok(())
1102}
1103
1104fn set_weights(chain: &mut Chain, value: &Value) -> Result<(), ParseError> {
1105    let items = floats(value, "weights", 3)?;
1106    let weights = [items[0], items[1], items[2]];
1107    for pass in &mut chain.passes {
1108        match pass {
1109            Pass::Bw(slot) => {
1110                slot.weights = weights;
1111                return Ok(());
1112            }
1113            Pass::Saturation(slot) => {
1114                slot.weights = weights;
1115                return Ok(());
1116            }
1117            _ => {}
1118        }
1119    }
1120    chain.passes.push(Pass::Bw(fx::Bw { weights }));
1121    Ok(())
1122}
1123
1124fn default_cel() -> fx::Cel {
1125    fx::Cel {
1126        bands: 3,
1127        shadow: 0.45,
1128        threshold: 0.45,
1129        softness: 0.08,
1130        spec: 0.0,
1131        spec_roughness: 0.25,
1132        spec_threshold: 0.72,
1133        light: [0.3, 0.75, 0.6],
1134    }
1135}
1136
1137fn set_cel(chain: &mut Chain, edit: impl FnOnce(&mut fx::Cel)) {
1138    if !chain.passes.iter().any(|pass| matches!(pass, Pass::Cel(_))) {
1139        chain.passes.insert(0, Pass::Cel(default_cel()));
1140    }
1141    if let Some(Pass::Cel(slot)) = chain
1142        .passes
1143        .iter_mut()
1144        .find(|pass| matches!(pass, Pass::Cel(_)))
1145    {
1146        edit(slot);
1147    }
1148}
1149
1150fn set_cavity(chain: &mut Chain, edit: impl FnOnce(&mut fx::Cavity)) {
1151    if !chain
1152        .passes
1153        .iter()
1154        .any(|pass| matches!(pass, Pass::Cavity(_)))
1155    {
1156        chain.passes.push(Pass::Cavity(fx::Cavity {
1157            strength: 0.35,
1158            distance: 2.0,
1159        }));
1160    }
1161    if let Some(Pass::Cavity(slot)) = chain
1162        .passes
1163        .iter_mut()
1164        .find(|pass| matches!(pass, Pass::Cavity(_)))
1165    {
1166        edit(slot);
1167    }
1168}
1169
1170fn set_rim(chain: &mut Chain, edit: impl FnOnce(&mut fx::Rim)) {
1171    if !chain.passes.iter().any(|pass| matches!(pass, Pass::Rim(_))) {
1172        chain.passes.push(Pass::Rim(fx::Rim {
1173            strength: 0.22,
1174            width: 0.35,
1175            color: [1.0, 0.95, 0.85],
1176        }));
1177    }
1178    if let Some(Pass::Rim(slot)) = chain
1179        .passes
1180        .iter_mut()
1181        .find(|pass| matches!(pass, Pass::Rim(_)))
1182    {
1183        edit(slot);
1184    }
1185}
1186
1187fn default_outline() -> fx::Outline {
1188    fx::Outline {
1189        enabled: true,
1190        thickness: 1.25,
1191        color: [0.02, 0.015, 0.02],
1192        local_color: false,
1193        alpha: 0.92,
1194        crease_angle: 35.0,
1195        depth_gap: 0.04,
1196    }
1197}
1198
1199fn set_outline(chain: &mut Chain, value: &Value) -> Result<(), ParseError> {
1200    if let Some(on) = value.as_bool() {
1201        set_outline_field(chain, |outline| outline.enabled = on);
1202        return Ok(());
1203    }
1204    let Some(table) = value.as_table() else {
1205        return Err(ParseError::msg("outline is a bool or a table"));
1206    };
1207    if let Some(bad) = table
1208        .keys()
1209        .find(|key| !OUTLINE_KEYS.contains(&key.as_str()))
1210    {
1211        return Err(unknown("outline", bad, OUTLINE_KEYS));
1212    }
1213    if let Some(v) = table.get("enabled") {
1214        let on = boolean(v, "outline.enabled")?;
1215        set_outline_field(chain, |o| o.enabled = on);
1216    } else {
1217        set_outline_field(chain, |_| {});
1218    }
1219    if let Some(v) = table.get("thickness") {
1220        let n = number(v, "outline.thickness")?;
1221        set_outline_field(chain, |o| o.thickness = n);
1222    }
1223    if let Some(v) = table.get("alpha") {
1224        let n = number(v, "outline.alpha")?;
1225        set_outline_field(chain, |o| o.alpha = n);
1226    }
1227    if let Some(v) = table.get("crease_angle") {
1228        let n = number(v, "outline.crease_angle")?;
1229        set_outline_field(chain, |o| o.crease_angle = n);
1230    }
1231    if let Some(v) = table.get("color") {
1232        let c = color(v, "outline.color")?;
1233        set_outline_field(chain, |o| o.color = c);
1234    }
1235    if let Some(v) = table.get("local_color") {
1236        let on = boolean(v, "outline.local_color")?;
1237        set_outline_field(chain, |o| o.local_color = on);
1238    }
1239    Ok(())
1240}
1241
1242fn set_outline_field(chain: &mut Chain, edit: impl FnOnce(&mut fx::Outline)) {
1243    if !chain
1244        .passes
1245        .iter()
1246        .any(|pass| matches!(pass, Pass::Outline(_)))
1247    {
1248        chain.passes.push(Pass::Outline(default_outline()));
1249    }
1250    if let Some(Pass::Outline(slot)) = chain
1251        .passes
1252        .iter_mut()
1253        .find(|pass| matches!(pass, Pass::Outline(_)))
1254    {
1255        edit(slot);
1256    }
1257}
1258
1259fn set_noir(chain: &mut Chain, edit: impl FnOnce(&mut fx::Noir)) {
1260    if !chain
1261        .passes
1262        .iter()
1263        .any(|pass| matches!(pass, Pass::Noir(_)))
1264    {
1265        chain.passes.insert(
1266            0,
1267            Pass::Noir(fx::Noir {
1268                contrast: 1.9,
1269                crush: 0.18,
1270                keep: None,
1271                keep_range: 0.45,
1272            }),
1273        );
1274    }
1275    if let Some(Pass::Noir(slot)) = chain
1276        .passes
1277        .iter_mut()
1278        .find(|pass| matches!(pass, Pass::Noir(_)))
1279    {
1280        edit(slot);
1281    }
1282}
1283
1284fn set_sepia(chain: &mut Chain, value: f32) {
1285    for pass in &mut chain.passes {
1286        if let Pass::Sepia(slot) = pass {
1287            *slot = value;
1288            return;
1289        }
1290    }
1291    chain.passes.push(Pass::Sepia(value));
1292}
1293
1294fn set_levels(chain: &mut Chain, value: f32) {
1295    let mut hit = false;
1296    for pass in &mut chain.passes {
1297        match pass {
1298            Pass::Posterize(slot) => {
1299                *slot = value;
1300                hit = true;
1301            }
1302            Pass::Comic(slot) => {
1303                slot.levels = value.max(2.0).round() as u32;
1304                hit = true;
1305            }
1306            _ => {}
1307        }
1308    }
1309    if !hit {
1310        chain.passes.push(Pass::Posterize(value));
1311    }
1312}
1313
1314fn set_modern_comic(chain: &mut Chain, edit: impl FnOnce(&mut ModernComic)) {
1315    if let Some(Pass::ModernComic(pass)) = chain
1316        .passes
1317        .iter_mut()
1318        .find(|pass| matches!(pass, Pass::ModernComic(_)))
1319    {
1320        edit(pass);
1321    } else {
1322        let mut pass = ModernComic::default();
1323        edit(&mut pass);
1324        chain.passes.push(Pass::ModernComic(pass));
1325    }
1326}
1327
1328fn set_f32(
1329    chain: &mut Chain,
1330    value: f32,
1331    found: impl Fn(&Pass) -> bool,
1332    make: impl Fn(f32) -> Pass,
1333) {
1334    for pass in &mut chain.passes {
1335        if found(pass) {
1336            match pass {
1337                Pass::Flicker(slot)
1338                | Pass::Weave(slot)
1339                | Pass::Dust(slot)
1340                | Pass::Aberration(slot)
1341                | Pass::Distortion(slot)
1342                | Pass::Curvature(slot)
1343                | Pass::Aperture(slot) => *slot = value,
1344                _ => {}
1345            }
1346            return;
1347        }
1348    }
1349    chain.passes.push(make(value));
1350}
1351
1352fn set_scratches(chain: &mut Chain, value: &Value) -> Result<(), ParseError> {
1353    if let Some(table) = value.as_table() {
1354        if let Some(bad) = table
1355            .keys()
1356            .find(|key| !SCRATCH_KEYS.contains(&key.as_str()))
1357        {
1358            return Err(unknown("scratches", bad, SCRATCH_KEYS));
1359        }
1360        ensure_scratches(chain);
1361        if let Some(v) = table.get("count") {
1362            let n = integer(v, "scratches.count")?;
1363            scratches_mut(chain).count = n;
1364        }
1365        if let Some(v) = table.get("strength") {
1366            let n = number(v, "scratches.strength")?;
1367            scratches_mut(chain).strength = n;
1368        }
1369        return Ok(());
1370    }
1371    let n = number(value, "scratches")?;
1372    if chain
1373        .passes
1374        .iter()
1375        .any(|pass| matches!(pass, Pass::Scratches(_)))
1376    {
1377        scratches_mut(chain).strength = n;
1378    } else {
1379        chain.passes.push(Pass::Scratches(fx::Scratches {
1380            count: 3,
1381            strength: n,
1382        }));
1383    }
1384    Ok(())
1385}
1386
1387fn ensure_scratches(chain: &mut Chain) {
1388    if !chain
1389        .passes
1390        .iter()
1391        .any(|pass| matches!(pass, Pass::Scratches(_)))
1392    {
1393        chain.passes.push(Pass::Scratches(fx::Scratches {
1394            count: 3,
1395            strength: 0.35,
1396        }));
1397    }
1398}
1399
1400fn scratches_mut(chain: &mut Chain) -> &mut fx::Scratches {
1401    ensure_scratches(chain);
1402    let Some(Pass::Scratches(slot)) = chain
1403        .passes
1404        .iter_mut()
1405        .find(|pass| matches!(pass, Pass::Scratches(_)))
1406    else {
1407        unreachable!("scratches were inserted");
1408    };
1409    slot
1410}
1411
1412fn set_halation(chain: &mut Chain, value: f32) {
1413    for pass in &mut chain.passes {
1414        if let Pass::Halation(slot) = pass {
1415            slot.strength = value;
1416            return;
1417        }
1418    }
1419    chain.passes.push(Pass::Halation(fx::Halation {
1420        strength: value,
1421        threshold: 0.65,
1422        radius: 6,
1423    }));
1424}
1425
1426fn set_scanlines(chain: &mut Chain, value: f32) {
1427    for pass in &mut chain.passes {
1428        if let Pass::Scanlines(slot) = pass {
1429            slot.strength = value;
1430            return;
1431        }
1432    }
1433    chain.passes.push(Pass::Scanlines(fx::Scanlines {
1434        strength: value,
1435        period: 2,
1436    }));
1437}
1438
1439fn set_halftone(chain: &mut Chain, edit: impl FnOnce(&mut fx::Halftone)) {
1440    if !chain
1441        .passes
1442        .iter()
1443        .any(|pass| matches!(pass, Pass::Halftone(_)))
1444    {
1445        chain.passes.push(Pass::Halftone(fx::Halftone {
1446            cell: 6.0,
1447            angle: 15.0,
1448            ink: [0.02, 0.02, 0.03],
1449        }));
1450    }
1451    if let Some(Pass::Halftone(slot)) = chain
1452        .passes
1453        .iter_mut()
1454        .find(|pass| matches!(pass, Pass::Halftone(_)))
1455    {
1456        edit(slot);
1457    }
1458}
1459
1460fn set_ink(chain: &mut Chain, value: &Value) -> Result<(), ParseError> {
1461    if value.as_array().is_some() || value.as_str().is_some() {
1462        let c = color(value, "ink")?;
1463        set_halftone(chain, |h| h.ink = c);
1464        return Ok(());
1465    }
1466    let n = number(value, "ink")?;
1467    for pass in &mut chain.passes {
1468        if let Pass::Comic(slot) = pass {
1469            slot.ink = n;
1470            return Ok(());
1471        }
1472    }
1473    chain
1474        .passes
1475        .push(Pass::Comic(fx::Comic { levels: 5, ink: n }));
1476    Ok(())
1477}
1478
1479fn set_water(chain: &mut Chain, edit: impl FnOnce(&mut fx::Watercolor)) {
1480    if !chain
1481        .passes
1482        .iter()
1483        .any(|pass| matches!(pass, Pass::Watercolor(_)))
1484    {
1485        chain.passes.push(Pass::Watercolor(fx::Watercolor {
1486            darkening: 0.65,
1487            grain: 0.22,
1488            scale: 6.0,
1489        }));
1490    }
1491    if let Some(Pass::Watercolor(slot)) = chain
1492        .passes
1493        .iter_mut()
1494        .find(|pass| matches!(pass, Pass::Watercolor(_)))
1495    {
1496        edit(slot);
1497    }
1498}
1499
1500fn set_paper_grain(chain: &mut Chain, value: f32) {
1501    let mut hit = false;
1502    for pass in &mut chain.passes {
1503        match pass {
1504            Pass::PaperGrain(slot) => {
1505                slot.strength = value;
1506                hit = true;
1507            }
1508            Pass::Watercolor(slot) => {
1509                slot.grain = value;
1510                hit = true;
1511            }
1512            _ => {}
1513        }
1514    }
1515    if !hit {
1516        chain.passes.push(Pass::PaperGrain(fx::PaperGrain {
1517            strength: value,
1518            scale: 5.0,
1519        }));
1520    }
1521}
1522
1523fn set_scale(chain: &mut Chain, value: f32) {
1524    let mut hit = false;
1525    for pass in &mut chain.passes {
1526        match pass {
1527            Pass::PaperGrain(slot) => {
1528                slot.scale = value;
1529                hit = true;
1530            }
1531            Pass::Watercolor(slot) => {
1532                slot.scale = value;
1533                hit = true;
1534            }
1535            _ => {}
1536        }
1537    }
1538    if !hit {
1539        chain.passes.push(Pass::PaperGrain(fx::PaperGrain {
1540            strength: 0.28,
1541            scale: value,
1542        }));
1543    }
1544}
1545
1546fn set_pixel_size(chain: &mut Chain, value: u32) {
1547    for pass in &mut chain.passes {
1548        if let Pass::Pixel(slot) = pass {
1549            slot.size = value.max(1);
1550            return;
1551        }
1552    }
1553    chain.passes.push(Pass::Pixel(fx::Pixelate {
1554        size: value.max(1),
1555        levels: 0,
1556        palette: true,
1557    }));
1558}
1559
1560fn set_palette(chain: &mut Chain, value: bool) {
1561    for pass in &mut chain.passes {
1562        if let Pass::Pixel(slot) = pass {
1563            slot.palette = value;
1564            return;
1565        }
1566    }
1567    chain.passes.push(Pass::Pixel(fx::Pixelate {
1568        size: 4,
1569        levels: 0,
1570        palette: value,
1571    }));
1572}
1573
1574fn set_duotone(chain: &mut Chain, edit: impl FnOnce(&mut fx::Duotone)) {
1575    if !chain
1576        .passes
1577        .iter()
1578        .any(|pass| matches!(pass, Pass::Duotone(_)))
1579    {
1580        chain.passes.push(Pass::Duotone(fx::Duotone {
1581            shadow: [0.05, 0.08, 0.18],
1582            highlight: [0.96, 0.82, 0.55],
1583        }));
1584    }
1585    if let Some(Pass::Duotone(slot)) = chain
1586        .passes
1587        .iter_mut()
1588        .find(|pass| matches!(pass, Pass::Duotone(_)))
1589    {
1590        edit(slot);
1591    }
1592}
1593
1594fn set_radius(chain: &mut Chain, value: f32) {
1595    let mut hit = false;
1596    for pass in &mut chain.passes {
1597        match pass {
1598            Pass::Bloom(slot) => {
1599                slot.radius = value.max(0.0).round() as u32;
1600                hit = true;
1601            }
1602            Pass::Kuwahara(slot) => {
1603                *slot = value.max(0.0).round() as u32;
1604                hit = true;
1605            }
1606            Pass::Halation(slot) => {
1607                slot.radius = value.max(0.0).round() as u32;
1608                hit = true;
1609            }
1610            Pass::Neon(slot) => {
1611                slot.radius = value.max(0.0).round() as u32;
1612                hit = true;
1613            }
1614            Pass::TiltShift(slot) => {
1615                slot.radius = value.max(0.0);
1616                hit = true;
1617            }
1618            _ => {}
1619        }
1620    }
1621    if !hit {
1622        chain
1623            .passes
1624            .push(Pass::Kuwahara(value.max(0.0).round() as u32));
1625    }
1626}
1627
1628fn set_tilt(chain: &mut Chain, edit: impl FnOnce(&mut fx::TiltShift)) {
1629    if !chain
1630        .passes
1631        .iter()
1632        .any(|pass| matches!(pass, Pass::TiltShift(_)))
1633    {
1634        chain.passes.push(Pass::TiltShift(fx::TiltShift {
1635            focus: 0.5,
1636            range: 0.22,
1637            radius: 5.0,
1638        }));
1639    }
1640    if let Some(Pass::TiltShift(slot)) = chain
1641        .passes
1642        .iter_mut()
1643        .find(|pass| matches!(pass, Pass::TiltShift(_)))
1644    {
1645        edit(slot);
1646    }
1647}