StreamPDFStreamPDF
返回博客

PDF.js 源码解读: scripting_utils

2026年10月27日 · StreamPDF Team

PDF.js 源码解读: scripting_utils

前言

src/shared/scripting_utils.js 是 PDF.js 脚本与批注路径共用的小型值转换层。它导出 ColorConverters、DateFormats 与 TimeFormats,本身并不执行 JavaScript action、解析日期或创建批注元素。

这种职责边界是有意为之。文件头提醒 display 与 scripting 构建都会导入它,随意增加依赖可能放大原本无关的构建产物。它唯一的导入是 MathClamp,实现也始终停留在数值转换与常量查询上。

模块总览

导出或辅助函数是否导出职责
makeColorComp(n)否将归一化分量限制在 0–1,向下取整到 0–255,再格式化为两个十六进制字符
scaleAndClamp(x)否将归一化分量限制在 0–1 并缩放到 0–255,不做取整
ColorConverters是在带标签的 PDF.js 颜色、CSS 颜色与批注存储 RGB 值之间转换
DateFormats是十四个按索引查询的 Acrobat 日期格式预设
TimeFormats是四个按索引查询的 Acrobat 时间格式预设

源码在 ColorConverters 上方引用了 PDF 规范第 10.3 节。实现做的是直接分量运算,并非 ICC 颜色管理引擎,也没有加入颜色配置或 gamma 处理。

makeColorComp 与 scaleAndClamp:输入相似,输出不同

function makeColorComp(n) {
  return Math.floor(MathClamp(n, 0, 1) * 255)
    .toString(16)
    .padStart(2, '0');
}
 
function scaleAndClamp(x) {
  return MathClamp(x, 0, 1) * 255;
}

两个辅助函数都接收归一化分量,并把它限制在 0 到 1 的闭区间内,但输出契约不同:

  • scaleAndClamp 返回 0 到 255 的 number,并保留小数。中点会变成 127.5。
  • makeColorComp 使用 Math.floor,再为经过范围限制和取整的字节值返回恰好两个十六进制字符。同一个中点会得到 7f,而不是 80。

单元测试清楚展示了这种差别:

const color = [0.5];
expect(ColorConverters.G_rgb(color)).toEqual([127.5, 127.5, 127.5]);
expect(ColorConverters.G_HTML(color)).toEqual('#7f7f7f');

两个辅助函数都不导出,也不校验非数值输入。上层脚本代码会在转换前检查颜色数组;不能把它们理解成通用 CSS 颜色解析器。

ColorConverters:真实存在的十四个转换方法

命名与返回形态

方法名采用 源_目标 形式。大写 G、RGB、CMYK 与 T 分别表示灰度、RGB、CMYK 和透明形式,目标名称决定返回形态:

目标返回形态在外层代码中的用途
G、RGB、CMYK第一项为目标标签的数组脚本颜色转换
rgb0–255 分量的裸数组,透明时为 [null]批注存储值
HTMLCSS 十六进制颜色字符串批注元素样式

Color.convert 等调用方会在调用转换器前移除源标签,因此方法只接收分量数组。下表保留当前源码的运算与返回值,不会仅凭命名规律虚构其他转换。

灰度与 RGB 方法

方法实际行为
G_CMYK([g])返回 ["CMYK", 0, 0, 0, 1 - g]
G_RGB([g])返回 ["RGB", g, g, g]
G_rgb([g])对 g 缩放一次,返回三个相同数值
G_HTML([g])用 makeColorComp 转换 g,并在 #rrggbb 中重复该分量
RGB_G([r, g, b])返回 ["G", 0.3 * r + 0.59 * g + 0.11 * b]
RGB_rgb(color)将每个分量映射到 scaleAndClamp
RGB_HTML(color)将分量映射到 makeColorComp,再在 # 后拼接
RGB_CMYK([r, g, b])计算 c = 1-r、m = 1-g、y = 1-b、k = min(c,m,y),返回 ["CMYK", c, m, y, k]

RGB_CMYK 会计算共享的黑色分量,但不会从 c、m、y 中减去它。这就是源码里的简单转换,并非更复杂的底色去除流程。

CMYK 方法及其字面参数顺序

