Vue3 + OFD 电子签章预览:OFD 文字渲染全变黑?一次颜色丢失的完整排查与修复复盘

摘要 :在基于 Vue3 + @ycsx/ofdjs + pdfjs-dist 实现 PDF/OFD 双格式预览与电子签章组件时,PDF 一切正常,OFD 却把红标题、蓝链接、彩色正文全部渲染成黑色。本文复盘了从"怀疑数据源"到"定位 ofdjs 颜色解析机制"的完整排查路径,拆解出三个递进根因,并给出"预渲染归一化 + 渲染后 DOM 补色 + 直读 OFD 原始 zip XML 兜底"的三层修复方案。所有结论均来自真实代码,初中级前端也能照着复现。

关键词:Vue3 · OFD · ofdjs · 电子签章 · PDF 预览 · 文本颜色丢失 · 前端踩坑复盘


一、背景:一个"半边黑"的预览组件

最近在做一个合同/公文在线预览 + 电子签章 的前端组件,技术栈是 Vue3(script setup)+ @ycsx/ofdjs(OFD 渲染)+ pdfjs-dist(PDF 渲染)。需求不复杂:传入 src(URL 或 File),组件把文档画出来,再在预览层上叠加电子印章、骑缝章、批量章、贴图,最后把印章位置换算回文档的"磅值(pt)"交给后端签章接口。

PDF 一路顺风顺水。问题出在 OFD------PDF 能正常显示彩色文字,OFD 一渲染,所有文字全变成黑色:红色的文件标题变黑、蓝色的链接变黑、甚至原本带透明度的水印也糊成一团。

更诡异的是:用 WPS / 数科阅读器打开同一个 OFD,颜色明明好好的。说明数据没丢,是前端渲染环节把颜色弄丢了。

二、排查:别急着改代码,先画"丢色链路"

OFD 本质是"带版式信息的 zip 包",里面是一堆 XML(页面、文字对象、颜色、字体等)。@ycsx/ofdjs 的工作流是:parseOfdDocument 解析 zip → 生成内存对象树 → renderOfd 把每页渲染成 <div> 里的一堆 <svg>/<canvas>。

颜色信息可能在三个节点丢失,我逐一验证:

  1. 数据源 :OFD 文件本身是否带颜色?→ 用压缩软件打开,XML 里 FillColor、DrawParam 明明有值,排除。
  2. 解析层 :parseOfdDocument 出来的对象树,颜色字段对不对?→ 打印 ofdDocument,发现 DrawParam.FillColor 是个对象而不是字符串。
  3. 渲染层 :renderOfd 生成的 <svg><text> 的 fill 属性是什么?→ 全是 black 或 rgb(0,0,0)。

结论:ofdjs 在把颜色解析成 SVG 的 fill 时,丢色了。顺藤摸瓜,我找到了三个递进式根因。

三、根因一:DrawParam 的 FillColor 是"对象",ofdjs 直接渲染成黑色

ofdjs 内部的 parseColor 期望颜色是字符串形式的 @_Value(如 "255 0 0")。源码里有一行注释直接点破了问题:

js 复制代码
// ofdjs DrawParam.FillColor 需要字符串,对象会被 parseColor 弄成黑色
const flattenOfdDrawParamColors = (drawParamResObj) => {
  for (const param of Object.values(drawParamResObj)) {
    if (param.FillColor && typeof param.FillColor !== 'string') {
      param.FillColor = normalizeOfdColorValue(readOfdColorRaw(param.FillColor));
    }
    // StrokeColor 同理......
  }
};

同时,OFD 里颜色还可能挂在任意层级的 FillColor/StrokeColor 节点上,所以又递归地把整棵文档树里的颜色节点统一拍平成 { '@_Value': 归一化字符串 }:

