技术文章

用 HotXLS 从 Excel 图片生成标记 PDF 图形元素

当 HotXLS 在启用自动标记的情况下把工作表导出为 PDF 时,携带替代文本的工作表图片现在会输出为独立的 /Figure 结构元素,带 Unicode /Alt 条目、稠密的页内标记内容标识符和精确的父树条目。没有替代文本的图片仍是装饰性 artifact,图表同样保持 artifact。这个精确的范围很重要:它让承载信息的图片能被屏幕阅读器触达,而它并不等于完整的 PDF/UA 一致性

它背后的机制比功能描述更有意思,因为其中两个属于那种细节:悄悄产出一个结构合法、但结构指向错误内容的 PDF

什么才算承载信息的图片?

只有一个非空的 AltTextTXLSXImage.AltText 属性与图片非视觉属性的 OOXML descr 属性往返对应——那正是 Excel 存放用户在替代文本窗格里输入的文字的地方。这是文件中唯一的信号,表明作者认为该图片承载信息而非装饰,因此也是导出器唯一信任的信号

两个近似项被刻意拒收。与描述分开存储的标题字段不是替代品:标题是对象的名字,不是它的文字等价物;把它升格为 /Alt 会产出一个通过自动化检查、却向屏幕阅读器播报"Picture 3"的文档。空描述也不是要用占位符填补的缺口;它意味着图片保持 artifact——对徽标或分隔线,这正是正确结果。图表目前也仍是 artifact,因为图表的文字等价物是它的数据,从数据系列合成一段描述是发明而不是提取

非空 AltText 的图片导出为带自己 MCID 的 PDF Figure 结构元素;空描述与图表保持 artifact
只有 AltText 中的作者描述才表示承载信息的图片;仅凭标题永远不会成为替代文本
uses
  lxHandleX, lxPDF;

var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
  Exporter: TXLSPDFExport;
  I: Integer;
begin
  Book := TXLSXWorkbook.Create;
  try
    Book.Open('regional-review.xlsx');
    Sheet := Book.Sheets.ByPos[0];

    // 导出前先审计:没有描述的图片
    // 将被导出为装饰性 artifact
    for I := 0 to Sheet.Images.Count - 1 do
      if Sheet.Images[I].AltText = '' then
        Sheet.Images[I].AltText := DescribeImage(Sheet.Images[I].Name);

    Exporter := TXLSPDFExport.Create;
    try
      Exporter.TagMode := xlsPdfTagsAutomatic;
      Exporter.DocumentLanguage := 'en-US';
      Exporter.SaveAsPDF(Sheet, 'regional-review.pdf');
    finally
      Exporter.Free;
    end;
  finally
    Book.Free;
  end;
end;

为什么页面必须只有一个 MCID 分配器?

因为父树是一个以标记内容标识符为索引的数组,两个分配器会产出两个争抢同一槽位的条目。标记 PDF 把内容与结构双向连接。在内容一侧,页面内容流的一段被 BDCEMC 操作符包裹,携带在该页内唯一的 /MCID 编号。在结构一侧,页面字典携带 /StructParents 键,指向文档 /ParentTree 的一行;那一行是数组,索引 n 处的元素是拥有 MCID n 的结构元素

工作表页面上有表格单元格,现在还有图形。如果单元格标记器从零起数自己的标识符,图形标记器也从零起数,第一个图形就会抢走第一个单元格已拥有的槽位。结果文件没有任何畸形到会被解析器拒绝的地方:结构树完好,标记内容配平,验证器看到一个带父树的文档。屏幕阅读器得到的是:把表格单元格当图片播报,或用某个单元格的文字播报一张图片。因此导出器从两个标记器共享的一个页级计数器分配,且只在页面对象号已知之后才冻结页面记录——父树行在被引用页面获得身份之前写不出来

独立的单元格与图形标记器在父树零号槽位相撞;一个页级 MCID 计数器让每个标记都映射到唯一拥有者
相撞的文件仍能通过结构验证器;错的只有屏幕阅读器的播报

Figure 必须包住整个可见实例

天真的做法是包住调用图像 XObject 的 Do 操作符,因为画图的就是它。这不够。工作表图片常常带背后的阴影与周围的裁剪路径一起绘制,那些标记是可见对象的一部分。留在 /Figure 作用域之外,它们就成了未标记内容——正是结构审计会标记的那种状态

因此标记内容作用域在阴影之前打开,在图像绘制之后关闭,把裁剪也一并覆盖。共享在共享正确的地方得以保留:显示同一图片载荷的两个单元格仍引用同一个图像 XObject,因为那是资源级优化,与语义无关。每个可见实例得到的是自己的 MCID 与自己的结构元素,因为同一个徽标在不同位置出现两次,就是读者遇到的两个东西。图片摆放与定位这些对象的 EMU 几何见图片几何一文

BDC 标记在阴影与裁剪之前打开 Figure 作用域,EMC 在 Do 图像绘制之后关闭,覆盖整个可见实例
只包住图像操作符会让阴影与裁剪沦为未标记内容;单元格之间的资源共享得以保留

工作表页面上的阅读顺序

阅读顺序是导出器必须做的决定,因为电子表格不像文档那样有编排好的流。采用的规则稳定且好解释:每页先表格,然后按绘制顺序排列图形。于是读者先听到页面的表格内容,再听到它的图片,而不是图片按绘图对象碰巧在文件里占据的位置被穿插进来

这个顺序按页而非按文档,这在分页成几十页的工作簿上很重要:每页的结构分支自包含,读者在页面之间移动时不会跳回更早的表格。如果你需要控制工作表一开始如何分页,页面设置与打印区域的相互作用见保护与页面设置一文

它证明了什么,又没证明什么

它证明的是:承载信息的图片带着作者提供的描述到达辅助技术,且内容到结构的映射是正确的,而不仅是存在。它没有让输出符合 PDF/UA,那样描述将是一个实现支撑不了的声明:图表仍是 artifact,完整的一致性声明需要对每种结构类型、每个字体以及文档元数据整体做审计

如果你的需求是归档或一致性 profile 而非无障碍改进,那是另一种导出配置与另一套检查,见PDF/A 归档导出一文。两者可以组合,但它们要应付的审计者不同

给报表管线一条务实建议:在生成工作簿的那一点上审计替代文本,而不是在导出时。生成器知道每张图表图片或嵌入示意图代表什么,可以把真正的描述写进 AltText;导出时的检查只能告诉你描述缺失。HotXLS 在 Delphi 与 C++Builder 中原生读写 XLS、XLSX、ODS 与 CSV,不依赖 Excel,其导出配置选项列在 HotXLS Delphi spreadsheet component 产品页