//! Fonts a presentation uses that this machine doesn't have, so opening it can say so (its text //! draws in a stand-in; see `FontDb::document_face`). use deckcraft_fonts::FontDb; use deckcraft_model::resolve::{self, Ctx}; use deckcraft_model::text::{Bullet, TextBody}; use deckcraft_model::{Presentation, Shape, ShapeKind}; /// Load each embedded face into the font database, so text in that family draws with the file's /// font on a machine that doesn't have it. A face already loaded (bundled or installed) stays; /// the part bytes are kept either way. MicroType Express subsets have no TrueType or OpenType /// font to load and are left for the round trip. pub fn install_embedded_fonts(p: &Presentation) { let db = FontDb::global(); for font in &p.embedded_fonts { for id in font.faces().into_iter().flatten() { let Some(item) = p.media(id) else { continue }; let Some(sfnt) = deckcraft_fonts::embedded_sfnt(&item.data) else { continue }; db.add_font(sfnt.to_vec()); } } } /// The font families the slides' text is drawn in (theme fonts resolved, bullet fonts included) /// that aren't installed or bundled, sorted, without duplicates (case-insensitive). pub fn missing_fonts(p: &Presentation) -> Vec { let db = FontDb::global(); let mut out: Vec = Vec::new(); for family in used_fonts(p) { if !db.has_family(&family) && !out.iter().any(|f| f.eq_ignore_ascii_case(&family)) { out.push(family); } } out.sort_by_key(|f| f.to_lowercase()); out } /// The families the slides' text runs and bullets are drawn in. pub fn used_fonts(p: &Presentation) -> Vec { let mut out: Vec = Vec::new(); let mut add = |f: String| { let f = f.trim().to_string(); if !f.is_empty() && !out.iter().any(|o| o.eq_ignore_ascii_case(&f)) { out.push(f); } }; for slide in &p.slides { let Some(ctx) = Ctx::for_slide(p, slide) else { continue }; deckcraft_model::walk(&slide.shapes, &mut |s, _| { if let Some(body) = &s.text { body_fonts(&ctx, s, body, &mut add); } if let ShapeKind::Table(t) = &s.kind { for cell in t.rows.iter().flat_map(|r| &r.cells) { body_fonts(&ctx, s, &cell.text, &mut add); } } }); } out } fn body_fonts(ctx: &Ctx, s: &Shape, body: &TextBody, add: &mut dyn FnMut(String)) { for para in &body.paragraphs { let has_text = para.runs.iter().any(|r| !r.text.trim().is_empty()); if !has_text { continue; } for run in para.runs.iter().filter(|r| !r.text.trim().is_empty()) { let r = resolve::run(ctx, s, para, &run.props); add(resolve::font_family(ctx, &r)); } let pp = resolve::para(ctx, s, para); if matches!(pp.bullet, Some(Bullet::Char { .. })) && let Some(f) = pp.bullet_font.as_deref() { add(ctx.master.theme.font(f)); } } } /// Replace the font family `from` with `to` everywhere it is named explicitly: text runs (and /// their east Asian, complex script and symbol faces), bullet fonts, paragraph end marks, list /// styles of shapes, layouts and masters, the master text styles, the deck default style, speaker /// notes, tables and the theme fonts. Names compare case-insensitively; theme references /// (`+mn-lt`) are left alone. Returns how many names changed. pub fn replace_font(p: &mut Presentation, from: &str, to: &str) -> usize { use std::sync::Arc; let from = from.trim(); let to = to.trim(); if from.is_empty() || to.is_empty() || from.starts_with('+') || from == to { return 0; } let mut n = 0usize; let mut fix = |f: &mut Option| { if f.as_deref().is_some_and(|x| x.trim().eq_ignore_ascii_case(from)) { *f = Some(to.to_string()); n = n.saturating_add(1); } }; replace_list(&mut p.default_text_style, &mut fix); for slide in &mut p.slides { let sl = Arc::make_mut(slide); replace_shapes(&mut sl.shapes, &mut fix); replace_body(&mut sl.notes, &mut fix); } let mut theme_hits = 0usize; let masters = p.masters.iter_mut().chain(p.notes_master.iter_mut()).chain(p.handout_master.iter_mut()); for m in masters { let m = Arc::make_mut(m); replace_shapes(&mut m.shapes, &mut fix); for l in [&mut m.title_style, &mut m.body_style, &mut m.other_style] { replace_list(l, &mut fix); } for lay in &mut m.layouts { replace_shapes(&mut lay.shapes, &mut fix); } for set in [&mut m.theme.fonts.major, &mut m.theme.fonts.minor] { for face in [&mut set.latin, &mut set.ea, &mut set.cs].into_iter().chain(set.scripts.iter_mut().map(|s| &mut s.typeface)) { if face.trim().eq_ignore_ascii_case(from) { *face = to.to_string(); theme_hits = theme_hits.saturating_add(1); } } } } n.saturating_add(theme_hits) } fn replace_shapes(shapes: &mut [Shape], fix: &mut dyn FnMut(&mut Option)) { for s in shapes { if let Some(body) = &mut s.text { replace_body(body, fix); } if let ShapeKind::Table(t) = &mut s.kind { for cell in t.rows.iter_mut().flat_map(|r| &mut r.cells) { replace_body(&mut cell.text, fix); } } if let Some(children) = s.children_mut() { replace_shapes(children, fix); } } } fn replace_body(body: &mut TextBody, fix: &mut dyn FnMut(&mut Option)) { replace_list(&mut body.list_style, fix); for para in &mut body.paragraphs { fix(&mut para.props.bullet_font); replace_run(&mut para.end_props, fix); for run in &mut para.runs { replace_run(&mut run.props, fix); } } } fn replace_list(l: &mut deckcraft_model::text::ListStyle, fix: &mut dyn FnMut(&mut Option)) { for lvl in l.levels.iter_mut().flatten() { fix(&mut lvl.para.bullet_font); replace_run(&mut lvl.run, fix); } } fn replace_run(r: &mut deckcraft_model::text::RunProps, fix: &mut dyn FnMut(&mut Option)) { for f in [&mut r.font, &mut r.font_ea, &mut r.font_cs, &mut r.font_sym] { fix(f); } }