Технічна стаття

Декодування повернутих QR-кодів у сторінках PDF з HotPDF

HotPDF декодує повернуті QR-символи на завантаженій сторінці PDF, нормалізуючи вибрану матрицю модулів крізь усі вісім орієнтацій D4 всередині самого декодера. Зовнішній повтор із обертанням, який працює для лінійних символогій, для QR працювати не може, і розуміння того, чому, економить вам день полювання на декодер, який виглядає зламаним, але ним не є

Сценарій цілком буденний. Скановані накладні приходять у PDF, кожна сторінка несе QR-етикетку, а оператор сканера годував стос аркушів у тому напрямку, який прийняв лоток. Деякі етикетки стоять прямо, деякі зсунуті на чверть обороту, кілька — догори ногами. Ви викликаєте декодер штрихкодів, половина сторінок розв'язується, а друга половина повертається порожньою, без жодної помилки

Чому обертання маски скану ніколи не чинить повернутий QR?

Бо розкладка пошукового патерну QR навмисно асиметрична, а обертання всього зображення зберігає цю асиметрію замість того, щоб її зняти. QR Code ставить три пошукові квадрати в кутах зверху-ліворуч, зверху-праворуч і знизу-ліворуч, а кут знизу-праворуч лишає порожнім (ISO/IEC 18004:2015 §6.3.3). Той відсутній кут — це орієнтаційна підказка. Оберніть бітмап сторінки на дев'яносто градусів — і проміжок просто переїде в інший кут. Не існує нетривіального обертання площини, яке відобразило б трикутну розкладку на саму себе, тож декодер, який приймає лише канонічне розташування, відхилить кожну спробу по черзі

Це важливо тому, що очевидний фікс — неправильний. Природний інстинкт — повісити повтор зовні: відрендерити сторінку, віддати маску декодеру, а якщо провал — обернути маску й спробувати знову на 90, 180 і 270 градусів. Для Code 39 така політика рівно правильна, бо лінійна символогія має старт- і стоп-патерн, який сканер знайде, щойно смуги ляжуть горизонтально. Для QR це чотири гарантовані провали, за якими слідує звіт про нічого не знайдено

Група D4, застосована до матриці модулів

Правильне місце для нормалізації — після семплювання, на булевій сітці модулів, а не на піксельній масці. Щойно декодер розв'язав символ у матрицю темних і світлих модулів розміром n на n, він може перелічити діедральну групу квадрата: чотири обертання разом із двома відображеннями, вісім кандидатів орієнтацій разом. Для кожного кандидата він перевіряє пошуковий трикутник, і перший кандидат, чиї три пошукові патерни приземляються в позиції зверху-ліворуч, зверху-праворуч і знизу-ліворуч, — це справжня орієнтація. Далі наявний конвеєр працює без змін, бо біти інформації формату, зигзаг-розкладання даних і корекція Reed-Solomon усі припускають канонічну матрицю — і тепер її отримують

Чотири зображення тієї самої матриці модулів QR у HotPDF під обертаннями групи D4 на 0, 90, 180 і 270 градусів, що показують три пошукові патерни, які мігрують по кутах, поки порожній кут рухається з ними, тож лише канонічна орієнтація підносить декодеру пошукові патерни зверху-ліворуч, зверху-праворуч і знизу-ліворуч
Обертання піксельної маски не може зняти асиметрію пошукових патернів QR, тож HotPDF перелічує орієнтації D4 на семпльованій матриці модулів і тримає першого кандидата, чиї пошукові патерни приземляються зверху-ліворуч, зверху-праворуч і знизу-ліворуч

Дві властивості роблять це дешевим. Матриця мала в порівнянні з відрендереним бітмапом, тож вісім транспонувань коштують значно менше, ніж вісім рендерів сторінки. І матриця — чистий булевий масив, збудований семплером, тож жодне перетворення по дорозі не може внести значень, які ніколи не семплювалися

Визначення версії — це пошук подільності, а не ділення

Кількість модулів не вивести діленням семпльованої ширини на припущений розмір модуля, і помилка тут — тонке джерело відмов декодування на високороздільних рендерах. QR-символ версії v має 4v + 17 модулів у ширину, тож версія 1 — це 21 модуль, а версія 40 — 177. Маска шириною 126 пікселів однаково сумісна з версією 1 по шість пікселів на модуль і з кількома вищими версіями за менших розмірів модуля. Лінійне ділення обирає одне з них і зазвичай помиляється

Працює пошук подільності по кандидатах версій. Ідіть від версії 40 вниз до версії 1, тримайте кандидатів, чия кількість модулів ділить семпльовану ширину без остачі й лишає щонайменше три пікселі на модуль, і беріть найменшу версію, що вижила. Стеля в три пікселі — це те, що не дає пошуку прийняти абсурдно щільне прочитання грубого символу, а правило найменшої версії розв'язує решту неоднозначності на користь прочитання, яке справді продукував би сканер

Прохід визначення версії HotPDF для QR-символа на семпльованій масці 126 пікселів: перевірка кожної кандидатської кількості модулів 4v плюс 17 від версії 40 вниз до версії 1 на рівну подільність і стелю модуля в три пікселі, перш ніж перемагає найменша версія, що вижила
Кількість модулів QR приходить із пошуку подільності по кандидатських версіях, а не з ділення ширини маски на припущений розмір модуля, і найменша версія, що вижила, розв'язує неоднозначність
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 керує лише зовнішнім повтором: bdrpNone рендерить один раз, bdrpFallback повторює інші орієнтації після проваленого першого проходу, а bdrpAll рендерить кожну орієнтацію безумовно. Оскільки QR-нормалізація відбувається всередині декодера, QR-сторінки розв'язуються з першої спроби за будь-якої з трьох політик. Політика — для лінійних символогій, які справді її потребують

