Skip to main content
Documents are usually authored content, but nothing stops you from writing them in play: a journal that gains an entry per chapter, a note that names the player’s last town, a manifest generated from inventory, or a whole document constructed from nothing. The document asset exposes a Blueprint mutation API built for exactly that.

Why every write goes through a function

UInkPageDocument::Pages is BlueprintReadOnly on purpose. It is not an oversight and there is no node to write it directly. Every page a display shows is baked: the page’s visible layers are built into a Slate widget and painted into a render target, which the page material multiplies over the paper. That bake is cached per world, keyed on document path + page index + content revision (a page’s back is cached under its own key beside the front’s). ContentRevision is a transient counter on the document, bumped by every mutation. If the Pages array were writable from Blueprint, a graph could change a page without the revision moving, and every display would keep showing the stale bake forever with no way to tell that it was stale. So the array is read-only, and the mutation functions each call Bump Revision for you. Change a page through one of them and the next bake request fails to match any cached key and re-bakes. Two consequences worth internalizing:
  1. The revision is never saved. It is Transient. It exists to invalidate caches within a session, not to version an asset.
  2. Displays do not watch their document. Invalidating the cache does not repaint anything already on screen — the material is still holding the render target it was handed. After mutating, call Refresh on whatever is showing the document.
Set Document already refreshes, so assigning a freshly built document needs no extra call.

The page API

Everything here is on the Ink Page Document asset itself, category Inkwell | Page Document. Drag off a document reference to find them. Normalization. Add Page, Set Page, and Set Page Layers run the page through the same repair pass the loader uses: if the page’s front has no Text layer, one is appended. This is what makes Make Ink Page SpecAdd PageSet Page Markup work — a default-constructed spec has no layers at all, and a page with no Text layer can never show markup. Set Page Markup runs the same pass, so it also repairs a page whose text layer was removed. The repair applies to the front only — see the back-of-sheet section below for why.

The layer API

A page side is an ordered back-to-front stack of layers: index 0 is drawn first (the bottom), the last entry is drawn last (the top). The full model is in Layers; these are the runtime entry points. Every one takes a Side input (default Front) selecting which face of the sheet it operates on. Reads that are useful alongside them: Get Page Size Cm (the page’s physical size — Page Width Cm with the height derived from the Page Size Pixels aspect), Get Paper For Page (the texture that page’s given side will actually use, including a per-page override or generated paper), and Has Back (below).
Remove Page Layer will happily remove the only Text layer on a page’s front. Nothing repairs it until something normalizes that page again, and until then the page’s markup has nowhere to draw. Use Find Text Layer Index before removing, or follow up with Set Page Markup, which re-adds a Text layer as part of its own repair pass.

Writing the back of a sheet

A page has two layer stacks: Layers (the front) and BackLayers (the back of the same physical sheet). An empty back means the back was never authored: the sheet renders bare paper there, and costs no bake and no render target. That is why the normalization pass never touches the back — repairing a Text layer into it would give every page a phantom back that starts paying for render targets. So building a back at runtime is explicit, and the order matters:
  1. Make Ink Page Layer — Type = Text (fill its Text payload, or leave the markup for step 3).
  2. Add Page Layer — Page Index, that layer, Side = Back. Note the returned index.
  3. Set Layer Markup — Page Index, that index, your string, Side = Back.
Has Back tells you whether a page’s back has any layers; the reader uses the same test to decide whether Next Page turns the sheet over (book order) and whether Flip Page has anything to show. The back’s paper can differ from the front’s too: the document’s Back Paper settings are authoring-time like the rest of the document-level look (see the note below), while the per-page Back Paper Override rides in the page spec, so a runtime-built page can carry its own.

The structs

