Teknisk artikkel

PDF/A-3 tilknyttede filer og AFRelationship i Delphi

For å feste en kildefil til et PDF/A-3-dokument fra Delphi, skriver PDFium Component en PDF 2.0 associated-file-kjede: en innebygd filstrøm med en MIME /Subtype, en filspesifikasjon som bærer /AFRelationship, og en /AF-array hengt på katalogen eller en side. InjectAssociateFiles og TPdf.SaveAsWithAssociateFiles bygger den kjeden i én inkrementell oppdatering, og siden v3.121.2 serialiseres MIME-typen som ett enkelt, korrekt escapet PDF-navn. Resten av dette innlegget dekker hva en validator sjekker, éntegns-feilen som knakk text/plain, og stedene der eldre utgivelser stille gjorde noe annet enn det du ba om

Hva trenger egentlig en PDF/A-3 tilknyttet fil?

Et PDF/A-3-vedlegg passerer validering bare når tre objekter er enige med hverandre: den innebygde filstrømmen deklarerer /Type /EmbeddedFile pluss en MIME /Subtype, filspesifikasjonsordboken (ISO 32000-2 §7.11.3) bærer /F, /UF, /EF og /AFRelationship, og noe i dokumentet refererer den filspesifikasjonen gjennom en /AF-array (ISO 32000-2 §14.13). Bare innbygging gjennom /Names /EmbeddedFiles-treet, som er det TPdf.CreateAttachment gjør, setter aldri assosiasjonsfeltene i det hele tatt. PDFium Components egen PDF/A-3b valideringsfixture gjør avhengigheten konkret: gi bare /AFRelationship-nøkkelen nytt navn, og filen feiler nøyaktig én regel i klausul 6.8 i ISO 19005-3; slipp bare MIME /Subtype, og en annen 6.8-regel feiler; legg samme vedlegg inn i en PDF/A-1b-kandidat, og den avvises kontant, for PDF/A-1 forbyr innebygde filer uansett hvor ryddig metadataen er

Trebobjektskjeden til en PDF/A-3 tilknyttet fil i PDFium Component: en EmbeddedFile-strøm med en MIME Subtype som application xml, en filspesifikasjon med F, UF, EF og AFRelationship satt til Data, og en AF-array for den fra katalogen eller en side — de tre objektene en validator sjekker før klausul 6.8 i ISO 19005-3 passerer
Strøm, filspesifikasjon og AF-array må være enige; den rene navnetre-innbyggingen i TPdf.CreateAttachment setter ingen av assosiasjonsfeltene og kommer aldri til å gjøre det

Relasjonsverdien er delen folk pleier å gjette på. TPdfAFRelationship i FPdfAssocFiles mapper ett enum-medlem til hvert navntoken injektoren kan skrive ut, og bare de fem første hører til delmengden ISO 19005-3 gjenkjenner:

  • afSource → /Source: originalen PDF-en ble produsert fra, som en tekstbehandlingsfil eller et regneark
  • afData → /Data: maskinlesbare data det synlige innholdet var utledet av eller representerer
  • afAlternative → /Alternative, afSupplement → /Supplement, afUnspecified → /Unspecified
  • afEncryptedPayload, afFormData, afTemplate: PDF 2.0-tillegg som faller utenfor PDF/A-3-delmengden, så hold dem unna arkivoutput

Hvorfor knakte /Subtype /text/plain valideringen?

MIME-feilen var en tokeniseringsfeil, ikke et compliance-gap: før v3.121.2 konkatenerte injektoren kallerens streng rett etter en skråstrek, noe som ga /Subtype /text/plain. I PDF-syntaks starter den andre skråstreken et nytt navnobjekt (ISO 32000-1 §7.3.5), så strømordboken holdt plutselig nøkkelen /Subtype, navnet /text, og et hengende ekstranavn /plain som gjorde nøkkelverdi-parene ubalanserte. En uavhengig PDF/A-validator avviste filen mens den parsede EmbeddedFile-ordboken, før den noen gang nådde en PDF/A-regel, noe som er grunnen til at feilen så ut som filkorrupsjon i stedet for en manglende vedleggegenskap

