510 lines
20 KiB
Rust
510 lines
20 KiB
Rust
//! Shape trees: `p:sp`, `p:pic`, `p:grpSp`, `p:cxnSp`, `p:graphicFrame` (tables, charts, SmartArt,
|
|
//! OLE and other objects).
|
|
|
|
use std::collections::{HashMap, HashSet};
|
|
|
|
use deckcraft_color::SchemeSlot;
|
|
use deckcraft_geom::{Xfrm, preset};
|
|
use deckcraft_model::style::{ColorRef, Effects, Fill};
|
|
use deckcraft_model::table::{Cell, Row, Table};
|
|
use deckcraft_model::text::{Anchor, TextDir};
|
|
use deckcraft_model::{Geom, MediaClip, PhType, Placeholder, Shape, ShapeId, ShapeKind, ShapeStyle};
|
|
|
|
use super::{Imp, Part, dml};
|
|
use crate::xml::El;
|
|
|
|
/// File shape ids at or above this are reassigned.
|
|
pub const MAX_KEPT_ID: u32 = 1 << 30;
|
|
const MAX_TREE_DEPTH: usize = 32;
|
|
const MAX_SHAPES: usize = 20_000;
|
|
|
|
/// Shape id bookkeeping for one part.
|
|
#[derive(Default)]
|
|
pub struct IdCtx {
|
|
pub used: HashSet<u32>,
|
|
/// File id → model id.
|
|
pub map: HashMap<u32, ShapeId>,
|
|
/// Always allocate new ids (shapes pulled in from other parts).
|
|
pub fresh: bool,
|
|
pub count: usize,
|
|
}
|
|
|
|
impl IdCtx {
|
|
fn assign(&mut self, imp: &mut Imp, file_id: Option<u32>) -> ShapeId {
|
|
if !self.fresh
|
|
&& let Some(i) = file_id
|
|
&& (2..MAX_KEPT_ID).contains(&i)
|
|
&& !self.used.contains(&i)
|
|
{
|
|
self.used.insert(i);
|
|
self.map.insert(i, ShapeId(i));
|
|
return ShapeId(i);
|
|
}
|
|
let n = imp.alloc();
|
|
self.used.insert(n);
|
|
if let Some(i) = file_id {
|
|
self.map.entry(i).or_insert(ShapeId(n));
|
|
}
|
|
ShapeId(n)
|
|
}
|
|
pub fn lookup(&self, file_id: u32) -> Option<ShapeId> {
|
|
self.map.get(&file_id).copied()
|
|
}
|
|
}
|
|
|
|
/// Read the children of an `spTree` / `grpSp`.
|
|
pub fn sp_tree(imp: &mut Imp, part: &Part, ctx: &mut IdCtx, tree: &El, depth: usize) -> Vec<Shape> {
|
|
let mut out = vec![];
|
|
if depth > MAX_TREE_DEPTH {
|
|
log::warn!("pptx: group nesting too deep in {}", part.name);
|
|
return out;
|
|
}
|
|
for e in tree.elements() {
|
|
if ctx.count >= MAX_SHAPES {
|
|
log::warn!("pptx: too many shapes in {}", part.name);
|
|
break;
|
|
}
|
|
let s = match e.local() {
|
|
"sp" => Some(sp(imp, part, ctx, e)),
|
|
"pic" => Some(pic(imp, part, ctx, e)),
|
|
"grpSp" => Some(grp(imp, part, ctx, e, depth)),
|
|
"cxnSp" => Some(cxn(imp, part, ctx, e)),
|
|
"graphicFrame" => Some(frame(imp, part, ctx, e, depth)),
|
|
"contentPart" => {
|
|
log::warn!("pptx: ink content part in {} not imported", part.name);
|
|
None
|
|
}
|
|
_ => None,
|
|
};
|
|
if let Some(s) = s {
|
|
ctx.count += 1;
|
|
out.push(s);
|
|
}
|
|
}
|
|
if depth == 0 {
|
|
remap_connectors(&mut out, ctx, 0);
|
|
}
|
|
out
|
|
}
|
|
|
|
/// ActiveX controls (`p:controls`): each control's picture (`p:pic`), the image PowerPoint shows
|
|
/// for it, as a picture above the slide's shapes. Controls without one are left out.
|
|
pub fn controls(imp: &mut Imp, part: &Part, ctx: &mut IdCtx, controls: &El) -> Vec<Shape> {
|
|
let mut out = vec![];
|
|
for c in controls.children_named("control") {
|
|
if ctx.count >= MAX_SHAPES {
|
|
break;
|
|
}
|
|
if let Some(p) = c.child("pic") {
|
|
ctx.count += 1;
|
|
out.push(pic(imp, part, ctx, p));
|
|
}
|
|
}
|
|
out
|
|
}
|
|
|
|
fn remap_connectors(shapes: &mut [Shape], ctx: &IdCtx, depth: usize) {
|
|
if depth > MAX_TREE_DEPTH {
|
|
return;
|
|
}
|
|
for s in shapes.iter_mut() {
|
|
match &mut s.kind {
|
|
ShapeKind::Connector { start, end } => {
|
|
for e in [start, end] {
|
|
*e = e.and_then(|(id, site)| ctx.lookup(id.0).map(|m| (m, site)));
|
|
}
|
|
}
|
|
ShapeKind::Group { children, .. } => remap_connectors(children, ctx, depth + 1),
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// PowerPoint's Lock (right-click > Lock, the Selection pane padlock) writes a full set of locking
|
|
/// attributes; it reads an object as locked only when the whole set for its kind is there, so a
|
|
/// lone `noMove` or `noResize` (other tools' partial locks) isn't Lock.
|
|
fn is_locked(l: &El) -> bool {
|
|
let set: &[&str] = match l.local() {
|
|
"grpSpLocks" => &["noGrp", "noUngrp", "noRot", "noMove", "noResize"],
|
|
"graphicFrameLocks" => &["noGrp", "noDrilldown", "noMove", "noResize"],
|
|
_ => &["noGrp", "noRot", "noMove", "noResize", "noEditPoints", "noAdjustHandles", "noChangeArrowheads", "noChangeShapeType"],
|
|
};
|
|
set.iter().all(|a| l.bool(a) == Some(true))
|
|
}
|
|
|
|
/// Common non-visual properties (`p:nvSpPr`, `p:nvPicPr`…).
|
|
fn base(imp: &mut Imp, part: &Part, ctx: &mut IdCtx, nv: Option<&El>) -> Shape {
|
|
let c = nv.and_then(|n| n.child("cNvPr"));
|
|
let mut s = Shape { id: ctx.assign(imp, c.and_then(|c| c.u32("id"))), ..Default::default() };
|
|
if let Some(c) = c {
|
|
s.name = c.attr("name").unwrap_or("").to_string();
|
|
s.descr = c.attr("descr").unwrap_or("").to_string();
|
|
s.title = c.attr("title").unwrap_or("").to_string();
|
|
s.hidden = c.bool("hidden").unwrap_or(false);
|
|
s.click = c.child("hlinkClick").and_then(|h| dml::hyperlink(imp, part, h));
|
|
s.hover = c.child("hlinkHover").and_then(|h| dml::hyperlink(imp, part, h));
|
|
s.decorative = c.find("decorative").and_then(|d| d.bool("val")).unwrap_or(false);
|
|
}
|
|
if let Some(nv) = nv {
|
|
for e in nv.elements() {
|
|
match e.local() {
|
|
"cNvSpPr" => {
|
|
s.text_box = e.bool("txBox").unwrap_or(false);
|
|
s.locked = e.child("spLocks").is_some_and(is_locked);
|
|
}
|
|
"cNvPicPr" | "cNvGrpSpPr" | "cNvCxnSpPr" | "cNvGraphicFramePr" => {
|
|
s.locked = e.elements().find(|l| l.local().ends_with("Locks")).is_some_and(is_locked);
|
|
}
|
|
"nvPr" => {
|
|
if let Some(ph) = e.child("ph") {
|
|
s.ph = Some(Placeholder {
|
|
kind: ph.attr("type").map(PhType::from_xml).unwrap_or(PhType::Obj),
|
|
idx: ph.u32("idx").unwrap_or(0),
|
|
vertical: ph.attr("orient") == Some("vert"),
|
|
size: ph.attr("sz").map(String::from),
|
|
has_custom_prompt: ph.bool("hasCustomPrompt").unwrap_or(false),
|
|
});
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
s
|
|
}
|
|
|
|
fn preset_geom(pg: &El) -> Geom {
|
|
let name = pg.attr("prst").unwrap_or("rect").to_string();
|
|
let defaults: Vec<f64> = preset::info(&name).map(|i| i.defaults.to_vec()).unwrap_or_default();
|
|
let mut adj = defaults;
|
|
let mut any = false;
|
|
if let Some(av) = pg.child("avLst") {
|
|
for gd in av.children_named("gd").take(16) {
|
|
let n = gd.attr("name").unwrap_or("");
|
|
let idx = if n == "adj" {
|
|
0
|
|
} else if let Some(k) = n.strip_prefix("adj").and_then(|k| k.parse::<usize>().ok()).filter(|k| (1..=16).contains(k)) {
|
|
k - 1
|
|
} else {
|
|
continue;
|
|
};
|
|
let Some(v) =
|
|
gd.attr("fmla").and_then(|f| f.trim().strip_prefix("val")).and_then(|v| v.trim().parse::<f64>().ok()).filter(|v| v.is_finite())
|
|
else {
|
|
continue;
|
|
};
|
|
if adj.len() <= idx {
|
|
adj.resize(idx + 1, 0.0);
|
|
}
|
|
if let Some(slot) = adj.get_mut(idx) {
|
|
*slot = v;
|
|
any = true;
|
|
}
|
|
}
|
|
}
|
|
if !any {
|
|
adj.clear();
|
|
}
|
|
Geom::Preset { name, adj }
|
|
}
|
|
|
|
/// A placeholder whose `spPr` names no geometry takes its layout's (or master's) placeholder's:
|
|
/// marked here with an empty custom geometry, filled in once the layouts are read
|
|
/// (`inherit_placeholder_geometry`).
|
|
fn mark_inherited_geometry(s: &mut Shape, pr: Option<&El>) {
|
|
if s.ph.is_some() && pr.is_none_or(|pr| pr.child("prstGeom").is_none() && pr.child("custGeom").is_none()) {
|
|
s.geom = Geom::Custom { paths: vec![] };
|
|
}
|
|
}
|
|
|
|
/// `spPr` → transform, geometry, fill, line, effects.
|
|
fn sp_pr(imp: &mut Imp, part: &Part, s: &mut Shape, pr: &El) {
|
|
if let Some(x) = pr.child("xfrm") {
|
|
s.xfrm = Some(dml::xfrm(x).0);
|
|
}
|
|
if let Some(pg) = pr.child("prstGeom") {
|
|
s.geom = preset_geom(pg);
|
|
} else if let Some(cg) = pr.child("custGeom") {
|
|
let (w, h) = s.xfrm.map(|x| (deckcraft_geom::pt_to_emu(x.w) as f64, deckcraft_geom::pt_to_emu(x.h) as f64)).unwrap_or((21600.0, 21600.0));
|
|
let paths = crate::custgeom::read_cust_geom(cg, w, h);
|
|
if !paths.is_empty() {
|
|
s.geom = Geom::Custom { paths };
|
|
}
|
|
}
|
|
s.fill = dml::fill_of(imp, part, pr);
|
|
if let Some(ln) = pr.child("ln") {
|
|
s.line = Some(dml::line(imp, part, ln));
|
|
}
|
|
if let Some(fx) = pr.child("effectLst") {
|
|
s.effects = Some(dml::effects(fx));
|
|
}
|
|
let raw: String = pr.elements().filter(|c| c.is("scene3d") || c.is("sp3d")).map(|c| part.keep(c)).collect();
|
|
if !raw.is_empty() {
|
|
let fx = s.effects.get_or_insert_with(Effects::default);
|
|
fx.bevel = pr.path(&["sp3d", "bevelT"]).map(|b| b.attr("prst").unwrap_or("circle").to_string());
|
|
fx.raw3d = Some(raw);
|
|
}
|
|
}
|
|
|
|
fn style(st: &El) -> ShapeStyle {
|
|
let r = |n: &str| {
|
|
st.child(n)
|
|
.map(|e| (e.u32("idx").unwrap_or(0).min(1003), dml::color(e).unwrap_or(ColorRef::scheme(SchemeSlot::Accent1))))
|
|
.unwrap_or((0, ColorRef::scheme(SchemeSlot::Accent1)))
|
|
};
|
|
let font = st.child("fontRef");
|
|
ShapeStyle {
|
|
line_ref: r("lnRef"),
|
|
fill_ref: r("fillRef"),
|
|
effect_ref: r("effectRef"),
|
|
font_ref: (font.and_then(|f| f.attr("idx")).unwrap_or("minor").to_string(), font.and_then(dml::color)),
|
|
}
|
|
}
|
|
|
|
fn sp(imp: &mut Imp, part: &Part, ctx: &mut IdCtx, e: &El) -> Shape {
|
|
let mut s = base(imp, part, ctx, e.child("nvSpPr"));
|
|
if let Some(pr) = e.child("spPr") {
|
|
sp_pr(imp, part, &mut s, pr);
|
|
}
|
|
mark_inherited_geometry(&mut s, e.child("spPr"));
|
|
if e.bool("useBgFill") == Some(true) {
|
|
s.fill = Some(Fill::Background);
|
|
}
|
|
s.style = e.child("style").map(style);
|
|
s.text = e.child("txBody").map(|t| dml::text_body(imp, part, t));
|
|
s
|
|
}
|
|
|
|
fn ms(v: Option<&str>) -> u32 {
|
|
v.and_then(|v| v.trim().parse::<f64>().ok()).filter(|v| v.is_finite()).map(|v| v.clamp(0.0, u32::MAX as f64) as u32).unwrap_or(0)
|
|
}
|
|
|
|
fn pic(imp: &mut Imp, part: &Part, ctx: &mut IdCtx, e: &El) -> Shape {
|
|
let mut s = base(imp, part, ctx, e.child("nvPicPr"));
|
|
if let Some(pr) = e.child("spPr") {
|
|
sp_pr(imp, part, &mut s, pr);
|
|
}
|
|
s.style = e.child("style").map(style);
|
|
let pf = e.child("blipFill").and_then(|b| dml::blip_fill(imp, part, b));
|
|
let nvpr = e.path(&["nvPicPr", "nvPr"]);
|
|
let vf = nvpr.and_then(|n| n.child("videoFile"));
|
|
let af = nvpr.and_then(|n| n.child("audioFile"));
|
|
let p14 = nvpr.and_then(|n| n.child("extLst")).and_then(|x| x.find("media"));
|
|
if vf.is_some() || af.is_some() || p14.is_some() {
|
|
let media = p14.and_then(|m| dml::media_ref(imp, part, m)).or_else(|| vf.or(af).and_then(|f| dml::media_ref(imp, part, f)));
|
|
if let Some(media) = media {
|
|
let mut clip =
|
|
MediaClip { media, video: vf.is_some() || af.is_none(), poster: pf.as_ref().map(|p| p.media), volume: 1.0, ..Default::default() };
|
|
if let Some(m) = p14 {
|
|
if let Some(t) = m.child("trim") {
|
|
clip.trim_start_ms = ms(t.attr("st"));
|
|
clip.trim_end_ms = ms(t.attr("end"));
|
|
}
|
|
if let Some(f) = m.child("fade") {
|
|
clip.fade_in_ms = ms(f.attr("in"));
|
|
clip.fade_out_ms = ms(f.attr("out"));
|
|
}
|
|
if let Some(l) = m.child("bmkLst") {
|
|
clip.bookmarks =
|
|
l.children_named("bmk").take(1000).map(|b| (b.attr("name").unwrap_or("").to_string(), ms(b.attr("time")))).collect();
|
|
}
|
|
}
|
|
s.kind = ShapeKind::Media(clip);
|
|
return s;
|
|
}
|
|
}
|
|
match pf {
|
|
Some(fill) => s.kind = ShapeKind::Picture { fill },
|
|
None => log::warn!("pptx: picture {} in {} has no readable image", s.name, part.name),
|
|
}
|
|
s
|
|
}
|
|
|
|
fn grp(imp: &mut Imp, part: &Part, ctx: &mut IdCtx, e: &El, depth: usize) -> Shape {
|
|
let mut s = base(imp, part, ctx, e.child("nvGrpSpPr"));
|
|
let mut child = None;
|
|
if let Some(pr) = e.child("grpSpPr") {
|
|
if let Some(x) = pr.child("xfrm") {
|
|
let (xf, ch) = dml::xfrm(x);
|
|
s.xfrm = Some(xf);
|
|
child = ch;
|
|
}
|
|
s.fill = dml::fill_of(imp, part, pr);
|
|
if let Some(fx) = pr.child("effectLst") {
|
|
s.effects = Some(dml::effects(fx));
|
|
}
|
|
}
|
|
let children = sp_tree(imp, part, ctx, e, depth + 1);
|
|
let child = child.or_else(|| s.xfrm.map(|x| Xfrm::new(x.x, x.y, x.w, x.h))).unwrap_or_default();
|
|
s.kind = ShapeKind::Group { children, child };
|
|
s
|
|
}
|
|
|
|
fn cxn(imp: &mut Imp, part: &Part, ctx: &mut IdCtx, e: &El) -> Shape {
|
|
let nv = e.child("nvCxnSpPr");
|
|
let mut s = base(imp, part, ctx, nv);
|
|
if let Some(pr) = e.child("spPr") {
|
|
sp_pr(imp, part, &mut s, pr);
|
|
}
|
|
s.style = e.child("style").map(style);
|
|
let c = nv.and_then(|n| n.child("cNvCxnSpPr"));
|
|
let end = |n: &str| c.and_then(|c| c.child(n)).and_then(|x| Some((ShapeId(x.u32("id")?), x.u32("idx").unwrap_or(0))));
|
|
s.kind = ShapeKind::Connector { start: end("stCxn"), end: end("endCxn") };
|
|
s
|
|
}
|
|
|
|
fn frame(imp: &mut Imp, part: &Part, ctx: &mut IdCtx, e: &El, depth: usize) -> Shape {
|
|
let mut s = base(imp, part, ctx, e.child("nvGraphicFramePr"));
|
|
if let Some(x) = e.child("xfrm") {
|
|
s.xfrm = Some(dml::xfrm(x).0);
|
|
}
|
|
let gd = e.path(&["graphic", "graphicData"]);
|
|
let uri = gd.and_then(|g| g.attr("uri")).unwrap_or("");
|
|
if let Some(gd) = gd {
|
|
if let Some(tbl) = gd.child("tbl") {
|
|
s.kind = ShapeKind::Table(table(imp, part, tbl));
|
|
return s;
|
|
}
|
|
if let Some(c) = gd.child("chart")
|
|
&& let Some(rel) = part.rels.get(c.attr("r:id").unwrap_or(""))
|
|
&& let Some(bytes) = imp.pkg.get(&rel.target)
|
|
&& let Some(chart) = super::read_chart_xml(bytes)
|
|
{
|
|
s.kind = ShapeKind::Chart(Box::new(chart));
|
|
return s;
|
|
}
|
|
if uri.ends_with("/diagram")
|
|
&& let Some(g) = smartart(imp, part, gd, &s, depth)
|
|
{
|
|
return g;
|
|
}
|
|
}
|
|
// Something we don't model: keep its XML and show a preview picture when it has one.
|
|
let preview = {
|
|
let mut pics = vec![];
|
|
e.find_all("blip", &mut pics);
|
|
pics.into_iter().find_map(|b| dml::media_ref(imp, part, b))
|
|
}
|
|
.or_else(|| gd.and_then(|g| g.find("oleObj")).and_then(|o| vml_preview(imp, part, o.attr("spid")?)));
|
|
let label = if let Some(o) = gd.and_then(|g| g.find("oleObj")) {
|
|
o.attr("progId").or(o.attr("name")).unwrap_or("Object").to_string()
|
|
} else if uri.ends_with("/diagram") {
|
|
"SmartArt".to_string()
|
|
} else if uri.contains("chart") {
|
|
"Chart".to_string()
|
|
} else {
|
|
uri.rsplit('/').next().unwrap_or("Object").to_string()
|
|
};
|
|
s.kind = ShapeKind::Opaque { xml: part.keep(e), preview, label };
|
|
s
|
|
}
|
|
|
|
/// The preview picture of an old-style embedded object (`p:oleObj spid` with no `p:pic`): the
|
|
/// `v:imagedata` of the legacy VML drawing shape with that id, as PowerPoint 2007 wrote them.
|
|
fn vml_preview(imp: &mut Imp, part: &Part, spid: &str) -> Option<deckcraft_model::MediaId> {
|
|
for rel in part.rels.items.iter().filter(|r| r.kind == "vmlDrawing" && !r.external) {
|
|
let Some(doc) = imp.pkg.xml(&rel.target) else { continue };
|
|
let mut shapes = vec![];
|
|
doc.root.find_all("shape", &mut shapes);
|
|
let Some(img) = shapes.into_iter().find(|s| s.attr("id") == Some(spid)).and_then(|s| s.find("imagedata")) else { continue };
|
|
let Some(id) = img.attr("o:relid").or(img.attr("r:id")) else { continue };
|
|
let target = imp.pkg.rels(&rel.target).get(id).filter(|r| !r.external).map(|r| r.target.clone())?;
|
|
return imp.media_for(&target);
|
|
}
|
|
None
|
|
}
|
|
|
|
/// 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> {
|
|
let rel_ids = gd.child("relIds")?;
|
|
let dm = part.rels.get(rel_ids.attr("r:dm").unwrap_or(""))?;
|
|
let data = imp.pkg.xml(&dm.target)?;
|
|
let drawing_rel = data.root.find("dataModelExt").and_then(|x| x.attr("relId")).and_then(|id| part.rels.get(id)).map(|r| r.target.clone())?;
|
|
let mut d = imp.pkg.xml(&drawing_rel)?;
|
|
super::resolve_mc(&mut d.root, 0);
|
|
let dpart = Part::new(imp.pkg, &drawing_rel, &d);
|
|
let tree = d.root.child("spTree")?;
|
|
let mut sub = IdCtx { fresh: true, ..Default::default() };
|
|
let children = sp_tree(imp, &dpart, &mut sub, tree, depth + 1);
|
|
if children.is_empty() {
|
|
return None;
|
|
}
|
|
let mut g = frame.clone();
|
|
let x = frame.xfrm.unwrap_or_default();
|
|
g.kind = ShapeKind::Group { children, child: Xfrm::new(0.0, 0.0, x.w, x.h) };
|
|
Some(g)
|
|
}
|
|
|
|
fn table(imp: &mut Imp, part: &Part, tbl: &El) -> Table {
|
|
let mut t = Table { style: "none".into(), first_row: false, band_row: false, ..Default::default() };
|
|
if let Some(pr) = tbl.child("tblPr") {
|
|
t.background = pr.elements().find(|e| e.local().ends_with("Fill") && e.local() != "noFill").and_then(|f| dml::fill_el(imp, part, f));
|
|
t.first_row = pr.bool("firstRow").unwrap_or(false);
|
|
t.first_col = pr.bool("firstCol").unwrap_or(false);
|
|
t.last_row = pr.bool("lastRow").unwrap_or(false);
|
|
t.last_col = pr.bool("lastCol").unwrap_or(false);
|
|
t.band_row = pr.bool("bandRow").unwrap_or(false);
|
|
t.band_col = pr.bool("bandCol").unwrap_or(false);
|
|
if let Some(id) = pr.child("tableStyleId").map(|e| e.text()) {
|
|
// A style the file defines is drawn from its definition (by its GUID).
|
|
// DeckCraft's own styles (written with GUIDs derived from their ids) map back to them.
|
|
let up = id.trim().to_ascii_uppercase();
|
|
t.style =
|
|
if imp.table_style_ids.contains(&up) && !crate::tables::is_own_guid(&up) { up } else { crate::tables::style_from_guid(id.trim()) };
|
|
}
|
|
}
|
|
t.cols = tbl
|
|
.child("tblGrid")
|
|
.map(|g| g.children_named("gridCol").take(1000).map(|c| dml::pt(c, "w").unwrap_or(72.0).clamp(0.0, 20_000.0)).collect())
|
|
.unwrap_or_default();
|
|
for tr in tbl.children_named("tr").take(1000) {
|
|
let mut row = Row { height: dml::pt(tr, "h").unwrap_or(0.0).clamp(0.0, 20_000.0), cells: vec![] };
|
|
for tc in tr.children_named("tc").take(1000) {
|
|
row.cells.push(cell(imp, part, tc));
|
|
}
|
|
t.rows.push(row);
|
|
}
|
|
if t.cols.is_empty() {
|
|
let n = t.rows.iter().map(|r| r.cells.len()).max().unwrap_or(0);
|
|
t.cols = vec![72.0; n];
|
|
}
|
|
t.normalize();
|
|
t
|
|
}
|
|
|
|
fn cell(imp: &mut Imp, part: &Part, tc: &El) -> Cell {
|
|
let mut c = Cell {
|
|
grid_span: tc.u32("gridSpan").unwrap_or(1).clamp(1, 1000),
|
|
row_span: tc.u32("rowSpan").unwrap_or(1).clamp(1, 1000),
|
|
h_merge: tc.bool("hMerge").unwrap_or(false),
|
|
v_merge: tc.bool("vMerge").unwrap_or(false),
|
|
..Default::default()
|
|
};
|
|
if let Some(tx) = tc.child("txBody") {
|
|
c.text = dml::text_body(imp, part, tx);
|
|
}
|
|
if let Some(pr) = tc.child("tcPr") {
|
|
if ["marL", "marR", "marT", "marB"].iter().any(|n| pr.has_attr(n)) {
|
|
c.margins = Some([
|
|
dml::pt(pr, "marL").unwrap_or(7.2),
|
|
dml::pt(pr, "marR").unwrap_or(7.2),
|
|
dml::pt(pr, "marT").unwrap_or(3.6),
|
|
dml::pt(pr, "marB").unwrap_or(3.6),
|
|
]);
|
|
}
|
|
c.anchor = pr.attr("anchor").and_then(Anchor::from_xml);
|
|
c.vertical = pr.attr("vert").and_then(TextDir::from_xml);
|
|
for (i, n) in ["lnL", "lnR", "lnT", "lnB"].iter().enumerate() {
|
|
if let Some(ln) = pr.child(n)
|
|
&& let Some(b) = c.borders.get_mut(i)
|
|
{
|
|
*b = Some(dml::line(imp, part, ln));
|
|
}
|
|
}
|
|
c.diag_down = pr.child("lnTlToBr").map(|l| dml::line(imp, part, l));
|
|
c.diag_up = pr.child("lnBlToTr").map(|l| dml::line(imp, part, l));
|
|
c.fill = dml::fill_of(imp, part, pr);
|
|
}
|
|
c
|
|
}
|