方法实际行为
CMYK_G([c, y, m, k])返回 ["G", 1 - min(1, 0.3*c + 0.59*m + 0.11*y + k)]
CMYK_RGB([c, y, m, k])返回带 RGB 标签的红色 1-min(1,c+k)、绿色 1-min(1,m+k)、蓝色 1-min(1,y+k)
CMYK_rgb([c, y, m, k])对上述三个 RGB 分量公式分别执行 scaleAndClamp,返回裸数组
CMYK_HTML(components)调用 CMYK_RGB,移除标签,再把分量交给 RGB_HTML

这些签名必须按字面阅读:三个 CMYK_* 输入方法都解构为 [c, y, m, k],而 RGB_CMYK 构造的是 ["CMYK", c, m, y, k]。这就是当前实现,单元测试预期也固定了相同行为。复用这些方法时,不应悄悄重命名位置,或替换成其他库的 CMYK 约定。

CMYK_HTML 还通过 this 调用其他转换器。现有的 ColorConverters[method](components) 动态调用会保留 receiver;把方法拆出来再在无 receiver 的情况下调用,会破坏这种内部委托。

透明方法刻意只有两种输出

static T_HTML() {
  return "#00000000";
}
 
static T_rgb() {
  return [null];
}

只有这两个转换器以 T 为源。在 T_HTML 与 T_rgb 中,目标分别是 HTML 和 rgb:前者返回用八个十六进制字符表示的透明黑,后者返回存储标记 [null],而不是 CSS 字符串或 [0, 0, 0]。文件中没有 T_G、T_RGB 或 G_T。请求透明目标时走的是另一条路径:Color.convert 会在向 ColorConverters 分派之前直接返回 ["T"],所以目标侧没有对应转换器本身就是实际 API 边界的一部分。

DateFormats 与 TimeFormats:数字预设表

const DateFormats = [
  'm/d',
  'm/d/yy',
  'mm/dd/yy',
  'mm/yy',
  'd-mmm',
  'd-mmm-yy',
  'dd-mmm-yy',
  'yy-mm-dd',
  'mmm-yy',
  'mmmm-yy',
  'mmm d, yyyy',
  'mmmm d, yyyy',
  'm/d/yy h:MM tt',
  'm/d/yy HH:MM',
];
const TimeFormats = ['HH:MM', 'h:MM tt', 'HH:MM:ss', 'h:MM:ss tt'];

数组位置本身就是 Acrobat 脚本 API 的一部分。例如日期索引 0 选择 m/d,索引 1 选择 m/d/yy,时间索引 3 选择 h:MM:ss tt。重排或按字母排序这些数组,会改变现有 PDF action 的含义。

这些条目是交给 PDF.js 脚本日期工具解释的格式 token,不是 Intl.DateTimeFormat 生成的本地化字符串。m 与 mm、月份名称、12 小时制的 tt、24 小时制的 HH 和可选秒数都保留在表里,由下游解析器与格式化器解释。

真实调用方以两种不同方式使用这些表:

  • AForm.AFDate_Format(pdf) 与 AFTime_Format(pdf) 计算 table[pdf] ?? pdf,已知数字预设会展开,其他传入格式则继续交给扩展格式化方法。
  • AFDate_Keystroke(pdf) 与 AFTime_Keystroke(pdf) 只在 pdf 位于对应数组范围内时继续执行,并不使用格式化方法的 fallback 表达式。
  • core 的批注解析也会通过这些数组解析数字格式 action。所选日期格式中的时间 token 可以把字段设为 datetime-local;纯日期格式选择 date,时间格式则选择 time 并设置合适的 step。

因此,共享数组连接了两层逻辑,却不实现其中任何一层:脚本沙箱得到预设字符串,批注代码得到足以选择原生 input 行为的格式信息。

总结

scripting_utils.js 刻意保持很窄的接口。颜色辅助函数区分保留小数的 0–255 值与向下取整的十六进制分量;ColorConverters 恰好暴露十四个转换,覆盖带标签颜色、CSS、存储 RGB 与透明形式;日期时间表则保留 Acrobat 数字预设的顺序。

这个模块的正确性取决于字面契约,而不是宽泛假设:CMYK 参数顺序、[null] 透明标记、CMYK_HTML 的 this 委托,以及格式化与 keystroke 方法不同的 fallback 行为都很重要。把这些细节放在轻依赖的共享文件中,脚本与批注代码就能保持一致,又不必把任一子系统拖进另一个子系统。

StreamPDF 的设计原则: 能前置处理的绝对不放到解析的时候才做, 由服务端一次解决所有性能问题. 浏览器只获取&渲染必须数据.

相关文章