WPS: PostScript for the Web

Welcome to WPS, a PostScript and PDF interpreter for HTML 5 canvas.

Note that to see and run the examples, JavaScript must be enabled and your browser must support HTML 5 canvas (latest Firefox, Opera and Chrome should work).

This document allows you to try simple PostScript programs in the WPS sandbox. A few examples are presented here accompanied by a brief description of the interpreter and listing some implementation notes for my future reference.

WPS sandbox

Sandbox:

code from sandbox.

PostScript interpreter

A few initial ideas and questions:

  • Learn and implement a Forth like language. PostScript seems like a good choice:
    • It has the right syntax and stack based evaluation.
    • It is practical and widely used.
    • It has long sucessful history in print and publishing (and more).
    • It is a predecessor of PDF.
    • Almost everything (e.g. editors, pictures, documentation) can be reused to a great extent.
    • It is ideal for HTML 5 canvas experiments because from the PostScript point of view, canvas is just another low level device.
  • Flexibility and simplicity first.
    • Optimize for fast code change, not for raw running speed. Keep the code small and regular if possible.
    • Can JavaScript be used as a portable assembler for the Web? Is building scripting languages on top of JavaScript feasible and efficient enough for real-world use? If not, why? Find the limits.
  • Keep the language/environment specific core as small as possible.
    • Allow to port the interpreter to other languages on both client and server side.
    • Be open for the possibility of running "the same code" on both the client and server side.
  • Can PDF documents be displayed in web browsers without server-side image rendering?
    • Implement a canvas based version of PDF page contents in OnDoc.
  • It might be possible to implement different backend devices to be used instead of HTML 5 canvas, for example an SVG device.
  • Investigate the possibility of implementing a Lisp interpreter suitable for production use in web applications.

There are several things WPS is about:

  • stack(s)
  • function (operator) dictionary
  • reader
  • interpreter/evaluator
  • data types
  • native bindings (JavaScript FFI)
  • PostScript and PDF API

PostScript can be seen as a crossover between Forth and Lisp programming languages. It is (roughly) a programming language with RPN, complex data types, garbage collection and specialized drawing operators.

Trivial example

The core essence of a RPN calculator is captured in the JavaScript code bellow.

function example1() {
   // define stack and operators
   var Os = [];
   var Sd = {};
   Sd["+"] = function() {Os.push(Os.pop() + Os.pop());};
   Sd["="] = function() {alert(Os.pop());};
   // compute 1 2 = 3 + =
   Os.push(1);
   Os.push(2);
   Sd["="]();
   Os.push(3);
   Sd["+"]();
   Sd["="]();
}
"1 2 = 3 + ="

Os stands for Operand Stack, which holds arguments for operators. Sd is a System Dictionary which contains definitions of operators (JavaScript functions in this case).

Example with PostScript reader

PostScript has simple but non-trivial syntax so a reader which reads text and creates internal PostScript objects is necessary. The reader and evaluator is called Ps0 (an empty PostScript interpreter) in the JavaScript code bellow.

function example2(T) {
   var Os = [];
   var Sd = {};
   var Ds = [Sd];
   var Es = [];
   Sd["+"] = function() {Os.push(Os.pop() + Os.pop());};
   Sd["dup"] = function() {var X = Os.pop(); Os.push(X); Os.push(X);};
   Sd["="] = function() {alert(Os.pop());};
   (new Ps0(Os, Ds, Es)).parse(T); // read and interpret code T
}
"12 34 + dup = 56 + ="

Ds is a Dictionary Stack allowing users to redefine existing operators and revert back to the original ones. Es is an Execution Stack which is used to implement a tail recursive evaluator.

Example with recursion

It is possible to write recursive code in PostScript. The following PostScript code is from the Recursion in PostScript PDF document.

/factorial1 {
  1 dict begin
    /n exch def
    n 0 eq {1}{n n 1 sub factorial1 mul} ifelse
  end
} def

5 factorial1 =

/factorial2 {
  dup 0 eq {pop 1}{dup 1 sub factorial2 mul} ifelse
} def

5 factorial2 =

% based on the PostScript example from
% http://partners.adobe.com/public/developer/en/ps/sdk/sample/BlueBook.zip

/factorial3 {
  dup 1 gt {dup 1 sub factorial3 mul} if
} def

