Tehnični članak

Povezane datoteke PDF/A-3 in AFRelationship v Delphiju

Da iz Delphija pripnete izvorno datoteko na dokument PDF/A-3, zapiše PDFium Component verigo povezanih datotek PDF 2.0: vgrajeni tok datoteke z MIME /Subtype, specifikacijo datoteke, ki nosi /AFRelationship, in tabelo /AF, obešeno na katalog ali stran. InjectAssociateFiles in TPdf.SaveAsWithAssociateFiles zgradita to verigo v eni prirastni posodobitvi, od v3.121.2 pa se MIME tip serializira kot eno samo, pravilno ubežano ime PDF. Preostanek tega prispevka pokriva, kaj preverja validirnik, napako enega znaka, ki je podrila text/plain, in mesta, kjer so starejše izdaje tiho naredile kaj drugega, kot ste prosili

Kaj povezana datoteka PDF/A-3 dejansko potrebuje?

Priloga PDF/A-3 prestane validacijo šele, ko se trije objekti strinjajo med seboj: vgrajeni tok datoteke deklarira /Type /EmbeddedFile plus MIME /Subtype, slovar specifikacije datoteke (ISO 32000-2 §7.11.3) nosi /F, /UF, /EF in /AFRelationship, nekaj v dokumentu pa se na to specifikacijo datoteke sklicuje čez tabelo /AF (ISO 32000-2 §14.13). Golo vgrajevanje skozi drevo /Names /EmbeddedFiles, kar dela TPdf.CreateAttachment, polj združevanja sploh nikoli ne nastavi. Lasten validacijski fixture PDF/A-3b pri PDFium Component odvisnost napravi konkretno: preimenujte samo ključ /AFRelationship in datoteka podre točno eno pravilo v odstavku 6.8 ISO 19005-3; spustite samo MIME /Subtype in podre drugačno pravilo 6.8; isto prilogo dajte v kandidata PDF/A-1b in je zavrnjena na mestu, ker PDF/A-1 prepoveduje vgrajene datoteke, kar koli že urejene so metapodatki

Veriga treh objektov povezane datoteke PDF A-3 v PDFium Component: tok EmbeddedFile z MIME Subtype, kot je application xml, specifikacija datoteke s F, UF, EF in AFRelationship, nastavljenim na Data, in tabela AF zanjo iz kataloga ali strani — trije objekti, ki jih validirnik preveri, preden prestane odstavka 6.8 ISO 19005-3
Tok, specifikacija datoteke in tabela AF se morajo strinjati; golo vgrajevanje v drevo imen pri TPdf.CreateAttachment ne nastavi nobenega od polj združevanja in tudi ne bo

Vrednost razmerja je del, ki ga ljudje radi uganejo. TPdfAFRelationship v FPdfAssocFiles preslika enega člana enuma na vsak žeton imena, ki ga injektor lahko izda, in le prvih pet pripada podmnožici, ki jo prepozna ISO 19005-3:

  • afSource → /Source: izvirnik, iz katerega je PDF izdelan, na primer datoteka za obdelavo besedil ali preglednica
  • afData → /Data: stroju berljivi podatki, iz katerih je vidna vsebina izpeljana ali ki jih predstavlja
  • afAlternative → /Alternative, afSupplement → /Supplement, afUnspecified → /Unspecified
  • afEncryptedPayload, afFormData, afTemplate: dodatki PDF 2.0, ki padajo izven podmnožice PDF/A-3, zato jih iz arhivskih izhodov držite zunaj

Zakaj je /Subtype /text/plain podrila validacijo?

Napaka MIME je bila napaka žetonizacije, ne vrzel skladnosti: pred v3.121.2 je injektor niz klicatelja prilepil naravnost za poševnico, iz česar je nastalo /Subtype /text/plain. V skladnji PDF druga poševnica zažene nov objekt imena (ISO 32000-1 §7.3.5), tako da je slovar toka nenadoma nosil ključ /Subtype, ime /text in obešeno dodatno ime /plain, ki je razvažalo parove ključ-vrednost. Neodvisen validirnik PDF/A je datoteko zavrnil med razčlenjevanjem slovarja EmbeddedFile, še preden je sploh dosegel pravilo PDF/A, zato je spodletelost zgledala kot pokvarjena datoteka, ne kot manjkajoča lastnost priloge

