Om vanuit Delphi een bronbestand aan een PDF/A-3-document te koppelen, schrijft PDFium Component een associated-file-keten volgens PDF 2.0: een embedded file-stream met een MIME-/Subtype, een bestandsspecificatie die /AFRelationship draagt, en een /AF-array die aan de catalog of een pagina hangt. InjectAssociateFiles en TPdf.SaveAsWithAssociateFiles bouwen die keten in één incrementele update, en sinds v3.121.2 wordt het MIME-type geserialiseerd als één correct geëscapete PDF-naam. De rest van dit bericht behandelt wat een validator controleert, de één-teken-bug die text/plain brak, en de plekken waar oudere releases geruisloos iets anders deden dan gevraagd
Wat heeft een PDF/A-3 associated file werkelijk nodig?
Een PDF/A-3-bijlage haalt de validatie alleen wanneer drie objecten het met elkaar eens zijn: de embedded file-stream declareert /Type /EmbeddedFile plus een MIME-/Subtype, de bestandsspecificatie-dictionary (ISO 32000-2 §7.11.3) draagt /F, /UF, /EF en /AFRelationship, en iets in het document verwijst via een /AF-array (ISO 32000-2 §14.13) naar die bestandsspecificatie. Gewoon inbedden via de /Names /EmbeddedFiles-boom, wat TPdf.CreateAttachment doet, zet de associatievelden helemaal niet. De eigen PDF/A-3b-validatiefixture van PDFium Component maakt de afhankelijkheid concreet: hernoem alleen de /AFRelationship-sleutel en het bestand faalt precies één regel in clausule 6.8 van ISO 19005-3; laat alleen het MIME-/Subtype vallen en een andere 6.8-regel faalt; stop dezelfde bijlage in een PDF/A-1b-kandidaat en hij wordt ronduit geweigerd, want PDF/A-1 verbiedt embedded files hoe netjes de metadata er ook uitziet
De relatie-waarde is het deel waar mensen doorgaans gokken. TPdfAFRelationship in FPdfAssocFiles mapt één enum-lid op elk naamtoken dat de injector kan uitzenden, en alleen de eerste vijf behoren tot de subset die ISO 19005-3 herkent:
afSource→/Source: het origineel waaruit de PDF is gemaakt, zoals een tekstverwerkingsbestand of een spreadsheetafData→/Data: machinaleesbare data waaruit de zichtbare inhoud is afgeleid of die hij vertegenwoordigtafAlternative→/Alternative,afSupplement→/Supplement,afUnspecified→/UnspecifiedafEncryptedPayload,afFormData,afTemplate: PDF 2.0-aanvullingen die buiten de PDF/A-3-subset vallen, dus houd ze uit archiefuitvoer
Waarom brak /Subtype /text/plain de validatie?
De MIME-bug was een tokenisatiefout, geen conformiteitsgat: vóór v3.121.2 plakte de injector de string van de aanroeper ronduit achter een slash, wat /Subtype /text/plain opleverde. In PDF-syntaxis begint de tweede slash een nieuw naamobject (ISO 32000-1 §7.3.5), dus de stream-dictionary bevatte plotseling de sleutel /Subtype, de naam /text, en een bungelende extra naam /plain die de sleutel-waarde-paren uit balans bracht. Een onafhankelijke PDF/A-validator weigerde het bestand al bij het parsen van de EmbeddedFile-dictionary, voordat hij ooit een PDF/A-regel bereikte, wat verklaart waarom de faling leek op bestandscorruptie in plaats van op een ontbrekende bijlageneigenschap
De fix stuurt de MIME-waarde door EscapePdfName, wat /text#2Fplain uitzendt: één naam waarvan de gedecodeerde waarde text/plain is. De escaping is bewust ruimer dan de slash. Elke byte op of onder 32 (spatie, tab, CR, LF), elke byte op of boven 127, de scheidingstekens ()<>[]{}/% en het #-escape-teken zelf worden #XX. Alleen de slash escapen zou een ander gat hebben achtergelaten: een MIME-string met >> of witruimte kon de dictionary vroegtijdig sluiten of extra sleutels injecteren, dus de regressietest voert een vijandige waarde met elk scheidingsteken plus tab, LF en CR en controleert de exacte gecodeerde uitvoer
// Wat de injector wegschrijft voor MIMEType = 'text/plain'
// vóór v3.121.2: /Type /EmbeddedFile /Subtype /text/plain (twee namen)
// v3.121.2: /Type /EmbeddedFile /Subtype /text#2Fplain (één naam)
//
// Aanroepers geven altijd de gewone MIME-waarde door. Zelf vooraf escapen
// codeert de '#' dubbel, wat 'text#2Fplain' in 'text#232Fplain' verandert
Options.Files[0].MIMEType := 'text/plain';
Een PDF/A-3-bestand bouwen met InjectAssociateFiles
Produceer voor PDF/A-3-uitvoer het conforme basisdocument met TPdf.SaveAsPdfAToStream en roep daarna InjectAssociateFiles op die stream aan; die tweestaps-pipeline is precies wat de validatiefixture draait voordat ze PDF/A-3b haalt. TPdf.SaveAsWithAssociateFiles is de gemakswikkel, maar hij slaat op via het gewone SaveAs-pad met saRemoveSecurity in plaats van via de PDF/A-writer, dus hij voegt de XMP-identificatie en output intent die PDF/A vereist niet toe. Bedenk dat de recordtypes in FPdfAssocFiles en FPdfPdfa wonen, dus beide units horen in uw uses-clausule. Sinds v3.121.3 hoeven FileName en Description niet meer plat ASCII te zijn: /UF en /Desc worden als PDF-tekststrings weggeschreven, printbaar ASCII letterlijk en al het restant als UTF-16BE met bytevolgorde-markering, terwijl de legacy-/F-naam altijd draagbaar printbaar ASCII is met elk ander teken vervangen door _, zodat readers die /F met hun eigen codepagina decoderen een underscore tonen in plaats van mojibake. Eerdere builds converteerden alle drie via de systeem-ANSI-codepagina op Delphi of schreven rauwe UTF-8-bytes op Free Pascal, dus houd namen alleen-ASCII als oudere builds dezelfde uitvoer moeten produceren
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 op catalogusniveau
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'; // weggeschreven als /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); // spoelt Base terug; gooit EPdfAssocFilesError bij faling
finally
Output.Free;
end;
finally
Base.Free;
end;
end;
Catalog of pagina: waar belandt de /AF-array?
TAssocFilesOptions.TargetPage bepaalt de eigenaar van de /AF-array: 0 hangt haar aan de catalog als documentniveau-associatie, en 1..N hangt haar aan die pagina-dictionary, 1-based. De injector voegt alles toe als één incrementele update in een vaste indeling (de embedded streams, dan de bestandsspecificaties, dan de /AF-array, dan een herschreven catalog- of pagina-object), dus bestaande objecten houden hun offsets en niets wordt opnieuw gecomprimeerd. Elke eerdere /AF-entry op de doeldictionary wordt vervangen, niet samengevoegd, wat een herhaalde opslag idempotent maakt maar ook betekent dat een tweede aanroep met een andere bestandslijst wint. Twee gedragingen verdienden voorheen een vangrail in uw eigen code, en beide zijn veranderd. Vóór v3.122.0 faalde een TargetPage buiten bereik niet; hij viel terug op de catalog, dus een typefout veranderde een pagina-niveau-associatie geruisloos in een op documentniveau. Sinds v3.122.0 werpen SaveAsWithAssociateFiles en SaveAsWithAssociateFilesToStream EPdfError wanneer TargetPage buiten 0..PageCount ligt, en InjectAssociateFiles werpt de nieuwe EPdfAssocFilesError voor een negatieve TargetPage of één die naar geen enkele bestaande pagina wijst, met de bestemmingsstream onveranderd. Vóór v3.121.4 zocht de paginalookup de opgeslagen bytes af naar /Type /Page-dictionaries in bestandsvolgorde, wat het bestand een keer aan een andere pagina kon hangen zodra pagina-objecten in een andere volgorde waren opgeslagen dan ze worden getoond, bijvoorbeeld na het herordenen of inservoegen van pagina's; sinds v3.121.4 noemt TargetPage de pagina op die positie in de documentpagina-volgorde
procedure AttachChartSource(Pdf: TPdf; PageNumber: Integer;
const CsvBytes: TBytes; const OutPath: string);
var
Options: TAssocFilesOptions;
begin
// Sinds v3.122.0 werpt een TargetPage buiten bereik EPdfError op (oudere builds
// vielen geruisloos terug op een /AF op catalogusniveau); eerst controleren noemt de pagina
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;
Hoe leest u AFRelationship betrouwbaar terug?
TPdf.AttachmentRelationship[Index] geeft de /AFRelationship-naam van een bijlage terug via de native FPDFAttachment_GetAFRelationship-export, maar een lege string heeft twee mogelijke betekenissen, dus roep eerst AttachmentRelationshipFeaturesAvailable aan. De binding wordt coulant geladen: mist de PDFium-DLL die export, dan leest elke relatie als leeg, wat niet te onderscheiden is van een bestandsspecificatie die simpelweg geen /AFRelationship heeft. De eigenschap deelt zijn index met AttachmentCount, dat entries in de /Names /EmbeddedFiles-boom telt. De injector schrijft alleen de /AF-keten en voegt geen naamboom-entry toe, dus een bestand dat via InjectAssociateFiles is gekoppeld valt buiten die index; bevestig de geïnjecteerde keten door de opgeslagen bytes te inspecteren of een PDF/A-validator te draaien. De binnenkant van die naamboom wordt behandeld in het werken met PDF-bijlagen in Delphi met 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; // een leeg antwoord zou dubbelzinnig zijn, dus vraag niet
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;
Wat garandeert SaveAsWithAssociateFiles niet?
TPdf.SaveAsWithAssociateFiles garandeert de bestandsformaatomhulling en dat de gevraagde bestanden zijn geïnjecteerd, geen conformiteit. Het injecteerdeel is nieuw: vóór v3.122.0, wanneer de opgeslagen bytes geen leesbare trailer hadden of de catalog-dictionary niet te vinden was, kopieerde InjectAssociateFiles de invoer ongewijzigd door en gaf de methode nog steeds True terug. Sinds v3.122.0 werpt InjectAssociateFiles EPdfAssocFilesError in die gevallen voordat er iets wordt weggeschreven, SaveAsWithAssociateFiles geeft False terug, en omdat hij nu de volledige uitvoer in een opslagwinkel bouwt voordat hij het doel opent, kapt een geweigerde of mislukte opslag een bestaand bestand niet langer af. Een lege Files-array kopieert het document nog steeds ongewijzigd door, bij ontwerp. De inhoud van de payload is ook uw verantwoordelijkheid: de injector controleert niet dat een XML-bestand welgevormd is, dat het MIME-type bij de bytes past, of dat het basisdocument überhaupt PDF/A is. Beschouw het definitieve bestand als ongeverifieerd totdat een validator het heeft gezien, dezelfde discipline als in PDFium Component en PDF/A-archiefconformiteit. Parseert u ook inkomende dictionaries zelf, dan gelden dezelfde #XX-naamregels in omgekeerde richting, een onderwerp uit naamtoken-valkuilen bij het parsen van PDF-dictionaries
Associated files, PDF/A-uitvoer, bijlagemetadata en validatie verschepen in dezelfde component, dus de pipeline hierboven draait zonder een tweede PDF-library in de build. De API-referentie, de proefdownload en de licentieopties staan op de productpagina van PDFium Component