مقاله فنی

کدگشایی کدهای QR چرخیده در صفحات PDF با HotPDF

HotPDF سمبل‌های QR چرخیده را در صفحه PDF بارگذاری‌شده با نرمال‌کردن ماتریس ماژول نمونه‌برداری‌شده از میان هر هشت جهت D4، درون خود کدگشا، کدگشایی می‌کند. تلاش مجدد با چرخش بیرونی که برای symbologyهای خطی جواب می‌دهد برای QR کار نمی‌کند، و فهمیدن چرایی یک روز دنبال‌کردن یک کدگشا را که خراب به نظر می‌رسد ولی نیست نجات‌تان می‌دهد

سناریو آن‌قدرهای معمولی است. برگه‌های تحویل اسکن‌شده به‌صورت PDF می‌رسند، هر صفحه یک برچسب QR دارد، و اپراتور اسکنر یک دسته برگه را در هر جهتی که سینی قبول کرد فرستاده. بعضی برچسب‌ها ایستاده‌اند، بعضی یک ربع چرخش اختلاف دارند، چندتایی هم وارونه‌اند. کدگشا بارکد را صدا می‌زنید، نیمی از صفحات حل می‌شوند و نیم دیگر بدون هیچ خطایی خالی برمی‌گردند

چرا چرخاندن ماسک اسکن هرگز یک QR چرخیده را fix نمی‌کند؟

چون چیدمان الگوی finder در QR عمداً نامتقارن است، و چرخش کل تصویر آن عدم‌تقارن را حفظ می‌کند نه حذفش. QR Code سه مربع finder را در گوشه‌های بالا-چپ، بالا-راست و پایین-چپ می‌گذارد و گوشه پایین-راست را خالی می‌گذارد (ISO/IEC 18004:2015 §6.3.3). همان گوشه غایب نشانه جهت‌گیری است. بیت‌مپ صفحه را نود درجه بچرخانید و حفره صرفاً به گوشه دیگری می‌رود. هیچ چرخش بدیهی‌ای از صفحه وجود ندارد که چیدمان سه‌گوشه‌ای را دوباره روی خودش نگاشت کند، پس کدگشایی که فقط چیدمان متعارف را قبول می‌کند هر تلاش را به نوبت رد می‌کند

این مهم است چون fix بدیهی همان fix غلط است. غریزه طبیعی این است که retry را به بیرون آویزان کنید: صفحه را رندر کنید، ماسک را به کدگشا بدهید، و اگر شکست خورد ماسک را بچرخانید و برای 90 و 180 و 270 درجه دوباره امتحان کنید. برای Code 39 این سیاست دقیقاً درست است، چون یک symbology خطی الگوی start و stopای دارد که اسکنر وقتی خط‌ها افقی شدند پیدایش می‌کند. برای QR چهار شکست تضمین‌شده است و بعد گزارشی که می‌گوید هیچ‌چیز پیدا نشد

گروه D4، اعمال‌شده روی ماتریس ماژول

جای درست نرمال‌سازی بعد از نمونه‌برداری است، روی گرید ماژول بولی نه روی ماسک پیکسلی. وقتی کدگشا سمبل را به یک ماتریس n در n از ماژول‌های تیره و روشن حل کرده، می‌تواند گروه دوجهته مربع را بشمارد: چهار چرخش ضربدر دو انعکاس، هشت جهت نامزد روی‌هم. برای هر نامزد مثلث finder را چک می‌کند، و اولین نامزدی که سه finder آن در جایگاه‌های بالا-چپ، بالا-راست و پایین-چپ فرود بیاید جهت واقعی است. از آن به بعد خط لوله موجود بدون تغییر اجرا می‌شود، چون بیت‌های format information، چیدمان زیگزاگی دیتا و تصحیح Reed-Solomon همگی یک ماتریس متعارف فرض می‌کنند و حالا یکی می‌گیرند

