[comp.lang.postscript] Building A PostScript Toolset

cramer@optilink.UUCP (Clayton Cramer) (08/30/89)

There was some discussion a few months back about building libraries
of PostScript functions, but very little seems to have come of it.
I have decided to bring the subject back up again, along with some
examples.

Reasons For A Toolset

1. The obvious one -- to avoid reinventing the wheel.

2. To build on other people's knowledge.

3. To provide some simple examples for those still learning PostScript.
(That includes me -- and I've been writing PostScript, intermittently,
since 1984).

Ways To Build Such A Toolset

1. Write single, simple functions.  As an example, the PostScript
function at the end of this posting does pie chart slices -- just
pie chart slices.  I originally intended it to compute where to
put legends, and invoke a function passed in to it to display 
legends next to the appropriate slices, but both because I don't
have time to debug, and because someone can then build such a
layer around my pie slice function, all it does it produce pie
slices.

2. Make minimal use of global variables.  As nice as it is to "setgray"
and have that in effect until changed, it also makes it hard to
distinguish where or how something was set.  (All the same arguments
that apply to programming languages apply to page description
languages as well).  This means that these functions will be passed
parameters that the same, much of the time.  The decision whether
to include all possible variations or not is one of those 
unpleasant trade-offs.

3. To prevent creating write-only code, set variable names to stack
items at the start of the function, and reference those variable
names thereafter whenever possible, rather than using lots of dups,
exchs, and rolls.  It's less efficient, certainly, but the goal
here is reusable code that can be easily changed to do something
else, if needed.

4. A standard (or at least understandable) convention for showing
the number of parameters and what they do.

5. A %%DEMO section at the end of each tool, so you can see how
it's called, and have one clear example, if the parameter description
isn't.  By using %%DEMO before the demonstration invocation, you
make it easier to automate the process of extracting just the
executable code from each tool.

I'm hoping that everyone will contribute general purpose tools
that they need to create, so that over time, we will have most
of the essential components -- triangles, parallelograms, log-log
graphs, bar charts, and so on -- needed to write complex applications.

%!
/PieSlice
    {% draw a pie slice
     % (pie point x, pie point y, start angle, size, radius, gray, width)
     gsave
     /Width exch def /Gray exch def /Radius exch def /Size exch def 
     /Angle exch def /Y exch def /X exch def
     newpath X Y moveto X Y Radius Angle Angle Size add arc closepath 
     Width setlinewidth Gray setgray fill
     grestore} bind def
%%DEMO
% Draw a complete pie, with different slices in different gray scales.
300 300  45  75 200 0.00 2 PieSlice
300 300 120  45 200 0.15 2 PieSlice
300 300 165  90 200 0.30 2 PieSlice
300 300 255 105 200 0.45 2 PieSlice
300 300   0  45 200 0.60 2 PieSlice 
showpage
-- 
Clayton E. Cramer {pyramid,pixar,tekbspa}!optilink!cramer
"No man is an island" is the beginning of the end of personal freedom.
----------------------------------------------------------------------------
Disclaimer?  You must be kidding!  No company would hold opinions like mine!

roy@phri.UUCP (Roy Smith) (08/31/89)

In <2251@optilink.UUCP> cramer@optilink.UUCP (Clayton Cramer) writes:
> 3. To prevent creating write-only code, set variable names to stack
> items at the start of the function [...] rather than using lots of dups,
> exchs, and rolls

	Well, here is a counter example.  I wrote this a while ago, and
have used it several times.  It is a bunch of routines to let you work with
vectors in 2-space (i.e. x,y coordinates).  It's all dups, exchs, and
rolls.  Given the triviality of these, I think it's pretty easy to follow
the thread of them, and it's all pretty well documented anyway.  But, yes,
in general I agree -- code which does lots of fancy stack manipulation is
hard to grok, even for somebody like me who grew up on HP (i.e. stack)
machines.  I did a lot of up,up,rolldown,exchange stuff when I was hacking
9810's.  Obviously, the following can be extended, dot and cross products,
and dimensions higher than 2, but that's all I needed so that's all I did.

