Technical Article

שיטוח קישורי טקסט עשיר של XFA לקישורי PDF ב-Delphi

ארכיטקטורת טפסי ה-XML, הלא היא XFA, מיושנת (deprecated). תקן ISO 32000-1 נושא אותה בסעיף 12.7 עם ההערה שהיא הוסרה מ-PDF 2.0, ומציגים מודרניים זונחים את מנועי ה-XFA שלהם בזה אחר זה. שום דבר מזה לא רוקן את הארכיונים. טפסי קליטה ממשלתיים, בקשות ביטוח והצהרות בנקאיות נכתבו כ-XFA במשך חלק ניכר משני עשורים, והקבצים הללו עדיין מגיעים לתיבות דואר נכנס ולצינורות מסמכים כיום. כאשר המציג שהיה רגיל לרנדר אותם מפסיק לעשות זאת, הטופס הופך לעמוד ריק עם מציין מיקום של "אנא פתח בקורא אחר". התיקון העמיד הוא לשטח את ה-XFA לתוכן PDF סטטי שכל קורא יכול לצבוע

החלק הקשה בשיטוח הזה אינו השדות. תיבות טקסט ותיבות סימון ממופות לרכיבי AcroForm (widgets) בצורה נקייה למדי. החלק הקשה הוא הטקסט העשיר ש-XFA שומר בתוך אלמנט ציור, בבלוק <exData contentType="text/html">. בלוק זה הוא תת-קבוצה של HTML עם עיצוב פנימי (inline styling) ולעיתים קרובות, עוגנים (anchors). הבאתו לעמוד פירושה שחזור הן של הטקסט המעוצב והן של הקישורים הפעילים, והקישורים הם המקום שבו רוב המימושים מוותרים בשקט

איך טקסט עשיר של XFA באמת נראה

גוף של exData הוא פרוסה קטנה של XHTML. פסקה היא <p>; קטע תווים מעוצב הוא <span> עם CSS פנימי משלו עבור משקל, נטייה, צבע וגודל; וקישור הוא <a href="..."> העוטף את הטקסט הגלוי שלו. שורה בודדת יכולה להחזיק מספר אלמנטי span ברצף, כל אחד עם עיצוב שונה, ואחד מהם יכול להיות עוגן. העיצוב אינו קישוט שניתן להשמיט. סעיף המרונדר באדום מודגש מכיוון שהוא אזהרה משפטית חייב להישאר מודגש ואדום לאחר השיטוח, אחרת המסמך המשוטח מציג מצג שווא של המקור

לכן מנוע השיטוח אינו יכול להתייחס לבלוק כמחרוזת אחת. הוא חייב לעבור על המבנה הפנימי, לפתור את הסגנון האפקטיבי של כל ריצה (run) על ידי שכבות של ה-CSS הפנימי של ה-span על גבי גופן הבסיס של אלמנט הציור, ולפרוס את הריצות זו אחר זו לאורך השורה. HotPDF ממדל כל אחד מהמקטעים הפרוסים הללו כרשומת TXFARichRun פנימית. הרשומה נושאת את טקסט הריצה, הסגנון הפתור שלה, התיבה שנמדדה שלה, ועבור עוגן, את ה-Href שאליו הוא מצביע

פריסת הריצות משמאל לימין

מיקום הוא השלב שבו טקסט עשיר מפסיק להיות בעיית פענוח והופך לבעיית סדר דפוס (typesetting). הריצות חולקות שורה, כך שכל ריצה מתחילה במקום שבו הקודמת הסתיימה. אין סימון (markup) המתעד את המיקומים הללו; יש למדוד אותם. שגרת LayoutRichText הפנימית של המנוע מודדת כל ריצה עם אותם מדדי גופן (font metrics) שיציירו אותה מאוחר יותר, ואז מגדירה את ההיסט האופקי של הריצה לסכום הרץ של כל רוחבי הריצות הקודמות. ריצה אחת מתחילה בראשית תיבת הציור, ריצה שנייה מתחילה ברוחב של ריצה אחת, ריצה שלישית ברוחב המשולב של השתיים הראשונות, וכן הלאה לאורך השורה

