use crate::utils::RenderError;
use ass_core::Script;
use tiny_skia::{Color, Paint, Pixmap, Stroke, Transform};
#[cfg(all(not(feature = "nostd"), feature = "serde"))]
use std::{fs, path::Path};
mod types;
mod util;
pub use types::{
BoundingBoxDebug, ComparisonResult, FontMetricsDebug, PixelDifference, RenderDebugInfo,
};
pub use util::create_comparison_image;
pub struct VisualComparison {
width: u32,
height: u32,
debug_enabled: bool,
debug_info: Vec<RenderDebugInfo>,
}
impl VisualComparison {
pub fn new(width: u32, height: u32) -> Self {
Self {
width,
height,
debug_enabled: true,
debug_info: Vec::new(),
}
}
pub fn set_debug(&mut self, enabled: bool) {
self.debug_enabled = enabled;
}
pub fn render_with_debug(
&mut self,
script: &Script,
_time_ms: u32,
) -> Result<Pixmap, RenderError> {
self.debug_info.clear();
use ass_core::parser::ast::SectionType;
let play_res_x = if let Some(ass_core::parser::ast::Section::ScriptInfo(info)) =
script.find_section(SectionType::ScriptInfo)
{
info.get_field("PlayResX")
.and_then(|v| v.parse::<u32>().ok())
.unwrap_or(384)
} else {
384
};
let play_res_y = if let Some(ass_core::parser::ast::Section::ScriptInfo(info)) =
script.find_section(SectionType::ScriptInfo)
{
info.get_field("PlayResY")
.and_then(|v| v.parse::<u32>().ok())
.unwrap_or(288)
} else {
288
};
let mut pixmap = Pixmap::new(self.width, self.height).ok_or(RenderError::InvalidPixmap)?;
if self.debug_enabled {
self.draw_debug_overlay(&mut pixmap, &self.debug_info)?;
self.draw_alignment_grid(&mut pixmap, play_res_x, play_res_y)?;
self.draw_color_reference(&mut pixmap)?;
}
Ok(pixmap)
}
fn draw_debug_overlay(
&self,
pixmap: &mut Pixmap,
debug_info: &[RenderDebugInfo],
) -> Result<(), RenderError> {
let mut paint = Paint::default();
paint.set_color(Color::from_rgba8(255, 255, 0, 180));
let mut y_offset = 20.0;
for (i, info) in debug_info.iter().enumerate() {
let text = format!(
"Event {}: Font: {:.1}pt (scaled: {:.1}pt), Color: {} -> RGBA({},{},{},{})",
i,
info.calculated_font_size,
info.scaled_font_size,
info.color_bbggrr,
info.color_rgba[0],
info.color_rgba[1],
info.color_rgba[2],
info.color_rgba[3],
);
let mut bg_paint = Paint::default();
bg_paint.set_color(Color::from_rgba8(0, 0, 0, 180));
pixmap.fill_rect(
tiny_skia::Rect::from_xywh(5.0, y_offset - 15.0, text.len() as f32 * 7.0, 20.0)
.unwrap(),
&bg_paint,
Transform::identity(),
None,
);
y_offset += 25.0;
}
Ok(())
}
fn draw_alignment_grid(
&self,
pixmap: &mut Pixmap,
_play_res_x: u32,
_play_res_y: u32,
) -> Result<(), RenderError> {
let mut paint = Paint::default();
paint.set_color(Color::from_rgba8(100, 100, 100, 50));
let width = pixmap.width() as f32;
let height = pixmap.height() as f32;
let h_third = width / 3.0;
let v_third = height / 3.0;
let stroke = Stroke {
width: 1.0,
..Default::default()
};
for i in 1..3 {
let x = h_third * i as f32;
if let Some(rect) = tiny_skia::Rect::from_xywh(x, 0.0, 1.0, height) {
let path = tiny_skia::PathBuilder::from_rect(rect);
pixmap.stroke_path(&path, &paint, &stroke, Transform::identity(), None);
}
}
for i in 1..3 {
let y = v_third * i as f32;
if let Some(rect) = tiny_skia::Rect::from_xywh(0.0, y, width, 1.0) {
let path = tiny_skia::PathBuilder::from_rect(rect);
pixmap.stroke_path(&path, &paint, &stroke, Transform::identity(), None);
}
}
Ok(())
}
fn draw_color_reference(&self, pixmap: &mut Pixmap) -> Result<(), RenderError> {
let colors = [
("White", [255, 255, 255, 255]),
("Cyan", [255, 255, 0, 255]), ("Yellow", [0, 255, 255, 255]), ("Red", [0, 0, 255, 255]), ("Blue", [255, 0, 0, 255]), ];
let mut x_offset = pixmap.width() as f32 - 250.0;
let y_offset = 10.0;
for (_name, rgba) in colors.iter() {
let mut paint = Paint::default();
paint.set_color(Color::from_rgba8(rgba[0], rgba[1], rgba[2], rgba[3]));
pixmap.fill_rect(
tiny_skia::Rect::from_xywh(x_offset, y_offset, 40.0, 20.0).unwrap(),
&paint,
Transform::identity(),
None,
);
x_offset += 45.0;
}
Ok(())
}
#[cfg(all(not(feature = "nostd"), feature = "serde"))]
pub fn export_debug_info(&self, path: &Path) -> Result<(), RenderError> {
let json = serde_json::to_string_pretty(&self.debug_info)
.map_err(|e| RenderError::BackendError(format!("Serialization failed: {e}")))?;
fs::write(path, json).map_err(|e| RenderError::IOError(e.to_string()))?;
Ok(())
}
pub fn compare_with_libass(
&self,
our_output: &Pixmap,
libass_output: &Pixmap,
) -> ComparisonResult {
let mut differences = Vec::new();
let mut total_diff = 0.0;
let mut max_diff = 0.0;
for y in 0..our_output.height().min(libass_output.height()) {
for x in 0..our_output.width().min(libass_output.width()) {
let our_pixel = our_output.pixel(x, y).unwrap();
let lib_pixel = libass_output.pixel(x, y).unwrap();
let r_diff = (our_pixel.red() as i32 - lib_pixel.red() as i32).abs() as f32;
let g_diff = (our_pixel.green() as i32 - lib_pixel.green() as i32).abs() as f32;
let b_diff = (our_pixel.blue() as i32 - lib_pixel.blue() as i32).abs() as f32;
let a_diff = (our_pixel.alpha() as i32 - lib_pixel.alpha() as i32).abs() as f32;
let pixel_diff = (r_diff + g_diff + b_diff + a_diff) / 4.0;
if pixel_diff > 10.0 {
differences.push(PixelDifference {
x,
y,
our_color: [
our_pixel.red(),
our_pixel.green(),
our_pixel.blue(),
our_pixel.alpha(),
],
libass_color: [
lib_pixel.red(),
lib_pixel.green(),
lib_pixel.blue(),
lib_pixel.alpha(),
],
difference: pixel_diff,
});
}
total_diff += pixel_diff;
max_diff = if pixel_diff > max_diff {
pixel_diff
} else {
max_diff
};
}
}
let pixel_count = (our_output.width() * our_output.height()) as f32;
ComparisonResult {
average_difference: total_diff / pixel_count,
max_difference: max_diff,
different_pixels: differences.len(),
total_pixels: pixel_count as usize,
pixel_differences: differences,
}
}
}