Skip to main content

i_slint_compiler/passes/
embed_glyphs.rs

1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3
4// cSpell: ignore fsdm msdf msdfgen
5use crate::CompilerConfiguration;
6use crate::diagnostics::BuildDiagnostics;
7#[cfg(not(target_arch = "wasm32"))]
8use crate::embedded_resources::{BitmapFont, BitmapGlyph, BitmapGlyphs, CharacterMapEntry};
9#[cfg(not(target_arch = "wasm32"))]
10use crate::expression_tree::BuiltinFunction;
11use crate::expression_tree::{Expression, Unit};
12use crate::object_tree::*;
13use std::collections::HashMap;
14use std::collections::HashSet;
15use std::rc::Rc;
16
17use i_slint_common::sharedfontique::{self, fontique, skrifa};
18#[cfg(not(target_arch = "wasm32"))]
19use skrifa::MetadataProvider;
20
21#[derive(Clone)]
22struct Font {
23    font: fontique::QueryFont,
24}
25
26/// The fontique collection shared by `embed_glyphs` and `embed_images`, together
27/// with the imported fonts' file paths (the collection only knows them as in-memory
28/// blobs, so the paths are tracked separately for embedding).
29#[cfg(feature = "software-renderer")]
30pub struct FontCollection {
31    pub collection: sharedfontique::Collection,
32    pub custom_font_paths: HashMap<fontique::FamilyId, std::path::PathBuf>,
33    pub custom_fonts: HashMap<std::path::PathBuf, fontique::QueryFont>,
34}
35
36/// Built once and shared (by reference) between the font and image passes. The
37/// `LazyLock` defers the system-font scan to the first lookup, so a build with no
38/// glyphs or text SVGs to embed never scans.
39#[cfg(feature = "software-renderer")]
40pub type SharedFontCollection = std::sync::Arc<
41    std::sync::LazyLock<
42        std::sync::Mutex<FontCollection>,
43        Box<dyn FnOnce() -> std::sync::Mutex<FontCollection> + Send + Sync>,
44    >,
45>;
46
47/// Reads every imported (`import "...ttf"`) font file, reporting load errors with
48/// their import span. The bytes feed [`shared_font_collection`].
49#[cfg(feature = "software-renderer")]
50pub fn read_custom_fonts<'a>(
51    all_docs: impl Iterator<Item = &'a Document>,
52    diag: &mut BuildDiagnostics,
53) -> Vec<(std::path::PathBuf, Vec<u8>)> {
54    let mut fonts = Vec::new();
55    for doc in all_docs {
56        for (font_path, import_token) in doc.custom_fonts.iter() {
57            match std::fs::read(font_path.as_str()) {
58                Err(e) => diag.push_error(format!("Error loading font: {e}"), import_token),
59                Ok(bytes) => fonts.push((font_path.as_str().into(), bytes)),
60            }
61        }
62    }
63    fonts
64}
65
66/// Wraps the system fonts plus the imported `custom_fonts` into a [`SharedFontCollection`].
67#[cfg(feature = "software-renderer")]
68pub fn shared_font_collection(
69    custom_fonts: Vec<(std::path::PathBuf, Vec<u8>)>,
70) -> SharedFontCollection {
71    let init: Box<dyn FnOnce() -> std::sync::Mutex<FontCollection> + Send + Sync> =
72        Box::new(move || {
73            let mut collection = sharedfontique::create_collection(true);
74            let mut custom_font_paths = HashMap::new();
75            let mut custom_font_map = HashMap::new();
76            for (path, bytes) in custom_fonts {
77                if let Some(font) = collection
78                    .register_fonts(bytes.into(), None)
79                    .first()
80                    .and_then(|(id, infos)| collection.get_font_for_info(*id, infos.first()?))
81                {
82                    custom_font_paths.insert(font.family.0, path.clone());
83                    custom_font_map.insert(path, font);
84                }
85            }
86            std::sync::Mutex::new(FontCollection {
87                collection,
88                custom_font_paths,
89                custom_fonts: custom_font_map,
90            })
91        });
92    std::sync::Arc::new(std::sync::LazyLock::new(init))
93}
94
95fn swash_font_ref(font: &Font) -> swash::FontRef<'_> {
96    swash::FontRef::from_index(font.font.blob.data(), font.font.index as usize).unwrap()
97}
98
99#[cfg(target_arch = "wasm32")]
100pub fn embed_glyphs<'a>(
101    _component: &Document,
102    _compiler_config: &CompilerConfiguration,
103    _scale_factor: f64,
104    _pixel_sizes: Vec<i16>,
105    _font_weights: Vec<u16>,
106    _characters_seen: HashSet<char>,
107    _all_docs: impl Iterator<Item = &'a crate::object_tree::Document> + 'a,
108    _diag: &mut BuildDiagnostics,
109) -> bool {
110    false
111}
112
113#[cfg(not(target_arch = "wasm32"))]
114pub fn embed_glyphs(
115    doc: &Document,
116    compiler_config: &CompilerConfiguration,
117    mut pixel_sizes: Vec<i16>,
118    font_weights: Vec<u16>,
119    mut characters_seen: HashSet<char>,
120    font_collection: &SharedFontCollection,
121    diag: &mut BuildDiagnostics,
122) {
123    use crate::diagnostics::Spanned;
124
125    let generic_diag_location = doc.node.as_ref().map(|n| n.to_source_location());
126    let scale_factor = compiler_config.const_scale_factor.unwrap_or(1.);
127
128    characters_seen.extend(
129        ('a'..='z')
130            .chain('A'..='Z')
131            .chain('0'..='9')
132            .chain(" '!\"#$%&()*+,-./:;<=>?@\\[]{}^_|~".chars())
133            .chain(std::iter::once('●'))
134            .chain(std::iter::once('…')),
135    );
136
137    if let Ok(sizes_str) = std::env::var("SLINT_FONT_SIZES") {
138        for custom_size_str in sizes_str.split(',') {
139            let custom_size = if let Ok(custom_size) = custom_size_str
140                .parse::<f32>()
141                .map(|size_as_float| (size_as_float * scale_factor) as i16)
142            {
143                custom_size
144            } else {
145                diag.push_error(
146                    format!(
147                        "Invalid font size '{custom_size_str}' specified in `SLINT_FONT_SIZES`"
148                    ),
149                    &generic_diag_location,
150                );
151                return;
152            };
153
154            if let Err(pos) = pixel_sizes.binary_search(&custom_size) {
155                pixel_sizes.insert(pos, custom_size)
156            }
157        }
158    }
159
160    let fallback_fonts = get_fallback_fonts();
161
162    // The collection (system fonts + imported fonts) is built once and shared with
163    // `embed_images`; the imported-font paths come with it.
164    let mut shared = font_collection.lock().unwrap();
165    let FontCollection { collection, custom_font_paths: font_paths, custom_fonts } = &mut *shared;
166
167    let mut custom_face_error = false;
168
169    let default_fonts: Vec<(std::path::PathBuf, fontique::QueryFont)> = if !collection
170        .default_fonts
171        .is_empty()
172    {
173        collection.default_fonts.as_ref().clone()
174    } else {
175        let mut default_fonts: Vec<(std::path::PathBuf, fontique::QueryFont)> = Vec::new();
176
177        for c in doc.exported_roots() {
178            let (family, source_location) = c
179                .root_element
180                .borrow()
181                .bindings
182                .get("default-font-family")
183                .and_then(|binding| match &binding.borrow().expression {
184                    Expression::StringLiteral(family) => {
185                        Some((Some(family.clone()), binding.borrow().span.clone()))
186                    }
187                    _ => None,
188                })
189                .unwrap_or_default();
190
191            let font = {
192                let mut query = collection.query();
193
194                query.set_families(
195                    family
196                        .as_ref()
197                        .map(|family| fontique::QueryFamily::from(family.as_str()))
198                        .into_iter()
199                        .chain(
200                            sharedfontique::FALLBACK_FAMILIES
201                                .into_iter()
202                                .map(fontique::QueryFamily::Generic),
203                        ),
204                );
205
206                let mut font = None;
207
208                query.matches_with(|queried_font| {
209                    font = Some(queried_font.clone());
210                    fontique::QueryStatus::Stop
211                });
212                font
213            };
214
215            match font {
216                None => {
217                    if let Some(source_location) = source_location {
218                        diag.push_error_with_span("could not find font that provides specified family, falling back to Sans-Serif".to_string(), source_location);
219                    } else {
220                        diag.push_error(
221                            "internal error: could not determine a default font for sans-serif"
222                                .to_string(),
223                            &generic_diag_location,
224                        );
225                    };
226                }
227                Some(query_font) => {
228                    if let Some(font_info) = collection
229                        .family(query_font.family.0)
230                        .and_then(|family_info| family_info.fonts().first().cloned())
231                    {
232                        let path = if let Some(path) = font_paths.get(&query_font.family.0) {
233                            path.clone()
234                        } else {
235                            match &font_info.source().kind {
236                                fontique::SourceKind::Path(path) => path.to_path_buf(),
237                                fontique::SourceKind::Memory(_) => {
238                                    diag.push_error(
239                                    "internal error: memory fonts are not supported in the compiler"
240                                        .to_string(),
241                                    &generic_diag_location,
242                                );
243                                    custom_face_error = true;
244                                    continue;
245                                }
246                            }
247                        };
248                        font_paths.insert(query_font.family.0, path.clone());
249                        default_fonts.push((path.clone(), query_font));
250                    }
251                }
252            }
253        }
254
255        default_fonts
256    };
257
258    if custom_face_error {
259        return;
260    }
261
262    let register_embedded_font = |path: &std::path::Path, embedded_bitmap_font: BitmapFont| {
263        let resource_id = doc.embedded_file_resources.borrow_mut().push_and_get_key(
264            crate::embedded_resources::EmbeddedResources {
265                path: Some(path.to_string_lossy().as_ref().into()),
266                kind: crate::embedded_resources::EmbeddedResourcesKind::BitmapFontData(
267                    embedded_bitmap_font,
268                ),
269            },
270        );
271
272        for c in doc.exported_roots() {
273            c.init_code.borrow_mut().font_registration_code.push(Expression::FunctionCall {
274                function: BuiltinFunction::RegisterBitmapFont.into(),
275                arguments: vec![Expression::NumberLiteral(resource_id.0 as _, Unit::None)],
276                source_location: None,
277            });
278        }
279    };
280
281    let mut embed_font_by_path = |path: &std::path::Path, font: &fontique::QueryFont| {
282        let Some(family_name) = collection.family_name(font.family.0).to_owned() else {
283            diag.push_error(
284                format!(
285                    "internal error: TrueType font without family name encountered: {}",
286                    path.display()
287                ),
288                &generic_diag_location,
289            );
290            return;
291        };
292
293        let Some(font_ref) = skrifa::FontRef::from_index(font.blob.data(), font.index).ok() else {
294            diag.push_error(
295                format!("internal error: failed to parse font: {}", path.display()),
296                &generic_diag_location,
297            );
298            return;
299        };
300        let axes = font_ref.axes();
301        let wght_axis = axes.iter().find(|axis| axis.tag() == skrifa::Tag::new(b"wght"));
302
303        if let Some(wght_axis) = wght_axis {
304            // Variable font: embed one BitmapFont per requested weight
305            let weights = if font_weights.is_empty() {
306                vec![fontique::FontWeight::NORMAL.value() as u16]
307            } else {
308                font_weights.clone()
309            };
310            for &weight in &weights {
311                let clamped = (weight as f32).clamp(wght_axis.min_value(), wght_axis.max_value());
312                let location = axes.location([("wght", clamped)]);
313                let variations = vec![(skrifa::Tag::new(b"wght"), clamped)];
314
315                let embedded = embed_font(
316                    family_name.to_owned(),
317                    Font { font: font.clone() },
318                    &pixel_sizes,
319                    characters_seen.iter().cloned(),
320                    &fallback_fonts,
321                    compiler_config,
322                    location.coords(),
323                    &variations,
324                    Some(weight),
325                );
326                register_embedded_font(path, embedded);
327            }
328        } else {
329            // Static font: embed once
330            let embedded = embed_font(
331                family_name.to_owned(),
332                Font { font: font.clone() },
333                &pixel_sizes,
334                characters_seen.iter().cloned(),
335                &fallback_fonts,
336                compiler_config,
337                &[],
338                &[],
339                None,
340            );
341            register_embedded_font(path, embedded);
342        }
343    };
344
345    // default_fonts is in primary-first order (set up by sharedfontique from
346    // SLINT_DEFAULT_FONT then SLINT_FONT_PATH); preserve it.
347    for (path, font) in default_fonts.iter() {
348        custom_fonts.remove(path);
349        embed_font_by_path(path, font);
350    }
351
352    for (path, font) in custom_fonts.iter() {
353        embed_font_by_path(path, font);
354    }
355}
356
357#[inline(never)] // workaround https://github.com/rust-lang/rust/issues/104099
358fn get_fallback_fonts() -> Vec<Font> {
359    let mut fallback_fonts = Vec::new();
360
361    let mut collection = sharedfontique::create_collection(false);
362    let mut query = collection.query();
363    query.set_families(
364        sharedfontique::FALLBACK_FAMILIES.into_iter().map(fontique::QueryFamily::Generic).chain(
365            core::iter::once(fontique::QueryFamily::Generic(fontique::GenericFamily::Emoji)),
366        ),
367    );
368
369    query.matches_with(|query_font| {
370        fallback_fonts.push(Font { font: query_font.clone() });
371        fontique::QueryStatus::Continue
372    });
373
374    fallback_fonts
375}
376
377#[cfg(not(target_arch = "wasm32"))]
378fn embed_font(
379    family_name: String,
380    font: Font,
381    pixel_sizes: &[i16],
382    character_coverage: impl Iterator<Item = char>,
383    fallback_fonts: &[Font],
384    _compiler_config: &CompilerConfiguration,
385    normalized_coords: &[skrifa::instance::NormalizedCoord],
386    _variations: &[(skrifa::Tag, f32)],
387    override_weight: Option<u16>,
388) -> BitmapFont {
389    let coords_i16: Vec<i16> = normalized_coords.iter().map(|c| c.to_bits()).collect();
390
391    let mut character_map: Vec<CharacterMapEntry> = character_coverage
392        .filter(|code_point| {
393            core::iter::once(&font)
394                .chain(fallback_fonts.iter())
395                .any(|font| swash_font_ref(font).charmap().map(*code_point) != 0)
396        })
397        .enumerate()
398        .map(|(glyph_index, code_point)| CharacterMapEntry {
399            code_point,
400            glyph_index: u16::try_from(glyph_index)
401                .expect("more than 65535 glyphs are not supported"),
402        })
403        .collect();
404
405    #[cfg(feature = "sdf-fonts")]
406    let glyphs = if _compiler_config.use_sdf_fonts {
407        embed_sdf_glyphs(pixel_sizes, &character_map, &font, fallback_fonts, _variations)
408    } else {
409        embed_alpha_map_glyphs(pixel_sizes, &character_map, &font, fallback_fonts, &coords_i16)
410    };
411    #[cfg(not(feature = "sdf-fonts"))]
412    let glyphs =
413        embed_alpha_map_glyphs(pixel_sizes, &character_map, &font, fallback_fonts, &coords_i16);
414
415    character_map.sort_by_key(|entry| entry.code_point);
416
417    let font_ref = skrifa::FontRef::from_index(font.font.blob.data(), font.font.index).unwrap();
418    let location = skrifa::instance::LocationRef::new(normalized_coords);
419    let metrics =
420        skrifa::metrics::Metrics::new(&font_ref, skrifa::instance::Size::unscaled(), location);
421    let attrs = skrifa::attribute::Attributes::new(&font_ref);
422
423    BitmapFont {
424        family_name,
425        character_map,
426        units_per_em: metrics.units_per_em as f32,
427        ascent: metrics.ascent,
428        descent: metrics.descent,
429        x_height: metrics.x_height.unwrap_or_default(),
430        cap_height: metrics.cap_height.unwrap_or_default(),
431        glyphs,
432        weight: override_weight.unwrap_or(attrs.weight.value() as u16),
433        italic: attrs.style != skrifa::attribute::Style::Normal,
434        #[cfg(feature = "sdf-fonts")]
435        sdf: _compiler_config.use_sdf_fonts,
436        #[cfg(not(feature = "sdf-fonts"))]
437        sdf: false,
438    }
439}
440
441#[cfg(not(target_arch = "wasm32"))]
442fn embed_alpha_map_glyphs(
443    pixel_sizes: &[i16],
444    character_map: &Vec<CharacterMapEntry>,
445    font: &Font,
446    fallback_fonts: &[Font],
447    normalized_coords: &[i16],
448) -> Vec<BitmapGlyphs> {
449    use rayon::prelude::*;
450    use std::cell::RefCell;
451
452    thread_local! {
453        static SCALE_CONTEXT: RefCell<swash::scale::ScaleContext> =
454            RefCell::new(swash::scale::ScaleContext::new());
455    }
456
457    pixel_sizes
458        .par_iter()
459        .map(|pixel_size| {
460            let glyph_data = character_map
461                .par_iter()
462                .map(|CharacterMapEntry { code_point, .. }| {
463                    let font_to_use = core::iter::once(font)
464                        .chain(fallback_fonts.iter())
465                        .find(|f| swash_font_ref(f).charmap().map(*code_point) != 0)
466                        .unwrap_or(font);
467
468                    let font_ref = swash_font_ref(font_to_use);
469                    let glyph_id = font_ref.charmap().map(*code_point);
470                    let gm = font_ref.glyph_metrics(normalized_coords);
471                    let fm = font_ref.metrics(normalized_coords);
472                    let scale = *pixel_size as f32 / fm.units_per_em as f32;
473                    let advance_width = gm.advance_width(glyph_id) * scale;
474
475                    SCALE_CONTEXT.with(|ctx| {
476                        let font_ref = swash_font_ref(font_to_use);
477                        let mut ctx = ctx.borrow_mut();
478                        let mut scaler = ctx
479                            .builder(font_ref)
480                            .size(*pixel_size as f32)
481                            .normalized_coords(normalized_coords)
482                            .build();
483                        let image = swash::scale::Render::new(&[swash::scale::Source::Outline])
484                            .format(swash::zeno::Format::Alpha)
485                            .render(&mut scaler, glyph_id);
486
487                        match image {
488                            Some(image) => {
489                                let p = image.placement;
490                                BitmapGlyph {
491                                    x: i16::try_from(p.left * 64)
492                                        .expect("large glyph x coordinate"),
493                                    y: i16::try_from((p.top - p.height as i32) * 64)
494                                        .expect("large glyph y coordinate"),
495                                    width: i16::try_from(p.width).expect("large width"),
496                                    height: i16::try_from(p.height).expect("large height"),
497                                    x_advance: i16::try_from((advance_width * 64.) as i64)
498                                        .expect("large advance width"),
499                                    data: image.data,
500                                }
501                            }
502                            None => BitmapGlyph {
503                                x: 0,
504                                y: 0,
505                                width: 0,
506                                height: 0,
507                                x_advance: i16::try_from((advance_width * 64.) as i64)
508                                    .expect("large advance width"),
509                                data: vec![],
510                            },
511                        }
512                    })
513                })
514                .collect();
515
516            BitmapGlyphs { pixel_size: *pixel_size, glyph_data }
517        })
518        .collect()
519}
520
521#[cfg(all(not(target_arch = "wasm32"), feature = "sdf-fonts"))]
522fn embed_sdf_glyphs(
523    pixel_sizes: &[i16],
524    character_map: &Vec<CharacterMapEntry>,
525    font: &Font,
526    fallback_fonts: &[Font],
527    variations: &[(skrifa::Tag, f32)],
528) -> Vec<BitmapGlyphs> {
529    use rayon::prelude::*;
530
531    const RANGE: f64 = 6.;
532
533    let Some(max_size) = pixel_sizes.iter().max() else {
534        return Vec::new();
535    };
536    let min_size = pixel_sizes.iter().min().expect("we have a 'max' so the vector is not empty");
537    let target_pixel_size = (max_size * 2 / 3).max(16).min(RANGE as i16 * min_size);
538
539    let glyph_data = character_map
540        .par_iter()
541        .map(|CharacterMapEntry { code_point, .. }| {
542            core::iter::once(font)
543                .chain(fallback_fonts.iter())
544                .find_map(|font| {
545                    (swash_font_ref(font).charmap().map(*code_point) != 0).then(|| {
546                        generate_sdf_for_glyph(
547                            font,
548                            *code_point,
549                            target_pixel_size,
550                            RANGE,
551                            variations,
552                        )
553                    })
554                })
555                .unwrap_or_else(|| {
556                    generate_sdf_for_glyph(font, *code_point, target_pixel_size, RANGE, variations)
557                })
558                .unwrap_or_default()
559        })
560        .collect::<Vec<_>>();
561
562    vec![BitmapGlyphs { pixel_size: target_pixel_size, glyph_data }]
563}
564
565#[cfg(all(not(target_arch = "wasm32"), feature = "sdf-fonts"))]
566fn generate_sdf_for_glyph(
567    font: &Font,
568    code_point: char,
569    target_pixel_size: i16,
570    range: f64,
571    variations: &[(skrifa::Tag, f32)],
572) -> Option<BitmapGlyph> {
573    use fdsm::transform::Transform;
574    use nalgebra::{Affine2, Similarity2, Vector2};
575
576    let mut face =
577        fdsm_ttf_parser::ttf_parser::Face::parse(font.font.blob.data(), font.font.index).unwrap();
578    for &(tag, value) in variations {
579        face.set_variation(
580            fdsm_ttf_parser::ttf_parser::Tag(u32::from_be_bytes(tag.to_be_bytes())),
581            value,
582        );
583    }
584    let glyph_id = face.glyph_index(code_point).unwrap_or_default();
585
586    let font_ref = skrifa::FontRef::from_index(font.font.blob.data(), font.font.index).unwrap();
587    let variation_settings: Vec<_> =
588        variations.iter().map(|&(tag, value)| (tag, value)).collect::<Vec<_>>();
589    let location = font_ref.axes().location(variation_settings);
590    let metrics = skrifa::metrics::Metrics::new(
591        &font_ref,
592        skrifa::instance::Size::unscaled(),
593        skrifa::instance::LocationRef::from(&location),
594    );
595    let target_pixel_size = target_pixel_size as f64;
596    let scale = target_pixel_size / metrics.units_per_em as f64;
597
598    // TODO: handle bitmap glyphs (emojis)
599    let Some(bbox) = face.glyph_bounding_box(glyph_id) else {
600        // For example, for space
601        return Some(BitmapGlyph {
602            x_advance: (face.glyph_hor_advance(glyph_id).unwrap_or(0) as f64 * scale * 64.) as i16,
603            ..Default::default()
604        });
605    };
606
607    let mut shape = fdsm_ttf_parser::load_shape_from_face(&face, glyph_id)?;
608
609    let width = ((bbox.x_max as f64 - bbox.x_min as f64) * scale + 2.).ceil() as u32;
610    let height = ((bbox.y_max as f64 - bbox.y_min as f64) * scale + 2.).ceil() as u32;
611    let transformation = nalgebra::convert::<_, Affine2<f64>>(Similarity2::new(
612        Vector2::new(1. - bbox.x_min as f64 * scale, 1. - bbox.y_min as f64 * scale),
613        0.,
614        scale,
615    ));
616
617    // Unlike msdfgen, the transformation is not passed into the
618    // `generate_msdf` function – the coordinates of the control points
619    // must be expressed in terms of pixels on the distance field. To get
620    // the correct units, we pre-transform the shape:
621
622    shape.transform(&transformation);
623
624    let prepared_shape = shape.prepare();
625
626    // Set up the resulting image and generate the distance field:
627
628    let mut sdf = image::GrayImage::new(width, height);
629    fdsm::generate::generate_sdf(&prepared_shape, range, &mut sdf);
630    fdsm::render::correct_sign_sdf(
631        &mut sdf,
632        &prepared_shape,
633        fdsm::bezier::scanline::FillRule::Nonzero,
634    );
635
636    let mut glyph_data = sdf.into_raw();
637
638    // normalize around 0
639    for x in &mut glyph_data {
640        *x = x.wrapping_sub(128);
641    }
642
643    // invert the y coordinate (as the fsdm crate has the y axis inverted)
644    let (w, h) = (width as usize, height as usize);
645    for idx in 0..glyph_data.len() / 2 {
646        glyph_data.swap(idx, (h - idx / w - 1) * w + idx % w);
647    }
648
649    // Add a "0" so that we can always access pos+1 without going out of bound
650    // (so that the last row will look like `data[len-1]*1 + data[len]*0`)
651    glyph_data.push(0);
652
653    let bg = BitmapGlyph {
654        x: i16::try_from((-(1. - bbox.x_min as f64 * scale) * 64.).ceil() as i32)
655            .expect("large glyph x coordinate"),
656        y: i16::try_from((-(1. - bbox.y_min as f64 * scale) * 64.).ceil() as i32)
657            .expect("large glyph y coordinate"),
658        width: i16::try_from(width).expect("large width"),
659        height: i16::try_from(height).expect("large height"),
660        x_advance: i16::try_from(
661            (face.glyph_hor_advance(glyph_id).unwrap() as f64 * scale * 64.).round() as i32,
662        )
663        .expect("large advance width"),
664        data: glyph_data,
665    };
666
667    Some(bg)
668}
669
670fn try_extract_literal_from_element(
671    elem: &ElementRc,
672    property_name: &str,
673    unit: Unit,
674) -> Option<f64> {
675    elem.borrow().bindings.get(property_name).and_then(|expression| {
676        match &expression.borrow().expression {
677            Expression::NumberLiteral(value, u) if *u == unit => Some(*value),
678            Expression::Cast { from, .. } => match from.as_ref() {
679                Expression::NumberLiteral(value, u) if *u == unit => Some(*value),
680                _ => None,
681            },
682            _ => None,
683        }
684    })
685}
686
687pub fn collect_font_sizes_used(
688    component: &Rc<Component>,
689    scale_factor: f64,
690    sizes_seen: &mut Vec<i16>,
691) {
692    let mut add_font_size = |logical_size: f64| {
693        let pixel_size = (logical_size * scale_factor) as i16;
694        match sizes_seen.binary_search(&pixel_size) {
695            Ok(_) => {}
696            Err(pos) => sizes_seen.insert(pos, pixel_size),
697        }
698    };
699
700    recurse_elem_including_sub_components(component, &(), &mut |elem, _| match elem
701        .borrow()
702        .base_type
703        .to_string()
704        .as_str()
705    {
706        "TextInput" | "Text" | "SimpleText" | "ComplexText" | "StyledTextItem" => {
707            if let Some(font_size) = try_extract_literal_from_element(elem, "font-size", Unit::Px) {
708                add_font_size(font_size)
709            }
710        }
711        "Dialog" | "Window" | "WindowItem" => {
712            if let Some(font_size) =
713                try_extract_literal_from_element(elem, "default-font-size", Unit::Px)
714            {
715                add_font_size(font_size)
716            }
717        }
718        _ => {}
719    });
720}
721
722pub fn collect_font_weights_used(component: &Rc<Component>, weights_seen: &mut Vec<u16>) {
723    let mut add_weight = |weight: f64| {
724        let weight = weight as u16;
725        if let Err(pos) = weights_seen.binary_search(&weight) {
726            weights_seen.insert(pos, weight);
727        }
728    };
729
730    recurse_elem_including_sub_components(component, &(), &mut |elem, _| match elem
731        .borrow()
732        .base_type
733        .to_string()
734        .as_str()
735    {
736        "TextInput" | "Text" | "SimpleText" | "ComplexText" | "StyledTextItem" => {
737            if let Some(weight) = try_extract_literal_from_element(elem, "font-weight", Unit::None)
738            {
739                add_weight(weight)
740            }
741        }
742        "Dialog" | "Window" | "WindowItem" => {
743            if let Some(weight) =
744                try_extract_literal_from_element(elem, "default-font-weight", Unit::None)
745            {
746                add_weight(weight)
747            }
748        }
749        _ => {}
750    });
751}
752
753pub fn scan_string_literals(component: &Rc<Component>, characters_seen: &mut HashSet<char>) {
754    visit_all_expressions(component, |expr, _| {
755        expr.visit_recursive(&mut |expr| {
756            if let Expression::StringLiteral(string) = expr {
757                characters_seen.extend(string.chars());
758            }
759        })
760    })
761}