use crate::{BuildContext, Diagnostic};
use bern_ir::{Application, Node};
pub fn used(app: &Application) -> bool {
let mut used = false;
for node in app
.components
.iter()
.map(|c| &c.body)
.chain(app.windows.windows.iter().map(|w| &w.content))
{
node.walk(&mut |node| {
if matches!(node, Node::Element(e) if e.kind == "Svg") {
used = true;
}
});
}
used
}
pub fn validate(app: &Application, context: &BuildContext) -> Result<(), Vec<Diagnostic>> {
let mut errors = Vec::new();
let mut seen = std::collections::BTreeSet::new();
for node in app
.components
.iter()
.map(|c| &c.body)
.chain(app.windows.windows.iter().map(|w| &w.content))
{
node.walk(&mut |node| {
if let Node::Element(e) = node
&& e.kind == "Svg"
&& let Some(name) = e.resource("source")
&& seen.insert(name.to_owned())
{
let result = std::fs::read_to_string(context.project_dir.join(name))
.map_err(|e| e.to_string())
.and_then(|source| bern_svg::root(&source).map(|_| ()));
if let Err(error) = result {
errors.push(
Diagnostic::error(format!("SVG `{name}`: {error}")).at(e.source.clone()),
);
}
}
});
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
pub fn template(source: &str, kotlin: bool) -> String {
let quote = |s: &str| {
let quoted = serde_json::to_string(s).unwrap();
if kotlin {
quoted.replace('$', "\\$")
} else {
quoted
}
};
source
.replace("/*bern:svg-prefix*/", "e(bern_svg::PREFIX))
.replace("/*bern:svg-suffix*/", "e(&bern_svg::suffix()))
}
pub fn cpp_template(source: &str) -> String {
source
.replace(
"/*bern:svg-prefix*/",
&format!("R\"BERNSVG({})BERNSVG\"", bern_svg::PREFIX),
)
.replace(
"/*bern:svg-suffix*/",
&format!("R\"BERNSVG({})BERNSVG\"", bern_svg::suffix()),
)
}
const STATIC_PREFIX: &str = "__bern_static_svg/";
const DARK_SUFFIX: &str = ".dark.png";
pub fn prepare(app: &Application, config: &crate::TargetConfig) -> Application {
let mut app = app.with_webview(config.has_webview());
if config.platform == "tvos" {
app = app.on_television();
}
let pictures: std::collections::BTreeMap<String, bern_ir::SvgPicture> = app
.resources
.iter()
.filter_map(|resource| Some((resource.name.clone(), resource.picture?)))
.collect();
let mut resources = std::collections::BTreeMap::new();
for node in app
.components
.iter_mut()
.map(|c| &mut c.body)
.chain(app.windows.windows.iter_mut().map(|w| &mut w.content))
{
node.elements_mut(&mut |e| {
if e.kind != "Svg" {
return;
}
let picture = e
.resource("source")
.and_then(|source| pictures.get(source))
.copied();
if picture.is_none() && config.has_webview() {
return;
}
let Some(source) = (match picture {
Some(_) => e.resource("source"),
None => e.resource("static_source").or_else(|| e.resource("source")),
}) else {
return;
};
let name = format!("{STATIC_PREFIX}{source}.png");
resources.insert(name.clone(), e.source.clone());
let dark = (picture == Some(bern_ir::SvgPicture::Themed))
.then(|| format!("{STATIC_PREFIX}{source}{DARK_SUFFIX}"));
if let Some(dark) = &dark {
resources.insert(dark.clone(), e.source.clone());
}
e.kind = "Image".into();
for prop in [
"static_source",
"playing",
"speed",
"restart",
"interactive",
"data",
"external_links",
] {
e.props.remove(prop);
}
e.props
.insert("source".into(), bern_ir::Prop::Resource { name });
if picture == Some(bern_ir::SvgPicture::Tinted) {
e.props.insert(
"rendering".into(),
bern_ir::Prop::Keyword {
value: "template".into(),
},
);
}
if let Some(name) = dark {
e.props
.insert("dark_source".into(), bern_ir::Prop::Resource { name });
}
});
}
for (name, declared) in resources {
if !app.resources.iter().any(|r| r.name == name) {
app.resources.push(bern_ir::ResourceDef {
name,
declared,
picture: None,
});
}
}
app
}
pub fn one_size(element: &bern_ir::Element) -> bool {
element.resource("source").is_some()
&& element.keyword("hero").is_none()
&& (element.prop("width").is_some() || element.prop("height").is_some())
&& ["width", "height"]
.iter()
.all(|side| element.prop(side).is_none() || element.number(side).is_some())
}
pub fn resource_path(context: &BuildContext, name: &str) -> std::path::PathBuf {
if name.starts_with(STATIC_PREFIX) {
context.output_dir.join(name)
} else {
context.project_dir.join(name)
}
}
pub fn static_files(
app: &Application,
context: &BuildContext,
) -> Result<Vec<crate::GeneratedFile>, Vec<Diagnostic>> {
let mut declared = std::collections::BTreeMap::<String, f64>::new();
for node in app
.components
.iter()
.map(|c| &c.body)
.chain(app.windows.windows.iter().map(|w| &w.content))
{
node.walk(&mut |node| {
let Node::Element(e) = node else { return };
if e.kind != "Image" {
return;
}
for prop in ["source", "dark_source"] {
if let Some(name) = e.resource(prop)
&& name.starts_with(STATIC_PREFIX)
{
let side = e
.number("width")
.unwrap_or(0.0)
.max(e.number("height").unwrap_or(0.0));
let largest = declared.entry(name.to_owned()).or_insert(0.0);
*largest = largest.max(side);
}
}
});
}
let mut files = Vec::new();
for resource in &app.resources {
let Some(picture) = resource.name.strip_prefix(STATIC_PREFIX) else {
continue;
};
let (source, dark) = match picture.strip_suffix(DARK_SUFFIX) {
Some(source) => (source, true),
None => match picture.strip_suffix(".png") {
Some(source) => (source, false),
None => continue,
},
};
let render = || -> Result<Vec<u8>, String> {
let path = context.project_dir.join(source);
let data = std::fs::read(&path).map_err(|e| e.to_string())?;
let data = match std::str::from_utf8(&data) {
Ok(text) => bern_svg::in_appearance(text, dark).into_bytes(),
Err(_) => data,
};
let mut options = resvg::usvg::Options {
resources_dir: path.parent().map(|p| p.to_owned()),
..Default::default()
};
options.fontdb_mut().load_system_fonts();
let tree = resvg::usvg::Tree::from_data(&data, &options).map_err(|e| e.to_string())?;
let size = tree.size();
let longer = size.width().max(size.height());
let shown = declared.get(&resource.name).copied().unwrap_or(0.0) as f32;
let pixels = if shown > 0.0 {
shown * 6.0
} else {
longer * 2.0
};
let scale = (pixels / longer).min(4096.0 / longer);
let mut pixmap = resvg::tiny_skia::Pixmap::new(
(size.width() * scale).ceil().max(1.0) as u32,
(size.height() * scale).ceil().max(1.0) as u32,
)
.ok_or("cannot allocate SVG raster")?;
resvg::render(
&tree,
resvg::tiny_skia::Transform::from_scale(scale, scale),
&mut pixmap.as_mut(),
);
pixmap.encode_png().map_err(|e| e.to_string())
};
files.push(crate::GeneratedFile {
path: resource.name.clone().into(),
contents: render().map_err(|e| {
vec![
Diagnostic::error(format!("static SVG `{source}`: {e}"))
.at(resource.declared.clone()),
]
})?,
});
}
Ok(files)
}