Technical Article

Toggle PDF Optional Content Layers in Delphi with PDFium

PDFium Component controls PDF optional content layers (OCGs) in Delphi through two TPdf methods: InspectOptionalContent lists every layer together with the visibility PDFium will actually render, and SaveAsOptionalContentConfigured writes a verified copy in which the layers you choose are switched on or off. The second method also neutralises the Usage and /AS rules that would otherwise quietly undo your edit. Both work on the document already open in TPdf, so there is no second parser to keep in sync with what the viewer shows

The request usually arrives from a CAD or GIS shop: the drawing set ships with dimensions, annotations and a title block on separate layers, and the customer wants a copy with the dimensions hidden before it goes to a supplier. PDFium renders optional content correctly, but its public ABI has no function to enumerate OCGs, pick a configuration or flip a layer state. So you drop to the object level, edit /OCProperties, save, reload, and the layer is still there. The reason is PDFium's visibility logic, and it is worth understanding before touching any bytes

Why does editing /ON and /OFF not change what PDFium renders?

Editing the /ON and /OFF arrays of the configuration dictionary is not enough, because PDFium lets an explicit state inside the OCG's own /Usage dictionary win over those arrays, and an /AS auto-state rule can then override both. ISO 32000-1 §8.11.4 describes configurations and usage dictionaries as separate mechanisms; PDFium's renderer folds them into a single decision, and InspectOptionalContent reproduces it in this order:

  • Start from the configuration's /BaseState, where /ON and /Unchanged both count as visible and only /OFF hides
  • Apply the configuration's /ON array, then its /OFF array, so a group listed in both ends up hidden
  • Apply the group's explicit Usage state for the requested usage, such as /Usage << /View << /ViewState /OFF >> >>, which overrides everything above
  • Treat a group whose /Intent contains neither /View nor /All as visible, since it takes no part in view-intent visibility
  • Finally run the /AS array of the selected configuration, whose entries for the matching event set the state of the groups they list
The five-step visibility decision that PDFium Component replays for every PDF optional content group in Delphi: BaseState sets the start, the configuration ON and OFF arrays apply in order, an explicit Usage ViewState or PrintState entry overrides both, Intent non-participation counts as visible, and the AS array runs last
Editing ON and OFF arrays is not enough because PDFium folds BaseState, both arrays, the group Usage state and finally the AS auto-state rules into one verdict that InspectOptionalContent reproduces step by step

The third step is the one that burns people. A file saved by a layout tool often carries /ViewState /ON on every OCG, and PDFium then ignores your carefully edited /OFF array: the save succeeds, the file reopens cleanly, and the layer still paints. For Print and Export, OcExplicitUsageState reads PrintState or ExportState first and falls back to ViewState when the specific entry is absent, so a lone ViewState /ON pins the layer for printing too. Marked content that references an OCMD (§8.11.2.2) is then resolved against these per-group results, through the /P policy or, when present, the /VE visibility expression

How do you list the layers PDFium will actually show?

TPdf.InspectOptionalContent returns a TPdfOptionalContentInventory whose Groups array carries each OCG's object number, name, intents, the three Usage states, language, zoom range, Locked flag, radio-group index and the computed EffectiveVisible. The method first has PDFium save the current in-memory document, expands object streams, and scans the result, so edits made earlier in the session are reflected. Configuration index 0 is always the default /D dictionary and the entries of /Configs follow from index 1; the default argument of -1 selects index 0. A document without /OCProperties makes the method return False with the reason in ErrorMessage rather than raising

procedure TFormMain.ListLayers;
var
  Inv: TPdfOptionalContentInventory;
  G: TPdfOptionalContentGroup;
begin
  // Usage defaults to ocuView; -1 selects configuration 0, the /D dictionary
  if not Pdf.InspectOptionalContent(Inv) then
  begin
    Memo1.Lines.Add('No usable layers: ' + Inv.ErrorMessage);
    Exit;
  end;
  Memo1.Lines.Add(Format('Configuration %d: %s',
    [Inv.SelectedConfigurationIndex,
     string(Inv.Configurations[Inv.SelectedConfigurationIndex].Name)]));
  for G in Inv.Groups do
    Memo1.Lines.Add(Format('obj %d  %s  visible=%s  locked=%s  radio=%d',
      [G.ObjectNumber, string(G.Name),
       BoolToStr(G.EffectiveVisible, True),
       BoolToStr(G.Locked, True), G.RadioGroupIndex]));
end;

The Memberships array reports every OCMD with its Policy (ocmpAnyOn, ocmpAllOn, ocmpAnyOff, ocmpAllOff), the raw VisibilityExpression text and its own EffectiveVisible. A few edge rules are deliberate. /P defaults to /AnyOn, and an OCMD with no groups counts as visible. A reference to an object number that is not a known OCG is treated as visible rather than failing the whole expression. /VE evaluation stops at a nesting depth of 32 and treats anything deeper as hidden, which keeps a hostile or self-referencing expression from turning inspection into a stack overflow

Writing a new layer state with SaveAsOptionalContentConfigured

TPdf.SaveAsOptionalContentConfigured takes an array of TPdfOptionalContentStateChange records (group object number plus Visible) and writes a document in which the selected configuration produces exactly that state. The selected configuration gets /BaseState /ON plus complete /ON and /OFF arrays covering every group, and each OCG that already has a Usage dictionary receives an explicit ViewState (or PrintState / ExportState, following Options.Usage) matching its new state. With TPdfOptionalContentConfigureOptions.Default, the /AS key of the selected configuration is removed so that an open, print or export event cannot flip the layers back

