מאמר טכני

ציור על קנבס HotPDF ב-Delphi: נתיבי וקטור וצבע

HotPDF מצייר גרפיקה וקטורית בכך שהוא בונה נתיב על העמוד הנוכחי ואז מבקש לצבוע אותו. אין שלב מפת סיביות באמצע. קו שאתה מצייר עם MoveTo ו-LineTo מגיע כאופרטורי נתיב של PDF בזרם התוכן, ולכן הוא נשאר וקטור אמיתי: חד בזום של 50%, חד ב-1600%, ובשבריר מהגודל שגרסה שעברה רסטריזציה הייתה עולה. עבור תרשימים, קווי טבלה, צירי גרף וקישוטי טפסים, זה בדיוק מה שאתה רוצה, והממשק שמאחוריו קטן מספיק כדי ללמוד אותו בישיבה אחת

כל משטח הציור חי על THotPDF.CurrentPage. בין BeginDoc ל-EndDoc אתה קובע צבע ועובי קו על אובייקט העמוד הזה, מניח גיאומטריה, וקורא לאופרטור צביעה כדי לחייב אותה. ארבע הפרימיטיבות שתשתמש בהן הכי הרבה הן MoveTo ו-LineTo לנתיבים שרירותיים, Rectangle לתיבות, Circle לעיגולים, ושני אופרטורי הצביעה Stroke ו-Fill

מערכת הצירים היא שמאלית-תחתונה

זה הדבר האחד שמכשיל כל מי שמגיע מ-VCL. ה-TCanvas שאתה צובע בו פקדים מציב את הראשית בפינה השמאלית העליונה כאשר Y גדל כלפי מטה. PDF עושה את ההפך. HotPDF מודד מהפינה השמאלית התחתונה של העמוד בנקודות (1/72 אינץ׳), כאשר Y גדל ככל שאתה עולה. נקודה ב-Y := 720 יושבת סמוך לראש עמוד US Letter, שגובהו 792 נקודות, ו-Y := 50 יושבת סמוך לתחתית. אם הציור הראשון שלך יוצא הפוך אנכית, זו הסיבה: קוד שהועבר מגרפיקת מסך מניח את הכיוון השגוי ובורח מעבר לשפה התחתונה

אותה מוסכמה שולטת גם ב-TextOut, ולכן טקסט וצורות חולקים מודל מנטלי אחד ברגע שהפנמת אותו. תכנן פריסה בכך שתחליט היכן יושבת התחתית של כל אלמנט, לא הראש, והשאר נובע מכך

השוואה בין ראשית שמאלית עליונה של קנבס מסך לבין ראשית שמאלית תחתונה של PDF: אותה נקודה סמוך לראש עמוד US Letter נקראת Y = 72 בקוד TCanvas אך Y = 720 בנקודות של HotPDF, ולכן קוד שהועבר בלי היפוך Y מצייר הפוך
HotPDF מודד מהפינה השמאלית התחתונה בנקודות, ולכן נקודה סמוך לראש העמוד בגודל 612 על 792 נושאת Y = 720 — אותו מיקום פיזי שקוד TCanvas מכתובת כ-Y קטן כלפי מטה

נתיבים: MoveTo, LineTo, Stroke

נתיב משורטט הוא עט שמורם, מונח ונגרר. MoveTo מרים את העט וקובע את נקודת ההתחלה בלי לסמן דבר. כל LineTo מרחיב את הנתיב הנוכחי לנקודה חדשה. דבר אינו מופיע על העמוד עד שאתה קורא ל-Stroke, שמצייר את הנתיב שנצבר באמצעות צבע הקו ועובי הקו הנוכחיים, ואז מנקה את הנתיב כך שה-MoveTo הבא מתחיל מחדש

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'DrawPaths.pdf';
    Pdf.BeginDoc;

    // עובי הקו נמדד בנקודות וחל עד שתשנה אותו.
    Pdf.CurrentPage.SetLineWidth(1.5);
    Pdf.CurrentPage.SetRGBStrokeColor(clBlack);

    // קו אופקי סמוך לראש העמוד (Y נמדד מהתחתית).
    Pdf.CurrentPage.MoveTo(72, 720);
    Pdf.CurrentPage.LineTo(523, 720);
    Pdf.CurrentPage.Stroke;          // מחייב את הנתיב; דבר לא צויר לפני כן

    // פוליליין מחובר ועבה יותר: שלושה מקטעים בנתיב אחד.
    Pdf.CurrentPage.SetLineWidth(3);
    Pdf.CurrentPage.SetRGBStrokeColor(RGB(30, 90, 200));
    Pdf.CurrentPage.MoveTo(72, 640);
    Pdf.CurrentPage.LineTo(172, 690);
    Pdf.CurrentPage.LineTo(272, 620);
    Pdf.CurrentPage.LineTo(372, 680);
    Pdf.CurrentPage.Stroke;

    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

