visual_cryptography/
algorithms.rs

1//! Visual cryptography algorithms implementation
2
3use 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/// Available visual cryptography algorithms
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum Algorithm {
16    /// Basic (k,n) threshold scheme
17    BasicThreshold,
18    /// Naor-Shamir original scheme (1994)
19    NaorShamir,
20    /// Taghaddos-Latif for grayscale (2014)
21    TaghaddosLatif,
22    /// Dhiman-Kasana for color images (2018)
23    DhimanKasana,
24    /// XOR-based scheme for better contrast
25    XorBased,
26    /// Yamaguchi-Nakajima Extended Visual Cryptography for Natural Images
27    YamaguchiNakajima,
28}
29
30/// Trait for visual cryptography schemes
31pub trait VCScheme {
32    /// Encrypt an image into shares
33    fn encrypt(
34        &self,
35        image: &DynamicImage,
36        config: &VCConfig,
37        cover_images: Option<Vec<DynamicImage>>,
38    ) -> Result<Vec<Share>>;
39
40    /// Decrypt shares back into an image
41    fn decrypt(&self, shares: &[Share], config: &VCConfig) -> Result<DynamicImage>;
42}
43
44/// Main encryption function that dispatches to the appropriate algorithm
45pub 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
60/// Main decryption function that dispatches to the appropriate algorithm
61pub 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
72/// XOR-based encryption for better contrast
73fn 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    // Generate XOR matrices
78    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    // Process each pixel
85    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            // Select appropriate matrix
91            let matrix = if is_black {
92                &xor_matrices.black_pixel
93            } else {
94                &xor_matrices.white_pixel
95            };
96
97            // Select random column
98            let col = rng.random_range(0..matrix.ncols());
99
100            // Distribute values to shares
101            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    // Convert to Share objects
110    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
129/// XOR-based decryption
130fn 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            // XOR result: 1 = black, 0 = white
154            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
162/// Basic (k,n) threshold scheme encryption
163fn basic_threshold_encrypt(image: &DynamicImage, config: &VCConfig) -> Result<Vec<Share>> {
164    // Convert to binary image
165    let binary = convert_to_binary(image);
166    let (width, height) = (binary.width(), binary.height());
167
168    // Generate sharing matrices
169    let matrices = generate_basic_matrices(config.threshold, config.num_shares, config.block_size)?;
170
171    // Create shares with expanded dimensions
172    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    // Process each pixel
180    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 }; // black = 0, white = 255
184            let matrix = &matrices[matrix_idx];
185
186            // Select a random column from the matrix
187            let col = rng.random_range(0..matrix.ncols());
188
189            // Distribute the column values to shares
190            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                // Expand pixel to block
195                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    // Convert to Share objects
208    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
227/// Basic threshold scheme decryption
228fn 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
238/// Naor-Shamir original scheme encryption
239fn naor_shamir_encrypt(image: &DynamicImage, config: &VCConfig) -> Result<Vec<Share>> {
240    // The original Naor-Shamir is a (2,2) scheme with 2x2 pixel expansion
241    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    // Fixed matrices for Naor-Shamir as per the original paper
251    // For white pixels: both shares get identical patterns
252    let white_matrix = vec![vec![1, 1, 0, 0], vec![1, 1, 0, 0]];
253    // For black pixels: shares get complementary patterns
254    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            // Select appropriate matrix based on pixel value
269            let matrix = if pixel == 0 {
270                // black pixel
271                &black_matrix
272            } else {
273                // white pixel
274                &white_matrix
275            };
276
277            // Randomly permute columns (this is the key step in Naor-Shamir)
278            let mut columns: Vec<usize> = (0..4).collect();
279            columns.as_mut_slice().shuffle(&mut rng);
280
281            // Create permuted patterns for both shares
282            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            // Apply 2x2 patterns to shares
296            // Share 1
297            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            // Share 2
319            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
364/// Naor-Shamir decryption
365fn naor_shamir_decrypt(shares: &[Share], config: &VCConfig) -> Result<DynamicImage> {
366    basic_threshold_decrypt(shares, config)
367}
368
369/// Taghaddos-Latif grayscale scheme following the original paper exactly
370fn 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    // The 6 specific patterns from the original paper
381    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    // Pixel expansion by 2 (2x2 blocks)
391    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    // Process each pixel
400    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            // Process each bit plane (0-7)
407            for bit_pos in 0..8 {
408                let bit = (pixel_value >> bit_pos) & 1;
409
410                // Randomly select one of the 6 patterns
411                let pattern = patterns[rng.random_range(0..6)];
412
413                if bit == 1 {
414                    // White pixel (bit = 1): both shares get identical patterns
415                    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                    // Black pixel (bit = 0): share B gets complement of share A
421                    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            // Draw 2x2 blocks for each share
429            let base_x = x * 2;
430            let base_y = y * 2;
431
432            // Share A 2x2 block
433            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 2x2 block
439            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, // pixel expansion = 2
454            false,
455        ),
456        Share::new(
457            DynamicImage::ImageLuma8(share_b),
458            2,
459            2,
460            width,
461            height,
462            2, // pixel expansion = 2
463            false,
464        ),
465    ])
466}
467
468/// Taghaddos-Latif decryption using AND operation as per original paper
469fn 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    // Original dimensions (accounting for pixel expansion)
480    let width = expanded_width / 2;
481    let height = expanded_height / 2;
482
483    let mut result = ImageBuffer::new(width, height);
484
485    // Extract share images
486    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    // Process each original pixel (reconstructing from 2x2 blocks)
503    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            // Get the 2x2 block values from both shares
509            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            // Reconstruct pixel value using AND operation
524            let mut reconstructed_value = 0u8;
525
526            for bit_pos in 0..8 {
527                let mut reconstructed_bits = [0u8; 4];
528
529                // Apply AND operation for each sub-pixel in the 2x2 block
530                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                // Average the 4 sub-pixels to get the final bit
537                // (this follows the HVS perception model from the paper)
538                let sum = reconstructed_bits.iter().map(|&x| x as u32).sum::<u32>();
539                let average_bit = if sum >= 2 { 1 } else { 0 }; // majority voting for sub-pixels
540
541                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
551/// Dhiman-Kasana EVCT(3,3) color scheme
552fn 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    // Specific bit position coordinates for each RGB channel
567    let components = [
568        // R channel positions
569        [
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        // G channel positions
580        [
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        // B channel positions
591        [
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    // 5x5 pixel expansion
604    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    // Check if cover images are provided before moving them
613    let has_cover_images = cover_images.is_some();
614
615    // Use cover images if provided, otherwise use default cover colors
616    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        // Default cover colors (white background)
625        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    // Process each pixel
633    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            // Process each share
639            for share_idx in 0..3 {
640                let cover_pixel = covers[share_idx].get_pixel(x, y);
641
642                // Create 5x5 block filled with cover pixel color
643                let mut block = ImageBuffer::from_pixel(5, 5, *cover_pixel);
644
645                // Process each color channel
646                for (channel_idx, &channel_value) in [r, g, b].iter().enumerate() {
647                    let bit_positions = &components[channel_idx];
648
649                    // Process each bit of the channel (8 bits)
650                    for (bit_idx, &(bit_y, bit_x)) in bit_positions.iter().enumerate() {
651                        let bit = (channel_value >> bit_idx) & 1;
652
653                        // Encode bit: 1 = black (0,0,0), 0 = dark grey (30,30,30)
654                        let pixel_color = if bit == 1 {
655                            Rgb([0, 0, 0]) // Black for bit 1
656                        } else {
657                            Rgb([30, 30, 30]) // Dark grey for bit 0
658                        };
659
660                        block.put_pixel(bit_x, bit_y, pixel_color);
661                    }
662                }
663
664                // Paste the 5x5 block into the share
665                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    // Convert to Share objects
678    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, // 5x5 pixel expansion
689                has_cover_images,
690            )
691        })
692        .collect();
693
694    Ok(result)
695}
696
697/// Dhiman-Kasana decryption using XOR operation
698fn 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    // Get dimensions from the first share (expanded)
707    let (expanded_width, expanded_height) = shares[0].dimensions();
708
709    // Original dimensions (accounting for 5x5 pixel expansion)
710    let width = expanded_width / 5;
711    let height = expanded_height / 5;
712
713    let mut result = ImageBuffer::new(width, height);
714
715    // Bit position coordinates for each RGB channel
716    let components = [
717        // R channel positions
718        [
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        // G channel positions
729        [
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        // B channel positions
740        [
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    // Extract RGB images from shares
753    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    // Process each original pixel
765    for y in 0..height {
766        for x in 0..width {
767            let mut reconstructed_pixel = [0u8; 3];
768
769            // Process each color channel
770            for channel_idx in 0..3 {
771                let bit_positions = &components[channel_idx];
772                let mut channel_value = 0u8;
773
774                // Extract bits from each position
775                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                    // Get the pixel from the corresponding share at the bit position
780                    let pixel = share_images[channel_idx].get_pixel(base_x + bit_x, base_y + bit_y);
781
782                    // Decode bit: (0,0,0) = 1, anything else = 0
783                    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
798/// Yamaguchi-Nakajima Extended Visual Cryptography for Natural Images
799/// Based on Nakajima & Yamaguchi (2002)
800fn 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    // Resize all images to same size
829    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    // Number of subpixels per pixel (default 16 for 4x4 subpixel structure)
843    let m = config.block_size.max(4);
844    let sub_size = (m as f64).sqrt() as usize;
845
846    // Contrast adjustment
847    let contrast = 0.6;
848    let l = (1.0 - contrast) / 2.0; // Lower bound
849
850    // Process images with contrast adjustment and halftoning
851    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    // Create output sheets with subpixel expansion
856    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    // Process each pixel
864    for y in 0..height {
865        for x in 0..width {
866            // Get transparency values (0.0 to 1.0)
867            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            // Adjust triplet if constraint violated
872            let (t1, t2, tt) = adjust_triplet(t1, t2, tt);
873
874            // Convert to subpixel counts
875            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            // Generate valid Boolean matrices
880            let matrices = generate_boolean_matrices(s1, s2, st, m);
881
882            if !matrices.is_empty() {
883                // Randomly select a matrix
884                let matrix = &matrices[rng.random_range(0..matrices.len())];
885
886                // Fill subpixels
887                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                // Fallback: fill with random pattern
909                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
952/// Yamaguchi-Nakajima decryption using AND operation
953fn 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    // Extract share images
964    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    // Create result image with expanded dimensions (no downsampling)
981    let mut result = ImageBuffer::new(expanded_width, expanded_height);
982
983    // Reveal target using Boolean AND operation
984    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            // Boolean AND: both must be white (255) for result to be white
990            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
1002/// Apply contrast adjustment and Floyd-Steinberg error diffusion halftoning
1003fn 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    // Apply contrast adjustment
1012    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    // Apply Floyd-Steinberg error diffusion
1021    floyd_steinberg_dithering(&working_image)
1022}
1023
1024/// Apply halftoning to target image
1025fn 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    // Apply contrast adjustment (different for target)
1033    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    // Apply Floyd-Steinberg error diffusion
1042    floyd_steinberg_dithering(&working_image)
1043}
1044
1045/// Floyd-Steinberg error diffusion algorithm
1046fn 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    // Copy to working array
1054    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    // Floyd-Steinberg error diffusion
1061    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            // Distribute error to neighboring pixels
1070            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
1088/// Adjust triplet values if they violate the constraint
1089fn 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
1104/// Generate all valid Boolean matrices for given transparency values
1105fn generate_boolean_matrices(s1: usize, s2: usize, st: usize, m: usize) -> Vec<Vec<Vec<u8>>> {
1106    // Calculate subpixel pair counts
1107    let p11 = st; // Both transparent
1108    let p10 = s1.saturating_sub(st); // Sheet1 transparent, sheet2 opaque
1109    let p01 = s2.saturating_sub(st); // Sheet1 opaque, sheet2 transparent
1110    let p00 = m.saturating_sub(p11 + p10 + p01); // Both opaque
1111
1112    // Check validity
1113    if p11 + p10 + p01 + p00 != m {
1114        return vec![];
1115    }
1116
1117    // Create base matrix patterns
1118    let mut base_patterns = Vec::new();
1119
1120    // Add transparent-transparent pairs
1121    for _ in 0..p11 {
1122        base_patterns.push([1, 1]);
1123    }
1124    // Add transparent-opaque pairs
1125    for _ in 0..p10 {
1126        base_patterns.push([1, 0]);
1127    }
1128    // Add opaque-transparent pairs
1129    for _ in 0..p01 {
1130        base_patterns.push([0, 1]);
1131    }
1132    // Add opaque-opaque pairs
1133    for _ in 0..p00 {
1134        base_patterns.push([0, 0]);
1135    }
1136
1137    // Generate a single valid matrix (could be extended to generate all permutations)
1138    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}