js 复制代码
const normalizeOfdColors = (obj) => {
  if (!obj || typeof obj !== 'object') return;
  if (Array.isArray(obj)) return obj.forEach(normalizeOfdColors);
  for (const [key, value] of Object.entries(obj)) {
    if (!isOfdColorKey(key)) { normalizeOfdColors(value); continue; }
    // 把字符串 / 对象 / 数组 统一规整为字符串 @_Value
    obj[key] = { '@_Value': normalizeOfdColorValue(value) };
  }
};

归一化函数 normalizeOfdColorValue 是这个坑的"地基":OFD 颜色写法极其混乱------

  • RGB:"255 0 0"
  • 0~1 浮点 RGB:"1 0 0"(必须 ×255)
  • CMYK:"0 1 1 0"(要转 RGB)
  • 十六进制:"#FF0000" 或裸 hex "FF0000"

不把这些口径统一,parseColor 解析失败就默认黑色。修复动作 :在 renderOFD 里、调用 renderOfd 之前,先跑 normalizeOfdColors(ofdDocument) 和 flattenOfdDrawParamColors(ofdDocument.drawParamResObj)。

这一层做完,部分简单 OFD(纯 RGB、无调色板)颜色就回来了。但复杂文档还是黑------引出根因二。

四、根因二:渲染后的 SVG 文本仍丢色,得"渲染后补刀"

即便把颜色喂对了,ofdjs 在生成 <svg> 时,对带调色板索引(Index + ColorSpace)、带 Alpha 透明度、或结构嵌套较深 的文字,依然可能把 fill 落空成黑色。预渲染归一化救不了它------因为渲染动作已经把错误结果画出来了。

对策是渲染后 DOM 后处理(post-process) :先自己建一张"文字内容 → 正确颜色"的映射表,再遍历每页 svg,把黑掉的 <text>/<tspan> 重新涂色。

js 复制代码
// 一个 TextObject 对应一个 svg,里面所有 text 必须同色,禁止按单字模糊匹配
const fixOfdRenderedTextFills = (pageDiv) => {
  pageDiv.querySelectorAll('svg').forEach((svg) => {
    const texts = [...svg.querySelectorAll('text')];
    if (!texts.length) return;
    const combined = texts.map((el) => el.textContent || '').join('');
    const color = lookupOfdTextFill(combined);   // 用整段文字反查颜色
    if (!color) return;
    const alpha = lookupOfdTextAlpha(combined);
    texts.forEach((el) => paintOfdTextEl(el, color, alpha)); // 强制 setAttribute('fill')
  });
};

这里有两个关键避坑点(都是踩出来的):

  1. 按"整段文字"匹配,不按单字模糊匹配 。同一个字可能出现在红标题和黑正文里,按单字查会串色。combined 用整段 textContent 做 key 才稳。
  2. 建立颜色表时过滤"近似黑" 。OFD 里大量正文本就是黑色,如果映射表把"解析失败/缺失"也记成黑,会覆盖掉真正该着色的文字。代码里用 isNearBlackRgb(灰度均值 < 45 且色差 < 20)把"坏颜色"挡在映射表外,避免越补越乱。

五、根因三:ofdjs 解析结果本身不可全信,直读原始 zip XML 兜底

做到根因二,颜色表来自 collectOfdTextFills(ofdDocument)------也就是 ofdjs 解析出的内存对象 。但问题来了:对象树在解析时已经可能把部分颜色丢了,拿一个"已经缺数据"的源去建映射表,补色自然不全。

最稳妥的办法是绕过 ofdjs,直接读 OFD 这个 zip 包里的原始 XML ------那才是权威数据源。OFD 是标准 zip,末尾有 EOCD(End Of Central Directory)记录,顺着它能枚举所有条目,再用 DecompressionStream 解压出 XML:

js 复制代码
const readZipXmlTexts = async (data) => {
  // 1. 从末尾 65535 字节里找 EOCD 签名 0x06054b50
  // 2. 读条目数、偏移,逐个解析文件名/压缩方式/大小
  // 3. 对 .xml 条目做 inflate(deflate-raw 失败再试 deflate)
  // 4. 返回所有 XML 文本数组
};

