makepad_widgets/
image_cache.rs

1use crate::makepad_draw::*;
2use std::collections::HashMap;
3use std::error::Error;
4use makepad_zune_jpeg::JpegDecoder;
5use makepad_zune_png::{post_process_image, PngDecoder};
6use std::fmt;
7use std::io::prelude::*;
8use std::fs::File;
9use std::path::{Path,PathBuf};
10use std::cell::RefCell;
11use std::sync::Arc;
12
13pub use makepad_zune_png::error::PngDecodeErrors;
14pub use makepad_zune_jpeg::errors::DecodeErrors as JpgDecodeErrors;
15
16#[derive(Live, LiveHook, Clone, Copy)]
17#[live_ignore]
18pub enum ImageFit {
19    #[pick] Stretch,
20    Horizontal,
21    Vertical,
22    Smallest,
23    Biggest,
24    Size
25}
26
27impl Default for ImageFit {
28    fn default() -> Self {
29        ImageFit::Stretch
30    }
31}
32
33#[derive(Debug, Default, Clone)] 
34pub struct ImageBuffer {
35    pub width: usize,
36    pub height: usize,
37    pub data: Vec<u32>,
38    pub animation: Option<TextureAnimation>,
39}
40
41impl ImageBuffer {
42    pub fn new(in_data: &[u8], width: usize, height: usize) -> Result<ImageBuffer, ImageError> {
43        let mut out = Vec::new();
44        let pixels = width * height;
45        out.resize(pixels, 0u32);
46        // input pixel packing
47        match in_data.len() / pixels {
48            4 => for i in 0..pixels {
49                let r = in_data[i*4];
50                let g = in_data[i*4+1];
51                let b = in_data[i*4+2];
52                let a = in_data[i*4+3];
53                out[i] = ((a as u32)<<24) | ((r as u32)<<16) | ((g as u32)<<8) | ((b as u32)<<0);
54            }
55            3 => for i in 0..pixels {
56                let r = in_data[i*3];
57                let g = in_data[i*3+1];
58                let b = in_data[i*3+2];
59                out[i] = 0xff000000 | ((r as u32)<<16) | ((g as u32)<<8) | ((b as u32)<<0);
60            }
61            2 => for i in 0..pixels {
62                let r = in_data[i*2];
63                let a = in_data[i*2+1];
64                out[i] = ((a as u32)<<24) | ((r as u32)<<16) | ((r as u32)<<8) | ((r as u32)<<0);
65            }
66            1 => for i in 0..pixels {
67                let r = in_data[i];
68                out[i] = ((0xff as u32)<<24) | ((r as u32)<<16) | ((r as u32)<<8) | ((r as u32)<<0);
69            }
70            unsupported => {
71                return Err(ImageError::InvalidPixelAlignment(unsupported));
72            }     
73        }
74        Ok(ImageBuffer {
75            width,
76            height,
77            data: out,
78            animation: None
79        })
80    }
81    
82    pub fn into_new_texture(self, cx:&mut Cx)->Texture{
83        let texture = Texture::new_with_format(cx, TextureFormat::VecBGRAu8_32 {
84            width: self.width,
85            height: self.height,
86            data: Some(self.data),
87            updated: TextureUpdated::Full,
88        });
89        texture.set_animation(cx, self.animation);
90        texture
91    }
92    
93    pub fn from_png(data: &[u8]) -> Result<Self, ImageError> {
94        let mut decoder = PngDecoder::new(data);
95        decoder.decode_headers()?;
96        
97        if decoder.is_animated() {
98            return Ok(Self::decode_animated_png(&mut decoder)?);
99        }
100
101        let image = decoder.decode()?;
102        let decoded_data = image.u8().ok_or(
103            ImageError::PngDecode(PngDecodeErrors::GenericStatic(
104                "Failed to decode PNG image data as a slice of u8 bytes"
105            )),
106        )?;
107        let (width, height) = decoder.get_dimensions().ok_or(
108            ImageError::PngDecode(PngDecodeErrors::GenericStatic(
109                "Failed to get PNG image dimensions"
110            ))
111        )?;
112        Self::new(&decoded_data, width, height)
113    }
114
115    fn decode_animated_png(decoder: &mut PngDecoder<&[u8]>) -> Result<ImageBuffer, ImageError> {
116        let colorspace = decoder.get_colorspace().ok_or(
117            ImageError::PngDecode(PngDecodeErrors::GenericStatic(
118                "Failed to get animated PNG colorspace"
119            ))
120        )?;
121        let (width, height) = decoder.get_dimensions().ok_or(
122            ImageError::PngDecode(PngDecodeErrors::GenericStatic(
123                "Failed to get animated PNG image dimensions"
124            ))
125        )?;
126        let actl_info = decoder.actl_info().ok_or(
127            ImageError::PngDecode(PngDecodeErrors::GenericStatic(
128                "Failed to get animated PNG actl info"
129            ))
130        )?;
131
132        let num_components = colorspace.num_components();
133        let mut output = vec![0; width * height * num_components];
134        let fits_horizontal = Cx::max_texture_width() / width;
135        let total_width = fits_horizontal * width;
136        let total_height = ((actl_info.num_frames as usize / fits_horizontal) + 1) * height;
137        let mut final_buffer = ImageBuffer::default();
138        final_buffer.data.resize(total_width * total_height, 0);
139        final_buffer.width = total_width;
140        final_buffer.height = total_height;
141        let mut cx = 0;
142        let mut cy = 0;
143        final_buffer.animation = Some(TextureAnimation {
144            width,
145            height,
146            num_frames: actl_info.num_frames as usize
147        });
148        let mut previous_frame = None;
149        while decoder.more_frames() {
150            // decoding a video
151            // decode the header, in case we haven't processed a frame header
152            decoder.decode_headers()?;
153            // then decode the current frame information,
154            // NB: Frame information is for current frame hence should be accessed before decoding the frame
155            // as it will change on subsequent frames
156            let frame = decoder.frame_info().expect("to have already been decoded");
157            // decode the raw pixels, even on smaller frames, we only allocate frame_info.width*frame_info.height
158            let pix = decoder.decode_raw()?;
159            // Get the PNG image info here instead of outside the loop, which prevents borrow checker errors.
160            // It is way more efficient to do this here instead of to clone the PngInfo outside of this loop.
161            let info = decoder.get_info().ok_or(
162                ImageError::PngDecode(PngDecodeErrors::GenericStatic(
163                    "Failed to get animated PNG image info"
164                ))
165            )?;
166            // call post process
167            post_process_image(
168                &info,
169                colorspace,
170                &frame,
171                &pix,
172                previous_frame.as_deref(),
173                &mut output,
174                None
175            )?;
176            previous_frame = Some(pix);
177            match num_components {
178                4 => {
179                    for y in 0..height {
180                        for x in 0..width {
181                            let r = output[y * width * 4 + x * 4 + 0];
182                            let g = output[y * width * 4 + x * 4 + 1];
183                            let b = output[y * width * 4 + x * 4 + 2];
184                            let a = output[y * width * 4 + x * 4 + 3];
185                            final_buffer.data[(y+cy) * total_width + (x+cx)] = ((a as u32)<<24) | ((r as u32)<<16) | ((g as u32)<<8) | ((b as u32)<<0);
186                        }
187                    }
188                }
189                3 => {
190                    for y in 0..height {
191                        for x in 0..width {
192                            let r = output[y * width * 3 + x * 3 + 0];
193                            let g = output[y * width * 3 + x * 3 + 1];
194                            let b = output[y * width * 3 + x * 3 + 2];
195                            final_buffer.data[(y+cy) * total_width + (x+cx)] = 0xff000000 | ((r as u32)<<16) | ((g as u32)<<8) | ((b as u32)<<0);
196                        }
197                    }
198                }
199                _ => {
200                    return Err(ImageError::InvalidPixelAlignment(num_components));
201                }     
202            }
203            cx += width;
204            if cx >= total_width {
205                cy += height;
206                cx = 0
207            } 
208        }
209        Ok(final_buffer)
210    }
211
212    pub fn from_jpg(data: &[u8]) -> Result<Self, ImageError> {
213        let mut decoder = JpegDecoder::new(&*data);
214        match decoder.decode() {
215            Ok(data) => {
216                let info = decoder.info().ok_or(
217                    ImageError::JpgDecode(JpgDecodeErrors::FormatStatic(
218                        "Failed to decode JPG image info"
219                    )),
220                )?;
221                ImageBuffer::new(&data, info.width as usize, info.height as usize)
222            },
223            Err(err) => Err(ImageError::JpgDecode(err)),
224        }
225    }
226}
227
228pub enum ImageCacheEntry{
229    Loaded(Texture),
230    Loading(usize, usize),
231}
232
233#[derive(Debug)]
234pub struct AsyncImageLoad{
235    pub image_path: PathBuf,
236    pub result: RefCell<Option<Result<ImageBuffer, ImageError>>>
237}
238
239pub struct ImageCache {
240    map: HashMap<PathBuf, ImageCacheEntry>,
241    pub thread_pool: Option<TagThreadPool<PathBuf>>,
242}
243
244impl ImageCache {
245    pub fn new() -> Self {
246        Self {
247            map: HashMap::new(),
248            thread_pool: None,
249        }
250    }
251}
252
253
254/// The possible errors that can occur when loading or creating an image texture.
255#[derive(Debug)]
256pub enum ImageError {
257    /// The image data buffer was empty or otherwise invalid.
258    EmptyData,
259    /// The image's pixel data was not aligned to 3-byte or 4-byte pixels.
260    /// The unsupported alignment value (in bytes) is included.
261    InvalidPixelAlignment(usize),
262    /// The image data could not be decoded as a JPEG.
263    JpgDecode(JpgDecodeErrors),
264    /// The image file at the given resource path could not be found.
265    PathNotFound(PathBuf),
266    /// The image data could not be decoded as a PNG.
267    PngDecode(PngDecodeErrors),
268    /// The image data was in an unsupported format.
269    /// Currently, only JPEG and PNG are supported.
270    UnsupportedFormat,
271}
272
273pub enum AsyncLoadResult{
274    Loading(usize,usize),
275    Loaded,
276}
277
278impl Error for ImageError {}
279
280impl From<PngDecodeErrors> for ImageError {
281    fn from(value: PngDecodeErrors) -> Self {
282        Self::PngDecode(value)
283    }
284}
285
286impl std::fmt::Display for ImageError {
287    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
288        write!(f, "{self:?}")
289    }
290}
291
292pub trait ImageCacheImpl {
293    fn get_texture(&self, id:usize) -> &Option<Texture>;
294    fn set_texture(&mut self, texture: Option<Texture>,id: usize);
295    
296    fn lazy_create_image_cache(&mut self,cx: &mut Cx) {
297        if !cx.has_global::<ImageCache>() {
298            cx.set_global(ImageCache::new());
299        }
300    }
301
302    fn load_png_from_data(&mut self, cx: &mut Cx, data: &[u8], id:usize) -> Result<(), ImageError> {
303        match ImageBuffer::from_png(&*data){
304            Ok(data)=>{
305                self.set_texture(Some(data.into_new_texture(cx)), id);
306                Ok(())
307            }
308            Err(err)=>{
309                Err(err)
310            }
311        }
312    }
313    
314    fn load_jpg_from_data(&mut self, cx: &mut Cx, data: &[u8], id:usize) -> Result<(), ImageError> {
315        match ImageBuffer::from_jpg(&*data){
316            Ok(data)=>{
317                self.set_texture(Some(data.into_new_texture(cx)), id);
318                Ok(())
319            }
320            Err(err)=>{
321                Err(err)
322            }
323        }
324    }
325    
326    fn image_size_by_data(data:&[u8], image_path:&Path)-> Result<(usize,usize), ImageError> {
327        if image_path.extension().map(|s| s == "jpg").unwrap_or(false) {
328            let mut decoder = JpegDecoder::new(&*data);
329            decoder.decode_headers().unwrap();
330            let image_info = decoder.info().unwrap();
331            return Ok((image_info.width as usize,image_info.height as usize))
332        } 
333        else if image_path.extension().map(|s| s == "png").unwrap_or(false) {
334            let mut decoder = PngDecoder::new(data);
335            decoder.decode_headers()?;
336            let (width,height) = decoder.get_dimensions().ok_or(
337                ImageError::PngDecode(PngDecodeErrors::GenericStatic(
338                    "Failed to get animated PNG image dimensions"
339                ))
340            )?;
341            return Ok((width,height))
342                                                            
343        } else {
344            return Err(ImageError::UnsupportedFormat)
345        }
346    }
347    
348    fn image_size_by_path(image_path:&Path)-> Result<(usize,usize), ImageError> {
349        if let Ok(mut f) = File::open(image_path){
350            let mut data = vec![0u8;1024]; // yolo chunk size
351            match f.read(&mut data) {
352                Ok(_len) => {
353                    Self::image_size_by_data(&data, image_path)
354                }
355                Err(err) => {
356                    error!("load_image_file_by_path: Resource not found {:?} {}", image_path, err);
357                    return Err(ImageError::PathNotFound(image_path.into()))
358                }
359            }
360        }
361        else{
362            error!("load_image_file_by_path: File not found {:?}", image_path);
363            return Err(ImageError::PathNotFound(image_path.into()))
364        }
365    }
366        
367    fn process_async_image_load(&mut self, cx:&mut Cx, image_path: &Path, result: Result<ImageBuffer, ImageError>)->bool{
368        // alright now we should stuff this thing into our cache
369        if let Ok(data) = result{
370            let texture = data.into_new_texture(cx);
371            cx.get_global::<ImageCache>().map.insert(image_path.into(), ImageCacheEntry::Loaded(texture.clone()));
372        }
373        false
374    }
375    
376    fn load_image_from_cache(&mut self, cx:&mut Cx, image_path: &Path, id: usize)->bool{
377        if let Some(texture) = cx.get_global::<ImageCache>().map.get(image_path){
378            match texture{
379                ImageCacheEntry::Loaded(texture)=>{
380                    self.set_texture(Some(texture.clone()), id);
381                    return true
382                }
383                _=>()
384            }
385        }
386        false
387    }
388    
389    fn load_image_from_data_async_impl(
390        &mut self,
391        cx: &mut Cx,
392        image_path: &Path,
393        data: Arc<Vec<u8>>,
394        id: usize,
395    ) -> Result<AsyncLoadResult, ImageError> {
396        if let Some(texture) = cx.get_global::<ImageCache>().map.get(image_path){
397            match texture{
398                ImageCacheEntry::Loaded(texture)=>{
399                    let texture = texture.clone();
400                    // lets fetch the texture size
401                    //let (_w,_h) = texture.get_format(cx).vec_width_height().unwrap_or((100,100));
402                    self.set_texture(Some(texture), id);
403                    Ok(AsyncLoadResult::Loaded)
404                }
405                ImageCacheEntry::Loading(w,h)=>{
406                    Ok(AsyncLoadResult::Loading(*w, *h))
407                }
408            }
409        }
410        else{
411            if  cx.get_global::<ImageCache>().thread_pool.is_none(){
412                cx.get_global::<ImageCache>().thread_pool = Some(TagThreadPool::new(cx, cx.cpu_cores().max(3) - 2))
413            }
414            let (w,h) = Self::image_size_by_data(&*data, image_path)?;
415            // open image file and read the headers
416            cx.get_global::<ImageCache>().map.insert(image_path.into(), ImageCacheEntry::Loading(w,h));
417                        
418            cx.get_global::<ImageCache>().thread_pool.as_mut().unwrap().execute_rev(image_path.into(), move |image_path|{
419                if image_path.extension().map(|s| s == "jpg").unwrap_or(false) {
420                    match ImageBuffer::from_jpg(&*data){
421                        Ok(data)=>{
422                            Cx::post_action(AsyncImageLoad{
423                                image_path, 
424                                result: RefCell::new(Some(Ok(data)))
425                            });
426                        }
427                        Err(err)=>{
428                            Cx::post_action(AsyncImageLoad{
429                                image_path, 
430                                result: RefCell::new(Some(Err(err)))
431                            });
432                        }
433                    }
434                } else if image_path.extension().map(|s| s == "png").unwrap_or(false) {
435                    match ImageBuffer::from_png(&*data){
436                        Ok(data)=>{
437                            Cx::post_action(AsyncImageLoad{
438                                image_path, 
439                                result: RefCell::new(Some(Ok(data)))
440                            });
441                        }
442                        Err(err)=>{
443                            Cx::post_action(AsyncImageLoad{
444                                image_path, 
445                                result: RefCell::new(Some(Err(err)))
446                            });
447                        }
448                    }
449                } else {
450                    Cx::post_action(AsyncImageLoad{
451                        image_path, 
452                        result: RefCell::new(Some(Err(ImageError::UnsupportedFormat)))
453                    });
454                }
455            });
456            Ok(AsyncLoadResult::Loading(w, h))
457        }
458    }
459    
460    fn load_image_file_by_path_async_impl(
461        &mut self,
462        cx: &mut Cx,
463        image_path: &Path,
464        id: usize,
465    ) -> Result<AsyncLoadResult, ImageError> {
466        if let Some(texture) = cx.get_global::<ImageCache>().map.get(image_path){
467            match texture{
468                ImageCacheEntry::Loaded(texture)=>{
469                    let texture = texture.clone();
470                    // lets fetch the texture size
471                    //let (_w,_h) = texture.get_format(cx).vec_width_height().unwrap_or((100,100));
472                    self.set_texture(Some(texture), id);
473                    Ok(AsyncLoadResult::Loaded)
474                }
475                ImageCacheEntry::Loading(w,h)=>{
476                    Ok(AsyncLoadResult::Loading(*w, *h))
477                }
478            }
479        }
480        else{
481            if  cx.get_global::<ImageCache>().thread_pool.is_none(){
482                 cx.get_global::<ImageCache>().thread_pool = Some(TagThreadPool::new(cx, cx.cpu_cores().max(3) - 2))
483            }
484            let (w,h) = Self::image_size_by_path(image_path)?;
485            // open image file and read the headers
486            cx.get_global::<ImageCache>().map.insert(image_path.into(), ImageCacheEntry::Loading(w,h));
487            
488            cx.get_global::<ImageCache>().thread_pool.as_mut().unwrap().execute_rev(image_path.into(), move |image_path|{
489                if let Ok(mut f) = File::open(&image_path){
490                    let mut data = Vec::new();
491                    match f.read_to_end(&mut data) {
492                        Ok(_len) => {        
493                            if image_path.extension().map(|s| s == "jpg").unwrap_or(false) {
494                                match ImageBuffer::from_jpg(&*data){
495                                    Ok(data)=>{
496                                        Cx::post_action(AsyncImageLoad{
497                                            image_path, 
498                                            result: RefCell::new(Some(Ok(data)))
499                                        });
500                                    }
501                                    Err(err)=>{
502                                        Cx::post_action(AsyncImageLoad{
503                                            image_path, 
504                                            result: RefCell::new(Some(Err(err)))
505                                        });
506                                    }
507                                }
508                            } else if image_path.extension().map(|s| s == "png").unwrap_or(false) {
509                                match ImageBuffer::from_png(&*data){
510                                    Ok(data)=>{
511                                        Cx::post_action(AsyncImageLoad{
512                                            image_path, 
513                                            result: RefCell::new(Some(Ok(data)))
514                                        });
515                                    }
516                                    Err(err)=>{
517                                        Cx::post_action(AsyncImageLoad{
518                                            image_path, 
519                                            result: RefCell::new(Some(Err(err)))
520                                        });
521                                    }
522                                }
523                            } else {
524                                Cx::post_action(AsyncImageLoad{
525                                    image_path, 
526                                    result: RefCell::new(Some(Err(ImageError::UnsupportedFormat)))
527                                });
528                            }
529                        }
530                        Err(_err) => {
531                            Cx::post_action(AsyncImageLoad{
532                                image_path: image_path.clone(), 
533                                result: RefCell::new(Some(Err(ImageError::PathNotFound(image_path))))
534                            });
535                        }
536                    }
537                }
538                else{
539                    Cx::post_action(AsyncImageLoad{
540                        image_path: image_path.clone(), 
541                        result: RefCell::new(Some(Err(ImageError::PathNotFound(image_path))))
542                    });
543                }
544            });
545            Ok(AsyncLoadResult::Loading(w, h))
546        }
547    }
548    
549    fn load_image_file_by_path_and_data(&mut self, cx:&mut Cx, data:&[u8], id:usize, image_path:&Path)-> Result<(), ImageError> {
550        if image_path.extension().map(|s| s == "jpg").unwrap_or(false) {
551            match ImageBuffer::from_jpg(&*data){
552                Ok(data)=>{
553                    let texture = data.into_new_texture(cx);
554                    cx.get_global::<ImageCache>().map.insert(image_path.into(), ImageCacheEntry::Loaded(texture.clone()));
555                    self.set_texture(Some(texture), id);
556                    Ok(())
557                }
558                Err(err)=>{
559                    error!("load_image_file_by_path_and_data: Cannot load jpeg image from path: {:?} {}", image_path, err);
560                    Err(err)
561                }
562            }
563        } else if image_path.extension().map(|s| s == "png").unwrap_or(false) {
564            match ImageBuffer::from_png(&*data){
565                Ok(data)=>{
566                    let texture = data.into_new_texture(cx);
567                    cx.get_global::<ImageCache>().map.insert(image_path.into(), ImageCacheEntry::Loaded(texture.clone()));
568                    self.set_texture(Some(texture), id);
569                    Ok(())
570                }
571                Err(err)=>{
572                    error!("load_image_file_by_path_and_data: Cannot load png image from path: {:?} {}", image_path, err);
573                    Err(err)
574                }
575            }
576        } else {
577            error!("load_image_file_by_path_and_data: Image format not supported {:?}", image_path);
578            Err(ImageError::UnsupportedFormat)
579        }
580    }
581        
582    fn load_image_file_by_path(
583        &mut self,
584        cx: &mut Cx,
585        image_path: &Path,
586        id: usize,
587    ) -> Result<(), ImageError> {
588        if let Some(ImageCacheEntry::Loaded(texture)) = cx.get_global::<ImageCache>().map.get(image_path){
589            self.set_texture(Some(texture.clone()), id);
590            Ok(())
591        }
592        else{
593            if let Ok(mut f) = File::open(image_path){
594                let mut data = Vec::new();
595                match f.read_to_end(&mut data) {
596                    Ok(_len) => {
597                        self.load_image_file_by_path_and_data(cx, &data, id, image_path)
598                    }
599                    Err(err) => {
600                        error!("load_image_file_by_path: Resource not found {:?} {}", image_path, err);
601                        Err(ImageError::PathNotFound(image_path.into()))
602                    }
603                }
604            }
605            else{
606                error!("load_image_file_by_path: File not found {:?}", image_path);
607                Err(ImageError::PathNotFound(image_path.into()))
608            }
609        }
610    }
611    
612    fn load_image_dep_by_path(
613        &mut self,
614        cx: &mut Cx,
615        image_path: &str,
616        id: usize,
617    ) -> Result<(), ImageError> {
618        let p_image_path = Path::new(image_path);
619        if let Some(ImageCacheEntry::Loaded(texture)) = cx.get_global::<ImageCache>().map.get(p_image_path){
620            self.set_texture(Some(texture.clone()), id);
621            Ok(())
622        } 
623        else{
624            match cx.take_dependency(image_path) {
625                Ok(data) => {
626                    self.load_image_file_by_path_and_data(cx, &data, id, p_image_path)
627                }
628                Err(err) => {
629                    error!("load_image_dep_by_path: Resource not found {} {}", image_path, err);
630                    Err(ImageError::PathNotFound(image_path.into()))
631                }
632            }
633        }
634    }
635}