چهار نمایش همان ماتریس ماژول QR در HotPDF زیر چرخش‌های گروه D4 در 0 و 90 و 180 و 270 درجه، که نشان می‌دهد سه الگوی finder گوشه عوض می‌کنند در حالی که گوشه خالی همراهشان حرکت می‌کند، پس فقط جهت متعارف finderها را در بالا-چپ، بالا-راست و پایین-چپ به کدگشا ارائه می‌دهد
چرخاندن ماسک پیکسلی نمی‌تواند عدم‌تقارن finder در QR را حذف کند، پس HotPDF جهت‌های D4 را روی ماتریس ماژول نمونه‌برداری‌شده می‌شمارد و اولین نامزدی را که finderهایش در بالا-چپ، بالا-راست و پایین-چپ فرود بیایند نگه می‌دارد

دو property این را ارزان می‌کنند. ماتریس در مقایسه با بیت‌مپ رندرشده کوچک است، پس هشت transpose خیلی کمتر از هشت رندر صفحه هزینه دارد. و ماتریس یک آرایه بولی تمیز است که sampler ساخته، پس هیچ transformی در مسیر نمی‌تواند مقداری وارد کند که هرگز نمونه‌برداری نشده

تشخیص نسخه یک جستجوی بخش‌پذیری است نه یک تقسیم

شمار ماژول را نمی‌شود با تقسیم پهنای نمونه‌برداری‌شده بر یک اندازه ماژول فرضی به دست آورد، و غلط‌گرفتنش منبع ظریفی از شکست‌های کدگشایی روی رندرهای با تفکیک‌پذیری بالا است. سمبل QR با نسخه v به پهنای 4v + 17 ماژول است، پس نسخه 1، ۲۱ ماژول است و نسخه 40، ۱۷۷تا. ماسکی که ۱۲۶ پیکسل پهنا دارد هم با نسخه 1 در شش پیکسل به‌ازای هر ماژول سازگار است هم با چند نسخه بالاتر در اندازه‌های ماژول کوچک‌تر. تقسیم خطی یکی را انتخاب می‌کند و معمولاً هم غلط انتخاب می‌کند

کاری که جواب می‌دهد یک جستجوی بخش‌پذیری روی نسخه‌های نامزد است. از نسخه 40 تا نسخه 1 بگردید، نامزدهایی را نگه دارید که شمار ماژولشان پهنای نمونه‌برداری‌شده را بی‌باقی‌مانده می‌شمارد و به‌ازای هر ماژول دست‌کم سه پیکسل باقی می‌گذارد، و کوچک‌ترین نسخه بازمانده را بردارید. کف سه‌پیکسلی همان چیزی است که مانع می‌شود جستجو یک قرائت بی‌اندازه متراکم از یک سمبل درشت را بپذیرد، و قاعده کوچک‌ترین‌نسخه ابهام باقی‌مانده را به سود قرائتی حل می‌کند که یک اسکنر واقعاً تولید می‌کرد

گردش تشخیص نسخه HotPDF برای سمبل QR روی ماسک نمونه‌برداری‌شده ۱۲۶ پیکسلی، که شمار ماژول نامزد یعنی 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 فقط retry بیرونی را کنترل می‌کند: bdrpNone یک بار رندر می‌کند، bdrpFallback بعد از یک گذر اول ناموفق جهت‌های دیگر را دوباره امتحان می‌کند، و bdrpAll هر جهت را بی‌قید و شرط رندر می‌کند. چون نرمال‌سازی QR درون کدگشا اتفاق می‌افتد، صفحات QR تحت هر سه سیاست در همان تلاش اول حل می‌شوند. این سیاست برای symbologyهای خطی‌ای است که واقعاً به آن نیاز دارند

چطور اثبات می‌کنید یک transform بیت‌مپی پیکسل از خودش نمی‌سازد؟

جوه را در دو طرف بشمارید و الزام کنید مجموع‌ها برابر باشند. چرخش یک جایگشت از پیکسل‌ها است، نه بیشتر، پس تعداد سلول‌های غیرصفر در خروجی باید برابر تعدادشان در ورودی باشد. وقتی یک چرخش ماسک در مسیر retry بیرونی 4800 سلول ست‌شده ورود و 7439 سلول خروج گزارش کرد، همین یک مقایسه کافی بود تا transform را محکوم کند بدون خواندن حتی یک خط از هندسه‌اش