5 factorial3 =
the example.

Execution stack

The interpreter manages its Execution Stack explicitly.

Most operators simply:

  1. get their arguments from the Operand Stack
  2. perform some computation and/or side effects
  3. push results to the Operand Stack

Some operators are more complex and involve some kind of control flow, e.g. if, repeat, for, loop operators. Such operators:

  1. get their arguments from the Operand Stack
  2. perform single step of some computation and/or side effects
  3. push the continuation (code and arguments to be executed next) to the Execution Stack

Tail Call Optimisation is implemented using trampoline. The evaluator runs in a loop getting the next continuation from the Execution Stack. Operators that want to "continue" their execution (i.e. use the interpreter to run other operators, including themselves) must perform only one step at a time and save the remaining steps (continuation) on the Execution Stack.

For example, the if operator saves the "then" or "else" code branch to the Execution Stack depending on the value of the "test" argument. It does not "evaluate" the chosen branch directly (recursively) but leaves the control to the evaluator loop.

The whole process of interpreting is fed from JavaScript strings which are obtained from the content of HTML elements (sometimes hidden from this document).

PostScript data types

PostScript has quite rich set of data types. See PostScript Language Reference PDF document for more details.

categorytypeexecutableexamplespec
simplebooleantrue false
fontID
integer42 -123 0
mark
nameYdraw /draw
nullnull
operatorY
real3.14 1e-10
save
compositearrayY[1 /hi 3.14] {1 2 add}
conditionDisplay PostScript
dictionary/a 1/b 2
file
gstateLevel 2
lockDisplay PostScript
packedarrayLevel 2
stringY(hi) <a33f>

The following data types are implemented in WPS:

categorytypedirectliteralexecutable
simplebooleanYY-
numberYY-
mark-Y-
name-YY
nullYY-
operatorY-Y
compositearrayYY-
proc--Y
dictionaryYY-
stringYY-

All the above types are represented directly in JavaScript except:

typerepresentation
markunique object
literal namequoted symbol
executable nameunquoted symbol
operatorfunction
procquoted array

The interpreter needs to understand when to evaluate an argument. The distinction between a "literal" and "executable" is the key. For the "proc" type, its origin from the Execution Stack is also important.

Quoting and execution

There are two important operators to control evaluation at the PostScript language level.

The exec operator usually leaves the argument as is except:

typeresult
executable nameexec value
operatorapply operator
procexec each element

The cvx operator makes the argument "executable". Usually leaves the argument as is except:

fromtohow
literal nameexecutable nameunquote
arrayprocquote
stringproc~ parse

The ~ (tilde) character in the above table means that the functionality has not been implemented yet.

Drawing with PostScript

As a convention, operators beginning with dot are non-standard, low level operators which are subject to change.

There is a difference in how HTML 5 canvas, PostScript and PDF measure angles:

language/deviceunit
canvasrad
PostScriptdeg
PDFrad

Many of the examples below set up their bounding box using the .gbox operator, e.g.

0 0 180 180 .gbox

Only the width and height of the canvas clipping rectangle are taken into account so far. The width and height is related to the drawing units rather than to the size of the canvas element.

Both PostScript and PDF documents have the origin of the coordinate system in the bottom left corner while HTML 5 canvas in the top left corner. Thus, some of the following pictures are displayed upside down unless an explicit coordinate transformation was added. This discrepancy between the origin of the coordinate system is a problem when drawing text because a simple coordinate transformation on its own would draw the text upside-down.

Bowtie example

See the original example in JavaScript.

% based on the JavaScript example from
% https://developer.mozilla.org/en/drawing_graphics_with_canvas#section_6

0 0 180 180 .gbox

/bowtie { % fillStyle --
  200 200 200 0.3 .rgba .setFillStyle
  -30 -30 60 60 rectfill
  .setFillStyle 1.0 .setGlobalAlpha
  newpath
   25  25 moveto
  -25 -25 lineto
   25 -25 lineto
  -25  25 lineto
  closepath
  fill
} bind def

/bowtieDot { % --
  0 0 0 setrgbcolor
  -2 -2 4 4 rectfill
} bind def

/bowtie1 { % fillStyle angle x y --
  gsave
  translate
  rotate
  bowtie
  bowtieDot
  grestore
} bind def

