PDFium Component giờ đặt FPDF_FORMFILLINFO.version thành 2 cho mọi form-fill environment mà nó khởi tạo, vì phiên bản mà một bản dựng PDFium native chấp nhận là thuộc tính của chính bản dựng đó, không phải của tài liệu đang được mở. Một pdfium.v8.dll bật XFA từ chối thẳng phiên bản 1, nên một PDF AcroForm thường mở qua nó trước đây fail ngay trong FPDFDOC_InitFormFillEnvironment mà chẳng thấy XFA đâu cả. Bản vá v3.116.0 thì nhỏ, nhưng sai lầm đằng sau nó là một sai lầm phổ quát và đáng gọi tên: một trường phiên bản giao thức mô tả bố cục bộ nhớ mà phía bên kia trông đợi, và nó không bao giờ được suy ra từ việc bạn có tình cờ cần những tính năng mà bố cục đó mang theo hay không
Vì sao FPDFDOC_InitFormFillEnvironment fail trên một PDF thường với pdfium.v8.dll?
Environment fail vì một bản dựng PDFium bật XFA kiểm tra trường version trước khi làm bất cứ việc gì khác, còn logic wrapper cũ lại đưa cho nó số 1 mỗi khi tài liệu hiện tại không phải form XFA. Triệu chứng trong một host Delphi là một EPdfError được ném ra từ TPdf.InitializeFormFill với thông báo Cannot initialize form fill environment, ném ngay khi đang mở một hóa đơn hay tờ khai thuế bình thường chỉ có các text field AcroForm. Cùng tệp đó mở ngon với pdfium.dll thường. Cùng DLL đó mở một tài liệu XFA thật cũng ngon. Chỉ có tổ hợp bản dựng V8 với một tài liệu không phải XFA là hỏng, và đó đúng là tổ hợp mà một host rơi vào sau khi nó bật EnableV8Engine để có JavaScript của AcroForm, hoặc sau khi cơ chế tự chọn trong LoadDocument đã cam kết cả tiến trình dùng pdfium.v8.dll vì một tệp XFA trước đó. Cam kết đó là toàn tiến trình: EnableV8Engine được đọc trước lần LoadLibrary đầu tiên, và một khi bản dựng XFA đã nạp thì mọi PDF thường về sau đều đi qua cùng bước thiết lập environment trên cùng một binary. Host không làm gì sai; wrapper đã hỏi sai câu hỏi khi nó điền vào record. Nếu bạn còn đang cân nhắc nên phát hành binary nào, bài về triển khai PDFium DLL và chẩn đoán lỗi nạp trình bày việc chọn bản thường hay bản V8, còn bài này giả định bản V8 đã nằm trong tiến trình
Trường version trong FPDF_FORMFILLINFO thật ra hứa điều gì?
FPDF_FORMFILLINFO.version cho PDFium biết nó được phép đọc những trường nào của record, và header công khai fpdf_formfill.h gắn các giá trị được chấp nhận với cách thư viện được biên dịch chứ không với tài liệu. Nói lại, giao kèo có ba phần. Phiên bản 1 bao gồm các callback ổn định từ FFI_Invalidate tới FFI_DoGoToAction cùng con trỏ m_pJsPlatform. Một bản dựng không có module XFA chấp nhận cả 1 lẫn 2, và với 2 thì nó cũng sẽ gọi thêm các callback thử nghiệm. Một bản dựng có module XFA đòi 2, hết, và header lặp lại yêu cầu đó hai lần như thể nó biết trước người ta sẽ bỏ sót. Không chỗ nào trong giao kèo nhắc tới tài liệu. Phiên bản là một phát biểu về cái record bạn đã cấp phát: với số 2, bạn cam kết rằng vùng bộ nhớ sau m_pJsPlatform tồn tại và chứa hoặc các con trỏ hàm hợp lệ hoặc NULL
Vùng phiên bản 2 là nơi toàn bộ cỗ máy XFA trú ngụ. Nó bắt đầu bằng xfa_disabled, một FPDF_BOOL mà header mô tả là bị bỏ qua dưới phiên bản 2 và chỉ có nghĩa khi module XFA được biên dịch vào, rồi tiếp tục với mười bảy con trỏ hàm, từ FFI_DisplayCaret tới FFI_DoURIActionWithKeyboardModifier. Mỗi cái trong số đó được ghi là bắt buộc với XFA và nếu không thì phải đặt NULL. Cách diễn đạt đó chính là chìa khóa của toàn bộ bản vá. NULL không phải trạng thái lỗi với những slot đó; nó là trạng thái được ghi rõ cho một host không điều khiển XFA. Một record đã được xóa bằng FillChar rồi đánh dấu là phiên bản 2 thỏa giao kèo trên bản dựng không có XFA đúng như một record phiên bản 1, và nó là record duy nhất mà bản dựng XFA chịu nhận
Cách chọn cũ đã buộc ABI vào tài liệu
Lỗi chỉ là một câu điều kiện trông có vẻ hợp lý khi đứng riêng. TPdf.InitializeFormFill tính một cờ RuntimeReady từ ba dữ kiện: tài liệu báo form type là XFA qua TPdf.XFA, các helper chuỗi XFA phân giải được qua XfaFeaturesAvailable, và các export V8 phân giải được qua V8FeaturesAvailable. Trước v3.116.0, chính cờ đó cũng chọn luôn phiên bản
// v3.115.0 và trước đó: phiên bản ABI đi theo tài liệu
RuntimeReady := XFA and XfaFeaturesAvailable and V8FeaturesAvailable;
if RuntimeReady then
FFormFillInfo.Info.version := 2
else
FFormFillInfo.Info.version := 1;
// ... và nhánh thiếu runtime ghim nó lần nữa
else if XFA then
begin
FFormFillInfo.Info.version := 1;
FFormFillInfo.Info.xfa_disabled := 1;
if Assigned(FOnXfaRuntimeMissing) then
FOnXfaRuntimeMissing(Self);
end;
Đọc nó với header trong tay thì lỗi hiện ra rõ ràng. RuntimeReady sai với mọi tài liệu AcroForm thường, nên mọi tài liệu thường đều khai phiên bản 1. Trên pdfium.dll thì không sao. Trên pdfium.v8.dll, tức bản dựng bật XFA, PDFium kiểm tra trường đó, thấy nó dưới mức 2 bắt buộc, và trả về một FPDF_FORMHANDLE null, thứ mà CheckPdf biến thành exception ở trên. Ý định của code cũ là phòng thủ: giữ phiên bản 1 để bản dựng XFA không bao giờ đọc các slot phiên bản 2 chưa được gán. Nó phòng một vấn đề mà header đã loại trừ sẵn, rồi tạo ra một vấn đề mà header cảnh báo tường minh. Code đã sửa quyết định phiên bản một lần, ngay từ đầu, dựa trên việc record thật sự là gì
procedure TPdf.InitializeFormFill;
var
RuntimeReady: Boolean;
begin
FXfaRuntimeUsable := False;
FXfaPageCountOverride := -1; // sentinel: dùng cây trang tĩnh
if not FormFill then
Exit;
FillChar(FFormFillInfo, SizeOf(FFormFillInfo), 0);
FFormFillInfo.Pdf := Self;
// Record phiên bản 2 đầy đủ được cấp phát và xóa sạch ở trên. PDFium
// chấp nhận phiên bản 2 khi không có XFA và đòi nó ở mọi bản dựng
// bật XFA, kể cả khi tài liệu này không chứa form XFA.
FFormFillInfo.Info.version := 2;
FFormFillInfo.Info.xfa_disabled := 1;
// RuntimeReady gác các callback XFA và xfa_disabled, không bao giờ gác version.
RuntimeReady := XFA and XfaFeaturesAvailable and V8FeaturesAvailable;
...
RuntimeReady vẫn thuộc về đâu: các callback và xfa_disabled
RuntimeReady giữ nguyên vai trò cổng gác cho hành vi XFA; nó chỉ không còn đụng tới bố cục record nữa. Các callback phiên bản 1, FFI_Invalidate, FFI_SetTimer, FFI_GetPage, FFI_DoURIAction, FFI_DoGoToAction cùng phần còn lại của khối đó, được nối vô điều kiện vì cả AcroForm lẫn XFA đều phụ thuộc vào chúng. Mười bảy con trỏ phiên bản 2 chỉ được gán bên trong nhánh RuntimeReady, cùng với xfa_disabled := 0. Khi tài liệu là XFA nhưng runtime không có mặt, record vẫn ở phiên bản 2 với xfa_disabled bằng 1 và các slot phiên bản 2 để NULL, còn wrapper kích OnXfaRuntimeMissing để host có thể gợi ý khởi động lại trên pdfium.v8.dll. Sau khi environment đã tồn tại, FPDF_LoadXFA chỉ được gọi khi RuntimeReady từng đúng, và chỉ một giá trị trả về true mới đặt FXfaRuntimeUsable, tức thứ mà TPdf.XfaRuntimeAvailable báo cáo
if RuntimeReady then
begin
FFormFillInfo.Info.xfa_disabled := 0; // 0 = bật XFA
FFormFillInfo.Info.FFI_DisplayCaret := FormFillDisplayCaret;
FFormFillInfo.Info.FFI_GetCurrentPageIndex := FormFillGetCurrentPageIndex;
FFormFillInfo.Info.FFI_SetCurrentPage := FormFillSetCurrentPage;
FFormFillInfo.Info.FFI_GotoURL := FormFillGotoURL;
FFormFillInfo.Info.FFI_GetPageViewRect := FormFillGetPageViewRect;
FFormFillInfo.Info.FFI_PageEvent := FormFillPageEvent;
FFormFillInfo.Info.FFI_PopupMenu := FormFillPopupMenu;
FFormFillInfo.Info.FFI_OpenFile := FormFillOpenFile;
FFormFillInfo.Info.FFI_EmailTo := FormFillEmailTo;
// ... FFI_UploadTo tới FFI_DoURIActionWithKeyboardModifier
end
else if XFA then
begin
// Runtime không có: giữ phiên bản 2, để XFA tắt, báo cho host.
if Assigned(FOnXfaRuntimeMissing) then
FOnXfaRuntimeMissing(Self);
end;
FFormHandle := FPDFDOC_InitFormFillEnvironment(FDocument, FFormFillInfo.Info);
CheckPdf(FFormHandle <> nil, 'Cannot initialize form fill environment');
if RuntimeReady then
FXfaRuntimeUsable := FPDF_LoadXFA(FDocument) <> 0;
Hai chi tiết trong khối đó dễ làm sai khi bạn tự viết binding. FXfaPageCountOverride được đặt lại về -1 như một sentinel trước khi mọi thứ khác xảy ra, nên PageCount rơi về cây trang tĩnh cho tới khi FFI_PageEvent báo có phân trang lại; một số 0 ở đó sẽ âm thầm khai rằng tài liệu rỗng. Và mỗi callback phiên bản 2 là một routine cdecl tĩnh, nó lấy lại TPdf sở hữu từ record rồi nuốt mọi exception Pascal trước khi trả về cho PDFium, đúng kỷ luật mà bài về gia cố ABI PDFium trong Delphi nói rõ cho FFI_OpenFile. Không có gì trong thay đổi phiên bản này nới lỏng cả hai quy tắc đó
Phiên bản 2 có an toàn khi DLL không có module XFA không?
Có, và lý do nằm trong chính record chứ không nằm ở một lời hứa nào từ thư viện. Trên bản dựng không có XFA, header nói phiên bản 2 khiến các callback thử nghiệm cũng được gọi, nên câu hỏi là PDFium thấy gì khi nó nhìn vào. TPdfFormFillInfo là một packed record có member Info là FPDF_FORMFILLINFO đầy đủ, gồm mọi trường phiên bản 2, và InitializeFormFill xóa sạch nó bằng FillChar trước khi chạm vào một byte nào. Nên trên một pdfium.dll thường với một tài liệu thường, thư viện thấy phiên bản 2, xfa_disabled được đặt, và NULL ở mọi slot thử nghiệm, đúng cái trạng thái mà header quy định cho một host không cài đặt XFA. Không có record bị cắt ngắn nào để thư viện đọc lố, vì record vốn chưa bao giờ ngắn hơn phiên bản 2. Logic cũ đang phòng một sự lệch bố cục mà chính khai báo Pascal đã loại bỏ từ trước
Ranh giới đáng nói thật thà là ranh giới mà record không thể che. Phiên bản 2 trên một tài liệu thường không bật JavaScript, không bật scripting XFA, cũng không bật bất kỳ host event nào đứng sau các callback đó. m_pJsPlatform chỉ được gắn khi V8FeaturesAvailable đúng, XFA vẫn tắt trừ khi RuntimeReady từng đúng, và TPdf.XFA vẫn báo form type từ FPDF_GetFormType bất kể environment đã thương lượng được gì. Một host muốn biết XFA động có thật sự render hay không thì nên tiếp tục đọc XfaRuntimeAvailable sau khi Active thành true, như bài về phát hiện form XFA và trích xuất gói XFA khuyến nghị, thay vì suy diễn bất cứ điều gì từ trường version
procedure TMainForm.PdfXfaRuntimeMissing(Sender: TObject);
begin
// Được kích từ InitializeFormFill khi tài liệu là XFA nhưng
// pdfium.dll đã nạp không chạy được engine. Form environment vẫn mở,
// vì dù sao phiên bản 2 cũng đã được truyền; chỉ runtime XFA là tắt.
StatusBar.SimpleText :=
'XFA form detected; restart with pdfium.v8.dll to enable dynamic rendering';
end;
procedure TMainForm.OpenDocument(const FileName: string);
begin
Pdf.Active := False;
Pdf.OnXfaRuntimeMissing := PdfXfaRuntimeMissing;
Pdf.FormFill := True;
Pdf.FileName := FileName;
Pdf.Active := True; // không còn ném lỗi với PDF thường dưới pdfium.v8.dll
if Pdf.XFA and not Pdf.XfaRuntimeAvailable then
ShowStaticXfaWarning;
end;
Phiên bản giao thức và khả năng tính năng là hai trục khác nhau
Quy tắc chung rút ra từ bản vá này là một trường phiên bản trong cấu trúc callback trả lời câu hỏi “record này lớn bao nhiêu và bạn được đọc gì từ nó”, còn phát hiện khả năng tính năng trả lời câu hỏi “slot nào trong đó sẽ làm được việc gì có ích”. Cái thứ nhất do binary native và do khai báo Pascal mà bạn biên dịch cùng quyết định. Cái thứ hai thay đổi theo từng tài liệu, theo từng bảng export của DLL và theo từng cấu hình host. Gộp hai thứ vào một boolean thì rất cám dỗ vì ca XFA tình cờ cần cả hai, nhưng ngay khi một bản dựng cưỡng chế phiên bản tối thiểu thì việc gộp đó vỡ với mọi tài liệu không cần tính năng ấy. Form XFA, được ISO 32000-1 §12.7.8 mô tả là một payload XML nằm cạnh dictionary AcroForm, chính là tính năng ở đây; bố cục record là giao thức, và PDFium có quyền khăng khăng đòi đúng bố cục trước khi nó nhìn vào tệp. Hình dạng đó cũng xuất hiện ở mọi nơi một thư viện C đánh phiên bản cho cấu trúc của nó: một khối viewer-info, một record render-options, một bảng callback platform. Mẫu an toàn chính là mẫu mà InitializeFormFill đã sửa tuân theo. Khai báo bố cục mới nhất mà bạn hiểu, xóa sạch nó hoàn toàn, đặt phiên bản khớp với bố cục đó một cách vô điều kiện, rồi để các phép kiểm tra khả năng quyết định slot nào sẽ được điền. Nếu một header PDFium tương lai thêm phiên bản 3, thay đổi nằm ở khai báo và ở đúng một phép gán đó, chứ không nằm ở một nhánh phụ thuộc tài liệu — thứ sẽ sai với bất kỳ tổ hợp nào không ai test
Phần khởi tạo form-fill đã sửa có trong PDFium Component cho Delphi, Lazarus và C++Builder, và nó áp dụng như nhau trên Win32 lẫn Win64 vì cả hai bản dựng dùng chung một khai báo record. Nếu ứng dụng của bạn đã chọn pdfium.v8.dll cho các AcroForm chạy bằng JavaScript, đây chính là thay đổi giúp nó mở phần còn lại của kho PDF qua cùng một binary mà không phải đối xử đặc biệt với form environment