Teknisk artikel

Dekod roterede QR-koder i PDF-sider med HotPDF

HotPDF dekoder roterede QR-symboler i en indlæst PDF-side ved at normalisere den samplessede modulematrix gennem alle otte D4-orienteringer inde i decoderen selv. Den ydre rotation-retry, der virker for lineære symbologier, kan ikke virke for QR, og at forstå hvorfor sparer dig for en dag med at jagte en decoder, der ser ødelagt ud, men ikke er det

Scenariet er ordinært nok. Scannede følgesedler ankommer som PDF'er, hver side bærer et QR-label, og scanneroperatøren fedtede en stak ark i den retning, bakken accepterede. Nogle labels står opret, nogle er en kvart drejning ved siden af, nogle få står på hovedet. Du kalder barcode-decoderen, halvdelen af siderne resolver, og den anden halvdel kommer tomme tilbage uden nogen fejl overhovedet

Hvorfor fikser rotation af scan-masken aldrig en roteret QR?

Fordi et QR finder pattern-layout er bevidst asymmetrisk, og en helbilledsrotation bevarer den asymmetri i stedet for at fjerne den. QR Code placerer tre finder-kvadrater i hjørnerne øverst-venstre, øverst-højre og nederst-venstre og efterlader nederst-højre hjørne tomt (ISO/IEC 18004:2015 §6.3.3). Det manglende hjørne er orienteringsvinket. Rotér side-bitmapmen halvfems grader, og hullet flytter bare til et andet hjørne. Der findes ingen ikke-triviel rotation af planet, der afbilder et trehjørne-layout tilbage på sig selv, så en decoder, der kun accepterer det kanoniske arrangement, afviser hvert forsøg i tur

Dette betyder noget, for det åbenlyse fix er det forkerte. Naturligt instinkt er at hænge retry'en på ydersiden: render siden, giv masken til decoderen, og hvis det fejler, rotér masken og prøv igen for 90, 180 og 270 grader. For Code 39 er den politik præcis rigtig, for en lineær symbologi har et start- og stopmønster, scanneren kan finde, så snart stregerne løber vandret. For QR er det fire garanterede fiaskoer efterfulgt af en melding om intet fundet

D4-gruppen, anvendt på modulematricen

Det korrekte sted for normaliseringen er efter sampling, på det booleanske modulegrid snarere end på pixel-masken. Når decoderen har opløst symbolet til en n gange n-matrix af mørke og lyse moduler, kan den enumerere kvadratets dihedrale gruppe: fire rotationer gange to refleksioner, otte kandidat-orienteringer i alt. For hver kandidat tjekker den finder-trekanten, og den første kandidat, hvis tre finders lander i positionerne øverst-venstre, øverst-højre og nederst-venstre, er den sande orientering. Derfra kører den eksisterende pipeline uændret, for format information-bitsene, zigzag-dataplaceringen og Reed-Solomon-korrektionen antager alle en kanonisk matrix og får nu én

Fire renderinger af samme HotPDF QR-modulematrix under D4-gruppens rotationer ved 0, 90, 180 og 270 grader, der viser de tre finder patterns vandre mellem hjørner, mens det tomme hjørne flytter sig med dem, så kun den kanoniske orientering præsenterer finders i øverst-venstre, øverst-højre og nederst-venstre for decoderen
Rotation af pixel-masken kan ikke fjerne QR finder-asymmetrien, så HotPDF enumererer D4-orienteringerne på den samplessede modulematrix og beholder den første kandidat, hvis finders lander øverst-venstre, øverst-højre og nederst-venstre

To egenskaber gør dette billigt. Matricen er lille sammenlignet med den renderede bitmap, så otte transponeringer koster langt mindre end otte siderenderinger. Og matricen er et rent booleansk array bygget af sampleren, så ingen transform undervejs kan introducere værdier, der aldrig blev samplet

Versionsdetektion er en delelighedssøgning, ikke en division

