Trong PDFium Component trước v3.121.1, việc đọc một annotation qua TPdf.Annotation[] rồi gán record ngược lại có thể thêm các entry /R và /D rỗng vào dictionary appearance /AP của nó, kể cả khi bản gốc chỉ mang /N. Các validator PDF/A từ chối dictionary đó. Từ v3.121.1, getter chỉ báo cáo appearance mà nó thực sự đọc được, nên một round trip không thay đổi gì sẽ chẳng ghi thêm gì mới. Cái lỗi này đáng được hiểu chi tiết, vì cái cò thông thường lại là một bản sửa nhằm cho file tuân thủ hơn, chứ không phải kém đi
Chuyện gì sai khi bạn ghi một annotation lại nguyên trạng?
Câu trả lời ngắn: annotation có thêm các appearance stream mà nó chưa từng có, và một file đã pass validation PDF/A trước khi bạn sửa thì gãy sau đó. Kịch bản điển hình diễn ra thế này. Một kho lưu trữ của khách hàng đến với các annotation square và text thiếu cờ Print, PDF/A đòi mọi annotation phải in được, nên bạn loop qua các trang, thêm afPrint, rồi gán từng record ngược lại. Chẳng gì trong đoạn code đó đụng tới appearance. Record từ TPdf.Annotation[] là một TPdfAnnotation, và SetAnnotationData ghi mọi field mà sentinel Has* được đặt, đúng cách mà các cặp HasContents / ContentsText được thiết kế để vận hành. Vấn đề là getter đặt HasAppearanceRollover và HasAppearanceDown thành True với chuỗi rỗng cho các mode không tồn tại, và setter chăm chỉ ghi ra hai stream rỗng:
procedure MarkAnnotationsPrintable(const FileName: string);
var
Pdf: TPdf;
PageNo, I: Integer;
A: TPdfAnnotation;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := FileName;
Pdf.Active := True;
for PageNo := 1 to Pdf.PageCount do
begin
Pdf.PageNumber := PageNo;
for I := 0 to Pdf.AnnotationCount - 1 do
begin
A := Pdf.Annotation[I];
if not (afPrint in A.Flags) then
begin
A.Flags := A.Flags + [afPrint] - [afHidden, afInvisible, afNoView];
// Trước v3.121.1, phép gán này còn ghi luôn các stream /AP/R và
// /AP/D rỗng khi annotation nguồn chỉ có /AP/N
Pdf.Annotation[I] := A;
end;
end;
end;
Pdf.SaveAs(ChangeFileExt(FileName, '.printable.pdf'));
finally
Pdf.Free;
end;
end;
ISO 32000-1 §12.5.5 định nghĩa dictionary appearance với ba entry: /N cho appearance thường, /R cho rollover, và /D cho down. /R và /D là tùy chọn, và khi chúng vắng mặt, viewer rơi về /N. Nhưng một stream /R rỗng thì không phải vắng mặt. Nó là một stream hợp lệ mà chẳng vẽ gì cả, nên một viewer tôn trọng appearance rollover sẽ hiện một hình chữ nhật trống ngay khoảnh khắc con trỏ đi ngang qua annotation. PDF/A còn khắt khe hơn nữa: ISO 19005-1 (kèm Corrigendum 2) và ISO 19005-2 / 19005-3 chỉ cho phép /N trong dictionary appearance của annotation. veraPDF báo file đã round-trip dưới luật 6.5.3-4 cho PDF/A-1 và luật 6.3.3-2 cho PDF/A-2 và PDF/A-3, còn TPdf.ValidatePdfA dựng sẵn liệt kê nó là pvaiAnnotationApDictViolation. Phép sửa thêm cờ Print để thỏa một điều khoản của chuẩn lại gãy một điều khoản khác
Vì sao FPDFAnnot_GetAP trả về 2 cho một appearance vắng mặt?
PDFium không bao giờ trả về 0 từ FPDFAnnot_GetAP, kể cả khi appearance stream được yêu cầu không tồn tại. Hàm theo đúng pattern hai-lần-gọi quen thuộc của PDFium: truyền buffer nil để lấy kích thước cần thiết tính bằng byte, cấp phát, rồi gọi lại để copy văn bản UTF-16LE. Kích thước luôn bao gồm terminator UTF-16, nên một stream vắng mặt báo 2 byte, một chuỗi rỗng cộng terminator của nó. Getter trước v3.121.1 kiểm tra ByteLength >= SizeOf(FPDF_WCHAR), một phép kiểm mà mọi lần gọi đều pass, nên cả ba cờ HasAppearance* đều về True với bất kỳ annotation nào có appearance nói chung. Một round trip qua record rồi yêu cầu FPDFAnnot_SetAP lưu một chuỗi rỗng cho từng mode, và PDFium tạo stream để chứa nó. Không exception, không cảnh báo, và trang nhìn thấy được trông hệt như cũ, đó là lý do khuyết tật chỉ lộ diện trong một fixture veraPDF chứ không phải trong một viewer
v3.121.1 quyết định một appearance tồn tại thế nào
ReadAppearance, helper bên trong GetPageAnnotation đổ đầy AppearanceNormal, AppearanceRollover và AppearanceDown, giờ chỉ coi một kết quả là nội dung khi nó mang ít nhất một ký tự vượt qua terminator. Lần gọi đầu phải trả nhiều hơn SizeOf(FPDF_WCHAR) byte và số byte phải chẵn, vì một độ dài lẻ không thể là UTF-16. Lần gọi thứ hai, thứ thực sự copy văn bản, lại được validate lần nữa: độ dài trả về bằng 2 hay ít hơn, hoặc lớn hơn buffer đã cấp phát, sẽ reset HasValue về False và để chuỗi rỗng. Phía ghi chẳng có gì thay đổi. SetAnnotationData vẫn chỉ gọi FPDFAnnot_SetAP cho các mode mà cờ HasAppearance* là True, nên một record đọc từ annotation chỉ có /N giờ ghi lại đúng mỗi /N. Fixture regression phủ cả hai chiều: một annotation square với appearance thường, đọc rồi ghi lại nguyên trạng, pass PDF/A-1b, PDF/A-2b và PDF/A-3b, trong khi cùng annotation đó khi bỏ cờ Print thì gãy đúng luật cờ kỳ vọng và chẳng gãy gì khác
Stream vắng mặt và stream rỗng nhìn giống hệt nhau, nên getter giữ thái độ bảo thủ
API native không thể phân biệt một appearance stream vắng mặt với một stream có mà rỗng, và PDFium Component không hứa gì quá khả năng của mình. Cả hai ca đều trả cùng 2 byte từ FPDFAnnot_GetAP, nên cả hai đọc ngược về HasAppearanceRollover = False với AppearanceRollover rỗng. Điều đó kéo theo hai hệ quả bạn nên thiết kế xoay quanh. Thứ nhất, một sentinel False nghĩa là “chẳng đọc được nội dung nào, nên write-back sẽ để nguyên mode này”, chứ không phải “khóa /R vắng mặt trong dictionary”. Thứ hai, record không thể phát hiện một stream rỗng đã có sẵn trong file: một tài liệu bị tổn thương bởi một build cũ hay bởi một công cụ khác đọc ngược về sạch bong, và gán record lại không sửa được cũng chẳng làm tệ thêm. Muốn tìm những file đó bạn cần một phép kiểm cấp byte, chính là việc của TPdf.ValidatePdfA và workflow validation preflight PDF/A với PDFium Component
Bạn xóa một appearance một cách chủ ý thế nào?
Bạn đặt sentinel một cách tường minh rồi truyền một chuỗi rỗng; setter sẽ ghi nó. Chặn chuỗi rỗng trong SetAnnotationData sẽ là bản sửa thô cho bug này, nhưng nó cũng làm gãy các caller chủ ý xóa một appearance, cùng hợp đồng mà HasContents và HasAuthor tuân theo cho văn bản. Vì thế bản sửa nằm hoàn toàn ở getter, còn setter tiếp tục vâng lời bất cứ gì caller yêu cầu:
// Thay appearance rollover, rồi xóa nó lần nữa
A := Pdf.Annotation[0];
A.HasAppearanceRollover := True;
A.AppearanceRollover := 'q Q';
Pdf.Annotation[0] := A;
A := Pdf.Annotation[0];
// A.HasAppearanceRollover là True và văn bản round-trip về 'q Q'
A.HasAppearanceRollover := True; // xác nhận lại ý định một cách tường minh
A.AppearanceRollover := ''; // chủ ý ghi một stream rỗng
Pdf.Annotation[0] := A;
A := Pdf.Annotation[0];
// Đọc ngược về HasAppearanceRollover = False với chuỗi rỗng:
// một stream rỗng và một stream vắng mặt không thể phân biệt ở đây
Hãy nhớ rằng một /R hay /D bị dọn rỗng một cách tường minh vẫn tính là một khóa thừa dưới các luật PDF/A trích ở trên. Nếu đích đến là một archive profile, ghi một /N không rỗng và để nguyên hai mode còn lại là hình dạng duy nhất validate được. Bất kỳ workflow nào di chuyển annotation giữa các tài liệu, như XFDF export và import với PDFium Component, nên theo cùng luật đó: copy đúng các mode mà nguồn thực sự có và để mọi sentinel còn lại là False
Một pattern read-modify-write giữ an toàn PDF/A
Hãy nâng cấp lên v3.121.1 hay mới hơn, để nguyên các sentinel appearance đúng như getter trả về, và validate file đã lưu trước khi giao nó. Vì một stream rỗng tồn dư đọc ngược về như vắng mặt, bước xác minh phải nhìn vào tài liệu đã serialize thay vì nhìn vào record, và nó đủ rẻ để chạy sau mỗi batch:
uses
PDFium, FPdfPdfa; // FPdfPdfa khai báo TPdfAValidationIssue
function AnnotationAppearancesAreClean(Pdf: TPdf): Boolean;
var
Report: TPdfAValidationResult;
begin
// Validate tài liệu đang nạp trong Pdf, bao gồm các phép sửa
// đã làm qua Pdf.Annotation[] kể từ khi nó được mở
Report := Pdf.ValidatePdfA;
Result := not (pvaiAnnotationApDictViolation in Report.Issues);
end;
Kỷ luật tương tự áp dụng cho bất kỳ panel nào tô lại màu hay chú thích trang phục vụ việc rà soát, một workflow đã trình bày trong dựng workflow review annotation Delphi với PDFium Component: record là một ảnh chụp của những gì engine đọc được, và một sentinel mà bạn không tự đặt nên đi ngược lại nguyên trạng. Toàn bộ annotation API, PDF/A preflight và engine PDFium native được giao cùng nhau trong PDFium Component for Delphi, C++Builder and Lazarus