ass_renderer/debug/visual_comparison/
mod.rs1use 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
18pub 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 pub fn set_debug(&mut self, enabled: bool) {
38 self.debug_enabled = enabled;
39 }
40
41 pub fn render_with_debug(
43 &mut self,
44 script: &Script,
45 _time_ms: u32,
46 ) -> Result<Pixmap, RenderError> {
47 self.debug_info.clear();
49
50 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 let mut pixmap = Pixmap::new(self.width, self.height).ok_or(RenderError::InvalidPixmap)?;
73
74 if self.debug_enabled {
75 self.draw_debug_overlay(&mut pixmap, &self.debug_info)?;
77
78 self.draw_alignment_grid(&mut pixmap, play_res_x, play_res_y)?;
80
81 self.draw_color_reference(&mut pixmap)?;
83 }
84
85 Ok(pixmap)
86 }
87
88 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)); 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 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 y_offset += 25.0;
126 }
127
128 Ok(())
129 }
130
131 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)); let width = pixmap.width() as f32;
142 let height = pixmap.height() as f32;
143
144 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 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 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 fn draw_color_reference(&self, pixmap: &mut Pixmap) -> Result<(), RenderError> {
176 let colors = [
178 ("White", [255, 255, 255, 255]),
179 ("Cyan", [255, 255, 0, 255]), ("Yellow", [0, 255, 255, 255]), ("Red", [0, 0, 255, 255]), ("Blue", [255, 0, 0, 255]), ];
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 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 #[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 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 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}