מאמר טכני

כשלי טעינה שקטים של PDF בדלפי: השתמשו בדוח טעינה של PDFium

ב-PDFium Component לדלפי ול-Lazarus, הצבה של TPdf.Active := True לעולם לא מעלה חריגה כשטעינת PDF נכשלת: TPdf.SetActive תופס כל חריגה ומשאיר את הרכיב לא פעיל. כדי לראות את השגיאה האמיתית, קוראים במקום זאת ל-TPdf.LoadDocument(Options, Report). ה-overload הזה מעלה מחדש את החריגה המקורית וממלא TPdfLoadReport עם סטטוס הטעינה, קוד השגיאה הנייטיבי של PDFium והאם טבלת ה-cross-reference הייתה צריכה להיבנות מחדש

הבעיה בדרך כלל עולה בקוד אצווה. משימת חילוץ טבלאות עוברת על תיקייה של 13 PDFs אמיתיים עם TPdf משותף אחד, ו-7 מהם חוזרים ככשלים. אף אחד משבעת הקבצים האלה אינו שבור באמת. בלוקי ה-except סביב הטעינה לעולם לא נדלקים, הלוג מאשים שמות קבצים לא נכונים, והשגיאה הראשונה שנראית היא EPdfError חשוף על רכיב לא פעיל, שמועלה מקריאת מאפיין כמה שורות אחרי הטעינה שנכשלה באמת. שתי התנהגויות נפרדות נערמות כדי להפיק את התמונה הזאת, ושתיהן עובדות כמתוכנן

למה TPdf.Active := True לא מעלה חריגה כשטעינת PDF נכשלת?

‏TPdf.SetActive עוטף את LoadDocument בתוך try..except שבולע כל מחלקת חריגה ופשוט משאיר את הרכיב לא פעיל. הבליעה מכוונת: אותו setter רץ גם כשמעצב טפסים מציב את Active ב-IDE, ונתיב שגוי לא רשאי לרסק את ה-IDE. בזמן ריצה TPdf.Active רק מדווח אם קיים handle מסמך נייטיבי, כך שאחרי טעינה שנכשלה הוא קורא False ושום דבר אחר לא קורה. מה שהועלה נעלם, בין אם זו הייתה EPdfError מהמפרסר, שגיאת stream או EAccessViolation מ-pdfium.dll שנקשר חלקית. הודעות ה-DLL המפורטות שמתוארות ב-אבחון כשלי טעינה של pdfium.dll בדלפי מגיעות אל ה-handler שלכם רק דרך קריאה שלא בולעת אותן

שני נתיבי טעינה ב-PDFium Component: הצבת Active ל-true בולעת כל חריגה ב-setter ודוחה את הכישלון אל הקריאה המוגנת הראשונה, שבה CheckActive מעלה EPdfError על רכיב לא פעיל, בזמן ש-LoadDocument עם TPdfLoadOptions ו-TPdfLoadReport מבקר את ה-header, ה-startxref, ה-xref וסימן סוף הקובץ, ואז מעלה מחדש את החריגה המקורית עם הגורם האמיתי מחובר
הבליעה מכוונת כי מעצב ה-IDE חולק את ה-setter; קוד אצווה זקוק ל-overload שמעלה, מדווח ומספר את הסיפור האמיתי של הקובץ
Pdf.FileName := FileName;
try
  Pdf.Active := True;       // SetActive בולע כל חריגת טעינה
except
  on E: Exception do
    Log.Add(FileName + ': ' + E.Message);   // לעולם לא רץ
end;
// הכישלון עולה כאן במקום, כ-EPdfError גנרי:
// 'Cannot perform this operation on an inactive Pdf1 component'
Log.Add(Format('%s: %d pages', [FileName, Pdf.PageCount]));

// תיקון מזערי לקוד קיים: בודקים את Active מיד אחרי ההצבה;
// החל מ-v3.122.1 LastLoadReport שומר את הטקסט של השגיאה שנבלעה
Pdf.Active := True;
if not Pdf.Active then
  Log.Add(FileName + ': load failed: ' + Pdf.LastLoadReport.ErrorMessage);

