konnektoren_core/certificates/
certificate_image.rs

1use super::CertificateData;
2use crate::certificates::error::{CertificateError, Result};
3use ab_glyph::{Font, FontRef, PxScale, ScaleFont};
4use base64::Engine as _;
5use base64::engine::general_purpose;
6use image::{DynamicImage, ImageBuffer, ImageFormat, ImageReader, Luma, Rgba, RgbaImage, imageops};
7use imageproc::drawing::draw_text_mut;
8use imageproc::rect::Rect;
9use lazy_static::lazy_static;
10use plot_icon::generate_png;
11use qrcode::{EcLevel, QrCode, types::QrError as QrCodeError};
12use std::io::Cursor;
13
14// Define color constants
15const BORDER_COLOR: Rgba<u8> = Rgba([0, 82, 165, 255]); // Deep Blue
16const BACKGROUND_COLOR: Rgba<u8> = Rgba([255, 253, 245, 255]); // Soft Cream
17const TEXT_COLOR: Rgba<u8> = Rgba([51, 51, 51, 255]); // Dark Gray
18const HIGHLIGHT_COLOR: Rgba<u8> = Rgba([0, 121, 193, 255]); // Bright Blue
19
20// Define font constants
21const TITLE_FONT_SIZE: f32 = 48.0;
22const BODY_FONT_SIZE: f32 = 24.0;
23const SMALL_FONT_SIZE: f32 = 18.0;
24
25lazy_static! {
26    static ref TITLE_FONT: FontRef<'static> =
27        FontRef::try_from_slice(include_bytes!("../../assets/Montserrat-Bold.ttf"))
28            .expect("Failed to load title font");
29    static ref BODY_FONT: FontRef<'static> =
30        FontRef::try_from_slice(include_bytes!("../../assets/Lora-Regular.ttf"))
31            .expect("Failed to load body font");
32}
33
34pub fn create_certificate(
35    certificate_data: &CertificateData,
36    url: &str,
37    issuer: &str,
38) -> Result<DynamicImage> {
39    let cert_width = 1200;
40    let cert_height = 900;
41    let border_thickness = 30u32;
42    let qr_code_size = 250;
43
44    // Create QR code
45    let qr_code =
46        QrCode::with_error_correction_level(url, EcLevel::H).map_err(|e: QrCodeError| {
47            CertificateError::ImageProcessingError(format!("Failed to create QR code: {}", e))
48        })?;
49
50    let qr_code_image = qr_code.render::<Luma<u8>>().quiet_zone(false).build();
51    let qr_code_image_rgba =
52        ImageBuffer::from_fn(qr_code_image.width(), qr_code_image.height(), |x, y| {
53            let pixel = qr_code_image.get_pixel(x, y);
54            if pixel[0] == 0 {
55                Rgba([0, 0, 0, 255])
56            } else {
57                Rgba([255, 255, 255, 255])
58            }
59        });
60    let resized_qr_code_image = imageops::resize(
61        &qr_code_image_rgba,
62        qr_code_size,
63        qr_code_size,
64        imageops::FilterType::Nearest,
65    );
66
67    // Create base image
68    let mut cert_image = RgbaImage::new(cert_width, cert_height);
69
70    // Draw border and background
71    imageproc::drawing::draw_filled_rect_mut(
72        &mut cert_image,
73        Rect::at(0, 0).of_size(cert_width, cert_height),
74        BORDER_COLOR,
75    );
76    imageproc::drawing::draw_filled_rect_mut(
77        &mut cert_image,
78        Rect::at(border_thickness as i32, border_thickness as i32).of_size(
79            cert_width - 2 * border_thickness,
80            cert_height - 2 * border_thickness,
81        ),
82        BACKGROUND_COLOR,
83    );
84
85    // Load and draw logo
86    let logo_bytes = include_bytes!("../../assets/favicon.png");
87    let logo_image = ImageReader::new(Cursor::new(logo_bytes))
88        .with_guessed_format()
89        .map_err(|e| {
90            CertificateError::ImageProcessingError(format!(
91                "Failed to guess logo image format: {}",
92                e
93            ))
94        })?
95        .decode()
96        .map_err(|e| {
97            CertificateError::ImageProcessingError(format!("Failed to decode logo image: {}", e))
98        })?;
99
100    let scaled_logo_image = imageops::resize(&logo_image, 120, 120, imageops::FilterType::Lanczos3);
101    image::imageops::overlay(&mut cert_image, &scaled_logo_image, 50, 50);
102
103    // Generate and draw identicon
104    let identicon_image = {
105        let data: Vec<u8> =
106            generate_png(certificate_data.to_base64().as_bytes(), 90).map_err(|e| {
107                CertificateError::ImageProcessingError(format!(
108                    "Failed to generate identicon: {}",
109                    e
110                ))
111            })?;
112
113        let mut image = ImageReader::new(Cursor::new(data));
114        image.set_format(image::ImageFormat::Png);
115
116        image.decode().map_err(|e| {
117            CertificateError::ImageProcessingError(format!("Failed to decode identicon: {}", e))
118        })?
119    };
120
121    image::imageops::overlay(
122        &mut cert_image,
123        &identicon_image,
124        cert_width as i64 - 140,
125        50,
126    );
127
128    // Draw title
129    let title = "Certificate of Achievement";
130    draw_text_centered(
131        &mut cert_image,
132        &TITLE_FONT,
133        TITLE_FONT_SIZE,
134        title,
135        120,
136        HIGHLIGHT_COLOR,
137    );
138
139    // Draw achievement message
140    let achievement = format!(
141        "Successfully completed the {} path",
142        certificate_data.game_path_name
143    );
144    draw_text_centered(
145        &mut cert_image,
146        &BODY_FONT,
147        BODY_FONT_SIZE,
148        &achievement,
149        300,
150        TEXT_COLOR,
151    );
152
153    // Draw performance summary
154    let performance = format!(
155        "Completed {} out of {} challenges with {}% performance",
156        certificate_data.solved_challenges,
157        certificate_data.total_challenges,
158        certificate_data.performance_percentage
159    );
160    draw_text_centered(
161        &mut cert_image,
162        &BODY_FONT,
163        BODY_FONT_SIZE,
164        &performance,
165        350,
166        TEXT_COLOR,
167    );
168
169    // Draw date
170    let date_str = format!("Issued on {}", certificate_data.date.format("%d %B %Y"));
171    draw_text_centered(
172        &mut cert_image,
173        &BODY_FONT,
174        SMALL_FONT_SIZE,
175        &date_str,
176        460,
177        TEXT_COLOR,
178    );
179
180    // Draw issuer
181    let issued_by_message = format!("Issued by {}", issuer);
182    draw_text_centered(
183        &mut cert_image,
184        &BODY_FONT,
185        SMALL_FONT_SIZE,
186        &issued_by_message,
187        510,
188        HIGHLIGHT_COLOR,
189    );
190
191    // Draw QR code
192    let qr_code_size = resized_qr_code_image.width() as u32;
193    imageops::overlay(
194        &mut cert_image,
195        &resized_qr_code_image,
196        (cert_width - qr_code_size) as i64 / 2,
197        (cert_height - qr_code_size - 80) as i64, // Moved up slightly
198    );
199
200    Ok(DynamicImage::ImageRgba8(cert_image))
201}
202
203pub fn calculate_text_width(font: &FontRef, scale: PxScale, text: &str) -> u32 {
204    let scaled_font = font.as_scaled(scale);
205    text.chars().fold(0.0, |acc, c| {
206        acc + scaled_font.h_advance(scaled_font.scaled_glyph(c).id)
207    }) as u32
208}
209
210pub fn create_certificate_data_url(
211    certificate_data: &CertificateData,
212    url: &str,
213    issuer: &str,
214) -> Result<String> {
215    let image = create_certificate(certificate_data, url, issuer)?;
216
217    let mut image_data: Vec<u8> = Vec::new();
218    let mut cursor = Cursor::new(&mut image_data);
219
220    image.write_to(&mut cursor, ImageFormat::Png).map_err(|e| {
221        CertificateError::ImageProcessingError(format!("Failed to write image to buffer: {}", e))
222    })?;
223
224    let res_base64 = general_purpose::STANDARD.encode(image_data);
225    Ok(format!("data:image/png;base64,{}", res_base64))
226}
227
228pub fn draw_text_centered(
229    image: &mut RgbaImage,
230    font: &FontRef,
231    font_size: f32,
232    text: &str,
233    y: u32,
234    color: Rgba<u8>,
235) {
236    let scale = PxScale::from(font_size);
237    let text_width = calculate_text_width(font, scale, text);
238    let x = (image.width() as i32 - text_width as i32) / 2;
239    draw_text_mut(image, color, x, y as i32, scale, font, text);
240}
241
242#[cfg(test)]
243mod tests {
244    use super::*;
245    use chrono::Utc;
246
247    #[test]
248    fn test_create_certificate() {
249        let certificate_data = CertificateData::new(
250            "Test Game Path".to_string(),
251            10,
252            5,
253            "Test Player".to_string(),
254            Utc::now(),
255        );
256        let url = "https://example.com".to_string();
257        let issuer = "Test Issuer";
258
259        let image = create_certificate(&certificate_data, &url, issuer).unwrap();
260
261        assert!(image.width() > 100);
262        assert!(image.height() > 100);
263    }
264
265    #[test]
266    fn test_create_certificate_data_url() {
267        let certificate_data = CertificateData::new(
268            "Test Game Path".to_string(),
269            10,
270            5,
271            "Test Player".to_string(),
272            Utc::now(),
273        );
274        let url = "https://example.com";
275        let issuer = "Test Issuer";
276
277        let data_url = create_certificate_data_url(&certificate_data, url, issuer).unwrap();
278
279        assert!(data_url.starts_with("data:image/png;base64,"));
280    }
281
282    #[test]
283    fn test_error_handling_invalid_url() {
284        // Test with an invalid URL that would cause QR code generation to fail
285        let certificate_data = CertificateData::new(
286            "Test Game Path".to_string(),
287            10,
288            5,
289            "Test Player".to_string(),
290            Utc::now(),
291        );
292
293        // Create an extremely long URL that exceeds QR code capacity for EcLevel::H
294        // The maximum capacity for QR code with error correction level H is around 1273 characters
295        // for version 40, so we'll use something longer than that
296        let url = "a".repeat(2000); // This should be too long for a QR code
297        let issuer = "Test Issuer";
298
299        let result = create_certificate(&certificate_data, &url, issuer);
300        assert!(result.is_err());
301
302        // Verify the error type
303        match result {
304            Err(CertificateError::ImageProcessingError(_)) => {} // Expected error
305            Err(e) => panic!("Unexpected error type: {:?}", e),
306            Ok(_) => panic!("Expected an error but got success"),
307        }
308    }
309}