45 45 translate
(red)      0  0  0 bowtie1
(green)   45 85  0 bowtie1
(blue)   135  0 85 bowtie1
(yellow)  90 85 85 bowtie1

Analog clock example

See the original example.

Click on the clock to start/stop it. (If using Chrome, you might need to reload the page for this to work. Not sure why?)

% based on the PostScript example from
% http://oreilly.com/openbook/cgi/ch06_02.html

0 0 150 150 .gbox
0 150 translate
1 -1 scale

/max      150 def
/width    1.5 def
/marker   5 def
/origin   {0 0} def
/center   {max 2 div} bind def
/radius   /center load def
/hsegment 0.50 radius mul def
/msegment 0.80 radius mul def
/ssegment 0.90 radius mul def

/yellow {1 1 0 setrgbcolor} bind def
/red    {1 0 0 setrgbcolor} bind def
/green  {0 1 0 setrgbcolor} bind def
/blue   {0 0 1 setrgbcolor} bind def
/black  {0 0 0 setrgbcolor} bind def

/hangle {/$h load 60 mul /$m load add 2 div neg .deg2rad} bind def
/mangle {/$m load 6 mul neg .deg2rad} bind def
/sangle {/$s load 6 mul neg .deg2rad} bind def

/hand { % segment angle color width --
  origin moveto
  width mul setlinewidth
  load exec
  2 copy   cos mul
  3 1 roll sin mul
  lineto stroke
} bind def

/draw {
  /$h .date (getHours)   0 .call def
  /$m .date (getMinutes) 0 .call def
  /$s .date (getSeconds) 0 .call def
  gsave
  width setlinewidth
  black clippath fill % background
  center dup translate
  90 rotate
  gsave % markers
  12 {
    radius marker sub 0 moveto 
    marker 0 rlineto red stroke
    360 12 div rotate
  } repeat
  grestore
  newpath origin radius 0 360 arc blue stroke % circle
  hsegment hangle /green  2   hand % hour
  msegment mangle /green  1   hand % minute
  ssegment sangle /yellow 0.5 hand % second
  origin width 2 mul 0 360 arc red fill % dot
  grestore
} bind def

draw

/timer    false def
/go       {{draw} .callback 1000 .setInterval /timer exch def} bind def
/halt     {/timer load .clearTimeout /timer false def} bind def
/callback {/timer load type (booleantype) eq {go} {halt} ifelse} bind def

.gcanvas (onclick) /callback load .callback .hook

Running the clock keeps the CPU noticeably busy. Chrome is best with very little overhead. Firefox and Opera perform significantly worse. WPS seems to be fast enough for one-off drawings, but its usability depends on the efficiency of the host JavaScript interpreter when running the interpreter in a tight loop.

Fill example

See the original example in JavaScript.

% based on the JavaScript example from
% https://developer.mozilla.org/samples/canvas-tutorial/4_1_canvas_fillstyle.html

/n 5 def
/w 25 def

0 0 n w mul dup .gbox

4 dict begin
  0 1 n {
    /i exch def
    /ii 1 1 n div i mul sub def
    0 1 n {
      /j exch def
      /jj 1 1 n div j mul sub def
      ii jj 0 setrgbcolor
      w j mul w i mul w w rectfill
    } for
  } for
end

Tiger example

The original example is included with Ghostscript.

Drawing took -- seconds.

the tiger (be patient).

Is this an interesting JavaScript and canvas benchmark?

browserWPS time [s]WPS time (no bind) [s]PostCanvas time [s]
Chrome2.74.11.6
Opera17.912.30
Firefox 3.021.019.07.0
Firefox 3.513.09.53.3
Safari2.900

The above times were on Vaio T7200 Core 2 2GHz 2GB running Ubuntu.

PostCanvas runs this example about 1.5 times (Chrome) to 3 times (Firefox) faster. I am actually surprised that WPS runs only about 1.5 times slower in Chrome even though it interprets almost everything with minimal number of operators coded directly in JavaScript (compared to PostCanvas which implements all operators directly in JavaScript). Time for Safari was reported by Will King and even though it was not run on the same machine as the other tests, it shows that the speed is comparable to Chrome.

Another surprise to me is that I expected more significant speed up after implementing the bind operator. Why does Opera and Firefox get slower in this case?

