2022-03-05 17:19:20 +00:00
"""
* LogarithmPlotter - 2 D plotter software to make BODE plots , sequences and distribution functions .
* Copyright ( C ) 2022 Ad5001
*
* This program is free software : you can redistribute it and / or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation , either version 3 of the License , or
* ( at your option ) any later version .
*
* This program is distributed in the hope that it will be useful ,
* but WITHOUT ANY WARRANTY ; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
* GNU General Public License for more details .
*
* You should have received a copy of the GNU General Public License
* along with this program . If not , see < https : / / www . gnu . org / licenses / > .
"""
2022-03-06 22:34:59 +00:00
from PySide2 . QtCore import QObject , Slot , Property , QCoreApplication
2022-03-05 17:19:20 +00:00
from PySide2 . QtGui import QImage , QColor
2022-03-06 22:34:59 +00:00
from PySide2 . QtWidgets import QApplication , QMessageBox
2022-03-05 17:19:20 +00:00
2022-03-06 22:34:59 +00:00
from os import path , remove
from string import Template
2022-03-05 17:19:20 +00:00
from tempfile import TemporaryDirectory
2022-03-06 22:34:59 +00:00
from subprocess import Popen , TimeoutExpired , PIPE
from platform import system
from shutil import which
2022-03-07 16:25:30 +00:00
from sys import argv
2022-03-05 17:19:20 +00:00
2022-03-06 22:34:59 +00:00
"""
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 }
""" )
2022-03-05 17:19:20 +00:00
class Latex ( QObject ) :
2022-03-06 22:34:59 +00:00
"""
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 ) :
2022-03-05 17:19:20 +00:00
QObject . __init__ ( self )
self . tempdir = tempdir
2022-03-06 22:34:59 +00:00
def check_latex_install ( self ) :
"""
Checks if the current latex installation is valid .
"""
if LATEX_PATH is None :
2022-04-02 16:20:04 +00:00
print ( " No Latex installation found. " )
if " --test-build " not in argv :
2022-03-07 16:25:30 +00:00
QMessageBox . warning ( None , " LogarithmPlotter - Latex setup " , QCoreApplication . translate ( " latex " , " No Latex installation found. \n If you already have a latex distribution installed, make sure it ' s installed on your path. \n Otherwise, you can download a Latex distribution like TeX Live at https://tug.org/texlive/. " ) )
2022-03-06 22:34:59 +00:00
elif DVIPNG_PATH is None :
2022-04-02 16:20:04 +00:00
print ( " DVIPNG not found. " )
if " --test-build " not in argv :
2022-03-07 16:25:30 +00:00
QMessageBox . warning ( None , " LogarithmPlotter - Latex setup " , QCoreApplication . translate ( " latex " , " DVIPNG was not found. Make sure you include it from your Latex distribution. " ) )
2022-03-06 22:34:59 +00:00
@Property ( bool )
def latexSupported ( self ) :
return LATEX_PATH is not None and DVIPNG_PATH is not None
2022-10-17 23:47:49 +00:00
2022-03-05 19:57:21 +00:00
@Slot ( str , float , QColor , result = str )
2022-10-17 23:47:49 +00:00
def render ( self , latex_markup : str , font_size : float , color : QColor ) - > str :
2022-03-06 22:34:59 +00:00
"""
2022-10-17 23:47:49 +00:00
Prepares and renders a latex string into a png file .
2022-03-06 22:34:59 +00:00
"""
2022-03-07 00:37:23 +00:00
markup_hash = hash ( latex_markup )
export_path = path . join ( self . tempdir . name , f ' { markup_hash } _ { font_size } _ { color . rgb ( ) } ' )
2022-03-06 22:34:59 +00:00
if self . latexSupported and not path . exists ( export_path + " .png " ) :
2022-03-06 23:11:12 +00:00
print ( " Rendering " , latex_markup , export_path )
2022-03-06 22:34:59 +00:00
# Generating file
try :
2022-03-07 00:37:23 +00:00
latex_path = path . join ( self . tempdir . name , str ( markup_hash ) )
# If the formula is just recolored or the font is just changed, no need to recreate the DVI.
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 . convert_dvi_to_png ( latex_path , export_path , font_size , color )
2022-03-06 22:34:59 +00:00
except Exception as e : # One of the processes failed. A message will be sent every time.
2022-03-07 00:37:23 +00:00
raise e
2022-10-17 23:47:49 +00:00
img = QImage ( export_path ) ;
2022-03-06 22:34:59 +00:00
# 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 "
] )
2022-03-07 00:37:23 +00:00
def convert_dvi_to_png ( self , dvi_path : str , export_path : str , font_size : float , color : QColor ) :
2022-03-06 22:34:59 +00:00
"""
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.
2022-03-07 00:37:23 +00:00
f ' { dvi_path } .dvi ' , # Input file
2022-03-06 22:34:59 +00:00
' -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 :
2022-03-07 01:46:38 +00:00
out , err = proc . communicate ( timeout = 2 ) # 2 seconds is already FAR too long.
2022-03-06 22:34:59 +00:00
if proc . returncode != 0 :
# Process errored
QMessageBox . warning ( None , " LogarithmPlotter - Latex " ,
2022-03-07 01:46:38 +00:00
QCoreApplication . translate ( " latex " , " An exception occured within the creation of the latex formula. \n Process ' {} ' ended with a non-zero return code {} : \n \n {} \n Please make sure your latex installation is correct and report a bug if so. " )
2022-03-06 22:34:59 +00:00
. 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 ' ) )
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. \n Process ' {} ' took too long to finish: \n {} \n Please 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 ) :
"""
2022-03-07 00:37:23 +00:00
Removes auxiliary , logs and Tex temporary files .
2022-03-06 22:34:59 +00:00
"""
2022-03-07 00:37:23 +00:00
for i in [ " .tex " , " .aux " , " .log " ] :
2022-03-06 22:34:59 +00:00
remove ( export_path + i )
2022-10-17 23:47:49 +00:00
"""
2022-03-06 22:34:59 +00:00
@Slot ( str , float , QColor , result = str )
def render_legacy ( self , latexstring , font_size , color = True ) :
2022-03-05 19:57:21 +00:00
exprpath = path . join ( self . tempdir . name , f ' { hash ( latexstring ) } _ { font_size } _ { color . rgb ( ) } .png ' )
print ( " Rendering " , latexstring , exprpath )
2022-03-05 17:19:20 +00:00
if not path . exists ( exprpath ) :
2022-03-05 19:57:21 +00:00
fg = color . convertTo ( QColor . Rgb )
fg = f ' rgb { fg . redF ( ) } { fg . greenF ( ) } { fg . blueF ( ) } '
2022-03-05 23:55:32 +00:00
preview ( ' $$ { ' + latexstring + ' }$$ ' , viewer = ' file ' , filename = exprpath , dvioptions = [
2022-03-05 17:19:20 +00:00
" -T " , " tight " ,
" -z " , " 0 " ,
" --truecolor " ,
2022-03-05 23:55:32 +00:00
f " -D { int ( font_size * 72.27 / 100 ) * 10 } " , # See https://linux.die.net/man/1/dvipng#-D for convertion
2022-03-05 17:19:20 +00:00
" -bg " , " Transparent " ,
" -fg " , fg ] ,
2022-03-05 23:55:32 +00:00
euler = False )
2022-03-05 19:57:21 +00:00
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 ( ) } '
2022-10-17 23:47:49 +00:00
"""