Articol tehnic

Decodarea codurilor QR rotite din paginile PDF cu HotPDF

HotPDF decodează simbolurile QR rotite dintr-o pagină PDF încărcată normalizând matricea de module eșantionată prin toate cele opt orientări D4, în interiorul decoder-ului însuși. Reîncercarea externă prin rotire, care funcționează pentru simbologiile liniare, nu poate funcționa pentru QR, iar înțelegerea motivului vă economisește o zi de vânătoare după un decoder care pare stricat, dar nu e

Scenariul e suficient de obișnuit. Avize de livrare scanate sosesc ca PDF-uri, fiecare pagină poartă o etichetă QR, iar operatorul de scaner a băgat un teanc de foi în direcția în care a acceptat-o tava. Unele etichete sunt verticale, altele decalate cu un sfert de tură, câteva sunt cu capul în jos. Apelați decoder-ul de coduri de bare, jumătate din pagini se rezolvă, iar cealaltă jumătate revine goală, fără nicio eroare

De ce rotirea măștii de scanare nu repară niciodată un QR rotit?

Pentru că aranjamentul pattern-urilor de localizare QR e deliberat asimetric, iar o rotire a întregii imagini păstrează asimetria în loc să o înlăture. QR Code plasează trei pătrate de localizare la colțurile stânga-sus, dreapta-sus și stânga-jos și lasă colțul dreapta-jos gol (ISO/IEC 18004:2015 §6.3.3). Colțul acela lipsă e indiciul de orientare. Rotiți bitmap-ul paginii cu nouăzeci de grade, iar golul pur și simplu se mută într-un alt colț. Nu există nicio rotire netrivială a planului care să mapeze un aranjament cu trei colțuri înapoi pe el însuși, deci un decoder care acceptă doar aranjamentul canonic va respinge fiecare încercare la rând

Asta contează pentru că remedierea evidentă e cea greșită. Instinctul natural e să atârnați reîncercarea în exterior: randati pagina, dați masca decoder-ului, iar dacă eșuează, rotiți masca și încercați din nou la 90, 180 și 270 de grade. Pentru Code 39 politica aceea e exact cea corectă, pentru că o simbologie liniară are un pattern de start și stop pe care scanerul îl găsește imediat ce barele aleargă orizontal. Pentru QR e patru eșecuri garantate, urmate de un raport de „nimic găsit”

Grupul D4, aplicat matricei de module

Locul corect pentru normalizare e după eșantionare, pe grila booleană de module, nu pe masca de pixeli. Odată ce decoder-ul a rezolvat simbolul într-o matrice de n pe n de module întunecate și luminoase, poate enumera grupul diedral al pătratului: patru rotații ori două reflexii, opt orientări candidate în total. Pentru fiecare candidat verifică triunghiul de localizare, iar primul candidat ai cărui trei finder-e cad în pozițiile stânga-sus, dreapta-sus și stânga-jos e orientarea adevărată. De acolo pipeline-ul existent rulează neschimbat, pentru că biții de informație de format, plasarea în zigzag a datelor și corecția Reed-Solomon presupun toate o matrice canonică și acum primesc una

Patru reprezentări ale aceleiași matrice de module QR HotPDF sub rotațiile grupului D4 la 0, 90, 180 și 270 de grade, arătând cele trei pattern-uri de localizare migrând între colțuri în timp ce colțul gol se mută împreună cu ele, astfel încât doar orientarea canonică prezintă decoder-ului finder-e la stânga-sus, dreapta-sus și stânga-jos
Rotirea măștii de pixeli nu poate înlătura asimetria finder-elor QR, deci HotPDF enumera orientările D4 pe matricea de module eșantionată și păstrează primul candidat ai cărui finder-e cad la stânga-sus, dreapta-sus și stânga-jos

Două proprietăți fac asta ieftin. Matricea e mică pe lângă bitmap-ul randat, deci opt transpuneri costă cu mult mai puțin decât opt randări de pagină. Iar matricea e un vector boolean curat, construit de eșantionator, deci nicio transformare pe parcurs nu poate introduce valori care n-au fost niciodată eșantionate

Detectarea versiunii e o căutare de divizibilitate, nu o diviziune

Numărul de module nu poate fi derivat împărțind lățimea eșantionată la o mărime presupusă de modul, iar a greși aici e o sursă subtilă de eșecuri de decodare la randări de rezoluție mare. Un simbol QR de versiune v are 4v + 17 module pe lățime, deci versiunea 1 are 21 de module, iar versiunea 40 are 177. O mască care măsoară 126 de pixeli în lățime e la fel de compatibilă cu versiunea 1 la șase pixeli per modul și cu mai multe versiuni mai mari la mărimi de modul mai mici. Diviziunea liniară alege una dintre ele și de obicei greșește

