文档扫描与图像处理

生成 AAMVA 驾照条码测试数据:71 个辖区的合成 PDF417

在线驾照条码生成器界面

写驾照扫描器的人都会遇到同一个问题:拿什么测?真实驾照是个人数据,不能提交到代码仓库,不能塞进 CI 任务,也不能当作样例发给客户。结果是解析逻辑上线时,只对着办公室里恰好有的那两三张照片验证过。在线 AAMVA 驾照条码生成器要解决的就是这半边问题——按规范合成载荷,造出可以随便用的测试素材。

在线演示

在线驾照条码生成器(英文名 Online Driver License Generator):打开就能用,不需要注册,也不需要安装;载荷拼装、PDF417 编码和卡片渲染都在浏览器内完成。

配合本文验证时可以这样走一遍:换几个辖区和证件类型各生成一张,下载卡片 PNG,再拿到驾照条码扫描器里读回来,确认不同辖区的载荷长度与字段都能正常解析——这正是本文提到的 426 个载荷往返验证在做的事。

关键要点

  • 纯前端页面,唯一的第三方依赖是 bwip-js@4.11.4,没有识别 SDK、没有许可证、没有上传。
  • 覆盖 71 个辖区:美国 50 州 + 哥伦比亚特区、5 个美国属地、13 个加拿大省与地区、2 个墨西哥州。
  • 支持 DL 与 ID 两种证件类型,以及 AAMVA 2020 / 2016 / 2013 三个版本号。
  • 载荷按规范拼装:21 字节文件头 + 10 字节子文件描述符 + 子文件体,数据元素按固定顺序排列。
  • 渲染成 1000×1340 逻辑尺寸的正反面卡片,导出 PNG;条码区域 740×310,模块以整数倍绘制。
  • 官方往返验证:71 辖区 × 3 版本 × 2 证件类型 = 426 个载荷,结构、编码、解码、AAMVA 解析四项全部 426/426 通过。

载荷结构:三段拼装

AAMVA 载荷是一段明文,结构分三层。

文件头(21 字节)

长度 内容
1 合规指示符 @
1 数据元素分隔符 \n
1 记录分隔符 \x1e
1 段终止符 \r
5 文件类型 ANSI (含尾随空格)
6 签发机构识别号 IIN
2 AAMVA 标准版本号
2 辖区扩展版本号
2 子文件数量

子文件描述符(10 字节):类型占 2 字节,偏移量占 4 字节,长度占 4 字节。偏移量的计算方式是文件头长度加上描述符自身的长度:

var entryCount = 1;
var header = '@' + LF + RS + CR + 'ANSI '
  + s.jurisdiction.iin
  + aamvaVersion
  + jurisdictionVersion
  + pad(entryCount, 2);

var offset = header.length + 10 * entryCount;   // 单子文件时为 31
var designator = s.cardType + pad(offset, 4) + pad(subfile.length, 4);
return header + designator + subfile;

子文件体:以证件类型(DLID)开头,之后是各数据元素,元素之间用 \n 分隔,整体以 \r 结尾。每个元素是“三字符代码 + 值”的形式,空值直接跳过:

function add(code, value) {
  if (value === undefined || value === null || value === '') return;
  elements.push(code + value);
}

输出的数据元素及顺序

DAQ 驾照号、DCS 姓、DDE(姓截断标记 N)、DAC 名、DDFDAD 中间名、DDGDCU 后缀、DCA 准驾车型、DCB 限制、DCD 签注、DBD 签发日期、DBB 出生日期、DBA 有效期至、DBC 性别、DAU 身高、DAY 眼睛颜色、DAZ 头发颜色、DAG 街道、DAI 城市、DAJ 辖区代码、DAK 邮编、DCF 文档识别码、DCG 签发国家、DDA 合规类型 FDDB 签发日期、DDD 卡片修订日期、DAW 体重。

日期一律用 MMDDYYYY 八位。有两点需要明确:一,DCU 后缀在这个生成器里始终为空,所以不会出现在输出中;二,没有按辖区区分必填字段——所有辖区输出完全相同的一组元素,差异只在 IIN、国家、示例城市与邮编、号码模式、默认准驾车型(美国 C、加拿大 G、墨西哥 A)以及默认限制/签注值(NONE)。

号码模式与随机数据

每个辖区的驾照号格式用一个模式串描述:# 表示随机数字,A 表示随机大写字母,X 表示随机字母数字,其余字符原样输出。

function randomFromPattern(pattern) {
  var out = '';
  for (var i = 0; i < pattern.length; i++) {
    var c = pattern[i];
    if (c === '#') out += Math.floor(Math.random() * 10);
    else if (c === 'A') out += String.fromCharCode(65 + Math.floor(Math.random() * 26));
    else if (c === 'X') out += '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'[Math.floor(Math.random() * 36)];
    else out += c;
  }
  return out;
}

安大略省的 A####-##### 就是这样生成的。需要重申的是这些模式是看起来合理的样例,不是权威的辖区规范

随机数据生成器的取值范围:年龄 18–62 岁(出生日期随机),签发月份取当月,有效期为签发日期后 4–8 年;身高 60–76 英寸(5 英尺 0 到 6 英尺 4),三位补零后加 " in";体重 110–239 磅;眼睛颜色 6 种、头发颜色 7 种;美国邮编补齐到 9 位加两个尾随空格,加拿大邮编去掉空格并转大写;文档识别码是 20 位随机数字,并且在页面会话内保持不变(否则每敲一个键都会变成一个新值)。

渲染:一张能拍能扫的卡片

卡片按 ID-1 比例(85.6 × 54 mm)排版,逻辑画布 1000×1340,实际按 2 倍设备像素比绘制成 2000×2680:

