1use crate::{BuildContext, Diagnostic};
3use bern_ir::{Application, Node};
4
5pub fn used(app: &Application) -> bool {
8 let mut used = false;
9 for node in app
10 .components
11 .iter()
12 .map(|c| &c.body)
13 .chain(app.windows.windows.iter().map(|w| &w.content))
14 {
15 node.walk(&mut |node| {
16 if matches!(node, Node::Element(e) if e.kind == "Svg" || e.kind == "WebPage") {
17 used = true;
18 }
19 });
20 }
21 used
22}
23
24pub fn validate(app: &Application, context: &BuildContext) -> Result<(), Vec<Diagnostic>> {
25 let mut errors = Vec::new();
26 let mut seen = std::collections::BTreeSet::new();
27 for node in app
28 .components
29 .iter()
30 .map(|c| &c.body)
31 .chain(app.windows.windows.iter().map(|w| &w.content))
32 {
33 node.walk(&mut |node| {
34 if let Node::Element(e) = node
35 && e.kind == "Svg"
36 && let Some(name) = e.resource("source")
37 && seen.insert(name.to_owned())
38 {
39 let result = std::fs::read_to_string(context.project_dir.join(name))
40 .map_err(|e| e.to_string())
41 .and_then(|source| bern_svg::root(&source).map(|_| ()));
42 if let Err(error) = result {
43 errors.push(
44 Diagnostic::error(format!("SVG `{name}`: {error}")).at(e.source.clone()),
45 );
46 }
47 }
48 });
49 }
50 if errors.is_empty() {
51 Ok(())
52 } else {
53 Err(errors)
54 }
55}
56
57pub fn template(source: &str, kotlin: bool) -> String {
60 let quote = |s: &str| {
61 let quoted = serde_json::to_string(s).unwrap();
62 if kotlin {
63 quoted.replace('$', "\\$")
64 } else {
65 quoted
66 }
67 };
68 source
69 .replace("/*bern:svg-prefix*/", "e(bern_svg::PREFIX))
70 .replace("/*bern:svg-suffix*/", "e(&bern_svg::suffix()))
71}
72
73pub fn cpp_template(source: &str) -> String {
74 source
75 .replace(
76 "/*bern:svg-prefix*/",
77 &format!("R\"BERNSVG({})BERNSVG\"", bern_svg::PREFIX),
78 )
79 .replace(
80 "/*bern:svg-suffix*/",
81 &format!("R\"BERNSVG({})BERNSVG\"", bern_svg::suffix()),
82 )
83}
84
85const STATIC_PREFIX: &str = "__bern_static_svg/";
86const DARK_SUFFIX: &str = ".dark.png";
89
90pub fn prepare(app: &Application, config: &crate::TargetConfig) -> Application {
98 let mut app = app.with_webview(config.has_webview());
99 if config.platform == "tvos" {
100 app = app.on_television();
101 }
102 let pictures: std::collections::BTreeMap<String, bern_ir::SvgPicture> = app
103 .resources
104 .iter()
105 .filter_map(|resource| Some((resource.name.clone(), resource.picture?)))
106 .collect();
107 let mut resources = std::collections::BTreeMap::new();
108 for node in app
109 .components
110 .iter_mut()
111 .map(|c| &mut c.body)
112 .chain(app.windows.windows.iter_mut().map(|w| &mut w.content))
113 {
114 node.elements_mut(&mut |e| {
115 if e.kind != "Svg" {
116 return;
117 }
118 let picture = e
119 .resource("source")
120 .and_then(|source| pictures.get(source))
121 .copied();
122 if picture.is_none() && config.has_webview() {
123 return;
124 }
125 let Some(source) = (match picture {
126 Some(_) => e.resource("source"),
127 None => e.resource("static_source").or_else(|| e.resource("source")),
128 }) else {
129 return;
130 };
131 let name = format!("{STATIC_PREFIX}{source}.png");
132 resources.insert(name.clone(), e.source.clone());
133 let dark = (picture == Some(bern_ir::SvgPicture::Themed))
134 .then(|| format!("{STATIC_PREFIX}{source}{DARK_SUFFIX}"));
135 if let Some(dark) = &dark {
136 resources.insert(dark.clone(), e.source.clone());
137 }
138 e.kind = "Image".into();
139 for prop in [
140 "static_source",
141 "playing",
142 "speed",
143 "restart",
144 "interactive",
145 "data",
146 "external_links",
147 ] {
148 e.props.remove(prop);
149 }
150 e.props
151 .insert("source".into(), bern_ir::Prop::Resource { name });
152 if picture == Some(bern_ir::SvgPicture::Tinted) {
153 e.props.insert(
154 "rendering".into(),
155 bern_ir::Prop::Keyword {
156 value: "template".into(),
157 },
158 );
159 }
160 if let Some(name) = dark {
161 e.props
162 .insert("dark_source".into(), bern_ir::Prop::Resource { name });
163 }
164 });
165 }
166 for (name, declared) in resources {
167 if !app.resources.iter().any(|r| r.name == name) {
168 app.resources.push(bern_ir::ResourceDef {
169 name,
170 declared,
171 picture: None,
172 });
173 }
174 }
175 app
176}
177
178pub fn one_size(element: &bern_ir::Element) -> bool {
184 element.resource("source").is_some()
185 && element.keyword("hero").is_none()
186 && (element.prop("width").is_some() || element.prop("height").is_some())
187 && ["width", "height"]
188 .iter()
189 .all(|side| element.prop(side).is_none() || element.number(side).is_some())
190}
191
192pub fn resource_path(context: &BuildContext, name: &str) -> std::path::PathBuf {
193 if name.starts_with(STATIC_PREFIX) {
194 context.output_dir.join(name)
195 } else {
196 context.project_dir.join(name)
197 }
198}
199
200pub fn static_files(
203 app: &Application,
204 context: &BuildContext,
205) -> Result<Vec<crate::GeneratedFile>, Vec<Diagnostic>> {
206 let mut declared = std::collections::BTreeMap::<String, f64>::new();
208 for node in app
209 .components
210 .iter()
211 .map(|c| &c.body)
212 .chain(app.windows.windows.iter().map(|w| &w.content))
213 {
214 node.walk(&mut |node| {
215 let Node::Element(e) = node else { return };
216 if e.kind != "Image" {
217 return;
218 }
219 for prop in ["source", "dark_source"] {
220 if let Some(name) = e.resource(prop)
221 && name.starts_with(STATIC_PREFIX)
222 {
223 let side = e
224 .number("width")
225 .unwrap_or(0.0)
226 .max(e.number("height").unwrap_or(0.0));
227 let largest = declared.entry(name.to_owned()).or_insert(0.0);
228 *largest = largest.max(side);
229 }
230 }
231 });
232 }
233 let mut files = Vec::new();
234 for resource in &app.resources {
235 let Some(picture) = resource.name.strip_prefix(STATIC_PREFIX) else {
236 continue;
237 };
238 let (source, dark) = match picture.strip_suffix(DARK_SUFFIX) {
239 Some(source) => (source, true),
240 None => match picture.strip_suffix(".png") {
241 Some(source) => (source, false),
242 None => continue,
243 },
244 };
245 let render = || -> Result<Vec<u8>, String> {
246 let path = context.project_dir.join(source);
247 let data = std::fs::read(&path).map_err(|e| e.to_string())?;
248 let data = match std::str::from_utf8(&data) {
251 Ok(text) => bern_svg::in_appearance(text, dark).into_bytes(),
252 Err(_) => data,
253 };
254 let mut options = resvg::usvg::Options {
255 resources_dir: path.parent().map(|p| p.to_owned()),
256 ..Default::default()
257 };
258 options.fontdb_mut().load_system_fonts();
259 let tree = resvg::usvg::Tree::from_data(&data, &options).map_err(|e| e.to_string())?;
260 let size = tree.size();
265 let longer = size.width().max(size.height());
266 let shown = declared.get(&resource.name).copied().unwrap_or(0.0) as f32;
267 let pixels = if shown > 0.0 {
268 shown * 6.0
269 } else {
270 longer * 2.0
271 };
272 let scale = (pixels / longer).min(4096.0 / longer);
273 let mut pixmap = resvg::tiny_skia::Pixmap::new(
274 (size.width() * scale).ceil().max(1.0) as u32,
275 (size.height() * scale).ceil().max(1.0) as u32,
276 )
277 .ok_or("cannot allocate SVG raster")?;
278 resvg::render(
279 &tree,
280 resvg::tiny_skia::Transform::from_scale(scale, scale),
281 &mut pixmap.as_mut(),
282 );
283 pixmap.encode_png().map_err(|e| e.to_string())
284 };
285 files.push(crate::GeneratedFile {
286 path: resource.name.clone().into(),
287 contents: render().map_err(|e| {
288 vec![
289 Diagnostic::error(format!("static SVG `{source}`: {e}"))
290 .at(resource.declared.clone()),
291 ]
292 })?,
293 });
294 }
295 Ok(files)
296}