https://johanley.github.io/postscript-notes/index.html

Why Learn PostScript?

PDF and PostScript are at the center of the print universe.

PostScript Basics

(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)

PostScript Got It Right?

Many simple 2D graphics tools are very similar to PostScript:

Basic Idea

(Fun fact: James Gosling, the creator of Java, made a windowing system based on PostScript.)

Convert PS to PDF

Interesting Parts

"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)

Unpleasant Parts

Pleasant Parts

Your Data Exists In Two Places

Basic Syntax

Literals:

Comments:

The documentation of a proc is with simple comments (input and output, for example).

Make a simple PDF

The idea is to make a PDF by first generating an intermediate PS file.

1. Text file with this content (8859-1 encoding):

%!PS-Adobe-3.0

/Helvetica 48 selectfont
300 500  moveto
(Hello) show

showpage
2. 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

Dynamic Namespace: Dictionary Stack

PostScript use a stack of dictionaries for its namespace:

Loops; Prolog + Script

Looping operators:

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

Show The Currentpoint; Representative Size

Sometimes you want to see exactly where you are on a page.

(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.

Document Structuring Conventions (DSC)

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    
%%EOF
Page 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.

Accents, Re-Encoding Fonts

In order to show French accents (for example):

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

Lifecycle of a Glyph

What happens when (Hello) show is executed? Probably not what you think.

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:

(Fun fact: if the character code you pass isn't supported by a font, it maps to .notdef, which usually maps to a weird placeholder glyph.)

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:

Text Height and Width

To align text, you need the dimensions of the bounding-box of the marks made by the text.

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