För att bifoga en källfil till ett PDF/A-3-dokument från Delphi skriver PDFium Component en PDF 2.0-kedja av associerade filer: en inbäddad filström med en MIME-/Subtype, en filspecifikation som bär /AFRelationship, och en /AF-array hängd på katalogen eller en sida. InjectAssociateFiles och TPdf.SaveAsWithAssociateFiles bygger den kedjan i en enda inkrementell uppdatering, och sedan v3.121.2 serialiseras MIME-typen som ett enda, korrekt eskapat PDF-namn. Resten av det här inlägget tar upp vad en validator kontrollerar, ett-teckens-buggen som bröt text/plain, och de ställen där äldre utgåvor tyst gjorde något annat än vad du bad om
Vad behöver en PDF/A-3-associerad fil egentligen?
En PDF/A-3-bilaga klarar valideringen bara när tre objekt håller ihop: den inbäddade filströmen deklarerar /Type /EmbeddedFile plus en MIME-/Subtype, filspecifikationens ordbok (ISO 32000-2 §7.11.3) bär /F, /UF, /EF och /AFRelationship, och något i dokumentet refererar den filspecifikationen genom en /AF-array (ISO 32000-2 §14.13). Ren inbäddning genom /Names /EmbeddedFiles-trädet, vilket är vad TPdf.CreateAttachment gör, sätter aldrig associationsfälten alls. PDFium Components egen PDF/A-3b-valideringsfixture gör beroendet konkret: byt bara namn på nyckeln /AFRelationship och filen faller på exakt en regel i klausul 6.8 i ISO 19005-3; ta bara bort MIME-/Subtype och en annan 6.8-regel faller; lägg samma bilaga i en PDF/A-1b-kandidat och den avvisas rakt av, för PDF/A-1 förbjuder inbäddade filer oavsett hur städat metadatan är
Relationsvärdet är delen folk tenderar att gissa. TPdfAFRelationship i FPdfAssocFiles mappar en enum-medlem till varje namntoken som injektorn kan avge, och bara de första fem hör till den delmängd ISO 19005-3 känner igen:
afSource→/Source: originalet som PDF:en producerades från, som ett ordbehandlingsdokument eller ett kalkylbladafData→/Data: maskinläsbar data som det synliga innehållet härletts från eller representerarafAlternative→/Alternative,afSupplement→/Supplement,afUnspecified→/UnspecifiedafEncryptedPayload,afFormData,afTemplate: PDF 2.0-tillägg som ligger utanför PDF/A-3-delmängden, så håll dem borta från arkivutdata
Varför bröt /Subtype /text/plain valideringen?
MIME-buggen var ett tokeniseringsfel, inte en efterlevnadslucka: före v3.121.2 konkatenerade injektorn anroparens sträng rakt efter ett snedstreck, vilket gav /Subtype /text/plain. I PDF-syntaxen startar det andra snedstrecket ett nytt namnobjekt (ISO 32000-1 §7.3.5), så att strömordboken plötsligt höll nyckeln /Subtype, namnet /text, och ett hängande extra namn /plain som gjorde nyckel-värde-paren obalanserade. En oberoende PDF/A-validator avvisade filen medan den tolkade EmbeddedFile-ordboken, innan den någonsin nådde en PDF/A-regel, vilket är därför felet såg ut som filkorruption i stället för en saknad bilageegenskap
Fixen leder MIME-värdet genom EscapePdfName, som avger /text#2Fplain: ett namn vars avkodade värde är text/plain. Escapningen är medvetet bredare än snedstrecket. Varje byte på eller under 32 (mellanslag, tab, CR, LF), varje byte på eller över 127, avgränsarna ()<>[]{}/% och escape-tecknet # självt blir #XX. Att escape:a bara snedstrecket hade lämnat ett annat hål: en MIME-sträng som innehåller >> eller blanktecken kunde stänga ordboken i förtid eller injicera extra nycklar, så regressionstestet matar ett fientligt värde med varje avgränsare plus tab, LF och CR och kontrollerar exakt kodad utdata
// Vad injektorn skriver för MIMEType = 'text/plain'
// före v3.121.2: /Type /EmbeddedFile /Subtype /text/plain (två namn)
// v3.121.2: /Type /EmbeddedFile /Subtype /text#2Fplain (ett namn)
//
// Anropare skickar alltid det vanliga MIME-värdet. Escape:ar du det själv
// i förväg dubbelkodas '#', vilket gör 'text#2Fplain' till 'text#232Fplain'
Options.Files[0].MIMEType := 'text/plain';
Bygga en PDF/A-3-fil med InjectAssociateFiles
För PDF/A-3-utdata, producera det konforma basdokumentet med TPdf.SaveAsPdfAToStream och anropa sedan InjectAssociateFiles på den strömmen; den tvåstegspipelinen är exakt vad valideringsfixturen kör innan den klarar PDF/A-3b. TPdf.SaveAsWithAssociateFiles är bekvämsomslaget, men det sparar genom den vanliga SaveAs-vägen med saRemoveSecurity i stället för genom PDF/A-skrivaren, så det lägger inte till den XMP-identifiering och output intent som PDF/A kräver. Notera att posttyperna bor i FPdfAssocFiles och FPdfPdfa, så båda uniterna hör hemma i din uses-sats. Sedan v3.121.3 behöver FileName och Description inte längre vara ren ASCII: /UF och /Desc skrivs som PDF-textsträngar, skrivbar ASCII bokstavligt och allt annat som UTF-16BE med byte order mark, medan det äldre /F-namnet alltid är portabel skrivbar ASCII med varje annat tecken ersatt av _, så att läsare som avkodar /F med egen teckenkodning visar ett understreck i stället för teckenkräp. Tidigare byggen konverterade alla tre genom systemets ANSI-teckenkodning på Delphi eller skrev råa UTF-8-byte på Free Pascal, så håll namnen ASCII bara om äldre byggen måste producera samma utdata
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 på katalognivå
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'; // skrivs 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); // spolarar tillbaka Base; kastar EPdfAssocFilesError vid fel
finally
Output.Free;
end;
finally
Base.Free;
end;
end;
Katalog eller sida: var hamnar /AF-arrayen?
TAssocFilesOptions.TargetPage avgör ägaren till /AF-arrayen: 0 fäster den vid katalogen som en dokumentnivåassociation, och 1..N fäster den vid den sidans ordbok, 1-baserat. Injektorn lägger till allting som en enda inkrementell uppdatering i en fast layout (de inbäddade strömmarna, sedan fils specifikationerna, sedan /AF-arrayen, sedan ett omskrivet katalog- eller sidobjekt), så att befintliga objekt behåller sina offset och ingenting komprimeras om. En tidigare /AF-post på målordboken ersätts, inte sammanfogas, vilket gör en upprepad sparning idempotent men också betyder att ett andra anrop med en annan fillista vinner. Två beteenden brukade förtjäna en vakt i din egen kod, och båda har ändrats. Före v3.122.0 misslyckades inte en TargetPage utanför intervallet; den föll tillbaka på katalogen, så att ett tryckfel gjorde en sidnivåassociation till en dokumentnivåassociation utan någon signal. Sedan v3.122.0 kastar SaveAsWithAssociateFiles och SaveAsWithAssociateFilesToStream EPdfError när TargetPage ligger utanför 0..PageCount, och InjectAssociateFiles kastar den nya EPdfAssocFilesError för en negativ TargetPage eller en som namnger ingen befintlig sida, och lämnar målströmmen oförändrad. Före v3.121.4 skannade siduppslagningen de sparade bytena efter /Type /Page-ordböcker i filordning, vilket kunde fästa filen vid en annan sida när sidobjekt lagrats i en annan ordning än de visas, till exempel efter att sidor sorterats om eller infogats; sedan v3.121.4 namnger TargetPage sidan på den positionen i dokumentets sidordning
procedure AttachChartSource(Pdf: TPdf; PageNumber: Integer;
const CsvBytes: TBytes; const OutPath: string);
var
Options: TAssocFilesOptions;
begin
// Sedan v3.122.0 kastar en TargetPage utanför intervallet EPdfError (äldre byggen
// föll tyst tillbaka på en /AF på katalognivå); att kontrollera först namnger sidan
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;
Hur läser du tillbaka AFRelationship pålitligt?
TPdf.AttachmentRelationship[Index] returnerar /AFRelationship-namnet för en bilaga genom den nativa exporten FPDFAttachment_GetAFRelationship, men en tom sträng har två möjliga betydelser, så anropa AttachmentRelationshipFeaturesAvailable först. Bindningen laddas på tolerant sätt: när PDFium-DLL:en saknar den exporten läses varje relation som tom, vilket är otskiljbart från en filspecifikation som helt enkelt saknar /AFRelationship. Egenskapen delar också sitt index med AttachmentCount, som räknar posterna i /Names /EmbeddedFiles-trädet. Injektorn skriver bara /AF-kedjan och lägger inte till någon namnträdespost, så en fil bifogad genom InjectAssociateFiles ligger utanför det indexet; för att bekräfta den injicerade kedjan, granska de sparade bytena eller kör en PDF/A-validator. Det namnträdets inälvor tas upp i att arbeta med PDF-bilagor i Delphi med 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; // ett tomt svar vore tvetydigt, så fråga inte
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;
Vad garanterar SaveAsWithAssociateFiles inte?
TPdf.SaveAsWithAssociateFiles garanterar filformatkuvertet och att de begärda filerna injicerades, inte konformitet. Injektionsdelen är ny: före v3.122.0, när de sparade bytena saknade läsbar trailer eller katalogordboken inte kunde hittas, kopierade InjectAssociateFiles inmatningen oförändrad genom och metoden returnerade ändå True. Sedan v3.122.0 kastar InjectAssociateFiles EPdfAssocFilesError i de fallen innan något skrivs, SaveAsWithAssociateFiles returnerar False, och eftersom den nu bygger hela utdata i ett sparandelager innan målet öppnas, avhugger en underkänd eller misslyckad sparning inte längre en befintlig fil. En tom Files-array kopierar fortfarande dokumentet oförändrat genom, avsiktligt. Innehållet i nyttolasten är också ditt ansvar: injektorn kontrollerar inte att en XML-fil är välformad, att MIME-typen stämmer med bytena, eller att basdokumentet överhuvudtaget är PDF/A. Behandla den färdiga filen som overifierad tills en validator sett den, samma disciplin som beskrivs i PDFium Component och PDF/A-arkivöverensstämmelse. Om du också själv tolkar inkommande ordböcker gäller samma #XX-namnregler omvänd, ett ämne som tas upp i namntoken-fällor vid tolkning av PDF-ordböcker
Associerade filer, PDF/A-utdata, bilagemetadata och validering skeppas alla i samma komponent, så att pipelinen ovan kör utan ett andra PDF-bibliotek i bygget. API-referensen, testnedladdningen och licensalternativen finns på produktsidan för PDFium Component