1use crate::{
4 error::{Result, VCError},
5 matrix::{generate_basic_matrices, generate_xor_matrices},
6 share::{stack_shares, Share},
7 utils::convert_to_binary,
8 VCConfig,
9};
10use image::{DynamicImage, ImageBuffer, Luma, Rgb};
11use rand::{seq::SliceRandom, Rng};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum Algorithm {
16 BasicThreshold,
18 NaorShamir,
20 TaghaddosLatif,
22 DhimanKasana,
24 XorBased,
26 YamaguchiNakajima,
28}
29
30pub trait VCScheme {
32 fn encrypt(
34 &self,
35 image: &DynamicImage,
36 config: &VCConfig,
37 cover_images: Option<Vec<DynamicImage>>,
38 ) -> Result<Vec<Share>>;
39
40 fn decrypt(&self, shares: &[Share], config: &VCConfig) -> Result<DynamicImage>;
42}
43
44pub fn encrypt(
46 image: &DynamicImage,
47 config: &VCConfig,
48 cover_images: Option<Vec<DynamicImage>>,
49) -> Result<Vec<Share>> {
50 match config.algorithm {
51 Algorithm::BasicThreshold => basic_threshold_encrypt(image, config),
52 Algorithm::NaorShamir => naor_shamir_encrypt(image, config),
53 Algorithm::TaghaddosLatif => taghaddos_latif_encrypt(image, config),
54 Algorithm::DhimanKasana => dhiman_kasana_encrypt(image, config, cover_images),
55 Algorithm::XorBased => xor_based_encrypt(image, config),
56 Algorithm::YamaguchiNakajima => yamaguchi_nakajima_encrypt(image, config, cover_images),
57 }
58}
59
60pub fn decrypt(shares: &[Share], config: &VCConfig) -> Result<DynamicImage> {
62 match config.algorithm {
63 Algorithm::BasicThreshold => basic_threshold_decrypt(shares, config),
64 Algorithm::NaorShamir => naor_shamir_decrypt(shares, config),
65 Algorithm::TaghaddosLatif => taghaddos_latif_decrypt(shares, config),
66 Algorithm::DhimanKasana => dhiman_kasana_decrypt(shares, config),
67 Algorithm::XorBased => xor_based_decrypt(shares, config),
68 Algorithm::YamaguchiNakajima => yamaguchi_nakajima_decrypt(shares, config),
69 }
70}
71
72fn xor_based_encrypt(image: &DynamicImage, config: &VCConfig) -> Result<Vec<Share>> {
74 let binary = convert_to_binary(image);
75 let (width, height) = (binary.width(), binary.height());
76
77 let xor_matrices = generate_xor_matrices(config.num_shares)?;
79
80 let mut shares = vec![ImageBuffer::new(width, height); config.num_shares];
81
82 let mut rng = rand::rng();
83
84 for y in 0..height {
86 for x in 0..width {
87 let pixel = binary.get_pixel(x, y)[0];
88 let is_black = pixel == 0;
89
90 let matrix = if is_black {
92 &xor_matrices.black_pixel
93 } else {
94 &xor_matrices.white_pixel
95 };
96
97 let col = rng.random_range(0..matrix.ncols());
99
100 for share_idx in 0..config.num_shares {
102 let value = matrix[(share_idx, col)];
103 let pixel_value = if value == 1 { 0u8 } else { 255u8 };
104 shares[share_idx].put_pixel(x, y, Luma([pixel_value]));
105 }
106 }
107 }
108
109 let result: Vec<Share> = shares
111 .into_iter()
112 .enumerate()
113 .map(|(i, img)| {
114 Share::new(
115 DynamicImage::ImageLuma8(img),
116 i + 1,
117 config.num_shares,
118 width,
119 height,
120 1,
121 false,
122 )
123 })
124 .collect();
125
126 Ok(result)
127}
128
129fn xor_based_decrypt(shares: &[Share], _config: &VCConfig) -> Result<DynamicImage> {
131 if shares.is_empty() {
132 return Err(VCError::InsufficientShares {
133 required: 1,
134 provided: 0,
135 });
136 }
137
138 let (width, height) = shares[0].dimensions();
139 let mut result = ImageBuffer::new(width, height);
140
141 for y in 0..height {
142 for x in 0..width {
143 let mut xor_value = 0u8;
144
145 for share in shares {
146 if let DynamicImage::ImageLuma8(img) = &share.image {
147 let pixel = img.get_pixel(x, y)[0];
148 let bit = if pixel == 0 { 1 } else { 0 };
149 xor_value ^= bit;
150 }
151 }
152
153 let pixel_value = if xor_value == 1 { 0u8 } else { 255u8 };
155 result.put_pixel(x, y, Luma([pixel_value]));
156 }
157 }
158
159 Ok(DynamicImage::ImageLuma8(result))
160}
161
162fn basic_threshold_encrypt(image: &DynamicImage, config: &VCConfig) -> Result<Vec<Share>> {
164 let binary = convert_to_binary(image);
166 let (width, height) = (binary.width(), binary.height());
167
168 let matrices = generate_basic_matrices(config.threshold, config.num_shares, config.block_size)?;
170
171 let share_width = width * config.block_size as u32;
173 let share_height = height * config.block_size as u32;
174
175 let mut shares = vec![ImageBuffer::new(share_width, share_height); config.num_shares];
176
177 let mut rng = rand::rng();
178
179 for y in 0..height {
181 for x in 0..width {
182 let pixel = binary.get_pixel(x, y)[0];
183 let matrix_idx = if pixel == 0 { 1 } else { 0 }; let matrix = &matrices[matrix_idx];
185
186 let col = rng.random_range(0..matrix.ncols());
188
189 for share_idx in 0..config.num_shares {
191 let value = matrix[(share_idx, col)];
192 let block_value = if value == 1 { 0u8 } else { 255u8 };
193
194 let base_x = x * config.block_size as u32;
196 let base_y = y * config.block_size as u32;
197
198 for dy in 0..config.block_size as u32 {
199 for dx in 0..config.block_size as u32 {
200 shares[share_idx].put_pixel(base_x + dx, base_y + dy, Luma([block_value]));
201 }
202 }
203 }
204 }
205 }
206
207 let result: Vec<Share> = shares
209 .into_iter()
210 .enumerate()
211 .map(|(i, img)| {
212 Share::new(
213 DynamicImage::ImageLuma8(img),
214 i + 1,
215 config.num_shares,
216 width,
217 height,
218 config.block_size,
219 false,
220 )
221 })
222 .collect();
223
224 Ok(result)
225}
226
227fn basic_threshold_decrypt(shares: &[Share], _config: &VCConfig) -> Result<DynamicImage> {
229 if let Some(stacked) = stack_shares(shares) {
230 Ok(DynamicImage::ImageLuma8(stacked))
231 } else {
232 Err(VCError::DecryptionError(
233 "Failed to stack shares".to_string(),
234 ))
235 }
236}
237
238fn naor_shamir_encrypt(image: &DynamicImage, config: &VCConfig) -> Result<Vec<Share>> {
240 if config.num_shares != 2 || config.threshold != 2 {
242 return Err(VCError::InvalidConfiguration(
243 "Original Naor-Shamir scheme requires exactly 2 shares with threshold 2".to_string(),
244 ));
245 }
246
247 let binary = convert_to_binary(image);
248 let (width, height) = (binary.width(), binary.height());
249
250 let white_matrix = vec![vec![1, 1, 0, 0], vec![1, 1, 0, 0]];
253 let black_matrix = vec![vec![1, 1, 0, 0], vec![0, 0, 1, 1]];
255
256 let share_width = width * 2;
257 let share_height = height * 2;
258
259 let mut share1 = ImageBuffer::new(share_width, share_height);
260 let mut share2 = ImageBuffer::new(share_width, share_height);
261
262 let mut rng = rand::rng();
263
264 for y in 0..height {
265 for x in 0..width {
266 let pixel = binary.get_pixel(x, y)[0];
267
268 let matrix = if pixel == 0 {
270 &black_matrix
272 } else {
273 &white_matrix
275 };
276
277 let mut columns: Vec<usize> = (0..4).collect();
279 columns.as_mut_slice().shuffle(&mut rng);
280
281 let share1_pattern = [
283 matrix[0][columns[0]],
284 matrix[0][columns[1]],
285 matrix[0][columns[2]],
286 matrix[0][columns[3]],
287 ];
288 let share2_pattern = [
289 matrix[1][columns[0]],
290 matrix[1][columns[1]],
291 matrix[1][columns[2]],
292 matrix[1][columns[3]],
293 ];
294
295 share1.put_pixel(
298 x * 2,
299 y * 2,
300 Luma([if share1_pattern[0] == 1 { 0 } else { 255 }]),
301 );
302 share1.put_pixel(
303 x * 2 + 1,
304 y * 2,
305 Luma([if share1_pattern[1] == 1 { 0 } else { 255 }]),
306 );
307 share1.put_pixel(
308 x * 2,
309 y * 2 + 1,
310 Luma([if share1_pattern[2] == 1 { 0 } else { 255 }]),
311 );
312 share1.put_pixel(
313 x * 2 + 1,
314 y * 2 + 1,
315 Luma([if share1_pattern[3] == 1 { 0 } else { 255 }]),
316 );
317
318 share2.put_pixel(
320 x * 2,
321 y * 2,
322 Luma([if share2_pattern[0] == 1 { 0 } else { 255 }]),
323 );
324 share2.put_pixel(
325 x * 2 + 1,
326 y * 2,
327 Luma([if share2_pattern[1] == 1 { 0 } else { 255 }]),
328 );
329 share2.put_pixel(
330 x * 2,
331 y * 2 + 1,
332 Luma([if share2_pattern[2] == 1 { 0 } else { 255 }]),
333 );
334 share2.put_pixel(
335 x * 2 + 1,
336 y * 2 + 1,
337 Luma([if share2_pattern[3] == 1 { 0 } else { 255 }]),
338 );
339 }
340 }
341
342 Ok(vec![
343 Share::new(
344 DynamicImage::ImageLuma8(share1),
345 1,
346 2,
347 width,
348 height,
349 2,
350 false,
351 ),
352 Share::new(
353 DynamicImage::ImageLuma8(share2),
354 2,
355 2,
356 width,
357 height,
358 2,
359 false,
360 ),
361 ])
362}
363
364fn naor_shamir_decrypt(shares: &[Share], config: &VCConfig) -> Result<DynamicImage> {
366 basic_threshold_decrypt(shares, config)
367}
368
369fn taghaddos_latif_encrypt(image: &DynamicImage, config: &VCConfig) -> Result<Vec<Share>> {
371 if config.num_shares != 2 {
372 return Err(VCError::InvalidConfiguration(
373 "Taghaddos-Latif scheme requires exactly 2 shares".to_string(),
374 ));
375 }
376
377 let gray = image.to_luma8();
378 let (width, height) = (gray.width(), gray.height());
379
380 let patterns = [
382 [1u8, 1u8, 0u8, 0u8],
383 [1u8, 0u8, 1u8, 0u8],
384 [1u8, 0u8, 0u8, 1u8],
385 [0u8, 1u8, 1u8, 0u8],
386 [0u8, 1u8, 0u8, 1u8],
387 [0u8, 0u8, 1u8, 1u8],
388 ];
389
390 let share_width = width * 2;
392 let share_height = height * 2;
393
394 let mut share_a = ImageBuffer::new(share_width, share_height);
395 let mut share_b = ImageBuffer::new(share_width, share_height);
396
397 let mut rng = rand::rng();
398
399 for y in 0..height {
401 for x in 0..width {
402 let pixel_value = gray.get_pixel(x, y)[0];
403 let mut share_a_colors = [0u8; 4];
404 let mut share_b_colors = [0u8; 4];
405
406 for bit_pos in 0..8 {
408 let bit = (pixel_value >> bit_pos) & 1;
409
410 let pattern = patterns[rng.random_range(0..6)];
412
413 if bit == 1 {
414 for i in 0..4 {
416 share_a_colors[i] |= pattern[i] << bit_pos;
417 }
418 share_b_colors.copy_from_slice(&share_a_colors);
419 } else {
420 for i in 0..4 {
422 share_a_colors[i] |= pattern[i] << bit_pos;
423 share_b_colors[i] |= (1 - pattern[i]) << bit_pos;
424 }
425 }
426 }
427
428 let base_x = x * 2;
430 let base_y = y * 2;
431
432 share_a.put_pixel(base_x, base_y, Luma([share_a_colors[0]]));
434 share_a.put_pixel(base_x + 1, base_y, Luma([share_a_colors[1]]));
435 share_a.put_pixel(base_x, base_y + 1, Luma([share_a_colors[2]]));
436 share_a.put_pixel(base_x + 1, base_y + 1, Luma([share_a_colors[3]]));
437
438 share_b.put_pixel(base_x, base_y, Luma([share_b_colors[0]]));
440 share_b.put_pixel(base_x + 1, base_y, Luma([share_b_colors[1]]));
441 share_b.put_pixel(base_x, base_y + 1, Luma([share_b_colors[2]]));
442 share_b.put_pixel(base_x + 1, base_y + 1, Luma([share_b_colors[3]]));
443 }
444 }
445
446 Ok(vec![
447 Share::new(
448 DynamicImage::ImageLuma8(share_a),
449 1,
450 2,
451 width,
452 height,
453 2, false,
455 ),
456 Share::new(
457 DynamicImage::ImageLuma8(share_b),
458 2,
459 2,
460 width,
461 height,
462 2, false,
464 ),
465 ])
466}
467
468fn taghaddos_latif_decrypt(shares: &[Share], _config: &VCConfig) -> Result<DynamicImage> {
470 if shares.len() < 2 {
471 return Err(VCError::InsufficientShares {
472 required: 2,
473 provided: shares.len(),
474 });
475 }
476
477 let (expanded_width, expanded_height) = shares[0].dimensions();
478
479 let width = expanded_width / 2;
481 let height = expanded_height / 2;
482
483 let mut result = ImageBuffer::new(width, height);
484
485 let share_a = if let DynamicImage::ImageLuma8(img) = &shares[0].image {
487 img
488 } else {
489 return Err(VCError::DecryptionError(
490 "Share A is not grayscale".to_string(),
491 ));
492 };
493
494 let share_b = if let DynamicImage::ImageLuma8(img) = &shares[1].image {
495 img
496 } else {
497 return Err(VCError::DecryptionError(
498 "Share B is not grayscale".to_string(),
499 ));
500 };
501
502 for y in 0..height {
504 for x in 0..width {
505 let base_x = x * 2;
506 let base_y = y * 2;
507
508 let share_a_block = [
510 share_a.get_pixel(base_x, base_y)[0],
511 share_a.get_pixel(base_x + 1, base_y)[0],
512 share_a.get_pixel(base_x, base_y + 1)[0],
513 share_a.get_pixel(base_x + 1, base_y + 1)[0],
514 ];
515
516 let share_b_block = [
517 share_b.get_pixel(base_x, base_y)[0],
518 share_b.get_pixel(base_x + 1, base_y)[0],
519 share_b.get_pixel(base_x, base_y + 1)[0],
520 share_b.get_pixel(base_x + 1, base_y + 1)[0],
521 ];
522
523 let mut reconstructed_value = 0u8;
525
526 for bit_pos in 0..8 {
527 let mut reconstructed_bits = [0u8; 4];
528
529 for i in 0..4 {
531 let bit_a = (share_a_block[i] >> bit_pos) & 1;
532 let bit_b = (share_b_block[i] >> bit_pos) & 1;
533 reconstructed_bits[i] = bit_a & bit_b;
534 }
535
536 let sum = reconstructed_bits.iter().map(|&x| x as u32).sum::<u32>();
539 let average_bit = if sum >= 2 { 1 } else { 0 }; reconstructed_value |= (average_bit as u8) << bit_pos;
542 }
543
544 result.put_pixel(x, y, Luma([reconstructed_value]));
545 }
546 }
547
548 Ok(DynamicImage::ImageLuma8(result))
549}
550
551fn dhiman_kasana_encrypt(
553 image: &DynamicImage,
554 config: &VCConfig,
555 cover_images: Option<Vec<DynamicImage>>,
556) -> Result<Vec<Share>> {
557 if config.num_shares != 3 {
558 return Err(VCError::InvalidConfiguration(
559 "Dhiman-Kasana EVCT(3,3) scheme requires exactly 3 shares".to_string(),
560 ));
561 }
562
563 let rgb = image.to_rgb8();
564 let (width, height) = (rgb.width(), rgb.height());
565
566 let components = [
568 [
570 (4, 4),
571 (4, 2),
572 (3, 1),
573 (2, 3),
574 (2, 0),
575 (1, 4),
576 (1, 2),
577 (0, 1),
578 ],
579 [
581 (4, 3),
582 (3, 4),
583 (3, 2),
584 (2, 1),
585 (1, 3),
586 (1, 0),
587 (0, 4),
588 (0, 2),
589 ],
590 [
592 (4, 1),
593 (3, 3),
594 (3, 0),
595 (2, 4),
596 (2, 2),
597 (1, 1),
598 (0, 3),
599 (0, 0),
600 ],
601 ];
602
603 let share_width = width * 5;
605 let share_height = height * 5;
606
607 let mut shares = Vec::new();
608 for _ in 0..3 {
609 shares.push(ImageBuffer::new(share_width, share_height));
610 }
611
612 let has_cover_images = cover_images.is_some();
614
615 let covers = if let Some(covers) = cover_images {
617 if covers.len() != 3 {
618 return Err(VCError::CoverImageError(
619 "Dhiman-Kasana requires exactly 3 cover images".to_string(),
620 ));
621 }
622 covers.into_iter().map(|img| img.to_rgb8()).collect()
623 } else {
624 vec![
626 ImageBuffer::from_pixel(width, height, Rgb([255, 255, 255])),
627 ImageBuffer::from_pixel(width, height, Rgb([255, 255, 255])),
628 ImageBuffer::from_pixel(width, height, Rgb([255, 255, 255])),
629 ]
630 };
631
632 for y in 0..height {
634 for x in 0..width {
635 let secret_pixel = rgb.get_pixel(x, y);
636 let [r, g, b] = secret_pixel.0;
637
638 for share_idx in 0..3 {
640 let cover_pixel = covers[share_idx].get_pixel(x, y);
641
642 let mut block = ImageBuffer::from_pixel(5, 5, *cover_pixel);
644
645 for (channel_idx, &channel_value) in [r, g, b].iter().enumerate() {
647 let bit_positions = &components[channel_idx];
648
649 for (bit_idx, &(bit_y, bit_x)) in bit_positions.iter().enumerate() {
651 let bit = (channel_value >> bit_idx) & 1;
652
653 let pixel_color = if bit == 1 {
655 Rgb([0, 0, 0]) } else {
657 Rgb([30, 30, 30]) };
659
660 block.put_pixel(bit_x, bit_y, pixel_color);
661 }
662 }
663
664 let base_x = x * 5;
666 let base_y = y * 5;
667 for block_y in 0..5 {
668 for block_x in 0..5 {
669 let pixel = block.get_pixel(block_x, block_y);
670 shares[share_idx].put_pixel(base_x + block_x, base_y + block_y, *pixel);
671 }
672 }
673 }
674 }
675 }
676
677 let result: Vec<Share> = shares
679 .into_iter()
680 .enumerate()
681 .map(|(i, img)| {
682 Share::new(
683 DynamicImage::ImageRgb8(img),
684 i + 1,
685 3,
686 width,
687 height,
688 5, has_cover_images,
690 )
691 })
692 .collect();
693
694 Ok(result)
695}
696
697fn dhiman_kasana_decrypt(shares: &[Share], _config: &VCConfig) -> Result<DynamicImage> {
699 if shares.len() < 3 {
700 return Err(VCError::InsufficientShares {
701 required: 3,
702 provided: shares.len(),
703 });
704 }
705
706 let (expanded_width, expanded_height) = shares[0].dimensions();
708
709 let width = expanded_width / 5;
711 let height = expanded_height / 5;
712
713 let mut result = ImageBuffer::new(width, height);
714
715 let components = [
717 [
719 (4, 4),
720 (4, 2),
721 (3, 1),
722 (2, 3),
723 (2, 0),
724 (1, 4),
725 (1, 2),
726 (0, 1),
727 ],
728 [
730 (4, 3),
731 (3, 4),
732 (3, 2),
733 (2, 1),
734 (1, 3),
735 (1, 0),
736 (0, 4),
737 (0, 2),
738 ],
739 [
741 (4, 1),
742 (3, 3),
743 (3, 0),
744 (2, 4),
745 (2, 2),
746 (1, 1),
747 (0, 3),
748 (0, 0),
749 ],
750 ];
751
752 let share_images: Vec<&ImageBuffer<Rgb<u8>, Vec<u8>>> = shares
754 .iter()
755 .map(|share| {
756 if let DynamicImage::ImageRgb8(img) = &share.image {
757 img
758 } else {
759 panic!("Share is not RGB format");
760 }
761 })
762 .collect();
763
764 for y in 0..height {
766 for x in 0..width {
767 let mut reconstructed_pixel = [0u8; 3];
768
769 for channel_idx in 0..3 {
771 let bit_positions = &components[channel_idx];
772 let mut channel_value = 0u8;
773
774 for (bit_idx, &(bit_y, bit_x)) in bit_positions.iter().enumerate() {
776 let base_x = x * 5;
777 let base_y = y * 5;
778
779 let pixel = share_images[channel_idx].get_pixel(base_x + bit_x, base_y + bit_y);
781
782 let bit = if pixel.0 == [0, 0, 0] { 1 } else { 0 };
784
785 channel_value |= bit << bit_idx;
786 }
787
788 reconstructed_pixel[channel_idx] = channel_value;
789 }
790
791 result.put_pixel(x, y, Rgb(reconstructed_pixel));
792 }
793 }
794
795 Ok(DynamicImage::ImageRgb8(result))
796}
797
798fn yamaguchi_nakajima_encrypt(
801 image: &DynamicImage,
802 config: &VCConfig,
803 cover_images: Option<Vec<DynamicImage>>,
804) -> Result<Vec<Share>> {
805 if config.num_shares != 2 {
806 return Err(VCError::InvalidConfiguration(
807 "Yamaguchi-Nakajima scheme requires exactly 2 shares".to_string(),
808 ));
809 }
810
811 let cover_images = cover_images.ok_or_else(|| {
812 VCError::CoverImageError(
813 "Yamaguchi-Nakajima scheme requires 2 cover images (sheet images)".to_string(),
814 )
815 })?;
816
817 if cover_images.len() != 2 {
818 return Err(VCError::CoverImageError(
819 "Yamaguchi-Nakajima scheme requires exactly 2 cover images".to_string(),
820 ));
821 }
822
823 let target = image.to_luma8();
824 let sheet1 = cover_images[0].to_luma8();
825 let sheet2 = cover_images[1].to_luma8();
826 let (width, height) = (target.width(), target.height());
827
828 let sheet1 = image::imageops::resize(
830 &sheet1,
831 width,
832 height,
833 image::imageops::FilterType::Lanczos3,
834 );
835 let sheet2 = image::imageops::resize(
836 &sheet2,
837 width,
838 height,
839 image::imageops::FilterType::Lanczos3,
840 );
841
842 let m = config.block_size.max(4);
844 let sub_size = (m as f64).sqrt() as usize;
845
846 let contrast = 0.6;
848 let l = (1.0 - contrast) / 2.0; let sheet1_processed = apply_contrast_and_halftone(&sheet1, contrast, l);
852 let sheet2_processed = apply_contrast_and_halftone(&sheet2, contrast, l);
853 let target_processed = apply_halftone_target(&target, contrast);
854
855 let out_width = width * sub_size as u32;
857 let out_height = height * sub_size as u32;
858 let mut out_sheet1 = ImageBuffer::new(out_width, out_height);
859 let mut out_sheet2 = ImageBuffer::new(out_width, out_height);
860
861 let mut rng = rand::rng();
862
863 for y in 0..height {
865 for x in 0..width {
866 let t1 = sheet1_processed.get_pixel(x, y)[0] as f64 / 255.0;
868 let t2 = sheet2_processed.get_pixel(x, y)[0] as f64 / 255.0;
869 let tt = target_processed.get_pixel(x, y)[0] as f64 / 255.0;
870
871 let (t1, t2, tt) = adjust_triplet(t1, t2, tt);
873
874 let s1 = (t1 * m as f64).round() as usize;
876 let s2 = (t2 * m as f64).round() as usize;
877 let st = (tt * m as f64).round() as usize;
878
879 let matrices = generate_boolean_matrices(s1, s2, st, m);
881
882 if !matrices.is_empty() {
883 let matrix = &matrices[rng.random_range(0..matrices.len())];
885
886 for i in 0..sub_size {
888 for j in 0..sub_size {
889 let idx = i * sub_size + j;
890 if idx < m {
891 let pixel_val1 = if matrix[0][idx] == 1 { 255 } else { 0 };
892 let pixel_val2 = if matrix[1][idx] == 1 { 255 } else { 0 };
893
894 out_sheet1.put_pixel(
895 x * sub_size as u32 + j as u32,
896 y * sub_size as u32 + i as u32,
897 Luma([pixel_val1]),
898 );
899 out_sheet2.put_pixel(
900 x * sub_size as u32 + j as u32,
901 y * sub_size as u32 + i as u32,
902 Luma([pixel_val2]),
903 );
904 }
905 }
906 }
907 } else {
908 for i in 0..sub_size {
910 for j in 0..sub_size {
911 let val1 = if rng.random_bool(0.5) { 255 } else { 0 };
912 let val2 = if rng.random_bool(0.5) { 255 } else { 0 };
913
914 out_sheet1.put_pixel(
915 x * sub_size as u32 + j as u32,
916 y * sub_size as u32 + i as u32,
917 Luma([val1]),
918 );
919 out_sheet2.put_pixel(
920 x * sub_size as u32 + j as u32,
921 y * sub_size as u32 + i as u32,
922 Luma([val2]),
923 );
924 }
925 }
926 }
927 }
928 }
929
930 Ok(vec![
931 Share::new(
932 DynamicImage::ImageLuma8(out_sheet1),
933 1,
934 2,
935 width,
936 height,
937 sub_size,
938 true,
939 ),
940 Share::new(
941 DynamicImage::ImageLuma8(out_sheet2),
942 2,
943 2,
944 width,
945 height,
946 sub_size,
947 true,
948 ),
949 ])
950}
951
952fn yamaguchi_nakajima_decrypt(shares: &[Share], _config: &VCConfig) -> Result<DynamicImage> {
954 if shares.len() < 2 {
955 return Err(VCError::InsufficientShares {
956 required: 2,
957 provided: shares.len(),
958 });
959 }
960
961 let (expanded_width, expanded_height) = shares[0].dimensions();
962
963 let sheet1 = if let DynamicImage::ImageLuma8(img) = &shares[0].image {
965 img
966 } else {
967 return Err(VCError::DecryptionError(
968 "Share 1 is not grayscale".to_string(),
969 ));
970 };
971
972 let sheet2 = if let DynamicImage::ImageLuma8(img) = &shares[1].image {
973 img
974 } else {
975 return Err(VCError::DecryptionError(
976 "Share 2 is not grayscale".to_string(),
977 ));
978 };
979
980 let mut result = ImageBuffer::new(expanded_width, expanded_height);
982
983 for y in 0..expanded_height {
985 for x in 0..expanded_width {
986 let pixel1 = sheet1.get_pixel(x, y)[0];
987 let pixel2 = sheet2.get_pixel(x, y)[0];
988
989 let result_pixel = if pixel1 == 255 && pixel2 == 255 {
991 255
992 } else {
993 0
994 };
995 result.put_pixel(x, y, Luma([result_pixel]));
996 }
997 }
998
999 Ok(DynamicImage::ImageLuma8(result))
1000}
1001
1002fn apply_contrast_and_halftone(
1004 image: &ImageBuffer<Luma<u8>, Vec<u8>>,
1005 contrast: f64,
1006 l: f64,
1007) -> ImageBuffer<Luma<u8>, Vec<u8>> {
1008 let (width, height) = (image.width(), image.height());
1009 let mut working_image = ImageBuffer::new(width, height);
1010
1011 for y in 0..height {
1013 for x in 0..width {
1014 let pixel = image.get_pixel(x, y)[0] as f64 / 255.0;
1015 let adjusted = l + pixel * contrast;
1016 working_image.put_pixel(x, y, Luma([(adjusted * 255.0) as u8]));
1017 }
1018 }
1019
1020 floyd_steinberg_dithering(&working_image)
1022}
1023
1024fn apply_halftone_target(
1026 image: &ImageBuffer<Luma<u8>, Vec<u8>>,
1027 contrast: f64,
1028) -> ImageBuffer<Luma<u8>, Vec<u8>> {
1029 let (width, height) = (image.width(), image.height());
1030 let mut working_image = ImageBuffer::new(width, height);
1031
1032 for y in 0..height {
1034 for x in 0..width {
1035 let pixel = image.get_pixel(x, y)[0] as f64 / 255.0;
1036 let adjusted = pixel * contrast;
1037 working_image.put_pixel(x, y, Luma([(adjusted * 255.0) as u8]));
1038 }
1039 }
1040
1041 floyd_steinberg_dithering(&working_image)
1043}
1044
1045fn floyd_steinberg_dithering(
1047 image: &ImageBuffer<Luma<u8>, Vec<u8>>,
1048) -> ImageBuffer<Luma<u8>, Vec<u8>> {
1049 let (width, height) = (image.width(), image.height());
1050 let mut working = vec![vec![0.0; width as usize]; height as usize];
1051 let mut result = ImageBuffer::new(width, height);
1052
1053 for y in 0..height {
1055 for x in 0..width {
1056 working[y as usize][x as usize] = image.get_pixel(x, y)[0] as f64 / 255.0;
1057 }
1058 }
1059
1060 for y in 0..height {
1062 for x in 0..width {
1063 let old_pixel = working[y as usize][x as usize];
1064 let new_pixel = if old_pixel > 0.5 { 1.0 } else { 0.0 };
1065 result.put_pixel(x, y, Luma([(new_pixel * 255.0) as u8]));
1066
1067 let error = old_pixel - new_pixel;
1068
1069 if x + 1 < width {
1071 working[y as usize][(x + 1) as usize] += error * 7.0 / 16.0;
1072 }
1073 if y + 1 < height {
1074 if x > 0 {
1075 working[(y + 1) as usize][(x - 1) as usize] += error * 3.0 / 16.0;
1076 }
1077 working[(y + 1) as usize][x as usize] += error * 5.0 / 16.0;
1078 if x + 1 < width {
1079 working[(y + 1) as usize][(x + 1) as usize] += error * 1.0 / 16.0;
1080 }
1081 }
1082 }
1083 }
1084
1085 result
1086}
1087
1088fn adjust_triplet(t1: f64, t2: f64, tt: f64) -> (f64, f64, f64) {
1090 let min_tt = (0.0_f64).max(t1 + t2 - 1.0);
1091 let max_tt = t1.min(t2);
1092
1093 let tt_adj = if tt < min_tt {
1094 min_tt
1095 } else if tt > max_tt {
1096 max_tt
1097 } else {
1098 tt
1099 };
1100
1101 (t1, t2, tt_adj)
1102}
1103
1104fn generate_boolean_matrices(s1: usize, s2: usize, st: usize, m: usize) -> Vec<Vec<Vec<u8>>> {
1106 let p11 = st; let p10 = s1.saturating_sub(st); let p01 = s2.saturating_sub(st); let p00 = m.saturating_sub(p11 + p10 + p01); if p11 + p10 + p01 + p00 != m {
1114 return vec![];
1115 }
1116
1117 let mut base_patterns = Vec::new();
1119
1120 for _ in 0..p11 {
1122 base_patterns.push([1, 1]);
1123 }
1124 for _ in 0..p10 {
1126 base_patterns.push([1, 0]);
1127 }
1128 for _ in 0..p01 {
1130 base_patterns.push([0, 1]);
1131 }
1132 for _ in 0..p00 {
1134 base_patterns.push([0, 0]);
1135 }
1136
1137 let mut rng = rand::rng();
1139 base_patterns.shuffle(&mut rng);
1140
1141 let matrix = vec![
1142 base_patterns
1143 .iter()
1144 .map(|pair| pair[0])
1145 .collect::<Vec<u8>>(),
1146 base_patterns
1147 .iter()
1148 .map(|pair| pair[1])
1149 .collect::<Vec<u8>>(),
1150 ];
1151
1152 vec![matrix]
1153}