Tekninen artikkeli

PDF/A-3:n liitetyt tiedostot ja AFRelationship Delphissä

Liittääkseen lähdetiedoston PDF/A-3-dokumenttiin Delphistä PDFium Component kirjoittaa PDF 2.0:n associated-file-ketjun: upotetun tiedostostreamin MIME-/Subtypella, tiedostospesifikaation joka kantaa /AFRelationshipia ja /AF-taulukon joka ripustetaan katalogiin tai sivulle. InjectAssociateFiles ja TPdf.SaveAsWithAssociateFiles rakentavat kyseisen ketjun yhdessä inkrementaalisessa päivityksessä, ja versiosta v3.121.2 alkaen MIME-tyyppi serialisoidaan yhtenä, oikein eskapoituna PDF-nimenä. Loput tästä artikkelista käsittelevät mitä validaattori tarkistaa, yhden merkin virheen joka rikkoi text/plainin, ja paikat joissa vanhemmat julkaisut tekivät hiljaisena jotain muuta kuin pyysit

Mitä PDF/A-3:n associated file oikeasti tarvitsee?

PDF/A-3-liite läpäisee validoinnin vain kun kolme objektia ovat keskenään samaa mieltä: upotettu tiedostostream declareeraa /Type /EmbeddedFilein plus MIME-/Subtypein, tiedostospesifikaatiosanakirja (ISO 32000-2 §7.11.3) kantaa /Fin, /UFin, /EFin ja /AFRelationshipin, ja jokin dokumentissa viittaa kyseiseen tiedostospesifikaatioon /AF-taulukon kautta (ISO 32000-2 §14.13). Pelkkä upotus /Names /EmbeddedFiles-puun kautta, mikä on se mitä TPdf.CreateAttachment tekee, ei aseta koskaan assosiaatiokenttiä ylipäätään. PDFium Componentin oma PDF/A-3b-validointifixtuuri tekee riippuvuudesta konkreettisen: nimeä vain /AFRelationship-avain uudelleen ja tiedosto kaatuu täsmälleen yhteen sääntöön ISO 19005-3:n pykälässä 6.8; pudota vain MIME-/Subtype ja eri 6.8-sääntö kaatuu; laita sama liite PDF/A-1b-kandidaattiin ja se hylätään suoraan, koska PDF/A-1 kieltää upotetut tiedostot riippumatta siitä kuinka siisti metadata on

PDF/A-3:n associated filen kolmen objektin ketju PDFium Componentissa: EmbeddedFile-stream MIME-Subtypella kuten application xml, tiedostospesifikaatio jolla F, UF, EF ja AFRelationship arvolla Data, ja sille AF-taulukko katalogista tai sivusta, kolme objektia jotka validaattori tarkistaa ennen kuin ISO 19005-3:n pykälä 6.8 läpäisee
Streamin, tiedostospesifikaation ja AF-taulukon on oltava samaa mieltä; TPdf.CreateAttachmentin pelkkä nimipuu-upotus ei aseta yhtään assosiaatiokenttää eikä koskaan aseta

Relaatioarvo on se osa jota ihmiset tapaavat arvailla. TPdfAFRelationship tiedostossa FPdfAssocFiles mapaa yhden enum-jäsenen kutakin nimitokenia kohden jonka injektori voi emittoida, ja vain viisi ensimmäistä kuuluu siihen osajoukoon jonka ISO 19005-3 tunnistaa:

  • afSource → /Source: alkuperäinen josta PDF tuotettiin, kuten tekstinkäsittelytiedosto tai taulukkolaskenta
  • afData → /Data: koneellisesti luettava data josta näkyvä sisältö johdettiin tai jota se edustaa
  • afAlternative → /Alternative, afSupplement → /Supplement, afUnspecified → /Unspecified
  • afEncryptedPayload, afFormData, afTemplate: PDF 2.0:n lisäykset jotka jäävät PDF/A-3-osajoukon ulkopuolelle, joten pidä ne arkistotuotoksen ulkopuolella

Miksi /Subtype /text/plain rikkoi validoinnin?

