A drawing set arrives with its dimensions on a layer of their own, and the obvious way to hide them is to move that group's reference from the /ON array of the default configuration into /OFF. The file saves and reopens, and the dimension lines are still printed across the title block. The cause sits one object away, inside the group dictionary: /Usage << /View << /ViewState /ON >> >>, an explicit state that the renderer reads ahead of the configuration arrays
A layer's state can live in several places in a PDF, and an edit to /OCProperties touches only one of them. Google's PDFium engine renders optional content correctly, but its public C API has no function to enumerate optional content groups (OCGs), choose a configuration or switch a group on and off. Delphi code that drives layers has to rewrite the objects itself and replay the renderer's decision order exactly, or its layer list reports one state while the page shows another
Why does editing /ON and /OFF not change what PDFium renders?
The /ON and /OFF arrays are only the second of five steps, and two later steps can overrule them. ISO 32000-1 §8.11.4.3 defines the optional content configuration dictionary with /BaseState, /ON, /OFF, /Locked, /RBGroups and /AS, while §8.11.4.4 defines the Usage dictionary an OCG may carry. In the specification, Usage describes what a group is for, and the auto-state entries in /AS turn that description into ON or OFF. PDFium takes a shorter road: when it renders for a purpose, the group's own explicit state for that purpose wins. Folded together, the order looks like this:
- Start from the configuration's
/BaseState, where/ONand/Unchangedboth count as visible and only/OFFhides - Apply the
/ONarray, then the/OFFarray, so a group listed in both ends up hidden - Apply the group's explicit Usage state for the requested purpose, such as
/Usage << /View << /ViewState /OFF >> >>, which overrides everything above - Treat a group whose
/Intentcontains neither/Viewnor/Allas visible, since it takes no part in view-intent visibility - Run the
/ASarray of the selected configuration last, letting entries for the matching event set the state of the groups they list
Step three explains the opening file: some exporters write /ViewState /ON into every group they create, and from then on no amount of array editing hides those layers. Printing and export follow the same rule with one twist: the renderer reads /PrintState or /ExportState first and falls back to /ViewState when the specific entry is missing. A lone /ViewState /ON therefore pins the layer on paper too. Step four is quieter. A group tagged /Intent /Design is outside view-intent visibility altogether and paints regardless of the arrays, whereas a group with no /Intent key is treated as /View, which is the default the specification gives
Listing layers and their rendered state with InspectOptionalContent
PDFium Component replays that order in TPdf.InspectOptionalContent. The method fills a TPdfOptionalContentInventory and returns True on success; its Usage parameter defaults to ocuView and its ConfigurationIndex parameter to -1, which selects index 0. Configuration index 0 is the default /D dictionary, and the entries of /Configs follow from index 1 in file order. Before scanning, the method has PDFium save the open document into memory and expands any object streams, so edits made earlier in the session show up in the result. A file without /OCProperties, or a configuration index outside the available range, makes the call return False with the reason in ErrorMessage; no exception is raised for either case
procedure TFormMain.ListLayers;
var
Inv: TPdfOptionalContentInventory;
Cfg: TPdfOptionalContentConfiguration;
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;
Cfg := Inv.Configurations[Inv.SelectedConfigurationIndex];
Memo1.Lines.Add(Format('Configuration %d "%s", %d groups, /AS present: %s',
[Inv.SelectedConfigurationIndex, string(Cfg.Name), Length(Inv.Groups),
BoolToStr(Cfg.HasAutomaticState, True)]));
for G in Inv.Groups do
Memo1.Lines.Add(Format('obj %d %-24s visible=%s locked=%s radio=%d',
[G.ObjectNumber, string(G.Name),
BoolToStr(G.EffectiveVisible, True),
BoolToStr(G.Locked, True), G.RadioGroupIndex]));
end;
Each TPdfOptionalContentGroup carries its object number, name, Intents and the three explicit states ViewState, PrintState and ExportState, each one of ocsUnspecified, ocsOn or ocsOff. When a state reads ocsOn and the group sits in the /OFF array, you have found the trap from the opening paragraph. Locked and RadioGroupIndex come from the selected configuration, and RadioGroupIndex is -1 for a group that belongs to no /RBGroups set
The rest of the Usage dictionary is reported as the file states it: Language and LanguagePreferred, UserType and UserNames, PageElementSubtype, and MinimumZoom and MaximumZoom behind a HasZoomRange flag. EffectiveVisible does not depend on any of them. Zoom-dependent display is up to your viewer. Each TPdfOptionalContentConfiguration exposes its name, BaseState, array contents and RadioGroups, plus OrderGroupObjectNumbers, which flattens /Order into group references and skips the text labels of nested label arrays
Print and export can disagree with the screen
The Usage argument changes the answer, and that is the point of having it. Under ocuPrint the inventory reads /PrintState with its fallback to /ViewState, and only /AS entries whose /Event is /Print take part. A watermark layer built to appear only on paper therefore reports hidden under ocuView and visible under ocuPrint. Comparing the two passes finds such layers before a printout surprises anyone
procedure TFormMain.ReportPrintOnlyLayers;
var
OnScreen, OnPaper: TPdfOptionalContentInventory;
I: Integer;
begin
if not Pdf.InspectOptionalContent(OnScreen, ocuView) or
not Pdf.InspectOptionalContent(OnPaper, ocuPrint) then
Exit;
// Both passes read the same /OCGs array, so group order matches
for I := 0 to High(OnScreen.Groups) do
if OnScreen.Groups[I].EffectiveVisible <> OnPaper.Groups[I].EffectiveVisible then
Memo1.Lines.Add(Format('%s: screen=%s, print=%s',
[string(OnScreen.Groups[I].Name),
BoolToStr(OnScreen.Groups[I].EffectiveVisible, True),
BoolToStr(OnPaper.Groups[I].EffectiveVisible, True)]));
end;
How are OCMD visibility expressions evaluated?
An optional content membership dictionary (OCMD, ISO 32000-1 §8.11.2.2) ties a piece of content to several groups through a /P policy, or through a /VE visibility expression that combines groups with /And, /Or and /Not. The specification gives /P a default of /AnyOn, lets /VE take precedence over /OCGs and /P when it is present, and says an OCMD with no groups has no effect on visibility
The inventory's Memberships array follows those rules against the per-group results of the same pass, so the usage you asked for carries through. Each membership reports its group object numbers, Policy (ocmpAnyOn, ocmpAllOn, ocmpAnyOff, ocmpAllOff, or ocmpUnknown for a name outside those four), the raw VisibilityExpression text and its own EffectiveVisible. An OCMD without groups counts as visible. A reference to an object number that is not a known OCG also counts as visible, so one dangling reference does not hide everything around it. /VE evaluation stops at a nesting depth of 32 and treats anything deeper as hidden, so a self-referencing expression cannot overflow the stack. The scan lists every OCMD stored as an indirect object with /Type /OCMD, object streams included
Writing a new layer state with SaveAsOptionalContentConfigured
TPdf.SaveAsOptionalContentConfigured takes an array of TPdfOptionalContentStateChange records, each holding a GroupObjectNumber and a Visible flag. It writes a copy in which the selected configuration produces exactly that state under the five-step order. Groups you do not mention keep the visibility the inventory computed for them
procedure TFormMain.SaveWithoutDimensions(DimensionsObj, NotesObj: Integer;
ForPrint: Boolean);
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;
Options := TPdfOptionalContentConfigureOptions.Default;
if ForPrint then
begin
Options.Usage := ocuPrint; // writes /Print << /PrintState ... >>
Options.ConfigurationIndex := 1; // first /Configs entry, /D stays as it was
end;
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 output keeps PDFium's own saved document 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 anything reaches your destination, the result is reopened in a separate TPdf under the strict load policy, and the method fails if that load does not succeed or the cross-reference table does not validate. The file overload writes to a temporary file beside the target and replaces the target only after verification, so a rejected update leaves the previous drawing untouched. It is the same verified incremental revision approach used by the PDF name tree and number tree editor in PDFium Component. To confirm a print preset, reload the output and call InspectOptionalContent(Inv, ocuPrint, 1); the groups should report the states you passed in
What does the configured save refuse to do?
The configured save refuses any change the document forbids or cannot carry safely, and every refusal happens before the destination is written. The checks run in a fixed order. A signed document raises EPdfError before anything else happens unless you pass AllowSignedDocument = True. An encrypted document is rejected next, because plaintext incremental objects cannot carry the active security handler. Then each requested change is checked: an object number that is not in /OCGs fails outright, and a change to a group listed in the configuration's /Locked array fails, although restating its current value passes. Last, with EnforceRadioGroups on, a /RBGroups set that would end up with more than one visible member is rejected. Every refusal except the signature one returns False with the reason in Report.ErrorMessage
Two of those gates go further than the specification strictly requires. §8.11.4.3 describes /Locked as protection against changes through the viewer's user interface and describes radio-button groups as switching the other members off when one is turned on. PDFium Component applies both to programmatic changes as well, and for radio groups it refuses instead of picking members to switch off. Asked for two members of a mutually exclusive set, the library cannot know which one you meant, and a guess would contradict your request. Turn the others off in the same Changes array and the save goes through
The signed-document gate exists because the appended revision sits on top of PDFium's full re-save, which is a new byte sequence; the byte range an existing signature covers cannot be assumed to survive. Pass AllowSignedDocument = True only when losing that signature is acceptable
Three side effects matter in a batch job. The rewrite normalises the selected configuration to /BaseState /ON, so an author's /Unchanged or /OFF baseline becomes explicit arrays with the same resulting visibility. Dropping /AS also removes print-only tricks such as a watermark layer that appears only on paper; set DisableAutomaticState to False to keep those rules and accept that they may override your requested state for their event. And the /ON and /OFF arrays are written from the states computed for Options.Usage, so a print preset saved into configuration 0 also changes what the screen shows for every group that has no Usage dictionary. Keep print presets in a /Configs entry and leave /D to the screen. On the compliance 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 clears one finding your PDF/A preflight validation with PDFium Component would otherwise report
Applying layer changes inside a Delphi viewer
In a viewer, layer control is a checklist built from the inventory, followed by a save to memory and a reload. Fill the checklist from Groups, disable entries whose Locked flag is set, treat members that share a RadioGroupIndex as mutually exclusive, and on apply write to a TMemoryStream through the stream overload. TPdf.LoadDocument expects an inactive component, so switch it off first; the buffered load copies the bytes, which lets you free the stream straight away
procedure TFormMain.ApplyLayerChecklist(
const Changes: TPdfOptionalContentStateChanges);
var
Report: TPdfOptionalContentConfigureReport;
Output: TMemoryStream;
KeepPage: Integer;
begin
Output := TMemoryStream.Create;
try
try
if not Pdf.SaveAsOptionalContentConfigured(Output, Changes,
TPdfOptionalContentConfigureOptions.Default, Report) then
begin
ShowMessage('Layer update rejected: ' + Report.ErrorMessage);
Exit;
end;
except
on E: EPdfError do
begin
ShowMessage(E.Message); // signed file: Output is still empty
Exit;
end;
end;
KeepPage := Pdf.PageNumber;
Pdf.Active := False;
Pdf.LoadDocument(Output); // Buffered defaults to True
Pdf.PageNumber := KeepPage;
finally
Output.Free;
end;
end;
After the reload, PDFium paints from the new configuration, so the screen and the next InspectOptionalContent call agree. The wiring between TPdf and TPdfView is covered in building a feature-rich PDF viewer with PDFium VCL in Delphi. When a document has no layers yet and you need to add them, creating PDF layers with OCG and OCMD in Delphi walks through SaveAsOptionalContentCreated
Pricing, the trial build and the component's other document features are listed on the PDFium Component for Delphi product page