It should be fairly easy to speed WPS up by coding more operators directly in JavaScript. The speed of PostCanvas could probably be taken as the best case that could be achieved by optimizing WPS.

tiger.png

Note by Dave Chapman:

I've found that reducing the number of function calls in complex scripts has by far the biggest gains in speed - but I guess you already know this. For instance, when I run the Tiger demo it takes about 19sec on my machine (FF3.0, dual core, 4gb ram) but according to the firebug profiler it's making nearly 4 million function calls (as a comparison PostCanvas is only making about 220,000 calls).

Note by Ray Johnson:

Tested Safari 4.0.4 (Win) and Firefox 3.5.5 (Win):

  • Safari 4.0.4 Tiger drawing time = 1.76
  • Firefox 3.5.5 Tiger drawing time = 6.945

I’m on a Dell T7400 Xeon Quad Core 3.0 GHz with 4GB Ram and Vista SP2 32 Bit-and

Firefox throws error about linecap and linejoin not being supported so these were not used here. Opera throws an error when running the PostCanvas example. The tiger does not look the same as rendered by Evince (poppler/cairo) so maybe the linecap and linejoin are really needed to get proper image as intended.

It is also interesting to observe that PDF operators and their names probably came up from shortening/compressing common "user-space" PostScript operators in PostScript files. The tiger.eps file was created in 1990 and contains some "shortcuts" that match PDF operators standardised later.

Drawing with PDF

PDF is rather complex format. WPS aims to implement only drawing operators that can be present in PDF content streams. The number of these operators is fixed and limited. Even though the full PostScript language is not required, it can be convenient to implement them in PostScript.

However, some aspects (e.g. colors) are handled differently in PDF compared to PostScript and these differences are not addressed by WPS. I imagine that a supporting server-side solution like OnDoc would provide necessary data (e.g. decomposing PDF into pages and objects, providing HTML 5 web fonts and font metrics) and WPS would only draw preprocessed page content.

Quoting from Adobe:

A PDF file is actually a PostScript file which has already been interpreted by a RIP and made into clearly defined objects.

Heart example

See also the original example in JavaScript.

% based on the JavaScript example from
% https://developer.mozilla.org/samples/canvas-tutorial/2_6_canvas_beziercurveto.html

0 0 150 150 .gbox

q
75 40 m
75 37 70 25 50 25 c
20 25 20 62.5 20 62.5 c
20 80 40 102 75 120 c
110 102 130 80 130 62.5 c
130 62.5 130 25 100 25 c
85 25 75 37 75 40 c
f
Q

Rectangle example

TODO find the original example

0 0 170 170 .gbox

1 0 0 1 80 80 cm
0 72 m
72 0 l
0 -72 l
-72 0 l
4 w
h S

Triangles example

See also the original example in JavaScript.

% based on the PDF example from
% https://developer.mozilla.org/samples/canvas-tutorial/2_3_canvas_lineto.html

0 0 150 150 .gbox

25 25 m
105 25 l
25 105 l
f

125 125 m
125 45 l
45 125 l
h
S

Smile example

See also the original example in JavaScript.

% based on the JavaScript example from
% http://developer.mozilla.org/samples/canvas-tutorial/2_2_canvas_moveto.html

0 0 150 150 .gbox

%0 0 m % TODO m op meaning?
newpath
75 75 50 0 360 arc % TODO pdf way to draw arc?
S
110 75 m
75 75 35 0 180 arcn
S
65 65 m
60 65 5 0 360 arc
S
95 65 m
90 65 5 0 360 arc
S

Star example

See also the original PDF document where this example is presented.

% based on the PDF example from
% http://www.adobe.com/technology/pdfs/presentations/KingPDFTutorial.pdf

0 0 100 100 .gbox
1 0 0 -1 0 100 cm

q
0 0 1 rg
4 0 0 4 50 50 cm
 0  5.5 m
-4 -5.5 l
 6  1   l
-6  1   l
 4 -5.5 l
f
Q

Squares example

See also the original example in JavaScript.

% based on the JavaScript example from
% https://developer.mozilla.org/samples/canvas-tutorial/5_1_canvas_savestate.html

0 0 150 150 .gbox

