Compare commits

..
3 changed files with 53 additions and 91 deletions
+11 -7
View File
@@ -1,6 +1,8 @@
//! 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 std::collections::HashSet;
use deckcraft_fonts::FontDb;
use deckcraft_model::resolve::{self, Ctx};
use deckcraft_model::text::{Bullet, TextBody};
@@ -12,8 +14,13 @@ use deckcraft_model::{Presentation, Shape, ShapeKind};
/// font to load and are left for the round trip.
pub fn install_embedded_fonts(p: &Presentation) {
let db = FontDb::global();
// Faces can name one media item many times; each is copied and enumerated once.
let mut seen = HashSet::new();
for font in &p.embedded_fonts {
for id in font.faces().into_iter().flatten() {
if !seen.insert(id) {
continue;
}
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());
@@ -27,12 +34,8 @@ pub fn install_embedded_fonts(p: &Presentation) {
/// and can't be installed, so they aren't reported.
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 !family.starts_with('.') && !db.has_family(&family) && !out.iter().any(|f| f.eq_ignore_ascii_case(&family)) {
out.push(family);
}
}
// used_fonts is already free of case-insensitive duplicates.
let mut out: Vec<String> = used_fonts(p).into_iter().filter(|f| !f.starts_with('.') && !db.has_family(f)).collect();
out.sort_by_key(|f| f.to_lowercase());
out
}
@@ -40,9 +43,10 @@ pub fn missing_fonts(p: &Presentation) -> Vec<String> {
/// The families the slides' text runs and bullets are drawn in.
pub fn used_fonts(p: &Presentation) -> Vec<String> {
let mut out: Vec<String> = Vec::new();
let mut seen: HashSet<String> = HashSet::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)) {
if !f.is_empty() && seen.insert(f.to_ascii_lowercase()) {
out.push(f);
}
};
+21
View File
@@ -6458,3 +6458,24 @@ fn master_body_placeholder_format_keeps_per_level_formatting() {
assert_eq!((level(&s, 0).run.size, level(&s, 1).run.size), (Some(28.0), Some(24.0)));
assert_eq!(shown(&s), vec![Some(28.0), Some(24.0)]);
}
#[test]
fn many_distinct_fonts_are_listed_once_each_without_a_quadratic_scan() {
// 30,000 runs in 15,000 families, each written twice in different case.
let mut p = deckcraft_model::Presentation::default();
let slide = std::sync::Arc::make_mut(&mut p.slides[0]);
let mut body = deckcraft_model::text::TextBody::from_text("x");
let run = body.paragraphs[0].runs[0].clone();
body.paragraphs[0].runs = (0..30_000)
.map(|i| {
let mut r = run.clone();
let name = format!("Qqq Font {}", i / 2);
r.props.font = Some(if i % 2 == 0 { name } else { name.to_uppercase() });
r
})
.collect();
slide.shapes[0].text = Some(body);
let used = crate::fonts::used_fonts(&p);
assert_eq!(used.iter().filter(|f| f.starts_with("Qqq Font ")).count(), 15_000);
assert_eq!(crate::fonts::missing_fonts(&p).iter().filter(|f| f.to_ascii_lowercase().starts_with("qqq font ")).count(), 15_000);
}
+21 -84
View File
@@ -100,7 +100,8 @@ impl Part {
}
/// Like [`Part::keep`], also declaring prefixes that only an enclosing wrapper binds.
pub fn keep_inner(&self, el: &El) -> String {
keep_with(el, &[&self.ns, &self.inner_ns])
let ns: Vec<(String, String)> = self.ns.iter().chain(&self.inner_ns).cloned().collect();
keep_xml(el, &ns)
}
}
@@ -135,58 +136,36 @@ fn inner_decls(root: &El, ns: &[(String, String)]) -> Vec<(String, String)> {
out
}
/// Longest namespace name copied onto a kept fragment or a resolved `mc` branch. Real ones are
/// URIs of well under 100 bytes; a longer one is not copied, so one declaration cannot be
/// multiplied by every fragment that uses its prefix.
const MAX_NS_VALUE: usize = 2048;
/// Prefixes `el` and its descendants use in element and attribute names, and those listed in
/// `mc:Ignorable` (which must stay declared for the markup-compatibility rules to hold).
fn used_prefixes(el: &El) -> HashSet<&str> {
fn rec<'a>(e: &'a El, used: &mut HashSet<&'a str>, depth: usize) {
/// Serialize `el`, declaring every prefix it uses that the source root declared.
pub fn keep_xml(el: &El, ns: &[(String, String)]) -> String {
let mut used = HashSet::new();
fn rec(e: &El, used: &mut HashSet<String>, depth: usize) {
if depth > crate::xml::MAX_DEPTH {
return;
}
if let Some((p, _)) = e.name.split_once(':') {
used.insert(p);
used.insert(p.to_string());
}
for (k, v) in &e.attrs {
if let Some((p, local)) = k.split_once(':')
for (k, _) in &e.attrs {
if let Some((p, _)) = k.split_once(':')
&& p != "xmlns"
&& p != "xml"
{
used.insert(p);
if local == "Ignorable" {
used.extend(v.split_whitespace());
}
used.insert(p.to_string());
}
}
for c in e.elements() {
rec(c, used, depth + 1);
}
}
let mut used = HashSet::new();
rec(el, &mut used, 0);
used
}
/// Serialize `el`, declaring every prefix it uses that the source root declared.
pub fn keep_xml(el: &El, ns: &[(String, String)]) -> String {
keep_with(el, &[ns])
}
fn keep_with(el: &El, decls: &[&[(String, String)]]) -> String {
let used = used_prefixes(el);
let mut have: HashSet<&str> = el.attrs.iter().map(|(a, _)| a.as_str()).collect();
let mut extra = Vec::new();
for (k, v) in decls.iter().flat_map(|d| d.iter()) {
let mut el = el.clone();
for (k, v) in ns {
let p = k.strip_prefix("xmlns:").unwrap_or("");
if !p.is_empty() && used.contains(p) && v.len() <= MAX_NS_VALUE && have.insert(k) {
extra.push((k.clone(), v.clone()));
if !p.is_empty() && used.contains(p) && !el.attrs.iter().any(|(a, _)| a == k) {
el.attrs.push((k.clone(), v.clone()));
}
}
let mut el = el.clone();
el.attrs.extend(extra);
el.to_xml()
}
@@ -336,12 +315,9 @@ fn kept_parts(pkg: &Package, pres_path: &str) -> Vec<deckcraft_model::KeptPart>
let rp = opc::rels_path(&name);
if let Some(rels_xml) = text(&rp) {
for sub in pkg.rels(&name).items.iter().filter(|s| !s.external && keepable(&s.target)) {
// Checked before the part is read: repeated relationships to one large part
// must not copy it again each time.
if out.len() >= 400 || out.iter().any(|k| k.name.eq_ignore_ascii_case(&sub.target)) {
continue;
}
if let Some(x) = text(&sub.target) {
if let Some(x) = text(&sub.target)
&& !out.iter().any(|k| k.name.eq_ignore_ascii_case(&sub.target))
{
out.push(KeptPart {
content_type: pkg.content_type(&sub.target).unwrap_or_default(),
name: sub.target.clone(),
@@ -389,19 +365,13 @@ fn resolve_mc_rels(el: &mut El, depth: usize, rels: Option<&Rels>) {
if let Some(b) = branch {
for n in &b.children {
if let Node::El(e) = n {
// Carry the namespace declarations from the branch that its content
// uses (all of them per child made children x declarations copies).
let used = used_prefixes(e);
let mut have: HashSet<&str> = e.attrs.iter().map(|(a, _)| a.as_str()).collect();
let mut extra = Vec::new();
let mut e = e.clone();
// Carry namespace declarations from the branch onto its content.
for (k, v) in b.attrs.iter().chain(ac.attrs.iter()) {
let needed = k == "xmlns" || k.strip_prefix("xmlns:").is_some_and(|p| used.contains(p));
if needed && v.len() <= MAX_NS_VALUE && have.insert(k) {
extra.push((k.clone(), v.clone()));
if k.starts_with("xmlns") && !e.attrs.iter().any(|(a, _)| a == k) {
e.attrs.push((k.clone(), v.clone()));
}
}
let mut e = e.clone();
e.attrs.extend(extra);
out.push(Node::El(e));
}
}
@@ -1444,36 +1414,3 @@ fn header_footer(p: &Presentation) -> HeaderFooter {
}
hf
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mc_branches_carry_only_the_declarations_their_content_uses() {
// 2,000 declarations on the branch and 2,000 children: each child gets the one it uses
// (and the default), not all 2,000.
let decls: String = (0..2000).map(|i| format!(r#" xmlns:n{i}="urn:n{i}""#)).collect();
let kids: String = (0..2000).map(|i| format!("<n{i}:x/>")).collect();
let x = format!(
r#"<r xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"><mc:AlternateContent><mc:Fallback xmlns="urn:d"{decls}>{kids}</mc:Fallback></mc:AlternateContent></r>"#
);
let mut d = crate::xml::parse(x.as_bytes()).unwrap();
resolve_mc(&mut d.root, 0);
let kids: Vec<&El> = d.root.elements().collect();
assert_eq!(kids.len(), 2000);
assert_eq!(kids[7].attrs, vec![("xmlns".to_string(), "urn:d".to_string()), ("xmlns:n7".to_string(), "urn:n7".to_string())]);
}
#[test]
fn kept_fragments_skip_oversized_namespace_names() {
let big = "u".repeat(MAX_NS_VALUE + 1);
let ns = vec![("xmlns:a".to_string(), "urn:a".to_string()), ("xmlns:b".to_string(), big)];
let d = crate::xml::parse(br#"<a:x xmlns:a="urn:a" xmlns:b="urn:b"><b:y/></a:x>"#).unwrap();
let mut el = d.root.clone();
el.attrs.clear();
let out = keep_xml(&el, &ns);
assert!(out.contains(r#"xmlns:a="urn:a""#), "{out}");
assert!(!out.contains("uuuu"), "the oversized declaration is not copied");
}
}