Starting latex rendering (canvas side).
Some checks failed
continuous-integration/drone/push Build is failing

This commit is contained in:
Adsooi 2022-03-05 20:57:21 +01:00
parent 23cd86a2e3
commit 1142ca1c00
Signed by: Ad5001
GPG key ID: EF45F9C6AFE20160
6 changed files with 191 additions and 82 deletions

View file

@ -110,6 +110,7 @@ def run():
helper = Helper(pwd, tmpfile) helper = Helper(pwd, tmpfile)
latex = Latex(tempdir, app.palette()) latex = Latex(tempdir, app.palette())
engine.rootContext().setContextProperty("Helper", helper) engine.rootContext().setContextProperty("Helper", helper)
engine.rootContext().setContextProperty("Latex", latex)
engine.rootContext().setContextProperty("TestBuild", "--test-build" in argv) engine.rootContext().setContextProperty("TestBuild", "--test-build" in argv)
engine.rootContext().setContextProperty("StartTime", dep_time) engine.rootContext().setContextProperty("StartTime", dep_time)

View file

@ -157,26 +157,40 @@ Canvas {
*/ */
property int drawMaxX: Math.ceil(Math.max(Math.abs(xmin), Math.abs(px2x(canvasSize.width)))/xaxisstep1) property int drawMaxX: Math.ceil(Math.max(Math.abs(xmin), Math.abs(px2x(canvasSize.width)))/xaxisstep1)
property var imageLoaders: {}
property var ctx
onPaint: { Component.onCompleted: imageLoaders = {}
onPaint: function(rect) {
//console.log('Redrawing') //console.log('Redrawing')
var ctx = getContext("2d"); if(rect.width == canvas.width) { // Redraw full canvas
reset(ctx) ctx = getContext("2d");
drawGrille(ctx) reset(ctx)
drawAxises(ctx) drawGrille(ctx)
ctx.lineWidth = linewidth drawAxises(ctx)
for(var objType in Objects.currentObjects) { drawLabels(ctx)
for(var obj of Objects.currentObjects[objType]){ ctx.lineWidth = linewidth
ctx.strokeStyle = obj.color for(var objType in Objects.currentObjects) {
ctx.fillStyle = obj.color for(var obj of Objects.currentObjects[objType]){
if(obj.visible) obj.draw(canvas, ctx) ctx.strokeStyle = obj.color
ctx.fillStyle = obj.color
if(obj.visible) obj.draw(canvas, ctx)
}
} }
ctx.lineWidth = 1
} }
ctx.lineWidth = 1
drawLabels(ctx)
} }
onImageLoaded: {
Object.keys(imageLoaders).forEach((key) => {
if(isImageLoaded(key)) {
// Calling callback
imageLoaders[key][0](canvas, ctx, imageLoaders[key][1])
delete imageLoaders[key]
}
})
}
/*! /*!
\qmlmethod void LogGraphCanvas::reset(var ctx) \qmlmethod void LogGraphCanvas::reset(var ctx)
@ -314,6 +328,19 @@ Canvas {
} }
} }
/*!
\qmlmethod void LogGraphCanvas::drawVisibleImage(var ctx, var image, double x, double y)
Draws an \c image onto the canvas using 2D \c ctx.
\note The \c x, \c y \c width and \c height properties here are relative to the canvas, not the plot.
*/
function drawVisibleImage(ctx, image, x, y, width, height) {
console.log("Drawing image", isImageLoaded(image), isImageError(image))
markDirty(Qt.rect(x, y, width, height));
ctx.drawImage(image, x, y, width, height)
/*if(true || (x > 0 && x < canvasSize.width && y > 0 && y < canvasSize.height)) {
}*/
}
/*! /*!
\qmlmethod var LogGraphCanvas::measureText(var ctx, string text) \qmlmethod var LogGraphCanvas::measureText(var ctx, string text)
Measures the wicth and height of a multiline \c text that would be drawn onto the canvas using 2D \c ctx. Measures the wicth and height of a multiline \c text that would be drawn onto the canvas using 2D \c ctx.
@ -415,4 +442,25 @@ Canvas {
} }
ctx.stroke(); ctx.stroke();
} }
/*!
\qmlmethod var LogGraphCanvas::renderLatexImage(string ltxText, color)
Renders latex markup \c ltxText to an image and loads it. Returns a dictionary with three values: source, width and height.
*/
function renderLatexImage(ltxText, color, callback) {
let [ltxSrc, ltxWidth, ltxHeight] = Latex.render(ltxText, textsize, color).split(",")
let imgData = {
"source": ltxSrc,
"width": parseFloat(ltxWidth),
"height": parseFloat(ltxHeight)
};
if(!isImageLoaded(ltxSrc) && !isImageLoading(ltxSrc)){
// Wait until the image is loaded to callback.
loadImage(ltxSrc)
imageLoaders[ltxSrc] = [callback, imgData]
} else {
// Callback directly
callback(canvas, ctx, imgData)
}
}
} }

