HotPDF משווה שני מסמכי PDF מתוך דלפי דרך THPDFDocComparison, שעוברת על גרף האובייקטים של שני הקבצים מהקטלוג כלפי חוץ, וכאשר מתבקש, גם מציגה כל זוג עמודים ומודדת את הפיקסלים שנבדלים. התוצאה היא דוח JSON הנוקב כל הבדל שנמצא, את התקציב שנוצל, ואם ההשוואה רצה עד סופה. שני המעברים חשובים, מכיוון שהשוואה מבנית והשוואה חזותית עונות על שאלות שונות
השאלה שמאחורי התכונה היא בדרך כלל שאלת שחרור. מנוע דוחות מקבל שינוי, הפלט נוצר מחדש, ומישהו צריך להחליט אם משהו זז. פתיחת שני הקבצים זה לצד זה מתפקדת עד כשלושה עמודים לפני שהקשב נכשל. השוואת בייטים גולמיים נכשלת מיד, מכיוון ששתי ריצות של אותו מחולל מייצרות בייטים שונים מסיבות שאין להן שום קשר למה שקורא רואה
למה קבצי PDF יכולים להיות שונים בבייטים וזהים חזותית?
שני קבצי PDF שנוצרו באופן עצמאי ומדפיסים באופן זהה נבדלים באופן שגרתי בבייטים שלהם, והסיבות הן מבניות ולא קוסמטיות. מספרי אובייקטים מוקצים לפי הסדר שבו האובייקטים במקרה נכתבים. תת-קבוצות גופנים מקצות CID לפי הסדר שבו גליפים נתקלים בהם לראשונה, כך שתת-קבוצה שנבנתה במהלך מעבר שונה במעט מייצרת בייטים שונים של זרם תוכן עבור אותו טקסט גלוי. היסטי טבלת ההפניות המצטלבות זזים בכל פעם שמשהו במעלה הזרם משנה אורך
זו הסיבה שמספרי אובייקטים אינם יכולים לשמש כזהות בין-מסמכית. במקום זאת, HotPDF בונה כל תמונת מצב על ידי מעבר מהקטלוג, פתיחת מילונים בסדר הבייטים של המפתחות שלהם ומערכים לפי אינדקס, כך שכל אובייקט נקוב לפי הנתיב שמגיע אליו. אובייקטים שהמעבר לא מצליח להגיע אליהם מהשורש נופלים בחזרה לנתיב סינתטי $Unreachable[...] הנושא את מספר האובייקט והדור, מה ששומר על תוכן יתום גלוי בדוח במקום נעדר בשקט
זרמים אינם מושווים בהעתקה. כל זרם תורם חתימת SHA-256 מצטברת, מחושבת תוך שחזור מיקום הזרם המקורי לאחר מכן, כך שהשוואת שני קבצים של מאה מגה-בייט לא אומרת שממש יוצרים ארבע מאות מגה-בייט
יישור עמודים כשמסמך אחד קיבל הוספה
השוואת עמוד 1 מול עמוד 1, עמוד 2 מול עמוד 2 וכן הלאה נכונה רק כאשר שום דבר לא הוכנס. הוסיפו עמוד שער והשוואה נאיבית תדווח שכל עמוד השתנה, מה שנכון טכנית וחסר תועלת תפעולית
HotPDF מיישרת עמודים לפני השוואתם. היא בונה חתימה לכל עמוד מטקסט הניתן לחילוץ, נופלת בחזרה לחתימה מבנית עבור עמודים ללא טקסט, ואז מחשבת את תת-הרצף העולה הארוך ביותר על פני אינדקסי היעד המותאמים. עמודים בתוך תת-רצף זה הם אלה שרק זזו; עמודים מחוצה לו הם הזזות אמיתיות. ההבחנה הזו היא מה שהופך השוואה של מדריך בן 400 עמודים לקריאה, מכיוון שהדוח אומר שעמוד אחד הוכנס במקום שארבע מאות עמודים השתנו
הרצת השוואה מבנית
הקריאה הפשוטה ביותר מקבלת שני מסמכים טעונים ומצב. cmStructural מבצעת את המעבר על גרף האובייקטים, cmRenderedImage מבצעת את השוואת הפיקסלים, cmFull עושה את שתיהן, והמצבים הקלים יותר cmPageCount, cmPageText ו-cmObjectCount קיימים לבדיקות עשן זולות:
uses
HPDFDoc, HPDFDocCompare;
var
DocA, DocB: THotPDF;
Report: AnsiString;
begin
DocA := THotPDF.Create(nil);
DocB := THotPDF.Create(nil);
try
if (DocA.LoadFromFile('baseline.pdf') <= 0) or
(DocB.LoadFromFile('candidate.pdf') <= 0) then
Exit;
Report := THPDFDocComparison.Compare(DocA, DocB, cmStructural);
with TFileStream.Create('diff.json', fmCreate) do
try
WriteBuffer(Report[1], Length(Report));
finally
Free;
end;
finally
DocB.Free;
DocA.Free;
end;
end;
הדוח מבחין בין שלושה מצבים שבוליאני לא יכול. identical אומר אם משהו נבדל, comparisonComplete אומר אם המעבר הסתיים, ו-comparisonBudget נוקב את המגבלה שעצרה אותו אם אחת עצרה. השוואה שמיצתה תקציב מדווחת comparisonComplete=false ו-identical=false יחד, מכיוון שלמעבר קטוע אין בסיס לטעון שוויון. כל אוטומציה שקוראת רק identical תתייחס בסופו של דבר לעצירת תקציב כהבדל אמיתי, אז קראו את כל השלושה
אילו מגבלות שומרות על המעבר חסום?
ברירות המחדל ב-THPDFStructuralCompareLimits.Default מותאמות למסמכים אמיתיים ולא לעוינים, וכל תקציב רלוונטי סמנטית נושא תקרה משלו: 250,000 אובייקטים, 2,000,000 קשתות, עומק 128, 10,000 הבדלים מדווחים, 64 מגה-בייט לכל זרם ו-512 מגה-בייט של בייטי זרם בסך הכל, מגה-בייט אחד לערך ו-4,096 בייטים לנתיב. הגדילו אותם במכוון כשמכירים את הקורפוס, והורידו אותם כשמשווים קבצים שהגיעו מבחוץ:
var
Limits: THPDFStructuralCompareLimits;
Options: THPDFRenderedCompareOptions;
begin
Limits := THPDFStructuralCompareLimits.Default;
Limits.MaxDifferences := 200; // כישלון מהיר ב-CI
Limits.MaxTotalStreamBytes := 128 * 1024 * 1024;
Options := THPDFRenderedCompareOptions.Default;
Options.DPI := 150; // ברירת המחדל היא 72
Options.ColorTolerance := 2; // התעלמות מרעש עיגול של 1-2 רמות
Options.MinimumSimilarity := 0.9995;
Options.MaxChangedPixelRatio := 0.0005;
Options.GenerateHeatmaps := True; // כתיבת תמונות שכבת-על לבדיקה
Report := THPDFDocComparison.CompareWithOptions(DocA, DocB, cmFull,
Limits, Options);
end;
המעבר המוצג מעריך את מספר הפיקסלים ממידות העמוד ומה-DPI המבוקש לפני שכל מפת סיביות מוקצית, ובודק מחדש את מפת הסיביות בפועל לאחר מכן, כך שגיאומטריית עמוד פגומה לא יכולה לעקוף את התקציב על ידי שקר לגבי גודלה. הגדלת ה-DPI מגדילה נאמנות ועלות באופן ריבועי: 150 DPI הוא פי ארבעה מהפיקסלים של 72, ותקרות הפיקסלים לעמוד ובסך הכל קיימות בדיוק כי עבודת אצווה ב-300 DPI תקצה אחרת את דרכה לצרות
עד כמה דומה מספיק דומה?
שני עמודים נחשבים דומים רק כששני התנאים מתקיימים: יחס הפיקסלים שהשתנו נמוך או שווה ל-MaxChangedPixelRatio והדמיון גבוה או שווה ל-MinimumSimilarity. שני ספים במקום אחד, מכיוון שקומץ פיקסלים שגויים באופן קטסטרופלי ושטיפה רחבה של שינויי צבע זעירים הם כשלים שונים, וכל אחד לבדו יכול להיות מקובל בזרימת עבודה אחת ופוסל בזרימת עבודה אחרת. בדיקות סף משתמשות בערכים לא מעוגלים; שש הספרות העשרוניות ב-JSON קיימות כדי לשמור על הדוחות יציבים וניתנים להשוואה, לא כדי להגדיר את ההשוואה
פיקסלים שהשתנו מקובצים לאזורים באמצעות אריחים בגודל קבוע כצמתים עם צמידות בארבעה כיוונים, במקום הצפת מילוי לכל פיקסל. זה שומר על הזיכרון חסום ועל רשימת האזורים יציבה בין ריצות. קטיעת פרטי האזור השמורים משפיעה רק על הרשימה, לא על ספירת האזורים המדווחת, כך שעמוד עם יותר אזורים שהשתנו מ-MaxChangedRegions עדיין מדווח כמה היו
התנהגות אחת ראויה לציון ברור כי היא הופכת את האינסטינקט הרגיל. כשלי מציג, כשלי הקצאה וכשלי שכבת-על לעולם אינם נבלעים. כל דבר מסוג זה נרשם כ-renderError או renderBudget וכופה renderComparisonComplete=false, מכיוון שעמוד שנכשל בהצגה הוא עמוד שאף אחד לא השווה, ודיווח עליו כזהה גרוע יותר מדיווח על כלום
איפה כל מצב שייך בצינור עבודה
השוואה מבנית עונה מה השתנה והיא ברירת המחדל הנכונה לחבילות רגרסיה: היא נוקבת את הנתיב, אינדקס העמוד ומספרי האובייקטים המעורבים, כך שכשל מצביע על הקוד שיצר אותו. השוואה מוצגת עונה אם מישהו יבחין, וזאת השאלה עבור אישורים ועבור אימות שמעבר אופטימיזציה באמת היה חסר אובדן
הן משתלבות היטב. הריצו cmStructural על כל בנייה ותנו לה להיכשל בקול רם על שינויים ברמת אובייקט שלא צפו; הריצו cmFull עם מפות חום לפני שחרור, כשאדם זמין להסתכל על שכבות-העל. עבור צינורות עבודה שכבר פולטים תגי עמוד מסיבות אחרות, פלט הטקסט המתואר בייצוא עמודי PDF ל-SVG נותן מבט שלישי, הניתן להשוואה על ידי בני אדם, והבדיקות האוטומטיות באוטומציית דוחות טרום-דפוס מכסות שאלות תאימות שאף אחד ממצבי ההשוואה לא נועד לענות עליהן
השוואה, טרום-דפוס והצגה חולקים את אותו מודל אובייקטים של מסמך טעון, כך שמעבר יחיד על קובץ יכול להזין את שלושתם. רשימת התכונות המלאה לדלפי ו-C++Builder נמצאת בעמוד רכיב HotPDF Delphi PDF