Adding colors is quite trivial. We already used setgray and currentgray with internal RGB values. We just have to add setrgbcolor and currentrgbcolor operators.
Run
postScriptEditor(`
10 dict begin /pdf 1 def /svg 1 def /canvas 1 def /textmode 1 def currentdict setpagedevice end
0.9 1.0 0.9 setrgbcolor
100 100 moveto 400 100 lineto 400 300 lineto 100 300 lineto closepath fill
/CMUSerif-Italic findfont 300 scalefont setfont
2 setlinewidth
0.0 0.0 0.8 setrgbcolor
0 100 moveto (Post) charpath stroke
/CMUSerif-Italic findfont 60 scalefont setfont
100 50 translate 60 rotate 0.7 setgray
7 { 0 0 moveto (PostScript) show
-10 rotate
currentgray 0.1 sub setgray } repeat
showpage
`);
We might also deal later with CMYK colors, but for the moment this is not our focus.
Transparency however is not standard in PostScript. But we will add custom operators setalpha and currentalpha
Run
operators.setalpha = function(context) {
const [a] = context.pop("number");
if (!a) return context;
const alimited = Math.min(Math.max(a.value,0),1);
context.graphics.color[3] = Math.round(alimited * 255);
return context;
};
operators.currentalpha = function(context) {
const a = context.graphics.color[3];
context.stack.push(new rpnNumber(a.value/255.0));
return context;
};
Alpha is the 4th component of context.graphics.color, so we are fine for the context. But currently, setgray and setrgbcolor overwrite it with 255, so we need to fix that and use the current alpha.
Run
operators.setgray = function(context) {
const [g] = context.pop("number");
if (!g) return context;
const glimited = Math.min(Math.max(g.value,0),1);
context.graphics.color[0] = Math.round(glimited * 255);
context.graphics.color[1] = Math.round(glimited * 255);
context.graphics.color[2] = Math.round(glimited * 255);
return context;
};
operators.setrgbcolor = function(context) {
const [b, g, r] = context.pop("number", "number", "number");
if (!g) return context;
const rlimited = Math.min(Math.max(r.value,0),1);
const glimited = Math.min(Math.max(g.value,0),1);
const blimited = Math.min(Math.max(b.value,0),1);
context.graphics.color[0] = Math.round(rlimited * 255);
context.graphics.color[1] = Math.round(glimited * 255);
context.graphics.color[2] = Math.round(blimited * 255);
return context;
};
Let's test alpha. The raw rendering is lighter, but there is nothing transparent. Note that alpha is inside the picture, Transparent makes the entire picture transparent instead of having a white background,
Run
postScriptEditor(`
10 dict begin /pdf 1 def /pdfurl 1 def /svg 1 def /canvas 1 def /textmode 1 def /transparent 1 def currentdict setpagedevice end
0.9 1.0 0.9 setrgbcolor
100 100 moveto 400 100 lineto 400 300 lineto 100 300 lineto closepath fill
/CMUSerif-Italic findfont 300 scalefont setfont
2 setlinewidth
0.0 0.0 0.8 setrgbcolor
0 100 moveto (Post) charpath stroke
0.5 setalpha
0 100 moveto (Post) charpath fill
showpage
`);
We need to implement alpha in all devices. First raw.
The traditional formula for alpha is D = F * Fa + B * (1 - Fa) where D is destination, B background, F foreground and Fa foreground alpha. The formula can be simplified as D = B + Fa * (Fa - B) .
However, this formula does not work if the background is itself transparent. We must calculate in 2 steps: Da = Fa + Ba * (1 - Fa) and D = (F * Fa + B * Ba * (1 - Fa)) / Da
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, 0);
}
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);
}
fill(context) {
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, 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 = [];
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;
this.data = scanFill([ ["L",x0a, y0a, x1a, y1a], ["L",x0a, y0a, x0b, y0b],["L", x0b, y0b, x1b, y1b], ["L",x1a, y1a, x1b, y1b]], context.width, context.height, context.graphics.color, this.data);
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) {
this.data = scanFill([ ["L",olda[3],olda[4], xa, ya], ["L",xa, ya, x0a, y0a],["L", x0a, y0a, x0b, y0b], ["L",x0b, y0b, xb, yb],["L",xb, yb, oldb[3], oldb[4]],["L", oldb[3], oldb[4], olda[3], olda[4]]], context.width, context.height, context.graphics.color, this.data);
}
}
olda = ["L", x0a, y0a, x1a, y1a];
oldb = ["L", x0b, y0b, x1b, y1b];
}
}
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;
}
}
scanFill = function (path, width, height, color, data) {
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;
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(x);
}
x += ex;
}
}
}
for (let y in crossings) {
const arr = crossings[y];
arr.sort(function (a, b) { return a - b; });
for (let i = 0; i < arr.length - 1; i += 2) {
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) {
// 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;
};
Canvas has a property ctx.globalAlpha.
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);
}
}
fill(context) {
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;
ctx.fill();
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]);
}
}
}
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;
}
};
SVG has attributes fill-alpha and stroke-alpha.
Run
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) {
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]));
}
}
}
p.push("Z");
return p.join(" ");
}
fill(context) {
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);
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));
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;
}
};
PDF does not have an operator for transparency. However PDF 1.4 allows to define graphics parameters in a dictionary. Therefore i created 16 dictionaries that set CA (stroke alpha) and ca (fill alpha) to values between 0 and 1. The dictionary are referenced in the source of the page. 16 levels should be enough to blend.
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]) }
For every fill, stroke and show setalpha is used.
setalpha(context) { const gs = Math.round(context.graphics.color[3]/16) this.elements.push("/GS"+gs+" gs"); }
Run
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) {
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");
}
}
}
p.push("h");
return p.join(" ");
}
fill(context) {
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);
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";
}
return context;
}
stroke(context) {
if (context.device.pdf + context.device.pdfurl < 1) return context;
this.elements.push(this.getPath(context));
this.setcolor(context);
this.setalpha(context);
const ws = this.numberFormat(context.graphics.linewidth);
this.elements.push(ws+' w');
this.elements.push("S");
return context;
}
};
3453 lines ps20241028.js
My Journey to PostScript