I PDFium Component før v3.121.1 kunne lesing av en annotering gjennom TPdf.Annotation[] og tildeling av recorden tilbake legge til tomme /R- og /D-oppføringer i /AP appearance-ordboken dens, selv når originalen bare bar /N. PDF/A-validatorer avviser den ordboken. Siden v3.121.1 rapporterer getteren bare et appearance den faktisk leste, så en uendret rundtur skriver ingenting nytt. Feilen er verdt å forstå i detalj, for den vanlige utløseren er en fiks ment å gjøre en fil mer standardkonform, ikke mindre
Hva går galt når du skriver en annotering tilbake uendret?
Det korte svaret: annoteringen får appearance-strømmer den aldri har hatt, og en fil som passerte PDF/A-validering før redigeringen din, feiler den etterpå. Det typiske scenariet går slik. Et kundearkiv kommer med firkant- og tekst-annoteringer som mangler Print-flagget, PDF/A krever at hver annotering skrives ut, så du løkker over sidene, legger til afPrint og tildeler hver record tilbake. Ingenting i den koden rører appearances. Recorden fra TPdf.Annotation[] er en TPdfAnnotation, og SetAnnotationData skriver hvert felt hvis Has*-sentinel er satt, som er nøyaktig hvordan HasContents / ContentsText-parene er ment å fungere. Problemet var at getteren satte HasAppearanceRollover og HasAppearanceDown til True med tomme strenger for moduser som ikke fantes, og setteren skrev pliktoppfyllende to tomme strømmer:
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ør v3.121.1 skrev denne tildelingen også tomme /AP/R og
// /AP/D-strømmer når kildeannoteringen bare hadde /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 definerer appearance-ordboken med tre oppføringer: /N for normalutseendet, /R for rollover og /D for down. /R og /D er valgfrie, og når de mangler, faller et visningsprogram tilbake på /N. En tom /R-strøm er ikke fraværende. Det er en gyldig strøm som maler ingenting, så et visningsprogram som ærer rollover-appearances viser et blankt rektangel i det øyeblikket pekeren beveger seg over annoteringen. PDF/A er strengere: ISO 19005-1 (med Corrigendum 2) og ISO 19005-2 / 19005-3 tillater bare /N i en annoterings appearance-ordbok. veraPDF rapporterer den rundturte filen under regel 6.5.3-4 for PDF/A-1 og regel 6.3.3-2 for PDF/A-2 og PDF/A-3, og den innebygde TPdf.ValidatePdfA lister den opp som pvaiAnnotationApDictViolation. Redigeringen som la til Print-flagget for å tilfredsstille ett ledd i standarden, brukte et annet
Hvorfor gir FPDFAnnot_GetAP 2 for et manglende appearance?
PDFium gir aldri null fra FPDFAnnot_GetAP, selv når den etterspurte appearance-strømmen ikke finnes. Funksjonen følger det vanlige PDFium to-kall-mønsteret: gi en nil-buffer for å få den nødvendige størrelsen i byte, alloker, og kall så igjen for å kopiere UTF-16LE-tekst. Størrelsen inkluderer alltid UTF-16-terminatoren, så en manglende strøm rapporterer 2 byte, en tom streng pluss terminatoren. Getteren før v3.121.1 testet ByteLength >= SizeOf(FPDF_WCHAR), en sjekk hvert kall passerer, så alle tre HasAppearance*-flaggene kom tilbake True for enhver annotering med noe appearance i det hele tatt. En rundtur gjennom recorden ba så FPDFAnnot_SetAP om å lagre en tom streng for hver modus, og PDFium lagde strømmen for å holde den. Ikke noe unntak, ingen advarsel, og den synlige siden så identisk ut, noe som er grunnen til at defekten dukket opp i en veraPDF-fixture i stedet for i en viser
Hvordan v3.121.1 avgjør at et appearance finnes
ReadAppearance, hjelperen inne i GetPageAnnotation som fyller AppearanceNormal, AppearanceRollover og AppearanceDown, behandler nå et resultat som innhold bare når det bærer minst ett tegn utover terminatoren. Det første kallet må gi mer enn SizeOf(FPDF_WCHAR) byte og et jevent antall byte, siden en oddetallslengde ikke kan være UTF-16. Det andre kallet, som faktisk kopierer teksten, valideres igjen: en returnert lengde på 2 eller mindre, eller en større enn bufferen som ble allokert, nullstiller HasValue til False og etterlater strengen tom. På skrivesiden er ingenting endret. SetAnnotationData kaller fortsatt FPDFAnnot_SetAP bare for moduser hvis HasAppearance*-flagg er True, så en record lest fra en annotering som bare har /N, skriver nå tilbake bare /N. Regresjons-fixturen dekker begge retninger: en firkant-annotering med et normalt appearance, lest og skrevet tilbake uendret, passerer PDF/A-1b, PDF/A-2b og PDF/A-3b, mens samme annotering med Print-flagget fjernet feiler på den forventede flaggregelen og ingenting annet
Manglende og tomme strømmer ser identiske ut, så getteren forblir konservativ
Det opprinnelige API-et kan ikke skille en manglende appearance-strøm fra én som finnes men er tom, og PDFium Component later som noe annet. Begge tilfeller gir de samme 2 bytene fra FPDFAnnot_GetAP, så begge leses tilbake som HasAppearanceRollover = False med en tom AppearanceRollover. Det har to konsekvenser du bør designe rundt. For det første betyr en False-sentinel «intet innhold ble lest, så en skriving tilbake lar denne modusen være i fred», ikke «/R-nøkkelen mangler i ordboken». For det andre kan ikke recorden oppdage en tom strøm som allerede er i filen: et dokument skadet av et eldre bygg eller av et annet verktøy leses tilbake rent, og å tildele recorden tilbake reparerer det verken eller gjør det verre. For å finne de filene trenger du en bytenivåsjekk, og det er TPdf.ValidatePdfA og PDF/A preflight-valideringsarbeidsflyten med PDFium Component til for
Hvordan tømmer du et appearance med vilje?
Du setter sentinel-en eksplisitt og sender en tom streng; setteren skriver den. Å blokkere tomme strenger i SetAnnotationData ville vært den grove fiksen for denne feilen, men den ville også knekke kallere som tømmer et appearance med vilje, samme kontrakt HasContents og HasAuthor følger for tekst. Så fiksen bor helt i getteren, og setteren fortsetter å ære det kalleren ber om:
// Erstatt rollover-appearance-en, tøm den så igjen
A := Pdf.Annotation[0];
A.HasAppearanceRollover := True;
A.AppearanceRollover := 'q Q';
Pdf.Annotation[0] := A;
A := Pdf.Annotation[0];
// A.HasAppearanceRollover er True og teksten overlever rundturen som 'q Q'
A.HasAppearanceRollover := True; // gjenopprett intensjonen eksplisitt
A.AppearanceRollover := ''; // skriv en tom strøm med vilje
Pdf.Annotation[0] := A;
A := Pdf.Annotation[0];
// Leses tilbake som HasAppearanceRollover = False med en tom streng:
// en tom strøm og en manglende er uatskelige her
Husk at en eksplisitt tømt /R eller /D fortsatt teller som en ekstra nøkkel under PDF/A-reglene sitert over. Er målet en arkivprofil, er det å skrive en ikke-tom /N og la de to andre modusene være urørt, den eneste formen som validerer. Enhver arbeidsflyt som flytter annoteringer mellom dokumenter, som XFDF-eksport og -import med PDFium Component, bør følge samme regel: kopier modusene kilden faktisk hadde og la resten av sentinel-ene være False
Et les-modifiser-skriv-mønster som holder seg PDF/A-sikker
Oppgrader til v3.121.1 eller nyere, la appearance-sentinelene være nøyaktig som getteren ga dem tilbake, og valider den lagrede filen før du sender den ut. Etter som en foreldet tom strøm leses tilbake som fraværende, må verifiseringssteget se på det serialiserte dokumentet i stedet for på recorden, og det er billig nok å kjøre etter hver batch:
uses
PDFium, FPdfPdfa; // FPdfPdfa deklarerer TPdfAValidationIssue
function AnnotationAppearancesAreClean(Pdf: TPdf): Boolean;
var
Report: TPdfAValidationResult;
begin
// Validerer dokumentet som er lastet i Pdf nå, inkludert redigeringer
// gjort gjennom Pdf.Annotation[] etter at det ble åpnet
Report := Pdf.ValidatePdfA;
Result := not (pvaiAnnotationApDictViolation in Report.Issues);
end;
Samme disiplin gjelder for ethvert panel som farger om eller annoterer sider til gjennomgang, en arbeidsflyt dekket i å bygge en arbeidsflyt for annoteringsgjennomgang i Delphi med PDFium Component: recorden er et øyeblikksbilde av hva motoren kunne lese, og en sentinel du ikke satte selv, bør reise tilbake uendret. Hele annoterings-API-en, PDF/A preflight og den opprinnelige PDFium-motoren leveres sammen i PDFium Component for Delphi, C++Builder og Lazarus