Skip to main content

Easy Document Format – programmatic document creation, editing and export

Project description

edof – Easy Document Format 3.0

License: MIT Python Tests PyPI

edof is a Python library for programmatic document creation, template filling and high-quality export.
Documents are stored as .edof files – a versioned ZIP archive with a JSON document tree and all embedded resources (fonts, images).

import edof

doc  = edof.new(width=210, height=297, title="My Certificate")
page = doc.add_page(dpi=300)

tb = page.add_textbox(10, 20, 190, 30, "")
tb.variable       = "recipient"       # bound to a template variable
tb.style.font_size   = 48
tb.style.auto_shrink = True           # shrinks if text doesn't fit, never enlarges
tb.style.alignment   = "center"

doc.define_variable("recipient", required=True)
doc.fill_variables({"recipient": "Jan Novák"})
doc.save("certificate.edof")
doc.export_bitmap("certificate.png", dpi=300)

Installation

# Core library (Pillow only)
pip install edof

# With PDF export
pip install edof[pdf]

# With QR code generation
pip install edof[qr]

# With PyQt6 editor / widget
pip install edof[pyqt6]

# Everything
pip install edof[all]

# Development
pip install edof[dev]

Python 3.9 – 3.13 supported.


Quick Start

Create a document

import edof

doc  = edof.new(width=210, height=297)   # A4 in mm, 300 dpi default
page = doc.add_page()

# Text box
tb = page.add_textbox(x=10, y=10, width=190, height=30, text="Hello!")
tb.style.font_size  = 36
tb.style.alignment  = "center"
tb.style.bold       = True

# Image
rid = doc.add_resource_from_file("logo.png")
page.add_image(rid, x=10, y=50, width=80, height=50, fit_mode="contain")

# Shape
sh = page.add_shape("rect", x=10, y=110, width=190, height=1)
sh.fill.color = (0, 0, 0, 255)

# QR code  (requires pip install edof[qr])
page.add_qrcode("https://github.com/DavidSchobl/edof", x=160, y=120, size=30)

doc.save("doc.edof")
doc.export_bitmap("doc.png", dpi=300)   # PNG
doc.export_pdf("doc.pdf")               # PDF (requires edof[pdf])

Templates & Batch Fill

Objects can be bound to named variables. At render time the variable value replaces the object's content.

doc = edof.new()
page = doc.add_page()

# Define variables with types and defaults
doc.define_variable("name",  type="text",   default="—",          required=True)
doc.define_variable("score", type="number", default="0")
doc.define_variable("logo",  type="image",  default="default.png") # file path or URL

# Bind objects to variables
tb_name = page.add_textbox(10, 10, 100, 20, "")
tb_name.variable      = "name"
tb_name.style.auto_shrink = True    # font shrinks if name is long

img = page.add_image(None, 150, 10, 40, 40)
img.variable = "logo"               # value = file path or HTTP URL

# Fill once
doc.fill_variables({"name": "Alice", "score": "98", "logo": "alice.png"})
doc.export_bitmap("alice.png")

# Batch loop
for row in [("Bob", "87", "bob.png"), ("Carol", "95", "carol.png")]:
    doc.fill_variables({"name": row[0], "score": row[1], "logo": row[2]})
    doc.export_bitmap(f"{row[0]}.png")

Missing variables are non-destructive

If a variable has no value set, the object shows its obj.text fallback — useful for previewing a template in the editor without filling all fields first.


Object Types

TextBox

tb = page.add_textbox(x, y, width, height, text="")

# Sizing modes (mutually exclusive):
tb.style.auto_shrink = True     # font_size = maximum; shrinks to fit; never enlarges
tb.style.auto_fill   = True     # fills the box (grows and shrinks up to max_font_size)
# both False = fixed font size

tb.style.font_family   = "Arial"
tb.style.font_size     = 18.0       # pt
tb.style.min_font_size = 4.0        # floor for auto-shrink/fill
tb.style.max_font_size = 200.0      # ceiling for auto-fill
tb.style.bold          = True
tb.style.italic        = True
tb.style.underline     = True
tb.style.strikethrough = True
tb.style.color         = (0, 0, 0)      # RGB
tb.style.alignment     = "center"       # left|center|right|justify
tb.style.vertical_align= "middle"       # top|middle|bottom
tb.style.line_height   = 1.2
tb.style.wrap          = True
tb.style.overflow_hidden = True

