V PDFium Component pred v3.121.1 mohlo prečítanie anotácie cez TPdf.Annotation[] a priradenie recordu späť pridať do jej slovníka vzhľadov /AP prázdne položky /R a /D, aj keď pôvodný niesol len /N. Validátory PDF/A taký slovník odmietajú. Od v3.121.1 getter hlási len vzhľad, ktorý naozaj prečítal, takže nezmenený round trip nezapisuje nič nové. Toto zlyhanie stojí za detailné pochopenie, pretože obvyklý spúšťač je oprava, ktorá má súbor spraviť viac súladným, nie menej
Čo sa pokazí, keď anotáciu zapíšete späť nezmenenú?
Krátka odpoveď: anotácia získa streamy vzhľadov, ktoré nikdy nemala, a súbor, ktorý pred vašou úpravou prešiel validáciou PDF/A, ju po nej neprejde. Typický scenár beží takto. Zákaznícky archív príde so štvorcovými a textovými anotáciami bez flagu Print, PDF/A vyžaduje, aby tlačila každá anotácia, takže prejdete stránky, pridáte afPrint a každý record priradíte späť. Nič v tom kóde sa nedotýka vzhľadov. Record z TPdf.Annotation[] je TPdfAnnotation a SetAnnotationData zapisuje každé pole, ktorého sentinel Has* je nastavený, presne takto majú fungovať páry HasContents / ContentsText. Problém bol v tom, že getter nastavil HasAppearanceRollover a HasAppearanceDown na True s prázdnymi reťazcami pre režimy, ktoré neexistovali, a setter ich poslušne zapísal ako dva prázdne streamy:
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];
// Pred v3.121.1 toto priradenie zapisovalo aj prázdne /AP/R a
// /AP/D streamy, keď zdrojová anotácia mala len /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 definuje slovník vzhľadov s troma položkami: /N pre normálny vzhľad, /R pre rollover a /D pre down. /R a /D sú voliteľné a keď chýbajú, viewer prepadne na /N. Prázdny stream /R ale nie je neprítomný. Je to platný stream, ktorý nič nenamaľuje, takže viewer rešpektujúci vzhľady rollover ukáže prázdny obdĺžnik v momente, keď sa kurzor nad anotáciou pohne. PDF/A je ešte prísnejšie: ISO 19005-1 (s Corrigendum 2) a ISO 19005-2 / 19005-3 pripúšťajú v slovníku vzhľadov anotácie len /N. veraPDF hlási round-tripovaný súbor pod pravidlom 6.5.3-4 pre PDF/A-1 a pravidlom 6.3.3-2 pre PDF/A-2 a PDF/A-3 a vstavané TPdf.ValidatePdfA ho vypíše ako pvaiAnnotationApDictViolation. Úprava, ktorá pridala flag Print kvôli jednej klauzule štandardu, pokorila inú
Prečo vracia FPDFAnnot_GetAP 2 pre chýbajúci vzhľad?
PDFium nikdy nevracia z FPDFAnnot_GetAP nulu, aj keď požadovaný stream vzhľadu neexistuje. Funkcia nasleduje obvyklý PDFium dvojfázový vzor: pošlite nil buffer, aby ste dostali potrebnú veľkosť v bajtoch, alokujte a zavolajte znova na skopírovanie UTF-16LE textu. Veľkosť vždy zahŕňa terminátor UTF-16, takže chýbajúci stream hlási 2 bajty, prázdny reťazec plus terminátor. Getter pred v3.121.1 testoval ByteLength >= SizeOf(FPDF_WCHAR), kontrolu, ktorou prejde každé volanie, takže všetky tri flagy HasAppearance* sa vrátili True pre akúkoľvek anotáciu s akýmkoľvek vzhľadom. Round trip cez record potom požiadal FPDFAnnot_SetAP o uloženie prázdneho reťazca pre každý režim a PDFium vytvoril stream, ktorý ho má niesť. Žiadna výnimka, žiadne varovanie a viditeľná stránka vyzerala rovnako, preto sa defekt objavil vo fixtúre pre veraPDF, nie vo vieweri
Ako v3.121.1 rozhoduje, že vzhľad existuje
ReadAppearance, helper vnútri GetPageAnnotation, ktorý plní AppearanceNormal, AppearanceRollover a AppearanceDown, teraz berie výsledok ako obsah len vtedy, keď nesie aspoň jeden znak nad rámec terminátoru. Prvé volanie musí vrátiť viac než SizeOf(FPDF_WCHAR) bajtov a párny počet bajtov, lebo nepárna dĺžka nemôže byť UTF-16. Druhé volanie, ktoré text naozaj kopíruje, sa validuje znova: vrátená dĺžka 2 a menej alebo väčšia než alokovaný buffer resetuje HasValue na False a nechá reťazec prázdny. Na strane zápisu sa nezmenilo nič. SetAnnotationData stále volá FPDFAnnot_SetAP len pre režimy s flagom HasAppearance* True, takže record prečítaný z anotácie, ktorá má len /N, teraz zapisuje späť len /N. Regresná fixtúra pokrýva oba smery: štvorcová anotácia s normálnym vzhľadom, prečítaná a zapísaná späť bez zmeny, prejde PDF/A-1b, PDF/A-2b aj PDF/A-3b, zatiaľ čo tá istá anotácia s odobratým flagom Print padne na očakávanom pravidle flagu a na ničom inom
Chýbajúce a prázdne streamy vyzerajú rovnako, preto getter ostáva konzervatívny
Natívne API nedokáže odlíšiť chýbajúci stream vzhľadu od takého, ktorý existuje, ale je prázdny, a PDFium Component netvrdí inak. Oba prípady vrátia z FPDFAnnot_GetAP tých istých 2 bajtov, takže oba sa načítajú ako HasAppearanceRollover = False s prázdnym AppearanceRollover. Z toho vyplievajú dva dôsledky, na ktoré si máte návrh pripraviť. Po prvé, sentinel False znamená "nebol prečítaný žiadny obsah, takže zápis späť nechá tento režim na pokoji", nie "kľúč /R chýba v slovníku". Po druhé, record nedokáže odhaliť prázdny stream, ktorý už v súbore je: dokument poškodený starším buildom alebo iným nástrojom sa načíta čistý a priradenie recordu späť ho ani nevylieči, ani nezhorší. Na ich nájdenie potrebujete kontrolu na úrovni bajtov, na čo sú TPdf.ValidatePdfA a workflow PDF/A preflight validácie s PDFium Component
Ako vymazať vzhľad zámerne?
Nastavíte sentinel explicitne a podáte prázdny reťazec; setter ho zapíše. Blokovať prázdne reťazce v SetAnnotationData by bola tupá oprava tohto bugu, ale rozbila by aj volajúcich, ktorí vzhľad mažú zámerne, tú istú zmluvu, ktorú pri texte dodržiavajú HasContents a HasAuthor. Oprava preto žije celá v getteri a setter naďalej plní, o čo volajúci poprosí:
// Nahraďte rollover vzhľad a potom ho znovu zmažte
A := Pdf.Annotation[0];
A.HasAppearanceRollover := True;
A.AppearanceRollover := 'q Q';
Pdf.Annotation[0] := A;
A := Pdf.Annotation[0];
// A.HasAppearanceRollover je True a text prejde round tripom ako 'q Q'
A.HasAppearanceRollover := True; // výslovne potvrďte úmysel
A.AppearanceRollover := ''; // zámerne zapíšte prázdny stream
Pdf.Annotation[0] := A;
A := Pdf.Annotation[0];
// Načíta sa ako HasAppearanceRollover = False s prázdnym reťazcom:
// prázdny stream a chýbajúci sú tu nerozlíšiteľné
Majte na pamäti, že výslovne vyprázdnené /R alebo /D sa pod pravidlami PDF/A citovanými vyššie stále počíta ako extra kľúč. Ak je cieľom archívny profil, zápis neprázdneho /N a nechanie ostatných dvoch režimov na pokoji je jediný tvar, ktorý prejde validáciou. Každý workflow presúvajúci anotácie medzi dokumentmi, ako export a import XFDF s PDFium Component, by mal nasledovať to isté pravidlo: kopírujte režimy, ktoré zdroj naozaj mal, a ostatné sentinely nechajte False
Vzor read-modify-write, ktorý ostáva v bezpečí PDF/A
Prejdite na v3.121.1 alebo novšiu, nechajte sentinely vzhľadov presne tak, ako ich getter vrátil, a pred odoslaním validujte uložený súbor. Keďže zastaralý prázdny stream sa načíta ako neprítomný, overovací krok sa musí pozrieť na serializovaný dokument, nie na record, a je dost lacný na to, aby bežal po každej dávke:
uses
PDFium, FPdfPdfa; // FPdfPdfa deklaruje TPdfAValidationIssue
function AnnotationAppearancesAreClean(Pdf: TPdf): Boolean;
var
Report: TPdfAValidationResult;
begin
// Validuje dokument práve načítaný v Pdf, vrátane úprav
// urobených cez Pdf.Annotation[] od jeho otvorenia
Report := Pdf.ValidatePdfA;
Result := not (pvaiAnnotationApDictViolation in Report.Issues);
end;
Tá istá disciplína platí pre každý panel, ktorý farbí alebo anotuje strany na revíziu, workflow popísaný v stavbe Delphi workflow na revíziu anotácií s PDFium Component: record je snímka toho, čo engine dokázal prečítať, a sentinel, ktorý ste si nenastavili sami, má cestovať späť nezmenený. Plné anotačné API, PDF/A preflight aj natívny engine PDFium idú spolu v PDFium Component pre Delphi, C++Builder a Lazarus