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