MIME-bugi oli tokenisointivirhe, ei sääntöjenmukaisuusaukko: ennen v3.121.2:aa injektori liitti soittajan merkkijonon suoraan kauttaviivan jälkeen, tuottaen /Subtype /text/plainin. PDF-syntaksissa toinen kauttaviiva aloittaa uuden nimiobjektin (ISO 32000-1 §7.3.5), joten streamisanakirja piti yhtäkkiä avainta /Subtype, nimeä /text ja ylimääräistä roikkuvaa nimeä /plain joka saattoi avain-arvoparit epätasapainoon. Itsenäinen PDF/A-validaattori hylkäsi tiedoston jo jäsentäessään EmbeddedFile-sanakirjaa, ennen kuin se koskaan pääsi PDF/A-sääntöön, mistä syystä virhe näytti tiedoston vioittumiselta eikä puuttuvalta liiteominaisuudelta

Korjaus reitittää MIME-arvon EscapePdfNamein läpi, joka emitoi /text#2Fplainin: yhden nimen jonka dekoodattu arvo on text/plain. Escaping on tarkoituksella laajempi kuin pelkkä kauttaviiva. Jokainen tavu joka on enintään 32 (välilyönti, sarkain, CR, LF), jokainen tavu joka on vähintään 127, erottimet ()<>[]{}/% ja #-escape-merkki itse muuttuvat muotoon #XX. Vain kauttaviivan eskapoiminen olisi jättänyt toisen aukon: MIME-merkkijono joka sisältää >>n tai välilyöntiä voisi sulkea sanakirjan aikaisin tai ruiskuttaa ylimääräisiä avaimia, joten regressiotesti syöttää vihamielisen arvon jokaisella erottimella plus sarkain, LF ja CR ja tarkistaa tarkan enkoodatun tulosteen

Miksi MIME-subtyyppi text kauttaviiva plain rikkoi PDF/A-3:n jäsennyksen PDFium Componentissa: arvon liittäminen kauttaviivan jälkeen tuotti kaksi nimiobjektia, /text arvona plus roikkuva /plain joka saattoi EmbeddedFile-sanakirjan epätasapainoon, ja v3.121.2:n korjaus reitittää arvon EscapePdfNamen läpi joten /text#2Fplain on yksi nimi joka dekoodautuu muotoon text/plain
Virhe näytti tiedoston vioittumiselta koska se tapahtui jäsentimessä ennen mitään PDF/A-sääntöä; eskapoitu nimi pitää parit tasapainossa ja validaattorin lukemassa
// Mitä injektori kirjoittaa arvolle MIMEType = 'text/plain'
//   ennen v3.121.2:aa:  /Type /EmbeddedFile /Subtype /text/plain     (kaksi nimeä)
//   v3.121.2:           /Type /EmbeddedFile /Subtype /text#2Fplain   (yksi nimi)
//
// Soittajat välittävät aina tavallisen MIME-arvon. Esi-escaping sen itse
// kaksoisenkoodaa '#':n, mikä kääntää 'text#2Fplain'in arvoksi 'text#232Fplain'
Options.Files[0].MIMEType := 'text/plain';

PDF/A-3-tiedoston rakentaminen InjectAssociateFilesilla

