מאמר טכני

פיצול מסמכי PDF עם PDFium Component ב-Delphi

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

כיצד פועלת לולאת הפיצול

התבנית זהה ללא קשר לאופן שבו אתה מחלק את מסמך המקור. צור מופע (instance) טרי של TPdf, קרא ל-CreateDocument עליו כדי לאתחל PDF ריק בזיכרון, יבא את הדפים שאתה רוצה עם ImportPages, שמור את התוצאה, ואז אפס את Active ל-False לפני האיטרציה הבאה. הצעד האחרון הזה הוא מה שאנשים מפספסים: CreateDocument תמיד מתחיל מסמך חדש, אבל אם Active עדיין True כשהוא רץ שוב, הוא משליך באופן משתמע את המסמך שעדיין בזיכרון. לכן, איפוס תחילה שומר את המצב (state) נקי ומוגדר היטב. מופע ה-TPdf החיצוני נמצא בשימוש חוזר על פני כל האיטרציות, מה ששומר על לחץ הקצאת זיכרון נמוך במשימות גדולות

כך נראה פיצול דף-אחר-דף כשהוא מופשט לעקרונותיו הבסיסיים:

procedure SplitIntoPages(Source: TPdf; const OutputDir: string);
var
  I: Integer;
  PdfOut: TPdf;
  OutFile: string;
begin
  PdfOut := TPdf.Create(nil);
  try
    for I := 1 to Source.PageCount do
    begin
      PdfOut.CreateDocument;

      // Range הוא מחרוזת מספר דף מבוססת-1; נקודת הכנסה 1 = המיקום הראשון
      PdfOut.ImportPages(Source, IntToStr(I), 1);

      OutFile := OutputDir + '\page_' + Format('%.4d', [I]) + '.pdf';
      PdfOut.SaveAs(OutFile);

      PdfOut.Active := False;   // איפוס לפני ה-CreateDocument הבא
    end;
  finally
    PdfOut.Free;
  end;
end;

הפרמטר Range ל-ImportPages הוא אותו פורמט מחרוזת ש-PDFium משתמש בו פנימית: רשימה מופרדת בפסיקים של מספרי דפים או טווחים מופרדים במקפים, כולם מבוססי-1. '3' מייבא את דף 3. '1-5' מייבא את הדפים 1 עד 5 לפי הסדר. '2,5,8' מייבא את שלושת הדפים הללו. הפרמטר השלישי הוא מיקום ההכנסה (מבוסס-1) במסמך היעד; העברת 1 ממקמת תמיד דפים מיובאים בתחילת קובץ שאחרת היה ריק, וזה מה שאתה רוצה כאן

פיצול לפי טווחי דפים

כאשר הקורא מספק רשימה כמו 1-12,13-24,25-36, אתה מנתח אותה לזוגות של התחלה/סוף ומריץ את אותה לולאה, בונה את מחרוזת הטווח מכל זוג:

procedure SplitByRanges(Source: TPdf; const RangeList: array of string;
  const OutputDir: string);
var
  I: Integer;
  PdfOut: TPdf;
  OutFile: string;
begin
  PdfOut := TPdf.Create(nil);
  try
    for I := 0 to High(RangeList) do
    begin
      PdfOut.CreateDocument;
      PdfOut.ImportPages(Source, RangeList[I], 1);
      OutFile := Format('%s\section_%d.pdf', [OutputDir, I + 1]);
      PdfOut.SaveAs(OutFile);
      PdfOut.Active := False;
    end;
  finally
    PdfOut.Free;
  end;
end;

האימות (validation) לפני שאתה מגיע ל-ImportPages חשוב כאן. ImportPages מחזיר False כאשר מספר דף במחרוזת הטווח עולה על Source.PageCount, אך הוא לא מעלה חריגה והוא לא מייצר קובץ פלט חלקי שניתן לזהות לפי שמו בלבד. בדוק את ערך ההחזרה של SaveAs ותעד (log) כשלונות בנפרד; טווח שמייצר קובץ פלט ריק לא נראה שגוי בעליל עד שמישהו פותח אותו

פיצול בגבולות סימניה

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

procedure SplitByBookmarks(Source: TPdf; const OutputDir: string);
var
  Bm: TBookmarks;
  I, StartPage, EndPage: Integer;
  PdfOut: TPdf;
  RangeStr, OutFile, SafeTitle: string;
begin
  Bm := Source.Bookmarks;
  if Length(Bm) = 0 then
    Exit;

  PdfOut := TPdf.Create(nil);
  try
    for I := 0 to High(Bm) do
    begin
      StartPage := Bm[I].PageNumber;
      if I < High(Bm) then
        EndPage := Bm[I + 1].PageNumber - 1
      else
        EndPage := Source.PageCount;

      if (StartPage < 1) or (EndPage < StartPage) then
        Continue;

      RangeStr := Format('%d-%d', [StartPage, EndPage]);

      PdfOut.CreateDocument;
      PdfOut.ImportPages(Source, RangeStr, 1);

      SafeTitle := StringReplace(Bm[I].Title, '/', '_', [rfReplaceAll]);
      SafeTitle := StringReplace(SafeTitle, ':', '_', [rfReplaceAll]);
      OutFile := Format('%s\%02d_%s.pdf', [OutputDir, I + 1, SafeTitle]);
      PdfOut.SaveAs(OutFile);

      PdfOut.Active := False;
    end;
  finally
    PdfOut.Free;
  end;
end;

מסמך שאין לו סימניות אינו מצב שגיאה ששווה להציג למשתמש ככזה; זה פשוט אומר שלמצב פיצול זה אין עם מה לעבוד. שומר ה-Length(Bm) = 0 מטפל בזה בשקט. מה שכן שווה להציג הוא כאשר מספר דף של סימניה נמצא מחוץ לטווח של המסמך, מה שקורה בקבצים פגומים שבהם מתאר המסמך (outline) מעולם לא עודכן לאחר מחיקת דפים. בדיקת הגבולות ב-StartPage ו-EndPage מדלגת על ערכים אלה במקום להעביר טווח זבל ל-ImportPages

שמות קובצי פלט ואיפוס Active

בטיחות שמות קבצים עבור שמות הנגזרים מסימניות דורשת תשומת לב מפורשת. כותרות סימניות יכולות להכיל תווים שהם תקפים במחרוזת PDF אך לא בנתיב של מערכת קבצים. לכל הפחות, החלף לוכסן ימני, לוכסן שמאלי ונקודתיים לפני בניית נתיב הפלט. ב-Windows, גם *, ?, ", <, >, ו-| אסורים; לולאה פשוטה על פני סט קבוע מכסה אותם מבלי להזדקק לביטויים רגולריים (regex)

השורה Active := False בסוף כל איטרציה ראויה להדגשה מכיוון שהיא הדרישה היחידה שאינה מובנת מאליה בתבנית. CreateDocument אינו סוגר באופן משתמע את מה שפתוח. אם Active עדיין True כאשר CreateDocument רץ שוב, PDFium משליך את המסמך הנוכחי ומתחיל מסמך חדש ללא שגיאה, אך ההתנהגות מוגדרת-על-ידי-יישום (implementation-defined) במקרי קצה והכוונה ברורה יותר כאשר מאפסים במפורש. חשוב על זה כמו על צמד ה-try/finally: בלוק ה-finally משחרר את האובייקט החיצוני; ה-Active := False מאפס את מצב המסמך הפנימי בין איטרציות הלולאה

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

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

השיטות ImportPages ו-CreateDocument המוצגות כאן הן חלק מ-PDFium Component עבור Delphi ו-C++Builder