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