מאמר טכני

Flexbox, CSS Grid והערות שוליים ב-PDF מדלפי

PDF Library for Delphi מציגה HTML לתוך עמוד PDF עם פריסה דו-ממדית אמיתית: display: flex ו-display: grid נמדדים ומוצבים במקום להתדרדר לבלוקים מוערמים, והערות שוליים שמורות בתחתית התיבה הנושאת את ההפניה שלהן, עם מספור שנשאר רציף על פני עמודות ועמודים. נקודות הכניסה הן המוכרות, DrawHTMLTextBox עבור תיבה בודדת ו-DrawHTMLStory עבור זרימה מרובת-עמודות

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

מה השתנה במודל הפריסה?

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

Flex ו-grid כל אחד מוסיף מעבר מדידה חסום על פני ילדי מכל, והמילה החשובה היא חסום. מכל flex מודד עד 256 ילדים ישירים לתוך מערך קבוע. grid משתמש במטריצת תפוסה של לכל היותר 64 על 64 תאים עבור מיקום אוטומטי דטרמיניסטי. תקרות אלה קיימות כך שגיליון סגנון עוין או פשוט שנוצר לא יוכל להניע רקורסיה בלתי חסומה או זיכרון מיקום ריבועי, מה שדאגה אמיתית כש-HTML מגיע מתבנית שלקוח עורך

איך פריטי flex מקבלים את הגדלים שלהם

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

justify-content, align-items, gap והכיוונים ההפוכים פועלים על גיאומטריה שכבר נמדדה. הם מזיזים תיבות; הם לעולם לא מפעילים מדידה מחדש של תוכן פריט. ההפרדה הזו היא מה ששומר על לוח מחוונים מורכב מלמדוד את ילדיו כמה פעמים

uses
  PDFlibrary;

var
  Lib: TPDFlib;
  Html, Remainder: WideString;
begin
  Lib := TPDFlib.Create;
  try
    Lib.NewDocument;
    Lib.SetPageSize('A4');
    Lib.NewPage;

    Html :=
      '<div style="display:flex; gap:12px;">' +
      '  <div style="flex:2 1 0; background:#f4f6f8; padding:8px;">' +
      '    <b>Revenue</b><br/>EUR 4,182,300</div>' +
      '  <div style="flex:1 1 0; background:#f4f6f8; padding:8px;">' +
      '    <b>Margin</b><br/>18.4%</div>' +
      '  <div style="flex:1 1 0; background:#f4f6f8; padding:8px;">' +
      '    <b>Backlog</b><br/>92 days</div>' +
      '</div>';

    Remainder := Lib.DrawHTMLTextBox(40, 40, 515, 120, Html);
    if Remainder <> '' then
      Log('content did not fit - carry the remainder to the next box');

    Lib.SaveToFile('dashboard.pdf');
  finally
    Lib.Free;
  end;
end;

ערך ההחזרה הוא מחרוזת ההמשך, וזו הדרך שבה כל נקודת כניסה לציור HTML מדווחת מה לא נכנס. העבירו אותה לתיבה הבאה או לעמוד הבא והזרימה מתחדשת מהמקום שבו נעצרה

מיקום grid, ומה מסלול יכול להיות

מסלולי grid מקבלים אורכים קבועים, אחוזים, יחידת fr, ביטויי repeat() פשוטים ו-minmax(). מיקום אוטומטי ממלא את מטריצת התפוסה באופן דטרמיניסטי, כך שאותו HTML תמיד מפיק את אותו סידור. קואורדינטות מפורשות מותרות להתחפף, וזה מכוון: עיצוב ששכב תג מעל כרטיס מבטא כוונה, לא שגיאה. כשרק ציר אחד ניתן במפורש, החיפוש מיקום מחפש רק בציר השני

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

Html :=
  '<div style="display:grid; grid-template-columns:repeat(3, 1fr); ' +
  '            gap:10px;">' +
  '  <div style="grid-row:span 2; background:#eef;">Site plan</div>' +
  '  <div>Inspector</div>' +
  '  <div>Date</div>' +
  '  <div style="grid-column:2 / span 2;">Findings summary</div>' +
  '</div>';

Remainder := Lib.DrawHTMLTextBox(40, 180, 515, 260, Html);

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

למה הערות שוליים הן בעיית חלוקה לעמודים?

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

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

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

// הערות שוליים בתוך סיפור מרובה-עמודות שומרות על רצף רץ אחד
Html := LoadTemplate('chapter.html');    // משתמש בסמני float:footnote
Remainder := Lib.DrawHTMLStory(40, 40, 515, 700,
  2,        // עמודות
  16,       // מרווח בין עמודות בנקודות
  20,       // מספר עמודים מרבי לסיפור זה
  Html);
if Remainder <> '' then
  Log('story exceeded its page budget');

הדרכה מעשית לכותבי תבניות

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

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

כשה-HTML מגיע ממנוע דוחות ולא מתבניות שנכתבו ידנית, המסלול המונע-קבוצת-נתונים במנוע דוחות מקבוצת הנתונים משתלב היטב עם זה, ומייצר את התגים ש-flex ו-grid אז מסדרים. וכשאותו תוכן צריך גם לצאת מה-PDF שוב, נתיב הייצוא הסמנטי בייצוא PDF ל-Markdown ו-DOCX סוגר את מסע ההלוך-חזור

פריסת HTML, יצירת דוחות וייצוא סמנטי הם חלק מספרייה אחת לדלפי, C++Builder ו-Free Pascal; רשימת התכונות המלאה נמצאת בעמוד PDF Library for Delphi