visual_cryptography/
algorithms.rs

1//! Visual cryptography algorithms implementation
2
3use crate::{
4    error::{Result, VCError},
5    matrix::{
6        generate_basic_matrices, generate_color_mixing_matrices, generate_dispatching_matrices,
7        generate_proper_sharing_matrices, generate_xor_matrices, select_dispatching_row,
8        ColorMixingMatrices, XorMatrices,
9    },
10    share::{stack_shares, Share},
11    utils::{apply_halftone, convert_to_binary, expand_pixel},
12    VCConfig,
13};
14use image::{DynamicImage, ImageBuffer, Luma, Rgb};
15use rand::{seq::SliceRandom, Rng};
16
17/// Available visual cryptography algorithms
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum Algorithm {
20    /// Basic (k,n) threshold scheme
21    BasicThreshold,
22    /// Progressive visual cryptography
23    Progressive,
24    /// Extended visual cryptography with meaningful shares
25    ExtendedMeaningful,
26    /// Naor-Shamir original scheme (1994)
27    NaorShamir,
28    /// Taghaddos-Latif for grayscale (2014)
29    TaghaddosLatif,
30    /// Dhiman-Kasana for color images (2018)
31    DhimanKasana,
32    /// XOR-based scheme for better contrast
33    XorBased,
34    /// Yamaguchi-Nakajima Extended Visual Cryptography for Natural Images
35    YamaguchiNakajima,
36}
37
38/// Trait for visual cryptography schemes
39pub trait VCScheme {
40    /// Encrypt an image into shares
41    fn encrypt(
42        &self,
43        image: &DynamicImage,
44        config: &VCConfig,
45        cover_images: Option<Vec<DynamicImage>>,
46    ) -> Result<Vec<Share>>;
47
48    /// Decrypt shares back into an image
49    fn decrypt(&self, shares: &[Share], config: &VCConfig) -> Result<DynamicImage>;
50}
51
52/// Main encryption function that dispatches to the appropriate algorithm
53pub fn encrypt(
54    image: &DynamicImage,
55    config: &VCConfig,
56    cover_images: Option<Vec<DynamicImage>>,
57) -> Result<Vec<Share>> {
58    match config.algorithm {
59        Algorithm::BasicThreshold => basic_threshold_encrypt(image, config),
60        Algorithm::Progressive => progressive_encrypt(image, config, cover_images),
61        Algorithm::ExtendedMeaningful => extended_meaningful_encrypt(image, config, cover_images),
62        Algorithm::NaorShamir => naor_shamir_encrypt(image, config),
63        Algorithm::TaghaddosLatif => taghaddos_latif_encrypt(image, config),
64        Algorithm::DhimanKasana => dhiman_kasana_encrypt(image, config, cover_images),
65        Algorithm::XorBased => xor_based_encrypt(image, config),
66        Algorithm::YamaguchiNakajima => yamaguchi_nakajima_encrypt(image, config, cover_images),
67    }
68}
69
70/// Main decryption function that dispatches to the appropriate algorithm
71pub fn decrypt(shares: &[Share], config: &VCConfig) -> Result<DynamicImage> {
72    match config.algorithm {
73        Algorithm::BasicThreshold => basic_threshold_decrypt(shares, config),
74        Algorithm::Progressive => progressive_decrypt(shares, config),
75        Algorithm::ExtendedMeaningful => extended_meaningful_decrypt(shares, config),
76        Algorithm::NaorShamir => naor_shamir_decrypt(shares, config),
77        Algorithm::TaghaddosLatif => taghaddos_latif_decrypt(shares, config),
78        Algorithm::DhimanKasana => dhiman_kasana_decrypt(shares, config),
79        Algorithm::XorBased => xor_based_decrypt(shares, config),
80        Algorithm::YamaguchiNakajima => yamaguchi_nakajima_decrypt(shares, config),
81    }
82}
83
84/// XOR-based encryption for better contrast
85fn xor_based_encrypt(image: &DynamicImage, config: &VCConfig) -> Result<Vec<Share>> {
86    let binary = convert_to_binary(image);
87    let (width, height) = (binary.width(), binary.height());
88
89    // Generate XOR matrices
90    let xor_matrices = generate_xor_matrices(config.num_shares)?;
91
92    let mut shares = Vec::new();
93    for i in 0..config.num_shares {
94        shares.push(ImageBuffer::new(width, height));
95    }
96
97    let mut rng = rand::thread_rng();
98
99    // Process each pixel
100    for y in 0..height {
101        for x in 0..width {
102            let pixel = binary.get_pixel(x, y)[0];
103            let is_black = pixel == 0;
104
105            // Select appropriate matrix
106            let matrix = if is_black {
107                &xor_matrices.black_pixel
108            } else {
109                &xor_matrices.white_pixel
110            };
111
112            // Select random column
113            let col = rng.gen_range(0..matrix.ncols());
114
115            // Distribute values to shares
116            for share_idx in 0..config.num_shares {
117                let value = matrix[(share_idx, col)];
118                let pixel_value = if value == 1 { 0u8 } else { 255u8 };
119                shares[share_idx].put_pixel(x, y, Luma([pixel_value]));
120            }
121        }
122    }
123
124    // Convert to Share objects
125    let result: Vec<Share> = shares
126        .into_iter()
127        .enumerate()
128        .map(|(i, img)| {
129            Share::new(
130                DynamicImage::ImageLuma8(img),
131                i + 1,
132                config.num_shares,
133                width,
134                height,
135                1,
136                false,
137            )
138        })
139        .collect();
140
141    Ok(result)
142}
143
144/// XOR-based decryption
145fn xor_based_decrypt(shares: &[Share], _config: &VCConfig) -> Result<DynamicImage> {
146    if shares.is_empty() {
147        return Err(VCError::InsufficientShares {
148            required: 1,
149            provided: 0,
150        });
151    }
152
153    let (width, height) = shares[0].dimensions();
154    let mut result = ImageBuffer::new(width, height);
155
156    for y in 0..height {
157        for x in 0..width {
158            let mut xor_value = 0u8;
159
160            for share in shares {
161                if let DynamicImage::ImageLuma8(img) = &share.image {
162                    let pixel = img.get_pixel(x, y)[0];
163                    let bit = if pixel == 0 { 1 } else { 0 };
164                    xor_value ^= bit;
165                }
166            }
167
168            // XOR result: 1 = black, 0 = white
169            let pixel_value = if xor_value == 1 { 0u8 } else { 255u8 };
170            result.put_pixel(x, y, Luma([pixel_value]));
171        }
172    }
173
174    Ok(DynamicImage::ImageLuma8(result))
175}
176
177/// Basic (k,n) threshold scheme encryption
178fn basic_threshold_encrypt(image: &DynamicImage, config: &VCConfig) -> Result<Vec<Share>> {
179    // Convert to binary image
180    let binary = convert_to_binary(image);
181    let (width, height) = (binary.width(), binary.height());
182
183    // Generate sharing matrices
184    let matrices = generate_basic_matrices(config.threshold, config.num_shares, config.block_size)?;
185
186    // Create shares with expanded dimensions
187    let share_width = width * config.block_size as u32;
188    let share_height = height * config.block_size as u32;
189
190    let mut shares = Vec::new();
191    for i in 0..config.num_shares {
192        shares.push(ImageBuffer::new(share_width, share_height));
193    }
194
195    let mut rng = rand::thread_rng();
196
197    // Process each pixel
198    for y in 0..height {
199        for x in 0..width {
200            let pixel = binary.get_pixel(x, y)[0];
201            let matrix_idx = if pixel == 0 { 1 } else { 0 }; // black = 0, white = 255
202            let matrix = &matrices[matrix_idx];
203
204            // Select a random column from the matrix
205            let col = rng.gen_range(0..matrix.ncols());
206
207            // Distribute the column values to shares
208            for share_idx in 0..config.num_shares {
209                let value = matrix[(share_idx, col)];
210                let block_value = if value == 1 { 0u8 } else { 255u8 };
211
212                // Expand pixel to block
213                let base_x = x * config.block_size as u32;
214                let base_y = y * config.block_size as u32;
215
216                for dy in 0..config.block_size as u32 {
217                    for dx in 0..config.block_size as u32 {
218                        shares[share_idx].put_pixel(base_x + dx, base_y + dy, Luma([block_value]));
219                    }
220                }
221            }
222        }
223    }
224
225    // Convert to Share objects
226    let result: Vec<Share> = shares
227        .into_iter()
228        .enumerate()
229        .map(|(i, img)| {
230            Share::new(
231                DynamicImage::ImageLuma8(img),
232                i + 1,
233                config.num_shares,
234                width,
235                height,
236                config.block_size,
237                false,
238            )
239        })
240        .collect();
241
242    Ok(result)
243}
244
245/// Basic threshold scheme decryption
246fn basic_threshold_decrypt(shares: &[Share], _config: &VCConfig) -> Result<DynamicImage> {
247    if let Some(stacked) = stack_shares(shares) {
248        Ok(DynamicImage::ImageLuma8(stacked))
249    } else {
250        Err(VCError::DecryptionError(
251            "Failed to stack shares".to_string(),
252        ))
253    }
254}
255
256/// Progressive visual cryptography encryption
257fn progressive_encrypt(
258    image: &DynamicImage,
259    config: &VCConfig,
260    cover_images: Option<Vec<DynamicImage>>,
261) -> Result<Vec<Share>> {
262    // Convert to binary
263    let binary = convert_to_binary(image);
264    let (width, height) = (binary.width(), binary.height());
265
266    // Use meaningful shares if cover images are provided
267    if let Some(covers) = cover_images {
268        if covers.len() != config.num_shares {
269            return Err(VCError::CoverImageError(format!(
270                "Number of cover images ({}) must match number of shares ({})",
271                covers.len(),
272                config.num_shares
273            )));
274        }
275
276        // Generate dispatching matrices
277        let i = config.num_shares; // Use n for maximum contrast
278        let matrices = generate_dispatching_matrices(config.num_shares, i)?;
279
280        let mut shares = Vec::new();
281
282        for share_idx in 0..config.num_shares {
283            let mut share_img = ImageBuffer::new(width, height);
284            let cover_binary = convert_to_binary(&covers[share_idx]);
285
286            for y in 0..height {
287                for x in 0..width {
288                    let secret_pixel = binary.get_pixel(x, y)[0] == 0; // true if black
289                    let cover_pixel = cover_binary.get_pixel(x, y)[0] == 0; // true if black
290
291                    let row = select_dispatching_row(&matrices, secret_pixel, cover_pixel);
292                    let value = if row[share_idx] == 1 { 0u8 } else { 255u8 };
293
294                    share_img.put_pixel(x, y, Luma([value]));
295                }
296            }
297
298            shares.push(Share::new(
299                DynamicImage::ImageLuma8(share_img),
300                share_idx + 1,
301                config.num_shares,
302                width,
303                height,
304                1,    // No pixel expansion
305                true, // Meaningful share
306            ));
307        }
308
309        Ok(shares)
310    } else {
311        // Use proper progressive matrix construction
312        progressive_matrix_based_encrypt(image, config)
313    }
314}
315
316/// Proper progressive encryption using matrix construction
317fn progressive_matrix_based_encrypt(image: &DynamicImage, config: &VCConfig) -> Result<Vec<Share>> {
318    let binary = convert_to_binary(image);
319    let (width, height) = (binary.width(), binary.height());
320
321    // Generate proper sharing matrices for progressive revelation
322    let (white_matrix, black_matrix) = generate_proper_sharing_matrices(2, config.num_shares)?;
323
324    let mut shares = Vec::new();
325    for i in 0..config.num_shares {
326        shares.push(ImageBuffer::new(width, height));
327    }
328
329    let mut rng = rand::thread_rng();
330
331    // Process each pixel
332    for y in 0..height {
333        for x in 0..width {
334            let pixel = binary.get_pixel(x, y)[0];
335            let is_black = pixel == 0;
336
337            // Select appropriate matrix
338            let matrix = if is_black {
339                &black_matrix
340            } else {
341                &white_matrix
342            };
343
344            // For progressive schemes, weight column selection by share index
345            let total_cols = matrix.ncols();
346            let mut col_weights = Vec::new();
347
348            for col in 0..total_cols {
349                // Calculate weight based on how many participants have black pixels
350                let black_count = (0..config.num_shares)
351                    .map(|row| matrix[(row, col)] as usize)
352                    .sum::<usize>();
353
354                // Higher weight for patterns that progressively reveal
355                let weight = if is_black {
356                    (config.num_shares - black_count) + 1
357                } else {
358                    black_count + 1
359                };
360
361                col_weights.push(weight);
362            }
363
364            // Weighted random selection
365            let total_weight: usize = col_weights.iter().sum();
366            let mut random_weight = rng.gen_range(0..total_weight);
367            let mut selected_col = 0;
368
369            for (col, &weight) in col_weights.iter().enumerate() {
370                if random_weight < weight {
371                    selected_col = col;
372                    break;
373                }
374                random_weight -= weight;
375            }
376
377            // Apply selected column to shares
378            for share_idx in 0..config.num_shares {
379                let value = matrix[(share_idx, selected_col)];
380                let pixel_value = if value == 1 { 0u8 } else { 255u8 };
381                shares[share_idx].put_pixel(x, y, Luma([pixel_value]));
382            }
383        }
384    }
385
386    // Convert to Share objects
387    let result: Vec<Share> = shares
388        .into_iter()
389        .enumerate()
390        .map(|(i, img)| {
391            Share::new(
392                DynamicImage::ImageLuma8(img),
393                i + 1,
394                config.num_shares,
395                width,
396                height,
397                1,
398                false,
399            )
400        })
401        .collect();
402
403    Ok(result)
404}
405
406/// Progressive decryption
407fn progressive_decrypt(shares: &[Share], _config: &VCConfig) -> Result<DynamicImage> {
408    basic_threshold_decrypt(shares, _config)
409}
410
411/// Extended meaningful shares encryption
412fn extended_meaningful_encrypt(
413    image: &DynamicImage,
414    config: &VCConfig,
415    cover_images: Option<Vec<DynamicImage>>,
416) -> Result<Vec<Share>> {
417    if cover_images.is_none() {
418        return Err(VCError::CoverImageError(
419            "Extended meaningful scheme requires cover images".to_string(),
420        ));
421    }
422
423    // Use proper extended VCS algorithm
424    let binary = convert_to_binary(image);
425    let (width, height) = (binary.width(), binary.height());
426    let covers = cover_images.unwrap();
427
428    // Generate dispatching matrices for extended scheme
429    let matrices = generate_dispatching_matrices(config.num_shares, config.num_shares)?;
430
431    let mut shares = Vec::new();
432
433    for share_idx in 0..config.num_shares {
434        let mut share_img = ImageBuffer::new(width, height);
435        let cover_binary = convert_to_binary(&covers[share_idx]);
436
437        for y in 0..height {
438            for x in 0..width {
439                let secret_pixel = binary.get_pixel(x, y)[0] == 0;
440                let cover_pixel = cover_binary.get_pixel(x, y)[0] == 0;
441
442                // Use extended algorithm logic
443                let row = select_dispatching_row(&matrices, secret_pixel, cover_pixel);
444                let value = if row[share_idx] == 1 { 0u8 } else { 255u8 };
445
446                share_img.put_pixel(x, y, Luma([value]));
447            }
448        }
449
450        shares.push(Share::new(
451            DynamicImage::ImageLuma8(share_img),
452            share_idx + 1,
453            config.num_shares,
454            width,
455            height,
456            1,
457            true,
458        ));
459    }
460
461    Ok(shares)
462}
463
464/// Extended meaningful decryption
465fn extended_meaningful_decrypt(shares: &[Share], config: &VCConfig) -> Result<DynamicImage> {
466    basic_threshold_decrypt(shares, config)
467}
468
469/// Naor-Shamir original scheme encryption
470fn naor_shamir_encrypt(image: &DynamicImage, config: &VCConfig) -> Result<Vec<Share>> {
471    // The original Naor-Shamir is a (2,2) scheme with 2x2 pixel expansion
472    if config.num_shares != 2 || config.threshold != 2 {
473        return Err(VCError::InvalidConfiguration(
474            "Original Naor-Shamir scheme requires exactly 2 shares with threshold 2".to_string(),
475        ));
476    }
477
478    let binary = convert_to_binary(image);
479    let (width, height) = (binary.width(), binary.height());
480
481    // Fixed matrices for Naor-Shamir as per the original paper
482    // For white pixels: both shares get identical patterns
483    let white_matrix = vec![vec![1, 1, 0, 0], vec![1, 1, 0, 0]];
484    // For black pixels: shares get complementary patterns
485    let black_matrix = vec![vec![1, 1, 0, 0], vec![0, 0, 1, 1]];
486
487    let share_width = width * 2;
488    let share_height = height * 2;
489
490    let mut share1 = ImageBuffer::new(share_width, share_height);
491    let mut share2 = ImageBuffer::new(share_width, share_height);
492
493    let mut rng = rand::thread_rng();
494
495    for y in 0..height {
496        for x in 0..width {
497            let pixel = binary.get_pixel(x, y)[0];
498
499            // Select appropriate matrix based on pixel value
500            let matrix = if pixel == 0 {
501                // black pixel
502                &black_matrix
503            } else {
504                // white pixel
505                &white_matrix
506            };
507
508            // Randomly permute columns (this is the key step in Naor-Shamir)
509            let mut columns: Vec<usize> = (0..4).collect();
510            columns.as_mut_slice().shuffle(&mut rng);
511
512            // Create permuted patterns for both shares
513            let share1_pattern = vec![
514                matrix[0][columns[0]],
515                matrix[0][columns[1]],
516                matrix[0][columns[2]],
517                matrix[0][columns[3]],
518            ];
519            let share2_pattern = vec![
520                matrix[1][columns[0]],
521                matrix[1][columns[1]],
522                matrix[1][columns[2]],
523                matrix[1][columns[3]],
524            ];
525
526            // Apply 2x2 patterns to shares
527            // Share 1
528            share1.put_pixel(
529                x * 2,
530                y * 2,
531                Luma([if share1_pattern[0] == 1 { 0 } else { 255 }]),
532            );
533            share1.put_pixel(
534                x * 2 + 1,
535                y * 2,
536                Luma([if share1_pattern[1] == 1 { 0 } else { 255 }]),
537            );
538            share1.put_pixel(
539                x * 2,
540                y * 2 + 1,
541                Luma([if share1_pattern[2] == 1 { 0 } else { 255 }]),
542            );
543            share1.put_pixel(
544                x * 2 + 1,
545                y * 2 + 1,
546                Luma([if share1_pattern[3] == 1 { 0 } else { 255 }]),
547            );
548
549            // Share 2
550            share2.put_pixel(
551                x * 2,
552                y * 2,
553                Luma([if share2_pattern[0] == 1 { 0 } else { 255 }]),
554            );
555            share2.put_pixel(
556                x * 2 + 1,
557                y * 2,
558                Luma([if share2_pattern[1] == 1 { 0 } else { 255 }]),
559            );
560            share2.put_pixel(
561                x * 2,
562                y * 2 + 1,
563                Luma([if share2_pattern[2] == 1 { 0 } else { 255 }]),
564            );
565            share2.put_pixel(
566                x * 2 + 1,
567                y * 2 + 1,
568                Luma([if share2_pattern[3] == 1 { 0 } else { 255 }]),
569            );
570        }
571    }
572
573    Ok(vec![
574        Share::new(
575            DynamicImage::ImageLuma8(share1),
576            1,
577            2,
578            width,
579            height,
580            2,
581            false,
582        ),
583        Share::new(
584            DynamicImage::ImageLuma8(share2),
585            2,
586            2,
587            width,
588            height,
589            2,
590            false,
591        ),
592    ])
593}
594
595/// Naor-Shamir decryption
596fn naor_shamir_decrypt(shares: &[Share], config: &VCConfig) -> Result<DynamicImage> {
597    basic_threshold_decrypt(shares, config)
598}
599
600/// Taghaddos-Latif grayscale scheme following the original paper exactly
601fn taghaddos_latif_encrypt(image: &DynamicImage, config: &VCConfig) -> Result<Vec<Share>> {
602    if config.num_shares != 2 {
603        return Err(VCError::InvalidConfiguration(
604            "Taghaddos-Latif scheme requires exactly 2 shares".to_string(),
605        ));
606    }
607
608    let gray = image.to_luma8();
609    let (width, height) = (gray.width(), gray.height());
610
611    // The 6 specific patterns from the original paper
612    let patterns = [
613        [1u8, 1u8, 0u8, 0u8],
614        [1u8, 0u8, 1u8, 0u8],
615        [1u8, 0u8, 0u8, 1u8],
616        [0u8, 1u8, 1u8, 0u8],
617        [0u8, 1u8, 0u8, 1u8],
618        [0u8, 0u8, 1u8, 1u8],
619    ];
620
621    // Pixel expansion by 2 (2x2 blocks)
622    let share_width = width * 2;
623    let share_height = height * 2;
624
625    let mut share_a = ImageBuffer::new(share_width, share_height);
626    let mut share_b = ImageBuffer::new(share_width, share_height);
627
628    let mut rng = rand::thread_rng();
629
630    // Process each pixel
631    for y in 0..height {
632        for x in 0..width {
633            let pixel_value = gray.get_pixel(x, y)[0];
634            let mut share_a_colors = [0u8; 4];
635            let mut share_b_colors = [0u8; 4];
636
637            // Process each bit plane (0-7)
638            for bit_pos in 0..8 {
639                let bit = (pixel_value >> bit_pos) & 1;
640
641                // Randomly select one of the 6 patterns
642                let pattern = patterns[rng.gen_range(0..6)];
643
644                if bit == 1 {
645                    // White pixel (bit = 1): both shares get identical patterns
646                    for i in 0..4 {
647                        share_a_colors[i] |= (pattern[i] << bit_pos);
648                        share_b_colors[i] = share_a_colors[i];
649                    }
650                } else {
651                    // Black pixel (bit = 0): share B gets complement of share A
652                    for i in 0..4 {
653                        share_a_colors[i] |= (pattern[i] << bit_pos);
654                        share_b_colors[i] |= ((1 - pattern[i]) << bit_pos);
655                    }
656                }
657            }
658
659            // Draw 2x2 blocks for each share
660            let base_x = x * 2;
661            let base_y = y * 2;
662
663            // Share A 2x2 block
664            share_a.put_pixel(base_x, base_y, Luma([share_a_colors[0]]));
665            share_a.put_pixel(base_x + 1, base_y, Luma([share_a_colors[1]]));
666            share_a.put_pixel(base_x, base_y + 1, Luma([share_a_colors[2]]));
667            share_a.put_pixel(base_x + 1, base_y + 1, Luma([share_a_colors[3]]));
668
669            // Share B 2x2 block
670            share_b.put_pixel(base_x, base_y, Luma([share_b_colors[0]]));
671            share_b.put_pixel(base_x + 1, base_y, Luma([share_b_colors[1]]));
672            share_b.put_pixel(base_x, base_y + 1, Luma([share_b_colors[2]]));
673            share_b.put_pixel(base_x + 1, base_y + 1, Luma([share_b_colors[3]]));
674        }
675    }
676
677    Ok(vec![
678        Share::new(
679            DynamicImage::ImageLuma8(share_a),
680            1,
681            2,
682            width,
683            height,
684            2, // pixel expansion = 2
685            false,
686        ),
687        Share::new(
688            DynamicImage::ImageLuma8(share_b),
689            2,
690            2,
691            width,
692            height,
693            2, // pixel expansion = 2
694            false,
695        ),
696    ])
697}
698
699/// Taghaddos-Latif decryption using AND operation as per original paper
700fn taghaddos_latif_decrypt(shares: &[Share], _config: &VCConfig) -> Result<DynamicImage> {
701    if shares.len() < 2 {
702        return Err(VCError::InsufficientShares {
703            required: 2,
704            provided: shares.len(),
705        });
706    }
707
708    let (expanded_width, expanded_height) = shares[0].dimensions();
709
710    // Original dimensions (accounting for pixel expansion)
711    let width = expanded_width / 2;
712    let height = expanded_height / 2;
713
714    let mut result = ImageBuffer::new(width, height);
715
716    // Extract share images
717    let share_a = if let DynamicImage::ImageLuma8(img) = &shares[0].image {
718        img
719    } else {
720        return Err(VCError::DecryptionError(
721            "Share A is not grayscale".to_string(),
722        ));
723    };
724
725    let share_b = if let DynamicImage::ImageLuma8(img) = &shares[1].image {
726        img
727    } else {
728        return Err(VCError::DecryptionError(
729            "Share B is not grayscale".to_string(),
730        ));
731    };
732
733    // Process each original pixel (reconstructing from 2x2 blocks)
734    for y in 0..height {
735        for x in 0..width {
736            let base_x = x * 2;
737            let base_y = y * 2;
738
739            // Get the 2x2 block values from both shares
740            let share_a_block = [
741                share_a.get_pixel(base_x, base_y)[0],
742                share_a.get_pixel(base_x + 1, base_y)[0],
743                share_a.get_pixel(base_x, base_y + 1)[0],
744                share_a.get_pixel(base_x + 1, base_y + 1)[0],
745            ];
746
747            let share_b_block = [
748                share_b.get_pixel(base_x, base_y)[0],
749                share_b.get_pixel(base_x + 1, base_y)[0],
750                share_b.get_pixel(base_x, base_y + 1)[0],
751                share_b.get_pixel(base_x + 1, base_y + 1)[0],
752            ];
753
754            // Reconstruct pixel value using AND operation
755            let mut reconstructed_value = 0u8;
756
757            for bit_pos in 0..8 {
758                let mut reconstructed_bits = [0u8; 4];
759
760                // Apply AND operation for each sub-pixel in the 2x2 block
761                for i in 0..4 {
762                    let bit_a = (share_a_block[i] >> bit_pos) & 1;
763                    let bit_b = (share_b_block[i] >> bit_pos) & 1;
764                    reconstructed_bits[i] = bit_a & bit_b;
765                }
766
767                // Average the 4 sub-pixels to get the final bit
768                // (this follows the HVS perception model from the paper)
769                let sum = reconstructed_bits.iter().map(|&x| x as u32).sum::<u32>();
770                let average_bit = if sum >= 2 { 1 } else { 0 }; // majority voting for sub-pixels
771
772                reconstructed_value |= (average_bit as u8) << bit_pos;
773            }
774
775            result.put_pixel(x, y, Luma([reconstructed_value]));
776        }
777    }
778
779    Ok(DynamicImage::ImageLuma8(result))
780}
781
782/// Dhiman-Kasana EVCT(3,3) color scheme
783fn dhiman_kasana_encrypt(
784    image: &DynamicImage,
785    config: &VCConfig,
786    cover_images: Option<Vec<DynamicImage>>,
787) -> Result<Vec<Share>> {
788    if config.num_shares != 3 {
789        return Err(VCError::InvalidConfiguration(
790            "Dhiman-Kasana EVCT(3,3) scheme requires exactly 3 shares".to_string(),
791        ));
792    }
793
794    let rgb = image.to_rgb8();
795    let (width, height) = (rgb.width(), rgb.height());
796
797    // Specific bit position coordinates for each RGB channel
798    let components = [
799        // R channel positions
800        [
801            (4, 4),
802            (4, 2),
803            (3, 1),
804            (2, 3),
805            (2, 0),
806            (1, 4),
807            (1, 2),
808            (0, 1),
809        ],
810        // G channel positions
811        [
812            (4, 3),
813            (3, 4),
814            (3, 2),
815            (2, 1),
816            (1, 3),
817            (1, 0),
818            (0, 4),
819            (0, 2),
820        ],
821        // B channel positions
822        [
823            (4, 1),
824            (3, 3),
825            (3, 0),
826            (2, 4),
827            (2, 2),
828            (1, 1),
829            (0, 3),
830            (0, 0),
831        ],
832    ];
833
834    // 5x5 pixel expansion
835    let share_width = width * 5;
836    let share_height = height * 5;
837
838    let mut shares = Vec::new();
839    for _ in 0..3 {
840        shares.push(ImageBuffer::new(share_width, share_height));
841    }
842
843    // Check if cover images are provided before moving them
844    let has_cover_images = cover_images.is_some();
845
846    // Use cover images if provided, otherwise use default cover colors
847    let covers = if let Some(covers) = cover_images {
848        if covers.len() != 3 {
849            return Err(VCError::CoverImageError(
850                "Dhiman-Kasana requires exactly 3 cover images".to_string(),
851            ));
852        }
853        covers.into_iter().map(|img| img.to_rgb8()).collect()
854    } else {
855        // Default cover colors (white background)
856        vec![
857            ImageBuffer::from_pixel(width, height, Rgb([255, 255, 255])),
858            ImageBuffer::from_pixel(width, height, Rgb([255, 255, 255])),
859            ImageBuffer::from_pixel(width, height, Rgb([255, 255, 255])),
860        ]
861    };
862
863    // Process each pixel
864    for y in 0..height {
865        for x in 0..width {
866            let secret_pixel = rgb.get_pixel(x, y);
867            let [r, g, b] = secret_pixel.0;
868
869            // Process each share
870            for share_idx in 0..3 {
871                let cover_pixel = covers[share_idx].get_pixel(x, y);
872
873                // Create 5x5 block filled with cover pixel color
874                let mut block = ImageBuffer::from_pixel(5, 5, *cover_pixel);
875
876                // Process each color channel
877                for (channel_idx, &channel_value) in [r, g, b].iter().enumerate() {
878                    let bit_positions = &components[channel_idx];
879
880                    // Process each bit of the channel (8 bits)
881                    for (bit_idx, &(bit_y, bit_x)) in bit_positions.iter().enumerate() {
882                        let bit = (channel_value >> bit_idx) & 1;
883
884                        // Encode bit: 1 = black (0,0,0), 0 = dark grey (30,30,30)
885                        let pixel_color = if bit == 1 {
886                            Rgb([0, 0, 0]) // Black for bit 1
887                        } else {
888                            Rgb([30, 30, 30]) // Dark grey for bit 0
889                        };
890
891                        block.put_pixel(bit_x, bit_y, pixel_color);
892                    }
893                }
894
895                // Paste the 5x5 block into the share
896                let base_x = x * 5;
897                let base_y = y * 5;
898                for block_y in 0..5 {
899                    for block_x in 0..5 {
900                        let pixel = block.get_pixel(block_x, block_y);
901                        shares[share_idx].put_pixel(base_x + block_x, base_y + block_y, *pixel);
902                    }
903                }
904            }
905        }
906    }
907
908    // Convert to Share objects
909    let result: Vec<Share> = shares
910        .into_iter()
911        .enumerate()
912        .map(|(i, img)| {
913            Share::new(
914                DynamicImage::ImageRgb8(img),
915                i + 1,
916                3,
917                width,
918                height,
919                5, // 5x5 pixel expansion
920                has_cover_images,
921            )
922        })
923        .collect();
924
925    Ok(result)
926}
927
928/// Dhiman-Kasana decryption using XOR operation
929fn dhiman_kasana_decrypt(shares: &[Share], _config: &VCConfig) -> Result<DynamicImage> {
930    if shares.len() < 3 {
931        return Err(VCError::InsufficientShares {
932            required: 3,
933            provided: shares.len(),
934        });
935    }
936
937    // Get dimensions from the first share (expanded)
938    let (expanded_width, expanded_height) = shares[0].dimensions();
939
940    // Original dimensions (accounting for 5x5 pixel expansion)
941    let width = expanded_width / 5;
942    let height = expanded_height / 5;
943
944    let mut result = ImageBuffer::new(width, height);
945
946    // Bit position coordinates for each RGB channel
947    let components = [
948        // R channel positions
949        [
950            (4, 4),
951            (4, 2),
952            (3, 1),
953            (2, 3),
954            (2, 0),
955            (1, 4),
956            (1, 2),
957            (0, 1),
958        ],
959        // G channel positions
960        [
961            (4, 3),
962            (3, 4),
963            (3, 2),
964            (2, 1),
965            (1, 3),
966            (1, 0),
967            (0, 4),
968            (0, 2),
969        ],
970        // B channel positions
971        [
972            (4, 1),
973            (3, 3),
974            (3, 0),
975            (2, 4),
976            (2, 2),
977            (1, 1),
978            (0, 3),
979            (0, 0),
980        ],
981    ];
982
983    // Extract RGB images from shares
984    let share_images: Vec<&ImageBuffer<Rgb<u8>, Vec<u8>>> = shares
985        .iter()
986        .map(|share| {
987            if let DynamicImage::ImageRgb8(img) = &share.image {
988                img
989            } else {
990                panic!("Share is not RGB format");
991            }
992        })
993        .collect();
994
995    // Process each original pixel
996    for y in 0..height {
997        for x in 0..width {
998            let mut reconstructed_pixel = [0u8; 3];
999
1000            // Process each color channel
1001            for channel_idx in 0..3 {
1002                let bit_positions = &components[channel_idx];
1003                let mut channel_value = 0u8;
1004
1005                // Extract bits from each position
1006                for (bit_idx, &(bit_y, bit_x)) in bit_positions.iter().enumerate() {
1007                    let base_x = x * 5;
1008                    let base_y = y * 5;
1009
1010                    // Get the pixel from the corresponding share at the bit position
1011                    let pixel = share_images[channel_idx].get_pixel(base_x + bit_x, base_y + bit_y);
1012
1013                    // Decode bit: (0,0,0) = 1, anything else = 0
1014                    let bit = if pixel.0 == [0, 0, 0] { 1 } else { 0 };
1015
1016                    channel_value |= bit << bit_idx;
1017                }
1018
1019                reconstructed_pixel[channel_idx] = channel_value;
1020            }
1021
1022            result.put_pixel(x, y, Rgb(reconstructed_pixel));
1023        }
1024    }
1025
1026    Ok(DynamicImage::ImageRgb8(result))
1027}
1028
1029/// Yamaguchi-Nakajima Extended Visual Cryptography for Natural Images
1030/// Based on Nakajima & Yamaguchi (2002)
1031fn yamaguchi_nakajima_encrypt(
1032    image: &DynamicImage,
1033    config: &VCConfig,
1034    cover_images: Option<Vec<DynamicImage>>,
1035) -> Result<Vec<Share>> {
1036    if config.num_shares != 2 {
1037        return Err(VCError::InvalidConfiguration(
1038            "Yamaguchi-Nakajima scheme requires exactly 2 shares".to_string(),
1039        ));
1040    }
1041
1042    let cover_images = cover_images.ok_or_else(|| {
1043        VCError::CoverImageError(
1044            "Yamaguchi-Nakajima scheme requires 2 cover images (sheet images)".to_string(),
1045        )
1046    })?;
1047
1048    if cover_images.len() != 2 {
1049        return Err(VCError::CoverImageError(
1050            "Yamaguchi-Nakajima scheme requires exactly 2 cover images".to_string(),
1051        ));
1052    }
1053
1054    let target = image.to_luma8();
1055    let sheet1 = cover_images[0].to_luma8();
1056    let sheet2 = cover_images[1].to_luma8();
1057    let (width, height) = (target.width(), target.height());
1058
1059    // Resize all images to same size
1060    let sheet1 = image::imageops::resize(
1061        &sheet1,
1062        width,
1063        height,
1064        image::imageops::FilterType::Lanczos3,
1065    );
1066    let sheet2 = image::imageops::resize(
1067        &sheet2,
1068        width,
1069        height,
1070        image::imageops::FilterType::Lanczos3,
1071    );
1072
1073    // Number of subpixels per pixel (default 16 for 4x4 subpixel structure)
1074    let m = config.block_size.max(4) as usize;
1075    let sub_size = (m as f64).sqrt() as usize;
1076
1077    // Contrast adjustment
1078    let contrast = 0.6;
1079    let l = (1.0 - contrast) / 2.0; // Lower bound
1080    let u = l + contrast; // Upper bound
1081
1082    // Process images with contrast adjustment and halftoning
1083    let sheet1_processed = apply_contrast_and_halftone(&sheet1, contrast, l);
1084    let sheet2_processed = apply_contrast_and_halftone(&sheet2, contrast, l);
1085    let target_processed = apply_halftone_target(&target, contrast);
1086
1087    // Create output sheets with subpixel expansion
1088    let out_width = width * sub_size as u32;
1089    let out_height = height * sub_size as u32;
1090    let mut out_sheet1 = ImageBuffer::new(out_width, out_height);
1091    let mut out_sheet2 = ImageBuffer::new(out_width, out_height);
1092
1093    let mut rng = rand::thread_rng();
1094
1095    // Process each pixel
1096    for y in 0..height {
1097        for x in 0..width {
1098            // Get transparency values (0.0 to 1.0)
1099            let t1 = sheet1_processed.get_pixel(x, y)[0] as f64 / 255.0;
1100            let t2 = sheet2_processed.get_pixel(x, y)[0] as f64 / 255.0;
1101            let tt = target_processed.get_pixel(x, y)[0] as f64 / 255.0;
1102
1103            // Adjust triplet if constraint violated
1104            let (t1, t2, tt) = adjust_triplet(t1, t2, tt);
1105
1106            // Convert to subpixel counts
1107            let s1 = (t1 * m as f64).round() as usize;
1108            let s2 = (t2 * m as f64).round() as usize;
1109            let st = (tt * m as f64).round() as usize;
1110
1111            // Generate valid Boolean matrices
1112            let matrices = generate_boolean_matrices(s1, s2, st, m);
1113
1114            if !matrices.is_empty() {
1115                // Randomly select a matrix
1116                let matrix = &matrices[rng.gen_range(0..matrices.len())];
1117
1118                // Fill subpixels
1119                for i in 0..sub_size {
1120                    for j in 0..sub_size {
1121                        let idx = i * sub_size + j;
1122                        if idx < m {
1123                            let pixel_val1 = if matrix[0][idx] == 1 { 255 } else { 0 };
1124                            let pixel_val2 = if matrix[1][idx] == 1 { 255 } else { 0 };
1125
1126                            out_sheet1.put_pixel(
1127                                x * sub_size as u32 + j as u32,
1128                                y * sub_size as u32 + i as u32,
1129                                Luma([pixel_val1]),
1130                            );
1131                            out_sheet2.put_pixel(
1132                                x * sub_size as u32 + j as u32,
1133                                y * sub_size as u32 + i as u32,
1134                                Luma([pixel_val2]),
1135                            );
1136                        }
1137                    }
1138                }
1139            } else {
1140                // Fallback: fill with random pattern
1141                for i in 0..sub_size {
1142                    for j in 0..sub_size {
1143                        let val1 = if rng.gen_bool(0.5) { 255 } else { 0 };
1144                        let val2 = if rng.gen_bool(0.5) { 255 } else { 0 };
1145
1146                        out_sheet1.put_pixel(
1147                            x * sub_size as u32 + j as u32,
1148                            y * sub_size as u32 + i as u32,
1149                            Luma([val1]),
1150                        );
1151                        out_sheet2.put_pixel(
1152                            x * sub_size as u32 + j as u32,
1153                            y * sub_size as u32 + i as u32,
1154                            Luma([val2]),
1155                        );
1156                    }
1157                }
1158            }
1159        }
1160    }
1161
1162    Ok(vec![
1163        Share::new(
1164            DynamicImage::ImageLuma8(out_sheet1),
1165            1,
1166            2,
1167            width,
1168            height,
1169            sub_size,
1170            true,
1171        ),
1172        Share::new(
1173            DynamicImage::ImageLuma8(out_sheet2),
1174            2,
1175            2,
1176            width,
1177            height,
1178            sub_size,
1179            true,
1180        ),
1181    ])
1182}
1183
1184/// Yamaguchi-Nakajima decryption using AND operation
1185fn yamaguchi_nakajima_decrypt(shares: &[Share], _config: &VCConfig) -> Result<DynamicImage> {
1186    if shares.len() < 2 {
1187        return Err(VCError::InsufficientShares {
1188            required: 2,
1189            provided: shares.len(),
1190        });
1191    }
1192
1193    let (expanded_width, expanded_height) = shares[0].dimensions();
1194
1195    // Extract share images
1196    let sheet1 = if let DynamicImage::ImageLuma8(img) = &shares[0].image {
1197        img
1198    } else {
1199        return Err(VCError::DecryptionError(
1200            "Share 1 is not grayscale".to_string(),
1201        ));
1202    };
1203
1204    let sheet2 = if let DynamicImage::ImageLuma8(img) = &shares[1].image {
1205        img
1206    } else {
1207        return Err(VCError::DecryptionError(
1208            "Share 2 is not grayscale".to_string(),
1209        ));
1210    };
1211
1212    // Create result image with expanded dimensions (no downsampling)
1213    let mut result = ImageBuffer::new(expanded_width, expanded_height);
1214
1215    // Reveal target using Boolean AND operation
1216    for y in 0..expanded_height {
1217        for x in 0..expanded_width {
1218            let pixel1 = sheet1.get_pixel(x, y)[0];
1219            let pixel2 = sheet2.get_pixel(x, y)[0];
1220
1221            // Boolean AND: both must be white (255) for result to be white
1222            let result_pixel = if pixel1 == 255 && pixel2 == 255 {
1223                255
1224            } else {
1225                0
1226            };
1227            result.put_pixel(x, y, Luma([result_pixel]));
1228        }
1229    }
1230
1231    Ok(DynamicImage::ImageLuma8(result))
1232}
1233
1234/// Apply contrast adjustment and Floyd-Steinberg error diffusion halftoning
1235fn apply_contrast_and_halftone(
1236    image: &ImageBuffer<Luma<u8>, Vec<u8>>,
1237    contrast: f64,
1238    l: f64,
1239) -> ImageBuffer<Luma<u8>, Vec<u8>> {
1240    let (width, height) = (image.width(), image.height());
1241    let mut working_image = ImageBuffer::new(width, height);
1242
1243    // Apply contrast adjustment
1244    for y in 0..height {
1245        for x in 0..width {
1246            let pixel = image.get_pixel(x, y)[0] as f64 / 255.0;
1247            let adjusted = l + pixel * contrast;
1248            working_image.put_pixel(x, y, Luma([(adjusted * 255.0) as u8]));
1249        }
1250    }
1251
1252    // Apply Floyd-Steinberg error diffusion
1253    floyd_steinberg_dithering(&working_image)
1254}
1255
1256/// Apply halftoning to target image
1257fn apply_halftone_target(
1258    image: &ImageBuffer<Luma<u8>, Vec<u8>>,
1259    contrast: f64,
1260) -> ImageBuffer<Luma<u8>, Vec<u8>> {
1261    let (width, height) = (image.width(), image.height());
1262    let mut working_image = ImageBuffer::new(width, height);
1263
1264    // Apply contrast adjustment (different for target)
1265    for y in 0..height {
1266        for x in 0..width {
1267            let pixel = image.get_pixel(x, y)[0] as f64 / 255.0;
1268            let adjusted = pixel * contrast;
1269            working_image.put_pixel(x, y, Luma([(adjusted * 255.0) as u8]));
1270        }
1271    }
1272
1273    // Apply Floyd-Steinberg error diffusion
1274    floyd_steinberg_dithering(&working_image)
1275}
1276
1277/// Floyd-Steinberg error diffusion algorithm
1278fn floyd_steinberg_dithering(
1279    image: &ImageBuffer<Luma<u8>, Vec<u8>>,
1280) -> ImageBuffer<Luma<u8>, Vec<u8>> {
1281    let (width, height) = (image.width(), image.height());
1282    let mut working = vec![vec![0.0; width as usize]; height as usize];
1283    let mut result = ImageBuffer::new(width, height);
1284
1285    // Copy to working array
1286    for y in 0..height {
1287        for x in 0..width {
1288            working[y as usize][x as usize] = image.get_pixel(x, y)[0] as f64 / 255.0;
1289        }
1290    }
1291
1292    // Floyd-Steinberg error diffusion
1293    for y in 0..height {
1294        for x in 0..width {
1295            let old_pixel = working[y as usize][x as usize];
1296            let new_pixel = if old_pixel > 0.5 { 1.0 } else { 0.0 };
1297            result.put_pixel(x, y, Luma([(new_pixel * 255.0) as u8]));
1298
1299            let error = old_pixel - new_pixel;
1300
1301            // Distribute error to neighboring pixels
1302            if x + 1 < width {
1303                working[y as usize][(x + 1) as usize] += error * 7.0 / 16.0;
1304            }
1305            if y + 1 < height {
1306                if x > 0 {
1307                    working[(y + 1) as usize][(x - 1) as usize] += error * 3.0 / 16.0;
1308                }
1309                working[(y + 1) as usize][x as usize] += error * 5.0 / 16.0;
1310                if x + 1 < width {
1311                    working[(y + 1) as usize][(x + 1) as usize] += error * 1.0 / 16.0;
1312                }
1313            }
1314        }
1315    }
1316
1317    result
1318}
1319
1320/// Check if a triplet satisfies the encryption constraint
1321fn check_constraint(t1: f64, t2: f64, tt: f64) -> bool {
1322    let min_tt = (0.0_f64).max(t1 + t2 - 1.0);
1323    let max_tt = t1.min(t2);
1324    min_tt <= tt && tt <= max_tt
1325}
1326
1327/// Adjust triplet values if they violate the constraint
1328fn adjust_triplet(t1: f64, t2: f64, tt: f64) -> (f64, f64, f64) {
1329    let min_tt = (0.0_f64).max(t1 + t2 - 1.0);
1330    let max_tt = t1.min(t2);
1331
1332    let tt_adj = if tt < min_tt {
1333        min_tt
1334    } else if tt > max_tt {
1335        max_tt
1336    } else {
1337        tt
1338    };
1339
1340    (t1, t2, tt_adj)
1341}
1342
1343/// Generate all valid Boolean matrices for given transparency values
1344fn generate_boolean_matrices(s1: usize, s2: usize, st: usize, m: usize) -> Vec<Vec<Vec<u8>>> {
1345    // Calculate subpixel pair counts
1346    let p11 = st; // Both transparent
1347    let p10 = s1.saturating_sub(st); // Sheet1 transparent, sheet2 opaque
1348    let p01 = s2.saturating_sub(st); // Sheet1 opaque, sheet2 transparent
1349    let p00 = m.saturating_sub(p11 + p10 + p01); // Both opaque
1350
1351    // Check validity
1352    if p11 + p10 + p01 + p00 != m {
1353        return vec![];
1354    }
1355
1356    // Create base matrix patterns
1357    let mut base_patterns = Vec::new();
1358
1359    // Add transparent-transparent pairs
1360    for _ in 0..p11 {
1361        base_patterns.push([1, 1]);
1362    }
1363    // Add transparent-opaque pairs
1364    for _ in 0..p10 {
1365        base_patterns.push([1, 0]);
1366    }
1367    // Add opaque-transparent pairs
1368    for _ in 0..p01 {
1369        base_patterns.push([0, 1]);
1370    }
1371    // Add opaque-opaque pairs
1372    for _ in 0..p00 {
1373        base_patterns.push([0, 0]);
1374    }
1375
1376    // Generate a single valid matrix (could be extended to generate all permutations)
1377    let mut rng = rand::thread_rng();
1378    base_patterns.shuffle(&mut rng);
1379
1380    let matrix = vec![
1381        base_patterns
1382            .iter()
1383            .map(|pair| pair[0])
1384            .collect::<Vec<u8>>(),
1385        base_patterns
1386            .iter()
1387            .map(|pair| pair[1])
1388            .collect::<Vec<u8>>(),
1389    ];
1390
1391    vec![matrix]
1392}