הכישלון סוף סוף מופיע בקריאה המוגנת הראשונה. TPdf.PageCount, כמו רוב מאפייני המסמך, מתחיל ב-CheckActive, שמעלה EPdfError שמציין את הרכיב אבל לא את הקובץ ולא את הגורם. בדיקת Pdf.Active מיד אחרי ההצבה הופכת קריסה מיוחסת שגוי לרשומת "נכשל" כנה. לפני PDFiumPas v3.122.1 הסיבה הייתה אובדת בנקודה הזאת; החל מ-v3.122.1 הצבה שנכשלה מחליפה את LastLoadReport בדוח plsFailed שנושא את טקסט השגיאה, כך שהגורם שורד. אובייקט החריגה עצמו והביקורת ברמת הבייט עדיין דורשים נקודת כניסה אחרת

למה שימוש חוזר ב-TPdf אחד נכשל מהקובץ השני ואילך?

את TPdf.FileName אפשר להציב רק כשהרכיב לא פעיל, כך שמופע משותף דוחה את הקובץ השני לפני שהוא מנסה בכלל לטעון אותו. TPdf.SetFileName מתחיל ב-CheckInactive, ואותה הגנה מגינה על Password ועל FormFill. אחרי הטעינה המוצלחת הראשונה המופע נשאר פעיל, ההצבה הבאה מעלה חריגה, ואם לולאת האצווה תופסת את החריגה וממשיכה הלאה, השגיאה נוחתת תחת שם הקובץ החדש בזמן שהמסמך הישן עדיין פתוח. בשילוב עם כשלי הטעינה הנבלעים, הלוג מפסיק להתאים למציאות. בשחזור של 13 הקבצים מופע משותף דיווח 7 כשלים, בזמן ש-TPdf.Create(nil) טרי לכל מסמך פתח את כל 13. הצבה של Active := False בין קבצים גם עובדת, אבל מופע אחד לכל מסמך מבודד כל קובץ כבר מתוך הבנייה

ציר הזמן של TPdf משותף של PDFium שנכשל מהקובץ השני ואילך: אחרי הטעינה הראשונה המופע נשאר פעיל, הצבת FileName הבאה מעלה חריגה ב-CheckInactive לפני כל ניסיון טעינה, ולולאת האצווה רושמת את השגיאה תחת שם הקובץ החדש בזמן שהמסמך הישן עדיין פתוח — המלכודת מאחורי 7 כשלים שגויים באצווה של 13 קבצים
SetFileName מגן עם CheckInactive, ולכן מופע משותף דוחה את הקובץ השני לפני שמנסה אותו; מבודדים כל מסמך עם TPdf משלו והלוג חוזר להתאים למציאות

מה TPdf.LoadDocument עם TPdfLoadReport נותן לכם?

‏TPdf.LoadDocument(const Options: TPdfLoadOptions; out Report: TPdfLoadReport) מעלה את החריגה האמיתית וגם מספר לכם מה קרה בצורה מובנית. ה-overload של הקובץ טוען את FileName; overloads אחים מקבלים TBytes או מצביע וגודל, ו-LoadCustomDocument(AStream, AOwnsStream, Options, Report) מכסה streams. כל אחד מהם מאמת את האפשרויות, בודק שהמופע אינו פעיל, מריץ ביקורת ברמת בייט של ה-header, של startxref, של מקטעי ה-xref ושל סימן ה-%%EOF, ואז מבצע את הטעינה הנייטיבית. הביקורת חסומה במגבלות מאותו סוג שנדונו ב-תקציבי משאבים של מפרסר עבור PDFs לא אמינים: TPdfLoadOptions.Default מציב את AuditByteLimit על 256 MiB, את MaxIssues על 256, את MaxXrefSections על 1024 ואת MaxXrefEntries על 4,000,000. בכישלון השיטה מציבה Report.Status := plsFailed ומעלה מחדש; מכיוון ש-Report נכתב במקום, תוכנו שורד את החריגה, ועותק נשמר ב-TPdf.LastLoadReport

