מאמר טכני

בניית סביבת עבודה לסקירה וקליטה של PDF ב-Delphi עם רכיב PDFium

סביבת עבודה לסקירה וקליטה של PDF היא תוכנית קטנה עם משימה אחת: לבדוק כל קובץ לפני שכל שלב שלאחריו רשאי לגעת בו. כדי לבצע משימה זו, עליה לאסוף מספר יכולות לתוך מעבר אחד. היא פותחת את הקובץ (מבלי לסמוך עליו), קוראת את מה שהקובץ טוען על עצמו, מחפשת תוכן שיטעה מחלץ נאיבי או יכיל מתקפה, מחליטה אם קיים טקסט ניתן לחילוץ כלל, ואז מנתבת את המסמך לתור בהתאם למה שנמצא. דילוג על הבדיקה מוביל לכשלים שקטים: קובץ PDF מוצפן עם סיסמת בעלים שמכיל טופס XFA עובר דרך מחלץ טקסט כמחרוזות ריקות, מתאונדקס כמסמך ריק, ואיש אינו שם לב עד שמישהו מורידה מחפש תוכן שלא נקרא מעולם. רכיב PDFium הוא ספריית צפייה ובדיקה של קוד מקור VCL/LCL עבור Delphi, C++Builder ו-Lazarus, והוא חושף את קריאות הפנים שסביבת עבודה זו זקוקה להן. הסעיפים שלהלן מפרטים איזו קריאה עונה על איזו שאלה, ושני המקומות בהם הקריאה הברורה מספקת תשובה שגויה בביטחון

חמש שאלות לענות לפני ניתוב קובץ

הסר את הרשת ורצועת התמונות הממוזערות, וסינון הקבצה מצטמצם לחמש שאלות:

  • האם ניתן לפתוח את הקובץ בכלל, ותחת איזו סיסמה?
  • מה הוא טוען להיות: כותרת, מחבר, תאריך יצירה?
  • האם הוא מכיל תוכן פעיל או מסוכן כגון JavaScript, טופס XFA, או קבצים מוטמעים?
  • האם קיים טקסט ניתן לחילוץ, או שמדובר בסריקה הפונה ל-OCR?
  • לאור כל זאת, לאיזה תור הוא שייך: עיבוד ישיר, סקירה ידנית, או הסגר?

כל שאלה ממופה לאחת או שתיים מקריאות רכיב PDFium. לשני המיפויים הללו יש פינות חדות שאחראיות לרוב הקבצים שנותבו בצורה שגויה שנאלצתי לנפות בסביבת ייצור. מטאדאטה של מסמך נמצאת בשני מקומות שונים שעלולים לסתור זה את זה, והצפנה אינה בהכרח מונעת פתיחת מסמך

פתיחה זולה: מילוי טופס מנוטרל, אפס עמודים מרונדרים

סינון קבצה צריך להיות הפתיחה הזולה ביותר האפשרית. הגדרת FormFill := False לפני Active := True מורה לרכיב לדלג על סביבת מילוי הטופס לחלוטין. זה מקצר את זמן הטעינה, ו(חשוב לא פחות עבור קבצים ממקור לא ידוע) מונע מכל JavaScript ברמת המסמך לאתחל. אף אחת מתכונות הבדיקה שבהן נשתמש להלן אינה דורשת רינדור של עמוד, כך שמעבר סינון לא צריך לייצר ולו מפת סיביות אחת

procedure InspectIncoming(const IncomingPath: string; var Rec: TIntakeRecord);
var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := IncomingPath;
    Pdf.FormFill := False;     // no form environment, no JavaScript init
    Pdf.Active := True;        // failure is silent: Active simply stays False

    if not Pdf.Active then
    begin
      Rec.OpenFailed := True;  // damaged file or user-password lock
      Exit;                    // the finally block still runs
    end;

    Rec.PageCount := Pdf.PageCount;
    CollectIdentity(Pdf, IncomingPath, Rec);
    CollectRiskSignals(Pdf, Rec);
  finally
    Pdf.Active := False;
    Pdf.Free;                  // never leak the instance on a malformed file
  end;
end;

הבדיקה לאחר ההקצאה אינה אופציונלית, והיא בדיקה ולא מטפל בחריגים מסיבה. כאשר המנוע אינו יכול לטעון את הקובץ, הרכיב בולע את EPdfError הפנימי ומשאיר את Active על False במקום להפיץ אותו. קוד שממתין לחריג יקרא בשמחה את PageCount ממסמך שמעולם לא נפתח. אם זרימת הדחייה זקוקה לטקסט השגיאה בפועל של המנוע, קרא את הקובץ למערך בייטים וקרא לאוברלוד LoadDocument שמקבל TBytes; נתיב זה אכן מעלה EPdfError עם ההודעה, כולל מקרה הסיסמה. ה-try..finally עדיין ממלא תפקידו. שירותי קבלה פועלים ללא השגחה במשך שבועות, ואף חריג מאוחר יותר לא יאפשר דליפת מופע TPdf או החזקת נעילה שמעבר הניסיון יתקל בה

