tclpdf - PDF generation for Tcl
Synopsis
Create PDF documents from Tcl: pages, graphics, embedded fonts, images, tables and electronic invoices. A pure Tcl package - no compiler, no binary extension, no Tk.
Beyond the usual page, text and graphics work it does two things a PDF writer often leaves out: general file attachments, and form XObjects that store a drawing once and place it as often as wanted. A document is a TclOO object, every command takes Tcl-style -options, and the topical modules are loaded on first use - a caller who writes text never loads the image code.
- Document and graphics - pages and the five page boxes, paths, arcs - the curve alone, a pie slice or a chord, with the angles counted as a Tk canvas counts them -, clipping to a rectangle or an arbitrary path, transparency, form XObjects, tiling patterns, axial and radial shadings, RGB / CMYK / grey / separation colours and 148 colour names without Tk. Colours are named, given as numbers in grey, RGB or CMYK, taken from an ICC profile, or measured - a Lab colour states a spot colour the way prepress measures it, which is what a licensed colour catalogue would otherwise be for.
- Fonts - the fourteen standard faces, and four kinds of file to embed:
TrueType as Type0/CIDFontType2 with Identity-H and a ToUnicode CMap,
subset down to the glyphs actually used; OpenType with CFF outlines, a
bare CFF program with no sfnt wrapper - a road of its own, written as a
Type 1 equivalent - and Type 1 whole, since subsetting any of the three
would need outlines this package does not read. A TrueType collection is
read as well: -face names the face meant, and the package rebuilds it as
a standalone font before reading it. Pair kerning and the standard ligatures
are applied from the font's own tables, both on by default. A missing
character raises an error instead of disappearing silently.
A font whose glyphs are drawn rather than read out of a file is written as
well - a Type 3 face, where a glyph is a piece of the same drawing the page
uses, so it may carry colour, a gradient or a picture. That is also the road
a colour font takes: PDF has none, so the colour glyphs of a
COLRface are read out and drawn into such a font - version 0, which is stacked outlines in palette colours, and version 1, which is a paint graph per glyph with gradients, transformations and compositing. A character sequence the face joins into one picture - an emoji with a skin tone, a family held together by zero width joiners, a flag - becomes one glyph with one advance, and itsToUnicodecarries the whole sequence back out. And where one face cannot set a character, a fallback chain names the faces that may: the line then falls into segments, one per face, and comes back out of a text extractor whole. - Images - JPEG and PNG, both passed through unchanged wherever possible: a JPEG goes in as /DCTDecode, a PNG without alpha as /FlateDecode with /Predictor 15, a PNG whose transparency is one colour keeps it as a colour-key mask on the same pass-through. Only an alpha channel is decoded, into a soft mask. A one-bit picture can be embedded as a stencil mask instead, which carries no colour of its own and lets the fill colour through its bits - one logo embedded once and placed in as many colours as wanted - and any embedded picture can serve as the mask of another.
- Text - single lines and paragraphs with justification, indents, a
height limit that hands back what did not fit, a type area the document is
given (
-typeArea),-height maxdown to its foot, a paragraph that paginates itself over as many pages as it takes - in as many columns as asked, balanced on the last page - text flowing around shapes, soft hyphens, leader rows, text along a path and page numbers. Right-to-left lines are set as well: Hebrew by order alone, Arabic with its contextual forms from the face's own GSUB tables - the contextual and chaining lookups among them, so a face that reaches its lam-alef that way gets it -, digits and brackets where UAX #9 puts them; what cannot be set correctly is refused rather than drawn wrong. The glyph runs are held against HarfBuzz rather than eyeballed. Ligatures and kerning: over 29 faces and 6 843 246 word comparisons, 0 differ. The cursive scripts are measured on the drawn positions, read back out of the content stream of a written document - over 89 Arabic, Persian and Urdu words in Noto Sans Arabic, Noto Naskh Arabic, Noto Kufi Arabic, Amiri and Scheherazade New every glyph stands wherehb-shapeputs it but one, a single word in Amiri one font unit out; and Noto Nastaliq Urdu, the hardest case there is, agrees on 79 of the 86 words it sets, the seven others being artefacts of the comparison rather than of the page. What comes back out is measured too: a glyph that two different words reach - the skeleton an Arabic face shares between sin and shin, or between beh, teh and theh - gets a character code of its own for each reading, so both words extract as they were written. Text is filled, outlined, both or drawn nowhere at all (-render) - the last of those is how a scanned page carries its recognised text under the picture of itself. - Tables - column widths, cell wrapping, page breaks with repeated header and footer rows inside the type area of each page, rowSpan and colSpan, decimal alignment and per-cell hooks.
- SVG - shapes, paths, transforms, groups, reuse, viewBox and gradients, drawn as PDF vectors rather than as a picture. Text in a drawing uses the same faces as the rest of the document, embedded ones included.
- Barcodes - through tzint, which encodes into SVG that tclpdf then draws as vectors. No encoder of its own, and none needed; tzint is optional.
- Layers - optional content as a reader shows it: a group is declared, drawn into and switched, several of them may exclude one another like radio buttons, and the whole of it survives PDF/A. A page taken over from another file keeps the layers it brought.
- Existing PDFs - one page of an existing file is taken over as a form and placed as often as wanted, scaled, rotated or faded: a letterhead is kept rather than rebuilt. Both cross-reference flavours are read, object streams, hybrid files and incremental updates included, and everything the page uses travels with it - fonts, images, profiles - bytes and filters untouched. What a finished file says about itself can be asked without taking anything over - pages, fonts, attachments, signatures, what it claims to conform to - and an encrypted file answers that much rather than being refused. A finished file can also be continued rather than only read: an incremental update appends objects and replaces existing ones without moving a single byte of what is already there, which is the only way a signed document can be changed at all.
- Tagged PDF and PDF/A - levels B, U and A of parts 2 and 3: a structure tree with the 49 standard types and the 2.0 additions, nesting checked against Annex L, artifacts kept out of it and named by kind, colour spaces held against the output intent - beside sRGB the package ships a CMYK and a grey intent for print work.
- Electronic invoices and orders - ZUGFeRD / Factur-X as PDF/A-3B: ICC profile, OutputIntent, XMP with the Factur-X extension schema, embedded XML, file specification, /AF entry and name tree. The conformance level is derived from BT-24 of the invoice XML rather than taken on trust; Order-X orders ride on the same call with their own levels, types and name.
- Encryption - AES-256 with the standard security handler in revision 6
(
/V 5 /R 6, crypt filter method/AESV3), a user and an owner password, and permissions by name: print, copy, modify, annotate, fill, assemble and printing at full resolution. The deprecated revisions and RC4 are not written, and reading someone else's encrypted file is not part of it. Interactive form fields - text fields single and multi-line, check boxes, radio sets, push buttons, list boxes and combo boxes. Every appearance is drawn by the package rather than left to the reader, which is the only way a form holds in PDF/A; in a tagged document each widget sits in a
Formstructure element of its own, and a radio set may spread its buttons over several pages. What a finished form holds is read back with ::tclpdf::pdf fields, in pure Tcl and without a foreign program.Digital signatures - the PDF side of a signature: the signature dictionary, the field,
/ByteRangeover the whole file except the signature itself, and the value written into the space reserved for it. The signature comes from outside - -signer is a command prefix answering a CMS object, and a two-stage way hands the bytes to a card reader, an HSM or a signing service and puts the answer back afterwards. Invisible by default, visible with a field and an appearance the caller draws; the profile is chosen rather than guessed, and a PAdES claim whose signature does not keep it is refused. A document that is already signed can be signed again: the second signature is appended as an incremental update, so the first one keeps the bytes it covers and stays valid - which readers confirm, each showing the two as separate revisions of the same file.Accessibility - a tagged structure tree, and on top of it PDF/UA-1 and PDF/UA-2 with the Well-Tagged PDF declaration. The claim is refused when the document does not keep it: a title, a language, every font embedded including the standard fourteen, headings without a gap, tables whose rows cover the same number of columns with spans counted, a description on every link and every link inside a Link element. All of them are reported at once when the file is written, each naming the call to change.
Everything listed here is built and tested. Measured against the page count of its chapters that covers roughly a third of ISO 32000-1 - and the missing two thirds are almost entirely what a reader of foreign PDFs needs rather than a writer - reading an encrypted file among them.
Dependencies
- Tcl >= 8.6.11, and it runs under Tcl 9 as well. The test suite is green on both.
- tdom (0.9.0 or newer) - the one package that matters, for two things: the XMP metadata packet is built with it, so every document that declares PDF/A, PDF/UA or ZUGFeRD needs it, and a document with no such claim runs without it; and the SVG module uses it where present, parsing 27 to 48 times faster than the element tree parser tclpdf brings along and refusing an entity expansion bomb - without it SVG still works through the built-in parser.
There is no other package that tclpdf itself loads. Barcodes look like an
exception and are not one: tzint is required by your script rather than by
tclpdf - without it there is no barcode, but nothing in this package knows about
them or asks for it. What tzint produces is SVG, and svg -data draws it.
zlib is a command built into Tcl, not a package, and Tk is never loaded -
not for colour names, not for images.
Documentation
- man-page - every command, the same text as the manual page
tclpdf(n)the package installs - plugins - extending tclpdf without touching its source: the event bus, the supported methods and two worked examples
- test-tools - the tools the test suite and the acceptance run use, and how to install them with Homebrew and with apt. None of them is needed to use tclpdf; without them the suite checks less and says so
- examples - every example's finished PDF, straight from the current trunk, next to the script that wrote it
- all wiki pages - the complete, always current list; pages not linked from here (such as math-formulas) are found there
- all tech notes - the technical notes of this repository in one timeline, newest first
The package ships that manual page, tclpdf(n), covering every command.
The source archive adds the test suite and 81 examples, which between them write 90 documents, starting with a hello-world of ten lines that needs nothing else in the directory: shapes, colour, text layout, attachments, forms, diagonal stamps, flowing text and text along a path, soft hyphens and leader rows, the type area and text that paginates itself, extending the package from outside, font embedding in four kinds of file, kerning, ligatures, writing systems in eleven faces, missing glyphs, variable fonts, colour fonts in both COLR versions and a real emoji face whose sequences are one glyph each, images, gradients, SVG, labels inside a drawing, barcodes, three kinds of table, two ZUGFeRD invoices and an Order-X order, PDF/A certificates in sRGB, CMYK and grey, two tagged documents, a weakly structured one, an accessible one and a PDF/UA-2 guide, a navigation demo, a letterhead taken over from an existing PDF, a file continued twice by incremental update, two encrypted documents, signed ones: invisible and visible, in one stage and in two, a document signed twice by two people, and a ZUGFeRD invoice that is approved and countersigned - and, in a group of its own, what a Tk canvas holds put on paper as paths rather than as a picture of the screen. Each one is a readable script of its own and says in its header what it is for.
Build them all at once and look at the result:
make examples
That runs every example into examples/out - one PDF each, except for a
handful that write more than one: the languages example writes the same page
three times - in a standard face, in an embedded one and in a smaller one - and
several write the same document twice, once plain and once as PDF/A.
Three of them depend on the machine rather than on the package and skip
themselves where it cannot serve them - the canvas example needs Tk, which
tclpdf never loads and does not require; the Noto half of the emoji example
needs a 25 MB face under a licence of its own, which is therefore not versioned
here; and its Apple half needs Apple Color Emoji, which ships with macOS and
exists nowhere else. A machine without any of the three writes 87 documents
instead of 90. make clean removes
the directory again. A single example also runs on its own and takes the output
path as its argument:
tclsh examples/00.01-hello-world.tcl hello.pdf
See Also
- ISO 32000-1 (PDF 1.7) - free of charge from the PDF Association since 2023
- ZUGFeRD / Factur-X - the German e-invoicing standard this implements
- veraPDF - the PDF/A validator both invoice examples are checked against
- tdom - builds the XMP packet, and parses SVG where it is installed
- tzint - the Tcl binding for libzint, and the reason barcodes need no code here
Legal Notice
Copyright (C) 2026 Alexander Schoepe, Bochum, DE
Tcl package: MIT license. That covers everything the package installs: the Tcl
modules, pkgIndex.tcl and the ICC profiles in icc/, which are a runtime part
because their bytes end up inside every PDF/A document - sRGB (free of
copyright), the ICC's own sRGB2014 under the ICC's terms, and basICColor's ISO
Coated v2 CMYK and grey intents under the zlib licence.
No font is part of the package. The faces under examples/assets/fonts are
there so that the tests and the examples have something to embed, and they
travel in the source archive alone - make install installs no font, and the
binary archives contain none. They are third-party work under their own terms,
sorted by origin in the source tree: DejaVu (Bitstream Vera licence), the Google
Fonts faces - Roboto, Arimo, Bitcount Prop Single, Niconne, Permanent Marker and ten
Noto faces (SIL Open Font License 1.1, Permanent Marker Apache 2.0) -, OCR A
and OCR B, the URW Core 35 faces, the Liberation faces and Adobe's core AFM
metrics, each with its licence beside it. Whether you may embed a font into a
PDF that tclpdf writes is a question for that font's licence, not for this
one.