View file

@ -42,8 +42,75 @@ function parif(elem, contents) {
return contents.some(x => elem.toString().includes(x)) ? par(elem) : elem return contents.some(x => elem.toString().includes(x)) ? par(elem) : elem
} }
/** /**
* This function converts expr-eval tokens to a latex string. * Creates a latex expression for a function.
*
* @param {string} f - Function to convert
* @param {Array} args - Arguments of the function
* @returns {string}
*/
function functionToLatex(f, args) {
switch(f) {
case "derivative":
return '\\frac{d' + args[0].substr(1, args[0].length-2).replace(new RegExp(by, 'g'), 'x') + '}{dx}';
break;
case "integral":
return '\\int\\limits^{' + args[0] + '}_{' + args[1] + '}' + args[2].substr(1, args[2].length-2) + ' d' + args[3];
break;
case "sqrt":
return '\\sqrt\\left(' + args.join(', ') + '\\right)';
break;
case "abs":
return '\\left|' + args.join(', ') + '\\right|';
break;
case "floor":
return '\\left\\lfloor' + args.join(', ') + '\\right\\rfloor';
break;
case "ceil":
return '\\left\\lceil' + args.join(', ') + '\\right\\rceil';
break;
default:
return '\\mathrm{' + f + '}\\left(' + args.join(', ') + '\\right)';
break;
}
}
/**
* Creates a latex variable from a variable.
*
* @param {string} vari - variable to convert
* @returns {string}
*/
function variableToLatex(vari) {
unicodechars = ["α","β","γ","δ","ε","ζ","η",
"π","θ","κ","λ","μ","ξ","ρ",
"ς","σ","τ","φ","χ","ψ","ω",
"Γ","Δ","Θ","Λ","Ξ","Π","Σ",
"Φ","Ψ","Ω","ₐ","ₑ","ₒ","ₓ",
"ₕ","ₖ","ₗ","ₘ","ₙ","ₚ","ₛ",
"ₜ","¹","²","³","⁴","⁵","⁶",
"⁷","⁸","⁹","⁰","₁","₂","₃",
"₄","₅","₆","₇","₈","₉","₀"]
equivalchars = ["alpha","beta","gamma","delta","epsilon","zeta","eta",
"pi","theta","kappa","lambda","mu","xi","rho",
"sigma","sigma","tau","phi","chi","psi","omega",
"Gamma","Delta","Theta","Lambda","Xi","Pi","Sigma",
"Phy","Psi","Omega","{}_{a}","{}_{e}","{}_{o}","{}_{x}",
"{}_{h}","{}_{k}","{}_{l}","{}_{m}","{}_{n}","{}_{p}","{}_{s}",
"{}_{t}","{}^{1}","{}^{2}","{}^{3}","{}^{4}","{}^{5}","{}^{6}",
"{}^{7}","{}^{8}","{}^{9}","{}^{0}","{}_{1}","{}_{2}","{}_{3}",
"{}_{4}","{}_{5}","{}_{6}","{}_{7}","{}_{8}","{}_{9}","{}_{0}"]
for(int i = 0; i < unicodechars.length; i++) {
if(vari.includes(unicodechars[i]))
vari = vari.replaceAll(unicodechars[i], equivalchars[i])
}
return vari;
}
/**
* Converts expr-eval tokens to a latex string.
* *
* @param {Array} tokens - expr-eval tokens list * @param {Array} tokens - expr-eval tokens list
* @returns {string} * @returns {string}
@ -115,10 +182,7 @@ function expressionToLatex(tokens) {
break; break;
case ExprEval.IVAR: case ExprEval.IVAR:
case ExprEval.IVARNAME: case ExprEval.IVARNAME:
if(item.value == "pi") nstack.push(variableToLatex(item.value));
nstack.push("π")
else
nstack.push(item.value);
break; break;
case ExprEval.IOP1: // Unary operator case ExprEval.IOP1: // Unary operator
n1 = nstack.pop(); n1 = nstack.pop();
@ -144,21 +208,7 @@ function expressionToLatex(tokens) {
} }
f = nstack.pop(); f = nstack.pop();
// Handling various functions // Handling various functions
if(f == "derivative") nstack.push(functionToLatex(f, args))
nstack.push('\\frac{d' + args[0].substr(1, args[0].length-2).replace(new RegExp(by, 'g'), 'x') + '}{dx}');
else if(f == "integral")
nstack.push('\\int\\limits^{' + args[0] + '}_{' + args[1] + '}' + args[2].substr(1, args[2].length-2) + ' d' + args[3]);
else if(f == "sqrt")
nstack.push('\\sqrt\\left(' + args.join(', ') + '\\right)');
else if(f == "abs")
nstack.push('\\left|' + args.join(', ') + '\\right|');
else if(f == "floor")
nstack.push('\\left\\lfloor' + args.join(', ') + '\\right\\rfloor');
else if(f == "ceil")
nstack.push('\\left\\lceil' + args.join(', ') + '\\right\\rceil');
else
nstack.push('\\mathrm{' + f + '}\\left(' + args.join(', ') + '\\right)');
break;
case ExprEval.IFUNDEF: case ExprEval.IFUNDEF:
nstack.push(par(n1 + '(' + args.join(', ') + ') = ' + n2)); nstack.push(par(n1 + '(' + args.join(', ') + ') = ' + n2));
break; break;
@ -191,5 +241,6 @@ function expressionToLatex(tokens) {
nstack = [ nstack.join(';') ]; nstack = [ nstack.join(';') ];
} }
} }
console.log(nstack[0]);
return String(nstack[0]); return String(nstack[0]);
} }

View file

@ -84,12 +84,16 @@ class DrawableObject {
return `${this.name} = Unknown` return `${this.name} = Unknown`
} }
toLatexString() {
return this.getReadableString()
}
getLabel() { getLabel() {
switch(this.labelContent) { switch(this.labelContent) {
case 'name': case 'name':
return this.name return this.name
case 'name + value': case 'name + value':
return this.getReadableString() return this.toLatexString()
case 'null': case 'null':
return '' return ''

View file

@ -58,6 +58,10 @@ class Point extends Common.DrawableObject {
return `${this.name} = (${this.x}, ${this.y})` return `${this.name} = (${this.x}, ${this.y})`
} }
toLatexString() {
return `${this.name} = \\left(${this.x.latexMarkup}, ${this.y.latexMarkup}\\right)`
}
export() { export() {
return [this.name, this.visible, this.color.toString(), this.labelContent, this.x.toEditableString(), this.y.toEditableString(), this.labelPosition, this.pointStyle] return [this.name, this.visible, this.color.toString(), this.labelContent, this.x.toEditableString(), this.y.toEditableString(), this.labelPosition, this.pointStyle]
} }
@ -80,41 +84,43 @@ class Point extends Common.DrawableObject {
ctx.fillRect(canvasX-1, canvasY-pointSize/2, 2, pointSize) ctx.fillRect(canvasX-1, canvasY-pointSize/2, 2, pointSize)
break; break;
} }
var text = this.getLabel()
ctx.font = `${canvas.textsize}px sans-serif` let drawLabel = function(canvas, ctx, ltxImg) {
var textSize = ctx.measureText(text).width //console.log(JSON.stringify(ltxImg), canvas.isImageLoaded(ltxImg.source), this, this.labelPosition)
switch(this.labelPosition) { switch(this.labelPosition) {
case 'top': case 'top':
case 'above': case 'above':
canvas.drawVisibleText(ctx, text, canvasX-textSize/2, canvasY-16) canvas.drawVisibleImage(ctx, ltxImg.source, canvasX-ltxImg.width/2, canvasY-(ltxImg.height+4), ltxImg.width, ltxImg.height)
break; break;
case 'bottom': case 'bottom':
case 'below': case 'below':
canvas.drawVisibleText(ctx, text, canvasX-textSize/2, canvasY+16) canvas.drawVisibleImage(ctx, ltxImg.source, canvasX-ltxImg.width/2, canvasY+4, ltxImg.width, ltxImg.height)
break; break;
case 'left': case 'left':
canvas.drawVisibleText(ctx, text, canvasX-textSize-10, canvasY+4) canvas.drawVisibleImage(ctx, ltxImg.source, canvasX-(ltxImg.width+4), canvasY+4, ltxImg.width, ltxImg.height)
break; break;
case 'right': case 'right':
canvas.drawVisibleText(ctx, text, canvasX+10, canvasY+4) canvas.drawVisibleImage(ctx, ltxImg.source, canvasX+4, canvasY+4, ltxImg.width, ltxImg.height)
break; break;
case 'top-left': case 'top-left':
case 'above-left': case 'above-left':
canvas.drawVisibleText(ctx, text, canvasX-textSize-10, canvasY-16) canvas.drawVisibleImage(ctx, ltxImg.source, canvasX-(ltxImg.width+4), canvasY-(ltxImg.height+4), ltxImg.width, ltxImg.height)
break; break;
case 'top-right': case 'top-right':
case 'above-right': case 'above-right':
canvas.drawVisibleText(ctx, text, canvasX+10, canvasY-16) canvas.drawVisibleImage(ctx, ltxImg.source, canvasX+4, canvasY-(ltxImg.height+4), ltxImg.width, ltxImg.height)
break; break;
case 'bottom-left': case 'bottom-left':
case 'below-left': case 'below-left':
canvas.drawVisibleText(ctx, text, canvasX-textSize-10, canvasY+16) canvas.drawVisibleImage(ctx, ltxImg.source, canvasX-(ltxImg.width+4), canvasY+4, ltxImg.width, ltxImg.height)
break; break;
case 'bottom-right': case 'bottom-right':
case 'below-right': case 'below-right':
canvas.drawVisibleText(ctx, text, canvasX+10, canvasY+16) canvas.drawVisibleImage(ctx, ltxImg.source, canvasX+4, canvasY+4, ltxImg.width, ltxImg.height)
break; break;
}
} }
canvas.renderLatexImage(this.getLabel(), this.color, drawLabel.bind(this))
//canvas.drawVisibleImage(ctx, ltxImg.source, canvasX, canvasY)
} }
} }

View file

@ -31,16 +31,13 @@ class Latex(QObject):
self.palette = palette self.palette = palette
fg = self.palette.windowText().color().convertTo(QColor.Rgb) fg = self.palette.windowText().color().convertTo(QColor.Rgb)
@Slot(str, float, bool, result=str) @Slot(str, float, QColor, result=str)
def render(self, latexstring, font_size, themeFg = True): def render(self, latexstring, font_size, color = True):
exprpath = path.join(self.tempdir.name, str(hash(latexstring)) + '.png') exprpath = path.join(self.tempdir.name, f'{hash(latexstring)}_{font_size}_{color.rgb()}.png')
print(latexstring, exprpath) print("Rendering", latexstring, exprpath)
if not path.exists(exprpath): if not path.exists(exprpath):
if themeFg: fg = color.convertTo(QColor.Rgb)
fg = self.palette.windowText().color().convertTo(QColor.Rgb) fg = f'rgb {fg.redF()} {fg.greenF()} {fg.blueF()}'
fg = f'rgb {fg.redF()} {fg.greenF()} {fg.blueF()}'
else:
fg = 'rgb 1.0 1.0 1.0'
preview('$$' + latexstring + '$$', viewer='file', filename=exprpath, dvioptions=[ preview('$$' + latexstring + '$$', viewer='file', filename=exprpath, dvioptions=[
"-T", "tight", "-T", "tight",
"-z", "0", "-z", "0",
@ -48,11 +45,13 @@ class Latex(QObject):
f"-D {font_size * 72.27 / 10}", # See https://linux.die.net/man/1/dvipng#-D for convertion f"-D {font_size * 72.27 / 10}", # See https://linux.die.net/man/1/dvipng#-D for convertion
"-bg", "Transparent", "-bg", "Transparent",
"-fg", fg], "-fg", fg],
euler=False) euler=True)
return exprpath img = QImage(exprpath);
# Small hack, not very optimized since we load the image twice, but you can't pass a QImage to QML and expect it to be loaded
return f'{exprpath},{img.width()},{img.height()}'
@Slot(str) @Slot(str)
def copyLatexImageToClipboard(self, latexstring): def copyLatexImageToClipboard(self, latexstring):
global tempfile global tempfile
clipboard = QApplication.clipboard() clipboard = QApplication.clipboard()
clipboard.setImage(QImage(self.render(latexstring))) clipboard.setImage(self.render(latexstring))