Technical Article

PDF Type 2/3/4 Functions in Delphi: Exponential, PostScript

HotPDF, the native VCL PDF component for Delphi and C++Builder, evaluates the three PDF function types built from formulas rather than sample grids: Type 2 exponential interpolation, Type 3 stitching, and Type 4 PostScript calculator functions, corresponding to ISO 32000-1 §7.10.3, §7.10.4, and §7.10.5. Type 2 blends between two output vectors along a curve, Type 3 chains several sub-functions across one input domain, and Type 4 runs a restricted PostScript program that can branch, compare, and compute almost anything a content stream needs from its inputs. Get any of the three slightly wrong and the failure never announces itself as a bug — it shows up as a gradient with a dead flat band, a spot color that renders pure black, or a calculator function that is off by exactly one at the inputs a test suite happened not to try

These three sit next to a fourth, Type 0, which stores a sampled grid instead of a formula and is covered separately in the companion article on Type 0 color lookup tables. The two families solve the same problem, mapping an input to an output, but Type 0 is data computed once and baked into the file, where Type 2, 3, and 4 are code the reader evaluates on every call. All four share one dispatch point in HotPDF's renderer, keyed off the function dictionary's /FunctionType entry, so a shading, a tint transform, or a halftone spot function never has to know which of the four it received before it can ask for a color

How does a PDF Type 2 exponential function work?

A PDF Type 2 function computes one formula — y = C0 + x^N × (C1 − C0), applied component by component — where x is the function's single input, normalized against its /Domain before the formula runs (ISO 32000-1 §7.10.3). /C0 and /C1 are the output vectors at the two ends of that range, one number per output component, and /N is the exponent that shapes the curve between them: N = 1 gives the straight linear ramp behind most gradient stops and duotone conversions, N above 1 pulls the curve toward C0, and N between 0 and 1 pushes it toward C1. RegisterExponentialFunction builds that dictionary from five arguments and hands back a function object ready to plug into a shading, a halftone spot function, or anywhere else the spec accepts a /Function key

The component-count relationship between C0 and C1 matters twice: once when you author a Type 2 function, and again whenever HotPDF has to render one it did not create. On the authoring side, RegisterExponentialFunction checks C0 and C1 against each other and raises if they disagree, so a call that reaches BeginDoc already has a self-consistent function object. On the rendering side, though, the evaluator has to trust whatever /C0 and /C1 arrays a source file actually declares — a print-shop file opened for preview, say, or a signed document displayed back to a user — and versions before 2.376.0 read those arrays into a buffer sized for four components, the CMYK case. A DeviceGray or DeviceRGB exponential tint, with a one- or three-element /C0 and /C1, failed that read silently and left both arrays at zero, so the tint painted flat black instead of its intended color. Version 2.376.0 resized the reader to the function's actual declared output count instead of a fixed buffer — exactly the kind of bug that only a non-CMYK test case exposes, since the existing suite ran CMYK throughout, where four-into-four always fit

var
  EaseIn: THPDFDictionaryObject;
begin
  // Type 2: one input, N > 1 biases the ramp toward C0 (an ease-in curve)
  EaseIn := Pdf.RegisterExponentialFunction(
    [0,1],           // Domain: single input, clamped to [0,1]
    [0, 0, 0],       // C0: output at x = 0
    [0.8, 0, 0],     // C1: output at x = 1
    3,               // N: exponent, 1 = linear, > 1 eases toward C0
    []);             // Range omitted: defaults to a [0,1] clamp per output
end;

Type 3 stitching: chaining sub-functions across a Bounds array

A PDF Type 3 function stitches k sub-functions into one piecewise mapping over a single input's /Domain, and the two arrays that make that work are /Bounds and /Encode (ISO 32000-1 §7.10.4). /Bounds holds k − 1 interior split points that carve /Domain into k consecutive intervals; the evaluator picks the first interval whose upper bound exceeds the input, or the last interval once the input reaches the final bound, and hands off to that interval's sub-function. /Encode then remaps the input from its position inside that interval onto whatever input range the chosen sub-function itself expects — typically [0, 1] if the sub-function is one more exponential segment — before evaluation continues, one call deeper, into that sub-function's own /Domain and /Range

HotPDF's stitching evaluator used to handle only exactly two sub-functions, and its /Bounds reader required a full eight-element array, so the single split point a two-segment gradient actually needs — one number in /Bounds — always failed to parse and the function returned nothing. /Encode was not applied at all. Version 2.376.0 rewrote the selection as the general k-sub-function search the spec describes and started reading /Bounds against its real declared length, so a three-, four-, or five-stop gradient stitched from that many exponential segments now resolves the same way a two-segment one always claimed to. The example below builds a two-segment black-through-red-to-white ramp, the shape an axial or radial shading reaches for whenever one exponential curve cannot carry every color stop a design calls for

var
  ToRed, ToWhite, Ramp: THPDFDictionaryObject;
begin
  // Two linear segments: black->red over [0, 0.5], red->white over [0.5, 1]
  ToRed   := Pdf.RegisterExponentialFunction([0,1], [0, 0, 0],   [0.8, 0, 0], 1, []);
  ToWhite := Pdf.RegisterExponentialFunction([0,1], [0.8, 0, 0], [1, 1, 1],   1, []);

  Ramp := Pdf.RegisterStitchingFunction(
    [0,1],                  // Domain: the stitched function's own input range
    [ToRed, ToWhite],       // Functions: k = 2 sub-functions
    [0.5],                  // Bounds: k - 1 = 1 split point
    [0,1, 0,1],             // Encode: 2 numbers per sub-function
    []);                    // Range omitted: inherited from each sub-function
end;

What can a Type 4 PostScript calculator function do that Type 2 and 3 cannot?

