color_blinder/
lib.rs

1pub mod gimp;
2pub mod web_algorithms;
3
4#[cfg(feature = "labels")]
5pub mod labels;
6
7use bitflags::bitflags;
8use cfg_if::cfg_if;
9use image::{GenericImage, ImageBuffer, Rgba, RgbaImage};
10use std::iter::{IntoIterator, Iterator};
11use std::path::PathBuf;
12use std::sync::{
13    mpsc::{channel, Receiver, Sender},
14    Arc,
15};
16use threadpool::ThreadPool;
17use web_algorithms::{anomylize, blindMK, monochrome};
18
19pub type RgbaBuf = ImageBuffer<Rgba<u8>, Vec<u8>>;
20pub type NamedBuf = (&'static str, RgbaBuf);
21pub type FilterFunc = Box<dyn Fn(&Rgba<u8>) -> Rgba<u8> + Send + Sync>;
22
23pub const COMBINED_MARGIN: u32 = 25;
24const ORIGINAL_TEXT: &'static str = "Original";
25
26#[derive(Clone)]
27pub enum RevBlind {
28    Protan,
29    Deutan,
30    Tritan,
31}
32
33bitflags! {
34    #[allow(non_camal_case)]
35    pub struct FilterKind: usize {
36        const ORIGINAL          = 0b0000_0000_0001;
37        const ACHROMATOMALY     = 0b0000_0000_0010;
38        const ACHROMATOPSIA     = 0b0000_0000_0100;
39        const DEUTERANOMALY     = 0b0000_0000_1000;
40        const DEUTERANOPIA      = 0b0000_0001_0000;
41        const DEUTERANOPIABVM97 = 0b0000_0010_0000;
42        const PROTANOMALY       = 0b0000_0100_0000;
43        const PROTANOPIA        = 0b0000_1000_0000;
44        const PROTANOPIABVM97   = 0b0001_0000_0000;
45        const TRITANOMALY       = 0b0010_0000_0000;
46        const TRITANOPIA        = 0b0100_0000_0000;
47        const TRITANOPIABVM97   = 0b1000_0000_0000;
48
49        const MONOCHROME = Self::ACHROMATOMALY.bits | Self::ACHROMATOPSIA.bits;
50        const DEUTAN = Self::DEUTERANOPIA.bits | Self::DEUTERANOPIABVM97.bits;
51        const PROTAN = Self::PROTANOMALY.bits | Self::PROTANOPIA.bits | Self::PROTANOPIABVM97.bits;
52        const TRITAN = Self::TRITANOMALY.bits | Self::TRITANOPIA.bits | Self::TRITANOPIABVM97.bits;
53        const BVM97 = Self::DEUTERANOPIABVM97.bits | Self::PROTANOPIABVM97.bits | Self::TRITANOPIABVM97.bits;
54        // All flags but the ORIGINAL
55        const ALL_FILTERS       = 0b1111_1111_1110;
56    }
57}
58
59impl FilterKind {
60    pub fn to_str(&self) -> &'static str {
61        match *self {
62            FilterKind::ORIGINAL => "Original",
63            FilterKind::ACHROMATOMALY => "Achromatomaly",
64            FilterKind::ACHROMATOPSIA => "Achromatopsia",
65            FilterKind::DEUTERANOMALY => "Deuteranomaly",
66            FilterKind::DEUTERANOPIA => "Deuteranopia",
67            FilterKind::DEUTERANOPIABVM97 => "DeuteranopiaBVM97",
68            FilterKind::PROTANOMALY => "Protanomaly",
69            FilterKind::PROTANOPIA => "Protanopia",
70            FilterKind::PROTANOPIABVM97 => "ProtanopiaBVM97",
71            FilterKind::TRITANOMALY => "Tritanomaly",
72            FilterKind::TRITANOPIA => "Tritanopia",
73            FilterKind::TRITANOPIABVM97 => "TritanopiaBVM97",
74            // Groups
75            FilterKind::MONOCHROME => "MONOCHROME",
76            FilterKind::DEUTAN => "DEUTAN",
77            FilterKind::PROTAN => "PROTAN",
78            FilterKind::TRITAN => "TRITAN",
79            FilterKind::BVM97 => "BVM97",
80            FilterKind::ALL_FILTERS => "ALL_FILTERS",
81            unknown => panic!("FilterKind::to_str(0b{:b})", unknown.bits),
82        }
83    }
84
85    pub fn from_strs(filters: &[&str]) -> Result<FilterKind, FilterKindParseError> {
86        let mut kind = FilterKind::empty();
87        let mut unused_filters = vec![];
88
89        for filter in filters {
90            let filter = filter.trim();
91            let matches = FilterKind::all()
92                .iter()
93                .filter(|name| name.to_str().eq_ignore_ascii_case(filter))
94                .fold(FilterKind::empty(), |b, c| b | c);
95
96            if matches.bits().count_ones() > 0 {
97                kind |= matches;
98            } else {
99                let mut used = false;
100
101                if "MONOCHROME".eq_ignore_ascii_case(filter) {
102                    kind |= FilterKind::MONOCHROME;
103                    used = true;
104                }
105                if "DEUTAN".eq_ignore_ascii_case(filter) {
106                    kind |= FilterKind::DEUTAN;
107                    used = true;
108                }
109                if "PROTAN".eq_ignore_ascii_case(filter) {
110                    kind |= FilterKind::PROTAN;
111                    used = true;
112                }
113                if "TRITAN".eq_ignore_ascii_case(filter) {
114                    kind |= FilterKind::TRITAN;
115                    used = true;
116                }
117                if "BVM97".eq_ignore_ascii_case(filter) {
118                    kind |= FilterKind::BVM97;
119                    used = true;
120                }
121
122                if used == false {
123                    unused_filters.push(filter.to_string());
124                }
125            }
126        }
127
128        if unused_filters.is_empty() {
129            Ok(kind)
130        } else {
131            Err(FilterKindParseError::UnknownMatches(unused_filters))
132        }
133    }
134
135    pub fn iter(&self) -> FilterKindIter {
136        FilterKindIter {
137            filter: *self,
138            state: 1,
139        }
140    }
141}
142
143#[derive(Debug, PartialEq, Eq)]
144pub enum FilterKindParseError {
145    UnknownMatches(Vec<String>),
146}
147
148pub struct FilterKindIter {
149    filter: FilterKind,
150    state: usize,
151}
152impl Iterator for FilterKindIter {
153    type Item = FilterKind;
154
155    fn next(&mut self) -> Option<Self::Item> {
156        let mut current = FilterKind::from_bits_truncate(self.state);
157
158        // Check to see if we've finished counting or not.
159        while current.is_empty() == false {
160            self.state <<= 1;
161            if self.filter.intersects(current) {
162                return Some(current);
163            }
164            current = FilterKind::from_bits_truncate(self.state)
165        }
166        None
167    }
168}
169
170#[derive(Debug, PartialEq)]
171pub struct CombineInfo {
172    total_height: u32,
173    total_width: u32,
174    positions: Vec<(u32, u32)>,
175}
176
177pub struct Config {
178    pub combine_output: bool,
179    pub processing: ProcessingStyle,
180    #[cfg(feature = "labels")]
181    pub render_label: bool,
182}
183
184#[derive(Clone)]
185pub struct RuntimeConfig {
186    pub combine_output: bool,
187    #[cfg(feature = "labels")]
188    pub render_label: bool,
189}
190
191#[derive(Clone)]
192pub enum ProcessingStyle {
193    /// Use the main thread for processing (single threaded)
194    Inline,
195    /// Use a threadpool for the work (autodectecion for how many workers)
196    MustOffload(Option<ThreadPool>),
197    /// Use a threadpool if there are more than one images to process
198    MayOffload(Option<ThreadPool>),
199}
200
201pub struct ProcessingContext {
202    filter_functions: Vec<(FilterKind, FilterFunc)>,
203    config: RuntimeConfig,
204    processing: ProcessingStyle,
205    pool: Option<ThreadPool>,
206}
207
208pub enum ProcessingResult {
209    Inline(Vec<NamedBuf>),
210    Offload {
211        pool: ThreadPool,
212        sender: Sender<NamedBuf>,
213        results: Receiver<NamedBuf>,
214    },
215}
216
217impl ProcessingResult {
218    fn new(pool: &Option<ThreadPool>) -> ProcessingResult {
219        let (sender, results) = channel();
220        use ProcessingResult::*;
221        match pool {
222            Some(pool) => Offload {
223                pool: pool.clone(),
224                sender,
225                results,
226            },
227            None => Inline(Vec::with_capacity(12)),
228        }
229    }
230    fn push(&mut self, buf: NamedBuf) {
231        use ProcessingResult::*;
232        match self {
233            Inline(processed_images) => processed_images.push(buf),
234            Offload { sender, .. } => sender.send(buf).unwrap(),
235        }
236    }
237
238    fn submit_task<F>(&mut self, func: Box<F>)
239    where
240        F: FnOnce() -> NamedBuf + Send,
241    {
242        use ProcessingResult::*;
243        match self {
244            Inline(_) => {
245                self.push(func());
246            }
247            Offload { pool, sender, .. } => {
248                let sender = sender.clone();
249                let func = unsafe {
250                    std::mem::transmute::<
251                        Box<dyn FnOnce() -> NamedBuf + Send>,
252                        Box<dyn FnOnce() -> NamedBuf + Send + 'static>,
253                    >(func)
254                };
255                pool.execute(move || sender.send(func()).expect("submit_task failed"));
256            }
257        }
258    }
259}
260impl IntoIterator for ProcessingResult {
261    type Item = NamedBuf;
262    type IntoIter = Box<dyn Iterator<Item = NamedBuf>>;
263
264    fn into_iter(self) -> Self::IntoIter {
265        use ProcessingResult::*;
266        match self {
267            Inline(bufs) => {
268                //let i: Box<dyn IntoIterator<Item = NamedBuf, IntoIter = dyn Iterator<Item = NamedBuf>>> =
269                Box::new(bufs.into_iter())
270                //; i
271            }
272            Offload { results, .. } => {
273                //let i: Box<dyn IntoIterator<Item = NamedBuf, IntoIter = dyn Iterator<Item = NamedBuf>>> =
274                Box::new(results.into_iter())
275                //; i
276            }
277        }
278        //self.0.into_iter()
279    }
280}
281
282impl Config {
283    pub fn into_context(self) -> ProcessingContext {
284        let filter_functions: Vec<(FilterKind, FilterFunc)> = vec![
285            (
286                FilterKind::ACHROMATOMALY,
287                Box::new(move |p| anomylize(p, monochrome(p))),
288            ),
289            (FilterKind::ACHROMATOPSIA, Box::new(move |p| monochrome(p))),
290            (
291                FilterKind::DEUTERANOMALY,
292                Box::new(move |p| anomylize(p, blindMK(p, RevBlind::Deutan))),
293            ),
294            (
295                FilterKind::DEUTERANOPIA,
296                Box::new(move |p| blindMK(p, RevBlind::Deutan)),
297            ),
298            (
299                FilterKind::DEUTERANOPIABVM97,
300                Box::new(move |p| gimp::bvm97(RevBlind::Deutan)(p)),
301            ),
302            (
303                FilterKind::PROTANOMALY,
304                Box::new(move |p| anomylize(p, blindMK(p, RevBlind::Protan))),
305            ),
306            (
307                FilterKind::PROTANOPIA,
308                Box::new(move |p| blindMK(p, RevBlind::Protan)),
309            ),
310            (
311                FilterKind::PROTANOPIABVM97,
312                Box::new(move |p| gimp::bvm97(RevBlind::Protan)(p)),
313            ),
314            (
315                FilterKind::TRITANOMALY,
316                Box::new(move |p| anomylize(p, blindMK(p, RevBlind::Tritan))),
317            ),
318            (
319                FilterKind::TRITANOPIA,
320                Box::new(move |p| blindMK(p, RevBlind::Tritan)),
321            ),
322            (
323                FilterKind::TRITANOPIABVM97,
324                Box::new(move |p| gimp::bvm97(RevBlind::Tritan)(p)),
325            ),
326        ];
327
328        let pool = match self.processing {
329            ProcessingStyle::Inline => None,
330            ProcessingStyle::MustOffload(ref pool) | ProcessingStyle::MayOffload(ref pool) => {
331                Some(match pool {
332                    None => threadpool::Builder::new().build(),
333                    Some(pool) => pool.clone(),
334                })
335            }
336        };
337
338        ProcessingContext {
339            config: RuntimeConfig {
340                combine_output: self.combine_output,
341                #[cfg(feature = "labels")]
342                render_label: self.render_label,
343            },
344            processing: self.processing,
345            filter_functions,
346            pool,
347        }
348    }
349}
350
351impl ProcessingContext {
352    pub fn combine_output(&mut self, combine_output: bool) {
353        self.config.combine_output = combine_output;
354    }
355    pub fn process_file(
356        &self,
357        input: &PathBuf,
358        filter_filter: FilterKind,
359    ) -> Result<(), Box<dyn std::error::Error>> {
360        let path = format!("{}", input.display());
361        let img: RgbaBuf = image::open(&input)?.to_rgba8();
362        let (name, extension) = path.split_at(
363            path.rfind(".")
364                .expect("path does not contain a detectable extension"),
365        );
366
367        let mut images = self.process(img, filter_filter)?.into_iter();
368
369        if self.config.combine_output {
370            let out_filename = format!("{}_combined{}", name, extension);
371
372            print!("\n    saving {} ... ", out_filename);
373            flush()?;
374            let (_label, buffer) = images.next().unwrap();
375            buffer.save(out_filename)?;
376            print!("done");
377            flush()?;
378        } else {
379            for (label, buffer) in images {
380                let out_filename = format!("{}_{}{}", name, label, extension);
381                print!("\n    saving {} ... ", out_filename);
382                flush()?;
383                buffer.save(out_filename)?;
384                print!("done");
385                flush()?;
386            }
387        }
388        Ok(())
389    }
390
391    pub fn process(
392        &self,
393        img: RgbaBuf,
394        filter_filter: FilterKind,
395    ) -> Result<ProcessingResult, Box<dyn std::error::Error>> {
396        let ref config = self.config;
397        let state = Arc::new((img, config.clone()));
398
399        let mut results = ProcessingResult::new(match self.processing {
400            ProcessingStyle::MayOffload(_) if filter_filter.bits().count_ones() <= 1 => &None,
401            _ => &self.pool,
402        });
403        if config.combine_output {
404            let state = state.clone();
405            results.submit_task(Box::new(move || {
406                let state = state.clone();
407                let mut buffer = setup_buffer(&state, ORIGINAL_TEXT);
408                buffer
409                    .copy_from(&state.0, 0, 0)
410                    .expect("unable to allocate buffer");
411                (ORIGINAL_TEXT, buffer)
412            }));
413        }
414
415        for (kind, func) in self
416            .filter_functions
417            .iter()
418            .filter(|(kind, _)| filter_filter.contains(*kind))
419        {
420            let label = kind.to_str();
421
422            let state = state.clone();
423            let func = func.clone();
424            results.submit_task(Box::new(move || {
425                let mut buffer = setup_buffer(&state, &label);
426
427                for (x, y, pixel) in state.0.enumerate_pixels() {
428                    buffer.put_pixel(x, y, func(pixel));
429                }
430
431                (label, buffer)
432            }));
433        }
434
435        if config.combine_output {
436            let mut combined_results = ProcessingResult::new(&self.pool);
437
438            combined_results.submit_task(Box::new(move || {
439                let mut processed_images = results.into_iter().collect::<Vec<_>>();
440                assert!(processed_images.len() > 0, "no partial images found");
441
442                // Reorder the partial images if they come out of order from the ThreadPool
443                // while keeping the ORIGINAL in the front
444                processed_images.sort_by(|a, b| {
445                    use std::cmp::Ordering::*;
446                    if a.0 == ORIGINAL_TEXT {
447                        return Less;
448                    }
449                    if b.0 == ORIGINAL_TEXT {
450                        return Greater;
451                    }
452                    a.0.cmp(b.0)
453                });
454
455                let max_dimensions = processed_images
456                    .iter()
457                    .map(|t: &NamedBuf| &t.1)
458                    .fold((0, 0), |b, i| b.max(i.dimensions()));
459                let positions =
460                    calculate_combined_positions(max_dimensions, processed_images.len());
461
462                let mut buffer: RgbaBuf =
463                    ImageBuffer::new(positions.total_width, positions.total_height);
464
465                for ((_label, img), (x, y)) in
466                    processed_images.iter().zip(positions.positions.iter())
467                {
468                    buffer
469                        .copy_from(img, *x, *y)
470                        .expect("combined_results copy_from failed");
471                }
472
473                ("Combined", buffer)
474            }));
475
476            Ok(combined_results)
477        } else {
478            Ok(results)
479        }
480    }
481}
482
483cfg_if! {
484    if #[cfg(feature = "labels")] {
485        fn setup_buffer(img: &Arc<(RgbaImage, RuntimeConfig)>, label: &str, ) -> RgbaImage {
486            let (img, config) = &**img;
487            let mut width = img.width();
488            let mut height = img.height();
489                let label = if config.render_label {
490                    let label = labels::render(label);
491                    height += label.height();
492                    width = width.max(label.width());
493                    Some(label)
494                } else {
495                    None
496                };
497            let mut buffer = ImageBuffer::new(width, height);
498                if let Some(label) = label {
499                    // centering text
500                    let x = width.saturating_sub(label.width()) / 2;
501                    buffer.copy_from(&label, x, img.height()).expect("setup_buffer::feature=labels");
502            }
503            buffer
504        }
505    } else {
506        fn setup_buffer(img: &Arc<(RgbaImage, RuntimeConfig)>, _label: &str, ) -> RgbaImage {
507            ImageBuffer::new(img.0.width(), img.0.height())
508        }
509    }
510}
511
512fn flush() -> std::io::Result<()> {
513    use std::io::Write;
514    std::io::stdout().flush()
515}
516
517/// Calculate a grid with collapsed margins
518fn calculate_combined_positions((width, height): (u32, u32), n_pictures: usize) -> CombineInfo {
519    assert!(n_pictures > 0, "n_pictures must be non-zero");
520
521    let sqrt = (n_pictures as f64).sqrt();
522    let columns = sqrt.ceil() as u32;
523    let mut rows = sqrt.floor() as u32;
524
525    if columns * rows < n_pictures as u32 {
526        rows += 1;
527    }
528
529    let mut positions = vec![];
530    for y in 0..rows {
531        for x in 0..columns {
532            positions.push((
533                COMBINED_MARGIN + x * (width + COMBINED_MARGIN),
534                COMBINED_MARGIN + y * (height + COMBINED_MARGIN),
535            ));
536        }
537    }
538    assert!(
539        n_pictures <= positions.len(),
540        "only {} positions generated, need {}",
541        positions.len(),
542        n_pictures
543    );
544
545    CombineInfo {
546        total_width: COMBINED_MARGIN + (COMBINED_MARGIN + width) * columns,
547        total_height: COMBINED_MARGIN + (COMBINED_MARGIN + height) * rows,
548        positions,
549    }
550}
551
552#[cfg(test)]
553mod test {
554    use super::*;
555    use pretty_assertions::assert_eq;
556
557    const MARGIN: u32 = 25;
558
559    #[test]
560    fn single() {
561        let positions = calculate_combined_positions((20, 10), 1);
562        assert_eq!(positions.total_width, MARGIN + 20 + MARGIN);
563        assert_eq!(positions.total_height, MARGIN + 10 + MARGIN);
564        assert_eq!(positions.positions, [(MARGIN, MARGIN)]);
565    }
566
567    #[test]
568    fn three() {
569        let positions = calculate_combined_positions((20, 10), 3);
570        assert_eq!(
571            CombineInfo {
572                total_width: MARGIN + 2 * (20 + MARGIN),
573                total_height: MARGIN + 2 * (10 + MARGIN),
574                positions: vec![
575                    (MARGIN, MARGIN),
576                    (MARGIN + 20 + MARGIN, MARGIN), // expect a 2x2 matrix
577                    (MARGIN, MARGIN + 10 + MARGIN),
578                    (MARGIN + 20 + MARGIN, MARGIN + 10 + MARGIN),
579                ],
580            },
581            positions
582        );
583    }
584
585    #[test]
586    fn four() {
587        let positions = calculate_combined_positions((20, 10), 4);
588        assert_eq!(
589            CombineInfo {
590                total_width: MARGIN + 2 * (20 + MARGIN),
591                total_height: MARGIN + 2 * (10 + MARGIN),
592                positions: vec![
593                    (MARGIN, MARGIN),
594                    (MARGIN + 20 + MARGIN, MARGIN), // expect a 2x2 matrix
595                    (MARGIN, MARGIN + 10 + MARGIN),
596                    (MARGIN + 20 + MARGIN, MARGIN + 10 + MARGIN),
597                ],
598            },
599            positions
600        );
601    }
602
603    #[test]
604    fn twelfe() {
605        let positions = calculate_combined_positions((20, 10), 12);
606        let row1 = MARGIN + 10 + MARGIN;
607        let row2 = MARGIN + 2 * (10 + MARGIN);
608        assert_eq!(
609            CombineInfo {
610                total_width: MARGIN + 4 * (20 + MARGIN),
611                total_height: MARGIN + 3 * (10 + MARGIN),
612                positions: vec![
613                    (MARGIN + 0 * 45, MARGIN),
614                    (MARGIN + 1 * 45, MARGIN),
615                    (MARGIN + 2 * 45, MARGIN),
616                    (MARGIN + 3 * 45, MARGIN), // expect a 4x3 matrix
617                    (MARGIN + 0 * 45, row1),
618                    (MARGIN + 1 * 45, row1),
619                    (MARGIN + 2 * 45, row1),
620                    (MARGIN + 3 * 45, row1), // row 2
621                    (MARGIN + 0 * 45, row2),
622                    (MARGIN + 1 * 45, row2),
623                    (MARGIN + 2 * 45, row2),
624                    (MARGIN + 3 * 45, row2)
625                ],
626            },
627            positions
628        );
629    }
630
631    #[test]
632    fn iter_empty() {
633        let mut iter = FilterKind::empty().iter();
634        assert_eq!(None, iter.next());
635    }
636
637    #[test]
638    fn iter_last() {
639        let mut iter = FilterKind::TRITANOPIABVM97.iter();
640        assert_eq!(Some(FilterKind::TRITANOPIABVM97), iter.next());
641        assert_eq!(None, iter.next());
642    }
643
644    #[test]
645    fn iter_first_two() {
646        let mut iter = (FilterKind::ORIGINAL | FilterKind::ACHROMATOMALY).iter();
647        assert_eq!(Some(FilterKind::ORIGINAL), iter.next());
648        assert_eq!(Some(FilterKind::ACHROMATOMALY), iter.next());
649        assert_eq!(None, iter.next());
650    }
651
652    #[test]
653    fn from_strs() {
654        let expect = Ok(FilterKind::PROTANOPIABVM97 | FilterKind::ACHROMATOMALY);
655        let input = ["ACHROMATOMALY", "  PROTANOPIABVM97  "];
656        assert_eq!(expect, FilterKind::from_strs(&input));
657    }
658
659    #[test]
660    fn from_strs_group() {
661        let expect = Ok(FilterKind::MONOCHROME);
662        let input = ["MONOCHROME"];
663        assert_eq!(expect, FilterKind::from_strs(&input));
664    }
665
666    #[test]
667    fn from_strs_not_found() {
668        let expect = Err(FilterKindParseError::UnknownMatches(vec![
669            "something else".to_string(),
670            "something different".to_string(),
671        ]));
672        let input = ["MONOCHROME", "something else", "something different"];
673        assert_eq!(expect, FilterKind::from_strs(&input));
674    }
675
676    #[test]
677    fn to_str() {
678        assert_eq!("MONOCHROME", FilterKind::MONOCHROME.to_str());
679    }
680}