מאמר טכני

בניית עצי מבנה מתויגים ל-PDF ב-Delphi עם PDFlibPas

PDF נגיש נשען על מבנה אחד שהדף הגלוי אינו מציג לעולם: עץ המבנה המוגדר ב-ISO 32000-1 §14.7. מדובר בהיררכיה לוגית של כותרות, פסקאות, טבלאות ואיורים, מרובדת מעל התוכן המצויר וממופה לתפקידים סטנדרטיים דרך מפת תפקידים. קורא מסך קורא את העץ הזה, לא את הסימנים על הדף. ללא עץ זה, חשבונית שנוצרה ונראית מושלמת ריקה מבחינה סמנטית, כי מספר התוכן מתעד סדר ציור ולא שום דבר אחר. הסכום יכול להיקרא לפני פריטי השורה, הכותרת התחתונה יכולה לחתוך פסקה, טבלת הפריטים יכולה להתמוטט לרצף אחד בלתי מובחן של מילים. עלות המניעה לטובתכם בצורה קיצונית. פליטת מבנה תוך כדי ציור היא דקות של קוד; הוספתו בדיעבד למסמכים גמורים היא פרויקט שיקום. losLab PDF Library (PDFlibPas) חושפת את העץ ל-Delphi ו-C++Builder דרך קבוצה קטנה של קריאות שעוטפות כל פעולת ציור בתפקיד הלוגי שלה

כיצד תוכן מסומן נקשר לעץ המבנה

שתי שכבות משתפות פעולה. בזרם התוכן, פעולות ציור מוקפות ברצפי marked-content, כל אחד נושא MCID שלם. בקטלוג המסמך, עץ המבנה ממפה את ה-MCIDs הללו להיררכיה של אלמנטים מטופוסים (H1, P, Table, Figure) עם תכונות כמו טקסט חלופי ושפה. סוגי אלמנטים מותאמים אישית הם חוקיים, אך כל אחד חייב לפתור לתפקיד סטנדרטי דרך מפת התפקידים (ISO 32000-1 §14.8.4). תוכן שאינו נושא שום משמעות כלל, כמו קווים, רקעים ורהיטי דף חוזרים, מסומן כ-artifact כדי שטכנולוגיה מסייעת תדלג עליו במקום לקרוא אותו באמצע משפט

PDFlibPas מנהלת את שתי השכבות מאחורי זוג סוגריים אחד. BeginTag פותח אלמנט מבנה ומתחיל את רצף ה-marked-content, קריאות ציור נוחתות בתוכו, ו-EndTag סוגר את שניהם. ניהול הספרים שמכשיל תיוג ידני, ה-MCIDs ועץ ההורים ורפרנסי הדפים, מתרחש פנימית שם שלא יכולים לשגות בו

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

תיוג תוך כדי ציור, לא לאחריו

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

var
  Lib: TPDFlib;
begin
  Lib := TPDFlib.Create;
  try
    Lib.SetOrigin(1);                          // top-left origin
    Lib.SetPDFUAMode('en-US');                 // bumps the save version to PDF 1.7
    Lib.SetInformation(1, 'Service Manual');   // /Title is mandatory for PDF/UA
    Lib.AddRoleMap('ManualTitle', 'H1');       // custom type -> standard role
    Lib.AddStandardFont(4);
    Lib.SetTextSize(18);
    Lib.BeginTagEx2('ManualTitle', '', '', 'en-US', '', 'h1-cover', '');
    Lib.DrawText(72, 96, 'Service Manual');
    Lib.EndTag;
    Lib.BeginTag('Figure', 'Exploded view of the gearbox assembly', '');
    Lib.AddImageFromFile('gearbox.png', 0);
    Lib.EndTag;
    Lib.BeginArtifact('Layout');               // page decoration: excluded from reading
    // ... draw rules and background tint ...
    Lib.EndArtifact;
    Lib.SaveToFile('manual.pdf');
  finally
    Lib.Free;
  end;
end;

שלוש קריאות ברצף הזה נושאות משקל תאימות. SetPDFUAMode מפעיל פלט PDF/UA ומגביה בשקט את גרסת המסמך ל-PDF 1.7, מה שמתנגש עם הצמדת גרסה. מסמך נעול ל-PDF 1.4 עם LockSaveVersion מסרב לשמור ומחזיר קוד שגיאה 602 ברגע שמצב UA פעיל, התנגשות שנוטה להופיע כאשר פרופילי ארכיון ודרישות נגישות מוגדרים על ידי צוותים שונים. SetInformation(1, ...) כותב את כותרת המסמך, שתקן ISO 14289 מצפה שצופים יציגו במקום שם הקובץ; היעדרה הוא אחד הממצאים הנפוצים ביותר של PDF/UA בשטח. AddRoleMap רושם את סוג ה-ManualTitle המותאם אישית כ-H1, ודילוג עליו משאיר את האבחון המתואר להלן מסמן תפקיד לא ממופה

רמות כותרות ראויות למדיניות מכוונת, לא לבחירות אד-הוק לפי מראה הדף. משתמשי קוראי מסך קופצים בין חלקים לפי קיצור מקלדת לכותרת, כך שתבנית שעוברת מ-H1 ל-H3 כי הרמה הביניים נראתה גדולה מדי בעיצוב הוויזואלי שוברת בשקט את הניווט הזה, ואף סקירה ויזואלית לא תתפוס זאת. זה בדיוק הפגם שהאבחון HEADING-LEVEL-SKIP קיים כדי לנקב בשמו. מפו את הסגנות הוויזואליים של כל תבנית לסולם כותרות קבוע, במקום אחד, ו-drift לעולם לא מתחיל

