Da bi iz Delphija prikačili izvornu datoteku uz PDF/A-3 dokument, PDFium Component zapisuje PDF 2.0 lanac associated-fileova: embedded file stream s MIME /Subtype-om, file specifikaciju koja nosi /AFRelationship i /AF niz obješen na katalog ili stranicu. InjectAssociateFiles i TPdf.SaveAsWithAssociateFiles taj lanac grade u jednom inkrementalnom updateu, a od v3.121.2 MIME tip serializira se kao jedno ispravno escapirano PDF ime. Ostatak ovog posta pokriva što validator provjerava, grešku od jednog znaka koja je slomila text/plain, i mjesta gdje su starija izdanja tiho radila nešto drugo nego što ste tražili
Što PDF/A-3 pridružena datoteka zapravo treba?
PDF/A-3 privitak prolazi validaciju samo kad se tri objekta slažu jedan s drugim: embedded file stream deklarira /Type /EmbeddedFile plus MIME /Subtype, rječnik file specifikacije (ISO 32000-2 §7.11.3) nosi /F, /UF, /EF i /AFRelationship, i nešto u dokumentu referencira tu file specifikaciju kroz /AF niz (ISO 32000-2 §14.13). Obično ugrađivanje kroz /Names /EmbeddedFiles stablo, koje radi TPdf.CreateAttachment, polja asocijacije uopće ne postavlja. PDFium Componentov vlastiti PDF/A-3b validacijski fixture tu ovisnost čini konkretnom: preimenujte samo ključ /AFRelationship pa datoteka pada na točno jedno pravilo u klauzuli 6.8 ISO 19005-3; skinite samo MIME /Subtype pa pada drugo pravilo 6.8; stavite isti privitak u PDF/A-1b kandidata pa se odbacuje odmah, jer PDF/A-1 zabranjuje ugrađene datoteke koliko god bili uredni metapodaci
Vrijednost relacije dio je koji ljudi vole pogađati. TPdfAFRelationship u FPdfAssocFiles mapira po jednog člana enuma na svaki name token koji injector smije ispisati, i samo prvih pet pripada podskupu koji ISO 19005-3 prepoznaje:
afSource→/Source: izvornik iz kojeg je PDF proizveden, poput datoteke za obradu teksta ili proračunske tabliceafData→/Data: strojno čitljivi podaci iz kojih je vidljivi sadržaj izveden ili ih on predstavljaafAlternative→/Alternative,afSupplement→/Supplement,afUnspecified→/UnspecifiedafEncryptedPayload,afFormData,afTemplate: PDF 2.0 dodaci koji ispadaju iz PDF/A-3 podskupa, pa ih držite izvan arhivskog izlaza
Zašto je /Subtype /text/plain slomio validaciju?
MIME greška bila je greška tokenizacije, ne rupa u usklađenosti: prije v3.121.2 injector je string pozivatelja nadovezao ravno iza kose crte, proizvodeći /Subtype /text/plain. U PDF sintaksi druga kosa crta počinje novi name objekt (ISO 32000-1 §7.3.5), pa je rječnik streama odjednom držao ključ /Subtype, ime /text i višak ime /plain koje je ostalo bez para i izbacilo iz ravnoteže parove ključ-vrijednost. Nezavisni PDF/A validator odbacio je datoteku dok je parsirao rječnik EmbeddedFile, prije nego je uopće došao do PDF/A pravila, pa je pad izgledao kao oštećenje datoteke a ne nedostajuće svojstvo privitka
Popravak MIME vrijednost provlači kroz EscapePdfName, koji ispisuje /text#2Fplain: jedno ime čija je dekodirana vrijednost text/plain. Escaping je namjerno širi od kose crte. Svaki bajt na 32 ili ispod (razmak, tab, CR, LF), svaki bajt na 127 ili iznad, graničnici ()<>[]{}/% i sam # znak za escapiranje postaju #XX. Escapiranje samo kose crte ostavilo bi drugu rupu: MIME string koji sadrži >> ili bjelinu mogao bi zatvoriti rječnik ranije ili ugnijezditi višak ključeva, pa u regresijski test ide neprijateljska vrijednost sa svim graničnicima plus tab, LF i CR i provjerava se točan kodirani izlaz
// Što injector zapisuje za MIMEType = 'text/plain'
// prije v3.121.2: /Type /EmbeddedFile /Subtype /text/plain (dva imena)
// v3.121.2: /Type /EmbeddedFile /Subtype /text#2Fplain (jedno ime)
//
// Pozivatelji uvijek šalju običnu MIME vrijednost. Ako je sami pre-escapate
// dvostruko kodirate '#' što 'text#2Fplain' pretvara u 'text#232Fplain'
Options.Files[0].MIMEType := 'text/plain';
Gradnja PDF/A-3 datoteke s InjectAssociateFiles
Za PDF/A-3 izlaz, proizvedite sukladan bazni dokument s TPdf.SaveAsPdfAToStream pa na taj stream pozovite InjectAssociateFiles; ta dvostupanjska pipeline točno je ono što validacijski fixture izvodi prije nego prođe PDF/A-3b. TPdf.SaveAsWithAssociateFiles je udobna omota, ali sprema kroz običan SaveAs put s saRemoveSecurity umjesto kroz PDF/A writer, pa ne dodaje XMP identifikaciju i output intent koje PDF/A traži. Imajte na umu da tipovi zapisa žive u FPdfAssocFiles i FPdfPdfa, pa obje unit-e pripadaju u Vašu uses klauzulu. Od v3.121.3 FileName i Description više ne moraju biti čisti ASCII: /UF i /Desc zapisuju se kao PDF text stringovi, ispisivi ASCII doslovno a sve ostalo kao UTF-16BE s byte order markom, dok se naslijeđeno /F ime uvijek piše kao prenosivi ispisivi ASCII sa svakim drugim znakom zamijenjenim s _, pa čitači koji /F dekodiraju vlastitom kodnom stranicom prikažu podcrtu umjesto razbacanih krivo kodiranih znakova. Stariji buildovi sva tri pretvarali su kroz sistemsku ANSI kodnu stranicu na Delphiju ili pisali sirove UTF-8 bajtove na Free Pascalu, pa imena držite ASCII samo ako stariji buildovi moraju proizvesti isti 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 razini 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'; // zapisuje se 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 natrag; podiže EPdfAssocFilesError pri padu
finally
Output.Free;
end;
finally
Base.Free;
end;
end;
Katalog ili stranica: gdje slijeće /AF niz?
TAssocFilesOptions.TargetPage odlučuje vlasnika /AF niza: 0 ga kači na katalog kao asocijaciju razine dokumenta, a 1..N na rječnik te stranice, od 1. Injector sve dodaje kao jedan inkrementalni update u fiksnom rasporedu (embedded streamovi, pa file specifikacije, pa /AF niz, pa prepisani katalog ili page objekt), pa postojeći objekti zadržavaju svoje pomake i ništa se ne rekomprimira. Svaki raniji /AF unos na ciljanom rječniku zamjenjuje se, a ne spaja, što ponovljeno spremanje čini idempotentnim ali znači i da drugi poziv s drugačijim popisom datoteka pobjeđuje. Dva ponašanja nekoć su zasluživala vlastitu strazu u Vašem kodu, i oboje se promijenilo. Prije v3.122.0 TargetPage izvan raspona nije padao; padao je natrag na katalog, pa je greška u kucanju asocijaciju razine stranice pretvarala u asocijaciju razine dokumenta bez ikakvog signala. Od v3.122.0 SaveAsWithAssociateFiles i SaveAsWithAssociateFilesToStream podižu EPdfError kad je TargetPage izvan 0..PageCount, a InjectAssociateFiles podiže novi EPdfAssocFilesError za negativan TargetPage ili onaj koji ne imenuje postojeću stranicu, ostavljajući odredišni stream nepromijenjenim. Prije v3.121.4 pretraga stranice skenirala je spremljene bajtove za rječnike /Type /Page u redoslijedu datoteke, što je moglo prikačiti datoteku na drugu stranicu jednom kad su se page objekti pohranili u drugačijem redoslijedu nego što se prikazuju, primjerice nakon što su stranice presložene ili umetnute; od v3.121.4 TargetPage imenuje stranicu na toj poziciji u redoslijedu stranica dokumenta
procedure AttachChartSource(Pdf: TPdf; PageNumber: Integer;
const CsvBytes: TBytes; const OutPath: string);
var
Options: TAssocFilesOptions;
begin
// Od v3.122.0 TargetPage izvan raspona podiže EPdfError (stariji buildovi
// tiho su padali na /AF razine kataloga); provjera ispred 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 AFRelationship pouzdano pročitati natrag?
TPdf.AttachmentRelationship[Index] vraća /AFRelationship ime privitka kroz nativni export FPDFAttachment_GetAFRelationship, ali prazan string ima dva moguća značenja, pa prvo pozovite AttachmentRelationshipFeaturesAvailable. Binding se loada blago: kad PDFium DLL-u fali taj export, svaka relacija čita se kao prazna, što je nerazlučivo od file specifikacije koja jednostavno nema /AFRelationship. Svojstvo dijeli indeks s AttachmentCount, koji broji unose u /Names /EmbeddedFiles stablu. Injector piše samo /AF lanac i ne dodaje name-tree unos, pa je datoteka prikačena kroz InjectAssociateFiles izvan tog indeksa; da potvrdite injektirani lanac, pregledajte spremljene bajtove ili pustite PDF/A validator. Unutrašnjost tog name stabla pokrivena je u radu s PDF privicima u Delphiju 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;
Što SaveAsWithAssociateFiles ne jamči?
TPdf.SaveAsWithAssociateFiles jamči omotnicu formata datoteke i da su zatražene datoteke injektirane, a ne usklađenost. Dio o injektiranju je nov: prije v3.122.0, kad spremljeni bajtovi nisu imali čitljiv trailer ili se rječnik kataloga nije mogao locirati, InjectAssociateFiles kopirao je ulaz kroz nepromijenjen a metoda je i dalje vraćala True. Od v3.122.0 InjectAssociateFiles u tim slučajevima podiže EPdfAssocFilesError prije nego što bilo što napiše, SaveAsWithAssociateFiles vraća False, i budući da sada gradi kompletan izlaz u save storeu prije nego otvori cilj, odbijeno ili palo spremanje više ne odrezuje postojeću datoteku. Prazan Files niz dizajnom i dalje provlači dokument kroz nepromijenjen. Sadržaj tereta je isto Vaša odgovornost: injector ne provjerava je li XML dobro oblikovan, da li MIME tip odgovara bajtovima ili je li bazni dokument uopće PDF/A. Konačnu datoteku tretirajte kao neprovjerenu dok je validator ne vidi, ista disciplina opisana u PDFium Component i PDF/A arhivskoj usklađenosti. Ako ulazne rječnike i sami parsirate, ista #XX name pravila vrijede obrnuto, tema obrađena u zamkama name tokena pri parsiranju PDF rječnika
Associated fileovi, PDF/A izlaz, metapodaci privitaka i validacija isporučuju se u istoj komponenti, pa pipeline gore radi bez druge PDF biblioteke u buildu. API referenca, probna verzija i licencne opcije su na stranici proizvoda PDFium Component