独立 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 能力提供底层支撑。
.docx 本质上是 Office Open XML(OOXML) 规范下的一组 XML 文件,打包在 ZIP 容器里。Python 生态有成熟的 python-docx;在 C++/嵌入式/桌面 DLL 场景,需要:
sdtech_docx_*),供 Electron/Koffi、Backend 会话层调用sdtech_docx 2.0 从 1.x 的「扁平 Block 列表」演进为 OPC + OXML + 对象模型 三层,与 python-docx 的分层一一对应。详细模块映射见库内 Libs/sdtech_docx/Docs/python-docx-port-map.md。
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)。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 在 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 配置)。
磁盘 .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)。
Package::createBlank() 生成最小合法包:
[Content_Types].xml 注册 word/document.xml_rels/.rels → rId1 指向主文档word/document.xml 含空 w:body 与 w:sectPrword/_rels/document.xml.rels 为空关系表随后同样经 DocumentLoader 解析到内存模型(空 body)。
应用层调用 add_paragraph / add_heading 等 → 修改 Document::body_ → Document::save():
DocumentWriter::saveBody(body_) 将内存模型序列化为 word/document.xml 的 XML 字符串mainDocumentPart 的 blobPackage::save(path) 将所有 Part(含未改动的 media、styles 等)重新打包为 ZIP要点:Writer 只重写已建模的正文 XML;其他 Part 若打开时已加载,则 原 bytes 保留,实现 round-trip 保真。
Dll/Src/opc/Part.h 中每个 Part 包含:
partName:ZIP 内路径(如 word/document.xml)contentType:OOXML MIMEblob:原始字节(文本 XML 或二进制图片)relationships:Outgoing 关系列表(也可由独立 .rels Part 承载)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
不依赖系统 libzip:自研 MinimalZip + miniz tinfl 处理 method 8(deflate)。支持:
Part::blob测试 fixture deflate_sample.docx 专门覆盖 deflate 压缩条目,GTest OpenDeflateDocx 验证解压与解析。
Dll/Src/opc/ContentTypes.cpp 维护 Default/Override;Package::save 前根据现有 Part 集合刷新 [Content_Types].xml,避免新增 media 后类型缺失。
Word ML 元素带 w: 前缀。解析时使用 pugixml,并通过 local-name() XPath 或 isElement(n, "p") 兼容带前缀节点(见 DocumentLoader 内工具函数)。
w:pPr / w:rPr / w:tblPr / w:tcPr 读取对齐、缩进、间距、边框、底纹等到 ParagraphFormat、RunFormat 等结构体(twips 单位保留,Bridge 层再换算 px)。word/styles.xml,建立 styleId → 段落/Run 默认格式;加载时 merge 到每个段落/Run,使预览接近 Word 渲染。numbering.xml,为列表段落生成 numLabel 展示用标签。Heading 识别:style 为 Heading1…Heading9 或 OOXML 等价 id 时设置 headingLevel,Bridge 输出 kind: "heading"。
Document
└─ body_: vector<BlockItem>
├─ ParagraphModel
│ ├─ runs_: vector<RunModel>
│ ├─ format: ParagraphFormat
│ └─ style / headingLevel / numLabel
└─ TableModel
└─ rows[][] → CellModel → paragraphs[](单元格内可嵌套段落)
表格支持 gridSpan / vMerge 读取(合并单元格元数据),嵌套表在 loader 中递归 parseTable。
二者同在 Dll/Src/model/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 一致)。
Dll/Src/parts/ImagePart.cpp 中 addInlinePicture:
word/media/<filename> Partdocument.xml.rels 追加 Image 关系InlineImage 的 RunModel 追加到段落C API sdtech_docx_add_picture 创建空段落后调用上述逻辑。
Bridge 将 内存模型 转为 OfficeExcel UI 可消费的 JSON(Dll/Src/bridge/BridgeJson.cpp)。
get_preview)输出 { "blocks": [ ... ], "pageWidth": ... }:
| block.kind | 含义 |
|---|---|
paragraph |
正文段,含 text、runs(粗体/斜体/字体) |
heading |
标题,含 level |
table |
表格行列、单元格文本、边框/底纹等 |
picture |
内联图,含 base64 或引用信息 |
Twips → 像素:96/1440 换算,用于前端纸张宽度与行高。
get_structure)输出 { "nodes": [ { "id", "label", "type", "indent", "props" } ] } 树形结构,供 OfficeExcel 左侧 结构检查区 展示 Document → Paragraph/Table → …
get_open_consistency)打开文件后对比 磁盘 Part 与 内存模型 是否一致,例如:
Heading1 vs Heading 1)GTest OpenConsistencyStyleNames 验证自写文档 ok: true。
公开头文件:Libs/sdtech_docx/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*(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 / jsonGetInt(Dll/Src/CApi/sdtech_docx_api.cpp),避免强依赖完整 JSON 库,减小 DLL 体积。
DocumentWriter 重写 word/document.xml(及图片新增时的 media/rels)| 手段 | 说明 |
|---|---|
| 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。
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
docx_create → add_paragraph → docx_save;日志记录 JSON 参数与返回码docx_open → get_preview + get_open_consistency 只读展示若需跟踪实现细节,建议阅读顺序: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++ 实现