use crate::{Attributes, Diagnostic, ResourceProvider, ResourceRequest, Severity, Span};
pub(crate) struct ImageSource<'a> {
pub provider: &'a dyn ResourceProvider,
pub referrer: String,
pub referrer_dir: Option<String>,
pub base_dir: Option<String>,
pub safe: crate::SafeMode,
}
pub(crate) struct Images<'a> {
source: Option<ImageSource<'a>>,
inline_macros: Option<InlineMacros>,
}
pub(crate) struct InlineMacros {
pub registry: std::sync::Arc<crate::Registry>,
pub safe: crate::SafeMode,
pub base_dir: Option<String>,
pub output: Option<crate::OutputTarget>,
pub mark: std::cell::Cell<Option<crate::DiagnosticCursor>>,
pub markup: Option<std::sync::Arc<dyn crate::extension::Markup>>,
}
impl<'a> Images<'a> {
pub fn new(source: Option<ImageSource<'a>>) -> Self {
Self {
source,
inline_macros: None,
}
}
pub fn with_inline_macros(mut self, inline_macros: Option<InlineMacros>) -> Self {
self.inline_macros = inline_macros;
self
}
pub fn inline_macros(&self) -> Option<&InlineMacros> {
self.inline_macros.as_ref()
}
fn system_path(
&self,
target: &str,
start: Option<&str>,
label: &str,
span: Span,
diagnostics: &mut Vec<Diagnostic>,
) -> (String, String) {
let (base_dir, referrer_dir, safe) = match &self.source {
Some(source) => (
source.base_dir.as_deref(),
source.referrer_dir.as_deref(),
source.safe,
),
None => (None, None, crate::SafeMode::default()),
};
let resolved =
crate::system_path::asset_path(target, start, base_dir, referrer_dir, safe, label);
for warning in resolved.warnings {
warn(diagnostics, span, warning);
}
(resolved.path, resolved.request)
}
pub fn uri(
&self,
target: &str,
dir: Option<&str>,
attrs: &Attributes,
span: Span,
diagnostics: &mut Vec<Diagnostic>,
) -> String {
if crate::context::SafeMode::of(attrs) >= crate::context::SafeMode::Secure
|| attrs.get("data-uri").is_none()
{
return crate::image::web_path(target, dir);
}
let uri = if uriish(target) {
Some(target.replace(' ', "%20"))
} else {
dir.filter(|dir| uriish(dir))
.map(|dir| crate::image::normalize_web_path(target, Some(dir), false))
};
match uri {
Some(uri) if attrs.get("allow-uri-read").is_some() => {
self.enable_cache(attrs);
self.embed_uri(&uri, span, diagnostics)
}
Some(uri) => uri,
None => self.embed_file(target, dir, span, diagnostics),
}
}
fn embed_file(
&self,
target: &str,
dir: Option<&str>,
span: Span,
diagnostics: &mut Vec<Diagnostic>,
) -> String {
let mimetype = match extname(target) {
Some(".svg") => "image/svg+xml".to_owned(),
Some(ext) => format!("image/{}", &ext[1..]),
None => "application/octet-stream".to_owned(),
};
let (path, request) = self.system_path(target, dir, "image", span, diagnostics);
match self.load(&request) {
Ok((_, bytes)) => format!("data:{mimetype};base64,{}", base64(&bytes)),
Err(name) => {
let name = if self.resolves() { path } else { name };
warn(
diagnostics,
span,
format!("image to embed not found or not readable: {name}"),
);
format!("data:{mimetype};base64,")
}
}
}
fn embed_uri(&self, uri: &str, span: Span, diagnostics: &mut Vec<Diagnostic>) -> String {
match self.load(uri) {
Ok((content_type, bytes)) => format!(
"data:{};base64,{}",
content_type.unwrap_or_else(|| "application/octet-stream".to_owned()),
base64(&bytes)
),
Err(_) => {
warn(
diagnostics,
span,
format!("could not retrieve image data from URI: {uri}"),
);
uri.to_owned()
}
}
}
#[allow(clippy::too_many_arguments)]
pub fn svg(
images: Option<&Self>,
target: &str,
named: &std::collections::BTreeMap<String, String>,
options: &[String],
attrs: &Attributes,
span: Span,
diagnostics: &mut Vec<Diagnostic>,
) -> Option<crate::Svg> {
let svg = named.get("format").is_some_and(|f| f == "svg") || target.contains(".svg");
if !svg || crate::context::SafeMode::of(attrs) >= crate::context::SafeMode::Secure {
return None;
}
let none = Self::new(None);
let images = images.unwrap_or(&none);
let option = |name: &str| options.iter().any(|o| o == name);
if option("inline") {
Some(crate::Svg::Inline(images.read_svg(
target,
attrs.get("imagesdir"),
named.get("docfile").map(String::as_str),
attrs,
span,
diagnostics,
)))
} else if option("interactive") {
let fallback = named.get("fallback").map(|fallback| {
images.uri(fallback, attrs.get("imagesdir"), attrs, span, diagnostics)
});
Some(crate::Svg::Interactive { fallback })
} else {
None
}
}
#[allow(clippy::too_many_arguments)]
fn read_svg(
&self,
target: &str,
dir: Option<&str>,
docfile: Option<&str>,
attrs: &Attributes,
span: Span,
diagnostics: &mut Vec<Diagnostic>,
) -> Option<String> {
let uri = if uriish(target) {
Some(target.to_owned())
} else {
dir.filter(|dir| uriish(dir))
.map(|dir| crate::image::normalize_web_path(target, Some(dir), false))
};
let (name, text) = match uri {
Some(uri) if attrs.get("allow-uri-read").is_none() => {
warn(
diagnostics,
span,
format!("cannot retrieve contents of SVG at URI: {uri} (allow-uri-read attribute not enabled)"),
);
return None;
}
Some(uri) => {
self.enable_cache(attrs);
match self.load_named(&uri) {
Ok(loaded) => loaded,
Err(_) => {
warn(
diagnostics,
span,
format!("could not retrieve contents of SVG at URI: {uri}"),
);
return None;
}
}
}
None => {
let (path, request) = self.system_path(target, dir, "SVG", span, diagnostics);
match self.load_named(&request) {
Ok((name, text)) if !self.resolves() => (name, text),
Ok((_, text)) => (path, text),
Err(name) => {
let name = if self.resolves() { path } else { name };
let docfile = docfile.unwrap_or("<stdin>");
warn(
diagnostics,
span,
format!("{docfile}: SVG does not exist or cannot be read: {name}"),
);
return None;
}
}
}
};
let text = text.strip_prefix('\u{feff}').unwrap_or(&text);
let contents = text
.split_inclusive('\n')
.map(|line| line.trim_end_matches([' ', '\t', '\n', '\x0b', '\x0c', '\r', '\0']))
.collect::<Vec<_>>()
.join("\n");
if contents.is_empty() {
warn(
diagnostics,
span,
format!("contents of SVG is empty: {name}"),
);
return None;
}
Some(contents)
}
fn resolves(&self) -> bool {
self.source
.as_ref()
.is_some_and(|source| source.base_dir.is_some())
}
fn enable_cache(&self, attrs: &Attributes) {
if attrs.get("cache-uri").is_some() {
if let Some(source) = &self.source {
source.provider.enable_uri_cache();
}
}
}
fn load_named(&self, target: &str) -> Result<(String, String), String> {
let Some(ImageSource {
provider, referrer, ..
}) = &self.source
else {
return Err(target.to_owned());
};
provider
.load_asset_text(ResourceRequest {
target,
referrer: Some(referrer.as_str()),
})
.map(|resource| (resource.name, resource.text))
.map_err(|error| error.target)
}
fn load(&self, target: &str) -> Result<(Option<String>, Vec<u8>), String> {
let Some(ImageSource {
provider, referrer, ..
}) = &self.source
else {
return Err(target.to_owned());
};
provider
.load_binary(ResourceRequest {
target,
referrer: Some(referrer.as_str()),
})
.map(|resource| (resource.content_type, resource.bytes))
.map_err(|error| error.target)
}
}
fn warn(diagnostics: &mut Vec<Diagnostic>, span: Span, message: String) {
diagnostics.push(Diagnostic {
omitted_span: None,
report: crate::DiagnosticReport {
location: crate::DiagnosticLocation::Unlocated,
..Default::default()
},
span,
severity: Severity::Warning,
message,
});
}
fn uriish(s: &str) -> bool {
s.contains(':') && crate::image::uri_prefix(s).is_some()
}
fn extname(path: &str) -> Option<&str> {
let dot = path.rfind('.')?;
(!path[dot..].contains('/')).then(|| &path[dot..])
}
fn base64(bytes: &[u8]) -> String {
const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4);
for chunk in bytes.chunks(3) {
let n = chunk
.iter()
.enumerate()
.fold(0u32, |n, (i, &b)| n | u32::from(b) << (16 - 8 * i));
for i in 0..4 {
if i <= chunk.len() {
out.push(char::from(ALPHABET[(n >> (18 - 6 * i)) as usize & 63]));
} else {
out.push('=');
}
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn base64_is_strict() {
assert_eq!(base64(b""), "");
assert_eq!(base64(b"f"), "Zg==");
assert_eq!(base64(b"fo"), "Zm8=");
assert_eq!(base64(b"foo"), "Zm9v");
assert_eq!(base64(&[0xff, 0xfe, 0x00, 0x01]), "//4AAQ==");
}
#[test]
fn extname_is_rubys() {
assert_eq!(extname("a/b.svg"), Some(".svg"));
assert_eq!(extname("a.d/b"), None);
assert_eq!(extname("b"), None);
}
}