From cf73b35a9ab1494485006ec4dafcb9653a29e106 Mon Sep 17 00:00:00 2001 From: Ad5001 Date: Tue, 15 Oct 2024 03:01:27 +0200 Subject: [PATCH] Adding experimental async LaTeX renderer (speeds up rendering ridiculously, but causes instability) --- common/src/module/interface.mjs | 10 +++++++- common/src/module/latex.mjs | 7 +++++- common/src/preferences/general.mjs | 9 ++++++- .../LogarithmPlotter/util/config.py | 1 + .../LogarithmPlotter/util/latex.py | 24 +++++++++++++++---- .../LogarithmPlotter/util/promise.py | 11 +++++---- 6 files changed, 50 insertions(+), 12 deletions(-) diff --git a/common/src/module/interface.mjs b/common/src/module/interface.mjs index b6d462f..a96621c 100644 --- a/common/src/module/interface.mjs +++ b/common/src/module/interface.mjs @@ -84,13 +84,21 @@ export class DialogInterface extends Interface { } export class LatexInterface extends Interface { + supportsAsyncRender = BOOLEAN /** * @param {string} markup - LaTeX markup to render * @param {number} fontSize - Font size (in pt) to render * @param {string} color - Color of the text to render * @returns {string} - Comma separated data of the image (source, width, height) */ - render = FUNCTION + renderSync = FUNCTION + /** + * @param {string} markup - LaTeX markup to render + * @param {number} fontSize - Font size (in pt) to render + * @param {string} color - Color of the text to render + * @returns {Promise} - Comma separated data of the image (source, width, height) + */ + renderAsync = FUNCTION /** * @param {string} markup - LaTeX markup to render * @param {number} fontSize - Font size (in pt) to render diff --git a/common/src/module/latex.mjs b/common/src/module/latex.mjs index a6aa216..830f096 100644 --- a/common/src/module/latex.mjs +++ b/common/src/module/latex.mjs @@ -112,7 +112,12 @@ class LatexAPI extends Module { */ async requestAsyncRender(markup, fontSize, color) { if(!this.initialized) throw new Error("Attempting requestAsyncRender before initialize!") - let args = this.#latex.render(markup, fontSize, color).split(",") + let render + if(this.#latex.supportsAsyncRender) + render = await this.#latex.renderAsync(markup, fontSize, color) + else + render = this.#latex.renderSync(markup, fontSize, color) + const args = render.split(",") return new LatexRenderResult(...args) } diff --git a/common/src/preferences/general.mjs b/common/src/preferences/general.mjs index 81c5e3d..4a1098f 100644 --- a/common/src/preferences/general.mjs +++ b/common/src/preferences/general.mjs @@ -46,8 +46,15 @@ class EnableLatex extends BoolSetting { } } +const ENABLE_LATEX_ASYNC = new BoolSetting( + qsTranslate("general", "Enable asynchronous LaTeX renderer (experimental)"), + "enable_latex_async", + "new" +) + export default [ CHECK_FOR_UPDATES, RESET_REDO_STACK, - new EnableLatex() + new EnableLatex(), + ENABLE_LATEX_ASYNC ] diff --git a/runtime-pyside6/LogarithmPlotter/util/config.py b/runtime-pyside6/LogarithmPlotter/util/config.py index a5ee23c..ae10adb 100644 --- a/runtime-pyside6/LogarithmPlotter/util/config.py +++ b/runtime-pyside6/LogarithmPlotter/util/config.py @@ -26,6 +26,7 @@ DEFAULT_SETTINGS = { "reset_redo_stack": True, "last_install_greet": "0", "enable_latex": False, + "enable_latex_async": False, "expression_editor": { "autoclose": True, "colorize": True, diff --git a/runtime-pyside6/LogarithmPlotter/util/latex.py b/runtime-pyside6/LogarithmPlotter/util/latex.py index aac5c5b..0d720c3 100644 --- a/runtime-pyside6/LogarithmPlotter/util/latex.py +++ b/runtime-pyside6/LogarithmPlotter/util/latex.py @@ -15,6 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . """ +from time import sleep from PySide6.QtCore import QObject, Slot, Property, QCoreApplication, Signal from PySide6.QtGui import QImage, QColor @@ -27,6 +28,9 @@ from hashlib import sha512 from shutil import which from sys import argv +from LogarithmPlotter.util import config +from LogarithmPlotter.util.promise import PyPromise + """ Searches for a valid Latex and DVIPNG (http://savannah.nongnu.org/projects/dvipng/) installation and collects the binary path in the DVIPNG_PATH variable. @@ -75,7 +79,6 @@ class Latex(QObject): dvipng to be installed on the system. """ - def __init__(self, cache_path): QObject.__init__(self) self.tempdir = path.join(cache_path, "latex") @@ -85,6 +88,10 @@ class Latex(QObject): def latexSupported(self) -> bool: return LATEX_PATH is not None and DVIPNG_PATH is not None + @Property(bool) + def supportsAsyncRender(self) -> bool: + return config.getSetting("enable_latex_async") + @Slot(result=bool) def checkLatexInstallation(self) -> bool: """ @@ -105,13 +112,20 @@ class Latex(QObject): valid_install = False else: try: - self.render("", 14, QColor(0, 0, 0, 255)) + self.renderSync("", 14, QColor(0, 0, 0, 255)) except MissingPackageException: valid_install = False # Should have sent an error message if failed to render return valid_install + @Slot(str, float, QColor, result=PyPromise) + def renderAsync(self, latex_markup: str, font_size: float, color: QColor) -> PyPromise: + """ + Prepares and renders a latex string into a png file asynchronously. + """ + return PyPromise(self.renderSync, [latex_markup, font_size, color]) + @Slot(str, float, QColor, result=str) - def render(self, latex_markup: str, font_size: float, color: QColor) -> str: + def renderSync(self, latex_markup: str, font_size: float, color: QColor) -> str: """ Prepares and renders a latex string into a png file. """ @@ -124,12 +138,14 @@ class Latex(QObject): if not path.exists(latex_path + ".dvi"): self.create_latex_doc(latex_path, latex_markup) self.convert_latex_to_dvi(latex_path) - self.cleanup(latex_path) + # self.cleanup(latex_path) # Creating four pictures of different sizes to better handle dpi. self.convert_dvi_to_png(latex_path, export_path, font_size, color) # self.convert_dvi_to_png(latex_path, export_path+"@2", font_size*2, color) # self.convert_dvi_to_png(latex_path, export_path+"@3", font_size*3, color) # self.convert_dvi_to_png(latex_path, export_path+"@4", font_size*4, color) + else: + sleep(0) 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()}' diff --git a/runtime-pyside6/LogarithmPlotter/util/promise.py b/runtime-pyside6/LogarithmPlotter/util/promise.py index 1ccfe8e..6868432 100644 --- a/runtime-pyside6/LogarithmPlotter/util/promise.py +++ b/runtime-pyside6/LogarithmPlotter/util/promise.py @@ -30,15 +30,15 @@ class PyPromiseRunner(QRunnable): """ QRunnable for running Promises in different threads. """ - def __init__(self, runner, promise): + def __init__(self, runner, promise, args): QRunnable.__init__(self) self.runner = runner self.promise = promise - print("Initialized", self.runner) + self.args = args def run(self): try: - data = self.runner() + data = self.runner(*self.args) if isinstance(data, QObject): data = data elif type(data) in [int, str, float, bool, bytes]: @@ -64,13 +64,13 @@ class PyPromise(QObject): finished = Signal((QJSValue,), (QObject,)) errored = Signal(Exception) - def __init__(self, to_run: Callable): + def __init__(self, to_run: Callable, args): QObject.__init__(self) self._fulfills = [] self._rejects = [] self.finished.connect(self._fulfill) self.errored.connect(self._reject) - self._runner = PyPromiseRunner(to_run, self) + self._runner = PyPromiseRunner(to_run, self, args) QThreadPool.globalInstance().start(self._runner) @@ -88,6 +88,7 @@ class PyPromise(QObject): self._rejects.append(PyJSValue(on_reject)) elif isinstance(on_reject, Callable): self._rejects.append(on_reject) + return self @Slot(QJSValue) @Slot(QObject)