HotPDF ממלא טפסי XFA דינמיים ב־Delphi דרך TXFAWidgetRuntime, שכבת ווידג'טים נייטרלית למארח שמתייחסת לכל עריכת שדה כטרנזקציה אחת: צילום מצב, אימות, חישוב, סידור מחדש, ואז פרסום או החזרה שלמה. היא רצה בתהליכון יחיד בתוך המארח מסוג VCL או FMX שלכם, לא דורשת Acrobat מותקן ואוכפת כל תקציב לפני שהיא מקצה משהו
התרחיש מוכר לכל מי ששילח תוכנת מסמכים לעבודה ממשלתית או ביטוחית. טופס תביעה או הצהרת מס מגיע כ־PDF שתוכן העמוד שלו הוא הודעת "Please wait... if this message is not eventually replaced" אחת, וכל שדה אמיתי חי בחבילת XFA שרק Adobe Acrobat מעבד. המשתמשים שלכם רוצים למלא אותו בתוך היישום שלכם. גם ריסוטר אינו פותר, כי הטופס מוסיף שורות תוך כדי הזנת נתונים, והפריסה אחרי השורה השלישית אינה הפריסה שיצאה מהקובץ
למה XFA דינמי עדיין בעיה ששווה לפתור
XFA דינמי שורד כי טפסים שנפרסו מאריכים ימים מהפורמט שנשא אותם. ISO 32000-1 §12.7.8 מתאר XFA כרשומת /XFA במילון ה־AcroForm המחזיקה זרם חבילת XDP, ו־ISO 32000-2 פוסל את כל המנגנון; הפסילה הוציאה אותו ממפת הדרכים, לא מהשטח, וטפסים שנכתבו לפי מפרט XFA 3.3 עדיין מופקים ועדיין מחייבים משפטית. XFA סטטי אפשר לצמצם להערות ווידג'ט רגילות, ו־HotPDF עושה זאת כשקוראים ל־ApplyXFAAsAcroForm, עם הפשרות שנסקרו במאמר על שיטוח טפסי XFA לשדות AcroForm. XFA דינמי הוא חיה אחרת: טווחי ה־occur, הטקסט הגדל וסקריפטי ה־calculate הופכים את קבוצת השדות לפונקציה של הנתונים, כך שאין רשימת הערות קבועה לשיטוח אליה עד שהמשתמש סיים להקליד. זה הפער ש־TXFAWidgetRuntime ממלא, כשהוא משאיר את ה־DOM של ה־XFA חי, מחשב מחדש את הפריסה אחרי כל עריכה מאושרת ונותן למארח שלכם מערך שטוח של ווידג'טים ממוקמים לציור ולבדיקת פגיעה
מה הריצה מוסרת ליישום מארח?
היא נותנת לכם גיאומטריה ומצב, ושום דבר שמניח ערכת כלי UI. TXFAWidgetRuntime חושף WidgetCount ו־Widgets[I] כרשומי TXFAWidgetState הנושאים ID, Name, Kind, PageIndex, Bounds בנקודות PDF, Value, EditValue ואת הדגלים Focused, Editing, ReadOnly, Valid, בעוד שהציור, ציור סמן ההקלדה וניתוב המקלדת נשארים בקוד שלכם. זהות הווידג'ט יציבה וסודרית: כל ווידג'ט מקבל ID מהצורה name[n], כאשר n סופר הופעות קודמות של אותו שם שדה בסדר הפריסה, ולכן השורה השנייה של תת־טופס חוזר היא amount[1]. הזהות הזאת היא מה ששורד בנייה מחדש, והיא מה ש־FocusWidget, BeginEdit, DispatchEvent ו־HitTest כולם מדברים. עבור מסמך שכבר פתוח במופע THotPDF, CreateLoadedXFAWidgetRuntime מחלץ את חבילות ה־XDP, לוקח את תיבת העמוד הראשונה כגודל עמוד הפריסה ומחזיר nil כשהקובץ אינו נושא XFA בכלל
var
Pdf: THotPDF;
Runtime: TXFAWidgetRuntime;
WidgetID: AnsiString;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.LoadFromFile('claim-dynamic.pdf');
Runtime := Pdf.CreateLoadedXFAWidgetRuntime; // nil כשאין /XFA
if Runtime = nil then
Exit;
try
for I := 0 to Runtime.WidgetCount - 1 do
Memo1.Lines.Add(Format('%s p%d [%.1f %.1f %.1f %.1f] = %s',
[string(Runtime.Widgets[I].ID), Runtime.Widgets[I].PageIndex,
Runtime.Widgets[I].Bounds.Left, Runtime.Widgets[I].Bounds.Top,
Runtime.Widgets[I].Bounds.Right, Runtime.Widgets[I].Bounds.Bottom,
string(Runtime.Widgets[I].Value)]));
// בדיקת פגיעה במרחב העמוד, הווידג'ט העליון מנצח
if Runtime.HitTest(0, 120.0, 96.0, WidgetID) then
Runtime.BeginEdit(WidgetID);
finally
Runtime.Free;
end;
finally
Pdf.Free;
end;
end;
מה חייב להיות אטומי כאשר שדה נאשר?
כל מה שהעריכה יכולה לגעת בו, וזה הרבה יותר מערך השדה. CommitEdit קורא ל־CaptureSnapshot לפני שהוא כותב משהו, והצילום הזה מכסה ארבעה דברים: את ה־DOM של ה־XFA המסודר מ־TXFADocument.SaveToBytes, את מערך רשומות האינטראקציה המלא של TXFAWidgetState, את מוני LastCalculationPasses ו־LastReflowPasses ואת ה־Warnings.Count הנוכחי. שמירת ערכי צמתים בלבד היא הקיצור המפתה והוא שגוי, כי סקריפט calculate או קשירה שלא הופענחה יכולים לקרוא ל־EnsureValueNode ולהגשים צמתי נתונים שלא היו קיימים כשהעריכה החלה; שחזור של ערכים בלבד אינו יכול להסיר אותם, ולכן עריכה שנדחתה הייתה משאירה שארית מבנית קבועה בחבילת ה־datasets. רצף האישור עצמו קפדני — כתיבת ערך המועמד, הרצת validate לשדה שנערך, הרצת calculate עד נקודה יציבה, ואז סידור מחדש עד שהפריסה מתייצבת — וכל כשל בכל שלב מופנה דרך FailAndRestore, שטוען את בייטי הצילום לתוך TXFADocument טרי, בונה מחדש את רשימת הווידג'טים, מיישם מחדש את מצבי האינטראקציה שנרשמו, מאפס את המונים ומקצץ את Warnings בחזרה לאורך הצילום. LastDiagnostic מחזיק את הסיבה בכשל, ומחזיק את הליטרלי XFA transaction rollback failed במקרה הפתולוגי שבו השחזור עצמו מעלה חריגה
function EditAmount(Runtime: TXFAWidgetRuntime;
const AWidgetID: AnsiString; const AText: UnicodeString): Boolean;
var
Current: UnicodeString;
begin
Result := False;
if not Runtime.BeginEdit(AWidgetID) then
Exit; // לקריאה בלבד, או אין ווידג'ט כזה
Current := Runtime.Widgets[Runtime.FocusedIndex].EditValue;
if not Runtime.ReplaceSelection(0, Length(Current), AText) then
begin
Runtime.CancelEdit; // טווח שגוי, או זוג סורוגט מפוצל
Exit;
end;
Result := Runtime.CommitEdit; // הכל או כלום
if not Result then
// המסמך, הווידג'טים, המונים והאזהרות כבר חזרו למצב שלפני
// העריכה; הווידג'ט הממוקד פשוט מסומן כפסול
ShowMessage(Runtime.LastDiagnostic);
end;
ReplaceSelection ראוי להערה משלו, כי שם הקלט הפגום הכי זול לדחות. הוא מסרב לבחירה שמפצלת זוג סורוגט UTF-16, מסרב לטקסט החלפה שמכיל סורוגט גבוה או נמוך שאינו מזווג, ומסרב לכל תוצאה ארוכה מ־MaxValueChars. לתפוס את זה בשכבת המקשים פירושו שמנגנון הטרנזקציות לעולם לא יצטרך לפרום חצי תו ממישור אחר שנכתב חלקית
בנייה מחדש לרשימה פרטית, פרסום בהחלפה אחת
בנייה מחדש של ווידג'טים אסור שתיראה חצי גמורה, ולכן RebuildWidgets בונה TObjectList נפרד לגמרי שבבעלותו ומחליף אותו למקום בהשמה אחת בסוף. הסיבה אינה אסתטיקה: TXFALayoutEngine.ComputeLayout רץ בזמן שהבנייה מחדש בעיצומה וקורא בחזרה לקוד מארח דרך פונקציית MeasureText שסיפקתם, והוא יכול להעלות EXFAWidgetRuntimeError כשמגבלת הווידג'טים נפגעת. אם הריצה הייתה משנה את הרשימה החיה במקום, כל אחד מהנתיבים היה משאיר את המארח מחזיק רשימה שחציה פריסה ישנה וחציה חדשה, עם מצביעי DataNode אל מסמך שעומד להתהפך. התכנסות הסידור מחדש נקבעת אז על ידי LayoutSignature, מחרוזת שנבנית ממספר הווידג'טים ועוד כל ID, אינדקס עמוד ותיבה תוחמת מעוגלים לארבע ספרות: CommitEdit בונה מחדש, משווה חתימות וחוזר עד ששתי חתימות עוקבות נכתבות זהות או שתקציב המעברים מתרוקן. כשהחתימה לא השתנתה כלל, LastReflowPasses נשאר 0, וזה איך מבדילים עריכת ערך בלבד מעריכה שהגדילה באמת את הטופס, ומצב האינטראקציה עובר בכל בנייה מחדש לפי ID של ווידג'ט, כך שהמיקוד והעריכה שבעיצומה שורדים הוספת שורה
למה שדה קשור יקרא רשומה שגויה?
כי הסקריפט רץ בלי הקשר נתונים. שדה שנושא <bind match="dataRef" ref="$record.actual"/> מפורש ושדה שנקרא על שם אותו צומת נתונים הם שני ווידג'טים שונים המצביעים על ערך אחד, ותת־טופס חוזר עם <occur max="2"/> מייצר כמה ווידג'טים שחולקים שם ונבדלים רק בשורת הנתונים שאליה הם שייכים; הערכת אימות וחישוב מול שורש המסמך גורמת לכל אחד מהם לפענח את this לצומת התואם הראשון בכל חבילת ה־datasets, ולכן שורה שנייה מאמתת בשקט את שורה אחת. HotPDF נמנע מזה כשהוא שומר את ה־DataNode המפוענח בכל רשומת ווידג'ט כשהפריסה מייצרת אותה, ואז משחל את הצומת הזה דרך שתי הקריאות של HPDFXFAEvaluateFieldScript, ל־xfskValidate ול־xfskCalculate כאחד. אותו הקשר קובע מול איזה צומת EnsureValueNode יוצר כשחישוב מטיל על קשירה שעדיין לא קיימת, וכשאין קשירה שאפשר לפענח האישור נכשל בנקיון עם XFA calculation target is not bound במקום לכתוב לשורה השגויה. סמנטיקת ה־FormCalc שמאחורי הסקריפטים האלה מהדהדת את מה שמסמכי AcroForm מקבלים מהפעולות שתוארו במאמר על סקריפטי format ו־calculate של AcroForm, אבל כללי הפענוח כאן מוגדרים בהיקף XFA ולא בהיקף שם שדה
התקציבים נבדקים לפני תופעות לוואי, ולא אחרי
כל מגבלה בריצה היא תנאי מקדים, כי תקציב שנאכף אחרי שההקצאה כבר קרתה אינו תקציב. TXFAWidgetRuntimeOptions.Default מגיע עם MaxWidgets של 10000, MaxValueChars של 1048576, MaxCalculationPasses של 16 ו־MaxReflowPasses של 4, וברירות המחדל של TXFAFormScriptOptions נושאות MaxOperations של 100000 עם MaxElapsedMilliseconds של 500. בתוך זה, ה־DOM של ה־XFA מיישם TXFADOMLimits משלו: תקרות של 128 MB על קלט ופלט מפורקים, 1024 חבילות לכל היותר שנתפרות יחד, 1000000 צמתים ועומק קינון של 256. שני פרטים חשובים יותר מהמספרים עצמם. ראשית, תקציבי הסקריפטים הם ברמת הטרנזקציה ולא לכל סקריפט: CommitEdit זורע מונה פעולות שנותרו יחיד ותאריך יעד מונוטוני אחד, וכל הפעלת validate ו־calculate שואבת מאותו מונה ומקבלת רק את המילישניות שנותרו, כך שטופס עם מאתיים שדות מחשבים לא יכול לבזבז את מלוא 500 ה־ms מאתיים פעמים. שנית, תאריך היעד מגיע מפונקציית MonotonicMilliseconds שניתנת להזרקה, וזה מה שהופך את התנהגות זמן החלפה לניתנת לשחזור בחבילת בדיקות במקום הטלת מטבע על סוכן בנייה עמוס
var
Options: TXFAWidgetRuntimeOptions;
Runtime: TXFAWidgetRuntime;
begin
Options := TXFAWidgetRuntimeOptions.Default;
Options.MaxWidgets := 2000; // ברירת מחדל 10000
Options.MaxCalculationPasses := 8; // ברירת מחדל 16
Options.MaxReflowPasses := 2; // ברירת מחדל 4
Options.ScriptOptions.Limits.MaxOperations := 20000; // כל הטרנזקציה
Options.ScriptOptions.Limits.MaxElapsedMilliseconds := 200;
Options.MeasureText :=
function(const AText: UnicodeString; const AFont: TXFAFontSpec;
AMaxWidth: Double): TXFATextExtent
begin
Result := MeasureWithHostCanvas(AText, AFont, AMaxWidth);
end;
Runtime := TXFAWidgetRuntime.Create(XDPBytes, 612, 792, Options);
try
Runtime.OnLayoutChanged :=
procedure
begin
RepaintAllPages; // נורה רק כשה־reflow באמת הזיז ווידג'טים
end;
// ... מנהלים את הטופס ...
finally
Runtime.Free;
end;
end;
איפה הריצה עוצרת, ולמה היא אומרת זאת בקול
הריצה אינה בכוונה מנוע סקריפטים כללי של XFA. DispatchEvent מטפל באופן טבעי בפעילויות enter ו־exit על ידי הזזת המיקוד, ולכל פעילות אחרת שנושאת סקריפט הוא מסרב עם אבחנה ספציפית ויציבה במקום להעמיד פנים: סקריפטים שמזכירים addInstance, removeInstance או instanceManager מחזירים XFA runtime does not support event-driven instance mutation, סקריפטים שנוגעים ב־.presence מחזירים את המקבילה של ה־presence, וכל השאר מחזירים XFA runtime does not support this event script. סירוב צפוי שאפשר להסתעף עליו עדיף על חיקוי חלקי שעובד על קובץ הדוגמה שלכם וסוטה אצל הלקוח
מודל התהליכונים צלול באותה מידה: מופע ריצה אחד שייך לתהליכון אחד, בלי נעילה פנימית, כי מנוע הפריסה חוזר לפונקציות מדידה של המארח ונעילה מסביבן היא מוות נעול שמחכה לריפוד. תוכן עשיר בתוך שדות הולך בשורה השמרנית של שאר הספרייה, שבה מטעני exData מטופלים כפי שתוארו במאמר על טקסט עשיר וקישורים ב־exData של XFA, ווידג'טים של חתימה וכפתור מוחזרים כ־ReadOnly בזמן שסוגי UI לא נתמכים עולים כ־xwkUnsupported ולא כתיבת טקסט לעריכה שמאבדת נתונים בשקט
ביחד, זו תשובה עובדת ל־XFA דינמי ב־Delphi: להשאיר את ה־DOM חי, להפוך כל עריכה לטרנזקציה שאו נוחתת כולה או לא משאירה דבר, לתחום כל מעבר ולהיות מפורשים לגבי מה שמחוץ להיקף. אם אתם מעריכים את זה לזרימת תביעות, מס או קצבאות, ריצת ה־XFA מגיעה כחלק מה־HotPDF Delphi PDF component, לצד נתיבי ה־AcroForm, השיטוח והרינדור שהפרויקטים האלה בדרך כלל מגלים שהם צריכים יחד