Article technique

Décoder des QR codes pivotés dans les pages PDF avec HotPDF

HotPDF décode les symboles QR pivotés d'une page PDF chargée en normalisant la matrice de modules échantillonnée à travers les huit orientations D4 à l'intérieur du décodeur lui-même. La retry de rotation externe qui fonctionne pour les symbologies linéaires ne peut pas fonctionner pour le QR, et comprendre pourquoi vous épargne une journée à traquer un décodeur qui semble cassé mais ne l'est pas

Le scénario est assez banal. Des bordereaux de livraison numérisés arrivent en PDF, chaque page porte une étiquette QR, et le opérateur de numérisation a introduit une pile de feuilles dans le sens que le bac acceptait. Certaines étiquettes sont droites, d'autres tournées d'un quart de tour, quelques-unes à l'envers. Vous appelez le décodeur de codes-barres, la moitié des pages se résout, et l'autre moitié revient vide sans la moindre erreur

Pourquoi tourner le masque de balayage ne répare jamais un QR pivoté ?

Parce que la disposition des patterns de détection du QR est délibérément asymétrique, et qu'une rotation de l'image entière préserve cette asymétrie au lieu de la retirer. QR Code place trois carrés de détection aux coins haut-gauche, haut-droit et bas-gauche, et laisse le coin bas-droit vide (ISO/IEC 18004:2015 §6.3.3). Ce coin manquant est l'indice d'orientation. Tournez le bitmap de page de quatre-vingt-dix degrés et le vide se déplace simplement vers un autre coin. Aucune rotation non triviale du plan ne ramène une disposition à trois coins sur elle-même, donc un décodeur qui n'accepte que l'arrangement canonique rejettera chaque tentative à tour de rôle

Cela compte parce que la correction évidente est la mauvaise. L'instinct naturel est de suspendre la retry à l'extérieur : rendre la page, remettre le masque au décodeur, et si cela échoue, tourner le masque et réessayer à 90, 180 et 270 degrés. Pour le Code 39 cette politique est exactement la bonne, car une symbologie linéaire a un pattern de début et de fin que le scanner trouve dès que les barres sont horizontales. Pour le QR, c'est quatre échecs garantis suivis d'un rapport de rien trouvé

Le groupe D4, appliqué à la matrice de modules

Le bon endroit pour la normalisation est après l'échantillonnage, sur la grille booléenne de modules plutôt que sur le masque de pixels. Une fois que le décodeur a résolu le symbole en une matrice n par n de modules sombres et clairs, il peut énumérer le groupe diédral du carré : quatre rotations fois deux réflexions, huit orientations candidates au total. Pour chaque candidate, il contrôle le triangle de détection, et la première candidate dont les trois patterns de détection atterrissent en positions haut-gauche, haut-droit et bas-gauche est la vraie orientation. De là, le pipeline existant tourne sans changement, car les bits d'information de format, le placement en zigzag des données et la correction Reed-Solomon supposent tous une matrice canonique et en obtiennent maintenant une

Quatre rendus de la même matrice de modules QR HotPDF sous les rotations du groupe D4 à 0, 90, 180 et 270 degrés, montrant les trois patterns de détection migrant de coin en coin pendant que le coin vide se déplace avec eux, si bien que seule l'orientation canonique présente des patterns de détection en haut-gauche, haut-droit et bas-gauche au décodeur
Tourner le masque de pixels ne peut pas retirer l'asymétrie de détection du QR, donc HotPDF énumère les orientations D4 sur la matrice de modules échantillonnée et garde la première candidate dont les patterns de détection atterrissent en haut-gauche, haut-droit et bas-gauche

Deux propriétés rendent cela bon marché. La matrice est petite comparée au bitmap rendu, donc huit transpositions coûtent bien moins que huit rendus de page. Et la matrice est un tableau booléen propre construit par l'échantillonneur, si bien qu'aucune transformation en chemin ne peut introduire des valeurs jamais échantillonnées

La détection de version est une recherche de divisibilité, pas une division

Le compte de modules ne peut pas être dérivé en divisant la largeur échantillonnée par une taille de module supposée, et se tromper ici est une source subtile d'échecs de décodage sur des rendus haute résolution. Un symbole QR de version v fait 4v + 17 modules de côté, donc la version 1 fait 21 modules et la version 40 en fait 177. Un masque qui mesure 126 pixels de large est également compatible avec la version 1 à six pixels par module et avec plusieurs versions supérieures à des tailles de module plus petites. La division linéaire en choisit une et se trompe en général

