From 19867b3679a2d37accb3a0ea198e4732b50fd6a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Labb=C3=A9?= Date: Thu, 30 Mar 2023 15:37:44 +0200 Subject: [PATCH 1/8] added more information on the reason when a latex feature is not functional --- src/sage/features/latex.py | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/sage/features/latex.py b/src/sage/features/latex.py index 1b01db39f67..19a93e9b924 100644 --- a/src/sage/features/latex.py +++ b/src/sage/features/latex.py @@ -50,6 +50,23 @@ def is_functional(self): sage: from sage.features.latex import latex sage: latex().is_functional() # optional - latex FeatureTestResult('latex', True) + + When the feature is not functional, more information on the reason + can be obtained as follows:: + + sage: result = latex().is_functional() # not tested + sage: print(result.reason) # not tested + Running latex on a sample file + (with command='latex -interaction=nonstopmode tmp_wmpos8ak.tex') + returned non-zero exit status='1' with stderr='' + and stdout='This is pdfTeX, + ... + Runaway argument? + {document + ! File ended while scanning use of \end. + ... + No pages of output. + Transcript written on tmp_wmpos8ak.log.' """ lines = [] lines.append(r"\documentclass{article}") @@ -77,8 +94,12 @@ def is_functional(self): return FeatureTestResult(self, True) else: return FeatureTestResult(self, False, reason="Running latex on " - "a sample file returned non-zero " - "exit status {}".format(result.returncode)) + "a sample file (with command='{}') returned non-zero " + "exit status='{}' with stderr='{}' " + "and stdout='{}'".format(result.args, + result.returncode, + result.stderr.strip(), + result.stdout.strip())) class latex(LaTeX): From 4e0712df04dcf4f46605ee2eb1bf2b1945140ec8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Labb=C3=A9?= Date: Thu, 30 Mar 2023 15:39:56 +0200 Subject: [PATCH 2/8] adding feature dvips to features/latex.py --- src/sage/features/latex.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/sage/features/latex.py b/src/sage/features/latex.py index 19a93e9b924..f1451e7bc4b 100644 --- a/src/sage/features/latex.py +++ b/src/sage/features/latex.py @@ -186,6 +186,26 @@ def __init__(self): super().__init__("lualatex") +class dvips(Executable): + r""" + A :class:`~sage.features.Feature` describing the presence of ``dvips`` + + EXAMPLES:: + + sage: from sage.features.latex import dvips + sage: dvips().is_present() # optional - dvips + FeatureTestResult('dvips', True) + """ + def __init__(self): + r""" + TESTS:: + + sage: from sage.features.latex import dvips + sage: isinstance(dvips(), dvips) + True + """ + Executable.__init__(self, "dvips", executable="dvips", + url="https://tug.org/texinfohtml/dvips.html") class TeXFile(StaticFile): r""" A :class:`sage.features.Feature` describing the presence of a TeX file @@ -275,4 +295,5 @@ def all_features(): pdflatex(), xelatex(), lualatex(), + dvips(), LaTeXPackage("tkz-graph")] From e313c0fe8049803f8a0fb074955c3be027178dd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Labb=C3=A9?= Date: Thu, 30 Mar 2023 15:43:10 +0200 Subject: [PATCH 3/8] adding dvi and eps methods to Standalone class --- src/sage/misc/latex_standalone.py | 224 ++++++++++++++++++++++++++++++ 1 file changed, 224 insertions(+) diff --git a/src/sage/misc/latex_standalone.py b/src/sage/misc/latex_standalone.py index 09339eddd1e..69b4fa75d10 100644 --- a/src/sage/misc/latex_standalone.py +++ b/src/sage/misc/latex_standalone.py @@ -746,6 +746,125 @@ def pdf(self, filename=None, view=True, program=None): return temp_filename_pdf + def dvi(self, filename=None, view=True, program='latex'): + r""" + Compiles the latex code with latex and create a dvi file. + + INPUT: + + - ``filename`` -- string (default: ``None``), the output filename. + If ``None``, it saves the file in a temporary directory. + + - ``view`` -- bool (default:``True``), whether to open the file in a + dvi viewer. This option is ignored and automatically set to + ``False`` if ``filename`` is not ``None``. + + - ``program`` -- string (default:``'latex'``), ``'latex'`` + + OUTPUT: + + string, path to dvi file + + EXAMPLES:: + + sage: from sage.misc.latex_standalone import Standalone + sage: t = Standalone('Hello World') + sage: _ = t.dvi(view=False) # long time (1s) # optional latex + + Same for instances of :class:`TikzPicture`:: + + sage: from sage.misc.latex_standalone import TikzPicture + sage: s = "\\begin{tikzpicture}\n\\draw (0,0) -- (1,1);\n\\end{tikzpicture}" + sage: t = TikzPicture(s) + sage: _ = t.dvi(view=False) # not tested + + A filename may be provided where to save the file, in which case + the viewer does not open the file:: + + sage: from sage.misc.temporary_file import tmp_filename + sage: filename = tmp_filename('temp','.dvi') + sage: path_to_file = t.dvi(filename) # long time (1s) # optional latex + sage: path_to_file[-4:] # long time (fast) # optional latex + '.dvi' + + The filename may contain spaces:: + + sage: filename = tmp_filename('filename with spaces','.dvi') + sage: path_to_file = t.dvi(filename) # long time (1s) # optional latex + + TESTS: + + We test the behavior when a wrong tex string is provided:: + + sage: s = "\\begin{tikzpicture}\n\\draw (0,0) -- (1,1);\n\\end{tikzpicture}" + sage: s_missing_last_character = s[:-1] + sage: t = TikzPicture(s_missing_last_character) + sage: _ = t.dvi() # optional latex + Traceback (most recent call last): + ... + CalledProcessError: Command '['latex', '-interaction=nonstopmode', + 'tikz_...tex']' returned non-zero exit status 1. + + """ + from sage.features.latex import latex + + # Set default program + if program is None: + program = 'latex' + + # Check availability of programs + if program == 'latex': + latex().require() + else: + raise ValueError("program(={}) should be latex".format(program)) + + # set up filenames + from sage.misc.temporary_file import tmp_filename + temp_filename_tex = tmp_filename('tikz_', '.tex') + with open(temp_filename_tex, 'w') as f: + f.write(str(self)) + base, temp_filename_tex = os.path.split(temp_filename_tex) + temp_filename, ext = os.path.splitext(temp_filename_tex) + + # running pdflatex or lualatex + cmd = [program, '-interaction=nonstopmode', temp_filename_tex] + result = run(cmd, cwd=base, capture_output=True, text=True) + + # If a problem with the tex source occurs, provide the log + if result.returncode != 0: + print("Command \n" + " '{}'\n" + "returned non-zero exit status {}.\n" + "Here is the content of the stderr:{}\n" + "Here is the content of the stdout:" + "{}\n".format(' '.join(result.args), + result.returncode, + result.stderr.strip(), + result.stdout.strip())) + result.check_returncode() + temp_filename_dvi = os.path.join(base, temp_filename + '.dvi') + + # move the pdf into the good location + if filename: + filename = os.path.abspath(filename) + import shutil + shutil.move(temp_filename_dvi, filename) + return filename + + # open the tmp dvi + elif view: + from sage.misc.viewer import dvi_viewer + cmd = dvi_viewer().split() + cmd.append(temp_filename_dvi) + # we use check_call as opposed to run, because + # it gives the sage prompt back to the user + # see https://stackoverflow.com/a/71342967 + # run(cmd, cwd=base, capture_output=True, check=True) + from subprocess import check_call, PIPE + check_call(cmd, cwd=base, stdout=PIPE, stderr=PIPE) + + return temp_filename_dvi + def png(self, filename=None, density=150, view=True): r""" Compiles the latex code with pdflatex and converts to a png file. @@ -935,6 +1054,111 @@ def svg(self, filename=None, view=True, program='pdftocairo'): return temp_filename_svg + def eps(self, filename=None, view=True, program='dvips'): + r""" + Compiles the latex code with pdflatex and converts to a eps file. + + INPUT: + + - ``filename`` -- string (default:``None``), the output filename. + If ``None``, it saves the file in a temporary directory. + + - ``view`` -- bool (default:``True``), whether to open the file in + a browser. This option is ignored and automatically set to + ``False`` if ``filename`` is not ``None``. + + - ``program`` -- string (default:``'dvips'``), + ``'pdftocairo'`` or ``'dvips'`` + + OUTPUT: + + string, path to eps file + + EXAMPLES:: + + sage: from sage.misc.latex_standalone import Standalone + sage: t = Standalone('Hello World') + sage: _ = t.eps(view=False) # not tested + + Same for instances of :class:`TikzPicture`:: + + sage: from sage.misc.latex_standalone import TikzPicture + sage: s = "\\begin{tikzpicture}\n\\draw (0,0) -- (1,1);\n\\end{tikzpicture}" + sage: t = TikzPicture(s) + sage: _ = t.eps(view=False) # not tested + + We test the creation of the files:: + + sage: from sage.misc.temporary_file import tmp_filename + sage: filename = tmp_filename('temp', '.eps') + sage: path_to_file = t.eps(filename, program='dvips') # long time (1s) # optional latex dvips + sage: path_to_file[-4:] # long time (fast) # optional latex dvips + '.eps' + sage: path_to_file = t.eps(filename, program='pdftocairo') # long time (1s) # optional latex pdftocairo + sage: path_to_file[-4:] # long time (fast) # optional latex pdftocairo + '.eps' + + """ + + if program == 'pdftocairo': + from sage.features.poppler import pdftocairo + pdftocairo().require() + # set the temporary filenames + temp_filename_pdf = self.pdf(filename=None, view=False) + temp_filename, ext = os.path.splitext(temp_filename_pdf) + temp_filename_eps = temp_filename + '.eps' + # set the command + cmd = ['pdftocairo', '-eps', temp_filename_pdf, temp_filename_eps] + elif program == 'dvips': + from sage.features.latex import dvips + dvips().require() + # set the temporary filenames + temp_filename_dvi = self.dvi(filename=None, view=False) + temp_filename, ext = os.path.splitext(temp_filename_dvi) + temp_filename_eps = temp_filename + '.eps' + # set the command + cmd = ['dvips', '-E', '-o', temp_filename_eps, temp_filename_dvi] + else: + raise ValueError("program(={}) should be 'pdftocairo' or" + " 'dvips'".format(program)) + + # convert to eps + result = run(cmd, capture_output=True, text=True) + + # If a problem occurs, provide the log + if result.returncode != 0: + print("Command \n" + " '{}'\n" + "returned non-zero exit status {}.\n" + "Here is the content of the stderr:{}\n" + "Here is the content of the stdout:" + "{}\n".format(' '.join(result.args), + result.returncode, + result.stderr.strip(), + result.stdout.strip())) + result.check_returncode() + + # move the eps into the good location + if filename: + filename = os.path.abspath(filename) + import shutil + shutil.move(temp_filename_eps, filename) + return filename + + # open the tmp eps + elif view: + from sage.misc.viewer import viewer + cmd = viewer().split() + cmd.append(temp_filename_eps) + # we use check_call as opposed to run, because + # it gives the sage prompt back to the user + # see https://stackoverflow.com/a/71342967 + # run(cmd, capture_output=True, check=True) + from subprocess import check_call, PIPE + check_call(cmd, stdout=PIPE, stderr=PIPE) + + return temp_filename_eps + def tex(self, filename=None, content_only=False, include_header=None): r""" Writes the latex code to a file. From 069784baa110d539975fe79e5e6d917fc7e5accf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Labb=C3=A9?= Date: Thu, 30 Mar 2023 14:03:46 +0200 Subject: [PATCH 4/8] adding method save to Standalone class (for compatibility with sagetex) --- src/sage/misc/latex_standalone.py | 58 +++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/src/sage/misc/latex_standalone.py b/src/sage/misc/latex_standalone.py index 69b4fa75d10..6166483d763 100644 --- a/src/sage/misc/latex_standalone.py +++ b/src/sage/misc/latex_standalone.py @@ -1223,6 +1223,64 @@ def tex(self, filename=None, content_only=False, include_header=None): return filename + def save(self, filename, **kwds): + r""" + Save the graphics to an image file. + + INPUT: + + - ``filename`` -- string. The filename and the image format + given by the extension, which can be one of the following: + + * ``.pdf``, + * ``.png``, + * ``.svg``, + * ``.eps``, + * ``.dvi``, + * ``.sobj`` (for a Sage object you can load later), + * empty extension will be treated as ``.sobj``. + + All other keyword arguments will be passed to the plotter. + + OUTPUT: + + - ``None`` + + .. NOTE:: + + This method follows the signature of the method + :meth:`sage.plot.Graphics.save` in order to be compatible with + with sagetex. In particular so that ``\sageplot{t}`` written + in a ``tex`` file works when ``t`` is an instance of + :class:`Standalone` or :class:`TikzPicture`. + + EXAMPLES:: + + sage: from sage.misc.temporary_file import tmp_filename + sage: from sage.misc.latex_standalone import Standalone + sage: t = Standalone('Hello World') + sage: filename = tmp_filename('temp','.pdf') + sage: t.save(filename) # long time (1s) # optional latex + sage: filename = tmp_filename('temp','.eps') + sage: t.save(filename) # long time (1s) # optional latex dvips + + """ + ext = os.path.splitext(filename)[1].lower() + if ext == '' or ext == '.sobj': + raise NotImplementedError() + elif ext == '.pdf': + self.pdf(filename, **kwds) + elif ext == '.png': + self.png(filename, **kwds) + elif ext == '.svg': + self.svg(filename, **kwds) + elif ext == '.eps': + self.eps(filename, **kwds) + elif ext == '.dvi': + self.dvi(filename, **kwds) + else: + raise ValueError("allowed file extensions for images are " + ".pdf, .png, .svg, .eps, .dvi!") class TikzPicture(Standalone): r""" From df984bc471fe500fff1715167e3e18a03c04589a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Labb=C3=A9?= Date: Thu, 30 Mar 2023 23:29:01 +0200 Subject: [PATCH 5/8] fixed one doctest --- src/sage/doctest/external.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/sage/doctest/external.py b/src/sage/doctest/external.py index f8374b13473..60299862755 100644 --- a/src/sage/doctest/external.py +++ b/src/sage/doctest/external.py @@ -380,6 +380,7 @@ class AvailableSoftware(): sage: from sage.doctest.external import external_software, available_software sage: external_software ['cplex', + 'dvips', 'ffmpeg', 'gurobi', 'internet', From 155ebb5f1f14cefd8ac0951947cb75234d5e7d51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Labb=C3=A9?= Date: Tue, 4 Apr 2023 17:06:18 +0200 Subject: [PATCH 6/8] Added a newline after class dvips --- src/sage/features/latex.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/sage/features/latex.py b/src/sage/features/latex.py index f1451e7bc4b..48b4576961c 100644 --- a/src/sage/features/latex.py +++ b/src/sage/features/latex.py @@ -206,6 +206,7 @@ def __init__(self): """ Executable.__init__(self, "dvips", executable="dvips", url="https://tug.org/texinfohtml/dvips.html") + class TeXFile(StaticFile): r""" A :class:`sage.features.Feature` describing the presence of a TeX file From 95cc4d0a3cf6e9d6209427e49866bd31c1e2dedc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Labb=C3=A9?= Date: Tue, 4 Apr 2023 17:10:10 +0200 Subject: [PATCH 7/8] Fixed Compiles -> Compile and OUTPUT spacing --- src/sage/misc/latex_standalone.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/sage/misc/latex_standalone.py b/src/sage/misc/latex_standalone.py index 6166483d763..7ef0a01f900 100644 --- a/src/sage/misc/latex_standalone.py +++ b/src/sage/misc/latex_standalone.py @@ -622,7 +622,7 @@ def add_macro(self, macro): def pdf(self, filename=None, view=True, program=None): r""" - Compiles the latex code with pdflatex and create a pdf file. + Compile the latex code with pdflatex and create a pdf file. INPUT: @@ -639,7 +639,7 @@ def pdf(self, filename=None, view=True, program=None): OUTPUT: - string, path to pdf file + string, path to pdf file EXAMPLES:: @@ -748,7 +748,7 @@ def pdf(self, filename=None, view=True, program=None): def dvi(self, filename=None, view=True, program='latex'): r""" - Compiles the latex code with latex and create a dvi file. + Compile the latex code with latex and create a dvi file. INPUT: @@ -763,7 +763,7 @@ def dvi(self, filename=None, view=True, program='latex'): OUTPUT: - string, path to dvi file + string, path to dvi file EXAMPLES:: @@ -867,7 +867,7 @@ def dvi(self, filename=None, view=True, program='latex'): def png(self, filename=None, density=150, view=True): r""" - Compiles the latex code with pdflatex and converts to a png file. + Compile the latex code with pdflatex and converts to a png file. INPUT: @@ -883,7 +883,7 @@ def png(self, filename=None, density=150, view=True): OUTPUT: - string, path to png file + string, path to png file EXAMPLES:: @@ -956,7 +956,7 @@ def png(self, filename=None, density=150, view=True): def svg(self, filename=None, view=True, program='pdftocairo'): r""" - Compiles the latex code with pdflatex and converts to a svg file. + Compile the latex code with pdflatex and converts to a svg file. INPUT: @@ -972,7 +972,7 @@ def svg(self, filename=None, view=True, program='pdftocairo'): OUTPUT: - string, path to svg file + string, path to svg file EXAMPLES:: @@ -1056,7 +1056,7 @@ def svg(self, filename=None, view=True, program='pdftocairo'): def eps(self, filename=None, view=True, program='dvips'): r""" - Compiles the latex code with pdflatex and converts to a eps file. + Compile the latex code with pdflatex and converts to a eps file. INPUT: @@ -1072,7 +1072,7 @@ def eps(self, filename=None, view=True, program='dvips'): OUTPUT: - string, path to eps file + string, path to eps file EXAMPLES:: @@ -1173,7 +1173,7 @@ def tex(self, filename=None, content_only=False, include_header=None): OUTPUT: - string, path to tex file + string, path to tex file EXAMPLES:: From fed096b678b3412af6a8956f016f78c4b2013cc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Labb=C3=A9?= Date: Tue, 4 Apr 2023 17:24:39 +0200 Subject: [PATCH 8/8] added doctests testing value errors --- src/sage/misc/latex_standalone.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/sage/misc/latex_standalone.py b/src/sage/misc/latex_standalone.py index 7ef0a01f900..cd42519c5ec 100644 --- a/src/sage/misc/latex_standalone.py +++ b/src/sage/misc/latex_standalone.py @@ -805,6 +805,14 @@ def dvi(self, filename=None, view=True, program='latex'): CalledProcessError: Command '['latex', '-interaction=nonstopmode', 'tikz_...tex']' returned non-zero exit status 1. + We test the behavior when a wrong value is provided:: + + sage: t = Standalone('Hello World') + sage: _ = t.dvi(program='lates') + Traceback (most recent call last): + ... + ValueError: program(=lates) should be latex + """ from sage.features.latex import latex @@ -1098,6 +1106,16 @@ def eps(self, filename=None, view=True, program='dvips'): sage: path_to_file[-4:] # long time (fast) # optional latex pdftocairo '.eps' + TESTS: + + We test the behavior when a wrong value is provided:: + + sage: t = Standalone('Hello World') + sage: _ = t.eps(program='convert') + Traceback (most recent call last): + ... + ValueError: program(=convert) should be 'pdftocairo' or 'dvips' + """ if program == 'pdftocairo':