Fiksen ruter MIME-verdien gjennom EscapePdfName, som skriver /text#2Fplain: ett navn hvis dekodede verdi er text/plain. Escapingen er bevisst bredere enn skråstreken. Hver byte på eller under 32 (mellomrom, tab, CR, LF), hver byte på eller over 127, avgrenserne ()<>[]{}/% og #-escape-tegnet selv blir #XX. Å escape bare skråstreken ville ha latt et annet hull igjen: en MIME-streng som inneholder >> eller mellomrom kunne lukke ordboken tidlig eller injisere ekstra nøkler, så regresjonstesten mater en fiendtlig verdi med hver avgrenser pluss tab, LF og CR og sjekker den eksakte innkodede outputen

Hvorfor MIME-subtypen text skråstrek plain knakk PDF/A-3-parsingen i PDFium Component: å konkatenere verdien etter en skråstrek ga to navnobjekter, /text som verdi pluss et hengende /plain som gjorde EmbeddedFile-ordboken ubalansert, og fiksen i v3.121.2 ruter verdien gjennom EscapePdfName slik at /text#2Fplain er ett navn som dekodes til text/plain
Feilen så ut som filkorrupsjon fordi den skjedde i parseren, før noen PDF/A-regel; det escapede navnet holder parene balansert og validatoren lesende
// Hva injektoren skriver for MIMEType = 'text/plain'
//   før v3.121.2:  /Type /EmbeddedFile /Subtype /text/plain     (to navn)
//   v3.121.2:         /Type /EmbeddedFile /Subtype /text#2Fplain   (ett navn)
//
// Kallere sender alltid den vanlige MIME-verdien. Pre-escaper du den selv,
// dobbeltenkoder du '#', som gjør 'text#2Fplain' om til 'text#232Fplain'
Options.Files[0].MIMEType := 'text/plain';

Å bygge en PDF/A-3-fil med InjectAssociateFiles

For PDF/A-3-output, produser det konforme basisdokumentet med TPdf.SaveAsPdfAToStream og kall så InjectAssociateFiles på den strømmen; den to-trinns-pipelinen er nøyaktig hva valideringsfixturen kjører før den passerer PDF/A-3b. TPdf.SaveAsWithAssociateFiles er bekvemmelighetsomslaget, men det lagrer gjennom den vanlige SaveAs-veien med saRemoveSecurity i stedet for gjennom PDF/A-writeren, så det legger ikke til XMP-identifikasjonen og output intent som PDF/A krever. Merk at record-typene bor i FPdfAssocFiles og FPdfPdfa, så begge enheter hører hjemme i uses-klausulen din. Siden v3.121.3 trenger ikke FileName og Description lenger å være ren ASCII: /UF og /Desc skrives som PDF-tekststrenger, utskriftsvennlig ASCII bokstavelig og alt annet som UTF-16BE med byte order mark, mens det eldre /F-navnet alltid er portabel utskriftsvennlig ASCII med hvert annet tegn erstattet av _, så lesere som dekoder /F med sin egen kodepage viser en understrek i stedet for mojibake. Tidligere bygg konverterte alle tre gjennom systemets ANSI-kodepage på Delphi eller skrev rå UTF-8-byter på Free Pascal, så hold navnene ASCII bare hvis eldre bygg må produsere samme output

uses
  System.SysUtils, System.Classes, System.IOUtils,
  PDFium, FPdfPdfa, FPdfAssocFiles;

procedure SaveWithSourceData(Pdf: TPdf; const XmlPath, OutPath: string);
var
  PdfAOptions: TPdfASaveOptions;
  Options: TAssocFilesOptions;
  Base: TMemoryStream;
  Output: TFileStream;
