1use crate::EmbedResourcesKind;
5use crate::diagnostics::BuildDiagnostics;
6use crate::embedded_resources::*;
7use crate::expression_tree::{Expression, ImageReference};
8use crate::object_tree::*;
9#[cfg(feature = "software-renderer")]
10use image::GenericImageView;
11use smol_str::SmolStr;
12use std::cell::RefCell;
13use std::collections::HashMap;
14use std::future::Future;
15use std::pin::Pin;
16use std::rc::Rc;
17use typed_index_collections::TiVec;
18
19pub async fn embed_images(
20 doc: &Document,
21 embed_files: EmbedResourcesKind,
22 scale_factor: f32,
23 resource_url_mapper: &Option<Rc<dyn Fn(&str) -> Pin<Box<dyn Future<Output = Option<String>>>>>>,
24 diag: &mut BuildDiagnostics,
25) {
26 if embed_files == EmbedResourcesKind::Nothing && resource_url_mapper.is_none() {
27 return;
28 }
29
30 let global_embedded_resources = &doc.embedded_file_resources;
31 let mut path_to_id = HashMap::<SmolStr, EmbeddedResourcesIdx>::new();
32
33 let mut all_components = Vec::new();
34 doc.visit_all_used_components(|c| all_components.push(c.clone()));
35 let all_components = all_components;
36
37 let mapped_urls = {
38 let mut urls = HashMap::<SmolStr, Option<SmolStr>>::new();
39
40 if let Some(mapper) = resource_url_mapper {
41 for component in &all_components {
43 visit_all_expressions(component, |e, _| {
44 collect_image_urls_from_expression(e, &mut urls)
45 });
46 }
47
48 for i in urls.iter_mut() {
50 *i.1 = (*mapper)(i.0).await.map(SmolStr::new);
51 }
52 }
53
54 urls
55 };
56
57 for component in &all_components {
59 visit_all_expressions(component, |e, _| {
60 embed_images_from_expression(
61 e,
62 &mapped_urls,
63 global_embedded_resources,
64 &mut path_to_id,
65 embed_files,
66 scale_factor,
67 diag,
68 )
69 });
70 }
71}
72
73fn collect_image_urls_from_expression(
74 e: &Expression,
75 urls: &mut HashMap<SmolStr, Option<SmolStr>>,
76) {
77 if let Expression::ImageReference { resource_ref, .. } = e
78 && let ImageReference::AbsolutePath(path) = resource_ref
79 {
80 urls.insert(path.clone(), None);
81 };
82
83 e.visit(|e| collect_image_urls_from_expression(e, urls));
84}
85
86fn embed_images_from_expression(
87 e: &mut Expression,
88 urls: &HashMap<SmolStr, Option<SmolStr>>,
89 global_embedded_resources: &RefCell<TiVec<EmbeddedResourcesIdx, EmbeddedResources>>,
90 path_to_id: &mut HashMap<SmolStr, EmbeddedResourcesIdx>,
91 embed_files: EmbedResourcesKind,
92 scale_factor: f32,
93 diag: &mut BuildDiagnostics,
94) {
95 if let Expression::ImageReference { resource_ref, source_location, nine_slice: _ } = e
96 && let ImageReference::AbsolutePath(path) = resource_ref
97 {
98 if path.starts_with("data:") {
99 let image_ref = embed_data_uri(
100 global_embedded_resources,
101 path,
102 embed_files,
103 scale_factor,
104 diag,
105 source_location,
106 );
107 *resource_ref = image_ref;
108 return;
109 }
110
111 let mapped_path =
113 urls.get(path).unwrap_or(&Some(path.clone())).clone().unwrap_or(path.clone());
114 *path = mapped_path;
115 if embed_files != EmbedResourcesKind::Nothing
116 && (embed_files != EmbedResourcesKind::OnlyBuiltinResources
117 || path.starts_with("builtin:/"))
118 {
119 let image_ref = embed_image(
120 global_embedded_resources,
121 path_to_id,
122 embed_files,
123 path,
124 scale_factor,
125 diag,
126 source_location,
127 );
128 if embed_files != EmbedResourcesKind::ListAllResources {
129 *resource_ref = image_ref;
130 }
131 }
132 };
133
134 e.visit_mut(|e| {
135 embed_images_from_expression(
136 e,
137 urls,
138 global_embedded_resources,
139 path_to_id,
140 embed_files,
141 scale_factor,
142 diag,
143 )
144 });
145}
146
147fn embed_image(
148 global_embedded_resources: &RefCell<TiVec<EmbeddedResourcesIdx, EmbeddedResources>>,
149 path_to_id: &mut HashMap<SmolStr, EmbeddedResourcesIdx>,
150 embed_files: EmbedResourcesKind,
151 path: &str,
152 _scale_factor: f32,
153 diag: &mut BuildDiagnostics,
154 source_location: &Option<crate::diagnostics::SourceLocation>,
155) -> ImageReference {
156 let extension = || {
157 std::path::Path::new(path)
158 .extension()
159 .and_then(|e| e.to_str())
160 .map(|x| x.to_string())
161 .unwrap_or_default()
162 };
163
164 if let Some(&resource_id) = path_to_id.get(path) {
165 return match global_embedded_resources.borrow()[resource_id].kind {
166 #[cfg(feature = "software-renderer")]
167 EmbeddedResourcesKind::TextureData { .. } => {
168 ImageReference::EmbeddedTexture { resource_id }
169 }
170 _ => ImageReference::EmbeddedData { resource_id, extension: extension() },
171 };
172 }
173
174 let mut resources = global_embedded_resources.borrow_mut();
175 let mut push = |kind| {
176 let id = resources.push_and_get_key(EmbeddedResources { path: Some(path.into()), kind });
177 path_to_id.insert(path.into(), id);
178 id
179 };
180
181 if embed_files == EmbedResourcesKind::ListAllResources {
182 push(EmbeddedResourcesKind::ListOnly);
183 return ImageReference::None;
184 }
185
186 let Some(_file) = crate::fileaccess::load_file(std::path::Path::new(path)) else {
187 diag.push_error(format!("Cannot find image file {path}"), source_location);
188 return ImageReference::None;
189 };
190
191 #[cfg(feature = "software-renderer")]
192 if embed_files == EmbedResourcesKind::EmbedTextures {
193 return match load_image(_file, _scale_factor) {
194 Ok((img, source_format, original_size)) => {
195 let resource_id = push(EmbeddedResourcesKind::TextureData(generate_texture(
196 img,
197 source_format,
198 original_size,
199 )));
200 ImageReference::EmbeddedTexture { resource_id }
201 }
202 Err(err) => {
203 diag.push_error(format!("Cannot load image file {path}: {err}"), source_location);
204 ImageReference::None
205 }
206 };
207 }
208
209 let resource_id = push(EmbeddedResourcesKind::FileData);
210 ImageReference::EmbeddedData { resource_id, extension: extension() }
211}
212
213#[cfg(feature = "software-renderer")]
214trait Pixel {
215 fn is_transparent(&self) -> bool;
218}
219#[cfg(feature = "software-renderer")]
220impl Pixel for image::Rgba<u8> {
221 fn is_transparent(&self) -> bool {
224 self[3] <= 1
225 }
226}
227
228#[cfg(feature = "software-renderer")]
229fn generate_texture(
230 image: image::RgbaImage,
231 source_format: SourceFormat,
232 original_size: Size,
233) -> Texture {
234 let mut top = 0;
236 let is_line_transparent = |y| {
237 for x in 0..image.width() {
238 if !image.get_pixel(x, y).is_transparent() {
239 return false;
240 }
241 }
242 true
243 };
244 while top < image.height() && is_line_transparent(top) {
245 top += 1;
246 }
247 if top == image.height() {
248 return Texture::new_empty();
249 }
250 let mut bottom = image.height() - 1;
251 while is_line_transparent(bottom) {
252 bottom -= 1;
253 assert!(bottom > top); }
255 let is_column_transparent = |x| {
256 for y in top..=bottom {
257 if !image.get_pixel(x, y).is_transparent() {
258 return false;
259 }
260 }
261 true
262 };
263 let mut left = 0;
264 while is_column_transparent(left) {
265 left += 1;
266 assert!(left < image.width()); }
268 let mut right = image.width() - 1;
269 while is_column_transparent(right) {
270 right -= 1;
271 assert!(right > left); }
273 let mut is_opaque = true;
274 enum ColorState {
275 Unset,
276 Different,
277 Rgb([u8; 3]),
278 }
279 let mut color = ColorState::Unset;
280 'outer: for y in top..=bottom {
281 for x in left..=right {
282 let p = image.get_pixel(x, y);
283 let alpha = p[3];
284 if alpha != 255 {
285 is_opaque = false;
286 }
287 if alpha == 0 {
288 continue;
289 }
290 let get_pixel = || match source_format {
291 SourceFormat::RgbaPremultiplied => <[u8; 3]>::try_from(&p.0[0..3])
292 .unwrap()
293 .map(|v| (v as u16 * 255 / alpha as u16) as u8),
294 SourceFormat::Rgba => p.0[0..3].try_into().unwrap(),
295 };
296 match color {
297 ColorState::Unset => {
298 color = ColorState::Rgb(get_pixel());
299 }
300 ColorState::Different => {
301 if !is_opaque {
302 break 'outer;
303 }
304 }
305 ColorState::Rgb([a, b, c]) => {
306 let px = get_pixel();
307 if a.abs_diff(px[0]) > 2 || b.abs_diff(px[1]) > 2 || c.abs_diff(px[2]) > 2 {
308 color = ColorState::Different
309 }
310 }
311 }
312 }
313 }
314
315 let format = if let ColorState::Rgb(c) = color {
316 PixelFormat::AlphaMap(c)
317 } else if is_opaque {
318 PixelFormat::Rgb
319 } else {
320 PixelFormat::RgbaPremultiplied
321 };
322
323 let rect = Rect::from_ltrb(left as _, top as _, (right + 1) as _, (bottom + 1) as _).unwrap();
324 Texture {
325 total_size: Size { width: image.width(), height: image.height() },
326 original_size,
327 rect,
328 data: convert_image(image, source_format, format, rect),
329 format,
330 }
331}
332
333#[cfg(feature = "software-renderer")]
334fn convert_image(
335 image: image::RgbaImage,
336 source_format: SourceFormat,
337 format: PixelFormat,
338 rect: Rect,
339) -> Vec<u8> {
340 let i = image::SubImage::new(&image, rect.x() as _, rect.y() as _, rect.width(), rect.height());
341 match (source_format, format) {
342 (_, PixelFormat::Rgb) => {
343 i.pixels().flat_map(|(_, _, p)| IntoIterator::into_iter(p.0).take(3)).collect()
344 }
345 (SourceFormat::RgbaPremultiplied, PixelFormat::RgbaPremultiplied)
346 | (SourceFormat::Rgba, PixelFormat::Rgba) => {
347 i.pixels().flat_map(|(_, _, p)| IntoIterator::into_iter(p.0)).collect()
348 }
349 (SourceFormat::Rgba, PixelFormat::RgbaPremultiplied) => i
350 .pixels()
351 .flat_map(|(_, _, p)| {
352 let a = p.0[3] as u32;
353 IntoIterator::into_iter(p.0)
354 .take(3)
355 .map(move |x| (x as u32 * a / 255) as u8)
356 .chain(std::iter::once(a as u8))
357 })
358 .collect(),
359 (SourceFormat::RgbaPremultiplied, PixelFormat::Rgba) => i
360 .pixels()
361 .flat_map(|(_, _, p)| {
362 let a = p.0[3] as u32;
363 IntoIterator::into_iter(p.0)
364 .take(3)
365 .map(move |x| (x as u32 * 255 / a) as u8)
366 .chain(std::iter::once(a as u8))
367 })
368 .collect(),
369 (_, PixelFormat::AlphaMap(_)) => i.pixels().map(|(_, _, p)| p[3]).collect(),
370 }
371}
372
373#[cfg(feature = "software-renderer")]
374enum SourceFormat {
375 RgbaPremultiplied,
376 Rgba,
377}
378
379#[cfg(feature = "software-renderer")]
380fn load_image_from_bytes(
381 data: &[u8],
382 extension: Option<&str>,
383 scale_factor: f32,
384) -> image::ImageResult<(image::RgbaImage, SourceFormat, Size)> {
385 use resvg::{tiny_skia, usvg};
386
387 let is_svg = matches!(extension, Some("svg") | Some("svgz"));
388
389 if is_svg {
390 let tree = {
391 let option = usvg::Options::default();
392 usvg::Tree::from_data(data, &option).map_err(|e| {
393 image::ImageError::Decoding(image::error::DecodingError::new(
394 image::error::ImageFormatHint::Name("svg".into()),
395 e,
396 ))
397 })?
398 };
399
400 let original_size = tree.size();
401 let width = (original_size.width() * scale_factor) as u32;
402 let height = (original_size.height() * scale_factor) as u32;
403
404 let mut buffer = vec![0u8; width as usize * height as usize * 4];
405
406 let size_error = || {
407 image::ImageError::Limits(image::error::LimitError::from_kind(
408 image::error::LimitErrorKind::DimensionError,
409 ))
410 };
411
412 let mut skia_buffer =
413 tiny_skia::PixmapMut::from_bytes(buffer.as_mut_slice(), width, height)
414 .ok_or_else(size_error)?;
415
416 resvg::render(
417 &tree,
418 tiny_skia::Transform::from_scale(scale_factor, scale_factor),
419 &mut skia_buffer,
420 );
421
422 return image::RgbaImage::from_raw(width, height, buffer).ok_or_else(size_error).map(
423 |img| {
424 (
425 img,
426 SourceFormat::RgbaPremultiplied,
427 Size { width: original_size.width() as _, height: original_size.height() as _ },
428 )
429 },
430 );
431 }
432
433 image::load_from_memory(data).map(|mut image| {
434 let (original_width, original_height) = image.dimensions();
435
436 if scale_factor < 1.0 {
437 image = image.resize_exact(
438 (original_width as f32 * scale_factor) as u32,
439 (original_height as f32 * scale_factor) as u32,
440 image::imageops::FilterType::Gaussian,
441 );
442 }
443
444 (
445 image.to_rgba8(),
446 SourceFormat::Rgba,
447 Size { width: original_width, height: original_height },
448 )
449 })
450}
451
452#[cfg(feature = "software-renderer")]
453fn load_image(
454 file: crate::fileaccess::VirtualFile,
455 scale_factor: f32,
456) -> image::ImageResult<(image::RgbaImage, SourceFormat, Size)> {
457 use std::ffi::OsStr;
458
459 let extension = file.canon_path.extension().and_then(OsStr::to_str);
460
461 let data = if let Some(buffer) = file.builtin_contents {
462 buffer.to_vec()
463 } else {
464 std::fs::read(&file.canon_path)?
465 };
466
467 load_image_from_bytes(&data, extension, scale_factor)
468}
469
470#[cfg(feature = "software-renderer")]
471fn load_image_from_data_uri(
472 decoded_data: &[u8],
473 extension: &str,
474 scale_factor: f32,
475) -> image::ImageResult<(image::RgbaImage, SourceFormat, Size)> {
476 load_image_from_bytes(decoded_data, Some(extension), scale_factor)
477}
478
479fn embed_data_uri(
480 global_embedded_resources: &RefCell<TiVec<EmbeddedResourcesIdx, EmbeddedResources>>,
481 data_uri: &str,
482 _embed_files: EmbedResourcesKind,
483 _scale_factor: f32,
484 diag: &mut BuildDiagnostics,
485 source_location: &Option<crate::diagnostics::SourceLocation>,
486) -> ImageReference {
487 let (decoded_data, extension) = match crate::data_uri::decode_data_uri(data_uri) {
488 Ok(result) => result,
489 Err(e) => {
490 diag.push_error(e, source_location);
491 return ImageReference::None;
492 }
493 };
494
495 let mut resources = global_embedded_resources.borrow_mut();
496
497 #[cfg(feature = "software-renderer")]
498 if _embed_files == EmbedResourcesKind::EmbedTextures {
499 let data_buffer = decoded_data.clone();
500 match load_image_from_data_uri(&data_buffer, &extension, _scale_factor)
501 .map_err(|e| e.to_string())
502 {
503 Ok((img, source_format, original_size)) => {
504 let resource_id = resources.push_and_get_key(EmbeddedResources {
505 path: None,
506 kind: EmbeddedResourcesKind::TextureData(generate_texture(
507 img,
508 source_format,
509 original_size,
510 )),
511 });
512 return ImageReference::EmbeddedTexture { resource_id };
513 }
514 Err(err) => {
515 diag.push_error(format!("Cannot load data URI image: {err}"), source_location);
516 return ImageReference::None;
517 }
518 }
519 }
520
521 let resource_id = resources.push_and_get_key(EmbeddedResources {
522 path: None,
523 kind: EmbeddedResourcesKind::DataUriPayload(decoded_data, extension.clone()),
524 });
525
526 ImageReference::EmbeddedData { resource_id, extension }
527}