diff options
Diffstat (limited to 'src/renderer.rs')
| -rw-r--r-- | src/renderer.rs | 165 |
1 files changed, 98 insertions, 67 deletions
diff --git a/src/renderer.rs b/src/renderer.rs index a673bac..69c3f70 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -32,16 +32,26 @@ use crate::utils::percent_to_pixel; /// # Returns /// /// A vector of bytes containing the PNG image data +/// +/// # Errors +/// +/// Returns `RenderError` if the surface creation, rendering, or PNG encoding fails pub fn render_to_png( map: &Map, stage_type: StageType, configuration: &Configuration, ) -> Result<Vec<u8>, RenderError> { - let total_width = - (configuration.theme.sizes.map_width + configuration.theme.sizes.padding * 2.0) as i32; + // Safe cast: map dimensions are typically in the range of hundreds to thousands of pixels + // Cairo's i32 surface dimensions can handle up to 2^31-1, so truncation is not a concern for typical maps + #[allow(clippy::cast_possible_truncation)] + let total_width = (configuration.theme.sizes.map_width + + configuration.theme.sizes.padding * 2.0) + .round() as i32; + #[allow(clippy::cast_possible_truncation)] let total_height = (configuration.theme.sizes.map_height + configuration.theme.sizes.padding - + configuration.theme.sizes.bottom_padding) as i32; + + configuration.theme.sizes.bottom_padding) + .round() as i32; let surface = ImageSurface::create(Format::ARgb32, total_width, total_height)?; let context = Context::new(&surface)?; @@ -63,17 +73,27 @@ pub fn render_to_png( /// /// # Returns /// -/// A string containing the SVG data +/// An `ImageSurface` containing the rendered map +/// +/// # Errors +/// +/// Returns `RenderError` if the surface creation or rendering fails pub fn render_to_surface( map: &Map, stage_type: StageType, configuration: &Configuration, ) -> Result<ImageSurface, RenderError> { - let total_width = - (configuration.theme.sizes.map_width + configuration.theme.sizes.padding * 2.0) as i32; + // Safe cast: map dimensions are typically in the range of hundreds to thousands of pixels + // Cairo's i32 surface dimensions can handle up to 2^31-1, so truncation is not a concern for typical maps + #[allow(clippy::cast_possible_truncation)] + let total_width = (configuration.theme.sizes.map_width + + configuration.theme.sizes.padding * 2.0) + .round() as i32; + #[allow(clippy::cast_possible_truncation)] let total_height = (configuration.theme.sizes.map_height + configuration.theme.sizes.padding - + configuration.theme.sizes.bottom_padding) as i32; + + configuration.theme.sizes.bottom_padding) + .round() as i32; let surface = ImageSurface::create(Format::ARgb32, total_width, total_height)?; let context = Context::new(&surface)?; @@ -94,6 +114,10 @@ pub fn render_to_surface( /// # Returns /// /// A string containing the SVG data +/// +/// # Errors +/// +/// Returns `RenderError` if the surface creation, rendering, or file I/O fails pub fn render_to_svg( map: &Map, stage_type: StageType, @@ -139,7 +163,7 @@ fn render( // Build component map once and reuse it (pre-allocate capacity for better performance) // Use FxHashMap for faster hashing (we don't need cryptographic security) let mut component_map: FxHashMap<String, (f64, f64)> = - FxHashMap::with_capacity_and_hasher(map.components.len(), Default::default()); + FxHashMap::with_capacity_and_hasher(map.components.len(), rustc_hash::FxBuildHasher); for component in &map.components { let x = percent_to_pixel(component.coordinates.0, configuration.theme.sizes.map_width); let y = percent_to_pixel( @@ -260,8 +284,8 @@ fn draw_axes( let fonts = &configuration.theme.fonts; context.save()?; - let (r, g, b) = configuration.theme.colors.axis; - context.set_source_rgb(r, g, b); + let (red, green, blue) = configuration.theme.colors.axis; + context.set_source_rgb(red, green, blue); context.set_line_width(sizes.line_width * 2.0); // Y-axis and X-axis @@ -271,8 +295,8 @@ fn draw_axes( context.stroke()?; // Text setup - let (r, g, b) = configuration.theme.colors.label; - context.set_source_rgb(r, g, b); + let (red, green, blue) = configuration.theme.colors.label; + context.set_source_rgb(red, green, blue); context.select_font_face( &configuration.theme.fonts.face, FontSlant::Normal, @@ -323,7 +347,7 @@ fn draw_axes( // Draw each stage label with wrapping for (i, &label) in stage_labels.iter().enumerate() { - let x = match i { + let stage_x = match i { 0 => 0.0, 1 => stages[0], 2 => stages[1], @@ -334,8 +358,10 @@ fn draw_axes( let wrapped_lines = wrap_text(context, label, stage_widths[i])?; for (line_index, line) in wrapped_lines.iter().enumerate() { - let y = stage_label_y + line_index as f64 * line_height; - context.move_to(x, y); + // Safe cast: line_index is a small number (typically < 10 for wrapped stage labels) + #[allow(clippy::cast_precision_loss)] + let text_y = stage_label_y + line_index as f64 * line_height; + context.move_to(stage_x, text_y); context.show_text(line)?; } } @@ -383,13 +409,11 @@ fn draw_dependencies( let arrow_angle = PI / 4.0; // 45 degrees for dependency in &map.dependencies { - let from_coords = match component_map.get(&dependency.from.to_lowercase()) { - Some(coords) => coords, - None => continue, + let Some(from_coords) = component_map.get(&dependency.from.to_lowercase()) else { + continue; }; - let to_coords = match component_map.get(&dependency.to.to_lowercase()) { - Some(coords) => coords, - None => continue, + let Some(to_coords) = component_map.get(&dependency.to.to_lowercase()) else { + continue; }; let origin_x = from_coords.0 + half_width; @@ -447,8 +471,11 @@ fn draw_groups( } // Create ONE offscreen surface for ALL groups for opacity handling + // Safe cast: map dimensions plus vertex size are small values (< 10000 typically) + #[allow(clippy::cast_possible_truncation)] let width = (configuration.theme.sizes.map_width + configuration.theme.sizes.vertex_size) as i32; + #[allow(clippy::cast_possible_truncation)] let height = (configuration.theme.sizes.map_height + configuration.theme.sizes.vertex_size) as i32; let offscreen = ImageSurface::create(Format::ARgb32, width, height)?; @@ -482,7 +509,7 @@ fn draw_groups( let segments = 16; let mut circle_coords = Vec::new(); for i in 0..=segments { - let angle = (i as f64 / segments as f64) * 2.0 * PI; + let angle = (f64::from(i) / f64::from(segments)) * 2.0 * PI; circle_coords.push((x + radius * angle.cos(), y + radius * angle.sin())); } circle_coords @@ -541,12 +568,13 @@ fn draw_vertices( configuration: &Configuration, ) -> Result<(), RenderError> { context.save()?; - let (r, g, b) = configuration.theme.colors.vertex; - context.set_source_rgb(r, g, b); + let (red, green, blue) = configuration.theme.colors.vertex; + context.set_source_rgb(red, green, blue); for component in &map.components { - let x = percent_to_pixel(component.coordinates.0, configuration.theme.sizes.map_width); - let y = percent_to_pixel( + let vertex_x = + percent_to_pixel(component.coordinates.0, configuration.theme.sizes.map_width); + let vertex_y = percent_to_pixel( component.coordinates.1, configuration.theme.sizes.map_height, ); @@ -554,8 +582,8 @@ fn draw_vertices( draw_vertex( context, &component.shape, - x, - y, + vertex_x, + vertex_y, configuration.theme.sizes.vertex_size, configuration.theme.sizes.vertex_size, )?; @@ -572,8 +600,8 @@ fn draw_vertex_labels( component_map: &rustc_hash::FxHashMap<String, (f64, f64)>, ) -> Result<(), RenderError> { context.save()?; - let (r, g, b) = configuration.theme.colors.label; - context.set_source_rgb(r, g, b); + let (red, green, blue) = configuration.theme.colors.label; + context.set_source_rgb(red, green, blue); context.select_font_face( &configuration.theme.fonts.face, FontSlant::Normal, @@ -636,14 +664,14 @@ fn draw_vertex_labels( } else { // Use simple positioning for simple maps for component in &map.components { - let x = + let vertex_x = percent_to_pixel(component.coordinates.0, configuration.theme.sizes.map_width); - let y = percent_to_pixel( + let vertex_y = percent_to_pixel( component.coordinates.1, configuration.theme.sizes.map_height, ); - let label_x = x + configuration.theme.sizes.vertex_size + 5.0; - let label_y = y + let label_x = vertex_x + configuration.theme.sizes.vertex_size + 5.0; + let label_y = vertex_y + configuration.theme.sizes.vertex_size / 2.0 + configuration.theme.fonts.vertex_label / 3.0; @@ -654,13 +682,14 @@ fn draw_vertex_labels( } else { // Simple label positioning (to the right of vertex) for component in &map.components { - let x = percent_to_pixel(component.coordinates.0, configuration.theme.sizes.map_width); - let y = percent_to_pixel( + let vertex_x = + percent_to_pixel(component.coordinates.0, configuration.theme.sizes.map_width); + let vertex_y = percent_to_pixel( component.coordinates.1, configuration.theme.sizes.map_height, ); - let label_x = x + configuration.theme.sizes.vertex_size + 5.0; - let label_y = y + let label_x = vertex_x + configuration.theme.sizes.vertex_size + 5.0; + let label_y = vertex_y + configuration.theme.sizes.vertex_size / 2.0 + configuration.theme.fonts.vertex_label / 3.0; @@ -680,8 +709,8 @@ fn draw_inertias( component_map: &rustc_hash::FxHashMap<String, (f64, f64)>, ) -> Result<(), RenderError> { context.save()?; - let (r, g, b) = configuration.theme.colors.inertia; - context.set_source_rgb(r, g, b); + let (red, green, blue) = configuration.theme.colors.inertia; + context.set_source_rgb(red, green, blue); let corner_radius = 2.0; let rect_width = configuration.theme.sizes.vertex_size / 2.0; @@ -690,11 +719,18 @@ fn draw_inertias( for inertia in &map.inertias { if let Some(&(origin_x, origin_y)) = component_map.get(&inertia.component.to_lowercase()) { // Position relative to vertex top-left corner - let x = origin_x + 3.0 * configuration.theme.sizes.vertex_size; - let y = origin_y - (configuration.theme.sizes.vertex_size * 2.0) / 3.0; + let inertia_x = origin_x + 3.0 * configuration.theme.sizes.vertex_size; + let inertia_y = origin_y - (configuration.theme.sizes.vertex_size * 2.0) / 3.0; // Draw rounded rectangle - draw_rounded_rect(context, x, y, rect_width, rect_height, corner_radius)?; + draw_rounded_rect( + context, + inertia_x, + inertia_y, + rect_width, + rect_height, + corner_radius, + ); context.fill()?; } } @@ -703,14 +739,7 @@ fn draw_inertias( Ok(()) } -fn draw_rounded_rect( - context: &Context, - x: f64, - y: f64, - width: f64, - height: f64, - radius: f64, -) -> Result<(), Error> { +fn draw_rounded_rect(context: &Context, x: f64, y: f64, width: f64, height: f64, radius: f64) { let r = radius; context.new_path(); context.arc( @@ -742,7 +771,6 @@ fn draw_rounded_rect( std::f64::consts::PI, ); context.close_path(); - Ok(()) } /// Wraps text to fit within a given width, breaking at word boundaries @@ -755,7 +783,7 @@ fn wrap_text(context: &Context, text: &str, max_width: f64) -> Result<Vec<String let test_line = if current_line.is_empty() { word.to_string() } else { - format!("{} {}", current_line, word) + format!("{current_line} {word}") }; let extents = context.text_extents(&test_line)?; @@ -864,8 +892,8 @@ fn draw_notes( context.set_font_size(configuration.theme.fonts.note); for note in &map.notes { - let x = percent_to_pixel(note.coordinates.0, configuration.theme.sizes.map_width); - let y = percent_to_pixel(note.coordinates.1, configuration.theme.sizes.map_height); + let note_x = percent_to_pixel(note.coordinates.0, configuration.theme.sizes.map_width); + let note_y = percent_to_pixel(note.coordinates.1, configuration.theme.sizes.map_height); // Split text by newlines (handle both \\n escape and actual newlines) let text = note.text.replace("\\n", "\n"); @@ -881,30 +909,33 @@ fn draw_notes( } let box_width = f64::min(max_width, widest_line + padding * 2.0); + // Safe cast: number of lines in a note is small (typically < 100) + #[allow(clippy::cast_precision_loss)] let box_height = lines.len() as f64 * configuration.theme.line_heights.note + padding * 2.0; // Draw background - let (r, g, b) = configuration.theme.colors.background; - context.set_source_rgb(r, g, b); - context.rectangle(x, y, box_width, box_height); + let (red, green, blue) = configuration.theme.colors.background; + context.set_source_rgb(red, green, blue); + context.rectangle(note_x, note_y, box_width, box_height); context.fill()?; // Draw border - let (r, g, b) = configuration.theme.colors.vertex; - context.set_source_rgb(r, g, b); + let (red, green, blue) = configuration.theme.colors.vertex; + context.set_source_rgb(red, green, blue); context.set_line_width(configuration.theme.sizes.line_width); - context.rectangle(x, y, box_width, box_height); + context.rectangle(note_x, note_y, box_width, box_height); context.stroke()?; // Draw text - let (r, g, b) = configuration.theme.colors.label; - context.set_source_rgb(r, g, b); + let (red, green, blue) = configuration.theme.colors.label; + context.set_source_rgb(red, green, blue); for (i, line) in lines.iter().enumerate() { + // Safe cast: line index is a small number (typically < 100 for note text) + #[allow(clippy::cast_precision_loss)] + let line_offset = i as f64 * configuration.theme.line_heights.note; context.move_to( - x + padding, - y + padding - + i as f64 * configuration.theme.line_heights.note - + configuration.theme.fonts.note, + note_x + padding, + note_y + padding + line_offset + configuration.theme.fonts.note, ); context.show_text(line)?; } |