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};
27#[cfg(feature = "remote-asset")]
28use reqwest::{
29 Url,
30 blocking::Client,
31};
32use torin::prelude::{
33 Size,
34 Size2D,
35};
36
37#[cfg(feature = "remote-asset")]
38use crate::http::Http;
39use crate::{
40 cache::*,
41 loader::CircularLoader,
42};
43
44#[derive(PartialEq, Clone)]
95pub enum ImageSource {
96 #[cfg(feature = "remote-asset")]
100 Uri(Url),
101
102 Path(PathBuf),
103
104 Bytes(u64, Bytes),
105}
106
107impl<H: Hash> From<(H, Bytes)> for ImageSource {
108 fn from((id, bytes): (H, Bytes)) -> Self {
109 let mut hasher = DefaultHasher::default();
110 id.hash(&mut hasher);
111 Self::Bytes(hasher.finish(), bytes)
112 }
113}
114
115impl<H: Hash> From<(H, &'static [u8])> for ImageSource {
116 fn from((id, bytes): (H, &'static [u8])) -> Self {
117 (id, Bytes::from_static(bytes)).into()
118 }
119}
120
121impl<const N: usize, H: Hash> From<(H, &'static [u8; N])> for ImageSource {
122 fn from((id, bytes): (H, &'static [u8; N])) -> Self {
123 (id, Bytes::from_static(bytes)).into()
124 }
125}
126
127impl From<Bytes> for ImageSource {
128 fn from(bytes: Bytes) -> Self {
129 let mut hasher = DefaultHasher::default();
130 bytes.hash(&mut hasher);
131 Self::Bytes(hasher.finish(), bytes)
132 }
133}
134
135impl From<&'static [u8]> for ImageSource {
136 fn from(bytes: &'static [u8]) -> Self {
137 Bytes::from_static(bytes).into()
138 }
139}
140
141impl<const N: usize> From<&'static [u8; N]> for ImageSource {
142 fn from(bytes: &'static [u8; N]) -> Self {
143 Bytes::from_static(bytes).into()
144 }
145}
146
147#[cfg_attr(feature = "docs", doc(cfg(feature = "remote-asset")))]
148#[cfg(feature = "remote-asset")]
149impl From<Url> for ImageSource {
150 fn from(uri: Url) -> Self {
151 Self::Uri(uri)
152 }
153}
154
155#[cfg_attr(feature = "docs", doc(cfg(feature = "remote-asset")))]
156#[cfg(feature = "remote-asset")]
157impl From<&'static str> for ImageSource {
158 fn from(src: &'static str) -> Self {
159 Self::Uri(Url::parse(src).expect("Invalid URL"))
160 }
161}
162
163impl From<PathBuf> for ImageSource {
164 fn from(path: PathBuf) -> Self {
165 Self::Path(path)
166 }
167}
168
169impl Hash for ImageSource {
170 fn hash<H: Hasher>(&self, state: &mut H) {
171 match self {
172 #[cfg(feature = "remote-asset")]
173 Self::Uri(uri) => uri.hash(state),
174 Self::Path(path) => path.hash(state),
175 Self::Bytes(id, _) => id.hash(state),
176 }
177 }
178}
179
180pub type DecodeSize = euclid::Size2D<u32, ()>;
181
182static DECODE_LIMIT: LazyLock<Semaphore> = LazyLock::new(|| Semaphore::new(4));
184
185impl ImageSource {
186 pub(crate) fn fetch(
188 self,
189 #[cfg(feature = "remote-asset")] client: &Client,
190 ) -> anyhow::Result<Bytes> {
191 Ok(match self {
192 #[cfg(feature = "remote-asset")]
193 Self::Uri(uri) => client.get(uri).send()?.error_for_status()?.bytes()?,
194 Self::Path(path) => fs::read(path).map(Bytes::from)?,
195 Self::Bytes(_, bytes) => bytes,
196 })
197 }
198
199 pub async fn load(
201 &self,
202 decode_size: Option<DecodeSize>,
203 sampling_mode: SamplingMode,
204 ) -> anyhow::Result<(SkImage, Bytes)> {
205 let source = self.clone();
206 #[cfg(feature = "remote-asset")]
207 let client = Http::get();
208 let _decode_permit = DECODE_LIMIT.acquire().await;
209 blocking::unblock(move || {
210 #[cfg(feature = "remote-asset")]
211 let bytes = source.fetch(&client)?;
212 #[cfg(not(feature = "remote-asset"))]
213 let bytes = source.fetch()?;
214 let image = SkImage::from_encoded(unsafe { SkData::new_bytes(&bytes) })
215 .context("Failed to decode Image.")?;
216 let image = image.make_raster_image(None, None).unwrap_or(image);
217 let image = decode_size
218 .and_then(|target| Self::downsample(&image, target, &sampling_mode))
219 .unwrap_or(image);
220 Ok((image, bytes))
221 })
222 .await
223 }
224
225 fn downsample(
226 encoded: &SkImage,
227 target: DecodeSize,
228 sampling_mode: &SamplingMode,
229 ) -> Option<SkImage> {
230 let natural_width = encoded.width() as f32;
231 let natural_height = encoded.height() as f32;
232 let target_width = target.width as f32;
233 let target_height = target.height as f32;
234 if natural_width <= target_width && natural_height <= target_height {
235 return None;
236 }
237 let ratio = (target_width / natural_width).min(target_height / natural_height);
238 let width = (natural_width * ratio).round().max(1.);
239 let height = (natural_height * ratio).round().max(1.);
240
241 let mut surface = raster_n32_premul((width as i32, height as i32))?;
242 let destination = SkRect::from_xywh(0., 0., width, height);
243 let sampling = sampling_mode.sampling_options();
244 let mut paint = Paint::default();
245 paint.set_anti_alias(true);
246 surface.canvas().draw_image_rect_with_sampling_options(
247 encoded,
248 None,
249 destination,
250 sampling,
251 &paint,
252 );
253 Some(surface.image_snapshot())
254 }
255}
256
257#[derive(Default, Clone, Debug, PartialEq, Copy)]
259pub enum DecodeMode {
260 #[default]
263 FromLayout,
264 Source,
266 Custom(Size2D),
268}
269
270impl DecodeMode {
271 fn resolve(&self, layout: &LayoutData, scale_factor: f64) -> Option<DecodeSize> {
272 let scale = scale_factor as f32;
273 let size = match self {
274 Self::Source => return None,
275 Self::FromLayout => match (&layout.width, &layout.height) {
276 (Size::Pixels(width), Size::Pixels(height)) => {
277 Size2D::new(width.get() * scale, height.get() * scale)
278 }
279 _ => return None,
280 },
281 Self::Custom(size) => *size,
282 };
283 Some(DecodeSize::new(
284 size.width.round().max(1.) as u32,
285 size.height.round().max(1.) as u32,
286 ))
287 }
288}
289
290#[cfg_attr(feature = "docs",
319 doc = embed_doc_image::embed_image!("image_viewer", "images/gallery_image_viewer.png")
320)]
321#[derive(PartialEq)]
322pub struct ImageViewer {
323 source: ImageSource,
324 asset_age: AssetAge,
325
326 layout: LayoutData,
327 image_data: ImageData,
328 accessibility: AccessibilityData,
329 effect: EffectData,
330 corner_radius: Option<CornerRadius>,
331 decode_mode: DecodeMode,
332
333 children: Vec<Element>,
334 loading_placeholder: Option<Element>,
335 error_renderer: Option<Callback<String, Element>>,
336
337 key: DiffKey,
338}
339
340impl ImageViewer {
341 pub fn new(source: impl Into<ImageSource>) -> Self {
342 ImageViewer {
343 source: source.into(),
344 asset_age: AssetAge::default(),
345 layout: LayoutData::default(),
346 image_data: ImageData::default(),
347 accessibility: AccessibilityData::default(),
348 effect: EffectData::default(),
349 corner_radius: None,
350 decode_mode: DecodeMode::default(),
351 children: Vec::new(),
352 loading_placeholder: None,
353 error_renderer: None,
354 key: DiffKey::None,
355 }
356 }
357}
358
359impl KeyExt for ImageViewer {
360 fn write_key(&mut self) -> &mut DiffKey {
361 &mut self.key
362 }
363}
364
365impl LayoutExt for ImageViewer {
366 fn get_layout(&mut self) -> &mut LayoutData {
367 &mut self.layout
368 }
369}
370
371impl ContainerSizeExt for ImageViewer {}
372impl ContainerWithContentExt for ImageViewer {}
373impl ContainerPositionExt for ImageViewer {}
374
375impl ImageExt for ImageViewer {
376 fn get_image_data(&mut self) -> &mut ImageData {
377 &mut self.image_data
378 }
379}
380
381impl AccessibilityExt for ImageViewer {
382 fn get_accessibility_data(&mut self) -> &mut AccessibilityData {
383 &mut self.accessibility
384 }
385}
386
387impl ChildrenExt for ImageViewer {
388 fn get_children(&mut self) -> &mut Vec<Element> {
389 &mut self.children
390 }
391}
392
393impl EffectExt for ImageViewer {
394 fn get_effect(&mut self) -> &mut EffectData {
395 &mut self.effect
396 }
397}
398
399impl ImageViewer {
400 pub fn corner_radius(mut self, corner_radius: impl Into<CornerRadius>) -> Self {
401 self.corner_radius = Some(corner_radius.into());
402 self
403 }
404
405 pub fn loading_placeholder(mut self, placeholder: impl Into<Element>) -> Self {
407 self.loading_placeholder = Some(placeholder.into());
408 self
409 }
410
411 pub fn decode_mode(mut self, decode_mode: DecodeMode) -> Self {
413 self.decode_mode = decode_mode;
414 self
415 }
416
417 pub fn asset_age(mut self, asset_age: impl Into<AssetAge>) -> Self {
421 self.asset_age = asset_age.into();
422 self
423 }
424
425 pub fn error_renderer(mut self, renderer: impl Into<Callback<String, Element>>) -> Self {
427 self.error_renderer = Some(renderer.into());
428 self
429 }
430}
431
432impl Component for ImageViewer {
433 fn render(&self) -> impl IntoElement {
434 let target = self
435 .decode_mode
436 .resolve(&self.layout, *Platform::get().scale_factor.read());
437 let sampling_mode = self.image_data.sampling_mode.clone();
438 let asset_config =
439 AssetConfiguration::new((&self.source, target, &sampling_mode), self.asset_age);
440 let asset = use_asset(&asset_config);
441 let mut asset_cacher = use_hook(AssetCacher::get);
442
443 use_side_effect_with_deps(
444 &(self.source.clone(), asset_config, target, sampling_mode),
445 move |(source, asset_config, target, sampling_mode)| {
446 if matches!(
447 asset_cacher.read_asset(asset_config),
448 Some(Asset::Pending) | Some(Asset::Error(_))
449 ) {
450 asset_cacher.update_asset(asset_config.clone(), Asset::Loading);
451
452 let source = source.clone();
453 let asset_config = asset_config.clone();
454 let target = *target;
455 let sampling_mode = sampling_mode.clone();
456 spawn_forever(async move {
457 match source.load(target, sampling_mode).await {
458 Ok((image, bytes)) => {
459 asset_cacher.update_asset(
460 asset_config,
461 Asset::Cached(Rc::new(ImageHandle::new(image, bytes))),
462 );
463 }
464 Err(err) => {
465 asset_cacher
466 .update_asset(asset_config, Asset::Error(err.to_string()));
467 }
468 }
469 });
470 }
471 },
472 );
473
474 match asset {
475 Asset::Cached(asset) => {
476 let asset = asset.downcast_ref::<ImageHandle>().unwrap().clone();
477 image(asset)
478 .accessibility(self.accessibility.clone())
479 .a11y_role(AccessibilityRole::Image)
480 .layout(self.layout.clone())
481 .image_data(self.image_data.clone())
482 .effect(self.effect.clone())
483 .children(self.children.clone())
484 .map(self.corner_radius, |img, corner_radius| {
485 img.corner_radius(corner_radius)
486 })
487 .into_element()
488 }
489 Asset::Pending | Asset::Loading => rect()
490 .layout(self.layout.clone())
491 .center()
492 .child(
493 self.loading_placeholder
494 .clone()
495 .unwrap_or_else(|| CircularLoader::new().into_element()),
496 )
497 .into(),
498 Asset::Error(err) => match &self.error_renderer {
499 Some(renderer) => renderer.call(err),
500 None => err.into(),
501 },
502 }
503 }
504
505 fn render_key(&self) -> DiffKey {
506 self.key.clone().or(self.default_key())
507 }
508}