ImageBox

img = page.add_image(resource_id, x, y, width, height,
                     fit_mode="contain")   # contain|cover|fill|stretch|none
# fit_mode="contain" – letterboxed, preserves aspect ratio
# fit_mode="cover"   – cropped, fills the box
# fit_mode="fill"    – alias for cover
# fit_mode="stretch" – distorts to fill exactly

rid = doc.add_resource_from_file("photo.jpg")
# or: rid = doc.add_resource(bytes_data, "photo.jpg", "image/jpeg")

Shape

sh = page.add_shape("rect", x, y, width, height)
# Types: "rect" | "ellipse" | "polygon" | "arrow"

sh.fill.color          = (100, 149, 237, 255)   # RGBA
sh.stroke.color        = (50, 80, 180, 255)
sh.stroke.width        = 1.5    # pt
sh.corner_radius       = 4.0    # mm (for rect)

Line

sh = page.add_shape("line", 0, 0, 1, 1)
sh.points = [[x1_mm, y1_mm], [x2_mm, y2_mm]]  # two absolute page coordinates
sh.stroke.color = (0, 0, 0, 255)
sh.stroke.width = 2.0   # pt

QR Code

qr = page.add_qrcode(data="https://example.com", x=10, y=10,
                     size=40, error_correction="M")
# error_correction: "L" | "M" | "Q" | "H"
qr.fg_color = (0, 0, 0, 255)        # RGBA – any color works
qr.bg_color = (255, 255, 255, 255)
qr.variable = "url"   # dynamic data from variable

Transform API

Every object supports a full transform chain:

obj.move_to(20, 30)                    # absolute position (mm)
obj.move(10, 5)                        # relative translate
obj.rotate_to(45)                      # absolute rotation (°, clockwise)
obj.rotate(15)                         # add 15° to current rotation
obj.resize_uniform(1.5)               # scale both axes, keep centre
obj.resize(100, 40)                   # set absolute width × height (mm)
obj.resize(100, 40, anchor="center")  # resize keeping centre fixed
obj.flip_h()
obj.flip_v()

# Chainable
obj.move_to(10, 10).resize(80, 30).rotate_to(15)

Color Spaces & Bit Depth

page = doc.add_page(color_space="L",  bit_depth=8)   # grayscale
page = doc.add_page(color_space="1")                  # black & white
page = doc.add_page(color_space="RGB", bit_depth=16)  # 16-bit

# Override on export
doc.export_bitmap("gray.tiff", color_space="L",  format="TIFF")
doc.export_bitmap("bw.png",    color_space="1")

Supported values: "RGB", "RGBA", "L" (grayscale), "1" (B&W), "CMYK".


File Format

.edof is a plain ZIP archive — inspectable with any ZIP tool:

document.edof
├── manifest.json        ← version header, quick metadata
├── document.json        ← full document tree (no binary blobs)
└── resources/
    ├── <uuid>           ← embedded image / font (raw bytes)
    └── …

Version compatibility

File version vs library Behaviour
Same Full compatibility
File older Loaded and migrated automatically, non-fatal notice in doc.errors
File newer EdofNewerVersionWarning emitted; content loaded best-effort
File too old (major < 1) EdofVersionError raised
import warnings
from edof.exceptions import EdofNewerVersionWarning

with warnings.catch_warnings(record=True) as w:
    warnings.simplefilter("always")
    doc = edof.load("newer.edof")

print(doc.errors)      # non-fatal notices from load

Command API

from edof.api.commands import execute, CommandHistory

doc = edof.new(); doc.add_page()

oid = execute(doc, {"cmd": "add_textbox", "page": 0,
                    "x": 10, "y": 10, "width": 80, "height": 20,
                    "text": "Hello API"})

execute(doc, {"cmd": "set_style", "object_id": oid, "page": 0,
              "style": {"font_size": 24, "auto_shrink": True}})

execute(doc, {"cmd": "export_bitmap", "page": 0,
              "path": "out.png", "dpi": 300})

# Undo / redo
history = CommandHistory(max_undo=50)
history.push(doc, "initial state")
# … make changes …
doc = history.undo(doc)   # returns restored Document or None
doc = history.redo(doc)

