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 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 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 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 Inline,
195 MustOffload(Option<ThreadPool>),
197 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 Box::new(bufs.into_iter())
270 }
272 Offload { results, .. } => {
273 Box::new(results.into_iter())
275 }
277 }
278 }
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 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 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
517fn 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), (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), (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), (MARGIN + 0 * 45, row1),
618 (MARGIN + 1 * 45, row1),
619 (MARGIN + 2 * 45, row1),
620 (MARGIN + 3 * 45, row1), (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}