Ce qui fonctionne est une recherche de divisibilité sur les versions candidates. Parcourez de la version 40 vers la version 1, gardez les candidates dont le compte de modules divise la largeur échantillonnée exactement et laisse au moins trois pixels par module, et prenez la plus petite version survivante. Le plancher de trois pixels est ce qui empêche la recherche d'accepter une lecture absurde et dense d'un symbole grossier, et la règle de la plus petite version résout l'ambiguïté restante en faveur de la lecture qu'un scanner produirait réellement

La marche de détection de version HotPDF pour un symbole QR sur un masque échantillonné de 126 pixels, testant chaque compte de modules candidat 4v plus 17 de la version 40 vers la version 1 pour la divisibilité exacte et un plancher de trois pixels par module avant que la plus petite version survivante ne gagne
Un compte de modules QR vient d'une recherche de divisibilité sur les versions candidates, pas de la division de la largeur du masque par une taille de module supposée, et la plus petite version survivante résout l'ambiguïté
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 rend un enregistrement rempli plutôt que mis à zéro, ce qui compte parce qu'un DPI de zéro ou un plafond de résultats à zéro est une façon d'apparence valide de ne rien recevoir. RotationPolicy ne contrôle que la retry externe : bdrpNone rend une fois, bdrpFallback réessaie les autres orientations après une première passe échouée, et bdrpAll rend chaque orientation sans condition. Comme la normalisation QR se passe à l'intérieur du décodeur, les pages QR se résoluent à la première tentative sous l'une ou l'autre des trois politiques. La politique est là pour les symbologies linéaires qui en ont réellement besoin

Comment prouver qu'une transformation de bitmap n'invente pas de pixels ?

Comptez l'encre des deux côtés et exigez que les totaux concordent. Une rotation est une permutation de pixels, rien de plus, donc le nombre de cellules non nulles en sortie doit égaler celui en entrée. Quand une rotation de masque dans le chemin de retry externe rapportait 4800 cellules posées à l'entrée et 7439 à la sortie, cette seule comparaison suffisait à condamner la transformation sans lire une seule ligne de sa géométrie

La cause était banale et mérite d'être emportée comme règle. Un tableau dynamique dimensionné avec SetLength n'est pas garanti d'arriver mis à zéro quand c'est un résultat de fonction voyageant par un chemin que le runtime ne nettoie pas, et les cellules que la rotation n'écrit jamais portent alors les octets qui s'y trouvaient avant. Certains de ces octets périmés sont non nuls, et non nul signifie encre. La correction tient en une ligne, FillChar(Result[0], N, 0) avant que la boucle de permutation ne tourne, et la discipline qu'elle implique est plus large : toute fonction renvoyant un masque ou un tampon bitmap doit nettoyer sa sortie explicitement au lieu de compter sur la sémantique d'allocation

Ce qui a fait survivre le défaut à trois versions est plus intéressant que le défaut. Une fois que le QR a déplacé sa gestion d'orientation dans le décodeur, le QR a cessé d'exercer entièrement la rotation externe de masque, et le seul consommant restant de ce chemin de code était le Code 39. L'infrastructure partagée cache ce genre de bugs tout le temps : la couverture d'une fonctionnalité fait paraître un chemin testé alors que la fonctionnalité qui en dépend réellement n'en a aucune à elle. Tout chemin qu'une nouvelle fonctionnalité cesse d'utiliser a besoin d'un test qui l'utilise encore

Lire les résultats en coordonnées de page

Chaque valeur géométrique que le décodeur produit est exprimée dans le repère de coordonnées du bitmap de tentative, et l'appelant en a besoin en espace utilisateur PDF. Cette conversion se déroule en deux étapes : défaire le quart de tour que la retry a appliqué, puis défaire la transformation de rendu qui a projeté l'espace utilisateur sur le bitmap. Ce qui arrive dans THPDFDecodedBarcode est une boîte englobante alignée sur les axes en espace utilisateur, avec Left, Bottom, Right et Top suivant la convention PDF d'un Y croissant vers le haut, plus un OrientationDegrees en sens antihoraire

