Blog › ICP guides

PostScript developer on retainer: page description programming, operand stack debugging, PDF pipeline maintenance, and PostScript language on monthly retainer

October 1, 2026 · ~15 min read

A PostScript developer was building a legal document generation system that produced multi-page PDF forms. A procedure /draw-cell drew a table cell: it took four values from the operand stack (x y width height), drew a rectangle, and was supposed to leave the stack in the same state as before the call. Inside /draw-cell, there was a conditional block that checked whether a cell had a highlighted background: isHighlighted { ... } if. Before the conditional, the procedure pushed a temporary grayscale value to the operand stack. In the true branch, that grayscale value was consumed by setgray and thus removed from the stack. In the false branch — no highlight needed — the code path reached gsave without the pop needed to discard that same temporary value. The push was at procedure scope, before the conditional; the pop to clean it up lived only inside the true branch. After each false-branch cell was drawn, one extra integer remained on the operand stack.

Over a page with 12 cells (8 of which took the false branch), 8 extra values accumulated. When PostScript processed the 9th false-branch cell, those accumulated stale integers sat under the procedure’s arguments. An exch inside /draw-cell that was supposed to swap width and height values swapped height with one of the stale accumulated integers instead, producing a cell with the wrong height. Eight wrong-height cells rendered before the pattern was identified.

The fix required a systematic approach. The developer added pstack — PostScript’s stack-printing operator — at the start of /draw-cell to print the entire operand stack to standard output on each call. The pattern was clear: the stack grew by exactly 1 on each false-branch call. The developer traced the unmatched push before the { ... } if block, found that the true branch consumed it via setgray while the false branch had no corresponding pop, and added pop at the end of the false branch. Wrong-height cells: 8 → 0. The work log said “fixed stack balance in /draw-cell conditional, 8h.” What is invisible to the client is what made 8 hours proportionate: PostScript does not report a stack balance error; the accumulation is silent and incremental; the wrong rendering appeared only after the 8th false-branch cell of each page; diagnosing it required understanding the PostScript operand stack as a LIFO structure shared across all procedure calls, tracing the execution path through the conditional, and connecting the stale integer at stack position 4 to the push-before-conditional that was never cleaned up in the false branch.

PostScript language overview: page description programming from Adobe to GhostScript

PostScript was designed by John Warnock and Charles Geschke at Adobe Systems between 1982 and 1984, and first shipped in the Apple LaserWriter in 1985. It is a full Turing-complete programming language designed specifically for describing the appearance of pages — text, paths, images — in a device-independent way. A PostScript program is executed by a PostScript interpreter built into a laser printer or implemented in software (GhostScript being the primary open-source implementation), which renders the described page to the output device. PostScript is unusual among programming languages in that the primary consumers of PostScript programs are printers and PDF generators, not CPUs executing application logic.

PostScript Level 1 (1984) defined the base language: the operand stack, the dictionary stack, the graphics state, path construction operators, and the imaging model. Level 2 (1991) added composite fonts for multi-byte character sets, forms and patterns for repeated graphical elements, binary encoding of PostScript programs, and in-RIP color separation. Level 3 (1997) added smooth shading (shfill with ShadingType 2 and 3 for axial and radial gradients), resource management improvements, and improved image handling with DeviceN color spaces. Each level is a strict superset of the previous; code written for Level 1 runs on Level 2 and Level 3 interpreters, but Level 2 or Level 3 code will fail on a Level 1 device.

PDF (Portable Document Format) is a subset of PostScript with additional structure for random-access page navigation: a PDF file contains a cross-reference table that allows a reader to jump directly to any page without interpreting all preceding pages. Many PDF generators produce PostScript-like page description code and then pass it through a PostScript-to-PDF converter, or generate PDF directly from PostScript operators with added PDF-specific structure. GhostScript (current version 10.x) is the primary open-source PostScript and PDF interpreter and converter, used for server-side document generation, PostScript-to-PDF conversion pipelines, image extraction, and testing. The gs command processes a PostScript file and can write PDF, PNG, TIFF, or other output formats.

PostScript retainer work today covers document generation systems in legal, financial, and medical industries where PostScript-based form generation predates PDF and has never been migrated; printer driver maintenance for device families whose RIPs (raster image processors) are PostScript interpreters; PDF pipeline debugging in publishing and prepress workflows; Type 1 font programming and rendering diagnostics; and legacy prepress systems that use PostScript-based color separation, imposition, and halftoning.

The operand stack: PostScript’s central data structure