Ce funcționează e o căutare de divizibilitate peste versiunile candidate. Parcurgeți de la versiunea 40 în jos până la versiunea 1, păstrați candidații al căror număr de module divide lățimea eșantionată exact și lasă cel puțin trei pixeli per modul și luați cea mai mică versiune supraviețuitoare. Plafonul de trei pixeli e cel care împiedică căutarea să accepte o lectură absurd de densă a unui simbol grosier, iar regula celei mai mici versiuni rezolvă ambiguitatea rămasă în favoarea lecturii pe care un scaner ar produce-o efectiv

Parcurgerea de detectare a versiunii HotPDF pentru un simbol QR pe o mască eșantionată de 126 de pixeli, testând fiecare număr candidat de module 4v plus 17 de la versiunea 40 în jos până la versiunea 1 pentru divizibilitate exactă și un plafon de trei pixeli per modul, înainte să câștige cea mai mică versiune supraviețuitoare
Numărul de module QR vine dintr-o căutare de divizibilitate peste versiunile candidate, nu din împărțirea lățimii măștii la o mărime presupusă de modul, iar cea mai mică versiune supraviețuitoare rezolvă ambiguitatea
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 întoarce o înregistrare populată, nu una zero, ceea ce contează pentru că un DPI de zero sau un plafon de rezultate zero e un mod cu aspect valid de a nu primi nimic înapoi. RotationPolicy controlează doar reîncercarea externă: bdrpNone randează o dată, bdrpFallback reîncearcă celelalte orientări după o primă trecere eșuată, iar bdrpAll randează fiecare orientare necondiționat. Pentru că normalizarea QR se întâmplă în interiorul decoder-ului, paginile QR se rezolvă la prima încercare sub oricare dintre cele trei politici. Politica e acolo pentru simbologiile liniare care chiar au nevoie de ea

Cum dovediți că o transformare de bitmap nu inventează pixeli?

Numărați cerneala de pe ambele părți și cereți totalurilor să se potrivească. O rotație e o permutare de pixeli, nimic mai mult, deci numărul de celule non-zero din ieșire trebuie să fie egal cu cel din intrare. Când o rotire de mască în calea externă de reîncercare a raportat 4800 de celule setate la intrare și 7439 la ieșire, comparația aceasta unică a fost de ajuns să condamne transformarea fără să citească vreo linie din geometria ei

Cauza era banală și merită dusă mai departe ca regulă. Un vector dinamic dimensionat cu SetLength nu are garanția că sosește zero-iz când e rezultatul unei funcții care călătorește pe o cale pe care runtime-ul nu o curăță, iar celulele pe care rotația nu le scrie niciodată poartă apoi orice octeți erau acolo înainte. O parte dintre acei octeți perimați sunt non-zero, iar non-zero înseamnă cerneală. Remedierea e de o linie, FillChar(Result[0], N, 0) înainte ca bucla de permutare să pornească, iar disciplina pe care o implică e mai largă: orice funcție care întoarce o mască sau un buffer de bitmap ar trebui să-și curățe explicit ieșirea, în loc să se bazeze pe semantica alocării

Ce a făcut defectul să supraviețuiască trei versiuni e mai interesant decât defectul. Odată ce QR și-a mutat tratarea orientării în decoder, QR a încetat să mai exerseze cu totul rotația externă de mască, iar singurul consumator rămas al acelei căi de cod era Code 39. Infrastructura partajată ascunde bug-uri ca ăsta tot timpul: acoperirea dintr-o trăsătură face o cale să pară testată, în timp ce trăsătura care chiar depinde de ea nu are nimic al ei. Fiecare cale pe care o trăsătură nouă încetează să o mai folosească are nevoie de un test care încă o folosește

Citirea rezultatelor înapoi în coordonate de pagină

Fiecare valoare geometrică produsă de decoder e exprimată în sistemul de coordonate al bitmap-ului încercării, iar apelantul are nevoie de ea în spațiul utilizator PDF. Conversia rulează în două etape: anulați sfertul de tură aplicat de reîncercare, apoi anulați transformarea de randare care a mapat spațiul utilizator pe bitmap. Ce sosește în THPDFDecodedBarcode e o cutie încadratoare aliniată pe axe în spațiul utilizator, cu Left, Bottom, Right și Top după convenția PDF că Y crește în sus, plus un OrientationDegrees în sens antiorar