procedure TFormMain.SaveWithoutDimensions(DimensionsObj, NotesObj: Integer);
var
  Changes: TPdfOptionalContentStateChanges;
  Options: TPdfOptionalContentConfigureOptions;
  Report: TPdfOptionalContentConfigureReport;
begin
  SetLength(Changes, 2);
  Changes[0].GroupObjectNumber := DimensionsObj;
  Changes[0].Visible := False;
  Changes[1].GroupObjectNumber := NotesObj;
  Changes[1].Visible := True;

  // Configuration 0, ocuView, DisableAutomaticState and EnforceRadioGroups True
  Options := TPdfOptionalContentConfigureOptions.Default;

  if not Pdf.SaveAsOptionalContentConfigured('C:\Out\Drawing-NoDims.pdf',
    Changes, Options, Report) then
    raise Exception.Create('Layer update rejected: ' + Report.ErrorMessage);

  Log(Format('%d of %d groups changed, %d Usage states rewritten, /AS removed: %s',
    [Report.ChangedGroupCount, Report.GroupCount,
     Report.UpdatedUsageStateCount,
     BoolToStr(Report.RemovedAutomaticState, True)]));
end;

The write path keeps PDFium's own saved output as a byte-for-byte prefix and appends only the rewritten configuration owner and the OCG objects that carry Usage dictionaries, followed by a new xref section and trailer. Before a single byte reaches your destination, the result is reopened in a separate TPdf under the strict load policy, and the method fails if the cross-reference table does not validate. The file overload goes one step further: it writes to a temporary file beside the target and replaces the target only after verification succeeds, so a rejected update never leaves a half-written drawing behind. It is the same verified incremental revision approach used by the PDF name tree and number tree editor in PDFium Component

How SaveAsOptionalContentConfigured in PDFium Component writes a Delphi PDF with layers toggled: state changes and options go in, the selected configuration is rewritten with complete ON and OFF arrays and Usage states, the verified incremental revision is appended, and a strict reopen must validate before anything is written
The configured save keeps PDFium's own re-save as a byte prefix, appends the rewritten configuration owner plus a new xref section, and reopens the result in a separate TPdf before the destination is touched

What does the configured save refuse to do?

The configured save refuses any change the document itself forbids or cannot represent safely, and every refusal happens before the destination is touched. An object number that is not in /OCGs fails outright. Changing a group listed in the configuration's /Locked array fails, although restating its current value is allowed. With EnforceRadioGroups on, any /RBGroups set that would end up with more than one visible member is rejected instead of silently switching the others off. Encrypted documents are rejected because plaintext incremental objects cannot carry the active security handler. Signed documents raise EPdfError unless you pass AllowSignedDocument = True, since changing what a page shows can break signature coverage or a certification policy

The refusal gates that SaveAsOptionalContentConfigured applies in PDFium Component before writing a configured Delphi PDF: an object number outside OCGs fails, locked groups fail, RBGroups sets with more than one visible member are rejected, encrypted documents cannot carry plaintext incremental objects, and signed files demand AllowSignedDocument
Every refusal happens before the destination is touched, and the failure reason lands in Report.ErrorMessage rather than leaving a half-written drawing behind
function TFormMain.SavePrintPreset(Target: TStream;
  const Changes: TPdfOptionalContentStateChanges): Boolean;
var
  Options: TPdfOptionalContentConfigureOptions;
  Report: TPdfOptionalContentConfigureReport;
begin
  Options := TPdfOptionalContentConfigureOptions.Default;
  Options.Usage := ocuPrint;          // writes /Print << /PrintState ... >>
  Options.ConfigurationIndex := 1;    // first entry of /Configs, not /D
  try
    Result := Pdf.SaveAsOptionalContentConfigured(Target, Changes, Options,
      Report);                        // AllowSignedDocument stays False
    if not Result then
      ShowMessage(Report.ErrorMessage);
  except
    on E: EPdfError do
    begin
      ShowMessage(E.Message);         // signed file: nothing written to Target
      Result := False;
    end;
  end;
end;

Know the trade-offs before wiring this into a batch job. The appended revision sits on top of PDFium's full re-save, not your original file bytes, which is exactly why signed input needs explicit consent. The rewrite also normalises the selected configuration to /BaseState /ON, so an author's /Unchanged or /OFF baseline is replaced by explicit arrays with the same resulting visibility. Dropping /AS removes print-only tricks such as a watermark layer that appears only on paper; set DisableAutomaticState to False to keep those rules, accepting that they may override your requested state for that event. On the plus side, PDF/A-2 (ISO 19005-2 clause 6.9) and PDF/UA (ISO 14289-1 clause 7.10) both forbid /AS in configuration dictionaries, so the default output removes one issue your PDF/A preflight validation with PDFium Component would otherwise report

Where layer control fits in a Delphi PDF viewer

In a viewer, layer control is a checklist driven by the inventory plus a reload of the saved result. Populate the checklist from Groups, disable the entries that are Locked, treat members sharing a RadioGroupIndex as mutually exclusive, and on apply write to a TMemoryStream and load that stream back into TPdf so the view paints the new state. The wiring between TPdf and TPdfView is covered in building a feature-rich PDF viewer with PDFium VCL in Delphi. Licensing, trial downloads and the rest of the feature set are on the PDFium Component for Delphi product page