var DPR = 2;
var SHEET_W = 1000, SHEET_H = 1340;
var CARD_X = 20, CARD_W = 960, CARD_H = 605;
var FRONT_Y = 48, BACK_Y = 713;
var BARCODE_BOX = { x: CARD_X + 24, y: BACK_Y + 240, w: CARD_W - 220, h: 310 };

按设备像素绘制不是为了好看,而是为了让 PDF417 的模块在导出的 PNG 里有若干像素宽——这是“下载再上传仍然能读”的前提。整张画布通过 setTransform(DPR, 0, 0, DPR, 0, 0) 建立起逻辑坐标系,之后所有绘制代码都按逻辑单位写,不必到处乘 2。

正面包含:圆角卡片与投影、90 条正弦纹路的底纹(透明度 0.07 防伪纹理)、按国家取色的渐变标题条(美国深蓝、加拿大深红、墨西哥深绿)、DRIVER LICENSEIDENTIFICATION CARD 标题、姓名 · 辖区代码 · IIN 副标题、人像占位区,以及两列共 7 行字段:驾照号/准驾车型、姓/限制、名/签注、地址(独占一行)、出生日期/性别、签发/有效期、身高/眼睛与头发颜色。性别在条码里是 1/2/9,在卡片上渲染成 M/F/X。

背面包含:底纹、右侧磁条黑色矩形、LIC/DOB/EXP/CLASS REST END/DCF 的打印摘要、居中的条码,以及一行小字 SAMPLE DATA — FOR SCANNER TESTING ONLY。条码以关闭平滑的方式按比例适配到条码框内居中:

ctx.imageSmoothingEnabled = false;
ctx.drawImage(barcodeCanvas, BARCODE_BOX.x + (BARCODE_BOX.w - w) / 2,
  BARCODE_BOX.y + (BARCODE_BOX.h - h) / 2, w, h);
ctx.imageSmoothingEnabled = true;

两个 SPECIMEN / NOT A REAL ID 水印分别以 56% 和 52% 的透明度旋转绘制在正反面,背面还有一处。

条码参数

var opts = {
  bcid: 'pdf417', text: payload, scale: BARCODE_SCALE, rowmult: 3,
  eclevel: 5, paddingwidth: 4, paddingheight: 4,
  backgroundcolor: 'FFFFFF', barcolor: '000000'
};

纠错等级 5 在容量和抗损之间取了一个偏稳妥的位置;留白 4 个模块是规范要求,不是美观问题。行列数与压缩模式没有手工指定,交给 bwip-js 自动选择。bwip-js 4.x 在没有回调参数时返回 Promise,所以渲染再和 15 秒超时做一次竞速,避免出问题时界面一直转圈。

往返验证:426 个载荷

生成器在 README 里记录了一组验证数据,这组数字比较能说明“合成数据能不能真的用于测试”:

验证项 结果
结构、编码、解码、AAMVA 解析(71 × 3 版本 × 2 类型 = 426 个载荷) 426/426 通过
端到端往返(生成 → 导出 → 扫描 → 解析比对) 79/79 通过
每次返回的字段数 27 个
载荷长度范围 269–293 字符

两个需要知道的例外:墨西哥的两个辖区(Coahuila CU、Hidalgo HL)在解析时会原样返回辖区代码,因为 SDK 的规范里没有映射这两个 IIN。

它明确不做什么

不是官方规范实现。IIN 与辖区代码是公开分配值,但各辖区具体的必填字段规则、地址结构、号码校验逻辑都没有逐项实现。

不生成真实证件。卡片上的水印、NOT A REAL IDSAMPLE DATA — FOR SCANNER TESTING ONLY 都是刻意的。合成数据仅用于测试自己的扫描器或解析器。

不保证真实性。AAMVA 条码是明文,一个能被正确解析的条码只能说明载荷格式合法,与证件是否真实毫无关系。生成器能做的是产出合法载荷,这正好是测试夹具需要的属性,也正好是绝不能被表述成“防伪”的那件事。

不支持 AAMVA 之外的驾照,也生成不了南非驾照(内容为 RSA 加密,密钥不在公开范围)。

常见问题

生成器里的辖区数据是官方规范吗?

不是。IIN 与辖区代码取自 AAMVA 公开的分配,但每个辖区具体的必填字段规则、地址格式和号码规则并没有逐项实现——所有辖区输出的是同一组数据元素,只有 IIN、国家、示例城市邮编和号码模式不同。它是测试素材工具,不是规范实现。

为什么每张卡片图上都有 SPECIMEN 水印?

因为这个工具的用途是生成测试素材,不是制作证件。水印、正面的 NOT A REAL ID 提示和背面的 SAMPLE DATA — FOR SCANNER TESTING ONLY 都是刻意保留的,避免合成数据被误当成真实证件使用。

能生成欧盟、英国、澳大利亚或印度的驾照吗?

不能。这个工具只做 AAMVA 文档,也就是美国、加拿大和墨西哥的驾照与身份证件。其它国家的驾照不是 AAMVA 格式,需要各自的国家规范。

为什么南非驾照不能生成?

因为南非驾照的 PDF417 内容不是明文——720 字节里除头部外由 6 个 RSA 加密块组成,密钥由签发机关持有。任何工具都生成不出可被解析的合成南非驾照,只能拿真实卡片作为输入。

PDF417 的编码参数是怎么设置的?

用 bwip-js 的 pdf417 编码器,scale 设为 8、行高倍率 3、纠错等级 5、四周留白 4 个模块;行列数与压缩模式交给编码器自动选择,没有手工指定。卡片整体按 2 倍设备像素比渲染,所以导出 PNG 里的模块宽度有若干像素,下载再上传后仍可识别。

相关阅读