When you add or change a value to the dictionary, all changes are global. This can become a problem when you use variables in operators you define.
As you see in this example, foo is first 1, then becomes 2 when you use bar which has defined foo as 2. When your code becomes more complex, it may become complicated to keep track of the variables.
There is a similar problem for transformation. An operator might change user space. How do you want to get back to the original user space.
Run
postScriptEditor(`/square { 100 100 moveto 100 200 lineto 200 200 lineto 200 100 lineto closepath } def
square 0.8 setgray fill 0 setgray
400 0 translate 30 rotate square stroke
-30 rotate -400 0 translate square stroke
showpage
`);
It is possible to get back to the original user space, if you make the inverse operations in the reverse order. However, this is difficult to track. You might also have to deal with rounding errors.
There must be a way to create locality. Actually, there is. PostScript provides a stack of dictionaries and also a stack of graphics state. You can add a new dictionary to the stack with n dict begin , remove it with end . You can add a new graphics state width grestore and remove it with gsave .
We adapt the context to host a stack of dictionaries and a stack of graphics and create a datatype dict, because dict can also be on the stack. The mechanism PostScript uses is to go down all the dictionary stack to find the value.
Run
rpnDictionary = class {
constructor(n) { this.value = {}; }
get type() { return "dictionary"; }
get dump() {
const list = [];
for (var key in this.value) {
if (key != "context") {
list.push(key + ": " + this.value[key].dump);
}
}
return " { " + list.join(", ") + " } ";
}
};
rpnContext = class {
constructor() {
this.stack = [];
this.heap = [];
this.lasterror = "";
this.currentcode = "";
this.width = 600;
this.height = 800;
this.data = new Uint8ClampedArray(this.width * this.height * 4);
this.dictstack = [];
this.dictstack.push({});
this.graphicsstack = [];
this.graphicsstack.push({ path: [], current: [], color: [0, 0, 0, 255], linewidth: 1, matrix : [1, 0, 0, 1, 0, 0] });
}
get dict() {
return this.dictstack[this.dictstack.length - 1];
}
get graphics() {
return this.graphicsstack[this.graphicsstack.length - 1];
}
error(s) {
this.lasterror = s;
this.stack.push(new rpnError(s));
return this;
}
itransform(x,y) {
const m = this.graphics.matrix;
const det = m[0]*m[3] - m[1]*m[2]
return [ (x*m[3] - y*m[2] +m[2]*m[5] - m[4]*m[3])/det,
(-x*m[1] + y*m[0] + m[4]*m[1] - m[0]*m[5])/det ]
}
pop(...parameters) {
if (this.stack.length < parameters.length) {
this.error("stackunderflow");
return [];
}
const result = [];
for (var p of parameters) {
const data = this.stack.pop();
if (p == "any") {
result.push(data);
} else if (p.includes(data.type)) {
result.push(data);
} else {
this.error("typeerror");
return [];
}
}
return result;
}
popArray() {
var found = false;
var arr = [];
while (this.stack.length && ! found) {
const value = this.stack.pop();
if (value.type == "mark") {
found = true;
} else {
arr.push(value);
}
}
if (! found) {
return this.error("stackunderflow");
}
arr.reverse();
const a = new rpnArray(arr, this);
a.reference.inc();
this.stack.push(a);
}
transform(x,y) {
const m = this.graphics.matrix;
return [ x * m[0] + y * m[2] + m[4],
x * m[1] + y * m[3] + m[5] ];
}
};
rpn = function(s, context = null ) {
if (!context) context = new rpnContext();
const list = s.concat(" ").split("");
var state = "start";
var current = "";
var depth = 0;
for (elem of list) {
if (context.lasterror) {
return context;
}
context.currentcode += elem;
switch (state) {
case "start":
if (/[0-9-]/.test(elem)) {
state = "number";
current = elem;
} else if (/[a-zA-Z]/.test(elem)) {
state = "operator";
current = elem;
} else if (elem == "/") {
state = "namestart";
} else if (elem == "{") {
state = "procedure";
} else if (elem == "(") {
state = "string";
} else if (elem == "[") {
context.stack.push(new rpnMark()); //mark
} else if (elem == "]") {
context.popArray();
} else if (elem == "%") {
state = "comment";
} else if (elem.trim() === '') {
// ignore whitespace
} else {
context.error("syntaxerror");
}
break;
case "number":
if (/[0-9]/.test(elem)) {
current += elem;
} else if (elem == ".") {
state = "fraction";
current += elem;
} else if (/[a-zA-Z-]/.test(elem)) {
context.error("syntaxerror");
} else if (elem.trim() === "") {
context.stack.push(new rpnNumber(parseFloat(current)));
state = "start";
current = "";
} else if (elem == "]") {
context.stack.push(new rpnNumber(parseFloat(current)));
state = "start";
current = "";
context.popArray();
} else {
context.error("syntaxerror");
}
break;
case "fraction":
if (/[0-9]/.test(elem)) {
current += elem;
} else if (elem.trim() === "") {
context.stack.push(new rpnNumber(parseFloat(current)));
state = "start";
current = "";
} else if (elem == "]") {
context.stack.push(new rpnNumber(parseFloat(current)));
state = "start";
current = "";
context.popArray();
} else {
context.error("syntaxerror");
}
break;
case "operator":
if (/[a-zA-Z0-9]/.test(elem)) {
current += elem;
} else if (elem.trim() !== "" && elem != "]"){
context.error("syntaxerror");
} else {
var data;
for(var i = context.dictstack.length -1 ; i >= 0; i--) {
data = context.dictstack[i][current];
if (data) {
i = 0; // break
}
}
const op = operators[current];
if (data) {
if (data.type == "procedure") {
context = rpn(data.value, context);
} else if (data.type == "array"){
data.reference.inc();
context.stack.push(data);
} else if (data.type == "string"){
data.reference.inc();
context.stack.push(data);
} else {
context.stack.push(data);
}
} else if (op) {
context = op(context);
} else {
context.error("syntaxerror");
}
state = "start";
current = "";
if (elem == "]") {
context.popArray();
}
}
break;
case "namestart":
if (/[a-zA-Z]/.test(elem)) {
state = "name";
current = elem;
} else {
context.error("syntaxerror");
}
break;
case "name":
if (/[a-zA-Z0-9]/.test(elem)) {
current += elem;
} else if (elem.trim() === "") {
context.stack.push(new rpnName(current));
state = "start";
current = "";
} else if (elem == "]") {
context.stack.push(new rpnName(current));
state = "start";
current = "";
context.popArray();
} else {
context.error("syntaxerror");
}
break;
case "procedure":
if (elem == "}") {
if (depth) {
depth--;
current += elem;
} else {
context.stack.push(new rpnProcedure(current));
state = "start";
current = "";
}
} else if (elem == "{") {
depth++;
current += elem;
} else {
current += elem;
}
break;
case "string":
if (elem == ")") {
if (depth) {
depth--;
current += elem;
} else {
const s = new rpnString(current, context);
s.reference.inc();
context.stack.push(s);
state = "start";
current = "";
}
} else if (elem == "(") {
depth++;
current += elem;
} else {
current += elem;
}
break;
case "comment":
if (elem == endofline ) {
state = "start";
}
} // switch state
} // for
if (state !== "start") {
context.error("syntaxerror");
}
return context;
};
We define the operators for dictionaries. n dict creates a new dictionary and puts it on the stack. n is the original size of the dictionary. That played a role in PostScript version 1 when memory was limited. You can give any size. begin puts the dictionary from the stack to the top of the dictionary stack. end removes it.
Run
operators.currentdict = function(context) {
const d = new rpnDictionary(1);
d.value = context.dict;
context.stack.push(d);
return context;
};
unitTest("/foo 1 def currentdict", "{ foo: 1 }")
operators.dict = function(context) {
const [n] = context.pop("number");
if (!n) return context;
context.stack.push(new rpnDictionary(n));
return context;
};
// unitTest("1 dict", "{ }")
operators.begin = function(context) {
const [d] = context.pop("dictionary");
if (!d) return context;
context.dictstack.push(d.value);
return context;
};
unitTest("1 dict begin /foo 1 def currentdict", "{ foo: 1 }")
operators.end = function(context) {
if (context.dictstack.length < 2) {
context.error("dictstackunderflow");
return context;
}
context.dictstack.pop();
return context;
};
unitTest("/foo 1 def 1 dict begin /bar 2 def end currentdict", "{ foo: 1 }")
No this works.
Run
postScriptEditor(`/foo 1 def
/bar { 1 dict begin /foo 2 def end } def
foo
bar
foo
/foo2 3 def
/bar2 { 1 dict begin /foo 2 def foo2 end } def
bar2
`);
For the graphics stack, the code is similar, with the difference that the set of graphics parameters is fixed, so that we just copy the entire graphics. Note that the stack never gets empty.
Run
objectSlice = function (ob) {
return JSON.parse(JSON.stringify(ob));
}
operators.gsave = function(context) {
context.graphicsstack.push(objectSlice(context.graphicsstack[context.graphicsstack.length - 1]));
return context;
};
operators.grestore = function(context) {
if (!context.graphicsstack.length) {
context.error("dictstackunderflow");
return context;
} else if (context.graphicsstack.length == 1) {
context.graphics = objectSlice(context.graphicsstack[0]);
} else {
context.graphics = context.graphicsstack.pop();
}
return context;
};
We can no rewrite the above code more savely
Run
postScriptEditor(`/square { 100 100 moveto 100 200 lineto 200 200 lineto 200 100 lineto closepath } def
square 0.8 setgray fill 0 setgray
gsave 400 0 translate 30 rotate square stroke grestore
square stroke
showpage
`);
The codebase is now 1633 lines ps20240802.js
My Journey to PostScript