זו הסיבה שיישור גופן המדידה חשוב כל כך. מעבר הפריסה מודד התקדמויות (advances); מעבר רינדור נפרד מצייר גליפים. אם שני המעברים הללו אינם מסכימים לגבי הגופן, התיבות שמעבר הפריסה חישב לא ישבו מתחת לגליפים שהמרנדר מצייר. HotPDF שומר עליהם מתואמים על ידי מיפוי הסגנון הפתור של כל ריצה למפרט גופן, באמצעות עוזר ה-RunStyleToFontSpec הפנימי, התואם לברירות המחדל של המרנדר עצמו שהן Arial ב-10 נקודות. ההתקדמות שנמדדה והטקסט המצויר מסכימים אז, והתיבה המחושבת של הריצה מכסה באמת את התווים שהקורא רואה

// Conceptual shape of one laid-out run. The engine builds an array of these
// internally; you never construct them yourself, but the fields explain how a
// link's hit box is derived from measured geometry rather than from text.
type
  TRichRunInfo = record
    Dx, Dy : Double;       // top-left, relative to the draw-box origin
    W, H   : Double;       // measured run box (width from the layout pass)
    Text   : AnsiString;   // the run's visible characters
    Href   : AnsiString;   // URI target for an <a> run, '' otherwise
  end;

מריצת עוגן לאנוטציית קישור PDF

קישור ב-PDF גמור אינו חלק מתוכן העמוד. זהו אובייקט נפרד, אנוטציית קישור (Link annotation), המתוארת ב-ISO 32000-1 §12.5.6.5. לאנוטציה יש /Rect שמגדיר את המלבן הניתן ללחיצה בעמוד ופעולה שמופעלת כאשר לוחצים על המלבן. עבור קישור חיצוני הפעולה היא פעולת URI: ‏/S /URI עם כתובת היעד כמחרוזת ה-/URI שלה. הטקסט הגלוי מתחת הוא תוכן עמוד רגיל; האנוטציה היא האזור החם הבלתי נראה המונח מעליו

נתיב השיטוח עוקב בדיוק אחר מודל זה. כאשר ריצה נושאת Href,‏ HotPDF מצייר תחילה את הטקסט המעוצב, ואז בונה אנוטציית קישור מעל תיבת הריצה. נקודת הכניסה הציבורית לאנוטציה זו היא מתודת העמוד AddURILink, היוצרת את האובייקט /Type /Annot /Subtype /Link עם פעולת /URI ומחזירה את מילון האנוטציה. המלבן שלה הוא התיבה שנמדדה של הריצה, מתורגמת מהקואורדינטות המקומיות של אלמנט הציור לקואורדינטות עמוד. התוצאה היא קישור שנוחת בדיוק על טקסט העוגן ולא בשום מקום אחר

// The same public API the flatten path uses for each anchor run. It produces
// an ISO 32000-1 12.5.6.5 Link annotation: /Subtype /Link with a /URI action
// over the given rectangle. The optional description fills /Contents so a
// screen reader can announce the target.
var
  LinkRect: TRect;
  Annot: THPDFDictionaryObject;
begin
  LinkRect := Rect(72, 690, 268, 706);  // page-space hit box for the run
  Annot := Pdf.CurrentPage.AddURILink(LinkRect,
    'https://www.example.gov/appeal', 'File an appeal online');
end;

מדוע תיבת הלחיצה חייבת להגיע מרוחבים שנמדדו

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

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

הנעת השיטוח מהקוד שלך

