U PDFium Component pre v3.121.1, čitanje anotacije kroz TPdf.Annotation[] i dodeljivanje zapisa nazad moglo je dodati prazne /R i /D unose u njen /AP appearance rečnik, čak i kada je original nosio samo /N. PDF/A validatori taj rečnik odbacuju. Od v3.121.1 getter prijavljuje samo appearance koji je zaista pročitao, pa nepromenjen round trip ne upisuje ništa novo. Taj kvar vredi razumeti u detalje, jer je običan okidač popravka koja je trebalo da fajl učini usklađenijim, a ne manje
Šta krene naopako kad anotaciju upišete nazad nepromenjenu?
Kratak odgovor: anotacija dobija appearance streamove koje nikada nije imala, i fajl koji je pre vaše izmene prolazio PDF/A validaciju posle nje pada. Tipičan scenario teče ovako. Klijentska arhiva stigne sa square i text anotacijama kojima fali Print zastavica, PDF/A zahteva da se svaka anotacija štampa, pa pređete kroz stranice, dodate afPrint i svaki zapis dodelite nazad. Ništa u tom kodu ne dira appearance streamove. Zapis iz TPdf.Annotation[] je TPdfAnnotation, a SetAnnotationData upisuje svako polje čiji je Has* sentinel postavljen, što je upravo namena parova HasContents / ContentsText. Problem je bio što je getter HasAppearanceRollover i HasAppearanceDown postavljao na True sa praznim stringovima za modove koji nisu postojali, a setter je savesno upisao dva prazna streama:
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];
// Pre v3.121.1 ova dodela upisivala je i prazne /AP/R i
// /AP/D streamove kad je izvorna anotacija imala samo /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 definiše appearance rečnik sa tri unosa: /N za normalni appearance, /R za rollover i /D za down. /R i /D su opciona, i kada ih nema pregledač se vraća na /N. Prazan /R stream nije odsutan, međutim. To je validan stream koji ne naslikava ništa, pa pregledač koji poštuje rollover appearance u trenutku kad pokazivač pređe preko anotacije pokazuje pravougaonik bez sadržaja. PDF/A je još stroži: ISO 19005-1 (sa Corrigendum 2) i ISO 19005-2 / 19005-3 dozvoljavaju samo /N u appearance rečniku anotacije. veraPDF round-trip fajl prijavljuje pod pravilom 6.5.3-4 za PDF/A-1 i pravilom 6.3.3-2 za PDF/A-2 i PDF/A-3, a ugrađeni TPdf.ValidatePdfA navodi ga kao pvaiAnnotationApDictViolation. Izmene koja je dodala Print zastavicu da bi zadovoljila jednu tačku standarda slomila je drugu
Zašto FPDFAnnot_GetAP vraća 2 za appearance koji ne postoji?
PDFium nikada ne vraća nulu iz FPDFAnnot_GetAP, čak ni kada traženi appearance stream ne postoji. Funkcija prati uobičajeni PDFium dvopozivni obrazac: prosledite nil bafer da saznate potrebnu veličinu u bajtovima, alocirate, pa pozovete ponovo da kopirate UTF-16LE tekst. Veličina uvek uključuje UTF-16 terminator, pa stream koji nedostaje prijavljuje 2 bajta, prazan string plus njegov terminator. Getter pre v3.121.1 testirao je ByteLength >= SizeOf(FPDF_WCHAR), uslov koji prolazi svaki poziv, pa su sve tri HasAppearance* zastavice vraćale True za svaku anotaciju koja je imala bilo kakav appearance. Round trip kroz zapis tada je od FPDFAnnot_SetAP tražio da za svaki mod sačuva prazan string, i PDFium je stvorio stream da ga drži. Bez izuzetka, bez upozorenja, i vidljiva stranica izgledala je isto, pa se defekt pokazao u veraPDF fixture-u, a ne u pregledaču
Kako v3.121.1 odlučuje da appearance postoji
ReadAppearance, helper unutar GetPageAnnotation-a koji puni AppearanceNormal, AppearanceRollover i AppearanceDown, sada rezultat tretira kao sadržaj tek kad on nosi bar jedan znak više od terminatora. Prvi poziv mora vratiti više od SizeOf(FPDF_WCHAR) bajtova i paran broj bajtova, jer neparna dužina ne može biti UTF-16. Drugi poziv, koji zaista kopira tekst, validira se ponovo: vraćena dužina od 2 ili manje, ili veća od alociranog bafera, vraća HasValue na False i ostavlja string prazan. Na strani upisa ništa se nije menjalo. SetAnnotationData i dalje zove FPDFAnnot_SetAP samo za modove čija je HasAppearance* zastavica True, pa zapis pročitan iz anotacije koja ima samo /N sada upisuje nazad samo /N. Regresioni fixture pokriva oba smera: square anotacija sa normalnim appearance-om, pročitana i upisana nazad nepromenjena, prolazi PDF/A-1b, PDF/A-2b i PDF/A-3b, dok ista anotacija sa uklonjenom Print zastavicom pada samo na pravilu očekivane zastavice i ni na čemu drugom
Streamovi koji nedostaju i prazni streamovi izgledaju isto, pa getter ostaje konzervativan
Nativni API ne ume da razlikuje appearance stream koji nedostaje od onoga koji postoji ali je prazan, i PDFium Component se ne pravi da može. Oba slučaja vraćaju ista 2 bajta iz FPDFAnnot_GetAP, pa se oboje čitaju nazad kao HasAppearanceRollover = False sa praznim AppearanceRollover. Iz toga slede dve posledice oko kojih treba da dizajnirate. Prvo, False sentinel znači „sadržaj nije pročitan, pa upis nazad ovaj mod ostavlja na miru", a ne „/R ključ nedostaje u rečniku". Drugo, zapis ne može otkriti prazan stream koji je već u fajlu: dokument oštećen starijim buildom ili drugim alatom čita se kao čist, i dodela zapisa nazad ga ni ne popravlja ni ne pogoršava. Da biste takve fajlove pronašli, treba vam provera na nivou bajtova, a za to služe TPdf.ValidatePdfA i PDF/A preflight validacioni workflow sa PDFium Component
Kako očistiti appearance namerno?
Postavite sentinel eksplicitno i prosledite prazan string; setter će ga upisati. Blokada praznih stringova u SetAnnotationData bila bi tupla popravka za ovaj bug, ali bi slomila i pozivaoce koji appearance brišu namerno, isti ugovor koji za tekst prate HasContents i HasAuthor. Pa je popravka u celini u getteru, a setter i dalje čini ono što ga pozivalac zamoli:
// Zamenite rollover appearance, pa ga ponovo očistite
A := Pdf.Annotation[0];
A.HasAppearanceRollover := True;
A.AppearanceRollover := 'q Q';
Pdf.Annotation[0] := A;
A := Pdf.Annotation[0];
// A.HasAppearanceRollover je True i tekst prolazi round trip kao 'q Q'
A.HasAppearanceRollover := True; // ponovo izričito potvrdite nameru
A.AppearanceRollover := ''; // namerno upišite prazan stream
Pdf.Annotation[0] := A;
A := Pdf.Annotation[0];
// Čita se nazad kao HasAppearanceRollover = False sa praznim stringom:
// prazan stream i onaj koji nedostaje ovde se ne razlikuju
Imajte na umu da i izričito ispražnjen /R ili /D i dalje računa kao dodatni ključ pod gore navedenim PDF/A pravilima. Ako je cilj arhivski profil, upisivanje nepraznog /N uz ostala dva moda netaknuta jedini je oblik koji prolazi validaciju. Svaki workflow koji premešta anotacije između dokumenata, poput XFDF exporta i importa sa PDFium Component, trebalo bi da sledi isto pravilo: prekopirajte modove koje je izvor zaista imao i ostavite ostale sentinele na False
Obrazac čitanje-izmena-upis koji ostaje PDF/A bezbedan
Nadogradite na v3.121.1 ili noviju, ostavite appearance sentinele tačno onakve kakve ih je getter vratio, i validirajte sačuvan fajl pre nego što ga otpremite. Pošto se zaostali prazan stream čita kao odsutan, korak provere mora gledati u serijalizovan dokument, a ne u zapis, i dovoljno je jeftin da se pokreće posle svake serije:
uses
PDFium, FPdfPdfa; // FPdfPdfa deklariše TPdfAValidationIssue
function AnnotationAppearancesAreClean(Pdf: TPdf): Boolean;
var
Report: TPdfAValidationResult;
begin
// Validira dokument trenutno učitan u Pdf, uključujući izmene
// načinjene kroz Pdf.Annotation[] od otvaranja
Report := Pdf.ValidatePdfA;
Result := not (pvaiAnnotationApDictViolation in Report.Issues);
end;
Ista disciplina važi za svaki panel koji boji ili anotira stranice radi pregleda, workflow koji pokriva građenje Delphi workflow-a za reviziju anotacija sa PDFium Component: zapis je snimak onoga što je engine mogao da pročita, i sentinel koji sami niste postavili trebalo bi da putuje nazad nepromenjen. Kompletan annotation API, PDF/A preflight i nativni PDFium engine stižu zajedno u PDFium Component za Delphi, C++Builder i Lazarus