tclpdf

plugins
Login

plugins

Extending tclpdf

How to add a capability to tclpdf without touching its source: a letterhead, a stamp, a house style, an extra piece of document data. The package is built from modules that attach themselves to the document class and subscribe to its events — ZUGFeRD, PDF/A, attachments and bookmarks are wired exactly this way, and third-party code can use the same two mechanisms.

The event bus

Every document is an event emitter. A subscriber registers a command prefix and is called with the document object as its first argument, followed by the event's own arguments — it never has to capture the object from its surroundings:

set token [$doc on beforeWrite ::myorg::stamp]
$doc off $token       ;# unsubscribe; unknown tokens are accepted silently

Subscribers run in registration order. The events a document fires:

Event When Extra arguments
pageAdded after page add the new page's index
beforeWrite at the start of every write and writeChannel
resources while the page resources are built
catalog while the catalog is built
info while the info dictionary is built
afterWrite after the file is complete the path; empty for writeChannel

The two contracts

Write-time events fire on every write. A document may be written more than once, so a subscriber that creates objects must be idempotent: reserve each object number once through reservation and write over it on later runs. A subscriber that allocates fresh numbers per run grows the file with every write and leaves the earlier objects unreachable — nothing reports that, the file merely gets larger.

Errors are not caught. The bus deliberately lets a subscriber's error reach the caller of write. A plugin that fails must fail audibly; an invoice attachment that failed silently would produce a file that passes every validator and carries no invoice data.

The building blocks

These methods are the supported surface for extensions:

Method Purpose
$doc reservation $key an object number that survives rebuilds — handed out once per key, stable across writes
$doc streamObject $pairs $content ?$number? write a stream object; with a number it writes over that object instead of allocating a fresh one
$doc resource $category ?$name? ?$value? register a resource: Font, XObject, ExtGState, ColorSpace, Pattern or Shading
$doc catalogEntry $key ?$value? set, read or clear a catalog key — how /AF, /Names and /OutputIntents get in without the core knowing them
$doc state $key ?$value? per-document state that does not go into the PDF
$doc writer the low-level writer object, for object numbers and bodies

A worked example — a subscriber that stores a private data stream and points a catalog key at it. Readers ignore catalog keys they do not know, so the file stays valid everywhere; the pattern is the same one the ZUGFeRD module uses for keys that matter.

Name private keys accordingly. ISO 32000-2 Annex E reserves unprefixed names for the standard itself: a key of your own carries either a prefix registered with the ISO maintenance agency or, without registration, XXXXMyOrgData rather than MyOrgData. An unprefixed name is not refused by any reader, and that is the problem: it silently claims a place in the standard's namespace and collides with whatever is put there later.

package require tclpdf

namespace eval ::myorg {}

proc ::myorg::write {doc} {
    # The same number on every write - this is what makes a second
    # [$doc write] come out identical instead of growing.
    set number [$doc reservation myorg::data]
    $doc streamObject {Type /XXMyOrgData} {payload bytes} $number
    $doc catalogEntry XXMyOrgData "$number 0 R"
}

set doc [tclpdf new -unit mm]
$doc page add
$doc on beforeWrite ::myorg::write
$doc write out.pdf

Adding methods to the document

The topical modules attach their methods with oo::define on the document class, and a third-party package can do the same:

package require tclpdf
package require tclpdf::document

oo::define ::tclpdf::document::document {
  method letterhead {} {
    my font -family helvetica -size 8
    my text "My Org Ltd - 44 Example Road" -at {20 285}
  }
}

package provide myorg::letterhead 1.0

After the oo::define, $doc letterhead is an ordinary method on every document. To extend a single object instead of the class, use oo::objdefine $doc with the same body.

Three conventions keep this safe:

A worked example of all of this together — the event bus, a method added with oo::define, a reserved object number and a private catalog key, plus the byte-identical second write that proves the extension is idempotent — is examples/01.07-extension.tcl in the source distribution.

What not to rely on

Methods whose names start with an uppercase letter are private, and everything not listed above is internal: it may change between releases without notice. If an extension needs something the surface above does not offer, that is worth a report — the surface is meant to grow from real cases.