Pipeline-ul de coduri de bare HotPDF, de la bitmap-ul randat al paginii prin eșantionare într-o matrice booleană de module, normalizare D4, detectarea versiunii prin divizibilitate și decodare Reed-Solomon, apoi conversia de coordonate în două etape care anulează sfertul de tură al reîncercării și transformarea de randare înainte ca THPDFDecodedBarcode să publice Left, Bottom, Right, Top și OrientationDegrees în spațiul utilizator
Normalizarea QR în interiorul decoder-ului lasă paginile să se rezolve la prima încercare, în timp ce conversia de coordonate în două etape transformă rezultatele din bitmap-ul încercării în cutii aliniate pe axe în spațiul utilizator

Greșiți direcția acelei a doua conversii, iar simptomul e neplăcut: textul se decodează perfect, dar cutia pe care o desenați pentru un overlay de revizuire cade pe imaginea în oglindă a poziției corecte. Oricine construiește o interfață de revizuire peste decoder ar trebui să afirme contra unui fixture cunoscut, cu un simbol plasat deliberat lângă un colț al paginii, astfel încât o axă Y întoarsă să se vadă dintr-o privire. Același raționament se aplică oricărei coordonate care traversează granița randării, motiv pentru care randarea unei pagini PDF într-un bitmap în Delphi merită înțeleasă înainte să construiți peste decoder

Ce face și ce nu face decoder-ul integrat

Decoder-ul integrat e o implementare cu limite și fără dependențe și e sincer în privința limitelor lui, în loc să degradeze pe furiș. Recunoaște Code 39 și QR, validează biții de format protejați BCH și pattern-ul de mască înainte să publice orice date și nu încearcă recuperarea de erori pe simboluri deteriorate. Dacă intrarea dumneavoastră e fotografia unei etichete curbate sub lumină inegală, aceea e o altă clasă de problemă și vrea un motor specializat

// Schimbați cu propriul motor: implementați IHPDFBarcodeDecoder și passați-l
// overload-ului care știe de decoder. HotPDF deține în continuare randarea
// paginii, bugetele, maparea coordonatelor și deduplicarea
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 e locul în care un pipeline de producție își câștigă pâinea. RotationAttemptCount și DecoderCallCount vă spun dacă reîncercarea externă a rulat deloc, ReceivedResultCount contra AcceptedResultCount separă un decoder care n-a găsit nimic de un prag de încredere care a respins tot ce a găsit, iar RenderedPixels cu PeakWorkingBytes e ce desenați în grafic când un job de lot începe să se scuipească. Un set de rezultate gol plus bdsSucceeded înseamnă că pagina chiar nu are niciun simbol lizibil, ceea ce e alt fapt operațional decât bdsBudgetExceeded

Câmpurile de buget merită o decizie deliberată, nu un implicit. MaxPixels și MaxWorkingBytes există pentru că DPI-ul se înmulțește pătratic: trecerea de la 300 la 600 DPI pe o pagină A4 patrulează și costul de randare și alocarea de vârf, iar o intrare neverificabilă care declară o cutie de pagină enormă poate transforma un job de scanare într-un incident de out-of-memory. Setați plafoanele la ce are nevoie cel mai rău document legitim al dumneavoastră, apoi lăsați bdsBudgetExceeded să dirijeze valorile aberante spre o cale mai lentă și izolată

Dacă documentele dumneavoastră amestecă etichete lizibile de mașină cu text tipărit pe care plănuiți să îl indexați, decoder-ul de coduri de bare se potrivește natural cu motorul de recunoaștere acoperit în OCR-ul cu potrivire de șabloane din HotPDF, iar partea de generare a aceleiași povești stă în desenarea codurilor de bare într-un PDF cu HotPDF. Ambele rulează pe aceeași infrastructură de randare și bugete, deci un pipeline care stabilește deja limite sănătoase pentru unul primește celălalt aproape gratis

Toleranța la rotire e una dintre trăsăturile care sunt invizibile când funcționează și enervante când nu, iar lecția de inginerie se generalizează dincolo de QR: normalizați cât mai aproape de reprezentarea semantică, nu la stratul de pixeli, unde datele mai poartă fiecare accident al modului în care au fost capturate. HotPDF livrează asta ca parte din HotPDF Delphi PDF component, alături de piesele de randare, OCR și analiză de pagină de care aceleași pipeline-uri de ingestie au de obicei nevoie