1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
use once_cell::sync::Lazy;
use percent_encoding::{utf8_percent_encode, DEFAULT_ENCODE_SET};
use regex::{Captures, Regex};
use std::borrow::Cow;

static WHITESPACES_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"\s+").unwrap());
static URL_HEX_PAIRS_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"%[\dA-F]{2}").unwrap());

trait SvgDataUriUtils: AsRef<str> {
    fn trim_byte_order_mark(&self) -> &str {
        let string = self.as_ref();
        match string.chars().nth(0) {
            Some('\u{FEFF}') => &string[1..],
            _ => string,
        }
    }

    fn collapse_whitespace(&self) -> Cow<str> {
        WHITESPACES_REGEX.replace_all(self.as_ref(), " ")
    }

    fn encode_uri_components(&self) -> Cow<str> {
        let string = self.as_ref();
        utf8_percent_encode(string, DEFAULT_ENCODE_SET).collect()
    }

    fn special_hex_encode(&self) -> Cow<str> {
        let string = self.as_ref();
        URL_HEX_PAIRS_REGEX.replace_all(string, |captures: &Captures| {
            match captures.get(0).map(|capture| capture.as_str()) {
                Some("%20") => " ".to_string(),
                Some("%3D") => "=".to_string(),
                Some("%3A") => ":".to_string(),
                Some("%2F") => "/".to_string(),
                Some(hex) => hex.to_lowercase(),
                None => "".to_string(),
            }
        })
    }
}

impl<T: AsRef<str>> SvgDataUriUtils for T {}

pub fn svg_str_to_data_uri(svg: impl AsRef<str>) -> String {
    format!(
        "data:image/svg+xml,{}",
        svg.trim_byte_order_mark()
            .trim()
            .collapse_whitespace()
            .replace('"', "'")
            .encode_uri_components()
            .special_hex_encode()
    )
}

pub fn image_to_png_data_uri<T>(image: &T) -> image::ImageResult<String>
where
    T: image::GenericImageView,
    T::InnerImageView: std::ops::Deref<Target = [u8]>,
{
    use image::Pixel;
    let mut buffer = Vec::new();
    let encoder = image::png::PNGEncoder::new(&mut buffer);
    encoder.encode(
        image.inner(),
        image.width(),
        image.height(),
        T::Pixel::color_type(),
    )?;
    Ok([
        "data:",
        mime::IMAGE_PNG.as_ref(),
        ";base64,",
        base64::encode(&buffer).as_ref(),
    ]
    .iter()
    .cloned()
    .collect())
}

pub fn image_to_jpeg_data_uri<T>(image: &T, quality: u8) -> image::ImageResult<String>
where
    T: image::GenericImageView,
    T::InnerImageView: std::ops::Deref<Target = [u8]>,
{
    use image::Pixel;
    let mut buffer = Vec::new();
    let mut encoder = image::jpeg::JPEGEncoder::new_with_quality(&mut buffer, quality);
    encoder.encode(
        image.inner(),
        image.width(),
        image.height(),
        T::Pixel::color_type(),
    )?;
    Ok([
        "data:",
        mime::IMAGE_JPEG.as_ref(),
        ";base64,",
        base64::encode(&buffer).as_ref(),
    ]
    .iter()
    .cloned()
    .collect())
}

#[cfg(test)]
mod tests {
    use crate::*;

    #[test]
    fn full_test() {
        let svg = r##"
            <svg xmlns="http://www.w3.org/2000/svg" viewBox="#000000 0 50 50">
                <path d="M22 38V51L32 32l19-19v12C44 26 43 10 38 0 52 15 49 39 22 38z"/>
            </svg>"##;
        let expected = r#"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 50 50'%3e %3cpath d='M22 38V51L32 32l19-19v12C44 26 43 10 38 0 52 15 49 39 22 38z'/%3e %3c/svg%3e"#;
        let result = svg_str_to_data_uri(svg);
        assert_eq!(result, expected);
    }
}