Skip to main content

pptx_rs/ppt97/
watermark.rs

1//! .ppt 文件水印注入。
2//!
3//! 本模块为 PowerPoint 97-2003 二进制格式(`.ppt`)文件注入水印。
4//! 水印作为 **MainMaster 母版的背景元素**注入到 PPDrawing 的 SpgrContainer 中,
5//! 位于"组形状本身"之后(z-order 最低的真正子形状),实现真正水印的视觉效果:
6//!
7//! - 全屏覆盖(ClientAnchor 0,0 → 5760,4320)
8//! - 大字号、可配置旋转角度、中灰色(默认)
9//! - 无填充、无边框、锁定不可编辑(FOPT 保护位 0x01C2)
10//! - 普通视图下不可选中/编辑(符合业界水印常识)
11//!
12//! # 与 python-pptx 的对应
13//!
14//! python-pptx 不支持 .ppt 二进制格式。本模块对标 Aspose.Slides 的
15//! `ISlideMaster.add_watermark` 和 LibreOffice 的母版背景元素注入。
16//!
17//! # 规范依据
18//!
19//! - [MS-ODRAW]:Office Drawing 97-2003 二进制格式(Escher OfficeArt)
20//! - [MS-PPT] 2.3.4:PPDrawing / OfficeArtClientTextbox 等
21//!
22//! # 水印 SpContainer 结构
23//!
24//! ```text
25//! SpContainer (0xF004, container)
26//! ├── FSP (0xF00A): 形状属性,inst=0xCA (TextBox)
27//! ├── FOPT (0xF00B): 形状选项(无填充、无线条、锁定、旋转)
28//! ├── ClientAnchor (0xF010): 形状锚点(全屏覆盖)
29//! └── ClientTextbox (0xF00D, container): 文本框
30//!     ├── TextHeaderAtom (0x0F9F): 文本类型
31//!     ├── TextCharsAtom (0x0FA0): 水印文本(UTF-16LE)
32//!     └── StyleTextPropAtom (0x0FA1): 文本样式(字号、颜色)
33//! ```
34//!
35//! [MS-ODRAW]: https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-odraw
36//! [MS-PPT]: https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-ppt
37
38use crate::error::{Error, Result};
39use crate::ppt97::record::{
40    find_main_masters, find_ppdrawing_in_master, parse_record_header, read_u32_le, write_u32_le,
41    RT_MAIN_MASTER,
42};
43
44// ============================================================================
45// OfficeArt record type(MS-ODRAW 规范)
46// ============================================================================
47
48/// DgContainer record type(Drawing Container)。
49const RT_DG_CONTAINER: u16 = 0xF002;
50
51/// SpgrContainer record type(Shape Group Container)。
52pub const RT_SPGR_CONTAINER: u16 = 0xF003;
53
54/// SpContainer record type(Shape Container)。
55pub const RT_SP_CONTAINER: u16 = 0xF004;
56
57/// FSP record type(File Shape Properties,ver=2,inst=MSOSPT 形状类型)。
58pub const RT_FSP: u16 = 0xF00A;
59
60/// FOPT record type(File Option,ver=3,inst=属性个数)。
61pub const RT_FOPT: u16 = 0xF00B;
62
63/// ClientTextbox record type(container 版本,ver=0xF)。
64pub const RT_CLIENT_TEXTBOX: u16 = 0xF00D;
65
66/// ClientAnchor record type(形状锚点,ver=0,inst=0)。
67pub const RT_CLIENT_ANCHOR: u16 = 0xF010;
68
69// ============================================================================
70// PPT Text record types
71// ============================================================================
72
73/// TextHeaderAtom record type。
74const RT_TEXT_HEADER_ATOM: u16 = 0x0F9F;
75
76/// TextCharsAtom record type(UTF-16LE 文本)。
77const RT_TEXT_CHARS_ATOM: u16 = 0x0FA0;
78
79/// StyleTextPropAtom record type(文本样式属性)。
80const RT_STYLE_TEXT_PROP_ATOM: u16 = 0x0FA1;
81
82// ============================================================================
83// 水印配置
84// ============================================================================
85
86/// 水印配置参数。
87///
88/// 参考 python-pptx 的 `shapes.add_textbox()` + `font` 组合 API 和
89/// Aspose.Slides 的 `PortionFormat` 参数化模式,将水印的可变属性集中管理,
90/// 避免硬编码散落在 [`build_watermark_spcontainer`] 各处。
91///
92/// # 字段对应关系
93///
94/// - `text` ↔ python-pptx `text_frame.text` / Aspose `add_text_frame(text)`
95/// - `font_size_pt` ↔ python-pptx `font.size = Pt(n)` / Aspose `font_height`
96/// - `color_rgb` ↔ python-pptx `font.color.rgb` / Aspose `solid_fill_color`
97/// - `rotation_deg` ↔ python-pptx `shape.rotation` / Aspose `shape.rotation`
98///
99/// # 示例
100///
101/// ```no_run
102/// use pptx_rs::ppt97::watermark::WatermarkConfig;
103///
104/// let config = WatermarkConfig {
105///     text: "机密".to_string(),
106///     font_size_pt: 72,
107///     color_rgb: (180, 180, 180),
108///     rotation_deg: -30,
109/// };
110/// ```
111#[derive(Clone, Debug)]
112pub struct WatermarkConfig {
113    /// 水印文本内容。
114    pub text: String,
115    /// 字号(磅),常见水印字号 44pt。
116    pub font_size_pt: u16,
117    /// 文字颜色 (r, g, b),每个分量 0-255。
118    pub color_rgb: (u8, u8, u8),
119    /// 旋转角度(度),正值顺时针,负值逆时针。
120    pub rotation_deg: i32,
121}
122
123impl Default for WatermarkConfig {
124    /// 默认水印配置:44pt 中灰色 "pptx-rs 水印",45 度旋转。
125    fn default() -> Self {
126        Self {
127            text: "pptx-rs 水印".to_string(),
128            font_size_pt: 44,
129            color_rgb: (200, 200, 200),
130            rotation_deg: 45,
131        }
132    }
133}
134
135// ============================================================================
136// 水印注入主逻辑
137// ============================================================================
138
139/// 在 PowerPoint Document stream 中注入水印。
140///
141/// 完整流程:
142/// 1. 找到所有 MainMaster record
143/// 2. 对每个 MainMaster 的 PPDrawing:
144///    - 幂等性检查(已存在水印文本则跳过)
145///    - 分配唯一 shapeId(避免与已有形状冲突)
146///    - 构造水印 SpContainer
147///    - 插入到 SpgrContainer 的"组形状本身"之后(z-order 最低的真正形状)
148///    - 更新 MainMaster / PPDrawing / DgContainer / SpgrContainer 的 recLen
149/// 3. 重新计算所有 persist 对象的 offset(因插入导致后移)
150/// 4. 更新 PersistDirectoryAtom 中存储的 offset
151///
152/// # 参数
153/// - `ppt_data`:PowerPoint Document stream(原地修改)
154/// - `config`:水印配置
155/// - `persist_entries`:旧的 persist entries(注入前的 offset)
156/// - `pd_offset_old`:旧的 PersistDirectoryAtom offset
157///
158/// # 返回
159/// - 成功:`(total_inserted, new_entries, pd_offset_new)`
160///   - `total_inserted`:总共插入的字节数
161///   - `new_entries`:更新后的 persist entries
162///   - `pd_offset_new`:新的 PersistDirectoryAtom offset
163///
164/// # 错误
165/// - [`Error::Ppt97`]:record 解析失败 / PPDrawing 结构异常 / shapeId 扫描失败
166#[allow(clippy::type_complexity)]
167pub fn inject_watermark(
168    ppt_data: &mut Vec<u8>,
169    config: &WatermarkConfig,
170    persist_entries: &[(u32, u32)],
171    pd_offset_old: usize,
172) -> Result<(usize, Vec<(u32, u32)>, usize)> {
173    // 返回类型 (total_inserted, new_entries, pd_offset_new) 较复杂,但语义清晰,
174    // 拆分为多个返回值会比封装成结构体更直观(调用方需要分别使用这三个值)。
175    let master_offsets = find_main_masters(ppt_data)?;
176    let mut insertions: Vec<(usize, usize)> = Vec::new();
177    let mut total_inserted = 0;
178
179    // 预计算水印文本的 UTF-16LE 字节序列,用于幂等性检测
180    let watermark_utf16: Vec<u8> = config
181        .text
182        .encode_utf16()
183        .flat_map(|c| c.to_le_bytes())
184        .collect();
185
186    // 扫描所有 MainMaster 中已有的最大 shapeId,避免 ID 冲突
187    // 参考 Apache POI XSLFSheet.allocateShapeId 的 BitSet + nextClearBit 模式
188    let mut next_shape_id: u32 = find_max_shape_id(ppt_data, &master_offsets)? + 1;
189    // 确保 shapeId 不低于常见起始值 0x1000(与 PowerPoint 内部约定一致)
190    if next_shape_id < 0x1000 {
191        next_shape_id = 0x1000;
192    }
193
194    // 从后往前处理 MainMaster,记录所有插入点
195    for master_offset in master_offsets.iter().rev() {
196        let master_offset = *master_offset;
197        if let Some(ppd_offset) = find_ppdrawing_in_master(ppt_data, master_offset)? {
198            // 幂等性检查:扫描 PPDrawing 中是否已存在水印文本
199            if has_watermark_text(ppt_data, ppd_offset, &watermark_utf16) {
200                // 已存在水印,跳过注入(幂等性保证)
201                continue;
202            }
203
204            let shape_id = next_shape_id;
205            next_shape_id += 1;
206            let watermark_sp = build_watermark_spcontainer(shape_id, config);
207
208            let (insert_pos, insert_len) =
209                inject_watermark_into_ppdrawing(ppt_data, ppd_offset, &watermark_sp)?;
210
211            // 更新 MainMaster 的 recLen
212            let (_, _, _, master_len) = parse_record_header(ppt_data, master_offset)?;
213            write_u32_le(ppt_data, master_offset + 4, master_len + insert_len as u32)?;
214
215            insertions.push((insert_pos, insert_len));
216            total_inserted += insert_len;
217        }
218    }
219
220    // 计算每个 persist 对象的新 offset
221    // 对于旧 offset O,新 offset = O + sum(len for all (pos, len) where pos < O)
222    // 因为插入点 pos < O 时,插入发生在 O 之前,O 需要后移
223    let mut new_entries = Vec::with_capacity(persist_entries.len());
224    for (pid, offset) in persist_entries {
225        let mut new_offset = *offset;
226        for (pos, len) in &insertions {
227            if *pos < new_offset as usize {
228                new_offset += *len as u32;
229            }
230        }
231        new_entries.push((*pid, new_offset));
232    }
233
234    // 计算 PersistDirectoryAtom 的新 offset
235    let mut pd_offset_new = pd_offset_old;
236    for (pos, len) in &insertions {
237        if *pos < pd_offset_new {
238            pd_offset_new += len;
239        }
240    }
241
242    // 更新 PersistDirectoryAtom 中存储的 offset
243    // PersistDirectoryAtom 结构:header(8) + entry(4) + rgPersistOffset(cPersist * 4)
244    let pd_data_start = pd_offset_new + 8;
245
246    // 更新每个 persist offset
247    for (i, (_, new_offset)) in new_entries.iter().enumerate() {
248        let offset_pos = pd_data_start + 4 + i * 4;
249        write_u32_le(ppt_data, offset_pos, *new_offset)?;
250    }
251
252    Ok((total_inserted, new_entries, pd_offset_new))
253}
254
255/// 在 PPDrawing 中注入水印 SpContainer。
256///
257/// PPDrawing 的完整结构(MS-ODRAW 规范):
258///
259/// ```text
260/// PPDrawing (container, 0x040C)
261///   └── DgContainer (container, 0xF002)
262///        └── SpgrContainer (container, 0xF003)
263///             ├── SpContainer (0xF004) — 组形状本身(FSP.inst=0, MSOSPT_Min)
264///             ├── SpContainer (0xF004) — 其他形状(z-order 从低到高)
265///             └── ...
266/// ```
267///
268/// **关键设计**:水印插入到 SpgrContainer 的"组形状本身"之后,成为 z-order
269/// 最低的真正子形状(背景层)。这正是 PowerPoint 自带"插入水印"功能的实现方式:
270///
271/// - 视觉上被其他内容覆盖,符合水印背景特性
272/// - 配合 FOPT 锁定属性(0x01C2=0x0D),在普通视图下不可选中/编辑
273///
274/// # 参数
275/// - `data`:PowerPoint Document stream(原地修改)
276/// - `ppd_offset`:PPDrawing record 的起始 offset
277/// - `watermark_sp`:构造好的水印 SpContainer 字节序列
278///
279/// # 返回
280/// - 成功:`(insert_pos, insert_len)` 插入位置与字节数
281///
282/// # 错误
283/// - [`Error::Ppt97`]:PPDrawing 中找不到 DgContainer / SpgrContainer / 组形状本身
284pub fn inject_watermark_into_ppdrawing(
285    data: &mut Vec<u8>,
286    ppd_offset: usize,
287    watermark_sp: &[u8],
288) -> Result<(usize, usize)> {
289    let (_, _, _, ppd_len) = parse_record_header(data, ppd_offset)?;
290
291    // 第 1 层:在 PPDrawing 中找到 DgContainer (0xF002)
292    let ppd_end = ppd_offset + 8 + ppd_len as usize;
293    let mut pos = ppd_offset + 8;
294    let mut dg_offset = None;
295    while pos + 8 <= ppd_end {
296        let (ver, _, rec_type, rec_len) = parse_record_header(data, pos)?;
297        let is_container = ver == 0xF;
298        let total_len = 8 + rec_len as usize;
299
300        if is_container && rec_type == RT_DG_CONTAINER {
301            dg_offset = Some(pos);
302            break;
303        }
304
305        pos += total_len;
306        if !is_container && rec_len == 0 {
307            break;
308        }
309    }
310
311    let dg_offset = dg_offset.ok_or_else(|| {
312        Error::ppt97("inject_watermark: DgContainer (0xF002) not found in PPDrawing")
313    })?;
314    let (_, _, _, dg_len) = parse_record_header(data, dg_offset)?;
315
316    // 第 2 层:在 DgContainer 中找到 SpgrContainer (0xF003)
317    let dg_end = dg_offset + 8 + dg_len as usize;
318    let mut pos = dg_offset + 8;
319    let mut spgr_offset = None;
320    while pos + 8 <= dg_end {
321        let (ver, _, rec_type, rec_len) = parse_record_header(data, pos)?;
322        let is_container = ver == 0xF;
323        let total_len = 8 + rec_len as usize;
324
325        if is_container && rec_type == RT_SPGR_CONTAINER {
326            spgr_offset = Some(pos);
327            break;
328        }
329
330        pos += total_len;
331        if !is_container && rec_len == 0 {
332            break;
333        }
334    }
335
336    let spgr_offset = spgr_offset.ok_or_else(|| {
337        Error::ppt97("inject_watermark: SpgrContainer (0xF003) not found in DgContainer")
338    })?;
339    let (_, _, _, spgr_len) = parse_record_header(data, spgr_offset)?;
340    let spgr_end = spgr_offset + 8 + spgr_len as usize;
341
342    // 关键设计:把水印插入到 SpgrContainer 的"组形状本身"之后,
343    // 成为 z-order 最低的真正子形状(背景层)。
344    //
345    // MS-ODRAW 规范 2.2.17:SpgrContainer 的第一个 SpContainer 必须是"组形状本身"
346    // (FSP.inst=0, MSOSPT_Min=0),描述整个组的属性;后续 SpContainer 才是
347    // 真正的子形状,按 z-order 从低到高排列。
348    //
349    // 之前把水印插到 SpgrContainer 末尾(z-order 最高),导致:
350    // 1. 水印在最上层,遮挡其他内容
351    // 2. 水印易被选中编辑,违背水印作为背景元素的特性
352    // 现在插到"组形状本身"之后,水印成为 z-order 最低的真正形状:
353    // - 视觉上被其他内容覆盖,符合水印背景特性
354    // - 配合 FOPT 锁定属性(0x01C2=0x0D),在普通视图下不可选中/编辑
355    // - 这正是 PowerPoint 自带"插入水印"功能的实现方式
356    let mut pos = spgr_offset + 8;
357    let mut first_sp_end = None;
358    while pos + 8 <= spgr_end {
359        let (ver, _, rec_type, rec_len) = parse_record_header(data, pos)?;
360        let is_container = ver == 0xF;
361        let total_len = 8 + rec_len as usize;
362
363        // 找到第一个 SpContainer(组形状本身),记录其结束位置
364        if is_container && rec_type == RT_SP_CONTAINER {
365            first_sp_end = Some(pos + total_len);
366            break;
367        }
368
369        pos += total_len;
370        if !is_container && rec_len == 0 {
371            break;
372        }
373    }
374
375    // 插入位置:第一个 SpContainer 之后(z-order 最低的真正形状位置)
376    // 如果找不到第一个 SpContainer(异常情况),退回到末尾插入以保证兼容
377    let insert_pos = first_sp_end.unwrap_or(spgr_end);
378    data.splice(insert_pos..insert_pos, watermark_sp.iter().copied());
379
380    let insert_len = watermark_sp.len();
381
382    // 更新三层 container 的 recLen(从内到外):
383    // 1. SpgrContainer 的 recLen
384    write_u32_le(data, spgr_offset + 4, spgr_len + insert_len as u32)?;
385    // 2. DgContainer 的 recLen
386    write_u32_le(data, dg_offset + 4, dg_len + insert_len as u32)?;
387    // 3. PPDrawing 的 recLen
388    write_u32_le(data, ppd_offset + 4, ppd_len + insert_len as u32)?;
389
390    Ok((insert_pos, insert_len))
391}
392
393/// 构造水印 SpContainer。
394///
395/// 结构(MS-ODRAW 规范):
396///
397/// ```text
398/// SpContainer (0xF004, container)
399/// ├── FSP (0xF00A): ver=2, inst=0xCA (MSOSPT_TextBox=202)
400/// │   ├── shapeId (u32): 唯一形状 ID
401/// │   └── flags (u32): fHaveAnchor + fHaveSpt
402/// ├── FOPT (0xF00B): ver=3, inst=属性个数
403/// │   ├── 0x00BD (rotation): 旋转角度(16.16 固定点数)
404/// │   ├── 0x0180 (fillType): 0 = No fill
405/// │   ├── 0x01BF (Fill Style Boolean): fNoFill + fFillOK + fNoFillHitTest
406/// │   ├── 0x01C1 (Line Style Boolean): fNoLine + fLineOK + fNoLineDrawDash
407/// │   └── 0x01C2 (Protection Boolean): 锁定分组+文本编辑+选择
408/// ├── ClientAnchor (0xF010): ver=0, 全屏覆盖 (0,0 → 5760,4320)
409/// └── ClientTextbox (0xF00D, container): 文本框
410///     ├── TextHeaderAtom (0x0F9F): txType=4 (not body)
411///     ├── TextCharsAtom (0x0FA0): 水印文本(UTF-16LE)
412///     └── StyleTextPropAtom (0x0FA1): 文本样式(字号、颜色)
413/// ```
414///
415/// # FOPT 保护位详解
416///
417/// `0x01C2` (Protection Boolean Properties) = `0x0000000D`:
418/// - bit 0 (fLockAgainstGrouping):防止选择、分组、移动
419/// - bit 2 (fLockAgainstTextEdit):防止文本编辑
420/// - bit 3 (fLockAgainstSelection):防止选择
421///
422/// # 参数
423/// - `shape_id`:水印形状的唯一 ID(由调用方确保不冲突)
424/// - `config`:水印配置(文本、字号、颜色、旋转角度)
425///
426/// # 返回
427/// 完整的 SpContainer 字节序列(含 record header)。
428pub fn build_watermark_spcontainer(shape_id: u32, config: &WatermarkConfig) -> Vec<u8> {
429    let mut children = Vec::new();
430
431    // 1. FSP (0xF00A): 形状属性
432    // ver=0x2, inst=0xCA (MSOSPT_TextBox=202), type=0xF00A, len=8
433    // shapeId (4 bytes) + flags (4 bytes)
434    // MSOSPT_TextBox = 202 = 0xCA(MS-ODRAW 规范 2.4.14 MSOSPT 枚举)
435    let mut fsp = Vec::new();
436    let ver_inst: u16 = (0xCA << 4) | 0x2; // inst=0xCA (textBox=202), ver=0x2
437    fsp.extend_from_slice(&ver_inst.to_le_bytes());
438    fsp.extend_from_slice(&RT_FSP.to_le_bytes());
439    fsp.extend_from_slice(&8u32.to_le_bytes()); // len=8
440    fsp.extend_from_slice(&shape_id.to_le_bytes()); // shapeId
441                                                    // flags (MS-ODRAW 2.3.1.1 FSP):
442                                                    //   bit 7 (0x80): fHaveAnchor — 1 = 形状有 anchor(必须有,否则 PowerPoint 忽略 ClientAnchor)
443                                                    //   bit 9 (0x200): fHaveSpt — 1 = 形状有 shape type
444                                                    // 正确值:fHaveAnchor + fHaveSpt = 0x80 + 0x200 = 0x280
445    fsp.extend_from_slice(&0x00000280u32.to_le_bytes()); // flags: fHaveAnchor + fHaveSpt
446    children.extend_from_slice(&fsp);
447
448    // 2. FOPT (0xF00B): 形状属性(无填充、无线条,旋转,锁定不可编辑)
449    // FOPT 属性按 property ID 升序排列
450    let mut fopt_props = Vec::new();
451    // 0x00BD (rotation): 旋转角度(固定点数 16.16 格式,1度 = 65536)
452    let rotation_fixed: u32 = ((config.rotation_deg as i64) * 65536) as u32;
453    fopt_props.extend_from_slice(&0x00BDu16.to_le_bytes());
454    fopt_props.extend_from_slice(&rotation_fixed.to_le_bytes());
455    // 0x0180 (fillType): 0 = No fill(明确指定无填充,避免 PowerPoint 使用默认白色背景)
456    fopt_props.extend_from_slice(&0x0180u16.to_le_bytes());
457    fopt_props.extend_from_slice(&0x00000000u32.to_le_bytes());
458    // 0x01BF (Fill Style Boolean Properties): fNoFill + fFillOK + fNoFillHitTest = 0x00000043
459    fopt_props.extend_from_slice(&0x01BFu16.to_le_bytes());
460    fopt_props.extend_from_slice(&0x00000043u32.to_le_bytes());
461    // 0x01C1 (Line Style Boolean Properties): fNoLine + fLineOK + fNoLineDrawDash = 0x00000043
462    fopt_props.extend_from_slice(&0x01C1u16.to_le_bytes());
463    fopt_props.extend_from_slice(&0x00000043u32.to_le_bytes());
464    // 0x01C2 (Protection Boolean Properties): 锁定分组+文本编辑+选择 = 0x0000000D
465    fopt_props.extend_from_slice(&0x01C2u16.to_le_bytes());
466    fopt_props.extend_from_slice(&0x0000000Du32.to_le_bytes());
467
468    let num_props = fopt_props.len() / 6;
469    let mut fopt = Vec::new();
470    let ver_inst: u16 = ((num_props as u16) << 4) | 0x3; // inst=num_props, ver=0x3
471    fopt.extend_from_slice(&ver_inst.to_le_bytes());
472    fopt.extend_from_slice(&RT_FOPT.to_le_bytes());
473    fopt.extend_from_slice(&(fopt_props.len() as u32).to_le_bytes());
474    fopt.extend_from_slice(&fopt_props);
475    children.extend_from_slice(&fopt);
476
477    // 3. ClientAnchor (0xF010): 形状位置
478    // ver=0, inst=0, len=8(SmallRectStruct: 4 个 int16: top, left, right, bottom)
479    // 单位是 master units(1/576 英寸),slide 标准尺寸 5760 x 4320(10 x 7.5 英寸)
480    // 水印覆盖整个 slide 区域,配合旋转和大字号,让水印文字斜向铺满整个幻灯片
481    let mut anchor = Vec::new();
482    let ver_inst: u16 = 0; // inst=0, ver=0
483    anchor.extend_from_slice(&ver_inst.to_le_bytes());
484    anchor.extend_from_slice(&RT_CLIENT_ANCHOR.to_le_bytes());
485    anchor.extend_from_slice(&8u32.to_le_bytes()); // len=8
486    anchor.extend_from_slice(&0i16.to_le_bytes()); // top = 0
487    anchor.extend_from_slice(&0i16.to_le_bytes()); // left = 0
488    anchor.extend_from_slice(&5760i16.to_le_bytes()); // right = 5760(10 英寸)
489    anchor.extend_from_slice(&4320i16.to_le_bytes()); // bottom = 4320(7.5 英寸)
490    children.extend_from_slice(&anchor);
491
492    // 4. ClientTextbox (0xF00D, container): 文本框
493    let mut textbox_children = Vec::new();
494
495    // 4.1 TextHeaderAtom (0x0F9F): txType=4 (not body)
496    let mut text_header = Vec::new();
497    let ver_inst: u16 = 0;
498    text_header.extend_from_slice(&ver_inst.to_le_bytes());
499    text_header.extend_from_slice(&RT_TEXT_HEADER_ATOM.to_le_bytes());
500    text_header.extend_from_slice(&4u32.to_le_bytes()); // len=4
501    text_header.extend_from_slice(&4u32.to_le_bytes()); // txType=4 (not body)
502    textbox_children.extend_from_slice(&text_header);
503
504    // 4.2 TextCharsAtom (0x0FA0): 水印文本(UTF-16LE)
505    let text_utf16: Vec<u8> = config
506        .text
507        .encode_utf16()
508        .flat_map(|c| c.to_le_bytes())
509        .collect();
510    let text_char_count = config.text.encode_utf16().count() as u32;
511    let mut text_chars = Vec::new();
512    let ver_inst: u16 = 0;
513    text_chars.extend_from_slice(&ver_inst.to_le_bytes());
514    text_chars.extend_from_slice(&RT_TEXT_CHARS_ATOM.to_le_bytes());
515    text_chars.extend_from_slice(&(text_utf16.len() as u32).to_le_bytes());
516    text_chars.extend_from_slice(&text_utf16);
517    textbox_children.extend_from_slice(&text_chars);
518
519    // 4.3 StyleTextPropAtom (0x0FA1): 文本样式(字体大小、颜色)
520    // StyleTextPropAtom 结构(MS-PPT 2.9.17 规范):
521    //   lfo (4 bytes): 段落格式 run 数量
522    //   rgTextPFRun[lfo]: 段落格式 run
523    //   rgTextCFRun[lfo]: 字符格式 run
524    // TextPFRun: count(4) + indentLevel(2) + pfFlags(4) = 10 bytes
525    // TextCFRun: count(4) + cfFlags(4) + [sz(2) if fSize] + [color(4) if fColor]
526    // cfFlags 位定义:
527    //   bit 6 (0x40): fSize — 设置字体大小
528    //   bit 7 (0x80): fColor — 设置文字颜色
529    let mut style_data = Vec::new();
530    style_data.extend_from_slice(&1u32.to_le_bytes()); // lfo = 1
531    style_data.extend_from_slice(&text_char_count.to_le_bytes()); // count
532    style_data.extend_from_slice(&0u16.to_le_bytes()); // indentLevel = 0
533    style_data.extend_from_slice(&0u32.to_le_bytes()); // pfFlags = 0
534    style_data.extend_from_slice(&text_char_count.to_le_bytes()); // count
535    style_data.extend_from_slice(&0x000000C0u32.to_le_bytes()); // cfFlags: fSize + fColor
536                                                                // 字号(单位是 1/100 pt)
537    let font_size_value = config.font_size_pt as u32 * 100;
538    style_data.extend_from_slice(&(font_size_value as u16).to_le_bytes()); // sz
539                                                                           // color: ColorIndexStruct (red, green, blue, index)
540                                                                           // 半透明效果:使用中灰色模拟半透明(PPT 97-2003 文本透明度需要复杂扩展属性)
541    style_data.extend_from_slice(&config.color_rgb.0.to_le_bytes()); // red
542    style_data.extend_from_slice(&config.color_rgb.1.to_le_bytes()); // green
543    style_data.extend_from_slice(&config.color_rgb.2.to_le_bytes()); // blue
544    style_data.extend_from_slice(&0u8.to_le_bytes()); // index = 0 (RGB)
545
546    let mut style_atom = Vec::new();
547    let ver_inst: u16 = 0;
548    style_atom.extend_from_slice(&ver_inst.to_le_bytes());
549    style_atom.extend_from_slice(&RT_STYLE_TEXT_PROP_ATOM.to_le_bytes());
550    style_atom.extend_from_slice(&(style_data.len() as u32).to_le_bytes());
551    style_atom.extend_from_slice(&style_data);
552    textbox_children.extend_from_slice(&style_atom);
553
554    // 组装 ClientTextbox
555    let mut client_textbox = Vec::new();
556    let ver_inst: u16 = 0xF; // container
557    client_textbox.extend_from_slice(&ver_inst.to_le_bytes());
558    client_textbox.extend_from_slice(&RT_CLIENT_TEXTBOX.to_le_bytes());
559    client_textbox.extend_from_slice(&(textbox_children.len() as u32).to_le_bytes());
560    client_textbox.extend_from_slice(&textbox_children);
561    children.extend_from_slice(&client_textbox);
562
563    // 组装 SpContainer
564    let mut sp_container = Vec::new();
565    let ver_inst: u16 = 0xF; // container
566    sp_container.extend_from_slice(&ver_inst.to_le_bytes());
567    sp_container.extend_from_slice(&RT_SP_CONTAINER.to_le_bytes());
568    sp_container.extend_from_slice(&(children.len() as u32).to_le_bytes());
569    sp_container.extend_from_slice(&children);
570
571    sp_container
572}
573
574/// 扫描所有 MainMaster 的 PPDrawing 中已有的最大 shapeId。
575///
576/// 参考 Apache POI `XSLFSheet.allocateShapeId()` 的 BitSet 模式:
577/// 遍历 SpgrContainer 中所有 FSP record (0xF00A),读取其 shapeId 字段,
578/// 返回最大值。新水印 shape 从 max+1 开始分配,避免 ID 冲突。
579///
580/// # 参数
581/// - `data`:PowerPoint Document stream
582/// - `master_offsets`:所有 MainMaster 的 offset 列表
583///
584/// # 返回
585/// 所有 MainMaster 中最大的 shapeId(若无形状返回 0)。
586pub fn find_max_shape_id(data: &[u8], master_offsets: &[usize]) -> Result<u32> {
587    let mut max_id: u32 = 0;
588    for &master_offset in master_offsets {
589        if let Some(ppd_offset) = find_ppdrawing_in_master(data, master_offset)? {
590            max_id = max_id.max(scan_shape_ids_in_ppdrawing(data, ppd_offset)?);
591        }
592    }
593    Ok(max_id)
594}
595
596/// 递归扫描 PPDrawing 子树中所有 FSP record 的 shapeId,返回最大值。
597///
598/// PPDrawing → DgContainer → SpgrContainer → [SpContainer | SpgrContainer]...
599/// 每个 SpContainer 内含一个 FSP (0xF00A),其前 4 字节数据是 shapeId。
600/// SpgrContainer 可嵌套,需递归遍历。
601fn scan_shape_ids_in_ppdrawing(data: &[u8], ppd_offset: usize) -> Result<u32> {
602    let (_, _, _, ppd_len) = parse_record_header(data, ppd_offset)?;
603    let ppd_end = ppd_offset + 8 + ppd_len as usize;
604    let mut max_id: u32 = 0;
605    let mut pos = ppd_offset + 8;
606
607    while pos + 8 <= ppd_end {
608        let (ver, _, rec_type, rec_len) = parse_record_header(data, pos)?;
609        let is_container = ver == 0xF;
610        let total_len = 8 + rec_len as usize;
611
612        if is_container {
613            // 递归扫描 container 子节点
614            let child_max = scan_shape_ids_in_ppdrawing(data, pos)?;
615            max_id = max_id.max(child_max);
616        } else if rec_type == RT_FSP && rec_len >= 8 {
617            // FSP record: shapeId 在 header 之后的第 4 字节
618            let shape_id = read_u32_le(data, pos + 8)?;
619            if shape_id > max_id {
620                max_id = shape_id;
621            }
622        }
623
624        pos += total_len;
625        if !is_container && rec_len == 0 {
626            break;
627        }
628    }
629
630    Ok(max_id)
631}
632
633/// 检查 PPDrawing 中是否已存在指定水印文本(幂等性检查)。
634///
635/// 参考 Aspose.Slides 按 shape name/text 查重的幂等性模式:
636/// 将水印文本编码为 UTF-16LE(与 TextCharsAtom 0x0FA0 的编码一致),
637/// 在 PPDrawing 的数据范围内搜索该字节序列。
638///
639/// # 参数
640/// - `data`:PowerPoint Document stream
641/// - `ppd_offset`:PPDrawing record 的起始 offset
642/// - `watermark_utf16`:水印文本的 UTF-16LE 字节序列
643///
644/// # 返回
645/// - `true`:PPDrawing 中已存在水印文本(应跳过注入)
646/// - `false`:未找到水印文本(可以注入)
647pub fn has_watermark_text(data: &[u8], ppd_offset: usize, watermark_utf16: &[u8]) -> bool {
648    let (_, _, _, ppd_len) = match parse_record_header(data, ppd_offset) {
649        Ok(v) => v,
650        Err(_) => return false,
651    };
652    let ppd_end = ppd_offset + 8 + ppd_len as usize;
653    if ppd_end > data.len() {
654        return false;
655    }
656    // 在 PPDrawing 的完整数据范围内搜索水印文本的 UTF-16LE 字节序列
657    data[ppd_offset..ppd_end]
658        .windows(watermark_utf16.len())
659        .any(|w| w == watermark_utf16)
660}
661
662/// 抑制未使用常量警告(RT_MAIN_MASTER 在 inject_watermark 中通过 find_main_masters 间接使用)。
663#[allow(dead_code)]
664const _UNUSED_RT_MAIN_MASTER: u16 = RT_MAIN_MASTER;