Ticket #132: formula.py

File formula.py, 9.9 kB (added by douardda, 3 years ago)

formula macro for Trac 0.9

Line 
1 """
2 Convert a latex formula into an image.
3 by Valient Gough <vgough@pobox.com>, David Douard <david.douard@gmail.com>
4
5 Changes:
6     2006-01-16 (David Douard):
7         * make this macro work with Trac 0.9
8         * make the generated images be saved in $PROJECT/htdocs/formulas
9         * make default image format be 'png'
10         * replaced every Tab by spaces
11         * make tmp dir creation recursive
12     2005-10-03:
13         * make image format selectable via 'image_format' configuration option
14           (defaults to 'jpg')
15         * allow paths to executables to be specified in configuration by
16           setting 'latex_path', 'dvips_path', 'convert_path' to point to
17           executable. Based on code by Reed Cartwright.
18     2005-10-01:
19         * add #display and #fleqn options to add html formatting around image
20           (Christian Marquardt).
21     2005-09-21:
22         * add #center and #indent options to add html formatting around image.
23     2005-08-02:
24         * remove hard-coded paths, read from configuration. Fixes #26
25     2005-07-27:
26         * figured out how to get rid of the annoying internal error after latex
27         was run.  Redirected latex output to /dev/null..
28         * found out that {{{#!figure ... }}} runs wiki macro, and doesn't have
29         the problem of not being able to use paranthesis.  So this is the
30         default usage now.  Can still use [[formula(...)]] for simple formula.
31         * add "nomode" command, which can be used to turn off automatic
32           enclosure of commands in display-math mode ("$$ ... $$")
33     2005-07-26: first release
34
35 Installation:
36     1. Copy into wiki-macros directory.
37     2. Edit conf/trac.ini and add a [latex] group with three values:
38         [latex]
39         # temp_dir points to directory where temporary files are created
40         temp_dir = /var/tmp/trac
41         # Set to 1 for fleqn style equations (default is centered)
42         fleqn = 0
43         # Indentation width for fleqn style equations
44         fleqn_width = '5%'
45
46 Usage:
47
48 {{{
49 #!formula
50 [latex code]
51 }}}
52
53 or, additional keywords can be specified before the latex code:
54 {{{
55 #!formula
56 #density=100
57 [latex code]
58 }}}
59
60 Optional keywords (must be specified before the latex code):
61     #density=100
62         Density defaults to 100.  Larger values produces larger images.
63     #nomode
64         Disable the default display mode setting.  Use this if you want to
65         include things outside of tex's display mode.
66     #display
67         Create a displayed equation (either centered or fleqn style,
68         depending on the fleqn variable in the config file.
69     #center
70         Center the equation on the page.
71     #fleqn
72         fleqn style equation; indentation is controlled by fleqn_witdh in
73         conf/trac.ini.
74     #indent [=class name]
75         places image link in a paragraph <p>...</p>
76         If class name is specified, then it is used to specify a CSS class for
77         the paragraph.
78
79 Notes:
80     A matrix macro is included in the tex code.  This allows you to do things
81     like:
82       \mat{1&2\\3&4}  to get a 2x2 matrix.  The "\\" separates rows, and "&"
83       separates columns.  Any size up to around 25? will work..
84
85 Images are automatically named based on a sha1 hash of the formula, the
86 density, and the script version.  This way the image doesn't have to be
87 regenerated every time it is used, and if anything is changed then a new image
88 is created.
89
90 Note that temporary files can build up in the tmpdir, and every time a formula
91 is modified, a new image will be created in the imagePath directory.  These can
92 be considered as cached files.  You can safely let the tmp file cleaner process
93 remove old files from these directories.
94
95 PS.  This is my first python program, so it is probably pretty ugly by python
96 standards (whatever those may be).  Feedback is welcome, but complaints about
97 ugliness will be redirected to /dev/null.
98 """
99
100 # if the output version string changes, then images will be regenerated
101 outputVersion = "0.1"
102
103
104 import re
105 import string
106 import os
107 import sha
108
109
110 def render(hdf, env, texData, density, fleqnMode, mathMode):
111     # gets paths from configuration
112     tmpdir = env.config.get('latex', 'temp_dir')
113
114     fleqnIndent = env.config.get('latex', 'fleqn_indent')
115     latexPath = env.config.get('latex', 'latex_path')
116     dvipsPath = env.config.get('latex', 'dvips_path')
117     convertPath = env.config.get('latex', 'convert_path')
118     texMag = env.config.get('latex', 'text_mag')
119     imageFormat = env.config.get('latex', 'image_format')
120
121     imagePath = os.path.normpath(os.path.join(env.get_htdocs_dir(), "formulas"))
122     if not os.path.exists(imagePath):
123         try:
124             os.mkdir(imagePath)
125         except:
126             return "<b>Error: unable to create image directory</b><br>"       
127
128     if not tmpdir:
129         return "<b>Error: missing configuration 'tmpdir' setting in 'latex' macro</b><br>"
130
131     # set defaults
132     if not fleqnIndent:
133         fleqnIndent = '5%'
134     if not latexPath:
135         latexPath = 'latex'
136     if not dvipsPath:
137         dvipsPath = 'dvips'
138     if not convertPath:
139         convertPath = 'convert'
140     if not texMag:
141         texMag = 1000 # I'm told this is latex's default value
142     if not imageFormat:
143         imageFormat = 'png'
144
145     path = tmpdir
146     # create temporary directory if necessary
147     def mkd(path):
148         if not os.path.exists(path):
149             d, t, = os.path.split(path)
150             if not os.path.exists(d):
151                 mkd(d)
152            
153             os.mkdir(path)
154                  
155     try:
156         if not os.path.exists(path):
157             mkd(path)
158     except:
159         return "Unable to create temporary directory " + path
160    
161     # generate final image name.  Use a hash of the parameters which affect
162     # the image, so we don't have to recreate it unless they change.
163     hash = sha.new(texData)
164     # include some options in the hash, as they affect the output image
165     hash.update( "%d %d" % (density, int(texMag)) )
166     hash.update( outputVersion )
167     name = hash.hexdigest()
168     imageFile = "%s/%s.%s" % (imagePath, name, imageFormat)
169
170     log = "<br>"
171     if not os.path.exists(imageFile):
172         # latex writes out lots of stuff to the current directory, so we have
173         # to run it from there.
174         cwd = os.getcwd()
175         os.chdir(path)
176
177         texFile = name + ".tex"
178         makeTexFile(texFile, texData, mathMode, texMag)
179
180         # the output from latex on stdout seems to cause problems, so sent it
181         # to /dev/null
182         cmd = "%s %s > /dev/null" % (latexPath, texFile)
183         log += execprog( cmd )
184         os.chdir(cwd)
185
186         # use dvips to convert to eps
187         dviFile = "%s/%s.dvi" % (path, name)
188         epsFile = "%s/%s.eps" % (path, name)
189         cmd = "%s -q -D 600 -E -n 1 -p 1 -o %s %s" % (dvipsPath, epsFile, dviFile)
190         log += execprog( cmd )
191
192         # and finally, ImageMagick to convert from eps to [imageFormat] type
193         cmd = "%s -antialias -density %ix%i %s %s" % (convertPath, density, density, epsFile, imageFile)
194         log += execprog( cmd )
195
196     if fleqnMode:
197         margin = " margin-left: %s" % fleqnIndent
198     else:
199         margin = ""
200        
201     html = "<img src='%s' border='0' style='vertical-align: middle;%s' alt='formula' />" % (env.href.chrome('site','formulas/%s.%s'%(name, imageFormat)), margin)
202     return html
203
204 def execprog(cmd):
205     os.system( cmd )
206     return cmd + "<br>"
207
208 def makeTexFile(texFile, texData, mathMode, texMag):
209     tex = "\\batchmode\n"
210     tex += "\\documentclass{article}\n"
211     tex += "\\usepackage{amsmath}\n"
212     tex += "\\usepackage{amssymb}\n"
213     tex += "\\usepackage{epsfig}\n"
214     tex += "\\pagestyle{empty}\n"
215     tex += "\\mag=%s\n" % texMag
216     # matrix macro
217     tex += "\\newcommand{\\mat}[2][rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr]{\n"
218     tex += \\left[\\begin{array}{#1}\n"
219     tex += "  #2\\\\\n"
220     tex += \\end{array}\n"
221     tex += \\right]}\n"
222     # start the document
223     tex += "\\begin{document}\n"
224     if mathMode:
225         tex += "$$\n"
226     tex += "%s\n" % texData
227     if mathMode:
228         tex += "$$\n"
229     tex += "\\pagebreak\n"
230     tex += "\\end{document}\n"
231        
232     FILE = open(texFile, "w")
233     FILE.write( tex )
234     FILE.close()
235
236 # arguments start with "#" on the beginning of a line
237 def execute(hdf, text, env):
238     # TODO: unescape all html escape codes
239
240     text = text.replace("&amp;", "&")
241        
242     # defaults
243     density = 100
244     mathMode = 1    # default to using display-math mode for LaTeX processing
245     displayMode = 0 # default to generating inline formula
246     fleqnMode   = env.config.get('latex', 'fleqn')
247     centerImage = 0
248     indentImage = 0
249     indentClass = ""
250
251     # find some number of arguments, followed by the formula
252     command = re.compile('^\s*#([^=]+)=?(.*)')
253     formula = ""
254     errors = ""
255     for line in text.split("\n"):
256         m = command.match(line)
257         if m:
258             if m.group(1) == "density":
259                 density = int(m.group(2))
260             elif m.group(1) == "nomode":
261                 mathMode = 0
262             elif m.group(1) == "center":
263                 centerImage = 1
264                 fleqnMode   = 0
265             elif m.group(1) == "indent":
266                 indentImage = 1
267                 indentClass = m.group(2)
268             elif m.group(1) == "display":
269                 displayMode = 1
270             elif m.group(1) == "fleqn":
271                 displayMode = 1
272                 fleqnMode   = 1
273             else:
274                 errors = '<br>Unknown <i>formula</i> command "%s"<br>' % m.group(1)
275         else:
276             formula += line + "\n"
277
278     # Set display and fleqn defaults
279     if displayMode:
280         if fleqnMode:
281             centerImage = 0
282         else:
283             centerImage = 1
284
285     # Render formula
286     format = '%s'
287     if centerImage:
288         format = '<center>%s</center>' % format
289     if indentImage:
290         if indentClass:
291             format = '<p class="%s">%s</p>' % (indentClass, format)
292         else:
293             format = '<p>%s</p>' % format
294    
295     result = errors + render(hdf, env, formula, density, fleqnMode, mathMode)
296     return format % result