Blueprint sees these as ordinary structs — build them with Make Ink Page Spec, Make Ink Page Layer, and so on, and read them apart with Break. Ink Page Spec (FInkPageSpec) — one page (one physical sheet): Ink Page Layer (FInkPageLayer) — one entry in a stack: A layer carries all three payloads and draws only the one matching Type, so switching a layer’s type throws nothing away. Ink Page Text (FInkPageText) — an independent text box: Ink Page Image (FInkPageImage) — a placed image, all coordinates normalized 0–1: Ink Drawing (FInkDrawing) holds an array of Ink Stroke (FInkStroke: Brush, Color, Width Px 6.0, Opacity 1.0, and an array of Ink Stroke Point with a normalized Position and a 0–1 Pressure). Strokes are drawn oldest first. Generating strokes procedurally works, but it is a niche use — see Drawing for what each brush does.
Document-level look and layout — paper mode and its settings, paper texture and material, back paper, tints, ink strength, default font, typeface, size, color, page size in pixels, margins, line height — are all BlueprintReadOnly. They are authoring decisions, and Blueprint can read them but not write them. The one exception is Page Width Cm, which is writable. The per-page escape hatches are Paper Override / Back Paper Override on the spec and per-run styling in the markup.

Worked example: a note whose text comes from gameplay

The scenario: a body in an alley carries a note that has to name the district the player is in and the number of witnesses they have already found. The best-looking result comes from authoring the look and generating only the words.
1

Author the template

Create an Ink Page Document — PD_CoronerNote — in the Page Editor. Set its paper, default font, size, color, and margins, and type a placeholder body so you can see the layout. One page, one text layer, filling the page.
2

Point a display at it

On the note actor, add an Ink Page Display and set Document to PD_CoronerNote. Nothing about the actor is special — see Integration.
3

Build the string

In the actor’s Event Graph (Begin Play, or whenever the facts are known), assemble the text with Format Text or Append:The body was found in {District}. {Count} others saw it happen.Plain text is fine and is what you want here: a markup string with no tags renders entirely in the document’s default text style. Newlines in the string become new lines on the page.
4

Write it into the page

Drag off the Ink Page Display component, get its Document, then call Set Page Markup with Page Index 0 and your string. The return value is false only if the page index is out of range — worth a Branch while you are wiring it up.
5

Refresh the display

Call Refresh on the Ink Page Display. The page re-bakes with the new text and the material picks it up the same frame.
Step 4 mutates the authored asset, in memory, for the whole session. Every display pointed at PD_CoronerNote shows the new text, and the change survives until the asset is reloaded. That is usually what you want for a one-per-level note. It is wrong when several actors must show different generated text from the same template — for that, build a document per actor, below.In a game world (PIE or packaged), the mutation deliberately does not dirty the asset’s package, so the editor will never offer to save your play-session text into the .uasset. Mutations from editor tooling and Editor Utility Blueprints do mark the package dirty, so your tooling’s changes can be saved.

A document per actor

Ink Page Document is a BlueprintType object, so you can build one in play:
1

Construct it

Construct Object from Class, Class = Ink Page Document, Outer = Self. Store the result in a variable.
2

Fill page 0

A newly constructed document is not empty — its constructor gives it one page. Call Set Page Markup (index 0): its repair pass gives that page a page-filling Text layer and the text lands on it.
3

Add more pages

Make Ink Page Spec (Layers empty is fine) → Add Page → note the returned index → Set Page Markup with it. Repeat per page.
4

Assign it

Set Document on the Ink Page Display, Stack, actor, or widget. No Refresh is needed: Set Document re-bakes on its own, and it broadcasts On Page Changed as well.
A document built this way carries the class defaults, not the defaults the asset factory applies. Creating a Page Document asset in the Content Browser seeds its page size, paper texture, and default font from Project Settings → Plugins → Inkwell; Construct Object from Class does not. So a runtime document starts at 1024 × 1400 px, 21 cm wide, 90 px margins, line height 1.18, 28 pt warm near-black text, paper mode Texture with no paper texture (the page material’s flat paper color) and no default font — which falls through to the project’s Inkwell default font, and to the engine’s Roboto if that is unset. Give each page a Paper Override in its spec if you want real paper on a runtime-built document.

Adding a second text box

To put a heading in its own box above a body that fills the page:
  1. Make Ink Page Text — Markup = your heading string, Justification = Center, Position = (0.1, 0.06), Size = (0.8, 0.12).
  2. Make Ink Page Layer — Type = Text, Name = Heading, Visible = true, Opacity = 1.0, Text = the struct from step 1.
  3. Add Page Layer — Page Index 0, Layer = the struct from step 2, Side = Front. It lands at the top of the stack and returns its index.
