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.
We add functions to the devices to test if it works. For raw, canvas and SVG we add an interval property to device. By default, the frames are shown immediately, but you can delay them by a certain number of microseconds. However, everything is first calculated and then rendered. We will fix that later.
Run
rpnRawDevice = class {
constructor(node, urlnode) {
this.node = node;
if (!this.node) this.node = document.createElement("CANVAS");
const ctx = this.node.getContext("2d");
this.urlnode = urlnode;
if (this.node) this.clear(this.node.width, this.node.height, 1, 1);
this.imagestack = [];
this.finalround = false;
this.timer = null;
}
finalize(context) {
this.finalround = true;
}
clear(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;
// touch interface only when changed
if (width != this.node.width) this.node.width = width;
if (height != this.node.height) this.node.height = 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) {
const 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);
const nodesmall = document.createElement("CANVAS");
nodesmall.width = context.width;
nodesmall.height = context.height;
const nodectx = nodesmall.getContext("2d");
nodectx.scale(1/context.device.oversampling, 1/context.device.oversampling);
nodectx.imageSmoothingEnabled = true;
nodectx.imageSmoothingQuality = "high";
nodectx.drawImage(nodebig,0,0);
this.imagestack.push(nodectx.getImageData(0,0, context.width, context.height));
} else {
this.imagestack.push(new ImageData(this.data, context.width * context.device.oversampling));
}
if (! this.timer) this.timer = setInterval ( () => {
ctx.putImageData(this.imagestack.shift(), 0,0);
if (this.finalround && this.imagestack.length == 0)
clearInterval(this.timer);
}, context.device.interval );
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";
}
this.clear(this.node.width, this.node.height, context.device.oversampling, context.device.transparent);
return context;
}
};
rpnCanvasDevice = class {
constructor(node, urlnode) {
this.node = node;
if (!node) document.createElement("CANVAS");
this.urlnode = urlnode;
this.clippath = "";
this.ctx = null;
this.imagestack = [];
this.timer = null;
this.finalround = false;
}
finalize(context) {
this.finalround = true;
}
clear(width, height, oversampling, transparent) {
this.node.width = width;
this.node.height = height;
const node = document.createElement("CANVAS");
node.width = width;
node.height = height;
this.ctx = node.getContext("2d");
this.ctx.clearRect(0, 0, width, height);
if (!transparent) {
this.ctx.fillStyle = "white";
this.ctx.fillRect(0, 0, width, height);
}
this.oversampling = oversampling;
this.transparent = transparent;
}
applyPath(path) {
if (!this.ctx) this.clear(this.node.width, this.node.height, this.oversampling, this.transparent);
this.ctx.beginPath();
for (let subpath of path) {
if (!subpath.length) continue;
this.ctx.moveTo(subpath[0][1], this.node.height - subpath[0][2]);
for (let line of subpath) {
if (line[0] == "C") {
this.ctx.bezierCurveTo(line[3], this.node.height - line[4], line[5], this.node.height - line[6], line[7], this.node.height - line[8]);
} else {
this.ctx.lineTo(line[3], this.node.height - line[4]);
if (line[0] == "Z") this.ctx.closePath();
}
}
}
}
clip(context) {
if (!this.ctx) this.clear(this.node.width, this.node.height, this.oversampling, this.transparent);
for (let clip of context.graphics.clip) {
this.applyPath(clip);
this.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";
if (!this.ctx) this.clear(this.node.width, this.node.height, this.oversampling, this.transparent);
this.ctx.save();
context = this.clip(context);
this.applyPath(context.graphics.path);
this.ctx.fillStyle = "rgb("+Math.round(context.graphics.color[0])+" "+Math.round(context.graphics.color[1])+" "+ Math.round(context.graphics.color[2])+")";
this.ctx.globalAlpha = context.graphics.color[3]/255.0;
if (zerowind) {
this.ctx.fill("nonzero");
} else {
this.ctx.fill("evenodd");
}
this.ctx.restore();
return context;
}
stroke(context) {
if (context.device.canvas + context.device.canvasurl < 1) return context;
if (!this.ctx) this.clear(this.node.width, this.node.height, this.oversampling, this.transparent);
this.ctx.save();
context = this.clip(context);
this.applyPath(context.graphics.path);
this.ctx.strokeStyle = "rgb("+Math.round(context.graphics.color[0])+" "+Math.round(context.graphics.color[1])+" "+ Math.round(context.graphics.color[2])+")";
this.ctx.globalAlpha = context.graphics.color[3]/255.0;
this.ctx.lineWidth = context.graphics.linewidth;
this.ctx.stroke();
this.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.ctx.canvas.toDataURL();
this.urlnode.href = url;
this.urlnode.setAttribute("download", "PS.png");
} else {
if (this.urlnode) this.urlnode.style.display = "none";
}
this.imagestack.push(this.ctx.getImageData(0, 0, context.width, context.height));
if (! this.timer) this.timer = setInterval ( () => {
const ctx = this.node.getContext("2d");
ctx.putImageData(this.imagestack.shift(), 0,0);
if (this.finalround && this.imagestack.length == 0)
clearInterval(this.timer);
}, context.device.interval );
this.ctx = null;
return context;
}
};
rpnPDFDevice = class {
constructor(node, urlnode) {
this.node = node;
if (!this.node) this.node = document.createElement("IMG");
this.urlnode = urlnode;
this.clear(this.node.width, this.node.height, 1, 1);
this.canshow = true;
this.catalog = {};
this.pageslist = {};
this.pages = [];
this.streams = [];
this.alphas = [];
this.elements = [];
this.usedfonts = {};
this.currentfont = {};
}
finalize(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.pageslist.Type = "/Pages";
this.pageslist.Kids = "[ ";
for (let i = 0; i < this.pages.length; i++)
this.pageslist.Kids += (20+i)+" 0 R ";
this.pageslist.Kids += "]";
this.pageslist.Count = this.pages.length;
this.pageslist.MediaBox = "[0 0 " + this.width + " " + this.height + "]";
objects.push([this.pageslist]);
for (let i = 0; i < 17; i++)
objects.push(this.alphas[i]);
var i = 0 ;
for (let dict of this.pages) {
i++;
dict[0].Contents = (19 + this.pages.length + i)+" 0 R";
objects.push(dict);
}
for (let dict of this.streams)
objects.push(dict);
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;
}
}
clear(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) {
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;
this.pages.push([dict]);
for (let i = 0; i < 17; i++ ) {
alphadict["GS"+i] = (3+i) + " 0 R";
const ad = {};
ad.type = "/ExtGState";
ad.ca = i / 16.0;
ad.CA = i / 16.0;
this.alphas.push([ad])
}
const streamdict = {};
const stream = this.elements.join(endofline);
this.elements = [];
streamdict.Length = stream.length;
this.streams.push([streamdict, stream]);
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.clear( 590, 330, 1, 1);
this.imagestack = [];
this.timer = null;
this.finalround = false;
}
finalize(context) {
this.finalround = true;
}
refresh() {
this.node.style.display='none';
this.node.offsetHeight;
this.node.style.display='block';
// https://martinwolf.org/before-2018/blog/2014/06/force-repaint-of-an-element-with-javascript/
}
clear(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.clippath = "";
this.clippathsource = "";
while(this.node.firstChild) this.node.removeChild(this.node.lastChild);
this.oversampling = oversampling;
this.transparent = transparent
this.ctx = true;
}
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) {
if (!this.ctx) this.clear(this.width, this.height, this.oversampling, this.transparent);
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";
if (!this.ctx) this.clear(this.width, this.height, this.oversampling, this.transparent);
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";
if (!this.ctx) this.clear(this.width, this.height, this.oversampling, this.transparent);
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;
}
if (!this.ctx) this.clear(this.width, this.height, this.oversampling, this.transparent);
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.innerHTML = this.node.innerHTML + " "; // force refresh
this.imagestack.push(this.node.innerHTML.slice());
if (! this.timer) this.timer = setInterval ( () => {
this.node.innerHTML = this.imagestack.shift();
if (this.finalround && this.imagestack.length == 0)
clearInterval(this.timer);
}, context.device.interval );
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";
this.ctx = null;
return context;
}
};