Technisch artikel

Gedraaide QR-codes in PDF-pagina's decoderen met HotPDF

HotPDF decodeert gedraaide QR-symbolen in een geladen PDF-pagina door de gesamplede modulematrix binnen de decoder zelf door alle acht D4-oriëntaties te normaliseren. De externe rotatie-retry die voor lineaire symbologieën werkt, kan niet werken voor QR, en begrijpen waarom scheelt u een dag jagen op een decoder die kapot lijkt maar het niet is

Het scenario is alledaags genoeg. Gescande pakbonnen komen als PDF's binnen, elke pagina draagt een QR-label, en de scanneroperator stopte een stapel vellen in welke richting de lade ook accepteerde. Sommige labels staan rechtop, soms staat er een kwartslag naast, en een paar staat op zijn kop. U roept de barcode-decoder aan, de helft van de pagina's resolved, en de andere helft komt leeg terug zonder enige foutmelding

Waarom lost het draaien van de scanmask nooit een gedraaide QR op?

Omdat een QR finder pattern-layout opzettelijk asymmetrisch is, en het roteren van de hele afbeelding die asymmetrie behoudt in plaats van wegneemt. QR Code plaatst drie finder-vierkanten op de hoeken linksboven, rechtsboven en linksonder, en laat de hoek rechtsonder leeg (ISO/IEC 18004:2015 §6.3.3). Die ontbrekende hoek is de oriëntatie-aanwijzing. Draait u de pagina-bitmap negentig graden, dan schuift het gat gewoon naar een andere hoek. Er is geen niet-triviale rotatie van het vlak die een driehoeksindeling op zichzelf terugafbeeldt, dus een decoder die alleen de canonieke ordening accepteert, wijst elke poging op zijn beurt af

Dat is relevant omdat de voor de hand liggende fix de verkeerde is. De natuurlijke reflex is de retry buiten hangen: render de pagina, geef de mask aan de decoder, en als dat faalt, draai de mask en probeer het opnieuw voor 90, 180 en 270 graden. Voor Code 39 is dat beleid precies goed, want een lineaire symbologie heeft een start- en stoppatroon dat de scanner kan vinden zodra de balken horizontaal lopen. Voor QR is het vier gegarandeerde mislukkingen gevolgd door de melding niets gevonden

De D4-groep, toegepast op de modulematrix

De juiste plek voor de normalisatie is na het samplen, op het booleaanse modulegrid in plaats van op de pixelmask. Zodra de decoder het symbool heeft geresolved tot een n bij n-matrix van donkere en lichte modules, kan hij de dihedrale groep van het vierkant opsommen: vier rotaties keer twee spiegelingen, acht kandidaat-oriëntaties in totaal. Per kandidaat checkt hij de finder-driehoek, en de eerste kandidaat waarvan de drie finders op linksboven, rechtsboven en linksonder belanden, is de echte oriëntatie. Vanaf daar draait de bestaande pipeline ongewijzigd door, want de format information bits, de zigzag-dataplaatsing en Reed-Solomon-correctie gaan allemaal uit van een canonieke matrix en krijgen er nu één

Vier weergaven van dezelfde HotPDF QR-modulematrix onder de rotaties van de D4-groep over 0, 90, 180 en 270 graden, waarop de drie finder-patronen van hoek wisselen terwijl de lege hoek met hen meebeweegt, zodat alleen de canonieke oriëntatie finders op linksboven, rechtsboven en linksonder aan de decoder voorschotelt
Het draaien van de pixelmask kan de QR-finder-asymmetrie niet wegnemen, dus HotPDF somt de D4-oriëntaties op de gesamplede modulematrix op en houdt de eerste kandidaat aan waarvan de finders op linksboven, rechtsboven en linksonder belanden

Twee eigenschappen maken dit goedkoop. De matrix is klein vergeleken bij de gerenderde bitmap, dus acht transposities kosten veel minder dan acht paginarenders. En de matrix is een kale booleaanse array gebouwd door de sampler, dus geen enkele transform onderweg kan waarden introduceren die nooit gesampled zijn

Versiedetectie is een deelbaarheidszoeker, geen deling

