Contact
CoCalc Logo Icon
StoreFeaturesDocsShareSupport News AboutSign UpSign In
| Download
Views: 39558
1
#!/usr/bin/env python2
2
# -*- coding: utf-8 -*-
3
###############################################################################
4
#
5
# CoCalc: Collaborative Calculation in the Cloud
6
#
7
# Copyright (C) 2016, Sagemath Inc.
8
#
9
# This program is free software: you can redistribute it and/or modify
10
# it under the terms of the GNU General Public License as published by
11
# the Free Software Foundation, either version 3 of the License, or
12
# (at your option) any later version.
13
#
14
# This program is distributed in the hope that it will be useful,
15
# but WITHOUT ANY WARRANTY; without even the implied warranty of
16
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
# GNU General Public License for more details.
18
#
19
# You should have received a copy of the GNU General Public License
20
# along with this program. If not, see <http://www.gnu.org/licenses/>.
21
#
22
###############################################################################
23
24
"""
25
Copyright (c) 2014 -- 2016 SageMath, Inc..
26
27
All rights reserved.
28
29
Redistribution and use in source and binary forms, with or without
30
modification, are permitted provided that the following conditions are met:
31
32
1. Redistributions of source code must retain the above copyright notice, this
33
list of conditions and the following disclaimer.
34
2. Redistributions in binary form must reproduce the above copyright notice,
35
this list of conditions and the following disclaimer in the documentation
36
and/or other materials provided with the distribution.
37
38
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
39
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
40
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
41
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
42
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
43
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
44
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
45
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
46
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
47
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
48
49
CONTRIBUTORS:
50
51
- William Stein - maintainer and initial author
52
- Cedric Sodhi - internationalization and bug fixes
53
- Tomas Kalvoda - internationalization
54
- Harald Schilly - inkscape svg2pdf, ThreadPool, bug fixes, ...
55
56
"""
57
58
MARKERS = {'cell':u"\uFE20", 'output':u"\uFE21"}
59
60
# ATTN styles have to start with a newline
61
STYLES = {
62
'classic': r"""
63
\documentclass{article}
64
\usepackage{fullpage}
65
\usepackage[utf8x]{inputenc}
66
\usepackage[T1]{fontenc}
67
\usepackage{amsmath}
68
\usepackage{amssymb}
69
""",
70
71
'modern': r"""
72
\documentclass[
73
paper=A4,
74
pagesize,
75
fontsize=11pt,
76
%headings=small,
77
titlepage=false,
78
fleqn,
79
toc=flat,
80
bibliography=totoc, %totocnumbered,
81
index=totoc,
82
listof=flat]{scrartcl}
83
\usepackage{scrhack}
84
\setuptoc{toc}{leveldown}
85
86
\usepackage[utf8x]{inputenc}
87
\usepackage[T1]{fontenc}
88
\usepackage{xltxtra} % xelatex
89
90
\usepackage[
91
left=3cm,
92
right=2cm,
93
top=2cm,
94
bottom=2cm,
95
includeheadfoot]{geometry}
96
\usepackage[automark,headsepline,ilines,komastyle]{scrpage2}
97
\pagestyle{scrheadings}
98
99
\usepackage{fixltx2e}
100
101
\raggedbottom
102
103
% font tweaks
104
\usepackage{ellipsis,ragged2e,marginnote}
105
\usepackage{inconsolata}
106
\renewcommand{\familydefault}{\sfdefault}
107
\setkomafont{sectioning}{\normalcolor\bfseries}
108
\setkomafont{disposition}{\normalcolor\bfseries}
109
110
\usepackage{mathtools}
111
\mathtoolsset{showonlyrefs=true}
112
\usepackage{amssymb}
113
\usepackage{sfmath}
114
"""
115
}
116
117
COMMON = r"""
118
\usepackage[USenglish]{babel}
119
\usepackage{etoolbox}
120
\usepackage{url}
121
\usepackage{hyperref}
122
123
% use includegraphics directly, but beware, that this is actually ...
124
\usepackage{graphicx}
125
% ... adjust box! http://latex-alive.tumblr.com/post/81481408449
126
\usepackage[Export]{adjustbox}
127
\adjustboxset{max size={\textwidth}{0.7\textheight}}
128
129
\usepackage{textcomp}
130
\def\leftqquote{``}\def\rightqqoute{''}
131
\catcode`\"=13
132
\def"{\bgroup\def"{\rightqqoute\egroup}\leftqquote}
133
134
\makeatletter
135
\preto{\@verbatim}{\topsep=0pt \partopsep=0pt }
136
\makeatother
137
138
\usepackage{color}
139
\definecolor{midgray}{rgb}{0.5,0.5,0.5}
140
\definecolor{lightyellow}{rgb}{1,1,.92}
141
\definecolor{dblackcolor}{rgb}{0.0,0.0,0.0}
142
\definecolor{dbluecolor}{rgb}{.01,.02,0.7}
143
\definecolor{dredcolor}{rgb}{1,0,0}
144
\definecolor{dbrowncolor}{rgb}{0.625,0.3125,0}
145
\definecolor{dgraycolor}{rgb}{0.30,0.3,0.30}
146
\definecolor{graycolor}{rgb}{0.35,0.35,0.35}
147
148
\usepackage{listings}
149
\lstdefinelanguage{Sage}[]{Python}
150
{morekeywords={True,False,sage,singular},
151
sensitive=true}
152
\lstset{
153
showtabs=False,
154
showspaces=False,
155
showstringspaces=False,
156
commentstyle={\ttfamily\color{dbrowncolor}},
157
keywordstyle={\ttfamily\color{dbluecolor}\bfseries},
158
stringstyle ={\ttfamily\color{dgraycolor}\bfseries},
159
numberstyle ={\tiny\color{midgray}},
160
backgroundcolor=\color{lightyellow},
161
language = Sage,
162
basicstyle={\ttfamily},
163
extendedchars=true,
164
keepspaces=true,
165
aboveskip=1em,
166
belowskip=0.1em,
167
breaklines=true,
168
prebreak = \raisebox{0ex}[0ex][0ex]{\ensuremath{\backslash}},
169
%frame=single
170
}
171
172
% sagemath macros
173
\newcommand{\Bold}[1]{\mathbb{#1}}
174
\newcommand{\ZZ}{\Bold{Z}}
175
\newcommand{\NN}{\Bold{N}}
176
\newcommand{\RR}{\Bold{R}}
177
\newcommand{\CC}{\Bold{C}}
178
\newcommand{\FF}{\Bold{F}}
179
\newcommand{\QQ}{\Bold{Q}}
180
\newcommand{\QQbar}{\overline{\QQ}}
181
\newcommand{\CDF}{\Bold{C}}
182
\newcommand{\CIF}{\Bold{C}}
183
\newcommand{\CLF}{\Bold{C}}
184
\newcommand{\RDF}{\Bold{R}}
185
\newcommand{\RIF}{\Bold{I} \Bold{R}}
186
\newcommand{\RLF}{\Bold{R}}
187
\newcommand{\CFF}{\Bold{CFF}}
188
\newcommand{\GF}[1]{\Bold{F}_{#1}}
189
\newcommand{\Zp}[1]{\ZZ_{#1}}
190
\newcommand{\Qp}[1]{\QQ_{#1}}
191
\newcommand{\Zmod}[1]{\ZZ/#1\ZZ}
192
"""
193
194
# this is part of the preamble above, although this time full of utf8 chars
195
COMMON += ur"""
196
% mathjax has \lt and \gt
197
\newcommand{\lt}{<}
198
\newcommand{\gt}{>}
199
% also support HTML's &le; and &ge;
200
\newcommand{\lequal}{≤}
201
\newcommand{\gequal}{≥}
202
\newcommand{\notequal}{≠}
203
204
% defining utf8 characters for listings
205
\lstset{literate=
206
{á}{{\'a}}1 {é}{{\'e}}1 {í}{{\'i}}1 {ó}{{\'o}}1 {ú}{{\'u}}1
207
{Á}{{\'A}}1 {É}{{\'E}}1 {Í}{{\'I}}1 {Ó}{{\'O}}1 {Ú}{{\'U}}1
208
{à}{{\`a}}1 {è}{{\`e}}1 {ì}{{\`i}}1 {ò}{{\`o}}1 {ù}{{\`u}}1
209
{À}{{\`A}}1 {È}{{\'E}}1 {Ì}{{\`I}}1 {Ò}{{\`O}}1 {Ù}{{\`U}}1
210
{ä}{{\"a}}1 {ë}{{\"e}}1 {ï}{{\"i}}1 {ö}{{\"o}}1 {ü}{{\"u}}1
211
{Ä}{{\"A}}1 {Ë}{{\"E}}1 {Ï}{{\"I}}1 {Ö}{{\"O}}1 {Ü}{{\"U}}1
212
{â}{{\^a}}1 {ê}{{\^e}}1 {î}{{\^i}}1 {ô}{{\^o}}1 {û}{{\^u}}1
213
{Â}{{\^A}}1 {Ê}{{\^E}}1 {Î}{{\^I}}1 {Ô}{{\^O}}1 {Û}{{\^U}}1
214
{œ}{{\oe}}1 {Œ}{{\OE}}1 {æ}{{\ae}}1 {Æ}{{\AE}}1 {ß}{{\ss}}1
215
{ã}{{\~a}}1 {Ã}{{\~A}}1 {õ}{{\~o}}1 {Õ}{{\~O}}1
216
{ç}{{\c c}}1 {Ç}{{\c C}}1 {ø}{{\o}}1 {å}{{\r a}}1 {Å}{{\r A}}1
217
{€}{{\EUR}}1 {£}{{\pounds}}1
218
}
219
220
"""
221
222
FOOTER = """
223
%configuration={"latex_command":"xelatex -synctex=1 -interact=nonstopmode 'tmp.tex'"}
224
"""
225
226
# TODO: this needs to use salvus.project_info() or an environment variable or something!
227
# This will work fine inside KuCalc.
228
BASE_URL = 'https://proxy'
229
230
import argparse, base64, cPickle, json, os, shutil, sys, textwrap, HTMLParser, tempfile, urllib
231
from uuid import uuid4
232
233
def escape_path(s):
234
# see http://stackoverflow.com/questions/946170/equivalent-javascript-functions-for-pythons-urllib-quote-and-urllib-unquote
235
s = urllib.quote(unicode(s).encode('utf-8'), safe='~@#$&()*!+=:;,.?/\'')
236
return s.replace('#','%23').replace("?",'%3F')
237
238
def wrap(s, c=90):
239
return '\n'.join(['\n'.join(textwrap.wrap(x, c)) for x in s.splitlines()])
240
241
# used in texifyHTML and then again, in tex_escape
242
# they're mapped to macros, defined in the latex preamble
243
relational_signs = [
244
('gt', 'gt'),
245
('lt', 'lt'),
246
('ge', 'gequal'),
247
('le', 'lequal'),
248
('ne', 'notequal')
249
]
250
251
def tex_escape(s):
252
replacements = [
253
('\\', '{\\textbackslash}'),
254
('_', r'\_'),
255
('^', r'\^'),
256
(r'{\textbackslash}$', r'\$' ),
257
('%', r'\%'),
258
('#', r'\#'),
259
('&', r'\&'),
260
]
261
for rep in replacements:
262
s = s.replace(*rep)
263
for rel in relational_signs:
264
a, b = r'{\textbackslash}%s' % rel[1], r'\%s ' % rel[1]
265
s = s.replace(a, b)
266
return s
267
268
269
# Parallel computing can be useful for IO bound tasks.
270
def thread_map(callable, inputs, nb_threads = 1):
271
"""
272
Computing [callable(args) for args in inputs]
273
in parallel using `nb_threads` separate *threads* (default: 2).
274
275
This helps a bit with I/O bound tasks and is rather conservative
276
to avoid excessive memory usage.
277
278
If an exception is raised by any thread, a RuntimeError exception
279
is instead raised.
280
"""
281
print "Doing the following in parallel:\n%s"%('\n'.join(inputs))
282
from multiprocessing.pool import ThreadPool
283
tp = ThreadPool(nb_threads)
284
exceptions = []
285
def callable_wrap(x):
286
try:
287
return callable(x)
288
except Exception, msg:
289
exceptions.append(msg)
290
results = tp.map(callable_wrap, inputs)
291
if len(exceptions) > 0:
292
raise RuntimeError(exceptions[0])
293
return results
294
295
296
# create a subclass and override the handler methods
297
298
class Parser(HTMLParser.HTMLParser):
299
def __init__(self, cmds):
300
HTMLParser.HTMLParser.__init__(self)
301
self.result = ''
302
self._commands = cmds
303
self._dont_close_img = False
304
305
def handle_starttag(self, tag, attrs):
306
if tag == 'h1':
307
self.result += '\\section{'
308
elif tag == 'h2':
309
self.result += '\\subsection{'
310
elif tag == 'h3':
311
self.result += '\\subsubsection{'
312
elif tag == 'i':
313
self.result += '\\textemph{'
314
elif tag == 'div' or tag == 'p':
315
self.result += '\n\n{'
316
elif tag == 'ul':
317
self.result += '\\begin{itemize}'
318
elif tag == 'ol':
319
self.result += '\\begin{enumerate}'
320
elif tag == 'hr':
321
# self.result += '\n\n' + '-'*80 + '\n\n'
322
self.result += '\n\n' + r'\noindent\makebox[\linewidth]{\rule{\textwidth}{0.4pt}}' + '\n\n'
323
elif tag == 'li':
324
self.result += '\\item{'
325
elif tag == 'strong':
326
self.result += '\\textbf{'
327
elif tag == 'em':
328
self.result += '\\textit{'
329
elif tag == 'a':
330
attrs = dict(attrs)
331
if 'href' in attrs:
332
self.result += '\\href{%s}{' % attrs['href']
333
else:
334
self.result += '\\url{'
335
elif tag == 'img':
336
attrs = dict(attrs)
337
if "src" in attrs:
338
href = attrs['src']
339
_, ext = os.path.splitext(href)
340
ext = ext.lower()
341
if '?' in ext:
342
ext = ext[:ext.index('?')]
343
# create a deterministic filename based on the href
344
from hashlib import sha1
345
base = sha1(href).hexdigest()
346
filename = base + ext
347
348
# href might start with /blobs/ or similar for e.g. octave plots
349
# in such a case, there is also a file output and we ignore the image in the html
350
if href[0] == '/':
351
self._dont_close_img = True
352
return
353
else:
354
href_download = href
355
356
# NOTE --no-check-certificate is needed since this query is done inside
357
# the cluster, where the cert won't match the local service name.
358
c = "rm -f '%s'; wget --no-check-certificate '%s' --output-document='%s'"%(filename, href_download, filename)
359
if ext == '.svg':
360
# convert to pdf
361
c += " && rm -f '%s'; inkscape --without-gui --export-pdf='%s' '%s'" % (base+'.pdf',base+'.pdf',filename)
362
filename = base+'.pdf'
363
self._commands.append(c)
364
# the choice of 120 is "informed" but also arbitrary
365
# besides that, if we scale it in sagews, we also have to scale it here
366
scaling = 1.
367
if 'smc-image-scaling' in attrs:
368
try:
369
# in practice (and if it is set at all) it is most likely 0.66
370
scaling = float(attrs['smc-image-scaling'])
371
except:
372
pass
373
resolution = int(120. / scaling)
374
self.result += '\\includegraphics[resolution=%s]{%s}'%(resolution, filename)
375
# alternatively, implicit scaling by adjbox and textwidth
376
# self.result += '\\includegraphics{%s}'%(filename)
377
else:
378
# fallback, because there is no src='...'
379
self.result += '\\verbatim{image: %s}' % str(attrs)
380
else:
381
self.result += '{' # fallback
382
383
def handle_endtag(self, tag):
384
if tag == 'ul':
385
self.result += '\\end{itemize}'
386
elif tag == 'ol':
387
self.result += '\\end{enumerate}'
388
elif tag == 'hr':
389
self.result += ''
390
elif tag == 'img' and self._dont_close_img:
391
self._dont_close_img = False
392
self.result += ''
393
else:
394
self.result += '}' # fallback
395
396
def handle_data(self, data):
397
# safe because all math stuff has already been escaped.
398
# print "handle_data:", data
399
self.result += tex_escape(data)
400
401
def sanitize_math_input(s):
402
from markdown2Mathjax import sanitizeInput
403
# it's critical that $$ be first!
404
delims = [('$$','$$'), ('\\(','\\)'), ('\\[','\\]'),
405
('\\begin{equation}', '\\end{equation}'), ('\\begin{equation*}', '\\end{equation*}'),
406
('\\begin{align}', '\\end{align}'), ('\\begin{align*}', '\\end{align*}'),
407
('\\begin{eqnarray}', '\\end{eqnarray}'), ('\\begin{eqnarray*}', '\\end{eqnarray*}'),
408
('\\begin{math}', '\\end{math}'),
409
('\\begin{displaymath}', '\\end{displaymath}')
410
]
411
412
tmp = [((s,None),None)]
413
for d in delims:
414
tmp.append((sanitizeInput(tmp[-1][0][0], equation_delims=d), d))
415
416
return tmp
417
418
def reconstruct_math(s, tmp):
419
print "s ='%r'"%s
420
print "tmp = '%r'"%tmp
421
from markdown2Mathjax import reconstructMath
422
while len(tmp) > 1:
423
s = reconstructMath(s, tmp[-1][0][1], equation_delims=tmp[-1][1])
424
del tmp[-1]
425
return s
426
427
def texifyHTML(s):
428
replacements = [
429
('&#8220;', '``'),
430
('&#8221;', "''"),
431
('&#8217;', "'"),
432
('&amp;', "&"),
433
]
434
for rep in replacements:
435
s = s.replace(*rep)
436
for rel in relational_signs:
437
a, b = '&%s;' % rel[0], r'\%s' % rel[1]
438
s = s.replace(a, b)
439
return s
440
441
def html2tex(doc, cmds):
442
doc = texifyHTML(doc)
443
tmp = sanitize_math_input(doc)
444
parser = Parser(cmds)
445
# The number of (unescaped) dollars or double-dollars found so far. An even
446
# number is assumed to indicate that we're outside of math and thus need to
447
# escape.
448
parser.dollars_found = 0
449
parser.feed(tmp[-1][0][0])
450
return reconstruct_math(parser.result, tmp)
451
452
453
def md2html(s):
454
from markdown2 import markdown
455
extras = ['code-friendly', 'footnotes', 'smarty-pants', 'wiki-tables', 'fenced-code-blocks']
456
457
tmp = sanitize_math_input(s)
458
markedDownText = markdown(tmp[-1][0][0], extras=extras)
459
return reconstruct_math(markedDownText, tmp)
460
461
def md2tex(doc, cmds):
462
x = md2html(doc)
463
#print "-" * 100
464
#print "md2html:", x
465
#print "-" * 100
466
y = html2tex(x, cmds)
467
#print "html2tex:", y
468
#print "-" * 100
469
return y
470
471
class Cell(object):
472
def __init__(self, s):
473
self.raw = s
474
v = s.split('\n' + MARKERS['output'])
475
if len(v) > 0:
476
w = v[0].split(MARKERS['cell']+'\n')
477
n = w[0].lstrip(MARKERS['cell'])
478
self.input_uuid = n[:36]
479
self.input_codes = n[36:]
480
if len(w) > 1:
481
self.input = w[1]
482
else:
483
self.input = ''
484
else:
485
self.input_uuid = self.input = ''
486
if len(v) > 1:
487
w = v[1].split(MARKERS['output'])
488
self.output_uuid = w[0] if len(w) > 0 else ''
489
self.output = []
490
for x in w[1:]:
491
if x:
492
try:
493
self.output.append(json.loads(x))
494
except ValueError:
495
try:
496
print "**WARNING:** Unable to de-json '%s'"%x
497
except:
498
print "Unable to de-json some output"
499
else:
500
self.output = self.output_uuid = ''
501
502
503
def latex(self):
504
"""
505
Returns the latex represenation of this cell along with a list of commands
506
that should be executed in the shell in order to obtain remote data files,
507
etc., to render this cell.
508
"""
509
self._commands = []
510
return self.latex_input() + self.latex_output(), self._commands
511
512
def latex_input(self):
513
if 'i' in self.input_codes: # hide input
514
return "\\begin{lstlisting}\n\\end{lstlisting}"
515
if self.input.strip():
516
return "\\begin{lstlisting}\n%s\n\\end{lstlisting}"%self.input
517
else:
518
return ""
519
520
def latex_output(self):
521
print "BASE_URL", BASE_URL
522
s = ''
523
if 'o' in self.input_codes: # hide output
524
return s
525
for x in self.output:
526
if 'stdout' in x:
527
s += "\\begin{verbatim}" + wrap(x['stdout']) + "\\end{verbatim}"
528
if 'stderr' in x:
529
s += "{\\color{dredcolor}\\begin{verbatim}" + wrap(x['stderr']) + "\\end{verbatim}}"
530
if 'code' in x:
531
# TODO: for now ignoring that not all code is Python...
532
s += "\\begin{lstlisting}" + x['code']['source'] + "\\end{lstlisting}"
533
if 'html' in x:
534
s += html2tex(x['html'], self._commands)
535
if 'md' in x:
536
s += md2tex(x['md'], self._commands)
537
if 'interact' in x:
538
pass
539
if 'tex' in x:
540
val = x['tex']
541
if 'display' in val:
542
s += "$$%s$$"%val['tex']
543
else:
544
s += "$%s$"%val['tex']
545
if 'file' in x:
546
val = x['file']
547
if 'url' in val:
548
target = val['url']
549
filename = os.path.split(target)[-1]
550
else:
551
filename = os.path.split(val['filename'])[-1]
552
target = "%s/blobs/%s?uuid=%s"%(BASE_URL, escape_path(filename), val['uuid'])
553
554
base, ext = os.path.splitext(filename)
555
ext = ext.lower()[1:]
556
# print "latex_output ext", ext
557
if ext in ['jpg', 'jpeg', 'png', 'eps', 'pdf', 'svg']:
558
img = ''
559
i = target.find("/raw/")
560
if i != -1:
561
src = os.path.join(os.environ['HOME'], target[i+5:])
562
if os.path.abspath(src) != os.path.abspath(filename):
563
try:
564
shutil.copyfile(src, filename)
565
except Exception, msg:
566
print msg
567
img = filename
568
else:
569
# Get the file from remote server
570
c = "rm -f '%s'; wget --no-check-certificate '%s' --output-document='%s'"%(filename, target, filename)
571
# If we succeeded, convert it to a png, which is what we can easily embed
572
# in a latex document (svg's don't work...)
573
self._commands.append(c)
574
if ext == 'svg':
575
# hack for svg files; in perfect world someday might do something with vector graphics,
576
# see http://tex.stackexchange.com/questions/2099/how-to-include-svg-diagrams-in-latex
577
# Now we live in a perfect world, and proudly introduce inkscape as a dependency for SMC :-)
578
#c += ' && rm -f "%s"; convert -antialias -density 150 "%s" "%s"'%(base+'.png',filename,base+'.png')
579
# converts the svg file into pdf
580
c += " && rm -f '%s'; inkscape --without-gui --export-pdf='%s' '%s'" % (base+'.pdf',base+'.pdf',filename)
581
self._commands.append(c)
582
filename = base+'.pdf'
583
img = filename
584
# omitting [width=\\textwidth] allows figsize to set displayed size
585
# see https://github.com/sagemathinc/cocalc/issues/114
586
s += '{\\centering\n\\includegraphics{%s}\n\\par\n}\n'%img
587
elif ext == 'sage3d' and 'sage3d' in extra_data and 'uuid' in val:
588
# render a static image, if available
589
v = extra_data['sage3d']
590
#print "KEYS", v.keys()
591
uuid = val['uuid']
592
if uuid in v:
593
#print "TARGET acquired!"
594
data = v[uuid].pop()
595
width = min(1, 1.2*data.get('width',0.5))
596
#print "width = ", width
597
if 'data-url' in data:
598
data_url = data['data-url'] # 'data:image/png;base64,iVBOR...'
599
i = data_url.find('/')
600
j = data_url.find(";")
601
k = data_url.find(',')
602
image_ext = data_url[i+1:j]
603
image_data = data_url[k+1:]
604
assert data_url[j+1:k] == 'base64'
605
filename = str(uuid4()) + "." + image_ext
606
open(filename, 'w').write(base64.b64decode(image_data))
607
s += '\\includegraphics[width=%s\\textwidth]{%s}\n'%(width, filename)
608
609
else:
610
if target.startswith('http'):
611
s += '\\url{%s}'%target
612
else:
613
s += '\\begin{verbatim}['+target+']\\end{verbatim}'
614
615
return s
616
617
class Worksheet(object):
618
def __init__(self, filename=None, s=None):
619
"""
620
The worksheet defined by the given filename or UTF unicode string s.
621
"""
622
self._default_title = ''
623
if filename:
624
self._filename = os.path.abspath(filename)
625
else:
626
self._filename = None
627
if filename is not None:
628
self._default_title = filename
629
self._init_from(open(filename).read().decode('utf8'))
630
elif s is not None:
631
self._init_from(s)
632
else:
633
raise ValueError("filename or s must be defined")
634
635
def _init_from(self, s):
636
self._cells = [Cell(x) for x in s.split('\n'+MARKERS['cell'])]
637
638
def __getitem__(self, i):
639
return self._cells[i]
640
641
def __len__(self):
642
return len(self._cells)
643
644
def latex_preamble(self, title='',author='', date='', style='modern', contents=True):
645
# The utf8x instead of utf8 below is because of http://tex.stackexchange.com/questions/83440/inputenc-error-unicode-char-u8-not-set-up-for-use-with-latex, which I needed due to approx symbols, etc. causing trouble.
646
#\usepackage{attachfile}
647
from datetime import datetime
648
top = '% generated by smc-sagews2pdf -- {timestamp}'
649
top = top.format(timestamp=str(datetime.utcnow()))
650
s = top + STYLES[style]
651
s += COMMON
652
s += r"\title{%s}"%tex_escape(title) + "\n"
653
s += r"\author{%s}"%tex_escape(author) + "\n"
654
if date:
655
s += r"\date{%s}"%tex_escape(date) + "\n"
656
s += "\\begin{document}\n"
657
s += "\\maketitle\n"
658
#if self._filename:
659
# s += "The Worksheet: \\attachfile{%s}\n\n"%self._filename
660
661
if contents:
662
s += "\\tableofcontents\n"
663
return s
664
665
def latex(self, title='', author='', date='', style='modern', contents=True):
666
if not title:
667
title = self._default_title
668
commands = []
669
tex = []
670
for c in self._cells:
671
t, cmd = c.latex()
672
tex.append(t)
673
if cmd:
674
commands.extend(cmd)
675
if commands:
676
thread_map(os.system, commands)
677
return self.latex_preamble(title=title,
678
author=author,
679
date=date,
680
style=style,
681
contents=contents) \
682
+ '\n'.join(tex) \
683
+ r"\end{document}" \
684
+ FOOTER
685
686
687
def sagews_to_pdf(filename, title='', author='', date='', outfile='', contents=True, remove_tmpdir=True, work_dir=None, style='modern'):
688
base = os.path.splitext(filename)[0]
689
if not outfile:
690
pdf = base + ".pdf"
691
else:
692
pdf = outfile
693
print "converting: %s --> %s"%(filename, pdf)
694
W = Worksheet(filename)
695
try:
696
if work_dir is None:
697
work_dir = tempfile.mkdtemp()
698
else:
699
if not os.path.exists(work_dir):
700
os.makedirs(work_dir)
701
if not remove_tmpdir:
702
print "Temporary directory retained: %s" % work_dir
703
cur = os.path.abspath('.')
704
os.chdir(work_dir)
705
from codecs import open
706
open('tmp.tex', 'w', 'utf8').write(
707
W.latex(title=title,
708
author=author,
709
date=date,
710
contents=contents,
711
style=style)
712
)#.encode('utf8'))
713
from subprocess import check_call
714
check_call('latexmk -pdf -xelatex -f -interaction=nonstopmode tmp.tex', shell=True)
715
if os.path.exists('tmp.pdf'):
716
shutil.move('tmp.pdf',os.path.join(cur, pdf))
717
print "Created", os.path.join(cur, pdf)
718
finally:
719
if work_dir and remove_tmpdir:
720
shutil.rmtree(work_dir)
721
else:
722
print "Leaving latex files in '%s'"%work_dir
723
724
def main():
725
global extra_data, BASE_URL
726
727
parser = argparse.ArgumentParser(
728
description="convert a sagews worksheet to a pdf file via latex",
729
formatter_class=argparse.ArgumentDefaultsHelpFormatter
730
)
731
parser.add_argument("filename", nargs='+', help="name of sagews file (required)", type=str)
732
parser.add_argument("--author", dest="author", help="author name for printout", type=str, default="")
733
parser.add_argument("--title", dest="title", help="title for printout", type=str, default="")
734
parser.add_argument("--date", dest="date", help="date for printout", type=str, default="")
735
parser.add_argument("--contents", dest="contents", help="include a table of contents 'true' or 'false'", type=str, default='true')
736
parser.add_argument("--outfile", dest="outfile", help="output filename (defaults to input file with sagews replaced by pdf)", type=str, default="")
737
parser.add_argument("--remove_tmpdir", dest="remove_tmpdir", help="if 'false' do not delete the temporary LaTeX files and print name of temporary directory", type=str, default='true')
738
parser.add_argument("--work_dir", dest="work_dir", help="if set, then this is used as the working directory where the tex files are generated and it won't be deleted like the temp dir.")
739
parser.add_argument('--subdir', dest="subdir", help="if set, the work_dir will be set (or overwritten) to be pointing to a subdirectory named after the file to be converted.", default='false')
740
parser.add_argument("--extra_data_file", dest="extra_data_file", help="JSON format file that contains extra data useful in printing this worksheet, e.g., 3d plots", type=str, default='')
741
parser.add_argument("--style", dest="style", help="Styling of the LaTeX document", type=str, choices=['classic', 'modern'], default="modern")
742
parser.add_argument("--base_url", dest="base_url", help="The 'BASE_URL' from where blobs and other files are being downloaded from", default=BASE_URL)
743
744
args = parser.parse_args()
745
args.contents = args.contents == 'true'
746
args.remove_tmpdir = args.remove_tmpdir == 'true'
747
args.subdir = args.subdir == 'true'
748
749
if args.extra_data_file:
750
import json
751
extra_data = json.loads(open(args.extra_data_file).read())
752
else:
753
extra_data = {}
754
755
BASE_URL = args.base_url
756
757
remove_tmpdir=args.remove_tmpdir
758
759
curdir = os.path.abspath('.')
760
for filename in args.filename:
761
os.chdir(curdir) # stuff below can change away from curdir
762
763
if args.subdir:
764
from os.path import dirname, basename, splitext, join
765
dir = dirname(filename)
766
subdir = '%s-sagews2pdf' % splitext(basename(filename))[0]
767
work_dir = join(dir, subdir)
768
remove_tmpdir = False
769
elif args.work_dir is not None:
770
work_dir = os.path.abspath(os.path.expanduser(args.work_dir))
771
remove_tmpdir = False
772
else:
773
work_dir = None
774
775
from subprocess import CalledProcessError
776
try:
777
sagews_to_pdf(filename,
778
title = args.title.decode('utf8'),
779
author = args.author.decode('utf8'),
780
date = args.date,
781
outfile = args.outfile,
782
contents = args.contents,
783
remove_tmpdir = remove_tmpdir,
784
work_dir = work_dir,
785
style = args.style
786
)
787
# subprocess.check_call might throw
788
except CalledProcessError as e:
789
sys.stderr.write('CalledProcessError: %s\n' % e)
790
exit(1)
791
792
if __name__ == "__main__":
793
main()
794