tclpdf

man-page
Login

man-page

NAME

tclpdf - PDF generation for Tcl

SYNOPSIS

package require Tcl 8.6.11-

package require tclpdf 1.7

DESCRIPTION

tclpdf creates PDF documents from Tcl. It is a pure Tcl package: no compiler, no binary extension, no Tk.

What is documented below is what the package provides — nothing here is planned or partial. Roughly a third of ISO 32000-1 is covered, weighted by the page count of its chapters; the remainder is almost entirely what a reader of foreign PDFs needs rather than a writer — reading an encrypted file among them.

INSTALLATION

The release archives tclpdf<version>.zip and tclpdf<version>.tar.gz hold the same thing: one directory tclpdf<version> with the Tcl modules, pkgIndex.tcl and the ICC profiles in icc/ — nothing to build. Extract it into a directory on the auto_path of your tclsh — the lib directory of the Tcl installation, or one you add with lappend auto_path — and package require tclpdf finds it. The source archive tclpdf<version>-src.tar.gz, and a checkout of the repository, carry the tests, the examples with their fonts, the manual sources and the tools besides; that one builds and installs the TEA way: ./configure, make test, make install, in the source directory (see the README for the details and for --prefix).

REQUIREMENTS

Tcl 8.6.11 or newer. The package also runs under Tcl 9 — note that this requires the open-ended form of the version requirement, since package require Tcl 8.6 is rejected by Tcl 9 with a version conflict.

The core needs nothing beyond Tcl itself. In particular zlib is a built-in command rather than a package, so it must not be requested with package require: the dummy package of that name exists only in Tcl 8.6.

Tk is not required and is never loaded. Two conveniences would pull it in and are therefore avoided: resolving colour names via winfo rgb, and decoding images via image create photo. tclpdf carries its own colour table and its own image parsers.

OPTIONAL PACKAGES

tclpdf loads and runs with nothing but Tcl 8.6.11 or later. One package matters, for two things:

tdom (0.9.0 or newer)
The metadata packet needs it: the XMP is built with it, so every document that declares PDF/A, PDF/UA or ZUGFeRD needs tdom, and the module that writes those declarations refuses to load without it. A document that makes no such claim never loads that module and runs without tdom.

Only those four modules need it: icc embed and an {icc alias …} colour do not, although the ICC profile they read is also what an output intent carries — the header reader they share lives in the colour module. A document that paints in ICC colours and claims nothing therefore runs without tdom.

The SVG module uses it for parsing where it is present, and prefers it: measured, tdom parses 27 to 48 times faster than the parser tclpdf brings along, and it rejects an entity expansion bomb that the built-in one would try to expand. Without tdom the package still reads SVG, through its own element tree parser — the same four accessors sit in front of both, so nothing else in the package can tell the difference.

Two more serve one command only, ::tclpdf::sign timestamp: http, which comes with Tcl, is loaded to request a document timestamp, and tls only when the timestamp authority is an https one — without tls such an authority is refused by name, and an http authority or a transport of your own through -tsa needs neither. Beyond these 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 — see “Barcodes” below.

COMMANDS

Creating a document

tclpdf new ?option value …?
Creates a document object and returns its command name. Options: -unit (mm, the default, or pt, cm, in; px is also accepted and is the same as pt), -format (a page format name such as a4, or a pair of numbers in the document unit), -orientation (portrait or landscape; hoch and quer are also accepted), -version (1.0 through 1.7, or 2.0; default 1.7), -compress (1 by default — content streams are deflated; 0 writes them plainly, which is for reading the output, not for shipping it) and -typeArea (the margins flowing text and breaking tables keep, {top bottom} or {top bottom left right} in the document unit; without it five percent of the page height top and bottom and of the page width at the sides — see page typeArea, text -height max and table; the margins are kept in the document unit and follow it: after configure -unit pt they are answered in points by cget -typeArea and page typeArea alike, and a -typeArea given in the same call as -unit is read in that unit). Every feature is checked against the version before it is written: an opacity in a document created with -version 1.3, a shading in a 1.2 file, an embedded font in a 1.1 file are refused with a message naming the feature and the version it needs (opacity needs PDF 1.4 - this document is written as PDF 1.3), rather than raised behind the caller’s back — whoever set the version said what the file may contain. The version is never raised silently, with three exceptions that are claims rather than features: pdfa and ua -part 1 lift the file to 1.7 and ua -part 2 to 2.0. Below 1.2 there is no FlateDecode, so a 1.0 or 1.1 document needs -compress 0 and can hold neither embedded fonts nor PNG pictures; the refusal comes when the first stream is written.

-unit, -format and -orientation are read at the call that gives them: tclpdf new and configure refuse a unit, a page format or an orientation they do not know, rather than storing it for the first page add to trip over.

A size given as two numbers is taken as it stands — {88 55} stays 88 by 55. It is turned only if an orientation is asked for as well, on tclpdf new, on configure or on the page add itself: -format {88 55} -orientation portrait gives 55 by 88. The default portrait is not a request, so a pair without any orientation is never touched.

tclpdf formats
The known page format names.

doc configure ?option value …?
Changes the document options after creation. It returns nothing; read a single option with cget. A page already added keeps the size it was given. -version cannot be lowered under a feature the document already uses — after opacity the document refuses configure -version 1.3, naming the feature. Nor can it be raised over a claim that caps it — after pdfa the document refuses configure -version 2.0, naming the claim (this document claims PDF/A-3, which is written as PDF 1.7 at most).

doc cget option
One option.

doc destroy
Releases the object. A document that was written is not needed afterwards.

Coordinates and units

Positions are given in the document unit, and y counts from the top of the page downwards — the opposite of PDF’s own convention, which the package converts on the way out. -at names the top left corner of what is being placed — with one exception, circle, ellipse and arc, where it is the centre, because that is how a circle is described and where an angle is measured from. A form or a pattern script is no exception to the rule: inside them the origin is that object’s own top left corner and y counts downwards, exactly as on the page — the conversion happens against the object’s height instead of the page’s. Every number written into the file is held to what a PDF real can carry (ISO 32000-1, Annex C.2, about ±3.403×10³⁸): a length or coordinate beyond that range is refused at the call that names it — the distances of a table, the margins of the type area, the shapes of -avoid and the width of a leader row included — because a reader would otherwise drop the whole content stream over it. A whole number is held to a second bound. Written without a decimal point it is an integer object (7.3.3), and Annex C.2 gives an integer the range ±2 147 483 647; beyond that the package writes the number as a real, with a trailing point, rather than as an integer a reader need not be able to read.

doc coords x y
The point in PDF user space, for a caller that needs it.

doc distance value ?unit?
A length in points.

doc extent size ?unit?
A {width height} pair in points.

Pages

doc page add ?-format f? ?-orientation o? ?-rotate deg?
Adds a page and makes it current. Without options the document defaults apply. -rotate is a multiple of 90 and is written as one of the four values Table 30 allows: negative angles and angles of 360 or more are normalised to 0, 90, 180 or 270 rather than written as they stand. page add reads its options like every other call — an unknown one is refused naming the three that exist (-format, -orientation, -rotate).

doc page count
The number of pages.

doc page current
The index of the current page, counting from zero.

doc page size ?index?
{width height} in the document unit.

doc page box name ?value? ?index?
Reads or sets one of the five page boxes: media, crop, bleed, trim or art. The value is {x0 y0 x1 y1} in the document unit — two corners, not a corner and a size. A box may start away from zero; the size is then the difference of the pairs, and the caller’s origin follows the box rather than the axis. A box whose corners are not in order (x1 must exceed x0 and y1 must exceed y0), or that reaches beyond the media box, is refused with both rectangles in the message — ISO 32000-1 14.11.2 would have the reader cut it back and 7.9.5 would have it swap the corners, both without a word, and tclpdf names the mistake instead. The media box itself must measure between 3 and 14400 pt on each side (Annex C; page add applies the same limit to -format), and cannot be shrunk under a box already set: set the media box first, or move the box that sticks out. index is the page, counted from 0 as page current counts, the current page without it — and because it comes after the value, reading a box of another page takes an empty value in between: page box media {} 0 reads the first page’s media box, page box media 0 is refused as a box that is not four numbers.

doc page typeArea ?index?
The type area of a page — index is one optional page index, the current page without it — as {x0 y0 x1 y1} in the document unit like a page box — where flowing text and breaking tables begin and end on it. It is the page less the margins -typeArea gave the document, or, without that option, less five percent of the height at the top and the bottom and five percent of the width at each side: 10.5 14.85 199.5 282.15 on A4. Nothing in the file records it; it is a rule for the layout, and one question with one answer — a running head goes above y0, a column starts at {x0 y0}, and text -height max, text -paginate and a table without -top/-bottom stop at y1 and resume at y0. Margins that meet on a page are refused when that page is asked, naming the page size and the margins.

doc page content ?index?
The content stream built for that page so far, as text. For diagnosis: it shows which operators a call actually produced, which is the only way to see a graphics state that leaks past the shape that set it.

Text

doc font ?-family f? ?-style s? ?-size n? ?-color c? …
Sets the font state, which stays in force until changed. Without arguments it returns the current state as a dictionary, including the resolved font name.

-family takes one of the fourteen standard faces (helvetica, times, courier, symbol, zapfdingbats; arial is accepted as another name for helvetica), an exact PostScript name such as Times-Italic — which names the face itself, so a -style in force does not apply to it — or the alias of an embedded face. -style takes bold, italic or both; oblique is read as italic when the face is resolved — font answers oblique back, since it hands back what was set, and the face that comes out is Helvetica-Oblique; any other word is refused, and so is a style asked of symbol or zapfdingbats, which carry one cut each. -size is always in points. Further options: -spacing (extra space between glyphs — see below), -wordSpacing, -stretch (horizontal scaling in percent), -leading (line spacing, default 1.2 times the size) and -rise (baseline shift, for super- and subscript).

Every option is checked at the call that writes it, in font and per call alike. -leading must be a number above zero; an empty value restores the default of 1.2 times the size. -spacing, -wordSpacing and -rise take a number of points of either sign — negative spacing tightens, a negative rise is a subscript. -kerning, -ligatures and -unshaped take a boolean. A -color the colour parser does not know is refused where it is written, not at the next piece of text. -spacing adds its space between glyphs, not between characters, because that is where the PDF operator behind it puts it. The two differ wherever a ligature or a combining mark is in play: office is six characters and, in a face that has the ffi ligature, four glyphs — so -spacing opens three gaps there, not five — and a decomposed accent is a glyph of its own, so it takes a gap the precomposed spelling does not; measured on DejaVu Sans at 20 points, Café Crème at -spacing 2 is 47.8944 mm written with U+00E9 and U+00E8 and 49.3055 mm written with U+0301 and U+0300. For classic letterspacing, where every letter stands apart, set -ligatures 0 in the same call; a ligature says the letters belong close together, which is the opposite of what letterspacing says.

-fallback {face …} names the faces that may set what -family cannot — see A fallback chain under font embed.

-ligatures applies the standard ligatures (liga) of an embedded face and is on by default. Where a face has one, the letters of fi, ff, ffi and their relatives are drawn as the single glyph the designer made for them. The characters are unaffected: the ToUnicode map carries the ligature back to the letters it was made from, so the text is copied and searched as it was written. Five features are applied to Latin text, and they are applied together, in the order of the font’s own lookup list rather than feature by feature: the required ligatures (rlig), the standard ones (liga), the contextual ones (clig) — a ligature with a rule about its surroundings, written as a chaining lookup that names an ordinary one — and the contextual alternates (calt and rclt). All five are on by default in the OpenType registry, and a shaper applies them without being asked. Four of the faces shipped with the examples carry clig and no liga at all, and in those the fi of a word is a single glyph while the ffi of another keeps its letters, exactly as the designer wrote the rule. -ligatures 0 switches off liga and clig and leaves rlig, calt and rclt applied, which is what hb-shape --features=-liga,-clig answers. The required ligatures stay because they are required: the face draws the pair as one shape because the script asks for it, and set as two letters it is not a plainer setting of the word but the wrong one — the lam-alef of Arabic is the standing example, and a Latin face that has an rlig is saying the same thing about its own pair. The contextual alternates stay because they are not ligatures at all: an alternate is another shape of the same letter — the swash that only starts a word, the connecting form of a script that has one — and a caller who wanted the letters to stand apart did not ask for a different letter. Not read: the discretionary (dlig) and historical (hlig) ligature sets, which the registry has off by default, so switching them on would overrule the type designer rather than follow them; and ccmp and locl, which need a language to choose by that the package has no way of being told. A cursive script reads ccmp as well and pauses between more of the five — see the section on Arabic. The required ligatures (rlig) of the Arabic scripts are applied by the cursive shaping under -direction rtl whether or not -ligatures is on: a lam-alef the face draws as a joined pair and this package set as two letters would not be a plainer setting of the word, it would be the wrong one. Which lam-alef, exactly, because a face may put the four of them in two different features: the plain lam-alef (ل + ا) is a required ligature and sits in rlig, so it stands whatever -ligatures says; the three that carry a madda or a hamza (لآ, لأ, لإ) sit in liga in DejaVu Sans, and those do fall when the option is off, coming out as two joined letters. That is not a departure — it is bit for bit what hb-shape --features=-liga,-clig gives for the same word, measured over 19 460 Arabic words and the three faces here that shape cursively, 0 differ on either setting. For cursive text all three features are read under the script of the text rather than under the Latin language system, in the two stages a shaper applies them in — rlig first, then liga and clig — so a face that keeps its Arabic ligatures under the arab script is set as its designer drew it. Note that a ligature need not change any width: measured on DejaVu Sans, fi and fl take exactly the room the two letters took, while ff is narrower.

-unshaped draws text from a writing system this package cannot set correctly, and is off by default — which means such text is refused rather than drawn wrong. See “Writing systems” below.

-kerning applies the pair kerning of an embedded face and is on by default. The amounts are read from the font while writing - from its GPOS table where it has kerning lookups, otherwise from its kern table, which is the order ISO/IEC 14496-22 prescribes - and are written into the content stream, so neither table is embedded. Five of the lookup types GPOS defines are read: single adjustment (type 1), pair adjustment (type 2), cursive attachment (type 3), and the contextual and chaining contextual positioning (types 7 and 8) that reach the other three by index. Mark attachment (types 4 to 6) is read as well and places the combining marks. What that is worth is measured rather than claimed, and it is measured on the drawn positions: the glyphs are read back out of the content stream of a written document and held against hb-shape. Over 89 Arabic, Persian and Urdu words in Noto Sans Arabic, Noto Naskh Arabic, Noto Kufi Arabic, Amiri and Scheherazade New, every glyph stands where hb-shape puts it but one — Amiri’s كلمة, one font unit out, the price the old kern table exacts where a face carries both tables. Noto Nastaliq Urdu, the hardest case, agrees on 79 of the 86 words it sets, and the seven others are artefacts of the comparison rather than of the page. Kerning changes the width of every line it touches, and the width is measured with it: textWidth, the line breaker and the table column widths all see the kerned figures. Set -kerning to 0 where a document has to come out exactly as an earlier release produced it. The fourteen standard faces are unaffected: the metrics shipped for them carry widths per byte value, not kerning pairs.

A combining accent between two letters does not interrupt a pair. Most faces tell the reader to leave marks out of the sequence while kerning - measured here, 51 of the 73 faces on this machine that kern from GPOS do - so A + U+0301 + V is kerned as the pair A V, exactly as the single character U+00C1 followed by V has always been. The adjustment is applied in front of the second letter, which leaves the accent where the font puts it. The mark itself is positioned too, out of the same table: GPOS mark attachment — lookup types 4, 5 and 6 of ISO/IEC 14496-22, 6.3.3 — states an anchor on the base and an anchor on the mark, and the two are brought together, so a combining glyph lands under or over its letter rather than at the pen position. Marks attach to marks as well (mkmk), which is what puts a tone mark above a vowel diacritic instead of on top of it.

What that means in practice. Text that arrives composed — Ü as U+00DC, Á as U+00C1 — is unaffected, because the face has a finished glyph for it and there is no mark to place. That covers every European language, and it is what databases, XML and the web deliver. Text that arrives decomposed — Ü as U+0055 followed by U+0308 — is set from the anchors of the face, and comes out where the composed spelling comes out: measured on DejaVu Sans at 12 points, both spellings of Übermut are 18.5229 mm wide, seven characters against eight. A combination Unicode has no composed form for at all — a letter carrying both a macron and an acute — was never the harder case and is not one now; it is what mkmk is for. The mark keeps its place on the letter under every spacing option. -spacing adds its value to the pen after each glyph (9.4.4) while the anchors speak of advances alone, so a mark would drift one -spacing to the right for every glyph since its base; the offset compensates for it, and -stretch scales the compensation and the spacing alike. Measured on DejaVu Sans with b and U+0300: the gap between mark and base is 790 font units under -spacing 10, -spacing -5, -stretch 50 and their combinations — the same number as without any of them. -wordSpacing is deliberately not compensated: it applies to the single-byte code 32 (9.3.3), which an Identity-H font never writes. The old advice to normalise to NFC first is therefore gone. What normalising still buys is a shorter string and one glyph instead of two, not a better result. What nothing can supply is an anchor a face does not state: a face without mark and mkmk lookups has nothing to attach to, and its marks sit at the pen position as they did before there was any placement at all.

-render says what showing the text does to the glyph outlines — the text rendering mode of ISO 32000-2, 9.3.6, written into the stream as the Tr operator. It takes a word rather than the number: fill, the default, in which the outlines are filled with -color; stroke, only the outline, in -stroke and -strokeWidth; fillStroke, filled first and then stroked, which gives a heading a contrasting edge in one call; and invisible, positioned, measured and extractable but painted nowhere — the mode a scanned page uses to carry its recognised text under the picture of itself. -stroke takes any colour the package takes and -strokeWidth a line width in the document unit, 0 or more as -width on a shape is — 0 being the thinnest line the device can draw (9.3.6 and 8.4.3.2 read the same width), and a negative one refused where it is written rather than at the next text call; both are written only in a mode that strokes, because that is the mode in which 9.3.6 consults the stroking colour. The width is a line width and is read in user space rather than text space, so the same 0.3 mm outline is the same 0.3 mm at every size — a rule, not a weight. All three are font state like the rest: set in font they stay in force, given per call they apply to that call, and they reach everything the state reaches, a paragraph and textPath and leader and pageNumbers alike. A mode other than fill is refused on a Type 3 family — one made with font define or colorFont (TCLPDF TEXT RENDER type3): ISO 32000-2, 9.3.6 notes that “the text rendering mode has no effect on text displayed in a Type 3 font”, so invisible on a colour face used to write 3 Tr and leave the emoji standing in full colour. It is said rather than dropped, as -direction rtl is on a face that cannot have it; a face reached through -fallback sets its own segments and honours the mode there.

The four clipping modes are refused, by name and with the reason. Modes 4 to 7 paint nothing: they turn the outlines into a clipping path that takes effect at ET and, in the words of 9.3.6, “remains in effect until a previous clipping path is restored by an invocation of the Q operator”. This package writes one BT/ET per line and wraps a line that sets a text state of its own in its own q/Q, so that Q would discard the clip in the same breath that made it — and leaving the brackets out instead would leak the mode into every later text on the page. A paragraph could not work at all: the clipping path at ET is the intersection with the previous one, so the second line of a block would clip to the empty set, which paints nothing and reports nothing. -render clip, fillClip, strokeClip and fillStrokeClip are therefore refused where the word is written, with all of that in the message.

Writing systems

tclpdf maps one character to one glyph and sets them in the order they arrive — left to right, or right to left where -direction rtl says so. For most scripts that is the entire job, and they are set correctly: Latin, Greek, Cyrillic, the CJK scripts, the Tibetan letters, Cuneiform, Egyptian Hieroglyphs, symbol and emoji faces.

Some scripts need more. What that “more” is decides whether tclpdf can set them at all — three of the four things it can do now that marks are placed, the rest it refuses to draw rather than draw something wrong:

needs scripts available
right-to-left ordering Hebrew with its nikud, Thaana with the fili, Samaritan with its points; and the non-cursive right-to-left scripts of the supplementary planes — Cypriot, Imperial Aramaic, Palmyrene, Nabataean, Hatran, Phoenician, Lydian, Sidetic, Meroitic Hieroglyphs and Meroitic Cursive, Kharoshthi, Old South and Old North Arabian, Avestan, Inscriptional Parthian and Inscriptional Pahlavi, Old Turkic, Old Hungarian, Garay, Yezidi, Old Sogdian, Elymaic, Mende Kikakui, the Indic and Ottoman Siyaq numbers and the Arabic mathematical alphabets — their combining marks with them, save in the four blocks the mark-reordering row names yes — -direction rtl
contextual forms — the glyph depends on its neighbours — and right-to-left ordering Arabic (including Extended-B and Extended-C), the harakat with it yes, in a face that carries the positional forms — -direction rtl
contextual shaping and right-to-left ordering Syriac (and Syriac Supplement), N’Ko, Mandaic, Manichaean, Psalter Pahlavi, Hanifi Rohingya, Sogdian, Old Uyghur, Chorasmian, Adlam no
contextual shaping Mongolian, Phags-pa no
stacked forms out of GSUB the Tibetan vowel signs and subjoined letters — U+0F71–U+0FBC and the other stacking marks of the block; the letters, the tsheg and the digits set no
mark reordering the combining marks of four of the supplementary-plane blocks above: Kharoshthi (the vowel signs and virama among them), Garay, Yezidi and Mende Kikakui no
reordering and conjunct forms Devanagari, Bengali, Gurmukhi, Gujarati, Oriya, Tamil, Telugu, Kannada, Malayalam, Sinhala, Myanmar, Khmer no
mark placement and reordering Thai, Lao no

Ordering is the one this package can supply. Hebrew letters, Thaana and Samaritan bases carry no contextual forms: the glyph the character map gives is the glyph a reader expects, and only the order has to be turned round. Their combining marks travel with them: the nikud, the fili and the Samaritan points are placed from the anchors of the face like any other mark. The archaic right-to-left scripts of the supplementary planes are the same case — the joining table of the Unicode database knows none of them, Meroitic Cursive included, which is cursive in name only. Four of those blocks hold marks all the same, and those stay refused: a shaper reorders a Kharoshthi, Garay, Yezidi or Mende Kikakui mark before it places it, and reordering is what this package does not have. They are found by their Joining_Type in the Unicode Character Database rather than by a hand-written range list, because they come in a scatter and not in ranges; and no face on this machine carries any of the four, so what is said about them rests on the mechanism. -direction rtl turns it — after the ligatures and the kerning have been worked out on the logical run, so the pairs looked up are the ones the type designer meant, and textWidth answers the same number in both directions. Without the option such text is still refused, and the message names the way out; example 02.09-writing-systems sets a line of the one such block a face of its pages carries, the Arabic mathematical letters in DejaVu Sans.

Arabic is set with its contextual forms, and that needs the right face as well as the option. Each character is asked which of the four shapes it stands in — isolated, initial, medial or final — by the cursive joining algorithm of the Unicode Standard, section 9.2, with the joining types taken from the Unicode Character Database. The shape itself then comes out of the face, through the GSUB features ccmp, then the positional isol, fina, medi and init — each glyph takes exactly one of the four, the one its position asks for — and rlig last, applied in that order. Every substitution lookup those features name is read, the contextual and chaining contextual ones (GSUB types 5 and 6) included: a rule of those matches a sequence and then applies other lookups at named positions of it, which is how a face selects the lam-alef pair or widens a medial skeleton before certain finals. The result is checked glyph by glyph against HarfBuzz: tests/forms.test shapes six words with hb-shape and compares the glyph numbers, and skips itself where HarfBuzz is not installed.

Which faces. A face has to carry the three positional features, and that is the whole condition — a face whose GSUB has no init, medi and fina is refused by name, and the message says so. How the face draws a letter no longer decides it. Noto Naskh Arabic, the one shipped with the examples, writes a letter as an undotted skeleton and hangs the dots on it as separate glyphs, made by ccmp and placed by GPOS mark attachment; while that attachment was missing, the dots would have landed beside their letters — measured on that face, the two dots of teh marbuta belong a third of an em lower than they would have been drawn — and the face was refused for it. It is set now, dots and all. A mark the face does not anchor — one the lookups leave alone — stays with its base all the same, and in a right-to-left line it lands at the base’s own origin rather than past its far edge: the pen runs the other way there, and a mark without an anchor has nothing else to go by. DejaVu Sans carries whole Arabic letters and sets the same line as it always did; example 02.12-vocalised-rtl puts one vocalised word in both faces, because only the second shows that a generated dot lands on its anchor.

The marks stay in the order they were written, with one exception. The Arabic shadda U+0651 and the seven modifier combining marks of UTR #53 — U+0654, U+0655, U+0658, U+06DC, U+06E3, U+06E7, U+06E8 — are drawn before the vowel signs of combining classes 27 to 32 and 35, whatever order the string carries them in. That is the Arabic Mark Transient Reordering Algorithm, which HarfBuzz applies in reorder_marks_arabic, and it is the reason the two spellings of a vocalised word draw the same picture: canonical order (and therefore NFC, and therefore any normalised input) sorts fatha (ccc 30) before shadda (ccc 33), while Arabic typography puts the shadda underneath. Measured on Noto Naskh Arabic: hb-shape answers [uni064E|uni0651|uni0645] for both orderings of U+0645 U+064E U+0651. Every other mark of every other script stays exactly where the caller wrote it — this is a named rule, not a sort by combining class, and the position a refusal names is still the position the character has in the string as written.

What is still missing in Arabic. Not the chaining lookups any more, and that is the measurement rather than the claim: over 6630 Arabic words set in Noto Naskh Arabic, the glyphs this package produces are the glyphs hb-shape produces with nothing switched off — 0 differ, where 1552 differed before those lookups were read. Over thirty faces and 135 750 words, that count is 0 as well, against 21 543 before. Cursive attachment is applied since 2026-08-26: a face that hangs its letters on each other’s entry and exit points — the Nastaliq style is the extreme case — is set with the staircase the face draws rather than with every letter on the baseline. The two things that were open there are closed since 2026-08-27. A face that inserts a zero-width spacer glyph after every initial form — Noto Nastaliq Urdu writes one, with no character of its own — used to have the dots and vowel signs that follow it drawn beside their letter rather than over it: measured over 86 Urdu words against hb-shape, 46 of them stood more than two font units out, most by a whole letter width, and the count is 7 now, all of them the ZWNJ and bidi artefacts of the comparison itself. And a mark written between the two components of a ligature — the fatha of the Arabic lam-alef is the everyday case — hung on the ligature’s last component, 920 font units away in Scheherazade New; the package keeps track of which component each mark was written behind, as ISO/IEC 14496-22, 6.3.3 requires of a layout client, and places it there. What is left besides is one feature: locl, localised forms, needs a language to choose by and this package has no language API. The liga gap left this list on 2026-08-26 — the ligatures a face keeps under the arab script are read under that script now, and the lam-alef forms of DejaVu Sans that used to come out as two joined letters come out as the single glyph the face draws. The harakat left it earlier: the vowel signs are combining marks, and marks are placed.

Nikud is set, and with it the harakat, the Thaana fili and the Samaritan points: the points are combining marks, the anchors for them are read (see -kerning above), and a vowel point lands under its letter instead of beside it. Measured against hb-shape of HarfBuzz 14.3.1: 366 of 366 consonant-and-point pairs of DejaVu Sans agree to the unit, and so do 5909 of 5909 letter-and-harakah pairs of Noto Naskh Arabic that the face anchors at all. Not every face is that clean. In Liberation Sans, Liberation Serif and Arimo, 25 of 1188 Hebrew pairs sit 25 units of 2048 out — a good hundredth of an em — and every one of them is the holam U+05B9, because those faces refine it in a chaining contextual lookup, GPOS type 8, which this package does not read. And two of the four are unseen: Thaana and Samaritan take the same road as Hebrew, but no face on this machine carries either script, so what is said of them rests on the mechanism and not on a page anyone has looked at.

A combining mark from outside the right-to-left blocks is refused as a mixed line. The marks of the right-to-left scripts themselves are set: nikud, harakat, the fili and the points travel with their letters. A combining mark from any other block in a -direction rtl line — a Latin accent of U+0300–U+036F, a symbol mark of U+20D0–U+20F0, the emoji variation selector U+FE0F — is refused too, but with the mixed-line message below: the mark counts as left-to-right, although UAX #9 (rule W1) gives such a mark the class of the character it follows, so the line mixes nothing — ✔️ in a Hebrew line is refused at the U+FE0F where ✔ alone is set. The ways out: set the piece that carries the mark as a call of its own without -direction rtl — the same marks pass in a left-to-right line — or use a form without the mark, the bare symbol where the selector only picked a presentation. The Hebrew presentation forms U+FB1D–U+FB4F are no longer a way round anything: ו plus dagesh U+05BC is set, so וּ as the single character U+FB35 is one more spelling of the same word rather than the escape it used to be. -unshaped 1 lifts this refusal along with the others.

Numbers keep their own order. A right-to-left line is not simply reversed — a run of digits inside it runs left to right, because it does in every script that uses digits. الفاتورة 4711 sets the invoice number as 4711 and not as 1174, which is what the plain reversal produced and what nobody looking at the page could tell was wrong. Which characters belong to a number is not a list of this package’s own but the Unicode Character Database (DerivedBidiClass.txt, Unicode 17.0.0), reduced to the classes of UAX #9 that rules W1–W7 fold into a number: the digits — European, Arabic-Indic (٠–٩), Extended Arabic-Indic, superscripts and the rest of EN and AN — keep their order; a Common Separator (CS: comma, full stop, colon, slash, no-break space, Arabic comma …) belongs to the number between two digits; a European Terminator (ET: percent and per mille, degree, the currency signs € $ £ ¥ ¢ ₹ …, number sign, plus-minus …) belongs to it directly beside one. After an Arabic letter a European digit counts as an Arabic one (rule W2; after a Hebrew letter, or at the start of the line, it stays European), and an Arabic number is stricter: a separator joins it only between two Arabic digits (W4), and no terminator or sign joins it at all (W5 applies to European digits) — so الفاتورة 4711 - 1.234,50 € (19%) keeps 4711 and 1.234,50 whole and sets the % and the brackets where the line runs, and -17,5° after an Arabic word is a sign, a number and a degree sign, as fribidi and UAX #9 have it. One thing is deliberately not the plain rule: a plus or minus beside a European digit is treated like a terminator, so that -5 and +3% in a Hebrew line keep their sign — measured against fribidi over 31 invoice lines, that sign is the only difference. The Arabic decimal and thousands separators (U+066B, U+066C) are AN in Unicode, part of the number wherever they touch it. This is the corner of the Unicode bidirectional algorithm (UAX #9, rules W2, W4 and W5) that one run of digits needs, and nothing beyond it.

Paired brackets are mirrored. ( is the opening bracket, and the opening bracket of a line that runs the other way is drawn with the glyph of ) — Unicode calls this mirroring (UAX #9, section 3.4) and it is a property of the display, not a different character. It applies to ( ), [ ], { }, < >, « » and ‹ ›; the German quotation marks ‚ ' look like a pair and are not mirrored, and neither is a slash. Because the ToUnicode map speaks about glyphs and the glyph of ) is the glyph of ) wherever it is used, each mirrored glyph is drawn inside a Span carrying the character it stands for as ActualText, so that the line still extracts as it was written. Measured with poppler 26.08.0: without that span (שלום) comes back as )שלום(.

A mixed line is refused. A line that mixes the two directions — an Arabic sentence with a Latin word in it, a Hebrew heading with a Cyrillic name — needs the full bidirectional algorithm to decide which run goes where, and tclpdf does not implement it. Such a call is therefore refused, naming the first character that runs the wrong way and what to do instead:

tclpdf: U+0052 (position 0) is left-to-right in a -direction rtl line
- a mixed line needs the bidi algorithm, which tclpdf does not have;
set the runs as separate calls, one per direction

It used to be accepted and to come out with the Latin words backwards — gnunhceR … rellüM — which is the kind of line that looks like text and is not. Everything that is not strongly left-to-right may stand in such a line: right-to-left letters, digits, spaces, punctuation, brackets, currency and symbols. -unshaped 1 turns the refusal off along with the others, for the caller who knows what it does.

The mirror image is unchanged: a right-to-left script in a left-to-right line is refused as it always was, and that message names -direction rtl as the way out.

Which calls take -direction. text, textWidth and textLines, and with them the paragraph forms of text; textPath, where the glyphs run backwards along the path and -align is mirrored with it; leader, where the two ends change places and the fill is measured from the other side; pageNumbers, so that صفحة 3 من 10 comes out with its numbers the right way round; and the table, where direction is a style key and a cell key like align — a right-to-left description column and a left-to-right amount column on the same row are two cells, not two tables. A decimal column does not mirror: it holds numbers, and numbers are set left to right in either direction.

The refusal is the same rule the package applies to a character the face has no glyph for: a reader shows the wrong text, a validator says nothing, and only this end can notice. Arabic drawn without shaping comes out as isolated letter forms — it looks like text and is not.

-unshaped 1 turns the refusal off altogether and draws the characters as isolated glyphs in the order they were given. It is not the answer for a script that needs only ordering — -direction rtl is, and it is the better one — nor for Arabic in a face that can be shaped. What is left for it is the case where the forms, the stacks or the reordering cannot be had and the caller decides that isolated glyphs are better than nothing — a Tibetan syllable, an Indic conjunct, a line of Thai; combined with -direction rtl such a line at least runs the right way. Example 02.12-vocalised-rtl draws one Tibetan syllable both ways, refused and with the option set.

What extraction gives back, measured with poppler pdftotext 26.08.0. A line reading שלום עולם, set with -direction rtl, extracts by default as U+202B שלום עולם U+202C — the string as it was written, wrapped in the two directional marks, because the reader reconstructs the logical order from the Unicode values in the ToUnicode map. pdftotext -raw, which reports the drawn order on purpose, gives the two words the other way round with the letters of each word in logical order. The same string drawn with -unshaped 1 instead — a logical run set left to right — comes back from the default pdftotext reversed, as םלוע םולש. So the option that makes a right-to-left line extract as it was written is -direction rtl, and the one that does not is -unshaped 1.

doc font family name ?-regular alias? ?-bold alias? ?-italic alias? ?-boldItalic alias?
Registers the faces of one embedded family under one name, so that -style reaches an embedded face the way it reaches a standard one. font embed gives one face one alias, and the alias is the face: font -family body -style bold set the regular face and said nothing, which is why every example embedded bodyBold beside body and named the bold alias itself wherever a heading stood. After font family text -regular body -bold bodyBold, -family text -style bold sets bodyBold — exactly as -family helvetica -style bold sets Helvetica-Bold — and so does every bold run of a paragraph set with -runs or -markup. The members are aliases of faces already embedded and are checked at the call (TCLPDF FONT FAMILY MEMBER); -regular is required (REGULAR), the other three may be left out, and a style that names a member the family does not have is refused by name rather than set in the regular face (TCLPDF FONT FAMILY STYLE family member) — that silence is the very thing this call ends. The name must be a name of its own, not an embedded alias and not one of the standard families (NAME). Without options the call answers the registration (UNKNOWN for a name nobody registered). The family’s members keep their aliases: font info, font names and everything else that takes an alias take the member, and font answers the member under resolved.

doc font embed alias ?path? ?-data bytes? ?-subset 0? ?-metrics path? ?-axes {tag value …}? ?-instance name? ?-face n?
Embeds a font file under an alias, which is then usable as -family. The file says what it is; the extension is not consulted. (PDF 1.2; a CFF face PDF 1.6.)

TrueType (.ttf) is subset to the glyphs actually used, addressed by glyph number, and gets kerning and ligatures from the font’s own tables. OpenType with CFF outlines (.otf) goes in whole, as /FontFile3 with /Subtype /OpenType and a /CIDFontType0 descendant — the Type 0 font is named <name>-Identity-H as ISO 32000-1 9.7.6.1 asks, the CIDFont and the descriptor carry the plain name — everything else about it, the character mapping and the widths included, is read exactly as for TrueType. A CID-keyed CFF face — one whose Top DICT carries the ROS operator, as CJK faces from Adobe and Apple do — is refused by font embed, naming the file: ISO 32000-1 9.7.4.2 lets a CIDFontType0 address glyphs by glyph index only when its program is name-keyed; for a CID-keyed program the CID goes through the program’s charset, and wherever that is not the identity a conforming reader would draw a different glyph than the one measured here, with no validator noticing. Convert such a face to TrueType or to a name-keyed CFF first. Type 1 (.pfb, .pfa, .t1) goes in whole as well and is addressed by single bytes through WinAnsiEncoding — see below. A TrueType collection (.ttc) is read as its first face unless -face names another: several faces stand side by side in such a file and font embed is handed one file name, so the face number is what says which of them is meant. 0 is the default everywhere; a number the collection has not got is refused naming the range it does have (-face is the face inside this collection, 0 to 5, not "99", TCLPDF FONT FACE). This is no edge case: the Japanese system faces of macOS and Windows are shipped exactly that way, and so are Helvetica, Times and Courier on macOS — measured, font embed a /System/Library/Fonts/Helvetica.ttc embeds Helvetica and -face 2 embeds Helvetica-Oblique, and font info answers the face that went in.

The six-letter subset tag is derived from the font program, from the glyphs used and from the point on the axes — not from the PostScript name alone, which is not the face: two files may carry the same name id 6 and different outlines, and one tag over two different subsets lets a reader that merges fonts by name and tag (which ISO 32000-2, 9.6.4 entitles it to) draw the one with the other’s glyphs. Same input, same tag, so two writes of one document stay byte-identical; a document that sets one letter more gets a different tag, which is right, because it is a different subset.

A glyph that stands for no character of its own gets a ToUnicode entry with an empty destination, <0142> <>. Such a glyph is a piece of a one-to-many substitution — the dot a Naskh face draws beside the letter skeleton, where the letter’s code point belongs to the skeleton — and writing the letter’s code here as well would extract the letter twice. Leaving the entry out was tried and taken back: ISO 19005-3, 6.2.11.7.2 asks the font dictionary to define the map of all used character codes, and veraPDF counts a CID with no bfchar as unmapped, so example 02.09 came out with four failed checks against a rule it had passed for months. Adobe TN 5411 shows only non-empty destinations in its grammar, which is a matter of style; the conformance level is not.

One CID per glyph and characters, not one per glyph. A ToUnicode CMap has room for exactly one destination per character code (ISO 32000-2, 9.10.3), so a glyph that two different pieces of text reach can be spelt back only one way — and the other reading is then wrong in every reader, with nothing to report it. It is not a rare case: Noto Naskh Arabic and Noto Sans Arabic write sin and shin, and beh, teh, theh, nun and yeh, as one skeleton glyph plus separate dots, so whichever word used the skeleton first decided the letter for the whole document. Measured over 89 Arabic words, 49 extracted as a different word. A CID is not a glyph number — Identity-H says the code is the CID, and CIDToGIDMap says which glyph the CID draws, and that table maps many CIDs onto one glyph (9.7.4.2, Table 116). So the second reading of a glyph gets a CID of its own with its own width and its own bfchar, and both words extract as they were written. The first reading keeps the glyph’s own number, so a document in which no glyph is read two ways is written exactly as it was before; the extra CIDs are counted from numGlyphs up. The price is measured: over the 89 Arabic words above, 95 glyphs came to 137 CIDs and the file grew by 817 bytes in 26.7 kB, and of the 90 example documents 88 are unchanged.

A face that draws nothing is refused. A colour font keeps its pictures in a table of its own — COLR/CPAL, CBDT, sbix or an SVG table — and leaves the outline of every character it covers empty. This package writes outlines and none of those tables, so such a face embeds, measures, extracts and validates without a word and prints a blank page: measured with Noto Color Emoji, pdftotext gave the code point back and pdffonts reported a clean subset over an empty sheet, and qpdf and veraPDF called the document flawless. font embed therefore refuses it, naming the file, with the error code TCLPDF FONT OUTLINES. The test is the empty outline, not the colour table: a colour font that carries real monochrome fallback outlines is embedded and draws them, and a face with no colour table whose outlines are all empty is refused just the same. A glyph with two points is empty too. Apple’s sbix faces carry a glyf record of 26 bytes for every character — one contour of two points and a bounding box — and two points enclose no area. Measured on 2026-08-26 with a subset of Apple Color Emoji: font embed took the face, text set it, textWidth answered 28.2 mm, pdffonts reported a clean subset and pdftotext gave the emoji back, and the rendered page was byte-identical to a blank one. The test is therefore the number of points, not the length of the record. A single empty outline says nothing and never triggers it — the space has none in most faces (measured in DejaVu Sans: 61 of its 5918 characters, the space among them). Where such a face names the table its pictures are in — sbix, CBDT or an SVG table without a COLR beside it — the refusal names that table as its fourth word, because “this is an sbix colour face” is something a caller can act on. A face whose pictures are in sbix or CBDT is refused whether or not it draws nothing at all, and that is the one place this rule parts from the empty-outline test beside it: measured on the real Apple Color Emoji rather than on a fixture, 42 of the 1469 characters its cmap covers do have outlines — the digits, the hash and the star, which are the bases of the keycap sequences — so the face draws something, truthfully, and an embedded copy sets 1397 blanks and 42 digits. sbix and CBDT hold pictures and nothing else, so such a face has no outline road at all; COLR and SVG are different in kind, since a face may carry either beside a complete monochrome set. colorFont is the road that works for an sbix face, and the message says so. A face carrying COLR/CPAL is the one kind this package does draw, with colorFont; Noto Color Emoji carries an SVG table beside its COLR and goes that road.

A TrueType face without glyf and loca is refused as well, with TCLPDF FONT TABLE glyf and at the font embed that named it. ISO 32000-2, 9.9, Table 128 requires glyf, head, hhea, hmtx, loca and maxp in an embedded TrueType program. Such a file is a bitmap or colour face whose pictures live elsewhere — the CBDT/CBLC emoji faces are the everyday case. Embedded whole with -subset 0 it used to produce a document qpdf calls clean, pdftotext reads back and pdffonts cannot make a font of; with the default -subset 1 it was taken, measured, set, and died at write.

Which character map is read. A face carries several; the order of preference is the one HarfBuzz uses — (3,10), then (0,6), (0,4), (3,1), then the older Unicode encodings — and formats 4 and 12 are read. A record in a format this package does not read falls through to the next one down rather than turning the face away; only when none of them can be read is the face refused, naming the format of the best candidate. The Windows records used to be ranked above every platform-0 record, which put the BMP-only (3,1) ahead of a (0,4) table in format 12: measured on a DejaVu Sans built with both, the face answered 5370 characters instead of 5918 and refused U+1F600, which it has.

Set such symbols with a monochrome face — Noto Emoji stands in for Noto Color Emoji — or draw them yourself as a Type 3 font with font define and font glyph.

Why a CFF face is not subsetted. Subsetting rewrites the loca and glyf tables, and a CFF font has neither: its outlines are charstrings, which this package reads — for the width that stands in front of the first hint, move or endchar operator, and for the top of the H a descriptor needs — but does not rewrite. So the whole file is embedded, around 40 to 100 KB depending on the face. Where the same face exists as .ttf, that form is the better choice for a document that uses it for a heading and nothing else — a subset of a few words is a fraction of either. -subset is accepted and has no effect; the face carries no subset prefix, because nothing was subsetted.

What the descriptor says comes from the file (ISO 32000-1 Table 122): the italic angle from post — -12 for Nimbus Sans Oblique, where a constant 0 stood before — the cap height from OS/2 where the table is recent enough to carry one, else the top of the H (DejaVu Sans, OS/2 version 1: 729 where the ascender is 928). A Type 1 face and a bare CFF have no such table at all: the cap height is then the top of the H — out of the AFM’s own per-character bounding box, or, for a bare CFF, out of the charstring of the H — and the ascent is the top of the d or the b, the two lower-case letters that reach it. The upper edge of the FontBBox is the last resort and no longer the first: that box is drawn round the whole face, accents and all, and Nimbus Sans reaches 1075 there where its letters reach 729, so the same face embedded as .t1 and as .otf disagreed by 35 pt at 100 pt under -anchor top. Descent is written negative whatever sign the file states (Table 120: “shall be a negative number”). The flags carry the italic bit where either the angle or the style says so; for an instance on a slnt axis the axis value is the italic angle, and slnt ≠ 0 or ital ≥ 0.5 sets the italic flag. The fixed-pitch bit (bit 1) is set for a Type 1 face and a bare CFF, where the font program states it outright as the Private DICT’s isFixedPitch or the AFM’s IsFixedPitch, and for no other kind: a TrueType or OpenType descriptor here is symbolic (bit 3) and carries the italic bit alone, because the flag would then have to be guessed from the advance widths, and a guess in a descriptor is worse than a bit a reader can do without. StemV, which TrueType has no field for, is estimated from the weight class as 50 + (weight / 65)² — 87 for a regular face, 165 for a bold one; for an instance of a variable face the wght axis is the weight class. A wrong StemV affects hinting hints, not the glyphs.

For an instance of a variable font the descriptor states the instance’s metrics, not the file’s: MVAR varies Ascent, Descent and CapHeight with the axes exactly as gvar varies the outlines. Measured against fontTools’ instancer on Bitcount at wght 700, slnt -8: cap height 660, where the file’s OS/2 says 600.

A character the face has no glyph for is an error, not a blank. text, textWidth, a table cell and text inside an SVG all refuse the string, and the message names the face, the character as U+XXXX and its position: the font "free" has no glyph for U+2714 (position 0) - it cannot be written with this face With a -fallback chain in force, the face name is the list of the faces that were asked, all of them, in the order they were tried.. This end is the only one that can notice: a reader shows a blank or a box, a validator says nothing, and the recipient reads an invoice with a gap where the check mark was. Do not take a font viewer’s word for what a face contains. A viewer that is asked for a character the face lacks substitutes the glyph from another face and shows the result without saying so — measured with FreeSans, whose viewer window shows U+2714 (✔) although the file has no glyph for it (hb-shape FreeSans.ttf --unicodes U+2714 answers .notdef). A PDF has no such fallback: the embedded font is all a reader has. What tells the truth is the file itself — hb-shape, or simply setting the string here and reading the message — and fc-list ':charset=2714' names the installed faces that carry the character; for the dingbats and symbols DejaVu Sans and Noto Sans Symbols 2 do.

A fallback chain sets what the family cannot. font -fallback {notoJP} names further faces — embedded aliases or standard families, in order — and every character the family has no glyph for is set from the first face in the list that has one. The line then falls into segments, one per face, each with its own font resource, its own glyph run, its own widths and its own ToUnicode map, so pdftotext gives the line back whole and in the order it was written. A character no face in the chain has is refused exactly as before, with the same TCLPDF FONT GLYPH error code. The option is font state like -size: set in font it stays in force, given per call it applies to that call, and it reaches everything the state reaches — a paragraph, a table cell, textPath, leader and pageNumbers. Every face named has to exist when the option is written, so embed it first; a misspelt alias is refused at the call that wrote it rather than by whichever line first needs it.

The chain is consulted only where the family has no glyph. Text the family can set produces the bytes it produced without a chain — same resource, same Tf, same show operator — so a chain set once for a document cannot change a table, an SVG drawing or a page number that never needed it, and the option can only turn what was an error into text. The decision is per character and does not depend on the neighbours: the same character is always set from the same face, which is what makes a measurement repeatable. It follows that the space between two Japanese words comes from the family wherever the family has one. A Type 3 family — font define, colorFont — usually has none, and the space then falls through to the chain like any other character: the word spacing is written as the operator that face needs, Tw for a face addressed by single bytes and a TJ adjustment for one addressed through Identity-H, decided per segment rather than for the line.

What ends at a segment boundary. Kerning and ligatures are looked up in one face’s own tables, so a pair or a ligature whose characters land in two faces does not exist — and the measurement sees exactly that, because textWidth, the line breaker, the table columns and the drawing all walk the same segments. Measured on TČ at 100 points, where Niconne carries the T and DejaVu Sans the Č: the chain answers 137.5 points, the sum of the two pieces, against 125.05 for the pair set in DejaVu alone, where it kerns — and the content stream draws to the last digit what textWidth answered. What the option buys is also what it costs: the line is set in two designs, and where the faces differ in weight, width or design size a reader sees it.

-fallback and -direction rtl are refused together. The drawing order of a right-to-left line is decided across the whole line (see Writing systems), and a face change inside it would reorder only its own piece — silently, for anyone who cannot read the script. Set the pieces as separate calls, or give -fallback {} for that one.

Type 1 needs its metrics beside it. The widths of a Type 1 face are inside its encrypted charstrings, so they come from the AFM instead: font embed looks for the same base name with .afm, and -metrics names it where it sits elsewhere. Without metrics the face is refused rather than embedded with no widths. A glyph the AFM lists and the program lacks is refused like any missing character (Adobe’s Helvetica.afm lists Euro, Helvetica.pfb has no such charstring — it used to set a blank). -axes and -instance on a Type 1 program are refused, as is -metrics on a TrueType or OpenType face.

Embedding a Type 1 program is a copy — the file already consists of the three pieces PDF asks for as Length1, Length2 and Length3. The encrypted section is decrypted only to read the names of the glyphs the program holds — nothing is rewritten and nothing is subsetted: the face goes in whole, which for a text face is 25 to 105 KB. -subset does not apply. The one thing that is not copied as it stands is the hexadecimal form of a .pfa: PDF wants the encrypted piece as bytes (ISO 32000-1 9.9.1), so the hex is decoded on reading, and a .pfa then embeds the same font file as the .t1 or .pfb of that face — measured, byte for byte, and veraPDF passes it where the pass-through failed 6.2.11.4. Kerning and ligatures do not either; a Type 1 program carries neither GPOS nor GSUB, and the kern pairs an AFM may list are not read.

The reach of such a face is the 224 positions of WinAnsiEncoding, as with the standard fourteen — a character outside it is an error, not a blank. Two of those positions are aliases the encoding itself defines (ISO 32000-1 Annex D): 0xA0, the no-break space U+00A0, is a second code for the space glyph and is set — and measured — as wide as a space, so 12 € keeps its unit on the line; and 0xAD is a second code for the hyphen, which is why a soft hyphen never reaches the font. The no-break space is written as byte 0xA0, not turned into a space; readers extract it as U+0020 for a standard or Type 1 face all the same (measured with pdftotext). Inside a paragraph it is neither a break point nor a stretchable gap — see “Where a line breaks” under text. Where a document needs more, TrueType is the format to embed.

Variable fonts carry one set of outlines plus a rule for bending them, and -axes or -instance says where on that rule to embed. -axes {wght 620 wdth 87} names axis values directly; -instance "Condensed Bold" names a point the designer named, taken from the font’s own name table. The two combine — -instance sets the starting point and -axes overrides single axes of it. Without either, the face is embedded at its default position, which is what sits in its outline table. An axis value has to be a finite number inside the axis’s range; NaN and Inf are refused with TCLPDF FONT AXES naming the axis, as a value outside the range is. An axis named twice in one -axes list is refused as well rather than resolved: -axes {wght 400 wght 700} is a Tcl list, and reading it as a dictionary takes the last value only, so the face came out at 700 and the 400 the caller wrote was never mentioned again.

Each point on the axes is its own embedded font, because PDF has nowhere to put an axis value: not in the font dictionary, not in the descriptor. The outlines are therefore computed while embedding and go into the file as a fixed instance. Nine weights on a page mean nine subsets, and each is named for what it is: a named instance carries the PostScript name the font gives it — Roboto-Bold, Roboto-CondensedLight — whether it was asked for by name or by the axis values that are that instance, and any other point is named by its axis values the way Adobe Technical Note 5902 lays down, Roboto_620wght, Roboto_620wght_87.5wdth. The face’s own name, Roboto-Regular, names its default and nothing else. The subset tag in front of the name is derived from what was subsetted — the name, the glyphs used, the point on the axes — so two subsets of one file in one document carry different tags, as ISO 32000-1 9.6.4 asks, and two writes of one document carry the same. pdffonts on example 02.10-variable-fonts shows twenty-four instances of one file, each under its own name and tag; it used to show twenty-four times SZGNUB+Roboto-Regular.

-subset 0 on an instance still embeds every glyph — but as the instance, not as the variable file. The file itself, fvar, gvar and all, would hand a reader the default outlines under the instance’s widths, Regular shapes spaced as Bold; measured with veraPDF, 22 width mismatches under 6.2.11.5. So all glyphs go through the instancer, the variation tables stay behind, and no subset tag is written because nothing is missing.

The advance widths vary with the axes and are read from the same source as the outlines, so textWidth, the line breaker and the table columns all measure the instance that is actually drawn. An axis the font does not have, or a named instance it does not offer, is an error that lists the ones it does, and a value outside an axis’s range is refused with the range (wght runs from 100 to 900) rather than clamped. -axes on a face without an fvar table is refused rather than ignored.

What varies and what does not: outlines, component offsets and advance widths do — the advance of an empty glyph included, so a space is 490 units wide in Roboto Thin and 510 in Black, as fontTools’ instancer has it. The bounding box and left side bearing of a composite glyph follow its moved components: after instancing, the box is rebuilt from the outlines the composite now places (as fontTools’ recalcBounds does) and the bearing set to its new xMin, as head flag bit 1 promises. Measured over the seven variable faces in the examples at their weight maximum, carrying the file’s box over would have left 6 207 of 6 275 composites off by ten units or more — Roboto Black’s A-dieresis by 90 on the right, its r-caron by 123 on the left. Neither poppler nor CoreGraphics render the difference, because both place a glyph at xMin minus bearing and the two stale values agreed with each other; the embedded font describes what it draws all the same. The bounding box of an instance follows the moved outlines as well: the head table of the embedded subset and /FontBBox in the font descriptor (ISO 32000-1 9.8.1, Table 122) both state the box of the instanced face, computed as the union of every moved glyph — measured at the axis corners of the seven variable faces in the examples, the file’s box was off by up to 351 units (NotoSerifTibetan at wght 900), and Roboto Black reaches 130 units further right than the file declares. The four summary fields of hhea — advanceWidthMax, minLeftSideBearing, minRightSideBearing and xMaxExtent — follow the same rule: for an instance they are computed from the moved glyphs by the format’s own definition (the maximum advance over every glyph, the bearings and the extent over the glyphs with contours only, as fontTools’ recalc does) — measured at the axis corners of the seven variable faces, the file’s numbers were off by up to 377 units (NotoSans’s minRightSideBearing at wght 100 wdth 62.5), and Roboto Black’s advanceWidthMax is 2475 where the file says 2378. A face embedded without axes keeps the file’s box in both places and the file’s hhea summary, byte for byte. Hinting programs are carried through unchanged. The advances are read off the phantom points of gvar, which every glyph carries; the HVAR table, which every variable face in the examples ships, repeats the same deltas for readers that do not walk gvar and is not read — an instancer that pins every axis drops it after applying the phantom points, and measured against fontTools glyph for glyph, the two sources agree. The vertical metric of an instance comes from the same place. Phantom point 3 of a glyph sits at its vertical origin and phantom point 4 an advance height below it, so an instance’s /W2 entry and the vmtx of the embedded subset are read off gvar exactly as its widths are; the VVAR table repeats the same deltas for a reader that does not walk gvar and is not read. Built the other way — the instanced yMax plus the file’s own top side bearing — the two halves stopped cancelling: measured on NotoSansSymbols set with -direction ttb, the arrows hung at 1470 and 1474 units at wght 100 and at 1492 and 1504 at wght 900, where fontTools’ instancer and the face’s own VVAR say 1480 at every weight.

A bare CFF — a CFF table with no sfnt around it, as a font compiler emits one and as a PDF carries one under /FontFile3 — is embedded too, and it is a different road from the .otf of the same outlines. None of the tables an .otf answers with is there: no cmap, so no character reaches a glyph; no hmtx, so no glyph has a width; no name, no head, no OS/2. Every one of those answers is nevertheless in the file, and tclpdf reads them out of the CFF’s own structures — the PostScript name from the Name INDEX, the em from the Top DICT’s FontMatrix (a face on a 2048 grid measures right and draws at half size if that is assumed to be 1000), the glyph names from the charset, the stem width from the Private DICT’s StdVW — the one number the TrueType road has to estimate — and the widths from the charstrings themselves, where each is an optional operand in front of the first hint, move or endchar operator. Such a face goes in as /FontFile3 with /Subtype /Type1C, a Type 1-equivalent font program, and is therefore addressed by single bytes through WinAnsiEncoding exactly as a .pfb is: it reaches the 224 positions of that encoding and no more, where the same outlines inside the .otf are addressed by glyph number and reach every character the cmap covers. Where the .otf exists, embed the .otf. A CID-keyed bare CFF is refused for the reason above, and so is a face none of whose glyphs carries a WinAnsi glyph name, since not one byte position could then be written — a symbol face is usually not that case, because the digits, the ASCII punctuation and the space keep their WinAnsi names: measured, StandardSymbolsPS reaches 44 byte positions of its 191 glyphs and D050000L reaches 2 of its 203. -metrics, -axes and -instance are refused on a bare CFF; -subset is accepted and has no effect.

The path is optional, and -data replaces it. A face that never was a file — one out of a database, one out of an archive, one fetched over the network — had to be written to a temporary file first, and the caller then had to remember to remove it. font embed house -data $bytes takes the bytes where the path would have stood, exactly as image embed and colorFont do; the alias stays first and positional, and what tells -data the option from a file that happens to be called that is the count of the remaining words, an odd number of them being a path. Naming both a path and -data is refused rather than ranked, and so is naming neither. Refusals over bytes describe them by their length instead of quoting a file name nobody passed. One case needs saying: a Type 1 program passed with -data has no file to look beside, so its AFM cannot be found by name — -metrics is then required and its absence is refused with TCLPDF FONT METRICS rather than reported as an unreadable .afm.

Which file to embed for a standard face. A document that has to be archivable may not leave a font unembedded, and that includes the fourteen standard faces — PDF/A makes no exception for them. Their outlines were never released, so an equivalent has to take their place. The table below maps each of the fourteen to the file that stands in for it: Adobe’s Type 1 and OpenType files, the URW Core 35, and what Windows ships. macOS ships the five families under the standard names themselves — the PostScript names inside Helvetica.ttc, Times.ttc, Courier.ttc and Symbol.ttf are the fourteen, only the display names spell the style out (Helvetica Bold Oblique, Times Bold Italic) — so the first column is the macOS column as well, with one exception: ZapfDingbats.ttf carries the PostScript name ZapfDingbatsITC.

PDF standard 14 / macOS Adobe Type 1 Adobe OpenType URW Core 35 Windows
Helvetica Helvetica.pfb HelveticaLTStd-Roman.otf NimbusSans-Regular.ttf Arial.ttf
Helvetica-Bold Helvetica-Bold.pfb HelveticaLTStd-Bold.otf NimbusSans-Bold.ttf Arialbd.ttf
Helvetica-Oblique Helvetica-Oblique.pfb HelveticaLTStd-Obl.otf NimbusSans-Oblique.ttf Ariali.ttf
Helvetica-BoldOblique Helvetica-BoldOblique.pfb HelveticaLTStd-BoldObl.otf NimbusSans-BoldOblique.ttf Arialbi.ttf
Times-Roman Times-Roman.pfb TimesLTStd-Roman.otf NimbusRoman-Regular.ttf Times.ttf
Times-Bold Times-Bold.pfb TimesLTStd-Bold.otf NimbusRoman-Bold.ttf Timesbd.ttf
Times-Italic Times-Italic.pfb TimesLTStd-Italic.otf NimbusRoman-Italic.ttf Timesi.ttf
Times-BoldItalic Times-BoldItalic.pfb TimesLTStd-BoldItalic.otf NimbusRoman-BoldItalic.ttf Timesbi.ttf
Courier Courier.pfb CourierStd.otf NimbusMonoPS-Regular.ttf Cour.ttf
Courier-Bold Courier-Bold.pfb CourierStd-Bold.otf NimbusMonoPS-Bold.ttf Courbd.ttf
Courier-Oblique Courier-Oblique.pfb CourierStd-Oblique.otf NimbusMonoPS-Italic.ttf Couri.ttf
Courier-BoldOblique Courier-BoldOblique.pfb CourierStd-BoldOblique.otf NimbusMonoPS-BoldItalic.ttf Courbi.ttf
Symbol Symbol.pfb SymbolStd.otf StandardSymbolsPS.ttf Symbol.ttf
ZapfDingbats ZapfDingbats.pfb ZapfDingbatsStd.otf D050000L.ttf —

A second free set: the Liberation fonts. Where a document need not match the standard fourteen metrically, any embeddable face will do; where it must — a form laid out for Helvetica, a table measured with Times — two free sets keep the measure, from two lineages. The URW Core 35 above are metric copies of the PostScript faces themselves (measured: 2592 of 2592 advances equal). Red Hat’s Liberation Sans, Serif and Mono (SIL Open Font License 1.1, https://github.com/liberationfonts/liberation-fonts; the copies used here sit under examples/assets/fonts/liberation-fonts/, release 2.1.5, four styles each) are metric copies of Arial, Times New Roman and Courier New, and those were drawn to the measure of Helvetica, Times and Courier bar a handful of symbols; measured over every WinAnsi position the AFM prices, at 1000/em, half a unit of tolerance (tests/font.test, font-13.1, holds the numbers):

Liberation stands in for advances equal differs at
Sans, four styles Helvetica, -Bold, -Oblique, -BoldOblique 211 of 216 each ¯ ± µ · ÷
Serif, four styles Times-Roman, -Bold, -Italic, -BoldItalic 211, 211, 212, 213 of 216 ¯ ± µ · ÷ (fewer in the italics)
Mono, four styles Courier, -Bold, -Oblique, -BoldOblique 216 of 216 each —

2555 of 2592 advances equal (216 per face, the count the URW figure uses); the 37 that differ are five symbols — macron, plus-minus, micro sign, middle dot, division sign — where Arial and Times New Roman never were Helvetica and Times. Running text sets to the same measure in URW and in Liberation alike; a form laid out on those five characters wants URW. Neither set is installed with the package; both travel in the source archive with their licences, and example 02.03 sets Liberation Sans beside the other families.

And at Google Fonts. The Liberation designs are there as well, under the names the licence reserves alongside Liberation — Arimo, Tinos and Cousine — and with them one face for the symbols; four families to look at, out of a collection that is a good source for everything that need not keep the measure:

Google Fonts stands in for why
Arimo Helvetica, Arial a sans drawn to the metrics of Arial — Liberation Sans
Tinos Times-Roman, Times New Roman a serif drawn to the metrics of Times New Roman — Liberation Serif
Cousine Courier, Courier New a monospace drawn to the metrics of Courier New — Liberation Mono
Noto Sans Symbols 2 Symbol, ZapfDingbats covers most of the same characters, at widths of its own

Noto Sans, Noto Serif and Noto Sans Mono, good faces otherwise, keep proportions of their own (measured: 0 of 191 Noto Sans advances equal Helvetica’s) and are not stand-ins. For Symbol and ZapfDingbats there is no one-to-one equivalent at Google Fonts; the URW StandardSymbolsPS and D050000L above are the metric matches, and font embed refuses them today for their symbol-only cmap, see the next paragraphs.

Three of those files can be embedded as they stand. The URW files are TrueType and go in unchanged; Adobe’s Type 1 files go in whole, provided their AFM is beside them; Adobe’s OpenType files go in whole as CFF. The macOS faces for Helvetica, Times and Courier are TrueType collections — several faces in one file — and -face picks the one that is meant, the first without it.

Two of the fourteen have no working substitute today. The URW files for Symbol and ZapfDingbats carry a (3,0) symbol cmap and no Unicode one, so font embed refuses them. The .otf files of the two embed and set the Latin positions with their glyphs (measured: 190 and 203 characters reachable), but a reader extracts Latin letters for them — use them for the page, not for copyable text; the .t1 files embed as well but reach only the glyphs whose names are WinAnsi names. Measured with font info characters, which counts byte positions rather than glyphs: 44 of the Symbol face over 43 different glyph names — the digits, the ASCII punctuation, ± × ÷ µ °, and € • … ƒ ¬ — and 2 of the Dingbats face over a single name, the space, which is to say nothing at all. The two counts differ because two byte positions may carry the same glyph, the space and the no-break space being that case in both files. The twelve text faces are unaffected and cover Helvetica, Times and Courier completely.

The URW faces are metric substitutes, and measured against the metrics this package ships: every one of the 2 592 advances of the twelve text faces matches the standard face it stands in for, across all mapped WinAnsi byte values. They are published under the SIL Open Font License 1.1, so they can be redistributed with a document workflow; Adobe’s own outlines cannot.

tclpdf does not install any font. Embedding one means having the file, and which file that is remains the caller’s choice — the package reads what it is given. The URW set is version 2.0 of the URW++ Core 35, kept at https://github.com/twardoch/urw-core35-fonts; distributions carry the same fonts as a package of their own, on Debian and its derivatives as fonts-urw-base35. The copies used to write the examples sit in the source archive under examples/assets/fonts/urw-core35-fonts, with the licence texts beside them.

doc font names
The aliases embedded so far.

doc font define alias ?-matrix {a b c d e f}? ?-ascent n? ?-bbox {x y w h}?
Creates a Type 3 font — one whose glyphs are drawn rather than read out of a file (ISO 32000-2, 9.6.4). Such a font has no font program: each glyph is a content stream in this document, built from the same calls that draw a page, so a glyph may be a filled shape, a gradient, a picture or anything else the package can put on paper. From the moment it exists the alias is a family like any other: font -family, textWidth, a paragraph and a table cell all take it, and the marks sit on the baseline, take the font size, and are copied and searched as the characters they stand for. -matrix is the font matrix, which maps glyph space to text space; its default {0.001 0 0 0.001 0 0} is the usual thousand-unit glyph space, and its horizontal scale has to be above zero. -ascent is the one number a caller has to remember: how far above the baseline the glyph script’s frame begins, in glyph units, four fifths of the em by default. There is no -descent, because nothing in this package asks a font how far it descends. -bbox states the font bounding box in the coordinates of the glyph script; without it four zeros are written, which Table 110 reads as “make no assumptions” — a box guessed from the ascent would be a claim about drawings this package never sees, and an inaccurate one leaves the result to the reader.

doc font glyph alias text -width w -script {…} ?-name g? ?-color own|text? ?-bbox {x y w h}?
Draws one glyph of a font made with font define. text is the character the glyph is set for — $doc text "☑" then draws it — and the character code, the glyph name and the ToUnicode entry all follow from it, so no two of them can disagree. It may also be a whole character sequence, and then the glyph stands for all of it: one drawing, one advance, one character code, and a ToUnicode entry carrying every code point back. That is what an emoji face needs — a family, a skin tone or a flag is one glyph made of several characters, and no one of those characters has a glyph of its own to fall back on — and it is what colorFont builds on. Which sequence a string holds is decided by the longest match at every position, so a font holding both an emoji and the family that begins with it draws the family where the family was written. The derived glyph name of a sequence is its component names joined by an underscore (u1F468_u200D_u1F469), the convention a real face names its ligature glyphs by; a name longer than the 127 bytes a PDF name holds is refused rather than shortened, and -name is the way past it. An empty text is refused: it names no character and would take a character code nothing could address. Inside -script the coordinates are glyph units, {0 0} is the top left of the frame and y grows downwards, exactly as on a page and inside a form; the baseline sits at the ascent, and the script runs in the caller’s frame, so a document held in a local variable is in reach. -width is the advance in glyph units and is required — it goes into /Widths and into the width operator, and it is what textWidth measures. -name overrides the derived glyph name (uni002B), which changes only how the file reads.

-color chooses the width operator, and the choice is not cosmetic. own, the default, writes d0 and lets the glyph bring its own colours — which is what every drawing method of this package sets. text writes d1, so the glyph describes only its shape and is painted in the colour of the text like a letter; the standard then ignores any colour the script sets, and -bbox becomes mandatory, because a d1 bounding box is binding: “If any marks fall outside this bounding box, the result is implementation-dependent.”

A -color text glyph carries no colour. ISO 32000-2, 9.6.4, Table 111 says a glyph description beginning with d1 “should not execute any operators that set the colour … any use of such operators shall be ignored” — the first half is the writer’s duty, the second the reader’s, and the readers do not agree: measured on 2026-08-26, poppler ignores a 0 g inside such a glyph and paints in the text colour, Quartz (QuickLook, Safari) executes it and paints black. Every drawing method of this package writes a colour with every figure, so the colour selectors — g, rg, k, cs, sc, scn and their stroking capitals — are struck out of the glyph stream together with their operands before it is written. What paints stays and paints in the text colour, which is what -color text asks for. sh is left alone: it paints its own colours and sets none, and both readers execute it. A -color own glyph (d0) is written exactly as the script drew it.

Every number option of font define and font glyph takes a finite number: -ascent NaN, -width NaN and a NaN inside -bbox are refused at the call (TCLPDF TYPE3 ARGUMENT, TCLPDF TYPE3 BBOX). NaN is a double to Tcl and every comparison with it is false, so such a value used to pass the range test and reach the FontMatrix arithmetic, the /FontBBox and the d0/d1 operand.

A Type 3 font is addressed by single bytes and holds at most 255 glyphs; a character it has no glyph for is refused with the usual TCLPDF FONT GLYPH error code, and -direction rtl is refused as it is for every face addressed through an encoding. Under PDF/A the question of embedding does not arise — the glyphs are the file — and a document with such a font passes A, U and B alike, PDF/UA included. Example 02.11 sets a checklist whose boxes are text.

doc colorFont alias ?path? ?-data bytes? -chars string ?-palette n? ?-face n? ?-strike ppem?
Builds a Type 3 font named alias out of the colour glyphs of a COLR face — version 0 or version 1 — one glyph per character or character sequence of -chars, and returns the alias so it can be handed straight to font -family. PDF has no colour font — the text operators paint an outline in one colour, whatever the face carries beside it, which is why such a face embeds blank and font embed refuses it — so the layers are read out of the face and drawn into glyph descriptions instead, and from there on the alias is a family like any other: font -family, textWidth, a paragraph, a table cell, /ToUnicode and all. path is the font file and follows the name, exactly as image embed takes them; -data replaces it for a face that never was a file — one built in memory, one out of a database, one out of an archive. -palette is the colour palette to draw the layers through, 0 by default; a face carries at least one and may carry several.

-chars takes text, and the package finds the sequences in it. An emoji face draws a family, a skin-tone variant and a flag as one glyph each, and that glyph has no character of its own: it is a GSUB ligature over several — 👨 + zero width joiner + 👩 + joiner + 👧 for the family, a base emoji plus a modifier for the skin tone, two regional indicators for a flag. So -chars is read as text rather than as a bag of characters: the ccmp feature of the face is applied to it, and every glyph that comes out is one glyph of the Type 3 font, carrying the whole sequence as its text. -chars "👨‍👩‍👧👍🏽🇩🇪" therefore gives three glyphs, and nobody has to take the string apart beforehand — which characters belong together is a property of the face, not of Unicode and not of the caller. ccmp and no other feature: compose-and-decompose is never a typographic option a caller switches on, while liga and its relatives are, and a colour font would then depend on a -ligatures nobody wrote in the call. Measured against hb-shape, the glyph picked for a sequence is the glyph HarfBuzz picks.

A sequence is one glyph with one width, everywhere: textWidth counts it once, a table cell reserves one advance for it, the line breaker treats it as one thing, and pdftotext gives the whole sequence back — joiners, modifiers and variation selectors included, because a bfchar may map one code to several UTF-16 units (9.10.3). The variation selectors are the one thing dropped from the glyph run and kept in the text: U+FE0F draws nothing and most faces have no glyph for it at all, so it rides along with the character in front of it — which is what a shaper does with such a default-ignorable, and what keeps the text complete. The boundary is the caller’s to watch: -chars is one string, so two emoji written side by side are offered to the face side by side and a face free to ligate them will — which is exactly what is wanted for a flag, since nothing but the order of two regional indicators says which flag it is. A caller who wants two glyphs where the face would make one asks twice, with two aliases.

A colour font holds the characters that were asked for and nothing else — not a space, not a letter — so it stands beside a real face rather than replacing one, and font -fallback is how the two meet. The chain is tried in order: a mark the first face has of its own comes from there, so a face of symbols that is to be seen in colour goes first and the words fall through behind it. A sequence is offered to the chain as one unit, and the longest registered sequence wins over the single characters it is made of — a font that draws 👨‍👩‍👧 as one glyph is a better answer than a face that draws the man and then has no glyph for the joiner. Registered is the whole of it: a unit longer than one character is offered only to a face that holds that very sequence as a glyph of its own, which is what font glyph and colorFont -chars put there, and the chain order decides among those as it decides everything else. A face is never asked whether it could spell a stretch of ordinary letters, so a standard family cannot jump the chain by answering yes to seven characters at once. Where no face in the chain holds sequences the chain behaves exactly as it always did, one character at a time and strictly in order.

A fourth kind of colour face: pictures. An sbix face keeps a PNG file per glyph and per size rather than outlines — Apple Color Emoji is 192 MB of them, at nine sizes from 20 to 160 pixels to the em — and colorFont takes such a face as it takes a COLR one, without the caller saying which is which: the glyph then becomes an image XObject placed by its glyph description, with the alpha channel of the PNG as the soft mask PDF needs for it. Everything the rest of this entry says holds unchanged — one glyph per sequence, one advance, one /ToUnicode entry — and what differs is what a picture is: it is sharp up to its own resolution and no further, and font info reports how many glyphs of the font are pictures under bitmaps. A character the face has no picture for is drawn from its own outline, exactly as a character the COLR table says nothing about is; Apple Color Emoji has 42 such characters, the digits and the keycap bases, and the female sign among them. CBDT/CBLC, the other bitmap format, is not read: it is a different table pair with its own index format, and no face measured here uses it.

-strike names the pixel size to draw from, in pixels to the em, and the default is the largest strike the face carries. The reason nothing cleverer is done is that nothing at build time knows the point size: a Type 3 font is built once and set at every size the document uses — 10 pt in a table cell and 36 pt in a heading — so a strike chosen for one of them is wrong for the other, and of the two ways to be wrong only one is visible. What the default costs is file size, and -strike is how it is traded: measured over 61 emoji, the 160-pixel strike comes to 900 KB of picture and the 64-pixel strike to 260 KB. A size the face has not got is refused with the sizes it has in the message (TCLPDF SBIX STRIKE); -strike on a COLR face is refused rather than ignored (TCLPDF COLORFONT STRIKE), as -palette is on a bitmap face (TCLPDF COLORFONT PALETTE) — a face draws its colours one way or the other and an option belonging to the other road has nothing to act on.

-face names one face of a TrueType collection, and 0 — the default — is what a file holding a single face is. A .ttc is a directory of faces that share their tables: Apple’s two faces name the same twenty-one byte ranges, one of them 191 MB, which is why the file holds them once. Naming a face makes it a font, and the package rebuilds that face as a standalone sfnt before reading it, so nothing downstream knows it came out of a collection. A number the file has not got is refused with the count in the message (TCLPDF FONT FACE).

A face of this size is never read whole. colorFont opens the file, reads its table directory and everything under a megabyte, and takes the PNGs it needs out of it by range; the bitmap tables stay in the file. Measured on Apple Color Emoji, 192 123 488 bytes: 0.04 s and under 30 MB of memory to open the face, 0.7 s and 50 MB to build a font of 61 emoji — against 13.2 s and 4.3 GB for a naive read. The file handle is given back before colorFont returns, which is why the glyphs carry their pictures rather than a reference to the file.

Where the sequences come from is not the same question in every face. An OpenType face forms them with the ccmp feature of its GSUB table. An Apple face has no GSUB at all: what joins a thumb to a skin tone there is morx, the extended glyph metamorphosis table — a chain of finite state machines, and in Apple Color Emoji 475 KB of them in 25 subtables of which 16 are on by default. colorFont applies it where the face carries one and no GSUB, and what comes out is compared with hb-shape as the ccmp road is: measured 2026-08-27 over 3196 sequences the two emoji faces have in common, tclpdf picks the glyphs HarfBuzz picks in every one of them. And where the face stacks them, it stacks them where HarfBuzz does. Apple Color Emoji draws 968 of its 997 multi-person sequences — the couple with a heart, the kiss, the handshake, with and without skin tone — as two glyphs, and binds the second to the first with a GPOS mark lookup at an offset of one em. colorFont reads that lookup and folds the bound glyph into the Type 3 glyph of its base: one drawing, one advance, the characters of both halves together. Without it the halves stood side by side, the sequence was two ems wide, and textWidth "👩‍❤️‍💋‍👨" answered 80 points at 40 where hb-shape says 40. Only the subtables a chain has on by default are applied, which is what HarfBuzz does when no feature is requested and what every caller of this package wants; there is no way to ask for an AAT feature, and none is offered.

Three kinds of glyph, and the call does not distinguish them. A character whose glyph the face describes with a version 1 paint graph is drawn from that graph; one described by the older version 0 layer records is drawn from those; and one the COLR table says nothing about is drawn from its own outline, in the colour of the text, like a letter. A face may use both halves of the table at once — the standard says so explicitly — and the version 1 description wins where a glyph has both.

What version 1 adds is a graph rather than a list: the leaves are solid colours and linear, radial and sweep gradients, and the branches clip a shape, transform a sub-graph, re-use another colour glyph, or composite two sub-graphs in one of twenty-eight modes. All of it is translated rather than approximated where PDF has a spelling for it — a gradient becomes a shading painted through a clipping path (never a shading pattern, which is anchored to the page and would stay put while the glyph moved); a colour line that fades is painted as nested bands of constant alpha, one clip and one ca per 1/64 step, and Source In as a clip path or a constant alpha where the masking side allows it, since a PDF shading has no alpha channel; a luminosity or alpha soft mask remains only where no plainer form exists. Fifteen of the composite modes are PDF blend modes under other names and go into an isolated transparency group.

Four numbers bound what the banding costs and none of them is reachable by a face written for a raster: a repeat or reflect colour line is unfolded along its own axis for at most 64 periods and 512 stops, and beyond that the middle of the range is written and the ends pad; the alpha staircase steps in 1/64, so no fill is more than half of that from the alpha the font asked for; a gradient needing more than 256 clip intervals goes back to the soft mask; and a radial gradient is banded only where its two circles nest, which 7894 of Noto Color Emoji’s 7894 varying-alpha radials do. A colour line whose stops all sit on one offset is a step rather than a gradient — the first stop below the offset, the last one at and above it, which is what the standard asks for — and it is written as a shading whose axis is a ten-thousandth of the box being painted, so the two colours meet inside one pixel of any raster. Two constructions have no PDF spelling at all: an angular gradient, which becomes a fan of Gouraud triangles at four degrees a wedge, and the repeat and reflect extend modes, whose colour line is copied along its own axis until it covers what is painted (PDF’s /Extend means pad and nothing else). Variable fonts are drawn at their default instance: the variable paint formats carry the same fields as their twins plus an index into an ItemVariationStore, and at the default instance every delta is zero, so reading past that index is exact rather than approximate.

What is refused, and why by name. A face without COLR and CPAL is not a colour font (TCLPDF COLORFONT TABLES); a character the face has no glyph for is refused rather than drawn blank (TCLPDF COLORFONT CHAR); a glyph that would draw nothing at all — layers that are all empty, a paint graph whose every shape is an outline the face has not got, or a plain glyph with an empty outline — is the blank-page trap of font embed one level down and goes the same way (TCLPDF COLORFONT EMPTY); a Type 3 font is addressed by single bytes and holds at most 255 glyphs (ISO 32000-2, 9.6.5.3), so the 256th glyph is an error and not a silent truncation (TCLPDF COLORFONT LIMIT) — counted in glyphs and not in characters, since a sequence of seven characters is one glyph, and therefore counted after the face’s ccmp has been applied and before the font is defined, since it is the face that says where the sequences are, and the answer for a document that needs more is a second colorFont call with a second alias, both named in -fallback; a face whose ccmp takes a character apart into several glyphs cannot be built into a Type 3 font at all, because such a glyph is one drawing with one advance and cannot be two (TCLPDF COLORFONT SPLIT); and a shape that is a composite glyph is refused (TCLPDF COLORFONT COMPOSITE) — measured over 33 179 layers of one real colour face and 129 823 shapes of another, none is one. Three refusals belong to version 1 alone: a colour glyph with no clip box that paints outside every shape in it has no size a page could reserve for it and “must not be rendered” (TCLPDF COLORFONT UNBOUNDED); the Plus composite mode adds the two colours and clamps the sum, which none of PDF’s fifteen blend modes does and no combination of them produces (TCLPDF COLORFONT COMPOSITE); three more composite modes go the same way over a side they cannot be written exactly over — Source Atop, Destination Atop and XOR keep the masking side’s own alpha in the result (W3C Compositing and Blending 1, 9.1, which the standard names as the definition of the modes), while PDF composites source over backdrop and multiplies the two alphas instead, the same picture only where that side’s alpha is nought or one: where the masking side is opaque inside its outlines, or one colour at one alpha inside one outline, the formula is written exactly, as a clip or as a group at a constant ca, and anything else is refused (TCLPDF COLORFONT COMPOSITE) rather than drawn a shade wrong with nothing reporting it — measured, Noto Color Emoji uses two composite modes in its 578 PaintComposite tables, Source In and Soft Light, and none of these three; and a gradient stop whose palette index is the 0xFFFF sentinel names a colour that is chosen when the glyph is drawn, while a shading’s colours stand in the file (TCLPDF COLORFONT FOREGROUND). What the COLR reader refuses on top of all that — an unknown version, an empty table, a truncated one, a cycle in the paint graph — keeps its own TCLPDF COLR codes and is passed through unchanged.

Two of the refusals go against the letter of the standard, and on purpose. An unknown paint format “should be ignored” by a reader, and where a composite mode is “an unrecognized value … COMPOSITE_CLEAR must be used” — and Clear paints nothing. Both rules say: draw the rest of the glyph and leave that piece out. This package refuses instead, by name (TCLPDF COLR PAINT and TCLPDF COLR COMPOSITE), because a mark that comes out missing a piece is valid, printable and unreported, which is the one outcome every refusal in this section exists to prevent. Neither can be reached by a face written to the present edition: all 32 paint formats and all 28 composite modes of ISO/IEC 14496-22 are known here, so it takes a COLR table from a later minor version — and a caller who would rather carry on with a piece missing catches both with trap {TCLPDF COLR}.

A palette index of 0xFFFF in a solid fill is a sentinel rather than an index — that fill takes the colour of the text, so a two-colour mark can follow font -color with one half and keep its brand colour with the other. A palette entry may carry an alpha byte; a translucent fill then costs an ExtGState, an opaque one costs nothing. Under PDF/A parts 2 and 3 the transparency all this needs is admissible and measured to pass; part 1 forbids it and is refused by pdfa anyway. Example 02.14 builds a two-colour version 0 face and sets it in running text; 02.17 builds a version 1 face and shows one badge per paint kind.

doc font info alias
What the file says about itself: format, family, postScript, glyphs, unitsPerEm, characters, ascender, descender, lineGap, capHeight, xHeight, vertical, variable, axes, masks, bitmaps, and the embedding permission as fsType and permission. Every kind of font answers every key; a format that states no value answers the empty string for it. postScript is the name the face is embedded under: for an instance of a variable font (-instance, -axes) that is the instance’s PostScript name — Roboto-Bold, Roboto_620wght — not the default the file itself is named for. axes is a list of {tag min default max}, one per axis of a variable font, and empty for a face that has none. For an instance of a variable font ascender, descender, lineGap, capHeight and xHeight are the instance’s numbers — the file’s tables plus its MVAR deltas — as postScript is already the instance’s name. permission also names a no-subsetting (fsType bit 8) or bitmap-only (bit 9) restriction, and answers unknown for a face without an OS/2 table.

bitmaps is the number of glyphs of a drawn colour face that are pictures rather than paths — an sbix face’s PNGs, placed as image XObjects by their glyph descriptions (see colorFont) — and 0 for every other kind of font, a COLR face included. It is the one thing about a drawn colour font that decides how it behaves under magnification: a path is sharp at every size, a picture up to its own resolution and no further, and -strike is what that number is traded against. A font whose glyphs and bitmaps are equal is pictures throughout; one where they differ carries ordinary outlines beside them, which is what a bitmap face does for the characters it draws in one colour.

masks is the number of glyphs of a drawn colour face that needed a soft mask (see colorFont) — the one construction inside a Type 3 glyph that two readers of seven place wrongly — and 0 for every other kind of font. It counts the masks of the graphics state, the /SMask of an ExtGState; the alpha channel of a picture is an image soft mask, an entry of the image dictionary rather than of the state, and is not counted, so an sbix face whose every glyph is a translucent PNG answers 0 here and its glyph count under bitmaps. A caller who has to be sure a document reads the same everywhere checks it against 0.

fsType is reported, and one bit of it is obeyed. Bit 1 (“restricted licence embedding”) is a licence term and is not enforced: no reader and no validator enforces it either, and a package that silently declines to embed a face the user holds a licence for is the more harmful of the two errors. Bit 8 (“the font may not be subsetted prior to embedding”) is about what may be done to the program rather than about the licence, and it is obeyed: such a face is embedded whole, as if -subset 0 had been written. A caller who means it otherwise writes -subset 1 and is obeyed.

doc text string ?-at {x y}? ?-width w? ?-align a? ?-direction ltr|rtl|ttb? ?-runs 0|1? ?-markup tags|markdown? …
Draws text. Without -width this is one line, and -align refers to the given point: left starts there, right ends there, center — centre is accepted too — is centred on it; the call returns nothing. Without -width the string is one line: a line feed in it is refused; a paragraph is asked for with -width. With -width the string is broken into a paragraph of that width, -align justify becomes available, and the call returns the y coordinate below the last line, so the next block can continue there.

-anchor chooses what the y coordinate means: baseline (the default), top, middle or bottom; any other value is refused. The three that are not baseline measure against the face’s line box — its ascender above the baseline and its descender below — and not against the ink the string happens to carry: top puts the ascender at y, bottom the descender, middle centres the box on it. That is what makes Text and Type line up, which is the whole point of an anchor; measuring the ink instead would move a line depending on whether it has a descender in it. A Type 3 face has no descender to read — its glyphs are content streams, and the only vertical measure it carries is the box font type3 was given — so its descent counts as zero and bottom is its baseline. On a paragraph (-width) the anchor applies to the first line, which is the line the y coordinate is about.

-fit {w h} puts one line into a box, which is the recipe examples/01.06 used to spell out before there was an option for it: measure, work out a factor, set the size. examples/01.15 is the option itself, with the same line in four boxes, what -spacing does to the arithmetic, and the refusals below. It squeezes the letters first — up to -shrinkLimit, 85 per cent by default — and only lowers the size for what is still missing, because a face narrowed by a few per cent is barely visible in a single line while a smaller size is visible at once beside its neighbours. There is no trial-and-error loop, and the measurement is made with the same options the line is drawn with: where nothing absolute is added, the width scales with the size and one measurement settles it; where -spacing or -wordSpacing is in force, those are absolute point values that do not shrink with the size, so a second measurement without them separates the flat part and the size is solved for rather than divided out. Asked together with -direction ttb it is refused (TCLPDF TEXT VERTICAL fit): fitting measures the width of a line and narrows the letters with Tz, and in a column neither is what the caller means. -shrinkLimit 100 forbids narrowing altogether and takes everything out of the size. A box too small for anything this package can write into it is refused rather than filled with a size no reader can show (TCLPDF TEXT FIT BOX). The floor is one point: a size below it is written into the file, is valid, and is invisible on every screen and every printer, so a box that leaves less than that came out of an arithmetic that went wrong rather than from a wish. A box that is not two finite lengths above zero is refused by the check every fitted placement shares, as TCLPDF FIT BOX; a length beyond what a PDF real can carry is turned away one step earlier, by the shared length check, as TCLPDF OPTION NUMBER. examples/01.15 prints exactly this refusal. The height is answered first and separately: where the line’s box is taller than the box given, the size comes down by that ratio, and the width is then worked out against the new size. One measurement is enough — the width scales linearly with the size — so there is no trial-and-error loop. -fit and -width are refused together (TCLPDF TEXT FIT WIDTH): one puts a single line into a rectangle, the other breaks a paragraph into as many lines as it takes, and they do not mean the same thing by it. There is no -align/-valign here, since -align on a line already means the typographic alignment. -rotate turns the text about -at. All font options are accepted per call without changing the state; a -size or -stretch of zero or less is refused, here and in font, rather than making the text vanish. -width must be a positive number.

-direction is ltr or rtl and says which way the line runs. It is an option of the line, not of the font state: the same face sets a right-to-left line and a left-to-right heading beside it, so font does not take it. Under rtl the glyph run is turned round before it is written — with a run of digits keeping its own order inside it and a paired bracket drawn mirrored — and -align is mirrored with it: left means the edge the line starts at in reading order, which is the right hand one, so -at marks the right edge of a line that is otherwise unaligned. center and justify mean the same thing either way. A line that also holds strongly left-to-right text is refused rather than set backwards, and so is rtl on one of the standard fourteen faces or an embedded Type 1 face: those are addressed through WinAnsiEncoding, which has no right-to-left letter, so the option could only have done nothing. Which scripts this makes drawable, and what the two rules above do exactly, is described under “Writing systems” below. ttb sets the line downwards, in the vertical writing mode of ISO 32000-2, 9.7.4.3. It is one column per call: -at is where the column begins, -align says where that point falls on the column’s own axis — left begins there, right ends there, center is centred on it — and the column is centred across -at without anything being written for it, because the glyph’s position vector already puts its x at half the horizontal advance. textWidth answers the extent along the line, which for ttb is its length down the page and not a width across it; -spacing opens the column as it opens a line, and -stretch does not, because horizontal scaling acts on the horizontal displacement only (9.4.4). Pair kerning is off in a vertical line: the pairs in GPOS kern are horizontal, and a face that kerns vertically names the feature vkrn, which tclpdf does not read. -anchor top is refused, being a second name for -align left here; so are -width and everything the paragraph road carries with it, textPath and leader, both because a vertical advance accumulates inside one show operator and those roads write one per cluster or per line — the option would have been accepted and drawn a horizontal line. And so is ttb on one of the standard fourteen or an embedded Type 1 or Type 3 face, for the same reason rtl is: the writing mode lives in the Type 0 font’s CMap and those are addressed through an encoding.

-height h limits the block. What fits is drawn and the return value becomes a dictionary with y and rest — the text that did not fit, ready to be set in the next column or on the next page. rest is the tail of the string as it was given, from the first character that was not set: soft hyphens are still soft, a word broken by character is still one word, and the paragraph breaks are where they were. y is where the next line’s baseline would sit — the last baseline drawn plus one leading, which is the same number the call answers without -height — and the paragraph spacing of a paragraph held back is not counted. Without -height the return value is the y coordinate as before — and without -height a paragraph knows nothing about the page: it runs on below the type area and below the page if it is long enough, and only the y it returns says so.

-height max is the height that is left: from -at down to the bottom of the type area of the current page (see page typeArea), less the ascent of an -anchor top block, so that the block ends inside the area rather than one line under it. The answer is the same dictionary — y is the last visible line, rest the text for the next page — and a block placed under the area draws nothing and hands everything back, answering the y it was given. It cannot be combined with -rotate: a turned block does not run down the page.

-paginate bool keeps the loop that -height leaves to the caller: what fits goes on this page, a page is added with the document defaults (as page add without options would), the text goes on from the top of the type area of the new page — hanging from it as with -anchor top, whatever the anchor of the first block was — and so on until nothing is left. Every page it adds fires pageAdded, so a running head hung on that event lands on every continuation page; -avoid holds on the first page only, its shapes being positions on that page. A paragraph broken by the page keeps its -firstIndent for paragraphs, not for pages: the first line on a continuation page is indented only when it opens a paragraph. The return value is {y rest page column} — y under the last line, rest always empty (it is there so that a caller reading -height’s answer reads this one the same way), page the index of the page the text ended on, column the column it ended in, 0 without -columns. In a tagged document the run stays one paragraph, with a marked-content reference on every page it touches (ISO 32000-1, 14.7.4.2) — the shape a table breaking over pages has, one Table with children on every page — and an artifact is declared again on every page. Needs -width, refuses -rotate and a numeric -height (the page is filled to the bottom of the area, which is -height max), and refuses a page it added itself when not one line fits into it rather than paginating for ever; on the page the block is placed on, an -at below the last line means the text begins on the next page.

-columns n sets the block in n columns side by side inside -width, -gutter g apart (five millimetres unless said otherwise), each column filled to the bottom of the type area before the next begins, and the page added only after the last one — so it wants -height max or -paginate, which are what fill a column. With -height max alone the columns of the current page are filled and the rest is handed back with column = the last one; with -paginate the columns of every page. -balance bool evens out the page the text ends on: instead of full columns and a short last one, the columns are cut to one height, found by measuring the block without drawing it and grown a line at a time until the last column takes the last line — so they end on a baseline together, up to n−1 lines apart when the line count does not divide. It needs -columns of 2 or more and cannot be combined with -avoid, whose shapes would make each column break differently from the measurement. A page that is filled anyway has nothing to balance and is not.

-indent, -indentRight and -firstIndent narrow the column; a negative first indent hangs the opening line out to the left, which is how a numbered clause is set. They have to leave a band inside the column: indents that take the whole width away are refused at the call (TCLPDF TEXT INDENT width), the way -columns with too wide a -gutter is, since a line with no band left is set one character at a time and runs off the page. A negative -firstIndent is a hanging indent and stays welcome. -paragraphSpacing adds room between paragraphs, on top of the leading, and is a distance of 0 or more: a negative one would set the next paragraph over the one before it.

-avoid takes a list of shapes the text runs around: {rect {x y} {w h}} and {circle {x y} r}. Each line is narrowed by whatever reaches into it and set in the widest free segment, so a circle is followed by its outline rather than by a box around it. A line whose free segment is narrower than the widest character of the text is left empty and the text goes on below the shape — a shape wider than the column pushes the text down rather than letting it run through one letter at a time. The band a shape leaves free is looked for down to the foot of the page and no further: where every line down to the bottom edge is covered, the search stops there instead of walking on for ever, and what happens then is what happens to any block that outgrows its sheet — without -height the lines are set on below the page and only the y answered says so, and with -height max or -paginate the whole text comes back as the rest, to be drawn on the next page. Until 2026-08-27 a shape as wide as a PDF real can measure, or an -avoidMargin past that range, meant the call did not return at all. The indents apply as they do without shapes, and the shapes are measured against the lines where they actually stand — with -anchor top the first baseline sits one ascender below -at, and a shape starting on that edge narrows the first line already. The shapes are not drawn — that is a separate call, and the text can just as well keep clear of something invisible. Where the shapes leave no line on the page at all, nothing is drawn and nothing is marked: with -paginate the text begins on the next page, with -height max the whole text comes back as rest; in a tagged document no empty paragraph element is written for such a page.

-avoidMargin d is checked whether or not shapes were given, and it is a distance of 0 or more: below zero it would widen the band into the shape, which is the opposite of holding the text off it. It holds the text off every avoided shape by that distance; a shape may state one of its own as a fourth element ({circle {x y} r 7}), which then wins. Without it the words touch the picture, which reads as a mistake however exact the geometry is.

-tag type names what the text is in a tagged document — P unless said otherwise, H1 for a heading, Artifact for what is outside the tree; -expansion text marks the string as an abbreviation and gives its expanded form. Both are described under “Structure and accessibility” below.

Soft hyphens are honoured. U+00AD is not a character but a permission — this word may be broken here. Where the breaker takes the offer, a real hyphen is set at the end of the line; everywhere else the mark stays invisible, in the drawing and in the measurement alike, so the same string can be set in any width. What it costs is named below under Hyphenation: extracting such a line yields the hyphen as well, unless the document is tagged, and a standard face is not refused for carrying the mark.

-hyphenate 0|1|tag breaks the words the offers of the text do not cover, using the patterns of the language named. 0 is the default and means what it has always meant — no word is broken except by the character fallback; 1 uses the language the document declares with language; anything else is a language tag such as de-AT. Only the two literal values 0 and 1 are the switch, and every other value is a tag: no is a Tcl false and the RFC 3066 tag for Norwegian, so a boolean reading would have turned hyphenation off for the one language whose name says otherwise. The patterns have to be loaded first with ::tclpdf::hyphenate load, and a language that is not loaded is refused by name — TCLPDF HYPHENATE LANGUAGE — rather than set unhyphenated in silence, because a document that is quietly not what was asked for is worse than one that stops. Accepted by textLines and textHeight as well, so a block is measured the way it will be set. A table takes it as a style key rather than as an option — {hyphenate de-DE} in -style, in a section style, in a column description or in a cell’s own style — because a table can hold columns in different languages; see there.

-breakHyphen 0|1 says that the - this line ends on is a break the caller made, not a character of the text — for whoever broke his lines himself and draws them one at a time. In a tagged document the hyphen is then shown inside a Span with an empty ActualText, exactly as -width does it for the lines it breaks itself, so extracting the line gives the word back whole (ISO 32000-1, 14.8.2.6); an untagged document is written as it always was. The default is 0. It is refused where it cannot be true: together with -width, because there the line breaker decides per line and a caller could only say it for all of them, and on a string with no hyphen in it, because there is nothing to call a break. The one exception, since 1.6, is a line of runs: -runs 1 and -markup need -width because a run is a thing of a paragraph, so there the two may stand together, though not beside -paginate or -columns — and the string then has to break into exactly one line at that width, or it is refused (TCLPDF TEXT BREAKHYPHEN lines); a table draws the lines of a cell of runs this way. textLines and textHeight accept it and ignore it, so one option list still serves a measurement and a drawing. The line keeps the width textWidth measured for it: the kerning pairs that straddle the hyphen are put back where the split lost them, which a caller who cut the string himself and made two text calls could not do. A table sets it for its own cells and needs nothing said. Which lines of a paragraph carry such a hyphen is asked for with textLines -hyphens 1, which is the other half of this.

-runs 0|1 — with 1 the string is not a string but a list of {text options} pairs, and the paragraph is set as runs: a bold word, an italic name, an underlined term, a link, in the middle of flowing text, with the line breaker running over all of it. Every piece is measured in the face it is set in — a bold word is wider than the same word regular — and drawn with its own Tf; a word may cross a run boundary and is still one word to the breaker and to the hyphenator. The size stays: a run changes the face and what is drawn over or under it, never the size, the colour or the spacing — those belong to the paragraph, which is what keeps every line one height. The options of a run: style bold, italic or both (oblique reads as italic), laid over the block’s own -style; underline 0|1 and strike 0|1, a stroke in the text’s colour whose place and weight come from the face — the post and OS/2 tables of an embedded face, the AFM’s UnderlinePosition of a standard one — and, where the face states nothing, a tenth of the size below the baseline and a twentieth thick for the underline, a quarter above for the strikeout; url address, a link annotation over the run on every line it spans, underlined unless the run says underline 0 — the annotation only: a Link structure element and the Contents PDF/UA asks of every link annotation are not written for a run, so a tagged document with a url run cannot claim ua (the write names the annotations, 7.18.5) and is set with link and structure Link where it has to; heading 1|2|3, which makes the pair a paragraph of its own rather than a run — set at 1.6, 1.3 and 1.15 times the block’s size, bold, with a leading grown by the same factor, and never the last line of a page or column: where a height limit would cut right under it, the heading goes with the rest; in a tagged document it is an H1, H2 or H3 element and the paragraphs around it keep the block’s -tag. item bullet|number, which makes the pair a list item: a paragraph of its own, like a heading, indented by 1.6 times the block’s size on every line of it — or by the widest label of its list plus half the size, where a number from 10. on needs more — with a label in front of its first line, right-aligned and ending half the size before the text, in the block’s face, size and colour: • for a bullet, 1., 2., 3. for numbers, which the package counts. Consecutive item paragraphs of one kind are one list; a paragraph without the option, a heading, an empty paragraph or the other kind ends it and the next list counts from 1 again. A list has one level; there is no nesting. In a tagged document a list is an L element — ListNumbering Disc or Decimal — holding an LI per item with the label as Lbl and the text as LBody, which is what ISO 32000-2 asks for and what PDF/UA checks. The rest of a height-limited block that cuts inside an item comes back with two options on its first pair that only the rest writes and a caller may write too: start N, the number the next numbered item continues with, and continued 1, which sets the pair without a label and without the paragraph’s first indent, the item’s indent kept. A run with no options is {}. strong and em are accepted as 1 beside style and carried for the tagged document; they set nothing that style does not. textLines and textHeight take the same list, and textLines answers every line as such a list — the pieces of the line with the options they were set with —, and the rest of a height-limited block comes back in the same shape, so that it can be set again with -runs 1 and the same faces. What is refused by name, all under TCLPDF TEXT RUNS: a list with an odd number of elements, an unknown run option, a style word other than the three, a heading outside 1 to 3, runs without -width, in a block set -direction rtl or ttb, or beside -avoid — the lines of a block that flows round a shape are broken again per column, and the runs would not follow —, a heading or a list beside -paginate or -columns in a tagged document (the marks of those roads are per page, a heading’s and a list’s per paragraph — untagged, both roads take lists), or beside -expansion, item beside heading in one pair, start on anything but a numbered item, continued on anything but an item, a face without the bullet glyph U+2022 or without the digits and the full stop a number needs (BULLET, NUMBER, naming the face), a list indent that leaves no band inside the column (TCLPDF TEXT INDENT), and a style on an embedded alias that no font family covers, because an embedded alias is one face and a bold run set regular in silence is the defect this whole road exists to avoid. Kerning ends at a run boundary — bold and regular have no pair table between them in any face — and a run in a rotated block is refused only where it carries a link, whose rectangle lies square to the page.

-markup tags reads the string as tagged text and sets it as the runs the tags name: <b>, <i>, <u>, <s>, <em>, <strong>, <a href="…">, <h1> to <h3> and <p> — eleven names, in any case, and nothing else between angle brackets is a tag. A < in front of a space, a digit, = or the end is text (a < b, < 5 %); a < in front of a letter or / has to open a known tag, or the call is refused with the name and the position (TCLPDF TEXT MARKUP UNKNOWN). One escape: &lt; for a literal < in front of a letter, with &gt; and &amp; beside it for symmetry; no other entity is read, and & is text. Nesting is strict — a closing tag closes the innermost open one, an open tag at the end is refused (NESTING, OPEN), a paragraph tag inside a run or a heading inside a paragraph is NESTING too; a tag repeated inside itself is allowed and does nothing. <a> takes href="…" in double quotes and nothing else takes an attribute (HREF, ATTRIBUTE). <p>…</p> and the headings are paragraphs, text between them is a paragraph of its own, and a line feed in the text stays a paragraph break exactly as without markup, so that a caller who wants nothing but <b> changes nothing else. Lists are <ul>…</ul> and <ol>…</ol> holding <li>…</li> and nothing else: each <li> is an item paragraph (see item under -runs) that may carry the inline tags, white space directly inside <ul>/<ol> is swallowed, and text or another tag there, an <li> outside a list, a list inside a run or a heading, a <p> or a heading inside an <li>, and a line feed inside an <li> — an item is one paragraph — are NESTING. What follows </ul> starts a new paragraph as after </p>; a line feed right after it is a paragraph of its own. <b> and <strong>, <i> and <em> set alike; what differs is what a tagged document is told. Tags rather than Markdown, deliberately: the vocabulary is closed, so the one character to watch is a < in front of a letter, where *, _, # and [ occur in every text; opening and closing are explicit, so a fault is a refusal with a position and not a guess; the names are the structure vocabulary of ISO 32000-2, 14.8.4; and [Text](url) in a double-quoted Tcl string is a command substitution. -markup takes tags or markdown (TCLPDF TEXT MARKUP KIND), excludes -runs 1 (MARKUP RUNS) — a string is written either as tags or as a list —, and the translator is a plain procedure, ::tclpdf::markup::parse, which answers the list a tagged string becomes; package require tclpdf::markup names it. A string without a single tag sets the bytes the plain block sets.

-markup markdown reads the string as Markdown and sets the same runs from the second notation: **bold** is <strong>, *italic* is <em>, ***both*** both, ~~struck~~ is <s>, [text](url) is <a href="url">, and #, ## and ### at the start of the string or right after a line feed open a heading that reaches to the next line feed. That is the whole vocabulary — there is no underline in Markdown, which is what -markup tags is for — and everything else Markdown knows (lists, code, images, quotes, tables, autolinks) is text, as is _, always, so that snake_case is no trap. The notation is read as strictly as the tags, deliberately against CommonMark: a *, **, ~~ or [ that is still open at the end of the text is refused (TCLPDF TEXT MARKDOWN OPEN), a closing delimiter that does not close the innermost open run is NESTING, [text] without (url) or with an empty address is LINK, and four or more # are HEADING — each with the position — because a run that quietly falls back to text because of one forgotten * is the silence this package refuses everywhere else. A *, ** or ~~ in front of a space or the end while no run is open is text (5 * 3), the counterpart of the < rule above, and so are four or more * in a row, a single ~, # without a space after it, ![alt](src) (an image is not a link) and a ] outside a link; *** opens <em> outside and <strong> inside, as CommonMark reads it. A [ in front of a letter opens a link, so [1] and [sic] in running text are refused as LINK and are written \[1]; the address reaches to the first ) and may hold no white space, which is where a Markdown title would stand. Backticks are text and what stands between them is read like any other text — there is no code span. A backslash in front of *, ~, [, ], # or \ writes that character, and in front of anything else is text. A line feed stays a paragraph break as without markup. Lists: a line that begins, at the start of the string or right after a line feed, with -, * or + is a bullet item, with one or two digits, a full stop and a space (1., 12.) a numbered item — the number in the source does not matter, the package counts, so 1. on every line is fine; the item ends at the line feed, consecutive items of one kind are one list, and a line beginning with -, *, + or 1. without the space, or not in column one, is text — there is no nesting by indentation, leading spaces belong to the item’s text. A date such as 12. March at the start of a line is written 12\. March, so the backslash also covers -, + and .. The two notations translate into one list and share the step that emits it, so the same paragraph written both ways sets the same bytes; the translator is ::tclpdf::markdown::parse, named by package require tclpdf::markdown.

Where a line breaks. A line breaks at ASCII white space — the space, a tab, a carriage return; a tab or a stray CR is set as the space that joins two words, which is the line breaker’s doing and therefore happens only where there is one: text -width and everything built on it. A single line has no breaker in front of it, so text "a\tb" without -width, and textWidth of the same string, reach the face with the character as it stands and are refused by a face that has no glyph for U+0009 — which the standard fourteen have not: TCLPDF FONT GLYPH U+0009 — and after the spaces UAX #14 lets a line break at: the Ogham space mark U+1680, en quad to hair space (U+2000–U+2006, U+2008–U+200A), the medium mathematical space U+205F and the ideographic space U+3000 (class BA), and the zero width space U+200B (class ZW). Where a line ends on one of these the character is dropped like a space; inside a line it is a character of the text — measured and set with its own glyph width, refused by a face that has no glyph for it, the standard fourteen among them — and never stretched. The zero width space is the exception: like the soft hyphen it never reaches a font, so it breaks where it stands, shows nothing and is set by every face. The byte order mark U+FEFF is dropped the same way — nothing to draw, no glyph asked of the face, by the standard fourteen as by an embedded face — instead of being refused as an Arabic mark or as a character outside WinAnsiEncoding. The no-break spaces U+00A0, U+2007 and U+202F are glue (class GL): the breaker never breaks at them, so 12 € stays on one line with its unit. Every other space of Unicode — U+2028, U+2029 and U+0085 among them — is a character of the word it stands in. Justification distributes the gap over the U+0020 characters of the line only, because that is what word spacing reaches (ISO 32000-1 9.3.3): in a justified line a no-break space keeps the width of a space and a thin space stays thin while the word spaces around them grow. The rest a height limit hands back is the tail of the string, so a thin space inside it is still a thin space. Example 01.10 sets the same amounts with a no-break and with a thin space side by side.

Hyphenation. Two things can offer a break inside a word: the soft hyphens the text already carries, and the patterns of tclpdf::hyphenate where -hyphenate asked for them. Where a word carries marks of its own those win outright — it is one source or the other, never both, because whoever wrote the marks knew that word and mixing the two would produce breaks neither of them asked for. The hyphen that appears at the break is a real one, U+002D, so the line ends the way a reader expects; in a tagged document it sits in a Span with an empty ActualText, which is how ISO 32000-1, 14.8.2.6 says to tell a reader that the character is not part of the text, so extracting the line gives the word back whole. An untagged document is written exactly as it was before, bracket and all left out. A hyphen the text brought itself — E-Mail — is never bracketed and never broken at.

tclpdf ships no hyphenation patterns, and that is a licence decision rather than an omission. Every published pattern set carries terms of its own — the German ones LGPL over LPPL, the American English ones BSD-style over the plain TeX table, others the LaTeX Project Public License — and this package is MIT; bundling them would make the smallest of those the licence every user has to read. The caller loads the file, from wherever that machine keeps it (/usr/share/hyphen/ on most Linux systems, the LibreOffice dictionary extensions on macOS and Windows), and the licence stays with the file.

The three commands below are package commands rather than document methods, and they live in a module nothing loads by itself: a script that hyphenates needs package require tclpdf::hyphenate of its own. A document loads it too when a block asks for -hyphenate, but the patterns have to be loaded before that, so the require comes first either way.

::tclpdf::hyphenate load tag path ?-left n? ?-right n? ?-min n? ?-exceptions words?
Reads a pattern file for a language. tag is RFC 3066, the same spelling language takes; path is a libhyphen .dic file — the format LibreOffice, Hunspell and the hyphen library install, hyph_de_DE.dic and its fifty siblings — which names its own encoding on its first line and its own minima in the lines after it. A file that carries several pattern tables separated by NEXTLEVEL follows the two-level rule libhyphen documents: the first table finds the joints of a compound over the whole word, and the last — the plain Liang table — is applied to each part between them, on its own. That is what those files are written for. Read with the last table alone, the Liang patterns find their own syllables across a joint: measured over the 55 581 compounds of hyph_de_DE.dic that name an inner joint, 18 per cent came out with a one-letter piece inside the word (aben-d-ap-pell) and a third did not offer the joint the file itself names (ab-baufront). That is the rule, not the program: libhyphen applies its own compound level recursively to each part it produced, so the two run apart on a compound whose parts are compounds again — measured over the German words of hyph_de_DE.dic, about five per cent fall differently, and nothing here copies the recursion. The price is paid once, at load: the compound table of that file is 69 127 entries against 8 718 in the Liang table, which is about seven tenths of a second and a good hundred megabytes of memory where the last table alone costs fifty milliseconds — measured 2026-08-27, 0.7 s and 120 MB of resident memory for hyph_de_DE.dic against 0.05 s and 20 MB for hyph_en_US.dic, over an interpreter that holds 12 MB before either is loaded. A file with one table — hyph_en_US.dic and every TeX conversion — is read exactly as before and costs nothing extra. Loading a tag again replaces it.

-left and -right are how many letters must stay on either side of a break, and they are what keeps Abend from being set as A-bend. Without them what the file says is used, and without that the plain TeX defaults 2 and 3. Pass -left 2 -right 2 for German: hyph_de_DE.dic states COMPOUNDLEFTHYPHENMIN and COMPOUNDRIGHTHYPHENMIN but neither of the two that matter here, and a right minimum of 3 refuses the two-letter endings German breaks off every day. -min is the shortest word broken at all, 5 unless said otherwise. -exceptions takes a list of words written with - at the breaks — {Wachs-tu-be Ur-in-stinkt} — which are looked up before the patterns and taken exactly as given, minima included; the .dic format has no exception block of its own, encoding its exceptions as whole-word patterns instead. A file that states a LEFTHYPHENMIN or RIGHTHYPHENMIN below 1 is refused as it loads, naming the file and the directive: a minimum of zero offers a break before the first letter, which is not a break at all, and the offer would otherwise reach the line breaker as a piece that consumes nothing of the word. Returns the tag.

::tclpdf::hyphenate languages ?tag?
Without a tag, the languages that are loaded, in the spelling they were loaded under. With one, what is known about that language: tag — which may differ from the one asked for, see below —, patterns, exceptions, left, right and min. A tag that is not loaded raises TCLPDF HYPHENATE LANGUAGE, which is how a caller asks whether a language is available without guessing.

A tag is looked up exactly first, then by its primary subtag: patterns loaded as de serve a document set in de-AT, and patterns loaded as de-DE serve one set in de. The tables of a language differ far less between its regions than they do between languages, and the alternative would be loading the same file under four names.

::tclpdf::hyphenate word tag word
Where a word may be broken, as the list of pieces it falls into — Sil ben tren nung. For checking a pattern file and for showing what it does; the line breaker uses the same answer. The pieces always concatenate back to the word handed in, character for character, punctuation and quotes included, so they may be joined with a hyphen to print or used to cut the original string.

Decomposed text is hyphenated like composed text. A word is canonically composed before the patterns see it and the breaks are mapped back onto the caller’s characters, so heru + U+0308 + ber falls into her über exactly as herüber does — measured over the 12 561 words with umlauts in hyph_de_DE.dic, both spellings now break identically, where before a third of them differed. The minima count letters, and a combining mark is not one: a break never falls between a letter and its own mark. The limits of the table are worth knowing — Latin, Greek and Cyrillic compose, Latin Extended Additional (the Vietnamese tone marks) and polytonic Greek do not, and marks are read in the order they were written rather than sorted into canonical order first.

A word that must not be broken comes back as a one-element list, which is deliberately the same answer as “no break was found”: neither is a failure. Not broken are a word shorter than -min, a run of fewer letters than the minima allow, a word with a digit anywhere in it, a word written in capitals throughout, and anything shaped like a URL, an address or a file name — a word that still holds a dot, a colon, a slash or an at sign once the punctuation has been taken off its ends. A word that carries a hyphen of its own is broken in its parts but never at the hyphen it already has, or the line would end in two of them. And a word carrying a soft hyphen is handed back whole, because those marks win.

Example 01.14 sets the same paragraph twice in the same column, with and without, and prints what each column’s worst word space measured; examples/assets/languages/README says where the two pattern files came from and under which licences.

doc leader left right -at {x y} -width w ?-fill “.”? ?-gap d? ?-tag type? ?font options?
A row with two ends and a filled middle — a table of contents, a price list, a total:

$doc leader "3. Embedding fonts" "24" -at {20 100} -width 120
# 3. Embedding fonts ........................... 24

Both ends are measured and the space between them is filled with as many whole copies of -fill as fit — measured as the run is drawn, so -spacing and -wordSpacing are counted between the copies — holding -gap clear of each end (1 unit by default). The remainder stays in front of the right hand end, so the figures of several rows line up. -fill may be any string, and an empty one draws nothing at all — which is what a sum under a rule wants. Either end may be empty. Returns the y coordinate one line down, so rows stack without measuring again.

-direction rtl turns the row round: the first argument is the leading end and belongs at the right edge, the second at the left, and the fill is measured from the other side. The alignment of each end follows, so a right-to-left row needs nothing but the option.

It does not wrap: each end is one line. A left side too long for the width keeps its full length and the fill disappears, rather than moving the figure a reader is looking for. In a tagged document the row is one element and the fill is an artifact — a reader that spelled the dots out would say “dot dot dot dot” between every entry and its number. -tag names the element (P by default), and -tag Artifact — or the kind as a list, -tag {Artifact Pagination Header}, as text takes it — takes the whole row, ends and fill, out of the tree, which is what a running head is. -width is a positive number and at most 14 400 points, the widest page a PDF file can hold (ISO 32000-1, Annex C.2): the row is filled with whole copies of -fill, so a width no page could show would be built and measured character by character. -gap is a distance of 0 or more and is held to the same bound.

doc textPath string -segments {…} ?-align a? ?-offset d? ?-tag type? ?font options?
Sets one line of text along a path, cluster by cluster, each one turned by the direction the path takes at its own position. The segments are the ones path takes (move, line, curve, close), checked exactly as path checks them — an unknown kind, a first segment that is not a move, a wrong operand count or a non-number is refused with the same message; -align — left, center or right, and centre for center — places the string at the start, the middle or the end of the path, and -offset lifts the baseline off it — positive above, negative below (a finite number; NaN, Inf and anything else are refused at the call, before the marked-content bracket of a tagged document is opened). An empty string, a path of no length — two points on the same spot — and a line feed in the string are refused for the same reason: each of them would leave a bracket around no glyph at all, and in a tagged document an element a reader announces and that holds nothing. Set each line on a path of its own. Returns the length of the path, which is what a caller measures a string against beforehand: glyphs that run past the end are dropped rather than piled up there. The path itself is not drawn.

A cluster, not a glyph. A combining mark has no advance and no place of its own: it is set against the glyph it hangs on, and GPOS can only reach it while the two stand in the same run — handed to the path singly it loses its base and lands on the character after it. So a base and the marks that follow it travel together: they are turned by the tangent as one piece, the marks are drawn inside the base’s own text object, and the path advances by the width of the base. Both spellings of a word therefore come out at the same place and the same size — measured on DejaVu Sans at 20 points on the curve of example 02.13-marks-on-a-path, Café Crème spelled with U+00E9 and U+00E8 and the same words spelled with U+0301 and U+0300 are both 41.5444 mm wide on the same 179.5325 mm of path.

-spacing counts glyphs there, not clusters, and that one is visible: the character spacing of PDF sits between every pair of glyphs, and a combining mark is a glyph, so the decomposed spelling gains one gap per mark — 49.3055 mm against 47.8944 mm at -spacing 2 for the words above. textWidth counts it the same way, which is why -align stays exact for either spelling: what was measured and what is drawn are the same number.

-direction rtl runs the glyphs backwards along the path and mirrors -align with them, exactly as it does on a straight baseline — including the numbers inside such a line, which keep their own order there too.

doc pageNumbers -at {x y} ?-format “Page %n of %m”? ?-align a? ?-from n? ?-total n? ?font options?
Puts a page number on every page. %n is the number, %m the total. %% is a literal per cent sign, and it is the only way to print one: it is replaced first, so %%n comes out as %n rather than as the number. -align places the number on -at as text places a line: left (the default) starts there, right ends there, center is centred on it. It takes the line options as well as the font ones, so -format "صفحة %n من %m" -direction rtl sets a right-to-left page number with the two figures the right way round. The numbers are drawn when the document is written, not when the call is made — which is the only moment the total is known — so the call may come before the pages it numbers. -from is the first page to number, 1 unless said otherwise, and leaves the leading pages unnumbered, -total states a total of its own for a document that is part of a larger set. -at, -format, -from, -total and -align are all refused at the call: -at takes two numbers a page can hold, -format is one line — a line feed in it is refused there rather than met by the write —, -total is a whole number of 1 or more and -align one of left, right, center. Several calls are independent of each other: a number at the foot and a running title at the head are two of them. A character the face cannot set is refused at write time with the usual error code — its position then refers to the expanded label (“Page 3 of 12”), not to the format string, because the expanded label is what is being drawn.

doc textWidth string ?font options?
The width of a string in the document unit. It takes the font options and nothing else: a layout option such as -at, -width, -align, -tag or -hyphenate is refused by name rather than accepted and ignored — a measurement that silently disregards half of what it was asked would be worse than one that stops, and the caller wanted textLines or textHeight.

doc textHeight string -width w ?options?
The height a paragraph of that width would take — with -runs 1 or -markup, of the runs the string names, headings and their leading included (see -runs under text). The height a paragraph of that width would take — exactly what text advances by with the same options: the difference between the y it returns and the y it was given, indents, paragraph spacing, the ascender of -anchor top and the lines a shape pushes down included. It takes the options text takes, so one option list serves measuring and drawing; what only the drawing uses (-align, -rotate, -tag, -expansion) is accepted and changes nothing, and so are -height, -paginate, -columns, -gutter and -balance — a measurement has no page to fill; an unknown option is an error. With -avoid the call needs -at, because the shapes are page positions.

doc textLines string -width w ?options?
The lines a paragraph would be broken into, with the same options and the same rules. With -runs 1 or -markup every line is answered as the list of {text options} pairs it is made of — the pieces of the line, each with the options it was set with, a heading’s pieces with theirs — so that a line can be drawn again with the faces it was broken with. Both calls also take the width as their first argument: textLines $s 80 is the same as textLines $s -width 80.

-hyphens 0|1 changes the answer: with 1 every element is a dictionary {text … hyphen …} instead — the line, and whether the - it ends on is a break the line breaker put there or a character of the text. A string cannot carry that difference and the caller cannot work it out afterwards: measured at one and the same width, Die Betriebskoste- ends a line on a hyphen made here and die Nord-und Su- holds one the text always had. It is the other half of text -breakHyphen, and the pair is what a caller needs who breaks his lines here and draws them one at a time — a break hyphen is not a character of the text (ISO 32000-1, 14.8.2.6) and has to be bracketed as such, or the line extracts as Betriebskostenab-rechnung:

-emergencyHyphen 0|1 puts a hyphen on the break a word gets when it is longer than the column, and text takes it as well as textLines and textHeight, marking the same break the same way. Such a word has always been broken inside rather than let run past the edge; what this adds is the mark on the break, and the hyphen is measured with the piece rather than hung on after it, so the line stays inside the column — one piece fewer, not one hyphen too wide. The break falls between the things a face draws, which is one character in the ordinary case and one sequence where a face has a glyph for several: a family emoji is never cut into a man and a joiner, and a mark is never cut from its variation selector. What a face draws with no advance of its own — a variation selector it carries as a glyph, a combining mark — belongs to the character in front of it and never opens a line, whether or not any face registered the two as a sequence. And the hyphen a break is marked with is the line breaker’s, not the text’s: a face with no glyph for U+002D refuses the paragraph as TCLPDF TEXT BREAKHYPHEN glyph, naming the break hyphen rather than a position in a string the caller never wrote it into. A single unit wider than the whole column is set and overflows, exactly as a single over-wide character always has — one line that says something beats no line at all. Off by default, and that is a decision rather than caution: the words that reach this fallback are as often a part number, a file path or a URL as a long word, and a hyphen inside one of those is not a typographic aid but a character the reader copies and a wrong value. A column too narrow to hold one letter and a hyphen drops the hyphen rather than the letter. -hyphens 1 reports these breaks like any other, so text -breakHyphen can bracket them.

foreach line [$doc textLines $body -width 60 -hyphenate de-DE -hyphens 1] {
    $doc text [dict get $line text] -at [list $x $y] -breakHyphen [dict get $line hyphen]
    set y [expr {$y + 5}]
}

The default is 0 and answers the list of strings it always did, so nothing that reads foreach line [$doc textLines …] changes. A dictionary rather than a two-element list because it says what it holds where it is read and can gain a key later without a caller noticing. textHeight takes the option and ignores it — one option list still serves the height and the lines — but refuses a value that is no boolean just as textLines does, under TCLPDF TEXT HYPHENS. textWidth refuses it by name like every other layout option, and text does not take it at all: it says what an answer looks like, and a drawing has no answer of that shape.

Graphics

doc line -from {x y} -to {x y} ?-stroke c? ?-width w?

doc rect -at {x y} -size {w h} ?-radius r? ?-fill c? ?-stroke c?

doc circle -at {x y} -radius r ?-fill c?

doc ellipse -at {x y} -size {w h} ?-fill c?

doc arc -at {x y} ( -radius r | -size {w h} ) ?-start deg? -extent deg ?-style arc|pieslice|chord?

doc polygon -points {x y x y …} ?-close 1? ?-fill c?

doc curve -from {x y} -c1 {x y} -c2 {x y} -to {x y}

doc path -segments list ?-fill c? ?-stroke c? ?-rule evenodd?
The shapes. Common options are -fill and -stroke (a colour), -width (line width, 0 or more — 0 is the thinnest line the device can draw), -dash (a pattern: a list of lengths of 0 or more, not all of them zero as the file writes them — a PDF real carries five decimals (7.3.3), so -dash {0.000001 0.000001} is refused rather than written out as [0 0] 0 d, which is the array 8.4.3.6 forbids; none or solid for an unbroken line), -cap (butt, round or square), -join (miter, round or bevel), -miter (the miter limit: how far a pointed join may reach before it is cut to a bevel; 1 or more and at most about 3.403e38, the range of a PDF real (Annex C.2), and 1 bevels every join — a value past that range is refused by this option rather than by the writer behind it), -opacity and -blend. -radius is a length of 0 or more; on a rectangle it rounds the corners and 0 leaves them square. -rule takes nonzero (the default) or evenodd and decides which parts of a self-intersecting path count as inside; anything else is refused. -from, -to, -at, -c1, -c2 and -size are exactly two numbers each, checked the way every other shape’s are: a third word is refused rather than silently dropped. A segment of -segments is {move x y}, {line x y}, {curve x1 y1 x2 y2 x y} or {close}, in document coordinates, and a path begins with a move. -close joins the end of a polygon, a curve or a path back to its start; a polygon is closed by default, the other two are not. circle and ellipse are one command under two names: either takes -radius for a circle or -size {w h} for an ellipse, and -at is the centre. arc draws a piece of that same shape and takes the same pair — -radius for a circular arc, -size for an elliptical one, exactly one of the two — with -at the centre again; naming both is refused, and so is a third word in -size. Its angles are in degrees, 0 at three o’clock, and they grow counter-clockwise as the page is read, so 90 points up: the Tk canvas convention, and deliberately not the direction the document’s downward y would suggest. -start defaults to 0; -extent is required and is how far the arc sweeps — negative runs the other way, 360 is the whole ellipse, and beyond ±360 is refused, as is 0. An -extent or a radius that rounds to nothing as the file writes it — five decimals, 7.3.3 — is refused for the same reason an -extent of 0 is: there is nothing to draw. At an -extent of ±360 a pieslice has no spoke: the two radii would fall on top of each other and the line would still be drawn and stroked. -style is arc (the curve alone, left open), pieslice (plus the two radii to the centre) or chord (plus the straight line back to where the curve began); the default is arc. -style arc takes no -fill, and a fill colour standing in the state from style counts as one: an open path is closed implicitly before it is filled (8.5.3.1), so a filled open arc would come out as a chord without saying so — ask for -style chord when that is the shape you mean. There is no -rotate, as there is none on ellipse; turn the arc with transform. A shape that is refused — a point that is not a number, an unknown segment, an odd count of -points — leaves nothing behind in the page: the check comes before the first byte is written.

doc clip -at {x y} -size {w h} ?-rule evenodd? / doc clip -segments list ?-rule evenodd?
Clips everything drawn afterwards to a rectangle or to an arbitrary path, until the graphics state is restored — so it wants a save around it. -segments takes the same list as path. The path itself is never drawn: it ends in W n instead of a painting operator and only decides what of the following output stays visible. -rule evenodd is what leaves the hole in a ring open; as on the shapes it takes nonzero or evenodd and nothing else. Give either -at with -size or -segments, not both.

doc save / doc restore
Push and pop the graphics state (q and Q). A restore with no save open in the same stream is refused and writes nothing. At most 28 saves of this API may be open at once (ISO 32000-1, Annex C.2), and one more is refused rather than written. That is fewer than the nesting of the file: every shape with a style of its own, every text run and every placement writes a q/Q of its own inside the bracket, and a form’s stream runs inside the save its Do makes (8.10.1), so the depth a reader counts can exceed the depth this package counts. The limit is held on what a caller opens, which is what a caller can close; ISO 32000-2 keeps the figure of 28 only historically. A content stream that ends with a save still open is refused at the write, naming the page, form or tile it was left open in — the mirror of the refusal a restore without a save already makes.

doc transform ?-translate {dx dy}? ?-rotate deg? ?-scale s? ?-skew {a b}? ?-at {x y}? ?-matrix {a b c d e f}?
Multiplies the current transformation matrix. -at names a fixed point to turn, scale or skew about; -translate is a displacement. The two are different things and must not be confused. The parts compose as if called one after another — translate, then rotate, then skew, then scale — the way a sequence of cm operators does: a shape drawn afterwards is scaled first, skewed, turned, and displaced last, so the displacement is in unscaled, unturned document units, as it is in Canvas or SVG (-translate {10 0} -rotate 90 moves 10 mm to the right, not 10 mm down). -scale is one factor or {sx sy}, none of them zero — a negative one mirrors. The singularity check applies to the matrix the parts compose to, not only to a raw -matrix: -skew {45 45} is refused, because the tangent of 45 degrees is 1 on both axes and the result collapses the plane onto a line. Without -at the fixed point is the origin of the PDF user space — the bottom left corner of the page, which in the caller’s own coordinates is {0 pageHeight}. A rotation or a scale given without -at therefore turns or scales the whole sheet about that corner, and a shape drawn afterwards at the middle of the page can land outside it. Name -at whenever the fixed point is meant to be somewhere on the page.

-skew shears by two angles in degrees: the first tilts vertically — y follows x — and the second horizontally, which is the slant an italic-looking stamp needs. -matrix takes the six numbers of a PDF matrix — exactly six, and not singular: a cm whose a·d − b·c is zero folds everything drawn after it onto a line, so it is refused before it is written — and multiplies them in as they stand, ignoring every other option: the values are the raw cm operands — points, origin at the bottom left, y upwards — for the caller who already has a matrix rather than wants one built.

doc opacity value ?fill|stroke|both?
Fill and stroke opacity between 0 and 1. The second word limits it to one of the two — both, the default, sets fill and stroke alike. (PDF 1.4.)

doc blend mode
The blend mode: how a colour is combined with what is already on the page. One of Normal, Multiply, Screen, Overlay, Darken, Lighten, ColorDodge, ColorBurn, HardLight, SoftLight, Difference, Exclusion, Hue, Saturation, Color or Luminosity. Like the alpha it is graphics state and holds until changed; the shapes take it per call as -blend, which keeps it inside their own save/restore. Compatible is refused — it has been deprecated since PDF 1.4 and means Normal. PDF/A parts 2 and 3 permit every mode listed. (PDF 1.4.) Inside a form create script a blend mode composites within the form’s own transparency group, not against the page: every form is an isolated group (ISO 32000-1, 11.6.6), and the backdrop a mode inside it sees is transparent. Set the mode on the placement instead when what is meant is “combine this form with the page”.

doc overprint ?-fill bool? ?-stroke bool? ?-mode 0|1?
Whether what is painted from here on knocks out what lies under it on the other colourants, or leaves it standing (8.6.7). It is the one graphics state parameter that changes nothing a reader shows and everything a press prints, so nothing in this package decides it for the caller.

Black text on a cyan panel is the standing case. Knocked out — the default — the cyan plate is punched where the letters go, and the sheet has to register to a tenth of a millimetre or a white edge shows around every letter. Overprinted, the cyan runs on underneath and a misregister is invisible. And the trap, which is why control strips exist: overprinting a light colour is a mistake — yellow over cyan, overprinted, is green. No viewer shows it and no validator reports it; the proof is where it is found.

-stroke governs stroking and -fill governs filling, and naming the stroke says something about the fill as well. A graphics state dictionary that carries /OP and no /op sets both parameters (Table 58), so a call that names -stroke writes the fill side out too: from -fill where the call gives it, and otherwise from the fill overprint already in force — unchanged, which is what leaving an option out has to mean. Where both sides come out the same the dictionary is written as /OP alone, which is word for word what that means and what a PDF 1.2 file can hold; where they differ the two keys are spelt apart and the file is PDF 1.3. The fill overprint is remembered per content stream and is taken back by restore, exactly as the colours of style are. -mode writes /OPM (PDF 1.3): it says what a zero in a CMYK value means — 0 leaves that plate alone, 1 (“nonzero overprint”) paints it at nought, which is how a caller sets one plate without disturbing the rest. The version asked for is the one the call actually writes, so a document that only sets the stroke is not raised for a key it never wrote. An option left out is not written; a call naming none of the three is refused (TCLPDF GRAPHICS OVERPRINT NONE) rather than writing a graphics state that changes nothing. The shapes take -overprint bool as well, which sets both sides at once inside the shape’s own q/Q — a shape that is filled and stroked in one call has no way of saying which of the two it meant — and style takes it too, where it holds until changed like every other drawing state.

doc style ?options?
Sets the drawing state — the same options the shapes take, in force until changed: a -width, -dash, -cap, -join, -miter, -opacity, -blend, -overprint, -fill or -stroke set here holds for every shape that does not name its own. The colours are the part worth spelling out: after style -fill red -stroke blue a bare rect is filled red and stroked blue, a rect -fill green is filled green and still stroked blue — a shape paints the sides style coloured plus the ones it names itself — and a line, which has no fill, is stroked blue instead of the black it draws in on its own; a text between them leaves them in force: it brackets its own colour. The state is that of the stream: save and restore take it back with the Q, a form or a pattern being built neither sees the page’s colours nor leaks its own, and a new page starts fresh.

Colour

A colour is a name (red, steelblue — 148 of them, without Tk), a hexadecimal triplet (#ffd700, or #fd7), a grey value, {r g b} between 0 and 1, {c m y k}, a separation, a DeviceN colour, an ICC based colour {icc alias components} — the alias registered with icc embed below — or a Lab colour {lab L a b}. Three and four numbers are told apart by their count; the space may be named instead — {gray 0.5} (grey as well), {rgb 1 0.84 0}, {cmyk 0 0.16 1 0} — which is the unambiguous form. Fills may also name a pattern: {pattern sky} — and so may the colour of text, through font -color or a text call’s -color, which fills the glyphs with the gradient or the tile. A name or hex triplet whose three components are equal — black, white, gray, silver, #808080 — is written as DeviceGray, not as three equal RGB numbers: the picture is the same, but PDF/A allows DeviceRGB only under an RGB output intent (ISO 19005-2, 6.2.4.3) and DeviceGray under any, so the achromatic names stay usable in a document whose intent is CMYK — and pdfa refuses at the write whatever does not fit the intent, naming the call. The one combination refused there that is not about the intent is overprinting: an ICC based CMYK colour that is filled or stroked while overprinting for that side is on and overprint -mode is 1 is forbidden outright by ISO 19005-2 and -3, 6.2.4.2, and is refused at the write (TCLPDF GRAPHICS OVERPRINT pdfa), naming the call and the page — the same colour written as {cmyk …}, the same overprint at mode 0, and the mode taken back by a restore are all conformant and are written. Where colours have to share one space, as the stops of a shading do, a grey stop is promoted to the space of the coloured ones — {white steelblue} is an RGB gradient. Components outside 0..1 are clamped to the range, not refused. NaN and infinity are the exception, and both are refused where the component is read, with TCLPDF COLOUR COMPONENTS number, before anything is written or stored. Neither of them left a range: NaN names no colour, no nearest valid value exists for it, and clamping cannot even see it — it is neither smaller nor larger than a bound — while Inf and -Inf left the numbers a PDF file can write at all, so clamping them to a bound would answer a question the caller never asked and a colour that came out of an overflow or a division by zero would be painted saturated without a word. That holds for every space and for font -color as well, where an accepted NaN used to sit in the text state and kill every later text. It holds for 1e400 too, which is how infinity usually arrives — nobody types Inf, a literal simply overflows the double.

A separation is a spot colour — a varnish, a security ink, a Pantone shade: {separation Name alternate ?tint?}. Name is the plate as the press will know it — a separation needs a name, and All and None, the special colourants of ISO 32000-1 8.6.6.4, are written as they stand; alternate is an ordinary colour in one of the three device spaces or in Lab — grey, RGB, CMYK or {lab L a b}, never another separation or a pattern — what a reader that has no such ink shows instead; tint is the coverage from 0 to 1 and defaults to 1. {separation Varnish {cmyk 0 0 0 0.2} 0.8} paints 80 % of a plate called Varnish. The colour space object and its tint transform are written on first use, once per name, and fill and stroke may both use it; a name that appears again must carry the same alternate, since one plate is one ink. Under PDF/A the alternate follows the output intent like every other colour (ISO 19005-2, 6.2.4.4): a CMYK alternate needs a CMYK profile, an RGB one an RGB profile, grey passes anywhere — see pdfa for the check. The names this writer keeps for itself are reserved here and for an icc embed alias, and the two halves of the list differ in how far the reservation reaches: Lab and DeviceN, with or without the eight uppercase hexadecimal digits this writer appends, are reserved whatever their case, because the writer builds those names itself and a caller’s lab0123ABCD would collide with one it made; DeviceGray, DeviceRGB, DeviceCMYK and Pattern are reserved in that spelling, which is the spelling ISO 32000-1, 8.6.8 gives the four families and the only one a /ColorSpace entry of that name could be mistaken for. A name of any kind is at most 127 bytes as the file writes it, escapes included (ISO 32000-1, Annex C.2), and a longer one is refused.

A Lab colour is a colour as it was measured rather than as a device would make it: {lab L a b}, where L* runs from 0 to 100 and a* and b* over the range the space names — {lab 54.29 80.81 69.88} is the red of sRGB, said as a measurement. The space has two parameters, and both are options on the colour itself: -whitePoint {Xw Yw Zw} is the tristimulus value of the diffuse white point, {0.9642 1.0 0.8249} — D50, the illuminant of the ICC profile connection space and of every printed measurement — unless you say otherwise; ISO 32000-2, 8.6.5.4, Table 64 requires Xw and Zw positive and Yw exactly 1.0, and a white point that breaks either is refused, because a wrong white point is not a stray value but a wrong space. -range {amin amax bmin bmax} bounds a* and b*, {-100 100 -100 100} by default, which is the standard’s own default; the wider {-128 127 -128 127} of the example in 8.6.5.4 is the usual choice in prepress and one option away. Components outside those bounds are clamped, not refused — the standard prescribes it: “Component values falling outside the specified range shall be adjusted to the nearest valid value without error indication” — so sRGB blue, whose b* is -112.05, comes out at -100 unless -range admits it. NaN and infinity are refused there too, as they are in every other space (TCLPDF COLOUR COMPONENTS number), and so is either of them among the numbers of -whitePoint or -range (TCLPDF COLOUR LAB): the clamping rule of 8.6.5.4 adjusts a value to the nearest valid one, and neither has one. The parameters are the place it mattered most, because they are not clamped at all and infinity passed both bound checks of Table 64 unharmed — Inf is positive, so -whitePoint {Inf 1 1} was taken, and -Inf is the smaller of a pair, so -range {-Inf 100 -100 100} was — and the value then reached the /WhitePoint array and killed the document at the moment of writing. The three numbers are not 0..1 like every other space here; a Lab colour clamped to that range would come out black. The colour space object is written on first use and stands in the document once per distinct white point and range, however many colours and separations use it. The point of the space is the spot colour: {separation "Spot Red" {lab 48.3 68.5 47.3} 0.6} gives a plate the measurement behind its name instead of a CMYK guess, and tint 0 there is L* 100 — paper, not ink. Lab is device independent like an ICC based colour, so pdfa admits it under every output intent — measured with veraPDF: a Lab fill, a Lab stroke and a Lab separation alternate pass PDF/A-3B under an sRGB, a grey and a CMYK intent alike, where the same colours as {rgb …} are refused under the CMYK one. A gradient cannot take one, for the same reason an ICC based colour cannot: a shading names its space by family in its dictionary, and a Lab space is an array. Lab, and Lab followed by eight uppercase hexadecimal digits, are the names this writer gives its Lab colour spaces and may not be used for a separation or an icc embed alias. (PDF 1.1.)

A DeviceN colour paints several named plates at once: {devicen {{Name colour} …} ?{tint …}?} — a list of colourants, each written exactly the way a separation is (Name the plate as the press will know it, colour what that plate paints at full tint), and one tint per name in the same order, all of them 1 unless said otherwise. {devicen {{Orange {cmyk 0 0.45 1 0}} {Green {cmyk 0.8 0 0.7 0.1}}} {0.7 0.3}} paints 70 % of an orange plate over 30 % of a green one. This is the space of high-fidelity printing — PANTONE Hexachrome is CMYK plus orange and green — and of duotones, and it is not a separation with several names: the space carries one function that turns n tints into the alternate space, so what it describes is how the inks look together, where a separation describes one ink alone (ISO 32000-2, 8.6.6.5). All the colourants therefore paint into one alternate space — a grey one is promoted to the space of the coloured ones, as a grey shading stop is, and mixing RGB with CMYK is refused; Lab and ICC based colourants are refused too, although the standard would admit them, because combining two measured appearances needs a colour engine this package has not got — paint such a plate on its own with {separation Name {lab L a b} tint}. All, which a separation may be, is refused here: 8.6.6.5 rules it out, since every plate at once cannot be one component among n. None is the opposite case and is written {None}, without a colour: it names a component that is never painted, it is the one name that may be repeated, and this writer keeps it out of the fallback colour as well. A space whose colourants are all None paints nothing and names no alternate at all, and is refused rather than written. At most 32 colourants (Annex C.2). A colourant and a separation of the same name are the same plate: they share one colour space object, and a colourant that contradicts an existing separation’s alternate is refused, naming the colour that stands.

The tint transform of that space is a type 4 function — PostScript calculator code — because a type 2 or type 3 function takes one input by definition (7.10.3, 7.10.4) and this one needs n; the standard points at type 4 for this very case, a sampled function being prohibitive at 4·m⁶ samples for a six-colourant space (7.10.5.1). In a CMYK alternate the ink amounts add and the function’s own /Range clips the sum, which is what a press does when two solids overprint; in an RGB or grey alternate, where the numbers count light rather than ink, they multiply instead, so no ink at all leaves paper rather than black. Both are exact where a caller can check them — no ink, and one plate alone: the same plate painted as a separation and as one component of a DeviceN space comes out the same colour, measured with pdftoppm. The space also carries an attributes dictionary with /Subtype /DeviceN and /Colorants, which holds each plate on its own — the one thing a combined transform cannot say — and under PDF/A that dictionary is required, not optional: ISO 19005-2, 6.2.4.4 wants an entry for every spot colourant of a DeviceN space, and veraPDF fails a document without it. /Process and /MixingHints are not written: the first belongs to the NChannel subtype, the second wants measured ink solidities and dot gain that this package has no source for. Because every entry of that dictionary is PDF 1.6, a DeviceN colour needs a 1.6 document although the space itself is 1.3 — said at the call, with the number to raise the document to. Under PDF/A the alternate space is what the output intent judges, exactly as for a separation (6.2.4.4). A gradient cannot take a DeviceN colour, for the same reason it cannot take a Lab or an ICC based one: a shading names its space by family in its dictionary, and a DeviceN space is an array. DeviceN followed by eight uppercase hexadecimal digits is the name this writer gives its DeviceN colour spaces and may not be used for a separation or an icc embed alias. (PDF 1.6.)

doc icc embed alias path / doc icc names
Registers an ICC profile under an alias; icc names lists the aliases. {icc alias components} is then a colour in that profile’s space wherever a colour is taken — fill, stroke, text — with as many components as the profile has: one for a grey profile, three for RGB, four for CMYK; a wrong count is refused naming the right one, and an unregistered alias is refused before anything is recorded. The profile has to describe a device space — grey, RGB or CMYK — and be a profile at all; anything else is refused at the icc embed call. Its own version is held against the version of the file (ISO 32000-1, 8.6.5.5, Table 66): a version 4 profile needs PDF 1.7, and a document written below that is refused naming the number to raise it to. The colour space object — a stream holding the profile, with /N for the component count (ISO 32000-1, 8.6.5.5) — is written on first use, and a profile stands in the document once however many roads it arrives by: as a registered colour space, as a picture’s embedded profile (see Images), or as the PDF/A output intent — pdfa with the same file as -profile points its intent at the same stream. An ICC based colour is anchored to its profile rather than to the device, so pdfa admits it under every output intent — measured with veraPDF: {icc srgb 0.2 0.45 0.75} passes PDF/A-3B under a CMYK press intent where the bare {rgb 0.2 0.45 0.75} is refused, because ISO 19005-2, 6.2.4.3 judges device colours only. A gradient cannot take one — a shading names its space by family in its dictionary; give the stops in grey, RGB or CMYK. (PDF 1.3.)

Images

doc image embed alias ?path? ?-data bytes? ?-type auto|jpeg|png|tiff? ?-icc bool? ?-stencil bool? ?-mask alias? ?-invert bool? ?-interpolate bool?
Reads a JPEG or PNG and prepares it for placing. JPEG data passes through untouched as /DCTDecode; a PNG without alpha passes through as /FlateDecode. A PNG whose transparency is a single colour — a greyscale or truecolor file with a tRNS chunk, or a palette whose entries are either opaque or fully transparent and whose transparent entries form one contiguous run of indices — keeps that transparency as a /Mask colour-key array on the same pass-through, at the file’s own bit depth; the reader keys the colour out, nothing is decoded. The one exception is a 16-bit greyscale or truecolor file: a /Mask array at sixteen bits is what the format asks for, but readers were measured to ignore it — poppler compares the ranges as if the image were 8-bit, and CoreGraphics renders the picture opaque too — so there the key is decoded once into an /SMask of 0 and 255; the picture itself still passes through at 16 bits, only the mask is computed. Only a PNG with an alpha channel, a palette with partially transparent entries or with transparent entries in more than one run, or such a 16-bit key is decoded to build an /SMask. (A PNG with a transparent colour needs PDF 1.3, one with an alpha channel or a computed soft mask 1.4, sixteen bits per component 1.5.) A PNG palette holds at most 256 entries (PNG 11.2.3), which is also the ceiling of an indexed colour space (hival ≤ 255, ISO 32000-2, 8.6.6.3); a longer one is refused as a damaged file. A progressive JPEG and an interlaced (Adam7) PNG are refused with the reason: JPEG data is embedded as it is and only baseline and extended-sequential files are taken — re-save as baseline; an interlaced PNG re-save without interlacing.

A three-component JPEG whose samples are not YCbCr is written with /DecodeParms << /ColorTransform 0 >>. ISO 32000-2, Table 13 lets a reader assume 1 for three components when the data carry no Adobe APP14 marker and the filter dictionary says nothing, so an RGB-coded file would be converted once too often — measured, Preview.app renders red as (134, 252, 82). Which file that is, is read off the markers in the order every decoder in the field reads them: a JFIF APP0 means YCbCr and nothing is written; an Adobe APP14 decides by its transform flag; with neither marker the component identifiers of the frame header decide, 1, 2, 3 meaning YCbCr and R, G, B (82, 71, 66) the convention libjpeg writes without a marker. An ordinary JFIF photograph therefore gets no entry, because the default is already right for it.

A PNG is held to the combinations of colour type and bit depth PNG 11.2.2 defines — greyscale at 1, 2, 4, 8 or 16 bits, truecolour and both alpha types at 8 or 16, a palette at 1, 2, 4 or 8 — which are also the depths ISO 32000-2, Table 87 admits for /BitsPerComponent. Any other pair is refused as a damaged file (TCLPDF IMAGE PNG DAMAGED IHDR), because the depth of the IHDR is written straight into the image dictionary and into the predictor’s /DecodeParms.

An ICC profile embedded in the file — JPEG carries one in APP2 ICC_PROFILE segments, PNG in an iCCP chunk — becomes the picture’s /ICCBased colour space instead of the bare device name, so the picture is anchored to its profile and pdfa admits it under every output intent (see Colour); for a palette PNG it becomes the base of the /Indexed space, and a soft mask stays DeviceGray — the mask is coverage, not colour. The profile stream is shared with icc embed and the output intent: one profile, one copy per document. A kept profile has to describe the space of the samples (/N, ISO 32000-1, 8.6.5.5) — a grey picture with an RGB profile, or an APP2/iCCP payload that is no profile, is refused at the embed as TCLPDF IMAGE ICC, with -icc 0 named as the way out: -icc 0 embeds the picture without its profile. One class covers both refusals about a profile; TCLPDF IMAGE FORMAT is kept for a file whose format this package does not read. (A picture with a profile needs PDF 1.3.)

-data takes the bytes instead of a file name — for an image that never was a file: a canvas posted from a browser, a plot from a subprocess, a value out of a database. The format is decided by the leading bytes in both cases, so nothing else changes; image info then reports an empty path. A file name and -data in one call are refused (TCLPDF IMAGE ARGUMENT source), at image embed and at image draw alike and for the same reason font embed refuses it: one of the two would win and the other be dropped in silence, and which one the caller meant is not a question a writer can answer. A PNG with an alpha channel is by far the most expensive way in — the channel has to be split out in pure Tcl, measured at about a hundred times the cost of the pass-through. Where transparency is not needed, JPEG or a PNG without alpha is the cheaper choice, and for a browser canvas that means toDataURL("image/jpeg").

-stencil 1 turns a one-bit PNG into a stencil mask (/ImageMask true, ISO 32000-2, 8.9.6.2). Such a picture has no colour of its own — Table 87 forbids it a ColorSpace entirely — and its samples “designate places on the page that should either be marked with the current colour or masked out”: every placement paints the fill colour then in force, the one style -fill last set, through the bits of the file. That is the way in for a single-colour logo or a stamp that has to take the colour of the document it lands in: one embedding, one object, any number of placements in any number of colours. Which of the two bit values is the ink is read out of the file rather than left to be guessed — in a greyscale picture 0 is black and paints, in a palette picture the darker of the two entries paints, unless a tRNS chunk makes exactly one of them fully transparent, in which case the other one does. -invert is not only a stencil option: on an ordinary greyscale picture it writes the /Decode [1 0] of Table 87, which is what a soft mask drawn the other way round needs, and on a greyscale TIFF it cancels the /Decode array a WhiteIsZero file already carries rather than reversing the samples twice. It has one component to reverse and no more — Table 87 fixes the array at twice the number of components — so a colour picture asked to invert is refused with TCLPDF IMAGE INVERT. -invert 1 reverses whatever that decided (the /Decode array becomes [1 0]). A stencil is a pass-through like any other PNG, never a decode, and the tRNS chunk it may carry reaches the file as nothing at all, because with ImageMask true the Mask entry shall not be present. It has to be one bit per sample: a JPEG is refused, because a DCTDecode filter always delivers 8-bit samples, and so is a PNG of any other depth or with more than one sample per pixel. Under pdfa a stencil is admitted whatever the output intent says — it brings no colour space to be judged, and what is judged is the fill colour it lets through, where that colour is set.

-mask alias names a second embedded picture as this one’s mask, which until now only a PNG’s own alpha channel could produce. What it becomes in the file depends on what the named picture is, and the difference is not one of spelling: a picture embedded with -stencil 1 is all-or-nothing and becomes the /Mask of explicit masking (8.9.6.3) — unmasked areas show the base picture, masked areas show nothing — while any other picture is coverage and becomes the /SMask of a soft mask (11.6.5.2), so the base picture fades rather than ends. The two roads have opposite polarity, and it follows from what each entry means rather than from a choice made here: a stencil lets the base picture through where the stencil is dark, because its dark half is the ink that paints (8.9.6.2), and a soft mask where it is light, because its value is coverage (11.6.5.2). -invert 1 on the mask reverses either of them. The mask has to be embedded before the picture that wears it, it gets an object of its own and no page resource, and two pictures may share one mask, which then travels once. The two need not be the same size: all images are defined on the unit square, so their boundaries on the page coincide. Table 143 fixes what a soft-mask image may be, and each of its conditions is a refusal here — a mask that is not greyscale, one carrying an ICC profile (-icc 0 is the way out) and one with transparency of its own. Every such question, for the picture and for its mask, is asked before the first object of either is written, so a refused mask leaves nothing behind — with one exception that is named rather than closed: a mask whose data fail only while its streams are built, a damaged alpha channel for instance, leaves the ICC profile of its carrier in the file as an object nothing refers to; the file stays valid and a reader never sees the profile. So is a picture that already brings its own mask, because an SMask entry overrides the image’s Mask entry and a second mask would silently replace the first, and so is a stencil asked to wear a mask, which with ImageMask true may have none.

-interpolate 1 sets /Interpolate true (8.9.5.3), the declaration that a picture “might render better if interpolation is used” where it is rendered well below the device’s resolution. It is a hint, and the standard says outright that a processor may ignore it; it is written only where it is asked for, since Table 87 gives it the default false. On a stencil the effect, where it is honoured, is to smooth the edges of the mask rather than the colour it lets through. Under PDF/A it is refused, on both roads: ISO 19005-2, 6.2.8 says the key “shall not be present, or shall have a value of false”, and veraPDF reports it — smoothing is a decision the archive format takes away from the reader on purpose, so that the same file looks the same in twenty years. Whichever comes first in the script is what refuses: a picture placed under a standing declaration is turned away as it goes down An inline image binds the version in both directions: a 1.x file that carries one cannot be raised to PDF 2.0 afterwards, because /L is required there and the bytes are already on the page; and a 2.0 file that wrote /L cannot be lowered., and a pdfa call after the fact is turned away naming the pictures already in the file. The error code is TCLPDF IMAGE INTERPOLATE PDFA either way.

A TIFF is read too. A TIFF is a directory of tags and a list of strips, each a band of the picture compressed on its own, and every compression that occurs has a PDF filter that undoes it — Deflate is /FlateDecode, PackBits is /RunLengthDecode, CCITT Group 3 and 4 are /CCITTFaxDecode, TIFF 6.0 Technote 2 JPEG is /DCTDecode — so the strips are passed through, Group 4 fax data included. Five things are not the file’s own bytes. LZW is unpacked and written out again as Flate, always, because ISO 19005-3, 6.1.7.2 forbids /LZWDecode and a picture’s stream is built when it is first placed while pdfa may be declared afterwards. An uncompressed file is deflated on the way out. A FillOrder 2 fax has the bits of every byte turned round, which PDF has no parameter to ask a filter for. A 16-bit file is unpacked where its samples are little-endian, since PDF reads a 16-bit sample high byte first (8.9.5.2), and where it carries TIFF Predictor 2, whose per-component difference no reader was measured to undo correctly at that depth; the difference is resolved here, the samples are swapped into PDF’s order, and what travels is plain deflated samples with no predictor. And a JPEG-in-TIFF is assembled rather than copied: TIFF 6.0 Technote 2 puts one bare JPEG stream in every strip and the tables they share in a tag of their own, so neither half is a file a decoder would take, and each strip goes out as its own stream with an APP14 Adobe segment and those tables put in front of it. PackBits is passed through, and only walked: its no-op byte is PDF’s end-of-data, so the runs are stepped over to drop those and to append the end-of-data marker TIFF does not write — measured, no file in the corpus holds one, so the walk copies the strip byte for byte and only proves that it may. A greyscale, RGB, palette or CMYK picture at 1, 2, 4, 8 or 16 bits is taken, an embedded ICC profile becomes the /ICCBased colour space as it does for the other two formats, a palette becomes an /Indexed space over DeviceRGB, and a WhiteIsZero picture reaches the file with the /Decode array that reconciles it with DeviceGray. TIFF writes a ColorMap in 16-bit values, and the commonest mistake made against the format is a writer that fills it with 0..255 instead; a map whose every entry is 255 or less is therefore read as an 8-bit one rather than shifted down to a palette of pure black — the heuristic libtiff has carried since 1993, and the alternative would be to render as black a picture every other reader shows in colour. Several IFDs are not several pages: the primary image is the one embedded, and the others are passed over in silence — image info says nothing about them, and there is no way to ask for one. A multi-page fax reaches the page as its first page. Refused by name, each with what the file is and what to re-save it as: BigTIFF, a tiled file, separate colour planes, an alpha channel (ExtraSamples — PDF carries no alpha inside an image and none is split out), YCbCr outside a JPEG stream, and any bit depth, sample format or predictor PDF cannot be told about.

A TIFF whose compression carries state from one row to the next becomes several image XObjects, one per strip, because such a stream begins afresh in every strip and the strips therefore cannot be joined. Nothing about the calls changes — the picture is embedded, sized, placed, rotated and made transparent as any other, and it counts as one Figure or one artifact in a tagged document — but image info answers strips, the file carries that many image objects, and a placement writes that many cm/Do pairs inside its one q … Q. Uncompressed, PackBits and LZW files are one image however many strips they have, since those carry no state and are joined into a single stream. Two things such a stack cannot be, and both are refused with TCLPDF TIFF STACKED: another picture’s mask, and the wearer of one — /Mask and /SMask name a single image XObject, and a mask laid over a stack would be drawn over each part in turn instead of over the picture. Above 256 parts the picture is refused with TCLPDF TIFF STRIPS rather than stacked. The way out of all three is the same and is named in the message: re-save the file with a RowsPerStrip that holds the whole picture. -stencil 1 stays a PNG option — a bilevel TIFF is placed as the picture it is.

doc image place alias -at {x y} ?-width w? ?-height h? ?-size {w h}? ?-scale s? ?-fit {w h}? ?-fitMode contain|cover? ?-align left|center|right? ?-valign top|middle|bottom? ?-dpi n? ?-rotate deg? ?-opacity o? ?-inline bool? ?-alt text? ?-artifact bool?
Places an embedded image. Giving only one of width and height keeps the aspect ratio; -size sets both extents at once and keeps nothing. Without any of them the natural size applies, and -dpi decides it: a pixel is 1/dpi of an inch. The default is -dpi auto, which is the resolution the file itself states — a PNG pHYs chunk, a JPEG JFIF density, an Exif resolution, a TIFF XResolution with its ResolutionUnit — and 72 only where the file states none, which makes one pixel one point as before. A file that gives a pixel aspect ratio without a resolution has that ratio applied to the 72. -dpi 300 overrides whatever the file says and places a scan at the size it was scanned from. -dpi is a resolution the file can hold: a value that rounds to nothing as a PDF real would divide the natural size by zero and place the picture past what a PDF number holds, and it is refused. The distinction matters because a scan is not a screenshot: 1728 pixels at the 200 dpi of a fax line are 219 mm of paper, and read as 72 dpi the same picture comes out 610 mm wide, off the sheet in any format. -scale multiplies that natural size and yields to any explicit width, height or size. -width, -height, the two lengths of -size, -scale and -dpi are numbers above zero — zero would fold the picture onto a line and a negative one place it mirrored beyond its corner, so both are refused; image size refuses the same values. What it does not refuse is a value that is positive but so small that the placement matrix would be written as 0 0 0 0 x y cm: that line is drawn where the matrix is written, so it is image place alone that answers TCLPDF IMAGE SIZE there, and image size still gives the number the arithmetic yields. -at is exactly two numbers, -rotate a number of degrees. The same image placed five times is stored once.

-fit {w h} is the box the picture goes into with its proportions kept, and it is the one sizing option that cannot be worked out from the natural size alone: the box decides the factor, and -fitMode says which of its two edges decides it. contain (the default) takes the smaller factor, so the whole picture is inside the box and a band is left on the axis that had room to spare; cover takes the larger, so the whole box is covered and what hangs over it is clipped away — the box is what the caller asked to see, and the overhang would otherwise be drawn across whatever stands beside it. A covering placement declares the box as its bounding box in a tagged document, not the placement: ISO 32000-2, 14.8.5.4.3 asks for the rectangle that encloses the visible content, and three quarters of a covering picture is not visible. -fit and -size, -width, -height or -scale contradict each other and are refused together — both say how large the picture comes out, and one would have to win in silence. -dpi is not among them: it says how large a pixel is, which is what the proportions are made of, and those are what -fit keeps.

-align and -valign say where in that box the picture sits — the words this package already uses for the two axes, left, center (or centre) and right across, top, middle and bottom down. -at names the top left corner of the box, exactly as it names the top left corner of a rect. Without -fit the box has no size, and the same two options are the plain anchor: a logo whose right edge is to sit on the right margin is -at {190 20} -align right, and nothing has to be measured first. They are deliberately not called -anchor: text has an -anchor and it means baseline or top, one axis rather than a corner. -rotate turns the placement about the point -at names, so it cannot be combined with -fit or with an -align/-valign that moves the corner (TCLPDF FIT ROTATE): the turned picture leaves its upright box, and a covered box would be cut by a rectangle standing at an angle to it. Where both are wanted, image size takes -fit and answers the fitted size, and -size takes that.

-inline 1 writes the picture into the content stream instead of beside it (ISO 32000-2, 8.9.7): BI, an abbreviated dictionary, ID, the bytes, EI — no object number, no entry in the page’s resources, no line in the cross-reference table, and no way of using it twice, so a picture placed inline five times travels five times. That is its whole case and its whole cost, and it is why it is asked for rather than decided by size here: whether a picture occurs once is something the caller knows and the writer does not. The classic use is a one-bit stamp of a few dozen bytes placed with -stencil, painted in the fill colour then in force. At most 4096 bytes of image data — the line the standard itself names twice — and a larger picture is refused rather than quietly written as an XObject. Five things an inline picture cannot be, each refused by name: masked, transparent of its own accord (an alpha channel, a colour key, a partly transparent palette), ICC-tagged (8.9.7 admits no CIE-based colour space), a stack (a striped TIFF is several images and BI … EI is one), or over the line. Two things about the road are worth knowing and are not refusals. In a 1.x file there is no /L, so a reader has nothing to go by but the EI token — data that happen to contain those two bytes were read correctly by every reader measured, but the standard gives no guarantee, and a picture whose bytes are known to be awkward belongs in an XObject. And /L binds the version in both directions: a 1.x file that carries an inline picture cannot be raised to 2.0 afterwards, and a 2.0 file that wrote /L cannot be lowered. The dictionary is written in the abbreviations of Table 92 — /W, /H, /BPC, /CS /G, /F /Fl — and /L only in a 2.0 file, where the standard requires it. Measured with veraPDF: PDF/A-2B, PDF/A-3B and PDF/UA-1 all take an inline picture, and pdfimages -list reports it with [inline] where the object number would be.

doc image draw ?path? ?-data bytes? -at {x y} ?options? ?-alt text? ?-artifact bool?
Embeds and places in one call, for an image used once. It takes -data as well; with no file name to key the cache on, the bytes themselves are the key, so the same picture drawn twice still travels once. A checksum over them only picks the shelf; two blocks of bytes that land on the same one are told apart by comparing them in full, so no collision can make one picture stand in for another. The comparison is reached only once the checksums have already agreed, and it costs 0.2 ms for 5 MB — a cryptographic digest over the same bytes costs 2.8 s in pure Tcl. The options are those of image place, -alt and -artifact among them — what they do is described under Structure and accessibility.

doc image info alias / doc image size alias / doc image names
What the file is, its size, and the aliases embedded so far. image info answers type, path, bytes, width, height, bitDepth and icc — the size in bytes of the ICC profile that will travel with the picture, 0 when the file carries none or -icc 0 left it behind — for all three formats, and with them xResolution, yResolution, resolution, pixelAspect, space, alpha, orientation, stencil, mask and interpolate. Those last six are answered for every format alike, whatever the picture is; the ones after them are what a format adds of its own. The two resolutions are in dpi and are empty when the file states none — empty is not 72: 72 is what a placement falls back on, while empty is the file saying nothing, and the two must not be confused by a caller that lays out around a picture. resolution names where the number came from: pHYs, JFIF, Exif, XResolution or none. Exif is read for the resolution and for Orientation. The orientation is reported and not applied — the JPEG data passes into the file as it stands and this package decodes no pixels — so orientation answers what the file states, 1 meaning “the rows are as they are stored”; a caller who wants the turn asks for it with transform around the placement. A TIFF is different: there the rows are re-assembled here, and an orientation other than 1 is refused. A JPEG may state a resolution twice and state it differently, and then Exif wins — measured, a file whose JFIF header says 72 dpi and whose Exif block says 300 is placed at 300, and resolution answers Exif. The recurring shape in real files is exactly that one: an encoder’s default left standing in the JFIF header beside a figure somebody meant. A segment that gives only a ratio between the axes never displaces one that gives a measure, whichever of the two it is; resolution says which segment the answer came from, so a caller can see what was preferred. pixelAspect carries a ratio for the files that give one without a measure. space is the colour space the samples reach the file in — DeviceGray, DeviceRGB, DeviceCMYK or Indexed — and alpha says whether the file carries an alpha channel. A PNG adds colorType and transparency — how the picture’s transparency reaches the file: none, colourKey (a /Mask array, the picture still passed through) or softMask (an /SMask: an alpha channel, a palette with partial entries or with transparent entries in more than one run, or a 16-bit colour key). A JPEG adds components (1 grey, 3 RGB, 4 CMYK). A TIFF adds components, compression (the name of its strips’ compression: none, packBits, lzw, deflate, jpeg, ccittRle, ccittG3, ccittG4), strips and rowsPerStrip — the two that decide how the picture reaches the file, answered before it has been placed. resolution answers XResolution where the file states one; ResolutionUnit 1 is a bare ratio between the axes, so the two dpi fields stay empty and only pixelAspect carries it. stencil (1 for an image mask), mask (the alias named with -mask, or empty) and interpolate are answered for every picture, not only for one that is a mask or wears one; for a stencil, transparency answers stencil — its bits are the transparency, so neither a /Mask array nor an /SMask is written for it. -invert is the one option of image embed that image info does not report: what it does is a /Decode array in the file, and the file is where it can be read. image size is the size a placement would come out at, in the document unit, and takes the same sizing options as place — what a caller needs to lay out around a picture. image names lists the aliases.

Tables

doc table -at {x y} ?-width w? ?-head rows? -body rows ?-foot rows? ?options?
Draws a table and returns the y coordinate below it. -width is the width of the table in the document unit; without it the table takes the page width less twice the larger of its left edge and 10 — 170 mm for a table at -at {20 y} on A4. A row is a list of cells; a cell is a string, or a dictionary with text — or, since 1.6, runs — and any of colSpan, rowSpan, align, valign, direction, markup and style, the last holding style keys. align, valign, direction and markup may equally be written inside style; on the cell itself they win over column, section and theme.

direction is ltr or rtl and says which way that cell’s text runs — a property of the cell rather than of the table, because an invoice has a right-to-left description column and a left-to-right amount column on the same row. Under rtl the cell’s align is mirrored with it, so the default left sets the text flush with the right hand edge of the cell. See “Writing systems” for what a right-to-left line does and does not do.

hyphenate breaks the words of a cell where the patterns of a language allow it, and it is a style key for the same reason direction is: a table can carry a German description column beside an English note, and a language said once for the whole table could not say that. 0 is the default and means what it has always meant — a word wider than what is left of its line is split between two characters and no hyphen is set; 1 is the language the document declares with language; anything else is a language tag such as de-AT. Only the two literal values 0 and 1 are the switch, so no is the tag for Norwegian rather than a Tcl false, exactly as under text. The patterns have to be loaded first with ::tclpdf::hyphenate load — none are shipped with the package, which is a licence decision and not an oversight — and a language nobody loaded refuses the whole table by name, TCLPDF HYPHENATE LANGUAGE, before a cell is drawn.

What it is worth, measured: the sentence Die Betriebskostenabrechnung wird nach der Wohnflaeche umgelegt. in a 40 mm column of 9 pt Helvetica takes four lines without it, the second of them broken as Betriebskostenabrechnun / g because the compound is wider than the rest of that line, and three lines with it — 13.95 mm of row instead of 17.61. Read the other way round, those same three lines fit a column of 36.5 mm where 46.5 mm was needed before: ten millimetres of table width, for one word in the column description.

A broken cell brackets its break hyphen exactly as a text -width paragraph does: in a tagged document the hyphen sits in a Span with an empty ActualText (ISO 32000-1, 14.8.2.6), so extracting the cell gives Betriebskostenabrechnung and not Betriebskostenab-rechnung. The cell is still measured first and set line by line from what was measured — the line is not cut up here, it is handed to text whole with -breakHyphen, and the cutting happens inside one text object after the position of the whole line has been computed. So the drawn line begins at the same coordinate and is as wide as the measured one, for every alignment; a table that cut its own strings would lose the kerning across the break and set a line wider than the column it was measured for. An untagged table is written byte for byte as it was before.

Runs and markup in a cell, since 1.6. A cell carries what a paragraph carries under text -runs 1 and -markup: runs holds the list of {text options} pairs — a bold client name, a struck old price, an underlined term, a link — written by a program from its data, with every option a run takes there except heading, which makes a pair a paragraph of its own size. markup names the notation the cell’s text is written in, tags or markdown, with the vocabulary and the strictness of text -markup; it is a style key, so a column of descriptions says {markup markdown} once, a section or the whole table may say it, and a cell may say markup tags for an underline Markdown cannot spell or markup plain to be set as it stands — plain rather than an empty value, because on a cell an empty markup, like an empty align, means that the cell says nothing and the column’s word stands. The string always stays under text: {markup {markdown …}} written as a pair is refused by name (TCLPDF TABLE CELL markup), because markup would then mean two things. A text in which the notation finds nothing to mark is a plain cell — the figures of a decimal column under a table that says markup markdown once are set as figures, with their escapes resolved —, and so is what a -didParseCell hook hands back, checked like a cell that was written. A cell of runs is measured as it is drawn — every piece in its own face, so a bold word is wider and the row is as tall as the lines it breaks into, the automatic column width measures the pieces the same way, and hyphenate breaks inside a run as it breaks plain text, bracketing the hyphen in a tagged document exactly as above. Its first baseline is the baseline of the plain cell beside it, and align left, right and center, valign and the theme’s fill and rules mean what they mean for text. In a tagged document the cell stays a TD or TH with its content, marked as text -runs 1 marks a paragraph, and a repeated head of runs is a pagination artifact like a plain one. A list in a cell, since 1.7, is a list as in flowing text: item, start and continued mean in a pair what they mean under text -runs 1, written as runs or read from <ul>, <ol> and <li> or from - and 1. in the notation — each item a paragraph of the cell, indented by the same measure and labelled as there, breaking inside its indent, consecutive items of one kind one list, a pair without item ending it —, and the row grows with the lines the items break into, at the cell’s one leading. In a tagged document the TD or TH holds an L with an LI, Lbl and LBody per item. A column narrower than the indent of its list is refused as text refuses it, TCLPDF TEXT INDENT width, before anything is drawn. Refused, each by name and before anything is drawn: text and runs in one cell and a list of an odd length (TCLPDF TABLE CELL runs), runs beside a markup on the cell or in its style (… CELL markup), a value of markup outside plain, tags and markdown (TCLPDF TABLE STYLE markup), align decimal on a body cell of runs — a decimal column hangs one string on one separator, and a line of pieces has none (… CELL decimal); a head or foot cell of runs over such a column is set flush right as a plain heading is, unless its text holds a digit —, heading in a pair, whether written or read from <h1> or # in the notation, because a heading is a paragraph of its own size while the lines of a cell are one height (… CELL paragraph heading), and a right-to-left cell, refused by textLines in the words text uses (TCLPDF TEXT RUNS DIRECTION). A notation left open is refused as text -markup refuses it, with its code and position; nothing falls back to plain text in silence — a caller who wants that fallback tries the translation in -didParseCell and sets markup plain on the cell where it fails. didParseCell sees such a cell as it was written — runs or text with markup —, and the two draw hooks see it measured: the pairs under runs, for a markup cell the pairs the notation was translated into, and lines as textLines -runs 1 answers them, a list of pair lists — every line of a list item, the first and the ones it breaks into, carrying the item’s {item …} —, with hyphens one flag per line as before.

A row is as tall as its tallest cell. Where one cell carries running text over several lines and its neighbours hold a single line each, valign says where in the row those single lines sit — top by default, or middle or bottom. In a table whose rows are all one line it makes no difference, which is why the default is the one that leaves such a table alone.

Unknown keys are an error, in a cell, in a style, and in a column description — as they already were among the -options. A dictionary written over defaults otherwise refuses nothing: a mistyped key is carried along, read by nobody, and the caller sees the default and no message. The values are checked as well: align, valign and border outside the lists below, a negative padding or lineWidth, a leading of 0 are refused, as is a colSpan reaching over a column no row has a cell in (unless -columns describes it) and a rowSpan reaching past the last row of its section.

A cell is read as a dictionary when its first word is one of the cell keys and it has an even word count; anything else is the cell’s text. Tcl draws no line between a string and a dictionary, so this is a decision rather than a detection, and it leaves one ambiguous case: a plain string that begins with text, runs, markup, align, colSpan, rowSpan, valign, direction or style and happens to have an even word count is read as a dictionary. It then names the offending key rather than silently keeping a fragment of the sentence — and where every word is a cell key, so that there is no offending one to name, the cell is refused as TCLPDF TABLE CELL text for naming no text and no runs: {"align right" b} is a well formed cell dictionary with an empty cell in it, and a caller who wrote two words meant them as text. Write such a string as a cell dictionary — {text "text is set here"} — and it is unambiguous.

Column widths come in three kinds, resolved in that order: fixed ({width 34}), weighted ({weight 1}) and automatic — the rest is shared according to how wide the content actually is. Both keys take a number above zero; there is no percent width, a share of the table is what weight is for. A width of nought draws every cell of the column on top of its neighbour and a negative one starts the next column to the left of -at, so both are refused (TCLPDF TABLE COLUMN width, … weight) — the same sentence the implicit zero width below already carries. -width on the table itself is a number above zero for the same reason (TCLPDF TABLE ARGUMENT width). Fixed widths that add up to more than the table are refused, and so are fixed widths that use the table up while a further column has no width of its own — that column would come out zero wide.

-columns describes the columns, -theme picks striped — the default, so a table drawn without a theme is a striped one — or grid or plain — their lines and grey fills are single DeviceGray values, the one space every PDF/A output intent allows (a grey given as an RGB triplet was the one failed check of a PDF/A file with a CMYK intent); the only colour a theme brings is the blue head of striped, an RGB value that is right in every plain PDF and under the default sRGB intent, and that a document with a CMYK or grey output intent (pdfa -profile) overrides with -headStyle {fill {cmyk …}}, or the write is refused under pdfa, naming a rect on that page (the table draws its cells through rect) — and -style, -headStyle, -bodyStyle and -footStyle set fonts, colours and padding. -alternateFill colours every second body row — the stripe the striped theme brings, replaceable with any colour; the other themes have none. -minRowHeight is the least height a row may take, for rows whose content alone would leave them shallower. The border style key takes none, all, horizontal, vertical or outer; outer frames the block once per page instead of ruling every cell. -repeatHead and -repeatFoot carry those sections onto each page: the head is repeated (1) and the foot is not (0) unless said otherwise, because a head names the columns the reader is looking at while a foot is usually a total that belongs under the last row only. -horizontalBreak deals a table too wide for the page over further pages, with -repeatColumns keeping the leading columns on each. In a tagged document the groups are one table, not one per page: a row of the table is one TR whatever column group its cells were drawn in, so a reader is told the shape the caller wrote rather than one twice as long and half as wide (ISO 32000-1, 14.8.4.3.4). The cells of the later groups are hung on the row they belong to as the drawing reaches them, each carrying its own page; the columns -repeatColumns carries into every group are in the tree once, and their repeats are pagination artifacts like a repeated head or foot (14.8.2.2.2). A cell cannot span across a group boundary: a colSpan that reaches into another group is refused, and the message names the group (TCLPDF TABLE ARGUMENT colSpan) — break the span at the edge, carry the columns with -repeatColumns, or set -horizontalBreak 0. -repeatColumns has to leave at least one column that is not repeated, and the repeated columns together have to be narrower than -width; otherwise a group could never be closed and would be drawn past the edge of the table. -at takes two numbers, like every other placement of this package, and a third word is not dropped in silence.

-top and -bottom are the type area a breaking table works within, which is not the same thing as where it sits. -at says where this table starts on its first page; -top says where it resumes on every page after that, and -bottom how far down it may run. Both default to the type area of the page the table is on (page typeArea) — with -typeArea the margins the document was given, without it five percent of the page height, 14.85 mm and 282.15 mm on A4 — read again on every page it continues on, so a table that starts on a landscape page and runs on to portrait ones keeps each page’s own margins; and neither is taken from -at, because where a table happens to begin says nothing about where the page ends. Set -top to clear a running head, and -bottom to clear a footer. -top and -bottom are numbers; -bottom has to lie on the page and below -top, or the call is refused. A table is broken between rows and only there: a head, a row, or the rows a rowSpan ties together that — with the head and foot drawn around them on a page — do not fit between -top and -bottom are refused before anything is drawn, naming the heights and the band. A table whose head and first row (or head and foot, without body rows) do not fit below -at starts on the next page instead of leaving the head alone at the foot of the page, and the foot after the last row is kept inside -bottom the same way. -minRowHeight takes a height of 0 or more, -repeatColumns a whole number of 0 or more.

-align decimal lines the decimal separators of a column up under each other, measured across head, body and foot together; -decimal picks the separator, . by default. A cell without the separator is aligned as if it ended at the separator, so integers line up with the integer parts of the other cells — 100 stands under the 1234 of 1234.56, which is where a column of figures wants it — and a cell that is not a number at all sits there too.

Rows tied together by a rowSpan are never split across a page break: the whole group moves.

Style keys. A style is a dictionary of the keys below, and the same keys are read at every level: -style for the whole table, -headStyle, -bodyStyle and -footStyle for a section, a column description, and the style of a cell — laid over each other in that order, so the most specific wins. A theme is nothing but a set of these.

The defaults in the table below are the base a theme is laid over, not what a table comes out as — a theme is read before any of the five style options and changes some of them. The second table says what the three themes make of it, measured on a drawn table; where a theme is silent the base value below stands.

key values default
family what font -family takes helvetica
fontStyle what font -style takes: bold, italic, both, or empty empty
size the font size, in points 9
leading line spacing as a factor of the size 1.15
padding space inside the cell, in the document unit 1.5
fill the cell background — a colour, or empty for none empty
color the text colour black
align left, right, center or decimal left
valign top, middle or bottom top
direction ltr or rtl ltr
hyphenate 0, 1, or a language tag such as de-DE 0
markup plain, tags or markdown: how the cell’s text is read plain
border none, all, horizontal, vertical or outer horizontal
lineColor the colour of the rules {0.6 0.6 0.6}
lineWidth the width of the rules, in the document unit 0.1

What the themes make of that. Every key not named here keeps the value above.

key striped (the default) grid plain
border, everywhere horizontal, but none in the head all none
lineColor 0.76 0.47 {0.6 0.6 0.6}
lineWidth 0.1 0.15 0.1
head fill / color {0.22 0.28 0.40} / 1 0.86 / black none / black
head fontStyle bold bold bold
foot fill 0.91 0.86 none
foot fontStyle bold bold bold
-alternateFill 0.965 none none

A column description in -columns takes the same keys plus width and weight, described above — {width 20 align decimal} is a fixed column of amounts. A cell dictionary takes text or runs, colSpan, rowSpan, align, valign, direction, markup and style, the last holding any of the keys above.

Four hooks are called: -didParseCell once per cell before it is measured, -willDrawCell and -didDrawCell around drawing it, and -didDrawPage after each page. Each receives a dictionary and the document, in that order; returning 0 from willDrawCell skips that cell, and a dictionary returned from didParseCell replaces the cell.

The dictionary a cell hook receives is the cell as the table holds it: the cell keys above, filled in — text, runs, markup, colSpan, rowSpan, align, valign, direction, style — plus row, column and section (head, body or foot), which is what didParseCell sees; a measured cell also carries listed, 1 where its runs hold a list item. willDrawCell and didDrawCell see the measured cell on top of that: lines (the text as broken — for a cell of runs a list of pair lists, and runs then holds the pairs the cell was set from), hyphens (which of those lines end in a hyphen the breaker put there), width, height, leading, resolved (the assembled style), x and y (the top left corner on the page), spanHeight for a cell spanning rows, and decimal and tail for the separator and the room its tail takes. didDrawPage receives page, the index of the page just finished, and y, where the table stopped on it — except after a page turn that a horizontal break forced, where only page is known.

doc table layout ?same options?
Measures without drawing — what a caller needs to decide whether a table still fits.

doc table themes
The names of the built-in themes: plain, striped, grid.

Gradients and patterns

doc shading axial -at {x y} -size {w h} -colors list ?-angle deg? ?-from {x y}? ?-to {x y}?

doc shading radial -at {x y} -size {w h} -colors list ?-center {x y}? ?-radius r? ?-innerRadius r? ?-focus {x y}?
Draws a gradient directly — shading types 2 and 3, clipped to the rectangle of -at and -size, which both forms require. More than two colours are stitched together. (PDF 1.3.) -size takes two numbers above zero and above zero as the file writes them — five decimals, 7.3.3: a shading is an area, and a rectangle without one either clips everything away or leaves the shading’s own matrix singular, which paints nothing and says nothing. -size {0.000001 0.000001} is therefore refused where the option is read, for every kind of shading alike, rather than reaching the file as 0 0 re W n with the sh inside it.

For an axial gradient, -angle turns the run: 0 is left to right, counting clockwise, so 90 runs top to bottom; the end points are derived from the rectangle. -from and -to name the two end points directly instead and win over -angle — for a run that starts or ends inside the rectangle, or one that does not pass through its centre. They must name two different places: coincident end points paint nothing at all (ISO 32000-1, Table 79), the file is valid, and what a reader does with it is its own invention — measured with poppler, the whole clipped rectangle came out in the first colour. Such an axis is refused as TCLPDF SHADING AXIS length, and “different” means different in the file: two points a millionth of a millimetre apart are the same point once they are written.

A radial gradient runs from an inner to an outer circle. -center and -radius describe the outer one — by default the middle of the rectangle and half its longer side. -innerRadius (default 0) is the radius of the inner circle and -focus its centre, the same as -center unless given; moved off it, the highlight of a sphere sits away from the middle. Both radii are lengths of 0 or more.

-stops positions the colours: one value per colour, between 0 and 1 and strictly ascending in the file — the inner stops become the /Bounds of a stitching function and are written with five decimals (7.3.3), so two stops the file cannot tell apart are two equal stops and are refused, as is a count that does not match -colors. Only the inner values have an effect — the first and the last colour sit at the ends regardless — so it matters from three colours on. Without it the colours are spaced evenly.

-extend is a pair of booleans, {1 1} by default: whether the first and the last colour continue beyond the ends of the gradient or stop there. -at, -size, -from, -to, -center, -focus are exactly two numbers and -extend two booleans, checked before the shading is written.

shading axial and shading radial each take their own options: -angle, -from and -to belong to the axial gradient, -center, -radius, -innerRadius and -focus to the radial one, and naming one on the other is refused with the list of the options that kind has.

-matrix is accepted by every direct form — the two gradients and the five that follow, function, triangles, lattice, coons and tensor — and there it changes only how the coordinates are read: with it, -from, -to, -center, -focus and the radii are taken as they stand, in the space the matrix maps from, rather than converted from document coordinates — the gradient itself is painted in whatever transformation is active. Writing the matrix into the file is what the pattern form below does. The value is checked like every -matrix — six numbers, not singular — even though it is not written.

doc shading function -at {x y} -size {w h} -space gray|rgb|cmyk -expression body ?-domain {x0 x1 y0 y1}?
Shading type 1: the colour at a point is what a function returns for that point, rather than a run between stops. -expression is the body of a PostScript calculator function (ISO 32000-2, 7.10.5) without its outer braces, which are written here — x and y arrive on the stack in domain coordinates and what is left when it ends is the colour, one number per component. -space says how many that is, and is the same three families a gradient’s stops may be in. -domain is the rectangle the calculation is written over, {0 1 0 1} unless given, with x0 below x1 and y0 below y1; -at and -size say where that rectangle goes, and the /Matrix is derived from the two rather than asked for a second time — the domain’s y therefore runs up the page, the way a calculation expects it to. (PDF 1.3.)

Every word of the expression is held to Table 42 — a misspelt operator is refused naming it, and so is a brace that does not close, because a reader answers either by painting nothing at all. What cannot be decided by reading is whether the right number of values is left on the stack: if, ifelse and roll make that depend on the values. A function that leaves too few is a valid file and an empty rectangle; poppler reports Stack underflow in PostScript function and draws nothing.

doc shading triangles ?-at {x y} -size {w h}? -vertices list

doc shading lattice ?-at {x y} -size {w h}? -perRow n -vertices list

doc shading coons ?-at {x y} -size {w h}? -patches list

doc shading tensor ?-at {x y} -size {w h}? -patches list
The four mesh shadings, types 4 to 7 — the ones that carry their colours as points rather than deriving them. A mesh bounds itself: it paints only where its triangles or patches are, so unlike a gradient it needs no rectangle, and -at with -size is a clip where one is wanted. Half of a rectangle is refused. (PDF 1.3.)

A vertex is {x y colour} with an optional fourth word, the edge flag. triangles is the free-form mesh: flag 0 begins a triangle and shall be followed by two more vertices with flag 0, while 1 and 2 each add a single vertex to the triangle before them — 1 keeps its second and third corner, which makes a strip, and 2 keeps its first and third, which makes a fan. The flags are checked: four vertices with the flags left at 0 is the ordinary mistake, and it is the one no tool reports — poppler draws the first triangle and drops the fourth vertex, qpdf and veraPDF say nothing. It is refused here as TCLPDF SHADING TRIANGLE; a flag that is no flag is TCLPDF SHADING FLAG. lattice is the same vertices without flags, read as rows of -perRow each, two or more of them and two or more rows; a flag written on a lattice vertex is refused rather than ignored, because the type cannot express it.

The same gradient written twice costs one object, not two. Every PDF function this package writes goes through one place, and a function whose bytes are identical to one already in the file is referenced again instead of repeated — the device the ICC profiles already use. A table with the same sheen on twenty cells therefore carries one function rather than twenty, and two gradients that differ anywhere, down to the fifth decimal of a stop, keep their own. What a reader sees is unchanged; what changes is the number of objects, and with it every object number after the first repeat.

A patch is a dictionary {points {…} colors {…}}. coons takes twelve control points, tensor sixteen, and both take four colours, one per corner. The points run round the boundary from the first corner — four cubic Bezier edges with the corners shared — and the corner colours belong to points 1, 4, 7 and 10 in the order the boundary passes them. A tensor patch adds its four interior points last, in the order Table 86 gives them — p₁₁, p₁₂, p₂₂, p₂₁, running round the inside the same way the boundary runs round the outside; left where a Coons patch would put them they change nothing, and pulled away from there they shape the inside of the patch rather than letting it be interpolated. Only complete patches are written: the standard’s flags 1 to 3, which let a patch borrow an edge and two colours from the one before it, save bytes in the file and nothing in the drawing, and a caller who repeats the shared edge gets the same picture.

How a mesh reaches the file. The points are packed as a binary stream, sixteen bits per coordinate over the bounding box of the mesh itself and eight per colour component — every field a whole number of bytes. A vertex is eight bytes in RGB, a Coons patch sixty-one, a tensor patch seventy-seven, and the stream is deflated with everything else under -compress. The /Decode range is computed from the points that are there rather than from the page, because that is what the encoding resolution is spent on. All the colours of one mesh share one space, exactly as the stops of a gradient do: a grey corner among coloured ones is promoted, and mixing RGB with CMYK is refused (TCLPDF SHADING SPACE). A pattern colour is refused there too, and under the same class: a shading names its space by family in its dictionary, and Pattern is not one of the three a mesh or a gradient may paint in — where every stop is a pattern the refusal is TCLPDF SHADING PATTERN instead, which says the same thing about the whole call. A shading’s space counts for PDF/A like a painted colour (ISO 19005-2, 6.2.4.3) — measured with veraPDF: a document carrying all five of these types passes PDF/A-2B and PDF/A-3B with no failed check, and the same mesh under a CMYK output intent is refused at the write, naming the call.

doc shading pattern name type ?options? ?-matrix {a b c d e f}?
Registers a shading as a pattern, usable afterwards as {pattern name} in any fill. type is any of the seven — axial, radial, function, triangles, lattice, coons, tensor — and the options are the ones above.

-matrix maps the gradient into the page. A pattern is bound to the default space of the content stream that carries it — the page here, a form’s own space inside a form — and ignores whatever transformation is active when the shape is painted (ISO 32000-2, 8.7.2) — filled under a transform, the shape lands in the right place and the gradient inside it somewhere else. A caller drawing under a transform passes that transform here — six numbers, not singular, checked before the shading itself is written; the coordinates are then read in the space the matrix maps from. That includes the end points -angle derives from -at and -size: under a -matrix they are read where the matrix maps from, and there y grows upwards as it does everywhere in PDF space. A gradient placed without -matrix therefore belongs to the stream it was placed in: using it in any other content stream is refused — another page, another form, another tile, or the other way round. Pattern space belongs to the stream that carries the pattern (ISO 32000-2, 8.7.2), and two streams are two spaces whether or not they sit at the same level of nesting; the right matrix would otherwise depend on where the form is placed and a form may be placed more than once, which is the one thing PDF does not carry over (the note beside 8.7.2). Define the gradient where it is used, or pass -matrix and say which space the numbers are in.

doc shading names
The gradients registered so far, by name.

doc pattern create name -size {w h} ?-step {sx sy}? ?-unit u? ?-matrix {a b c d e f}? ?-origin {x y}? ?-script body?
A tiling pattern. Inside the script the tile is drawn like a small page, and every shape command works unchanged. -script may be left out, and the tile is then empty — a pattern that paints nothing, which is a thing a caller may want and is not refused. (PDF 1.2.)

-step is how far apart the tiles sit, the tile size unless given: equal, they touch; larger, and the background shows through between them — a sparse watermark rather than a hatch. Neither the size nor a step may be zero in the file (ISO 32000-1, Table 75, and 7.3.3 for what “in the file” means): a length below 0.00001 point is written as 0, so -step {0.000001 4} is refused at the call rather than written as /XStep 0. A step may be negative — Table 75 forbids zero, not the sign. page add inside the script is refused as well (TCLPDF PAGE CANVAS add): the script fills a content stream of its own and is on no page, so the new page would stay empty while the drawing went on into the tile. -unit reads -size and -step in another unit than the document’s.

Pattern space hangs off the content stream, not off the shape being filled: a tiling pattern is bound to the default space of the stream that carries it — the page, or a form’s own space inside a form — and ignores whatever transformation is active when the shape is painted (ISO 32000-2, 8.7.2). Without a matrix the tiles are laid from the page’s bottom left corner, and a rectangle anywhere else gets them cut at whatever phase falls on its edge. -origin anchors the tile grid at a point in document coordinates — the unit, y from the top, like -at — so a tile begins flush at that point; give the corner of the shape and the first tile sits square in it. -matrix takes the six numbers of a PDF matrix — exactly six, and not singular — and writes them as they stand: the raw pattern matrix (Table 75), points, origin at the bottom left, y upwards, for turning or scaling the tile or for a caller who already has the matrix. Both together are refused — a matrix carries its own translation. The matrix belongs to the pattern object, which is written once: the same hatch anchored at two corners is two patterns under two names. A tile anchored with -origin belongs to the stream it was created in and is refused in any other content stream — another page, another form, another tile — the same way a gradient is: pattern space belongs to the stream that carries the pattern, and two streams are two spaces whether or not they sit at the same level of nesting; a tile with neither -origin nor -matrix has no place of its own and crosses freely — it lays from the corner of whatever space it lands in, which is what a hatch is for. pattern size answers the tile in pattern space, unscaled — a matrix that doubles the tile does not double the answer.

doc pattern names / doc pattern size name
The registered tiling patterns, and the size of one of them as {w h} in the document unit, before any -matrix.

Reusable content

doc form create name -size {w h} ?-unit u? ?-script body?
Defines a form XObject — a drawing stored once and placed as often as wanted. Inside the script the origin is the form’s own top left corner and y counts downwards, the same way it does on a page. -unit reads -size in another unit than the document’s. -size is two lengths above zero as the file writes them — five decimals, 7.3.3 — checked before the script runs: a size below 0.00001 point reaches the /BBox as 0 (Table 95), and a bounding box with no inside clips away everything the script drew. -script may be left out, and the form is then empty — an object that places nothing, which is not refused. page add inside the script is refused (TCLPDF PAGE CANVAS add): the script fills a content stream of its own and is on no page, so the new page would stay empty while the drawing went on into the form; layer draw refuses the same call for the same reason. An annotation cannot be made inside the script either: annot and link are refused there (TCLPDF ANNOT PLACE form, TCLPDF LINK PLACE form), and so is a call inside pattern create — an annotation belongs to a page and its /Rect is in that page’s default user space (ISO 32000-2, 12.5.2), while the script draws in the form’s own, so the call used to be taken and put the remark at the form’s coordinates on whatever page was current. Place the annotation on the page after form place, where -at means what it says. A field is different and needs no rule: field works out where its widget lands at the write, against the page it names.

doc form place name -at {x y} ?-scale s? ?-fit {w h}? ?-fitMode contain|cover? ?-align left|center|right? ?-valign top|middle|bottom? ?-rotate deg? ?-opacity o? ?-alt text? ?-artifact bool?
Places it. -scale is a factor above zero, -rotate an angle in degrees, -at exactly two numbers; each is checked before anything is written. -rotate turns the placement about the point -at names — the top left corner of the upright placement — which is the same promise image place makes and the same point the refusal of a fitted box beside a -rotate speaks of. And the matrix the options compose to is held to what a raw -matrix is held to anywhere else in this package: one that comes out singular as the file writes it is refused (TCLPDF GEOMETRY MATRIX {form place}), so -scale 0.000001 is turned away rather than written as 0 0 0 0 e f cm, under which the form collapses onto a point with no way back to the page. -fit {w h} names a box instead of a factor and works out the factor from it, keeping the form’s proportions — contain (the default) fits the whole form inside the box, cover covers the whole box and clips what hangs over, and -align/-valign say where in the box the form sits (left and top by default). Without -fit the box has no size, and the same two options are the plain anchor — exactly as for a picture, and measured against it: a form whose right edge is to sit on the right margin is -at {190 20} -align right. One factor, never two: a form scaled unevenly is a distorted drawing, and neither mode asks for that. -fit and -scale contradict each other and are refused together, as are a fitted box and a -rotate that is not zero — a turned placement leaves the upright box it was fitted into. It is the same arithmetic image place uses and the same error codes, so a form and a picture given the same box and the same words come out in the same place. Placing is a transformation, not a redraw: the object stays one object in the file. Every form is written as an isolated transparency group (ISO 32000-1, 11.6.6), so -opacity fades the placement as one object: where shapes inside the form overlap, the one on top hides the one below at the given opacity, rather than each shape being faded on its own and the overlap coming out darker; and an opacity set inside the form applies within the group, so a translucent element there is faded by the placement too. The group names no colour space of its own: it composites in the colour space of the page it is placed on, which under PDF/A is the one the output intent describes. A group declaring DeviceRGB would count as a use of DeviceRGB, which ISO 19005-2 (6.2.4.3) allows only under an RGB output intent — so a document with a CMYK or grey intent (pdfa -profile) stays valid whatever profile it carries, and it makes no difference whether pdfa is called before or after form create. Documents set to a -version before 1.4 have no groups, and -opacity then applies per object.

doc form names / doc form size name
The names of the forms this document holds, and the size of one of them in the document unit — the created and the imported alike.

Layers

doc layer create name ?-title text? ?-visible bool? ?-intent View|Design?
Declares an optional content group (ISO 32000-2, 8.11) — what readers call a layer. -title is the name shown in a reader’s layer panel; without it the alias stands in, because a row a user cannot tell from the next is worse than a long name. -visible 0 starts the group off; the state belongs to the configuration rather than to the group, so layer state can change it after the drawing. -intent is View, Design or both (Table 96) — note that the default configuration considers only groups whose intent it shares, and its intent is View, so a group created with Design alone is not switched by it, which is what the standard prescribes. Optional content is PDF 1.5: a document set to an older -version is refused, naming the version it has.

doc layer draw name ?-script body?
Puts what the script draws into the group, bracketing it with /OC /name BDC … EMC. Layers nest, and so do their brackets; in a tagged document the layer bracket and the structure marks nest cleanly rather than crossing (14.6.1), and the bracket consumes no marked-content identifier of its own. The script runs in the frame that called it, so a document held in a local variable is in reach, and the bracket is closed even when the script fails — the error is then raised as it stands. An empty script writes nothing at all rather than an empty bracket. A bracket opens and closes in one content stream: a script that adds a page, or that leaves the stream it began in, is refused by name, and what it drew before is still closed properly. The script may draw anything, form place included — placing a form inside a bracket is how a reusable block is made optional.

doc layer names / doc layer state name ?bool? / doc layer radio {name name …}
The layers this document holds, in the order they were declared; whether one of them starts out visible, read with no value and set with one; and a set of layers that behaves like radio buttons, at most one of them on at a time (/RBGroups, Table 99) — two or more names, each a layer of this document, each named once. One language per layer is the usual case.

doc layer configure ?-title text? ?-listMode AllPages|VisiblePages?
The default configuration’s own settings: the name a reader may show for it, and whether its panel lists every group or only those a visible page uses. Read with no arguments. The name may not be empty — ISO 19005-2/-3, 6.9 requires one on every optional content configuration dictionary, and an empty one fails veraPDF’s check 6.9-1.

What is written is /OCProperties in the catalogue, with /OCGs listing every group in the file and a default configuration /D carrying /Name, /Order, /ON, and /OFF, /RBGroups and /ListMode where they apply. /AS and /BaseState are never written. /Order lists every group, which is what ISO 19005-2/-3, 6.9 asks of it — so a PDF/A document with layers stays conforming, measured with veraPDF 1.30: a PDF/A-3B document with three layers, one of them off, and a radio group over all three comes out isCompliant with 0 failed checks. A page taken over with pdf import that brought layers of its own keeps them: its groups are merged into the same entry and appear in /OCGs and /Order, however many imports and writes follow. That holds for a document whose only layers came in with an import — the configuration it gets is written the same way, /Name and /Order included, because a configuration dictionary without a name is what veraPDF’s check 6.9-1 fails and a group missing from /Order is one no reader offers to switch.

What is not offered is an /OC entry on an XObject or on an annotation. Bracketing the placement does the same for a form or a picture, and says it per placement rather than once for all of them; an annotation, though, cannot be put into a layer with this package — a link is not hidden by one. An annot, a link or a field written INSIDE layer draw -script is taken and lands outside the group: a layer is a bracket in the content stream and an annotation is not content, so nothing in the stream names it. Where a remark is to be switched with the layer, the way is /OC on the annotation, which this package does not write. (This is not the case of form create, where such a call is refused — there the coordinates would be wrong as well.) Alternate configurations (/Configs) and usage dictionaries are not written either: one default configuration is what a document written from a script needs, and /AS is forbidden under PDF/A anyway.

Importing a page from an existing PDF

doc pdf import alias path ?-page n?
Takes over one page of an existing PDF file as a form: the page is registered under alias exactly as form create registers one, and form place puts it down — as often as wanted, scaled, rotated, faded — while the page is stored once. Taking over a letterhead is one pdf import and one form place per page; examples/06.01-pdf-import.tcl shows it end to end. -page counts from 1; without it the first page comes. The page is stored once per alias, and what the page brings with it is stored once altogether: every imported object is shared where its content is the same, so importing one file twice — the front and the back of a letterhead — brings each embedded face, each picture and each ICC profile once. Measured on 02.01-embedding.pdf: one import 24 790 bytes, two imports 27 263 (the second form XObject and its own content stream), pdf fonts four either way — it was 48 831 bytes and eight faces before. Two things are copied rather than shared, each for a reason: anything standing on a reference cycle, since the content of each object on one holds the number of the next and there is no content to key on; and an optional content group or membership dictionary, since a group is a row in the reader’s layer panel and a thing this document keeps a state for, not an interchangeable value — two imports that each brought a layer are two layers the caller can switch apart.

What is read. Both cross-reference flavours of ISO 32000: the classic table (7.5.4) and, since PDF 1.5, cross-reference streams with object streams (7.5.7–7.5.8) — measured on a stock of real invoices, 29 of 32 use the stream flavour — hybrid files included, and incremental updates followed over /Prev, newest section first. The page’s inheritable attributes (/Resources, /MediaBox, /CropBox, /Rotate) are taken from the page tree where the page itself is silent. The form’s extent is the CropBox where one exists, intersected with the MediaBox (ISO 32000-2, 14.11.2.2) — that is what a viewer shows — and the MediaBox otherwise; a CropBox that does not intersect the MediaBox at all is refused, since nothing of the page would be visible; a /Rotate of 90, 180 or 270 becomes the form’s matrix, so the placement shows the page upright, the way a viewer shows it, with width and height swapped accordingly in form size.

What travels, and how. Everything reachable from the page’s resources is copied object by object and renumbered — fonts, images, ICC profiles, extended graphics states — with their bytes and their /Filter entries untouched: the import never decodes what it does not have to, so an exotic image filter is no obstacle. The copy walks PARSED objects rather than substituting text, so a literal string that happens to contain 5 0 R stays a string. Only the page’s CONTENT streams are decoded — they become one stream inside the form, written behind this document’s own -compress — which is why their filters must be ones this package can read: FlateDecode, LZWDecode, CCITTFaxDecode, ASCII85Decode, ASCIIHexDecode, or none; anything else is refused naming the filter. LZW was the general-purpose filter before PDF 1.2 and is read since 2026-08-21, /EarlyChange among its parameters — the same decoder reads an LZW-compressed scan, which is where it came from. ASCII85Decode and ASCIIHexDecode refuse what they cannot read rather than decoding it into rubbish: “Any other characters, and any character sequences that represent impossible combinations in the ASCII base-85 encoding, shall cause an error” (7.4.3), and 7.4.2 says the same of the hexadecimal filter. A z inside a group, a character outside ! to u, a group worth more than the four bytes it stands for, and anything in a hexadecimal stream that is neither a digit nor white space are named — where the bytes used to come out shifted or wrong and the resulting file passed every tool. CCITTFaxDecode is read since 2026-08-24 — Group 4 and both flavours of Group 3, with every parameter of Table 11 taken from the stream’s /DecodeParms: /Columns, /Rows, /K, /BlackIs1, /EncodedByteAlign, /EndOfLine, /EndOfBlock and /DamagedRowsBeforeError, and /Height standing in where /Rows is silent. The parameters decide everything, because the geometry is not in the fax data: a wrong /Columns does not fail but decodes rows of the wrong width, and /BlackIs1 is the one whose default is a trap — false means a 0 bit is black, so a stream read the other way round gives a perfectly well-formed negative of the page. Refused by name rather than guessed at: a two-dimensional extension code, a stream that ends before its rows are full, a row that codes more colour changes than its width can hold, and a code that is none — the last three tolerated up to /DamagedRowsBeforeError rows, after which the refusal names the row. Measured over 25 232 files on this machine: 1681 name the filter and all 11 398 fax images in them are Group 4, so that is the case the decoder was checked hardest on — 406 foreign streams, 570 megapixels, not one pixel different from libtiff and poppler. What this does not change is the writing side: a TIFF whose strips are Group 3 or Group 4 still travels into /CCITTFaxDecode untouched, since decoding a scan in order to re-encode it would cost the picture and gain nothing. Every optional content group the page reaches gets its catalogue counterpart in /OCProperties, so the layers stay configurable and keep the state the source file gave them: ISO 32000-2, 8.11.4.2 asks that entry for “all the optional content groups in the document”, and a group reaches a page by more roads than one — a /Properties entry, which a BDC bracket switches (8.11.3.2); the /OC of an image or a form XObject, which is how Illustrator and every CAD exporter write a layer (8.11.3.3); the /OC of an annotation; and an OCMD standing in front of any of them. All of them are objects of the copied closure, and that is where they are collected. A group that arrived in no configuration would be a group switched ON, since 8.11.4.3 gives /BaseState the default ON — so a layer the source file hides would have been drawn. Groups from several imports are merged into that one entry. The imported page carries the same transparency group a form built with form create carries, so form place -opacity fades it as a whole.

What does not travel is named here rather than left to be discovered: the source page’s /Annots — a letterhead’s links, notes and form fields are gone, since an annotation belongs to a page and the page has become a form; the source page’s own transparency group, which is replaced by the isolated one every form of this package carries; and /UserUnit, which is not read, so the form is as large as its box is in points. The foreign page’s structure marks are taken out as well: a tagged page carries /P << /MCID 0 >> BDC … EMC brackets whose numbers index the structure tree of the file they stand in, that tree does not travel, and ISO 32000-1, 14.7.4.2 makes such a sequence a content item only through the XObject’s own /StructParents, which the import writes none of — so what would be left is a mark pointing at nothing, inside the artifact or Figure bracket form place puts around the placement. Two things go, and the rule is the same for both: a bracket whose property list holds an /MCID — written out or standing in a named entry of the page’s /Properties — and a bracket tagged /Artifact, in either spelling (/Artifact BMC and /Artifact << … >> BDC). Both say something about the structure tree of the file the page came from, and that tree does not travel: what the page is HERE is said by the placement — an artifact under form place -artifact 1, a Figure under -alt. An /Artifact bracket left standing inside a Figure is content marked as artifact inside tagged content, which ISO 14289-1, 7.1 forbids and veraPDF names (7.1-1, 7.1-2). A /OC bracket stays, since it is what switches the layer inside it and dropping it would make a hidden layer permanently visible; a /Span << /Lang (de) >> stays, since it says something about the content rather than about a tree. Nothing else in the stream is touched, and a stream carrying no bracket at all is copied byte for byte.

A refused import leaves the document exactly as it was — every question that can turn a page away is asked before the first object number is handed out — every question, over the whole closure and not object by object: the strings of every copied object and the filter of every copied stream are asked about in a pass of their own, so a refusal at the fifth resource of a page leaves the four before it unreserved, and a pdf import the caller caught does not make the document unwritable later on. What is refused, naming the file: encrypted files (this package carries no decryption), a page number beyond the count (the message says how many pages the file has), a broken or truncated cross reference — startxref is looked for in the last 1024 bytes rather than at the very end of the file, since ISO 32000-2, 7.5.5 puts %%EOF on the last line and a mail gateway or a print spooler that appends a line of its own does not make the file unreadable; a file with no startxref in that window is refused as one that is not a PDF or is truncated, and the message says which window it looked in. Where the offset itself is wrong the refusal names the cross reference rather than the byte: “the cross-reference offset 404 points at no object — startxref or a /Prev names a byte that is not the beginning of one”. A classic table whose lines are not the twenty bytes 7.5.4 prescribes is refused at the first short line, naming the object it belongs to and how many entries were read before it — the drift used to be reported several entries later, at a line where nothing was wrong. Further: content filters the package cannot decode, a stream whose /Length does not end at the endstream keyword — a wrong length would silently corrupt the imported content —, a /Rotate that is not a multiple of 90 (an integer-valued real such as 90.0, found in defective files, is read as its integer), a page that has no /MediaBox, its own or inherited, since nothing then says how large the page is, and a /Contents entry pointing at an object that is not a stream, which is a page description that does not exist. Further still, a number that is not one: an integer is decimal digits with an optional sign and a real the same with one period (ISO 32000-2, 7.3.3), and the standard says in as many words that a writer “shall not use the PostScript language syntax for numbers with non-decimal radices (such as 16#FFFE) or in exponential format (such as 6.02E23)”. Such a token is refused where it is read, naming the token and the byte it stands at — because what is read is written back byte for byte, and a file taken over with /ca 1e3 in it used to leave this package again with /ca 1e3 in it, which qpdf --check reads as “unknown token while reading object; treating as string” and a viewer reads as no opacity at all. Nothing is repaired on the way: 1e3 is not turned into 1000, since that would decide quietly what a damaged file meant. The check reaches objects, not content streams — the bytes of a page description are copied through untouched, so an exponential number inside a stream travels with it. And an object stream whose header does not fit its stream (7.5.7, Table 16): /First has to lie inside the decoded stream, and /N cannot announce more objects than the bytes before /First can number — a pair is four bytes at the shortest. Both are numbers out of the file, and the work of reading the header used to follow them rather than the file: a four-hundred-byte file saying /N 20000000 spent 78 seconds and 3.4 GB before answering, and answered that the file was fine. Every one of the header’s numbers is now a non-negative integer whose offset stays inside the stream. The order of the offsets is not held against the file — 7.5.7 has them increasing, but a file with them out of order is readable and is read. And the header of an object stream is held against the cross-reference in the other direction as well: “the first integer in each pair shall represent the object number of a compressed object” (7.5.7), so the pair is the object’s name — a table that points object 4 at an index whose header says object 9 is refused by name, where it used to hand out object 9 under the name 4 and a page came out with the wrong font. Refusing beats guessing — a letterhead that is silently wrong reaches the recipient.

An object is named by its number AND its generation (ISO 32000-2, 7.3.10), and both are held against the cross-reference: a reference naming a generation the file does not have at that number is the null object, not the object that stands there — which is what qpdf and poppler answer about the same file. The same holds where the file has no entry for the number at all, or a free one: “An indirect reference to an undefined object shall not be considered an error by a PDF processor; it shall be treated as a reference to the null object”. Such a reference is copied as a reference to a null object of this document’s own, so the entry that held it keeps its key. Where the cross-reference and the object’s own N G obj header disagree about EITHER number, the file is refused naming both — a corrupt or forged table, and reading the wrong object there is the silent wrong answer this reader exists to avoid. update and sign add write a reference with the generation the file gives it: an update writes into the file’s own numbering, where 7 1 R has to stay 7 1 R.

Three things are read as the standard reads them rather than as they stand. A dictionary that names one key twice — which 7.3.7 forbids and files carry all the same — is read once and written once, the LAST occurrence counting, as qpdf and poppler read it; the copy used to carry both entries over, so this package answered a file that breaks 7.3.7 with a file of its own that breaks it. An end-of-line marker inside a literal string is one byte and that byte is LF, “irrespective of whether the end-of-line marker was a CARRIAGE RETURN (0Dh), a LINE FEED (0Ah), or both” (7.3.4.2), and an octal escape is one byte with its high-order overflow ignored, so \777 is 255. A whole number beyond ±2 147 483 647 is written back with a trailing point: Annex C.2 bounds an integer there and a real at about ±3.403 × 10³⁸, so the value is unchanged and the token becomes one every reader can hold — it is the rule this package applies to every number it writes itself.

A cross-reference entry of an unknown type is read as the null object rather than as a hole in the table: “In PDF 1.5 through PDF 2.0, only types 0, 1, and 2 are allowed. Any other value shall be interpreted as a reference to the null object, thus permitting new entry types to be defined in the future” (7.5.8.3). Such an entry shadows a live entry an older section carries for the same object, the same way a free entry does — the newest section decides. qpdf 12.4.0 hands out the older object there; this reader does not.

What the source file claims, the copy claims. The header of the file names the version it conforms to (ISO 32000-2, 7.5.2) and the catalogue’s /Version raises it from PDF 1.4 on (7.5.5); the higher of the two is entered as a floor of the document the page is copied into, in the same way every feature of this package enters one. The objects travel with their bytes untouched, so nothing else could see them: a 1.4 document used to take over a page carrying /BitsPerComponent 16 — PDF 1.5, Table 89 — and write %PDF-1.4 over it. The document is not raised behind the caller’s back, so a source that needs more than the document was created with is refused by name (TCLPDF VERSION), and the message names the version to create it with. A header naming a version no standard defines — 1.8, say — is passed over rather than turned into a refusal nobody can answer.

What the import does not judge. The imported page is taken as it is: pdfa and ua of this document vouch for what this document draws, not for what the foreign page contains. A document that claims conformance and imports foreign content is the caller’s responsibility — validate the result, exactly as make check does for the examples.

Continuing an existing file

::tclpdf::update open path
Answers a handle on a finished PDF file, to be continued by an incremental update (ISO 32000-2, 7.5.6): the changes are appended to the end of the file and every byte already in it stays exactly where it is. add, reserve/put and addStream write objects the file does not have yet, numbering upwards from its /Size; replace and replaceStream write a second copy of an object the file already carries, whose current body body hands back as the file states it; trailer sets an entry of the added trailer, id pins the file identifier, and write puts the original bytes and the appendix behind each other — without a path, continuing the file in place. The added cross-reference section carries entries only for what this update wrote, the added trailer carries every entry of the previous one except Prev plus a Prev of its own, and it ends in its own %%EOF.

The handle is a package command rather than a document method, and nothing on the way to a document pulls its module in: package require tclpdf::update first. Continuing a file is not something a document being written ever does. The one road that loads the module by itself is ::tclpdf::sign add, which is built on it — where a second signature has already been put on, the handle is there.

Why this exists is the second signature. A signature covers the bytes of the file it sits in, so signing an already signed document means appending rather than rewriting — there is no other way that leaves the first signature intact. Everything else the handle can do follows from that one purpose; this is not a PDF editor, and it cannot become one: the object graph of a foreign file is not in memory, and what the objects mean is exactly the knowledge a reader does not have.

The handle speaks the same vocabulary as the writer, deliberately: add, reserve, put, addStream, stream, ref, body and count mean here what they mean while a document is being written. A builder written against the one works on the other unchanged, which is why a signature does not need a second implementation of the objects it builds. ref number answers a reference with the generation the file gives that object, and generation 0 for a number this update handed out itself. A reference is a number and a generation together (7.3.10): written as 7 0 R for an object the file carries as 7 1 obj, it resolves to the null object, and an /Annots array built with it loses the annotation without a word — qpdf --show-object shows [ null … ] and reports no error. release is among them too: it gives a reserved number back after a refusal, filling it with the null object rather than handing it out again — whoever already points at it is the caller’s business, and ISO 32000-1, 7.3.9 makes a reference to a missing object and one to null the same thing. An update session takes nothing back from the file it appends to, but it hands out numbers of its own, and a refusal between reserve and put would otherwise leave its cross-reference naming an object that is not there.

The update is always written as a classic cross-reference table, also onto a file whose own cross-reference is a stream — and that is the common case, since most foreign files use one. The standard does not say what flavour a section must have given its predecessor’s, and the sentence that forbids xref and trailer (7.5.8.1) applies to files that use streams entirely, which a file with an appended table is no longer — a justification that is circular enough not to be leaned on. What the decision rests on is measurement: qpdf, pdfinfo, pdftotext and pyHanko all read such a file, a replaced object overrides even a compressed one inside an object stream, and a PDF/A document stays conformant. It also takes nothing away, where appending a stream would: a reader that cannot read cross-reference streams could not open the original either.

Refused by name rather than written wrong: a path that does not exist, names a directory, or cannot be read by this user — asked before the file is opened, so the answer is TCLPDF UPDATE FILE and not the ENOENT, EISDIR or EACCES of the open underneath; an encrypted file, whose strings and streams would have to be encrypted with its own key — the refusal is TCLPDF UPDATE ENCRYPTED and it says “is encrypted and is not updated”, where it used to come from the reader in the words of the import, under TCLPDF IMPORT ENCRYPTED, about a command the caller never called; an object at a generation other than 0; an object stream or a cross-reference stream as the target of replace, because the objects compressed inside the one would keep pointing at the copy no longer read and the other is the file’s own bookkeeping; and deleting an object — 7.5.6 provides for it, but nothing in an update says what still points at the object being dropped, and a dangling reference produces a file that opens with pieces missing.

/Info and the metadata stream are never rewritten, so a PDF/A or ZUGFeRD claim in the XMP survives an update untouched — measured on foreign tools, an update that writes fresh metadata without the pdfaid properties turns an archivable invoice into an ordinary PDF file. Of /ID, the permanent first string is kept and the changing second one is recomputed from what the update appends (14.4), which makes the same update repeatable byte for byte. A base file that carries no /ID at all gets none: the first string is supposed to be the one the file was created with, and inventing one would say this update knew what that was. id pins the pair where a caller does know it.

Reading a finished file

::tclpdf::pdf info path
What a finished PDF says about itself, as a dictionary — the questions pdfinfo answers, out of this package’s own parser. It is the reader pdf import uses, so nothing new is read and nothing outside has to be installed: no foreign program, no exit status to interpret, no output format to parse. A package command rather than a document method, beside ::tclpdf::update open and ::tclpdf::sign digest, and for the same reason: the subject is a finished file made by anyone, addressed by its path, while a document object is a file this package is about to write. The five ::tclpdf::pdf commands live in the module tclpdf::importInfo — that is the name to hand package require, and it is not the name of the ensemble, because the ensemble is the reader’s public face while the module is one of several the reader is built from. Nothing else loads it, so a script that only reads files begins with package require tclpdf and package require tclpdf::importInfo; examples/06.03-pdf-info.tcl does exactly that. Every key is always present, with an empty value where the file says nothing, so a field can be read without first asking whether it exists: path and bytes; version and catalogVersion; xref (table or stream); sections and revisions; id; encrypted and encryption; pages and size; info with the document information dictionary; xmp, pdfa and pdfua; tagged and form (none, AcroForm or XFA, where pdf fields answers what stands in the fields); outputIntents; attachments; signatures; certified.

certified says whether the file carries a certification signature: /Perms /DocMDP in the catalogue (12.8.4.2), answered as a dictionary of field — the signature field it sits in, empty where none of the form’s fields holds it — and p, the /P of the transform parameters of Table 257: 1 “no changes to the document shall be permitted”, 2 filling in forms and signing, 3 those and annotations. /P is optional and defaults to 2, which is what is reported where the parameters are silent. A file that is not certified answers the empty string. It is reported and not judged, like every other claim here: whether the certificate is trusted and whether the digest still matches is cryptography against an outside store.

revisions is not the number of sections. An incremental update appends its cross-reference section, so its offset is higher than the one its /Prev names; a linearized file carries a second section at the front, chaining forwards to the main one — two sections written in one go. So revisions is sections minus every step of the chain that runs forwards. Measured over 166 files, that forward step sits exactly where qpdf --check reports “File is linearized” and nowhere else. Whether a file is linearized is deliberately not claimed: that would need the hint tables checked against what they promise.

An encrypted file is answered by this command, not refused — the one place in this package where it is, and it is this command alone: pdf pages, pdf fonts, pdf metadata and pdf fields refuse an encrypted file by name, as pdf import and update open do, because each of the four would have to read cipher text. pdf import and update open have to refuse one, since they would build a document out of cipher text; an inventory is the caller for which “encrypted, standard handler, revision 6, AES-256” is the answer, and it is more than pdfinfo gives, which stops at “Command Line Error: Incorrect password”. What comes back is what the trailer and the cross-reference say — size, version, revisions, /ID, and encryption with filter, subfilter, v, r, length, p and method, where method is the crypt filter’s /CFM — the one /StmF and /StrF name, and where those name /Identity it is the /CFM of a filter nothing in the file uses —, and subfilter is the handler’s /SubFilter — the syntax a handler of its own defines for the rest of the dictionary (Table 20), which the standard handler has none of, so it is empty for every file this package writes and for most it reads. Every content-derived key stays empty, tagged and form included, which answer the empty string rather than 0 and none: those two words mean “read, and there is none”, and nothing was read. It stays empty because strings and streams are cipher text without the key, and /P is handed on as the number it is rather than decoded: from revision 6 on the authoritative copy of the permissions is the encrypted /Perms string, so decoded bits would state as fact what only decryption can confirm.

::tclpdf::pdf pages path / ::tclpdf::pdf fonts path / ::tclpdf::pdf metadata path
The three answers that cost more than the summary does, which is why they are separate calls — measured on a 62 MB catalogue of 220 pages, info takes 52 ms and fonts 1.6 seconds, because the latter walks every page and everything its resources reach.

pages answers one dictionary per page: number, mediabox, cropbox, rotate, and the width and height a viewer shows — the CropBox intersected with the MediaBox that bounds it (14.11.2.2), with the rotation applied, so a page turned by 90 is as wide as its box is high. pdfinfo prints the CropBox of page one and states the rotation separately; both numbers are here, with the raw boxes beside them.

fonts answers one dictionary per face: object, subtype, cid, basefont, subset, embedded, file (which of FontFile, FontFile2, FontFile3 or CharProcs carries the glyphs), encoding, differences and pages. The walk covers every page, the forms and patterns its resources reach, and the appearance streams of annotations — a face used only by a filled form field or a stamp is in the file just the same. Measured against pdffonts on a foreign form: it passes over the ZapfDingbats that all 35 checkbox appearances name, and that face is unembedded, so it is as missing on another machine as one in the page content would be. A face without /BaseFont, which a Type 3 font has none of, is named by its descriptor’s /FontName.

metadata answers the XMP packet as it stands in the file, or the empty string. It is handed out raw rather than parsed: reading XML properly means tdom, which this package requires only where a document builds a packet of its own. info scans the same packet for pdfaid:part, pdfaid:conformance and pdfuaid:part. Note what that is: a claim, not a conformance — veraPDF decides the latter over hundreds of rules.

Refused by name: an encrypted file — info is the only one of the five that answers one — a path that is not there, names a directory, or cannot be read by this user (all three TCLPDF IMPORT FILE), a file that carries no startxref, a broken or circular cross-reference chain, a stream whose filter this package cannot decode, a page that has no /MediaBox anywhere up its tree, and a page whose /Rotate is not a multiple of 90 — the same refusals the import makes, for the same reason. One case is answered rather than refused: a file whose /Root resolves to nothing is a file with no catalogue, and info answers it with an empty pages and an empty size beside the trailer facts it did read, because the point of an inventory is to say what a damaged file still holds. Deliberately not answered, because an honest answer would need more than reading: the text, words, images and colours of a page (that is rendering — pdftotext does it); whether a signature is valid (cryptography against a trust store — pdfsig does it, and what is reported here is the field, its claims, and whether /ByteRange reaches the last byte); whether the file really is PDF/A; and annotations other than the form widgets pdf fields reports, outlines, page labels and the structure tree, each a tree of its own.

::tclpdf::pdf fields path ?-xfa bool?
What stands in an interactive form (12.7), as a list of dictionaries — one per field, in the order the file lists them, a parent field before the children beneath it. It is the reading half of field, and it answers the one question a filled-in form is ever asked: what did the sender type. Until it existed that answer needed a second program — qpdf --json --json-key=acroform, or PDFBox behind a Java runtime — for a walk this reader was making anyway, since info reports the signatures out of the same tree. A file with no /AcroForm answers the empty list rather than an error.

type carries the words of the writing side, not the raw /FT names: text, check, radio, button, listbox, combo and signature, so what comes back as combo is what field combo writes and the two halves of the package speak one language. /FT alone does not say it in any case — three of those words are one /FT told apart by a bit of /Ff (Tables 229 and 233), which is why the flags are read before the type and not after it. A /FT this package does not build travels in the file’s own spelling; the words above are lower case, so a raw Tx, Btn or Ch is recognisable as one. A node that names no type at all, which 12.7.4.2 allows for one that only groups names, answers the empty string. signature is the one word with no subcommand behind it: such a field is written by sign, and what its value claims is answered by info under signatures.

Every key is always present, with an empty value where the file says nothing, as everywhere in this reader: name, the fully qualified name (12.7.4.2) — the partial names of the chain joined with periods; partial, the /T of this node alone; object, the object number of the field dictionary, empty where the field stands in the array rather than as a reference; type; value; default, which is /DV read exactly as value is; flags; options; selected; tooltip, the /TU that 14.9.3 makes the accessible name of the field; readonly and required, the two flags a caller asks after without wanting the whole list; pages; widgets; and appearance.

/V is a different data type in every field type, and a reader that treats them alike answers nonsense — Table 226 says only “(various)” and the types are in the sections below it. A text field holds a string (12.7.5.3). A check box and a radio set hold a name, the on state or the /Off that 12.7.5.2.3 reserves, which comes back without its slash and is the name the widget’s /AS points at. A choice field holds the displayed text, or an array of them where several are selected. A signature field holds a dictionary (12.7.5.5), which answers signed. A push button holds nothing at all — “this type of field retains no permanent value” (12.7.5.2.2) — so a value a file writes there anyway is not passed on. A check box with no /V answers empty rather than Off: the two mean the same thing to a reader, and every other key is empty where the file is silent.

A line break comes back as \n, whichever of the three spellings the file holds. ISO 32000-2, 12.7.4.3 spells a break inside a field value as a CARRIAGE RETURN and that is what field text writes; reading it back handed the CR out unchanged, so a script that wrote, read and compared saw a difference it never made. \r\n and a lone \r both come back as one \n, and the round trip through field text -value and pdf fields is therefore symmetrical. The file is unchanged: it holds (a\rb).

The export value of a choice field is not in /V — it is the first half of the matching /Opt entry and stands nowhere else in the field. options hands out /Opt as {export display} pairs, one per option, whether the file wrote the entry as one string or as the two-element array of Table 234; where it wrote one string the two halves are that string, which is what the standard means by it. selected answers the export values actually in force: out of /I, “the zero-based indices in the Opt array of the currently selected option items”, where the file writes one, and otherwise by matching /V against the display halves. /I wins where it is there, because it is the entry the standard invented for the case the other road cannot decide — two options showing the same text. A value matching no option travels unchanged: an editable combo box may hold whatever its user typed, and that is the value. For a check box and a radio button selected is /V itself without the reserved /Off, so an unticked box answers the empty list whether the file writes /Off or nothing.

flags is a list of names rather than a number — the set bits of /Ff in bit order, counted from one as the standard counts them: ReadOnly, Required and NoExport (Table 227), and the ones each type has of its own (Tables 229, 231 and 233). It is not the writing side’s table turned round, and the reason is bit 26: it is RichText on a text field and RadiosInUnison on a button, so one number has two names and only the field type says which. A bit no table names is reported as bit26 and the like rather than dropped — a foreign file that sets one is saying something.

A field is not a widget, and /T alone tells them apart. “A field dictionary that does not have a partial field name (T entry) of its own shall not be considered a field but simply a Widget annotation” (12.7.4.2) — so a radio set of four buttons is one record with four widgets, and a node that carries only a name is a record with none. /FT, /Ff, /V and /DV are inheritable down /Kids (Table 226) and are read as inherited, a parent naming the type while the kids carry the widgets. widgets holds one dictionary per widget annotation, in the order the file lists them: page, rect, state — its /AS — and appearance, whether it carries /AP /N. rect is the file’s own /Rect, in PDF units from the bottom left corner of the page, not the top-left -rect the writing side counts in: nothing here converts, the file being anyone’s. pages collects the pages those widgets sit on, ascending and without repeats, counted from 1 as pages counts them and not from 0 as -page does; a widget no page lists has no page and says so. appearance is 1 where every widget carries /AP /N, 0 where one does not, and empty where the field has no widget at all — a widget without one is drawn by nobody unless the file asks the reader to draw, which is what /NeedAppearances does and 12.7.3 deprecates. An invisible signature field answers 0 without anything being wrong with it, its rectangle being degenerate in both axes (12.5.2). Text comes back as Tcl characters either way it is written, PDFDocEncoding or UTF-16BE behind a byte-order mark (7.9.2.2), which is what a reader filling in a form writes.

Measured against two other readers over 65 documents and 703 fields — 56 foreign forms plus this package’s own examples: name, type, value, tooltip and the widget count agree with Apache PDFBox on every field but one kind, and that one is deliberate — where a check box or a radio set carries no /V, PDFBox answers Off and this answers empty; selected is empty in both readings. qpdf --json --json-key=acroform agrees on name, type and flags and differs in what it counts: it enumerates widgets, so a radio set of three buttons is three rows there and one record here; it therefore omits a field that has no widget, which this lists as PDFBox does; and its alternativename is read off the widget and falls back to the field’s name, so the /TU of a radio set is lost there and answered here.

Refused by name: an XFA form (Table 224, deprecated in PDF 2.0) unless -xfa 1 asks for it. Its data is an XML stream in a format defined outside ISO 32000, and the /AcroForm fields beneath it are a shadow copy that need not agree with it — so the file is refused rather than half answered, and the option says “the shadow copy is what I want”; info reports the form as XFA either way. And an encrypted file, as every read but info refuses one: a field name and a text value are strings, and strings are the one thing every security handler encrypts (7.6.2). Measured on a 40-bit RC4 form, /FT, /Ff and /AS come through — names and numbers are not encrypted — and every /T and /V is binary rubbish. Half an answer with no way to tell which half is why the refusal stands. examples/09.03-form-read.tcl reads a form it filled in itself, or any file named on its command line.

SVG

doc svg path -at {x y} ?-width w? ?-height h? ?-size {w h}? ?-scale s? ?-fit {w h}? ?-fitMode contain|cover? ?-align left|center|right? ?-valign top|middle|bottom? ?-opacity o? ?-alt text? ?-artifact bool?

doc svg -data markup -at {x y} ?same options?
Draws an SVG as real vectors — paths, shapes, groups, transforms, use, text and gradients become PDF operators, not a picture. The shapes follow the geometry rules of the format, not the numbers as they stand: a corner radius larger than half the side is capped to half (SVG 1.1, 9.2), rx and ry stand in for one another where only one is given, a radius of zero means a square corner and a circle of radius zero draws nothing at all (9.3), and a negative width, height or radius — which 9.2 and 9.3 call an error — is reported through svg info rather than drawn as its absolute value. Since a clipPath is made of the same shapes, the clipping region follows the same rules. The geometry rules go further than the shapes. A viewBox whose width or height is zero disables the drawing altogether (SVG 1.1, 7.7): nothing is written, and the rectangle answered is the empty one at -at. A negative extent there is an error and is reported through svg info. A stroke-width of zero paints no stroke at all (11.4) rather than the hairline 0 w would give in PDF (ISO 32000-2, 8.4.3.2), and a polyline or polygon with an odd number of coordinates is drawn up to the last complete point, with the loss reported as points (9.6/9.7). A path whose data run out in the middle of a command is drawn up to — but not including — that command (8.3.9), reported as path-error; the drawing is not lost over it, and neither is anything after it. Returns {x y width height} of what is visible — the rectangle a reader sees, which is what a caption goes under, and it is the rectangle under the transformation in force, so a drawing placed inside transform -translate reports where it actually is rather than where it would have been without the transform. Fitted rather than stretched, the drawing is smaller than the rectangle asked for and sits inside it; under slice or -fitMode cover it is larger and the rest is clipped away, and then the answer is the box it was cut back to. That is the same rectangle the Figure gets as its /BBox (ISO 32000-2, 14.8.5.4.3 asks for the rectangle enclosing the visible content), and the same one image place and form place answer for the same call. Without a size the drawing’s own dimensions apply; with one it is fitted, keeping the aspect ratio. -size gives both extents at once, -scale multiplies the drawing’s own size, -fit {w h} names a box and fits the drawing into it — contain (the default) puts the whole drawing inside, cover covers the whole box and clips what hangs over — and -opacity applies to the drawing as a whole. -width, -height, -size and -scale take values above zero; -at is exactly two numbers. opacity, fill-opacity and stroke-opacity are honoured; on a shape, opacity multiplies with the per-side values. The functional colour notation is read — rgb(255,136,0), rgb(100%,50%,0%) and rgba(), whose fourth value multiplies into the opacity of the side it paints (SVG 1.1, 11.13.1). A component that is a number but not a finite one is refused where it stands, with the error code TCLPDF SVG COLOUR and the attribute — fill or stroke — as its fourth word: NaN and Inf are doubles to Tcl and neither of them names a colour, and rgb(Inf,0,0) would otherwise clamp to a full channel the file never asked for. An opacity is different in kind and is not refused: neither NaN nor Inf is an SVG <number> (SVG 1.1, 4.2), so such a declaration is in error and the property falls back to its initial value of 1, the way any other unreadable opacity does. A group element (g, svg, a, switch, use) with opacity below one becomes a transparency group: its children composite among themselves first and the alpha applies once to the result, so overlapping children stay exactly as light as a single one. That needs PDF 1.4; below it the alpha is refused with the version in the message, and it is PDF/A-clean under every output intent.

-align and -valign say where in a -fit box the drawing sits. They default to center and middle, and that is not an arbitrary choice: it is what a drawing has always done — xMidYMid meet, the default of preserveAspectRatio — written out, so a script from before the option writes the same bytes. Only a caller who names an edge moves anything.

preserveAspectRatio is read, all ten alignments with meet and slice, and none. meet fits the drawing inside the rectangle, slice covers the rectangle and is clipped back to it, none stretches — the one spelling that turns a circle into an ellipse, and it does so because the file asked. The alignment says which edge is kept: Min the near one, Max the far one, Mid splits what is left over. A value in error is the default, as SVG 1.1 7.8 requires, and is reported through svg info — a drawing whose fitting differs from what it says is what reading the attribute prevents. The caller’s own options win over the file’s: a script that says -fitMode cover has asked for a covered box in this document, and a drawing carrying meet does not undo it. defer is accepted and passed over, since it applies to a use pointing at an image, which this package does not draw.

Lengths carry their units. px, pt, mm, cm, in, pc, em, ex and per cent are all read (SVG 1.1, 4.2 and 7.10). One user unit is one point here, so 50mm is 50 mm on the paper and svg size says so; a per cent is measured against the current viewport, which is the viewBox in force, and em and ex against the font-size at that element, whose initial value is 16. A unit this package does not know makes the declaration an error and the property falls back to its initial value, as CSS asks.

Viewports nest. An svg element below the root and a symbol reached through a use each establish a viewport of their own, mapped through their viewBox and preserveAspectRatio and clipped to it — overflow: hidden is the initial value for both (7.2, 7.7, 14.3.3). width and height on a use apply to exactly these two (5.6).

<switch> renders one child. The first direct child whose requiredFeatures, requiredExtensions and systemLanguage all evaluate to true is drawn and the others are bypassed (5.8). No feature string and no extension is claimed, so naming either is false; systemLanguage is held against the document’s own language, and against en where none is set.

display and visibility are both read. display:none — as an attribute or in style="", which wins (6.4) — leaves the element and its children out; it is not inherited. visibility is inherited and is read where the painting happens, so hidden or collapse on a group can be taken back by a child that says visible (11.5).

currentColor is a paint. It stands for the value of the color property, whose initial value is black (4.2, 11.3) — the spelling an icon uses to take the colour of its surroundings. !important and a /* comment */ are stripped from a style="" declaration rather than taken for part of its value (CSS 2.1, 6.4.2).

clip-rule is inherited, as SVG 1.1 6.2 makes it: a value on the clipPath itself, or on any element above it, is the default its shapes start from, and a shape’s own value beats it. clip-path and mask are honoured, each through the device PDF has for it. A clipPath becomes a clipping path: its shapes are written as subpaths of one path and clipped with W — or W* where clip-rule says evenodd — so several shapes make their union the window, which two clipping operators could not (a second one narrows the region, and PDF has no operator that widens one). A mask becomes a luminosity soft mask: the mask is drawn in greys and the grey value is the alpha, so a gradient in the mask fades the shape — something no single opacity value can do. Neither is an approximation and nothing is rasterised. That needs PDF 1.4 for the mask, and the same mask used twice is one resource. userSpaceOnUse only: clipPathUnits or maskUnits saying objectBoundingBox, a reference to nothing, a transform on a clipping shape, a shape the package cannot draw, and a mask that draws nothing are each reported through svg info and the element is then drawn whole — visible and complete beats invisible or wrongly cut, and the report says which happened. Filters are not drawn and are reported the same way. A mask is converted to grey with the coefficients SVG 1.1, 14.4 names — 0.2125 R + 0.7154 G + 0.0721 B — and not with the ones a PDF reader would apply to a DeviceRGB content in a DeviceGray group (ISO 32000-2, 8.6.5.4, which is 0.3/0.59/0.11): for a red mask the two differ by 42 %, and for a grey or white one, which is the common case, they agree. A mask that reaches itself has no rendering (5.6) — the reference is dropped and reported as mask-cycle — while a use that reaches itself is refused before anything is drawn (TCLPDF SVG REFERENCE cycle), because the reference graph of the use elements is walked before the first operator is written; a cycle that walk could not follow is caught in the drawing and reported as use-cycle, and a nesting deeper than 25 levels as use-depth. A drawing whose use elements unfold to more than 20 000 copies is refused as TCLPDF SVG LIMIT use: nesting them multiplies, and the limit is this package’s own, for availability.

-data takes the markup from a Tcl variable instead of a file, which is what a generator wants: whatever produces the SVG hands it over directly, with no temporary file in between. Everything else is the same, including -alt and -artifact — with a description the drawing becomes a Figure carrying it, without one an artifact, and -artifact 1 says that this is what was meant.

set markup "<svg xmlns=\"http://www.w3.org/2000/svg\"\
    width=\"40\" height=\"40\">\
    <circle cx=\"20\" cy=\"20\" r=\"18\" fill=\"$colour\"/></svg>"
$doc svg -data $markup -at {20 20} -width 12 -alt "Status: $state"

Note that svg size below takes a file name only; the size of markup in a variable is what drawing it returns.

What a gradient inherits and how it is measured. xlink:href (or href) carries the attributes as well as the stops: gradientUnits, gradientTransform, spreadMethod, the end points and the circle are taken from the referenced gradient wherever the element itself does not state them (13.2.4). Which meaning a number has is decided by gradientUnits alone and not by its magnitude (7.11) — under userSpaceOnUse every number is a length, under objectBoundingBox every number is a fraction, and a value outside 0..1 is legal there. A radial gradient in objectBoundingBox units is therefore an ellipse on a box that is not square (13.2.3). Offsets are clamped into 0..1 and into order the way 13.2.4 prescribes rather than refused; a gradient with exactly one stop is a solid fill in that stop’s colour, and only a gradient with no stop at all cannot be honoured. The bounding box a gradient is measured against is the true hull of the shape, curve extrema included, not the hull of the control points.

A fill naming a gradient works on text as it does on a shape; the gradient is measured against the line’s em box — the width it was measured at, by the face’s ascent and descent — rather than the outlines of the letters, which the text road does not hand back. Text in a drawing uses the same faces as text does. font-family is a comma-separated wish list and the first name that resolves wins; an embedded face resolves under the alias it was embedded as, so font-family="house" finds it after font embed house …. That settles three things at once: the drawing may use any character the face has rather than the 224 positions of WinAnsi — Łódź and Москва are ordinary labels, not errors —, it can be set in the house face, and it can be part of an archivable document, which embeds every font it finds. A <text> is a sequence. Its character data and its tspan children are set in document order, each tspan with its own painting properties and its own position; x and y are coordinate lists and the first value of each is honoured. fill="none" on text paints nothing. White space is collapsed as SVG 1.1, 10.15 asks — newlines dropped, tabs to spaces, runs of spaces to one, the ends trimmed — unless xml:space="preserve" says otherwise. Kerning and ligatures apply as they do elsewhere.

A list nobody can satisfy ends at helvetica rather than failing, which is what a drawing wants; in a PDF/A document that face is not embedded and write then refuses the document, so it is worth naming a face the document has. font-weight is read: bold, bolder, or a number, where 600 and up is bold — the line CSS 2.1 15.5 draws. A family with no bold cut of its own is reported through svg info rather than quietly coming out light. That is every embedded family, since an alias carries a single cut, and it is also symbol and zapfdingbats, which are the two standard families with one cut. A missing cut is not an error in a drawing: SVG 10.10 and CSS 2.1 15.5 make font-weight a wish, and a family with one cut satisfies it with that cut — the refusal font -style makes for the same pair does not reach a drawing. The way to a bold embedded face is to embed it under an alias and name that. The document’s own weight is never inherited by a drawing — a drawing must not come out bold because the running text around it happens to be. font-style is not read.

doc svg info
What the LAST drawing skipped — the elements the module does not draw. It reports on the document, not on a file: anything written after info is accepted and ignored, so svg info some.svg says nothing about some.svg.

Everything a drawing loses is counted, and the list is the answer to “what came out different from the file”: filter and its primitives, image, foreignObject and every other element this package does not draw; clipPathUnits, maskUnits, maskContentUnits and maskRegion in objectBoundingBox units; a dangling clip-path or mask, a transform on a clipping shape (clip-transform), a shape inside a clipPath this package cannot draw (clip-<name>), two clip-rule values that contradict each other, a mask that draws nothing, a mask-cycle, a use-cycle the reference walk could not see and a use nested deeper than 25 levels (use-depth); a paint naming a gradient that cannot be honoured (gradient); a negative width, height or radius (rect-size, <name>-radius); a family with no bold cut (font-weight); an unreadable preserveAspectRatio; a <style> block, a spreadMethod and a gradientTransform, none of which is built; a stop-opacity below one; a stop for which no room is left in the ramp (stop-offset); an odd coordinate count on a polyline (points); a path that stops at an error (path-error); and a negative viewBox or nested viewport (viewBox, viewport).

doc svg size path
The natural size of an SVG file.

Barcodes

tclpdf has no barcode encoder and does not need one. tzint, the Tcl binding for libzint, encodes into SVG, and svg -data draws that as real vectors — a MaxiCode keeps its hexagons and rings, and the digits under an EAN stay characters that can be copied. No temporary file is involved: the markup goes from one variable into the drawing.

tzint is not a dependency. It is a C extension and therefore platform bound; nothing in tclpdf requires it, and a document that never draws a barcode never notices it.

package require tzint
::tzint::Encode svg markup "1234567890128" -barcode ean13
$doc svg -data $markup -at {20 20} -height 16 -alt "EAN-13 1234567890128"

Two things about the encoder are worth knowing, because neither is obvious and both cost an afternoon:

The status is three-valued. 0 means silence, 1 to 4 are warnings with a perfectly good symbol, and only 5 and up mean nothing was produced. Code that tests for “not zero” throws usable barcodes away — and on an error the target variable is not cleared but left as it was, so code that looks at the variable instead of the status quietly draws the previous barcode again. Both were measured: a Euro sign is a warning for qrcode and an error for code128.

set rc [::tzint::Encode svg markup $data -barcode qrcode -stat info]
if {$rc >= 5} {
    error "no barcode: [dict get $info error]"
}

An EPC-QR (GiroCode) needs -eci 26. The dataset states its own character set in line 3, and without the option the encoder picks one itself — the symbol scans, but its encoding is not the one the data claims. -security 2 is the error correction level the specification asks for.

For an archivable document the clear text line needs an embedded face, and the encoder names the one it wants: tzint writes font-family="OCRB, monospace" into its markup, so embedding a face under the alias OCRB is enough — the digits are then set in real OCR-B, stay text rather than becoming picture, and the markup is not touched. Where that face is not to hand, -notext 1 leaves the line out and text sets it.

doc attach path ?-name n? ?-mime m? ?-description d? ?-relationship r? ?-date d? ?-compress 1?

doc attach -data bytes -name n -date d ?same options?
Attaches a file. -relationship is the /AFRelationship value — Alternative, Data, Source, Supplement or Unspecified, the last of them unless said otherwise. (PDF 1.3.)

-mime is the media type written as /Subtype, application/octet-stream unless given, and takes only type/subtype; -description goes into /Desc; -date is the modification date written to /Params /ModDate, a PDF date like info CreationDate takes — required, not optional: every attachment is an associated file here, the file specification carries /AFRelationship and the catalog lists it under /AF, and ISO 32000-2, 14.13.2 with Tables 44 and 45 requires /ModDate of one. Given a path, the file’s own modification time is used unless -date says otherwise, which is what the entry means; given -data there is no file to take a time from, so -date has to be there and the call is refused without it (TCLPDF ATTACH ARGUMENT date). The current clock is never stamped in: the same document written twice has to give the same bytes; -compress is on by default. -name may not contain / or \, and it carries no control character: it is a file name, offered by a reader as what to save the attachment as, and neither a NUL nor a line break is part of one. A name outside printable ASCII is written to /UF as text and to /F with such characters replaced by one _ per code point — one, not one per UTF-16 unit, so the fallback bytes are identical under Tcl 8.6 and 9. -data may be empty; it must be bytes (encode text first).

-data takes the bytes instead of a file — for an attachment that never was one. -name and -date are then required, because there is neither a file name to fall back on nor a file whose modification time could stand in; without either the call is refused, and it is refused before anything is recorded.

Names in the embedded file name tree are written in one encoding per document: as soon as one name needs UTF-16BE, every name of the tree is written that way. ISO 32000-2, 7.9.6 asks for exactly that — “any encoding of the keys may be used as long as it is self-consistent” — and a tree that mixed the two spellings was in byte order for one kind of reader and out of order for the other.

doc attachments
What has been attached.

doc link -at {x y} -size {w h} ?-url u? ?-page n? ?-structure name? ?-to {x y}? ?-zoom z? ?-tooltip t?
A link rectangle over an area of the page — to a URL, to a page of this document, or to a named structure element. It is drawn as nothing: the visible text is a separate call. (PDF 1.1.)

-page is the page index, counted from 0 as page current counts — not the number printed on the page. The page need not exist yet: a link on the first page may point at a contents page added last, and the target is resolved when the document is written — a page that never came is then reported, naming the link. A negative index is refused at once. bookmark -page works the same way.

-url is written in 7-bit ASCII as the format demands: whatever is outside it — an umlaut, a space — becomes %XX from its UTF-8 bytes, and a % already in the address is left alone, so an encoded URL is not encoded twice.

For a page destination, -to names the point to land on and -zoom the magnification the reader applies there (1 is 100 %, 0 the magnification the reader is already at, and below 0 there is none to mean — the same range initialView -zoom takes). Without -zoom the reader keeps the one it has. Without -to the whole page is fitted, and -zoom is then refused rather than dropped: it describes how close a reader comes to a place, and no place was given.

-structure takes the -name of a structure element and writes a structure destination (12.3.2.3), which names the element rather than a place on a page and therefore still lands on the right thing after the content above it has moved. PDF/UA-2 asks for internal targets to be written that way. It is written as a GoTo action carrying both the structure destination (/SD) and a page destination (/D) to the element’s first page — /XYZ at the top of its first content, or /Fit when no position is known — so a reader that does not understand structure destinations still lands on the right page. It needs a tagged document and a 2.0 file (ua -part 2 gives both). bookmark takes the same option.

One target per call. A link goes to a URL, to a page or to a named structure element, never to two: -url with -page, -to or -zoom, and -structure with -to or -zoom, are refused. Before, the call ranked them and dropped the loser without a word. -at and -size are two finite numbers each, and a width or height of zero or less is refused — a link is nothing but its rectangle, and one with no area is an annotation no pointer reaches and, under PDF/UA, one a reader announces and cannot follow. A link belongs to a page. Its rectangle is in the page’s default user space (ISO 32000-2, 12.5.2), which a content stream of its own does not have, so a link inside form create, pattern create or the page-number XObject is refused (TCLPDF LINK PLACE form): the rectangle used to be measured against the form’s height and then hung on the page, which put it wherever that arithmetic landed rather than over the form. Place the form first and lay the link over where it landed. (annot is refused there in the same words, as TCLPDF ANNOT PLACE form.)

-tooltip becomes the annotation’s Contents, which PDF/UA requires on every link (7.18.5): it is what a reader announces instead of just saying “link”. Under PDF/UA the link annotation must also sit inside a structure Link (part 2: Link or Reference).

doc bookmark title ?-page n? ?-at {x y}? ?-parent id? ?-open 1? ?-structure name?
Adds an outline entry and returns its id, which can be the -parent of further entries. -page counts from 0, as page current counts, and defaults to the current page; as with link -page, the page need not exist yet — the target is resolved when the document is written, and a page that never came is then reported, naming the bookmark; a negative index is refused at once. Bookmarks are turned into objects when the document is written. -open decides whether the entry shows its children unfolded, takes a boolean and is on by default; -open 0 collapses a branch until the reader asks for it. Each entry’s /Count is the number of descendants a reader shows for it, not the number of its children, and the root counts everything visible at all levels (ISO 32000-1 12.3.3). The catalog then carries /PageMode /UseOutlines, so a reader opens the outline pane — unless a catalogEntry PageMode was set already. As with link, a bookmark has one target: -structure together with -page or -at is refused rather than ranked.

doc bookmarks
The outline built so far.

doc destination page ?-to {x y}? ?-zoom z?
A destination in PDF syntax (ISO 32000-1 12.3.2) — where a link or a bookmark points. link -page and bookmark build theirs through it; calling it directly is for a place the package does not write itself, and catalogEntry is the standing one: catalogEntry OpenAction [$doc destination 1] makes a reader open the document on the second page. page counts from 0 as page current counts — not the number printed on the page.

-to and -zoom are link’s two options, because they end up here: without -to the whole page is fitted (/Fit) and -zoom does not apply; with -to {x y} — in the document unit, from the top left corner as every coordinate here — the reader lands on that point (/XYZ), and -zoom is the magnification it applies there, 1 being 100 %; without -zoom the reader keeps the one it has. -zoom is a magnification of 0 or more, 0 meaning the magnification the reader is already at (ISO 32000-2, 12.3.2.2, Table 151) — the same rule link and initialView keep.

For a page that exists the call returns the destination array itself; a page beyond the last is allowed and returns a reference to an object filled when the document is written — a link on the first page may point at a contents page added last. A page that never came is then reported — as the link or the bookmark that asked for it, and as a destination when this method was called directly. A negative or non-numeric page is refused at once.

Vertical writing

The CMap is the smaller half. -direction ttb writes /Identity-V where a horizontal line writes /Identity-H, as a second Type 0 font over the same descendant, the same descriptor and one embedded font file — the writing mode is a property of the CMap and not a second copy of the face. What actually decides whether the page is right is that the metric is a different one: horizontally a glyph advances by its width out of hmtx, vertically it advances by its height and hangs from a vertical origin, and neither number is in hmtx. They come from vhea/vmtx, or from VORG where an OpenType/CFF face carries one, and PDF wants them restated in the CID font as /W2 and /DW2. Measured in Noto Sans JP: U+3031, the vertical kana repeat mark, is 1000 units wide and 2000 tall — a column built from the horizontal advances puts it in half the room it needs, the glyph below overlaps it, and qpdf, veraPDF and pdftotext all call the file clean.

/DW2 is written at the standard’s own default, [880 -1000], and /W2 only for the glyphs that differ from it. That is not a saving of a few bytes: in Noto Sans JP 16 522 of 17 103 glyphs sit at the default origin and 17 096 advance the default height, so a document of ordinary Japanese writes no /W2 at all. An entry, where one is needed, is three numbers — the vertical displacement and the position vector, whose x is half the horizontal advance. Neither is written for a face nothing was set vertically with, so a document that never asks for a column is unchanged.

A face with no vertical metric is not refused. ISO 32000-2 defines the case and gives it a default of one em per glyph, so DejaVu Sans sets a column of square cells — the only defined answer — and the file says /DW2 and no /W2.

Some of the glyphs are other glyphs. A bracket, the ideographic comma and full stop and the ellipsis have vertical forms — the comma moves from the bottom left of its square to the top right, the brackets turn — and a face names them in the GSUB feature vert. They are a lookup and not a rotation: the horizontal glyph turned by ninety degrees gives a bracket leaning the wrong way. tclpdf applies vert, falling back to vrt2 for a face that has no vert. That inverts the preference the OpenType specification states, and it is measured rather than assumed: in Noto Sans JP vert holds 383 substitution rules and vrt2 holds 378, the 378 are the same rules with the same outputs, and the five only vert has include the fullwidth colon. The substitution does not reach the ToUnicode map — both the horizontal glyph and its vertical form map back to the character that was written, so a column extracts in the order it was set.

The columns are broken here, and placed by the caller. textLines $text -width $columnHeight -direction ttb breaks a vertical text into columns and returns them in reading order — -width is the column height for such a text, because what the breaker asks of every candidate is “how far does this reach” and that is answered along whichever axis the text writes. Placing them is a loop of one text call per column at its own -at, moving leftwards, which is also what decides that Japanese columns run right to left:

set x 180
foreach column [$doc textLines $text -width 120 -direction ttb] {
    $doc text $column -at [list $x 30] -direction ttb
    set x [expr {$x - 8}]
}

What is deliberately not built is the placing. text -width -direction ttb is refused, and the message names the two lines above as the way through. A horizontal block steps its lines down the page by the leading; a vertical one has to step its columns left by it — and the height limit with its remainder, the column balance, the flow around shapes and the table are all written along that one axis. Turning them is the second half of this subject rather than a flag, and half of it built would be worse than the loop. There is still no vertical table.

Notes, stamps and marked-up text

An annotation is a remark laid over a page rather than drawn into it. The link of the previous section and the widget of a form field are annotations too; this section holds the kinds that say something about a page — a sticky note, a rubber stamp, the four ways of marking a passage of text, the five geometry annotations, and a file clipped to the place it belongs. Each is a subcommand of annot, and each returns the object number of the annotation it wrote.

Who draws the picture decides everything else. A note, and a stamp given -name, are drawn by the reader from a fixed set of symbols — which is what makes a note look like the reader’s other notes, and which is also why a document claiming PDF/A refuses one: ISO 32000-2, Table 166 requires an appearance stream of every annotation, and veraPDF fails such a file under clause 6.3.3. The way out is -appearance, which hands the annotation a form built with form create; then it looks the same everywhere. The four text markups need none of that — the package draws their appearance itself, always and without being asked, because the marked rectangles and the colour are the whole picture.

doc annot note -at {x y} -contents text ?-icon n? ?-title t? ?-colour c? ?-open 1? ?-size {w h}? ?-appearance form? ?-opacity o? ?-date seconds?
A closed sticky note: a symbol on the page, and the text behind it. -contents is required — it is the note, and under PDF/UA it is the description a reader announces as well. -icon picks the symbol: Comment, Key, Note (the default), Help, NewParagraph, Paragraph or Insert (ISO 32000-2, Table 174); a name outside them is refused, because no reader draws one. -open 1 shows the note unfolded when the document is opened. -size defaults to the 20 by 20 points a reader draws its symbol in — the symbol does not scale with the rectangle, so the number decides only where it sits. The annotation carries /F 28: printed, and neither zoomed nor rotated with the page. (PDF 1.1.)

doc annot stamp -at {x y} -name n -size {w h} ?shared options?

doc annot stamp -at {x y} -appearance form ?-size {w h}? ?shared options?
A rubber stamp. -name is one of the fourteen a reader draws itself — Approved, Experimental, NotApproved, AsIs, Expired, NotForPublicRelease, Confidential, Final, Sold, Departmental, ForComment, TopSecret, Draft, ForPublicRelease (ISO 32000-1, Table 181) — and -size is then required, since there is nothing to derive one from. -appearance names a form built with form create, and its own size is used where -size is left out, so the numbers are not written in two places and cannot come to disagree. The two are exclusive: with an appearance present a reader never looks at /Name. (PDF 1.3.)

doc annot highlight | underline | strikeout | squiggly …
Marks a passage that is already on the page — highlighted, underlined, struck through, or underlined with a squiggle. What such an annotation consists of is its /QuadPoints, the rectangles of the marked text, and there are three ways to say where those are; one of them is required. (underline and strikeout are PDF 1.3; squiggly is PDF 1.4 because the standard says so, and highlight because its band is drawn under the Multiply blend mode, which is.)

-text string -at {x y} marks ONE line and repeats what the text call that drew it said — the same string and the same point, plus -align and -anchor where those were given. The width is textWidth’s, which is the width that was drawn; the height is the face’s ascender above the baseline plus its descender below it, so the band covers the capitals and the descenders and nothing else. The measurement is made in the font state then in force — family, style, size and stretch as font last set them — because that is what the road has to go by: a text call that carried its own -size or -family per call left the state untouched, so a line drawn that way is marked with -quads and the rectangles the caller already has.

-lines {…} -at {x y} marks a PARAGRAPH, and takes what textLines answered for it — the plain list, or the dictionaries of -hyphens 1; both are read. One quadrilateral per line, one leading apart, and one annotation for the whole passage, which is what /QuadPoints is an array for and what makes a reader announce the remark once rather than once per line. The leading is the text state’s own, which is what the block used; -leading overrides it for a block broken some other way.

-quads {{x y w h} …} takes the rectangles directly, counted from the top left corner as -at is everywhere else. It is the road for a passage this package did not set, for a table cell whose geometry the caller already has, for a justified block — whose lines were stretched after they were measured — and for text in a Type 3 face, which carries no descender to measure with. Each of those three refusals names this option.

The colour is /C and defaults to yellow for highlight and red for the other three; the appearance is drawn in the same colour, the highlight under the Multiply blend mode so that the words show through the band rather than disappearing under it.

Shared with the note and the stamp: -contents, the description; -title, the owner of the remark (/T, which a reader shows in the note’s title bar); -colour (or -color), which takes a grey, an RGB or a CMYK colour, those being the three /C has a spelling for; -opacity 0…1, which fades the whole annotation, appearance and all (/CA, PDF 1.4); -date seconds, a time as clock seconds answers, written to /M and /CreationDate — the first is PDF 1.1 and the second PDF 1.5 (Table 172), so the option needs a document written as PDF 1.5 or higher; without it an annotation carries no date at all, so that the same script writes the same bytes twice; and -appearance, which puts a form of one’s own in place of the picture that would otherwise be drawn — the last of them not for the five geometry annotations below, which do not take it: their picture follows from the geometry, and there is nothing to hand over.

doc annot line -from {x y} -to {x y} / annot square | circle -at {x y} -size {w h} / annot polygon | polyline -points {{x y} …} ?-colour c? ?-fill c? ?-width w? ?shared options?
The geometry annotations (12.5.6.7 to 12.5.6.9): a line, a rectangle, an ellipse and the two poly kinds, laid over the page as remarks. The difference from drawing the same shape is not the picture but what it is: a line drawn with line is content — it prints, and a reader can neither switch it off nor ask who put it there — while the same line as an annotation carries an author, a date and a description, and a reader lists it beside the other remarks. That is why the two exist side by side rather than one replacing the other.

The coordinates are the caller’s own, counted from the top left as everywhere else in this API; /L, /Vertices and /Rect count from the bottom left and the conversion happens where the numbers are written. -colour is the outline (/C, red by default, since a remark in the text colour does not read as a remark) and -fill the inside (/IC) — the two words the drawing methods already use. A line and a polyline have no inside, so an -fill on them writes no /IC. -width is the thickness, and /Rect is grown by half of it on every side: a line is drawn on its path, so half its thickness lies outside the points, and a reader that clips to /Rect would cut that half away.

circle is the standard’s word and it means an ellipse inscribed in the rectangle, not a circle — worth saying once, because the name misleads. The appearance is drawn without being asked for, unlike a note’s: the picture follows entirely from the geometry, the colours and the width, so there is nothing left for a reader to decide and nothing to refuse under a PDF/A claim. Square, circle and line are PDF 1.3; polygon and polyline are 1.5, and the version guard says so. -fill is younger than all of them: /IC is a PDF 1.4 entry (ISO 32000-2, Table 176 for a line, Table 177 for a square or a circle), so a document written as PDF 1.3 takes the shape and refuses the fill, naming 1.4.

Each kind takes its own geometry and no other’s. line reads -from and -to, square and circle read -at and -size, polygon and polyline read -points; the four an annotation does not read are refused by name, with the ones it does read listed. They used to be accepted and dropped in silence — annot line -from … -to … -at {5 5} -size {3 3} -points {{9 9}} wrote the line and said nothing about the rest — which is the one mistake this package refuses everywhere else, down to a misspelt key in a table cell. The shared options are shared, -appearance excepted as above.

NaN and Inf are refused where they are read as a measurement rather than where the arithmetic later trips over them: -at and -size of every kind, -opacity, -width of a geometry annotation and every number in -quads. Both are doubles as far as Tcl is concerned, both compare false against every bound written as a comparison, and both used to reach the arithmetic and come back in Tcl’s own words under ARITH DOMAIN or TCL VALUE DOUBLE NAN.

And a shape that would show nothing is refused too, under TCLPDF ANNOT RECT EMPTY. What counts as area differs by kind: a square and a circle are drawn into their box and need a width and a height above zero, while a line, a polyline and a polygon are drawn along their points and need only two points that differ — a horizontal line has no height and is perfectly visible. A -size with a negative number is refused by name, the two numbers counting to the right and downwards from -at as they do at rect. -width is refused where the file cannot carry it at either end: beyond about ±3.403e38 (Annex C.2), and below what five decimals can hold, since a /BS /W that rounds to zero is what Table 168 reads as no border at all. The same rule holds for the rectangle of every annotation: a -size that is positive and comes out as the empty /Rect once written is refused rather than written. A polygon takes at least three corners and a polyline at least two points: ISO 32000-2, 12.5.6.9 calls the one “a closed polygon on the page” and the other “a series of connected line segments”, and two corners close to a line drawn twice.

doc annot attachment -at {x y} -name n ?-icon Graph|Paperclip|PushPin|Tag? ?-appearance form? ?-size {w h}? ?shared options?
A file that travels with the document, sitting at a point on the page (12.5.6.15). -icon is the symbol a reader draws — Graph, Paperclip (the default), PushPin or Tag (Table 178). -name is the name an attach call gave the file, not a path — the annotation points at a file that is already embedded rather than embedding a second copy, and a name nothing was attached under is refused. What it adds over the document’s attachment list is where: an attachment reached through the list is a file somebody has to know is there, while this one is a paperclip at the paragraph it belongs to. Under ZUGFeRD it is neither — the invoice XML is reached through /AF and belongs to no page.

Without -appearance the picture is the reader’s icon, exactly as it is for annot note, and under a claim that requires an appearance the annotation is refused for the same reason a note is. -appearance names a form built with form create and is the way through; /Name is then not written at all, since /AP wins over it (Table 166) and a /Name nothing draws is a statement with no reader.

Under PDF/UA the caller does one thing and the package does the rest. -contents is required on every annotation and refused without it, naming the call — it is what a reader announces, and “annotation” is what it says otherwise (ISO 14289-1, 7.18.1). The structure element is the package’s: one Annot element per annotation, opened inside whatever element is open at the call, which is what puts it in reading order, with /StructParent in the annotation and the /OBJR back to it in the element. veraPDF checks both by name. An annotation written before tagged 1 stands outside the tree, and that is reported when the document is written.

Interactive form fields

A form field is one call: $doc field text customer -rect {20 40 80 8} -value "Erika Mustermann". What makes the result a form rather than a drawing is written by the package — an /AcroForm entry in the catalogue listing the fields, one widget annotation per field on the page it sits on, and an appearance stream per widget, which is the piece that decides whether a form is worth anything, since it is what a reader actually shows. Six types are provided, each a subcommand of field: text, check, radio, button, listbox and combo. Every one of them takes -rect {x y w h}, counted from the top left corner of the page in the unit of the document — as link -at with link -size and sign -rect count — and -page n, the page index counted from 0 as page current counts, defaulting to the page being drawn on. The page need not exist yet, but it has to exist when the document is written and is reported by name if it never came. A rectangle of no area is refused at the call; that it is four numbers a PDF can hold is checked there too. Where it lands is a question for the page, and therefore for the write: a rectangle sticking out over the page edge is allowed, one lying entirely beside the page is refused when the document is written, with both rectangles in the message — that is where a y counted from the bottom, and a -rect in millimetres on a document set in points, show up. (PDF 1.2.)

A widget on a page with a /Rotate turns with the page, and no /MK /R is written. /R (Table 192) is “the number of degrees by which the widget annotation shall be rotated counterclockwise relative to the page”, and it guides a reader that builds an appearance of its own; it does not touch the /AP this package draws. Since -rect counts in the page’s own unrotated space, as text -at and every other placement of this API do, an appearance drawn there and a /R of 0 say the same thing — the field turns with the page exactly as the text beside it does. Writing /R equal to the page’s rotation would set a regenerated value crosswise to the printed one instead of straightening it, and straightening the package’s own picture as well would leave a field upright inside a rectangle that is not. A form meant to be read turned is written on a page whose format is turned, not on one whose /Rotate is.

name is the partial field name (/T, ISO 32000-2, 12.7.4.2): the word an exported form, a script inside a reader and a filled document all address the field by, so it is the caller’s and not a generated one. It may hold no period — the fully qualified name is joined out of the partial names with periods between them, so a period inside one names a parent field that does not exist — and it holds no control character either, no NUL, tab or line break, since the name is what an exported form, a script inside a reader and field list all address the field by and all three read it as one line. It has to be free as well: a name already taken, by another field or by sign -field, is refused, since two fields under one name resolve to one and nothing reports the one that becomes unreachable. field list answers the names in the order they were declared, which is the order they go into /Fields.

/NeedAppearances is never written, not even as false. It is the flag that asks the reader to draw the fields instead, and ISO 32000-2, Table 224 marks it deprecated in PDF 2.0. Three reasons, and the first is enough: the standard says what the ingredients are (/DA, /DR, /MK, /Q) and nowhere how a reader is to combine them, so the result is unpredictable; a reader that is not interactive does not draw at all; and the flag fires field events on opening, which on a signed document is the last thing anyone wants. Every /AP is drawn by the package instead — which is also the only way a form passes PDF/A, Table 166 requiring an appearance dictionary on every annotation whose subtype is Widget and veraPDF failing a file without one in every part (clause 6.3.3). An absent key means false, and that is what a reader is told either way: qpdf --json --json-key=acroform over a file written here answers "needappearances": false with the key nowhere in it.

doc field text name -rect {x y w h} ?-page n? ?-value text? ?-default text? ?-maxlen n? ?-align left|center|right? ?-multiline bool? ?-readonly bool? ?-required bool? ?-noexport bool? ?-password bool? ?-family f? ?-style s? ?-size n? ?-color c? ?-border c? ?-borderWidth n? ?-background c? ?-tooltip text? ?-contents text? ?-label script?
A field to type into (/FT /Tx, 12.7.5.3). Returns the name, as every field type does. Field and widget are written as a single object, which 12.5.6.19 permits wherever a field has exactly one widget and which every type here but the radio set therefore uses. -value is what stands in it (/V) and -default what a reset button puts back (/DV); a line break in either needs -multiline 1, a single-line field showing one line, and it goes into the file as the carriage return the standard prescribes rather than as the line feed a Tcl script hands in. A line break is \n here whichever of the three spellings the caller wrote: \r\n and a lone \r are one break as well, settled at the call so that the multiline check, the length against -maxlen, the font and the picture all see one spelling. /V gets one carriage return of 12.7.4.3, where a Windows script’s \r\n used to become two and a reader showed an empty line nobody typed. -maxlen is the greatest number of characters the field takes (/MaxLen), 1 or more, and a value longer than it is refused rather than left for a reader to truncate at the first keystroke. -align is /Q — left, center or right; the default is left and is not written, a key that may only ever hold its own default being one to leave out. -readonly, -required and -noexport are the three flags of Table 227 that every type has; -multiline and -password two of Table 231.

-password refuses -value and -default. Table 231 says of the Password flag that a PDF processor shall “never store the value of the text field in the PDF file”, and /V is plain text in a file anyone can open with a text editor — so the value is refused rather than quietly dropped, a package whose refusals cannot be trusted being worse than one that says no. -password and -multiline cannot both be set either: the same table forbids the pair, a field that echoes nothing having no second line to echo it on.

-family, -style, -size and -color are the font the field is set in — the same four names font takes, falling back on field default where they are left out. They go into the field’s default appearance string (/DA, 12.7.4.3) and into the appearance the package draws, settled at the call so that the two cannot disagree; drawing them at the write instead read the document’s text state, and a field came out in the courier of an unrelated code block three paragraphs further down the page. Naming the font also registers it, which is what makes the /DA resolvable against /DR and an embedded face usable in a field — a /DA naming a font /DR does not have passes qpdf --check without a word and draws as nothing in a reader.

A field the recipient can fill in gets its face embedded WHOLE. ISO 32000-2, 12.7.4.3 makes /DA and /DR what a reader shall build an appearance stream from when the value of a variable-text field changes — so a text field or a choice field that is not -readonly promises an entry this document never wrote, and a subsetted face holds only the glyphs it did write. A reader then refuses the new value rather than the font: PDFBox answers setValue("Quiz") with “No glyph for U+0051 (Q) in font OVMOXL+DejaVuSans”, and no validator sees it. The package therefore clears the subset flag of the face a fillable field’s /DA names, exactly as fsType bit 8 clears it for a vendor who forbids subsetting, and the file carries the whole font program. The price is the file: measured on a document of one page and one text field in DejaVu Sans, 4 856 bytes with -readonly 1 and 384 013 with the field left fillable. A read-only field keeps its subset — it shows what was written and nothing else — and so does a push button, whose caption is fixed, and a check box, which draws a path rather than a glyph. Naming one of the standard fourteen costs nothing either way, since none of them is embedded. An explicit -subset 1 on font embed does not survive a fillable field naming the face: the field wins and the face is embedded whole all the same, without a message, because the two wishes cannot both be kept and the field is the later one. -size is a size in points above zero: a size of 0 is legal and means auto-size, which a package that draws the appearance itself cannot predict a reader’s redrawing of. The colour is grey, RGB or CMYK — /DA is read outside any content stream, so the name of a colour space resource would resolve against nothing.

-border, -borderWidth and -background are the frame and the fill. They are drawn into the appearance and written as /MK /BC and /BG (Table 192), with a /BS beside them: the stream is what a reader shows, /MK is what it rebuilds one from the moment a user types in the field, and without it a field with a drawn frame loses the frame at the first keystroke. A colour there is grey, RGB or CMYK for a second reason — Table 192 holds components rather than an operator and tells the space apart by counting them. -borderWidth is 0.4 mm unless said otherwise, and -borderWidth 0 is a field with no frame. -border itself is empty — no frame — for field text, listbox and combo, and black for check, radio and button, which are read as controls rather than as lines to write on. The value is drawn inside a /Tx BMC … EMC bracket and the frame outside it, which is not cosmetic: a reader setting a new value replaces the stream from that mark to the matching EMC, and the frame is not the value.

What the field shows has to be something the field’s own font can set. A field is drawn in the one face its /DA names (12.7.4.3) and there is no fallback inside an appearance stream, so a value the face has no code for — anything outside WinAnsiEncoding in one of the standard fourteen — is refused at the call, under TCLPDF FIELD GLYPH, with the same words the drawing would have used. It used to be taken and to fail at the write, inside the form the value is painted into, by which time the field could not be taken back: every further write of that document died in the same place. The rule reaches -value and -default here, field button -caption, and the display half of field listbox/field combo -options and their values; the export half of a choice field’s -options is not asked about, since it never leaves the file, and a value typed into an editable combo box is.

doc field check name -rect {x y w h} ?-page n? ?-checked bool? ?-default bool? ?-export name? ?-mark check|cross|circle|square? ?-markSize n? ?-color c? ?-border c? ?-borderWidth n? ?-background c? ?-readonly bool? ?-required bool? ?-noexport bool? ?-tooltip text? ?-contents text? ?-label script?
A check box — /FT /Btn with neither the Radio nor the Pushbutton flag set (12.7.5.2.3). -checked is the state it starts in, -default the state a reset puts it back into, and -export the name of the on state, Yes unless given. That name is what an exported form carries as the value of the field, so it is the caller’s word: a form whose boxes all export Yes says nothing about which box it was. Off is the one name the standard reserves, for the off state, and is refused as an export value. The value of a button is a name object rather than a text string, and /V and /AS are written from one source here, so the two cannot drift apart — a reader given two that disagree silently prefers /AS.

Both states are drawn, always. /AP /N of a check box is a subdictionary with one entry per state (12.5.5, Table 170), and leaving the off entry out is what makes the frame of an unticked box disappear: a reader “shall also attempt to provide reasonable behaviour (such as displaying nothing)” for a state it has no appearance for, and displaying nothing is exactly what happens. With both in the file a reader toggling the box switches /AS between two streams that are already there and regenerates nothing.

-mark is the shape drawn inside the box — check, cross, circle or square, check unless given — and -markSize its size in the unit of the document, fitted to the box where it is left out. The four are paths, not glyphs: the examples in 12.7.5.2.3 set the tick in ZapfDingbats, which would put a font into the file for four vector strokes, tie the mark to the glyph repertoire of one face and make a check box depend on the text topic. No /MK /CA is written for a check box or a radio button, for the same reason — it is the caption a reader builds a dynamic appearance from, and neither of the two ever needs one built.

doc field radio name -buttons {{value {x y w h} ?description? ?-page n? ?-label script? ?-contents text?} …} ?-page n? ?-value name? ?-default name? ?-unison bool? ?-notoggle bool? ?-mark check|cross|circle|square? ?-markSize n? ?-color c? ?-border c? ?-borderWidth n? ?-background c? ?-readonly bool? ?-required bool? ?-noexport bool? ?-tooltip text?
A set of radio buttons, which is one field with several widgets: the field carries /Kids and each button is a widget annotation beneath it (12.7.5.2.4). It cannot be built any other way — “a field dictionary that does not have a partial field name (T entry) of its own shall not be considered a field but simply a Widget annotation” (12.7.4.2) — so four radio buttons are four widgets and one field, the whole set is declared in one call, and field list and field state show one field however many buttons it has. A single switch that is on or off is field check.

-buttons is the set: one {value {x y w h} ?description? ?-page n? ?-label script? ?-contents text?} entry per button, in the order they are to stand in /Kids. The value is the name of that button’s on state and what an exported form carries when it is the one selected; the rectangle counts from the top left corner of the page like every other -rect; the third word is that button’s own accessible description, written as /Contents on its widget. After it the entry goes on in -name value pairs, the form every call of this package takes: -page n is the page that button sits on, the field’s own -page where it is left out; -label script draws that button’s visible caption; -contents text is the description by name. The dash is what tells an option from the description, so a description that itself begins with one — -- none of them -- — is given as -contents, and saying it both ways is refused, both being the one /Contents of that widget. -value and -default name one of those values — /V of a radio field is “a name object corresponding to the appearance state of whichever child field is currently in the on state” — and a value no button answers to is refused, naming the ones there are. It is called -buttons rather than -options because in this package “options” means the -name value pairs of a call, and because 12.7.5.2.4 calls them buttons.

-unison is the RadiosInUnison flag (Table 229, PDF 1.5), which makes buttons sharing an on-state name switch together; two buttons under one name without it are refused, being a set in which two buttons light up at once and nothing says why. -notoggle is NoToggleToOff — “exactly one radio button shall be selected at all times” — and it needs -value: a set written with the flag and nothing selected opens in the one state its own flag forbids, and the way out would be the click that turns a button off. The default -mark here is circle, and a circular mark gets a round frame, a dot in a square box being a check box drawn wrong.

A set may span pages. Each button carries its own -page, so a form that asks the same question on every sheet collects all the answers in one field: /Kids holds “an array of widget annotations representing the individual buttons in the set” and nothing there says they share a page. Buttons that repeat one question carry one on-state name and -unison 1 with it, so that ticking the box on one sheet ticks it on the rest. Measured with qpdf --json --json-key=acroform: one field, every button with a parent and an empty partialname — 12.7.4.2, a dictionary without a /T of its own “shall not be considered a field but simply a Widget annotation” — and pageposfrom1 counting the sheet each one landed on.

A radio field takes no -contents and no -label; its buttons take both. Each describes ONE widget and the field is the whole set — a /TU on a field with four widgets describes none of the four (ISO 14289-2, 8.10.2.4) — so a button’s description is the third word of its entry and its label is that entry’s -label. A -label script that draws nothing is not a label: the element is asked afterwards whether anything reached it, and an empty one counts as a missing description rather than passing a claim the recipient’s validator then refuses. The label is the caption seen beside the button, and it goes where 8.10.2.2 puts it: into a Lbl inside the Form structure element of that button and of no other, before the object reference, because the order of /K is the reading order. The script runs in the caller’s scope and draws with the caller’s own variables. A label for the group is a third thing: 8.10.2.2 wants that one “within the parent structure element that also contains … the Form structure element for each widget”, which is the element the caller opened, so it is drawn beside the field in a structure Lbl of its own.

A button’s label needs PDF 2.0, and so therefore does a labelled set. ISO 32000-1, Table 340 let a Form element hold only the object reference, and veraPDF holds a 1.7 file to it (rule 7.18.4-2); ISO 14289-1, 6.1 makes PDF/UA-1 a PDF 1.7 format, so ua -part 1 refuses a 2.0 document before a validator ever sees it. A UA-1 form therefore describes its buttons with the third word and a UA-2 form may label them: measured, the same labelled bytes pass verapdf -f ua2 with no findings and fail -f ua1 under 7.18.4-2. Take the label away again and UA-2 answers 8.10.2.3-1, “If a label for a widget annotation is not present, a Contents entry shall be provided to supply description and context for the widget”.

doc field button name -rect {x y w h} -caption text ?-page n? ?-action reset? ?-fields {name …}? ?-family f? ?-style s? ?-size n? ?-color c? ?-border c? ?-borderWidth n? ?-background c? ?-readonly bool? ?-tooltip text? ?-contents text? ?-label script?
A push button (the Pushbutton flag, 12.7.5.2.2). It has no -value, no -default and no -checked: such a field “shall not use the V and DV entries in the field dictionary”, “because this type of field retains no permanent value” — and no -required and no -noexport either, both of those speaking about a value it does not have. -caption is therefore the whole of what the button says and is required. It is drawn into the appearance and written as /MK /CA, with a /DA beside it, because a reader may well rebuild the appearance of a button and those two are what it has to work from. The four font options are the caption’s and are named as field text names them. /AP /N is a subdictionary here too, although a push button has one appearance and 12.5.5 would take the stream directly: veraPDF fails rule 6.4.1-1 over a widget whose /FT is Btn and whose /N is a stream, so the one entry is written under /Off and /AS goes with it.

-action reset is the one form action this package writes (/ResetForm, 12.7.6.3). It is the one that is purely declarative — it puts /V back to /DV and removes /V where there is no /DV — and needs neither a script nor a server. -fields names the fields it resets and leaves the rest alone; without -action it is refused, there being no action for the list to belong to. Every name in -fields is a field of this document, checked at the write rather than at the call, since a reset button may name a field declared after it. Table 242 calls the array “names of fields”; a name that is none is an action that does nothing, and /Fields [(nope)] used to go into the file without a word. The refusal is TCLPDF FIELD BUTTON FIELDS name and it lists the fields the document has.

Under a PDF/A claim the reset action is refused, and at the write. The profile admits no action on a widget annotation at all — veraPDF 6.3.3-3, “A Widget annotation dictionary shall not contain the A or AA keys” — and names ResetForm among the actions it forbids outright (6.5.1-1). The check falls at the write because a document declares its profile and its fields in either order, and it falls in both orders. An archived document is a record, and a record does not reset itself: the way out is a copy written without the button, which examples/09.02-form-application.tcl shows by asking for both on purpose, catching the refusal and printing it. A refused write leaves no file behind, so the second attempt may use the same name.

doc field listbox name -rect {x y w h} -options list ?-page n? ?-value v? ?-default v? ?-index {i …}? ?-multi bool? ?-top n? ?-highlight c? ?-sort bool? ?-commit bool? ?-align left|center|right? ?-readonly bool? ?-required bool? ?-noexport bool? ?font, frame and description options?

doc field combo name -rect {x y w h} -options list ?-editable bool? ?-spellcheck bool? ?and the same otherwise, without -multi, -top and -highlight?
The two shapes of a choice field (/FT /Ch, 12.7.5.4): a list box shows its options in rows, a combo box shows one line and opens a list. They are one dictionary with one flag bit between them — Table 233, bit 18 — which is why everything below is shared but for five options: -multi, -top and -highlight belong to the list box and -editable and -spellcheck to the combo box, and each is refused on the other, an option that exists and does nothing being worse than one that is refused. The subcommand is not called choice, although that is the family name in the standard, because it would name the family and deliver one of its two members; list was taken by the core.

-options is the /Opt array of Table 234, written in Tcl. An entry of one word is an option whose export value is the text it shows; an entry of two is {export display}. So a display text that is itself two words has to say so the way every nested list does, with a brace pair of its own: -options {{{very good}} good poor} offers three options, the first of them shown as “very good”, while -options {{1 {very good}} {2 good}} offers two with export values in front. Counting the words is the only rule that lets both forms stand in one list, and it is the rule the standard’s own array uses. At least one option is required — a choice field without /Opt is one where “no choices should be presented to the user” — and a field the user types free text into is field text.

/V holds the DISPLAYED text, not the export value. 12.7.5.4: V is “a text string representing the selected item, as given in the field dictionary’s Opt array … (For items represented in the Opt array by a two-element array, the name string is the second of the two array elements.)” The export value is the first element and appears nowhere else in the field dictionary. -value and -default name an option by whichever of the two the caller has — the export value is tried first, being the option’s identity — and what lands in /V is the display text either way. A word that answers for two options is refused, because the field dictionary could not say which of them was meant either.

-index names the selection by position instead, counting the options from zero, and is /I. It cannot stand with -value, both of them naming the selection, nor with -sort, which reorders the options before they are written so that a position counted in -options is not the position in the file. /I itself is written only where it says something /V cannot — a multiple selection, or two options that show the same text, where /V names both and neither — and left out where it would repeat what /V already says.

-sort really sorts. Table 233 addresses bit 20 to the writer in so many words: “This flag is intended for use by PDF writers, not by PDF readers. PDF readers shall display the options in the order in which they occur in the Opt array.” So -sort 1 sorts the array by display text and sets the flag afterwards; the flag set over an unsorted array achieves nothing at all, in any reader.

-multi is MultiSelect, a list box flag, and it is the one option that makes -value read its argument as a list: everywhere else one value is one option, whole, so “very good” is the option of that name and not the two options “very” and “good”. -top is /TI, the index of the first visible option, and -highlight the colour of the bar behind a selected row — a light blue unless given, which is what a reader paints. -editable is the Edit flag and the one thing that lets -value name a value the options do not offer; -spellcheck 0 is DoNotSpellCheck, which Table 233 admits only on an editable combo box, there being nothing to spell-check in a field the user cannot type in. -commit is CommitOnSelChange. Each of the three carries a version floor: MultiSelect and DoNotSpellCheck are PDF 1.4 and CommitOnSelChange is PDF 1.5 (Table 233), and a document written below one of them is refused naming it. -index writes /I, which is PDF 1.4 (Table 234) — and only where /V cannot say the selection on its own, which is a list box with -multi or a field whose options show the same text twice; a document older than 1.4 is refused only in those two cases, since in every other one no /I is written. The fonts, the frame, -align and the three flags of Table 227 are as field text takes them.

doc field list / doc field names / doc field state name / doc field default ?-family f? ?-style s? ?-size n? ?-color c?
The names of the fields this document holds, in the order they were declared; what was declared for one of them; and the font a field is set in when it names none. list and names are the same word twice. field state answers a dictionary — name, type, page, rect, tooltip, flags, contents, label and the type’s own data — without the object numbers and the write-time methods, which are a writer’s business and nothing a caller can act on. A field with several widgets carries widgets in place of rect, contents and label: one entry per button, each with its index, its own rect, page, contents, label and whatever the type put in entry. An unknown name is refused, naming the fields there are. All three speak about the document being written. What stands in a finished form — one this package wrote, or one somebody filled in and sent back — is answered by ::tclpdf::pdf fields, which reads the file by its path and names the type in the same words field takes; see “Reading a finished file”.

field default read with no arguments answers the current values: Helvetica at 10 points in black unless it was set, one of the fourteen standard faces, so that a document which declares a field embeds nothing it did not ask for. Set, it is both the fallback for every field that names no font and the /DA of the /AcroForm itself, which is inheritable — one call rather than one per field, since a form whose fields disagree about their type looks assembled from parts. The four values are checked at the call, by building the string they will produce: a family that does not resolve and a colour with no /DA spelling are refused there rather than inside a catalogue entry at the write.

The document-level /DA and /DR are written only where a field needs them. By 12.7.4.3 that is a field with variable text — a text field or a choice field — and nothing else. It is not cosmetic: the string names a font, naming a font registers it, and a form of nothing but buttons therefore used to put Helvetica into a document that had asked for no font at all — which under a PDF/A declaration is not a finding at the validator but a refusal at the write, for a font the caller never named. Under PDF/A a field’s font is embedded like any other, so a document claiming a profile gives its fields an embedded face with field default -family; examples/09.02-form-application.tcl writes the same form twice, once in Helvetica and once, as PDF/A-3B, in an embedded face.

Accessibility costs one call. $doc tagged 1 is the whole of what the caller does: every widget gets its own Form structure element, made at the call that declares the field and therefore standing where the field stands in reading order, with an object reference (/OBJR) to the annotation inside it and /StructParent on the annotation pointing back through the parent tree (14.7.5.4). It is one element per widget and not per field — ISO 14289-2, 8.10.1: “A Form structure element shall enclose at most one widget annotation … a Form structure element does not semantically equate to a form field” — so a radio set of four buttons is four elements and one field, which is also what veraPDF checks by name. /Tabs /S stands on every page that carries annotations. A field declared before tagged 1 gets none of it, and ua reports it by name; there is no other way for a widget to end up outside the tree.

-tooltip is not a tooltip under a UA claim; it is the name of the field for someone who cannot see it. It is written as /TU, which is a PDF 1.3 entry (Table 228) where the field itself is PDF 1.2, so a document written as PDF 1.2 takes the field and refuses the description. ISO 32000-2, 14.9.3 makes /TU the alternative name that “shall be used in place of the actual field name”, and PDF/UA requires it: ua refuses a document whose fields have none and names them, rather than filling in the field name — a form whose fields announce themselves as “iban” and “plz2” is the reason the entry exists at all. -contents describes ONE widget and is written as /Contents on the annotation; PDF/UA-2 asks for a label or a description on every widget (8.10.2.3) and says why the field’s /TU does not stand in once there are several (8.10.2.4), which is why the buttons of a radio set take theirs from -buttons. Under PDF/UA-1 the field’s /TU is enough, measured rather than assumed: veraPDF 1.30 passes a UA-1 form whose widgets carry neither.

-label script draws the widget’s label into the widget’s own Form element, as the Lbl element that ISO 14289-2, 8.10.2.2 puts there — “which shall in turn be a direct descendent of a Form structure element that also includes the object reference to the widget annotation”. The script runs in the scope of the field call, so the caller’s own variables are in reach, and it runs before the object reference is written, the order of /K being the reading order and a label read out after the control it names arriving too late. It needs a PDF 2.0 file and is refused below one: ISO 32000-1, Table 340 gave a Form “only one child: an object reference identifying the widget annotation”, and veraPDF still holds a 1.7 file to it (rule 7.18.4-2). The Role attribute of the PrintField class is not the way round that — 14.8.5.6 is explicit that those attributes are for non-interactive forms, so writing one to quiet a validator would be a false statement about the document. A PDF/UA-1 document describes its widgets with -contents instead, and a label for a whole radio group is a structure Lbl beside the field either way. A -label script that fails leaves no element behind. The Form element and its Lbl are opened before the script runs, so the script is the one thing about a field that cannot be checked first — and where it raises, both are taken back and the tree looks as if the field had never been declared. The exception is a script that had already DRAWN when it failed: the mark in the content stream names the Lbl, and a tree without its half of a mark is worse than a tree with an element too many, so there the element stays.

What is deliberately not built, named rather than half written: XFA (Table 224, deprecated in PDF 2.0, and forbidden outright by ISO 14289-2, 8.10.1) and with it /DS, /RV and the RichText flag, all three defined in the XFA format rather than in the PDF standard; ECMAScript actions, whose contents 12.6.4.17 defines in ISO/DIS 21757-1, another standard entirely — and therefore calculated fields, whose /C action is an ECMAScript action by Table 199; the submit-form and import-data actions (12.7.6.2, 12.7.6.4), which need a server and a reader’s file dialogue; the icon entries of Table 192 (/I, /RI, /IX, /IF, /TP) and the rollover and down captions /RC and /AC; and /NeedAppearances. A value that has to be computed is computed in the script and written as /V with its /AP drawn — which is what a document produced by a script can do, and what a script inside the document cannot be measured doing here.

A further field type is a module of its own. The six shipped types attach to the core through a contract written out at the head of field.tcl in the source distribution: register a subcommand, declare the field, answer at write time the dictionary pairs that are the type’s own — the list is exhaustive and a key outside it is refused by name — and draw the appearance into a canvas the size of the widget, where {0 0} is its top left corner and every drawing method works as it does on a page. /AcroForm, /Fields, /SigFlags, /DA, /DR, the page’s /Annots, every object number, the version floor and the whole structure tree stay the core’s, which is why a field type gets accessibility by declaring its field and nothing else. examples/09.01-form-fields.tcl shows the text field on its own and examples/09.02-form-application.tcl a whole form of all six types.

Metadata

doc info key ?value?
Reads or sets an entry of the information dictionary: the text keys Title, Author, Subject, Keywords, Creator, Producer; the two dates CreationDate and ModDate, which take a PDF date such as D:20260818120000+02'00' (ISO 32000-1, 7.9.4) and are refused otherwise. A date is held against the spelling this document’s version uses: ISO 32000-1, 7.9.4 writes the zone offset as +HH'mm' and ISO 32000-2 struck the closing apostrophe, so D:20260818120000+02'00' is a date in a 1.7 document and D:20260818120000+02'00 is one in a 2.0 document. A version raised afterwards — by ua -part 2, or by configure -version — is not a mistake and is not refused: info still answers what was set, and the file carries the moment in the spelling the file uses. The same rule holds for attach -date, sign -date and ::tclpdf::sign add -date, which reads the version out of the file’s own header. And a date without a time zone keeps the day and loses the time in the XMP packet, because 7.9.4 makes the offset optional while XMP (Part 1, 8.2.1.4) has no form for a time without one; writing Z would claim UTC and the local offset would claim the machine that made the run, and both are inventions; and Trapped, a name — True, False or Unknown (PDF 1.3). Each of the text keys goes into the XMP packet as well, under the property ISO 32000-1 Table 317 pairs it with: Title as dc:title, Author as dc:creator, Subject as dc:description, Keywords as pdf:Keywords, Creator — the application that made the document — as xmp:CreatorTool, and Producer — the writer, tclpdf unless replaced — as pdf:Producer. Without a Creator the creating tool in the packet is the producer. CreationDate and ModDate are mirrored into the packet as xmp:CreateDate and xmp:ModifyDate; Trapped is not — the predefined PDF schema of ISO 19005 has no pdf:Trapped, and veraPDF fails a PDF/A file that carries it.

A key this list does not name is taken and written all the same, and that is deliberate — it is the one option list in this package that is not closed. ISO 32000-1, 14.3.3 lets a producer put keys of its own into the information dictionary, real files are full of them (/Company, /SourceModified), and ::tclpdf::pdf info hands them back, so refusing to write what this package is willing to read would be a hole rather than a guard. Such a key goes into the dictionary as a text string under exactly the name it was given, is checked no further, and reaches no XMP property: only the eight keys above have one that ISO 32000-1, Table 317 pairs them with (Trapped, the ninth, deliberately has none). That is not a PDF/A problem — ISO 19005 asks the two to agree for the properties that are paired, and a key with no counterpart pairs with nothing; measured, a PDF/A-3B document carrying /Company passes veraPDF with 0 failed checks. Use xmpSchema to put a property of your own into the packet where the packet is where it belongs. The price of the openness is the usual one: a mistyped Titel is written as /Titel and nothing says so.

doc language ?tag?
The natural language of the document as an RFC 3066 tag — de, de-DE, en-GB. It goes into the catalog and is what lets a screen reader pronounce the text correctly. PDF/A-3a and PDF/UA require it. (PDF 1.4.)

doc metadata ?xml?
Reads or sets the XMP packet directly. Normally the package writes it: set by the caller it is kept as given — as text, answered back unchanged, and encoded to UTF-8 once when the file is written, so metadata [$doc metadata] is a no-op; otherwise the packet is rebuilt on every write from the document’s current title, language and declarations, so it never lags behind the Info dictionary. (PDF 1.4.)

A packet set with metadata replaces the built one entirely — a document that also claims pdfa, ua or zugferd has to carry the descriptions those claims write in that packet itself, or what they claim never reaches the file. That is the pdfaid and pdfuaid identification, and it is every whole description a topic contributes: the PDF/A extension schema container of ua and of pdfa extension, and the fx properties of zugferd, which a validator of the hybrid standards reads and veraPDF has no opinion about. It is checked when the file is written, and every schema the packet is missing is named at once: a description whose namespace is declared nowhere in the caller’s packet is refused with TCLPDF XMP CALLER, in both orders, rather than written as an output intent and an attachment that nobody’s XMP backs up. Nothing is merged into the packet — a property may stand in an XMP packet exactly once (XMP part 1, 6.1), and a caller who spells fx:ConformanceLevel or pdfaid:part themselves means it. A namespace that the packet declares without using it counts as carried: the declaration is the packet saying the schema is there.

doc xmpSchema prefix uri tags method
Registers a schema that writes an rdf:Description of its own into the XMP packet each time the packet is built — the mechanism by which PDF/A and PDF/UA put their identification there, open to an extension with properties of its own to declare. prefix and uri name the schema and tags lists every property name it may write; a property answered but not listed fails the write. method names a method of the document object — an extension adds one with oo::define, see doc/PLUGINS.md in the source distribution — that is called when the packet is built and answers the properties as a list of {kind tag value} entries: kind text writes the value as the element’s text, kind bag takes a list of tag-value pair lists and writes an rdf:Bag whose items carry them as resources. A method rather than the values themselves, because the values may change after registration — pdfa -part 3 can follow — and a callback reads the state as it is at write time, so a second write stays truthful. An empty answer omits the description. Registration order is packet order, and registering a prefix again replaces its entry rather than adding a second one. A tag in tags that is not a valid XML name is refused at the call, naming the tag and the schema. The prefix is held to the same rule as the tags and to two more: it is an XML name without a colon, and it is neither xml nor xmlns, which Namespaces in XML 1.0, 2.1 and 3 reserve. The namespace URI may not be empty — xmlns:my="" is an error in XML 1.0 with namespaces, 6.2 — and carries only characters XML allows. All of it is refused at the call, so a bad prefix no longer surfaces as a parser message out of the write. Returns nothing.

Both commands live in the tclpdf::xmp module, which the core loads on first use like every other topic — no package require of its own. It needs tdom, as the packet does.

doc xmpRaw xml
Adds ready-made XML to the packet, for what does not fit the tag-and-value form of xmpSchema — parsed at the call, so that XML which is not well formed is refused there rather than at every later write, leaving the document unwritable with no way to take the contribution back: whole rdf:Description elements, appended under the packet’s rdf:RDF as given — a PDF/A extension schema is a page of RDF no accessor could usefully model, and pdfa extension rides on this call. The rdf prefix may be used without declaring it, since the element around it always has; every other namespace the fragment must declare itself. The XML is parsed when the packet is built, and a malformed contribution fails the write naming itself rather than travelling on to a validator. The packet is built once a schema is registered or a raw contribution is added — every conformance claim registers a schema, and a raw contribution alone builds the packet just the same, since a document needs no conformance claim to carry metadata. It is parsed at the call and held to the shape RDF/XML gives it: what a contribution may hold is rdf:Description elements, with comments and whitespace between them. Bare text and a foreign element are refused (TCLPDF XMP RDF) — they are appended to the packet’s own rdf:RDF, where XMP part 1, 7.9.2.2 admits nothing else. The values are not checked: a pdfaProperty:valueType nobody knows is the caller’s statement and goes in as it stands. Returns nothing.

doc catalogEntry key ?value?
An entry of the document catalog, for anything the package does not offer by name.

Structure and accessibility

doc tagged ?0|1?
Whether the document writes a structure tree. Off by default, and it has to be set before anything is drawn — once a page has content, tagged 1 is refused: the brackets go into the content stream as it is written. (PDF 1.4.)

A tagged document carries a second, invisible layer saying what the marks on a page are — a heading, a paragraph, a table cell — rather than how they look. The drawing does not change. Reading software needs it: without a tree it follows the order the content stream happens to have, which on a two column page runs across both columns. PDF/UA and PDF/A level A require it.

doc structure type ?-name name? ?-id string? ?-ref names? ?-alt text? ?-lang tag? ?-title text? ?-actualText text? ?-expansion text? ?-scope side? ?-numbering style? ?-bbox {x y w h}? ?-colSpan n? ?-rowSpan n? -script body
Opens a structure element, runs body with it open and closes it again — including when the body fails, so a half open tree cannot reach the file. A body that fails is undone as well: the element, the children it managed to open and the names and ids they claimed all go, so a refusal the caller traps leaves the tree exactly as it was. The one boundary the undo cannot cross is the content stream. A mark is two halves that meet by number (ISO 32000-1, 14.7) — the BDC bracket in the page and a leaf in the tree — and what a page already carries cannot be taken back, so a bracket whose body drew something before it failed stays, with what it drew; the same holds for an element an annotation joined, whose /StructParent already names an entry in the parent tree, and for one a structure destination points at. Nothing marked, nothing annotated, nothing pointed at: the call never happened. Returns whatever the body returned. type is one of the standard types of ISO 32000-1 14.8.4; an unknown one is refused at the call rather than in a validator later.

Elements nest by nesting the calls. Grouping types — Sect, Div, L, LI, Table, TR and their kin — do not hold content themselves: text drawn inside an open Sect becomes a P within it, which is what the nesting rules ask for.

Most documents need few of these. A table knows it is a table and a paragraph knows it is a paragraph, so those tag themselves; structure is for the grouping a writer cannot infer.

An empty -tag is refused where nothing would come of it. Inside an open leaf it means the same as no tag at all and the content is marked on that leaf; where no leaf is open there is nothing to mark it with, and content that reaches the page unmarked is invisible to a screen reader while every validator passes the file. Every other unusable value — an unknown type, a type that may not stand there — is refused by name, and the empty string is no different. What “may not stand there” covers is the strict part of ISO/TS 32005 Table 5: what a Table, a list, a Ruby or a Warichu holds; a block inside a leaf; inline markup with nothing around it; a block inside inline markup (a Table in a Link); a Caption outside the table, list or illustration it captions; an Art inside an Art; a Document below the root. The rest of the table is the validator’s. An artifact kind behind an ordinary tag (-tag {P Pagination}) is refused too: only Artifact and auto read a second word, and it used to be dropped in silence.

What is derived, and what has to be said. text becomes a P, and one call is one element however many lines it breaks into. table becomes a Table with TR, TH and TD, and the fill and rules of its cells become artifacts. image becomes an artifact unless -alt describes it, and then a Figure carrying that description — or unless it is placed inside an open Figure (structure Figure -alt … -script), whose content it then is: the Figure’s -alt covers it, and neither -alt nor -artifact is needed. Inside a form or a pattern -alt on an image, drawing or form placement opens no element; the invocation’s -alt describes it.

-alt appears on image place, image draw, form place and svg for the same reason and with the same effect: with it the drawing becomes a Figure carrying that description, without it an artifact. -artifact 1 on the same four calls says that the artifact is intended — this one is decoration, and no description is missing. The two contradict each other and are refused together. The difference matters because an artifact is the one way real content passes a reader entirely, and PDF/UA allows it only for decoration: a graphic that became an artifact with neither option was never judged either way, and the document keeps a note of it — an artifact by default is what most pictures are, an artifact by intent is what a validator cannot ask for and a caller can say. In an untagged document neither option changes anything. A form or a drawing is one piece of marked content however many operators it contains — bracketing per element would scatter one illustration over dozens of leaves, and the parts of a drawing mean nothing on their own. Nothing inside a form, a pattern or a page-number XObject is marked at all: those are content streams of their own, mark numbers are unique per stream, and it is the invocation that carries the marking. And because nothing there is marked, structure is refused inside a form, a pattern or a page-number XObject: the element would stay empty, and an empty H1 opened there satisfies the PDF/UA heading rule without a word on the page. Describe the invocation instead — -alt or -artifact 1 on form place.

A hyphen the line breaker inserted at a soft-hyphen offer is bracketed as a break rather than left to look like part of the word: it sits in a Span with an empty ActualText, so extracted text gives the word back whole (14.8.2.6). A hyphen the text brought with it is untouched — E-Mail stays E-Mail. Only in a tagged document.

What no writer can infer is whether a line of text is a heading — neither its size nor its weight says so. That is what -tag on text is for: -tag H1, -tag Caption, and so on. Inside an open leaf element -tag names a child: -tag Span inside a structure P is a Span in that P, -tag Caption inside a Figure its caption; a type the open element may not hold (-tag H1 inside a P) is refused naming both. -tag P, the default, marks the text as the open element’s own. The same holds inside an open LI: -tag Lbl is the item’s label, -tag LBody and -tag P — and no -tag at all — are its body, and every other type is refused naming both, because an LI holds a Lbl and an LBody and nothing else (ISO 32000-2, Annex L). Until 2026-08-27 every tag there was rewritten to LBody without a word, so a list numbered with text "1." -tag Lbl came out with two bodies, no label, and a ua claim that was then refused for the missing labels. A Caption is held to Table 372 — “The Caption shall be the first or the last structure element inside its parent structure element. The number of captions cannot exceed 1” — so one that stands neither first nor last inside its parent, and a second one beside the first, are refused when the parent closes; all four homes are checked, Table, L, Figure and Formula, the last two in every file, because Table 372 is the only rule they have, and the first two only in a 2.0 file, because ISO 32000-1 Annex L puts theirs first. A grouping type (Table, L, Sect, …) is refused on -tag: open it with structure. So are Link, Annot and Form: they stand for an annotation and hold a reference to it rather than text (ISO 32000-1, Tables 338 and 340), and a derived tag closes its element before an annotation could enter it. Open one with structure and draw the annotation inside it. -tag Artifact takes the text out of the tree altogether, which is what a running head or a page number needs; under PDF/UA anything left unmarked counts as a defect.

The numbered headings are one of two forms. H1 to Hn state the level themselves — a strongly structured document, and the form to write when the levels are known. The generic H leaves the level to the nesting: one Sect inside another, each opening with an H, and the depth of the element is the level — a weakly structured document (ISO 32000-1 14.8.4.4.2), which is what a converter honestly writes when its source nests sections without numbering them. A tree uses one form or the other, never both — mixed, the nesting and the numbers can disagree and no reader can choose. Under a ua claim the mixture is refused when the document is written (Matterhorn 14-006; measured, veraPDF fails it against ua1 under clause 7.4.4 and passes either form alone), and part 2 refuses the generic H altogether — ISO 32000-2 knows only the numbered headings. Example 05.14 writes a weakly structured document.

-expansion text is the expanded form of an abbreviation (ISO 32000 14.9.5) — PDF/UA asks that abbreviations be expanded (7.20), and a reader that is asked reads the expansion out. On structure it goes onto the element as /E; on text — text "EU" -expansion "European Union" — the word becomes a Span carrying it, inside the paragraph that is open, or inside the one the call would have made. It needs a tagged document and is refused without one, and it cannot go on an artifact.

Attributes. Five options write standard attributes onto the element, each one only where the standard allows it — used elsewhere they would be written and then ignored, so they are refused at the call instead.

-scope takes Row, Column or Both and belongs on a TH; it says which way a header cell heads. A table sets it by itself: its head row heads columns. -bbox takes four numbers and says so if it does not. -numbering belongs on an L and takes None, Disc, Circle, Square, Decimal, UpperRoman, LowerRoman, UpperAlpha, LowerAlpha (ISO 32000-1 Table 347); in a 2.0 file also Unordered, Description and Ordered (ISO 32000-2 Table 369), which a 1.7 file refuses; PDF/UA makes it mandatory for an ordered list, and no writer can derive it — the label is drawn text, and 1. and - look the same from here. Under ua -part 2 the value None is refused as well on a list whose items carry a Lbl (ISO 14289-2, 8.2.5.25); the refusal comes at the write, with the rest of the claim, and names the values that are open instead. -bbox belongs on a Figure, Formula or Table and takes the same four numbers as a rectangle; it is not required by the letter of the standard, but the reading tools rely on it. A picture, a drawing and a form placement described with -alt get one by themselves: image place and form place name the rectangle the placement covers, -scale included, and svg names the rectangle the drawing was actually fitted into rather than the one asked for — a 2:1 drawing in a square box is drawn 2:1 with empty bands beside it, and the bands are not the figure, which is why it is the same four numbers svg answers with. Under -rotate the box is the upright one around the turned placement, wider and higher than the placement itself: ISO 32000-2, 14.8.5.4.3 asks for the rectangle that completely encloses the visible content, and the untouched rectangle would leave part of the figure outside it while covering page that is not the figure. -colSpan and -rowSpan belong on a cell and are written by the table itself where it spans.

Types beyond 1.7. ISO 32000-2 adds Title, Aside, DocumentFragment, Sub, FENote, Em, Strong and the headings H7 to H10. They are accepted only in a 2.0 file — in a 1.7 one they would validate as non-standard types with no role map — and ua -part 2 is the ordinary way to get one. Each such construct pins the version: after a 2.0-only type, a 2.0 attribute value such as -numbering Ordered, or a structure destination from link -structure or bookmark -structure, configure -version 1.7 is refused naming what the document already uses. As soon as one of them is used, the tree names the 2.0 structure namespace (/NS on every element that lives in it, and /Namespaces on the root) whether or not PDF/UA-2 is claimed; the twelve 1.7-only types keep the default. Twelve older types (Art, BlockQuote, TOC, TOCI, Index, Private, Quote, Note, Reference, BibEntry, Code and the generic H — the 2.0 namespace knows only the numbered headings) exist only in the 1.7 namespace and keep it even inside a 2.0 tree. Inline types — Span, Quote, BibEntry, Em, Strong, Sub, Ruby, Warichu — need an element that holds text around them (a P, a heading, a cell, a Figure); at the top level or straight in a Sect they are refused (ISO 32005 Table 5). Reference is refused at the top level only. -lang takes a language tag as language does (RFC 3066) and refuses anything else. -script may be an empty body — an element with nothing in it is a legitimate placeholder — but a missing -script is refused.

A leaf type such as P or H1 holds text and inline markup — Span, Em, Strong, Link, Figure and their kin — but no block element: a P inside a P is the standing example of what Annex L forbids. Ruby and Warichu hold a fixed sequence, not a set: exactly RB RT or RB RP RT RP, and WP WT WP (ISO 32000-2, Table 369) — a part outside its parent is refused where it opens, a wrong or incomplete sequence when the bracket closes. A Table holds at most one THead and one TFoot (Annex L); the second is refused where it is opened.

Naming an element. -name gives the element a name that a link or a bookmark points at with -structure. A structure destination names the element rather than a place on a page (12.3.2.3), so it still lands on the right thing after the content above it has grown — PDF/UA-2 asks for internal targets to be written that way. It is written as a GoTo action carrying both the structure destination (/SD) and a page destination (/D) to the element’s first page — /XYZ at the top of its first content, or /Fit when no position is known — so a reader that does not understand structure destinations still lands on the right page. The name has to be unique and may be used before it is declared, which a link pointing forward at a later section needs. An unknown one is reported when the document is written, naming it.

-id is something else: the element identifier of ISO 32000-1 14.7.2, a string written as /ID on the element and entered in the IDTree of the structure tree root, by which a reader — not this package — looks the element up. It has to be unique in the document. A Note gets one on its own when none is given (Note followed by its position in the tree), because PDF/UA-1 asks every note for one (7.9); every other element has one only when asked.

-ref names what this element points at — the -name of another element, or a list of them, written as the Ref entry of ISO 32000-2, Table 355. It is what makes a table of contents usable: PDF/UA-2, 8.2.5.8 asks every TOCI to identify its target that way, either on the entry itself or on one of its children, and ua -part 2 refuses to write the claim for a TOCI that names none. The names need not exist yet — a contents page is written before the sections it lists, so they are resolved when the document is written, and one that never came is reported by name. Ref is a 2.0 entry: below that version it is refused rather than written as a key no reader of the file knows (ua -part 2 raises the version).

Artifacts name their kind. What is not in the tree is bracketed as an artifact, and the bracket says which sort it is: Pagination for a page number, Layout for everything else this package produces. PDF/UA-2 requires the naming; earlier versions permit it, so it is written either way and a document does not have to be redrawn when it is upgraded.

-tag takes the kind as well, as a list: -tag {Artifact Pagination Header} marks a running head, {Artifact Pagination Footer} a running foot, which is what PDF/UA asks for (7.8). The kinds are the four of ISO 32000-1 Table 330 — Pagination, Layout, Page and Background — and only Pagination takes a subtype: Header, Footer or Watermark (Table 331). Anything else is refused at the call. pageNumbers works it out by itself — above the middle of the page it is a head, below it a foot — because that is the one place that knows.

doc structureResume name -script body
Makes an element that is already in the tree current again, so that a caller drawing in several passes can hang further children on it. name is the -name an element was opened with, or its -id. The bracket is the same promise structure makes: what is opened is closed again, whatever the body does, and the body’s result is the call’s.

What is repeated is only the opening. The element keeps the type, the attributes and the page it was created with, and its place in the tree was judged then; what is drawn inside is judged now, exactly as it would have been on the first pass — a child the element may not hold is refused, and the Caption of a table appended on a later pass is in the wrong place and is refused as such.

The children keep their own pages. An element whose content sits on several pages is what text -paginate already produces and what the tree writes as marked-content references naming their page (14.7.4.2); a row whose cells were drawn on two pages needs nothing further. This is how a table broken across columns (table -horizontalBreak 1) comes out as one TR per logical row rather than as twice as many rows, half as wide.

A leaf is refused: what a P, a heading or a Lbl holds is text, and further text for an element that exists is a further mark on it — the road text -paginate takes — not a further child. So is an element that is open already, and an element resumed inside a form, a pattern or a page-number XObject, where nothing is marked at all.

PDF/UA

doc ua ?0|1? | doc ua ?-part 1|2? ?-revision year? ?-wtpdf levels?
Declares PDF/UA conformance — the promise that the document can be used by someone who cannot see the page. The boolean form means part 1 (ISO 14289-1) and is what a letter, an invoice or a briefing needs; part 2 (ISO 14289-2) is a PDF 2.0 format and is asked for by name. Part 1 lifts the file to PDF 1.7 and part 2 to 2.0, through configure, and the version cannot be lowered again afterwards. Part 1 does not cap the version — a document may move on to part 2 — but a part-1 claim on a file that was raised to 2.0 is refused when the file is written (ISO 14289-1, 6.1: the header is %PDF-1.n), naming part 2 as the way out. ua 0 withdraws the claim and everything it contributed — the schema, DisplayDocTitle where ua set it, the namespace demand; the file version stays raised.

Off by default and explicit, as pdfa is. The claim is legally meaningful in public procurement, so nothing should acquire it as a side effect — and a document using the standard 14 faces cannot make it at all.

What it insists on, checked when the file is written. A title (info Title), a language (language), every font embedded — a Figure or a Formula described with -alt or -actualText, wherever it was opened — and a Figure or Formula derived by text -tag can carry neither, so it is structure Figure -alt … that describes one, since an empty -alt is not a description and veraPDF says so — the standard 14 included, which is stricter than PDF/A and rules out Symbol and ZapfDingbats entirely, as neither has an embeddable representative. Headings starting at H1 with no level skipped, and one kind of heading throughout: a tree that mixes the generic H with numbered ones is neither weakly nor strongly structured and is refused (ISO 14289-1, 7.4.4; Matterhorn 14-006). Tables whose rows all cover the same number of columns, spans counted — a colSpan covers as many columns as it says and a rowSpan covers its column in the rows below (Matterhorn 15-003, UA-2 8.2.5.26); a table whose rows still differ is refused. A description on every link, which is what link -tooltip writes, and every link annotation inside a Link element (part 2: Link or Reference). A description on every form field, which is what field -tooltip writes, and every widget annotation inside a Form element — a field declared before tagged 1 misses that and is named; part 2 adds a label or a description on every widget besides (-label, -contents, and for a radio set the third word of -buttons). A visible signature field counts as a form field here: sign -tooltip writes its /TU and sign -contents its /Contents, and a sign call made before tagged 1 is named like any other field. An invisible signature — the default, /Rect [0 0 0 0] — is an artifact under ISO 14289-2, 8.9.2.4.13 and is asked nothing. Every picture, drawing and form placement either described with -alt or declared decoration with -artifact 1 — one that became an artifact with neither was never judged, and an artifact may carry nothing a reader needs (7.1). Lists whose numbering and labels agree (7.6): an L with -numbering other than None needs a Lbl in every LI, and items carrying a Lbl need the L to say what they are — Decimal, Disc, … or None. Under part 2 None is not among them where a Lbl is present: ISO 14289-2, 8.2.5.25 says the attribute shall be there and that “in such cases the value None shall not be used”, so the L names the scheme its labels come closest to — Disc, Circle, Square, Decimal, … or the 2.0 word Unordered, which needs the list drawn after ua -part 2 has raised the file version. Part 1 keeps None with labels, which is what a list of arbitrary markers says, and is unchanged (7.6). DisplayDocTitle still on: ua sets it, and a later viewerPreferences -displayDocTitle 0 is taken at the call and refused when the file is written, with the other claims this paragraph lists — the whole paragraph is checked at the write, and this entry is no exception to that. Part 2 adds a Desc on every attachment and forbids the generic H and Note (use FENote). Part 2 also asks every TOCI of a table of contents to name what it points at, through structure -ref on the entry or on one of its children (ISO 14289-2, 8.2.5.8); a contents page whose entries point nowhere is refused, naming the page it stands on. Part 2 asks three more things of what points at what. No page destinations inside the document (ISO 14289-2, 8.8: all destinations whose target lies within the same document shall be structure destinations): link -page, bookmark -page and initialView -page are refused when the file is written, naming every call that made one, and the way out is a named element and -structure on the same three calls — a link to a URL is not affected. One target per Link element (8.2.5.20): several link annotations may share a Link or a Reference while they all go to the same place — an icon and the word beside it — but two that go to different places need two elements, and the second is refused, at the call where the claim already stands and at the write where it comes later. A footnote and its citation point at each other (8.2.5.14.1): a FENote names the content that cites it with structure -ref, and that content names the FENote, both through the -name the elements were opened with; a note that names nothing, or that nothing names, is refused. Example 05.08 writes the pair.

All of them are reported at once rather than one per run, and each message names the call to change. Writing fails; no file is left behind.

What it contributes by itself: the pdfuaid schema in the XMP, ViewerPreferences with DisplayDocTitle, and for part 2 the 2.0 structure namespace and the file version. None of them moves a mark on a page. In a document that also declares PDF/A, the PDF/A extension schema describing pdfuaid is written as well — without it PDF/A refuses a schema it does not know. Where a second schema is described too — zugferd writes the Factur-X one, and pdfa extension whatever a caller needs — all of them share ONE pdfaExtension:schemas container, whose bag carries one entry per schema: XMP part 1, 6.1 admits a property once per packet, and a packet carrying the container twice is thrown away whole, taking both identifications with it.

-revision is the year of the edition claimed, four digits, and goes into the metadata as pdfuaid:rev for part 2 (ISO 14289-2 Table 1); the default is 2024, the year part 2 was published. Anything that is not a four-digit year is refused at the call.

-wtpdf adds a Well-Tagged PDF declaration and takes reuse, accessibility or both. It goes with part 2 only. The identifier is written in two spellings: WTPDF 1.0 gives the URI with a slash before the fragment, veraPDF 1.30 tests for the form without one, and a document that has to satisfy both carries both — which the declaration mechanism expressly allows.

PDF/UA-2 and PDF/A-3 cannot be combined. Part 2 needs PDF 2.0, PDF/A-3 is a 1.7 format, and so an accessible ZUGFeRD invoice is ua 1 together with pdfa -part 3. Declaring both is refused at the call that creates the contradiction.

doc ua state
What has been declared, as a dictionary: part, revision, wtpdf, registered. Empty before ua was called. ua state answers part, revision, wtpdf and registered; none of them is an option of ua — ua -registered 1 is refused as unknown.

Viewer preferences

doc viewerPreferences ?-key value …?
How a reader should present the document (ISO 32000 12.2). Without arguments it answers with what has been set so far. (PDF 1.2; -direction 1.3, -displayDocTitle 1.4, the value UseOC 1.5, -printScaling 1.6, -duplex, -pickTrayByPDFSize and -numCopies 1.7.)

Calls accumulate: each one sets the keys it names and leaves the rest alone, so a document can state its window wishes in one place and its printing wishes in another.

Booleans: -hideToolbar, -hideMenubar, -hideWindowUI, -fitWindow, -centerWindow, -displayDocTitle, -pickTrayByPDFSize. Names: -nonFullScreenPageMode (UseNone, UseOutlines, UseThumbs, UseOC), -direction (L2R, R2L), -printScaling (None, AppDefault), -duplex (Simplex, DuplexFlipShortEdge, DuplexFlipLongEdge). And -numCopies, an integer from 2 to 5 — the values ISO 32000-1 Table 150 says a reader supports; anything else is refused rather than written to be ignored. That is about the value: -nonFullScreenPageMode is written whatever initialView -pageMode says, although Table 150 has a reader ignore it outside FullScreen, since the two calls are independent of each other and either may come last.

A misspelled value is refused at the call. It has to be: a reader that meets one falls back to its default silently and nothing anywhere reports a problem — the document simply prints on one side for the rest of its life.

ViewArea, ViewClip, PrintArea and PrintClip are deliberately absent, and so is PrintPageRange. The four are deprecated in PDF 2.0 and no reader tested here acts on them.

Page labels

doc initialView ?-pageMode name? ?-pageLayout name? ?-page n? ?-to {x y}? ?-zoom z?
How a reader opens the document: which panel it shows beside the page (/PageMode), how it arranges the pages (/PageLayout), and where it puts the reader first (/OpenAction) — the catalogue entries of ISO 32000-2, Table 28. Without arguments it answers what has been set so far, and calls accumulate, as viewerPreferences does. -pageMode takes UseNone, UseOutlines, UseThumbs, FullScreen, UseOC (PDF 1.5) or UseAttachments (1.6); -pageLayout takes SinglePage, OneColumn, TwoColumnLeft, TwoColumnRight, TwoPageLeft or TwoPageRight (the last two 1.5). A value outside those lists is refused rather than written: a misspelt name is not an error a validator reports either — the reader falls back on its default and the document opens the ordinary way for the rest of its life. A caller’s -pageMode wins over the /UseOutlines a bookmark sets by itself.

A command of its own, not three more keys of viewerPreferences. A viewer preference is a wish a reader may ignore; these are what it does when the file opens. viewerPreferences answers with the dictionary that goes into /ViewerPreferences — the PDF/UA check reads that answer to find DisplayDocTitle in it — and three catalogue keys mixed into it would make one return value mean two things; besides, that dictionary already has a -nonFullScreenPageMode whose value list overlaps /PageMode’s in four of six words.

-page, -to and -zoom are the three words destination and link use for the same three choices, and they go through the same code, so an open action and a bookmark pointing at the same place cannot disagree. -page counts from 0, as page current counts, and may name a page not added yet — the target is filled in when the document is written, as a forward link’s is. Without -to the destination is /Fit, the whole page. -to and -zoom without -page are refused: they describe a place on a page and none was given. A zoom of 0 is a number a reader acts on (12.3.2.2: it means the magnification the reader is already at), below it there is none to mean. An action is not offered. Table 28 lets /OpenAction be a destination or an action dictionary, and the second is left to catalogEntry OpenAction: ISO 19005 forbids most action types outright, so an -action option would be a promise half of whose values break the moment pdfa is declared. Measured with veraPDF: a /Launch and a /JavaScript open action each fail clause 6.5.1 test 1, while a destination in /OpenAction — and /PageMode /FullScreen with it — passes PDF/A-3B.

doc pageLabels / doc pageLabels -from index ?-style s? ?-prefix text? ?-start n?
What a reader calls each page (ISO 32000 12.4.2). A page has two numbers — the index the file counts it by and the number printed on it — and a reader shows the first unless the file says otherwise; then “go to page 3” lands on the third sheet while the sheet reading “3” is the sixth. One call is one range: it begins at the page -from, an index counted from 0 like everywhere else, and runs until the next range begins. Several calls make several ranges, in whatever order they come; a second call for the same index replaces the first. Without arguments it answers the ranges set so far, as a dictionary from index to style, prefix and start, in page order. (PDF 1.3.)

-style takes D (decimal, the default), R and r (roman), A and a (letters), or none for a range that carries a prefix and no number — a cover called “Cover”. -prefix is put before the number, -start is the number the range begins with, a positive integer up to 2 147 483 647, 1 unless said otherwise: /St is an integer (ISO 32000-2, Table 159) and a PDF integer stops there (Annex C.2), so a larger one is refused at the call rather than written as a real. A misspelled style is refused at the call, because a reader that meets one ignores it silently.

The tree has to begin at index 0. A document that labels only its body from page 4 on has said nothing about the pages before, so a range without a style is put in front — the standard’s own way of saying “no number here” — rather than writing a tree a reader may refuse.

Labels and printed numbers have to agree: Matterhorn 15-001 counts a visible page number that differs from the page label as an accessibility failure, and it is one no validator can see, because the printed number is drawn text. Nothing here is automatic, and the two commands count differently: pageNumbers -from is a page number counted from 1, while pageLabels -from is a sheet index counted from 0, as page current counts. So a document that prints its numbers with pageNumbers -from 3 — which leaves the first two sheets unnumbered and prints Page 3 on the third — says pageLabels -from 2 -start 3 as well, and the reader’s page field then shows what the sheet shows. pageLabels -from 0 -start 3 would name the first sheet 3 and the third one 5.

PDF/A and ZUGFeRD

doc pdfa ?-part n? ?-conformance level? ?-profile path? ?-identifier text?
Declares PDF/A conformance, writes the output intent with the given ICC profile and raises the file version to match — -part is 3 unless said otherwise — through configure, so cget -version answers 1.7 afterwards, and the version can be neither lowered nor raised to 2.0 again — PDF/A-2 and -3 are PDF 1.7 formats (ISO 19005-2/-3, 6.1.2: the header is %PDF-1.n), so configure -version 2.0 after pdfa is refused naming the claim, as pdfa after -version 2.0 is. Parts 2 and 3 are accepted; part 1 is refused because it forbids the transparency this package writes, and part 4 is not offered. It is a PDF 2.0 format (ISO 19005-4) and this package does write PDF 2.0, so what stands in the way is not the version but the conformance work: nothing here has been measured against part 4, and a claim nobody has checked is worth less than no claim. Part 2 admits no embedded file that is not itself PDF/A (ISO 19005-2, 6.8): pdfa -part 2 on a document with attachments is refused, and so is attach under a part-2 claim — use part 3.

Without -profile the sRGB profile shipped with the package is used, so every PDF/A file carries an output intent — this package paints in DeviceRGB, and ISO 19005 requires the intent for that. The output condition identifier is read from the profile’s own desc tag, falls back to the file name, and -identifier overrides it; the shipped profile is named sRGB IEC61966-2.1, the colour space it implements, as it always was. A profile that does not exist is refused at the call. So is a file that is no ICC profile (no acsp signature) or one describing a space other than grey, RGB or CMYK. What is not checked is the profile’s version and its device class: ISO 19005-2/-3, 6.2.2 bounds an output intent profile to the ICC versions the part names and asks for an output or a display class, and a v5 profile or a scanner profile (scnr) passes here and is left to the validator. The path is normalized on the way in; pdfa state answers the normalized path. A second profile ships beside it for print work in process colours: icc/ISOcoated_v2_bas.ICC (basICColor’s ISO Coated v2, FOGRA39, zlib licence — the file LICENSE-ZLIB-bICC next to it is the grant), a CMYK output intent that a document painting in {cmyk …} names with -profile — and icc/ISOcoated_v2_grey1c_bas.ICC, its grey component, for a document that paints in grey alone (example 05.10); and icc/sRGB2014.icc, the ICC’s own sRGB profile of 2015, beside the default so that the two can be compared — examples 05.11 and 05.12 write the same page under each and print the profile facts; same primaries and tone curve, D50 white point with a chad tag against the older D65 without one, and in the file both compress to much the same size; the intent may describe grey, RGB or CMYK, and the package’s own objects — the transparency groups of forms among them — name no colour space that could contradict it. Example 05.09 writes such a file.

-conformance takes B (the default), U or A. Level B promises the document looks the same in fifteen years; level U adds that its text can be extracted and searched reliably, which rests on the ToUnicode map written for every embedded face anyway — so U is the stronger claim at no cost and is worth asking for. Level A adds the structure tree, so it needs tagged 1 before anything is drawn; asked for without it, pdfa names the missing call rather than writing a file that claims 3a and fails validation. Level A also asks the question ua asks: a picture, a drawing or a form placed with neither -alt nor -artifact 1 would go out as an artifact — content a screen reader never sees, in a file veraPDF declares conformant — so the write is refused naming each graphic’s page and both ways out (ISO 19005-2, 6.7.3). Levels B and U promise appearance and extractable text, not reachable content, and stay silent. Both orders are checked, as they are for fonts, colours and graphics: tagged 0 after the claim is refused at the write, since a document declares its conformance and its content in either order and only the write sees both.

Declaring conformance also turns on a check: every font in the document must be embedded, and writing fails with a message naming the offending face rather than producing a file that a validator rejects later. The same is true of colour: ISO 19005-2, 6.2.4.3 admits DeviceGray under any output intent, DeviceRGB only under an RGB one and DeviceCMYK only under a CMYK one, and the rule reaches every road a colour takes — fills and strokes, text, a picture’s colour space (an Indexed PNG counts as RGB, a CMYK JPEG as CMYK), a shading, the content of a tiling pattern or a form, and the alternate of a separation (6.2.4.4). The package keeps a record of which spaces the document uses and where, whether or not pdfa was declared, and holds it against the profile when the file is written: a {cmyk …} fill under the shipped sRGB profile, or a steelblue under icc/ISOcoated_v2_bas.ICC, is refused with the calls and pages that used it — used by rect on page 1, text on page 2 — and the two ways out named: paint in the intent’s space or in grey, or give a profile of the space you paint in. Every offending space is listed at once, and the check does not depend on whether pdfa came before or after the drawing. Measured against veraPDF: what is refused here is exactly what fails there.

doc pdfa state
What has been set, as a dictionary: part, conformance, profile, identifier, extensions and registered. Empty before pdfa was called. registered and extensions are state, not options — pdfa -registered is refused as unknown.

doc pdfa extension xml
Adds an extension schema to the XMP packet — the way a profile such as ZUGFeRD announces its own properties. zugferd uses it. It needs a pdfa call before it; without one it is refused rather than declaring PDF/A-3B by itself.

doc zugferd path ?-name n? ?-profile p? ?-type t? ?-icc path? ?-version v? ?-relationship r? ?-description d? ?-compress 0?
The one call an electronic invoice — or an Order-X order, see below — needs. It reads the profile from the invoice XML (BT-24), declares PDF/A-3B, writes the output intent with the sRGB profile shipped with the package, adds the Factur-X XMP extension schema, and attaches the file as factur-x.xml at document level with the /AFRelationship the profile prescribes by default — Data for MINIMUM and BASIC WL, Alternative for every fuller profile; -relationship may name Data, Source or Alternative instead, while Supplement and Unspecified belong to the other attachments and are refused for the invoice as they are for the order — plus an entry in the names tree and a modification date. Returns the detected profile. A UTF-16 encoded XML is refused as such, and so is one that declares any encoding other than UTF-8, or that carries bytes no UTF-8 sequence can hold — instead of being reported as carrying no BT-24 identifier. The invoice is embedded byte for byte and is read as UTF-8 by every reader of the attachment; re-encode it as UTF-8 and say so in its declaration. A UTF-8 byte order mark in front of the declaration is allowed (XML 1.0, 4.3.3).

-name is the name a reader looks the attachment up by, and the standards allow exactly four: factur-x.xml, zugferd-invoice.xml and xrechnung.xml for an invoice, order-x.xml for an order. But not all four to every invoice: xrechnung.xml belongs to the reference profile XRECHNUNG and to no other profile, and no other name belongs to it — Factur-X 1.09.2, 6.2 and 7.7 bind the two, and 7.7 forbids an XRECHNUNG document a factur-x.xml outright. Without the option the file’s own name is kept where it is one the document’s family and profile allow, and the profile’s default is taken otherwise — xrechnung.xml for XRECHNUNG, factur-x.xml for every other invoice, order-x.xml for an order; any other -name is refused, and so is a name of the other family: an invoice is never order-x.xml, an order nothing else.

The reserved names stay the invoice’s. Once the document carries the invoice or the order, attach refuses each of the four for anything else, in any spelling — the embedded file name tree keys on bytes (7.9.6), so factur-x.XML is a key of its own and the uniqueness check never sees it, while a reader looking the invoice up by name would find two candidates. The same holds the other way round: a document that already carries an attachment under one of those names refuses zugferd. A supporting document takes a name of its own; a document that carries no invoice at all is not held to any of this, since building the hybrid by hand out of attach and pdfa extension is a thing this package allows. -type goes into fx:DocumentType: INVOICE, ORDER, ORDER_RESPONSE or ORDER_CHANGE; anything else is refused. Without the option the type follows BT-24 — ORDER for an Order-X identifier, INVOICE for every other — and a response or change has to be named. -description replaces the attachment description, which is “profile invoice data” — “profile order data” for an order — unless given. -compress Flate-compresses the embedded XML and is off by default, so the invoice sits in the file byte for byte as it arrived.

Order-X (Order-X 1.0, 4.1.1 and 4.1.2) rides on the same call: BT-24 urn:order-x.eu:1p0:basic, :comfort or :extended is read as BASIC, COMFORT or EXTENDED, the document becomes an ORDER unless -type says ORDER_RESPONSE or ORDER_CHANGE, the XML is embedded as order-x.xml — the one name the standard allows — with /AFRelationship Data by default, and the extension schema is written under the Order-X namespace URI, urn:factur-x:pdfa:CrossIndustryDocument:1p0#, without the :invoice of Factur-X. -relationship may say Source or Alternative instead, the two other values 4.1.1 permits for order-x.xml; Supplement and Unspecified are for the other attachments and refused here. Levels, types and names are bound to their family — invoice: MINIMUM, BASIC WL, BASIC, EN 16931, EXTENDED, XRECHNUNG under INVOICE; order: BASIC, COMFORT, EXTENDED under the three order types — and a -profile or a BT-24 identifier of the other family is refused naming both, so that a Factur-X BASIC invoice cannot go out labelled an order at BASIC or the other way round.

PDF/A-3 is fixed at this call, and level B is what it settles on where nothing else has been claimed — the level every invoice reaches without a structure tree, and the least the standards ask for. A claim already made survives: pdfa -conformance A before zugferd keeps its A rather than being taken back to B, which used to happen silently and left the structure tree paid for but not claimed. A document that wants more can also say so afterwards: pdfa -conformance U after zugferd raises the claim to 3U with the Factur-X extension in place, and A needs tagged 1 besides — measured, the file then validates against the higher profile. -icc names another output intent profile in place of the shipped sRGB one — a CMYK profile for an invoice painted in process colours, see pdfa -profile — and the colours are held against it at write time like everywhere else. -profile overrides the level read from BT-24 (MINIMUM, BASIC WL, BASIC, EN 16931, EXTENDED, or XRECHNUNG, which is what an XRechnung identifier is read as; BASIC, COMFORT or EXTENDED for an order), for an XML whose identifier the reader does not recognise — any other word is refused, because it would go into the XMP as a conformance level nobody validates against; with -profile given BT-24 is still read for the family where it names one, so an Order-X XML stays an order, and with -profile and -type both given it is not read at all; -version is the fx:Version written to the XMP, 1.0 unless given, and takes digits and dots only. Every option is checked before the document is changed; a refused zugferd leaves no PDF/A claim, no schema and no attachment behind.

doc zugferd profile xml
The conformance level named in BT-24 of an invoice or order XML — the XML itself as a string of bytes, not a path — read without writing anything: EN 16931 for the EN 16931 identifier, COMFORT for urn:order-x.eu:1p0:comfort. Refuses XML that carries no such identifier or one the reader does not know.

doc zugferd state
What was attached and under which profile: name, profile, type, version, relationship — the /AFRelationship that was written, prescribed by the profile or given — and bytes. Empty before the call.

Encryption

doc encrypt ?-user text? ?-owner text? ?-permissions list? ?-metadata bool?
Encrypts the document with the standard security handler in revision 6: AES-256 for strings, streams and the file as a whole (/V 5 /R 6, crypt filter method /AESV3). The older revisions — RC4 and AES-128 — are deprecated in PDF 2.0 and are not written: a writer that offers them offers a choice whose wrong half looks exactly like the right one from outside. Returns the state as a dictionary, without the key and without the derived values. encrypt state answers the same later.

The two passwords. The user password opens the document with the permissions below, the owner password opens it with all of them. Without -owner the user password serves as both; an empty owner password is not offered on its own, being the first one any reader tries. An empty -user password is allowed and is often what is wanted — the document opens for everyone and it is the owner password that lifts the restrictions — but then -owner has to say what that password is: both empty is refused, since it would hand every reader every right and leave a file that is encrypted and restricts nothing.

A password is cut to its first 127 bytes (ISO 32000-2, 7.6.4.3.2), and to bytes rather than characters: the text is encoded to UTF-8 first, so a long password of umlauts reaches the limit sooner than its length suggests, and the cut may fall inside a UTF-8 sequence. That is what the standard prescribes and what a reader truncating the same password does, so the two still agree; it is named here because a caller generating passwords needs to know that two that differ only after the 127th byte are the same password.

-permissions takes a list of names: print, modify (change the content), copy (extract text and graphics), annotate (annotations and form fields), fill (fill in existing form fields), assemble (insert, rotate and delete pages, add bookmarks) and highres (print at full quality). The default is all; the empty list grants nothing and still leaves a document that can be read on screen, since reading is not a permission. The bits the standard fixes are set by the package (ISO 32000-2, Table 22). Note what permissions are worth: they are a statement of intent that a conforming reader honours, not a lock — the content is decrypted either way once the file opens.

-metadata 0 leaves the XMP packet in the clear, so a cataloguing system can index a document it may not open — where there is one: a document that claims nothing and sets no xmpSchema of its own carries no /Metadata at all, and the option then has nothing to leave in the clear. The metadata stream then says so at the stream as well: ISO 32000-2, 7.6.6 has it carry /Filter /Crypt /DecodeParms << /Name /Identity >>, and without those entries a reader that goes by the stream rather than by /EncryptMetadata applies the document’s stream filter to bytes that were never encrypted — measured with poppler 26.08.0, which handed back the packet run through AES.

AESV3 is a PDF 2.0 feature, so the document needs -version 2.0; the version is not raised behind the caller’s back, the missing call is named instead.

encrypt has to come first — before anything is drawn or put into the document, and before language and link, which build their entries at the call rather than at write time. A stream or a string written before the cipher was installed would stay in the clear inside a file whose /Encrypt says otherwise, and a reader decrypting what was never encrypted ends up with an empty value and no complaint from any tool. The call is refused rather than repaired, naming the page or the object in the way — annot and field are in that list too, and the message for an annotation says “link”, the entry both build. What may stand before encrypt is everything that is written at the write rather than at the call: info, attach, bookmark, pageLabels, tagged and ua, and an empty page as well. What may not: language, link, annot, field, form create, pattern create, pdf import, and any drawing at all. pdfa is a case of its own and is refused in either order (TCLPDF ENCRYPT STATE pdfa): ISO 19005 forbids an archivable file the Encrypt keyword outright.

PDF/A forbids encryption (the Encrypt keyword shall not be used in the trailer), and a ZUGFeRD or Order-X invoice is a PDF/A-3 document — both combinations are refused, in whichever order the two calls are made.

Two things stay readable on purpose: the file identifier /ID in the trailer, which a reader compares before it has any key, and the strings of the encryption dictionary itself (ISO 32000-2, 7.6.2).

What is not there: revisions 2 to 4 and RC4; reading someone else’s encrypted file (pdf import refuses it by name); the public-key handler; encrypting only the attachments; and SASLprep (RFC 4013) on the password — for an ASCII password that is the identity, but a password with combining characters may be spelled differently here than by a reader that normalises. ISO 32000-2, 7.6.4.3.3 a) makes that normalisation a “shall”; qpdf 12.4.0 does not do it either, so a password with a ligature or a non-normalised form has to be handed in exactly as it was set. The cipher protects confidentiality and nothing else: it is not a signature, and pure Tcl gives no promise about side channels.

Signatures

doc sign ?-signer prefix? ?-size bytes? ?-subfilter profile? ?-name text? ?-reason text? ?-location text? ?-contact text? ?-date date? ?-field name? ?-page n? ?-rect {x y w h}? ?-appearance name? ?-tooltip text? ?-contents text?
Prepares the document for a digital signature (ISO 32000-2, 12.8) — an approval signature over the whole file, invisible unless -rect and -appearance give it a place on a page — and signs it as it is written where -signer says what to sign it with. Returns what was declared as a dictionary; sign state answers the same later. The call is made once and before the document is written; nothing is built at the call itself, so it may stand anywhere before the write.

How a signature gets into a file. The signature dictionary goes out with placeholders in it: /ByteRange, four numbers of fixed width, /Contents, a hexadecimal string of zeros as wide as -size asks for, and — where the signing happens elsewhere and later — /M, a date of nothing but zeros. They are written over once the file exists, and every one of those edits is length-neutral by construction — the cross-reference offsets are already in the file, and /ByteRange describes the very file it sits in, so a byte gained or lost here would invalidate exactly the thing being written. /ByteRange covers the whole file except the value of /Contents, its < and > included; those bytes are what gets signed, and the CMS object made over them goes into the place the zeros held. Reserving the space first and padding it with zeros afterwards is not a trick but what 12.8.3.3.1 prescribes.

tclpdf prepares, the signature comes from outside. Nothing in this package holds a private key, computes a digest or speaks CMS. -signer is a command prefix, called as {*}$prefix $bytes from the global context, and it has to answer a CMS SignedData object in DER — what openssl cms -sign -outform DER writes. The reason for the cut is not convenience: in production the private key is rarely in the application. It is on a smart card, in an HSM or with a signing service, and the prefix is where that gets plugged in. A prefix that answers nothing, or that answers text where bytes were asked for, is refused at the write rather than turned into hexadecimal nonsense inside the file.

proc mySigner {bytes} {
    # hand the bytes to whatever holds the key - a pipe to
    # "openssl cms -sign ... -outform DER", a card reader, a signing
    # service - and return the DER object it answers
}
$doc sign -signer mySigner -reason "Approved" -name "E. Mustermann"
$doc write invoice.pdf

-size is the room reserved for the signature value, in bytes, 16384 unless said otherwise. Measured here: a CMS object with an RSA-2048 certificate and its issuer is 2599 bytes, one with ECDSA P-256 2209 — an RFC 3161 timestamp, a longer chain or embedded revocation data come on top of that, which is why the default is generous rather than tight. Reserving too much costs twice the number in hexadecimal digits in the file and nothing else; reserving too little costs the write, which is refused with TCLPDF SIGN SPACE naming both numbers. There is an upper end as well, and it is said at the call rather than in the write: the room becomes twice its number in hexadecimal digits, and a PDF integer stops at 2 147 483 647 (7.3.3, Annex C.2), so the largest reservation is 1 073 741 823 bytes; beyond it -size is refused with TCLPDF SIGN ARGUMENT size naming both numbers.

What the dictionary says about the signature is the rest of the options, each written only where it was given: -name becomes /Name — the standard asks for it “only when it is not possible to extract the name from the signature”, so a CMS object carrying the signer’s certificate makes it redundant — -reason /Reason, -location /Location and -contact /ContactInfo. -field is the partial field name of the signature field, Signature1 unless said otherwise, and it cannot be empty: a field dictionary without one is not a field at all but a widget annotation (12.7.4.2). -page is the page the widget sits on, counted from 0 as page current counts and 0 unless said otherwise; it has to exist when the file is written, which is where it is checked.

-date is the time of signing, /M in the signature dictionary: a PDF date such as D:20260820132111+02'00', the word now, or the empty string for no entry at all. /M is written in either way of signing, and that is the point of the option’s default. now means the moment of signing, not the moment of preparing, and the two ways reach it differently: where -signer signs the document as it is written, the moment is the write and the value is written outright; where there is no -signer, the entry goes out as the placeholder D:00000000000000+00'00' — a year no clock produces — and ::tclpdf::sign digest writes the real time over it, into exactly that room, before it hands the bytes out. Either way the time states when the signature was made, and either way it lies inside the bytes the signature covers, because /M is written behind the reserved room and everything behind that room falls into the second range. An explicit date is written as it stands in both ways: that one is the caller’s own statement about a time it knows, not a guess this package made. -date {} keeps /M out of the dictionary altogether. The zone offset is spelled as the file version spells it, exactly as /CreationDate is (7.9.4) — a 2.0 file drops the trailing apostrophe, and the placeholder is one character shorter there.

Why /M is written at all, since Table 255 asks for it “only when the time of signing is not available in the signature” and a CMS object carries a signingTime attribute: ETSI EN 319 142-1, Table 1 requires the entry (“shall be present”) for every PAdES baseline level while forbidding that attribute, and a shall beats a should. The measurements agree with the shall. Acrobat takes the time from /M alone and reports “signing time not available” for a file whose CMS object states the time and whose time pdfsig prints out; and every compared signer — pyHanko, the EU DSS library, iText, LibreOffice, PDFBox — writes /M without exception. -date {} is the way back for whoever reads Table 255 to the letter, and it is the one way to get a signature out of this package with no time in the dictionary.

Without -signer the document is written prepared rather than signed, and /ByteRange is filled in all the same. That is not a half-finished file but the first stage of the way a signature is usually made — see ::tclpdf::sign digest below. For a PDF/A-3 document that is true of the file and not of the profile: a prepared file validates only once the value is embedded — the zeros in /Contents fail ISO 19005-3, 6.4.3. Filling in /ByteRange at that point is the whole purpose of the stage: without it nobody on the other side knows which bytes to hash. What such a file still carries as a placeholder is /Contents and, unless -date {} said otherwise, /M.

-subfilter is the signature profile, and it is the caller’s choice rather than a consequence of the file version. It takes pkcs7, which writes /adbe.pkcs7.detached and is the default, or cades, which writes /ETSI.CAdES.detached. Table 255 leaves a writer exactly these two — the three other values it lists are not offered at all: adbe.x509.rsa_sha1 and adbe.pkcs7.sha1 are deprecated in PDF 2.0 with “PDF writers shall not use this value for this key”, and ETSI.RFC3161 belongs to a document timestamp (/Type /DocTimeStamp), which is a different thing from a signature. Each value brings its own version floor, 1.6 for the one and 2.0 for the other, and a document below it is refused naming the version rather than raised behind the caller’s back. The default is the one that claims less, and it is the lower floor as well — deliberately: PDF/A-2 and -3 are profiles of ISO 32000-1 and are written as 1.7 at most, so a package that always demanded 2.0 would refuse every ZUGFeRD invoice by the back door, through a version error.

cades is a promise about the signature, not a second name for it. /ETSI.CAdES.detached claims a PAdES signature (ETSI EN 319 142-1), and that claim is about the CMS object the -signer prefix hands back — which this package does not make. Table 1 of that standard puts the signing-time attribute at “shall not be present” for every baseline level, and a signer writes that attribute unless it is told otherwise: openssl cms -sign leaves it out with -no_signing_time, which OpenSSL 3 has and the LibreSSL shipped as /usr/bin/openssl on macOS does not, while pyHanko and the EU DSS library omit it by themselves and BouncyCastle adds it unasked. Measured with OpenSSL 3.6.3: -cades -no_signing_time produces an object this package takes under the claim, and the same signer without the switch is refused. So whoever asks for cades needs a signer that omits it, and a CMS object that carries it is refused rather than written, with TCLPDF SIGN SIGNINGTIME, on both ways in. pkcs7 claims nothing of the sort and takes the object as it comes. Which value is in force matters to the signer either way, since one promises a CAdES-BES object and the other a plain CMS one, so sign state answers it.

The signature is invisible unless it is given a place to be seen. Without -rect the widget carries /Rect [0 0 0 0] and /F 4, which is the standard’s own example of an invisible signature (12.8.5.3) and the only spelling of it PDF/A admits — veraPDF’s rule 6.3.2-2 requires the Print flag set and Hidden, Invisible, ToggleNoView and NoView clear, which rules out the two other ways 12.7.5.5 offers. A rectangle of no area is also the one case Table 166 exempts from carrying an appearance stream, and there is none. /F 4 is written for a visible field just the same: rule 6.3.2-2 asks those flags of every annotation, and a visible one is no exception. Field and widget are written as a single object, which 12.5.6.19 permits where a field has exactly one widget and a signature field never has more (12.7.5.5); /AcroForm carries the field and /SigFlags 3.

-rect and -appearance make the field visible, and neither works without the other. -rect is {x y w h} — the top left corner of the field and its size, in the unit of the document, y counted from the top of the page the way link -at and rect -at count it — on the page -page names. -appearance is the name of a form XObject the caller built with form create; it is hung into the widget as /AP << /N … >>, which is what an appearance stream is (12.5.5, Table 168). The rectangle is checked for shape at the call, but where it lands on the page and which form the name stands for are worked out at the write, exactly as -page is, so both the page and the form may be created after the sign call as well as before it; a name no form of this document carries is refused there, naming the ones it has.

This package does not draw the appearance, and that is deliberate. A signature appearance is a frame with a name, a reason and a date in it — that is layout, and the caller’s document does layout already: it draws form XObjects, breaks lines, embeds faces and knows its own unit. A drawing engine here would be a second place where the font, the wording and the arrangement are decided, and the caller could overrule none of it. So what is built here is the part only a signature module can build — the field, the rectangle and the reference — and what goes into the rectangle is the caller’s; examples/08.02-signature-visible.tcl draws one and shows both ways of signing it. Give the form the size the -rect has: an appearance stream is mapped from its bounding box into the rectangle of its annotation (12.5.5), so a form of 70 by 26 shown in a rectangle of 35 by 13 comes out at half size, lettering and all.

Each half without the other is refused rather than silently completed. -rect without -appearance is a visible annotation with nothing to show: veraPDF’s rule 6.3.3-1 fails it, and a reader draws it as nothing at all. Measured on the same bytes, once with the /AP entry and once with it blanked out to the same length: with it, isCompliant true, 555 checks passed and none failed; without it, isCompliant false, 482 passed and one failed — clause 6.3.3, test 1, “An annotation does not contain an appearance dictionary”. -appearance without -rect is a drawing on a rectangle of no area, which nothing ever paints. A -rect that is not four values, that carries something other than a number, or whose width or height is not above zero is refused as well, and so is one lying entirely beside the page it names — that message gives the rectangle and the media box both in points, which is what tells apart a y counted from the bottom and a rectangle given in millimetres on a document set to points. A rectangle merely sticking out over the edge is allowed: nothing else in this package takes the page edge for a boundary, a media box need not begin at zero, and a field at the very edge is the caller’s business.

A signature field is a form field, and a tagged document treats it as one. Where the document is tagged, a visible signature — one with -rect — gets what every other widget annotation gets, from the same code: a Form structure element opened at the sign call, so that the field stands in reading order where the caller put it; an object reference from that element to the annotation and a /StructParent key back to it, which are the two halves of ISO 32000-2, 14.7.5.4; and the description PDF/UA reads out. -tooltip writes /TU, the field’s accessible name (ISO 32000-2, 14.9.3), and -contents writes /Contents, the description of that one widget (ISO 14289-2, 8.10.2.3); both are written wherever they are given, claim or no claim. Under a PDF/UA claim they are asked for: ua 1 and ua -part 2 both want the /TU, part 2 wants the /Contents besides, and a signature without them is refused when the file is written, in the same words a form field without them is. Where the document is not tagged, none of it is written. The order of ua and sign decides nothing — either may stand first.

An invisible signature is an artifact, and stays out of the tree. ISO 14289-2, 8.9.2.4.13 puts it plainly — “a widget annotation of zero height and width shall be an artifact” — 8.10.1 exempts an artifact from the Form element, and 8.10.3.5 repeats it for the signature field by name; NOTE 1 gives the workflow it comes from, the invisible widgets of document timestamps in a PAdES-B-LTA signature, which “be exempt from any tagging requirements otherwise imposed by this document”. So the default signature — /Rect [0 0 0 0] — carries no /StructParent, sits in no Form element and is asked nothing by PDF/UA. The rectangle decides this and not the claim, deliberately: ua may stand before the sign call or after it, and a tree that depended on the order would make the two orders write different files. Measured with veraPDF 1.30: a Form element around a zero-area widget fails rule 8.9.2.4.13-1 under UA-2, and a visible signature without one failed rules 7.18.1-3 and 7.18.4-1 under UA-1 until this was built.

What a visible appearance cannot show is the time of signing, unless the signing happens at the write. The form is part of the document and is drawn when the document is written, while the time of signing is only known at that moment where -signer signs it there and then. In that one-stage way the time is known, and an appearance may state it — pass the same value to -date and the picture and /M say the same thing. In the two-stage way it is not: the file is handed on and signed later, possibly days later on another machine, so an appearance naming a time there would be a picture of something that has not happened. What is left is to say in the appearance where the time is to be found — which is what the example does — or to reserve a placeholder of fixed width in the drawing as well and overwrite it in the finished file the way /M is overwritten, which binds the layout to one text width and one set of face metrics. That is why the package does it for nobody.

A PDF/A or ZUGFeRD document may be signed — measured, not assumed. The ZUGFeRD example signed comes out of veraPDF with isCompliant true and 0 failed checks over 2476 checks performed, where the unsigned file has 2407; Mustangproject calls it valid, and the embedded invoice is byte-identical to the one in the unsigned file. The 69 additional checks are the signature rules of ISO 19005-3, 6.4.3 — the digest covering the entire file, the signer’s certificate being present in the CMS object, a single SignerInfo in it — which were simply not applicable before.

A timestamp needs nothing here, and that is worth knowing. A PAdES baseline signature at level B-T carries an RFC 3161 timestamp as an unsigned attribute of the CMS object, which means the signer puts it there and this package never sees it as anything but bytes. Nothing has to be built for it: reserve enough room and hand the work to a signer that timestamps. Measured on such an object, made by pyHanko against a public timestamp authority: 7108 bytes against the 16384 reserved by default. The check that refuses a signing-time under cades knows about this — a timestamp token carries a signingTime of its own, and one that sits behind the token belongs to it rather than to the signature. What this package does not write is a document timestamp, the other way the standard offers: that is a dictionary of its own with /Type /DocTimeStamp and /SubFilter /ETSI.RFC3161, and it would have to be built.

What this version does not do. A second signature is not written by sign, which prepares a document written in one piece: a further sign call on the same document is refused. It is written by ::tclpdf::sign add onto the finished file instead, as an incremental update (7.5.6) — see below. No /DocMDP certification; a document timestamp is not written by sign either, and has its own call — ::tclpdf::sign timestamp, below. A signed document is not byte-identical over two writes: /M names the moment of the write, and the CMS object carries its own signingTime and a random number besides. A document prepared without -signer is, and stays so as long as it is only written: its /M is the placeholder, which is a constant, and its /Contents is zeros. That is the file the two-stage way hands on, and ::tclpdf::sign digest is what makes it particular — from there on it names a time. Encryption and a signature exclude each other and are refused in whichever order the two calls are made: 7.6.2 exempts exactly one thing from encryption, the hexadecimal string of /Contents, and not the other strings of the dictionary around it.

A signed document has to be written to a file. writeChannel is refused while a signature is pending, and refused before the bytes go anywhere rather than after: /ByteRange is written into the finished file and computed from it, and a socket or a pipe cannot be read back. Where a signed document has to reach a channel, the two-stage way is the road — write it, sign it, send the file.

What a signature is not. It shows that the bytes have not changed since they were signed, and that whoever holds a particular private key signed them. Whether that key’s certificate means anything is decided by the trust store of whoever opens the document, not by the document. This package builds no certificate chain, checks none, and embeds no revocation information; a signature that is to stay verifiable for years has to carry all of that in the CMS object the signer hands back.

doc sign state
What was declared and what the last write made of it, as a dictionary: date (what -date was given, as it was given — the resolved time stands in the file), field, page, rect and appearance (what was given for a visible field, both empty for the invisible one), tooltip and contents (the accessible name of the field and the description of its widget, as they were given), size, subFilter (the name that went into the dictionary, /adbe.pkcs7.detached or /ETSI.CAdES.detached, which is what a signer has to produce for), signed (whether a value was actually written), byteRange (the four numbers as they went into the file) and length (the size of the CMS object in bytes, empty where none was written). Empty before sign was called. The signer is deliberately not in it: a command prefix may carry a passphrase or the path to a key file, and a state dictionary is the kind of thing that ends up in a log.

::tclpdf::sign digest path ?-date date?
What is to be signed in a file that sign prepared and write wrote — the two-stage way, for a signature that is not made in this process. Returns a dictionary: bytes (what the CMS object has to be made over), byteRange (the four numbers as the file states them), offset (where the value will go), size (how many bytes are reserved for it), subFilter (what the file claims to be — the value sign -subfilter put there, read back out of the file, since there is no document object left to ask) and date (the time of signing this call wrote, empty where the file already stated one of its own). The file is checked to agree with itself before anything is handed out — a /ByteRange that does not describe this very file, because something was appended to it or because the gap is not where /Contents is, would produce a digest no reader recomputes and a signature that fails with nothing to point at. A file whose /ByteRange is still the placeholder was never written through write; that is refused as well, rather than signing four zeros along with the rest. A signature still waiting for its value is found by its /ByteRange with a /Contents of nothing but zeros next to it — an annotation whose /Contents happens to be a hexadecimal string of zeros is not one.

This call writes the file. It is not the read-only operation its name suggests, and it cannot be: /M lies inside the bytes it is about to hand out, so the time of signing has to be in the file before they leave — a date written afterwards would be a claim nobody signed, and one written when the document was made would name the moment of preparing instead. So the placeholder is replaced by the current time, in exactly the room it occupies and not a character more, and the file is written back before the bytes are answered. Everything else about it is unchanged, to the byte; that the length did not move is verified rather than assumed.

What follows from it: the second call answers the same time as the first. Once the placeholder is filled it is no longer a placeholder, and a further digest of the same file leaves /M alone — the bytes it hands out are the same bytes, so a signature made after the second call is as good as one made after the first. What differs is the date key, which is empty from the second call on: it reports what this call wrote, and this one wrote nothing. The file itself states the time either way. The same holds from the first call for a document written with a -date of its own or with -date {}: both are the caller’s own statement about the time, and neither is overwritten by a call that knows less.

-date names a time other than the current one, for a caller who has to state the moment of signing rather than measure it; now is the default and means the moment the bytes are handed out. It takes a PDF date as 7.9.4 spells it, and it is written over the placeholder, so it has to be exactly as long as one — which is what ::tclpdf::pdfObj date produces for the version the file states in its header. A date of the wrong length is refused rather than fitted in, since nothing inside the signed range may change length.

These two are package commands rather than document methods, because the second stage happens in another process — often on another machine, and days later. A script that only embeds a signature never creates a document, so it never loads the module by using one: it needs package require tclpdf::sign of its own. Where sign was called on a document in the same process, the module is already there.

$doc sign -reason "Approved"
$doc write invoice.pdf
set job [::tclpdf::sign digest invoice.pdf]
set der [signWithTheCard [dict get $job bytes]]
::tclpdf::sign embed invoice.pdf $der

::tclpdf::sign embed path der
Puts a DER-encoded CMS object into a file prepared that way, over the reserved zeros and padded back out to the reserved length, and returns the length of the object in bytes. The file keeps the length it had, to the byte, and that is verified rather than assumed. An object that does not fit is refused with TCLPDF SIGN SPACE instead of being truncated — a truncated signature yields a file that every reader calls signed and no verifier accepts. Two more refusals, and both were measured on what happens without them: text where bytes were asked for — a character above 0xff cannot be a byte of a DER object — went in silently under Tcl 8.6 and raised an error about code points under Tcl 9, and a file that was changed after it was written took the object without a word. The second is the same check digest makes, and for the same reason: a real signature over the wrong bytes verifies nowhere and passes every structural check on the way. A third refusal applies only where the file claims PAdES — the claim is read out of its /SubFilter, since no document object is left to ask: an object stating a signing time is refused with TCLPDF SIGN SIGNINGTIME, exactly as it is on the -signer way.

::tclpdf::sign add path ?-signer prefix? ?-size bytes? ?-subfilter profile? ?-name text? ?-reason text? ?-location text? ?-contact text? ?-date date? ?-field name? ?-page n?
Adds another signature to a file that is already finished — signed or not — by appending an incremental update (7.5.6) to it. Every byte the file already has stays where it is, which is the whole point: an existing signature covers the bytes of its own revision, and those bytes must not move. The options are the ones sign takes, and they mean the same; what is different is the subject, a path rather than a document, and that is why this is a package command like digest and embed. It answers what was written, including appended — how many bytes the update added.

What it writes is more than the signature dictionary. A signature needs a field, and a field has to be reachable: the update therefore carries a new copy of the catalogue, whose /AcroForm /Fields gains the new field and whose /SigFlags is set, and a new copy of the page object, whose /Annots gains the widget. Both old copies stay in the file, as 7.5.6 prescribes — the added cross-reference section decides which one a reader sees. A file that carries no /AcroForm at all, because it was never signed, gets one. A file certified with DocMDP /P 1 is refused: ISO 32000-2, 12.8.4.2, Table 257 permits no change to such a document at all, and appending a signature is a change that would invalidate the certification signature already there. /P 2 and /P 3 do allow a further signature.

The older signature stays valid, and pdfsig will say something about it that looks like a complaint and is not. It reports Not total document signed for the older one — correctly: that signature covers its own revision, and the file has grown since. The newest signature covers everything, and for it the same tool says Total document signed. Measured on a document signed twice this way: ranges [0 586] [33356 35649] for the first and [0 35785] [68555 69333] for the second, where 69333 is the size of the file. That last number has to be exact — ISO 32000-2, 12.8.1 puts the end of the range at the end of the %%EOF that terminates the update, and one byte beside it turns Total document signed into its opposite without anything being cryptographically wrong.

A PDF/A or ZUGFeRD document survives it — measured: the ZUGFeRD invoice of examples/05.02 signed twice this way comes out of veraPDF with isCompliant true and 0 failed checks over 2528 checks performed, where the unsigned file has 2407, and Mustangproject calls it valid. Nothing of /Info or the metadata stream is rewritten, which is what keeps the claim intact.

Refused by name, rather than written wrong: a visible field, because what a visible field shows is a form XObject and drawing one takes a document — a font, a unit, a layout — while this call has a file and no document; raising the file’s version, because the header lies inside the bytes that stay untouched, so a file below the version floor of the -subfilter asked for is refused instead of being lifted behind the caller’s back — and what the file’s version IS is read off the file rather than off its header alone: a /Version name in the document catalogue counts where it is later (ISO 32000-2, 7.7.2, Table 29, and 7.5.6 NOTE 4, which names it as the one way an incremental update can raise a version at all), so a file with %PDF-1.7 in its header and /Version /2.0 in its catalogue is a 2.0 file here, takes a CAdES signature, and gets its /M in the 2.0 spelling of 7.9.4; and a second signature onto an unfinished one, a file whose newest signature still holds its reserved zeros — digest and embed reach the newest signature of a file, so filling the older one afterwards would not be possible, and filling it first would sign bytes this update then changes. Finish that signature, then add the next.

A file that claims PDF/UA is refused, with TCLPDF SIGN STATE ua, before the update session is opened and therefore before a byte is written. A signature widget is an annotation, and PDF/UA asks three things of the page and the tree it lands in: /Tabs /S on every page carrying an annotation (ISO 14289-1, 7.18.3), a Form structure element around the widget (7.18.4; ISO 14289-2, 8.10.1) and a description (7.18.1). An incremental update can only add objects and replace whole ones, so meeting them means rewriting the page, the parent tree, the element the Form belongs under and the structure tree root — of a file this package did not write, whose number tree may be several levels of /Kids and /Limits deep and whose element nesting differs from one producer to the next. That is the difference between the two entry points: the document method sign knows the tree because it built it, and this call is for files it has never seen. Measured 2026-08-26: a veraPDF-conformant PDF/UA-1 file came out of this call failing clause 7.18.3, test 1 — and a /Tabs alone would have made it validator-green while the widget still stood outside the tree, which is half a repair on a conformance claim and worse than none. The claim is read from the file’s XMP packet, the same pdfua entry ::tclpdf::pdf info answers with. The way to a signed PDF/UA document is sign on the document as it is written, or that document handed to ::tclpdf::sign digest and ::tclpdf::sign embed. The file is left exactly as it was.

::tclpdf::sign timestamp path ?-url url? ?-tsa prefix? ?-hash sha256|sha384|sha512? ?-size bytes? ?-field name? ?-page n? ?-policy oid? ?-timeout ms?
Puts a document timestamp (ISO 32000-2, 12.8.5) onto a finished file, as an incremental update: the digest over every byte of the file is sent to an RFC 3161 timestamp authority, and the TimeStampToken that comes back becomes the /Contents of a /DocTimeStamp signature field with /SubFilter /ETSI.RFC3161. The file needs no signature for this — a timestamp proves that these bytes existed no later than the time in the token, and says nothing about who wrote them. A signed file may carry one too, and any number of further ones (12.8.1); each covers everything before it, which is how an archive keeps a stamped file provable after the previous token’s certificate expires (12.8.5.3). Requires package require tclpdf::timestamp.

The authority is the caller’s choice, with a public default. Without options the request goes to https://tsr.open-tsa.eu — Open TSA, a free RFC 3161 service; free is not qualified, so where law asks for a qualified seal, name your provider’s authority instead. -url takes any RFC 3161 authority over HTTP or HTTPS; https needs the tls package, which is required only at that moment, and the refusal without it names the way around. -tsa takes a command prefix that owns the transport entirely — it is called with the DER request appended and answers the DER response — for an authority behind authentication, a proxy, or no HTTP at all; tests/timestamp.test runs its whole suite through such a prefix against a local openssl ts and never touches the network.

What comes back is checked before it is embedded, not believed. The token has to answer this request: its message imprint must equal the digest that was sent, under the algorithm that was named, and its nonce must be the one the request carried — a replayed or foreign token is refused by name (TCLPDF TIMESTAMP IMPRINT, NONCE), and an authority’s refusal is carried out with its status and its own words (TCLPDF TIMESTAMP STATUS). Only a token that passes is written; every refusal on the way leaves the file byte-for-byte unchanged.

-hash names the digest sent to the authority, sha256 unless said otherwise; SHA-1 is not offered. -size is the room reserved for the token, 8192 unless said otherwise — a token that does not fit is refused with the size to ask for, measured on the answer rather than guessed. -policy asks the authority for a specific policy OID, for providers that issue under several. The answer names the field, the byte range, the token’s length, its serial and its genTime — the authority’s statement of when, which is the whole point: there is deliberately no /M in the dictionary, because the time is in the token and a second statement beside it could only disagree.

A PDF/A file survives it — the update only adds objects, and make check holds the stamped example (examples/08.06) against veraPDF like every other document. A file with a signature still waiting for its value is refused, as it is for add; so is a certified file with /P 1.

Writing

Two calls write the document, and neither finishes it.

PDF 2.0 differences that are written for you. A 2.0 file spells the zone offset of a date without the trailing apostrophe (7.9.4), and the version decides it — declaring ua -part 2 is enough. ProcSet, CharSet and CIDSet, all deprecated in 2.0, are written by no version of this package. The text keys of the information dictionary — Title, Author, Subject, Keywords, Creator, Producer — are deprecated in 2.0 as well (14.3.3, which excepts CreationDate and ModDate) and are written all the same: they are what a 1.x reader shows in its document properties, deprecated is not forbidden, and the XMP packet carries every one of them under the property Table 317 pairs it with.

doc write path
Writes the document to a file. Writing does not finish the document: a second write of an unchanged document produces a byte-identical file — unless the document is signed, where /M and the CMS object carry the moment of the write, or encrypted, where every string and every stream takes a fresh initialisation vector and reusing one would be the mistake; there the second file is a different sequence of bytes carrying the same objects, opened by the same password. Drawing between two writes works either way — the second file carries the additions. The file identifier is the exception to that: both strings of /ID are made once, when the document is created, so a document that has been drawn on between two writes carries the same pair in both files. ISO 32000-2, 14.4 wants the second string to change with the file’s contents, and ::tclpdf::update does recompute it for each incremental update; the write path does not. A path whose directory does not exist, or is a file, or a path that is itself a directory, is refused with TCLPDF IO DIRECTORY before anything is opened. Everything else — a read-only directory, a full disk — stays with the operating system’s own message.

doc writeChannel channel
Writes to an open channel instead of a file — a CGI response, a socket, a pipe. The caller opens and closes it; the channel is put into binary translation here, because that is what decides whether the bytes arrive unchanged. The cross-reference offsets are counted from the %PDF- header as they are written, not asked of the channel, so a pipe or a socket — where tell answers −1 — and a channel that already carries a CGI header get a correct table. write and writeChannel may be combined freely — the same document can go to a file and into a response; the one exception is a document prepared with sign, where writeChannel is refused by name, because /ByteRange and the signature are written into the finished file and a channel cannot be read back.

Events

doc on event script / doc off token / doc events / doc subscribers ?event?
The document publishes events while it is written: beforeWrite, resources, catalog, info and afterWrite, plus pageAdded after each page add — the ones text -paginate and a breaking table add included. This is how attachments, ZUGFeRD and the output intent attach themselves without the core knowing about them, and it is available to callers for the same purpose. on returns a token and refuses an event that is not one of these six, naming them; events answers the list. off takes that token, not the event and script again, and accepts an unknown one silently. Subscribers run in registration order and are called with the emitting object followed by whatever the emitter passes on. The write-time events fire on every write, so a subscriber that creates objects must be idempotent: take its object numbers from reservation once and write over them on later runs, instead of reserving fresh ones each time. How to build an extension on top of this — the supported methods, the contracts and two worked examples — is the subject of doc/PLUGINS.md in the source distribution.

Error codes

Every refusal is a Tcl error whose message begins with tclpdf: and names what went wrong and where. A number that is not one — NaN, Inf — is turned away where it is read as a measurement rather than where the arithmetic later trips over it, so the message names the option the caller wrote. A colour component is not a measurement and is clamped rather than refused where it merely leaves its range, but NaN, Inf and -Inf are turned away there as well: none of the three merely left a range. The wording of a message is not a contract — it may be sharpened in any release. Where a script is meant to handle a refusal programmatically, the error also carries a machine-readable -errorcode, and that is a contract: a Tcl list whose first word is TCLPDF, followed by a fixed hierarchy and then the facts of the case. try … trap matches such a list by prefix, so trap {TCLPDF FONT GLYPH} {msg opts} {…} handles the whole class below without reading the message; the full list is [dict get $opts -errorcode]. Every refusal this package makes carries one, measured over the whole tree and held there by a guard in the suite: on 2026-08-25 five hundred and eleven of them did not, and they had accumulated one module at a time — each of them individually harmless, together a coin toss over whether a given mistake could be handled at all. The first two words are the contract: TCLPDF and the topic. Where a topic has classes, the third word is one of them and is a contract too; what follows are the facts of the case, and those may grow. The codes described one by one below are the ones a script is most likely to want; the table at the end of this section lists every topic and every class in the package. Further codes may be added in later releases, and a class may gain members — a trap written against a prefix keeps working when that happens.

TCLPDF FONT GLYPH codepoint position fontname
A character cannot be written with the font in use — it lies outside the font’s encoding, or the face carries no glyph for it. codepoint is the character in U+XXXX notation (uppercase hexadecimal, four digits or more), position its 0-based index in the string as handed in, and fontname the name the document knows the font by: the PostScript name of a standard face, or the alias given to font embed. The same code is thrown wherever the refusal happens — drawing, measuring, breaking a paragraph, a table cell — so one handler covers them all; a typical one retries the string with a fallback face, which is what examples/02.04-missing-glyphs.tcl does. For text drawn right-to-left the codepoint named is the mirrored partner of the character handed in — the glyph the face would have to carry — exactly as the message names it. codepoint is always a whole character: a character beyond the BMP is named as one code point under Tcl 9 and under Tcl 8.6 alike, never as one half of the surrogate pair 8.6 stores it in. position counts code points as well, on every road: the paragraph breaker names the same index as textWidth does, under both interpreters. A variation selector gets its own advice. U+FE0F and its relatives modify the character in front of them and are part of the text rather than characters of their own, and a face built for ✅ refuses ✅️ — the two look identical in every editor — so the message says what a selector is and that the spelling is registered with font glyph or colorFont -chars rather than sought as a glyph.

TCLPDF FONT KIND kind alias
The font alias is of a kind the writer has no case for — thrown while the document is being written, not while the font is being taken in. The kinds written are truetype, type1 and type3. It is reached only by a font entry the package did not build itself, and it exists because the alternative is worse: before it, an unknown kind fell through to the TrueType branch and died inside a dict get, naming a key rather than the font.

TCLPDF FONT OUTLINES alias path
Every character the face covers has an empty outline, so the face draws nothing — thrown by font embed, before the face is taken into the document. alias is the name the call asked for and path the file as it was handed in. This is what a colour font looks like from the outside: its pictures sit in a COLR/CPAL, CBDT, sbix or SVG table, none of which this package writes, and a document embedding it would come out blank with nothing anywhere reporting it — not the API, not pdftotext, not pdffonts, not qpdf, not veraPDF. The refusal hangs on the empty outline and not on the colour table, so a colour face carrying real monochrome fallback outlines is embedded as any other; one empty outline among many, which is what the space is in most faces, is not this case. Where the face names the table its pictures are in — an sbix, CBDT or SVG table with no COLR beside it — that name is the fourth word of the code and the message says which table it is. A handler embeds a monochrome face instead, or draws the symbols as a Type 3 font with font define and font glyph.

TCLPDF FONT SOURCE format
What was handed to font embed is not a font of the format the road it took expects — thrown by the readers behind the facade, before anything is taken into the document. format names the reader that turned it away: sfnt for a TrueType or OpenType file, cff for a bare CFF table, type1 for a .pfb, .pfa or .t1. This is the answer that says wrong format, and it is worth telling apart from the next one: a script that tries a file through several readers retries on this code and gives up on the others. The reader that answers is the last one the file was offered to — Type 1 first, then the bare CFF, then the sfnt — so a file whose own header is too damaged to be recognised as either of the first two is turned away as sfnt, whatever it was meant to be: a bare CFF whose header names an offSize outside 1 to 4 is that case.

TCLPDF FONT DAMAGED part
The file is of the right format and is broken — a table directory that ends inside an entry, a cmap promising more groups than the file holds, a PFB segment reaching past the end, a CFF whose Top DICT is empty. part names what was being read: directory, cmap, cff, charset, collection (a .ttc whose directory ends inside an entry), head, pfb, type1, vhea. Retrying such a file with another reader is pointless, which is the whole reason it does not share a code with SOURCE.

TCLPDF FONT UNSUPPORTED format ?detail?
The file is sound and is a format, or a variant of one, that this package does not read: collection (a .ttc handed to a reader that was given no face number — font embed and colorFont take -face and never reach this), cff2 (the variable-font CFF, a different charstring interpreter altogether), cmap with the format number after it (formats 4 and 12 are read), charset and encoding for the predefined CFF tables. Nothing is wrong with the file; a handler converts it once, outside, or picks another cut of the face.

TCLPDF FONT FACE asked
-face names a face a TrueType collection has not got, or is not a face number at all — thrown by font embed and by colorFont, before anything is read. asked is the value as it was written, and the refusal comes at the call, before the file is opened: a -face that is not a whole number of 0 or more used to arrive as TCLPDF FONT ARGUMENT face, which says “this option does not belong to this kind of face”, where the question is the one colorFont answers as TCLPDF COLORFONT FACE and the answer is the same word. A .ttc is a directory of faces and the message names how many it holds, counting from 0; a file that is not a collection has one face, which is face 0, and any other number is refused with a message saying so. 0 is the default everywhere, so a caller who has never heard of collections writes nothing and gets the face they expect.

TCLPDF FONT STREAMED tag
The tag table of this face is read from the file by range and is not held in memory — thrown by the internal table accessor, and only for a face opened out of a very large file. The bitmap tables (sbix, CBDT, EBDT) are the only sfnt tables that reach hundreds of megabytes: Apple Color Emoji’s sbix is 191 MB of a 192 MB file, so it is left where it is and read a record at a time. This code exists because answering the empty string instead would tell a caller that a face which is nothing but sbix has no sbix at all. Nothing a public call does raises it.

TCLPDF FONT TABLE tag
The face has no tag table and cannot be embedded without it — head, hhea, hmtx and maxp for any sfnt, CFF for one whose signature promises CFF outlines, and glyf (which stands for glyf and loca together) for one whose signature promises TrueType outlines, as ISO 32000-2, 9.9, Table 128 requires of a FontFile2. A cmap is deliberately not among them: a subset this package built carries none on purpose, and demanding one would make the package unable to re-read its own output.

TCLPDF FONT SUBSET reason ?alias?
A face cannot be reduced to the glyphs a document uses. outlines is the only reason there is: the face carries no glyf and no loca, so there is nothing to rewrite. No call of the public API reaches it any more — since round 7 such a face is turned away by font embed with TCLPDF FONT TABLE glyf, at the call that named it rather than at write — and it is kept as the last line of defence for a caller of ::tclpdf::subset build itself; the alias, where one was passed, is the fourth word. A CFF face never reaches this code either: its outlines are charstrings, and it is embedded whole (see -subset). A second reason, table, is internal to the subset builder and names a table it asked its own output for and did not find; no input can produce it, and it is here because the class is generated from the source and this list is complete or it is nothing.

TCLPDF TEXT ANCHOR anchor
-anchor was given a word that is none of the four: baseline, top, middle, bottom. Refused rather than read as baseline in silence, which is what a misspelt anchor used to do.

TCLPDF ANNOT LINE end / TCLPDF ANNOT BOX kind / TCLPDF ANNOT POINTS kind
A geometry annotation is missing the geometry it is made of — the named end of a line (-from or -to), the corner and extent of a square or circle, or at least two points for a polygon or polyline. Raised before anything is written.

TCLPDF ANNOT WIDTH kind width
-width of a geometry annotation is the thickness of the line it is drawn with and has to be above zero.

TCLPDF ANNOT ATTACHMENT what …
A file attachment annotation could not be made. AT — no -at; NAME — no -name; UNKNOWN with the name after it — nothing was attached under that name, so attach has to come first; ICON with the value after it — -icon is one of the four of Table 184.

TCLPDF TABLE SPAN what / TCLPDF TABLE COLUMN key / TCLPDF TABLE WIDTH what / TCLPDF TABLE STYLE key
A table could not be laid out: a rowSpan or colSpan that reaches past the last row or column or is not a positive integer, a column property that is not a number, fixed column widths that do not fit the table’s own width, or a style key with a value outside what it admits. All of them are raised before a cell is measured, so a table with a mistyped value draws nothing at all rather than half of itself.

TCLPDF TABLE CELL what
A cell said something about its content that cannot be true: text for a dictionary that names neither text nor runs, runs for a list of an odd length or beside text, markup for the pair form {markup {markdown …}} or beside runs, decimal for a body cell of runs in a decimal column, a list included, and paragraph heading for a heading inside a cell. All before a cell is measured.

TCLPDF TABLE DIRECTION direction
A table style asked for a writing direction a table cannot set. ttb is the case: a table breaks its cells by the column width and steps the lines down the row, which is the direction a vertical line already writes in. There is no vertical table — a page of columns is textLines -direction ttb for the breaking and one text call per column for the placing.

TCLPDF GRAPHICS OVERPRINT what
An overprint call is wrong in itself. what is fill or stroke where the value is not a boolean, mode where it is neither 0 nor 1, and NONE where the call named none of the three switches and would have written a graphics state that changes nothing. A wrong PDF version is not this code: asking for -fill or -mode below PDF 1.3, or -stroke below 1.2, raises TCLPDF VERSION like every other version floor.

TCLPDF IMAGE INTERPOLATE PDFA alias …
A picture was placed with -interpolate 1 in a document claiming PDF/A. Raised at the placement where the declaration came first, and by pdfa where the picture came first — in that direction the code carries every alias already in the file rather than one. Drop -interpolate, or drop the declaration; there is no third answer, since the key is written into the picture’s dictionary and the dictionary is in the file by the time the profile is claimed.

TCLPDF COLORFONT TABLES alias
The face has no COLR and CPAL tables and therefore no colour glyphs — thrown by colorFont, before anything is defined. alias is the name the call asked for. An ordinary face goes into the document with font embed.

TCLPDF COLORFONT CHAR codepoint alias
The face has no glyph for codepoint, so there is no colour glyph to draw for it — thrown by colorFont, and before the font is defined, so nothing is left half built. codepoint is in U+XXXX notation, and the message names its position in -chars. One character is exempt and is not refused here: a variation selector the face has no glyph for rides along with its neighbour instead of being looked up. A -chars consisting of nothing but selectors has no neighbour to attach to and does reach this code.

TCLPDF COLORFONT SPLIT codepoint alias
The ccmp feature of the face draws one of the characters of -chars as several glyphs — a Multiple substitution, where one of the pieces carries the text and the others carry none — thrown by colorFont, before the font is defined. A Type 3 glyph is one drawing with one advance and cannot be two, so such a face cannot be built into a colour font. codepoint is the character that was taken apart, in U+XXXX notation, and the message names its position in -chars and one of the pieces by glyph number — the same two facts TCLPDF COLORFONT CHAR names, because a glyph number alone is nothing a caller can look up in the string they wrote. No colour face measured here does it: an emoji face’s ccmp holds ligatures and contextual lookups and no Multiple substitution at all, which is why this is a refusal and not a road. Set the face with font embed instead. A sequence a face draws as two glyphs and positions — the second bound to the first by a GPOS mark lookup, as Apple Color Emoji does for 968 of its multi-person sequences — is not this refusal: the two are folded into one Type 3 glyph with one advance. This code is for a glyph that carries no text of its own and that nothing binds to anything.

TCLPDF COLORFONT COMPOSITE …
Two cases, told apart by what follows the class. With codepoint glyph alias a shape of codepoint — a version 0 layer, a version 1 PaintGlyph, or the plain outline of a glyph the table says nothing about — is a composite glyph: it draws other glyphs rather than an outline of its own, and this package assembles those only when a face is embedded. The shapes of a colour glyph are normally drawn figures rather than letters with accents, so this is unusual; a handler sets the character with an embedded face. With alias plus a PaintComposite asks for the Plus composite mode, which adds the two colours and clamps the sum: PDF has fifteen blend modes (ISO 32000-2, 11.3.5) and no additive one, and none of the fifteen can be combined into it, so the glyph would come out darker than the font says with nothing reporting it.

TCLPDF COLORFONT EMPTY codepoint alias
codepoint would draw nothing — every version 0 layer has an empty outline, or every shape of its version 1 paint graph is an outline the face has not got or every fill in it is fully transparent, or the glyph has no COLR entry at all and no outline of its own either, or — for a bitmap face — it has no picture in the strike that was chosen. Refused for the same reason as TCLPDF FONT OUTLINES one level down: the document would be valid, extractable and empty, with nothing anywhere reporting it.

TCLPDF COLORFONT FOREGROUND alias
A colour stop of a version 1 gradient names palette index 0xFFFF, the sentinel for “the colour the text has” (ISO/IEC 14496-22, 5.7.11). A PDF shading carries its colours in the file and is written when the font is defined, while the text colour is chosen every time the glyph is set, so there is no colour to write. The same sentinel works in a solid fill, where the glyph simply sets no colour of its own. Measured over a real version 1 face: not used in any of its 18 471 colour lines.

TCLPDF COLORFONT UNBOUNDED codepoint alias
The version 1 colour description of codepoint paints outside every shape in it and the table gives no clip box to bound it. Such a glyph “must not be rendered” (ISO/IEC 14496-22, 5.7.11), because it has no size a page could reserve for it: a solid fill and the three gradients are unbounded by definition, and only a PaintGlyph or a clip box bounds them.

TCLPDF COLORFONT PAINT kind
The drawing side was handed a paint node it does not know. It is a guard against the reader learning a paint format the drawer has not, and no font can raise it.

TCLPDF COLORFONT LIMIT count
-chars names more than 255 glyphs and a Type 3 font is addressed by single bytes, so it holds at most 255 (ISO 32000-2, 9.6.5.3); count is how many were asked for. Counted in glyphs and not in characters — a sequence of seven characters is one glyph — and therefore counted after the face’s ccmp has been applied and before the font is defined, since it is the face that says where the sequences are. A handler builds a second font for the rest and names both in -fallback.

TCLPDF COLORFONT FACE asked
-face of colorFont is not a face number — a whole number, 0 or more. asked is the value as it was written. A number that is one and that the file has not got is refused one step further on, by the reader, as TCLPDF FONT FACE.

TCLPDF COLORFONT STRIKE …
Two cases, told apart by what follows the class. With a value that is not a positive whole number, -strike was not a pixel size at all. With an alias, -strike was given for a face that draws its colour glyphs with the COLR table — paths, at every size — where there is no strike to choose. A size the face does not carry is refused by the reader instead, as TCLPDF SBIX STRIKE, whose message lists the sizes it does carry.

TCLPDF COLORFONT IMAGE name
An image of the caller’s own is already embedded under the name colorFont builds for one of its pictures — alias#sbixN, where N is the glyph the picture belongs to. Refused rather than reused: what would otherwise reach the page is the caller’s picture in place of the glyph. Embed that one under another name.

TCLPDF COLORFONT PALETTE asked count
-palette names palette asked and the face has count of them, numbered from 0. Checked before anything is built — the reader would refuse a wrong number too, but only on the first layer that is not the text-colour sentinel, so a glyph that takes the text colour throughout would get away with it until the next one.

TCLPDF SBIX …
What the sbix reader itself refuses — the codes colorFont passes through unchanged, since they say something about the face rather than about the call. VERSION (an sbix table above version 1), DAMAGED (a header, a strike, a record or a PNG that ends before its own format promises; the word after it says which), STRIKE (-strike names a pixel size the face has not got, with the sizes it has in the message), FORMAT (a picture in a format other than png — jpg and tiff exist in the standard, 'pdf ' and 'mask' are Apple’s own, no face measured here uses any of them, and a refusal that names the format is worth more than a road that has never been run) and CYCLE (a dupe or flip record naming a glyph that names its way back to one already passed through, or a chain of more than eight of them). A flip record is not a picture but another glyph’s number, drawn left to right — Apple Color Emoji uses it 108 times, for the runner and the walker facing the other way, and no edition of ISO/IEC 14496-22 describes it — and the two glyphs then share one image XObject. A dupe or flip naming a glyph that lies outside the face, or one with no picture in the chosen strike, is DAMAGED too: such a record exists to say where the picture comes from, so one that points at nothing is damage, and read as “this glyph has no picture” it sent the glyph to its own outline — and the outlines of an sbix face are empty boxes, which is a blank glyph in a valid document.

TCLPDF MORX …
What the morx reader itself refuses. VERSION (a table above version 3; version 1 is mort, a different table under a different name), DAMAGED (a header, a chain, a subtable, a state table or a lookup that ends before its own format promises, or a state table declaring fewer than the four glyph classes the format reserves; the word after it says which) and LOOKUP (a lookup in a format the standard does not define — 0, 2, 4, 6, 8 and 10 are the six it does). A subtable of a type this reader does not know is skipped rather than refused: a chain is a list of independent steps, and a face is not unusable because one of them is unknown, which is what HarfBuzz does too.

TCLPDF COLR …
What the COLR/CPAL reader itself refuses, one class rather than an entry apiece — these are the codes colorFont passes through unchanged, since they say something about the face rather than about the call. Fourteen of them. Seven belong to both table versions: MISSING (the face has a COLR table and no CPAL, or neither — the word after it is the table that is absent), VERSION (a COLR above version 1, or a CPAL above version 1; the two words after it are the table and the version found), EMPTY (a COLR declaring no base glyphs in either half of it, or a CPAL holding no colours; the word after it is the table), TRUNCATED (a table that ends before what its own header promises; the word after it is the table), RECORDS (a palette whose colour run reaches past the last colour record), PALETTE (-palette names a palette the face does not have — the same case TCLPDF COLORFONT PALETTE catches at the call, reached here only by a face whose header and body disagree) and ENTRY (a layer naming a palette entry that is not there). LAYERS covers both formats’ way of overrunning a layer array: a version 0 base glyph whose layer run reaches past the last layer record, or a version 1 PaintColrLayers whose slice reaches past the LayerList. Five belong to the version 1 paint graph alone: CYCLE (the graph comes back to a paint table it has already passed through — a colour glyph that contains itself, which the two indirect links of the format, PaintColrLayers and PaintColrGlyph, make possible), DEPTH (a graph nested more than 64 tables deep, where a real face measures 9), REUSE (a PaintColrGlyph naming a base glyph the BaseGlyphList has not got) and PAINT (a paint format this reader does not know — the standard says such a format “should be ignored” and the rest of the glyph drawn, and this package refuses instead, because what is ignored comes out as a symbol missing a piece, valid, printable and unreported). COMPOSITE names a composite mode outside the enumeration, which ends at 27 — the standard says such a value is to be read as COMPOSITE_CLEAR, which paints nothing, and this package refuses for the same reason it refuses an unknown PAINT. UNSUPPORTED names a value outside one of the three smaller enumerations of the format — a colour line’s extend mode, a ClipList format, a ClipBox format — with the enumeration and the value after it. All three are refused for the reason PAINT gives, and the ClipList is refused although the standard calls it an optimisation: a clip box is a clip, so a graph that paints outside every outline in it is bounded by the box and TCLPDF COLORFONT UNBOUNDED without it, and a box smaller than the outlines cuts off a piece that would otherwise be drawn. No font written to the present edition reaches any of the three. Each message says what the table holds and what was asked of it; trap {TCLPDF COLR} catches the lot, and trap {TCLPDF COLORFONT} does not, which is why they are written out here.

TCLPDF TIFF STRIPS
A TIFF has more strips than this package will stack. A compression that carries state from one row to the next begins afresh in every strip, so such a file becomes one image XObject per strip; above 256 of them the picture is refused rather than placed, and nothing is written for it. Measured over 194 files: 80 are single-stripped, 13 lie above this line, and the largest has 1922 strips. The message names how many strips the file has and where the line lies. The way out is to re-save it with a RowsPerStrip that holds the whole picture, or with no compression or PackBits, whose strips are joined into a single stream.

TCLPDF TIFF STACKED
A TIFF that is several image XObjects was asked to be a mask, or to wear one. Mask and SMask name a single image XObject (ISO 32000-2, Table 87 and Table 143), and a mask laid over a stack would be drawn over each part in turn instead of over the picture — every part is an image of its own on the unit square (8.9.6.3). A TIFF that reaches the file as ONE image is refused neither way: it may be a soft mask like any greyscale picture, and it may wear one. Thrown when the picture’s objects are built, so nothing is written for it; a picture already placed on an earlier page is caught too.

TCLPDF TIFF …
What the TIFF reader itself refuses, one class rather than an entry apiece: SIGNATURE (not a TIFF at all — measured in a real holding, two of them were mail texts named .tif), BIGTIFF, TILED, PLANAR, ALPHA, CCITT, COMPRESSION, PHOTOMETRIC, ORIENTATION, PALETTE, STENCIL, STACKED, STRIPS, DEPTH, SAMPLES, SAMPLEFORMAT, PREDICTOR, FILLORDER, BYTEORDER, DIRECTORY, CIRCULAR, RANGE and DAMAGED. There is no JPEG class any more: a strip or a JPEGTables tag that is not JPEG is DAMAGED jpeg, and a directory pointing past the end of the file is DAMAGED range, while RANGE is kept for the one refusal that really is a range — a strip index the caller made up. Each names what the file is and what it would have to be re-saved as; trap {TCLPDF TIFF} catches the lot.

TCLPDF IMPORT …
What the PDF reader refuses in a foreign file, one class rather than an entry apiece: ARGUMENT, BOX, DEPTH, ENCRYPTED, FILE, FILTER, FOREIGN, NAME, OBJECT, OBJSTM, OPTION, PAGES, PREDICTOR, RECURSION, ROOM, ROOT, ROTATE, SERIALIZE, STREAM, SUBCOMMAND, SYNTAX, XFA and XREF — twenty-three of them. trap {TCLPDF IMPORT} catches every one of them, under Tcl 9.0 as under 8.6, and they reach the caller from pdf import, from pdf info, pages, fonts, metadata and fields, and from ::tclpdf::update open alike — the commands share one reader. A damaged or hostile file is the case they exist for: a cross-reference chain that runs in a circle, an object stream that contains itself, a /Length that points at its own object, a page tree that names itself among its children, an array whose bracket never closes, a number written in exponential form, an object stream announcing twenty million objects in four hundred bytes, a deflate stream that unpacks without end.

How far a deflate stream is followed. /Length says how many bytes to read and the data say how many come out, which is the one header number of a foreign file that binds nothing: a 306 KB file whose page content unpacks to 300 MB was read, and 858 MB of memory spent, before the import accepted it. A stream is followed to 64 MiB and refused past it (TCLPDF IMPORT ROOM). The figure is measured rather than felt: over 3984 foreign PDF files the largest decoded content stream of a first page is 7.75 MiB. This is a matter of availability, not of security — the package still makes no promise about a hostile file. Each of those used to end in a loop that did not return or in a raw Tcl error naming an operand instead of the file; naming the file and what is wrong with it is the whole point of the class.

TCLPDF HYPHENATE LANGUAGE tag
No hyphenation patterns are loaded for tag — thrown by ::tclpdf::hyphenate word and ::tclpdf::hyphenate languages tag, and by every block that names an unloaded language with -hyphenate — a table whose style carries hyphenate among them, where the whole table is refused before its first cell is drawn — before a byte of it reaches the stream. tag is the spelling that was asked for, not one it might have resolved to. The refusal is the point: a block whose language is missing could only be set unhyphenated, and that produces a document which looks like the one that was asked for and is not. A handler has three ways on — load the file, fall back to another language, or set the block without -hyphenate and know that it did.

TCLPDF HYPHENATE UNSET
-hyphenate 1 asks for the language the document declares and the document declares none. The code carries no further words: there is one way into it and one way out of it. A handler either calls language first or names the language on the block itself, or, from a table, in the style that carries hyphenate.

TCLPDF SIGN SPACE needed reserved
A CMS object does not fit into the space a signature reserved for it — thrown by write where a sign -signer prefix handed back one that is too long, and by ::tclpdf::sign embed for the same reason. needed is the length of the DER object in bytes, reserved what sign -size put aside, and the two are what a handler needs: build the document again from the start with sign -size at needed or more. It is the document that has to be built again, not just written again — sign is called once and refuses a second call, so -size cannot be raised on a document that already carries a declaration, and the space is reserved before the digest is computed and cannot be grown afterwards without moving every byte behind it. Where the second stage failed instead, ::tclpdf::sign embed raises the same code against the file’s placeholder, and the placeholder is as fixed as the offsets around it: that file has to be produced again too. The refusal is the point — a signature written into too little room would have to be truncated, and a truncated one produces a file that every reader calls signed and no verifier accepts. The space is reserved before the digest is computed and cannot be grown afterwards without moving every byte behind it, which is why this is an error at the write rather than a repair.

TCLPDF SIGN SIGNINGTIME
A CMS object states the time of signing where the document claims to be a PAdES signature — thrown by write where a sign -signer prefix handed back such an object, and by ::tclpdf::sign embed for the same reason, in both cases only under sign -subfilter cades. The code carries no further words: there is one way into it and one way out of it, and neither needs a number. ETSI EN 319 142-1, Table 1 puts the signing-time attribute at “shall not be present” for every PAdES baseline level, while requiring the time in /M, which this package writes. The check looks for the attribute’s object identifier, 1.2.840.113549.1.9.5, in the DER as a byte pattern; it does not parse ASN.1, and it is the one place this package looks inside the object at all. A signature that carries an RFC 3161 timestamp token is not caught by it, which matters because that is what a PAdES B-T signature is: such a token holds a SignedData of its own with a signingTime inside it, and refusing on that would refuse the very objects the profile asks for. The two are told apart by position — RFC 5652 puts signedAttrs before unsignedAttrs, so an outer signing time always stands before the first token identifier (1.2.840.113549.1.9.16.2.14) and one belonging to a token always after it — and the refusal falls only where the time comes first or where there is no token at all. An object that carries both is refused as before. The price of that coarseness is stated rather than hidden: certificates stand before the SignerInfos in a DER, so this identifier inside a certificate would be read as an outer signing time; -subfilter pkcs7 is the way past it. A handler has two ways on: have the object made by a signer that leaves the attribute out — pyHanko and the EU DSS library do so by themselves, openssl cms -sign does it with -no_signing_time (OpenSSL 3; the LibreSSL shipped as /usr/bin/openssl on macOS has no such switch), while BouncyCastle adds it unasked — or write the document with -subfilter pkcs7, which claims no PAdES and takes the object as it is. Note what the coarseness costs: the same identifier inside an embedded RFC 3161 timestamp token would be refused too, and this package produces no such token, so an object carrying one was assembled elsewhere by someone who knows what is in it.

TCLPDF FIELD …
What an interactive form field refuses, one class rather than an entry apiece: trap {TCLPDF FIELD} catches all of them — a misspelt option name excepted, which the shared option parser refuses under TCLPDF OPTION UNKNOWN below, here as everywhere in this package — and none of them leaves anything behind — a refused call leaves the document exactly as it was, so a second attempt does not trip over the leavings of the first, and a refused write leaves no file. The word after FIELD names the case. About the field itself: NAME (no name, one holding a period, or one holding a control character — a NUL, a tab or a line break), TAKEN (a name already used, by another field or by sign -field), UNKNOWN (field state asked about a field that was never declared), SUBCOMMAND and TYPE (a word that is no field type), PAGE, RECT (not four numbers a PDF can hold, or no area — and RECT OUTSIDE, at the write, for a rectangle lying entirely beside its page), STRUCTURE (a widget that cannot go into the structure tree where it stands). About the values of a call: BOOLEAN, ALIGN, SIZE, BORDERWIDTH, COLOUR, COLOURSPACE (a colour with no /DA or /MK spelling), FONT, GLYPH (a value, a default, a caption or an option the field’s own face cannot set — which is value, default, caption or options), MAXLEN, LENGTH, LINES, PASSWORD, COMBINATION (two options that cannot both be given), OPTIONS, INDEX, SELECT, TOP. And FLAG, SIGFLAGS, REGISTER, DECLARE and BUILD, which are reached only by a field type of one’s own. The three button types put their own word after TCLPDF FIELD BUTTON: BUTTONS, CAPTION, ACTION, FIELDS, EXPORT, LABEL, MARK, MARKSIZE, VALUE, UNISON, NOTOGGLE, CONTENTS, BOOLEAN, BORDERWIDTH, COLOUR and the PDFA below. Each message names what was asked for and what the standard says about it; the facts of the case follow the class, as everywhere here.

TCLPDF FIELD BUTTON PDFA name
The push button name carries -action reset and the document claims PDF/A — thrown when the document is written, and not at the call, because a document declares its profile and its fields in either order and the check can only be made once both are known. Three field refusals fall there rather than at the call, and they are the three that depend on something the call cannot yet know: this one, TCLPDF FIELD PAGE for a page that never came, and TCLPDF FIELD RECT OUTSIDE for a widget entirely beside its page. The profile admits no action on a widget annotation at all (veraPDF 6.3.3-3) and forbids ResetForm by name (6.5.1-1). A handler drops the button, or drops the PDF/A declaration; the refused write leaves no file behind, so the same script may write again to the same name.

TCLPDF ANNOT …
What a note, a stamp or a text markup refuses, one class rather than an entry apiece: trap {TCLPDF ANNOT} catches all of them — a misspelt option name excepted, which the shared option parser refuses under TCLPDF OPTION UNKNOWN below — and none of them leaves anything behind, since every refusal is made before a byte reaches the file — the colour, the opacity and the date included, which used to be checked after the appearance stream had been built and therefore written: a refused annot square -opacity 5 left a form XObject in the file that nothing pointed at. The word after ANNOT names the case. About the call: KIND (an annotation this package does not write; the message names the ones it does), MODULE (a registered kind whose module does not provide it, reached only by a module of one’s own), NOTE with AT, CONTENTS, ICON or OPEN, STAMP with AT, NAME, SIZE or BOTH (both -name and -appearance), RECT with AT, SIZE, NUMBER or EMPTY, APPEARANCE (a form of that name was never created) and STRUCTURE (a tagged document where an Annot element may not stand). About a markup’s geometry, all under QUADS: NONE and BOTH (no road to the rectangles, or two), AT, SHAPE, NUMBER, EMPTY, LINES (a line break in -text), EMPTYLINES, EMPTYTEXT, LEADING, ALIGN (-align justify, which the measurement cannot follow) and TYPE3 (a face with no descender to measure with). About the values: COLOUR (-colour and -color both given), COLOURSPACE (a colour /C has no spelling for), OPACITY, DATE. And three about a claim the document makes: PDFA subtype, for an archived document that would get an annotation with no appearance stream; UA CONTENTS subtype, for one with no description; UA STRUCTURE subtype, for one outside the structure tree. The last three are thrown at the call where the claim already stands, and at the write where it was made afterwards — a document declares its profile and its annotations in either order, and only the write has seen both. A refused write leaves no file behind, so the same script may write again to the same name.

TCLPDF OPTION …
What the shared option parser refuses — every command of this package reads its -name value arguments through it, some seventy-five calls out of forty-three modules, so this is the code a mistake in the call carries whatever the command was, and trap {TCLPDF OPTION} catches the lot. The word after OPTION names the case, then come the facts: the option or key that was named, and last the context — field text, text, pdf import — which is empty where the calling command passed none. Eight of them: UNKNOWN option context (no such option; the message lists the ones that exist, which is what a caller who wrote -colour needs), PAIRS context (an odd number of words, so an option was left without its value), REQUIRED option context (an option that has to be given was not), POINT option context, NUMBER what context, ZOOM option context (a magnification below 0, or one that is not finite), and DICT what context and KEY key what context for the places a caller writes a dictionary rather than options: a table cell, a style, a column description.

POINT and NUMBER divide the same mistake between them, and which of the two answers depends on the road the option took. The text, link, annotation and field calls read a coordinate through the shared parser, and it answers POINT for every way of getting one wrong: not two words (-at {20 [expr {$y+5}]} is the usual way in, since braces do not substitute), a word that is no number, a NaN or an Inf, or a number beyond what a PDF real can hold. The drawing primitives — rect, line, circle, arc and their kin — check the shape of a point in their own words, TCLPDF GRAPHICS POINT option, and hand the values to the shared parser, which answers NUMBER option command for a NaN, an Inf or a number the file cannot hold; a word that is no measurement at all is TCLPDF GEOMETRY MEASURE. A handler that means “this position is wrong, whoever was asked” therefore traps TCLPDF OPTION and TCLPDF GRAPHICS POINT together. A single measurement — a width, a size, an angle, an opacity — is refused by the call that reads it and under that call’s own class (TCLPDF GRAPHICS ARGUMENT, TCLPDF TEXT ARGUMENT, TCLPDF FIELD SIZE and their kin); NUMBER is what the shared parser answers where a measurement goes through it. NaN and Inf are refused on every one of those roads, and for the same reason: both are doubles as far as Tcl is concerned, and both place nothing. A misspelt option name was long the one refusal in this package that reached the caller as NONE, which made the commonest mistake of all the one a handler could not see: trap {TCLPDF FIELD} caught every wrong value and no wrong name. The caller’s own spelling is a fact behind the class word rather than the class itself — a code of TCLPDF OPTION -recht would read the mistyped word as a class of its own, and nothing below TCLPDF OPTION could then be trapped.

TCLPDF TEXT HYPHENS value context
-hyphens was given a value that is no boolean. value is what was written, context the call that read it — textLines or textHeight. Refused by both, although only textLines reads the option, so that a caller who builds one option list is told at the first of the two calls and not at the second.

TCLPDF VERSION version
A feature was asked for that the document’s PDF version does not offer — opacity in a 1.3 file, a form field in a 1.1 one, layer create below 1.5, field -label below 2.0. version is the version the document has to be at for the feature, which is what a handler needs: create the document with -version at that number, or raise it with configure -version, or leave the feature out. Nothing is ever raised behind the caller’s back — whoever set -version 1.3 said what the file may contain — and the code is the same from every module, some sixty-five calls in the package, because they all ask through one place. The two mirror refusals carry the same code and a different meaning for its fact: lowering the version below a feature the document already uses answers TCLPDF VERSION 1.4 — the version the document is at and may not go below — and raising it past a ceiling a claim such as pdfa -part 3 has fixed answers TCLPDF VERSION 1.7, the ceiling rather than a floor. Both are trapped as TCLPDF VERSION; the message says which of the three cases it is, and a handler that reads the number as “raise the document to this” has to read the message too.

TCLPDF FONT METRICS what
A Type 1 font program was handed over without the metrics that go with it: the AFM is neither beside the program under the same base name nor named with font embed -metrics, or it is not readable. The .pfb carries the outlines and the .afm carries the widths, and a PDF needs both. what is the path that was tried, or afm where there is no file to look beside — a program passed with -data. This is the one class a handler answers by supplying a file. Two cases that used to wear it do not any more: -metrics given for a face that carries its own metrics is TCLPDF FONT ARGUMENT metrics, which a handler answers by dropping the option; and a file whose own metrics are unusable — head.unitsPerEm 0, a CFF FontMatrix whose first element is not positive — is TCLPDF FONT DAMAGED head or TCLPDF FONT DAMAGED cff, which nothing the caller passes in can mend. A handler that followed the old wording answered the second of them by passing the very option it complains about.

TCLPDF TEXT FIT what
text -fit was given something it cannot fit into a box. what is WIDTH for the combination with -width, which asks for a paragraph and a fitted line at once; SHRINK for a -shrinkLimit outside 1 to 100; and BOX for a box too small for anything this package can write into it — under one point of size, where the text would be written, be valid and be invisible. A box that is not two finite lengths above zero is refused by the check every fitted placement shares, text, image place and form place alike, and that one reads TCLPDF FIT BOX. A length beyond what a PDF real can carry never reaches it: the shared length check has it first, as TCLPDF OPTION NUMBER.

TCLPDF TEXT VERTICAL what
A vertical line was asked for something that only reads along the horizontal axis. what says what was asked for and cannot be had: anchor for -anchor top, which places the top of the letters at y where a vertical line already begins at y; fit for -fit, which measures a width and narrows the letters; block for text -width, which places its lines down the page and not across it — textLines -direction ttb breaks such a text and one text call per column places it; and path for textPath, which turns every cluster by the direction of the path.

TCLPDF FIT ROTATE angle command
-fit and -rotate were given together. The box is upright and the picture is not, so the fitted extent and the drawn extent are different rectangles — measured, the rotated picture lands beside its box. command names the call, since the same refusal serves image place, form place and svg.

TCLPDF SHADING PATTERN
A shading was asked to paint in a pattern colour space, which ISO 32000-2, 8.7.4.5.1 forbids it — a reader given one draws nothing at all. A shading is what a shading pattern is made of: build the gradient in device colours and register it with shading pattern.

TCLPDF LEADER FILL fill
The fill character of a leader does not get longer the more copies it has — a -spacing more negative than the character is wide. The row would never reach the width asked for, and the search for the right number of copies would not end.

TCLPDF FONT EMBED alias
font embed was called wrongly — with a path and -data, or with neither. Distinct from TCLPDF FONT SOURCE on purpose: that one says a reader turned the bytes away and invites a script to try the next reader, and a script that retried on this one would retry for ever, having handed over no font at all.

TCLPDF FONT VERTICAL alias
A face was asked to set a vertical line and cannot: only a TrueType or OpenType face carries the vertical metrics and the vert feature that vertical writing is made of. Raised by the font module before any resource is registered, so that a refused vertical run leaves the document exactly as it was.

TCLPDF XMP CHAR field codepoint
A value bound for the XMP packet carries a character XML does not allow — a C0 control such as U+0000, U+0007 or U+000B (XML 1.0, 2.2). It is refused rather than repaired: dropping it would deliver a different title than the one given, and a replacement character would claim something was there. field names where it came from, so that the call can be found.

TCLPDF XMP SURROGATE field codepoint
A value bound for the XMP packet carries an unpaired surrogate, which is not a character at all and has no UTF-8 form. Under Tcl 8.6 it used to reach the file as three bytes of CESU-8; under Tcl 9 it raised a bare Tcl error out of the encoder. Both roads now answer the same way.

TCLPDF STRUCTURE TYPE vocabulary
The word that should have named a structure type is not one that can be used here. vocabulary names what it was measured against rather than the word itself: structure for the standard types of ISO 32000-1, 14.8.4 and Annex M, artifact and subtype for the artifact tables 330 and 331, tag for what -tag takes on a drawing call — a multi-word tag without Artifact, a grouping type that holds no text, an empty tag with no element open. A typo in a type name lands here, and the message names the full list.

TCLPDF STRUCTURE PLACE rule
A type the standard knows, in a place it does not allow it — checked where the element is opened or closed, so that the message names the line rather than an object number later on. rule names the rule: parent (this type belongs under other parents), child (this parent takes other children), inline (markup with no surrounding text — a Span in a LI, say), block (a block inside inline markup: a Table inside a Link), nested (a type that may not hold its own kind, an Art inside an Art), root (a Document below the tree’s root), label (a Lbl after content), leaf (a leaf element holds text and markup, not a block), duplicate (a Table holds at most one THead and one TFoot), caption (the Caption is not where it belongs inside its parent, ISO 32000-2, Table 372), sequence (Ruby and Warichu hold a fixed sequence, Table 369).

TCLPDF STRUCTURE ATTRIBUTE option
An attribute of structure is wrong — wrong value, wrong form, or on a type that does not take it. option is the option without its dash: lang, scope, numbering, bbox, colSpan, rowSpan, expansion, ref. Passing an option is the caller’s doing, so the option and not the kind of mistake is what the code carries.

TCLPDF STRUCTURE VERSION what
What was asked for exists only in a newer PDF version than the document’s, and raising the version is the whole remedy — which is why this stands beside TYPE and ATTRIBUTE rather than inside them. what names what needs the version: type (a structure type from ISO 32000-2, Annex M), attribute (an attribute value from Table 369), destination (a structure destination, 12.3.2.3), ref (-ref, which ISO 32000-2, 14.7.4.3 adds). ua -part 2 raises the version with it.

TCLPDF STRUCTURE NAME trouble namespace
An identifier. trouble is duplicate or unknown, and namespace says which one: name is this package’s own handle, under which a link or a bookmark finds an element, id is the /ID of ISO 32000-1, 14.7.2. unknown name is the one refusal here that comes at the write: a link or a bookmark points at a name no element ever carried.

TCLPDF STRUCTURE STATE call
Not the arguments refusing but the state of the document, and the remedy is always to do something earlier. call names the call concerned: tagged (tagged 1 comes too late, content is already on a page), expansion (-expansion is read out of the structure tree and needs a tagged document), destination (a structure destination needs the same tree), element (elements were closed out of order), suspend (an element opened or resumed inside a form, a pattern or a page-number XObject), resume (structureResume on an element that is already open).

TCLPDF STRUCTURE ARGUMENT argument
The call itself is malformed, and no state of the document would make it right. argument names the guilty one: script for a missing -script on structure, tagged for tagged with more than one word or without a truth value.

TCLPDF AFM what name
One of the fourteen standard fonts was asked for by a name it does not have. what is FAMILY for a family nobody carries, STYLE for a family that has no such cut, FONT for a base name outside the fourteen; name is what was asked for. The message lists what there is — these are a closed set, so the list is short and complete.

STYLE is worth a sentence of its own: five of the six families carry four cuts each, while symbol and zapfdingbats carry one, and a style asked of them is refused rather than dropped — there is no bold Symbol to fall back to. Until round 7 this class was unreachable and font -family symbol -style bold was silently set in Symbol. A style that names no cut at all — -style regular — is the empty style and passes.

TCLPDF COLORFONT what
A colorFont call is incomplete. what is ALIAS for a missing name, SOURCE for neither a path nor -data, CHARS for the missing -chars — a Type 3 font holds the glyphs it is told to hold, and there is no way to derive that list from a colour font.

TCLPDF COLORFONT ANCHOR codepoint alias
The face binds the first glyph of -chars to a glyph in front of it, and there is none — thrown by colorFont, before the font is defined. A GPOS mark lookup says where a glyph sits on the one it hangs on, and a run that begins with such a glyph has nothing to hang it on: there is no base to fold it into and no advance to give it. codepoint is the character in U+XXXX notation. No face measured here does it.

TCLPDF COLORFONT AMBIGUOUS codepoint alias
-chars reaches the same text twice and the face draws it with two different glyphs — thrown by colorFont, before the font is defined. A Type 3 font holds one glyph per character code, so one of the two drawings would be lost and which one would depend on the order the characters were written in. The message names both glyph numbers. Name the two in separate colorFont calls and put both in font -fallback.

TCLPDF FONT AXES what
-axes is an axis tag and a value for each axis to set, {wght 620 wdth 90}, and either the list does not read that way or the face has no axes at all — thrown by font embed, at the call, before any glyph is taken. what is the axis tag at fault where one can be named, the literal odd where the list has an odd number of elements and it is precisely which word lacks a value that is in doubt, and the source itself — the path or the length of -data — where the face is a Type 1 program or a bare CFF, neither of which has axes to set.

TCLPDF TEXT INDENT width
-indent, -indentRight and -firstIndent together leave the column no width to set a line in — refused at the call, the way -columns with too wide a -gutter is, because a line with no band left is set one character at a time and runs off the page.

TCLPDF SHADING AXIS length
The axis of an axial gradient has no length: -from and -to fall together as the file writes them (five decimals, 7.3.3). Coincident end points paint nothing at all (ISO 32000-1, Table 79), the file is valid, and what a reader makes of it is its own invention.

TCLPDF PAGE CANVAS what
A call that belongs to a page was made inside a content stream that is not one — the script of form create, pattern create, layer draw or the page-number XObject. what is add for page add, whose new page would stay empty while the drawing went on into the form, and pop for an internal canvas closed without a matching open.

TCLPDF GEOMETRY MATRIX owner
A transformation matrix is singular as the file writes it — it collapses the plane onto a line or a point, and nothing drawn through it can be seen. owner says whose matrix it is: transform, a pattern’s or a shading’s -matrix, or {form place} for the matrix -at, -scale and -rotate compose to.

TCLPDF GRAPHICS OVERPRINT pdfa
An ICC based CMYK colour was filled or stroked while overprinting for that side was on and overprint -mode was 1, in a document claiming PDF/A. ISO 19005-2 and -3, 6.2.4.2 forbid the combination outright; the refusal comes at the write, with the other conformance checks, and names the call and the page. The same colour written as {cmyk …}, the same overprint at mode 0, and the mode taken back by a restore are all conformant.

TCLPDF IMAGE PNG what
A PNG this package will not read. Beside signature, compression, filter, interlaced, colorType, stencil, iCCP, depth, colorkey and DAMAGED with the chunk after it (IHDR, IDAT, PLTE, tRNS, pHYs, iCCP), what is indices for palette indices at a bit depth a palette image is not written at (PNG 11.2.2). A colour type and bit depth PNG 11.2.2 does not pair is DAMAGED IHDR, because the depth of the IHDR goes straight into the image dictionary and into the predictor’s /DecodeParms.

TCLPDF SVG REFERENCE cycle / TCLPDF SVG LIMIT use
A use element that reaches itself, directly or round a corner — SVG 1.1, 5.6 calls it an error and gives the drawing no rendering, and the reference graph is walked before the first operator is written, so nothing has been drawn when the refusal comes. LIMIT use is the other end of the same road: the drawing’s use elements unfold to more than 20 000 copies, nesting them multiplies, and the limit is this package’s own, for availability rather than for security.

TCLPDF XML ENTITY what
The entities of an XML document — an SVG file, an invoice — expand it past a hundredfold (expansion), or one of them is defined in terms of itself (cycle). Both are refused before the document is parsed. This is availability again: the built-in parser would otherwise try to expand what it was handed.

TCLPDF ANNOT PLACE form / TCLPDF LINK PLACE what
An annotation belongs to a page. Its /Rect is in that page’s default user space (ISO 32000-2, 12.5.2), which the script of form create, pattern create or the page-number XObject does not have, so annot, link and the calls behind them are refused there — PLACE form in both topics. TCLPDF LINK PLACE target is the other case: two link annotations pointing at different places were put into one Link structure element, which ISO 14289-2, 8.2.5.20 allows only where they all go to the same place.

TCLPDF ANNOT VERSION key
Internal: an annotation key was written for which no version floor is recorded. The table in annot.tcl is what a RequireVersion is read out of, and a key missing from it would be written into a file too old for it with nothing reporting the fact.

TCLPDF ANNOT RECT EMPTY size
A geometry annotation, a note or a stamp with no area — a -size whose width or height is zero or negative, or one that is positive and comes out as the empty rectangle once written with five decimals. No reader shows such an annotation and no validator objects to it.

TCLPDF TYPE3 ALIAS alias
A Type 3 font is being defined under a name the document already carries. colorFont throws it first, before the face is opened and before anything is embedded: it used to reach the caller from font define at the end of the build, by which time every picture of the font had been embedded and written, so a refused call left image objects and names in the document that nothing referenced and qpdf said nothing about them.

TCLPDF FILTER what
What the stream decoders refuse. ASCII85 and ASCIIHEX for a character or a group the encodings do not define — 7.4.2 and 7.4.3 both say such data “shall cause an error” — CCITT, LZW and PREDICTOR for a stream that does not decode, FLATE for a deflate stream that ends inside its own data, and ROOM for one that unpacks past the 64 MiB a foreign stream is followed to. The reading commands rewrite ROOM and FLATE into their own topic, so an import answers TCLPDF IMPORT ROOM.

STANDARDS

These are the documents the package is written against, and — where a validator exists for one — the documents every release is measured against. A profile a document claims is checked on every run of make check: veraPDF for PDF/A and PDF/UA, Mustangproject for the electronic invoices, qpdf over every document, pdfsig over every signed one.

The file itself
ISO 32000-1:2008 (PDF 1.7) is what a document declares by default, and ISO 32000-2:2020 (PDF 2.0) what tclpdf new -version 2.0 declares. The version is a property of the document rather than a constant of the writer: encryption in the revision PDF 2.0 prescribes and a CAdES signature need 2.0, PDF/A-3 and therefore ZUGFeRD need 1.7.

Archiving and accessibility
ISO 19005-2 and ISO 19005-3 for PDF/A parts 2 and 3, conformance levels B, U and A. ISO 14289-1 (PDF/UA-1) and ISO 14289-2 (PDF/UA-2), with ISO/TS 32005 for the structure namespaces PDF/UA-2 requires, and the WTPDF 1.0 profile of the PDF Association for well-tagged documents. The XMP packet follows ISO 16684-1 (XMP part 1).

Electronic invoices
EN 16931 as the semantic model, in the syntax of ZUGFeRD 2.3 / Factur-X 1.09.2 and Order-X 1.0, carried in a PDF/A-3 document as ISO 19005-3 and the ZUGFeRD technical specification require. The attachment relationship follows Table 43 of ISO 32000-2.

Signatures
ETSI EN 319 142-1 (PAdES baseline profiles) and ETSI EN 319 122-1 (CAdES), over RFC 5652 (CMS) and RFC 3161 (timestamp tokens). The signature dictionary itself is ISO 32000-2, 12.8.

Fonts
ISO/IEC 14496-22:2019 (Open Font Format, the ISO edition of OpenType) for the sfnt tables, the character map, GSUB and GPOS, the variable-font tables and COLR/CPAL. Beside it Adobe’s own specifications for what that standard does not cover: the Type 1 Font Format, Technical Note #5176 (the Compact Font Format), Technical Note #5902 (PostScript name generation), the Adobe Font Metrics of the fourteen standard faces, and the Adobe Glyph List. Right-to-left text follows UAX #9, hyphenation Liang’s algorithm in the pattern format of libhyphen.

Pictures
ISO/IEC 10918-1 (JPEG) with the JFIF and Exif 2.32 application segments, the PNG specification (ISO/IEC 15948) including its predictors, and TIFF 6.0 with Technote 2 for JPEG-compressed strips. Colour follows the ICC specification for embedded profiles, and CIE L*a*b* as ISO 32000-2, 8.6.5.4 states it.

Language and country codes follow RFC 3066 where a document, a block or a hyphenation pattern set names a language.

SEE ALSO

tdom, qpdf(1), veraPDF, pdffonts(1), pdftotext(1), tzint

tdom builds the XMP metadata packet — every document that declares PDF/A, PDF/UA or ZUGFeRD needs it — and, where present, parses SVG in place of the built-in parser; see “Optional packages” at the top. Its home is https://tdom.org.

tzint is a Tcl binding to the Zint barcode library, at https://fossil.sowaswie.de/tzint. It produces SVG, which svg -data draws — so barcodes need no code in this package and are not a dependency of it, but drawing one does need tzint installed. See the Barcodes section above.

Every topic and its classes

Read as TCLPDF <topic> <class> <facts…>. A topic without classes puts the facts straight after its name. This list is generated from the source rather than kept by hand, by tools/mkerrorcodes.tcl, and make check fails if it and the sources have drifted apart. Two further guards stand behind it: tests/errorcode.test fails if a refusal appears without a code, and if a code appears whose first word is neither TCLPDF nor Tcl’s own.

topic classes
AFM FAMILY FONT STYLE
ANNOT APPEARANCE ATTACHMENT BOX COLOUR COLOURSPACE DATE KIND LINE MODULE NOTE OPACITY PDFA PLACE POINTS QUADS RECT STAMP STRUCTURE UA VERSION WIDTH
ATTACH ARGUMENT NAME STATE
COLORFONT ALIAS AMBIGUOUS ANCHOR CHAR CHARS COMPOSITE EMPTY FACE FOREGROUND IMAGE LIMIT PAINT PALETTE SOURCE SPLIT STRIKE TABLES UNBOUNDED
COLOUR COLOURANT COMPONENTS DEVICEN HEX ICC LAB PROFILE SEPARATION SPACE SUBCOMMAND TARGET UNKNOWN
COLR COMPOSITE CYCLE DEPTH EMPTY ENTRY LAYERS MISSING PAINT PALETTE RECORDS REUSE TRUNCATED UNSUPPORTED VERSION
CRYPTO BLOCKLENGTH COUNT IVLENGTH KEYLENGTH RANDOM
DOCUMENT ARGUMENT CATALOG DATE INFO LANGUAGE METADATA MODULE PAGE RESOURCE STATE TRAILER
ENCRYPT ARGUMENT KEY ORDER PASSWORD PERMISSION SEAM STATE SUBCOMMAND
ENVIRONMENT the facts follow the topic directly
EVENT CALLBACK NAME
FIELD ALIGN BOOLEAN BORDERWIDTH BUILD BUTTON COLOUR COLOURSPACE COMBINATION DECLARE FLAG FONT GLYPH INDEX LENGTH LINES MAXLEN NAME OPTIONS PAGE PASSWORD RECT REGISTER SELECT SIGFLAGS SIZE STRUCTURE SUBCOMMAND TAKEN TOP TYPE UNKNOWN
FILTER ASCII85 ASCIIHEX CCITT FLATE LZW PREDICTOR ROOM
FIT ALIGN BOX MODE ROTATE SIZE
FONT ALIAS ARGUMENT AXES BIDI CIDKEYED DAMAGED EMBED ENCODING FACE FALLBACK FAMILY GLYPH INSTANCE KIND METRICS OUTLINES SHAPING SOURCE STREAMED SUBSET TABLE UNSUPPORTED VERTICAL
FORM ARGUMENT NAME SUBCOMMAND
FUNCTION BOUNDS
GEOMETRY ARGUMENT FORMAT MATRIX MEASURE ORIENTATION SIZE UNIT
GLYF OUTLINE
GRAPHICS ARGUMENT BLEND OPACITY OVERPRINT POINT RESTORE SAVE
HYPHENATE ARGUMENT LANGUAGE PATTERNS SUBCOMMAND UNSET
IMAGE ALIAS ARGUMENT FORMAT ICC INLINE INTERPOLATE INVERT JPEG MASK PNG SIZE SOFTMASK STENCIL SUBCOMMAND
IMPORT ARGUMENT BOX DEPTH ENCRYPTED FILE FILTER FOREIGN NAME OBJECT OBJSTM OPTION PAGES PREDICTOR RECURSION ROOM ROOT ROTATE SERIALIZE STREAM SUBCOMMAND SYNTAX XFA XREF
INITIALVIEW PAGE PAGELAYOUT PAGEMODE ZOOM
IO DIRECTORY MISSING
LAYER ARGUMENT CONFIGURE NAME RADIO SCRIPT STATE SUBCOMMAND
LAYOUT RANGE
LEADER ARGUMENT FILL
LINK ARGUMENT PLACE
MORX DAMAGED LOOKUP VERSION
OPTION DICT KEY NUMBER PAIRS POINT REQUIRED UNKNOWN ZOOM
OUTLINE ARGUMENT PAGE PARENT
OUTPUT CHANNEL EMPTY
PAGE ARGUMENT BOX CANVAS INDEX ROTATE SUBCOMMAND TYPEAREA
PAGELABEL ARGUMENT INDEX
PAGENUMBER ARGUMENT
PATTERN ARGUMENT NAME SPACE STEP SUBCOMMAND
PDFA ATTACHMENT COLOUR CONFORMANCE FONT PART PROFILE STATE SUBCOMMAND
PDFOBJ NAME NUMBER STREAM
SBIX CYCLE DAMAGED FORMAT STRIKE VERSION
SHADING ARGUMENT AXIS COLOURS DOMAIN EXPRESSION FLAG KIND NAME PATCH PATCHES PATTERN PERROW RECT SIZE SPACE STOPS TRIANGLE VERTEX VERTICES
SHAPE ANGLE ARGUMENT POINTS SEGMENTS SIZE
SIGN ARGUMENT CHANGED FIELD FOREIGN INTERNAL MANY NONE ROOM SIGNER SIGNINGTIME SPACE STATE SUBCOMMAND
STRUCTURE ARGUMENT ATTRIBUTE NAME PLACE STATE TYPE VERSION
SVG ARGUMENT COLOUR LIMIT REFERENCE ROOT XML
TABLE ARGUMENT CELL COLUMN DIRECTION ROOM SPAN STYLE SUBCOMMAND THEME WIDTH
TEXT ALIGN ANCHOR ARGUMENT AVOID BALANCE BLOCK BREAKHYPHEN COLUMNS DIRECTION FIT HYPHENS INDENT LINEFEED MARKDOWN MARKUP PAGINATE PATH RENDER RUNS VERTICAL
TIFF ALPHA BIGTIFF BYTEORDER CCITT CIRCULAR COMPRESSION DAMAGED DEPTH DIRECTORY FILLORDER ORIENTATION PALETTE PHOTOMETRIC PLANAR PREDICTOR RANGE SAMPLEFORMAT SAMPLES SIGNATURE STACKED STENCIL STRIPS TILED
TIMESTAMP HASH HTTP IMPRINT NONCE SIZE STATUS TLS TOKEN
TYPE3 ALIAS ARGUMENT BBOX EMPTY FULL GLYPH MATRIX RECURSION
UA ARGUMENT CLAIM PART STATE SUBCOMMAND
UPDATE EMPTY ENCRYPTED FILE FOREIGN OBJECT SUBCOMMAND TRAILER
VERSION the facts follow the topic directly
VIEWERPREFERENCES ARGUMENT
WRITER ARGUMENT OBJECT RESERVED VERSION
XML EMPTY ENTITY MALFORMED
XMP CALLER CHAR PREFIX RAW RDF SURROGATE TAG URI
ZUGFERD ARGUMENT NAME PROFILE RELATIONSHIP STATE TYPE XML

The reading commands share one reader and therefore one error class — pdf import, pdf info, pdf pages, pdf fonts, pdf metadata and pdf fields, all under TCLPDF IMPORT. A path that names a directory, or a file this user cannot read, is refused by all six of them under TCLPDF IMPORT FILE, in this package’s words and before anything is opened: a directory exists, and opening one succeeds, so the raw “illegal operation on a directory” used to arrive from the read afterwards under POSIX EISDIR, and an unreadable file as EACCES. ::tclpdf::update open is not among them: it stands on the same reader and refuses the same files, but a handler written around an update is not listening for the import, so its refusals carry TCLPDF UPDATE — its own ENCRYPTED, FOREIGN, OBJECT, TRAILER, EMPTY and SUBCOMMAND, and FILE for a path that is not there, is a directory, or cannot be read. That holds for the whole session and not only for the command that opens it: body and replace read an object the first time they are asked for it, so a file damaged below its cross-reference refuses long after open answered, and it refuses under TCLPDF UPDATE as well. Where the refusal is the reader’s own — a file with no startxref, an object at an offset where another object is written, a /Length naming an object the file has not got — only the topic word is rewritten and the class word is kept exactly as the reader wrote it, because the class says what the caller has to do about it and that does not change with the command that ran into it: TCLPDF IMPORT OBJECT reaches an update as TCLPDF UPDATE OBJECT. Until 2026-08-27 those refusals arrived under TCLPDF IMPORT after all, and a missing path arrived as Tcl’s own POSIX ENOENT. A run over a directory of foreign files therefore traps TCLPDF IMPORT where it reads them and TCLPDF UPDATE where it continues them. One refusal of an update session stands outside both, and deliberately: a number reserve handed out and nothing ever filled is the writer’s question rather than the reader’s — the two object stores of this package differ in where their numbers come from and in nothing else — so write answers it as TCLPDF WRITER RESERVED unwritten, naming the numbers, exactly as a document being written answers it.

An unknown subcommand of one of the package’s own ensembles answers in this package’s words and under this package’s codes, with the subcommands that exist listed: TCLPDF IMPORT SUBCOMMAND for ::tclpdf::pdf, TCLPDF UPDATE SUBCOMMAND for ::tclpdf::update, TCLPDF SIGN SUBCOMMAND for ::tclpdf::sign and TCLPDF HYPHENATE SUBCOMMAND for ::tclpdf::hyphenate. One refusal deliberately carries no TCLPDF code: a mistyped name under tclpdf itself answers in Tcl’s own wording, so that a mistyped command behaves exactly as a mistyped command does everywhere else — ::tclpdf::bogus as TCL LOOKUP COMMAND, and the ensemble form tclpdf bogus as TCL LOOKUP SUBCOMMAND, which lists every exported name the namespace holds at that moment, the topical modules a document has loaded included.

KEYWORDS

pdf, pdf/a, pdf/ua, zugferd, factur-x, truetype, font embedding, invoice, accessibility, barcode

COPYRIGHT

Copyright (C) 2026 Alexander Schoepe, Bochum, DE

Distributed under the MIT License; see the file license.terms.