From 8d6891c4f095a8ab77e854203f1b64d536270d07 Mon Sep 17 00:00:00 2001 From: Ad5001 Date: Mon, 16 Sep 2024 22:05:17 +0200 Subject: [PATCH] Starting async LaTeX rendering. --- .../ObjectLists/ObjectRow.qml | 8 +-- .../eu/ad5001/LogarithmPlotter/js/canvas.mjs | 40 ++++++------ .../LogarithmPlotter/js/history/common.mjs | 6 +- .../ad5001/LogarithmPlotter/js/math/latex.mjs | 63 +++++++++++++++++-- LogarithmPlotter/util/latex.py | 26 +++++++- 5 files changed, 110 insertions(+), 33 deletions(-) diff --git a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/ObjectLists/ObjectRow.qml b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/ObjectLists/ObjectRow.qml index 0772da8..f1292cd 100644 --- a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/ObjectLists/ObjectRow.qml +++ b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/ObjectLists/ObjectRow.qml @@ -100,10 +100,10 @@ Item { anchors.left: parent.left visible: Modules.Latex.enabled property double depth: Screen.devicePixelRatio - property var ltxInfo: visible ? Latex.render(obj.getLatexString(), depth*(parent.font.pixelSize+2), parent.color).split(",") : ["","0","0"] - source: visible ? ltxInfo[0] : "" - width: parseInt(ltxInfo[1])/depth - height: parseInt(ltxInfo[2])/depth + property var ltxInfo: visible ? Modules.Latex.renderSync(obj.getLatexString(), depth*(parent.font.pixelSize+2), parent.color) : { source: "", width: 0, height: 0 } + source: visible ? ltxInfo.source : "" + width: ltxInfo.width/depth + height: ltxInfo.height/depth } MouseArea { diff --git a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/canvas.mjs b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/canvas.mjs index cdd0777..cff8ae9 100644 --- a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/canvas.mjs +++ b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/canvas.mjs @@ -16,9 +16,10 @@ * along with this program. If not, see . */ -import {Module} from "./modules.mjs" -import {textsup} from "./utils.mjs" -import {Expression} from "./mathlib.mjs" +import { Module } from "./modules.mjs" +import { textsup } from "./utils.mjs" +import { Expression } from "./mathlib.mjs" +import Latex from "./math/latex.mjs" class CanvasAPI extends Module { @@ -168,6 +169,8 @@ class CanvasAPI extends Module { obj.draw(this) } catch(e) { // Drawing throws an error. Generally, it's due to a new modification (or the opening of a file) + console.error(e) + console.log(e.stack) this._drawingErrorDialog.showDialog(objType, obj.name, e.message) Modules.History.undo() } @@ -452,23 +455,24 @@ class CanvasAPI extends Module { * Renders latex markup ltxText to an image and loads it. Returns a dictionary with three values: source, width and height. * @param {string} ltxText * @param {string} color - * @param {function({width: number, height: number, source: string})} callback + * @param {function(LatexRenderResult|{width: number, height: number, source: string})} callback */ renderLatexImage(ltxText, color, callback) { - let [ltxSrc, ltxWidth, ltxHeight] = Latex.render(ltxText, this.textsize, color).split(",") - let imgData = { - "source": ltxSrc, - "width": parseFloat(ltxWidth), - "height": parseFloat(ltxHeight) - }; - if(!this._canvas.isImageLoaded(ltxSrc) && !this._canvas.isImageLoading(ltxSrc)){ - // Wait until the image is loaded to callback. - this._canvas.loadImage(ltxSrc) - this._canvas.imageLoaders[ltxSrc] = [callback, imgData] - } else { - // Callback directly - callback(imgData) + const onRendered = (imgData) => { + if(!this._canvas.isImageLoaded(imgData.source) && !this._canvas.isImageLoading(imgData.source)){ + // Wait until the image is loaded to callback. + this._canvas.loadImage(imgData.source) + this._canvas.imageLoaders[imgData.source] = [callback, imgData] + } else { + // Callback directly + callback(imgData) + } } + const prerendered = Latex.findPrerendered(ltxText, this.textsize, color) + if(prerendered !== null) + onRendered(prerendered) + else + Latex.requestAsyncRender(ltxText, this.textsize, color).then(onRendered) } // @@ -519,4 +523,4 @@ class CanvasAPI extends Module { /** @type {CanvasAPI} */ Modules.Canvas = Modules.Canvas || new CanvasAPI() -export const API = Modules.Canvas \ No newline at end of file +export const API = Modules.Canvas diff --git a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/history/common.mjs b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/history/common.mjs index 229a294..3df3a77 100644 --- a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/history/common.mjs +++ b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/history/common.mjs @@ -115,12 +115,12 @@ export class Action { if(!Latex.enabled) throw new Error("Cannot render an item as LaTeX when LaTeX is disabled.") let imgDepth = Modules.History.imageDepth - let [src, width, height] = Latex.render( + let { source, width, height } = Latex.renderSync( latexString, imgDepth * (Modules.History.fontSize + 2), Modules.History.themeTextColor - ).split(",") - return `` + ) + return `` } /** diff --git a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/math/latex.mjs b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/math/latex.mjs index 7418b9c..306f5be 100644 --- a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/math/latex.mjs +++ b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/math/latex.mjs @@ -39,6 +39,20 @@ const equivalchars = ["\\alpha","\\beta","\\gamma","\\delta","\\epsilon","\\zeta "{}_{4}","{}_{5}","{}_{6}","{}_{7}","{}_{8}","{}_{9}","{}_{0}", "\\pi", "\\infty"] +/** + * Class containing the result of a LaTeX render. + * + * @property {string} source - Exported PNG file + * @property {number} width + * @property {number} height + */ +class LatexRenderResult { + constructor(source, width, height) { + this.source = source + this.width = parseFloat(width) + this.height = parseFloat(height) + } +} class LatexAPI extends Module { constructor() { @@ -50,11 +64,50 @@ class LatexAPI extends Module { * true if latex has been enabled by the user, false otherwise. */ this.enabled = Helper.getSettingBool("enable_latex") - /** - * Mirror method for Python object. - * @type {function(string, number, string): string}. - */ - this.render = Latex.render + } + + /** + * Prepares and renders a latex string into a png file. + * + * @param {string} markup - LaTeX markup to render. + * @param {number} fontSize - Font size (in pt) to render. + * @param {color} color - Color of the text to render. + * @returns {LatexRenderResult} + */ + renderSync(markup, fontSize, color) { + let args = Latex.render(markup, fontSize, color).split(",") + return new LatexRenderResult(...args) + } + + /** + * Checks if the given markup (with given font size and color) has already been + * rendered, and if so, returns its data. Otherwise, returns null. + * + * @param {string} markup - LaTeX markup to render. + * @param {number} fontSize - Font size (in pt) to render. + * @param {color} color - Color of the text to render. + * @returns {LatexRenderResult|null} + */ + findPrerendered(markup, fontSize, color) { + const data = Latex.findPrerendered(markup, fontSize, color) + let ret = null + if(data !== "") + ret = new LatexRenderResult(...data.split(",")) + return ret + } + + /** + * Prepares and renders a latex string into a png file asynchronously. + * + * @param {string} markup - LaTeX markup to render. + * @param {number} fontSize - Font size (in pt) to render. + * @param {color} color - Color of the text to render. + * @returns {Promize} + */ + requestAsyncRender(markup, fontSize, color) { + return new Promise(resolve => { + resolve(this.renderSync(markup, fontSize, color)) + }) } /** diff --git a/LogarithmPlotter/util/latex.py b/LogarithmPlotter/util/latex.py index 9ebb1c4..166e224 100644 --- a/LogarithmPlotter/util/latex.py +++ b/LogarithmPlotter/util/latex.py @@ -92,15 +92,13 @@ class Latex(QObject): except Exception as e: valid_install = False # Should have sent an error message if failed to render return valid_install - @Slot(str, float, QColor, result=str) def render(self, latex_markup: str, font_size: float, color: QColor) -> str: """ Prepares and renders a latex string into a png file. """ - markup_hash = "render" + str(hash(latex_markup)) - export_path = path.join(self.tempdir.name, f'{markup_hash}_{int(font_size)}_{color.rgb()}') + markup_hash, export_path = self.create_export_path(latex_markup, font_size, color) if self.latexSupported and not path.exists(export_path + ".png"): print("Rendering", latex_markup, export_path) # Generating file @@ -121,6 +119,28 @@ class Latex(QObject): img = QImage(export_path) # 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'{export_path}.png,{img.width()},{img.height()}' + + @Slot(str, float, QColor, result=str) + def findPrerendered(self, latex_markup: str, font_size: float, color: QColor): + """ + Finds a prerendered image and returns its data if possible, and an empty string if not. + """ + markup_hash, export_path = self.create_export_path(latex_markup, font_size, color) + data = "" + if path.exists(export_path + ".png"): + img = QImage(export_path) + data = f'{export_path}.png,{img.width()},{img.height()}' + return data + + + def create_export_path(self, latex_markup: str, font_size: float, color: QColor): + """ + Standardizes export path for renders. + """ + markup_hash = "render" + str(hash(latex_markup)) + export_path = path.join(self.tempdir.name, f'{markup_hash}_{int(font_size)}_{color.rgb()}') + return markup_hash, export_path + def create_latex_doc(self, export_path: str, latex_markup: str): """