Technical Article

Hidden on Screen, Printed on Paper: PDF Layer Usage in Delphi

A DRAFT layer is switched off in your PDF viewer, yet every printed page comes back stamped DRAFT, and a note flagged NoView prints too. PDF optional content keeps screen and paper on separate switches, and HotPDF, the Delphi PDF component, writes and renders each of them so the printout follows a rule you chose

How /D, /Usage and /AS split screen from paper

Of the three switches, only the static default configuration is something every layer-aware reader agrees to start from. The other two are either descriptions waiting for an instruction or rules that belong to annotations, which is how a layer can look right on screen and still print the opposite. ISO 32000-1 spreads the mechanism over several sections:

  • Static state — the /D entry of /OCProperties is the default configuration dictionary (§8.11.4.3). Its /BaseState (ON, OFF or Unchanged) sets every group at once, the /ON and /OFF arrays list the exceptions, and /Locked keeps a group out of the user's reach in the layers panel
  • Usage hints and automatic state — a group's /Usage dictionary (§8.11.4.4) says what its content is for: /View with /ViewState, /Print with /PrintState and an optional /Subtype such as Watermark, plus /Export, /Zoom, /Language and /User. By itself it changes nothing. The configuration's /AS array holds usage application dictionaries, each naming an /Event (View, Print or Export), the groups concerned and the /Category of usage to consult, and that array is what tells a reader to recompute states when it prints or exports, in the order §8.11.4.5 describes
  • Annotation flags — the /F bits of an annotation (§12.5.3) decide whether a visible annotation is displayed and whether it is printed, independent of any layer

Membership is more flexible than a layers panel suggests. Page content joins a group through a marked-content sequence, /OC /OC1 BDC … EMC, while Form XObjects, images and annotations carry their own /OC entry (§8.11.3); in every case the value can be a single group or an optional content membership dictionary. An OCMD (§8.11.2.2) lists groups under a /P policy — AnyOn by default, or AllOn, AnyOff and AllOff — and since PDF 1.6 it can carry a /VE visibility expression that nests And, Or and Not over groups and replaces /OCGs and /P when present. An annotation whose /OC evaluates OFF disappears from screen and paper whatever its flags say. One more trap hides in /Intent: a group whose intent does not match the configuration's (View unless stated otherwise) has no effect on visibility at all, so a group tagged Design-only stays visible in an ordinary viewing context

Building a screen-only watermark and a print-only draft layer

The intuitive construction puts each mark in its own layer and lets /Usage tell readers when to show it. THotPDF.RegisterOptionalContentGroup(LayerName, VisibleByDefault, UserToggleAllowed) creates the group and returns its resource name (OC1, OC2 and so on), THPDFPage.BeginOptionalContent and EndOptionalContent wrap your drawing calls in the matching BDC and EMC, and SetOptionalContentGroupUsage(LayerName, Category, StateValue) attaches the usage entry. That last call wants the returned resource name, not the display name you registered. Category takes View, Print or Export with ON or OFF, Zoom with a minimum and optional maximum magnification in one string such as '1.5 4', or Language with a tag like en-US. Registering a group raises the output version to PDF 1.5 unless StrictVersionLock pins an older one, in which case the call returns an empty name, and a PDF/A-1 target raises an exception because ISO 19005-1 §6.1.13 forbids /OCProperties

uses
  Graphics, HPDFDoc;

procedure BuildQuoteWithLayers(const FileName: string);
var
  Pdf: THotPDF;
  Preview, Draft: AnsiString;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := FileName;
    Pdf.BeginDoc;
    // Static state: ON when the file opens, the user may toggle it
    Preview := Pdf.RegisterOptionalContentGroup('Preview watermark', True);
    // Static state: OFF when the file opens, listed in /D /Locked
    Draft := Pdf.RegisterOptionalContentGroup('Draft stamp', False, False);
    // Usage hints take the returned resource name, not the display name
    Pdf.SetOptionalContentGroupUsage(Preview, 'Print', 'OFF');
    Pdf.SetOptionalContentGroupUsage(Draft, 'View', 'OFF');
    Pdf.SetOptionalContentGroupUsage(Draft, 'Print', 'ON');

    Pdf.CurrentPage.SetFont('Arial', [], 11);
    Pdf.CurrentPage.TextOut(72, 72, 0, 'Quotation Q-2026-118');

    Pdf.CurrentPage.BeginOptionalContent(Preview);
    Pdf.CurrentPage.SetRGBFillColor(clSilver);
    Pdf.CurrentPage.SetFont('Arial', [fsBold], 54);
    Pdf.CurrentPage.TextOut(150, 400, 0, 'PREVIEW');
    Pdf.CurrentPage.EndOptionalContent;

    Pdf.CurrentPage.BeginOptionalContent(Draft);
    Pdf.CurrentPage.SetRGBFillColor(clRed);
    Pdf.CurrentPage.SetFont('Arial', [fsBold], 20);
    Pdf.CurrentPage.TextOut(400, 60, 0, 'DRAFT');
    Pdf.CurrentPage.EndOptionalContent;

    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Expected: the catalog gets an /OCProperties dictionary whose /D configuration has /BaseState /ON, lists the watermark group in /ON and the draft group in /OFF and /Locked, and each group carries the /Usage you set. No /AS array is written. Walk that file through a reader that honors only /D and the screen looks right while the paper is inverted: PREVIEW prints, DRAFT never does. A reader that applies print usage automatically would swap the two at print time, but only when the configuration asks for it through /AS, and this file does not ask. Files from authoring tools that do write /AS produce the opposite complaint from the opening paragraph: DRAFT hidden on screen, stamped on every page by a reader that applies the Print application, and missing again when a print server that reads /D alone handles the same job

