Di PDFium Component sebelum v3.121.1, membaca sebuah anotasi lewat TPdf.Annotation[] lalu meng-assign record itu kembali bisa menambahkan entry /R dan /D kosong ke dictionary appearance /AP-nya, bahkan saat aslinya hanya membawa /N. Validator PDF/A menolak dictionary begitu. Sejak v3.121.1 getter hanya melaporkan appearance yang benar-benar ia baca, jadi round trip yang tak berubah tidak menulis apa pun yang baru. Kegagalan ini layak dipahami sampai ke detailnya, karena pemicu yang biasa adalah fix yang memang dimaksudkan membuat berkas lebih patuh, bukan sebaliknya
Apa yang salah saat Anda menulis anotasi kembali tanpa perubahan?
Jawaban singkatnya: anotasi mendapat stream appearance yang tak pernah ia punya, dan berkas yang lolos validasi PDF/A sebelum edit Anda gagal sesudahnya. Skenario tipikalnya berjalan begini. Arsip pelanggan datang dengan anotasi square dan text yang tak membawa flag Print, PDF/A mensyaratkan semua anotasi tercetak, jadi Anda melakukan loop di semua halaman, menambah afPrint, dan meng-assign tiap record kembali. Tak ada satu baris pun di kode itu yang menyentuh appearance. Record dari TPdf.Annotation[] adalah TPdfAnnotation, dan SetAnnotationData menulis setiap field yang sentinel Has*-nya terpasang, persis cara pasangan HasContents / ContentsText dimaksudkan bekerja. Masalahnya, getter menyetel HasAppearanceRollover dan HasAppearanceDown ke True dengan string kosong untuk mode yang tidak ada, dan setter dengan patuh menulis dua stream kosong:
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];
// Sebelum v3.121.1, assign ini juga menulis /AP/R kosong dan
// stream /AP/D saat anotasi sumber hanya punya /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 mendefinisikan dictionary appearance dengan tiga entry: /N untuk appearance normal, /R untuk rollover, dan /D untuk down. /R dan /D opsional, dan saat keduanya absen, viewer jatuh kembali ke /N. Tapi stream /R yang kosong bukan berarti absen. Itu stream valid yang tidak melukis apa pun, jadi viewer yang menghormati appearance rollover memperlihatkan persegi kosong begitu pointer bergerak di atas anotasi. PDF/A lebih ketat lagi: ISO 19005-1 (dengan Corrigendum 2) serta ISO 19005-2 / 19005-3 hanya mengizinkan /N di dictionary appearance anotasi. veraPDF melaporkan berkas hasil round trip di rule 6.5.3-4 untuk PDF/A-1 dan rule 6.3.3-2 untuk PDF/A-2 dan PDF/A-3, dan TPdf.ValidatePdfA bawaan mencatatnya sebagai pvaiAnnotationApDictViolation. Edit yang menambah flag Print demi memenuhi satu klausul standar malah merusak klausul lain
Mengapa FPDFAnnot_GetAP mengembalikan 2 untuk appearance yang hilang?
PDFium tidak pernah mengembalikan nol dari FPDFAnnot_GetAP, bahkan saat stream appearance yang diminta tidak ada. Fungsi ini mengikuti pola dua panggilan khas PDFium: kirim buffer nil untuk mendapatkan ukuran yang dibutuhkan dalam byte, alokasikan, lalu panggil lagi untuk menyalin teks UTF-16LE. Ukurannya selalu menyertakan terminator UTF-16, jadi stream yang hilang melapor 2 byte, string kosong plus terminatornya. Getter pra-v3.121.1 menguji ByteLength >= SizeOf(FPDF_WCHAR), pemeriksaan yang lolos setiap panggilan, jadi ketiga flag HasAppearance* kembali True untuk anotasi mana pun yang punya appearance apa pun. Round trip lewat record lalu meminta FPDFAnnot_SetAP menyimpan string kosong untuk tiap mode, dan PDFium menciptakan stream untuk menampungnya. Tak ada exception, tak ada peringatan, dan halaman yang terlihat identik, itulah sebabnya cacat ini muncul di fixture veraPDF alih-alih di viewer
Cara v3.121.1 memutuskan bahwa sebuah appearance ada
ReadAppearance, helper di dalam GetPageAnnotation yang mengisi AppearanceNormal, AppearanceRollover, dan AppearanceDown, kini memperlakukan hasil sebagai konten hanya saat ia membawa minimal satu karakter di luar terminator. Panggilan pertama harus mengembalikan lebih dari SizeOf(FPDF_WCHAR) byte dan jumlah byte genap, karena panjang ganjil tidak mungkin UTF-16. Panggilan kedua, yang benar-benar menyalin teksnya, divalidasi lagi: panjang yang kembali 2 atau kurang, atau lebih besar dari buffer yang dialokasikan, mereset HasValue ke False dan membiarkan string kosong. Di sisi tulis tidak ada yang berubah. SetAnnotationData tetap memanggil FPDFAnnot_SetAP hanya untuk mode yang flag HasAppearance*-nya True, jadi record yang dibaca dari anotasi yang hanya punya /N kini menulis kembali hanya /N. Fixture regresinya mencakup kedua arah: anotasi square dengan appearance normal, dibaca dan ditulis kembali tanpa perubahan, lolos PDF/A-1b, PDF/A-2b, dan PDF/A-3b, sementara anotasi yang sama dengan flag Print-nya dihapus gagal di rule flag yang diharapkan dan tak di tempat lain
Stream hilang dan kosong tampak identik, jadi getter tetap konservatif
API native tidak bisa membedakan stream appearance yang hilang dari yang ada tapi kosong, dan PDFium Component tidak berpura-pura sebaliknya. Kedua kasus mengembalikan 2 byte yang sama dari FPDFAnnot_GetAP, jadi keduanya terbaca sebagai HasAppearanceRollover = False dengan AppearanceRollover kosong. Ada dua konsekuensi yang harus Anda desain di sekitarnya. Pertama, sentinel False berarti "tak ada konten yang terbaca, jadi write-back akan membiarkan mode ini sendiri", bukan "key /R absen dari dictionary". Kedua, record tidak bisa mendeteksi stream kosong yang sudah ada di berkas: dokumen yang rusak oleh build lama atau tool lain terbaca bersih, dan meng-assign record kembali tak memperbaiki maupun memperburuknya. Untuk menemukan berkas-berkas itu Anda butuh pemeriksaan level byte, dan itulah gunanya TPdf.ValidatePdfA serta workflow validasi preflight PDF/A dengan PDFium Component
Bagaimana cara mengosongkan appearance dengan sengaja?
Anda set sentinel-nya secara eksplisit dan kirim string kosong; setter menulisnya. Memblokir string kosong di SetAnnotationData memang akan jadi fix paling blunt untuk bug ini, tapi itu juga merusak caller yang mengosongkan appearance dengan sengaja, kontrak yang sama yang diikuti HasContents dan HasAuthor untuk teks. Jadi fix-nya sepenuhnya tinggal di getter, dan setter terus mengabulkan apa pun yang diminta caller:
// Ganti appearance rollover, lalu kosongkan lagi
A := Pdf.Annotation[0];
A.HasAppearanceRollover := True;
A.AppearanceRollover := 'q Q';
Pdf.Annotation[0] := A;
A := Pdf.Annotation[0];
// A.HasAppearanceRollover True dan teksnya round-trip sebagai 'q Q'
A.HasAppearanceRollover := True; // tegaskan lagi maksudnya secara eksplisit
A.AppearanceRollover := ''; // tulis stream kosong dengan sengaja
Pdf.Annotation[0] := A;
A := Pdf.Annotation[0];
// Terbaca kembali sebagai HasAppearanceRollover = False dengan string kosong:
// stream kosong dan yang hilang tak terbedakan di sini
Ingat bahwa /R atau /D yang dikosongkan secara eksplisit tetap terhitung sebagai key ekstra di bawah aturan PDF/A yang dikutip di atas. Kalau targetnya adalah profil arsip, menulis /N yang tak kosong dan membiarkan dua mode lain tak tersentuh adalah satu-satunya bentuk yang valid. Workflow apa pun yang memindahkan anotasi antar dokumen, seperti export dan import XFDF dengan PDFium Component, sebaiknya mengikuti aturan yang sama: salin mode yang benar-benar dimiliki sumber dan biarkan sentinel sisanya False
Pola read-modify-write yang tetap aman PDF/A
Upgrade ke v3.121.1 atau lebih baru, biarkan sentinel appearance persis seperti yang getter kembalikan, dan validasi berkas yang disimpan sebelum dikirim. Karena stream kosong yang basi terbaca sebagai absen, langkah verifikasi harus melihat dokumen yang terserialisasi alih-alih record-nya, dan itu cukup murah untuk dijalankan setelah tiap batch:
uses
PDFium, FPdfPdfa; // FPdfPdfa mendeklarasikan TPdfAValidationIssue
function AnnotationAppearancesAreClean(Pdf: TPdf): Boolean;
var
Report: TPdfAValidationResult;
begin
// Memvalidasi dokumen yang sedang termuat di Pdf, termasuk edit
// yang dilakukan lewat Pdf.Annotation[] sejak ia dibuka
Report := Pdf.ValidatePdfA;
Result := not (pvaiAnnotationApDictViolation in Report.Issues);
end;
Disiplin yang sama berlaku untuk panel apa pun yang mewarnai ulang atau menganotasi halaman untuk review, workflow yang dibahas di membangun workflow review anotasi Delphi dengan PDFium Component: record adalah snapshot dari apa yang bisa dibaca engine, dan sentinel yang bukan Anda set sendiri sebaiknya kembali tanpa berubah. API anotasi lengkap, preflight PDF/A, dan engine PDFium native dikirim bersamaan di PDFium Component untuk Delphi, C++Builder, dan Lazarus