Het moduleaantal valt niet af te leiden door de gesamplede breedte te delen door een aangenomen modulegrootte, en dit verkeerd doen is een subtiele bron van decode-failures bij hoogresolutie-renders. Een QR-symbool van versie v is 4v + 17 modules breed, dus versie 1 is 21 modules en versie 40 is 177. Een mask van 126 pixels breed is evengoed verenigbaar met versie 1 op zes pixels per module als met verschillende hogere versies op kleinere modulegroottes. Lineair delen pikt er één uit en zit er meestal naast

Wat werkt, is een deelbaarheidszoeker over de kandidaatversies. Loop van versie 40 omlaag naar versie 1, houd de kandidaten aan waarvan het moduleaantal de gesamplede breedte restloos deelt en ten minste drie pixels per module overlaat, en neem de kleinste overlevende versie. De drempel van drie pixels voorkomt dat de zoektocht een absurd dichte lezing van een grof symbool accepteert, en de kleinste-versie-regel lost de resterende ambiguïteit op ten faveure van de lezing die een scanner werkelijk zou produceren

De versiedetectiewandeling van HotPDF voor een QR-symbool op een gesamplede mask van 126 pixels, waarbij elke kandidaat-moduletelling 4v plus 17 van versie 40 omlaag naar versie 1 wordt getest op restloze deelbaarheid en een moduledrempel van drie pixels voordat de kleinste overlevende versie wint
Een QR-moduletelling komt uit een deelbaarheidszoeker over kandidaatversies, niet uit het delen van de maskbreedte door een aangenomen modulegrootte, en de kleinste overlevende versie lost de ambiguïteit op
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 geeft een gevuld record terug in plaats van een leeggezet record, en dat is belangrijk want een DPI van nul of een resultaatslimiet van nul is een plausibel ogende manier om niets terug te krijgen. RotationPolicy stuurt alleen de externe retry: bdrpNone rendert één keer, bdrpFallback probeert de andere oriëntaties opnieuw na een mislukte eerste pas, en bdrpAll rendert elke oriëntatie onvoorwaardelijk. Omdat QR-normalisatie binnen de decoder gebeurt, komen QR-pagina's bij elk van de drie beleidsregels al bij de eerste poging uit. Het beleid is er voor de lineaire symbologieën die het echt nodig hebben

Hoe bewijst u dat een bitmaptransform geen pixels verzint?

Tel de inkt aan beide kanten en eis dat de totalen matchen. Een rotatie is een permutatie van pixels, niets meer, dus het aantal niet-nul cellen in de output moet gelijk zijn aan het aantal in de input. Toen een maskrotatie in het externe retry-pad 4800 gezette cellen meldde bij de ingang en 7439 bij de uitgang, was die ene vergelijking genoeg om de transform te veroordelen zonder ook maar één regel van zijn geometrie te lezen

De oorzaak was alledaags en de moeite waard als regel mee te nemen. Een dynamische array op maat gezet met SetLength komt niet gegarandeerd op nul gezet aan als hij een functieresultaat is dat een pad afloopt dat de runtime niet schoonmaakt, en cellen die de rotatie nooit beschrijft dragen dan de bytes die er eerder stonden. Sommige van die achtergebleven bytes zijn niet-nul, en niet-nul betekent inkt. De fix is één regel, FillChar(Result[0], N, 0) voordat de permutatielus start, en de discipline die dat impliceert is breder: elke functie die een mask- of bitmapbuffer teruggeeft, moet zijn output expliciet leegmaken in plaats van op allocatiesemantiek te vertrouwen

Wat het defect drie releases liet overleven, is interessanter dan het defect. Zodra QR zijn oriëntatieafhandeling naar de decoder verhuisde, raakte QR het externe mask-rotatiepad helemaal kwijt als exercitie, en de enige overgebleven consument van die coderoute was Code 39. Gedeelde infrastructuur verbergt dit soort bugs continu: coverage van de ene feature laat een pad getest lijken terwijl de feature die er werkelijk van afhangt er geen eigen coverage heeft. Elke route die een nieuwe feature niet meer gebruikt, heeft een test nodig die hem nog wel gebruikt

De resultaten teruglezen in paginacoördinaten

Elke geometrische waarde die de decoder produceert, staat in het coördinaatkader van de attempt-bitmap, en de aanroeper heeft hem nodig in PDF user space. Die conversie verloopt in twee fasen: maak eerst de kwartslag ongedaan die de retry toepaste, en daarna de rendertransform die user space op de bitmap afbeeldde. Wat er in THPDFDecodedBarcode aankomt, is een axis-aligned bounding box in user space, met Left, Bottom, Right en Top volgens de PDF-conventie dat Y omhoog groeit, plus een OrientationDegrees die tegen de klok in loopt