When does a reader actually apply the Print usage?

Only when the configuration carries an /AS application for the print event and the reader chooses to act on it; /Usage alone is a description. HotPDF's default configuration carries /BaseState, /ON, /OFF, /Order, /Locked and the /RBGroups written by RegisterOptionalContentRadioGroup, and leaves /AS out. Adding it by hand would not make the file portable: ISO 19005-2 §6.9 forbids the /AS key in optional content configuration dictionaries for PDF/A-2 and PDF/A-3, ISO 14289-1 §7.10 does the same for PDF/UA, and HotPDF's PDF/UA-2 checks report an /AS entry as an error. Where /AS is legal, a viewer that implements the full model recomputes group states when the user presses Print, while a batch rasterizer or a server print queue, with no user and no print event, naturally draws the static /D state. Open-source engines expose the same choice in their APIs: MuPDF's page runner takes a usage string, View or Print, Poppler produces raster output and PostScript through different tools (pdftoppm and pdftops), and neither engine always matches the specification on OCMD policies, /VE expressions and /Intent handling. Our rule follows from that: treat /Usage as a courtesy to interactive viewers, and never let /AS decide whether something reaches paper. A mark that must print, or must never print, belongs in the static state you control or in an annotation flag every printing path reads

Why does a NoView annotation still print?

NoView is a screen flag, and ISO 32000-1 §12.5.3 hands printing to a single bit. When Print is clear the annotation never prints, regardless of what the screen shows; when it is set the annotation prints unless Hidden is also set. NoView hides the annotation and blocks interaction on screen while leaving that decision alone, which makes NoView plus Print the specification's own recipe for a print-only mark. For an annotation whose type the reader recognizes, and whose /OC (if any) evaluates ON, the flags combine like this:

Flag (bit)On screenOn paper
Print (3) clearDisplayedNever printed
Print (3) setDisplayedPrinted
NoView (6) with PrintNot displayed, no interactionPrinted
Hidden (2)Not displayedNot printed, even with Print set
Invisible (1)Applies only to annotation types the reader does not recognize, although one widely used reader also applies it to standard typesSame rule on paper

Every reader that prints annotations has to consult Print, so a print-only DRAFT stamp belongs on an annotation with Print and NoView set, and a screen-only reviewer note on one with Print clear. PDF/A output is the exception to plan for: THPDFPage.SetAnnotationFlags forces Print on and strips Hidden, Invisible and NoView whenever PDFACompliance is set, because ISO 19005-1 §6.5.3 requires conforming annotations to print. An archival file therefore cannot carry screen-only or print-only annotations at all, and marks of that kind have to live in the static layer state instead

uses
  Types, Graphics, HPDFDoc, HPDFObjs;

procedure AddPaperAndScreenMarks(Page: THPDFPage);
var
  Annot: THPDFDictionaryObject;
begin
  // Print-only: NoView hides it on screen, Print sends it to paper
  Page.AddStampAnnotation('Draft copy - not for signature',
    Rect(400, 40, 540, 84), satDraft, clRed);
  Annot := Page.LastAnnotation;
  Page.SetAnnotationFlags(Annot, [afPrint, afNoView]);

  // Screen-only: Print clear means the note is never printed
  Page.AddTextAnnotation('Check the discount line before sending',
    Rect(520, 300, 540, 320), False, taComment, clYellow);
  Page.SetAnnotationFlags(Page.LastAnnotation, []);
end;

A Stamp annotation with a standard name such as Draft leans on the reader's built-in artwork unless it carries an /AP appearance stream, and a renderer that draws only from /AP, including the headless renderer in the next section, shows nothing for it. Give such stamps an appearance before the print path depends on them; synthesizing annotation appearances before flattening covers how

How can you preflight what the printer will receive?

