Teknisk artikel

PDF/A-arkivoverensstemmelse i Delphi med PDFium VCL

Du sender en konverter, der mærker hver fil som PDF/A-1b, kundens arkivsystem indtager dem i et år, og så kører et audit hele batchen gennem veraPDF, og en tredjedel af dem kommer tilbage som ikke-konforme. Intet crashede, ingen undtagelse blev rejst, filerne åbner fint i alle viewere på dit skrivebord. De var bare ikke den standard, du stemplede på dem. Det er den normale fejltilstand for arkiv-PDF, og det er derfor, "vi satte flaget" aldrig er det samme udsagn som "det validerer"

Det første, man skal forstå om PDFium og PDF/A, er, at motoren ikke har noget med sagen at gøre. PDFium renderer, parser og skriver PDF, men dens offentlige overflade har ingen ConvertToPDFA, ingen OutputIntent-writer, ingen XMP-API. Hele arkivoverensstemmelsen, XMP-pakken, OutputIntent og dens ICC-profil, catalog-markeringerne, valideringen, ligger i selve PDFiumPas, i en pure-Pascal-enhed på cirka 2.000 linjer (FPdfPdfa.pas), der parser de gemte bytes og omskriver dem gennem en trinvis opdatering. At vide, hvor arbejdet foregår, fortæller dig, hvor fejlene gemmer sig, og de gemmer sig ikke i PDFium

Hvad PDF/A faktisk kræver, og hvor det bider

PDF/A er ikke ét format. ISO 19005 definerer tre dele (PDF/A-1, -2, -3) og, inden for hver, overensstemmelsesniveauer, der lover forskellige ting. Niveau B (basis) garanterer kun, at det visuelle udseende kan reproduceres. Niveau A (tilgængeligt) tilføjer et tagget strukturtræ og Unicode-mapping oven på B. Niveau U, som kun findes for del 2 og 3, ligger mellem dem: pålidelig Unicode-tekst uden det fulde strukturtræ. ISO 19005-1 har intet Niveau U, en begrænsning biblioteket koder direkte ind

En håndfuld af formatets regler er dem, der bider i praksis. Kryptering er forbudt helt og aldeles (ISO 19005-1 §6.1.3 og dens efterfølgere): en PDF/A-fil kan ikke bære et /Encrypt-dictionary. Dokumentet skal erklære en output-gengivelsesbetingelse gennem en OutputIntent, hvis destination er en gyldig ICC-profil (§6.2.3.2). Selve overensstemmelsespåstanden skal fremgå som XMP-metadata under PDF/A-identifikationsskemaet. Niveau A kræver desuden §6.8 logisk struktur, det tag-træ, der gør dokumentet maskinlæsbart. Springer du et af disse over, afviser en overensstemmelsesverifikator filen, selvom den renderer perfekt

Det ene kald, der producerer et arkiv

PDFiumPas eksponerer hele pipelinen bag TPdf.SaveAsPdfA. Den simple overload tager en måloverensstemmelse og bruger som standard PDF/A-1b, hvilket er det rigtige standardvalg for det almindelige tilfælde "gør dette renderbart for evigt"

Diagram over den tofasede PDFium Component PDF/A gempipeline i Delphi, hvor FPDF_SaveAsCopy serialiserer dokumentet, og InjectPdfAMarkers tilføjer XMP-metadata, sRGB OutputIntent og et omskrevet catalog som én inkrementel opdatering
SaveAsPdfA kører i to faser — PDFium serialiserer dokumentet, hvorefter InjectPdfAMarkers tilføjer arkivmarkørerne som én inkrementel opdatering
var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'invoice.pdf';
    Pdf.Active := True;
    // Standardoverensstemmelse er pac1b (PDF/A-1b)
    if Pdf.SaveAsPdfA('invoice_archive.pdf') then
      // filen bærer nu XMP, sRGB OutputIntent og catalog-markeringer
    else
      raise Exception.Create('PDF/A save failed');
  finally
    Pdf.Free;
  end;
end;

Under motorhjelmen er dette et to-trins træk. SaveAsPdfA beder først PDFium om at serialisere dokumentet med FPDF_SaveAsCopy, og sender derefter den bytestrøm til InjectPdfAMarkers, som tilføjer XMP-metadataene, sRGB-OutputIntenten med dens indlejrede ICC-profil, og et omskrevet catalog som en trinvis opdatering. Kilden læses fra position nul, og destinationen skrives fra position nul; det originale objekttræ efterlades intakt, og markeringerne kommer med efter det eksisterende %%EOF. Har du brug for bytesene i stedet for en fil, tager SaveAsPdfAToStream en TStream og de samme indstillinger

Vælg overensstemmelse med options-recordet

For at målrette en bestemt del og et bestemt niveau skal du give en TPdfASaveOptions-record. Dens Conformance-felt tager en TPdfAConformance-værdi. Enumen dækker enhver gyldig kombination og intet andet: pac1b, pac1a for del 1; pac2b, pac2u, pac2a for del 2; pac3b, pac3u, pac3a for del 3, plus pacUnknown og pacNone til valideringssiden. Der findes ikke noget pac1u, fordi det niveau ikke findes i standarden

