Műszaki cikk

PDF/A-3 társított fájlok és AFRelationship Delphiben

Ahhoz, hogy Delphiből forrásfájlt csatoljon egy PDF/A-3 dokumentumhoz, a PDFium Component egy PDF 2.0-s associated-file láncot ír: beágyazott fájlstreamet MIME /Subtype-tal, /AFRelationship-et hordozó fájlspecifikációt, valamint egy /AF tömböt, amely a katalógusra vagy egy oldalra akasztódik. Az InjectAssociateFiles és a TPdf.SaveAsWithAssociateFiles egyetlen növekményes frissítésben építi fel azt a láncot, a v3.121.2 óta pedig a MIME típus egyetlen, helyesen escape-elt PDF-névként szerializálódik. A bejegyzés többi része azt tárgyalja, mit ellenőriz egy validátor, azt az egykarakteres hibát, amely megtörte a text/plain-t, és azokat a helyeket, ahol a régebbi kiadások csendben mást tettek, mint amit Ön kért

Mire van valójában szüksége egy PDF/A-3 társított fájlnak?

Egy PDF/A-3 csatolmány csak akkor megy át az ellenőrzésen, ha három objektum egyetért egymással: a beágyazott fájlstream /Type /EmbeddedFile-t deklarál plusz MIME /Subtype-ot, a fájlspecifikációs szótár (ISO 32000-2 §7.11.3) /F-et, /UF-ot, /EF-et és /AFRelationship-et hordoz, és valami a dokumentumban egy /AF tömbön át hivatkozik arra a fájlspecifikációra (ISO 32000-2 §14.13). A /Names /EmbeddedFiles fán át való sima beágyazás, amit a TPdf.CreateAttachment csinál, a társítás mezőit egyáltalán nem állítja be. A PDFium Component saját PDF/A-3b ellenőrzési tesztállománya kézzelfoghatóvá teszi a függőséget: nevezze át csak az /AFRelationship kulcsot, és a fájl pontosan egy szabályon bukik meg az ISO 19005-3 6.8 klauzulájában; dobja el csak a MIME /Subtype-ot, és egy másik 6.8-as szabály bukik el; tegye ugyanezt a csatolmányt egy PDF/A-1b jelöltbe, és lapból elutasításra kerül, mert a PDF/A-1 a beágyazott fájlokat tiltja, bármilyen rendes is a metaadat

A PDF A-3 társított fájl háromobjektumos lánca a PDFium Componentben: EmbeddedFile stream MIME Subtype-tal, például application xml-lel, fájlspecifikáció F-fel, UF-fal, EF-fel és Data-ra állított AFRelationship-szel, valamint neki egy AF tömb a katalógusból vagy egy oldalról — azt a három objektumot ellenőriz egy validátor, mielőtt az ISO 19005-3 6.8 klauzulája átmenne
Stream, fájlspecifikáció és AF tömb egyetértsen; a TPdf.CreateAttachment sima névfa-beágyazása a társítás mezőit egyiket sem állítja be, és sosem fogja

A kapcsolat értéke az a rész, amit az emberek szívesen találgatnak. A FPdfAssocFiles-ban élő TPdfAFRelationship minden névtokenre egy-egy enum taget képez, amelyet az injektor kiírhat, és csak az első öt tartozik ahhoz a részhalmazhoz, amelyet az ISO 19005-3 felismer:

  • afSource → /Source: az eredeti, amelyből a PDF készült, például szövegszerkesztő fájl vagy táblázat
  • afData → /Data: géppel olvasható adat, amelyből a látható tartalom származik, vagy amelyet az ábrázol
  • afAlternative → /Alternative, afSupplement → /Supplement, afUnspecified → /Unspecified
  • afEncryptedPayload, afFormData, afTemplate: PDF 2.0-s újdonságok, amelyek a PDF/A-3 részhalmazon kívül esnek, ezért tartsa őket távol az archív kimenettől

Miért törte meg az ellenőrzést a /Subtype /text/plain?

A MIME hiba tokenizálási hiba volt, nem megfelelőségi rés: a v3.121.2 előtt az injektor a hívó karakterláncát egyenessen a perjel után fűzte, így lett belőle /Subtype /text/plain. A PDF szintaxisban a második perjel új name objektumot nyit (ISO 32000-1 §7.3.5), tehát a stream szótárban hirtelen ott állt a /Subtype kulcs, a /text név, és egy feleslegesen lógó /plain név, amely szétszedte a kulcs-érték párokat. Egy független PDF/A validátor már az EmbeddedFile szótár elemzésekor elutasította a fájlt, még mielőtt bármilyen PDF/A szabályhoz ért volna — ezért nézett ki a hiba fájlsérülésként, nem hiányzó csatolmány-tulajdonságként

