use std::{ collections::HashMap, path::{Path, PathBuf}, }; use gdk::{gdk_pixbuf::Pixbuf, prelude::GdkContextExt}; use librsvg::SvgHandle; use mime_guess::mime; use diaphragm_core::{ styles::{DefinedDashStyle, Pattern}, text::{DefinedFontDescription, FontDescription}, Renderer, }; enum Image { Pixbuf(Pixbuf), Svg(SvgHandle), } impl Image { fn width(&self) -> f64 { match self { Image::Pixbuf(pixbuf) => pixbuf.width() as f64, Image::Svg(handle) => { let dimensions = librsvg::CairoRenderer::new(handle).intrinsic_dimensions(); // TODO: this doesn't check if the SVG has different unit for width of height dimensions.width.length } } } fn height(&self) -> f64 { match self { Image::Pixbuf(pixbuf) => pixbuf.height() as f64, Image::Svg(handle) => { let dimensions = librsvg::CairoRenderer::new(handle).intrinsic_dimensions(); // TODO: this doesn't check if the SVG has different unit for width of height dimensions.height.length } } } } pub struct CairoRenderer { ctx: cairo::Context, loaded_images: HashMap, } impl CairoRenderer { pub fn new() -> Self { // HACK: a bit ugly, but text_extents requires a context, so we have to create a "dummy" // surface let surface = cairo::SvgSurface::for_stream(1920., 1080., std::io::sink()).unwrap(); let ctx = cairo::Context::new(&surface).unwrap(); Self { ctx, loaded_images: HashMap::new(), } } fn load_image(&mut self, path: &Path) { if self.loaded_images.contains_key(path) { return; } let mime_type = mime_guess::from_path(&path).first().unwrap(); assert_eq!(mime_type.type_(), "image", "File is not an image"); let image = match mime_type.subtype() { mime::SVG => Image::Svg(librsvg::Loader::new().read_path(path).unwrap()), // TODO: use Pixbuf::formats() to check for supported formats mime::PNG | mime::GIF | mime::JPEG => Image::Pixbuf(Pixbuf::from_file(path).unwrap()), other => panic!("Unsupported image type: {}", other), }; self.loaded_images.insert(path.to_owned(), image); } fn get_image(&self, path: &Path) -> &Image { self.loaded_images.get(path).unwrap() } } impl Default for CairoRenderer { fn default() -> Self { Self::new() } } impl Renderer for CairoRenderer { fn set_size(&mut self, width: f64, height: f64) { let surface = cairo::SvgSurface::for_stream(width, height, std::io::stdout()).unwrap(); self.ctx = cairo::Context::new(&surface).unwrap(); } fn move_to(&mut self, x: f64, y: f64) { self.ctx.move_to(x, y) } fn stroke(&mut self) { self.ctx.stroke().unwrap(); } fn fill(&mut self) { self.ctx.fill().unwrap(); } fn fill_preserve(&mut self) { self.ctx.fill_preserve().unwrap(); } fn set_pattern(&mut self, pattern: &Pattern) { match pattern { Pattern::Solid(color) => { let (red, green, blue, alpha) = color.rgba(); self.ctx.set_source_rgba(red, green, blue, alpha) } Pattern::None => {} _ => panic!("Unhandled pattern"), } } fn set_dash(&mut self, dash: &DefinedDashStyle) { self.ctx.set_dash(dash.dashes(), dash.offset()); } fn clear_dash(&mut self) { self.ctx.set_dash(&[], 0.); } fn set_line_width(&mut self, width: f64) { self.ctx.set_line_width(width); } fn line_to(&mut self, x: f64, y: f64) { self.ctx.line_to(x, y) } fn rectangle(&mut self, x: f64, y: f64, width: f64, height: f64) { self.ctx.rectangle(x, y, width, height); } fn text_extents(&self, text: &str, font: &FontDescription) -> (f64, f64) { // Pango gives us integer back, so we lose some precision. We use this for that. const TEST_ABSOLUTE_SIZE: f64 = 10_000.; let layout = pangocairo::create_layout(&self.ctx); let mut font_desc = pango::FontDescription::from_string(&font.family); font_desc.set_absolute_size(TEST_ABSOLUTE_SIZE * pango::SCALE as f64); layout.set_font_description(Some(&font_desc)); layout.set_markup(text); // TODO: get height from the baseline // let mut layout_iter = layout.iter(); // let _height = loop { // if layout_iter.at_last_line() { // break layout_iter.baseline(); // } // layout_iter.next_line(); // }; let (_, extents) = layout.pixel_extents(); ( extents.width() as f64 / TEST_ABSOLUTE_SIZE, extents.height() as f64 / TEST_ABSOLUTE_SIZE, ) } fn show_text(&mut self, text: &str, font: &DefinedFontDescription) { let layout = pangocairo::create_layout(&self.ctx); let mut font_desc = pango::FontDescription::from_string(&font.family); font_desc.set_absolute_size(font.size * pango::SCALE as f64); layout.set_font_description(Some(&font_desc)); layout.set_markup(text); pangocairo::show_layout(&self.ctx, &layout); } fn show_image(&mut self, path: &Path, x: f64, y: f64, width: f64, height: f64) { self.load_image(path); let image = self.get_image(path); self.ctx.save().unwrap(); match image { Image::Pixbuf(pixbuf) => { let scale_x = width / image.width(); let scale_y = height / image.height(); self.ctx.scale(scale_x, scale_y); self.ctx.set_source_pixbuf(pixbuf, x / scale_x, y / scale_y); self.ctx.paint().unwrap(); } Image::Svg(handle) => { // TODO: if aspect ratio is not kept, the image is not "scaled", only margins are // added librsvg::CairoRenderer::new(handle) .render_document(&self.ctx, &cairo::Rectangle::new(x, y, width, height)) .unwrap(); } } self.ctx.restore().unwrap(); } fn geometry_for_image(&mut self, path: &Path) -> (f64, f64) { self.load_image(path); let image = self.get_image(path); (image.width(), image.height()) } }