Tuottaaksesi PDF/A-3-tuotoksen, tuota sääntöjen mukainen perusdokumentti funktiolla TPdf.SaveAsPdfAToStream ja kutsu sitten InjectAssociateFilesia kyseiselle streamille; kyseinen kaksivaiheinen putki on täsmälleen se mitä validointifixtuuri ajaa ennen kuin se läpäisee PDF/A-3b:n. TPdf.SaveAsWithAssociateFiles on mukavuuskääre, mutta se tallentaa tavallista SaveAs-polkua pitkin saRemoveSecurityilla sen sijaan että käyttäisi PDF/A-kirjoittajaa, joten se ei lisää XMP-tunnistusta ja output intentia joita PDF/A vaatii. Huomaa että tietuetyypit asuvat yksiköissä FPdfAssocFiles ja FPdfPdfa, joten molemmat kuuluvat uses-lauseeseesi. Versiosta v3.121.3 alkaen FileNamein ja Descriptionin ei enää tarvitse olla pelkkää ASCII:a: /UF ja /Desc kirjoitetaan PDF-tekstimerkkijonoina, tulostettava ASCII kirjaimellisesti ja kaikki muu UTF-16BE:nä tavujärjestysmerkin kanssa, kun taas perinteinen /F-nimi on aina kannettavaa tulostettavaa ASCII:a jokainen muu merkki korvattuna merkillä _, joten lukijat jotka dekoodaavat /Fin omalla koodisivullaan näyttävät alaviivan mojibaken sijaan. Vanhemmat buildit konvertoivat kaikki kolme järjestelmän ANSI-koodisivun kautta Delphissä tai kirjoittivat raakoja UTF-8-tavuja Free Pascalissa, joten pidä nimet ASCII:na vain jos vanhempien buildien on tuotettava sama tuloste

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: katalogitason /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';  // kirjoitetaan muotoon /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);  // kelaa Base:n takaisin; nostaa EPdfAssocFilesErrorin epäonnistuessa
    finally
      Output.Free;
    end;
  finally
    Base.Free;
  end;
end;

Katalogi vai sivu: mihin /AF-taulukko laskeutuu?

TAssocFilesOptions.TargetPage päättää /AF-taulukon omistajan: 0 kiinnittää sen katalogiin dokumenttitason assosiaationa, ja 1..N kiinnittää sen kyseisen sivun sanakirjaan, 1-pohjaisesti. Injektori lisää kaiken yhtenä inkrementaalisena päivityksenä kiinteässä asettelussa (upotetut streamit, sitten tiedostospesifikaatiot, sitten /AF-taulukko, sitten uudelleenkirjoitettu katalogi- tai sivuobjekti), joten olemassa olevat objektit pitävät siirtymänsä eikä mitään pakata uudelleen. Mikä tahansa aiempi /AF-merkintä kohdesanakirjassa korvataan, ei yhdistetä, mikä tekee toistetusta tallennuksesta idempotentin mutta tarkoittaa myös että toinen kutsu erilaisella tiedostolistalla voittaa. Kaksi käyttäytymistä ansaitsi aiemmin vartioinnin omassa koodissasi, ja molemmat ovat muuttuneet. Ennen v3.122.0:aa alueen ulkopuolinen TargetPage ei epäonnistunut; se palautui katalogiin, joten kirjoitusvirhe käänsi sivutason assosiaation dokumenttitason assosiaatioksi ilman mitään signaalia. Versiosta v3.122.0 alkaen SaveAsWithAssociateFiles ja SaveAsWithAssociateFilesToStream nostavat EPdfErrorin kun TargetPage on alueen 0..PageCount ulkopuolella, ja InjectAssociateFiles nostaa uuden EPdfAssocFilesErrorin negatiiviselle TargetPagelle tai sellaiselle joka ei nimeä olemassa olevaa sivua, jättäen kohdestreamin muuttumattomaksi. Ennen v3.121.4:ää sivuhaku skannasi tallennettuja tavuja /Type /Page-sanakirjojen varalta tiedostojärjestyksessä, mikä saattoi kiinnittää tiedoston eri sivulle kun sivuobjektit oli talletettu toisessa järjestyksessä kuin ne näytetään, esimerkiksi sivujen uudelleenjärjestämisen tai lisäämisen jälkeen; versiosta v3.121.4 alkaen TargetPage nimeää sivun kyseisessä positiossa dokumentin sivujärjestyksessä

Mihin AF-taulukko laskeutuu PDFium Componentissa: TargetPage nolla kiinnittää sen katalogiin, sivut 1–N kiinnittävät sen sivusanakirjaan, ja alueen ulkopuolinen arvo joka ennen v3.122.0:aa palautui hiljaisena katalogiin nostaa nyt poikkeuksen, kun taas injektori lisää kaiken yhtenä inkrementaalisena päivityksenä kiinteässä asettelussa joka pitää olemassa olevat siirtymät ja korvaa aiemman AF-merkinnän
Ennen v3.122.0:aa alueen ulkopuolinen TargetPage muuttui hiljaisena dokumenttitason assosiaatioksi; nykyiset julkaisut nostavat sen sijaan poikkeuksen, ja toinen kutsu erilaisella tiedostolistalla voittaa yhä
procedure AttachChartSource(Pdf: TPdf; PageNumber: Integer;
  const CsvBytes: TBytes; const OutPath: string);
