sympy / sympy

A computer algebra system written in pure Python
https://sympy.org/
Other
12.9k stars 4.42k forks source link

Tell `sympy` to try to use the same LaTeX rendering engine as `matplotlib` if `latex` isn't installed? #27148

Open Ricyteach opened 1 week ago

Ricyteach commented 1 week ago

I'd like to use Python in Excel to display some LATEX math formulas in cells.

Creating these images is easy when just using the sympy library, but one has to have latex installed. Python in Excel runs on Anaconda distribution and it does not include a standalone LATEX rendering library (that I am aware of).

I came up with a solution to this here, but it's really kludgy:

https://stackoverflow.com/questions/79067766/render-latex-math-in-excel-using-python-in-excel

My idea: matplotlib, which comes with Anaconda distribution, is able to render LATEX. I wonder if there's a way to tell sympy "fall back" into using the same LATEX rendering library that is being used by matplotlib?

moorepants commented 1 week ago

As far as I remember, we do fall back to matplotlib latex rendering when used in jupyter and qtconsole. But maybe that has changed over the years.

moorepants commented 1 week ago

We use IPython's latex_to_png: https://ipython.readthedocs.io/en/stable/api/generated/IPython.lib.latextools.html#IPython.lib.latextools.latex_to_png

This defaults to matplotlib's renderer. If you adjust our printer settings you should be able to enable png printing for math. But I don't know how this would interact with Excel.

Ricyteach commented 1 week ago

Thank you for replying.

I think the problem is it hits these lines, and I get the "latex program is not installed" error:

https://github.com/sympy/sympy/blob/a6ee2937450480469cb7dc16eb2e6eae1220d067/sympy/printing/preview.py#L307-L308

But matplotlib is able to render the latex just fine, so there is a latex program somewhere.

Should I modify my path so that sympy is able to find the latex program being used by IPython? If so, any idea how I can find it?

Should sympy try to find this program automatically...?

moorepants commented 1 week ago

If you are using the preview function, on quick glance it doesn't seem to connected up to matplotlib's renderer.

Ricyteach commented 1 week ago

You are right, it's not.

Thanks to you pointing me in the right direction I am now just trying to use the IPython rendering function directly:

from PIL import Image
import io
from IPython.lib.latextools import latex_to_png 

latex_expr = '\\int \\frac{1}{x}\\, dx' 
img = Image.open(io.BytesIO(latex_to_png(latex_expr, wrap=True)))

(However the resulting image is really low DPI; I am still trying to figure out how to increase the resolution.)

It would be really really nice if sympy.preview just was able to do this automatically when used inside of the Anaconda environment used by Python in Excel. I suggest this as a feature. Once I figure out how to get it working properly I might do a pull request... would that be welcome?

However I consider myself a pretty novice programmer. So maybe it's not a good idea.

moorepants commented 1 week ago

would that be welcome?

Yes, I think having preview be able to use the matplotlib latex backend would be helpful to many.

moorepants commented 1 week ago

We use latex_to_png() in here: https://github.com/sympy/sympy/blob/master/sympy/interactive/printing.py#L113, maybe that is helpful. The scale may bump the DPI, not sure though.

Ricyteach commented 1 week ago

Just an update, it looks like the code below works, and it is very simple.

However, the math_to_image function does not allow for a transparent background... matplotlib has that implemented separately in figure.Figure. It's a bit convoluted to make a transparent version. The second version is essentially what latex_to_png is doing in the background.

I am wondering if it might be worthwhile to talk to the matplotlib folks about this. I'm just a guy and don't know any of them... do you think it be welcome to suggest that they add a transparent argument directly to matplotlib.mathtext.math_to_image...?

Anyway, as I said this simple code renders an image but you can't make it transparent:

from matplotlib.mathtext import math_to_image
from PIL import Image
import io

# Define LaTeX expression
latex_expr = r'$\int \frac{1}{x} \, dx$'

# Set up DPI and font size
dpi = 300
font_size = 24

# Render the LaTeX expression to a PNG image with specified DPI ( can also provide font info)
math_to_image(latex_expr, buffer, dpi=dpi, format='png')

# Load the image with PIL and save or display it
img = Image.open(io.BytesIO(buffer.getvalue()))
img.save('lala.png')

But to get a transparent version you have to do this (CORRECTION: no no no, you can just call latex_to_png directly! duh):

from matplotlib import mathtext, figure
from matplotlib.backends import backend_agg
from PIL import Image
import io

color = 'Black'

# Initialize the mathtext parser
parser = mathtext.MathTextParser('path')

# Define LaTeX expression
latex_expr = r'\int \frac{1}{x} \, dx'
s = u'${0}$'.format(latex_expr)

# Set up DPI and font size
dpi = 300
font_size = 24
buffer = io.BytesIO()