The operand stack is the defining data structure of PostScript. Every operator in PostScript pops its arguments from the top of the stack and pushes its results back onto the stack. There are no registers; there are no named local variables in standard PostScript (the dictionary system can simulate them but with different semantics); the operand stack is the only place where intermediate values live during computation. A PostScript programmer who writes 3 4 add is pushing 3 onto the stack, then pushing 4, then calling add, which pops both and pushes their sum 7. The result of add is not stored in a variable; it sits on the top of the operand stack until the next operator consumes it.

The standard stack manipulation operators are: dup duplicates the top item (leaves two copies of the same value); pop discards the top item; exch swaps the top two items; copy n copies the top n items as a group; roll n j rotates the top n items by j positions (positive j rolls the top item to the nth position); index n copies the item at depth n from the top (0-indexed, so 0 index is equivalent to dup); count pushes the current number of items on the operand stack; pstack prints the entire stack contents to standard output without modifying the stack; stack is an alias for pstack in most interpreters.

Stack management in PostScript is tricky for reasons that go beyond the absence of named local variables. The fundamental issue is that PostScript has no mechanism to check that a procedure leaves the stack balanced. There is no equivalent of a function signature that declares “this procedure consumes 4 items and produces 0.” Procedure stack effects are programmer convention documented in comments: % x y width height draw-cell -- means the procedure takes x y width height from the stack and leaves nothing. If the procedure actually leaves 1 item on the stack (due to a missing pop in a conditional branch), PostScript does not raise an error. The extra item sits on the stack, invisible, until a downstream operator pops it as one of its arguments and produces a wrong result.

This is why count is the standard defensive tool for stack balance auditing. Adding count at the start of a procedure and count at the end, then using sub to compute the net effect, allows a PostScript developer to assert zero net stack effect by checking whether the subtraction result equals the expected value. More practically, adding count = (which pops and prints) at strategic points during debugging — or adding pstack to print the entire stack state without modifying it — is the standard diagnostic method for identifying where accumulation begins.

The specific failure mode in the retainer scenario — a push before a conditional that is only consumed in one branch — is one of three common stack imbalance patterns. The second is a conditional where one branch calls a procedure that has a different stack effect than assumed: if the developer believed /some-helper consumed 1 item and produced 1 item, but actually it consumed 1 item and produced 2, every call to /some-helper leaks 1 item. The third is a loop body that conditionally exits via exit from inside a { ... } loop without cleaning up items it pushed before the exit call. Each of these produces silent accumulation that only manifests as a wrong argument to a downstream operator after enough iterations.

The dictionary stack: name lookup and procedure scoping in PostScript

PostScript has a second stack alongside the operand stack: the dictionary stack. Where the operand stack holds values being computed, the dictionary stack holds dictionaries that map names to values. When PostScript encounters a name (a token starting with a letter or slash-prefixed), it looks up the name in each dictionary on the dictionary stack from top to bottom, returning the first match found. The built-in operators are defined in systemdict, which sits at the bottom of the dictionary stack and is always present. User-defined names go into userdict, which sits above systemdict in the default configuration.

The operator /name value def defines name in the topmost dictionary on the dictionary stack. Since the topmost dictionary is typically userdict, /draw-cell { ... } def defines the /draw-cell procedure in userdict where it is accessible from anywhere in the program. Executing the name draw-cell (without the slash prefix) looks up draw-cell in the dictionary stack, finds the procedure object in userdict, and executes it.

The operators begin and end push and pop dictionaries on the dictionary stack. This is how PostScript implements procedure-local name scoping: a procedure that needs local names creates a dictionary with 20 dict (allocating a dictionary with initial capacity for 20 entries), pushes it with begin, defines its local names with def, does its work, then pops the local dictionary with end. The standard idiom is:

20 dict begin /localVar 42 def ... end

This creates a procedure-local namespace that shadows any same-named definition in outer dictionaries for the duration of the begin...end block. The currentdict operator pushes the currently topmost dictionary onto the operand stack, which is useful for introspection — checking what names are currently defined in the local scope.

statusdict is a printer-specific dictionary pushed by the device firmware that contains device-dependent procedures and settings: paper tray selection, duplex configuration, page size, job timeout. PostScript Level 2 formalized many statusdict entries into standard names, but printer-specific extensions remain common. Retainer work on printer driver maintenance frequently involves statusdict entries that differ between printer families.

Dictionary stack mismanagement is less common than operand stack mismanagement but produces equally subtle bugs. A procedure that calls begin to push a local dictionary but exits via an early return-equivalent path (a conditional that jumps past the matching end) leaves a spurious dictionary on the dictionary stack. Subsequent name lookups find the stale local dictionary’s entries before userdict’s entries, producing wrong name resolutions. The diagnostic tool is countdictstack, which pushes the current dictionary stack depth, allowing assertions analogous to count on the operand stack.

