Compare commits

..
5 changed files with 50 additions and 49 deletions
+31 -16
View File
@@ -498,6 +498,9 @@ impl Backdrops {
/// Most pixels one slide raster may have; it is RGBA, and the backdrop and the content are two rasters.
const MAX_RASTER_PIXELS: f64 = 64_000_000.0;
/// Most ruled note lines one handout area gets (a letter page holds about 30).
const MAX_NOTE_LINES: usize = 500;
/// Render scale for a slide of `slide` units drawn `box_width` points wide at `dpi`, with the total pixel count capped.
fn raster_scale(box_width: f64, slide: (f64, f64), dpi: f64) -> f64 {
let scale = (box_width / 72.0 * dpi.clamp(36.0, 600.0)) / slide.0;
@@ -573,20 +576,17 @@ impl Exporter<'_> {
/// Invisible glyphs exactly over the rasterised text.
fn text_layer(&mut self, s: &mut Surface, pt: &PlacedText, view: Affine, visible: bool) -> Result<()> {
// Each paragraph's characters, gathered once: a long paragraph wraps into many runs.
let mut para_chars: std::collections::HashMap<usize, Vec<char>> = std::collections::HashMap::new();
for run in &pt.layout.runs {
if run.glyphs.is_empty() || run.size <= 0.0 {
continue;
}
let text: String = pt
.body
.paragraphs
.get(run.para)
.map(|p| p.runs.iter().map(|r| r.text.as_str()).collect::<String>())
.unwrap_or_default()
.chars()
.skip(run.chars.0)
.take(run.chars.1.saturating_sub(run.chars.0))
.collect();
let chars = para_chars.entry(run.para).or_insert_with(|| {
pt.body.paragraphs.get(run.para).map(|p| p.runs.iter().flat_map(|r| r.text.chars()).collect()).unwrap_or_default()
});
let (a, b) = (run.chars.0.min(chars.len()), run.chars.1.min(chars.len()));
let text: String = chars.get(a..b.max(a)).unwrap_or_default().iter().collect();
if text.trim().is_empty() {
continue;
}
@@ -666,19 +666,28 @@ impl Exporter<'_> {
let lh = size * 1.3;
let mut y = area.y0 + size;
s.set_fill(Some(grey(0x20, 1.0)));
let space = width(" ");
'outer: for para in text.split('\n') {
// The line's width is kept as words are added, not measured again for each word.
let mut line = String::new();
let mut line_w = 0.0;
for word in para.split(' ') {
let cand = if line.is_empty() { word.to_string() } else { format!("{line} {word}") };
if width(&cand) > area.width() && !line.is_empty() {
let word_w = width(word);
let cand_w = if line.is_empty() { word_w } else { line_w + space + word_w };
if cand_w > area.width() && !line.is_empty() {
if y > area.y1 {
break 'outer;
}
s.draw_text(Point::from_xy(area.x0 as f32, y as f32), font.clone(), size as f32, &line, false, TextDirection::Auto);
y += lh;
line = word.to_string();
line_w = word_w;
} else {
line = cand;
if !line.is_empty() {
line.push(' ');
}
line.push_str(word);
line_w = cand_w;
}
}
if y > area.y1 {
@@ -695,20 +704,26 @@ impl Exporter<'_> {
/// Ruled lines for handwritten notes (3-per-page handouts).
fn note_lines(&self, s: &mut Surface, r: Rect) {
let mut y = r.y0 + 18.0;
// Counted, not stepped: at huge coordinates adding 22 leaves y unchanged, and a huge page
// would want millions of rules.
let first = r.y0 + 18.0;
let count = ((r.y1 - first) / 22.0).floor() + 1.0;
if count.is_nan() || count < 1.0 {
return;
}
s.set_stroke(Some(krilla::paint::Stroke {
paint: krilla::color::rgb::Color::new(0xB0, 0xB0, 0xB0).into(),
width: 0.5,
..Default::default()
}));
while y <= r.y1 {
for i in 0..(count.min(MAX_NOTE_LINES as f64) as usize) {
let y = first + 22.0 * i as f64;
let mut p = kurbo::BezPath::new();
p.move_to((r.x0, y));
p.line_to((r.x1, y));
if let Some(path) = to_path(&p) {
s.draw_path(&path);
}
y += 22.0;
}
s.set_stroke(None);
}
+13
View File
@@ -677,3 +677,16 @@ fn raster_scale_caps_total_pixels_not_each_side() {
let s = raster_scale(720.0, (960.0, 540.0), 200.0);
assert!((s - 720.0 / 72.0 * 200.0 / 960.0).abs() < 1e-12);
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), test)]
fn huge_paper_and_long_notes_export_in_bounded_work() {
// At 1e19 pt adding a rule's 22 pt leaves y unchanged: the rules are counted, not stepped. A narrow page keeps the raster small.
let p = deck(1);
let _ = export(&p, &PdfOptions { paper: (612.0, 1.0e19), ..opts(PageLayout::Handouts { per_page: 3 }) });
// 20,000 short words of notes, wrapped with the line width kept as words are added.
let mut p = deck(1);
Arc::make_mut(&mut p.slides[0]).notes = TextBody::from_text(&"ab ".repeat(20_000));
let bytes = export(&p, &opts(PageLayout::Notes)).expect("export");
assert!(bytes.starts_with(b"%PDF-"));
}
-5
View File
@@ -46,9 +46,6 @@ pub struct Imp<'a> {
pub tag_bytes: usize,
/// The tag budget ran out and the warning was given.
pub tags_capped: bool,
/// Whether a part has relationships of its own, by normalized part name: looked up once
/// however many embedded objects point at the part.
pub has_rels: HashMap<String, bool>,
}
impl Imp<'_> {
@@ -449,7 +446,6 @@ pub fn import_theme(bytes: &[u8]) -> Result<(deckcraft_model::theme::Theme, Vec<
tags_by_part: HashMap::new(),
tag_bytes: 0,
tags_capped: false,
has_rels: HashMap::new(),
};
let part = Part::new(&pkg, &theme_path, &doc);
let theme = theme::read_theme(&mut imp, &part, &doc.root);
@@ -491,7 +487,6 @@ pub fn import_checked(bytes: &[u8]) -> Result<(Presentation, Vec<String>), PptxE
tags_by_part: HashMap::new(),
tag_bytes: 0,
tags_capped: false,
has_rels: HashMap::new(),
};
// Slide ids first so hyperlinks anywhere can point at slides.
let slide_ids = register_slides(&mut imp, &docs, &slide_entries, &mut lost_slides);
+5 -26
View File
@@ -627,22 +627,12 @@ fn push_text_runs(para: &mut Paragraph, props: RunProps, text: &str) {
/// Appends an Office Math (a14:m) run: its plain text and its OMML.
fn push_math_run(para: &mut Paragraph, part: &Part, e: &El) {
if para.runs.len() < MAX_PARAGRAPH_RUNS {
// The text of the outermost `t` elements: a `t` nested in another is already part of
// the outer one's text, and taking it again would copy it once per nesting level.
fn outer_t(e: &El, depth: usize, text: &mut String) {
if depth > crate::xml::MAX_DEPTH {
return;
}
for c in e.elements() {
if c.is("t") {
text.push_str(&c.text());
} else {
outer_t(c, depth + 1, text);
}
}
}
let mut text = String::new();
outer_t(e, 0, &mut text);
let mut ts = vec![];
e.find_all("t", &mut ts);
for t in ts {
text.push_str(&t.text());
}
let omml = e.elements().map(|c| part.keep_inner(c)).collect::<String>();
para.runs.push(Run { text, props: RunProps::default(), kind: RunKind::Math { omml } });
}
@@ -687,17 +677,6 @@ mod tests {
use super::*;
use crate::xml::parse;
#[test]
fn nested_math_text_is_taken_once() {
// 200 `m:t` nested in each other around one word: the word, not 200 copies of it.
let x = format!(r#"<a14:m xmlns:a14="a" xmlns:m="m"><m:oMath>{}word{}</m:oMath></a14:m>"#, "<m:t>".repeat(200), "</m:t>".repeat(200));
let d = parse(x.as_bytes()).unwrap();
let part = Part { name: "ppt/slides/slide1.xml".into(), rels: Default::default(), ns: vec![], inner_ns: vec![] };
let mut para = Paragraph::default();
push_math_run(&mut para, &part, &d.root);
assert_eq!(para.runs[0].text, "word");
}
#[test]
fn colors_and_transforms() {
let d = parse(
+1 -2
View File
@@ -475,8 +475,7 @@ fn frame(imp: &mut Imp, part: &Part, ctx: &mut IdCtx, e: &El, depth: usize) -> S
.into_iter()
.map(|id| {
let rel = part.rels.get(&id).filter(|r| !r.external)?;
let pkg = imp.pkg;
if *imp.has_rels.entry(crate::opc::norm(&rel.target)).or_insert_with(|| !pkg.rels(&rel.target).items.is_empty()) {
if !imp.pkg.rels(&rel.target).items.is_empty() {
return None;
}
let (kind, target) = (rel.ty.clone(), rel.target.clone());