Articolo tecnico

Decodificare codici QR ruotati nelle pagine PDF con HotPDF

HotPDF decodifica i simboli QR ruotati in una pagina PDF caricata normalizzando la matrice di moduli campionata attraverso tutte e otto le orientazioni D4 dentro il decoder stesso. Il retry di rotazione esterno che funziona per le simbologie lineari non può funzionare per il QR, e capire perché ti risparmia una giornata a inseguire un decoder che sembra rotto ma non lo è

Lo scenario è abbastanza ordinario. Le bolle di consegna scansionate arrivano come PDF, ogni pagina porta un'etichetta QR, e l'operatore dello scanner ha infilato una pila di fogli nella direzione che il vassoio accettava. Alcune etichette sono dritte, alcune sono spostate di un quarto di giro, un paio sono capovolte. Chiami il decoder di codici a barre, metà delle pagine si risolve, e l'altra metà torna vuota senza nessun errore

Perché ruotare la maschera di scansione non aggiusta mai un QR ruotato?

Perché il layout dei finder pattern di un QR è volutamente asimmetrico, e una rotazione dell'intera immagine preserva quella asimmetria invece di eliminarla. QR Code piazza tre quadrati finder agli angoli in alto a sinistra, in alto a destra e in basso a sinistra, e lascia vuoto l'angolo in basso a destra (ISO/IEC 18004:2015 §6.3.3). Quell'angolo mancante è il segnale di orientamento. Ruota la bitmap di pagina di novanta gradi e il vuoto semplicemente si sposta in un altro angolo. Non esiste una rotazione non banale del piano che riporti un layout a tre angoli su se stesso, quindi un decoder che accetta solo la disposizione canonica rifiuterà ogni tentativo a turno

Questo conta perché la correzione ovvia è quella sbagliata. L'istinto naturale è appendere il retry all'esterno: renderizza la pagina, passa la maschera al decoder, e se fallisce, ruota la maschera e riprova a 90, 180 e 270 gradi. Per Code 39 quella politica è esattamente giusta, perché una simbologia lineare ha un pattern di start e stop che lo scanner trova appena le barre corrono in orizzontale. Per il QR sono quattro fallimenti garantiti seguiti da un rapporto di nessun risultato trovato

Il gruppo D4, applicato alla matrice di moduli

Il posto giusto per la normalizzazione è dopo il campionamento, sulla griglia booleana dei moduli invece che sulla maschera di pixel. Una volta che il decoder ha risolto il simbolo in una matrice n per n di moduli scuri e chiari, può enumerare il gruppo diedrale del quadrato: quattro rotazioni per due riflessioni, otto orientazioni candidate in totale. Per ogni candidata controlla il triangolo dei finder, e la prima candidata i cui tre finder cadono nelle posizioni in alto a sinistra, in alto a destra e in basso a sinistra è la vera orientazione. Da lì la pipeline esistente gira immutata, perché i bit delle informazioni di formato, la disposizione a zigzag dei dati e la correzione Reed-Solomon assumono tutti una matrice canonica e ora la ottengono

Quattro render della stessa matrice di moduli QR di HotPDF sotto le rotazioni del gruppo D4 a 0, 90, 180 e 270 gradi, che mostrano i tre finder pattern migrare di angolo mentre l'angolo vuoto si muove con loro, così solo l'orientazione canonica presenta i finder in alto a sinistra, in alto a destra e in basso a sinistra al decoder
Ruotare la maschera di pixel non può eliminare l'asimmetria dei finder QR, quindi HotPDF enumera le orientazioni D4 sulla matrice di moduli campionata e conserva la prima candidata i cui finder cadono in alto a sinistra, in alto a destra e in basso a sinistra

Due proprietà rendono la cosa economica. La matrice è piccola rispetto alla bitmap renderizzata, quindi otto trasposizioni costano molto meno di otto render di pagina. E la matrice è un array booleano pulito costruito dal campionatore, quindi nessuna trasformazione lungo la strada può introdurre valori mai campionati

Il rilevamento della versione è una ricerca di divisibilità, non una divisione

