HotPDF מפענח סמלי QR מסובבים בעמוד PDF שנטען על ידי נרמול מטריצת ה-modules הנדגמת דרך כל שמונה הכיוונים של D4 בתוך המפענח עצמו. ניסיון הסיבוב החוזר החיצוני שעובד עבור סימבולוגיות ליניאריות לא יכול לעבוד עבור QR, והבנת הסיבה חוסכת לך יום של מרדף אחרי מפענח שנראה שבור ואיננו
התרחיש מוכר לגמרי. תעודות משלוח סרוקות מגיעות כ-PDF, כל עמוד נושא תווית QR, ומפעיל הסורק הזין ערימת דפים בכל כיוון שהמגש הסכים לקבל. חלק מהתוויות זקופות, חלק סטו ברבע סיבוב, ואחדות הפוכות. קוראים למפענח הברקודים, חצי מהעמודים נפתרים, והחצי השני חוזר ריק בלי שום שגיאה
מדוע סיבוב מסכת הסריקה לעולם לא מתקן QR מסובב?
כי פריסת דפוסי ה-finder של QR אסימטרית בכוונה, וסיבוב של תמונה שלמה משמר את האסימטריה הזאת במקום להסיר אותה. QR Code מציב שלושה ריבועי finder בפינות שמאל-עליון, ימין-עליון ושמאל-תחתון, ומשאיר את הפינה השמאל-תחתונה ריקה (ISO/IEC 18004:2015 §6.3.3). אותה פינה חסרה היא רמז הכיוון. מסובבים את ה-bitmap של העמוד בתשעים מעלות והחור פשוט עובר לפינה אחרת. אין סיבוב לא-טריוויאלי של המישור שמחזיר פריסה של שלוש פינות אל עצמה, ולכן מפענח שמקבל רק את הסידור הקנוני ידחה כל ניסיון בתורו
זה חשוב כי התיקון המובן מאליו הוא התיקון הלא נכון. האינסטינקט הטבעי הוא לתלות את הניסיון החוזר בחוץ: לעבד את העמוד, למסור את המסכה למפענח, ואם זה נכשל, לסובב את המסכה ולנסות שוב ב-90, ב-180 וב-270 מעלות. עבור Code 39 המדיניות הזאת בדיוק נכונה, כי לסימבולוגיה ליניארית יש דפוסי התחלה וסיום שהסורק מוצא ברגע שהפסים רצים אופקית. עבור QR זה ארבעה כישלונות מובטחים ואחר כך דיווח שלא נמצא דבר
קבוצת D4, מיושמת על מטריצת ה-modules
המקום הנכון לנרמול הוא אחרי הדגימה, על רשת ה-modules הבוליאנית ולא על מסכת הפיקסלים. ברגע שהמפענח הצליח לפתור את הסמל למטריצה של n על n של modules כהים ובהירים, הוא יכול למנות את הקבוצה הדיאדרלית של הריבוע: ארבעה סיבובים כפול שתי השתקפויות, שמונה כיווני מועמדים בסך הכל. עבור כל מועמד הוא בודק את משולש ה-finder, והמועמד הראשון ששלושת ה-finder שלו נוחתים בעמדות שמאל-עליון, ימין-עליון ושמאל-תחתון הוא הכיוון האמיתי. משם ה-pipeline הקיים רץ ללא שינוי, כי סיביות מידע הפורמט, מקומם של הנתונים בזיגזג ותיקון Reed-Solomon מניחים כולם מטריצה קנונית ועכשיו מקבלים כזאת
שני מאפיינים הופכים את זה לזול. המטריצה קטנה לעומת ה-bitmap המעובד, כך ששמונה transposes עולים הרבה פחות משמונה עיבודי עמוד. והמטריצה היא מערך בוליאני נקי שנבנה על ידי הדוגם, כך שאף טרנספורמציה בדרך לא יכולה להכניס ערכים שמעולם לא נדגמו
זיהוי הגרסה הוא חיפוש התחלקות, לא חילוק
את מספר ה-modules אי אפשר לגזור מחלוקת רוחב הדגימה בגודל module מונח, וטעות כאן היא מקור עדין לכישלונות פענוח בעיבודים ברזולוציה גבוהה. סמל QR של גרסה v הוא בן 4v + 17 modules, כך שגרסה 1 היא 21 modules וגרסה 40 היא 177. מסכה שרוחבה 126 פיקסלים עקבית באותה מידה עם גרסה 1 בשישה פיקסלים ל-module ועם כמה גרסאות גבוהות יותר בגדלי module קטנים. חילוק ליניארי בוחר אחת מהן ובדרך כלל טועה
מה שעובד הוא חיפוש התחלקות על פני הגרסאות המועמדות. הולכים מגרסה 40 ומטה עד גרסה 1, שומרים על המועמדים שמספר ה-modules שלהם מחלק את רוחב הדגימה באופן שלם ומשאיר לפחות שלושה פיקסלים לכל module, ולוקחים את הגרסה הקטנה ביותר ששרדה. רצפת שלושת הפיקסלים היא מה שעוצר את החיפוש מלקבל קריאה מטורפת בצפיפות של סמל גס, וכלל הגרסה-הקטנה מפזר את העמימות הנותרת לטובת הקריאה שסורק באמת היה מייצר
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 נפתרים בניסיון הראשון תחת כל אחת משלוש המדיניות. המדיניות נוכחת בשביל הסימבולוגיות הליניאריות שבאמת זקוקות לה
איך מוכיחים שטרנספורמציה של bitmap לא ממציאה פיקסלים?
סופרים את הדיו משני הצדדים ודורשים שהסכומים ישתוו. סיבוב הוא תמורה של פיקסלים, ולא יותר, ולכן מספר התאים הלא-אפסיים בפלט חייב להשתוות למספרם בקלט. כשסיבוב מסכה בנתיב הניסיון החוזר החיצוני דיווח 4800 תאים דולקים בכניסה ו-7439 ביציאה, השוואה אחת זו הספיקה להרשיע את הטרנספורמציה בלי לקרוא שורה אחת מהגיאומטריה שלה
הגורם היה מונדיאלי ושווה לשאת משם ככלל. מערך דינמי שמוקצה עם SetLength אינו מובטח להגיע מאופס כשהוא תוצאת פונקציה שנוסעת בנתיב שהרנטיים לא מנקה, ותאים שהסיבוב מעולם לא כתב נושאים אז את הבייטים שהיו שם קודם. חלק מהבייטים הישנים האלה לא-אפסיים, ולא-אפס פירושו דיו. התיקון שורה אחת, FillChar(Result[0], N, 0) לפני שלולאת התמורה רצה, והמשמעת שהיא כופה רחבה יותר: כל פונקציה שמחזירה מסכה או חוצץ bitmap צריכה לנקות את הפלט שלה במפורש במקום לסמוך על סמנטיקת ההקצאה
מה שגרם לפגם לשרוד שלוש גרסאות מעניין מהפגם עצמו. מרגע ש-QR העביר את טיפול הכיוון לתוך המפענח, QR הפסיק להפעיל בכלל את סיבוב המסכה החיצוני, והצרכן היחיד שנותר של אותו נתיב קוד היה Code 39. תשתית משותפת מסתירה באגים כאלה כל הזמן: כיסוי מתכונה אחת גורם לנתיב להיראות נבדק בזמן שהתכונה שבאמת תלויה בו אינה מחזיקה בדיקה משלה. כל נתיב שתכונה חדשה מפסיקה להשתמש בו זקוק לבדיקה שעדיין משתמשת בו
קריאת התוצאות חזרה בקואורדינטות עמוד
כל ערך גיאומטרי שהמפענח מייצר מבוטא במסגרת הקואורדינטות של bitmap הניסיון, והקורא לפונקציה צריך אותו במרחב המשתמש של PDF. ההמרה רצה בשני שלבים: לבטל את רבע הסיבוב שהניסיון החוזר החיל, ואז לבטל את הטרנספורמציה של העיבוד שמיפתה את מרחב המשתמש אל ה-bitmap. מה שמגיע ב-THPDFDecodedBarcode הוא תיבת חוסך מיושרת צירים במרחב המשתמש, עם Left, Bottom, Right ו-Top לפי מוסכמת ה-PDF ש-Y גדל כלפי מעלה, בתוספת OrientationDegrees נגד כיוון השעון
מקבלים את כיוון ההמרה השנייה לא נכון והסימפטום מעולה-מרושע: הטקסט מפוענח מושלם, אבל התיבה שמציירים עבור שכבת סימון לסקירה נוחתת על תמונת המראה של המקום הנכון. כל מי שבונה ממשק סקירה מעל המפענח צריך לטעון מול fixture מוכר, עם סמל שהוצב בכוונה ליד אחת מפינות העמוד כך שציר Y הפוך נראה במבט אחד. אותו היגיון חל על כל קואורדינטה שחוצה את גבול העיבוד, וזו הסיבה שעיבוד עמוד PDF ל-bitmap ב-Delphi שווה הבנה לפני שבונים מעל המפענח
מה המפענח המובנה כן ולא עושה
המפענח המובנה הוא מימוש מוגבל וחף מתלויות, והוא כן לגבי גבולותיו במקום להתדרדר בשקט. הוא מזהה Code 39 ו-QR, מאמת את סיביות הפורמט המוגנות ב-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 הוא המקום שבו pipeline של פרודקשן מרוויח את לחמו. RotationAttemptCount ו-DecoderCallCount אומרים לך אם הניסיון החוזר החיצוני רץ בכלל, ReceivedResultCount מול AcceptedResultCount מפריד מפענח שלא מצא דבר מסף ביטחון שדחה הכל שמצא, ו-RenderedPixels עם PeakWorkingBytes הוא מה שמגרפים כשעבודת אצווה מתחילה להתקשות. קבוצת תוצאות ריקה בתוספת bdsSucceeded אומרת שלעמוד באמת אין סמל קריא, וזו עובדה תפעולית אחרת מ-bdsBudgetExceeded
שדות התקציב דורשים החלטה מכוונת ולא ברירת מחדל. MaxPixels ו-MaxWorkingBytes קיימים כי DPI מכפיל בריבוע: מעבר מ-300 ל-600 DPI על עמוד A4 מכפיל בארבעה הן את עלות העיבוד והן את הקצאת השיא, וקלט לא מהימן שמכריז על תיבת עמוד עצומה יכול להפוך עבודת סריקה לאירוע של אין-זיכרון. קובעים את התקרות למה שהמסמך הלגיטימי הגרוע שלך צריך, ואז משאירים ל-bdsBudgetExceeded לנתב חריגים לנתיב איטי ומבודד
אם המסמכים שלך מערבבים תוויות קריאות-למכונה עם טקסט מודפס שמתכננים לאנדקס, מפענח הברקודים מזדווג באופן טבעי עם מנוע הזיהוי שמכוסה בOCR מבוסס התאמת תבניות בתוך HotPDF, וצד הייצור של אותו סיפור נמצא בציור ברקודים לתוך PDF עם HotPDF. שניהם רצים על אותה תשתית עיבוד ותקציבים, כך ש-pipeline שכבר קובע גבולות סבירים לאחד מקבל את השני כמעט בחינם
סובלנות הסיבוב היא אחת התכונות הבלתי נראות כשהן עובדות ומעציבות כשהן לא, והלקח ההנדסי מתכלל מעבר ל-QR: מנרמלים כמה שיותר קרוב לייצוג הסמנטי, ולא בשכבת הפיקסלים שבה הנתונים עדיין נושאים כל תאונה של איך נתפסו. HotPDF משלח זאת כחלק מרכיב ה-PDF של HotPDF ל-Delphi, לצד חלקי העיבוד, ה-OCR וניתוח העמודים ש-pipeline של קליטה בדרך כלל צריך