1 | // Copyright © SixtyFPS GmbH <info@slint.dev> |
2 | // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial |
3 | |
4 | /*! |
5 | This module contains image decoding and caching related types for the run-time library. |
6 | */ |
7 | |
8 | use crate::lengths::{PhysicalPx, ScaleFactor}; |
9 | use crate::slice::Slice; |
10 | use crate::{SharedString, SharedVector}; |
11 | |
12 | use super::{IntRect, IntSize}; |
13 | use crate::items::{ImageFit, ImageHorizontalAlignment, ImageTiling, ImageVerticalAlignment}; |
14 | |
15 | #[cfg (feature = "image-decoders" )] |
16 | pub mod cache; |
17 | #[cfg (target_arch = "wasm32" )] |
18 | mod htmlimage; |
19 | #[cfg (feature = "svg" )] |
20 | mod svg; |
21 | |
22 | #[allow (missing_docs)] |
23 | #[vtable::vtable ] |
24 | #[repr (C)] |
25 | pub struct OpaqueImageVTable { |
26 | drop_in_place: fn(VRefMut<OpaqueImageVTable>) -> Layout, |
27 | dealloc: fn(&OpaqueImageVTable, ptr: *mut u8, layout: Layout), |
28 | /// Returns the image size |
29 | size: fn(VRef<OpaqueImageVTable>) -> IntSize, |
30 | /// Returns a cache key |
31 | cache_key: fn(VRef<OpaqueImageVTable>) -> ImageCacheKey, |
32 | } |
33 | |
34 | #[cfg (feature = "svg" )] |
35 | OpaqueImageVTable_static! { |
36 | /// VTable for RC wrapped SVG helper struct. |
37 | pub static PARSED_SVG_VT for svg::ParsedSVG |
38 | } |
39 | |
40 | #[cfg (target_arch = "wasm32" )] |
41 | OpaqueImageVTable_static! { |
42 | /// VTable for RC wrapped HtmlImage helper struct. |
43 | pub static HTML_IMAGE_VT for htmlimage::HTMLImage |
44 | } |
45 | |
46 | OpaqueImageVTable_static! { |
47 | /// VTable for RC wrapped SVG helper struct. |
48 | pub static NINE_SLICE_VT for NineSliceImage |
49 | } |
50 | |
51 | /// SharedPixelBuffer is a container for storing image data as pixels. It is |
52 | /// internally reference counted and cheap to clone. |
53 | /// |
54 | /// You can construct a new empty shared pixel buffer with [`SharedPixelBuffer::new`], |
55 | /// or you can clone it from an existing contiguous buffer that you might already have, using |
56 | /// [`SharedPixelBuffer::clone_from_slice`]. |
57 | /// |
58 | /// See the documentation for [`Image`] for examples how to use this type to integrate |
59 | /// Slint with external rendering functions. |
60 | #[derive (Debug, Clone)] |
61 | #[repr (C)] |
62 | pub struct SharedPixelBuffer<Pixel> { |
63 | width: u32, |
64 | height: u32, |
65 | data: SharedVector<Pixel>, |
66 | } |
67 | |
68 | impl<Pixel> SharedPixelBuffer<Pixel> { |
69 | /// Returns the width of the image in pixels. |
70 | pub fn width(&self) -> u32 { |
71 | self.width |
72 | } |
73 | |
74 | /// Returns the height of the image in pixels. |
75 | pub fn height(&self) -> u32 { |
76 | self.height |
77 | } |
78 | |
79 | /// Returns the size of the image in pixels. |
80 | pub fn size(&self) -> IntSize { |
81 | [self.width, self.height].into() |
82 | } |
83 | } |
84 | |
85 | impl<Pixel: Clone> SharedPixelBuffer<Pixel> { |
86 | /// Return a mutable slice to the pixel data. If the SharedPixelBuffer was shared, this will make a copy of the buffer. |
87 | pub fn make_mut_slice(&mut self) -> &mut [Pixel] { |
88 | self.data.make_mut_slice() |
89 | } |
90 | } |
91 | |
92 | impl<Pixel: Clone + rgb::Pod> SharedPixelBuffer<Pixel> |
93 | where |
94 | [Pixel]: rgb::ComponentBytes<u8>, |
95 | { |
96 | /// Returns the pixels interpreted as raw bytes. |
97 | pub fn as_bytes(&self) -> &[u8] { |
98 | use rgb::ComponentBytes; |
99 | self.data.as_slice().as_bytes() |
100 | } |
101 | |
102 | /// Returns the pixels interpreted as raw bytes. |
103 | pub fn make_mut_bytes(&mut self) -> &mut [u8] { |
104 | use rgb::ComponentBytes; |
105 | self.data.make_mut_slice().as_bytes_mut() |
106 | } |
107 | } |
108 | |
109 | impl<Pixel> SharedPixelBuffer<Pixel> { |
110 | /// Return a slice to the pixel data. |
111 | pub fn as_slice(&self) -> &[Pixel] { |
112 | self.data.as_slice() |
113 | } |
114 | } |
115 | |
116 | impl<Pixel: Clone + Default> SharedPixelBuffer<Pixel> { |
117 | /// Creates a new SharedPixelBuffer with the given width and height. Each pixel will be initialized with the value |
118 | /// that [`Default::default()`] returns for the Pixel type. |
119 | pub fn new(width: u32, height: u32) -> Self { |
120 | Self { |
121 | width, |
122 | height, |
123 | data: coreimpl Iterator ::iter::repeat(Pixel::default()) |
124 | .take(width as usize * height as usize) |
125 | .collect(), |
126 | } |
127 | } |
128 | } |
129 | |
130 | impl<Pixel: Clone> SharedPixelBuffer<Pixel> { |
131 | /// Creates a new SharedPixelBuffer by cloning and converting pixels from an existing |
132 | /// slice. This function is useful when another crate was used to allocate an image |
133 | /// and you would like to convert it for use in Slint. |
134 | pub fn clone_from_slice<SourcePixelType>( |
135 | pixel_slice: &[SourcePixelType], |
136 | width: u32, |
137 | height: u32, |
138 | ) -> Self |
139 | where |
140 | [SourcePixelType]: rgb::AsPixels<Pixel>, |
141 | { |
142 | use rgb::AsPixels; |
143 | Self { width, height, data: pixel_slice.as_pixels().into() } |
144 | } |
145 | } |
146 | |
147 | /// Convenience alias for a pixel with three color channels (red, green and blue), each |
148 | /// encoded as u8. |
149 | pub type Rgb8Pixel = rgb::RGB8; |
150 | /// Convenience alias for a pixel with four color channels (red, green, blue and alpha), each |
151 | /// encoded as u8. |
152 | pub type Rgba8Pixel = rgb::RGBA8; |
153 | |
154 | /// SharedImageBuffer is a container for images that are stored in CPU accessible memory. |
155 | /// |
156 | /// The SharedImageBuffer's variants represent the different common formats for encoding |
157 | /// images in pixels. |
158 | #[derive (Clone, Debug)] |
159 | #[repr (C)] |
160 | pub enum SharedImageBuffer { |
161 | /// This variant holds the data for an image where each pixel has three color channels (red, green, |
162 | /// and blue) and each channel is encoded as unsigned byte. |
163 | RGB8(SharedPixelBuffer<Rgb8Pixel>), |
164 | /// This variant holds the data for an image where each pixel has four color channels (red, green, |
165 | /// blue and alpha) and each channel is encoded as unsigned byte. |
166 | RGBA8(SharedPixelBuffer<Rgba8Pixel>), |
167 | /// This variant holds the data for an image where each pixel has four color channels (red, green, |
168 | /// blue and alpha) and each channel is encoded as unsigned byte. In contrast to [`Self::RGBA8`], |
169 | /// this variant assumes that the alpha channel is also already multiplied to each red, green and blue |
170 | /// component of each pixel. |
171 | /// Only construct this format if you know that your pixels are encoded this way. It is more efficient |
172 | /// for rendering. |
173 | RGBA8Premultiplied(SharedPixelBuffer<Rgba8Pixel>), |
174 | } |
175 | |
176 | impl SharedImageBuffer { |
177 | /// Returns the width of the image in pixels. |
178 | #[inline ] |
179 | pub fn width(&self) -> u32 { |
180 | match self { |
181 | Self::RGB8(buffer) => buffer.width(), |
182 | Self::RGBA8(buffer) => buffer.width(), |
183 | Self::RGBA8Premultiplied(buffer) => buffer.width(), |
184 | } |
185 | } |
186 | |
187 | /// Returns the height of the image in pixels. |
188 | #[inline ] |
189 | pub fn height(&self) -> u32 { |
190 | match self { |
191 | Self::RGB8(buffer) => buffer.height(), |
192 | Self::RGBA8(buffer) => buffer.height(), |
193 | Self::RGBA8Premultiplied(buffer) => buffer.height(), |
194 | } |
195 | } |
196 | |
197 | /// Returns the size of the image in pixels. |
198 | #[inline ] |
199 | pub fn size(&self) -> IntSize { |
200 | match self { |
201 | Self::RGB8(buffer) => buffer.size(), |
202 | Self::RGBA8(buffer) => buffer.size(), |
203 | Self::RGBA8Premultiplied(buffer) => buffer.size(), |
204 | } |
205 | } |
206 | } |
207 | |
208 | impl PartialEq for SharedImageBuffer { |
209 | fn eq(&self, other: &Self) -> bool { |
210 | match self { |
211 | Self::RGB8(lhs_buffer: &SharedPixelBuffer>) => { |
212 | matches!(other, Self::RGB8(rhs_buffer) if lhs_buffer.data.as_ptr().eq(&rhs_buffer.data.as_ptr())) |
213 | } |
214 | Self::RGBA8(lhs_buffer: &SharedPixelBuffer>) => { |
215 | matches!(other, Self::RGBA8(rhs_buffer) if lhs_buffer.data.as_ptr().eq(&rhs_buffer.data.as_ptr())) |
216 | } |
217 | Self::RGBA8Premultiplied(lhs_buffer: &SharedPixelBuffer>) => { |
218 | matches!(other, Self::RGBA8Premultiplied(rhs_buffer) if lhs_buffer.data.as_ptr().eq(&rhs_buffer.data.as_ptr())) |
219 | } |
220 | } |
221 | } |
222 | } |
223 | |
224 | #[repr (u8)] |
225 | #[derive (Clone, PartialEq, Debug, Copy)] |
226 | /// The pixel format of a StaticTexture |
227 | pub enum PixelFormat { |
228 | /// red, green, blue. 24bits. |
229 | Rgb, |
230 | /// Red, green, blue, alpha. 32bits. |
231 | Rgba, |
232 | /// Red, green, blue, alpha. 32bits. The color are premultiplied by alpha |
233 | RgbaPremultiplied, |
234 | /// Alpha map. 8bits. Each pixel is an alpha value. The color is specified separately. |
235 | AlphaMap, |
236 | } |
237 | |
238 | impl PixelFormat { |
239 | /// The number of bytes in a pixel |
240 | pub fn bpp(self) -> usize { |
241 | match self { |
242 | PixelFormat::Rgb => 3, |
243 | PixelFormat::Rgba => 4, |
244 | PixelFormat::RgbaPremultiplied => 4, |
245 | PixelFormat::AlphaMap => 1, |
246 | } |
247 | } |
248 | } |
249 | |
250 | #[repr (C)] |
251 | #[derive (Clone, PartialEq, Debug)] |
252 | /// Some raw pixel data which is typically stored in the binary |
253 | pub struct StaticTexture { |
254 | /// The position and size of the texture within the image |
255 | pub rect: IntRect, |
256 | /// The pixel format of this texture |
257 | pub format: PixelFormat, |
258 | /// The color, for the alpha map ones |
259 | pub color: crate::Color, |
260 | /// index in the data array |
261 | pub index: usize, |
262 | } |
263 | |
264 | #[repr (C)] |
265 | #[derive (Clone, PartialEq, Debug)] |
266 | /// A texture is stored in read-only memory and may be composed of sub-textures. |
267 | |
268 | pub struct StaticTextures { |
269 | /// The total size of the image (this might not be the size of the full image |
270 | /// as some transparent part are not part of any texture) |
271 | pub size: IntSize, |
272 | /// The size of the image before the compiler applied any scaling |
273 | pub original_size: IntSize, |
274 | /// The pixel data referenced by the textures |
275 | pub data: Slice<'static, u8>, |
276 | /// The list of textures |
277 | pub textures: Slice<'static, StaticTexture>, |
278 | } |
279 | |
280 | /// ImageCacheKey encapsulates the different ways of indexing images in the |
281 | /// cache of decoded images. |
282 | #[derive (PartialEq, Eq, Debug, Hash, Clone)] |
283 | #[repr (u8)] |
284 | pub enum ImageCacheKey { |
285 | /// This variant indicates that no image cache key can be created for the image. |
286 | /// For example this is the case for programmatically created images. |
287 | Invalid = 0, |
288 | /// The image is identified by its path on the file system. |
289 | Path(SharedString) = 1, |
290 | /// The image is identified by a URL. |
291 | #[cfg (target_arch = "wasm32" )] |
292 | URL(SharedString) = 2, |
293 | /// The image is identified by the static address of its encoded data. |
294 | EmbeddedData(usize) = 3, |
295 | } |
296 | |
297 | impl ImageCacheKey { |
298 | /// Returns a new cache key if decoded image data can be stored in image cache for |
299 | /// the given ImageInner. |
300 | pub fn new(resource: &ImageInner) -> Option<Self> { |
301 | let key = match resource { |
302 | ImageInner::None => return None, |
303 | ImageInner::EmbeddedImage { cache_key, .. } => cache_key.clone(), |
304 | ImageInner::StaticTextures(textures) => { |
305 | Self::from_embedded_image_data(textures.data.as_slice()) |
306 | } |
307 | #[cfg (feature = "svg" )] |
308 | ImageInner::Svg(parsed_svg) => parsed_svg.cache_key(), |
309 | #[cfg (target_arch = "wasm32" )] |
310 | ImageInner::HTMLImage(htmlimage) => Self::URL(htmlimage.source().into()), |
311 | ImageInner::BackendStorage(x) => vtable::VRc::borrow(x).cache_key(), |
312 | #[cfg (not(target_arch = "wasm32" ))] |
313 | ImageInner::BorrowedOpenGLTexture(..) => return None, |
314 | ImageInner::NineSlice(nine) => vtable::VRc::borrow(nine).cache_key(), |
315 | }; |
316 | if matches!(key, ImageCacheKey::Invalid) { |
317 | None |
318 | } else { |
319 | Some(key) |
320 | } |
321 | } |
322 | |
323 | /// Returns a cache key for static embedded image data. |
324 | pub fn from_embedded_image_data(data: &'static [u8]) -> Self { |
325 | Self::EmbeddedData(data.as_ptr() as usize) |
326 | } |
327 | } |
328 | |
329 | /// Represent a nine-slice image with the base image and the 4 borders |
330 | pub struct NineSliceImage(pub ImageInner, pub [u16; 4]); |
331 | |
332 | impl NineSliceImage { |
333 | /// return the backing Image |
334 | pub fn image(&self) -> Image { |
335 | Image(self.0.clone()) |
336 | } |
337 | } |
338 | |
339 | impl OpaqueImage for NineSliceImage { |
340 | fn size(&self) -> IntSize { |
341 | self.0.size() |
342 | } |
343 | fn cache_key(&self) -> ImageCacheKey { |
344 | ImageCacheKey::new(&self.0).unwrap_or(default:ImageCacheKey::Invalid) |
345 | } |
346 | } |
347 | |
348 | /// A resource is a reference to binary data, for example images. They can be accessible on the file |
349 | /// system or embedded in the resulting binary. Or they might be URLs to a web server and a downloaded |
350 | /// is necessary before they can be used. |
351 | /// cbindgen:prefix-with-name |
352 | #[derive (Clone, Debug, Default)] |
353 | #[repr (u8)] |
354 | #[allow (missing_docs)] |
355 | pub enum ImageInner { |
356 | /// A resource that does not represent any data. |
357 | #[default] |
358 | None = 0, |
359 | EmbeddedImage { |
360 | cache_key: ImageCacheKey, |
361 | buffer: SharedImageBuffer, |
362 | } = 1, |
363 | #[cfg (feature = "svg" )] |
364 | Svg(vtable::VRc<OpaqueImageVTable, svg::ParsedSVG>) = 2, |
365 | StaticTextures(&'static StaticTextures) = 3, |
366 | #[cfg (target_arch = "wasm32" )] |
367 | HTMLImage(vtable::VRc<OpaqueImageVTable, htmlimage::HTMLImage>) = 4, |
368 | BackendStorage(vtable::VRc<OpaqueImageVTable>) = 5, |
369 | #[cfg (not(target_arch = "wasm32" ))] |
370 | BorrowedOpenGLTexture(BorrowedOpenGLTexture) = 6, |
371 | NineSlice(vtable::VRc<OpaqueImageVTable, NineSliceImage>) = 7, |
372 | } |
373 | |
374 | impl ImageInner { |
375 | /// Return or render the image into a buffer |
376 | /// |
377 | /// `target_size_for_scalable_source` is the size to use if the image is scalable. |
378 | /// (when unspecified, will default to the intrinsic size of the image) |
379 | /// |
380 | /// Returns None if the image can't be rendered in a buffer or if the image is empty |
381 | pub fn render_to_buffer( |
382 | &self, |
383 | _target_size_for_scalable_source: Option<euclid::Size2D<u32, PhysicalPx>>, |
384 | ) -> Option<SharedImageBuffer> { |
385 | match self { |
386 | ImageInner::EmbeddedImage { buffer, .. } => Some(buffer.clone()), |
387 | #[cfg (feature = "svg" )] |
388 | ImageInner::Svg(svg) => match svg.render(_target_size_for_scalable_source) { |
389 | Ok(b) => Some(b), |
390 | // Ignore error when rendering a 0x0 image, that's just an empty image |
391 | Err(resvg::usvg::Error::InvalidSize) => None, |
392 | Err(err) => { |
393 | eprintln!("Error rendering SVG: {err}" ); |
394 | None |
395 | } |
396 | }, |
397 | ImageInner::StaticTextures(ts) => { |
398 | let mut buffer = |
399 | SharedPixelBuffer::<Rgba8Pixel>::new(ts.size.width, ts.size.height); |
400 | let stride = buffer.width() as usize; |
401 | let slice = buffer.make_mut_slice(); |
402 | for t in ts.textures.iter() { |
403 | let rect = t.rect.to_usize(); |
404 | for y in 0..rect.height() { |
405 | let slice = &mut slice[(rect.min_y() + y) * stride..][rect.x_range()]; |
406 | let source = &ts.data[t.index + y * rect.width() * t.format.bpp()..]; |
407 | match t.format { |
408 | PixelFormat::Rgb => { |
409 | let mut iter = source.chunks_exact(3).map(|p| Rgba8Pixel { |
410 | r: p[0], |
411 | g: p[1], |
412 | b: p[2], |
413 | a: 255, |
414 | }); |
415 | slice.fill_with(|| iter.next().unwrap()); |
416 | } |
417 | PixelFormat::RgbaPremultiplied => { |
418 | let mut iter = source.chunks_exact(4).map(|p| Rgba8Pixel { |
419 | r: p[0], |
420 | g: p[1], |
421 | b: p[2], |
422 | a: p[3], |
423 | }); |
424 | slice.fill_with(|| iter.next().unwrap()); |
425 | } |
426 | PixelFormat::Rgba => { |
427 | let mut iter = source.chunks_exact(4).map(|p| { |
428 | let a = p[3]; |
429 | Rgba8Pixel { |
430 | r: (p[0] as u16 * a as u16 / 255) as u8, |
431 | g: (p[1] as u16 * a as u16 / 255) as u8, |
432 | b: (p[2] as u16 * a as u16 / 255) as u8, |
433 | a, |
434 | } |
435 | }); |
436 | slice.fill_with(|| iter.next().unwrap()); |
437 | } |
438 | PixelFormat::AlphaMap => { |
439 | let col = t.color.to_argb_u8(); |
440 | let mut iter = source.iter().map(|p| { |
441 | let a = *p as u32 * col.alpha as u32; |
442 | Rgba8Pixel { |
443 | r: (col.red as u32 * a / (255 * 255)) as u8, |
444 | g: (col.green as u32 * a / (255 * 255)) as u8, |
445 | b: (col.blue as u32 * a / (255 * 255)) as u8, |
446 | a: (a / 255) as u8, |
447 | } |
448 | }); |
449 | slice.fill_with(|| iter.next().unwrap()); |
450 | } |
451 | }; |
452 | } |
453 | } |
454 | Some(SharedImageBuffer::RGBA8Premultiplied(buffer)) |
455 | } |
456 | ImageInner::NineSlice(nine) => nine.0.render_to_buffer(None), |
457 | _ => None, |
458 | } |
459 | } |
460 | |
461 | /// Returns true if the image is an SVG (either backed by resvg or HTML image wrapper). |
462 | pub fn is_svg(&self) -> bool { |
463 | match self { |
464 | #[cfg (feature = "svg" )] |
465 | Self::Svg(_) => true, |
466 | #[cfg (target_arch = "wasm32" )] |
467 | Self::HTMLImage(html_image) => html_image.is_svg(), |
468 | _ => false, |
469 | } |
470 | } |
471 | |
472 | /// Return the image size |
473 | pub fn size(&self) -> IntSize { |
474 | match self { |
475 | ImageInner::None => Default::default(), |
476 | ImageInner::EmbeddedImage { buffer, .. } => buffer.size(), |
477 | ImageInner::StaticTextures(StaticTextures { original_size, .. }) => *original_size, |
478 | #[cfg (feature = "svg" )] |
479 | ImageInner::Svg(svg) => svg.size(), |
480 | #[cfg (target_arch = "wasm32" )] |
481 | ImageInner::HTMLImage(htmlimage) => htmlimage.size().unwrap_or_default(), |
482 | ImageInner::BackendStorage(x) => vtable::VRc::borrow(x).size(), |
483 | #[cfg (not(target_arch = "wasm32" ))] |
484 | ImageInner::BorrowedOpenGLTexture(BorrowedOpenGLTexture { size, .. }) => *size, |
485 | ImageInner::NineSlice(nine) => nine.0.size(), |
486 | } |
487 | } |
488 | } |
489 | |
490 | impl PartialEq for ImageInner { |
491 | fn eq(&self, other: &Self) -> bool { |
492 | match (self, other) { |
493 | ( |
494 | Self::EmbeddedImage { cache_key: l_cache_key: &ImageCacheKey, buffer: l_buffer: &SharedImageBuffer }, |
495 | Self::EmbeddedImage { cache_key: r_cache_key: &ImageCacheKey, buffer: r_buffer: &SharedImageBuffer }, |
496 | ) => l_cache_key == r_cache_key && l_buffer == r_buffer, |
497 | #[cfg (feature = "svg" )] |
498 | (Self::Svg(l0: &VRc), Self::Svg(r0: &VRc)) => vtable::VRc::ptr_eq(this:l0, other:r0), |
499 | (Self::StaticTextures(l0: &&StaticTextures), Self::StaticTextures(r0: &&StaticTextures)) => l0 == r0, |
500 | #[cfg (target_arch = "wasm32" )] |
501 | (Self::HTMLImage(l0), Self::HTMLImage(r0)) => vtable::VRc::ptr_eq(l0, r0), |
502 | (Self::BackendStorage(l0: &VRc), Self::BackendStorage(r0: &VRc)) => vtable::VRc::ptr_eq(this:l0, other:r0), |
503 | #[cfg (not(target_arch = "wasm32" ))] |
504 | (Self::BorrowedOpenGLTexture(l0: &BorrowedOpenGLTexture), Self::BorrowedOpenGLTexture(r0: &BorrowedOpenGLTexture)) => l0 == r0, |
505 | (Self::NineSlice(l: &VRc), Self::NineSlice(r: &VRc)) => l.0 == r.0 && l.1 == r.1, |
506 | _ => false, |
507 | } |
508 | } |
509 | } |
510 | |
511 | impl<'a> From<&'a Image> for &'a ImageInner { |
512 | fn from(other: &'a Image) -> Self { |
513 | &other.0 |
514 | } |
515 | } |
516 | |
517 | /// Error generated if an image cannot be loaded for any reasons. |
518 | #[derive (Default, Debug, PartialEq)] |
519 | pub struct LoadImageError(()); |
520 | |
521 | impl core::fmt::Display for LoadImageError { |
522 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { |
523 | f.write_str(data:"The image cannot be loaded" ) |
524 | } |
525 | } |
526 | |
527 | #[cfg (feature = "std" )] |
528 | impl std::error::Error for LoadImageError {} |
529 | |
530 | /// An image type that can be displayed by the Image element. You can construct |
531 | /// Image objects from a path to an image file on disk, using [`Self::load_from_path`]. |
532 | /// |
533 | /// Another typical use-case is to render the image content with Rust code. |
534 | /// For this it's most efficient to create a new SharedPixelBuffer with the known dimensions |
535 | /// and pass the mutable slice to your rendering function. Afterwards you can create an |
536 | /// Image. |
537 | /// |
538 | /// The following example creates a 320x200 RGB pixel buffer and calls an external |
539 | /// low_level_render() function to draw a shape into it. Finally the result is |
540 | /// stored in an Image with [`Self::from_rgb8()`]: |
541 | /// ``` |
542 | /// # use i_slint_core::graphics::{SharedPixelBuffer, Image, Rgb8Pixel}; |
543 | /// |
544 | /// fn low_level_render(width: u32, height: u32, buffer: &mut [u8]) { |
545 | /// // render beautiful circle or other shapes here |
546 | /// } |
547 | /// |
548 | /// let mut pixel_buffer = SharedPixelBuffer::<Rgb8Pixel>::new(320, 200); |
549 | /// |
550 | /// low_level_render(pixel_buffer.width(), pixel_buffer.height(), |
551 | /// pixel_buffer.make_mut_bytes()); |
552 | /// |
553 | /// let image = Image::from_rgb8(pixel_buffer); |
554 | /// ``` |
555 | /// |
556 | /// Another use-case is to import existing image data into Slint, by |
557 | /// creating a new Image through cloning of another image type. |
558 | /// |
559 | /// The following example uses the popular [image crate](https://docs.rs/image/) to |
560 | /// load a `.png` file from disk, apply brightening filter on it and then import |
561 | /// it into an [`Image`]: |
562 | /// ```no_run |
563 | /// # use i_slint_core::graphics::{SharedPixelBuffer, Image, Rgba8Pixel}; |
564 | /// let mut cat_image = image::open("cat.png" ).expect("Error loading cat image" ).into_rgba8(); |
565 | /// |
566 | /// image::imageops::colorops::brighten_in_place(&mut cat_image, 20); |
567 | /// |
568 | /// let buffer = SharedPixelBuffer::<Rgba8Pixel>::clone_from_slice( |
569 | /// cat_image.as_raw(), |
570 | /// cat_image.width(), |
571 | /// cat_image.height(), |
572 | /// ); |
573 | /// let image = Image::from_rgba8(buffer); |
574 | /// ``` |
575 | /// |
576 | /// A popular software (CPU) rendering library in Rust is tiny-skia. The following example shows |
577 | /// how to use tiny-skia to render into a [`SharedPixelBuffer`]: |
578 | /// ``` |
579 | /// # use i_slint_core::graphics::{SharedPixelBuffer, Image, Rgba8Pixel}; |
580 | /// let mut pixel_buffer = SharedPixelBuffer::<Rgba8Pixel>::new(640, 480); |
581 | /// let width = pixel_buffer.width(); |
582 | /// let height = pixel_buffer.height(); |
583 | /// let mut pixmap = tiny_skia::PixmapMut::from_bytes( |
584 | /// pixel_buffer.make_mut_bytes(), width, height |
585 | /// ).unwrap(); |
586 | /// pixmap.fill(tiny_skia::Color::TRANSPARENT); |
587 | /// |
588 | /// let circle = tiny_skia::PathBuilder::from_circle(320., 240., 150.).unwrap(); |
589 | /// |
590 | /// let mut paint = tiny_skia::Paint::default(); |
591 | /// paint.shader = tiny_skia::LinearGradient::new( |
592 | /// tiny_skia::Point::from_xy(100.0, 100.0), |
593 | /// tiny_skia::Point::from_xy(400.0, 400.0), |
594 | /// vec![ |
595 | /// tiny_skia::GradientStop::new(0.0, tiny_skia::Color::from_rgba8(50, 127, 150, 200)), |
596 | /// tiny_skia::GradientStop::new(1.0, tiny_skia::Color::from_rgba8(220, 140, 75, 180)), |
597 | /// ], |
598 | /// tiny_skia::SpreadMode::Pad, |
599 | /// tiny_skia::Transform::identity(), |
600 | /// ).unwrap(); |
601 | /// |
602 | /// pixmap.fill_path(&circle, &paint, tiny_skia::FillRule::Winding, Default::default(), None); |
603 | /// |
604 | /// let image = Image::from_rgba8_premultiplied(pixel_buffer); |
605 | /// ``` |
606 | /// |
607 | /// ### Sending Image to a thread |
608 | /// |
609 | /// `Image` is not [`Send`], because it uses internal cache that are local to the Slint thread. |
610 | /// If you want to create image data in a thread and send that to slint, construct the |
611 | /// [`SharedPixelBuffer`] in a thread, and send that to Slint's UI thread. |
612 | /// |
613 | /// ```rust,no_run |
614 | /// # use i_slint_core::graphics::{SharedPixelBuffer, Image, Rgba8Pixel}; |
615 | /// std::thread::spawn(move || { |
616 | /// let mut pixel_buffer = SharedPixelBuffer::<Rgba8Pixel>::new(640, 480); |
617 | /// // ... fill the pixel_buffer with data as shown in the previous example ... |
618 | /// slint::invoke_from_event_loop(move || { |
619 | /// // this will run in the Slint's UI thread |
620 | /// let image = Image::from_rgba8_premultiplied(pixel_buffer); |
621 | /// // ... use the image, eg: |
622 | /// // my_ui_handle.upgrade().unwrap().set_image(image); |
623 | /// }); |
624 | /// }); |
625 | /// ``` |
626 | #[repr (transparent)] |
627 | #[derive (Default, Clone, Debug, PartialEq, derive_more::From)] |
628 | pub struct Image(ImageInner); |
629 | |
630 | impl Image { |
631 | #[cfg (feature = "image-decoders" )] |
632 | /// Load an Image from a path to a file containing an image |
633 | pub fn load_from_path(path: &std::path::Path) -> Result<Self, LoadImageError> { |
634 | self::cache::IMAGE_CACHE.with(|global_cache| { |
635 | let path: SharedString = path.to_str().ok_or(LoadImageError(()))?.into(); |
636 | global_cache.borrow_mut().load_image_from_path(&path).ok_or(LoadImageError(())) |
637 | }) |
638 | } |
639 | |
640 | /// Creates a new Image from the specified shared pixel buffer, where each pixel has three color |
641 | /// channels (red, green and blue) encoded as u8. |
642 | pub fn from_rgb8(buffer: SharedPixelBuffer<Rgb8Pixel>) -> Self { |
643 | Image(ImageInner::EmbeddedImage { |
644 | cache_key: ImageCacheKey::Invalid, |
645 | buffer: SharedImageBuffer::RGB8(buffer), |
646 | }) |
647 | } |
648 | |
649 | /// Creates a new Image from the specified shared pixel buffer, where each pixel has four color |
650 | /// channels (red, green, blue and alpha) encoded as u8. |
651 | pub fn from_rgba8(buffer: SharedPixelBuffer<Rgba8Pixel>) -> Self { |
652 | Image(ImageInner::EmbeddedImage { |
653 | cache_key: ImageCacheKey::Invalid, |
654 | buffer: SharedImageBuffer::RGBA8(buffer), |
655 | }) |
656 | } |
657 | |
658 | /// Creates a new Image from the specified shared pixel buffer, where each pixel has four color |
659 | /// channels (red, green, blue and alpha) encoded as u8 and, in contrast to [`Self::from_rgba8`], |
660 | /// the alpha channel is also assumed to be multiplied to the red, green and blue channels. |
661 | /// |
662 | /// Only construct an Image with this function if you know that your pixels are encoded this way. |
663 | pub fn from_rgba8_premultiplied(buffer: SharedPixelBuffer<Rgba8Pixel>) -> Self { |
664 | Image(ImageInner::EmbeddedImage { |
665 | cache_key: ImageCacheKey::Invalid, |
666 | buffer: SharedImageBuffer::RGBA8Premultiplied(buffer), |
667 | }) |
668 | } |
669 | |
670 | /// Creates a new Image from an existing OpenGL texture. The texture remains borrowed by Slint |
671 | /// for the duration of being used for rendering, such as when assigned as source property to |
672 | /// an `Image` element. It's the application's responsibility to delete the texture when it is |
673 | /// not used anymore. |
674 | /// |
675 | /// The texture must be bindable against the `GL_TEXTURE_2D` target, have `GL_RGBA` as format |
676 | /// for the pixel data. |
677 | /// |
678 | /// When Slint renders the texture, it assumes that the origin of the texture is at the top-left. |
679 | /// This is different from the default OpenGL coordinate system. |
680 | /// |
681 | /// # Safety |
682 | /// |
683 | /// This function is unsafe because invalid texture ids may lead to undefined behavior in OpenGL |
684 | /// drivers. A valid texture id is one that was created by the same OpenGL context that is |
685 | /// current during any of the invocations of the callback set on [`Window::set_rendering_notifier()`](crate::api::Window::set_rendering_notifier). |
686 | /// OpenGL contexts between instances of [`slint::Window`](crate::api::Window) are not sharing resources. Consequently |
687 | /// [`slint::Image`](Self) objects created from borrowed OpenGL textures cannot be shared between |
688 | /// different windows. |
689 | #[allow (unsafe_code)] |
690 | #[cfg (not(target_arch = "wasm32" ))] |
691 | #[deprecated (since = "1.2.0" , note = "Use BorrowedOpenGLTextureBuilder" )] |
692 | pub unsafe fn from_borrowed_gl_2d_rgba_texture( |
693 | texture_id: core::num::NonZeroU32, |
694 | size: IntSize, |
695 | ) -> Self { |
696 | BorrowedOpenGLTextureBuilder::new_gl_2d_rgba_texture(texture_id, size).build() |
697 | } |
698 | |
699 | /// Creates a new Image from the specified buffer, which contains SVG raw data. |
700 | #[cfg (feature = "svg" )] |
701 | pub fn load_from_svg_data(buffer: &[u8]) -> Result<Self, LoadImageError> { |
702 | let cache_key = ImageCacheKey::Invalid; |
703 | Ok(Image(ImageInner::Svg(vtable::VRc::new( |
704 | svg::load_from_data(buffer, cache_key).map_err(|_| LoadImageError(()))?, |
705 | )))) |
706 | } |
707 | |
708 | /// Sets the nine-slice edges of the image. |
709 | /// |
710 | /// [Nine-slice scaling](https://en.wikipedia.org/wiki/9-slice_scaling) is a method for scaling |
711 | /// images in such a way that the corners are not distorted. |
712 | /// The arguments define the pixel sizes of the edges that cut the image into 9 slices. |
713 | pub fn set_nine_slice_edges(&mut self, top: u16, right: u16, bottom: u16, left: u16) { |
714 | if top == 0 && left == 0 && right == 0 && bottom == 0 { |
715 | if let ImageInner::NineSlice(n) = &self.0 { |
716 | self.0 = n.0.clone(); |
717 | } |
718 | } else { |
719 | let array = [top, right, bottom, left]; |
720 | let inner = if let ImageInner::NineSlice(n) = &mut self.0 { |
721 | n.0.clone() |
722 | } else { |
723 | self.0.clone() |
724 | }; |
725 | self.0 = ImageInner::NineSlice(vtable::VRc::new(NineSliceImage(inner, array))); |
726 | } |
727 | } |
728 | |
729 | /// Returns the size of the Image in pixels. |
730 | pub fn size(&self) -> IntSize { |
731 | self.0.size() |
732 | } |
733 | |
734 | #[cfg (feature = "std" )] |
735 | /// Returns the path of the image on disk, if it was constructed via [`Self::load_from_path`]. |
736 | /// |
737 | /// For example: |
738 | /// ``` |
739 | /// # use std::path::Path; |
740 | /// # use i_slint_core::graphics::*; |
741 | /// let path_buf = Path::new(env!("CARGO_MANIFEST_DIR" )) |
742 | /// .join("../../examples/printerdemo/ui/images/cat.jpg" ); |
743 | /// let image = Image::load_from_path(&path_buf).unwrap(); |
744 | /// assert_eq!(image.path(), Some(path_buf.as_path())); |
745 | /// ``` |
746 | pub fn path(&self) -> Option<&std::path::Path> { |
747 | match &self.0 { |
748 | ImageInner::EmbeddedImage { cache_key: ImageCacheKey::Path(path), .. } => { |
749 | Some(std::path::Path::new(path.as_str())) |
750 | } |
751 | ImageInner::NineSlice(nine) => match &nine.0 { |
752 | ImageInner::EmbeddedImage { cache_key: ImageCacheKey::Path(path), .. } => { |
753 | Some(std::path::Path::new(path.as_str())) |
754 | } |
755 | _ => None, |
756 | }, |
757 | _ => None, |
758 | } |
759 | } |
760 | } |
761 | |
762 | /// This enum describes the origin to use when rendering a borrowed OpenGL texture. |
763 | /// Use this with [`BorrowedOpenGLTextureBuilder::origin`]. |
764 | #[derive (Copy, Clone, Debug, PartialEq, Default)] |
765 | #[repr (u8)] |
766 | #[non_exhaustive ] |
767 | pub enum BorrowedOpenGLTextureOrigin { |
768 | /// The top-left of the texture is the top-left of the texture drawn on the screen. |
769 | #[default] |
770 | TopLeft, |
771 | /// The bottom-left of the texture is the top-left of the texture draw on the screen, |
772 | /// flipping it vertically. |
773 | BottomLeft, |
774 | } |
775 | |
776 | /// Factory to create [`slint::Image`](crate::graphics::Image) from an existing OpenGL texture. |
777 | /// |
778 | /// Methods can be chained on it in order to configure it. |
779 | /// |
780 | /// * `origin`: Change the texture's origin when rendering (default: TopLeft). |
781 | /// |
782 | /// Complete the builder by calling [`Self::build()`] to create a [`slint::Image`](crate::graphics::Image): |
783 | /// |
784 | /// ``` |
785 | /// # use i_slint_core::graphics::{BorrowedOpenGLTextureBuilder, Image, IntSize, BorrowedOpenGLTextureOrigin}; |
786 | /// # let texture_id = core::num::NonZeroU32::new(1).unwrap(); |
787 | /// # let size = IntSize::new(100, 100); |
788 | /// let builder = unsafe { BorrowedOpenGLTextureBuilder::new_gl_2d_rgba_texture(texture_id, size) } |
789 | /// .origin(BorrowedOpenGLTextureOrigin::TopLeft); |
790 | /// |
791 | /// let image: slint::Image = builder.build(); |
792 | /// ``` |
793 | #[cfg (not(target_arch = "wasm32" ))] |
794 | pub struct BorrowedOpenGLTextureBuilder(BorrowedOpenGLTexture); |
795 | |
796 | #[cfg (not(target_arch = "wasm32" ))] |
797 | impl BorrowedOpenGLTextureBuilder { |
798 | /// Generates the base configuration for a borrowed OpenGL texture. |
799 | /// |
800 | /// The texture must be bindable against the `GL_TEXTURE_2D` target, have `GL_RGBA` as format |
801 | /// for the pixel data. |
802 | /// |
803 | /// By default, when Slint renders the texture, it assumes that the origin of the texture is at the top-left. |
804 | /// This is different from the default OpenGL coordinate system. Use the `mirror_vertically` function |
805 | /// to reconfigure this. |
806 | /// |
807 | /// # Safety |
808 | /// |
809 | /// This function is unsafe because invalid texture ids may lead to undefined behavior in OpenGL |
810 | /// drivers. A valid texture id is one that was created by the same OpenGL context that is |
811 | /// current during any of the invocations of the callback set on [`Window::set_rendering_notifier()`](crate::api::Window::set_rendering_notifier). |
812 | /// OpenGL contexts between instances of [`slint::Window`](crate::api::Window) are not sharing resources. Consequently |
813 | /// [`slint::Image`](Self) objects created from borrowed OpenGL textures cannot be shared between |
814 | /// different windows. |
815 | #[allow (unsafe_code)] |
816 | pub unsafe fn new_gl_2d_rgba_texture(texture_id: core::num::NonZeroU32, size: IntSize) -> Self { |
817 | Self(BorrowedOpenGLTexture { texture_id, size, origin: Default::default() }) |
818 | } |
819 | |
820 | /// Configures the texture to be rendered vertically mirrored. |
821 | pub fn origin(mut self, origin: BorrowedOpenGLTextureOrigin) -> Self { |
822 | self.0.origin = origin; |
823 | self |
824 | } |
825 | |
826 | /// Completes the process of building a slint::Image that holds a borrowed OpenGL texture. |
827 | pub fn build(self) -> Image { |
828 | Image(ImageInner::BorrowedOpenGLTexture(self.0)) |
829 | } |
830 | } |
831 | |
832 | /// Load an image from an image embedded in the binary. |
833 | /// This is called by the generated code. |
834 | #[cfg (feature = "image-decoders" )] |
835 | pub fn load_image_from_embedded_data(data: Slice<'static, u8>, format: Slice<'_, u8>) -> Image { |
836 | self::cache::IMAGE_CACHE.with(|global_cache: &RefCell| { |
837 | global_cache.borrow_mut().load_image_from_embedded_data(data, format).unwrap_or_default() |
838 | }) |
839 | } |
840 | |
841 | #[test ] |
842 | fn test_image_size_from_buffer_without_backend() { |
843 | { |
844 | assert_eq!(Image::default().size(), Default::default()); |
845 | } |
846 | { |
847 | let buffer: SharedPixelBuffer> = SharedPixelBuffer::<Rgb8Pixel>::new(width:320, height:200); |
848 | let image: Image = Image::from_rgb8(buffer); |
849 | assert_eq!(image.size(), [320, 200].into()) |
850 | } |
851 | } |
852 | |
853 | #[cfg (feature = "svg" )] |
854 | #[test ] |
855 | fn test_image_size_from_svg() { |
856 | let simple_svg: &str = r#"<svg width="320" height="200" xmlns="http://www.w3.org/2000/svg"></svg>"# ; |
857 | let image: Image = Image::load_from_svg_data(buffer:simple_svg.as_bytes()).unwrap(); |
858 | assert_eq!(image.size(), [320, 200].into()); |
859 | } |
860 | |
861 | #[cfg (feature = "svg" )] |
862 | #[test ] |
863 | fn test_image_invalid_svg() { |
864 | let invalid_svg: &str = r#"AaBbCcDd"# ; |
865 | let result: Result = Image::load_from_svg_data(buffer:invalid_svg.as_bytes()); |
866 | assert!(result.is_err()); |
867 | } |
868 | |
869 | /// The result of the fit function |
870 | #[derive (Debug)] |
871 | pub struct FitResult { |
872 | /// The clip rect in the source image (in source image coordinate) |
873 | pub clip_rect: IntRect, |
874 | /// The scale to apply to go from the source to the target horizontally |
875 | pub source_to_target_x: f32, |
876 | /// The scale to apply to go from the source to the target vertically |
877 | pub source_to_target_y: f32, |
878 | /// The size of the target |
879 | pub size: euclid::Size2D<f32, PhysicalPx>, |
880 | /// The offset in the target in which we draw the image |
881 | pub offset: euclid::Point2D<f32, PhysicalPx>, |
882 | /// When Some, it means the image should be tiled instead of stretched to the target |
883 | /// but still scaled with the source_to_target_x and source_to_target_y factor |
884 | /// The point is the coordinate within the image's clip_rect of the pixel at the offset |
885 | pub tiled: Option<euclid::default::Point2D<u32>>, |
886 | } |
887 | |
888 | impl FitResult { |
889 | fn adjust_for_tiling( |
890 | self, |
891 | ratio: f32, |
892 | alignment: (ImageHorizontalAlignment, ImageVerticalAlignment), |
893 | tiling: (ImageTiling, ImageTiling), |
894 | ) -> Self { |
895 | let mut r = self; |
896 | let mut tiled = euclid::Point2D::default(); |
897 | let target = r.size; |
898 | let o = r.clip_rect.size.cast::<f32>(); |
899 | match tiling.0 { |
900 | ImageTiling::None => { |
901 | r.size.width = o.width * r.source_to_target_x; |
902 | if (o.width as f32) > target.width / r.source_to_target_x { |
903 | let diff = (o.width as f32 - target.width / r.source_to_target_x) as i32; |
904 | r.clip_rect.size.width -= diff; |
905 | r.clip_rect.origin.x += match alignment.0 { |
906 | ImageHorizontalAlignment::Center => diff / 2, |
907 | ImageHorizontalAlignment::Left => 0, |
908 | ImageHorizontalAlignment::Right => diff, |
909 | }; |
910 | r.size.width = target.width; |
911 | } else if (o.width as f32) < target.width / r.source_to_target_x { |
912 | r.offset.x += match alignment.0 { |
913 | ImageHorizontalAlignment::Center => { |
914 | (target.width - o.width as f32 * r.source_to_target_x) / 2. |
915 | } |
916 | ImageHorizontalAlignment::Left => 0., |
917 | ImageHorizontalAlignment::Right => { |
918 | target.width - o.width as f32 * r.source_to_target_x |
919 | } |
920 | }; |
921 | } |
922 | } |
923 | ImageTiling::Repeat => { |
924 | tiled.x = match alignment.0 { |
925 | ImageHorizontalAlignment::Left => 0, |
926 | ImageHorizontalAlignment::Center => { |
927 | ((o.width - target.width / ratio) / 2.).rem_euclid(o.width) as u32 |
928 | } |
929 | ImageHorizontalAlignment::Right => { |
930 | (-target.width / ratio).rem_euclid(o.width) as u32 |
931 | } |
932 | }; |
933 | r.source_to_target_x = ratio; |
934 | } |
935 | ImageTiling::Round => { |
936 | if target.width / ratio <= o.width * 1.5 { |
937 | r.source_to_target_x = target.width / o.width; |
938 | } else { |
939 | let mut rem = (target.width / ratio).rem_euclid(o.width); |
940 | if rem > o.width / 2. { |
941 | rem -= o.width; |
942 | } |
943 | r.source_to_target_x = ratio * target.width / (target.width - rem * ratio); |
944 | } |
945 | } |
946 | } |
947 | |
948 | match tiling.1 { |
949 | ImageTiling::None => { |
950 | r.size.height = o.height * r.source_to_target_y; |
951 | if (o.height as f32) > target.height / r.source_to_target_y { |
952 | let diff = (o.height as f32 - target.height / r.source_to_target_y) as i32; |
953 | r.clip_rect.size.height -= diff; |
954 | r.clip_rect.origin.y += match alignment.1 { |
955 | ImageVerticalAlignment::Center => diff / 2, |
956 | ImageVerticalAlignment::Top => 0, |
957 | ImageVerticalAlignment::Bottom => diff, |
958 | }; |
959 | r.size.height = target.height; |
960 | } else if (o.height as f32) < target.height / r.source_to_target_y { |
961 | r.offset.y += match alignment.1 { |
962 | ImageVerticalAlignment::Center => { |
963 | (target.height - o.height as f32 * r.source_to_target_y) / 2. |
964 | } |
965 | ImageVerticalAlignment::Top => 0., |
966 | ImageVerticalAlignment::Bottom => { |
967 | target.height - o.height as f32 * r.source_to_target_y |
968 | } |
969 | }; |
970 | } |
971 | } |
972 | ImageTiling::Repeat => { |
973 | tiled.y = match alignment.1 { |
974 | ImageVerticalAlignment::Top => 0, |
975 | ImageVerticalAlignment::Center => { |
976 | ((o.height - target.height / ratio) / 2.).rem_euclid(o.height) as u32 |
977 | } |
978 | ImageVerticalAlignment::Bottom => { |
979 | (-target.height / ratio).rem_euclid(o.height) as u32 |
980 | } |
981 | }; |
982 | r.source_to_target_y = ratio; |
983 | } |
984 | ImageTiling::Round => { |
985 | if target.height / ratio <= o.height * 1.5 { |
986 | r.source_to_target_y = target.height / o.height; |
987 | } else { |
988 | let mut rem = (target.height / ratio).rem_euclid(o.height); |
989 | if rem > o.height / 2. { |
990 | rem -= o.height; |
991 | } |
992 | r.source_to_target_y = ratio * target.height / (target.height - rem * ratio); |
993 | } |
994 | } |
995 | } |
996 | let has_tiling = tiling != (ImageTiling::None, ImageTiling::None); |
997 | r.tiled = has_tiling.then_some(tiled); |
998 | r |
999 | } |
1000 | } |
1001 | |
1002 | #[cfg (not(feature = "std" ))] |
1003 | trait RemEuclid { |
1004 | fn rem_euclid(self, b: f32) -> f32; |
1005 | } |
1006 | #[cfg (not(feature = "std" ))] |
1007 | impl RemEuclid for f32 { |
1008 | fn rem_euclid(self, b: f32) -> f32 { |
1009 | return num_traits::Euclid::rem_euclid(&self, &b); |
1010 | } |
1011 | } |
1012 | |
1013 | /// Return an FitResult that can be used to render an image in a buffer that matches a given ImageFit |
1014 | pub fn fit( |
1015 | image_fit: ImageFit, |
1016 | target: euclid::Size2D<f32, PhysicalPx>, |
1017 | source_rect: IntRect, |
1018 | scale_factor: ScaleFactor, |
1019 | alignment: (ImageHorizontalAlignment, ImageVerticalAlignment), |
1020 | tiling: (ImageTiling, ImageTiling), |
1021 | ) -> FitResult { |
1022 | let has_tiling = tiling != (ImageTiling::None, ImageTiling::None); |
1023 | let o = source_rect.size.cast::<f32>(); |
1024 | let ratio = match image_fit { |
1025 | // If there is any tiling, we ignore image_fit |
1026 | _ if has_tiling => scale_factor.get(), |
1027 | ImageFit::Fill => { |
1028 | return FitResult { |
1029 | clip_rect: source_rect, |
1030 | source_to_target_x: target.width / o.width, |
1031 | source_to_target_y: target.height / o.height, |
1032 | size: target, |
1033 | offset: Default::default(), |
1034 | tiled: None, |
1035 | } |
1036 | } |
1037 | ImageFit::Preserve => scale_factor.get(), |
1038 | ImageFit::Contain => f32::min(target.width / o.width, target.height / o.height), |
1039 | ImageFit::Cover => f32::max(target.width / o.width, target.height / o.height), |
1040 | }; |
1041 | |
1042 | FitResult { |
1043 | clip_rect: source_rect, |
1044 | source_to_target_x: ratio, |
1045 | source_to_target_y: ratio, |
1046 | size: target, |
1047 | offset: euclid::Point2D::default(), |
1048 | tiled: None, |
1049 | } |
1050 | .adjust_for_tiling(ratio, alignment, tiling) |
1051 | } |
1052 | |
1053 | /// Generate an iterator of [`FitResult`] for each slice of a nine-slice border image |
1054 | pub fn fit9slice( |
1055 | source_rect: IntSize, |
1056 | [t: u16, r: u16, b: u16, l: u16]: [u16; 4], |
1057 | target: euclid::Size2D<f32, PhysicalPx>, |
1058 | scale_factor: ScaleFactor, |
1059 | alignment: (ImageHorizontalAlignment, ImageVerticalAlignment), |
1060 | tiling: (ImageTiling, ImageTiling), |
1061 | ) -> impl Iterator<Item = FitResult> { |
1062 | let fit_to = |clip_rect: euclid::default::Rect<u16>, target: euclid::Rect<f32, PhysicalPx>| { |
1063 | (!clip_rect.is_empty() && !target.is_empty()).then(|| { |
1064 | FitResult { |
1065 | clip_rect: clip_rect.cast(), |
1066 | source_to_target_x: target.width() / clip_rect.width() as f32, |
1067 | source_to_target_y: target.height() / clip_rect.height() as f32, |
1068 | size: target.size, |
1069 | offset: target.origin, |
1070 | tiled: None, |
1071 | } |
1072 | .adjust_for_tiling(scale_factor.get(), alignment, tiling) |
1073 | }) |
1074 | }; |
1075 | use euclid::rect; |
1076 | let sf = |x| scale_factor.get() * x as f32; |
1077 | let source = source_rect.cast::<u16>(); |
1078 | if t + b > source.height || l + r > source.width { |
1079 | [None, None, None, None, None, None, None, None, None] |
1080 | } else { |
1081 | [ |
1082 | fit_to(rect(0, 0, l, t), rect(0., 0., sf(l), sf(t))), |
1083 | fit_to( |
1084 | rect(l, 0, source.width - l - r, t), |
1085 | rect(sf(l), 0., target.width - sf(l) - sf(r), sf(t)), |
1086 | ), |
1087 | fit_to(rect(source.width - r, 0, r, t), rect(target.width - sf(r), 0., sf(r), sf(t))), |
1088 | fit_to( |
1089 | rect(0, t, l, source.height - t - b), |
1090 | rect(0., sf(t), sf(l), target.height - sf(t) - sf(b)), |
1091 | ), |
1092 | fit_to( |
1093 | rect(l, t, source.width - l - r, source.height - t - b), |
1094 | rect(sf(l), sf(t), target.width - sf(l) - sf(r), target.height - sf(t) - sf(b)), |
1095 | ), |
1096 | fit_to( |
1097 | rect(source.width - r, t, r, source.height - t - b), |
1098 | rect(target.width - sf(r), sf(t), sf(r), target.height - sf(t) - sf(b)), |
1099 | ), |
1100 | fit_to(rect(0, source.height - b, l, b), rect(0., target.height - sf(b), sf(l), sf(b))), |
1101 | fit_to( |
1102 | rect(l, source.height - b, source.width - l - r, b), |
1103 | rect(sf(l), target.height - sf(b), target.width - sf(l) - sf(r), sf(b)), |
1104 | ), |
1105 | fit_to( |
1106 | rect(source.width - r, source.height - b, r, b), |
1107 | rect(target.width - sf(r), target.height - sf(b), sf(r), sf(b)), |
1108 | ), |
1109 | ] |
1110 | } |
1111 | .into_iter() |
1112 | .flatten() |
1113 | } |
1114 | |
1115 | #[cfg (feature = "ffi" )] |
1116 | pub(crate) mod ffi { |
1117 | #![allow (unsafe_code)] |
1118 | |
1119 | use super::*; |
1120 | |
1121 | // Expand Rgb8Pixel so that cbindgen can see it. (is in fact rgb::RGB<u8>) |
1122 | /// Represents an RGB pixel. |
1123 | #[cfg (cbindgen)] |
1124 | #[repr (C)] |
1125 | struct Rgb8Pixel { |
1126 | /// red value (between 0 and 255) |
1127 | r: u8, |
1128 | /// green value (between 0 and 255) |
1129 | g: u8, |
1130 | /// blue value (between 0 and 255) |
1131 | b: u8, |
1132 | } |
1133 | |
1134 | // Expand Rgba8Pixel so that cbindgen can see it. (is in fact rgb::RGBA<u8>) |
1135 | /// Represents an RGBA pixel. |
1136 | #[cfg (cbindgen)] |
1137 | #[repr (C)] |
1138 | struct Rgba8Pixel { |
1139 | /// red value (between 0 and 255) |
1140 | r: u8, |
1141 | /// green value (between 0 and 255) |
1142 | g: u8, |
1143 | /// blue value (between 0 and 255) |
1144 | b: u8, |
1145 | /// alpha value (between 0 and 255) |
1146 | a: u8, |
1147 | } |
1148 | |
1149 | #[cfg (feature = "image-decoders" )] |
1150 | #[no_mangle ] |
1151 | pub unsafe extern "C" fn slint_image_load_from_path(path: &SharedString, image: *mut Image) { |
1152 | core::ptr::write( |
1153 | image, |
1154 | Image::load_from_path(std::path::Path::new(path.as_str())).unwrap_or(Image::default()), |
1155 | ) |
1156 | } |
1157 | |
1158 | #[cfg (feature = "std" )] |
1159 | #[no_mangle ] |
1160 | pub unsafe extern "C" fn slint_image_load_from_embedded_data( |
1161 | data: Slice<'static, u8>, |
1162 | format: Slice<'static, u8>, |
1163 | image: *mut Image, |
1164 | ) { |
1165 | core::ptr::write(image, super::load_image_from_embedded_data(data, format)); |
1166 | } |
1167 | |
1168 | #[no_mangle ] |
1169 | pub unsafe extern "C" fn slint_image_size(image: &Image) -> IntSize { |
1170 | image.size() |
1171 | } |
1172 | |
1173 | #[no_mangle ] |
1174 | pub extern "C" fn slint_image_path(image: &Image) -> Option<&SharedString> { |
1175 | match &image.0 { |
1176 | ImageInner::EmbeddedImage { cache_key, .. } => match cache_key { |
1177 | ImageCacheKey::Path(path) => Some(path), |
1178 | _ => None, |
1179 | }, |
1180 | ImageInner::NineSlice(nine) => match &nine.0 { |
1181 | ImageInner::EmbeddedImage { cache_key, .. } => match cache_key { |
1182 | ImageCacheKey::Path(path) => Some(path), |
1183 | _ => None, |
1184 | }, |
1185 | _ => None, |
1186 | }, |
1187 | _ => None, |
1188 | } |
1189 | } |
1190 | |
1191 | #[no_mangle ] |
1192 | pub unsafe extern "C" fn slint_image_from_embedded_textures( |
1193 | textures: &'static StaticTextures, |
1194 | image: *mut Image, |
1195 | ) { |
1196 | core::ptr::write(image, Image::from(ImageInner::StaticTextures(textures))); |
1197 | } |
1198 | |
1199 | #[no_mangle ] |
1200 | pub unsafe extern "C" fn slint_image_compare_equal(image1: &Image, image2: &Image) -> bool { |
1201 | return image1.eq(image2); |
1202 | } |
1203 | |
1204 | /// Call [`Image::set_nine_slice_edges`] |
1205 | #[no_mangle ] |
1206 | pub extern "C" fn slint_image_set_nine_slice_edges( |
1207 | image: &mut Image, |
1208 | top: u16, |
1209 | right: u16, |
1210 | bottom: u16, |
1211 | left: u16, |
1212 | ) { |
1213 | image.set_nine_slice_edges(top, right, bottom, left); |
1214 | } |
1215 | } |
1216 | |
1217 | /// This structure contains fields to identify and render an OpenGL texture that Slint borrows from the application code. |
1218 | /// Use this to embed a native OpenGL texture into a Slint scene. |
1219 | /// |
1220 | /// The ownership of the texture remains with the application. It is the application's responsibility to delete the texture |
1221 | /// when it is not used anymore. |
1222 | /// |
1223 | /// Note that only 2D RGBA textures are supported. |
1224 | #[derive (Clone, Debug, PartialEq)] |
1225 | #[non_exhaustive ] |
1226 | #[cfg (not(target_arch = "wasm32" ))] |
1227 | #[repr (C)] |
1228 | pub struct BorrowedOpenGLTexture { |
1229 | /// The id or name of the texture, as created by [`glGenTextures`](https://registry.khronos.org/OpenGL-Refpages/gl4/html/glGenTextures.xhtml). |
1230 | pub texture_id: core::num::NonZeroU32, |
1231 | /// The size of the texture in pixels. |
1232 | pub size: IntSize, |
1233 | /// Origin of the texture when rendering. |
1234 | pub origin: BorrowedOpenGLTextureOrigin, |
1235 | } |
1236 | |