html5_picture/
html5.rs

1use {
2    crate::{core::Config, utils::ResizedImageDetails},
3    serde::{Deserialize, Serialize},
4    std::{collections::HashMap, path::PathBuf},
5};
6
7type PathBufPictureRegister = HashMap<PathBuf, Picture>;
8
9/// Contains information about the MediaWidth property of a ```<picture>``` that
10/// are required for its creation.
11#[derive(Serialize, Deserialize, Debug)]
12pub enum MediaWidth {
13    Max(String),
14    Min(String),
15}
16
17/// Information about the source attributes in a ```<picture>``` tag.
18#[derive(Serialize, Deserialize, Debug)]
19pub struct SourceAttributes {
20    pub media_width: MediaWidth,
21    pub srcset: String,
22}
23
24/// Represents the HTML5 ```<picture>``` tag.
25#[derive(Serialize, Deserialize, Debug)]
26pub struct Picture {
27    /// Contains the `<source>` tags of the picture.
28    pub sources: Vec<SourceAttributes>,
29    /// Specifies the fallback uri of the picture.
30    pub fallback_uri: String,
31}
32
33impl Picture {
34    /// Collects all information about the image required for the creation of a
35    /// ```<picture>``` tag.
36    pub fn from(
37        image_file_name: &PathBuf,
38        scaled_images_count: u8,
39    ) -> Result<Self, String> {
40        if scaled_images_count == 0 {
41            return Err("scaled_images_count must be > 0".to_string());
42        }
43        let resized_image_details =
44            ResizedImageDetails::from(&image_file_name, scaled_images_count)?;
45        let mut sources = vec![];
46        let mut input_dir = image_file_name.clone();
47        input_dir.pop();
48        for details in &resized_image_details {
49            let out_file_name =
50                match input_dir.join(&details.output_file_name).to_str() {
51                    Some(v) => String::from(v),
52                    None => {
53                        return Err(String::from(
54                            "Could not convert output_file_name!",
55                        ));
56                    }
57                };
58            sources.push(SourceAttributes {
59                media_width: MediaWidth::Max(details.width.to_string()),
60                srcset: out_file_name,
61            });
62        }
63        let mut full_scale_image = image_file_name.clone();
64        full_scale_image.set_extension("webp");
65        let full_scale_image = match full_scale_image.to_str() {
66            Some(v) => String::from(v),
67            None => {
68                return Err(String::from(
69                    "Could not convert full_scale_image file name!",
70                ));
71            }
72        };
73        sources.push(SourceAttributes {
74            media_width: MediaWidth::Min(
75                // unwrap allowed as long as scaled_images_count > 0
76                (resized_image_details.last().unwrap().width + 1).to_string(),
77            ),
78            srcset: full_scale_image,
79        });
80
81        Ok(Self {
82            sources,
83            fallback_uri: image_file_name.to_str().unwrap().to_string(),
84        })
85    }
86
87    /// Creates a string that contains the full ```<picture>``` tag. It can
88    /// directly be embedded into a webpage.
89    pub fn to_html_string(
90        &self,
91        srcset_prefix: Option<String>,
92        alt_text: &str,
93    ) -> String {
94        let mut html = String::from("<picture>");
95        let uri_prefix = match &srcset_prefix {
96            Some(v) => format!("{}/", v),
97            None => String::new(),
98        };
99        for src_attrs in &self.sources {
100            let (min_max, value) = match &src_attrs.media_width {
101                MediaWidth::Max(v) => ("max", v),
102                MediaWidth::Min(v) => ("min", v),
103            };
104            html.push_str(&format!(
105                "<source media=\"({}-width: {}px)\" srcset=\"{}{}\">",
106                min_max, value, &uri_prefix, src_attrs.srcset
107            ));
108        }
109        // add fallback image
110        html.push_str(&format!(
111            "<img src=\"{}{}\" alt=\"{}\" />",
112            uri_prefix, self.fallback_uri, alt_text
113        ));
114        html.push_str("</picture>");
115        html
116    }
117}
118
119/// Contains a HashMap with all required details of the images to create an
120/// according picture tag.
121/// The image details are loaded into memory to be able to retrieve them as fast
122/// as possible.
123/// The install_images_into parameter is used to determine which images can be
124/// used.
125#[derive(Debug)]
126pub struct PictureRegister {
127    register: PathBufPictureRegister,
128}
129
130impl PictureRegister {
131    /// Creates a new instance from the given config.
132    pub fn from(config: &Config) -> Result<Self, String> {
133        match &config.install_images_into {
134            None => {
135                return Err(
136                    "The install_images_into parameter needs to be set!"
137                        .to_string(),
138                );
139            }
140            Some(v) => {
141                if !v.is_dir() {
142                    return Err("The install_images_into parameter is not a valid directory".to_string());
143                }
144            }
145        }
146
147        Ok(Self {
148            register: Self::create_register(&config)?,
149        })
150    }
151
152    /// Creates the register from the given config.
153    fn create_register(
154        config: &Config,
155    ) -> Result<PathBufPictureRegister, String> {
156        let images_path = match &config.install_images_into {
157            None => {
158                return Err(
159                    "The install_images_into parameter needs to be set!"
160                        .to_string(),
161                );
162            }
163            Some(v) => {
164                if !v.is_dir() {
165                    return Err("The install_images_into parameter is not a valid directory".to_string());
166                }
167                v
168            }
169        };
170
171        let mut register = PathBufPictureRegister::new();
172        let png_file_names = crate::collect_png_file_names(&images_path, None);
173        for png in png_file_names {
174            let pic = Picture::from(&png, config.scaled_images_count)?;
175            register.insert(png, pic);
176        }
177        Ok(register)
178    }
179
180    /// Returns a reference to the ```Picture``` instance of the given image.
181    /// Please use the original filename of the picture that you want to use,
182    /// for example:
183    /// ```ignore
184    /// let p = register_instance.get(&PathBuf::from("assets/image-1.png")).unwrap();
185    /// ```
186    pub fn get(&self, image: &PathBuf) -> Result<&Picture, String> {
187        match self.register.get(image) {
188            None => Err("Image not found!".to_string()),
189            Some(v) => Ok(v),
190        }
191    }
192}