edof

Reference: Variables & Templates

The variable system turns documents into reusable templates. Variables are typed, can have defaults, and are substituted into text via {name} placeholders or bound directly to images and QR codes.

Defining a variable

doc.define_variable(
    name="recipient",
    type="text",
    default="",
    required=True,
    description="Person being awarded the certificate",
    choices=None,                # optional list of allowed values
)

Variable types

Type constant String Purpose
VAR_TEXT "text" String value
VAR_NUMBER "number" Integer or float
VAR_DATE "date" Date string (ISO 8601: YYYY-MM-DD)
VAR_BOOL "bool" True / False
VAR_URL "url" URL string (validated as a URL on set)
VAR_IMAGE "image" Resource ID or file path
VAR_QR "qr" Text/URL to encode as QR (used by QRCode objects)

You can use the constants or the strings interchangeably:

from edof import VAR_NUMBER

doc.define_variable("score", type=VAR_NUMBER, default=0)
doc.define_variable("score", type="number", default=0)   # equivalent

Optional fields


Setting variable values

doc.set_variable("recipient", "Jan Novák")
doc.set_variable("score", 95)

# Bulk
doc.fill_variables({
    "recipient": "Jan Novák",
    "score":     95,
    "date":      "2026-05-04",
})

fill_variables() is a wrapper that calls set_variable() repeatedly.

Type checking is enforced — setting a non-numeric value on a number variable raises EdofVariableError. Same for invalid dates, malformed URLs, etc.


Using variables in text

The {variable_name} syntax substitutes at render time:

doc.define_variable("name")
doc.define_variable("amount", type="number")

page.add_textbox(15, 15, 180, 8, "Hello {name}, you owe {amount} CZK.")

doc.set_variable("name", "Alice")
doc.set_variable("amount", 1500)
doc.export_pdf("invoice.pdf")

The result reads “Hello Alice, you owe 1500 CZK.”

Number formatting

By default, numbers render as Python’s str(value). To format more carefully (currency, decimals, etc.), pre-format the string in code before setting the variable:

doc.set_variable("amount", f"{1500.50:,.2f}")   # "1,500.50"

Or define the variable as text instead of number.

Date formatting

Dates render in ISO format by default. For custom formatting, do it in code:

import datetime
date = datetime.date.today()
doc.set_variable("today", date.strftime("%d. %B %Y"))   # "4. May 2026"

Binding objects to variables directly

Some object types support a variable attribute that overrides their content at render time.

TextBox bound to a variable

doc.define_variable("title", default="Untitled")

tb = page.add_textbox(15, 15, 180, 12, "")
tb.variable = "title"
# tb.text is ignored at render — value of "title" is used instead

This is equivalent to tb.text = "{title}", but the binding is explicit and the editor shows it differently in the UI.

ImageBox bound to a variable

doc.define_variable("logo", type="image")

ib = page.add_image(default_logo_id, x=15, y=15, w=40, h=40)
ib.variable = "logo"

# Now at render time, "logo" can be a resource ID OR a path to a file:
doc.set_variable("logo", "/path/to/customer_logo.png")
doc.export_pdf("output.pdf")

# Or another resource ID already in the document:
new_id = doc.add_resource_from_file("alternate.png")
doc.set_variable("logo", new_id)

QRCode bound to a variable

doc.define_variable("verify_url", type="url")

qr = page.add_qrcode(160, 15, 30, 30, data="https://default.com")
qr.variable = "verify_url"

doc.set_variable("verify_url", "https://verify.example.com/abc123")

VariableStore (low-level access)

doc.variables is a VariableStore instance. Most users don’t interact with it directly, but it has these methods:

store = doc.variables

store.names()                  # list of all variable names
store.get_def("score") is not None   # defined?
store.get("score")             # current value (or default if not set)
store.get_def("score")         # VariableDef object
store.set("score", 42)         # same as doc.set_variable
store.reset_all()              # drop values (fall back to defaults)
store.all_values()             # dict of all current values
store.undefine("score")        # remove the definition entirely
store.missing_required()       # names of required vars without a value

VariableDef

The metadata about a variable (separate from its current value):

defn = doc.variables.get_def("score")
print(defn.name, defn.type, defn.default, defn.required)

Fields: name, type, default, required, label, help, choices, max_length.


Repeating sections

page.repeat_objects() is the powerful template feature: it duplicates a set of “template” objects for each row of a data list and auto-paginates onto new pages.

# Build a header that goes on every page
header = page.add_textbox(15, 10, 180, 8, "Sales Report")
header.style.bold = True

# Build a template row
row_tpl = page.add_textbox(15, 25, 180, 8, "{name}: {amount} CZK")
row_tpl.style.font_size = 10

# Important: remove the template from the page before repeating it
page.objects.remove(row_tpl)