Popravek vrednost MIME pelje skozi EscapePdfName, ki izda /text#2Fplain: eno ime, katerega dekodirana vrednost je text/plain. Ubežanje je namerno širše od poševnice. Vsak bajt na 32 ali pod njim (presledek, tabulator, CR, LF), vsak bajt na 127 ali nad njim, ločila ()<>[]{}/% in sam ubežni znak # postanejo #XX. Ubežanje samo poševnice bi pustilo drugačno vrzel: niz MIME, ki vsebuje >> ali beli prostor, bi mogel zgodaj zapreti slovar ali vbrizgati dodatne ključe, zato regresijski preizkus požre sovražno vrednost z vsakim ločilom plus tabulator, LF in CR ter preveri točno kodiran izhod

Zakaj je MIME subtype text poševnica plain podrila razčlenjevanje PDF A-3 v PDFium Component: prilepljanje vrednosti za poševnico je izdelalo dva objekta imen, /text kot vrednost plus obešeno /plain, ki je razvažala slovar EmbeddedFile, popravek v3.121.2 pa vrednost pelje skozi EscapePdfName, tako da je /text#2Fplain eno ime, ki se dekodira v text/plain
Spodletelost je zgledala kot pokvarjena datoteka, ker se je zgodila pri razčlenjevalniku, pred katerim koli pravilom PDF/A; ubežano ime ohranja parove uravnotežene in validirnik bere
// Kaj injektor zapiše za MIMEType = 'text/plain'
//   pred v3.121.2:  /Type /EmbeddedFile /Subtype /text/plain     (dve imeni)
//   v3.121.2:         /Type /EmbeddedFile /Subtype /text#2Fplain   (eno ime)
//
// Klicatelji vedno podajo običajno vrednost MIME. Če jo sami vnaprej ubežite,
// dvakrat kodirate '#', kar 'text#2Fplain' spremeni v 'text#232Fplain'
Options.Files[0].MIMEType := 'text/plain';

Gradnja datoteke PDF/A-3 z InjectAssociateFiles

Za izhod PDF/A-3 izdelajte skladni osnovni dokument s TPdf.SaveAsPdfAToStream in nato pokličite InjectAssociateFiles na tem toku; ta dvokorakni cevovod je točno tisto, kar validacijski fixture požene, preden prestane PDF/A-3b. TPdf.SaveAsWithAssociateFiles je priročni ovijalnik, shranjuje pa skozi običajno pot SaveAs s saRemoveSecurity, namesto skozi pisatelja PDF/A, zato ne doda identifikacije XMP in output intent, ki ju zahteva PDF/A. Upoštevajte, da živijo tipi zapisov v FPdfAssocFiles in FPdfPdfa, tako da pripadata obe enoti v vašemu stavku uses. Od v3.121.3 FileName in Description ne morata več biti le goli ASCII: /UF in /Desc se zapišeta kot niza besedil PDF, natisnljivi ASCII dobesedno in karkoli drugega kot UTF-16BE z oznako vrstnega reda bajtov, ime /F, starejše, pa je vedno prenosljiv natisnljivi ASCII z vsakim drugim znakom, zamenjanim s _, tako da bralci, ki /F dekodirajo s svojo kodno stranjo, pokažejo podčrtaj namesto mojibake. Starejše gradnje so vse tri pretvorile prek sistemske kodne strani ANSI na Delphiju ali zapisale surove bajte UTF-8 na Free Pascal, zato imena držite v ASCII le, če morajo starejše gradnje izdelati isti izhod

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: /AF na ravni kataloga
  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';  // zapisano kot /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);  // previje Base; ob spodletelosti sproži EPdfAssocFilesError
    finally
      Output.Free;
    end;
  finally
    Base.Free;
  end;
end;

Katalog ali stran: kam pristane tabela /AF?

TAssocFilesOptions.TargetPage odloči lastnika tabele /AF: 0 jo pripne katalogu kot združevanje na ravni dokumenta, 1..N pa slovarju te strani, z osnovo 1. Injektor doda vse kot eno prirastno posodobitev v fiksni razporeditvi (vgrajeni tokovi, nato specifikacije datotek, nato tabela /AF, nato prepisan katalog ali objekt strani), tako da obstoječi objekti obdržijo svoje odmike in se nič ne znova stisne. Vsak zgodnejši vnos /AF na ciljnem slovarju se zamenja, ne spoji, kar ponavljajoče shranjevanje naredi idempotentno, pomeni pa tudi, da zmaga drugi klic z drugačnim seznamom datotek. Dve obnašanji sta si nekdaj zaslužili varovalo v lastni kodi, oboje pa se je spremenilo. Pred v3.122.0 izven obsega TargetPage ni spodletel; vrnil se je na katalog, tako da je natipkanka pretvorila združevanje na ravni strani v združevanje na ravni dokumenta brez vsakega signala. Od v3.122.0 SaveAsWithAssociateFiles in SaveAsWithAssociateFilesToStream sprožita EPdfError, kadar je TargetPage izven 0..PageCount, InjectAssociateFiles pa sproži nov EPdfAssocFilesError za negativen TargetPage ali takšnega, ki ne poimenuje obstoječe strani, in pusti ciljni tok nespremenjen. Pred v3.121.4 je iskanje strani pregledalo shranjene bajte po slovarjih /Type /Page v vrstem redu datoteke, kar je lahko pripelo datoteko na drugačno stran, takrat ko so bili objekti strani shranjeni v drugačnem vrstem redu, kot so prikazani, na primer potem, ko so bile strani preurejene ali vstavljene; od v3.121.4 TargetPage poimenuje stran na tem položaju v vrstem redu strani dokumenta

