From 1142ca1c00ddb58d26b3c5b023524aaed33b9ade Mon Sep 17 00:00:00 2001 From: Ad5001 Date: Sat, 5 Mar 2022 20:57:21 +0100 Subject: [PATCH] Starting latex rendering (canvas side). --- LogarithmPlotter/logarithmplotter.py | 1 + .../LogarithmPlotter/LogGraphCanvas.qml | 76 +++++++++++++--- .../ad5001/LogarithmPlotter/js/math/latex.js | 91 +++++++++++++++---- .../ad5001/LogarithmPlotter/js/objs/common.js | 6 +- .../ad5001/LogarithmPlotter/js/objs/point.js | 76 +++++++++------- LogarithmPlotter/util/latex.py | 23 +++-- 6 files changed, 191 insertions(+), 82 deletions(-) diff --git a/LogarithmPlotter/logarithmplotter.py b/LogarithmPlotter/logarithmplotter.py index 6a3cc9d..2ce7b94 100644 --- a/LogarithmPlotter/logarithmplotter.py +++ b/LogarithmPlotter/logarithmplotter.py @@ -110,6 +110,7 @@ def run(): helper = Helper(pwd, tmpfile) latex = Latex(tempdir, app.palette()) engine.rootContext().setContextProperty("Helper", helper) + engine.rootContext().setContextProperty("Latex", latex) engine.rootContext().setContextProperty("TestBuild", "--test-build" in argv) engine.rootContext().setContextProperty("StartTime", dep_time) diff --git a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/LogGraphCanvas.qml b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/LogGraphCanvas.qml index f140007..3009868 100644 --- a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/LogGraphCanvas.qml +++ b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/LogGraphCanvas.qml @@ -157,26 +157,40 @@ Canvas { */ 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') - var ctx = getContext("2d"); - reset(ctx) - drawGrille(ctx) - drawAxises(ctx) - ctx.lineWidth = linewidth - for(var objType in Objects.currentObjects) { - for(var obj of Objects.currentObjects[objType]){ - ctx.strokeStyle = obj.color - ctx.fillStyle = obj.color - if(obj.visible) obj.draw(canvas, ctx) + if(rect.width == canvas.width) { // Redraw full canvas + ctx = getContext("2d"); + reset(ctx) + drawGrille(ctx) + drawAxises(ctx) + drawLabels(ctx) + ctx.lineWidth = linewidth + for(var objType in Objects.currentObjects) { + for(var obj of Objects.currentObjects[objType]){ + 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) @@ -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) 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(); } + + /*! + \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) + } + } } diff --git a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/math/latex.js b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/math/latex.js index d7620fc..efb4cba 100644 --- a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/math/latex.js +++ b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/math/latex.js @@ -42,8 +42,75 @@ function parif(elem, contents) { 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 * @returns {string} @@ -115,10 +182,7 @@ function expressionToLatex(tokens) { break; case ExprEval.IVAR: case ExprEval.IVARNAME: - if(item.value == "pi") - nstack.push("π") - else - nstack.push(item.value); + nstack.push(variableToLatex(item.value)); break; case ExprEval.IOP1: // Unary operator n1 = nstack.pop(); @@ -144,21 +208,7 @@ function expressionToLatex(tokens) { } f = nstack.pop(); // Handling various functions - if(f == "derivative") - 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; + nstack.push(functionToLatex(f, args)) case ExprEval.IFUNDEF: nstack.push(par(n1 + '(' + args.join(', ') + ') = ' + n2)); break; @@ -191,5 +241,6 @@ function expressionToLatex(tokens) { nstack = [ nstack.join(';') ]; } } + console.log(nstack[0]); return String(nstack[0]); } diff --git a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/objs/common.js b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/objs/common.js index 8eb3036..d9dc33f 100644 --- a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/objs/common.js +++ b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/objs/common.js @@ -84,12 +84,16 @@ class DrawableObject { return `${this.name} = Unknown` } + toLatexString() { + return this.getReadableString() + } + getLabel() { switch(this.labelContent) { case 'name': return this.name case 'name + value': - return this.getReadableString() + return this.toLatexString() case 'null': return '' diff --git a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/objs/point.js b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/objs/point.js index e0b9c58..feb965f 100644 --- a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/objs/point.js +++ b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/objs/point.js @@ -58,6 +58,10 @@ class Point extends Common.DrawableObject { return `${this.name} = (${this.x}, ${this.y})` } + toLatexString() { + return `${this.name} = \\left(${this.x.latexMarkup}, ${this.y.latexMarkup}\\right)` + } + export() { 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) break; } - var text = this.getLabel() - ctx.font = `${canvas.textsize}px sans-serif` - var textSize = ctx.measureText(text).width - switch(this.labelPosition) { - case 'top': - case 'above': - canvas.drawVisibleText(ctx, text, canvasX-textSize/2, canvasY-16) - break; - case 'bottom': - case 'below': - canvas.drawVisibleText(ctx, text, canvasX-textSize/2, canvasY+16) - break; - case 'left': - canvas.drawVisibleText(ctx, text, canvasX-textSize-10, canvasY+4) - break; - case 'right': - canvas.drawVisibleText(ctx, text, canvasX+10, canvasY+4) - break; - case 'top-left': - case 'above-left': - canvas.drawVisibleText(ctx, text, canvasX-textSize-10, canvasY-16) - break; - case 'top-right': - case 'above-right': - canvas.drawVisibleText(ctx, text, canvasX+10, canvasY-16) - break; - case 'bottom-left': - case 'below-left': - canvas.drawVisibleText(ctx, text, canvasX-textSize-10, canvasY+16) - break; - case 'bottom-right': - case 'below-right': - canvas.drawVisibleText(ctx, text, canvasX+10, canvasY+16) - break; - + + let drawLabel = function(canvas, ctx, ltxImg) { + //console.log(JSON.stringify(ltxImg), canvas.isImageLoaded(ltxImg.source), this, this.labelPosition) + switch(this.labelPosition) { + case 'top': + case 'above': + canvas.drawVisibleImage(ctx, ltxImg.source, canvasX-ltxImg.width/2, canvasY-(ltxImg.height+4), ltxImg.width, ltxImg.height) + break; + case 'bottom': + case 'below': + canvas.drawVisibleImage(ctx, ltxImg.source, canvasX-ltxImg.width/2, canvasY+4, ltxImg.width, ltxImg.height) + break; + case 'left': + canvas.drawVisibleImage(ctx, ltxImg.source, canvasX-(ltxImg.width+4), canvasY+4, ltxImg.width, ltxImg.height) + break; + case 'right': + canvas.drawVisibleImage(ctx, ltxImg.source, canvasX+4, canvasY+4, ltxImg.width, ltxImg.height) + break; + case 'top-left': + case 'above-left': + canvas.drawVisibleImage(ctx, ltxImg.source, canvasX-(ltxImg.width+4), canvasY-(ltxImg.height+4), ltxImg.width, ltxImg.height) + break; + case 'top-right': + case 'above-right': + canvas.drawVisibleImage(ctx, ltxImg.source, canvasX+4, canvasY-(ltxImg.height+4), ltxImg.width, ltxImg.height) + break; + case 'bottom-left': + case 'below-left': + canvas.drawVisibleImage(ctx, ltxImg.source, canvasX-(ltxImg.width+4), canvasY+4, ltxImg.width, ltxImg.height) + break; + case 'bottom-right': + case 'below-right': + canvas.drawVisibleImage(ctx, ltxImg.source, canvasX+4, canvasY+4, ltxImg.width, ltxImg.height) + break; + } } + canvas.renderLatexImage(this.getLabel(), this.color, drawLabel.bind(this)) + //canvas.drawVisibleImage(ctx, ltxImg.source, canvasX, canvasY) } } diff --git a/LogarithmPlotter/util/latex.py b/LogarithmPlotter/util/latex.py index 943edfc..680613a 100644 --- a/LogarithmPlotter/util/latex.py +++ b/LogarithmPlotter/util/latex.py @@ -31,16 +31,13 @@ class Latex(QObject): self.palette = palette fg = self.palette.windowText().color().convertTo(QColor.Rgb) - @Slot(str, float, bool, result=str) - def render(self, latexstring, font_size, themeFg = True): - exprpath = path.join(self.tempdir.name, str(hash(latexstring)) + '.png') - print(latexstring, exprpath) + @Slot(str, float, QColor, result=str) + def render(self, latexstring, font_size, color = True): + exprpath = path.join(self.tempdir.name, f'{hash(latexstring)}_{font_size}_{color.rgb()}.png') + print("Rendering", latexstring, exprpath) if not path.exists(exprpath): - if themeFg: - fg = self.palette.windowText().color().convertTo(QColor.Rgb) - fg = f'rgb {fg.redF()} {fg.greenF()} {fg.blueF()}' - else: - fg = 'rgb 1.0 1.0 1.0' + fg = color.convertTo(QColor.Rgb) + fg = f'rgb {fg.redF()} {fg.greenF()} {fg.blueF()}' preview('$$' + latexstring + '$$', viewer='file', filename=exprpath, dvioptions=[ "-T", "tight", "-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 "-bg", "Transparent", "-fg", fg], - euler=False) - return exprpath + euler=True) + 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) def copyLatexImageToClipboard(self, latexstring): global tempfile clipboard = QApplication.clipboard() - clipboard.setImage(QImage(self.render(latexstring))) + clipboard.setImage(self.render(latexstring))