mirror of
https://github.com/storytold/deckcraft.git
synced 2026-11-04 01:19:40 -06:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ad81f944f | ||
|
|
d021e2baed | ||
|
|
bb0048e78d |
@@ -1,8 +1,6 @@
|
||||
//! 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};
|
||||
@@ -14,13 +12,8 @@ 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());
|
||||
@@ -34,8 +27,12 @@ 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();
|
||||
// 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();
|
||||
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);
|
||||
}
|
||||
}
|
||||
out.sort_by_key(|f| f.to_lowercase());
|
||||
out
|
||||
}
|
||||
@@ -43,10 +40,9 @@ 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() && seen.insert(f.to_ascii_lowercase()) {
|
||||
if !f.is_empty() && !out.iter().any(|o| o.eq_ignore_ascii_case(&f)) {
|
||||
out.push(f);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -811,10 +811,32 @@ fn clamp_selection(st: &mut DocState) {
|
||||
/// Default for [`History::byte_limit`].
|
||||
pub const UNDO_BYTE_BUDGET: usize = 256 << 20;
|
||||
|
||||
/// A rough size of a text body in memory: its paragraphs and runs, at their inline struct sizes.
|
||||
fn text_cost(t: &deckcraft_model::text::TextBody) -> usize {
|
||||
use deckcraft_model::text::{Paragraph, Run};
|
||||
let (para, run) = (std::mem::size_of::<Paragraph>(), std::mem::size_of::<Run>());
|
||||
t.paragraphs.iter().map(|p| para + p.runs.iter().map(|r| run + r.text.len()).sum::<usize>()).sum()
|
||||
}
|
||||
|
||||
/// A rough size of comments in memory, replies included.
|
||||
fn comments_cost(cs: &[deckcraft_model::Comment], depth: usize) -> usize {
|
||||
let kids = |c: &deckcraft_model::Comment| if depth < 64 { comments_cost(&c.replies, depth + 1) } else { 0 };
|
||||
cs.iter().map(|c| 256 + c.author.len() + c.initials.len() + c.text.len() + c.date.len() + kids(c)).sum()
|
||||
}
|
||||
|
||||
/// A rough size of a slide's own data besides its shapes: notes, comments, tags and kept XML,
|
||||
/// all copied with the slide when an edit changes it.
|
||||
fn slide_extra_cost(s: &deckcraft_model::Slide) -> usize {
|
||||
text_cost(&s.notes)
|
||||
+ comments_cost(&s.comments, 0)
|
||||
+ s.name.len()
|
||||
+ s.tags.iter().map(|(k, v)| 48 + k.len() + v.len()).sum::<usize>()
|
||||
+ s.raw_ext.as_ref().map_or(0, String::len)
|
||||
}
|
||||
|
||||
/// A rough size of a shape in memory: the struct, its text and kept XML, its children.
|
||||
fn shape_cost(sh: &deckcraft_model::Shape, depth: usize) -> usize {
|
||||
let text: usize =
|
||||
sh.text.as_ref().map_or(0, |t| t.paragraphs.iter().map(|p| 96 + p.runs.iter().map(|r| 128 + r.text.len()).sum::<usize>()).sum());
|
||||
let text: usize = sh.text.as_ref().map_or(0, text_cost);
|
||||
let kids: usize = if depth < 64 { sh.children().iter().map(|c| shape_cost(c, depth + 1)).sum() } else { 0 };
|
||||
768 + text + sh.raw_ext.as_ref().map_or(0, String::len) + kids
|
||||
}
|
||||
@@ -832,7 +854,7 @@ pub fn undo_cost(doc: &Presentation, newer: &Presentation) -> usize {
|
||||
let media: HashSet<*const Vec<u8>> = newer.media.iter().map(|m| Arc::as_ptr(&m.data)).collect();
|
||||
let mut n = 64 * doc.slides.len();
|
||||
for s in doc.slides.iter().filter(|s| !slides.contains(&Arc::as_ptr(s))) {
|
||||
n += 512 + shapes_cost(&s.shapes);
|
||||
n += 512 + shapes_cost(&s.shapes) + slide_extra_cost(s);
|
||||
}
|
||||
for m in doc.masters.iter().filter(|m| !masters.contains(&Arc::as_ptr(m))) {
|
||||
n += 4096 + shapes_cost(&m.shapes) + m.layouts.iter().map(|l| 512 + shapes_cost(&l.shapes)).sum::<usize>();
|
||||
|
||||
+19
-21
@@ -3338,6 +3338,25 @@ fn undo_history_is_capped_by_memory_as_well_as_steps() {
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), 120);
|
||||
}
|
||||
|
||||
/// A slide's notes count toward the undo budget: small edits to a slide with large notes keep a
|
||||
/// copy of those notes per step, and must not slip past the budget as if the slide were empty.
|
||||
#[test]
|
||||
fn undo_history_counts_slide_notes() {
|
||||
let mut s = Session::with_new();
|
||||
let notes = "n".repeat(1 << 20);
|
||||
s.edit(|doc, _| {
|
||||
std::sync::Arc::make_mut(&mut doc.slides[0]).notes = deckcraft_model::text::TextBody::from_text(¬es);
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
s.doc_mut().unwrap().history = History { limit: 500, byte_limit: 8 << 20, ..Default::default() };
|
||||
for i in 0..40 {
|
||||
s.execute("slide.rename", &json!({"name": format!("v{i}"), "index": 0})).unwrap();
|
||||
}
|
||||
let n = s.doc().unwrap().history.undo.len();
|
||||
assert!((1..=8).contains(&n), "{n} steps of 1 MB notes kept under an 8 MB budget");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn picture_styles_replace_each_other_in_one_step() {
|
||||
const PNG: &[u8] = &[
|
||||
@@ -6458,24 +6477,3 @@ 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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user