Norėdami iš Delphi prisegti šaltinio failą prie PDF/A-3 dokumento, PDFium Component rašo PDF 2.0 associated-file grandinę: įmontuotą failo srautą su MIME /Subtype, failo specifikaciją, nešančią /AFRelationship, ir /AF masyvą, pakabintą ant katalogo arba puslapio. InjectAssociateFiles ir TPdf.SaveAsWithAssociateFiles tą grandinę pastato vienu inkrementiniu atnaujinimu, o nuo v3.121.2 MIME tipas serializuojamas kaip vienas, teisingai išegspintas PDF vardas. Likusi šio įrašo dalis — apie tai, ką tikrina validatorius, vieno simbolio bugą, sulaužiusį text/plain, ir apie vietas, kur senesni leidimai tyliai darė ką nors kita, nei paprašėte
Ko iš tikrųjų reikia PDF/A-3 associated file?
PDF/A-3 priedas praeina validaciją tik tada, kai trys objektai sutaria tarpusavyje: įmontuotas failo srautas deklaruoja /Type /EmbeddedFile plus MIME /Subtype, failo specifikacijos žodynas (ISO 32000-2 §7.11.3) neša /F, /UF, /EF ir /AFRelationship, o kažkas dokumente rodo į tą failo specifikaciją per /AF masyvą (ISO 32000-2 §14.13). Paprastas įmontavimas per /Names /EmbeddedFiles medį, kurį daro TPdf.CreateAttachment, asociacijos laukų apskritai nenustato. PDFium Component paties PDF/A-3b validacijos fixture priklausomybę padaro konkrečia: pervadinkite vien /AFRelationship raktą, ir failas krenta lygiai ant vienos ISO 19005-3 6.8 skyriaus taisyklės; numeskite vien MIME /Subtype — krenta kita 6.8 taisyklė; tą patį priedą įstatykite į PDF/A-1b kandidatą ir jis atmetamas iš karto, nes PDF/A-1 draudžia įmontuotus failus, kiek besitvarkytų metadata
Santykio reikšmė — dalis, kurios žmonės linkę spėlioti. TPdfAFRelationship iš FPdfAssocFiles vieną enum narį sieja su kiekvienu vardo token, kurį injektorius gali išduoti, ir tik pirmi penki priklauso ISO 19005-3 pripažįstamam poaibiui:
afSource→/Source: originalas, iš kurio pagamintas PDF, pavyzdžiui teksto apdorojimo failas ar skaičiuoklėafData→/Data: mašininė duomenų forma, iš kurios gautas ar kurią atstovauja matomas turinysafAlternative→/Alternative,afSupplement→/Supplement,afUnspecified→/UnspecifiedafEncryptedPayload,afFormData,afTemplate: PDF 2.0 papildymai, iškritę iš PDF/A-3 poaibio, tad laikykite juos nuošaly nuo archyvinės išvesties
Kodėl /Subtype /text/plain sulaužė validaciją?
MIME bugas buvo tokenizacijos klaida, o ne atitikties plyšys: iki v3.121.2 injektorius kvietėjo stringą prikabindavo tiesiai po pasviro brūkšnio, gaudamas /Subtype /text/plain. PDF sintaksėje antrasis pasvirasis brūkšnis pradeda naują vardo objektą (ISO 32000-1 §7.3.5), tad srauto žodyne staiga atsirado raktas /Subtype, vardas /text ir pakibęs papildomas vardas /plain, išbalansavęs rakto ir reikšmės poras. Nepriklausomas PDF/A validatorius failą atmetė dar išskaidydamas EmbeddedFile žodyną, dar prieš pasiekdamas bet kurią PDF/A taisyklę — todėl nesėkmė atrodė kaip failo sugadinimas, o ne kaip trūkstama priedo savybė
Pataisymas MIME reikšmę praleidžia per EscapePdfName, kuris išduoda /text#2Fplain: vieną vardą, kurio dekoduota reikšmė yra text/plain. Escaping sąmoningai platesnis už vien pasvirąjį brūkšnį. Kiekvienas baitas iki 32 imtinai (tarpsimbolis, tab, CR, LF), kiekvienas baitas nuo 127 imtinai, skiriamieji ()<>[]{}/% ir pats # escape simbolis virsta #XX. Vieno brūkšnio escaping paliktų kitokią skylę: MIME string, turintis >> ar tarpą, galėtų per anksti uždaryti žodyną arba įkišti papildomų raktų, todėl regresijos testas sulea priešišką reikšmę su kiekvienu skiriamuoju plus tab, LF ir CR ir tikrina tikslią užkoduotą išvestį
// Ką injektorius rašo MIMEType = 'text/plain' atveju
// iki v3.121.2: /Type /EmbeddedFile /Subtype /text/plain (du vardai)
// v3.121.2: /Type /EmbeddedFile /Subtype /text#2Fplain (vienas vardas)
//
// Kvietėjai visada perduoda paprastą MIME reikšmę. Patiems ją iš anksto
// užkodavus, '#' užkoduojasi dar kartą: 'text#2Fplain' virsta 'text#232Fplain'
Options.Files[0].MIMEType := 'text/plain';
PDF/A-3 failo statymas su InjectAssociateFiles
PDF/A-3 išvesčiai pagaminkite atitinkantį bazinį dokumentą per TPdf.SaveAsPdfAToStream, o paskui ant to srauto kvieskite InjectAssociateFiles; būtent tą dviejų žingsnių konvejerį paleidžia validacijos fixture, prieš praleisdamas PDF/A-3b. TPdf.SaveAsWithAssociateFiles — patogus apvalkalas, bet jis išsaugo per įprastą SaveAs kelią su saRemoveSecurity, o ne per PDF/A rašytoją, tad neprideda XMP identifikacijos ir output intent, kurių PDF/A reikalauja. Turėkite omenyje, jog įrašų tipai gyvena FPdfAssocFiles ir FPdfPdfa, tad abu vienetus reikia įtraukti į savą uses eilutę. Nuo v3.121.3 FileName ir Description nebeprivalo būti paprastas ASCII: /UF ir /Desc rašomi kaip PDF text string, spausdinamas ASCII — pažodžiui, o visa kita kaip UTF-16BE su baitų eiliškumo ženklu, tuo tarpu paveldėtasis /F vardas visada yra perkeliamas spausdinamas ASCII, kiekvieną kitą simbolį pakeičiant _, tad skaitytojai, dekoduojantys /F savo kodų puse, vietoj mojibake parodys pabraukimą. Ankstesni build visus tris pavardavo per sistemos ANSI kodų pusę Delphi arba rašydavo žalius UTF-8 baitus Free Pascal, tad jei senesni build turi duoti tą patį rezultatą — laikykitės vien ASCII vardų
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: katalogo lygio /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'; // rašoma kaip /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); // sugražina Base į pradžią; nesėkmės atveju kelia EPdfAssocFilesError
finally
Output.Free;
end;
finally
Base.Free;
end;
end;
Katalogas ar puslapis: kur atsiduria /AF masyvas?
TAssocFilesOptions.TargetPage nusprendžia /AF masyvo savininką: 0 prisega jį prie katalogo kaip dokumento lygio asociaciją, o 1..N — prie to puslapio žodyno, numeruojant nuo 1. Injektorius viską prideda vienu inkrementiniu atnaujinimu fiksuotame išdėstyme (įmontuotieji srautai, tada failo specifikacijos, tada /AF masyvas, tada perrašytas katalogas ar puslapio objektas), tad esami objektai išlaiko savo poslinkius, ir niekas nespaudžiamas iš naujo. Bet koks ankstesnis /AF įrašas ant target žodyno pakeičiamas, o ne sujungiamas, kas pakartotinį išsaugojimą daro idempotentišką, bet ir reiškia, jog antras kvietimas su kitokiu failų sąrašu laimi. Dvi elgsenos anksčiau nusipelnydavo apsaugos jūsų pačių kode, ir abi pasikeitė. Iki v3.122.0 už ribų esantis TargetPage nepavykdavo; jis grįždavo prie katalogo, tad spausdinimo klaida be jokio signalo puslapio lygio asociaciją paveldavo į dokumento lygio. Nuo v3.122.0 SaveAsWithAssociateFiles ir SaveAsWithAssociateFilesToStream kelia EPdfError, kai TargetPage už 0..PageCount ribų, o InjectAssociateFiles kelia naująjį EPdfAssocFilesError neigiamam TargetPage ar tokiam, kuris neįvardija jokio egzistuojančio puslapio, palikdamas paskirties srautą nepaliestą. Iki v3.121.4 puslapio paieška skenavo išsaugotus baitus ieškodama /Type /Page žodynų failo tvarka, kas galėjo prisegti failą prie kito puslapio, vos tik puslapio objektai būdavo saugomi kita tvarka, nei rodomi, pavyzdžiui po puslapių perrikiavimo ar įterpimo; nuo v3.121.4 TargetPage įvardija puslapį toje pozicijoje dokumento puslapių tvarkoje
procedure AttachChartSource(Pdf: TPdf; PageNumber: Integer;
const CsvBytes: TBytes; const OutPath: string);
var
Options: TAssocFilesOptions;
begin
// Nuo v3.122.0 už ribų esantis TargetPage kelia EPdfError (senesni build
// tyliai grįždavo prie katalogo lygio /AF); patikrinimas pirmiau įvardija puslapį
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;
Kaip patikimai perskaityti AFRelationship atgal?
TPdf.AttachmentRelationship[Index] grąžina priedo /AFRelationship vardą per natyviąjį FPDFAttachment_GetAFRelationship eksportą, bet tuščia eilutė turi dvi galimas reikšmes, tad pirmiau pakvieskite AttachmentRelationshipFeaturesAvailable. Binding įkeliamas kantriai: kai PDFium DLL neturi to eksporto, kiekvienas santykis skaitomas kaip tuščias, kas neatskiriama nuo failo specifikacijos, paprasčiausiai neturinčios /AFRelationship. Savybė taip pat dalijasi savo indeksu su AttachmentCount, skaičiuojančiu /Names /EmbeddedFiles medžio įrašus. Injektorius rašo vien /AF grandinę ir neprideda name-tree įrašo, tad per InjectAssociateFiles prisegtas failas yra už to indekso ribų; kad įsitikintumėte dėl įleistosios grandinės, apžiūrėkite išsaugotus baitus arba paleiskite PDF/A validatorių. To name-tree vidaus reikalai aprašyti kaip dirbti su PDF priedais Delphi su 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; // tuščias atsakymas būtų dviprasmis, todėl nebeklauskite
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;
Ko SaveAsWithAssociateFiles negarantuoja?
TPdf.SaveAsWithAssociateFiles garantuoja failo formato voką ir tai, jog prašyti failai buvo įleisti, o ne atitiktį. Įleidimo dalis nauja: iki v3.122.0, kai išsaugotuose baituose nebūdavo įskaitomo trailer ar nepavykdavo rasti katalogo žodyno, InjectAssociateFiles pervedinėdavo įvestį nepakeistą, o metodas vis tiek grąžindavo True. Nuo v3.122.0 InjectAssociateFiles tais atvejais kelia EPdfAssocFilesError dar prieš ką nors rašydamas, SaveAsWithAssociateFiles grąžina False, ir, kadangi jis dabar visą išvestį supila į save store dar prieš atverdamas target, atmestas ar nepavykęs išsaugojimas daugiau nenukerpa esamo failo. Tuščias Files masyvas pagal projektą vis tiek perveda dokumentą nepakeistą. Ir pačios naštą turinys — jūsų atsakomybė: injektorius netikrina, ar XML failas geras, ar MIME tipas atitinka baitus, ar bazinis dokumentas apskritai yra PDF/A. Laikykite galutinį failą nepatikrintu, kol jo nematė validatorius — ta pati drausmė, aprašyta PDFium Component ir PDF/A archyvavimo atitiktyje. Jei įeinančius žodynus išskaidote ir patys, tos pačios #XX vardo taisyklės galioja atvirkščiai — tema, aptarta vardo token spąstuose išskaidant PDF žodynus
Associated files, PDF/A išvestis, priedų metadata ir validacija keliauja tame pačiame komponente, tad aukščiau aprašytas konvejeris veikia be antros PDF bibliotekos projekte. API nuoroda, bandomasis atsisiuntimas ir licencijavimo parinktys yra PDFium Component produkto puslapyje