Je levert een converter die elk bestand als PDF/A-1b markeert, het recordsysteem van de klant neemt ze een jaar lang op, en dan draait een audit de hele batch door veraPDF waarbij een derde niet-conform terugkomt. Er is niets gecrasht, er is geen exception gegooid, de bestanden openen prima in elke viewer op je bureau. Ze voldoen gewoon niet aan de standaard die je erop hebt geplakt. Dat is de normale faalmodus voor archief-PDF, en daarom is "we hebben de vlag gezet" nooit hetzelfde als "het valideert"
Het eerste om te begrijpen over PDFium en PDF/A is dat de engine daar niets mee te maken heeft. PDFium rendert, parseert en schrijft PDF, maar de publieke API heeft geen ConvertToPDFA, geen OutputIntent-writer, geen XMP-API. Alles rond archiefconformiteit, het XMP-pakket, de OutputIntent en zijn ICC-profiel, de catalogmarkers, de validatie, zit in PDFiumPas zelf, in een ongeveer 2000-regelige pure-Pascal unit (FPdfPdfa.pas) die de opgeslagen bytes parset en ze via een incremental update herschrijft. Weten waar het werk gebeurt, vertelt je waar de bugs zitten, en die zitten niet in PDFium
Wat PDF/A eigenlijk eist, en waar het wringt
PDF/A is geen enkel formaat. ISO 19005 definieert drie delen, PDF/A-1, -2 en -3, en daarbinnen conformiteitsniveaus die verschillende dingen beloven. Niveau B, basic, garandeert alleen dat het visuele uiterlijk reproduceerbaar is. Niveau A, accessible, voegt daar een getagde structuurboom en Unicode-mapping aan toe. Niveau U, dat alleen bestaat voor delen 2 en 3, zit ertussenin: betrouwbare Unicode-tekst zonder de volledige structuurboom. ISO 19005-1 kent geen Level U, een beperking die de bibliotheek direct encodeert
Een handvol regels van het formaat zijn in de praktijk de pijnpunten. Encryptie is simpelweg verboden (ISO 19005-1 §6.1.3 en opvolgers): een PDF/A-bestand mag geen /Encrypt-dictionary dragen. Het document moet een output rendering condition declareren via een OutputIntent waarvan de bestemming een geldig ICC-profiel is (§6.2.3.2). De conformiteitsclaim zelf moet verschijnen als XMP-metadata onder het PDF/A-identificatieschema. Level A vereist daarnaast §6.8 logische structuur, de tag tree die het document machineleesbaar maakt. Mis je een van deze punten, dan wijst een conformance verifier het bestand af, ook al rendert het perfect
De ene aanroep die een archief produceert
PDFiumPas legt de hele pijplijn achter TPdf.SaveAsPdfA bloot. De eenvoudige overload neemt één doelconformiteit en valt standaard terug op PDF/A-1b, wat voor het gebruikelijke geval "maak dit voor altijd renderbaar" de juiste keuze is
Onder de motorkap is dit een tweestapsbeweging. SaveAsPdfA vraagt eerst aan PDFium om het document te serialiseren met FPDF_SaveAsCopy, en geeft die bytestring daarna aan InjectPdfAMarkers, dat de XMP-metadata, de sRGB OutputIntent met ingesloten ICC-profiel en een herschreven catalog als incremental update toevoegt. De bron wordt vanaf positie nul gelezen en de bestemming vanaf positie nul geschreven; de originele objectboom blijft intact en de markers schuiven achter de bestaande %%EOF aan. Als je bytes nodig hebt in plaats van een bestand, neemt SaveAsPdfAToStream een TStream met dezelfde opties
De conformiteit kiezen met het options-record
Om een specifiek deel en niveau te kiezen, geef je een TPdfASaveOptions-record door. Het Conformance-veld neemt een TPdfAConformance-waarde. De enumeratie dekt elke geldige combinatie en niets anders: pac1b, pac1a voor deel 1; pac2b, pac2u, pac2a voor deel 2; pac3b, pac3u, pac3a voor deel 3, plus pacUnknown en pacNone voor de validatiekant. Er bestaat geen pac1u, omdat dat niveau niet in de standaard voorkomt
Het grootste deel van het record mag leeg blijven. Laat Title, Author, Subject, Keywords, Creator en Producer leeg, en SaveAsPdfA vult ze automatisch uit het Info-dictionary van het document via FPDF_GetMetaText. Laat CreationDate en ModDate leeg, en het gebruikt de huidige UTC-tijd voor beide XMP-datums. Laat DocumentId en InstanceId leeg, en de bibliotheek vult ze vooraf met FPDF_GetFileIdentifier, met terugval op een deterministische ID die uit de bronbytes is afgeleid. Het ene veld dat je bewust wilt overschrijven is IccProfileData: leeg betekent het meegeleverde sRGB IEC61966-2.1-profiel, maar een CMYK- of grayscale-workflow moet zijn eigen profiel aanleveren
Waarom Level A degradeert, en waarom dat de eerlijke keuze is
Hier zit een subtiliteit die mensen verrast die van een vlag een garantie verwachten. Je kunt pac1a vragen op een document zonder tag tree, maar PDF/A-1a vereist §6.8 logische structuur, en de bibliotheek kan geen structuurboom uit een niet-getagd PDF toveren. In plaats van een bestand uit te geven dat Level A claimt maar het niet haalt, controleert SaveAsPdfA op een echte getagde structuur (/StructTreeRoot plus /MarkInfo met /Marked true) en verlaagt, als die ontbreekt, de claim: pac1a wordt pac1b, pac2a wordt pac2b, en zo verder over alle drie de delen. De interne helpers zijn PdfAIsLevelA en PdfADowngradeToLevelB
De redenering is de moeite waard om helder te zeggen: een bestand dat eerlijk meldt welk niveau het haalt, is nuttiger dan een bestand dat liegt over een niveau dat het niet haalt. Level U wordt anders behandeld. Echte Unicode-dekking detecteren zou een naïeve "heeft het /ToUnicode"-test vereisen, die legitieme documenten te ver zou degraderen (WinAnsi en vergelijkbare encoderingen zijn uitgezonderd), dus de save-kant geeft de U-claim uit zoals de aanroeper die heeft opgegeven en laat de afwijking aan de validatiekant rapporteren. Als je gegarandeerd een Level A-archief nodig hebt, tag het document voordat je het converteert; de converter zal geen structuur verzinnen die er niet is
De ICC-valkuil die alleen een echte validator vangt
Dit is de fout die de hardste les gaf, omdat de eigen checker van de bibliotheek hem wel accepteerde maar veraPDF, de referentievalidator voor ISO 19005, niet. PDF/A eist dat het destination profile van de OutputIntent een geldige ICCBased-stream is, en §6.2.3.2 laat een verifier die stream als een kleurspace valideren. Een ICCBased-stream moet /N declareren, het aantal kleurcomponenten. Een vroege versie van de injector schreef de ICC-streamdictionary alleen met /Length en zonder /N, en veraPDF wees het resultaat af met "The N entry (value null)... is missing"
Wat het verraderlijk maakte, is dat de afwijzing alleen optrad voor PDF/A-1b en -1a. De conformiteitsmodellen voor deel 2 en deel 3 voerden die specifieke controle op het destination profile niet uit, dus dezelfde geïnjecteerde structuur valideerde onder pac2b, pac3b en pac2u, maar faalde onder pac1b puur op de waarde van pdfaid:part. Een unit test kon dit nooit zien, omdat de eigen ValidatePdfACompliance van de bibliotheek alleen controleerde dat de sleutel /DestOutputProfile bestond, niet wat er in de streamdictionary zat. Interne tests bleven groen; echte archiefvalidatie faalde
De oplossing is IccComponentCount, die de data-color-space signature op offset 16 van de ICC-header leest en die op een componentenaantal mapt: GRAY is 1, RGB, Lab en XYZ zijn 3, CMYK is 4, en een onbekend profiel valt terug op 3. Dat aantal gaat als /N in de streamdictionary. Het wordt berekend, niet hard-coded op 3 gezet, zodat een aanroeper die via IccProfileData een CMYK- of grayscale-profiel aanlevert, nog steeds de juiste waarde krijgt. De bredere les is methodologisch: de in-library checker en een gezaghebbende validator hebben elk blinde vlekken, en PDF/A-uitvoer moet end to end getest worden tegen een referentie-implementatie zoals veraPDF in plaats van te vertrouwen op zelfchecks. Dezelfde incremental-update-discipline achter schone archieven wordt behandeld in validating compressed object and xref streams, wat belangrijk is omdat moderne PDF's die de injector verwerkt vaak op cross-reference streams zijn gebouwd
Encryptie, xref streams en andere randen
Omdat ISO 19005 encryptie verbiedt, haalt het save-pad die er eerst uit voordat er geschreven wordt. SaveAsPdfA past FPDF_REMOVE_SECURITY toe bij het serialiseren, zodat een versleutelde bron, geladen met zijn wachtwoord, onderweg naar het archief wordt ontsleuteld. Op een niet-versleuteld document is dat een no-op en verandert er niets. De omkering van diezelfde regel is wat HotPDF vanaf de andere kant afdwingt: één bestand kan niet tegelijk versleuteld en PDF/A zijn. Als een workflow beide nodig heeft, zijn er twee artefacten nodig, een versleutelde distributiekopie en een aparte schone kopie voor het archief
Nog een randgeval duikt pas op als het bijt: PDF 1.5+-documenten die een zuivere cross-reference stream gebruiken en geen trailer-keyword dragen. De injector leest de trailer om de bron /Info te vinden en de incremental update toe te voegen, en moet de xref-stream-vorm accepteren, anders zou zo'n document worden gekopieerd terwijl de markers stilletjes worden weggelaten. ISO 32000-1 §7.5.6 laat expliciet toe dat op een xref-stream-document een klassieke trailer-incremental-update volgt, met /Prev die naar de xref-stream-offset wijst, precies de structuur die de injector uitzendt. PDFium's eigen FPDF_SaveAsCopy schrijft altijd een klassieke trailer, dus in de normale pijplijn ontmoet de injector nooit een zuivere xref-stream-bron, maar het leespad verwerkt dat voor documenten die ergens anders vandaan komen
Valideren vóór je de claim vertrouwt
De bibliotheek levert een byte-level checker, TPdf.ValidatePdfA, die een TPdfAValidationResult teruggeeft. Het Conformance-veld rapporteert het gedetecteerde niveau en Issues is een set van TPdfAValidationIssue-waarden; de hulpfunctie IsCompliant is alleen waar als een echt niveau werd gedetecteerd en de issueset leeg is. Draai hem als eerste snelle poort in een batch
Wees eerlijk over wat je hiermee koopt. De byte-level checker vangt structurele problemen, zoals een ontbrekende OutputIntent, een verboden actie, een aanwezige /Encrypt, transparantie waar deel 1 dat verbiedt, met hoge zekerheid, en font-embeddingdetectie gebruikt een telheuristiek die bewust alleen een hoogbetrouwbaar signaal rapporteert in plaats van per-glyph dekking na te jagen. Wat hij niet doet, is analyse op het niveau van content-streamoperators, want daarvoor zou een volledige contentparser nodig zijn en dat valt bewust buiten scope. Voor een releasegate combineer je de in-library checker met veraPDF: de checker is direct en draait overal zonder DLL, veraPDF is gezaghebbend. Die combinatie in een batch-run hangen is het onderwerp van de batch preflight report CLI, en daar hoort deze validatie thuis in een echte archiefworkflow
De SaveAsPdfA, InjectPdfAMarkers en ValidatePdfA-API's die hier zijn getoond, worden meegeleverd met PDFium Component voor Delphi, C++Builder en Lazarus/FPC. De productpagina linkt de volledige API-referentie, inclusief de complete conformiteitsenumeratie en het options-record achter deze voorbeelden
var
Pdf: TPdf;
begin
Pdf := TPdf.Create(nil);
try
Pdf.LoadFromFile('invoice.pdf');
// Default conformance is pac1b (PDF/A-1b)
if Pdf.SaveAsPdfA('invoice_archive.pdf') then
// file now carries XMP, sRGB OutputIntent, and catalog markers
else
raise Exception.Create('PDF/A save failed');
finally
Pdf.Free;
end;
end;var
Pdf: TPdf;
Opts: TPdfASaveOptions;
begin
Pdf := TPdf.Create(nil);
try
Pdf.LoadFromFile('report.pdf');
Opts := TPdfASaveOptions.Default;
Opts.Conformance := pac2u; // PDF/A-2u: reliable Unicode text
Opts.Title := 'Quarterly Report 2026';
Opts.Author := 'Finance';
// Leave IccProfileData empty to use the built-in sRGB IEC61966-2.1 profile
if not Pdf.SaveAsPdfA('report_a2u.pdf', Opts) then
raise Exception.Create('PDF/A-2u save failed');
finally
Pdf.Free;
end;
end;var
Pdf: TPdf;
Res: TPdfAValidationResult;
begin
Pdf := TPdf.Create(nil);
try
Pdf.LoadFromFile('invoice_archive.pdf');
Res := Pdf.ValidatePdfA;
if Res.IsCompliant then
Writeln('Conformant: detected level ', Ord(Res.Conformance))
else
Writeln('Issues found: ', SizeOf(Res.Issues), ' flags set');
finally
Pdf.Free;
end;
end;