Kam pristane tabela AF v PDFium Component: TargetPage nič jo pripne katalogu, strani 1 do N slovarju strani, vrednost izven obsega, ki se je pred v3.122.0 tiho vrnila na katalog, pa zdaj sproži izjemo, injektor pa doda vse kot eno prirastno posodobitev v fiksni razporeditvi, ki obdrži obstoječe odmike in zamenja vsak zgodnejši vnos AF
Pred v3.122.0 se je TargetPage izven obsega tiho spremenil v združevanje na ravni dokumenta; trenutne izdaje namesto tega sprožijo, drugi klic z drugačnim seznamom datotek pa še vedno zmaga
procedure AttachChartSource(Pdf: TPdf; PageNumber: Integer;
  const CsvBytes: TBytes; const OutPath: string);
var
  Options: TAssocFilesOptions;
begin
  // Od v3.122.0 TargetPage izven obsega sproži EPdfError (starejše gradnje
  // so se tiho vrnila na /AF na ravni kataloga); preizkus najprej poimenuje stran
  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;

Kako berete AFRelationship nazaj zanesljivo?

TPdf.AttachmentRelationship[Index] vrne ime /AFRelationship priloge skozi izvoz FPDFAttachment_GetAFRelationship, prazen niz pa ima dva možna pomena, zato najprej pokličite AttachmentRelationshipFeaturesAvailable. Vezava se nalaga strpno: kadar DLL PDFium tega izvoza nima, se vsako razmerje prebere kot prazno, kar je nedločljivo od specifikacije datoteke, ki preprosto nima /AFRelationship. Lastnost si deli svoj indeks s AttachmentCount, ki šteje vnose v drevesu /Names /EmbeddedFiles. Injektor zapiše le verigo /AF in ne doda vnosa v drevo imen, tako da je datoteka, pripeta prek InjectAssociateFiles, izven tega indeksa; da potrdite vbrizgano verigo, preglejte shranjene bajte ali poženite validirnik PDF/A. Notranjost tega drevesa imen pokriva delo s prilogami PDF v Delphiju s 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;  // prazen odgovor bi bil dvoumen, zato ne vprašajte
  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;

Kaj SaveAsWithAssociateFiles ne jamči?

TPdf.SaveAsWithAssociateFiles jamči ovojnico formata datoteke in to, da so bili zahtevani datoteki vbrizgani, ne skladnosti. Del vbrizga je nov: pred v3.122.0, kadar shranjeni bajti niso imeli berljivega repa ali se slovarja kataloga ni dalo najti, je InjectAssociateFiles vhod prekopiral nespremenjen skozi in metoda je še vedno vrnila True. Od v3.122.0 InjectAssociateFiles v teh primerih sproži EPdfAssocFilesError, preden kaj zapiše, SaveAsWithAssociateFiles vrne False, ker pa zdaj zgradi popoln izhod v shrambi shranjevanja, preden odpre cilj, spodletelo ali zavrnjeno shranjevanje obstoječe datoteke ni več odreže. Prazen seznam Files še vedno po načrtu prekopira dokument nespremenjen skozi. Vsebina koristnega tovora je prav tako vaša odgovornost: injektor ne preveri, da je datoteka XML dobro oblikovana, da se MIME tip ujema z bajti ali da je osnovni dokument sploh PDF/A. Končno datoteko obravnavajte kot nepreverjeno, dokler je ni videl validirnik — ista disciplina, opisana v PDFium Component in arhivski skladnosti PDF/A. Če tudi sami razčlenjujete dohodne slovarje, veljajo ista pravila imen #XX obrnjeno, tema, ki jo pokriva pasti žetonov imen pri razčlenjevanju slovarjev PDF

Povezane datoteke, izhod PDF/A, metapodatki prilog in validacija vsi odplujejo v isti komponenti, tako da cevovod zgoraj teče brez druge knjižnice PDF v gradnji. Sklic API, preizkusni prenos in licenčne možnosti so na strani izdelka PDFium Component