1use crate::curve::Spline;
30use crate::error::{Error, Result};
31use crate::gradient::{Mixing, Ramp};
32
33#[derive(Debug, Clone, PartialEq)]
35#[non_exhaustive]
36pub enum Correction {
37 BrightnessContrast {
39 brightness: i32,
41 contrast: i32,
43 },
44 Levels(Vec<[f64; 5]>),
47 ToneCurve(Vec<Vec<(f64, f64)>>),
49 HueSaturationValue {
51 hue: i32,
53 saturation: i32,
55 value: i32,
57 },
58 ColorBalance {
60 preserve_luminosity: bool,
62 shadows: [i32; 3],
64 midtones: [i32; 3],
66 highlights: [i32; 3],
68 },
69 Invert,
71 Posterize(i32),
73 Threshold(i32),
75 GradientMap(Ramp),
77}
78
79fn int(data: &[u8], index: usize) -> Result<i32> {
80 data.get(4 * index..4 * index + 4)
81 .map(|b| i32::from_be_bytes(b.try_into().expect("4")))
82 .ok_or_else(|| Error::format("FilterLayerInfo is cut short"))
83}
84
85fn short(data: &[u8], offset: usize) -> Result<f64> {
86 data.get(offset..offset + 2)
87 .map(|b| u16::from_be_bytes([b[0], b[1]]) as f64 / 65535.0)
88 .ok_or_else(|| Error::format("FilterLayerInfo is cut short"))
89}
90
91impl Correction {
92 pub fn parse(data: &[u8]) -> Result<Self> {
94 let kind = int(data, 0)?;
95 let payload = data.get(8..).unwrap_or_default();
96 Ok(match kind {
97 1 => Correction::BrightnessContrast { brightness: int(payload, 0)?, contrast: int(payload, 1)? },
98 2 => Correction::Levels(
99 (0..4)
100 .map(|slot| -> Result<[f64; 5]> {
101 let at = slot * 10;
102 Ok([
103 short(payload, at)?,
104 short(payload, at + 2)?,
105 short(payload, at + 4)?,
106 short(payload, at + 6)?,
107 short(payload, at + 8)?,
108 ])
109 })
110 .collect::<Result<_>>()?,
111 ),
112 3 => Correction::ToneCurve(
113 (0..4)
114 .map(|slot| -> Result<Vec<(f64, f64)>> {
115 let at = slot * 130;
116 let count = (short(payload, at)? * 65535.0).round() as usize;
117 (0..count.min(32))
118 .map(|i| Ok((short(payload, at + 2 + 4 * i)?, short(payload, at + 4 + 4 * i)?)))
119 .collect()
120 })
121 .collect::<Result<_>>()?,
122 ),
123 4 => Correction::HueSaturationValue {
124 hue: int(payload, 0)?,
125 saturation: int(payload, 1)?,
126 value: int(payload, 2)?,
127 },
128 5 => {
129 let v = |i| int(payload, i);
130 Correction::ColorBalance {
131 preserve_luminosity: v(0)? != 0,
132 shadows: [v(1)?, v(2)?, v(3)?],
133 midtones: [v(4)?, v(5)?, v(6)?],
134 highlights: [v(7)?, v(8)?, v(9)?],
135 }
136 }
137 6 => Correction::Invert,
138 7 => Correction::Posterize(int(payload, 0)?),
139 8 => Correction::Threshold(int(payload, 0)?),
140 9 => Correction::GradientMap(Ramp::parse(payload)?),
141 other => return Err(Error::format(format!("FilterLayerInfo type {other}"))),
142 })
143 }
144
145 pub fn is_approximate(&self) -> bool {
148 match self {
149 Correction::GradientMap(ramp) => ramp.mixing == Mixing::Perceptual,
150 _ => false,
151 }
152 }
153
154 pub fn filter(&self) -> Filter<'_> {
156 let tables = match self {
157 Correction::BrightnessContrast { brightness, contrast } => {
158 Some([brightness_contrast(*brightness, *contrast); 3])
159 }
160 Correction::Levels(slots) => {
161 let table = |slot: usize| slots.get(slot).map(|s| levels(*s)).unwrap_or_else(identity);
162 Some(compose(table(0), [table(1), table(2), table(3)]))
163 }
164 Correction::ToneCurve(slots) => {
165 let table = |slot: usize| match slots.get(slot) {
166 Some(points) if !points.is_empty() => Spline::new(points).table(),
167 _ => identity(),
168 };
169 Some(compose(table(0), [table(1), table(2), table(3)]))
170 }
171 Correction::ColorBalance { preserve_luminosity: false, shadows, midtones, highlights } => {
172 Some(std::array::from_fn(|c| balance(shadows[c], midtones[c], highlights[c])))
173 }
174 Correction::ColorBalance { preserve_luminosity: true, shadows, midtones, highlights } => {
175 Some(std::array::from_fn(|c| balance_preserving(c, *shadows, *midtones, *highlights)))
176 }
177 Correction::Invert => Some([std::array::from_fn(|v| 255.0 - v as f32); 3]),
178 Correction::Posterize(levels) => {
179 let n = (*levels).max(2) as f32;
180 Some([std::array::from_fn(|v| ((v as f32 * n / 256.0).floor() * 255.0 / (n - 1.0)).floor()); 3])
181 }
182 _ => None,
183 };
184 Filter { correction: self, tables }
185 }
186}
187
188pub struct Filter<'a> {
191 correction: &'a Correction,
192 tables: Option<[[f32; 256]; 3]>,
193}
194
195impl Filter<'_> {
196 pub fn apply(&self, rgb: [f32; 3]) -> [f32; 3] {
198 if let Some(tables) = &self.tables {
199 return std::array::from_fn(|c| lookup(&tables[c], rgb[c]));
200 }
201 let out = match self.correction {
202 Correction::HueSaturationValue { hue, saturation, value } => {
203 let rgb = rgb.map(|v| v as f64);
204 let rgb = value_op(rgb, *value as f64 / 100.0);
205 let rgb = hue_op(rgb, *hue as f64);
206 saturation_op(rgb, *saturation as f64 / 100.0).map(|v| v as f32)
207 }
208 Correction::Threshold(t) => {
209 let [r, g, b] = rgb.map(|v| v.round().clamp(0.0, 255.0) as i32);
210 let y = (306 * r + 601 * g + 117 * b) >> 10;
211 [if y >= *t { 255.0 } else { 0.0 }; 3]
212 }
213 Correction::GradientMap(ramp) => {
214 let y = 0.299 * rgb[0] + 0.587 * rgb[1] + 0.114 * rgb[2];
215 let c = ramp.color((y / 255.0) as f64);
216 let a = c[3] as f32;
217 std::array::from_fn(|i| c[i] as f32 * 255.0 * a + rgb[i] * (1.0 - a))
218 }
219 _ => rgb,
220 };
221 out.map(|v| v.clamp(0.0, 255.0))
222 }
223}
224
225fn identity() -> [f32; 256] {
226 std::array::from_fn(|v| v as f32)
227}
228
229fn lookup(table: &[f32; 256], v: f32) -> f32 {
230 let v = v.clamp(0.0, 255.0);
231 let i = (v.floor() as usize).min(254);
232 let f = v - i as f32;
233 table[i] + (table[i + 1] - table[i]) * f
234}
235
236fn compose(master: [f32; 256], channels: [[f32; 256]; 3]) -> [[f32; 256]; 3] {
238 channels.map(|table| table.map(|v| lookup(&master, v)))
239}
240
241fn brightness_contrast(brightness: i32, contrast: i32) -> [f32; 256] {
244 let (b, c) = (brightness as f64, contrast as f64);
245 std::array::from_fn(|v| {
246 let v = v as f64;
247 let out = if c >= 100.0 {
248 if v + b > 127.5 { 255.0 } else { 0.0 }
249 } else if c > 0.0 {
250 (v + b - 127.5) / (1.0 - c / 100.0) + 127.5
251 } else {
252 (v - 127.5) * (1.0 + c / 100.0) + 127.5 + b
253 };
254 out.clamp(0.0, 255.0) as f32
255 })
256}
257
258fn levels([black, gamma_point, white, out_black, out_white]: [f64; 5]) -> [f32; 256] {
261 let range = (white - black).max(1e-6);
262 let middle = ((gamma_point - black) / range).clamp(1e-6, 1.0 - 1e-6);
263 let g = 0.5f64.ln() / middle.ln();
264 std::array::from_fn(|v| {
265 let t = ((v as f64 / 255.0 - black) / range).clamp(0.0, 1.0);
266 ((out_black + (out_white - out_black) * t.powf(g)) * 255.0).clamp(0.0, 255.0) as f32
267 })
268}
269
270fn balance(shadows: i32, midtones: i32, highlights: i32) -> [f32; 256] {
275 let (s, m, h) = (shadows as f64, midtones as f64, highlights as f64);
276 let (mut log_gamma, mut black, mut white) = (0.0, 0.0, 255.0);
277 if s > 0.0 {
278 log_gamma -= 0.002_25 * s;
279 } else if s < 0.0 {
280 log_gamma += 0.0034 * -s + 3.4e-6 * s * s;
281 black = -s * (0.99 - 0.000_32 * -s);
282 }
283 if m > 0.0 {
284 log_gamma -= 0.0055 * m;
285 } else if m < 0.0 {
286 log_gamma += 0.004_66 * -m + 1.88e-5 * m * m;
287 }
288 if h > 0.0 {
289 log_gamma -= 0.003_31 * h;
290 white = 255.0 - 0.9575 * h;
291 } else if h < 0.0 {
292 log_gamma += 0.0024 * -h;
293 }
294 let gamma = log_gamma.exp();
295 std::array::from_fn(|v| {
296 let t = ((v as f64 - black) / (white - black)).clamp(0.0, 1.0);
297 (255.0 * t.powf(gamma)) as f32
298 })
299}
300
301fn balance_preserving(c: usize, shadows: [i32; 3], midtones: [i32; 3], highlights: [i32; 3]) -> [f32; 256] {
309 let others = |values: [i32; 3], own: fn(i32) -> i32, other: fn(i32) -> i32| -> f64 {
310 (0..3).map(|i| if i == c { own(values[i]) } else { other(values[i]) }).sum::<i32>() as f64
311 };
312 let black = others(shadows, |v| (-v).max(0), |v| v.max(0)).min(254.0);
313 let white = (255.0 - others(highlights, |v| v.max(0), |v| (-v).max(0))).max(black + 1.0);
314 let mean = midtones.iter().sum::<i32>() as f64 / 3.0;
315 let gamma = (-0.0064 * (midtones[c] as f64 - mean)).exp();
316 std::array::from_fn(|v| {
317 let t = ((v as f64 - black) / (white - black)).clamp(0.0, 1.0);
318 (255.0 * t.powf(gamma)) as f32
319 })
320}
321
322fn max3(c: [f64; 3]) -> f64 {
323 c[0].max(c[1]).max(c[2])
324}
325
326fn min3(c: [f64; 3]) -> f64 {
327 c[0].min(c[1]).min(c[2])
328}
329
330fn value_op(c: [f64; 3], t: f64) -> [f64; 3] {
333 if t <= 0.0 {
334 return c.map(|v| v * (1.0 + t));
335 }
336 let max = max3(c);
337 let new_max = max + (255.0 - max) * t;
338 c.map(|v| {
339 let below = if max > 0.0 { (max - v) / max } else { 0.0 };
340 new_max - below * (1.0 - t) * new_max
341 })
342}
343
344fn hue_op(c: [f64; 3], degrees: f64) -> [f64; 3] {
346 if degrees == 0.0 {
347 return c;
348 }
349 let (max, min) = (max3(c), min3(c));
350 let chroma = max - min;
351 if chroma <= 0.0 {
352 return c;
353 }
354 let [r, g, b] = c;
355 let sector = if max == r {
356 ((g - b) / chroma).rem_euclid(6.0)
357 } else if max == g {
358 (b - r) / chroma + 2.0
359 } else {
360 (r - g) / chroma + 4.0
361 };
362 let h = (sector + degrees / 60.0).rem_euclid(6.0);
363 let x = chroma * (1.0 - (h % 2.0 - 1.0).abs());
364 let (r, g, b) = match h as u32 {
365 0 => (chroma, x, 0.0),
366 1 => (x, chroma, 0.0),
367 2 => (0.0, chroma, x),
368 3 => (0.0, x, chroma),
369 4 => (x, 0.0, chroma),
370 _ => (chroma, 0.0, x),
371 };
372 [r + min, g + min, b + min]
373}
374
375fn saturation_op(c: [f64; 3], t: f64) -> [f64; 3] {
380 let (max, min) = (max3(c), min3(c));
381 let chroma = max - min;
382 if t == 0.0 || chroma <= 0.0 || max <= 0.0 {
383 return c;
384 }
385 let s = chroma / max;
386 if t < 0.0 {
387 let scale = 1.0 + t * s / 2.0;
388 return c.map(|v| (max - (max - v) * (1.0 + t)) * scale);
389 }
390 let room = if s < 1.0 { (255.0 / max - 1.0) / (1.0 - s) } else { f64::INFINITY };
391 let u = t.min(room) * (1.0 - s);
392 let (new_max, new_s) = (max * (1.0 + u), s + u);
393 let new_min = new_max * (1.0 - new_s);
394 c.map(|v| new_min + (v - min) / chroma * (new_max - new_min))
395}
396
397#[cfg(test)]
398mod tests {
399 use super::*;
400
401 fn blob(kind: i32, ints: &[i32]) -> Vec<u8> {
402 let mut data = kind.to_be_bytes().to_vec();
403 data.extend(((ints.len() * 4) as u32).to_be_bytes());
404 for v in ints {
405 data.extend(v.to_be_bytes());
406 }
407 data
408 }
409
410 fn apply(kind: i32, ints: &[i32], rgb: [f32; 3]) -> [f32; 3] {
411 let correction = Correction::parse(&blob(kind, ints)).unwrap();
412 correction.filter().apply(rgb).map(|v| v.round())
413 }
414
415 fn near(kind: i32, ints: &[i32], rgb: [f32; 3], csp: [f32; 3]) {
417 let ours = apply(kind, ints, rgb);
418 assert!(ours.iter().zip(csp).all(|(a, b)| (a - b).abs() <= 1.0), "{ours:?} vs CSP {csp:?}");
419 }
420
421 #[test]
424 fn invert_posterize_threshold() {
425 assert_eq!(apply(6, &[], [0.0, 100.0, 255.0]), [255.0, 155.0, 0.0]);
426 assert_eq!(apply(7, &[4], [63.0, 64.0, 255.0]), [0.0, 85.0, 255.0]);
427 assert_eq!(apply(8, &[128], [128.0, 128.0, 128.0]), [255.0; 3]);
428 }
429
430 #[test]
431 fn brightness_contrast_rule() {
432 near(1, &[-20, 40], [127.0; 3], [93.0; 3]);
434 assert_eq!(apply(1, &[-18, 100], [145.0, 146.0, 0.0]), [0.0, 255.0, 0.0]);
436 }
437
438 #[test]
439 fn hue_saturation_value_rules() {
440 assert_eq!(apply(4, &[0, 0, -30], [100.0, 50.0, 0.0]), [70.0, 35.0, 0.0]);
442 near(4, &[0, 0, 30], [55.0, 6.0, 6.0], [114.0, 42.0, 42.0]);
443 near(4, &[0, -50, 0], [55.0, 6.0, 6.0], [43.0, 23.0, 23.0]);
445 near(4, &[0, 50, 0], [128.0, 96.0, 64.0], [160.0, 100.0, 40.0]);
446 assert_eq!(apply(4, &[0, 100, 0], [250.0, 233.0, 217.0])[2], 216.0);
448 assert_eq!(apply(4, &[120, 0, 0], [200.0, 0.0, 0.0]), [0.0, 200.0, 0.0]);
450 assert_eq!(apply(4, &[0, 100, 0], [90.0; 3]), [90.0; 3]);
451 }
452
453 #[test]
454 fn colour_balance_fit() {
455 let shadows = apply(5, &[0, -100, 0, 0, 0, 0, 0, 0, 0, 0], [96.0, 96.0, 96.0]);
457 assert_eq!(shadows, [0.0, 96.0, 96.0]);
458 near(5, &[0, 0, 0, 0, 100, 0, 0, 0, 0, 0], [128.0; 3], [172.0, 128.0, 128.0]);
459 }
460
461 #[test]
462 fn colour_balance_preserving_luminosity() {
463 near(5, &[1, 0, 0, 0, 50, 0, 0, 0, 0, 0], [128.0; 3], [146.0, 118.0, 118.0]);
466 near(5, &[1, -50, 0, 0, 0, 0, 0, 0, 0, 0], [128.0; 3], [97.0, 128.0, 128.0]);
467 near(5, &[1, 0, 0, 0, 0, 0, 0, 50, 0, 0], [128.0; 3], [159.0, 128.0, 128.0]);
468 near(5, &[1, 50, 0, 0, 0, 0, 0, 0, 0, 0], [128.0; 3], [128.0, 97.0, 97.0]);
469 }
470
471 #[test]
472 fn tone_curve_channel_then_master() {
473 let mut payload = Vec::new();
474 let slots: [&[(u16, u16)]; 2] =
475 [&[(0, 0), (0x8000, 0x4000), (0xffff, 0xffff)], &[(0, 0x4000), (0xffff, 0xffff)]];
476 for slot in 0..32 {
477 let points: &[(u16, u16)] = slots.get(slot).copied().unwrap_or(&[(0, 0), (0xffff, 0xffff)]);
478 payload.extend((points.len() as u16).to_be_bytes());
479 for i in 0..32 {
480 let (x, y) = points.get(i).copied().unwrap_or((0, 0));
481 payload.extend(x.to_be_bytes());
482 payload.extend(y.to_be_bytes());
483 }
484 }
485 let mut data = 3i32.to_be_bytes().to_vec();
486 data.extend((payload.len() as u32).to_be_bytes());
487 data.extend(payload);
488 let correction = Correction::parse(&data).unwrap();
489 let out = correction.filter().apply([0.0, 128.0, 255.0]);
490 assert!((out[0] - 40.0).abs() < 1.0, "{out:?}");
492 assert!((out[1] - 96.0).abs() < 1.0 && out[2] > 254.0, "{out:?}");
493 }
494}