Removed dependency on Sympy for subprocesses directly.
New dependencies: latex, dvipng. Slight changes to default for fonts to avoid too many anti aliasing issues. Also adds proper checks for latex installation.
This commit is contained in:
parent
8251504fbe
commit
2ce66df4dd
7 changed files with 155 additions and 26 deletions
|
@ -18,8 +18,6 @@
|
||||||
|
|
||||||
from time import time
|
from time import time
|
||||||
|
|
||||||
start_time = time()
|
|
||||||
|
|
||||||
from PySide2.QtWidgets import QApplication
|
from PySide2.QtWidgets import QApplication
|
||||||
from PySide2.QtQml import QQmlApplicationEngine
|
from PySide2.QtQml import QQmlApplicationEngine
|
||||||
from PySide2.QtCore import Qt, QTranslator, QLocale
|
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 os import getcwd, chdir, environ, path, remove, close
|
||||||
from platform import release as os_release
|
from platform import release as os_release
|
||||||
from sys import platform, argv, version as sys_version, exit
|
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
|
# Create the temporary directory for saving copied screenshots and latex files
|
||||||
tempdir = TemporaryDirectory()
|
tempdir = TemporaryDirectory()
|
||||||
|
@ -37,7 +38,6 @@ pwd = getcwd()
|
||||||
|
|
||||||
chdir(path.dirname(path.realpath(__file__)))
|
chdir(path.dirname(path.realpath(__file__)))
|
||||||
|
|
||||||
from sys import path as sys_path
|
|
||||||
if path.realpath(path.join(getcwd(), "..")) not in sys_path:
|
if path.realpath(path.join(getcwd(), "..")) not in sys_path:
|
||||||
sys_path.append(path.realpath(path.join(getcwd(), "..")))
|
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")))
|
icon_fallbacks.append(path.realpath(path.join(base_icon_path, "settings", "custom")))
|
||||||
QIcon.setFallbackSearchPaths(icon_fallbacks);
|
QIcon.setFallbackSearchPaths(icon_fallbacks);
|
||||||
|
|
||||||
|
QApplication.setAttribute(Qt.AA_EnableHighDpiScaling)
|
||||||
app = QApplication(argv)
|
app = QApplication(argv)
|
||||||
app.setApplicationName("LogarithmPlotter")
|
app.setApplicationName("LogarithmPlotter")
|
||||||
app.setOrganizationName("Ad5001")
|
app.setOrganizationName("Ad5001")
|
||||||
app.styleHints().setShowShortcutsInContextMenus(True)
|
app.styleHints().setShowShortcutsInContextMenus(True)
|
||||||
app.setAttribute(Qt.AA_EnableHighDpiScaling)
|
|
||||||
app.setWindowIcon(QIcon(path.realpath(path.join(getcwd(), "logarithmplotter.svg"))))
|
app.setWindowIcon(QIcon(path.realpath(path.join(getcwd(), "logarithmplotter.svg"))))
|
||||||
|
|
||||||
# Installing translators
|
# Installing translators
|
||||||
|
@ -109,7 +109,7 @@ def run():
|
||||||
engine = QQmlApplicationEngine()
|
engine = QQmlApplicationEngine()
|
||||||
global tmpfile
|
global tmpfile
|
||||||
helper = Helper(pwd, tmpfile)
|
helper = Helper(pwd, tmpfile)
|
||||||
latex = Latex(tempdir, app.palette())
|
latex = Latex(tempdir)
|
||||||
engine.rootContext().setContextProperty("Helper", helper)
|
engine.rootContext().setContextProperty("Helper", helper)
|
||||||
engine.rootContext().setContextProperty("Latex", latex)
|
engine.rootContext().setContextProperty("Latex", latex)
|
||||||
engine.rootContext().setContextProperty("TestBuild", "--test-build" in argv)
|
engine.rootContext().setContextProperty("TestBuild", "--test-build" in argv)
|
||||||
|
@ -135,6 +135,8 @@ def run():
|
||||||
if platform == "darwin":
|
if platform == "darwin":
|
||||||
macOSFileOpenHandler.init_graphics(engine.rootObjects()[0])
|
macOSFileOpenHandler.init_graphics(engine.rootObjects()[0])
|
||||||
|
|
||||||
|
latex.check_latex_install()
|
||||||
|
|
||||||
# Check for updates
|
# Check for updates
|
||||||
if config.getSetting("check_for_updates"):
|
if config.getSetting("check_for_updates"):
|
||||||
check_for_updates(__VERSION__, engine.rootObjects()[0])
|
check_for_updates(__VERSION__, engine.rootObjects()[0])
|
||||||
|
|
|
@ -200,7 +200,7 @@ Canvas {
|
||||||
// Reset
|
// Reset
|
||||||
ctx.fillStyle = "#FFFFFF"
|
ctx.fillStyle = "#FFFFFF"
|
||||||
ctx.strokeStyle = "#000000"
|
ctx.strokeStyle = "#000000"
|
||||||
ctx.font = `${canvas.textsize-2}px sans-serif`
|
ctx.font = `${canvas.textsize}px sans-serif`
|
||||||
ctx.fillRect(0,0,width,height)
|
ctx.fillRect(0,0,width,height)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -257,12 +257,12 @@ Canvas {
|
||||||
var axisxpx = y2px(0) // Y coordinate of X axis
|
var axisxpx = y2px(0) // Y coordinate of X axis
|
||||||
// Labels
|
// Labels
|
||||||
ctx.fillStyle = "#000000"
|
ctx.fillStyle = "#000000"
|
||||||
ctx.font = `${canvas.textsize+2}px sans-serif`
|
ctx.font = `${canvas.textsize}px sans-serif`
|
||||||
ctx.fillText(ylabel, axisypx+10, 24)
|
ctx.fillText(ylabel, axisypx+10, 24)
|
||||||
var textSize = ctx.measureText(xlabel).width
|
var textSize = ctx.measureText(xlabel).width
|
||||||
ctx.fillText(xlabel, canvasSize.width-14-textSize, axisxpx-5)
|
ctx.fillText(xlabel, canvasSize.width-14-textSize, axisxpx-5)
|
||||||
// Axis graduation labels
|
// Axis graduation labels
|
||||||
ctx.font = `${canvas.textsize-2}px sans-serif`
|
ctx.font = `${canvas.textsize-4}px sans-serif`
|
||||||
|
|
||||||
var txtMinus = ctx.measureText('-').width
|
var txtMinus = ctx.measureText('-').width
|
||||||
if(showxgrad) {
|
if(showxgrad) {
|
||||||
|
|
|
@ -99,7 +99,7 @@ ScrollView {
|
||||||
Font size of the text that will be drawn into the canvas, provided from settings.
|
Font size of the text that will be drawn into the canvas, provided from settings.
|
||||||
\sa Settings
|
\sa Settings
|
||||||
*/
|
*/
|
||||||
property double textsize: 14
|
property double textsize: 18
|
||||||
/*!
|
/*!
|
||||||
\qmlproperty bool Settings::logscalex
|
\qmlproperty bool Settings::logscalex
|
||||||
true if the canvas should be in logarithmic mode, false otherwise.
|
true if the canvas should be in logarithmic mode, false otherwise.
|
||||||
|
|
|
@ -16,23 +16,156 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from PySide2.QtCore import QObject, Slot
|
from PySide2.QtCore import QObject, Slot, Property, QCoreApplication
|
||||||
from PySide2.QtGui import QImage, QColor
|
from PySide2.QtGui import QImage, QColor
|
||||||
from PySide2.QtWidgets import QApplication
|
from PySide2.QtWidgets import QApplication, QMessageBox
|
||||||
|
|
||||||
from os import path
|
from os import path, remove
|
||||||
from sympy import preview
|
from string import Template
|
||||||
from tempfile import TemporaryDirectory
|
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):
|
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)
|
QObject.__init__(self)
|
||||||
self.tempdir = tempdir
|
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)
|
@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')
|
exprpath = path.join(self.tempdir.name, f'{hash(latexstring)}_{font_size}_{color.rgb()}.png')
|
||||||
print("Rendering", latexstring, exprpath)
|
print("Rendering", latexstring, exprpath)
|
||||||
if not path.exists(exprpath):
|
if not path.exists(exprpath):
|
||||||
|
@ -49,9 +182,3 @@ class Latex(QObject):
|
||||||
img = QImage(exprpath);
|
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
|
# 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()}'
|
return f'{exprpath},{img.width()},{img.height()}'
|
||||||
|
|
||||||
@Slot(str)
|
|
||||||
def copyLatexImageToClipboard(self, latexstring):
|
|
||||||
global tempfile
|
|
||||||
clipboard = QApplication.clipboard()
|
|
||||||
clipboard.setImage(self.render(latexstring))
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ Source: logarithmplotter
|
||||||
Version: 0.1.9
|
Version: 0.1.9
|
||||||
Architecture: all
|
Architecture: all
|
||||||
Maintainer: Ad5001 <mail@ad5001.eu>
|
Maintainer: Ad5001 <mail@ad5001.eu>
|
||||||
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)
|
Build-Depends: debhelper (>=11~), dh-python, dpkg-dev (>= 1.16.1~), python-setuptools, python3-all-dev (>=3.6)
|
||||||
Section: science
|
Section: science
|
||||||
|
|
|
@ -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
|
||||||
|
|
2
setup.py
2
setup.py
|
@ -127,7 +127,7 @@ if sys.platform == 'linux':
|
||||||
os.remove(os.environ["PREFIX"] + '/icons/hicolor/scalable/apps/logplotter.svg')
|
os.remove(os.environ["PREFIX"] + '/icons/hicolor/scalable/apps/logplotter.svg')
|
||||||
|
|
||||||
setuptools.setup(
|
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',
|
python_requires='>=3.8',
|
||||||
|
|
||||||
name='logarithmplotter',
|
name='logarithmplotter',
|
||||||
|
|
Loading…
Reference in a new issue