Moduletallet kan ikke udledes ved at dividere den samplessede bredde med en antaget modulstørrelse, og at tage fejl her er en diskret kilde til decode-fejl ved højopløste renderinger. Et QR-symbol af version v er 4v + 17 moduler på tværs, så version 1 er 21 moduler og version 40 er 177. En maske, der måler 126 pixels på bredden, er lige konsistent med version 1 ved seks pixels per modul og med flere højere versioner ved mindre modulstørrelser. Lineær division vælger én af dem og tager som regel fejl

Det, der virker, er en delelighedssøgning over kandidatversionerne. Gå fra version 40 ned til version 1, behold kandidaterne, hvis modultal deler den samplessede bredde jævnt og efterlader mindst tre pixels per modul, og tag den mindste overlevende version. Tre-pixel-gulvet er det, der stopper søgningen fra at acceptere en absurd tæt aflæsning af et groft symbol, og mindste-version-reglen resolver den resterende tvetydighed til fordel for den aflæsning, en scanner reelt ville producere

HotPDF-versionsdetektionsgangen for et QR-symbol på en 126 pixel samplessede maske, der tester hvert kandidat-modultal 4v plus 17 fra version 40 ned til version 1 for jævn delelighed og et tre pixel modulgulv, før den mindste overlevende version vinder
Et QR-modultal kommer fra en delelighedssøgning over kandidatversioner, ikke fra at dividere maskebredden med en antaget modulstørrelse, og den mindste overlevende version resolver tvetydigheden
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 giver en udfyldt record tilbage snarere end en nulstillet, hvilket betyder noget, for en DPI på nul eller et resultatloft på nul er en plausibel måde at få intet tilbage på. RotationPolicy styrer kun det ydre retry: bdrpNone renderer én gang, bdrpFallback retrier de andre orienteringer efter et fejlet første forsøg, og bdrpAll renderer alle orienteringer ubetinget. Fordi QR-normalisering sker inde i decoderen, resolver QR-sider ved første forsøg under alle tre politikker. Politikken er der for de lineære symbologier, der reelt behøver den

Hvordan beviser du, at en bitmap-transform ikke opfinder pixels?

Tæl blækket på begge sider, og kræv, at totalerne matcher. En rotation er en permutation af pixels, intet mere, så antallet af ikke-nul-celler i output skal svare til antallet i input. Da en maske-rotation i den ydre retry-vej meldte 4800 satte celler ind og 7439 ud, var den eneste sammenligning nok til at dømme transformen skyldig uden at læse en eneste linje af dens geometri

Årsagen var banal og værd at tage med som regel. Et dynamisk array størrelsesbestemt med SetLength er ikke garanteret at ankomme nulstillet, når det er et funktionsresultat, der rejser en vej, runtime ikke rydder, og celler, rotationen aldrig skriver, bærer så de byte, der var der før. Nogle af de forældede byte er ikke-nul, og ikke-nul betyder blæk. Fixet er én linje, FillChar(Result[0], N, 0), før permutationsløkken kører, og disciplinen, det indebærer, er bredere: enhver funktion, der returnerer en maske- eller bitmap-buffer, bør rydde sit output eksplicit i stedet for at stole på allokering-semantik

Det, der fik defekten til at overleve tre releaser, er mere interessant end defekten. Da QR flyttede sin orienteringshåndtering ind i decoderen, holdt QR op med at bruge den ydre maske-rotation helt, og den eneste tilbageværende forbruger af den kodevej var Code 39. Delt infrastruktur skjuler bugs som denne hele tiden: dækning fra én feature får en vej til at se testet ud, mens den feature, der reelt afhænger af den, ingen har. Enhver vej, en ny feature holder op med at bruge, behøver en test, der stadig bruger den

Læse resultaterne tilbage i sidekoordinater

Hver geometrisk værdi, decoderen producerer, er udtrykt i koordinatrammen for forsøgs-bitmapmen, og kalderen behøver den i PDF user space. Konverteringen kører i to etaper: fortryd den kvarte drejning, retry'en anvendte, og fortryd derefter render-transformen, der afbildede user space over på bitmapmen. Det, der ankommer i THPDFDecodedBarcode, er en axis-aligned bounding box i user space, med Left, Bottom, Right og Top efter PDF-konventionen om, at Y vokser opad, plus en mod uret gående OrientationDegrees

