diff --git a/LogarithmPlotter/logarithmplotter.py b/LogarithmPlotter/logarithmplotter.py index 56985e2..70073ef 100644 --- a/LogarithmPlotter/logarithmplotter.py +++ b/LogarithmPlotter/logarithmplotter.py @@ -18,8 +18,6 @@ from time import time -start_time = time() - from PySide2.QtWidgets import QApplication from PySide2.QtQml import QQmlApplicationEngine from PySide2.QtCore import Qt, QTranslator, QLocale @@ -29,6 +27,9 @@ from tempfile import TemporaryDirectory from os import getcwd, chdir, environ, path, remove, close from platform import release as os_release from sys import platform, argv, version as sys_version, exit +from sys import path as sys_path + +start_time = time() # Create the temporary directory for saving copied screenshots and latex files tempdir = TemporaryDirectory() @@ -37,7 +38,6 @@ pwd = getcwd() chdir(path.dirname(path.realpath(__file__))) -from sys import path as sys_path if path.realpath(path.join(getcwd(), "..")) not in sys_path: sys_path.append(path.realpath(path.join(getcwd(), ".."))) @@ -85,11 +85,11 @@ def run(): icon_fallbacks.append(path.realpath(path.join(base_icon_path, "settings", "custom"))) QIcon.setFallbackSearchPaths(icon_fallbacks); + QApplication.setAttribute(Qt.AA_EnableHighDpiScaling) app = QApplication(argv) app.setApplicationName("LogarithmPlotter") app.setOrganizationName("Ad5001") app.styleHints().setShowShortcutsInContextMenus(True) - app.setAttribute(Qt.AA_EnableHighDpiScaling) app.setWindowIcon(QIcon(path.realpath(path.join(getcwd(), "logarithmplotter.svg")))) # Installing translators @@ -109,7 +109,7 @@ def run(): engine = QQmlApplicationEngine() global tmpfile helper = Helper(pwd, tmpfile) - latex = Latex(tempdir, app.palette()) + latex = Latex(tempdir) engine.rootContext().setContextProperty("Helper", helper) engine.rootContext().setContextProperty("Latex", latex) engine.rootContext().setContextProperty("TestBuild", "--test-build" in argv) @@ -135,6 +135,8 @@ def run(): if platform == "darwin": macOSFileOpenHandler.init_graphics(engine.rootObjects()[0]) + latex.check_latex_install() + # Check for updates if config.getSetting("check_for_updates"): check_for_updates(__VERSION__, engine.rootObjects()[0]) diff --git a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/LogGraphCanvas.qml b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/LogGraphCanvas.qml index a819da1..19b82e9 100644 --- a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/LogGraphCanvas.qml +++ b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/LogGraphCanvas.qml @@ -200,7 +200,7 @@ Canvas { // Reset ctx.fillStyle = "#FFFFFF" ctx.strokeStyle = "#000000" - ctx.font = `${canvas.textsize-2}px sans-serif` + ctx.font = `${canvas.textsize}px sans-serif` ctx.fillRect(0,0,width,height) } @@ -257,12 +257,12 @@ Canvas { var axisxpx = y2px(0) // Y coordinate of X axis // Labels ctx.fillStyle = "#000000" - ctx.font = `${canvas.textsize+2}px sans-serif` + ctx.font = `${canvas.textsize}px sans-serif` ctx.fillText(ylabel, axisypx+10, 24) var textSize = ctx.measureText(xlabel).width ctx.fillText(xlabel, canvasSize.width-14-textSize, axisxpx-5) // Axis graduation labels - ctx.font = `${canvas.textsize-2}px sans-serif` + ctx.font = `${canvas.textsize-4}px sans-serif` var txtMinus = ctx.measureText('-').width if(showxgrad) { diff --git a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/Settings.qml b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/Settings.qml index 2115b6b..6bde410 100644 --- a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/Settings.qml +++ b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/Settings.qml @@ -99,7 +99,7 @@ ScrollView { Font size of the text that will be drawn into the canvas, provided from settings. \sa Settings */ - property double textsize: 14 + property double textsize: 18 /*! \qmlproperty bool Settings::logscalex true if the canvas should be in logarithmic mode, false otherwise. diff --git a/LogarithmPlotter/util/latex.py b/LogarithmPlotter/util/latex.py index 07b4b9d..e76fd3e 100644 --- a/LogarithmPlotter/util/latex.py +++ b/LogarithmPlotter/util/latex.py @@ -16,23 +16,156 @@ * along with this program. If not, see . """ -from PySide2.QtCore import QObject, Slot +from PySide2.QtCore import QObject, Slot, Property, QCoreApplication from PySide2.QtGui import QImage, QColor -from PySide2.QtWidgets import QApplication +from PySide2.QtWidgets import QApplication, QMessageBox -from os import path -from sympy import preview +from os import path, remove +from string import Template from tempfile import TemporaryDirectory +from subprocess import Popen, TimeoutExpired, PIPE +from platform import system +from shutil import which +""" +Searches for a valid Latex and DVIPNG (http://savannah.nongnu.org/projects/dvipng/) +installation and collects the binary path in the DVIPNG_PATH variable. +If not found, it will send an alert to the user. +""" +LATEX_PATH = which('latex') +DVIPNG_PATH = which('dvipng') +#subprocess.run(["ls", "-l", "/dev/null"], capture_output=True) + +DEFAULT_LATEX_DOC = Template(r""" +\documentclass[]{minimal} +\usepackage[utf8]{inputenc} +\usepackage{calligra} +\usepackage{amsfonts} + +\title{} +\author{} + +\begin{document} + +$$$$ $markup $$$$ + +\end{document} +""") + class Latex(QObject): - def __init__(self, tempdir: str, palette): + """ + Base class to convert Latex equations into PNG images with custom font color and size. + It doesn't have any python dependency, but requires a working latex installation and + dvipng to be installed on the system. + """ + def __init__(self, tempdir: TemporaryDirectory): QObject.__init__(self) self.tempdir = tempdir - self.palette = palette - fg = self.palette.windowText().color().convertTo(QColor.Rgb) + + def check_latex_install(self): + """ + Checks if the current latex installation is valid. + """ + if LATEX_PATH is None: + QMessageBox.warning(None, "LogarithmPlotter - Latex setup", QCoreApplication.translate("latex", "No Latex installation found.\nIf you already have a latex distribution installed, make sure it's installed on your path.\nOtherwise, you can download a Latex distribution like TeX Live at https://tug.org/texlive/.")) + elif DVIPNG_PATH is None: + QMessageBox.warning(None, "LogarithmPlotter - Latex setup", QCoreApplication.translate("latex", "DVIPNG was not found. Make sure you include it from your Latex distribution.")) + + @Property(bool) + def latexSupported(self): + return LATEX_PATH is not None and DVIPNG_PATH is not None @Slot(str, float, QColor, result=str) - def render(self, latexstring, font_size, color = True): + def render(self, latex_markup: str, font_size: float, color: QColor = True) -> str: + """ + Renders a latex string into a png file. + """ + export_path = path.join(self.tempdir.name, f'{hash(latex_markup)}_{font_size}_{color.rgb()}') + print(export_path) + if self.latexSupported and not path.exists(export_path + ".png"): + # Generating file + try: + self.create_latex_doc(export_path, latex_markup) + self.convert_latex_to_dvi(export_path) + self.convert_dvi_to_png(export_path, font_size, color) + self.cleanup(export_path) + except Exception as e: # One of the processes failed. A message will be sent every time. + raise e + img = QImage(export_path + ".png"); + # 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()}' + + def create_latex_doc(self, export_path: str, latex_markup: str): + """ + Creates a temporary latex document with base file_hash as file name and a given expression markup latex_markup. + """ + ltx_path = export_path + ".tex" + f = open(export_path + ".tex", 'w') + f.write(DEFAULT_LATEX_DOC.substitute(markup = latex_markup)) + f.close() + + def convert_latex_to_dvi(self, export_path: str): + """ + Converts a DVI file to a PNG file. + """ + self.run([ + LATEX_PATH, + export_path + ".tex" + ]) + + + def convert_dvi_to_png(self, export_path: str, font_size: float, color: QColor): + """ + Converts a DVI file to a PNG file. + Documentation: https://linux.die.net/man/1/dvipng + """ + fg = color.convertTo(QColor.Rgb) + fg = f'rgb {fg.redF()} {fg.greenF()} {fg.blueF()}' + depth = int(font_size * 72.27 / 100) * 10 + self.run([ + DVIPNG_PATH, + '-T', 'tight', # Make sure image borders are as tight around the equation as possible to avoid blank space. + '--truecolor', # Make sure it's rendered in 24 bit colors. + '-D',f'{depth}', # Depth of the image + '-bg', 'Transparent', # Transparent background + '-fg',f'{fg}', # Foreground of the wanted color. + f'{export_path}.dvi', # Input file + '-o',f'{export_path}.png', # Output file + ]) + + def run(self, process: list): + """ + Runs a subprocess and handles exceptions and messages them to the user. + """ + proc = Popen(process, stdout=PIPE, stderr=PIPE, cwd=self.tempdir.name) + try: + out, err = proc.communicate(timeout=5) # 5 seconds is already FAR too long. + if proc.returncode != 0: + # Process errored + QMessageBox.warning(None, "LogarithmPlotter - Latex", + QCoreApplication.translate("latex", "An exception occured within the creation of the latex formula.\nProcess '{}' ended with a non-zero return code {}:\n{}\nPlease make sure your latex installation is correct and report a bug if so.") + .format(" ".join(process), proc.returncode, str(out, 'utf8')+"\n"+str(err,'utf8'))) + raise Exception(" ".join(process) + " process exited with return code " + str(proc.returncode) + ":\n" + str(out, 'utf8')+"\n"+str(err,'utf8')) + print(out) + except TimeoutExpired as e: + # Process timed out + proc.kill() + out, err = proc.communicate() + QMessageBox.warning(None, "LogarithmPlotter - Latex", + QCoreApplication.translate("latex", "An exception occured within the creation of the latex formula.\nProcess '{}' took too long to finish:\n{}\nPlease make sure your latex installation is correct and report a bug if so.") + .format(" ".join(process), str(out, 'utf8')+"\n"+str(err,'utf8'))) + raise Exception(" ".join(process) + " process timed out:\n" + str(out, 'utf8')+"\n"+str(err,'utf8')) + + def cleanup(self, export_path): + """ + Removes Tex, auxiliary, logs and DVI temporary files. + """ + for i in [".tex", ".dvi", ".aux", ".log"]: + remove(export_path + i) + + + @Slot(str, float, QColor, result=str) + def render_legacy(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): @@ -49,9 +182,3 @@ class Latex(QObject): 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(self.render(latexstring)) diff --git a/linux/debian/control b/linux/debian/control index a4fe523..6379384 100644 --- a/linux/debian/control +++ b/linux/debian/control @@ -3,7 +3,7 @@ Source: logarithmplotter Version: 0.1.9 Architecture: all Maintainer: Ad5001 -Depends: python3, python3-pip, qml-module-qtquick-controls2 (>= 5.12.0), qml-module-qtmultimedia (>= 5.12.0), qml-module-qtgraphicaleffects (>= 5.12.0), qml-module-qtquick2 (>= 5.12.0), qml-module-qtqml-models2 (>= 5.12.0), qml-module-qtquick-controls (>= 5.12.0), python3-pyside2.qtcore (>= 5.12.0), python3-pyside2.qtqml (>= 5.12.0), python3-pyside2.qtgui (>= 5.12.0), python3-pyside2.qtquick (>= 5.12.0), python3-pyside2.qtwidgets (>= 5.12.0), python3-pyside2.qtmultimedia (>= 5.12.0), python3-pyside2.qtnetwork (>= 5.12.0), python3-sympy +Depends: python3, python3-pip, qml-module-qtquick-controls2 (>= 5.12.0), qml-module-qtmultimedia (>= 5.12.0), qml-module-qtgraphicaleffects (>= 5.12.0), qml-module-qtquick2 (>= 5.12.0), qml-module-qtqml-models2 (>= 5.12.0), qml-module-qtquick-controls (>= 5.12.0), python3-pyside2.qtcore (>= 5.12.0), python3-pyside2.qtqml (>= 5.12.0), python3-pyside2.qtgui (>= 5.12.0), python3-pyside2.qtquick (>= 5.12.0), python3-pyside2.qtwidgets (>= 5.12.0), python3-pyside2.qtmultimedia (>= 5.12.0), python3-pyside2.qtnetwork (>= 5.12.0), texlive-latex-base, dvipng Build-Depends: debhelper (>=11~), dh-python, dpkg-dev (>= 1.16.1~), python-setuptools, python3-all-dev (>=3.6) Section: science diff --git a/linux/debian/depends b/linux/debian/depends index 725059d..53dcda3 100644 --- a/linux/debian/depends +++ b/linux/debian/depends @@ -1 +1 @@ -python3-pip, qml-module-qtquick-controls2 (>= 5.12.0), qml-module-qtmultimedia (>= 5.12.0), qml-module-qtgraphicaleffects (>= 5.12.0), qml-module-qtquick2 (>= 5.12.0), qml-module-qtqml-models2 (>= 5.12.0), qml-module-qtquick-controls (>= 5.12.0), python3-pyside2.qtcore (>= 5.12.0), python3-pyside2.qtqml (>= 5.12.0), python3-pyside2.qtgui (>= 5.12.0), python3-pyside2.qtquick (>= 5.12.0), python3-pyside2.qtwidgets (>= 5.12.0), python3-pyside2.qtmultimedia (>= 5.12.0), python3-pyside2.qtnetwork (>= 5.12.0), python3-sympy +python3-pip, qml-module-qtquick-controls2 (>= 5.12.0), qml-module-qtmultimedia (>= 5.12.0), qml-module-qtgraphicaleffects (>= 5.12.0), qml-module-qtquick2 (>= 5.12.0), qml-module-qtqml-models2 (>= 5.12.0), qml-module-qtquick-controls (>= 5.12.0), python3-pyside2.qtcore (>= 5.12.0), python3-pyside2.qtqml (>= 5.12.0), python3-pyside2.qtgui (>= 5.12.0), python3-pyside2.qtquick (>= 5.12.0), python3-pyside2.qtwidgets (>= 5.12.0), python3-pyside2.qtmultimedia (>= 5.12.0), python3-pyside2.qtnetwork (>= 5.12.0), texlive-latex-base, dvipng diff --git a/setup.py b/setup.py index b7817bf..20568b5 100644 --- a/setup.py +++ b/setup.py @@ -127,7 +127,7 @@ if sys.platform == 'linux': os.remove(os.environ["PREFIX"] + '/icons/hicolor/scalable/apps/logplotter.svg') setuptools.setup( - install_requires=([] if "FLATPAK_INSTALL" in os.environ else ["PySide2", "sympy"]), + install_requires=([] if "FLATPAK_INSTALL" in os.environ else ["PySide2"]), python_requires='>=3.8', name='logarithmplotter',