Starting latex render locking.

This commit is contained in:
Adsooi 2024-10-15 19:21:40 +02:00
parent ccddb068a6
commit aeaaba759f
Signed by: Ad5001
GPG key ID: EF45F9C6AFE20160
4 changed files with 99 additions and 17 deletions

View file

@ -529,11 +529,15 @@ class CanvasAPI extends Module {
this.#canvas.loadImageAsync(imgData.source).then(() => {
if(this.#redrawCount === currentRedrawCount)
callback(imgData)
else
console.log("2Discard render of", imgData.source, this.#redrawCount, currentRedrawCount)
})
} else {
// Callback directly
if(this.#redrawCount === currentRedrawCount)
callback(imgData)
else
console.log("2Discard render of", imgData.source, this.#redrawCount, currentRedrawCount)
}
}
const prerendered = Latex.findPrerendered(ltxText, this.textsize, color)

Binary file not shown.

View file

@ -82,6 +82,7 @@ class Latex(QObject):
def __init__(self, cache_path):
QObject.__init__(self)
self.tempdir = path.join(cache_path, "latex")
self.render_pipeline_locks = {}
makedirs(self.tempdir, exist_ok=True)
@Property(bool)
@ -117,19 +118,70 @@ class Latex(QObject):
valid_install = False # Should have sent an error message if failed to render
return valid_install
def lock(self, markup_hash, render_hash, promise):
"""
Locks the render pipeline for a given markup hash and render hash.
"""
# print("Locking", markup_hash, render_hash)
if markup_hash not in self.render_pipeline_locks:
self.render_pipeline_locks[markup_hash] = promise
self.render_pipeline_locks[render_hash] = promise
def release_lock(self, markup_hash, render_hash):
"""
Release locks on the markup and render hashes.
"""
# print("Releasing", markup_hash, render_hash)
if markup_hash in self.render_pipeline_locks:
del self.render_pipeline_locks[markup_hash]
del self.render_pipeline_locks[render_hash]
@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])
markup_hash, render_hash, export_path = self.create_export_path(latex_markup, font_size, color)
promise = None
if render_hash in self.render_pipeline_locks:
# A PyPromise for this specific render is already running.
# print("Already running render of", latex_markup)
promise = self.render_pipeline_locks[render_hash]
elif markup_hash in self.render_pipeline_locks:
# A PyPromise with the same markup, but not the same color or font size is already running.
print("Chaining render of", latex_markup)
existing_promise = self.render_pipeline_locks[markup_hash]
promise = self._create_async_promise(latex_markup, font_size, color)
existing_promise.then(lambda x, latex_markup=latex_markup: print("> Starting chained render of", latex_markup))
promise.then(lambda x, latex_markup=latex_markup: print("> Fulfilled chained render of", latex_markup, "\n with", x.toVariant()))
existing_promise.then(promise.start)
else:
# No such PyPromise is running.
promise = self._create_async_promise(latex_markup, font_size, color)
promise.start()
return promise
def _create_async_promise(self, latex_markup: str, font_size: float, color: QColor) -> PyPromise:
"""
Createsa PyPromise to render a latex string into a PNG file.
Internal method. Use renderAsync that makes use of locks.
"""
markup_hash, render_hash, export_path = self.create_export_path(latex_markup, font_size, color)
promise = PyPromise(self.renderSync, [latex_markup, font_size, color], start_automatically=False)
self.lock(markup_hash, render_hash, promise)
# Make the lock release at the end.
def unlock(data, markup_hash=markup_hash, render_hash=render_hash):
self.release_lock(markup_hash, render_hash)
promise.then(unlock, unlock)
return promise
@Slot(str, float, QColor, result=str)
def renderSync(self, latex_markup: str, font_size: float, color: QColor) -> str:
"""
Prepares and renders a latex string into a png file.
"""
markup_hash, export_path = self.create_export_path(latex_markup, font_size, color)
markup_hash, render_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
@ -138,14 +190,12 @@ 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()}'
@ -155,7 +205,7 @@ class Latex(QObject):
"""
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)
markup_hash, render_hash, export_path = self.create_export_path(latex_markup, font_size, color)
data = ""
if path.exists(export_path + ".png"):
img = QImage(export_path)
@ -165,10 +215,13 @@ class Latex(QObject):
def create_export_path(self, latex_markup: str, font_size: float, color: QColor):
"""
Standardizes export path for renders.
Markup hash is unique for the markup
Render hash is unique for the markup, the font size and the color.
"""
markup_hash = "render" + str(sha512(latex_markup.encode()).hexdigest())
export_path = path.join(self.tempdir, f'{markup_hash}_{int(font_size)}_{color.rgb()}')
return markup_hash, export_path
render_hash = f'{markup_hash}_{int(font_size)}_{color.rgb()}'
export_path = path.join(self.tempdir, render_hash)
return markup_hash, render_hash, export_path
def create_latex_doc(self, export_path: str, latex_markup: str):
"""

View file

@ -23,6 +23,18 @@ from PySide6.QtQml import QJSValue
from LogarithmPlotter.util.js import PyJSValue
def check_callable(function: Callable|QJSValue) -> Callable|None:
"""
Checks if the given function can be called (either a python callable
or a QJSValue function), and returns the object that can be called directly.
Returns None if not a function.
"""
if isinstance(function, QJSValue) and function.isCallable():
return PyJSValue(function)
elif isinstance(function, Callable):
return function
return None
class InvalidReturnValue(Exception): pass
@ -69,15 +81,29 @@ class PyPromise(QObject):
fulfilled = Signal((QJSValue,), (QObject,))
rejected = Signal(Exception)
def __init__(self, to_run: Callable, args=[]):
def __init__(self, to_run: Callable|QJSValue, args=[], start_automatically=True):
QObject.__init__(self)
self._fulfills = []
self._rejects = []
self._state = "pending"
self._started = False
self.fulfilled.connect(self._fulfill)
self.rejected.connect(self._reject)
to_run = check_callable(to_run)
if to_run is None:
raise ValueError("New PyPromise created with invalid function")
self._runner = PyPromiseRunner(to_run, self, args)
if start_automatically:
self._start()
@Slot()
def start(self, *args, **kwargs):
"""
Starts the thread that will run the promise.
"""
if not self._started: # Avoid getting started twice.
QThreadPool.globalInstance().start(self._runner)
self._started = True
@Property(str)
def state(self):
@ -89,13 +115,11 @@ class PyPromise(QObject):
"""
Adds listeners for both fulfilment and catching errors of the Promise.
"""
if isinstance(on_fulfill, QJSValue):
self._fulfills.append(PyJSValue(on_fulfill))
elif isinstance(on_fulfill, Callable):
on_fulfill = check_callable(on_fulfill)
on_reject = check_callable(on_reject)
if on_fulfill is not None:
self._fulfills.append(on_fulfill)
if isinstance(on_reject, QJSValue):
self._rejects.append(PyJSValue(on_reject))
elif isinstance(on_reject, Callable):
if on_reject is not None:
self._rejects.append(on_reject)
return self
@ -107,6 +131,7 @@ class PyPromise(QObject):
for on_fulfill in self._fulfills:
try:
result = on_fulfill(data)
result = result.qjs_value if isinstance(result, PyJSValue) else result
data = result if result not in no_return else data # Forward data.
except Exception as e:
self._reject(repr(e))