var
  Pdf: TPdf;
  Opts: TPdfASaveOptions;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'report.pdf';
    Pdf.Active := True;
    Opts := TPdfASaveOptions.Default;
    Opts.Conformance := pac2u;           // PDF/A-2u: pålidelig Unicode-tekst
    Opts.Title := 'Quarterly Report 2026';
    Opts.Author := 'Finance';
    // Lad IccProfileData være tom for at bruge den indbyggede sRGB IEC61966-2.1-profil
    if not Pdf.SaveAsPdfA('report_a2u.pdf', Opts) then
      raise Exception.Create('PDF/A-2u save failed');
  finally
    Pdf.Free;
  end;
end;

Det meste af recorden kan stå tom. Lad Title, Author, Subject, Keywords, Creator og Producer stå tomme, så udfylder SaveAsPdfA dem automatisk fra dokumentets Info-dictionary via FPDF_GetMetaText. Lad CreationDate og ModDate stå tomme, så bruger den det aktuelle UTC-tidspunkt til begge XMP-datoer. Lad DocumentId og InstanceId stå tomme, så forudfylder biblioteket dem fra FPDF_GetFileIdentifier, med fallback til et deterministisk ID afledt af kildebytesene. Det ene felt, du bevidst kunne ønske at overstyre, er IccProfileData: tom betyder den medfølgende sRGB IEC61966-2.1-profil, men en CMYK- eller gråtoneworkflow bør levere sin egen

Hvorfor Level A degraderes, og hvorfor det er det ærlige valg

Her er en finesse, der snubler folk, som forventer, at et flag er en garanti. Du kan bede om pac1a på et dokument, der ikke har noget tag-træ, men PDF/A-1a kræver §6.8 logisk struktur, og biblioteket kan ikke fremstille et strukturtræ ud af en utagget PDF. I stedet for at udsende en fil, der hævder Niveau A, mens den fejler det, tjekker SaveAsPdfA for en reel tagget struktur (/StructTreeRoot plus /MarkInfo med /Marked true) og nedgraderer påstanden, hvis den mangler: pac1a bliver til pac1b, pac2a bliver til pac2b, og så videre på tværs af alle tre dele. De interne hjælpefunktioner er PdfAIsLevelA og PdfADowngradeToLevelB

Ræsonnementet er værd at sige ligeud: en fil, der ærligt erklærer det niveau, den opfylder, er mere nyttig end en, der lyver om et niveau, den ikke opfylder. Niveau U håndteres anderledes. At registrere ægte Unicode-dækning ville betyde en naiv "har den /ToUnicode"-test, der overdegraderer legitime dokumenter (WinAnsi og lignende kodninger er undtaget), så gemmesiden udsender U-påstanden, som den kaldende kode erklærede den, og lader i stedet uoverensstemmelsen blive flagget på valideringssiden. Har du brug for et garanteret Niveau A-arkiv, skal du tagge dokumentet, før du konverterer det; konverteren opfinder ikke struktur, der ikke er der

Beslutningsdiagram, der viser SaveAsPdfA i Delphi nedgradere et PDF/A Level A-krav til Level B, når dokumentet ikke bærer et tagget strukturtræ, mens et Level U-krav udsendes som erklæret
Et manglende tag-træ nedgraderer påstanden ærligt — pac1a bliver pac1b, mens Level U udsendes præcis som deklareret og bedømmes på valideringssiden

ICC-faldgruben, som kun en rigtig validator fanger

Det er fejlen, der lærte den hårdeste lektie, fordi bibliotekets eget checker bestod den, mens veraPDF, ISO 19005-referencevalidatoren, ikke gjorde. PDF/A kræver, at OutputIntentens destinationsprofil er en gyldig ICCBased-stream, og §6.2.3.2 får en verifikator til at validere den stream som et farverum. En ICCBased-stream skal erklære /N, antallet af farvekomponenter. En tidlig version af injektoren skrev ICC-stream-dictionaryet med kun /Length og intet /N, og veraPDF afviste resultatet med "The N entry (value null)... is missing"

Det, der gjorde det lumsk, var, at afvisningen kun udløstes for PDF/A-1b og -1a. Overensstemmelsesmodellerne for del 2 og del 3 kørte ikke den bestemte kontrol på destinationsprofilen, så den identiske injicerede struktur validerede under pac2b, pac3b og pac2u, men fejlede under pac1b på intet andet end værdien af pdfaid:part. En enhedstest kunne aldrig se det, fordi bibliotekets eget ValidatePdfACompliance kun tjekkede, at nøglen /DestOutputProfile fandtes, ikke hvad der lå inde i stream-dictionaryet. Interne tests forblev grønne; reel arkivvalidering fejlede

