Bài viết kỹ thuật

Vì sao chỉnh sửa trường XFA biến mất khi lưu trong PDFium với Delphi

TPdf.SetFocusedFormFieldText trong PDFium Component ghi vào bộ đệm chỉnh sửa sống của trường biểu mẫu đang được focus, và với một biểu mẫu XFA, bộ đệm đó không bao giờ đến được gói datasets được serialize ra đĩa — nên một giá trị người dùng gõ vào, và code của bạn xác nhận đã được chấp nhận, âm thầm biến mất lần tiếp theo file mở lên. Các trường AcroForm không có vấn đề này: cùng lệnh gọi đó commit vào entry /V của trường ngay khi focus rời đi. Một người dùng điền một biểu mẫu tiếp nhận XFA, lưu, và mở lại để thấy trường số tiền trống trơn lần nữa không gặp một trục trặc render — họ đang chạm vào ranh giới của những gì bản thân engine PDFium phơi bày cho việc ghi dữ liệu biểu mẫu

Đây là một câu hỏi hẹp hơn việc phát hiện một biểu mẫu XFA ngay từ đầu, hay làm cho JavaScript của nó chạy: không phải "PDFium có hỗ trợ XFA không" và không phải "làm sao để tôi thực thi script AcroForm" mà cụ thể là điều gì xảy ra với một giá trị sau khi SetFocusedFormFieldText báo cáo thành công. Phiên bản ngắn gọn là AcroForm và XFA không phải hai phương ngữ của cùng một mô hình biểu mẫu xét theo đường ghi của PDFium — chúng là hai mô hình biểu mẫu với hai mối quan hệ hoàn toàn khác nhau giữa những gì người dùng gõ và những gì một lượt lưu thực sự nắm bắt, và việc gộp hai cái làm một chính là điều biến một lệnh gọi API một dòng thành một ticket hỗ trợ ba tuần sau khi triển khai thử nghiệm của khách hàng lên sóng. Bài viết AcroForm JavaScript chỉ ra lệnh gọi một dòng và nêu kết quả AcroForm-so-với-XFA trong một comment code; bài viết này ở lại trên cùng API đó và đi qua đường ghi nội bộ, bằng chứng gói-datasets rằng lượt ghi XFA không bao giờ đáp xuống, vì sao khoảng trống nằm trong chính PDFium chứ không phải binding Delphi, và một cách giải quyết vá-XML-của-riêng-bạn cho các tài liệu cần chỉnh sửa sống sót qua một lượt lưu

SetFocusedFormFieldText ghi một giá trị trường như thế nào?

TPdf.SetFocusedFormFieldText hoạt động bằng cách mô phỏng một chỉnh sửa cấp gõ-phím, không phải bằng cách chọc một giá trị vào mô hình tài liệu trực tiếp. Nội bộ nó gọi FORM_SelectAllText để chọn nội dung hiện tại của trường được focus, sau đó FORM_ReplaceSelection để ghi đè vùng chọn bằng chuỗi mới — cùng hai thao tác mà một lượt chọn-tất-cả-và-gõ điều khiển bằng bàn phím sẽ kích hoạt. Vì lượt ghi đi qua đường chỉnh sửa văn bản tương tác của PDFium thay vì vòng quanh nó, bất kỳ script gõ-phím, format, hay calculate nào gắn với trường phát ra đúng như nó sẽ làm với một con người đang gõ, đó là điều khiến API hữu ích cho việc điền biểu mẫu theo chương trình trong một trình xem giữ JavaScript sống. Đối tác phía đọc là FocusedFormFieldText, được hậu thuẫn bởi FORM_GetFocusedText, và nó phản ánh chính bộ đệm sống mà SetFocusedFormFieldText vừa ghi

if Pdf.FocusedFormFieldIndex >= 0 then
begin
  if Pdf.SetFocusedFormFieldText('1284.50') then
    Log('Buffer now reads: ' + Pdf.FocusedFormFieldText)
  else
    Log('No field is focused, or it does not accept text');
end
else
  Log('Focus a field first - FocusFormField or a real click');

Vì sao AcroForm giữ giá trị và XFA mất nó?