Il numero di moduli non si può derivare dividendo la larghezza campionata per una dimensione di modulo assunta, e sbagliare questo è una fonte sottile di fallimenti di decodifica sui render ad alta risoluzione. Un simbolo QR di versione v è largo 4v + 17 moduli, quindi la versione 1 è 21 moduli e la versione 40 è 177. Una maschera larga 126 pixel è equamente compatibile con la versione 1 a sei pixel per modulo e con diverse versioni superiori a dimensioni di modulo minori. La divisione lineare ne sceglie una ed è di solito quella sbagliata

Ciò che funziona è una ricerca di divisibilità sulle versioni candidate. Percorri dalla versione 40 giù alla versione 1, conserva le candidate il cui numero di moduli divide la larghezza campionata in modo pari e lascia almeno tre pixel per modulo, e prendi la versione sopravvissuta più piccola. Il pavimento dei tre pixel è ciò che impedisce alla ricerca di accettare una lettura assurdammente densa di un simbolo grossolano, e la regola della versione minima risolve l'ambiguità residua a favore della lettura che uno scanner produrrebbe davvero

La camminata di rilevamento versione di HotPDF per un simbolo QR su una maschera campionata da 126 pixel, testando ogni numero di moduli candidato 4v più 17 dalla versione 40 giù alla versione 1 per divisibilità pari e un pavimento di tre pixel per modulo prima che vinca la versione sopravvissuta più piccola
Il numero di moduli QR viene da una ricerca di divisibilità sulle versioni candidate, non dal dividere la larghezza della maschera per una dimensione di modulo assunta, e la versione sopravvissuta più piccola risolve l'ambiguità
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 restituisce un record popolato invece che azzerato, il che conta perché un DPI a zero o un tetto risultati a zero è un modo che sembra valido per non ricevere niente. RotationPolicy controlla solo il retry esterno: bdrpNone renderizza una volta, bdrpFallback riprova le altre orientazioni dopo una prima passata fallita, e bdrpAll renderizza ogni orientazione incondizionatamente. Dato che la normalizzazione QR avviene dentro il decoder, le pagine QR si risolvono al primo tentativo sotto una qualsiasi delle tre politiche. La politica esiste per le simbologie lineari che ne hanno davvero bisogno

Come dimostri che una trasformazione di bitmap non sta inventando pixel?

Conta l'inchiostro da entrambe le parti ed esigi che i totali combacino. Una rotazione è una permutazione di pixel, niente di più, quindi il numero di celle non nere nell'output deve eguagliare quello nell'input. Quando una rotazione di maschera nel percorso di retry esterno riportava 4800 celle impostate in entrata e 7439 in uscita, quel singolo confronto è bastato a incastrare la trasformazione senza leggere una riga della sua geometria

La causa era banale e vale la pena portarsela via come regola. Un dynamic array dimensionato con SetLength non ha garanzia di arrivare azzerato quando è un risultato di funzione che percorre un sentiero il runtime non ripulisce, e le celle che la rotazione non scrive mai portano allora i byte che c'erano prima. Alcuni di quei byte stantii sono non nulli, e non nullo significa inchiostro. La correzione è una riga, FillChar(Result[0], N, 0) prima che il ciclo di permutazione parta, e la disciplina che implica è più ampia: qualunque funzione che restituisce una maschera o un buffer bitmap dovrebbe azzerare esplicitamente il proprio output invece di contare sulle semantiche di allocazione

Ciò che ha fatto sopravvivere il difetto per tre release è più interessante del difetto. Una volta che il QR ha spostato la gestione dell'orientazione dentro il decoder, il QR ha smesso di esercitare del tutto la rotazione esterna della maschera, e l'unico consumatore rimasto di quel percorso di codice era Code 39. L'infrastruttura condivisa nasconde bug così continuamente: la copertura di una funzione fa sembrare testato un percorso mentre la funzione che dipende davvero da lui non ne ha di propri. Ogni percorso che una nuova funzione smette di usare ha bisogno di un test che lo usi ancora

Rileggere i risultati nelle coordinate di pagina

Ogni valore geometrico che il decoder produce è espresso nel sistema di coordinate della bitmap del tentativo, e il chiamante lo serve nello user space PDF. Quella conversione gira in due stadi: annulla il quarto di giro applicato dal retry, poi annulla la trasformazione di render che ha mappato lo user space sulla bitmap. Ciò che arriva in THPDFDecodedBarcode è un bounding box allineato agli assi nello user space, con Left, Bottom, Right e Top secondo la convenzione PDF che la Y cresce verso l'alto, più una OrientationDegrees in senso antiorario

