PDFium Component נותן לך מתודה אחת לפיצול PDF: ImportPages. כל השאר, בין שאתה מבודד עמוד יחיד, חותך בגבולות שרירותיים, או הולך לפי מבנה הסימניות של המסמך עצמו, הוא רק דרכים שונות להחליט אילו מספרי עמודים ייכנסו לכל קובץ פלט. המכניקה נשארת זהה. הבנת זה מוקדם חוסכת הרבה פניות שגויות
איך לולאת הפיצול עובדת
התבנית זהה ללא קשר לאופן שבו אתה מחלק את מסמך המקור. צור מופע TPdf חדש, קרא עליו ל-CreateDocument כדי לאתחל PDF ריק בזיכרון, ייבא את העמודים שאתה רוצה עם ImportPages, שמור את התוצאה, ואז אפס את Active ל-False לפני האיטרציה הבאה. השלב האחרון הזה הוא זה שאנשים מפספסים: CreateDocument אינה סוגרת במשתמע את המסמך שעדיין בזיכרון, ולכן אתה חייב לשמור את הפלט שלך ולאפס Active := False במפורש לפני קריאה נוספת לה; איפוס תחילה שומר על מצב נקי ומוגדר היטב. מופע ה-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 = המיקום הראשון
if not PdfOut.ImportPages(Source, IntToStr(I), 1) then
raise Exception.CreateFmt('Failed to import page %d', [I]);
OutFile := OutputDir + '\page_' + Format('%.4d', [I]) + '.pdf';
if not PdfOut.SaveAs(OutFile) then
raise Exception.Create('Failed to save ' + OutFile);
PdfOut.Active := False; // אפס לפני ה-CreateDocument הבא
end;
finally
PdfOut.Free;
end;
end;
הפרמטר Range של ImportPages הוא אותו פורמט מחרוזת ש-PDFium משתמש בו פנימית: רשימה מופרדת בפסיקים של מספרי עמודים או טווחים מופרדים במקף, כולם מבוססי אחד. '3' מייבא את עמוד 3. '1-5' מייבא את עמודים 1 עד 5 לפי הסדר. '2,5,8' מייבא את שלושת העמודים האלה. הפרמטר השלישי הוא מיקום ההכנסה מבוסס אחד במסמך היעד; העברת 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;
if not PdfOut.ImportPages(Source, RangeList[I], 1) then
raise Exception.Create('Invalid page range: ' + RangeList[I]);
OutFile := Format('%s\section_%d.pdf', [OutputDir, I + 1]);
if not PdfOut.SaveAs(OutFile) then
raise Exception.Create('Failed to save ' + OutFile);
PdfOut.Active := False;
end;
finally
PdfOut.Free;
end;
end;
אימות לפני שאתה מגיע ל-ImportPages חשוב כאן. ImportPages מחזירה False כשמספר עמוד במחרוזת הטווח חורג מ-Source.PageCount, אבל היא אינה מעלה חריגה והיא אינה מייצרת קובץ פלט חלקי שתוכל לזהות לפי שם בלבד. בדוק את ערך ההחזרה של SaveAs ותעד כשלים בנפרד; טווח שמייצר קובץ פלט ריק אינו שגוי בעליל עד שמישהו פותח אותו
פיצול בגבולות סימניות
הגישה השלישית משתמשת במבנה של המסמך עצמו ולא ברשימה שסופקה מבחוץ. כל סימנייה ברמה העליונה נושאת מספר עמוד יעד; המקטע שהיא מגדירה רץ מהעמוד הזה עד עמוד אחד לפני העמוד של הסימנייה הבאה, או עד סוף המסמך עבור הרשומה האחרונה
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;
if not PdfOut.ImportPages(Source, RangeStr, 1) then
begin
PdfOut.Active := False;
Continue; // דלג על מקטע פגום במקום לכתוב קובץ ריק
end;
SafeTitle := StringReplace(Bm[I].Title, '/', '_', [rfReplaceAll]);
SafeTitle := StringReplace(SafeTitle, ':', '_', [rfReplaceAll]);
OutFile := Format('%s\%02d_%s.pdf', [OutputDir, I + 1, SafeTitle]);
if not PdfOut.SaveAs(OutFile) then
raise Exception.Create('Failed to save ' + OutFile);
PdfOut.Active := False;
end;
finally
PdfOut.Free;
end;
end;
מסמך שאין בו סימניות אינו מצב שגיאה שראוי להציג למשתמש ככזה; זה רק אומר שלמצב הפיצול הזה אין ממה לעבוד. השומר Length(Bm) = 0 מטפל בזה בשקט. מה שכן ראוי להצגה הוא כשמספר העמוד של סימנייה נמצא מחוץ לתחום המסמך, וזה קורה בקבצים פגומים שבהם תוכן העניינים מעולם לא עודכן אחרי מחיקת עמודים. בדיקת הגבולות על StartPage ו-EndPage מדלגת על הרשומות האלה במקום להעביר טווח זבל אל ImportPages
שמות קובצי הפלט ואיפוס Active
בטיחות שמות קבצים עבור שמות שנגזרו מסימניות דורשת תשומת לב מפורשת. כותרות סימניות יכולות להכיל תווים שתקפים במחרוזת PDF אך לא בנתיב של מערכת קבצים. לכל הפחות, החלף קו נטוי, קו נטוי הפוך ונקודתיים לפני בניית נתיב הפלט. ב-Windows גם *, ?, ", <, > ו-| אסורים; לולאה פשוטה על קבוצה קבועה מכסה אותם בלי לגרור ביטויים רגולריים
השורה Active := False בסוף כל איטרציה ראויה להדגשה משום שהיא הדרישה היחידה בתבנית שאינה מובנת מאליה. CreateDocument אינה סוגרת במשתמע את מה שפתוח. אם Active עדיין True כש-CreateDocument רצה שוב, המסמך שעדיין בזיכרון מעולם לא נסגר או נשמר כראוי, ואינך יכול לסמוך על התנהגות מוגדרת היטב במצב הזה, ולכן שמור ואפס במפורש לפני שאתה מתחיל את המסמך הבא. חשוב על זה כעל בן הזוג של try/finally: בלוק ה-finally משחרר את האובייקט החיצוני; ה-Active := False מאפס את מצב המסמך הפנימי בין איטרציות הלולאה
השימוש בזיכרון על פני עבודת פיצול גדולה נשאר שטוח בגישה הזו משום שלעולם אינך מחזיק יותר ממסמך פלט אחד בזיכרון בו זמנית. מסמך המקור נשאר פתוח ולקריאה בלבד לאורך כל הדרך; ImportPages מעתיקה נתוני עמוד אל המסמך החדש בלי לשנות את המקור. אם המקור מוצפן, פתח אותו עם הסיסמה שלו לפני הלולאה והעמודים המועתקים בכל קובץ פלט יהיו לא מוצפנים, וזו בדרך כלל ההתנהגות הנכונה עבור פלט מפוצל שמופץ לנמענים שונים
עוד דבר אחד בנוגע ל-SaveAs: היא מחזירה Boolean. תיקיית פלט שאינה קיימת, נתיב עם תווים שמערכת ההפעלה דוחה, או מצב של דיסק מלא, כולם יגרמו ל-SaveAs להחזיר False בלי להעלות חריגה. בעבודת אצווה שמפצלת מסמך בן 200 עמודים ל-200 קבצים בני עמוד אחד, קל לפספס כשל שקט בעמוד 147. בדוק את ערך ההחזרה בכל קריאה וספור הצלחות מול הסך הצפוי כשהלולאה מסתיימת
המתודות ImportPages ו-CreateDocument שמוצגות כאן הן חלק מ-PDFium Component עבור Delphi ו-C++Builder