תפוקה לרוב אינה צוואר הבקבוק. כאשר מילוי הטופס מנוטרל ואין רינדור, פתיחת סינון נשלטת על ידי קלט/פלט, ועובד בודד בודק בנוחות מספר קבצים בשנייה מדיסק מקומי. אם נפח הקבלה אי פעם יגדל מעל עובד אחד, חלק את העבודה לפי קובץ ולא לפי בדיקה. חמש השאלות חולקות פתיחה אחת, ופיצולן על פני תהליכים יכפיל את השלב היקר ביותר במקום לפזר את עלותו

מטאדאטה נמצאת בשני מקומות, והם סותרים זה את זה

ISO 32000-1 מגדיר שני בתים למטאדאטה של מסמך: מילון המידע של המסמך (סעיף 14.3.3) וחבילת XMP המצורפת לקטלוג (סעיף 14.3.2). מאפייני Title, Author, Subject ו-CreationDate קוראים ממילון Info, עם MetaText[] לכל מפתח אחר ו-DecodeDate לניתוח מחרוזת התאריך D:YYYYMMDD.... הבעיה היא שיצרנים מודרניים כותבים יותר ויותר XMP בלבד, כיוון שISO 32000-2 מסמיך מגמה זו על ידי הוצאת רוב מפתחות מילון Info משימוש ב-PDF 2.0. הסימפטום בכלי קבלה הוא קונקרטי. סביבת העבודה שלך מציגה כותרת ריקה בעוד Adobe Acrobat מציג אחת, משום ש-Acrobat נסוג ל-dc:title בתוך חבילת XMP, שמאפייני מילון Info לא נוגעים בה לעולם

procedure CollectIdentity(Pdf: TPdf; const FilePath: string;
  var Rec: TIntakeRecord);
begin
  Rec.Title := Pdf.Title;             // Info dictionary value
  Rec.Author := Pdf.Author;
  Rec.CreatedAt := Pdf.CreationDate;  // raw PDF date string ("D:2026...")

  // An empty Info title does not mean the document is untitled. The
  // component does not expose the XMP packet, so probe the raw file
  // bytes for the dc:title element before trusting the blank.
  if (Rec.Title = '') and FileContainsText(FilePath, 'dc:title') then
    Include(Rec.Flags, ifTitleInXmpOnly);
end;

אפילו בדיקת המחרוזת הגסה שלמעלה ממלאת את תפקידה: "מטאדאטה קיימת, אך לא במקום שכלים מורשים מחפשים" היא עובדה רלוונטית לניתוב עבור כל צינור ארכיון שמאנדקס לפי כותרת או מחבר. אם האינדקס הדאונסטרים שלך קורא רק ממילון Info, קבצים המסומנים בדרך זו יהפכו בשקט לכאלה שאי אפשר לחפש בהם

קבצים מוצפנים שנפתחים בכל זאת

מסמך מוצפן אינו בהכרח נכשל בפתיחה. מטפל האבטחה הסטנדרטי (ISO 32000-1 סעיף 7.6.3) מבחין בין סיסמת משתמש, הנדרשת לפתיחת המסמך, לבין סיסמת בעלים שרק שומרת על הרשאות כגון הדפסה והעתקה. חלק גדול ממסמכים עסקיים "מוגנים" מוצפן עם סיסמת בעלים וסיסמת משתמש ריקה. הם נפתחים ללא הנחיה, מפוענחים לחלוטין, ומסתמכים על כך שצופים יבחרו לכבד את דגלי ההרשאות. זוהי מדיניות, לא הגנה, ומצבי הקבלה שלך צריכים לשקף את ההבדל

זיהוי הצפנה לאחר פתיחה מוצלחת דורש קריאת מנוע אחת ומסלול חלופי. FPDF_GetSecurityHandlerRevision(Pdf.Document) מחזיר -1 לקבצים לא מוגנים ואת גרסת המטפל אחרת, ו-Pdf.Permissions שמחזיר כל דבר מלבד המסכה $FFFFFFFF עם כל הביטים מוגדרים הוא האות המאשש. לקבצים הנעולים באמת בסיסמת משתמש, הקצה Password לפני הגדרת Active := True; אם הפתיחה עדיין נכשלת, נתב את הקובץ למצב חסום שמבקש אישורים מהשולח דרך ערוץ מאובטח במקום לנסות שוב בעיוורון. והתנגד לפיתוי לטפל ב"מוצפן" כהסגר אוטומטי. ברוב תעשיות עתירות מסמכים, קבצים מוצפנים-אך-פתוחים הם המקרה הרגיל, לא החשוד

