מאמר טכני

ביטויי נתיב XFA SOM ב-Delphi: נתוני טפסים דינמיים

ספריית losLab PDF Library קוראת וכותבת צומתי נתונים חוזרים בטפסי XFA דינמיים באמצעות ביטויי נתיב XFA 3.3 SOM: הפונקציות GetXFAFormFieldValue ו-SetXFAFormFieldValue מקבלות את השורשים $data, $record ו-!data, תו כללי למופעים [*], בוררי צאצאים ותווים כלליים לילדים, המאפיין parent, בוררי מחלקות #dataGroup/#dataValue ופרדיקטים פשוטים בסגנון FormCalc, כך שתוכנית Delphi יכולה לעדכן כל שורת חשבונית בטופס מס ממשלתי בקריאה אחת. מאמר זה מהווה את מדריך הכתובות; להעברת קבוצות נתונים שלמות בין קבצים, ראו את המאמר הנלווה על חילופי נתוני טפסים מסוג FDF, XFDF ו-XFA

הבעיה מופיעה ברגע שהטופס מפסיק להיות שטוח. טופס החזר מע"מ או הצהרת מכס שנבנה כטופס XFA דינמי אינו מכיל שנים עשר שדות בשמות מ-Total_Price_1 עד Total_Price_12; יש לו תת-טופס Detail אחד המוצהר פעם אחת ב-DOM של התבנית ומיוצג כמספר הפעמים שהנתונים דורשים. הערכים חיים בעץ שני, ה-DOM של הנתונים בתוך חבילת ה-datasets, שבה כל פריט שורה הוא אלמנט <Detail> חוזר תחת <Receipt>. שמות שדות שטוחים אינם יכולים להצביע על "המחיר בשורה השלישית" או "כל שורה מעל 200"; זהו בדיוק התפקיד שפרק Scripting Object Model של מפרט Adobe XFA 3.3 נותן לביטויי SOM, וזהו התחביר שממשקי ה-API של XFA field-value ב-losLab PDF Library מדברים

כיצד מנווטים לצומתי נתונים של XFA מ-Delphi?

כל נתיב datasets מתחיל משורש (root), וספריית losLab PDF Library מקבלת את צורות הכתיבה הסטנדרטיות ללא הבדל: $data הוא הקיצור ב-XFA 3.3 עבור xfa.datasets.data, !data הוא הקיצור המושרש ב-xfa.datasets, ובחבילות שאינן משתמשות בעיבוד רשומות, $record מפוענח לרשומת הנתונים החיצונית, האלמנט הראשון תחת צומת הנתונים. ממשקי API של צד התבנית כמו SetXFAFormFieldAccess מקבלים את $template ו-xfa.template באותו אופן. מקטעים עשויים להיות מופרדים בנקודה או בלוכסן ($data.Receipt.Tax או $data/Receipt/Tax), דבר שיש לו חשיבות מכיוון ש-GetXFAFormFieldNames מונה שדות כנתיבים מופרדים בלוכסנים המוזנים ישירות חזרה אל ממשקי ה-API של הערכים והתבנית. אינדקס המופעים מבוסס אפס לפי המפרט, כך ש-Detail[0] הוא השורה הראשונה. שני כללי מילוט שומרים על שמות חריגים כניתנים להפניה: בנתיב מופרד בנקודות, \. מייצג נקודה מילולית (Line\.Item), בעוד שנתיבים מופרדים בלוכסנים מתייחסים לנקודות כאל תווים רגילים. נתוני תעשייה הנושאים קידומת מרחב שמות XML משלהם מותאמים לפי שמם המקומי, כך ש-<m:Receipt> עדיין עונה ל-Receipt

var
  Lib: TPDFlib;
  Tax: WideString;
