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 FilterMode,
21 MipmapMode,
22 Paint,
23 SamplingOptions,
24 SkData,
25 SkImage,
26 SkRect,
27 raster_n32_premul,
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 let encoded = SkImage::from_encoded(unsafe { SkData::new_bytes(&bytes) })
174 .context("Failed to decode Image.")?;
175 let image = match decode_size.and_then(|t| Self::downsample(&encoded, t)) {
176 Some(scaled) => scaled,
177 None => encoded.make_raster_image(None, None).unwrap_or(encoded),
178 };
179 Ok((image, bytes))
180 })
181 .await
182 }
183
184 fn downsample(encoded: &SkImage, target: DecodeSize) -> Option<SkImage> {
185 let natural_width = encoded.width() as f32;
186 let natural_height = encoded.height() as f32;
187 let target_width = target.width as f32;
188 let target_height = target.height as f32;
189 if natural_width <= target_width && natural_height <= target_height {
190 return None;
191 }
192 let ratio = (target_width / natural_width).min(target_height / natural_height);
193 let width = (natural_width * ratio).round().max(1.);
194 let height = (natural_height * ratio).round().max(1.);
195
196 let mut surface = raster_n32_premul((width as i32, height as i32))?;
197 let destination = SkRect::from_xywh(0., 0., width, height);
198 let sampling = SamplingOptions::new(FilterMode::Linear, MipmapMode::Linear);
199 let mut paint = Paint::default();
200 paint.set_anti_alias(true);
201 surface.canvas().draw_image_rect_with_sampling_options(
202 encoded,
203 None,
204 destination,
205 sampling,
206 &paint,
207 );
208 Some(surface.image_snapshot())
209 }
210}
211
212#[derive(Default, Clone, Debug, PartialEq, Copy)]
214pub enum DecodeMode {
215 #[default]
217 FromLayout,
218 Source,
220 Custom(Size2D),
222}
223
224impl DecodeMode {
225 fn resolve(&self, layout: &LayoutData, scale_factor: f64) -> Option<DecodeSize> {
226 let scale = scale_factor as f32;
227 let size = match self {
228 Self::Source => return None,
229 Self::FromLayout => match (&layout.width, &layout.height) {
230 (Size::Pixels(width), Size::Pixels(height)) => {
231 Size2D::new(width.get() * scale, height.get() * scale)
232 }
233 _ => {
234 tracing::debug!("DecodeMode::FromLayout decoded at natural size.");
235 return None;
236 }
237 },
238 Self::Custom(size) => *size,
239 };
240 Some(DecodeSize::new(
241 size.width.round().max(1.) as u32,
242 size.height.round().max(1.) as u32,
243 ))
244 }
245}
246
247#[cfg_attr(feature = "docs",
276 doc = embed_doc_image::embed_image!("image_viewer", "images/gallery_image_viewer.png")
277)]
278#[derive(PartialEq)]
279pub struct ImageViewer {
280 source: ImageSource,
281 asset_age: AssetAge,
282
283 layout: LayoutData,
284 image_data: ImageData,
285 accessibility: AccessibilityData,
286 effect: EffectData,
287 corner_radius: Option<CornerRadius>,
288 decode_mode: DecodeMode,
289
290 children: Vec<Element>,
291 loading_placeholder: Option<Element>,
292 error_renderer: Option<Callback<String, Element>>,
293
294 key: DiffKey,
295}
296
297impl ImageViewer {
298 pub fn new(source: impl Into<ImageSource>) -> Self {
299 ImageViewer {
300 source: source.into(),
301 asset_age: AssetAge::default(),
302 layout: LayoutData::default(),
303 image_data: ImageData::default(),
304 accessibility: AccessibilityData::default(),
305 effect: EffectData::default(),
306 corner_radius: None,
307 decode_mode: DecodeMode::default(),
308 children: Vec::new(),
309 loading_placeholder: None,
310 error_renderer: None,
311 key: DiffKey::None,
312 }
313 }
314}
315
316impl KeyExt for ImageViewer {
317 fn write_key(&mut self) -> &mut DiffKey {
318 &mut self.key
319 }
320}
321
322impl LayoutExt for ImageViewer {
323 fn get_layout(&mut self) -> &mut LayoutData {
324 &mut self.layout
325 }
326}
327
328impl ContainerSizeExt for ImageViewer {}
329impl ContainerWithContentExt for ImageViewer {}
330
331impl ImageExt for ImageViewer {
332 fn get_image_data(&mut self) -> &mut ImageData {
333 &mut self.image_data
334 }
335}
336
337impl AccessibilityExt for ImageViewer {
338 fn get_accessibility_data(&mut self) -> &mut AccessibilityData {
339 &mut self.accessibility
340 }
341}
342
343impl ChildrenExt for ImageViewer {
344 fn get_children(&mut self) -> &mut Vec<Element> {
345 &mut self.children
346 }
347}
348
349impl EffectExt for ImageViewer {
350 fn get_effect(&mut self) -> &mut EffectData {
351 &mut self.effect
352 }
353}
354
355impl ImageViewer {
356 pub fn corner_radius(mut self, corner_radius: impl Into<CornerRadius>) -> Self {
357 self.corner_radius = Some(corner_radius.into());
358 self
359 }
360
361 pub fn loading_placeholder(mut self, placeholder: impl Into<Element>) -> Self {
363 self.loading_placeholder = Some(placeholder.into());
364 self
365 }
366
367 pub fn decode_mode(mut self, decode_mode: DecodeMode) -> Self {
369 self.decode_mode = decode_mode;
370 self
371 }
372
373 pub fn asset_age(mut self, asset_age: impl Into<AssetAge>) -> Self {
377 self.asset_age = asset_age.into();
378 self
379 }
380
381 pub fn error_renderer(mut self, renderer: impl Into<Callback<String, Element>>) -> Self {
383 self.error_renderer = Some(renderer.into());
384 self
385 }
386}
387
388impl Component for ImageViewer {
389 fn render(&self) -> impl IntoElement {
390 let target = self
391 .decode_mode
392 .resolve(&self.layout, *Platform::get().scale_factor.read());
393 let asset_config = AssetConfiguration::new((&self.source, target), self.asset_age);
394 let asset = use_asset(&asset_config);
395 let mut asset_cacher = use_hook(AssetCacher::get);
396
397 use_side_effect_with_deps(
398 &(self.source.clone(), asset_config, target),
399 move |(source, asset_config, target)| {
400 if matches!(
401 asset_cacher.read_asset(asset_config),
402 Some(Asset::Pending) | Some(Asset::Error(_))
403 ) {
404 asset_cacher.update_asset(asset_config.clone(), Asset::Loading);
405
406 let source = source.clone();
407 let asset_config = asset_config.clone();
408 let target = *target;
409 spawn_forever(async move {
410 match source.bytes(target).await {
411 Ok((image, bytes)) => {
412 let image_holder = ImageHolder {
414 bytes,
415 image: Rc::new(RefCell::new(image)),
416 };
417 asset_cacher.update_asset(
418 asset_config,
419 Asset::Cached(Rc::new(image_holder)),
420 );
421 }
422 Err(err) => {
423 asset_cacher
425 .update_asset(asset_config, Asset::Error(err.to_string()));
426 }
427 }
428 });
429 }
430 },
431 );
432
433 match asset {
434 Asset::Cached(asset) => {
435 let asset = asset.downcast_ref::<ImageHolder>().unwrap().clone();
436 image(asset)
437 .accessibility(self.accessibility.clone())
438 .a11y_role(AccessibilityRole::Image)
439 .layout(self.layout.clone())
440 .image_data(self.image_data.clone())
441 .effect(self.effect.clone())
442 .children(self.children.clone())
443 .map(self.corner_radius, |img, corner_radius| {
444 img.corner_radius(corner_radius)
445 })
446 .into_element()
447 }
448 Asset::Pending | Asset::Loading => rect()
449 .layout(self.layout.clone())
450 .center()
451 .child(
452 self.loading_placeholder
453 .clone()
454 .unwrap_or_else(|| CircularLoader::new().into_element()),
455 )
456 .into(),
457 Asset::Error(err) => match &self.error_renderer {
458 Some(renderer) => renderer.call(err),
459 None => err.into(),
460 },
461 }
462 }
463
464 fn render_key(&self) -> DiffKey {
465 self.key.clone().or(self.default_key())
466 }
467}