Skip to main content

pptx_rs/oxml/
parse_sld.rs

1//! `<p:sld>` / `<p:sp>` / `<p:txBody>` 的反序列化(read 路径)。
2//!
3//! 本文件承担"读"职责——把 `slideN.xml` 的 XML 文本反序列化为强类型 [`Sld`]。
4//! 由于 OOXML 元素嵌套极深、命名空间混杂,这里采用 **SAX 风格**流式解析:
5//!
6//! 1. 用 [`quick_xml`] 逐事件读取(`Start` / `Empty` / `End` / `Text`);
7//! 2. 在 `<p:spTree>` 阶段对每个 `<p:sp>` 调用 `parse_sp`;
8//! 3. 在 sp 内部分别 parse `nvSpPr` / `spPr` / `txBody`;
9//! 4. 跨过我们不识别的元素(pic / grpSp / cxnSp / graphicFrame)时**直接吞掉**子节点。
10//!
11//! # 设计取舍
12//!
13//! - **不递归到所有子元素**——例如 `a:xfrm` 内部的 off/ext 不做位置/尺寸校验,
14//!   仅提取 4 个数字属性;
15//! - **`a:rPr` 仅解析属性**——RunProperties 的子元素(`<a:solidFill>` 等)不递归
16//!   解析为 Color,而是 **保留原始 XML 字符串** 以便原样回写;
17//! - **遇到未知命名空间或属性** 静默忽略(OOXML 经常带 PowerPoint 私有扩展)。
18//!
19//! # 与 python-pptx 的差异
20//!
21//! python-pptx 中 `Slide.shapes` 持有 `SlideShapes`,每个 shape 是一个 `Shape`
22//! 子类(`TextBox` / `Picture` / ...)。本库采用**强类型枚举** `SlideShape`,
23//! 调用方在 `match` 时决定行为。
24//!
25//! # 失败模式
26//!
27//! 任何 XML 解析错误(标签不闭合、属性不是数字、必须属性缺失)均返回
28//! [`crate::Error::Xml`],不会 panic。`pic` / `grpSp` / `cxnSp` / `graphicFrame`
29//! 的 XML 仅做"原样保留字符串"处理;它们被保存时**不会**从字符串恢复成结构体,
30//! 仍可保证 zip 字节不丢。
31
32// SAX 解析中用 Default 初始化结构体后逐字段赋值是正常模式,允许此 clippy 警告。
33#![allow(clippy::field_reassign_with_default)]
34
35use quick_xml::events::Event;
36use quick_xml::reader::Reader;
37
38use crate::oxml::color::{Color, PresetColor, SchemeColor};
39use crate::oxml::shape::{
40    Connector, GraphicFrame, Group, GroupChild, Pic, ShapeLocks, ShapeStyle, Sp, StyleRef,
41};
42use crate::oxml::simpletypes::{
43    Alignment, MsoAnchor, PresetGeometry, TabAlignment, TextWrapping, Underline,
44};
45use crate::oxml::slide::Sld;
46use crate::oxml::slide::{
47    MorphOption, SplitOrientation, Transition, TransitionDirection, TransitionSpeed, TransitionType,
48};
49use crate::oxml::sppr::{
50    ArrowHead, ArrowSize, ArrowType, Backdrop, Bevel, Camera, CameraPreset, CustomGeometry,
51    EffectList, Fill, GeomRect, Geometry, GlowEffect, GradientFill, GradientPath, GradientStop,
52    GradientType, LightRig, LightRigDirection, LightRigType, Line, LineJoin, MaterialPreset, Path,
53    PathSegment, PatternFill, Point3d, ReflectionEffect, Rotation3d, Scene3d, ShadowEffect,
54    ShapeProperties, SoftEdgeEffect, Sp3d,
55};
56use crate::oxml::txbody::{
57    BodyProperties, BulletStyle, Field, FieldType, Hyperlink, Paragraph, ParagraphProperties, Run,
58    RunProperties, TabStop, TextBody,
59};
60use crate::oxml::SlideShape as OxmlSlideShape;
61use crate::units::{Emu, Pt, RGBColor};
62
63/// 从 `slideN.xml` 文本解析出 [`Sld`]。
64///
65/// # 错误
66/// - [`crate::Error::Xml`]:XML 语法错误或关键属性缺失。
67///
68/// # 注意
69/// - 返回的 `Sld` 字段中,`layout_rid` **不会**自动填充——`Presentation::load`
70///   会从 `slideN.xml.rels` 中解析并回填;
71/// - `ext_lst` 暂不解析(保留 `None`),如需原样保留需扩展实现。
72pub fn parse_sld(xml: &str) -> crate::Result<Sld> {
73    let mut rd = Reader::from_str(xml);
74    rd.config_mut().trim_text(true);
75    let mut buf = Vec::new();
76    let mut sld = Sld::default();
77    // 状态机:
78    //   0: 等待 <p:sld> 入口;
79    //   1: 在 <p:sld> 内部;
80    //   2: 在 <p:spTree> 内部。
81    let mut state: u8 = 0;
82    // 使用 Vec 而非 Option,因为一张 slide 可以包含多个同类型形状
83    let mut sp_bufs: Vec<String> = Vec::new();
84    let mut pic_bufs: Vec<String> = Vec::new();
85    let mut cxn_bufs: Vec<String> = Vec::new();
86    let mut grp_bufs: Vec<String> = Vec::new();
87    let mut gfx_bufs: Vec<String> = Vec::new();
88
89    loop {
90        match rd.read_event_into(&mut buf) {
91            Ok(Event::Start(e)) => {
92                let name = e.name();
93                let local = local_name(name.as_ref());
94                match state {
95                    0 if local == b"sld" => {
96                        // 读取 <p:sld> 标签上的 id 属性(虽然 0.1.0 sld.id 主要由
97                        // Presentation 内部 sldIdLst 决定,但保留供 read-modify-write)。
98                        for a in e.attributes().flatten() {
99                            if a.key.as_ref() == b"id" {
100                                if let Ok(v) = a
101                                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
102                                    .unwrap_or_default()
103                                    .parse::<u32>()
104                                {
105                                    sld.id = v;
106                                }
107                            }
108                        }
109                        state = 1;
110                    }
111                    1 if local == b"cSld" => {
112                        // 读取 <p:cSld name="...">(slide 用户可读名,对标 python-pptx Slide.name)。
113                        for a in e.attributes().flatten() {
114                            if a.key.as_ref() == b"name" {
115                                sld.name = a
116                                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
117                                    .unwrap_or_default()
118                                    .to_string();
119                            }
120                        }
121                    }
122                    1 if local == b"spTree" => {
123                        state = 2;
124                    }
125                    1 if local == b"transition" => {
126                        // <p:transition>(TODO-020:幻灯片过渡)
127                        // 位于 <p:cSld> 之后、<p:timing> 之前
128                        let inner = collect_full_element(&mut rd, e.into_owned())?;
129                        if let Ok(tr) = parse_transition(&inner) {
130                            sld.transition = Some(tr);
131                        }
132                    }
133                    2 if local == b"sp" => {
134                        // 累积 sp 的内部 XML(含闭合标签)—— 走子解析
135                        let inner = collect_full_element(&mut rd, e.into_owned())?;
136                        sp_bufs.push(inner);
137                    }
138                    2 if local == b"pic" => {
139                        let inner = collect_full_element(&mut rd, e.into_owned())?;
140                        pic_bufs.push(inner);
141                    }
142                    2 if local == b"cxnSp" => {
143                        let inner = collect_full_element(&mut rd, e.into_owned())?;
144                        cxn_bufs.push(inner);
145                    }
146                    2 if local == b"grpSp" => {
147                        let inner = collect_full_element(&mut rd, e.into_owned())?;
148                        grp_bufs.push(inner);
149                    }
150                    2 if local == b"graphicFrame" => {
151                        let inner = collect_full_element(&mut rd, e.into_owned())?;
152                        gfx_bufs.push(inner);
153                    }
154                    _ => {
155                        // 其它子元素(nvGrpSpPr / grpSpPr 等)—— 整个吞掉
156                        let _ = collect_full_element(&mut rd, e.into_owned());
157                    }
158                }
159            }
160            Ok(Event::Empty(e)) => {
161                let name = e.name();
162                let local = local_name(name.as_ref());
163                if state == 2 {
164                    if local == b"sp" {
165                        // 自闭合 sp(极少见)—— 不处理
166                    } else if local == b"pic" {
167                        // 自闭合 pic —— 跳过
168                    }
169                }
170            }
171            Ok(Event::End(e)) => {
172                let name = e.name();
173                let local = local_name(name.as_ref());
174                if state == 1 && local == b"sld" {
175                    break;
176                } else if state == 2 && local == b"spTree" {
177                    state = 1;
178                }
179            }
180            Ok(Event::Eof) => break,
181            Err(e) => return Err(crate::Error::Xml(format!("sld parse: {e}"))),
182            _ => {}
183        }
184        buf.clear();
185    }
186    // 解析 sp(最常见类型)
187    for s in sp_bufs {
188        match parse_sp(&s) {
189            Ok(sp) => sld.shapes.push(OxmlSlideShape::Sp(sp)),
190            Err(e) => {
191                // 容错:单个 sp 解析失败不致命,但记入 result
192                return Err(e);
193            }
194        }
195    }
196    // 解析 pic(图片)
197    for p in pic_bufs {
198        if let Ok(pic) = parse_pic(&p) {
199            sld.shapes.push(OxmlSlideShape::Pic(pic));
200        }
201        // 解析失败时直接跳过该 pic —— 重要:避免一次失败拖垮整张 slide
202    }
203    // 解析 cxnSp(连接器)
204    for c in cxn_bufs {
205        if let Ok(cxn) = parse_cxn_sp(&c) {
206            sld.shapes.push(OxmlSlideShape::CxnSp(cxn));
207        }
208    }
209    // 解析 grpSp(组合形状,递归)
210    for g in grp_bufs {
211        if let Ok(grp) = parse_grp_sp(&g) {
212            sld.shapes.push(OxmlSlideShape::Group(Box::new(grp)));
213        }
214    }
215    // 解析 graphicFrame(图形框:表格/图表)
216    for f in gfx_bufs {
217        if let Ok(frame) = parse_graphic_frame(&f) {
218            sld.shapes.push(OxmlSlideShape::GraphicFrame(frame));
219        }
220    }
221    Ok(sld)
222}
223
224/// 解析 `<p:transition>` 元素(TODO-020:幻灯片过渡)。
225///
226/// # 元素结构
227///
228/// ```text
229/// <p:transition spd="slow|med|fast" advClick="0|1" advTm="...">
230///   <p:fade thruBlk="1"/>       ← 或 push/wipe/split/cover/pull/cut/zoom/morph
231/// </p:transition>
232/// ```
233///
234/// # 参数
235/// - `xml`:包含 `<p:transition>...</p:transition>` 的完整 XML 片段。
236///
237/// # 返回值
238/// - 成功:返回 [`Transition`];失败:返回 [`crate::Error::Xml`]。
239pub fn parse_transition(xml: &str) -> crate::Result<Transition> {
240    let mut rd = Reader::from_str(xml);
241    rd.config_mut().trim_text(true);
242    let mut buf = Vec::new();
243    let mut tr = Transition::default();
244    // 默认点击换片为 true(OOXML 默认值)
245    tr.advance_click = true;
246    let mut in_transition = false;
247
248    loop {
249        match rd.read_event_into(&mut buf) {
250            Ok(Event::Start(e)) => {
251                let name = e.name();
252                let local = local_name(name.as_ref());
253                if !in_transition && local == b"transition" {
254                    in_transition = true;
255                    // 读取 <p:transition> 上的属性
256                    for a in e.attributes().flatten() {
257                        let key = a.key.as_ref();
258                        let v = a
259                            .normalized_value(quick_xml::XmlVersion::Implicit1_0)
260                            .unwrap_or_default();
261                        match key {
262                            b"spd" => {
263                                tr.speed = match v.as_ref() {
264                                    "slow" => TransitionSpeed::Slow,
265                                    "fast" => TransitionSpeed::Fast,
266                                    _ => TransitionSpeed::Medium,
267                                };
268                            }
269                            b"advClick" => {
270                                tr.advance_click = v == "1" || v == "true";
271                            }
272                            b"advTm" => {
273                                tr.advance_after_ms = v.parse::<u32>().ok();
274                            }
275                            _ => {}
276                        }
277                    }
278                } else if in_transition {
279                    // 解析过渡类型子元素
280                    parse_transition_type_child(local, &e, &mut tr, false)?;
281                }
282            }
283            Ok(Event::Empty(e)) => {
284                let name = e.name();
285                let local = local_name(name.as_ref());
286                if !in_transition && local == b"transition" {
287                    // 自闭合 <p:transition/>:无子元素,使用默认值
288                    in_transition = true;
289                    for a in e.attributes().flatten() {
290                        let key = a.key.as_ref();
291                        let v = a
292                            .normalized_value(quick_xml::XmlVersion::Implicit1_0)
293                            .unwrap_or_default();
294                        match key {
295                            b"spd" => {
296                                tr.speed = match v.as_ref() {
297                                    "slow" => TransitionSpeed::Slow,
298                                    "fast" => TransitionSpeed::Fast,
299                                    _ => TransitionSpeed::Medium,
300                                };
301                            }
302                            b"advClick" => {
303                                tr.advance_click = v == "1" || v == "true";
304                            }
305                            b"advTm" => {
306                                tr.advance_after_ms = v.parse::<u32>().ok();
307                            }
308                            _ => {}
309                        }
310                    }
311                } else if in_transition {
312                    // 自闭合过渡类型子元素
313                    parse_transition_type_child(local, &e, &mut tr, true)?;
314                }
315            }
316            Ok(Event::End(e)) => {
317                let name = e.name();
318                let local = local_name(name.as_ref());
319                if in_transition && local == b"transition" {
320                    break;
321                }
322            }
323            Ok(Event::Eof) => break,
324            Err(e) => return Err(crate::Error::Xml(format!("transition parse: {e}"))),
325            _ => {}
326        }
327        buf.clear();
328    }
329    Ok(tr)
330}
331
332/// 解析过渡类型子元素(fade/push/wipe/split/cover/pull/cut/zoom/morph)。
333///
334/// # 参数
335/// - `local`:元素本地名(不含命名空间前缀)。
336/// - `e`:XML 事件(Start 或 Empty)。
337/// - `tr`:待填充的 [`Transition`]。
338/// - `is_empty`:是否为自闭合元素(Empty 事件)。
339fn parse_transition_type_child(
340    local: &[u8],
341    e: &quick_xml::events::BytesStart<'_>,
342    tr: &mut Transition,
343    _is_empty: bool,
344) -> crate::Result<()> {
345    // 提取属性辅助闭包
346    let attr = |key: &[u8]| -> Option<String> {
347        for a in e.attributes().flatten() {
348            if a.key.as_ref() == key {
349                return Some(
350                    a.normalized_value(quick_xml::XmlVersion::Implicit1_0)
351                        .unwrap_or_default()
352                        .to_string(),
353                );
354            }
355        }
356        None
357    };
358
359    match local {
360        b"fade" => {
361            let thru_blk = attr(b"thruBlk")
362                .map(|v| v == "1" || v == "true")
363                .unwrap_or(false);
364            tr.transition_type = TransitionType::Fade { thru_blk };
365        }
366        b"push" => {
367            let dir = parse_transition_dir(&attr(b"dir").unwrap_or_default());
368            tr.transition_type = TransitionType::Push { dir };
369        }
370        b"wipe" => {
371            let dir = parse_transition_dir(&attr(b"dir").unwrap_or_default());
372            tr.transition_type = TransitionType::Wipe { dir };
373        }
374        b"split" => {
375            let orient = match attr(b"orient").as_deref() {
376                Some("vert") => SplitOrientation::Vertical,
377                _ => SplitOrientation::Horizontal,
378            };
379            let dir = parse_transition_dir(&attr(b"dir").unwrap_or_default());
380            tr.transition_type = TransitionType::Split { orient, dir };
381        }
382        b"cover" => {
383            let dir = parse_transition_dir(&attr(b"dir").unwrap_or_default());
384            tr.transition_type = TransitionType::Cover { dir };
385        }
386        b"pull" => {
387            let dir = parse_transition_dir(&attr(b"dir").unwrap_or_default());
388            tr.transition_type = TransitionType::Pull { dir };
389        }
390        b"cut" => {
391            let thru_blk = attr(b"thruBlk")
392                .map(|v| v == "1" || v == "true")
393                .unwrap_or(false);
394            tr.transition_type = TransitionType::Cut { thru_blk };
395        }
396        b"zoom" => {
397            let dir = parse_transition_dir(&attr(b"dir").unwrap_or_default());
398            tr.transition_type = TransitionType::Zoom { dir };
399        }
400        b"morph" => {
401            let option = match attr(b"option").as_deref() {
402                Some("byWord") => MorphOption::ByWord,
403                Some("byChar") => MorphOption::ByChar,
404                _ => MorphOption::ByObject,
405            };
406            tr.transition_type = TransitionType::Morph { option };
407        }
408        _ => {
409            // 未知过渡类型,保持默认(None)
410        }
411    }
412    Ok(())
413}
414
415/// 解析过渡方向字符串为 [`TransitionDirection`]。
416fn parse_transition_dir(s: &str) -> TransitionDirection {
417    match s {
418        "l" => TransitionDirection::Left,
419        "r" => TransitionDirection::Right,
420        "u" => TransitionDirection::Up,
421        "d" => TransitionDirection::Down,
422        "lu" => TransitionDirection::LeftUp,
423        "ld" => TransitionDirection::LeftDown,
424        "ru" => TransitionDirection::RightUp,
425        "rd" => TransitionDirection::RightDown,
426        _ => TransitionDirection::Right,
427    }
428}
429
430/// 从 `slideN.xml.rels` 文本中查找 `rId` 对应的 `Target`。
431///
432/// # 错误
433/// - [`crate::Error::Xml`]:XML 解析失败。
434/// - `Error::Opc`:`rId` 不存在。
435pub fn find_relationship_target(rels_xml: &str, rid: &str) -> crate::Result<String> {
436    let rels = crate::opc::rels::Relationships::from_xml(rels_xml)?;
437    rels.get(rid)
438        .map(|r| r.target.as_str().to_string())
439        .ok_or_else(|| crate::Error::opc(format!("relationship not found: {rid}")))
440}
441
442/// 从 `slideN.xml.rels` 中枚举所有关系(rid → target 路径)。
443pub fn all_relationships(rels_xml: &str) -> crate::Result<Vec<(String, String, String)>> {
444    // 返回 (rid, reltype_uri, target)
445    use crate::opc::rels::Relationships;
446    let rels = Relationships::from_xml(rels_xml)?;
447    Ok(rels
448        .iter()
449        .map(|r| {
450            (
451                r.id.clone(),
452                r.reltype.uri().to_string(),
453                r.target.as_str().to_string(),
454            )
455        })
456        .collect())
457}
458
459/// 从一段 XML 文本中解析 `p:sp` 元素(包含开始 + 子元素 + 结束)。
460pub fn parse_sp(xml: &str) -> crate::Result<Sp> {
461    let mut rd = Reader::from_str(xml);
462    rd.config_mut().trim_text(true);
463    let mut buf = Vec::new();
464    let mut sp = Sp::default();
465    let mut state = 0u8; // 0: top, 1: nvSpPr, 2: spPr, 3: txBody
466
467    loop {
468        match rd.read_event_into(&mut buf) {
469            Ok(Event::Start(e)) => {
470                let name = e.name();
471                let local = local_name(name.as_ref());
472                match state {
473                    0 if local == b"sp" => {
474                        state = 1;
475                    }
476                    1 if local == b"nvSpPr" => {
477                        // 手动遍历 nvSpPr 子元素提取 cNvPr/cNvSpPr/ph 属性,
478                        // 不使用 collect_full_element_skipping(否则 Event::Empty 中的
479                        // cNvPr 不会被处理)
480                        let mut nv_depth = 1i32;
481                        loop {
482                            match rd.read_event_into(&mut buf) {
483                                Ok(Event::Start(e2)) => {
484                                    nv_depth += 1;
485                                    // cNvSpPr 以 Start 事件出现时(含子元素如 spLocks),
486                                    // 需要提取 txBox 属性
487                                    let name2 = e2.name();
488                                    let local2 = local_name(name2.as_ref());
489                                    if local2 == b"cNvSpPr" {
490                                        for a in e2.attributes().flatten() {
491                                            if a.key.as_ref() == b"txBox" {
492                                                let v = a
493                                                    .normalized_value(
494                                                        quick_xml::XmlVersion::Implicit1_0,
495                                                    )
496                                                    .unwrap_or_default();
497                                                if v == "1" || v == "true" {
498                                                    sp.c_nv_sp_pr_tx_box = true;
499                                                }
500                                            }
501                                        }
502                                    }
503                                }
504                                Ok(Event::End(_)) => {
505                                    nv_depth -= 1;
506                                    if nv_depth == 0 {
507                                        break;
508                                    }
509                                }
510                                Ok(Event::Empty(e2)) => {
511                                    let name2 = e2.name();
512                                    let local2 = local_name(name2.as_ref());
513                                    if local2 == b"cNvPr" {
514                                        for a in e2.attributes().flatten() {
515                                            match a.key.as_ref() {
516                                                b"id" => {
517                                                    if let Ok(v) = a
518                                                        .normalized_value(
519                                                            quick_xml::XmlVersion::Implicit1_0,
520                                                        )
521                                                        .unwrap_or_default()
522                                                        .parse::<u32>()
523                                                    {
524                                                        sp.id = v;
525                                                    }
526                                                }
527                                                b"name" => {
528                                                    sp.name = a
529                                                        .normalized_value(
530                                                            quick_xml::XmlVersion::Implicit1_0,
531                                                        )
532                                                        .unwrap_or_default()
533                                                        .to_string();
534                                                }
535                                                _ => {}
536                                            }
537                                        }
538                                    } else if local2 == b"cNvSpPr" {
539                                        for a in e2.attributes().flatten() {
540                                            if a.key.as_ref() == b"txBox" {
541                                                let v = a
542                                                    .normalized_value(
543                                                        quick_xml::XmlVersion::Implicit1_0,
544                                                    )
545                                                    .unwrap_or_default();
546                                                if v == "1" || v == "true" {
547                                                    sp.c_nv_sp_pr_tx_box = true;
548                                                }
549                                            }
550                                        }
551                                    } else if local2 == b"spLocks" {
552                                        // <a:spLocks noGrp="1" noSelect="1"/>
553                                        sp.locks = Some(parse_sp_locks_attrs(&e2));
554                                    } else if local2 == b"ph" {
555                                        sp.is_placeholder = true;
556                                        for a in e2.attributes().flatten() {
557                                            match a.key.as_ref() {
558                                                b"type" => {
559                                                    sp.ph_type = Some(
560                                                        String::from_utf8_lossy(a.value.as_ref())
561                                                            .into_owned(),
562                                                    )
563                                                }
564                                                b"idx" => {
565                                                    if let Ok(v) = a
566                                                        .normalized_value(
567                                                            quick_xml::XmlVersion::Implicit1_0,
568                                                        )
569                                                        .unwrap_or_default()
570                                                        .parse::<u32>()
571                                                    {
572                                                        sp.ph_idx = Some(v);
573                                                    }
574                                                }
575                                                _ => {}
576                                            }
577                                        }
578                                    }
579                                }
580                                Ok(Event::Eof) => break,
581                                Err(_) => break,
582                                _ => {}
583                            }
584                            buf.clear();
585                        }
586                    }
587                    1 if local == b"spPr" => {
588                        let inner = collect_full_element(&mut rd, e.into_owned())?;
589                        // spPr 解析(独立子解析器),直接使用 inner 避免从 Option 中 unwrap
590                        sp.properties = parse_sppr(&inner)?;
591                        state = 2;
592                    }
593                    _ if state < 2 && local == b"spPr" => {
594                        let inner = collect_full_element(&mut rd, e.into_owned())?;
595                        sp.properties = parse_sppr(&inner)?;
596                        state = 2;
597                    }
598                    2 if local == b"txBody" => {
599                        let inner = collect_full_element(&mut rd, e.into_owned())?;
600                        // 直接使用 inner 避免从 Option 中 unwrap
601                        sp.text = parse_txbody(&inner)?;
602                        state = 3;
603                    }
604                    _ if state >= 2 && local == b"style" => {
605                        // <p:style> 在 spPr 之后、txBody 之前(或之后)出现
606                        let inner = collect_full_element(&mut rd, e.into_owned())?;
607                        sp.style = Some(parse_shape_style(&inner)?);
608                    }
609                    _ => {
610                        // 其它元素:吞掉
611                        let _ = collect_full_element(&mut rd, e.into_owned());
612                    }
613                }
614            }
615            Ok(Event::Empty(e)) => {
616                let name = e.name();
617                let local = local_name(name.as_ref());
618                if state == 1 && local == b"ph" {
619                    // 占位符(自闭合)
620                    sp.is_placeholder = true;
621                    for a in e.attributes().flatten() {
622                        match a.key.as_ref() {
623                            b"type" => {
624                                sp.ph_type =
625                                    Some(String::from_utf8_lossy(a.value.as_ref()).into_owned())
626                            }
627                            b"idx" => {
628                                if let Ok(v) = a
629                                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
630                                    .unwrap_or_default()
631                                    .parse::<u32>()
632                                {
633                                    sp.ph_idx = Some(v);
634                                }
635                            }
636                            _ => {}
637                        }
638                    }
639                } else if state == 1 && local == b"cNvPr" {
640                    // cNvPr 自闭合(在 nvSpPr 内部)
641                    for a in e.attributes().flatten() {
642                        match a.key.as_ref() {
643                            b"id" => {
644                                if let Ok(v) = a
645                                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
646                                    .unwrap_or_default()
647                                    .parse::<u32>()
648                                {
649                                    sp.id = v;
650                                }
651                            }
652                            b"name" => {
653                                sp.name = a
654                                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
655                                    .unwrap_or_default()
656                                    .to_string();
657                            }
658                            _ => {}
659                        }
660                    }
661                } else if state == 1 && local == b"cNvSpPr" {
662                    // cNvSpPr 自闭合
663                    for a in e.attributes().flatten() {
664                        if a.key.as_ref() == b"txBox" {
665                            let v = a
666                                .normalized_value(quick_xml::XmlVersion::Implicit1_0)
667                                .unwrap_or_default();
668                            if v == "1" || v == "true" {
669                                sp.c_nv_sp_pr_tx_box = true;
670                            }
671                        }
672                    }
673                } else if state == 1 && local == b"spPr" {
674                    // 自闭合 <p:spPr/>:无子元素,直接转到 state 2,
675                    // 以便后续 <p:style> 能被正确匹配(state >= 2 守卫)
676                    state = 2;
677                } else if state == 2 && local == b"prstGeom" {
678                    for a in e.attributes().flatten() {
679                        if a.key.as_ref() == b"prst" {
680                            let v = a
681                                .normalized_value(quick_xml::XmlVersion::Implicit1_0)
682                                .unwrap_or_default();
683                            if let Ok(g) = v.parse::<PresetGeometry>() {
684                                sp.properties.geometry = Some(Geometry::preset(g));
685                            }
686                        }
687                    }
688                } else if state == 2 && local == b"custGeom" {
689                    // 自定义几何(TODO-024)
690                    let inner = collect_full_element(&mut rd, e.into_owned())?;
691                    if let Ok(cg) = parse_custom_geometry(&inner) {
692                        sp.properties.geometry = Some(Geometry::Custom(cg));
693                    }
694                } else if state >= 2 && local == b"style" {
695                    // 自闭合 <p:style/>:无子元素,创建空的 ShapeStyle
696                    sp.style = Some(ShapeStyle::default());
697                }
698            }
699            Ok(Event::End(e)) => {
700                let name = e.name();
701                let local = local_name(name.as_ref());
702                if state == 3 && local == b"sp" {
703                    break;
704                }
705                if state == 2 && local == b"sp" {
706                    // 没有 txBody
707                    break;
708                }
709                if state == 1 && local == b"sp" {
710                    break;
711                }
712            }
713            Ok(Event::Eof) => break,
714            Err(e) => return Err(crate::Error::Xml(format!("sp parse: {e}"))),
715            _ => {}
716        }
717        buf.clear();
718    }
719    // 兜底:若 spPr 解析失败,properties 已是默认
720    Ok(sp)
721}
722
723/// 解析 `p:spPr` 元素。
724pub fn parse_sppr(xml: &str) -> crate::Result<ShapeProperties> {
725    let mut rd = Reader::from_str(xml);
726    rd.config_mut().trim_text(true);
727    let mut buf = Vec::new();
728    let mut sp = ShapeProperties::default();
729    loop {
730        match rd.read_event_into(&mut buf) {
731            Ok(Event::Start(e)) => {
732                let name = e.name();
733                let local = local_name(name.as_ref());
734                if local == b"spPr" {
735                    // 根元素 <p:spPr>:仅标记进入,不调用 collect_full_element
736                    // (否则会吞掉所有子元素,导致 xfrm / prstGeom 等无法解析)
737                } else if local == b"xfrm" {
738                    // 先提取 xfrm 自身的属性(rot/flipH/flipV)
739                    for a in e.attributes().flatten() {
740                        match a.key.as_ref() {
741                            b"rot" => {
742                                if let Ok(v) = a
743                                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
744                                    .unwrap_or_default()
745                                    .parse::<i32>()
746                                {
747                                    sp.xfrm.rot = Some(v);
748                                }
749                            }
750                            b"flipH" => {
751                                let v = a
752                                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
753                                    .unwrap_or_default();
754                                if v == "1" {
755                                    sp.xfrm.flip_h = true;
756                                }
757                            }
758                            b"flipV" => {
759                                let v = a
760                                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
761                                    .unwrap_or_default();
762                                if v == "1" {
763                                    sp.xfrm.flip_v = true;
764                                }
765                            }
766                            _ => {}
767                        }
768                    }
769                    // 再解析子元素(off/ext)
770                    parse_xfrm_into(&mut rd, &mut sp);
771                } else if local == b"prstGeom" {
772                    // 先提取 prst 属性,再收集完整元素以解析 <a:avLst>
773                    let mut prst_val: Option<String> = None;
774                    for a in e.attributes().flatten() {
775                        if a.key.as_ref() == b"prst" {
776                            prst_val = Some(
777                                a.normalized_value(quick_xml::XmlVersion::Implicit1_0)
778                                    .unwrap_or_default()
779                                    .to_string(),
780                            );
781                        }
782                    }
783                    let inner = collect_full_element(&mut rd, e.into_owned())?;
784                    if let Some(v) = prst_val {
785                        if let Ok(g) = v.parse::<PresetGeometry>() {
786                            let adjustments = parse_av_lst(&inner);
787                            sp.geometry = Some(Geometry::Preset(g, adjustments));
788                        }
789                    }
790                } else if local == b"custGeom" {
791                    // 自定义几何(TODO-024)
792                    let inner = collect_full_element(&mut rd, e.into_owned())?;
793                    if let Ok(cg) = parse_custom_geometry(&inner) {
794                        sp.geometry = Some(Geometry::Custom(cg));
795                    }
796                } else if local == b"noFill" {
797                    sp.fill = Fill::None;
798                } else if local == b"solidFill" {
799                    // 解析 a:solidFill 子元素 a:srgbClr / a:schemeClr
800                    let color = parse_solid_fill(&mut rd, e.into_owned())?;
801                    sp.fill = Fill::Solid(color);
802                } else if local == b"gradFill" {
803                    // 渐变填充:<a:gradFill><a:gsLst>...</a:gsLst><a:lin/>或<a:path/></a:gradFill>
804                    let grad = parse_grad_fill(&mut rd, e.into_owned())?;
805                    sp.fill = Fill::Gradient(grad);
806                } else if local == b"pattFill" {
807                    // 图案填充:<a:pattFill prst="..."><a:fgClr/>...<a:bgClr/>...</a:pattFill>
808                    let patt = parse_patt_fill(&mut rd, e.into_owned())?;
809                    sp.fill = Fill::Pattern(patt);
810                } else if local == b"blipFill" {
811                    // 图片填充(TODO-003/048):解析 rid + mode,组装为 Fill::Blip。
812                    // 注意:rid 仅是关系 id 字符串(如 "rId1"),实际 partname 解析
813                    // 需要在外层 from_opc 路径中通过 slideN.xml.rels 映射——这里只保留 rid。
814                    let (rid, mode) = parse_blip_fill(&mut rd, e.into_owned())?;
815                    if !rid.is_empty() {
816                        sp.fill = Fill::Blip { rid, mode };
817                    } else {
818                        // rid 缺失:保持 Inherit(避免写出无效的 Fill::Blip)
819                    }
820                } else if local == b"ln" {
821                    let ln = parse_ln(&mut rd, e.into_owned())?;
822                    sp.line = Some(ln);
823                } else if local == b"effectLst" {
824                    // 效果列表(TODO-011:形状效果)
825                    let effects = parse_effect_lst(&mut rd, e.into_owned())?;
826                    sp.effects = Some(effects);
827                } else if local == b"scene3d" {
828                    // 三维场景(TODO-050)
829                    let scene = parse_scene_3d(&mut rd, e.into_owned())?;
830                    sp.scene3d = Some(scene);
831                } else if local == b"sp3d" {
832                    // 形状 3D 属性(TODO-050)
833                    let sp3d = parse_sp_3d(&mut rd, e.into_owned())?;
834                    sp.sp3d = Some(sp3d);
835                } else {
836                    let _ = collect_full_element(&mut rd, e.into_owned());
837                }
838            }
839            Ok(Event::Empty(e)) => {
840                let name = e.name();
841                let local = local_name(name.as_ref());
842                if local == b"prstGeom" {
843                    // 自闭合 <a:prstGeom prst="..."/>:无 avLst,调整值为空
844                    for a in e.attributes().flatten() {
845                        if a.key.as_ref() == b"prst" {
846                            let v = a
847                                .normalized_value(quick_xml::XmlVersion::Implicit1_0)
848                                .unwrap_or_default();
849                            if let Ok(g) = v.parse::<PresetGeometry>() {
850                                sp.geometry = Some(Geometry::preset(g));
851                            }
852                        }
853                    }
854                } else if local == b"noFill" {
855                    sp.fill = Fill::None;
856                } else if local == b"custGeom" {
857                    // 自闭合 <a:custGeom/>:罕见,用空自定义几何
858                    sp.geometry = Some(Geometry::Custom(CustomGeometry::default()));
859                }
860            }
861            Ok(Event::Eof) => break,
862            Err(e) => return Err(crate::Error::Xml(format!("spPr parse: {e}"))),
863            _ => {}
864        }
865        buf.clear();
866    }
867    Ok(sp)
868}
869
870/// 解析 `a:xfrm`,就地更新 `ShapeProperties.xfrm`。
871///
872/// # 元素顺序(OOXML)
873///
874/// ```text
875/// <a:xfrm rot="..." flipH="1" flipV="1">     ← 可选属性
876///   <a:off x="..." y="..."/>                 ← 可选
877///   <a:ext cx="..." cy="..."/>                ← 可选
878/// </a:xfrm>
879/// ```
880/// 解析 `a:xfrm` 的子元素(off/ext),就地更新 `ShapeProperties.xfrm`。
881///
882/// # 调用约定
883/// 调用方**必须**已消费 `<a:xfrm>` 的 Start 事件。本函数从 xfrm 的第一个子元素
884/// 开始读取,直到遇到 `</a:xfrm>` End 事件为止。
885///
886/// xfrm 自身的属性(rot/flipH/flipV)由调用方从 Start 事件中提取,
887/// 在调用本函数之前设置到 `sp.xfrm` 上。
888///
889/// # 元素顺序(OOXML)
890///
891/// ```text
892/// <a:xfrm rot="..." flipH="1" flipV="1">     ← 属性由调用方处理
893///   <a:off x="..." y="..."/>                 ← 可选,自闭合
894///   <a:ext cx="..." cy="..."/>                ← 可选,自闭合
895/// </a:xfrm>
896/// ```
897fn parse_xfrm_into<R: std::io::BufRead>(rd: &mut Reader<R>, sp: &mut ShapeProperties) {
898    let mut buf = Vec::new();
899    // 遍历 xfrm 的子元素:off / ext(均为自闭合 Empty 事件)
900    loop {
901        match rd.read_event_into(&mut buf) {
902            Ok(Event::Empty(e)) => {
903                let name = e.name();
904                let local = local_name(name.as_ref());
905                if local == b"off" {
906                    let (mut x, mut y) = (None, None);
907                    for a in e.attributes().flatten() {
908                        match a.key.as_ref() {
909                            b"x" => {
910                                x = a
911                                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
912                                    .unwrap_or_default()
913                                    .parse()
914                                    .ok()
915                            }
916                            b"y" => {
917                                y = a
918                                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
919                                    .unwrap_or_default()
920                                    .parse()
921                                    .ok()
922                            }
923                            _ => {}
924                        }
925                    }
926                    sp.xfrm.off_x = x.map(Emu);
927                    sp.xfrm.off_y = y.map(Emu);
928                } else if local == b"ext" {
929                    let (mut cx, mut cy) = (None, None);
930                    for a in e.attributes().flatten() {
931                        match a.key.as_ref() {
932                            b"cx" => {
933                                cx = a
934                                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
935                                    .unwrap_or_default()
936                                    .parse()
937                                    .ok()
938                            }
939                            b"cy" => {
940                                cy = a
941                                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
942                                    .unwrap_or_default()
943                                    .parse()
944                                    .ok()
945                            }
946                            _ => {}
947                        }
948                    }
949                    sp.xfrm.ext_cx = cx.map(Emu);
950                    sp.xfrm.ext_cy = cy.map(Emu);
951                }
952                // chOff / chExt 在组合形状中由 parse_grp_sppr_into 单独处理
953            }
954            Ok(Event::End(e)) => {
955                if local_name(e.name().as_ref()) == b"xfrm" {
956                    return;
957                }
958            }
959            Ok(Event::Eof) => return,
960            _ => {}
961        }
962        buf.clear();
963    }
964}
965
966/// 解析 `<a:prstGeom>` 内的 `<a:avLst>` 调整值列表。
967///
968/// 从 `collect_full_element` 产生的完整 `<a:prstGeom>` XML 中提取 `<a:gd>` 元素。
969///
970/// # 参数
971/// - `xml`:包含 `<a:prstGeom>` 根元素的完整 XML 字符串。
972///
973/// # 返回值
974/// 调整值列表。无 `<a:avLst>` 或 `<a:avLst/>` 为空时返回空 Vec。
975fn parse_av_lst(xml: &str) -> Vec<crate::oxml::sppr::AdjustmentValue> {
976    use crate::oxml::sppr::AdjustmentValue;
977
978    let mut result = Vec::new();
979    let mut rd = Reader::from_str(xml);
980    let mut buf = Vec::new();
981    loop {
982        match rd.read_event_into(&mut buf) {
983            Ok(Event::Empty(e)) => {
984                if local_name(e.name().as_ref()) == b"gd" {
985                    let mut name = String::new();
986                    let mut fmla = String::new();
987                    for a in e.attributes().flatten() {
988                        let v = a
989                            .normalized_value(quick_xml::XmlVersion::Implicit1_0)
990                            .unwrap_or_default();
991                        match a.key.as_ref() {
992                            b"name" => name = v.to_string(),
993                            b"fmla" => fmla = v.to_string(),
994                            _ => {}
995                        }
996                    }
997                    // 解析 fmla="val <number>" 格式
998                    if let Some(raw) = parse_fmla_val(&fmla) {
999                        result.push(AdjustmentValue::new(name, raw));
1000                    }
1001                }
1002            }
1003            Ok(Event::Start(e)) => {
1004                if local_name(e.name().as_ref()) == b"gd" {
1005                    let mut name = String::new();
1006                    let mut fmla = String::new();
1007                    for a in e.attributes().flatten() {
1008                        let v = a
1009                            .normalized_value(quick_xml::XmlVersion::Implicit1_0)
1010                            .unwrap_or_default();
1011                        match a.key.as_ref() {
1012                            b"name" => name = v.to_string(),
1013                            b"fmla" => fmla = v.to_string(),
1014                            _ => {}
1015                        }
1016                    }
1017                    // 跳过子元素(gd 通常无子元素,但保险起见)
1018                    let _ = collect_full_element(&mut rd, e.into_owned());
1019                    if let Some(raw) = parse_fmla_val(&fmla) {
1020                        result.push(AdjustmentValue::new(name, raw));
1021                    }
1022                }
1023            }
1024            Ok(Event::Eof) => break,
1025            _ => {}
1026        }
1027        buf.clear();
1028    }
1029    result
1030}
1031
1032/// 解析 `fmla="val <number>"` 格式的公式,提取数值部分。
1033///
1034/// 支持常见格式 `val 16667`(返回 `Some(16667)`)。
1035/// 不支持复杂公式(如 `*/ adj1 100000 50000`),返回 `None`。
1036fn parse_fmla_val(fmla: &str) -> Option<i64> {
1037    let trimmed = fmla.trim();
1038    if let Some(rest) = trimmed.strip_prefix("val ") {
1039        rest.trim().parse::<i64>().ok()
1040    } else if let Some(rest) = trimmed.strip_prefix("val") {
1041        rest.trim().parse::<i64>().ok()
1042    } else {
1043        None
1044    }
1045}
1046
1047/// 解析 `<a:custGeom>...</a:custGeom>` 内部结构为 [`CustomGeometry`]。
1048///
1049/// # 元素顺序(OOXML 规范)
1050///
1051/// ```text
1052/// <a:custGeom>
1053///   <a:avLst/>          ← 调整手柄列表(暂跳过)
1054///   <a:fill>...</a:fill> ← 可选
1055///   <a:stroke>...</a:stroke> ← 可选
1056///   <a:rect l="..." t="..." r="..." b="..."/> ← 可选
1057///   <a:pathLst>
1058///     <a:path w="..." h="..." fill="..." stroke="...">
1059///       <a:moveTo>|<a:lnTo>|<a:cubicBezTo>|<a:quadBezTo>|<a:arcTo>|<a:close/>
1060///     </a:path>
1061///   </a:pathLst>
1062/// </a:custGeom>
1063/// ```
1064///
1065/// # 参数
1066/// - `xml`:包含 `<a:custGeom>` 根元素的完整 XML 字符串(由 `collect_full_element` 产生)。
1067///
1068/// # 返回值
1069/// - 成功:返回 [`CustomGeometry`];失败:返回 `Error::Xml`。
1070fn parse_custom_geometry(xml: &str) -> crate::Result<CustomGeometry> {
1071    let mut rd = Reader::from_str(xml);
1072    rd.config_mut().trim_text(true);
1073    let mut buf = Vec::new();
1074    let mut geom = CustomGeometry::default();
1075    // 状态:0 = 顶层(寻找 custGeom 子元素),1 = 在 pathLst 内,2 = 在 path 内
1076    let mut state: u8 = 0;
1077    let mut current_path: Option<Path> = None;
1078    // 用于 cubicBezTo / quadBezTo 累积控制点
1079    let mut bez_pts: Vec<(i64, i64)> = Vec::new();
1080
1081    loop {
1082        match rd.read_event_into(&mut buf) {
1083            Ok(Event::Start(e)) => {
1084                let ev_name = e.name();
1085                let local = local_name(ev_name.as_ref());
1086                match state {
1087                    0 => match local {
1088                        b"fill" => {
1089                            // 读取文本内容
1090                            let text = read_element_text(&mut rd, b"fill");
1091                            geom.fill = Some(text);
1092                        }
1093                        b"stroke" => {
1094                            let text = read_element_text(&mut rd, b"stroke");
1095                            geom.stroke = Some(text);
1096                        }
1097                        b"rect" => {
1098                            // rect 是 Empty 元素,但若以 Start 形式出现也处理属性
1099                            geom.rect = Some(parse_geom_rect(&e));
1100                        }
1101                        b"pathLst" => {
1102                            state = 1;
1103                        }
1104                        b"avLst" => {
1105                            // 调整手柄列表,跳过(collect_full_element 已吞掉子元素,
1106                            // 但此处是 Start 事件,需要继续读到 End)
1107                            // 不做处理,让循环自然推进
1108                        }
1109                        _ => {}
1110                    },
1111                    1 => {
1112                        if local == b"path" {
1113                            // 进入 path 元素,读取属性
1114                            let mut p = Path::default();
1115                            for a in e.attributes().flatten() {
1116                                let key = a.key.as_ref();
1117                                let v = a
1118                                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
1119                                    .unwrap_or_default()
1120                                    .to_string();
1121                                match key {
1122                                    b"w" => p.width = v.parse().unwrap_or(0),
1123                                    b"h" => p.height = v.parse().unwrap_or(0),
1124                                    b"fill" => p.fill = Some(v),
1125                                    b"stroke" => p.stroke = Some(v),
1126                                    _ => {}
1127                                }
1128                            }
1129                            current_path = Some(p);
1130                            state = 2;
1131                        }
1132                    }
1133                    2 => {
1134                        // 在 path 内,处理路径段
1135                        match local {
1136                            b"moveTo" | b"lnTo" => {
1137                                // 读取单个 pt 子元素
1138                                let pt = read_single_pt(&mut rd, local);
1139                                if let Some((x, y)) = pt {
1140                                    let seg = if local == b"moveTo" {
1141                                        PathSegment::MoveTo { x, y }
1142                                    } else {
1143                                        PathSegment::LineTo { x, y }
1144                                    };
1145                                    if let Some(p) = &mut current_path {
1146                                        p.segments.push(seg);
1147                                    }
1148                                }
1149                            }
1150                            b"cubicBezTo" => {
1151                                // 读取 3 个 pt 子元素
1152                                bez_pts.clear();
1153                                read_multiple_pts(&mut rd, b"cubicBezTo", &mut bez_pts);
1154                                if bez_pts.len() >= 3 {
1155                                    let seg = PathSegment::CubicBezTo {
1156                                        x1: bez_pts[0].0,
1157                                        y1: bez_pts[0].1,
1158                                        x2: bez_pts[1].0,
1159                                        y2: bez_pts[1].1,
1160                                        x3: bez_pts[2].0,
1161                                        y3: bez_pts[2].1,
1162                                    };
1163                                    if let Some(p) = &mut current_path {
1164                                        p.segments.push(seg);
1165                                    }
1166                                }
1167                            }
1168                            b"quadBezTo" => {
1169                                // 读取 2 个 pt 子元素
1170                                bez_pts.clear();
1171                                read_multiple_pts(&mut rd, b"quadBezTo", &mut bez_pts);
1172                                if bez_pts.len() >= 2 {
1173                                    let seg = PathSegment::QuadBezTo {
1174                                        x1: bez_pts[0].0,
1175                                        y1: bez_pts[0].1,
1176                                        x2: bez_pts[1].0,
1177                                        y2: bez_pts[1].1,
1178                                    };
1179                                    if let Some(p) = &mut current_path {
1180                                        p.segments.push(seg);
1181                                    }
1182                                }
1183                            }
1184                            b"arcTo" => {
1185                                // arcTo 是自闭合元素,属性直接在标签上
1186                                let seg = parse_arc_to(&e);
1187                                if let Some(p) = &mut current_path {
1188                                    p.segments.push(seg);
1189                                }
1190                            }
1191                            _ => {}
1192                        }
1193                    }
1194                    _ => {}
1195                }
1196            }
1197            Ok(Event::Empty(e)) => {
1198                let ev_name = e.name();
1199                let local = local_name(ev_name.as_ref());
1200                match state {
1201                    0 => {
1202                        if local == b"rect" {
1203                            geom.rect = Some(parse_geom_rect(&e));
1204                        }
1205                        // avLst 空元素、其它空元素均忽略
1206                    }
1207                    2 => {
1208                        if local == b"close" {
1209                            if let Some(p) = &mut current_path {
1210                                p.segments.push(PathSegment::Close);
1211                            }
1212                        } else if local == b"arcTo" {
1213                            let seg = parse_arc_to(&e);
1214                            if let Some(p) = &mut current_path {
1215                                p.segments.push(seg);
1216                            }
1217                        } else if local == b"moveTo" || local == b"lnTo" {
1218                            // 自闭合的 moveTo/lnTo(罕见,但规范允许)含一个 pt 子元素
1219                            // Empty 事件没有子元素,所以无法读取 pt,跳过
1220                        }
1221                    }
1222                    _ => {}
1223                }
1224            }
1225            Ok(Event::End(e)) => {
1226                let ev_name = e.name();
1227                let local = local_name(ev_name.as_ref());
1228                match state {
1229                    2 => {
1230                        if local == b"path" {
1231                            // path 结束,推入 path_list
1232                            if let Some(p) = current_path.take() {
1233                                geom.path_list.push(p);
1234                            }
1235                            state = 1;
1236                        }
1237                    }
1238                    1 => {
1239                        if local == b"pathLst" {
1240                            state = 0;
1241                        }
1242                    }
1243                    0 if local == b"custGeom" => {
1244                        return Ok(geom);
1245                    }
1246                    _ => {}
1247                }
1248            }
1249            Ok(Event::Eof) => return Ok(geom),
1250            Err(e) => return Err(crate::Error::Xml(format!("parse_custom_geometry: {e}"))),
1251            _ => {}
1252        }
1253        buf.clear();
1254    }
1255}
1256
1257/// 读取元素的文本内容(直到匹配的 End 事件)。
1258///
1259/// 用于 `<a:fill>norm</a:fill>` 这类文本叶子元素。
1260fn read_element_text<R: std::io::BufRead>(rd: &mut Reader<R>, expected_end: &[u8]) -> String {
1261    let mut buf = Vec::new();
1262    let mut text = String::new();
1263    loop {
1264        match rd.read_event_into(&mut buf) {
1265            Ok(Event::Text(t)) => {
1266                text.push_str(&t.decode().unwrap_or_default());
1267            }
1268            Ok(Event::End(e)) => {
1269                if local_name(e.name().as_ref()) == expected_end {
1270                    return text;
1271                }
1272            }
1273            Ok(Event::Eof) => return text,
1274            _ => {}
1275        }
1276        buf.clear();
1277    }
1278}
1279
1280/// 从 `<a:pt x="..." y="..."/>` 属性中读取坐标。
1281fn parse_geom_rect(e: &quick_xml::events::BytesStart<'_>) -> GeomRect {
1282    let mut r = GeomRect::default();
1283    for a in e.attributes().flatten() {
1284        let v = a
1285            .normalized_value(quick_xml::XmlVersion::Implicit1_0)
1286            .unwrap_or_default()
1287            .to_string();
1288        match a.key.as_ref() {
1289            b"l" => r.l = v,
1290            b"t" => r.t = v,
1291            b"r" => r.r = v,
1292            b"b" => r.b = v,
1293            _ => {}
1294        }
1295    }
1296    r
1297}
1298
1299/// 读取 moveTo / lnTo 内的单个 `<a:pt>` 子元素。
1300fn read_single_pt<R: std::io::BufRead>(rd: &mut Reader<R>, parent: &[u8]) -> Option<(i64, i64)> {
1301    let mut buf = Vec::new();
1302    let mut result = None;
1303    loop {
1304        match rd.read_event_into(&mut buf) {
1305            Ok(Event::Empty(e)) => {
1306                let ev_name = e.name();
1307                if local_name(ev_name.as_ref()) == b"pt" {
1308                    result = parse_pt_attrs(&e);
1309                }
1310            }
1311            Ok(Event::Start(e)) => {
1312                let ev_name = e.name();
1313                if local_name(ev_name.as_ref()) == b"pt" {
1314                    // pt 可能含子元素(罕见),读取属性后跳到 End
1315                    result = parse_pt_attrs(&e);
1316                }
1317            }
1318            Ok(Event::End(e)) => {
1319                if local_name(e.name().as_ref()) == parent {
1320                    return result;
1321                }
1322            }
1323            Ok(Event::Eof) => return result,
1324            _ => {}
1325        }
1326        buf.clear();
1327    }
1328}
1329
1330/// 读取 cubicBezTo / quadBezTo 内的多个 `<a:pt>` 子元素。
1331fn read_multiple_pts<R: std::io::BufRead>(
1332    rd: &mut Reader<R>,
1333    parent: &[u8],
1334    pts: &mut Vec<(i64, i64)>,
1335) {
1336    let mut buf = Vec::new();
1337    loop {
1338        match rd.read_event_into(&mut buf) {
1339            Ok(Event::Empty(e)) => {
1340                let ev_name = e.name();
1341                if local_name(ev_name.as_ref()) == b"pt" {
1342                    if let Some(p) = parse_pt_attrs(&e) {
1343                        pts.push(p);
1344                    }
1345                }
1346            }
1347            Ok(Event::Start(e)) => {
1348                let ev_name = e.name();
1349                if local_name(ev_name.as_ref()) == b"pt" {
1350                    if let Some(p) = parse_pt_attrs(&e) {
1351                        pts.push(p);
1352                    }
1353                }
1354            }
1355            Ok(Event::End(e)) => {
1356                if local_name(e.name().as_ref()) == parent {
1357                    return;
1358                }
1359            }
1360            Ok(Event::Eof) => return,
1361            _ => {}
1362        }
1363        buf.clear();
1364    }
1365}
1366
1367/// 从 `<a:pt>` 元素的属性中读取 (x, y) 坐标。
1368fn parse_pt_attrs(e: &quick_xml::events::BytesStart<'_>) -> Option<(i64, i64)> {
1369    let mut x: Option<i64> = None;
1370    let mut y: Option<i64> = None;
1371    for a in e.attributes().flatten() {
1372        let v = a
1373            .normalized_value(quick_xml::XmlVersion::Implicit1_0)
1374            .unwrap_or_default();
1375        match a.key.as_ref() {
1376            b"x" => x = v.parse().ok(),
1377            b"y" => y = v.parse().ok(),
1378            _ => {}
1379        }
1380    }
1381    match (x, y) {
1382        (Some(x), Some(y)) => Some((x, y)),
1383        _ => None,
1384    }
1385}
1386
1387/// 从 `<a:arcTo>` 元素的属性中读取弧线参数。
1388fn parse_arc_to(e: &quick_xml::events::BytesStart<'_>) -> PathSegment {
1389    let mut w_r: i64 = 0;
1390    let mut h_r: i64 = 0;
1391    let mut st_ang: i32 = 0;
1392    let mut sw_ang: i32 = 0;
1393    for a in e.attributes().flatten() {
1394        let v = a
1395            .normalized_value(quick_xml::XmlVersion::Implicit1_0)
1396            .unwrap_or_default();
1397        match a.key.as_ref() {
1398            b"wR" => w_r = v.parse().unwrap_or(0),
1399            b"hR" => h_r = v.parse().unwrap_or(0),
1400            b"stAng" => st_ang = v.parse().unwrap_or(0),
1401            b"swAng" => sw_ang = v.parse().unwrap_or(0),
1402            _ => {}
1403        }
1404    }
1405    PathSegment::ArcTo {
1406        w_r,
1407        h_r,
1408        st_ang,
1409        sw_ang,
1410    }
1411}
1412
1413/// 解析 `<a:solidFill>...</a:solidFill>` 内的颜色(必须包含 a:srgbClr 或 a:schemeClr)。
1414fn parse_solid_fill<R: std::io::BufRead>(
1415    rd: &mut Reader<R>,
1416    _start: quick_xml::events::BytesStart<'static>,
1417) -> crate::Result<Color> {
1418    let mut buf = Vec::new();
1419    // 读取第一个子元素
1420    loop {
1421        match rd.read_event_into(&mut buf) {
1422            Ok(Event::Empty(e)) => {
1423                let name = e.name();
1424                let local = local_name(name.as_ref());
1425                if local == b"srgbClr" {
1426                    for a in e.attributes().flatten() {
1427                        if a.key.as_ref() == b"val" {
1428                            let v = a
1429                                .normalized_value(quick_xml::XmlVersion::Implicit1_0)
1430                                .unwrap_or_default();
1431                            if v.len() == 6 {
1432                                let r = u8::from_str_radix(&v[0..2], 16).unwrap_or(0);
1433                                let g = u8::from_str_radix(&v[2..4], 16).unwrap_or(0);
1434                                let b = u8::from_str_radix(&v[4..6], 16).unwrap_or(0);
1435                                return Ok(Color::RGB(RGBColor(r, g, b)));
1436                            }
1437                        }
1438                    }
1439                } else if local == b"schemeClr" {
1440                    for a in e.attributes().flatten() {
1441                        if a.key.as_ref() == b"val" {
1442                            let v = a
1443                                .normalized_value(quick_xml::XmlVersion::Implicit1_0)
1444                                .unwrap_or_default();
1445                            if let Ok(sc) = v.parse::<SchemeColor>() {
1446                                return Ok(Color::Scheme(sc));
1447                            }
1448                        }
1449                    }
1450                } else if local == b"prstClr" {
1451                    for a in e.attributes().flatten() {
1452                        if a.key.as_ref() == b"val" {
1453                            let v = a
1454                                .normalized_value(quick_xml::XmlVersion::Implicit1_0)
1455                                .unwrap_or_default();
1456                            if let Ok(p) = v.parse::<PresetColor>() {
1457                                return Ok(Color::Preset(p));
1458                            }
1459                        }
1460                    }
1461                }
1462            }
1463            Ok(Event::End(e)) => {
1464                let name = e.name();
1465                if local_name(name.as_ref()) == b"solidFill" {
1466                    return Ok(Color::None);
1467                }
1468            }
1469            Ok(Event::Eof) => return Ok(Color::None),
1470            _ => {}
1471        }
1472        buf.clear();
1473    }
1474}
1475
1476/// 解析 `<a:gradFill>` 元素为 [`GradientFill`]。
1477///
1478/// # OOXML 结构
1479///
1480/// ```text
1481/// <a:gradFill flip="none" rotWithShape="1">
1482///   <a:gsLst>
1483///     <a:gs pos="0"><a:srgbClr val="FF0000"/></a:gs>
1484///     <a:gs pos="100000"><a:srgbClr val="00FF00"/></a:gs>
1485///   </a:gsLst>
1486///   <a:lin ang="5400000" scaled="1"/>     ← 线性渐变
1487///   <!-- 或 -->
1488///   <a:path path="circle">...</a:path>    ← 路径渐变
1489/// </a:gradFill>
1490/// ```
1491///
1492/// # 参数
1493/// - `rd`:已位于 `<a:gradFill>` Start 事件之后的 reader;
1494/// - `_start`:`<a:gradFill>` 的 Start 事件(用于提取 flip/rotWithShape 属性)。
1495fn parse_grad_fill<R: std::io::BufRead>(
1496    rd: &mut Reader<R>,
1497    start: quick_xml::events::BytesStart<'static>,
1498) -> crate::Result<GradientFill> {
1499    // 先从 gradFill 自身属性提取 flip / rotWithShape
1500    let mut flip = None;
1501    let mut rot_with_shape = None;
1502    for a in start.attributes().flatten() {
1503        match a.key.as_ref() {
1504            b"flip" => {
1505                flip = Some(
1506                    a.normalized_value(quick_xml::XmlVersion::Implicit1_0)
1507                        .unwrap_or_default()
1508                        .to_string(),
1509                );
1510            }
1511            b"rotWithShape" => {
1512                let v = a
1513                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
1514                    .unwrap_or_default();
1515                rot_with_shape = Some(v == "1");
1516            }
1517            _ => {}
1518        }
1519    }
1520
1521    let mut stops: Vec<GradientStop> = Vec::new();
1522    let mut gradient_type = GradientType::Linear(0);
1523    let mut buf = Vec::new();
1524
1525    loop {
1526        match rd.read_event_into(&mut buf) {
1527            Ok(Event::Start(e)) => {
1528                let name = e.name();
1529                let local = local_name(name.as_ref());
1530                if local == b"gsLst" {
1531                    // 解析光轨列表
1532                    let mut gs_depth = 1i32;
1533                    loop {
1534                        match rd.read_event_into(&mut buf) {
1535                            Ok(Event::Start(ref e2)) => {
1536                                if local_name(e2.name().as_ref()) == b"gs" && gs_depth == 1 {
1537                                    // 解析单个 <a:gs pos="...">
1538                                    let mut pos: u32 = 0;
1539                                    for a in e2.attributes().flatten() {
1540                                        if a.key.as_ref() == b"pos" {
1541                                            pos = a
1542                                                .normalized_value(
1543                                                    quick_xml::XmlVersion::Implicit1_0,
1544                                                )
1545                                                .unwrap_or_default()
1546                                                .parse()
1547                                                .unwrap_or(0);
1548                                        }
1549                                    }
1550                                    gs_depth += 1;
1551                                    // 读取 gs 内部的颜色子元素
1552                                    let color = parse_color_child(rd, b"gs")?;
1553                                    stops.push(GradientStop { pos, color });
1554                                } else {
1555                                    gs_depth += 1;
1556                                }
1557                            }
1558                            Ok(Event::End(ref e2)) => {
1559                                let n2 = e2.name();
1560                                let ln = local_name(n2.as_ref());
1561                                if ln == b"gs" {
1562                                    gs_depth -= 1;
1563                                } else if ln == b"gsLst" {
1564                                    break;
1565                                }
1566                            }
1567                            Ok(Event::Eof) => break,
1568                            Err(_) => break,
1569                            _ => {}
1570                        }
1571                        buf.clear();
1572                    }
1573                } else if local == b"lin" {
1574                    // 线性渐变:<a:lin ang="5400000" scaled="1"/>
1575                    let mut ang: i32 = 0;
1576                    for a in e.attributes().flatten() {
1577                        if a.key.as_ref() == b"ang" {
1578                            ang = a
1579                                .normalized_value(quick_xml::XmlVersion::Implicit1_0)
1580                                .unwrap_or_default()
1581                                .parse()
1582                                .unwrap_or(0);
1583                        }
1584                    }
1585                    gradient_type = GradientType::Linear(ang);
1586                    // lin 可能是 Start 或 Empty;如果是 Start 需要读到 End
1587                    if e.is_empty() {
1588                        // Empty 事件不会触发 End,无需处理
1589                    } else {
1590                        // 读到 </a:lin>
1591                        let _ = collect_full_element(rd, e.into_owned())?;
1592                    }
1593                } else if local == b"path" {
1594                    // 路径渐变:<a:path path="circle|rect|shape">...</a:path>
1595                    let mut path_type = GradientPath::Circle;
1596                    for a in e.attributes().flatten() {
1597                        if a.key.as_ref() == b"path" {
1598                            let v = a
1599                                .normalized_value(quick_xml::XmlVersion::Implicit1_0)
1600                                .unwrap_or_default();
1601                            path_type = match v.as_ref() {
1602                                "circle" => GradientPath::Circle,
1603                                "rect" => GradientPath::Rect,
1604                                "shape" => GradientPath::Shape,
1605                                _ => GradientPath::Circle,
1606                            };
1607                        }
1608                    }
1609                    gradient_type = GradientType::Path(path_type);
1610                    // 吞掉 path 的子元素(fillToRect 等)
1611                    let _ = collect_full_element(rd, e.into_owned())?;
1612                } else {
1613                    let _ = collect_full_element(rd, e.into_owned())?;
1614                }
1615            }
1616            Ok(Event::Empty(e)) => {
1617                let name = e.name();
1618                let local = local_name(name.as_ref());
1619                if local == b"lin" {
1620                    // 自闭合的 <a:lin ang="..." scaled="..."/>
1621                    let mut ang: i32 = 0;
1622                    for a in e.attributes().flatten() {
1623                        if a.key.as_ref() == b"ang" {
1624                            ang = a
1625                                .normalized_value(quick_xml::XmlVersion::Implicit1_0)
1626                                .unwrap_or_default()
1627                                .parse()
1628                                .unwrap_or(0);
1629                        }
1630                    }
1631                    gradient_type = GradientType::Linear(ang);
1632                } else if local == b"path" {
1633                    let mut path_type = GradientPath::Circle;
1634                    for a in e.attributes().flatten() {
1635                        if a.key.as_ref() == b"path" {
1636                            let v = a
1637                                .normalized_value(quick_xml::XmlVersion::Implicit1_0)
1638                                .unwrap_or_default();
1639                            path_type = match v.as_ref() {
1640                                "circle" => GradientPath::Circle,
1641                                "rect" => GradientPath::Rect,
1642                                "shape" => GradientPath::Shape,
1643                                _ => GradientPath::Circle,
1644                            };
1645                        }
1646                    }
1647                    gradient_type = GradientType::Path(path_type);
1648                }
1649            }
1650            Ok(Event::End(e)) => {
1651                if local_name(e.name().as_ref()) == b"gradFill" {
1652                    break;
1653                }
1654            }
1655            Ok(Event::Eof) => break,
1656            Err(e) => return Err(crate::Error::Xml(format!("gradFill parse: {e}"))),
1657            _ => {}
1658        }
1659        buf.clear();
1660    }
1661
1662    Ok(GradientFill {
1663        stops,
1664        gradient_type,
1665        flip,
1666        rot_with_shape,
1667    })
1668}
1669
1670/// 解析 `<a:pattFill>` 元素为 [`PatternFill`]。
1671///
1672/// # OOXML 结构
1673///
1674/// ```text
1675/// <a:pattFill prst="pct5">
1676///   <a:fgClr><a:srgbClr val="FF0000"/></a:fgClr>
1677///   <a:bgClr><a:srgbClr val="FFFFFF"/></a:bgClr>
1678/// </a:pattFill>
1679/// ```
1680fn parse_patt_fill<R: std::io::BufRead>(
1681    rd: &mut Reader<R>,
1682    start: quick_xml::events::BytesStart<'static>,
1683) -> crate::Result<PatternFill> {
1684    let mut prst = String::new();
1685    for a in start.attributes().flatten() {
1686        if a.key.as_ref() == b"prst" {
1687            prst = a
1688                .normalized_value(quick_xml::XmlVersion::Implicit1_0)
1689                .unwrap_or_default()
1690                .to_string();
1691        }
1692    }
1693
1694    let mut fg_color = Color::None;
1695    let mut bg_color = Color::None;
1696    let mut buf = Vec::new();
1697
1698    loop {
1699        match rd.read_event_into(&mut buf) {
1700            Ok(Event::Start(e)) => {
1701                let name = e.name();
1702                let local = local_name(name.as_ref());
1703                if local == b"fgClr" {
1704                    fg_color = parse_color_child(rd, b"fgClr")?;
1705                } else if local == b"bgClr" {
1706                    bg_color = parse_color_child(rd, b"bgClr")?;
1707                } else {
1708                    let _ = collect_full_element(rd, e.into_owned())?;
1709                }
1710            }
1711            Ok(Event::End(e)) => {
1712                if local_name(e.name().as_ref()) == b"pattFill" {
1713                    break;
1714                }
1715            }
1716            Ok(Event::Eof) => break,
1717            Err(e) => return Err(crate::Error::Xml(format!("pattFill parse: {e}"))),
1718            _ => {}
1719        }
1720        buf.clear();
1721    }
1722
1723    Ok(PatternFill {
1724        prst,
1725        fg_color,
1726        bg_color,
1727    })
1728}
1729
1730/// 从 `<a:blipFill>` 事件中解析图片填充(TODO-003/048)。
1731///
1732/// 解析 `<a:blip r:embed="rIdN"/>` 提取 rid,以及可选的 `<a:stretch>` / `<a:tile>` 填充模式。
1733/// `<a:srcRect>` 暂不解析(SpPr 级 blipFill 较少用 srcRect,主要在 `<p:pic>` 内使用)。
1734///
1735/// # 参数
1736/// - `rd`:reader,已消费 `<a:blipFill>` 的 Start 事件;
1737/// - `start`:`<a:blipFill>` 的 Start 事件(保留属性,目前不用)。
1738///
1739/// # 返回值
1740/// 返回 `(rid, mode)` 二元组,由调用方组装为 `Fill::Blip { rid, mode }`。
1741///
1742/// # OOXML 结构
1743///
1744/// ```text
1745/// <a:blipFill>
1746///   <a:blip r:embed="rId1"/>          ← 必填,引用图片 part
1747///   <a:srcRect l="..." t="..." .../>  ← 可选,裁剪(暂不解析)
1748///   <a:stretch><a:fillRect/></a:stretch>  ← 拉伸(默认)
1749///   或
1750///   <a:tile tx="..." ty="..." .../>   ← 平铺
1751/// </a:blipFill>
1752/// ```
1753fn parse_blip_fill<R: std::io::BufRead>(
1754    rd: &mut Reader<R>,
1755    _start: quick_xml::events::BytesStart<'static>,
1756) -> crate::Result<(String, crate::oxml::sppr::BlipFillMode)> {
1757    let mut rid = String::new();
1758    let mut mode = crate::oxml::sppr::BlipFillMode::default(); // 默认 Stretch
1759    let mut buf = Vec::new();
1760
1761    loop {
1762        match rd.read_event_into(&mut buf) {
1763            Ok(Event::Start(e)) => {
1764                let name = e.name();
1765                let local = local_name(name.as_ref());
1766                if local == b"blip" {
1767                    // `<a:blip r:embed="rIdN">...</a:blip>`:可能有子元素(如 alphaModFix)
1768                    for a in e.attributes().flatten() {
1769                        let key = a.key.as_ref();
1770                        if key == b"r:embed" || key.ends_with(b":embed") {
1771                            rid = a
1772                                .normalized_value(quick_xml::XmlVersion::Implicit1_0)
1773                                .unwrap_or_default()
1774                                .to_string();
1775                        }
1776                    }
1777                    // 吞掉 blip 的子元素(如 alphaModFix),直到 blip End
1778                    let _ = collect_full_element(rd, e.into_owned())?;
1779                } else if local == b"stretch" {
1780                    // `<a:stretch><a:fillRect/></a:stretch>`
1781                    let _ = collect_full_element(rd, e.into_owned())?;
1782                    mode = crate::oxml::sppr::BlipFillMode::Stretch;
1783                } else if local == b"tile" {
1784                    // `<a:tile tx="..." ty="..." sx="..." sy="..." flip="..." algn="..."/>`
1785                    mode = parse_tile_attrs(&e);
1786                    let _ = collect_full_element(rd, e.into_owned())?;
1787                } else if local == b"srcRect" {
1788                    // 暂不解析 srcRect(SpPr 级 blipFill 较少用)
1789                    let _ = collect_full_element(rd, e.into_owned())?;
1790                } else {
1791                    let _ = collect_full_element(rd, e.into_owned())?;
1792                }
1793            }
1794            Ok(Event::Empty(e)) => {
1795                let name = e.name();
1796                let local = local_name(name.as_ref());
1797                if local == b"blip" {
1798                    // 自闭合 `<a:blip r:embed="rIdN"/>`
1799                    for a in e.attributes().flatten() {
1800                        let key = a.key.as_ref();
1801                        if key == b"r:embed" || key.ends_with(b":embed") {
1802                            rid = a
1803                                .normalized_value(quick_xml::XmlVersion::Implicit1_0)
1804                                .unwrap_or_default()
1805                                .to_string();
1806                        }
1807                    }
1808                } else if local == b"stretch" {
1809                    mode = crate::oxml::sppr::BlipFillMode::Stretch;
1810                } else if local == b"tile" {
1811                    mode = parse_tile_attrs(&e);
1812                }
1813                // srcRect / 其它自闭合元素忽略
1814            }
1815            Ok(Event::End(e)) => {
1816                if local_name(e.name().as_ref()) == b"blipFill" {
1817                    break;
1818                }
1819            }
1820            Ok(Event::Eof) => break,
1821            Err(e) => return Err(crate::Error::Xml(format!("blipFill parse: {e}"))),
1822            _ => {}
1823        }
1824        buf.clear();
1825    }
1826
1827    Ok((rid, mode))
1828}
1829
1830/// 从 reader 中解析一个颜色子元素(`<a:srgbClr>` / `<a:schemeClr>` / `<a:prstClr>`)。
1831///
1832/// 调用方必须已消费父元素的 Start 事件。本函数读取父元素的子元素直到 End。
1833///
1834/// # 参数
1835/// - `rd`:reader;
1836/// - `parent`:父元素名(用于匹配 End 事件,如 `b"gs"` / `b"fgClr"` / `b"bgClr"`)。
1837fn parse_color_child<R: std::io::BufRead>(
1838    rd: &mut Reader<R>,
1839    parent: &[u8],
1840) -> crate::Result<Color> {
1841    let mut buf = Vec::new();
1842    loop {
1843        match rd.read_event_into(&mut buf) {
1844            Ok(Event::Empty(e)) => {
1845                let name = e.name();
1846                let local = local_name(name.as_ref());
1847                if let Some(c) = parse_color_from_event(&e) {
1848                    return Ok(c);
1849                }
1850                // 不是颜色元素则忽略
1851                let _ = local;
1852            }
1853            Ok(Event::Start(e)) => {
1854                let name = e.name();
1855                let local = local_name(name.as_ref());
1856                if local == b"srgbClr" || local == b"schemeClr" || local == b"prstClr" {
1857                    // 非自闭合颜色元素:读属性后跳到 End
1858                    if let Some(c) = parse_color_from_event(&e) {
1859                        // 吞掉子元素(如 alpha)直到 End
1860                        let _ = collect_full_element(rd, e.into_owned())?;
1861                        return Ok(c);
1862                    }
1863                } else {
1864                    let _ = collect_full_element(rd, e.into_owned())?;
1865                }
1866            }
1867            Ok(Event::End(e)) => {
1868                if local_name(e.name().as_ref()) == parent {
1869                    return Ok(Color::None);
1870                }
1871            }
1872            Ok(Event::Eof) => return Ok(Color::None),
1873            Err(_) => return Ok(Color::None),
1874            _ => {}
1875        }
1876        buf.clear();
1877    }
1878}
1879
1880/// 从一个 XML 事件(Start 或 Empty)中提取颜色。
1881///
1882/// 支持 `<a:srgbClr val="RRGGBB"/>` / `<a:schemeClr val="..."/>` / `<a:prstClr val="..."/>`。
1883fn parse_color_from_event(e: &quick_xml::events::BytesStart<'_>) -> Option<Color> {
1884    let name = e.name();
1885    let local = local_name(name.as_ref());
1886    for a in e.attributes().flatten() {
1887        if a.key.as_ref() == b"val" {
1888            let v = a
1889                .normalized_value(quick_xml::XmlVersion::Implicit1_0)
1890                .unwrap_or_default();
1891            if local == b"srgbClr" && v.len() == 6 {
1892                let r = u8::from_str_radix(&v[0..2], 16).unwrap_or(0);
1893                let g = u8::from_str_radix(&v[2..4], 16).unwrap_or(0);
1894                let b = u8::from_str_radix(&v[4..6], 16).unwrap_or(0);
1895                return Some(Color::RGB(RGBColor(r, g, b)));
1896            } else if local == b"schemeClr" {
1897                if let Ok(sc) = v.parse::<SchemeColor>() {
1898                    return Some(Color::Scheme(sc));
1899                }
1900            } else if local == b"prstClr" {
1901                if let Ok(p) = v.parse::<PresetColor>() {
1902                    return Some(Color::Preset(p));
1903                }
1904            }
1905        }
1906    }
1907    None
1908}
1909
1910/// 解析 `<a:ln>...</a:ln>` 元素。
1911fn parse_ln<R: std::io::BufRead>(
1912    rd: &mut Reader<R>,
1913    start: quick_xml::events::BytesStart<'static>,
1914) -> crate::Result<Line> {
1915    let mut ln = Line::default();
1916    // 解析 w / cap / cmpd 等属性
1917    for a in start.attributes().flatten() {
1918        match a.key.as_ref() {
1919            b"w" => {
1920                if let Ok(v) = a
1921                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
1922                    .unwrap_or_default()
1923                    .parse::<i64>()
1924                {
1925                    ln.width = Some(Emu(v));
1926                }
1927            }
1928            b"cap" => {
1929                ln.cap = Some(
1930                    a.normalized_value(quick_xml::XmlVersion::Implicit1_0)
1931                        .unwrap_or_default()
1932                        .to_string(),
1933                );
1934            }
1935            b"cmpd" => {
1936                ln.compound = Some(
1937                    a.normalized_value(quick_xml::XmlVersion::Implicit1_0)
1938                        .unwrap_or_default()
1939                        .to_string(),
1940                );
1941            }
1942            _ => {}
1943        }
1944    }
1945    let mut buf = Vec::new();
1946    loop {
1947        match rd.read_event_into(&mut buf) {
1948            Ok(Event::Start(e)) => {
1949                let name = e.name();
1950                let local = local_name(name.as_ref());
1951                if local == b"solidFill" {
1952                    ln.color = parse_solid_fill(rd, e.into_owned())?;
1953                } else if local == b"noFill" {
1954                    ln.no_fill = true;
1955                    let _ = collect_full_element(rd, e.into_owned());
1956                } else if local == b"prstDash" {
1957                    // 寻找子元素 a:prstDash val="..."
1958                    parse_prstdash_into(rd, &mut ln);
1959                } else if local == b"gradFill" {
1960                    // 线条渐变填充
1961                    let grad = parse_grad_fill(rd, e.into_owned())?;
1962                    ln.fill = Fill::Gradient(grad);
1963                } else if local == b"pattFill" {
1964                    // 线条图案填充
1965                    let patt = parse_patt_fill(rd, e.into_owned())?;
1966                    ln.fill = Fill::Pattern(patt);
1967                } else if local == b"headEnd" {
1968                    ln.head_end = Some(parse_arrow_head(&e));
1969                    let _ = collect_full_element(rd, e.into_owned());
1970                } else if local == b"tailEnd" {
1971                    ln.tail_end = Some(parse_arrow_head(&e));
1972                    let _ = collect_full_element(rd, e.into_owned());
1973                } else if local == b"round" {
1974                    ln.join = Some(LineJoin::Round);
1975                    let _ = collect_full_element(rd, e.into_owned());
1976                } else if local == b"miter" {
1977                    let mut lim = 800000i32;
1978                    for a in e.attributes().flatten() {
1979                        if a.key.as_ref() == b"lim" {
1980                            lim = a
1981                                .normalized_value(quick_xml::XmlVersion::Implicit1_0)
1982                                .unwrap_or_default()
1983                                .parse()
1984                                .unwrap_or(800000);
1985                        }
1986                    }
1987                    ln.join = Some(LineJoin::Miter(lim));
1988                    let _ = collect_full_element(rd, e.into_owned());
1989                } else if local == b"bevel" {
1990                    ln.join = Some(LineJoin::Bevel);
1991                    let _ = collect_full_element(rd, e.into_owned());
1992                } else {
1993                    let _ = collect_full_element(rd, e.into_owned());
1994                }
1995            }
1996            Ok(Event::Empty(e)) => {
1997                let name = e.name();
1998                let local = local_name(name.as_ref());
1999                if local == b"noFill" {
2000                    ln.no_fill = true;
2001                } else if local == b"headEnd" {
2002                    ln.head_end = Some(parse_arrow_head(&e));
2003                } else if local == b"tailEnd" {
2004                    ln.tail_end = Some(parse_arrow_head(&e));
2005                } else if local == b"round" {
2006                    ln.join = Some(LineJoin::Round);
2007                } else if local == b"miter" {
2008                    let mut lim = 800000i32;
2009                    for a in e.attributes().flatten() {
2010                        if a.key.as_ref() == b"lim" {
2011                            lim = a
2012                                .normalized_value(quick_xml::XmlVersion::Implicit1_0)
2013                                .unwrap_or_default()
2014                                .parse()
2015                                .unwrap_or(800000);
2016                        }
2017                    }
2018                    ln.join = Some(LineJoin::Miter(lim));
2019                } else if local == b"bevel" {
2020                    ln.join = Some(LineJoin::Bevel);
2021                }
2022                // solidFill 自闭合罕见,兜底忽略
2023            }
2024            Ok(Event::End(e)) => {
2025                if local_name(e.name().as_ref()) == b"ln" {
2026                    return Ok(ln);
2027                }
2028            }
2029            Ok(Event::Eof) => return Ok(ln),
2030            _ => {}
2031        }
2032        buf.clear();
2033    }
2034}
2035
2036/// 从 `<a:headEnd>` / `<a:tailEnd>` 事件中提取箭头属性(type/w/len)。
2037///
2038/// 该函数仅读取属性,**不**消费子元素——调用方负责处理后续事件。
2039fn parse_arrow_head(e: &quick_xml::events::BytesStart<'_>) -> ArrowHead {
2040    let mut arrow_type = ArrowType::None;
2041    let mut width = ArrowSize::Medium;
2042    let mut length = ArrowSize::Medium;
2043    for a in e.attributes().flatten() {
2044        match a.key.as_ref() {
2045            b"type" => {
2046                let v = a
2047                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
2048                    .unwrap_or_default();
2049                arrow_type = match v.as_ref() {
2050                    "none" => ArrowType::None,
2051                    "triangle" => ArrowType::Triangle,
2052                    "stealth" => ArrowType::Stealth,
2053                    "diamond" => ArrowType::Diamond,
2054                    "oval" => ArrowType::Oval,
2055                    "arrow" => ArrowType::Arrow,
2056                    _ => ArrowType::None,
2057                };
2058            }
2059            b"w" => {
2060                let v = a
2061                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
2062                    .unwrap_or_default();
2063                width = match v.as_ref() {
2064                    "sm" => ArrowSize::Small,
2065                    "lg" => ArrowSize::Large,
2066                    _ => ArrowSize::Medium,
2067                };
2068            }
2069            b"len" => {
2070                let v = a
2071                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
2072                    .unwrap_or_default();
2073                length = match v.as_ref() {
2074                    "sm" => ArrowSize::Small,
2075                    "lg" => ArrowSize::Large,
2076                    _ => ArrowSize::Medium,
2077                };
2078            }
2079            _ => {}
2080        }
2081    }
2082    ArrowHead {
2083        arrow_type,
2084        width,
2085        length,
2086    }
2087}
2088
2089fn parse_prstdash_into<R: std::io::BufRead>(rd: &mut Reader<R>, ln: &mut Line) {
2090    let mut buf = Vec::new();
2091    loop {
2092        match rd.read_event_into(&mut buf) {
2093            Ok(Event::Empty(e)) => {
2094                let name = e.name();
2095                let local = local_name(name.as_ref());
2096                if local == b"prst" {
2097                    for a in e.attributes().flatten() {
2098                        if a.key.as_ref() == b"val" {
2099                            let v = a
2100                                .normalized_value(quick_xml::XmlVersion::Implicit1_0)
2101                                .unwrap_or_default();
2102                            if let Ok(d) = v.parse::<crate::oxml::sppr::Dash>() {
2103                                ln.dash = Some(d);
2104                            }
2105                        }
2106                    }
2107                }
2108            }
2109            Ok(Event::End(e)) => {
2110                let name = e.name();
2111                if local_name(name.as_ref()) == b"prstDash" {
2112                    return;
2113                }
2114            }
2115            Ok(Event::Eof) => return,
2116            _ => {}
2117        }
2118        buf.clear();
2119    }
2120}
2121
2122/// 解析 `p:txBody` 元素。
2123pub fn parse_txbody(xml: &str) -> crate::Result<TextBody> {
2124    let mut tb = TextBody::new();
2125    let mut rd = Reader::from_str(xml);
2126    rd.config_mut().trim_text(true);
2127    let mut buf = Vec::new();
2128    let mut p_bufs: Vec<String> = Vec::new();
2129    loop {
2130        match rd.read_event_into(&mut buf) {
2131            Ok(Event::Start(e)) => {
2132                let name = e.name();
2133                let local = local_name(name.as_ref());
2134                if local == b"txBody" {
2135                    // 根元素 <p:txBody>:仅标记进入,不调用 collect_full_element
2136                    // (否则会吞掉所有子元素,导致 <a:p> 等无法解析)
2137                } else if local == b"p" {
2138                    let inner = collect_full_element(&mut rd, e.into_owned())?;
2139                    p_bufs.push(inner);
2140                } else if local == b"bodyPr" {
2141                    // 解析 <a:bodyPr> 的属性(numCol / spcCol / anchor / wrap 等)
2142                    // 并保留其子元素(如 <a:spAutoFit/>)
2143                    let inner = collect_full_element(&mut rd, e.into_owned())?;
2144                    let bp = parse_body_pr(&inner)?;
2145                    tb.body_properties = Some(bp);
2146                } else {
2147                    // lstStyle 等跳过
2148                    let _ = collect_full_element(&mut rd, e.into_owned());
2149                }
2150            }
2151            Ok(Event::Empty(e)) => {
2152                let name = e.name();
2153                let local = local_name(name.as_ref());
2154                if local == b"bodyPr" {
2155                    // 自闭合 <a:bodyPr/>:仅解析属性
2156                    let bp = parse_body_pr_attrs(&e);
2157                    tb.body_properties = Some(bp);
2158                }
2159            }
2160            Ok(Event::Eof) => break,
2161            Err(e) => return Err(crate::Error::Xml(format!("txBody parse: {e}"))),
2162            _ => {}
2163        }
2164        buf.clear();
2165    }
2166    for p_xml in p_bufs {
2167        if let Ok(p) = parse_paragraph(&p_xml) {
2168            tb.paragraphs.push(p);
2169        }
2170    }
2171    Ok(tb)
2172}
2173
2174/// 解析 `<a:effectLst>` 元素(TODO-011:形状效果)。
2175///
2176/// # 支持的子元素
2177/// - `<a:outerShdw>`:外阴影(dir/dist/blurRad/color/rotWithShape)
2178/// - `<a:innerShdw>`:内阴影(dir/dist/blurRad/color)
2179/// - `<a:glow>`:发光(rad/color)
2180/// - `<a:softEdge>`:柔化边缘(rad)
2181/// - `<a:reflection>`:反射(blurRad/stA/stPos/endA/endPos/dist/dir/rotWithShape)
2182fn parse_effect_lst<R: std::io::BufRead>(
2183    rd: &mut Reader<R>,
2184    start: quick_xml::events::BytesStart<'static>,
2185) -> crate::Result<EffectList> {
2186    let mut effects = EffectList::default();
2187    let mut buf = Vec::new();
2188    // 消费 start 事件的属性(effectLst 本身无属性,但需进入子元素循环)
2189    let _ = start;
2190    loop {
2191        match rd.read_event_into(&mut buf) {
2192            Ok(Event::Start(e)) => {
2193                let name = e.name();
2194                let local = local_name(name.as_ref());
2195                match local {
2196                    b"outerShdw" => {
2197                        let shdw = parse_shadow_attrs(&e, false);
2198                        // 解析子元素(srgbClr/schemeClr)
2199                        let color = parse_color_child(rd, b"outerShdw")?;
2200                        let mut shdw = shdw;
2201                        shdw.color = color;
2202                        effects.outer_shadow = Some(shdw);
2203                    }
2204                    b"innerShdw" => {
2205                        let shdw = parse_shadow_attrs(&e, true);
2206                        let color = parse_color_child(rd, b"innerShdw")?;
2207                        let mut shdw = shdw;
2208                        shdw.color = color;
2209                        effects.inner_shadow = Some(shdw);
2210                    }
2211                    b"glow" => {
2212                        let mut glow = GlowEffect::default();
2213                        for a in e.attributes().flatten() {
2214                            if a.key.as_ref() == b"rad" {
2215                                glow.rad = a
2216                                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
2217                                    .unwrap_or_default()
2218                                    .parse()
2219                                    .unwrap_or(0);
2220                            }
2221                        }
2222                        glow.color = parse_color_child(rd, b"glow")?;
2223                        effects.glow = Some(glow);
2224                    }
2225                    b"softEdge" => {
2226                        let mut se = SoftEdgeEffect::default();
2227                        for a in e.attributes().flatten() {
2228                            if a.key.as_ref() == b"rad" {
2229                                se.rad = a
2230                                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
2231                                    .unwrap_or_default()
2232                                    .parse()
2233                                    .unwrap_or(0);
2234                            }
2235                        }
2236                        // softEdge 无子元素,消费到 End
2237                        let _ = collect_full_element(rd, e.into_owned());
2238                        effects.soft_edge = Some(se);
2239                    }
2240                    b"reflection" => {
2241                        let mut refl = ReflectionEffect::default();
2242                        for a in e.attributes().flatten() {
2243                            let key = a.key.as_ref();
2244                            let v = a
2245                                .normalized_value(quick_xml::XmlVersion::Implicit1_0)
2246                                .unwrap_or_default();
2247                            match key {
2248                                b"blurRad" => refl.blur_rad = v.parse().ok(),
2249                                b"stA" => refl.st_a = v.parse().ok(),
2250                                b"stPos" => refl.st_pos = v.parse().ok(),
2251                                b"endA" => refl.end_a = v.parse().ok(),
2252                                b"endPos" => refl.end_pos = v.parse().ok(),
2253                                b"dist" => refl.dist = v.parse().ok(),
2254                                b"dir" => refl.dir = v.parse().ok(),
2255                                b"rotWithShape" => {
2256                                    refl.rot_with_shape = Some(v == "1" || v == "true")
2257                                }
2258                                _ => {}
2259                            }
2260                        }
2261                        // reflection 无子元素,消费到 End
2262                        let _ = collect_full_element(rd, e.into_owned());
2263                        effects.reflection = Some(refl);
2264                    }
2265                    _ => {
2266                        // 未识别的效果子元素:吞掉
2267                        let _ = collect_full_element(rd, e.into_owned());
2268                    }
2269                }
2270            }
2271            Ok(Event::Empty(e)) => {
2272                let name = e.name();
2273                let local = local_name(name.as_ref());
2274                if local == b"outerShdw" {
2275                    let mut shdw = parse_shadow_attrs(&e, false);
2276                    shdw.color = Color::None;
2277                    effects.outer_shadow = Some(shdw);
2278                } else if local == b"innerShdw" {
2279                    let mut shdw = parse_shadow_attrs(&e, true);
2280                    shdw.color = Color::None;
2281                    effects.inner_shadow = Some(shdw);
2282                } else if local == b"glow" {
2283                    let mut glow = GlowEffect::default();
2284                    for a in e.attributes().flatten() {
2285                        if a.key.as_ref() == b"rad" {
2286                            glow.rad = a
2287                                .normalized_value(quick_xml::XmlVersion::Implicit1_0)
2288                                .unwrap_or_default()
2289                                .parse()
2290                                .unwrap_or(0);
2291                        }
2292                    }
2293                    effects.glow = Some(glow);
2294                } else if local == b"softEdge" {
2295                    let mut se = SoftEdgeEffect::default();
2296                    for a in e.attributes().flatten() {
2297                        if a.key.as_ref() == b"rad" {
2298                            se.rad = a
2299                                .normalized_value(quick_xml::XmlVersion::Implicit1_0)
2300                                .unwrap_or_default()
2301                                .parse()
2302                                .unwrap_or(0);
2303                        }
2304                    }
2305                    effects.soft_edge = Some(se);
2306                } else if local == b"reflection" {
2307                    let mut refl = ReflectionEffect::default();
2308                    for a in e.attributes().flatten() {
2309                        let key = a.key.as_ref();
2310                        let v = a
2311                            .normalized_value(quick_xml::XmlVersion::Implicit1_0)
2312                            .unwrap_or_default();
2313                        match key {
2314                            b"blurRad" => refl.blur_rad = v.parse().ok(),
2315                            b"stA" => refl.st_a = v.parse().ok(),
2316                            b"stPos" => refl.st_pos = v.parse().ok(),
2317                            b"endA" => refl.end_a = v.parse().ok(),
2318                            b"endPos" => refl.end_pos = v.parse().ok(),
2319                            b"dist" => refl.dist = v.parse().ok(),
2320                            b"dir" => refl.dir = v.parse().ok(),
2321                            b"rotWithShape" => refl.rot_with_shape = Some(v == "1" || v == "true"),
2322                            _ => {}
2323                        }
2324                    }
2325                    effects.reflection = Some(refl);
2326                }
2327            }
2328            Ok(Event::End(e)) => {
2329                if local_name(e.name().as_ref()) == b"effectLst" {
2330                    return Ok(effects);
2331                }
2332            }
2333            Ok(Event::Eof) => return Ok(effects),
2334            Err(e) => return Err(crate::Error::Xml(format!("effectLst parse: {e}"))),
2335            _ => {}
2336        }
2337        buf.clear();
2338    }
2339}
2340
2341/// 解析 `<a:rot lat="..." lon="..." rev="..."/>` 的三个属性为 [`Rotation3d`]。
2342///
2343/// 用于 `camera` / `lightRig` 内的可选旋转元素。
2344fn parse_rotation_3d(e: &quick_xml::events::BytesStart<'_>) -> Rotation3d {
2345    let mut rot = Rotation3d::default();
2346    for a in e.attributes().flatten() {
2347        let v = a
2348            .normalized_value(quick_xml::XmlVersion::Implicit1_0)
2349            .unwrap_or_default();
2350        match a.key.as_ref() {
2351            b"lat" => rot.lat = v.parse().unwrap_or(0),
2352            b"lon" => rot.lon = v.parse().unwrap_or(0),
2353            b"rev" => rot.rev = v.parse().unwrap_or(0),
2354            _ => {}
2355        }
2356    }
2357    rot
2358}
2359
2360/// 解析 `<a:scene3d>` 元素(TODO-050)。
2361///
2362/// # 元素顺序(OOXML)
2363/// ```text
2364/// <a:scene3d>
2365///   <a:camera prst="..." fov="..." zoom="...">    ← 必填
2366///     <a:rot lat="..." lon="..." rev="..."/>      ← 可选
2367///   </a:camera>
2368///   <a:lightRig rig="..." dir="...">              ← 必填
2369///     <a:rot lat="..." lon="..." rev="..."/>      ← 可选
2370///   </a:lightRig>
2371/// </a:scene3d>
2372/// ```
2373fn parse_scene_3d<R: std::io::BufRead>(
2374    rd: &mut Reader<R>,
2375    start: quick_xml::events::BytesStart<'static>,
2376) -> crate::Result<Scene3d> {
2377    let mut scene = Scene3d::default();
2378    let mut buf = Vec::new();
2379    let _ = start;
2380    loop {
2381        match rd.read_event_into(&mut buf) {
2382            Ok(Event::Start(e)) => {
2383                // 把 e.name() 绑定到 let,避免临时 QName 在 as_ref() 后 drop
2384                let name = e.name();
2385                let local = local_name(name.as_ref());
2386                if local == b"camera" {
2387                    let mut camera = Camera::default();
2388                    for a in e.attributes().flatten() {
2389                        let v = a
2390                            .normalized_value(quick_xml::XmlVersion::Implicit1_0)
2391                            .unwrap_or_default();
2392                        match a.key.as_ref() {
2393                            // v 是 Cow<'_, str>,from_str 期望 &str,需要 &v
2394                            b"prst" => camera.preset = CameraPreset::parse(&v),
2395                            b"fov" => camera.fov = v.parse().unwrap_or(0),
2396                            b"zoom" => camera.zoom = v.parse().unwrap_or(0),
2397                            _ => {}
2398                        }
2399                    }
2400                    // 解析可选 <a:rot>
2401                    let mut buf2 = Vec::new();
2402                    loop {
2403                        match rd.read_event_into(&mut buf2) {
2404                            Ok(Event::Start(e2)) => {
2405                                let n2 = e2.name();
2406                                if local_name(n2.as_ref()) == b"rot" {
2407                                    camera.rotation = Some(parse_rotation_3d(&e2));
2408                                    let _ = collect_full_element(rd, e2.into_owned());
2409                                }
2410                            }
2411                            Ok(Event::End(e2)) => {
2412                                let n2 = e2.name();
2413                                if local_name(n2.as_ref()) == b"camera" {
2414                                    break;
2415                                }
2416                            }
2417                            Ok(Event::Eof) => break,
2418                            _ => {}
2419                        }
2420                        buf2.clear();
2421                    }
2422                    scene.camera = camera;
2423                } else if local == b"lightRig" {
2424                    let mut rig = LightRig::default();
2425                    for a in e.attributes().flatten() {
2426                        let v = a
2427                            .normalized_value(quick_xml::XmlVersion::Implicit1_0)
2428                            .unwrap_or_default();
2429                        match a.key.as_ref() {
2430                            // v 是 Cow<'_, str>,from_str 期望 &str,需要 &v
2431                            b"rig" => rig.rig = LightRigType::parse(&v),
2432                            b"dir" => rig.dir = LightRigDirection::parse(&v),
2433                            _ => {}
2434                        }
2435                    }
2436                    let mut buf2 = Vec::new();
2437                    loop {
2438                        match rd.read_event_into(&mut buf2) {
2439                            Ok(Event::Start(e2)) => {
2440                                let n2 = e2.name();
2441                                if local_name(n2.as_ref()) == b"rot" {
2442                                    rig.rotation = Some(parse_rotation_3d(&e2));
2443                                    let _ = collect_full_element(rd, e2.into_owned());
2444                                }
2445                            }
2446                            Ok(Event::End(e2)) => {
2447                                let n2 = e2.name();
2448                                if local_name(n2.as_ref()) == b"lightRig" {
2449                                    break;
2450                                }
2451                            }
2452                            Ok(Event::Eof) => break,
2453                            _ => {}
2454                        }
2455                        buf2.clear();
2456                    }
2457                    scene.light_rig = rig;
2458                } else if local == b"backdrop" {
2459                    // 解析可选 <a:backdrop>(TODO-050)
2460                    let backdrop = parse_backdrop(rd, e.into_owned())?;
2461                    scene.backdrop = Some(backdrop);
2462                } else {
2463                    let _ = collect_full_element(rd, e.into_owned());
2464                }
2465            }
2466            Ok(Event::End(e)) => {
2467                let name = e.name();
2468                if local_name(name.as_ref()) == b"scene3d" {
2469                    return Ok(scene);
2470                }
2471            }
2472            Ok(Event::Eof) => return Ok(scene),
2473            Err(e) => return Err(crate::Error::Xml(format!("scene3d parse: {e}"))),
2474            _ => {}
2475        }
2476        buf.clear();
2477    }
2478}
2479
2480/// 解析 `<a:backdrop>` 元素(TODO-050)。
2481///
2482/// # 元素顺序(OOXML CT_Backdrop)
2483/// ```text
2484/// <a:backdrop>
2485///   <a:anchor x="..." y="..." z="..."/>   ← 可选
2486///   <a:floor/>                            ← 可选
2487///   <a:wall/>                             ← 可选
2488///   <a:l/>                                ← 可选
2489///   <a:r/>                                ← 可选
2490///   <a:t/>                                ← 可选
2491///   <a:b/>                                ← 可选
2492/// </a:backdrop>
2493/// ```
2494fn parse_backdrop<R: std::io::BufRead>(
2495    rd: &mut Reader<R>,
2496    start: quick_xml::events::BytesStart<'static>,
2497) -> crate::Result<Backdrop> {
2498    let mut bd = Backdrop::default();
2499    let mut buf = Vec::new();
2500    let _ = start;
2501    loop {
2502        match rd.read_event_into(&mut buf) {
2503            Ok(Event::Empty(e)) => {
2504                // 把 e.name() 绑定到 let,避免临时 QName 在 as_ref() 后 drop
2505                let name = e.name();
2506                let local = local_name(name.as_ref());
2507                if local == b"anchor" {
2508                    let mut pt = Point3d::default();
2509                    for a in e.attributes().flatten() {
2510                        let v = a
2511                            .normalized_value(quick_xml::XmlVersion::Implicit1_0)
2512                            .unwrap_or_default();
2513                        match a.key.as_ref() {
2514                            b"x" => pt.x = v.parse().unwrap_or(0),
2515                            b"y" => pt.y = v.parse().unwrap_or(0),
2516                            b"z" => pt.z = v.parse().unwrap_or(0),
2517                            _ => {}
2518                        }
2519                    }
2520                    bd.anchor = Some(pt);
2521                } else if local == b"floor" {
2522                    bd.floor = true;
2523                } else if local == b"wall" {
2524                    bd.wall = true;
2525                } else if local == b"l" {
2526                    bd.left = true;
2527                } else if local == b"r" {
2528                    bd.right = true;
2529                } else if local == b"t" {
2530                    bd.top = true;
2531                } else if local == b"b" {
2532                    bd.bottom = true;
2533                }
2534            }
2535            Ok(Event::Start(e)) => {
2536                // backdrop 的子元素都是自闭合的(Empty),无 Start 分支需要处理;
2537                // 但兼容罕见的 open-close 形式(如 <a:floor></a:floor>),用 collect_full_element 吞掉。
2538                let name = e.name();
2539                let local = local_name(name.as_ref());
2540                if local == b"floor" {
2541                    bd.floor = true;
2542                } else if local == b"wall" {
2543                    bd.wall = true;
2544                } else if local == b"l" {
2545                    bd.left = true;
2546                } else if local == b"r" {
2547                    bd.right = true;
2548                } else if local == b"t" {
2549                    bd.top = true;
2550                } else if local == b"b" {
2551                    bd.bottom = true;
2552                }
2553                let _ = collect_full_element(rd, e.into_owned());
2554            }
2555            Ok(Event::End(e)) => {
2556                let name = e.name();
2557                if local_name(name.as_ref()) == b"backdrop" {
2558                    return Ok(bd);
2559                }
2560            }
2561            Ok(Event::Eof) => return Ok(bd),
2562            Err(e) => return Err(crate::Error::Xml(format!("backdrop parse: {e}"))),
2563            _ => {}
2564        }
2565        buf.clear();
2566    }
2567}
2568
2569/// 解析 `<a:sp3d>` 元素(TODO-050)。
2570///
2571/// # 元素顺序(OOXML)
2572/// ```text
2573/// <a:sp3d extrusionH="..." contourW="..." prstMaterial="...">
2574///   <a:bevelT w="..." h="..."/>     ← 可选
2575///   <a:bevelB w="..." h="..."/>     ← 可选
2576///   <a:extrusionClr>...</a:extrusionClr>   ← 可选
2577///   <a:contourClr>...</a:contourClr>       ← 可选
2578/// </a:sp3d>
2579/// ```
2580fn parse_sp_3d<R: std::io::BufRead>(
2581    rd: &mut Reader<R>,
2582    start: quick_xml::events::BytesStart<'static>,
2583) -> crate::Result<Sp3d> {
2584    let mut sp3d = Sp3d::default();
2585    // 解析 sp3d 自身属性
2586    for a in start.attributes().flatten() {
2587        let v = a
2588            .normalized_value(quick_xml::XmlVersion::Implicit1_0)
2589            .unwrap_or_default();
2590        match a.key.as_ref() {
2591            b"extrusionH" => sp3d.extrusion_h = v.parse().unwrap_or(0),
2592            b"contourW" => sp3d.contour_w = v.parse().unwrap_or(0),
2593            // v 是 Cow<'_, str>,from_str 期望 &str,需要 &v
2594            b"prstMaterial" => sp3d.prst_material = MaterialPreset::parse(&v),
2595            _ => {}
2596        }
2597    }
2598    let mut buf = Vec::new();
2599    loop {
2600        match rd.read_event_into(&mut buf) {
2601            Ok(Event::Empty(e)) => {
2602                // 自闭合 <a:bevelT w="..." h="..."/> 等
2603                // 把 e.name() 绑定到 let,避免临时 QName 在 as_ref() 后 drop
2604                let name = e.name();
2605                let local = local_name(name.as_ref());
2606                match local {
2607                    b"bevelT" => {
2608                        sp3d.bevel_top = Some(parse_bevel_attrs(&e));
2609                    }
2610                    b"bevelB" => {
2611                        sp3d.bevel_bottom = Some(parse_bevel_attrs(&e));
2612                    }
2613                    _ => {}
2614                }
2615            }
2616            Ok(Event::Start(e)) => {
2617                let name = e.name();
2618                let local = local_name(name.as_ref());
2619                match local {
2620                    b"bevelT" => {
2621                        let bvl = parse_bevel_attrs(&e);
2622                        let _ = collect_full_element(rd, e.into_owned());
2623                        sp3d.bevel_top = Some(bvl);
2624                    }
2625                    b"bevelB" => {
2626                        let bvl = parse_bevel_attrs(&e);
2627                        let _ = collect_full_element(rd, e.into_owned());
2628                        sp3d.bevel_bottom = Some(bvl);
2629                    }
2630                    b"extrusionClr" => {
2631                        // 解析子元素 srgbClr / schemeClr
2632                        let color = parse_color_child(rd, b"extrusionClr")?;
2633                        sp3d.extrusion_color = Some(color);
2634                    }
2635                    b"contourClr" => {
2636                        let color = parse_color_child(rd, b"contourClr")?;
2637                        sp3d.contour_color = Some(color);
2638                    }
2639                    _ => {
2640                        let _ = collect_full_element(rd, e.into_owned());
2641                    }
2642                }
2643            }
2644            Ok(Event::End(e)) => {
2645                let name = e.name();
2646                if local_name(name.as_ref()) == b"sp3d" {
2647                    return Ok(sp3d);
2648                }
2649            }
2650            Ok(Event::Eof) => return Ok(sp3d),
2651            Err(e) => return Err(crate::Error::Xml(format!("sp3d parse: {e}"))),
2652            _ => {}
2653        }
2654        buf.clear();
2655    }
2656}
2657
2658/// 从 `<a:bevelT>` / `<a:bevelB>` 的 Start/Empty 事件提取 w/h 属性。
2659fn parse_bevel_attrs(e: &quick_xml::events::BytesStart<'_>) -> Bevel {
2660    let mut bvl = Bevel::default();
2661    for a in e.attributes().flatten() {
2662        let v = a
2663            .normalized_value(quick_xml::XmlVersion::Implicit1_0)
2664            .unwrap_or_default();
2665        match a.key.as_ref() {
2666            b"w" => bvl.w = v.parse().unwrap_or(0),
2667            b"h" => bvl.h = v.parse().unwrap_or(0),
2668            _ => {}
2669        }
2670    }
2671    bvl
2672}
2673
2674/// 从 `<a:outerShdw>` / `<a:innerShdw>` 的 Start/Empty 事件提取属性。
2675///
2676/// `is_inner` 为 true 时忽略 `rotWithShape`(仅 outerShdw 有此属性)。
2677fn parse_shadow_attrs(e: &quick_xml::events::BytesStart<'_>, is_inner: bool) -> ShadowEffect {
2678    let mut shdw = ShadowEffect::default();
2679    for a in e.attributes().flatten() {
2680        let key = a.key.as_ref();
2681        let v = a
2682            .normalized_value(quick_xml::XmlVersion::Implicit1_0)
2683            .unwrap_or_default();
2684        match key {
2685            b"dir" => shdw.dir = v.parse().unwrap_or(0),
2686            b"dist" => shdw.dist = v.parse().unwrap_or(0),
2687            b"blurRad" => shdw.blur_rad = v.parse().unwrap_or(0),
2688            b"rotWithShape" if !is_inner => {
2689                shdw.rot_with_shape = Some(v == "1" || v == "true");
2690            }
2691            _ => {}
2692        }
2693    }
2694    shdw
2695}
2696
2697/// 从 `<a:bodyPr ...>...</a:bodyPr>` 完整 XML 字符串解析 [`BodyProperties`]。
2698///
2699/// 提取属性:`numCol` / `spcCol` / `anchor` / `wrap` / `lIns`/`tIns`/`rIns`/`bIns` /
2700/// `vert` / `rot`,以及子元素 `a:spAutoFit` / `a:normAutofit` 标志位。
2701fn parse_body_pr(xml: &str) -> crate::Result<BodyProperties> {
2702    let mut bp = BodyProperties::default();
2703    let mut rd = Reader::from_str(xml);
2704    rd.config_mut().trim_text(true);
2705    let mut buf = Vec::new();
2706    loop {
2707        match rd.read_event_into(&mut buf) {
2708            Ok(Event::Start(e)) => {
2709                let name = e.name();
2710                let local = local_name(name.as_ref());
2711                if local == b"bodyPr" {
2712                    // 根元素:解析属性
2713                    apply_body_pr_attrs(&mut bp, &e);
2714                } else if local == b"spAutoFit" {
2715                    bp.sp_auto_fit = true;
2716                    let _ = collect_full_element(&mut rd, e.into_owned());
2717                } else if local == b"normAutofit" {
2718                    bp.norm_autofit = true;
2719                    let _ = collect_full_element(&mut rd, e.into_owned());
2720                } else {
2721                    let _ = collect_full_element(&mut rd, e.into_owned());
2722                }
2723            }
2724            Ok(Event::Empty(e)) => {
2725                let name = e.name();
2726                let local = local_name(name.as_ref());
2727                if local == b"bodyPr" {
2728                    apply_body_pr_attrs(&mut bp, &e);
2729                } else if local == b"spAutoFit" {
2730                    bp.sp_auto_fit = true;
2731                } else if local == b"normAutofit" {
2732                    bp.norm_autofit = true;
2733                }
2734            }
2735            Ok(Event::Eof) => break,
2736            Err(e) => return Err(crate::Error::Xml(format!("bodyPr parse: {e}"))),
2737            _ => {}
2738        }
2739        buf.clear();
2740    }
2741    Ok(bp)
2742}
2743
2744/// 从自闭合 `<a:bodyPr .../>` 事件解析 [`BodyProperties`]。
2745fn parse_body_pr_attrs(e: &quick_xml::events::BytesStart<'_>) -> BodyProperties {
2746    let mut bp = BodyProperties::default();
2747    apply_body_pr_attrs(&mut bp, e);
2748    bp
2749}
2750
2751/// 把 `<a:bodyPr>` 元素的属性应用到 [`BodyProperties`]。
2752///
2753/// 支持的属性:`numCol` / `spcCol` / `anchor` / `wrap` / `vert` / `rot` /
2754/// `lIns` / `tIns` / `rIns` / `bIns`。
2755fn apply_body_pr_attrs(bp: &mut BodyProperties, e: &quick_xml::events::BytesStart<'_>) {
2756    for a in e.attributes().flatten() {
2757        match a.key.as_ref() {
2758            b"numCol" => {
2759                let v = a
2760                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
2761                    .unwrap_or_default();
2762                bp.num_cols = v.parse::<u32>().ok();
2763            }
2764            b"spcCol" => {
2765                let v = a
2766                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
2767                    .unwrap_or_default();
2768                if let Ok(n) = v.parse::<i64>() {
2769                    bp.col_spacing = Some(Emu(n));
2770                }
2771            }
2772            b"anchor" => {
2773                let v = a
2774                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
2775                    .unwrap_or_default();
2776                bp.anchor = match v.as_ref() {
2777                    "t" => Some(MsoAnchor::Top),
2778                    "ctr" => Some(MsoAnchor::Middle),
2779                    "b" => Some(MsoAnchor::Bottom),
2780                    _ => None,
2781                };
2782            }
2783            b"wrap" => {
2784                let v = a
2785                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
2786                    .unwrap_or_default();
2787                bp.wrap = match v.as_ref() {
2788                    "square" => Some(TextWrapping::Square),
2789                    "none" => Some(TextWrapping::None),
2790                    _ => None,
2791                };
2792            }
2793            b"vert" => {
2794                let v = a
2795                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
2796                    .unwrap_or_default()
2797                    .to_string();
2798                bp.vertical = Some(v);
2799            }
2800            b"rot" => {
2801                let v = a
2802                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
2803                    .unwrap_or_default();
2804                bp.rotation = v.parse::<i32>().ok();
2805            }
2806            b"lIns" => {
2807                let v = a
2808                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
2809                    .unwrap_or_default();
2810                if let Ok(n) = v.parse::<i64>() {
2811                    bp.insets.get_or_insert_with(Default::default).left = Emu(n);
2812                }
2813            }
2814            b"tIns" => {
2815                let v = a
2816                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
2817                    .unwrap_or_default();
2818                if let Ok(n) = v.parse::<i64>() {
2819                    bp.insets.get_or_insert_with(Default::default).top = Emu(n);
2820                }
2821            }
2822            b"rIns" => {
2823                let v = a
2824                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
2825                    .unwrap_or_default();
2826                if let Ok(n) = v.parse::<i64>() {
2827                    bp.insets.get_or_insert_with(Default::default).right = Emu(n);
2828                }
2829            }
2830            b"bIns" => {
2831                let v = a
2832                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
2833                    .unwrap_or_default();
2834                if let Ok(n) = v.parse::<i64>() {
2835                    bp.insets.get_or_insert_with(Default::default).bottom = Emu(n);
2836                }
2837            }
2838            _ => {}
2839        }
2840    }
2841}
2842
2843/// 解析 `a:p` 元素(段落)。
2844pub fn parse_paragraph(xml: &str) -> crate::Result<Paragraph> {
2845    let mut p = Paragraph::new();
2846    let mut rd = Reader::from_str(xml);
2847    rd.config_mut().trim_text(true);
2848    let mut buf = Vec::new();
2849    let mut in_ppr = false;
2850    let mut ppr_done = false;
2851    loop {
2852        match rd.read_event_into(&mut buf) {
2853            Ok(Event::Start(e)) => {
2854                let name = e.name();
2855                let local = local_name(name.as_ref());
2856                if local == b"p" {
2857                    // 根元素 <a:p>:仅标记进入,不调用 collect_full_element
2858                    // (否则会吞掉所有子元素,导致 pPr / r 等无法解析)
2859                } else if local == b"pPr" {
2860                    let inner = collect_full_element(&mut rd, e.into_owned())?;
2861                    p.properties = parse_paragraph_properties(&inner)?;
2862                    ppr_done = true;
2863                } else if local == b"r" {
2864                    let inner = collect_full_element(&mut rd, e.into_owned())?;
2865                    if let Ok(r) = parse_run(&inner) {
2866                        p.runs.push(r);
2867                    }
2868                } else if local == b"fld" {
2869                    // 字段元素 <a:fld id="..." type="...">
2870                    let inner = collect_full_element(&mut rd, e.into_owned())?;
2871                    if let Ok(f) = parse_field(&inner) {
2872                        p.fields.push(f);
2873                    }
2874                } else if local == b"br" {
2875                    // 段落内的换行
2876                    p.runs.push(Run::line_break());
2877                } else if local == b"endParaRPr" {
2878                    // 带子元素的 endParaRPr(如 <a:endParaRPr lang="en-US"><a:latin typeface="..."/></a:endParaRPr>)
2879                    // 收集完整元素后用 parse_run_properties 解析属性 + 子元素
2880                    let inner = collect_full_element(&mut rd, e.into_owned())?;
2881                    p.end_properties = Some(parse_run_properties(&inner)?);
2882                } else {
2883                    let _ = collect_full_element(&mut rd, e.into_owned());
2884                }
2885                in_ppr = false;
2886            }
2887            Ok(Event::Empty(e)) => {
2888                let name = e.name();
2889                let local = local_name(name.as_ref());
2890                if local == b"pPr" {
2891                    // 自闭合 pPr
2892                    p.properties = parse_paragraph_properties_attrs(&e);
2893                    ppr_done = true;
2894                } else if local == b"endParaRPr" {
2895                    // 自闭合 endParaRPr
2896                    p.end_properties = Some(RunProperties::from_bytes_start(&e));
2897                } else if local == b"fld" {
2898                    // 自闭合 <a:fld/>(无文本,罕见但合法)
2899                    if let Some(f) = parse_field_attrs(&e) {
2900                        p.fields.push(f);
2901                    }
2902                }
2903            }
2904            Ok(Event::End(_)) => {
2905                if !ppr_done {
2906                    in_ppr = false;
2907                }
2908            }
2909            Ok(Event::Eof) => break,
2910            Err(e) => return Err(crate::Error::Xml(format!("paragraph parse: {e}"))),
2911            _ => {}
2912        }
2913        buf.clear();
2914    }
2915    let _ = in_ppr;
2916    Ok(p)
2917}
2918
2919fn parse_paragraph_properties(xml: &str) -> crate::Result<ParagraphProperties> {
2920    let mut ppr = ParagraphProperties::default();
2921    let mut rd = Reader::from_str(xml);
2922    rd.config_mut().trim_text(true);
2923    let mut buf = Vec::new();
2924    loop {
2925        match rd.read_event_into(&mut buf) {
2926            Ok(Event::Start(e)) => {
2927                let name = e.name();
2928                let local = local_name(name.as_ref());
2929                if local == b"pPr" {
2930                    // 根元素 <a:pPr>:仅标记进入,不调用 collect_full_element
2931                    // (否则会吞掉所有子元素,导致 lnSpc / spcBef 等无法解析)
2932                } else if local == b"lnSpc" {
2933                    parse_ln_spc_into(&mut rd, &mut ppr, e.into_owned())?;
2934                } else if local == b"spcBef" {
2935                    parse_spc_bef_into(&mut rd, &mut ppr, e.into_owned())?;
2936                } else if local == b"spcAft" {
2937                    parse_spc_aft_into(&mut rd, &mut ppr, e.into_owned())?;
2938                } else if local == b"buNone" {
2939                    // 无项目符号:保留详细信息到 bullet_style
2940                    ppr.bullet = false;
2941                    ppr.bullet_style = Some(BulletStyle::None);
2942                    let _ = collect_full_element(&mut rd, e.into_owned());
2943                } else if local == b"buChar" {
2944                    // 自定义字符项目符号:保留 char 属性
2945                    let bs = parse_bu_char(&e);
2946                    ppr.bullet = true;
2947                    ppr.bullet_style = Some(bs);
2948                    let _ = collect_full_element(&mut rd, e.into_owned());
2949                } else if local == b"buAutoNum" {
2950                    // 自动编号项目符号:保留 type/startAt 属性
2951                    let bs = parse_bu_auto_num(&e);
2952                    ppr.bullet = true;
2953                    ppr.bullet_style = Some(bs);
2954                    let _ = collect_full_element(&mut rd, e.into_owned());
2955                } else if local == b"tabLst" {
2956                    // 制表位列表:解析所有 <a:tab> 子元素
2957                    let inner = collect_full_element(&mut rd, e.into_owned())?;
2958                    ppr.tab_stops = parse_tab_lst(&inner)?;
2959                } else {
2960                    let _ = collect_full_element(&mut rd, e.into_owned());
2961                }
2962            }
2963            Ok(Event::Empty(e)) => {
2964                let name = e.name();
2965                let local = local_name(name.as_ref());
2966                if local == b"buNone" {
2967                    ppr.bullet = false;
2968                    ppr.bullet_style = Some(BulletStyle::None);
2969                } else if local == b"buChar" {
2970                    ppr.bullet = true;
2971                    ppr.bullet_style = Some(parse_bu_char(&e));
2972                } else if local == b"buAutoNum" {
2973                    ppr.bullet = true;
2974                    ppr.bullet_style = Some(parse_bu_auto_num(&e));
2975                } else if local == b"tab" {
2976                    // 自闭合 <a:tab/>(在 tabLst 外罕见,但兼容)
2977                    if let Some(tab) = parse_tab(&e) {
2978                        ppr.tab_stops.push(tab);
2979                    }
2980                } else {
2981                    apply_ppr_attrs(&e, &mut ppr);
2982                }
2983            }
2984            Ok(Event::Eof) => break,
2985            Err(e) => return Err(crate::Error::Xml(format!("pPr parse: {e}"))),
2986            _ => {}
2987        }
2988        buf.clear();
2989    }
2990    Ok(ppr)
2991}
2992
2993fn parse_paragraph_properties_attrs(e: &quick_xml::events::BytesStart<'_>) -> ParagraphProperties {
2994    let mut ppr = ParagraphProperties::default();
2995    apply_ppr_attrs(e, &mut ppr);
2996    ppr
2997}
2998
2999/// 从 `<a:buChar char="..."/>` 事件解析 [`BulletStyle::Char`]。
3000///
3001/// 提取 `char` 属性(项目符号字符,如 `"•"` / `"▪"` / `"→"`)。
3002fn parse_bu_char(e: &quick_xml::events::BytesStart<'_>) -> BulletStyle {
3003    let mut ch = String::new();
3004    for a in e.attributes().flatten() {
3005        if a.key.as_ref() == b"char" {
3006            ch = a
3007                .normalized_value(quick_xml::XmlVersion::Implicit1_0)
3008                .unwrap_or_default()
3009                .to_string();
3010        }
3011    }
3012    BulletStyle::Char { char: ch }
3013}
3014
3015/// 从 `<a:buAutoNum type="..." startAt="..."/>` 事件解析 [`BulletStyle::AutoNum`]。
3016///
3017/// 提取 `type` 属性(编号类型,如 `"arabicPeriod"` / `"alphaLcParenR"` / `"romanLcParenBoth"`)
3018/// 和可选的 `startAt` 属性(起始编号)。
3019fn parse_bu_auto_num(e: &quick_xml::events::BytesStart<'_>) -> BulletStyle {
3020    let mut auto_num_type = String::new();
3021    let mut start_at: Option<u32> = None;
3022    for a in e.attributes().flatten() {
3023        match a.key.as_ref() {
3024            b"type" => {
3025                auto_num_type = a
3026                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
3027                    .unwrap_or_default()
3028                    .to_string();
3029            }
3030            b"startAt" => {
3031                let v = a
3032                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
3033                    .unwrap_or_default();
3034                start_at = v.parse::<u32>().ok();
3035            }
3036            _ => {}
3037        }
3038    }
3039    BulletStyle::AutoNum {
3040        auto_num_type,
3041        start_at,
3042    }
3043}
3044
3045/// 从 `<a:tabLst>...</a:tabLst>` XML 字符串解析制表位列表。
3046///
3047/// 提取所有 `<a:tab pos="..." algn="..."/>` 子元素。
3048fn parse_tab_lst(xml: &str) -> crate::Result<Vec<TabStop>> {
3049    let mut tabs = Vec::new();
3050    let mut rd = Reader::from_str(xml);
3051    rd.config_mut().trim_text(true);
3052    let mut buf = Vec::new();
3053    loop {
3054        match rd.read_event_into(&mut buf) {
3055            Ok(Event::Start(e)) => {
3056                let name = e.name();
3057                let local = local_name(name.as_ref());
3058                if local == b"tabLst" {
3059                    // 根元素 <a:tabLst>:仅标记进入,不调用 collect_full_element
3060                    // (否则会吞掉所有 <a:tab> 子元素)
3061                } else if local == b"tab" {
3062                    if let Some(tab) = parse_tab(&e) {
3063                        tabs.push(tab);
3064                    }
3065                    let _ = collect_full_element(&mut rd, e.into_owned());
3066                } else {
3067                    let _ = collect_full_element(&mut rd, e.into_owned());
3068                }
3069            }
3070            Ok(Event::Empty(e)) => {
3071                let name = e.name();
3072                let local = local_name(name.as_ref());
3073                if local == b"tab" {
3074                    if let Some(tab) = parse_tab(&e) {
3075                        tabs.push(tab);
3076                    }
3077                }
3078            }
3079            Ok(Event::Eof) => break,
3080            Err(e) => return Err(crate::Error::Xml(format!("tabLst parse: {e}"))),
3081            _ => {}
3082        }
3083        buf.clear();
3084    }
3085    Ok(tabs)
3086}
3087
3088/// 从 `<a:tab pos="..." algn="..."/>` 事件解析单个 [`TabStop`]。
3089///
3090/// 返回 `None` 表示缺少 `pos` 属性或解析失败。
3091fn parse_tab(e: &quick_xml::events::BytesStart<'_>) -> Option<TabStop> {
3092    let mut pos: Option<i64> = None;
3093    let mut alignment = TabAlignment::Left;
3094    for a in e.attributes().flatten() {
3095        match a.key.as_ref() {
3096            b"pos" => {
3097                let v = a
3098                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
3099                    .unwrap_or_default();
3100                pos = v.parse::<i64>().ok();
3101            }
3102            b"algn" => {
3103                let v = a
3104                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
3105                    .unwrap_or_default();
3106                if let Ok(al) = v.parse::<TabAlignment>() {
3107                    alignment = al;
3108                }
3109            }
3110            _ => {}
3111        }
3112    }
3113    pos.map(|p| TabStop {
3114        pos: Emu(p),
3115        alignment,
3116    })
3117}
3118
3119/// 从 `<a:fld id="..." type="...">...</a:fld>` XML 字符串解析 [`Field`]。
3120///
3121/// 提取 `id` / `type` 属性,以及 `<a:rPr>` 和 `<a:t>` 子元素。
3122fn parse_field(xml: &str) -> crate::Result<Field> {
3123    let mut field = Field::default();
3124    let mut rd = Reader::from_str(xml);
3125    rd.config_mut().trim_text(true);
3126    let mut buf = Vec::new();
3127    loop {
3128        match rd.read_event_into(&mut buf) {
3129            Ok(Event::Start(e)) => {
3130                let name = e.name();
3131                let local = local_name(name.as_ref());
3132                if local == b"fld" {
3133                    // 根元素:提取 id / type 属性
3134                    apply_field_attrs(&mut field, &e);
3135                } else if local == b"rPr" {
3136                    let inner = collect_full_element(&mut rd, e.into_owned())?;
3137                    field.properties = parse_run_properties(&inner)?;
3138                } else if local == b"t" {
3139                    let inner = collect_full_element(&mut rd, e.into_owned())?;
3140                    field.text = extract_text_content(&inner);
3141                } else {
3142                    let _ = collect_full_element(&mut rd, e.into_owned());
3143                }
3144            }
3145            Ok(Event::Empty(e)) => {
3146                let name = e.name();
3147                let local = local_name(name.as_ref());
3148                if local == b"fld" {
3149                    apply_field_attrs(&mut field, &e);
3150                } else if local == b"rPr" {
3151                    field.properties = RunProperties::from_bytes_start(&e);
3152                }
3153                // <a:t/> 自闭合(空文本)——text 保持默认空字符串
3154            }
3155            Ok(Event::Eof) => break,
3156            Err(e) => return Err(crate::Error::Xml(format!("fld parse: {e}"))),
3157            _ => {}
3158        }
3159        buf.clear();
3160    }
3161    Ok(field)
3162}
3163
3164/// 从自闭合 `<a:fld id="..." type="..."/>` 事件解析 [`Field`]。
3165///
3166/// 仅提取属性,text 为空。
3167fn parse_field_attrs(e: &quick_xml::events::BytesStart<'_>) -> Option<Field> {
3168    let mut field = Field::default();
3169    let mut has_type = false;
3170    for a in e.attributes().flatten() {
3171        match a.key.as_ref() {
3172            b"id" => {
3173                field.id = a
3174                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
3175                    .unwrap_or_default()
3176                    .to_string();
3177            }
3178            b"type" => {
3179                let v = a
3180                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
3181                    .unwrap_or_default();
3182                field.field_type = FieldType::from_str_value(&v);
3183                has_type = true;
3184            }
3185            _ => {}
3186        }
3187    }
3188    if has_type {
3189        Some(field)
3190    } else {
3191        None
3192    }
3193}
3194
3195/// 把 `<a:fld>` 元素的属性应用到 [`Field`]。
3196fn apply_field_attrs(field: &mut Field, e: &quick_xml::events::BytesStart<'_>) {
3197    for a in e.attributes().flatten() {
3198        match a.key.as_ref() {
3199            b"id" => {
3200                field.id = a
3201                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
3202                    .unwrap_or_default()
3203                    .to_string();
3204            }
3205            b"type" => {
3206                let v = a
3207                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
3208                    .unwrap_or_default();
3209                field.field_type = FieldType::from_str_value(&v);
3210            }
3211            _ => {}
3212        }
3213    }
3214}
3215
3216/// 从 `<a:t>...</a:t>` XML 字符串提取纯文本内容。
3217fn extract_text_content(xml: &str) -> String {
3218    let mut text = String::new();
3219    let mut rd = Reader::from_str(xml);
3220    rd.config_mut().trim_text(true);
3221    let mut buf = Vec::new();
3222    loop {
3223        match rd.read_event_into(&mut buf) {
3224            Ok(Event::Text(t)) => {
3225                text.push_str(&t.decode().unwrap_or_default());
3226            }
3227            Ok(Event::Eof) => break,
3228            Err(_) => break,
3229            _ => {}
3230        }
3231        buf.clear();
3232    }
3233    text
3234}
3235
3236fn apply_ppr_attrs(e: &quick_xml::events::BytesStart<'_>, ppr: &mut ParagraphProperties) {
3237    for a in e.attributes().flatten() {
3238        match a.key.as_ref() {
3239            b"algn" => {
3240                let v = a
3241                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
3242                    .unwrap_or_default();
3243                if let Ok(al) = v.parse::<Alignment>() {
3244                    ppr.alignment = Some(al);
3245                }
3246            }
3247            b"lvl" => {
3248                if let Ok(v) = a
3249                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
3250                    .unwrap_or_default()
3251                    .parse::<u8>()
3252                {
3253                    ppr.level = v;
3254                }
3255            }
3256            b"indent" | b"marL" => {
3257                if let Ok(v) = a
3258                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
3259                    .unwrap_or_default()
3260                    .parse::<i64>()
3261                {
3262                    ppr.indent.left = Some(Emu(v));
3263                }
3264            }
3265            #[allow(unreachable_patterns)]
3266            b"indent" | b"r" => {
3267                if let Ok(v) = a
3268                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
3269                    .unwrap_or_default()
3270                    .parse::<i64>()
3271                {
3272                    ppr.indent.right = Some(Emu(v));
3273                }
3274            }
3275            #[allow(unreachable_patterns)]
3276            b"indent" | b"firstLine" => {
3277                if let Ok(v) = a
3278                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
3279                    .unwrap_or_default()
3280                    .parse::<i64>()
3281                {
3282                    ppr.indent.first_line = Some(Emu(v));
3283                }
3284            }
3285            _ => {}
3286        }
3287    }
3288}
3289
3290fn parse_ln_spc_into<R: std::io::BufRead>(
3291    rd: &mut Reader<R>,
3292    ppr: &mut ParagraphProperties,
3293    _start: quick_xml::events::BytesStart<'static>,
3294) -> crate::Result<()> {
3295    let mut buf = Vec::new();
3296    loop {
3297        match rd.read_event_into(&mut buf) {
3298            Ok(Event::Empty(e)) => {
3299                let name = e.name();
3300                let local = local_name(name.as_ref());
3301                if local == b"spcPct" {
3302                    for a in e.attributes().flatten() {
3303                        if a.key.as_ref() == b"val" {
3304                            if let Ok(v) = a
3305                                .normalized_value(quick_xml::XmlVersion::Implicit1_0)
3306                                .unwrap_or_default()
3307                                .parse::<i32>()
3308                            {
3309                                ppr.line_spacing_pct = Some(v);
3310                            }
3311                        }
3312                    }
3313                } else if local == b"spcPts" {
3314                    for a in e.attributes().flatten() {
3315                        if a.key.as_ref() == b"val" {
3316                            if let Ok(v) = a
3317                                .normalized_value(quick_xml::XmlVersion::Implicit1_0)
3318                                .unwrap_or_default()
3319                                .parse::<i32>()
3320                            {
3321                                ppr.line_spacing = Some(v);
3322                            }
3323                        }
3324                    }
3325                }
3326            }
3327            Ok(Event::End(e)) => {
3328                if local_name(e.name().as_ref()) == b"lnSpc" {
3329                    return Ok(());
3330                }
3331            }
3332            Ok(Event::Eof) => return Ok(()),
3333            _ => {}
3334        }
3335        buf.clear();
3336    }
3337}
3338
3339fn parse_spc_bef_into<R: std::io::BufRead>(
3340    rd: &mut Reader<R>,
3341    ppr: &mut ParagraphProperties,
3342    start: quick_xml::events::BytesStart<'static>,
3343) -> crate::Result<()> {
3344    let _ = start;
3345    let mut buf = Vec::new();
3346    loop {
3347        match rd.read_event_into(&mut buf) {
3348            Ok(Event::Empty(e)) => {
3349                let name = e.name();
3350                let local = local_name(name.as_ref());
3351                if local == b"spcPts" {
3352                    for a in e.attributes().flatten() {
3353                        if a.key.as_ref() == b"val" {
3354                            if let Ok(v) = a
3355                                .normalized_value(quick_xml::XmlVersion::Implicit1_0)
3356                                .unwrap_or_default()
3357                                .parse::<i32>()
3358                            {
3359                                ppr.space_before = Some(Emu(v as i64 * 100)); // 1pt = 100 EMU
3360                            }
3361                        }
3362                    }
3363                }
3364            }
3365            Ok(Event::End(e)) => {
3366                let name = e.name();
3367                if local_name(name.as_ref()) == b"spcBef" {
3368                    return Ok(());
3369                }
3370            }
3371            Ok(Event::Eof) => return Ok(()),
3372            _ => {}
3373        }
3374        buf.clear();
3375    }
3376}
3377
3378fn parse_spc_aft_into<R: std::io::BufRead>(
3379    rd: &mut Reader<R>,
3380    ppr: &mut ParagraphProperties,
3381    start: quick_xml::events::BytesStart<'static>,
3382) -> crate::Result<()> {
3383    let _ = start;
3384    let mut buf = Vec::new();
3385    loop {
3386        match rd.read_event_into(&mut buf) {
3387            Ok(Event::Empty(e)) => {
3388                let name = e.name();
3389                let local = local_name(name.as_ref());
3390                if local == b"spcPts" {
3391                    for a in e.attributes().flatten() {
3392                        if a.key.as_ref() == b"val" {
3393                            if let Ok(v) = a
3394                                .normalized_value(quick_xml::XmlVersion::Implicit1_0)
3395                                .unwrap_or_default()
3396                                .parse::<i32>()
3397                            {
3398                                ppr.space_after = Some(Emu(v as i64 * 100));
3399                            }
3400                        }
3401                    }
3402                }
3403            }
3404            Ok(Event::End(e)) => {
3405                let name = e.name();
3406                if local_name(name.as_ref()) == b"spcAft" {
3407                    return Ok(());
3408                }
3409            }
3410            Ok(Event::Eof) => return Ok(()),
3411            _ => {}
3412        }
3413        buf.clear();
3414    }
3415}
3416
3417/// 解析 `a:r` 元素(Run)。
3418pub fn parse_run(xml: &str) -> crate::Result<Run> {
3419    let mut run = Run::default();
3420    let mut rd = Reader::from_str(xml);
3421    rd.config_mut().trim_text(true);
3422    let mut buf = Vec::new();
3423    let mut rpr_props: Option<RunProperties> = None;
3424    loop {
3425        match rd.read_event_into(&mut buf) {
3426            Ok(Event::Start(e)) => {
3427                let name = e.name();
3428                let local = local_name(name.as_ref());
3429                if local == b"r" {
3430                    // 根元素 <a:r>:仅标记进入,不调用 collect_full_element
3431                    // (否则会吞掉所有子元素,导致 rPr / t 等无法解析)
3432                } else if local == b"rPr" {
3433                    let inner = collect_full_element(&mut rd, e.into_owned())?;
3434                    rpr_props = Some(parse_run_properties(&inner)?);
3435                } else if local == b"t" {
3436                    // 文本内容
3437                    let txt = collect_inner_text_only(&mut rd)?;
3438                    run.text = txt;
3439                } else {
3440                    let _ = collect_full_element(&mut rd, e.into_owned());
3441                }
3442            }
3443            Ok(Event::Empty(e)) => {
3444                let name = e.name();
3445                let local = local_name(name.as_ref());
3446                if local == b"rPr" {
3447                    // 自闭合 rPr
3448                    rpr_props = Some(RunProperties::from_bytes_start(&e));
3449                } else if local == b"t" {
3450                    // 自闭合:空文本
3451                }
3452            }
3453            Ok(Event::Eof) => break,
3454            Err(e) => return Err(crate::Error::Xml(format!("run parse: {e}"))),
3455            _ => {}
3456        }
3457        buf.clear();
3458    }
3459    if let Some(p) = rpr_props {
3460        run.properties = p;
3461    }
3462    Ok(run)
3463}
3464
3465/// 收集当前 Start 之后的**所有**事件(包括子元素)直到匹配的 End 出现。
3466/// 返回完整 XML 字符串。
3467pub fn collect_full_element<R: std::io::BufRead>(
3468    rd: &mut Reader<R>,
3469    start: quick_xml::events::BytesStart<'static>,
3470) -> crate::Result<String> {
3471    let mut out = String::new();
3472    // 写开始标签
3473    out.push('<');
3474    // 绑定到本地变量以延长临时值的生命周期(quick-xml 0.40 的 `name()` 返回
3475    // 借用的 `QName<'_>`,直接 .as_ref() 链式写法会被新版 borrow checker 拒绝)。
3476    let name = start.name();
3477    out.push_str(&String::from_utf8_lossy(name.as_ref()));
3478    for a in start.attributes().flatten() {
3479        out.push(' ');
3480        out.push_str(&String::from_utf8_lossy(a.key.as_ref()));
3481        out.push_str("=\"");
3482        out.push_str(
3483            &a.normalized_value(quick_xml::XmlVersion::Implicit1_0)
3484                .unwrap_or_default(),
3485        );
3486        out.push('"');
3487    }
3488    out.push('>');
3489    // 递归
3490    let mut depth = 1i32;
3491    let mut buf = Vec::new();
3492    loop {
3493        match rd.read_event_into(&mut buf) {
3494            Ok(Event::Start(e)) => {
3495                depth += 1;
3496                write_event_start(&mut out, &e);
3497            }
3498            Ok(Event::End(e)) => {
3499                depth -= 1;
3500                write_event_end(&mut out, &e);
3501                if depth == 0 {
3502                    return Ok(out);
3503                }
3504            }
3505            Ok(Event::Empty(e)) => {
3506                write_event_empty(&mut out, &e);
3507            }
3508            Ok(Event::Text(t)) => {
3509                out.push_str(&t.decode().unwrap_or_default());
3510            }
3511            Ok(Event::CData(c)) => {
3512                // 直接拼字符串,不走 `write!`(String 不实现 `std::io::Write`)。
3513                out.push_str("<![CDATA[");
3514                let cd = String::from_utf8_lossy(c.as_ref());
3515                out.push_str(&cd);
3516                out.push_str("]]>");
3517            }
3518            Ok(Event::Eof) => return Ok(out),
3519            Err(e) => return Err(crate::Error::Xml(format!("collect: {e}"))),
3520            _ => {}
3521        }
3522        buf.clear();
3523    }
3524}
3525
3526/// 与 [`collect_full_element`] 类似,但**不**保留原始 XML,只跳过事件。
3527/// 用于"我们知道这个元素不需要解析、但仍要消费完以保持 reader 状态正确"的场景。
3528pub fn collect_full_element_skipping<R: std::io::BufRead>(
3529    rd: &mut Reader<R>,
3530    _start: quick_xml::events::BytesStart<'static>,
3531) {
3532    let mut depth = 1i32;
3533    let mut buf = Vec::new();
3534    loop {
3535        match rd.read_event_into(&mut buf) {
3536            Ok(Event::Start(_)) => {
3537                depth += 1;
3538            }
3539            Ok(Event::End(_)) => {
3540                depth -= 1;
3541                if depth == 0 {
3542                    return;
3543                }
3544            }
3545            Ok(Event::Eof) => return,
3546            _ => {}
3547        }
3548        buf.clear();
3549    }
3550}
3551
3552fn write_event_start(out: &mut String, e: &quick_xml::events::BytesStart<'_>) {
3553    out.push('<');
3554    // 同 `collect_full_element`:绑定到本地变量延长 `QName<'_>` 借用的生命周期。
3555    let name = e.name();
3556    out.push_str(&String::from_utf8_lossy(name.as_ref()));
3557    for a in e.attributes().flatten() {
3558        out.push(' ');
3559        out.push_str(&String::from_utf8_lossy(a.key.as_ref()));
3560        out.push_str("=\"");
3561        out.push_str(
3562            &a.normalized_value(quick_xml::XmlVersion::Implicit1_0)
3563                .unwrap_or_default(),
3564        );
3565        out.push('"');
3566    }
3567    out.push('>');
3568}
3569
3570fn write_event_empty(out: &mut String, e: &quick_xml::events::BytesStart<'_>) {
3571    out.push('<');
3572    let name = e.name();
3573    out.push_str(&String::from_utf8_lossy(name.as_ref()));
3574    for a in e.attributes().flatten() {
3575        out.push(' ');
3576        out.push_str(&String::from_utf8_lossy(a.key.as_ref()));
3577        out.push_str("=\"");
3578        out.push_str(
3579            &a.normalized_value(quick_xml::XmlVersion::Implicit1_0)
3580                .unwrap_or_default(),
3581        );
3582        out.push('"');
3583    }
3584    out.push_str("/>");
3585}
3586
3587fn write_event_end(out: &mut String, e: &quick_xml::events::BytesEnd<'_>) {
3588    out.push_str("</");
3589    let name = e.name();
3590    out.push_str(&String::from_utf8_lossy(name.as_ref()));
3591    out.push('>');
3592}
3593
3594/// 解析 `a:rPr` 元素(含子元素如 `<a:solidFill>` 等)。
3595pub fn parse_run_properties(xml: &str) -> crate::Result<RunProperties> {
3596    let mut rp = RunProperties::default();
3597    let mut rd = Reader::from_str(xml);
3598    rd.config_mut().trim_text(true);
3599    let mut buf = Vec::new();
3600    loop {
3601        match rd.read_event_into(&mut buf) {
3602            Ok(Event::Start(e)) => {
3603                let name = e.name();
3604                let local = local_name(name.as_ref());
3605                if local == b"rPr" || local == b"endParaRPr" {
3606                    // 根元素 <a:rPr> 或 <a:endParaRPr>:
3607                    // 1. 解析起始标签上的属性(sz/b/i/u/strike/lang/...)
3608                    // 2. 仅标记进入,不调用 collect_full_element
3609                    //    (否则会吞掉所有子元素,导致 solidFill / latin 等无法解析)
3610                    // 先把起始标签属性读入 rp,后续子元素解析会补充 color/font 等
3611                    rp = RunProperties::from_bytes_start(&e);
3612                } else if local == b"solidFill" {
3613                    rp.color = parse_solid_fill(&mut rd, e.into_owned())?;
3614                } else if local == b"highlight" {
3615                    let color = parse_solid_fill(&mut rd, e.into_owned())?;
3616                    rp.highlight = Some(color);
3617                } else if local == b"latin" {
3618                    rp.latin_font = Some(collect_attr_value(&mut rd, "typeface")?);
3619                } else if local == b"ea" {
3620                    rp.eastasia_font = Some(collect_attr_value(&mut rd, "typeface")?);
3621                } else if local == b"cs" {
3622                    rp.cs_font = Some(collect_attr_value(&mut rd, "typeface")?);
3623                } else if local == b"hlinkClick" {
3624                    // 超链接(点击):提取 r:id / tooltip / action 属性,然后跳过子元素
3625                    let hl = parse_hyperlink_attrs(&e);
3626                    collect_full_element_skipping(&mut rd, e.into_owned());
3627                    rp.hlink_click = Some(hl);
3628                } else if local == b"hlinkHover" {
3629                    // 超链接(悬停):提取 r:id / tooltip / action 属性,然后跳过子元素
3630                    let hl = parse_hyperlink_attrs(&e);
3631                    collect_full_element_skipping(&mut rd, e.into_owned());
3632                    rp.hlink_hover = Some(hl);
3633                } else {
3634                    let _ = collect_full_element(&mut rd, e.into_owned());
3635                }
3636            }
3637            Ok(Event::Empty(e)) => {
3638                let name = e.name();
3639                let local = local_name(name.as_ref());
3640                if local == b"latin" {
3641                    for a in e.attributes().flatten() {
3642                        if a.key.as_ref() == b"typeface" {
3643                            rp.latin_font = Some(
3644                                a.normalized_value(quick_xml::XmlVersion::Implicit1_0)
3645                                    .unwrap_or_default()
3646                                    .to_string(),
3647                            );
3648                        }
3649                    }
3650                } else if local == b"ea" {
3651                    for a in e.attributes().flatten() {
3652                        if a.key.as_ref() == b"typeface" {
3653                            rp.eastasia_font = Some(
3654                                a.normalized_value(quick_xml::XmlVersion::Implicit1_0)
3655                                    .unwrap_or_default()
3656                                    .to_string(),
3657                            );
3658                        }
3659                    }
3660                } else if local == b"cs" {
3661                    for a in e.attributes().flatten() {
3662                        if a.key.as_ref() == b"typeface" {
3663                            rp.cs_font = Some(
3664                                a.normalized_value(quick_xml::XmlVersion::Implicit1_0)
3665                                    .unwrap_or_default()
3666                                    .to_string(),
3667                            );
3668                        }
3669                    }
3670                } else if local == b"hlinkClick" {
3671                    // 自闭合的超链接(点击):<a:hlinkClick r:id="..." tooltip="..."/>
3672                    rp.hlink_click = Some(parse_hyperlink_attrs(&e));
3673                } else if local == b"hlinkHover" {
3674                    // 自闭合的超链接(悬停):<a:hlinkHover r:id="..." tooltip="..."/>
3675                    rp.hlink_hover = Some(parse_hyperlink_attrs(&e));
3676                }
3677            }
3678            Ok(Event::Eof) => break,
3679            Err(e) => return Err(crate::Error::Xml(format!("rPr parse: {e}"))),
3680            _ => {}
3681        }
3682        buf.clear();
3683    }
3684    Ok(rp)
3685}
3686
3687/// 从 `<a:hlinkClick>` / `<a:hlinkHover>` 事件中提取属性,构造 [`Hyperlink`]。
3688///
3689/// 提取的属性:
3690/// - `r:id`(或任意 `:id` 后缀):关系 ID,指向 `.rels` 中的目标 URL;
3691/// - `tooltip`:鼠标悬停提示;
3692/// - `action`:动作类型(如 `"ppaction://hlinksldjump"` 跳转幻灯片)。
3693///
3694/// 若三个属性均缺失,返回的 `Hyperlink` 的 `invalid` 标记为 `true`(用于继承场景)。
3695fn parse_hyperlink_attrs(e: &quick_xml::events::BytesStart<'_>) -> Hyperlink {
3696    let mut hl = Hyperlink::default();
3697    let mut has_any = false;
3698    for a in e.attributes().flatten() {
3699        let key = a.key.as_ref();
3700        let val = a
3701            .normalized_value(quick_xml::XmlVersion::Implicit1_0)
3702            .unwrap_or_default()
3703            .to_string();
3704        // r:id 可能以不同前缀出现(r:id / ns0:id 等),统一用后缀匹配
3705        if key == b"r:id" || key.ends_with(b":id") {
3706            hl.rid = Some(val);
3707            has_any = true;
3708        } else if key == b"tooltip" {
3709            hl.tooltip = Some(val);
3710            has_any = true;
3711        } else if key == b"action" {
3712            hl.action = Some(val);
3713            has_any = true;
3714        }
3715    }
3716    if !has_any {
3717        hl.invalid = true;
3718    }
3719    hl
3720}
3721
3722/// 从 `<a:tile>` 事件中提取属性,构造 [`BlipFillMode::Tile`]。
3723///
3724/// 提取的属性:
3725/// - `tx` / `ty`:水平/垂直偏移(EMU);
3726/// - `sx` / `sy`:水平/垂直缩放(千分比,100000 = 100%);
3727/// - `flip`:翻转模式(`"none"` / `"x"` / `"y"` / `"xy"`);
3728/// - `algn`:对齐方式(`"tl"` / `"ctr"` / `"br"` 等)。
3729fn parse_tile_attrs(e: &quick_xml::events::BytesStart<'_>) -> crate::oxml::sppr::BlipFillMode {
3730    let mut tx = None;
3731    let mut ty = None;
3732    let mut sx = None;
3733    let mut sy = None;
3734    let mut flip = None;
3735    let mut algn = None;
3736    for a in e.attributes().flatten() {
3737        let key = a.key.as_ref();
3738        let val = a
3739            .normalized_value(quick_xml::XmlVersion::Implicit1_0)
3740            .unwrap_or_default()
3741            .to_string();
3742        match key {
3743            b"tx" => tx = val.parse().ok(),
3744            b"ty" => ty = val.parse().ok(),
3745            b"sx" => sx = val.parse().ok(),
3746            b"sy" => sy = val.parse().ok(),
3747            b"flip" => flip = Some(val),
3748            b"algn" => algn = Some(val),
3749            _ => {}
3750        }
3751    }
3752    crate::oxml::sppr::BlipFillMode::Tile {
3753        tx,
3754        ty,
3755        sx,
3756        sy,
3757        flip,
3758        algn,
3759    }
3760}
3761
3762/// 从 `<a:spLocks>` 事件中提取锁定属性,构造 [`ShapeLocks`]。
3763///
3764/// 所有属性均为布尔值,值为 `"1"` 或 `"true"` 时设为 `true`。
3765fn parse_sp_locks_attrs(e: &quick_xml::events::BytesStart<'_>) -> ShapeLocks {
3766    let mut locks = ShapeLocks::default();
3767    for a in e.attributes().flatten() {
3768        let key = a.key.as_ref();
3769        let v = a
3770            .normalized_value(quick_xml::XmlVersion::Implicit1_0)
3771            .unwrap_or_default();
3772        let is_true = v == "1" || v == "true";
3773        match key {
3774            b"noGrp" => locks.no_grp = is_true,
3775            b"noDrilldown" => locks.no_drilldown = is_true,
3776            b"noSelect" => locks.no_select = is_true,
3777            b"noChangeAspect" => locks.no_change_aspect = is_true,
3778            b"noMove" => locks.no_move = is_true,
3779            b"noResize" => locks.no_resize = is_true,
3780            b"noRot" => locks.no_rot = is_true,
3781            b"noEditPoints" => locks.no_edit_points = is_true,
3782            b"noAdjustHandles" => locks.no_adjust_handles = is_true,
3783            b"noChangeArrowheads" => locks.no_change_arrowheads = is_true,
3784            b"noChangeShapeType" => locks.no_change_shape_type = is_true,
3785            b"noCrop" => locks.no_crop = is_true,
3786            _ => {}
3787        }
3788    }
3789    locks
3790}
3791
3792/// 解析 `<p:style>` XML 为 [`ShapeStyle`]。
3793///
3794/// `<p:style>` 包含 4 个子元素(按 OOXML 顺序):
3795/// - `<a:lnRef idx="..." schemeClr="..."/>` 线条样式引用
3796/// - `<a:fillRef idx="..." schemeClr="..."/>` 填充样式引用
3797/// - `<a:effectRef idx="..." schemeClr="..."/>` 效果样式引用
3798/// - `<a:fontRef idx="minor|major" schemeClr="..."/>` 字体样式引用
3799pub fn parse_shape_style(xml: &str) -> crate::Result<ShapeStyle> {
3800    let mut style = ShapeStyle::default();
3801    let mut rd = Reader::from_str(xml);
3802    rd.config_mut().trim_text(true);
3803    let mut buf = Vec::new();
3804    loop {
3805        match rd.read_event_into(&mut buf) {
3806            Ok(Event::Start(e)) => {
3807                let name = e.name();
3808                let local = local_name(name.as_ref());
3809                match local {
3810                    b"lnRef" => {
3811                        style.line_ref = Some(parse_style_ref(&mut rd, &e)?);
3812                    }
3813                    b"fillRef" => {
3814                        style.fill_ref = Some(parse_style_ref(&mut rd, &e)?);
3815                    }
3816                    b"effectRef" => {
3817                        style.effect_ref = Some(parse_style_ref(&mut rd, &e)?);
3818                    }
3819                    b"fontRef" => {
3820                        style.font_ref = Some(parse_style_ref(&mut rd, &e)?);
3821                    }
3822                    b"style" => {
3823                        // 根元素 <p:style>:仅标记进入,不调用 collect_full_element
3824                    }
3825                    _ => {
3826                        let _ = collect_full_element(&mut rd, e.into_owned());
3827                    }
3828                }
3829            }
3830            Ok(Event::Empty(e)) => {
3831                let name = e.name();
3832                let local = local_name(name.as_ref());
3833                match local {
3834                    b"lnRef" => {
3835                        style.line_ref = Some(parse_style_ref_empty(&e));
3836                    }
3837                    b"fillRef" => {
3838                        style.fill_ref = Some(parse_style_ref_empty(&e));
3839                    }
3840                    b"effectRef" => {
3841                        style.effect_ref = Some(parse_style_ref_empty(&e));
3842                    }
3843                    b"fontRef" => {
3844                        style.font_ref = Some(parse_style_ref_empty(&e));
3845                    }
3846                    _ => {}
3847                }
3848            }
3849            Ok(Event::Eof) => break,
3850            Err(e) => return Err(crate::Error::Xml(format!("style parse: {e}"))),
3851            _ => {}
3852        }
3853        buf.clear();
3854    }
3855    Ok(style)
3856}
3857
3858/// 解析 `<a:lnRef>` / `<a:fillRef>` / `<a:effectRef>` / `<a:fontRef>` 的 Start 事件。
3859///
3860/// 提取 `idx` 属性和子元素 `<a:schemeClr val="..."/>`。
3861fn parse_style_ref<R: std::io::BufRead>(
3862    rd: &mut Reader<R>,
3863    e: &quick_xml::events::BytesStart<'_>,
3864) -> crate::Result<StyleRef> {
3865    let mut sr = parse_style_ref_empty(e);
3866    // 读取子元素直到匹配的 End
3867    let mut buf = Vec::new();
3868    loop {
3869        match rd.read_event_into(&mut buf) {
3870            Ok(Event::Empty(e2)) => {
3871                let name2 = e2.name();
3872                let local = local_name(name2.as_ref());
3873                if local == b"schemeClr" {
3874                    for a in e2.attributes().flatten() {
3875                        if a.key.as_ref() == b"val" {
3876                            sr.scheme_color = Some(
3877                                a.normalized_value(quick_xml::XmlVersion::Implicit1_0)
3878                                    .unwrap_or_default()
3879                                    .to_string(),
3880                            );
3881                        }
3882                    }
3883                }
3884            }
3885            Ok(Event::End(_)) => return Ok(sr),
3886            Ok(Event::Eof) => return Ok(sr),
3887            _ => {}
3888        }
3889        buf.clear();
3890    }
3891}
3892
3893/// 从 Empty 事件提取 StyleRef 属性(idx)。
3894fn parse_style_ref_empty(e: &quick_xml::events::BytesStart<'_>) -> StyleRef {
3895    let mut sr = StyleRef::default();
3896    for a in e.attributes().flatten() {
3897        if a.key.as_ref() == b"idx" {
3898            sr.idx = Some(
3899                a.normalized_value(quick_xml::XmlVersion::Implicit1_0)
3900                    .unwrap_or_default()
3901                    .to_string(),
3902            );
3903        }
3904    }
3905    sr
3906}
3907
3908fn collect_attr_value<R: std::io::BufRead>(rd: &mut Reader<R>, key: &str) -> crate::Result<String> {
3909    // 期望的语义:在当前层找到一个自闭合元素 `<... key="value"/>` 并返回 `value`。
3910    // 常见用法:`<a:latin typeface="Calibri"/>` / `<a:ea typeface="宋体"/>` / `<a:cs typeface="Times"/>`。
3911    //
3912    // 早期实现把"元素名 == key"作为匹配条件,导致**永远匹配不上**(元素名是 latin/ea/cs,
3913    // key 是 typeface)。修正后:忽略元素名,只看属性 key 是否命中。
3914    let mut buf = Vec::new();
3915    let mut val = String::new();
3916    let key_b = key.as_bytes();
3917    loop {
3918        match rd.read_event_into(&mut buf) {
3919            Ok(Event::Empty(e)) => {
3920                for a in e.attributes().flatten() {
3921                    if a.key.as_ref() == key_b {
3922                        val = a
3923                            .normalized_value(quick_xml::XmlVersion::Implicit1_0)
3924                            .unwrap_or_default()
3925                            .to_string();
3926                    }
3927                }
3928            }
3929            Ok(Event::End(_)) => return Ok(val),
3930            Ok(Event::Eof) => return Ok(val),
3931            _ => {}
3932        }
3933        buf.clear();
3934    }
3935}
3936
3937/// 取 quick-xml 元素名的**本地部分**(去前缀)。
3938///
3939/// 历史上存在的 `local_name_check` noop 包装已被删除(它没有做任何"去前缀"工作,
3940/// 只是把字节返回),保留仅是历史包袱。
3941fn local_name(name: &[u8]) -> &[u8] {
3942    match name.iter().position(|&c| c == b':') {
3943        Some(i) => &name[i + 1..],
3944        None => name,
3945    }
3946}
3947
3948/// 仅收集 Text 内容(用于 `<a:t>...</a:t>`)。
3949fn collect_inner_text_only<R: std::io::BufRead>(rd: &mut Reader<R>) -> crate::Result<String> {
3950    let mut out = String::new();
3951    let mut buf = Vec::new();
3952    loop {
3953        match rd.read_event_into(&mut buf) {
3954            Ok(Event::Text(t)) => {
3955                out.push_str(&t.decode().unwrap_or_default());
3956            }
3957            Ok(Event::CData(c)) => {
3958                out.push_str(&String::from_utf8_lossy(c.as_ref()));
3959            }
3960            Ok(Event::End(_)) => return Ok(out),
3961            Ok(Event::Eof) => return Ok(out),
3962            _ => {}
3963        }
3964        buf.clear();
3965    }
3966}
3967
3968impl RunProperties {
3969    /// 从 `a:rPr` 的自闭合属性中解析(不递归子元素)。
3970    pub fn from_bytes_start(e: &quick_xml::events::BytesStart<'_>) -> Self {
3971        let mut rp = RunProperties::default();
3972        for a in e.attributes().flatten() {
3973            let v = a
3974                .normalized_value(quick_xml::XmlVersion::Implicit1_0)
3975                .unwrap_or_default();
3976            match a.key.as_ref() {
3977                b"sz" => {
3978                    if let Ok(p) = v.parse::<i32>() {
3979                        rp.size = Some(Pt(p as f64 / 100.0));
3980                    }
3981                }
3982                b"b" => {
3983                    if v == "1" || v == "true" {
3984                        rp.bold = true;
3985                    }
3986                }
3987                b"i" => {
3988                    if v == "1" || v == "true" {
3989                        rp.italic = true;
3990                    }
3991                }
3992                b"u" => {
3993                    if let Ok(u) = v.parse::<Underline>() {
3994                        rp.underline = Some(u);
3995                    }
3996                }
3997                b"strike" => {
3998                    if v == "sngStrike" {
3999                        rp.strike = true;
4000                    }
4001                    if v == "dblStrike" {
4002                        rp.strike_dbl = true;
4003                    }
4004                }
4005                b"baseline" => {
4006                    if let Ok(v) = v.parse::<i32>() {
4007                        rp.baseline = Some(v);
4008                    }
4009                }
4010                b"kern" => {
4011                    if let Ok(v) = v.parse::<i32>() {
4012                        rp.kerning = Some(v);
4013                    }
4014                }
4015                b"spc" => {
4016                    if let Ok(v) = v.parse::<i32>() {
4017                        rp.spc = Some(v);
4018                    }
4019                }
4020                b"lang" => {
4021                    rp.lang = Some(v.to_string());
4022                }
4023                _ => {}
4024            }
4025        }
4026        rp
4027    }
4028}
4029
4030/// 解析 `p:pic` 元素(仅最小字段;其它信息丢失)。
4031///
4032/// 走 **正规 SAX**:在 `<p:blipFill>` 内逐事件读 `<a:blip r:embed="..."/>`
4033/// 等自闭合元素;不再使用"字符串 find r:embed"——后者在缩进/属性顺序变化时会漏取。
4034pub fn parse_pic(xml: &str) -> crate::Result<Pic> {
4035    let mut pic = Pic::default();
4036    let mut rd = Reader::from_str(xml);
4037    rd.config_mut().trim_text(true);
4038    let mut buf = Vec::new();
4039    let mut in_blipfill = false;
4040    let mut blipfill_depth: i32 = 0;
4041    loop {
4042        match rd.read_event_into(&mut buf) {
4043            Ok(Event::Start(e)) => {
4044                let name = e.name();
4045                let local = local_name(name.as_ref());
4046                if local == b"pic" {
4047                    // 根元素 <p:pic>:仅标记进入,不调用 collect_full_element
4048                    // (否则会吞掉所有子元素,导致 nvPicPr / blipFill 等无法解析)
4049                } else if local == b"nvPicPr" {
4050                    // 不使用 collect_full_element 吞掉 nvPicPr,
4051                    // 而是手动遍历子元素提取 cNvPr 的 id/name 属性
4052                    let mut nv_depth = 1i32;
4053                    loop {
4054                        match rd.read_event_into(&mut buf) {
4055                            Ok(Event::Start(_)) => nv_depth += 1,
4056                            Ok(Event::End(_)) => {
4057                                nv_depth -= 1;
4058                                if nv_depth == 0 {
4059                                    break;
4060                                }
4061                            }
4062                            Ok(Event::Empty(e2)) => {
4063                                let name2 = e2.name();
4064                                let local2 = local_name(name2.as_ref());
4065                                if local2 == b"cNvPr" {
4066                                    for a in e2.attributes().flatten() {
4067                                        match a.key.as_ref() {
4068                                            b"id" => {
4069                                                if let Ok(v) = a
4070                                                    .normalized_value(
4071                                                        quick_xml::XmlVersion::Implicit1_0,
4072                                                    )
4073                                                    .unwrap_or_default()
4074                                                    .parse::<u32>()
4075                                                {
4076                                                    pic.id = v;
4077                                                }
4078                                            }
4079                                            b"name" => {
4080                                                pic.name = a
4081                                                    .normalized_value(
4082                                                        quick_xml::XmlVersion::Implicit1_0,
4083                                                    )
4084                                                    .unwrap_or_default()
4085                                                    .to_string();
4086                                            }
4087                                            _ => {}
4088                                        }
4089                                    }
4090                                }
4091                            }
4092                            Ok(Event::Eof) => break,
4093                            Err(_) => break,
4094                            _ => {}
4095                        }
4096                        buf.clear();
4097                    }
4098                } else if local == b"blipFill" {
4099                    in_blipfill = true;
4100                    blipfill_depth = 1;
4101                    // 不立即吞掉,改为手写 SAX 解析 blipFill 的子元素
4102                    // 让 Event::Start/Empty 单独触发下方的 blip 处理
4103                } else if local == b"spPr" {
4104                    let inner = collect_full_element(&mut rd, e.into_owned())?;
4105                    pic.properties = parse_sppr(&inner)?;
4106                } else if in_blipfill && local == b"blip" {
4107                    // `<a:blip r:embed="rIdN"/>` 可能是 Start+End 也可能是 Empty。
4108                    // 直接从当前事件读取属性,**不再**走 collect_full_element。
4109                    for a in e.attributes().flatten() {
4110                        let key = a.key.as_ref();
4111                        // 兼容 `r:embed` 与 `embed` 两种命名空间形态
4112                        if key == b"r:embed" || key.ends_with(b":embed") {
4113                            pic.rid = a
4114                                .normalized_value(quick_xml::XmlVersion::Implicit1_0)
4115                                .unwrap_or_default()
4116                                .to_string();
4117                        }
4118                    }
4119                } else if in_blipfill && local == b"srcRect" {
4120                    // `<a:srcRect l="..." t="..." r="..." b="..."/>`
4121                    let (mut l, mut t, mut r, mut b) = (None, None, None, None);
4122                    for a in e.attributes().flatten() {
4123                        let v = a
4124                            .normalized_value(quick_xml::XmlVersion::Implicit1_0)
4125                            .unwrap_or_default();
4126                        match a.key.as_ref() {
4127                            b"l" => l = v.parse().ok(),
4128                            b"t" => t = v.parse().ok(),
4129                            b"r" => r = v.parse().ok(),
4130                            b"b" => b = v.parse().ok(),
4131                            _ => {}
4132                        }
4133                    }
4134                    if let (Some(ll), Some(tt), Some(rr), Some(bb)) = (l, t, r, b) {
4135                        pic.src_rect = Some((ll, tt, rr, bb));
4136                    }
4137                } else if in_blipfill && local == b"stretch" {
4138                    // `<a:stretch><a:fillRect/></a:stretch>`:拉伸填充模式
4139                    let _ = collect_full_element(&mut rd, e.into_owned())?;
4140                    pic.fill_mode = crate::oxml::sppr::BlipFillMode::Stretch;
4141                } else if in_blipfill && local == b"tile" {
4142                    // `<a:tile tx="..." ty="..." sx="..." sy="..." flip="..." algn="..."/>`
4143                    // 先解析属性,再吞掉子元素(tile 很少有子元素,但保险起见)
4144                    let mode = parse_tile_attrs(&e);
4145                    let _ = collect_full_element(&mut rd, e.into_owned())?;
4146                    pic.fill_mode = mode;
4147                } else {
4148                    let _ = collect_full_element(&mut rd, e.into_owned());
4149                }
4150            }
4151            Ok(Event::Empty(e)) => {
4152                let name = e.name();
4153                let local = local_name(name.as_ref());
4154                if in_blipfill && local == b"blip" {
4155                    // 自闭合形态的 `<a:blip r:embed="rIdN"/>`
4156                    for a in e.attributes().flatten() {
4157                        let key = a.key.as_ref();
4158                        if key == b"r:embed" || key.ends_with(b":embed") {
4159                            pic.rid = a
4160                                .normalized_value(quick_xml::XmlVersion::Implicit1_0)
4161                                .unwrap_or_default()
4162                                .to_string();
4163                        }
4164                    }
4165                } else if in_blipfill && local == b"srcRect" {
4166                    let (mut l, mut t, mut r, mut b) = (None, None, None, None);
4167                    for a in e.attributes().flatten() {
4168                        let v = a
4169                            .normalized_value(quick_xml::XmlVersion::Implicit1_0)
4170                            .unwrap_or_default();
4171                        match a.key.as_ref() {
4172                            b"l" => l = v.parse().ok(),
4173                            b"t" => t = v.parse().ok(),
4174                            b"r" => r = v.parse().ok(),
4175                            b"b" => b = v.parse().ok(),
4176                            _ => {}
4177                        }
4178                    }
4179                    if let (Some(ll), Some(tt), Some(rr), Some(bb)) = (l, t, r, b) {
4180                        pic.src_rect = Some((ll, tt, rr, bb));
4181                    }
4182                } else if in_blipfill && local == b"stretch" {
4183                    // 自闭合 `<a:stretch/>`(罕见,通常带 fillRect 子元素)
4184                    pic.fill_mode = crate::oxml::sppr::BlipFillMode::Stretch;
4185                } else if in_blipfill && local == b"tile" {
4186                    // 自闭合 `<a:tile .../>`
4187                    pic.fill_mode = parse_tile_attrs(&e);
4188                }
4189            }
4190            Ok(Event::End(e)) => {
4191                let name = e.name();
4192                let local = local_name(name.as_ref());
4193                // blipFill 可能是 `<a:blipFill><a:blip/>...</a:blipFill>` 这种
4194                // 形式:进入 blipFill 时 depth=1,遇到结束事件 depth-1。
4195                // depth 归 0 即离开 blipFill 范围。
4196                if in_blipfill && local == b"blipFill" {
4197                    blipfill_depth -= 1;
4198                    if blipfill_depth == 0 {
4199                        in_blipfill = false;
4200                    }
4201                }
4202            }
4203            Ok(Event::Eof) => break,
4204            Err(e) => return Err(crate::Error::Xml(format!("pic parse: {e}"))),
4205            _ => {}
4206        }
4207        buf.clear();
4208    }
4209    Ok(pic)
4210}
4211
4212/// 解析 `<p:cxnSp>` 元素(连接器)。
4213///
4214/// # 元素结构(OOXML)
4215///
4216/// ```text
4217/// <p:cxnSp>
4218///   <p:nvCxnSpPr>
4219///     <p:cNvPr id="..." name="..."/>
4220///     <p:cNvCxnSpPr/>
4221///     <p:nvPr/>
4222///     <p:stCxn id="..." idx="..."/>   ← 可选:起点挂接
4223///     <p:endCxn id="..." idx="..."/>  ← 可选:终点挂接
4224///   </p:nvCxnSpPr>
4225///   <p:spPr>...</p:spPr>
4226///   <p:style>...</p:style>           ← 可选
4227///   <p:extLst>...</p:extLst>         ← 可选
4228/// </p:cxnSp>
4229/// ```
4230///
4231/// # 行为
4232/// - 提取 `id` / `name` 属性;
4233/// - 解析 `stCxn` / `endCxn` 挂接信息(`Some((shape_id, idx))`);
4234/// - 解析 `spPr`(变换 / 几何 / 填充 / 边框);
4235/// - `style` / `extLst` 暂不解析(保留 `None`)。
4236pub fn parse_cxn_sp(xml: &str) -> crate::Result<Connector> {
4237    let mut cxn = Connector::default();
4238    let mut rd = Reader::from_str(xml);
4239    rd.config_mut().trim_text(true);
4240    let mut buf = Vec::new();
4241    loop {
4242        match rd.read_event_into(&mut buf) {
4243            Ok(Event::Start(e)) => {
4244                let name = e.name();
4245                let local = local_name(name.as_ref());
4246                if local == b"cxnSp" {
4247                    // 根元素 <p:cxnSp>:仅标记进入
4248                } else if local == b"nvCxnSpPr" {
4249                    // 手动遍历 nvCxnSpPr 子元素提取 cNvPr / stCxn / endCxn
4250                    let mut nv_depth = 1i32;
4251                    loop {
4252                        match rd.read_event_into(&mut buf) {
4253                            Ok(Event::Start(_)) => nv_depth += 1,
4254                            Ok(Event::End(_)) => {
4255                                nv_depth -= 1;
4256                                if nv_depth == 0 {
4257                                    break;
4258                                }
4259                            }
4260                            Ok(Event::Empty(e2)) => {
4261                                let name2 = e2.name();
4262                                let local2 = local_name(name2.as_ref());
4263                                if local2 == b"cNvPr" {
4264                                    for a in e2.attributes().flatten() {
4265                                        match a.key.as_ref() {
4266                                            b"id" => {
4267                                                if let Ok(v) = a
4268                                                    .normalized_value(
4269                                                        quick_xml::XmlVersion::Implicit1_0,
4270                                                    )
4271                                                    .unwrap_or_default()
4272                                                    .parse::<u32>()
4273                                                {
4274                                                    cxn.id = v;
4275                                                }
4276                                            }
4277                                            b"name" => {
4278                                                cxn.name = a
4279                                                    .normalized_value(
4280                                                        quick_xml::XmlVersion::Implicit1_0,
4281                                                    )
4282                                                    .unwrap_or_default()
4283                                                    .to_string();
4284                                            }
4285                                            _ => {}
4286                                        }
4287                                    }
4288                                } else if local2 == b"stCxn" {
4289                                    // 起点挂接:<p:stCxn id="..." idx="..."/>
4290                                    let mut sid: Option<u32> = None;
4291                                    let mut idx: Option<u32> = None;
4292                                    for a in e2.attributes().flatten() {
4293                                        match a.key.as_ref() {
4294                                            b"id" => {
4295                                                sid = a
4296                                                    .normalized_value(
4297                                                        quick_xml::XmlVersion::Implicit1_0,
4298                                                    )
4299                                                    .unwrap_or_default()
4300                                                    .parse()
4301                                                    .ok();
4302                                            }
4303                                            b"idx" => {
4304                                                idx = a
4305                                                    .normalized_value(
4306                                                        quick_xml::XmlVersion::Implicit1_0,
4307                                                    )
4308                                                    .unwrap_or_default()
4309                                                    .parse()
4310                                                    .ok();
4311                                            }
4312                                            _ => {}
4313                                        }
4314                                    }
4315                                    if let (Some(s), Some(i)) = (sid, idx) {
4316                                        cxn.st_cxn = Some((s, i));
4317                                    }
4318                                } else if local2 == b"endCxn" {
4319                                    // 终点挂接:<p:endCxn id="..." idx="..."/>
4320                                    let mut sid: Option<u32> = None;
4321                                    let mut idx: Option<u32> = None;
4322                                    for a in e2.attributes().flatten() {
4323                                        match a.key.as_ref() {
4324                                            b"id" => {
4325                                                sid = a
4326                                                    .normalized_value(
4327                                                        quick_xml::XmlVersion::Implicit1_0,
4328                                                    )
4329                                                    .unwrap_or_default()
4330                                                    .parse()
4331                                                    .ok();
4332                                            }
4333                                            b"idx" => {
4334                                                idx = a
4335                                                    .normalized_value(
4336                                                        quick_xml::XmlVersion::Implicit1_0,
4337                                                    )
4338                                                    .unwrap_or_default()
4339                                                    .parse()
4340                                                    .ok();
4341                                            }
4342                                            _ => {}
4343                                        }
4344                                    }
4345                                    if let (Some(s), Some(i)) = (sid, idx) {
4346                                        cxn.end_cxn = Some((s, i));
4347                                    }
4348                                }
4349                            }
4350                            Ok(Event::Eof) => break,
4351                            Err(_) => break,
4352                            _ => {}
4353                        }
4354                        buf.clear();
4355                    }
4356                } else if local == b"spPr" {
4357                    let inner = collect_full_element(&mut rd, e.into_owned())?;
4358                    cxn.properties = parse_sppr(&inner)?;
4359                } else if local == b"style" {
4360                    // <p:style> 主题样式引用(TODO-006 一致性补全)
4361                    let inner = collect_full_element(&mut rd, e.into_owned())?;
4362                    cxn.style = Some(parse_shape_style(&inner)?);
4363                } else {
4364                    // extLst 等暂不解析,吞掉
4365                    let _ = collect_full_element(&mut rd, e.into_owned());
4366                }
4367            }
4368            Ok(Event::End(e)) => {
4369                if local_name(e.name().as_ref()) == b"cxnSp" {
4370                    break;
4371                }
4372            }
4373            Ok(Event::Eof) => break,
4374            Err(e) => return Err(crate::Error::Xml(format!("cxnSp parse: {e}"))),
4375            _ => {}
4376        }
4377        buf.clear();
4378    }
4379    Ok(cxn)
4380}
4381
4382/// 解析 `<p:grpSp>` 元素(组合形状,递归)。
4383///
4384/// # 元素结构(OOXML)
4385///
4386/// ```text
4387/// <p:grpSp>
4388///   <p:nvGrpSpPr>
4389///     <p:cNvPr id="..." name="..."/>
4390///     <p:cNvGrpSpPr/>
4391///     <p:nvPr/>
4392///   </p:nvGrpSpPr>
4393///   <p:grpSpPr>
4394///     <a:xfrm>
4395///       <a:off x="..." y="..."/>
4396///       <a:ext cx="..." cy="..."/>
4397///       <a:chOff x="..." y="..."/>
4398///       <a:chExt cx="..." cy="..."/>
4399///     </a:xfrm>
4400///   </p:grpSpPr>
4401///   子形状 (sp/pic/cxnSp/grpSp/graphicFrame)   ← 递归
4402///   <p:extLst>...</p:extLst>                  ← 可选
4403/// </p:grpSp>
4404/// ```
4405///
4406/// # 行为
4407/// - 提取 `id` / `name`;
4408/// - 解析 `grpSpPr` 内的 `a:xfrm`(off/ext/chOff/chExt);
4409/// - 递归解析子形状(sp/pic/cxnSp/grpSp/graphicFrame)。
4410pub fn parse_grp_sp(xml: &str) -> crate::Result<Group> {
4411    let mut grp = Group::default();
4412    let mut rd = Reader::from_str(xml);
4413    rd.config_mut().trim_text(true);
4414    let mut buf = Vec::new();
4415    // 收集子形状的原始 XML,稍后递归解析
4416    let mut child_sp_bufs: Vec<String> = Vec::new();
4417    let mut child_pic_bufs: Vec<String> = Vec::new();
4418    let mut child_cxn_bufs: Vec<String> = Vec::new();
4419    let mut child_grp_bufs: Vec<String> = Vec::new();
4420    let mut child_gfx_bufs: Vec<String> = Vec::new();
4421    // 标记是否已遇到根 <p:grpSp>(用于区分根元素与嵌套 grpSp)
4422    let mut entered_root = false;
4423
4424    loop {
4425        match rd.read_event_into(&mut buf) {
4426            Ok(Event::Start(e)) => {
4427                let name = e.name();
4428                let local = local_name(name.as_ref());
4429                if local == b"grpSp" {
4430                    if !entered_root {
4431                        // 根元素 <p:grpSp>:仅标记进入
4432                        entered_root = true;
4433                    } else {
4434                        // 嵌套组合:递归
4435                        let inner = collect_full_element(&mut rd, e.into_owned())?;
4436                        child_grp_bufs.push(inner);
4437                    }
4438                } else if local == b"nvGrpSpPr" {
4439                    // 手动遍历 nvGrpSpPr 子元素提取 cNvPr
4440                    let mut nv_depth = 1i32;
4441                    loop {
4442                        match rd.read_event_into(&mut buf) {
4443                            Ok(Event::Start(_)) => nv_depth += 1,
4444                            Ok(Event::End(_)) => {
4445                                nv_depth -= 1;
4446                                if nv_depth == 0 {
4447                                    break;
4448                                }
4449                            }
4450                            Ok(Event::Empty(e2)) => {
4451                                let name2 = e2.name();
4452                                let local2 = local_name(name2.as_ref());
4453                                if local2 == b"cNvPr" {
4454                                    for a in e2.attributes().flatten() {
4455                                        match a.key.as_ref() {
4456                                            b"id" => {
4457                                                if let Ok(v) = a
4458                                                    .normalized_value(
4459                                                        quick_xml::XmlVersion::Implicit1_0,
4460                                                    )
4461                                                    .unwrap_or_default()
4462                                                    .parse::<u32>()
4463                                                {
4464                                                    grp.id = v;
4465                                                }
4466                                            }
4467                                            b"name" => {
4468                                                grp.name = a
4469                                                    .normalized_value(
4470                                                        quick_xml::XmlVersion::Implicit1_0,
4471                                                    )
4472                                                    .unwrap_or_default()
4473                                                    .to_string();
4474                                            }
4475                                            _ => {}
4476                                        }
4477                                    }
4478                                }
4479                            }
4480                            Ok(Event::Eof) => break,
4481                            Err(_) => break,
4482                            _ => {}
4483                        }
4484                        buf.clear();
4485                    }
4486                } else if local == b"grpSpPr" {
4487                    // 解析 grpSpPr 内的 a:xfrm(off/ext/chOff/chExt)
4488                    parse_grp_sppr_into(&mut rd, &mut grp, e.into_owned())?;
4489                } else if local == b"sp" {
4490                    let inner = collect_full_element(&mut rd, e.into_owned())?;
4491                    child_sp_bufs.push(inner);
4492                } else if local == b"pic" {
4493                    let inner = collect_full_element(&mut rd, e.into_owned())?;
4494                    child_pic_bufs.push(inner);
4495                } else if local == b"cxnSp" {
4496                    let inner = collect_full_element(&mut rd, e.into_owned())?;
4497                    child_cxn_bufs.push(inner);
4498                } else if local == b"graphicFrame" {
4499                    let inner = collect_full_element(&mut rd, e.into_owned())?;
4500                    child_gfx_bufs.push(inner);
4501                } else if local == b"style" {
4502                    // <p:style> 主题样式引用(TODO-006 一致性补全)
4503                    let inner = collect_full_element(&mut rd, e.into_owned())?;
4504                    grp.style = Some(parse_shape_style(&inner)?);
4505                } else {
4506                    // extLst 等暂不解析,吞掉
4507                    let _ = collect_full_element(&mut rd, e.into_owned());
4508                }
4509            }
4510            Ok(Event::End(e)) => {
4511                if local_name(e.name().as_ref()) == b"grpSp" {
4512                    break;
4513                }
4514            }
4515            Ok(Event::Eof) => break,
4516            Err(e) => return Err(crate::Error::Xml(format!("grpSp parse: {e}"))),
4517            _ => {}
4518        }
4519        buf.clear();
4520    }
4521    // 递归解析子形状
4522    for s in child_sp_bufs {
4523        if let Ok(sp) = parse_sp(&s) {
4524            grp.children.push(GroupChild::Sp(sp));
4525        }
4526    }
4527    for p in child_pic_bufs {
4528        if let Ok(pic) = parse_pic(&p) {
4529            grp.children.push(GroupChild::Pic(pic));
4530        }
4531    }
4532    for c in child_cxn_bufs {
4533        if let Ok(cxn) = parse_cxn_sp(&c) {
4534            grp.children.push(GroupChild::CxnSp(cxn));
4535        }
4536    }
4537    for g in child_grp_bufs {
4538        if let Ok(sub) = parse_grp_sp(&g) {
4539            grp.children.push(GroupChild::Group(Box::new(sub)));
4540        }
4541    }
4542    for f in child_gfx_bufs {
4543        if let Ok(frame) = parse_graphic_frame(&f) {
4544            grp.children.push(GroupChild::GraphicFrame(frame));
4545        }
4546    }
4547    Ok(grp)
4548}
4549
4550/// 解析 `<p:grpSpPr>` 内的 `<a:xfrm>`,提取 off/ext/chOff/chExt。
4551fn parse_grp_sppr_into<R: std::io::BufRead>(
4552    rd: &mut Reader<R>,
4553    grp: &mut Group,
4554    _start: quick_xml::events::BytesStart<'static>,
4555) -> crate::Result<()> {
4556    let mut buf = Vec::new();
4557    loop {
4558        match rd.read_event_into(&mut buf) {
4559            Ok(Event::Empty(e)) => {
4560                let name = e.name();
4561                let local = local_name(name.as_ref());
4562                if local == b"off" {
4563                    let (mut x, mut y) = (None, None);
4564                    for a in e.attributes().flatten() {
4565                        match a.key.as_ref() {
4566                            b"x" => {
4567                                x = a
4568                                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
4569                                    .unwrap_or_default()
4570                                    .parse()
4571                                    .ok()
4572                            }
4573                            b"y" => {
4574                                y = a
4575                                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
4576                                    .unwrap_or_default()
4577                                    .parse()
4578                                    .ok()
4579                            }
4580                            _ => {}
4581                        }
4582                    }
4583                    if let (Some(x), Some(y)) = (x, y) {
4584                        grp.off = (Emu(x), Emu(y));
4585                    }
4586                } else if local == b"ext" {
4587                    let (mut cx, mut cy) = (None, None);
4588                    for a in e.attributes().flatten() {
4589                        match a.key.as_ref() {
4590                            b"cx" => {
4591                                cx = a
4592                                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
4593                                    .unwrap_or_default()
4594                                    .parse()
4595                                    .ok()
4596                            }
4597                            b"cy" => {
4598                                cy = a
4599                                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
4600                                    .unwrap_or_default()
4601                                    .parse()
4602                                    .ok()
4603                            }
4604                            _ => {}
4605                        }
4606                    }
4607                    if let (Some(cx), Some(cy)) = (cx, cy) {
4608                        grp.ext = (Emu(cx), Emu(cy));
4609                    }
4610                }
4611                // chOff / chExt 暂不解析(组合内部坐标系,0.1.0 写出时固定为 0,0)
4612            }
4613            Ok(Event::End(e)) => {
4614                if local_name(e.name().as_ref()) == b"grpSpPr" {
4615                    return Ok(());
4616                }
4617            }
4618            Ok(Event::Eof) => return Ok(()),
4619            _ => {}
4620        }
4621        buf.clear();
4622    }
4623}
4624
4625/// 解析 `<p:graphicFrame>` 元素(图形框:表格/图表)。
4626///
4627/// # 元素结构(OOXML)
4628///
4629/// ```text
4630/// <p:graphicFrame>
4631///   <p:nvGraphicFramePr>
4632///     <p:cNvPr id="..." name="..."/>
4633///     <p:cNvGraphicFramePr/>
4634///     <p:nvPr/>
4635///   </p:nvGraphicFramePr>
4636///   <p:xfrm>
4637///     <a:off x="..." y="..."/>
4638///     <a:ext cx="..." cy="..."/>
4639///   </p:xfrm>
4640///   <a:graphic>
4641///     <a:graphicData uri="...">
4642///       <a:tbl>...</a:tbl>     ← 或 chart / smartArt
4643///     </a:graphicData>
4644///   </a:graphic>
4645///   <p:extLst>...</p:extLst>   ← 可选
4646/// </p:graphicFrame>
4647/// ```
4648///
4649/// # 行为
4650/// - 提取 `id` / `name`;
4651/// - 解析 `<p:xfrm>` 内的 off/ext(写入 `properties.xfrm`);
4652/// - 解析 `<a:graphicData uri="...">` 内的 `<a:tbl>`(仅表格,其它类型暂留空)。
4653pub fn parse_graphic_frame(xml: &str) -> crate::Result<GraphicFrame> {
4654    let mut frame = GraphicFrame::default();
4655    let mut rd = Reader::from_str(xml);
4656    rd.config_mut().trim_text(true);
4657    let mut buf = Vec::new();
4658    loop {
4659        match rd.read_event_into(&mut buf) {
4660            Ok(Event::Start(e)) => {
4661                let name = e.name();
4662                let local = local_name(name.as_ref());
4663                if local == b"graphicFrame" {
4664                    // 根元素 <p:graphicFrame>:仅标记进入
4665                } else if local == b"nvGraphicFramePr" {
4666                    // 手动遍历 nvGraphicFramePr 子元素提取 cNvPr
4667                    let mut nv_depth = 1i32;
4668                    loop {
4669                        match rd.read_event_into(&mut buf) {
4670                            Ok(Event::Start(_)) => nv_depth += 1,
4671                            Ok(Event::End(_)) => {
4672                                nv_depth -= 1;
4673                                if nv_depth == 0 {
4674                                    break;
4675                                }
4676                            }
4677                            Ok(Event::Empty(e2)) => {
4678                                let name2 = e2.name();
4679                                let local2 = local_name(name2.as_ref());
4680                                if local2 == b"cNvPr" {
4681                                    for a in e2.attributes().flatten() {
4682                                        match a.key.as_ref() {
4683                                            b"id" => {
4684                                                if let Ok(v) = a
4685                                                    .normalized_value(
4686                                                        quick_xml::XmlVersion::Implicit1_0,
4687                                                    )
4688                                                    .unwrap_or_default()
4689                                                    .parse::<u32>()
4690                                                {
4691                                                    frame.id = v;
4692                                                }
4693                                            }
4694                                            b"name" => {
4695                                                frame.name = a
4696                                                    .normalized_value(
4697                                                        quick_xml::XmlVersion::Implicit1_0,
4698                                                    )
4699                                                    .unwrap_or_default()
4700                                                    .to_string();
4701                                            }
4702                                            _ => {}
4703                                        }
4704                                    }
4705                                }
4706                            }
4707                            Ok(Event::Eof) => break,
4708                            Err(_) => break,
4709                            _ => {}
4710                        }
4711                        buf.clear();
4712                    }
4713                } else if local == b"xfrm" {
4714                    // 先提取 xfrm 自身的属性(rot/flipH/flipV)
4715                    for a in e.attributes().flatten() {
4716                        match a.key.as_ref() {
4717                            b"rot" => {
4718                                if let Ok(v) = a
4719                                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
4720                                    .unwrap_or_default()
4721                                    .parse::<i32>()
4722                                {
4723                                    frame.properties.xfrm.rot = Some(v);
4724                                }
4725                            }
4726                            b"flipH" => {
4727                                let v = a
4728                                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
4729                                    .unwrap_or_default();
4730                                if v == "1" {
4731                                    frame.properties.xfrm.flip_h = true;
4732                                }
4733                            }
4734                            b"flipV" => {
4735                                let v = a
4736                                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
4737                                    .unwrap_or_default();
4738                                if v == "1" {
4739                                    frame.properties.xfrm.flip_v = true;
4740                                }
4741                            }
4742                            _ => {}
4743                        }
4744                    }
4745                    // 再解析子元素(off/ext)
4746                    parse_xfrm_into(&mut rd, &mut frame.properties);
4747                } else if local == b"graphic" {
4748                    // 解析 <a:graphic> 内的 <a:graphicData uri="...">
4749                    parse_graphic_into(&mut rd, &mut frame)?;
4750                } else {
4751                    // extLst 等暂不解析,吞掉
4752                    let _ = collect_full_element(&mut rd, e.into_owned());
4753                }
4754            }
4755            Ok(Event::End(e)) => {
4756                if local_name(e.name().as_ref()) == b"graphicFrame" {
4757                    break;
4758                }
4759            }
4760            Ok(Event::Eof) => break,
4761            Err(e) => return Err(crate::Error::Xml(format!("graphicFrame parse: {e}"))),
4762            _ => {}
4763        }
4764        buf.clear();
4765    }
4766    Ok(frame)
4767}
4768
4769/// 解析 `<a:graphic>` 内的 `<a:graphicData uri="...">`,根据 uri 分发到表格解析器。
4770fn parse_graphic_into<R: std::io::BufRead>(
4771    rd: &mut Reader<R>,
4772    frame: &mut GraphicFrame,
4773) -> crate::Result<()> {
4774    let mut buf = Vec::new();
4775    loop {
4776        match rd.read_event_into(&mut buf) {
4777            Ok(Event::Start(e)) => {
4778                let name = e.name();
4779                let local = local_name(name.as_ref());
4780                if local == b"graphicData" {
4781                    // 读取 uri 属性判断图形类型
4782                    let mut uri = String::new();
4783                    for a in e.attributes().flatten() {
4784                        if a.key.as_ref() == b"uri" {
4785                            uri = a
4786                                .normalized_value(quick_xml::XmlVersion::Implicit1_0)
4787                                .unwrap_or_default()
4788                                .to_string();
4789                        }
4790                    }
4791                    // 根据 uri 分发:表格 / 图表 / smartArt
4792                    if uri == "http://schemas.openxmlformats.org/drawingml/2006/table" {
4793                        // 收集 <a:tbl> 子元素
4794                        loop {
4795                            match rd.read_event_into(&mut buf) {
4796                                Ok(Event::Start(e2)) => {
4797                                    if local_name(e2.name().as_ref()) == b"tbl" {
4798                                        let tbl_xml = collect_full_element(rd, e2.into_owned())?;
4799                                        if let Ok(tbl) = parse_table(&tbl_xml) {
4800                                            frame.graphic = crate::oxml::shape::Graphic::Table(tbl);
4801                                        }
4802                                    } else {
4803                                        let _ = collect_full_element(rd, e2.into_owned());
4804                                    }
4805                                }
4806                                Ok(Event::End(e2)) => {
4807                                    if local_name(e2.name().as_ref()) == b"graphicData" {
4808                                        break;
4809                                    }
4810                                }
4811                                Ok(Event::Eof) => break,
4812                                _ => {}
4813                            }
4814                            buf.clear();
4815                        }
4816                    } else if uri == "http://schemas.openxmlformats.org/drawingml/2006/diagram" {
4817                        // SmartArt 最小保留(TODO-037):保留完整 <a:graphicData> 元素 XML。
4818                        // collect_full_element 会从当前 Start 事件(graphicData)开始,
4819                        // 收集到对应 End 事件,返回完整 XML(含外壳)。
4820                        let raw_xml = collect_full_element(rd, e.into_owned())?;
4821                        // 从 raw_xml 中提取 dgm:relIds 的 4 个关系 id(r:dm / r:lo / r:qs / r:cs)。
4822                        // 这些 id 仅供调用方查询,序列化时不单独使用(直接走 raw_xml)。
4823                        let (dm_rid, lo_rid, qs_rid, cs_rid) = parse_smartart_rel_ids(&raw_xml);
4824                        frame.graphic = crate::oxml::shape::Graphic::SmartArt(
4825                            crate::oxml::shape::SmartArtRef {
4826                                raw_xml,
4827                                dm_rid,
4828                                lo_rid,
4829                                qs_rid,
4830                                cs_rid,
4831                            },
4832                        );
4833                    } else if uri == "http://schemas.openxmlformats.org/drawingml/2006/chart" {
4834                        // 图表读路径(TODO-004):slide 的 graphicFrame 仅含 `<c:chart r:id="rIdX"/>`
4835                        // 引用,真正的图表内容在独立的 `/ppt/charts/chartN.xml` part 中。
4836                        // 这里仅提取 r:id 构造 Chart 占位模型,由 `Presentation::from_opc`
4837                        // 读取 chartN.xml 后调用 `Chart::parse_from_xml` 填充真实类型与数据。
4838                        let raw_xml = collect_full_element(rd, e.into_owned())?;
4839                        let rid = parse_chart_rid(&raw_xml);
4840                        frame.graphic =
4841                            crate::oxml::shape::Graphic::Chart(crate::oxml::chart::Chart::new(
4842                                crate::oxml::chart::ChartType::Column,
4843                                crate::oxml::chart::ChartData::default(),
4844                            ));
4845                        // 把 rid 写入 Chart.rid,from_opc 阶段根据 rid 查 chart_rel_map 读 chartN.xml。
4846                        if let crate::oxml::shape::Graphic::Chart(c) = &mut frame.graphic {
4847                            c.rid = rid;
4848                        }
4849                    } else {
4850                        // 其它类型(chart 等)暂不解析,吞掉
4851                        let _ = collect_full_element(rd, e.into_owned());
4852                    }
4853                } else {
4854                    let _ = collect_full_element(rd, e.into_owned());
4855                }
4856            }
4857            Ok(Event::End(e)) => {
4858                if local_name(e.name().as_ref()) == b"graphic" {
4859                    return Ok(());
4860                }
4861            }
4862            Ok(Event::Eof) => return Ok(()),
4863            _ => {}
4864        }
4865        buf.clear();
4866    }
4867}
4868
4869/// 从 SmartArt 的 `<a:graphicData>` 完整 XML 中提取 `<dgm:relIds>` 的 4 个关系 id(TODO-037)。
4870///
4871/// # 元素结构(OOXML)
4872///
4873/// ```text
4874/// <a:graphicData uri=".../diagram">
4875///   <dgm:relIds r:dm="rId1" r:lo="rId2" r:qs="rId3" r:cs="rId4"/>
4876/// </a:graphicData>
4877/// ```
4878///
4879/// # 行为
4880///
4881/// - 用简单的字符串查找提取 `r:dm` / `r:lo` / `r:qs` / `r:cs` 属性值;
4882/// - 不做完整 XML 解析(避免 quick-xml 命名空间处理的复杂性,且 raw_xml 已 byte-exact 保留);
4883/// - 找不到时对应字段返回 `None`。
4884///
4885/// # 返回值
4886///
4887/// `(dm_rid, lo_rid, qs_rid, cs_rid)` 四元组,每个字段为 `Option<String>`。
4888fn parse_smartart_rel_ids(
4889    raw_xml: &str,
4890) -> (
4891    Option<String>,
4892    Option<String>,
4893    Option<String>,
4894    Option<String>,
4895) {
4896    /// 在 XML 字符串中查找 `attr_name="value"` 模式的 value。
4897    ///
4898    /// 仅做简单字符串查找,不处理转义(OOXML 关系 id 不含特殊字符,安全)。
4899    fn find_attr(xml: &str, attr_name: &str) -> Option<String> {
4900        // 查找 `attr_name="` 模式(注意 r:dm 等带冒号的属性名)
4901        let pattern = format!("{}=\"", attr_name);
4902        let pos = xml.find(&pattern)?;
4903        let value_start = pos + pattern.len();
4904        let value_end = xml[value_start..].find('"')?;
4905        Some(xml[value_start..value_start + value_end].to_string())
4906    }
4907
4908    let dm = find_attr(raw_xml, "r:dm");
4909    let lo = find_attr(raw_xml, "r:lo");
4910    let qs = find_attr(raw_xml, "r:qs");
4911    let cs = find_attr(raw_xml, "r:cs");
4912    (dm, lo, qs, cs)
4913}
4914
4915/// 从 chart 的 `<a:graphicData>` 完整 XML 中提取 `<c:chart r:id="..."/>` 的关系 id(TODO-004 读路径)。
4916///
4917/// # 元素结构(OOXML)
4918///
4919/// ```text
4920/// <a:graphicData uri=".../chart">
4921///   <c:chart xmlns:c="..." xmlns:r="..." r:id="rId1"/>
4922/// </a:graphicData>
4923/// ```
4924///
4925/// # 行为
4926///
4927/// - 用简单的字符串查找提取 `r:id` 属性值;
4928/// - 兼容 `r:id="..."` 与 `r:id = "..."`(带空格,罕见)两种写法;
4929/// - 找不到时返回空字符串(`from_opc` 阶段会跳过该 chart 的 part 读取)。
4930///
4931/// # 返回值
4932///
4933/// 关系 id 字符串(如 `"rId1"`)。找不到时返回空字符串。
4934fn parse_chart_rid(raw_xml: &str) -> String {
4935    // 优先匹配 `r:id="value"` 模式(OOXML 关系 id 不含特殊字符,安全)
4936    let pattern = "r:id=\"";
4937    if let Some(pos) = raw_xml.find(pattern) {
4938        let value_start = pos + pattern.len();
4939        if let Some(end) = raw_xml[value_start..].find('"') {
4940            return raw_xml[value_start..value_start + end].to_string();
4941        }
4942    }
4943    String::new()
4944}
4945
4946/// 解析 `<a:tbl>` 元素为 [`crate::oxml::table::Table`]。
4947///
4948/// # 元素结构
4949///
4950/// ```text
4951/// <a:tbl>
4952///   <a:tblPr>...</a:tblPr>
4953///   <a:tblGrid>
4954///     <a:gridCol w="..."/>
4955///     ...
4956///   </a:tblGrid>
4957///   <a:tr h="...">
4958///     <a:tc>
4959///       <a:txBody>...</a:txBody>
4960///       <a:tcPr anchor="..." marT="..." .../>
4961///     </a:tc>
4962///     ...
4963///   </a:tr>
4964///   ...
4965/// </a:tbl>
4966/// ```
4967fn parse_table(xml: &str) -> crate::Result<crate::oxml::table::Table> {
4968    use crate::oxml::table::{Col, Table};
4969
4970    let mut tbl = Table::default();
4971    let mut rd = Reader::from_str(xml);
4972    rd.config_mut().trim_text(true);
4973    let mut buf = Vec::new();
4974    // 收集行和列的原始 XML
4975    let mut tr_bufs: Vec<String> = Vec::new();
4976    let mut grid_col_widths: Vec<Emu> = Vec::new();
4977
4978    loop {
4979        match rd.read_event_into(&mut buf) {
4980            Ok(Event::Start(e)) => {
4981                let name = e.name();
4982                let local = local_name(name.as_ref());
4983                if local == b"tbl" {
4984                    // 根元素
4985                } else if local == b"tblPr" {
4986                    // 解析 tblPr 内的 tblLook 属性和 tableStyleId
4987                    let inner = collect_full_element(&mut rd, e.into_owned())?;
4988                    parse_tbl_pr_into(&inner, &mut tbl);
4989                } else if local == b"tblGrid" {
4990                    // 解析 tblGrid 内的 gridCol
4991                    loop {
4992                        match rd.read_event_into(&mut buf) {
4993                            Ok(Event::Empty(e2)) => {
4994                                if local_name(e2.name().as_ref()) == b"gridCol" {
4995                                    let mut w: Option<i64> = None;
4996                                    for a in e2.attributes().flatten() {
4997                                        if a.key.as_ref() == b"w" {
4998                                            w = a
4999                                                .normalized_value(
5000                                                    quick_xml::XmlVersion::Implicit1_0,
5001                                                )
5002                                                .unwrap_or_default()
5003                                                .parse()
5004                                                .ok();
5005                                        }
5006                                    }
5007                                    grid_col_widths.push(Emu(w.unwrap_or(0)));
5008                                }
5009                            }
5010                            Ok(Event::End(e2)) => {
5011                                if local_name(e2.name().as_ref()) == b"tblGrid" {
5012                                    break;
5013                                }
5014                            }
5015                            Ok(Event::Eof) => break,
5016                            _ => {}
5017                        }
5018                        buf.clear();
5019                    }
5020                } else if local == b"tr" {
5021                    let inner = collect_full_element(&mut rd, e.into_owned())?;
5022                    tr_bufs.push(inner);
5023                } else {
5024                    let _ = collect_full_element(&mut rd, e.into_owned());
5025                }
5026            }
5027            Ok(Event::Eof) => break,
5028            Err(e) => return Err(crate::Error::Xml(format!("tbl parse: {e}"))),
5029            _ => {}
5030        }
5031        buf.clear();
5032    }
5033
5034    // 填充列
5035    tbl.cols = grid_col_widths.iter().map(|w| Col { width: *w }).collect();
5036
5037    // 解析行
5038    for tr_xml in tr_bufs {
5039        if let Ok(row) = parse_table_row(&tr_xml) {
5040            tbl.rows.push(row);
5041        }
5042    }
5043
5044    Ok(tbl)
5045}
5046
5047/// 解析 `<a:tblPr>` 内的 `<a:tblLook>` 属性和 `<a:tableStyleId>` 元素。
5048///
5049/// 将解析结果写入 `tbl.tbl_look` 和 `tbl.table_style`。
5050fn parse_tbl_pr_into(xml: &str, tbl: &mut crate::oxml::table::Table) {
5051    use crate::oxml::table::TableStyle;
5052
5053    let look = &mut tbl.tbl_look;
5054    let mut rd = Reader::from_str(xml);
5055    let mut buf = Vec::new();
5056    loop {
5057        match rd.read_event_into(&mut buf) {
5058            Ok(Event::Empty(e)) => {
5059                if local_name(e.name().as_ref()) == b"tblLook" {
5060                    for a in e.attributes().flatten() {
5061                        let v = a
5062                            .normalized_value(quick_xml::XmlVersion::Implicit1_0)
5063                            .unwrap_or_default();
5064                        match a.key.as_ref() {
5065                            b"val" => look.val = v.to_string(),
5066                            b"firstRow" => look.first_row = v == "1",
5067                            b"lastRow" => look.last_row = v == "1",
5068                            b"firstColumn" => look.first_column = v == "1",
5069                            b"lastColumn" => look.last_column = v == "1",
5070                            b"noHBand" => look.no_h_band = v == "1",
5071                            b"noVBand" => look.no_v_band = v == "1",
5072                            _ => {}
5073                        }
5074                    }
5075                }
5076                // tableStyleId 自闭合形式(罕见但合法:<a:tableStyleId/>)
5077                if local_name(e.name().as_ref()) == b"tableStyleId" {
5078                    // 自闭合无文本内容,跳过
5079                }
5080            }
5081            Ok(Event::Start(e)) => {
5082                if local_name(e.name().as_ref()) == b"tableStyleId" {
5083                    // 读取文本内容(GUID)
5084                    let mut guid = String::new();
5085                    loop {
5086                        match rd.read_event_into(&mut buf) {
5087                            Ok(Event::Text(t)) => {
5088                                guid.push_str(&t.decode().unwrap_or_default());
5089                            }
5090                            Ok(Event::End(e2)) => {
5091                                if local_name(e2.name().as_ref()) == b"tableStyleId" {
5092                                    break;
5093                                }
5094                            }
5095                            Ok(Event::Eof) => break,
5096                            _ => {}
5097                        }
5098                        buf.clear();
5099                    }
5100                    let trimmed = guid.trim();
5101                    if !trimmed.is_empty() {
5102                        tbl.table_style = Some(TableStyle::new(trimmed));
5103                    }
5104                }
5105            }
5106            Ok(Event::Eof) => break,
5107            _ => {}
5108        }
5109        buf.clear();
5110    }
5111}
5112
5113/// 解析 `<a:tr>` 元素为 [`Row`]。
5114fn parse_table_row(xml: &str) -> crate::Result<crate::oxml::table::Row> {
5115    use crate::oxml::table::Row;
5116
5117    let mut row = Row::default();
5118    let mut rd = Reader::from_str(xml);
5119    rd.config_mut().trim_text(true);
5120    let mut buf = Vec::new();
5121    // 行高
5122    // 从根元素 <a:tr h="..."> 读取属性
5123    let mut tc_bufs: Vec<String> = Vec::new();
5124
5125    loop {
5126        match rd.read_event_into(&mut buf) {
5127            Ok(Event::Start(e)) => {
5128                let name = e.name();
5129                let local = local_name(name.as_ref());
5130                if local == b"tr" {
5131                    // 读取行高属性
5132                    for a in e.attributes().flatten() {
5133                        if a.key.as_ref() == b"h" {
5134                            if let Ok(v) = a
5135                                .normalized_value(quick_xml::XmlVersion::Implicit1_0)
5136                                .unwrap_or_default()
5137                                .parse::<i64>()
5138                            {
5139                                row.height = Emu(v);
5140                            }
5141                        }
5142                    }
5143                } else if local == b"tc" {
5144                    let inner = collect_full_element(&mut rd, e.into_owned())?;
5145                    tc_bufs.push(inner);
5146                } else {
5147                    let _ = collect_full_element(&mut rd, e.into_owned());
5148                }
5149            }
5150            Ok(Event::Empty(_e)) => {
5151                // 自闭合 tr(罕见)
5152            }
5153            Ok(Event::End(e)) => {
5154                if local_name(e.name().as_ref()) == b"tr" {
5155                    break;
5156                }
5157            }
5158            Ok(Event::Eof) => break,
5159            Err(e) => return Err(crate::Error::Xml(format!("tr parse: {e}"))),
5160            _ => {}
5161        }
5162        buf.clear();
5163    }
5164
5165    // 解析单元格
5166    for tc_xml in tc_bufs {
5167        if let Ok(cell) = parse_table_cell(&tc_xml) {
5168            row.cells.push(cell);
5169        }
5170    }
5171
5172    Ok(row)
5173}
5174
5175/// 解析 `<a:tc>` 元素为 [`Cell`]。
5176fn parse_table_cell(xml: &str) -> crate::Result<crate::oxml::table::Cell> {
5177    let mut cell = crate::oxml::table::Cell::default();
5178    let mut rd = Reader::from_str(xml);
5179    rd.config_mut().trim_text(true);
5180    let mut buf = Vec::new();
5181
5182    loop {
5183        match rd.read_event_into(&mut buf) {
5184            Ok(Event::Start(e)) => {
5185                let name = e.name();
5186                let local = local_name(name.as_ref());
5187                if local == b"tc" {
5188                    // 读取 gridSpan / rowSpan / hMerge / vMerge 属性
5189                    for a in e.attributes().flatten() {
5190                        let v = a
5191                            .normalized_value(quick_xml::XmlVersion::Implicit1_0)
5192                            .unwrap_or_default();
5193                        match a.key.as_ref() {
5194                            b"gridSpan" => {
5195                                if let Ok(g) = v.parse::<u32>() {
5196                                    cell.grid_span = g;
5197                                }
5198                            }
5199                            b"rowSpan" => {
5200                                if let Ok(r) = v.parse::<u32>() {
5201                                    cell.row_span = r;
5202                                }
5203                            }
5204                            b"hMerge" if v == "1" => {
5205                                cell.h_merge = true;
5206                            }
5207                            b"vMerge" if v == "1" => {
5208                                cell.v_merge = true;
5209                            }
5210                            _ => {}
5211                        }
5212                    }
5213                } else if local == b"txBody" {
5214                    let inner = collect_full_element(&mut rd, e.into_owned())?;
5215                    cell.text = parse_txbody(&inner)?;
5216                } else if local == b"tcPr" {
5217                    // 解析 tcPr 属性(marT/marL/marB/marR/anchor)和子元素(lnL/lnR/lnT/lnB/solidFill)
5218                    let inner = collect_full_element(&mut rd, e.into_owned())?;
5219                    parse_tc_pr_into(&inner, &mut cell);
5220                } else {
5221                    let _ = collect_full_element(&mut rd, e.into_owned());
5222                }
5223            }
5224            Ok(Event::End(e)) => {
5225                if local_name(e.name().as_ref()) == b"tc" {
5226                    break;
5227                }
5228            }
5229            Ok(Event::Eof) => break,
5230            Err(e) => return Err(crate::Error::Xml(format!("tc parse: {e}"))),
5231            _ => {}
5232        }
5233        buf.clear();
5234    }
5235
5236    Ok(cell)
5237}
5238
5239/// 解析 `<a:tcPr>` 的属性和子元素到 [`Cell`]。
5240fn parse_tc_pr_into(xml: &str, cell: &mut crate::oxml::table::Cell) {
5241    use crate::oxml::table::VerticalAnchor;
5242
5243    let mut rd = Reader::from_str(xml);
5244    let mut buf = Vec::new();
5245
5246    loop {
5247        match rd.read_event_into(&mut buf) {
5248            Ok(Event::Start(e)) => {
5249                let name = e.name();
5250                let local = local_name(name.as_ref());
5251                // tcPr 自身的属性(marT/marL/marB/marR/anchor)
5252                // 注意:根元素是 tcPr,其属性在 Start 事件上
5253                if local == b"tcPr" {
5254                    for a in e.attributes().flatten() {
5255                        let v = a
5256                            .normalized_value(quick_xml::XmlVersion::Implicit1_0)
5257                            .unwrap_or_default();
5258                        match a.key.as_ref() {
5259                            b"marT" => {
5260                                if let Ok(m) = v.parse::<i64>() {
5261                                    cell.margin.0 = Some(Emu(m));
5262                                }
5263                            }
5264                            b"marL" => {
5265                                if let Ok(m) = v.parse::<i64>() {
5266                                    cell.margin.1 = Some(Emu(m));
5267                                }
5268                            }
5269                            b"marB" => {
5270                                if let Ok(m) = v.parse::<i64>() {
5271                                    cell.margin.2 = Some(Emu(m));
5272                                }
5273                            }
5274                            b"marR" => {
5275                                if let Ok(m) = v.parse::<i64>() {
5276                                    cell.margin.3 = Some(Emu(m));
5277                                }
5278                            }
5279                            b"anchor" => {
5280                                // 使用 &*v 解引用 Cow<str> 为 &str,避免不稳定的 str::as_str()
5281                                cell.anchor = match &*v {
5282                                    "t" => VerticalAnchor::Top,
5283                                    "ctr" => VerticalAnchor::Middle,
5284                                    "b" => VerticalAnchor::Bottom,
5285                                    _ => VerticalAnchor::Middle,
5286                                };
5287                            }
5288                            _ => {}
5289                        }
5290                    }
5291                } else if local == b"solidFill" {
5292                    // 单元格填充色
5293                    if let Ok(color) = parse_solid_fill(&mut rd, e.into_owned()) {
5294                        cell.fill = color;
5295                    }
5296                } else if local == b"lnL" {
5297                    // 单元格左边框:分支独立,避免 e.into_owned() 后 local 引用失效
5298                    let border = parse_cell_border(&mut rd, e.into_owned());
5299                    cell.border_left = Some(border);
5300                } else if local == b"lnR" {
5301                    let border = parse_cell_border(&mut rd, e.into_owned());
5302                    cell.border_right = Some(border);
5303                } else if local == b"lnT" {
5304                    let border = parse_cell_border(&mut rd, e.into_owned());
5305                    cell.border_top = Some(border);
5306                } else if local == b"lnB" {
5307                    let border = parse_cell_border(&mut rd, e.into_owned());
5308                    cell.border_bottom = Some(border);
5309                } else {
5310                    let _ = collect_full_element(&mut rd, e.into_owned());
5311                }
5312            }
5313            Ok(Event::Empty(e)) => {
5314                let name = e.name();
5315                let local = local_name(name.as_ref());
5316                if local == b"tcPr" {
5317                    // 自闭合 tcPr:仅属性
5318                    for a in e.attributes().flatten() {
5319                        let v = a
5320                            .normalized_value(quick_xml::XmlVersion::Implicit1_0)
5321                            .unwrap_or_default();
5322                        match a.key.as_ref() {
5323                            b"marT" => {
5324                                if let Ok(m) = v.parse::<i64>() {
5325                                    cell.margin.0 = Some(Emu(m));
5326                                }
5327                            }
5328                            b"marL" => {
5329                                if let Ok(m) = v.parse::<i64>() {
5330                                    cell.margin.1 = Some(Emu(m));
5331                                }
5332                            }
5333                            b"marB" => {
5334                                if let Ok(m) = v.parse::<i64>() {
5335                                    cell.margin.2 = Some(Emu(m));
5336                                }
5337                            }
5338                            b"marR" => {
5339                                if let Ok(m) = v.parse::<i64>() {
5340                                    cell.margin.3 = Some(Emu(m));
5341                                }
5342                            }
5343                            b"anchor" => {
5344                                // 使用 &*v 解引用 Cow<str> 为 &str,避免不稳定的 str::as_str()
5345                                cell.anchor = match &*v {
5346                                    "t" => VerticalAnchor::Top,
5347                                    "ctr" => VerticalAnchor::Middle,
5348                                    "b" => VerticalAnchor::Bottom,
5349                                    _ => VerticalAnchor::Middle,
5350                                };
5351                            }
5352                            _ => {}
5353                        }
5354                    }
5355                }
5356            }
5357            Ok(Event::End(e)) => {
5358                if local_name(e.name().as_ref()) == b"tcPr" {
5359                    return;
5360                }
5361            }
5362            Ok(Event::Eof) => return,
5363            _ => {}
5364        }
5365        buf.clear();
5366    }
5367}
5368
5369/// 解析单元格边框 `<a:lnL>` / `<a:lnR>` / `<a:lnT>` / `<a:lnB>` 为 [`CellBorder`]。
5370fn parse_cell_border<R: std::io::BufRead>(
5371    rd: &mut Reader<R>,
5372    start: quick_xml::events::BytesStart<'static>,
5373) -> crate::oxml::table::CellBorder {
5374    use crate::oxml::table::CellBorder;
5375    let mut border = CellBorder::default();
5376    // 读取 w 属性
5377    for a in start.attributes().flatten() {
5378        if a.key.as_ref() == b"w" {
5379            if let Ok(v) = a
5380                .normalized_value(quick_xml::XmlVersion::Implicit1_0)
5381                .unwrap_or_default()
5382                .parse::<i64>()
5383            {
5384                border.width = Emu(v);
5385            }
5386        }
5387    }
5388    let mut buf = Vec::new();
5389    loop {
5390        match rd.read_event_into(&mut buf) {
5391            Ok(Event::Empty(e)) => {
5392                let name = e.name();
5393                let local = local_name(name.as_ref());
5394                if local == b"noFill" {
5395                    border.no_fill = true;
5396                }
5397                // solidFill 自闭合(<a:solidFill/>)无颜色子元素,跳过
5398            }
5399            Ok(Event::Start(e)) => {
5400                let name = e.name();
5401                let local = local_name(name.as_ref());
5402                if local == b"solidFill" {
5403                    if let Ok(color) = parse_solid_fill(rd, e.into_owned()) {
5404                        border.color = color;
5405                    }
5406                } else {
5407                    let _ = collect_full_element(rd, e.into_owned());
5408                }
5409            }
5410            Ok(Event::End(e)) => {
5411                // 返回到调用者:边框元素结束
5412                let name = e.name();
5413                let local = local_name(name.as_ref());
5414                if local == b"lnL" || local == b"lnR" || local == b"lnT" || local == b"lnB" {
5415                    return border;
5416                }
5417            }
5418            Ok(Event::Eof) => return border,
5419            _ => {}
5420        }
5421        buf.clear();
5422    }
5423}
5424
5425/// 从 `<p:notes>` / `<p:notesSlide>` 元素解析出 `TextBody`。
5426///
5427/// 元素结构(OOXML 严格顺序):
5428///
5429/// ```text
5430/// <p:notes>
5431///   <p:cSld>
5432///     <p:spTree>
5433///       <p:nvGrpSpPr/>
5434///       <p:grpSpPr/>
5435///       <p:sp>
5436///         <p:nvSpPr>...</p:nvSpPr>
5437///         <p:spPr>...</p:spPr>
5438///         <p:txBody>...</p:txBody>   ← 备注文本
5439///       </p:sp>
5440///     </p:spTree>
5441///   </p:cSld>
5442/// </p:notes>
5443/// ```
5444///
5445/// # 行为
5446/// - 仅取第一个 `p:sp` 内的 `p:txBody`,**忽略** `p:sp` 之外的其它内容;
5447/// - 若没有 `p:sp` 或没有 `p:txBody`,返回 `Ok(TextBody::new())`(空备注)。
5448///
5449/// # 错误
5450/// - [`crate::Error::Xml`]:XML 语法错误。
5451pub fn parse_notes(xml: &str) -> crate::Result<TextBody> {
5452    let mut rd = Reader::from_str(xml);
5453    rd.config_mut().trim_text(true);
5454    let mut buf = Vec::new();
5455    let mut tx_buf: Option<String> = None;
5456    loop {
5457        match rd.read_event_into(&mut buf) {
5458            Ok(Event::Start(e)) => {
5459                let name = e.name();
5460                let local = local_name(name.as_ref());
5461                if local == b"txBody" && tx_buf.is_none() {
5462                    let inner = collect_full_element(&mut rd, e.into_owned())?;
5463                    tx_buf = Some(inner);
5464                }
5465                // 其它 Start 事件(notes / cSld / spTree / sp / nvSpPr / spPr 等)
5466                // 不调用 collect_full_element——否则会把 txBody 后代一起吞掉。
5467                // 让子元素在后续迭代中自然展开,直到遇到 txBody 为止。
5468            }
5469            Ok(Event::Eof) => break,
5470            Err(e) => return Err(crate::Error::Xml(format!("notes parse: {e}"))),
5471            _ => {}
5472        }
5473        buf.clear();
5474    }
5475    if let Some(buf) = tx_buf {
5476        parse_txbody(&buf)
5477    } else {
5478        Ok(TextBody::new())
5479    }
5480}
5481
5482/// 从 `commentN.xml` 解析评论列表(`<p:cmLst>`)。
5483///
5484/// # 行为
5485/// - 遍历所有 `<p:cm>` 元素,提取 `authorId` / `dt` / `idx` 属性;
5486/// - 对每个 `<p:cm>` 内部的 `<p:pos x="..." y="..."/>` 和 `<p:text>...</p:text>` 分别解析;
5487/// - 忽略无法识别的子元素。
5488///
5489/// # 错误
5490/// - [`crate::Error::Xml`]:XML 语法错误。
5491pub fn parse_comments(xml: &str) -> crate::Result<crate::oxml::comments::CommentList> {
5492    use crate::oxml::comments::{Comment, CommentList};
5493
5494    let mut lst = CommentList::new();
5495    let mut rd = Reader::from_str(xml);
5496    rd.config_mut().trim_text(true);
5497    let mut buf = Vec::new();
5498    // 当前正在解析的 <p:cm> 的属性
5499    let mut cur_author_id: Option<u32> = None;
5500    let mut cur_dt: String = String::new();
5501    let mut cur_idx: Option<u32> = None;
5502    // 当前是否在 <p:cm> 内部
5503    let mut in_cm = false;
5504    // 当前是否在 <p:text> 内部(用于收集文本)
5505    let mut in_text = false;
5506    // 当前 <p:cm> 的 pos
5507    let mut cur_pos_x: i64 = 0;
5508    let mut cur_pos_y: i64 = 0;
5509    let mut cur_text = String::new();
5510
5511    loop {
5512        match rd.read_event_into(&mut buf) {
5513            Ok(Event::Start(e)) => {
5514                let name = e.name();
5515                let local = local_name(name.as_ref());
5516                if local == b"cm" {
5517                    // 进入新的 <p:cm>:重置状态
5518                    in_cm = true;
5519                    cur_author_id = None;
5520                    cur_dt.clear();
5521                    cur_idx = None;
5522                    cur_pos_x = 0;
5523                    cur_pos_y = 0;
5524                    cur_text.clear();
5525                    // 提取属性
5526                    for a in e.attributes().flatten() {
5527                        let key = a.key.as_ref();
5528                        let val = a
5529                            .normalized_value(quick_xml::XmlVersion::Implicit1_0)
5530                            .unwrap_or_default()
5531                            .to_string();
5532                        match key {
5533                            b"authorId" => {
5534                                cur_author_id = val.parse::<u32>().ok();
5535                            }
5536                            b"dt" => {
5537                                cur_dt = val;
5538                            }
5539                            b"idx" => {
5540                                cur_idx = val.parse::<u32>().ok();
5541                            }
5542                            _ => {}
5543                        }
5544                    }
5545                } else if in_cm && local == b"text" {
5546                    in_text = true;
5547                    cur_text.clear();
5548                }
5549            }
5550            Ok(Event::Empty(e)) => {
5551                let name = e.name();
5552                let local = local_name(name.as_ref());
5553                if in_cm && local == b"pos" {
5554                    // 自闭合 <p:pos x="..." y="..."/>
5555                    for a in e.attributes().flatten() {
5556                        let key = a.key.as_ref();
5557                        let val = a
5558                            .normalized_value(quick_xml::XmlVersion::Implicit1_0)
5559                            .unwrap_or_default();
5560                        match key {
5561                            b"x" => {
5562                                cur_pos_x = val.parse::<i64>().unwrap_or(0);
5563                            }
5564                            b"y" => {
5565                                cur_pos_y = val.parse::<i64>().unwrap_or(0);
5566                            }
5567                            _ => {}
5568                        }
5569                    }
5570                }
5571            }
5572            Ok(Event::Text(t)) => {
5573                if in_text {
5574                    cur_text.push_str(&t.decode().unwrap_or_default());
5575                }
5576            }
5577            Ok(Event::End(e)) => {
5578                let name = e.name();
5579                let local = local_name(name.as_ref());
5580                if local == b"cm" && in_cm {
5581                    // 结束当前 <p:cm>:组装并推入列表
5582                    let c = Comment {
5583                        author_id: cur_author_id.unwrap_or(0),
5584                        date_time: std::mem::take(&mut cur_dt),
5585                        idx: cur_idx.unwrap_or(0),
5586                        pos_x: cur_pos_x,
5587                        pos_y: cur_pos_y,
5588                        text: std::mem::take(&mut cur_text),
5589                    };
5590                    lst.push(c);
5591                    in_cm = false;
5592                    in_text = false;
5593                } else if local == b"text" && in_text {
5594                    in_text = false;
5595                }
5596            }
5597            Ok(Event::Eof) => break,
5598            Err(e) => return Err(crate::Error::Xml(format!("comments parse: {e}"))),
5599            _ => {}
5600        }
5601        buf.clear();
5602    }
5603    Ok(lst)
5604}
5605
5606/// 从 `commentAuthors.xml` 解析评论作者列表(`<p:cmAuthorLst>`)。
5607///
5608/// # 错误
5609/// - [`crate::Error::Xml`]:XML 语法错误。
5610pub fn parse_comment_authors(xml: &str) -> crate::Result<crate::oxml::comments::CommentAuthorList> {
5611    use crate::oxml::comments::{CommentAuthor, CommentAuthorList};
5612
5613    let mut lst = CommentAuthorList::new();
5614    let mut rd = Reader::from_str(xml);
5615    rd.config_mut().trim_text(true);
5616    let mut buf = Vec::new();
5617
5618    loop {
5619        match rd.read_event_into(&mut buf) {
5620            Ok(Event::Start(e)) | Ok(Event::Empty(e)) => {
5621                let name = e.name();
5622                let local = local_name(name.as_ref());
5623                if local == b"cmAuthor" {
5624                    let mut id: u32 = 0;
5625                    let mut name_val = String::new();
5626                    let mut initials = String::new();
5627                    for a in e.attributes().flatten() {
5628                        let key = a.key.as_ref();
5629                        let val = a
5630                            .normalized_value(quick_xml::XmlVersion::Implicit1_0)
5631                            .unwrap_or_default()
5632                            .to_string();
5633                        match key {
5634                            b"id" => {
5635                                id = val.parse::<u32>().unwrap_or(0);
5636                            }
5637                            b"name" => {
5638                                name_val = val;
5639                            }
5640                            b"initials" => {
5641                                initials = val;
5642                            }
5643                            _ => {}
5644                        }
5645                    }
5646                    lst.push(CommentAuthor::new(id, name_val, initials));
5647                }
5648            }
5649            Ok(Event::Eof) => break,
5650            Err(e) => return Err(crate::Error::Xml(format!("commentAuthors parse: {e}"))),
5651            _ => {}
5652        }
5653        buf.clear();
5654    }
5655    Ok(lst)
5656}
5657
5658/// 从 `presentation.xml` 解析出关键字段(`sldIdLst` + `sldSz` + `sectionLst`)。
5659///
5660/// # 返回值
5661/// 元组 `(slide_ids, slide_width, slide_height, sld_master_ids, sections)`:
5662/// - `slide_ids`:按文档顺序排列的 `(id, rid)` 列表;
5663/// - `slide_width` / `slide_height`:EMU 尺寸;若未指定则回落到 `None`;
5664/// - `sld_master_ids`:母版 `(id, rid)` 列表(TODO-001);
5665/// - `sections`:章节分组列表(TODO-039),若未指定则为空。
5666///
5667/// # 错误
5668/// - [`crate::Error::Xml`]:XML 语法错误或关键属性缺失。
5669#[allow(clippy::type_complexity)]
5670pub fn parse_pres_root(
5671    xml: &str,
5672) -> crate::Result<(
5673    Vec<(u32, String)>,
5674    Option<Emu>,
5675    Option<Emu>,
5676    Vec<(u32, String)>,
5677    crate::oxml::section::SectionList,
5678)> {
5679    let mut rd = Reader::from_str(xml);
5680    rd.config_mut().trim_text(true);
5681    let mut buf = Vec::new();
5682    let mut sld_ids: Vec<(u32, String)> = Vec::new();
5683    let mut sld_sz: (Option<Emu>, Option<Emu>) = (None, None);
5684    // TODO-001:解析 sldMasterIdLst
5685    let mut sld_master_ids: Vec<(u32, String)> = Vec::new();
5686    // TODO-039:解析 sectionLst(位于 extLst 内的 p14 扩展)
5687    let mut sections = crate::oxml::section::SectionList::default();
5688    loop {
5689        match rd.read_event_into(&mut buf) {
5690            Ok(Event::Empty(e)) => {
5691                let name = e.name();
5692                let local = local_name(name.as_ref());
5693                if local == b"sldId" {
5694                    let mut id: Option<u32> = None;
5695                    let mut rid: Option<String> = None;
5696                    for a in e.attributes().flatten() {
5697                        let k = a.key.as_ref();
5698                        // 无前缀 `id` 与带前缀 `r:id` 都接受。
5699                        if k == b"id" {
5700                            if let Ok(v) = a
5701                                .normalized_value(quick_xml::XmlVersion::Implicit1_0)
5702                                .unwrap_or_default()
5703                                .parse::<u32>()
5704                            {
5705                                id = Some(v);
5706                            }
5707                        } else if k == b"r:id" || k.ends_with(b":id") {
5708                            rid = Some(
5709                                a.normalized_value(quick_xml::XmlVersion::Implicit1_0)
5710                                    .unwrap_or_default()
5711                                    .to_string(),
5712                            );
5713                        }
5714                    }
5715                    if let (Some(i), Some(r)) = (id, rid) {
5716                        sld_ids.push((i, r));
5717                    }
5718                } else if local == b"sldMasterId" {
5719                    // TODO-001:解析 sldMasterId 元素
5720                    let mut id: Option<u32> = None;
5721                    let mut rid: Option<String> = None;
5722                    for a in e.attributes().flatten() {
5723                        let k = a.key.as_ref();
5724                        if k == b"id" {
5725                            if let Ok(v) = a
5726                                .normalized_value(quick_xml::XmlVersion::Implicit1_0)
5727                                .unwrap_or_default()
5728                                .parse::<u32>()
5729                            {
5730                                id = Some(v);
5731                            }
5732                        } else if k == b"r:id" || k.ends_with(b":id") {
5733                            rid = Some(
5734                                a.normalized_value(quick_xml::XmlVersion::Implicit1_0)
5735                                    .unwrap_or_default()
5736                                    .to_string(),
5737                            );
5738                        }
5739                    }
5740                    if let (Some(i), Some(r)) = (id, rid) {
5741                        sld_master_ids.push((i, r));
5742                    }
5743                } else if local == b"sldSz" {
5744                    let mut cx: Option<i64> = None;
5745                    let mut cy: Option<i64> = None;
5746                    for a in e.attributes().flatten() {
5747                        match a.key.as_ref() {
5748                            b"cx" => {
5749                                cx = a
5750                                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
5751                                    .unwrap_or_default()
5752                                    .parse()
5753                                    .ok()
5754                            }
5755                            b"cy" => {
5756                                cy = a
5757                                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
5758                                    .unwrap_or_default()
5759                                    .parse()
5760                                    .ok()
5761                            }
5762                            _ => {}
5763                        }
5764                    }
5765                    sld_sz = (cx.map(Emu), cy.map(Emu));
5766                }
5767            }
5768            Ok(Event::Start(e)) => {
5769                let name = e.name();
5770                let local = local_name(name.as_ref());
5771                if local == b"sldMasterId" {
5772                    // Start 形式的 sldMasterId(含子元素,罕见但规范允许)
5773                    let mut id: Option<u32> = None;
5774                    let mut rid: Option<String> = None;
5775                    for a in e.attributes().flatten() {
5776                        let k = a.key.as_ref();
5777                        if k == b"id" {
5778                            if let Ok(v) = a
5779                                .normalized_value(quick_xml::XmlVersion::Implicit1_0)
5780                                .unwrap_or_default()
5781                                .parse::<u32>()
5782                            {
5783                                id = Some(v);
5784                            }
5785                        } else if k == b"r:id" || k.ends_with(b":id") {
5786                            rid = Some(
5787                                a.normalized_value(quick_xml::XmlVersion::Implicit1_0)
5788                                    .unwrap_or_default()
5789                                    .to_string(),
5790                            );
5791                        }
5792                    }
5793                    if let (Some(i), Some(r)) = (id, rid) {
5794                        sld_master_ids.push((i, r));
5795                    }
5796                    // 跳过子元素直到 End
5797                    let _ = collect_full_element(&mut rd, e.into_owned());
5798                } else if local == b"extLst" {
5799                    // TODO-039:解析 extLst 内的 sectionLst 扩展(p14 命名空间)。
5800                    // 收集完整 extLst XML 后单独解析,避免主状态机嵌套过深。
5801                    let ext_lst_xml = collect_full_element(&mut rd, e.into_owned())?;
5802                    let parsed = parse_sections_from_ext_lst(&ext_lst_xml)?;
5803                    if !parsed.is_empty() {
5804                        sections = parsed;
5805                    }
5806                }
5807            }
5808            Ok(Event::Eof) => break,
5809            Err(e) => return Err(crate::Error::Xml(format!("pres root parse: {e}"))),
5810            _ => {}
5811        }
5812        buf.clear();
5813    }
5814    Ok((sld_ids, sld_sz.0, sld_sz.1, sld_master_ids, sections))
5815}
5816
5817/// 从 `<p:extLst>` 的 XML 片段中解析出 [`crate::oxml::section::SectionList`]。
5818///
5819/// 仅识别 `uri="{521415D9-36F7-43E2-AB2F-B90AF26B5E64}"` 的 `<p:ext>`,
5820/// 内部 `<p14:sectionLst>` 中的每个 `<p14:section>` 会被还原为
5821/// [`crate::oxml::section::Section`](name + slide_ids)。
5822///
5823/// # 返回值
5824/// - 解析到的章节列表(可能为空,表示 extLst 中无 section 扩展)。
5825///
5826/// # 错误
5827/// - [`crate::Error::Xml`]:XML 语法错误。
5828///
5829/// # 示例输入
5830/// ```xml
5831/// <p:extLst>
5832///   <p:ext uri="{521415D9-36F7-43E2-AB2F-B90AF26B5E64}">
5833///     <p14:sectionLst>
5834///       <p14:section name="章节一">
5835///         <p14:sldIdLst>
5836///           <p14:sldId id="256"/>
5837///         </p14:sldIdLst>
5838///       </p14:section>
5839///     </p14:sectionLst>
5840///   </p:ext>
5841/// </p:extLst>
5842/// ```
5843fn parse_sections_from_ext_lst(xml: &str) -> crate::Result<crate::oxml::section::SectionList> {
5844    let mut rd = Reader::from_str(xml);
5845    rd.config_mut().trim_text(true);
5846    let mut buf = Vec::new();
5847    let mut result = crate::oxml::section::SectionList::default();
5848    // 状态机:
5849    //   0 = 在 extLst 内寻找 ext
5850    //   1 = 在匹配的 ext 内寻找 sectionLst
5851    //   2 = 在 sectionLst 内解析 section
5852    //   3 = 在 section 内解析 sldIdLst
5853    let mut state: u8 = 0;
5854    // 当前正在构建的 section(在状态 2/3 中累积)
5855    let mut cur_name = String::new();
5856    let mut cur_ids: Vec<u32> = Vec::new();
5857
5858    loop {
5859        match rd.read_event_into(&mut buf) {
5860            Ok(Event::Start(e)) => {
5861                // 把 e.name() 绑定到 let,避免临时 QName 在 as_ref() 后 drop
5862                let name = e.name();
5863                let local = local_name(name.as_ref());
5864                match state {
5865                    0 if local == b"ext" => {
5866                        // 检查 uri 属性是否匹配 section 扩展
5867                        let mut uri_val = String::new();
5868                        for a in e.attributes().flatten() {
5869                            if a.key.as_ref() == b"uri" {
5870                                uri_val = a
5871                                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
5872                                    .unwrap_or_default()
5873                                    .to_string();
5874                            }
5875                        }
5876                        if uri_val == crate::oxml::section::SECTION_EXT_URI {
5877                            state = 1;
5878                        } else {
5879                            // 非 section 扩展——跳过整个 ext
5880                            let _ = collect_full_element(&mut rd, e.into_owned());
5881                        }
5882                    }
5883                    1 if local == b"sectionLst" => {
5884                        state = 2;
5885                    }
5886                    2 if local == b"section" => {
5887                        // 读取 name 属性
5888                        cur_name.clear();
5889                        cur_ids.clear();
5890                        for a in e.attributes().flatten() {
5891                            if a.key.as_ref() == b"name" {
5892                                cur_name = a
5893                                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
5894                                    .unwrap_or_default()
5895                                    .to_string();
5896                            }
5897                        }
5898                        state = 3;
5899                    }
5900                    3 if local == b"sldIdLst" => {
5901                        // 进入 sldIdLst,继续读取其中的 sldId(Empty 事件)
5902                    }
5903                    _ => {
5904                        // 其它未识别的子元素——整体跳过
5905                        let _ = collect_full_element(&mut rd, e.into_owned());
5906                    }
5907                }
5908            }
5909            Ok(Event::Empty(e)) => {
5910                // 把 e.name() 绑定到 let,避免临时 QName 在 as_ref() 后 drop
5911                let name = e.name();
5912                let local = local_name(name.as_ref());
5913                // 在 sldIdLst 内遇到自闭合的 sldId
5914                if state == 3 && local == b"sldId" {
5915                    for a in e.attributes().flatten() {
5916                        if a.key.as_ref() == b"id" {
5917                            if let Ok(v) = a
5918                                .normalized_value(quick_xml::XmlVersion::Implicit1_0)
5919                                .unwrap_or_default()
5920                                .parse::<u32>()
5921                            {
5922                                cur_ids.push(v);
5923                            }
5924                        }
5925                    }
5926                }
5927            }
5928            Ok(Event::End(e)) => {
5929                // 把 e.name() 绑定到 let,避免临时 QName 在 as_ref() 后 drop
5930                let name = e.name();
5931                let local = local_name(name.as_ref());
5932                match state {
5933                    3 if local == b"section" => {
5934                        // section 结束——提交
5935                        result.push(crate::oxml::section::Section {
5936                            name: std::mem::take(&mut cur_name),
5937                            slide_ids: std::mem::take(&mut cur_ids),
5938                        });
5939                        state = 2;
5940                    }
5941                    2 if local == b"sectionLst" => {
5942                        state = 1;
5943                    }
5944                    1 if local == b"ext" => {
5945                        state = 0;
5946                    }
5947                    0 if local == b"extLst" => {
5948                        break;
5949                    }
5950                    _ => {}
5951                }
5952            }
5953            Ok(Event::Eof) => break,
5954            Err(e) => return Err(crate::Error::Xml(format!("sectionLst parse: {e}"))),
5955            _ => {}
5956        }
5957        buf.clear();
5958    }
5959    Ok(result)
5960}
5961
5962/// 从 `slideMasterN.xml` 文本解析出 `SldMaster`。
5963///
5964/// # 解析内容
5965/// - `<p:spTree>` 内的所有 `<p:sp>`(其它子类型如 pic/grpSp 暂不解析,保留位置)
5966/// - `<p:sldLayoutIdLst>` 内的所有 `r:id`(layout 关系 id 列表)
5967///
5968/// # 忽略内容(后续扩展)
5969/// - `<p:clrMap>` 颜色映射
5970/// - `<p:txStyles>` 文本样式
5971/// - `<p:extLst>` 扩展列表
5972///
5973/// # 错误
5974/// - [`crate::Error::Xml`]:XML 语法错误。
5975pub fn parse_sld_master(xml: &str) -> crate::Result<crate::oxml::slidemaster::SldMaster> {
5976    let mut rd = Reader::from_str(xml);
5977    rd.config_mut().trim_text(true);
5978    let mut buf = Vec::new();
5979    let mut master = crate::oxml::slidemaster::SldMaster::default();
5980    // 状态机:0=等待 sldMaster,1=在 sldMaster 内,2=在 spTree 内,3=在 sldLayoutIdLst 内
5981    let mut state: u8 = 0;
5982    let mut sp_bufs: Vec<String> = Vec::new();
5983
5984    loop {
5985        match rd.read_event_into(&mut buf) {
5986            Ok(Event::Start(e)) => {
5987                let name = e.name();
5988                let local = local_name(name.as_ref());
5989                match state {
5990                    0 if local == b"sldMaster" => {
5991                        state = 1;
5992                    }
5993                    // cSld 是容器元素,不能跳过(内含 spTree)
5994                    1 if local == b"cSld" => {}
5995                    1 if local == b"spTree" => {
5996                        state = 2;
5997                    }
5998                    1 if local == b"sldLayoutIdLst" => {
5999                        state = 3;
6000                    }
6001                    2 if local == b"sp" => {
6002                        // 累积 sp 的内部 XML,走子解析
6003                        let inner = collect_full_element(&mut rd, e.into_owned())?;
6004                        sp_bufs.push(inner);
6005                    }
6006                    _ => {
6007                        // 其它子元素(clrMap / txStyles 等)—— 整个吞掉
6008                        let _ = collect_full_element(&mut rd, e.into_owned());
6009                    }
6010                }
6011            }
6012            Ok(Event::Empty(e)) => {
6013                let name = e.name();
6014                let local = local_name(name.as_ref());
6015                if state == 3 && local == b"sldLayoutId" {
6016                    // 提取 r:id 属性
6017                    for a in e.attributes().flatten() {
6018                        let k = a.key.as_ref();
6019                        if k == b"r:id" || k.ends_with(b":id") {
6020                            let rid = a
6021                                .normalized_value(quick_xml::XmlVersion::Implicit1_0)
6022                                .unwrap_or_default()
6023                                .to_string();
6024                            master.layout_rids.push(rid);
6025                        }
6026                    }
6027                }
6028            }
6029            Ok(Event::End(e)) => {
6030                let name = e.name();
6031                let local = local_name(name.as_ref());
6032                // state==2(spTree 结束)或 state==3(sldLayoutIdLst 结束)都回到 state=1
6033                if (state == 2 && local == b"spTree") || (state == 3 && local == b"sldLayoutIdLst")
6034                {
6035                    state = 1;
6036                } else if state == 1 && local == b"sldMaster" {
6037                    break;
6038                }
6039            }
6040            Ok(Event::Eof) => break,
6041            Err(e) => return Err(crate::Error::Xml(format!("sldMaster parse: {e}"))),
6042            _ => {}
6043        }
6044        buf.clear();
6045    }
6046    // 解析所有累积的 sp
6047    for s in sp_bufs {
6048        if let Ok(sp) = parse_sp(&s) {
6049            master.shapes.push(sp);
6050        }
6051    }
6052    Ok(master)
6053}
6054
6055/// 从 `notesMasterN.xml` 文本解析出 [`crate::oxml::notesmaster::NotesMaster`](TODO-045)。
6056///
6057/// 与 [`parse_sld_master`] 结构类似,但根元素是 `<p:notesMaster>`,
6058/// 且不含 `<p:sldLayoutIdLst>`(备注母版不挂版式)。
6059///
6060/// # 解析内容
6061/// - `<p:spTree>` 内的所有 `<p:sp>`(其它子类型如 pic/grpSp 暂不解析,保留位置)
6062///
6063/// # 忽略内容(后续扩展)
6064/// - `<p:clrMap>` 颜色映射
6065/// - `<p:notesStyle>` 备注文本样式
6066/// - `<p:extLst>` 扩展列表
6067///
6068/// # 错误
6069/// - [`crate::Error::Xml`]:XML 语法错误。
6070pub fn parse_notes_master(xml: &str) -> crate::Result<crate::oxml::notesmaster::NotesMaster> {
6071    let mut rd = Reader::from_str(xml);
6072    rd.config_mut().trim_text(true);
6073    let mut buf = Vec::new();
6074    let mut master = crate::oxml::notesmaster::NotesMaster::default();
6075    // 状态机:0=等待 notesMaster,1=在 notesMaster 内,2=在 spTree 内
6076    let mut state: u8 = 0;
6077    let mut sp_bufs: Vec<String> = Vec::new();
6078
6079    loop {
6080        match rd.read_event_into(&mut buf) {
6081            Ok(Event::Start(e)) => {
6082                let name = e.name();
6083                let local = local_name(name.as_ref());
6084                match state {
6085                    0 if local == b"notesMaster" => {
6086                        state = 1;
6087                    }
6088                    // cSld 是容器元素,不能跳过(内含 spTree)
6089                    1 if local == b"cSld" => {}
6090                    1 if local == b"spTree" => {
6091                        state = 2;
6092                    }
6093                    2 if local == b"sp" => {
6094                        // 累积 sp 的内部 XML,走子解析
6095                        let inner = collect_full_element(&mut rd, e.into_owned())?;
6096                        sp_bufs.push(inner);
6097                    }
6098                    _ => {
6099                        // 其它子元素(clrMap / notesStyle 等)—— 整个吞掉
6100                        let _ = collect_full_element(&mut rd, e.into_owned());
6101                    }
6102                }
6103            }
6104            Ok(Event::End(e)) => {
6105                let name = e.name();
6106                let local = local_name(name.as_ref());
6107                if state == 2 && local == b"spTree" {
6108                    state = 1;
6109                } else if state == 1 && local == b"notesMaster" {
6110                    break;
6111                }
6112            }
6113            Ok(Event::Eof) => break,
6114            Err(e) => return Err(crate::Error::Xml(format!("notesMaster parse: {e}"))),
6115            _ => {}
6116        }
6117        buf.clear();
6118    }
6119    // 解析所有累积的 sp
6120    for s in sp_bufs {
6121        if let Ok(sp) = parse_sp(&s) {
6122            master.shapes.push(sp);
6123        }
6124    }
6125    Ok(master)
6126}
6127
6128/// 从 `slideLayoutN.xml` 文本解析出 `SldLayout`。
6129///
6130/// # 解析内容
6131/// - `<p:sldLayout type="...">` 根元素的 `type` 属性
6132/// - `<p:cSld name="...">` 的 `name` 属性
6133/// - `<p:spTree>` 内的所有 `<p:sp>`
6134///
6135/// # 忽略内容(后续扩展)
6136/// - `<p:clrMapOvr>` 颜色映射覆盖
6137/// - `<p:transition>` 过渡
6138///
6139/// # 错误
6140/// - [`crate::Error::Xml`]:XML 语法错误。
6141pub fn parse_sld_layout(xml: &str) -> crate::Result<crate::oxml::slidelayout::SldLayout> {
6142    let mut rd = Reader::from_str(xml);
6143    rd.config_mut().trim_text(true);
6144    let mut buf = Vec::new();
6145    let mut layout = crate::oxml::slidelayout::SldLayout::default();
6146    // 状态机:0=等待 sldLayout,1=在 sldLayout 内,2=在 spTree 内
6147    let mut state: u8 = 0;
6148    let mut sp_bufs: Vec<String> = Vec::new();
6149
6150    loop {
6151        match rd.read_event_into(&mut buf) {
6152            Ok(Event::Start(e)) => {
6153                let name = e.name();
6154                let local = local_name(name.as_ref());
6155                match state {
6156                    0 if local == b"sldLayout" => {
6157                        // 提取 type 属性
6158                        for a in e.attributes().flatten() {
6159                            if a.key.as_ref() == b"type" {
6160                                layout.type_ = a
6161                                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
6162                                    .unwrap_or_default()
6163                                    .to_string();
6164                            }
6165                        }
6166                        state = 1;
6167                    }
6168                    1 if local == b"cSld" => {
6169                        // 提取 name 属性
6170                        for a in e.attributes().flatten() {
6171                            if a.key.as_ref() == b"name" {
6172                                layout.name = a
6173                                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
6174                                    .unwrap_or_default()
6175                                    .to_string();
6176                            }
6177                        }
6178                    }
6179                    1 if local == b"spTree" => {
6180                        state = 2;
6181                    }
6182                    2 if local == b"sp" => {
6183                        let inner = collect_full_element(&mut rd, e.into_owned())?;
6184                        sp_bufs.push(inner);
6185                    }
6186                    _ => {
6187                        let _ = collect_full_element(&mut rd, e.into_owned());
6188                    }
6189                }
6190            }
6191            Ok(Event::End(e)) => {
6192                let name = e.name();
6193                let local = local_name(name.as_ref());
6194                if state == 2 && local == b"spTree" {
6195                    state = 1;
6196                } else if state == 1 && local == b"sldLayout" {
6197                    break;
6198                }
6199            }
6200            Ok(Event::Eof) => break,
6201            Err(e) => return Err(crate::Error::Xml(format!("sldLayout parse: {e}"))),
6202            _ => {}
6203        }
6204        buf.clear();
6205    }
6206    // 解析所有累积的 sp
6207    for s in sp_bufs {
6208        if let Ok(sp) = parse_sp(&s) {
6209            layout.shapes.push(sp);
6210        }
6211    }
6212    Ok(layout)
6213}
6214
6215/// 从 `themeN.xml` 文本解析出 `Theme`。
6216///
6217/// # 解析内容
6218/// - `<a:theme name="...">` 根元素的 `name` 属性
6219/// - `<a:clrScheme>` 内的 12 个颜色(dk1/lt1/dk2/lt2/accent1-6/hlink/folHlink)
6220/// - `<a:fontScheme>` 内的 majorFont/minorFont 的 latin typeface
6221///
6222/// # 忽略内容(后续扩展)
6223/// - `<a:fmtScheme>` 格式方案
6224/// - `<a:objectDefaults>` 对象默认值
6225/// - `<a:extraClrSchemeLst>` 额外颜色方案
6226///
6227/// # 错误
6228/// - [`crate::Error::Xml`]:XML 语法错误。
6229pub fn parse_theme(xml: &str) -> crate::Result<crate::oxml::theme::Theme> {
6230    let mut rd = Reader::from_str(xml);
6231    rd.config_mut().trim_text(true);
6232    let mut buf = Vec::new();
6233    let mut theme = crate::oxml::theme::Theme::default();
6234
6235    loop {
6236        match rd.read_event_into(&mut buf) {
6237            Ok(Event::Start(e)) => {
6238                let name = e.name();
6239                let local = local_name(name.as_ref());
6240                if local == b"theme" {
6241                    // 提取 name 属性
6242                    for a in e.attributes().flatten() {
6243                        if a.key.as_ref() == b"name" {
6244                            theme.name = a
6245                                .normalized_value(quick_xml::XmlVersion::Implicit1_0)
6246                                .unwrap_or_default()
6247                                .to_string();
6248                        }
6249                    }
6250                } else if local == b"clrScheme" {
6251                    // 解析颜色方案
6252                    let inner = collect_full_element(&mut rd, e.into_owned())?;
6253                    theme.color_scheme = parse_clr_scheme(&inner);
6254                } else if local == b"fontScheme" {
6255                    // 解析字体方案
6256                    let inner = collect_full_element(&mut rd, e.into_owned())?;
6257                    theme.font_scheme = parse_font_scheme(&inner);
6258                } else if local == b"fmtScheme" {
6259                    // 解析格式方案(TODO-005:FormatScheme 结构化解析)
6260                    // 保留原始 XML 支持 round-trip,同时填充 4 个结构化字段
6261                    let inner = collect_full_element(&mut rd, e.into_owned())?;
6262                    let (fmt_name, raw) = parse_fmt_scheme(&inner);
6263                    theme.format_scheme.name = fmt_name;
6264                    theme.format_scheme.raw_xml = raw;
6265                    // 结构化解析:把 raw_xml 拆分为 fill_styles / line_styles / effect_styles / bg_fill_styles
6266                    theme.format_scheme.parse_from_raw_xml();
6267                } else if local == b"themeElements" {
6268                    // themeElements 是容器元素,不跳过(内含 clrScheme / fontScheme / fmtScheme)
6269                } else {
6270                    let _ = collect_full_element(&mut rd, e.into_owned());
6271                }
6272            }
6273            Ok(Event::Eof) => break,
6274            Err(e) => return Err(crate::Error::Xml(format!("theme parse: {e}"))),
6275            _ => {}
6276        }
6277        buf.clear();
6278    }
6279    Ok(theme)
6280}
6281
6282/// 解析 `<a:fmtScheme>` 的 name 属性和内部 raw XML。
6283///
6284/// 返回 `(name, raw_xml)`:
6285/// - `name`:`<a:fmtScheme name="...">` 的 name 属性值;
6286/// - `raw_xml`:`<a:fmtScheme>` 内部所有子元素的原始 XML 字符串(不含 fmtScheme 标签本身)。
6287fn parse_fmt_scheme(xml: &str) -> (String, String) {
6288    let mut fmt_name = String::new();
6289    let mut raw_xml = String::new();
6290    let mut rd = Reader::from_str(xml);
6291    let mut buf = Vec::new();
6292    let mut in_fmt_scheme = false;
6293
6294    loop {
6295        match rd.read_event_into(&mut buf) {
6296            Ok(Event::Start(e)) => {
6297                let ev_name = e.name();
6298                let local = local_name(ev_name.as_ref());
6299                if !in_fmt_scheme && local == b"fmtScheme" {
6300                    in_fmt_scheme = true;
6301                    // 提取 name 属性
6302                    for a in e.attributes().flatten() {
6303                        if a.key.as_ref() == b"name" {
6304                            fmt_name = a
6305                                .normalized_value(quick_xml::XmlVersion::Implicit1_0)
6306                                .unwrap_or_default()
6307                                .to_string();
6308                        }
6309                    }
6310                } else if in_fmt_scheme {
6311                    // 收集子元素到 raw_xml
6312                    let inner = collect_full_element(&mut rd, e.into_owned()).unwrap_or_default();
6313                    raw_xml.push_str(&inner);
6314                }
6315            }
6316            Ok(Event::Empty(e)) => {
6317                let ev_name = e.name();
6318                let local = local_name(ev_name.as_ref());
6319                if in_fmt_scheme {
6320                    // 自闭合子元素,写入 raw_xml
6321                    let mut s = String::new();
6322                    s.push('<');
6323                    s.push_str(std::str::from_utf8(local).unwrap_or(""));
6324                    for a in e.attributes().flatten() {
6325                        s.push(' ');
6326                        s.push_str(std::str::from_utf8(a.key.as_ref()).unwrap_or(""));
6327                        s.push_str("=\"");
6328                        s.push_str(
6329                            &a.normalized_value(quick_xml::XmlVersion::Implicit1_0)
6330                                .unwrap_or_default(),
6331                        );
6332                        s.push('"');
6333                    }
6334                    s.push_str("/>");
6335                    raw_xml.push_str(&s);
6336                }
6337            }
6338            Ok(Event::End(e)) => {
6339                let ev_name = e.name();
6340                let local = local_name(ev_name.as_ref());
6341                if in_fmt_scheme && local == b"fmtScheme" {
6342                    break;
6343                }
6344            }
6345            Ok(Event::Eof) => break,
6346            _ => {}
6347        }
6348        buf.clear();
6349    }
6350    (fmt_name, raw_xml)
6351}
6352
6353/// 解析 `<a:clrScheme>` 内的 12 个颜色。
6354fn parse_clr_scheme(xml: &str) -> crate::oxml::theme::ColorScheme {
6355    use crate::oxml::theme::{ColorScheme, ThemeColor};
6356
6357    let mut scheme = ColorScheme::default();
6358    let mut rd = Reader::from_str(xml);
6359    let mut buf = Vec::new();
6360
6361    loop {
6362        match rd.read_event_into(&mut buf) {
6363            Ok(Event::Start(e)) => {
6364                let name = e.name();
6365                let local = local_name(name.as_ref());
6366                // 根据颜色槽位名分发,把颜色值写入对应字段
6367                let slot: &mut Option<ThemeColor> = match local {
6368                    b"dk1" => &mut scheme.dk1,
6369                    b"lt1" => &mut scheme.lt1,
6370                    b"dk2" => &mut scheme.dk2,
6371                    b"lt2" => &mut scheme.lt2,
6372                    b"accent1" => &mut scheme.accent1,
6373                    b"accent2" => &mut scheme.accent2,
6374                    b"accent3" => &mut scheme.accent3,
6375                    b"accent4" => &mut scheme.accent4,
6376                    b"accent5" => &mut scheme.accent5,
6377                    b"accent6" => &mut scheme.accent6,
6378                    b"hlink" => &mut scheme.hlink,
6379                    b"folHlink" => &mut scheme.fol_hlink,
6380                    // clrScheme 根元素自身 —— 提取 name 属性,不跳过子树
6381                    b"clrScheme" => {
6382                        // 提取 name 属性(TODO-005)
6383                        for a in e.attributes().flatten() {
6384                            if a.key.as_ref() == b"name" {
6385                                scheme.name = a
6386                                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
6387                                    .unwrap_or_default()
6388                                    .to_string();
6389                            }
6390                        }
6391                        continue;
6392                    }
6393                    _ => {
6394                        // 非颜色槽位未知元素 —— 跳过子树
6395                        let _ = collect_full_element(&mut rd, e.into_owned());
6396                        continue;
6397                    }
6398                };
6399                *slot = Some(read_color_child(&mut rd, local));
6400            }
6401            Ok(Event::End(e)) => {
6402                if local_name(e.name().as_ref()) == b"clrScheme" {
6403                    break;
6404                }
6405            }
6406            Ok(Event::Eof) => break,
6407            _ => {}
6408        }
6409        buf.clear();
6410    }
6411    scheme
6412}
6413
6414/// 读取颜色槽位(如 `<a:dk1>`)内的子元素 `<a:srgbClr>` 或 `<a:sysClr>`。
6415///
6416/// `parent_local` 是颜色槽位的 local name(如 `b"dk1"`),用于判断何时返回。
6417fn read_color_child<R: std::io::BufRead>(
6418    rd: &mut Reader<R>,
6419    parent_local: &[u8],
6420) -> crate::oxml::theme::ThemeColor {
6421    use crate::oxml::theme::ThemeColor;
6422    let mut buf = Vec::new();
6423    loop {
6424        match rd.read_event_into(&mut buf) {
6425            Ok(Event::Start(e)) => {
6426                let name = e.name();
6427                let local = local_name(name.as_ref());
6428                if local == b"srgbClr" {
6429                    let mut val = String::new();
6430                    for a in e.attributes().flatten() {
6431                        if a.key.as_ref() == b"val" {
6432                            val = a
6433                                .normalized_value(quick_xml::XmlVersion::Implicit1_0)
6434                                .unwrap_or_default()
6435                                .to_string();
6436                        }
6437                    }
6438                    // 消费 srgbClr 的 End 事件
6439                    let _ = collect_full_element(rd, e.into_owned());
6440                    return ThemeColor::Srgb(val);
6441                } else if local == b"sysClr" {
6442                    let mut val = String::new();
6443                    let mut last_clr = String::new();
6444                    for a in e.attributes().flatten() {
6445                        match a.key.as_ref() {
6446                            b"val" => {
6447                                val = a
6448                                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
6449                                    .unwrap_or_default()
6450                                    .to_string();
6451                            }
6452                            b"lastClr" => {
6453                                last_clr = a
6454                                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
6455                                    .unwrap_or_default()
6456                                    .to_string();
6457                            }
6458                            _ => {}
6459                        }
6460                    }
6461                    let _ = collect_full_element(rd, e.into_owned());
6462                    return ThemeColor::Sys(val, last_clr);
6463                } else {
6464                    let _ = collect_full_element(rd, e.into_owned());
6465                }
6466            }
6467            Ok(Event::Empty(e)) => {
6468                let name = e.name();
6469                let local = local_name(name.as_ref());
6470                if local == b"srgbClr" {
6471                    for a in e.attributes().flatten() {
6472                        if a.key.as_ref() == b"val" {
6473                            return ThemeColor::Srgb(
6474                                a.normalized_value(quick_xml::XmlVersion::Implicit1_0)
6475                                    .unwrap_or_default()
6476                                    .to_string(),
6477                            );
6478                        }
6479                    }
6480                } else if local == b"sysClr" {
6481                    let mut val = String::new();
6482                    let mut last_clr = String::new();
6483                    for a in e.attributes().flatten() {
6484                        match a.key.as_ref() {
6485                            b"val" => {
6486                                val = a
6487                                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
6488                                    .unwrap_or_default()
6489                                    .to_string();
6490                            }
6491                            b"lastClr" => {
6492                                last_clr = a
6493                                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
6494                                    .unwrap_or_default()
6495                                    .to_string();
6496                            }
6497                            _ => {}
6498                        }
6499                    }
6500                    return ThemeColor::Sys(val, last_clr);
6501                }
6502            }
6503            Ok(Event::End(e)) => {
6504                // 回到颜色槽位的 End —— 没有找到颜色子元素
6505                if local_name(e.name().as_ref()) == parent_local {
6506                    return ThemeColor::None;
6507                }
6508            }
6509            Ok(Event::Eof) => return ThemeColor::None,
6510            _ => {}
6511        }
6512        buf.clear();
6513    }
6514}
6515
6516/// 解析 `<a:fontScheme>` 内的 majorFont/minorFont。
6517fn parse_font_scheme(xml: &str) -> crate::oxml::theme::FontScheme {
6518    let mut scheme = crate::oxml::theme::FontScheme::default();
6519    let mut rd = Reader::from_str(xml);
6520    let mut buf = Vec::new();
6521    // 0=等待 fontScheme,1=在 fontScheme 内
6522    let mut state: u8 = 0;
6523
6524    loop {
6525        match rd.read_event_into(&mut buf) {
6526            Ok(Event::Start(e)) => {
6527                let name = e.name();
6528                let local = local_name(name.as_ref());
6529                if local == b"fontScheme" {
6530                    state = 1;
6531                    // 提取 name 属性(TODO-005)
6532                    for a in e.attributes().flatten() {
6533                        if a.key.as_ref() == b"name" {
6534                            scheme.name = a
6535                                .normalized_value(quick_xml::XmlVersion::Implicit1_0)
6536                                .unwrap_or_default()
6537                                .to_string();
6538                        }
6539                    }
6540                } else if state == 1 && local == b"majorFont" {
6541                    // 读取 latin / ea / cs typeface(TODO-005:扩展 FontScheme)
6542                    let (latin, ea, cs) = read_font_typefaces(&mut rd);
6543                    scheme.major_latin = latin;
6544                    scheme.major_ea = ea;
6545                    scheme.major_cs = cs;
6546                } else if state == 1 && local == b"minorFont" {
6547                    let (latin, ea, cs) = read_font_typefaces(&mut rd);
6548                    scheme.minor_latin = latin;
6549                    scheme.minor_ea = ea;
6550                    scheme.minor_cs = cs;
6551                }
6552            }
6553            Ok(Event::End(e)) => {
6554                if local_name(e.name().as_ref()) == b"fontScheme" {
6555                    break;
6556                }
6557            }
6558            Ok(Event::Eof) => break,
6559            _ => {}
6560        }
6561        buf.clear();
6562    }
6563    scheme
6564}
6565
6566/// 从 majorFont/minorFont 内读取 `<a:latin>` / `<a:ea>` / `<a:cs>` 的 typeface。
6567///
6568/// 返回 `(latin, ea, cs)` 三个 typeface 字符串。
6569/// 在 majorFont/minorFont 的 End 事件时返回(空值表示未找到)。
6570fn read_font_typefaces<R: std::io::BufRead>(rd: &mut Reader<R>) -> (String, String, String) {
6571    let mut buf = Vec::new();
6572    let mut latin = String::new();
6573    let mut ea = String::new();
6574    let mut cs = String::new();
6575    loop {
6576        match rd.read_event_into(&mut buf) {
6577            Ok(Event::Empty(e)) => {
6578                let name = e.name();
6579                let local = local_name(name.as_ref());
6580                // 提取 typeface 属性
6581                let typeface = extract_typeface(&e);
6582                match local {
6583                    b"latin" => latin = typeface,
6584                    b"ea" => ea = typeface,
6585                    b"cs" => cs = typeface,
6586                    _ => {}
6587                }
6588            }
6589            Ok(Event::Start(e)) => {
6590                let name = e.name();
6591                let local = local_name(name.as_ref());
6592                let typeface = extract_typeface(&e);
6593                match local {
6594                    b"latin" => latin = typeface,
6595                    b"ea" => ea = typeface,
6596                    b"cs" => cs = typeface,
6597                    _ => {}
6598                }
6599                // 消费 Start 元素的子树(含 End)
6600                let _ = collect_full_element(rd, e.into_owned());
6601            }
6602            Ok(Event::End(e)) => {
6603                let name = e.name();
6604                let local = local_name(name.as_ref());
6605                if local == b"majorFont" || local == b"minorFont" {
6606                    return (latin, ea, cs);
6607                }
6608            }
6609            Ok(Event::Eof) => return (latin, ea, cs),
6610            _ => {}
6611        }
6612        buf.clear();
6613    }
6614}
6615
6616/// 从元素的属性中提取 `typeface` 值。
6617fn extract_typeface(e: &quick_xml::events::BytesStart<'_>) -> String {
6618    for a in e.attributes().flatten() {
6619        if a.key.as_ref() == b"typeface" {
6620            return a
6621                .normalized_value(quick_xml::XmlVersion::Implicit1_0)
6622                .unwrap_or_default()
6623                .to_string();
6624        }
6625    }
6626    String::new()
6627}
6628
6629#[cfg(test)]
6630mod tests {
6631    use super::*;
6632    use crate::oxml::txbody::TextBody;
6633
6634    // ===================== TODO-050 backdrop 解析测试 =====================
6635
6636    /// 验证 `parse_backdrop` 解析完整 backdrop(含 anchor + 全平面)。
6637    #[test]
6638    fn parse_backdrop_full() {
6639        let xml = r#"<?xml version="1.0"?>
6640<a:backdrop xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
6641  <a:anchor x="100000" y="200000" z="300000"/>
6642  <a:floor/>
6643  <a:wall/>
6644  <a:l/>
6645  <a:r/>
6646  <a:t/>
6647  <a:b/>
6648</a:backdrop>"#;
6649        let mut rd = Reader::from_str(xml);
6650        rd.config_mut().trim_text(true);
6651        let mut buf = Vec::new();
6652        let start = loop {
6653            match rd.read_event_into(&mut buf) {
6654                Ok(Event::Start(e)) if local_name(e.name().as_ref()) == b"backdrop" => {
6655                    break e.into_owned();
6656                }
6657                Ok(Event::Eof) => panic!("backdrop not found"),
6658                _ => {}
6659            }
6660            buf.clear();
6661        };
6662        let bd = parse_backdrop(&mut rd, start).expect("parse backdrop");
6663
6664        assert_eq!(
6665            bd.anchor,
6666            Some(Point3d {
6667                x: 100000,
6668                y: 200000,
6669                z: 300000
6670            })
6671        );
6672        assert!(bd.floor);
6673        assert!(bd.wall);
6674        assert!(bd.left);
6675        assert!(bd.right);
6676        assert!(bd.top);
6677        assert!(bd.bottom);
6678    }
6679
6680    /// 验证 `parse_backdrop` 仅启用部分平面。
6681    #[test]
6682    fn parse_backdrop_partial() {
6683        let xml = r#"<?xml version="1.0"?>
6684<a:backdrop xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
6685  <a:floor/>
6686  <a:wall/>
6687</a:backdrop>"#;
6688        let mut rd = Reader::from_str(xml);
6689        rd.config_mut().trim_text(true);
6690        let mut buf = Vec::new();
6691        let start = loop {
6692            match rd.read_event_into(&mut buf) {
6693                Ok(Event::Start(e)) if local_name(e.name().as_ref()) == b"backdrop" => {
6694                    break e.into_owned();
6695                }
6696                Ok(Event::Eof) => panic!("backdrop not found"),
6697                _ => {}
6698            }
6699            buf.clear();
6700        };
6701        let bd = parse_backdrop(&mut rd, start).expect("parse backdrop");
6702
6703        assert!(bd.anchor.is_none());
6704        assert!(bd.floor);
6705        assert!(bd.wall);
6706        assert!(!bd.left);
6707        assert!(!bd.right);
6708        assert!(!bd.top);
6709        assert!(!bd.bottom);
6710    }
6711
6712    /// 验证 `parse_scene_3d` 正确解析 backdrop 子元素(round-trip 验证)。
6713    #[test]
6714    fn parse_scene_3d_with_backdrop() {
6715        let xml = r#"<?xml version="1.0"?>
6716<a:scene3d xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
6717  <a:camera prst="orthographicFront"/>
6718  <a:lightRig rig="balanced" dir="t"/>
6719  <a:backdrop>
6720    <a:anchor x="0" y="0" z="0"/>
6721    <a:floor/>
6722    <a:wall/>
6723  </a:backdrop>
6724</a:scene3d>"#;
6725        let mut rd = Reader::from_str(xml);
6726        rd.config_mut().trim_text(true);
6727        let mut buf = Vec::new();
6728        let start = loop {
6729            match rd.read_event_into(&mut buf) {
6730                Ok(Event::Start(e)) if local_name(e.name().as_ref()) == b"scene3d" => {
6731                    break e.into_owned();
6732                }
6733                Ok(Event::Eof) => panic!("scene3d not found"),
6734                _ => {}
6735            }
6736            buf.clear();
6737        };
6738        let scene = parse_scene_3d(&mut rd, start).expect("parse scene3d");
6739
6740        // 验证 backdrop 被解析
6741        let bd = scene.backdrop.expect("backdrop should be parsed");
6742        assert!(bd.floor);
6743        assert!(bd.wall);
6744        assert!(!bd.left);
6745        assert_eq!(bd.anchor, Some(Point3d { x: 0, y: 0, z: 0 }));
6746    }
6747
6748    /// 验证 `parse_scene_3d` 无 backdrop 时 backdrop 字段为 None(向后兼容)。
6749    #[test]
6750    fn parse_scene_3d_without_backdrop() {
6751        let xml = r#"<?xml version="1.0"?>
6752<a:scene3d xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
6753  <a:camera prst="orthographicFront"/>
6754  <a:lightRig rig="balanced" dir="t"/>
6755</a:scene3d>"#;
6756        let mut rd = Reader::from_str(xml);
6757        rd.config_mut().trim_text(true);
6758        let mut buf = Vec::new();
6759        let start = loop {
6760            match rd.read_event_into(&mut buf) {
6761                Ok(Event::Start(e)) if local_name(e.name().as_ref()) == b"scene3d" => {
6762                    break e.into_owned();
6763                }
6764                Ok(Event::Eof) => panic!("scene3d not found"),
6765                _ => {}
6766            }
6767            buf.clear();
6768        };
6769        let scene = parse_scene_3d(&mut rd, start).expect("parse scene3d");
6770        assert!(scene.backdrop.is_none());
6771    }
6772
6773    #[test]
6774    fn parse_minimal_sld() {
6775        let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
6776<p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
6777       xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
6778       xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
6779  <p:cSld>
6780    <p:spTree>
6781      <p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
6782      <p:grpSpPr><a:xfrm/></p:grpSpPr>
6783      <p:sp>
6784        <p:nvSpPr>
6785          <p:cNvPr id="2" name="TextBox 1"/>
6786          <p:cNvSpPr txBox="1"/>
6787          <p:nvPr/>
6788        </p:nvSpPr>
6789        <p:spPr>
6790          <a:xfrm>
6791            <a:off x="914400" y="914400"/>
6792            <a:ext cx="3657600" cy="457200"/>
6793          </a:xfrm>
6794          <a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
6795          <a:noFill/>
6796        </p:spPr>
6797        <p:txBody>
6798          <a:bodyPr wrap="square" rtlCol="0"><a:noAutofit/></a:bodyPr>
6799          <a:p>
6800            <a:r><a:rPr lang="en-US"/><a:t>Hello</a:t></a:r>
6801          </a:p>
6802        </p:txBody>
6803      </p:sp>
6804    </p:spTree>
6805  </p:cSld>
6806  <p:clrMapOvr/>
6807</p:sld>"#;
6808        let sld = parse_sld(xml).expect("parse ok");
6809        assert_eq!(sld.shapes.len(), 1);
6810        if let OxmlSlideShape::Sp(sp) = &sld.shapes[0] {
6811            assert_eq!(sp.id, 2);
6812            assert_eq!(sp.name, "TextBox 1");
6813            assert!(sp.c_nv_sp_pr_tx_box);
6814            assert_eq!(sp.text.paragraphs.len(), 1);
6815            assert_eq!(sp.text.paragraphs[0].runs.len(), 1);
6816            assert_eq!(sp.text.paragraphs[0].runs[0].text, "Hello");
6817        } else {
6818            panic!("expected Sp");
6819        }
6820    }
6821
6822    #[test]
6823    fn parse_empty_txbody_keeps_one_paragraph() {
6824        let xml = r#"<p:txBody xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
6825            <a:bodyPr/>
6826        </p:txBody>"#;
6827        let tb = parse_txbody(xml).unwrap();
6828        assert!(tb.paragraphs.is_empty());
6829        // TextBody 解析出来确实没有段落——这与 PowerPoint 期望"至少一个段落"略不同,
6830        // 但符合"无段落就不生成"的最小语义。
6831        let tb2 = TextBody::new();
6832        assert!(tb2.paragraphs.is_empty());
6833    }
6834
6835    #[test]
6836    fn parse_pres_root_extracts_sld_id_lst_and_size() {
6837        let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
6838<p:presentation xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
6839                xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
6840  <p:sldMasterIdLst><p:sldMasterId id="2147483648" r:id="rId1"/></p:sldMasterIdLst>
6841  <p:sldIdLst>
6842    <p:sldId id="256" r:id="rId2"/>
6843    <p:sldId id="257" r:id="rId3"/>
6844  </p:sldIdLst>
6845  <p:sldSz cx="9144000" cy="6858000"/>
6846</p:presentation>"#;
6847        let (ids, w, h, master_ids, _sections) = parse_pres_root(xml).expect("parse ok");
6848        assert_eq!(ids.len(), 2);
6849        assert_eq!(ids[0], (256, "rId2".to_string()));
6850        assert_eq!(ids[1], (257, "rId3".to_string()));
6851        assert_eq!(w.map(|v| v.0), Some(9_144_000));
6852        assert_eq!(h.map(|v| v.0), Some(6_858_000));
6853        // TODO-001:验证 sldMasterIdLst 解析
6854        assert_eq!(master_ids.len(), 1);
6855        assert_eq!(master_ids[0], (2147483648, "rId1".to_string()));
6856    }
6857
6858    #[test]
6859    fn parse_notes_returns_text_body() {
6860        let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
6861<p:notes xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
6862         xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
6863  <p:cSld>
6864    <p:spTree>
6865      <p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
6866      <p:grpSpPr/>
6867      <p:sp>
6868        <p:nvSpPr>
6869          <p:cNvPr id="2" name="Notes Placeholder"/>
6870          <p:cNvSpPr txBox="1"/>
6871          <p:nvPr><p:ph type="body" idx="1"/></p:nvPr>
6872        </p:nvSpPr>
6873        <p:spPr/>
6874        <p:txBody>
6875          <a:bodyPr/>
6876          <a:p><a:r><a:t>hello notes</a:t></a:r></a:p>
6877        </p:txBody>
6878      </p:sp>
6879    </p:spTree>
6880  </p:cSld>
6881</p:notes>"#;
6882        let tb = parse_notes(xml).expect("parse notes ok");
6883        assert_eq!(tb.paragraphs.len(), 1);
6884        assert_eq!(tb.paragraphs[0].runs.len(), 1);
6885        assert_eq!(tb.paragraphs[0].runs[0].text, "hello notes");
6886    }
6887
6888    /// `parse_pic` 必须能正确解析 r:embed 与 srcRect。
6889    ///
6890    /// 早期版本用 `find("r:embed=")` 字符串扫描;本测试覆盖**自闭合 `<a:blip/>`** 与
6891    /// **非自闭合 `<a:blip>...</a:blip>`** 两种形态 + 缩进变化,确保 SAX 解析稳定。
6892    #[test]
6893    fn parse_pic_extracts_rid_and_src_rect() {
6894        // 自闭合 blip + 有 srcRect
6895        let xml1 = r#"<p:pic xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
6896                            xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
6897                            xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
6898  <p:nvPicPr><p:cNvPr id="42" name="MyPic"/><p:cNvPicPr/><p:nvPr/></p:nvPicPr>
6899  <p:blipFill>
6900    <a:blip r:embed="rIdImg5"/>
6901    <a:srcRect l="1000" t="2000" r="3000" b="4000"/>
6902  </p:blipFill>
6903  <p:spPr/>
6904</p:pic>"#;
6905        let p1 = parse_pic(xml1).expect("parse pic 1");
6906        assert_eq!(p1.id, 42);
6907        assert_eq!(p1.name, "MyPic");
6908        assert_eq!(p1.rid, "rIdImg5");
6909        assert_eq!(p1.src_rect, Some((1000, 2000, 3000, 4000)));
6910
6911        // 非自闭合 blip
6912        let xml2 = r#"<p:pic xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
6913                            xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
6914                            xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
6915<p:nvPicPr><p:cNvPr id="7" name="p2"/><p:cNvPicPr/><p:nvPr/></p:nvPicPr>
6916<p:blipFill><a:blip r:embed="rIdImg99"></a:blip></p:blipFill>
6917<p:spPr/>
6918</p:pic>"#;
6919        let p2 = parse_pic(xml2).expect("parse pic 2");
6920        assert_eq!(p2.id, 7);
6921        assert_eq!(p2.rid, "rIdImg99");
6922    }
6923
6924    /// `collect_attr_value` 之前是 noop + 比较 noop,导致 typeface 永远取不到。
6925    /// 本测试确保 `<a:latin typeface="..."/>` 能正确解析。
6926    #[test]
6927    fn parse_run_with_typeface_works() {
6928        let xml = r#"<a:r xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
6929  <a:rPr>
6930    <a:latin typeface="Calibri"/>
6931    <a:ea typeface="宋体"/>
6932    <a:cs typeface="Times New Roman"/>
6933  </a:rPr>
6934  <a:t>hello</a:t>
6935</a:r>"#;
6936        let r = parse_run(xml).expect("parse run");
6937        assert_eq!(r.text, "hello");
6938        assert_eq!(r.properties.latin_font.as_deref(), Some("Calibri"));
6939        assert_eq!(r.properties.eastasia_font.as_deref(), Some("宋体"));
6940        assert_eq!(r.properties.cs_font.as_deref(), Some("Times New Roman"));
6941    }
6942
6943    /// `Sp::write_xml` 在 `is_placeholder=true` 但 `ph_type=None` 时,**必须**默认写出 `type="body"`。
6944    /// 早期版本会写出无属性的 `<p:ph/>`,PowerPoint 弹警告。
6945    #[test]
6946    fn write_sp_placeholder_defaults_to_body() {
6947        use crate::oxml::shape::Sp;
6948        use crate::oxml::writer::XmlWriter;
6949        let sp = Sp {
6950            id: 3,
6951            name: "Ph".into(),
6952            is_placeholder: true,
6953            ph_idx: Some(0),
6954            ph_type: None, // 关键:None 时应默认 body
6955            ..Default::default()
6956        };
6957        let mut w = XmlWriter::new();
6958        sp.write_xml(&mut w);
6959        let s = w.into_string();
6960        assert!(
6961            s.contains(r#"<p:ph type="body" idx="0"/>"#),
6962            "应写出 type=\"body\",实际:{s}"
6963        );
6964    }
6965
6966    /// `Connector::set_begin/set_end` 必须**自动**同步 xfrm 边界盒,
6967    /// 否则序列化后 begin/end 位置信息丢失。
6968    #[test]
6969    fn connector_set_begin_end_recomputes_xfrm() {
6970        use crate::oxml::simpletypes::MsoConnectorType;
6971        use crate::shape::connector::Connector;
6972        use crate::units::EmuPoint;
6973        let mut c = Connector::new_with_type("c1", MsoConnectorType::Straight);
6974        c.set_begin(EmuPoint::new(1000, 2000));
6975        c.set_end(EmuPoint::new(5000, 6000));
6976        // bounding box: off=(1000,2000), ext=(4000,4000)
6977        assert_eq!(c.properties().xfrm.off_x.unwrap().value(), 1000);
6978        assert_eq!(c.properties().xfrm.off_y.unwrap().value(), 2000);
6979        assert_eq!(c.properties().xfrm.ext_cx.unwrap().value(), 4000);
6980        assert_eq!(c.properties().xfrm.ext_cy.unwrap().value(), 4000);
6981    }
6982
6983    /// 验证 `parse_sld_master` 能从最小 slideMaster XML 中提取 shapes 和 layout_rids。
6984    #[test]
6985    fn parse_minimal_sld_master() {
6986        let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
6987<p:sldMaster xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
6988             xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
6989             xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
6990  <p:cSld>
6991    <p:spTree>
6992      <p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
6993      <p:grpSpPr><a:xfrm/></p:grpSpPr>
6994      <p:sp>
6995        <p:nvSpPr>
6996          <p:cNvPr id="2" name="Title Placeholder 1"/>
6997          <p:cNvSpPr txBox="1"/>
6998          <p:nvPr><p:ph type="title" idx="0"/></p:nvPr>
6999        </p:nvSpPr>
7000        <p:spPr><a:xfrm><a:off x="457200" y="274638"/><a:ext cx="8229600" cy="1143000"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></p:spPr>
7001        <p:txBody><a:bodyPr/><a:lstStyle/><a:p><a:endParaRPr lang="en-US"/></a:p></p:txBody>
7002      </p:sp>
7003    </p:spTree>
7004  </p:cSld>
7005  <p:clrMap bg1="lt1" tx1="dk1" bg2="lt2" tx2="dk2" accent1="accent1" accent2="accent2" accent3="accent3" accent4="accent4" accent5="accent5" accent6="accent6" hlink="hlink" folHlink="folHlink"/>
7006  <p:sldLayoutIdLst><p:sldLayoutId id="2147483649" r:id="rId1"/></p:sldLayoutIdLst>
7007</p:sldMaster>"#;
7008        let master = parse_sld_master(xml).unwrap();
7009        assert_eq!(master.shapes.len(), 1, "应解析出 1 个 shape");
7010        assert_eq!(master.shapes[0].id, 2);
7011        assert_eq!(master.shapes[0].name, "Title Placeholder 1");
7012        assert!(master.shapes[0].is_placeholder);
7013        assert_eq!(master.shapes[0].ph_type.as_deref(), Some("title"));
7014        assert_eq!(master.layout_rids.len(), 1, "应解析出 1 个 layout rid");
7015        assert_eq!(master.layout_rids[0], "rId1");
7016    }
7017
7018    /// 验证 `parse_sld_layout` 能从最小 slideLayout XML 中提取 type/name/shapes。
7019    #[test]
7020    fn parse_minimal_sld_layout() {
7021        let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
7022<p:sldLayout xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
7023             xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
7024             xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
7025             type="title" preserve="1">
7026  <p:cSld name="Title Slide">
7027    <p:spTree>
7028      <p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
7029      <p:grpSpPr><a:xfrm/></p:grpSpPr>
7030      <p:sp>
7031        <p:nvSpPr>
7032          <p:cNvPr id="2" name="Title 1"/>
7033          <p:cNvSpPr txBox="1"/>
7034          <p:nvPr><p:ph type="title" idx="0"/></p:nvPr>
7035        </p:nvSpPr>
7036        <p:spPr><a:xfrm><a:off x="685800" y="2130425"/><a:ext cx="7772400" cy="1470025"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></p:spPr>
7037        <p:txBody><a:bodyPr/><a:lstStyle/><a:p><a:endParaRPr lang="en-US"/></a:p></p:txBody>
7038      </p:sp>
7039    </p:spTree>
7040  </p:cSld>
7041  <p:clrMapOvr bg1="lt1" tx1="dk1" bg2="lt2" tx2="dk2"/>
7042</p:sldLayout>"#;
7043        let layout = parse_sld_layout(xml).unwrap();
7044        assert_eq!(layout.type_, "title");
7045        assert_eq!(layout.name, "Title Slide");
7046        assert_eq!(layout.shapes.len(), 1, "应解析出 1 个 shape");
7047        assert_eq!(layout.shapes[0].name, "Title 1");
7048        assert!(layout.shapes[0].is_placeholder);
7049    }
7050
7051    /// 验证 `parse_theme` 能从 theme XML 中提取 name/color_scheme/font_scheme/fmt_scheme。
7052    #[test]
7053    fn parse_theme_extracts_name_and_colors() {
7054        let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
7055<a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" name="Office Theme">
7056  <a:themeElements>
7057    <a:clrScheme name="Office">
7058      <a:dk1><a:sysClr val="windowText" lastClr="000000"/></a:dk1>
7059      <a:lt1><a:sysClr val="window" lastClr="FFFFFF"/></a:lt1>
7060      <a:dk2><a:srgbClr val="1F497D"/></a:dk2>
7061      <a:lt2><a:srgbClr val="EEECE1"/></a:lt2>
7062      <a:accent1><a:srgbClr val="4F81BD"/></a:accent1>
7063      <a:accent2><a:srgbClr val="C0504D"/></a:accent2>
7064      <a:accent3><a:srgbClr val="9BBB59"/></a:accent3>
7065      <a:accent4><a:srgbClr val="8064A2"/></a:accent4>
7066      <a:accent5><a:srgbClr val="4BACC6"/></a:accent5>
7067      <a:accent6><a:srgbClr val="F79646"/></a:accent6>
7068      <a:hlink><a:srgbClr val="0000FF"/></a:hlink>
7069      <a:folHlink><a:srgbClr val="800080"/></a:folHlink>
7070    </a:clrScheme>
7071    <a:fontScheme name="Office">
7072      <a:majorFont><a:latin typeface="Calibri"/><a:ea typeface=""/><a:cs typeface=""/></a:majorFont>
7073      <a:minorFont><a:latin typeface="Calibri"/><a:ea typeface=""/><a:cs typeface=""/></a:minorFont>
7074    </a:fontScheme>
7075    <a:fmtScheme name="Office">
7076      <a:fillStyleLst>
7077        <a:solidFill><a:schemeClr val="phClr"/></a:solidFill>
7078      </a:fillStyleLst>
7079      <a:lnStyleLst/>
7080      <a:effectStyleLst/>
7081      <a:bgFillStyleLst/>
7082    </a:fmtScheme>
7083  </a:themeElements>
7084</a:theme>"#;
7085        let theme = parse_theme(xml).unwrap();
7086        assert_eq!(theme.name, "Office Theme");
7087        // 验证颜色方案名(TODO-005)
7088        assert_eq!(theme.color_scheme.name, "Office");
7089        // 验证颜色方案(用 assert_eq! + Debug 格式以便定位问题)
7090        assert_eq!(
7091            format!("{:?}", theme.color_scheme.dk1),
7092            r#"Some(Sys("windowText", "000000"))"#,
7093            "dk1 mismatch"
7094        );
7095        assert_eq!(
7096            format!("{:?}", theme.color_scheme.lt1),
7097            r#"Some(Sys("window", "FFFFFF"))"#,
7098            "lt1 mismatch"
7099        );
7100        assert_eq!(
7101            format!("{:?}", theme.color_scheme.accent1),
7102            r#"Some(Srgb("4F81BD"))"#,
7103            "accent1 mismatch"
7104        );
7105        assert_eq!(
7106            format!("{:?}", theme.color_scheme.hlink),
7107            r#"Some(Srgb("0000FF"))"#,
7108            "hlink mismatch"
7109        );
7110        // 验证字体方案名(TODO-005)
7111        assert_eq!(theme.font_scheme.name, "Office");
7112        // 验证字体方案
7113        assert_eq!(theme.font_scheme.major_latin, "Calibri");
7114        assert_eq!(theme.font_scheme.minor_latin, "Calibri");
7115        // 验证格式方案(TODO-005)
7116        assert_eq!(theme.format_scheme.name, "Office");
7117        assert!(
7118            !theme.format_scheme.raw_xml.is_empty(),
7119            "fmtScheme raw_xml should not be empty"
7120        );
7121        assert!(
7122            theme.format_scheme.raw_xml.contains("<a:fillStyleLst>"),
7123            "fmtScheme raw_xml should contain fillStyleLst"
7124        );
7125    }
7126
7127    /// 验证 `parse_theme` 能解析 fontScheme 的 ea/cs typeface(TODO-005)。
7128    #[test]
7129    fn parse_theme_font_scheme_ea_cs() {
7130        let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
7131<a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" name="Test">
7132  <a:themeElements>
7133    <a:clrScheme name="Test"/>
7134    <a:fontScheme name="Test">
7135      <a:majorFont>
7136        <a:latin typeface="Calibri Light"/>
7137        <a:ea typeface="宋体"/>
7138        <a:cs typeface="Times New Roman"/>
7139      </a:majorFont>
7140      <a:minorFont>
7141        <a:latin typeface="Calibri"/>
7142        <a:ea typeface="黑体"/>
7143        <a:cs typeface="Arial"/>
7144      </a:minorFont>
7145    </a:fontScheme>
7146    <a:fmtScheme name="Test"/>
7147  </a:themeElements>
7148</a:theme>"#;
7149        let theme = parse_theme(xml).unwrap();
7150        assert_eq!(theme.font_scheme.major_latin, "Calibri Light");
7151        assert_eq!(theme.font_scheme.major_ea, "宋体");
7152        assert_eq!(theme.font_scheme.major_cs, "Times New Roman");
7153        assert_eq!(theme.font_scheme.minor_latin, "Calibri");
7154        assert_eq!(theme.font_scheme.minor_ea, "黑体");
7155        assert_eq!(theme.font_scheme.minor_cs, "Arial");
7156    }
7157
7158    /// 验证 theme 的 round-trip:parse → to_xml → parse 一致性(TODO-005)。
7159    #[test]
7160    fn theme_round_trip() {
7161        let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
7162<a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" name="RoundTrip">
7163  <a:themeElements>
7164    <a:clrScheme name="RT">
7165      <a:dk1><a:sysClr val="windowText" lastClr="000000"/></a:dk1>
7166      <a:lt1><a:sysClr val="window" lastClr="FFFFFF"/></a:lt1>
7167      <a:dk2><a:srgbClr val="1F497D"/></a:dk2>
7168      <a:lt2><a:srgbClr val="EEECE1"/></a:lt2>
7169      <a:accent1><a:srgbClr val="4F81BD"/></a:accent1>
7170      <a:accent2><a:srgbClr val="C0504D"/></a:accent2>
7171      <a:accent3><a:srgbClr val="9BBB59"/></a:accent3>
7172      <a:accent4><a:srgbClr val="8064A2"/></a:accent4>
7173      <a:accent5><a:srgbClr val="4BACC6"/></a:accent5>
7174      <a:accent6><a:srgbClr val="F79646"/></a:accent6>
7175      <a:hlink><a:srgbClr val="0000FF"/></a:hlink>
7176      <a:folHlink><a:srgbClr val="800080"/></a:folHlink>
7177    </a:clrScheme>
7178    <a:fontScheme name="RT">
7179      <a:majorFont><a:latin typeface="Calibri"/><a:ea typeface=""/><a:cs typeface=""/></a:majorFont>
7180      <a:minorFont><a:latin typeface="Calibri"/><a:ea typeface=""/><a:cs typeface=""/></a:minorFont>
7181    </a:fontScheme>
7182    <a:fmtScheme name="Office"/>
7183  </a:themeElements>
7184</a:theme>"#;
7185        // 第一次解析
7186        let theme1 = parse_theme(xml).unwrap();
7187        assert_eq!(theme1.name, "RoundTrip");
7188        assert_eq!(theme1.color_scheme.name, "RT");
7189        assert_eq!(theme1.font_scheme.name, "RT");
7190        // 序列化回 XML
7191        let serialized = theme1.to_xml();
7192        // 第二次解析
7193        let theme2 = parse_theme(&serialized).unwrap();
7194        // 验证关键字段一致
7195        assert_eq!(theme2.name, theme1.name);
7196        assert_eq!(theme2.color_scheme.name, theme1.color_scheme.name);
7197        assert_eq!(theme2.color_scheme.dk1, theme1.color_scheme.dk1);
7198        assert_eq!(theme2.color_scheme.accent1, theme1.color_scheme.accent1);
7199        assert_eq!(theme2.font_scheme.name, theme1.font_scheme.name);
7200        assert_eq!(
7201            theme2.font_scheme.major_latin,
7202            theme1.font_scheme.major_latin
7203        );
7204        assert_eq!(
7205            theme2.font_scheme.minor_latin,
7206            theme1.font_scheme.minor_latin
7207        );
7208    }
7209
7210    /// 验证 `parse_cxn_sp` 能正确解析连接器的 id/name/begin/end/stCxn/endCxn。
7211    ///
7212    /// 连接器是 OOXML 中特殊的形状(`<p:cxnSp>`),用于在形状间画线。
7213    /// `stCxn`/`endCxn` 是"挂接"信息——表示连接线粘附到哪个形状的哪个连接点。
7214    #[test]
7215    fn parse_cxn_sp_extracts_geometry_and_connections() {
7216        let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
7217<p:cxnSp xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
7218         xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
7219  <p:nvCxnSpPr>
7220    <p:cNvPr id="10" name="Connector1"/>
7221    <p:cNvCxnSpPr/>
7222    <p:nvPr>
7223      <p:stCxn id="2" idx="3"/>
7224      <p:endCxn id="5" idx="1"/>
7225    </p:nvPr>
7226  </p:nvCxnSpPr>
7227  <p:spPr>
7228    <a:xfrm>
7229      <a:off x="1000" y="2000"/>
7230      <a:ext cx="5000" cy="4000"/>
7231    </a:xfrm>
7232    <a:prstGeom prst="line"><a:avLst/></a:prstGeom>
7233  </p:spPr>
7234</p:cxnSp>"#;
7235        let cxn = parse_cxn_sp(xml).expect("parse cxnSp ok");
7236        assert_eq!(cxn.id, 10);
7237        assert_eq!(cxn.name, "Connector1");
7238        // 挂接信息
7239        assert_eq!(cxn.st_cxn, Some((2, 3)), "stCxn 应解析出 (id=2, idx=3)");
7240        assert_eq!(cxn.end_cxn, Some((5, 1)), "endCxn 应解析出 (id=5, idx=1)");
7241        // 几何坐标(xfrm 内的 off/ext)
7242        assert_eq!(cxn.properties.xfrm.off_x.unwrap().value(), 1000);
7243        assert_eq!(cxn.properties.xfrm.off_y.unwrap().value(), 2000);
7244        assert_eq!(cxn.properties.xfrm.ext_cx.unwrap().value(), 5000);
7245        assert_eq!(cxn.properties.xfrm.ext_cy.unwrap().value(), 4000);
7246    }
7247
7248    /// 验证 `parse_cxn_sp` 对**无挂接**的连接器也能正常解析(纯几何连接线)。
7249    #[test]
7250    fn parse_cxn_sp_without_connections() {
7251        let xml = r#"<p:cxnSp xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
7252         xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
7253  <p:nvCxnSpPr><p:cNvPr id="20" name="FreeLine"/><p:cNvCxnSpPr/><p:nvPr/></p:nvCxnSpPr>
7254  <p:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="100" cy="200"/></a:xfrm><a:prstGeom prst="line"><a:avLst/></a:prstGeom></p:spPr>
7255</p:cxnSp>"#;
7256        let cxn = parse_cxn_sp(xml).unwrap();
7257        assert_eq!(cxn.id, 20);
7258        assert_eq!(cxn.name, "FreeLine");
7259        assert!(cxn.st_cxn.is_none(), "无 stCxn 时应为 None");
7260        assert!(cxn.end_cxn.is_none(), "无 endCxn 时应为 None");
7261    }
7262
7263    /// 验证 `parse_cxn_sp` 能解析 `<p:style>` 主题样式引用(TODO-002 round-trip 补全)。
7264    #[test]
7265    fn parse_cxn_sp_with_style() {
7266        let xml = r#"<p:cxnSp xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
7267         xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
7268  <p:nvCxnSpPr><p:cNvPr id="30" name="StyledConn"/><p:cNvCxnSpPr/><p:nvPr/></p:nvCxnSpPr>
7269  <p:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="100" cy="200"/></a:xfrm><a:prstGeom prst="line"><a:avLst/></a:prstGeom></p:spPr>
7270  <p:style>
7271    <a:lnRef idx="2"><a:schemeClr val="accent1"/></a:lnRef>
7272    <a:fillRef idx="0"><a:schemeClr val="accent2"/></a:fillRef>
7273  </p:style>
7274</p:cxnSp>"#;
7275        let cxn = parse_cxn_sp(xml).expect("parse cxnSp ok");
7276        let style = cxn.style.as_ref().expect("style 应存在");
7277        let ln = style.line_ref.as_ref().expect("line_ref 应存在");
7278        assert_eq!(ln.idx.as_deref(), Some("2"));
7279        assert_eq!(ln.scheme_color.as_deref(), Some("accent1"));
7280        let fill = style.fill_ref.as_ref().expect("fill_ref 应存在");
7281        assert_eq!(fill.idx.as_deref(), Some("0"));
7282        assert_eq!(fill.scheme_color.as_deref(), Some("accent2"));
7283    }
7284
7285    /// 验证 `parse_grp_sp` 能解析组合自身的 `<p:style>` 主题样式引用。
7286    #[test]
7287    fn parse_grp_sp_with_style() {
7288        let xml = r#"<p:grpSp xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
7289         xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
7290  <p:nvGrpSpPr><p:cNvPr id="40" name="StyledGroup"/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
7291  <p:grpSpPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="1000" cy="1000"/><a:chOff x="0" y="0"/><a:chExt cx="1000" cy="1000"/></a:xfrm></p:grpSpPr>
7292  <p:style>
7293    <a:lnRef idx="1"><a:schemeClr val="tx1"/></a:lnRef>
7294    <a:fontRef idx="minor"><a:schemeClr val="dk1"/></a:fontRef>
7295  </p:style>
7296</p:grpSp>"#;
7297        let grp = parse_grp_sp(xml).expect("parse grpSp ok");
7298        assert_eq!(grp.id, 40);
7299        let style = grp.style.as_ref().expect("style 应存在");
7300        let ln = style.line_ref.as_ref().expect("line_ref 应存在");
7301        assert_eq!(ln.idx.as_deref(), Some("1"));
7302        assert_eq!(ln.scheme_color.as_deref(), Some("tx1"));
7303        let font = style.font_ref.as_ref().expect("font_ref 应存在");
7304        assert_eq!(font.idx.as_deref(), Some("minor"));
7305        assert_eq!(font.scheme_color.as_deref(), Some("dk1"));
7306    }
7307
7308    /// 验证 `parse_grp_sp` 能解析组合形状及其子形状。
7309    ///
7310    /// 组合(`<p:grpSp>`)是 OOXML 中把多个形状打包为一个整体的容器。
7311    /// 子形状可以是 sp/pic/cxnSp/grpSp(嵌套)/graphicFrame。
7312    #[test]
7313    fn parse_grp_sp_extracts_children() {
7314        let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
7315<p:grpSp xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
7316         xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
7317         xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
7318  <p:nvGrpSpPr>
7319    <p:cNvPr id="100" name="Group1"/>
7320    <p:cNvGrpSpPr/>
7321    <p:nvPr/>
7322  </p:nvGrpSpPr>
7323  <p:grpSpPr>
7324    <a:xfrm>
7325      <a:off x="0" y="0"/>
7326      <a:ext cx="8000" cy="6000"/>
7327      <a:chOff x="0" y="0"/>
7328      <a:chExt cx="8000" cy="6000"/>
7329    </a:xfrm>
7330  </p:grpSpPr>
7331  <p:sp>
7332    <p:nvSpPr><p:cNvPr id="101" name="Child1"/><p:cNvSpPr txBox="1"/><p:nvPr/></p:nvSpPr>
7333    <p:spPr><a:xfrm><a:off x="100" y="100"/><a:ext cx="2000" cy="1000"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></p:spPr>
7334    <p:txBody><a:bodyPr/><a:p><a:r><a:t>child text</a:t></a:r></a:p></p:txBody>
7335  </p:sp>
7336  <p:cxnSp>
7337    <p:nvCxnSpPr><p:cNvPr id="102" name="Link"/><p:cNvCxnSpPr/><p:nvPr/></p:nvCxnSpPr>
7338    <p:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="3000" cy="3000"/></a:xfrm><a:prstGeom prst="line"><a:avLst/></a:prstGeom></p:spPr>
7339  </p:cxnSp>
7340</p:grpSp>"#;
7341        let grp = parse_grp_sp(xml).expect("parse grpSp ok");
7342        assert_eq!(grp.id, 100);
7343        assert_eq!(grp.name, "Group1");
7344        assert_eq!(grp.children.len(), 2, "应解析出 2 个子形状");
7345
7346        // 第一个子形状:Sp
7347        match &grp.children[0] {
7348            GroupChild::Sp(sp) => {
7349                assert_eq!(sp.id, 101);
7350                assert_eq!(sp.name, "Child1");
7351                assert_eq!(sp.text.paragraphs[0].runs[0].text, "child text");
7352            }
7353            other => panic!("第一个子形状应为 Sp,实际:{other:?}"),
7354        }
7355        // 第二个子形状:CxnSp
7356        match &grp.children[1] {
7357            GroupChild::CxnSp(cxn) => {
7358                assert_eq!(cxn.id, 102);
7359                assert_eq!(cxn.name, "Link");
7360            }
7361            other => panic!("第二个子形状应为 CxnSp,实际:{other:?}"),
7362        }
7363    }
7364
7365    /// 验证 `parse_graphic_frame` 能解析承载表格的图形框。
7366    ///
7367    /// `<p:graphicFrame>` 是 OOXML 中承载表格/图表等复合图形的容器。
7368    /// 内部 `<a:graphicData uri="...">` 的 uri 决定具体类型:
7369    /// - `http://schemas.openxmlformats.org/drawingml/2006/table` → 表格
7370    #[test]
7371    fn parse_graphic_frame_with_table() {
7372        let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
7373<p:graphicFrame xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
7374                xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
7375                xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
7376  <p:nvGraphicFramePr>
7377    <p:cNvPr id="200" name="TableFrame"/>
7378    <p:cNvGraphicFramePr/>
7379    <p:nvPr/>
7380  </p:nvGraphicFramePr>
7381  <p:xfrm>
7382    <a:off x="457200" y="1828800"/>
7383    <a:ext cx="8229600" cy="1143000"/>
7384  </p:xfrm>
7385  <a:graphic>
7386    <a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/table">
7387      <a:tbl>
7388        <a:tblPr firstRow="1" bandRow="1"/>
7389        <a:tblGrid>
7390          <a:gridCol w="4114800"/>
7391          <a:gridCol w="4114800"/>
7392        </a:tblGrid>
7393        <a:tr h="370840" h="0">
7394          <a:tc>
7395            <a:txBody><a:bodyPr/><a:p><a:r><a:t>A1</a:t></a:r></a:p></a:txBody>
7396            <a:tcPr/>
7397          </a:tc>
7398          <a:tc>
7399            <a:txBody><a:bodyPr/><a:p><a:r><a:t>B1</a:t></a:r></a:p></a:txBody>
7400            <a:tcPr/>
7401          </a:tc>
7402        </a:tr>
7403        <a:tr h="370840" h="0">
7404          <a:tc>
7405            <a:txBody><a:bodyPr/><a:p><a:r><a:t>A2</a:t></a:r></a:p></a:txBody>
7406            <a:tcPr/>
7407          </a:tc>
7408          <a:tc>
7409            <a:txBody><a:bodyPr/><a:p><a:r><a:t>B2</a:t></a:r></a:p></a:txBody>
7410            <a:tcPr/>
7411          </a:tc>
7412        </a:tr>
7413      </a:tbl>
7414    </a:graphicData>
7415  </a:graphic>
7416</p:graphicFrame>"#;
7417        let frame = parse_graphic_frame(xml).expect("parse graphicFrame ok");
7418        assert_eq!(frame.id, 200);
7419        assert_eq!(frame.name, "TableFrame");
7420        // xfrm
7421        assert_eq!(frame.properties.xfrm.off_x.unwrap().value(), 457200);
7422        assert_eq!(frame.properties.xfrm.off_y.unwrap().value(), 1828800);
7423        assert_eq!(frame.properties.xfrm.ext_cx.unwrap().value(), 8229600);
7424        assert_eq!(frame.properties.xfrm.ext_cy.unwrap().value(), 1143000);
7425        // 表格内容
7426        match &frame.graphic {
7427            crate::oxml::shape::Graphic::Table(tbl) => {
7428                assert_eq!(tbl.cols.len(), 2, "应有 2 列");
7429                assert_eq!(tbl.rows.len(), 2, "应有 2 行");
7430                assert_eq!(tbl.cols[0].width.value(), 4114800);
7431                // 验证单元格文本
7432                let cell_a1 = &tbl.rows[0].cells[0];
7433                let text_a1: String = cell_a1
7434                    .text
7435                    .paragraphs
7436                    .iter()
7437                    .flat_map(|p| p.runs.iter().map(|r| r.text.as_str()))
7438                    .collect();
7439                assert_eq!(text_a1, "A1");
7440                let cell_b2 = &tbl.rows[1].cells[1];
7441                let text_b2: String = cell_b2
7442                    .text
7443                    .paragraphs
7444                    .iter()
7445                    .flat_map(|p| p.runs.iter().map(|r| r.text.as_str()))
7446                    .collect();
7447                assert_eq!(text_b2, "B2");
7448            }
7449            _ => panic!("期望 Table 变体"),
7450        }
7451    }
7452
7453    /// 验证 `parse_sld` 能解析包含**多种形状类型**的 slide(Sp + Pic + CxnSp + Group)。
7454    ///
7455    /// 这是 round-trip 测试的基础:如果 parse 阶段就丢失形状,save 后内容必然缺失。
7456    #[test]
7457    fn parse_sld_with_mixed_shape_types() {
7458        let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
7459<p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
7460       xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
7461       xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
7462  <p:cSld>
7463    <p:spTree>
7464      <p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
7465      <p:grpSpPr><a:xfrm/></p:grpSpPr>
7466      <p:sp>
7467        <p:nvSpPr><p:cNvPr id="2" name="TextBox"/><p:cNvSpPr txBox="1"/><p:nvPr/></p:nvSpPr>
7468        <p:spPr><a:xfrm><a:off x="100" y="100"/><a:ext cx="2000" cy="1000"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></p:spPr>
7469        <p:txBody><a:bodyPr/><a:p><a:r><a:t>text</a:t></a:r></a:p></p:txBody>
7470      </p:sp>
7471      <p:pic>
7472        <p:nvPicPr><p:cNvPr id="3" name="Pic1"/><p:cNvPicPr/><p:nvPr/></p:nvPicPr>
7473        <p:blipFill><a:blip r:embed="rIdImg1"/></p:blipFill>
7474        <p:spPr><a:xfrm><a:off x="5000" y="5000"/><a:ext cx="3000" cy="2000"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></p:spPr>
7475      </p:pic>
7476      <p:cxnSp>
7477        <p:nvCxnSpPr><p:cNvPr id="4" name="Conn1"/><p:cNvCxnSpPr/><p:nvPr/></p:nvCxnSpPr>
7478        <p:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="8000" cy="6000"/></a:xfrm><a:prstGeom prst="line"><a:avLst/></a:prstGeom></p:spPr>
7479      </p:cxnSp>
7480      <p:grpSp>
7481        <p:nvGrpSpPr><p:cNvPr id="5" name="Group1"/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
7482        <p:grpSpPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="1000" cy="1000"/><a:chOff x="0" y="0"/><a:chExt cx="1000" cy="1000"/></a:xfrm></p:grpSpPr>
7483        <p:sp>
7484          <p:nvSpPr><p:cNvPr id="6" name="Inner"/><p:cNvSpPr txBox="1"/><p:nvPr/></p:nvSpPr>
7485          <p:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="500" cy="500"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></p:spPr>
7486          <p:txBody><a:bodyPr/><a:p><a:r><a:t>inner</a:t></a:r></a:p></p:txBody>
7487        </p:sp>
7488      </p:grpSp>
7489    </p:spTree>
7490  </p:cSld>
7491  <p:clrMapOvr/>
7492</p:sld>"#;
7493        let sld = parse_sld(xml).expect("parse sld ok");
7494        assert_eq!(sld.shapes.len(), 4, "应解析出 4 个形状");
7495
7496        // 验证每个形状的类型和关键字段
7497        assert!(
7498            matches!(&sld.shapes[0], OxmlSlideShape::Sp(s) if s.id == 2 && s.name == "TextBox")
7499        );
7500        assert!(
7501            matches!(&sld.shapes[1], OxmlSlideShape::Pic(p) if p.id == 3 && p.rid == "rIdImg1")
7502        );
7503        assert!(
7504            matches!(&sld.shapes[2], OxmlSlideShape::CxnSp(c) if c.id == 4 && c.name == "Conn1")
7505        );
7506        match &sld.shapes[3] {
7507            OxmlSlideShape::Group(grp) => {
7508                assert_eq!(grp.id, 5);
7509                assert_eq!(grp.name, "Group1");
7510                assert_eq!(grp.children.len(), 1, "组合内应有 1 个子形状");
7511                assert!(
7512                    matches!(&grp.children[0], GroupChild::Sp(s) if s.id == 6 && s.name == "Inner")
7513                );
7514            }
7515            other => panic!("第 4 个形状应为 Group,实际:{other:?}"),
7516        }
7517    }
7518
7519    /// **Round-trip 保留测试**:parse → write → parse,验证关键字段不丢失。
7520    ///
7521    /// 这是 TODO-041 的核心测试:打开已有 PPTX 后修改再保存,
7522    /// 形状的 id/name/文本/几何坐标/挂接信息必须全部保留。
7523    #[test]
7524    fn round_trip_sld_preserves_shapes() {
7525        let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
7526<p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
7527       xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
7528       xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
7529  <p:cSld>
7530    <p:spTree>
7531      <p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
7532      <p:grpSpPr><a:xfrm/></p:grpSpPr>
7533      <p:sp>
7534        <p:nvSpPr><p:cNvPr id="2" name="TB1"/><p:cNvSpPr txBox="1"/><p:nvPr/></p:nvSpPr>
7535        <p:spPr><a:xfrm><a:off x="914400" y="457200"/><a:ext cx="3657600" cy="457200"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></p:spPr>
7536        <p:txBody><a:bodyPr/><a:p><a:r><a:t>round-trip</a:t></a:r></a:p></p:txBody>
7537      </p:sp>
7538      <p:cxnSp>
7539        <p:nvCxnSpPr><p:cNvPr id="3" name="C1"/><p:cNvCxnSpPr/><p:nvPr><p:stCxn id="2" idx="0"/><p:endCxn id="5" idx="1"/></p:nvPr></p:nvCxnSpPr>
7540        <p:spPr><a:xfrm><a:off x="100" y="200"/><a:ext cx="3000" cy="4000"/></a:xfrm><a:prstGeom prst="line"><a:avLst/></a:prstGeom></p:spPr>
7541      </p:cxnSp>
7542    </p:spTree>
7543  </p:cSld>
7544  <p:clrMapOvr/>
7545</p:sld>"#;
7546
7547        // 第一次解析
7548        let sld1 = parse_sld(xml).expect("first parse ok");
7549        assert_eq!(sld1.shapes.len(), 2);
7550
7551        // 序列化回 XML
7552        let rewritten = sld1.to_xml();
7553
7554        // 第二次解析(round-trip)
7555        let sld2 = parse_sld(&rewritten).expect("second parse ok");
7556        assert_eq!(sld2.shapes.len(), 2, "round-trip 后形状数应不变");
7557
7558        // 验证 Sp
7559        match (&sld1.shapes[0], &sld2.shapes[0]) {
7560            (OxmlSlideShape::Sp(a), OxmlSlideShape::Sp(b)) => {
7561                assert_eq!(a.id, b.id, "Sp id 应保留");
7562                assert_eq!(a.name, b.name, "Sp name 应保留");
7563                assert_eq!(
7564                    a.text.paragraphs[0].runs[0].text, b.text.paragraphs[0].runs[0].text,
7565                    "Sp 文本应保留"
7566                );
7567                // 几何坐标
7568                assert_eq!(
7569                    a.properties.xfrm.off_x, b.properties.xfrm.off_x,
7570                    "Sp off_x 应保留"
7571                );
7572                assert_eq!(
7573                    a.properties.xfrm.ext_cx, b.properties.xfrm.ext_cx,
7574                    "Sp ext_cx 应保留"
7575                );
7576            }
7577            _ => panic!("形状类型不匹配"),
7578        }
7579
7580        // 验证 CxnSp(含挂接信息)
7581        match (&sld1.shapes[1], &sld2.shapes[1]) {
7582            (OxmlSlideShape::CxnSp(a), OxmlSlideShape::CxnSp(b)) => {
7583                assert_eq!(a.id, b.id, "CxnSp id 应保留");
7584                assert_eq!(a.name, b.name, "CxnSp name 应保留");
7585                assert_eq!(a.st_cxn, b.st_cxn, "stCxn 挂接应保留");
7586                assert_eq!(a.end_cxn, b.end_cxn, "endCxn 挂接应保留");
7587            }
7588            _ => panic!("形状类型不匹配"),
7589        }
7590    }
7591
7592    /// **Round-trip 表格保留测试**:parse → write → parse,验证表格结构/单元格文本不丢失。
7593    #[test]
7594    fn round_trip_table_preserves_cells() {
7595        let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
7596<p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
7597       xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
7598       xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
7599  <p:cSld>
7600    <p:spTree>
7601      <p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
7602      <p:grpSpPr><a:xfrm/></p:grpSpPr>
7603      <p:graphicFrame>
7604        <p:nvGraphicFramePr><p:cNvPr id="10" name="Tbl1"/><p:cNvGraphicFramePr/><p:nvPr/></p:nvGraphicFramePr>
7605        <p:xfrm><a:off x="0" y="0"/><a:ext cx="8000" cy="4000"/></p:xfrm>
7606        <a:graphic>
7607          <a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/table">
7608            <a:tbl>
7609              <a:tblPr/>
7610              <a:tblGrid><a:gridCol w="4000"/><a:gridCol w="4000"/></a:tblGrid>
7611              <a:tr h="2000">
7612                <a:tc><a:txBody><a:bodyPr/><a:p><a:r><a:t>R1C1</a:t></a:r></a:p></a:txBody><a:tcPr/></a:tc>
7613                <a:tc><a:txBody><a:bodyPr/><a:p><a:r><a:t>R1C2</a:t></a:r></a:p></a:txBody><a:tcPr/></a:tc>
7614              </a:tr>
7615            </a:tbl>
7616          </a:graphicData>
7617        </a:graphic>
7618      </p:graphicFrame>
7619    </p:spTree>
7620  </p:cSld>
7621  <p:clrMapOvr/>
7622</p:sld>"#;
7623
7624        let sld1 = parse_sld(xml).expect("first parse ok");
7625        let rewritten = sld1.to_xml();
7626        let sld2 = parse_sld(&rewritten).expect("second parse ok");
7627
7628        assert_eq!(sld1.shapes.len(), sld2.shapes.len(), "形状数应一致");
7629        match (&sld1.shapes[0], &sld2.shapes[0]) {
7630            (OxmlSlideShape::GraphicFrame(f1), OxmlSlideShape::GraphicFrame(f2)) => {
7631                assert_eq!(f1.id, f2.id, "GraphicFrame id 应保留");
7632                assert_eq!(f1.name, f2.name, "GraphicFrame name 应保留");
7633                match (&f1.graphic, &f2.graphic) {
7634                    (
7635                        crate::oxml::shape::Graphic::Table(t1),
7636                        crate::oxml::shape::Graphic::Table(t2),
7637                    ) => {
7638                        assert_eq!(t1.cols.len(), t2.cols.len(), "列数应保留");
7639                        assert_eq!(t1.rows.len(), t2.rows.len(), "行数应保留");
7640                        // 验证单元格文本
7641                        let get_text = |t: &crate::oxml::table::Table| -> String {
7642                            t.rows[0].cells[0]
7643                                .text
7644                                .paragraphs
7645                                .iter()
7646                                .flat_map(|p| p.runs.iter().map(|r| r.text.as_str()))
7647                                .collect()
7648                        };
7649                        assert_eq!(get_text(t1), get_text(t2), "单元格文本应保留");
7650                        assert_eq!(get_text(t1), "R1C1");
7651                    }
7652                    _ => panic!("期望两侧均为 Table 变体"),
7653                }
7654            }
7655            _ => panic!("形状类型不匹配"),
7656        }
7657    }
7658
7659    /// 验证 `parse_sld` 对**空 spTree**(无形状)的容错。
7660    #[test]
7661    fn parse_sld_empty_sp_tree() {
7662        let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
7663<p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
7664       xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
7665  <p:cSld>
7666    <p:spTree>
7667      <p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
7668      <p:grpSpPr><a:xfrm/></p:grpSpPr>
7669    </p:spTree>
7670  </p:cSld>
7671  <p:clrMapOvr/>
7672</p:sld>"#;
7673        let sld = parse_sld(xml).expect("parse ok");
7674        assert_eq!(sld.shapes.len(), 0, "空 spTree 应解析出 0 个形状");
7675    }
7676
7677    /// 验证 `parse_grp_sp` 能正确解析**嵌套组合**(grpSp 内有 grpSp)。
7678    ///
7679    /// 早期版本因 `if local == b"grpSp"` 分支重复,嵌套组合被根元素分支吞掉,
7680    /// 导致嵌套 grpSp 的子形状全部丢失。本测试回归此 bug。
7681    #[test]
7682    fn parse_grp_sp_nested_group() {
7683        let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
7684<p:grpSp xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
7685         xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
7686  <p:nvGrpSpPr><p:cNvPr id="1" name="Outer"/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
7687  <p:grpSpPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="10000" cy="10000"/><a:chOff x="0" y="0"/><a:chExt cx="10000" cy="10000"/></a:xfrm></p:grpSpPr>
7688  <p:sp>
7689    <p:nvSpPr><p:cNvPr id="2" name="OuterChild"/><p:cNvSpPr txBox="1"/><p:nvPr/></p:nvSpPr>
7690    <p:spPr><a:xfrm><a:off x="100" y="100"/><a:ext cx="1000" cy="1000"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></p:spPr>
7691    <p:txBody><a:bodyPr/><a:p><a:r><a:t>outer</a:t></a:r></a:p></p:txBody>
7692  </p:sp>
7693  <p:grpSp>
7694    <p:nvGrpSpPr><p:cNvPr id="3" name="Inner"/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
7695    <p:grpSpPr><a:xfrm><a:off x="2000" y="2000"/><a:ext cx="5000" cy="5000"/><a:chOff x="0" y="0"/><a:chExt cx="5000" cy="5000"/></a:xfrm></p:grpSpPr>
7696    <p:sp>
7697      <p:nvSpPr><p:cNvPr id="4" name="InnerChild"/><p:cNvSpPr txBox="1"/><p:nvPr/></p:nvSpPr>
7698      <p:spPr><a:xfrm><a:off x="50" y="50"/><a:ext cx="500" cy="500"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></p:spPr>
7699      <p:txBody><a:bodyPr/><a:p><a:r><a:t>inner</a:t></a:r></a:p></p:txBody>
7700    </p:sp>
7701  </p:grpSp>
7702</p:grpSp>"#;
7703        let grp = parse_grp_sp(xml).expect("parse ok");
7704        assert_eq!(grp.id, 1);
7705        assert_eq!(grp.name, "Outer");
7706        assert_eq!(
7707            grp.children.len(),
7708            2,
7709            "外层组合应有 2 个子形状(sp + 嵌套 grpSp)"
7710        );
7711
7712        // 第一个子形状:外层 Sp
7713        assert!(
7714            matches!(&grp.children[0], GroupChild::Sp(s) if s.id == 2 && s.name == "OuterChild")
7715        );
7716
7717        // 第二个子形状:嵌套组合
7718        match &grp.children[1] {
7719            GroupChild::Group(inner) => {
7720                assert_eq!(inner.id, 3);
7721                assert_eq!(inner.name, "Inner");
7722                assert_eq!(inner.children.len(), 1, "内层组合应有 1 个子形状");
7723                assert!(
7724                    matches!(&inner.children[0], GroupChild::Sp(s) if s.id == 4 && s.name == "InnerChild")
7725                );
7726            }
7727            other => panic!("第二个子形状应为 Group,实际:{other:?}"),
7728        }
7729    }
7730
7731    /// 验证 `parse_sppr` 能解析**线性渐变填充**(`<a:gradFill><a:gsLst>...<a:lin ang="..."/></a:gradFill>`)。
7732    ///
7733    /// 这是 TODO-003 的核心测试:确保渐变光轨、角度、颜色全部正确提取。
7734    #[test]
7735    fn parse_sppr_grad_fill_linear() {
7736        let xml = r#"<p:spPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
7737                    xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
7738  <a:xfrm><a:off x="100" y="200"/><a:ext cx="300" cy="400"/></a:xfrm>
7739  <a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
7740  <a:gradFill flip="none" rotWithShape="1">
7741    <a:gsLst>
7742      <a:gs pos="0"><a:srgbClr val="FF0000"/></a:gs>
7743      <a:gs pos="100000"><a:srgbClr val="0000FF"/></a:gs>
7744    </a:gsLst>
7745    <a:lin ang="5400000" scaled="1"/>
7746  </a:gradFill>
7747</p:spPr>"#;
7748        let sp = parse_sppr(xml).expect("parse ok");
7749        match &sp.fill {
7750            Fill::Gradient(grad) => {
7751                assert_eq!(grad.stops.len(), 2, "应有 2 个渐变光轨");
7752                assert_eq!(grad.stops[0].pos, 0);
7753                assert_eq!(grad.stops[1].pos, 100000);
7754                // 验证颜色
7755                assert!(
7756                    matches!(grad.stops[0].color, Color::RGB(c) if c.0 == 0xFF && c.1 == 0x00 && c.2 == 0x00)
7757                );
7758                assert!(
7759                    matches!(grad.stops[1].color, Color::RGB(c) if c.0 == 0x00 && c.1 == 0x00 && c.2 == 0xFF)
7760                );
7761                // 验证线性角度(5400000 = 90° = 向下)
7762                assert_eq!(grad.gradient_type, GradientType::Linear(5400000));
7763                // 验证属性
7764                assert_eq!(grad.flip.as_deref(), Some("none"));
7765                assert_eq!(grad.rot_with_shape, Some(true));
7766            }
7767            other => panic!("fill 应为 Gradient,实际:{other:?}"),
7768        }
7769    }
7770
7771    /// 验证 `parse_sppr` 能解析**路径渐变填充**(`<a:gradFill><a:path path="circle"/></a:gradFill>`)。
7772    #[test]
7773    fn parse_sppr_grad_fill_path() {
7774        let xml = r#"<p:spPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
7775                    xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
7776  <a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
7777  <a:gradFill>
7778    <a:gsLst>
7779      <a:gs pos="0"><a:srgbClr val="00FF00"/></a:gs>
7780      <a:gs pos="50000"><a:srgbClr val="FFFF00"/></a:gs>
7781      <a:gs pos="100000"><a:srgbClr val="FF00FF"/></a:gs>
7782    </a:gsLst>
7783    <a:path path="circle"><a:fillToRect l="50000" t="50000" r="50000" b="50000"/></a:path>
7784  </a:gradFill>
7785</p:spPr>"#;
7786        let sp = parse_sppr(xml).expect("parse ok");
7787        match &sp.fill {
7788            Fill::Gradient(grad) => {
7789                assert_eq!(grad.stops.len(), 3, "应有 3 个渐变光轨");
7790                assert_eq!(grad.stops[1].pos, 50000);
7791                // 验证路径渐变类型
7792                assert_eq!(grad.gradient_type, GradientType::Path(GradientPath::Circle));
7793            }
7794            other => panic!("fill 应为 Gradient,实际:{other:?}"),
7795        }
7796    }
7797
7798    /// 验证 `parse_sppr` 能解析**图案填充**(`<a:pattFill prst="..."><a:fgClr/>...<a:bgClr/>...</a:pattFill>`)。
7799    #[test]
7800    fn parse_sppr_patt_fill() {
7801        let xml = r#"<p:spPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
7802                    xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
7803  <a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
7804  <a:pattFill prst="pct5">
7805    <a:fgClr><a:srgbClr val="FF0000"/></a:fgClr>
7806    <a:bgClr><a:srgbClr val="FFFFFF"/></a:bgClr>
7807  </a:pattFill>
7808</p:spPr>"#;
7809        let sp = parse_sppr(xml).expect("parse ok");
7810        match &sp.fill {
7811            Fill::Pattern(patt) => {
7812                assert_eq!(patt.prst, "pct5", "预置图案应为 pct5");
7813                // 前景色:红色
7814                assert!(
7815                    matches!(patt.fg_color, Color::RGB(c) if c.0 == 0xFF && c.1 == 0x00 && c.2 == 0x00)
7816                );
7817                // 背景色:白色
7818                assert!(
7819                    matches!(patt.bg_color, Color::RGB(c) if c.0 == 0xFF && c.1 == 0xFF && c.2 == 0xFF)
7820                );
7821            }
7822            other => panic!("fill 应为 Pattern,实际:{other:?}"),
7823        }
7824    }
7825
7826    /// 验证 `parse_sppr` 能解析**图片填充(blipFill)**的 stretch 模式(TODO-003/048)。
7827    ///
7828    /// 这是 TODO-003/048 的核心测试:确保 `<a:blip r:embed="rIdN"/>` + `<a:stretch>`
7829    /// 被正确解析为 `Fill::Blip { rid, mode: Stretch }`。
7830    #[test]
7831    fn parse_sppr_blip_fill_stretch() {
7832        let xml = r#"<p:spPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
7833                    xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
7834                    xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
7835  <a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
7836  <a:blipFill>
7837    <a:blip r:embed="rId1"/>
7838    <a:stretch><a:fillRect/></a:stretch>
7839  </a:blipFill>
7840</p:spPr>"#;
7841        let sp = parse_sppr(xml).expect("parse ok");
7842        match &sp.fill {
7843            Fill::Blip { rid, mode } => {
7844                assert_eq!(rid, "rId1", "rid 应为 rId1");
7845                assert!(
7846                    matches!(mode, crate::oxml::sppr::BlipFillMode::Stretch),
7847                    "mode 应为 Stretch,实际:{:?}",
7848                    mode
7849                );
7850            }
7851            other => panic!("fill 应为 Blip,实际:{other:?}"),
7852        }
7853    }
7854
7855    /// 验证 `parse_sppr` 能解析**图片填充(blipFill)**的 tile 模式 + 自闭合 blip。
7856    #[test]
7857    fn parse_sppr_blip_fill_tile() {
7858        let xml = r#"<p:spPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
7859                    xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
7860                    xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
7861  <a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
7862  <a:blipFill>
7863    <a:blip r:embed="rIdImg7"/>
7864    <a:tile tx="100" ty="200" sx="50000" sy="50000" flip="x" algn="ctr"/>
7865  </a:blipFill>
7866</p:spPr>"#;
7867        let sp = parse_sppr(xml).expect("parse ok");
7868        match &sp.fill {
7869            Fill::Blip { rid, mode } => {
7870                assert_eq!(rid, "rIdImg7", "rid 应为 rIdImg7");
7871                match mode {
7872                    crate::oxml::sppr::BlipFillMode::Tile {
7873                        tx,
7874                        ty,
7875                        sx,
7876                        sy,
7877                        flip,
7878                        algn,
7879                    } => {
7880                        assert_eq!(*tx, Some(100), "tx 应为 100");
7881                        assert_eq!(*ty, Some(200), "ty 应为 200");
7882                        assert_eq!(*sx, Some(50000), "sx 应为 50000");
7883                        assert_eq!(*sy, Some(50000), "sy 应为 50000");
7884                        assert_eq!(flip.as_deref(), Some("x"), "flip 应为 x");
7885                        assert_eq!(algn.as_deref(), Some("ctr"), "algn 应为 ctr");
7886                    }
7887                    other => panic!("mode 应为 Tile,实际:{other:?}"),
7888                }
7889            }
7890            other => panic!("fill 应为 Blip,实际:{other:?}"),
7891        }
7892    }
7893
7894    /// 验证 `parse_sppr` 能解析**自闭合 blipFill 子元素**(`<a:blip/>` + `<a:stretch/>` 均为 Empty)。
7895    #[test]
7896    fn parse_sppr_blip_fill_self_closing() {
7897        let xml = r#"<p:spPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
7898                    xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
7899                    xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
7900  <a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
7901  <a:blipFill>
7902    <a:blip r:embed="rId2"/>
7903    <a:stretch/>
7904  </a:blipFill>
7905</p:spPr>"#;
7906        let sp = parse_sppr(xml).expect("parse ok");
7907        match &sp.fill {
7908            Fill::Blip { rid, mode } => {
7909                assert_eq!(rid, "rId2");
7910                assert!(matches!(mode, crate::oxml::sppr::BlipFillMode::Stretch));
7911            }
7912            other => panic!("fill 应为 Blip,实际:{other:?}"),
7913        }
7914    }
7915
7916    /// 验证 `parse_sppr` 能解析**自闭合的渐变填充**(`<a:lin/>` 为 Empty 事件)。
7917    ///
7918    /// 某些 PPTX 生成器会把 `<a:lin ang="..." scaled="..."/>` 写成自闭合标签,
7919    /// 本测试确保 Empty 分支也能正确提取角度。
7920    #[test]
7921    fn parse_sppr_grad_fill_self_closing_lin() {
7922        let xml = r#"<p:spPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
7923                    xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
7924  <a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
7925  <a:gradFill>
7926    <a:gsLst>
7927      <a:gs pos="0"><a:srgbClr val="FF0000"/></a:gs>
7928      <a:gs pos="100000"><a:srgbClr val="0000FF"/></a:gs>
7929    </a:gsLst>
7930    <a:lin ang="2700000" scaled="0"/>
7931  </a:gradFill>
7932</p:spPr>"#;
7933        let sp = parse_sppr(xml).expect("parse ok");
7934        match &sp.fill {
7935            Fill::Gradient(grad) => {
7936                assert_eq!(grad.gradient_type, GradientType::Linear(2700000));
7937                assert_eq!(grad.stops.len(), 2);
7938            }
7939            other => panic!("fill 应为 Gradient,实际:{other:?}"),
7940        }
7941    }
7942
7943    /// 验证 `parse_ln` 能解析**箭头端点**(`<a:headEnd>` / `<a:tailEnd>`)。
7944    ///
7945    /// 这是 TODO-012 的核心测试:确保线条起止箭头的 type/w/len 属性全部正确提取。
7946    #[test]
7947    fn parse_sppr_ln_with_arrow_heads() {
7948        let xml = r#"<p:spPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
7949                    xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
7950  <a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
7951  <a:ln w="12700">
7952    <a:solidFill><a:srgbClr val="000000"/></a:solidFill>
7953    <a:headEnd type="triangle" w="lg" len="med"/>
7954    <a:tailEnd type="stealth" w="sm" len="lg"/>
7955  </a:ln>
7956</p:spPr>"#;
7957        let sp = parse_sppr(xml).expect("parse ok");
7958        let ln = sp.line.expect("应有 line");
7959        // headEnd
7960        let head = ln.head_end.expect("应有 headEnd");
7961        assert_eq!(head.arrow_type, ArrowType::Triangle);
7962        assert_eq!(head.width, ArrowSize::Large);
7963        assert_eq!(head.length, ArrowSize::Medium);
7964        // tailEnd
7965        let tail = ln.tail_end.expect("应有 tailEnd");
7966        assert_eq!(tail.arrow_type, ArrowType::Stealth);
7967        assert_eq!(tail.width, ArrowSize::Small);
7968        assert_eq!(tail.length, ArrowSize::Large);
7969    }
7970
7971    /// 验证 `parse_ln` 能解析**连接类型**(`<a:round>` / `<a:miter>` / `<a:bevel>`)。
7972    #[test]
7973    fn parse_sppr_ln_with_join() {
7974        // round 连接
7975        let xml = r#"<p:spPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
7976                    xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
7977  <a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
7978  <a:ln><a:solidFill><a:srgbClr val="000000"/></a:solidFill><a:round/></a:ln>
7979</p:spPr>"#;
7980        let sp = parse_sppr(xml).expect("parse ok");
7981        let ln = sp.line.expect("应有 line");
7982        assert_eq!(ln.join, Some(LineJoin::Round));
7983
7984        // miter 连接(带 lim 属性)
7985        let xml = r#"<p:spPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
7986                    xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
7987  <a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
7988  <a:ln><a:solidFill><a:srgbClr val="000000"/></a:solidFill><a:miter lim="600000"/></a:ln>
7989</p:spPr>"#;
7990        let sp = parse_sppr(xml).expect("parse ok");
7991        let ln = sp.line.expect("应有 line");
7992        assert_eq!(ln.join, Some(LineJoin::Miter(600000)));
7993
7994        // bevel 连接
7995        let xml = r#"<p:spPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
7996                    xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
7997  <a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
7998  <a:ln><a:solidFill><a:srgbClr val="000000"/></a:solidFill><a:bevel/></a:ln>
7999</p:spPr>"#;
8000        let sp = parse_sppr(xml).expect("parse ok");
8001        let ln = sp.line.expect("应有 line");
8002        assert_eq!(ln.join, Some(LineJoin::Bevel));
8003    }
8004
8005    /// 验证 `parse_ln` 能解析**线条渐变填充**(`<a:gradFill>` 在 `<a:ln>` 内)。
8006    #[test]
8007    fn parse_sppr_ln_with_grad_fill() {
8008        let xml = r#"<p:spPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
8009                    xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
8010  <a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
8011  <a:ln w="12700">
8012    <a:gradFill>
8013      <a:gsLst>
8014        <a:gs pos="0"><a:srgbClr val="FF0000"/></a:gs>
8015        <a:gs pos="100000"><a:srgbClr val="00FF00"/></a:gs>
8016      </a:gsLst>
8017      <a:lin ang="0" scaled="1"/>
8018    </a:gradFill>
8019  </a:ln>
8020</p:spPr>"#;
8021        let sp = parse_sppr(xml).expect("parse ok");
8022        let ln = sp.line.expect("应有 line");
8023        match &ln.fill {
8024            Fill::Gradient(grad) => {
8025                assert_eq!(grad.stops.len(), 2);
8026                assert_eq!(grad.gradient_type, GradientType::Linear(0));
8027            }
8028            other => panic!("line fill 应为 Gradient,实际:{other:?}"),
8029        }
8030    }
8031
8032    /// 验证 `parse_ln` 能解析**线条图案填充**(`<a:pattFill>` 在 `<a:ln>` 内)。
8033    #[test]
8034    fn parse_sppr_ln_with_patt_fill() {
8035        let xml = r#"<p:spPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
8036                    xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
8037  <a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
8038  <a:ln w="12700">
8039    <a:pattFill prst="cross">
8040      <a:fgClr><a:srgbClr val="FF0000"/></a:fgClr>
8041      <a:bgClr><a:srgbClr val="FFFFFF"/></a:bgClr>
8042    </a:pattFill>
8043  </a:ln>
8044</p:spPr>"#;
8045        let sp = parse_sppr(xml).expect("parse ok");
8046        let ln = sp.line.expect("应有 line");
8047        match &ln.fill {
8048            Fill::Pattern(patt) => {
8049                assert_eq!(patt.prst, "cross");
8050                assert!(matches!(patt.fg_color, Color::RGB(c) if c.0 == 0xFF));
8051                assert!(
8052                    matches!(patt.bg_color, Color::RGB(c) if c.0 == 0xFF && c.1 == 0xFF && c.2 == 0xFF)
8053                );
8054            }
8055            other => panic!("line fill 应为 Pattern,实际:{other:?}"),
8056        }
8057    }
8058
8059    /// 验证 `parse_sppr` 能解析 `<a:effectLst>` 中的外阴影(TODO-011)。
8060    #[test]
8061    fn parse_sppr_effect_lst_outer_shadow() {
8062        let xml = r#"<p:spPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
8063                    xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
8064  <a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
8065  <a:effectLst>
8066    <a:outerShdw blurRad="40000" dist="38100" dir="2700000" rotWithShape="0">
8067      <a:srgbClr val="000000"/>
8068    </a:outerShdw>
8069  </a:effectLst>
8070</p:spPr>"#;
8071        let sp = parse_sppr(xml).expect("parse ok");
8072        let effects = sp.effects.expect("应有 effects");
8073        let shdw = effects.outer_shadow.expect("应有 outer_shadow");
8074        assert_eq!(shdw.dir, 2_700_000);
8075        assert_eq!(shdw.dist, 38100);
8076        assert_eq!(shdw.blur_rad, 40000);
8077        assert_eq!(shdw.rot_with_shape, Some(false));
8078        assert!(matches!(shdw.color, Color::RGB(c) if c.0 == 0x00 && c.1 == 0x00 && c.2 == 0x00));
8079    }
8080
8081    /// 验证 `parse_sppr` 能解析发光和柔化边缘效果。
8082    #[test]
8083    fn parse_sppr_effect_lst_glow_and_soft_edge() {
8084        let xml = r#"<p:spPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
8085                    xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
8086  <a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
8087  <a:effectLst>
8088    <a:glow rad="50000">
8089      <a:srgbClr val="FF00FF"/>
8090    </a:glow>
8091    <a:softEdge rad="25000"/>
8092  </a:effectLst>
8093</p:spPr>"#;
8094        let sp = parse_sppr(xml).expect("parse ok");
8095        let effects = sp.effects.expect("应有 effects");
8096        let glow = effects.glow.expect("应有 glow");
8097        assert_eq!(glow.rad, 50000);
8098        assert!(matches!(glow.color, Color::RGB(c) if c.0 == 0xFF && c.1 == 0x00 && c.2 == 0xFF));
8099        let se = effects.soft_edge.expect("应有 soft_edge");
8100        assert_eq!(se.rad, 25000);
8101    }
8102
8103    /// 验证 `parse_sppr` 能解析反射效果(仅属性)。
8104    #[test]
8105    fn parse_sppr_effect_lst_reflection() {
8106        let xml = r#"<p:spPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
8107                    xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
8108  <a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
8109  <a:effectLst>
8110    <a:reflection blurRad="50000" stA="52000" stPos="0" endA="30000" endPos="50000" dist="38100" dir="5400000"/>
8111  </a:effectLst>
8112</p:spPr>"#;
8113        let sp = parse_sppr(xml).expect("parse ok");
8114        let effects = sp.effects.expect("应有 effects");
8115        let refl = effects.reflection.expect("应有 reflection");
8116        assert_eq!(refl.blur_rad, Some(50000));
8117        assert_eq!(refl.st_a, Some(52000));
8118        assert_eq!(refl.st_pos, Some(0));
8119        assert_eq!(refl.end_a, Some(30000));
8120        assert_eq!(refl.end_pos, Some(50000));
8121        assert_eq!(refl.dist, Some(38100));
8122        assert_eq!(refl.dir, Some(5_400_000));
8123    }
8124
8125    /// 验证 `Cell` 的 `gridSpan`/`rowSpan`/`hMerge`/`vMerge` 属性在 XML 往返中保持不变。
8126    ///
8127    /// 这是 TODO-029 的核心测试:确保合并单元格信息在序列化 → 解析后不丢失。
8128    #[test]
8129    fn round_trip_cell_merge_attributes() {
8130        let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
8131<p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
8132       xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
8133       xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
8134  <p:cSld><p:spTree>
8135    <p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
8136    <p:grpSpPr><a:xfrm/></p:grpSpPr>
8137    <p:graphicFrame>
8138      <p:nvGraphicFramePr><p:cNvPr id="2" name="MergedTable"/><p:cNvGraphicFramePr/><p:nvPr/></p:nvGraphicFramePr>
8139      <p:xfrm><a:off x="0" y="0"/><a:ext cx="6000000" cy="4000000"/></p:xfrm>
8140      <a:graphic>
8141        <a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/table">
8142          <a:tbl>
8143            <a:tblPr><a:tableStyleId>{5940675A-B579-460E-94D1-54222C63F5DA}</a:tableStyleId></a:tblPr>
8144            <a:tblGrid>
8145              <a:gridCol w="2000000"/>
8146              <a:gridCol w="2000000"/>
8147              <a:gridCol w="2000000"/>
8148            </a:tblGrid>
8149            <a:tr h="2000000">
8150              <a:tc gridSpan="3" rowSpan="2">
8151                <a:txBody><a:bodyPr/><a:p><a:r><a:t>Merged</a:t></a:r></a:p></a:txBody>
8152                <a:tcPr/>
8153              </a:tc>
8154            </a:tr>
8155            <a:tr h="2000000">
8156              <a:tc hMerge="1"><a:txBody><a:bodyPr/><a:p/></a:txBody><a:tcPr/></a:tc>
8157              <a:tc hMerge="1"><a:txBody><a:bodyPr/><a:p/></a:txBody><a:tcPr/></a:tc>
8158              <a:tc vMerge="1"><a:txBody><a:bodyPr/><a:p/></a:txBody><a:tcPr/></a:tc>
8159            </a:tr>
8160          </a:tbl>
8161        </a:graphicData>
8162      </a:graphic>
8163    </p:graphicFrame>
8164  </p:spTree></p:cSld>
8165</p:sld>"#;
8166        let sld = parse_sld(xml).expect("parse ok");
8167        // 找到 graphicFrame
8168        let frame = sld
8169            .shapes
8170            .iter()
8171            .find_map(|s| {
8172                if let OxmlSlideShape::GraphicFrame(g) = s {
8173                    Some(g)
8174                } else {
8175                    None
8176                }
8177            })
8178            .expect("应有 graphicFrame");
8179        match &frame.graphic {
8180            crate::oxml::shape::Graphic::Table(tbl) => {
8181                assert_eq!(tbl.rows.len(), 2, "应有 2 行");
8182                // 验证 tableStyleId 解析(TODO-030)
8183                assert_eq!(
8184                    tbl.table_style
8185                        .as_ref()
8186                        .expect("应有 table_style")
8187                        .style_id(),
8188                    "{5940675A-B579-460E-94D1-54222C63F5DA}"
8189                );
8190                // 第一行:合并源单元格
8191                let origin = &tbl.rows[0].cells[0];
8192                assert_eq!(origin.grid_span, 3, "gridSpan 应为 3");
8193                assert_eq!(origin.row_span, 2, "rowSpan 应为 2");
8194                assert!(!origin.h_merge, "合并源不应有 hMerge");
8195                assert!(!origin.v_merge, "合并源不应有 vMerge");
8196                // 第二行:被合并方
8197                let h1 = &tbl.rows[1].cells[0];
8198                assert!(h1.h_merge, "应有 hMerge");
8199                let v1 = &tbl.rows[1].cells[2];
8200                assert!(v1.v_merge, "应有 vMerge");
8201            }
8202            _ => panic!("期望 Table 变体"),
8203        }
8204    }
8205
8206    /// 验证表格样式(`<a:tableStyleId>`)在序列化 → 解析后保持不变。
8207    ///
8208    /// 这是 TODO-030 的核心 round-trip 测试。
8209    #[test]
8210    fn round_trip_table_style_id() {
8211        let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
8212<p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
8213       xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
8214       xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
8215  <p:cSld><p:spTree>
8216    <p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
8217    <p:grpSpPr><a:xfrm/></p:grpSpPr>
8218    <p:graphicFrame>
8219      <p:nvGraphicFramePr><p:cNvPr id="2" name="StyledTable"/><p:cNvGraphicFramePr/><p:nvPr/></p:nvGraphicFramePr>
8220      <p:xfrm><a:off x="0" y="0"/><a:ext cx="4000000" cy="2000000"/></p:xfrm>
8221      <a:graphic>
8222        <a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/table">
8223          <a:tbl>
8224            <a:tblPr><a:tableStyleId>{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}</a:tableStyleId></a:tblPr>
8225            <a:tblGrid><a:gridCol w="2000000"/><a:gridCol w="2000000"/></a:tblGrid>
8226            <a:tr h="2000000">
8227              <a:tc><a:txBody><a:bodyPr/><a:p/></a:txBody><a:tcPr/></a:tc>
8228              <a:tc><a:txBody><a:bodyPr/><a:p/></a:txBody><a:tcPr/></a:tc>
8229            </a:tr>
8230          </a:tbl>
8231        </a:graphicData>
8232      </a:graphic>
8233    </p:graphicFrame>
8234  </p:spTree></p:cSld>
8235</p:sld>"#;
8236        let sld = parse_sld(xml).expect("parse ok");
8237        let frame = sld
8238            .shapes
8239            .iter()
8240            .find_map(|s| {
8241                if let OxmlSlideShape::GraphicFrame(g) = s {
8242                    Some(g)
8243                } else {
8244                    None
8245                }
8246            })
8247            .expect("应有 graphicFrame");
8248        match &frame.graphic {
8249            crate::oxml::shape::Graphic::Table(tbl) => {
8250                // 验证 tableStyleId 解析
8251                let style = tbl.table_style.as_ref().expect("应有 table_style");
8252                assert_eq!(style.style_id(), "{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}");
8253                // 验证序列化后仍包含 tableStyleId
8254                let mut w = crate::oxml::writer::XmlWriter::new();
8255                tbl.write_xml(&mut w);
8256                let out = &w.buf;
8257                assert!(out.contains(
8258                    "<a:tableStyleId>{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}</a:tableStyleId>"
8259                ));
8260            }
8261            _ => panic!("期望 Table 变体"),
8262        }
8263    }
8264
8265    /// 验证表格无 `<a:tableStyleId>` 时 `table_style` 为 `None`。
8266    #[test]
8267    fn parse_table_without_style_id() {
8268        let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
8269<p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
8270       xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
8271       xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
8272  <p:cSld><p:spTree>
8273    <p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
8274    <p:grpSpPr><a:xfrm/></p:grpSpPr>
8275    <p:graphicFrame>
8276      <p:nvGraphicFramePr><p:cNvPr id="2" name="PlainTable"/><p:cNvGraphicFramePr/><p:nvPr/></p:nvGraphicFramePr>
8277      <p:xfrm><a:off x="0" y="0"/><a:ext cx="4000000" cy="2000000"/></p:xfrm>
8278      <a:graphic>
8279        <a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/table">
8280          <a:tbl>
8281            <a:tblPr/>
8282            <a:tblGrid><a:gridCol w="2000000"/><a:gridCol w="2000000"/></a:tblGrid>
8283            <a:tr h="2000000">
8284              <a:tc><a:txBody><a:bodyPr/><a:p/></a:txBody><a:tcPr/></a:tc>
8285              <a:tc><a:txBody><a:bodyPr/><a:p/></a:txBody><a:tcPr/></a:tc>
8286            </a:tr>
8287          </a:tbl>
8288        </a:graphicData>
8289      </a:graphic>
8290    </p:graphicFrame>
8291  </p:spTree></p:cSld>
8292</p:sld>"#;
8293        let sld = parse_sld(xml).expect("parse ok");
8294        let frame = sld
8295            .shapes
8296            .iter()
8297            .find_map(|s| {
8298                if let OxmlSlideShape::GraphicFrame(g) = s {
8299                    Some(g)
8300                } else {
8301                    None
8302                }
8303            })
8304            .expect("应有 graphicFrame");
8305        match &frame.graphic {
8306            crate::oxml::shape::Graphic::Table(tbl) => {
8307                assert!(
8308                    tbl.table_style.is_none(),
8309                    "无 tableStyleId 时 table_style 应为 None"
8310                );
8311            }
8312            _ => panic!("期望 Table 变体"),
8313        }
8314    }
8315
8316    /// 验证 `Cell` 的边框(lnL/lnR/lnT/lnB)、边距(marT/marL/marB/marR)、垂直对齐(anchor)
8317    /// 在 XML 往返中保持不变。
8318    ///
8319    /// 这是 TODO-013 的核心测试:确保单元格格式化信息在序列化 → 解析后不丢失。
8320    #[test]
8321    fn round_trip_cell_borders_margins_anchor() {
8322        let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
8323<p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
8324       xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
8325       xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
8326  <p:cSld><p:spTree>
8327    <p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
8328    <p:grpSpPr><a:xfrm/></p:grpSpPr>
8329    <p:graphicFrame>
8330      <p:nvGraphicFramePr><p:cNvPr id="2" name="FormattedTable"/><p:cNvGraphicFramePr/><p:nvPr/></p:nvGraphicFramePr>
8331      <p:xfrm><a:off x="0" y="0"/><a:ext cx="4000000" cy="2000000"/></p:xfrm>
8332      <a:graphic>
8333        <a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/table">
8334          <a:tbl>
8335            <a:tblPr><a:tableStyleId>{5940675A-B579-460E-94D1-54222C63F5DA}</a:tableStyleId></a:tblPr>
8336            <a:tblGrid><a:gridCol w="2000000"/><a:gridCol w="2000000"/></a:tblGrid>
8337            <a:tr h="2000000">
8338              <a:tc>
8339                <a:txBody><a:bodyPr/><a:p><a:r><a:t>A1</a:t></a:r></a:p></a:txBody>
8340                <a:tcPr marT="50000" marL="60000" marB="70000" marR="80000" anchor="ctr">
8341                  <a:lnL w="9525"><a:solidFill><a:srgbClr val="FF0000"/></a:solidFill></a:lnL>
8342                  <a:lnR w="9525"><a:solidFill><a:srgbClr val="00FF00"/></a:solidFill></a:lnR>
8343                  <a:lnT w="9525"><a:solidFill><a:srgbClr val="0000FF"/></a:solidFill></a:lnT>
8344                  <a:lnB w="9525"><a:noFill/></a:lnB>
8345                </a:tcPr>
8346              </a:tc>
8347              <a:tc>
8348                <a:txBody><a:bodyPr/><a:p><a:r><a:t>B1</a:t></a:r></a:p></a:txBody>
8349                <a:tcPr/>
8350              </a:tc>
8351            </a:tr>
8352          </a:tbl>
8353        </a:graphicData>
8354      </a:graphic>
8355    </p:graphicFrame>
8356  </p:spTree></p:cSld>
8357</p:sld>"#;
8358        let sld = parse_sld(xml).expect("parse ok");
8359        let frame = sld
8360            .shapes
8361            .iter()
8362            .find_map(|s| {
8363                if let OxmlSlideShape::GraphicFrame(g) = s {
8364                    Some(g)
8365                } else {
8366                    None
8367                }
8368            })
8369            .expect("应有 graphicFrame");
8370        let tbl = match &frame.graphic {
8371            crate::oxml::shape::Graphic::Table(t) => t,
8372            _ => panic!("期望 Table 变体"),
8373        };
8374        let cell = &tbl.rows[0].cells[0];
8375        // 边距
8376        assert_eq!(cell.margin.0.map(|m| m.value()), Some(50000), "marT");
8377        assert_eq!(cell.margin.1.map(|m| m.value()), Some(60000), "marL");
8378        assert_eq!(cell.margin.2.map(|m| m.value()), Some(70000), "marB");
8379        assert_eq!(cell.margin.3.map(|m| m.value()), Some(80000), "marR");
8380        // 垂直对齐
8381        assert_eq!(
8382            cell.anchor,
8383            crate::oxml::table::VerticalAnchor::Middle,
8384            "anchor=ctr"
8385        );
8386        // 边框
8387        let bl = cell.border_left.as_ref().expect("应有 lnL");
8388        assert_eq!(bl.width.value(), 9525);
8389        assert!(matches!(bl.color, Color::RGB(c) if c.0 == 0xFF && c.1 == 0x00 && c.2 == 0x00));
8390        let br = cell.border_right.as_ref().expect("应有 lnR");
8391        assert!(matches!(br.color, Color::RGB(c) if c.0 == 0x00 && c.1 == 0xFF && c.2 == 0x00));
8392        let bt = cell.border_top.as_ref().expect("应有 lnT");
8393        assert!(matches!(bt.color, Color::RGB(c) if c.0 == 0x00 && c.1 == 0x00 && c.2 == 0xFF));
8394        let bb = cell.border_bottom.as_ref().expect("应有 lnB");
8395        assert!(bb.no_fill, "lnB 应为 noFill");
8396    }
8397
8398    /// 验证 `<a:bodyPr>` 的 `numCol` / `spcCol` / `anchor` / `wrap` 属性解析。
8399    ///
8400    /// 这是 TODO-019 的 round-trip 测试。
8401    #[test]
8402    fn parse_txbody_multi_column() {
8403        let xml = r#"<p:txBody xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
8404                    xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
8405  <a:bodyPr numCol="3" spcCol="91440" anchor="ctr" wrap="square" lIns="91440" tIns="45720" rIns="91440" bIns="45720"/>
8406  <a:p><a:r><a:t>multi-col text</a:t></a:r></a:p>
8407</p:txBody>"#;
8408        let tb = parse_txbody(xml).expect("parse ok");
8409        let bp = tb.body_properties.as_ref().expect("应有 body_properties");
8410        assert_eq!(bp.num_cols, Some(3), "numCol");
8411        assert_eq!(bp.col_spacing.map(|v| v.value()), Some(91440), "spcCol");
8412        assert_eq!(bp.anchor, Some(MsoAnchor::Middle), "anchor=ctr");
8413        assert_eq!(bp.wrap, Some(TextWrapping::Square), "wrap=square");
8414        let insets = bp.insets.as_ref().expect("应有 insets");
8415        assert_eq!(insets.left.value(), 91440, "lIns");
8416        assert_eq!(insets.top.value(), 45720, "tIns");
8417        assert_eq!(insets.right.value(), 91440, "rIns");
8418        assert_eq!(insets.bottom.value(), 45720, "bIns");
8419        // 段落仍应正确解析
8420        assert_eq!(tb.paragraphs.len(), 1, "应有一个段落");
8421        assert_eq!(tb.paragraphs[0].runs.len(), 1, "应有一个 run");
8422        assert_eq!(tb.paragraphs[0].runs[0].text, "multi-col text");
8423    }
8424
8425    /// 验证自闭合 `<a:bodyPr/>` 的属性解析。
8426    ///
8427    /// 这是 TODO-019 的测试。
8428    #[test]
8429    fn parse_txbody_self_closing_body_pr() {
8430        let xml = r#"<p:txBody xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
8431                    xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
8432  <a:bodyPr numCol="2" spcCol="45720"/>
8433  <a:p/>
8434</p:txBody>"#;
8435        let tb = parse_txbody(xml).expect("parse ok");
8436        let bp = tb.body_properties.as_ref().expect("应有 body_properties");
8437        assert_eq!(bp.num_cols, Some(2), "numCol");
8438        assert_eq!(bp.col_spacing.map(|v| v.value()), Some(45720), "spcCol");
8439    }
8440
8441    /// 验证 `<a:bodyPr>` 含 `<a:spAutoFit/>` 子元素的解析。
8442    ///
8443    /// 这是 TODO-019 的测试(顺带覆盖 autoSize 子元素解析)。
8444    #[test]
8445    fn parse_txbody_with_sp_auto_fit() {
8446        let xml = r#"<p:txBody xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
8447                    xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
8448  <a:bodyPr numCol="1"><a:spAutoFit/></a:bodyPr>
8449  <a:p/>
8450</p:txBody>"#;
8451        let tb = parse_txbody(xml).expect("parse ok");
8452        let bp = tb.body_properties.as_ref().expect("应有 body_properties");
8453        assert_eq!(bp.num_cols, Some(1), "numCol=1");
8454        assert!(bp.sp_auto_fit, "应启用 spAutoFit");
8455        assert!(!bp.norm_autofit, "不应启用 normAutofit");
8456    }
8457
8458    /// 验证 `<a:buChar>` / `<a:buAutoNum>` / `<a:buNone>` 的详细属性解析。
8459    ///
8460    /// 这是 TODO-014 的 round-trip 测试。
8461    #[test]
8462    fn parse_paragraph_bullet_styles() {
8463        // buChar
8464        let xml = r#"<a:pPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
8465  <a:buChar char="•"/>
8466</a:pPr>"#;
8467        let ppr = parse_paragraph_properties(xml).expect("parse ok");
8468        assert!(ppr.bullet, "bullet 应为 true");
8469        match &ppr.bullet_style {
8470            Some(BulletStyle::Char { char }) => assert_eq!(char, "•"),
8471            other => panic!("期望 Char,实际: {other:?}"),
8472        }
8473
8474        // buAutoNum with startAt
8475        let xml = r#"<a:pPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
8476  <a:buAutoNum type="arabicPeriod" startAt="3"/>
8477</a:pPr>"#;
8478        let ppr = parse_paragraph_properties(xml).expect("parse ok");
8479        assert!(ppr.bullet);
8480        match &ppr.bullet_style {
8481            Some(BulletStyle::AutoNum {
8482                auto_num_type,
8483                start_at,
8484            }) => {
8485                assert_eq!(auto_num_type, "arabicPeriod");
8486                assert_eq!(*start_at, Some(3));
8487            }
8488            other => panic!("期望 AutoNum,实际: {other:?}"),
8489        }
8490
8491        // buAutoNum without startAt
8492        let xml = r#"<a:pPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
8493  <a:buAutoNum type="alphaLcParenR"/>
8494</a:pPr>"#;
8495        let ppr = parse_paragraph_properties(xml).expect("parse ok");
8496        match &ppr.bullet_style {
8497            Some(BulletStyle::AutoNum {
8498                auto_num_type,
8499                start_at,
8500            }) => {
8501                assert_eq!(auto_num_type, "alphaLcParenR");
8502                assert_eq!(*start_at, None);
8503            }
8504            other => panic!("期望 AutoNum,实际: {other:?}"),
8505        }
8506
8507        // buNone
8508        let xml = r#"<a:pPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
8509  <a:buNone/>
8510</a:pPr>"#;
8511        let ppr = parse_paragraph_properties(xml).expect("parse ok");
8512        assert!(!ppr.bullet, "bullet 应为 false");
8513        assert!(matches!(ppr.bullet_style, Some(BulletStyle::None)));
8514    }
8515
8516    /// 验证自闭合的 `<a:buChar/>` / `<a:buAutoNum/>` / `<a:buNone/>` 解析。
8517    ///
8518    /// 这是 TODO-014 的测试。
8519    #[test]
8520    fn parse_paragraph_bullet_styles_self_closing() {
8521        // 自闭合 buChar(无 char 属性)
8522        let xml = r#"<a:pPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
8523  <a:buChar char="▪"/>
8524</a:pPr>"#;
8525        let ppr = parse_paragraph_properties(xml).expect("parse ok");
8526        match &ppr.bullet_style {
8527            Some(BulletStyle::Char { char }) => assert_eq!(char, "▪"),
8528            other => panic!("期望 Char,实际: {other:?}"),
8529        }
8530
8531        // 自闭合 buAutoNum
8532        let xml = r#"<a:pPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
8533  <a:buAutoNum type="romanLcParenBoth" startAt="5"/>
8534</a:pPr>"#;
8535        let ppr = parse_paragraph_properties(xml).expect("parse ok");
8536        match &ppr.bullet_style {
8537            Some(BulletStyle::AutoNum {
8538                auto_num_type,
8539                start_at,
8540            }) => {
8541                assert_eq!(auto_num_type, "romanLcParenBoth");
8542                assert_eq!(*start_at, Some(5));
8543            }
8544            other => panic!("期望 AutoNum,实际: {other:?}"),
8545        }
8546    }
8547
8548    /// 验证 `<a:tabLst>` / `<a:tab>` 的解析。
8549    ///
8550    /// 这是 TODO-015 的 round-trip 测试。
8551    #[test]
8552    fn parse_paragraph_tab_stops() {
8553        let xml = r#"<a:pPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
8554  <a:tabLst>
8555    <a:tab pos="914400" algn="l"/>
8556    <a:tab pos="1828800" algn="r"/>
8557    <a:tab pos="2743200" algn="ctr"/>
8558    <a:tab pos="3657600" algn="dec"/>
8559  </a:tabLst>
8560</a:pPr>"#;
8561        let ppr = parse_paragraph_properties(xml).expect("parse ok");
8562        assert_eq!(ppr.tab_stops.len(), 4, "应有 4 个制表位");
8563        assert_eq!(ppr.tab_stops[0].pos.value(), 914400);
8564        assert_eq!(ppr.tab_stops[0].alignment, TabAlignment::Left);
8565        assert_eq!(ppr.tab_stops[1].pos.value(), 1828800);
8566        assert_eq!(ppr.tab_stops[1].alignment, TabAlignment::Right);
8567        assert_eq!(ppr.tab_stops[2].pos.value(), 2743200);
8568        assert_eq!(ppr.tab_stops[2].alignment, TabAlignment::Center);
8569        assert_eq!(ppr.tab_stops[3].pos.value(), 3657600);
8570        assert_eq!(ppr.tab_stops[3].alignment, TabAlignment::Decimal);
8571    }
8572
8573    /// 验证空 `<a:tabLst/>` 的解析。
8574    ///
8575    /// 这是 TODO-015 的测试。
8576    #[test]
8577    fn parse_paragraph_empty_tab_lst() {
8578        let xml = r#"<a:pPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
8579  <a:tabLst/>
8580</a:pPr>"#;
8581        let ppr = parse_paragraph_properties(xml).expect("parse ok");
8582        assert!(ppr.tab_stops.is_empty(), "空 tabLst 应无制表位");
8583    }
8584
8585    /// 验证 `<a:fld>` 字段元素的解析。
8586    ///
8587    /// 这是 TODO-016 的 round-trip 测试。
8588    #[test]
8589    fn parse_paragraph_field() {
8590        let xml = r#"<a:p xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
8591  <a:fld id="{12345678-ABCD-EF01-2345-678901234567}" type="slidenum">
8592    <a:rPr lang="en-US"/>
8593    <a:t>1</a:t>
8594  </a:fld>
8595</a:p>"#;
8596        let p = parse_paragraph(xml).expect("parse ok");
8597        assert_eq!(p.fields.len(), 1, "应有 1 个字段");
8598        let f = &p.fields[0];
8599        assert_eq!(f.id, "{12345678-ABCD-EF01-2345-678901234567}");
8600        assert_eq!(f.field_type, FieldType::SlideNumber);
8601        assert_eq!(f.text, "1");
8602    }
8603
8604    /// 验证含多个字段和 Run 的段落解析。
8605    ///
8606    /// 这是 TODO-016 的测试。
8607    #[test]
8608    fn parse_paragraph_mixed_runs_and_fields() {
8609        let xml = r#"<a:p xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
8610  <a:r><a:t>Page </a:t></a:r>
8611  <a:fld id="{AAA}" type="slidenum"><a:t>1</a:t></a:fld>
8612  <a:fld id="{BBB}" type="datetime"><a:t>1/1/2024</a:t></a:fld>
8613</a:p>"#;
8614        let p = parse_paragraph(xml).expect("parse ok");
8615        assert_eq!(p.runs.len(), 1, "应有 1 个 run");
8616        // 注意:trim_text(true) 会去掉尾部空格
8617        assert_eq!(p.runs[0].text, "Page");
8618        assert_eq!(p.fields.len(), 2, "应有 2 个字段");
8619        assert_eq!(p.fields[0].field_type, FieldType::SlideNumber);
8620        assert_eq!(p.fields[0].text, "1");
8621        assert_eq!(p.fields[1].field_type, FieldType::DateTime);
8622        assert_eq!(p.fields[1].text, "1/1/2024");
8623    }
8624
8625    /// 验证自定义字段类型的解析。
8626    ///
8627    /// 这是 TODO-016 的测试。
8628    #[test]
8629    fn parse_paragraph_custom_field_type() {
8630        let xml = r#"<a:p xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
8631  <a:fld id="{CCC}" type="customField"><a:t>custom</a:t></a:fld>
8632</a:p>"#;
8633        let p = parse_paragraph(xml).expect("parse ok");
8634        assert_eq!(p.fields.len(), 1);
8635        assert_eq!(
8636            p.fields[0].field_type,
8637            FieldType::Custom("customField".to_string())
8638        );
8639        assert_eq!(p.fields[0].text, "custom");
8640    }
8641
8642    /// 验证 `<a:hlinkClick>` 自闭合超链接的解析(r:id + tooltip)。
8643    ///
8644    /// 这是 TODO-026 的测试。
8645    #[test]
8646    fn parse_run_properties_hlink_click_self_closing() {
8647        let xml = r#"<a:rPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" lang="en-US">
8648  <a:hlinkClick r:id="rId3" tooltip="点击访问"/>
8649</a:rPr>"#;
8650        let rp = parse_run_properties(xml).expect("parse ok");
8651        let hl = rp.hlink_click.expect("hlink_click 应存在");
8652        assert_eq!(hl.rid.as_deref(), Some("rId3"));
8653        assert_eq!(hl.tooltip.as_deref(), Some("点击访问"));
8654        assert!(hl.action.is_none());
8655        assert!(!hl.invalid);
8656    }
8657
8658    /// 验证 `<a:hlinkClick>` 带动作(跳转幻灯片)的解析。
8659    ///
8660    /// 这是 TODO-026 的测试。
8661    #[test]
8662    fn parse_run_properties_hlink_click_action() {
8663        let xml = r#"<a:rPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
8664  <a:hlinkClick action="ppaction://hlinksldjump"/>
8665</a:rPr>"#;
8666        let rp = parse_run_properties(xml).expect("parse ok");
8667        let hl = rp.hlink_click.expect("hlink_click 应存在");
8668        assert_eq!(hl.action.as_deref(), Some("ppaction://hlinksldjump"));
8669        assert!(hl.rid.is_none());
8670    }
8671
8672    /// 验证 `<a:hlinkHover>` 的解析。
8673    ///
8674    /// 这是 TODO-026 的测试。
8675    #[test]
8676    fn parse_run_properties_hlink_hover() {
8677        let xml = r#"<a:rPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
8678  <a:hlinkHover r:id="rId5" tooltip="悬停提示"/>
8679</a:rPr>"#;
8680        let rp = parse_run_properties(xml).expect("parse ok");
8681        let hl = rp.hlink_hover.expect("hlink_hover 应存在");
8682        assert_eq!(hl.rid.as_deref(), Some("rId5"));
8683        assert_eq!(hl.tooltip.as_deref(), Some("悬停提示"));
8684    }
8685
8686    /// 验证同时存在 hlinkClick 和 hlinkHover 的解析。
8687    ///
8688    /// 这是 TODO-026 的测试。
8689    #[test]
8690    fn parse_run_properties_both_hlinks() {
8691        let xml = r#"<a:rPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
8692  <a:hlinkClick r:id="rId1" tooltip="click"/>
8693  <a:hlinkHover r:id="rId2" tooltip="hover"/>
8694</a:rPr>"#;
8695        let rp = parse_run_properties(xml).expect("parse ok");
8696        assert_eq!(
8697            rp.hlink_click.as_ref().unwrap().rid.as_deref(),
8698            Some("rId1")
8699        );
8700        assert_eq!(
8701            rp.hlink_hover.as_ref().unwrap().rid.as_deref(),
8702            Some("rId2")
8703        );
8704    }
8705
8706    /// 验证空属性 `<a:hlinkClick/>` 标记为 invalid(继承场景)。
8707    ///
8708    /// 这是 TODO-026 的测试。
8709    #[test]
8710    fn parse_run_properties_hlink_click_empty() {
8711        let xml = r#"<a:rPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
8712  <a:hlinkClick/>
8713</a:rPr>"#;
8714        let rp = parse_run_properties(xml).expect("parse ok");
8715        let hl = rp.hlink_click.expect("hlink_click 应存在");
8716        assert!(hl.invalid);
8717        assert!(hl.rid.is_none());
8718    }
8719
8720    /// 验证 `<a:spLocks>` 自闭合形状锁定的解析。
8721    ///
8722    /// 这是 TODO-027 的测试。
8723    #[test]
8724    fn parse_sp_with_locks() {
8725        let xml = r#"<p:sp xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
8726  <p:nvSpPr>
8727    <p:cNvPr id="2" name="Locked"/>
8728    <p:cNvSpPr>
8729      <a:spLocks noGrp="1" noSelect="1" noResize="1"/>
8730    </p:cNvSpPr>
8731    <p:nvPr/>
8732  </p:nvSpPr>
8733  <p:spPr/>
8734  <p:txBody><a:bodyPr/><a:p/></p:txBody>
8735</p:sp>"#;
8736        let sp = parse_sp(xml).expect("parse ok");
8737        let locks = sp.locks.as_ref().expect("locks 应存在");
8738        assert!(locks.no_grp);
8739        assert!(locks.no_select);
8740        assert!(locks.no_resize);
8741        assert!(!locks.no_move);
8742        assert!(!locks.no_rot);
8743    }
8744
8745    /// 验证 `<a:spLocks>` 与 `txBox` 同时存在的解析。
8746    ///
8747    /// 这是 TODO-027 的测试。
8748    #[test]
8749    fn parse_sp_locks_with_txbox() {
8750        let xml = r#"<p:sp xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
8751  <p:nvSpPr>
8752    <p:cNvPr id="3" name="TB"/>
8753    <p:cNvSpPr txBox="1">
8754      <a:spLocks noChangeAspect="1"/>
8755    </p:cNvSpPr>
8756    <p:nvPr/>
8757  </p:nvSpPr>
8758  <p:spPr/>
8759  <p:txBody><a:bodyPr/><a:p/></p:txBody>
8760</p:sp>"#;
8761        let sp = parse_sp(xml).expect("parse ok");
8762        assert!(sp.c_nv_sp_pr_tx_box, "txBox 应为 true");
8763        let locks = sp.locks.as_ref().expect("locks 应存在");
8764        assert!(locks.no_change_aspect);
8765        assert!(!locks.no_grp);
8766    }
8767
8768    /// 验证 `<p:style>` 主题样式引用的解析。
8769    ///
8770    /// 这是 TODO-006 的测试。
8771    #[test]
8772    fn parse_sp_with_style() {
8773        let xml = r#"<p:sp xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
8774  <p:nvSpPr>
8775    <p:cNvPr id="4" name="Styled"/>
8776    <p:cNvSpPr/>
8777    <p:nvPr/>
8778  </p:nvSpPr>
8779  <p:spPr/>
8780  <p:style>
8781    <a:lnRef idx="1"><a:schemeClr val="accent1"/></a:lnRef>
8782    <a:fillRef idx="2"><a:schemeClr val="accent2"/></a:fillRef>
8783    <a:effectRef idx="0"><a:schemeClr val="accent3"/></a:effectRef>
8784    <a:fontRef idx="minor"><a:schemeClr val="tx1"/></a:fontRef>
8785  </p:style>
8786  <p:txBody><a:bodyPr/><a:p/></p:txBody>
8787</p:sp>"#;
8788        let sp = parse_sp(xml).expect("parse ok");
8789        let style = sp.style.as_ref().expect("style 应存在");
8790        let ln = style.line_ref.as_ref().expect("line_ref 应存在");
8791        assert_eq!(ln.idx.as_deref(), Some("1"));
8792        assert_eq!(ln.scheme_color.as_deref(), Some("accent1"));
8793        let fill = style.fill_ref.as_ref().expect("fill_ref 应存在");
8794        assert_eq!(fill.idx.as_deref(), Some("2"));
8795        assert_eq!(fill.scheme_color.as_deref(), Some("accent2"));
8796        let eff = style.effect_ref.as_ref().expect("effect_ref 应存在");
8797        assert_eq!(eff.idx.as_deref(), Some("0"));
8798        let font = style.font_ref.as_ref().expect("font_ref 应存在");
8799        assert_eq!(font.idx.as_deref(), Some("minor"));
8800        assert_eq!(font.scheme_color.as_deref(), Some("tx1"));
8801    }
8802
8803    /// 验证 `<p:style>` 自闭合子元素的解析。
8804    ///
8805    /// 这是 TODO-006 的测试。
8806    #[test]
8807    fn parse_sp_style_self_closing_refs() {
8808        let xml = r#"<p:style xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
8809  <a:lnRef idx="3"/>
8810  <a:fillRef idx="1"/>
8811</p:style>"#;
8812        let style = parse_shape_style(xml).expect("parse ok");
8813        assert_eq!(style.line_ref.as_ref().unwrap().idx.as_deref(), Some("3"));
8814        assert!(style.line_ref.as_ref().unwrap().scheme_color.is_none());
8815        assert_eq!(style.fill_ref.as_ref().unwrap().idx.as_deref(), Some("1"));
8816        assert!(style.effect_ref.is_none());
8817        assert!(style.font_ref.is_none());
8818    }
8819
8820    // ===== TODO-020:幻灯片过渡测试 =====
8821
8822    /// 验证 `<p:transition>` 的 fade 类型解析。
8823    #[test]
8824    fn parse_transition_fade() {
8825        let xml = r#"<p:transition xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" spd="slow" advClick="0" advTm="5000">
8826  <p:fade thruBlk="1"/>
8827</p:transition>"#;
8828        let tr = parse_transition(xml).expect("parse ok");
8829        assert_eq!(tr.speed, TransitionSpeed::Slow);
8830        assert!(!tr.advance_click);
8831        assert_eq!(tr.advance_after_ms, Some(5000));
8832        match &tr.transition_type {
8833            TransitionType::Fade { thru_blk } => assert!(*thru_blk),
8834            other => panic!("expected Fade, got {:?}", other),
8835        }
8836    }
8837
8838    /// 验证 `<p:transition>` 的 push 类型解析(带方向)。
8839    #[test]
8840    fn parse_transition_push() {
8841        let xml = r#"<p:transition xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" spd="fast">
8842  <p:push dir="l"/>
8843</p:transition>"#;
8844        let tr = parse_transition(xml).expect("parse ok");
8845        assert_eq!(tr.speed, TransitionSpeed::Fast);
8846        // 默认 advClick=true
8847        assert!(tr.advance_click);
8848        assert!(tr.advance_after_ms.is_none());
8849        match &tr.transition_type {
8850            TransitionType::Push { dir } => assert_eq!(*dir, TransitionDirection::Left),
8851            other => panic!("expected Push, got {:?}", other),
8852        }
8853    }
8854
8855    /// 验证 `<p:transition>` 的 morph 类型解析。
8856    #[test]
8857    fn parse_transition_morph() {
8858        let xml = r#"<p:transition xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
8859  <p:morph option="byChar"/>
8860</p:transition>"#;
8861        let tr = parse_transition(xml).expect("parse ok");
8862        match &tr.transition_type {
8863            TransitionType::Morph { option } => assert_eq!(*option, MorphOption::ByChar),
8864            other => panic!("expected Morph, got {:?}", other),
8865        }
8866    }
8867
8868    /// 验证 `<p:transition>` 的 split 类型解析(带方向和方向)。
8869    #[test]
8870    fn parse_transition_split() {
8871        let xml = r#"<p:transition xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
8872  <p:split orient="vert" dir="d"/>
8873</p:transition>"#;
8874        let tr = parse_transition(xml).expect("parse ok");
8875        match &tr.transition_type {
8876            TransitionType::Split { orient, dir } => {
8877                assert_eq!(*orient, SplitOrientation::Vertical);
8878                assert_eq!(*dir, TransitionDirection::Down);
8879            }
8880            other => panic!("expected Split, got {:?}", other),
8881        }
8882    }
8883
8884    /// 验证 `<p:transition>` 自闭合(无子元素)解析。
8885    #[test]
8886    fn parse_transition_self_closing() {
8887        let xml = r#"<p:transition xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" spd="med" advClick="1"/>"#;
8888        let tr = parse_transition(xml).expect("parse ok");
8889        assert_eq!(tr.speed, TransitionSpeed::Medium);
8890        assert!(tr.advance_click);
8891        // 无子元素时,transition_type 为 None
8892        assert_eq!(tr.transition_type, TransitionType::None);
8893    }
8894
8895    /// 验证 `parse_sld` 能正确解析包含 `<p:transition>` 的 slide。
8896    #[test]
8897    fn parse_sld_with_transition() {
8898        let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
8899<p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
8900       xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
8901       xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
8902  <p:cSld>
8903    <p:spTree>
8904      <p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
8905      <p:grpSpPr><a:xfrm/></p:grpSpPr>
8906    </p:spTree>
8907  </p:cSld>
8908  <p:clrMapOvr/>
8909  <p:transition spd="slow" advTm="3000">
8910    <p:fade/>
8911  </p:transition>
8912</p:sld>"#;
8913        let sld = parse_sld(xml).expect("parse ok");
8914        let tr = sld.transition.expect("transition should exist");
8915        assert_eq!(tr.speed, TransitionSpeed::Slow);
8916        assert_eq!(tr.advance_after_ms, Some(3000));
8917        match &tr.transition_type {
8918            TransitionType::Fade { thru_blk } => assert!(!*thru_blk),
8919            other => panic!("expected Fade, got {:?}", other),
8920        }
8921    }
8922
8923    /// 验证 `Transition::write_xml` 序列化 + `parse_transition` 反序列化的 round-trip。
8924    #[test]
8925    fn transition_round_trip() {
8926        use crate::oxml::slide::Transition;
8927        use crate::oxml::writer::XmlWriter;
8928
8929        let original = Transition {
8930            speed: TransitionSpeed::Fast,
8931            advance_click: false,
8932            advance_after_ms: Some(8000),
8933            transition_type: TransitionType::Push {
8934                dir: TransitionDirection::Up,
8935            },
8936        };
8937        // 序列化
8938        let mut w = XmlWriter::new();
8939        original.write_xml(&mut w);
8940        let xml = w.buf.clone();
8941        // 反序列化
8942        let parsed = parse_transition(&xml).expect("parse ok");
8943        assert_eq!(parsed.speed, original.speed);
8944        assert_eq!(parsed.advance_click, original.advance_click);
8945        assert_eq!(parsed.advance_after_ms, original.advance_after_ms);
8946        assert_eq!(parsed.transition_type, original.transition_type);
8947    }
8948
8949    // ===== TODO-024:自定义几何(custGeom)测试 =====
8950
8951    /// 验证 `parse_custom_geometry` 能解析基本的 moveTo/lnTo/close 路径。
8952    #[test]
8953    fn parse_custom_geometry_basic() {
8954        let xml = r#"<a:custGeom>
8955<a:avLst/>
8956<a:rect l="0" t="0" r="100" b="100"/>
8957<a:pathLst>
8958<a:path w="100" h="100" fill="norm" stroke="norm">
8959<a:moveTo><a:pt x="0" y="0"/></a:moveTo>
8960<a:lnTo><a:pt x="100" y="0"/></a:lnTo>
8961<a:lnTo><a:pt x="50" y="100"/></a:lnTo>
8962<a:close/>
8963</a:path>
8964</a:pathLst>
8965</a:custGeom>"#;
8966        let geom = parse_custom_geometry(xml).expect("parse ok");
8967        assert_eq!(
8968            geom.rect,
8969            Some(GeomRect {
8970                l: "0".to_string(),
8971                t: "0".to_string(),
8972                r: "100".to_string(),
8973                b: "100".to_string(),
8974            })
8975        );
8976        assert_eq!(geom.path_list.len(), 1);
8977        let p = &geom.path_list[0];
8978        assert_eq!(p.width, 100);
8979        assert_eq!(p.height, 100);
8980        assert_eq!(p.fill.as_deref(), Some("norm"));
8981        assert_eq!(p.stroke.as_deref(), Some("norm"));
8982        assert_eq!(p.segments.len(), 4);
8983        // 验证各段
8984        match &p.segments[0] {
8985            PathSegment::MoveTo { x, y } => {
8986                assert_eq!(*x, 0);
8987                assert_eq!(*y, 0);
8988            }
8989            other => panic!("expected MoveTo, got {:?}", other),
8990        }
8991        match &p.segments[1] {
8992            PathSegment::LineTo { x, y } => {
8993                assert_eq!(*x, 100);
8994                assert_eq!(*y, 0);
8995            }
8996            other => panic!("expected LineTo, got {:?}", other),
8997        }
8998        match &p.segments[2] {
8999            PathSegment::LineTo { x, y } => {
9000                assert_eq!(*x, 50);
9001                assert_eq!(*y, 100);
9002            }
9003            other => panic!("expected LineTo, got {:?}", other),
9004        }
9005        match &p.segments[3] {
9006            PathSegment::Close => {}
9007            other => panic!("expected Close, got {:?}", other),
9008        }
9009    }
9010
9011    /// 验证 `parse_custom_geometry` 能解析贝塞尔曲线段。
9012    #[test]
9013    fn parse_custom_geometry_with_bez() {
9014        let xml = r#"<a:custGeom>
9015<a:avLst/>
9016<a:pathLst>
9017<a:path w="200" h="200">
9018<a:moveTo><a:pt x="10" y="10"/></a:moveTo>
9019<a:cubicBezTo>
9020<a:pt x="50" y="0"/>
9021<a:pt x="150" y="0"/>
9022<a:pt x="190" y="10"/>
9023</a:cubicBezTo>
9024<a:quadBezTo>
9025<a:pt x="190" y="100"/>
9026<a:pt x="100" y="190"/>
9027</a:quadBezTo>
9028</a:path>
9029</a:pathLst>
9030</a:custGeom>"#;
9031        let geom = parse_custom_geometry(xml).expect("parse ok");
9032        assert_eq!(geom.path_list.len(), 1);
9033        let p = &geom.path_list[0];
9034        assert_eq!(p.segments.len(), 3);
9035        match &p.segments[1] {
9036            PathSegment::CubicBezTo {
9037                x1,
9038                y1,
9039                x2,
9040                y2,
9041                x3,
9042                y3,
9043            } => {
9044                assert_eq!(*x1, 50);
9045                assert_eq!(*y1, 0);
9046                assert_eq!(*x2, 150);
9047                assert_eq!(*y2, 0);
9048                assert_eq!(*x3, 190);
9049                assert_eq!(*y3, 10);
9050            }
9051            other => panic!("expected CubicBezTo, got {:?}", other),
9052        }
9053        match &p.segments[2] {
9054            PathSegment::QuadBezTo { x1, y1, x2, y2 } => {
9055                assert_eq!(*x1, 190);
9056                assert_eq!(*y1, 100);
9057                assert_eq!(*x2, 100);
9058                assert_eq!(*y2, 190);
9059            }
9060            other => panic!("expected QuadBezTo, got {:?}", other),
9061        }
9062    }
9063
9064    /// 验证 `parse_custom_geometry` 能解析 arcTo 段。
9065    #[test]
9066    fn parse_custom_geometry_with_arc() {
9067        let xml = r#"<a:custGeom>
9068<a:avLst/>
9069<a:pathLst>
9070<a:path w="100" h="100">
9071<a:moveTo><a:pt x="0" y="50"/></a:moveTo>
9072<a:arcTo wR="50" hR="50" stAng="0" swAng="5400000"/>
9073<a:close/>
9074</a:path>
9075</a:pathLst>
9076</a:custGeom>"#;
9077        let geom = parse_custom_geometry(xml).expect("parse ok");
9078        let p = &geom.path_list[0];
9079        assert_eq!(p.segments.len(), 3);
9080        match &p.segments[1] {
9081            PathSegment::ArcTo {
9082                w_r,
9083                h_r,
9084                st_ang,
9085                sw_ang,
9086            } => {
9087                assert_eq!(*w_r, 50);
9088                assert_eq!(*h_r, 50);
9089                assert_eq!(*st_ang, 0);
9090                assert_eq!(*sw_ang, 5400000);
9091            }
9092            other => panic!("expected ArcTo, got {:?}", other),
9093        }
9094    }
9095
9096    /// 验证 `parse_custom_geometry` 能解析 fill/stroke 文本元素。
9097    #[test]
9098    fn parse_custom_geometry_fill_stroke() {
9099        let xml = r#"<a:custGeom>
9100<a:avLst/>
9101<a:fill>norm</a:fill>
9102<a:stroke>none</a:stroke>
9103<a:pathLst>
9104<a:path w="10" h="10">
9105<a:moveTo><a:pt x="0" y="0"/></a:moveTo>
9106</a:path>
9107</a:pathLst>
9108</a:custGeom>"#;
9109        let geom = parse_custom_geometry(xml).expect("parse ok");
9110        assert_eq!(geom.fill.as_deref(), Some("norm"));
9111        assert_eq!(geom.stroke.as_deref(), Some("none"));
9112    }
9113
9114    /// 验证 `CustomGeometry::write_xml` + `parse_custom_geometry` 的 round-trip。
9115    #[test]
9116    fn custom_geometry_round_trip() {
9117        use crate::oxml::writer::XmlWriter;
9118
9119        let original = CustomGeometry {
9120            fill: Some("norm".to_string()),
9121            stroke: None,
9122            rect: Some(GeomRect {
9123                l: "0".to_string(),
9124                t: "0".to_string(),
9125                r: "100".to_string(),
9126                b: "100".to_string(),
9127            }),
9128            path_list: vec![Path {
9129                width: 100,
9130                height: 100,
9131                fill: Some("norm".to_string()),
9132                stroke: None,
9133                segments: vec![
9134                    PathSegment::MoveTo { x: 0, y: 0 },
9135                    PathSegment::LineTo { x: 100, y: 0 },
9136                    PathSegment::LineTo { x: 100, y: 100 },
9137                    PathSegment::LineTo { x: 0, y: 100 },
9138                    PathSegment::Close,
9139                ],
9140            }],
9141        };
9142        // 序列化
9143        let mut w = XmlWriter::new();
9144        original.write_xml(&mut w);
9145        let xml = w.buf.clone();
9146        // 反序列化
9147        let parsed = parse_custom_geometry(&xml).expect("parse ok");
9148        assert_eq!(parsed.fill, original.fill);
9149        assert_eq!(parsed.stroke, original.stroke);
9150        assert_eq!(parsed.rect, original.rect);
9151        assert_eq!(parsed.path_list.len(), 1);
9152        let p0 = &parsed.path_list[0];
9153        assert_eq!(p0.width, 100);
9154        assert_eq!(p0.height, 100);
9155        assert_eq!(p0.fill.as_deref(), Some("norm"));
9156        assert_eq!(p0.segments.len(), 5);
9157        // 验证第一段
9158        match &p0.segments[0] {
9159            PathSegment::MoveTo { x, y } => {
9160                assert_eq!(*x, 0);
9161                assert_eq!(*y, 0);
9162            }
9163            other => panic!("expected MoveTo, got {:?}", other),
9164        }
9165        // 验证最后一段
9166        match &p0.segments[4] {
9167            PathSegment::Close => {}
9168            other => panic!("expected Close, got {:?}", other),
9169        }
9170    }
9171
9172    /// 验证 `parse_sld` 能从完整 slide XML 中解析带 custGeom 的形状。
9173    #[test]
9174    fn parse_sp_with_custgeom() {
9175        let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
9176<p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
9177       xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
9178       xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
9179  <p:cSld>
9180    <p:spTree>
9181      <p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
9182      <p:grpSpPr><a:xfrm/></p:grpSpPr>
9183      <p:sp>
9184        <p:nvSpPr>
9185          <p:cNvPr id="10" name="Freeform 1"/>
9186          <p:cNvSpPr/>
9187          <p:nvPr/>
9188        </p:nvSpPr>
9189        <p:spPr>
9190          <a:xfrm>
9191            <a:off x="100" y="100"/>
9192            <a:ext cx="200" cy="200"/>
9193          </a:xfrm>
9194          <a:custGeom>
9195            <a:avLst/>
9196            <a:pathLst>
9197              <a:path w="200" h="200">
9198                <a:moveTo><a:pt x="0" y="0"/></a:moveTo>
9199                <a:lnTo><a:pt x="200" y="0"/></a:lnTo>
9200                <a:lnTo><a:pt x="100" y="200"/></a:lnTo>
9201                <a:close/>
9202              </a:path>
9203            </a:pathLst>
9204          </a:custGeom>
9205        </p:spPr>
9206      </p:sp>
9207    </p:spTree>
9208  </p:cSld>
9209  <p:clrMapOvr/>
9210</p:sld>"#;
9211        let sld = parse_sld(xml).expect("parse ok");
9212        assert_eq!(sld.shapes.len(), 1);
9213        match &sld.shapes[0] {
9214            OxmlSlideShape::Sp(sp) => {
9215                // 验证几何是 Custom
9216                let geom = sp
9217                    .properties
9218                    .geometry
9219                    .as_ref()
9220                    .expect("geometry should exist");
9221                match geom {
9222                    Geometry::Custom(cg) => {
9223                        assert_eq!(cg.path_list.len(), 1);
9224                        let p = &cg.path_list[0];
9225                        assert_eq!(p.width, 200);
9226                        assert_eq!(p.height, 200);
9227                        assert_eq!(p.segments.len(), 4);
9228                        match &p.segments[0] {
9229                            PathSegment::MoveTo { x, y } => {
9230                                assert_eq!(*x, 0);
9231                                assert_eq!(*y, 0);
9232                            }
9233                            other => panic!("expected MoveTo, got {:?}", other),
9234                        }
9235                    }
9236                    other => panic!("expected Custom geometry, got {:?}", other),
9237                }
9238            }
9239            other => panic!("expected Sp, got {:?}", other),
9240        }
9241    }
9242
9243    /// 验证 `Geometry::write_xml` 对 Preset 变体的序列化(确保 prstGeom 路径未被破坏)。
9244    #[test]
9245    fn geometry_preset_write_xml() {
9246        use crate::oxml::simpletypes::PresetGeometry;
9247        use crate::oxml::writer::XmlWriter;
9248
9249        let geom = Geometry::preset(PresetGeometry::Rectangle);
9250        let mut w = XmlWriter::new();
9251        geom.write_xml(&mut w);
9252        let xml = w.buf.clone();
9253        assert!(xml.contains("<a:prstGeom prst=\"rect\">"), "xml: {}", xml);
9254        assert!(xml.contains("<a:avLst/>"), "xml: {}", xml);
9255        assert!(xml.contains("</a:prstGeom>"), "xml: {}", xml);
9256    }
9257
9258    /// 验证 `Geometry::write_xml` 对 Custom 变体的序列化。
9259    #[test]
9260    fn geometry_custom_write_xml() {
9261        use crate::oxml::writer::XmlWriter;
9262
9263        let geom = Geometry::Custom(CustomGeometry {
9264            fill: None,
9265            stroke: None,
9266            rect: None,
9267            path_list: vec![Path {
9268                width: 50,
9269                height: 50,
9270                fill: None,
9271                stroke: None,
9272                segments: vec![
9273                    PathSegment::MoveTo { x: 0, y: 0 },
9274                    PathSegment::LineTo { x: 50, y: 50 },
9275                    PathSegment::Close,
9276                ],
9277            }],
9278        });
9279        let mut w = XmlWriter::new();
9280        geom.write_xml(&mut w);
9281        let xml = w.buf.clone();
9282        assert!(xml.contains("<a:custGeom>"), "xml: {}", xml);
9283        assert!(xml.contains("<a:avLst/>"), "xml: {}", xml);
9284        assert!(xml.contains("<a:pathLst>"), "xml: {}", xml);
9285        assert!(xml.contains("<a:path w=\"50\" h=\"50\">"), "xml: {}", xml);
9286        assert!(xml.contains("<a:moveTo>"), "xml: {}", xml);
9287        assert!(xml.contains("<a:lnTo>"), "xml: {}", xml);
9288        assert!(xml.contains("<a:close/>"), "xml: {}", xml);
9289    }
9290
9291    /// 验证带调整值的 `<a:prstGeom>` 在 XML 往返中保持不变(TODO-038)。
9292    ///
9293    /// 测试场景:圆角矩形(roundRect)带单个调整值 `adj=16667`(16.667%)。
9294    /// 期望:解析后 `Geometry::Preset` 携带一个 `AdjustmentValue`,
9295    /// 序列化后 XML 仍包含 `<a:gd name="adj" fmla="val 16667"/>`。
9296    #[test]
9297    fn round_trip_prst_geom_with_adjustments() {
9298        let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
9299<p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
9300       xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
9301       xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
9302  <p:cSld><p:spTree>
9303    <p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
9304    <p:grpSpPr><a:xfrm/></p:grpSpPr>
9305    <p:sp>
9306      <p:nvSpPr><p:cNvPr id="2" name="RoundRect"/><p:cNvSpPr/><p:nvPr/></p:nvSpPr>
9307      <p:spPr>
9308        <a:xfrm><a:off x="100000" y="100000"/><a:ext cx="500000" cy="300000"/></a:xfrm>
9309        <a:prstGeom prst="roundRect">
9310          <a:avLst>
9311            <a:gd name="adj" fmla="val 16667"/>
9312          </a:avLst>
9313        </a:prstGeom>
9314      </p:spPr>
9315      <p:txBody><a:bodyPr/><a:p/></p:txBody>
9316    </p:sp>
9317  </p:spTree></p:cSld>
9318</p:sld>"#;
9319        let sld = parse_sld(xml).expect("parse ok");
9320        let sp = sld
9321            .shapes
9322            .iter()
9323            .find_map(|s| {
9324                if let OxmlSlideShape::Sp(sp) = s {
9325                    Some(sp)
9326                } else {
9327                    None
9328                }
9329            })
9330            .expect("应有 Sp");
9331        // 验证几何为 Preset(roundRect) 且携带一个调整值
9332        match &sp.properties.geometry {
9333            Some(Geometry::Preset(prst, adjustments)) => {
9334                assert_eq!(*prst, PresetGeometry::RoundRectangle);
9335                assert_eq!(adjustments.len(), 1, "应有 1 个调整值");
9336                let adj = &adjustments[0];
9337                assert_eq!(adj.name, "adj");
9338                assert_eq!(adj.raw_value, 16667);
9339                assert!((adj.effective_value() - 0.16667).abs() < 1e-6);
9340            }
9341            other => panic!("期望 Preset 几何,得到 {:?}", other),
9342        }
9343        // 验证序列化后 XML 仍包含调整值
9344        let mut w = crate::oxml::writer::XmlWriter::new();
9345        sp.properties.geometry.as_ref().unwrap().write_xml(&mut w);
9346        let out = &w.buf;
9347        assert!(
9348            out.contains("<a:prstGeom prst=\"roundRect\">"),
9349            "xml: {}",
9350            out
9351        );
9352        assert!(out.contains("<a:avLst>"), "xml: {}", out);
9353        assert!(
9354            out.contains("<a:gd name=\"adj\" fmla=\"val 16667\"/>"),
9355            "xml: {}",
9356            out
9357        );
9358        assert!(out.contains("</a:prstGeom>"), "xml: {}", out);
9359    }
9360
9361    /// 验证带多个调整值的 `<a:prstGeom>` 在 XML 往返中保持不变(TODO-038)。
9362    ///
9363    /// 测试场景:`rect` 形状带两个调整值 `adj1=50000` 和 `adj2=25000`。
9364    /// 注意:rect 通常无调整值,但本测试验证解析器能正确处理多个 `<a:gd>`。
9365    #[test]
9366    fn round_trip_prst_geom_with_multiple_adjustments() {
9367        let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
9368<p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
9369       xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
9370       xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
9371  <p:cSld><p:spTree>
9372    <p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
9373    <p:grpSpPr><a:xfrm/></p:grpSpPr>
9374    <p:sp>
9375      <p:nvSpPr><p:cNvPr id="3" name="MultiAdj"/><p:cNvSpPr/><p:nvPr/></p:nvSpPr>
9376      <p:spPr>
9377        <a:xfrm><a:off x="0" y="0"/><a:ext cx="1000000" cy="500000"/></a:xfrm>
9378        <a:prstGeom prst="rect">
9379          <a:avLst>
9380            <a:gd name="adj1" fmla="val 50000"/>
9381            <a:gd name="adj2" fmla="val 25000"/>
9382          </a:avLst>
9383        </a:prstGeom>
9384      </p:spPr>
9385      <p:txBody><a:bodyPr/><a:p/></p:txBody>
9386    </p:sp>
9387  </p:spTree></p:cSld>
9388</p:sld>"#;
9389        let sld = parse_sld(xml).expect("parse ok");
9390        let sp = sld
9391            .shapes
9392            .iter()
9393            .find_map(|s| {
9394                if let OxmlSlideShape::Sp(sp) = s {
9395                    Some(sp)
9396                } else {
9397                    None
9398                }
9399            })
9400            .expect("应有 Sp");
9401        match &sp.properties.geometry {
9402            Some(Geometry::Preset(_, adjustments)) => {
9403                assert_eq!(adjustments.len(), 2, "应有 2 个调整值");
9404                assert_eq!(adjustments[0].name, "adj1");
9405                assert_eq!(adjustments[0].raw_value, 50000);
9406                assert_eq!(adjustments[1].name, "adj2");
9407                assert_eq!(adjustments[1].raw_value, 25000);
9408            }
9409            other => panic!("期望 Preset 几何,得到 {:?}", other),
9410        }
9411        // 验证序列化
9412        let mut w = crate::oxml::writer::XmlWriter::new();
9413        sp.properties.geometry.as_ref().unwrap().write_xml(&mut w);
9414        let out = &w.buf;
9415        assert!(
9416            out.contains("<a:gd name=\"adj1\" fmla=\"val 50000\"/>"),
9417            "xml: {}",
9418            out
9419        );
9420        assert!(
9421            out.contains("<a:gd name=\"adj2\" fmla=\"val 25000\"/>"),
9422            "xml: {}",
9423            out
9424        );
9425    }
9426
9427    /// 验证空 `<a:avLst/>` 解析为空调整值列表(TODO-038)。
9428    #[test]
9429    fn parse_prst_geom_with_empty_av_lst() {
9430        let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
9431<p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
9432       xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
9433       xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
9434  <p:cSld><p:spTree>
9435    <p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
9436    <p:grpSpPr><a:xfrm/></p:grpSpPr>
9437    <p:sp>
9438      <p:nvSpPr><p:cNvPr id="4" name="EmptyAdj"/><p:cNvSpPr/><p:nvPr/></p:nvSpPr>
9439      <p:spPr>
9440        <a:xfrm><a:off x="0" y="0"/><a:ext cx="1000000" cy="500000"/></a:xfrm>
9441        <a:prstGeom prst="rect">
9442          <a:avLst/>
9443        </a:prstGeom>
9444      </p:spPr>
9445      <p:txBody><a:bodyPr/><a:p/></p:txBody>
9446    </p:sp>
9447  </p:spTree></p:cSld>
9448</p:sld>"#;
9449        let sld = parse_sld(xml).expect("parse ok");
9450        let sp = sld
9451            .shapes
9452            .iter()
9453            .find_map(|s| {
9454                if let OxmlSlideShape::Sp(sp) = s {
9455                    Some(sp)
9456                } else {
9457                    None
9458                }
9459            })
9460            .expect("应有 Sp");
9461        match &sp.properties.geometry {
9462            Some(Geometry::Preset(prst, adjustments)) => {
9463                assert_eq!(*prst, PresetGeometry::Rectangle);
9464                assert!(adjustments.is_empty(), "空 avLst 应解析为空列表");
9465            }
9466            other => panic!("期望 Preset 几何,得到 {:?}", other),
9467        }
9468    }
9469
9470    /// 验证无 `<a:avLst>` 的 `<a:prstGeom>` 解析为空调整值列表(TODO-038)。
9471    #[test]
9472    fn parse_prst_geom_without_av_lst() {
9473        let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
9474<p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
9475       xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
9476       xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
9477  <p:cSld><p:spTree>
9478    <p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
9479    <p:grpSpPr><a:xfrm/></p:grpSpPr>
9480    <p:sp>
9481      <p:nvSpPr><p:cNvPr id="5" name="NoAdj"/><p:cNvSpPr/><p:nvPr/></p:nvSpPr>
9482      <p:spPr>
9483        <a:xfrm><a:off x="0" y="0"/><a:ext cx="1000000" cy="500000"/></a:xfrm>
9484        <a:prstGeom prst="rect"/>
9485      </p:spPr>
9486      <p:txBody><a:bodyPr/><a:p/></p:txBody>
9487    </p:sp>
9488  </p:spTree></p:cSld>
9489</p:sld>"#;
9490        let sld = parse_sld(xml).expect("parse ok");
9491        let sp = sld
9492            .shapes
9493            .iter()
9494            .find_map(|s| {
9495                if let OxmlSlideShape::Sp(sp) = s {
9496                    Some(sp)
9497                } else {
9498                    None
9499                }
9500            })
9501            .expect("应有 Sp");
9502        match &sp.properties.geometry {
9503            Some(Geometry::Preset(prst, adjustments)) => {
9504                assert_eq!(*prst, PresetGeometry::Rectangle);
9505                assert!(adjustments.is_empty(), "无 avLst 应解析为空列表");
9506            }
9507            other => panic!("期望 Preset 几何,得到 {:?}", other),
9508        }
9509    }
9510
9511    /// 验证 `parse_fmla_val` 对各种公式格式的解析(TODO-038)。
9512    #[test]
9513    fn parse_fmla_val_formats() {
9514        // 标准格式 "val 16667"
9515        assert_eq!(parse_fmla_val("val 16667"), Some(16667));
9516        // 无空格 "val16667"(罕见但合法)
9517        assert_eq!(parse_fmla_val("val16667"), Some(16667));
9518        // 带前后空白
9519        assert_eq!(parse_fmla_val("  val 16667  "), Some(16667));
9520        // 负数
9521        assert_eq!(parse_fmla_val("val -5000"), Some(-5000));
9522        // 非数字
9523        assert_eq!(parse_fmla_val("val abc"), None);
9524        // 非 val 公式(如乘法公式)
9525        assert_eq!(parse_fmla_val("*/ adj1 100000 50000"), None);
9526        // 空字符串
9527        assert_eq!(parse_fmla_val(""), None);
9528    }
9529
9530    /// 验证 `parse_graphic_frame` 能识别 SmartArt 并保留 raw_xml(TODO-037 最小保留)。
9531    ///
9532    /// 关键断言:
9533    /// - `<a:graphicData uri=".../diagram">` 应被识别为 `Graphic::SmartArt`;
9534    /// - `raw_xml` 应包含完整的 `<a:graphicData>` 元素(含外壳);
9535    /// - 4 个关系 id(r:dm / r:lo / r:qs / r:cs)应被正确提取。
9536    #[test]
9537    fn parse_graphic_frame_with_smartart() {
9538        let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
9539<p:graphicFrame xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
9540                xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
9541                xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
9542                xmlns:dgm="http://schemas.openxmlformats.org/drawingml/2006/diagram">
9543  <p:nvGraphicFramePr>
9544    <p:cNvPr id="300" name="SmartArt 1"/>
9545    <p:cNvGraphicFramePr/>
9546    <p:nvPr/>
9547  </p:nvGraphicFramePr>
9548  <p:xfrm>
9549    <a:off x="457200" y="1828800"/>
9550    <a:ext cx="8229600" cy="1143000"/>
9551  </p:xfrm>
9552  <a:graphic>
9553    <a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/diagram">
9554      <dgm:relIds r:dm="rIdDm1" r:lo="rIdLo1" r:qs="rIdQs1" r:cs="rIdCs1"/>
9555    </a:graphicData>
9556  </a:graphic>
9557</p:graphicFrame>"#;
9558        let frame = parse_graphic_frame(xml).expect("parse ok");
9559        assert_eq!(frame.id, 300);
9560        assert_eq!(frame.name, "SmartArt 1");
9561        match &frame.graphic {
9562            crate::oxml::shape::Graphic::SmartArt(s) => {
9563                // raw_xml 应包含完整 graphicData 元素
9564                assert!(
9565                    s.raw_xml.contains("<a:graphicData"),
9566                    "raw_xml 应包含 graphicData 外壳,实际: {}",
9567                    s.raw_xml
9568                );
9569                assert!(
9570                    s.raw_xml.contains(
9571                        "uri=\"http://schemas.openxmlformats.org/drawingml/2006/diagram\""
9572                    ),
9573                    "raw_xml 应包含 diagram uri,实际: {}",
9574                    s.raw_xml
9575                );
9576                // 4 个关系 id 应被正确提取
9577                assert_eq!(s.dm_rid.as_deref(), Some("rIdDm1"));
9578                assert_eq!(s.lo_rid.as_deref(), Some("rIdLo1"));
9579                assert_eq!(s.qs_rid.as_deref(), Some("rIdQs1"));
9580                assert_eq!(s.cs_rid.as_deref(), Some("rIdCs1"));
9581            }
9582            other => panic!("期望 SmartArt,得到 {:?}", other),
9583        }
9584    }
9585
9586    /// 验证 `parse_smartart_rel_ids` 对各种 raw_xml 格式的解析(TODO-037)。
9587    #[test]
9588    fn parse_smartart_rel_ids_formats() {
9589        // 标准格式:4 个关系 id 都存在
9590        let raw = r#"<a:graphicData uri=".../diagram"><dgm:relIds r:dm="rId1" r:lo="rId2" r:qs="rId3" r:cs="rId4"/></a:graphicData>"#;
9591        let (dm, lo, qs, cs) = parse_smartart_rel_ids(raw);
9592        assert_eq!(dm.as_deref(), Some("rId1"));
9593        assert_eq!(lo.as_deref(), Some("rId2"));
9594        assert_eq!(qs.as_deref(), Some("rId3"));
9595        assert_eq!(cs.as_deref(), Some("rId4"));
9596
9597        // 缺失部分属性
9598        let raw_partial =
9599            r#"<a:graphicData uri=".../diagram"><dgm:relIds r:dm="rId1"/></a:graphicData>"#;
9600        let (dm, lo, qs, cs) = parse_smartart_rel_ids(raw_partial);
9601        assert_eq!(dm.as_deref(), Some("rId1"));
9602        assert!(lo.is_none(), "lo 应为 None");
9603        assert!(qs.is_none(), "qs 应为 None");
9604        assert!(cs.is_none(), "cs 应为 None");
9605
9606        // 完全缺失 dgm:relIds
9607        let raw_empty = r#"<a:graphicData uri=".../diagram"></a:graphicData>"#;
9608        let (dm, lo, qs, cs) = parse_smartart_rel_ids(raw_empty);
9609        assert!(dm.is_none() && lo.is_none() && qs.is_none() && cs.is_none());
9610    }
9611}