sdtech_docx 原理与实现详解

独立 C++ Word (.docx) 库 sdtech_docx 2.0 的设计与实现说明。架构对标 python-docx,为 OfficeExcel 提供 Word 读写、JSON 预览与 OPC 保真能力。

独立 C++ Word (.docx) 库 sdtech_docx 2.0 的设计与实现说明。架构对标 python-docx 1.2,为 OfficeExcel 功能介绍 工程中的 Word 能力提供底层支撑。


一、为什么要做 sdtech_docx

.docx 本质上是 Office Open XML(OOXML) 规范下的一组 XML 文件,打包在 ZIP 容器里。Python 生态有成熟的 python-docx;在 C++/嵌入式/桌面 DLL 场景,需要:

sdtech_docx 2.0 从 1.x 的「扁平 Block 列表」演进为 OPC + OXML + 对象模型 三层,与 python-docx 的分层一一对应。详细模块映射见库内 Libs/sdtech_docx/Docs/python-docx-port-map.md

纯 C++ 实现的优势

Word 文档能力常见实现路径包括:嵌入 Python + python-docxCOM 调用本机 Office跨语言绑定 Java POI 等。sdtech_docx 选择 纯 C++ 直读 OOXML,在 OfficeExcel 及同类桌面/工业软件场景下更匹配。

无额外依赖优势

「无额外依赖」指:终端用户与集成方不必再安装 Python、Word、Java 运行时或独立文档服务,产品携带的 DLL 即可工作。

项目 sdtech_docx Python + python-docx COM + Word
必须预装 sdtech_docx.dll(+ 工程已有的 SDTech 基础库) Python 3.x、pip 包、常需 venv Microsoft Word 桌面版
ZIP/XML 内置 MinimalZip + 源码编入 pugixml/miniz C 扩展或 lxml 等二级轮子 不涉及(Office 内部处理)
集成方式 LoadLibrary / 链接导入,零配置路径 配置 PYTHONHOME、脚本路径、编码 注册 COM、处理 apartment、版本差异
离线工控机 拷贝 prebuilt 即用 往往无法装完整 Python 栈 通常禁止或未装 Office
许可证与审计 单一 C++ 产物,依赖清单短 解释器 + 多个 PyPI 包 Office 批量授权、自动化策略

OfficeExcel 协作者只需 npm run sync-docx 同步 prebuilt/docx/x64-windows/无需本机安装 Python 或 Word 即可编译 Backend、跑验证台。对客户现场而言,安装包内多一个 DLL,而不是多一套运行时环境——部署、杀毒白名单、合规问卷都更简单。

库内第三方代码以 源码 vendoring 方式纳入(pugixml.cppminiz_tinfl.c),不依赖系统 libzip、不依赖 MSXML 以外的外部 XML 库,避免「用户机器缺 VC 运行库 / 缺某个 DLL」之外的第二套包管理问题

性能优势

性能收益来自 全链路 native、同进程、无解释器、无 COM 跨进程

环节 C++ 路径 典型慢路径(对比)
冷启动 Electron 已加载 backend.dll 后,sdtech_docx_* 随调随用 每次 python script.py 或启动 Word 进程:百毫秒~秒级
打开 docx MinimalZip 解压 → Part 进内存 map → pugixml 解析正文 Python:解释器 + 对象分配 + lxml/python-docx 多层包装
写入保存 Model → XML 字符串 → 写回 Part blob → 一次 ZIP 打包 COM:RPC 到 Word 进程,大文件更明显
UI 联调 Backend 同进程返回 JSON,Koffi 一次 FFI 子进程需管道/文件中转 JSON,多一次拷贝与序列化
并发模型 C API 层 mutex + 每文档独立句柄,无 GIL CPython GIL 限制 CPU 并行;COM STA 线程模型复杂

更具体地说:

  1. 内存数据面Packageunordered_map<partName, Part> 持有 blob,打开后随机 Part 访问是内存级;未建模 Part 不参与 XML 解析,打开大模板时仍可按需只解析 document.xml 主路径(loader 聚焦 body)。
  2. 无跨进程拷贝:图片、样式等二进制 Part 始终在同一进程地址空间;COM 方案往往要在 Office 与宿主之间 marshalling。
  3. 可预测延迟:批量「创建 → 写段 → 保存」在 GTest / 验证台里是可重复的毫秒级操作,不受「Word 是否在后台弹对话框」「Python 首次 import 慢」影响。
  4. 与 xlsx 栈一致sdtech_xlsx 同样 OPC + native,Word/Excel 双库并行加载时 共享同一套部署与性能模型,无需为 Excel 走 C++、为 Word 走 Python 的双栈损耗。

