mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-27 12:07:02 -06:00
A deck that embeds a TrueType or OpenType face, including one wrapped as Embedded OpenType, now draws with that face and saves it back. MicroType Express subsets stay in the file and still draw in a stand-in. Co-authored-by: Cursor <cursoragent@cursor.com>
81 lines
3.0 KiB
Rust
81 lines
3.0 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};
|
|
|
|
/// 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<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));
|
|
}
|
|
}
|
|
}
|