var
  Options: TAssocFilesOptions;
begin
  // Versiosta v3.122.0 alkaen alueen ulkopuolinen TargetPage nostaa EPdfErrorin (vanhemmat buildit
  // palautuivat hiljaisena katalogitason /AF:iin); tarkistus ensin nimeää sivun
  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;

Miten luet AFRelationshipin takaisin luotettavasti?

TPdf.AttachmentRelationship[Index] palauttaa liitteen /AFRelationship-nimen natiivin FPDFAttachment_GetAFRelationship-viennin kautta, mutta tyhjällä merkkijonolla on kaksi mahdollista merkitystä, joten kutsu ensin AttachmentRelationshipFeaturesAvailableia. Sidonta ladataan sallivasti: kun PDFium-DLL:ltä puuttuu kyseinen vienti, jokainen relaatio lukee tyhjänä, mikä on erotamatonta tiedostospesifikaatiosta jolla yksinkertaisesti ei ole /AFRelationshipia. Property jakaa indeksinsä AttachmentCountin kanssa, joka laskee merkinnät /Names /EmbeddedFiles-puussa. Injektori kirjoittaa vain /AF-ketjun eikä lisää nimipuumerkintää, joten InjectAssociateFilesin kautta kiinnitetty tiedosto on kyseisen indeksin ulkopuolella; vahvistaaksesi injektoidun ketjun tutki tallennetut tavut tai aja PDF/A-validaattori. Kyseisen nimipuun sisäiset yksityiskohdat käsittelee artikkeli PDF-liitteiden käsittelystä Delphissä PDFium Componentilla

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;  // tyhjä vastaus olisi moniselitteinen, joten älä kysy
  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;

Mitä SaveAsWithAssociateFiles ei takaa?

TPdf.SaveAsWithAssociateFiles takaa tiedostomuotokuoren ja että pyydetyt tiedostot injektoitiin, ei sääntöjenmukaisuutta. Injektointiosa on uusi: ennen v3.122.0:aa kun tallennetuilta tavuilta puuttui luettava traileri tai katalogisanakirjaa ei löytynyt, InjectAssociateFiles kopioi syötteen läpi muuttumattomana ja metodi palautti yhä arvon True. Versiosta v3.122.0 alkaen InjectAssociateFiles nostaa EPdfAssocFilesErrorin kyseisissä tapauksissa ennen kuin kirjoittaa mitään, SaveAsWithAssociateFiles palauttaa arvon False, ja koska se rakentaa nyt kokonaisen tulosteen tallennusvarastoon ennen kohteen avaamista, hylätty tai epäonnistunut tallennus ei enää typistä olemassa olevaa tiedostoa. Tyhjä Files-taulukko kopioi yhä dokumentin läpi muuttumattomana suunnittelusta johtuen. Hyötykuorman sisältö on myös sinun vastuullasi: injektori ei tarkista että XML-tiedosto on hyvin muodostettu, että MIME-tyyppi täsmää tavuihin tai että perusdokumentti on ylipäätään PDF/A:a. Kohtele lopullista tiedostoa varifioimattomana kunnes validaattori on nähnyt sen, sama kuri jota käsittelee artikkeli PDFium Component ja PDF/A-arkistointienmukaisuus. Jos jäsentät itsekin saapuvia sanakirjoja, samat #XX-nimen säännöt pätevät päinvastoin, aihe jota käsittelee artikkeli nimitokenien sudenkuopat PDF-sanakirjoja jäsentäessä

Associated filet, PDF/A-tuotoksen, liitemetadata ja validointi toimitetaan kaikkina samassa komponentissa, joten yllä oleva putki ajaa ilman toista PDF-kirjastoa buildissa. API-referenssi, kokeiluversion lataus ja lisenssivaihtoehdot ovat PDFium Componentin tuotesivulla