A text layer with a non-zero Size wraps at its own box width and grows down from the top of the box. A layer whose Size is zero on either axis ignores Position entirely and fills the page inside the document’s margins — which is what Set Page Markup writes into by default.

The markup format

Markup is the standard UE rich-text format with one tag, TextStyle. The Page Editor writes it; you can write it too. Plain text. A string with no tags renders entirely in the document’s default style — default font, typeface, size, and color. For generated content, localized strings, and journal entries, this is normally all you need. Styled runs. A styled run looks like this, and closes with the rich-text short close tag:
Runs sit side by side to mix styles inside a paragraph; any attribute a run omits falls back to the document’s defaults. These attribute keys are the on-disk format and are frozen — the same markup is read by the in-game bake and by the Page Editor’s WYSIWYG box, which is what keeps the two pixel-identical. The fastest way to get a styled string right is to style one in the Page Editor first and copy what it produced.
Never splice player- or network-supplied strings into tag attributes. The markup writer does not escape attribute values — in authored documents every value is machine-generated (asset paths, numbers, color tuples), so escaping was unnecessary. A quote character in a spliced-in value breaks the run, and everything after it in that string is at the mercy of the parser. Generated body text between the tags is fine, but strip < from it, or it may be read as the start of a tag.
Fonts referenced only by markup are invisible to the cooker. Markup stores fonts as string paths, so the Page Editor maintains the document’s hidden FontReferences list on save to keep them cooked and loadable. If you generate styled markup at runtime that names a font by path, make sure something else in your project hard-references that font — or stay on plain text, which uses the document’s own (hard-referenced) default font.

Performance, caching, and cost

What a bake costs. Baking one page side builds a Slate widget for its visible layers and paints it into a render target — twice. (Slate’s auto-wrap measures against the geometry cached by the previous paint, and an offscreen widget has no previous paint; the first pass primes it, the second draws the correctly wrapped layout.) It runs on the game thread, in linear space, at the document’s page size clamped proportionally to Max Page Bake Dimension (Project Settings → Plugins → Inkwell, default 4096). At the default 1024 × 1400 that is roughly 5.7 MB of render target per distinct page side. What is cached, and on what. Bakes live in a per-world subsystem keyed on document path | page index | content revision, with a page’s back cached under its own key beside the front. Game and PIE worlds have it; editor worlds do not, and bake uncached through the tools path instead. Twenty sheets showing the same page of the same document cost one bake. A page with no authored back never bakes a back and never allocates a render target for one — an unauthored back is free by design. What invalidates. Any mutation function, Bump Revision, and any editor-side edit to the document. Nothing else — a page turn, a flip, a Refresh, or a new display of an already-baked page all hit the cache.
A bump does not free the old bake. The cache gains a new key; the previous render target stays in the map for the life of the world. Mutating a document in a loop — a page rewritten every frame, or per second — leaks a render target per revision. If you mutate repeatedly, call Clear Page Cache (Ink Page Render Library, takes the document) or the cache subsystem’s Clear Cache (no document = drop everything) to reclaim them. Both cover front and back bakes alike.
Pre-warming. Bakes happen on demand, on the game thread, so the first display of a long generated document can hitch. Call Get Page Ink Texture for the pages you expect during a load screen or a fade — once per side you will show, via its Side input — and the cache will already hold them when the player opens the note. Procedural paper is cached separately. Generated paper is rasterized once per distinct set of procedural settings and held on the document itself, keyed on a hash of those settings plus the clamped page size — not on the content revision. A back that generates its own sheet is cached the same way. Twenty pages sharing a document share one sheet of paper, and mutating page text never regenerates it. Runtime pages are not save data. The revision is transient and the pages you write live only in memory. If generated pages must survive a reload, save the source facts — the strings you built, the district name, the entry list — in your own save game and rebuild the document on load. Reference documents from save data by soft object path, never by hard pointer. Multiplayer. Documents are not replicated, and a bake needs a Slate renderer, so a dedicated server bakes nothing. Treat pages as a client-side visual: replicate the facts, and build the page locally on each client that needs to read it.

Next

Blueprint API

Every Inkwell node in one place, grouped by the class it lives on.

Layers

The stacking model these layer functions operate on, and the Layers panel.

Rendering

The bake pipeline, the page material, and its parameters end to end.