begin
  Lib := TPDFlib.Create;
  try
    Lib.LoadFromFile('vat-return.pdf', '');
    // Equivalent addresses for the same data node
    Tax := Lib.GetXFAFormFieldValue('Receipt.Tax');
    Tax := Lib.GetXFAFormFieldValue('$data.Receipt.Tax');
    Tax := Lib.GetXFAFormFieldValue('xfa.datasets.data.Receipt.Tax');
    Tax := Lib.GetXFAFormFieldValue('$record.Tax');
    // Slash form with a zero-based occurrence index
    Lib.SetXFAFormFieldValue('$record/Detail[0]/Total_Price', '251.00');
    Lib.SaveToFile('vat-return-updated.pdf');
  finally
    Lib.Free;
  end;
end;

כיצד ממלאים שורות חוזרות בטופס XFA תוכניתית?

תו הכללי למופעים [*] הוא כלי עיבוד האצוות. במקום שבו אינדקס מספרי בוחר אח בודד, [*] בוחר את כל האחים בעלי אותו שם, כך ש-$data.Receipt.Detail[*].Total_Price פונה לשדה המחיר בכל פריטי השורות בבת אחת. בקריאה, הפונקציה GetXFAFormFieldValue מחזירה את הערכים של כל הצמתים התואמים כשהם מחוברים באמצעות מפריד |; בכתיבה, SetXFAFormFieldValue מעדכנת כל צומת תואם באותו ערך ומחזירה 1 בהצלחה. נתיב שאינו תואם לשום דבר נקרא חזרה כמחרוזת ריקה, שהיא הדרך הזולה לבדוק אם ענף קיים לפני שכותבים לתוכו

var
  Lib: TPDFlib;
  Prices: WideString;
begin
  Lib := TPDFlib.Create;
  try
    Lib.LoadFromFile('invoice.pdf', '');
    // All Detail rows at once: '250.00|60.00'
    Prices := Lib.GetXFAFormFieldValue('$data.Receipt.Detail[*].Total_Price');
    // Reset one column across every repeated row in a single call
    Lib.SetXFAFormFieldValue('$data.Receipt.Detail[*].Surcharge', '0.00');
    Lib.SaveToFile('invoice-updated.pdf');
  finally
    Lib.Free;
  end;
end;

בוררי צאצאים, תווים כלליים לילדים ובוררי הורים

שלושה בוררים מבניים מכסים את המקרים שבהם אתם יודעים את שם השדה אך לא את העומק המדויק שלו. בורר הצאצאים .. מתאים לכל עומק מתחת לצומת הנוכחי: $data..Total_Price מוצא את ה-Total_Price הראשון בכל מקום תחת שורש הנתונים, וכתיבה דרך נתיב צאצא מעדכנת את הצומת התואם הראשון. שימו לב למגבלה: אינדקס מופעים אחרי התאמת צאצא אינו פוסע על פני ענפי אחים, כך שאם $data..Total_Price[0] מפוענח בתוך שורת ה-Detail הראשונה, $data..Total_Price[1] מחזיר ערך ריק במקום לקפוץ לשורה הבאה; השתמשו ב-Detail[*] כאשר אתם רוצים את כולם. התו הכללי לילדים .* מתאים לכל ילד ישיר ומאפשר למקטעים הנותרים לסנן: $data.Receipt.*.Total_Price מגיע ל-Total_Price בתוך כל ענף ילד של Receipt מבלי לבחור גם שדה סיכום בעל שם זהה היושב ישירות על Receipt עצמו. לבסוף, המאפיין parent עולה רמה אחת, דבר ש-XFA 3.3 מפריד במכוון מ-..: $data.Receipt.Detail[1].parent.Tax מתחיל בפריט השורה השני, עולה ל-Receipt ונוחת על ערך ה-Tax האח שלו

בוררי מחלקות ופרדיקטים: #dataGroup, #dataValue, .[expression]

