מיזוג ופיצול הם שתי פעולות העמודים שכולם פונים אליהן תחילה, והן מכסות שטח רב. הן אינן מכסות הכל. ישנה משפחה נפרדת של עבודות המארגנת מחדש עמודים במקום להעביר קבצים שלמים: הנחת ארבע שקופיות על גיליון אחד לצורך חלוקה, גרירת עמוד מגב המסמך לחזיתו, או משיכת עמודים 3, 7 ו-12 לקטע קצר מבלי לגעת בשאר. PDFium חושף שלוש מתודות בדיוק עבור זה, וכל אחת מתנהגת אחרת מהמיזוג והפיצול שאתה כבר מכיר. מאמר זה עובר על מה שהן עושות, היכן נמצאות נקודות הפלט, ופרט בעלות אחד שגרם לקריסה בשטח.
השלוש הן ImportNPagesToOne עבור imposition של N-up, MovePages עבור סידור מחדש במקום, ו-ImportPagesByIndex עבור חילוץ תת-קבוצה. מיזוג עורם מסמכים מקצה לקצה ומשאיר את ספירת העמודים שווה לסכום הקלטים. פיצול כותב מספר קובצי פלט מקלט אחד. שלוש הפעולות כאן יושבות באמצע: אחת מהן משנה כמה עמודי מקור חולקים גיליון, אחת מהן משנה את הסדר בתוך מסמך בודד, ואחת מהן מעתיקה קומץ עמודים נבחר לתוך מסמך אחר. ידיעה איזו היא איזו תחסוך ממך כפיית ריקוד של מיזוג ומחיקה שבו קריאה בודדת הייתה מספיקה.
מה ש-N-up imposition באמת עושה
Imposition הוא המונח בעולם הדפוס לסידור מספר עמודי מקור על גיליון גדול יותר כך שהתוצאה המודפסת והמקופלת תיקרא בסדר הנכון. הגרסה היומיומית היא דף חלוקה של 2 עמודים, חוברת של 4 עמודים, או גיליון אינדקס המציג תריסר תמונות ממוזערות בעמוד. PDFium מטפל בגיאומטריה באמצעות קריאה אחת:
function ImportNPagesToOne(
OutputWidth, OutputHeight: Single;
NumX, NumY : Cardinal): TPdf;
NumX ו-NumY מתארים את הגריד. ערך של 2, 1 מציב שני עמודי מקור זה לצד זה; 2, 2 אורז ארבעה לתוך פריסת רבעים; 4, 3 בונה גיליון אינדקס של 12 עמודים. PDFium קורא את עמודי המקור לפי הסדר, מקטין כל אחד מהם כך שיתאים לתא שלו, וממלא את הגריד משמאל לימין, מלמעלה למטה, תוך התחלת גיליון פלט חדש בכל פעם שהגריד הנוכחי מלא. עמודי המקור אינם משתנים. מה שאתה מקבל בחזרה הוא מסמך חדש שעמודיו הם שילובים של עמודי המקור.
גודל הפלט הוא בנקודות, לא בפיקסלים
OutputWidth ו-OutputHeight הם יחידות משתמש של PDF, ויחידת משתמש של PDF היא נקודה אחת (point), שהיא חלק אחד מתוך שבעים ושניים של אינץ'. היחידה מצהירה על הגודל הפיזי של גיליון הפלט, ואין לה שום קשר לפיקסלים במסך או ל-DPI של הרינדור. זהו המקום הנפוץ ביותר לטעות בו ב-imposition, מכיוון שמפתח הרגיל למפות סיביות (bitmaps) פונה לספירת פיקסלים ומסיים עם גיליון בגודל של בול דואר או של שלט חוצות.
המספרים ששווה לזכור הם שני גודלי העמודים שבהם תשתמש הכי הרבה. US Letter הוא 612 על 792 נקודות, מכיוון ש-8.5 אינץ' כפול 72 הוא 612 ו-11 אינץ' כפול 72 הוא 792. A4 הוא בערך 595 על 842 נקודות, מממדיו של 210 על 297 מילימטרים. כותרת הקישור עצמו מצהירה על הכלל בפשטות, שיחידה אחת היא חלק אחד מתוך שבעים ושניים של אינץ', והיחידה מספקת קבוע PointsPerInch השווה ל-72 אם אתה מעדיף לחשב גודל מאינצ'ים בקוד מאשר לכתוב את הערך המילולי.
const
LetterW = 612.0; // 8.5 in * 72
LetterH = 792.0; // 11 in * 72
var
Source, Composite: TPdf;
begin
Source := TPdf.Create(nil);
Composite := nil;
try
Source.FileName := 'slides.pdf';
Source.Active := True;
// Four source pages per Letter sheet, 2 by 2 grid.
Composite := Source.ImportNPagesToOne(LetterW, LetterH, 2, 2);
if Composite = nil then
raise Exception.Create('PDFium rejected the imposition arguments');
Composite.SaveAs('slides-4up.pdf');
finally
Composite.Free; // see the next section: this is mandatory
Source.Free;
end;
end;
המזהה המוחזר הוא שלך לשחרור
קרא את החתימה שוב. ImportNPagesToOne מחזיר TPdf, לא בוליאני. ערך חזרה זה הוא מזהה מסמך חדש לחלוטין, שהוקצה בנפרד מהמקור, והקורא הוא הבעלים שלו. מקור ה-TPdf שקראת לו למתודה אינו משתנה ועדיין מחזיק במזהה משלו; השילוב הוא אובייקט שני עצמאי. אם תאפשר ל-TPdf המוחזר לצאת מטווח הראייה (scope) מבלי לשחרר אותו, תדליף מסמך PDFium שלם.
הטעות המסוכנת יותר פועלת בכיוון ההפוך. מתחת לפני השטח, המתודה מבקשת מ-PDFium מזהה FPDF_DOCUMENT חדש באמצעות FPDF_ImportNPagesToOne, ואז עוטפת את המזהה הגולמי הזה בתוך ה-TPdf המוחזר כך שזמן החיים של העטיפה שולט בזה של המזהה. מאותה נקודה ואילך ישנו בדיוק בעלים אחד של המזהה, ובדיוק מקום אחד שבו הוא אמור להיסגר: כאשר אתה משחרר (Free) את האובייקט המוחזר. נתיב שגיאה רשלני שגם משחרר את העטיפה וגם קורא ל-FPDF_CloseDocument על המזהה הגולמי שהוא לכד סוגר את אותו מסמך PDFium פעמיים. זהו שחרור כפול, וזהו הבאג הספציפי שפגע בקורא כאן בעבר. הכלל שמונע זאת הוא פשוט. סגור את המסמך בנתיב אחד בלבד, על ידי שחרור ה-TPdf שהמתודה מסרה לך, ולעולם אל תנסה לעקוף את העטיפה כדי לסגור את המזהה שהיא כבר אימצה.
שתי מסקנות נובעות מכך. ראשית, המתודה מחזירה nil כאשר PDFium דוחה את הארגומנטים, כגון אפס באחד מצירי הגריד או כשל בהקצאה, כך שבדיקת nil נדרשת לפני שאתה נוגע בתוצאה. שנית, אתחל את משתנה הפלט שלך ל-nil לפני ה-try ושחרר אותו ב-finally, כפי שהדוגמה למעלה עושה, כך שכשל באמצע הדרך לא ישאיר אותך משחרר הפניה לא מוגדרת או מדלג על השחרור לחלוטין.
סידור מחדש של עמודים מבלי לשכתב אותם
Imposition בונה מסמך חדש. סידור מחדש משנה מסמך אחד במקום. MovePages מרים קבוצת עמודים ממיקומם הנוכחי ומניח אותם ביעד, תוך הזזת כל השאר סביב הבלוק שהועבר כך שספירת העמודים נשארת זהה:
function MovePages(
const PageIndices: array of Integer;
DestPageIndex : Integer): Boolean;
האינדקסים מבוססים על אפס. PageIndices מונה את העמודים להעברה, בסדר שבו הם אמורים לסיים, ו-DestPageIndex הוא האינדקס שעליו נוחת העמוד המועבר הראשון לאחר שההעברה מתייצבת. מכיוון ש-PDFium מעביר את העמודים במקום להעתיק ולדחוס מחדש את התוכן שלהם, הפעולה היא זולה וללא אובדן נתונים: אובייקטי העמוד שומרים על הזרמים שלהם, המשאבים שלהם והדיוק שלהם. זוהי הקריאה שמאחורי פאנל עמודים של גרירה לסידור מחדש, שבו משתמש מושך תמונה ממוזערת למשבצת חדשה ואתה שומר את הסדר החדש בהעברה אחת. היא מחזירה False כאשר אינדקס חורג מהטווח, לכן אמת את התוצאה במקום להניח שהסידור מחדש הצליח.
var
Doc: TPdf;
begin
Doc := TPdf.Create(nil);
try
Doc.FileName := 'report.pdf';
Doc.Active := True;
// Move the last page (index 4 in a 5-page file) to the very front.
if not Doc.MovePages([4], 0) then
raise Exception.Create('MovePages rejected the index');
Doc.SaveAs('report-reordered.pdf');
finally
Doc.Free;
end;
end;
משיכת תת-קבוצה לפי אינדקס
הפעולה השלישית מעתיקה קבוצה מפורשת של עמודים ממסמך אחד לאחר. ImportPagesByIndex מקבל את מסמך המקור ומערך אינדקסים המבוסס על אפס, ומכניס עמודים אלה לתוך היעד במיקום שנבחר:
function ImportPagesByIndex(
Source : TPdf;
const PageIndices: array of Integer;
InsertAt : Integer= 0): Boolean;
אתה קורא לה על מסמך היעד ומעביר את המקור כארגומנט הראשון. PageIndices מציין את עמודי המקור למשיכה, בסדר שאתה רוצה אותם; InsertAt הוא המשבצת המבוססת על אפס ביעד שאליה הולך העמוד המיובא הראשון, כך ש-0 מציב אותם לפני העמוד הראשון הקיים וספירת העמודים הנוכחית של היעד מתווספת בסוף. מערך ריק מייבא כל עמוד, מה שהופך את הקריאה להעתקה מלאה כשאתה צריך כזו. היא מחזירה False אם אינדקס כלשהו חורג מהטווח במקור.
כאן ההבדל מול פיצול בא לידי ביטוי. פיצול כותב קבצים נפרדים, כאשר פעולה אחת מפיקה פלטים רבים על הדיסק. ImportPagesByIndex מבצע עבודה בצורה ההפוכה: הוא אוסף קבוצה נבחרת של עמודים לתוך מסמך יעד בודד בזיכרון, שאותו אתה שומר פעם אחת. כאשר המשימה היא "תן לי את עמודים 3, 7 ו-12 כקובץ PDF קצר אחד", זוהי הדרך הישירה, והיא עוטפת את FPDF_ImportPagesByIndex מתחת לפני השטח.
var
Source, Excerpt: TPdf;
begin
Source := TPdf.Create(nil);
Excerpt := TPdf.Create(nil);
try
Source.FileName := 'manual.pdf';
Source.Active := True;
Excerpt.CreateDocument; // start an empty target
// Pull pages 3, 7 and 12 (zero-based 2, 6, 11) into the excerpt.
if not Excerpt.ImportPagesByIndex(Source, [2, 6, 11], 0) then
raise Exception.Create('A requested page index is out of range');
Excerpt.SaveAs('manual-excerpt.pdf');
finally
Excerpt.Free;
Source.Free;
end;
end;
חיבור הכל יחד בצורה נקייה
הצורה מקצה לקצה זהה בכל השלוש: פתח את המקור על ידי הגדרת FileName והעברת Active ל-True, בצע את הפעולה, שמור באמצעות SaveAs, ושחרר את מה שבבעלותך. הענף היחיד שדורש זהירות הוא אילו קריאות מקצות מסמך חדש. MovePages משנה את המסמך שאתה כבר מחזיק, ולכן יש אובייקט אחד לשחרר. ImportPagesByIndex כותב לתוך יעד שיצרת בעצמך, ולכן אתה משחרר את המקור ואת היעד שפתחת. ImportNPagesToOne הוא החריג, מכיוון שהמסמך החדש הוא ערך החזרה של המתודה ולא משהו שבנית, ושכחה מכך שמדובר במזהה נפרד בבעלות הקורא היא הדרך שבה גם ההדלפה והשחרור הכפול מתרחשים. אתחל את התוצאה ל-nil, בדוק אותה לאחר הקריאה, ושחרר אותה בנתיב יחיד.
אם העבודה שיש לך בפועל היא שילוב קבצים שלמים במקום סידור מחדש של עמודים, ראה מיזוג מספר קובצי PDF למסמך אחד. אם זה ההפך, פירוק מסמך אחד למספר קבצים, ראה פיצול מסמכי PDF למספר קבצים. שיטות ה-imposition והסידור מחדש המתוארות כאן נשלחות כחלק מ-PDFium Component עבור Delphi ו-C++Builder לצד ממשקי ה-API לטעינה, רינדור ועריכה המכוסים במקומות אחרים בבלוג זה.