שני פרטים חוסכים זמן דיבוג אמיתי. עובי הקו הוא מצב, לא ארגומנט: SetLineWidth קובע אותו פעם אחת וכל Stroke שאחריו משתמש בערך הזה עד שתשנה אותו שוב, וזו הסיבה שהפוליליין למעלה עבה יותר מהקו. והנתיב מתאפס אחרי כל Stroke, ולכן Stroke שנשכח משמעו שהגיאומטריה שהנחת בקפידה אינה מרונדרת כלל. אם צורה חסרה בפלט, קריאת הצביעה היא המקום הראשון לבדוק בו

הקואורדינטות הן נקודות, ונקודות הן שבריות. MoveTo ו-LineTo מקבלים ערכי Single, ולכן קו שערה בעובי 0.5 נקודות או מיקום ב-72.25 הוא חוקי ומשמעותי, ואינו מעוגל אל היחידה השלמה הקרובה. הדיוק הזה חשוב בשני כיוונים הפוכים. עובי קו מתחת ל-0.5 בערך יכול להצטייר כקו הדק ביותר האפשרי התלוי בהתקן, שנעלם על המסך וחוזר בהדפסה, ולכן קו גלוי דורש עובי שאתה קובע בכוונה ולא את ברירת המחדל. בקצה השני, הצמדת קווי טבלה וקווי רשת לקואורדינטות של נקודות שלמות מונעת מרשת צפופה להיראות מעט לא אחידה במקומות שבהם קווים סמוכים מתעגלים אחרת. החלט על מרווח הרשת בנקודות מראש ושאר הפריסה יורשת אותו

צורות מלאות וצבע

פרימיטיבות סגורות יכולות להתמלא במקום להיות משורטטות בקו מתאר. Rectangle מקבל מיקום וגודל, Circle מקבל מרכז ורדיוס, וכל אחד מהם מחויב עם Fill, שצובע את הפנים בצבע המילוי הנוכחי, או עם Stroke לקו מתאר בלבד. צבע המילוי וצבע הקו הם פיסות מצב נפרדות, הנקבעות עם SetRGBFillColor ועם SetRGBStrokeColor, ששתיהן מקבלות TColor יחיד. משמעות הדבר שאתה יכול להשתמש ישירות בקבועי הצבע של Delphi ובפונקציה העוזרת RGB

מודל הנתיבים של HotPDF: MoveTo, LineTo, Rectangle ו-Circle בונים נתיב נוכחי בלתי נראה בזיכרון, ורק החיוב של Stroke, Fill או FillAndStroke צובע אותו באמצעות מצב הגרפיקה המתמיד של צבע קו, צבע מילוי ועובי קו לפני שהוא מנקה את החוצץ
הגיאומטריה נצברת בשקט בנתיב הנוכחי עד שאופרטור צביעה מחייב אותה עם צבע הקו, צבע המילוי ועובי הקו השמורים — קריאת צביעה שנשכחה משאירה את הצורה בלתי מצוירת
// Rectangle(X, Y, Width, Height): X ו-Y הם הפינה השמאלית התחתונה.
Pdf.CurrentPage.SetRGBFillColor(RGB(220, 60, 60));
Pdf.CurrentPage.Rectangle(72, 500, 160, 90);
Pdf.CurrentPage.Fill;

// Circle(X, Y, Radius): X ו-Y הם המרכז.
Pdf.CurrentPage.SetRGBFillColor(clNavy);
Pdf.CurrentPage.Circle(420, 545, 45);
Pdf.CurrentPage.Fill;

// קו מתאר בלבד: קבע צבע קו ועובי, ואז Stroke.
Pdf.CurrentPage.SetLineWidth(2);
Pdf.CurrentPage.SetRGBStrokeColor(clBlack);
Pdf.CurrentPage.Rectangle(72, 400, 160, 60);
Pdf.CurrentPage.Stroke;

שים לב לצורת הארגומנטים של Rectangle. היא מיקום בתוספת גודל, X, Y, Width, Height, ולא שתי פינות מנוגדות. ה-TCanvas.Rectangle שמפתחי Delphi מכירים מקבל (Left, Top, Right, Bottom), ולכן זיכרון השרירים ימסור ל-HotPDF פינה שנייה במקום שבו הוא מצפה לרוחב ולגובה, והתיבה תצא בגודל שגוי. הזוג (X, Y) הוא הפינה השמאלית התחתונה, בעקביות עם ראשית העמוד. עבור מעגל, (X, Y) הוא המרכז והארגומנט השלישי הוא הרדיוס בנקודות

