文档扫描与图像处理

JavaScript 驾照扫描器:读取 PDF417 并解析 AAMVA 字段

在线驾照条码扫描器界面

驾照背面的那个 PDF417 条码里,是一段按 AAMVA 规范组织的明文数据:姓名、出生日期、住址、驾照号、有效期、身高、眼睛颜色……全都塞在一条几十到几百字节的记录里。解码只是第一步,真正有价值的是把这段没有分隔符的连续文本切成一列具名字段。在线驾照条码扫描器做的就是这件事。

在线演示

在线驾照条码扫描器(英文名 Online Driver License Scanner):打开就能用,不需要注册,也不需要安装;条码解码与 AAMVA 解析都在浏览器本地完成,上传的图片不会离开设备。

配合本文验证时可以这样走一遍:用配套的 AAMVA 生成器下载一张合成驾照 PNG,在上传模式里扫一次,对照右侧结果面板里的字段名与 AAMVA 元素代码逐项核对;再打开摄像头试一次实时模式,看读到之后的蜂鸣反馈与自动停止采集。

关键要点

  • SDK 为 dynamsoft-barcode-reader-bundle@11.6.3200,加载 ["DBR", "DCP"] 两个模块:一个解码、一个解析。
  • 识别范围被收窄到只认 PDF417,避免正面的一维码或其它码制产生无关结果。
  • 解析规范加载两套:AAMVA_DL_ID(同时覆盖磁条版驾照)与 SOUTH_AFRICA_DL
  • 上传图片必须转成原始 RGBA 字节缓冲(stride = 4 × 宽度format = 10)再调用 capture();传 Blob、HTMLImageElement 或 canvas 会走另一条内部路径,那条路径会静默返回空结果。
  • 相机路径带帧间去重与打开失败重试,相机不可用时自动回退到上传模式。

把识别范围收窄到 PDF417

通用扫码工具会开启几十种码制,但驾照扫描只需要 PDF417。收窄范围既提升速度,也避免正面的一维码被误读成结果:

const filter = new Dynamsoft.Utility.MultiFrameResultCrossFilter();
filter.enableResultDeduplication('barcode', true);
await cvRouter.addResultFilter(filter);

const settings = await cvRouter.getSimplifiedSettings('ReadDenseBarcodes');
settings.barcodeSettings.barcodeFormatIds = Dynamsoft.DBR.EnumBarcodeFormat.BF_PDF417;
await cvRouter.updateSettings('ReadDenseBarcodes', settings);

模板名 ReadDenseBarcodes 是预设之一,适合高密度条码。帧间去重是必须的:相机每秒产出几十帧,同一张驾照会被反复读到,没有去重的话结果面板会每秒刷新几十次。

初始化顺序与许可证

许可证必须在创建任何组件之前激活,否则 CaptureVisionRouter.createInstance() 直接抛错:

await Dynamsoft.License.LicenseManager.initLicense(resolveLicenseKey(), true);
await Dynamsoft.Core.CoreModule.loadWasm(['DBR', 'DCP']);
await Dynamsoft.DCP.CodeParserModule.loadSpec('AAMVA_DL_ID');
await Dynamsoft.DCP.CodeParserModule.loadSpec('SOUTH_AFRICA_DL');
parser = await Dynamsoft.DCP.CodeParser.createInstance();

initLicense 的第二个参数让许可证校验在 worker 里执行,不阻塞主线程。从 11.6 版本起,解析器资源按规范拆成独立的 .data 文件,AAMVA_DL_ID 这一份已经包含了磁条版驾照,不需要单独加载。

许可证按域名选择:托管在 dynamsoft.com 上时用绑定该域名的 Codepool 许可证,其它主机(本地调试、fork)自动回退到 SDK 的公共 24 小时试用 key,并在控制台打印说明、上报一条 license_fallback 事件。

上传图片:为什么必须构造 RGBA 缓冲

这是整个示例里最容易踩、报错信息又最不友好的一个坑:

function imageToDsImageData(img) {
    var canvas = document.createElement('canvas');
    canvas.width = width; canvas.height = height;
    var ctx = canvas.getContext('2d', { willReadFrequently: true });
    ctx.drawImage(img, 0, 0, width, height);
    var data = ctx.getImageData(0, 0, width, height);
    return {
        bytes: new Uint8Array(data.data.buffer, data.data.byteOffset, data.data.length),
        width: width, height: height,
        stride: 4 * width,   // 每行字节数
        format: 10           // IPF_ABGR_8888
    };
}

这正是 SDK 内部处理“用户选中一个文件”时构造的结构。如果直接把 Blob、HTMLImageElement 或 canvas 传给 capture(),SDK 会走另一条内部路径,那条路径不会报错,只是返回空结果——现象是“上传了图片但什么也没识别到”,很难从错误日志里定位。宽度超过 4000 px 的图会先等比缩小再处理。

为什么不用 SDK 自带的图片模式

SDK 有一个 singleFrameMode = 'image' 的单帧模式,但它会在激活时立刻弹出操作系统的文件选择框,页面没法先显示一个拖放区域。所以示例自己做了拖放区、粘贴区和文件选择,只在真正拿到图片后调用 capture()。另外 singleFrameMode 只能在相机关闭状态下修改。

解析:三种 CodeType 分别处理

解析器返回的是 JSON 字符串,里面有一个 CodeType 字段标识它是哪种证件:

