I PDFium Component före v3.121.1 kunde läsning av en annotation genom TPdf.Annotation[] och tilldelning av posten tillbaka lägga till tomma /R- och /D-poster i dess /AP-appearance-ordlista, även när originalet bar bara /N. PDF/A-validatorer avvisar den ordlistan. Sedan v3.121.1 rapporterar gettern bara en appearance den faktiskt läst, så en oförändrad rundtur skriver ingenting nytt. Felet är värt att förstå i detalj, för den vanliga utlösaren är en fix som var tänkt att få filen att bättre leva upp till standarden, inte tvärtom
Vad går fel när du skriver tillbaka en annotation oförändrad?
Det korta svaret: annotationen får appearance-strömmar den aldrig haft, och en fil som klarade PDF/A-valideringen före din redigering faller på den efteråt. Det typiska scenariot går till så här. Ett kundarkiv anländer med square- och text-annotationer som saknar Print-flaggan, PDF/A kräver att varje annotation skrivs ut, så du loopar över sidorna, lägger till afPrint och tilldelar varje post tillbaka. Ingenting i den koden rör appearances. Posten från TPdf.Annotation[] är en TPdfAnnotation, och SetAnnotationData skriver varje fält vars Has*-sentinel är satt, vilket är exakt hur paren HasContents / ContentsText är tänkta att fungera. Problemet var att gettern satte HasAppearanceRollover och HasAppearanceDown till True med tomma strängar för lägen som inte fanns, och settern plikttrogen skrev två tomma strömmar:
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];
// Före v3.121.1 skrev denna tilldelning också tomma /AP/R- och
// /AP/D-strömmar när källannotationen bara hade /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 definierar appearance-ordlistan med tre poster: /N för normal appearance, /R för rollover och /D för down. /R och /D är valfria, och när de saknas faller en viewer tillbaka på /N. En tom /R-ström är dock inte frånvarande. Den är en giltig ström som ritar ingenting, så en viewer som hedrar rollover-appearances visar en tom rektangel i samma stund som pekaren rör sig över annotationen. PDF/A är strängare: ISO 19005-1 (med Corrigendum 2) och ISO 19005-2 / 19005-3 tillåter bara /N i en annotations appearance-ordlista. veraPDF rapporterar den rundturade filen under regel 6.5.3-4 för PDF/A-1 och regel 6.3.3-2 för PDF/A-2 och PDF/A-3, och inbyggda TPdf.ValidatePdfA listar den som pvaiAnnotationApDictViolation. Redigeringen som lade till Print-flaggan för att tillfredsställa en klausul i standarden bröt en annan
Varför returnerar FPDFAnnot_GetAP 2 för en saknad appearance?
PDFium returnerar aldrig noll från FPDFAnnot_GetAP, inte ens när den efterfrågade appearance-strömmen inte finns. Funktionen följer det vanliga PDFium-mönstret med två anrop: skicka en nil-buffert för att få den erforderliga storleken i byte, allokera, anropa sedan igen för att kopiera UTF-16LE-text. Storleken inkluderar alltid UTF-16-terminatorn, så en saknad ström rapporterar 2 byte, en tom sträng plus dess terminator. Gettern före v3.121.1 testade ByteLength >= SizeOf(FPDF_WCHAR), en kontroll som varje anrop klarar, så alla tre HasAppearance*-flaggor kom tillbaka True för varje annotation med någon appearance alls. En rundtur genom posten bad sedan FPDFAnnot_SetAP lagra en tom sträng för varje läge, och PDFium skapade strömmen för att hysa den. Inget undantag, ingen varning, och den synliga sidan såg identisk ut, vilket är därför felet dök upp i en veraPDF-fixture i stället för i en viewer
Hur v3.121.1 avgör att en appearance finns
ReadAppearance, hjälparen inne i GetPageAnnotation som fyller AppearanceNormal, AppearanceRollover och AppearanceDown, behandlar nu ett resultat som innehåll bara när det bär minst ett tecken utöver terminatorn. Det första anropet måste returnera mer än SizeOf(FPDF_WCHAR) byte och ett jämnt byteantal, eftersom en udda längd inte kan vara UTF-16. Det andra anropet, som faktiskt kopierar texten, valideras igen: en returnerad längd på 2 eller mindre, eller en större än den allokerade bufferten, återställer HasValue till False och lämnar strängen tom. På skrivsidan ändrades ingenting. SetAnnotationData anropar fortfarande FPDFAnnot_SetAP bara för lägen vars HasAppearance*-flagga är True, så en post läst från en annotation som bara har /N skriver nu tillbaka bara /N. Regressionfixturen täcker båda riktningarna: en square-annotation med en normal appearance, läst och skriven tillbaka oförändrad, klarar PDF/A-1b, PDF/A-2b och PDF/A-3b, medan samma annotation utan sin Print-flagga faller på den förväntade flaggregeln och inget annat
Saknade och tomma strömmar ser identiska ut, så gettern förblir konservativ
Det nativa API:t kan inte skilja en saknad appearance-ström från en som finns men är tom, och PDFium Component låtsas inte annat. Båda fallen returnerar samma 2 byte från FPDFAnnot_GetAP, så båda läses tillbaka som HasAppearanceRollover = False med en tom AppearanceRollover. Det har två konsekvenser du bör designa kring. För det första betyder en False-sentinel "inget innehåll lästes, så en tillbakaskrivning lämnar det här läget orört", inte "nyckeln /R saknas i ordlistan". För det andra kan posten inte upptäcka en tom ström som redan finns i filen: ett dokument skadat av ett äldre bygge eller av annat verktyg läses tillbaka rent, och att tilldela posten tillbaka varken reparerar eller förvärrar det. För att hitta de filerna behöver du en kontroll på bytenivå, vilket är vad TPdf.ValidatePdfA och PDF/A-preflight-valideringsflödet med PDFium Component är till för
Hur rensar du en appearance medvetet?
Du sätter sentineln explicit och skickar en tom sträng; settern skriver den. Att blockera tomma strängar i SetAnnotationData hade varit den grova fixen för den här buggen, men den skulle också bryta anropare som rensar en appearance med flit, samma kontrakt som HasContents och HasAuthor följer för text. Så fixen bor helt i gettern, och settern fortsätter hedra vad anroparen ber om:
// Ersätt rollover-appearancen, rensa den sedan igen
A := Pdf.Annotation[0];
A.HasAppearanceRollover := True;
A.AppearanceRollover := 'q Q';
Pdf.Annotation[0] := A;
A := Pdf.Annotation[0];
// A.HasAppearanceRollover är True och texten klarar rundturen som 'q Q'
A.HasAppearanceRollover := True; // hävda avsikten explicit igen
A.AppearanceRollover := ''; // skriv en tom ström med flit
Pdf.Annotation[0] := A;
A := Pdf.Annotation[0];
// Läses tillbaka som HasAppearanceRollover = False med en tom sträng:
// en tom ström och en saknad är otskiljbara här
Kom ihåg att en explicit tömd /R eller /D fortfarande räknas som en extra nyckel under de PDF/A-regler som citerats ovan. Om målet är en arkivprofil är det enda utformning som validerar att skriva en icke-tom /N och lämna de två andra lägena orörda. Varenda flöde som flyttar annotationer mellan dokument, som XFDF-export och -import med PDFium Component, bör följa samma regel: kopiera de lägen källan faktiskt hade och lämna resten av sentinelna False
Ett read-modify-write-mönster som förblir PDF/A-säkert
Uppgradera till v3.121.1 eller senare, lämna appearance-sentinelsna exakt som gettern returnerade dem, och validera den sparade filen innan du skickar den. Eftersom en kvarlämnad tom ström läses tillbaka som frånvarande måste verifieringssteget titta på det serialiserade dokumentet i stället för på posten, och det är billigt nog att köra efter varje batch:
uses
PDFium, FPdfPdfa; // FPdfPdfa deklarerar TPdfAValidationIssue
function AnnotationAppearancesAreClean(Pdf: TPdf): Boolean;
var
Report: TPdfAValidationResult;
begin
// Validerar dokumentet som för närvarande är inläst i Pdf, inklusive ändringar
// gjorda genom Pdf.Annotation[] sedan det öppnades
Report := Pdf.ValidatePdfA;
Result := not (pvaiAnnotationApDictViolation in Report.Issues);
end;
Samma disciplin gäller alla paneler som färgar om eller annoterar sidor för granskning, ett flöde som tas upp i att bygga ett Delphi-granskningsflöde för annotationer med PDFium Component: posten är en ögonblicksbild av vad motorn kunde läsa, och en sentinel du inte själv satt bör färdas tillbaka oförändrad. Det fullständiga annotations-API:t, PDF/A-preflight och den nativa PDFium-motorn skeppas tillsammans i PDFium Component för Delphi, C++Builder och Lazarus