Graphics state: gsave/grestore, path operators, and coordinate transformations

The graphics state in PostScript holds all the parameters that affect rendering: the current transformation matrix (CTM), the current path, the current color, the current line width, the current line join and cap styles, the current font, and the current clipping path. The gsave and grestore operators push and pop the entire graphics state as a unit. Each gsave pushes a copy of the current graphics state onto the graphics state stack; each grestore pops the most recent saved state and restores it, discarding any graphics state changes made since the last gsave.

The standard PostScript pattern for a procedure that modifies the graphics state is to wrap its body in a gsave...grestore pair: gsave ... grestore. This guarantees that the procedure’s color changes, coordinate transformations, and clipping path changes do not affect the caller. A procedure that calls gsave but fails to call a matching grestore — typically because an early exit path was added without a corresponding grestore — leaves the graphics state stack with one extra entry. Each subsequent grestore in the caller pops this spurious entry instead of the entry the caller pushed, restoring the wrong state.

Path construction operators build the current path, which is a sequence of line segments and curves that is stroked or filled by imaging operators. The standard sequence is: newpath to initialize an empty path; x y moveto to position without drawing; x y lineto to add a straight line segment; x1 y1 x2 y2 x3 y3 curveto to add a cubic Bezier curve with two control points; closepath to close the current subpath with a straight line back to the starting point. After building the path, stroke renders the path as a line using the current color, line width, and join/cap styles; fill fills the interior with the current color; clip sets the current path as the new clipping boundary, restricting subsequent rendering to the intersection of the current clipping path and the new path.

Color operators: setgray sets the current color to a gray level (0.0 is black, 1.0 is white); setrgbcolor r g b sets an RGB color with each component in the range 0.0 to 1.0; setcmykcolor c m y k sets a CMYK color for press-quality printing. In a CMYK prepress context, using setrgbcolor instead of setcmykcolor in a procedure that will be used in a CMYK separation workflow is a common source of color separation errors.

Coordinate transformations modify the current transformation matrix, which maps user-space coordinates to device-space coordinates. x y translate shifts the origin; sx sy scale scales (negative values mirror); angle rotate rotates; a b c d e f concat concatenates an arbitrary 2D transformation matrix. These operators accumulate: three translate calls compose into a single net translation. Without gsave...grestore wrapping, each transformation applied inside a procedure modifies the CTM for all subsequent rendering operations, which is a common source of wrong-position rendering artifacts in document generation systems.

Typical PostScript retainer work and what it looks like in a work log

A PostScript retainer covers three recurring categories. The first is stack accumulation diagnosis: a rendering artifact (wrong height, wrong position, wrong color, wrong size) appears only on certain cells, certain pages, or certain document types; the developer adds pstack calls at the entry points of the procedures involved; the stack printout shows more items than expected; the developer traces backward through the procedure sequence to find where the extra items originate; the diagnosis finds a push before a conditional that is consumed in the true branch but left unconsumed in the false branch; one extra pop in the false branch eliminates the accumulation; wrong-rendering instances: 8 → 0. Work log entry: “/draw-cell stack imbalance: grayscale push before isHighlighted { ... } if conditional, pop only in true branch; false branch accumulates 1 extra item per call; 8 false-branch cells per page, 8 accumulated items by end of page; exch inside /draw-cell swapped height with accumulated integer at stack depth 4; added pop to false branch; wrong-height cells before: 8; after: 0; 8h.” Without the PostScript stack context, the entry reads as eight hours to add one operator. What is invisible is that PostScript never reports the imbalance, that the wrong rendering only appeared after 8 accumulations, that the exch that consumed the stale item was doing the right thing for the wrong value, and that finding the source of the extra item required understanding the full execution path through the procedure.

The second category is conditional branch balance auditing. A document generation system with dozens of named procedures needs each procedure verified to have zero net stack effect (or a documented non-zero effect). The audit process adds count at the start and end of each procedure and verifies the difference matches the documented stack effect. For procedures with conditionals, the audit verifies each branch independently: executing the procedure with test inputs that force the true branch, checking the count delta, then forcing the false branch and checking again. A procedure with three nested if structures has up to eight execution paths; each path must show the same net stack effect. Work log entry: “stack balance audit of 14 procedures in form-generation library; procedures /draw-cell, /draw-header, /draw-footer, /draw-section-label verified zero net effect on all branches; /draw-footnote had +1 imbalance on the skip-footnote branch (footnote counter push not cleaned up when footnote omitted); fixed; audit complete; 9h.”

