HotPDF dekoduje obrócone symbole QR na załadowanej stronie PDF, normalizując próbkowaną macierz modułów przez wszystkie osiem orientacji D4 wewnątrz samego dekodera. Zewnętrzna próba rotacji, która działa dla symbologii liniowych, nie może zadziałać dla QR, a zrozumienie dlaczego oszczędza ci dnia gonienia za dekoderem, który wygląda na zepsuty, a nie jest
Scenariusz jest dość zwyczajny. Zeskanowane listy przewozowe przychodzą jako PDF-y, każda strona niesie etykietę QR, a operator skanera wsadził stos arkuszy w taką orientację, jaką przyjęła podajka. Część etykiet jest prosto, część odchylona o ćwierć obrotu, kilka do góry nogami. Wołasz dekoder kodów, połowa stron się rozwiązuje, a druga wraca pusta, bez żadnego błędu
Dlaczego obracanie maski skanu nigdy nie naprawia obróconego QR?
Bo layout wzorców finder QR jest świadomie asymetryczny, a obrót całego obrazu zachowuje tę asymetrię zamiast jej usunąć. QR Code kładzie trzy kwadraty finder w narożnikach lewym górnym, prawym górnym i lewym dolnym, a prawy dolny zostawia pusty (ISO/IEC 18004:2015 §6.3.3). Ten brakujący narożnik jest wskazówką orientacji. Obróć bitmapę strony o dziewięćdziesiąt stopni, a luka po prostu przeniesie się do innego narożnika. Nie istnieje nietrywialny obrót płaszczyzny, który odwzorowałby layout trójnarożnikowy na sam siebie, więc dekoder akceptujący wyłącznie kanoniczne ułożenie odrzuci kolejno każdą próbę
Ma to znaczenie, bo oczywista naprawa jest zła. Naturalny instynkt każe powiesić retry na zewnątrz: wyrenderuj stronę, podaj maskę dekoderowi, a jak polegnie, obróć maskę i spróbuj ponownie dla 90, 180 i 270 stopni. Dla Code 39 ta polityka jest dokładnie trafna, bo symbologia liniowa ma wzorzec start i stop, który skaner znajdzie, gdy tylko paski pobiegną poziomo. Dla QR to cztery zagwarantowane porażki, po których następuje raport nic nie znaleziono
Grupa D4 nałożona na macierz modułów
Właściwe miejsce normalizacji jest po próbkowaniu, na booleanowej siatce modułów, a nie na masce pikselowej. Gdy dekoder rozwiąże symbol do macierzy n na n modułów ciemnych i jasnych, może wyliczyć grupę diedralną kwadratu: cztery obroty razy dwa odbicia, osiem kandydackich orientacji łącznie. Dla każdego kandydata sprawdza trójkąt finderów, a pierwszy kandydat, którego trzy findery wylądują w pozycjach lewy górny, prawy górny i lewy dolny, jest prawdziwą orientacją. Stąd istniejący potok biegnie bez zmian, bo bity informacji o formacie, zygzakowe rozmieszczenie danych i korekcja Reed-Solomon wszystkie zakładają kanoniczną macierz i teraz ją dostają
Dwie własności robią z tego tanie rozwiązanie. Macierz jest mała w porównaniu z wyrenderowaną bitmapą, więc osiem transpozycji kosztuje znacznie mniej niż osiem renderowań strony. A macierz to czysta tablica booleanowa zbudowana przez sampler, więc żadna transformacja po drodze nie wniesie wartości, które nigdy nie były próbkowane
Detekcja wersji to przeszukiwanie podzielności, nie dzielenie
Liczby modułów nie da się wyprowadzić dzieląc próbkowaną szerokość przez założony rozmiar modułu, a pomyłka tutaj to subtelne źródło niepowodzeń dekodowania przy renderach wysokiej rozdzielczości. Symbol QR wersji v ma 4v + 17 modułów w poprzek, więc wersja 1 to 21 modułów, a wersja 40 to 177. Maska mierząca 126 pikseli szerokości jest równie zgodna z wersją 1 przy sześciu pikselach na moduł, co z kilkoma wyższymi wersjami przy mniejszych rozmiarach modułu. Liniowe dzielenie wybiera jedno z nich i zwykle jest w błędzie
Działa przeszukiwanie podzielności po kandydujących wersjach. Idź od wersji 40 w dół do wersji 1, zatrzymaj kandydatów, których liczba modułów dzieli próbkowaną szerokość bez reszty i zostawia co najmniej trzy piksele na moduł, i weź najmniejszą przetrwałą wersję. Podłoga trzech pikseli pilnuje, by wyszukiwanie nie przyjęło absurdalnie gęstego odczytu grubego symbolu, a reguła najmniejszej wersji rozstrzyga pozostałą dwuznaczność na korzyść odczytu, jaki faktycznie wyprodukowałby skaner
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 oddaje wypełniony rekord, a nie wyzerowany, co ma znaczenie, bo DPI równe zero albo limit wyników zero to wyglądający poprawnie sposób, by nie dostać nic. RotationPolicy steruje wyłącznie zewnętrznym retrym: bdrpNone renderuje raz, bdrpFallback powtarza pozostałe orientacje po nieudanym pierwszym przebiegu, a bdrpAll renderuje każdą orientację bezwarunkowo. Ponieważ normalizacja QR dzieje się wewnątrz dekodera, strony QR rozwiążą się za pierwszym podejściem przy każdej z trzech polityk. Polityka jest tam dla symbologii liniowych, które naprawdę jej potrzebują
Jak udowodnić, że transformacja bitmapy nie wymyśla pikseli?
Policz atrament po obu stronach i wymagaj, by sumy się zgadzały. Obrót to permutacja pikseli i nic więcej, więc liczba niezerowych komórek w wyjściu musi równać się liczbie w wejściu. Gdy obrót maski w ścieżce zewnętrznego retrya zgłosił 4800 ustawionych komórek na wejściu i 7439 na wyjściu, to pojedyncze porównanie wystarczyło, by skazać transformację bez czytania ani linii jej geometrii
Przyczyna była przyziemna i warta zapamiętania jako reguła. Tablica dynamiczna wymiarowana SetLength nie ma gwarancji, że przyjdzie wyzerowana, gdy jest wynikiem funkcji podróżującym ścieżką, którą runtime nie czyści, i komórki, których obrót nigdy nie zapisze, niosą wtedy cokolwiek, co tam było przedtem. Część tych nieświeżych bajtów jest niezerowa, a niezerowe znaczy atrament. Naprawa to jedna linia, FillChar(Result[0], N, 0) przed startem pętli permutacji, a dyscyplina, którą implikuje, jest szersza: każda funkcja zwracająca maskę albo bufor bitmapy powinna jawnie wyczyścić swoje wyjście, zamiast polegać na semantyce alokacji
To, co pozwoliło defektowi przetrwać trzy wydania, jest ciekawsze niż defekt. Gdy QR przeniósł swoją obsługę orientacji do dekodera, QR przestał całkowicie ćwiczyć zewnętrzny obrót maski, a jedynym pozostającym konsumentem tej ścieżki kodu został Code 39. Współdzielona infrastruktura chowa takie błędy non stop: pokrycie z jednej funkcji sprawia, że ścieżka wygląda na przetestowaną, podczas gdy funkcja faktycznie od niej zależna nie ma nic własnego. Każda ścieżka, której nowa funkcja przestaje używać, potrzebuje testu, który nadal jej używa
Odczyt wyników we współrzędnych strony
Każda wartość geometryczna produkowana przez dekoder jest wyrażona w układzie współrzędnych bitmapy próby, a wołający potrzebuje jej w przestrzeni użytkownika PDF-a. Ta konwersja biegnie w dwóch etapach: cofnij ćwierć obrotu nałożony przez retry, potem cofnij transformację renderowania, która odwzorowała przestrzeń użytkownika na bitmapę. To, co przychodzi w THPDFDecodedBarcode, to prostokąt otaczający wyrównany do osi w przestrzeni użytkownika, z Left, Bottom, Right i Top według konwencji PDF, że Y rośnie w górę, plus liczący się przeciwnie do wskazówek zegara OrientationDegrees
Pomyłka w kierunku tej drugiej konwersji daje paskudny objaw: tekst dekoduje się doskonale, ale box, który rysujesz jako warstwę przeglądu, ląduje na lustrzanym odbiciu właściwej pozycji. Każdy budujący interfejs przeglądu na dekoderze powinien asertować przeciw znanej fixture, z symbolem umieszczonym celowo blisko jednego narożnika strony, żeby odwrócona oś Y była widoczna na pierwszy rzut oka. To samo rozumowanie dotyczy każdej współrzędnej przekraczającej granicę renderowania, dlatego renderowanie strony PDF do bitmapy w Delphi warto zrozumieć, zanim zaczniesz budować na dekoderze
Co wbudowany dekoder zrobi, a czego nie
Wbudowany dekoder to ograniczona implementacja bez zależności i jest szczera co do swoich limitów, zamiast cicho się degradować. Rozpoznaje Code 39 i QR, waliduje bity formatu chronione BCH oraz wzorzec maski, zanim opublikuje jakiekolwiek dane, i nie próbuje odzyskiwać błędów na uszkodzonych symbolach. Jeśli twoje wejście to fotografia zakrzywionej etykiety pod nierównym światłem, to inna klasa problemu i chce sobie wyspecjalizowany silnik
// Podmień na własny silnik: zaimplementuj IHPDFBarcodeDecoder i przekaż go
// do przeciążenia świadomego dekoderów. HotPDF nadal włada renderowaniem
// stron, budżetami, mapowaniem współrzędnych i deduplikacją
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 to miejsce, gdzie produkcyjny potok zarabia na swoje utrzymanie. RotationAttemptCount i DecoderCallCount mówią ci, czy zewnętrzny retry w ogóle chodził, ReceivedResultCount na tle AcceptedResultCount oddziela dekoder, który nic nie znalazł, od progu pewności odrzucającego wszystko, co znalazł, a RenderedPixels z PeakWorkingBytes to to, co wykreślasz, gdy zadanie wsadowe zaczyna thrashować. Pusty zbiór wyników plus bdsSucceeded znaczy, że strona naprawdę nie ma czytelnego symbolu, co jest innym faktem operacyjnym niż bdsBudgetExceeded
Pola budżetu zasługują na świadomą decyzję, nie na domyślne. MaxPixels i MaxWorkingBytes istnieją, bo DPI mnoży się kwadratowo: przejście z 300 na 600 DPI na stronie A4 czterokrotnie zwiększa i koszt renderowania, i szczytową alokację, a niezaufane wejście deklarujące gigantyczny box strony potrafi zamienić zadanie skanowania w incydent braku pamięci. Ustaw capa na to, czego potrzebuje twój najgorszy legalny dokument, a potem pozwól, by bdsBudgetExceeded kierował odstających na wolniejszą, izolowaną ścieżkę
Jeśli twoje dokumenty mieszają etykiety czytelne maszynowo z drukowanym tekstem, który planujesz indeksować, dekoder kodów naturalnie paruje się z silnikiem rozpoznawania opisanym w artykule OCR przez dopasowanie szablonów wewnątrz HotPDF, a strona generująca tej samej historii jest w rysowaniu kodów kreskowych do PDF-a z HotPDF. Oba chodzą na tej samej infrastrukturze renderowania i budżetów, więc potok, który już ustawił rozsądne limity dla jednego, dostaje drugiego prawie gratis
Tolerancja obrotu to jedna z tych funkcji, które są niewidoczne, gdy działają, i wkurzające, gdy nie, a lekcja inżynierska uogólnia się poza QR: normalizuj tak blisko reprezentacji semantycznej, jak się da, nie na warstwie pikselowej, gdzie dane wciąż niosą każdy wypadek tego, jak zostały przechwycone. HotPDF dostarcza to w ramach komponentu PDF HotPDF dla Delphi, razem z kawałkami renderowania, OCR i analizy stron, których te same potoki przyjęcia zwykle potrzebują