技术文章

Delphi 中的 PDF 条码:QR、PDF417、DataMatrix

运输标签或发票上的条码只有一个任务,就是让扫描器第一次就能读出来。它能不能通过这一次读取,往往在包裹送到装卸平台之前就已经决定好了,决定因素是这个符号究竟怎样落到页面上。Delphi 报表流水线里最常见的错误,是先在别处把条码渲染成位图,再把这张图片塞进 PDF。它在屏幕上某个缩放倍率下看着没问题,换个环境就开始变差

另一种做法,是直接把符号作为矢量内容绘制到页面里。PDFlibPas 正是为此提供了一组绘图调用,覆盖 QR、PDF417、DataMatrix 这些二维矩阵码,也覆盖 Code128、GS1-128 这样的线性码,以及用于邮政自动化的 USPS Intelligent Mail。选择矢量不是为了好看,而是为了让条和空真正落在扫描器预期的位置上

为什么矢量优于放置的位图

条码本质上是条与空的图案,在二维情况下则是明暗模块组成的网格。解码器靠测量这些宽度之间的比例工作,任何扭曲这种比例的因素都会变成噪声,侵蚀符号本来就有限的容错预算。栅格化的条码图像携带的是固定像素;当 PDF 被输出到打印设备,而设备点阵又不能与图像网格整齐对齐时,光栅化器就必须重采样,本来应该锐利的模块边缘会被摊到两个设备像素上。窄条可能变胖,相邻的空白可能变薄,于是解码器依赖的宽度比例就漂掉了

如果用矢量内容绘制,同一个符号会变成一组在 PDF 用户空间坐标中描述的实心矩形,不再受固定像素网格牵制。到了打印时,设备会按自身真实分辨率去渲染每一个矩形,所以无论比例还是打印尺寸如何变化,模块边缘都能保持设备允许的最高清晰度。你把矢量符号放大到托盘标签,或者缩小到包裹标签,几何形状都不会失真。正是这种精度,支撑了高首读率,而这本来就是把条码放上页面的全部意义

QR 码与四个纠错级别

QR 是一种在两个轴上同时读取的二维矩阵符号,所以它能把大量数据压进一个小方块里。它的抗损伤能力来自 Reed-Solomon 纠错,共有四个级别:L 级大约能恢复 7% 的码字,M 级约 15%,Q 级约 25%,H 级约 30%。更高的纠错不是白来的,恢复码字会占掉模块容量,所以在数据量固定的前提下,级别越高,符号要么越密,要么物理尺寸越大

这里的取舍要看符号会落在什么环境里。只会在屏幕上扫描的干净数字文档,可以用 L 级并保持紧凑;如果是要打印、搬运、摩擦,甚至可能被胶带部分遮住的标签,就更适合 Q 或 H,因为额外冗余才是解码器在符号不再完美时仍能重建有效载荷的依靠。DrawQRCode 接收位置和决定绘制宽高的 SymbolSize,再加一个选择数据模式的 EncodeOptions 值,以及一个控制方向的 DrawOptions 值。数据模式里 0 表示自动,另外还包括数字、字母数字、ISO-8859-1 和 UTF-8 变体

var
  Pdf: TPDFlib;
begin
  Pdf := TPDFlib.Create(nil);
  try
    Pdf.NewDocument;
    Pdf.SetPageSize('A4');
    Pdf.SetMeasurementUnits(1);   // 1 = millimetres
    Pdf.NewPage;

    // 30 mm square QR, automatic encoding, normal orientation
    Pdf.DrawQRCode(20, 20, 30, 'https://www.loslab.com/', 0, 0);

    Pdf.SaveToFile('Label_QR.pdf');
  finally
    Pdf.Free;
  end;
end;

纠错级别本身由编码器选择,以便把数据塞进你要求的符号里。如果你在恶劣环境下必须拿到有保证的高等级,就要把符号尺寸留得更宽裕,让编码器有足够模块预算花在冗余上,而不是被迫为了塞得下而降级

用于证件和运输标签的 PDF417

PDF417 是一种堆叠式线性符号。每一行都是一个短线性条码,多行堆起来形成一个块,所以它常见于驾照、登机牌以及承运商的运输标签上,这些场景都需要把较宽的一段数据装进矩形占位里。它的纠错级别范围是 0 到 8,每提升一级,大致都会让纠错码字数量翻倍,因此 5 级比 1 级拥有多得多的冗余,代价则是页面上的码字更多

PDF417 块的形状可以调,这一点很关键,因为标签往往只有固定区域可用。DrawPDF417SymbolEx 暴露了基础调用没有提供的控制项。FixedColumnsFixedRows 用来固定数据列数和行数,其中 0 表示让编码器自行决定;ErrorLevel 可以是 -1 表示自动,也可以显式指定 0 到 8;ModuleSize 表示当前测量单位下最窄元素的宽度;HeightWidthRatio 则决定模块相对于宽度的高度,让你能把整个块做成矮而宽,或者高而窄,以匹配现有空间

// Fixed 10 data columns, automatic rows, error level 5,
// module 0.30 mm wide, rows three times the module width tall
Pdf.DrawPDF417SymbolEx(20, 60, 'PDF417 PAYLOAD 0123456789',
  0,        // Options: 0 = normal orientation
  10,       // FixedColumns
  0,        // FixedRows: 0 = automatic
  5,        // ErrorLevel: 0 to 8
  0.30,     // ModuleSize, in the current measurement unit
  3.0);     // HeightWidthRatio