The third category is PDF pipeline maintenance. A document generation system that runs PostScript source through GhostScript produces PDFs correctly in testing but fails in production with specific document inputs. The developer sets up a minimal reproduction of the failing document, runs it through GhostScript with verbose error output (gs -dBATCH -dNOPAUSE -sDEVICE=pdfwrite -dDebugPDFMarkInput), identifies the PostScript operator or resource reference that GhostScript handles differently than the device-resident interpreter used in testing, and patches the PostScript source to use the portable form. Common GhostScript compatibility issues: use of statusdict entries that are undefined in GhostScript; use of undocumented Level 2 operators that GhostScript implements more strictly than the original Adobe implementation; font resource references that fail when the font is not embedded. Work log entry: “GhostScript pipeline failure on documents with more than 50 pages: /statusdict /setpagedevice call using device-specific key pagerotate not defined in GhostScript; replaced with standard Level 2 setpagedevice with Orientation key; tested on 200-page document; PDF output correct; 7h.”

Track PostScript developer retainer hours without the status emails

When an 8-hour debugging session traces wrong table cell heights to a mismatched pop inside a conditional branch — a push before { ... } if with pop only in the true branch, leaving 1 extra item per false-branch call, accumulating to 8 stale items per page, causing exch to swap height with a stale integer — the work log needs to name the procedure, the conditional structure, the stack accumulation rate, and the wrong-height cell count before and after. HourTab gives your PostScript retainer client a public dashboard URL they can bookmark: hours used, hours remaining, and a work log that names the operand stack mechanism. No client login. No status emails. CSV in, URL out.

See HourTab pricing →

How HourTab tracks PostScript developer retainer hours

PostScript retainer work is invisible by the same mechanism that makes PostScript powerful: any value left on the operand stack after a procedure returns is a valid program state. PostScript does not check that procedures leave the stack balanced. There is no equivalent of a type-checked function signature that would flag a procedure returning 1 extra item as a compile-time error. Stack pollution accumulates silently across many procedure calls, manifesting as a wrong rendering artifact only when the accumulated stale items happen to sit under the arguments of a specific operator that produces a visible wrong result. A client who sees “8h — fixed stack balance in /draw-cell” cannot assess whether eight hours was proportionate to what sounds like adding a single pop operator.

The work log needs to say: /draw-cell had a push before the isHighlighted { ... } if conditional; the true branch consumed that value via setgray; the false branch had no corresponding pop; each false-branch call left 1 extra item on the operand stack; 8 false-branch cells per page meant 8 extra items accumulated per page; the stale items sat at depths 4 through 11 from the top of the stack; when the 9th false-branch call entered /draw-cell and pushed its 4 arguments (x y width height), the exch operator at step 7 of /draw-cell found the stale integer at depth 1 instead of the correct height value; the cell was drawn with the wrong height; pstack inserted at procedure entry showed the accumulation pattern; wrong-height cells before fix: 8; after: 0; fix: added pop to the false branch of the isHighlighted conditional. That log entry is auditable. It justifies the hours by showing the accumulation mechanism, the detective method, and the concrete before-and-after metric.

HourTab gives PostScript developers a public retainer-hours URL they send to clients — typically legal and financial document generation shops maintaining PostScript-based form systems, publishing and prepress operations with GhostScript-based conversion pipelines, and printer driver developers maintaining device families whose RIPs are PostScript interpreters. For PostScript retainers, each work log entry should name the mechanism at the level of the operand stack: which procedure, which conditional branch, the stack effect before and after the fix measured in items per call, and the concrete wrong-rendering count before and after. Comparative context: PostScript retainer work has conceptual overlap with retainer work on other stack-based or page description languages — PDF direct generation (which uses a similar operator model but is not Turing-complete), SVG (a path description language with a very different architecture: XML, no stack, no Turing completeness), and HTML/CSS for print (which abstracts away the page description entirely, leaving the rendering model to the browser or print engine). PostScript is uniquely positioned as the only Turing-complete page description language with a full operand stack model; expertise in the stack model is what commands $130 to $235 per hour at the senior level and what makes the diagnostic hours invisible without a detailed work log.

FAQ: PostScript developer retainers

What does a PostScript developer on retainer typically do?

