מאמר טכני

דוחות Preflight של PDF באצווה ב-Delphi עם ממשק שורת פקודה של רכיב PDFium

כלי preflight באצווה הוא תוכנית קונסולה ללא חלון, המכוונת לתיקיית קובצי PDF, מאמתת כל אחד מהם מול תקני התאימות שאתה מציין, ומשאירה אחריה הוכחה קריאה למכונה של מה שהיא מצאה. אף אחד לא יושב וצופה בה. היא רצה בשתיים לפנות בוקר תחת cron או Windows Task Scheduler, או כשער בצינור תהליך CI, והאדם הבא שאכפת לו מהפלט שלה הוא מתזמן שקורא קוד יציאה (exit code) או מבקר (auditor) שפותח דוח שבועות לאחר מכן. הדבר הזה משנה את ההגדרה של "נכון". מנוע ה-preflight של PDFium Component, ספריית קוד מקור ל-PDF עבור Delphi,‏ C++Builder ו-Lazarus, הופך את קריאות האימות עצמן לטריוויאליות כמעט. העבודה שמחליטה אם הכלי מצדיק את קיומו יושבת סביב הקריאות הללו: איזה פרופיל בדקת, מה אמר קוד היציאה למתזמן, והאם הדוח שהיה תופס טעות עדיין קיים כאשר מישהו הולך לחפש אותו

החוזה: מה מתזמן יכול לראות באמת

כלי ריצת CI או Windows Task Scheduler רואה בדיוק שני דברים מהכלי שלך: קוד היציאה וכל קובץ שהוא השאיר אחריו. שורות יומן (log), צבעי קונסולה, פלט התקדמות: כל אלה מיועדים לאדם שצופה בזמן אמת, ובשתיים לפנות בוקר אף אחד לא עושה זאת. לכן, קבע את אוצר המילים של קודי היציאה לפני שאתה נוגע ב-API, ושמור עליו משעמם:

  • 0: כל קובץ תאם לכל פרופיל שהתבקש
  • 1: קובץ אחד לפחות יצר ממצאי אימות
  • 2: הכלי עצמו נכשל בקובץ אחד לפחות (קלט פגום, נעילה, קריסה)

ההבחנה בין קודים 1 ו-2 היא זו שצוותים מדלגים עליה ומתחרטים לאחר מכן. PDF פגום שלא ייפתח אינו כשל אימות. אם תשלב אותו לתוך קוד 1, משאית של סריקות פגומות תופיע בלוחות הבקרה שלך כקריסה פתאומית בתאימות, מה שישלח מישהו לרדוף אחרי נסיגה בתקנים שמעולם לא קרתה, כאשר הסיפור האמיתי הוא סורק שבור במעלה הזרם

עוד שני פריטים שייכים לחוזה. הראשון הוא פסק זמן לכל קובץ. PDF פתולוגי, אלפי עמודים עם מבני אובייקטים מקוננים עמוק, יכול להחזיק מהלך אימות יחיד במשך דקות, ולחלון לילי אין סבלנות לכך. הרוג את המשימה של אותו קובץ במועד האחרון (deadline), ספור זאת ככשל של הכלי, והמשך את האצווה. השני הוא ספריית הסגר: הזז כל קלט שפג תוקפו (timed out) או שלא ניתן לפתוח אותו הצידה במקום להשאיר אותו במקום. במשך מספר חודשים, הספרייה הזו אוספת בשקט את המסמכים הגרועים ביותר שהלקוחות האמיתיים שלך שולחים, וגוף התוכן (corpus) הזה שווה יותר לבדיקות שחרור מכל דוגמה סינתטית שתוכל לכתוב ביד

בחירת תקנים, ומדוע רמת התאימות משנה

הספירה TPdfPreflightStandard מכסה את המשפחות שעולות בפועל: ppsPdfA לתאימות ארכיונית של ISO 19005,‏ ppsPdfUa לנגישות של ISO 14289,‏ ppsPdfX להחלפת הדפסות, ובנוסף ppsPdfE,‏ ppsPdfR, ו-ppsPdfVT להנדסה, רסטר ועבודות נתונים משתנים. בתוך משפחה, המנוע קורא את רמת התאימות שהמסמך מצהיר עליה ומדווח עליה לכל תקן ב-ConformanceName של התוצאה. ציון שמה של המשפחה הוא לעתים רחוקות מספיק, מכיוון שהרמה היא המקום שבו ההבדל האמיתי חי. PDF/A-2b מבטיח שחזור חזותי ותו לא. PDF/A-3a מוסיף דרישה לתיוג מבנה לוגי ומאפשר קובצי מקור מוטמעים, שזהו רף הרבה יותר קשה לעבור עבור חומר סרוק שאין לו עץ תיוג (tag tree) כלל. טעה בזה בכל כיוון והאצווה תשקר לך. אם מדיניות השמירה שלך באמת רוצה PDF/A-2b אבל אתה מכשיל קבצים בגלל היעדר תגי מבנה, הדוח יתמלא בממצאים שאף אחד לעולם לא יתקן. קבל כל תווית PDF/A מבלי לבדוק את הרמה, ואתה חותם על מסמכים שעומדים ברף חלש יותר ממה שהבטחת. דרישות נגישות מקונים ממשלתיים מערמות יותר ויותר PDF/UA על כל זה, מה שלא מוסיף עלות לריצה מכיוון ש-BuildPdfPreflightReport (מהיחידה FPdfPreflightReport) מקבל אוסף (set) של תקנים:

