Bài viết Kỹ thuật

Biểu thức đường dẫn XFA SOM trong Delphi: Dữ liệu Form động

losLab PDF Library đọc và ghi các node dữ liệu lặp lại trong các form XFA động thông qua biểu thức đường dẫn XFA 3.3 SOM: GetXFAFormFieldValueSetXFAFormFieldValue chấp nhận các root $data, $record!data, ký tự đại diện xuất hiện [*], bộ chọn con cháu (descendant) và con đại diện (child-wildcard selectors), property parent, bộ chọn class #dataGroup/#dataValue, và các vị từ (predicate) kiểu FormCalc đơn giản, nhờ đó một chương trình Delphi có thể cập nhật mọi dòng hóa đơn trong form thuế của chính phủ chỉ bằng một lệnh gọi. Bài viết này là tài liệu tham khảo về cách định địa chỉ; đối với việc di chuyển toàn bộ tập dữ liệu giữa các file, hãy xem bài viết đi kèm về trao đổi dữ liệu form FDF, XFDF và XFA

Vấn đề xuất hiện ngay khi một form không còn ở dạng phẳng (flat) nữa. Một tờ khai thuế GTGT hoặc tờ khai hải quan được xây dựng dưới dạng form XFA động không có mười hai trường được đặt tên từ Total_Price_1 đến Total_Price_12; nó chỉ có một subform Detail được khai báo một lần trong template DOM và được sinh ra (instantiated) nhiều lần tùy theo yêu cầu của dữ liệu. Các giá trị nằm trong một cây thứ hai, Data DOM bên trong datasets packet, nơi mỗi dòng mục (line item) là một phần tử <Detail> lặp lại dưới <Receipt>. Tên trường dạng phẳng không thể định vị "giá ở dòng thứ ba" hoặc "mọi dòng có giá trị trên 200"; đó chính là nhiệm vụ mà chương Scripting Object Model của đặc tả Adobe XFA 3.3 giao cho các biểu thức SOM, và đó cũng là cú pháp mà các API giá trị trường XFA của losLab PDF Library sử dụng

Làm thế nào để định địa chỉ các node dữ liệu XFA từ Delphi?

Mọi đường dẫn datasets đều bắt đầu từ một root, và losLab PDF Library chấp nhận các cách viết tiêu chuẩn có thể thay thế cho nhau: $data là dạng viết tắt trong XFA 3.3 của xfa.datasets.data, !data là dạng viết tắt bắt đầu từ xfa.datasets, và trong các packet không sử dụng record processing, $record sẽ phân giải thành record dữ liệu bên ngoài, phần tử đầu tiên dưới node dữ liệu. Các API phía template như SetXFAFormFieldAccess chấp nhận $templatexfa.template theo cùng một cách. Các phân đoạn có thể được phân tách bằng dấu chấm hoặc dấu gạch chéo ($data.Receipt.Tax hoặc $data/Receipt/Tax), điều này rất quan trọng vì GetXFAFormFieldNames liệt kê các trường dưới dạng đường dẫn phân tách bằng dấu gạch chéo để đưa trực tiếp vào các API giá trị và template. Chỉ mục xuất hiện bắt đầu từ 0 theo đặc tả, vì vậy Detail[0] là hàng đầu tiên. Hai quy tắc escape giúp định địa chỉ các tên bất thường: trong một đường dẫn phân tách bằng dấu chấm, \. biểu thị một dấu chấm thực tế (Line\.Item), trong khi đường dẫn phân tách bằng dấu gạch chéo coi dấu chấm là ký tự bình thường. Dữ liệu doanh nghiệp mang prefix XML namespace của riêng nó sẽ được so khớp theo tên cục bộ (local name), vì vậy <m:Receipt> vẫn tương ứng với 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;

Làm thế nào để điền dữ liệu vào các dòng lặp lại trong form XFA bằng lập trình?

Ký tự đại diện xuất hiện [*] là công cụ xử lý hàng loạt. Trong khi một chỉ mục số chọn một phần tử cùng cấp duy nhất, [*] chọn tất cả các phần tử cùng cấp có cùng tên, do đó $data.Receipt.Detail[*].Total_Price định địa chỉ trường giá trên mọi dòng mục cùng một lúc. Khi đọc, GetXFAFormFieldValue trả về giá trị của tất cả các node khớp được nối với nhau bằng dấu phân cách |; khi ghi, SetXFAFormFieldValue cập nhật mọi node khớp với cùng một giá trị và trả về 1 nếu thành công. Một đường dẫn không khớp với bất kỳ thứ gì sẽ trả về một chuỗi trống, đây là cách nhanh chóng để kiểm tra xem một nhánh có tồn tại hay không trước khi ghi dữ liệu vào đó

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;

Các bộ chọn con cháu (descendant), con đại diện (child-wildcard) và cha (parent)

