Du levererar en konverterare som märker varje fil som PDF/A-1b, kundens journalsystem tar emot dem i ett år, och sedan kör en revision hela batchen genom veraPDF och en tredjedel kommer tillbaka som icke-kompatibla. Inget kraschade, inget undantag kastades, filerna öppnas fint i varje visare på skrivbordet. De uppfyllde helt enkelt inte standarden du stämplade på dem. Det här är det normala felutfallet för arkiv-PDF, och det är därför "vi satte flaggan" aldrig är samma sak som "det validerar"
Det första man behöver förstå om PDFium och PDF/A är att motorn inte har något med saken att göra. PDFium renderar, parsar och skriver PDF, men dess publika yta har ingen ConvertToPDFA, ingen OutputIntent-skrivare, inget XMP-API. All arkivkompatibilitet, XMP-paketet, OutputIntent och dess ICC-profil, katalogmarkörerna och valideringen, ligger i själva PDFiumPas, i en ungefär 2 000 rader lång ren Pascal-enhet (FPdfPdfa.pas) som parsar de sparade bytena och skriver om dem genom en inkrementell uppdatering. Att veta var arbetet sker säger dig också var buggarna gömmer sig, och de gömmer sig inte i PDFium
Vad PDF/A faktiskt kräver, och var det biter
PDF/A är inte ett format. ISO 19005 definierar tre delar (PDF/A-1, -2, -3) och, inom varje del, överensstämmelsenivåer som lovar olika saker. Level B (basic) garanterar bara att det visuella utseendet kan återskapas. Level A (accessible) lägger till ett taggat strukturträd och Unicode-mappning ovanpå B. Level U, som bara finns för del 2 och 3, ligger mellan dem: pålitlig Unicode-text utan hela strukturträdet. ISO 19005-1 har ingen Level U, en begränsning som biblioteket kodar direkt
Ett fåtal av formatets regler är just de som biter i praktiken. Kryptering är helt förbjuden (ISO 19005-1 §6.1.3 och efterföljare): en PDF/A-fil får inte bära en /Encrypt ordbok. Dokumentet måste deklarera ett återgivningsvillkor via en OutputIntent vars mål är en giltig ICC-profil (§6.2.3.2). Själva överensstämmelsepåståendet måste visas som XMP-metadata under PDF/A:s identifikationsschema. Level A kräver dessutom §6.8 logisk struktur, taggträdet som gör dokumentet maskinläsbart. Missar du något av detta så avvisar en överensstämmelseverifierare filen även om den återges perfekt
Det enda anropet som producerar ett arkiv
PDFiumPas exponerar hela kedjan bakom TPdf.SaveAsPdfA. Den enkla overloaden tar en målkonformitet och faller tillbaka till PDF/A-1b, vilket är rätt standard för det vanliga fallet "gör detta återgivningsbart för alltid"
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;
Under huven är detta en tvåstegsrörelse. SaveAsPdfA ber först PDFium serialisera dokumentet med FPDF_SaveAsCopy, och lämnar sedan den byte-strömmen till InjectPdfAMarkers, som lägger till XMP-metadata, sRGB OutputIntent med sin inbäddade ICC-profil och en omskriven katalog som en inkrementell uppdatering. Källan läses från position noll och målet skrivs från position noll; det ursprungliga objekträdet lämnas intakt och markörerna följer med efter den befintliga %%EOF. Om du behöver bytena i stället för en fil, SaveAsPdfAToStream tar en TStream och samma alternativ
Att välja överensstämmelse med options-recorden
För att rikta in dig på en viss del och nivå, skicka en TPdfASaveOptions record. Dess Conformance fält tar ett TPdfAConformance värde. Uppräkningen täcker varje giltig kombination och inget annat: pac1b, pac1a för del 1; pac2b, pac2u, pac2a för del 2; pac3b, pac3u, pac3a för del 3, plus pacUnknown och pacNone för valideringssidan. Det finns ingen pac1u, eftersom den nivån inte finns i standarden
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;
Det mesta i recorden kan lämnas tomt. Lämna TitleDet mesta i recorden kan lämnas tomt. Lämna Author, Subject, Keywords, och Creator tomt och Producer fyller automatiskt i dem från dokumentets Info-ordbok via SaveAsPdfA. Lämna FPDF_GetMetaText och CreationDate tomt och då används den aktuella UTC-tiden för båda XMP-datumen. Lämna ModDate och DocumentId tomt och biblioteket förhandsfyller dem från InstanceId, med ett deterministiskt ID som reserv, härlett från källbytena. Det enda fält du kan vilja skriva över medvetet är FPDF_GetFileIdentifier: tomt betyder den medföljande sRGB IEC61966-2.1-profilen, men ett CMYK- eller gråskolearbetsflöde bör leverera sin egen.IccProfileDataNär Level A degraderas, och varför det är det ärliga valet
Här finns en detalj som brukar fånga den som tror att en flagga är en garanti. Du kan begära
på ett dokument som saknar taggträd, men PDF/A-1a kräver §6.8 logisk struktur och biblioteket kan inte trolla fram ett strukturträd ur en otaggad PDF. I stället för att ge ut en fil som påstår Level A men faller igenom den, pac1a kontrollerar efter en verklig taggad struktur (SaveAsPdfA plus /StructTreeRoot med /MarkInfo) och, om den saknas, nedgraderar påståendet: /Marked true blir pac1a, pac1b blir pac2a, och så vidare över alla tre delar. De interna hjälparna är pac2b och PdfAIsLevelA.PdfADowngradeToLevelBResonemanget är värt att säga rakt ut: en fil som ärligt anger den nivå den uppfyller är mer användbar än en som ljuger om en nivå den inte uppfyller. Level U hanteras annorlunda. Att upptäcka verklig Unicode-täckning skulle kräva ett naivt "har den
" test som överdegraderar legitima dokument (WinAnsi och liknande kodningar är undantagna), så sparvägen skickar U-påståendet som anroparen angav det och låter avvikelsen markeras på valideringssidan i stället. Om du behöver ett garanterat Level A-arkiv, tagga dokumentet innan du konverterar det; konverteraren kommer inte att uppfinna struktur som inte finns där./ToUnicodeICC-fällan som bara en riktig verifierare fångar
Det här är felet som lärde den hårdaste läxan, eftersom bibliotekets egen kontroll godkände det medan veraPDF, ISO 19005:s referensverifierare, inte gjorde det. PDF/A kräver att OutputIntentens målprofil är en giltig ICCBased-ström, och §6.2.3.2 låter en verifierare validera den strömmen som en färgrymd. En ICCBased-ström måste deklarera
, antalet färgkomponenter. En tidig version av injektorn skrev ICC-strömordboken med bara /N och ingen /Length, och veraPDF avvisade resultatet med "The N entry (value null)... is missing"./NDet som gjorde felet lurigt var att avvisningen bara slog till för PDF/A-1b och -1a. Överensstämmelsemodellerna för del 2 och del 3 körde inte den särskilda kontrollen på målprofilen, så den identiska injicerade strukturen validerade under
, pac2b och pac3b men misslyckades under pac2u på bara pac1b värde. Ett enhetstest kunde aldrig se det, eftersom bibliotekets egen pdfaid:part bara kontrollerade att ValidatePdfACompliancehar bara kontrollerat att /DestOutputProfile nyckeln existerade, inte vad som låg inuti strömordboken. Interna tester förblev gröna; den verkliga arkivvalideringen misslyckades
Lösningen är IccComponentCount, som läser datas färgrymdssignatur vid offset 16 i ICC-huvudet och mappar den till ett komponentantal: GRAY är 1, RGB , Lab , och XYZ är 3, CMYK är 4, med en okänd profil som standard till 3. Det talet läggs in i strömordboken som /N. Det beräknas, inte hårdkodas till 3, så att en anropare som skickar en CMYK- eller gråskaleprofil via IccProfileData fortfarande får rätt värde. Den bredare lärdomen är metodisk: kontrollern i biblioteket och en auktoritativ verifierare har varsin blind fläck, och PDF/A-utdata måste testas hela vägen mot en referensimplementation som veraPDF i stället för att lita på självtester. Samma inkrementella uppdateringsdisciplin bakom rena arkiv behandlas i validering av komprimerade objekt- och xref-strömmar, vilket spelar roll eftersom moderna PDFs som injektorn tar emot ofta bygger på cross-reference-strömmar
Kryptering, xref-strömmar och andra kanter
Eftersom ISO 19005 förbjuder kryptering, strippas den bort innan skrivning. SaveAsPdfA tillämpar FPDF_REMOVE_SECURITY när den serialiserar, så att en krypterad källa (inläst med sitt lösenord) dekrypteras på vägen in i arkivet. På ett okrypterat dokument är detta en no-op och ändrar ingenting. Slutsatsen är samma begränsning som HotPDF upprätthåller från andra hållet: en enda fil kan inte vara både krypterad och PDF/A. När ett arbetsflöde behöver båda är svaret två artefakter, en krypterad kopia för distribution och en separat ren kopia för arkivet
En till kant är osynlig tills den biter: PDF 1.5+-dokument som använder en ren cross-reference-ström och saknar någon trailer nyckel. Injektorn läser trailern för att hitta källans /Info och lägga till dess inkrementella uppdatering, och den måste acceptera xref-stream-formen, annars skulle ett sådant dokument kopieras igenom med markörerna tyst borttagna. ISO 32000-1 §7.5.6 tillåter uttryckligen att en klassisk trailer-inkrementell uppdatering följer ett xref-stream-dokument, med /Prev som pekar på xref-stream-offseten, vilket är exakt den struktur injektorn emitterar. PDFiums egen FPDF_SaveAsCopy skriver alltid en klassisk trailer, så i den normala kedjan möter injektorn aldrig en ren xref-stream-källa, men läsbanan hanterar den för dokument som kommer in från annat håll
Verifiering innan du litar på påståendet
Biblioteket levererar en byte-nivåkontroll, TPdf.ValidatePdfA, som returnerar en TPdfAValidationResult. Dess Conformance fält rapporterar den detekterade nivån och Issues är en mängd av TPdfAValidationIssue värden; hjälpfunktionen IsCompliant är sann bara när en verklig nivå har detekterats och issues-mängden är tom. Kör den som ett snabbt första kontrollsteg i en batch
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;
Var ärlig med vad detta faktiskt ger dig. Byte-nivåkontrollen fångar strukturella problem (ett saknat OutputIntent, en förbjuden åtgärd, en närvarande /Encrypt, transparens där del 1 förbjuder det) med hög träffsäkerhet, och detektering av fontinbäddning använder en antalheuristik som medvetet rapporterar bara en signal med hög säkerhet i stället för att jaga per-glyf-täckning. Det den inte gör är operatoranalys av innehållsströmmar, vilket skulle kräva en fullständig innehållsparsare och ligger utanför omfånget med flit. För en releasegrind, para ihop kontrollern i biblioteket med veraPDF: kontrollern är omedelbar och körs överallt utan DLL, veraPDF är auktoritativ. Att koppla ihop de två i en batchkörning är ämnet för CLI:t för batchpreflightrapport, vilket är där denna validering hör hemma i ett verkligt arkivflöde
De SaveAsPdfA, InjectPdfAMarkers och ValidatePdfA API:erna som visas här levereras med PDFium Component för Delphi, C++Builder och Lazarus/FPC. Produktsidan länkar till hela API-referensen, inklusive den fullständiga överensstämmelseuppräkningen och options-recorden bakom dessa exempel