mirror of
https://github.com/storytold/deckcraft.git
synced 2026-11-10 04:19:33 -06:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
953ae8d060 | ||
|
|
1b2b48c0d7 | ||
|
|
9e7fab1038 |
@@ -46,6 +46,12 @@ pub struct Imp<'a> {
|
|||||||
pub tag_bytes: usize,
|
pub tag_bytes: usize,
|
||||||
/// The tag budget ran out and the warning was given.
|
/// The tag budget ran out and the warning was given.
|
||||||
pub tags_capped: bool,
|
pub tags_capped: bool,
|
||||||
|
/// Bytes of preserved theme XML kept by the masters read so far.
|
||||||
|
pub theme_extra_bytes: usize,
|
||||||
|
/// Shapes read from SmartArt drawings so far (see `shapes::MAX_SMARTART_SHAPES`).
|
||||||
|
pub smartart_shapes: usize,
|
||||||
|
/// Bytes of SmartArt layout, style and colour definitions copied into diagrams so far.
|
||||||
|
pub diagram_xml_bytes: usize,
|
||||||
/// Whether a part has relationships of its own, by normalized part name: looked up once
|
/// Whether a part has relationships of its own, by normalized part name: looked up once
|
||||||
/// however many embedded objects point at the part.
|
/// however many embedded objects point at the part.
|
||||||
pub has_rels: HashMap<String, bool>,
|
pub has_rels: HashMap<String, bool>,
|
||||||
@@ -449,6 +455,9 @@ pub fn import_theme(bytes: &[u8]) -> Result<(deckcraft_model::theme::Theme, Vec<
|
|||||||
tags_by_part: HashMap::new(),
|
tags_by_part: HashMap::new(),
|
||||||
tag_bytes: 0,
|
tag_bytes: 0,
|
||||||
tags_capped: false,
|
tags_capped: false,
|
||||||
|
theme_extra_bytes: 0,
|
||||||
|
smartart_shapes: 0,
|
||||||
|
diagram_xml_bytes: 0,
|
||||||
has_rels: HashMap::new(),
|
has_rels: HashMap::new(),
|
||||||
};
|
};
|
||||||
let part = Part::new(&pkg, &theme_path, &doc);
|
let part = Part::new(&pkg, &theme_path, &doc);
|
||||||
@@ -491,6 +500,9 @@ pub fn import_checked(bytes: &[u8]) -> Result<(Presentation, Vec<String>), PptxE
|
|||||||
tags_by_part: HashMap::new(),
|
tags_by_part: HashMap::new(),
|
||||||
tag_bytes: 0,
|
tag_bytes: 0,
|
||||||
tags_capped: false,
|
tags_capped: false,
|
||||||
|
theme_extra_bytes: 0,
|
||||||
|
smartart_shapes: 0,
|
||||||
|
diagram_xml_bytes: 0,
|
||||||
has_rels: HashMap::new(),
|
has_rels: HashMap::new(),
|
||||||
};
|
};
|
||||||
// Slide ids first so hyperlinks anywhere can point at slides.
|
// Slide ids first so hyperlinks anywhere can point at slides.
|
||||||
|
|||||||
@@ -559,9 +559,20 @@ fn diagram_data(imp: &mut Imp, target: &str) -> Option<deckcraft_model::MediaId>
|
|||||||
Some(id)
|
Some(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Shapes all SmartArt drawings of one presentation may add; a frame read after that is left as a
|
||||||
|
/// placeholder. Each frame that references a drawing reads it again.
|
||||||
|
const MAX_SMARTART_SHAPES: usize = 250_000;
|
||||||
|
|
||||||
|
/// SmartArt definition XML (layout, style, colours) all diagrams of one presentation may copy.
|
||||||
|
const MAX_DIAGRAM_XML_BYTES: usize = 32 << 20;
|
||||||
|
|
||||||
|
fn count_shapes(shapes: &[Shape]) -> usize {
|
||||||
|
shapes.iter().map(|s| 1 + count_shapes(s.children())).sum()
|
||||||
|
}
|
||||||
|
|
||||||
/// SmartArt: read the pre-drawn shapes of its drawing part as a group.
|
/// SmartArt: read the pre-drawn shapes of its drawing part as a group.
|
||||||
fn smartart(imp: &mut Imp, part: &Part, gd: &El, frame: &Shape, depth: usize) -> Option<Shape> {
|
fn smartart(imp: &mut Imp, part: &Part, gd: &El, frame: &Shape, depth: usize) -> Option<Shape> {
|
||||||
if depth > 8 {
|
if depth > 8 || imp.smartart_shapes >= MAX_SMARTART_SHAPES {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let rel_ids = gd.child("relIds")?;
|
let rel_ids = gd.child("relIds")?;
|
||||||
@@ -573,10 +584,13 @@ fn smartart(imp: &mut Imp, part: &Part, gd: &El, frame: &Shape, depth: usize) ->
|
|||||||
let dpart = Part::new(imp.pkg, &drawing_rel, &d);
|
let dpart = Part::new(imp.pkg, &drawing_rel, &d);
|
||||||
let tree = d.root.child("spTree")?;
|
let tree = d.root.child("spTree")?;
|
||||||
let mut sub = IdCtx { fresh: true, ..Default::default() };
|
let mut sub = IdCtx { fresh: true, ..Default::default() };
|
||||||
|
let before = imp.smartart_shapes;
|
||||||
let children = sp_tree(imp, &dpart, &mut sub, tree, depth + 1);
|
let children = sp_tree(imp, &dpart, &mut sub, tree, depth + 1);
|
||||||
if children.is_empty() {
|
if children.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
// Counted once here, nested SmartArt included: a drawing that frames reuse counts every time it is read.
|
||||||
|
imp.smartart_shapes = before.saturating_add(count_shapes(&children));
|
||||||
let children = smartart_text_rects(imp, &mut sub, tree, children, depth + 1);
|
let children = smartart_text_rects(imp, &mut sub, tree, children, depth + 1);
|
||||||
let mut g = frame.clone();
|
let mut g = frame.clone();
|
||||||
let x = frame.xfrm.unwrap_or_default();
|
let x = frame.xfrm.unwrap_or_default();
|
||||||
@@ -704,15 +718,22 @@ fn text_area(geom: &Geom, w: f64, h: f64) -> deckcraft_geom::Rect {
|
|||||||
|
|
||||||
/// The outline of a SmartArt graphic (for the Text Pane), with its layout, quick style and colour
|
/// The outline of a SmartArt graphic (for the Text Pane), with its layout, quick style and colour
|
||||||
/// parts kept to be written back.
|
/// parts kept to be written back.
|
||||||
fn diagram_outline(imp: &Imp, part: &Part, rel_ids: &El, data: &El) -> Option<deckcraft_model::Diagram> {
|
fn diagram_outline(imp: &mut Imp, part: &Part, rel_ids: &El, data: &El) -> Option<deckcraft_model::Diagram> {
|
||||||
let nodes = crate::dgm::outline(data);
|
let nodes = crate::dgm::outline(data);
|
||||||
if nodes.is_empty() {
|
if nodes.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
let (pkg, spent) = (imp.pkg, std::cell::Cell::new(imp.diagram_xml_bytes));
|
||||||
let raw = |attr: &str| -> Option<String> {
|
let raw = |attr: &str| -> Option<String> {
|
||||||
let rel = part.rels.get(rel_ids.attr(attr)?)?;
|
let rel = part.rels.get(rel_ids.attr(attr)?)?;
|
||||||
let bytes = imp.pkg.get(&rel.target)?;
|
let bytes = pkg.get(&rel.target)?;
|
||||||
(bytes.len() <= 4 << 20).then(|| String::from_utf8(bytes.to_vec()).ok()).flatten()
|
// Every diagram copies its definitions, so the copies share one presentation-wide budget.
|
||||||
|
if bytes.len() > 4 << 20 || spent.get().saturating_add(bytes.len()) > MAX_DIAGRAM_XML_BYTES {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let xml = String::from_utf8(bytes.to_vec()).ok()?;
|
||||||
|
spent.set(spent.get() + xml.len());
|
||||||
|
Some(xml)
|
||||||
};
|
};
|
||||||
let layout_xml = raw("r:lo");
|
let layout_xml = raw("r:lo");
|
||||||
let (layout_id, cat) = layout_xml
|
let (layout_id, cat) = layout_xml
|
||||||
@@ -733,6 +754,8 @@ fn diagram_outline(imp: &Imp, part: &Part, rel_ids: &El, data: &El) -> Option<de
|
|||||||
let pr = data.path(&["ptLst"]).and_then(|l| l.children_named("pt").find(|p| p.attr("type") == Some("doc"))).and_then(|p| p.child("prSet"));
|
let pr = data.path(&["ptLst"]).and_then(|l| l.children_named("pt").find(|p| p.attr("type") == Some("doc"))).and_then(|p| p.child("prSet"));
|
||||||
let style = crate::dgm::style_for(pr.and_then(|p| p.attr("qsTypeId")).unwrap_or("")).to_string();
|
let style = crate::dgm::style_for(pr.and_then(|p| p.attr("qsTypeId")).unwrap_or("")).to_string();
|
||||||
let colors = crate::dgm::colors_for(pr.and_then(|p| p.attr("csTypeId")).unwrap_or(""));
|
let colors = crate::dgm::colors_for(pr.and_then(|p| p.attr("csTypeId")).unwrap_or(""));
|
||||||
|
let (style_xml, colors_xml) = (raw("r:qs"), raw("r:cs"));
|
||||||
|
imp.diagram_xml_bytes = spent.get();
|
||||||
Some(deckcraft_model::Diagram {
|
Some(deckcraft_model::Diagram {
|
||||||
kind: crate::dgm::kind_for(&id, &cat).to_string(),
|
kind: crate::dgm::kind_for(&id, &cat).to_string(),
|
||||||
nodes,
|
nodes,
|
||||||
@@ -741,8 +764,8 @@ fn diagram_outline(imp: &Imp, part: &Part, rel_ids: &El, data: &El) -> Option<de
|
|||||||
imported: true,
|
imported: true,
|
||||||
layout_id: id,
|
layout_id: id,
|
||||||
layout_xml,
|
layout_xml,
|
||||||
style_xml: raw("r:qs"),
|
style_xml,
|
||||||
colors_xml: raw("r:cs"),
|
colors_xml,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ fn font_set(e: Option<&El>) -> FontSet {
|
|||||||
FontSet { latin: tf("latin"), ea: tf("ea"), cs: tf("cs"), scripts }
|
FontSet { latin: tf("latin"), ea: tf("ea"), cs: tf("cs"), scripts }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Bytes of preserved theme XML (object defaults and extra colour schemes) all masters of one
|
||||||
|
/// presentation may keep.
|
||||||
|
const MAX_THEME_EXTRA_BYTES: usize = 16 << 20;
|
||||||
|
|
||||||
pub fn read_theme(imp: &mut Imp, part: &Part, root: &El) -> Theme {
|
pub fn read_theme(imp: &mut Imp, part: &Part, root: &El) -> Theme {
|
||||||
let mut t = Theme { name: root.attr("name").unwrap_or("Theme").to_string(), ..Default::default() };
|
let mut t = Theme { name: root.attr("name").unwrap_or("Theme").to_string(), ..Default::default() };
|
||||||
let Some(te) = root.child("themeElements") else { return t };
|
let Some(te) = root.child("themeElements") else { return t };
|
||||||
@@ -89,8 +93,13 @@ pub fn read_theme(imp: &mut Imp, part: &Part, root: &El) -> Theme {
|
|||||||
.filter(|e| !super::has_rel_refs(e))
|
.filter(|e| !super::has_rel_refs(e))
|
||||||
.map(|e| part.keep(e))
|
.map(|e| part.keep(e))
|
||||||
.collect();
|
.collect();
|
||||||
if !extra.is_empty() {
|
// Every master that references this theme keeps its own copy of the preserved XML, so the
|
||||||
|
// copies share one budget for the presentation.
|
||||||
|
if !extra.is_empty() && imp.theme_extra_bytes.saturating_add(extra.len()) <= MAX_THEME_EXTRA_BYTES {
|
||||||
|
imp.theme_extra_bytes += extra.len();
|
||||||
t.raw_extra = Some(extra);
|
t.raw_extra = Some(extra);
|
||||||
|
} else if !extra.is_empty() {
|
||||||
|
log::warn!("pptx: theme object defaults not kept; the preserved-XML budget is spent");
|
||||||
}
|
}
|
||||||
t
|
t
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user