Da biste iz Delphi-ja zakačili izvorni fajl na PDF/A-3 dokument, PDFium Component upisuje PDF 2.0 lanac pridruženih fajlova: embedded file stream sa MIME /Subtype-om, file specification koja nosi /AFRelationship i /AF niz okačen na katalog ili stranicu. InjectAssociateFiles i TPdf.SaveAsWithAssociateFiles grade taj lanac u jednom inkrementalnom ažuriranju, i od v3.121.2 MIME tip se serijalizuje kao jedno, pravilno escapovano PDF ime. Ostatak teksta pokriva šta validator proverava, bug od jednog znaka koji je slomio text/plain i mesta na kojima su starija izdanja tiho radila nešto drugačije od onoga što ste tražili
Šta PDF/A-3 pridruženi fajl zapravo treba?
PDF/A-3 prilog prolazi validaciju samo kada se tri objekta slažu međusobno: embedded file stream deklariše /Type /EmbeddedFile plus MIME /Subtype, rečnik file specification (ISO 32000-2 §7.11.3) nosi /F, /UF, /EF i /AFRelationship, i nešto u dokumentu referencira tu file specification kroz /AF niz (ISO 32000-2 §14.13). Obično ugrađivanje kroz /Names /EmbeddedFiles stablo, kakvo radi TPdf.CreateAttachment, polja asocijacije uopšte ne postavlja. PDFium Component-ov sopstveni PDF/A-3b validacioni fixture tu zavisnost čini konkretnom: preimenujte samo ključ /AFRelationship i fajl pada na tačno jedno pravilo u tački 6.8 ISO 19005-3; izbacite samo MIME /Subtype i pada drugo pravilo iz 6.8; ubacite isti prilog u PDF/A-1b kandidata i odmah je odbijen, jer PDF/A-1 zabranjuje ugrađene fajlove ma koliko metapodaci bili uredni
Vrednost relacije je deo oko koga ljudi rado nagađaju. TPdfAFRelationship u FPdfAssocFiles-u preslikava po jednog člana enum-a na svaki name token koji injektor ume da emituje, i samo prvih pet pripada podskupu koji ISO 19005-3 prepoznaje:
afSource→/Source: original iz koga je PDF proizveden, poput fajla iz tekst procesora ili tabeleafData→/Data: mašinski čitljivi podaci iz kojih je vidljivi sadržaj izveden ili koje predstavljaafAlternative→/Alternative,afSupplement→/Supplement,afUnspecified→/UnspecifiedafEncryptedPayload,afFormData,afTemplate: PDF 2.0 dodaci van PDF/A-3 podskupa, pa ih držite van arhivskog izlaza
Zašto je /Subtype /text/plain slomilo validaciju?
MIME bug bila je greška u tokenizaciji, ne rupa u usaglašenosti: pre v3.121.2 injektor je string pozivaoca lepio pravo iza kose crte, proizvodeći /Subtype /text/plain. Po PDF sintaksi druga kosa crta počinje novi name objekat (ISO 32000-1 §7.3.5), pa je rečnik streama iznenada držao ključ /Subtype, ime /text i višak, objeseno ime /plain koje je izbacilo iz ravnoteže parove ključ-vrednost. Nezavisni PDF/A validator odbio je fajl tokom parsiranja EmbeddedFile rečnika, pre nego što je uopšte došao do nekog PDF/A pravila, pa je kvar izgledao kao oštećen fajl, a ne kao nedostajuće svojstvo priloga
Popravka MIME vrednost provlači kroz EscapePdfName, koji emituje /text#2Fplain: jedno ime čija je dekodovana vrednost text/plain. Escapovanje je namerno šire od same kose crte. Svaki bajt na ili ispod 32 (razmak, tab, CR, LF), svaki bajt na ili iznad 127, graničnici ()<>[]{}/% i sam # escape znak postaju #XX. Bekstvo samo kose crte ostavilo bi drugu rupu: MIME string koji sadrži >> ili beli prostor mogao je ranije zatvoriti rečnik ili ubaciti dodatne ključeve, pa regresioni test daje neprijateljsku vrednost sa svakim graničnikom plus tab, LF i CR i proverava tačan enkodovani izlaz
// Šta injektor upisuje za MIMEType = 'text/plain'
// pre v3.121.2: /Type /EmbeddedFile /Subtype /text/plain (dva imena)
// v3.121.2: /Type /EmbeddedFile /Subtype /text#2Fplain (jedno ime)
//
// Pozivaoci uvek predaju običnu MIME vrednost. Ako je sami pre-escapujete
// duplo enkodujete '#', što od 'text#2Fplain' pravi 'text#232Fplain'
Options.Files[0].MIMEType := 'text/plain';
Građenje PDF/A-3 fajla uz InjectAssociateFiles
Za PDF/A-3 izlaz, proizvedite usaglašen osnovni dokument sa TPdf.SaveAsPdfAToStream, a zatim pozovite InjectAssociateFiles nad tim streamom; taj dvokoračni pipeline je baš ono što validacioni fixture pokreće pre nego što prođe PDF/A-3b. TPdf.SaveAsWithAssociateFiles je udobni omot, ali čuva preko obične SaveAs putanje sa saRemoveSecurity umesto preko PDF/A pisca, pa ne dodaje XMP identifikaciju i output intent koje PDF/A traži. Primetite da tipovi zapisa žive u FPdfAssocFiles-u i FPdfPdfa-i, pa obe jedinice pripadaju vašoj uses klauzuli. Od v3.121.3, FileName i Description više ne moraju biti čist ASCII: /UF i /Desc upisuju se kao PDF text stringovi, štampani ASCII bukvalno a sve ostalo kao UTF-16BE sa byte order mark-om, dok je zastarelo /F ime uvek prenosivi štampani ASCII sa svakim drugim znakom zamenjenim za _, pa čitači koji /F dekoduju svojom code page stranicom pokazuju donju crtu umesto krnjeg teksta. Stariji buildovi sva tri pretvarali su kroz sistemsku ANSI code page stranicu na Delphi-ju ili pisali sirove UTF-8 bajtove na Free Pascal-u, pa imena držite ASCII samo ako stariji buildovi moraju dati identičan izlaz
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 nivou 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'; // upisano kao /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); // namota Base nazad; diže EPdfAssocFilesError pri padu
finally
Output.Free;
end;
finally
Base.Free;
end;
end;
Katalog ili stranica: gde sleće /AF niz?
TAssocFilesOptions.TargetPage odlučuje ko poseduje /AF niz: 0 ga kači na katalog kao asocijaciju na nivou dokumenta, a 1..N na rečnik te stranice, od 1. Injektor sve dodaje kao jedno inkrementalno ažuriranje u fiksnom rasporedu (ugrađeni streamovi, pa file specification-i, pa /AF niz, pa prepisan katalog ili objekat stranice), pa postojeći objekti zadržavaju svoje offsete i ništa se ne rekompresuje. Svaki raniji /AF unos na ciljnom rečniku zamenjuje se, ne spaja, što ponovljeno čuvanje čini idempotentnim ali znači i da drugi poziv sa drugačijom listom fajlova pobeđuje. Dva ponašanja su nekada zasluživala čuvu u vašem sopstvenom kodu, i obe su se promenila. Pre v3.122.0 TargetPage van opsega nije padao; vraćao se na katalog, pa je greška u kucanju pretvarala asocijaciju sa nivoa stranice u dokumentnu bez ikakvog signala. Od v3.122.0, SaveAsWithAssociateFiles i SaveAsWithAssociateFilesToStream dižu EPdfError kada je TargetPage van 0..PageCount, a InjectAssociateFiles diže novi EPdfAssocFilesError za negativan TargetPage ili onaj koji ne imenuje postojeću stranicu, ostavljajući odredišni stream netaknut. Pre v3.121.4 pretraga stranice skenirala je sačuvane bajtove za /Type /Page rečnike po redosledu u fajlu, što je fajl moglo zakačiti za drugu stranicu čim su objekti stranica zapisani drugačijim redosledom nego što se prikazuju, na primer posle premeštanja ili ubacivanja stranica; od v3.121.4 TargetPage imenuje stranicu na toj poziciji u redosledu stranica dokumenta
procedure AttachChartSource(Pdf: TPdf; PageNumber: Integer;
const CsvBytes: TBytes; const OutPath: string);
var
Options: TAssocFilesOptions;
begin
// Od v3.122.0 TargetPage van opsega diže EPdfError (stariji buildovi
// su tiho padali na /AF nivou kataloga); provera prvo imenuje stranicu
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 pouzdano pročitati AFRelationship nazad?
TPdf.AttachmentRelationship[Index] vraća /AFRelationship ime priloga kroz nativni FPDFAttachment_GetAFRelationship export, ali prazan string ima dva moguća značenja, pa prvo pozovite AttachmentRelationshipFeaturesAvailable. Povezivanje se učitava blago: kada PDFium DLL nema taj export, svaka relacija čita se kao prazna, što se ne razlikuje od file specification koja jednostavno nema /AFRelationship. Svojstvo deli indeks sa AttachmentCount-om, koji broji unose u /Names /EmbeddedFiles stablu. Injektor upisuje samo /AF lanac i ne dodaje unos u name-tree, pa je fajl zakačen kroz InjectAssociateFiles van tog indeksa; da potvrdite ubačeni lanac, pregledajte sačuvane bajtove ili pustite PDF/A validator. Unutrašnjost tog stabla imena pokriva rad sa PDF prilozima u Delphi-ju uz 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; // prazan odgovor bio bi dvosmislen, pa nemojte ni pitati
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;
Šta SaveAsWithAssociateFiles ne garantuje?
TPdf.SaveAsWithAssociateFiles garantuje omot fajl formata i da su traženi fajlovi ubačeni, a ne usaglašenost. Deo sa ubacivanjem je nov: pre v3.122.0, kada sačuvani bajtovi nisu imali čitljiv trailer ili kada se rečnik kataloga nije mogao locirati, InjectAssociateFiles prepisao bi ulaz nepromenjen a metod bi i dalje vratio True. Od v3.122.0 InjectAssociateFiles u tim slučajevima diže EPdfAssocFilesError pre nego što bilo šta napiše, SaveAsWithAssociateFiles vraća False, i pošto sada kompletan izlaz gradi u spremištu pre nego što otvori cilj, odbijeno ili pokvareno čuvanje više ne odseca postojeći fajl. Prazan Files niz i dalje po dizajnu prepisuje dokument nepromenjen. Sadržaj payload-a je isto vaša odgovornost: injektor ne proverava da li je XML fajl dobro oblikovan, da li MIME tip odgovara bajtovima, niti da li je osnovni dokument uopšte PDF/A. Konačni fajl tretirajte kao neverifikovan dok ga validator ne vidi, ista disciplina koju opisuje PDFium Component i PDF/A arhivska usaglašenost. Ako i sami parsirate pristigle rečnike, ista #XX pravila imena važe i obrnuto, tema koju pokriva zamke name tokena pri parsiranju PDF rečnika
Pridruženi fajlovi, PDF/A izlaz, metapodaci priloga i validacija stižu u istoj komponenti, pa pipeline gore radi bez druge PDF biblioteke u buildu. API referenca, probna verzija i opcije licenciranja su na stranici proizvoda PDFium Component