begin
  PdfAOptions := TPdfASaveOptions.Default;
  PdfAOptions.Conformance := pac3b;

  Options := TAssocFilesOptions.Default;      // TargetPage = 0: katalognivå /AF
  SetLength(Options.Files, 1);
  Options.Files[0].FileName := 'invoice-data.xml';
  Options.Files[0].Description := 'Structured invoice data';
  Options.Files[0].Content := TFile.ReadAllBytes(XmlPath);
  Options.Files[0].Relationship := afData;
  Options.Files[0].MIMEType := 'application/xml';  // skrives som /application#2Fxml

  Base := TMemoryStream.Create;
  try
    if not Pdf.SaveAsPdfAToStream(Base, PdfAOptions) then
      raise Exception.Create('PDF/A-3 base save failed');
    Output := TFileStream.Create(OutPath, fmCreate);
    try
      InjectAssociateFiles(Base, Output, Options);  // spoler Base tilbake; reiser EPdfAssocFilesError ved feil
    finally
      Output.Free;
    end;
  finally
    Base.Free;
  end;
end;

Katalog eller side: hvor lander /AF-arrayen?

TAssocFilesOptions.TargetPage avgjør eieren av /AF-arrayen: 0 fester den til katalogen som en dokumentnivå-assosiasjon, og 1..N fester den til den sidens ordbok, 1-basert. Injektoren legger alt til som én enkelt inkrementell oppdatering i et fast oppsett (de innebygde strømmene, så filspesifikasjonene, så /AF-arrayen, så et omskrevet katalog- eller sideobjekt), så eksisterende objekter beholder offsettene sine og ingenting komprimeres på nytt. Enhver tidligere /AF-oppføring på mål-ordboken erstattes, ikke flettes, noe som gjør en gjentatt lagring idempotent, men også betyr at et andre kall med en annen filliste vinner. To oppførsler pleide å fortjene en vakt i din egen kode, og begge er endret. Før v3.122.0 feilet ikke en TargetPage utenfor området; den falt tilbake til katalogen, så en skrivefeil gjorde en sidenivå-assosiasjon om til en dokumentnivå uten noe signal. Siden v3.122.0 reiser SaveAsWithAssociateFiles og SaveAsWithAssociateFilesToStream EPdfError når TargetPage er utenfor 0..PageCount, og InjectAssociateFiles reiser den nye EPdfAssocFilesError for en negativ TargetPage eller én som ikke navngir noen eksisterende side, og lar målstrømmen umodifisert. Før v3.121.4 skannet sideoppslaget de lagrede bytene etter /Type /Page-ordbøker i filrekkefølge, noe som kunne feste filen til en annen side når sideobjekter ble lagret i en annen rekkefølge enn de vises, for eksempel etter at sider var omordnet eller satt inn; siden v3.121.4 navngir TargetPage siden på den posisjonen i dokumentets siderekkefølge

Hvor AF-arrayen lander i PDFium Component: TargetPage null fester den til katalogen, sider 1 til N fester den til sideordboken, og en verdi utenfor området, som før v3.122.0 stille falt tilbake til katalogen, reiser nå et unntak, mens injektoren legger alt til som én inkrementell oppdatering i et fast oppsett som beholder eksisterende offsetter og erstatter enhver tidligere AF-oppføring
Før v3.122.0 ble en TargetPage utenfor området stille en dokumentnivå-assosiasjon; nåværende utgivelser reiser i stedet, og et andre kall med en annen filliste vinner fortsatt
procedure AttachChartSource(Pdf: TPdf; PageNumber: Integer;
  const CsvBytes: TBytes; const OutPath: string);
var
  Options: TAssocFilesOptions;
