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.
This commit is contained in:
parent
da67465a6c
commit
2cf6a3a9a6
|
|
@ -227,10 +227,10 @@ impl CaptureState {
|
||||||
tx.send((
|
tx.send((
|
||||||
viewport_id,
|
viewport_id,
|
||||||
data,
|
data,
|
||||||
ColorImage {
|
ColorImage::new(
|
||||||
size: [tex_extent.width as usize, tex_extent.height as usize],
|
[tex_extent.width as usize, tex_extent.height as usize],
|
||||||
pixels,
|
pixels,
|
||||||
},
|
),
|
||||||
))
|
))
|
||||||
.ok();
|
.ok();
|
||||||
ctx.request_repaint();
|
ctx.request_repaint();
|
||||||
|
|
|
||||||
|
|
@ -143,12 +143,16 @@ pub type Result<T, E = LoadError> = std::result::Result<T, E>;
|
||||||
/// Given as a hint for image loading requests.
|
/// Given as a hint for image loading requests.
|
||||||
///
|
///
|
||||||
/// Used mostly for rendering SVG:s to a good size.
|
/// Used mostly for rendering SVG:s to a good size.
|
||||||
/// The size is measured in texels, with the pixels per point already factored in.
|
/// The [`SizeHint`] determines at what resolution the image should be rasterized.
|
||||||
///
|
|
||||||
/// All variants will preserve the original aspect ratio.
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
pub enum SizeHint {
|
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<f32>),
|
Scale(OrderedFloat<f32>),
|
||||||
|
|
||||||
/// Scale to exactly this pixel width, keeping the original aspect ratio.
|
/// 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 {
|
impl Default for SizeHint {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
|
|
@ -249,12 +273,16 @@ impl Deref for Bytes {
|
||||||
pub enum BytesPoll {
|
pub enum BytesPoll {
|
||||||
/// Bytes are being loaded.
|
/// Bytes are being loaded.
|
||||||
Pending {
|
Pending {
|
||||||
|
/// Point size of the image.
|
||||||
|
///
|
||||||
/// Set if known (e.g. from a HTTP header, or by parsing the image file header).
|
/// Set if known (e.g. from a HTTP header, or by parsing the image file header).
|
||||||
size: Option<Vec2>,
|
size: Option<Vec2>,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Bytes are loaded.
|
/// Bytes are loaded.
|
||||||
Ready {
|
Ready {
|
||||||
|
/// Point size of the image.
|
||||||
|
///
|
||||||
/// Set if known (e.g. from a HTTP header, or by parsing the image file header).
|
/// Set if known (e.g. from a HTTP header, or by parsing the image file header).
|
||||||
size: Option<Vec2>,
|
size: Option<Vec2>,
|
||||||
|
|
||||||
|
|
@ -344,6 +372,8 @@ pub trait BytesLoader {
|
||||||
pub enum ImagePoll {
|
pub enum ImagePoll {
|
||||||
/// Image is loading.
|
/// Image is loading.
|
||||||
Pending {
|
Pending {
|
||||||
|
/// Point size of the image.
|
||||||
|
///
|
||||||
/// Set if known (e.g. from a HTTP header, or by parsing the image file header).
|
/// Set if known (e.g. from a HTTP header, or by parsing the image file header).
|
||||||
size: Option<Vec2>,
|
size: Option<Vec2>,
|
||||||
},
|
},
|
||||||
|
|
@ -414,7 +444,7 @@ pub trait ImageLoader {
|
||||||
pub struct SizedTexture {
|
pub struct SizedTexture {
|
||||||
pub id: TextureId,
|
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,
|
pub size: Vec2,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -460,6 +490,8 @@ impl<'a> From<&'a TextureHandle> for SizedTexture {
|
||||||
pub enum TexturePoll {
|
pub enum TexturePoll {
|
||||||
/// Texture is loading.
|
/// Texture is loading.
|
||||||
Pending {
|
Pending {
|
||||||
|
/// Point size of the image.
|
||||||
|
///
|
||||||
/// Set if known (e.g. from a HTTP header, or by parsing the image file header).
|
/// Set if known (e.g. from a HTTP header, or by parsing the image file header).
|
||||||
size: Option<Vec2>,
|
size: Option<Vec2>,
|
||||||
},
|
},
|
||||||
|
|
@ -469,6 +501,7 @@ pub enum TexturePoll {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TexturePoll {
|
impl TexturePoll {
|
||||||
|
/// Point size of the original SVG, or the size of the image in texels.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn size(&self) -> Option<Vec2> {
|
pub fn size(&self) -> Option<Vec2> {
|
||||||
match self {
|
match self {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
use std::sync::atomic::{AtomicU64, Ordering::Relaxed};
|
use std::sync::atomic::{AtomicU64, Ordering::Relaxed};
|
||||||
|
|
||||||
|
use emath::Vec2;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
BytesLoader as _, Context, HashMap, ImagePoll, Mutex, SizeHint, SizedTexture, TextureHandle,
|
BytesLoader as _, Context, HashMap, ImagePoll, Mutex, SizeHint, SizedTexture, TextureHandle,
|
||||||
TextureLoadResult, TextureLoader, TextureOptions, TexturePoll,
|
TextureLoadResult, TextureLoader, TextureOptions, TexturePoll,
|
||||||
|
|
@ -16,6 +18,10 @@ type Bucket = HashMap<Option<SizeHint>, Entry>;
|
||||||
|
|
||||||
struct Entry {
|
struct Entry {
|
||||||
last_used: AtomicU64,
|
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,
|
handle: TextureHandle,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -61,18 +67,20 @@ impl TextureLoader for DefaultTextureLoader {
|
||||||
texture
|
texture
|
||||||
.last_used
|
.last_used
|
||||||
.store(self.pass_index.load(Relaxed), Relaxed);
|
.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 })
|
Ok(TexturePoll::Ready { texture })
|
||||||
} else {
|
} else {
|
||||||
match ctx.try_load_image(uri, size_hint)? {
|
match ctx.try_load_image(uri, size_hint)? {
|
||||||
ImagePoll::Pending { size } => Ok(TexturePoll::Pending { size }),
|
ImagePoll::Pending { size } => Ok(TexturePoll::Pending { size }),
|
||||||
ImagePoll::Ready { image } => {
|
ImagePoll::Ready { image } => {
|
||||||
|
let source_size = image.source_size;
|
||||||
let handle = ctx.load_texture(uri, image, texture_options);
|
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(
|
bucket.insert(
|
||||||
svg_size_hint,
|
svg_size_hint,
|
||||||
Entry {
|
Entry {
|
||||||
last_used: AtomicU64::new(self.pass_index.load(Relaxed)),
|
last_used: AtomicU64::new(self.pass_index.load(Relaxed)),
|
||||||
|
source_size,
|
||||||
handle,
|
handle,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -156,6 +156,9 @@ impl<'a> Image<'a> {
|
||||||
|
|
||||||
/// Fit the image to its original size with some scaling.
|
/// 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.
|
/// 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.
|
/// If [`Image::max_size`] is set, this is guaranteed to never exceed that limit.
|
||||||
|
|
@ -291,9 +294,9 @@ impl<'a, T: Into<ImageSource<'a>>> From<T> for Image<'a> {
|
||||||
impl<'a> Image<'a> {
|
impl<'a> Image<'a> {
|
||||||
/// Returns the size the image will occupy in the final UI.
|
/// Returns the size the image will occupy in the final UI.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn calc_size(&self, available_size: Vec2, original_image_size: Option<Vec2>) -> Vec2 {
|
pub fn calc_size(&self, available_size: Vec2, image_source_size: Option<Vec2>) -> Vec2 {
|
||||||
let original_image_size = original_image_size.unwrap_or(Vec2::splat(24.0)); // Fallback for still-loading textures, or failure to load.
|
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, original_image_size)
|
self.size.calc_size(available_size, image_source_size)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_and_calc_size(&self, ui: &Ui, available_size: Vec2) -> Option<Vec2> {
|
pub fn load_and_calc_size(&self, ui: &Ui, available_size: Vec2) -> Option<Vec2> {
|
||||||
|
|
@ -405,8 +408,8 @@ impl<'a> Image<'a> {
|
||||||
impl Widget for Image<'_> {
|
impl Widget for Image<'_> {
|
||||||
fn ui(self, ui: &mut Ui) -> Response {
|
fn ui(self, ui: &mut Ui) -> Response {
|
||||||
let tlr = self.load_for_size(ui.ctx(), ui.available_size());
|
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 image_source_size = tlr.as_ref().ok().and_then(|t| t.size());
|
||||||
let ui_size = self.calc_size(ui.available_size(), original_image_size);
|
let ui_size = self.calc_size(ui.available_size(), image_source_size);
|
||||||
|
|
||||||
let (rect, response) = ui.allocate_exact_size(ui_size, self.sense);
|
let (rect, response) = ui.allocate_exact_size(ui_size, self.sense);
|
||||||
response.widget_info(|| {
|
response.widget_info(|| {
|
||||||
|
|
@ -458,7 +461,10 @@ pub struct ImageSize {
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
pub enum ImageFit {
|
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.
|
/// Ignores how much space is actually available in the ui.
|
||||||
Original { scale: f32 },
|
Original { scale: f32 },
|
||||||
|
|
@ -516,7 +522,7 @@ impl ImageSize {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Calculate the final on-screen size in points.
|
/// 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 {
|
let Self {
|
||||||
maintain_aspect_ratio,
|
maintain_aspect_ratio,
|
||||||
max_size,
|
max_size,
|
||||||
|
|
@ -524,7 +530,7 @@ impl ImageSize {
|
||||||
} = *self;
|
} = *self;
|
||||||
match fit {
|
match fit {
|
||||||
ImageFit::Original { scale } => {
|
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 {
|
if image_size.x <= max_size.x && image_size.y <= max_size.y {
|
||||||
image_size
|
image_size
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -533,11 +539,11 @@ impl ImageSize {
|
||||||
}
|
}
|
||||||
ImageFit::Fraction(fract) => {
|
ImageFit::Fraction(fract) => {
|
||||||
let scale_to_size = (available_size * fract).min(max_size);
|
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) => {
|
ImageFit::Exact(size) => {
|
||||||
let scale_to_size = size.min(max_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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -93,10 +93,10 @@ impl Widget for ImageButton<'_> {
|
||||||
|
|
||||||
let available_size_for_image = ui.available_size() - 2.0 * padding;
|
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 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
|
let image_size = self
|
||||||
.image
|
.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 padded_size = image_size + 2.0 * padding;
|
||||||
let (rect, response) = ui.allocate_exact_size(padded_size, self.sense);
|
let (rect, response) = ui.allocate_exact_size(padded_size, self.sense);
|
||||||
|
|
|
||||||
|
|
@ -29,10 +29,11 @@ impl crate::View for SvgTest {
|
||||||
ui.color_edit_button_srgba(color);
|
ui.color_edit_button_srgba(color);
|
||||||
let img_src = egui::include_image!("../../../data/peace.svg");
|
let img_src = egui::include_image!("../../../data/peace.svg");
|
||||||
|
|
||||||
// First paint a small version…
|
// First paint a small version, sized the same as the source…
|
||||||
ui.add_sized(
|
ui.add(
|
||||||
egui::Vec2 { x: 20.0, y: 20.0 },
|
egui::Image::new(img_src.clone())
|
||||||
egui::Image::new(img_src.clone()).tint(*color),
|
.fit_to_original_size(1.0)
|
||||||
|
.tint(*color),
|
||||||
);
|
);
|
||||||
|
|
||||||
// …then a big one, to make sure they are both crisp
|
// …then a big one, to make sure they are both crisp
|
||||||
|
|
|
||||||
|
|
@ -419,10 +419,7 @@ impl TextureManager {
|
||||||
let height = 1;
|
let height = 1;
|
||||||
ctx.load_texture(
|
ctx.load_texture(
|
||||||
"color_test_gradient",
|
"color_test_gradient",
|
||||||
epaint::ColorImage {
|
epaint::ColorImage::new([width, height], pixels),
|
||||||
size: [width, height],
|
|
||||||
pixels,
|
|
||||||
},
|
|
||||||
TextureOptions::LINEAR,
|
TextureOptions::LINEAR,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:1160361c41ffa9cde6d83cb32eeb9f9b75b275e98b97b625eababee460b69ba9
|
oid sha256:22515363a812443b65cbe9060e2352e18eed04dc382fc993c33bd8a4b5ddff91
|
||||||
size 24072
|
size 24817
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
#![allow(deprecated)]
|
#![allow(deprecated)]
|
||||||
|
|
||||||
use egui::{mutex::Mutex, TextureOptions};
|
use egui::{load::SizedTexture, mutex::Mutex, ColorImage, TextureOptions, Vec2};
|
||||||
|
|
||||||
#[cfg(feature = "svg")]
|
#[cfg(feature = "svg")]
|
||||||
use egui::SizeHint;
|
use egui::SizeHint;
|
||||||
|
|
@ -16,10 +16,16 @@ use egui::SizeHint;
|
||||||
pub struct RetainedImage {
|
pub struct RetainedImage {
|
||||||
debug_name: String,
|
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.
|
/// Cleared once [`Self::texture`] has been loaded.
|
||||||
image: Mutex<egui::ColorImage>,
|
image: Mutex<ColorImage>,
|
||||||
|
|
||||||
/// Lazily loaded when we have an egui context.
|
/// Lazily loaded when we have an egui context.
|
||||||
texture: Mutex<Option<egui::TextureHandle>>,
|
texture: Mutex<Option<egui::TextureHandle>>,
|
||||||
|
|
@ -28,10 +34,11 @@ pub struct RetainedImage {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RetainedImage {
|
impl RetainedImage {
|
||||||
pub fn from_color_image(debug_name: impl Into<String>, image: egui::ColorImage) -> Self {
|
pub fn from_color_image(debug_name: impl Into<String>, image: ColorImage) -> Self {
|
||||||
Self {
|
Self {
|
||||||
debug_name: debug_name.into(),
|
debug_name: debug_name.into(),
|
||||||
size: image.size,
|
texel_size: image.size,
|
||||||
|
source_size: image.source_size,
|
||||||
image: Mutex::new(image),
|
image: Mutex::new(image),
|
||||||
texture: Default::default(),
|
texture: Default::default(),
|
||||||
options: Default::default(),
|
options: Default::default(),
|
||||||
|
|
@ -68,7 +75,7 @@ impl RetainedImage {
|
||||||
svg_bytes: &[u8],
|
svg_bytes: &[u8],
|
||||||
options: &resvg::usvg::Options<'_>,
|
options: &resvg::usvg::Options<'_>,
|
||||||
) -> Result<Self, String> {
|
) -> Result<Self, String> {
|
||||||
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.
|
/// Pass in the str of an SVG that you've loaded.
|
||||||
|
|
@ -81,11 +88,11 @@ impl RetainedImage {
|
||||||
svg_str: &str,
|
svg_str: &str,
|
||||||
options: &resvg::usvg::Options<'_>,
|
options: &resvg::usvg::Options<'_>,
|
||||||
) -> Result<Self, String> {
|
) -> Result<Self, String> {
|
||||||
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
|
/// 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
|
/// # Errors
|
||||||
/// On invalid image
|
/// On invalid image
|
||||||
|
|
@ -93,7 +100,7 @@ impl RetainedImage {
|
||||||
pub fn from_svg_bytes_with_size(
|
pub fn from_svg_bytes_with_size(
|
||||||
debug_name: impl Into<String>,
|
debug_name: impl Into<String>,
|
||||||
svg_bytes: &[u8],
|
svg_bytes: &[u8],
|
||||||
size_hint: Option<SizeHint>,
|
size_hint: SizeHint,
|
||||||
options: &resvg::usvg::Options<'_>,
|
options: &resvg::usvg::Options<'_>,
|
||||||
) -> Result<Self, String> {
|
) -> Result<Self, String> {
|
||||||
Ok(Self::from_color_image(
|
Ok(Self::from_color_image(
|
||||||
|
|
@ -112,10 +119,7 @@ impl RetainedImage {
|
||||||
/// # use egui_extras::RetainedImage;
|
/// # use egui_extras::RetainedImage;
|
||||||
/// # use egui::{Color32, epaint::{ColorImage, textures::TextureOptions}};
|
/// # use egui::{Color32, epaint::{ColorImage, textures::TextureOptions}};
|
||||||
/// # let pixels = vec![Color32::BLACK];
|
/// # let pixels = vec![Color32::BLACK];
|
||||||
/// # let color_image = ColorImage {
|
/// # let color_image = ColorImage::new([1, 1], pixels);
|
||||||
/// # size: [1, 1],
|
|
||||||
/// # pixels,
|
|
||||||
/// # };
|
|
||||||
/// #
|
/// #
|
||||||
/// // Upload a pixel art image without it getting blurry when resized
|
/// // Upload a pixel art image without it getting blurry when resized
|
||||||
/// let image = RetainedImage::from_color_image("my_image", color_image)
|
/// 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).
|
/// 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] {
|
pub fn size(&self) -> [usize; 2] {
|
||||||
self.size
|
self.texel_size
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The width of the image.
|
/// The width of the image.
|
||||||
|
#[deprecated = "use `texel_size` or `source_size` instead"]
|
||||||
pub fn width(&self) -> usize {
|
pub fn width(&self) -> usize {
|
||||||
self.size[0]
|
self.texel_size[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The height of the image.
|
/// The height of the image.
|
||||||
|
#[deprecated = "use `texel_size` or `source_size` instead"]
|
||||||
pub fn height(&self) -> usize {
|
pub fn height(&self) -> usize {
|
||||||
self.size[1]
|
self.texel_size[1]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The size of the image data (number of pixels wide/high).
|
/// 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 {
|
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)
|
egui::vec2(w as f32, h as f32)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -163,7 +183,7 @@ impl RetainedImage {
|
||||||
self.texture
|
self.texture
|
||||||
.lock()
|
.lock()
|
||||||
.get_or_insert_with(|| {
|
.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);
|
let image = std::mem::take(image);
|
||||||
ctx.load_texture(&self.debug_name, image, self.options)
|
ctx.load_texture(&self.debug_name, image, self.options)
|
||||||
})
|
})
|
||||||
|
|
@ -172,7 +192,7 @@ impl RetainedImage {
|
||||||
|
|
||||||
/// Show the image with the given maximum size.
|
/// Show the image with the given maximum size.
|
||||||
pub fn show_max_size(&self, ui: &mut egui::Ui, max_size: egui::Vec2) -> egui::Response {
|
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.x / desired_size.x).min(1.0);
|
||||||
desired_size *= (max_size.y / desired_size.y).min(1.0);
|
desired_size *= (max_size.y / desired_size.y).min(1.0);
|
||||||
self.show_size(ui, desired_size)
|
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).
|
/// Show the image with the original size (one image pixel = one gui point).
|
||||||
pub fn show(&self, ui: &mut egui::Ui) -> egui::Response {
|
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).
|
/// 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 {
|
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.
|
/// 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:
|
// We need to convert the SVG to a texture to display it:
|
||||||
// Future improvement: tell backend to do mip-mapping of the image to
|
// Future improvement: tell backend to do mip-mapping of the image to
|
||||||
// make it look smoother when downsized.
|
// 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
|
/// # Errors
|
||||||
/// On invalid image or unsupported image format.
|
/// On invalid image or unsupported image format.
|
||||||
#[cfg(feature = "image")]
|
#[cfg(feature = "image")]
|
||||||
pub fn load_image_bytes(image_bytes: &[u8]) -> Result<egui::ColorImage, egui::load::LoadError> {
|
pub fn load_image_bytes(image_bytes: &[u8]) -> Result<ColorImage, egui::load::LoadError> {
|
||||||
profiling::function_scope!();
|
profiling::function_scope!();
|
||||||
let image = image::load_from_memory(image_bytes).map_err(|err| match err {
|
let image = image::load_from_memory(image_bytes).map_err(|err| match err {
|
||||||
image::ImageError::Unsupported(err) => match err.kind() {
|
image::ImageError::Unsupported(err) => match err.kind() {
|
||||||
|
|
@ -223,10 +243,11 @@ pub fn load_image_bytes(image_bytes: &[u8]) -> Result<egui::ColorImage, egui::lo
|
||||||
let size = [image.width() as _, image.height() as _];
|
let size = [image.width() as _, image.height() as _];
|
||||||
let image_buffer = image.to_rgba8();
|
let image_buffer = image.to_rgba8();
|
||||||
let pixels = image_buffer.as_flat_samples();
|
let pixels = image_buffer.as_flat_samples();
|
||||||
Ok(egui::ColorImage::from_rgba_unmultiplied(
|
|
||||||
size,
|
// TODO(emilk): if this is a PNG, looks for DPI info to calculate the source size,
|
||||||
pixels.as_slice(),
|
// e.g. for screenshots taken on a high-DPI/retina display.
|
||||||
))
|
|
||||||
|
Ok(ColorImage::from_rgba_unmultiplied(size, pixels.as_slice()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load an SVG and rasterize it into an egui image.
|
/// Load an SVG and rasterize it into an egui image.
|
||||||
|
|
@ -239,8 +260,8 @@ pub fn load_image_bytes(image_bytes: &[u8]) -> Result<egui::ColorImage, egui::lo
|
||||||
pub fn load_svg_bytes(
|
pub fn load_svg_bytes(
|
||||||
svg_bytes: &[u8],
|
svg_bytes: &[u8],
|
||||||
options: &resvg::usvg::Options<'_>,
|
options: &resvg::usvg::Options<'_>,
|
||||||
) -> Result<egui::ColorImage, String> {
|
) -> Result<ColorImage, String> {
|
||||||
load_svg_bytes_with_size(svg_bytes, None, options)
|
load_svg_bytes_with_size(svg_bytes, Default::default(), options)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load an SVG and rasterize it into an egui image with a scaling parameter.
|
/// 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")]
|
#[cfg(feature = "svg")]
|
||||||
pub fn load_svg_bytes_with_size(
|
pub fn load_svg_bytes_with_size(
|
||||||
svg_bytes: &[u8],
|
svg_bytes: &[u8],
|
||||||
size_hint: Option<SizeHint>,
|
size_hint: SizeHint,
|
||||||
options: &resvg::usvg::Options<'_>,
|
options: &resvg::usvg::Options<'_>,
|
||||||
) -> Result<egui::ColorImage, String> {
|
) -> Result<ColorImage, String> {
|
||||||
use egui::Vec2;
|
use egui::Vec2;
|
||||||
use resvg::{
|
use resvg::{
|
||||||
tiny_skia::Pixmap,
|
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 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 {
|
let scaled_size = match size_hint {
|
||||||
None => original_size,
|
SizeHint::Size {
|
||||||
Some(SizeHint::Size {
|
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
maintain_aspect_ratio,
|
maintain_aspect_ratio,
|
||||||
}) => {
|
} => {
|
||||||
if maintain_aspect_ratio {
|
if maintain_aspect_ratio {
|
||||||
// As large as possible, without exceeding the given size:
|
// As large as possible, without exceeding the given size:
|
||||||
let mut size = original_size;
|
let mut size = source_size;
|
||||||
size *= width as f32 / original_size.x;
|
size *= width as f32 / source_size.x;
|
||||||
if size.y > height as f32 {
|
if size.y > height as f32 {
|
||||||
size *= height as f32 / size.y;
|
size *= height as f32 / size.y;
|
||||||
}
|
}
|
||||||
|
|
@ -286,9 +306,9 @@ pub fn load_svg_bytes_with_size(
|
||||||
Vec2::new(width as _, height as _)
|
Vec2::new(width as _, height as _)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(SizeHint::Height(h)) => original_size * (h as f32 / original_size.y),
|
SizeHint::Height(h) => source_size * (h as f32 / source_size.y),
|
||||||
Some(SizeHint::Width(w)) => original_size * (w as f32 / original_size.x),
|
SizeHint::Width(w) => source_size * (w as f32 / source_size.x),
|
||||||
Some(SizeHint::Scale(scale)) => scale.into_inner() * original_size,
|
SizeHint::Scale(scale) => scale.into_inner() * source_size,
|
||||||
};
|
};
|
||||||
|
|
||||||
let scaled_size = scaled_size.round();
|
let scaled_size = scaled_size.round();
|
||||||
|
|
@ -299,11 +319,12 @@ pub fn load_svg_bytes_with_size(
|
||||||
|
|
||||||
resvg::render(
|
resvg::render(
|
||||||
&rtree,
|
&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(),
|
&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)
|
Ok(image)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -75,12 +75,9 @@ impl ImageLoader for SvgLoader {
|
||||||
match ctx.try_load_bytes(uri) {
|
match ctx.try_load_bytes(uri) {
|
||||||
Ok(BytesPoll::Ready { bytes, .. }) => {
|
Ok(BytesPoll::Ready { bytes, .. }) => {
|
||||||
log::trace!("Started loading {uri:?}");
|
log::trace!("Started loading {uri:?}");
|
||||||
let result = crate::image::load_svg_bytes_with_size(
|
let result =
|
||||||
&bytes,
|
crate::image::load_svg_bytes_with_size(&bytes, size_hint, &self.options)
|
||||||
Some(size_hint),
|
.map(Arc::new);
|
||||||
&self.options,
|
|
||||||
)
|
|
||||||
.map(Arc::new);
|
|
||||||
|
|
||||||
log::trace!("Finished loading {uri:?}");
|
log::trace!("Finished loading {uri:?}");
|
||||||
bucket.insert(
|
bucket.insert(
|
||||||
|
|
|
||||||
|
|
@ -696,10 +696,7 @@ impl Painter {
|
||||||
for row in pixels.chunks_exact((w * 4) as usize).rev() {
|
for row in pixels.chunks_exact((w * 4) as usize).rev() {
|
||||||
flipped.extend_from_slice(bytemuck::cast_slice(row));
|
flipped.extend_from_slice(bytemuck::cast_slice(row));
|
||||||
}
|
}
|
||||||
egui::ColorImage {
|
egui::ColorImage::new([w as usize, h as usize], flipped)
|
||||||
size: [w as usize, h as usize],
|
|
||||||
pixels: flipped,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read_screen_rgb(&self, [w, h]: [u32; 2]) -> Vec<u8> {
|
pub fn read_screen_rgb(&self, [w, h]: [u32; 2]) -> Vec<u8> {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
use emath::Vec2;
|
||||||
|
|
||||||
use crate::{textures::TextureOptions, Color32};
|
use crate::{textures::TextureOptions, Color32};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
|
@ -47,18 +49,36 @@ impl ImageData {
|
||||||
#[derive(Clone, Default, PartialEq, Eq)]
|
#[derive(Clone, Default, PartialEq, Eq)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
pub struct ColorImage {
|
pub struct ColorImage {
|
||||||
/// width, height.
|
/// width, height in texels.
|
||||||
pub size: [usize; 2],
|
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.
|
/// The pixels, row by row, from top to bottom.
|
||||||
pub pixels: Vec<Color32>,
|
pub pixels: Vec<Color32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ColorImage {
|
impl ColorImage {
|
||||||
/// Create an image filled with the given color.
|
/// 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<Color32>) -> Self {
|
||||||
|
debug_assert!(
|
||||||
|
size[0] * size[1] == pixels.len(),
|
||||||
|
"size: {size:?}, pixels.len(): {}",
|
||||||
|
pixels.len()
|
||||||
|
);
|
||||||
Self {
|
Self {
|
||||||
size,
|
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]],
|
pixels: vec![color; size[0] * size[1]],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -105,7 +125,7 @@ impl ColorImage {
|
||||||
.chunks_exact(4)
|
.chunks_exact(4)
|
||||||
.map(|p| Color32::from_rgba_unmultiplied(p[0], p[1], p[2], p[3]))
|
.map(|p| Color32::from_rgba_unmultiplied(p[0], p[1], p[2], p[3]))
|
||||||
.collect();
|
.collect();
|
||||||
Self { size, pixels }
|
Self::new(size, pixels)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_rgba_premultiplied(size: [usize; 2], rgba: &[u8]) -> Self {
|
pub fn from_rgba_premultiplied(size: [usize; 2], rgba: &[u8]) -> Self {
|
||||||
|
|
@ -120,7 +140,7 @@ impl ColorImage {
|
||||||
.chunks_exact(4)
|
.chunks_exact(4)
|
||||||
.map(|p| Color32::from_rgba_premultiplied(p[0], p[1], p[2], p[3]))
|
.map(|p| Color32::from_rgba_premultiplied(p[0], p[1], p[2], p[3]))
|
||||||
.collect();
|
.collect();
|
||||||
Self { size, pixels }
|
Self::new(size, pixels)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a [`ColorImage`] from flat opaque gray data.
|
/// Create a [`ColorImage`] from flat opaque gray data.
|
||||||
|
|
@ -135,7 +155,7 @@ impl ColorImage {
|
||||||
gray.len()
|
gray.len()
|
||||||
);
|
);
|
||||||
let pixels = gray.iter().map(|p| Color32::from_gray(*p)).collect();
|
let pixels = gray.iter().map(|p| Color32::from_gray(*p)).collect();
|
||||||
Self { size, pixels }
|
Self::new(size, pixels)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Alternative method to `from_gray`.
|
/// Alternative method to `from_gray`.
|
||||||
|
|
@ -152,7 +172,7 @@ impl ColorImage {
|
||||||
size,
|
size,
|
||||||
pixels.len()
|
pixels.len()
|
||||||
);
|
);
|
||||||
Self { size, pixels }
|
Self::new(size, pixels)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A view of the underlying data as `&[u8]`
|
/// A view of the underlying data as `&[u8]`
|
||||||
|
|
@ -185,14 +205,14 @@ impl ColorImage {
|
||||||
.chunks_exact(3)
|
.chunks_exact(3)
|
||||||
.map(|p| Color32::from_rgb(p[0], p[1], p[2]))
|
.map(|p| Color32::from_rgb(p[0], p[1], p[2]))
|
||||||
.collect();
|
.collect();
|
||||||
Self { size, pixels }
|
Self::new(size, pixels)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An example color image, useful for tests.
|
/// An example color image, useful for tests.
|
||||||
pub fn example() -> Self {
|
pub fn example() -> Self {
|
||||||
let width = 128;
|
let width = 128;
|
||||||
let height = 64;
|
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 y in 0..height {
|
||||||
for x in 0..width {
|
for x in 0..width {
|
||||||
let h = x as f32 / width as f32;
|
let h = x as f32 / width as f32;
|
||||||
|
|
@ -205,6 +225,13 @@ impl ColorImage {
|
||||||
img
|
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]
|
#[inline]
|
||||||
pub fn width(&self) -> usize {
|
pub fn width(&self) -> usize {
|
||||||
self.size[0]
|
self.size[0]
|
||||||
|
|
@ -242,10 +269,7 @@ impl ColorImage {
|
||||||
&self.pixels[row * row_stride + min_x..row * row_stride + max_x],
|
&self.pixels[row * row_stride + min_x..row * row_stride + max_x],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Self {
|
Self::new([width, height], output)
|
||||||
size: [width, height],
|
|
||||||
pixels: output,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue