Clipping is limiting fill, stroke a show to a region you have defined with a path. When you start a page, you can fill, stroke and show to the entire page. When you use the operator clip with a current path, this path is becoming the clipping path. If you use clip operator again, the intersection of the current clipping path becomes the new clipping path.
The clipping path is part of the current graphics state. So we have to add the clipping first to the context
The operator clip keeps an arrow of paths in the graphics state so it can always provide the current clipping paths.
Run
operators.clip = function(context) {
context.graphics.clip.push(context.graphics.path.slice());
return context;
};
The scanFill algorithm is extended to use a clip region
Run
scanFill = function (path, width, height, color, zerowind, data, clipdata) {
const crossings = [];
const oversampling = Math.sqrt(data.length / width / height / 4);
const width2 = width * oversampling;
const height2 = height * oversampling;
for (let line of path) {
var [type, x0, y0, x1, y1] = line;
x0 *= oversampling;
y0 *= oversampling;
x1 *= oversampling;
y1 *= oversampling;
const dx = x1 - x0;
const dy = y1 - y0;
const up = (dy > 0) ? 0.5 : 0.0;
if (dy) {
if (y1 < y0) {
[y0, y1] = [y1, y0];
[x0, x1] = [x1, x0];
}
const ex = dx / dy;
const y0c = Math.ceil(y0);
x = (x0 + ex * (y0c - y0));
for (let y = y0c; y < y1; y++) {
if (y >= 0 && y < height2) {
if (! crossings[y] ) crossings[y] = [];
crossings[y].push(Math.floor(x) + up);
}
x += ex;
}
}
}
for (let y in crossings) {
const arr = crossings[y];
arr.sort(function (a, b) { return a - b; });
var odd = 0;
for (let i = 0; i < arr.length - 1; i++) {
if (zerowind) {
let up = 2 * (arr[i] - Math.floor(arr[i]));
odd += up ? 1 : -1;
} else {
odd = 1 - odd;
}
if (odd) {
var xstart = Math.floor(arr[i]);
var xend = Math.floor(arr[i + 1]);
var offset = ((height2 - 1 - y) * width2 + xstart) * 4;
for (let j = xstart; j < xend; j++) {
// we must check bounds 0 <= j < width2
if (j >= 0 && j < width2 && clipdata[offset] ) {
// Da = C1a + C2a * (1 - C1a)
const da = color[3]/255.0 + data[offset + 3]/255.0 * (1 - color[3]/255.0);
for (let c = 0; c < 3; c++) {
// D = C1 * C1a + C2 * C2a * (1 - C1a)
data[offset + c] = color[c] * color[3]/255.0 + data[offset + c] * data[offset + 3] / 255.0 * (1 - color[3]/255.0) ;
if (da) data[offset + c] /= da;
}
data[offset + 3] = 255.0 * da;
}
offset += 4;
}
}
}
}
return data;
};
On the raw device, we keep a clipping. Whenever we fill or stroke, we check if the clipping path has changed and if yes, we calculate a transfer layer for the clipping.
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.clipdata = new Uint8ClampedArray(width * height * 4 * oversampling * oversampling);
this.clippath = "";
for (let i = 0; i < this.clipdata.length; i++) this.clipdata[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);
}
getFlatPath(path) {
const flatpath = [];
for (let subpath of 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);
}
}
}
return flatpath;
}
clip(context) {
const test = JSON.stringify(context.graphics.clip);
if (this.clippath == test) return context;
this.clipdata = new Uint8ClampedArray(this.data.length);
for (let i = 0; i < this.clipdata.length; i++) this.clipdata[i] = 255;
for (let clip of context.graphics.clip) {
const flatpath = this.getFlatPath(clip);
const newclip = new Uint8ClampedArray(this.data.length);
this.clipdata = scanFill(flatpath, context.width, context.height, [255,255,255,255], true, newclip, this.clipdata);
}
this.clippath = test;
return context;
}
eofill(context) {
return this.fill(context, false);
}
fill(context, zerowind = true) {
if (context.device.raw + context.device.rawurl < 1) return context;
const flatpath = this.getFlatPath(context.graphics.path);
context = this.clip(context);
this.data = scanFill(flatpath, context.width, context.height, context.graphics.color, zerowind, this.data, this.clipdata);
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) {
const subflatpath = this.getFlatPath([subpath]);
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];
}
context = this.clip(context);
this.data = scanFill(fillpath, context.width, context.height, context.graphics.color, true, this.data, this.clipdata);
}
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;
}
};
And add it for the other devices. In Canvas and PDF we wrap the clipping in gsave - restore. We need to do this for every stroke, fill and show operation, which may be expensive depending how it is implemented. We do not know. In SVG, the paths are separate and referenced.
Run
rpnCanvasDevice = class {
constructor(node, urlnode) {
this.node = node;
if (!node) document.createElement("CANVAS");
this.urlnode = urlnode;
this.clippath = "";
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);
}
}
applyPath(path) {
const ctx = this.node.getContext("2d");
ctx.beginPath();
for (let subpath of path) {
if (!subpath.length) continue;
ctx.moveTo(subpath[0][1], this.node.height - subpath[0][2]);
for (let line of subpath) {
if (line[0] == "C") {
ctx.bezierCurveTo(line[3], this.node.height - line[4], line[5], this.node.height - line[6], line[7], this.node.height - line[8]);
} else {
ctx.lineTo(line[3], this.node.height - line[4]);
if (line[0] == "Z") ctx.closePath();
}
}
}
}
clip(context) {
const ctx = this.node.getContext("2d");
for (let clip of context.graphics.clip) {
this.applyPath(clip);
ctx.clip();
}
return context;
}
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.save();
context = this.clip(context);
this.applyPath(context.graphics.path);
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");
}
ctx.restore();
return context;
}
stroke(context) {
if (context.device.canvas + context.device.canvasurl < 1) return context;
const ctx = this.node.getContext("2d");
ctx.save();
context = this.clip(context);
this.applyPath(context.graphics.path);
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();
ctx.restore();
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(path, close = true) {
const p = [];
for (let subpath of 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(" ");
}
clip(context) {
this.elements.push("q");
for (let clip of context.graphics.clip) {
this.elements.push(this.getPath(clip));
this.elements.push("W n");
}
return context;
}
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;
if (context.graphics.clip.length) context = this.clip(context);
this.elements.push(this.getPath(context.graphics.path));
this.setcolor(context);
this.setalpha(context);
if (zerowind) {
this.elements.push("f");
} else {
this.elements.push("f*");
}
if (context.graphics.clip.length) this.elements.push("Q");
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;
if (context.graphics.clip.length) context = this.clip(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");
}
if (context.graphics.clip.length) this.elements.push("Q");
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;
if (context.graphics.clip.length) context = this.clip(context);
this.elements.push(this.getPath(context.graphics.path, false));
this.setcolor(context);
this.setalpha(context);
const ws = this.numberFormat(context.graphics.linewidth);
this.elements.push(ws+' w');
this.elements.push("S");
if (context.graphics.clip.length) this.elements.push("Q");
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( 590, 330, 1);
}
initgraphics(width, height, oversampling, transparent) {
if (Number.isFinite(width)) {
this.node.setAttribute("width",width+"px");
this.width = width;
}
if (Number.isFinite(height)) {
this.node.setAttribute("height",height+"px");
this.height = height;
}
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.clippath = "";
this.clippathsource = "";
this.node.innerHTML = "";
}
getPath(path, close = true) {
const p = [];
if (!path.length) return "";
for (let subpath of path) {
if (!subpath.length) continue;
p.push("M " + (subpath[0][1]) + " " + (this.height - subpath[0][2]));
for (let line of subpath) {
if (line[0] == "C") {
p.push("C " +(line[3]) + " " + (this.height - line[4]) + " " + (line[5]) + " " + (this.height - line[6]) + " " +(line[7]) + " " + (this.height - line[8]));
} else {
p.push("L "+(line[3]) + " " + (this.height - line[4]));
if (line[0] == "Z") p.push("Z");
}
}
}
if (close) p.push("Z");
return p.join(" ");
}
clip(context) {
const test = JSON.stringify(context.graphics.clip);
if (this.clippathsource == test) return context;
this.clippath = "";
this.clippathsource == test;
for (let clip of context.graphics.clip) {
const node = document.createElement("CLIPPATH");
if (this.clippath) node.setAttribute("clip-path", "url(#"+this.clippath+")");
this.clippath = "clippath"+this.node.childElementCount;
node.setAttribute("id", this.clippath);
const node2 = document.createElement("PATH");
node2.setAttribute("d", this.getPath(clip));
node.appendChild(node2);
this.node.appendChild(node);
}
return context;
}
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";
context = this.clip(context);
const node = document.createElement("PATH");
node.setAttribute("id","fill"+this.node.childElementCount);
node.setAttribute("d", this.getPath(context.graphics.path));
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 (this.clippath) node.setAttribute("clip-path", "url(#"+this.clippath+")");
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";
context = this.clip(context);
const node = document.createElement("PATH");
node.setAttribute("id","stroke" + this.node.childElementCount);
node.setAttribute("d", this.getPath(context.graphics.path, 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);
if (this.clippath) node.setAttribute("clip-path", "url(#"+this.clippath+")");
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;
}
context = this.clip(context);
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);
if (this.clippath) node.setAttribute("clip-path", "url(#"+this.clippath+")");
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;
}
};
We test with the exercice. We add a text at the end to make sure that it is not clipped.
Run
postScriptEditor(`
10 dict begin /canvas 1 def /svg 1 def /pdfurl 1 def currentdict setpagedevice end
/inch { 72 mul } def
/screenbox {
/d exch def
newpath
.5 inch 0 moveto
d 0 d d 0.25 inch arcto 4 { pop } repeat
d d 0 d 0.25 inch arcto 4 { pop } repeat
0 d 0 0 0.25 inch arcto 4 { pop } repeat
0 0 d 0 0.25 inch arcto 4 { pop } repeat
closepath
} def
/isotriangle {
/b exch def
/h exch def
/hb b 2 div def
newpath
0 0 moveto
hb h rlineto
hb h neg rlineto
closepath
} def
/horizontallines {
/w exch def
/h exch def
newpath
0 10 h { 0 exch moveto w 0 rlineto } for
stroke
} def
gsave
3 inch screenbox
stroke
5 inch 4 inch isotriangle
gsave stroke grestore
clip
3 inch screenbox
clip
4 inch 6 inch horizontallines
grestore
/CMUSerif-Bold findfont 96 scalefont setfont
50 50 moveto (Hello World) show
showpage
`);
A vector drawing inside letters. This one is slower.
Run
postScriptEditor(`
10 dict begin /height 768 def /canvas 1 def /svg 1 def /pdfurl 1 def currentdict setpagedevice end
/inch { 72 mul } def
/rays {
0 5 180 { gsave rotate 0 0 moveto 5 inch 0 rlineto stroke grestore } for
} def
% x y sunrays
/sunrays {
translate newpath
.5 setlinewidth
rays
0 0 25 0 180 arc gsave 1 setgray fill grestore 2 setlinewidth stroke
} def
/CMUSansSerif-BoldOblique findfont 3 inch scalefont setfont
1 inch 1 inch moveto
(SUN) charpath fill
1 inch 4 inch moveto
1 setlinewidth
(SUN) charpath stroke
1 inch 7 inch moveto
(SUN) charpath gsave stroke grestore clip
(SUN) stringwidth pop 2 div 1 inch add 7 inch sunrays
showpage
`);
ps20241119.js 3691 lines 133 KB
My Journey to PostScript