A PostScript developer on monthly retainer covers document generation system maintenance (legal forms, financial statements, and medical reports produced as PostScript programs); PDF pipeline debugging (PostScript through GhostScript to PDF conversion, identifying code that a device-resident interpreter handles differently than GhostScript 10.x); Type 1 font programming and rendering diagnostics (Type 1 glyph procedures are PostScript programs; metrics errors, encoding vector mismatches, and hinting anomalies); stack balance auditing (verifying every procedure leaves the stack with the documented net effect; auditing all conditional branches); graphics state debugging (coordinate transformation errors from unguarded CTM modifications; incorrect clipping from missing grestore; color space mismatches between RGB and CMYK contexts); and printer driver maintenance (Level 1, Level 2, and Level 3 compatibility; statusdict entries that vary across device families; page device parameter compatibility).

What PostScript work is most commonly underlogged?

Stack accumulation diagnosis is the most underlogged work: using pstack to trace wrong rendering back to stale items from unbalanced conditional branches (6 to 10 hours, produces a one-line fix). Conditional branch balance auditing: verifying that every if, ifelse, and loop in a procedure leaves the same number of items on the stack regardless of which branch executes (5 to 9 hours per procedure set). Graphics state debugging: tracing wrong colors or wrong clipping to a missing grestore after an early exit path was added to a procedure (4 to 8 hours invisible). Coordinate transformation analysis: verifying that translate, scale, and rotate sequences produce the expected CTM and finding the unguarded transformation that modifies the CTM for all subsequent operations (5 to 8 hours invisible). GhostScript compatibility debugging: finding PostScript code that works in a device-resident interpreter but fails in GhostScript due to interpreter differences, stricter error handling, or undefined statusdict entries (6 to 10 hours invisible).

What are typical PostScript developer retainer rates?

Entry-level PostScript developers with 1 to 2 years covering basic stack operators, path operations, simple procedures, and GhostScript execution typically bill at $55 to $100 per hour. Mid-level PostScript programmers with 2 to 4 years covering stack balance auditing, conditional branch analysis, Type 1 font programming, PDF pipeline integration, and Level 2/3 features typically bill at $85 to $155 per hour. Senior PostScript language developers with 4 or more years covering complex document generation systems, printer driver development, Level 3 smooth shading, in-RIP color separation, and prepress system maintenance typically bill at $130 to $235 per hour. Monthly retainer ranges: $1,600 to $3,000 per month for advisory engagements covering architecture reviews, stack auditing, and PDF pipeline diagnostics (12 to 20 hours per month); $3,200 to $9,000 per month for full engagement PostScript document system development including procedure library maintenance and ongoing GhostScript pipeline support.

What should a PostScript developer retainer agreement include?

A PostScript developer retainer agreement should specify: stack audit scope (which procedures are in scope for balance auditing; whether the engagement covers adding count assertions or only diagnosing known-broken procedures; whether every conditional branch is audited or only top-level branches); conditional branch scope (which if and ifelse structures are in scope; whether the engagement covers rewriting unbalanced structures or auditing only; threshold accumulation rate that triggers a rewrite); PDF pipeline scope (which conversion steps are in scope; GhostScript version compatibility range; device-resident interpreter compatibility; which GhostScript flags and resource settings are in scope); Type 1 font scope (whether font glyph procedure programming or rendering diagnostics are in scope; which font families are covered); and hour logging format (procedure name; stack count before call; stack count after call; expected net effect; actual net effect; accumulation rate if non-zero; conditional branch responsible for imbalance; fix applied; wrong-output count before and after).

How should PostScript developer retainer hours be logged?

Log each PostScript retainer session with: procedure name (e.g., /draw-cell); stack state before call (items on operand stack from count at procedure entry, e.g., 4 items: x y width height); stack state after call (items from count after return, e.g., 5 items if net accumulation of +1); expected net effect (e.g., -4 for a procedure that consumes x y width height and leaves nothing; 0 for a procedure that should leave the stack unchanged); actual net effect (e.g., 0 on true branch, +1 on false branch); accumulation rate (e.g., +1 item per false-branch call; 8 false-branch calls per page; 8 accumulated items per page); conditional branch responsible (e.g., isHighlighted { ... } if — grayscale value pushed before conditional, consumed by setgray in true branch only, not discarded in false branch); fix applied (e.g., added pop to false branch; or restructured to push grayscale value only inside the true branch, eliminating the pre-conditional push); wrong-rendering instances before: 8; after: 0. For graphics state issues: gsave count before entering procedure; gsave count after return; missing grestore: which procedure, which exit path; downstream effect: what color or clipping state the following procedure received vs. expected; fix: added grestore before each early exit path; wrong-rendering instances before and after. For all categories: before and after count of wrong-rendering instances per full document render pass as the primary quality metric.