Compare commits

...

3 commits

8 changed files with 196 additions and 55 deletions

View file

@ -116,10 +116,23 @@ Button {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: !hidden visible: !hidden
font.pixelSize: 14 font.pixelSize: 14
text: historyAction.getHTMLString().replace(/\$\{tag_color\}/g, clr) text: ""
textFormat: Text.RichText textFormat: Text.RichText
clip: true clip: true
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
Component.onCompleted: function() {
// Render HTML, might be string, but could also be a promise
const html = historyAction.getHTMLString()
if(typeof html === "string") {
label.text = html.replace(/\$\{tag_color\}/g, clr)
} else {
// Promise! We need to way to wait for it to be completed.
html.then(rendered => {
label.text = rendered.replace(/\$\{tag_color\}/g, clr)
})
}
}
} }
Rectangle { Rectangle {

View file

@ -100,10 +100,26 @@ Item {
anchors.left: parent.left anchors.left: parent.left
visible: Modules.Latex.enabled visible: Modules.Latex.enabled
property double depth: Screen.devicePixelRatio property double depth: Screen.devicePixelRatio
property var ltxInfo: visible ? Latex.render(obj.getLatexString(), depth*(parent.font.pixelSize+2), parent.color).split(",") : ["","0","0"] source: ""
source: visible ? ltxInfo[0] : "" width: 0/depth
width: parseInt(ltxInfo[1])/depth height: 0/depth
height: parseInt(ltxInfo[2])/depth
Component.onCompleted: function() {
if(Modules.Latex.enabled) {
const args = [obj.getLatexString(), depth*(parent.font.pixelSize+2), parent.color]
const prerendered = Modules.Latex.findPrerendered(...args)
if(prerendered !== null) {
source = prerendered.source
width = prerendered.width/depth
height = prerendered.height/depth
} else
Modules.Latex.requestAsyncRender(...args).then(info => {
source = info.source
width = info.width/depth
height = info.height/depth
})
}
}
} }
MouseArea { MouseArea {

View file

@ -16,9 +16,10 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {Module} from "./modules.mjs" import { Module } from "./modules.mjs"
import {textsup} from "./utils.mjs" import { textsup } from "./utils.mjs"
import {Expression} from "./mathlib.mjs" import { Expression } from "./mathlib.mjs"
import Latex from "./math/latex.mjs"
class CanvasAPI extends Module { class CanvasAPI extends Module {
@ -168,6 +169,8 @@ class CanvasAPI extends Module {
obj.draw(this) obj.draw(this)
} catch(e) { } catch(e) {
// Drawing throws an error. Generally, it's due to a new modification (or the opening of a file) // 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) this._drawingErrorDialog.showDialog(objType, obj.name, e.message)
Modules.History.undo() 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. * 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} ltxText
* @param {string} color * @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) { renderLatexImage(ltxText, color, callback) {
let [ltxSrc, ltxWidth, ltxHeight] = Latex.render(ltxText, this.textsize, color).split(",") const onRendered = (imgData) => {
let imgData = { if(!this._canvas.isImageLoaded(imgData.source) && !this._canvas.isImageLoading(imgData.source)){
"source": ltxSrc, // Wait until the image is loaded to callback.
"width": parseFloat(ltxWidth), this._canvas.loadImage(imgData.source)
"height": parseFloat(ltxHeight) this._canvas.imageLoaders[imgData.source] = [callback, imgData]
}; } else {
if(!this._canvas.isImageLoaded(ltxSrc) && !this._canvas.isImageLoading(ltxSrc)){ // Callback directly
// Wait until the image is loaded to callback. callback(imgData)
this._canvas.loadImage(ltxSrc) }
this._canvas.imageLoaders[ltxSrc] = [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} */ /** @type {CanvasAPI} */
Modules.Canvas = Modules.Canvas || new CanvasAPI() Modules.Canvas = Modules.Canvas || new CanvasAPI()
export const API = Modules.Canvas export const API = Modules.Canvas

View file

@ -109,24 +109,28 @@ export class Action {
* Renders a LaTeX-formatted string to an image and wraps it in an HTML tag in a string. * Renders a LaTeX-formatted string to an image and wraps it in an HTML tag in a string.
* *
* @param {string} latexString - Source string of the latex. * @param {string} latexString - Source string of the latex.
* @returns {string} * @returns {Promise<string>}
*/ */
renderLatexAsHtml(latexString) { renderLatexAsHtml(latexString) {
if(!Latex.enabled) if(!Latex.enabled)
throw new Error("Cannot render an item as LaTeX when LaTeX is disabled.") throw new Error("Cannot render an item as LaTeX when LaTeX is disabled.")
let imgDepth = Modules.History.imageDepth return new Promise(resolve => {
let [src, width, height] = Latex.render( let imgDepth = Modules.History.imageDepth
latexString, Latex.requestAsyncRender(
imgDepth * (Modules.History.fontSize + 2), latexString,
Modules.History.themeTextColor imgDepth * (Modules.History.fontSize + 2),
).split(",") Modules.History.themeTextColor
return `<img src="${src}" width="${parseInt(width)/imgDepth}" height="${parseInt(height)/imgDepth}" style="vertical-align: middle"/>` ).then((imgData) => {
const { source, width, height } = imgData
resolve(`<img src="${source}" width="${width/imgDepth}" height="${height/imgDepth}" style="vertical-align: middle"/>`)
})
})
} }
/** /**
* Returns a string with the HTML-formatted description of the action. * Returns a string with the HTML-formatted description of the action.
* *
* @returns {string} * @returns {string|Promise<string>}
*/ */
getHTMLString() { getHTMLString() {
return this.getReadableString() return this.getReadableString()

View file

@ -85,8 +85,9 @@ export default class EditedProperty extends Action {
} }
setReadableValues() { setReadableValues() {
this.prevString = ""; this.prevString = ""
this.nextString = ""; this.nextString = ""
this._renderPromises = []
if(this.propertyType instanceof Object) { if(this.propertyType instanceof Object) {
switch(this.propertyType.type) { switch(this.propertyType.type) {
case "Enum": case "Enum":
@ -118,8 +119,11 @@ export default class EditedProperty extends Action {
this.prevHTML = '<tt style="background: rgba(128,128,128,0.1);">&nbsp;'+this.prevString+'&nbsp;</tt>' this.prevHTML = '<tt style="background: rgba(128,128,128,0.1);">&nbsp;'+this.prevString+'&nbsp;</tt>'
this.nextHTML = '<tt style="background: rgba(128,128,128,0.1);">&nbsp;'+this.nextString+'&nbsp;</tt>' this.nextHTML = '<tt style="background: rgba(128,128,128,0.1);">&nbsp;'+this.nextString+'&nbsp;</tt>'
if(Latex.enabled && typeof this.propertyType == 'object' && this.propertyType.type === "Expression") { if(Latex.enabled && typeof this.propertyType == 'object' && this.propertyType.type === "Expression") {
this.prevHTML= this.renderLatexAsHtml(this.previousValue.latexMarkup) // Store promises so that querying can wait for them to finish.
this.nextHTML= this.renderLatexAsHtml(this.newValue.latexMarkup) this._renderPromises = [
this.renderLatexAsHtml(this.previousValue.latexMarkup).then(prev => this.prevHTML = prev),
this.renderLatexAsHtml(this.newValue.latexMarkup).then(next => this.nextHTML = prev)
]
} }
} }
@ -131,10 +135,21 @@ export default class EditedProperty extends Action {
} }
getHTMLString() { getHTMLString() {
return qsTr('%1 of %2 changed from %3 to %4.') return new Promise(resolve => {
.arg(this.targetPropertyReadable) const translation = qsTr('%1 of %2 changed from %3 to %4.')
.arg('<b style="font-size: 15px;">&nbsp;' + this.targetName + '&nbsp;</b>') .arg(this.targetPropertyReadable)
.arg(this.prevHTML) .arg('<b style="font-size: 15px;">&nbsp;' + this.targetName + '&nbsp;</b>')
.arg(this.nextHTML) // Check if we need to wait for LaTeX HTML to be rendered.
if(this.prevHTML !== undefined && this.nextHTML !== undefined)
resolve(translation.arg(this.prevHTML).arg(this.nextHTML))
else
Promise.all(this._renderPromises).then((rendered) => {
// Rendered are (potentially) two HTML strings which are defined during rendering
this.prevHTML = this.prevHTML ?? rendered[0]
this.nextHTML = this.prevHTML ?? rendered[1]
resolve(translation.arg(this.prevHTML).arg(this.nextHTML))
})
})
} }
} }

View file

@ -61,10 +61,15 @@ export default class EditedPosition extends Action {
setReadableValues() { setReadableValues() {
this.prevString = `(${this.previousXValue.toString()},${this.previousYValue.toString()})` this.prevString = `(${this.previousXValue.toString()},${this.previousYValue.toString()})`
this.nextString = `(${this.newXValue.toString()},${this.newYValue.toString()})` this.nextString = `(${this.newXValue.toString()},${this.newYValue.toString()})`
this._renderPromises = []
// Render as LaTeX // Render as LaTeX
if(Latex.enabled) { if(Latex.enabled) {
this.prevHTML = this.renderLatexAsHtml(`\\left(${this.previousXValue.latexMarkup},${this.previousYValue.latexMarkup}\\right)`) const prevMarkup = `\\left(${this.previousXValue.latexMarkup},${this.previousYValue.latexMarkup}\\right)`
this.nextHTML = this.renderLatexAsHtml(`\\left(${this.newXValue.latexMarkup},${this.newYValue.latexMarkup}\\right)`) const nextMarkup = `\\left(${this.newXValue.latexMarkup},${this.newYValue.latexMarkup}\\right)`
this._renderPromises = [ // Will be taken in promise.all
this.renderLatexAsHtml(prevMarkup),
this.renderLatexAsHtml(nextMarkup)
]
} else { } else {
this.prevHTML = '<tt style="background: rgba(128,128,128,0.1);">&nbsp;'+escapeHTML(this.prevString)+'&nbsp;</tt>' this.prevHTML = '<tt style="background: rgba(128,128,128,0.1);">&nbsp;'+escapeHTML(this.prevString)+'&nbsp;</tt>'
this.nextHTML = '<tt style="background: rgba(128,128,128,0.1);">&nbsp;'+escapeHTML(this.nextString)+'&nbsp;</tt>' this.nextHTML = '<tt style="background: rgba(128,128,128,0.1);">&nbsp;'+escapeHTML(this.nextString)+'&nbsp;</tt>'
@ -85,9 +90,20 @@ export default class EditedPosition extends Action {
} }
getHTMLString() { getHTMLString() {
return qsTr('Position of %1 set from %2 to %3.') return new Promise(resolve => {
.arg('<b style="font-size: 15px;">&nbsp;' + this.targetName + '&nbsp;</b>') const translation = qsTr('Position of %1 set from %2 to %3.')
.arg(this.prevHTML) .arg('<b style="font-size: 15px;">&nbsp;' + this.targetName + '&nbsp;</b>')
.arg(this.nextHTML) // Check if we need to wait for LaTeX HTML to be rendered.
if(this.prevHTML !== undefined && this.nextHTML !== undefined)
resolve(translation.arg(this.prevHTML).arg(this.nextHTML))
else
Promise.all(this._renderPromises).then((rendered) => {
// Rendered are (potentially) two HTML strings which are defined during rendering
this.prevHTML = this.prevHTML ?? rendered[0]
this.nextHTML = this.prevHTML ?? rendered[1]
resolve(translation.arg(this.prevHTML).arg(this.nextHTML))
})
})
} }
} }

View file

@ -39,6 +39,20 @@ const equivalchars = ["\\alpha","\\beta","\\gamma","\\delta","\\epsilon","\\zeta
"{}_{4}","{}_{5}","{}_{6}","{}_{7}","{}_{8}","{}_{9}","{}_{0}", "{}_{4}","{}_{5}","{}_{6}","{}_{7}","{}_{8}","{}_{9}","{}_{0}",
"\\pi", "\\infty"] "\\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 { class LatexAPI extends Module {
constructor() { constructor() {
@ -50,11 +64,50 @@ class LatexAPI extends Module {
* true if latex has been enabled by the user, false otherwise. * true if latex has been enabled by the user, false otherwise.
*/ */
this.enabled = Helper.getSettingBool("enable_latex") this.enabled = Helper.getSettingBool("enable_latex")
/** }
* Mirror method for Python object.
* @type {function(string, number, string): string}. /**
*/ * Prepares and renders a latex string into a png file.
this.render = Latex.render *
* @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<LatexRenderResult>}
*/
requestAsyncRender(markup, fontSize, color) {
return new Promise(resolve => {
resolve(this.renderSync(markup, fontSize, color))
})
} }
/** /**

View file

@ -92,15 +92,13 @@ class Latex(QObject):
except Exception as e: except Exception as e:
valid_install = False # Should have sent an error message if failed to render valid_install = False # Should have sent an error message if failed to render
return valid_install return valid_install
@Slot(str, float, QColor, result=str) @Slot(str, float, QColor, result=str)
def render(self, latex_markup: str, font_size: float, color: QColor) -> str: def render(self, latex_markup: str, font_size: float, color: QColor) -> str:
""" """
Prepares and renders a latex string into a png file. Prepares and renders a latex string into a png file.
""" """
markup_hash = "render" + str(hash(latex_markup)) markup_hash, export_path = self.create_export_path(latex_markup, font_size, color)
export_path = path.join(self.tempdir.name, f'{markup_hash}_{int(font_size)}_{color.rgb()}')
if self.latexSupported and not path.exists(export_path + ".png"): if self.latexSupported and not path.exists(export_path + ".png"):
print("Rendering", latex_markup, export_path) print("Rendering", latex_markup, export_path)
# Generating file # Generating file
@ -121,6 +119,28 @@ class Latex(QObject):
img = QImage(export_path) 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 # 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()}' 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): def create_latex_doc(self, export_path: str, latex_markup: str):
""" """