# Parse the LaTeX expression and render it into a transparent background PNG
width, height, depth, _, _ = parser.parse(s, dpi=dpi)
fig = figure.Figure(figsize=(width / 72, height / 72))
fig.text(0, depth / height, s, color=color)
backend_agg.FigureCanvasAgg(fig)
fig.savefig(buffer, dpi=dpi, format="png", transparent=True)

# Create an image from the buffer and show it
img = Image.open(io.BytesIO(buffer.getvalue()))
img.save('lala.png', transparent=True)
Ricyteach commented 1 week ago

I suppose it might be more simple to just request that matplotlib adds some more control to the latex_to_png function rather than the math_to_image function... I guess that would a discussion to have with them though.

Ricyteach commented 1 week ago

We use latex_to_png() in here: https://github.com/sympy/sympy/blob/master/sympy/interactive/printing.py#L113, maybe that is helpful. The scale may bump the DPI, not sure though.

YES! You're right: scale does bump the DPI. So that solves that problem. Use scale= 2.5 to get 300 dpi.

But you still can't make it transparent. :(

Ricyteach commented 1 week ago

Ugh sorry for all the replies, there are several different functions here and I am not keeping them all straight.

The latex_to_png function DOES make the background transparent. So this code to make a transparent image, but it does not allow you to control the font (size, etc):

from PIL import Image
import io
from IPython.lib.latextools import latex_to_png 

latex_expr = '\\int \\frac{1}{x}\\, dx' 
img = Image.open(io.BytesIO(latex_to_png(latex_expr, wrap=True, scale=2.5, color='Black')))
img.save('lala_transparent.png')

And the other code I posted before, using math_to_image, works to make it non-transparent, and you are able to control the font, but you also have to manually wrap the latex first to make it a math expression:

from matplotlib.mathtext import math_to_image
from PIL import Image
import io

# Define LaTeX expression
latex_expr = r'$\int \frac{1}{x} \, dx$'

# Set up DPI
dpi = 300

# Render the LaTeX expression to a PNG image with specified DPI ( can also provide font info)
math_to_image(latex_expr, buffer, dpi=dpi, format='png')

# Load the image with PIL and save or display it
img = Image.open(io.BytesIO(buffer.getvalue()))
img.save('lala_non_transparent.png')

I wonder if reaching out to matplotlib about this would be a good idea... seems like you should be able to use just one function and control both font and transparency with it.

asmeurer commented 3 days ago

Making the matplotlib renderer usable from preview does sound like it would be useful.

Also, I wonder if we should remove the dependence on IPython to do the conversion. The code here does not look that complicated https://github.com/ipython/ipython/blob/main/IPython/lib/latextools.py

Ricyteach commented 3 days ago

Making the matplotlib renderer usable from preview does sound like it would be useful.

Also, I wonder if we should remove the dependence on IPython to do the conversion. The code here does not look that complicated https://github.com/ipython/ipython/blob/main/IPython/lib/latextools.py

I mentioned this above but it is probably confused/lost in all my replies (sorry): you can absolutely remove the IPython dependency and depend only on matplotlib for this.

Basically: there are currently two ways to render an equation to PNG (which was the impetus of me posting this issue) using matplotlib that I have found.

  1. Use this function, which is what sympy turns out to be using by relying on IPython by default (output='png'). This function allows you to change the scale (ie, DPI) and font color, but the font size is set at 12 and it is set to a transparent background:

https://github.com/ipython/ipython/blob/a49046c77e94025501c64a8856498107589b729a/IPython/lib/latextools.py#L111-L138

(Of course other formats are also available via the latex_to_png function sympy is already using.)

  1. You can use the matplotlib.mathtext API, and call the math_to_image function. This function has more options: you can set the DPI directly (without using "scale", which is a bit cryptic), you can choose all font options you want, and (similar to latex_to_png) you can specify several different output formats ('svg', 'pdf', 'ps' or 'png'). However, this one does NOT support a transparent background:

https://matplotlib.org/stable/api/mathtext_api.html#matplotlib.mathtext.math_to_image

(The lack of support for a transparent background for no. 2 seems to me to have been the motivation for no. 1 above to be written the way it is, but I'm not certain.)

asmeurer commented 2 days ago

(The lack of support for a transparent background for no. 2 seems to me to have been the motivation for no. 1 above to be written the way it is, but I'm not certain.)

I don't know the history of this code, but it sounds plausible since you really want that for output in a notebook. However, I expect matplotlib would be open to a fix to the function to add support for that.

asmeurer commented 2 days ago

And just to clarify my point, even if we do have to have something manual instead directly calling matplotlib.math_to_image, it would still be better to copy that code from IPython directly into SymPy. It's the sort of code that makes more sense to have maintained inside of SymPy than IPython IMO.