Technical Article

PDF/VCR-1 Template Placeholders in Delphi with PDFlibPas

PDFlibPas implements PDF/VCR-1 as a tagged structure profile: the replacement definition lives under StructTreeRoot as a direct child branch, where a template root carries /O /GTS_Template to declare the field set and each placeholder carries /O /GTS_Replacement to bind one data field. Nothing about it is a page-level dictionary

That single sentence is the part most implementations get wrong, and getting it wrong costs more than a failed validation. A variable-data job whose replacement plan is misread does not crash. It prints, at volume, with the wrong field in the wrong box, and the first person to notice is the customer holding fifty thousand mailers

Where the replacement definition actually lives

PDF/VCR-1 puts its replacement model in the logical structure hierarchy described in ISO 32000-1 §14.7, not in a private page dictionary. The template root is a structure element hanging directly off StructTreeRoot, distinguished by an attribute owner of GTS_Template, and it declares the whole field vocabulary in one place through GTS_Fields plus an optional GTS_Pages naming the field that drives page selection

Placeholders are structure elements nested inside that root, with attribute owner GTS_Replacement. Each one carries GTS_Generator (which PDFlibPas requires to be PassThrough), a GTS_Data value naming exactly one declared field, and an optional GTS_BBox. The binding to the page is not a page annotation either: the element points at its page through /Pg and at the marked-content sequence through /K. Choosing structure over a page dictionary then carries a consequence that is easy to state and easy to underestimate: the structure tree is a general graph reached from the catalog, so anything that consumes the profile has to be a tree walker with all the hazards that implies, rather than a dictionary reader. If you have not worked with the structure tree before, the mechanics are laid out in tagged PDF structure and accessibility

Why does checking the first top-level branch let bad files through?

Because StructTreeRoot may have many direct children, and a validator that stops after locating the template root never sees the rest of them. This is the specific failure PDFlibPas is built to avoid, and it produces exactly the kind of false pass that is worse than no check at all: a file that reports conformant and contains a second template root, or a placeholder sitting outside the template branch where no consumer will honor it

So ValidatePDFVCRStructure in PDFlibPas runs two passes over the same tree. The first pass enumerates every direct child of StructTreeRoot and counts the ones bearing GTS_Template; anything other than exactly one is refused before a single placeholder is read. The second pass descends into all top-level branches, not just the template one, and rejects two things at once: a nested element carrying GTS_Template that is not the root, and any GTS_Replacement element found outside the template branch

The traversal is bounded at 64 levels of depth and 1,000,000 nodes, and it records the object and generation number of every element it visits so a cycle or a repeated reference is reported rather than followed. Those numbers are not performance tuning. A structure tree is an object graph an attacker controls, and a validator that recurses without a ceiling is a stack overflow with a file extension

Authoring a template, and the constraints the API refuses to bend

PDFlibPas exposes authoring through a bracketing API that enforces the structural rules at write time rather than discovering them at save time. SetPDFVCRMode switches the document into the profile, which internally requires the PDF/X-4 base mode of ISO 15930-7 and turns on the mark-info flag, because a PDF/VCR-1 file without marked content has nothing for a placeholder to point at. From there, BeginPDFVCRTemplate takes the field list and the page-selection field, and it is strict in ways worth knowing before you design your data schema. Field names go through PDFVCRValidFieldName, which accepts only ASCII letters, digits, underscore, hyphen and period, because those are the characters that serialize into a PDF name without escaping ambiguity. PDFVCRNormalizeFieldList keeps the list case-sensitive and rejects duplicates outright, and the page-selection field must itself appear in the declared list

var
  Lib: TPDFlib;
  Diag: WideString;
begin
  Lib := TPDFlib.Create(nil);
  try
    Lib.NewDocument;

    if Lib.SetPDFVCRMode(1) <> 1 then
      raise Exception.Create('PDF/VCR mode was refused');

    Lib.NewPage;
    if Lib.BeginPDFVCRTemplate('Salutation,FullName,AccountNo',
                               'AccountNo') <> 1 then
      raise Exception.Create('Field list rejected');

    // One placeholder binds exactly one declared field.
    Lib.BeginPDFVCRPlaceholder('FullName', 72, 690, 320, 712);
    Lib.TextOut(72, 694, 0, 'SAMPLE NAME');
    Lib.EndPDFVCRPlaceholder;

    Lib.EndPDFVCRTemplate;

    Diag := Lib.GetPDFVCRDiagnostics;
    if Diag <> '' then
    begin
      Writeln('PDF/VCR blockers: ', Diag);
      Exit;
    end;

    Lib.SaveToFile('campaign-template.pdf');
  finally
    Lib.Free;
  end;
end;

Note what the bracketing buys you. BeginPDFVCRPlaceholder refuses to open unless a template is active and the tag stack is exactly one level deep, which is how PDFlibPas guarantees placeholders are direct children of the template root and never nest inside each other. Coordinates are validated too, rejecting NaN, infinity and inverted rectangles, so a bad bounding box fails at the call rather than in a preflight report a week later