תוכן פעיל: JavaScript, XFA וקבצים מוטמעים

שלושה ממצאים צריכים תמיד להגיע להחלטת הניתוב. ראשית, JavaScript: האירוע OnUnsupportedFeature מדווח על תכונות מבניות כגון XFA או תוכן תלת-ממדי כשהמנוע נתקל בהן, אך הוא אינו מזהה JavaScript. בדוק במקום זאת את JavaScriptActionCount ותתייחס לתוצאה שאינה אפס כאל תוכן פעיל. שנית, XFA: כאשר FormType מחזיר ftXfaFull, העמודים הגלויים הם לרוב לא יותר מרינדור של תבנית XFA, וחילוץ טקסט קונבנציונלי יראה טקסט סטנדרטי ולא את הערכים שמולאו. שלישית, קבצים מצורפים: PDF הוא פורמט מכל, ו-AttachmentCount מספר לך אם זה מכיל נוסעים

procedure CollectRiskSignals(Pdf: TPdf; var Rec: TIntakeRecord);
var
  i, PageNo: Integer;
  Ext: string;
begin
  Rec.IsEncrypted := Assigned(FPDF_GetSecurityHandlerRevision) and
    (FPDF_GetSecurityHandlerRevision(Pdf.Document) <> -1);
  Rec.HasForms := Pdf.FormType <> ftNone;
  Rec.IsXfa := Pdf.FormType = ftXfaFull;
  Rec.HasJavaScript := Pdf.JavaScriptActionCount > 0;

  // AnnotationCount is a per-page property; walk the pages to total
  // it. Loading a page object renders nothing, so this stays cheap.
  Rec.Annotations := 0;
  for PageNo := 1 to Pdf.PageCount do
  begin
    Pdf.PageNumber := PageNo;
    Inc(Rec.Annotations, Pdf.AnnotationCount);
  end;

  Rec.Attachments := Pdf.AttachmentCount;

  for i := 0 to Rec.Attachments - 1 do
  begin
    Ext := LowerCase(ExtractFileExt(string(Pdf.AttachmentName[i])));
    if (Ext = '.exe') or (Ext = '.js') or (Ext = '.vbs') or (Ext = '.dll') then
      Include(Rec.Flags, ifDangerousAttachment);
  end;
end;

שני פרטים בלולאה זו ראויים לתשומת לב. שם הקובץ המצורף מגיע מתוך המסמך, לכן לעולם אל תעשה בו שימוש חוזר כנתיב פלט ללא ניקוי; שם מוטמע כגון ..\..\start.exe הוא מעבר נתיב שממתין לקריאת שמירה רשלנית. ורשימה חסומה של סיומות היא מלכודת, לא ערובה. תפקידה לאלץ החלטה אנושית, לא להעיד שהקובץ נקי

הפיכת אותות למצבי ניתוב

מודל מצב ברוי זקוק לפחות מצבים ממה שרוב הצוותים מצפים: מוכן (אין חסמים, טקסט קיים), סקירה (הפתיחה הצליחה אך משהו זקוק לעיניים, כגון טופס XFA, JavaScript, שכבת טקסט ריקה, או כותרת רק ב-XMP), חסום (נדרשת סיסמת משתמש), ופגום (הפתיחה נכשלה). תעד את הראיות לצד המצב. גיבוב הקובץ, ספירת העמודים, הדגלים המדויקים, והודעת שגיאת המנוע לקבצים פגומים כולם חשובים, משום שהאדם שיטיל ספק בהחלטת ניתוב יעשה זאת שבועות מאוחר יותר, מול קובץ שאולי הוחלף או שונה מאז

כאשר מפעיל זקוק לבדוק קובץ מוסגר, אל תמסור אותו לצופה ברירת המחדל של המעטפת. רנדר אותו בתוך חלונית מוקשחת עם ניטרול סקריפטינג וטיפול בקישורים, הגישה המתוארת בבניית משטח תצוגה מקדימה מאובטח של PDF ב-Delphi. ואם הקבלה שלך מזינה ארכיון עם דרישות תאימות, מעבר הסינון הוא המקום הטבעי לתזמן בדיקה מעמיקה יותר; אימות preflight באצווה מול פרופילי PDF/A ו-PDF/UA ממשיך בדיוק מהמקום בו בדיקה זו מסתיימת

דף המוצר של הרכיב מכסה רישוי, ממשק ה-API המלא לבדיקה, ודמואים הכלולים, כולל בודק מסמכים בסגנון קבלה: PDFium Component