1use std::{
2 collections::hash_map::DefaultHasher,
3 fs,
4 hash::{
5 Hash,
6 Hasher,
7 },
8 path::PathBuf,
9 rc::Rc,
10 sync::LazyLock,
11};
12
13use anyhow::Context;
14use async_lock::Semaphore;
15use bytes::Bytes;
16use freya_core::{
17 elements::image::*,
18 prelude::*,
19};
20use freya_engine::prelude::{
21 Paint,
22 SkData,
23 SkImage,
24 SkRect,
25 raster_n32_premul,
26};
27use torin::prelude::{
28 Size,
29 Size2D,
30};
31#[cfg(feature = "remote-asset")]
32use ureq::http::Uri;
33
34use crate::{
35 cache::*,
36 loader::CircularLoader,
37};
38
39#[derive(PartialEq, Clone)]
90pub enum ImageSource {
91 #[cfg(feature = "remote-asset")]
95 Uri(Uri),
96
97 Path(PathBuf),
98
99 Bytes(u64, Bytes),
100}
101
102impl<H: Hash> From<(H, Bytes)> for ImageSource {
103 fn from((id, bytes): (H, Bytes)) -> Self {
104 let mut hasher = DefaultHasher::default();
105 id.hash(&mut hasher);
106 Self::Bytes(hasher.finish(), bytes)
107 }
108}
109
110impl<H: Hash> From<(H, &'static [u8])> for ImageSource {
111 fn from((id, bytes): (H, &'static [u8])) -> Self {
112 (id, Bytes::from_static(bytes)).into()
113 }
114}
115
116impl<const N: usize, H: Hash> From<(H, &'static [u8; N])> for ImageSource {
117 fn from((id, bytes): (H, &'static [u8; N])) -> Self {
118 (id, Bytes::from_static(bytes)).into()
119 }
120}
121
122#[cfg_attr(feature = "docs", doc(cfg(feature = "remote-asset")))]
123#[cfg(feature = "remote-asset")]
124impl From<Uri> for ImageSource {
125 fn from(uri: Uri) -> Self {
126 Self::Uri(uri)
127 }
128}
129
130#[cfg_attr(feature = "docs", doc(cfg(feature = "remote-asset")))]
131#[cfg(feature = "remote-asset")]
132impl From<&'static str> for ImageSource {
133 fn from(src: &'static str) -> Self {
134 Self::Uri(Uri::from_static(src))
135 }
136}
137
138impl From<PathBuf> for ImageSource {
139 fn from(path: PathBuf) -> Self {
140 Self::Path(path)
141 }
142}
143
144impl Hash for ImageSource {
145 fn hash<H: Hasher>(&self, state: &mut H) {
146 match self {
147 #[cfg(feature = "remote-asset")]
148 Self::Uri(uri) => uri.hash(state),
149 Self::Path(path) => path.hash(state),
150 Self::Bytes(id, _) => id.hash(state),
151 }
152 }
153}
154
155pub type DecodeSize = euclid::Size2D<u32, ()>;
156
157static DECODE_LIMIT: LazyLock<Semaphore> = LazyLock::new(|| Semaphore::new(4));
159
160impl ImageSource {
161 pub async fn load(
163 &self,
164 decode_size: Option<DecodeSize>,
165 sampling_mode: SamplingMode,
166 ) -> anyhow::Result<(SkImage, Bytes)> {
167 let source = self.clone();
168 let _decode_permit = DECODE_LIMIT.acquire().await;
169 blocking::unblock(move || {
170 let bytes = match source {
171 #[cfg(feature = "remote-asset")]
172 Self::Uri(uri) => ureq::get(uri)
173 .call()?
174 .body_mut()
175 .read_to_vec()
176 .map(Bytes::from)?,
177 Self::Path(path) => fs::read(path).map(Bytes::from)?,
178 Self::Bytes(_, bytes) => bytes,
179 };
180 let image = SkImage::from_encoded(unsafe { SkData::new_bytes(&bytes) })
181 .context("Failed to decode Image.")?;
182 let image = image.make_raster_image(None, None).unwrap_or(image);
183 let image = decode_size
184 .and_then(|target| Self::downsample(&image, target, &sampling_mode))
185 .unwrap_or(image);
186 Ok((image, bytes))
187 })
188 .await
189 }
190
191 fn downsample(
192 encoded: &SkImage,
193 target: DecodeSize,
194 sampling_mode: &SamplingMode,
195 ) -> Option<SkImage> {
196 let natural_width = encoded.width() as f32;
197 let natural_height = encoded.height() as f32;
198 let target_width = target.width as f32;
199 let target_height = target.height as f32;
200 if natural_width <= target_width && natural_height <= target_height {
201 return None;
202 }
203 let ratio = (target_width / natural_width).min(target_height / natural_height);
204 let width = (natural_width * ratio).round().max(1.);
205 let height = (natural_height * ratio).round().max(1.);
206
207 let mut surface = raster_n32_premul((width as i32, height as i32))?;
208 let destination = SkRect::from_xywh(0., 0., width, height);
209 let sampling = sampling_mode.sampling_options();
210 let mut paint = Paint::default();
211 paint.set_anti_alias(true);
212 surface.canvas().draw_image_rect_with_sampling_options(
213 encoded,
214 None,
215 destination,
216 sampling,
217 &paint,
218 );
219 Some(surface.image_snapshot())
220 }
221}
222
223#[derive(Default, Clone, Debug, PartialEq, Copy)]
225pub enum DecodeMode {
226 #[default]
229 FromLayout,
230 Source,
232 Custom(Size2D),
234}
235
236impl DecodeMode {
237 fn resolve(&self, layout: &LayoutData, scale_factor: f64) -> Option<DecodeSize> {
238 let scale = scale_factor as f32;
239 let size = match self {
240 Self::Source => return None,
241 Self::FromLayout => match (&layout.width, &layout.height) {
242 (Size::Pixels(width), Size::Pixels(height)) => {
243 Size2D::new(width.get() * scale, height.get() * scale)
244 }
245 _ => return None,
246 },
247 Self::Custom(size) => *size,
248 };
249 Some(DecodeSize::new(
250 size.width.round().max(1.) as u32,
251 size.height.round().max(1.) as u32,
252 ))
253 }
254}
255
256#[cfg_attr(feature = "docs",
285 doc = embed_doc_image::embed_image!("image_viewer", "images/gallery_image_viewer.png")
286)]
287#[derive(PartialEq)]
288pub struct ImageViewer {
289 source: ImageSource,
290 asset_age: AssetAge,
291
292 layout: LayoutData,
293 image_data: ImageData,
294 accessibility: AccessibilityData,
295 effect: EffectData,
296 corner_radius: Option<CornerRadius>,
297 decode_mode: DecodeMode,
298
299 children: Vec<Element>,
300 loading_placeholder: Option<Element>,
301 error_renderer: Option<Callback<String, Element>>,
302
303 key: DiffKey,
304}
305
306impl ImageViewer {
307 pub fn new(source: impl Into<ImageSource>) -> Self {
308 ImageViewer {
309 source: source.into(),
310 asset_age: AssetAge::default(),
311 layout: LayoutData::default(),
312 image_data: ImageData::default(),
313 accessibility: AccessibilityData::default(),
314 effect: EffectData::default(),
315 corner_radius: None,
316 decode_mode: DecodeMode::default(),
317 children: Vec::new(),
318 loading_placeholder: None,
319 error_renderer: None,
320 key: DiffKey::None,
321 }
322 }
323}
324
325impl KeyExt for ImageViewer {
326 fn write_key(&mut self) -> &mut DiffKey {
327 &mut self.key
328 }
329}
330
331impl LayoutExt for ImageViewer {
332 fn get_layout(&mut self) -> &mut LayoutData {
333 &mut self.layout
334 }
335}
336
337impl ContainerSizeExt for ImageViewer {}
338impl ContainerWithContentExt for ImageViewer {}
339impl ContainerPositionExt for ImageViewer {}
340
341impl ImageExt for ImageViewer {
342 fn get_image_data(&mut self) -> &mut ImageData {
343 &mut self.image_data
344 }
345}
346
347impl AccessibilityExt for ImageViewer {
348 fn get_accessibility_data(&mut self) -> &mut AccessibilityData {
349 &mut self.accessibility
350 }
351}
352
353impl ChildrenExt for ImageViewer {
354 fn get_children(&mut self) -> &mut Vec<Element> {
355 &mut self.children
356 }
357}
358
359impl EffectExt for ImageViewer {
360 fn get_effect(&mut self) -> &mut EffectData {
361 &mut self.effect
362 }
363}
364
365impl ImageViewer {
366 pub fn corner_radius(mut self, corner_radius: impl Into<CornerRadius>) -> Self {
367 self.corner_radius = Some(corner_radius.into());
368 self
369 }
370
371 pub fn loading_placeholder(mut self, placeholder: impl Into<Element>) -> Self {
373 self.loading_placeholder = Some(placeholder.into());
374 self
375 }
376
377 pub fn decode_mode(mut self, decode_mode: DecodeMode) -> Self {
379 self.decode_mode = decode_mode;
380 self
381 }
382
383 pub fn asset_age(mut self, asset_age: impl Into<AssetAge>) -> Self {
387 self.asset_age = asset_age.into();
388 self
389 }
390
391 pub fn error_renderer(mut self, renderer: impl Into<Callback<String, Element>>) -> Self {
393 self.error_renderer = Some(renderer.into());
394 self
395 }
396}
397
398impl Component for ImageViewer {
399 fn render(&self) -> impl IntoElement {
400 let target = self
401 .decode_mode
402 .resolve(&self.layout, *Platform::get().scale_factor.read());
403 let sampling_mode = self.image_data.sampling_mode.clone();
404 let asset_config =
405 AssetConfiguration::new((&self.source, target, &sampling_mode), self.asset_age);
406 let asset = use_asset(&asset_config);
407 let mut asset_cacher = use_hook(AssetCacher::get);
408
409 use_side_effect_with_deps(
410 &(self.source.clone(), asset_config, target, sampling_mode),
411 move |(source, asset_config, target, sampling_mode)| {
412 if matches!(
413 asset_cacher.read_asset(asset_config),
414 Some(Asset::Pending) | Some(Asset::Error(_))
415 ) {
416 asset_cacher.update_asset(asset_config.clone(), Asset::Loading);
417
418 let source = source.clone();
419 let asset_config = asset_config.clone();
420 let target = *target;
421 let sampling_mode = sampling_mode.clone();
422 spawn_forever(async move {
423 match source.load(target, sampling_mode).await {
424 Ok((image, bytes)) => {
425 asset_cacher.update_asset(
426 asset_config,
427 Asset::Cached(Rc::new(ImageHandle::new(image, bytes))),
428 );
429 }
430 Err(err) => {
431 asset_cacher
432 .update_asset(asset_config, Asset::Error(err.to_string()));
433 }
434 }
435 });
436 }
437 },
438 );
439
440 match asset {
441 Asset::Cached(asset) => {
442 let asset = asset.downcast_ref::<ImageHandle>().unwrap().clone();
443 image(asset)
444 .accessibility(self.accessibility.clone())
445 .a11y_role(AccessibilityRole::Image)
446 .layout(self.layout.clone())
447 .image_data(self.image_data.clone())
448 .effect(self.effect.clone())
449 .children(self.children.clone())
450 .map(self.corner_radius, |img, corner_radius| {
451 img.corner_radius(corner_radius)
452 })
453 .into_element()
454 }
455 Asset::Pending | Asset::Loading => rect()
456 .layout(self.layout.clone())
457 .center()
458 .child(
459 self.loading_placeholder
460 .clone()
461 .unwrap_or_else(|| CircularLoader::new().into_element()),
462 )
463 .into(),
464 Asset::Error(err) => match &self.error_renderer {
465 Some(renderer) => renderer.call(err),
466 None => err.into(),
467 },
468 }
469 }
470
471 fn render_key(&self) -> DiffKey {
472 self.key.clone().or(self.default_key())
473 }
474}