Available commands: add_page, remove_page, add_textbox, add_image, add_shape, add_qrcode, remove_object, set_text, set_variable, fill_variables, move_object, resize_object, rotate_object, set_style, set_visibility, set_layer, export_bitmap, export_pdf, save, validate.


EDOF Editor

A full desktop document editor ships with the repository:

pip install edof[pyqt6]
python edof_editor.py
python edof_editor.py myfile.edof

Canvas features:

  • 8 resize handles (corners + edges) with correct rotated-object resize
  • Rotation handle — Shift = snap to 15°, Alt = free rotation
  • Double-click TextBox → WYSIWYG inline text editor (font size matches canvas zoom)
  • Double-click QRCode → inline data/URL editor
  • Double-click ImageBox → file picker to replace the source
  • Ghost outlines for hidden objects — still selectable
  • Middle-mouse pan · Scroll-wheel zoom

Property panel — type-aware:

Object Properties shown
TextBox Content (live update), font, size, color + alpha, alignment, sizing mode radio buttons
ImageBox Fit mode, replace image
Shape Fill color + alpha, stroke color + alpha + width, corner radius
Line X1/Y1/X2/Y2 endpoints, stroke
QRCode Data/URL (live update), error correction, FG/BG color + alpha

Other editor features:

  • Object list panel (layer order, type, name, variable, visibility, lock)
  • Custom RGBA color dialog (#RRGGBBAA hex + sliders)
  • Layer ordering: bring to front / forward / backward / send to back
  • Variables dialog with live re-render on apply
  • Print preview (QPrintPreviewDialog) with correct page rendering
  • Export PNG / JPEG / TIFF / PDF
  • 60-step undo/redo
  • Internationalisation — add editor_lang/XX.json for any language (base: en.json)

EDOF CLI

Fill templates and export from the command line without opening the editor:

# Inspect a template
python edof_cli.py info      template.edof
python edof_cli.py objects   template.edof
python edof_cli.py validate  template.edof

# Export with variables
python edof_cli.py export template.edof output.png \
    --set name="Jan Novák" \
    --set date="2025-01-01" \
    --dpi 300

# JSON variables
python edof_cli.py export template.edof output.png \
    --json-vars '{"name":"Jan","score":"98"}'

# All pages  (use {page} or {n} in filename)
python edof_cli.py export template.edof page_{page}.png --all-pages

# PDF
python edof_cli.py export template.edof output.pdf
Flag Short Description
--set KEY=VALUE -s Set one variable (repeatable)
--json-vars JSON -j Set multiple variables as JSON
--page N -p Page index (0-based, default 0)
--all-pages -A Export all pages
--format -f png jpg tiff bmp pdf
--dpi N -d Resolution (default 300)
--color-space -c RGB RGBA L 1 CMYK

Running Tests

pip install edof[dev]
pytest
pytest --cov=edof --cov-report=html   # with coverage report

License

MIT – see LICENSE.


Links

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

edof-3.0.0.tar.gz (54.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

edof-3.0.0-py3-none-any.whl (52.4 kB view details)

Uploaded Python 3

File details

Details for the file edof-3.0.0.tar.gz.

File metadata

  • Download URL: edof-3.0.0.tar.gz
  • Upload date:
  • Size: 54.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.11

File hashes

Hashes for edof-3.0.0.tar.gz
Algorithm Hash digest
SHA256 a115ada3dde75842ef9a8b74d40ac5ea755a86fa44f4288769d79e37f0b3309c
MD5 4bcbdbe7631dfe229375c28a966cffd5
BLAKE2b-256 7fbba5e6c703e02c2f571f1b1625966acf8d7d9d149bdfb5dd2abeb9daaefdf2

See more details on using hashes here.

File details

Details for the file edof-3.0.0-py3-none-any.whl.

File metadata

  • Download URL: edof-3.0.0-py3-none-any.whl
  • Upload date:
  • Size: 52.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.11

File hashes

Hashes for edof-3.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 3a157271cf57ddc983c82d9c28651804e4ff099fb3d73f3984ae1daaf1bbefd5
MD5 0b1871b622f353f250ef86d52f748afb
BLAKE2b-256 ce7bfdfb76bbc2f76e38150fa61b3c6145c905e56a46a50f2cc33d4f53c8e507

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page