Ba bộ chọn cấu trúc bao quát các trường hợp bạn biết tên trường nhưng không biết độ sâu chính xác của nó. Bộ chọn con cháu .. khớp ở bất kỳ độ sâu nào bên dưới node hiện tại: $data..Total_Price tìm thấy Total_Price đầu tiên ở bất kỳ đâu dưới root dữ liệu, và một thao tác ghi thông qua đường dẫn con cháu sẽ cập nhật node khớp đầu tiên đó. Hãy lưu ý giới hạn: một chỉ mục xuất hiện sau một so khớp con cháu không nhảy qua các nhánh cùng cấp khác, vì vậy nếu $data..Total_Price[0] phân giải bên trong hàng Detail đầu tiên, thì $data..Total_Price[1] sẽ trả về kết quả trống chứ không nhảy sang hàng tiếp theo; hãy sử dụng Detail[*] khi bạn muốn chọn tất cả các hàng. Ký tự đại diện con .* khớp với mọi con trực tiếp và cho phép các phân đoạn còn lại lọc: $data.Receipt.*.Total_Price tiếp cận Total_Price bên trong mỗi nhánh con của Receipt mà không chọn trường tóm tắt cùng tên nằm trực tiếp trên chính Receipt. Cuối cùng, property parent đi lên một cấp, tính năng mà XFA 3.3 phân biệt rõ ràng với ..: $data.Receipt.Detail[1].parent.Tax bắt đầu từ dòng mục thứ hai, đi lên Receipt, và dừng lại ở giá trị Tax cùng cấp của nó

Các bộ chọn class và vị từ (predicate): #dataGroup, #dataValue, .[expression]

Cú pháp #class định địa chỉ các node Data DOM theo class đối tượng thay vì theo tên, đây là con đường thực tế khi các tag XML chứa các ký tự khó viết dưới dạng tên script. losLab PDF Library ánh xạ #dataGroup cho các phần tử có phần tử con và #dataValue cho các phần tử lá không có phần tử con, và cả hai đều kết hợp với chỉ mục số hoặc xuất hiện [*]: $data.Receipt.#dataGroup[0].#dataValue[0] chọn giá trị đầu tiên bên trong nhóm đầu tiên dưới Receipt. Bộ chọn vị từ .[expression] lọc các phần tử cùng cấp có cùng tên theo nội dung. Các vị từ được hỗ trợ là các phép so sánh đơn giản giữa giá trị con và một literal, với các toán tử quan hệ như >, <<=; khi một vị từ khớp với nhiều hàng, kết quả đọc trả về được nối bằng | và thao tác ghi sẽ cập nhật mọi kết quả khớp

// 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]');

Những tính năng XFA SOM nào không được hỗ trợ?

Hỗ trợ SOM trong losLab PDF Library được giới hạn trong các đường dẫn trường datasets và template, và các giới hạn này là rõ ràng chứ không phải nỗ lực tối đa. Biết trước những giới hạn này sẽ giúp tránh phải debug một đường dẫn âm thầm trả về chuỗi trống

  • Các vị từ (predicate) không thực thi FormCalc hoặc JavaScript tùy ý: không có lệnh gọi hàm (vị từ contains(...) sẽ trả về chuỗi trống), không có biểu thức boolean phức hợp, chỉ có một so sánh child op literal duy nhất
  • $record chỉ hoạt động trong các packet không xử lý record; tính năng phân trang record dataWindow và xoay nhóm record không được triển khai
  • Phân giải được thực hiện trực tiếp đối với Data DOM và template DOM; việc phân giải tương đối Form DOM, tức là chế độ xem kết hợp mà trình xem xây dựng lúc chạy, không được thực hiện
  • Ngữ nghĩa tải thuộc tính Data DOM không được áp dụng; thư viện đọc datasets packet dưới dạng XML thô, do đó các đường dẫn định địa chỉ các phần tử chứ không phải các thuộc tính được chuyển thành node

Những giới hạn này hiếm khi ảnh hưởng đến công việc điền dữ liệu hàng loạt, vì trình điền dữ liệu định vị dữ liệu theo cấu trúc hơn là theo logic script. Khi gặp phải, lối thoát là xử lý ở cấp độ packet: đọc toàn bộ XML datasets, biến đổi nó trong Delphi, rồi ghi ngược trở lại

Ghi toàn bộ packet bằng SetXFAFromString

SetXFAFromString là điểm đầu vào cấp độ packet đó: nó cài đặt một chuỗi XDP hoàn chỉnh, cả template và datasets cùng nhau, tạo ra container AcroForm trong một tài liệu mới khi chưa tồn tại, và nó chấp nhận các packet mã hóa UTF-16 có byte-order mark cũng như UTF-8. Một quy trình sản xuất phổ biến là giữ XDP do cơ quan ban hành làm bản mẫu, tải nó bằng SetXFAFromString, sau đó chạy các lệnh gọi SetXFAFormFieldValue được định địa chỉ bằng SOM cho các giá trị biến động của từng hóa đơn trước khi lưu. Vì XFA là một trong hai mô hình form mà một file PDF có thể mang theo, phía AcroForm cổ điển có câu chuyện tự động hóa riêng của nó, được đề cập trong bài viết về các hành động form tương tác và JavaScript

Các API giá trị trường, liệt kê và packet XFA được hiển thị ở đây đi kèm trong losLab PDF Library dành cho Delphi, C# và VB.NET; trang sản phẩm cung cấp tài liệu tham khảo đầy đủ về xử lý form