Rettelsen er IccComponentCount, som læser datafarverumssignaturen ved offset 16 i ICC-headeren og mapper den til et komponentantal: GRAY er 1, RGB , Lab og XYZ er 3, CMYK er 4, med en ukendt profil, der som standard sættes til 3. Det antal går ind i stream-dictionaryet som /N. Det beregnes, ikke hard-codes til 3, så en kaldende kode, der leverer en CMYK- eller gråtoneprofil gennem IccProfileData, stadig får den korrekte værdi. Den bredere lektie er metodisk: det interne checker i biblioteket og en autoritativ validator har hver sine blinde vinkler, og PDF/A-output skal testes fra ende til anden mod en referenceimplementering som veraPDF i stedet for at blive betroet selv-tjek. Den samme trinvis-opdaterings-disciplin bag rene arkiver er dækket i validering af komprimerede objekt- og xref-streams, hvilket har betydning, fordi moderne PDF'er, injektoren indtager, ofte er bygget på krydsreferencestreams

Kryptering, xref streams og andre kanter

Fordi ISO 19005 forbyder kryptering, fjerner gemmestien den, før der skrives. SaveAsPdfA anvender FPDF_REMOVE_SECURITY, når der serialiseres, så en krypteret kilde (indlæst med sin adgangskode) dekrypteres på vej ind i arkivet. På et ukrypteret dokument er dette en no-op og ændrer intet. Følgeslutningen er den samme begrænsning, HotPDF håndhæver fra den anden retning: en enkelt fil kan ikke være både krypteret og PDF/A. Når en workflow har brug for begge dele, er svaret to artefakter, en krypteret kopi til distribution og en separat ren kopi til arkivet

Endnu en kant er usynlig, indtil den bider: PDF 1.5+-dokumenter, der bruger en ren krydsreferencestream og ikke bærer noget trailer-nøgleord. Injektoren læser trailer'en for at finde kildens /Info og tilføje sin trinvise opdatering, og den skal acceptere xref-stream-formen, ellers ville et sådant dokument blive kopieret igennem med markeringerne stille droppet. ISO 32000-1 §7.5.6 tillader eksplicit en klassisk trailer-trinvis opdatering efter et xref-stream-dokument, med /Prev, der peger på xref-stream-offsettet, hvilket er præcis den struktur, injektoren udsender. PDFiums egen FPDF_SaveAsCopy skriver altid en klassisk trailer, så i den normale pipeline møder injektoren aldrig en ren xref-stream-kilde, men læsestien håndterer det for dokumenter, der ankommer et andet sted fra

Verificér, før du stoler på påstanden

Biblioteket leverer et checker på byte-niveau, TPdf.ValidatePdfA, som returnerer et TPdfAValidationResult. Dets Conformance-felt rapporterer det registrerede niveau, og Issues er et sæt TPdfAValidationIssue-værdier; bekvemmelighedsmetoden IsCompliant er kun true, når et reelt niveau blev registreret, og problemsættet er tomt. Kør den som en hurtig første port i en batch

Diagram over IccComponentCount læse ICC farverumssignaturen ved header-offset 16 for at skrive /N-posten, den manglende ordbogspost, der fik veraPDF til at afvise PDF/A-1b-filer, den interne Delphi-kontrol bestod
IccComponentCount udleder /N fra ICC-header-signaturen og lukker hullet, som kun veraPDF fangede på part 1-filer
var
  Pdf: TPdf;
  Res: TPdfAValidationResult;
  Issue: TPdfAValidationIssue;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'invoice_archive.pdf';
    Pdf.Active := True;
    Res := Pdf.ValidatePdfA;
    if Res.IsCompliant then
      Writeln('Conformant: detected level ', Ord(Res.Conformance))
    else
      for Issue in Res.Issues do
        Writeln('Issue: ', Ord(Issue));
  finally
    Pdf.Free;
  end;
end;

Vær ærlig om, hvad dette giver dig. Checkeren på byte-niveau fanger strukturelle problemer (en manglende OutputIntent, en forbudt handling, et tilstedeværende /Encrypt, transparens, hvor del 1 forbyder det) med høj sikkerhed, og skrifttype-indlejringsdetektionen bruger en antalsheuristik, der bevidst kun rapporterer et højsikkerhedssignal frem for at jagte dækning pr. glyf. Det, den ikke gør, er analyse af content-stream-operatorer, hvilket ville kræve en fuld content-parser og er uden for scope med vilje. Til en udgivelsesport skal du parre det interne checker med veraPDF: checkeren er øjeblikkelig og kører overalt uden DLL, veraPDF er autoritativ. At koble den parring ind i en batch-kørsel er emnet for batch-preflight-rapport-CLI'en, som er, hvor denne validering hører hjemme i en reel arkivworkflow

API'erne SaveAsPdfA, InjectPdfAMarkers og ValidatePdfA, der er vist her, følger med PDFium-komponenten til Delphi, C++Builder og Lazarus/FPC. Produktsiden linker til den fulde API-reference, inklusive den komplette overensstemmelses-enum og options-recorden bag disse eksempler