עבור PDF שכבר מכיל חבילת XFA, נקודת הכניסה היא FlattenLoadedXFA. טען את המסמך, קרא למתודה ושמור את התוצאה. הפרמטר Editable קובע מה קורה לשדות הטופס: העבר True כדי לשמור עליהם כרכיבי AcroForm הניתנים למילוי, או False כדי לסמן כל רכיב כקריאה בלבד כך שהפלט יהיה רשומה קפואה. בלוקי הציור של טקסט עשיר, עם הריצות המעוצבות ואנוטציות הקישורים שלהם, מופקים בשני המקרים. הפונקציה מחזירה את ספירת הרכיבים שהיא פלטה

קרא תמיד את XFAFlattenWarnings לאחר הקריאה. הרשימה מנוקה בתחילת כל שיטוח ומצברת שורה עבור כל אלמנט שהמנוע סירב לרנדר: סוג שדה שאינו נתמך, תמונת ציור שלא פוענחה, בלוק exData ללא אלמנטי span שמישים. אף אחד מאלה אינו מעלה חריגה, כך שרשימת אזהרות ריקה היא ההוכחה שלך שהכל מופה, ורשימה שאינה ריקה מספרת לך בדיוק אילו מקורות לבדוק. כאשר אתה מחזיק ב-XFA הגולמי כבתיי XDP ולא כ-PDF טעון, מתודת האחות ApplyXFAAsAcroForm מקבלת את הבתים הללו ישירות וחולקת את אותו נתיב קוד ואותה התנהגות אזהרות. המתודה המשלימה AddXFAPacket הולכת בכיוון ההפוך, ומטמיעה חבילת XFA לתוך מסמך שאתה בונה

var
  Pdf: THotPDF;
  Emitted, i: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.LoadFromFile('xfa_appeal_form.pdf');
    // True keeps fields fillable; False freezes them read-only.
    Emitted := Pdf.FlattenLoadedXFA(True);

    // Anything the engine could not map is reported, not raised.
    for i := 0 to Pdf.XFAFlattenWarnings.Count - 1 do
      Writeln('XFA warning: ', Pdf.XFAFlattenWarnings[i]);

    Pdf.SaveLoadedDocument('appeal_form_flat.pdf');
    Writeln('Widgets emitted: ', Emitted);
  finally
    Pdf.Free;
  end;
end;

אימות התוצאה בקורא

פתח את הקובץ המשוטח ב-Acrobat, או בכל מציג נוכחי אחר, ובדוק שני דברים. ראשית, שהטקסט העשיר מרונדר כאשר העיצוב שלו שלם: הריצות המודגשות הן מודגשות, הריצות הצבעוניות נושאות את צבען, וה-spans יושבים בסדר הנכון בשורה במקום לחפוף או לחרוג מהתיבה. שנית, שהקישורים פעילים. רחף מעל עוגן ושורת המצב צריכה להציג את כתובת היעד; לחץ עליו ופעולת ה-URI צריכה לפתוח אותו. השתמש במפקח האנוטציות של המציג כדי לאשר שכל אחד מהם הוא אנוטציית /Link אמיתית שה-/Rect שלה מחבק את טקסט העוגן, ויושב מעל תוכן שהוא כעת גליפים מצוירים פשוטים ולא XFA מרונדר של טופס. השילוב הזה, טקסט סטטי מעוצב פלוס אנוטציות קישורים (Link annotations) אמיתיות במלבנים הנכונים, הוא מה שגורם למסמך המשוטח להאריך ימים מעבר למנועי ה-XFA שהוא אינו זקוק להם עוד

שיטוח השדות עצמם, תיבות הטקסט, תיבות הסימון ורשימות הבחירה המקיפות את הטקסט העשיר הזה, מכוסה במדריך שלנו על שיטוח טפסי XFA לרכיבי AcroForm. עבור הסיפור הרחב יותר של בנייה ומיקום של אנוטציות קישורים ביד, מעבר לאלו שנתיב השיטוח מייצר, ראה עבודה עם אנוטציות PDF ב-HotPDF. שניהם נבנים על אותו מנוע, הHotPDF Component עבור Delphi ו-C++Builder