固定列数通常是标签模板里最常用的控制杆。列数恒定之后,块的宽度就变得可预测,因此当编码有效载荷在不同文档里长度发生变化时,周围布局不会跟着横向漂移,而编码器只需要向下增加行数去吸收差异

用于小型标记的 DataMatrix

当标记必须非常小的时候,DataMatrix 往往是首选。它是一个紧凑的二维网格,使用 ECC 200 这一现代 Reed-Solomon 方案,在相同数据量下,它能在 QR 会显得别扭的尺寸上继续保持可读。这让它成为直接零件打标、小型电子元件以及高密度物流标签的标准选择

DrawDataMatrixSymbol 接收决定点距的 ModuleSize、表示 ASCII 的 Encoding 值 1,以及一个 SymbolSize,它要么是 0 表示自动,要么是 10x10 到 132x132 这些标准正方形与矩形尺寸之一。Options 会把方向与静区宽度组合在一起,其中加上 100 到 400 表示一到四个模块宽的白边。静区不是装饰,解码器需要这圈干净的留白来识别符号的定位图样;如果符号紧贴其他墨迹,它往往连捕获都做不到

// Auto-sized ASCII DataMatrix, 0.5 mm module, normal orientation
// with a one-module quiet zone (Options 0 + 100)
Pdf.DrawDataMatrixSymbol(20, 110, 0.5, 'DMX-SN-4408812',
  1,        // Encoding: 1 = ASCII
  0,        // SymbolSize: 0 = automatic
  100);     // Options: normal + one-module quiet zone

一维条码仍然占统治地位的地方

二维符号很吸睛,但线性条码依旧在零售和物流的大量场景里占据主导地位,原因是单次扫掠式激光扫描器的存量极大。Code128 是字母数字数据的主力,它的效率来自三个字符集:A 集覆盖控制字符和大写字母,B 集覆盖完整的可打印 ASCII 范围,而对数字最关键的是 C 集。C 子集把一对数字编码进一个符号字符里,因此长串数字数据占用的符号字符数,只有在 A 或 B 中的一半。这是铺设长数字条码最紧凑的方式,而 PDFlibPas 的 Code128 实现会自动在 B 与 C 之间切换,以得到这一结果

GS1-128,也就是以前叫 EAN-128 的标准,是构建在 Code128 之上的,它通过携带 Application Identifier,也就是那些带括号的前缀,告诉接收系统后面的数字到底是序列号、批次代码,还是有效期。这个结构由 FNC1 这种特殊的非数据字符标记,它既声明符号采用 GS1 编码,也分隔可变长度字段。在 PDFlibPas 中,你通过 DrawBarcode 使用 Code128 类型,并在每个应用标识符开始的位置放入字面量 [FNC1] 标记,来绘制 GS1-128 符号

var
  W: Double;
begin
  // Code128, with FNC1 markers this becomes a GS1-128 symbol.
  // AI 21 (serial) = ABC123, AI 20 (variant) = 13
  Pdf.DrawBarcode(20, 150, 60, 18, '[FNC1]21ABC123[FNC1]2013',
    3,        // Barcode: 3 = Code128
    0);       // Options: 0 = default drawing

  // Measure the rendered width for a 0.30 mm narrow bar before laying out
  W := Pdf.GetBarcodeWidth(0.30, '[FNC1]21ABC123[FNC1]2013', 3);
end;

在邮件领域,USPS Intelligent Mail,也叫 OneCode,会把路由和跟踪数据编码进一个通过高度调制的条码中,以实现邮政自动化。DrawIntelligentMailBarcode 需要显式指定条宽、完整条高度、跟踪条高度和间隔宽度;数据则是只含数字的 20、25、29 或 31 位字符串。之所以需要显式的条与跟踪条高度,是因为这个符号的含义本身就体现在每一根条究竟是全高条、上升条还是下降条,而邮政读取设备依赖这些高度严格符合规范

绘制到页面里,并为布局做测量

这里展示的每一次调用,都会直接绘制到当前选中页面的内容流里,也就是与你的文字和图片共用的那一层表面。因此条码是普通文档生成流程中的一个组成部分,而不是一个额外导入的独立资源。因为这些符号是矢量内容,它们编码的数据和占用的几何足迹在绘制时就都是已知的,所以你可以确定性地摆放它们

线性条码家族的布局,往往适合先测量。GetBarcodeWidth 会返回给定窄条宽度和条码类型下的总绘制宽度,因此你可以在真正下笔之前,预留出精确的水平空间,而不是靠猜,等页面建完才发现和别的内容撞在一起。二维符号更容易放置,因为你直接通过 SymbolSizeModuleSize 决定其绘制尺寸,符号就会填满这个足迹。无论是哪一种,原则都一样:根据扫描环境决定物理尺寸,确认符号能装进现有槽位,然后让矢量几何把每一条边从屏幕预览一直保持到最终打印

如果你还想把这些条码放回到更完整的页面构建工作流里,我们关于文本、图像和字体提取的文章 介绍了如何从 PDF 里把内容再读出来,而大型 PDF 合并与拆分及直接访问指南则展示了如何高效组装高吞吐量文档。这两部分都能与这里介绍的绘图 API 自然配合使用;这套 API 作为 Delphi PDF Library 的一部分提供给 Delphi 和 C++Builder,同时还包含本博客其他文章讨论过的文本、图形、表单和签名 API