mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-28 20:47:15 -06:00
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
66 lines
2.3 KiB
Rust
66 lines
2.3 KiB
Rust
//! 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};
|
|
|
|
/// 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<String> {
|
|
let db = FontDb::global();
|
|
let mut out: Vec<String> = 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.
|
|
fn used_fonts(p: &Presentation) -> Vec<String> {
|
|
let mut out: Vec<String> = 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));
|
|
}
|
|
}
|
|
}
|