Các trường văn bản và combo AcroForm tồn tại lâu dài vì môi trường điền biểu mẫu riêng của PDFium commit bộ đệm chỉnh sửa cho bạn: ngay khi trường mất focus, bộ đệm được ghi vào entry /V của trường, cùng khóa mà mọi trình đọc PDF tuân thủ chuẩn nhìn vào để biết giá trị đã lưu của một trường. TPdf.ClearFormFieldFocus — gọi FORM_ForceToKillFocus bên dưới — buộc lượt commit đó theo yêu cầu, nên code đặt một giá trị theo chương trình không cần chờ một cú nhấp chuột thật ở nơi khác trong UI. Lưu ngay sau đó, và văn bản mới là một phần của đồ thị đối tượng tài liệu trước cả khi TPdf.SaveAs từng chạy, vì /V là một entry thật trong một từ điển trường thật, không phải thứ gì gắn thêm sau này

Pdf.FocusFormField(FieldIndex);
Pdf.SetFocusedFormFieldText('1284.50');
Pdf.ClearFormFieldFocus;              // forces the /V commit now
Pdf.SaveAs('invoice-acroform.pdf');

// Reopen and confirm - this is an AcroForm document, so it holds
Pdf.Active := False;
Pdf.FileName := 'invoice-acroform.pdf';
Pdf.Active := True;
Pdf.FocusFormField(FieldIndex);
Assert(Pdf.FocusedFormFieldValue = '1284.50');   // passes

Một chỉnh sửa trường XFA thực sự sống ở đâu?

Các trường XFA không có hệ thống nối dây như vậy. Văn bản người dùng gõ vào đáp xuống một bộ đệm CPWL_Edit thuộc về lớp render và tương tác XFA của PDFium, và lớp đó không có đường code nào copy bộ đệm trở lại vào gói datasets lưu trong PDF. TPdf.GetXfaDatasets làm cho khoảng trống hiển thị: gọi nó trước và sau một chỉnh sửa trên một trường XFA và các byte nó trả về giống hệt nhau, vì phương thức đọc gói gốc mà tài liệu được mở với, không bao giờ đọc trạng thái sống của widget bạn vừa chỉnh sửa. Không điều gì trong đó là một lỗi cache hay một vấn đề định thời làm mới — gói datasets trên đĩa và bộ đệm chỉnh sửa trong bộ nhớ đơn giản là hai mảnh trạng thái khác nhau mà API công khai của PDFium không bao giờ kết nối

var
  Before, After: TBytes;
begin
  Before := Pdf.GetXfaDatasets;
  Pdf.FocusFormField(FieldIndex);
  Pdf.SetFocusedFormFieldText('1284.50');
  After := Pdf.GetXfaDatasets;
  // Before and After are byte-for-byte identical on an XFA document -
  // the edit never touched the packet GetXfaDatasets reads from
end;

Đây có phải một lỗi của PDFium Component hay một giới hạn của PDFium?

Mảnh còn thiếu nằm trong chính PDFium, không phải trong binding Delphi trên nó. API công khai của PDFium không có FPDF_SetXFAPacket để tiêm một gói đã cập nhật và không có FPDF_SaveAsXFA để yêu cầu engine XFA serialize DOM hiện tại của nó trở lại thành XML datasets trước một lượt lưu. FPDF_SaveAsCopy — export đứng sau TPdf.SaveAs — ghi ra đồ thị đối tượng tài liệu mà PDFium đã có sẵn; nó không có hook nào để yêu cầu engine XFA xả trạng thái sống của nó trước, vì hook đó không tồn tại ở thượng nguồn. PDFium Component không thể thêm sự đối chiếu mà bản thân PDFium chưa bao giờ triển khai, và việc phát hành một bộ serialize DOM-sang-XML tự chế đoán mò trạng thái XFA nội bộ của PDFium sẽ tệ hơn khoảng trống trung thực: nó sẽ trông như thể nó hoạt động cho đến khi phiên bản PDFium tiếp theo thay đổi gì đó mà không ai bên ngoài dự án có thể thấy

Ranh giới này lộ ra trong cùng cuộc kiểm toán v2.13.2 đã xây dựng SetFocusedFormFieldText ngay từ đầu. FORM_ReplaceSelection đã được ràng buộc trong bảng import DLL qua nhiều phiên bản mà chưa bao giờ được gọi từ code Pascal, và việc thêm đường ghi cuối cùng đã dùng đến nó là điều khiến khoảng trống bền vững đủ cụ thể để ghi chép thay vì lý thuyết. Cùng đợt kiểm toán đó lộ ra một khoảng trống không liên quan nhưng liên quan về mặt tinh thần: JavaScript AcroForm đã bị âm thầm vô hiệu hóa kể từ v2.13.0 vì nền tảng JS chỉ được nối dây bên trong nhánh khởi tạo XFA, nên các tài liệu AcroForm thông thường với app.alert hay trường tính toán hoàn toàn không bao giờ có một engine script. Cái đó có thể sửa được — mở rộng nền tảng JS đến mọi tài liệu bất kể XFA — và nó được phát hành trong cùng phiên bản; khoảng trống bền vững được nói đến ở đây thì không thể sửa được, vì các lý do ở trên. Bản sửa JavaScript và các sự kiện host-veto quanh nó được nói đến trong chạy JavaScript AcroForm với PDFium Component

