ל-PDFium יש מוניטין כמנוע תצוגה, המרנדר מאחורי כרטיסיית ה-PDF של Chrome, לכן הדבר הראשון שיש להבהיר הוא ש-PDFium Component יכול גם לבנות מסמך שמעולם לא היה קיים קודם. צד החיבור (authoring) עוטף את ממשק העמוד-אובייקט (page-object) של PDFium: אתה יוצר מסמך ריק, מוסיף עמודים בממדים מפורשים, וזורק טקסט, נתיבים וקטוריים, ותמונות לכל עמוד בקואורדינטות שאתה בוחר. אין שפת תיאור עמוד (page description language) ללמוד ואין מנהל התקן מדפסת (print driver) בלולאה. אתה קורא למתודות, הספרייה מרכיבה אובייקטי PDF, ו-SaveAs מבצעת סריאליזציה (serializes) לתוצאה
מה שאינך מקבל הוא מנוע פריסה (layout). זה מספיק חשוב כדי לומר זאת מראש, מכיוון שזה מעצב כל דוגמה להלן. PDFium Component ממקם תוכן היכן שאתה אומר לו, בקואורדינטות מוחלטות, ובשום מקום אחר. הוא לא ישבור שורות בפסקה (wrap), יזרים טקסט מעבר למעבר-עמוד, או יחשב טבלה מתוך שורות ועמודות. אלו הן העבודה שלך. אם הגעת לכאן בציפייה למשהו שיזרים מחדש (reflows) טקסט כמו שמעבד תמלילים עושה, כייל כעת: זהו ממשק מיקום (placement API) מדויק וברמה-נמוכה, שקרוב יותר לציור על קנבס מאשר לסידור-דפוס של מסמך (typesetting). עבור חשבוניות, תעודות, תוויות ועמודי דוחות מיוצרים, שבהם אתה כבר יודע לאן שייך כל רכיב, הדיוק הזה הוא בדיוק מה שאתה רוצה
המינימום שמפיק קובץ
שלוש קריאות עומדות בין TPdf ריק ל-PDF שמור: צור את המסמך, הוסף עמוד, כתוב אותו החוצה. כל השאר זה תוכן שאתה פורס בשכבות באמצע
uses
Vcl.Graphics, // עבור clBlack ו-TColor
PDFium; // TPdf יושב כאן
procedure CreateBlankPdf(const FileName: string);
var
Pdf: TPdf;
begin
Pdf := TPdf.Create(nil);
try
Pdf.CreateDocument; // מסמך ריק בזיכרון
Pdf.AddPage(0, 595, 842); // A4 לאורך/דיוקן, בנקודות
Pdf.AddText('First page', 'Arial', 18, 50, 780);
Pdf.SaveAs(FileName); // סריאליזציה לדיסק
finally
Pdf.Active := False;
Pdf.Free;
end;
end;
פרט אחד מכשיל אנשים שראו קטעי קוד (snippets) ישנים יותר: אינך מקצה Pdf.Active := True לאחר CreateDocument. המאפיין Active מדווח האם ידית (handle) המסמך קיימת, ו-CreateDocument כבר יצר אחת, כך שהמאפיין הוא True ברגע שהקריאה חוזרת. הגדרה מחדש היא במקרה הטוב פעולת-סרק (no-op) ובמקרה הרע מטעה את הקורא הבא. Active מרוויח את לחמו בדרך החוצה: הקצאת False משחררת את המסמך הבסיסי לפני Free, שזהו סדר הפירוק (teardown) הנקי. התייחס ל-CreateDocument ולפתיחה שטוענת-קובץ כפעולות המוציאות זו את זו (mutually exclusive). הספרייה מסרבת ליצור מסמך חדש על TPdf שכבר יש לו אחד פתוח, כך ששימוש חוזר פירושו סגירת המסמך הנוכחי תחילה
קואורדינטות מתחילות בשמאל-למטה
זוג הארגומנטים השני ב-AddText, ובכל קריאת מיקום, הוא נקודה במרחב המשתמש של PDF. המקור (origin) יושב בפינה השמאלית-תחתונה של העמוד, X רץ ימינה, ו-Y רץ למעלה. יחידה אחת היא נקודה אחת, 1/72 אינץ', כך שעמוד A4 הוא 595 על 842 יחידות ו-US Letter הוא 612 על 792. ה-Y הכלפי-מעלה הזה הוא המקור הנפוץ ביותר לבלבול של "הטקסט שלי מחוץ לעמוד", משום שקואורדינטות מסך ומפות-סיביות שמות את המקור למעלה כאשר Y גדל כלפי מטה. בעמוד בגובה 842 נקודות, כותרת קרוב למעלה יושבת סביב Y 780, לא Y 60. כאשר ריצה (run) נוחתת במקום לא צפוי, גובה העמוד פחות ה-Y שלך הוא כמעט תמיד המספר שאליו באמת התכוונת
AddPage לוקח עמדת הוספה כארגומנט הראשון שלו, מבוסס-1, כאשר 0 הוא קיצור-דרך נוח ל-"תחילת המסמך". העבר 0 או 1 עבור העמוד הראשון והעמוד מוכנס בחזית; העבר את הערך התואם לספירה שאליה אתה מצרף כדי להוסיף בסוף. העמוד שזה עתה נוסף הופך גם לעמוד הנוכחי, זה שקריאות ציור עוקבות מכוונות אליו, כך שאין שלב נפרד של "בחר עמוד זה" לאחר הוספתו. אם אתה מוסיף כמה עמודים ומאוחר יותר צריך לצייר בחזרה על אחד מוקדם יותר, הגדר את PageNumber כדי להזיז את הסמן; כל עוד אתה ממלא עמודים לפי הסדר כפי שאתה יוצר אותם, אתה יכול לעזוב אותו בשקט
כתיבת טקסט, וכלל הגופן שנושך בשקט
החתימה של AddText נושאת כל מה שריצה יחידה צריכה: המחרוזת, שם גופן, גודל בנקודות, עוגן (anchor) ה-X וה-Y, ואז צבע אופציונלי, בית אלפא לשקיפות, וזווית סיבוב במעלות
procedure WriteHeader(Pdf: TPdf; const Title, Author: string);
begin
// כותרת בשחור, אטימות בררת-מחדל, ללא סיבוב
Pdf.AddText(Title, 'Arial', 20, 50, 780);
// שורת כותב בהירה יותר 24 נקודות מתחתיה
Pdf.AddText('By ' + Author, 'Arial', 11, 50, 756, clGray);
// חותמת טיוטה אלכסונית חלשה לרוחב העמוד
Pdf.AddText('DRAFT', 'Arial', 64, 180, 380, clGray, $30, 45.0);
end;
בית האלפא רץ מ-$00 (בלתי נראה) עד $FF (אטום), שזה מה שהופך את חותמת הטיוטה לסימן-מים (watermark) ולא לבלוק אטום: $30 הוא בערך תשעה עשר אחוזי אטימות, מספיק כדי לקרוא דרכו. הזווית מסובבת את הריצה נגד כיוון השעון סביב העוגן שלה, כך ש-45 מעלות נותן את החותמת הקלאסית מפינה-לפינה. כל זה לא זקוק לתכונת סימן-מים נפרדת. סימן-מים הוא פשוט קריאת AddText גדולה, חצי-שקופה ומסובבת, וציור שלה לפני או אחרי גוף הטקסט (body) קובע אם היא תשב מאחורי או מעל התוכן
גופנים ראויים למשפט זהיר, מכיוון שמצב הכשל שקט. כאשר אתה מעביר שם גופן, PDFium Component מבקש ממערכת ההפעלה את נתוני ה-TrueType של הגופן הזה ומטמיע אותם במסמך, וזו הסיבה שקובץ שנבנה על המכונה שלך מרונדר באופן זהה במכונה שמעולם לא הותקן בה הגופן. העוקץ הוא מה שקורה כאשר השם לא נפתר (resolve): שגיאת כתיב, או פני-גופן שפשוט לא קיימים במכונת הבנייה (build machine). אין חריגה (exception). הספרייה תיסוג ליצירת אובייקט טקסט שנושא את השם כתווית בלבד, מבלי להטמיע דבר, ומשאירה למציג (viewer) להחליף את זה במה שהוא מחשיב קרוב. הטקסט מופיע בבדיקות שלך, נראה סביר, ומשנה מדדים (metrics) או גליפים ברגע שהקובץ נפתח איפשהו שמותקנים בו גופנים אחרים. השתמש בשמות שאתה יודע שקיימים במכונה המייצרת, התייחס לרשימת הגופנים כתלות פריסה (deployment dependency), ופתח דוגמה במציג במערכת נקייה לפני שאתה סומך על הפלט
צורות וקטוריות: בנה נתיב, ואז בצע (commit) אותו
קווים, מלבנים, ואזורים מלאים עוברים דרך נתיב (path). אתה פותח אחד עם CreatePath, שמגדיר את נקודת ההתחלה ואת כל העיצוב בבת אחת, מצב מילוי, צבעי מילוי וקו (stroke) עם בתי האלפא משלהם, רוחב קו, סיומות (caps) וחיבורי (joins) קווים. ואז אתה מרחיב אותו עם LineTo, BezierTo ו-ClosePath, ולבסוף AddPath מבצע (commits) את הנתיב הגמור על העמוד. קל לשכוח את שלב הביצוע (commit) והוא לא מפיק כלום אם תדלג עליו
procedure DrawDivider(Pdf: TPdf; X, Y, Width: Single);
begin
// קו אופקי דק. העמסת המלבן (rectangle overload) מגדירה תיבה ישירות:
// X, Y, Width, Height, ואז מצב מילוי וצבעים.
Pdf.CreatePath(X, Y, Width, 0.5, fmNone, clBlack, $FF,
True, clBlack, $FF, 1.0);
Pdf.AddPath;
end;
procedure DrawTriangle(Pdf: TPdf);
begin
// העמסת נקודה (Point overload): התחל בקודקוד הראשון, שרטט קו לשאר, סגור.
Pdf.CreatePath(200, 300, fmWinding, clBlue, $80, True, clNavy, $FF, 2.0);
Pdf.LineTo(300, 300);
Pdf.LineTo(250, 400);
Pdf.ClosePath;
Pdf.AddPath; // שום דבר לא מצויר עד שזה רץ
end;
שתי העמסות (overloads) מכסות את המקרים הנפוצים. הצורה בעלת ארבע-קואורדינטות לוקחת X, Y, רוחב, וגובה ונותנת לך מלבן מיושר-צירים בקריאה אחת, שזה מה שאתה שולח ידו כדי לצייר קו אופקי, גבול של תא, או פאנל רקע מלא. הצורה בעלת שתי-קואורדינטות מגדירה רק נקודת התחלה, ואתה מתחקה אחר שאר קווי המתאר בעצמך עם LineTo ו-BezierTo. מצב המילוי שולט כיצד נצבעים אזורים חופפים: fmWinding מתאים לרוב הצורות המוצקות, fmAlternate מטפל בחיתוכים (cutouts) וקווים המצטלבים בעצמם, ו-fmNone משאיר נתיב של קו-בלבד (stroked-only) ללא מילוי, שזה מה שהמפריד לעיל משתמש בו
טבלאות הן נתיבים וטקסט, המורכבים ביד
מכיוון שאין פרימיטיב של טבלה, טבלה היא לולאה. אתה מחליט על היסטי ה-X (offsets) של העמודות וגובה השורה, כותב כל תא בעזרת AddText, ומצייר את קווי ההפרדה בעזרת נתיבי מלבן. האריתמטיקה היא שלך, אבל היא פשוטה, ומרגע שנכתבה היא מכלילה (generalizes) לכל רשת (grid) שתצטרך
procedure DrawTable(Pdf: TPdf; Left, Top: Double);
const
ColX: array[0..2] of Double = (0, 110, 210); // היסטים לעמודות
RowH = 20;
var
Y: Double;
Row: Integer;
begin
// שורת כותרת
Pdf.AddText('Item', 'Arial', 10, Left + ColX[0], Top);
Pdf.AddText('Qty', 'Arial', 10, Left + ColX[1], Top);
Pdf.AddText('Price', 'Arial', 10, Left + ColX[2], Top);
// קו מתחת לכותרת
Pdf.CreatePath(Left, Top - 5, 260, 0.5, fmNone, clBlack, $FF);
Pdf.AddPath;
// שורות נתונים, פסיעה של Y כלפי מטה בכל איטרציה
Y := Top;
for Row := 1 to 3 do
begin
Y := Y - RowH;
Pdf.AddText('Item ' + IntToStr(Row), 'Arial', 9, Left + ColX[0], Y);
Pdf.AddText(IntToStr(Row * 2), 'Arial', 9, Left + ColX[1], Y);
Pdf.AddText('$' + IntToStr(Row * 10) + '.00', 'Arial', 9, Left + ColX[2], Y);
end;
end;
שים לב ל-Y שפוסע כלפי מטה בגובה השורה בכל מעבר, שוב משום שכלפי מעלה הוא חיובי. כאן גם מתגלה היעדרה של מדידת-טקסט: שום דבר לא עוצר שם פריט ארוך מלגלוש לעמודה הבאה, משום שהספרייה לא יודעת עד כמה רחבה המרונדרת המחרוזת שלך. עבור פלט בפורמט-קבוע שבו אתה שולט בנתונים, אתה מקצה גודל נדיב לעמודות וממשיך הלאה. עבור תוכן משתנה באמת, אתה או מגביל קלטים או מודד רוחבי גליפים בעצמך לפני שאתה ממקם אותם, וזו הנקודה שבה ספריית הרכבה (composition library) ייעודית מתחילה להחזיר את ההשקעה
תמונות ומרובה-עמודים
תוכן רסטר (Raster) נכנס פנימה דרך עוזרי-התמונות. AddPicture לוקח TPicture טעון וממקם אותו בנקודה, עם רוחב וגובה אופציונליים כדי לשנות את קנה המידה שלו; AddImage מקבל נתיב קובץ או TBitmap ישירות, ו-AddJpegImage מזרים בתי JPEG ללא סבב הלוך-חזור דרך מפת-סיביות. כמו בכל דבר אחר, קואורדינטות המיקום הן הפינה השמאלית-תחתונה של התמונה במרחב המשתמש, והרוחב והגובה הם הגודל על-העמוד בנקודות, לא ממדי הפיקסל של המקור
procedure CreateMultiPageReport(const FileName: string; PageCount: Integer);
var
Pdf: TPdf;
P: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.CreateDocument;
for P := 1 to PageCount do
begin
Pdf.AddPage(P, 595, 842); // הוסף (append); העמוד החדש הופך לנוכחי
Pdf.AddText('Page ' + IntToStr(P) + ' of ' + IntToStr(PageCount),
'Arial', 10, 50, 30); // כותרת תחתונה ליד הקצה התחתון
// ... צייר את גוף הטקסט של עמוד זה כאן ...
end;
Pdf.SaveAs(FileName);
finally
Pdf.Active := False;
Pdf.Free;
end;
end;
מסמך מרובה-עמודים הוא הדפוס של עמוד-יחיד בלולאה. כל AddPage מוסיף עמוד והופך אותו לנוכחי, כך שגוף הטקסט והכותרת התחתונה שתצייר מיד אחר-כך ינחתו על העמוד שזה עתה הוספת. אינך מקצה מחדש את PageNumber בתוך הלולאה הזו, משום שהוספת עמוד כבר הזיזה את הסמן לשם; אתה צריך את PageNumber רק כאשר אתה חוזר לעמוד שלא לפי סדר היצירה. קרא ל-SaveAs פעם אחת בסוף, לאחר שהעמוד האחרון מלא. אם אתה זקוק לפרופיל ארכיוני במקום לקובץ רגיל, אותו אובייקט מסמך חושף את SaveAsPdfA ואת משתני התאימות האחרים, כך שהבחירה בתקן הפלט היא קריאת שמירה שונה, לא נתיב בנייה שונה
איפה זה משתלב
המסגור (framing) הכן הוא שממשק החיבור (authoring API) של PDFium Component הוא שכבה נאמנה ודקה מעל מודל העמוד-אובייקט של PDFium: יצירת מסמכים אמיתית, גופנים מוטמעים אמיתיים, תוכן וקטורי ורסטר אמיתי, עם סריאליזציה לקובץ התואם תקנים. הוא אינו, ולא מתיימר להיות, מנוע מסמכים מזרים-מחדש (reflowing). קו ההפרדה הוא פריסת הטקסט (layout). אם הפלט שלך מבוסס תבניות – חשבוניות, תעודות, תוויות, לוחות מחוונים (dashboards) מרונדרים לרשת (grid) קבועה – מודל הקואורדינטות-המוחלטות הוא ישיר ומהיר והקוד נשאר קריא. אם הפלט שלך הוא טקסט בפורמט-ארוך שחייב לשבור-שורות ולעמד את עצמו, אתה תמצא את עצמך בונה-מחדש מנוע פריסה על גבי הקריאות הללו, וזהו הכלי הלא נכון לעבודה. לדעת באיזה צד של הקו הזה אתה נמצא זה רוב ההחלטה
מתודות היצירה המתוארות כאן הן חלק מ-PDFium Component עבור Delphi, המזווג נתיב חיבור זה עם תכונות הרינדור וחילוץ-הטקסט ש-PDFium ידוע בהן יותר