Technical Article

在 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,此外还有选择数据模式(0 代表自动,或数字、字母数字、ISO-8859-1 和 UTF-8 变体)的 EncodeOptions 值和代表方向的 DrawOptions 值。

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(现代里德-所罗门方案)的紧凑二维网格,在相同数据的 QR 符号会显得别扭的尺寸下它仍然保持可读。这使其成为直接零件打标、微型电子元件和高密度物流标签的标准选择。

DrawDataMatrixSymbol 接受用于点距的 ModuleSize、用于 ASCII 的 Encoding(值为 1),以及要么为 0(代表自动)或者是从 10x10到 132x132 的标准正方形和矩形尺寸之一的 SymbolSizeOptions 参数将方向与静空区(quiet-zone)宽度相结合,其中加上 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 的标准)通过携带应用标识符(Application Identifiers,告诉接收系统随后的数字是序列号、批次代码还是有效期的括号前缀)构建在 Code128 之上。该结构由 FNC1(一种将符号标记为 GS1 编码并分隔可变长度字段的特殊非数据字符)标记。在 PDFlibPas 中,你使用 Code128 类型以及放置在每个应用标识符开始的数据字符串中的字面 [FNC1] 标记,通过 DrawBarcode 绘制 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 和 C++Builder 的 Delphi PDF Library 的一部分提供,同时还包括本博客其他地方介绍的文本、图形、表单和签名 API。