-------------------
%
% Vectorops.ps -- vector operators for PostScript.
%
% Copyright 1987 Roy Smith
% Permission is hereby granted for any use of this software other
% than selling it or including it in a commercial product.
%
% $Header: vectorops.ps,v 1.1 87/09/16 20:31:59 roy Exp $
%

% vadd -- add two vectors.
% x1 y1 x2 y2 vadd ==> x1+x2 y1+y2
/vadd
{
	3 -1 roll add		% ==> x1 x2 y2+y1
	3 1 roll add		% ==> y2+y1 x1+x2
	exch			% ==> x1+x2 y2+y1
} def

% vsub -- subtract two vectors.
% x1 y1 x2 y2 vsub ==> x1-x2 y1-y2
/vsub
{
	3 -1 roll sub neg	% ==> x1 x2 -(y2-y1)
	3 1 roll sub		% ==> -(y2-y1) x1-x2
	exch			% ==> x1-x2 -(y2-y1)
} def

% vneg -- take negative of vector (scalar multiply by -1).
% x y vneg ==> -x -y
/vneg
{
	exch neg		% ==> y -x
	exch neg		% ==> -x -y
} def

% vdiv -- divide vector by scalar.
% x y s vdiv ==> x/s y/s
/vdiv
{
	dup			% ==> x y s s
	3 1 roll div		% ==> x s y/s
	3 1 roll div		% ==> y/s x/s
	exch			% ==> x/s y/s
} def

% vmul -- multiply vector by scalar.
% x y s vmul ==> x*s y*s
/vmul
{
	dup			% ==> x y s s
	3 1 roll mul		% ==> x s y*s
	3 1 roll mul		% ==> y*s x*s
	exch			% ==> x*s y*s
} def

% vexch -- exchange two vectors on stack.
% x1 y1 x2 y2 vexch => x2 y2 x1 y1
/vexch
{
	4 2 roll		% ==> x2 y2 x1 y1
} def

% vpop -- pop a vector off the stack.
% x1 y1 vpop => ---
/vpop
{
	pop pop
} def

% vdup -- duplicate a vector on the stack.
% x1 y1 vdup => x1 y1 x1 y1
/vdup
{
	dup			% ==> x1 y1 y1
	3 -1 roll dup		% ==> y1 y1 x1 x1
	4 1 roll exch		% ==> x1 y1 x1 y1
} def
-- 
Roy Smith, Public Health Research Institute
455 First Avenue, New York, NY 10016
{att,philabs,cmcl2,rutgers,hombre}!phri!roy -or- roy@alanine.phri.nyu.edu
"The connector is the network"

labc-1aa@e260-2d.berkeley.edu (08/31/89)

A couple of points:

In order to minimize potential side effects, when you define names in a
ToolSet procedure, you should put them in their own dictionary. 
Start with a "n dict begin" where "n" is the number of locals you'll use, and
finish with an "end".

Example:

/MyProcedure 		% arg1 arg2 MyProcedure result
{
  2 dict begin
  /arg2 exch def /arg1 exch def

  ...

  end
} def

