Chuyển một khối trường biểu mẫu từ template năm ngoái sang layout năm nay là chỗ mà các vòng round-trip FDF và XFDF hết đủ dùng: giá trị tới nơi, nhưng appearance stream, calculation action và default resource thì không. PDFiumPas xử lý trường hợp đó bằng GraftPdfAcroForm, hàm clone toàn bộ đồ thị đối tượng trường từ một PDF và ghi sang PDF khác
Lý do một xuất dữ liệu cấp giá trị không làm được chuyện này là bản chất cấu trúc. Một trường không phải một bản ghi, nó là một subgraph. ISO 32000-1 §12.7 định nghĩa từ điển biểu mẫu tương tác chứa /Fields, /CO, /DR và /DA, §12.7.3 định nghĩa các từ điển trường treo bên dưới nó, và §12.5.6.19 định nghĩa các widget annotation cho trường một hộp hiển thị trên trang. XFDF mang theo lá của cấu trúc đó. Còn grafting mang chính cấu trúc
Vì sao chép mảng /Fields chưa bao giờ là đủ
Chép /Fields từ tài liệu này sang tài liệu khác tạo ra một biểu mẫu hỏng theo mọi cách thú vị, vì mảng chỉ chứa tham chiếu indirect và không gì khác. ISO 32000-1 §7.3.10 khiến một indirect object đánh địa chỉ được bằng số đối tượng cộng generation, và các con số đó chỉ có ý nghĩa bên trong tệp mà chúng đến từ đó. Dán mảng sang phía bên kia và mọi tham chiếu trong đó hoặc đuối, hoặc tệ hơn, âm thầm phân giải sang một đối tượng không liên quan tình cờ chiếm slot đó ở đích. Bên dưới mỗi tham chiếu là một đồ thị vừa chia sẻ vừa có chu kỳ. Một từ điển trường trỏ tới các kid của nó, mỗi kid trỏ ngược lại /Parent của nó, một widget trỏ tới các appearance stream của nó và tới trang mang nó qua /P, appearance stream trỏ tới các font trong từ điển default resource của biểu mẫu, và các từ điển additional-action dưới /AA trỏ tới thêm các đối tượng khác nữa. Hai widget trên hai trang khác nhau thường dùng chung một font và một appearance XObject. Nên một graft đúng phải đi qua đồ thị đó, clone mỗi đối tượng chạm được đúng một lần, hướng lại /P của mọi widget về trang đích đã ánh xạ, và thêm widget đã clone vào mảng /Annots của trang đó — nếu không trường tồn tại trong biểu mẫu nhưng vô hình trên trang. Nếu bạn đã từng đuổi theo sự khác biệt giữa một trường, widget của nó và annotation trang hiển thị nó, ghi chú của chúng tôi về chỉ số widget so với chỉ số annotation nói đúng về sự tách đó
GraftPdfAcroForm cần gì từ bạn?
Nó cần ba stream tách biệt và một phép ánh xạ trang tường minh. GraftPdfAcroForm nhận Source, Destination và Output là các thực thể TStream riêng, một mảng TPdfGraftPageMappings, một bản ghi TPdfAcroFormGraftOptions, một TPdfCrossDocumentGraftMap tùy chọn, và một out TPdfAcroFormGraftReport. Nó trả Boolean thay vì raise, và khi thất bại report mang lý do trong ErrorMessage. Phép ánh xạ trang là 1-based ở cả hai phía và không được suy diễn: mọi trang nguồn mang một widget bạn định graft phải xuất hiện trong đó. Truyền nil cho graft map là hợp lệ — hàm khi đó tự tạo và giải phóng một map riêng cho suốt lời gọi — và TPdfAcroFormGraftOptions.Default cho bạn CollisionPolicy là pagcpReject, RenamePrefix là Imported_, MaxObjects 100000, MaxDepth 128 và AllowSignedDestination là False. Ba cái cuối là các ngân sách, và chúng tồn tại vì đồ thị đối tượng bạn sắp đi qua đến từ một tệp bạn không phải người viết
uses
Classes, SysUtils, FPdfCompress;
var
Source, Destination, Output: TMemoryStream;
Options: TPdfAcroFormGraftOptions;
Mappings: TPdfGraftPageMappings;
Report: TPdfAcroFormGraftReport;
begin
Source := TMemoryStream.Create;
Destination := TMemoryStream.Create;
Output := TMemoryStream.Create;
try
Source.LoadFromFile('claim-template-2025.pdf');
Destination.LoadFromFile('claim-layout-2026.pdf');
Source.Position := 0;
Destination.Position := 0;
Options := TPdfAcroFormGraftOptions.Default;
SetLength(Mappings, 2);
Mappings[0].SourcePageNumber := 1;
Mappings[0].DestinationPageNumber := 1;
Mappings[1].SourcePageNumber := 2;
Mappings[1].DestinationPageNumber := 3;
if GraftPdfAcroForm(Source, Destination, Output, Mappings,
Options, nil, Report) then
Output.SaveToFile('claim-2026-with-fields.pdf')
else
raise Exception.Create(Report.ErrorMessage);
finally
Output.Free;
Destination.Free;
Source.Free;
end;
end;
Graft map tránh clone một font dùng chung hai lần thế nào?
TPdfCrossDocumentGraftMap giữ một bảng tham chiếu nguồn-đích mà khóa của nó mang cả số đối tượng lẫn generation, và bộ clone đệ quy tra bảng này trước khi đi sâu xuống. Thứ tự thao tác là thứ khiến chu kỳ an toàn: bộ clone cấp phát số đối tượng đích và đăng ký ánh xạ trước, rồi mới đi qua các tham chiếu con của đối tượng nguồn. Một parent chạm tới kid trỏ ngược lại parent sẽ thấy parent đã được đăng ký và trả về tham chiếu đích hiện có thay vì đệ quy. Cùng phép tra đó khiến một font, một appearance stream hay một action được sáu widget dùng chung chỉ clone một lần và được tham chiếu sáu lần. Map được gắn với tài liệu nguồn bằng một hash SHA-256 của các byte nguồn, phơi ra thành SourceIdentity. Nếu bạn đưa GraftPdfAcroForm một map có identity không khớp với nguồn bạn truyền, nó từ chối lời gọi thay vì tái dùng các tham chiếu chưa từng hợp lệ cho tệp này. Các phép ánh xạ trang được gieo vào cùng map trước khi clone bắt đầu, và đó chính là cách /P của một widget cuối cùng trỏ về trang đích: đối tượng trang nguồn đã phân giải tới đối tượng trang đích được ánh xạ, nên lượt viết lại tham chiếu thông thường xử lý nó không cần trường hợp đặc biệt
uses
Classes, SysUtils, FPdfCompress, FPdfSha256;
var
GraftMap: TPdfCrossDocumentGraftMap;
SourceBytes: TBytes;
EntriesBefore: Integer;
begin
SetLength(SourceBytes, Source.Size);
Source.Position := 0;
if Length(SourceBytes) > 0 then
Source.ReadBuffer(SourceBytes[0], Length(SourceBytes));
GraftMap := TPdfCrossDocumentGraftMap.Create(
AnsiString(SHA256Hex(SHA256Bytes(SourceBytes))));
try
EntriesBefore := GraftMap.Count;
Source.Position := 0;
if not GraftPdfAcroForm(Source, Destination, Output, Mappings,
Options, GraftMap, Report) then
begin
// Các mục lời gọi này thêm vào đã được rollback;
// mọi thứ đăng ký trước nó vẫn nguyên vẹn.
Assert(GraftMap.Count = EntriesBefore);
WriteLn('graft refused: ', Report.ErrorMessage);
end;
finally
GraftMap.Free;
end;
end;
Việc rollback đó chính là lý do nên sở hữu map bằng chính tay bạn. PDFiumPas xử lý map do caller cung cấp như một giao dịch: một lần graft thất bại bỏ các mục mà lời gọi đó thêm vào và giữ mọi ánh xạ tồn tại từ trước, nên một lần từ chối không bao giờ để lại một cache tham chiếu trỏ tới các đối tượng chưa từng được ghi. Nhưng hãy giữ một map cho mỗi tài liệu đích — phía đích của mỗi mục là một số đối tượng trong đúng tệp đó, và nó chẳng nghĩa lý gì trong một tệp khác
Va chạm tên trường: từ chối hay đổi tên
Tên trường đầy đủ phải giữ tính duy nhất bên trong một biểu mẫu, và PDFiumPas sẽ không đoán ý bạn khi chúng va nhau. TPdfAcroFormCollisionPolicy đưa ra đúng hai câu trả lời. Dưới pagcpReject, mặc định, trường nguồn đầu tiên có tiêu đề đã tồn tại ở đích sẽ hủy toàn bộ graft với một lỗi và để stream đầu ra rỗng. Dưới pagcpRename, trường nguồn va chạm được đổi tên bằng cách thêm tiền tố RenamePrefix và graft tiếp tục, với Report.RenamedFieldCount cho biết chuyện đó xảy ra bao nhiêu lần
Options := TPdfAcroFormGraftOptions.Default;
Options.CollisionPolicy := pagcpRename;
Options.RenamePrefix := 'Y2025_';
Options.MaxObjects := 20000;
Options.MaxDepth := 64;
if GraftPdfAcroForm(Source, Destination, Output, Mappings,
Options, nil, Report) then
begin
WriteLn('source fields : ', Report.SourceFieldCount);
WriteLn('existing fields: ', Report.DestinationFieldCount);
WriteLn('grafted fields : ', Report.GraftedFieldCount);
WriteLn('renamed fields : ', Report.RenamedFieldCount);
WriteLn('cloned objects : ', Report.GraftedObjectCount);
WriteLn('reused objects : ', Report.ReusedObjectCount);
WriteLn('mapped pages : ', Report.MappedPageCount);
WriteLn('output bytes : ', Report.OutputByteCount);
end
else
WriteLn('graft refused : ', Report.ErrorMessage);
Đổi tên không miễn phí, và bạn nên quyết định nó một cách chủ đích thay vì đưa tay lấy nó để cho một lỗi biến mất. Một trường đã đổi tên là một trường khác: bất kỳ JavaScript nào ở đích gọi nó theo tên, bất kỳ mục calculation trong /CO mà con người viết dựa trên tên cũ, và bất kỳ consumer phía sau nào khóa theo tên trường đều cần biết về tiền tố. Nếu hai tài liệu thật sự mô tả cùng một trường, bản sửa trung thực thường là dàn hòa tên phía thượng nguồn, chứ không phải tại thời điểm graft. Khi graft đã hạ cánh, việc đi qua biểu mẫu hợp nhất để xác nhận bạn thực sự nhận được gì là bước kế tiếp tự nhiên, và điều hướng trường biểu mẫu trong PDFiumPas nói về việc đi qua đó
Những chỗ graft cố ý fail closed
Mọi điều kiện mơ hồ đều là một lỗi, không bao giờ là một kết quả cố hết sức, và đó là một quyết định thiết kế đáng hiểu trước khi nó làm bạn ngạc nhiên trong production. GraftPdfAcroForm trả False, reset stream đầu ra và báo lý do khi chạm vào bất kỳ trường hợp nào sau đây
- Biểu mẫu nguồn mang một mục
/XFA— các packet XFA là một mô hình biểu mẫu song song và không thể rút gọn thành các từ điển trường AcroForm - Một widget nằm trên trang nguồn không có mục nào trong phép ánh xạ trang, nếu không sẽ âm thầm mất trường hoặc gắn nó vào nhầm trang
- Các phép ánh xạ trang ngoài phạm vi, hoặc hai ánh xạ dùng chung một trang nguồn hay trang đích
- Cả hai biểu mẫu cùng định nghĩa một từ điển default resource
/DR, vì việc hợp nhất hai không gian tên resource có nguy cơ trỏ lại một tên hiện có sang một font khác - Đồ thị đối tượng vượt
MaxObjectshoặc đệ quy vượtMaxDepth - Tài liệu đích chứa một chữ ký và
AllowSignedDestinationlàFalse - Graft map được cung cấp thuộc về một tài liệu nguồn khác, hoặc một tham chiếu nguồn bị đuối
Đường ghi cũng bảo thủ không kém. PDFiumPas phát kết quả như một bản sửa đổi incremental thưa được nối thêm vào tài liệu đích, rồi hiện thực lại đầu ra đã ghi và đọc lại biểu mẫu của nó: nếu số trường của kết quả không bằng số trường gốc của đích cộng số trường của nguồn, toàn bộ graft bị từ chối và đầu ra bị xóa. Bạn không bao giờ nhận một tệp được graft dở. Cái giá của chính sách này là thật — một va chạm /DR hay một đích có chữ ký chặn bạn đứng im, và bạn phải tự phân giải thay vì chấp nhận một bản hợp nhất xấp xỉ — nhưng phương án thay thế là một biểu mẫu mở lên ngon lành mà tính sai
Khi grafting là công cụ sai
Grafting di chuyển cấu trúc, nên hãy dùng nó khi cấu trúc mới là thứ bạn còn thiếu. Nếu cả hai tài liệu đã mang cùng một bộ trường và bạn chỉ cần chuyển giá trị lẫn annotation giữa chúng, đường xuất và nhập trong bài về dữ liệu biểu mẫu XFDF nhẹ hơn, chuẩn hơn và đảo ngược được. Hãy với tới GraftPdfAcroForm khi đích không có trường nào, hoặc có một bộ khác, và bạn cần các widget, appearance stream, action cùng thứ tự tính toán đi qua nguyên vẹn. Một lưu ý thực tế cuối về identity: vì graft map khóa theo số đối tượng cộng generation và bị gắn với một SHA-256 của byte nguồn, việc lưu lại hay tối ưu nguồn giữa các lần chạy tạo ra một identity khác và một map không còn áp dụng được. Hãy chụp nhanh nguồn bạn graft từ đó và giữ nó ổn định suốt lô; coi nó như một artifact đầu vào, chứ không phải thứ gì một job chạy đêm có thể tự ý viết lại
GraftPdfAcroForm, TPdfCrossDocumentGraftMap và bộ công cụ PDF cấp stream xung quanh đi kèm trong PDFiumPas Delphi PDFium Component cho Delphi, C++Builder và Lazarus, nơi trang sản phẩm mang tài liệu API đầy đủ cho các tùy chọn graft, các field của report và phần bề mặt soạn thảo tài liệu còn lại