const parsedResult = await parser.parse(bytesToParse);
if (parsedResult.exception) {
    showResults('条码读到了,但不是受支持的驾照格式。');
    return false;
}
const dlInfo = JSON.parse(parsedResult.jsonString);
CodeType 处理方式
AAMVA_DL_ID ResultInfo 里找 commonSubfile 节点,递归展开其 ChildFields
AAMVA_DL_ID_WITH_MAG_STRIPE 遍历名称包含 track 的节点,再展开子字段
SOUTH_AFRICA_DL 读取顶层 FieldName/Value 以及子字段
其它 打印警告,不展示结果

展开子字段时会排除四个结构性的伪字段,它们不是身份数据:

const excluded = ['dataElementSeparator', 'segmentTerminator', 'subfile', 'subfileType'];

最后一个坑在结果形态上:相机路径用流式接收器,条码在 result.barcodeResultItems 里;上传路径用 capture(),条码在 result.items 里。两处都要兼容,否则会出现“相机能扫、上传不能扫”这种看起来毫无道理的现象。

南非驾照为什么单独说

南非驾照的 PDF417 是 720 字节,前 4 字节是版本头(v2 为 01 9b 09 45,v1 为 01 e1 02 45),两字节零,剩下 714 字节分成 6 个 RSA 加密块(5 × 128 加 1 × 74)。密钥由签发机关持有,所以这个规范只能读真实驾照——没有可用的合成样本。这也是为什么配套的生成器不提供南非选项:不是工具不支持,是这类数据天然无法合成。

相机路径的现实问题

相机相关的代码里有一半在处理失败情况,这些都是在真实使用里撞出来的:

打开失败要重试一次。上一个会话释放设备需要时间,紧接着再打开会收到 Error opening camera: Camera closed.,等 400 毫秒重试通常就成功:

async function openCamera() {
    try {
        await cameraEnhancer.open();
    } catch (error) {
        console.warn('Camera open failed, retrying once:', error);
        await new Promise(function (resolve) { setTimeout(resolve, 400); });
        await cameraEnhancer.open();
    }
}

相机不可用要回退。没有摄像头、用户拒绝授权、设备被占用——这些情况下不能把访问者留在一个黑框上,直接切到上传模式即可,上传模式本来就不需要相机。

读到了要有反馈。解析成功后蜂鸣一声并停止采集,用户立刻知道“成了”,不用盯着屏幕猜:

async function handleBarcodeResult(result) {
    if (!result.barcodeResultItems || !result.barcodeResultItems.length) return;
    Dynamsoft.DCE.Feedback.beep();
    const ok = await parseDriverLicense(result.barcodeResultItems[0].bytes);
    if (ok) { await cvRouter.stopCapturing(); }
}

错误提示要区分层次,因为这三件事的处理方式完全不同:

  • 图片里没有 PDF417:这张图片里没有找到 PDF417 条码。请重新拍一张驾照背面的清晰照片。
  • 条码读到了但解析失败:条码读到了,但不是受支持的驾照格式。
  • 解析过程抛异常:无法解析这个条码。

粘贴监听挂在 document 上而不是拖放区上,这样用户点过页面任何位置之后直接 Ctrl+V 都能用。

常见问题

这个扫描器支持哪些证件?

支持 AAMVA DL/ID 规范覆盖的北美驾照与身份证件(含磁条版驾照,CodeType 为 AAMVA_DL_ID_WITH_MAG_STRIPE),以及南非驾照规范。欧盟、英国、澳大利亚、印度、日本、巴西等地的驾照不是 AAMVA 文档,需要各自的国家规范。

为什么不直接显示条码里的原始文本?

原始文本是一串以回车和不可见控制字符分隔的定长字段,直接显示出来对使用者没有意义。扫描器把它解析成具名字段——姓名、出生日期、住址、驾照号、有效期等——再展示,这正是与通用扫码工具的区别。字段名保持 SDK 的驼峰命名(licenseNumber 等),只是在显示时拆成带空格的标题,方便和 AAMVA 元素代码对照。

上传图片总是提示没找到条码怎么办?

先确认拍的是驾照背面的 PDF417 区域,并尽量在光照均匀、无反光的角度拍摄。这个示例只识别 PDF417,不识别正面的一维码;另外上传图片宽度超过 4000 px 时会先等比缩小,极端模糊的图仍可能失败。

摄像头打不开怎么办?

页面会自动回退到上传模式。摄像头访问要求安全上下文,所以必须用 HTTPS 或 localhost,并在浏览器弹窗里允许使用摄像头。

条码能解析成功是否说明这张驾照是真的?

不是。AAMVA 条码是明文,解析成功只能说明数据格式合法,不包含任何防伪校验。真实性与否要看证件的物理特征和签发机关的数据库。

测试闭环:先合成一张,再扫回来

真实驾照是个人数据,不能进测试仓库、不能进 CI、也不能当样例发给客户——所以驾照扫描器往往只对着办公室随手拍的几张照片测过。配套的 AAMVA 驾照条码生成器可以为美国、加拿大、墨西哥共 71 个辖区生成带正确校验位的合成 PDF417,并渲染成带 SPECIMEN 水印的正反面卡片 PNG,正好把这条链路补上:生成 → 下载 → 扫描 → 比对字段。

需要说明的是生成器的边界:它产出的是格式合法的测试数据,不是真实证件,也不代表任何辖区官方规范;合成的证件不能冒充真实证件使用。

相关阅读