Як довести, що бітмап-перетворення не вигадує пікселів?

Порахуйте чорнило з обох боків і вимагайте збігу сум. Обертання — це перестановка пікселів, і нічого більше, тож кількість ненульових комірок у виході мусить дорівнювати кількості у вході. Коли обертання маски у зовнішньому шляху повтору повідомило 4800 встановлених комірок на вході та 7439 на виході, того одного порівняння вистачило, щоб засудити перетворення, не читаючи жодного рядка його геометрії

Причина була буденна і варта того, щоб забрати її як правило. Динамічний масив, розмірений через SetLength, не гарантує прийти обнуленим, коли він — результат функції, який іде шляхом, який рантайм не чистить, і комірки, які обертання ніколи не писало, несуть тоді ті байти, що були там раніше. Частина цих застарілих байтів ненульова, а ненульове означає чорнило. Фікс — один рядок, FillChar(Result[0], N, 0) перед запуском циклу перестановки, а дисципліна, яку він імплікує, ширша: будь-яка функція, що повертає буфер маски чи бітмапу, має явно чистити свій вивід, замість того щоб покладатися на семантику виділення

Те, чому дефект пережив три релізи, цікавіше за сам дефект. Щойно QR переніс обробку орієнтації в декодер, QR перестав зовсім задіювати зовнішнє обертання маски, і єдиним рештою споживачем того кодового шляху лишився Code 39. Спільна інфраструктура ховає такі баги постійно: покриття від однієї можливості робить шлях протестованим на вигляд, тоді як можливість, що реально від нього залежить, не має власного. Кожен шлях, який нова можливість перестала використовувати, потребує тесту, який його досі використовує

Читання результатів назад у координатах сторінки

Кожне геометричне значення, яке продукує декодер, виражене в координатній системі бітмапу спроби, а викликаючому воно потрібне в користувацькому просторі PDF. Та конвертація йде у два етапи: скасувати чверть обороту, який застосував повтор, потім скасувати рендер-перетворення, яке відобразило користувацький простір на бітмап. Те, що приходить у THPDFDecodedBarcode, — це вирівняний по осях bounding box у користувацькому просторі, з Left, Bottom, Right і Top за конвенцією PDF, де Y зростає вгору, плюс проти годинникової стрілки OrientationDegrees

Конвеєр штрихкодів HotPDF від відрендереного бітмапу сторінки крізь семплювання в булеву матрицю модулів, нормалізацію D4, визначення версії подільністю та декодування Reed-Solomon, а потім двоетапна конвертація координат, яка скасовує чверть обороту повтору та рендер-перетворення, перш ніж THPDFDecodedBarcode опублікує Left, Bottom, Right, Top і OrientationDegrees у користувацькому просторі
QR-нормалізація всередині декодера дозволяє сторінкам розв'язуватися з першої спроби, тоді як двоетапна конвертація координат перетворює результати бітмапу спроби на вирівняні по осях бокси користувацького простору

Переплутайте напрям тієї другої конвертації — і симптом огидний: текст декодується ідеально, але бокс, який ви малюєте для оверлея рецензування, приземляється на дзеркальне відображення правильної позиції. Кожен, хто будує інтерфейс рецензування поверх декодера, мусить стверджувати проти відомої фікстури — із символом, навмисно поставленим біля одного з кутів сторінки, тож перевернута вісь 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 кажуть, чи взагалі запускався зовнішній повтор, ReceivedResultCount проти AcceptedResultCount розділяє декодер, який не знайшов нічого, від порога впевненості, який відхилив усе знайдене, а RenderedPixels із PeakWorkingBytes — це те, що ви графуєте, коли пакетна робота починає трешити. Порожній набір результатів плюс bdsSucceeded означає, що сторінка справді не має читабельного символу, — це інший операційний факт, ніж bdsBudgetExceeded

Бюджетні поля заслуговують на навмисне рішення, а не на дефолт. MaxPixels і MaxWorkingBytes існують тому, що DPI множиться квадратично: перехід із 300 на 600 DPI на сторінці A4 чотириразово збільшує і вартість рендеру, і пікове виділення, а недовірений вхід, що декларує величезний бокс сторінки, може перетворити скан-роботу на інцидент браку пам'яті. Встановіть стелі на те, що потребує ваш найгірший легітимний документ, а тоді нехай bdsBudgetExceeded скеровує аутлаєрів у повільніший, ізольований шлях

Якщо ваші документи змішують машинозчитувані етикетки з друкованим текстом, який ви плануєте індексувати, декодер штрихкодів природно париється з рушієм розпізнавання, розглянутим у OCR на шаблонному збігу всередині HotPDF, а генераційний бік тієї самої історії — у малюванні штрихкодів у PDF з HotPDF. Обидва працюють на тій самій інфраструктурі рендерингу та бюджетів, тож конвеєр, який уже встановив розумні ліміти для одного, отримує другий майже безкоштовно

Толерантність до обертання — одна з тих можливостей, які невидимі, коли працюють, і шалені, коли ні, і інженерний урок узагальнюється за межі QR: нормалізуйте якнайближче до семантичного подання, а не на піксельному шарі, де дані досі несуть кожен нещасний випадок того, як їх було знято. HotPDF постачає це в складі HotPDF Delphi PDF component, поруч із частинами рендерингу, OCR та аналізу сторінок, які ті самі приймальні конвеєри зазвичай потребують