WORK IN PROGRES. A book has multiple pages, so has PDF. We will extend the rpnPDFdevice to support multiple pages. PostScript already provides multi page support through the showpage operator. Each time showpage renders the page, initatiates the graphics and starts a new page. However, our main loop must tell all the devices to clean up when we are at the end of the code. But this should happen only on the main loop, not when rpn is called inside the context. rpn will delegate this to context so it does not have to know about the devices.
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);
}
finalize() {}
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;
}
};
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);
}
finalize() {}
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 = {};
}
finalize() {}
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);
}
finalize() {}
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;
}
};