PDF and PostScript are at the center of the print universe.
(Fun fact: in the 1980s, the most powerful computer in a typical office was the printer.)
"The PostScript language is relatively simple. It derives its power from the ability to combine [its] features in unlimited ways without arbitrary restrictions." (Red Book)
"However, there is not a distinction between data and programs; any PostScript object may be treated as data or be executed as part of a program." (Red Book)
"There is no notion of 'reading in' a program before executing it. Instead, the PostScript interpreter consumes a program by reading and executing one syntactic entity at a time." (Red Book)
(blah)
/blah
[1 2 3 (blah) false]
<<(name)(Bob) (age)28 (silly?)true>> - key-value pairs, in order
{0.01 mul mul} - an executable array of tokens
Comments:
% This is a comment
%%Title: My Lovely Document - a Document Structuring Convention (DSC) comment
The documentation of a proc is with simple comments (input and output, for example).
1. Text file with this content (8859-1 encoding):
%!PS-Adobe-3.0 /Helvetica 48 selectfont 300 500 moveto (Hello) show showpage2. Run Ghostscript (or Adobe's Distiller) to read in PS and output PDF:
C:\ghostscript\gs10.04.0\bin> gswin64c.exe -dNOSAFER -sDEVICE=pdfwrite -o C:\temp\hello.pdf C:\temp\hello.ps
3. View the output: hello.pdf
Using absolute coordinates can easily lead to a coding horror in which a simple change of page format leads to wide-spread ripple effects. A more tasteful way is to define the page format in one place, and to use percentages everywhere in your code. (PostScript's scale operator doesn't help if the aspect ratio changes.)
This is trivial to implement, and lets your output be responsive to changes in the width/height.
Here's a more tasteful version of the above:
Output: hello-again.pdf
%!PS-Adobe-3.0
% set the format explicitly
% 72 = 1 inch, so this means 3" x 4"
/page-width 3.0 72 mul def % 216
/page-height 4.0 72 mul def % 288
<</PageSize [page-width page-height]>> setpagedevice
% define a simple 'proc' (procedure) for percentages
% pass two numbers; a value and a percent
% example 'page-width 15 pct' is 15% of the page-width
/pct {
% the current operand stack (for example):
% 216 15
0.01 % 216 15 0.01
mul % 216 0.15
mul % 32.4 (result left on the operand stack)
} def
/Helvetica 16 selectfont
page-width 25 pct
page-height 70 pct moveto
(Hello again...) show
showpage
forall
for
repeat
An important idea is the separation of code into two parts:
The prolog is usually written by hand, and the script is usually generated by a program that you write.
Output: loops.pdf
%!PS-Adobe-3.0
/wd 3.0 72 mul def
/ht 4.0 72 mul def
<</PageSize [wd ht]>> setpagedevice
/pct { 0.01 mul mul } def
/Helvetica 8 selectfont
% obj -> show it
% 'show' only works with strings
/show-it {
% COMMON IDIOM: a temporary dict
1 dict begin % temp dict!
/sbuff 20 string def % string buffer for cvs
sbuff cvs % convert arg into a string
show
end % pop the temp dict off the dict-stack
} def
% dict x y -> print the data-dict key-value pairs
% example input:
% <<(name)(Annapurna) (height)8030 (country)(Nepal)>> 100 200
/oh-mighty-mountain {
moveto % eats the x y on the stack
{ % puts each key-value pair the operand stack
% WARNING: iteration order is not specified for a dict!
% WARNING: (height) coerced by PS into /height!
% /height 8030
% swap the two top items on the stack
exch % 8030 /height
show-it % 8030
(:) show % 8030
show-it
( ) show % spacing
} forall % iterate over all key-value pairs
} def
% array x y -> print the array items
% example input:
% [(Mercury) (Venus) (Earth) (Mars)] 100 200
/oh-glorious-planets {
moveto % eats the x y on the stack
{ % puts each array-item on the stack in sequence
show
( ) show % spacing
} forall
} def
% THIS IS AN IMPORTANT PATTERN: procs defined first, then pass
% a data structure to the proc (in practice, usually a dict).
%
% PROLOG: the procs at the top (hand-crafted!).
% SCRIPT: the part where a data structure is passed
% to the procs (generated by a program!).
<<(name)(Annapurna) (height)8091 (country)(Nepal)>>
wd 5 pct ht 90 pct
oh-mighty-mountain
[(Mercury) (Venus) (Earth) (Mars)]
wd 5 pct ht 80 pct
oh-glorious-planets
showpage
Some variations on loops:
Output: loops-again.pdf
%!PS-Adobe-3.0
/wd 3.0 72 mul def % 216
/ht 4.0 72 mul def % 288
<</PageSize [wd ht]>> setpagedevice
/pct { 0.01 mul mul } def
/Helvetica 8 selectfont
% a repeat-loop, do something N times
/blah-blah-blah {
3 {
(Blah) show
wd 2 pct 0 rmoveto
} repeat
} def
% a for-loop
% iteration index: initial, step, final (inclusive)
/yada-yada-yada {
1 1 5 {
% the iteration index is placed on the stack
% during each iteration
2 mod 0 eq {
(YADA)
} {
(yada)
} ifelse
show
wd 2 pct 0 rmoveto
} for
} def
wd 5 pct ht 90 pct moveto
blah-blah-blah
wd 5 pct ht 80 pct moveto
yada-yada-yada
showpage
(The default origin of coordinates is the bottom-left corner of the page.)
Output: show-currentpoint.pdf
%!PS-Adobe-3.0
/wd 3.0 72 mul def
/ht 4.0 72 mul def
<</PageSize [wd ht]>> setpagedevice
/pct { 0.01 mul mul } def
/Helvetica 8 selectfont
% r
% pass a representative size for the marks made by this proc
% debugging - show the location of the currentpoint
% shows a circle plus axes
% the +X-axis is in black, the +Y-axis in red
% the currentpoint must exist
/show-currentpoint {
3 dict begin
gsave
/r exch def % very common task - exch is needed here
currentpoint
/y exch def
/x exch def
r 0.1 mul setlinewidth
% circle and x-axis in black
% cmyk colors for print; rgb for web
0 0 0 1 setcmykcolor
% we want the currentpoint, not the currentpath
newpath
x y moveto
x y r 0 360 arc
% x-axis
x y moveto
r neg 0 rmoveto % 'r' for relative!
r 3 mul 0 rlineto % horizontal
stroke
% y-axis in red
0 1 1 0 setcmykcolor
x y moveto
0 r neg rmoveto
0 r 3 mul rlineto % vertical
stroke
grestore
end
} def
wd 40 pct ht 80 pct moveto
wd 10 pct show-currentpoint
% save-restore because of the rotate and scale
gsave
wd 40 pct ht 50 pct moveto
45 rotate
2 2 scale
wd 10 pct show-currentpoint
grestore
wd 40 pct ht 25 pct moveto
wd 2 pct show-currentpoint
wd 10 pct ht 10 pct moveto
(An affordance to show the currentpoint.) show
showpage
Recurring theme: passing in data that's used as a representative size. For example, in marking a rectangular area, you would pass in dx and dy, the width and height of the rectangle. Or, you could simply def the two values, and thus add them to the current namespace.
Example to give you the general idea:
%!PS-Adobe-3.0 %%Title: Une Vie %%Version: 1.0 %%Creator: Guy de Maupassant %%CreationDate: 2025-04-02 %%LanguageLevel: 3.0 %%DocumentMedia: Plain 360.0 576.0 60 white () %%DocumentNeededResources: font Georgia Georgia-Italic Georgia-Bold %%BoundingBox: 0 0 360 576 %%Orientation: Portrait %%Pages: 2 %%EndComments %%BeginDefaults %%EndDefaults %%BeginProlog ... procs of general utility go here ... % You can also reference items in other files, % somewhat like an #include directive in C: (basic-chart.ps) runlibfile (basic-table.ps) runlibfile (line-breaks.ps) runlibfile %%EndProlog %%BeginSetup /page-width 5.0 72 mul def /page-height 8.0 72 mul def <</PageSize [page-width page-height]>> setpagedevice % minimizes off-by-one-pixel issues with lines true setstrokeadjust % make sure the fonts are always embedded in the PDF <</NeverEmbed [ ]>> setdistillerparams %%EndSetup %%Page: 1 1 %%BeginPageSetup /pgsave save def %%EndPageSetup .. page 1 goes here .. .. a few commands, but mostly pass data to a proc.. pgsave restore showpage %%PageTrailer %%Page: 2 2 %%BeginPageSetup /pgsave save def %%EndPageSetup .. page 2 goes here .. pgsave restore showpage %%PageTrailer %%Trailer %%EOFPage independence: your pages should not depend on any other page. This helps printers reorder pages and so on.
The save-restore operators help implement page independence. They act on objects in local VM (virtual memory - a heap-like structure?). This includes the graphic state, but it does not include the operand stack or the dictionary stack.
Output: encode.pdf
%!PS-Adobe-3.0
/wd 6.0 72 mul def
/ht 8.0 72 mul def
<</PageSize [wd ht]>> setpagedevice
/pct { 0.01 mul mul } def
% new-font-name old-font-name
%
% 'Re-encode' a font.
%
% Create a new font from an existing one, which uses the
% ISOLatin1Encoding instead of the standard encoding.
% The encoding of this PostScript file is itself 8859-1 = ISOLatin1.
% https://stackoverflow.com/questions/270672/unicode-in-postscript
/latinize {
findfont
dup length dict
begin
% copy everything in the font's dict except
% for the FID (a unique id for a font)
{ 1 index /FID ne {def}{pop pop} ifelse } forall
% override the encoding entry in this dict
/Encoding ISOLatin1Encoding def
% push this font-dict onto the operand stack
currentdict
end
% internally sets a FID (font-id) for the new font
definefont
% remove the new font object from the operand stack
pop
} def
% str xp yp
% 'p' means percent here
/show-it {
2 dict begin
/yp exch def
/xp exch def
wd xp pct
ht yp pct moveto
show
end
} def
% re-encode Times-Roman
/my-lovely-font /Times-Roman latinize
% now /my-lovely-font can be used like any other font
/my-lovely-font 12 selectfont
/left 3 def
(File encoding 8859-1 (Latin-1).) left 90 show-it
(Accented chars: à ç é è À Ç) left 80 show-it
(- Ça va bien, Madame Brontë?) left 75 show-it
% this fails, because the font hasn't been re-encoded
/Helvetica 12 selectfont
(- Ça va bien, Madame Brontë?) left 70 show-it
showpage
Take the H character for instance. The system doesn't pass H to the show operator. It passes a number determined by the encoding of the text.
'H'
-> character code 156
-> font's encoding vector [/.notdef .. /A /B .. /ydieresis]
-> position 156 has the glyph /H
-> font's drawing instructions for /H
Here's the lifecycle of a character being shown:
The glyph is drawn using the currentpoint as the origin of coordinates. When finished, the currentpoint is updated by the font to a new position. The delta between the starting and final position is defined by the font. In general, that delta has two parts, dx and dy. Those deltas are available from the stringwidth operator.
Confusing:
Output: text-dim.pdf
%!PS-Adobe-3.0
/pg-wd 400 def
/pg-ht 500 def
<</PageSize [pg-wd pg-ht]>> setpagedevice
/Times-Roman 20 selectfont
/pct {0.01 mul mul} def
/sbuff 20 string def
% str -> dx dy
% return the width/height (dx, dy) of the given text
% this is useful for centering in various ways
/text-dim {
gsave
0 0 moveto
% the bounding box of the text
false charpath flattenpath pathbbox % x0 y0 x1 y1
grestore
% the 'roll' operator treats the stack like a merry-go-round
3 -1 roll sub % x0 x1 dy
3 1 roll sub % dy -dx
-1 mul % dy dx
exch % dx dy
} def
/the-width (Blah) text-dim pop def
pg-wd 5 pct pg-ht 90 pct moveto
(Width of 'Blah': ) show
the-width sbuff cvs show
showpage