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