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
4use crate::CompilerConfiguration;
5use crate::diagnostics::BuildDiagnostics;
6#[cfg(not(target_arch = "wasm32"))]
7use crate::embedded_resources::{BitmapFont, BitmapGlyph, BitmapGlyphs, CharacterMapEntry};
8#[cfg(not(target_arch = "wasm32"))]
9use crate::expression_tree::BuiltinFunction;
10use crate::expression_tree::{Expression, Unit};
11use crate::object_tree::*;
12use std::collections::HashMap;
13use std::collections::HashSet;
14use std::rc::Rc;
15use std::sync::Arc;
16
17use i_slint_common::sharedfontique::{self, fontique, ttf_parser};
18
19#[derive(Clone, derive_more::Deref)]
20struct Font {
21    font: fontique::QueryFont,
22    #[deref]
23    fontdue_font: Arc<fontdue::Font>,
24}
25
26#[cfg(target_arch = "wasm32")]
27pub fn embed_glyphs<'a>(
28    _component: &Document,
29    _compiler_config: &CompilerConfiguration,
30    _scale_factor: f64,
31    _pixel_sizes: Vec<i16>,
32    _characters_seen: HashSet<char>,
33    _all_docs: impl Iterator<Item = &'a crate::object_tree::Document> + 'a,
34    _diag: &mut BuildDiagnostics,
35) -> bool {
36    false
37}
38
39#[cfg(not(target_arch = "wasm32"))]
40pub fn embed_glyphs<'a>(
41    doc: &Document,
42    compiler_config: &CompilerConfiguration,
43    mut pixel_sizes: Vec<i16>,
44    mut characters_seen: HashSet<char>,
45    all_docs: impl Iterator<Item = &'a crate::object_tree::Document> + 'a,
46    diag: &mut BuildDiagnostics,
47) {
48    use crate::diagnostics::Spanned;
49
50    let generic_diag_location = doc.node.as_ref().map(|n| n.to_source_location());
51    let scale_factor = compiler_config.const_scale_factor.unwrap_or(1.);
52
53    characters_seen.extend(
54        ('a'..='z')
55            .chain('A'..='Z')
56            .chain('0'..='9')
57            .chain(" '!\"#$%&()*+,-./:;<=>?@\\[]{}^_|~".chars())
58            .chain(std::iter::once('●'))
59            .chain(std::iter::once('…')),
60    );
61
62    if let Ok(sizes_str) = std::env::var("SLINT_FONT_SIZES") {
63        for custom_size_str in sizes_str.split(',') {
64            let custom_size = if let Ok(custom_size) = custom_size_str
65                .parse::<f32>()
66                .map(|size_as_float| (size_as_float * scale_factor) as i16)
67            {
68                custom_size
69            } else {
70                diag.push_error(
71                    format!(
72                        "Invalid font size '{custom_size_str}' specified in `SLINT_FONT_SIZES`"
73                    ),
74                    &generic_diag_location,
75                );
76                return;
77            };
78
79            if let Err(pos) = pixel_sizes.binary_search(&custom_size) {
80                pixel_sizes.insert(pos, custom_size)
81            }
82        }
83    }
84
85    let fallback_fonts = get_fallback_fonts(compiler_config);
86
87    let mut custom_fonts: HashMap<std::path::PathBuf, fontique::QueryFont> = Default::default();
88    let mut font_paths: HashMap<fontique::FamilyId, std::path::PathBuf> = Default::default();
89
90    let mut collection = sharedfontique::get_collection();
91
92    for doc in all_docs {
93        for (font_path, import_token) in doc.custom_fonts.iter() {
94            match std::fs::read(&font_path) {
95                Err(e) => {
96                    diag.push_error(format!("Error loading font: {e}"), import_token);
97                }
98                Ok(bytes) => {
99                    if let Some(font) = collection
100                        .register_fonts(bytes.into(), None)
101                        .first()
102                        .and_then(|(id, infos)| {
103                            let info = infos.first()?;
104                            collection.get_font_for_info(*id, info)
105                        })
106                    {
107                        font_paths.insert(font.family.0, font_path.into());
108                        custom_fonts.insert(font_path.into(), font);
109                    }
110                }
111            }
112        }
113    }
114
115    let mut custom_face_error = false;
116
117    let default_fonts = if !collection.default_fonts.is_empty() {
118        collection.default_fonts.as_ref().clone()
119    } else {
120        let mut default_fonts: HashMap<std::path::PathBuf, fontique::QueryFont> =
121            Default::default();
122
123        for c in doc.exported_roots() {
124            let (family, source_location) = c
125                .root_element
126                .borrow()
127                .bindings
128                .get("default-font-family")
129                .and_then(|binding| match &binding.borrow().expression {
130                    Expression::StringLiteral(family) => {
131                        Some((Some(family.clone()), binding.borrow().span.clone()))
132                    }
133                    _ => None,
134                })
135                .unwrap_or_default();
136
137            let font = {
138                let mut query = collection.query();
139
140                query.set_families(
141                    family
142                        .as_ref()
143                        .map(|family| fontique::QueryFamily::from(family.as_str()))
144                        .into_iter()
145                        .chain(
146                            sharedfontique::FALLBACK_FAMILIES
147                                .into_iter()
148                                .map(fontique::QueryFamily::Generic),
149                        ),
150                );
151
152                let mut font = None;
153
154                query.matches_with(|queried_font| {
155                    font = Some(queried_font.clone());
156                    fontique::QueryStatus::Stop
157                });
158                font
159            };
160
161            match font {
162                None => {
163                    if let Some(source_location) = source_location {
164                        diag.push_error_with_span("could not find font that provides specified family, falling back to Sans-Serif".to_string(), source_location);
165                    } else {
166                        diag.push_error(
167                            "internal error: could not determine a default font for sans-serif"
168                                .to_string(),
169                            &generic_diag_location,
170                        );
171                    };
172                }
173                Some(query_font) => {
174                    if let Some(font_info) = collection
175                        .family(query_font.family.0)
176                        .and_then(|family_info| family_info.fonts().first().cloned())
177                    {
178                        let path = if let Some(path) = font_paths.get(&query_font.family.0) {
179                            path.clone()
180                        } else {
181                            match &font_info.source().kind {
182                                fontique::SourceKind::Path(path) => path.to_path_buf(),
183                                fontique::SourceKind::Memory(_) => {
184                                    diag.push_error(
185                                    "internal error: memory fonts are not supported in the compiler"
186                                        .to_string(),
187                                    &generic_diag_location,
188                                );
189                                    custom_face_error = true;
190                                    continue;
191                                }
192                            }
193                        };
194                        font_paths.insert(query_font.family.0, path.clone());
195                        default_fonts.insert(path.clone(), query_font);
196                    }
197                }
198            }
199        }
200
201        default_fonts
202    };
203
204    if custom_face_error {
205        return;
206    }
207
208    let mut embed_font_by_path = |path: &std::path::Path, font: &fontique::QueryFont| {
209        let fontdue_font = match compiler_config.load_font_by_id(font) {
210            Ok(font) => font,
211            Err(msg) => {
212                diag.push_error(
213                    format!("error loading font for embedding {}: {msg}", path.display()),
214                    &generic_diag_location,
215                );
216                return;
217            }
218        };
219
220        let Some(family_name) = collection.family_name(font.family.0).to_owned() else {
221            diag.push_error(
222                format!(
223                    "internal error: TrueType font without family name encountered: {}",
224                    path.display()
225                ),
226                &generic_diag_location,
227            );
228            return;
229        };
230
231        let embedded_bitmap_font = embed_font(
232            family_name.to_owned(),
233            Font { font: font.clone(), fontdue_font },
234            &pixel_sizes,
235            characters_seen.iter().cloned(),
236            &fallback_fonts,
237            compiler_config,
238        );
239
240        let resource_id = doc.embedded_file_resources.borrow().len();
241        doc.embedded_file_resources.borrow_mut().insert(
242            path.to_string_lossy().into(),
243            crate::embedded_resources::EmbeddedResources {
244                id: resource_id,
245                kind: crate::embedded_resources::EmbeddedResourcesKind::BitmapFontData(
246                    embedded_bitmap_font,
247                ),
248            },
249        );
250
251        for c in doc.exported_roots() {
252            c.init_code.borrow_mut().font_registration_code.push(Expression::FunctionCall {
253                function: BuiltinFunction::RegisterBitmapFont.into(),
254                arguments: vec![Expression::NumberLiteral(resource_id as _, Unit::None)],
255                source_location: None,
256            });
257        }
258    };
259
260    for (path, font) in default_fonts.iter() {
261        custom_fonts.remove(&path.to_owned());
262        embed_font_by_path(&path, &font);
263    }
264
265    for (path, font) in custom_fonts {
266        embed_font_by_path(&path, &font);
267    }
268}
269
270#[inline(never)] // workaround https://github.com/rust-lang/rust/issues/104099
271fn get_fallback_fonts(compiler_config: &CompilerConfiguration) -> Vec<Font> {
272    let mut fallback_fonts = Vec::new();
273
274    let mut collection = sharedfontique::get_collection();
275    let mut query = collection.query();
276    query.set_families(
277        sharedfontique::FALLBACK_FAMILIES.into_iter().map(fontique::QueryFamily::Generic).chain(
278            core::iter::once(fontique::QueryFamily::Generic(fontique::GenericFamily::Emoji)),
279        ),
280    );
281
282    query.matches_with(|query_font| {
283        if let Some(font) = compiler_config
284            .load_font_by_id(&query_font)
285            .ok()
286            .map(|fontdue_font| Font { font: query_font.clone(), fontdue_font })
287        {
288            fallback_fonts.push(font);
289        }
290
291        fontique::QueryStatus::Continue
292    });
293
294    fallback_fonts
295}
296
297#[cfg(not(target_arch = "wasm32"))]
298fn embed_font(
299    family_name: String,
300    font: Font,
301    pixel_sizes: &[i16],
302    character_coverage: impl Iterator<Item = char>,
303    fallback_fonts: &[Font],
304    _compiler_config: &CompilerConfiguration,
305) -> BitmapFont {
306    let mut character_map: Vec<CharacterMapEntry> = character_coverage
307        .filter(|code_point| {
308            core::iter::once(&font)
309                .chain(fallback_fonts.iter())
310                .any(|font| font.fontdue_font.lookup_glyph_index(*code_point) != 0)
311        })
312        .enumerate()
313        .map(|(glyph_index, code_point)| CharacterMapEntry {
314            code_point,
315            glyph_index: u16::try_from(glyph_index)
316                .expect("more than 65535 glyphs are not supported"),
317        })
318        .collect();
319
320    #[cfg(feature = "sdf-fonts")]
321    let glyphs = if _compiler_config.use_sdf_fonts {
322        embed_sdf_glyphs(pixel_sizes, &character_map, &font, fallback_fonts)
323    } else {
324        embed_alpha_map_glyphs(pixel_sizes, &character_map, &font, fallback_fonts)
325    };
326    #[cfg(not(feature = "sdf-fonts"))]
327    let glyphs = embed_alpha_map_glyphs(pixel_sizes, &character_map, &font, fallback_fonts);
328
329    character_map.sort_by_key(|entry| entry.code_point);
330
331    let face_info = ttf_parser::Face::parse(font.font.blob.data(), font.font.index).unwrap();
332
333    let metrics = sharedfontique::DesignFontMetrics::new_from_face(&face_info);
334
335    BitmapFont {
336        family_name,
337        character_map,
338        units_per_em: metrics.units_per_em,
339        ascent: metrics.ascent,
340        descent: metrics.descent,
341        x_height: metrics.x_height,
342        cap_height: metrics.cap_height,
343        glyphs,
344        weight: face_info.weight().to_number(),
345        italic: face_info.style() != ttf_parser::Style::Normal,
346        #[cfg(feature = "sdf-fonts")]
347        sdf: _compiler_config.use_sdf_fonts,
348        #[cfg(not(feature = "sdf-fonts"))]
349        sdf: false,
350    }
351}
352
353#[cfg(all(not(target_arch = "wasm32")))]
354fn embed_alpha_map_glyphs(
355    pixel_sizes: &[i16],
356    character_map: &Vec<CharacterMapEntry>,
357    font: &Font,
358    fallback_fonts: &[Font],
359) -> Vec<BitmapGlyphs> {
360    use rayon::prelude::*;
361
362    let glyphs = pixel_sizes
363        .par_iter()
364        .map(|pixel_size| {
365            let glyph_data = character_map
366                .par_iter()
367                .map(|CharacterMapEntry { code_point, .. }| {
368                    let (metrics, bitmap) = core::iter::once(font)
369                        .chain(fallback_fonts.iter())
370                        .find_map(|font| {
371                            font.chars()
372                                .contains_key(code_point)
373                                .then(|| font.rasterize(*code_point, *pixel_size as _))
374                        })
375                        .unwrap_or_else(|| font.rasterize(*code_point, *pixel_size as _));
376
377                    BitmapGlyph {
378                        x: i16::try_from(metrics.xmin * 64).expect("large glyph x coordinate"),
379                        y: i16::try_from(metrics.ymin * 64).expect("large glyph y coordinate"),
380                        width: i16::try_from(metrics.width).expect("large width"),
381                        height: i16::try_from(metrics.height).expect("large height"),
382                        x_advance: i16::try_from((metrics.advance_width * 64.) as i64)
383                            .expect("large advance width"),
384                        data: bitmap,
385                    }
386                })
387                .collect();
388
389            BitmapGlyphs { pixel_size: *pixel_size, glyph_data }
390        })
391        .collect();
392    glyphs
393}
394
395#[cfg(all(not(target_arch = "wasm32"), feature = "sdf-fonts"))]
396fn embed_sdf_glyphs(
397    pixel_sizes: &[i16],
398    character_map: &Vec<CharacterMapEntry>,
399    font: &Font,
400    fallback_fonts: &[Font],
401) -> Vec<BitmapGlyphs> {
402    use rayon::prelude::*;
403
404    const RANGE: f64 = 6.;
405
406    let Some(max_size) = pixel_sizes.iter().max() else {
407        return Vec::new();
408    };
409    let min_size = pixel_sizes.iter().min().expect("we have a 'max' so the vector is not empty");
410    let target_pixel_size = (max_size * 2 / 3).max(16).min(RANGE as i16 * min_size);
411
412    let glyph_data = character_map
413        .par_iter()
414        .map(|CharacterMapEntry { code_point, .. }| {
415            core::iter::once(font)
416                .chain(fallback_fonts.iter())
417                .find_map(|font| {
418                    (font.lookup_glyph_index(*code_point) != 0).then(|| {
419                        generate_sdf_for_glyph(font, *code_point, target_pixel_size, RANGE)
420                    })
421                })
422                .unwrap_or_else(|| {
423                    generate_sdf_for_glyph(font, *code_point, target_pixel_size, RANGE)
424                })
425                .unwrap_or_default()
426        })
427        .collect::<Vec<_>>();
428
429    vec![BitmapGlyphs { pixel_size: target_pixel_size, glyph_data }]
430}
431
432#[cfg(all(not(target_arch = "wasm32"), feature = "sdf-fonts"))]
433fn generate_sdf_for_glyph(
434    font: &Font,
435    code_point: char,
436    target_pixel_size: i16,
437    range: f64,
438) -> Option<BitmapGlyph> {
439    use fdsm::transform::Transform;
440    use nalgebra::{Affine2, Similarity2, Vector2};
441
442    let face =
443        fdsm_ttf_parser::ttf_parser::Face::parse(font.font.blob.data(), font.font.index).unwrap();
444    let glyph_id = face.glyph_index(code_point).unwrap_or_default();
445    let mut shape = fdsm_ttf_parser::load_shape_from_face(&face, glyph_id)?;
446
447    let metrics = sharedfontique::DesignFontMetrics::new(&font.font);
448    let target_pixel_size = target_pixel_size as f64;
449    let scale = target_pixel_size / metrics.units_per_em as f64;
450
451    // TODO: handle bitmap glyphs (emojis)
452    let Some(bbox) = face.glyph_bounding_box(glyph_id) else {
453        // For example, for space
454        return Some(BitmapGlyph {
455            x_advance: (face.glyph_hor_advance(glyph_id).unwrap_or(0) as f64 * scale * 64.) as i16,
456            ..Default::default()
457        });
458    };
459
460    let width = ((bbox.x_max as f64 - bbox.x_min as f64) * scale + 2.).ceil() as u32;
461    let height = ((bbox.y_max as f64 - bbox.y_min as f64) * scale + 2.).ceil() as u32;
462    let transformation = nalgebra::convert::<_, Affine2<f64>>(Similarity2::new(
463        Vector2::new(1. - bbox.x_min as f64 * scale, 1. - bbox.y_min as f64 * scale),
464        0.,
465        scale,
466    ));
467
468    // Unlike msdfgen, the transformation is not passed into the
469    // `generate_msdf` function – the coordinates of the control points
470    // must be expressed in terms of pixels on the distance field. To get
471    // the correct units, we pre-transform the shape:
472
473    shape.transform(&transformation);
474
475    let prepared_shape = shape.prepare();
476
477    // Set up the resulting image and generate the distance field:
478
479    let mut sdf = image::GrayImage::new(width, height);
480    fdsm::generate::generate_sdf(&prepared_shape, range, &mut sdf);
481    fdsm::render::correct_sign_sdf(
482        &mut sdf,
483        &prepared_shape,
484        fdsm::bezier::scanline::FillRule::Nonzero,
485    );
486
487    let mut glyph_data = sdf.into_raw();
488
489    // normalize around 0
490    for x in &mut glyph_data {
491        *x = x.wrapping_sub(128);
492    }
493
494    // invert the y coordinate (as the fsdm crate has the y axis inverted)
495    let (w, h) = (width as usize, height as usize);
496    for idx in 0..glyph_data.len() / 2 {
497        glyph_data.swap(idx, (h - idx / w - 1) * w + idx % w);
498    }
499
500    // Add a "0" so that we can always access pos+1 without going out of bound
501    // (so that the last row will look like `data[len-1]*1 + data[len]*0`)
502    glyph_data.push(0);
503
504    let bg = BitmapGlyph {
505        x: i16::try_from((-(1. - bbox.x_min as f64 * scale) * 64.).ceil() as i32)
506            .expect("large glyph x coordinate"),
507        y: i16::try_from((-(1. - bbox.y_min as f64 * scale) * 64.).ceil() as i32)
508            .expect("large glyph y coordinate"),
509        width: i16::try_from(width).expect("large width"),
510        height: i16::try_from(height).expect("large height"),
511        x_advance: i16::try_from(
512            (face.glyph_hor_advance(glyph_id).unwrap() as f64 * scale * 64.).round() as i32,
513        )
514        .expect("large advance width"),
515        data: glyph_data,
516    };
517
518    Some(bg)
519}
520
521fn try_extract_font_size_from_element(elem: &ElementRc, property_name: &str) -> Option<f64> {
522    elem.borrow().bindings.get(property_name).and_then(|expression| {
523        match &expression.borrow().expression {
524            Expression::NumberLiteral(value, Unit::Px) => Some(*value),
525            _ => None,
526        }
527    })
528}
529
530pub fn collect_font_sizes_used(
531    component: &Rc<Component>,
532    scale_factor: f64,
533    sizes_seen: &mut Vec<i16>,
534) {
535    let mut add_font_size = |logical_size: f64| {
536        let pixel_size = (logical_size * scale_factor) as i16;
537        match sizes_seen.binary_search(&pixel_size) {
538            Ok(_) => {}
539            Err(pos) => sizes_seen.insert(pos, pixel_size),
540        }
541    };
542
543    recurse_elem_including_sub_components(component, &(), &mut |elem, _| match elem
544        .borrow()
545        .base_type
546        .to_string()
547        .as_str()
548    {
549        "TextInput" | "Text" | "SimpleText" | "ComplexText" | "StyledTextItem" => {
550            if let Some(font_size) = try_extract_font_size_from_element(elem, "font-size") {
551                add_font_size(font_size)
552            }
553        }
554        "Dialog" | "Window" | "WindowItem" => {
555            if let Some(font_size) = try_extract_font_size_from_element(elem, "default-font-size") {
556                add_font_size(font_size)
557            }
558        }
559        _ => {}
560    });
561}
562
563pub fn scan_string_literals(component: &Rc<Component>, characters_seen: &mut HashSet<char>) {
564    visit_all_expressions(component, |expr, _| {
565        expr.visit_recursive(&mut |expr| {
566            if let Expression::StringLiteral(string) = expr {
567                characters_seen.extend(string.chars());
568            }
569        })
570    })
571}