use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct ExportImage {
pub bytes: Vec<u8>,
pub mime_type: String,
}
impl ExportImage {
pub fn new(bytes: impl Into<Vec<u8>>, mime_type: impl Into<String>) -> Self {
Self {
bytes: bytes.into(),
mime_type: mime_type.into(),
}
}
pub fn extension(&self) -> &'static str {
match self.mime_type.as_str() {
"image/png" => "png",
"image/jpeg" => "jpg",
"image/webp" => "webp",
"image/gif" => "gif",
"image/svg+xml" => "svg",
_ => "bin",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct ExportImages(BTreeMap<String, ExportImage>);
impl ExportImages {
pub fn new() -> Self {
Self::default()
}
pub fn insert(&mut self, src: impl Into<String>, image: ExportImage) -> &mut Self {
self.0.insert(src.into(), image);
self
}
pub fn get(&self, src: &str) -> Option<&ExportImage> {
self.0.get(src)
}
pub fn iter(&self) -> impl Iterator<Item = (&String, &ExportImage)> {
self.0.iter()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn len(&self) -> usize {
self.0.len()
}
}
impl<S: Into<String>> FromIterator<(S, ExportImage)> for ExportImages {
fn from_iter<I: IntoIterator<Item = (S, ExportImage)>>(iter: I) -> Self {
Self(iter.into_iter().map(|(s, i)| (s.into(), i)).collect())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum HtmlImageMode {
#[default]
Reference,
DataUri,
Omit,
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct HtmlExportOptions {
pub image_mode: HtmlImageMode,
#[serde(default)]
pub images: ExportImages,
}
pub fn base64_encode(bytes: &[u8]) -> String {
use base64::Engine;
base64::engine::general_purpose::STANDARD.encode(bytes)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn base64_matches_the_rfc_test_vectors() {
for (input, expected) in [
("", ""),
("f", "Zg=="),
("fo", "Zm8="),
("foo", "Zm9v"),
("foob", "Zm9vYg=="),
("fooba", "Zm9vYmE="),
("foobar", "Zm9vYmFy"),
] {
assert_eq!(base64_encode(input.as_bytes()), expected, "{input:?}");
}
}
#[test]
fn base64_handles_bytes_above_ascii() {
assert_eq!(base64_encode(&[0xff, 0xfe, 0xfd]), "//79");
assert_eq!(base64_encode(&[0x00, 0x00, 0x00]), "AAAA");
}
#[test]
fn extension_falls_back_visibly_for_unknown_types() {
assert_eq!(ExportImage::new(vec![], "image/png").extension(), "png");
assert_eq!(ExportImage::new(vec![], "image/jpeg").extension(), "jpg");
assert_eq!(
ExportImage::new(vec![], "application/x-thing").extension(),
"bin"
);
}
#[test]
fn images_iterate_in_a_stable_order() {
let build = || {
ExportImages::from_iter([
("z.png", ExportImage::new(vec![1], "image/png")),
("a.png", ExportImage::new(vec![2], "image/png")),
("m.png", ExportImage::new(vec![3], "image/png")),
])
};
let first: Vec<String> = build().iter().map(|(k, _)| k.clone()).collect();
let second: Vec<String> = build().iter().map(|(k, _)| k.clone()).collect();
assert_eq!(first, second);
assert_eq!(first, vec!["a.png", "m.png", "z.png"]);
}
}