Render the page three ways with HotPDF's headless Cairo renderer and compare the PNGs. HPDFRenderHeadlessPagePNGFile takes a page from THPDFHeadlessDocument.GetRenderAccess(PageIndex) plus a THPDFHeadlessRenderOptions record, and two fields of that record map onto the two halves of this article. Usage selects the annotation rules: hruView, the default, behaves like a screen, while hruPrint requires Print, honors Hidden, ignores NoView and NoZoom and keeps NoRotate. ViewerState selects the layer rules: left nil, the renderer draws the static /D state, while a THPDFHeadlessOptionalContentState whose Event is hocePrint applies the document's Print applications, along with any Zoom, Language or User categories they list, using the context you pass in. The renderer needs no display server and runs on Cairo and FreeType — libcairo-2.dll and libfreetype-6.dll on Windows, libcairo.so.2 and libfreetype.so.6 on Linux — which your application ships; it requires the document's extraction permission, and hruPrint grants no print permission of its own

uses
  SysUtils, HPDFHeadlessDocument, HPDFHeadlessRenderAccess,
  HPDFHeadlessCairoDevice, HPDFHeadlessOptionalContentState;

procedure PreflightPrintLayers(const SourceFile, OutDir: string);
var
  Doc: THPDFHeadlessDocument;
  Access: THPDFHeadlessRenderAccess;
  Options: THPDFHeadlessRenderOptions;
  PrintState: THPDFHeadlessOptionalContentState;
  Dir: string;
begin
  Dir := IncludeTrailingPathDelimiter(OutDir);   // must already exist
  Doc := THPDFHeadlessDocument.Create;
  try
    Doc.LoadFromFile(SourceFile);
    Access := Doc.GetRenderAccess(0);

    // Screen, static /D: what a viewer shows when the file opens
    Options := THPDFHeadlessRenderOptions.Default;   // hruView, ViewerState = nil
    HPDFRenderHeadlessPagePNGFile(Access, Dir + 'view-static.png', Options);

    // Paper, static /D: a print path that never applies /AS
    Options.Usage := hruPrint;
    HPDFRenderHeadlessPagePNGFile(Access, Dir + 'print-static.png', Options);

    // Paper with the document's /AS print applications applied
    PrintState := THPDFHeadlessOptionalContentState.Create(Access,
      THPDFOptionalContentContext.Default);
    try
      PrintState.Event := hocePrint;
      Options.ViewerState := PrintState;   // borrowed, keep it alive while rendering
      HPDFRenderHeadlessPagePNGFile(Access, Dir + 'print-as.png', Options);
    finally
      PrintState.Free;
    end;
  finally
    Doc.Free;
  end;
end;

Expected for the quotation built earlier: view-static.png shows PREVIEW and no DRAFT, and print-static.png and print-as.png are identical, both carrying PREVIEW and neither carrying DRAFT, because the file gives the print event nothing to act on. An identical pair is the signal you want before handing a document to printers you do not control. When a file from another producer gives you two different print images, its printout depends on whether the reader applies /AS, and the cure is to choose the state yourself before printing. With the VCL component, the effective state of each group in a loaded document is the /D configuration plus whatever you set through SetLoadedOptionalContentGroupVisible; overrides reset on the next load and drive the same page renderer behind RenderLoadedPageToBitmap and PrintLoadedDocument, the call covered in printing a loaded PDF through TPrinter

uses
  Printers, HPDFDoc;

function PrintFinalCopy(const FileName: string): Integer;
var
  Pdf: THotPDF;
  Opts: THPDFPrintOptions;
  I: Integer;
  LayerName: AnsiString;
begin
  Result := 0;
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile(FileName) > 0 then
    begin
      for I := 0 to Pdf.GetLoadedOptionalContentGroupCount - 1 do
      begin
        LayerName := Pdf.GetLoadedOptionalContentGroupName(I);
        // Decide the paper state here instead of trusting /AS
        if LayerName = 'Preview watermark' then
          Pdf.SetLoadedOptionalContentGroupVisible(I, False)
        else if LayerName = 'Draft stamp' then
          Pdf.SetLoadedOptionalContentGroupVisible(I, True);
      end;
      Opts := THPDFPrintOptions.Default;
      Result := Pdf.PrintLoadedDocument(Printer, Opts);
    end;
  finally
    Pdf.Free;
  end;
end;

Hidden content still executes: Type 3 glyphs in clipping text modes

A hidden layer suppresses paint while its operators keep running: color, the transformation matrix, the clipping path and the text position still change inside a hidden marked-content sequence, and HotPDF's headless renderer keeps those effects. Text rendering modes 4 to 7 add glyph outlines to the clip, so hidden text in those modes still clips without painting. Type 3 fonts break that tidy rule, because their glyphs are small content streams in /CharProcs that paint on their own, and a renderer that runs them to honor the clip mode paints hidden glyphs onto the page. HotPDF's renderer skips Type 3 glyph procedures while their group is hidden, in every mode. The input is rare — TeX output with Type 3 bitmap fonts inside a layer — but it can turn a hidden layer into a printed one

For anything that must appear on paper, set the Print flag or switch the layer ON in /D, and let a hruPrint render confirm it before the file leaves your build; editions and platform support are listed on the HotPDF Delphi PDF component page