מאמר טכני

מיזוג מספר קבצי PDF למסמך אחד עם PDFium Component

PDFium Component חושף מיזוג PDF דרך מתודה יחידה: ImportPages. הדפוס הוא תמיד אותו דפוס: צור מסמך יעד ריק, פתח כל קובץ מקור, קרא ל-ImportPages כדי להעתיק את העמודים מעבר, סגור את המקור, וחזור על כך. כאשר הלולאה מסתיימת, SaveAs כותב את התוצאה לדיסק. אין מצב מיזוג מיוחד, אין תצורה (configuration) להפעיל. המורכבות חיה במקרי הקצה (edge cases), וישנם כמה שנושכים ללא אזהרה

לולאת הליבה

שני מופעי (instances) TPdf הם כל מה שאתה צריך. האחד מחזיק את מסמך היעד, שנוצר ריק עם CreateDocument. השני פותח כל קובץ מקור בתורו. להלן פרוצדורה (procedure) שלוקחת רשימת נתיבי קבצים וכותבת את הפלט הממוזג לנתיב יחיד:

procedure MergeFiles(const FileList: TStrings; const OutputPath: string);
var
  PdfDest, PdfSrc: TPdf;
  InsertAt, I: Integer;
begin
  PdfDest := TPdf.Create(nil);
  PdfSrc  := TPdf.Create(nil);
  try
    PdfDest.CreateDocument;
    InsertAt := 1;  // ImportPages משתמש במיקום יעד מבוסס-1

    for I := 0 to FileList.Count - 1 do
    begin
      PdfSrc.FileName := FileList[I];
      PdfSrc.Active   := True;

      if not PdfSrc.Active then
        raise Exception.CreateFmt('Cannot open: %s', [FileList[I]]);

      PdfDest.ImportPages(
        PdfSrc,
        '1-' + IntToStr(PdfSrc.PageCount),  // טווח מסמך מלא
        InsertAt);

      Inc(InsertAt, PdfSrc.PageCount);
      PdfSrc.Active := False;
    end;

    PdfDest.SaveAs(OutputPath);
  finally
    PdfSrc.Free;
    PdfDest.Free;
  end;
end;

משני דברים בקוד זה קל להתעלם בקריאה ראשונה. הראשון הוא האופן שבו PDFium מדווח על כשלוני טעינה. Active := True לעולם אינו מעלה חריגה: אם הקובץ חסר, פגום, או מוגן בסיסמה, PDFium תופס את השגיאה פנימית ומשאיר את Active כ-False. ללא הבדיקה המפורשת בשורה 10, קובץ פגום היה נושר בשקט מהמיזוג ללא שום אינדיקציה בפלט. ה-PDF הסופי היה כולל פחות עמודים מהצפוי ולא היית יודע איזה קובץ היה האשם

השני הוא המונה InsertAt. הארגומנט השלישי ב-ImportPages הוא המיקום מבוסס-1 ביעד שבו נוחת העמוד המיובא הראשון. התחלה ב-1 שמה את מסמך המקור הראשון בתחילת קובץ שאחרת היה ריק. לאחר כל מקור, המונה מתקדם ב-PdfSrc.PageCount, כך שאצוות העמודים הבאה (batch) מתווספת לאחר האחרונה. תשכח לקדם אותו וכל מקור עוקב ידרוס עמודים במיקום 1, מה שייתן לך את המסמך האחרון ברשימה ושום דבר אחר

טווחי עמודים בררניים

אינך חייב לקחת כל עמוד ממקור. מחרוזת הטווח (range string) המועברת כארגומנט השני פועלת לפי פורמט פסיק-ומקף פשוט: "1-3" לוקח עמודים 1 עד 3, "2,4,6" בוחר שלושה עמודים ספציפיים, ו-"1-" משמעותו מעמוד 1 עד סוף המסמך. ניתן לשלב טווחים במחרוזת אחת, כך ש-"1-3,5,7-" מדלג על עמודים 4 ו-6. דקות (subtlety) אחת חשובה כאן: המספרים תמיד מתייחסים לעמודים במסמך המקור, החל מ-1, ללא קשר להיכן העמודים הללו מסיימים ביעד. אם אתה רוצה את עמודים 40 עד 50 מתוך קטלוג של 200 עמודים, מחרוזת הטווח היא "40-50", ולא מיקום יחסי למה שכבר נמצא ביעד

// חלץ שער פלוס תקציר מנהלים בן שלושה עמודים מדוח ארוך
PdfSrc.FileName := 'annual-report.pdf';
PdfSrc.Active   := True;
if PdfSrc.Active then
begin
  // עמוד 1 הוא השער; עמודים 3-5 הם התקציר
  PdfDest.ImportPages(PdfSrc, '1,3-5', InsertAt);
  Inc(InsertAt, 4);  // 1 שער + 3 עמודי תקציר = 4 עמודים נוספו
  PdfSrc.Active := False;
end;

