Word 文本与内容控件设计
WordTemplateProcessor 使用 Open XML SDK 3.5.1 编辑 DOCX 包,不启动 Office,也不执行模板表达式。字段由结构化内容控件的 w:tag 确定,避免普通文字搜索破坏跨 run 的占位文本。使用示例和参数默认值见 模块 README。
只读文本提取
DOCX 是 OPC ZIP 包:主文档、页眉和页脚由包关系连接,不一定采用固定文件名。WordTextExtractor 先用内部 BoundedBuffer 从调用方当前位置读取有界副本,再检查条目数量和名称重复。通过根关系定位主文档,验证其 DOCX 内容类型,以禁止 DTD 和外部 XML 解析器的 XmlReader 流式读取正文;同时收集章节中的页眉/页脚关系 ID。可选读取这些内部引用,分别按部件路径排序并去重,不根据关系声明顺序或分页结果重复输出。
每个实际读取的 XML 先受剩余展开字节预算限制,读取结束按实际长度扣减;XML 解析另受单部件字符预算限制。返回文本每次追加前检查总字符预算。图片、签名、设置和其他不需要的部件不展开,不把 ZIP 声明长度当作实际解压大小的证明。读取过程中检查取消,所有自有流均释放,调用方输入流保持打开。限制针对编码、读取 XML 和文本规模,不等于进程内存硬配额。
正文按 XML 顺序提取 w:t,w:tab 生成 TAB,w:br/w:cr 和段落结束生成 LF。表格单元格结束移除尾部段落 LF 并生成 TAB,行结束移除尾部 TAB 并生成 LF;最终移除末尾分隔符。插入修订保留,w:del 与 w:moveFrom 子树跳过;域指令不执行,只保留其已经保存的文字结果。Transitional 和 Strict 的 Word/关系命名空间分别匹配。
此流程不调用模板编辑的 WordPackage.Validate,因此不会因签名或编辑保护拒绝只读内容;也不调用会展开所有二进制部件的模板完整包检查。读取不验证签名真实性,不解密密码保护包,不执行宏、外部关系或嵌入内容。对受支持文本部件执行结构根节点检查,而非完整 Open XML schema 验证;返回文字不能推导 Word 页码、视觉顺序或最终排版。脚注、尾注、批注、DrawingML 独立文字与嵌入文档不在首版提取范围。
参数不合法抛出 ArgumentException,字节/条目/文字超限及缺失关系、部件、结构错误抛出 InvalidDataException;XML 语法、DTD 和字符预算错误保留 XmlException。非 DOCX 主内容类型抛出 NotSupportedException;输入 I/O 与取消直接传播。失败不返回部分成功文本。
处理顺序与所有权
- 校验参数,按当前位置读取源流;每次最多读取剩余编码预算加一个探测字节,超限立即停止。
- 检查 ZIP 条目数量、名称重复和累计声明大小,再实际解压所有条目。所有 XML 部件按扩展名和 OPC 内容类型识别,以禁用 DTD、外部解析器和单部件字符上限的
XmlReader读取;SDK 的MaxCharactersInPart提供第二层限制。 - 将输入复制到独立、有界、可读写的内存流。以
AutoSave = false打开 SDK 文档,检查文档类型、签名与保护,并用OpenXmlValidator(Microsoft365)验证,最多返回一个错误。 - 遍历正文及其页眉、页脚部件中的控件,填充匹配值,显式保存相应 XML 根。关闭包以完成 ZIP 写入。
- 对最终工作副本重新进行 ZIP、展开大小、XML 和 SDK 验证,通过后才复制给目标流。
原始流不交给可编辑的 SDK 包,所有调用方流保持打开。验证或替换失败时尚未写入目标;最终复制开始后,取消或 I/O 失败可能留下部分输出。文件覆盖、临时文件和原子发布由应用层负责。取消传给异步读取、SDK 验证和复制,并在控件处理及包生命周期边界检查;同步 DOM 解析、保存和单个控件替换不能被强制中断。
文本与格式
字典在入口以 Ordinal 比较复制。无 Tag 的控件跳过;缺少值的带 Tag 控件按选定策略报错或保留。重复 Tag 每次都替换,未使用键不解析;null 等同空字符串,文本不进行递归插值。
支持叶级 SdtRun,以及只含一个段落的 SdtBlock。表格单元格使用段落内的普通控件;整行或整单元格的结构控件不受支持。目标只允许普通 run,以及 run 内的格式、文字、换行和制表符。绑定、选择、日期、图片、嵌套结构、域或绘图会改变替换语义,因此匹配到这类目标时明确拒绝。未匹配且使用 KeepOriginal 的控件保持原样。
替换保留控件的 Tag、ID、其他允许属性、结束字符属性和段落属性。新文本采用首个 run 的 RunProperties,首个 run 无格式时采用控件默认格式;占位文本各 run 的混合格式不映射到新文本字符。仅禁止删除控件的锁允许填充并保留,内容锁定拒绝;填充后移除 ShowingPlaceholder 状态。
CRLF、CR 和 LF 统一生成 Break,制表符生成 TabChar,普通片段生成带 xml:space="preserve" 的 Text。空值保留空文字节点。XmlConvert.VerifyXmlChars 拒绝无效 XML 字符及不成对代理项,SDK 负责转义 <、& 等字符。单行纯文本控件拒绝换行,不自动修改其 multiLine 属性。
支持范围与预算
仅接受未签名、未启用保护的 Transitional DOCX。Strict 文档会被 SDK 转换且验证器不支持其原始模型,因此在 XML 检查阶段拒绝;宏文档和模板文档按 SDK 文档类型拒绝。不请求外部关系的内容,也不执行文档中的代码。页脚、页眉之外的其他故事部件不遍历;文字框内部控件不作为受支持目标。
输入编码上限与工作副本/输出上限不同,工作副本必须先容纳完整输入。展开字节预算覆盖所有 ZIP 条目,单 XML 字符预算同时覆盖内容类型标记为 XML 的非常规扩展名部件。输出重新检查这些预算,避免压缩率较高的长文本绕过编码上限。替换字符预算按每个实际控件累加,重复字段会再次计费。
PDF 与 Word 共用内部 BoundedBuffer,在同步写入、异步写入和 SetLength 前限制容量增长。SDK、ZIP 内部缓存和 DOM 对象仍有额外内存开销,编码与展开预算不等于进程内存硬配额;大文档并发需由宿主限制。
实际解压读取用 1 + min(bufferSize - 1, remaining) 计算探测长度,避免正数 long.MaxValue 预算加一溢出为零长度读取。读取后先比较剩余额度,再累加实际字节数;声明大小只是预检,不能替代读取。
异常与验证证据
参数及同一流错误抛出 ArgumentException,缺少字段抛出 KeyNotFoundException,超限及 SDK 结构错误抛出 InvalidDataException,不支持的控件或文档契约抛出 NotSupportedException。XML 语法/字符错误和 SDK 包打开错误保留各自异常;取消与目标写入错误直接传播,不返回成功结果。
测试构造真实 DOCX 并重新用 SDK 验证输出,覆盖跨 run 字段、中文和 emoji、XML 特殊字符、段落格式、正文/表格/页眉/页脚、非目标自定义 XML、重复和缺失 Tag、空值及大小写。边界包括输入和输出大小精确命中/少一字节、重复文本累计预算、替换后的展开大小、非常规扩展名 XML、损坏 ZIP/DEFLATE、重复条目、DTD、Strict/宏/模板/签名/保护、非寻址短读、读取和复制取消、写入失败与流所有权。
两个容易误判的测试边界也保留:SDK 允许没有正文的空文档,其填充数量为零;不成对代理项在测试方法运行时构造,避免测试框架传输参数时先替换为合法的 U+FFFD。SDK 验证确认结构和约束,不证明 Word 的分页、字体呈现或最终视觉布局。