(v13) Tips for writing PostScript-language code emitted by a filter
This page applies to Harlequin v13.1r0 and later; both Harlequin Core and Harlequin MultiRIP.
When writing your own filter for some other language or file format, it is our experience that it is much easier to maintain the state of the page being prepared in the PostScript-language VM rather than in the filter implementation’s memory, and to keep the filter C code as simple as possible.
For example, consider a fictitious language that models the way lines are drawn with the concept of a “pen” selected from a carousel of four pens. This model perhaps reflects the origins of the language in a plotter for which the language was originally developed, which did indeed have such a carousel. Different pens would be represented in PostScript language by altering various settings in the graphics state, such as line width and color.
The language might contain a cryptic “select next pen” command: for example, “<ESC>PN~
”, which conceptually rotates the carousel by one position and picks the pen up from it. It is easier to remember which pen is in use and what the characteristics of the pens are in PostScript dictionary entries, rather than in data structures in the filter itself.
Then the easy translation of “<ESC>PN~
” is a simple bit of PostScript language such as SelectNextPen
(with suitable delimiting white space). The definition of SelectNextPen
could consult the “current pen number” stored in some dictionary, increment it, look up the definition of the new pen in some other dictionary, and set the appropriate graphics state properties from it.
Another example of keeping state out of the filter is deciding when to execute a showpage
operator. Many simple printer languages require a line of ASCII text to be printed and then the position moved down one line. When the end of the page is reached the printer throws to the next page automatically.
This requires a showpage
in PostScript language terms. The position of the “next line” may be influenced by the size of font and other factors. The technique in this case would be to emit each line of text as a string (suitably escaped for necessary characters) with a procedure name following, for example:
(some text \(which was in the original input\)) ShowLine
That procedure would then keep track of the position on the page and execute showpage
when necessary, as well as displaying the characters in the appropriate size and font.
The definitions of SelectNextPen
and ShowLine
could be emitted by the filter near the beginning. However, this means that the filter implementation contains a large body of constant strings which is simply copied to the filter’s output buffer on the first read from the filter; this requires added complexity in the state machine implementing the filter to cope with the case when all that text does not fit in the buffer). This can be simplified significantly by wrapping up the definitions in a separately edited procset resource. If this is called PenLanguage
, then all the constant filter has to emit at the beginning is something like this:
/PenLanguage /ProcSet findresource begin
to access the resource. A procset resource is a read-only dictionary. Therefore, the above is not sufficient if the filter requires to emit def
operators or similar, or if any of the procedures in the procset need to def
. There are many ways to handle this: one is to copy the procedures from the procset to another dictionary that is used to store the other values emitted by the filter, as in:
100 dict begin /PenLanguage /ProcSet findresource begin { def } forall
Another is to put a second dictionary onto the dictionary stack:
/PenLanguage /ProcSet findresource begin 100 dict begin
Where the procedures in the procset maintain state themselves, as is suggested here, it is most elegant to hide their internal data in a dictionary or dictionaries created within the procset for the purpose. Here is an abbreviated version of a procset for this example. Some notes on the implementation follow:
%!PS-Adobe-3.0
%%Title: (PenLanguage)
%%Creator: Harlequin
%%CreationDate: ...
%%For: ...
%%EndComments
%%BeginResource: procset PenLanguage currentglobal false setglobal
10 dict begin
/globalness exch def
/max-pen 4 def
/workspace <<
/current-pen 1
>> def
/pens << 1 [
1.0 % linewidth (points)
0.0 0.0 0.0 % rgb color (a black pen)
]
2 [ 1.0 1.0 0.0 0.0 ] % a red pen
3 [ 5.0 0.0 0.0 0.0 ] % a thick black pen
4 [ 1.0 1.0 1.0 1.0 ] % an "erasing" pen
>> def
...
/SelectNextPen {
//workspace begin
% increment the current pen number
/current-pen current-pen 1 add
% wrap round to the first pen if necessary dup //max-pen gt { pop 1 } if def
% set the graphics state according to new current pen
//pens current-pen get aload pop setrgbcolor setlinewidth end
} bind def
/ShowLine {
//workspace begin
...
end
} bind def
...
[ /workspace /pens /max-pen ]
{ currentdict exch undef } forall currentdict
end
/PenLanguage exch /ProcSet defineresource % returns read-only copy
/globalness get setglobal
%%EndResource
The comments at the top and tail of the file are included so that where necessary comment parsing can recognize the file (see (v13) Comment parsing).
In general, it is impossible to predict whether memory is global or local when we execute the procset definition. Therefore, we take care to ensure it is local while we define the procset and reset afterwards. This is the purpose of globalness
.
workspace
, max-pen
, and pens
are only used by the procedures of the procset. They are removed by an undef
at the end: note the technique of using forall
on an array, which means that changes are easily accommodated. The use of //workspace
embeds a reference to the dictionary inside the procedures SelectNextPen
and ShowLine
so the name is only used for convenience during the definition.
In this simple example, the pens are defined in the dictionary pens
. In use, it may be more appropriate to define them along with the filter, as filter parameters. Of course, it is also possible that the language being interpreted contains “define pen” commands.