De HotPDF barcode-pipeline van gerenderde pagina-bitmap via sampling naar een booleaanse modulematrix, D4-normalisatie, deelbaarheidsversiedetectie en Reed-Solomon-decoding, en daarna de tweefasen-coördinaatconversie die de kwartslag van de retry en de rendertransform ongedaan maakt voordat THPDFDecodedBarcode Left, Bottom, Right, Top en OrientationDegrees in user space publiceert
QR-normalisatie binnen de decoder laat pagina's bij de eerste poging al slagen, terwijl de tweefasen-coördinaatconversie attempt-bitmapresultaten omzet in axis-aligned user space-boxes

De richting van die tweede conversie verkeerd hebben en het symptoom is gemeen: de tekst decodeert feilloos, maar de box die u tekent voor een review overlay belandt op het spiegelbeeld van de juiste positie. Wie een review-interface bovenop de decoder bouwt, moet asserten tegen een bekende fixture, met een symbool bewust dicht bij een paginahoek geplaatst zodat een gespiegelde Y-as in één oogopslag zichtbaar is. Dezelfde redenering geldt voor elke coördinaat die de rendergrens oversteekt, en dat is waarom een PDF-pagina naar een bitmap renderen in Delphi de moeite van begrijpen waard is voordat u bovenop de decoder bouwt

Wat de ingebouwde decoder wel en niet doet

De ingebouwde decoder is een afgebakende, afhankelijkheidsvrije implementatie, en hij is eerlijk over zijn grenzen in plaats van stilletjes te degraderen. Hij herkent Code 39 en QR, valideert de BCH-beschermde format bits en het maskpatroon voordat hij enige data publiceert, en hij probeert geen foutherstel op beschadigde symbolen. Is uw input een foto van een krom etiket bij wisselend licht, dan is dat een andere probleemklasse en vraagt die om een gespecialiseerde engine

// Koppel uw eigen engine aan: implementeer IHPDFBarcodeDecoder en geef
// hem mee aan de overload die een decoder accepteert. HotPDF blijft
// eigenaar van paginarendering, budgets, coördinaatmapping en de-duplicatie
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 is waar een productiepipeline zijn brood verdient. RotationAttemptCount en DecoderCallCount vertellen of de externe retry überhaupt draaide, ReceivedResultCount naast AcceptedResultCount scheidt een decoder die niets vond van een confidence-drempel die alles weigerde wat hij vond, en RenderedPixels met PeakWorkingBytes is wat u in een grafiek zet op het moment dat een batchjob begint te sputteren. Een lege resultaatset plus bdsSucceeded betekent dat de pagina werkelijk geen leesbaar symbool heeft, en dat is een andere operationele vaststelling dan bdsBudgetExceeded

De budgetvelden verdienen een bewuste beslissing in plaats van een default. MaxPixels en MaxWorkingBytes bestaan omdat DPI kwadratisch vermenigvuldigt: van 300 naar 600 DPI op een A4-pagina verviervoudigt zowel de renderkosten als de piekallocatie, en een untrusted input die een enorm page box opgeeft, kan van een scanjob een out-of-memory-incident maken. Zet de limieten op wat uw ergste legitieme document nodig heeft, en laat bdsBudgetExceeded de uitschieters naar een tragere, geïsoleerde route sturen

Als uw documenten machineleesbare labels mengen met gedrukte tekst die u wilt indexeren, paart de barcode-decoder vanzelfsprekend met de herkenningsengine uit template-matching OCR in HotPDF, en de generatiekant van hetzelfde verhaal staat in barcodes in een PDF tekenen met HotPDF. Beide draaien op dezelfde rendering- en budgetinfrastructuur, dus een pipeline die al zinnige limieten voor de één zet, krijgt de ander vrijwel gratis

Rotatietolerantie is zo'n feature die onzichtbaar is als hij werkt en woedend maakt als hij niet werkt, en de engineeringles veralgemeent voorbij QR: normaliseer zo dicht mogelijk bij de semantische representatie, niet op de pixellaag waar de data nog elk toeval draagt van hoe hij werd vastgelegd. HotPDF levert dit als onderdeel van de HotPDF Delphi PDF component, naast de rendering-, OCR- en pagina-analyseonderdelen die dezelfde intake-pipelines doorgaans nodig hebben