רוב קוד Delphi הנוגע ב-PDF מתייחס לפורמט כאל מכל לשני דברים: מקטעי טקסט ומספר מפות סיביות (bitmaps) ממוקמות. ההשקפה הזו נכונה ככל שהיא מגיעה, והיא משאירה את החלק בעל היכולות הגבוהות ביותר של הפורמט ללא שימוש. עמוד PDF הוא קנבס דו-ממדי בלתי תלוי ברזולוציה הבנוי על אותו מודל דימות כמו PostScript. הוא יכול לצייר קווים, עקומות, אזורים מלאים, מעברי צבע ותבניות חוזרות, כולם כוקטורים שנשארים חדים בכל זום ומודפסים ברזולוציה המלאה של המכשיר. אם אתה מצייר לוגו, תרשים, סימן מים או מסגרת תעודה, הנתיב הוקטורי הוא כמעט תמיד הפרימיטיב הנכון, והוא קטן וחד יותר מהתמונה המרוסטררת שתוכנות רבות בוחרות להשתמש בה במקום
מאמר זה עובר על המודל הוקטורי כפי שמוגדר בתקן ISO 32000-1 ומראה את קריאות ה-PDFlibPas התואמות. המטרה היא להפוך את המפרט למוחשי, מכיוון שה-API ממופה אליו בצורה הדוקה, והבנת אחד מלמדת אותך את השני
העמוד הוא מכונת נתיבים
תקן ISO 32000-1 §8.5 מתאר גרפיקה בשני שלבים שלעולם אינם חופפים. ראשית אתה בונה נתיב, שהוא גיאומטריה טהורה ללא תוצאה נראית לעין. לאחר מכן אתה צובע את הנתיב הזה בפעולה בודדת המשרטטת את קו המתאר שלו (stroke), ממלאת את החלק הפנימי שלו (fill), או עושה את שניהם. שום דבר אינו מופיע בעמוד במהלך הבנייה. הנתיב הוא רצף מופשט של נקודות ומקטעים המוחזקים במצב הגרפי (graphics state) עד שאופרטור צביעה צורך אותו, ובשלב זה הוא מרונדר ונזרק
נתיב עשוי מתת-נתיב אחד או יותר. תת-נתיב מתחיל בנקודה וגדל על ידי הוספת מקטעים: קווים ישרים, עקומי בזייה מעוקבים, ובפלטפורמות מסוימות מלבנים שלמים הנוספים כתת-נתיב סגור משלהם. ב-PDFlibPas אתה פותח נתיב באמצעות StartPath, המגדיר את נקודת ההתחלה, ואז מרחיב אותו באמצעות AddLineToPath ו-AddCurveToPath. כל קריאה מקדמת נקודה נוכחית מרומזת, כך שהמקטע הבא ממשיך מהמקום שבו האחרון הסתיים. ClosePath מצייר מקטע ישר אחרון בחזרה להתחלת תת-הנתיב, מה שחשוב לשרטוט קו מתאר (stroking) מכיוון שהוא מייצר חיבור קו אמיתי בקודקוד הסגירה במקום שני קצוות חופשיים
// A closed quadrilateral, stroked then filled
PDF.SetLineColor(0, 0, 0);
PDF.SetFillColor(0.6, 0.8, 1.0);
PDF.SetLineWidth(1.5);
PDF.StartPath(150, 100); // open the path at the first vertex
PDF.AddLineToPath(220, 140);
PDF.AddLineToPath(180, 210);
PDF.AddLineToPath(110, 170);
PDF.ClosePath; // straight segment back to (150, 100)
PDF.DrawPath(2); // 2 = fill and stroke; path is consumed
עקומות משתמשות ב-AddCurveToPath, המקבל שתי נקודות בקרה של בזייה ונקודת קצה: AddCurveToPath(CtAX, CtAY, CtBX, CtBY, EndX, EndY). העקומה רצה מהנקודה הנוכחית אל (EndX, EndY), כשהיא נמשכת לעבר שתי נקודות הבקרה לאורך הדרך. קשתות מעגליות זמינות דרך AddArcToPath(CenterX, CenterY, TotalAngle), שבו הרדיוס נלקח מהמרחק בין הנקודה הנוכחית למרכז, והמנוע פולט את הקשת כשרשרת של מקטעי בזייה. למלבנים יש קיצור דרך, AddBoxToPath(Left, Top, Width, Height), המוסיף מלבן סגור שלם כתת-נתיב משלו ללא צורך ב-StartPath קודם
שני כללי מילוי, ומדוע הם אינם מסכימים
כאשר אתה ממלא נתיב שמצטלב עם עצמו או מכיל לולאה פנימית, המרנדר זקוק לכלל כדי להחליט אילו אזורים נמצאים בתוך הצורה ואילו הם חורים. תקן ISO 32000-1 §8.5.3.3 מגדיר שני כללים, והם יכולים לצבוע את אותה גיאומטריה באופן שונה. כלל הפיתול שאינו אפס (nonzero winding rule) סופר את ההצטלבויות בעלות הסימן של קרן הנשלחת מנקודת בדיקה לאינסוף, תוך הוספת אחת עבור כל מקטע שחוצה משמאל לימין וחיסור אחת עבור כל אחד שחוצה בכיוון ההפוך; הנקודה היא בפנים כאשר הסכום אינו אפס. כלל הזוגי-אי-זוגי (even-odd rule) מתעלם מהכיוון ופשוט סופר הצטלבויות, ומגדיר את הנקודה כפנימית כאשר הספירה היא אי-זוגית
המקרה הקלאסי שבו הם מתפצלים הוא צורה עם חור, דונאט או שייבה. צייר גבול חיצוני וגבול פנימי בתוכו. תחת כלל הזוגי-אי-זוגי הלולאה הפנימית תמיד מגלפת חור, מכיוון שכל נקודה בין שני הגבולות נחצית פעם אחת וכל נקודה בתוך הלולאה הפנימית נחצית פעמיים. תחת כלל הפיתול שאינו אפס החור מופיע רק אם הלולאה הפנימית מתפתלת בכיוון ההפוך לזו החיצונית; אם תפתל אותן באותו כיוון, הפיתולים יחזקו זה את זה במקום להתבטל, והאזור הפנימי יתמלא לחלוטין. כוכב בעל חמישה קצוות המצויר כקו מתאר בודד המצטלב עם עצמו מראה את אותו פיצול: זוגי-אי-זוגי משאיר את המחומש המרכזי ריק בעוד שפיתול שאינו אפס ממלא אותו
ב-PDFlibPas בוחר את הכלל לפי הקריאה שאתה מבצע לצביעה, ולא לפי דגל. DrawPath ממלא לפי כלל הפיתול שאינו אפס; DrawPathEvenOdd ממלא לפי כלל הזוגי-אי-זוגי. שניהם מקבלים את אותו מצב מספר שלם: 0 משרטט קו מתאר בלבד, 1 ממלא בלבד, ו-2 ממלא ומשרטט קו מתאר. כלל הזוגי-אי-זוגי הוא הכלי הקל יותר לחורים מנוקבים בדיוק מכיוון שהוא אינו דורש ממך לנהל את כיוון תת-הנתיב
// Same two boxes, two fill rules, two different results.
// Nonzero winding: both boxes wind the same way, so the inner one
// does NOT cut a hole and the whole outer box fills solid.
PDF.SetFillColor(0.2, 0.4, 0.8);
PDF.AddBoxToPath(100, 100, 200, 120); // outer
PDF.AddBoxToPath(140, 130, 120, 60); // inner
PDF.DrawPath(1); // 1 = fill, nonzero winding
// Even-odd: the inner box is crossed an even number of times,
// so it punches a clean rectangular hole through the outer box.
PDF.SetFillColor(0.2, 0.4, 0.8);
PDF.AddBoxToPath(100, 300, 200, 120); // outer
PDF.AddBoxToPath(140, 330, 120, 60); // inner cut-out
PDF.DrawPathEvenOdd(1); // 1 = fill, even-odd
מעברי צבע ציריים משנים צבע לאורך קו
צבע מילוי שטוח הוא ערך בודד על פני האזור כולו. מעבר צבע משנה את הצבע באופן רציף, והסוג הפשוט ביותר הוא מעבר צבע צירי (axial), או ליניארי. תקן ISO 32000-1 §8.7.4.5 מגדיר זאת כהצללה צירית מטיפוס 2: אתה נותן שתי נקודות המגדירות ציר, צבע התחלה בנקודה הראשונה וצבע סיום בנקודה השנייה, והמרנדר מבצע אינטרפולציה לצבע לאורך הציר הזה. כל נקודה באזור המלא מקבלת את הצבע של ההטלה המאונכת שלה על הציר, כך שמעבר הצבע רץ בפסים בזוויות ישרות לקו שבין שתי הנקודות
ב-PDFlibPas מעבר צבע הוא משאב מסמך בעל שם שאתה יוצר פעם אחת ואז בוחר כצבע הפעיל. NewRGBAxialShader רושם אותו. החתימה היא NewRGBAxialShader(ShaderName, StartX, StartY, StartRed, StartGreen, StartBlue, EndX, EndY, EndRed, EndGreen, EndBlue, Extend): שתי נקודות הקצה של הציר, שלשות ה-RGB בכל קצה כערכים בטווח של 0 עד 1, ודגל Extend. כאשר Extend מוגדר כ-1, צבעי הקצה ממשיכים כמילוי אחיד מעבר לנקודות הקצה של הציר, וזה מה שאתה בדרך כלל רוצה כדי שפינות של אזור מחוץ לציר לא יישארו ללא צבע; 0 משאיר אותן ללא שינוי. ברגע שהשיידר קיים אתה קושר אותו עם SetFillShader עבור אזורים מלאים, SetLineShader עבור קווי מתאר, או SetTextShader עבור טקסט. הקישור נשאר פעיל עבור קריאות הציור הבאות, כך שהנתיב שתצייר הבא יקבל את מעבר הצבע במקום צבע שטוח
// Define a vertical gradient once: blue at the bottom to white at the top.
PDF.NewRGBAxialShader('panelGrad',
0, 100, 0.10, 0.25, 0.55, // start point and start RGB
0, 260, 1.00, 1.00, 1.00, // end point and end RGB
1); // 1 = extend ends as solid color
// Select the gradient as the fill, then paint a rectangle with it.
PDF.SetFillShader('panelGrad');
PDF.AddBoxToPath(80, 100, 300, 160);
PDF.DrawPath(1); // 1 = fill, now filled by the shader
תבניות אריחים חוזרות על תא
במקום שבו מעבר צבע משנה צבע בודד בצורה חלקה, תבנית אריחים (tiling pattern) חוזרת על יצירת אמנות קטנה על פני אזור. תקן ISO 32000-1 §8.7.3.1 מגדיר תבנית אריחים כתא תבנית, פיסת תוכן עצמאית, שהמרנדר משכפל על גבי רשת קבועה כדי לרצף את האזור המצויר. כך אתה בונה קווקוו (hatching) עבור מילוי הנדסי, מוטיב מותג חוזר מאחורי כותרת, או רקע בעל טקסטורה שנשאר חד וקטורית ושוקל כמעט כלום לא משנה כמה גדול האזור, מכיוון שהתא נשמר פעם אחת ומאוזכר בכל מקום
ב-PDFlibPas בונה את תא התבנית מתוכן עמוד שנלכד. אתה לוכד עמוד או אזור באמצעות CapturePage, הופך את הלכידה לתבנית בעלת שם באמצעות NewTilingPatternFromCapturedPage(PatternName, CaptureID), ואז בוחר בתבנית זו כמילוי הנוכחי באמצעות SetFillTilingPattern(PatternName). מראותו רגע והלאה, כל נתיב שתמלא ייצבע עם התא החוזר במקום צבע שטוח, בדיוק כפי שמילוי שיידר עובד אך עם תא מרוצף כמקור הצבע. הרצף מורכב יותר מקריאה בודדת, כך שאם שלב הלכידה אינו מוכר לך, התייחס לתבנית כאל פעולה דו-שלבית: הפק את התא שנלכד תחילה, ואז קשור אותו כמילוי לפי שם לפני ציור האזור שברצונך לרצף
חיבור הפרימיטיבים יחד
החלקים מתחברים ישירות. בועת בזייה מלאה היא נתיב של עקומות המצוירות עם DrawPath. אותו קו מתאר המצויר עם DrawPathEvenOdd לאחר הוספת לולאה פנימית מציג חור שמילוי הפיתול היה סוגר. מלבן מלא במעבר צבע הוא תיבה הקשורה לשיידר. הדוגמה להלן מציירת את שלושתם ברצף כך שההבדל בין שני כללי המילוי נראה בעמוד אחד, ואז מניחה פאנל מעבר צבע מתחתיהם
// 1. A filled Bezier shape (nonzero winding).
PDF.SetFillColor(0.85, 0.30, 0.25);
PDF.StartPath(120, 480);
PDF.AddCurveToPath(160, 560, 240, 560, 280, 480); // top lobe
PDF.AddCurveToPath(240, 420, 160, 420, 120, 480); // bottom lobe
PDF.ClosePath;
PDF.DrawPath(1); // 1 = fill
// 2. The same outline, plus an inner loop, filled even-odd to show a hole.
PDF.SetFillColor(0.85, 0.30, 0.25);
PDF.StartPath(120, 300);
PDF.AddCurveToPath(160, 380, 240, 380, 280, 300);
PDF.AddCurveToPath(240, 240, 160, 240, 120, 300);
PDF.ClosePath;
PDF.MovePath(180, 300); // new subpath: the hole
PDF.AddArcToPath(200, 300, 360); // a full circle
PDF.ClosePath;
PDF.DrawPathEvenOdd(1); // hole is punched out
// 3. A rectangle filled with an axial gradient.
PDF.NewRGBAxialShader('footerGrad',
60, 100, 0.95, 0.55, 0.10,
60, 200, 0.20, 0.10, 0.40,
1);
PDF.SetFillShader('footerGrad');
PDF.AddBoxToPath(60, 100, 340, 100);
PDF.DrawPath(1);
שני פרטים כדאי לזכור. קריאת הצביעה קובעת את כלל המילוי, כך שהבחירה בין DrawPath ל-DrawPathEvenOdd היא הבחירה בין פיתול שאינו אפס לבין זוגי-אי-זוגי, ועבור צורות עם חורים כלל הזוגי-אי-זוגי חוסך ממך את הצורך לחשב את כיוון תת-הנתיב. והמצב הגרפי נדגם ברגע שאתה צובע: הגדר את הצבעים שלך, רוחב הקו וקישור השיידר לפני קריאת הצביעה, מכיוון שזה המצב שהמנוע קורא. בנה תחילה, הגדר את המצב, צבע אחרון, והמודל הוקטורי יתנהג בצורה צפויה בכל פעם
מכאן, הצעדים הבאים הטבעיים הם קריאת וקטורים וטקסט בחזרה מתוך מסמך קיים, המכוסה במאמר שלנו על חילוץ טקסט, תמונות וגופנים, ורינדור של אותו מודל ציור להקשר התקן (device context) של Windows עבור תצוגה מקדימה על המסך והדפסה, המכוסה במדריך הדפסה ותצוגה מקדימה. קריאות הנתיב, השיידר והתבנית המתוארות כאן נשלחות כחלק מספריית PDF עבור Delphi לצד ממשקי ה-API לטקסט, תמונות, טפסים וחתימה המכוסים במקומות אחרים בבלוג זה