HotPDF barcode-pipelinen fra renderet side-bitmap gennem sampling til en booleansk modulematrix, D4-normalisering, deleligheds-versionsdetektion og Reed-Solomon-dekodning, og derefter den to-etapers koordinatkonvertering, der fortryder retryens kvarte drejning og render-transformen, før THPDFDecodedBarcode publicerer Left, Bottom, Right, Top og OrientationDegrees i user space
QR-normalisering inde i decoderen lader sider resolve ved første forsøg, mens den to-etapers koordinatkonvertering forvandler forsøgs-bitmap-resultater til axis-aligned user space-bokse

Tager man fejl af retningen af den anden konvertering, er symptomet ubehageligt: tekst dekoder perfekt, men boksen, du tegner til et review-overlay, lander på spejlbilledet af den rigtige position. Enhver, der bygger et review-interface oven på decoderen, bør asserte mod en kendt fixture med et symbol placeret bevidst nær ét sidehjørne, så en flipped Y-akse er synlig ved et blik. Samme ræsonnement gælder enhver koordinat, der krydser renderingsgrænsen, hvilket er grunden til, at rendering af en PDF-side til en bitmap i Delphi er værd at forstå, inden du bygger oven på decoderen

Hvad den indbyggede decoder vil og ikke vil

Den indbyggede decoder er en bounded, afhængighedsfri implementation, og den er ærlig om sine grænser i stedet for at degradere lydløst. Den genkender Code 39 og QR, validerer de BCH-beskyttede formatbits og maskemønsteret, inden den publicerer nogen data, og den forsøger ikke error recovery på beskadigede symboler. Er dit input et fotografi af et buet label under ujævnt lys, er det en anden problemklasse og behøver en specialiseret engine

// Skift din egen engine ind: implementér IHPDFBarcodeDecoder og giv den
// til den decoder-bevidste overload. HotPDF ejer stadig siderendering,
// budgetter, koordinatkortlægning og de-duplikering
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 er der, hvor en produktionspipeline tjener sit brød. RotationAttemptCount og DecoderCallCount fortæller dig, om det ydre retry overhovedet kørte, ReceivedResultCount mod AcceptedResultCount adskiller en decoder, der ikke fandt noget, fra en confidence-tærskel, der afviste alt, hvad den fandt, og RenderedPixels med PeakWorkingBytes er det, du grafiserer, når et batchjob begynder at thrashe. Et tomt resultatsæt plus bdsSucceeded betyder, at siden reelt ikke har noget læsbart symbol, hvilket er en anden operationel kendsgerning end bdsBudgetExceeded

Budgetfelterne fortjener en bevidst beslutning snarere end en default. MaxPixels og MaxWorkingBytes findes, for DPI multiplicerer kvadratisk: fra 300 til 600 DPI på en A4-side firdobler både render-omkostningen og peak-allokeringen, og et utroværdigt input, der deklarerer en enorm page box, kan forvandle et scanjob til en out-of-memory-hændelse. Sæt lofterne til, hvad dit værste legitime dokument behøver, og lad så bdsBudgetExceeded route afvigerne til en langsommere, isoleret vej

Blander dine dokumenter maskinlæselige labels med trykt tekst, du planlægger at indeksere, parrer barcode-decoderen sig naturligt med genkendelsesmotoren dækket i template-matching OCR inde i HotPDF, og generationssiden af samme historie er i at tegne barcodes ind i en PDF med HotPDF. Begge kører på den samme renderings- og budgetinfrastruktur, så en pipeline, der allerede sætter fornuftige grænser for den ene, får den anden næsten gratis

Rotationstolerance er én af de features, der er usynlig, når den virker, og rasende irriterende, når den ikke gør, og ingeniørlen generaliserer ud over QR: normalisér så tæt på den semantiske repræsentation, som du kan komme, ikke i pixellaget, hvor dataene stadig bærer alle tilfældighederne ved, hvordan de blev fanget. HotPDF shipper dette som del af HotPDF Delphi PDF-komponenten, sammen med rendering-, OCR- og sideanalyse-brikkerne, som de samme indgangspipelines normalt behøver