HotPDF dekodiert rotierte QR-Symbole in einer geladenen PDF-Seite, indem es die gesampelte Modulmatrix im Decoder selbst durch alle acht D4-Orientierungen normalisiert. Der äußere Rotations-Retry, der bei linearen Symbologien funktioniert, kann bei QR nicht funktionieren, und zu verstehen, warum, erspart Ihnen einen Tag, in dem Sie einem Decoder hinterherjagen, der kaputt aussieht, aber keiner ist
Das Szenario ist alltäglicher geht kaum: Gescannte Lieferscheine kommen als PDFs an, jede Seite trägt ein QR-Etikett, und der Scanner-Operator hat einen Stapel Blätter in jener Richtung eingelegt, die die Zufuhr gerade akzeptiert hat. Manche Etiketten stehen aufrecht, manche um eine Vierteldrehung verdreht, ein paar auf dem Kopf. Man ruft den Barcode-Decoder auf, die halben Seiten lösen auf, die andere Hälfte kommt leer zurück, ohne jegliche Fehlermeldung
Warum behebt Drehen der Scan-Maske nie einen rotierten QR?
Weil das Finder-Pattern-Layout eines QR bewusst asymmetrisch ist und eine Rotation des ganzen Bildes diese Asymmetrie bewahrt statt sie zu beseitigen. QR Code platziert drei Finder-Quadrate an den Ecken oben links, oben rechts und unten links und lässt die Ecke unten rechts leer (ISO/IEC 18004:2015 §6.3.3). Diese fehlende Ecke ist der Orientierungshinweis. Dreht man die Seiten-Bitmap um neunzig Grad, wandert die Lücke schlicht in eine andere Ecke. Keine nicht-triviale Rotation der Ebene bildet ein Dreiecken-Layout auf sich selbst ab, also weist ein Decoder, der nur die kanonische Anordnung akzeptiert, jeden Versuch der Reihe nach zurück
Das ist wichtig, weil der naheliegende Fix der falsche ist. Der natürliche Instinkt hängt den Retry nach außen: Seite rendern, die Maske dem Decoder geben, und wenn das scheitert, die Maske drehen und es für 90, 180 und 270 Grad nochmal versuchen. Für Code 39 ist diese Policy genau richtig, denn eine lineare Symbologie hat ein Start- und Stoppmuster, das der Scanner findet, sobald die Balken horizontal laufen. Für QR sind das vier garantierte Fehlschläge, gefolgt von der Meldung, nichts gefunden zu haben
Die D4-Gruppe, angewandt auf die Modulmatrix
Der richtige Ort für die Normalisierung ist nach dem Sampling, auf dem booleschen Modulgitter statt auf der Pixelmaske. Hat der Decoder das Symbol erst in eine n-mal-n-Matrix aus dunklen und hellen Modulen aufgelöst, kann er die Diedergruppe des Quadrats enumerieren: vier Rotationen mal zwei Spiegelungen, insgesamt acht Kandidaten-Orientierungen. Bei jedem Kandidaten prüft er das Finder-Dreieck, und der erste Kandidat, dessen drei Finder auf den Positionen oben links, oben rechts und unten links landen, ist die echte Orientierung. Von dort läuft die bestehende Pipeline unverändert weiter, denn die Format-Infobits, die Zickzack-Platzierung der Daten und die Reed-Solomon-Korrektur setzen alle eine kanonische Matrix voraus und bekommen jetzt eine
Zwei Eigenschaften machen das billig. Die Matrix ist klein gegenüber der gerenderten Bitmap, also kosten acht Transponierungen weit weniger als acht Seiten-Renderings. Und die Matrix ist ein sauberes boolesches Array, das der Sampler gebaut hat, sodass keine Transformation unterwegs Werte einführen kann, die nie gesampelt wurden
Versionserkennung ist eine Teilbarkeitssuche, keine Division
Die Modulzahl lässt sich nicht ableiten, indem man die gesampelte Breite durch eine angenommene Modulgröße teilt, und das falsch zu machen ist eine subtile Quelle von Decode-Fehlern bei hochaufgelösten Renderings. Ein QR-Symbol der Version v ist 4v + 17 Module breit, Version 1 also 21 Module und Version 40 deren 177. Eine 126 Pixel breite Maske ist gleichermaßen verträglich mit Version 1 bei sechs Pixel pro Modul und mit mehreren höheren Versionen bei kleineren Modulgrößen. Lineare Division wählt eine davon und liegt meist falsch
Was funktioniert, ist eine Teilbarkeitssuche über die Kandidaten-Versionen. Von Version 40 hinab bis Version 1 gehen, die Kandidaten behalten, deren Modulzahl die gesampelte Breite restlos teilt und mindestens drei Pixel pro Modul übrig lässt, und die kleinste überlebende Version nehmen. Die Drei-Pixel-Untergrenze verhindert, dass die Suche eine absurddichte Lesung eines groben Symbols akzeptiert, und die Kleinstversion-Regel löst die verbleibende Mehrdeutigkeit zugunsten der Lesung, die ein Scanner tatsächlich liefern würde
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 gibt einen gefüllten Record zurück statt eines genullten, was zählt, denn ein DPI von null oder ein Ergebnisdeckel von null ist eine plausibel aussehende Art, nichts zurückzubekommen. RotationPolicy steuert nur den äußeren Retry: bdrpNone rendert einmal, bdrpFallback wiederholt die anderen Orientierungen nach einem fehlgeschlagenen ersten Durchlauf, und bdrpAll rendert jede Orientierung bedingungslos. Weil die QR-Normalisierung im Decoder passiert, lösen QR-Seiten beim ersten Versuch unter jeder der drei Policies auf. Die Policy ist für die linearen Symbologien da, die sie wirklich brauchen
Wie beweist man, dass eine Bitmap-Transformation keine Pixel erfindet?
Zählen Sie die Tinte auf beiden Seiten und verlangen Sie, dass die Summen übereinstimmen. Eine Rotation ist eine Permutation von Pixeln, nichts weiter, also muss die Zahl der Nicht-Null-Zellen in der Ausgabe gleich der in der Eingabe sein. Als eine Maskenrotation im äußeren Retry-Pfad 4800 gesetzte Zellen am Eingang und 7439 am Ausgang meldete, reichte dieser eine Vergleich, um die Transformation zu überführen, ohne eine Zeile ihrer Geometrie zu lesen
Die Ursache war profan und verdient es, als Regel mitgenommen zu werden. Ein mit SetLength dimensioniertes dynamisches Array ist nicht garantiert genullt, wenn es als Funktionsergebnis einen Weg nimmt, den die Runtime nicht leert, und Zellen, die die Rotation nie beschreibt, tragen dann die Bytes, die vorher dort standen. Manche dieser alten Bytes sind nicht null, und nicht null heißt Tinte. Der Fix ist eine Zeile, FillChar(Result[0], N, 0), bevor die Permutationsschleife läuft, und die Disziplin, die das impliziert, ist breiter: Jede Funktion, die einen Masken- oder Bitmap-Puffer zurückgibt, sollte ihre Ausgabe explizit leeren, statt sich auf Allokationssemantik zu verlassen
Was den Defekt drei Releases überleben ließ, ist interessanter als der Defekt. Sobald QR seine Orientierungsbehandlung in den Decoder verlagerte, lief QR den äußeren Maskenrotation-Pfad überhaupt nicht mehr, und der einzige verbleibende Konsument dieses Codepfads war Code 39. Geteilte Infrastruktur versteckt solche Bugs ständig: Abdeckung aus einem Feature lässt einen Pfad getestet aussehen, während das Feature, das tatsächlich davon abhängt, keine eigene hat. Jeder Pfad, den ein neues Feature nicht mehr benutzt, braucht einen Test, der ihn noch benutzt
Ergebnisse in Seitenkoordinaten zurücklesen
Jeder geometrische Wert, den der Decoder erzeugt, ist im Koordinatenrahmen der Attempt-Bitmap ausgedrückt, und der Aufrufer braucht ihn im PDF-User-Space. Diese Konvertierung läuft in zwei Stufen: die Vierteldrehung zurücknehmen, die der Retry angewandt hat, dann die Render-Transformation zurücknehmen, die den User-Space auf die Bitmap abgebildet hat. Was in THPDFDecodedBarcode ankommt, ist eine achsenparallele Bounding Box im User-Space, mit Left, Bottom, Right und Top nach der PDF-Konvention, dass Y nach oben wächst, plus einem gegen den Uhrzeigersinn laufenden OrientationDegrees
Die Richtung dieser zweiten Konvertierung falsch zu haben, hat ein fieses Symptom: Der Text dekodiert perfekt, aber die Box, die man für ein Review-Overlay zeichnet, landet auf dem Spiegelbild der richtigen Position. Wer ein Review-Interface auf dem Decoder aufbaut, sollte gegen ein bekanntes Fixture behaupten, mit einem Symbol bewusst nahe einer Seitenecke platziert, damit eine gespiegelte Y-Achse auf einen Blick sichtbar ist. Dasselbe Denken gilt für jede Koordinate, die die Rendering-Grenze überquert — deshalb lohnt es sich, das Rendern einer PDF-Seite in eine Bitmap in Delphi zu verstehen, bevor man auf dem Decoder aufbaut
Was der eingebaute Decoder tut und was nicht
Der eingebaute Decoder ist eine begrenzte, abhängigkeitsfreie Implementierung, und er ist ehrlich über seine Grenzen, statt still zu degradieren. Er erkennt Code 39 und QR, validiert die BCH-geschützten Formatbits und das Maskenmuster, bevor er irgendeine veröffentlicht, und er versucht keine Fehlerkorrektur an beschädigten Symbolen. Ist Ihre Eingabe ein Foto eines gewölbten Etiketts bei ungleichmäßigem Licht, ist das eine andere Problemklasse und will eine spezialisierte Engine
// Eigene Engine einsetzen: IHPDFBarcodeDecoder implementieren und an
// die decoderbewusste Überladung durchreichen. HotPDF behält Seiten-Rendering,
// Budgets, Koordinatenabbildung und Deduplizierung
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 ist der Ort, an dem eine Produktions-Pipeline ihr Geld verdient. RotationAttemptCount und DecoderCallCount verraten, ob der äußere Retry überhaupt lief; ReceivedResultCount gegen AcceptedResultCount trennt einen Decoder, der nichts fand, von einem Confidence-Schwellwert, der alles Verworfene ausschlug; und RenderedPixels samt PeakWorkingBytes ist das, was man grafisch aufträgt, wenn ein Batch-Job anfängt zu thrashen. Ein leeres Ergebnisset plus bdsSucceeded heißt, die Seite hat wirklich kein lesbares Symbol — eine andere operationale Tatsache als bdsBudgetExceeded
Die Budgetfelder verdienen eine bewusste Entscheidung statt eines Defaults. MaxPixels und MaxWorkingBytes existieren, weil DPI quadratisch multipliziert: Von 300 auf 600 DPI bei einer A4-Seite vervierfacht sowohl die Renderkosten als auch die Spitzenallokation, und eine nicht vertrauenswürdige Eingabe, die eine riesige Seitenbox deklariert, kann aus einem Scanjob einen Out-of-Memory-Vorfall machen. Setzen Sie die Deckel auf das, was Ihr schlimmstes legitimes Dokument braucht, und lassen Sie bdsBudgetExceeded die Ausreißer auf einen langsameren, isolierten Pfad umleiten
Mischen Ihre Dokumente maschinenlesbare Etiketten mit gedrucktem Text, den Sie indexieren wollen, paart sich der Barcode-Decoder natürlich mit der Erkennungs-Engine aus Template-Matching-OCR in HotPDF, und die Erzeugerseite derselben Geschichte steht in Barcodes ins PDF zeichnen mit HotPDF. Beide laufen auf derselben Rendering- und Budget-Infrastruktur, also bekommt eine Pipeline, die für das eine schon vernünftige Grenzen setzt, das andere fast gratis
Rotationstoleranz ist eines dieser Features, das unsichtbar ist, wenn es funktioniert, und enervierend, wenn nicht, und die Konstruktionslektion verallgemeinert über QR hinaus: So nah an der semantischen Repräsentation normalisieren wie möglich, nicht auf der Pixelebene, wo die Daten noch jeden Zufall ihrer Aufzeichnung mitschleppen. HotPDF liefert das als Teil der HotPDF Delphi PDF component aus, neben den Rendering-, OCR- und Seitenanalyse-Bausteinen, die dieselben Eingangs-Pipelines üblicherweise brauchen