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 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ázatafData→/Data: géppel olvasható adat, amelyből a látható tartalom származik, vagy amelyet az ábrázolafAlternative→/Alternative,afSupplement→/Supplement,afUnspecified→/UnspecifiedafEncryptedPayload,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
// 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
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