技術文章

在 Delphi 中產生 PDF 條碼:QR、PDF417、DataMatrix

出貨標籤或發票上的條碼只有一項工作,那就是要在掃描器的第一刷就被讀取。它能否在第一刷存活下來,遠在包裹抵達碼頭前就已決定。這取決於該符號是如何被放置在頁面上的。在 Delphi 的報表管線中最常見的錯誤,就是在別處將條碼渲染為點陣圖 (bitmap),然後將該圖片放置到 PDF 中。它在螢幕上的某個縮放級別看起來很好,但在其他所有地方都會劣化

替代方案是將符號繪製為向量內容,直接畫進頁面中。PDFlibPas 公開了一個專門為此設計的繪圖呼叫家族,涵蓋了 2D 矩陣符號 QR、PDF417 和 DataMatrix,透過 Code128 和 GS1-128 涵蓋線性條碼家族,以及用於郵政自動化的 USPS Intelligent Mail。採用向量的理由無關美學。這關乎條碼的線條是否能精準落在掃描器預期的位置上

為何向量勝過放置點陣圖

條碼是一種由線條與空白,或者在二維中由深色與淺色模組 (module) 組成的網格圖案。解碼器是透過測量這些寬度的比例來運作。任何扭曲了比例的東西都是會吃掉符號錯誤預算的雜訊。點陣化的條碼圖片帶有固定的像素。當 PDF 被渲染至一台其墨點無法均勻整除圖片網格的印表機時,光柵化引擎就必須重新取樣 (resample),而原本應該銳利的模組邊緣就會散布在兩個裝置像素之間。一條窄的線條可能會變粗,相鄰的空白可能會變細,解碼器所依賴的寬度比例就會發生偏移

若是繪製為向量內容,相同的符號就是一組以 PDF 使用者空間座標來描述的填滿矩形。沒有固定的像素網格需要對抗。在列印時,裝置會以它實際擁有的解析度來渲染每一個矩形,所以每一個模組的邊緣都會如同硬體所能允許的那般清晰銳利,且不論任何比例與任何列印尺寸皆然。將一個向量符號放大以用於棧板標籤,或縮小以用於包裹,幾何形狀都能保持精確。正是這份精確度維持了第一刷的高讀取率,而這也正是將條碼放在頁面上的全部意義

QR code 與四種錯誤修正級別

QR 是一種 2D 矩陣符號,會在兩個軸向上同時讀取,這也是為什麼它能將大量資料封裝在一個小方塊中。它的容錯能力來自於 Reed-Solomon 錯誤修正,提供四個級別。級別 L 大約能復原 7% 的字碼 (codeword),M 大約 15%,Q 大約 25%,而 H 大約 30%。更高的修正是要付出代價的。復原字碼會佔據模組容量,所以對於固定數量的資料,較高的級別會迫使符號變得更密集或在實體上更大

這種權衡是關於該符號未來所處環境的考量。一份永遠只會從螢幕上掃描的乾淨數位文件可以設定在 L 並保持小巧。而一張會被列印、搬運、磨損,並可能被膠帶部分覆蓋的標籤則需要 Q 或 H,因為這份額外的備援 (redundancy),正是能讓解碼器從一個不再完好如初的符號中重建出負載資料 (payload) 的關鍵。DrawQRCode 接受位置與一個固定繪製寬度及高度的 SymbolSize,加上一個選擇資料模式的 EncodeOptions 值(0 為自動,或數值、英數字、ISO-8859-1 及 UTF-8 變體),以及一個用於方向的 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

固定行數 (column) 是標籤範本上常見的槓桿。一個恆定的行數賦予了區塊一個可預測的寬度,因此周圍的版面配置不會隨著編碼負載從一份文件到下一份文件的長度改變而位移,同時編碼器會向下增加列數 (row) 來吸收差異

適合小型標記的 DataMatrix

當標記必須很小的時候,DataMatrix 就是您該尋求的符號。它是一個緊湊的 2D 網格,使用 ECC 200(現代的 Reed-Solomon 機制),而且在相同資料量的 QR 符號會顯得笨拙的尺寸下,它依然能保持可讀性。這使它成為直接零件標記 (direct part marking)、小型電子元件以及高密度物流標籤的標準選擇

