Skip to main content

ass_renderer/debug/visual_comparison/
mod.rs

1//! Visual comparison and debugging tools for ASS rendering
2
3use crate::utils::RenderError;
4use ass_core::Script;
5use tiny_skia::{Color, Paint, Pixmap, Stroke, Transform};
6
7#[cfg(all(not(feature = "nostd"), feature = "serde"))]
8use std::{fs, path::Path};
9
10mod types;
11mod util;
12
13pub use types::{
14    BoundingBoxDebug, ComparisonResult, FontMetricsDebug, PixelDifference, RenderDebugInfo,
15};
16pub use util::create_comparison_image;
17
18/// Visual comparison renderer
19pub struct VisualComparison {
20    width: u32,
21    height: u32,
22    debug_enabled: bool,
23    debug_info: Vec<RenderDebugInfo>,
24}
25
26impl VisualComparison {
27    pub fn new(width: u32, height: u32) -> Self {
28        Self {
29            width,
30            height,
31            debug_enabled: true,
32            debug_info: Vec::new(),
33        }
34    }
35
36    /// Enable/disable debug mode
37    pub fn set_debug(&mut self, enabled: bool) {
38        self.debug_enabled = enabled;
39    }
40
41    /// Render with debug overlay
42    pub fn render_with_debug(
43        &mut self,
44        script: &Script,
45        _time_ms: u32,
46    ) -> Result<Pixmap, RenderError> {
47        // Clear debug info
48        self.debug_info.clear();
49
50        // Get script info
51        use ass_core::parser::ast::SectionType;
52        let play_res_x = if let Some(ass_core::parser::ast::Section::ScriptInfo(info)) =
53            script.find_section(SectionType::ScriptInfo)
54        {
55            info.get_field("PlayResX")
56                .and_then(|v| v.parse::<u32>().ok())
57                .unwrap_or(384)
58        } else {
59            384
60        };
61        let play_res_y = if let Some(ass_core::parser::ast::Section::ScriptInfo(info)) =
62            script.find_section(SectionType::ScriptInfo)
63        {
64            info.get_field("PlayResY")
65                .and_then(|v| v.parse::<u32>().ok())
66                .unwrap_or(288)
67        } else {
68            288
69        };
70
71        // Create a basic pixmap for now - full rendering would require more setup
72        let mut pixmap = Pixmap::new(self.width, self.height).ok_or(RenderError::InvalidPixmap)?;
73
74        if self.debug_enabled {
75            // Add debug overlay
76            self.draw_debug_overlay(&mut pixmap, &self.debug_info)?;
77
78            // Add grid for alignment reference
79            self.draw_alignment_grid(&mut pixmap, play_res_x, play_res_y)?;
80
81            // Add color reference
82            self.draw_color_reference(&mut pixmap)?;
83        }
84
85        Ok(pixmap)
86    }
87
88    /// Draw debug overlay with rendering info
89    fn draw_debug_overlay(
90        &self,
91        pixmap: &mut Pixmap,
92        debug_info: &[RenderDebugInfo],
93    ) -> Result<(), RenderError> {
94        let mut paint = Paint::default();
95        paint.set_color(Color::from_rgba8(255, 255, 0, 180)); // Yellow for debug text
96
97        // Draw debug info in top-left corner
98        let mut y_offset = 20.0;
99        for (i, info) in debug_info.iter().enumerate() {
100            let text = format!(
101                "Event {}: Font: {:.1}pt (scaled: {:.1}pt), Color: {} -> RGBA({},{},{},{})",
102                i,
103                info.calculated_font_size,
104                info.scaled_font_size,
105                info.color_bbggrr,
106                info.color_rgba[0],
107                info.color_rgba[1],
108                info.color_rgba[2],
109                info.color_rgba[3],
110            );
111
112            // Draw text background for readability
113            let mut bg_paint = Paint::default();
114            bg_paint.set_color(Color::from_rgba8(0, 0, 0, 180));
115            pixmap.fill_rect(
116                tiny_skia::Rect::from_xywh(5.0, y_offset - 15.0, text.len() as f32 * 7.0, 20.0)
117                    .unwrap(),
118                &bg_paint,
119                Transform::identity(),
120                None,
121            );
122
123            // Would draw actual text here with a proper text renderer
124            // For now, just indicate where it would be
125            y_offset += 25.0;
126        }
127
128        Ok(())
129    }
130
131    /// Draw alignment grid for reference
132    fn draw_alignment_grid(
133        &self,
134        pixmap: &mut Pixmap,
135        _play_res_x: u32,
136        _play_res_y: u32,
137    ) -> Result<(), RenderError> {
138        let mut paint = Paint::default();
139        paint.set_color(Color::from_rgba8(100, 100, 100, 50)); // Semi-transparent gray
140
141        let width = pixmap.width() as f32;
142        let height = pixmap.height() as f32;
143
144        // Draw 3x3 grid for alignment positions
145        let h_third = width / 3.0;
146        let v_third = height / 3.0;
147
148        let stroke = Stroke {
149            width: 1.0,
150            ..Default::default()
151        };
152
153        // Vertical lines
154        for i in 1..3 {
155            let x = h_third * i as f32;
156            if let Some(rect) = tiny_skia::Rect::from_xywh(x, 0.0, 1.0, height) {
157                let path = tiny_skia::PathBuilder::from_rect(rect);
158                pixmap.stroke_path(&path, &paint, &stroke, Transform::identity(), None);
159            }
160        }
161
162        // Horizontal lines
163        for i in 1..3 {
164            let y = v_third * i as f32;
165            if let Some(rect) = tiny_skia::Rect::from_xywh(0.0, y, width, 1.0) {
166                let path = tiny_skia::PathBuilder::from_rect(rect);
167                pixmap.stroke_path(&path, &paint, &stroke, Transform::identity(), None);
168            }
169        }
170
171        Ok(())
172    }
173
174    /// Draw color reference swatches
175    fn draw_color_reference(&self, pixmap: &mut Pixmap) -> Result<(), RenderError> {
176        // Draw common ASS colors for reference
177        let colors = [
178            ("White", [255, 255, 255, 255]),
179            ("Cyan", [255, 255, 0, 255]),   // In BBGGRR: &H00FFFF&
180            ("Yellow", [0, 255, 255, 255]), // In BBGGRR: &H00FFFF&
181            ("Red", [0, 0, 255, 255]),      // In BBGGRR: &H0000FF&
182            ("Blue", [255, 0, 0, 255]),     // In BBGGRR: &HFF0000&
183        ];
184
185        let mut x_offset = pixmap.width() as f32 - 250.0;
186        let y_offset = 10.0;
187
188        for (_name, rgba) in colors.iter() {
189            let mut paint = Paint::default();
190            paint.set_color(Color::from_rgba8(rgba[0], rgba[1], rgba[2], rgba[3]));
191
192            // Draw color swatch
193            pixmap.fill_rect(
194                tiny_skia::Rect::from_xywh(x_offset, y_offset, 40.0, 20.0).unwrap(),
195                &paint,
196                Transform::identity(),
197                None,
198            );
199
200            x_offset += 45.0;
201        }
202
203        Ok(())
204    }
205
206    /// Export debug info to file
207    #[cfg(all(not(feature = "nostd"), feature = "serde"))]
208    pub fn export_debug_info(&self, path: &Path) -> Result<(), RenderError> {
209        let json = serde_json::to_string_pretty(&self.debug_info)
210            .map_err(|e| RenderError::BackendError(format!("Serialization failed: {e}")))?;
211        fs::write(path, json).map_err(|e| RenderError::IOError(e.to_string()))?;
212        Ok(())
213    }
214
215    /// Compare with libass output
216    pub fn compare_with_libass(
217        &self,
218        our_output: &Pixmap,
219        libass_output: &Pixmap,
220    ) -> ComparisonResult {
221        let mut differences = Vec::new();
222        let mut total_diff = 0.0;
223        let mut max_diff = 0.0;
224
225        // Compare pixel by pixel
226        for y in 0..our_output.height().min(libass_output.height()) {
227            for x in 0..our_output.width().min(libass_output.width()) {
228                let our_pixel = our_output.pixel(x, y).unwrap();
229                let lib_pixel = libass_output.pixel(x, y).unwrap();
230
231                let r_diff = (our_pixel.red() as i32 - lib_pixel.red() as i32).abs() as f32;
232                let g_diff = (our_pixel.green() as i32 - lib_pixel.green() as i32).abs() as f32;
233                let b_diff = (our_pixel.blue() as i32 - lib_pixel.blue() as i32).abs() as f32;
234                let a_diff = (our_pixel.alpha() as i32 - lib_pixel.alpha() as i32).abs() as f32;
235
236                let pixel_diff = (r_diff + g_diff + b_diff + a_diff) / 4.0;
237
238                if pixel_diff > 10.0 {
239                    differences.push(PixelDifference {
240                        x,
241                        y,
242                        our_color: [
243                            our_pixel.red(),
244                            our_pixel.green(),
245                            our_pixel.blue(),
246                            our_pixel.alpha(),
247                        ],
248                        libass_color: [
249                            lib_pixel.red(),
250                            lib_pixel.green(),
251                            lib_pixel.blue(),
252                            lib_pixel.alpha(),
253                        ],
254                        difference: pixel_diff,
255                    });
256                }
257
258                total_diff += pixel_diff;
259                max_diff = if pixel_diff > max_diff {
260                    pixel_diff
261                } else {
262                    max_diff
263                };
264            }
265        }
266
267        let pixel_count = (our_output.width() * our_output.height()) as f32;
268
269        ComparisonResult {
270            average_difference: total_diff / pixel_count,
271            max_difference: max_diff,
272            different_pixels: differences.len(),
273            total_pixels: pixel_count as usize,
274            pixel_differences: differences,
275        }
276    }
277}