I'm in the situation where I have to translate python expression to Latex Bitmap for the enduser (who feels confident enough to write python functions by himself but prefers to watch result in Latex).
I'm using Matplotlib.mathtext to do the job (from a translated latex raw string) with the following code.
import wx
import wx.lib.scrolledpanel as scrolled
import matplotlib as mpl
from matplotlib import cm
from matplotlib import mathtext
class LatexBitmapFactory():
""" Latex Expression to Bitmap """
mpl.rc('image', origin='upper')
parser = mathtext.MathTextParser("Bitmap")
mpl.rc('text', usetex=True)
DefaultProps = mpl.font_manager.FontProperties(family = "sans-serif",\
style = "normal",\
weight = "medium",\
size = 6)
# size is changed from 6 to 7
#-------------------------------------------------------------------------------
def SetBitmap(self, _parent, _line, dpi = 150, prop = DefaultProps):
bmp = self.mathtext_to_wxbitmap(_line, dpi, prop = prop)
w,h = bmp.GetWidth(), bmp.GetHeight()
return wx.StaticBitmap(_parent, -1, bmp, (80, 50), (w, h))
#-------------------------------------------------------------------------------
def mathtext_to_wxbitmap(self, _s, dpi = 150, prop = DefaultProps):
ftimage, depth = self.parser.parse(_s, dpi, prop)
w,h = ftimage.get_width(), ftimage.get_height()
return wx.BitmapFromBufferRGBA(w, h, ftimage.as_rgba_str())
EXP = r'$\left(\frac{A \cdot \left(vds \cdot rs + \operatorname{Vdp}\left(ri, Rn, Hr, Hd\right) \cdot rh\right) \cdot \left(rSurf + \left(1.0 - rSurf\right) \cdot ft\right) \cdot \left(1.0 - e^{- \left(\left(lr + \frac{\operatorname{Log}\left(2\right)}{tem \cdot 86400.0}\right)\right) \cdot tFr \cdot 3600.0}\right)}{rc \cdot \left(lr + \frac{\operatorname{Log}\left(2\right)}{tem \cdot 86400.0}\right) \cdot tFr \cdot 3600.0} + 1\right)$'
class aFrame(wx.Frame):
def __init__(self, title="Edition"):
wx.Frame.__init__(self, None, wx.ID_ANY, title=title, size=(600,400),
style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
self.SetBackgroundColour(wx.Colour(255,255,255))
sizer = wx.FlexGridSizer(cols=25, vgap=4, hgap=4)
panel = scrolled.ScrolledPanel(self)
image_latex = LatexBitmapFactory().SetBitmap(panel, EXP)
sizer.Add(image_latex, flag=wx.EXPAND|wx.ALL)
panel.SetSizer(sizer)
panel.SetAutoLayout(1)
panel.SetupScrolling()
app = wx.App(redirect=True, filename="latexlog.txt")
frame = aFrame()
frame.Show()
app.MainLoop()
with size=6, the following picture is displayed
with size=7, I have this one
The latex expression is correct, the second picture is correct. I don't have any error message, just a right parenthesis replaced with "!".
If I continue writing the expression I still have "!" with size 6.
If the expression is simpler, the right parenthesis is correctly displayed.
Any idea to solve it ?
TL;DR There is a bug in the following line of mathtext.py
Line 727. It relates the right parenthesis at size Bigg
to an index '\x21'
, but this is the index for an exclamation point. The line with a bit of context reads
_size_alternatives = {
'(' : [('rm', '('), ('ex', '\xa1'), ('ex', '\xb3'),
('ex', '\xb5'), ('ex', '\xc3')],
')' : [('rm', ')'), ('ex', '\xa2'), ('ex', '\xb4'),
('ex', '\xb6'), ('ex', '\x21')],
I am not exactly sure which index to change to, but I suggest you change your local copy of mathtext.py
to read as follows:
_size_alternatives = {
'(' : [('rm', '('), ('ex', '\xa1'), ('ex', '\xb3'),
('ex', '\xb5'), ('ex', '\x28')],
')' : [('rm', ')'), ('ex', '\xa2'), ('ex', '\xb4'),
('ex', '\xb6'), ('ex', '\x29')]
The parantheses this produces are a little over rounded as they are the basic parantheses, but they work. Equally - you could substitute in the bigg
size ones - '\xb5'
and 'xb6'
Reported on matplotlib github Issue 5210
I can reproduce this problem with size=6
using the code as provided
(made the constant a bit bigger in case it was a width issue). I cannot
reproduce the "fix" by setting size = 7
, but I can if I go up to size = 8
or higher - suggesting this might be a nasty edge case bug and possibly system dependent...
I did quite a bit of investigation / diagnosis (see below), but it seems there is a bug - reported on matplotlib github here.
However, reducing to a matplotlib
only example yields a very nice
rendering as follows. Note I've setup my matplotlib to use latex
rendering by default - but you can set the options explicitly for the
same results.
import matplotlib.pyplot as plt
import matplotlib as mpl
mpl.rc('image', origin='upper')
mpl.rc('text', usetex=True)
DefaultProps = mpl.font_manager.FontProperties(family = "sans-serif",\
style = "normal",\
weight = "medium",\
size = 6)
EXP = r'$\left(\frac{A \cdot \left(vds \cdot rs + \operatorname{Vdp}\left(ri, Rn, Hr, Hd\right) \cdot rh\right) \cdot \left(rSurf + \left(1.0 - rSurf\right) \cdot ft\right) \cdot \left(1.0 - e^{- \left(\left(lr + \frac{\operatorname{Log}\left(2\right)}{tem \cdot 86400.0}\right)\right) \cdot tFr \cdot 3600.0}\right)}{rc \cdot \left(lr + \frac{\operatorname{Log}\left(2\right)}{tem \cdot 86400.0}\right) \cdot tFr \cdot 3600.0} + 10589 \right)$'
plt.title(EXP, fontsize=6)
plt.gca().set_axis_off() # Get rid of the plotting axis for clarity
plt.show()
Output window cropped and zoomed a bit for clarity, but you can see the bracket is rendered OK
This suggests that the problem is either the way the matplotlib
rendering engine is being used, the output to bitmap, or the interaction
with wxPython
From experimentation, I noticed that if you increase the dpi to 300 the code works fine at size = 6
, but starts to fail again at size = 3
. This implies the problem is that one of the libraries does not think it can render the element in a certain number of pixels.
Diagnosing which bit was doing this is hard (IMO)
First, I added
self.parser.to_png('D:\\math_out.png', _s, color=u'black', dpi=150)
as the first line of mathtext_to_wxbitmap(self, _s, dpi = 150, prop = DefaultProps)
. This gave a nice output png
, making me think it probably wasn't the matplotlib parser at fault... EDIT based on @Baptiste's useful answer, I tested this a bit more. Actually - if I explicitly pass in fontsize
to this call, I can replicate the appearance of the exclamation point. Also, dpi
passed into this call is ignored - so in my tests I am actually dealing with a 300 dpi image. So, focus should rest on the MathTextParser
and it could be a dpi issue.
A bit more investigation - I monkey patched my matplotlib installation - putting in a print(result)
directly after the call to parseString()
here. With a working expression that goes fine and prints out a textual representation. With the bugged scenario, I see:
Traceback (most recent call last):
File "D:\baptiste_test.py", line 9, in <module>
parser.to_png(filename, s, fontsize=size)
File "C:\Python27\lib\site-packages\matplotlib\mathtext.py", line 3101, in to_
png
rgba, depth = self.to_rgba(texstr, color=color, dpi=dpi, fontsize=fontsize)
File "C:\Python27\lib\site-packages\matplotlib\mathtext.py", line 3066, in to_
rgba
x, depth = self.to_mask(texstr, dpi=dpi, fontsize=fontsize)
File "C:\Python27\lib\site-packages\matplotlib\mathtext.py", line 3039, in to_
mask
ftimage, depth = self.parse(texstr, dpi=dpi, prop=prop)
File "C:\Python27\lib\site-packages\matplotlib\mathtext.py", line 3012, in par
se
box = self._parser.parse(s, font_output, fontsize, dpi)
File "C:\Python27\lib\site-packages\matplotlib\mathtext.py", line 2339, in par
se
print(result[0])
File "C:\Python27\lib\site-packages\matplotlib\mathtext.py", line 1403, in __r
epr__
' '.join([repr(x) for x in self.children]))
File "C:\Python27\lib\site-packages\matplotlib\mathtext.py", line 1403, in __r
epr__
' '.join([repr(x) for x in self.children]))
File "C:\Python27\lib\site-packages\matplotlib\mathtext.py", line 1403, in __r
epr__
' '.join([repr(x) for x in self.children]))
File "C:\Python27\lib\site-packages\matplotlib\mathtext.py", line 1403, in __r
epr__
' '.join([repr(x) for x in self.children]))
UnicodeEncodeError: 'ascii' codec can't encode character u'\xb3' in position 1:
ordinal not in range(128)
This indicates that the bug might originate in a mis-translated character - maybe a missing codepoint in the font?
I also noted that you can reproduce without the N
letter in Baptiste's minimal example.
Sticking some debug prints in _get_glyph within the BakomaFonts class. In the failing case, the code seems to be looking up an exclamation (u'!') when you'd expect it to be looking for u'\xc4' and returning parenrightBigg (i.e. where the corresponding left bracket is looking up u'\xc3' and returning parenleftBigg). In situations where it only uses parenrightbigg, there is no problem (this occurs for fontsize=5 in the given example but no others). The debug line I put in _get_glyph was:
print('Getting glyph for symbol',repr(unicode(sym)))
print('Returning',cached_font, num, symbol_name, fontsize, slanted)
I guess whether it needs the bigg or Bigg version is based on a combination of fontsize and dpi
OK - I think problem is in this line : https://github.com/matplotlib/matplotlib/blob/master/lib/matplotlib/mathtext.py#L727
this reads (with a little context):
_size_alternatives = {
'(' : [('rm', '('), ('ex', '\xa1'), ('ex', '\xb3'),
('ex', '\xb5'), ('ex', '\xc3')],
')' : [('rm', ')'), ('ex', '\xa2'), ('ex', '\xb4'),
('ex', '\xb6'), ('ex', '\x21')], ## <---- Incorrect line.
the '\x21'
is wrong - but I can't work out what the right one is '\x29'
is close, but "too curved". I guessed at '\xc4'
following the pattern, but that is a down arrow. Hopefully one of the core devs can easily lookup this number (decimal 195) against the glyph it renders and correct.
Here is the shortest piece of code I was able to write to get this unexpected behavior:
import matplotlib.mathtext as mt
s = r'$\left(\frac{\frac{\frac{M}{I}}{N}}' \
r'{\frac{\frac{B}{U}}{G}}\right)$'
parser = mt.MathTextParser("Bitmap")
for size in range(1, 30):
filename = "figure{0}.png".format(size)
parser.to_png(filename, s, fontsize=size)
The outputs look like this (a selection 6-12):
This post was more to share the minimal code to reproduce the error rather than an answer to the question
I still have "!" with dpi='300' at size 9, but around 9 and except 3, it's ok.
I made a new Frame to play with both dpi and size and with '()' and '[]'. Just reuse 'LatexBitmapFactory' to play.
I'm on windows XP, using python 2.7. I have the same error on Windows 7.
class aFrame(wx.Frame):
def __init__(self, title="Edition"):
wx.Frame.__init__(self, None, wx.ID_ANY, title=title, size=(1300,600),
style=wx.DEFAULT_DIALOG_STYLE)
self.SetBackgroundColour(wx.Colour(255,255,255))
self.sizer = wx.BoxSizer(wx.VERTICAL)
self.panel = scrolled.ScrolledPanel(self)
init_dpi = 300
min_dpi, max_dpi = 100, 500
self.dpi_slider = wx.Slider(self.panel, wx.ID_ANY,
init_dpi, min_dpi, max_dpi, (30, 60), (250, -1),
wx.SL_HORIZONTAL | wx.SL_AUTOTICKS | wx.SL_LABELS)
init_size = 9
min_size, max_size = 3, 40
self.size_slider = wx.Slider(
self.panel, wx.ID_ANY, init_size, min_size, max_size, (30, 60), (250, -1),
wx.SL_HORIZONTAL | wx.SL_AUTOTICKS | wx.SL_LABELS)
self.log = wx.StaticText(self.panel, wx.ID_ANY, "DPI:%d SIZE:%d" %(init_dpi, init_size))
image_latex1 = self.create_lateximage(init_dpi, init_size)
image_latex2 = self.create_lateximage(init_dpi, init_size, replace_parenthesis=True)
self.sizer.Add(image_latex1, 1, flag=wx.ALIGN_CENTER|wx.EXPAND|wx.ALL)
self.sizer.Add(image_latex2, 1, flag=wx.ALIGN_CENTER|wx.EXPAND|wx.ALL)
self.sizer.Add(self.dpi_slider, 0, wx.ALIGN_CENTER)
self.sizer.Add(self.size_slider, 0, wx.ALIGN_CENTER)
self.sizer.Add(self.log, 0, wx.ALIGN_CENTER)
self.dpi_slider.Bind(wx.EVT_SCROLL_CHANGED, self.OnSliderChanged)
self.size_slider.Bind(wx.EVT_SCROLL_CHANGED, self.OnSliderChanged)
self.panel.SetSizer(self.sizer)
self.panel.SetAutoLayout(1)
self.panel.SetupScrolling()
def create_lateximage(self, dpi, size, replace_parenthesis=False):
updatedProps = mpl.font_manager.FontProperties(family = "sans-serif",\
style = "normal",\
weight = "medium",\
size = size)
if replace_parenthesis:
tp_exp = EXP.replace("right)", "right]")
tp_exp = tp_exp.replace("left(", "left[")
return LatexBitmapFactory().SetBitmap(self.panel, tp_exp, dpi=dpi, prop=updatedProps)
return LatexBitmapFactory().SetBitmap(self.panel, EXP, dpi=dpi, prop=updatedProps)
def OnSliderChanged(self, evt):
dpi = int(self.dpi_slider.GetValue())
size = int(self.size_slider.GetValue())
self.log.SetLabel("DPI:%d SIZE:%d" %(dpi, size))
self.Freeze()
new_image_latex1 = self.create_lateximage(dpi, size)
new_image_latex2 = self.create_lateximage(dpi, size, True)
prev_image_latex1 = self.sizer.Remove(0)
prev_image_latex2 = self.sizer.Remove(0)
del prev_image_latex1
del prev_image_latex2
self.sizer.Insert(0, new_image_latex2, 1, flag=wx.ALIGN_CENTER|wx.EXPAND|wx.ALL)
self.sizer.Insert(0, new_image_latex1, 1, flag=wx.ALIGN_CENTER|wx.EXPAND|wx.ALL)
self.sizer.Layout()
assert len(self.sizer.GetChildren()) == 5,\
" must have len 5, now %d"%len(self.sizer.GetChildren())
self.panel.SetupScrolling()
self.panel.SetScrollRate(100,100)
self.Thaw()
app = wx.App(redirect=True, filename="latexlog.txt")
frame = aFrame()
frame.Show()
app.MainLoop()
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With