HotPDF יכולה לבנות מסמך מחולק לעמודים מתוך עץ דקלרטיבי במקום מקואורדינטות. מרכיבים THPDFDOMDocument מסעיפים, ערימות, טקסט, רשימות וטבלאות, מוסרים אותו ל-THPDFDOMRenderer, והמציג מודד, מחלק לעמודים, מצייר את ריהוט העמוד, וכשמתבקש, פולט את עץ המבנה של PDF/UA שהופך את התוצאה לנגישה. קוד הפריסה לעולם לא מחשב קואורדינטת y
מי שתחזק אי פעם מחולל דוחות מונחה-קואורדינטות יודע למה זה חשוב. הגרסה הראשונה עובדת. אחר כך כתובת לקוח גדלה לשלוש שורות, טבלה מוסיפה שורות, כותרת מותאמת שפה עוברת לשורה חדשה, וכל קואורדינטת y במורד הזרם שגויה. התיקונים מצטברים כבדיקות שבירת-עמוד ידניות המפוזרות בלוגיקה עסקית, ודרישת ה-PDF המתויג שמגיעה כעבור שנתיים לא ניתנת להתאמה בדיעבד לקוד שאין לו מושג מהו פסקה
מה העץ מחזיק בבעלותו, ולמה הבעלות קפדנית
ה-DOM אוכף בעלות יחידה בכל רמה: המסמך הוא הבעלים של סעיפיו, סעיף הוא הבעלים של גופו, כותרתו ותחתיתו, וערימות, מכלים וטבלאות הן הבעלות של ילדיהן. שימוש חוזר מתרחש דרך Clone או דרך מפעל רשום, אף פעם לא על ידי צירוף אותו אובייקט לשני הורים. כלל זה אינו טקס. רכיב שמופיע פעמיים בעץ ימדד פעמיים עם אילוצים שונים וישוחרר פעמיים בפירוק
ההשלכה המעשית עבור קוד קורא היא שפונקציות עזר מחזירות מופעים חדשים. רישום מפעל עם RegisterComponent וקריאה ל-CreateComponent נותנים מתכון בעל שם המפיק רכיב טרי בכל פעם, שכך ריהוט חוזר כגון בלוק חתימה או תחתית משפטית שייך לעץ
uses
HPDFDoc, HPDFLayoutDOM;
var
Doc: THPDFDOMDocument;
Section: THPDFDOMSection;
Table: THPDFDOMTable;
Row: THPDFDOMTableRow;
I: Integer;
begin
Doc := THPDFDOMDocument.Create;
Doc.GenerateStructure := True; // פליטת עץ המבנה של PDF/UA
Doc.Language := 'en-US';
Section := Doc.AddSection;
Section.PageWidth := 595; // A4 בנקודות
Section.PageHeight := 842;
Section.MarginLeft := 56;
Section.MarginTop := 56;
Section.MarginRight := 56;
Section.MarginBottom := 56;
Section.Style.FontName := 'Helvetica';
Section.Style.FontSize := 10;
Section.Body.AddHeading('Annual maintenance report', 1);
Section.Body.AddText('Every asset inspected during the reporting ' +
'period is listed below, grouped by site.');
Section.Body.AddSpacer(12);
Table := THPDFDOMTable.Create('assets');
Table.AddColumn(3); // משקלים, לא רוחבים מוחלטים
Table.AddColumn(1);
Table.AddColumn(1);
Table.RepeatHeaders := True;
Row := Table.AddRow(18, True); // שורת כותרת
Row[0].Text := 'Asset';
Row[1].Text := 'Last service';
Row[2].Text := 'Status';
for I := 0 to High(Assets) do
begin
Row := Table.AddRow(16);
Row[0].Text := Assets[I].Name;
Row[1].Text := Assets[I].ServiceDate;
Row[2].Text := Assets[I].Status;
end;
Section.Body.Add(Table);
end;
איך חלוקה לעמודים נמנעת מעלות ריבועית?
הדרך הנאיבית לחלק עץ לעמודים היא לשכפל את מה שלא נכנס ולהעביר אותו לעמוד הבא. בטבלה עם עשרת אלפים שורות, זה משכפל את השורות הנותרות פעם אחת לכל עמוד והופך מסמך ליניארי לריבועי
HotPDF מפצלת בצורה צרה במקום זאת. המציג ברמה העליונה עובר על ילדי הגוף לפי אינדקס ולעולם לא משכפל סעיף שלם או גוף שלם. רק ערימות ומכלים מקוננים שבאמת חוצים גבול עמוד מקבלים את תת-העץ המושפע שלהם משוכפל, ושני סוגי העלים הכבדים נושאים סמן במקום עותק: המשך טקסט שומר את טווח התווים המקורי שהוא עדיין חייב, ומשך טבלה שומר את פלח השורות שעדיין לא הוצב. מסמכים ארוכים נשארים ליניאריים, ופסקאות ארוכות עולות אותו הדבר בין אם הן נשברות פעם אחת או חמש פעמים
המדידה נשארת כנה לגבי תופעות לוואי. THPDFLayoutElement.Measure חייבת להיות נטולת תופעות לוואי של ציור, וההצבה בפועל תמיד רצה דרך THotPDF.PlaceLayoutElement, אותה שגרה מרכזית שמודדת מחדש את הקטע שהוצב, מקימה בעלות על גלישה ורושמת אבחון. מציג ה-DOM מחליט רק על מדיניות עמוד חדש, ריהוט עמוד, ריווח ואורך החיים של המשכים
כללי כותרת הטבלה שמונעים מסמך אינסופי
חזרה על כותרות טבלה על פני עמודים נשמעת פשוט ומסתירה שני מצבי כשל. HotPDF דורשת שכותרות שורה יופיעו רק ברצף הראשון של שורות רצופות, ושהפיצול הראשון יכיל את כל שורות הכותרת בתוספת שורת גוף אחת לפחות. בלי הכלל השני, כותרת גבוהה יותר מהשטח הנותר מייצרת עמוד שמכיל רק את הכותרת, ואחריו עמוד זהה נוסף, לנצח
עמודי המשך מציירים מחדש את הכותרת, ואותו עותק מצויר-מחדש מסומן כפריט מלאכותי ולא כתוכן, שזוהי התשובה הנכונה גם עבור נגישות וגם עבור חילוץ טקסט. שורת הכותרת המקורית נשארת במבנה הטבלה הלוגי בדיוק פעם אחת. דלגו על כך וקורא מסך יכריז על כותרות העמודות שוב באמצע הנתונים, וכלי חילוץ טקסט יכניס שורת כותרת כפולה בין שורות הגוף
יש גם תקרה הגנתית על עומק ההמשך, מכיוון שרכיב מותאם אישית חופשי לממש Split באופן שתמיד מחזיר זנב שקול. המציג בודק את המגבלה אחרי ניתוק הזנב ולפני התחלת העמוד הבא, והאיטרציה הנוכחית משחררת את הזנב בבלוק ה-finally שלה, כך שרכיב צד-שלישי בעייתי נכשל עם שגיאה הניתנת לאבחון במקום למלא דיסק
רכיב לוגי אחד, קטעי עמוד רבים
תיוג אוטומטי הוא המקום שבו מודל חלוקת העמודים ומודל המבנה חייבים להסכים. פסקה המפוצלת על פני שני עמודים היא פסקה לוגית אחת, כך שהיא חייבת להישאר רכיב מבנה אחד. אבל מזהי תוכן מסומן הם לכל עמוד, כך שכל קטע גלוי זקוק ל-MCID משלו בעמוד שבו הוא מופיע
HotPDF פותרת זאת על ידי שמירה על רכיב מבנה יחיד וצירוף הפניית תוכן מסומן למערך ה-/K שלו עבור כל קטע, כאשר זוג ה-/Pg וה-/MCID מזהה את העמוד ואת המזהה. המקום ב-ParentTree עבור אותו MCID מצביע חזרה על אותו רכיב. זה בדיוק מה ש-ISO 14289 מצפה, וזאת הסיבה ששכפולי המשך נבדלים משכפולים רגילים: Clone רגיל משמעו תוכן לוגי חדש ומקבל זהות סמנטית חדשה, בעוד ששכפול המשך הפנימי יורש את הזהות של הרכיב שהוא ממשיך
שימוש חוזר ברכיבים מתבצע דרך אינדקס של זהויות סמנטיות ממוין לפי מצביע רכיב ומחופש בהשוואה בינארית, שכך החיפוש נשאר לוגריתמי על עצים גדולים. האינדקס מחזיק הפניות שאינן-בעלות בלבד; אורך החיים של אובייקטי המבנה עצמם נשאר עם גרף אובייקטי ה-PDF
כללי מבנה שהמציג אוכף מראש
עם GenerateStructure מופעל, כמה כללי PDF/UA נבדקים בזמן שהעץ מוצג במקום אחרי שהקובץ כבר קיים. כותרות מתחילות ברמה 1 ואסור שידלגו רמות. LI יכולה להופיע רק בתוך L, ו-Lbl ו-LBody רק בתוך LI. TR שייכת לטבלה, ו-TH ו-TD לשורה. איור ללא טקסט חלופי נדחה במצב PDF/UA
דחייה מוקדמת היא הבחירה המכוונת כאן. מאמת שמדווח על טקסט חלופי חסר אחרי שהמסמך כבר נכתב אומר לכם שאצווה של עשרת אלפים דוחות צריכה להיווצר מחדש; מציג שדוחה את עץ הרכיב אומר לכם איזה רכיב, בעוד הנתונים שיצרו אותו עדיין בהיקף. אימות תאימות עדיין שייך לצינור העבודה כשלב נפרד, והמנגנונים של זה מכוסים באימות PDF/A, PDF/X ו-PDF/UA
var
Pdf: THotPDF;
Renderer: THPDFDOMRenderer;
Stats: THPDFDOMRenderStatistics;
begin
Pdf := THotPDF.Create(nil);
Renderer := THPDFDOMRenderer.Create;
try
Pdf.FileName := 'maintenance-report.pdf';
Pdf.BeginDoc;
Stats := Renderer.Render(Doc, Pdf);
Pdf.EndDoc;
Writeln(Format('%d page(s), %d placement(s), %d split(s)',
[Stats.PageCount, Stats.PlacementCount, Stats.SplitCount]));
Writeln(Format('structure elements=%d marked content=%d artifacts=%d',
[Stats.StructureElementCount, Stats.MarkedContentCount,
Stats.ArtifactCount]));
Writeln(Format('deepest continuation chain: %d',
[Stats.MaximumContinuationDepth]));
finally
Renderer.Free;
Doc.Free;
Pdf.Free;
end;
end;
רשומת הסטטיסטיקה שימושית יותר ממה שהיא נראית במבט ראשון. עלייה חדה ב-SplitCount אחרי שינוי תבנית בדרך כלל אומרת שרכיב התחיל להימדד גבוה יותר מהמכל שלו. MaximumContinuationDepth שמטפס בהדרגה הוא האזהרה המוקדמת לרכיב שה-Split שלו מתקדם מעט מדי לכל עמוד. והשוואת ArtifactCount למספר עמודי ההמשך מאשרת שכותרות חוזרות אכן תויגו כפריטים מלאכותיים
איפה ה-DOM משתלב לצד ה-API הישיר
ה-DOM לא מחליף ציור ישיר; הוא יושב מעל אותם אובייקטי עמוד. כל מה שהמציג מציב יכול להשתלב עם קריאות ישירות על THotPDF, מה שחשוב כשדוח צריך רכיב אחד ממוקם ידנית כגון תמונת חתימה במיקום מדויק. סגירת עמודים נשארת תחת שליטת AddPage ו-EndDoc, כך שמצב ההזרמה המיידית לא מחזיק עמודים גמורים בזיכרון וזיכרון תושב נשלט על ידי ההמשכים הנוכחיים, משאבי הגופנים וגרף אובייקטי המסמך הרגיל
בחרו ב-DOM כאשר התוכן מונע נתונים והפריסה מונעת כללים, ושמרו על ציור ישיר עבור אמנות קבועה. אם הכאב הנוכחי שלכם הוא ספציפית חלוקת טבלאות לעמודים, שווה לקרוא קודם על הגישה הצרה יותר ביצירת טבלאות ב-PDF, והתנהגות ברמת טקסט כגון יישור מתוארת ביישור טקסט
פריסה דקלרטיבית, תיוג אוטומטי וה-API לציור ישיר משוגרים באותו רכיב לדלפי ו-C++Builder; רשימת התכונות המלאה נמצאת בעמוד רכיב HotPDF Delphi PDF