HotPDF điền biểu mẫu XFA động trong Delphi thông qua TXFAWidgetRuntime, một tầng widget trung lập với host coi mỗi lần chỉnh sửa trường là một giao dịch: snapshot, validate, calculate, reflow, rồi publish hoặc rollback toàn bộ. Nó chạy đơn luồng bên trong host VCL hoặc FMX của bạn, không cần cài Acrobat, và kiểm mọi giới hạn trước khi cấp phát bất cứ thứ gì
Tình huống này quen thuộc với bất kỳ ai từng đưa phần mềm tài liệu vào khối chính phủ hoặc bảo hiểm. Một tờ khai bồi thường hay tờ khai thuế đến dưới dạng PDF mà nội dung trang chỉ là một thông báo "Please wait... if this message is not eventually replaced", còn mọi trường thật nằm trong packet XFA mà chỉ Adobe Acrobat mới render được. Người dùng của bạn muốn điền nó ngay trong ứng dụng của bạn. Bạn cũng không thể lách bằng cách rasterise, vì biểu mẫu mọc thêm hàng khi dữ liệu được nhập, và bố cục sau hàng thứ ba không còn là bố cục đi kèm tệp ban đầu
Vì sao XFA động vẫn là bài toán đáng giải
XFA động vẫn tồn tại vì các biểu mẫu đã triển khai sống lâu hơn định dạng mang chúng. ISO 32000-1 §12.7.8 mô tả XFA là một entry /XFA trên dictionary AcroForm giữ một stream packet XDP, và ISO 32000-2 đã deprecate toàn bộ cơ chế này; việc deprecate đưa nó ra khỏi lộ trình, không phải ra khỏi thực địa, và các biểu mẫu soạn theo đặc tả XFA 3.3 vẫn được phát hành và vẫn có giá trị pháp lý. XFA tĩnh có thể rút gọn thành các widget annotation thường, và HotPDF làm điều đó khi bạn gọi ApplyXFAAsAcroForm, với các đánh đổi được trình bày trong làm phẳng biểu mẫu XFA thành trường AcroForm. XFA động là một chuyện khác: các dải occur, văn bản có thể lớn dần và script calculate khiến tập trường trở thành hàm của dữ liệu, nên không có danh sách annotation cố định nào để làm phẳng vào cho đến khi người dùng gõ xong. Đó chính là khoảng trống TXFAWidgetRuntime lấp đầy, bằng cách giữ DOM XFA luôn sống, tính lại bố cục sau mỗi lần chỉnh sửa được chấp nhận, và trao cho host một mảng phẳng các widget đã định vị để vẽ và hit-test
Runtime trao gì cho ứng dụng host?
Nó trao cho bạn hình học và trạng thái, và không gì hàm ý một bộ UI toolkit cụ thể. TXFAWidgetRuntime để lộ WidgetCount và Widgets[I] dưới dạng record TXFAWidgetState mang ID, Name, Kind, PageIndex, Bounds theo điểm PDF, Value, EditValue cùng các cờ Focused, Editing, ReadOnly, Valid, còn việc vẽ, vẽ con trỏ soạn thảo và định tuyến bàn phím nằm trong code của bạn. Danh tính widget ổn định và theo thứ tự: mỗi widget nhận một ID dạng name[n], trong đó n đếm các lần xuất hiện trước của tên trường đó theo thứ tự bố cục, nên hàng thứ hai của một subform lặp là amount[1]. Danh tính đó là thứ sống sót qua mỗi lần rebuild, và cũng là thứ FocusWidget, BeginEdit, DispatchEvent và HitTest đều dùng để nói chuyện. Với tài liệu đang mở trong một instance THotPDF, CreateLoadedXFAWidgetRuntime trích các packet XDP, lấy page box đầu tiên làm kích thước trang bố cục, và trả về nil khi tệp hoàn toàn không có 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 khi không có /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)]));
// hit test trong không gian trang, widget trên cùng thắng
if Runtime.HitTest(0, 120.0, 96.0, WidgetID) then
Runtime.BeginEdit(WidgetID);
finally
Runtime.Free;
end;
finally
Pdf.Free;
end;
end;
Cái gì phải là nguyên tử khi một trường được commit?
Mọi thứ mà lần chỉnh sửa chạm tới, tức nhiều hơn hẳn giá trị trường. CommitEdit gọi CaptureSnapshot trước khi ghi bất cứ gì, và snapshot đó bao gồm bốn thứ: DOM XFA đã tuần tự hoá từ TXFADocument.SaveToBytes, toàn bộ mảng record tương tác TXFAWidgetState, các bộ đếm LastCalculationPasses và LastReflowPasses, cùng Warnings.Count hiện tại. Chỉ lưu giá trị node là lối tắt hấp dẫn nhưng sai, vì một script calculate hoặc một binding chưa phân giải có thể gọi EnsureValueNode và hiện thực hoá các node dữ liệu vốn không tồn tại khi lần chỉnh sửa bắt đầu; một phục hồi chỉ theo giá trị không có cách nào xoá chúng đi, nên một lần chỉnh sửa bị từ chối sẽ để lại phần dư cấu trúc vĩnh viễn trong packet datasets. Trình tự commit vốn rất nghiêm ngặt — ghi giá trị ứng viên, chạy validate cho trường vừa sửa, chạy calculate đến điểm cố định, rồi reflow đến khi bố cục ổn định — và mọi thất bại ở bất kỳ giai đoạn nào đều đi qua FailAndRestore, hàm này nạp lại các byte snapshot vào một TXFADocument mới, dựng lại danh sách widget, áp lại các trạng thái tương tác đã ghi, đặt lại bộ đếm và cắt Warnings về đúng độ dài lúc snapshot. LastDiagnostic giữ nguyên nhân khi thất bại, và giữ literal XFA transaction rollback failed trong trường hợp bệnh lý mà chính việc phục hồi lại raise
function EditAmount(Runtime: TXFAWidgetRuntime;
const AWidgetID: AnsiString; const AText: UnicodeString): Boolean;
var
Current: UnicodeString;
begin
Result := False;
if not Runtime.BeginEdit(AWidgetID) then
Exit; // chỉ đọc, hoặc không có widget đó
Current := Runtime.Widgets[Runtime.FocusedIndex].EditValue;
if not Runtime.ReplaceSelection(0, Length(Current), AText) then
begin
Runtime.CancelEdit; // range sai, hoặc surrogate bị cắt đôi
Exit;
end;
Result := Runtime.CommitEdit; // tất cả hoặc không có gì
if not Result then
// tài liệu, widget, bộ đếm và cảnh báo đã trở về
// trạng thái trước chỉnh sửa; widget đang focus chỉ bị đánh dấu invalid
ShowMessage(Runtime.LastDiagnostic);
end;
ReplaceSelection xứng đáng có một đoạn riêng, vì đây là chỗ rẻ nhất để loại bỏ đầu vào dị dạng. Nó từ chối một vùng chọn cắt đôi một cặp surrogate UTF-16, từ chối văn bản thay thế chứa surrogate cao hoặc thấp không ghép cặp, và từ chối mọi kết quả dài hơn MaxValueChars. Bắt lỗi này ở tầng phím nghĩa là cỗ máy giao dịch không bao giờ phải cuộn ngược một ký tự astral-plane viết dở
Rebuild vào một danh sách riêng, publish bằng một phép swap
Một lần rebuild widget không bao giờ được phép để người quan sát thấy nửa chừng, nên RebuildWidgets dựng một TObjectList sở hữu hoàn toàn tách biệt rồi swap nó vào chỗ bằng đúng một phép gán ở cuối. Lý do không phải để đẹp: TXFALayoutEngine.ComputeLayout chạy trong lúc rebuild đang diễn ra và gọi ngược vào code host qua hàm MeasureText bạn cung cấp, và nó có thể raise EXFAWidgetRuntimeError khi chạm giới hạn widget. Nếu runtime biến đổi danh sách sống tại chỗ, cả hai đường đều có thể để host giữ một danh sách nửa cũ nửa mới, với các con trỏ DataNode trỏ vào một tài liệu sắp bị rollback. Độ hội tụ của reflow sau đó do LayoutSignature quyết định, một chuỗi dựng từ số widget cộng với mọi ID, chỉ số trang và bounding box làm tròn bốn chữ số thập phân: CommitEdit rebuild, so chữ ký, và lặp lại cho đến khi hai chữ ký liên tiếp khớp nhau hoặc ngân sách lượt cạn. Khi chữ ký chẳng hề thay đổi, LastReflowPasses giữ nguyên 0, đó là cách bạn phân biệt một lần sửa chỉ tác động giá trị với một lần thực sự làm biểu mẫu lớn lên, và trạng thái tương tác được chuyển qua mỗi lần rebuild theo widget ID, nên focus và việc đang gõ dở sống sót qua một lần chèn hàng
Vì sao một trường có binding lại đọc nhầm bản ghi?
Vì script chạy mà không có ngữ cảnh dữ liệu. Một trường mang khai báo tường minh <bind match="dataRef" ref="$record.actual"/> và một trường được đặt tên theo đúng node dữ liệu đó là hai widget khác nhau trỏ vào một giá trị, còn một subform lặp với <occur max="2"/> sinh ra vài widget chung tên và chỉ khác nhau ở thuộc hàng dữ liệu nào; đánh giá validation và calculation từ gốc tài liệu thì tất cả chúng đều phân giải this thành node khớp đầu tiên trong toàn bộ packet datasets, nên hàng hai lặng lẽ validate hộ hàng một. HotPDF né điều đó bằng cách lưu DataNode đã phân giải trên từng entry widget khi bố cục sinh ra nó, rồi luồn node đó qua cả hai lời gọi HPDFXFAEvaluateFieldScript, cho xfskValidate lẫn xfskCalculate. Chính ngữ cảnh đó quyết định EnsureValueNode tạo node dựa vào đâu khi một phép tính nhắm vào một binding chưa tồn tại, và khi không phân giải được binding nào thì commit thất bại sạch sẽ với XFA calculation target is not bound thay vì ghi vào hàng nhầm. Ngữ nghĩa FormCalc đằng sau các script đó vang vọng điều mà tài liệu AcroForm nhận được từ các action được mô tả trong định dạng AcroForm và script calculate, nhưng luật phân giải ở đây là phạm vi XFA chứ không phải phạm vi tên trường
Ngân sách được kiểm tra trước tác dụng phụ, không phải sau
Mọi giới hạn trong runtime đều là điều kiện tiên quyết, vì một ngân sách được thi hành sau khi việc cấp phát đã xảy ra thì không còn là ngân sách. TXFAWidgetRuntimeOptions.Default giao MaxWidgets ở 10000, MaxValueChars ở 1048576, MaxCalculationPasses ở 16 và MaxReflowPasses ở 4, còn TXFAFormScriptOptions mặc định mang MaxOperations ở 100000 với MaxElapsedMilliseconds ở 500. Bên dưới, DOM XFA áp TXFADOMLimits của riêng nó: trần 128 MB cho đầu vào và đầu ra đã giải nén, nhiều nhất 1024 packet được khâu lại với nhau, 1000000 node, và độ sâu lồng nhau 256. Hai chi tiết quan trọng hơn chính các con số. Thứ nhất, ngân sách script áp cho cả giao dịch chứ không theo từng script: CommitEdit gieo một bộ đếm số thao tác còn lại duy nhất và một deadline monotonic, và mỗi lời gọi validate lẫn calculate đều rút từ cùng bộ đếm đó và chỉ nhận được số mili giây còn lại, nên một biểu mẫu với hai trăm trường tính toán không thể tiêu trọn 500 ms hai trăm lần. Thứ hai, deadline đến từ một hàm MonotonicMilliseconds có thể tiêm vào, và đó là thứ làm cho hành vi thời gian trở nên tái lập được trong bộ kiểm thử thay vì tung đồng xu trên một build agent đang bận
var
Options: TXFAWidgetRuntimeOptions;
Runtime: TXFAWidgetRuntime;
begin
Options := TXFAWidgetRuntimeOptions.Default;
Options.MaxWidgets := 2000; // mặc định 10000
Options.MaxCalculationPasses := 8; // mặc định 16
Options.MaxReflowPasses := 2; // mặc định 4
Options.ScriptOptions.Limits.MaxOperations := 20000; // cả giao dịch
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; // chỉ phát khi reflow thực sự dịch chuyển widget
end;
// ... điều khiển biểu mẫu ...
finally
Runtime.Free;
end;
end;
Runtime dừng ở đâu, và vì sao nó nói thẳng điều đó
Runtime cố tình không phải một engine script XFA tổng quát. DispatchEvent xử lý nguyên bản các activity enter và exit bằng cách chuyển focus, còn với mọi activity khác có kèm script nó từ chối bằng một diagnostic cụ thể, ổn định thay vì giả vờ: script nhắc đến addInstance, removeInstance hoặc instanceManager trả về XFA runtime does not support event-driven instance mutation, script đụng vào .presence trả về thông điệp presence tương ứng, và mọi thứ khác trả về XFA runtime does not support this event script. Một lời từ chối đoán được mà bạn có thể rẽ nhánh tốt hơn một bản mô phỏng cục bộ chạy đúng trên tệp mẫu của bạn và lệch đi trên tệp của khách hàng
Mô hình luồng cũng thẳng thắn không kém: một instance runtime thuộc về một luồng, không có khoá nội bộ, vì engine bố cục gọi ngược vào các callback đo đạc của host và một khoá quanh chỗ đó là một deadlock đang chờ repaint. Nội dung rich text bên trong trường đi theo đường lối bảo thủ giống như những chỗ khác trong thư viện, nơi các payload exData được xử lý như mô tả trong rich text và siêu liên kết exData của XFA, còn widget chữ ký và nút bấm trả về dưới dạng ReadOnly trong khi các loại UI không được hỗ trợ lộ diện dưới dạng xwkUnsupported thay vì một ô văn bản có thể sửa mà lặng lẽ mất dữ liệu
Gộp lại, đó là một câu trả lời khả dụng cho XFA động trong Delphi: giữ DOM sống, biến mỗi lần chỉnh sửa thành một giao dịch hoặc hạ cánh trọn vẹn hoặc không để lại gì, giới hạn mọi lượt, và nói rõ những gì nằm ngoài phạm vi. Nếu bạn đang cân nhắc nó cho quy trình bồi thường, thuế hay trợ cấp, runtime XFA được giao kèm trong HotPDF Delphi PDF component, cùng các đường AcroForm, làm phẳng và render mà những dự án đó thường cuối cùng cần đến cùng nhau