use quick_xml::events::{BytesStart, Event};
use quick_xml::Reader;
use crate::enums::shapes::{PlaceholderOrientation, PlaceholderSize, PpPlaceholderType};
use crate::error::{PptxError, PptxResult};
use crate::shapes::placeholder::PlaceholderFormat;
use crate::shapes::Shape;
use crate::units::{PlaceholderIndex, ShapeId};
use crate::xml_util::{attr_value, attr_value_ns, local_name_owned};
use super::parse_accum::{ShapeAccum, ShapeKind};
use super::xml_capture::{CaptureTarget, XmlCapture};
#[derive(Debug, PartialEq)]
enum ParseState {
Seeking,
InSpTree,
InShape,
}
struct ElementCtx {
local: String,
}
fn process_start_element(
local: &str,
e: &BytesStart<'_>,
accum: &mut ShapeAccum,
) -> PptxResult<()> {
match local {
"cNvPr" => {
accum.shape_id = ShapeId(parse_u32_attr(e, b"id")?);
accum.name = attr_value(e, b"name")?
.map(std::borrow::Cow::into_owned)
.unwrap_or_default();
if let Some(desc) = attr_value(e, b"descr")? {
accum.description = Some(desc.into_owned());
}
}
"cNvSpPr" => {
accum.is_textbox = attr_value(e, b"txBox")?.as_deref() == Some("1");
}
"ph" => {
accum.placeholder = Some(PlaceholderFormat {
ph_type: attr_value(e, b"type")?.and_then(|c| PpPlaceholderType::from_xml_str(&c)),
idx: PlaceholderIndex(parse_u32_attr(e, b"idx")?),
orient: attr_value(e, b"orient")?
.and_then(|c| PlaceholderOrientation::from_xml_str(&c)),
sz: attr_value(e, b"sz")?.and_then(|c| PlaceholderSize::from_xml_str(&c)),
});
}
"off" => {
accum.left = parse_i64_attr(e, b"x")?;
accum.top = parse_i64_attr(e, b"y")?;
}
"ext" => {
accum.width = parse_i64_attr(e, b"cx")?;
accum.height = parse_i64_attr(e, b"cy")?;
}
"xfrm" => {
if let Some(rot_str) = attr_value(e, b"rot")? {
if let Ok(rot_val) = rot_str.parse::<i64>() {
#[allow(clippy::cast_precision_loss)]
{
accum.rotation = rot_val as f64 / 60000.0;
}
}
}
accum.flip_h = attr_value(e, b"flipH")?.as_deref() == Some("1");
accum.flip_v = attr_value(e, b"flipV")?.as_deref() == Some("1");
}
"prstGeom" => {
accum.prst_geom = attr_value(e, b"prst")?.map(std::borrow::Cow::into_owned);
}
"txBody" => {
accum.has_tx_body = true;
}
"blip" => {
accum.image_r_id = attr_value_ns(e, b"embed")?.map(std::borrow::Cow::into_owned);
}
"graphicData" => {
accum.graphic_data_uri = attr_value(e, b"uri")?.map(std::borrow::Cow::into_owned);
}
"relIds" => {
accum.smartart_r_id = attr_value_ns(e, b"dm")?.map(std::borrow::Cow::into_owned);
}
_ => {}
}
Ok(())
}
fn parse_u32_attr(e: &BytesStart<'_>, key: &[u8]) -> PptxResult<u32> {
Ok(attr_value(e, key)?
.and_then(|s| s.parse().ok())
.unwrap_or(0))
}
fn parse_i64_attr(e: &BytesStart<'_>, key: &[u8]) -> PptxResult<i64> {
Ok(attr_value(e, key)?
.and_then(|s| s.parse().ok())
.unwrap_or(0))
}
#[allow(clippy::cognitive_complexity, clippy::too_many_lines)]
pub(super) fn parse_shapes_from_slide_xml(xml: &[u8]) -> PptxResult<Vec<Shape>> {
let mut reader = Reader::from_reader(xml);
reader.config_mut().trim_text(true);
let mut shapes = Vec::new();
let mut buf = Vec::new();
let mut state = ParseState::Seeking;
let mut element_stack: Vec<ElementCtx> = Vec::new();
let mut sp_tree_depth: Option<usize> = None;
let mut current_shape: Option<ShapeAccum> = None;
let mut capture: Option<XmlCapture> = None;
loop {
buf.clear();
match reader.read_event_into(&mut buf) {
Ok(Event::Start(ref e)) => {
let qname = e.name();
let local = local_name_owned(qname.as_ref());
let stack_depth = element_stack.len();
if let Some(ref mut cap) = capture {
cap.depth += 1;
cap.push_start(e);
if let Some(ref mut accum) = current_shape {
process_start_element(&local, e, accum)?;
}
} else {
match &state {
ParseState::Seeking => {
if local == "spTree" {
sp_tree_depth = Some(stack_depth);
state = ParseState::InSpTree;
}
}
ParseState::InSpTree => {
if Some(stack_depth) == sp_tree_depth.map(|d| d + 1) {
let kind = match local.as_str() {
"sp" => Some(ShapeKind::Sp),
"pic" => Some(ShapeKind::Pic),
"graphicFrame" => Some(ShapeKind::GraphicFrame),
"cxnSp" => Some(ShapeKind::CxnSp),
"grpSp" => Some(ShapeKind::GrpSp),
_ => None,
};
if let Some(k) = kind {
current_shape = Some(ShapeAccum::new(k));
state = ParseState::InShape;
}
}
}
ParseState::InShape => {
if let Some(ref mut accum) = current_shape {
process_start_element(&local, e, accum)?;
}
match local.as_str() {
"spPr" => {
let mut cap = XmlCapture::new(CaptureTarget::SpPr);
cap.push_start_with_tag("p:spPr", e);
cap.depth = 1;
capture = Some(cap);
}
"txBody" => {
let mut cap = XmlCapture::new(CaptureTarget::TxBody);
cap.push_start_with_tag("p:txBody", e);
cap.depth = 1;
capture = Some(cap);
}
_ => {}
}
}
}
}
element_stack.push(ElementCtx { local });
}
Ok(Event::Empty(ref e)) => {
let qname = e.name();
let local = local_name_owned(qname.as_ref());
if let Some(ref mut cap) = capture {
cap.push_empty(e);
if let Some(ref mut accum) = current_shape {
process_start_element(&local, e, accum)?;
}
} else if state == ParseState::InShape {
if let Some(ref mut accum) = current_shape {
process_start_element(&local, e, accum)?;
}
}
}
Ok(Event::Text(ref t)) => {
if let Some(ref mut cap) = capture {
cap.push_text(t.as_ref());
}
}
Ok(Event::End(ref e)) => {
if let Some(ref mut cap) = capture {
cap.depth -= 1;
if cap.depth == 0 {
cap.push_end_raw(e.name().as_ref());
let Some(finished) = capture.take() else {
unreachable!("capture is always Some when depth reaches zero");
};
if let Some(ref mut accum) = current_shape {
match finished.target {
CaptureTarget::SpPr => {
accum.sp_pr_xml = Some(finished.xml);
}
CaptureTarget::TxBody => {
accum.tx_body_xml_bytes = Some(finished.xml);
}
}
}
} else {
cap.push_end_raw(e.name().as_ref());
}
} else {
let popped = element_stack.pop();
if state == ParseState::InShape {
if let Some(sp_depth) = sp_tree_depth {
if element_stack.len() == sp_depth + 1 {
if let Some(accum) = current_shape.take() {
shapes.push(accum.into_shape());
}
state = ParseState::InSpTree;
}
}
} else if state == ParseState::InSpTree {
if let Some(ref popped) = popped {
if popped.local == "spTree" {
state = ParseState::Seeking;
sp_tree_depth = None;
}
}
}
continue;
}
element_stack.pop();
}
Ok(Event::Eof) => break,
Err(e) => return Err(PptxError::Xml(e)),
_ => {}
}
}
Ok(shapes)
}