begin
  // Siden v3.122.0 reiser en TargetPage utenfor området EPdfError (eldre bygg
  // falt stille tilbake til katalognivå /AF); å sjekke først navngir siden
  if (PageNumber < 1) or (PageNumber > Pdf.PageCount) then
    raise EArgumentOutOfRangeException.CreateFmt('No page %d', [PageNumber]);

  Options := TAssocFilesOptions.Default;
  Options.TargetPage := PageNumber;
  SetLength(Options.Files, 1);
  Options.Files[0].FileName := 'chart-data.csv';
  Options.Files[0].Content := CsvBytes;
  Options.Files[0].Relationship := afSource;
  Options.Files[0].MIMEType := 'text/csv';

  if not Pdf.SaveAsWithAssociateFiles(OutPath, Options) then
    raise Exception.Create('Associated-file save failed');
end;

Hvordan leser du AFRelationship tilbake pålitelig?

TPdf.AttachmentRelationship[Index] gir /AFRelationship-navnet til et vedlegg gjennom den opprinnelige FPDFAttachment_GetAFRelationship-eksporten, men en tom streng har to mulige betydninger, så kall AttachmentRelationshipFeaturesAvailable først. Bindingen lastes tolerantly: når PDFium-DLL-en mangler den eksporten, leses hver relasjon som tom, noe som er uatskeligelig fra en filspesifikasjon som rett og slett ikke har /AFRelationship. Egenskapen deler også indeksen sin med AttachmentCount, som teller oppføringer i /Names /EmbeddedFiles-treet. Injektoren skriver bare /AF-kjeden og legger ikke til noe navnetre-oppføring, så en fil festet gjennom InjectAssociateFiles er utenfor den indeksen; for å bekrefte den injiserte kjeden, inspiser de lagrede bytene eller kjør en PDF/A-validator. Innmaten av det navnetreet dekkes i å arbeide med PDF-vedlegg i Delphi med PDFium Component

procedure ReportRelationships(const FileName: string);
var
  Pdf: TPdf;
  I: Integer;
  Rel: string;
begin
  if not AttachmentRelationshipFeaturesAvailable then
  begin
    Writeln('This PDFium build cannot report /AFRelationship');
    Exit;  // et tomt svar ville vært tvetydig, så ikke spør
  end;

  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := FileName;
    Pdf.Active := True;
    for I := 0 to Pdf.AttachmentCount - 1 do
    begin
      Rel := Pdf.AttachmentRelationship[I];
      if Rel = '' then
        Rel := '(no /AFRelationship)';
      Writeln(Pdf.AttachmentName[I], ': ', Rel);
    end;
  finally
    Pdf.Free;
  end;
end;

Hva garanterer ikke SaveAsWithAssociateFiles?

TPdf.SaveAsWithAssociateFiles garanterer filformat-konvolutten og at de etterspurte filene ble injisert, ikke konformitet. Injiseringsdelen er ny: før v3.122.0, når de lagrede bytene ikke hadde noen lesbar trailer eller katalogordboken ikke kunne lokaliseres, kopierte InjectAssociateFiles inputen gjennom uendret, og metoden ga fortsatt True. Siden v3.122.0 reiser InjectAssociateFiles EPdfAssocFilesError i de tilfellene før noe skrives, SaveAsWithAssociateFiles gir False, og etter som den nå bygger det komplette outputen i en lagringsbutikk før målet åpnes, avkorter en avvist eller feilet lagring ikke lenger en eksisterende fil. En tom Files-array kopierer fortsatt dokumentet gjennom uendret, ved design. Innholdet i nyttelasten er også ditt ansvar: injektoren sjekker ikke at en XML-fil er velformet, at MIME-typen matcher bytene, eller at basisdokumentet i det hele tatt er PDF/A. Behandle den endelige filen som uverifisert helt til en validator har sett den, samme disiplin beskrevet i PDFium Component og PDF/A arkiveringscompliance. Parser du også innkommende ordbøker selv, gjelder de samme #XX-navnereglene omvendt, et tema dekket i navntoken-feller ved parsing av PDF-ordbøker

Tilknyttede filer, PDF/A-output, vedleggsmetadata og validering leveres alle i samme komponent, så pipelinen over kjører uten et annet PDF-bibliotek i bygget. API-referansen, prøveversjonen og lisensalternativene ligger på PDFium Component produktsiden