A javítás a MIME értéket a EscapePdfName-en vezeti át, amely /text#2Fplain-t ad: egyetlen nevet, amelynek dekódolt értéke text/plain. Az escape-elés szándékosan tágabb a perjelnél. Minden 32-es vagy alatti bájt (szóköz, tab, CR, LF), minden 127-es vagy feletti bájt, a ()<>[]{}/% határolók és a # escape karakter maga #XX-dé változik. Csak a perjel escape-elése más lyukat hagyott volna: egy >>-t vagy üres helyet tartalmazó MIME karakterlánc korán becsukhatta volna a szótárat, vagy extra kulcsokat injektálhatott volna, ezért a regressziós teszt minden határolót, plusz tabot, LF-et és CR-t tartalmazó ellenséges értéket ad be, és a pontos kódolt kimenetet ellenőrzi

Miért törte meg a MIME altípus, a text perjellel plain, a PDF A-3 elemzését a PDFium Componentben: az érték perjel utáni összefűzése két name objektumot adott, /text értékként plusz egy lógó /plain, amely szétszedte az EmbeddedFile szótárat, a v3.121.2-es javítás pedig az értéket az EscapePdfName-en vezeti át, így a /text#2Fplain egy name, amely text/plain-re dekódolódik
A hiba fájlsérülésnek tűnt, mert az elemzőnél történt, minden PDF/A szabály előtt; az escape-elt név a párokat egyensúlyban tartja, a validátort pedig olvasásban
// Amit az injektor ír a MIMEType = 'text/plain' esetére
//   a v3.121.2 előtt:  /Type /EmbeddedFile /Subtype /text/plain     (két name)
//   v3.121.2:          /Type /EmbeddedFile /Subtype /text#2Fplain   (egy name)
//
// A hívók mindig a közönséges MIME értéket adják át. Ha előre maga escape-eli,
// a '#' kétszer kódolódik, és a 'text#2Fplain'-ből 'text#232Fplain' lesz
Options.Files[0].MIMEType := 'text/plain';

PDF/A-3 fájl építése az InjectAssociateFiles-szel

PDF/A-3 kimenethez állítsa elő a megfelelő alapdokumentumot a TPdf.SaveAsPdfAToStream-mel, majd hívja meg azon a streamen az InjectAssociateFiles-t; ezt a kétlépéses futószalagot futtatja pontosan az ellenőrzési tesztállomány, mielőtt átmenne PDF/A-3b-n. A TPdf.SaveAsWithAssociateFiles a kényelmi csomagoló, de a közönséges SaveAs úton menti saRemoveSecurity-szel, nem a PDF/A írón át, így nem adja hozzá azt az XMP-azonosítást és kimeneti szándékot, amelyet a PDF/A megkövetel. Vegye figyelembe, hogy a rekordtípusok a FPdfAssocFiles-ban és a FPdfPdfa-ban élnek, tehát mindkét unitnak benne kell lennie az Ön uses záradékában. A v3.121.3 óta a FileName-nak és a Description-nek nem kell sima ASCII-nak lennie: az /UF és a /Desc PDF szöveges karakterláncként íródik, a nyomtatható ASCII szó szerint, minden más UTF-16BE-ként bájtsorrend-jelölővel, míg a legacy /F név mindig hordozható, nyomtatható ASCII, minden más karakter _-re cserélve, így azok az olvasók, amelyek az /F-et saját kódlapjukkal dekódolják, aláhúzást mutatnak mojibake helyett. A korábbi build-ek mindhármat a rendszer ANSI kódlapján konvertálták Delphiben, vagy nyers UTF-8 bájtokat írtak Free Pascalban, ezért csak akkor tartson a neveket ASCII-n, ha régebbi build-eknek kell azonos kimenetet gyártaniuk

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: katalógusszintű /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';  // /application#2Fxml-ként írva

  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);  // visszatekeri a Base-t; hibánál EPdfAssocFilesError-t dob
    finally
      Output.Free;
    end;
  finally
    Base.Free;
  end;
end;

Katalógus vagy oldal: hova kerül az /AF tömb?

A TAssocFilesOptions.TargetPage dönti el, kié az /AF tömb: a 0 a katalógushoz csatolja dokumentumszintű társításként, az 1..N pedig az adott oldal szótárához, 1-alapúan. Az injektor mindent egyetlen növekményes frissítésként fűz hozzá rögzített elrendezésben (előbb a beágyazott streamek, aztán a fájlspecifikációk, majd az /AF tömb, végül egy újraírt katalógus vagy oldalobjektum), így a meglévő objektumok megtartják eltolásaikat, és semmi nem tömörítődik újra. A célszótár korábbi /AF bejegyzése helyettesítődik, nem összeolvad, ami az ismételt mentést idempotentté teszi, de azt is jelenti, hogy egy másik fájllistával történő második hívás nyer. Két viselkedés megérdemelt volna egy őrt az Ön kódjában, és mindkettő megváltozott. A v3.122.0 előtt a tartományon kívüli TargetPage nem bukott el; visszaesett a katalógusra, tehát egy elírás jel nélkül tett oldalszintű társításból dokumentumszintűt. A v3.122.0 óta a SaveAsWithAssociateFiles és a SaveAsWithAssociateFilesToStream EPdfError-t dob, ha a TargetPage a 0..PageCount intervallumon kívül van, az InjectAssociateFiles pedig az új EPdfAssocFilesError-t dobja negatív TargetPage-re vagy olyanra, amely nem nevez meg létező oldalt, és a célstreamet érintetlenül hagyja. A v3.121.4 előtt az oldalkeresés fájlrendben szögte az elmentett bájtok /Type /Page szótárait, ami azután másik oldalra akaszthatta a fájlt, hogy az oldalobjektumok más sorrendben kerültek tárolásra, mint ahogy megjelennek — például oldalak átrendezése vagy beszúrása után; a v3.121.4 óta a TargetPage a dokumentum oldalsorrendjében az adott pozíción álló oldalt nevezi meg

