sdtech_docx 原理与实现详解
独立 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 场景,需要:
- 无 Python 运行时 的本地读写能力
- 稳定 C ABI(
sdtech_docx_*),供 Electron/Koffi、Backend 会话层调用 - 打开—修改—保存 时对未建模 Part 的 保真(不丢脚注、主题、自定义 XML 等)
- JSON 桥接:结构树、UI 预览、读写一致性检查,与 OfficeExcel 前端对齐
sdtech_docx 2.0 从 1.x 的「扁平 Block 列表」演进为 OPC + OXML + 对象模型 三层,与 python-docx 的分层一一对应。详细模块映射见库内 python-docx-port-map.md。
纯 C++ 实现的优势
Word 文档能力常见实现路径包括:嵌入 Python + python-docx、COM 调用本机 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.cpp、miniz_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 线程模型复杂 |
更具体地说:
- 内存数据面:
Package用unordered_map<partName, Part>持有 blob,打开后随机 Part 访问是内存级;未建模 Part 不参与 XML 解析,打开大模板时仍可按需只解析document.xml主路径(loader 聚焦 body)。 - 无跨进程拷贝:图片、样式等二进制 Part 始终在同一进程地址空间;COM 方案往往要在 Office 与宿主之间 marshalling。
- 可预测延迟:批量「创建 → 写段 → 保存」在 GTest / 验证台里是可重复的毫秒级操作,不受「Word 是否在后台弹对话框」「Python 首次 import 慢」影响。
- 与 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 策略一致)。
三、整体架构
| 层级 | 目录 | 职责 |
|---|---|---|
| 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 是正文块级单元,类型为 Paragraph 或 Table(见 DocumentTypes.h)。段落内再拆 Run(RunModel):文本、粗体/斜体/字体,或 内联图片(InlineImage + relationship id)。
4.2 新建文档
Package::createBlank() 生成最小合法包:
[Content_Types].xml注册word/document.xml_rels/.rels→rId1指向主文档word/document.xml含空w:body与w:sectPrword/_rels/document.xml.rels为空关系表
随后同样经 DocumentLoader 解析到内存模型(空 body)。
4.3 写入与保存
应用层调用 add_paragraph / add_heading 等 → 修改 Document::body_ → Document::save():
DocumentWriter::saveBody(body_)将内存模型序列化为word/document.xml的 XML 字符串- 写回
mainDocumentPart的 blob Package::save(path)将所有 Part(含未改动的 media、styles 等)重新打包为 ZIP
要点:Writer 只重写已建模的正文 XML;其他 Part 若打开时已加载,则 原 bytes 保留,实现 round-trip 保真。
五、OPC 层实现要点
5.1 Part 与 Relationship
Part.h 中每个 Part 包含:
partName:ZIP 内路径(如word/document.xml)contentType:OOXML MIMEblob:原始字节(文本 XML 或二进制图片)relationships:Outgoing 关系列表(也可由独立.relsPart 承载)
Relationship 含 rId、Type(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)。支持:
- 打开时解压各 entry 到
Part::blob - 保存时按 Central Directory 规则写回
测试 fixture deflate_sample.docx 专门覆盖 deflate 压缩条目,GTest OpenDeflateDocx 验证解压与解析。
5.3 ContentTypes
ContentTypes 维护 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
- FormatReader:从
w:pPr/w:rPr/w:tblPr/w:tcPr读取对齐、缩进、间距、边框、底纹等到ParagraphFormat、RunFormat等结构体(twips 单位保留,Bridge 层再换算 px)。 - StylesResolver:解析
word/styles.xml,建立 styleId → 段落/Run 默认格式;加载时 merge 到每个段落/Run,使预览接近 Word 渲染。 - NumberingResolver:解析
numbering.xml,为列表段落生成numLabel展示用标签。
Heading 识别:style 为 Heading1…Heading9 或 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
二者同在 DocumentLoader.cpp:
| 类 | 方向 | 作用 |
|---|---|---|
DocumentLoader |
XML → Model | parseParagraph / parseRun / parseTable |
DocumentWriter |
Model → XML | paragraphXml / runXml / tableXml → buildDocumentXml |
Writer 在 Run 级输出 w:r/w:t;图片 Run 输出 w:drawing + a:blip 的 embed rId(需与 ImagePart 写入的 relationship 一致)。
7.3 内联图片
ImagePart.cpp 中 addInlinePicture:
- 读本地文件 →
word/media/<filename>Part - 在
document.xml.rels追加 Image 关系 - 构造带
InlineImage的RunModel追加到段落
C API sdtech_docx_add_picture 创建空段落后调用上述逻辑。
八、Bridge 层:JSON 协议
Bridge 将 内存模型 转为 OfficeExcel UI 可消费的 JSON(BridgeJson.cpp)。
8.1 Preview(get_preview)
输出 { "blocks": [ ... ], "pageWidth": ... }:
| block.kind | 含义 |
|---|---|
paragraph |
正文段,含 text、runs(粗体/斜体/字体) |
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 与 内存模型 是否一致,例如:
- 样式名规范化(
Heading1vsHeading 1) - 段落/表格数量
- 关键 XML 节点是否可 round-trip
GTest OpenConsistencyStyleNames 验证自写文档 ok: true。
九、C ABI 设计
公开头文件:include/sdtech_docx_c.h
| 类别 | API |
|---|---|
| 生命周期 | init / cleanup / document_create / open / save / destroy |
| 写入 | add_paragraph / add_heading / add_table / add_picture(JSON 字符串入参) |
| 读出 | get_structure / get_preview / get_open_consistency(堆分配 JSON,free_string 释放) |
| 错误 | 返回 int32_t 错误码 + last_error() 线程局部消息 |
句柄 void* 在 C++ 侧实为 DocxDocument*(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 / jsonGetInt(sdtech_docx_api.cpp),避免强依赖完整 JSON 库,减小 DLL 体积。
十、保真策略与能力边界
10.1 保真
- 打开 Package 时 全量 Part 进内存 map
- 仅
DocumentWriter重写word/document.xml(及图片新增时的 media/rels) - Footnotes / Endnotes:保留 Part,不解析(与 python-docx 相同)
- 未识别 Part:不删除、不改 content
10.2 当前 MVP 能力(2.0)
- 段落、标题、Run 级粗体/斜体/字体
- 表格创建与读取、单元格合并信息、嵌套表
- 内联 PNG/JPEG 图片
- 样式/编号解析用于预览增强
- 完整 OPC 打包/解包(含 deflate)
10.3 尚未覆盖(可按 python-docx 映射扩展)
- 页眉页脚编辑、分节符复杂属性
- 修订/批注写入、域代码、公式
- 浮动图片、文本框、chart
- 加密 document(Agile/Standard,xlsx 侧 sdtech_xlsx 已实现可参考)
十一、测试与质量
| 手段 | 说明 |
|---|---|
| GTest | 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 中的位置
- 工作台:
docx_create→add_paragraph→docx_save;日志记录 JSON 参数与返回码 - 打开文件:
docx_open→get_preview+get_open_consistency只读展示 - 与 sdtech_xlsx 并列:同一壳层、同一 Bridge 模式(JSON 预览 + 结构 + 一致性)
十三、设计小结
- 分层清晰:OPC 管容器,OXML 管语法,Model 管语义,Bridge 管 UI 协议,C API 管跨语言边界。
- 对标 python-docx:降低从 Python 原型迁移到 C++ 的心智成本;port-map 文档可持续跟踪差异。
- 保真优先:企业场景常见「只改正文、保留模板 Part」;全 Part 保留是正确默认。
- JSON 双工:写入 JSON 简化 C ABI;读出 JSON 服务 Electron 预览,避免在 JS 侧重复 OOXML 解析。
- 纯 C++ 交付:无 Python/Office 等额外运行时;prebuilt 即用,native 路径保证低延迟与同进程集成。
若需跟踪实现细节,建议阅读顺序:Package.cpp → DocumentLoader.cpp → BridgeJson.cpp → sdtech_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++ 实现
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)