Technical Article

Page-Level Associated Files in PDF 2.0 with PDFlibPas

PDFlibPas attaches an embedded file to one specific page rather than to the document as a whole, by writing an /AF array into the page dictionary while the payload itself stays registered in the document EmbeddedFiles name tree. That split is what ISO 32000-2 §14.13 describes, and it is what lets a reader answer the question a document-level attachment cannot: which page does this data belong to

The use cases are more specific than general attachments. A survey report where each page carries the raw measurement series behind its chart. A scanned batch where every page keeps the OCR result that produced its text layer. A drawing set where each sheet carries the CAD extract it was rendered from. In each case a document-level attachment list would be a pile of files with names encoding page numbers, which is a convention rather than a structure

One payload, two places it is referenced from

The important structural point is that page-level association does not create a second copy of anything. The file is embedded once and registered in the EmbeddedFiles name tree exactly as a document-level attachment is, using the same file specification machinery. What differs is where the reference and its relationship key are written: into the page dictionary instead of the document catalog

Two consequences follow. First, a reader that only knows about document-level attachments still finds the payload, because it is in the name tree where such a reader looks. Second, clearing the page association removes the binding, not the file. ClearPageAssociatedFiles detaches the page from its associated files and leaves the payloads reachable through the name tree, which is the conservative behavior: an operation that says clear the association should not silently destroy data another part of the document may reference

Structure of a page-level associated file in a PDF 2.0 document written by PDFlibPas: the payload is embedded once and registered in the EmbeddedFiles name tree under the document catalog, while the page dictionary carries an /AF array referencing the same file specification with an AFRelationship key, so ClearPageAssociatedFiles detaches the binding without destroying data
Page-level association adds a second reference, not a second copy: readers that only know document-level attachments still find the payload in the name tree, and clearing the page binding leaves the embedded stream reachable

That function has one deliberately narrow success condition worth knowing. It reports success only when the page actually carried an /AF key. A page that never had associations returns failure rather than a cheerful confirmation, so a caller cannot mistake a no-op for a completed cleanup

var
  Lib: TPDFlib;
  Idx, I: Integer;
begin
  Lib := TPDFlib.Create(nil);
  try
    Lib.LoadFromFile('survey-report.pdf');

    // Attach the measurement series that produced the chart on page 3
    Idx := Lib.AddPageAssociatedFileFromFile(3,
      'series-03.csv',            // file on disk
      'measurements.csv',         // display name inside the PDF
      'text/csv',                 // MIME type
      'Raw measurement series for figure 3',
      'Data');                    // AFRelationship, ISO 32000-2 14.13

    if Idx < 0 then
      raise Exception.Create('page association refused');

    for I := 0 to Lib.GetPageAssociatedFileCount(3) - 1 do
      Writeln('page 3 associated file, embedded index ',
        Lib.GetPageAssociatedFileEmbeddedIndex(3, I));

    Lib.SaveToFile('survey-report-with-data.pdf');
  finally
    Lib.Free;
  end;
end;

The relationship string is not free text in practice. ISO 32000-2 defines a vocabulary, Source, Data, Alternative, Supplement, EncryptedPayload, FormData, Schema and Unspecified, and consumers key off it. Data for the numbers behind a chart, Source for the document a page was generated from, Alternative for an equivalent representation. Pick from the vocabulary even when nothing in your pipeline reads it yet, because the next tool in the chain might

Why does the same lookup need FollowRef in both directions?

Because reference following answers two different questions, and the code has to know which one it is asking. A key lookup that follows indirect references returns the object the reference points at. A lookup that does not follow returns the reference itself. Both are correct, and using the wrong one produces a silent misbehavior rather than an error

Reading an associated file demonstrates the first direction. To obtain the object number of the embedded stream behind the file specification /EF and /F keys, the lookup must not follow, because following resolves the reference into the stream object and the object number is gone. The rule generalizes: any code path that needs an object identity rather than object content has to take the raw reference

Optional content shows the opposite direction, and it cost more to find. The optional content properties dictionary is written into the catalog as an indirect object, so code that reads it back without following gets a reference rather than a dictionary. A type check on that value then fails, and the natural fallback branch, if there is no configuration, create one, runs and overwrites the configuration that was already there. Nothing raises. The layers described in optional content groups and layers simply lose their default visibility state

The lesson generalizes past both cases. When a lookup can return either a reference or the object, a bare type check is not error handling: it is a branch that will eventually be taken for the wrong reason. Decide explicitly what each call site needs, and prefer the public API that answers the question directly, such as an optional-content count property, over reaching into a protected accessor for the catalog dictionary

Decision map for reference following in PDF lookups as implemented in PDFlibPas: reading /EF and /F under a file specification must not follow the reference because the object number of the embedded stream is the answer, while the indirect /OCProperties dictionary in the catalog must be followed or a failed type check silently overwrites the existing optional content configuration
The same lookup answers two different questions: identity needs the raw reference, content needs the resolved object, and a bare type check in place of that decision eventually runs the wrong branch without raising
// Document-level attachments and page-level associations coexist. An
// embedded file can be marked associated at document level too
if Lib.IsEmbeddedFileAssociated(0) = 0 then
  Lib.SetEmbeddedFileAssociated(0, 1, 'Supplement');

Writeln('document associated files: ', Lib.GetAssociatedFileCount);
Writeln('page 3 associated files  : ',
        Lib.GetPageAssociatedFileCount(3));

// Clearing detaches the page binding; the payload stays in the name tree
if Lib.ClearPageAssociatedFiles(3) > 0 then
  Writeln('page 3 associations removed, payloads still reachable');

What conformance modes do to attachments

Archival profiles restrict what may be embedded, and the restriction is enforced at the entry point rather than at save time. PDF/A-1 forbids embedded files entirely, PDF/A-2 permits only embedded PDF/A documents, and PDF/A-3 is the profile that opened embedding to arbitrary file types, which is precisely why hybrid invoice formats are built on it

PDFlibPas refuses the attachment when the active conformance mode does not allow it, at the call, not hundreds of operations later during output. That is a deliberate choice about where an error is cheapest to act on: a refusal at the call site names the file you were adding, while a refusal at save time names a document and leaves you to work out which of forty attachments caused it

This is also why associated files show up so often in electronic invoicing. A hybrid invoice is a PDF a human reads with a machine-readable XML payload attached and marked with the right relationship, and both the container profile and the relationship key are part of the specification rather than conventions. That construction is covered in building Factur-X and ZUGFeRD hybrid invoices, with the metadata side in the PDF/A-3 XMP extension schema

When should the association be per page rather than per document?

When a consumer needs to know which page the data belongs to, and only then. Document-level attachments are simpler, more widely supported by viewers, and adequate whenever the payload describes the whole document, an invoice XML, a signature manifest, a source archive. Reach for page-level association when the payload is genuinely page-scoped and the page identity is part of its meaning

Support is the practical constraint. Page-level associated files are a PDF 2.0 construct, and viewer support is thinner than for document-level attachments. Because the payload sits in the name tree either way, a viewer that ignores /AF on pages still shows the file in its attachment list, so the degradation is graceful. But if the page binding is essential to your consumer rather than useful metadata, verify the reader you actually target rather than assuming

Page-level associated files, document-level attachments and the archival profile gates that govern both ship in the PDFlibPas Delphi PDF library. If you are also repairing older files on the way in, the metadata and conformance work in converting to PDF/A with metadata repair is what decides which of these attachment routes is available to you in the first place