From 2cf6a3a9a688c1a976fb1da41f9b926411a53b46 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Wed, 28 May 2025 08:33:01 +0200 Subject: [PATCH] Track original SVG size (#7098) This fixes bugs related to how an `Image` follows the size of an SVG. We track the "source size" of each image, i.e. the original width/height of the SVG, which can be different from whatever it was rasterized as. --- crates/egui-wgpu/src/capture.rs | 6 +- crates/egui/src/load.rs | 43 ++++++- crates/egui/src/load/texture_loader.rs | 12 +- crates/egui/src/widgets/image.rs | 26 +++-- crates/egui/src/widgets/image_button.rs | 4 +- .../egui_demo_lib/src/demo/tests/svg_test.rs | 9 +- crates/egui_demo_lib/src/rendering_test.rs | 5 +- .../tests/snapshots/demos/SVG Test.png | 4 +- crates/egui_extras/src/image.rs | 105 +++++++++++------- crates/egui_extras/src/loaders/svg_loader.rs | 9 +- crates/egui_glow/src/painter.rs | 5 +- crates/epaint/src/image.rs | 48 ++++++-- 12 files changed, 180 insertions(+), 96 deletions(-) diff --git a/crates/egui-wgpu/src/capture.rs b/crates/egui-wgpu/src/capture.rs index b4072a8b..38595bb6 100644 --- a/crates/egui-wgpu/src/capture.rs +++ b/crates/egui-wgpu/src/capture.rs @@ -227,10 +227,10 @@ impl CaptureState { tx.send(( viewport_id, data, - ColorImage { - size: [tex_extent.width as usize, tex_extent.height as usize], + ColorImage::new( + [tex_extent.width as usize, tex_extent.height as usize], pixels, - }, + ), )) .ok(); ctx.request_repaint(); diff --git a/crates/egui/src/load.rs b/crates/egui/src/load.rs index 27bc9ae3..9c9df5a9 100644 --- a/crates/egui/src/load.rs +++ b/crates/egui/src/load.rs @@ -143,12 +143,16 @@ pub type Result = std::result::Result; /// Given as a hint for image loading requests. /// /// Used mostly for rendering SVG:s to a good size. -/// The size is measured in texels, with the pixels per point already factored in. -/// -/// All variants will preserve the original aspect ratio. +/// The [`SizeHint`] determines at what resolution the image should be rasterized. #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum SizeHint { - /// Scale original size by some factor. + /// Scale original size by some factor, keeping the original aspect ratio. + /// + /// The original size of the image is usually its texel resolution, + /// but for an SVG it's the point size of the SVG. + /// + /// For instance, setting `Scale(2.0)` will rasterize SVG:s to twice their original size, + /// which is useful for high-DPI displays. Scale(OrderedFloat), /// Scale to exactly this pixel width, keeping the original aspect ratio. @@ -168,6 +172,26 @@ pub enum SizeHint { }, } +impl SizeHint { + /// Multiply size hint by a factor. + pub fn scale_by(self, factor: f32) -> Self { + match self { + Self::Scale(scale) => Self::Scale(OrderedFloat(factor * scale.0)), + Self::Width(width) => Self::Width((factor * width as f32).round() as _), + Self::Height(height) => Self::Height((factor * height as f32).round() as _), + Self::Size { + width, + height, + maintain_aspect_ratio, + } => Self::Size { + width: (factor * width as f32).round() as _, + height: (factor * height as f32).round() as _, + maintain_aspect_ratio, + }, + } + } +} + impl Default for SizeHint { #[inline] fn default() -> Self { @@ -249,12 +273,16 @@ impl Deref for Bytes { pub enum BytesPoll { /// Bytes are being loaded. Pending { + /// Point size of the image. + /// /// Set if known (e.g. from a HTTP header, or by parsing the image file header). size: Option, }, /// Bytes are loaded. Ready { + /// Point size of the image. + /// /// Set if known (e.g. from a HTTP header, or by parsing the image file header). size: Option, @@ -344,6 +372,8 @@ pub trait BytesLoader { pub enum ImagePoll { /// Image is loading. Pending { + /// Point size of the image. + /// /// Set if known (e.g. from a HTTP header, or by parsing the image file header). size: Option, }, @@ -414,7 +444,7 @@ pub trait ImageLoader { pub struct SizedTexture { pub id: TextureId, - /// Size in logical ui points. + /// Point size of the original SVG, or the size of the image in texels. pub size: Vec2, } @@ -460,6 +490,8 @@ impl<'a> From<&'a TextureHandle> for SizedTexture { pub enum TexturePoll { /// Texture is loading. Pending { + /// Point size of the image. + /// /// Set if known (e.g. from a HTTP header, or by parsing the image file header). size: Option, }, @@ -469,6 +501,7 @@ pub enum TexturePoll { } impl TexturePoll { + /// Point size of the original SVG, or the size of the image in texels. #[inline] pub fn size(&self) -> Option { match self { diff --git a/crates/egui/src/load/texture_loader.rs b/crates/egui/src/load/texture_loader.rs index 8b23580b..39d8ff94 100644 --- a/crates/egui/src/load/texture_loader.rs +++ b/crates/egui/src/load/texture_loader.rs @@ -1,5 +1,7 @@ use std::sync::atomic::{AtomicU64, Ordering::Relaxed}; +use emath::Vec2; + use super::{ BytesLoader as _, Context, HashMap, ImagePoll, Mutex, SizeHint, SizedTexture, TextureHandle, TextureLoadResult, TextureLoader, TextureOptions, TexturePoll, @@ -16,6 +18,10 @@ type Bucket = HashMap, Entry>; struct Entry { last_used: AtomicU64, + + /// Size of the original SVG, if any, or the texel size of the image if not an SVG. + source_size: Vec2, + handle: TextureHandle, } @@ -61,18 +67,20 @@ impl TextureLoader for DefaultTextureLoader { texture .last_used .store(self.pass_index.load(Relaxed), Relaxed); - let texture = SizedTexture::from_handle(&texture.handle); + let texture = SizedTexture::new(texture.handle.id(), texture.source_size); Ok(TexturePoll::Ready { texture }) } else { match ctx.try_load_image(uri, size_hint)? { ImagePoll::Pending { size } => Ok(TexturePoll::Pending { size }), ImagePoll::Ready { image } => { + let source_size = image.source_size; let handle = ctx.load_texture(uri, image, texture_options); - let texture = SizedTexture::from_handle(&handle); + let texture = SizedTexture::new(handle.id(), source_size); bucket.insert( svg_size_hint, Entry { last_used: AtomicU64::new(self.pass_index.load(Relaxed)), + source_size, handle, }, ); diff --git a/crates/egui/src/widgets/image.rs b/crates/egui/src/widgets/image.rs index 79bd6d2c..e0ff7d36 100644 --- a/crates/egui/src/widgets/image.rs +++ b/crates/egui/src/widgets/image.rs @@ -156,6 +156,9 @@ impl<'a> Image<'a> { /// Fit the image to its original size with some scaling. /// + /// The texel size of the source image will be multiplied by the `scale` factor, + /// and then become the _ui_ size of the [`Image`]. + /// /// This will cause the image to overflow if it is larger than the available space. /// /// If [`Image::max_size`] is set, this is guaranteed to never exceed that limit. @@ -291,9 +294,9 @@ impl<'a, T: Into>> From for Image<'a> { impl<'a> Image<'a> { /// Returns the size the image will occupy in the final UI. #[inline] - pub fn calc_size(&self, available_size: Vec2, original_image_size: Option) -> Vec2 { - let original_image_size = original_image_size.unwrap_or(Vec2::splat(24.0)); // Fallback for still-loading textures, or failure to load. - self.size.calc_size(available_size, original_image_size) + pub fn calc_size(&self, available_size: Vec2, image_source_size: Option) -> Vec2 { + let image_source_size = image_source_size.unwrap_or(Vec2::splat(24.0)); // Fallback for still-loading textures, or failure to load. + self.size.calc_size(available_size, image_source_size) } pub fn load_and_calc_size(&self, ui: &Ui, available_size: Vec2) -> Option { @@ -405,8 +408,8 @@ impl<'a> Image<'a> { impl Widget for Image<'_> { fn ui(self, ui: &mut Ui) -> Response { let tlr = self.load_for_size(ui.ctx(), ui.available_size()); - let original_image_size = tlr.as_ref().ok().and_then(|t| t.size()); - let ui_size = self.calc_size(ui.available_size(), original_image_size); + let image_source_size = tlr.as_ref().ok().and_then(|t| t.size()); + let ui_size = self.calc_size(ui.available_size(), image_source_size); let (rect, response) = ui.allocate_exact_size(ui_size, self.sense); response.widget_info(|| { @@ -458,7 +461,10 @@ pub struct ImageSize { #[derive(Debug, Clone, Copy)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub enum ImageFit { - /// Fit the image to its original size, scaled by some factor. + /// Fit the image to its original srce size, scaled by some factor. + /// + /// The original size of the image is usually its texel resolution, + /// but for an SVG it's the point size of the SVG. /// /// Ignores how much space is actually available in the ui. Original { scale: f32 }, @@ -516,7 +522,7 @@ impl ImageSize { } /// Calculate the final on-screen size in points. - pub fn calc_size(&self, available_size: Vec2, original_image_size: Vec2) -> Vec2 { + pub fn calc_size(&self, available_size: Vec2, image_source_size: Vec2) -> Vec2 { let Self { maintain_aspect_ratio, max_size, @@ -524,7 +530,7 @@ impl ImageSize { } = *self; match fit { ImageFit::Original { scale } => { - let image_size = original_image_size * scale; + let image_size = scale * image_source_size; if image_size.x <= max_size.x && image_size.y <= max_size.y { image_size } else { @@ -533,11 +539,11 @@ impl ImageSize { } ImageFit::Fraction(fract) => { let scale_to_size = (available_size * fract).min(max_size); - scale_to_fit(original_image_size, scale_to_size, maintain_aspect_ratio) + scale_to_fit(image_source_size, scale_to_size, maintain_aspect_ratio) } ImageFit::Exact(size) => { let scale_to_size = size.min(max_size); - scale_to_fit(original_image_size, scale_to_size, maintain_aspect_ratio) + scale_to_fit(image_source_size, scale_to_size, maintain_aspect_ratio) } } } diff --git a/crates/egui/src/widgets/image_button.rs b/crates/egui/src/widgets/image_button.rs index b1dddbf7..7962e5a4 100644 --- a/crates/egui/src/widgets/image_button.rs +++ b/crates/egui/src/widgets/image_button.rs @@ -93,10 +93,10 @@ impl Widget for ImageButton<'_> { let available_size_for_image = ui.available_size() - 2.0 * padding; let tlr = self.image.load_for_size(ui.ctx(), available_size_for_image); - let original_image_size = tlr.as_ref().ok().and_then(|t| t.size()); + let image_source_size = tlr.as_ref().ok().and_then(|t| t.size()); let image_size = self .image - .calc_size(available_size_for_image, original_image_size); + .calc_size(available_size_for_image, image_source_size); let padded_size = image_size + 2.0 * padding; let (rect, response) = ui.allocate_exact_size(padded_size, self.sense); diff --git a/crates/egui_demo_lib/src/demo/tests/svg_test.rs b/crates/egui_demo_lib/src/demo/tests/svg_test.rs index 0079a2a7..930d1b5f 100644 --- a/crates/egui_demo_lib/src/demo/tests/svg_test.rs +++ b/crates/egui_demo_lib/src/demo/tests/svg_test.rs @@ -29,10 +29,11 @@ impl crate::View for SvgTest { ui.color_edit_button_srgba(color); let img_src = egui::include_image!("../../../data/peace.svg"); - // First paint a small version… - ui.add_sized( - egui::Vec2 { x: 20.0, y: 20.0 }, - egui::Image::new(img_src.clone()).tint(*color), + // First paint a small version, sized the same as the source… + ui.add( + egui::Image::new(img_src.clone()) + .fit_to_original_size(1.0) + .tint(*color), ); // …then a big one, to make sure they are both crisp diff --git a/crates/egui_demo_lib/src/rendering_test.rs b/crates/egui_demo_lib/src/rendering_test.rs index ddf07f04..5f0e91bc 100644 --- a/crates/egui_demo_lib/src/rendering_test.rs +++ b/crates/egui_demo_lib/src/rendering_test.rs @@ -419,10 +419,7 @@ impl TextureManager { let height = 1; ctx.load_texture( "color_test_gradient", - epaint::ColorImage { - size: [width, height], - pixels, - }, + epaint::ColorImage::new([width, height], pixels), TextureOptions::LINEAR, ) }) diff --git a/crates/egui_demo_lib/tests/snapshots/demos/SVG Test.png b/crates/egui_demo_lib/tests/snapshots/demos/SVG Test.png index c02536cc..9e14e624 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/SVG Test.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/SVG Test.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1160361c41ffa9cde6d83cb32eeb9f9b75b275e98b97b625eababee460b69ba9 -size 24072 +oid sha256:22515363a812443b65cbe9060e2352e18eed04dc382fc993c33bd8a4b5ddff91 +size 24817 diff --git a/crates/egui_extras/src/image.rs b/crates/egui_extras/src/image.rs index 40cbdc53..b181a6bc 100644 --- a/crates/egui_extras/src/image.rs +++ b/crates/egui_extras/src/image.rs @@ -1,6 +1,6 @@ #![allow(deprecated)] -use egui::{mutex::Mutex, TextureOptions}; +use egui::{load::SizedTexture, mutex::Mutex, ColorImage, TextureOptions, Vec2}; #[cfg(feature = "svg")] use egui::SizeHint; @@ -16,10 +16,16 @@ use egui::SizeHint; pub struct RetainedImage { debug_name: String, - size: [usize; 2], + /// Texel size. + /// + /// Same as [`Self.image`]`.size` + texel_size: [usize; 2], + + /// Original SVG size (if this is an SVG), or same as [`Self::texel_size`]. + source_size: Vec2, /// Cleared once [`Self::texture`] has been loaded. - image: Mutex, + image: Mutex, /// Lazily loaded when we have an egui context. texture: Mutex>, @@ -28,10 +34,11 @@ pub struct RetainedImage { } impl RetainedImage { - pub fn from_color_image(debug_name: impl Into, image: egui::ColorImage) -> Self { + pub fn from_color_image(debug_name: impl Into, image: ColorImage) -> Self { Self { debug_name: debug_name.into(), - size: image.size, + texel_size: image.size, + source_size: image.source_size, image: Mutex::new(image), texture: Default::default(), options: Default::default(), @@ -68,7 +75,7 @@ impl RetainedImage { svg_bytes: &[u8], options: &resvg::usvg::Options<'_>, ) -> Result { - Self::from_svg_bytes_with_size(debug_name, svg_bytes, None, options) + Self::from_svg_bytes_with_size(debug_name, svg_bytes, Default::default(), options) } /// Pass in the str of an SVG that you've loaded. @@ -81,11 +88,11 @@ impl RetainedImage { svg_str: &str, options: &resvg::usvg::Options<'_>, ) -> Result { - Self::from_svg_bytes(debug_name, svg_str.as_bytes(), options) + Self::from_svg_bytes_with_size(debug_name, svg_str.as_bytes(), Default::default(), options) } /// Pass in the bytes of an SVG that you've loaded - /// and the scaling option to resize the SVG with + /// and the scaling option to resize the SVG with. /// /// # Errors /// On invalid image @@ -93,7 +100,7 @@ impl RetainedImage { pub fn from_svg_bytes_with_size( debug_name: impl Into, svg_bytes: &[u8], - size_hint: Option, + size_hint: SizeHint, options: &resvg::usvg::Options<'_>, ) -> Result { Ok(Self::from_color_image( @@ -112,10 +119,7 @@ impl RetainedImage { /// # use egui_extras::RetainedImage; /// # use egui::{Color32, epaint::{ColorImage, textures::TextureOptions}}; /// # let pixels = vec![Color32::BLACK]; - /// # let color_image = ColorImage { - /// # size: [1, 1], - /// # pixels, - /// # }; + /// # let color_image = ColorImage::new([1, 1], pixels); /// # /// // Upload a pixel art image without it getting blurry when resized /// let image = RetainedImage::from_color_image("my_image", color_image) @@ -133,23 +137,39 @@ impl RetainedImage { } /// The size of the image data (number of pixels wide/high). + pub fn texel_size(&self) -> [usize; 2] { + self.texel_size + } + + /// The size of the original SVG image (if any). + /// + /// Note that this can differ from [`Self::texel_size`] if the SVG was rasterized at a different + /// resolution than the size of the original SVG. + pub fn source_size(&self) -> Vec2 { + self.source_size + } + + #[deprecated = "use `texel_size` or `source_size` instead"] pub fn size(&self) -> [usize; 2] { - self.size + self.texel_size } /// The width of the image. + #[deprecated = "use `texel_size` or `source_size` instead"] pub fn width(&self) -> usize { - self.size[0] + self.texel_size[0] } /// The height of the image. + #[deprecated = "use `texel_size` or `source_size` instead"] pub fn height(&self) -> usize { - self.size[1] + self.texel_size[1] } /// The size of the image data (number of pixels wide/high). + #[deprecated = "use `texel_size` or `source_size` instead"] pub fn size_vec2(&self) -> egui::Vec2 { - let [w, h] = self.size(); + let [w, h] = self.texel_size; egui::vec2(w as f32, h as f32) } @@ -163,7 +183,7 @@ impl RetainedImage { self.texture .lock() .get_or_insert_with(|| { - let image: &mut egui::ColorImage = &mut self.image.lock(); + let image: &mut ColorImage = &mut self.image.lock(); let image = std::mem::take(image); ctx.load_texture(&self.debug_name, image, self.options) }) @@ -172,7 +192,7 @@ impl RetainedImage { /// Show the image with the given maximum size. pub fn show_max_size(&self, ui: &mut egui::Ui, max_size: egui::Vec2) -> egui::Response { - let mut desired_size = self.size_vec2(); + let mut desired_size = self.source_size(); desired_size *= (max_size.x / desired_size.x).min(1.0); desired_size *= (max_size.y / desired_size.y).min(1.0); self.show_size(ui, desired_size) @@ -180,12 +200,12 @@ impl RetainedImage { /// Show the image with the original size (one image pixel = one gui point). pub fn show(&self, ui: &mut egui::Ui) -> egui::Response { - self.show_size(ui, self.size_vec2()) + self.show_size(ui, self.source_size()) } /// Show the image with the given scale factor (1.0 = original size). pub fn show_scaled(&self, ui: &mut egui::Ui, scale: f32) -> egui::Response { - self.show_size(ui, self.size_vec2() * scale) + self.show_size(ui, self.source_size() * scale) } /// Show the image with the given size. @@ -193,7 +213,7 @@ impl RetainedImage { // We need to convert the SVG to a texture to display it: // Future improvement: tell backend to do mip-mapping of the image to // make it look smoother when downsized. - ui.image((self.texture_id(ui.ctx()), desired_size)) + ui.image(SizedTexture::new(self.texture_id(ui.ctx()), desired_size)) } } @@ -207,7 +227,7 @@ impl RetainedImage { /// # Errors /// On invalid image or unsupported image format. #[cfg(feature = "image")] -pub fn load_image_bytes(image_bytes: &[u8]) -> Result { +pub fn load_image_bytes(image_bytes: &[u8]) -> Result { profiling::function_scope!(); let image = image::load_from_memory(image_bytes).map_err(|err| match err { image::ImageError::Unsupported(err) => match err.kind() { @@ -223,10 +243,11 @@ pub fn load_image_bytes(image_bytes: &[u8]) -> Result Result, -) -> Result { - load_svg_bytes_with_size(svg_bytes, None, options) +) -> Result { + load_svg_bytes_with_size(svg_bytes, Default::default(), options) } /// Load an SVG and rasterize it into an egui image with a scaling parameter. @@ -252,9 +273,9 @@ pub fn load_svg_bytes( #[cfg(feature = "svg")] pub fn load_svg_bytes_with_size( svg_bytes: &[u8], - size_hint: Option, + size_hint: SizeHint, options: &resvg::usvg::Options<'_>, -) -> Result { +) -> Result { use egui::Vec2; use resvg::{ tiny_skia::Pixmap, @@ -265,19 +286,18 @@ pub fn load_svg_bytes_with_size( let rtree = Tree::from_data(svg_bytes, options).map_err(|err| err.to_string())?; - let original_size = Vec2::new(rtree.size().width(), rtree.size().height()); + let source_size = Vec2::new(rtree.size().width(), rtree.size().height()); let scaled_size = match size_hint { - None => original_size, - Some(SizeHint::Size { + SizeHint::Size { width, height, maintain_aspect_ratio, - }) => { + } => { if maintain_aspect_ratio { // As large as possible, without exceeding the given size: - let mut size = original_size; - size *= width as f32 / original_size.x; + let mut size = source_size; + size *= width as f32 / source_size.x; if size.y > height as f32 { size *= height as f32 / size.y; } @@ -286,9 +306,9 @@ pub fn load_svg_bytes_with_size( Vec2::new(width as _, height as _) } } - Some(SizeHint::Height(h)) => original_size * (h as f32 / original_size.y), - Some(SizeHint::Width(w)) => original_size * (w as f32 / original_size.x), - Some(SizeHint::Scale(scale)) => scale.into_inner() * original_size, + SizeHint::Height(h) => source_size * (h as f32 / source_size.y), + SizeHint::Width(w) => source_size * (w as f32 / source_size.x), + SizeHint::Scale(scale) => scale.into_inner() * source_size, }; let scaled_size = scaled_size.round(); @@ -299,11 +319,12 @@ pub fn load_svg_bytes_with_size( resvg::render( &rtree, - Transform::from_scale(w as f32 / original_size.x, h as f32 / original_size.y), + Transform::from_scale(w as f32 / source_size.x, h as f32 / source_size.y), &mut pixmap.as_mut(), ); - let image = egui::ColorImage::from_rgba_premultiplied([w as _, h as _], pixmap.data()); + let image = ColorImage::from_rgba_premultiplied([w as _, h as _], pixmap.data()) + .with_source_size(source_size); Ok(image) } diff --git a/crates/egui_extras/src/loaders/svg_loader.rs b/crates/egui_extras/src/loaders/svg_loader.rs index 43f23c82..fab70151 100644 --- a/crates/egui_extras/src/loaders/svg_loader.rs +++ b/crates/egui_extras/src/loaders/svg_loader.rs @@ -75,12 +75,9 @@ impl ImageLoader for SvgLoader { match ctx.try_load_bytes(uri) { Ok(BytesPoll::Ready { bytes, .. }) => { log::trace!("Started loading {uri:?}"); - let result = crate::image::load_svg_bytes_with_size( - &bytes, - Some(size_hint), - &self.options, - ) - .map(Arc::new); + let result = + crate::image::load_svg_bytes_with_size(&bytes, size_hint, &self.options) + .map(Arc::new); log::trace!("Finished loading {uri:?}"); bucket.insert( diff --git a/crates/egui_glow/src/painter.rs b/crates/egui_glow/src/painter.rs index 6b94bb89..0646b560 100644 --- a/crates/egui_glow/src/painter.rs +++ b/crates/egui_glow/src/painter.rs @@ -696,10 +696,7 @@ impl Painter { for row in pixels.chunks_exact((w * 4) as usize).rev() { flipped.extend_from_slice(bytemuck::cast_slice(row)); } - egui::ColorImage { - size: [w as usize, h as usize], - pixels: flipped, - } + egui::ColorImage::new([w as usize, h as usize], flipped) } pub fn read_screen_rgb(&self, [w, h]: [u32; 2]) -> Vec { diff --git a/crates/epaint/src/image.rs b/crates/epaint/src/image.rs index e50e33e6..b6183e7a 100644 --- a/crates/epaint/src/image.rs +++ b/crates/epaint/src/image.rs @@ -1,3 +1,5 @@ +use emath::Vec2; + use crate::{textures::TextureOptions, Color32}; use std::sync::Arc; @@ -47,18 +49,36 @@ impl ImageData { #[derive(Clone, Default, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct ColorImage { - /// width, height. + /// width, height in texels. pub size: [usize; 2], + /// Size of the original SVG image (if any), or just the texel size of the image. + pub source_size: Vec2, + /// The pixels, row by row, from top to bottom. pub pixels: Vec, } impl ColorImage { /// Create an image filled with the given color. - pub fn new(size: [usize; 2], color: Color32) -> Self { + pub fn new(size: [usize; 2], pixels: Vec) -> Self { + debug_assert!( + size[0] * size[1] == pixels.len(), + "size: {size:?}, pixels.len(): {}", + pixels.len() + ); Self { size, + source_size: Vec2::new(size[0] as f32, size[1] as f32), + pixels, + } + } + + /// Create an image filled with the given color. + pub fn filled(size: [usize; 2], color: Color32) -> Self { + Self { + size, + source_size: Vec2::new(size[0] as f32, size[1] as f32), pixels: vec![color; size[0] * size[1]], } } @@ -105,7 +125,7 @@ impl ColorImage { .chunks_exact(4) .map(|p| Color32::from_rgba_unmultiplied(p[0], p[1], p[2], p[3])) .collect(); - Self { size, pixels } + Self::new(size, pixels) } pub fn from_rgba_premultiplied(size: [usize; 2], rgba: &[u8]) -> Self { @@ -120,7 +140,7 @@ impl ColorImage { .chunks_exact(4) .map(|p| Color32::from_rgba_premultiplied(p[0], p[1], p[2], p[3])) .collect(); - Self { size, pixels } + Self::new(size, pixels) } /// Create a [`ColorImage`] from flat opaque gray data. @@ -135,7 +155,7 @@ impl ColorImage { gray.len() ); let pixels = gray.iter().map(|p| Color32::from_gray(*p)).collect(); - Self { size, pixels } + Self::new(size, pixels) } /// Alternative method to `from_gray`. @@ -152,7 +172,7 @@ impl ColorImage { size, pixels.len() ); - Self { size, pixels } + Self::new(size, pixels) } /// A view of the underlying data as `&[u8]` @@ -185,14 +205,14 @@ impl ColorImage { .chunks_exact(3) .map(|p| Color32::from_rgb(p[0], p[1], p[2])) .collect(); - Self { size, pixels } + Self::new(size, pixels) } /// An example color image, useful for tests. pub fn example() -> Self { let width = 128; let height = 64; - let mut img = Self::new([width, height], Color32::TRANSPARENT); + let mut img = Self::filled([width, height], Color32::TRANSPARENT); for y in 0..height { for x in 0..width { let h = x as f32 / width as f32; @@ -205,6 +225,13 @@ impl ColorImage { img } + /// Set the source size of e.g. the original SVG image. + #[inline] + pub fn with_source_size(mut self, source_size: Vec2) -> Self { + self.source_size = source_size; + self + } + #[inline] pub fn width(&self) -> usize { self.size[0] @@ -242,10 +269,7 @@ impl ColorImage { &self.pixels[row * row_stride + min_x..row * row_stride + max_x], ); } - Self { - size: [width, height], - pixels: output, - } + Self::new([width, height], output) } }