mirror of
https://github.com/slint-ui/slint.git
synced 2025-08-12 06:38:50 +00:00
443 lines
15 KiB
Rust
443 lines
15 KiB
Rust
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
|
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
|
|
|
use crate::diagnostics::BuildDiagnostics;
|
|
use crate::embedded_resources::*;
|
|
use crate::expression_tree::{Expression, ImageReference};
|
|
use crate::object_tree::*;
|
|
use crate::EmbedResourcesKind;
|
|
#[cfg(feature = "software-renderer")]
|
|
use image::GenericImageView;
|
|
use smol_str::SmolStr;
|
|
use std::cell::RefCell;
|
|
use std::collections::{BTreeMap, HashMap};
|
|
use std::future::Future;
|
|
use std::pin::Pin;
|
|
use std::rc::Rc;
|
|
|
|
pub async fn embed_images(
|
|
doc: &Document,
|
|
embed_files: EmbedResourcesKind,
|
|
scale_factor: f64,
|
|
resource_url_mapper: &Option<Rc<dyn Fn(&str) -> Pin<Box<dyn Future<Output = Option<String>>>>>>,
|
|
diag: &mut BuildDiagnostics,
|
|
) {
|
|
if embed_files == EmbedResourcesKind::Nothing && resource_url_mapper.is_none() {
|
|
return;
|
|
}
|
|
|
|
let global_embedded_resources = &doc.embedded_file_resources;
|
|
|
|
let mut all_components = Vec::new();
|
|
doc.visit_all_used_components(|c| all_components.push(c.clone()));
|
|
let all_components = all_components;
|
|
|
|
let mapped_urls = {
|
|
let mut urls = HashMap::<SmolStr, Option<SmolStr>>::new();
|
|
|
|
if let Some(mapper) = resource_url_mapper {
|
|
// Collect URLs (sync!):
|
|
for component in &all_components {
|
|
visit_all_expressions(component, |e, _| {
|
|
collect_image_urls_from_expression(e, &mut urls)
|
|
});
|
|
}
|
|
|
|
// Map URLs (async -- well, not really):
|
|
for i in urls.iter_mut() {
|
|
*i.1 = (*mapper)(i.0).await.map(SmolStr::new);
|
|
}
|
|
}
|
|
|
|
urls
|
|
};
|
|
|
|
// Use URLs (sync!):
|
|
for component in &all_components {
|
|
visit_all_expressions(component, |e, _| {
|
|
embed_images_from_expression(
|
|
e,
|
|
&mapped_urls,
|
|
global_embedded_resources,
|
|
embed_files,
|
|
scale_factor,
|
|
diag,
|
|
)
|
|
});
|
|
}
|
|
}
|
|
|
|
fn collect_image_urls_from_expression(
|
|
e: &Expression,
|
|
urls: &mut HashMap<SmolStr, Option<SmolStr>>,
|
|
) {
|
|
if let Expression::ImageReference { ref resource_ref, .. } = e {
|
|
if let ImageReference::AbsolutePath(path) = resource_ref {
|
|
urls.insert(path.clone(), None);
|
|
}
|
|
};
|
|
|
|
e.visit(|e| collect_image_urls_from_expression(e, urls));
|
|
}
|
|
|
|
fn embed_images_from_expression(
|
|
e: &mut Expression,
|
|
urls: &HashMap<SmolStr, Option<SmolStr>>,
|
|
global_embedded_resources: &RefCell<BTreeMap<SmolStr, EmbeddedResources>>,
|
|
embed_files: EmbedResourcesKind,
|
|
scale_factor: f64,
|
|
diag: &mut BuildDiagnostics,
|
|
) {
|
|
if let Expression::ImageReference { ref mut resource_ref, source_location, nine_slice: _ } = e {
|
|
if let ImageReference::AbsolutePath(path) = resource_ref {
|
|
// used mapped path:
|
|
let mapped_path =
|
|
urls.get(path).unwrap_or(&Some(path.clone())).clone().unwrap_or(path.clone());
|
|
*path = mapped_path;
|
|
if embed_files != EmbedResourcesKind::Nothing
|
|
&& (embed_files != EmbedResourcesKind::OnlyBuiltinResources
|
|
|| path.starts_with("builtin:/"))
|
|
{
|
|
let image_ref = embed_image(
|
|
global_embedded_resources,
|
|
embed_files,
|
|
path,
|
|
scale_factor,
|
|
diag,
|
|
source_location,
|
|
);
|
|
if embed_files != EmbedResourcesKind::ListAllResources {
|
|
*resource_ref = image_ref;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
e.visit_mut(|e| {
|
|
embed_images_from_expression(
|
|
e,
|
|
urls,
|
|
global_embedded_resources,
|
|
embed_files,
|
|
scale_factor,
|
|
diag,
|
|
)
|
|
});
|
|
}
|
|
|
|
fn embed_image(
|
|
global_embedded_resources: &RefCell<BTreeMap<SmolStr, EmbeddedResources>>,
|
|
embed_files: EmbedResourcesKind,
|
|
path: &str,
|
|
_scale_factor: f64,
|
|
diag: &mut BuildDiagnostics,
|
|
source_location: &Option<crate::diagnostics::SourceLocation>,
|
|
) -> ImageReference {
|
|
let mut resources = global_embedded_resources.borrow_mut();
|
|
let maybe_id = resources.len();
|
|
let e = match resources.entry(path.into()) {
|
|
std::collections::btree_map::Entry::Occupied(e) => e.into_mut(),
|
|
std::collections::btree_map::Entry::Vacant(e) => {
|
|
// Check that the file exists, so that later we can unwrap safely in the generators, etc.
|
|
if embed_files == EmbedResourcesKind::ListAllResources {
|
|
// Really do nothing with the image!
|
|
e.insert(EmbeddedResources { id: maybe_id, kind: EmbeddedResourcesKind::ListOnly });
|
|
return ImageReference::None;
|
|
} else if let Some(_file) = crate::fileaccess::load_file(std::path::Path::new(path)) {
|
|
#[allow(unused_mut)]
|
|
let mut kind = EmbeddedResourcesKind::RawData;
|
|
#[cfg(feature = "software-renderer")]
|
|
if embed_files == EmbedResourcesKind::EmbedTextures {
|
|
match load_image(_file, _scale_factor) {
|
|
Ok((img, source_format, original_size)) => {
|
|
kind = EmbeddedResourcesKind::TextureData(generate_texture(
|
|
img,
|
|
source_format,
|
|
original_size,
|
|
))
|
|
}
|
|
Err(err) => {
|
|
diag.push_error(
|
|
format!("Cannot load image file {path}: {err}"),
|
|
source_location,
|
|
);
|
|
return ImageReference::None;
|
|
}
|
|
}
|
|
}
|
|
e.insert(EmbeddedResources { id: maybe_id, kind })
|
|
} else {
|
|
diag.push_error(format!("Cannot find image file {path}"), source_location);
|
|
return ImageReference::None;
|
|
}
|
|
}
|
|
};
|
|
|
|
match e.kind {
|
|
#[cfg(feature = "software-renderer")]
|
|
EmbeddedResourcesKind::TextureData { .. } => {
|
|
ImageReference::EmbeddedTexture { resource_id: e.id }
|
|
}
|
|
_ => ImageReference::EmbeddedData {
|
|
resource_id: e.id,
|
|
extension: std::path::Path::new(path)
|
|
.extension()
|
|
.and_then(|e| e.to_str())
|
|
.map(|x| x.to_string())
|
|
.unwrap_or_default(),
|
|
},
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "software-renderer")]
|
|
trait Pixel {
|
|
//fn alpha(&self) -> f32;
|
|
//fn rgb(&self) -> (u8, u8, u8);
|
|
fn is_transparent(&self) -> bool;
|
|
}
|
|
#[cfg(feature = "software-renderer")]
|
|
impl Pixel for image::Rgba<u8> {
|
|
/*fn alpha(&self) -> f32 { self[3] as f32 / 255. }
|
|
fn rgb(&self) -> (u8, u8, u8) { (self[0], self[1], self[2]) }*/
|
|
fn is_transparent(&self) -> bool {
|
|
self[3] <= 1
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "software-renderer")]
|
|
fn generate_texture(
|
|
image: image::RgbaImage,
|
|
source_format: SourceFormat,
|
|
original_size: Size,
|
|
) -> Texture {
|
|
// Analyze each pixels
|
|
let mut top = 0;
|
|
let is_line_transparent = |y| {
|
|
for x in 0..image.width() {
|
|
if !image.get_pixel(x, y).is_transparent() {
|
|
return false;
|
|
}
|
|
}
|
|
true
|
|
};
|
|
while top < image.height() && is_line_transparent(top) {
|
|
top += 1;
|
|
}
|
|
if top == image.height() {
|
|
return Texture::new_empty();
|
|
}
|
|
let mut bottom = image.height() - 1;
|
|
while is_line_transparent(bottom) {
|
|
bottom -= 1;
|
|
assert!(bottom > top); // otherwise we would have a transparent image
|
|
}
|
|
let is_column_transparent = |x| {
|
|
for y in top..=bottom {
|
|
if !image.get_pixel(x, y).is_transparent() {
|
|
return false;
|
|
}
|
|
}
|
|
true
|
|
};
|
|
let mut left = 0;
|
|
while is_column_transparent(left) {
|
|
left += 1;
|
|
assert!(left < image.width()); // otherwise we would have a transparent image
|
|
}
|
|
let mut right = image.width() - 1;
|
|
while is_column_transparent(right) {
|
|
right -= 1;
|
|
assert!(right > left); // otherwise we would have a transparent image
|
|
}
|
|
let mut is_opaque = true;
|
|
enum ColorState {
|
|
Unset,
|
|
Different,
|
|
Rgb([u8; 3]),
|
|
}
|
|
let mut color = ColorState::Unset;
|
|
'outer: for y in top..=bottom {
|
|
for x in left..=right {
|
|
let p = image.get_pixel(x, y);
|
|
let alpha = p[3];
|
|
if alpha != 255 {
|
|
is_opaque = false;
|
|
}
|
|
if alpha == 0 {
|
|
continue;
|
|
}
|
|
let get_pixel = || match source_format {
|
|
SourceFormat::RgbaPremultiplied => <[u8; 3]>::try_from(&p.0[0..3])
|
|
.unwrap()
|
|
.map(|v| (v as u16 * 255 / alpha as u16) as u8),
|
|
SourceFormat::Rgba => p.0[0..3].try_into().unwrap(),
|
|
};
|
|
match color {
|
|
ColorState::Unset => {
|
|
color = ColorState::Rgb(get_pixel());
|
|
}
|
|
ColorState::Different => {
|
|
if !is_opaque {
|
|
break 'outer;
|
|
}
|
|
}
|
|
ColorState::Rgb([a, b, c]) => {
|
|
let abs_diff = |t, u| {
|
|
if t < u {
|
|
u - t
|
|
} else {
|
|
t - u
|
|
}
|
|
};
|
|
let px = get_pixel();
|
|
if abs_diff(a, px[0]) > 2 || abs_diff(b, px[1]) > 2 || abs_diff(c, px[2]) > 2 {
|
|
color = ColorState::Different
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let format = if let ColorState::Rgb(c) = color {
|
|
PixelFormat::AlphaMap(c)
|
|
} else if is_opaque {
|
|
PixelFormat::Rgb
|
|
} else {
|
|
PixelFormat::RgbaPremultiplied
|
|
};
|
|
|
|
let rect = Rect::from_ltrb(left as _, top as _, (right + 1) as _, (bottom + 1) as _).unwrap();
|
|
Texture {
|
|
total_size: Size { width: image.width(), height: image.height() },
|
|
original_size,
|
|
rect,
|
|
data: convert_image(image, source_format, format, rect),
|
|
format,
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "software-renderer")]
|
|
fn convert_image(
|
|
image: image::RgbaImage,
|
|
source_format: SourceFormat,
|
|
format: PixelFormat,
|
|
rect: Rect,
|
|
) -> Vec<u8> {
|
|
let i = image::SubImage::new(&image, rect.x() as _, rect.y() as _, rect.width(), rect.height());
|
|
match (source_format, format) {
|
|
(_, PixelFormat::Rgb) => {
|
|
i.pixels().flat_map(|(_, _, p)| IntoIterator::into_iter(p.0).take(3)).collect()
|
|
}
|
|
(SourceFormat::RgbaPremultiplied, PixelFormat::RgbaPremultiplied)
|
|
| (SourceFormat::Rgba, PixelFormat::Rgba) => {
|
|
i.pixels().flat_map(|(_, _, p)| IntoIterator::into_iter(p.0)).collect()
|
|
}
|
|
(SourceFormat::Rgba, PixelFormat::RgbaPremultiplied) => i
|
|
.pixels()
|
|
.flat_map(|(_, _, p)| {
|
|
let a = p.0[3] as u32;
|
|
IntoIterator::into_iter(p.0)
|
|
.take(3)
|
|
.map(move |x| (x as u32 * a / 255) as u8)
|
|
.chain(std::iter::once(a as u8))
|
|
})
|
|
.collect(),
|
|
(SourceFormat::RgbaPremultiplied, PixelFormat::Rgba) => i
|
|
.pixels()
|
|
.flat_map(|(_, _, p)| {
|
|
let a = p.0[3] as u32;
|
|
IntoIterator::into_iter(p.0)
|
|
.take(3)
|
|
.map(move |x| (x as u32 * 255 / a) as u8)
|
|
.chain(std::iter::once(a as u8))
|
|
})
|
|
.collect(),
|
|
(_, PixelFormat::AlphaMap(_)) => i.pixels().map(|(_, _, p)| p[3]).collect(),
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "software-renderer")]
|
|
enum SourceFormat {
|
|
RgbaPremultiplied,
|
|
Rgba,
|
|
}
|
|
|
|
#[cfg(feature = "software-renderer")]
|
|
fn load_image(
|
|
file: crate::fileaccess::VirtualFile,
|
|
scale_factor: f64,
|
|
) -> image::ImageResult<(image::RgbaImage, SourceFormat, Size)> {
|
|
use resvg::{tiny_skia, usvg};
|
|
use std::ffi::OsStr;
|
|
if file.canon_path.extension() == Some(OsStr::new("svg"))
|
|
|| file.canon_path.extension() == Some(OsStr::new("svgz"))
|
|
{
|
|
let tree = i_slint_common::sharedfontdb::FONT_DB.with_borrow(|db| {
|
|
let option = usvg::Options { fontdb: (*db).clone(), ..Default::default() };
|
|
match file.builtin_contents {
|
|
Some(data) => usvg::Tree::from_data(data, &option),
|
|
None => usvg::Tree::from_data(
|
|
std::fs::read(&file.canon_path).map_err(image::ImageError::IoError)?.as_slice(),
|
|
&option,
|
|
),
|
|
}
|
|
.map_err(|e| {
|
|
image::ImageError::Decoding(image::error::DecodingError::new(
|
|
image::error::ImageFormatHint::Name("svg".into()),
|
|
e,
|
|
))
|
|
})
|
|
})?;
|
|
let scale_factor = scale_factor as f32;
|
|
// TODO: ideally we should find the size used for that `Image`
|
|
let original_size = tree.size();
|
|
let width = original_size.width() * scale_factor;
|
|
let height = original_size.height() * scale_factor;
|
|
|
|
let mut buffer = vec![0u8; width as usize * height as usize * 4];
|
|
let size_error = || {
|
|
image::ImageError::Limits(image::error::LimitError::from_kind(
|
|
image::error::LimitErrorKind::DimensionError,
|
|
))
|
|
};
|
|
let mut skia_buffer =
|
|
tiny_skia::PixmapMut::from_bytes(buffer.as_mut_slice(), width as u32, height as u32)
|
|
.ok_or_else(size_error)?;
|
|
resvg::render(
|
|
&tree,
|
|
tiny_skia::Transform::from_scale(scale_factor as _, scale_factor as _),
|
|
&mut skia_buffer,
|
|
);
|
|
return image::RgbaImage::from_raw(width as u32, height as u32, buffer)
|
|
.ok_or_else(size_error)
|
|
.map(|img| {
|
|
(
|
|
img,
|
|
SourceFormat::RgbaPremultiplied,
|
|
Size { width: original_size.width() as _, height: original_size.height() as _ },
|
|
)
|
|
});
|
|
}
|
|
if let Some(buffer) = file.builtin_contents {
|
|
image::load_from_memory(buffer)
|
|
} else {
|
|
image::open(file.canon_path)
|
|
}
|
|
.map(|mut image| {
|
|
let (original_width, original_height) = image.dimensions();
|
|
|
|
if scale_factor < 1. {
|
|
image = image.resize_exact(
|
|
(original_width as f64 * scale_factor) as u32,
|
|
(original_height as f64 * scale_factor) as u32,
|
|
image::imageops::FilterType::Gaussian,
|
|
);
|
|
}
|
|
|
|
(
|
|
image.to_rgba8(),
|
|
SourceFormat::Rgba,
|
|
Size { width: original_width, height: original_height },
|
|
)
|
|
})
|
|
}
|