בעת חישוב התוספת ל-InsertAt, ספור את העמודים שייבאת בפועל, לא את ספירת העמודים של המקור. אם תעביר '1,3-5' ייבאת 4 עמודים, אז התקדם ב-4. התקדמות ב-PdfSrc.PageCount תשאיר פער של עמדות יעד ריקות ותמקם את מסמך המקור הבא הלאה לתוך הקובץ מכפי שהתכוונת

מה ImportPages משמר ומה לא

עמודים המועתקים על ידי ImportPages נושאים את התוכן הנראה שלהם בשלמותו. טקסט, גרפיקה וקטורית, תמונות רסטר (raster), גופנים מוטמעים, ו-XObjects של טופס כולם עוברים כחלק מזרמי תוכן העמוד. גם ביאורים (annotations) ברמת העמוד, כולל הערות (comments), הדגשות, ומשיחות דיו, עוברים, משום שהם מאוחסנים בתוך מילון העמוד ולא ברמת המסמך

מטא-נתונים ברמת המסמך (Document-level metadata) הם סיפור אחר. מחרוזות הכותרת, המחבר, הנושא ומילות המפתח במילון ה-Info של המקור נשארות מאחור. מסמך היעד מתחיל עם מטא-נתונים ריקים לאחר CreateDocument, כך שאם הפלט הממוזג דורש שדות אלו מאוכלסים, עליך להקצות אותם ל-PdfDest ישירות לפני הקריאה ל-SaveAs. המאפיינים Title, Author, Subject, Keywords ו-Creator ב-TPdf לוקחים מחרוזות רגילות וכותבים לתוך מילון ה-Info בעת השמירה

שדות טופס אינטראקטיביים הם מורכבים יותר. הגדרות שדה AcroForm יושבות במילון ברמת המסמך ולא בתוך זרמי עמודים בודדים. כאשר ImportPages מעתיק עמוד המכיל שדות טופס, המראה החזותי של שדות אלו עובר משום שהוא מרונדר לתוך זרם תוכן העמוד, אבל יישומוני (widgets) השדה שהופכים אותם לאינטראקטיביים הם חלק ממבנה ה-AcroForm ואינם עוקבים (do not follow). במיזוג טיפוסי, שדה טקסט ממסמך מקור יציג את הערך שהיה לו בזמן הייבוא, אך הוא לא יהיה ניתן לעריכה בקובץ הממוזג. אם אתה זקוק לשדות שיישארו ניתנים למילוי (fillable), שטֵח (flatten) אותם בכל מסמך מקור לפני הייבוא: זה אופה (bakes) את הערכים הנוכחיים לתוך זרם התוכן ומסיר את שכבת העל (overlay) האינטראקטיבית, מה שנותן לך תוצאה חזותית נקייה ללא יישומונים שבורים בפלט

קובצי מקור מוצפנים

מסמכי מקור המוגנים בסיסמה נפתחים באותה דרך כמו מסמכים לא מוצפנים, עם מאפיין אחד נוסף שיש להגדיר קודם. הקצה את הסיסמה ל-PdfSrc.Password לפני העברת Active := True, ו-PDFium ישתמש בה במהלך הפתיחה:

PdfSrc.Password := 'user-password';
PdfSrc.FileName := 'protected.pdf';
PdfSrc.Active   := True;
if not PdfSrc.Active then
  raise Exception.Create('Wrong password or file cannot be opened');

PdfDest.ImportPages(PdfSrc, '1-' + IntToStr(PdfSrc.PageCount), InsertAt);
Inc(InsertAt, PdfSrc.PageCount);
PdfSrc.Active := False;

סיסמה שגויה גורמת לאותה תוצאה שקטה של Active = False כמו קובץ חסר, כך שהבדיקה המפורשת הכרחית כאן באותה מידה. ההצפנה לא עוברת ליעד: עמודים המיובאים ממקור מוגן נוחתים ביעד כתוכן לא מוגן. אם הפלט הממוזג זקוק גם הוא להצפנה, קבע את התצורה שלה ב-PdfDest לפני הקריאה ל-SaveAs

שמירת התוצאה

SaveAs ב-TPdf מקבל נתיב קובץ או TStream. עבור רוב המיזוגים, עומס-היתר (overload) של קובץ הוא מה שאתה רוצה:

PdfDest.SaveAs('merged-output.pdf');

הארגומנט השני האופציונלי הוא TSaveOption השולט במצב השמירה. בררת-המחדל, saNone, כותבת עדכון הדרגתי (incremental) אם המסמך נטען מקובץ, או כתיבה-מחדש מלאה אם הוא נוצר טרי. מכיוון שיעד שנבנה עם CreateDocument הוא תמיד טרי, הפלט יהיה קובץ קומפקטי בעל רוויזיה יחידה. הארגומנט השלישי, TPdfVersion, מאפשר לך להצמיד (pin) את כותרת גרסת ה-PDF כאשר יש לך צרכנים במורד הזרם שדורשים גרסה ספציפית; השארתו כ-pvUnknown מאפשרת ל-PDFium לבחור על סמך התוכן

המתודות ImportPages ו-SaveAs המוצגות כאן הן חלק מ-PDFium Component עבור Delphi ו-C++Builder