In PDFium Component vóór v3.121.1 kon het lezen van een annotatie via TPdf.Annotation[] en het terugschrijven van het record lege /R- en /D-entries toevoegen aan zijn /AP-appearance-dictionary, zelfs wanneer het origineel alleen /N droeg. PDF/A-validators wijzen die dictionary af. Sinds v3.121.1 meldt de getter alleen een appearance die hij daadwerkelijk heeft gelezen, dus een ongewijzigde round trip schrijft niets nieuws weg. De fout is het detail waard, want de gebruikelijke aanleiding was een fix die een bestand juist conformeerder moest maken, niet minder
Wat gaat er mis wanneer u een annotatie ongewijzigd terugschrijft?
Het korte antwoord: de annotatie krijgt appearance-streams die ze nooit had, en een bestand dat vóór uw bewerking door de PDF/A-validatie kwam, valt er daarna doorheen. Het typische scenario verloopt zo. Een klantarchief komt binnen met vierkant- en tekstannotaties zonder de Print-vlag, PDF/A eist dat elke annotatie print, dus u loopt de pagina's af, voegt afPrint toe en schrijft elk record terug. Niets in die code raakt appearances aan. Het record uit TPdf.Annotation[] is een TPdfAnnotation, en SetAnnotationData schrijft elk veld weg waarvan de Has*-sentinel staat, precies zoals de HasContents / ContentsText-paren bedoeld zijn. Het probleem was dat de getter HasAppearanceRollover en HasAppearanceDown op True zette met lege strings voor modi die niet bestonden, en de setter schreef plichtsgetrouw twee lege streams weg:
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];
// Vóór v3.121.1 schreef deze toekenning ook lege /AP/R- en
// /AP/D-streams weg wanneer de bronannotatie alleen /AP/N had
Pdf.Annotation[I] := A;
end;
end;
end;
Pdf.SaveAs(ChangeFileExt(FileName, '.printable.pdf'));
finally
Pdf.Free;
end;
end;
ISO 32000-1 §12.5.5 definieert de appearance-dictionary met drie entries: /N voor de normale appearance, /R voor rollover en /D voor down. /R en /D zijn optioneel, en wanneer ze ontbreken valt een viewer terug op /N. Een lege /R-stream is echter niet afwezig. Het is een geldige stream die niets tekent, dus een viewer die rollover-appearances eert toont een leeg vierkant zodra de aanwijzer over de annotatie beweegt. PDF/A is nog strenger: ISO 19005-1 (met Corrigendum 2) en ISO 19005-2 / 19005-3 staan in een annotatie-appearance-dictionary alleen /N toe. veraPDF meldt het teruggeschreven bestand onder regel 6.5.3-4 voor PDF/A-1 en regel 6.3.3-2 voor PDF/A-2 en PDF/A-3, en de ingebouwde TPdf.ValidatePdfA zet het op de lijst als pvaiAnnotationApDictViolation. De bewerking die de Print-vlag toevoegde om aan één clausule van de standaard te voldoen, brak een andere
Waarom geeft FPDFAnnot_GetAP 2 terug voor een ontbrekende appearance?
PDFium geeft nooit nul terug uit FPDFAnnot_GetAP, ook niet wanneer de gevraagde appearance-stream niet bestaat. De functie volgt het gebruikelijke PDFium-twee-aanroepen-patroon: geef een nil-buffer door om de benodigde grootte in bytes te krijgen, alloceer, roep dan opnieuw aan om UTF-16LE-tekst te kopiëren. De grootte omvat altijd de UTF-16-beëindiger, dus een ontbrekende stream meldt 2 bytes, een lege string plus zijn beëindiger. De getter van vóór v3.121.1 toetste ByteLength >= SizeOf(FPDF_WCHAR), een controle die elke aanroep haalt, dus alle drie de HasAppearance*-vlaggen kwamen True terug voor elke annotatie met überhaupt een appearance. Een round trip door het record vroeg FPDFAnnot_SetAP toen per modus een lege string op te slaan, en PDFium maakte de stream om haar te huisvesten. Geen exception, geen waarschuwing, en de zichtbare pagina zag er identiek uit, wat verklaart waarom het defect in een veraPDF-fixture opdook en niet in een viewer
Hoe v3.121.1 beslist dat een appearance bestaat
ReadAppearance, de helper in GetPageAnnotation die AppearanceNormal, AppearanceRollover en AppearanceDown vult, behandelt een resultaat nu alleen als inhoud wanneer het ten minste één teken voorbij de beëindiger draagt. De eerste aanroep moet meer dan SizeOf(FPDF_WCHAR) bytes teruggeven en een even aantal bytes, want een oneven lengte kan geen UTF-16 zijn. De tweede aanroep, die de tekst werkelijk kopieert, wordt opnieuw gevalideerd: een teruggegeven lengte van 2 of minder, of één groter dan de toegewezen buffer, zet HasValue terug op False en laat de string leeg. Aan de schrijfkant is niets veranderd. SetAnnotationData roept FPDFAnnot_SetAP nog steeds alleen aan voor modi waarvan de HasAppearance*-vlag True is, dus een record gelezen uit een annotatie die alleen /N heeft schrijft nu alleen /N terug. De regressiefixture dekt beide kanten: een vierkant-annotatie met een normale appearance, gelezen en ongewijzigd teruggeschreven, haalt PDF/A-1b, PDF/A-2b en PDF/A-3b, terwijl dezelfde annotatie met verwijderde Print-vlag faalt op de verwachte vlagregel en op niets anders
Ontbrekende en lege streams zien er identiek uit, dus de getter blijft conservatief
De native API kan een ontbrekende appearance-stream niet onderscheiden van een die bestaat maar leeg is, en dat suggereert PDFium Component ook niet. Beide gevallen geven dezelfde 2 bytes terug uit FPDFAnnot_GetAP, dus beide lezen terug als HasAppearanceRollover = False met een lege AppearanceRollover. Dat heeft twee gevolgen waar u uw ontwerp op moet inrichten. Ten eerste betekent een False-sentinel "er is geen inhoud gelezen, dus een terugschrijfactie laat deze modus met rust", niet "de /R-sleutel ontbreekt in de dictionary". Ten tweede kan het record een lege stream die al in het bestand zit niet detecteren: een document dat door een oudere build of een ander hulpmiddel is beschadigd leest schoon terug, en het record terugschrijven herstelt hem niet maar maakt het ook niet erger. Om die bestanden te vinden heeft u een controle op byteniveau nodig, en daarvoor bestaan TPdf.ValidatePdfA en de PDF/A-preflight-validatieworkflow met PDFium Component
Hoe wist u een appearance met opzet?
U zet de sentinel expliciet en geeft een lege string door; de setter schrijft haar weg. Lege strings blokkeren in SetAnnotationData was de botte fix voor deze bug geweest, maar hij zou ook aanroepers breken die een appearance bewust wissen, hetzelfde contract dat HasContents en HasAuthor voor tekst volgen. Dus de fix woont volledig in de getter, en de setter blijft doen wat de aanroeper vraagt:
// Vervang de rollover-appearance en wis haar daarna weer
A := Pdf.Annotation[0];
A.HasAppearanceRollover := True;
A.AppearanceRollover := 'q Q';
Pdf.Annotation[0] := A;
A := Pdf.Annotation[0];
// A.HasAppearanceRollover is True en de tekst round-tript als 'q Q'
A.HasAppearanceRollover := True; // bevestig de bedoeling expliciet opnieuw
A.AppearanceRollover := ''; // schrijf bewust een lege stream weg
Pdf.Annotation[0] := A;
A := Pdf.Annotation[0];
// Leest terug als HasAppearanceRollover = False met een lege string:
// een lege stream en een ontbrekende zijn hier niet te onderscheiden
Bedenk dat een bewust geleegde /R of /D onder de hierboven geciteerde PDF/A-regels nog steeds als extra sleutel telt. Is het doel een archiefprofiel, dan is het schrijven van een niet-lege /N en de andere twee modi met rust laten de enige vorm die valideert. Elke workflow die annotaties tussen documenten verplaatst, zoals XFDF-export en -import met PDFium Component, zou dezelfde regel moeten volgen: kopieer de modi die de bron werkelijk had en laat de overige sentinels op False
Een read-modify-write-patroon dat PDF/A-veilig blijft
Upgrade naar v3.121.1 of later, laat de appearance-sentinels precies zoals de getter ze teruggaf, en valideer het opgeslagen bestand voordat u het verscheept. Omdat een achtergebleven lege stream als afwezig terugleest, moet de verificatiestap naar het geserialiseerde document kijken in plaats van naar het record, en die is goedkoop genoeg om na elke batch te draaien:
uses
PDFium, FPdfPdfa; // FPdfPdfa declareert TPdfAValidationIssue
function AnnotationAppearancesAreClean(Pdf: TPdf): Boolean;
var
Report: TPdfAValidationResult;
begin
// Valideert het document dat nu in Pdf is geladen, inclusief bewerkingen
// gedaan via Pdf.Annotation[] sinds het werd geopend
Report := Pdf.ValidatePdfA;
Result := not (pvaiAnnotationApDictViolation in Report.Issues);
end;
Dezelfde discipline geldt voor elk paneel dat pagina's van kleur voorziet of van aantekeningen voor review, een workflow uit het bouwen van een Delphi-annotatie-reviewworkflow met PDFium Component: het record is een momentopname van wat de motor kon lezen, en een sentinel die u niet zelf heeft gezet moet ongewijzigd terugreizen. De volledige annotatie-API, PDF/A-preflight en de native PDFium-engine verschepen samen in PDFium Component for Delphi, C++Builder and Lazarus