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
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
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
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