DrawDataMatrixSymbol 接受一個用於點距 (dot pitch) 的 ModuleSize、一個值為 1 代表 ASCII 的 Encoding,以及一個 SymbolSize,它要麼是 0 代表自動,要麼是標準的正方形與矩形尺寸之一,從 10x10 一直到 132x132。Options 參數將方向與靜區 (quiet-zone) 寬度結合,加上 100 到 400 會設定一個至四個模組的白色邊框。靜區不是裝飾品。解碼器需要那塊乾淨的邊緣來尋找符號的尋像圖案 (finder pattern),而一個緊貼著其他墨水的符號就是一個會擷取失敗的符號

// 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

1D 條碼仍然稱王的地方

二維符號吸引了眾人目光,但線性條碼仍然佔據著零售與物流的一大半江山,原因在於那些以單次掃描 (single sweep) 讀取的雷射掃描器安裝基礎。Code128 是英數字資料的主力,它的效率來自於三個字元集。集合 A 涵蓋了控制字元與大寫字母,集合 B 涵蓋了完整的可列印 ASCII 範圍,而集合 C 則是對數字至關重要的集合。子集 C 將一對數字編碼在單一個符號字元中,所以一串連續的數值資料只需花費在集合 A 或 B 中一半的符號字元。那是放置長數字條碼最緊湊的方式,而 PDFlibPas 的 Code128 實作會自動結合 B 與 C 集合來達成此目的

GS1-128(舊稱 EAN-128 標準)是建立在 Code128 之上,藉由攜帶應用識別碼 (Application Identifier),也就是括號內的字首,來告訴接收系統後面的數字是序號、批號還是有效期限。該結構是由 FNC1 來標記,這是一個特殊的非資料字元,用於標示該符號是經過 GS1 編碼的,並分隔可變長度的欄位。在 PDFlibPas 中,您使用 Code128 類型來透過 DrawBarcode 繪製一個 GS1-128 符號,並將文字的 [FNC1] 標記放在資料字串中每一個應用識別碼開始的地方

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)將路線和追蹤資料編碼在單一個高度調變 (height-modulated) 的條碼中,用於郵政自動化。DrawIntelligentMailBarcode 接受明確的幾何形狀:線條寬度、全高線高度、追蹤線高度以及空白寬度,而資料則以 20、25、29 或 31 位的純數字字串提供。存在明確的線條與追蹤線高度是因為該符號將資訊承載於每一條線是全高線 (full bar)、上行線 (ascender) 還是下行線 (descender) 中,而且郵政閱讀器依賴這些高度被保持在規格之內

畫進頁面中並為版面配置進行測量

這裡顯示的每一次呼叫都是畫進目前選取頁面的內容中,也就是接收您文字與圖片的同一表面,所以條碼是作為一般文件產生的一部分而產生,而不是作為一個獨立的資產被匯入。因為符號是向量內容,所以它們編碼的資料與佔據的幾何形狀在繪製時都是已知的,這正是讓您能確定地 (deterministically) 放置它們的原因

線性家族的版面配置得益於先測量。GetBarcodeWidth 會針對給定的窄線寬度與條碼類型傳回條碼繪製的總寬度,所以您可以在承諾繪製之前保留確切的水平空間,而不是靠猜測,然後在頁面建置完成後才發現重疊。2D 符號在放置上比較簡單,因為您透過 SymbolSizeModuleSize 直接設定了它們繪製的尺寸,而該符號就會填滿那個足跡。無論哪種方式,紀律是一樣的。從掃描環境決定實體尺寸,確認該符號適合您擁有的插槽,然後讓向量幾何形狀從螢幕預覽到最終列印,保持每一個邊緣銳利

對於這些條碼所投入的更廣泛的頁面建置工作流程,我們關於文字、圖片及字型提取的文章涵蓋了從 PDF 中將內容讀出來的技巧,而透過直接存取進行大型 PDF 合併與分割的指南則展示了如何有效率地組裝高印量 (high-volume) 文件。兩者都很自然地與此處描述的繪圖 API 搭配,該 API 隨附於適用於 Delphi 與 C++Builder 的 Delphi PDF 函式庫中,與本部落格其他地方所涵蓋的文字、圖形、表單及簽章 API 一起提供