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
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 taulukkolaskentaafData→/Data: koneellisesti luettava data josta näkyvä sisältö johdettiin tai jota se edustaaafAlternative→/Alternative,afSupplement→/Supplement,afUnspecified→/UnspecifiedafEncryptedPayload,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
// 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ä
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