q
0 0 m 0 0 150 150 re f
q
0 0.4 1 rg
0 0 m 15 15 120 120 re f
q
1 1 1 rg
0.5 .setGlobalAlpha
0 0 m 30 30 90 90 re f
Q
0 0 m 45 45 60 60 re f
Q
0 0 m 60 60 30 30 re f
Q

Two squares example

See also the original example in JavaScript.

% based on the JavaScript example from
% https://developer.mozilla.org/en/drawing_graphics_with_canvas

0 0 100 100 .gbox

q
0.8 0 0 rg
0 0 m
10 10 55 50 re
f
0 0 0.8 rg
0.5 .setGlobalAlpha
0 0 m
30 30 55 50 re
f
Q

Operators and JavaScript bindings

WPS implements a minimum core in JavaScript and the rest is implemented in PostScript itself.

Many JavaScript data types map quite easily to PostScript data types so native bindings can be implemented mostly in PostScript via PostScript dictionaries (JavaScript objects). HTML 5 canvas API bindings are quite straightforward.

Native operators

categoryinoperatorout
Trivialtruetrue
falsefalse
nullnull
Mathx ysubx-y
x ymulx*y
x ydivx/y
x ymodx%y
Stackmarkmark
counttomarkn
x yexchy x
clear
xpop
xn … x0 nindexxn … x0 xn
x(n-1) … x0 n jrollx((j-1) mod n) … x0 … x(n-1) … x(j mod n)
x1 … xn ncopyx1 … xn x1 … xn
Arrayarraylengthn
xn … x0 arrayastorearray
narrayarray
Conditionalsx yeqbool
x yltbool
Controlbool then elseifelse
n procrepeat
i j k procfor
anyexec
anycvxany
Dictionaryndictdict
dict keygetany
dict key anyput
dictbegin
end
currentdictdict
symwherefalse / dict true
Miscellaneoussavedstack
dstackrestore
anytypename
bool.strictBind
anybindany
Debuggingx=
x==
stack
pstack
JavaScript FFIx1 … xn dict key n.callany
.mathMath
.date(new Date)
.windowwindow
proc.callbackcallback
HTMLm.minvm-1
m1 m2.mmul(m1 x m2)
x y m.xyx' y'
r g b.rgbtext
r g b a.rgbatext

Some of the above operators could still be implemented in PostScript instead of directly in JavaScript.

Core operators

TODO update

categoryinoperatorout
Mathabs
.acos
.asin
atan
.atan2
ceiling
cos
.exp
floor
log
.max
.min
.pow
.random
rand
round
sin
sqrt
.tan
truncate
.e
.ln2
.ln10
.log2e
.log10e
.pi
.sqrt12
.sqrt2
sub
idiv
Stackxdupx x
Conditionalsx ynebool
x ygebool
x ylebool
x ygtbool
bool procif
HTML 5key.gget
any key.gput
key nargs.gcall0
key nargs.gcall1
.gcanvascanvas
w h.gdim
x0 y0 x1 y1.gbox

HTML 5 canvas methods and attributes

Canvas methods

incanvasoutpspdf
.savegsaveq
.restoregrestoreQ
x y.scalescale-
angle.rotaterotate-
x y.translatetranslate-
m11 m12 m21 m22 dx dy.transform-cm
m11 m12 m21 m22 dx dy.setTransform--
x0 y0 x1 y1.createLinearGradientcanvasGradient
x0 y0 r0 x1 y1 r1.createRadialGradientcanvasGradient
image repetition.createPatterncanvasPattern
x y w h.clearRectrectclip
x y w h.fillRectrectfill
x y w h.strokeRectrectstroke
.beginPathnewpathm ?
.closePathclosepath~ h ? ~ n ?
x y.moveTomovetom ?
x y.lineTolinetol
cpx cpy x y.quadraticCurveTo
cp1x cp1y cp2x cp2y x y.bezierCurveToc
x1 y1 x2 y2 radius.arcToarcto
x y w h.rect-~ re
x y radius startAngle endAngle anticlockwise.arc~ arc arcn
.fillfill~ f ?
.strokestrokeS
.clipclip~ W ?
x y.isPointInPathboolean
text x y.fillText1
text x y maxWidth.fillText2
text x y.strokeText1
text x y maxWidth.strokeText2
text.measureTexttextMetrics
image dx dy.drawImage1
image dx dy dw dh.drawImage2
image sx sy sw sh dx dy dw dh.drawImage3
imagedata.createImageData1imageData
sw sh.createImageData1imageData
sx sy sw sh.getImageDataimageData
imagedata dx dy.putImageData1
imagedata dx dy dirtyX dirtyY dirtyW dirtyH.putImageData2