Le pipeline de codes-barres HotPDF depuis le bitmap de page rendu, à travers l'échantillonnage en une matrice booléenne de modules, la normalisation D4, la détection de version par divisibilité et le décodage Reed-Solomon, puis la conversion de coordonnées en deux étapes qui défait le quart de tour de la retry et la transformation de rendu avant que THPDFDecodedBarcode ne publie Left, Bottom, Right, Top et OrientationDegrees en espace utilisateur
La normalisation QR à l'intérieur du décodeur laisse les pages se résoudre à la première tentative, tandis que la conversion de coordonnées en deux étapes transforme les résultats du bitmap de tentative en boîtes alignées en espace utilisateur

Se tromper dans le sens de cette seconde conversion donne un symptôme pénible : le texte se décode parfaitement, mais la boîte que vous dessinez pour une surcouche de relecture atterrit sur l'image miroir de la bonne position. Quiconque construit une interface de relecture au-dessus du décodeur devrait affirmer contre un fixture connu, avec un symbole placé délibérément près d'un coin de page pour qu'un axe Y inversé se voie d'un coup d'œil. Le même raisonnement s'applique à toute coordonnée traversant la frontière de rendu, et c'est pourquoi le rendu d'une page PDF vers un bitmap en Delphi mérite d'être compris avant de construire sur le décodeur

Ce que le décodeur intégré fait et ne fait pas

Le décodeur intégré est une implémentation bornée et sans dépendance, et il est honnête sur ses limites au lieu de se dégrader en silence. Il reconnaît le Code 39 et le QR, valide les bits de format protégés par BCH et le pattern de masque avant de publier la moindre donnée, et il ne tente pas de récupération d'erreur sur des symboles endommagés. Si votre entrée est une photographie d'étiquette courbée sous un éclairage inégal, c'est une autre classe de problème et cela veut un moteur spécialisé

// Branchez votre propre moteur : implémentez IHPDFBarcodeDecoder et passez-le à la
// surcharge consciente du décodeur. HotPDF garde toujours le rendu de page, les budgets,
// la cartographie des coordonnées et la dé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 est l'endroit où un pipeline de production gagne sa vie. RotationAttemptCount et DecoderCallCount vous disent si la retry externe a tourné du tout, ReceivedResultCount face à AcceptedResultCount sépare un décodeur qui n'a rien trouvé d'un seuil de confiance qui a rejeté tout ce qu'il a trouvé, et RenderedPixels avec PeakWorkingBytes est ce que vous tracez quand un travail par lot se met à suffoquer. Un ensemble de résultats vide plus bdsSucceeded signifie que la page n'a vraiment aucun symbole lisible, ce qui est un fait opérationnel différent de bdsBudgetExceeded

Les champs de budget méritent une décision délibérée plutôt qu'un défaut. MaxPixels et MaxWorkingBytes existent parce que le DPI multiplie quadratiquement : passer de 300 à 600 DPI sur une page A4 quadruple à la fois le coût de rendu et l'allocation de pointe, et une entrée non fiable déclarant une boîte de page énorme peut transformer un travail de numérisation en incident de mémoire épuisée. Posez les plafonds à ce que votre pire document légitime exige, puis laissez bdsBudgetExceeded router les valeurs aberrantes vers un chemin plus lent et isolé

Si vos documents mélangent étiquettes lisibles par machine et texte imprimé que vous comptez indexer, le décodeur de codes-barres se marie naturellement avec le moteur de reconnaissance couvert dans l'OCR par appariement de gabarits dans HotPDF, et le côté génération de la même histoire se trouve dans le dessin de codes-barres dans un PDF avec HotPDF. Les deux tournent sur la même infrastructure de rendu et de budgets, si bien qu'un pipeline qui a déjà posé des limites saines pour l'un obtient l'autre presque gratuitement

La tolérance à la rotation est de ces fonctionnalités invisibles quand elles fonctionnent et exaspérantes quand elles échouent, et la leçon d'ingénierie se généralise au-delà du QR : normalisez aussi près de la représentation sémantique que vous pouvez atteindre, pas à la couche pixels où les données portent encore chaque accident de leur capture. HotPDF livre cela au sein du composant PDF Delphi HotPDF, aux côtés des pièces de rendu, d'OCR et d'analyse de page dont les mêmes pipelines d'ingestion ont habituellement besoin