ב-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 שלכם רק דרך קריאה שלא בולעת אותן
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.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 קיצר את הרשימה
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