In the last chapter Zero winding vs. even odd rule we have shown that the scanFill algorithm can be easily adapted to deal both with zero winding and even odd fill rules. So we first use the new scanfill function with a zerowind parameter.
And we rewrite the rawDevice, implementing the zerowind as true for fill and as false for the eofill method. We profit to rewrite stroke. If it uses zerowind, the transfer layer we have introduced is not necessary any more.
Run
rpnRawDevice = class {
constructor(node, urlnode) {
this.node = node;
if (!node) document.createElement("CANVAS");
this.urlnode = urlnode;
if (node) this.initgraphics(node.width, node.height, 1);
}
initgraphics(width, height, oversampling, transparent) {
this.data = new Uint8ClampedArray(width * height * 4 * oversampling * oversampling);
if (!transparent) {
for (let i = 0; i < this.data.length; i++) this.data[i] = 255;
}
this.node.width = width;
this.node.height = height;
const ctx = this.node.getContext("2d");
ctx.fillStyle = "white";
ctx.fillRect(0, 0, width, height);
}
eofill(context) {
return this.fill(context, false);
}
fill(context, zerowind = true) {
if (context.device.raw + context.device.rawurl < 1) return context;
const flatpath = [];
for (let subpath of context.graphics.path) {
for (let line of subpath) {
if (line[0] == "C") {
const lines = bezier(line,-1);
for (let elem of lines) {
flatpath.push(elem);
}
} else {
flatpath.push(line);
}
}
}
this.data = scanFill(flatpath, context.width, context.height, context.graphics.color, zerowind, this.data);
return context;
}
stroke(context) {
if (context.device.raw + context.device.rawurl < 1) return context;
const w = context.graphics.linewidth / 2;
const ad = Math.PI / 2;
if (!w) return context;
for (let subpath of context.graphics.path) {
var subflatpath = [];
for (let line of subpath) {
if (line[0] == "C") {
const lines = bezier(line,-1);
for (let elem of lines) {
subflatpath.push(elem);
}
} else {
subflatpath.push(line);
}
}
var olda = [];
var oldb = [];
const fillpath = [];
if (!subflatpath.length) continue;
if (subflatpath[0][1] == subflatpath[subflatpath.length-1][3] && subflatpath[0][2] == subflatpath[subflatpath.length-1][4])
subflatpath.push(subflatpath[0]);
for (let line of subflatpath) {
const [type, x0, y0, x1, y1] = line;
const a = Math.atan2(y1 - y0, x1 - x0);
const x0a = x0 + Math.cos(a - ad) * w;
const y0a = y0 + Math.sin(a - ad) * w;
const x1a = x1 + Math.cos(a - ad) * w;
const y1a = y1 + Math.sin(a - ad) * w;
const x0b = x0 + Math.cos(a + ad) * w;
const y0b = y0 + Math.sin(a + ad) * w;
const x1b = x1 + Math.cos(a + ad) * w;
const y1b = y1 + Math.sin(a + ad) * w;
fillpath.push(["L", x0a, y0a, x1a, y1a]);
fillpath.push(["L", x1a, y1a, x1b, y1b]);
fillpath.push(["L", x1b, y1b, x0b, y0b]);
fillpath.push(["L", x0b, y0b, x0a, y0a]);
if (olda.length) {
const [xa, ya] = lineIntersection(olda[1],olda[2],olda[3],olda[4],x0a, y0a, x1a, y1a);
const [xb, yb] = lineIntersection(oldb[1],oldb[2],oldb[3],oldb[4],x0b, y0b, x1b, y1b);
if (xa !== null && xb !== null) {
fillpath.push(["L", olda[3],olda[4], xa, ya]);
fillpath.push(["L", xa, ya, x0a, y0a]);
fillpath.push(["L", x0a, y0a, x0b, y0b]);
fillpath.push(["L", x0b, y0b, xb, yb]);
fillpath.push(["L", xb, yb, oldb[3], oldb[4]]);
fillpath.push(["L", oldb[3], oldb[4], olda[3], olda[4]]);
}
}
olda = ["L", x0a, y0a, x1a, y1a];
oldb = ["L", x0b, y0b, x1b, y1b];
}
this.data = scanFill(fillpath, context.width, context.height, context.graphics.color, true, this.data);
}
return context;
}
showpage(context) {
if (context.device.raw + context.device.rawurl < 1) {
this.node.style.display = "none";
if (this.urlnode) this.urlnode.style.display = "none";
return context;
}
this.node.style.display = (context.device.raw) ? "block" : "none";
const image = new ImageData(this.data, context.width * context.device.oversampling);
const canvas = (this.node) ? this.node : document.createElement("CANVAS");
const ctx = canvas.getContext("2d");
if (context.device.oversampling > 1) {
var nodebig = document.createElement("CANVAS");
nodebig.width = context.width * context.device.oversampling;
nodebig.height = context.height * context.device.oversampling;
nodebig.getContext("2d").putImageData(image, 0,0);
ctx.save();
ctx.scale(1/context.device.oversampling, 1/context.device.oversampling);
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = "high";
ctx.drawImage(nodebig,0,0);
ctx.restore();
} else {
ctx.putImageData(image, 0,0);
}
if (this.urlnode && context.device.rawurl) {
this.urlnode.style.display = "block";
const url = canvas.toDataURL();
this.urlnode.href = url;
this.urlnode.setAttribute("download", "PS.png");
} else {
if (this.urlnode) this.urlnode.style.display = "none";
}
return context;
}
};
We will have to extend all devices with eofill.
Run
rpnCanvasDevice = class {
constructor(node, urlnode) {
this.node = node;
if (!node) document.createElement("CANVAS");
this.urlnode = urlnode;
this.initgraphics(this.node.width, this.node.height, 1);
}
initgraphics(width, height, oversampling, transparent) {
this.node.width = width;
this.node.height = height;
const ctx = this.node.getContext("2d");
if (!transparent) {
ctx.fillStyle = "white";
ctx.fillRect(0, 0, width, height);
}
}
eofill(context) {
return this.fill(context, false);
}
fill(context, zerowind = true) {
if (context.device.canvas + context.device.canvasurl < 1) return context;
if (context.device.canvas) this.node.style.display = "block";
const ctx = this.node.getContext("2d");
ctx.beginPath();
for (let subpath of context.graphics.path) {
if (!subpath.length) continue;
ctx.moveTo(subpath[0][1], context.height - subpath[0][2]);
for (let line of subpath) {
if (line[0] == "C") {
ctx.bezierCurveTo(line[3], context.height - line[4], line[5], context.height - line[6], line[7], context.height - line[8]);
} else {
ctx.lineTo(line[3], context.height - line[4]);
}
}
}
ctx.closePath();
ctx.fillStyle = "rgb("+Math.round(context.graphics.color[0])+" "+Math.round(context.graphics.color[1])+" "+ Math.round(context.graphics.color[2])+")";
ctx.globalAlpha = context.graphics.color[3]/255.0;
if (zerowind) {
ctx.fill("nonzero");
} else {
ctx.fill("evenodd");
}
return context;
}
stroke(context) {
if (context.device.canvas + context.device.canvasurl < 1) return context;
const ctx = this.node.getContext("2d");
ctx.beginPath();
for (let subpath of context.graphics.path) {
if (!subpath.length) continue;
ctx.moveTo(subpath[0][1], context.height - subpath[0][2]);
for (let line of subpath) {
if (line[0] == "C") {
ctx.bezierCurveTo(line[3], context.height - line[4], line[5], context.height - line[6], line[7], context.height - line[8]);
} else {
ctx.lineTo(line[3], context.height - line[4]);
if (line[0] == "Z") ctx.closePath();
}
}
}
ctx.strokeStyle = "rgb("+Math.round(context.graphics.color[0])+" "+Math.round(context.graphics.color[1])+" "+ Math.round(context.graphics.color[2])+")";
ctx.globalAlpha = context.graphics.color[3]/255.0;
ctx.lineWidth = context.graphics.linewidth;
ctx.stroke();
return context;
}
showpage(context) {
if (context.device.canvas + context.device.canvasurl < 1) {
this.node.style.display = "none";
if (this.urlnode) this.urlnode.style.display = "none";
return context;
}
this.node.style.display = (context.device.canvas) ? "block" : "none";
if (context.device.canvasurl) {
this.urlnode.style.display = "block";
const url = this.node.toDataURL();
this.urlnode.href = url;
this.urlnode.setAttribute("download", "PS.png");
} else {
if (this.urlnode) this.urlnode.style.display = "none";
}
return context;
}
};
rpnPDFDevice = class {
constructor(node, urlnode) {
this.node = node;
if (!this.node) this.node = document.createElement("IMG");
this.urlnode = urlnode;
this.initgraphics(this.node.width, this.node.height, 1);
this.canshow = true;
this.catalog = {};
this.pages = {};
this.elements = [];
this.usedfonts = {};
this.currentfont = {};
}
initgraphics(width, height, oversampling, transparent) {
if (Number.isFinite(width)) this.width = width;
if (Number.isFinite(height)) this.height = height;
this.node.style.backgroundColor = (transparent) ? "transparent" : "white";
}
numberFormat(z) {
return Intl.NumberFormat('en-IN', { minimumFractionDigits: 3, maximumFractionDigits: 3 }).format(z);
}
asciiHexEncode(buffer) {
const data = new Array(buffer.length);
for (let i=0; i < buffer.length; i++) {
data[i] = ("0" + (buffer.codePointAt(i).toString(16))).substr(0,2);
}
return data.join(" ")+endtag;
}
objectDict(obj) {
const elements = [];
for (let k in obj) {
const v = obj[k];
if (typeof v == "object") {
elements.push("/" + k + " " + this.objectDict(v));
} else {
elements.push("/" + k + " " + v);
}
}
return "<< " + elements.join(" ") + " >> ";
}
getPath(context, close = true) {
const p = [];
for (let subpath of context.graphics.path) {
if (!subpath.length) continue;
p.push(this.numberFormat(subpath[0][1]) + " " + this.numberFormat(subpath[0][2]) + " m");
for (let line of subpath) {
if (line[0] == "C") {
p.push(this.numberFormat(line[3]) + " " + this.numberFormat(line[4]) + " " + this.numberFormat(line[5]) + " " + this.numberFormat(line[6]) + " " + this.numberFormat(line[7]) + " " + line[8] + " c");
} else {
p.push(this.numberFormat(line[3]) + " " + this.numberFormat(line[4]) + " l");
if (line[0] == "Z") p.push("h");
}
}
}
if (close) p.push("h");
return p.join(" ");
}
eofill (context) {
return this.fill(context, false);
}
fill(context, zerowind = true) {
if (context.device.pdf + context.device.pdfurl < 1) return context;
if (context.device.textmode * context.showmode) return context;
this.elements.push(this.getPath(context));
this.setcolor(context);
this.setalpha(context);
if (zerowind) {
this.elements.push("f");
} else {
this.elements.push("f*");
}
return context;
}
setcolor(context) {
const rs = this.numberFormat(context.graphics.color[0]/255);
const gs = this.numberFormat(context.graphics.color[1]/255);
const bs = this.numberFormat(context.graphics.color[2]/255);
this.elements.push(rs+' '+gs+' '+bs+' rg');
this.elements.push(rs+' '+gs+' '+bs+' RG');
}
setalpha(context)
{
const gs = Math.round(context.graphics.color[3]/16)
this.elements.push("/GS"+gs+" gs");
}
setfont(context) {
const f = {};
f.Type = "/Font";
f.Subtype = "/Type1";
f.Encoding = "/MacRomanEncoding";
f.BaseFont = "/" + context.graphics.font;
const key = f.BaseFont;
var k;
if (this.usedfonts.hasOwnProperty(key)) {
k = this.usedfonts[key].key;
} else {
k = Object.keys(this.usedfonts).length + 1;
f.key = k;
this.usedfonts[key] = f;
}
if (f != this.currentfont) {
this.currentfont = f;
this.elements.push("BT /F" + k + " " + this.numberFormat(context.graphics.size) + " Tf ET");
}
}
show(s, context, targetwidth = 0, extraspace = 0) {
if (context.device.pdf + context.device.pdfurl < 1) return context;
this.setfont(context);
this.setcolor(context);
this.setalpha(context);
const matrix = context.graphics.matrix.slice();
const decomposed = decompose_2d_matrix(matrix);
const s2 = macRomanEncoding(s);
if (decomposed.rotation) {
const tmatrix = [ 1, 0, 0, 1, this.numberFormat(context.graphics.current[0]), this.numberFormat (context.graphics.current[1])];
const rmatrix = [ this.numberFormat (Math.cos(decomposed.rotation)), this.numberFormat (Math.sin(decomposed.rotation)), this.numberFormat(-Math.sin(decomposed.rotation)), this.numberFormat(Math.cos(decomposed.rotation)), 0, 0];
this.elements.push("q " + tmatrix.join(" ") + " cm " + rmatrix.join(" ") + " cm");
this.elements.push("BT 0 0 Td " + extraspace + " Tw (" + s2 + ") Tj ET");
this.elements.push ("Q");
} else {
this.elements.push("BT " + this.numberFormat(context.graphics.current[0]) + " " + this.numberFormat(context.graphics.current[1]) + " Td " + extraspace + " Tw (" + s2 + ") Tj ET");
}
return context;
}
showpage(context) {
if (context.device.pdf + context.device.pdfurl < 1) return context;
const objects = [];
this.catalog.Type = "/Catalog";
this.catalog.Pages = "2 0 R";
objects.push([this.catalog]);
this.pages.Type = "/Pages";
this.pages.Kids = "[ 3 0 R ]";
this.pages.Count = "1";
this.pages.MediaBox = "[0 0 " + this.width + " " + this.height + "]";
objects.push([this.pages]);
const dict = {};
dict.Type = "/Page";
dict.Parent = "2 0 R";
const ressourcesdict = {};
const fontdict= {};
for (let k in this.usedfonts) {
const f = this.usedfonts[k];
const substitute = fontSubstitution(f.BaseFont);
f.BaseFont = substitute;
fontdict["F" + f.key] = f;
}
ressourcesdict.Font = fontdict;
const alphadict = {};
ressourcesdict.ExtGState = alphadict;
dict.Resources = ressourcesdict;
dict.Contents = "4 0 R";
objects.push([dict]);
const streamdict = {};
const stream = this.elements.join(endofline);
streamdict.Length = stream.length;
objects.push([streamdict, stream]);
for (let i = 0; i < 17; i++ ) {
alphadict["GS"+i] = (5+i) + " 0 R";
const ad = {};
ad.type = "/ExtGState";
ad.ca = i / 16.0;
ad.CA = i / 16.0;
objects.push([ad])
}
const xrefoffset = [];
var file = "%PDF-1.1" + endofline; // signature
file += "%¥±ë rpn" + endofline; // random binary characters
for (let k in objects) {
const o = objects[k];
xrefoffset.push(file.length);
file += xrefoffset.length + " 0 obj" + endofline;
file += this.objectDict(o[0]) + endofline;
if (o.length == 2) {
file += "stream" + endofline;
file += o[1] + endofline;
file += "endstream" + endofline;
}
file += "endobj" + endofline + endofline;
}
const startxref = file.length;
file += "xref" + endofline;
file += "0 6" + endofline;
file += "0000000000 65535 f" + endofline;
for (let i in xrefoffset) {
const x = new Intl.NumberFormat('en-IN', { minimumIntegerDigits: 10 , useGrouping: false}).format(xrefoffset[i]);
file += x + ' 00000 n ' + endofline;
}
// trailer
const trailderdict = {};
trailderdict.Root = "1 0 R";
trailderdict.Size = 5;
file += "trailer " + this.objectDict(trailderdict) + endofline;
file += 'startxref'+ endofline;
file += startxref + endofline;
file +='%%EOF' + endofline;
const url = "data:application/pdf;base64," + btoa(file);
this.urlnode.style.display = (context.device.pdfurl) ? "block" : "none";
this.node.style.display = (context.device.pdf) ? "block" : "none";
if (context.device.pdfurl) {
this.urlnode.href = url;
this.urlnode.setAttribute("download", "PS.pdf");
}
if (context.device.pdf) {
this.node.src = url;
this.node.style.display = "block";
this.node.width = this.width;
this.node.height = this.height;
}
return context;
}
stroke(context) {
if (context.device.pdf + context.device.pdfurl < 1) return context;
this.elements.push(this.getPath(context, false));
this.setcolor(context);
this.setalpha(context);
const ws = this.numberFormat(context.graphics.linewidth);
this.elements.push(ws+' w');
this.elements.push("S");
return context;
}
};
rpnSVGDevice = class {
constructor(node, urlnode) {
this.node = node;
this.canshow = true;
this.fonts = {};
if (!node) this.node = document.createElement("SVG");
this.node.setAttribute("xmlns","http://www.w3.org/2000/svg");
this.urlnode = urlnode;
this.initgraphics( this.node.width, this.node.height, 1);
}
initgraphics(width, height, oversampling, transparent) {
if (Number.isFinite(width)) this.node.setAttribute("width",width+"px");
if (Number.isFinite(height))
this.node.setAttribute("height",height+"px");
if (Number.isFinite(width) && Number.isFinite(height))
this.node.setAttribute("viewbox", "0 0 " + width+" " + height);
this.node.style.backgroundColor = (transparent) ? "transparent" : "white";
this.node.style.display = (context.device.svg) ? "block" : "none";
this.node.innerHTML = "";
}
getPath(context, close = true) {
const p = [];
for (let subpath of context.graphics.path) {
if (!subpath.length) continue;
p.push("M " + (subpath[0][1]) + " " + (context.height - subpath[0][2]));
for (let line of subpath) {
if (line[0] == "C") {
p.push("C " +(line[3]) + " " + (context.height - line[4]) + " " + (line[5]) + " " + (context.height - line[6]) + " " +(line[7]) + " " + (context.height - line[8]));
} else {
p.push("L "+(line[3]) + " " + (context.height - line[4]));
if (line[0] == "Z") p.push("Z");
}
}
}
if (close) p.push("Z");
return p.join(" ");
}
eofill (context) {
return this.fill(context, false);
}
fill(context, zerowind = true) {
if (context.device.svg + context.device.svgurl < 1) return context;
if (context.device.textmode * context.showmode) return context;
if (context.device.svg) this.node.style.display = "block";
const node = document.createElement("PATH");
node.setAttribute("id","fill"+this.node.childElementCount);
node.setAttribute("d", this.getPath(context));
node.setAttribute("stroke","none");
node.setAttribute("fill", "rgb(" + Math.round(context.graphics.color[0]) + ", " + Math.round(context.graphics.color[1]) + ", " + Math.round(context.graphics.color[2]) + ")");
node.setAttribute("fill-opacity", context.graphics.color[3]/255.0);
if (zerowind) {
node.setAttribute("fill-rule", "nonzero");
} else {
node.setAttribute("fill-rule", "evenodd");
}
this.node.appendChild(node);
return context;
}
stroke(context) {
if (context.device.svg + context.device.svgurl < 1) return context;
if (context.device.svg) this.node.style.display = "block";
const node = document.createElement("PATH");
node.setAttribute("id","stroke" + this.node.childElementCount);
node.setAttribute("d", this.getPath(context, false));
node.setAttribute("fill","none");
node.setAttribute("stroke-width",context.graphics.linewidth);
node.setAttribute("stroke", "rgb(" + Math.round(context.graphics.color[0]) + ", " + Math.round(context.graphics.color[1]) + ", " + Math.round(context.graphics.color[2]) + ")");
node.setAttribute("stroke-opacity", context.graphics.color[3]/255.0);
this.node.appendChild(node);
return context;
}
show(s, context, targetwidth = 0) {
if (context.device.svg + context.device.svgurl < 1) return context;
if (context.device.svg) this.node.style.display = "block";
if (!this.fonts.hasOwnProperty(context.graphics.font)) {
this.fonts[context.graphics.font] = context.graphics.font;
}
const node = document.createElement("TEXT");
node.setAttribute("x", "0");
node.setAttribute("y", "0");
node.setAttribute("font-family", context.graphics.font);
node.setAttribute("font-size", context.graphics.size);
node.setAttribute("fill", "rgb(" + Math.round(context.graphics.color[0]) + ", " + Math.round(context.graphics.color[1]) + ", " + Math.round(context.graphics.color[2]) + ")" );
node.setAttribute("fill-opacity", context.graphics.color[3]/255.0);
const matrix = context.graphics.matrix.slice();
const decomposed = decompose_2d_matrix(matrix);
const x = context.graphics.current[0];
const y = context.height - context.graphics.current[1];
node.setAttribute("text-anchor","start");
node.setAttribute("transform", "translate(" + x + " " + y +") rotate(" + -decomposed.rotation*180/Math.PI + ")" );
if (targetwidth) {
node.setAttribute("textLength", targetwidth);
node.setAttribute("lengthAdjust", "spacing");
}
node.innerHTML = htmlspecialchars(s); // clean XML
this.node.appendChild(node);
return context;
}
showpage(context) {
if (context.device.svg + context.device.svgurl < 1) {
this.node.style.display = "none";
if (this.urlnode) this.urlnode.style.display = "none";
return context;
}
const node = document.createElement("DEFS");
this.node.insertBefore(node, this.node.firstChild);
for (const font in this.fonts) {
const style = document.createElement("STYLE");
const src = readSyncDataURL(fontbasepath + font + ".ttf", "font/ttf");
style.innerHTML = "@font-face { font-family: '" + font + "'; font-weight: normal; src: url('" + src + "') format('truetype')} }";
node.appendChild(style);
}
this.node.outerHTML = this.node.outerHTML; // force refresh
if (context.device.svgurl) {
const file = starttag + "?xml version='1.0' encoding='UTF-8'?" + endtag + this.node.outerHTML;
const url = "data:image/svg+xml;base64," + btoa(file);
this.urlnode.href = url;
this.urlnode.setAttribute("download", "PS.svg");
}
this.node.style.display = (context.device.svg) ? "block" : "none";
if (this.urlnode)
this.urlnode.style.display = (context.device.svgurl) ? "block" : "none";
return context;
}
};
And we add an eofill operator. We fix also the closepath operator. A last line segment is now of type "Z" instead of "L" so that the devices can properly close the stroke path.
Run
operators.eofill = function(context) {
for (let n of context.nodes) context = n.eofill(context);
context.graphics.path = [];
context.graphics.current = [];
return context;
};
operators.closepath = function(context) {
if (!context.graphics.current.length) {
context.stack.push(new rpnError("nocurrentpoint"));
} else if (!context.graphics.path.length) {
context.stack.push(new rpnError("nocurrentpath"));
} else {
if (context.graphics.path.length) {
const subpath = context.graphics.path.pop();
const x0 = subpath[0][1];
const y0 = subpath[0][2];
subpath.push( [ "Z",context.graphics.current[0], context.graphics.current[1], x0, y0 ]);
context.graphics.path.push(subpath);
context.graphics.current = [ x0, y0 ];
}
}
return context;
};
Test fill
Run
postScriptEditor(`
10 dict begin /canvas 1 def /svg 1 def /pdfurl 1 def currentdict setpagedevice end
20 100 moveto 100 300 lineto 180 100 lineto 0 230 lineto 200 230 lineto closepath fill
200 100 moveto 200 200 lineto 300 200 lineto 300 100 lineto closepath
250 50 moveto 250 150 lineto 350 150 lineto 350 50 lineto closepath fill
400 100 moveto 400 200 lineto 500 200 lineto 500 100 lineto closepath
450 50 moveto 550 50 lineto 550 150 lineto 450 150 lineto closepath fill
showpage
`);
Test eofill
Run
postScriptEditor(`
10 dict begin /canvas 1 def /svg 1 def /pdfurl 1 def currentdict setpagedevice end
20 100 moveto 100 300 lineto 180 100 lineto 0 230 lineto 200 230 lineto closepath eofill
200 100 moveto 200 200 lineto 300 200 lineto 300 100 lineto closepath
250 50 moveto 250 150 lineto 350 150 lineto 350 50 lineto closepath eofill
400 100 moveto 400 200 lineto 500 200 lineto 500 100 lineto closepath
450 50 moveto 550 50 lineto 550 150 lineto 450 150 lineto closepath eofill
showpage
`);
Test stroke
Run
postScriptEditor(`
10 dict begin /transparent 1 def /canvas 1 def /svg 1 def /pdfurl 1 def currentdict setpagedevice end
0.2 0.2 0.9 setrgbcolor
0.5 setalpha
20 setlinewidth
100 100 moveto 200 100 lineto 200 200 lineto 100 200 lineto closepath stroke
250 100 moveto 350 100 lineto 300 200 lineto closepath stroke
450 100 moveto
480 100 500 120 500 150 curveto
500 180 480 200 450 200 curveto
420 200 400 180 400 150 curveto
400 120 420 100 450 100 curveto stroke
showpage
`);
ps20241117.js 3525 lines 130 KB
My Journey to PostScript