性能不是相对 python-docx 做微基准「赢多少 ms」——在桌面验证台场景下,稳定、无额外进程、无运行时冷启动本身就是体验与运维上的优势;在批量服务端生成 docx(若未来扩展)时,native 路径也更容易做内存与并发控制。

与其他方案的总览对比

对比维度 纯 C++(sdtech_docx) 常见替代方案
运行时依赖 仅 DLL + SDTech 基础库 Python 解释器 / 本机 Word
部署体积 单一 DLL,进 prebuilt Python 环境 + 依赖常数十 MB 以上
跨语言边界 稳定 C ABI 需再包一层;COM 限 Windows
行为可预期 只解析 OOXML COM 受版本、宏、弹窗影响
与 xlsx 栈统一 同架构 sdtech_xlsx Word/Excel 两套运行时

其余工程收益(发布、安全合规、可控演进、GTest + 黄金 JSON 调试闭环)仍成立;python-docx 继续作为 参考实现(port-map、黄金 dump),C++ 版负责 零额外运行时、native 性能 的可交付形态——这也是 OfficeExcel 将 docx/xlsx 都做成独立 DLL 的原因。


二、.docx 文件里有什么

一个典型 docx 在 ZIP 内包含:

路径 作用
[Content_Types].xml 各 Part 的 MIME 类型注册
_rels/.rels 包级关系,指向主文档 word/document.xml
word/document.xml 正文:w:body 下的段落 w:p、表格 w:tbl、分节 w:sectPr
word/_rels/document.xml.rels 文档级关系:图片、页眉页脚、样式等
word/styles.xml 命名样式与 docDefaults
word/numbering.xml 列表编号定义
word/media/* 内联图片二进制
其他 Part 页眉/页脚、comments、settings…

库的核心思路:能建模的走对象模型读写;不能建模的 Part 以原始 bytes 保留,保存时原样写回(与 python-docx 策略一致)。


三、整体架构

flowchart TB
  subgraph api [对外接口]
    CAPI["CApi sdtech_docx_c.h"]
    Bridge["bridge/ BridgeJson"]
  end
  subgraph model [对象模型 model/]
    Doc["Document"]
    Loader["DocumentLoader"]
    Writer["DocumentWriter"]
  end
  subgraph oxml [OXML oxml/]
    Pugixml["pugixml DOM"]
    Format["FormatReader StylesResolver NumberingResolver"]
  end
  subgraph opc [OPC opc/]
    Pkg["Package Part Relationship"]
    Zip["MinimalZip miniz"]
  end
  subgraph parts [Parts parts/]
    Img["ImagePart 内联图"]
  end
  CAPI --> Doc
  CAPI --> Bridge
  Bridge --> Doc
  Doc --> Loader
  Doc --> Writer
  Loader --> Pugixml
  Loader --> Format
  Writer --> Pkg
  Doc --> Pkg
  Pkg --> Zip
  Img --> Pkg
层级 目录 职责
OPC Dll/Src/opc/ ZIP 包、Part 字典、Relationship、ContentTypes
OXML Dll/Src/oxml/ WordprocessingML 元素解析辅助(local-name、命名空间)
Parts Dll/Src/parts/ 特定 Part 类型逻辑(如 ImagePart
Model Dll/Src/model/ Document、段落/Run/表格/单元格等领域对象
Bridge Dll/Src/bridge/ 预览/结构/一致性 JSON
C API Dll/Src/CApi/ C ABI 导出、JSON 入参解析

依赖:pugixml(XML DOM)、miniz(ZIP 读写的 deflate 支持,见 Impl/MinimalZip.cpp)、SDTech SDK(日志等,按工程 CMake 配置)。


四、核心数据流

4.1 打开文档

磁盘 .docx
  → MinimalZip 解压为 Part 名 → blob 映射
  → Package::wireRelationships() 解析 _rels
  → 定位 mainDocumentPart (word/document.xml)
  → DocumentLoader::load()
       ├─ StylesResolver / NumberingResolver 加载 styles.xml、numbering.xml
       ├─ 遍历 w:body 下 w:p / w:tbl
       └─ 填充 std::vector<BlockItem> body_
  → Document::syncFromLoader()
  → DocxDocument 持有 model + sourcePath

BlockItem 是正文块级单元,类型为 ParagraphTable(见 DocumentTypes.h)。段落内再拆 RunRunModel):文本、粗体/斜体/字体,或 内联图片InlineImage + relationship id)。

4.2 新建文档

Package::createBlank() 生成最小合法包:

随后同样经 DocumentLoader 解析到内存模型(空 body)。

4.3 写入与保存

应用层调用 add_paragraph / add_heading 等 → 修改 Document::body_Document::save()

  1. DocumentWriter::saveBody(body_) 将内存模型序列化为 word/document.xml 的 XML 字符串
  2. 写回 mainDocumentPart 的 blob
  3. Package::save(path) 将所有 Part(含未改动的 media、styles 等)重新打包为 ZIP

要点:Writer 只重写已建模的正文 XML;其他 Part 若打开时已加载,则 原 bytes 保留,实现 round-trip 保真。


五、OPC 层实现要点

5.1 Part 与 Relationship

Dll/Src/opc/Part.h 中每个 Part 包含:

Relationship 含 rIdType(Office Document / Image / Styles…)、Target(相对路径)。图片引用链:

word/document.xml 内 w:drawing → r:embed="rId5"
  → document.xml.rels 中 rId5 → Target="media/image1.png"
  → Part word/media/image1.png

5.2 MinimalZip

不依赖系统 libzip:自研 MinimalZip + miniz tinfl 处理 method 8(deflate)。支持:

测试 fixture deflate_sample.docx 专门覆盖 deflate 压缩条目,GTest OpenDeflateDocx 验证解压与解析。

5.3 ContentTypes

Dll/Src/opc/ContentTypes.cpp 维护 Default/Override;Package::save 前根据现有 Part 集合刷新 [Content_Types].xml,避免新增 media 后类型缺失。


六、OXML 与格式解析

6.1 命名空间与 local-name

Word ML 元素带 w: 前缀。解析时使用 pugixml,并通过 local-name() XPath 或 isElement(n, "p") 兼容带前缀节点(见 DocumentLoader 内工具函数)。

6.2 FormatReader 与 StylesResolver

Heading 识别:styleHeading1Heading9 或 OOXML 等价 id 时设置 headingLevel,Bridge 输出 kind: "heading"


七、对象模型

7.1 层次结构

Document
  └─ body_: vector<BlockItem>
       ├─ ParagraphModel
       │    ├─ runs_: vector<RunModel>
       │    ├─ format: ParagraphFormat
       │    └─ style / headingLevel / numLabel
       └─ TableModel
            └─ rows[][] → CellModel → paragraphs[](单元格内可嵌套段落)

表格支持 gridSpan / vMerge 读取(合并单元格元数据),嵌套表在 loader 中递归 parseTable

7.2 DocumentLoader / DocumentWriter

二者同在 Dll/Src/model/DocumentLoader.cpp

方向 作用
DocumentLoader XML → Model parseParagraph / parseRun / parseTable
DocumentWriter Model → XML paragraphXml / runXml / tableXmlbuildDocumentXml

Writer 在 Run 级输出 w:r/w:t;图片 Run 输出 w:drawing + a:blip 的 embed rId(需与 ImagePart 写入的 relationship 一致)。

7.3 内联图片

Dll/Src/parts/ImagePart.cppaddInlinePicture

  1. 读本地文件 → word/media/<filename> Part
  2. document.xml.rels 追加 Image 关系
  3. 构造带 InlineImageRunModel 追加到段落

C API sdtech_docx_add_picture 创建空段落后调用上述逻辑。


八、Bridge 层:JSON 协议

Bridge 将 内存模型 转为 OfficeExcel UI 可消费的 JSON(Dll/Src/bridge/BridgeJson.cpp)。

8.1 Preview(get_preview

输出 { "blocks": [ ... ], "pageWidth": ... }

block.kind 含义
paragraph 正文段,含 textruns(粗体/斜体/字体)
heading 标题,含 level
table 表格行列、单元格文本、边框/底纹等
picture 内联图,含 base64 或引用信息

Twips → 像素:96/1440 换算,用于前端纸张宽度与行高。

8.2 Structure(get_structure

输出 { "nodes": [ { "id", "label", "type", "indent", "props" } ] } 树形结构,供 OfficeExcel 左侧 结构检查区 展示 Document → Paragraph/Table → …

8.3 Consistency(get_open_consistency

打开文件后对比 磁盘 Part内存模型 是否一致,例如:

GTest OpenConsistencyStyleNames 验证自写文档 ok: true


九、C ABI 设计

公开头文件:Libs/sdtech_docx/include/sdtech_docx_c.h

类别 API
生命周期 init / cleanup / document_create / open / save / destroy
写入 add_paragraph / add_heading / add_table / add_pictureJSON 字符串入参
读出 get_structure / get_preview / get_open_consistency堆分配 JSONfree_string 释放)
错误 返回 int32_t 错误码 + last_error() 线程局部消息

句柄 void* 在 C++ 侧实为 DocxDocument*Dll/Src/Impl/DocxDocument.h),OfficeExcel Backend 再映射为 doc_id 整数,不向 JS 暴露指针

JSON 入参示例:

{"text":"本段由 DLL 写入","bold":true,"font":"Microsoft YaHei"}
{"text":"第二章","level":2}
{"rows":3,"cols":4}
{"path":"chart.png"}

C API 层使用轻量 jsonGetString / jsonGetIntDll/Src/CApi/sdtech_docx_api.cpp),避免强依赖完整 JSON 库,减小 DLL 体积。


十、保真策略与能力边界

10.1 保真

10.2 当前 MVP 能力(2.0)

10.3 尚未覆盖(可按 python-docx 映射扩展)


十一、测试与质量

手段 说明
GTest Libs/sdtech_docx/Tests/DocxTest.cpp:创建/保存/打开、consistency、deflate fixture
黄金 JSON Python dump_document.py 与 C++ DocumentDumper 输出 diff(见 port-map 文档)
OfficeExcel 验证台 人工点击 API + 预览/结构树/调用日志

构建:

cd Libs/sdtech_docx
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --config Release
ctest -C Release

协作者通过 npm run sync-docx 获取 prebuilt/docx/x64-windows,无需本地编译库即可联调 Backend/Electron。


十二、在 OfficeExcel 中的位置

flowchart LR
  UI["OfficeExcel React"]
  BE["backend.dll sdtech_officeexcel_*"]
  DOCX["sdtech_docx.dll"]
  ZIP[".docx OPC"]
  UI -->|Koffi doc_id| BE
  BE -->|sdtech_docx_*| DOCX
  DOCX --> ZIP

十三、设计小结

  1. 分层清晰:OPC 管容器,OXML 管语法,Model 管语义,Bridge 管 UI 协议,C API 管跨语言边界。
  2. 对标 python-docx:降低从 Python 原型迁移到 C++ 的心智成本;port-map 文档可持续跟踪差异。
  3. 保真优先:企业场景常见「只改正文、保留模板 Part」;全 Part 保留是正确默认。
  4. JSON 双工:写入 JSON 简化 C ABI;读出 JSON 服务 Electron 预览,避免在 JS 侧重复 OOXML 解析。
  5. 纯 C++ 交付:无 Python/Office 等额外运行时;prebuilt 即用,native 路径保证低延迟与同进程集成。

若需跟踪实现细节,建议阅读顺序:Package.cppDocumentLoader.cppBridgeJson.cppsdtech_docx_api.cpp


附录:目录速查

Libs/sdtech_docx/
├── include/sdtech_docx_c.h      # C ABI
├── Dll/Src/
│   ├── opc/                       # Package Part Relationship
│   ├── oxml/                      # XML 工具
│   ├── model/                     # Document Loader Writer Types
│   ├── parts/                     # ImagePart
│   ├── bridge/BridgeJson.cpp      # Preview Structure Consistency
│   ├── CApi/sdtech_docx_api.cpp
│   └── Impl/MinimalZip.cpp        # ZIP
├── Docs/python-docx-port-map.md
└── Tests/DocxTest.cpp

版本:2.0.0 · 独立库 · Apache POI / python-docx 思路的 C++ 实现