Report := BuildPdfPreflightReport(Pdf, [ppsPdfA, ppsPdfUa]);

קריאה אחת מעריכה את שני התקנים ומחזירה רשומת דוח מאוחדת אחת

מדוע רשימת ממצאים ריקה אינה אישור

הדוח מונה ממצאים לכל תקן, ורשימת בעיות ריקה משמעותה רק "לא נמצאו בעיות בתקנים שרצו בפועל." זוהי טענה צרה יותר מ"הקובץ תואם לתקן שאכפת לך ממנו," והפער בין השניים הוא המקום שבו preflight באצווה נרקב בשקט. שגיאת הקלדה בתצורה שמשמיטה את ppsPdfA מהאוסף מייצרת בדיוק אותה רשימת בעיות ריקה כמו קובץ נקי באמת. אז התייחס לשתיקה כחשודה. עבור על Report.Results ואמת שני דברים עבור כל תקן שהתכוונת לבדוק: שרשומת תוצאה עבורו קיימת בכלל, ושהדגל IsCompliant שלה, המגובה על ידי Status = pfsPass, הוא אמיתי (true). עבודת לילה שמשווה "אין ממצאים" ל"מוכן לארכיון" מבלי לאשר אי פעם אילו תקנים הוערכו היא הדרך הקלאסית שבה תיקייה של קבצים לא תואמים עוברת במשך חודשים, עד שמבקר חיצוני פותח אחד עם veraPDF וכל הארכיון עומד בסימן שאלה

מלכודת שנייה מסתתרת במה שממצא הוא בכלל. כל TPdfPreflightIssue נושא Code,‏ Category,‏ Description, ו-Recommendation, והוא מפרט את הכלל שהופר, לא עמוד או אובייקט. זוהי בחירת עיצוב עם השלכות על לולאת המשוב. הדוח אומר לצוות המייצר איזה סוג של פגם קיים, גופן לא מוטמע או מזהה XMP חסר, ומציאת האובייקט הפוגע הספציפי היא תפקידו של כלי התיקון בהמשך הזרם (downstream), לא של המאמת. בנה את צרכני הדוחות שלך כנגד הערכים היציבים של Code, לעולם לא כנגד טקסט התיאור הקריא לאדם, שניתן לנסחו מחדש בין מהדורות ללא אזהרה

קובצי דוחות למכונות ולאדם בכוננות

רשומת הדוח כותבת את אותם ממצאים בחמישה פורמטים: SaveJsonToFile,‏ SaveCsvToFile,‏ SaveHtmlToFile,‏ SaveTextToFile, ו-SaveMarkdownToFile, כל אחד עם פונקציה תואמת בסגנון ToJson כשאתה רוצה את המחרוזת בזיכרון במקום בדיסק. התנגד לדחף לבחור אחד. כתוב JSON עבור הצינור, כך שה-CI יוכל לצרף אותו לרשומת המשימה ולנתח קודי בעיות וסטטוסים לפי תקן מבלי לגרד טקסט. כתוב HTML עבור האדם שמקבל זימונית (paged), כי זה נפתח בכל דפדפן ללא צורך בכלים כלל. שניהם יחד עולים שורה אחת נוספת לכל קובץ וחוסכים למהנדס הכונן שלך את המשימה הגרועה ביותר בעיבוד אצווה, שהיא הנדסה לאחור של גוש JSON גולמי בשתיים לפנות בוקר כדי לגלות איזה קובץ נשבר. משמעת אחת חשובה יותר מבחירת הפורמט: גזור כל שם דוח משם הקובץ של הקלט, לעולם לא מחותמת זמן, אחרת שתי ריצות מקבילות ישזרו דוחות שלא תוכל עוד להתאים חזרה לקלטים שלהם

ספי חומרה (Severity thresholds) שייכים לתצורה ולא לקוד. הערה (annotation) ללא תיאור חלופי היא כשל קשיח עבור פורטל הגשת PDF/UA והערה שניתן להתעלם ממנה עבור ארכיון פנימי, ובכל זאת זהו אותו ממצא זהה בשניהם. חשוף רמת fail-on לכל פרופיל כדי שהמדיניות תוכל להשתנות ללא צורך בהידור מחדש (recompile), והטבע את הרמה שהייתה בתוקף לתוך סיכום המשימה עצמו. ברבעון הבא אף אחד לא יזכור תחת איזה סף רצה האצווה של אוקטובר האחרון, והסיכום הוא המקום היחיד שבו זיכרון זה שורד

בידוד קבצים כדי ש-PDF גרוע אחד לא יוכל להטביע את האצווה

