HotPDF декодирует повёрнутые QR-символы на загруженной PDF-странице, нормализуя сэмплированную матрицу модулей по всем восьми ориентациям D4 внутри самого декодера. Внешний retry с вращением, работающий для линейных символик, для QR работать не может, и понимание почему экономит день охоты за декодером, который выглядит сломанным, но не сломан
Сценарий вполне заурядный. Отсканированные накладные приходят в PDF, на каждой странице QR-наклейка, а оператор сканера закладывал стопку листов в ту сторону, куда лоток согласился принимать. Часть наклеек стоит прямо, часть повёрнута на четверть оборота, несколько — вверх ногами. Вы вызываете декодер штрихкодов, половина страниц разрешается, а вторая половина возвращается пустой и без единой ошибки
Почему вращение маски сканирования никогда не чинит повёрнутый QR?
Потому что раскладка finder-паттернов QR намеренно асимметрична, и вращение целого изображения сохраняет эту асимметрию, а не убирает её. QR Code кладёт три finder-квадрата в верхний левый, верхний правый и нижний левый углы, оставляя нижний правый угол пустым (ISO/IEC 18004:2015 §6.3.3). Этот отсутствующий угол и есть подсказка об ориентации. Поверните битмап страницы на девяносто градусов — и пустота просто переедет в другой угол. Нет нетривиального вращения плоскости, которое отобразило бы трёхугловую раскладку на себя, поэтому декодер, принимающий только каноничное расположение, отвергнет каждую попытку по очереди
Это важно, потому что очевидный фикс — неправильный. Естественный инстинкт — повесить retry снаружи: отрендерить страницу, отдать маску декодеру, а если неудача — повернуть маску и попробовать снова на 90, 180 и 270 градусов. Для Code 39 такая политика ровно правильна, потому что у линейной символики есть start- и stop-паттерны, которые сканер найдёт, как только штрихи пойдут горизонтально. Для QR это четыре гарантированных провала, за которыми следует отчёт «ничего не найдено»
Группа D4, применённая к матрице модулей
Правильное место нормализации — после сэмплирования, на булевой сетке модулей, а не на пиксельной маске. Когда декодер уже свёл символ к матрице n на n из тёмных и светлых модулей, он может перечислить диэдральную группу квадрата: четыре вращения на два отражения, восемь кандидатов-ориентаций в сумме. Для каждого кандидата он проверяет finder-треугольник, и первый кандидат, чьи три finder'а легли в верхний левый, верхний правый и нижний левый позиции, — истинная ориентация. Дальше существующий пайплайн идёт без изменений, потому что биты format information, зигзаг-размещение данных и коррекция Рида-Соломона все предполагают каноничную матрицу и теперь её получают
Два свойства делают это дешёвым. Матрица мала по сравнению с отрендеренным битмапом, поэтому восемь транспонирований стоят куда меньше восьми рендеров страницы. И матрица — чистый булев массив, построенный сэмплером, так что никакой трансформ по пути не введёт значения, которые никогда не сэмплировались
Определение версии — поиск по делимости, а не деление
Число модулей нельзя вывести делением сэмплированной ширины на предполагаемый размер модуля, и ошибка здесь — тонкий источник провалов декодирования на рендерах высокого разрешения. QR-символ версии v — это 4v + 17 модулей в поперечнике, то есть версия 1 — 21 модуль, версия 40 — 177. Маска шириной 126 пикселей одинаково согласуется с версией 1 при шести пикселях на модуль и с несколькими старшими версиями при меньших размерах модуля. Линейное деление выберет одну из них и обычно ошибётся
Работает поиск по делимости среди кандидатов-версий. Идите от версии 40 вниз до версии 1, оставляйте кандидатов, чьё число модулей делит сэмплированную ширину нацело и оставляет хотя бы три пикселя на модуль, и берите наименьшую выжившую версию. Пол в три пикселя — то, что не даёт поиску принять абсурдно плотное прочтение грубого символа, а правило наименьшей версии снимает оставшуюся двусмысленность в пользу прочтения, которое реально выдал бы сканер
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 возвращает заполненную запись, а не обнулённую, и это важно, потому что DPI ноль или лимит результатов ноль — выглядящий валидным способ не получить ничего. RotationPolicy управляет только внешним retry: bdrpNone рендерит один раз, bdrpFallback ретраит остальные ориентации после провала первого прохода, а bdrpAll рендерит каждую ориентацию безусловно. Поскольку нормализация QR происходит внутри декодера, QR-страницы разрешаются с первой попытки при любой из трёх политик. Политика нужна линейным символикам, которым она реально необходима
Как доказать, что битмап-трансформ не выдумывает пиксели?
Посчитайте чернила по обе стороны и потребуйте совпадения сумм. Вращение — перестановка пикселей, и ничего больше, поэтому число ненулевых ячеек на выходе обязано равняться числу на входе. Когда вращение маски во внешнем retry-пути отрапортило 4800 установленных ячеек на входе и 7439 на выходе, одной этой сверки хватило, чтобы осудить трансформ, не читая ни строчки его геометрии
Причина оказалась будничной, и её стоит унести как правило. Динамический массив, размеченный SetLength, не гарантированно приходит обнулённым, когда он результат функции, идущий путём, который рантайм не чистит, и ячейки, которые вращение никогда не записывало, несут те байты, что лежали там раньше. Часть этих протухших байтов ненулевая, а ненулевое означает чернила. Фикс — одна строка, FillChar(Result[0], N, 0) до запуска цикла перестановки, а дисциплина шире: любая функция, возвращающая буфер маски или битмапа, должна явно чистить вывод, а не полагаться на семантику аллокации
Что позволило дефекту пережить три релиза, интереснее самого дефекта. Когда QR перенёс обработку ориентации в декодер, QR перестал вообще упражнять внешнее вращение маски, и единственным оставшимся потребителем этого пути кода остался Code 39. Общая инфраструктура постоянно прячет такие баги: покрытие от одной фичи делает путь выглядящим протестированным, тогда как фича, реально зависящая от него, не имеет своего. Каждый путь, который новая фича перестала использовать, нуждается в тесте, который всё ещё его использует
Чтение результатов обратно в координатах страницы
Каждое геометрическое значение, которое выдаёт декодер, выражено в системе координат attempt-битмапа, а вызывающему оно нужно в user space PDF. Конверсия идёт в два этапа: отменить четверть оборота, которую применил retry, затем отменить рендер-трансформ, отобразивший user space на битмап. То, что приходит в THPDFDecodedBarcode, — axis-aligned bounding box в user space, с Left, Bottom, Right и Top по PDF-конвенции, где Y растёт вверх, плюс counter-clockwise OrientationDegrees
Перепутаете направление второй конверсии — симптом мерзкий: текст декодируется идеально, но бокс, который вы рисуете для оверлея ревью, падает на зеркальное отражение правильной позиции. Кому строит интерфейс ревью поверх декодера, стоит проверить себя фикстурой с известным ответом: символ положен намеренно близко к углу страницы, чтобы перевёрнутая ось Y была видна с одного взгляда. Та же логика приложима к любой координате, пересекающей границу рендеринга, поэтому рендер страницы PDF в битмап в Delphi стоит понять, прежде чем строить поверх декодера
Что встроенный декодер умеет, а что нет
Встроенный декодер — ограниченная реализация без зависимостей, и она честна о своих пределах, а не деградирует тихо. Она распознаёт Code 39 и QR, валидирует защищённые BCH биты формата и паттерн маски, прежде чем публиковать любые данные, и не пытается восстанавливать повреждённые символы. Если ваш вход — фотография изогнутой наклейки при неровном свете, это другой класс задач, и ему нужен специализированный движок
// Подсадите свой движок: реализуйте IHPDFBarcodeDecoder и передайте его
// в перегрузку, принимающую декодер. HotPDF по-прежнему владеет рендером
// страниц, бюджетами, маппингом координат и дедупликацией
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 — то место, где продакшен-пайплайн отрабатывает своё. RotationAttemptCount и DecoderCallCount говорят, запускался ли вообще внешний retry, ReceivedResultCount против AcceptedResultCount разделяет декодер, не нашедший ничего, от порога уверенности, отвергнувшего всё найденное, а RenderedPixels с PeakWorkingBytes — то, что вы рисуете графиком, когда батч-задача начинает трэшить. Пустой набор результатов плюс bdsSucceeded означает, что на странице реально нет читаемого символа, — операционный факт иного рода, чем bdsBudgetExceeded
Бюджетные поля заслуживают осознанного решения, а не дефолта. MaxPixels и MaxWorkingBytes существуют потому, что DPI умножается квадратично: переход с 300 на 600 DPI на странице A4 учетверяет и стоимость рендера, и пиковую аллокацию, а недоверенный ввод, объявляющий огромный page box, может превратить задачу сканирования в инцидент с out-of-memory. Выставьте лимиты под худший легитимный документ, а выбросы пусть bdsBudgetExceeded маршрутизирует в более медленный изолированный путь
Если ваши документы мешают машиночитаемые наклейки с печатным текстом, который вы собираетесь индексировать, декодер штрихкодов естественно дружит с движком распознавания из template-matching OCR внутри HotPDF, а генеративная сторона той же истории — в рисовании штрихкодов в PDF с HotPDF. Оба работают на одной инфраструктуре рендеринга и бюджетов, поэтому пайплайн, уже выставивший разумные лимиты для одного, получает второй почти бесплатно
Толерантность к вращению — из тех фич, которые невидимы, когда работают, и бесят, когда нет, и инженерный урок обобщается за пределы QR: нормализуйте настолько близко к семантическому представлению, насколько сумеете добраться, а не на пиксельном слое, где данные всё ещё несут все случайности того, как их сняли. HotPDF поставляет это в составе Delphi PDF-компонента HotPDF рядом с рендерингом, OCR и анализом страниц, которые intake-пайплайнам обычно тоже нужны