בחירת צבע אחת שהדוגמה המקורית שגתה בה

גרסה ישנה יותר של הדוגמה הזו זרעה צבעים עם Random($FFFFFF) על כל צורה. זה נראה חיוני, וזה האינסטינקט השגוי למסמכים שנוצרים בקוד. PDF שאתה בונה מקוד הוא בדרך כלל משהו שאתה גם רוצה לבדוק, וצבעי מילוי אקראיים הופכים את הפלט לבלתי אפשרי להשוואה בין הרצה להרצה: השוואת בית-מול-בית מול קובץ ידוע-תקין נכשלת בכל פעם, בלי סיבה אמיתית. בחר צבעים מפורשים. כשאתה רוצה גיוון על פני סדרת צורות, הנע אותו מהנתונים שלך או ממערך פלטה קבוע, כך שאותו קלט תמיד מייצר את אותו קובץ. דטרמיניזם שווה יותר מחידוש כשהתוצר עובר דרך צינור שחרור

הרכבת הפרימיטיבות יחד: תיבת הסבר

כל פרימיטיבה פשוטה בפני עצמה; התמורה מתגלה כשקומץ מהן מתחבר למשהו שדוח באמת צריך. תיבת הסבר, אותה תיבה מוערת שמצביעה על איור ומסבירה אותו, משתמשת בכל מה שכוסה עד כה: מלבן מלא עם מסגרת, קו מחוון משורטט, נקודה שמעגנת את המחוון, וטקסט שנפרס בתוך התיבה באמצעות אותן קואורדינטות שמאליות-תחתונות שהצורות משתמשות בהן. FillAndStroke מצדיק את מקומו כאן, בכך שהוא צובע את הפנים ואת קו המתאר של נתיב אחד בחיוב יחיד במקום לבנות את המלבן פעמיים

אנטומיה של תיבת הסבר המורכבת מפרימיטיבות של HotPDF: מלבן המחויב עם FillAndStroke, קו מחוון משורטט מ-MoveTo ו-LineTo, נקודת עוגן מלאה, ופסי תווית של TextOut החולקים את אותה רשת שמאלית-תחתונה כמו הצורות
ארבעה חיובים בונים את תיבת ההסבר — FillAndStroke צובע פאנל ומסגרת יחד, המחוון והנקודה משתמשים שוב במצב הקו והמילוי, וכל היסט תווית הוא חשבון פשוט מול פינת התיבה (90, 600)
var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'Callout.pdf';
    Pdf.BeginDoc;

    // 1. התיבה: מילוי חיוור בתוספת מסגרת גלויה, נתיב אחד, חיוב אחד.
    //    Rectangle הוא פינה שמאלית תחתונה בתוספת גודל, Y נמדד מהתחתית
    Pdf.CurrentPage.SetRGBFillColor(RGB(255, 244, 214));   // פאנל ענבר חיוור
    Pdf.CurrentPage.SetRGBStrokeColor(RGB(180, 130, 40));  // מסגרת כהה יותר
    Pdf.CurrentPage.SetLineWidth(1);
    Pdf.CurrentPage.Rectangle(90, 600, 240, 70);
    Pdf.CurrentPage.FillAndStroke;

    // 2. המחוון: מקטע משורטט אחד משפת התיבה כלפי מטה
    //    לעבר הדבר שמוער
    Pdf.CurrentPage.SetLineWidth(1.5);
    Pdf.CurrentPage.MoveTo(90, 615);        // שפת התיבה השמאלית
    Pdf.CurrentPage.LineTo(66, 546);
    Pdf.CurrentPage.Stroke;

    // 3. נקודה מלאה מעגנת את המחוון ביעדו
    Pdf.CurrentPage.SetRGBFillColor(RGB(180, 130, 40));
    Pdf.CurrentPage.Circle(64, 542, 3);
    Pdf.CurrentPage.Fill;

    // 4. התווית, ממוקמת יחסית לפינה השמאלית התחתונה של התיבה.
    //    טקסט וצורות חולקים מערכת צירים אחת, ולכן ההיסטים
    //    הם חשבון פשוט מול (90, 600)
    Pdf.CurrentPage.SetFont('Arial', [fsBold], 10);
    Pdf.CurrentPage.TextOut(102, 645, 0, 'Check this total');
    Pdf.CurrentPage.SetFont('Arial', [], 9);
    Pdf.CurrentPage.TextOut(102, 628, 0, 'The rounding rule changed in the');
    Pdf.CurrentPage.TextOut(102, 616, 0, 'June release; verify against v2.1');

    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