שדות הדוח עונים על השאלות שלוג אצווה באמת צריך. Status הוא אחד מ-plsNotAttempted, plsLoaded, plsLoadedWithRecovery, plsRejected או plsFailed. NativeErrorCode מחזיק את FPDF_GetLastError, כך ש-FPDF_ERR_PASSWORD (4) מפריד סיסמה חסרה או שגויה מקובץ פגום שמדווח כ-FPDF_ERR_FORMAT (3). UsedRecovery, CrossReferenceTableValid ו-RecoveryRoute אומרים אם PDFium נאלצה לבנות מחדש את טבלת ה-xref, ו-Issues היא רשימת כל ממצא ביקורת עם Code, Severity, Offset, ObjectNumber ו-MessageText, כש-IssuesTruncated מוצב כש-MaxIssues קיצר את הרשימה

צינור ה-LoadDocument של PDFium Component וה-TPdfLoadReport שלו: אימות האפשרויות ובדיקת אי-הפעילות מעלות חריגה לפני שקיים דוח כלשהו, ביקורת בייטים עוברת על ה-header, ה-startxref, מקטעי ה-xref וסימן סוף הקובץ, הטעינה הנייטיבית רושמת את FPDF_GetLastError, והתוצאות מתפצלות לטעינה, לטעינה עם התאוששות אחרי בניית xref מחדש, דחייה מחמירה או כישלון
Status, NativeErrorCode ורשימת הממצאים עונים על מה שלוג אצווה צריך; רק overload עם אפשרויות מוסיף את ביקורת הבייטים, ומאז v3.122.1 גם Active שנכשל עם True מרשים plsFailed ב-LastLoadReport
uses
  SysUtils, Classes, TypInfo, FPdfView, PDFium;

procedure ProcessBatch(Files, Log: TStrings);
var
  I: Integer;
  Pdf: TPdf;
  Options: TPdfLoadOptions;
  Report: TPdfLoadReport;
begin
  Options := TPdfLoadOptions.Default(plmCompatible);
  for I := 0 to Files.Count - 1 do
  begin
    Pdf := TPdf.Create(nil);          // מופע אחד לכל מסמך
    try
      Pdf.FileName := Files[I];
      try
        Pdf.LoadDocument(Options, Report);
      except
        on E: Exception do
        begin
          // ה-Report מתמלא גם כש-LoadDocument מעלה חריגה
          if Report.NativeErrorCode = FPDF_ERR_PASSWORD then
            Log.Add(Files[I] + ': password required')
          else
            Log.Add(Format('%s: %s (%s)', [Files[I],
              GetEnumName(TypeInfo(TPdfLoadStatus), Ord(Report.Status)),
              E.Message]));
          Continue;
        end;
      end;
      if Report.UsedRecovery then
        Log.Add(Files[I] + ': opened after PDFium rebuilt the xref table');
      ExtractTables(Pdf, Log);
    finally
      Pdf.Free;
    end;
  end;
end;

מתי טוענים עם plmStrict?