La pipeline barcode di HotPDF dalla bitmap di pagina renderizzata attraverso il campionamento in una matrice booleana di moduli, normalizzazione D4, rilevamento versione per divisibilità e decodifica Reed-Solomon, poi la conversione di coordinate a due stadi che annulla il quarto di giro del retry e la trasformazione di render prima che THPDFDecodedBarcode pubblichi Left, Bottom, Right, Top e OrientationDegrees nello user space
La normalizzazione QR dentro il decoder permette alle pagine di risolversi al primo tentativo, mentre la conversione di coordinate a due stadi trasforma i risultati della bitmap del tentativo in box allineati agli assi nello user space

Sbaglia la direzione di quella seconda conversione e il sintomo è cattivo: il testo decodifica alla perfezione, ma il box che disegni per un overlay di revisione atterra sull'immagine speculare della posizione giusta. Chi costruisce un'interfaccia di revisione sopra il decoder dovrebbe asserire contro una fixture nota, con un simbolo piazzato apposta vicino a un angolo di pagina così un asse Y capovolto si vede a colpo d'occhio. Lo stesso ragionamento vale per qualunque coordinata che attraversi il confine di rendering, ed è per questo che renderizzare una pagina PDF in una bitmap in Delphi merita di essere capito prima di costruire sopra il decoder

Cosa farà e cosa non farà il decoder integrato

Il decoder integrato è un'implementazione limitata e senza dipendenze, ed è onesto sui propri limiti invece che degradare in silenzio. Riconosce Code 39 e QR, valida i bit di formato protetti BCH e il pattern di maschera prima di pubblicare qualunque dato, e non tenta il recupero degli errori sui simboli danneggiati. Se il tuo input è la fotografia di un'etichetta curva sotto luce disomogenea, quella è una classe di problema diversa e vuole un motore specializzato

// Sostituisci con il tuo motore: implementa IHPDFBarcodeDecoder e passalo
// all'overload consapevole del decoder. HotPDF resta proprietaria di render
// delle pagine, budget, mappatura coordinate e de-duplicazione
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 è dove una pipeline di produzione guadagna il proprio stipendio. RotationAttemptCount e DecoderCallCount ti dicono se il retry esterno è girato o no, ReceivedResultCount contro AcceptedResultCount separa un decoder che non ha trovato niente da una soglia di confidenza che ha scartato tutto ciò che ha trovato, e RenderedPixels con PeakWorkingBytes è ciò che metti in grafico quando un job batch comincia a fare thrashing. Un insieme di risultati vuoto più bdsSucceeded significa che la pagina davvero non ha simboli leggibili, che è un fatto operativo diverso da bdsBudgetExceeded

I campi di budget meritano una decisione deliberata invece di un default. MaxPixels e MaxWorkingBytes esistono perché il DPI moltiplica quadraticamente: passare da 300 a 600 DPI su una pagina A4 quadruplica sia il costo di render sia l'allocazione di picco, e un input non fidato che dichiara un page box enorme può trasformare un lavoro di scansione in un incidente di memoria esaurita. Imposta i tetti a ciò che il tuo peggiore documento legittimo richiede, poi lascia che bdsBudgetExceeded instradi i valori anomali verso un percorso più lento e isolato

Se i tuoi documenti mescolano etichette leggibili dalle macchine con testo stampato che pensi di indicizzare, il decoder di codici a barre si abbina naturalmente al motore di riconoscimento trattato in OCR a confronto di template dentro HotPDF, e il lato generazione della stessa storia è in disegnare codici a barre in un PDF con HotPDF. Entrambi girano sulla stessa infrastruttura di rendering e budget, quindi una pipeline che già imposta limiti sani per l'uno ottiene l'altro quasi gratis

La tolleranza alle rotazioni è una di quelle funzioni invisibili quando funziona e infuriante quando non funziona, e la lezione di ingegneria si generalizza oltre il QR: normalizza il più vicino possibile alla rappresentazione semantica, non al layer di pixel dove i dati portano ancora ogni incidente di come sono stati catturati. HotPDF spedisce questo come parte del HotPDF Delphi PDF component, accanto ai pezzi di rendering, OCR e analisi di pagina che le stesse pipeline di intake di solito richiedono