For at vedhæfte en kildefil til et PDF/A-3-dokument fra Delphi skriver PDFium Component en PDF 2.0 associated files-kæde: en embedded file-stream med en MIME-/Subtype, en file specification, der bærer /AFRelationship, og et /AF-array hængt på kataloget eller en side. InjectAssociateFiles og TPdf.SaveAsWithAssociateFiles bygger den kæde i én inkrementel opdatering, og siden v3.121.2 serialiseres MIME-typen som ét enkelt, korrekt escapat PDF-navn. Resten af dette indlæg handler om, hvad en validator tjekker, det ét-tegns lang bug, der brød text/plain, og stederne, hvor ældre releases stille gjorde noget andet, end du bad om
Hvad skal en PDF/A-3 associated file reelt have?
Et PDF/A-3-vedhæftning består validering kun, når tre objekter er enige med hinanden: embedded file-streamen deklarerer /Type /EmbeddedFile plus en MIME-/Subtype, file specification-dictionaryen (ISO 32000-2 §7.11.3) bærer /F, /UF, /EF og /AFRelationship, og noget i dokumentet refererer den file specification gennem et /AF-array (ISO 32000-2 §14.13). Almindelig embedding gennem /Names /EmbeddedFiles-træet, hvilket er hvad TPdf.CreateAttachment gør, sætter slet ikke associationsfelterne. PDFium Components egen PDF/A-3b-valideringsfixture gør afhængigheden konkret: omdøb kun /AFRelationship-nøglen, og filen fejler præcis én regel i kapitel 6.8 i ISO 19005-3; dropp kun MIME-/Subtype, og en anden 6.8-regel fejler; læg samme vedhæftning i en PDF/A-1b-kandidat, og den afvises på stedet, for PDF/A-1 forbyder embedded files, uanset hvor pæn metadataen er
Relationship-værdien er den del, folk plejer at gætte sig til. TPdfAFRelationship i FPdfAssocFiles mapper ét enum-medlem til hvert navne-token, injectoren kan udsende, og kun de første fem hører til den delmængde, ISO 19005-3 anerkender:
afSource→/Source: originalen, som PDF'en blev produceret fra, såsom en tekstbehandlingsfil eller et regnearkafData→/Data: maskinlæsbare data, det synlige indhold er afledt af eller repræsentererafAlternative→/Alternative,afSupplement→/Supplement,afUnspecified→/UnspecifiedafEncryptedPayload,afFormData,afTemplate: PDF 2.0-tilføjelser, der falder uden for PDF/A-3-delmængden, så hold dem ude af arkivoutput
Hvorfor brød /Subtype /text/plain valideringen?
MIME-bugen var en tokeniseringsfejl, ikke et compliance-hul: før v3.121.2 konkatenerede injectoren kalderens streng direkte efter en skråstreg, hvilket producerede /Subtype /text/plain. I PDF-syntaks starter den anden skråstreg et nyt name-objekt (ISO 32000-1 §7.3.5), så stream-dictionaryen holdt pludselig nøglen /Subtype, navnet /text og et hængende ekstra navn /plain, som gjorde nøgle-værdi-parrene ubalancerede. En uafhængig PDF/A-validator afviste filen, mens den parsede EmbeddedFile-dictionaryen, før den nogensinde nåede en PDF/A-regel, hvilket er derfor, fejlen lignede filkorruption frem for en manglende vedhæftningsegenskab
Fixet sender MIME-værdien gennem EscapePdfName, som udsender /text#2Fplain: ét navn, hvis afkodede værdi er text/plain. Escapingen er bevidst bredere end skråstregen. Enhver byte på eller under 32 (mellemrum, tab, CR, LF), enhver byte på eller over 127, afgrænserne ()<>[]{}/% og #-escapetegnet selv bliver til #XX. Kun at escape skråstregen ville have efterladt et andet hul: en MIME-streng med >> eller whitespace kunne lukke dictionaryen for tidligt eller injicere ekstra nøgler, så regressionstesten fodrer en fjendtlig værdi med hver afgrænser plus tab, LF og CR og tjekker det eksakte encodede output
// Hvad injectoren skriver for MIMEType = 'text/plain'
// før v3.121.2: /Type /EmbeddedFile /Subtype /text/plain (to navne)
// v3.121.2: /Type /EmbeddedFile /Subtype /text#2Fplain (ét navn)
//
// Kaldere giver altid den almindelige MIME-værdi. Escaper du den selv forud,
// double-encoder den '#', hvilket gør 'text#2Fplain' til 'text#232Fplain'
Options.Files[0].MIMEType := 'text/plain';
Bygning af en PDF/A-3-fil med InjectAssociateFiles
Til PDF/A-3-output produceres det conforme basisdokument med TPdf.SaveAsPdfAToStream, og derefter kaldes InjectAssociateFiles på den stream; den to-trins pipeline er præcis, hvad valideringsfixturen kører, før den består PDF/A-3b. TPdf.SaveAsWithAssociateFiles er bekvemmelighedswrapperen, men den gemmer gennem den almindelige SaveAs-vej med saRemoveSecurity frem for gennem PDF/A-writeren, så den tilføjer ikke den XMP-identifikation og output intent, som PDF/A kræver. Bemærk, at record-typerne bor i FPdfAssocFiles og FPdfPdfa, så begge units hører hjemme i din uses-klausul. Siden v3.121.3 skal FileName og Description ikke længere være ren ASCII: /UF og /Desc skrives som PDF-tekststrenge, printbar ASCII bogstaveligt og alt andet som UTF-16BE med en byte order mark, mens det legacy /F-navn altid er portabel printbar ASCII med hvert andet tegn erstattet af _, så readers, der dekoder /F med deres egen code page, viser en underscore i stedet for mojibake. Tidligere builds konverterede alle tre gennem systemets ANSI code page på Delphi eller skrev rå UTF-8-bytes på Free Pascal, så hold navnene ASCII-only, hvis ældre builds skal producere samme output
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: katalogniveau-/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'; // skrives som /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); // spoler Base tilbage; rejser EPdfAssocFilesError ved fejl
finally
Output.Free;
end;
finally
Base.Free;
end;
end;
Katalog eller side: hvor lander /AF-arrayet?
TAssocFilesOptions.TargetPage afgør ejeren af /AF-arrayet: 0 hæfter det på kataloget som en dokumentniveau-association, og 1..N hæfter det på den side-dictionary, 1-baseret. Injectoren tilføjer alt som én enkelt inkrementel opdatering i et fast layout (de embeddede streams, derefter file specifications, derefter /AF-arrayet, derefter et omskrevet katalog- eller sideobjekt), så eksisterende objekter beholder deres offsets, og intet rekomprimeres. Enhver tidligere /AF-entry på mål-dictionaryen erstattes, ikke flettes, hvilket gør en gentaget gemning idempotent, men også betyder, at et andet kald med en anden filliste vinder. To adfærdsmønstre plejede at fortjene en vagt i din egen kode, og begge er ændret. Før v3.122.0 fejlede en TargetPage uden for intervallet ikke; den faldt tilbage til kataloget, så en tastefejl gjorde en sideniveau-association til en dokumentniveau-association uden noget signal. Siden v3.122.0 rejser SaveAsWithAssociateFiles og SaveAsWithAssociateFilesToStream en EPdfError, når TargetPage er uden for 0..PageCount, og InjectAssociateFiles rejser den nye EPdfAssocFilesError for en negativ TargetPage eller én, der ikke navngiver en eksisterende side, og efterlader destinationsstreamen uændret. Før v3.121.4 scannede sideopslaget de gemte bytes efter /Type /Page-dictionaryer i filrækkefølge, hvilket kunne hæfte filen på en anden side, så snart sideobjekter blev gemt i en anden rækkefølge, end de vises, for eksempel efter at sider var flyttet eller indsat; siden v3.121.4 navngiver TargetPage siden på den position i dokumentets siderækkefølge
procedure AttachChartSource(Pdf: TPdf; PageNumber: Integer;
const CsvBytes: TBytes; const OutPath: string);
var
Options: TAssocFilesOptions;
begin
// Siden v3.122.0 rejser en TargetPage uden for intervallet EPdfError (ældre builds
// faldt stille tilbage til katalogniveau-/AF); at tjekke først navngiver siden
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;
Hvordan læser du AFRelationship tilbage pålideligt?
TPdf.AttachmentRelationship[Index] returnerer /AFRelationship-navnet på en vedhæftning gennem den native FPDFAttachment_GetAFRelationship-eksport, men en tom streng har to mulige betydninger, så kald AttachmentRelationshipFeaturesAvailable først. Bindingen indlæses tolerant: mangler PDFium-DLL'en den eksport, læses hver relationship som tom, hvilket er uadskilleligt fra en file specification, der simpelthen ikke har nogen /AFRelationship. Egenskaben deler sit indeks med AttachmentCount, som tæller entries i /Names /EmbeddedFiles-træet. Injectoren skriver kun /AF-kæden og tilføjer ingen name-tree-entry, så en fil hæftet gennem InjectAssociateFiles ligger uden for det indeks; for at bekræfte den injicerede kæde, inspícér de gemte bytes eller kør en PDF/A-validator. Det name-træs indvolde er gennemgået i working with PDF attachments in Delphi using 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; // et tomt svar ville være tvetydigt, så spørg ikke
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;
Hvad garanterer SaveAsWithAssociateFiles ikke?
TPdf.SaveAsWithAssociateFiles garanterer filformat-kuverten og, at de ønskede filer blev injiceret — ikke conformance. Injektionsdelen er ny: før v3.122.0, når de gemte bytes ikke havde en læsbar trailer, eller katalog-dictionaryen ikke kunne lokaliseres, kopierede InjectAssociateFiles inputtet igennem uændret, og metoden returnerede stadig True. Siden v3.122.0 rejser InjectAssociateFiles en EPdfAssocFilesError i de tilfælde, før noget skrives, SaveAsWithAssociateFiles returnerer False, og fordi den nu bygger det komplette output i en save store, før målet åbnes, trunkerer en afvist eller fejlet gemning ikke længere en eksisterende fil. Et tomt Files-array kopierer stadig dokumentet igennem uændret af design. Payloadens indhold er også dit ansvar: injectoren tjekker ikke, at en XML-fil er well formed, at MIME-typen matcher bytesene, eller at basisdokumentet overhovedet er PDF/A. Behandl den endelige fil som uverificeret, til en validator har set den — samme disciplin som beskrevet i PDFium Component and PDF/A archival compliance. Parser du også indkommende dictionaryer selv, gælder de samme #XX-navneregler omvendt, et emne gennemgået i name token pitfalls when parsing PDF dictionaries
Associated files, PDF/A-output, metadata for vedhæftninger og validering skibes i samme komponent, så pipelinen ovenfor kører uden et ekstra PDF-bibliotek i buildet. API-referencen, prøvedownloaden og licensmulighederne står på PDFium Component product page