HotPDF decode QR symbol bị xoay trên một page PDF đã load bằng cách normalize ma trận module đã sample qua cả tám hướng D4 ngay bên trong decoder. Chiến thuật retry xoay từ bên ngoài mà hiệu quả với các symbology tuyến tính thì không thể chạy với QR, và hiểu được lý do giúp bạn tiết kiệm một ngày đuổi theo một decoder trông có vẻ hỏng mà thực ra không
Tình huống thì thường tới mức nhàm chán. Phiếu giao hàng scan đến dưới dạng PDF, mỗi page mang một nhãn QR, và người vận hành máy scan nhồi cả chồng giấy theo hướng nào khay cũng nuốt. Vài nhãn đứng thẳng, vài nhãn lệch một góc tư, một ít thì úp ngược. Bạn gọi barcode decoder, một nửa page resolve, nửa còn lại trả về rỗng mà không một tiếng error
Vì sao xoay scan mask không bao giờ sửa được một QR bị xoay?
Vì layout finder pattern của QR được thiết kế bất đối xứng một cách chủ ý, và xoay toàn ảnh giữ nguyên sự bất đối xứng đó thay vì xóa nó. QR Code đặt ba ô vuông finder ở góc trên-trái, trên-phải và dưới-trái, và để trống góc dưới-phải (ISO/IEC 18004:2015 §6.3.3). Góc bị bỏ trống đó chính là cue định hướng. Xoay page bitmap chín mươi độ và khoảng trống chỉ chuyển sang một góc khác. Không có phép xoay nào khác tầm thường của mặt phẳng đưa được layout ba góc về chính nó, nên một decoder chỉ chấp nhận arrangement chuẩn tắc sẽ từ chối lần lượt mọi attempt
Điều này quan trọng vì cái fix hiển nhiên lại là cái sai. Bản năng tự nhiên là treo retry ở bên ngoài: render page, đưa mask cho decoder, nếu fail thì xoay mask và thử lại ở 90, 180 và 270 độ. Với Code 39 thì policy đó chuẩn không cần chỉnh, vì symbology tuyến tính có start và stop pattern mà scanner tìm thấy được một khi các vạch chạy ngang. Với QR, đó là bốn lần fail được bảo đảm trước một báo cáo không tìm thấy gì
Nhóm D4, áp lên ma trận module
Nơi đúng cho phép normalize là sau khi sample, trên lưới module boolean chứ không phải trên pixel mask. Khi decoder đã resolve symbol thành một ma trận n nhân n các module đen và trắng, nó có thể liệt kê dihedral group của hình vuông: bốn phép xoay nhân hai phép phản xạ, tám hướng ứng viên cộng lại. Với từng ứng viên, nó check tam giác finder, và ứng viên đầu tiên có ba finder rơi đúng vào vị trí trên-trái, trên-phải và dưới-trái chính là hướng thật. Từ đó trở đi, pipeline hiện có chạy nguyên vẹn, vì các format information bit, cách đặt dữ liệu zigzag và Reed-Solomon correction đều mặc định một ma trận chuẩn tắc — và giờ thì chúng có đúng cái đó
Hai tính chất khiến chuyện này rẻ. Ma trận nhỏ so với bitmap đã render, nên tám lần transpose rẻ hơn xa tám lần render page. Và ma trận là một mảng boolean sạch do sampler dựng nên, nên không phép transform nào trên đường đi có thể giới thiệu những giá trị chưa từng được sample
Detect version là một phép tìm khả chia hết, không phải một phép chia
Module count không suy ra được bằng cách lấy width đã sample chia cho một module size giả định, và làm sai chỗ này là một nguồn decode failure tinh vi trên các bản render độ phân giải cao. Một QR symbol version v rộng 4v + 17 module, nên version 1 là 21 module còn version 40 là 177. Một mask đo 126 pixel rộng khớp ngang nhau với version 1 sáu pixel mỗi module và với vài version cao hơn ở module size nhỏ hơn. Phép chia tuyến tính chọn một trong số chúng và thường chọn sai
Cái chạy được là một phép tìm khả chia hết trên các version ứng viên. Đi từ version 40 xuống version 1, giữ những ứng viên mà module count chia hết width đã sample và chừa ít nhất ba pixel mỗi module, rồi lấy version nhỏ nhất còn sót. Sàn ba pixel là thứ chặn phép tìm khỏi chấp nhận một cách đọc dày vô lý của một symbol thô, và luật version nhỏ nhất phân xử phần mơ hồ còn lại theo hướng mà một scanner thật sự sẽ đọc ra
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 trả về một record đã điền sẵn chứ không phải một record toàn số 0, điều này có ý nghĩa vì một DPI bằng 0 hay một result cap bằng 0 là những cách trông rất hợp lệ để nhận lại không gì cả. RotationPolicy chỉ điều khiển retry bên ngoài: bdrpNone render một lần, bdrpFallback thử lại các hướng còn lại sau khi pass đầu fail, và bdrpAll render mọi hướng vô điều kiện. Vì normalize QR xảy ra bên trong decoder, các page QR resolve ngay từ attempt đầu dưới bất kỳ policy nào trong ba. Policy tồn tại là cho những symbology tuyến tính thật sự cần nó
Làm sao chứng minh một bitmap transform không bịa ra pixel?
Đếm mực ở cả hai phía và đòi hai tổng khớp nhau. Một phép xoay là một hoán vị của pixel, chẳng hơn gì nữa, nên số cell khác 0 trong output phải bằng số trong input. Khi một phép xoay mask trên đường retry bên ngoài báo 4800 cell set lúc vào và 7439 lúc ra, đúng một phép so sánh đó đủ để kết tội transform mà chưa cần đọc lấy một dòng geometry của nó
Nguyên nhân thì tầm thường và đáng mang về làm luật. Một dynamic array cấp bằng SetLength không được bảo đảm đến tay đã về 0 khi nó là function result đi qua một đường runtime không dọn, và những cell phép xoay không ghi sẽ mang theo byte nào từng nằm ở đó. Vài byte cũ đó khác 0, và khác 0 nghĩa là có mực. Fix là một dòng, FillChar(Result[0], N, 0) trước khi vòng hoán vị chạy, và kỷ luật nó hàm ý thì rộng hơn: bất kỳ hàm nào trả về mask hay bitmap buffer cũng nên tự dọn output tường minh thay vì dựa vào allocation semantics
Cái khiến defect sống sót qua ba bản release còn thú vị hơn chính defect. Sau khi QR chuyển việc xử lý orientation vào trong decoder, QR không còn đụng tới phép xoay mask bên ngoài nữa, và consumer duy nhất còn lại của code path đó là Code 39. Shared infrastructure che giấu bug kiểu này hoài: coverage từ một tính năng làm một đường trông như đã được test trong khi tính năng thật sự phụ thuộc vào đường đó thì không có tí coverage nào của riêng nó. Mọi đường một tính năng mới ngừng dùng đều cần một test vẫn dùng nó
Đọc kết quả về tọa độ page
Mọi giá trị hình học decoder sinh ra đều nằm trong hệ tọa độ của attempt bitmap, và caller cần chúng trong PDF user space. Phép chuyển đó chạy hai giai đoạn: gỡ góc tư mà retry đã áp, rồi gỡ render transform đã map user space lên bitmap. Cái đến tay bạn trong THPDFDecodedBarcode là một axis-aligned bounding box trong user space, với Left, Bottom, Right và Top theo quy ước PDF là Y tăng lên phía trên, cộng thêm một OrientationDegrees đo ngược chiều kim đồng hồ
Làm sai chiều của phép chuyển thứ hai thì triệu chứng rất khó chịu: text decode hoàn hảo, nhưng cái box bạn vẽ cho một review overlay đáp xuống ảnh mirror của vị trí đúng. Ai đang dựng review interface trên decoder nên assert với một fixture đã biết, đặt symbol chủ ý gần một góc page để một trục Y bị lật thấy được ngay một cái nhìn. Cùng lý lẽ đó áp dụng cho mọi tọa độ vượt qua ranh giới rendering, vì thế render một page PDF ra bitmap trong Delphi đáng hiểu kỹ trước khi bạn dựng lên trên decoder
Decoder có sẵn làm được gì và không làm gì
Decoder có sẵn là một bản triển khai có chặn, không dependency, và thẳng thắn về giới hạn của nó thay vì suy giảm một cách lặng lẽ. Nó nhận Code 39 và QR, validate các format bit được BCH bảo hộ cùng mask pattern trước khi công bố bất kỳ dữ liệu nào, và nó không cố recovery lỗi trên symbol hư hại. Nếu input của bạn là ảnh chụp một nhãn cong dưới ánh sáng chập chờn, đó là một lớp bài toán khác và cần một engine chuyên dụng
// Thay engine của riêng bạn: implement IHPDFBarcodeDecoder rồi truyền
// vào overload nhận decoder. HotPDF vẫn nắm page rendering,
// budget, coordinate mapping và de-duplication
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 là nơi một production pipeline chứng minh giá trị của nó. RotationAttemptCount và DecoderCallCount cho biết outer retry có chạy hay chưa, ReceivedResultCount đặt cạnh AcceptedResultCount tách được một decoder không tìm thấy gì khỏi một confidence threshold từ chối mọi thứ nó tìm được, còn RenderedPixels cùng PeakWorkingBytes là thứ bạn vẽ đồ thị khi một batch job bắt đầu thrashing bộ nhớ. Một result set rỗng kèm bdsSucceeded nghĩa là page thật sự không có symbol đọc được — một sự thật vận hành khác hẳn bdsBudgetExceeded
Các budget field xứng đáng một quyết định có chủ ý hơn là nhận default. MaxPixels và MaxWorkingBytes tồn tại vì DPI nhân theo bình phương: chuyển từ 300 lên 600 DPI trên một page A4 gấp bốn cả chi phí render lẫn peak allocation, và một input không đáng tin khai một page box khổng lồ có thể biến một job scan thành một sự cố out-of-memory. Hãy set cap theo cái document hợp lệ tệ nhất của bạn cần, rồi để bdsBudgetExceeded chuyển các case ngoại lệ sang một đường chậm hơn, cách ly hơn
Nếu document của bạn trộn nhãn machine-readable với text in ra mà bạn định index, barcode decoder kết đôi rất tự nhiên với recognition engine nói trong template-matching OCR bên trong HotPDF, còn phía sinh ra mã của cùng câu chuyện nằm ở vẽ barcode vào một PDF với HotPDF. Cả hai chạy trên cùng hạ tầng rendering và budget, nên một pipeline đã set giới hạn hợp lý cho bên này thì nhận bên kia gần như miễn phí
Rotation tolerance là kiểu tính năng vô hình khi chạy đúng và điên đầu khi chạy sai, và bài học kỹ thuật của nó vượt ra ngoài QR: normalize càng gần biểu diễn semantic càng tốt, đừng ở tầng pixel nơi dữ liệu còn mang theo mọi tai nạn của quá trình thu được nó. HotPDF ship tính năng này như một phần của HotPDF Delphi PDF component, cạnh các mảnh rendering, OCR và page analysis mà các intake pipeline thường cần