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