Hova kerül az AF tömb a PDFium Componentben: a TargetPage nulla a katalógushoz csatolja, az 1–N oldalak az oldal szótárához, a tartományon kívüli érték — amely a v3.122.0 előtt csendben a katalógusra esett vissza — ma már kivételt dob, miközben az injektor mindent egyetlen növekményes frissítésként fűz hozzá rögzített elrendezésben, megtartva a meglévő eltolásokat és helyettesítve minden korábbi AF bejegyzést
A v3.122.0 előtt egy tartományon kívüli TargetPage csendben dokumentumszintű társítássá vált; a mostani kiadások helyette kivételt dobnak, és egy másik fájllistával történő második hívás továbbra is nyer
procedure AttachChartSource(Pdf: TPdf; PageNumber: Integer;
  const CsvBytes: TBytes; const OutPath: string);
var
  Options: TAssocFilesOptions;
begin
  // A v3.122.0 óta a tartományon kívüli TargetPage EPdfError-t dob (régebbi build-ek
  // csendben katalógusszintű /AF-re esettek vissza); az előzetes ellenőrzés megnevezi az oldalt
  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;

Hogyan olvassa vissza megbízhatóan az AFRelationship-et?

A TPdf.AttachmentRelationship[Index] egy csatolmány /AFRelationship nevét adja vissza a natív FPDFAttachment_GetAFRelationship exporton át, de az üres karakterláncnak két lehetséges jelentése van, ezért előbb hívja a AttachmentRelationshipFeaturesAvailable-t. A kötés toleránsan töltődik: ha a PDFium DLL-ből hiányzik az export, minden kapcsolat üresen olvas, ami megkülönböztethetetlen attól a fájlspecifikációtól, amely egyszerűen nem hordoz /AFRelationship-et. A tulajdonság az indexét a AttachmentCount-tal osztja meg, amely a /Names /EmbeddedFiles fa bejegyzéseit számolja. Az injektor csak az /AF láncot írja, névfa-bejegyzést nem ad hozzá, így az InjectAssociateFiles-szel csatolt fájl ezen az indexen kívül esik; az injektált lánc megerősítéséhez vizsgálja meg az elmentett bájtokat, vagy futtasson PDF/A validátort. Annak a névfanak a belső felépítését a PDF csatolmányok kezelése Delphiben a PDFium Componenttel tárgyalja

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;  // az üres válasz kétértelmű lenne, ezért ne is kérdezze
  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 nem garantál a SaveAsWithAssociateFiles?

A TPdf.SaveAsWithAssociateFiles a fájlformátum-borítékot garantálja, és azt, hogy a kért fájlok bekerültek, nem a megfelelőséget. A beillesztési rész újdonság: a v3.122.0 előtt, ha az elmentett bájtokból hiányzott az olvasható trailer, vagy a katalógusszótár nem volt megtalálható, az InjectAssociateFiles a bemenetet változatlanul átmásolta, és a metódus továbbra is True-t adott. A v3.122.0 óta az InjectAssociateFiles ezekben az esetekben még az írás előtt EPdfAssocFilesError-t dob, a SaveAsWithAssociateFiles False-ot ad, és mivel ma már a teljes kimenetet megnyitás előtt felépíti egy mentési tárolóban, egy elutasított vagy elbukott mentés nem csonkít meg meglévő fájlt. Az üres Files tömb tervezett módon továbbra is változatlanul átmásolja a dokumentumot. A tartalom is az Ön felelőssége: az injektor nem ellenőrzi, hogy egy XML fájl jól formált-e, hogy a MIME típus egyezik-e a bájtokkal, vagy hogy az alapdokumentum egyáltalán PDF/A-e. A végfájlt addig ellenőrizetlennek kezelje, amíg egy validátor nem látta — ugyanaz a fegyelem, amelyet a PDFium Component és a PDF/A archiválási megfelelőség ír le. Ha Ön maga is elemez bejövő szótárakat, ugyanazok a #XX névszabályok fordítva is élnek, amelyet a névtoken-csapdákról PDF szótárak elemzésekor szóló anyag tárgyal

A társított fájlok, a PDF/A kimenet, a csatolmány-metaadatok és az ellenőrzés ugyanabban a komponensben érkezik, így a fenti futószalag a buildben második PDF-könyvtár nélkül fut. Az API referencia, a próbaverzió és a licencelési lehetőségek a PDFium Component termékoldalán