علت پیش‌پاافتاده بود و ارزش دارد به‌عنوان یک قاعده با خودتان ببرید. آرایه پویایی که با SetLength اندازه گرفته شده تضمینی ندارد که صفرشده برسد وقتی نتیجه یک تابع است که مسیری را طی می‌کند که runtime پاکش نمی‌کند، و سلول‌هایی که چرخش هرگز نمی‌نویسد همان بایت‌های قبلی را حمل می‌کنند. بعضی از آن بایت‌های کهنه غیرصفرند، و غیرصفر یعنی جوهر. fix یک خط است، FillChar(Result[0], N, 0) پیش از اجرای حلقه جایگشت، و انضباطی که تحمیل می‌کند فراتر می‌رود: هر تابعی که ماسک یا بافر بیت‌مپ برمی‌گرداند باید خروجی‌اش را صریح پاک کند نه اینکه به معناشناسی تخصیص تکیه کند

چیزی که این عیب را سه انتشار زنده نگه داشت جالب‌تر از خود عیب است. وقتی QR مدیریت جهتش را به کدگشا منتقل کرد، QR دیگر اصلاً چرخش ماسک بیرونی را ورز نمی‌داد، و تنها consumer باقی‌مانده آن مسیر کد Code 39 بود. زیرساخت مشترک همیشه همین‌جوری باگ قایم می‌کند: پوشش یک قابلیت مسیری را تست‌شده نشان می‌دهد در حالی که قابلیتی که واقعاً به آن وابسته است هیچ پوشش خودش را ندارد. هر مسیری که یک قابلیت جدید دست از استفاده‌اش برمی‌دارد به تستی نیاز دارد که هنوز استفاده‌اش کند

خواندن نتیجه‌ها در مختصات صفحه

هر مقدار هندسی که کدگشا تولید می‌کند در چارچوب مختصاتی بیت‌مپ تلاش بیان شده، و caller به آن در فضای کاربری PDF نیاز دارد. این تبدیل در دو مرحله اجرا می‌شود: خنثی‌کردن چرخش ربعی که retry اعمال کرد، بعد خنثی‌کردن transform رندر که فضای کاربری را روی بیت‌مپ نگاشت کرد. چیزی که در THPDFDecodedBarcode می‌رسد یک جعبه محیطی هم‌محور در فضای کاربری است، با Left و Bottom و Right و Top مطابق قرارداد PDF که Y رو به بالا رشد می‌کند، به‌علاوه یک OrientationDegrees پادساعتگرد

خط لوله بارکد HotPDF از بیت‌مپ صفحه رندرشده به نمونه‌برداری به ماتریس ماژول بولی، نرمال‌سازی D4، تشخیص نسخه با بخش‌پذیری و کدگشایی Reed-Solomon، و بعد تبدیل مختصات دومرحله‌ای که چرخش ربعی retry و transform رندر را خنثی می‌کند پیش از آنکه THPDFDecodedBarcode مقادیر Left و Bottom و Right و Top و OrientationDegrees را در فضای کاربری منتشر کند
نرمال‌سازی QR درون کدگشا اجازه می‌دهد صفحات در همان تلاش اول حل شوند، در حالی که تبدیل مختصات دومرحله‌ای نتیجه‌های بیت‌مپ تلاش را به جعبه‌های هم‌محور فضای کاربری تبدیل می‌کند

جهت آن تبدیل دوم را غلط بگیرید و علامت کثیف است: متن بی‌نقص کدگشایی می‌شود، ولی جعبه‌ای که برای یک overlay بازبینی می‌کشید روی تصویر آینه‌ایِ موقعیت درست فرود می‌آید. هر کسی که روی کدگشا یک interface بازبینی می‌سازد باید روی یک fixture معروف assert بگذارد، با سمبی که عمداً نزدیک یک گوشه صفحه گذاشته شده تا محور Y وارونه یک نگاه پیدا باشد. همان استدلال روی هر مختصاتی که مرز رندر را رد می‌کند صادق است، به همین دلیل است که رندر صفحه PDF به بیت‌مپ در Delphi ارزش فهمیدن دارد قبل از اینکه روی کدگشا بسازید

