stapix

Yet another static page generator for photo galleries

Commit
2624bb554ef0db20f193fd539d57a83ff73893a5
Parent
114e44204990754bac9e392cc5ce5986ad4260c5
Author
Pablo <pablo-escobar@riseup.net>
Date

Implemented rendering of licensing info

Implemented a feature to read and write info on each picture's author and the correspoding redistribution license

Diffstat

5 files changed, 401 insertions, 21 deletions

Status File Name N° Changes Insertions Deletions
Modified Cargo.lock 2 1 1
Modified Cargo.toml 2 1 1
Modified README.md 33 20 13
Modified src/main.rs 26 24 2
Modified src/picture.rs 359 355 4
diff --git a/Cargo.lock b/Cargo.lock
@@ -584,7 +584,7 @@ dependencies = [
 
 [[package]]
 name = "stapix"
-version = "0.1.0"
+version = "0.2.0"
 dependencies = [
  "crossterm",
  "image",
diff --git a/Cargo.toml b/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "stapix"
-version = "0.1.1"
+version = "0.2.0"
 edition = "2021"
 license = "GPLv3"
 
diff --git a/README.md b/README.md
@@ -26,34 +26,41 @@ To use stapix run:
 $ stapix config.yml
 ```
 
-The configuration file `config.yml` should consist of a list of file-paths
-pointing to the images the user wants in the gallery and text alternatives
-(visual descriptions) for each image, such as in the following example:
+The configuration file `config.yml` should consist of a list of struct entries
+describing the contents of each picture, such as in the following example:
 
 ```yaml
 - path: ./path/to/first.jpg
   alt: "Text alternative for the first photo"
+  author: John Doe
+  license: PD
 - path: ./path/to/second.png
   alt: "Text alternative for the second photo"
+  author: Jane Doe
+  license: CC-BY-SA-4
 ...
 ```
 
-Each entry on the list may additionally contain a field named `caption`
-containing additional information to be displayed in the captions of the
-corresponding image, as in the following example:
+Each entry in the list should contain the following fields:
 
-```yaml
-- path: ./path/to/third.jpg
-  alt: "Text alternative for the first photo"
-  caption: "The third picture of the gallery"
-```
+* **`path`:** The path to the file in question
+* **`alt`:** Text altenative for the picture
+* **`caption` (optional):** A description of the picture
+* **`author`:** The name of the author of the picture
+* **`author-url` (optional):** A URL to a webpage by/on the picture's author
+* **`license`:** The license type of the picture. Should be one of
+`PD` (public domain), `CC0`, `CC-BY-1`, `CC-BY-2`, `CC-BY-3`, `CC-BY-4`,
+`CC-BY-NC-1`, `CC-BY-NC-2`, `CC-BY-NC-3`, `CC-BY-NC-4`, `CC-BY-NC-SA-1`,
+`CC-BY-NC-SA-2`, `CC-BY-NC-SA-3`, `CC-BY-NC-SA-4`, `CC-BY-ND-1`, `CC-BY-ND-2`,
+`CC-BY-ND-3`, `CC-BY-ND-4`, `CC-BY-NC-ND-1`, `CC-BY-NC-ND-2`, `CC-BY-NC-ND-3`
+or `CC-BY-NC-ND-4`
 
 For best accessibility, the `alt` field should contain a concise visual
 description of the picture in question (including subjects, colors and scenery)
 to be displayed by screen readers, while the `caption` field should contain
 _additional_ information on the picture (such as the location or date when it
-was taken) to be displayed for all users. _The `alt` and `caption` attributes
-should not be the same!_ See
+was taken) to be displayed for all users. **The `alt` and `caption` attributes
+should not be the same!** See
 <https://www.htmhell.dev/adventcalendar/2022/22/> for further details.
 
 ## Installation
diff --git a/src/main.rs b/src/main.rs
@@ -109,10 +109,16 @@ macro_rules! usage_config {
                   usage_header_msg = "[USAGE]".yellow().bold());
         eprintln!("    - {path_attr} ./path/to/first.jpg
       {alt_attr} \"Text alternative for the first photo\"
+      {author_attr} John Doe
+      {license_attr} PD
     - {path_attr} ./path/to/second.png
       {alt_attr} \"Text alternative for the second photo\"
+      {author_attr} Jane Doe
+      {license_attr} CC-BY-SA-4
     ...",
-                  path_attr = "path:".green(), alt_attr = "alt:".green());
+                  path_attr = "path:".green(), alt_attr = "alt:".green(),
+                  author_attr = "author:".green(),
+                  license_attr = "license:".green());
     }
 }
 
@@ -329,16 +335,32 @@ fn render_pic_page(pic: &Picture) -> io::Result<()> {
                  name = Escaped(&pic.file_name))?;
     }
     writeln!(f, "<div id=\"picture-container\">")?;
+    writeln!(f, "<div id=\"picture-frame\">")?;
     writeln!(
         f,
         "<img alt=\"{alt}\" src=\"/{PHOTOS_PATH}/{file_name}\">",
         alt = Escaped(&pic.alt),
         file_name = Escaped(&pic.file_name)
     )?;
+    writeln!(f, "<div>")?;
+    if let Some(url) = &pic.author_url {
+        writeln!(f, "by <a role=\"author\" href=\"{url}\">{author}</a>",
+                 author = Escaped(&pic.author))?;
+    } else {
+        writeln!(f, "by {author}", author = Escaped(&pic.author))?;
+    }
+    if let Some(license) = &pic.license {
+        writeln!(f, "(licensed under <a role=\"license\" href=\"{url}\">{license}</a>)",
+                 url = license.url())?;
+    } else {
+        writeln!(f, "(public domain)")?;
+    }
+    writeln!(f, "</div>")?;
+    writeln!(f, "</div>")?;
     writeln!(f, "</div>")?;
     if let Some(caption) = &pic.caption {
         writeln!(f, "<figcaption>")?;
-        writeln!(f, "{}", Escaped(&caption))?;
+        writeln!(f, "{}", Escaped(caption))?;
         writeln!(f, "</figcaption>")?;
     }
     writeln!(f, "</figure>")?;
diff --git a/src/picture.rs b/src/picture.rs
@@ -2,7 +2,32 @@ use serde::{
     de::{Deserializer, Error, Unexpected},
     Deserialize,
 };
-use std::path::PathBuf;
+use std::{fmt::{self, Display}, path::PathBuf};
+
+const LICENSES: &'static [&'static str] = &[
+    "PD",
+    "CC0",
+    "CC-BY-1",
+    "CC-BY-2",
+    "CC-BY-3",
+    "CC-BY-4",
+    "CC-BY-NC-1",
+    "CC-BY-NC-2",
+    "CC-BY-NC-3",
+    "CC-BY-NC-4",
+    "CC-BY-NC-SA-1",
+    "CC-BY-NC-SA-2",
+    "CC-BY-NC-SA-3",
+    "CC-BY-NC-SA-4",
+    "CC-BY-ND-1",
+    "CC-BY-ND-2",
+    "CC-BY-ND-3",
+    "CC-BY-ND-4",
+    "CC-BY-NC-ND-1",
+    "CC-BY-NC-ND-2",
+    "CC-BY-NC-ND-3",
+    "CC-BY-NC-ND-4",
+];
 
 /// Info on a individual entry on the gallery
 #[derive(Debug, Clone, PartialEq, Eq)]
@@ -11,6 +36,285 @@ pub struct Picture {
     pub file_name: String,
     pub alt: String,
     pub caption: Option<String>,
+    pub license: Option<LicenseType>,
+    pub author: String,
+    pub author_url: Option<String>,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub struct LicenseType(CreativeCommons);
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+enum CreativeCommons {
+    /// Creative Commons (without attribution)
+    CC0,
+    /// Creative Commons Attributions (derivatives allowed)
+    CCBY {
+        version: CreativeCommonsVersion,
+        non_commercial: bool,
+        share_alike: bool,
+    },
+    // The ND (non-derivatives) option excludes the SA (share alike) option
+    /// Creative Commons Attributions Non-Derivatives
+    CCBYND {
+        version: CreativeCommonsVersion,
+        non_commercial: bool,
+    },
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+enum CreativeCommonsVersion {
+    One,
+    Two,
+    Three,
+    Four,
+}
+
+impl LicenseType {
+    pub const fn url(&self) -> &'static str {
+        match self.0 {
+            // CC0
+            CreativeCommons::CC0 => {
+                "https://creativecommons.org/publicdomain/zero/1.0/"
+            },
+            // CC-BY-1
+            CreativeCommons::CCBY {
+                version: CreativeCommonsVersion::One,
+                non_commercial: false,
+                share_alike: false,
+            } => "http://creativecommons.org/licenses/by/1.0/",
+            // CC-BY-2
+            CreativeCommons::CCBY {
+                version: CreativeCommonsVersion::Two,
+                non_commercial: false,
+                share_alike: false,
+            } => "http://creativecommons.org/licenses/by/2.0/",
+            // CC-BY-3
+            CreativeCommons::CCBY {
+                version: CreativeCommonsVersion::Three,
+                non_commercial: false,
+                share_alike: false,
+            } => "http://creativecommons.org/licenses/by/3.0/",
+            // CC-BY-4
+            CreativeCommons::CCBY {
+                version: CreativeCommonsVersion::Four,
+                non_commercial: false,
+                share_alike: false,
+            } => "http://creativecommons.org/licenses/by/4.0/",
+            // CC-BY-SA-1
+            CreativeCommons::CCBY {
+                version: CreativeCommonsVersion::One,
+                non_commercial: false,
+                share_alike: true,
+            } => "http://creativecommons.org/licenses/by-sa/1.0/",
+            // CC-BY-SA-2
+            CreativeCommons::CCBY {
+                version: CreativeCommonsVersion::Two,
+                non_commercial: false,
+                share_alike: true,
+            } => "http://creativecommons.org/licenses/by-sa/2.0/",
+            // CC-BY-SA-3
+            CreativeCommons::CCBY {
+                version: CreativeCommonsVersion::Three,
+                non_commercial: false,
+                share_alike: true,
+            } => "http://creativecommons.org/licenses/by-sa/3.0/",
+            // CC-BY-SA-4
+            CreativeCommons::CCBY {
+                version: CreativeCommonsVersion::Four,
+                non_commercial: false,
+                share_alike: true,
+            } => "http://creativecommons.org/licenses/by-sa/4.0/",
+            // CC-BY-NC-1
+            CreativeCommons::CCBY {
+                version: CreativeCommonsVersion::One,
+                non_commercial: true,
+                share_alike: false,
+            } => "http://creativecommons.org/licenses/by-nc/1.0/",
+            // CC-BY-NC-2
+            CreativeCommons::CCBY {
+                version: CreativeCommonsVersion::Two,
+                non_commercial: true,
+                share_alike: false,
+            } => "http://creativecommons.org/licenses/by-nc/2.0/",
+            // CC-BY-NC-3
+            CreativeCommons::CCBY {
+                version: CreativeCommonsVersion::Three,
+                non_commercial: true,
+                share_alike: false,
+            } => "http://creativecommons.org/licenses/by-nc/3.0/",
+            // CC-BY-NC-4
+            CreativeCommons::CCBY {
+                version: CreativeCommonsVersion::Four,
+                non_commercial: true,
+                share_alike: false,
+            } => "http://creativecommons.org/licenses/by-nc/4.0/",
+            // CC-BY-NC-SA-1
+            CreativeCommons::CCBY {
+                version: CreativeCommonsVersion::One,
+                non_commercial: true,
+                share_alike: true,
+            } => "http://creativecommons.org/licenses/by-nc-sa/1.0/",
+            // CC-BY-NC-SA-2
+            CreativeCommons::CCBY {
+                version: CreativeCommonsVersion::Two,
+                non_commercial: true,
+                share_alike: true,
+            } => "http://creativecommons.org/licenses/by-nc-sa/2.0/",
+            // CC-BY-NC-SA-3
+            CreativeCommons::CCBY {
+                version: CreativeCommonsVersion::Three,
+                non_commercial: true,
+                share_alike: true,
+            } => "http://creativecommons.org/licenses/by-nc-sa/3.0/",
+            // CC-BY-NC-SA-4
+            CreativeCommons::CCBY {
+                version: CreativeCommonsVersion::Four,
+                non_commercial: true,
+                share_alike: true,
+            } => "http://creativecommons.org/licenses/by-nc-sa/4.0/",
+            // CC-BY-ND-1
+            CreativeCommons::CCBYND {
+                version: CreativeCommonsVersion::One,
+                non_commercial: false,
+            } => "http://creativecommons.org/licenses/by-nd/1.0/",
+            // CC-BY-ND-2
+            CreativeCommons::CCBYND {
+                version: CreativeCommonsVersion::Two,
+                non_commercial: false,
+            } => "http://creativecommons.org/licenses/by-nd/2.0/",
+            // CC-BY-ND-3
+            CreativeCommons::CCBYND {
+                version: CreativeCommonsVersion::Three,
+                non_commercial: false,
+            } => "http://creativecommons.org/licenses/by-nd/3.0/",
+            // CC-BY-ND-4
+            CreativeCommons::CCBYND {
+                version: CreativeCommonsVersion::Four,
+                non_commercial: false,
+            } => "http://creativecommons.org/licenses/by-nd/4.0/",
+            // CC-BY-NC-ND-1
+            CreativeCommons::CCBYND {
+                version: CreativeCommonsVersion::One,
+                non_commercial: true,
+            } => "http://creativecommons.org/licenses/by-nc-nd/1.0/",
+            // CC-BY-NC-ND-2
+            CreativeCommons::CCBYND {
+                version: CreativeCommonsVersion::Two,
+                non_commercial: true,
+            } => "http://creativecommons.org/licenses/by-nc-nd/2.0/",
+            // CC-BY-NC-ND-3
+            CreativeCommons::CCBYND {
+                version: CreativeCommonsVersion::Three,
+                non_commercial: true,
+            } => "http://creativecommons.org/licenses/by-nc-nd/3.0/",
+            // CC-BY-NC-ND-4
+            CreativeCommons::CCBYND {
+                version: CreativeCommonsVersion::Four,
+                non_commercial: true,
+            } => "http://creativecommons.org/licenses/by-nc-nd/4.0/",
+        }
+    }
+
+    pub fn parse(s: &str) -> Result<Option<Self>, ()> {
+        if s == "PD" {
+            return Ok(None);
+        } else if s == "CC0" {
+            return Ok(Some(Self(CreativeCommons::CC0)));
+        }
+
+        if s.len() < 3 {
+            return Err(());
+        }
+
+        let version = match &s[s.len() - 1..] {
+            "1" => CreativeCommonsVersion::One,
+            "2" => CreativeCommonsVersion::Two,
+            "3" => CreativeCommonsVersion::Three,
+            "4" => CreativeCommonsVersion::Four,
+            _   => return Err(())
+        };
+
+        match &s[..s.len() - 1] {
+            "CC-BY-" => {
+                Ok(
+                    Some(
+                        Self(
+                            CreativeCommons::CCBY {
+                                version,
+                                non_commercial: false,
+                                share_alike: false,
+                            }
+                        )
+                    )
+                )
+            },
+            "CC-BY-NC-" => {
+                Ok(
+                    Some(
+                        Self(
+                            CreativeCommons::CCBY {
+                                version,
+                                non_commercial: true,
+                                share_alike: false,
+                            }
+                        )
+                    )
+                )
+            },
+            "CC-BY-SA-" => {
+                Ok(
+                    Some(
+                        Self(
+                            CreativeCommons::CCBY {
+                                version,
+                                non_commercial: false,
+                                share_alike: true,
+                            }
+                        )
+                    )
+                )
+            },
+            "CC-BY-NC-SA-" => {
+                Ok(
+                    Some(
+                        Self(
+                            CreativeCommons::CCBY {
+                                version,
+                                non_commercial: true,
+                                share_alike: true,
+                            }
+                        )
+                    )
+                )
+            },
+            "CC-BY-ND-" => {
+                Ok(
+                    Some(
+                        Self(
+                            CreativeCommons::CCBYND {
+                                version,
+                                non_commercial: false,
+                            }
+                        )
+                    )
+                )
+            },
+            "CC-BY-NC-ND-" => {
+                Ok(
+                    Some(
+                        Self(
+                            CreativeCommons::CCBYND {
+                                version,
+                                non_commercial: true,
+                            }
+                        )
+                    )
+                )
+            },
+            _ => Err(())
+        }
+    }
 }
 
 impl<'de> Deserialize<'de> for Picture {
@@ -23,23 +327,34 @@ impl<'de> Deserialize<'de> for Picture {
             path: String,
             alt: String,
             caption: Option<String>,
+            license: String,
+            author: String,
+            #[serde(alias = "author-url")]
+            author_url: Option<String>,
         }
 
         let Info {
             path: path_str,
             alt,
             caption,
+            license,
+            author,
+            author_url,
         } = Info::deserialize(deserializer)?;
 
-        let mut path = PathBuf::new();
-        path.push(path_str.clone());
+        let license = LicenseType::parse(&license)
+            .map_err(|_| D::Error::unknown_variant(&license, LICENSES))?;
+        let path = PathBuf::from(&path_str);
 
         if let Some(file_name) = path.file_name().and_then(|s| s.to_str()) {
             Ok(Self {
                 path: path.clone(),
                 alt: alt.trim().to_string(),
                 file_name: String::from(file_name),
-                caption: caption.clone(),
+                caption,
+                author: author.trim().to_string(),
+                author_url: author_url.map(|s| s.trim().to_string()),
+                license,
             })
         } else {
             Err(D::Error::invalid_value(
@@ -49,3 +364,39 @@ impl<'de> Deserialize<'de> for Picture {
         }
     }
 }
+
+impl Display for LicenseType {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
+        match self.0 {
+            CreativeCommons::CC0 => write!(f, "CC0"),
+            CreativeCommons::CCBY { version, non_commercial, share_alike } => {
+                write!(f, "CC-BY-")?;
+                if non_commercial {
+                    write!(f, "NC-")?;
+                }
+                if share_alike {
+                    write!(f, "SA-")?;
+                }
+                write!(f, "{}", version)
+            },
+            CreativeCommons::CCBYND { version, non_commercial } => {
+                write!(f, "CC-BY-")?;
+                if non_commercial {
+                    write!(f, "NC-")?;
+                }
+                write!(f, "ND-{}", version)
+            }
+        }
+    }
+}
+
+impl Display for CreativeCommonsVersion {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
+        match self {
+            CreativeCommonsVersion::One   => write!(f, "1.0"),
+            CreativeCommonsVersion::Two   => write!(f, "2.0"),
+            CreativeCommonsVersion::Three => write!(f, "3.0"),
+            CreativeCommonsVersion::Four  => write!(f, "4.0"),
+        }
+    }
+}