Also, "%%DEMO" *might* confuse some spooler software since it looks like
a document structuring comment (obviously, this is how you're using it, but
it isn't part of the standard).  How about just "% DEMO"?

Finally, my basic rule about using local variables is to make
a dictionary entry only if I'm going to use a value more than once.  A comment
can make up for not having defined a name for an argument.  I guess I'd
recommend trying to achieve a balance between naming and efficiency.

Bob Heiney
labc-1aa@web.Berkeley.edu

lemay@lorelei.Sun.COM (Laura Lemay) (08/31/89)

In <2251@optilink.UUCP> cramer@optilink.UUCP (Clayton Cramer) writes:
> 3. To prevent creating write-only code, set variable names to stack
> items at the start of the function [...] rather than using lots of dups,
> exchs, and rolls

Hm.  I was taught to use stack operations rather than lookup operations
whenever possible, since its faster that way.

Evil as hell to trace, tho.




-Laura Lemay			lemay%lorelei@sun.com
Redhead.  Drummer.  Geek.

jcgs@wundt.harlqn.uucp (John Sturdy) (08/31/89)

Yes, that's a fine idea. But let's be organized about procedure
naming! It would be good to make these toolbits into ProcSets (as in
EPSF %%BeginProcSet etc) and it would be even better to be able to
concatenate them to build prologues, without worrying about name
clashes.

My suggestion here (clumsy but effective) is to give each chunk a
string name, and use that string at the start of each name that the
chunk describes globally. Taking that a little further, they could be
given some EPSF description other than ProcSet, thus telling handlers
that, unlike ProcSets, they can be concatenated safely to make
prologues.

--
__John            When asked to attend a court case, Father Moses took with him
          a leaking jug of water. Asked about it, he said: "You ask me to judge
               the faults of another, while mine run out like water behind me."

                jcgs@uk.co.harlqn (UK notation) jcgs@harlqn.co.uk (most places)
    ...!mcvax!ukc!harlqn!jcgs (uucp - really has more stages, but ukc knows us)
John Sturdy                                            Telephone +44-223-872522
                      Harlequin Ltd, Barrington Hall, Barrington, Cambridge, UK

cplai@daisy.UUCP (Chung-Pang Lai) (08/31/89)

This is for your tool set.

This routine sorts the dictionary keys and leave them on stack for
printing or other purpose.  It uses insertion sort.  See comment for details.

%!
% Sort Dictionary key
%	Stack:	Before => After
%		dict => (keyn) ... (key3) (key2) (key1) n
% Dictionary is a hash table, this function is useful when the keys
% needed to be printed in increasing order, the results are left on
% stack in string datatype.  Necessary conversion is needed if used
% for other purposes.
% Limitation: According to the Red Book, the Operand stack's maximum
%             depth is 500, this function will fail if the results
%             overflow the stack.
% Written by C.P.Lai  Dec 17, 1988
/SortDictKey { % function def
	/dictcount 1 def
	{ %forall	% sort dict keys on operand stack
		pop 	% consume the value part
		% convert Key to string since type is unknown, or else ge may choke
		80 string cvs	% leave each key in a unique string on stack
		dictcount -1 2 {% for
			dup /dictindex exch def	% save index of key being checked
			1 index exch	% copy latest key for ge
			index		% use dictindex to get compare key for ge 
			ge {dictindex 1 roll exit} if	% insert into place
		} for 	% check from the already sorted high end
		/dictcount dictcount 1 add def
	} forall	% standard insertion sort algorithm
	dictcount 1 sub		% leave final count on stack
} def

% DEMO
% yourdictionary SortDictKey {
%    show newline
% } repeat
-- 
.signature under construction ...
{pyramid, osu-cis, uunet, killer}!daisy!cplai    C.P. Lai
cplai%daisy.UUCP@uunet.UU.NET   cplai%daisy@killer.DALLAS.TX.USA
Daisy Systems Corp, 700B Middlefield Road, Mtn View CA 94039.  (415)960-6961

hemphill@cit-vax.Caltech.Edu (Scott Hemphill) (09/01/89)

A general purpose Quicksort for your toolset.

Scott Hemphill	hemphill@csvax.caltech.edu	...!ames!elroy!cit-vax!hemphill
-------------------------------------------------------------------------------
%!
% August 31, 1989  by Scott Hemphill
% I wrote this code, and hereby place it in the public domain, i.e. there are
% no copying restrictions of any kind.
%
% Quicksort an Array
%	Stack:	Before => After
%		array proc =>
% This Quicksort implementation will sort an array of arbitrary PostScript(tm)
% objects, since you provide the procedure that does the object comparisons.
% Your procedure should remove two objects from the stack, pushing true on
% the stack if the first object is strictly less than the second object in
% sort order, otherwise pushing false.  (E.g. if you wish to sort an array of
% strings alphabetically, or an array of numbers into increasing order, you
% can simply use the PostScript operator "lt".  See below for examples.)
% The array is sorted in place, and is not left on the stack, so you need
% another copy of it somewhere.

/qsortdict 7 dict def
qsortdict begin
   /q-compare 0 def		% user-supplied comparison procedure
   /q-array 1 def		% current sub-array being sorted by qsortsub
   /q-length 2 def		% length of q-array
   /q-left 3 def		% left scan index
   /q-right 4 def		% right scan index
   /q-last 5 def		% partitioning element
   /q-temp 6 def		% temporary array element
end

/qsort
{
   qsortdict begin
   /q-compare exch def
   qsortsub
   end
}
bind def

/qsortsub
{
   /q-array exch def
   /q-length q-array length def
   q-length 1 gt
   {
      /q-left 0 def
      /q-right q-length 1 sub def
      /q-last q-array q-right get def
      {
         {
            q-array q-left get q-last q-compare
            {/q-left q-left 1 add def}
            {exit}
            ifelse
         }
         loop
         {
            q-left q-right eq {exit} if
            q-array q-right get q-last q-compare
            {exit}
            {/q-right q-right 1 sub def}
            ifelse
         }
         loop
         q-left q-right eq {exit} if
         /q-temp q-array q-left get def
         q-array q-left q-array q-right get put
         q-array q-right q-temp put
      }
      loop
      q-array q-length 1 sub q-array q-left get put
      q-array q-left q-last put
      q-array q-left 1 add q-length q-left sub 1 sub getinterval
         q-array 0 q-left getinterval
         qsortsub
      qsortsub
   }
   if
}
bind def

% Implementation notes:
% 1. The numbers in the initial qsortdict definitions are just place holders
%    for the real definitions which will be made when qsort is invoked.
% 2. qsort is just a shell for the recursive procedure qsortsub, which does
%    the real work.
% 3. qsortsub is a straightforward implementation of the Quicksort algorithm.
%    It removes the array on the stack, and sorts it in place by calling itself
%    recursively on two pieces of the initial array.  Instead of passing the
%    initial array plus left and right indices, it passes a subarray, taking
%    advantage of the fact that the subarray shares the same memory as the
%    original array.  This involves slightly more overhead, but is cleaner
%    to implement.
% 4. The qsortdict dictionary is used at all recursion levels, but there are
%    no definition conflicts!  This is because all references to the dictionary
%    definitions happen before the two recursive qsortsub calls at the end.
% 5. There are quite a number of performance improvements to be made which have
%    been deliberately left out, for clarity.  One important improvement would
%    be to use a different procedure to sort small sub-arrays.  There is a lot
%    of time wasted sorting two-element arrays, for example.  Even as written,
%    however, you should see a significant improvement over insertion-sort or
%    other n^2 algorithms for moderately sized arrays.  Another improvement
%    might be to define all the qsortdict variables as one-element executable
%    arrays.  The variables can be set by storing a value into the array, and
%    read by executing the array.  The point is that the array itself can be
%    put in the qsortsub procedure instead of its name.  Each time you do this
%    you save one dictionary name lookup.

% DEMO

% This example uses the "lt" operator to do numeric comparisons:
%	[3 1 4 1 5 9 2 6 5] dup /lt load qsort ==


% The "lt" operator is used here to do an alphabetical comparisons of strings.
%	[(twas) (brillig) (and) (the) (slithy) (toves)] dup /lt load qsort ==

% This final example sorts the keys in a dictionary.  It performs the
% comparison with a 6-element procedure which converts the keys to strings.
% Since the strings have been exchanged, it uses a "gt" operator instead of
% "lt".  It would be faster to convert all of the keys in the dictionary to
% strings first, but this would waste more memory.  Note that particular
% care has been given to the comparison procedure.  Execution of the "bind"
% operator after the procedure definition converts the names "cvs", "exch",
% and "gt" to their corresponding operators.  The "//" lexical operator
% (in PostScript version 38.0) converts the names "thing1" and "thing2" to
% the actual strings (the locations in memory--not the contents of the
% strings).  This means that all 6 names in the procedure have been converted
% to the objects they refer to, and there are no dictionary name lookups
% during the execution of this procedure.
%	/arr [/Times-Roman findfont /CharStrings get {pop} forall] def
%	/thing1 128 string def
%	/thing2 128 string def
%	(initial array:) = arr ==
%	arr {//thing2 cvs exch //thing1 cvs gt} bind qsort
%	(final array:) = arr ==
-- 
Scott Hemphill	hemphill@csvax.caltech.edu	...!ames!elroy!cit-vax!hemphill

greid@adobe.com (Glenn Reid) (09/02/89)

In article <1989Aug30.184607.21647@agate.uucp> labc-1aa@web.Berkeley.edu (Bob Heiney) writes:
>
>In order to minimize potential side effects, when you define names in a
>ToolSet procedure, you should put them in their own dictionary. 
>Start with a "n dict begin" where "n" is the number of locals you'll use, and
>finish with an "end".
>
>Example:
>
>/MyProcedure 		% arg1 arg2 MyProcedure result
>{
>  2 dict begin
>  /arg2 exch def /arg1 exch def
>
>  ...
>
>  end
>} def

It is a good idea to keep names local if you can.  Unfortunately,
your example isn't good because it creates a new dictionary every time
you call the procedure, which is probably not what you want.

Here are three ways to get the local dictionary without paying for it
each time you call the procedure.  The first is nice but won't
work on a version 23.0 LaserWriter (which was discontinued in 1986
or something like that, when the LaserWriter Plus came out).  The
second one will work on all implementations, but you have to do
a name lookup on the dictionary every time you call the procedure.
The last one is the least intrusive (no extra names defined) the
most efficient, and the hardest to understand :-)

% version 1:
/localdict 2 dict def
/MyProcedure 		% arg1 arg2 MyProcedure result
{
	//localdict begin
		/ arg2 exch def / arg1 exch def
		...
		end
} def

% version 2:
/localdict 2 dict def
/MyProcedure 		% arg1 arg2 MyProcedure result
{
  localdict begin
	/arg2 exch def /arg1 exch def
	...
  end
} def

/MyProcedure 		% arg1 arg2 MyProcedure result
{
  REPLACEME begin
	/arg2 exch def /arg1 exch def
	...
  end
}
dup	% dup the procedure body
0	% the 0'th element is the REPLACEME name
2 dict	% you want a dictionary of 2 elements there
put	% put 2 dict into slot 0 of procedure body (array)
def	% /MyProcedure {...} def


As a final footnote, if you have many procedures that are always
together, there is no real need to have separate dictionaries
for each one.  You could define a single name like $mydict that's
big enough for all of them, then start each proc with //$mydict begin.

Glenn Reid
Adobe Systems

cet1@cl.cam.ac.uk (C.E. Thompson) (09/07/89)

In article <653@windy.dsir.govt.nz> SRWMCLN@windy.dsir.govt.nz (Clive Nicolson) writes:
>Your third version of the "create dictionary once" problem seems to be trying
>to "put" into a readonly array (procedure), you might known how to do this when
>the memory protection is off on one of your PostScript interpreters, but I
>dont know how to do it other than by something like:
>
>/procname [/READONLY .....] .... put cvx def

Procedures created by {...} are readonly only if there has been a
previous "true setpacking". I suppose that there ought to be code like
the following round the example:

    systemdict /setpacking known 
      {/savepacking currentpacking def  false setpacking} if
    ....
    systemdict /setpacking known 
      {savepacking setpacking} if

I think that the version using "cvx" is probably cleaner, though!

Chris Thompson (cet1@uk.ac.cam.phx)

SRWMCLN@windy.dsir.govt.nz (Clive Nicolson) (09/07/89)

Your third version of the "create dictionary once" problem seems to be trying
to "put" into a readonly array (procedure), you might known how to do this when
the memory protection is off on one of your PostScript interpreters, but I
dont know how to do it other than by something like:

/procname [/READONLY .....] .... put cvx def

debruyne@prles3.prl.philips.nl (09/07/89)

Here are some PostScript routines that can be added to the toolset:


/SelectFontDict 5 dict def
/SelectFont { % call: /font height width angle SelectFont -
  SelectFontDict begin
    /Angle exch def /Width exch def /Height exch def /Font exch def
    /AngleNum Angle sin Angle cos div 12 mul def
    Font findfont [Width 0 AngleNum Height 0 0] makefont setfont
  end
} bind def % SelectFont
% DEMO 
%   /Times-Roman 100 40 40 SelectFont
%   30 500 moveto (XYZZY) show


/ShowStrDict 9 dict def
/ShowStr { % call: x y height width angle gray /font (string) ShowStr -
  gsave
  ShowStrDict begin
    /Str exch def /Font exch def /Gray exch def /Angle exch def 
    /Width exch def /Height exch def /Y1 exch def /X1 exch def
    /AngleNum Angle sin Angle cos div 12 mul def
    Font findfont [Width 0 AngleNum Height 0 0] makefont setfont
    X1 Y1 moveto Gray setgray Str show
  end
  grestore
} bind def % ShowStr
% DEMO 
%   10 400 20 20 0  0.75 /Times-Roman 
%		(20hght 20wdth 0ang Times-Roman) ShowStr
%   10 360 20 30 20 0.25 /Times-Roman 
%		(20hght 30wdth 20ang Times-Roman) ShowStr
%   10 320 20 10 40 0.25 /Times-Roman 
%		(20hght 10wdth 40ang Times-Roman) ShowStr
%   10 280 30 20 330 0.25 /Times-Roman 
%		(30hght 20wdth 330ang Times-Roman) ShowStr


/ShowStrGrayDict 8 dict def
/ShowStrGray { % call: x y mingray maxgray string ShowStrGray -
  % show a string with increasing (decreasing) gray-levels
  gsave
  ShowStrGrayDict begin
    /Str exch def /MaxG exch def /MinG exch def /Y exch def /X exch def
    /Step MaxG MinG sub Str length 1 sub div def
    /TmpStr 2 string def
    X Y moveto
    0 1 Str length 1 sub {
      /LoopVar exch def
      MinG setgray
      TmpStr 0 Str LoopVar get put TmpStr show
      /MinG MinG Step add def
    } for
  end
  grestore
} bind def % ShowStrGray
% DEMO
%   /Times-Roman findfont [30 0 0 40 0 0] makefont setfont
%   30 400 0.0 0.9 (Abcde Fghij Klmno) ShowStrGray
%   30 300 0.9 0.0 (Abcde Fghij Klmno) ShowStrGray


/SquareEmptyDict 6 dict def
/SquareEmpty { % call: xbl ybl xtr ytr gray width SquareEmpty -
  % bl = bottom-left, tr = top-right
  gsave
  SquareEmptyDict begin
    /Width exch def /Gray exch def
    /Ytr exch def /Xtr exch def /Ybl exch def /Xbl exch def
    newpath Xbl Ybl moveto Xtr Ybl lineto Xtr Ytr lineto
    Xbl Ytr lineto Xbl Ybl lineto closepath
    Width setlinewidth Gray setgray stroke 
  end
  grestore
} bind def % SquareEmpty
% DEMO 
%   100 100 300 500 0.75 5 SquareEmpty


/SquareSolidDict 5 dict def
/SquareSolid { % call: xbl ybl xtr ytr gray SquareEmpty -
  % bl = bottom-left, tr = top-right
  gsave
  SquareSolidDict begin
    /Gray exch def /Ytr exch def /Xtr exch def /Ybl exch def /Xbl exch def
    newpath Xbl Ybl moveto Xtr Ybl lineto Xtr Ytr lineto
    Xbl Ytr lineto Xbl Ybl lineto closepath
    Gray setgray fill 
  end
  grestore
} bind def % SquareSolid
% DEMO 
%   50 400 400 600 0.25 SquareSolid


Please report any remarks and/or bugs to:
Jos de Bruijne
Philips Research Labs Eindhoven, 
Holland (The country in Europe, NOT the city in the USA!)
Email: debruyne@prles3.prl.philips.nl

debruyne@prles3.prl.philips.nl (09/08/89)

Oops, I found one bug myself in the toolset routines that I posted 
yesterday:
In the procedure ShowStrGray a string is defined as "/TmpStr 2 string def".
The "2" however should be "1" !

So, here is what should be changed:
8c8
<     /TmpStr 2 string def
---
>     /TmpStr 1 string def


Jos de Bruijne
Philips Research Labs 
Eindhoven, Holland
Email: debruyne@prles3.prl.philips.nl