Bạn nên làm gì về điều này trong Delphi?

Với các tài liệu AcroForm, cách sửa không hơn gì một thói quen tốt: gọi ClearFormFieldFocus (hay bằng cách khác di chuyển focus đi) trước SaveAs bất cứ khi nào một giá trị được đặt theo chương trình, thay vì giả định một tương tác UI sau này sẽ kích hoạt lượt commit cho bạn. Với một tài liệu có thể là AcroForm hoặc XFA — trường hợp phổ biến trong một trình xem đa dụng — hãy kiểm tra FormType hay boolean XFA trước khi bạn hứa hẹn với một caller rằng một lượt lưu sẽ giữ lại, và đọc phát hiện biểu mẫu XFA và trích xuất gói XFA để có đầy đủ các phép thử, bao gồm trường hợp XFAF nơi nội dung XFA được xếp lớp lên trên các widget AcroForm thông thường khác vẫn tôn trọng /V

Với một biểu mẫu XFA động đích thực nơi các giá trị đã chỉnh sửa phải sống sót qua một lượt lưu, bộ đệm chỉnh sửa tương tác hoàn toàn không phải công cụ đúng. Con đường bền vững là coi GetXfaDatasets như đường cơ sở của bạn, không phải kết quả của bạn: đọc nó một lần khi tài liệu mở, tự giữ bản ghi riêng của bạn về những gì người dùng đã thay đổi theo từng trường — chính xác các giá trị UI của bạn đã có sẵn, vì PDFium sẽ không trả chúng lại cho bạn sau thực tế — tự vá chúng vào XML cơ sở, và tự điều khiển đầu ra của bạn. Một lượt ghi đi qua XML mà chính code của bạn kiểm soát sống sót qua một lượt lưu mà một bộ đệm CPWL_Edit không bao giờ có thể

function ExportEditedXfaValue(Pdf: TPdf; const FieldPath,
  NewValue: string): TBytes;
var
  DatasetsXml: string;
begin
  // GetXfaDatasets ships with PDFium Component; PatchXmlNode below is
  // your own helper over your own XML library, nothing PDFium provides
  DatasetsXml := TEncoding.UTF8.GetString(Pdf.GetXfaDatasets);
  DatasetsXml := PatchXmlNode(DatasetsXml, FieldPath, NewValue);
  Result := TEncoding.UTF8.GetBytes(DatasetsXml);
end;

Bắt được khoảng trống trước khi một khách hàng làm điều đó

TPdf.SaveAs trả về True bất kể một giá trị trường XFA có sống sót hay không, vì từ góc nhìn của PDFium, lượt lưu thực sự đã thành công — nó đã ghi mọi byte nó được yêu cầu ghi. Điều đó khiến đây chính xác là loại lỗi lọt qua một smoke test và đến được một khách hàng: không gì ném lỗi, không gì ghi log, file mở tốt, chỉ có giá trị cụ thể là sai. Một test khứ hồi thực sự mở lại file đã lưu và so sánh giá trị của trường — hay so sánh GetXfaDatasets trước và sau, theo ví dụ trước — thuộc về bộ hồi quy cho bất kỳ trình xem nào cho phép người dùng chỉnh sửa nội dung XFA, không chỉ các đường AcroForm tình cờ hoạt động theo mặc định

Không điều nào trong số này là một khiếm khuyết để báo cáo với PDFium Component nhiều bằng một ranh giới để thiết kế xoay quanh: SetFocusedFormFieldText làm chính xác những gì tên nó nói cho cả hai mô hình biểu mẫu, và sự khác biệt về kết quả truy nguyên sạch sẽ về những gì AcroForm và XFA mỗi bên nối bộ đệm đó vào ở phía PDFium. API, các primitive focus và save, và các bộ đọc gói được nhắc đến ở đây là một phần của PDFium Component dành cho Delphi và C++Builder