技术文章

从 Delphi 写出 ISO 29500 Strict 版 XLSX 文件

HotXLS 只需在保存前设置一个属性 StrictOOXML,即可从 Delphi 和 C++Builder 写出符合 ISO/IEC 29500 Strict 规范的 Open XML 工作簿。包内每个部件,从 xl/workbook.xml 到关系文件和内容类型,都会改用 Strict 版的 purl.oclc.org 词汇表,而不是过渡版的 schemas.openxmlformats.org 词汇表,Strict 不允许的特性会在保存时被显式抛出异常拒绝,而不是照样写出去

大多数开发者是通过采购文档接触到这项要求的。一些司法辖区的公共部门招标要求提交 Open XML 的 ISO 标准化形式,而不是 Office 默认写出的过渡形式,一个强制要求 ISO 29500 Strict 的档案系统会拒绝一份普通的 .xlsx,哪怕 Excel 能完美打开它。过渡版命名空间的存在是为了兼容遗留二进制格式的行为;Strict 版才是真正意义上的标准

Strict 和 Transitional 到底差在哪里?

可见的差异在于词汇表。一个 Strict 工作簿部件把 http://purl.oclc.org/ooxml/spreadsheetml/main 声明为根命名空间,把 http://purl.oclc.org/ooxml/officeDocument/relationships 用于关系引用,包内任何地方都不能残留过渡版命名空间。关系类型也随之改变,因此根关系部件命名为 .../ooxml/officeDocument/relationships/officeDocument,而不是熟悉的 openxmlformats 对应形式,扩展属性类型也变成驼峰式的 extendedProperties

不可见的差异在于范围。Strict 有意省略了过渡版本架构中那些只是为了兼容遗留二进制文件才存在的部分,以及 Office 后来加上的厂商扩展。这正是为什么这项转换不能靠字符串搜索替换来完成:有些特性根本没有 Strict 拼法,必须完全不写

启用它

常规的编写代码不需要改动。照常构建工作簿,设置标志,然后保存:

var
  Wb: TXLSXWorkbook;
  Sh: TXLSXWorksheet;
begin
  Wb := TXLSXWorkbook.Create;
  try
    Sh := Wb.Sheets.Add('Data');
    Sh.Cells[1, 1].Value := 'Product';
    Sh.Cells[1, 2].Value := 'Amount';
    Sh.Cells[2, 1].Value := 'Widget';
    Sh.Cells[2, 2].Value := 17;
    Sh.Cells[3, 2].Formula := '=SUM(B2:B2)';

    Wb.StrictOOXML := True;          // ISO/IEC 29500 Strict 输出
    if Wb.SaveAs('archive-copy.xlsx') <> 1 then
      raise Exception.Create('strict save failed');
  finally
    Wb.Free;
  end;
end;

该标志会在每次保存操作开始时重置,并从工作簿属性重新赋值,因此一次保存中发生的异常不会把 Strict 模式泄漏到下一次保存里。这个细节在一个工作簿对象要为多个导出请求提供服务的服务端进程中很重要

为什么一次 Strict 保存会被拒绝?

有四类特性是微软扩展,没有对应的 ISO 29500 Strict 等价形式,HotXLS 会在保存时抛出异常,而不是生成一份声称符合 Strict 规范、实际上并不符合的包:

// Strict 输出无法嵌入 VBA 工程
//   -> 请把启用宏的工作簿另存为过渡版 .xlsm
// Strict 输出无法携带表单控件
//   -> 按钮、复选框、下拉框及其 ctrlProps
// Strict 输出无法携带线程化批注
//   -> 现代的 persons/threads 模型,而非经典批注
// Strict 输出无法携带动态数组元数据
//   -> 通过元数据部件记录的溢出区域

在这里大声报错才是正确的取舍。悄悄丢弃的 VBA 工程会把一份能用的工作簿变成一份仍能打开、实则已损坏的工作簿,而这个失败的报告要等几周后才会从用户那里传回来。异常会在调用代码还清楚自己在导出什么的时候,直接点明是哪个特性、需要改哪个属性。过渡路径下宏和外部链接的保留方式在 保留 VBA 工程和外部链接 中有介绍

有两类扩展特性的处理方式不同,值得了解原因。数据条、迷你图之类的特性位于 x14xm 词汇表中,图片的 SVG 变体则位于 c15 中。这些都是自描述命名空间的扩展列表内容,通用的电子表格解析器能够容忍它们,而且没有对应的 ISO 等价形式可以转换。HotXLS 会保留它们,而不是把用户内容直接丢弃。如果你流水线里的校验器对扩展和命名空间同样严格,请在导出前先从源工作簿中移除这些特性

转换必须触及通常没人重写的部件

Strict 输出中真正有意思的工程问题不在工作表 XML 上,而在于那些快速写入器宁愿原样复制的部件。HotXLS 通过直接复制原始压缩字节来保留主题、连接、外部链接、图表和数据透视表数据块,这对保真度来说完全正确,对 Strict 输出来说却完全错误,因为复制过来的字节携带着过渡版命名空间

StrictOOXML 模式下,这五条保留路径会切换为重建或转换式重放,绕过原本的字节直拷快速路径。所有 XML 都会经过同一个转换例程,该例程以双引号包裹的属性值为锚点,这样一来单元格内看起来像 URI 的字符串就绝不会被意外改写。单元格文本中出现的相同 URI 会在 XML 中以实体形式转义,因此锚点式替换不会误伤它。流式写入器会先转换其骨架,再从 sheetData 处切分,因为行数据块本身根本不含任何词汇表 URI。保留路径的相关机制在 无损往返保留主题、扩展列表和 calcChain 中有介绍

读取 Excel 保存为 Strict 格式的文件

输出只是故事的一半。Excel 提供了"严格 Open XML 电子表格"这一保存选项,以这种方式生成的文件必须能被正确打开。HotXLS 会在包内每一处解析关系的地方,包括根、外部链接、工作表、绘图和数据透视表,对关系类型做归一化处理,让 Strict 版关系类型对应到与其过渡版对应类型相同的内部常量

读取端相应的机制是命名空间前缀归一化,它允许任意前缀以及两种词汇表都能解析到同一份规范名称表。这项工作对普通文件和 Strict 文件都有好处,因为第三方生成器绑定前缀相当随意,这与 XLSX 包中的 OPC 关系解析 中描述的是同一套机制

发布 Strict 输出前的一份简短检查清单

要用包本身来验证,不能只靠 Excel。两种形式 Excel 都能顺利打开,因此打开成功并不能证明任何合规性。解压结果,确认 xl/workbook.xml 声明的是 purl 命名空间,确认没有任何部件包含 schemas.openxmlformats.org/spreadsheetml,并确认 _rels/.relsxl/_rels/workbook.xml.rels 中的关系类型都是 Strict 形式

然后通过 HotXLS 重新打开文件,把数值、公式、格式和超链接与源文件逐一比对。回读测试是证明转换没有损坏内容的唯一低成本方式,同时它也会顺带验证读取端的归一化逻辑。如果你的工作簿带有图表,也要一并检查,因为图表部件正是 Strict 模式下切换为重建路径的保留部件之一

Strict 输出、宽容的读取和无损保留都属于适用于 Delphi 和 C++Builder 的同一个 OOXML 引擎;完整功能列表见 HotXLS Delphi 电子表格组件页面