טבלאות שקורא מסך יכול לנווט בהן בפועל

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

Lib.BeginTag('Table', '', '');
Lib.BeginTag('TR', '', '');
Lib.BeginTagEx2('TH', '', '', '', '', 'col-part', '');
Lib.SetStructElemScope('Column');          // valid only while this TH is open
Lib.DrawText(72, 120, 'Part');
Lib.EndTag;
Lib.BeginTagEx2('TH', '', '', '', '', 'col-torque', '');
Lib.SetStructElemScope('Column');
Lib.SetStructElemColSpan(2);               // header spans the value and unit columns
Lib.DrawText(200, 120, 'Tightening torque');
Lib.EndTag;
Lib.EndTag;
Lib.BeginTag('TR', '', '');
Lib.BeginTag('TD', '', '');
Lib.SetStructElemHeaders('col-part');      // explicit binding for irregular tables
Lib.DrawText(72, 140, 'M8 flange bolt');
Lib.EndTag;
Lib.EndTag;
Lib.EndTag; // Table

כלל הסדר הוא נוקשה ומיושם בשקט. כל קריאת SetStructElem* חלה על התג שפתוח ברגע זה, בין ה-BeginTag שלו ל-EndTag שלו, והיא מחזירה 0 מבלי להעלות שום דבר כאשר שום תג אינו פתוח או שהתכונה אינה חלה על הנוכחי. קריאה שגויה פשוט נעלמת. עטיפת ערכי ההחזרה בהתקפות במהלך הפיתוח תופסת את ה-drift כאשר עדיין אפשר לראות אותו; אם משאירים לבד, scope חסר מופיע רק כאשר ביקורת נגישות מריצה קורא מסך אמיתי על הטבלה. מזהי האלמנטים שמועברים דרך BeginTagEx2 מזינים את עץ ה-ID (ISO 32000-1 §14.7.4), וזה מה שהופך את הקישור של SetStructElemHeaders לניתן לפתרון מלכתחילה

אותה משפחת תכונות מכסה את שאר מה שעליו נשענת טכנולוגיה מסייעת. SetStructElemListNumbering מצהיר כיצד פריטי רשימה ממוספרים, כך שקורא מסך מכריז על מיקום ברשימה במקום לשנן גלyphs של נקודות. SetStructElemBBox מתעד את תיבת התחום של איורים וטבלאות, שתצוגות reflow משתמשות בהן למיקום תוכן. SetStructElemActualText מספק טקסט חלופי לרצפים שה-glyphs שלהם אינם ממופים לתווים קריאים, כמו drop cap שנבנה מאמנות וקטורית. כל אחד מהם עוקב אחר אותו כלל: הוא נקשר לתג הפתוח, או שהוא נעלם

Artifacts, שפה ושער האבחון לפני שמירה

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

לפני שמירה, GetPDFUADiagnostics מריץ את בדיקות המבנה של הספרייה על המסמך שבזיכרון ומחזיר ממצאים כטקסט, כאשר מחרוזת ריקה פירושה שלא נמצא כלום. הקודים מנקבים ישירות בשם טעויות כתיבה קלאסיות: FIGURE-NO-ALT עבור תמונה ללא טקסט חלופי, HEADING-LEVEL-SKIP עבור H3 שעוקב אחרי H1, ROLEMAP-UNMAPPED עבור סוג מותאם אישית שלא נרשם מעולם. חיבורו לתהליך הבנייה (צור את ערכת המסמכים, כשל בשלב אם האבחון אינו ריק) הופך רגרסיות נגישות לכשלים מסוג compile-time במקום ממצאי ביקורת חודשים לאחר מכן. פסיקת התאימות המלאה עדיין שייכת לבדיקה מקדימה על הקובץ השמור, המכוסה בבדיקה מקדימה של PDF/A ו-PDF/UA ב-Delphi, כי חלק מנירמולים מיושמים רק במהלך סדרות

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

עצי מבנה אינם שורדים כל מיזוג

מסמכים מתויגים נשארים מתויגים רק כאשר כל שלב עיבוד מאוחר יותר שומר על העץ, והקצה החד בתוך PDFlibPas הוא משפחת merge-list. MergeFileListFast מוותר על שמירת עץ המבנה לטובת מהירות. זה המחיר הנכון עבור אצוות תמונות סרוקות והלא נכון עבור דוחות מתויגים, כי הפלט נפתח כרגיל, מרנדר בצורה זהה, ואיבד בשקט את שכבת הנגישות שלו. השתמשו ב-MergeFileList ברירת המחדל או בגרסה המחמירה בכל עת שכלשהו מהקלט מתויג, ועשו את IsTaggedPDF חלק מהטענות שלאחר ה-assembly כדי שאצווה מישורית לא תישלח מבלי שאחד ישים לב. לצינורות ה-assembly של ערכות מסמכים גדולות יש יותר פשרות מסוג זה, הנחקרות במיזוג, פיצול וגישה ישירה ל-PDF גדולים

לולאת האימות נסגרת מחוץ לספרייה: פתחו את הפלט ב-Acrobat, בדקו את לוח התגים, וקראו לפחות מסמך אחד לכל משפחת תבניות עם קורא מסך אמיתי. האבחון תופס טעויות מבניות; רק אוזן אנושית תופסת סדר קריאה שתקין מבחינה טכנית ומבלבל בפועל. גרסאות הערכה וסימוכין ה-API המלא לתיוג נמצאים בדף המוצר losLab PDF Library for Delphi