Canvas attributes

typeattributevaluespspdf
num.globalAlpha(1.0)
str.globalCompositeOperation(source-over)
any.strokeStyle(black)~ setdash ?~ d ?
any.fillStyle(black)
num.lineWidth(1)setlinewidthw
str.lineCap(butt) round square~ setlinecapJ
str.lineJoinround bevel (miter)~ setlinejoinj
num.miterLimit(10)setmiterlimitM
num.shadowOffsetX(0)
num.shadowOffsetY(0)
num.shadowBlur(0)
str.shadowColor(transparent black)
str.font(10px sans-serif)
str.textAlign(start) end left right center
str.textBaselinetop hanging middle (alphabetic) ideographic bottom

Other operators

incanvasoutpspdf
canvasGradient offset color.addColorStop

Other attributes

dicttypeattributevaluespspdf
textMetricsnumwidth
imageDatacntwidth
imageDatacntheigth
imageDatacanvasPixelArraydata
canvasPixelArraycntlength

TODO [IndexGetter, IndexSetter] CanvasPixelArray

PostScript operators

TODO update

categoryinoperatorout
x y [m]transformx y
x y [m]itransformx y
graysetgray
r g bsetrgbcolor
???setfont ?
clippath ?
textshow ?
x yrlineto

PDF operators

categoryoperatormeaning
General graphics statewsetlinewidth
J~ setlinecap
j~ setlinejoin
Msetmiterlimit
d~ setdash ?
ri
i1 .min setflat
gs
Special graphics stateqgsave
Qgrestore
cm.transform
Path constructionmmoveto
llineto
c.bezierCurveTo (~ curveto)
vcurrentpoint 6 2 roll c
y2 copy c
hclosepath
re! x y m , x+w y l , x+w y+h l , x y+h l , h
Path paintingSstroke
sh S
f~ fill
Ff
f*~ eofill
Bf S ! q f Q S
B*f* S ! q f* Q S
bh B
b*h B*
n~ newpath
Clipping pathsWclip
W*eoclip
Text objectsBT~ q
ET~ Q
Text stateTc
Tw
Tz
TL
Tf
Tr
Ts
Text positioningTd
TD
Tm
T*
Text showingTj~ show
TJ
'
"
Type 3 fontsd0setcharwidth
d1setcachedevice
ColorCS
cs
SC
SCN
sc
scn
Gg
gsetgray
RGrg
rgsetrgbcolor
Kk
ksetcmykcolor
Shading patternssh
Inline imagesBI
ID
EI
XObjectsDo
Marked contentMP
DP
BMC
BDC
EMC
CompatibilityBX
EX

Supported Browsers

I have tried the following browsers so far:

BrowserVersionNote
Firefox3.0.11no text drawing, linecap, linejoin
3.5b4pre~ same as Firefox 3.0.11?
3.5.5 Winreported by Ray Johnson
Opera10.00 Betano text drawing, ugly aliasing
Chrome3.0.189.0lines not joined properly
Safarifor Mac Version 4.0.2 (5530.19)reported by Will King
4.0.4 Winreported by Ray Johnson

If you are using a different browser, please let me know if it works for you.

Limitations and Known Issues

  • many PostScript operators are still to be implemented
  • only small fraction of PDF operators has been implemented
  • text drawing and font related functionality has not been implemented

Changes

2009-07-15 v0.2

  • Capable of drawing tiger.eps
  • JavaScript callbacks and timer added
  • bind operator implemented
  • Refactored JavaScript code: parser, evaluator and PostScript interpreter separated
  • Improved documentation

2009-06-30 v0.1

  • Initial version

Links

Discussions about WPS on reddit and ajaxian.

PostCanvas is a RPN interpreter with many PostScript operators implemented directly in JavaScript. It is faster than WPS but not a "real" PostScript.

SVGKit has a PostScript interpreter on the wish list.

PostScript is a registered trademark of Adobe Systems Incorporated.