תחביר ה-#class פונה לצומתי DOM של נתונים לפי מחלקת האובייקט במקום לפי שם, שהיא הדרך המעשית כאשר תגיות XML מכילות תווים שקשה לכתוב כשמות סקריפט. ספריית losLab PDF Library ממפה את #dataGroup לאלמנטים שיש להם ילדי אלמנט ואת #dataValue לאלמנטים מסוג עלה ללא ילדי אלמנט, ושניהם משתלבים עם מופעים מספריים או מופעי [*]: $data.Receipt.#dataGroup[0].#dataValue[0] בוחר את הערך הראשון בתוך הקבוצה הראשונה תחת Receipt. בורר הפרדיקטים .[expression] מסנן אחים בעלי אותו שם לפי תוכן. הפרדיקטים הנתמכים הם השוואות פשוטות של ערך ילד מול ערך מילולי, עם אופרטורים של יחס כגון >, < ו-<=; כאשר פרדיקט מתאים למספר שורות, קריאות מוחזרות כשהן מחוברות ב-| וכתיבות מעדכנות כל התאמה

// Flag every line whose Total_Price exceeds 200
Lib.SetXFAFormFieldValue(
  '$data.Receipt.Detail.[Total_Price > 200].Review_Flag', '1');

// Descriptions of all low-value lines, '|'-joined when several match
Desc := Lib.GetXFAFormFieldValue(
  '$data.Receipt.Detail.[Total_Price <= 200].Description');

// Class-based addressing when tag names resist script spelling
Total := Lib.GetXFAFormFieldValue(
  '$data.Receipt.#dataGroup[0].#dataValue[0]');

אילו תכונות של XFA SOM אינן נתמכות?

התמיכה ב-SOM ב-losLab PDF Library מוגבלת לנתיבי שדות datasets ותבניות, והקצוות הם מפורשים ולא מבוססי מאמץ מרבי. הכרתם מראש חוסכת איתור באגים של נתיב שמחזיר בשקט מחרוזת ריקה

  • פרדיקטים אינם מריצים קוד FormCalc או JavaScript שרירותי: ללא קריאות לפונקציות (פרדיקט contains(...) מחזיר ערך ריק), ללא ביטויים בוליאניים מורכבים, אלא רק השוואת child op literal יחידה
  • השורש $record עובד בחבילות שאינן משתמשות בעיבוד רשומות בלבד; דפדוף רשומות של dataWindow וסיבוב קבוצות רשומות אינם מיושמים
  • הפענוח הוא מול ה-DOM של הנתונים וה-DOM של התבנית ישירות; פענוח יחסי של Form DOM, התצוגה הממוזגת שמציג בונה בזמן ריצה, אינו מבוצע
  • סמנטיקת טעינת תכונות (attribute-loading) של Data DOM אינה מוחלת; הספרייה קוראת את חבילת ה-datasets כ-XML גולמי, כך שנתיבים פונים לאלמנטים ולא לתכונות שקודמו לצמתים

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

כתיבת החבילה כולה עם SetXFAFromString

הפונקציה SetXFAFromString היא נקודת כניסה זו ברמת החבילה: היא מתקינה מחרוזת XDP מלאה, תבנית ו-datasets יחד, ויוצרת את מכל ה-AcroForm במסמך חדש כאשר לא קיים כזה עדיין, והיא מקבלת חבילות מקודדות UTF-16 עם סימני סדר בייטים (BOM) וכן UTF-8. צורת ייצור נפוצה היא לשמור את ה-XDP שהונפק על ידי הסוכנות כקבוע, לטעון אותו עם SetXFAFromString, ואז להריץ קריאות SetXFAFormFieldValue המופנות ל-SOM עבור ערכי החשבוניות המשתנים לפני השמירה. מכיוון ש-XFA הוא אחד משני מודלי הטפסים שקובץ PDF יכול לשאת, לצד ה-AcroForm הקלאסי יש סיפור אוטומציה משלו, המכוסה במאמר על פעולות טפסים אינטראקטיביות ו-JavaScript

ממשקי ה-API של XFA field-value, פירוט וחבילות המוצגים כאן מגיעים ב-losLab PDF Library עבור Delphi, C# ו-VB.NET; דף המוצר נושא את המדריך המלא לטיפול בטפסים