הטבעת סימן מים או לוגו על כל עמוד של מסמך נראית כמו עבודה של חמש דקות עד שפותחים את התוצאה במפקח על גודל הקובץ. הגישה הברורה היא ללכת בעמודים ובכל אחד מהם לבנות שוב את אותם אובייקטי טקסט או תמונה. זה עובד ויזואלית, וזה בזבזני בצורה מצטברת. סימן מים אלכסוני "DRAFT" המצויר ישירות על דוח בן מאה עמודים הוא מאה עותקים של אותם נתוני נתיב וטקסט היושבים בזרמי התוכן, והקובץ השמור נושא כל אחד מהם
אובייקט Form XObject הוא המבנה ש-PDF מספק כדי למנוע בדיוק את זה. הוא עוטף פיסת תוכן לשימוש חוזר, עמוד שלם או תבנית קטנה, לאובייקט יחיד בעל שם שניתן לצבוע פעמים רבות במיקומים רבים. התוכן חי בקובץ פעם אחת. כל עמוד שרוצה את החותמת מחזיק בהוראה קצרה שאומרת "צבע את XObject N כאן, עם טרנספורמציה זו." סימן מים של מאה עמודים מוסיף אז אובייקט תוכן אחד לקובץ במקום מאה, וזה ההבדל בין מסמך שגדל ליניארית עם ספירת העמודים שלו לבין מסמך שלא. סימני מים, חותמות לוגו, תבניות מספור עמודים וחותמות רשמיות הם כולם אותה צורה של בעיה, וה-Form XObject הוא הכלי הנכון לכל אחד מהם
מדוע אובייקט שמור אחד מנצח מאה ציורים מחדש
החיסכון הוא מבני, לא קוסמטי. עמוד PDF מרונדר על ידי הרצת זרם התוכן שלו, רצף של אופרטורים של ציור. כאשר אתה מצייר מחדש חותמת לכל עמוד, אתה מצרף את רצף האופרטורים המלא עבור החותמת ההיא לזרם של כל עמוד, והבתים משוכפלים כמספר העמודים שיש לך. אובייקט Form XObject מעביר את האופרטורים הללו לזרם בודד השמור פעם אחת במסמך. ההפניה שעמוד בודד שומר היא קטנה: היא דוחפת מטריצת טרנספורמציה, קוראת ל-XObject, ומשחזרת את המצב. ספירת העמודים אינה מכפילה עוד את עלות היצירה האמנותית
זה חשוב ביותר כאשר החותמת כבדה. חותמת וקטורית עם מאות מקטעי נתיב, או מפת סיביות של לוגו, היא יקרה לאחסון. כאשר היא נשמרת פעם אחת ומאוזכרת, החלק הכבד משולם פעם אחת והתקורה לכל עמוד היא בתים ספורים של קריאה. התוצאה הויזואלית בעמוד זהה לציור מחדש ישיר, וזה העניין. הקורא אינו יכול להבחין בהבדל; גודל הקובץ בהחלט יכול
לכידת עמוד לתוך XObject
התוכנה PDFium בונה את האובייקט לשימוש חוזר מעמוד קיים. המקור הוא עמוד במסמך כלשהו שפתוח אצלך, PDF קטן בן עמוד אחד המכיל רק את יצירת האמנות של סימן המים שלך, או עמוד מסוים של קובץ גדול יותר. CreateXObjectFromPage לוכד את תוכן עמוד המקור ההוא לתוך מזהה (handle) לשימוש חוזר שנמצא בבעלות מסמך היעד, זה שאתה מחתים
var
Dest, Stamp: TPdf;
XObject: TPdfXObject;
begin
Dest := TPdf.Create;
Stamp := TPdf.Create;
try
Dest.LoadFromFile('Report.pdf');
Stamp.LoadFromFile('Watermark.pdf'); // one page of artwork
// Capture page 0 of the stamp document into a reusable handle that
// is owned by Dest. Source must be active; the index is zero-based.
XObject := Dest.CreateXObjectFromPage(Stamp, 0);
if XObject = nil then
raise Exception.Create('Could not build the stamp XObject');
// ... place it, then free it before closing Stamp (see below) ...
החתימה היא CreateXObjectFromPage(Source: TPdf; SourcePageIndex: Integer): TPdfXObject. המתודה מחזירה nil במקרה של כישלון במקום להעלות שגיאה, כך שהבדיקה המפורשת לעיל אינה אופציונלית. המזהה שחוזר הוא TPdfXObject שאתה בעליו, ושתי מגבלות אורך החיים המחוברות אליו הן החלק בכל התרגיל הזה שמכשיל אנשים, ולכן הן מקבלות סעיף משלהן למטה
מיקום החותמת בעמוד
אובייקט XObject שנלכד אינו עושה דבר בפני עצמו. כדי לגרום לו להופיע, אתה מכניס עותק שלו לעמוד הנוכחי של המסמך באמצעות InsertFormObjectFromXObject. קריאה זו מחזירה את אובייקט העמוד הבסיסי, FPDF_PAGEOBJECT, והמזהה שמוחזר הוא האופן שבו אתה ממקם את ההצבה. ללא טרנספורמציה, החותמת נוחתת בראשית הקואורדינטות בעמוד המקור עצמו, וזה רק לעיתים נדירות המקום שבו אתה רוצה אותה
מכיוון ש-InsertFormObjectFromXObject מכניס עותק אחד לכל קריאה ומחזיר אובייקט עמוד חדש בכל פעם, אתה יכול לצבוע את אותו XObject מספר פעמים בעמוד אחד בטרנספורמציות שונות, והתוכן השמור עדיין נספר פעם אחת בקובץ. לוגו פינתי וסימן מים חלש על פני כל העמוד יכולים להגיע מאותו אובייקט שנלכד
var
PageObj: FPDF_PAGEOBJECT;
M: TPdfMatrix;
begin
// The current page of Dest receives one copy of the XObject.
PageObj := Dest.InsertFormObjectFromXObject(XObject);
if PageObj = nil then
raise Exception.Create('Insert failed on this page');
// Position it: move 200 units right, 500 up, at 70% scale.
M := TPdfMatrix.Create;
try
M.Scale(0.7, 0.7);
M.Translate(200, 500);
FPDFPageObj_SetMatrix(PageObj, M.Handle);
finally
M.Free;
end;
// Dest.SaveLoadedDocument(...) when every page is done.
end;
פרט בעלות אחד הופך את הניקוי לבטוח. ברגע שהוכנס, אובייקט העמוד שייך לעמוד, ולא ל-XObject. שחרור ה-XObject מאוחר יותר אינו מבטל את ההצבות שכבר ביצעת. זה מה שמאפשר לסדר של צור-מקם-שחרר המתואר למטה לעבוד
כלל אורך החיים של המזהה שמכשיל אנשים
שתי מגבלות שולטםי במזהה ה-XObject, והתעלמות מכל אחת מהן מייצרת כישלון שנראה לא קשור לסיבתו. ראשית, מסמך המקור חייב להיות פעיל ברגע שאתה קורא ל-CreateXObjectFromPage. הלכידה קוראת את תוכן עמוד המקור ממסמך המקור הפעיל, ולכן המסמך ההוא והעמוד שלו חייבים להיות פתוחים ותקינים בעת בניית המזהה. שנית, וזה החלק שמפתיע אנשים, יש לשחרר את המזהה לפני שעמוד המקור נסגר, ובפועל לפני שאתה סוגר או משחרר את מסמך המקור שממנו הוא הגיע
הסיבה היא שה-XObject הוא הפניה לתוך מבנה שמסמך המקור עדיין מחזיק בו. זה אינו עותק מנותק ועצמאי שאתה יכול לשאת איתך לאחר שהמקור נעלם. סגור את המקור תחילה והמזהה יישאר מצביע על תוכן שפורק, כך ששחרור שלו מאוחר יותר, או כל שימוש אחר בו, פועל על זיכרון שאינו תקין עוד. הסימפטום הוא הקלאסי עבור מזהה תלוי: חריגת גישה (access violation) בעת כיבוי, או השחתה לסירוגין שמשתנה בהתאם לסדר ההקצאות, עם מחסנית שמצביעה על קוד ניקוי ולא על השורה שבאמת גרמה לבעיה. התיקון הוא סדר הפעולות, לא קוד הגנתי. בנה את ה-XObject, הכנס אותו לכל עמוד שזקוק לו, שחרר את ה-XObject, ורק אז סגור את מסמך המקור. ה-destructor של TPdfXObject משחרר את מזהה ה-PDFium הבסיסי עבורך, כך ששחרור העטיפה בזמן הנכון הוא כל האחריות שלך
המטריצה, ומה שששת המספרים שלה מביעים
מיקום הוא טרנספורמציה דו-ממדית (2D affine transform), אותה אחת ש-PDF משתמש בה בכל מקום למיקום תוכן (תקן ISO 32000-1, סעיף 8.3.4). אלו שישה מספרים, הנכתבים a, b, c, d, e, f, ו-PDFium חושף אותם כרשומת FS_MATRIX. הם ממפים נקודה ממרחב האובייקט למרחב העמוד
// x' = a*x + c*y + e
// y' = b*x + d*y + f
//
// a, d : horizontal and vertical scale
// b, c : the shear / rotation terms
// e, f : translation (where the origin lands on the page)
אתה יכול למלא את ששת הערכים הללו ביד, אך הרכבתם ביד היא המקום שבו סיבוב משתבש, מכיוון שסיבוב מערבב את כל הארבעה a, b, c, d יחד. עטיפת ה-TPdfMatrix מרכיבה את הפעולות הנפוצות עבורך ומבצעת כפל-מצד-ימין ככל שהיא מתקדמת, כך ש-Translate, Scale ו-Rotate משתרשרים בסדר שבו אתה קורא להם. סימן מים אלכסוני הוא סיבוב שלאחריו מגיע תרגום (translate) כדי למקם אותו מחדש במרכז; לוגו פינתי הוא קנה מידה שלאחריו מגיע תרגום. כאשר המטריצה מוכנה, מסור את הערך הגולמי שלה ל-FPDFPageObj_SetMatrix(PageObj, M.Handle), שבו M.Handle הוא ה-FS_MATRIX הבסיסי. הפונקציה ברמה הנמוכה יותר FPDFPageObj_Transform, המקבלת את ששת הערכים ישירות כערכי double, זמינה כאשר תעדיף להעביר מספרים במקום לבנות עטיפה
החתמת כל עמוד, בסדר הנכון
התבנית המלאה מחברת את החלקים יחד עם הסדר שכלל אורך החיים דורש. פתח את שני המסמכים, לוכד את החותמת פעם אחת, עבר בעמודי היעד תוך בחירת כל אחד בתורו והכנסה פלוס מיקום של עותק, ואז שחרר את ה-XObject, ואז שמור, ואפשר למסמך המקור להיסגר אחרון
procedure StampEveryPage(const ASource, AStamp, AOutput: string);
var
Dest, Stamp: TPdf;
XObject: TPdfXObject;
PageObj: FPDF_PAGEOBJECT;
M: TPdfMatrix;
i: Integer;
begin
Dest := TPdf.Create;
Stamp := TPdf.Create;
try
Dest.LoadFromFile(ASource);
Stamp.LoadFromFile(AStamp);
// 1. Capture the artwork once. Stamp is active here.
XObject := Dest.CreateXObjectFromPage(Stamp, 0);
if XObject = nil then
raise Exception.Create('Could not capture the stamp page');
try
// 2. Place a copy on every page of Dest.
for i := 0 to Dest.PageCount - 1 do
begin
Dest.CurrentPageIndex := i; // make page i current
PageObj := Dest.InsertFormObjectFromXObject(XObject);
if PageObj = nil then
Continue;
M := TPdfMatrix.Create;
try
M.Rotate(45); // diagonal watermark
M.Translate(150, 100); // nudge into position
FPDFPageObj_SetMatrix(PageObj, M.Handle);
finally
M.Free;
end;
end;
finally
XObject.Free; // 3. free BEFORE Stamp closes
end;
// 4. Write the result while Dest is still open.
Dest.SaveLoadedDocument(AOutput);
finally
Stamp.Free; // source closes last
Dest.Free;
end;
end;
צורת בלוקי ה-try היא שעושה את העבודה האמיתית. ה-finally הפנימי משחרר את ה-XObject לפני שהבקרה יכולה להגיע ל-finally החיצוני שמשחרר את Stamp, כך שהמזהה תמיד משוחרר בעוד המקור שלו עדיין חי, גם אם חריגה מתרחשת באמצע הלולאה. הבן את הקינון הזה נכון וכלל אורך החיים יטופל מעצמו. (השתמש בבורר העמוד הנוכחי שהבנייה שלך חושפת; גוף הלולאה זהה בכל מקרה)
החתמה היא פינה אחת של ארגז כלים גדול יותר לבנייה ועריכה של תוכן עמודים. אם החותמת שלך היא תמונה בעצמה ולא עמוד שנלכד, המרת תמונות למסמכי PDF עם PDFium מכסה את הכנסת מפת הסיביות ההיא למסמך תחילה. וכאשר הדבר שברצונך לשאת לצד החותמת הגלויה הוא קובץ ולא דיו בעמוד, עבודה עם קבצים מצורפים ל-PDF ב-Delphi מראה את צד הקבצים המוטמעים. כל זה נשלח עם PDFium Component עבור Delphi ו-C++Builder, לצד ממשקי ה-API לרינדור, עריכה ומסמכים המכוסים במקומות אחרים בבלוג זה