În PDFium Component de dinainte de v3.121.1, citirea unei adnotări prin TPdf.Annotation[] și asignarea înregistrării înapoi putea adăuga intrări /R și /D vide dicționarului său de aspect /AP, chiar când originalul purta doar /N. Validatorii PDF/A resping acel dicționar. Din v3.121.1, getter-ul raportează doar un aspect pe care l-a citit efectiv, deci un round trip neschimbat nu scrie nimic nou. Defecțiunea merită înțeleasă în detaliu, pentru că declanșatorul obișnuit este o reparare menită să facă un fișier mai conform, nu mai puțin
Ce merge prost când scrieți o adnotare înapoi neschimbată?
Răspunsul scurt: adnotarea câștigă fluxuri de aspect pe care nu le-a avut niciodată, iar un fișier care trecea validarea PDF/A înainte de editarea dumneavoastră o pică după. Scenariul tipic decurge așa. Arhiva unui client sosește cu adnotări pătrat și text cărora le lipsește fanionul Print, PDF/A cere ca fiecare adnotare să se tipărească, deci parcurgeți paginile în buclă, adăugați afPrint și asignați fiecare înregistrare înapoi. Nimic din codul acela nu atinge aspectele. Înregistrarea din TPdf.Annotation[] este un TPdfAnnotation, iar SetAnnotationData scrie fiecare câmp al cărui sentinel Has* e setat, exact cum funcționează perechile HasContents / ContentsText. Problema era că getter-ul seta HasAppearanceRollover și HasAppearanceDown la True cu șiruri vide pentru modurile care nu existau, iar setter-ul scria sfințitor două fluxuri vide:
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];
// Înainte de v3.121.1, această asignare scria și /AP/R și
// /AP/D vide când adnotarea sursă avea doar /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 definește dicționarul de aspect cu trei intrări: /N pentru aspectul normal, /R pentru rollover și /D pentru down. /R și /D sunt opționale, iar când lipsesc un viewer cade pe /N. Un flux /R vid nu este însă absent. Este un flux valid care nu pictează nimic, deci un viewer care onorează aspectele de rollover arată un dreptunghi gol în clipa în care pointerul se plimbă peste adnotare. PDF/A e și mai strict: ISO 19005-1 (cu Corrigendum 2) și ISO 19005-2 / 19005-3 permit doar /N într-un dicționar de aspect de adnotare. veraPDF raportează fișierul parcurs prin round-trip sub regula 6.5.3-4 pentru PDF/A-1 și regula 6.3.3-2 pentru PDF/A-2 și PDF/A-3, iar TPdf.ValidatePdfA-ul integrat îl listează ca pvaiAnnotationApDictViolation. Editarea care adăuga fanionul Print ca să satisfacă o clauză a standardului a spart alta
De ce întoarce FPDFAnnot_GetAP 2 pentru un aspect lipsă?
PDFium nu întoarce niciodată zero din FPDFAnnot_GetAP, nici măcar când fluxul de aspect cerut nu există. Funcția urmează tiparul obișnuit PDFium cu două apeluri: pasați un buffer nil ca să obțineți mărimea necesară în bytes, alocați, apoi apelați din nou ca să copiați textul UTF-16LE. Mărimea include întotdeauna terminatorul UTF-16, deci un flux lipsă raportează 2 bytes, un șir vid plus terminatorul lui. Getter-ul de dinainte de v3.121.1 testa ByteLength >= SizeOf(FPDF_WCHAR), o verificare pe care orice apel o trece, deci toate cele trei fanioane HasAppearance* se întorceau True pentru orice adnotare cu orice aspect deloc. Un round trip prin înregistrare cerea apoi lui FPDFAnnot_SetAP să stocheze un șir vid pentru fiecare mod, iar PDFium crea fluxul care să-l țină. Nicio excepție, niciun avertisment, iar pagina vizibilă arăta identic, motiv pentru care defectul a apărut într-o fixtură veraPDF, nu într-un viewer
Cum decide v3.121.1 că un aspect există
ReadAppearance, helper-ul din interiorul lui GetPageAnnotation care umple AppearanceNormal, AppearanceRollover și AppearanceDown, tratează acum un rezultat ca conținut doar când poartă cel puțin un caracter în plus față de terminator. Primul apel trebuie să întoarcă mai mult de SizeOf(FPDF_WCHAR) bytes și un număr par de bytes, fiindcă o lungime impară nu poate fi UTF-16. Al doilea apel, care copiază efectiv textul, este validat și el: o lungime întoarsă de 2 sau mai puțin, sau una mai mare decât bufferul alocat, resetează HasValue la False și lasă șirul vid. Pe partea de scriere nimic nu s-a schimbat. SetAnnotationData apelează tot FPDFAnnot_SetAP doar pentru modurile al căror fanion HasAppearance* este True, deci o înregistrare citită dintr-o adnotare care are doar /N scrie înapoi doar /N. Fixtura de regresie acoperă ambele direcții: o adnotare pătrat cu aspect normal, citită și scrisă înapoi neschimbată, trece PDF/A-1b, PDF/A-2b și PDF/A-3b, în timp ce aceeași adnotare cu fanionul Print eliminat pică pe regula de fanion așteptat și pe nimic altceva
Fluxurile lipsă și cele vide arată identic, deci getter-ul rămâne conservator
API-ul nativ nu poate deosebi un flux de aspect lipsă de unul care există dar e vid, iar PDFium Component nu pretinde altceva. Ambele cazuri întorc aceiași 2 bytes din FPDFAnnot_GetAP, deci ambele se citesc înapoi ca HasAppearanceRollover = False cu un AppearanceRollover vid. De aici decurg două consecințe în jurul cărora merită să proiectați. În primul rând, un sentinel False înseamnă „nu s-a citit conținut, deci o scriere înapoi lasă acest mod în pace", nu „cheia /R lipsește din dicționar". În al doilea rând, înregistrarea nu poate detecta un flux vid care e deja în fișier: un document avariat de un build mai vechi sau de o altă unealtă se citește înapoi curat, iar asignarea înregistrării înapoi nu îl repară și nici nu îl agravează. Ca să găsiți acele fișiere aveți nevoie de o verificare la nivel de octet, ceea ce fac TPdf.ValidatePdfA și fluxul de validare preflight PDF/A cu PDFium Component
Cum goliți un aspect din intenție?
Setați sentinel-ul explicit și pasați un șir vid; setter-ul îl scrie. Blocarea șirurilor vide în SetAnnotationData ar fi fost repararea bătută în cuie pentru bug-ul acesta, dar ar fi spart și apelanții care golesc un aspect în mod deliberat, același contract pe care îl urmează HasContents și HasAuthor pentru text. Deci repararea trăiește în întregime în getter, iar setter-ul continuă să onoreze orice cere apelantul:
// Înlocuiți aspectul rollover, apoi goliți-l din nou
A := Pdf.Annotation[0];
A.HasAppearanceRollover := True;
A.AppearanceRollover := 'q Q';
Pdf.Annotation[0] := A;
A := Pdf.Annotation[0];
// A.HasAppearanceRollover este True și textul face round-trip ca 'q Q'
A.HasAppearanceRollover := True; // reafirmați intenția explicit
A.AppearanceRollover := ''; // scrieți un flux vid din intenție
Pdf.Annotation[0] := A;
A := Pdf.Annotation[0];
// Se citește înapoi ca HasAppearanceRollover = False cu un șir vid:
// un flux vid și unul lipsă sunt de neosemit aici
Țineți minte că un /R sau /D golit explicit contează tot ca o cheie în plus sub regulile PDF/A citate mai sus. Dacă ținta este un profil de arhivă, scrierea unui /N non-vid și lăsarea celorlalte două moduri în pace este singura formă care validează. Orice flux de lucru care mută adnotări între documente, precum exportul și importul XFDF cu PDFium Component, ar trebui să urmeze aceeași regulă: copiați modurile pe care sursa le-a avut efectiv și lăsați restul sentinel-urilor False
Un tipar read-modify-write care rămâne în siguranță PDF/A
Faceți upgrade la v3.121.1 sau mai nou, lăsați sentinel-urile de aspect exact așa cum le-a întors getter-ul și validați fișierul salvat înainte să-l trimiteți. Pentru că un flux vid vechi se citește înapoi ca absent, pasul de verificare trebuie să privească documentul serializat, nu înregistrarea, și e destul de ieftin de rulat după fiecare lot:
uses
PDFium, FPdfPdfa; // FPdfPdfa declară TPdfAValidationIssue
function AnnotationAppearancesAreClean(Pdf: TPdf): Boolean;
var
Report: TPdfAValidationResult;
begin
// Validează documentul încărcat acum în Pdf, inclusiv editările
// făcute prin Pdf.Annotation[] de la deschidere
Report := Pdf.ValidatePdfA;
Result := not (pvaiAnnotationApDictViolation in Report.Issues);
end;
Aceeași disciplină se aplică oricărui panou care recolorează sau adnotează pagini pentru revizuire, flux de lucru acoperit în construirea unui flux de revizuire de adnotări în Delphi cu PDFium Component: înregistrarea este o fotografie a ceea ce motorul putea citi, iar un sentinel pe care nu l-ați setat singur ar trebui să plece la drum neschimbat. API-ul complet de adnotări, preflight-ul PDF/A și motorul nativ PDFium sosesc împreună în PDFium Component pentru Delphi, C++Builder și Lazarus