کدگشای داخلی چه می‌کند و چه نمی‌کند

کدگشای داخلی یک پیاده‌سازی محدوددامنه و بدون وابستگی است، و درباره محدودیت‌هایش صادق است نه اینکه بی‌سروصدا افت کند. Code 39 و QR را می‌شناسد، بیت‌های format محافظت‌شده با BCH و الگوی ماسک را پیش از انتشار هر داده‌ای اعتبارسنجی می‌کند، و روی سمبل‌های آسیب‌دیده تلاشی برای بازیابی خطا نمی‌کند. اگر ورودی شما عکسی از یک برچسب خمیده زیر نور ناهمگن است، آن یک رده مسئله دیگر است و موتور تخصصی می‌خواهد

// موتور خودتان را جایگزین کنید: IHPDFBarcodeDecoder را پیاده کنید و به
// overload آگاه-از-کدگشا پاس بدهید. 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 جایی است که خط لوله production نانش را درمی‌آورد. RotationAttemptCount و DecoderCallCount به شما می‌گویند اصلاً retry بیرونی اجرا شده یا نه، ReceivedResultCount در برابر AcceptedResultCount کدگشایی را که هیچ چیز پیدا نکرده از آستانه اطمینانی را که همه چیزهای پیدا شده را رد کرده جدا می‌کند، و RenderedPixels همراه PeakWorkingBytes چیزی است که وقتی یک کار دسته‌ای شروع به تلاطم می‌کند ترسیمش می‌کنید. مجموعه نتیجه خالی به‌علاوه bdsSucceeded یعنی صفحه واقعاً هیچ سمبل خوانایی ندارد، که یک واقعیت عملیاتی متفاوت از bdsBudgetExceeded است

فیلدهای بودجه ارزش یک تصمیم آگاهانه دارند نه یک پیش‌فرض. MaxPixels و MaxWorkingBytes به این دلیل وجود دارند که DPI به‌طور مربعی ضرب می‌شود: رفتن از ۳۰۰ به ۶۰۰ DPI روی یک صفحه A4 هم هزینه رندر و هم پیک تخصیص را چهار برابر می‌کند، و یک ورودی نامطمئن که جعبه صفحه غول‌آسایی اعلام کند می‌تواند یک کار اسکن را به یک حادثه کمبود حافظه تبدیل کند. سقف‌ها را به اندازه بدترین سند مشروع‌تان ست کنید، بعد اجازه بدهید bdsBudgetExceeded خارج‌ازقاعده‌ها را به مسیری کندتر و ایزوله هدایت کند

اگر اسنادتان برچسب‌های ماشین‌خوان را با متن چاپی‌ای که قصد نمایه‌کردنش را دارید قاطی می‌کنند، کدگشا بارکد به‌طور طبیعی با موتور بازشناسی که در OCR تطبیق قالب درون HotPDF پوشش داده شد جفت می‌شود، و سمت تولید همان ماجرا در کشیدن بارکد در PDF با HotPDF است. هر دو روی همان زیرساخت رندر و بودجه اجرا می‌شوند، پس خط لوله‌ای که از قبل برای یکی سقف‌های منطقی ست کرده دومی را تقریباً مجانی می‌گیرد

تحمل چرخش از آن قابلیت‌هاست که وقتی کار می‌کند نامرئی است و وقتی نمی‌کند خشمگین‌کننده، و درس مهندسی‌اش از QR فراتر می‌رود: نرمال‌سازی را هرچه به بازنمایی معنایی نزدیک‌تر است انجام دهید، نه در لایه پیکسل که داده هنوز هر اتفاق نحوه گرفتنش را حمل می‌کند. HotPDF این را به‌عنوان بخشی از HotPDF Delphi PDF component عرضه می‌کند، کنار قطعه‌های رندر، OCR و تحلیل صفحه که همان خط لوله‌های دریافت معمولاً لازمشان دارند