שים לב כמה מעט ניהול מצב הרכיב המורכב הזה דורש. צבע המילוי, צבע הקו ועובי הקו נקבעים כל אחד מיד לפני הצורה שמשתמשת בהם, ולכן כל בלוק בציור נקרא כיחידה עצמאית וניתן לסדר אותו מחדש או לחלץ אותו לפונקציה עוזרת בלי לגרור מצב נסתר. עטוף את זה בפרוצדורה שמקבלת את נקודת העיגון ואת המחרוזות, ויש לך הערת תרשים לשימוש חוזר במחיר של ארבעים שורות

היכן ציור וקטורי משתלם, והיכן לא

פנה לקריאות הנתיב והצורה האלה כשהגיאומטריה נוצרת בקוד: קווי רשת ועמודות של גרף, קווי הסרגל של טבלת חשבונית, תיבות הסבר על תרשים, סמל לוגו המובע כקומץ נתיבים. כל זה מתקנה בלי טשטוש ומוסיף כמעט כלום לגודל הקובץ, מכיוון שמלבן הוא כמה מספרים ולא אלפי פיקסלים. הצד השני כן גם הוא. אם מה שיש לך בפועל הוא תצלום או צילום מסך, צייר אותו כתמונה עם AddImage ו-ShowImage במקום זאת; מעקב אחר מפת סיביות בקריאות וקטור אינו קונה לך דבר. המקטעים הישרים, המלבנים והמעגלים שלמעלה נושאים את הרוב המכריע של עבודת הדיווח האמיתית, ושלושת השכלולים שמפתחים שואלים עליהם אחר כך, עקומות, תבניות מקווקוות ושקיפות, יושבים על אותו אובייקט עמוד

עקומות, קווים מקווקווים ושקיפות בקצרה

עקומות חופשיות מרחיבות את אותו מנגנון נתיבים שכבר יש לך. CurveToC(X1, Y1, X2, Y2, X3, Y3) מוסיף מקטע בזייה קובי מהנקודה הנוכחית אל (X3, Y3), המתעקל לעבר שתי נקודות הבקרה, והווריאנטים המקוצרים CurveToV ו-CurveToY מכסים את המקרים שבהם נקודת בקרה אחת מתלכדת עם נקודת קצה. נתיב יכול לערבב מקטעי LineTo ו-CurveToC בחופשיות לפני ש-Stroke או Fill יחיד מחייב אותו, וכך נבנות פינות מעוגלות וקווי גרף חלקים

קווים מקווקווים הם מצב, בדיוק כמו עובי קו. SetDash([3, 3], 0) מחליף כל שרטוט שאחריו לתבנית של שלוש נקודות דלוקות ושלוש כבויות, כשהמערך מפרט את אורכי המקטעים הדלוקים והכבויים בנקודות והארגומנט השני מפעיל היסט פאזה של תחילת המחזור; NoDash מחזיר את העט לקו רציף. קבע אותו, שרטט את קווי הרשת שרוצים אותו, ואפס אותו לפני הקו הרציף הבא, אחרת הקו המקווקו מדביק בשקט את כל מה שבא אחריו

השקיפות עוברת דרך מצב גרפיקה בעל שם ולא דרך ארגומנט צבע, מכיוון שאלפא ב-PDF הוא מאפיין של מילון מצב הגרפיקה. רשום אחד כזה על המסמך עם RegisterExtGState, בהעברת אלפא מילוי ואלפא קו בין 0 ל-1, ואז החל את השם שהוא מחזיר עם CurrentPage.SetGraphicsState; מילויים ושרטוטים מהנקודה הזו נצבעים באטימות הרשומה. זה טקס כבד יותר ממקבעי הצבע, והוא שווה את זה בפעם הראשונה שפס הדגשה חייב לשבת מעל טקסט בלי להסתיר אותו

ההרגל הנותר ששווה לשמור הוא אימות. גיאומטריה שנוצרת בקוד יכולה לעבור על המכונה שלך ולהיכשל על זו של לקוח, בדרך כלל בגלל החלפת גופנים בכל טקסט שאתה מערבב פנימה או הנחה על גודל עמוד שאינה מתקיימת. פתח את הקובץ המוגמר בכמה רמות זום כדי לאשר שהשפות נשארות נקיות, ובדוק שכל צורה נוחתת בתוך תיבת השוליים שהתכוונת אליה. עם ערכת צבעים דטרמיניסטית, אפשר לבצע את הבדיקה הזו אוטומטית מול PDF ייחוס במקום לבחון אותה בעין

הקריאות MoveTo, LineTo, Stroke, Fill וקריאות הצבע המוצגות כאן הן חלק מרכיב HotPDF ל-Delphi עבור Delphi ו-C++Builder