A PDF Type 4 function runs a genuine, if deliberately restricted, program: a PostScript calculator that pushes its inputs onto an operand stack, executes arithmetic, comparison, stack-manipulation, and boolean operators plus if/ifelse conditionals, and leaves its outputs on the stack when it finishes (ISO 32000-1 §7.10.5, Table 42). There is no loop construct and no named-variable storage, only the stack, which keeps a conforming program easy to reason about — but within that restricted operator set, Type 4 can express things Type 2 and Type 3 cannot, such as a real multi-ink mixing formula for a DeviceN separation or a halftone spot function with a conditional threshold. HotPDF's evaluator, HPDFEvalPostScriptCalculator, tokenizes the program once — numbers, operators, and { } procedure blocks — then walks a 100-entry operand stack, the depth ISO 32000-1 §7.10.5 calls for, behind a hard ceiling of 50,000 evaluated operators as a defensive backstop against pathological or hand-written programs

The roll operator: direction is easy to get backwards

roll is the operator most likely to come out backwards on a first attempt, because its argument order and its rotation direction both run opposite to how English describes them. n j roll pops a count n and a rotation amount j, then cyclically shifts the top n stack entries by j positions, wrapping items that fall off one end back onto the other; the canonical example, straight from the spec, is a b c 3 1 roll producing c a b — the top item moves to the bottom of the group, not the other way around, and every other item shifts up by one to make room. HotPDF's evaluator computes the new position of stack entry i as (i + j) mod n, which matches that example exactly, but it is a two-line loop that is just as easy to write with the rotation flipped, and a mirrored roll still produces a plausible-looking color — it is just not the color the file author asked for

const
  Prog = '{ 3 1 roll }';   // (a b c) -> (c a b): the third input moves to the front
var
  Reorder: THPDFStreamObject;
begin
  // Type 4: 3 inputs, 3 outputs, no extra clamping beyond Domain/Range
  Reorder := Pdf.RegisterPostScriptFunction(
    [0,1, 0,1, 0,1],   // Domain: 2 numbers per input
    [0,1, 0,1, 0,1],   // Range: 2 numbers per output (required for Type 4)
    Prog);
end;

round is not Delphi's Round: half-up versus banker's rounding

PostScript's round operator resolves a .5 tie toward the larger integer every time, and Delphi's built-in Round function does not: it rounds half-to-even, the banker's-rounding convention that alternates which way a .5 tie falls so repeated rounding does not accumulate bias. The two agree almost everywhere and disagree exactly on the boundary that matters here — Delphi's Round(0.5) returns 0 and Round(2.5) returns 2, while the PDF spec's round wants 1 and 3 for those same inputs — so the mismatch hides through casual testing and then reproduces as a consistent off-by-one wherever a calculator program's intermediate math lands exactly on a half-integer. ISO 32000-1 §7.10.5 Table 42 is explicit that round pushes a fractional .5 toward the greater integer, so HotPDF implements the operator as Floor(x + 0.5) instead of calling Delphi's Round, and any code that reimplements or spot-checks a Type 4 program's arithmetic by hand needs the same substitution

function PostScriptRound(const X: Double): Double;
begin
  // ISO 32000-1 7.10.5 Table 42: round pushes .5 toward the greater
  // integer. Delphi's Round() is banker's rounding and disagrees here:
  // Round(0.5) = 0, Round(2.5) = 2 - both one short of the spec value.
  Result := Floor(X + 0.5);
end;

Registration-time validation catches a bad calculator program early

A malformed Type 4 program is cheap to catch at authoring time and expensive to catch anywhere else, so RegisterPostScriptFunction does not just store the source text: it trial-evaluates the program once, at the midpoint of the declared /Domain, before the function object is ever written into the document. Unbalanced { } blocks, an unrecognized operator, a stack underflow, or an output count that does not match /Range all fail that trial run and raise an exception immediately, with the call stack pointing at the RegisterPostScriptFunction call instead of at a rendering artifact discovered during QA on a file that already shipped. The midpoint trial does not prove the program is correct across its whole /Domain — a conditional branch that only misbehaves near one edge of the input range can still slip past a single sample point — but it closes off the entire class of programs that are structurally broken rather than merely wrong in one corner

Where gradients and spot colors put these functions to work

Type 2, 3, and 4 rarely appear in isolation in a real PDF; they show up wherever the spec accepts a /Function key, and the two most common consumers are shadings and spot-color tint transforms. An axial or radial gradient's sh operator (ISO 32000-1 §8.7.4.5) evaluates its /Function once per position along the gradient axis, which is exactly the multi-stop case Type 3 stitching exists for. A Separation or DeviceN color space's tint transform is the other frequent home for these three types, and it is where Type 4 earns its keep: a single spot ink usually reduces to a Type 2 or Type 0 curve, but a DeviceN blend of several inks with real trapping and overprint behavior often needs the conditional logic only a PostScript calculator can express, the case covered in the article on rendering Separation and DeviceN spot colors. RegisterSeparationFunc is the pairing call on the authoring side: it takes a colorant name, an alternate color space, and any object the Register*Function family returns, and wires that tint transform into a Separation color-space resource the rest of the page can select with scn/SCN

Together, Type 0's sampled grids and these three formula-driven types cover every /Function a PDF can declare, and choosing the right one is mostly a question of what you already have: a lookup table computed elsewhere becomes Type 0, a two-endpoint blend becomes Type 2, several blends chained across a domain become Type 3, and anything with real conditional logic becomes Type 4. RegisterExponentialFunction, RegisterStitchingFunction, and RegisterPostScriptFunction are part of the standard HotPDF Component for Delphi and C++Builder, alongside the rest of its ISO 32000-1 function and shading API