Why does a placeholder need its own MCID serialization path?

Because PDF/VCR-1 wants /K to be a bare integer, and that is not how a general tagged-PDF writer serializes the link between a structure element and its content. Under ISO 32000-1 §14.6, a structure element normally references marked content either through a direct MCID integer, when the element already identifies its page through /Pg, or through a marked-content reference dictionary that names the page explicitly. A library that emits tagged output for many purposes tends to standardize on the dictionary form because it composes better across pages

PDFlibPas took the narrow option here rather than the general one. SetCurrentStructElemDirectMCIDKid flags the current structure element, and only elements carrying that flag serialize /K as a direct MCID with the page carried on /Pg; everything else keeps the existing MCR dictionary path untouched. This is a deliberate trade and it is the kind worth naming out loud: adding a profile should not change the bytes of output every other tagged document already produces, so the profile gets a path of its own instead of a global switch. Every existing tagged file keeps its previous serialization, and the regression surface stays the size of the new feature

One traversal, two consumers

PDFlibPas derives the replacement manifest and the compliance verdict from the same two-pass walk, which is why the manifest can be trusted as a job plan. The traversal fills a TPDFVCRManifestData record while it validates: Fields from the template root, PageSelectionField when one is declared, and a Placeholders array of TPDFVCRPlaceholderInfo entries carrying FieldName, PageNumber, MCID, HasBBox and BBox. The compliance entry point is CheckCompliancePDFVCR(InputStream, Password, Options, ResultList), and it runs its checks in dependency order rather than all at once: the full PDF/X check first, then GTS_PDFXVersion against PDF/X-4, then GTS_PDFVCRVersion read from the http://www.npes.org/pdfvcr/ns/id/ namespace, and only then the structure walk. Passing Options as 1 makes it stop at the first issue, which is what you want in a batch gate; passing 0 collects everything, which is what you want in a report

// Verify a template you received rather than produced
var
  Issues: TStringList;
  Stream: TFileStream;
  I: Integer;
begin
  Issues := TStringList.Create;
  Stream := TFileStream.Create('supplier-template.pdf',
                               fmOpenRead or fmShareDenyWrite);
  try
    if CheckCompliancePDFVCR(Stream, '', 0, Issues) <> 100 then
      for I := 0 to Issues.Count - 1 do
        Writeln('PDF/VCR: ', Issues[I]);
  finally
    Stream.Free;
    Issues.Free;
  end;
end;

The manifest is a contract, so it only exists for conformant files

BuildPDFVCRManifest returns JSON only when the issue list comes back empty, and returns an empty string otherwise. That is not defensive coding for its own sake. A manifest is consumed by a composition engine that will drive real output from it, and handing back a partial field list extracted from a file with two template roots invites the engine to treat a non-conformant structure as a trustworthy replacement plan. Through the document API, GetPDFVCRManifest(InputFileName, Password) wraps the same call and sets the last error code to PDFLIB_ERROR_PDFVCR_COMPLIANCE (608) when the file does not qualify, so a failed extraction and an empty template are distinguishable. The JSON reports fields, pageSelectionField as a string or null, and a placeholders array whose entries give field, page, mcid and either a four-number bbox or null

var
  Lib: TPDFlib;
  Manifest: WideString;
begin
  Lib := TPDFlib.Create(nil);
  try
    Manifest := Lib.GetPDFVCRManifest('campaign-template.pdf', '');
    if Manifest = '' then
      // 608 means the file is not PDF/VCR-1 conformant,
      // 401 means the file could not be opened
      Writeln('No manifest, error ', Lib.LastErrorCode)
    else
      Writeln(Manifest);
  finally
    Lib.Free;
  end;
end;

One detail to carry into your consumer: the page value follows the library-wide zero-based page convention, not the one-based numbering a human sees in a viewer. Mixing those up is a silent off-by-one that lands content on the wrong sheet, and it will not be caught by any validator because both numbers are structurally valid

Where the boundaries are

PDF/VCR-1 support in PDFlibPas covers template authoring, the save gate, standalone preflight and manifest extraction. It does not perform the replacement. Substituting data into placeholders and producing the personalized instances is the composition step, and it lives outside the library on purpose, because the manifest is precisely the interface that lets a composition engine be written independently. The save gate also refuses rather than repairs. Unlike profiles where a wrong annotation flag can be normalized silently, every PDF/VCR-1 constraint that PDFVCRReadyForSave enforces is one where guessing would change what the document means: inventing a template root, or picking which field an unbound placeholder belongs to, produces a file that validates and lies. The same repair-versus-refuse line is discussed for a different profile in the PDF/E-1 author mode and bounded preflight, and the workflow for running several conformance checks over one document is covered in the compliance and signing workbench

Authoring, the bounded structure walk, the save gate and the manifest extractor all ship in the PDFlibPas Delphi PDF library, which means a template can be produced under the profile and then verified through a separate code path that never saw the writer state, and that separation is the only reason a conformance claim is worth anything