然后用正则 + DOMParser 双保险 ,从原始 XML 里把 ColorSpace/Palette/DrawParam/TextObject 的颜色信息抠出来,和 ofdjs 的结果合并:

js 复制代码
const renderOFD = async (data) => {
  ofdPalettes = new Map();
  const zipFills = collectFillsFromXmlList(await readZipXmlTexts(data), {});
  // ......
  ofdTextFills = [...collectOfdTextFills(ofdDocument), ...zipFills];
  // 两份来源合并,映射表更全
};

颜色解析还要处理 OFD 的"调色板索引"这种高级写法:TextObject 只写 FillColor Index="3" ColorSpace="CS1",真正的 RGB 藏在 ColorSpace 的 Palette 里。代码里 resolveColorNode 专门按 ColorSpace 去 ofdPalettes 查表还原,再 isNearBlackRgb 过滤。

三层修复的分工:根因一(预渲染归一化)治"大部分简单文档";根因二(DOM 补色)治"渲染后仍丢色";根因三(直读 zip)保证"补色用的映射表是完整、权威的"。三者缺一不可。

六、避坑清单(可直接抄进团队 Wiki)

坑 现象 修复动作
DrawParam 颜色是对象 整片文字变黑 flattenOfdDrawParamColors 拍平成字符串
颜色写法口径不一 部分彩色变灰/变黑 normalizeOfdColorValue 统一 RGB/0-1/CMYK/hex
渲染后 SVG 仍黑 复杂文档丢色 fixOfdRenderedTextFills 渲染后补 fill
按单字查色串色 标题被正文染色 用整段 textContent 作匹配 key
坏颜色覆盖真黑字 越补越乱 isNearBlackRgb 过滤近似黑
依赖 ofdjs 解析结果 映射表不全 readZipXmlTexts 直读原始 zip XML 兜底
调色板索引颜色 索引色变默认 resolveColorNode 按 ColorSpace 查 Palette

七、结语

OFD 在前端生态里远不如 PDF 成熟,@ycsx/ofdjs 已经做得很好,但在颜色解析的边界情况 上仍有盲区。这次排障给我的核心启发是:当渲染库的结果不可全信时,与其和它死磕,不如回到"数据源头"------直接解析原始文件格式------拿权威数据做后处理修正。预渲染归一化 + 渲染后补色 + 源文件兜底,这套组合拳之后,红标题、蓝链接、带透明度的水印章全部正确还原。

如果你的项目也在做 OFD 预览签章,希望这篇复盘能帮你省下两天排查时间。如果踩到本文没覆盖的坑,欢迎在评论区一起交流。


本文代码均来自真实生产组件,已做脱敏与精简。转载请标注来源。

相关推荐
玖玥拾23 天前
Unity Shader 基础与图形学(一)
unity·图形渲染·图形学·shader
SmalBox1 个月前
【交互着色效果】Unity实现-交互式雪地效果
unity3d·游戏开发·图形学
SmalBox1 个月前
【高级着色器】Unity实现-卡通风格水着色器
unity3d·游戏开发·图形学
SmalBox1 个月前
【高级着色器】Unity实现-彩虹泡泡着色器
unity3d·游戏开发·图形学
SmalBox1 个月前
【动态着色】Unity实现3D扫描线
unity3d·游戏开发·图形学
SmalBox1 个月前
【动态着色】Unity 实现消融效果
unity3d·游戏开发·图形学
SmalBox1 个月前
【动态着色】Unity 实现全息投影着色器
unity3d·游戏开发·图形学
SmalBox1 个月前
【动态着色】Unity 实现箭头图案
unity3d·游戏开发·图形学
SmalBox1 个月前
【扭曲着色器】Unity实现-黑洞扭曲效果
unity3d·游戏开发·图形学