HotPDF 解碼已載入 PDF 頁面裡旋轉過的 QR 符號,做法是在解碼器內部把取樣出的模組矩陣正規化過全部八個 D4 方位。對線性符號體系有用的外層旋轉重試,對 QR 行不通,而弄懂為什麼,能幫您省下追著一個看似壞掉、其實沒壞的解碼器跑一整天的時間
場景再普通不過。掃描的送貨單以 PDF 進來,每頁帶一個 QR 標籤,操作掃描器的人把整疊紙往進紙槽肯收的方向塞。有的標籤是正的,有的偏了四分之一圈,有幾張是倒的。您呼叫條碼解碼器,一半的頁面解析出來,另一半什麼都沒帶回來,連個錯誤都沒有
為什麼旋轉掃描遮罩永遠救不回旋轉的 QR?
因為 QR 的 finder 圖樣佈局是刻意不對稱的,而整張影像的旋轉只會保存這個不對稱,不會消除它。QR Code 把三個 finder 方塊放在左上、右上與左下角,右下角刻意留空(ISO/IEC 18004:2015 §6.3.3)。那個空角就是方位提示。把頁面點陣圖轉九十度,缺口只是搬到另一個角。平面上不存在任何非平凡的旋轉能把三角佈局映射回它自己,所以只接受標準方位的解碼器,會把每一次嘗試都依序拒收
這件事要緊,因為直覺的修法恰恰是錯的那個。自然的本能是把重試掛在外面:算繪頁面,把遮罩交給解碼器,失敗了就旋轉遮罩,再試 90、180、270 度。對 Code 39,這個策略完全正確,因為線性符號體系有起始與終止圖樣,只要條紋轉成水平,掃描器就找得到。對 QR,這是四次必然的失敗,外加一份「找不到」的報告
把 D4 群用在模組矩陣上
正規化的正確位置在取樣之後,作用在布林模組格上,而不是像素遮罩上。一旦解碼器把符號解析成 n 乘 n 的深淺模組矩陣,它就可以列舉正方形的二面體群:四個旋轉乘兩個鏡射,共八個候選方位。對每個候選檢查 finder 三角形,第一個讓三個 finder 落在左上、右上與左下位置的候選,就是真正的方位。從那裡開始,既有管線原樣運行,因為格式資訊位元、之字形資料放置與 Reed-Solomon 糾錯都假設拿到一個標準方位的矩陣,而現在真的拿到了
兩個性質讓這件事很便宜。矩陣跟算繪出來的點陣圖相比非常小,所以八次轉置遠比八次頁面算繪便宜。而且矩陣是取樣器建出的乾淨布林陣列,途中任何變換都不可能引入從未被取樣過的值
版本偵測是一次整除搜尋,不是一次除法
模組數沒辦法靠「取樣寬度除以假設的模組尺寸」推導出來,弄錯這件事是高解析算繪下一個隱蔽的解碼失敗來源。版本 v 的 QR 符號每邊是 4v + 17 個模組,所以版本 1 是 21 個模組,版本 40 是 177。一個量起來 126 像素寬的遮罩,同樣符合「版本 1、每模組六像素」,也同樣符合好幾個模組更小的更高版本。線性除法會挑其中一個,而且通常挑錯
行得通的是對候選版本做整除搜尋。從版本 40 往下走到版本 1,留下模組數能整除取樣寬度、而且每個模組還能分到至少三個像素的候選,取存活版本中最小的那個。三像素的下限擋住了搜尋把一個粗糙符號讀成荒謬密度的解讀,最小版本規則則把剩下的歧義推向掃描器實際上會產出的那個讀法
var
Pdf: THotPDF;
Options: THPDFBarcodeDecodeOptions;
Codes: THPDFDecodedBarcodes;
Info: THPDFBarcodeDecodeInfo;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.LoadFromFile('delivery-notes.pdf');
Options := THPDFBarcodeDecodeOptions.Default;
Options.DPI := 300;
Options.RotationPolicy := bdrpFallback;
Options.MinimumConfidence := 0.5;
Options.MaxResults := 16;
if Pdf.DecodeLoadedPageBarcodes(0, Options, Codes, Info) then
for I := 0 to High(Codes) do
if Codes[I].Symbology = bsyQRCode then
Writeln(Codes[I].Text, ' at ',
Format('%.0f', [Codes[I].OrientationDegrees]), ' degrees');
finally
Pdf.Free;
end;
end;
THPDFBarcodeDecodeOptions.Default 回傳的是一個填好預設值的記錄,不是歸零的,這一點要緊,因為 DPI 設零或結果上限設零,都是一種看起來合法、實際上什麼都拿不回來的寫法。RotationPolicy 只管外層重試:bdrpNone 只算繪一次,bdrpFallback 在第一輪失敗後重試其他方位,bdrpAll 無條件把每個方位都算繪一遍。因為 QR 的正規化發生在解碼器內部,三種策略之下 QR 頁面都在第一次嘗試就解析出來。這個策略是為真正需要它的線性符號體系準備的
怎麼證明一個點陣圖變換沒有憑空發明像素?
兩邊都數一數墨水,要求總數相等。旋轉只是像素的排列,僅此而已,所以輸出裡非零格的數量必須等於輸入。當外層重試路徑裡的遮罩旋轉回報進去 4800 個置位格、出來 7439 個,光這一個比對就足以定罪那個變換,一行幾何程式碼都不必讀
肇因平凡,卻值得當成一條規則帶走。用 SetLength 配大小的動態陣列,當它是一個函式結果、走的又是 runtime 不會清空的路徑時,並不保證以歸零狀態送達,於是旋轉從未寫入的格子就帶著之前就在那裡的任意位元組。這些殘留位元組有些非零,而非零就是墨水。修法只有一行:在排列迴圈跑之前先 FillChar(Result[0], N, 0),它背後的紀律更廣:任何回傳遮罩或點陣圖緩衝區的函式,都該明確清空輸出,而不是依賴配置語意
比缺陷本身更有趣的,是它怎麼活過三個版本。QR 把方位處理搬進解碼器之後,QR 就完全不再行使外層遮罩旋轉,那條程式路徑剩下的唯一消費者是 Code 39。共用基礎建設向來就是這樣藏蟲的:一個功能的覆蓋讓某條路徑看起來有被測到,而真正依賴它的那個功能自己卻什麼測試都沒有。新功能不再使用的每一條路徑,都需要一個仍然在用它的測試
用頁面座標把結果讀回來
解碼器產出的每一個幾何值,都表達在嘗試用點陣圖的座標框架裡,而呼叫端要的是 PDF 使用者空間。這道轉換分兩段:先抵銷重試施加的四分之一圈,再抵銷把使用者空間映射到點陣圖的算繪變換。THPDFDecodedBarcode 裡抵達的是使用者空間中一個軸對齊的包圍盒,Left、Bottom、Right 與 Top 遵循 Y 軸向上的 PDF 慣例,外加一個逆時針的 OrientationDegrees
第二段轉換的方向弄反了,症狀很賊:文字解得完美無缺,但您為審閱覆疊畫的那個框,落在正確位置的鏡像上。任何要在解碼器之上建審閱介面的人,都該拿一個已知樣本做斷言,把符號刻意放在接近某個頁角的位置,讓 Y 軸翻轉一眼就看得出來。同樣的道理適用於任何跨越算繪邊界的座標,這也是為什麼在解碼器上動工之前,值得先讀懂在 Delphi 把 PDF 頁面算繪成點陣圖
內建解碼器會做什麼、不會做什麼
內建解碼器是一個有邊界、零相依的實作,對自己的極限很誠實,不會默默退化。它認得 Code 39 與 QR,在公布任何資料之前先驗證 BCH 保護的格式位元與遮罩圖樣,也不對受損符號嘗試錯誤復原。如果您的輸入是一張彎曲標籤在不均勻光線下的照片,那是另一個問題類別,需要專門的引擎
// 換上您自己的引擎:實作 IHPDFBarcodeDecoder,
// 傳給解碼器感知的那個多載。頁面算繪、預算、
// 座標映射與去重仍然由 HotPDF 負責
if not Pdf.DecodeLoadedPageBarcodes(PageIndex, MyDecoder, Options,
Codes, Info) then
case Info.Status of
bdsBudgetExceeded:
Log('raise MaxPixels or lower DPI: ' + string(Info.Diagnostic));
bdsRenderError:
Log('page did not render: ' + string(Info.Diagnostic));
bdsDecoderError:
Log(string(Info.DecoderName) + ' failed: ' + string(Info.Diagnostic));
end;
THPDFBarcodeDecodeInfo 是生產管線真正賺到錢的地方。RotationAttemptCount 與 DecoderCallCount 告訴您外層重試到底跑過沒有;ReceivedResultCount 對照 AcceptedResultCount,可以區分「解碼器什麼都沒找到」與「信心度門檻把它找到的東西全數退貨」;RenderedPixels 加 PeakWorkingBytes,是批次作業開始記憶體抖動時您該畫圖看的兩個數。空的結果集配上 bdsSucceeded,代表頁面真的沒有可讀符號,這與 bdsBudgetExceeded 是兩種完全不同的營運事實
預算欄位值得一次深思熟慮的決定,而不是留預設。MaxPixels 與 MaxWorkingBytes 存在的理由是 DPI 以平方倍數放大:A4 頁面從 300 DPI 走到 600 DPI,算繪成本與配置尖峰都翻四倍,而一個宣告了巨大頁面框的不可信任輸入,能把一次掃描作業變成一起記憶體耗盡事故。上限設在您最惡劣的合法文件所需要的程度,然後讓 bdsBudgetExceeded 把離群值導向一條更慢、但隔離的路徑
如果您的文件把機器可讀標籤與打算建索引的印刷文字混在一起,條碼解碼器跟 HotPDF 內建的範本比對 OCR一文的辨識引擎是天然一對,同一個故事的產生端則在用 HotPDF 把條碼畫進 PDF。兩者跑在同一套算繪與預算基礎建設上,已經為其中一個設好合理上限的管線,幾乎免費得到另一個
旋轉容錯屬於那種「正常時沒人看見、失效時人人暴怒」的功能,而它的工程教訓可以推廣到 QR 之外:正規化要盡量貼近語意表示,不要留在像素層——那裡的資料還帶著擷取過程的每一次意外。HotPDF 把這一切當成 HotPDF Delphi PDF component 的一部分出貨,與同一批 intake 管線通常需要的算繪、OCR 與頁面分析元件同在