# Generate one row per data entry; repeat_objects creates new pages as needed
new_pages = page.repeat_objects(
    template=[row_tpl],
    data=[
        {"name": "Alice", "amount": 1500},
        {"name": "Bob",   "amount": 2300},
        {"name": "Carol", "amount": 1850},
        # ... 200 more ...
    ],
    gap=2.0,                    # vertical spacing in mm between repeated rows
)

print(f"Added {len(new_pages)} extra pages.")

Parameters

How it works

  1. The template is “snapshotted” — relative positions of objects are preserved.
  2. For each row in data:
    • All template objects are deep-copied
    • {column} placeholders are replaced with row[column]
    • The copies are added to the current page, shifted vertically
  3. When the next repetition would overflow the page, a new page is added (with the same dimensions) and rendering continues there.
  4. The new pages are returned so you can add page-specific elements (page numbers, etc.).

Multi-object templates

header = page.add_textbox(15, 0, 100, 6, "{name}")
header.style.bold = True
header.style.font_size = 10

body = page.add_textbox(15, 6, 180, 12, "{description}")
body.style.font_size = 8

shape = page.add_shape("line", 15, 18, 180, 0)
shape.points = [[0, 0], [180, 0]]

# Remove the template objects from the page
page.objects.remove(header)
page.objects.remove(body)
page.objects.remove(shape)

# Repeat them as a unit
page.repeat_objects(
    template=[header, body, shape],
    data=[
        {"name": "Section 1", "description": "Some description text."},
        {"name": "Section 2", "description": "More description."},
        # ...
    ],
    gap=4.0,
)

Adding page-specific elements after repeat

new_pages = page.repeat_objects(template=[row_tpl], data=data, gap=1.0)

# Add a page number to every page
for i, p in enumerate([page] + new_pages):
    pn = p.add_textbox(95, 285, 20, 6, f"Page {i+1}")
    pn.style.font_size = 8
    pn.style.alignment = "center"

Validation

doc.validate() checks (among other things) that all required=True variables have values:

doc.define_variable("recipient", required=True)
issues = doc.validate()
print(issues)
# ['Required variable "recipient" has no value']

doc.set_variable("recipient", "Alice")
issues = doc.validate()
print(issues)
# []

fill_variables() does NOT auto-validate; call validate() explicitly to check.


Variable expressions in visible_if

obj.visible_if is a small expression evaluated against doc.variables at render time:

discount_label = page.add_textbox(15, 200, 180, 8, "DISCOUNT: -{discount} CZK")
discount_label.visible_if = "discount > 0"

vip_section = page.add_group()
vip_section.visible_if = "tier == 'gold' or score >= 90"

See reference/02-objects.md for the full expression syntax.


Text (run) variables — 3D Batch (v4.3.6+ / v4.4.0)

Besides document-level {name} variables, any span of text can become a batch variable: the span gets a stable rid on its runs plus a human var_name, and a batch column bound to that rid fills it per row. The rid survives editing around the span and page reflow.

Naming (v4.4.0). Every variable/column name is unique, always. An unnamed column gets a systematic name persisted from the object and attribute (“textbox-1.text”, “shape-1.fill-color”; an object’s own name from the Objects panel wins over the type prefix). A custom name that is already taken is auto-suffixed (“Name_2”) at creation; renaming to a taken name is rejected in the table header editor and auto-suffixed in the variable rename flows. Uniqueness is case-insensitive and enforced at the model level (BatchConfig.add_column, unique_header, header_in_use), so the [{Header}] filename tags and CSV header matching are never ambiguous.

Creating: select text in the editor and use the { } toolbar button, Ctrl+Shift+B, or the right-click menu. The dialog also offers Add to an existing variable (the selection becomes another occurrence of it).

Managing (Objects panel). Every variable appears as a virtual child of its text box (rainbow chip). A plain click focuses and highlights the span; the checkbox on the row selects it into the shared set — check several to edit shared run attributes or to link them. Right-click offers Rename, Remove, Change range (use editor selection) — the current editor selection becomes the variable’s new span while its columns and values survive — and the shared attribute toggles.

Linking variables (v4.4.0). Link objects to variable… can LINK other variables to a column: one value then drives every linked span, but each variable keeps its own rid, name and panel entry (BatchColumn. extra_run_ids). Unchecking unlinks. Redundant same-attribute columns of newly linked variables are dropped so two columns never fight over a span.

Highlighting. View → Show Variables marks every variable span with a rainbow underlay; selecting a batch-table column highlights exactly the span that column drives. Both are view-only aids and never export.

Header/footer. Variables work inside the header/footer band; the rid is persisted to the body template so it survives repagination (see the Header & Footer reference).

Typing at a span boundary never extends the span: new text typed right after a variable (or anchor/link) is plain — the identity does not leak into what you type next; typing inside the span still belongs to it.