1use std::fs;
2use std::path::PathBuf;
3use std::sync::Arc;
4
5use crate::{
6 point, px, size, AbsoluteLength, Asset, Bounds, DefiniteLength, DevicePixels, Element,
7 ElementContext, Hitbox, ImageData, InteractiveElement, Interactivity, IntoElement, LayoutId,
8 Length, Pixels, SharedUri, Size, StyleRefinement, Styled, SvgSize, UriOrPath, WindowContext,
9};
10use futures::{AsyncReadExt, Future};
11use image::{ImageBuffer, ImageError};
12#[cfg(target_os = "macos")]
13use media::core_video::CVImageBuffer;
14
15use thiserror::Error;
16use util::{http, ResultExt};
17
18/// A source of image content.
19#[derive(Clone, Debug)]
20pub enum ImageSource {
21 /// Image content will be loaded from provided URI at render time.
22 Uri(SharedUri),
23 /// Image content will be loaded from the provided file at render time.
24 File(Arc<PathBuf>),
25 /// Cached image data
26 Data(Arc<ImageData>),
27 // TODO: move surface definitions into mac platform module
28 /// A CoreVideo image buffer
29 #[cfg(target_os = "macos")]
30 Surface(CVImageBuffer),
31}
32
33impl From<SharedUri> for ImageSource {
34 fn from(value: SharedUri) -> Self {
35 Self::Uri(value)
36 }
37}
38
39impl From<&'static str> for ImageSource {
40 fn from(uri: &'static str) -> Self {
41 Self::Uri(uri.into())
42 }
43}
44
45impl From<String> for ImageSource {
46 fn from(uri: String) -> Self {
47 Self::Uri(uri.into())
48 }
49}
50
51impl From<Arc<PathBuf>> for ImageSource {
52 fn from(value: Arc<PathBuf>) -> Self {
53 Self::File(value)
54 }
55}
56
57impl From<PathBuf> for ImageSource {
58 fn from(value: PathBuf) -> Self {
59 Self::File(value.into())
60 }
61}
62
63impl From<Arc<ImageData>> for ImageSource {
64 fn from(value: Arc<ImageData>) -> Self {
65 Self::Data(value)
66 }
67}
68
69#[cfg(target_os = "macos")]
70impl From<CVImageBuffer> for ImageSource {
71 fn from(value: CVImageBuffer) -> Self {
72 Self::Surface(value)
73 }
74}
75
76/// An image element.
77pub struct Img {
78 interactivity: Interactivity,
79 source: ImageSource,
80 grayscale: bool,
81 object_fit: ObjectFit,
82}
83
84/// Create a new image element.
85pub fn img(source: impl Into<ImageSource>) -> Img {
86 Img {
87 interactivity: Interactivity::default(),
88 source: source.into(),
89 grayscale: false,
90 object_fit: ObjectFit::Contain,
91 }
92}
93
94/// How to fit the image into the bounds of the element.
95pub enum ObjectFit {
96 /// The image will be stretched to fill the bounds of the element.
97 Fill,
98 /// The image will be scaled to fit within the bounds of the element.
99 Contain,
100 /// The image will be scaled to cover the bounds of the element.
101 Cover,
102 /// The image will maintain its original size.
103 None,
104}
105
106impl ObjectFit {
107 /// Get the bounds of the image within the given bounds.
108 pub fn get_bounds(
109 &self,
110 bounds: Bounds<Pixels>,
111 image_size: Size<DevicePixels>,
112 ) -> Bounds<Pixels> {
113 let image_size = image_size.map(|dimension| Pixels::from(u32::from(dimension)));
114 let image_ratio = image_size.width / image_size.height;
115 let bounds_ratio = bounds.size.width / bounds.size.height;
116
117 match self {
118 ObjectFit::Fill => bounds,
119 ObjectFit::Contain => {
120 let new_size = if bounds_ratio > image_ratio {
121 size(
122 image_size.width * (bounds.size.height / image_size.height),
123 bounds.size.height,
124 )
125 } else {
126 size(
127 bounds.size.width,
128 image_size.height * (bounds.size.width / image_size.width),
129 )
130 };
131
132 Bounds {
133 origin: point(
134 bounds.origin.x + (bounds.size.width - new_size.width) / 2.0,
135 bounds.origin.y + (bounds.size.height - new_size.height) / 2.0,
136 ),
137 size: new_size,
138 }
139 }
140 ObjectFit::Cover => {
141 let new_size = if bounds_ratio > image_ratio {
142 size(
143 bounds.size.width,
144 image_size.height * (bounds.size.width / image_size.width),
145 )
146 } else {
147 size(
148 image_size.width * (bounds.size.height / image_size.height),
149 bounds.size.height,
150 )
151 };
152
153 Bounds {
154 origin: point(
155 bounds.origin.x + (bounds.size.width - new_size.width) / 2.0,
156 bounds.origin.y + (bounds.size.height - new_size.height) / 2.0,
157 ),
158 size: new_size,
159 }
160 }
161 ObjectFit::None => Bounds {
162 origin: bounds.origin,
163 size: image_size,
164 },
165 }
166 }
167}
168
169impl Img {
170 /// A list of all format extensions currently supported by this img element
171 pub fn extensions() -> &'static [&'static str] {
172 // This is the list in [image::ImageFormat::from_extension] + `svg`
173 &[
174 "avif", "jpg", "jpeg", "png", "gif", "webp", "tif", "tiff", "tga", "dds", "bmp", "ico",
175 "hdr", "exr", "pbm", "pam", "ppm", "pgm", "ff", "farbfeld", "qoi", "svg",
176 ]
177 }
178
179 /// Set the image to be displayed in grayscale.
180 pub fn grayscale(mut self, grayscale: bool) -> Self {
181 self.grayscale = grayscale;
182 self
183 }
184 /// Set the object fit for the image.
185 pub fn object_fit(mut self, object_fit: ObjectFit) -> Self {
186 self.object_fit = object_fit;
187 self
188 }
189}
190
191impl Element for Img {
192 type BeforeLayout = ();
193 type AfterLayout = Option<Hitbox>;
194
195 fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) {
196 let layout_id = self.interactivity.before_layout(cx, |mut style, cx| {
197 if let Some(data) = self.source.data(cx) {
198 let image_size = data.size();
199 match (style.size.width, style.size.height) {
200 (Length::Auto, Length::Auto) => {
201 style.size = Size {
202 width: Length::Definite(DefiniteLength::Absolute(
203 AbsoluteLength::Pixels(px(image_size.width.0 as f32)),
204 )),
205 height: Length::Definite(DefiniteLength::Absolute(
206 AbsoluteLength::Pixels(px(image_size.height.0 as f32)),
207 )),
208 }
209 }
210 _ => {}
211 }
212 }
213
214 cx.request_layout(&style, [])
215 });
216 (layout_id, ())
217 }
218
219 fn after_layout(
220 &mut self,
221 bounds: Bounds<Pixels>,
222 _before_layout: &mut Self::BeforeLayout,
223 cx: &mut ElementContext,
224 ) -> Option<Hitbox> {
225 self.interactivity
226 .after_layout(bounds, bounds.size, cx, |_, _, hitbox, _| hitbox)
227 }
228
229 fn paint(
230 &mut self,
231 bounds: Bounds<Pixels>,
232 _: &mut Self::BeforeLayout,
233 hitbox: &mut Self::AfterLayout,
234 cx: &mut ElementContext,
235 ) {
236 let source = self.source.clone();
237 self.interactivity
238 .paint(bounds, hitbox.as_ref(), cx, |style, cx| {
239 let corner_radii = style.corner_radii.to_pixels(bounds.size, cx.rem_size());
240
241 if let Some(data) = source.data(cx) {
242 let new_bounds = self.object_fit.get_bounds(bounds, data.size());
243 cx.paint_image(new_bounds, corner_radii, data.clone(), self.grayscale)
244 .log_err();
245 }
246
247 match source {
248 #[cfg(target_os = "macos")]
249 ImageSource::Surface(surface) => {
250 let size = size(surface.width().into(), surface.height().into());
251 let new_bounds = self.object_fit.get_bounds(bounds, size);
252 // TODO: Add support for corner_radii and grayscale.
253 cx.paint_surface(new_bounds, surface);
254 }
255 _ => {}
256 }
257 })
258 }
259}
260
261impl IntoElement for Img {
262 type Element = Self;
263
264 fn into_element(self) -> Self::Element {
265 self
266 }
267}
268
269impl Styled for Img {
270 fn style(&mut self) -> &mut StyleRefinement {
271 &mut self.interactivity.base_style
272 }
273}
274
275impl InteractiveElement for Img {
276 fn interactivity(&mut self) -> &mut Interactivity {
277 &mut self.interactivity
278 }
279}
280
281impl ImageSource {
282 fn data(&self, cx: &mut ElementContext) -> Option<Arc<ImageData>> {
283 match self {
284 ImageSource::Uri(_) | ImageSource::File(_) => {
285 let uri_or_path: UriOrPath = match self {
286 ImageSource::Uri(uri) => uri.clone().into(),
287 ImageSource::File(path) => path.clone().into(),
288 _ => unreachable!(),
289 };
290
291 cx.use_cached_asset::<Image>(&uri_or_path)?.log_err()
292 }
293
294 ImageSource::Data(data) => Some(data.to_owned()),
295 #[cfg(target_os = "macos")]
296 ImageSource::Surface(_) => None,
297 }
298 }
299}
300
301#[derive(Clone)]
302enum Image {}
303
304impl Asset for Image {
305 type Source = UriOrPath;
306 type Output = Result<Arc<ImageData>, ImageCacheError>;
307
308 fn load(
309 source: Self::Source,
310 cx: &mut WindowContext,
311 ) -> impl Future<Output = Self::Output> + Send + 'static {
312 let client = cx.http_client();
313 let scale_factor = cx.scale_factor();
314 let svg_renderer = cx.svg_renderer();
315 async move {
316 let bytes = match source.clone() {
317 UriOrPath::Path(uri) => fs::read(uri.as_ref())?,
318 UriOrPath::Uri(uri) => {
319 let mut response = client.get(uri.as_ref(), ().into(), true).await?;
320 let mut body = Vec::new();
321 response.body_mut().read_to_end(&mut body).await?;
322 if !response.status().is_success() {
323 return Err(ImageCacheError::BadStatus {
324 status: response.status(),
325 body: String::from_utf8_lossy(&body).into_owned(),
326 });
327 }
328 body
329 }
330 };
331
332 let data = if let Ok(format) = image::guess_format(&bytes) {
333 let data = image::load_from_memory_with_format(&bytes, format)?.into_bgra8();
334 ImageData::new(data)
335 } else {
336 let pixmap =
337 svg_renderer.render_pixmap(&bytes, SvgSize::ScaleFactor(scale_factor))?;
338
339 let buffer =
340 ImageBuffer::from_raw(pixmap.width(), pixmap.height(), pixmap.take()).unwrap();
341
342 ImageData::new(buffer)
343 };
344
345 Ok(Arc::new(data))
346 }
347 }
348}
349
350/// An error that can occur when interacting with the image cache.
351#[derive(Debug, Error, Clone)]
352pub enum ImageCacheError {
353 /// An error that occurred while fetching an image from a remote source.
354 #[error("http error: {0}")]
355 Client(#[from] http::Error),
356 /// An error that occurred while reading the image from disk.
357 #[error("IO error: {0}")]
358 Io(Arc<std::io::Error>),
359 /// An error that occurred while processing an image.
360 #[error("unexpected http status: {status}, body: {body}")]
361 BadStatus {
362 /// The HTTP status code.
363 status: http::StatusCode,
364 /// The HTTP response body.
365 body: String,
366 },
367 /// An error that occurred while processing an image.
368 #[error("image error: {0}")]
369 Image(Arc<ImageError>),
370 /// An error that occurred while processing an SVG.
371 #[error("svg error: {0}")]
372 Usvg(Arc<usvg::Error>),
373}
374
375impl From<std::io::Error> for ImageCacheError {
376 fn from(error: std::io::Error) -> Self {
377 Self::Io(Arc::new(error))
378 }
379}
380
381impl From<ImageError> for ImageCacheError {
382 fn from(error: ImageError) -> Self {
383 Self::Image(Arc::new(error))
384 }
385}
386
387impl From<usvg::Error> for ImageCacheError {
388 fn from(error: usvg::Error) -> Self {
389 Self::Usvg(Arc::new(error))
390 }
391}