משתמשים ב-plmStrict בכל מקום שבו קובץ שתוקן בשקט גרוע מקובץ נדחה, כמו קליטת ארכיון, טיפול בראיות או צינור חתימה. PDFium בונה בשקט מחדש טבלת cross-reference שבורה (ISO 32000-1 §7.5.4) על ידי סריקת הקובץ אחר אובייקטים, מה שמצוין ל-viewer ובעייתי לכל מה שחייב לעבד בדיוק את הבייטים שקיבל. אחרי הטעינה הנייטיבית הרכיב שואל את FPDF_DocumentHasValidCrossReferenceTable. במצב plmCompatible בנייה מחדש מניבה plsLoadedWithRecovery בתוספת אזהרת plicNativeCrossReferenceRebuild. במצב plmStrict הרכיב פורק את המסמך, מציב plsRejected, מוסיף plicStrictModeRejected ומעלה EPdfError עם "Strict PDF load rejected the document". מצב מחמיר דוחה גם כל שגיאת ביקורת, ו-TPdfLoadOptions.Default(plmStrict) מפעיל את RequireFinalEndOfFileMarker, שמקדם %%EOF חסר או נתונים אחרי האחרון שבהם (§7.5.5) מאזהרה לשגיאה. ביקורת ה-xref משלימה את הבדיקות ברמת האובייקט ב-אימות streams של object ו-xref עם PDFium VCL

function AcceptForArchive(const FileName: string; out Reason: string): Boolean;
var
  Pdf: TPdf;
  Report: TPdfLoadReport;
  I: Integer;
begin
  Result := False;
  Reason := '';
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := FileName;
    try
      Pdf.LoadDocument(TPdfLoadOptions.Default(plmStrict), Report);
      Result := True;               // xref תקין, אין שגיאות ביקורת
    except
      on E: EPdfError do
      begin
        Reason := E.Message;
        for I := 0 to High(Report.Issues) do
          if Report.Issues[I].Severity = plisError then
            Reason := Reason + sLineBreak + Format('  at offset %d: %s',
              [Report.Issues[I].Offset, Report.Issues[I].MessageText]);
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

איפה TPdf.LastLoadReport מפסיק לדבר אמת?

‏TPdf.LastLoadReport שלם רק אחרי overload של LoadDocument שמקבל אפשרויות, כי רק ה-overloads האלה מריצים את ביקורת הבייטים. הצבה מוצלחת של Active := True כותבת דוח מצב תואם בלי ביקורת בייטים, כך ש-AuditAttempted נשאר False. לפני PDFiumPas v3.122.1 הצבה שנכשלה לא כתבה דבר, מה שאמר שעל מופע משותף LastLoadReport עדיין תיאר את הקובץ הקודם, לעיתים קרובות עם plsLoaded מרגיע. החל מ-v3.122.1 כל טעינה שנכשלה מחליפה את הדוח: הצבה שנכשלה של Active := True, שעדיין משאירה את הרכיב לא פעיל בלי להעלות חריגה, וקריאה שנכשלה של LoadDocument רגיל או של LoadCustomDocument רושמות plsFailed עם טקסט השגיאה, שוב בלי ביקורת. שני חורים נוספים חשובים בפועל. אימות האפשרויות ו-CheckInactive רצים לפני שהדוח מאותחל, כך ש-AuditByteLimit שלילי או מופע שכבר פעיל מעלים חריגה בלי להפיק דוח. ו-NativeErrorCode משמעותי רק כש-PDFium באמת ניסתה לפרסר; עבור קובץ חסר המעטפת מעלה חריגה לפני ש-PDFium רצה, ולכן רושמים בלוג את ErrorMessage ואת טקסט החריגה במקום

הכלל המעשי קצר. משאירים Active := True ל-viewers הקשורים למעצב, שבהם רכיב לא פעיל הוא תוצאה מקובלת. בכל מקום אחר, ומעל הכול בקוד אצווה ושרתים, יוצרים TPdf אחד לכל מסמך, קוראים ל-LoadDocument(Options, Report), תופסים את החריגה שהוא מעלה ורושמים בלוג את Report.Status, NativeErrorCode וה-Issues ברמת שגיאה יחד עם שם הקובץ. העלות היא כמה שורות לכל אתר קריאה, וכל כישלון מיוחס לקובץ הנכון עם הגורם האמיתי שלו

ה-API של דוח הטעינה, המצב המחמיר והביקורת ברמת הבייט מגיעים יחד עם PDFium Component עבור Delphi, C++Builder ו-Lazarus, לצד ציור, חילוץ טקסט, מילוי טפסים ואימות PDF/A