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(parse_run_properties_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 = parse_run_properties_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(parse_run_properties_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 = parse_run_properties_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
3968/// 从 `a:rPr` 的自闭合属性中解析 `RunProperties`(不递归子元素)。
3969///
3970/// 自 v0.5.0 起,`RunProperties` 类型来自 [`ooxml_core::oxml::txbody`],
3971/// 受孤儿规则限制不能在 pptx-rs 主 crate 为其定义固有方法,
3972/// 因此把原 `RunProperties::from_bytes_start` 改为自由函数。
3973pub fn parse_run_properties_from_bytes_start(
3974    e: &quick_xml::events::BytesStart<'_>,
3975) -> RunProperties {
3976    let mut rp = RunProperties::default();
3977    for a in e.attributes().flatten() {
3978        let v = a
3979            .normalized_value(quick_xml::XmlVersion::Implicit1_0)
3980            .unwrap_or_default();
3981        match a.key.as_ref() {
3982            b"sz" => {
3983                if let Ok(p) = v.parse::<i32>() {
3984                    rp.size = Some(Pt(p as f64 / 100.0));
3985                }
3986            }
3987            b"b" => {
3988                if v == "1" || v == "true" {
3989                    rp.bold = true;
3990                }
3991            }
3992            b"i" => {
3993                if v == "1" || v == "true" {
3994                    rp.italic = true;
3995                }
3996            }
3997            b"u" => {
3998                if let Ok(u) = v.parse::<Underline>() {
3999                    rp.underline = Some(u);
4000                }
4001            }
4002            b"strike" => {
4003                if v == "sngStrike" {
4004                    rp.strike = true;
4005                }
4006                if v == "dblStrike" {
4007                    rp.strike_dbl = true;
4008                }
4009            }
4010            b"baseline" => {
4011                if let Ok(v) = v.parse::<i32>() {
4012                    rp.baseline = Some(v);
4013                }
4014            }
4015            b"kern" => {
4016                if let Ok(v) = v.parse::<i32>() {
4017                    rp.kerning = Some(v);
4018                }
4019            }
4020            b"spc" => {
4021                if let Ok(v) = v.parse::<i32>() {
4022                    rp.spc = Some(v);
4023                }
4024            }
4025            b"lang" => {
4026                rp.lang = Some(v.to_string());
4027            }
4028            _ => {}
4029        }
4030    }
4031    rp
4032}
4033
4034/// 解析 `p:pic` 元素(仅最小字段;其它信息丢失)。
4035///
4036/// 走 **正规 SAX**:在 `<p:blipFill>` 内逐事件读 `<a:blip r:embed="..."/>`
4037/// 等自闭合元素;不再使用"字符串 find r:embed"——后者在缩进/属性顺序变化时会漏取。
4038pub fn parse_pic(xml: &str) -> crate::Result<Pic> {
4039    let mut pic = Pic::default();
4040    let mut rd = Reader::from_str(xml);
4041    rd.config_mut().trim_text(true);
4042    let mut buf = Vec::new();
4043    let mut in_blipfill = false;
4044    let mut blipfill_depth: i32 = 0;
4045    loop {
4046        match rd.read_event_into(&mut buf) {
4047            Ok(Event::Start(e)) => {
4048                let name = e.name();
4049                let local = local_name(name.as_ref());
4050                if local == b"pic" {
4051                    // 根元素 <p:pic>:仅标记进入,不调用 collect_full_element
4052                    // (否则会吞掉所有子元素,导致 nvPicPr / blipFill 等无法解析)
4053                } else if local == b"nvPicPr" {
4054                    // 不使用 collect_full_element 吞掉 nvPicPr,
4055                    // 而是手动遍历子元素提取 cNvPr 的 id/name 属性
4056                    let mut nv_depth = 1i32;
4057                    loop {
4058                        match rd.read_event_into(&mut buf) {
4059                            Ok(Event::Start(_)) => nv_depth += 1,
4060                            Ok(Event::End(_)) => {
4061                                nv_depth -= 1;
4062                                if nv_depth == 0 {
4063                                    break;
4064                                }
4065                            }
4066                            Ok(Event::Empty(e2)) => {
4067                                let name2 = e2.name();
4068                                let local2 = local_name(name2.as_ref());
4069                                if local2 == b"cNvPr" {
4070                                    for a in e2.attributes().flatten() {
4071                                        match a.key.as_ref() {
4072                                            b"id" => {
4073                                                if let Ok(v) = a
4074                                                    .normalized_value(
4075                                                        quick_xml::XmlVersion::Implicit1_0,
4076                                                    )
4077                                                    .unwrap_or_default()
4078                                                    .parse::<u32>()
4079                                                {
4080                                                    pic.id = v;
4081                                                }
4082                                            }
4083                                            b"name" => {
4084                                                pic.name = a
4085                                                    .normalized_value(
4086                                                        quick_xml::XmlVersion::Implicit1_0,
4087                                                    )
4088                                                    .unwrap_or_default()
4089                                                    .to_string();
4090                                            }
4091                                            _ => {}
4092                                        }
4093                                    }
4094                                }
4095                            }
4096                            Ok(Event::Eof) => break,
4097                            Err(_) => break,
4098                            _ => {}
4099                        }
4100                        buf.clear();
4101                    }
4102                } else if local == b"blipFill" {
4103                    in_blipfill = true;
4104                    blipfill_depth = 1;
4105                    // 不立即吞掉,改为手写 SAX 解析 blipFill 的子元素
4106                    // 让 Event::Start/Empty 单独触发下方的 blip 处理
4107                } else if local == b"spPr" {
4108                    let inner = collect_full_element(&mut rd, e.into_owned())?;
4109                    pic.properties = parse_sppr(&inner)?;
4110                } else if in_blipfill && local == b"blip" {
4111                    // `<a:blip r:embed="rIdN"/>` 可能是 Start+End 也可能是 Empty。
4112                    // 直接从当前事件读取属性,**不再**走 collect_full_element。
4113                    for a in e.attributes().flatten() {
4114                        let key = a.key.as_ref();
4115                        // 兼容 `r:embed` 与 `embed` 两种命名空间形态
4116                        if key == b"r:embed" || key.ends_with(b":embed") {
4117                            pic.rid = a
4118                                .normalized_value(quick_xml::XmlVersion::Implicit1_0)
4119                                .unwrap_or_default()
4120                                .to_string();
4121                        }
4122                    }
4123                } else if in_blipfill && local == b"srcRect" {
4124                    // `<a:srcRect l="..." t="..." r="..." b="..."/>`
4125                    let (mut l, mut t, mut r, mut b) = (None, None, None, None);
4126                    for a in e.attributes().flatten() {
4127                        let v = a
4128                            .normalized_value(quick_xml::XmlVersion::Implicit1_0)
4129                            .unwrap_or_default();
4130                        match a.key.as_ref() {
4131                            b"l" => l = v.parse().ok(),
4132                            b"t" => t = v.parse().ok(),
4133                            b"r" => r = v.parse().ok(),
4134                            b"b" => b = v.parse().ok(),
4135                            _ => {}
4136                        }
4137                    }
4138                    if let (Some(ll), Some(tt), Some(rr), Some(bb)) = (l, t, r, b) {
4139                        pic.src_rect = Some((ll, tt, rr, bb));
4140                    }
4141                } else if in_blipfill && local == b"stretch" {
4142                    // `<a:stretch><a:fillRect/></a:stretch>`:拉伸填充模式
4143                    let _ = collect_full_element(&mut rd, e.into_owned())?;
4144                    pic.fill_mode = crate::oxml::sppr::BlipFillMode::Stretch;
4145                } else if in_blipfill && local == b"tile" {
4146                    // `<a:tile tx="..." ty="..." sx="..." sy="..." flip="..." algn="..."/>`
4147                    // 先解析属性,再吞掉子元素(tile 很少有子元素,但保险起见)
4148                    let mode = parse_tile_attrs(&e);
4149                    let _ = collect_full_element(&mut rd, e.into_owned())?;
4150                    pic.fill_mode = mode;
4151                } else {
4152                    let _ = collect_full_element(&mut rd, e.into_owned());
4153                }
4154            }
4155            Ok(Event::Empty(e)) => {
4156                let name = e.name();
4157                let local = local_name(name.as_ref());
4158                if in_blipfill && local == b"blip" {
4159                    // 自闭合形态的 `<a:blip r:embed="rIdN"/>`
4160                    for a in e.attributes().flatten() {
4161                        let key = a.key.as_ref();
4162                        if key == b"r:embed" || key.ends_with(b":embed") {
4163                            pic.rid = a
4164                                .normalized_value(quick_xml::XmlVersion::Implicit1_0)
4165                                .unwrap_or_default()
4166                                .to_string();
4167                        }
4168                    }
4169                } else if in_blipfill && local == b"srcRect" {
4170                    let (mut l, mut t, mut r, mut b) = (None, None, None, None);
4171                    for a in e.attributes().flatten() {
4172                        let v = a
4173                            .normalized_value(quick_xml::XmlVersion::Implicit1_0)
4174                            .unwrap_or_default();
4175                        match a.key.as_ref() {
4176                            b"l" => l = v.parse().ok(),
4177                            b"t" => t = v.parse().ok(),
4178                            b"r" => r = v.parse().ok(),
4179                            b"b" => b = v.parse().ok(),
4180                            _ => {}
4181                        }
4182                    }
4183                    if let (Some(ll), Some(tt), Some(rr), Some(bb)) = (l, t, r, b) {
4184                        pic.src_rect = Some((ll, tt, rr, bb));
4185                    }
4186                } else if in_blipfill && local == b"stretch" {
4187                    // 自闭合 `<a:stretch/>`(罕见,通常带 fillRect 子元素)
4188                    pic.fill_mode = crate::oxml::sppr::BlipFillMode::Stretch;
4189                } else if in_blipfill && local == b"tile" {
4190                    // 自闭合 `<a:tile .../>`
4191                    pic.fill_mode = parse_tile_attrs(&e);
4192                }
4193            }
4194            Ok(Event::End(e)) => {
4195                let name = e.name();
4196                let local = local_name(name.as_ref());
4197                // blipFill 可能是 `<a:blipFill><a:blip/>...</a:blipFill>` 这种
4198                // 形式:进入 blipFill 时 depth=1,遇到结束事件 depth-1。
4199                // depth 归 0 即离开 blipFill 范围。
4200                if in_blipfill && local == b"blipFill" {
4201                    blipfill_depth -= 1;
4202                    if blipfill_depth == 0 {
4203                        in_blipfill = false;
4204                    }
4205                }
4206            }
4207            Ok(Event::Eof) => break,
4208            Err(e) => return Err(crate::Error::Xml(format!("pic parse: {e}"))),
4209            _ => {}
4210        }
4211        buf.clear();
4212    }
4213    Ok(pic)
4214}
4215
4216/// 解析 `<p:cxnSp>` 元素(连接器)。
4217///
4218/// # 元素结构(OOXML)
4219///
4220/// ```text
4221/// <p:cxnSp>
4222///   <p:nvCxnSpPr>
4223///     <p:cNvPr id="..." name="..."/>
4224///     <p:cNvCxnSpPr/>
4225///     <p:nvPr/>
4226///     <p:stCxn id="..." idx="..."/>   ← 可选:起点挂接
4227///     <p:endCxn id="..." idx="..."/>  ← 可选:终点挂接
4228///   </p:nvCxnSpPr>
4229///   <p:spPr>...</p:spPr>
4230///   <p:style>...</p:style>           ← 可选
4231///   <p:extLst>...</p:extLst>         ← 可选
4232/// </p:cxnSp>
4233/// ```
4234///
4235/// # 行为
4236/// - 提取 `id` / `name` 属性;
4237/// - 解析 `stCxn` / `endCxn` 挂接信息(`Some((shape_id, idx))`);
4238/// - 解析 `spPr`(变换 / 几何 / 填充 / 边框);
4239/// - `style` / `extLst` 暂不解析(保留 `None`)。
4240pub fn parse_cxn_sp(xml: &str) -> crate::Result<Connector> {
4241    let mut cxn = Connector::default();
4242    let mut rd = Reader::from_str(xml);
4243    rd.config_mut().trim_text(true);
4244    let mut buf = Vec::new();
4245    loop {
4246        match rd.read_event_into(&mut buf) {
4247            Ok(Event::Start(e)) => {
4248                let name = e.name();
4249                let local = local_name(name.as_ref());
4250                if local == b"cxnSp" {
4251                    // 根元素 <p:cxnSp>:仅标记进入
4252                } else if local == b"nvCxnSpPr" {
4253                    // 手动遍历 nvCxnSpPr 子元素提取 cNvPr / stCxn / endCxn
4254                    let mut nv_depth = 1i32;
4255                    loop {
4256                        match rd.read_event_into(&mut buf) {
4257                            Ok(Event::Start(_)) => nv_depth += 1,
4258                            Ok(Event::End(_)) => {
4259                                nv_depth -= 1;
4260                                if nv_depth == 0 {
4261                                    break;
4262                                }
4263                            }
4264                            Ok(Event::Empty(e2)) => {
4265                                let name2 = e2.name();
4266                                let local2 = local_name(name2.as_ref());
4267                                if local2 == b"cNvPr" {
4268                                    for a in e2.attributes().flatten() {
4269                                        match a.key.as_ref() {
4270                                            b"id" => {
4271                                                if let Ok(v) = a
4272                                                    .normalized_value(
4273                                                        quick_xml::XmlVersion::Implicit1_0,
4274                                                    )
4275                                                    .unwrap_or_default()
4276                                                    .parse::<u32>()
4277                                                {
4278                                                    cxn.id = v;
4279                                                }
4280                                            }
4281                                            b"name" => {
4282                                                cxn.name = a
4283                                                    .normalized_value(
4284                                                        quick_xml::XmlVersion::Implicit1_0,
4285                                                    )
4286                                                    .unwrap_or_default()
4287                                                    .to_string();
4288                                            }
4289                                            _ => {}
4290                                        }
4291                                    }
4292                                } else if local2 == b"stCxn" {
4293                                    // 起点挂接:<p:stCxn id="..." idx="..."/>
4294                                    let mut sid: Option<u32> = None;
4295                                    let mut idx: Option<u32> = None;
4296                                    for a in e2.attributes().flatten() {
4297                                        match a.key.as_ref() {
4298                                            b"id" => {
4299                                                sid = a
4300                                                    .normalized_value(
4301                                                        quick_xml::XmlVersion::Implicit1_0,
4302                                                    )
4303                                                    .unwrap_or_default()
4304                                                    .parse()
4305                                                    .ok();
4306                                            }
4307                                            b"idx" => {
4308                                                idx = a
4309                                                    .normalized_value(
4310                                                        quick_xml::XmlVersion::Implicit1_0,
4311                                                    )
4312                                                    .unwrap_or_default()
4313                                                    .parse()
4314                                                    .ok();
4315                                            }
4316                                            _ => {}
4317                                        }
4318                                    }
4319                                    if let (Some(s), Some(i)) = (sid, idx) {
4320                                        cxn.st_cxn = Some((s, i));
4321                                    }
4322                                } else if local2 == b"endCxn" {
4323                                    // 终点挂接:<p:endCxn id="..." idx="..."/>
4324                                    let mut sid: Option<u32> = None;
4325                                    let mut idx: Option<u32> = None;
4326                                    for a in e2.attributes().flatten() {
4327                                        match a.key.as_ref() {
4328                                            b"id" => {
4329                                                sid = a
4330                                                    .normalized_value(
4331                                                        quick_xml::XmlVersion::Implicit1_0,
4332                                                    )
4333                                                    .unwrap_or_default()
4334                                                    .parse()
4335                                                    .ok();
4336                                            }
4337                                            b"idx" => {
4338                                                idx = a
4339                                                    .normalized_value(
4340                                                        quick_xml::XmlVersion::Implicit1_0,
4341                                                    )
4342                                                    .unwrap_or_default()
4343                                                    .parse()
4344                                                    .ok();
4345                                            }
4346                                            _ => {}
4347                                        }
4348                                    }
4349                                    if let (Some(s), Some(i)) = (sid, idx) {
4350                                        cxn.end_cxn = Some((s, i));
4351                                    }
4352                                }
4353                            }
4354                            Ok(Event::Eof) => break,
4355                            Err(_) => break,
4356                            _ => {}
4357                        }
4358                        buf.clear();
4359                    }
4360                } else if local == b"spPr" {
4361                    let inner = collect_full_element(&mut rd, e.into_owned())?;
4362                    cxn.properties = parse_sppr(&inner)?;
4363                } else if local == b"style" {
4364                    // <p:style> 主题样式引用(TODO-006 一致性补全)
4365                    let inner = collect_full_element(&mut rd, e.into_owned())?;
4366                    cxn.style = Some(parse_shape_style(&inner)?);
4367                } else {
4368                    // extLst 等暂不解析,吞掉
4369                    let _ = collect_full_element(&mut rd, e.into_owned());
4370                }
4371            }
4372            Ok(Event::End(e)) => {
4373                if local_name(e.name().as_ref()) == b"cxnSp" {
4374                    break;
4375                }
4376            }
4377            Ok(Event::Eof) => break,
4378            Err(e) => return Err(crate::Error::Xml(format!("cxnSp parse: {e}"))),
4379            _ => {}
4380        }
4381        buf.clear();
4382    }
4383    Ok(cxn)
4384}
4385
4386/// 解析 `<p:grpSp>` 元素(组合形状,递归)。
4387///
4388/// # 元素结构(OOXML)
4389///
4390/// ```text
4391/// <p:grpSp>
4392///   <p:nvGrpSpPr>
4393///     <p:cNvPr id="..." name="..."/>
4394///     <p:cNvGrpSpPr/>
4395///     <p:nvPr/>
4396///   </p:nvGrpSpPr>
4397///   <p:grpSpPr>
4398///     <a:xfrm>
4399///       <a:off x="..." y="..."/>
4400///       <a:ext cx="..." cy="..."/>
4401///       <a:chOff x="..." y="..."/>
4402///       <a:chExt cx="..." cy="..."/>
4403///     </a:xfrm>
4404///   </p:grpSpPr>
4405///   子形状 (sp/pic/cxnSp/grpSp/graphicFrame)   ← 递归
4406///   <p:extLst>...</p:extLst>                  ← 可选
4407/// </p:grpSp>
4408/// ```
4409///
4410/// # 行为
4411/// - 提取 `id` / `name`;
4412/// - 解析 `grpSpPr` 内的 `a:xfrm`(off/ext/chOff/chExt);
4413/// - 递归解析子形状(sp/pic/cxnSp/grpSp/graphicFrame)。
4414pub fn parse_grp_sp(xml: &str) -> crate::Result<Group> {
4415    let mut grp = Group::default();
4416    let mut rd = Reader::from_str(xml);
4417    rd.config_mut().trim_text(true);
4418    let mut buf = Vec::new();
4419    // 收集子形状的原始 XML,稍后递归解析
4420    let mut child_sp_bufs: Vec<String> = Vec::new();
4421    let mut child_pic_bufs: Vec<String> = Vec::new();
4422    let mut child_cxn_bufs: Vec<String> = Vec::new();
4423    let mut child_grp_bufs: Vec<String> = Vec::new();
4424    let mut child_gfx_bufs: Vec<String> = Vec::new();
4425    // 标记是否已遇到根 <p:grpSp>(用于区分根元素与嵌套 grpSp)
4426    let mut entered_root = false;
4427
4428    loop {
4429        match rd.read_event_into(&mut buf) {
4430            Ok(Event::Start(e)) => {
4431                let name = e.name();
4432                let local = local_name(name.as_ref());
4433                if local == b"grpSp" {
4434                    if !entered_root {
4435                        // 根元素 <p:grpSp>:仅标记进入
4436                        entered_root = true;
4437                    } else {
4438                        // 嵌套组合:递归
4439                        let inner = collect_full_element(&mut rd, e.into_owned())?;
4440                        child_grp_bufs.push(inner);
4441                    }
4442                } else if local == b"nvGrpSpPr" {
4443                    // 手动遍历 nvGrpSpPr 子元素提取 cNvPr
4444                    let mut nv_depth = 1i32;
4445                    loop {
4446                        match rd.read_event_into(&mut buf) {
4447                            Ok(Event::Start(_)) => nv_depth += 1,
4448                            Ok(Event::End(_)) => {
4449                                nv_depth -= 1;
4450                                if nv_depth == 0 {
4451                                    break;
4452                                }
4453                            }
4454                            Ok(Event::Empty(e2)) => {
4455                                let name2 = e2.name();
4456                                let local2 = local_name(name2.as_ref());
4457                                if local2 == b"cNvPr" {
4458                                    for a in e2.attributes().flatten() {
4459                                        match a.key.as_ref() {
4460                                            b"id" => {
4461                                                if let Ok(v) = a
4462                                                    .normalized_value(
4463                                                        quick_xml::XmlVersion::Implicit1_0,
4464                                                    )
4465                                                    .unwrap_or_default()
4466                                                    .parse::<u32>()
4467                                                {
4468                                                    grp.id = v;
4469                                                }
4470                                            }
4471                                            b"name" => {
4472                                                grp.name = a
4473                                                    .normalized_value(
4474                                                        quick_xml::XmlVersion::Implicit1_0,
4475                                                    )
4476                                                    .unwrap_or_default()
4477                                                    .to_string();
4478                                            }
4479                                            _ => {}
4480                                        }
4481                                    }
4482                                }
4483                            }
4484                            Ok(Event::Eof) => break,
4485                            Err(_) => break,
4486                            _ => {}
4487                        }
4488                        buf.clear();
4489                    }
4490                } else if local == b"grpSpPr" {
4491                    // 解析 grpSpPr 内的 a:xfrm(off/ext/chOff/chExt)
4492                    parse_grp_sppr_into(&mut rd, &mut grp, e.into_owned())?;
4493                } else if local == b"sp" {
4494                    let inner = collect_full_element(&mut rd, e.into_owned())?;
4495                    child_sp_bufs.push(inner);
4496                } else if local == b"pic" {
4497                    let inner = collect_full_element(&mut rd, e.into_owned())?;
4498                    child_pic_bufs.push(inner);
4499                } else if local == b"cxnSp" {
4500                    let inner = collect_full_element(&mut rd, e.into_owned())?;
4501                    child_cxn_bufs.push(inner);
4502                } else if local == b"graphicFrame" {
4503                    let inner = collect_full_element(&mut rd, e.into_owned())?;
4504                    child_gfx_bufs.push(inner);
4505                } else if local == b"style" {
4506                    // <p:style> 主题样式引用(TODO-006 一致性补全)
4507                    let inner = collect_full_element(&mut rd, e.into_owned())?;
4508                    grp.style = Some(parse_shape_style(&inner)?);
4509                } else {
4510                    // extLst 等暂不解析,吞掉
4511                    let _ = collect_full_element(&mut rd, e.into_owned());
4512                }
4513            }
4514            Ok(Event::End(e)) => {
4515                if local_name(e.name().as_ref()) == b"grpSp" {
4516                    break;
4517                }
4518            }
4519            Ok(Event::Eof) => break,
4520            Err(e) => return Err(crate::Error::Xml(format!("grpSp parse: {e}"))),
4521            _ => {}
4522        }
4523        buf.clear();
4524    }
4525    // 递归解析子形状
4526    for s in child_sp_bufs {
4527        if let Ok(sp) = parse_sp(&s) {
4528            grp.children.push(GroupChild::Sp(sp));
4529        }
4530    }
4531    for p in child_pic_bufs {
4532        if let Ok(pic) = parse_pic(&p) {
4533            grp.children.push(GroupChild::Pic(pic));
4534        }
4535    }
4536    for c in child_cxn_bufs {
4537        if let Ok(cxn) = parse_cxn_sp(&c) {
4538            grp.children.push(GroupChild::CxnSp(cxn));
4539        }
4540    }
4541    for g in child_grp_bufs {
4542        if let Ok(sub) = parse_grp_sp(&g) {
4543            grp.children.push(GroupChild::Group(Box::new(sub)));
4544        }
4545    }
4546    for f in child_gfx_bufs {
4547        if let Ok(frame) = parse_graphic_frame(&f) {
4548            grp.children.push(GroupChild::GraphicFrame(frame));
4549        }
4550    }
4551    Ok(grp)
4552}
4553
4554/// 解析 `<p:grpSpPr>` 内的 `<a:xfrm>`,提取 off/ext/chOff/chExt。
4555fn parse_grp_sppr_into<R: std::io::BufRead>(
4556    rd: &mut Reader<R>,
4557    grp: &mut Group,
4558    _start: quick_xml::events::BytesStart<'static>,
4559) -> crate::Result<()> {
4560    let mut buf = Vec::new();
4561    loop {
4562        match rd.read_event_into(&mut buf) {
4563            Ok(Event::Empty(e)) => {
4564                let name = e.name();
4565                let local = local_name(name.as_ref());
4566                if local == b"off" {
4567                    let (mut x, mut y) = (None, None);
4568                    for a in e.attributes().flatten() {
4569                        match a.key.as_ref() {
4570                            b"x" => {
4571                                x = a
4572                                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
4573                                    .unwrap_or_default()
4574                                    .parse()
4575                                    .ok()
4576                            }
4577                            b"y" => {
4578                                y = a
4579                                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
4580                                    .unwrap_or_default()
4581                                    .parse()
4582                                    .ok()
4583                            }
4584                            _ => {}
4585                        }
4586                    }
4587                    if let (Some(x), Some(y)) = (x, y) {
4588                        grp.off = (Emu(x), Emu(y));
4589                    }
4590                } else if local == b"ext" {
4591                    let (mut cx, mut cy) = (None, None);
4592                    for a in e.attributes().flatten() {
4593                        match a.key.as_ref() {
4594                            b"cx" => {
4595                                cx = a
4596                                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
4597                                    .unwrap_or_default()
4598                                    .parse()
4599                                    .ok()
4600                            }
4601                            b"cy" => {
4602                                cy = a
4603                                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
4604                                    .unwrap_or_default()
4605                                    .parse()
4606                                    .ok()
4607                            }
4608                            _ => {}
4609                        }
4610                    }
4611                    if let (Some(cx), Some(cy)) = (cx, cy) {
4612                        grp.ext = (Emu(cx), Emu(cy));
4613                    }
4614                }
4615                // chOff / chExt 暂不解析(组合内部坐标系,0.1.0 写出时固定为 0,0)
4616            }
4617            Ok(Event::End(e)) => {
4618                if local_name(e.name().as_ref()) == b"grpSpPr" {
4619                    return Ok(());
4620                }
4621            }
4622            Ok(Event::Eof) => return Ok(()),
4623            _ => {}
4624        }
4625        buf.clear();
4626    }
4627}
4628
4629/// 解析 `<p:graphicFrame>` 元素(图形框:表格/图表)。
4630///
4631/// # 元素结构(OOXML)
4632///
4633/// ```text
4634/// <p:graphicFrame>
4635///   <p:nvGraphicFramePr>
4636///     <p:cNvPr id="..." name="..."/>
4637///     <p:cNvGraphicFramePr/>
4638///     <p:nvPr/>
4639///   </p:nvGraphicFramePr>
4640///   <p:xfrm>
4641///     <a:off x="..." y="..."/>
4642///     <a:ext cx="..." cy="..."/>
4643///   </p:xfrm>
4644///   <a:graphic>
4645///     <a:graphicData uri="...">
4646///       <a:tbl>...</a:tbl>     ← 或 chart / smartArt
4647///     </a:graphicData>
4648///   </a:graphic>
4649///   <p:extLst>...</p:extLst>   ← 可选
4650/// </p:graphicFrame>
4651/// ```
4652///
4653/// # 行为
4654/// - 提取 `id` / `name`;
4655/// - 解析 `<p:xfrm>` 内的 off/ext(写入 `properties.xfrm`);
4656/// - 解析 `<a:graphicData uri="...">` 内的 `<a:tbl>`(仅表格,其它类型暂留空)。
4657pub fn parse_graphic_frame(xml: &str) -> crate::Result<GraphicFrame> {
4658    let mut frame = GraphicFrame::default();
4659    let mut rd = Reader::from_str(xml);
4660    rd.config_mut().trim_text(true);
4661    let mut buf = Vec::new();
4662    loop {
4663        match rd.read_event_into(&mut buf) {
4664            Ok(Event::Start(e)) => {
4665                let name = e.name();
4666                let local = local_name(name.as_ref());
4667                if local == b"graphicFrame" {
4668                    // 根元素 <p:graphicFrame>:仅标记进入
4669                } else if local == b"nvGraphicFramePr" {
4670                    // 手动遍历 nvGraphicFramePr 子元素提取 cNvPr
4671                    let mut nv_depth = 1i32;
4672                    loop {
4673                        match rd.read_event_into(&mut buf) {
4674                            Ok(Event::Start(_)) => nv_depth += 1,
4675                            Ok(Event::End(_)) => {
4676                                nv_depth -= 1;
4677                                if nv_depth == 0 {
4678                                    break;
4679                                }
4680                            }
4681                            Ok(Event::Empty(e2)) => {
4682                                let name2 = e2.name();
4683                                let local2 = local_name(name2.as_ref());
4684                                if local2 == b"cNvPr" {
4685                                    for a in e2.attributes().flatten() {
4686                                        match a.key.as_ref() {
4687                                            b"id" => {
4688                                                if let Ok(v) = a
4689                                                    .normalized_value(
4690                                                        quick_xml::XmlVersion::Implicit1_0,
4691                                                    )
4692                                                    .unwrap_or_default()
4693                                                    .parse::<u32>()
4694                                                {
4695                                                    frame.id = v;
4696                                                }
4697                                            }
4698                                            b"name" => {
4699                                                frame.name = a
4700                                                    .normalized_value(
4701                                                        quick_xml::XmlVersion::Implicit1_0,
4702                                                    )
4703                                                    .unwrap_or_default()
4704                                                    .to_string();
4705                                            }
4706                                            _ => {}
4707                                        }
4708                                    }
4709                                }
4710                            }
4711                            Ok(Event::Eof) => break,
4712                            Err(_) => break,
4713                            _ => {}
4714                        }
4715                        buf.clear();
4716                    }
4717                } else if local == b"xfrm" {
4718                    // 先提取 xfrm 自身的属性(rot/flipH/flipV)
4719                    for a in e.attributes().flatten() {
4720                        match a.key.as_ref() {
4721                            b"rot" => {
4722                                if let Ok(v) = a
4723                                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
4724                                    .unwrap_or_default()
4725                                    .parse::<i32>()
4726                                {
4727                                    frame.properties.xfrm.rot = Some(v);
4728                                }
4729                            }
4730                            b"flipH" => {
4731                                let v = a
4732                                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
4733                                    .unwrap_or_default();
4734                                if v == "1" {
4735                                    frame.properties.xfrm.flip_h = true;
4736                                }
4737                            }
4738                            b"flipV" => {
4739                                let v = a
4740                                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
4741                                    .unwrap_or_default();
4742                                if v == "1" {
4743                                    frame.properties.xfrm.flip_v = true;
4744                                }
4745                            }
4746                            _ => {}
4747                        }
4748                    }
4749                    // 再解析子元素(off/ext)
4750                    parse_xfrm_into(&mut rd, &mut frame.properties);
4751                } else if local == b"graphic" {
4752                    // 解析 <a:graphic> 内的 <a:graphicData uri="...">
4753                    parse_graphic_into(&mut rd, &mut frame)?;
4754                } else {
4755                    // extLst 等暂不解析,吞掉
4756                    let _ = collect_full_element(&mut rd, e.into_owned());
4757                }
4758            }
4759            Ok(Event::End(e)) => {
4760                if local_name(e.name().as_ref()) == b"graphicFrame" {
4761                    break;
4762                }
4763            }
4764            Ok(Event::Eof) => break,
4765            Err(e) => return Err(crate::Error::Xml(format!("graphicFrame parse: {e}"))),
4766            _ => {}
4767        }
4768        buf.clear();
4769    }
4770    Ok(frame)
4771}
4772
4773/// 解析 `<a:graphic>` 内的 `<a:graphicData uri="...">`,根据 uri 分发到表格解析器。
4774fn parse_graphic_into<R: std::io::BufRead>(
4775    rd: &mut Reader<R>,
4776    frame: &mut GraphicFrame,
4777) -> crate::Result<()> {
4778    let mut buf = Vec::new();
4779    loop {
4780        match rd.read_event_into(&mut buf) {
4781            Ok(Event::Start(e)) => {
4782                let name = e.name();
4783                let local = local_name(name.as_ref());
4784                if local == b"graphicData" {
4785                    // 读取 uri 属性判断图形类型
4786                    let mut uri = String::new();
4787                    for a in e.attributes().flatten() {
4788                        if a.key.as_ref() == b"uri" {
4789                            uri = a
4790                                .normalized_value(quick_xml::XmlVersion::Implicit1_0)
4791                                .unwrap_or_default()
4792                                .to_string();
4793                        }
4794                    }
4795                    // 根据 uri 分发:表格 / 图表 / smartArt
4796                    if uri == "http://schemas.openxmlformats.org/drawingml/2006/table" {
4797                        // 收集 <a:tbl> 子元素
4798                        loop {
4799                            match rd.read_event_into(&mut buf) {
4800                                Ok(Event::Start(e2)) => {
4801                                    if local_name(e2.name().as_ref()) == b"tbl" {
4802                                        let tbl_xml = collect_full_element(rd, e2.into_owned())?;
4803                                        if let Ok(tbl) = parse_table(&tbl_xml) {
4804                                            frame.graphic = crate::oxml::shape::Graphic::Table(tbl);
4805                                        }
4806                                    } else {
4807                                        let _ = collect_full_element(rd, e2.into_owned());
4808                                    }
4809                                }
4810                                Ok(Event::End(e2)) => {
4811                                    if local_name(e2.name().as_ref()) == b"graphicData" {
4812                                        break;
4813                                    }
4814                                }
4815                                Ok(Event::Eof) => break,
4816                                _ => {}
4817                            }
4818                            buf.clear();
4819                        }
4820                    } else if uri == "http://schemas.openxmlformats.org/drawingml/2006/diagram" {
4821                        // SmartArt 最小保留(TODO-037):保留完整 <a:graphicData> 元素 XML。
4822                        // collect_full_element 会从当前 Start 事件(graphicData)开始,
4823                        // 收集到对应 End 事件,返回完整 XML(含外壳)。
4824                        let raw_xml = collect_full_element(rd, e.into_owned())?;
4825                        // 从 raw_xml 中提取 dgm:relIds 的 4 个关系 id(r:dm / r:lo / r:qs / r:cs)。
4826                        // 这些 id 仅供调用方查询,序列化时不单独使用(直接走 raw_xml)。
4827                        let (dm_rid, lo_rid, qs_rid, cs_rid) = parse_smartart_rel_ids(&raw_xml);
4828                        frame.graphic = crate::oxml::shape::Graphic::SmartArt(
4829                            crate::oxml::shape::SmartArtRef {
4830                                raw_xml,
4831                                dm_rid,
4832                                lo_rid,
4833                                qs_rid,
4834                                cs_rid,
4835                            },
4836                        );
4837                    } else if uri == "http://schemas.openxmlformats.org/drawingml/2006/chart" {
4838                        // 图表读路径(TODO-004):slide 的 graphicFrame 仅含 `<c:chart r:id="rIdX"/>`
4839                        // 引用,真正的图表内容在独立的 `/ppt/charts/chartN.xml` part 中。
4840                        // 这里仅提取 r:id 构造 Chart 占位模型,由 `Presentation::from_opc`
4841                        // 读取 chartN.xml 后调用 `Chart::parse_from_xml` 填充真实类型与数据。
4842                        let raw_xml = collect_full_element(rd, e.into_owned())?;
4843                        let rid = parse_chart_rid(&raw_xml);
4844                        frame.graphic =
4845                            crate::oxml::shape::Graphic::Chart(crate::oxml::chart::Chart::new(
4846                                crate::oxml::chart::ChartType::Column,
4847                                crate::oxml::chart::ChartData::default(),
4848                            ));
4849                        // 把 rid 写入 Chart.rid,from_opc 阶段根据 rid 查 chart_rel_map 读 chartN.xml。
4850                        if let crate::oxml::shape::Graphic::Chart(c) = &mut frame.graphic {
4851                            c.rid = rid;
4852                        }
4853                    } else {
4854                        // 其它类型(chart 等)暂不解析,吞掉
4855                        let _ = collect_full_element(rd, e.into_owned());
4856                    }
4857                } else {
4858                    let _ = collect_full_element(rd, e.into_owned());
4859                }
4860            }
4861            Ok(Event::End(e)) => {
4862                if local_name(e.name().as_ref()) == b"graphic" {
4863                    return Ok(());
4864                }
4865            }
4866            Ok(Event::Eof) => return Ok(()),
4867            _ => {}
4868        }
4869        buf.clear();
4870    }
4871}
4872
4873/// 从 SmartArt 的 `<a:graphicData>` 完整 XML 中提取 `<dgm:relIds>` 的 4 个关系 id(TODO-037)。
4874///
4875/// # 元素结构(OOXML)
4876///
4877/// ```text
4878/// <a:graphicData uri=".../diagram">
4879///   <dgm:relIds r:dm="rId1" r:lo="rId2" r:qs="rId3" r:cs="rId4"/>
4880/// </a:graphicData>
4881/// ```
4882///
4883/// # 行为
4884///
4885/// - 用简单的字符串查找提取 `r:dm` / `r:lo` / `r:qs` / `r:cs` 属性值;
4886/// - 不做完整 XML 解析(避免 quick-xml 命名空间处理的复杂性,且 raw_xml 已 byte-exact 保留);
4887/// - 找不到时对应字段返回 `None`。
4888///
4889/// # 返回值
4890///
4891/// `(dm_rid, lo_rid, qs_rid, cs_rid)` 四元组,每个字段为 `Option<String>`。
4892fn parse_smartart_rel_ids(
4893    raw_xml: &str,
4894) -> (
4895    Option<String>,
4896    Option<String>,
4897    Option<String>,
4898    Option<String>,
4899) {
4900    /// 在 XML 字符串中查找 `attr_name="value"` 模式的 value。
4901    ///
4902    /// 仅做简单字符串查找,不处理转义(OOXML 关系 id 不含特殊字符,安全)。
4903    fn find_attr(xml: &str, attr_name: &str) -> Option<String> {
4904        // 查找 `attr_name="` 模式(注意 r:dm 等带冒号的属性名)
4905        let pattern = format!("{}=\"", attr_name);
4906        let pos = xml.find(&pattern)?;
4907        let value_start = pos + pattern.len();
4908        let value_end = xml[value_start..].find('"')?;
4909        Some(xml[value_start..value_start + value_end].to_string())
4910    }
4911
4912    let dm = find_attr(raw_xml, "r:dm");
4913    let lo = find_attr(raw_xml, "r:lo");
4914    let qs = find_attr(raw_xml, "r:qs");
4915    let cs = find_attr(raw_xml, "r:cs");
4916    (dm, lo, qs, cs)
4917}
4918
4919/// 从 chart 的 `<a:graphicData>` 完整 XML 中提取 `<c:chart r:id="..."/>` 的关系 id(TODO-004 读路径)。
4920///
4921/// # 元素结构(OOXML)
4922///
4923/// ```text
4924/// <a:graphicData uri=".../chart">
4925///   <c:chart xmlns:c="..." xmlns:r="..." r:id="rId1"/>
4926/// </a:graphicData>
4927/// ```
4928///
4929/// # 行为
4930///
4931/// - 用简单的字符串查找提取 `r:id` 属性值;
4932/// - 兼容 `r:id="..."` 与 `r:id = "..."`(带空格,罕见)两种写法;
4933/// - 找不到时返回空字符串(`from_opc` 阶段会跳过该 chart 的 part 读取)。
4934///
4935/// # 返回值
4936///
4937/// 关系 id 字符串(如 `"rId1"`)。找不到时返回空字符串。
4938fn parse_chart_rid(raw_xml: &str) -> String {
4939    // 优先匹配 `r:id="value"` 模式(OOXML 关系 id 不含特殊字符,安全)
4940    let pattern = "r:id=\"";
4941    if let Some(pos) = raw_xml.find(pattern) {
4942        let value_start = pos + pattern.len();
4943        if let Some(end) = raw_xml[value_start..].find('"') {
4944            return raw_xml[value_start..value_start + end].to_string();
4945        }
4946    }
4947    String::new()
4948}
4949
4950/// 解析 `<a:tbl>` 元素为 [`crate::oxml::table::Table`]。
4951///
4952/// # 元素结构
4953///
4954/// ```text
4955/// <a:tbl>
4956///   <a:tblPr>...</a:tblPr>
4957///   <a:tblGrid>
4958///     <a:gridCol w="..."/>
4959///     ...
4960///   </a:tblGrid>
4961///   <a:tr h="...">
4962///     <a:tc>
4963///       <a:txBody>...</a:txBody>
4964///       <a:tcPr anchor="..." marT="..." .../>
4965///     </a:tc>
4966///     ...
4967///   </a:tr>
4968///   ...
4969/// </a:tbl>
4970/// ```
4971fn parse_table(xml: &str) -> crate::Result<crate::oxml::table::Table> {
4972    use crate::oxml::table::{Col, Table};
4973
4974    let mut tbl = Table::default();
4975    let mut rd = Reader::from_str(xml);
4976    rd.config_mut().trim_text(true);
4977    let mut buf = Vec::new();
4978    // 收集行和列的原始 XML
4979    let mut tr_bufs: Vec<String> = Vec::new();
4980    let mut grid_col_widths: Vec<Emu> = Vec::new();
4981
4982    loop {
4983        match rd.read_event_into(&mut buf) {
4984            Ok(Event::Start(e)) => {
4985                let name = e.name();
4986                let local = local_name(name.as_ref());
4987                if local == b"tbl" {
4988                    // 根元素
4989                } else if local == b"tblPr" {
4990                    // 解析 tblPr 内的 tblLook 属性和 tableStyleId
4991                    let inner = collect_full_element(&mut rd, e.into_owned())?;
4992                    parse_tbl_pr_into(&inner, &mut tbl);
4993                } else if local == b"tblGrid" {
4994                    // 解析 tblGrid 内的 gridCol
4995                    loop {
4996                        match rd.read_event_into(&mut buf) {
4997                            Ok(Event::Empty(e2)) => {
4998                                if local_name(e2.name().as_ref()) == b"gridCol" {
4999                                    let mut w: Option<i64> = None;
5000                                    for a in e2.attributes().flatten() {
5001                                        if a.key.as_ref() == b"w" {
5002                                            w = a
5003                                                .normalized_value(
5004                                                    quick_xml::XmlVersion::Implicit1_0,
5005                                                )
5006                                                .unwrap_or_default()
5007                                                .parse()
5008                                                .ok();
5009                                        }
5010                                    }
5011                                    grid_col_widths.push(Emu(w.unwrap_or(0)));
5012                                }
5013                            }
5014                            Ok(Event::End(e2)) => {
5015                                if local_name(e2.name().as_ref()) == b"tblGrid" {
5016                                    break;
5017                                }
5018                            }
5019                            Ok(Event::Eof) => break,
5020                            _ => {}
5021                        }
5022                        buf.clear();
5023                    }
5024                } else if local == b"tr" {
5025                    let inner = collect_full_element(&mut rd, e.into_owned())?;
5026                    tr_bufs.push(inner);
5027                } else {
5028                    let _ = collect_full_element(&mut rd, e.into_owned());
5029                }
5030            }
5031            Ok(Event::Eof) => break,
5032            Err(e) => return Err(crate::Error::Xml(format!("tbl parse: {e}"))),
5033            _ => {}
5034        }
5035        buf.clear();
5036    }
5037
5038    // 填充列
5039    tbl.cols = grid_col_widths.iter().map(|w| Col { width: *w }).collect();
5040
5041    // 解析行
5042    for tr_xml in tr_bufs {
5043        if let Ok(row) = parse_table_row(&tr_xml) {
5044            tbl.rows.push(row);
5045        }
5046    }
5047
5048    Ok(tbl)
5049}
5050
5051/// 解析 `<a:tblPr>` 内的 `<a:tblLook>` 属性和 `<a:tableStyleId>` 元素。
5052///
5053/// 将解析结果写入 `tbl.tbl_look` 和 `tbl.table_style`。
5054fn parse_tbl_pr_into(xml: &str, tbl: &mut crate::oxml::table::Table) {
5055    use crate::oxml::table::TableStyle;
5056
5057    let look = &mut tbl.tbl_look;
5058    let mut rd = Reader::from_str(xml);
5059    let mut buf = Vec::new();
5060    loop {
5061        match rd.read_event_into(&mut buf) {
5062            Ok(Event::Empty(e)) => {
5063                if local_name(e.name().as_ref()) == b"tblLook" {
5064                    for a in e.attributes().flatten() {
5065                        let v = a
5066                            .normalized_value(quick_xml::XmlVersion::Implicit1_0)
5067                            .unwrap_or_default();
5068                        match a.key.as_ref() {
5069                            b"val" => look.val = v.to_string(),
5070                            b"firstRow" => look.first_row = v == "1",
5071                            b"lastRow" => look.last_row = v == "1",
5072                            b"firstColumn" => look.first_column = v == "1",
5073                            b"lastColumn" => look.last_column = v == "1",
5074                            b"noHBand" => look.no_h_band = v == "1",
5075                            b"noVBand" => look.no_v_band = v == "1",
5076                            _ => {}
5077                        }
5078                    }
5079                }
5080                // tableStyleId 自闭合形式(罕见但合法:<a:tableStyleId/>)
5081                if local_name(e.name().as_ref()) == b"tableStyleId" {
5082                    // 自闭合无文本内容,跳过
5083                }
5084            }
5085            Ok(Event::Start(e)) => {
5086                if local_name(e.name().as_ref()) == b"tableStyleId" {
5087                    // 读取文本内容(GUID)
5088                    let mut guid = String::new();
5089                    loop {
5090                        match rd.read_event_into(&mut buf) {
5091                            Ok(Event::Text(t)) => {
5092                                guid.push_str(&t.decode().unwrap_or_default());
5093                            }
5094                            Ok(Event::End(e2)) => {
5095                                if local_name(e2.name().as_ref()) == b"tableStyleId" {
5096                                    break;
5097                                }
5098                            }
5099                            Ok(Event::Eof) => break,
5100                            _ => {}
5101                        }
5102                        buf.clear();
5103                    }
5104                    let trimmed = guid.trim();
5105                    if !trimmed.is_empty() {
5106                        tbl.table_style = Some(TableStyle::new(trimmed));
5107                    }
5108                }
5109            }
5110            Ok(Event::Eof) => break,
5111            _ => {}
5112        }
5113        buf.clear();
5114    }
5115}
5116
5117/// 解析 `<a:tr>` 元素为 [`Row`]。
5118fn parse_table_row(xml: &str) -> crate::Result<crate::oxml::table::Row> {
5119    use crate::oxml::table::Row;
5120
5121    let mut row = Row::default();
5122    let mut rd = Reader::from_str(xml);
5123    rd.config_mut().trim_text(true);
5124    let mut buf = Vec::new();
5125    // 行高
5126    // 从根元素 <a:tr h="..."> 读取属性
5127    let mut tc_bufs: Vec<String> = Vec::new();
5128
5129    loop {
5130        match rd.read_event_into(&mut buf) {
5131            Ok(Event::Start(e)) => {
5132                let name = e.name();
5133                let local = local_name(name.as_ref());
5134                if local == b"tr" {
5135                    // 读取行高属性
5136                    for a in e.attributes().flatten() {
5137                        if a.key.as_ref() == b"h" {
5138                            if let Ok(v) = a
5139                                .normalized_value(quick_xml::XmlVersion::Implicit1_0)
5140                                .unwrap_or_default()
5141                                .parse::<i64>()
5142                            {
5143                                row.height = Emu(v);
5144                            }
5145                        }
5146                    }
5147                } else if local == b"tc" {
5148                    let inner = collect_full_element(&mut rd, e.into_owned())?;
5149                    tc_bufs.push(inner);
5150                } else {
5151                    let _ = collect_full_element(&mut rd, e.into_owned());
5152                }
5153            }
5154            Ok(Event::Empty(_e)) => {
5155                // 自闭合 tr(罕见)
5156            }
5157            Ok(Event::End(e)) => {
5158                if local_name(e.name().as_ref()) == b"tr" {
5159                    break;
5160                }
5161            }
5162            Ok(Event::Eof) => break,
5163            Err(e) => return Err(crate::Error::Xml(format!("tr parse: {e}"))),
5164            _ => {}
5165        }
5166        buf.clear();
5167    }
5168
5169    // 解析单元格
5170    for tc_xml in tc_bufs {
5171        if let Ok(cell) = parse_table_cell(&tc_xml) {
5172            row.cells.push(cell);
5173        }
5174    }
5175
5176    Ok(row)
5177}
5178
5179/// 解析 `<a:tc>` 元素为 [`Cell`]。
5180fn parse_table_cell(xml: &str) -> crate::Result<crate::oxml::table::Cell> {
5181    let mut cell = crate::oxml::table::Cell::default();
5182    let mut rd = Reader::from_str(xml);
5183    rd.config_mut().trim_text(true);
5184    let mut buf = Vec::new();
5185
5186    loop {
5187        match rd.read_event_into(&mut buf) {
5188            Ok(Event::Start(e)) => {
5189                let name = e.name();
5190                let local = local_name(name.as_ref());
5191                if local == b"tc" {
5192                    // 读取 gridSpan / rowSpan / hMerge / vMerge 属性
5193                    for a in e.attributes().flatten() {
5194                        let v = a
5195                            .normalized_value(quick_xml::XmlVersion::Implicit1_0)
5196                            .unwrap_or_default();
5197                        match a.key.as_ref() {
5198                            b"gridSpan" => {
5199                                if let Ok(g) = v.parse::<u32>() {
5200                                    cell.grid_span = g;
5201                                }
5202                            }
5203                            b"rowSpan" => {
5204                                if let Ok(r) = v.parse::<u32>() {
5205                                    cell.row_span = r;
5206                                }
5207                            }
5208                            b"hMerge" if v == "1" => {
5209                                cell.h_merge = true;
5210                            }
5211                            b"vMerge" if v == "1" => {
5212                                cell.v_merge = true;
5213                            }
5214                            _ => {}
5215                        }
5216                    }
5217                } else if local == b"txBody" {
5218                    let inner = collect_full_element(&mut rd, e.into_owned())?;
5219                    cell.text = parse_txbody(&inner)?;
5220                } else if local == b"tcPr" {
5221                    // 解析 tcPr 属性(marT/marL/marB/marR/anchor)和子元素(lnL/lnR/lnT/lnB/solidFill)
5222                    let inner = collect_full_element(&mut rd, e.into_owned())?;
5223                    parse_tc_pr_into(&inner, &mut cell);
5224                } else {
5225                    let _ = collect_full_element(&mut rd, e.into_owned());
5226                }
5227            }
5228            Ok(Event::End(e)) => {
5229                if local_name(e.name().as_ref()) == b"tc" {
5230                    break;
5231                }
5232            }
5233            Ok(Event::Eof) => break,
5234            Err(e) => return Err(crate::Error::Xml(format!("tc parse: {e}"))),
5235            _ => {}
5236        }
5237        buf.clear();
5238    }
5239
5240    Ok(cell)
5241}
5242
5243/// 解析 `<a:tcPr>` 的属性和子元素到 [`Cell`]。
5244fn parse_tc_pr_into(xml: &str, cell: &mut crate::oxml::table::Cell) {
5245    use crate::oxml::table::VerticalAnchor;
5246
5247    let mut rd = Reader::from_str(xml);
5248    let mut buf = Vec::new();
5249
5250    loop {
5251        match rd.read_event_into(&mut buf) {
5252            Ok(Event::Start(e)) => {
5253                let name = e.name();
5254                let local = local_name(name.as_ref());
5255                // tcPr 自身的属性(marT/marL/marB/marR/anchor)
5256                // 注意:根元素是 tcPr,其属性在 Start 事件上
5257                if local == b"tcPr" {
5258                    for a in e.attributes().flatten() {
5259                        let v = a
5260                            .normalized_value(quick_xml::XmlVersion::Implicit1_0)
5261                            .unwrap_or_default();
5262                        match a.key.as_ref() {
5263                            b"marT" => {
5264                                if let Ok(m) = v.parse::<i64>() {
5265                                    cell.margin.0 = Some(Emu(m));
5266                                }
5267                            }
5268                            b"marL" => {
5269                                if let Ok(m) = v.parse::<i64>() {
5270                                    cell.margin.1 = Some(Emu(m));
5271                                }
5272                            }
5273                            b"marB" => {
5274                                if let Ok(m) = v.parse::<i64>() {
5275                                    cell.margin.2 = Some(Emu(m));
5276                                }
5277                            }
5278                            b"marR" => {
5279                                if let Ok(m) = v.parse::<i64>() {
5280                                    cell.margin.3 = Some(Emu(m));
5281                                }
5282                            }
5283                            b"anchor" => {
5284                                // 使用 &*v 解引用 Cow<str> 为 &str,避免不稳定的 str::as_str()
5285                                cell.anchor = match &*v {
5286                                    "t" => VerticalAnchor::Top,
5287                                    "ctr" => VerticalAnchor::Middle,
5288                                    "b" => VerticalAnchor::Bottom,
5289                                    _ => VerticalAnchor::Middle,
5290                                };
5291                            }
5292                            _ => {}
5293                        }
5294                    }
5295                } else if local == b"solidFill" {
5296                    // 单元格填充色
5297                    if let Ok(color) = parse_solid_fill(&mut rd, e.into_owned()) {
5298                        cell.fill = color;
5299                    }
5300                } else if local == b"lnL" {
5301                    // 单元格左边框:分支独立,避免 e.into_owned() 后 local 引用失效
5302                    let border = parse_cell_border(&mut rd, e.into_owned());
5303                    cell.border_left = Some(border);
5304                } else if local == b"lnR" {
5305                    let border = parse_cell_border(&mut rd, e.into_owned());
5306                    cell.border_right = Some(border);
5307                } else if local == b"lnT" {
5308                    let border = parse_cell_border(&mut rd, e.into_owned());
5309                    cell.border_top = Some(border);
5310                } else if local == b"lnB" {
5311                    let border = parse_cell_border(&mut rd, e.into_owned());
5312                    cell.border_bottom = Some(border);
5313                } else {
5314                    let _ = collect_full_element(&mut rd, e.into_owned());
5315                }
5316            }
5317            Ok(Event::Empty(e)) => {
5318                let name = e.name();
5319                let local = local_name(name.as_ref());
5320                if local == b"tcPr" {
5321                    // 自闭合 tcPr:仅属性
5322                    for a in e.attributes().flatten() {
5323                        let v = a
5324                            .normalized_value(quick_xml::XmlVersion::Implicit1_0)
5325                            .unwrap_or_default();
5326                        match a.key.as_ref() {
5327                            b"marT" => {
5328                                if let Ok(m) = v.parse::<i64>() {
5329                                    cell.margin.0 = Some(Emu(m));
5330                                }
5331                            }
5332                            b"marL" => {
5333                                if let Ok(m) = v.parse::<i64>() {
5334                                    cell.margin.1 = Some(Emu(m));
5335                                }
5336                            }
5337                            b"marB" => {
5338                                if let Ok(m) = v.parse::<i64>() {
5339                                    cell.margin.2 = Some(Emu(m));
5340                                }
5341                            }
5342                            b"marR" => {
5343                                if let Ok(m) = v.parse::<i64>() {
5344                                    cell.margin.3 = Some(Emu(m));
5345                                }
5346                            }
5347                            b"anchor" => {
5348                                // 使用 &*v 解引用 Cow<str> 为 &str,避免不稳定的 str::as_str()
5349                                cell.anchor = match &*v {
5350                                    "t" => VerticalAnchor::Top,
5351                                    "ctr" => VerticalAnchor::Middle,
5352                                    "b" => VerticalAnchor::Bottom,
5353                                    _ => VerticalAnchor::Middle,
5354                                };
5355                            }
5356                            _ => {}
5357                        }
5358                    }
5359                }
5360            }
5361            Ok(Event::End(e)) => {
5362                if local_name(e.name().as_ref()) == b"tcPr" {
5363                    return;
5364                }
5365            }
5366            Ok(Event::Eof) => return,
5367            _ => {}
5368        }
5369        buf.clear();
5370    }
5371}
5372
5373/// 解析单元格边框 `<a:lnL>` / `<a:lnR>` / `<a:lnT>` / `<a:lnB>` 为 [`CellBorder`]。
5374fn parse_cell_border<R: std::io::BufRead>(
5375    rd: &mut Reader<R>,
5376    start: quick_xml::events::BytesStart<'static>,
5377) -> crate::oxml::table::CellBorder {
5378    use crate::oxml::table::CellBorder;
5379    let mut border = CellBorder::default();
5380    // 读取 w 属性
5381    for a in start.attributes().flatten() {
5382        if a.key.as_ref() == b"w" {
5383            if let Ok(v) = a
5384                .normalized_value(quick_xml::XmlVersion::Implicit1_0)
5385                .unwrap_or_default()
5386                .parse::<i64>()
5387            {
5388                border.width = Emu(v);
5389            }
5390        }
5391    }
5392    let mut buf = Vec::new();
5393    loop {
5394        match rd.read_event_into(&mut buf) {
5395            Ok(Event::Empty(e)) => {
5396                let name = e.name();
5397                let local = local_name(name.as_ref());
5398                if local == b"noFill" {
5399                    border.no_fill = true;
5400                }
5401                // solidFill 自闭合(<a:solidFill/>)无颜色子元素,跳过
5402            }
5403            Ok(Event::Start(e)) => {
5404                let name = e.name();
5405                let local = local_name(name.as_ref());
5406                if local == b"solidFill" {
5407                    if let Ok(color) = parse_solid_fill(rd, e.into_owned()) {
5408                        border.color = color;
5409                    }
5410                } else {
5411                    let _ = collect_full_element(rd, e.into_owned());
5412                }
5413            }
5414            Ok(Event::End(e)) => {
5415                // 返回到调用者:边框元素结束
5416                let name = e.name();
5417                let local = local_name(name.as_ref());
5418                if local == b"lnL" || local == b"lnR" || local == b"lnT" || local == b"lnB" {
5419                    return border;
5420                }
5421            }
5422            Ok(Event::Eof) => return border,
5423            _ => {}
5424        }
5425        buf.clear();
5426    }
5427}
5428
5429/// 从 `<p:notes>` / `<p:notesSlide>` 元素解析出 `TextBody`。
5430///
5431/// 元素结构(OOXML 严格顺序):
5432///
5433/// ```text
5434/// <p:notes>
5435///   <p:cSld>
5436///     <p:spTree>
5437///       <p:nvGrpSpPr/>
5438///       <p:grpSpPr/>
5439///       <p:sp>
5440///         <p:nvSpPr>...</p:nvSpPr>
5441///         <p:spPr>...</p:spPr>
5442///         <p:txBody>...</p:txBody>   ← 备注文本
5443///       </p:sp>
5444///     </p:spTree>
5445///   </p:cSld>
5446/// </p:notes>
5447/// ```
5448///
5449/// # 行为
5450/// - 仅取第一个 `p:sp` 内的 `p:txBody`,**忽略** `p:sp` 之外的其它内容;
5451/// - 若没有 `p:sp` 或没有 `p:txBody`,返回 `Ok(TextBody::new())`(空备注)。
5452///
5453/// # 错误
5454/// - [`crate::Error::Xml`]:XML 语法错误。
5455pub fn parse_notes(xml: &str) -> crate::Result<TextBody> {
5456    let mut rd = Reader::from_str(xml);
5457    rd.config_mut().trim_text(true);
5458    let mut buf = Vec::new();
5459    let mut tx_buf: Option<String> = None;
5460    loop {
5461        match rd.read_event_into(&mut buf) {
5462            Ok(Event::Start(e)) => {
5463                let name = e.name();
5464                let local = local_name(name.as_ref());
5465                if local == b"txBody" && tx_buf.is_none() {
5466                    let inner = collect_full_element(&mut rd, e.into_owned())?;
5467                    tx_buf = Some(inner);
5468                }
5469                // 其它 Start 事件(notes / cSld / spTree / sp / nvSpPr / spPr 等)
5470                // 不调用 collect_full_element——否则会把 txBody 后代一起吞掉。
5471                // 让子元素在后续迭代中自然展开,直到遇到 txBody 为止。
5472            }
5473            Ok(Event::Eof) => break,
5474            Err(e) => return Err(crate::Error::Xml(format!("notes parse: {e}"))),
5475            _ => {}
5476        }
5477        buf.clear();
5478    }
5479    if let Some(buf) = tx_buf {
5480        parse_txbody(&buf)
5481    } else {
5482        Ok(TextBody::new())
5483    }
5484}
5485
5486/// 从 `commentN.xml` 解析评论列表(`<p:cmLst>`)。
5487///
5488/// # 行为
5489/// - 遍历所有 `<p:cm>` 元素,提取 `authorId` / `dt` / `idx` 属性;
5490/// - 对每个 `<p:cm>` 内部的 `<p:pos x="..." y="..."/>` 和 `<p:text>...</p:text>` 分别解析;
5491/// - 忽略无法识别的子元素。
5492///
5493/// # 错误
5494/// - [`crate::Error::Xml`]:XML 语法错误。
5495pub fn parse_comments(xml: &str) -> crate::Result<crate::oxml::comments::CommentList> {
5496    use crate::oxml::comments::{Comment, CommentList};
5497
5498    let mut lst = CommentList::new();
5499    let mut rd = Reader::from_str(xml);
5500    rd.config_mut().trim_text(true);
5501    let mut buf = Vec::new();
5502    // 当前正在解析的 <p:cm> 的属性
5503    let mut cur_author_id: Option<u32> = None;
5504    let mut cur_dt: String = String::new();
5505    let mut cur_idx: Option<u32> = None;
5506    // 当前是否在 <p:cm> 内部
5507    let mut in_cm = false;
5508    // 当前是否在 <p:text> 内部(用于收集文本)
5509    let mut in_text = false;
5510    // 当前 <p:cm> 的 pos
5511    let mut cur_pos_x: i64 = 0;
5512    let mut cur_pos_y: i64 = 0;
5513    let mut cur_text = String::new();
5514
5515    loop {
5516        match rd.read_event_into(&mut buf) {
5517            Ok(Event::Start(e)) => {
5518                let name = e.name();
5519                let local = local_name(name.as_ref());
5520                if local == b"cm" {
5521                    // 进入新的 <p:cm>:重置状态
5522                    in_cm = true;
5523                    cur_author_id = None;
5524                    cur_dt.clear();
5525                    cur_idx = None;
5526                    cur_pos_x = 0;
5527                    cur_pos_y = 0;
5528                    cur_text.clear();
5529                    // 提取属性
5530                    for a in e.attributes().flatten() {
5531                        let key = a.key.as_ref();
5532                        let val = a
5533                            .normalized_value(quick_xml::XmlVersion::Implicit1_0)
5534                            .unwrap_or_default()
5535                            .to_string();
5536                        match key {
5537                            b"authorId" => {
5538                                cur_author_id = val.parse::<u32>().ok();
5539                            }
5540                            b"dt" => {
5541                                cur_dt = val;
5542                            }
5543                            b"idx" => {
5544                                cur_idx = val.parse::<u32>().ok();
5545                            }
5546                            _ => {}
5547                        }
5548                    }
5549                } else if in_cm && local == b"text" {
5550                    in_text = true;
5551                    cur_text.clear();
5552                }
5553            }
5554            Ok(Event::Empty(e)) => {
5555                let name = e.name();
5556                let local = local_name(name.as_ref());
5557                if in_cm && local == b"pos" {
5558                    // 自闭合 <p:pos x="..." y="..."/>
5559                    for a in e.attributes().flatten() {
5560                        let key = a.key.as_ref();
5561                        let val = a
5562                            .normalized_value(quick_xml::XmlVersion::Implicit1_0)
5563                            .unwrap_or_default();
5564                        match key {
5565                            b"x" => {
5566                                cur_pos_x = val.parse::<i64>().unwrap_or(0);
5567                            }
5568                            b"y" => {
5569                                cur_pos_y = val.parse::<i64>().unwrap_or(0);
5570                            }
5571                            _ => {}
5572                        }
5573                    }
5574                }
5575            }
5576            Ok(Event::Text(t)) => {
5577                if in_text {
5578                    cur_text.push_str(&t.decode().unwrap_or_default());
5579                }
5580            }
5581            Ok(Event::End(e)) => {
5582                let name = e.name();
5583                let local = local_name(name.as_ref());
5584                if local == b"cm" && in_cm {
5585                    // 结束当前 <p:cm>:组装并推入列表
5586                    let c = Comment {
5587                        author_id: cur_author_id.unwrap_or(0),
5588                        date_time: std::mem::take(&mut cur_dt),
5589                        idx: cur_idx.unwrap_or(0),
5590                        pos_x: cur_pos_x,
5591                        pos_y: cur_pos_y,
5592                        text: std::mem::take(&mut cur_text),
5593                    };
5594                    lst.push(c);
5595                    in_cm = false;
5596                    in_text = false;
5597                } else if local == b"text" && in_text {
5598                    in_text = false;
5599                }
5600            }
5601            Ok(Event::Eof) => break,
5602            Err(e) => return Err(crate::Error::Xml(format!("comments parse: {e}"))),
5603            _ => {}
5604        }
5605        buf.clear();
5606    }
5607    Ok(lst)
5608}
5609
5610/// 从 `commentAuthors.xml` 解析评论作者列表(`<p:cmAuthorLst>`)。
5611///
5612/// # 错误
5613/// - [`crate::Error::Xml`]:XML 语法错误。
5614pub fn parse_comment_authors(xml: &str) -> crate::Result<crate::oxml::comments::CommentAuthorList> {
5615    use crate::oxml::comments::{CommentAuthor, CommentAuthorList};
5616
5617    let mut lst = CommentAuthorList::new();
5618    let mut rd = Reader::from_str(xml);
5619    rd.config_mut().trim_text(true);
5620    let mut buf = Vec::new();
5621
5622    loop {
5623        match rd.read_event_into(&mut buf) {
5624            Ok(Event::Start(e)) | Ok(Event::Empty(e)) => {
5625                let name = e.name();
5626                let local = local_name(name.as_ref());
5627                if local == b"cmAuthor" {
5628                    let mut id: u32 = 0;
5629                    let mut name_val = String::new();
5630                    let mut initials = String::new();
5631                    for a in e.attributes().flatten() {
5632                        let key = a.key.as_ref();
5633                        let val = a
5634                            .normalized_value(quick_xml::XmlVersion::Implicit1_0)
5635                            .unwrap_or_default()
5636                            .to_string();
5637                        match key {
5638                            b"id" => {
5639                                id = val.parse::<u32>().unwrap_or(0);
5640                            }
5641                            b"name" => {
5642                                name_val = val;
5643                            }
5644                            b"initials" => {
5645                                initials = val;
5646                            }
5647                            _ => {}
5648                        }
5649                    }
5650                    lst.push(CommentAuthor::new(id, name_val, initials));
5651                }
5652            }
5653            Ok(Event::Eof) => break,
5654            Err(e) => return Err(crate::Error::Xml(format!("commentAuthors parse: {e}"))),
5655            _ => {}
5656        }
5657        buf.clear();
5658    }
5659    Ok(lst)
5660}
5661
5662/// 从 `presentation.xml` 解析出关键字段(`sldIdLst` + `sldSz` + `sectionLst`)。
5663///
5664/// # 返回值
5665/// 元组 `(slide_ids, slide_width, slide_height, sld_master_ids, sections, notes_master_ids)`:
5666/// - `slide_ids`:按文档顺序排列的 `(id, rid)` 列表;
5667/// - `slide_width` / `slide_height`:EMU 尺寸;若未指定则回落到 `None`;
5668/// - `sld_master_ids`:母版 `(id, rid)` 列表(已实现 round-trip 保真);
5669/// - `sections`:章节分组列表(TODO-039),若未指定则为空;
5670/// - `notes_master_ids`:备注母版 `(id, rid)` 列表(round-trip 保真,TODO-045)。
5671///
5672/// # 错误
5673/// - [`crate::Error::Xml`]:XML 语法错误或关键属性缺失。
5674#[allow(clippy::type_complexity)]
5675pub fn parse_pres_root(
5676    xml: &str,
5677) -> crate::Result<(
5678    Vec<(u32, String)>,
5679    Option<Emu>,
5680    Option<Emu>,
5681    Vec<(u32, String)>,
5682    crate::oxml::section::SectionList,
5683    Vec<(u32, String)>,
5684)> {
5685    let mut rd = Reader::from_str(xml);
5686    rd.config_mut().trim_text(true);
5687    let mut buf = Vec::new();
5688    let mut sld_ids: Vec<(u32, String)> = Vec::new();
5689    let mut sld_sz: (Option<Emu>, Option<Emu>) = (None, None);
5690    // 解析 sldMasterIdLst(round-trip 保真)
5691    let mut sld_master_ids: Vec<(u32, String)> = Vec::new();
5692    // 解析 notesMasterIdLst(round-trip 保真,TODO-045)
5693    let mut notes_master_ids: Vec<(u32, String)> = Vec::new();
5694    // TODO-039:解析 sectionLst(位于 extLst 内的 p14 扩展)
5695    let mut sections = crate::oxml::section::SectionList::default();
5696    loop {
5697        match rd.read_event_into(&mut buf) {
5698            Ok(Event::Empty(e)) => {
5699                let name = e.name();
5700                let local = local_name(name.as_ref());
5701                if local == b"sldId" {
5702                    let mut id: Option<u32> = None;
5703                    let mut rid: Option<String> = None;
5704                    for a in e.attributes().flatten() {
5705                        let k = a.key.as_ref();
5706                        // 无前缀 `id` 与带前缀 `r:id` 都接受。
5707                        if k == b"id" {
5708                            if let Ok(v) = a
5709                                .normalized_value(quick_xml::XmlVersion::Implicit1_0)
5710                                .unwrap_or_default()
5711                                .parse::<u32>()
5712                            {
5713                                id = Some(v);
5714                            }
5715                        } else if k == b"r:id" || k.ends_with(b":id") {
5716                            rid = Some(
5717                                a.normalized_value(quick_xml::XmlVersion::Implicit1_0)
5718                                    .unwrap_or_default()
5719                                    .to_string(),
5720                            );
5721                        }
5722                    }
5723                    if let (Some(i), Some(r)) = (id, rid) {
5724                        sld_ids.push((i, r));
5725                    }
5726                } else if local == b"sldMasterId" {
5727                    // 解析 sldMasterId 元素(id + r:id)
5728                    let mut id: Option<u32> = None;
5729                    let mut rid: Option<String> = None;
5730                    for a in e.attributes().flatten() {
5731                        let k = a.key.as_ref();
5732                        if k == b"id" {
5733                            if let Ok(v) = a
5734                                .normalized_value(quick_xml::XmlVersion::Implicit1_0)
5735                                .unwrap_or_default()
5736                                .parse::<u32>()
5737                            {
5738                                id = Some(v);
5739                            }
5740                        } else if k == b"r:id" || k.ends_with(b":id") {
5741                            rid = Some(
5742                                a.normalized_value(quick_xml::XmlVersion::Implicit1_0)
5743                                    .unwrap_or_default()
5744                                    .to_string(),
5745                            );
5746                        }
5747                    }
5748                    if let (Some(i), Some(r)) = (id, rid) {
5749                        sld_master_ids.push((i, r));
5750                    }
5751                } else if local == b"notesMasterId" {
5752                    // 解析 notesMasterId 元素(id + r:id),与 sldMasterId 结构相同。
5753                    // TODO-045:round-trip 保真,供 to_opc_package 写出 notesMaster parts。
5754                    let mut id: Option<u32> = None;
5755                    let mut rid: Option<String> = None;
5756                    for a in e.attributes().flatten() {
5757                        let k = a.key.as_ref();
5758                        if k == b"id" {
5759                            if let Ok(v) = a
5760                                .normalized_value(quick_xml::XmlVersion::Implicit1_0)
5761                                .unwrap_or_default()
5762                                .parse::<u32>()
5763                            {
5764                                id = Some(v);
5765                            }
5766                        } else if k == b"r:id" || k.ends_with(b":id") {
5767                            rid = Some(
5768                                a.normalized_value(quick_xml::XmlVersion::Implicit1_0)
5769                                    .unwrap_or_default()
5770                                    .to_string(),
5771                            );
5772                        }
5773                    }
5774                    if let (Some(i), Some(r)) = (id, rid) {
5775                        notes_master_ids.push((i, r));
5776                    }
5777                } else if local == b"sldSz" {
5778                    let mut cx: Option<i64> = None;
5779                    let mut cy: Option<i64> = None;
5780                    for a in e.attributes().flatten() {
5781                        match a.key.as_ref() {
5782                            b"cx" => {
5783                                cx = a
5784                                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
5785                                    .unwrap_or_default()
5786                                    .parse()
5787                                    .ok()
5788                            }
5789                            b"cy" => {
5790                                cy = a
5791                                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
5792                                    .unwrap_or_default()
5793                                    .parse()
5794                                    .ok()
5795                            }
5796                            _ => {}
5797                        }
5798                    }
5799                    sld_sz = (cx.map(Emu), cy.map(Emu));
5800                }
5801            }
5802            Ok(Event::Start(e)) => {
5803                let name = e.name();
5804                let local = local_name(name.as_ref());
5805                if local == b"sldMasterId" {
5806                    // Start 形式的 sldMasterId(含子元素,罕见但规范允许)
5807                    let mut id: Option<u32> = None;
5808                    let mut rid: Option<String> = None;
5809                    for a in e.attributes().flatten() {
5810                        let k = a.key.as_ref();
5811                        if k == b"id" {
5812                            if let Ok(v) = a
5813                                .normalized_value(quick_xml::XmlVersion::Implicit1_0)
5814                                .unwrap_or_default()
5815                                .parse::<u32>()
5816                            {
5817                                id = Some(v);
5818                            }
5819                        } else if k == b"r:id" || k.ends_with(b":id") {
5820                            rid = Some(
5821                                a.normalized_value(quick_xml::XmlVersion::Implicit1_0)
5822                                    .unwrap_or_default()
5823                                    .to_string(),
5824                            );
5825                        }
5826                    }
5827                    if let (Some(i), Some(r)) = (id, rid) {
5828                        sld_master_ids.push((i, r));
5829                    }
5830                    // 跳过子元素直到 End
5831                    let _ = collect_full_element(&mut rd, e.into_owned());
5832                } else if local == b"notesMasterId" {
5833                    // Start 形式的 notesMasterId(含子元素,罕见但规范允许)。
5834                    // 与 sldMasterId 的 Start 分支结构相同。
5835                    let mut id: Option<u32> = None;
5836                    let mut rid: Option<String> = None;
5837                    for a in e.attributes().flatten() {
5838                        let k = a.key.as_ref();
5839                        if k == b"id" {
5840                            if let Ok(v) = a
5841                                .normalized_value(quick_xml::XmlVersion::Implicit1_0)
5842                                .unwrap_or_default()
5843                                .parse::<u32>()
5844                            {
5845                                id = Some(v);
5846                            }
5847                        } else if k == b"r:id" || k.ends_with(b":id") {
5848                            rid = Some(
5849                                a.normalized_value(quick_xml::XmlVersion::Implicit1_0)
5850                                    .unwrap_or_default()
5851                                    .to_string(),
5852                            );
5853                        }
5854                    }
5855                    if let (Some(i), Some(r)) = (id, rid) {
5856                        notes_master_ids.push((i, r));
5857                    }
5858                    // 跳过子元素直到 End
5859                    let _ = collect_full_element(&mut rd, e.into_owned());
5860                } else if local == b"extLst" {
5861                    // TODO-039:解析 extLst 内的 sectionLst 扩展(p14 命名空间)。
5862                    // 收集完整 extLst XML 后单独解析,避免主状态机嵌套过深。
5863                    let ext_lst_xml = collect_full_element(&mut rd, e.into_owned())?;
5864                    let parsed = parse_sections_from_ext_lst(&ext_lst_xml)?;
5865                    if !parsed.is_empty() {
5866                        sections = parsed;
5867                    }
5868                }
5869            }
5870            Ok(Event::Eof) => break,
5871            Err(e) => return Err(crate::Error::Xml(format!("pres root parse: {e}"))),
5872            _ => {}
5873        }
5874        buf.clear();
5875    }
5876    Ok((
5877        sld_ids,
5878        sld_sz.0,
5879        sld_sz.1,
5880        sld_master_ids,
5881        sections,
5882        notes_master_ids,
5883    ))
5884}
5885
5886/// 从 `<p:extLst>` 的 XML 片段中解析出 [`crate::oxml::section::SectionList`]。
5887///
5888/// 仅识别 `uri="{521415D9-36F7-43E2-AB2F-B90AF26B5E64}"` 的 `<p:ext>`,
5889/// 内部 `<p14:sectionLst>` 中的每个 `<p14:section>` 会被还原为
5890/// [`crate::oxml::section::Section`](name + slide_ids)。
5891///
5892/// # 返回值
5893/// - 解析到的章节列表(可能为空,表示 extLst 中无 section 扩展)。
5894///
5895/// # 错误
5896/// - [`crate::Error::Xml`]:XML 语法错误。
5897///
5898/// # 示例输入
5899/// ```xml
5900/// <p:extLst>
5901///   <p:ext uri="{521415D9-36F7-43E2-AB2F-B90AF26B5E64}">
5902///     <p14:sectionLst>
5903///       <p14:section name="章节一">
5904///         <p14:sldIdLst>
5905///           <p14:sldId id="256"/>
5906///         </p14:sldIdLst>
5907///       </p14:section>
5908///     </p14:sectionLst>
5909///   </p:ext>
5910/// </p:extLst>
5911/// ```
5912fn parse_sections_from_ext_lst(xml: &str) -> crate::Result<crate::oxml::section::SectionList> {
5913    let mut rd = Reader::from_str(xml);
5914    rd.config_mut().trim_text(true);
5915    let mut buf = Vec::new();
5916    let mut result = crate::oxml::section::SectionList::default();
5917    // 状态机:
5918    //   0 = 在 extLst 内寻找 ext
5919    //   1 = 在匹配的 ext 内寻找 sectionLst
5920    //   2 = 在 sectionLst 内解析 section
5921    //   3 = 在 section 内解析 sldIdLst
5922    let mut state: u8 = 0;
5923    // 当前正在构建的 section(在状态 2/3 中累积)
5924    let mut cur_name = String::new();
5925    let mut cur_ids: Vec<u32> = Vec::new();
5926
5927    loop {
5928        match rd.read_event_into(&mut buf) {
5929            Ok(Event::Start(e)) => {
5930                // 把 e.name() 绑定到 let,避免临时 QName 在 as_ref() 后 drop
5931                let name = e.name();
5932                let local = local_name(name.as_ref());
5933                match state {
5934                    0 if local == b"ext" => {
5935                        // 检查 uri 属性是否匹配 section 扩展
5936                        let mut uri_val = String::new();
5937                        for a in e.attributes().flatten() {
5938                            if a.key.as_ref() == b"uri" {
5939                                uri_val = a
5940                                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
5941                                    .unwrap_or_default()
5942                                    .to_string();
5943                            }
5944                        }
5945                        if uri_val == crate::oxml::section::SECTION_EXT_URI {
5946                            state = 1;
5947                        } else {
5948                            // 非 section 扩展——跳过整个 ext
5949                            let _ = collect_full_element(&mut rd, e.into_owned());
5950                        }
5951                    }
5952                    1 if local == b"sectionLst" => {
5953                        state = 2;
5954                    }
5955                    2 if local == b"section" => {
5956                        // 读取 name 属性
5957                        cur_name.clear();
5958                        cur_ids.clear();
5959                        for a in e.attributes().flatten() {
5960                            if a.key.as_ref() == b"name" {
5961                                cur_name = a
5962                                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
5963                                    .unwrap_or_default()
5964                                    .to_string();
5965                            }
5966                        }
5967                        state = 3;
5968                    }
5969                    3 if local == b"sldIdLst" => {
5970                        // 进入 sldIdLst,继续读取其中的 sldId(Empty 事件)
5971                    }
5972                    _ => {
5973                        // 其它未识别的子元素——整体跳过
5974                        let _ = collect_full_element(&mut rd, e.into_owned());
5975                    }
5976                }
5977            }
5978            Ok(Event::Empty(e)) => {
5979                // 把 e.name() 绑定到 let,避免临时 QName 在 as_ref() 后 drop
5980                let name = e.name();
5981                let local = local_name(name.as_ref());
5982                // 在 sldIdLst 内遇到自闭合的 sldId
5983                if state == 3 && local == b"sldId" {
5984                    for a in e.attributes().flatten() {
5985                        if a.key.as_ref() == b"id" {
5986                            if let Ok(v) = a
5987                                .normalized_value(quick_xml::XmlVersion::Implicit1_0)
5988                                .unwrap_or_default()
5989                                .parse::<u32>()
5990                            {
5991                                cur_ids.push(v);
5992                            }
5993                        }
5994                    }
5995                }
5996            }
5997            Ok(Event::End(e)) => {
5998                // 把 e.name() 绑定到 let,避免临时 QName 在 as_ref() 后 drop
5999                let name = e.name();
6000                let local = local_name(name.as_ref());
6001                match state {
6002                    3 if local == b"section" => {
6003                        // section 结束——提交
6004                        result.push(crate::oxml::section::Section {
6005                            name: std::mem::take(&mut cur_name),
6006                            slide_ids: std::mem::take(&mut cur_ids),
6007                        });
6008                        state = 2;
6009                    }
6010                    2 if local == b"sectionLst" => {
6011                        state = 1;
6012                    }
6013                    1 if local == b"ext" => {
6014                        state = 0;
6015                    }
6016                    0 if local == b"extLst" => {
6017                        break;
6018                    }
6019                    _ => {}
6020                }
6021            }
6022            Ok(Event::Eof) => break,
6023            Err(e) => return Err(crate::Error::Xml(format!("sectionLst parse: {e}"))),
6024            _ => {}
6025        }
6026        buf.clear();
6027    }
6028    Ok(result)
6029}
6030
6031/// 从 `slideMasterN.xml` 文本解析出 `SldMaster`。
6032///
6033/// # 解析内容
6034/// - `<p:spTree>` 内的所有 `<p:sp>`(其它子类型如 pic/grpSp 暂不解析,保留位置)
6035/// - `<p:sldLayoutIdLst>` 内的所有 `r:id`(layout 关系 id 列表)
6036///
6037/// # 忽略内容(后续扩展)
6038/// - `<p:clrMap>` 颜色映射
6039/// - `<p:txStyles>` 文本样式
6040/// - `<p:extLst>` 扩展列表
6041///
6042/// # 错误
6043/// - [`crate::Error::Xml`]:XML 语法错误。
6044pub fn parse_sld_master(xml: &str) -> crate::Result<crate::oxml::slidemaster::SldMaster> {
6045    let mut rd = Reader::from_str(xml);
6046    rd.config_mut().trim_text(true);
6047    let mut buf = Vec::new();
6048    let mut master = crate::oxml::slidemaster::SldMaster::default();
6049    // 状态机:0=等待 sldMaster,1=在 sldMaster 内,2=在 spTree 内,3=在 sldLayoutIdLst 内
6050    let mut state: u8 = 0;
6051    let mut sp_bufs: Vec<String> = Vec::new();
6052
6053    loop {
6054        match rd.read_event_into(&mut buf) {
6055            Ok(Event::Start(e)) => {
6056                let name = e.name();
6057                let local = local_name(name.as_ref());
6058                match state {
6059                    0 if local == b"sldMaster" => {
6060                        state = 1;
6061                    }
6062                    // cSld 是容器元素,不能跳过(内含 spTree)
6063                    1 if local == b"cSld" => {}
6064                    1 if local == b"spTree" => {
6065                        state = 2;
6066                    }
6067                    1 if local == b"sldLayoutIdLst" => {
6068                        state = 3;
6069                    }
6070                    2 if local == b"sp" => {
6071                        // 累积 sp 的内部 XML,走子解析
6072                        let inner = collect_full_element(&mut rd, e.into_owned())?;
6073                        sp_bufs.push(inner);
6074                    }
6075                    _ => {
6076                        // 其它子元素(clrMap / txStyles 等)—— 整个吞掉
6077                        let _ = collect_full_element(&mut rd, e.into_owned());
6078                    }
6079                }
6080            }
6081            Ok(Event::Empty(e)) => {
6082                let name = e.name();
6083                let local = local_name(name.as_ref());
6084                if state == 3 && local == b"sldLayoutId" {
6085                    // 提取 r:id 属性
6086                    for a in e.attributes().flatten() {
6087                        let k = a.key.as_ref();
6088                        if k == b"r:id" || k.ends_with(b":id") {
6089                            let rid = a
6090                                .normalized_value(quick_xml::XmlVersion::Implicit1_0)
6091                                .unwrap_or_default()
6092                                .to_string();
6093                            master.layout_rids.push(rid);
6094                        }
6095                    }
6096                }
6097            }
6098            Ok(Event::End(e)) => {
6099                let name = e.name();
6100                let local = local_name(name.as_ref());
6101                // state==2(spTree 结束)或 state==3(sldLayoutIdLst 结束)都回到 state=1
6102                if (state == 2 && local == b"spTree") || (state == 3 && local == b"sldLayoutIdLst")
6103                {
6104                    state = 1;
6105                } else if state == 1 && local == b"sldMaster" {
6106                    break;
6107                }
6108            }
6109            Ok(Event::Eof) => break,
6110            Err(e) => return Err(crate::Error::Xml(format!("sldMaster parse: {e}"))),
6111            _ => {}
6112        }
6113        buf.clear();
6114    }
6115    // 解析所有累积的 sp
6116    for s in sp_bufs {
6117        if let Ok(sp) = parse_sp(&s) {
6118            master.shapes.push(sp);
6119        }
6120    }
6121    Ok(master)
6122}
6123
6124/// 从 `notesMasterN.xml` 文本解析出 [`crate::oxml::notesmaster::NotesMaster`](TODO-045)。
6125///
6126/// 与 [`parse_sld_master`] 结构类似,但根元素是 `<p:notesMaster>`,
6127/// 且不含 `<p:sldLayoutIdLst>`(备注母版不挂版式)。
6128///
6129/// # 解析内容
6130/// - `<p:spTree>` 内的所有 `<p:sp>`(其它子类型如 pic/grpSp 暂不解析,保留位置)
6131///
6132/// # 忽略内容(后续扩展)
6133/// - `<p:clrMap>` 颜色映射
6134/// - `<p:notesStyle>` 备注文本样式
6135/// - `<p:extLst>` 扩展列表
6136///
6137/// # 错误
6138/// - [`crate::Error::Xml`]:XML 语法错误。
6139pub fn parse_notes_master(xml: &str) -> crate::Result<crate::oxml::notesmaster::NotesMaster> {
6140    let mut rd = Reader::from_str(xml);
6141    rd.config_mut().trim_text(true);
6142    let mut buf = Vec::new();
6143    let mut master = crate::oxml::notesmaster::NotesMaster::default();
6144    // 状态机:0=等待 notesMaster,1=在 notesMaster 内,2=在 spTree 内
6145    let mut state: u8 = 0;
6146    let mut sp_bufs: Vec<String> = Vec::new();
6147
6148    loop {
6149        match rd.read_event_into(&mut buf) {
6150            Ok(Event::Start(e)) => {
6151                let name = e.name();
6152                let local = local_name(name.as_ref());
6153                match state {
6154                    0 if local == b"notesMaster" => {
6155                        state = 1;
6156                    }
6157                    // cSld 是容器元素,不能跳过(内含 spTree)
6158                    1 if local == b"cSld" => {}
6159                    1 if local == b"spTree" => {
6160                        state = 2;
6161                    }
6162                    2 if local == b"sp" => {
6163                        // 累积 sp 的内部 XML,走子解析
6164                        let inner = collect_full_element(&mut rd, e.into_owned())?;
6165                        sp_bufs.push(inner);
6166                    }
6167                    _ => {
6168                        // 其它子元素:clrMap / notesStyle 需保留原始 XML 以支持 round-trip,
6169                        // 其余(extLst 等)整个吞掉。
6170                        // 注意:collect_full_element 返回 Result<String>,用 ? 传播错误
6171                        // (项目规则 §6.3 禁止 _ = some_func() 静默失败)。
6172                        //
6173                        // 借用约束:`local` 派生自 `e.name()`(借用 `e`),而下方
6174                        // `e.into_owned()` 会移动 `e`。为避免借用冲突,先在 move 之前
6175                        // 把 `local` 的判断结果存为 Copy 类型的 bool,再执行 move。
6176                        let is_clr_map = local == b"clrMap";
6177                        let is_notes_style = local == b"notesStyle";
6178                        let full_xml = collect_full_element(&mut rd, e.into_owned())?;
6179                        if is_clr_map {
6180                            master.clr_map_xml = Some(full_xml);
6181                        } else if is_notes_style {
6182                            master.notes_style_xml = Some(full_xml);
6183                        }
6184                        // 其余元素(extLst 等)丢弃:full_xml 已读出但不存储
6185                    }
6186                }
6187            }
6188            Ok(Event::End(e)) => {
6189                let name = e.name();
6190                let local = local_name(name.as_ref());
6191                if state == 2 && local == b"spTree" {
6192                    state = 1;
6193                } else if state == 1 && local == b"notesMaster" {
6194                    break;
6195                }
6196            }
6197            Ok(Event::Eof) => break,
6198            Err(e) => return Err(crate::Error::Xml(format!("notesMaster parse: {e}"))),
6199            _ => {}
6200        }
6201        buf.clear();
6202    }
6203    // 解析所有累积的 sp
6204    for s in sp_bufs {
6205        if let Ok(sp) = parse_sp(&s) {
6206            master.shapes.push(sp);
6207        }
6208    }
6209    Ok(master)
6210}
6211
6212/// 从 `slideLayoutN.xml` 文本解析出 `SldLayout`。
6213///
6214/// # 解析内容
6215/// - `<p:sldLayout type="...">` 根元素的 `type` 属性
6216/// - `<p:cSld name="...">` 的 `name` 属性
6217/// - `<p:spTree>` 内的所有 `<p:sp>`
6218///
6219/// # 忽略内容(后续扩展)
6220/// - `<p:clrMapOvr>` 颜色映射覆盖
6221/// - `<p:transition>` 过渡
6222///
6223/// # 错误
6224/// - [`crate::Error::Xml`]:XML 语法错误。
6225pub fn parse_sld_layout(xml: &str) -> crate::Result<crate::oxml::slidelayout::SldLayout> {
6226    let mut rd = Reader::from_str(xml);
6227    rd.config_mut().trim_text(true);
6228    let mut buf = Vec::new();
6229    let mut layout = crate::oxml::slidelayout::SldLayout::default();
6230    // 状态机:0=等待 sldLayout,1=在 sldLayout 内,2=在 spTree 内
6231    let mut state: u8 = 0;
6232    let mut sp_bufs: Vec<String> = Vec::new();
6233
6234    loop {
6235        match rd.read_event_into(&mut buf) {
6236            Ok(Event::Start(e)) => {
6237                let name = e.name();
6238                let local = local_name(name.as_ref());
6239                match state {
6240                    0 if local == b"sldLayout" => {
6241                        // 提取 type 属性
6242                        for a in e.attributes().flatten() {
6243                            if a.key.as_ref() == b"type" {
6244                                layout.type_ = a
6245                                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
6246                                    .unwrap_or_default()
6247                                    .to_string();
6248                            }
6249                        }
6250                        state = 1;
6251                    }
6252                    1 if local == b"cSld" => {
6253                        // 提取 name 属性
6254                        for a in e.attributes().flatten() {
6255                            if a.key.as_ref() == b"name" {
6256                                layout.name = a
6257                                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
6258                                    .unwrap_or_default()
6259                                    .to_string();
6260                            }
6261                        }
6262                    }
6263                    1 if local == b"spTree" => {
6264                        state = 2;
6265                    }
6266                    2 if local == b"sp" => {
6267                        let inner = collect_full_element(&mut rd, e.into_owned())?;
6268                        sp_bufs.push(inner);
6269                    }
6270                    _ => {
6271                        let _ = collect_full_element(&mut rd, e.into_owned());
6272                    }
6273                }
6274            }
6275            Ok(Event::End(e)) => {
6276                let name = e.name();
6277                let local = local_name(name.as_ref());
6278                if state == 2 && local == b"spTree" {
6279                    state = 1;
6280                } else if state == 1 && local == b"sldLayout" {
6281                    break;
6282                }
6283            }
6284            Ok(Event::Eof) => break,
6285            Err(e) => return Err(crate::Error::Xml(format!("sldLayout parse: {e}"))),
6286            _ => {}
6287        }
6288        buf.clear();
6289    }
6290    // 解析所有累积的 sp
6291    for s in sp_bufs {
6292        if let Ok(sp) = parse_sp(&s) {
6293            layout.shapes.push(sp);
6294        }
6295    }
6296    Ok(layout)
6297}
6298
6299/// 从 `themeN.xml` 文本解析出 `Theme`。
6300///
6301/// # 解析内容
6302/// - `<a:theme name="...">` 根元素的 `name` 属性
6303/// - `<a:clrScheme>` 内的 12 个颜色(dk1/lt1/dk2/lt2/accent1-6/hlink/folHlink)
6304/// - `<a:fontScheme>` 内的 majorFont/minorFont 的 latin typeface
6305///
6306/// # 忽略内容(后续扩展)
6307/// - `<a:fmtScheme>` 格式方案
6308/// - `<a:objectDefaults>` 对象默认值
6309/// - `<a:extraClrSchemeLst>` 额外颜色方案
6310///
6311/// # 错误
6312/// - [`crate::Error::Xml`]:XML 语法错误。
6313pub fn parse_theme(xml: &str) -> crate::Result<crate::oxml::theme::Theme> {
6314    let mut rd = Reader::from_str(xml);
6315    rd.config_mut().trim_text(true);
6316    let mut buf = Vec::new();
6317    let mut theme = crate::oxml::theme::Theme::default();
6318
6319    loop {
6320        match rd.read_event_into(&mut buf) {
6321            Ok(Event::Start(e)) => {
6322                let name = e.name();
6323                let local = local_name(name.as_ref());
6324                if local == b"theme" {
6325                    // 提取 name 属性
6326                    for a in e.attributes().flatten() {
6327                        if a.key.as_ref() == b"name" {
6328                            theme.name = a
6329                                .normalized_value(quick_xml::XmlVersion::Implicit1_0)
6330                                .unwrap_or_default()
6331                                .to_string();
6332                        }
6333                    }
6334                } else if local == b"clrScheme" {
6335                    // 解析颜色方案
6336                    let inner = collect_full_element(&mut rd, e.into_owned())?;
6337                    theme.color_scheme = parse_clr_scheme(&inner);
6338                } else if local == b"fontScheme" {
6339                    // 解析字体方案
6340                    let inner = collect_full_element(&mut rd, e.into_owned())?;
6341                    theme.font_scheme = parse_font_scheme(&inner);
6342                } else if local == b"fmtScheme" {
6343                    // 解析格式方案(TODO-005:FormatScheme 结构化解析)
6344                    // 保留原始 XML 支持 round-trip,同时填充 4 个结构化字段
6345                    let inner = collect_full_element(&mut rd, e.into_owned())?;
6346                    let (fmt_name, raw) = parse_fmt_scheme(&inner);
6347                    theme.format_scheme.name = fmt_name;
6348                    theme.format_scheme.raw_xml = raw;
6349                    // 结构化解析:把 raw_xml 拆分为 fill_styles / line_styles / effect_styles / bg_fill_styles
6350                    theme.format_scheme.parse_from_raw_xml();
6351                } else if local == b"themeElements" {
6352                    // themeElements 是容器元素,不跳过(内含 clrScheme / fontScheme / fmtScheme)
6353                } else {
6354                    let _ = collect_full_element(&mut rd, e.into_owned());
6355                }
6356            }
6357            Ok(Event::Eof) => break,
6358            Err(e) => return Err(crate::Error::Xml(format!("theme parse: {e}"))),
6359            _ => {}
6360        }
6361        buf.clear();
6362    }
6363    Ok(theme)
6364}
6365
6366/// 解析 `<a:fmtScheme>` 的 name 属性和内部 raw XML。
6367///
6368/// 返回 `(name, raw_xml)`:
6369/// - `name`:`<a:fmtScheme name="...">` 的 name 属性值;
6370/// - `raw_xml`:`<a:fmtScheme>` 内部所有子元素的原始 XML 字符串(不含 fmtScheme 标签本身)。
6371fn parse_fmt_scheme(xml: &str) -> (String, String) {
6372    let mut fmt_name = String::new();
6373    let mut raw_xml = String::new();
6374    let mut rd = Reader::from_str(xml);
6375    let mut buf = Vec::new();
6376    let mut in_fmt_scheme = false;
6377
6378    loop {
6379        match rd.read_event_into(&mut buf) {
6380            Ok(Event::Start(e)) => {
6381                let ev_name = e.name();
6382                let local = local_name(ev_name.as_ref());
6383                if !in_fmt_scheme && local == b"fmtScheme" {
6384                    in_fmt_scheme = true;
6385                    // 提取 name 属性
6386                    for a in e.attributes().flatten() {
6387                        if a.key.as_ref() == b"name" {
6388                            fmt_name = a
6389                                .normalized_value(quick_xml::XmlVersion::Implicit1_0)
6390                                .unwrap_or_default()
6391                                .to_string();
6392                        }
6393                    }
6394                } else if in_fmt_scheme {
6395                    // 收集子元素到 raw_xml
6396                    let inner = collect_full_element(&mut rd, e.into_owned()).unwrap_or_default();
6397                    raw_xml.push_str(&inner);
6398                }
6399            }
6400            Ok(Event::Empty(e)) => {
6401                let ev_name = e.name();
6402                let local = local_name(ev_name.as_ref());
6403                if in_fmt_scheme {
6404                    // 自闭合子元素,写入 raw_xml
6405                    let mut s = String::new();
6406                    s.push('<');
6407                    s.push_str(std::str::from_utf8(local).unwrap_or(""));
6408                    for a in e.attributes().flatten() {
6409                        s.push(' ');
6410                        s.push_str(std::str::from_utf8(a.key.as_ref()).unwrap_or(""));
6411                        s.push_str("=\"");
6412                        s.push_str(
6413                            &a.normalized_value(quick_xml::XmlVersion::Implicit1_0)
6414                                .unwrap_or_default(),
6415                        );
6416                        s.push('"');
6417                    }
6418                    s.push_str("/>");
6419                    raw_xml.push_str(&s);
6420                }
6421            }
6422            Ok(Event::End(e)) => {
6423                let ev_name = e.name();
6424                let local = local_name(ev_name.as_ref());
6425                if in_fmt_scheme && local == b"fmtScheme" {
6426                    break;
6427                }
6428            }
6429            Ok(Event::Eof) => break,
6430            _ => {}
6431        }
6432        buf.clear();
6433    }
6434    (fmt_name, raw_xml)
6435}
6436
6437/// 解析 `<a:clrScheme>` 内的 12 个颜色。
6438fn parse_clr_scheme(xml: &str) -> crate::oxml::theme::ColorScheme {
6439    use crate::oxml::theme::{ColorScheme, ThemeColor};
6440
6441    let mut scheme = ColorScheme::default();
6442    let mut rd = Reader::from_str(xml);
6443    let mut buf = Vec::new();
6444
6445    loop {
6446        match rd.read_event_into(&mut buf) {
6447            Ok(Event::Start(e)) => {
6448                let name = e.name();
6449                let local = local_name(name.as_ref());
6450                // 根据颜色槽位名分发,把颜色值写入对应字段
6451                let slot: &mut Option<ThemeColor> = match local {
6452                    b"dk1" => &mut scheme.dk1,
6453                    b"lt1" => &mut scheme.lt1,
6454                    b"dk2" => &mut scheme.dk2,
6455                    b"lt2" => &mut scheme.lt2,
6456                    b"accent1" => &mut scheme.accent1,
6457                    b"accent2" => &mut scheme.accent2,
6458                    b"accent3" => &mut scheme.accent3,
6459                    b"accent4" => &mut scheme.accent4,
6460                    b"accent5" => &mut scheme.accent5,
6461                    b"accent6" => &mut scheme.accent6,
6462                    b"hlink" => &mut scheme.hlink,
6463                    b"folHlink" => &mut scheme.fol_hlink,
6464                    // clrScheme 根元素自身 —— 提取 name 属性,不跳过子树
6465                    b"clrScheme" => {
6466                        // 提取 name 属性(TODO-005)
6467                        for a in e.attributes().flatten() {
6468                            if a.key.as_ref() == b"name" {
6469                                scheme.name = a
6470                                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
6471                                    .unwrap_or_default()
6472                                    .to_string();
6473                            }
6474                        }
6475                        continue;
6476                    }
6477                    _ => {
6478                        // 非颜色槽位未知元素 —— 跳过子树
6479                        let _ = collect_full_element(&mut rd, e.into_owned());
6480                        continue;
6481                    }
6482                };
6483                *slot = Some(read_color_child(&mut rd, local));
6484            }
6485            Ok(Event::End(e)) => {
6486                if local_name(e.name().as_ref()) == b"clrScheme" {
6487                    break;
6488                }
6489            }
6490            Ok(Event::Eof) => break,
6491            _ => {}
6492        }
6493        buf.clear();
6494    }
6495    scheme
6496}
6497
6498/// 读取颜色槽位(如 `<a:dk1>`)内的子元素 `<a:srgbClr>` 或 `<a:sysClr>`。
6499///
6500/// `parent_local` 是颜色槽位的 local name(如 `b"dk1"`),用于判断何时返回。
6501fn read_color_child<R: std::io::BufRead>(
6502    rd: &mut Reader<R>,
6503    parent_local: &[u8],
6504) -> crate::oxml::theme::ThemeColor {
6505    use crate::oxml::theme::ThemeColor;
6506    let mut buf = Vec::new();
6507    loop {
6508        match rd.read_event_into(&mut buf) {
6509            Ok(Event::Start(e)) => {
6510                let name = e.name();
6511                let local = local_name(name.as_ref());
6512                if local == b"srgbClr" {
6513                    let mut val = String::new();
6514                    for a in e.attributes().flatten() {
6515                        if a.key.as_ref() == b"val" {
6516                            val = a
6517                                .normalized_value(quick_xml::XmlVersion::Implicit1_0)
6518                                .unwrap_or_default()
6519                                .to_string();
6520                        }
6521                    }
6522                    // 消费 srgbClr 的 End 事件
6523                    let _ = collect_full_element(rd, e.into_owned());
6524                    return ThemeColor::Srgb(val);
6525                } else if local == b"sysClr" {
6526                    let mut val = String::new();
6527                    let mut last_clr = String::new();
6528                    for a in e.attributes().flatten() {
6529                        match a.key.as_ref() {
6530                            b"val" => {
6531                                val = a
6532                                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
6533                                    .unwrap_or_default()
6534                                    .to_string();
6535                            }
6536                            b"lastClr" => {
6537                                last_clr = a
6538                                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
6539                                    .unwrap_or_default()
6540                                    .to_string();
6541                            }
6542                            _ => {}
6543                        }
6544                    }
6545                    let _ = collect_full_element(rd, e.into_owned());
6546                    return ThemeColor::Sys(val, last_clr);
6547                } else {
6548                    let _ = collect_full_element(rd, e.into_owned());
6549                }
6550            }
6551            Ok(Event::Empty(e)) => {
6552                let name = e.name();
6553                let local = local_name(name.as_ref());
6554                if local == b"srgbClr" {
6555                    for a in e.attributes().flatten() {
6556                        if a.key.as_ref() == b"val" {
6557                            return ThemeColor::Srgb(
6558                                a.normalized_value(quick_xml::XmlVersion::Implicit1_0)
6559                                    .unwrap_or_default()
6560                                    .to_string(),
6561                            );
6562                        }
6563                    }
6564                } else if local == b"sysClr" {
6565                    let mut val = String::new();
6566                    let mut last_clr = String::new();
6567                    for a in e.attributes().flatten() {
6568                        match a.key.as_ref() {
6569                            b"val" => {
6570                                val = a
6571                                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
6572                                    .unwrap_or_default()
6573                                    .to_string();
6574                            }
6575                            b"lastClr" => {
6576                                last_clr = a
6577                                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
6578                                    .unwrap_or_default()
6579                                    .to_string();
6580                            }
6581                            _ => {}
6582                        }
6583                    }
6584                    return ThemeColor::Sys(val, last_clr);
6585                }
6586            }
6587            Ok(Event::End(e)) => {
6588                // 回到颜色槽位的 End —— 没有找到颜色子元素
6589                if local_name(e.name().as_ref()) == parent_local {
6590                    return ThemeColor::None;
6591                }
6592            }
6593            Ok(Event::Eof) => return ThemeColor::None,
6594            _ => {}
6595        }
6596        buf.clear();
6597    }
6598}
6599
6600/// 解析 `<a:fontScheme>` 内的 majorFont/minorFont。
6601fn parse_font_scheme(xml: &str) -> crate::oxml::theme::FontScheme {
6602    let mut scheme = crate::oxml::theme::FontScheme::default();
6603    let mut rd = Reader::from_str(xml);
6604    let mut buf = Vec::new();
6605    // 0=等待 fontScheme,1=在 fontScheme 内
6606    let mut state: u8 = 0;
6607
6608    loop {
6609        match rd.read_event_into(&mut buf) {
6610            Ok(Event::Start(e)) => {
6611                let name = e.name();
6612                let local = local_name(name.as_ref());
6613                if local == b"fontScheme" {
6614                    state = 1;
6615                    // 提取 name 属性(TODO-005)
6616                    for a in e.attributes().flatten() {
6617                        if a.key.as_ref() == b"name" {
6618                            scheme.name = a
6619                                .normalized_value(quick_xml::XmlVersion::Implicit1_0)
6620                                .unwrap_or_default()
6621                                .to_string();
6622                        }
6623                    }
6624                } else if state == 1 && local == b"majorFont" {
6625                    // 读取 latin / ea / cs typeface(TODO-005:扩展 FontScheme)
6626                    let (latin, ea, cs) = read_font_typefaces(&mut rd);
6627                    scheme.major_latin = latin;
6628                    scheme.major_ea = ea;
6629                    scheme.major_cs = cs;
6630                } else if state == 1 && local == b"minorFont" {
6631                    let (latin, ea, cs) = read_font_typefaces(&mut rd);
6632                    scheme.minor_latin = latin;
6633                    scheme.minor_ea = ea;
6634                    scheme.minor_cs = cs;
6635                }
6636            }
6637            Ok(Event::End(e)) => {
6638                if local_name(e.name().as_ref()) == b"fontScheme" {
6639                    break;
6640                }
6641            }
6642            Ok(Event::Eof) => break,
6643            _ => {}
6644        }
6645        buf.clear();
6646    }
6647    scheme
6648}
6649
6650/// 从 majorFont/minorFont 内读取 `<a:latin>` / `<a:ea>` / `<a:cs>` 的 typeface。
6651///
6652/// 返回 `(latin, ea, cs)` 三个 typeface 字符串。
6653/// 在 majorFont/minorFont 的 End 事件时返回(空值表示未找到)。
6654fn read_font_typefaces<R: std::io::BufRead>(rd: &mut Reader<R>) -> (String, String, String) {
6655    let mut buf = Vec::new();
6656    let mut latin = String::new();
6657    let mut ea = String::new();
6658    let mut cs = String::new();
6659    loop {
6660        match rd.read_event_into(&mut buf) {
6661            Ok(Event::Empty(e)) => {
6662                let name = e.name();
6663                let local = local_name(name.as_ref());
6664                // 提取 typeface 属性
6665                let typeface = extract_typeface(&e);
6666                match local {
6667                    b"latin" => latin = typeface,
6668                    b"ea" => ea = typeface,
6669                    b"cs" => cs = typeface,
6670                    _ => {}
6671                }
6672            }
6673            Ok(Event::Start(e)) => {
6674                let name = e.name();
6675                let local = local_name(name.as_ref());
6676                let typeface = extract_typeface(&e);
6677                match local {
6678                    b"latin" => latin = typeface,
6679                    b"ea" => ea = typeface,
6680                    b"cs" => cs = typeface,
6681                    _ => {}
6682                }
6683                // 消费 Start 元素的子树(含 End)
6684                let _ = collect_full_element(rd, e.into_owned());
6685            }
6686            Ok(Event::End(e)) => {
6687                let name = e.name();
6688                let local = local_name(name.as_ref());
6689                if local == b"majorFont" || local == b"minorFont" {
6690                    return (latin, ea, cs);
6691                }
6692            }
6693            Ok(Event::Eof) => return (latin, ea, cs),
6694            _ => {}
6695        }
6696        buf.clear();
6697    }
6698}
6699
6700/// 从元素的属性中提取 `typeface` 值。
6701fn extract_typeface(e: &quick_xml::events::BytesStart<'_>) -> String {
6702    for a in e.attributes().flatten() {
6703        if a.key.as_ref() == b"typeface" {
6704            return a
6705                .normalized_value(quick_xml::XmlVersion::Implicit1_0)
6706                .unwrap_or_default()
6707                .to_string();
6708        }
6709    }
6710    String::new()
6711}
6712
6713#[cfg(test)]
6714mod tests {
6715    use super::*;
6716    use crate::oxml::txbody::TextBody;
6717
6718    // ===================== TODO-050 backdrop 解析测试 =====================
6719
6720    /// 验证 `parse_backdrop` 解析完整 backdrop(含 anchor + 全平面)。
6721    #[test]
6722    fn parse_backdrop_full() {
6723        let xml = r#"<?xml version="1.0"?>
6724<a:backdrop xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
6725  <a:anchor x="100000" y="200000" z="300000"/>
6726  <a:floor/>
6727  <a:wall/>
6728  <a:l/>
6729  <a:r/>
6730  <a:t/>
6731  <a:b/>
6732</a:backdrop>"#;
6733        let mut rd = Reader::from_str(xml);
6734        rd.config_mut().trim_text(true);
6735        let mut buf = Vec::new();
6736        let start = loop {
6737            match rd.read_event_into(&mut buf) {
6738                Ok(Event::Start(e)) if local_name(e.name().as_ref()) == b"backdrop" => {
6739                    break e.into_owned();
6740                }
6741                Ok(Event::Eof) => panic!("backdrop not found"),
6742                _ => {}
6743            }
6744            buf.clear();
6745        };
6746        let bd = parse_backdrop(&mut rd, start).expect("parse backdrop");
6747
6748        assert_eq!(
6749            bd.anchor,
6750            Some(Point3d {
6751                x: 100000,
6752                y: 200000,
6753                z: 300000
6754            })
6755        );
6756        assert!(bd.floor);
6757        assert!(bd.wall);
6758        assert!(bd.left);
6759        assert!(bd.right);
6760        assert!(bd.top);
6761        assert!(bd.bottom);
6762    }
6763
6764    /// 验证 `parse_backdrop` 仅启用部分平面。
6765    #[test]
6766    fn parse_backdrop_partial() {
6767        let xml = r#"<?xml version="1.0"?>
6768<a:backdrop xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
6769  <a:floor/>
6770  <a:wall/>
6771</a:backdrop>"#;
6772        let mut rd = Reader::from_str(xml);
6773        rd.config_mut().trim_text(true);
6774        let mut buf = Vec::new();
6775        let start = loop {
6776            match rd.read_event_into(&mut buf) {
6777                Ok(Event::Start(e)) if local_name(e.name().as_ref()) == b"backdrop" => {
6778                    break e.into_owned();
6779                }
6780                Ok(Event::Eof) => panic!("backdrop not found"),
6781                _ => {}
6782            }
6783            buf.clear();
6784        };
6785        let bd = parse_backdrop(&mut rd, start).expect("parse backdrop");
6786
6787        assert!(bd.anchor.is_none());
6788        assert!(bd.floor);
6789        assert!(bd.wall);
6790        assert!(!bd.left);
6791        assert!(!bd.right);
6792        assert!(!bd.top);
6793        assert!(!bd.bottom);
6794    }
6795
6796    /// 验证 `parse_scene_3d` 正确解析 backdrop 子元素(round-trip 验证)。
6797    #[test]
6798    fn parse_scene_3d_with_backdrop() {
6799        let xml = r#"<?xml version="1.0"?>
6800<a:scene3d xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
6801  <a:camera prst="orthographicFront"/>
6802  <a:lightRig rig="balanced" dir="t"/>
6803  <a:backdrop>
6804    <a:anchor x="0" y="0" z="0"/>
6805    <a:floor/>
6806    <a:wall/>
6807  </a:backdrop>
6808</a:scene3d>"#;
6809        let mut rd = Reader::from_str(xml);
6810        rd.config_mut().trim_text(true);
6811        let mut buf = Vec::new();
6812        let start = loop {
6813            match rd.read_event_into(&mut buf) {
6814                Ok(Event::Start(e)) if local_name(e.name().as_ref()) == b"scene3d" => {
6815                    break e.into_owned();
6816                }
6817                Ok(Event::Eof) => panic!("scene3d not found"),
6818                _ => {}
6819            }
6820            buf.clear();
6821        };
6822        let scene = parse_scene_3d(&mut rd, start).expect("parse scene3d");
6823
6824        // 验证 backdrop 被解析
6825        let bd = scene.backdrop.expect("backdrop should be parsed");
6826        assert!(bd.floor);
6827        assert!(bd.wall);
6828        assert!(!bd.left);
6829        assert_eq!(bd.anchor, Some(Point3d { x: 0, y: 0, z: 0 }));
6830    }
6831
6832    /// 验证 `parse_scene_3d` 无 backdrop 时 backdrop 字段为 None(向后兼容)。
6833    #[test]
6834    fn parse_scene_3d_without_backdrop() {
6835        let xml = r#"<?xml version="1.0"?>
6836<a:scene3d xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
6837  <a:camera prst="orthographicFront"/>
6838  <a:lightRig rig="balanced" dir="t"/>
6839</a:scene3d>"#;
6840        let mut rd = Reader::from_str(xml);
6841        rd.config_mut().trim_text(true);
6842        let mut buf = Vec::new();
6843        let start = loop {
6844            match rd.read_event_into(&mut buf) {
6845                Ok(Event::Start(e)) if local_name(e.name().as_ref()) == b"scene3d" => {
6846                    break e.into_owned();
6847                }
6848                Ok(Event::Eof) => panic!("scene3d not found"),
6849                _ => {}
6850            }
6851            buf.clear();
6852        };
6853        let scene = parse_scene_3d(&mut rd, start).expect("parse scene3d");
6854        assert!(scene.backdrop.is_none());
6855    }
6856
6857    #[test]
6858    fn parse_minimal_sld() {
6859        let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
6860<p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
6861       xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
6862       xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
6863  <p:cSld>
6864    <p:spTree>
6865      <p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
6866      <p:grpSpPr><a:xfrm/></p:grpSpPr>
6867      <p:sp>
6868        <p:nvSpPr>
6869          <p:cNvPr id="2" name="TextBox 1"/>
6870          <p:cNvSpPr txBox="1"/>
6871          <p:nvPr/>
6872        </p:nvSpPr>
6873        <p:spPr>
6874          <a:xfrm>
6875            <a:off x="914400" y="914400"/>
6876            <a:ext cx="3657600" cy="457200"/>
6877          </a:xfrm>
6878          <a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
6879          <a:noFill/>
6880        </p:spPr>
6881        <p:txBody>
6882          <a:bodyPr wrap="square" rtlCol="0"><a:noAutofit/></a:bodyPr>
6883          <a:p>
6884            <a:r><a:rPr lang="en-US"/><a:t>Hello</a:t></a:r>
6885          </a:p>
6886        </p:txBody>
6887      </p:sp>
6888    </p:spTree>
6889  </p:cSld>
6890  <p:clrMapOvr/>
6891</p:sld>"#;
6892        let sld = parse_sld(xml).expect("parse ok");
6893        assert_eq!(sld.shapes.len(), 1);
6894        if let OxmlSlideShape::Sp(sp) = &sld.shapes[0] {
6895            assert_eq!(sp.id, 2);
6896            assert_eq!(sp.name, "TextBox 1");
6897            assert!(sp.c_nv_sp_pr_tx_box);
6898            assert_eq!(sp.text.paragraphs.len(), 1);
6899            assert_eq!(sp.text.paragraphs[0].runs.len(), 1);
6900            assert_eq!(sp.text.paragraphs[0].runs[0].text, "Hello");
6901        } else {
6902            panic!("expected Sp");
6903        }
6904    }
6905
6906    #[test]
6907    fn parse_empty_txbody_keeps_one_paragraph() {
6908        let xml = r#"<p:txBody xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
6909            <a:bodyPr/>
6910        </p:txBody>"#;
6911        let tb = parse_txbody(xml).unwrap();
6912        assert!(tb.paragraphs.is_empty());
6913        // TextBody 解析出来确实没有段落——这与 PowerPoint 期望"至少一个段落"略不同,
6914        // 但符合"无段落就不生成"的最小语义。
6915        let tb2 = TextBody::new();
6916        assert!(tb2.paragraphs.is_empty());
6917    }
6918
6919    #[test]
6920    fn parse_pres_root_extracts_sld_id_lst_and_size() {
6921        let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
6922<p:presentation xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
6923                xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
6924  <p:sldMasterIdLst><p:sldMasterId id="2147483648" r:id="rId1"/></p:sldMasterIdLst>
6925  <p:sldIdLst>
6926    <p:sldId id="256" r:id="rId2"/>
6927    <p:sldId id="257" r:id="rId3"/>
6928  </p:sldIdLst>
6929  <p:sldSz cx="9144000" cy="6858000"/>
6930</p:presentation>"#;
6931        let (ids, w, h, master_ids, _sections, _notes_master_ids) =
6932            parse_pres_root(xml).expect("parse ok");
6933        assert_eq!(ids.len(), 2);
6934        assert_eq!(ids[0], (256, "rId2".to_string()));
6935        assert_eq!(ids[1], (257, "rId3".to_string()));
6936        assert_eq!(w.map(|v| v.0), Some(9_144_000));
6937        assert_eq!(h.map(|v| v.0), Some(6_858_000));
6938        // 验证 sldMasterIdLst 解析
6939        assert_eq!(master_ids.len(), 1);
6940        assert_eq!(master_ids[0], (2147483648, "rId1".to_string()));
6941    }
6942
6943    #[test]
6944    fn parse_notes_returns_text_body() {
6945        let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
6946<p:notes xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
6947         xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
6948  <p:cSld>
6949    <p:spTree>
6950      <p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
6951      <p:grpSpPr/>
6952      <p:sp>
6953        <p:nvSpPr>
6954          <p:cNvPr id="2" name="Notes Placeholder"/>
6955          <p:cNvSpPr txBox="1"/>
6956          <p:nvPr><p:ph type="body" idx="1"/></p:nvPr>
6957        </p:nvSpPr>
6958        <p:spPr/>
6959        <p:txBody>
6960          <a:bodyPr/>
6961          <a:p><a:r><a:t>hello notes</a:t></a:r></a:p>
6962        </p:txBody>
6963      </p:sp>
6964    </p:spTree>
6965  </p:cSld>
6966</p:notes>"#;
6967        let tb = parse_notes(xml).expect("parse notes ok");
6968        assert_eq!(tb.paragraphs.len(), 1);
6969        assert_eq!(tb.paragraphs[0].runs.len(), 1);
6970        assert_eq!(tb.paragraphs[0].runs[0].text, "hello notes");
6971    }
6972
6973    /// `parse_pic` 必须能正确解析 r:embed 与 srcRect。
6974    ///
6975    /// 早期版本用 `find("r:embed=")` 字符串扫描;本测试覆盖**自闭合 `<a:blip/>`** 与
6976    /// **非自闭合 `<a:blip>...</a:blip>`** 两种形态 + 缩进变化,确保 SAX 解析稳定。
6977    #[test]
6978    fn parse_pic_extracts_rid_and_src_rect() {
6979        // 自闭合 blip + 有 srcRect
6980        let xml1 = r#"<p:pic xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
6981                            xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
6982                            xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
6983  <p:nvPicPr><p:cNvPr id="42" name="MyPic"/><p:cNvPicPr/><p:nvPr/></p:nvPicPr>
6984  <p:blipFill>
6985    <a:blip r:embed="rIdImg5"/>
6986    <a:srcRect l="1000" t="2000" r="3000" b="4000"/>
6987  </p:blipFill>
6988  <p:spPr/>
6989</p:pic>"#;
6990        let p1 = parse_pic(xml1).expect("parse pic 1");
6991        assert_eq!(p1.id, 42);
6992        assert_eq!(p1.name, "MyPic");
6993        assert_eq!(p1.rid, "rIdImg5");
6994        assert_eq!(p1.src_rect, Some((1000, 2000, 3000, 4000)));
6995
6996        // 非自闭合 blip
6997        let xml2 = r#"<p:pic xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
6998                            xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
6999                            xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
7000<p:nvPicPr><p:cNvPr id="7" name="p2"/><p:cNvPicPr/><p:nvPr/></p:nvPicPr>
7001<p:blipFill><a:blip r:embed="rIdImg99"></a:blip></p:blipFill>
7002<p:spPr/>
7003</p:pic>"#;
7004        let p2 = parse_pic(xml2).expect("parse pic 2");
7005        assert_eq!(p2.id, 7);
7006        assert_eq!(p2.rid, "rIdImg99");
7007    }
7008
7009    /// `collect_attr_value` 之前是 noop + 比较 noop,导致 typeface 永远取不到。
7010    /// 本测试确保 `<a:latin typeface="..."/>` 能正确解析。
7011    #[test]
7012    fn parse_run_with_typeface_works() {
7013        let xml = r#"<a:r xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
7014  <a:rPr>
7015    <a:latin typeface="Calibri"/>
7016    <a:ea typeface="宋体"/>
7017    <a:cs typeface="Times New Roman"/>
7018  </a:rPr>
7019  <a:t>hello</a:t>
7020</a:r>"#;
7021        let r = parse_run(xml).expect("parse run");
7022        assert_eq!(r.text, "hello");
7023        assert_eq!(r.properties.latin_font.as_deref(), Some("Calibri"));
7024        assert_eq!(r.properties.eastasia_font.as_deref(), Some("宋体"));
7025        assert_eq!(r.properties.cs_font.as_deref(), Some("Times New Roman"));
7026    }
7027
7028    /// `Sp::write_xml` 在 `is_placeholder=true` 但 `ph_type=None` 时,**必须**默认写出 `type="body"`。
7029    /// 早期版本会写出无属性的 `<p:ph/>`,PowerPoint 弹警告。
7030    #[test]
7031    fn write_sp_placeholder_defaults_to_body() {
7032        use crate::oxml::shape::Sp;
7033        use crate::oxml::writer::XmlWriter;
7034        let sp = Sp {
7035            id: 3,
7036            name: "Ph".into(),
7037            is_placeholder: true,
7038            ph_idx: Some(0),
7039            ph_type: None, // 关键:None 时应默认 body
7040            ..Default::default()
7041        };
7042        let mut w = XmlWriter::new();
7043        sp.write_xml(&mut w);
7044        let s = w.into_string();
7045        assert!(
7046            s.contains(r#"<p:ph type="body" idx="0"/>"#),
7047            "应写出 type=\"body\",实际:{s}"
7048        );
7049    }
7050
7051    /// `Connector::set_begin/set_end` 必须**自动**同步 xfrm 边界盒,
7052    /// 否则序列化后 begin/end 位置信息丢失。
7053    #[test]
7054    fn connector_set_begin_end_recomputes_xfrm() {
7055        use crate::oxml::simpletypes::MsoConnectorType;
7056        use crate::shape::connector::Connector;
7057        use crate::units::EmuPoint;
7058        let mut c = Connector::new_with_type("c1", MsoConnectorType::Straight);
7059        c.set_begin(EmuPoint::new(1000, 2000));
7060        c.set_end(EmuPoint::new(5000, 6000));
7061        // bounding box: off=(1000,2000), ext=(4000,4000)
7062        assert_eq!(c.properties().xfrm.off_x.unwrap().value(), 1000);
7063        assert_eq!(c.properties().xfrm.off_y.unwrap().value(), 2000);
7064        assert_eq!(c.properties().xfrm.ext_cx.unwrap().value(), 4000);
7065        assert_eq!(c.properties().xfrm.ext_cy.unwrap().value(), 4000);
7066    }
7067
7068    /// 验证 `parse_sld_master` 能从最小 slideMaster XML 中提取 shapes 和 layout_rids。
7069    #[test]
7070    fn parse_minimal_sld_master() {
7071        let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
7072<p:sldMaster xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
7073             xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
7074             xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
7075  <p:cSld>
7076    <p:spTree>
7077      <p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
7078      <p:grpSpPr><a:xfrm/></p:grpSpPr>
7079      <p:sp>
7080        <p:nvSpPr>
7081          <p:cNvPr id="2" name="Title Placeholder 1"/>
7082          <p:cNvSpPr txBox="1"/>
7083          <p:nvPr><p:ph type="title" idx="0"/></p:nvPr>
7084        </p:nvSpPr>
7085        <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>
7086        <p:txBody><a:bodyPr/><a:lstStyle/><a:p><a:endParaRPr lang="en-US"/></a:p></p:txBody>
7087      </p:sp>
7088    </p:spTree>
7089  </p:cSld>
7090  <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"/>
7091  <p:sldLayoutIdLst><p:sldLayoutId id="2147483649" r:id="rId1"/></p:sldLayoutIdLst>
7092</p:sldMaster>"#;
7093        let master = parse_sld_master(xml).unwrap();
7094        assert_eq!(master.shapes.len(), 1, "应解析出 1 个 shape");
7095        assert_eq!(master.shapes[0].id, 2);
7096        assert_eq!(master.shapes[0].name, "Title Placeholder 1");
7097        assert!(master.shapes[0].is_placeholder);
7098        assert_eq!(master.shapes[0].ph_type.as_deref(), Some("title"));
7099        assert_eq!(master.layout_rids.len(), 1, "应解析出 1 个 layout rid");
7100        assert_eq!(master.layout_rids[0], "rId1");
7101    }
7102
7103    /// 验证 `parse_sld_layout` 能从最小 slideLayout XML 中提取 type/name/shapes。
7104    #[test]
7105    fn parse_minimal_sld_layout() {
7106        let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
7107<p:sldLayout xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
7108             xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
7109             xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
7110             type="title" preserve="1">
7111  <p:cSld name="Title Slide">
7112    <p:spTree>
7113      <p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
7114      <p:grpSpPr><a:xfrm/></p:grpSpPr>
7115      <p:sp>
7116        <p:nvSpPr>
7117          <p:cNvPr id="2" name="Title 1"/>
7118          <p:cNvSpPr txBox="1"/>
7119          <p:nvPr><p:ph type="title" idx="0"/></p:nvPr>
7120        </p:nvSpPr>
7121        <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>
7122        <p:txBody><a:bodyPr/><a:lstStyle/><a:p><a:endParaRPr lang="en-US"/></a:p></p:txBody>
7123      </p:sp>
7124    </p:spTree>
7125  </p:cSld>
7126  <p:clrMapOvr bg1="lt1" tx1="dk1" bg2="lt2" tx2="dk2"/>
7127</p:sldLayout>"#;
7128        let layout = parse_sld_layout(xml).unwrap();
7129        assert_eq!(layout.type_, "title");
7130        assert_eq!(layout.name, "Title Slide");
7131        assert_eq!(layout.shapes.len(), 1, "应解析出 1 个 shape");
7132        assert_eq!(layout.shapes[0].name, "Title 1");
7133        assert!(layout.shapes[0].is_placeholder);
7134    }
7135
7136    /// 验证 `parse_theme` 能从 theme XML 中提取 name/color_scheme/font_scheme/fmt_scheme。
7137    #[test]
7138    fn parse_theme_extracts_name_and_colors() {
7139        let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
7140<a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" name="Office Theme">
7141  <a:themeElements>
7142    <a:clrScheme name="Office">
7143      <a:dk1><a:sysClr val="windowText" lastClr="000000"/></a:dk1>
7144      <a:lt1><a:sysClr val="window" lastClr="FFFFFF"/></a:lt1>
7145      <a:dk2><a:srgbClr val="1F497D"/></a:dk2>
7146      <a:lt2><a:srgbClr val="EEECE1"/></a:lt2>
7147      <a:accent1><a:srgbClr val="4F81BD"/></a:accent1>
7148      <a:accent2><a:srgbClr val="C0504D"/></a:accent2>
7149      <a:accent3><a:srgbClr val="9BBB59"/></a:accent3>
7150      <a:accent4><a:srgbClr val="8064A2"/></a:accent4>
7151      <a:accent5><a:srgbClr val="4BACC6"/></a:accent5>
7152      <a:accent6><a:srgbClr val="F79646"/></a:accent6>
7153      <a:hlink><a:srgbClr val="0000FF"/></a:hlink>
7154      <a:folHlink><a:srgbClr val="800080"/></a:folHlink>
7155    </a:clrScheme>
7156    <a:fontScheme name="Office">
7157      <a:majorFont><a:latin typeface="Calibri"/><a:ea typeface=""/><a:cs typeface=""/></a:majorFont>
7158      <a:minorFont><a:latin typeface="Calibri"/><a:ea typeface=""/><a:cs typeface=""/></a:minorFont>
7159    </a:fontScheme>
7160    <a:fmtScheme name="Office">
7161      <a:fillStyleLst>
7162        <a:solidFill><a:schemeClr val="phClr"/></a:solidFill>
7163      </a:fillStyleLst>
7164      <a:lnStyleLst/>
7165      <a:effectStyleLst/>
7166      <a:bgFillStyleLst/>
7167    </a:fmtScheme>
7168  </a:themeElements>
7169</a:theme>"#;
7170        let theme = parse_theme(xml).unwrap();
7171        assert_eq!(theme.name, "Office Theme");
7172        // 验证颜色方案名(TODO-005)
7173        assert_eq!(theme.color_scheme.name, "Office");
7174        // 验证颜色方案(用 assert_eq! + Debug 格式以便定位问题)
7175        assert_eq!(
7176            format!("{:?}", theme.color_scheme.dk1),
7177            r#"Some(Sys("windowText", "000000"))"#,
7178            "dk1 mismatch"
7179        );
7180        assert_eq!(
7181            format!("{:?}", theme.color_scheme.lt1),
7182            r#"Some(Sys("window", "FFFFFF"))"#,
7183            "lt1 mismatch"
7184        );
7185        assert_eq!(
7186            format!("{:?}", theme.color_scheme.accent1),
7187            r#"Some(Srgb("4F81BD"))"#,
7188            "accent1 mismatch"
7189        );
7190        assert_eq!(
7191            format!("{:?}", theme.color_scheme.hlink),
7192            r#"Some(Srgb("0000FF"))"#,
7193            "hlink mismatch"
7194        );
7195        // 验证字体方案名(TODO-005)
7196        assert_eq!(theme.font_scheme.name, "Office");
7197        // 验证字体方案
7198        assert_eq!(theme.font_scheme.major_latin, "Calibri");
7199        assert_eq!(theme.font_scheme.minor_latin, "Calibri");
7200        // 验证格式方案(TODO-005)
7201        assert_eq!(theme.format_scheme.name, "Office");
7202        assert!(
7203            !theme.format_scheme.raw_xml.is_empty(),
7204            "fmtScheme raw_xml should not be empty"
7205        );
7206        assert!(
7207            theme.format_scheme.raw_xml.contains("<a:fillStyleLst>"),
7208            "fmtScheme raw_xml should contain fillStyleLst"
7209        );
7210    }
7211
7212    /// 验证 `parse_theme` 能解析 fontScheme 的 ea/cs typeface(TODO-005)。
7213    #[test]
7214    fn parse_theme_font_scheme_ea_cs() {
7215        let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
7216<a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" name="Test">
7217  <a:themeElements>
7218    <a:clrScheme name="Test"/>
7219    <a:fontScheme name="Test">
7220      <a:majorFont>
7221        <a:latin typeface="Calibri Light"/>
7222        <a:ea typeface="宋体"/>
7223        <a:cs typeface="Times New Roman"/>
7224      </a:majorFont>
7225      <a:minorFont>
7226        <a:latin typeface="Calibri"/>
7227        <a:ea typeface="黑体"/>
7228        <a:cs typeface="Arial"/>
7229      </a:minorFont>
7230    </a:fontScheme>
7231    <a:fmtScheme name="Test"/>
7232  </a:themeElements>
7233</a:theme>"#;
7234        let theme = parse_theme(xml).unwrap();
7235        assert_eq!(theme.font_scheme.major_latin, "Calibri Light");
7236        assert_eq!(theme.font_scheme.major_ea, "宋体");
7237        assert_eq!(theme.font_scheme.major_cs, "Times New Roman");
7238        assert_eq!(theme.font_scheme.minor_latin, "Calibri");
7239        assert_eq!(theme.font_scheme.minor_ea, "黑体");
7240        assert_eq!(theme.font_scheme.minor_cs, "Arial");
7241    }
7242
7243    /// 验证 theme 的 round-trip:parse → to_xml → parse 一致性(TODO-005)。
7244    #[test]
7245    fn theme_round_trip() {
7246        let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
7247<a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" name="RoundTrip">
7248  <a:themeElements>
7249    <a:clrScheme name="RT">
7250      <a:dk1><a:sysClr val="windowText" lastClr="000000"/></a:dk1>
7251      <a:lt1><a:sysClr val="window" lastClr="FFFFFF"/></a:lt1>
7252      <a:dk2><a:srgbClr val="1F497D"/></a:dk2>
7253      <a:lt2><a:srgbClr val="EEECE1"/></a:lt2>
7254      <a:accent1><a:srgbClr val="4F81BD"/></a:accent1>
7255      <a:accent2><a:srgbClr val="C0504D"/></a:accent2>
7256      <a:accent3><a:srgbClr val="9BBB59"/></a:accent3>
7257      <a:accent4><a:srgbClr val="8064A2"/></a:accent4>
7258      <a:accent5><a:srgbClr val="4BACC6"/></a:accent5>
7259      <a:accent6><a:srgbClr val="F79646"/></a:accent6>
7260      <a:hlink><a:srgbClr val="0000FF"/></a:hlink>
7261      <a:folHlink><a:srgbClr val="800080"/></a:folHlink>
7262    </a:clrScheme>
7263    <a:fontScheme name="RT">
7264      <a:majorFont><a:latin typeface="Calibri"/><a:ea typeface=""/><a:cs typeface=""/></a:majorFont>
7265      <a:minorFont><a:latin typeface="Calibri"/><a:ea typeface=""/><a:cs typeface=""/></a:minorFont>
7266    </a:fontScheme>
7267    <a:fmtScheme name="Office"/>
7268  </a:themeElements>
7269</a:theme>"#;
7270        // 第一次解析
7271        let theme1 = parse_theme(xml).unwrap();
7272        assert_eq!(theme1.name, "RoundTrip");
7273        assert_eq!(theme1.color_scheme.name, "RT");
7274        assert_eq!(theme1.font_scheme.name, "RT");
7275        // 序列化回 XML
7276        let serialized = theme1.to_xml();
7277        // 第二次解析
7278        let theme2 = parse_theme(&serialized).unwrap();
7279        // 验证关键字段一致
7280        assert_eq!(theme2.name, theme1.name);
7281        assert_eq!(theme2.color_scheme.name, theme1.color_scheme.name);
7282        assert_eq!(theme2.color_scheme.dk1, theme1.color_scheme.dk1);
7283        assert_eq!(theme2.color_scheme.accent1, theme1.color_scheme.accent1);
7284        assert_eq!(theme2.font_scheme.name, theme1.font_scheme.name);
7285        assert_eq!(
7286            theme2.font_scheme.major_latin,
7287            theme1.font_scheme.major_latin
7288        );
7289        assert_eq!(
7290            theme2.font_scheme.minor_latin,
7291            theme1.font_scheme.minor_latin
7292        );
7293    }
7294
7295    /// 验证 `parse_cxn_sp` 能正确解析连接器的 id/name/begin/end/stCxn/endCxn。
7296    ///
7297    /// 连接器是 OOXML 中特殊的形状(`<p:cxnSp>`),用于在形状间画线。
7298    /// `stCxn`/`endCxn` 是"挂接"信息——表示连接线粘附到哪个形状的哪个连接点。
7299    #[test]
7300    fn parse_cxn_sp_extracts_geometry_and_connections() {
7301        let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
7302<p:cxnSp xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
7303         xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
7304  <p:nvCxnSpPr>
7305    <p:cNvPr id="10" name="Connector1"/>
7306    <p:cNvCxnSpPr/>
7307    <p:nvPr>
7308      <p:stCxn id="2" idx="3"/>
7309      <p:endCxn id="5" idx="1"/>
7310    </p:nvPr>
7311  </p:nvCxnSpPr>
7312  <p:spPr>
7313    <a:xfrm>
7314      <a:off x="1000" y="2000"/>
7315      <a:ext cx="5000" cy="4000"/>
7316    </a:xfrm>
7317    <a:prstGeom prst="line"><a:avLst/></a:prstGeom>
7318  </p:spPr>
7319</p:cxnSp>"#;
7320        let cxn = parse_cxn_sp(xml).expect("parse cxnSp ok");
7321        assert_eq!(cxn.id, 10);
7322        assert_eq!(cxn.name, "Connector1");
7323        // 挂接信息
7324        assert_eq!(cxn.st_cxn, Some((2, 3)), "stCxn 应解析出 (id=2, idx=3)");
7325        assert_eq!(cxn.end_cxn, Some((5, 1)), "endCxn 应解析出 (id=5, idx=1)");
7326        // 几何坐标(xfrm 内的 off/ext)
7327        assert_eq!(cxn.properties.xfrm.off_x.unwrap().value(), 1000);
7328        assert_eq!(cxn.properties.xfrm.off_y.unwrap().value(), 2000);
7329        assert_eq!(cxn.properties.xfrm.ext_cx.unwrap().value(), 5000);
7330        assert_eq!(cxn.properties.xfrm.ext_cy.unwrap().value(), 4000);
7331    }
7332
7333    /// 验证 `parse_cxn_sp` 对**无挂接**的连接器也能正常解析(纯几何连接线)。
7334    #[test]
7335    fn parse_cxn_sp_without_connections() {
7336        let xml = r#"<p:cxnSp xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
7337         xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
7338  <p:nvCxnSpPr><p:cNvPr id="20" name="FreeLine"/><p:cNvCxnSpPr/><p:nvPr/></p:nvCxnSpPr>
7339  <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>
7340</p:cxnSp>"#;
7341        let cxn = parse_cxn_sp(xml).unwrap();
7342        assert_eq!(cxn.id, 20);
7343        assert_eq!(cxn.name, "FreeLine");
7344        assert!(cxn.st_cxn.is_none(), "无 stCxn 时应为 None");
7345        assert!(cxn.end_cxn.is_none(), "无 endCxn 时应为 None");
7346    }
7347
7348    /// 验证 `parse_cxn_sp` 能解析 `<p:style>` 主题样式引用(TODO-002 round-trip 补全)。
7349    #[test]
7350    fn parse_cxn_sp_with_style() {
7351        let xml = r#"<p:cxnSp xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
7352         xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
7353  <p:nvCxnSpPr><p:cNvPr id="30" name="StyledConn"/><p:cNvCxnSpPr/><p:nvPr/></p:nvCxnSpPr>
7354  <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>
7355  <p:style>
7356    <a:lnRef idx="2"><a:schemeClr val="accent1"/></a:lnRef>
7357    <a:fillRef idx="0"><a:schemeClr val="accent2"/></a:fillRef>
7358  </p:style>
7359</p:cxnSp>"#;
7360        let cxn = parse_cxn_sp(xml).expect("parse cxnSp ok");
7361        let style = cxn.style.as_ref().expect("style 应存在");
7362        let ln = style.line_ref.as_ref().expect("line_ref 应存在");
7363        assert_eq!(ln.idx.as_deref(), Some("2"));
7364        assert_eq!(ln.scheme_color.as_deref(), Some("accent1"));
7365        let fill = style.fill_ref.as_ref().expect("fill_ref 应存在");
7366        assert_eq!(fill.idx.as_deref(), Some("0"));
7367        assert_eq!(fill.scheme_color.as_deref(), Some("accent2"));
7368    }
7369
7370    /// 验证 `parse_grp_sp` 能解析组合自身的 `<p:style>` 主题样式引用。
7371    #[test]
7372    fn parse_grp_sp_with_style() {
7373        let xml = r#"<p:grpSp xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
7374         xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
7375  <p:nvGrpSpPr><p:cNvPr id="40" name="StyledGroup"/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
7376  <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>
7377  <p:style>
7378    <a:lnRef idx="1"><a:schemeClr val="tx1"/></a:lnRef>
7379    <a:fontRef idx="minor"><a:schemeClr val="dk1"/></a:fontRef>
7380  </p:style>
7381</p:grpSp>"#;
7382        let grp = parse_grp_sp(xml).expect("parse grpSp ok");
7383        assert_eq!(grp.id, 40);
7384        let style = grp.style.as_ref().expect("style 应存在");
7385        let ln = style.line_ref.as_ref().expect("line_ref 应存在");
7386        assert_eq!(ln.idx.as_deref(), Some("1"));
7387        assert_eq!(ln.scheme_color.as_deref(), Some("tx1"));
7388        let font = style.font_ref.as_ref().expect("font_ref 应存在");
7389        assert_eq!(font.idx.as_deref(), Some("minor"));
7390        assert_eq!(font.scheme_color.as_deref(), Some("dk1"));
7391    }
7392
7393    /// 验证 `parse_grp_sp` 能解析组合形状及其子形状。
7394    ///
7395    /// 组合(`<p:grpSp>`)是 OOXML 中把多个形状打包为一个整体的容器。
7396    /// 子形状可以是 sp/pic/cxnSp/grpSp(嵌套)/graphicFrame。
7397    #[test]
7398    fn parse_grp_sp_extracts_children() {
7399        let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
7400<p:grpSp xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
7401         xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
7402         xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
7403  <p:nvGrpSpPr>
7404    <p:cNvPr id="100" name="Group1"/>
7405    <p:cNvGrpSpPr/>
7406    <p:nvPr/>
7407  </p:nvGrpSpPr>
7408  <p:grpSpPr>
7409    <a:xfrm>
7410      <a:off x="0" y="0"/>
7411      <a:ext cx="8000" cy="6000"/>
7412      <a:chOff x="0" y="0"/>
7413      <a:chExt cx="8000" cy="6000"/>
7414    </a:xfrm>
7415  </p:grpSpPr>
7416  <p:sp>
7417    <p:nvSpPr><p:cNvPr id="101" name="Child1"/><p:cNvSpPr txBox="1"/><p:nvPr/></p:nvSpPr>
7418    <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>
7419    <p:txBody><a:bodyPr/><a:p><a:r><a:t>child text</a:t></a:r></a:p></p:txBody>
7420  </p:sp>
7421  <p:cxnSp>
7422    <p:nvCxnSpPr><p:cNvPr id="102" name="Link"/><p:cNvCxnSpPr/><p:nvPr/></p:nvCxnSpPr>
7423    <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>
7424  </p:cxnSp>
7425</p:grpSp>"#;
7426        let grp = parse_grp_sp(xml).expect("parse grpSp ok");
7427        assert_eq!(grp.id, 100);
7428        assert_eq!(grp.name, "Group1");
7429        assert_eq!(grp.children.len(), 2, "应解析出 2 个子形状");
7430
7431        // 第一个子形状:Sp
7432        match &grp.children[0] {
7433            GroupChild::Sp(sp) => {
7434                assert_eq!(sp.id, 101);
7435                assert_eq!(sp.name, "Child1");
7436                assert_eq!(sp.text.paragraphs[0].runs[0].text, "child text");
7437            }
7438            other => panic!("第一个子形状应为 Sp,实际:{other:?}"),
7439        }
7440        // 第二个子形状:CxnSp
7441        match &grp.children[1] {
7442            GroupChild::CxnSp(cxn) => {
7443                assert_eq!(cxn.id, 102);
7444                assert_eq!(cxn.name, "Link");
7445            }
7446            other => panic!("第二个子形状应为 CxnSp,实际:{other:?}"),
7447        }
7448    }
7449
7450    /// 验证 `parse_graphic_frame` 能解析承载表格的图形框。
7451    ///
7452    /// `<p:graphicFrame>` 是 OOXML 中承载表格/图表等复合图形的容器。
7453    /// 内部 `<a:graphicData uri="...">` 的 uri 决定具体类型:
7454    /// - `http://schemas.openxmlformats.org/drawingml/2006/table` → 表格
7455    #[test]
7456    fn parse_graphic_frame_with_table() {
7457        let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
7458<p:graphicFrame xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
7459                xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
7460                xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
7461  <p:nvGraphicFramePr>
7462    <p:cNvPr id="200" name="TableFrame"/>
7463    <p:cNvGraphicFramePr/>
7464    <p:nvPr/>
7465  </p:nvGraphicFramePr>
7466  <p:xfrm>
7467    <a:off x="457200" y="1828800"/>
7468    <a:ext cx="8229600" cy="1143000"/>
7469  </p:xfrm>
7470  <a:graphic>
7471    <a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/table">
7472      <a:tbl>
7473        <a:tblPr firstRow="1" bandRow="1"/>
7474        <a:tblGrid>
7475          <a:gridCol w="4114800"/>
7476          <a:gridCol w="4114800"/>
7477        </a:tblGrid>
7478        <a:tr h="370840" h="0">
7479          <a:tc>
7480            <a:txBody><a:bodyPr/><a:p><a:r><a:t>A1</a:t></a:r></a:p></a:txBody>
7481            <a:tcPr/>
7482          </a:tc>
7483          <a:tc>
7484            <a:txBody><a:bodyPr/><a:p><a:r><a:t>B1</a:t></a:r></a:p></a:txBody>
7485            <a:tcPr/>
7486          </a:tc>
7487        </a:tr>
7488        <a:tr h="370840" h="0">
7489          <a:tc>
7490            <a:txBody><a:bodyPr/><a:p><a:r><a:t>A2</a:t></a:r></a:p></a:txBody>
7491            <a:tcPr/>
7492          </a:tc>
7493          <a:tc>
7494            <a:txBody><a:bodyPr/><a:p><a:r><a:t>B2</a:t></a:r></a:p></a:txBody>
7495            <a:tcPr/>
7496          </a:tc>
7497        </a:tr>
7498      </a:tbl>
7499    </a:graphicData>
7500  </a:graphic>
7501</p:graphicFrame>"#;
7502        let frame = parse_graphic_frame(xml).expect("parse graphicFrame ok");
7503        assert_eq!(frame.id, 200);
7504        assert_eq!(frame.name, "TableFrame");
7505        // xfrm
7506        assert_eq!(frame.properties.xfrm.off_x.unwrap().value(), 457200);
7507        assert_eq!(frame.properties.xfrm.off_y.unwrap().value(), 1828800);
7508        assert_eq!(frame.properties.xfrm.ext_cx.unwrap().value(), 8229600);
7509        assert_eq!(frame.properties.xfrm.ext_cy.unwrap().value(), 1143000);
7510        // 表格内容
7511        match &frame.graphic {
7512            crate::oxml::shape::Graphic::Table(tbl) => {
7513                assert_eq!(tbl.cols.len(), 2, "应有 2 列");
7514                assert_eq!(tbl.rows.len(), 2, "应有 2 行");
7515                assert_eq!(tbl.cols[0].width.value(), 4114800);
7516                // 验证单元格文本
7517                let cell_a1 = &tbl.rows[0].cells[0];
7518                let text_a1: String = cell_a1
7519                    .text
7520                    .paragraphs
7521                    .iter()
7522                    .flat_map(|p| p.runs.iter().map(|r| r.text.as_str()))
7523                    .collect();
7524                assert_eq!(text_a1, "A1");
7525                let cell_b2 = &tbl.rows[1].cells[1];
7526                let text_b2: String = cell_b2
7527                    .text
7528                    .paragraphs
7529                    .iter()
7530                    .flat_map(|p| p.runs.iter().map(|r| r.text.as_str()))
7531                    .collect();
7532                assert_eq!(text_b2, "B2");
7533            }
7534            _ => panic!("期望 Table 变体"),
7535        }
7536    }
7537
7538    /// 验证 `parse_sld` 能解析包含**多种形状类型**的 slide(Sp + Pic + CxnSp + Group)。
7539    ///
7540    /// 这是 round-trip 测试的基础:如果 parse 阶段就丢失形状,save 后内容必然缺失。
7541    #[test]
7542    fn parse_sld_with_mixed_shape_types() {
7543        let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
7544<p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
7545       xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
7546       xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
7547  <p:cSld>
7548    <p:spTree>
7549      <p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
7550      <p:grpSpPr><a:xfrm/></p:grpSpPr>
7551      <p:sp>
7552        <p:nvSpPr><p:cNvPr id="2" name="TextBox"/><p:cNvSpPr txBox="1"/><p:nvPr/></p:nvSpPr>
7553        <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>
7554        <p:txBody><a:bodyPr/><a:p><a:r><a:t>text</a:t></a:r></a:p></p:txBody>
7555      </p:sp>
7556      <p:pic>
7557        <p:nvPicPr><p:cNvPr id="3" name="Pic1"/><p:cNvPicPr/><p:nvPr/></p:nvPicPr>
7558        <p:blipFill><a:blip r:embed="rIdImg1"/></p:blipFill>
7559        <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>
7560      </p:pic>
7561      <p:cxnSp>
7562        <p:nvCxnSpPr><p:cNvPr id="4" name="Conn1"/><p:cNvCxnSpPr/><p:nvPr/></p:nvCxnSpPr>
7563        <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>
7564      </p:cxnSp>
7565      <p:grpSp>
7566        <p:nvGrpSpPr><p:cNvPr id="5" name="Group1"/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
7567        <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>
7568        <p:sp>
7569          <p:nvSpPr><p:cNvPr id="6" name="Inner"/><p:cNvSpPr txBox="1"/><p:nvPr/></p:nvSpPr>
7570          <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>
7571          <p:txBody><a:bodyPr/><a:p><a:r><a:t>inner</a:t></a:r></a:p></p:txBody>
7572        </p:sp>
7573      </p:grpSp>
7574    </p:spTree>
7575  </p:cSld>
7576  <p:clrMapOvr/>
7577</p:sld>"#;
7578        let sld = parse_sld(xml).expect("parse sld ok");
7579        assert_eq!(sld.shapes.len(), 4, "应解析出 4 个形状");
7580
7581        // 验证每个形状的类型和关键字段
7582        assert!(
7583            matches!(&sld.shapes[0], OxmlSlideShape::Sp(s) if s.id == 2 && s.name == "TextBox")
7584        );
7585        assert!(
7586            matches!(&sld.shapes[1], OxmlSlideShape::Pic(p) if p.id == 3 && p.rid == "rIdImg1")
7587        );
7588        assert!(
7589            matches!(&sld.shapes[2], OxmlSlideShape::CxnSp(c) if c.id == 4 && c.name == "Conn1")
7590        );
7591        match &sld.shapes[3] {
7592            OxmlSlideShape::Group(grp) => {
7593                assert_eq!(grp.id, 5);
7594                assert_eq!(grp.name, "Group1");
7595                assert_eq!(grp.children.len(), 1, "组合内应有 1 个子形状");
7596                assert!(
7597                    matches!(&grp.children[0], GroupChild::Sp(s) if s.id == 6 && s.name == "Inner")
7598                );
7599            }
7600            other => panic!("第 4 个形状应为 Group,实际:{other:?}"),
7601        }
7602    }
7603
7604    /// **Round-trip 保留测试**:parse → write → parse,验证关键字段不丢失。
7605    ///
7606    /// 这是 TODO-041 的核心测试:打开已有 PPTX 后修改再保存,
7607    /// 形状的 id/name/文本/几何坐标/挂接信息必须全部保留。
7608    #[test]
7609    fn round_trip_sld_preserves_shapes() {
7610        let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
7611<p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
7612       xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
7613       xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
7614  <p:cSld>
7615    <p:spTree>
7616      <p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
7617      <p:grpSpPr><a:xfrm/></p:grpSpPr>
7618      <p:sp>
7619        <p:nvSpPr><p:cNvPr id="2" name="TB1"/><p:cNvSpPr txBox="1"/><p:nvPr/></p:nvSpPr>
7620        <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>
7621        <p:txBody><a:bodyPr/><a:p><a:r><a:t>round-trip</a:t></a:r></a:p></p:txBody>
7622      </p:sp>
7623      <p:cxnSp>
7624        <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>
7625        <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>
7626      </p:cxnSp>
7627    </p:spTree>
7628  </p:cSld>
7629  <p:clrMapOvr/>
7630</p:sld>"#;
7631
7632        // 第一次解析
7633        let sld1 = parse_sld(xml).expect("first parse ok");
7634        assert_eq!(sld1.shapes.len(), 2);
7635
7636        // 序列化回 XML
7637        let rewritten = sld1.to_xml();
7638
7639        // 第二次解析(round-trip)
7640        let sld2 = parse_sld(&rewritten).expect("second parse ok");
7641        assert_eq!(sld2.shapes.len(), 2, "round-trip 后形状数应不变");
7642
7643        // 验证 Sp
7644        match (&sld1.shapes[0], &sld2.shapes[0]) {
7645            (OxmlSlideShape::Sp(a), OxmlSlideShape::Sp(b)) => {
7646                assert_eq!(a.id, b.id, "Sp id 应保留");
7647                assert_eq!(a.name, b.name, "Sp name 应保留");
7648                assert_eq!(
7649                    a.text.paragraphs[0].runs[0].text, b.text.paragraphs[0].runs[0].text,
7650                    "Sp 文本应保留"
7651                );
7652                // 几何坐标
7653                assert_eq!(
7654                    a.properties.xfrm.off_x, b.properties.xfrm.off_x,
7655                    "Sp off_x 应保留"
7656                );
7657                assert_eq!(
7658                    a.properties.xfrm.ext_cx, b.properties.xfrm.ext_cx,
7659                    "Sp ext_cx 应保留"
7660                );
7661            }
7662            _ => panic!("形状类型不匹配"),
7663        }
7664
7665        // 验证 CxnSp(含挂接信息)
7666        match (&sld1.shapes[1], &sld2.shapes[1]) {
7667            (OxmlSlideShape::CxnSp(a), OxmlSlideShape::CxnSp(b)) => {
7668                assert_eq!(a.id, b.id, "CxnSp id 应保留");
7669                assert_eq!(a.name, b.name, "CxnSp name 应保留");
7670                assert_eq!(a.st_cxn, b.st_cxn, "stCxn 挂接应保留");
7671                assert_eq!(a.end_cxn, b.end_cxn, "endCxn 挂接应保留");
7672            }
7673            _ => panic!("形状类型不匹配"),
7674        }
7675    }
7676
7677    /// **Round-trip 表格保留测试**:parse → write → parse,验证表格结构/单元格文本不丢失。
7678    #[test]
7679    fn round_trip_table_preserves_cells() {
7680        let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
7681<p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
7682       xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
7683       xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
7684  <p:cSld>
7685    <p:spTree>
7686      <p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
7687      <p:grpSpPr><a:xfrm/></p:grpSpPr>
7688      <p:graphicFrame>
7689        <p:nvGraphicFramePr><p:cNvPr id="10" name="Tbl1"/><p:cNvGraphicFramePr/><p:nvPr/></p:nvGraphicFramePr>
7690        <p:xfrm><a:off x="0" y="0"/><a:ext cx="8000" cy="4000"/></p:xfrm>
7691        <a:graphic>
7692          <a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/table">
7693            <a:tbl>
7694              <a:tblPr/>
7695              <a:tblGrid><a:gridCol w="4000"/><a:gridCol w="4000"/></a:tblGrid>
7696              <a:tr h="2000">
7697                <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>
7698                <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>
7699              </a:tr>
7700            </a:tbl>
7701          </a:graphicData>
7702        </a:graphic>
7703      </p:graphicFrame>
7704    </p:spTree>
7705  </p:cSld>
7706  <p:clrMapOvr/>
7707</p:sld>"#;
7708
7709        let sld1 = parse_sld(xml).expect("first parse ok");
7710        let rewritten = sld1.to_xml();
7711        let sld2 = parse_sld(&rewritten).expect("second parse ok");
7712
7713        assert_eq!(sld1.shapes.len(), sld2.shapes.len(), "形状数应一致");
7714        match (&sld1.shapes[0], &sld2.shapes[0]) {
7715            (OxmlSlideShape::GraphicFrame(f1), OxmlSlideShape::GraphicFrame(f2)) => {
7716                assert_eq!(f1.id, f2.id, "GraphicFrame id 应保留");
7717                assert_eq!(f1.name, f2.name, "GraphicFrame name 应保留");
7718                match (&f1.graphic, &f2.graphic) {
7719                    (
7720                        crate::oxml::shape::Graphic::Table(t1),
7721                        crate::oxml::shape::Graphic::Table(t2),
7722                    ) => {
7723                        assert_eq!(t1.cols.len(), t2.cols.len(), "列数应保留");
7724                        assert_eq!(t1.rows.len(), t2.rows.len(), "行数应保留");
7725                        // 验证单元格文本
7726                        let get_text = |t: &crate::oxml::table::Table| -> String {
7727                            t.rows[0].cells[0]
7728                                .text
7729                                .paragraphs
7730                                .iter()
7731                                .flat_map(|p| p.runs.iter().map(|r| r.text.as_str()))
7732                                .collect()
7733                        };
7734                        assert_eq!(get_text(t1), get_text(t2), "单元格文本应保留");
7735                        assert_eq!(get_text(t1), "R1C1");
7736                    }
7737                    _ => panic!("期望两侧均为 Table 变体"),
7738                }
7739            }
7740            _ => panic!("形状类型不匹配"),
7741        }
7742    }
7743
7744    /// 验证 `parse_sld` 对**空 spTree**(无形状)的容错。
7745    #[test]
7746    fn parse_sld_empty_sp_tree() {
7747        let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
7748<p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
7749       xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
7750  <p:cSld>
7751    <p:spTree>
7752      <p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
7753      <p:grpSpPr><a:xfrm/></p:grpSpPr>
7754    </p:spTree>
7755  </p:cSld>
7756  <p:clrMapOvr/>
7757</p:sld>"#;
7758        let sld = parse_sld(xml).expect("parse ok");
7759        assert_eq!(sld.shapes.len(), 0, "空 spTree 应解析出 0 个形状");
7760    }
7761
7762    /// 验证 `parse_grp_sp` 能正确解析**嵌套组合**(grpSp 内有 grpSp)。
7763    ///
7764    /// 早期版本因 `if local == b"grpSp"` 分支重复,嵌套组合被根元素分支吞掉,
7765    /// 导致嵌套 grpSp 的子形状全部丢失。本测试回归此 bug。
7766    #[test]
7767    fn parse_grp_sp_nested_group() {
7768        let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
7769<p:grpSp xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
7770         xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
7771  <p:nvGrpSpPr><p:cNvPr id="1" name="Outer"/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
7772  <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>
7773  <p:sp>
7774    <p:nvSpPr><p:cNvPr id="2" name="OuterChild"/><p:cNvSpPr txBox="1"/><p:nvPr/></p:nvSpPr>
7775    <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>
7776    <p:txBody><a:bodyPr/><a:p><a:r><a:t>outer</a:t></a:r></a:p></p:txBody>
7777  </p:sp>
7778  <p:grpSp>
7779    <p:nvGrpSpPr><p:cNvPr id="3" name="Inner"/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
7780    <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>
7781    <p:sp>
7782      <p:nvSpPr><p:cNvPr id="4" name="InnerChild"/><p:cNvSpPr txBox="1"/><p:nvPr/></p:nvSpPr>
7783      <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>
7784      <p:txBody><a:bodyPr/><a:p><a:r><a:t>inner</a:t></a:r></a:p></p:txBody>
7785    </p:sp>
7786  </p:grpSp>
7787</p:grpSp>"#;
7788        let grp = parse_grp_sp(xml).expect("parse ok");
7789        assert_eq!(grp.id, 1);
7790        assert_eq!(grp.name, "Outer");
7791        assert_eq!(
7792            grp.children.len(),
7793            2,
7794            "外层组合应有 2 个子形状(sp + 嵌套 grpSp)"
7795        );
7796
7797        // 第一个子形状:外层 Sp
7798        assert!(
7799            matches!(&grp.children[0], GroupChild::Sp(s) if s.id == 2 && s.name == "OuterChild")
7800        );
7801
7802        // 第二个子形状:嵌套组合
7803        match &grp.children[1] {
7804            GroupChild::Group(inner) => {
7805                assert_eq!(inner.id, 3);
7806                assert_eq!(inner.name, "Inner");
7807                assert_eq!(inner.children.len(), 1, "内层组合应有 1 个子形状");
7808                assert!(
7809                    matches!(&inner.children[0], GroupChild::Sp(s) if s.id == 4 && s.name == "InnerChild")
7810                );
7811            }
7812            other => panic!("第二个子形状应为 Group,实际:{other:?}"),
7813        }
7814    }
7815
7816    /// 验证 `parse_sppr` 能解析**线性渐变填充**(`<a:gradFill><a:gsLst>...<a:lin ang="..."/></a:gradFill>`)。
7817    ///
7818    /// 这是 TODO-003 的核心测试:确保渐变光轨、角度、颜色全部正确提取。
7819    #[test]
7820    fn parse_sppr_grad_fill_linear() {
7821        let xml = r#"<p:spPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
7822                    xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
7823  <a:xfrm><a:off x="100" y="200"/><a:ext cx="300" cy="400"/></a:xfrm>
7824  <a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
7825  <a:gradFill flip="none" rotWithShape="1">
7826    <a:gsLst>
7827      <a:gs pos="0"><a:srgbClr val="FF0000"/></a:gs>
7828      <a:gs pos="100000"><a:srgbClr val="0000FF"/></a:gs>
7829    </a:gsLst>
7830    <a:lin ang="5400000" scaled="1"/>
7831  </a:gradFill>
7832</p:spPr>"#;
7833        let sp = parse_sppr(xml).expect("parse ok");
7834        match &sp.fill {
7835            Fill::Gradient(grad) => {
7836                assert_eq!(grad.stops.len(), 2, "应有 2 个渐变光轨");
7837                assert_eq!(grad.stops[0].pos, 0);
7838                assert_eq!(grad.stops[1].pos, 100000);
7839                // 验证颜色
7840                assert!(
7841                    matches!(grad.stops[0].color, Color::RGB(c) if c.0 == 0xFF && c.1 == 0x00 && c.2 == 0x00)
7842                );
7843                assert!(
7844                    matches!(grad.stops[1].color, Color::RGB(c) if c.0 == 0x00 && c.1 == 0x00 && c.2 == 0xFF)
7845                );
7846                // 验证线性角度(5400000 = 90° = 向下)
7847                assert_eq!(grad.gradient_type, GradientType::Linear(5400000));
7848                // 验证属性
7849                assert_eq!(grad.flip.as_deref(), Some("none"));
7850                assert_eq!(grad.rot_with_shape, Some(true));
7851            }
7852            other => panic!("fill 应为 Gradient,实际:{other:?}"),
7853        }
7854    }
7855
7856    /// 验证 `parse_sppr` 能解析**路径渐变填充**(`<a:gradFill><a:path path="circle"/></a:gradFill>`)。
7857    #[test]
7858    fn parse_sppr_grad_fill_path() {
7859        let xml = r#"<p:spPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
7860                    xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
7861  <a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
7862  <a:gradFill>
7863    <a:gsLst>
7864      <a:gs pos="0"><a:srgbClr val="00FF00"/></a:gs>
7865      <a:gs pos="50000"><a:srgbClr val="FFFF00"/></a:gs>
7866      <a:gs pos="100000"><a:srgbClr val="FF00FF"/></a:gs>
7867    </a:gsLst>
7868    <a:path path="circle"><a:fillToRect l="50000" t="50000" r="50000" b="50000"/></a:path>
7869  </a:gradFill>
7870</p:spPr>"#;
7871        let sp = parse_sppr(xml).expect("parse ok");
7872        match &sp.fill {
7873            Fill::Gradient(grad) => {
7874                assert_eq!(grad.stops.len(), 3, "应有 3 个渐变光轨");
7875                assert_eq!(grad.stops[1].pos, 50000);
7876                // 验证路径渐变类型
7877                assert_eq!(grad.gradient_type, GradientType::Path(GradientPath::Circle));
7878            }
7879            other => panic!("fill 应为 Gradient,实际:{other:?}"),
7880        }
7881    }
7882
7883    /// 验证 `parse_sppr` 能解析**图案填充**(`<a:pattFill prst="..."><a:fgClr/>...<a:bgClr/>...</a:pattFill>`)。
7884    #[test]
7885    fn parse_sppr_patt_fill() {
7886        let xml = r#"<p:spPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
7887                    xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
7888  <a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
7889  <a:pattFill prst="pct5">
7890    <a:fgClr><a:srgbClr val="FF0000"/></a:fgClr>
7891    <a:bgClr><a:srgbClr val="FFFFFF"/></a:bgClr>
7892  </a:pattFill>
7893</p:spPr>"#;
7894        let sp = parse_sppr(xml).expect("parse ok");
7895        match &sp.fill {
7896            Fill::Pattern(patt) => {
7897                assert_eq!(patt.prst, "pct5", "预置图案应为 pct5");
7898                // 前景色:红色
7899                assert!(
7900                    matches!(patt.fg_color, Color::RGB(c) if c.0 == 0xFF && c.1 == 0x00 && c.2 == 0x00)
7901                );
7902                // 背景色:白色
7903                assert!(
7904                    matches!(patt.bg_color, Color::RGB(c) if c.0 == 0xFF && c.1 == 0xFF && c.2 == 0xFF)
7905                );
7906            }
7907            other => panic!("fill 应为 Pattern,实际:{other:?}"),
7908        }
7909    }
7910
7911    /// 验证 `parse_sppr` 能解析**图片填充(blipFill)**的 stretch 模式(TODO-003/048)。
7912    ///
7913    /// 这是 TODO-003/048 的核心测试:确保 `<a:blip r:embed="rIdN"/>` + `<a:stretch>`
7914    /// 被正确解析为 `Fill::Blip { rid, mode: Stretch }`。
7915    #[test]
7916    fn parse_sppr_blip_fill_stretch() {
7917        let xml = r#"<p:spPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
7918                    xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
7919                    xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
7920  <a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
7921  <a:blipFill>
7922    <a:blip r:embed="rId1"/>
7923    <a:stretch><a:fillRect/></a:stretch>
7924  </a:blipFill>
7925</p:spPr>"#;
7926        let sp = parse_sppr(xml).expect("parse ok");
7927        match &sp.fill {
7928            Fill::Blip { rid, mode } => {
7929                assert_eq!(rid, "rId1", "rid 应为 rId1");
7930                assert!(
7931                    matches!(mode, crate::oxml::sppr::BlipFillMode::Stretch),
7932                    "mode 应为 Stretch,实际:{:?}",
7933                    mode
7934                );
7935            }
7936            other => panic!("fill 应为 Blip,实际:{other:?}"),
7937        }
7938    }
7939
7940    /// 验证 `parse_sppr` 能解析**图片填充(blipFill)**的 tile 模式 + 自闭合 blip。
7941    #[test]
7942    fn parse_sppr_blip_fill_tile() {
7943        let xml = r#"<p:spPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
7944                    xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
7945                    xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
7946  <a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
7947  <a:blipFill>
7948    <a:blip r:embed="rIdImg7"/>
7949    <a:tile tx="100" ty="200" sx="50000" sy="50000" flip="x" algn="ctr"/>
7950  </a:blipFill>
7951</p:spPr>"#;
7952        let sp = parse_sppr(xml).expect("parse ok");
7953        match &sp.fill {
7954            Fill::Blip { rid, mode } => {
7955                assert_eq!(rid, "rIdImg7", "rid 应为 rIdImg7");
7956                match mode {
7957                    crate::oxml::sppr::BlipFillMode::Tile {
7958                        tx,
7959                        ty,
7960                        sx,
7961                        sy,
7962                        flip,
7963                        algn,
7964                    } => {
7965                        assert_eq!(*tx, Some(100), "tx 应为 100");
7966                        assert_eq!(*ty, Some(200), "ty 应为 200");
7967                        assert_eq!(*sx, Some(50000), "sx 应为 50000");
7968                        assert_eq!(*sy, Some(50000), "sy 应为 50000");
7969                        assert_eq!(flip.as_deref(), Some("x"), "flip 应为 x");
7970                        assert_eq!(algn.as_deref(), Some("ctr"), "algn 应为 ctr");
7971                    }
7972                    other => panic!("mode 应为 Tile,实际:{other:?}"),
7973                }
7974            }
7975            other => panic!("fill 应为 Blip,实际:{other:?}"),
7976        }
7977    }
7978
7979    /// 验证 `parse_sppr` 能解析**自闭合 blipFill 子元素**(`<a:blip/>` + `<a:stretch/>` 均为 Empty)。
7980    #[test]
7981    fn parse_sppr_blip_fill_self_closing() {
7982        let xml = r#"<p:spPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
7983                    xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
7984                    xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
7985  <a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
7986  <a:blipFill>
7987    <a:blip r:embed="rId2"/>
7988    <a:stretch/>
7989  </a:blipFill>
7990</p:spPr>"#;
7991        let sp = parse_sppr(xml).expect("parse ok");
7992        match &sp.fill {
7993            Fill::Blip { rid, mode } => {
7994                assert_eq!(rid, "rId2");
7995                assert!(matches!(mode, crate::oxml::sppr::BlipFillMode::Stretch));
7996            }
7997            other => panic!("fill 应为 Blip,实际:{other:?}"),
7998        }
7999    }
8000
8001    /// 验证 `parse_sppr` 能解析**自闭合的渐变填充**(`<a:lin/>` 为 Empty 事件)。
8002    ///
8003    /// 某些 PPTX 生成器会把 `<a:lin ang="..." scaled="..."/>` 写成自闭合标签,
8004    /// 本测试确保 Empty 分支也能正确提取角度。
8005    #[test]
8006    fn parse_sppr_grad_fill_self_closing_lin() {
8007        let xml = r#"<p:spPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
8008                    xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
8009  <a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
8010  <a:gradFill>
8011    <a:gsLst>
8012      <a:gs pos="0"><a:srgbClr val="FF0000"/></a:gs>
8013      <a:gs pos="100000"><a:srgbClr val="0000FF"/></a:gs>
8014    </a:gsLst>
8015    <a:lin ang="2700000" scaled="0"/>
8016  </a:gradFill>
8017</p:spPr>"#;
8018        let sp = parse_sppr(xml).expect("parse ok");
8019        match &sp.fill {
8020            Fill::Gradient(grad) => {
8021                assert_eq!(grad.gradient_type, GradientType::Linear(2700000));
8022                assert_eq!(grad.stops.len(), 2);
8023            }
8024            other => panic!("fill 应为 Gradient,实际:{other:?}"),
8025        }
8026    }
8027
8028    /// 验证 `parse_ln` 能解析**箭头端点**(`<a:headEnd>` / `<a:tailEnd>`)。
8029    ///
8030    /// 这是 TODO-012 的核心测试:确保线条起止箭头的 type/w/len 属性全部正确提取。
8031    #[test]
8032    fn parse_sppr_ln_with_arrow_heads() {
8033        let xml = r#"<p:spPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
8034                    xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
8035  <a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
8036  <a:ln w="12700">
8037    <a:solidFill><a:srgbClr val="000000"/></a:solidFill>
8038    <a:headEnd type="triangle" w="lg" len="med"/>
8039    <a:tailEnd type="stealth" w="sm" len="lg"/>
8040  </a:ln>
8041</p:spPr>"#;
8042        let sp = parse_sppr(xml).expect("parse ok");
8043        let ln = sp.line.expect("应有 line");
8044        // headEnd
8045        let head = ln.head_end.expect("应有 headEnd");
8046        assert_eq!(head.arrow_type, ArrowType::Triangle);
8047        assert_eq!(head.width, ArrowSize::Large);
8048        assert_eq!(head.length, ArrowSize::Medium);
8049        // tailEnd
8050        let tail = ln.tail_end.expect("应有 tailEnd");
8051        assert_eq!(tail.arrow_type, ArrowType::Stealth);
8052        assert_eq!(tail.width, ArrowSize::Small);
8053        assert_eq!(tail.length, ArrowSize::Large);
8054    }
8055
8056    /// 验证 `parse_ln` 能解析**连接类型**(`<a:round>` / `<a:miter>` / `<a:bevel>`)。
8057    #[test]
8058    fn parse_sppr_ln_with_join() {
8059        // round 连接
8060        let xml = r#"<p:spPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
8061                    xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
8062  <a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
8063  <a:ln><a:solidFill><a:srgbClr val="000000"/></a:solidFill><a:round/></a:ln>
8064</p:spPr>"#;
8065        let sp = parse_sppr(xml).expect("parse ok");
8066        let ln = sp.line.expect("应有 line");
8067        assert_eq!(ln.join, Some(LineJoin::Round));
8068
8069        // miter 连接(带 lim 属性)
8070        let xml = r#"<p:spPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
8071                    xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
8072  <a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
8073  <a:ln><a:solidFill><a:srgbClr val="000000"/></a:solidFill><a:miter lim="600000"/></a:ln>
8074</p:spPr>"#;
8075        let sp = parse_sppr(xml).expect("parse ok");
8076        let ln = sp.line.expect("应有 line");
8077        assert_eq!(ln.join, Some(LineJoin::Miter(600000)));
8078
8079        // bevel 连接
8080        let xml = r#"<p:spPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
8081                    xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
8082  <a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
8083  <a:ln><a:solidFill><a:srgbClr val="000000"/></a:solidFill><a:bevel/></a:ln>
8084</p:spPr>"#;
8085        let sp = parse_sppr(xml).expect("parse ok");
8086        let ln = sp.line.expect("应有 line");
8087        assert_eq!(ln.join, Some(LineJoin::Bevel));
8088    }
8089
8090    /// 验证 `parse_ln` 能解析**线条渐变填充**(`<a:gradFill>` 在 `<a:ln>` 内)。
8091    #[test]
8092    fn parse_sppr_ln_with_grad_fill() {
8093        let xml = r#"<p:spPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
8094                    xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
8095  <a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
8096  <a:ln w="12700">
8097    <a:gradFill>
8098      <a:gsLst>
8099        <a:gs pos="0"><a:srgbClr val="FF0000"/></a:gs>
8100        <a:gs pos="100000"><a:srgbClr val="00FF00"/></a:gs>
8101      </a:gsLst>
8102      <a:lin ang="0" scaled="1"/>
8103    </a:gradFill>
8104  </a:ln>
8105</p:spPr>"#;
8106        let sp = parse_sppr(xml).expect("parse ok");
8107        let ln = sp.line.expect("应有 line");
8108        match &ln.fill {
8109            Fill::Gradient(grad) => {
8110                assert_eq!(grad.stops.len(), 2);
8111                assert_eq!(grad.gradient_type, GradientType::Linear(0));
8112            }
8113            other => panic!("line fill 应为 Gradient,实际:{other:?}"),
8114        }
8115    }
8116
8117    /// 验证 `parse_ln` 能解析**线条图案填充**(`<a:pattFill>` 在 `<a:ln>` 内)。
8118    #[test]
8119    fn parse_sppr_ln_with_patt_fill() {
8120        let xml = r#"<p:spPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
8121                    xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
8122  <a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
8123  <a:ln w="12700">
8124    <a:pattFill prst="cross">
8125      <a:fgClr><a:srgbClr val="FF0000"/></a:fgClr>
8126      <a:bgClr><a:srgbClr val="FFFFFF"/></a:bgClr>
8127    </a:pattFill>
8128  </a:ln>
8129</p:spPr>"#;
8130        let sp = parse_sppr(xml).expect("parse ok");
8131        let ln = sp.line.expect("应有 line");
8132        match &ln.fill {
8133            Fill::Pattern(patt) => {
8134                assert_eq!(patt.prst, "cross");
8135                assert!(matches!(patt.fg_color, Color::RGB(c) if c.0 == 0xFF));
8136                assert!(
8137                    matches!(patt.bg_color, Color::RGB(c) if c.0 == 0xFF && c.1 == 0xFF && c.2 == 0xFF)
8138                );
8139            }
8140            other => panic!("line fill 应为 Pattern,实际:{other:?}"),
8141        }
8142    }
8143
8144    /// 验证 `parse_sppr` 能解析 `<a:effectLst>` 中的外阴影(TODO-011)。
8145    #[test]
8146    fn parse_sppr_effect_lst_outer_shadow() {
8147        let xml = r#"<p:spPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
8148                    xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
8149  <a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
8150  <a:effectLst>
8151    <a:outerShdw blurRad="40000" dist="38100" dir="2700000" rotWithShape="0">
8152      <a:srgbClr val="000000"/>
8153    </a:outerShdw>
8154  </a:effectLst>
8155</p:spPr>"#;
8156        let sp = parse_sppr(xml).expect("parse ok");
8157        let effects = sp.effects.expect("应有 effects");
8158        let shdw = effects.outer_shadow.expect("应有 outer_shadow");
8159        assert_eq!(shdw.dir, 2_700_000);
8160        assert_eq!(shdw.dist, 38100);
8161        assert_eq!(shdw.blur_rad, 40000);
8162        assert_eq!(shdw.rot_with_shape, Some(false));
8163        assert!(matches!(shdw.color, Color::RGB(c) if c.0 == 0x00 && c.1 == 0x00 && c.2 == 0x00));
8164    }
8165
8166    /// 验证 `parse_sppr` 能解析发光和柔化边缘效果。
8167    #[test]
8168    fn parse_sppr_effect_lst_glow_and_soft_edge() {
8169        let xml = r#"<p:spPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
8170                    xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
8171  <a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
8172  <a:effectLst>
8173    <a:glow rad="50000">
8174      <a:srgbClr val="FF00FF"/>
8175    </a:glow>
8176    <a:softEdge rad="25000"/>
8177  </a:effectLst>
8178</p:spPr>"#;
8179        let sp = parse_sppr(xml).expect("parse ok");
8180        let effects = sp.effects.expect("应有 effects");
8181        let glow = effects.glow.expect("应有 glow");
8182        assert_eq!(glow.rad, 50000);
8183        assert!(matches!(glow.color, Color::RGB(c) if c.0 == 0xFF && c.1 == 0x00 && c.2 == 0xFF));
8184        let se = effects.soft_edge.expect("应有 soft_edge");
8185        assert_eq!(se.rad, 25000);
8186    }
8187
8188    /// 验证 `parse_sppr` 能解析反射效果(仅属性)。
8189    #[test]
8190    fn parse_sppr_effect_lst_reflection() {
8191        let xml = r#"<p:spPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
8192                    xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
8193  <a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
8194  <a:effectLst>
8195    <a:reflection blurRad="50000" stA="52000" stPos="0" endA="30000" endPos="50000" dist="38100" dir="5400000"/>
8196  </a:effectLst>
8197</p:spPr>"#;
8198        let sp = parse_sppr(xml).expect("parse ok");
8199        let effects = sp.effects.expect("应有 effects");
8200        let refl = effects.reflection.expect("应有 reflection");
8201        assert_eq!(refl.blur_rad, Some(50000));
8202        assert_eq!(refl.st_a, Some(52000));
8203        assert_eq!(refl.st_pos, Some(0));
8204        assert_eq!(refl.end_a, Some(30000));
8205        assert_eq!(refl.end_pos, Some(50000));
8206        assert_eq!(refl.dist, Some(38100));
8207        assert_eq!(refl.dir, Some(5_400_000));
8208    }
8209
8210    /// 验证 `Cell` 的 `gridSpan`/`rowSpan`/`hMerge`/`vMerge` 属性在 XML 往返中保持不变。
8211    ///
8212    /// 这是 TODO-029 的核心测试:确保合并单元格信息在序列化 → 解析后不丢失。
8213    #[test]
8214    fn round_trip_cell_merge_attributes() {
8215        let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
8216<p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
8217       xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
8218       xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
8219  <p:cSld><p:spTree>
8220    <p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
8221    <p:grpSpPr><a:xfrm/></p:grpSpPr>
8222    <p:graphicFrame>
8223      <p:nvGraphicFramePr><p:cNvPr id="2" name="MergedTable"/><p:cNvGraphicFramePr/><p:nvPr/></p:nvGraphicFramePr>
8224      <p:xfrm><a:off x="0" y="0"/><a:ext cx="6000000" cy="4000000"/></p:xfrm>
8225      <a:graphic>
8226        <a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/table">
8227          <a:tbl>
8228            <a:tblPr><a:tableStyleId>{5940675A-B579-460E-94D1-54222C63F5DA}</a:tableStyleId></a:tblPr>
8229            <a:tblGrid>
8230              <a:gridCol w="2000000"/>
8231              <a:gridCol w="2000000"/>
8232              <a:gridCol w="2000000"/>
8233            </a:tblGrid>
8234            <a:tr h="2000000">
8235              <a:tc gridSpan="3" rowSpan="2">
8236                <a:txBody><a:bodyPr/><a:p><a:r><a:t>Merged</a:t></a:r></a:p></a:txBody>
8237                <a:tcPr/>
8238              </a:tc>
8239            </a:tr>
8240            <a:tr h="2000000">
8241              <a:tc hMerge="1"><a:txBody><a:bodyPr/><a:p/></a:txBody><a:tcPr/></a:tc>
8242              <a:tc hMerge="1"><a:txBody><a:bodyPr/><a:p/></a:txBody><a:tcPr/></a:tc>
8243              <a:tc vMerge="1"><a:txBody><a:bodyPr/><a:p/></a:txBody><a:tcPr/></a:tc>
8244            </a:tr>
8245          </a:tbl>
8246        </a:graphicData>
8247      </a:graphic>
8248    </p:graphicFrame>
8249  </p:spTree></p:cSld>
8250</p:sld>"#;
8251        let sld = parse_sld(xml).expect("parse ok");
8252        // 找到 graphicFrame
8253        let frame = sld
8254            .shapes
8255            .iter()
8256            .find_map(|s| {
8257                if let OxmlSlideShape::GraphicFrame(g) = s {
8258                    Some(g)
8259                } else {
8260                    None
8261                }
8262            })
8263            .expect("应有 graphicFrame");
8264        match &frame.graphic {
8265            crate::oxml::shape::Graphic::Table(tbl) => {
8266                assert_eq!(tbl.rows.len(), 2, "应有 2 行");
8267                // 验证 tableStyleId 解析(TODO-030)
8268                assert_eq!(
8269                    tbl.table_style
8270                        .as_ref()
8271                        .expect("应有 table_style")
8272                        .style_id(),
8273                    "{5940675A-B579-460E-94D1-54222C63F5DA}"
8274                );
8275                // 第一行:合并源单元格
8276                let origin = &tbl.rows[0].cells[0];
8277                assert_eq!(origin.grid_span, 3, "gridSpan 应为 3");
8278                assert_eq!(origin.row_span, 2, "rowSpan 应为 2");
8279                assert!(!origin.h_merge, "合并源不应有 hMerge");
8280                assert!(!origin.v_merge, "合并源不应有 vMerge");
8281                // 第二行:被合并方
8282                let h1 = &tbl.rows[1].cells[0];
8283                assert!(h1.h_merge, "应有 hMerge");
8284                let v1 = &tbl.rows[1].cells[2];
8285                assert!(v1.v_merge, "应有 vMerge");
8286            }
8287            _ => panic!("期望 Table 变体"),
8288        }
8289    }
8290
8291    /// 验证表格样式(`<a:tableStyleId>`)在序列化 → 解析后保持不变。
8292    ///
8293    /// 这是 TODO-030 的核心 round-trip 测试。
8294    #[test]
8295    fn round_trip_table_style_id() {
8296        let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
8297<p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
8298       xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
8299       xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
8300  <p:cSld><p:spTree>
8301    <p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
8302    <p:grpSpPr><a:xfrm/></p:grpSpPr>
8303    <p:graphicFrame>
8304      <p:nvGraphicFramePr><p:cNvPr id="2" name="StyledTable"/><p:cNvGraphicFramePr/><p:nvPr/></p:nvGraphicFramePr>
8305      <p:xfrm><a:off x="0" y="0"/><a:ext cx="4000000" cy="2000000"/></p:xfrm>
8306      <a:graphic>
8307        <a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/table">
8308          <a:tbl>
8309            <a:tblPr><a:tableStyleId>{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}</a:tableStyleId></a:tblPr>
8310            <a:tblGrid><a:gridCol w="2000000"/><a:gridCol w="2000000"/></a:tblGrid>
8311            <a:tr h="2000000">
8312              <a:tc><a:txBody><a:bodyPr/><a:p/></a:txBody><a:tcPr/></a:tc>
8313              <a:tc><a:txBody><a:bodyPr/><a:p/></a:txBody><a:tcPr/></a:tc>
8314            </a:tr>
8315          </a:tbl>
8316        </a:graphicData>
8317      </a:graphic>
8318    </p:graphicFrame>
8319  </p:spTree></p:cSld>
8320</p:sld>"#;
8321        let sld = parse_sld(xml).expect("parse ok");
8322        let frame = sld
8323            .shapes
8324            .iter()
8325            .find_map(|s| {
8326                if let OxmlSlideShape::GraphicFrame(g) = s {
8327                    Some(g)
8328                } else {
8329                    None
8330                }
8331            })
8332            .expect("应有 graphicFrame");
8333        match &frame.graphic {
8334            crate::oxml::shape::Graphic::Table(tbl) => {
8335                // 验证 tableStyleId 解析
8336                let style = tbl.table_style.as_ref().expect("应有 table_style");
8337                assert_eq!(style.style_id(), "{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}");
8338                // 验证序列化后仍包含 tableStyleId
8339                let mut w = crate::oxml::writer::XmlWriter::new();
8340                tbl.write_xml(&mut w);
8341                let out = &w.buf;
8342                assert!(out.contains(
8343                    "<a:tableStyleId>{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}</a:tableStyleId>"
8344                ));
8345            }
8346            _ => panic!("期望 Table 变体"),
8347        }
8348    }
8349
8350    /// 验证表格无 `<a:tableStyleId>` 时 `table_style` 为 `None`。
8351    #[test]
8352    fn parse_table_without_style_id() {
8353        let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
8354<p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
8355       xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
8356       xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
8357  <p:cSld><p:spTree>
8358    <p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
8359    <p:grpSpPr><a:xfrm/></p:grpSpPr>
8360    <p:graphicFrame>
8361      <p:nvGraphicFramePr><p:cNvPr id="2" name="PlainTable"/><p:cNvGraphicFramePr/><p:nvPr/></p:nvGraphicFramePr>
8362      <p:xfrm><a:off x="0" y="0"/><a:ext cx="4000000" cy="2000000"/></p:xfrm>
8363      <a:graphic>
8364        <a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/table">
8365          <a:tbl>
8366            <a:tblPr/>
8367            <a:tblGrid><a:gridCol w="2000000"/><a:gridCol w="2000000"/></a:tblGrid>
8368            <a:tr h="2000000">
8369              <a:tc><a:txBody><a:bodyPr/><a:p/></a:txBody><a:tcPr/></a:tc>
8370              <a:tc><a:txBody><a:bodyPr/><a:p/></a:txBody><a:tcPr/></a:tc>
8371            </a:tr>
8372          </a:tbl>
8373        </a:graphicData>
8374      </a:graphic>
8375    </p:graphicFrame>
8376  </p:spTree></p:cSld>
8377</p:sld>"#;
8378        let sld = parse_sld(xml).expect("parse ok");
8379        let frame = sld
8380            .shapes
8381            .iter()
8382            .find_map(|s| {
8383                if let OxmlSlideShape::GraphicFrame(g) = s {
8384                    Some(g)
8385                } else {
8386                    None
8387                }
8388            })
8389            .expect("应有 graphicFrame");
8390        match &frame.graphic {
8391            crate::oxml::shape::Graphic::Table(tbl) => {
8392                assert!(
8393                    tbl.table_style.is_none(),
8394                    "无 tableStyleId 时 table_style 应为 None"
8395                );
8396            }
8397            _ => panic!("期望 Table 变体"),
8398        }
8399    }
8400
8401    /// 验证 `Cell` 的边框(lnL/lnR/lnT/lnB)、边距(marT/marL/marB/marR)、垂直对齐(anchor)
8402    /// 在 XML 往返中保持不变。
8403    ///
8404    /// 这是 TODO-013 的核心测试:确保单元格格式化信息在序列化 → 解析后不丢失。
8405    #[test]
8406    fn round_trip_cell_borders_margins_anchor() {
8407        let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
8408<p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
8409       xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
8410       xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
8411  <p:cSld><p:spTree>
8412    <p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
8413    <p:grpSpPr><a:xfrm/></p:grpSpPr>
8414    <p:graphicFrame>
8415      <p:nvGraphicFramePr><p:cNvPr id="2" name="FormattedTable"/><p:cNvGraphicFramePr/><p:nvPr/></p:nvGraphicFramePr>
8416      <p:xfrm><a:off x="0" y="0"/><a:ext cx="4000000" cy="2000000"/></p:xfrm>
8417      <a:graphic>
8418        <a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/table">
8419          <a:tbl>
8420            <a:tblPr><a:tableStyleId>{5940675A-B579-460E-94D1-54222C63F5DA}</a:tableStyleId></a:tblPr>
8421            <a:tblGrid><a:gridCol w="2000000"/><a:gridCol w="2000000"/></a:tblGrid>
8422            <a:tr h="2000000">
8423              <a:tc>
8424                <a:txBody><a:bodyPr/><a:p><a:r><a:t>A1</a:t></a:r></a:p></a:txBody>
8425                <a:tcPr marT="50000" marL="60000" marB="70000" marR="80000" anchor="ctr">
8426                  <a:lnL w="9525"><a:solidFill><a:srgbClr val="FF0000"/></a:solidFill></a:lnL>
8427                  <a:lnR w="9525"><a:solidFill><a:srgbClr val="00FF00"/></a:solidFill></a:lnR>
8428                  <a:lnT w="9525"><a:solidFill><a:srgbClr val="0000FF"/></a:solidFill></a:lnT>
8429                  <a:lnB w="9525"><a:noFill/></a:lnB>
8430                </a:tcPr>
8431              </a:tc>
8432              <a:tc>
8433                <a:txBody><a:bodyPr/><a:p><a:r><a:t>B1</a:t></a:r></a:p></a:txBody>
8434                <a:tcPr/>
8435              </a:tc>
8436            </a:tr>
8437          </a:tbl>
8438        </a:graphicData>
8439      </a:graphic>
8440    </p:graphicFrame>
8441  </p:spTree></p:cSld>
8442</p:sld>"#;
8443        let sld = parse_sld(xml).expect("parse ok");
8444        let frame = sld
8445            .shapes
8446            .iter()
8447            .find_map(|s| {
8448                if let OxmlSlideShape::GraphicFrame(g) = s {
8449                    Some(g)
8450                } else {
8451                    None
8452                }
8453            })
8454            .expect("应有 graphicFrame");
8455        let tbl = match &frame.graphic {
8456            crate::oxml::shape::Graphic::Table(t) => t,
8457            _ => panic!("期望 Table 变体"),
8458        };
8459        let cell = &tbl.rows[0].cells[0];
8460        // 边距
8461        assert_eq!(cell.margin.0.map(|m| m.value()), Some(50000), "marT");
8462        assert_eq!(cell.margin.1.map(|m| m.value()), Some(60000), "marL");
8463        assert_eq!(cell.margin.2.map(|m| m.value()), Some(70000), "marB");
8464        assert_eq!(cell.margin.3.map(|m| m.value()), Some(80000), "marR");
8465        // 垂直对齐
8466        assert_eq!(
8467            cell.anchor,
8468            crate::oxml::table::VerticalAnchor::Middle,
8469            "anchor=ctr"
8470        );
8471        // 边框
8472        let bl = cell.border_left.as_ref().expect("应有 lnL");
8473        assert_eq!(bl.width.value(), 9525);
8474        assert!(matches!(bl.color, Color::RGB(c) if c.0 == 0xFF && c.1 == 0x00 && c.2 == 0x00));
8475        let br = cell.border_right.as_ref().expect("应有 lnR");
8476        assert!(matches!(br.color, Color::RGB(c) if c.0 == 0x00 && c.1 == 0xFF && c.2 == 0x00));
8477        let bt = cell.border_top.as_ref().expect("应有 lnT");
8478        assert!(matches!(bt.color, Color::RGB(c) if c.0 == 0x00 && c.1 == 0x00 && c.2 == 0xFF));
8479        let bb = cell.border_bottom.as_ref().expect("应有 lnB");
8480        assert!(bb.no_fill, "lnB 应为 noFill");
8481    }
8482
8483    /// 验证 `<a:bodyPr>` 的 `numCol` / `spcCol` / `anchor` / `wrap` 属性解析。
8484    ///
8485    /// 这是 TODO-019 的 round-trip 测试。
8486    #[test]
8487    fn parse_txbody_multi_column() {
8488        let xml = r#"<p:txBody xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
8489                    xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
8490  <a:bodyPr numCol="3" spcCol="91440" anchor="ctr" wrap="square" lIns="91440" tIns="45720" rIns="91440" bIns="45720"/>
8491  <a:p><a:r><a:t>multi-col text</a:t></a:r></a:p>
8492</p:txBody>"#;
8493        let tb = parse_txbody(xml).expect("parse ok");
8494        let bp = tb.body_properties.as_ref().expect("应有 body_properties");
8495        assert_eq!(bp.num_cols, Some(3), "numCol");
8496        assert_eq!(bp.col_spacing.map(|v| v.value()), Some(91440), "spcCol");
8497        assert_eq!(bp.anchor, Some(MsoAnchor::Middle), "anchor=ctr");
8498        assert_eq!(bp.wrap, Some(TextWrapping::Square), "wrap=square");
8499        let insets = bp.insets.as_ref().expect("应有 insets");
8500        assert_eq!(insets.left.value(), 91440, "lIns");
8501        assert_eq!(insets.top.value(), 45720, "tIns");
8502        assert_eq!(insets.right.value(), 91440, "rIns");
8503        assert_eq!(insets.bottom.value(), 45720, "bIns");
8504        // 段落仍应正确解析
8505        assert_eq!(tb.paragraphs.len(), 1, "应有一个段落");
8506        assert_eq!(tb.paragraphs[0].runs.len(), 1, "应有一个 run");
8507        assert_eq!(tb.paragraphs[0].runs[0].text, "multi-col text");
8508    }
8509
8510    /// 验证自闭合 `<a:bodyPr/>` 的属性解析。
8511    ///
8512    /// 这是 TODO-019 的测试。
8513    #[test]
8514    fn parse_txbody_self_closing_body_pr() {
8515        let xml = r#"<p:txBody xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
8516                    xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
8517  <a:bodyPr numCol="2" spcCol="45720"/>
8518  <a:p/>
8519</p:txBody>"#;
8520        let tb = parse_txbody(xml).expect("parse ok");
8521        let bp = tb.body_properties.as_ref().expect("应有 body_properties");
8522        assert_eq!(bp.num_cols, Some(2), "numCol");
8523        assert_eq!(bp.col_spacing.map(|v| v.value()), Some(45720), "spcCol");
8524    }
8525
8526    /// 验证 `<a:bodyPr>` 含 `<a:spAutoFit/>` 子元素的解析。
8527    ///
8528    /// 这是 TODO-019 的测试(顺带覆盖 autoSize 子元素解析)。
8529    #[test]
8530    fn parse_txbody_with_sp_auto_fit() {
8531        let xml = r#"<p:txBody xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
8532                    xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
8533  <a:bodyPr numCol="1"><a:spAutoFit/></a:bodyPr>
8534  <a:p/>
8535</p:txBody>"#;
8536        let tb = parse_txbody(xml).expect("parse ok");
8537        let bp = tb.body_properties.as_ref().expect("应有 body_properties");
8538        assert_eq!(bp.num_cols, Some(1), "numCol=1");
8539        assert!(bp.sp_auto_fit, "应启用 spAutoFit");
8540        assert!(!bp.norm_autofit, "不应启用 normAutofit");
8541    }
8542
8543    /// 验证 `<a:buChar>` / `<a:buAutoNum>` / `<a:buNone>` 的详细属性解析。
8544    ///
8545    /// 这是 TODO-014 的 round-trip 测试。
8546    #[test]
8547    fn parse_paragraph_bullet_styles() {
8548        // buChar
8549        let xml = r#"<a:pPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
8550  <a:buChar char="•"/>
8551</a:pPr>"#;
8552        let ppr = parse_paragraph_properties(xml).expect("parse ok");
8553        assert!(ppr.bullet, "bullet 应为 true");
8554        match &ppr.bullet_style {
8555            Some(BulletStyle::Char { char }) => assert_eq!(char, "•"),
8556            other => panic!("期望 Char,实际: {other:?}"),
8557        }
8558
8559        // buAutoNum with startAt
8560        let xml = r#"<a:pPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
8561  <a:buAutoNum type="arabicPeriod" startAt="3"/>
8562</a:pPr>"#;
8563        let ppr = parse_paragraph_properties(xml).expect("parse ok");
8564        assert!(ppr.bullet);
8565        match &ppr.bullet_style {
8566            Some(BulletStyle::AutoNum {
8567                auto_num_type,
8568                start_at,
8569            }) => {
8570                assert_eq!(auto_num_type, "arabicPeriod");
8571                assert_eq!(*start_at, Some(3));
8572            }
8573            other => panic!("期望 AutoNum,实际: {other:?}"),
8574        }
8575
8576        // buAutoNum without startAt
8577        let xml = r#"<a:pPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
8578  <a:buAutoNum type="alphaLcParenR"/>
8579</a:pPr>"#;
8580        let ppr = parse_paragraph_properties(xml).expect("parse ok");
8581        match &ppr.bullet_style {
8582            Some(BulletStyle::AutoNum {
8583                auto_num_type,
8584                start_at,
8585            }) => {
8586                assert_eq!(auto_num_type, "alphaLcParenR");
8587                assert_eq!(*start_at, None);
8588            }
8589            other => panic!("期望 AutoNum,实际: {other:?}"),
8590        }
8591
8592        // buNone
8593        let xml = r#"<a:pPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
8594  <a:buNone/>
8595</a:pPr>"#;
8596        let ppr = parse_paragraph_properties(xml).expect("parse ok");
8597        assert!(!ppr.bullet, "bullet 应为 false");
8598        assert!(matches!(ppr.bullet_style, Some(BulletStyle::None)));
8599    }
8600
8601    /// 验证自闭合的 `<a:buChar/>` / `<a:buAutoNum/>` / `<a:buNone/>` 解析。
8602    ///
8603    /// 这是 TODO-014 的测试。
8604    #[test]
8605    fn parse_paragraph_bullet_styles_self_closing() {
8606        // 自闭合 buChar(无 char 属性)
8607        let xml = r#"<a:pPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
8608  <a:buChar char="▪"/>
8609</a:pPr>"#;
8610        let ppr = parse_paragraph_properties(xml).expect("parse ok");
8611        match &ppr.bullet_style {
8612            Some(BulletStyle::Char { char }) => assert_eq!(char, "▪"),
8613            other => panic!("期望 Char,实际: {other:?}"),
8614        }
8615
8616        // 自闭合 buAutoNum
8617        let xml = r#"<a:pPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
8618  <a:buAutoNum type="romanLcParenBoth" startAt="5"/>
8619</a:pPr>"#;
8620        let ppr = parse_paragraph_properties(xml).expect("parse ok");
8621        match &ppr.bullet_style {
8622            Some(BulletStyle::AutoNum {
8623                auto_num_type,
8624                start_at,
8625            }) => {
8626                assert_eq!(auto_num_type, "romanLcParenBoth");
8627                assert_eq!(*start_at, Some(5));
8628            }
8629            other => panic!("期望 AutoNum,实际: {other:?}"),
8630        }
8631    }
8632
8633    /// 验证 `<a:tabLst>` / `<a:tab>` 的解析。
8634    ///
8635    /// 这是 TODO-015 的 round-trip 测试。
8636    #[test]
8637    fn parse_paragraph_tab_stops() {
8638        let xml = r#"<a:pPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
8639  <a:tabLst>
8640    <a:tab pos="914400" algn="l"/>
8641    <a:tab pos="1828800" algn="r"/>
8642    <a:tab pos="2743200" algn="ctr"/>
8643    <a:tab pos="3657600" algn="dec"/>
8644  </a:tabLst>
8645</a:pPr>"#;
8646        let ppr = parse_paragraph_properties(xml).expect("parse ok");
8647        assert_eq!(ppr.tab_stops.len(), 4, "应有 4 个制表位");
8648        assert_eq!(ppr.tab_stops[0].pos.value(), 914400);
8649        assert_eq!(ppr.tab_stops[0].alignment, TabAlignment::Left);
8650        assert_eq!(ppr.tab_stops[1].pos.value(), 1828800);
8651        assert_eq!(ppr.tab_stops[1].alignment, TabAlignment::Right);
8652        assert_eq!(ppr.tab_stops[2].pos.value(), 2743200);
8653        assert_eq!(ppr.tab_stops[2].alignment, TabAlignment::Center);
8654        assert_eq!(ppr.tab_stops[3].pos.value(), 3657600);
8655        assert_eq!(ppr.tab_stops[3].alignment, TabAlignment::Decimal);
8656    }
8657
8658    /// 验证空 `<a:tabLst/>` 的解析。
8659    ///
8660    /// 这是 TODO-015 的测试。
8661    #[test]
8662    fn parse_paragraph_empty_tab_lst() {
8663        let xml = r#"<a:pPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
8664  <a:tabLst/>
8665</a:pPr>"#;
8666        let ppr = parse_paragraph_properties(xml).expect("parse ok");
8667        assert!(ppr.tab_stops.is_empty(), "空 tabLst 应无制表位");
8668    }
8669
8670    /// 验证 `<a:fld>` 字段元素的解析。
8671    ///
8672    /// 这是 TODO-016 的 round-trip 测试。
8673    #[test]
8674    fn parse_paragraph_field() {
8675        let xml = r#"<a:p xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
8676  <a:fld id="{12345678-ABCD-EF01-2345-678901234567}" type="slidenum">
8677    <a:rPr lang="en-US"/>
8678    <a:t>1</a:t>
8679  </a:fld>
8680</a:p>"#;
8681        let p = parse_paragraph(xml).expect("parse ok");
8682        assert_eq!(p.fields.len(), 1, "应有 1 个字段");
8683        let f = &p.fields[0];
8684        assert_eq!(f.id, "{12345678-ABCD-EF01-2345-678901234567}");
8685        assert_eq!(f.field_type, FieldType::SlideNumber);
8686        assert_eq!(f.text, "1");
8687    }
8688
8689    /// 验证含多个字段和 Run 的段落解析。
8690    ///
8691    /// 这是 TODO-016 的测试。
8692    #[test]
8693    fn parse_paragraph_mixed_runs_and_fields() {
8694        let xml = r#"<a:p xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
8695  <a:r><a:t>Page </a:t></a:r>
8696  <a:fld id="{AAA}" type="slidenum"><a:t>1</a:t></a:fld>
8697  <a:fld id="{BBB}" type="datetime"><a:t>1/1/2024</a:t></a:fld>
8698</a:p>"#;
8699        let p = parse_paragraph(xml).expect("parse ok");
8700        assert_eq!(p.runs.len(), 1, "应有 1 个 run");
8701        // 注意:trim_text(true) 会去掉尾部空格
8702        assert_eq!(p.runs[0].text, "Page");
8703        assert_eq!(p.fields.len(), 2, "应有 2 个字段");
8704        assert_eq!(p.fields[0].field_type, FieldType::SlideNumber);
8705        assert_eq!(p.fields[0].text, "1");
8706        assert_eq!(p.fields[1].field_type, FieldType::DateTime);
8707        assert_eq!(p.fields[1].text, "1/1/2024");
8708    }
8709
8710    /// 验证自定义字段类型的解析。
8711    ///
8712    /// 这是 TODO-016 的测试。
8713    #[test]
8714    fn parse_paragraph_custom_field_type() {
8715        let xml = r#"<a:p xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
8716  <a:fld id="{CCC}" type="customField"><a:t>custom</a:t></a:fld>
8717</a:p>"#;
8718        let p = parse_paragraph(xml).expect("parse ok");
8719        assert_eq!(p.fields.len(), 1);
8720        assert_eq!(
8721            p.fields[0].field_type,
8722            FieldType::Custom("customField".to_string())
8723        );
8724        assert_eq!(p.fields[0].text, "custom");
8725    }
8726
8727    /// 验证 `<a:hlinkClick>` 自闭合超链接的解析(r:id + tooltip)。
8728    ///
8729    /// 这是 TODO-026 的测试。
8730    #[test]
8731    fn parse_run_properties_hlink_click_self_closing() {
8732        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">
8733  <a:hlinkClick r:id="rId3" tooltip="点击访问"/>
8734</a:rPr>"#;
8735        let rp = parse_run_properties(xml).expect("parse ok");
8736        let hl = rp.hlink_click.expect("hlink_click 应存在");
8737        assert_eq!(hl.rid.as_deref(), Some("rId3"));
8738        assert_eq!(hl.tooltip.as_deref(), Some("点击访问"));
8739        assert!(hl.action.is_none());
8740        assert!(!hl.invalid);
8741    }
8742
8743    /// 验证 `<a:hlinkClick>` 带动作(跳转幻灯片)的解析。
8744    ///
8745    /// 这是 TODO-026 的测试。
8746    #[test]
8747    fn parse_run_properties_hlink_click_action() {
8748        let xml = r#"<a:rPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
8749  <a:hlinkClick action="ppaction://hlinksldjump"/>
8750</a:rPr>"#;
8751        let rp = parse_run_properties(xml).expect("parse ok");
8752        let hl = rp.hlink_click.expect("hlink_click 应存在");
8753        assert_eq!(hl.action.as_deref(), Some("ppaction://hlinksldjump"));
8754        assert!(hl.rid.is_none());
8755    }
8756
8757    /// 验证 `<a:hlinkHover>` 的解析。
8758    ///
8759    /// 这是 TODO-026 的测试。
8760    #[test]
8761    fn parse_run_properties_hlink_hover() {
8762        let xml = r#"<a:rPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
8763  <a:hlinkHover r:id="rId5" tooltip="悬停提示"/>
8764</a:rPr>"#;
8765        let rp = parse_run_properties(xml).expect("parse ok");
8766        let hl = rp.hlink_hover.expect("hlink_hover 应存在");
8767        assert_eq!(hl.rid.as_deref(), Some("rId5"));
8768        assert_eq!(hl.tooltip.as_deref(), Some("悬停提示"));
8769    }
8770
8771    /// 验证同时存在 hlinkClick 和 hlinkHover 的解析。
8772    ///
8773    /// 这是 TODO-026 的测试。
8774    #[test]
8775    fn parse_run_properties_both_hlinks() {
8776        let xml = r#"<a:rPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
8777  <a:hlinkClick r:id="rId1" tooltip="click"/>
8778  <a:hlinkHover r:id="rId2" tooltip="hover"/>
8779</a:rPr>"#;
8780        let rp = parse_run_properties(xml).expect("parse ok");
8781        assert_eq!(
8782            rp.hlink_click.as_ref().unwrap().rid.as_deref(),
8783            Some("rId1")
8784        );
8785        assert_eq!(
8786            rp.hlink_hover.as_ref().unwrap().rid.as_deref(),
8787            Some("rId2")
8788        );
8789    }
8790
8791    /// 验证空属性 `<a:hlinkClick/>` 标记为 invalid(继承场景)。
8792    ///
8793    /// 这是 TODO-026 的测试。
8794    #[test]
8795    fn parse_run_properties_hlink_click_empty() {
8796        let xml = r#"<a:rPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
8797  <a:hlinkClick/>
8798</a:rPr>"#;
8799        let rp = parse_run_properties(xml).expect("parse ok");
8800        let hl = rp.hlink_click.expect("hlink_click 应存在");
8801        assert!(hl.invalid);
8802        assert!(hl.rid.is_none());
8803    }
8804
8805    /// 验证 `<a:spLocks>` 自闭合形状锁定的解析。
8806    ///
8807    /// 这是 TODO-027 的测试。
8808    #[test]
8809    fn parse_sp_with_locks() {
8810        let xml = r#"<p:sp xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
8811  <p:nvSpPr>
8812    <p:cNvPr id="2" name="Locked"/>
8813    <p:cNvSpPr>
8814      <a:spLocks noGrp="1" noSelect="1" noResize="1"/>
8815    </p:cNvSpPr>
8816    <p:nvPr/>
8817  </p:nvSpPr>
8818  <p:spPr/>
8819  <p:txBody><a:bodyPr/><a:p/></p:txBody>
8820</p:sp>"#;
8821        let sp = parse_sp(xml).expect("parse ok");
8822        let locks = sp.locks.as_ref().expect("locks 应存在");
8823        assert!(locks.no_grp);
8824        assert!(locks.no_select);
8825        assert!(locks.no_resize);
8826        assert!(!locks.no_move);
8827        assert!(!locks.no_rot);
8828    }
8829
8830    /// 验证 `<a:spLocks>` 与 `txBox` 同时存在的解析。
8831    ///
8832    /// 这是 TODO-027 的测试。
8833    #[test]
8834    fn parse_sp_locks_with_txbox() {
8835        let xml = r#"<p:sp xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
8836  <p:nvSpPr>
8837    <p:cNvPr id="3" name="TB"/>
8838    <p:cNvSpPr txBox="1">
8839      <a:spLocks noChangeAspect="1"/>
8840    </p:cNvSpPr>
8841    <p:nvPr/>
8842  </p:nvSpPr>
8843  <p:spPr/>
8844  <p:txBody><a:bodyPr/><a:p/></p:txBody>
8845</p:sp>"#;
8846        let sp = parse_sp(xml).expect("parse ok");
8847        assert!(sp.c_nv_sp_pr_tx_box, "txBox 应为 true");
8848        let locks = sp.locks.as_ref().expect("locks 应存在");
8849        assert!(locks.no_change_aspect);
8850        assert!(!locks.no_grp);
8851    }
8852
8853    /// 验证 `<p:style>` 主题样式引用的解析。
8854    ///
8855    /// 这是 TODO-006 的测试。
8856    #[test]
8857    fn parse_sp_with_style() {
8858        let xml = r#"<p:sp xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
8859  <p:nvSpPr>
8860    <p:cNvPr id="4" name="Styled"/>
8861    <p:cNvSpPr/>
8862    <p:nvPr/>
8863  </p:nvSpPr>
8864  <p:spPr/>
8865  <p:style>
8866    <a:lnRef idx="1"><a:schemeClr val="accent1"/></a:lnRef>
8867    <a:fillRef idx="2"><a:schemeClr val="accent2"/></a:fillRef>
8868    <a:effectRef idx="0"><a:schemeClr val="accent3"/></a:effectRef>
8869    <a:fontRef idx="minor"><a:schemeClr val="tx1"/></a:fontRef>
8870  </p:style>
8871  <p:txBody><a:bodyPr/><a:p/></p:txBody>
8872</p:sp>"#;
8873        let sp = parse_sp(xml).expect("parse ok");
8874        let style = sp.style.as_ref().expect("style 应存在");
8875        let ln = style.line_ref.as_ref().expect("line_ref 应存在");
8876        assert_eq!(ln.idx.as_deref(), Some("1"));
8877        assert_eq!(ln.scheme_color.as_deref(), Some("accent1"));
8878        let fill = style.fill_ref.as_ref().expect("fill_ref 应存在");
8879        assert_eq!(fill.idx.as_deref(), Some("2"));
8880        assert_eq!(fill.scheme_color.as_deref(), Some("accent2"));
8881        let eff = style.effect_ref.as_ref().expect("effect_ref 应存在");
8882        assert_eq!(eff.idx.as_deref(), Some("0"));
8883        let font = style.font_ref.as_ref().expect("font_ref 应存在");
8884        assert_eq!(font.idx.as_deref(), Some("minor"));
8885        assert_eq!(font.scheme_color.as_deref(), Some("tx1"));
8886    }
8887
8888    /// 验证 `<p:style>` 自闭合子元素的解析。
8889    ///
8890    /// 这是 TODO-006 的测试。
8891    #[test]
8892    fn parse_sp_style_self_closing_refs() {
8893        let xml = r#"<p:style xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
8894  <a:lnRef idx="3"/>
8895  <a:fillRef idx="1"/>
8896</p:style>"#;
8897        let style = parse_shape_style(xml).expect("parse ok");
8898        assert_eq!(style.line_ref.as_ref().unwrap().idx.as_deref(), Some("3"));
8899        assert!(style.line_ref.as_ref().unwrap().scheme_color.is_none());
8900        assert_eq!(style.fill_ref.as_ref().unwrap().idx.as_deref(), Some("1"));
8901        assert!(style.effect_ref.is_none());
8902        assert!(style.font_ref.is_none());
8903    }
8904
8905    // ===== TODO-020:幻灯片过渡测试 =====
8906
8907    /// 验证 `<p:transition>` 的 fade 类型解析。
8908    #[test]
8909    fn parse_transition_fade() {
8910        let xml = r#"<p:transition xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" spd="slow" advClick="0" advTm="5000">
8911  <p:fade thruBlk="1"/>
8912</p:transition>"#;
8913        let tr = parse_transition(xml).expect("parse ok");
8914        assert_eq!(tr.speed, TransitionSpeed::Slow);
8915        assert!(!tr.advance_click);
8916        assert_eq!(tr.advance_after_ms, Some(5000));
8917        match &tr.transition_type {
8918            TransitionType::Fade { thru_blk } => assert!(*thru_blk),
8919            other => panic!("expected Fade, got {:?}", other),
8920        }
8921    }
8922
8923    /// 验证 `<p:transition>` 的 push 类型解析(带方向)。
8924    #[test]
8925    fn parse_transition_push() {
8926        let xml = r#"<p:transition xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" spd="fast">
8927  <p:push dir="l"/>
8928</p:transition>"#;
8929        let tr = parse_transition(xml).expect("parse ok");
8930        assert_eq!(tr.speed, TransitionSpeed::Fast);
8931        // 默认 advClick=true
8932        assert!(tr.advance_click);
8933        assert!(tr.advance_after_ms.is_none());
8934        match &tr.transition_type {
8935            TransitionType::Push { dir } => assert_eq!(*dir, TransitionDirection::Left),
8936            other => panic!("expected Push, got {:?}", other),
8937        }
8938    }
8939
8940    /// 验证 `<p:transition>` 的 morph 类型解析。
8941    #[test]
8942    fn parse_transition_morph() {
8943        let xml = r#"<p:transition xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
8944  <p:morph option="byChar"/>
8945</p:transition>"#;
8946        let tr = parse_transition(xml).expect("parse ok");
8947        match &tr.transition_type {
8948            TransitionType::Morph { option } => assert_eq!(*option, MorphOption::ByChar),
8949            other => panic!("expected Morph, got {:?}", other),
8950        }
8951    }
8952
8953    /// 验证 `<p:transition>` 的 split 类型解析(带方向和方向)。
8954    #[test]
8955    fn parse_transition_split() {
8956        let xml = r#"<p:transition xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
8957  <p:split orient="vert" dir="d"/>
8958</p:transition>"#;
8959        let tr = parse_transition(xml).expect("parse ok");
8960        match &tr.transition_type {
8961            TransitionType::Split { orient, dir } => {
8962                assert_eq!(*orient, SplitOrientation::Vertical);
8963                assert_eq!(*dir, TransitionDirection::Down);
8964            }
8965            other => panic!("expected Split, got {:?}", other),
8966        }
8967    }
8968
8969    /// 验证 `<p:transition>` 自闭合(无子元素)解析。
8970    #[test]
8971    fn parse_transition_self_closing() {
8972        let xml = r#"<p:transition xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" spd="med" advClick="1"/>"#;
8973        let tr = parse_transition(xml).expect("parse ok");
8974        assert_eq!(tr.speed, TransitionSpeed::Medium);
8975        assert!(tr.advance_click);
8976        // 无子元素时,transition_type 为 None
8977        assert_eq!(tr.transition_type, TransitionType::None);
8978    }
8979
8980    /// 验证 `parse_sld` 能正确解析包含 `<p:transition>` 的 slide。
8981    #[test]
8982    fn parse_sld_with_transition() {
8983        let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
8984<p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
8985       xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
8986       xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
8987  <p:cSld>
8988    <p:spTree>
8989      <p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
8990      <p:grpSpPr><a:xfrm/></p:grpSpPr>
8991    </p:spTree>
8992  </p:cSld>
8993  <p:clrMapOvr/>
8994  <p:transition spd="slow" advTm="3000">
8995    <p:fade/>
8996  </p:transition>
8997</p:sld>"#;
8998        let sld = parse_sld(xml).expect("parse ok");
8999        let tr = sld.transition.expect("transition should exist");
9000        assert_eq!(tr.speed, TransitionSpeed::Slow);
9001        assert_eq!(tr.advance_after_ms, Some(3000));
9002        match &tr.transition_type {
9003            TransitionType::Fade { thru_blk } => assert!(!*thru_blk),
9004            other => panic!("expected Fade, got {:?}", other),
9005        }
9006    }
9007
9008    /// 验证 `Transition::write_xml` 序列化 + `parse_transition` 反序列化的 round-trip。
9009    #[test]
9010    fn transition_round_trip() {
9011        use crate::oxml::slide::Transition;
9012        use crate::oxml::writer::XmlWriter;
9013
9014        let original = Transition {
9015            speed: TransitionSpeed::Fast,
9016            advance_click: false,
9017            advance_after_ms: Some(8000),
9018            transition_type: TransitionType::Push {
9019                dir: TransitionDirection::Up,
9020            },
9021        };
9022        // 序列化
9023        let mut w = XmlWriter::new();
9024        original.write_xml(&mut w);
9025        let xml = w.buf.clone();
9026        // 反序列化
9027        let parsed = parse_transition(&xml).expect("parse ok");
9028        assert_eq!(parsed.speed, original.speed);
9029        assert_eq!(parsed.advance_click, original.advance_click);
9030        assert_eq!(parsed.advance_after_ms, original.advance_after_ms);
9031        assert_eq!(parsed.transition_type, original.transition_type);
9032    }
9033
9034    // ===== TODO-024:自定义几何(custGeom)测试 =====
9035
9036    /// 验证 `parse_custom_geometry` 能解析基本的 moveTo/lnTo/close 路径。
9037    #[test]
9038    fn parse_custom_geometry_basic() {
9039        let xml = r#"<a:custGeom>
9040<a:avLst/>
9041<a:rect l="0" t="0" r="100" b="100"/>
9042<a:pathLst>
9043<a:path w="100" h="100" fill="norm" stroke="norm">
9044<a:moveTo><a:pt x="0" y="0"/></a:moveTo>
9045<a:lnTo><a:pt x="100" y="0"/></a:lnTo>
9046<a:lnTo><a:pt x="50" y="100"/></a:lnTo>
9047<a:close/>
9048</a:path>
9049</a:pathLst>
9050</a:custGeom>"#;
9051        let geom = parse_custom_geometry(xml).expect("parse ok");
9052        assert_eq!(
9053            geom.rect,
9054            Some(GeomRect {
9055                l: "0".to_string(),
9056                t: "0".to_string(),
9057                r: "100".to_string(),
9058                b: "100".to_string(),
9059            })
9060        );
9061        assert_eq!(geom.path_list.len(), 1);
9062        let p = &geom.path_list[0];
9063        assert_eq!(p.width, 100);
9064        assert_eq!(p.height, 100);
9065        assert_eq!(p.fill.as_deref(), Some("norm"));
9066        assert_eq!(p.stroke.as_deref(), Some("norm"));
9067        assert_eq!(p.segments.len(), 4);
9068        // 验证各段
9069        match &p.segments[0] {
9070            PathSegment::MoveTo { x, y } => {
9071                assert_eq!(*x, 0);
9072                assert_eq!(*y, 0);
9073            }
9074            other => panic!("expected MoveTo, got {:?}", other),
9075        }
9076        match &p.segments[1] {
9077            PathSegment::LineTo { x, y } => {
9078                assert_eq!(*x, 100);
9079                assert_eq!(*y, 0);
9080            }
9081            other => panic!("expected LineTo, got {:?}", other),
9082        }
9083        match &p.segments[2] {
9084            PathSegment::LineTo { x, y } => {
9085                assert_eq!(*x, 50);
9086                assert_eq!(*y, 100);
9087            }
9088            other => panic!("expected LineTo, got {:?}", other),
9089        }
9090        match &p.segments[3] {
9091            PathSegment::Close => {}
9092            other => panic!("expected Close, got {:?}", other),
9093        }
9094    }
9095
9096    /// 验证 `parse_custom_geometry` 能解析贝塞尔曲线段。
9097    #[test]
9098    fn parse_custom_geometry_with_bez() {
9099        let xml = r#"<a:custGeom>
9100<a:avLst/>
9101<a:pathLst>
9102<a:path w="200" h="200">
9103<a:moveTo><a:pt x="10" y="10"/></a:moveTo>
9104<a:cubicBezTo>
9105<a:pt x="50" y="0"/>
9106<a:pt x="150" y="0"/>
9107<a:pt x="190" y="10"/>
9108</a:cubicBezTo>
9109<a:quadBezTo>
9110<a:pt x="190" y="100"/>
9111<a:pt x="100" y="190"/>
9112</a:quadBezTo>
9113</a:path>
9114</a:pathLst>
9115</a:custGeom>"#;
9116        let geom = parse_custom_geometry(xml).expect("parse ok");
9117        assert_eq!(geom.path_list.len(), 1);
9118        let p = &geom.path_list[0];
9119        assert_eq!(p.segments.len(), 3);
9120        match &p.segments[1] {
9121            PathSegment::CubicBezTo {
9122                x1,
9123                y1,
9124                x2,
9125                y2,
9126                x3,
9127                y3,
9128            } => {
9129                assert_eq!(*x1, 50);
9130                assert_eq!(*y1, 0);
9131                assert_eq!(*x2, 150);
9132                assert_eq!(*y2, 0);
9133                assert_eq!(*x3, 190);
9134                assert_eq!(*y3, 10);
9135            }
9136            other => panic!("expected CubicBezTo, got {:?}", other),
9137        }
9138        match &p.segments[2] {
9139            PathSegment::QuadBezTo { x1, y1, x2, y2 } => {
9140                assert_eq!(*x1, 190);
9141                assert_eq!(*y1, 100);
9142                assert_eq!(*x2, 100);
9143                assert_eq!(*y2, 190);
9144            }
9145            other => panic!("expected QuadBezTo, got {:?}", other),
9146        }
9147    }
9148
9149    /// 验证 `parse_custom_geometry` 能解析 arcTo 段。
9150    #[test]
9151    fn parse_custom_geometry_with_arc() {
9152        let xml = r#"<a:custGeom>
9153<a:avLst/>
9154<a:pathLst>
9155<a:path w="100" h="100">
9156<a:moveTo><a:pt x="0" y="50"/></a:moveTo>
9157<a:arcTo wR="50" hR="50" stAng="0" swAng="5400000"/>
9158<a:close/>
9159</a:path>
9160</a:pathLst>
9161</a:custGeom>"#;
9162        let geom = parse_custom_geometry(xml).expect("parse ok");
9163        let p = &geom.path_list[0];
9164        assert_eq!(p.segments.len(), 3);
9165        match &p.segments[1] {
9166            PathSegment::ArcTo {
9167                w_r,
9168                h_r,
9169                st_ang,
9170                sw_ang,
9171            } => {
9172                assert_eq!(*w_r, 50);
9173                assert_eq!(*h_r, 50);
9174                assert_eq!(*st_ang, 0);
9175                assert_eq!(*sw_ang, 5400000);
9176            }
9177            other => panic!("expected ArcTo, got {:?}", other),
9178        }
9179    }
9180
9181    /// 验证 `parse_custom_geometry` 能解析 fill/stroke 文本元素。
9182    #[test]
9183    fn parse_custom_geometry_fill_stroke() {
9184        let xml = r#"<a:custGeom>
9185<a:avLst/>
9186<a:fill>norm</a:fill>
9187<a:stroke>none</a:stroke>
9188<a:pathLst>
9189<a:path w="10" h="10">
9190<a:moveTo><a:pt x="0" y="0"/></a:moveTo>
9191</a:path>
9192</a:pathLst>
9193</a:custGeom>"#;
9194        let geom = parse_custom_geometry(xml).expect("parse ok");
9195        assert_eq!(geom.fill.as_deref(), Some("norm"));
9196        assert_eq!(geom.stroke.as_deref(), Some("none"));
9197    }
9198
9199    /// 验证 `CustomGeometry::write_xml` + `parse_custom_geometry` 的 round-trip。
9200    #[test]
9201    fn custom_geometry_round_trip() {
9202        use crate::oxml::writer::XmlWriter;
9203
9204        let original = CustomGeometry {
9205            fill: Some("norm".to_string()),
9206            stroke: None,
9207            rect: Some(GeomRect {
9208                l: "0".to_string(),
9209                t: "0".to_string(),
9210                r: "100".to_string(),
9211                b: "100".to_string(),
9212            }),
9213            path_list: vec![Path {
9214                width: 100,
9215                height: 100,
9216                fill: Some("norm".to_string()),
9217                stroke: None,
9218                segments: vec![
9219                    PathSegment::MoveTo { x: 0, y: 0 },
9220                    PathSegment::LineTo { x: 100, y: 0 },
9221                    PathSegment::LineTo { x: 100, y: 100 },
9222                    PathSegment::LineTo { x: 0, y: 100 },
9223                    PathSegment::Close,
9224                ],
9225            }],
9226        };
9227        // 序列化
9228        let mut w = XmlWriter::new();
9229        original.write_xml(&mut w);
9230        let xml = w.buf.clone();
9231        // 反序列化
9232        let parsed = parse_custom_geometry(&xml).expect("parse ok");
9233        assert_eq!(parsed.fill, original.fill);
9234        assert_eq!(parsed.stroke, original.stroke);
9235        assert_eq!(parsed.rect, original.rect);
9236        assert_eq!(parsed.path_list.len(), 1);
9237        let p0 = &parsed.path_list[0];
9238        assert_eq!(p0.width, 100);
9239        assert_eq!(p0.height, 100);
9240        assert_eq!(p0.fill.as_deref(), Some("norm"));
9241        assert_eq!(p0.segments.len(), 5);
9242        // 验证第一段
9243        match &p0.segments[0] {
9244            PathSegment::MoveTo { x, y } => {
9245                assert_eq!(*x, 0);
9246                assert_eq!(*y, 0);
9247            }
9248            other => panic!("expected MoveTo, got {:?}", other),
9249        }
9250        // 验证最后一段
9251        match &p0.segments[4] {
9252            PathSegment::Close => {}
9253            other => panic!("expected Close, got {:?}", other),
9254        }
9255    }
9256
9257    /// 验证 `parse_sld` 能从完整 slide XML 中解析带 custGeom 的形状。
9258    #[test]
9259    fn parse_sp_with_custgeom() {
9260        let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
9261<p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
9262       xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
9263       xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
9264  <p:cSld>
9265    <p:spTree>
9266      <p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
9267      <p:grpSpPr><a:xfrm/></p:grpSpPr>
9268      <p:sp>
9269        <p:nvSpPr>
9270          <p:cNvPr id="10" name="Freeform 1"/>
9271          <p:cNvSpPr/>
9272          <p:nvPr/>
9273        </p:nvSpPr>
9274        <p:spPr>
9275          <a:xfrm>
9276            <a:off x="100" y="100"/>
9277            <a:ext cx="200" cy="200"/>
9278          </a:xfrm>
9279          <a:custGeom>
9280            <a:avLst/>
9281            <a:pathLst>
9282              <a:path w="200" h="200">
9283                <a:moveTo><a:pt x="0" y="0"/></a:moveTo>
9284                <a:lnTo><a:pt x="200" y="0"/></a:lnTo>
9285                <a:lnTo><a:pt x="100" y="200"/></a:lnTo>
9286                <a:close/>
9287              </a:path>
9288            </a:pathLst>
9289          </a:custGeom>
9290        </p:spPr>
9291      </p:sp>
9292    </p:spTree>
9293  </p:cSld>
9294  <p:clrMapOvr/>
9295</p:sld>"#;
9296        let sld = parse_sld(xml).expect("parse ok");
9297        assert_eq!(sld.shapes.len(), 1);
9298        match &sld.shapes[0] {
9299            OxmlSlideShape::Sp(sp) => {
9300                // 验证几何是 Custom
9301                let geom = sp
9302                    .properties
9303                    .geometry
9304                    .as_ref()
9305                    .expect("geometry should exist");
9306                match geom {
9307                    Geometry::Custom(cg) => {
9308                        assert_eq!(cg.path_list.len(), 1);
9309                        let p = &cg.path_list[0];
9310                        assert_eq!(p.width, 200);
9311                        assert_eq!(p.height, 200);
9312                        assert_eq!(p.segments.len(), 4);
9313                        match &p.segments[0] {
9314                            PathSegment::MoveTo { x, y } => {
9315                                assert_eq!(*x, 0);
9316                                assert_eq!(*y, 0);
9317                            }
9318                            other => panic!("expected MoveTo, got {:?}", other),
9319                        }
9320                    }
9321                    other => panic!("expected Custom geometry, got {:?}", other),
9322                }
9323            }
9324            other => panic!("expected Sp, got {:?}", other),
9325        }
9326    }
9327
9328    /// 验证 `Geometry::write_xml` 对 Preset 变体的序列化(确保 prstGeom 路径未被破坏)。
9329    #[test]
9330    fn geometry_preset_write_xml() {
9331        use crate::oxml::simpletypes::PresetGeometry;
9332        use crate::oxml::writer::XmlWriter;
9333
9334        let geom = Geometry::preset(PresetGeometry::Rectangle);
9335        let mut w = XmlWriter::new();
9336        geom.write_xml(&mut w);
9337        let xml = w.buf.clone();
9338        assert!(xml.contains("<a:prstGeom prst=\"rect\">"), "xml: {}", xml);
9339        assert!(xml.contains("<a:avLst/>"), "xml: {}", xml);
9340        assert!(xml.contains("</a:prstGeom>"), "xml: {}", xml);
9341    }
9342
9343    /// 验证 `Geometry::write_xml` 对 Custom 变体的序列化。
9344    #[test]
9345    fn geometry_custom_write_xml() {
9346        use crate::oxml::writer::XmlWriter;
9347
9348        let geom = Geometry::Custom(CustomGeometry {
9349            fill: None,
9350            stroke: None,
9351            rect: None,
9352            path_list: vec![Path {
9353                width: 50,
9354                height: 50,
9355                fill: None,
9356                stroke: None,
9357                segments: vec![
9358                    PathSegment::MoveTo { x: 0, y: 0 },
9359                    PathSegment::LineTo { x: 50, y: 50 },
9360                    PathSegment::Close,
9361                ],
9362            }],
9363        });
9364        let mut w = XmlWriter::new();
9365        geom.write_xml(&mut w);
9366        let xml = w.buf.clone();
9367        assert!(xml.contains("<a:custGeom>"), "xml: {}", xml);
9368        assert!(xml.contains("<a:avLst/>"), "xml: {}", xml);
9369        assert!(xml.contains("<a:pathLst>"), "xml: {}", xml);
9370        assert!(xml.contains("<a:path w=\"50\" h=\"50\">"), "xml: {}", xml);
9371        assert!(xml.contains("<a:moveTo>"), "xml: {}", xml);
9372        assert!(xml.contains("<a:lnTo>"), "xml: {}", xml);
9373        assert!(xml.contains("<a:close/>"), "xml: {}", xml);
9374    }
9375
9376    /// 验证带调整值的 `<a:prstGeom>` 在 XML 往返中保持不变(TODO-038)。
9377    ///
9378    /// 测试场景:圆角矩形(roundRect)带单个调整值 `adj=16667`(16.667%)。
9379    /// 期望:解析后 `Geometry::Preset` 携带一个 `AdjustmentValue`,
9380    /// 序列化后 XML 仍包含 `<a:gd name="adj" fmla="val 16667"/>`。
9381    #[test]
9382    fn round_trip_prst_geom_with_adjustments() {
9383        let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
9384<p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
9385       xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
9386       xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
9387  <p:cSld><p:spTree>
9388    <p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
9389    <p:grpSpPr><a:xfrm/></p:grpSpPr>
9390    <p:sp>
9391      <p:nvSpPr><p:cNvPr id="2" name="RoundRect"/><p:cNvSpPr/><p:nvPr/></p:nvSpPr>
9392      <p:spPr>
9393        <a:xfrm><a:off x="100000" y="100000"/><a:ext cx="500000" cy="300000"/></a:xfrm>
9394        <a:prstGeom prst="roundRect">
9395          <a:avLst>
9396            <a:gd name="adj" fmla="val 16667"/>
9397          </a:avLst>
9398        </a:prstGeom>
9399      </p:spPr>
9400      <p:txBody><a:bodyPr/><a:p/></p:txBody>
9401    </p:sp>
9402  </p:spTree></p:cSld>
9403</p:sld>"#;
9404        let sld = parse_sld(xml).expect("parse ok");
9405        let sp = sld
9406            .shapes
9407            .iter()
9408            .find_map(|s| {
9409                if let OxmlSlideShape::Sp(sp) = s {
9410                    Some(sp)
9411                } else {
9412                    None
9413                }
9414            })
9415            .expect("应有 Sp");
9416        // 验证几何为 Preset(roundRect) 且携带一个调整值
9417        match &sp.properties.geometry {
9418            Some(Geometry::Preset(prst, adjustments)) => {
9419                assert_eq!(*prst, PresetGeometry::RoundRectangle);
9420                assert_eq!(adjustments.len(), 1, "应有 1 个调整值");
9421                let adj = &adjustments[0];
9422                assert_eq!(adj.name, "adj");
9423                assert_eq!(adj.raw_value, 16667);
9424                assert!((adj.effective_value() - 0.16667).abs() < 1e-6);
9425            }
9426            other => panic!("期望 Preset 几何,得到 {:?}", other),
9427        }
9428        // 验证序列化后 XML 仍包含调整值
9429        let mut w = crate::oxml::writer::XmlWriter::new();
9430        sp.properties.geometry.as_ref().unwrap().write_xml(&mut w);
9431        let out = &w.buf;
9432        assert!(
9433            out.contains("<a:prstGeom prst=\"roundRect\">"),
9434            "xml: {}",
9435            out
9436        );
9437        assert!(out.contains("<a:avLst>"), "xml: {}", out);
9438        assert!(
9439            out.contains("<a:gd name=\"adj\" fmla=\"val 16667\"/>"),
9440            "xml: {}",
9441            out
9442        );
9443        assert!(out.contains("</a:prstGeom>"), "xml: {}", out);
9444    }
9445
9446    /// 验证带多个调整值的 `<a:prstGeom>` 在 XML 往返中保持不变(TODO-038)。
9447    ///
9448    /// 测试场景:`rect` 形状带两个调整值 `adj1=50000` 和 `adj2=25000`。
9449    /// 注意:rect 通常无调整值,但本测试验证解析器能正确处理多个 `<a:gd>`。
9450    #[test]
9451    fn round_trip_prst_geom_with_multiple_adjustments() {
9452        let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
9453<p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
9454       xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
9455       xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
9456  <p:cSld><p:spTree>
9457    <p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
9458    <p:grpSpPr><a:xfrm/></p:grpSpPr>
9459    <p:sp>
9460      <p:nvSpPr><p:cNvPr id="3" name="MultiAdj"/><p:cNvSpPr/><p:nvPr/></p:nvSpPr>
9461      <p:spPr>
9462        <a:xfrm><a:off x="0" y="0"/><a:ext cx="1000000" cy="500000"/></a:xfrm>
9463        <a:prstGeom prst="rect">
9464          <a:avLst>
9465            <a:gd name="adj1" fmla="val 50000"/>
9466            <a:gd name="adj2" fmla="val 25000"/>
9467          </a:avLst>
9468        </a:prstGeom>
9469      </p:spPr>
9470      <p:txBody><a:bodyPr/><a:p/></p:txBody>
9471    </p:sp>
9472  </p:spTree></p:cSld>
9473</p:sld>"#;
9474        let sld = parse_sld(xml).expect("parse ok");
9475        let sp = sld
9476            .shapes
9477            .iter()
9478            .find_map(|s| {
9479                if let OxmlSlideShape::Sp(sp) = s {
9480                    Some(sp)
9481                } else {
9482                    None
9483                }
9484            })
9485            .expect("应有 Sp");
9486        match &sp.properties.geometry {
9487            Some(Geometry::Preset(_, adjustments)) => {
9488                assert_eq!(adjustments.len(), 2, "应有 2 个调整值");
9489                assert_eq!(adjustments[0].name, "adj1");
9490                assert_eq!(adjustments[0].raw_value, 50000);
9491                assert_eq!(adjustments[1].name, "adj2");
9492                assert_eq!(adjustments[1].raw_value, 25000);
9493            }
9494            other => panic!("期望 Preset 几何,得到 {:?}", other),
9495        }
9496        // 验证序列化
9497        let mut w = crate::oxml::writer::XmlWriter::new();
9498        sp.properties.geometry.as_ref().unwrap().write_xml(&mut w);
9499        let out = &w.buf;
9500        assert!(
9501            out.contains("<a:gd name=\"adj1\" fmla=\"val 50000\"/>"),
9502            "xml: {}",
9503            out
9504        );
9505        assert!(
9506            out.contains("<a:gd name=\"adj2\" fmla=\"val 25000\"/>"),
9507            "xml: {}",
9508            out
9509        );
9510    }
9511
9512    /// 验证空 `<a:avLst/>` 解析为空调整值列表(TODO-038)。
9513    #[test]
9514    fn parse_prst_geom_with_empty_av_lst() {
9515        let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
9516<p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
9517       xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
9518       xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
9519  <p:cSld><p:spTree>
9520    <p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
9521    <p:grpSpPr><a:xfrm/></p:grpSpPr>
9522    <p:sp>
9523      <p:nvSpPr><p:cNvPr id="4" name="EmptyAdj"/><p:cNvSpPr/><p:nvPr/></p:nvSpPr>
9524      <p:spPr>
9525        <a:xfrm><a:off x="0" y="0"/><a:ext cx="1000000" cy="500000"/></a:xfrm>
9526        <a:prstGeom prst="rect">
9527          <a:avLst/>
9528        </a:prstGeom>
9529      </p:spPr>
9530      <p:txBody><a:bodyPr/><a:p/></p:txBody>
9531    </p:sp>
9532  </p:spTree></p:cSld>
9533</p:sld>"#;
9534        let sld = parse_sld(xml).expect("parse ok");
9535        let sp = sld
9536            .shapes
9537            .iter()
9538            .find_map(|s| {
9539                if let OxmlSlideShape::Sp(sp) = s {
9540                    Some(sp)
9541                } else {
9542                    None
9543                }
9544            })
9545            .expect("应有 Sp");
9546        match &sp.properties.geometry {
9547            Some(Geometry::Preset(prst, adjustments)) => {
9548                assert_eq!(*prst, PresetGeometry::Rectangle);
9549                assert!(adjustments.is_empty(), "空 avLst 应解析为空列表");
9550            }
9551            other => panic!("期望 Preset 几何,得到 {:?}", other),
9552        }
9553    }
9554
9555    /// 验证无 `<a:avLst>` 的 `<a:prstGeom>` 解析为空调整值列表(TODO-038)。
9556    #[test]
9557    fn parse_prst_geom_without_av_lst() {
9558        let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
9559<p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
9560       xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
9561       xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
9562  <p:cSld><p:spTree>
9563    <p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
9564    <p:grpSpPr><a:xfrm/></p:grpSpPr>
9565    <p:sp>
9566      <p:nvSpPr><p:cNvPr id="5" name="NoAdj"/><p:cNvSpPr/><p:nvPr/></p:nvSpPr>
9567      <p:spPr>
9568        <a:xfrm><a:off x="0" y="0"/><a:ext cx="1000000" cy="500000"/></a:xfrm>
9569        <a:prstGeom prst="rect"/>
9570      </p:spPr>
9571      <p:txBody><a:bodyPr/><a:p/></p:txBody>
9572    </p:sp>
9573  </p:spTree></p:cSld>
9574</p:sld>"#;
9575        let sld = parse_sld(xml).expect("parse ok");
9576        let sp = sld
9577            .shapes
9578            .iter()
9579            .find_map(|s| {
9580                if let OxmlSlideShape::Sp(sp) = s {
9581                    Some(sp)
9582                } else {
9583                    None
9584                }
9585            })
9586            .expect("应有 Sp");
9587        match &sp.properties.geometry {
9588            Some(Geometry::Preset(prst, adjustments)) => {
9589                assert_eq!(*prst, PresetGeometry::Rectangle);
9590                assert!(adjustments.is_empty(), "无 avLst 应解析为空列表");
9591            }
9592            other => panic!("期望 Preset 几何,得到 {:?}", other),
9593        }
9594    }
9595
9596    /// 验证 `parse_fmla_val` 对各种公式格式的解析(TODO-038)。
9597    #[test]
9598    fn parse_fmla_val_formats() {
9599        // 标准格式 "val 16667"
9600        assert_eq!(parse_fmla_val("val 16667"), Some(16667));
9601        // 无空格 "val16667"(罕见但合法)
9602        assert_eq!(parse_fmla_val("val16667"), Some(16667));
9603        // 带前后空白
9604        assert_eq!(parse_fmla_val("  val 16667  "), Some(16667));
9605        // 负数
9606        assert_eq!(parse_fmla_val("val -5000"), Some(-5000));
9607        // 非数字
9608        assert_eq!(parse_fmla_val("val abc"), None);
9609        // 非 val 公式(如乘法公式)
9610        assert_eq!(parse_fmla_val("*/ adj1 100000 50000"), None);
9611        // 空字符串
9612        assert_eq!(parse_fmla_val(""), None);
9613    }
9614
9615    /// 验证 `parse_graphic_frame` 能识别 SmartArt 并保留 raw_xml(TODO-037 最小保留)。
9616    ///
9617    /// 关键断言:
9618    /// - `<a:graphicData uri=".../diagram">` 应被识别为 `Graphic::SmartArt`;
9619    /// - `raw_xml` 应包含完整的 `<a:graphicData>` 元素(含外壳);
9620    /// - 4 个关系 id(r:dm / r:lo / r:qs / r:cs)应被正确提取。
9621    #[test]
9622    fn parse_graphic_frame_with_smartart() {
9623        let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
9624<p:graphicFrame xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
9625                xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
9626                xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
9627                xmlns:dgm="http://schemas.openxmlformats.org/drawingml/2006/diagram">
9628  <p:nvGraphicFramePr>
9629    <p:cNvPr id="300" name="SmartArt 1"/>
9630    <p:cNvGraphicFramePr/>
9631    <p:nvPr/>
9632  </p:nvGraphicFramePr>
9633  <p:xfrm>
9634    <a:off x="457200" y="1828800"/>
9635    <a:ext cx="8229600" cy="1143000"/>
9636  </p:xfrm>
9637  <a:graphic>
9638    <a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/diagram">
9639      <dgm:relIds r:dm="rIdDm1" r:lo="rIdLo1" r:qs="rIdQs1" r:cs="rIdCs1"/>
9640    </a:graphicData>
9641  </a:graphic>
9642</p:graphicFrame>"#;
9643        let frame = parse_graphic_frame(xml).expect("parse ok");
9644        assert_eq!(frame.id, 300);
9645        assert_eq!(frame.name, "SmartArt 1");
9646        match &frame.graphic {
9647            crate::oxml::shape::Graphic::SmartArt(s) => {
9648                // raw_xml 应包含完整 graphicData 元素
9649                assert!(
9650                    s.raw_xml.contains("<a:graphicData"),
9651                    "raw_xml 应包含 graphicData 外壳,实际: {}",
9652                    s.raw_xml
9653                );
9654                assert!(
9655                    s.raw_xml.contains(
9656                        "uri=\"http://schemas.openxmlformats.org/drawingml/2006/diagram\""
9657                    ),
9658                    "raw_xml 应包含 diagram uri,实际: {}",
9659                    s.raw_xml
9660                );
9661                // 4 个关系 id 应被正确提取
9662                assert_eq!(s.dm_rid.as_deref(), Some("rIdDm1"));
9663                assert_eq!(s.lo_rid.as_deref(), Some("rIdLo1"));
9664                assert_eq!(s.qs_rid.as_deref(), Some("rIdQs1"));
9665                assert_eq!(s.cs_rid.as_deref(), Some("rIdCs1"));
9666            }
9667            other => panic!("期望 SmartArt,得到 {:?}", other),
9668        }
9669    }
9670
9671    /// 验证 `parse_smartart_rel_ids` 对各种 raw_xml 格式的解析(TODO-037)。
9672    #[test]
9673    fn parse_smartart_rel_ids_formats() {
9674        // 标准格式:4 个关系 id 都存在
9675        let raw = r#"<a:graphicData uri=".../diagram"><dgm:relIds r:dm="rId1" r:lo="rId2" r:qs="rId3" r:cs="rId4"/></a:graphicData>"#;
9676        let (dm, lo, qs, cs) = parse_smartart_rel_ids(raw);
9677        assert_eq!(dm.as_deref(), Some("rId1"));
9678        assert_eq!(lo.as_deref(), Some("rId2"));
9679        assert_eq!(qs.as_deref(), Some("rId3"));
9680        assert_eq!(cs.as_deref(), Some("rId4"));
9681
9682        // 缺失部分属性
9683        let raw_partial =
9684            r#"<a:graphicData uri=".../diagram"><dgm:relIds r:dm="rId1"/></a:graphicData>"#;
9685        let (dm, lo, qs, cs) = parse_smartart_rel_ids(raw_partial);
9686        assert_eq!(dm.as_deref(), Some("rId1"));
9687        assert!(lo.is_none(), "lo 应为 None");
9688        assert!(qs.is_none(), "qs 应为 None");
9689        assert!(cs.is_none(), "cs 应为 None");
9690
9691        // 完全缺失 dgm:relIds
9692        let raw_empty = r#"<a:graphicData uri=".../diagram"></a:graphicData>"#;
9693        let (dm, lo, qs, cs) = parse_smartart_rel_ids(raw_empty);
9694        assert!(dm.is_none() && lo.is_none() && qs.is_none() && cs.is_none());
9695    }
9696}