use super::package::{
load_chart_data, load_slide_images, load_smartart_data, parse_rels_xml, rels_path_for,
resolve_layout_master_paths, resolve_relative_path, scan_chart_refs,
};
use super::placeholders::PlaceholderGeometryMap;
use super::*;
struct SlideInheritanceChain {
slide_xml: String,
slide_color_map: ColorMapData,
layout_path: Option<String>,
layout_xml: Option<String>,
layout_color_map: Option<ColorMapData>,
master_path: Option<String>,
master_xml: Option<String>,
master_color_map: ColorMapData,
master_text_styles: PptxMasterTextStyles,
}
fn resolve_inheritance_chain<R: Read + std::io::Seek>(
slide_path: &str,
theme: &ThemeData,
archive: &mut ZipArchive<R>,
) -> Result<SlideInheritanceChain, ConvertError> {
let slide_xml: String = read_zip_entry(archive, slide_path)?;
let (layout_path, master_path) = resolve_layout_master_paths(slide_path, archive);
let master_xml: Option<String> = master_path
.as_ref()
.and_then(|path| read_zip_entry(archive, path).ok());
let layout_xml: Option<String> = layout_path
.as_ref()
.and_then(|path| read_zip_entry(archive, path).ok());
let master_color_map: ColorMapData = master_xml
.as_deref()
.map(parse_master_color_map)
.unwrap_or_else(default_color_map);
let master_text_styles: PptxMasterTextStyles = master_xml
.as_deref()
.map(|xml| parse_master_text_styles(xml, theme, &master_color_map))
.unwrap_or_default();
let slide_color_map: ColorMapData = resolve_effective_color_map(&slide_xml, &master_color_map);
let layout_color_map: Option<ColorMapData> = layout_xml
.as_deref()
.map(|xml| resolve_effective_color_map(xml, &master_color_map));
Ok(SlideInheritanceChain {
slide_xml,
slide_color_map,
layout_path,
layout_xml,
layout_color_map,
master_path,
master_xml,
master_color_map,
master_text_styles,
})
}
fn parse_layer_elements<R: Read + std::io::Seek>(
layer_path: &str,
layer_xml: &str,
color_map: &ColorMapData,
theme: &ThemeData,
label: &str,
text_style_defaults: &PptxTextBodyStyleDefaults,
archive: &mut ZipArchive<R>,
) -> (Vec<FixedElement>, Vec<ConvertWarning>) {
let images: SlideImageMap = load_slide_images(layer_path, archive);
let empty_table_styles: table_styles::TableStyleMap = table_styles::TableStyleMap::new();
parse_slide_xml_inner(
layer_xml,
&images,
theme,
color_map,
label,
text_style_defaults,
&empty_table_styles,
true, None,
)
.unwrap_or_default()
}
fn collect_smartart_elements<R: Read + std::io::Seek>(
slide_xml: &str,
slide_path: &str,
archive: &mut ZipArchive<R>,
theme: &ThemeData,
color_map: &ColorMapData,
) -> Vec<FixedElement> {
let smartart_refs = smartart::scan_smartart_refs(slide_xml);
if smartart_refs.is_empty() {
return Vec::new();
}
let smartart_data = load_smartart_data(slide_path, archive);
let mut elements: Vec<FixedElement> = Vec::new();
for sa_ref in &smartart_refs {
let drawing_elems: Vec<FixedElement> =
load_smartart_drawing_xml(slide_path, archive, &sa_ref.data_rid)
.map(|xml| {
parse_smartart_drawing(
&xml,
theme,
color_map,
emu_to_pt(sa_ref.x),
emu_to_pt(sa_ref.y),
)
})
.unwrap_or_default();
if !drawing_elems.is_empty() {
elements.extend(drawing_elems);
} else if let Some(items) = smartart_data.get(&sa_ref.data_rid) {
elements.push(FixedElement {
x: emu_to_pt(sa_ref.x),
y: emu_to_pt(sa_ref.y),
width: emu_to_pt(sa_ref.cx),
height: emu_to_pt(sa_ref.cy),
kind: FixedElementKind::SmartArt(SmartArt {
items: items.clone(),
}),
});
}
}
elements
}
fn load_smartart_drawing_xml<R: Read + std::io::Seek>(
slide_path: &str,
archive: &mut ZipArchive<R>,
data_rid: &str,
) -> Option<String> {
let rels_xml: String = read_zip_entry(archive, &rels_path_for(slide_path)).ok()?;
let rels: HashMap<String, String> = parse_rels_xml(&rels_xml);
let slide_dir: &str = slide_path
.rsplit_once('/')
.map(|(dir, _)| dir)
.unwrap_or("");
let data_target: &str = rels.get(data_rid)?;
let data_path: String = match data_target.strip_prefix('/') {
Some(stripped) => stripped.to_string(),
None => resolve_relative_path(slide_dir, data_target),
};
let data_xml: String = read_zip_entry(archive, &data_path).ok()?;
let drawing_rid: String = extract_data_model_ext_rel_id(&data_xml)?;
let drawing_target: &str = rels.get(&drawing_rid)?;
let drawing_path: String = match drawing_target.strip_prefix('/') {
Some(stripped) => stripped.to_string(),
None => resolve_relative_path(slide_dir, drawing_target),
};
read_zip_entry(archive, &drawing_path).ok()
}
fn extract_data_model_ext_rel_id(data_xml: &str) -> Option<String> {
let mut reader = Reader::from_str(data_xml);
loop {
match reader.read_event() {
Ok(Event::Empty(ref e)) | Ok(Event::Start(ref e))
if e.local_name().as_ref() == b"dataModelExt" =>
{
return e.attributes().flatten().find_map(|attr| {
(attr.key.local_name().as_ref() == b"relId")
.then(|| attr.unescape_value().ok())
.flatten()
.map(|v| v.to_string())
});
}
Ok(Event::Eof) | Err(_) => return None,
_ => {}
}
}
}
fn parse_smartart_drawing(
drawing_xml: &str,
theme: &ThemeData,
color_map: &ColorMapData,
frame_x_pt: f64,
frame_y_pt: f64,
) -> Vec<FixedElement> {
let mut reader = Reader::from_str(drawing_xml);
let mut elements: Vec<FixedElement> = Vec::new();
#[derive(Default)]
struct DrawShape {
x: i64,
y: i64,
cx: i64,
cy: i64,
preset: Option<String>,
fill: Option<Color>,
line: Option<Color>,
line_w: i64,
texts: Vec<String>,
}
let mut current: Option<DrawShape> = None;
let mut in_sp_pr = false;
let mut in_ln = false;
let mut in_fill = false;
let mut in_tx_body = false;
let mut in_text = false;
loop {
match reader.read_event() {
Ok(Event::Start(ref e)) => match e.local_name().as_ref() {
b"sp" => current = Some(DrawShape::default()),
b"spPr" => in_sp_pr = true,
b"ln" if in_sp_pr => {
in_ln = true;
if let Some(shape) = current.as_mut() {
shape.line_w = get_attr_i64(e, b"w").unwrap_or(0);
}
}
b"solidFill" if in_sp_pr && !in_ln => in_fill = true,
b"txBody" => in_tx_body = true,
b"t" if in_tx_body => in_text = true,
b"srgbClr" | b"schemeClr" | b"sysClr" if in_fill => {
let parsed =
parse_color_from_start(reader_ref(&mut reader), e, theme, color_map);
if let (Some(shape), Some(color)) = (current.as_mut(), parsed.color) {
shape.fill = Some(color);
}
}
b"srgbClr" | b"schemeClr" | b"sysClr" if in_ln => {
let parsed =
parse_color_from_start(reader_ref(&mut reader), e, theme, color_map);
if let (Some(shape), Some(color)) = (current.as_mut(), parsed.color) {
shape.line = Some(color);
}
}
_ => {}
},
Ok(Event::Empty(ref e)) => match e.local_name().as_ref() {
b"off" if in_sp_pr => {
if let Some(shape) = current.as_mut() {
shape.x = get_attr_i64(e, b"x").unwrap_or(0);
shape.y = get_attr_i64(e, b"y").unwrap_or(0);
}
}
b"ext" if in_sp_pr => {
if let Some(shape) = current.as_mut() {
shape.cx = get_attr_i64(e, b"cx").unwrap_or(0);
shape.cy = get_attr_i64(e, b"cy").unwrap_or(0);
}
}
b"prstGeom" => {
if let Some(shape) = current.as_mut() {
shape.preset = get_attr_str(e, b"prst");
}
}
b"srgbClr" | b"schemeClr" | b"sysClr" if in_fill => {
let parsed = parse_color_from_empty(e, theme, color_map);
if let (Some(shape), Some(color)) = (current.as_mut(), parsed.color) {
shape.fill = Some(color);
}
}
b"srgbClr" | b"schemeClr" | b"sysClr" if in_ln => {
let parsed = parse_color_from_empty(e, theme, color_map);
if let (Some(shape), Some(color)) = (current.as_mut(), parsed.color) {
shape.line = Some(color);
}
}
_ => {}
},
Ok(Event::Text(ref t)) => {
if in_text
&& let Some(text) = decode_pptx_text_event(t)
&& let Some(shape) = current.as_mut()
{
let trimmed = text.trim();
if !trimmed.is_empty() {
shape.texts.push(trimmed.to_string());
}
}
}
Ok(Event::End(ref e)) => match e.local_name().as_ref() {
b"spPr" => in_sp_pr = false,
b"ln" => in_ln = false,
b"solidFill" => in_fill = false,
b"txBody" => in_tx_body = false,
b"t" => in_text = false,
b"sp" => {
if let Some(shape) = current.take()
&& shape.cx > 0
&& shape.cy > 0
{
elements.extend(smartart_shape_to_elements(shape_fields(
&shape.preset,
shape.fill,
shape.line,
shape.line_w,
&shape.texts,
frame_x_pt + emu_to_pt(shape.x),
frame_y_pt + emu_to_pt(shape.y),
emu_to_pt(shape.cx),
emu_to_pt(shape.cy),
)));
}
}
_ => {}
},
Ok(Event::Eof) | Err(_) => break,
_ => {}
}
}
elements
}
#[allow(clippy::too_many_arguments)]
fn shape_fields(
preset: &Option<String>,
fill: Option<Color>,
line: Option<Color>,
line_w: i64,
texts: &[String],
x: f64,
y: f64,
width: f64,
height: f64,
) -> SmartArtShapeFields {
SmartArtShapeFields {
preset: preset.clone(),
fill,
line,
line_w,
texts: texts.to_vec(),
x,
y,
width,
height,
}
}
struct SmartArtShapeFields {
preset: Option<String>,
fill: Option<Color>,
line: Option<Color>,
line_w: i64,
texts: Vec<String>,
x: f64,
y: f64,
width: f64,
height: f64,
}
fn smartart_shape_to_elements(f: SmartArtShapeFields) -> Vec<FixedElement> {
let mut out: Vec<FixedElement> = Vec::new();
let kind: ShapeKind = f
.preset
.as_deref()
.map(|prst| {
prst_to_shape_kind(
prst,
f.width,
f.height,
false,
false,
ArrowHead::None,
ArrowHead::None,
&[],
)
})
.unwrap_or(ShapeKind::Rectangle);
let stroke: Option<BorderSide> = f.line.map(|color| BorderSide {
width: emu_to_pt(f.line_w.max(0)),
color,
style: BorderLineStyle::Solid,
});
out.push(FixedElement {
x: f.x,
y: f.y,
width: f.width,
height: f.height,
kind: FixedElementKind::Shape(Shape {
kind,
fill: f.fill,
gradient_fill: None,
stroke,
rotation_deg: None,
opacity: None,
shadow: None,
}),
});
if !f.texts.is_empty() {
let runs: Vec<Run> = f
.texts
.iter()
.map(|text| Run {
text: text.clone(),
style: TextStyle {
color: Some(Color::new(0xFF, 0xFF, 0xFF)),
..TextStyle::default()
},
href: None,
footnote: None,
})
.collect();
out.push(FixedElement {
x: f.x,
y: f.y,
width: f.width,
height: f.height,
kind: FixedElementKind::TextBox(TextBoxData {
content: vec![Block::Paragraph(Paragraph {
style: ParagraphStyle {
alignment: Some(Alignment::Center),
..ParagraphStyle::default()
},
runs,
})],
padding: Insets::default(),
vertical_align: TextBoxVerticalAlign::Center,
fill: None,
opacity: None,
stroke: None,
shape_kind: None,
no_wrap: false,
auto_fit: false,
text_rotation_deg: None,
}),
});
}
out
}
fn reader_ref<'a, 'b>(reader: &'a mut Reader<&'b [u8]>) -> &'a mut Reader<&'b [u8]> {
reader
}
fn collect_chart_elements<R: Read + std::io::Seek>(
slide_xml: &str,
slide_path: &str,
archive: &mut ZipArchive<R>,
) -> Vec<FixedElement> {
let chart_refs = scan_chart_refs(slide_xml);
if chart_refs.is_empty() {
return Vec::new();
}
let chart_data = load_chart_data(slide_path, archive);
chart_refs
.iter()
.filter_map(|c_ref| {
chart_data.get(&c_ref.chart_rid).map(|chart| FixedElement {
x: emu_to_pt(c_ref.x),
y: emu_to_pt(c_ref.y),
width: emu_to_pt(c_ref.cx),
height: emu_to_pt(c_ref.cy),
kind: FixedElementKind::Chart(chart.clone()),
})
})
.collect()
}
struct ResolvedBackground {
color: Option<Color>,
gradient: Option<GradientFill>,
image: Option<(String, String)>,
}
fn resolve_slide_background(
chain: &SlideInheritanceChain,
slide_path: &str,
theme: &ThemeData,
) -> ResolvedBackground {
let layers: [(Option<&str>, &str, &ColorMapData); 3] = [
(
Some(chain.slide_xml.as_str()),
slide_path,
&chain.slide_color_map,
),
(
chain.layout_xml.as_deref(),
chain.layout_path.as_deref().unwrap_or(""),
chain
.layout_color_map
.as_ref()
.unwrap_or(&chain.master_color_map),
),
(
chain.master_xml.as_deref(),
chain.master_path.as_deref().unwrap_or(""),
&chain.master_color_map,
),
];
for (layer_xml, layer_path, color_map) in layers {
let Some(xml) = layer_xml else { continue };
if let Some(gradient) = parse_background_gradient(xml, theme, color_map) {
return ResolvedBackground {
color: gradient.stops.first().map(|s| s.color),
gradient: Some(gradient),
image: None,
};
}
if let Some(color) = parse_background_color(xml, theme, color_map) {
return ResolvedBackground {
color: Some(color),
gradient: None,
image: None,
};
}
if let Some(rid) = parse_background_image_rid(xml) {
return ResolvedBackground {
color: None,
gradient: None,
image: Some((layer_path.to_string(), rid)),
};
}
if let Some((color, gradient)) = parse_background_ref(xml, theme, color_map) {
return ResolvedBackground {
color,
gradient,
image: None,
};
}
}
ResolvedBackground {
color: None,
gradient: None,
image: None,
}
}
fn build_background_image_element<R: Read + std::io::Seek>(
layer_path: &str,
rid: &str,
slide_size: PageSize,
archive: &mut ZipArchive<R>,
) -> Option<FixedElement> {
let images: SlideImageMap = load_slide_images(layer_path, archive);
let asset = images.get(rid)?;
let format = asset.format()?;
Some(FixedElement {
x: 0.0,
y: 0.0,
width: slide_size.width,
height: slide_size.height,
kind: FixedElementKind::Image(ImageData {
data: asset.data.clone(),
format,
width: Some(slide_size.width),
height: Some(slide_size.height),
crop: None,
stroke: None,
alignment: None,
clip_shape: None,
shadow: None,
}),
})
}
fn is_hidden_slide(slide_xml: &str) -> bool {
let mut reader: Reader<&[u8]> = Reader::from_str(slide_xml);
loop {
match reader.read_event() {
Ok(Event::Start(ref e)) | Ok(Event::Empty(ref e)) => {
return e.local_name().as_ref() == b"sld"
&& get_attr_str(e, b"show").is_some_and(|v| v == "0" || v == "false");
}
Ok(Event::Eof) | Err(_) => return false,
_ => {}
}
}
}
pub(super) fn parse_single_slide<R: Read + std::io::Seek>(
slide_path: &str,
slide_label: &str,
slide_size: PageSize,
theme: &ThemeData,
table_styles: &table_styles::TableStyleMap,
archive: &mut ZipArchive<R>,
) -> Result<Option<(Page, Vec<ConvertWarning>)>, ConvertError> {
let chain: SlideInheritanceChain = resolve_inheritance_chain(slide_path, theme, archive)?;
if is_hidden_slide(&chain.slide_xml) {
tracing::debug!(slide = slide_label, "skipping hidden slide");
return Ok(None);
}
let slide_images: SlideImageMap = load_slide_images(slide_path, archive);
let mut warnings: Vec<ConvertWarning> = Vec::new();
let placeholder_geometry: PlaceholderGeometryMap = PlaceholderGeometryMap::build(
chain.layout_xml.as_deref(),
chain.master_xml.as_deref(),
theme,
chain
.layout_color_map
.as_ref()
.unwrap_or(&chain.master_color_map),
&chain.master_color_map,
chain.master_text_styles.clone(),
);
let (slide_elements, slide_warnings) = parse_slide_xml(
&chain.slide_xml,
&slide_images,
theme,
&chain.slide_color_map,
slide_label,
&chain.master_text_styles.other,
table_styles,
Some(&placeholder_geometry),
)?;
warnings.extend(slide_warnings);
let mut elements: Vec<FixedElement> = Vec::new();
if let Some(ref path) = chain.master_path
&& let Some(ref xml) = chain.master_xml
{
let master_label: String = format!("{slide_label} master");
let (master_elems, master_warnings) = parse_layer_elements(
path,
xml,
&chain.master_color_map,
theme,
&master_label,
&chain.master_text_styles.other,
archive,
);
elements.extend(master_elems);
warnings.extend(master_warnings);
}
if let Some(ref path) = chain.layout_path
&& let Some(ref xml) = chain.layout_xml
&& let Some(ref color_map) = chain.layout_color_map
{
let layout_label: String = format!("{slide_label} layout");
let (layout_elems, layout_warnings) = parse_layer_elements(
path,
xml,
color_map,
theme,
&layout_label,
&chain.master_text_styles.other,
archive,
);
elements.extend(layout_elems);
warnings.extend(layout_warnings);
}
elements.extend(slide_elements);
elements.extend(collect_smartart_elements(
&chain.slide_xml,
slide_path,
archive,
theme,
&chain.slide_color_map,
));
elements.extend(collect_chart_elements(
&chain.slide_xml,
slide_path,
archive,
));
let background: ResolvedBackground = resolve_slide_background(&chain, slide_path, theme);
if let Some((layer_path, rid)) = &background.image
&& let Some(element) = build_background_image_element(layer_path, rid, slide_size, archive)
{
elements.insert(0, element);
}
Ok(Some((
Page::Fixed(FixedPage {
size: slide_size,
elements,
background_color: background.color,
background_gradient: background.gradient,
}),
warnings,
)))
}
fn describe_assets(assets: impl IntoIterator<Item = String>) -> String {
assets.into_iter().collect::<Vec<_>>().join(", ")
}
fn pick_supported_asset(rid: &str, images: &SlideImageMap) -> Option<SlideImageAsset> {
images
.get(rid)
.filter(|asset| asset.is_supported())
.cloned()
}
fn select_picture_asset(
images: &SlideImageMap,
warning_context: &str,
base_rid: Option<&str>,
svg_rid: Option<&str>,
img_layer_rids: &[String],
) -> (Option<SlideImageAsset>, Vec<ConvertWarning>) {
let mut warnings = Vec::new();
let unsupported_layers: Vec<String> = img_layer_rids
.iter()
.filter_map(|rid| images.get(rid))
.filter(|asset| !asset.is_supported())
.map(|asset| asset.file_name().to_string())
.collect();
if !unsupported_layers.is_empty() {
warnings.push(ConvertWarning::PartialElement {
format: "PPTX".to_string(),
element: format!("{warning_context} picture"),
detail: format!(
"unsupported image layer omitted: {}",
describe_assets(unsupported_layers)
),
});
}
let selected = svg_rid
.and_then(|rid| pick_supported_asset(rid, images))
.or_else(|| base_rid.and_then(|rid| pick_supported_asset(rid, images)))
.or_else(|| {
img_layer_rids
.iter()
.find_map(|rid| pick_supported_asset(rid, images))
});
if selected.is_some() {
return (selected, warnings);
}
let omitted_assets = svg_rid
.into_iter()
.chain(base_rid)
.chain(img_layer_rids.iter().map(String::as_str))
.filter_map(|rid| images.get(rid))
.map(|asset| asset.file_name().to_string())
.collect::<Vec<_>>();
if !omitted_assets.is_empty() {
warnings.push(ConvertWarning::UnsupportedElement {
format: "PPTX".to_string(),
element: format!(
"{warning_context} image omitted: {}",
describe_assets(omitted_assets)
),
});
}
(None, warnings)
}
#[derive(Default)]
struct PictureState {
x: i64,
y: i64,
cx: i64,
cy: i64,
has_placeholder: bool,
ph_type: Option<String>,
ph_idx: Option<String>,
has_explicit_xfrm: bool,
blip_embed: Option<String>,
blip_alpha: Option<f64>,
prst_geom: Option<String>,
shadow: Option<Shadow>,
prst_adj: Option<f64>,
in_prst_geom: bool,
svg_blip_embed: Option<String>,
img_layer_embeds: Vec<String>,
crop: Option<ImageCrop>,
in_xfrm: bool,
in_sp_pr: bool,
in_ln: bool,
ln_width_emu: i64,
ln_color: Option<Color>,
ln_dash_style: BorderLineStyle,
}
impl PictureState {
fn reset(&mut self) {
*self = Self::default();
}
}
#[derive(Default)]
struct GraphicFrameState {
x: i64,
y: i64,
cx: i64,
cy: i64,
in_xfrm: bool,
}
impl GraphicFrameState {
fn reset(&mut self) {
*self = Self::default();
}
}
struct ShapeState {
depth: usize,
x: i64,
y: i64,
cx: i64,
cy: i64,
has_placeholder: bool,
ph_type: Option<String>,
ph_idx: Option<String>,
has_explicit_xfrm: bool,
rotation_deg: Option<f64>,
flip_h: bool,
flip_v: bool,
opacity: Option<f64>,
shadow: Option<Shadow>,
in_sp_pr: bool,
prst_geom: Option<String>,
fill: Option<Color>,
gradient_fill: Option<GradientFill>,
in_xfrm: bool,
in_ln: bool,
ln_width_emu: i64,
ln_color: Option<Color>,
ln_dash_style: BorderLineStyle,
head_end: ArrowHead,
tail_end: ArrowHead,
adj_values: Vec<f64>,
style_ln_color: Option<Color>,
style_ln_idx: Option<usize>,
style_fill_color: Option<Color>,
style_font_color: Option<Color>,
explicit_no_fill: bool,
}
impl Default for ShapeState {
fn default() -> Self {
Self {
depth: 0,
x: 0,
y: 0,
cx: 0,
cy: 0,
has_placeholder: false,
ph_type: None,
ph_idx: None,
has_explicit_xfrm: false,
rotation_deg: None,
flip_h: false,
flip_v: false,
opacity: None,
shadow: None,
in_sp_pr: false,
prst_geom: None,
fill: None,
gradient_fill: None,
in_xfrm: false,
in_ln: false,
ln_width_emu: 0,
ln_color: None,
ln_dash_style: BorderLineStyle::Solid,
head_end: ArrowHead::None,
tail_end: ArrowHead::None,
adj_values: Vec::new(),
style_ln_color: None,
style_ln_idx: None,
style_fill_color: None,
style_font_color: None,
explicit_no_fill: false,
}
}
}
impl ShapeState {
fn reset(&mut self) {
*self = Self::default();
}
}
#[allow(clippy::too_many_arguments)]
fn finalize_shape(
shape: &mut ShapeState,
paragraphs: &mut Vec<PptxParagraphEntry>,
text_box_padding: Insets,
text_box_vertical_align: TextBoxVerticalAlign,
text_box_no_wrap: bool,
text_box_auto_fit: bool,
text_box_text_rotation_deg: Option<f64>,
theme_line_style_widths: &[i64],
) -> Vec<FixedElement> {
let effective_ln_width_emu: i64 = if shape.ln_width_emu > 0 {
shape.ln_width_emu
} else {
shape
.style_ln_idx
.and_then(|idx| theme_line_style_widths.get(idx - 1).copied())
.unwrap_or(shape.ln_width_emu)
};
let effective_ln_width_pt: f64 = emu_to_pt(effective_ln_width_emu);
let effective_fill: Option<Color> = if shape.fill.is_some() {
shape.fill
} else if shape.explicit_no_fill {
None
} else {
shape.style_fill_color
};
let has_text = paragraphs
.iter()
.any(|entry| !entry.paragraph.runs.is_empty());
if has_text {
let blocks: Vec<Block> = group_pptx_text_blocks(std::mem::take(paragraphs));
let effective_ln_color: Option<Color> = shape.ln_color.or(shape.style_ln_color);
let stroke: Option<BorderSide> = effective_ln_color.map(|color| BorderSide {
width: effective_ln_width_pt,
color,
style: shape.ln_dash_style,
});
let text_shape_kind: Option<ShapeKind> = shape.prst_geom.as_deref().and_then(|geom| {
let width: f64 = emu_to_pt(shape.cx);
let height: f64 = emu_to_pt(shape.cy);
let kind: ShapeKind = prst_to_shape_kind(
geom,
width,
height,
shape.flip_h,
shape.flip_v,
shape.head_end,
shape.tail_end,
&shape.adj_values,
);
match kind {
ShapeKind::Rectangle => None,
other => Some(other),
}
});
let mut elements: Vec<FixedElement> = Vec::new();
if let Some(kind) = text_shape_kind {
elements.push(FixedElement {
x: emu_to_pt(shape.x),
y: emu_to_pt(shape.y),
width: emu_to_pt(shape.cx),
height: emu_to_pt(shape.cy),
kind: FixedElementKind::Shape(Shape {
kind,
fill: effective_fill,
gradient_fill: shape.gradient_fill.take(),
stroke: stroke.clone(),
rotation_deg: shape.rotation_deg,
opacity: shape.opacity,
shadow: shape.shadow.take(),
}),
});
let overlay_vertical_align = if text_box_text_rotation_deg.is_some() {
TextBoxVerticalAlign::Center
} else {
text_box_vertical_align
};
elements.push(FixedElement {
x: emu_to_pt(shape.x),
y: emu_to_pt(shape.y),
width: emu_to_pt(shape.cx),
height: emu_to_pt(shape.cy),
kind: FixedElementKind::TextBox(TextBoxData {
content: blocks,
padding: text_box_padding,
vertical_align: overlay_vertical_align,
fill: None,
opacity: None,
stroke: None,
shape_kind: None,
no_wrap: text_box_no_wrap,
auto_fit: text_box_auto_fit,
text_rotation_deg: text_box_text_rotation_deg,
}),
});
} else {
elements.push(FixedElement {
x: emu_to_pt(shape.x),
y: emu_to_pt(shape.y),
width: emu_to_pt(shape.cx),
height: emu_to_pt(shape.cy),
kind: FixedElementKind::TextBox(TextBoxData {
content: blocks,
padding: text_box_padding,
vertical_align: text_box_vertical_align,
fill: effective_fill,
opacity: shape.opacity,
stroke,
shape_kind: None,
no_wrap: text_box_no_wrap,
auto_fit: text_box_auto_fit,
text_rotation_deg: text_box_text_rotation_deg,
}),
});
}
elements
} else if let Some(ref geom) = shape.prst_geom {
let width: f64 = emu_to_pt(shape.cx);
let height: f64 = emu_to_pt(shape.cy);
let kind: ShapeKind = prst_to_shape_kind(
geom,
width,
height,
shape.flip_h,
shape.flip_v,
shape.head_end,
shape.tail_end,
&shape.adj_values,
);
let effective_ln_color: Option<Color> = shape.ln_color.or(shape.style_ln_color);
let stroke: Option<BorderSide> = effective_ln_color.map(|color| BorderSide {
width: effective_ln_width_pt,
color,
style: shape.ln_dash_style,
});
vec![FixedElement {
x: emu_to_pt(shape.x),
y: emu_to_pt(shape.y),
width,
height,
kind: FixedElementKind::Shape(Shape {
kind,
fill: effective_fill,
gradient_fill: shape.gradient_fill.take(),
stroke,
rotation_deg: shape.rotation_deg,
opacity: shape.opacity,
shadow: shape.shadow.take(),
}),
}]
} else {
Vec::new()
}
}
fn finalize_picture(
pic: &PictureState,
images: &SlideImageMap,
warning_context: &str,
) -> (Option<FixedElement>, Vec<ConvertWarning>) {
let (selected_asset, picture_warnings) = select_picture_asset(
images,
warning_context,
pic.blip_embed.as_deref(),
pic.svg_blip_embed.as_deref(),
&pic.img_layer_embeds,
);
let stroke: Option<BorderSide> = pic.ln_color.map(|color| BorderSide {
width: emu_to_pt(pic.ln_width_emu),
color,
style: pic.ln_dash_style,
});
let element = selected_asset.and_then(|asset| {
asset.format().map(|format| {
let mut clip_shape = picture_clip_shape(pic.prst_geom.as_deref(), pic.prst_adj);
let (data, format) = match pic.blip_alpha {
Some(alpha) if alpha < 1.0 => apply_image_alpha(&asset.data, alpha)
.unwrap_or_else(|| (asset.data.clone(), format)),
_ => (asset.data.clone(), format),
};
let (data, format) = if clip_shape == Some(ImageClipShape::Ellipse) {
match apply_ellipse_mask(&data) {
Some(masked) => {
clip_shape = None;
masked
}
None => (data, format),
}
} else {
(data, format)
};
FixedElement {
x: emu_to_pt(pic.x),
y: emu_to_pt(pic.y),
width: emu_to_pt(pic.cx),
height: emu_to_pt(pic.cy),
kind: FixedElementKind::Image(ImageData {
data,
format,
width: Some(emu_to_pt(pic.cx)),
height: Some(emu_to_pt(pic.cy)),
crop: pic.crop,
stroke: stroke.clone(),
alignment: None,
clip_shape,
shadow: pic.shadow.clone(),
}),
}
})
});
(element, picture_warnings)
}
fn picture_clip_shape(
prst: Option<&str>,
adjust: Option<f64>,
) -> Option<crate::ir::ImageClipShape> {
match prst? {
"ellipse" => Some(crate::ir::ImageClipShape::Ellipse),
"roundRect" | "round1Rect" | "round2SameRect" => Some(
crate::ir::ImageClipShape::RoundedRect(adjust.unwrap_or(0.16667).clamp(0.0, 0.5)),
),
_ => None,
}
}
fn apply_ellipse_mask(data: &[u8]) -> Option<(Vec<u8>, ImageFormat)> {
let decoded = image::load_from_memory(data).ok()?;
let mut rgba = decoded.into_rgba8();
let (width, height) = rgba.dimensions();
if width == 0 || height == 0 {
return None;
}
let (cx, cy) = (f64::from(width) / 2.0, f64::from(height) / 2.0);
for (x, y, pixel) in rgba.enumerate_pixels_mut() {
let nx = (f64::from(x) + 0.5 - cx) / cx;
let ny = (f64::from(y) + 0.5 - cy) / cy;
if nx * nx + ny * ny > 1.0 {
pixel[3] = 0;
}
}
let mut out = std::io::Cursor::new(Vec::new());
image::DynamicImage::ImageRgba8(rgba)
.write_to(&mut out, image::ImageFormat::Png)
.ok()?;
Some((out.into_inner(), ImageFormat::Png))
}
fn apply_image_alpha(data: &[u8], alpha: f64) -> Option<(Vec<u8>, ImageFormat)> {
let decoded = image::load_from_memory(data).ok()?;
let mut rgba = decoded.into_rgba8();
for pixel in rgba.pixels_mut() {
pixel[3] = (f64::from(pixel[3]) * alpha).round().clamp(0.0, 255.0) as u8;
}
let mut out = std::io::Cursor::new(Vec::new());
image::DynamicImage::ImageRgba8(rgba)
.write_to(&mut out, image::ImageFormat::Png)
.ok()?;
Some((out.into_inner(), ImageFormat::Png))
}
fn apply_solid_fill_color(
ctx: SolidFillCtx,
parsed: &ParsedColor,
shape: &mut ShapeState,
run_style: &mut TextStyle,
end_run_style: &mut TextStyle,
bullet_def: &mut PptxBulletDefinition,
pic: &mut PictureState,
) {
match ctx {
SolidFillCtx::ShapeFill => {
shape.fill = parsed.color;
if let Some(alpha) = parsed.alpha {
shape.opacity = Some(alpha);
}
}
SolidFillCtx::LineFill => shape.ln_color = parsed.color,
SolidFillCtx::RunFill => run_style.color = parsed.color,
SolidFillCtx::EndParaFill => end_run_style.color = parsed.color,
SolidFillCtx::BulletFill => {
bullet_def.color = parsed.color.map(PptxBulletColorSource::Explicit);
}
SolidFillCtx::PicLineFill => pic.ln_color = parsed.color,
SolidFillCtx::None => {}
}
}
struct SlideXmlParser<'a> {
xml: &'a str,
images: &'a SlideImageMap,
theme: &'a ThemeData,
color_map: &'a ColorMapData,
warning_context: &'a str,
inherited_text_body_defaults: &'a PptxTextBodyStyleDefaults,
table_styles: &'a table_styles::TableStyleMap,
skip_placeholders: bool,
placeholder_geometry: Option<&'a PlaceholderGeometryMap>,
elements: Vec<FixedElement>,
warnings: Vec<ConvertWarning>,
in_shape: bool,
shape: ShapeState,
in_txbody: bool,
paragraphs: Vec<PptxParagraphEntry>,
text_box_padding: Insets,
text_box_vertical_align: TextBoxVerticalAlign,
text_box_no_wrap: bool,
text_box_auto_fit: bool,
text_box_text_rotation_deg: Option<f64>,
text_body_style_defaults: PptxTextBodyStyleDefaults,
in_para: bool,
para_style: ParagraphStyle,
para_level: u32,
para_default_run_style: TextStyle,
para_end_run_style: TextStyle,
para_bullet_definition: PptxBulletDefinition,
in_ln_spc: bool,
in_spc_bef: bool,
in_spc_aft: bool,
runs: Vec<Run>,
in_run: bool,
run_style: TextStyle,
run_text: String,
in_text: bool,
in_rpr: bool,
rpr_applied_typeface: bool,
in_end_para_rpr: bool,
in_text_line: bool,
solid_fill_ctx: SolidFillCtx,
in_style_ln_ref: bool,
in_style_fill_ref: bool,
in_style_font_ref: bool,
in_pic: bool,
pic: PictureState,
in_graphic_frame: bool,
gf: GraphicFrameState,
}
impl<'a> SlideXmlParser<'a> {
fn new(
xml: &'a str,
images: &'a SlideImageMap,
theme: &'a ThemeData,
color_map: &'a ColorMapData,
warning_context: &'a str,
inherited_text_body_defaults: &'a PptxTextBodyStyleDefaults,
table_styles: &'a table_styles::TableStyleMap,
) -> Self {
Self {
xml,
images,
theme,
color_map,
warning_context,
inherited_text_body_defaults,
table_styles,
skip_placeholders: false,
placeholder_geometry: None,
elements: Vec::new(),
warnings: Vec::new(),
in_shape: false,
shape: ShapeState::default(),
in_txbody: false,
paragraphs: Vec::new(),
text_box_padding: default_pptx_text_box_padding(),
text_box_vertical_align: TextBoxVerticalAlign::Top,
text_box_no_wrap: false,
text_box_auto_fit: false,
text_box_text_rotation_deg: None,
text_body_style_defaults: PptxTextBodyStyleDefaults::default(),
in_para: false,
para_style: ParagraphStyle::default(),
para_level: 0,
para_default_run_style: TextStyle::default(),
para_end_run_style: TextStyle::default(),
para_bullet_definition: PptxBulletDefinition::default(),
in_ln_spc: false,
in_spc_bef: false,
in_spc_aft: false,
runs: Vec::new(),
in_run: false,
run_style: TextStyle::default(),
run_text: String::new(),
in_text: false,
in_rpr: false,
rpr_applied_typeface: false,
in_end_para_rpr: false,
in_text_line: false,
solid_fill_ctx: SolidFillCtx::None,
in_style_ln_ref: false,
in_style_fill_ref: false,
in_style_font_ref: false,
in_pic: false,
pic: PictureState::default(),
in_graphic_frame: false,
gf: GraphicFrameState::default(),
}
}
fn handle_start(&mut self, reader: &mut Reader<&[u8]>, e: &BytesStart<'_>) {
let local = e.local_name();
match local.as_ref() {
b"graphicFrame" if !self.in_shape && !self.in_pic && !self.in_graphic_frame => {
self.in_graphic_frame = true;
self.gf.reset();
}
b"xfrm" if self.in_graphic_frame && !self.in_shape => {
self.gf.in_xfrm = true;
}
b"tbl" if self.in_graphic_frame => {
if let Ok(mut table) =
parse_pptx_table(reader, self.theme, self.color_map, self.table_styles)
{
scale_pptx_table_geometry_to_frame(
&mut table,
emu_to_pt(self.gf.cx),
emu_to_pt(self.gf.cy),
);
table.use_content_driven_row_heights = false;
self.elements.push(FixedElement {
x: emu_to_pt(self.gf.x),
y: emu_to_pt(self.gf.y),
width: emu_to_pt(self.gf.cx),
height: emu_to_pt(self.gf.cy),
kind: FixedElementKind::Table(table),
});
}
}
b"grpSp" if !self.in_shape && !self.in_pic && !self.in_graphic_frame => {
if let Ok((group_elems, group_warnings)) = parse_group_shape(
reader,
self.xml,
self.images,
self.theme,
self.color_map,
self.warning_context,
self.inherited_text_body_defaults,
self.table_styles,
) {
self.elements.extend(group_elems);
self.warnings.extend(group_warnings);
}
}
b"sp" | b"cxnSp" if !self.in_shape && !self.in_pic => {
self.in_shape = true;
self.shape.reset();
self.shape.depth = 1;
self.in_txbody = false;
self.paragraphs.clear();
self.text_box_padding = default_pptx_text_box_padding();
self.text_box_vertical_align = TextBoxVerticalAlign::Top;
self.text_box_no_wrap = false;
self.text_box_auto_fit = false;
self.text_box_text_rotation_deg = None;
}
b"sp" | b"cxnSp" if self.in_shape => {
self.shape.depth += 1;
}
b"spPr" if self.in_shape && !self.in_txbody => {
self.shape.in_sp_pr = true;
}
b"xfrm" if self.in_shape && self.shape.in_sp_pr => {
self.shape.in_xfrm = true;
self.shape.has_explicit_xfrm = true;
if let Some(rot) = get_attr_i64(e, b"rot") {
self.shape.rotation_deg = Some(rot as f64 / 60_000.0);
}
self.shape.flip_h =
get_attr_str(e, b"flipH").is_some_and(|v| v == "1" || v == "true");
self.shape.flip_v =
get_attr_str(e, b"flipV").is_some_and(|v| v == "1" || v == "true");
}
b"prstGeom" if self.in_pic && self.pic.in_sp_pr => {
self.pic.prst_geom = get_attr_str(e, b"prst");
self.pic.in_prst_geom = true;
}
b"effectLst" if self.in_pic && self.pic.in_sp_pr => {
self.pic.shadow = parse_effect_list(reader, self.theme, self.color_map);
}
b"gd" if self.in_pic && self.pic.in_prst_geom => {
if self.pic.prst_adj.is_none()
&& let Some(formula) = get_attr_str(e, b"fmla")
&& let Some(value) = formula.strip_prefix("val ")
&& let Ok(value) = value.trim().parse::<f64>()
{
self.pic.prst_adj = Some(value / 100_000.0);
}
}
b"prstGeom" if self.shape.in_sp_pr => {
if let Some(prst) = get_attr_str(e, b"prst") {
self.shape.prst_geom = Some(prst);
}
}
b"custGeom" if self.shape.in_sp_pr && self.shape.prst_geom.is_none() => {
self.shape.prst_geom = Some("rect".to_string());
}
b"noFill" if self.shape.in_sp_pr && !self.shape.in_ln && !self.in_rpr => {
self.shape.explicit_no_fill = true;
}
b"solidFill" if self.shape.in_sp_pr && !self.shape.in_ln && !self.in_rpr => {
self.solid_fill_ctx = SolidFillCtx::ShapeFill;
}
b"gradFill" if self.shape.in_sp_pr && !self.shape.in_ln && !self.in_rpr => {
self.shape.gradient_fill =
parse_shape_gradient_fill(reader, self.theme, self.color_map);
if let Some(ref gradient_fill) = self.shape.gradient_fill
&& self.shape.fill.is_none()
{
self.shape.fill = gradient_fill.stops.first().map(|stop| stop.color);
}
}
b"effectLst" if self.shape.in_sp_pr && !self.shape.in_ln => {
self.shape.shadow = parse_effect_list(reader, self.theme, self.color_map);
}
b"extLst" if self.shape.in_sp_pr && !self.in_txbody => {
crate::parser::xml_util::skip_element(reader, b"extLst");
}
b"ln" if self.shape.in_sp_pr => {
self.shape.in_ln = true;
self.shape.ln_width_emu = get_attr_i64(e, b"w").unwrap_or(12700);
self.shape.ln_dash_style = BorderLineStyle::Solid;
}
b"prstDash" if self.shape.in_ln => {
self.shape.ln_dash_style = get_attr_str(e, b"val")
.as_deref()
.map(pptx_dash_to_border_style)
.unwrap_or(BorderLineStyle::Solid);
}
b"tailEnd" if self.shape.in_ln => {
self.shape.tail_end = parse_arrow_head(get_attr_str(e, b"type").as_deref());
}
b"headEnd" if self.shape.in_ln => {
self.shape.head_end = parse_arrow_head(get_attr_str(e, b"type").as_deref());
}
b"solidFill" if self.shape.in_ln => {
self.solid_fill_ctx = SolidFillCtx::LineFill;
}
b"ph" if self.in_shape => {
self.shape.has_placeholder = true;
self.shape.ph_type = get_attr_str(e, b"type");
self.shape.ph_idx = get_attr_str(e, b"idx");
}
b"ph" if self.in_pic => {
self.pic.has_placeholder = true;
self.pic.ph_type = get_attr_str(e, b"type");
self.pic.ph_idx = get_attr_str(e, b"idx");
}
b"txBody" if self.in_shape => {
self.in_txbody = true;
self.text_body_style_defaults = if self.shape.has_placeholder {
self.placeholder_geometry
.map(|map| {
map.text_defaults(
self.shape.ph_type.as_deref(),
self.shape.ph_idx.as_deref(),
)
})
.unwrap_or_default()
} else {
self.inherited_text_body_defaults.clone()
};
if let Some(color) = self.shape.style_font_color {
self.text_body_style_defaults.apply_default_color(color);
}
}
b"bodyPr" if self.in_shape && self.in_txbody => {
extract_pptx_text_box_body_props(
e,
&mut self.text_box_padding,
&mut self.text_box_vertical_align,
&mut self.text_box_no_wrap,
&mut self.text_box_text_rotation_deg,
);
}
b"spAutoFit" | b"normAutofit" if self.in_shape && self.in_txbody => {
self.text_box_auto_fit = true;
}
b"lstStyle" if self.in_shape && self.in_txbody => {
let local_defaults = parse_pptx_list_style(reader, self.theme, self.color_map);
self.text_body_style_defaults.merge_from(&local_defaults);
}
b"p" if self.in_txbody => {
self.in_para = true;
self.para_level = 0;
self.para_style = self
.text_body_style_defaults
.paragraph_style_for_level(self.para_level);
self.para_default_run_style = self
.text_body_style_defaults
.run_style_for_level(self.para_level);
self.para_end_run_style = self.para_default_run_style.clone();
self.para_bullet_definition = self
.text_body_style_defaults
.bullet_for_level(self.para_level);
self.in_ln_spc = false;
self.runs.clear();
}
b"pPr" if self.in_para && !self.in_run => {
self.para_level = extract_paragraph_level(e);
self.para_style = self
.text_body_style_defaults
.paragraph_style_for_level(self.para_level);
self.para_default_run_style = self
.text_body_style_defaults
.run_style_for_level(self.para_level);
self.para_end_run_style = self.para_default_run_style.clone();
self.para_bullet_definition = self
.text_body_style_defaults
.bullet_for_level(self.para_level);
extract_paragraph_props(e, &mut self.para_style);
}
b"lnSpc" if self.in_para && !self.in_run => {
self.in_ln_spc = true;
}
b"spcBef" if self.in_para && !self.in_run => {
self.in_spc_bef = true;
}
b"spcAft" if self.in_para && !self.in_run => {
self.in_spc_aft = true;
}
b"spcPct" if self.in_ln_spc => {
extract_pptx_line_spacing_pct(e, &mut self.para_style);
}
b"spcPts" if self.in_ln_spc => {
extract_pptx_line_spacing_pts(e, &mut self.para_style);
}
b"spcPts" if self.in_spc_bef => {
extract_pptx_space_points(e, &mut self.para_style.space_before);
}
b"spcPts" if self.in_spc_aft => {
extract_pptx_space_points(e, &mut self.para_style.space_after);
}
b"buAutoNum" if self.in_para && !self.in_run => {
self.para_bullet_definition.kind = Some(PptxBulletKind::AutoNumber(
parse_pptx_auto_numbering(e, self.para_level),
));
}
b"buChar" if self.in_para && !self.in_run => {
self.para_bullet_definition.kind = parse_pptx_bullet_marker(e, self.para_level);
}
b"buNone" if self.in_para && !self.in_run => {
self.para_bullet_definition.kind = Some(PptxBulletKind::None);
}
b"buFontTx" if self.in_para && !self.in_run => {
self.para_bullet_definition.font = Some(PptxBulletFontSource::FollowText);
}
b"buFont" if self.in_para && !self.in_run => {
if let Some(typeface) = get_attr_str(e, b"typeface") {
self.para_bullet_definition.font = Some(PptxBulletFontSource::Explicit(
resolve_theme_font(&typeface, self.theme),
));
}
}
b"buClrTx" if self.in_para && !self.in_run => {
self.para_bullet_definition.color = Some(PptxBulletColorSource::FollowText);
}
b"buClr" if self.in_para && !self.in_run => {
self.solid_fill_ctx = SolidFillCtx::BulletFill;
}
b"buSzTx" if self.in_para && !self.in_run => {
self.para_bullet_definition.size = Some(PptxBulletSizeSource::FollowText);
}
b"buSzPct" if self.in_para && !self.in_run => {
if let Some(val) = get_attr_i64(e, b"val") {
self.para_bullet_definition.size =
Some(PptxBulletSizeSource::Percent(val as f64 / 100_000.0));
}
}
b"buSzPts" if self.in_para && !self.in_run => {
if let Some(val) = get_attr_i64(e, b"val") {
self.para_bullet_definition.size =
Some(PptxBulletSizeSource::Points(val as f64 / 100.0));
}
}
b"br" if self.in_para && !self.in_run => {
push_pptx_soft_line_break(&mut self.runs, &self.para_default_run_style);
}
b"r" if self.in_para => {
self.in_run = true;
self.run_style = self.para_default_run_style.clone();
self.run_text.clear();
}
b"rPr" if self.in_run => {
self.in_rpr = true;
self.rpr_applied_typeface = false;
extract_rpr_attributes(e, &mut self.run_style);
}
b"endParaRPr" if self.in_para && !self.in_run => {
self.in_end_para_rpr = true;
self.rpr_applied_typeface = false;
self.para_end_run_style = self.para_default_run_style.clone();
extract_rpr_attributes(e, &mut self.para_end_run_style);
}
b"ln" if self.in_rpr || self.in_end_para_rpr => {
self.in_text_line = true;
}
b"solidFill" if self.in_rpr && !self.in_text_line => {
self.solid_fill_ctx = SolidFillCtx::RunFill;
}
b"solidFill" if self.in_end_para_rpr && !self.in_text_line => {
self.solid_fill_ctx = SolidFillCtx::EndParaFill;
}
b"srgbClr" | b"schemeClr" | b"sysClr" if self.solid_fill_ctx != SolidFillCtx::None => {
let parsed = parse_color_from_start(reader, e, self.theme, self.color_map);
apply_solid_fill_color(
self.solid_fill_ctx,
&parsed,
&mut self.shape,
&mut self.run_style,
&mut self.para_end_run_style,
&mut self.para_bullet_definition,
&mut self.pic,
);
}
b"srgbClr" | b"schemeClr" | b"sysClr" if self.in_style_ln_ref => {
let parsed = parse_color_from_start(reader, e, self.theme, self.color_map);
self.shape.style_ln_color = parsed.color;
}
b"srgbClr" | b"schemeClr" | b"sysClr" if self.in_style_fill_ref => {
let parsed = parse_color_from_start(reader, e, self.theme, self.color_map);
self.shape.style_fill_color = parsed.color;
}
b"srgbClr" | b"schemeClr" | b"sysClr" if self.in_style_font_ref => {
let parsed = parse_color_from_start(reader, e, self.theme, self.color_map);
self.shape.style_font_color = parsed.color;
}
b"lnRef" if self.in_shape && !self.shape.in_sp_pr && !self.in_txbody => {
self.in_style_ln_ref = true;
self.shape.style_ln_idx = get_attr_str(e, b"idx")
.and_then(|value| value.parse::<usize>().ok())
.filter(|idx| *idx > 0);
}
b"fillRef" if self.in_shape && !self.shape.in_sp_pr && !self.in_txbody => {
self.in_style_fill_ref = true;
}
b"fontRef" if self.in_shape && !self.shape.in_sp_pr && !self.in_txbody => {
self.in_style_font_ref = true;
}
b"t" if self.in_run => {
self.in_text = true;
}
b"pic" if !self.in_shape && !self.in_pic => {
self.in_pic = true;
self.pic.reset();
}
b"spPr" if self.in_pic => {
self.pic.in_sp_pr = true;
}
b"xfrm" if self.in_pic && self.pic.in_sp_pr => {
self.pic.in_xfrm = true;
self.pic.has_explicit_xfrm = true;
}
b"ln" if self.in_pic && self.pic.in_sp_pr => {
self.pic.in_ln = true;
self.pic.ln_width_emu = get_attr_i64(e, b"w").unwrap_or(12700);
self.pic.ln_dash_style = BorderLineStyle::Solid;
}
b"solidFill" if self.in_pic && self.pic.in_ln => {
self.solid_fill_ctx = SolidFillCtx::PicLineFill;
}
b"prstDash" if self.in_pic && self.pic.in_ln => {
self.pic.ln_dash_style = get_attr_str(e, b"val")
.as_deref()
.map(pptx_dash_to_border_style)
.unwrap_or(BorderLineStyle::Solid);
}
b"blipFill" if self.in_pic => {}
b"blip" if self.in_pic => {
self.pic.blip_embed = get_attr_str(e, b"r:embed");
}
b"alphaModFix" if self.in_pic => {
if let Some(amount) = get_attr_i64(e, b"amt") {
self.pic.blip_alpha = Some((amount as f64 / 100_000.0).clamp(0.0, 1.0));
}
}
b"svgBlip" if self.in_pic => {
self.pic.svg_blip_embed = get_attr_str(e, b"r:embed");
}
b"imgLayer" if self.in_pic => {
if let Some(rid) = get_attr_str(e, b"r:embed") {
self.pic.img_layer_embeds.push(rid);
}
}
b"srcRect" if self.in_pic => {
self.pic.crop = parse_src_rect(e);
}
_ => {}
}
}
fn handle_empty(&mut self, e: &BytesStart<'_>) {
let local = e.local_name();
match local.as_ref() {
b"off" if self.shape.in_xfrm => {
self.shape.x = get_attr_i64(e, b"x").unwrap_or(0);
self.shape.y = get_attr_i64(e, b"y").unwrap_or(0);
}
b"ext" if self.shape.in_xfrm => {
self.shape.cx = get_attr_i64(e, b"cx").unwrap_or(0);
self.shape.cy = get_attr_i64(e, b"cy").unwrap_or(0);
}
b"off" if self.pic.in_xfrm => {
self.pic.x = get_attr_i64(e, b"x").unwrap_or(0);
self.pic.y = get_attr_i64(e, b"y").unwrap_or(0);
}
b"ext" if self.pic.in_xfrm => {
self.pic.cx = get_attr_i64(e, b"cx").unwrap_or(0);
self.pic.cy = get_attr_i64(e, b"cy").unwrap_or(0);
}
b"off" if self.gf.in_xfrm => {
self.gf.x = get_attr_i64(e, b"x").unwrap_or(0);
self.gf.y = get_attr_i64(e, b"y").unwrap_or(0);
}
b"ext" if self.gf.in_xfrm => {
self.gf.cx = get_attr_i64(e, b"cx").unwrap_or(0);
self.gf.cy = get_attr_i64(e, b"cy").unwrap_or(0);
}
b"blip" if self.in_pic => {
self.pic.blip_embed = get_attr_str(e, b"r:embed");
}
b"alphaModFix" if self.in_pic => {
if let Some(amount) = get_attr_i64(e, b"amt") {
self.pic.blip_alpha = Some((amount as f64 / 100_000.0).clamp(0.0, 1.0));
}
}
b"svgBlip" if self.in_pic => {
self.pic.svg_blip_embed = get_attr_str(e, b"r:embed");
}
b"imgLayer" if self.in_pic => {
if let Some(rid) = get_attr_str(e, b"r:embed") {
self.pic.img_layer_embeds.push(rid);
}
}
b"srcRect" if self.in_pic => {
self.pic.crop = parse_src_rect(e);
}
b"prstDash" if self.in_pic && self.pic.in_ln => {
self.pic.ln_dash_style = get_attr_str(e, b"val")
.as_deref()
.map(pptx_dash_to_border_style)
.unwrap_or(BorderLineStyle::Solid);
}
b"ph" if self.in_shape => {
self.shape.has_placeholder = true;
self.shape.ph_type = get_attr_str(e, b"type");
self.shape.ph_idx = get_attr_str(e, b"idx");
}
b"ph" if self.in_pic => {
self.pic.has_placeholder = true;
self.pic.ph_type = get_attr_str(e, b"type");
self.pic.ph_idx = get_attr_str(e, b"idx");
}
b"bodyPr" if self.in_shape && self.in_txbody => {
extract_pptx_text_box_body_props(
e,
&mut self.text_box_padding,
&mut self.text_box_vertical_align,
&mut self.text_box_no_wrap,
&mut self.text_box_text_rotation_deg,
);
}
b"spAutoFit" | b"normAutofit" if self.in_shape && self.in_txbody => {
self.text_box_auto_fit = true;
}
b"prstGeom" if self.in_pic && self.pic.in_sp_pr => {
self.pic.prst_geom = get_attr_str(e, b"prst");
}
b"gd" if self.in_pic && self.pic.in_prst_geom => {
if self.pic.prst_adj.is_none()
&& let Some(formula) = get_attr_str(e, b"fmla")
&& let Some(value) = formula.strip_prefix("val ")
&& let Ok(value) = value.trim().parse::<f64>()
{
self.pic.prst_adj = Some(value / 100_000.0);
}
}
b"prstGeom" if self.shape.in_sp_pr => {
if let Some(prst) = get_attr_str(e, b"prst") {
self.shape.prst_geom = Some(prst);
}
}
b"custGeom" if self.shape.in_sp_pr && self.shape.prst_geom.is_none() => {
self.shape.prst_geom = Some("rect".to_string());
}
b"ln" if self.shape.in_sp_pr => {
self.shape.ln_width_emu = get_attr_i64(e, b"w").unwrap_or(12700);
}
b"prstDash" if self.shape.in_ln => {
self.shape.ln_dash_style = get_attr_str(e, b"val")
.as_deref()
.map(pptx_dash_to_border_style)
.unwrap_or(BorderLineStyle::Solid);
}
b"tailEnd" if self.shape.in_ln => {
self.shape.tail_end = parse_arrow_head(get_attr_str(e, b"type").as_deref());
}
b"headEnd" if self.shape.in_ln => {
self.shape.head_end = parse_arrow_head(get_attr_str(e, b"type").as_deref());
}
b"gd" if self.in_shape && self.shape.in_sp_pr => {
if let Some(val) = get_attr_str(e, b"fmla")
.as_deref()
.and_then(|f| f.strip_prefix("val "))
.and_then(|s| s.parse::<f64>().ok())
{
self.shape.adj_values.push(val);
}
}
b"noFill" if self.shape.in_sp_pr && !self.shape.in_ln => {
self.shape.explicit_no_fill = true;
}
b"srgbClr" | b"schemeClr" | b"sysClr" if self.in_style_font_ref => {
let parsed = parse_color_from_empty(e, self.theme, self.color_map);
self.shape.style_font_color = parsed.color;
}
b"srgbClr" | b"schemeClr" | b"sysClr" if self.in_style_fill_ref => {
let parsed = parse_color_from_empty(e, self.theme, self.color_map);
self.shape.style_fill_color = parsed.color;
}
b"srgbClr" | b"schemeClr" | b"sysClr" if self.in_style_ln_ref => {
let parsed = parse_color_from_empty(e, self.theme, self.color_map);
self.shape.style_ln_color = parsed.color;
}
b"srgbClr" | b"schemeClr" | b"sysClr" if self.solid_fill_ctx != SolidFillCtx::None => {
let parsed = parse_color_from_empty(e, self.theme, self.color_map);
apply_solid_fill_color(
self.solid_fill_ctx,
&parsed,
&mut self.shape,
&mut self.run_style,
&mut self.para_end_run_style,
&mut self.para_bullet_definition,
&mut self.pic,
);
}
b"rPr" if self.in_run => {
extract_rpr_attributes(e, &mut self.run_style);
}
b"endParaRPr" if self.in_para && !self.in_run => {
self.para_end_run_style = self.para_default_run_style.clone();
extract_rpr_attributes(e, &mut self.para_end_run_style);
}
b"ln" if self.in_rpr || self.in_end_para_rpr => {
self.in_text_line = true;
}
b"pPr" if self.in_para && !self.in_run => {
self.para_level = extract_paragraph_level(e);
self.para_style = self
.text_body_style_defaults
.paragraph_style_for_level(self.para_level);
self.para_default_run_style = self
.text_body_style_defaults
.run_style_for_level(self.para_level);
self.para_end_run_style = self.para_default_run_style.clone();
self.para_bullet_definition = self
.text_body_style_defaults
.bullet_for_level(self.para_level);
extract_paragraph_props(e, &mut self.para_style);
}
b"lnSpc" if self.in_para && !self.in_run => {
self.in_ln_spc = true;
}
b"spcBef" if self.in_para && !self.in_run => {
self.in_spc_bef = true;
}
b"spcAft" if self.in_para && !self.in_run => {
self.in_spc_aft = true;
}
b"spcPct" if self.in_ln_spc => {
extract_pptx_line_spacing_pct(e, &mut self.para_style);
}
b"spcPts" if self.in_ln_spc => {
extract_pptx_line_spacing_pts(e, &mut self.para_style);
}
b"spcPts" if self.in_spc_bef => {
extract_pptx_space_points(e, &mut self.para_style.space_before);
}
b"spcPts" if self.in_spc_aft => {
extract_pptx_space_points(e, &mut self.para_style.space_after);
}
b"buAutoNum" if self.in_para && !self.in_run => {
self.para_bullet_definition.kind = Some(PptxBulletKind::AutoNumber(
parse_pptx_auto_numbering(e, self.para_level),
));
}
b"buChar" if self.in_para && !self.in_run => {
self.para_bullet_definition.kind = parse_pptx_bullet_marker(e, self.para_level);
}
b"buNone" if self.in_para && !self.in_run => {
self.para_bullet_definition.kind = Some(PptxBulletKind::None);
}
b"buFontTx" if self.in_para && !self.in_run => {
self.para_bullet_definition.font = Some(PptxBulletFontSource::FollowText);
}
b"buFont" if self.in_para && !self.in_run => {
if let Some(typeface) = get_attr_str(e, b"typeface") {
self.para_bullet_definition.font = Some(PptxBulletFontSource::Explicit(
resolve_theme_font(&typeface, self.theme),
));
}
}
b"buClrTx" if self.in_para && !self.in_run => {
self.para_bullet_definition.color = Some(PptxBulletColorSource::FollowText);
}
b"buClr" if self.in_para && !self.in_run => {
self.solid_fill_ctx = SolidFillCtx::BulletFill;
}
b"buSzTx" if self.in_para && !self.in_run => {
self.para_bullet_definition.size = Some(PptxBulletSizeSource::FollowText);
}
b"buSzPct" if self.in_para && !self.in_run => {
if let Some(val) = get_attr_i64(e, b"val") {
self.para_bullet_definition.size =
Some(PptxBulletSizeSource::Percent(val as f64 / 100_000.0));
}
}
b"buSzPts" if self.in_para && !self.in_run => {
if let Some(val) = get_attr_i64(e, b"val") {
self.para_bullet_definition.size =
Some(PptxBulletSizeSource::Points(val as f64 / 100.0));
}
}
b"br" if self.in_para && !self.in_run => {
push_pptx_soft_line_break(&mut self.runs, &self.para_default_run_style);
}
b"latin" | b"ea" | b"cs" if self.in_rpr => {
if !self.rpr_applied_typeface {
self.run_style.font_family = None;
}
apply_typeface_to_style(e, &mut self.run_style, self.theme);
self.rpr_applied_typeface |= self.run_style.font_family.is_some();
}
b"latin" | b"ea" | b"cs" if self.in_end_para_rpr => {
if !self.rpr_applied_typeface {
self.para_end_run_style.font_family = None;
}
apply_typeface_to_style(e, &mut self.para_end_run_style, self.theme);
self.rpr_applied_typeface |= self.para_end_run_style.font_family.is_some();
}
_ => {}
}
}
fn handle_text(&mut self, text: &str) {
if self.in_text {
self.run_text.push_str(text);
}
}
fn handle_end(&mut self, local_name: &[u8]) {
match local_name {
b"sp" | b"cxnSp" if self.in_shape => {
self.shape.depth -= 1;
if self.shape.depth == 0 {
if self.shape.has_placeholder
&& !self.shape.has_explicit_xfrm
&& let Some(geometry) = self.placeholder_geometry.and_then(|map| {
map.lookup(self.shape.ph_type.as_deref(), self.shape.ph_idx.as_deref())
})
{
self.shape.x = geometry.x;
self.shape.y = geometry.y;
self.shape.cx = geometry.cx;
self.shape.cy = geometry.cy;
}
if !(self.skip_placeholders && self.shape.has_placeholder) {
self.elements.extend(finalize_shape(
&mut self.shape,
&mut self.paragraphs,
self.text_box_padding,
self.text_box_vertical_align,
self.text_box_no_wrap,
self.text_box_auto_fit,
self.text_box_text_rotation_deg,
&self.theme.line_style_widths,
));
}
self.in_shape = false;
}
}
b"spPr" if self.shape.in_sp_pr => {
self.shape.in_sp_pr = false;
}
b"xfrm" if self.shape.in_xfrm => {
self.shape.in_xfrm = false;
}
b"ln" if self.shape.in_ln => {
self.shape.in_ln = false;
}
b"txBody" if self.in_txbody => {
self.in_txbody = false;
}
b"p" if self.in_para => {
let resolved_list_marker = resolve_pptx_list_marker(
&self.para_bullet_definition,
self.para_level,
&self.runs,
&self.para_end_run_style,
&self.para_default_run_style,
);
let paragraph_runs = std::mem::take(&mut self.runs);
self.paragraphs.push(PptxParagraphEntry {
paragraph: Paragraph {
style: self.para_style.clone(),
runs: paragraph_runs,
},
list_marker: resolved_list_marker,
});
self.in_para = false;
}
b"r" if self.in_run => {
if !self.run_text.is_empty() {
push_pptx_run(
&mut self.runs,
Run {
text: std::mem::take(&mut self.run_text),
style: self.run_style.clone(),
href: None,
footnote: None,
},
);
}
self.in_run = false;
}
b"rPr" if self.in_rpr => {
self.in_rpr = false;
}
b"endParaRPr" if self.in_end_para_rpr => {
self.in_end_para_rpr = false;
}
b"ln" if self.in_text_line => {
self.in_text_line = false;
}
b"lnSpc" if self.in_ln_spc => {
self.in_ln_spc = false;
}
b"spcBef" if self.in_spc_bef => {
self.in_spc_bef = false;
}
b"spcAft" if self.in_spc_aft => {
self.in_spc_aft = false;
}
b"solidFill" if self.solid_fill_ctx != SolidFillCtx::None => {
self.solid_fill_ctx = SolidFillCtx::None;
}
b"lnRef" if self.in_style_ln_ref => {
self.in_style_ln_ref = false;
}
b"fillRef" if self.in_style_fill_ref => {
self.in_style_fill_ref = false;
}
b"fontRef" if self.in_style_font_ref => {
self.in_style_font_ref = false;
}
b"t" if self.in_text => {
self.in_text = false;
}
b"pic" if self.in_pic => {
if self.pic.has_placeholder
&& !self.pic.has_explicit_xfrm
&& let Some(geometry) = self.placeholder_geometry.and_then(|map| {
map.lookup(self.pic.ph_type.as_deref(), self.pic.ph_idx.as_deref())
})
{
self.pic.x = geometry.x;
self.pic.y = geometry.y;
self.pic.cx = geometry.cx;
self.pic.cy = geometry.cy;
}
let (element, picture_warnings) =
finalize_picture(&self.pic, self.images, self.warning_context);
self.warnings.extend(picture_warnings);
if let Some(element) = element {
self.elements.push(element);
}
self.in_pic = false;
}
b"spPr" if self.in_pic && self.pic.in_sp_pr => {
self.pic.in_sp_pr = false;
}
b"prstGeom" if self.in_pic && self.pic.in_prst_geom => {
self.pic.in_prst_geom = false;
}
b"ln" if self.in_pic && self.pic.in_ln => {
self.pic.in_ln = false;
}
b"xfrm" if self.pic.in_xfrm => {
self.pic.in_xfrm = false;
}
b"graphicFrame" if self.in_graphic_frame => {
self.in_graphic_frame = false;
}
b"xfrm" if self.gf.in_xfrm => {
self.gf.in_xfrm = false;
}
_ => {}
}
}
fn finish(self) -> (Vec<FixedElement>, Vec<ConvertWarning>) {
(self.elements, self.warnings)
}
}
#[allow(clippy::too_many_arguments)]
pub(super) fn parse_slide_xml(
xml: &str,
images: &SlideImageMap,
theme: &ThemeData,
color_map: &ColorMapData,
warning_context: &str,
inherited_text_body_defaults: &PptxTextBodyStyleDefaults,
table_styles: &table_styles::TableStyleMap,
placeholder_geometry: Option<&PlaceholderGeometryMap>,
) -> Result<(Vec<FixedElement>, Vec<ConvertWarning>), ConvertError> {
parse_slide_xml_inner(
xml,
images,
theme,
color_map,
warning_context,
inherited_text_body_defaults,
table_styles,
false,
placeholder_geometry,
)
}
#[allow(clippy::too_many_arguments)]
fn parse_slide_xml_inner(
xml: &str,
images: &SlideImageMap,
theme: &ThemeData,
color_map: &ColorMapData,
warning_context: &str,
inherited_text_body_defaults: &PptxTextBodyStyleDefaults,
table_styles: &table_styles::TableStyleMap,
skip_placeholders: bool,
placeholder_geometry: Option<&PlaceholderGeometryMap>,
) -> Result<(Vec<FixedElement>, Vec<ConvertWarning>), ConvertError> {
let mut reader = Reader::from_str(xml);
let mut parser = SlideXmlParser::new(
xml,
images,
theme,
color_map,
warning_context,
inherited_text_body_defaults,
table_styles,
);
parser.skip_placeholders = skip_placeholders;
parser.placeholder_geometry = placeholder_geometry;
loop {
match reader.read_event() {
Ok(Event::Start(ref e)) => {
parser.handle_start(&mut reader, e);
}
Ok(Event::Empty(ref e)) => {
parser.handle_empty(e);
}
Ok(Event::Text(ref t)) => {
if let Some(text) = decode_pptx_text_event(t) {
parser.handle_text(&text);
}
}
Ok(Event::GeneralRef(ref reference)) => {
if let Some(text) = decode_pptx_general_ref(reference) {
parser.handle_text(&text);
}
}
Ok(Event::End(ref e)) => {
parser.handle_end(e.local_name().as_ref());
}
Ok(Event::Eof) => break,
Err(error) => {
return Err(crate::parser::parse_err(format!(
"XML error in slide: {error}"
)));
}
_ => {}
}
}
Ok(parser.finish())
}