procedure RunPreflightBatch(const InputDir, ReportDir: string;
  out FilesWithFindings, ToolFailures: Integer);
var
  SR: TSearchRec;
  Pdf: TPdf;
  Report: TPdfPreflightReport;
begin
  FilesWithFindings := 0;
  ToolFailures := 0;
  if FindFirst(InputDir + '*.pdf', faAnyFile, SR) = 0 then
  try
    repeat
      Pdf := TPdf.Create(nil);   // מופע חדש לכל קובץ: ללא זליגת מצב
      try
        try
          Pdf.FileName := InputDir + SR.Name;
          Pdf.Active := True;
          if not Pdf.Active then  // כשלי טעינה הם שקטים, לא מועלים (raised)
            raise EPdfError.Create('Cannot open ' + SR.Name);
          Report := BuildPdfPreflightReport(Pdf, [ppsPdfA, ppsPdfUa]);
          Report.SaveJsonToFile(ReportDir + ChangeFileExt(SR.Name, '.json'));
          Report.SaveHtmlToFile(ReportDir + ChangeFileExt(SR.Name, '.html'));
          if Report.TotalIssueCount > 0 then
            Inc(FilesWithFindings);
        except
          on E: Exception do
          begin
            Inc(ToolFailures);   // טריטוריה של קוד יציאה 2, לא פסק דין של אימות
            WriteLn(ErrOutput, SR.Name + ': ' + E.Message);
          end;
        end;
      finally
        Pdf.Free;
      end;
    until FindNext(SR) <> 0;
  finally
    FindClose(SR);
  end;
end;

שלוש בחירות מכוונות חיות בלולאה הזו. TPdf חדש לכל קובץ מבטיח שמסמך אחד שמשחית את מצב המנוע לא יוכל להרעיל את הקבצים הבאים אחריו. בדיקת Active המפורשת מרוויחה את מקומה מכיוון ש-Active := True בולע שגיאות טעינה במקום להעלות אותן (raising); השמט את השומר וקובץ קטוע נסחף אל תוך קריאת האימות לפני שהוא נכשל איפשהו בהמשך עם הודעה מטעה. ה-try..except הפנימי חי בכוונה בתוך תחום הראות (scope) של קובץ בודד, כך שחריגה (exception) בודדת מקדמת את מונה הכישלונות והלולאה ממשיכה. אתה רוצה דוחות נקיים עבור 4,999 הקבצים הטובים גם כאשר קובץ ה-5,000 גרוס. ושני פורמטי הדוח נכתבים לדיסק לפני שמחשבים את פסק הדין, מה שאומר שהראיות שורדות גם אם באג מאוחר יותר בלוגיקת הסיכום טועה בספירה

המיפוי של קוד היציאה קורס אז למספר שורות בקובץ הפרויקט:

begin
  RunPreflightBatch(ParamStr(1), ParamStr(2), Findings, Failures);
  if Failures > 0 then
    Halt(2)
  else if Findings > 0 then
    Halt(1);
  // נפילה דרך יוצאת עם 0: כל קובץ היה תואם
end.

מה ש-preflight לא יעשה בשבילך

המנוע מזהה; הוא אינו מתקן. ממצא על גופן לא מוטמע או מרחב צבעים תלוי התקן הוא הוראת עבודה עבור מי שמייצר את הקבצים, ולמאמת אין שום דרך לתקן אותו במקום. לכן תכנן את לולאת המשוב בכוונה תחילה. דוחות חייבים לנחות היכן שהצוות המייצר קורא אותם בפועל, אחרת אותם ממצאים מופיעים מחדש כל לילה עד שמישהו שואל לבסוף מדוע קצב התאימות לעולם אינו משתפר. כדאי גם להצליב (cross-check) מדגם של פסקי דין מול מאמת עצמאי, veraPDF ל-PDF/A או ה-preflight של Acrobat ל-PDF/X, לפני שמבקר חיצוני יצליב אותם עבורך. כששני מנועים חלוקים על קובץ לקוח אמיתי, המסמך הזה אינו מטרד; זה בדיוק מקרה הנסיגה שהיה חסר בבדיקות השחרור שלך. שמור אותו, תן לו שם והרץ אותו בכל בניה (build)

כדאי להכיר עוד זיווג אחד. אותו מנוע אימות מניע את הבדיקות האינטראקטיביות בממשק משתמש של סקירה (review UI), כך שממשק שורת פקודה חסר חלון זה וסביבת עבודה לסקירת קליטת PDF הפונה לאנליסט, יכולים לחלוק אוצר מילים של אימות אחד במקום להתרחק זה מזה לאורך זמן. ומכיוון ש-[ppsPdfA, ppsPdfUa] מעריך נגישות באותו מעבר, הצד של PDF/UA של האצווה מתיישר בצורה נקיה עם עבודת צד-הצופה (viewer) כמו בניית קורא PDF נגיש ב-Delphi. פרופילים, פורמטי דוחות, וה-API המלא של ה-preflight מתועדים בדף המוצר של PDFium Component