schilder.py 13.3 KB
Newer Older
Robin Sonnabend's avatar
Robin Sonnabend committed
1
#!/usr/bin/env python3
Dave Kliczbor's avatar
Dave Kliczbor committed
2
3
4
# -*- encoding: utf8 -*-

from flask import Flask, flash, session, redirect, url_for, escape, request, Response, Markup
5
6
7
8
import sys
import os
import os.path
import glob
Dave Kliczbor's avatar
Dave Kliczbor committed
9
10
11
12
13
from genshi.template import TemplateLoader
from genshi.template.text import NewTextTemplate
from flaskext.genshi import Genshi, render_response
from werkzeug.utils import secure_filename
from collections import defaultdict
14
from docutils.core import publish_parts
Dave Kliczbor's avatar
Dave Kliczbor committed
15
16
17
18
19
20
21
22
23
24
import warnings
import shutil
import subprocess
from subprocess import CalledProcessError, STDOUT
import PythonMagick
import json
import tempfile
import config

app = Flask(__name__)
Dave Kliczbor's avatar
Dave Kliczbor committed
25
26
27
app.config.update(
    UPLOAD_FOLDER = config.uploaddir,
    PROPAGATE_EXCEPTIONS = True,
Robin Sonnabend's avatar
Robin Sonnabend committed
28
    MAX_CONTENT_LENGTH = 8388608
Dave Kliczbor's avatar
Dave Kliczbor committed
29
)
Dave Kliczbor's avatar
Dave Kliczbor committed
30
31
32
33
app.secret_key = config.app_secret
genshi = Genshi(app)
genshi.extensions['html'] = 'html5'

34

Dave Kliczbor's avatar
Dave Kliczbor committed
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
def check_output(*popenargs, **kwargs):
    # Copied from py2.7s subprocess module
    r"""Run command with arguments and return its output as a byte string.

    If the exit code was non-zero it raises a CalledProcessError.  The
    CalledProcessError object will have the return code in the returncode
    attribute and output in the output attribute.

    The arguments are the same as for the Popen constructor.  Example:

    >>> check_output(["ls", "-l", "/dev/null"])
    'crw-rw-rw- 1 root root 1, 3 Oct 18  2007 /dev/null\n'

    The stdout argument is not allowed as it is used internally.
    To capture standard error in the result, use stderr=STDOUT.

    >>> check_output(["/bin/sh", "-c",
    ...               "ls -l non_existent_file ; exit 0"],
    ...              stderr=STDOUT)
    'ls: non_existent_file: No such file or directory\n'
    """
    if 'stdout' in kwargs:
        raise ValueError('stdout argument not allowed, it will be overridden.')
    process = subprocess.Popen(stdout=subprocess.PIPE, *popenargs, **kwargs)
    output, unused_err = process.communicate()
    retcode = process.poll()
    if retcode != 0:
        cmd = kwargs.get("args")
        if cmd is None:
            cmd = popenargs[0]
        raise CalledProcessError(retcode, cmd, output=output)
        #raise Exception(output)
    return output

def allowed_file(filename):
70
71
    return '.' in filename and filename.rsplit('.', 1)[1] in config.allowed_extensions

72
def load_data(filename):
73
74
    with open(os.path.join(config.datadir, filename), 'r') as infile:
        formdata = defaultdict(str, json.load(infile))
75
        formdata['filename'] = filename
76
77
        if len(formdata['markup']) < 1:
            formdata['markup'] = 'latex'
78
79
80
81
82
83
        return formdata
        
def save_data(formdata, outfilename):
    with open(os.path.join(config.datadir, outfilename), 'w') as outfile:
        json.dump(formdata, outfile)
    
84
def run_pdflatex(context, outputfilename, overwrite=True):
Robin Sonnabend's avatar
Robin Sonnabend committed
85
    if 'textemplate' not in context:
86
87
88
89
90
91
92
93
        context['textemplate'] = "text-image-quer.tex"
    genshitex = TemplateLoader([config.textemplatedir])
    template = genshitex.load(
        context['textemplate'], cls=NewTextTemplate, encoding='utf8')
    if not overwrite and os.path.isfile(outputfilename) and os.path.getmtime(template.filepath) < os.path.getmtime(outputfilename):
        return
    if context['markup'] == 'rst':
        context['text'] = publish_parts(context['text'], writer_name='latex')['body']
94
        #context['headline'] = publish_parts(context['headline'], writer_name='latex')['body']
95
    tmpdir = tempfile.mkdtemp(dir=config.tmpdir)
Robin Sonnabend's avatar
Robin Sonnabend committed
96
    if 'img' in context and context['img'] and context['img'] != '__none':
97
        try:
Dave Kliczbor's avatar
Dave Kliczbor committed
98
99
            shutil.copy(os.path.join(config.imagedir, context['img']), 
                        os.path.join(tmpdir, context['img']))
100
101
102
103
104
105
106
107
108
109
110
111
        except:
            raise IOError("COULD NOT COPY")
    else:
        # print "MEH No image"
        pass
    tmptexfile = os.path.join(tmpdir, 'output.tex')
    tmppdffile = os.path.join(tmpdir, 'output.pdf')
    with open(tmptexfile, 'w') as texfile:
        texfile.write(template.generate(form=context).render(encoding='utf8'))
    cwd = os.getcwd()
    os.chdir(tmpdir)
    os.symlink(config.texsupportdir, os.path.join(tmpdir, 'support'))
Dave Kliczbor's avatar
Dave Kliczbor committed
112
    try:
113
114
115
116
        texlog = check_output(
            ['pdflatex', '--halt-on-error', tmptexfile], stderr=STDOUT)
    except CalledProcessError as e:
        if overwrite:
117
118
119
            try:
                flash(Markup("<p>PDFLaTeX Output:</p><pre>%s</pre>" % e.output), 'log')
            except:
Robin Sonnabend's avatar
Robin Sonnabend committed
120
                print((e.output))
121
122
123
        raise SyntaxWarning("PDFLaTeX bailed out")
    finally:
        os.chdir(cwd)
124
    if overwrite:
125
126
127
128
        try:
            flash(Markup("<p>PDFLaTeX Output:</p><pre>%s</pre>" % texlog), 'log')
        except:
            print(texlog)
129
130
131
    shutil.copy(tmppdffile, outputfilename)
    shutil.rmtree(tmpdir)

Dave Kliczbor's avatar
Dave Kliczbor committed
132
def save_and_convert_image_upload(inputname):
133
134
135
    imgfile = request.files[inputname]
    if imgfile:
        if not allowed_file(imgfile.filename):
136
137
138
            raise UserWarning(
                "Uploaded image is not in the list of allowed file types.")
        filename = os.path.join(
139
140
            config.uploaddir, secure_filename(imgfile.filename))
        imgfile.save(filename)
141
        img = PythonMagick.Image(filename)
142
        imgname = os.path.splitext(secure_filename(imgfile.filename))[
143
144
145
146
147
148
149
            0].replace('.', '_') + '.png'
        savedfilename = os.path.join(config.imagedir, imgname)
        img.write(savedfilename)
        os.remove(filename)
        return imgname
    return None

150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
def make_thumb(filename, maxgeometry):
    thumbpath = filename + '.' + str(maxgeometry)
    if not os.path.exists(thumbpath) or os.path.getmtime(filename) > os.path.getmtime(thumbpath):
        img = PythonMagick.Image(str(filename))
        img.transform("%sx%s" % (maxgeometry, maxgeometry))
        img.quality(90)
        img.write(str("png:%s" % thumbpath))
    return thumbpath



@app.route('/')
def index(**kwargs):
    data = defaultdict(str)
    data.update(**kwargs)
    filelist = glob.glob(config.datadir + '/*.schild')
Robin Sonnabend's avatar
Robin Sonnabend committed
166
    data['files'] = [str(os.path.basename(f)) for f in sorted(filelist)]
167
168
169
170
171
172
173
    return render_response('index.html', data)


@app.route('/edit')
def edit(**kwargs):
    data = defaultdict(str)
    data.update(**kwargs)
Dave Kliczbor's avatar
Dave Kliczbor committed
174
    imagelist = sorted(glob.glob(config.imagedir + '/*.png'))
175
176
    data['images'] = [os.path.basename(f) for f in imagelist]
    templatelist = glob.glob(config.textemplatedir + '/*.tex')
Robin Sonnabend's avatar
Robin Sonnabend committed
177
    data['templates'] = [str(os.path.basename(f))
178
                         for f in sorted(templatelist)]
Dave Kliczbor's avatar
Dave Kliczbor committed
179
    data['imageextensions'] = config.allowed_extensions
180
181
182
183
184
185
186
    return render_response('edit.html', data)


@app.route('/edit/<filename>')
def edit_one(filename):
    return edit(form=load_data(filename))

Dave Kliczbor's avatar
Dave Kliczbor committed
187
188
189

@app.route('/create', methods=['POST'])
def create():
190
191
192
    if request.method == 'POST':
        formdata = defaultdict(str, request.form.to_dict(flat=True))
        for a in ('headline', 'text'):
Robin Sonnabend's avatar
Robin Sonnabend committed
193
            formdata[a] = str(formdata[a])
194
195
196
197
198
199
        try:
            imgpath = save_and_convert_image_upload('imgupload')
            if imgpath is not None:
                formdata['img'] = imgpath
            outfilename = secure_filename(formdata['headline'][:16]) + str(hash(formdata['headline'] + formdata[
                'text'] + os.path.splitext(formdata['textemplate'])[0] + os.path.splitext(formdata['img'])[0])) + '.schild'
200
201
            if formdata['reusefilename']:
                outfilename = secure_filename(formdata['filename'])
202
            outpdfname = outfilename + '.pdf'
203
            formdata['filename'] = outfilename
204
            formdata['pdfname'] = outpdfname
205
            save_data(formdata, outfilename)
206
            run_pdflatex(formdata, os.path.join(config.pdfdir, outpdfname))
207
            try:
Robin Sonnabend's avatar
Robin Sonnabend committed
208
                flash(Markup("""PDF created and data saved. You might create another one. Here's a preview. Click to print.<br/>
209
                                <a href="%s"><img src="%s"/></a>""" %
210
211
212
                         (url_for('schild', filename=outfilename), url_for(
                             'pdfthumbnail', pdfname=outpdfname, maxgeometry=200))
                         ))
213
            except:
Robin Sonnabend's avatar
Robin Sonnabend committed
214
                print(("%s created" % outpdfname))
215
        except Exception as e:
216
            try:
Robin Sonnabend's avatar
Robin Sonnabend committed
217
                flash("Could not create pdf or save data: %s" % str(e), 'error')
218
            except:
Robin Sonnabend's avatar
Robin Sonnabend committed
219
                print(("Could not create pdf or save data: %s" % str(e)))
220
221
222
223
224
225

        data = {'form': formdata}
        imagelist = glob.glob(config.imagedir + '/*.png')
        data['images'] = [os.path.basename(f) for f in imagelist]
        templatelist = glob.glob(config.textemplatedir + '/*.tex')
        data['templates'] = [os.path.basename(f) for f in sorted(templatelist)]
226
227
228
229
230
231
232
233
234
        try:
            return redirect(url_for('edit_one', filename=outfilename))
        except:
            pass
    try:
        flash("No POST data. You've been redirected to the edit page.", 'warning')
        return redirect(url_for('edit'))
    except:
        pass
Dave Kliczbor's avatar
Dave Kliczbor committed
235
236
237
238


@app.route('/schild/<filename>')
def schild(filename):
Robin Sonnabend's avatar
Robin Sonnabend committed
239
    return render_response('schild.html', {'filename': filename, 'printer': [str(f) for f in sorted(config.printers.keys())]})
240

Dave Kliczbor's avatar
Dave Kliczbor committed
241
242
243

@app.route('/printout', methods=['POST'])
def printout():
244
245
246
247
248
249
250
251
    filename = os.path.join(
        config.pdfdir, secure_filename(request.form['filename']))
    printer = config.printers[request.form['printer']]
    copies = int(request.form['copies']) or 0
    if copies > 0 and copies <= 6:
        try:
            lprout = check_output(['lpr', '-H', str(config.printserver), '-P', str(
                printer), '-#', str(copies)] + config.lproptions + [filename], stderr=STDOUT)
Robin Sonnabend's avatar
Robin Sonnabend committed
252
            flash('Schild wurde zum Drucker geschickt!')
253
254
255
        except CalledProcessError as e:
            flash(Markup("<p>Could not print:</p><pre>%s</pre>" % e.output), 'error')
    else:
Robin Sonnabend's avatar
Robin Sonnabend committed
256
        flash('Ungültige Anzahl Kopien!')
257
258
    return redirect(url_for('index'))

Dave Kliczbor's avatar
Dave Kliczbor committed
259
def delete_file(filename):
260
261
262
263
    try:
        os.unlink(os.path.join(config.datadir, filename))
        for f in glob.glob(os.path.join(config.pdfdir, filename + '.pdf*')):
            os.unlink(f)
Robin Sonnabend's avatar
Robin Sonnabend committed
264
        flash("Schild %s wurde gelöscht" % filename)
265
266
        return redirect(url_for('index'))
    except:
Robin Sonnabend's avatar
Robin Sonnabend committed
267
        flash("Schild %s konnte nicht gelöscht werden." % filename, 'error')
268
269
        return redirect(url_for('schild', filename=filename))

Dave Kliczbor's avatar
Dave Kliczbor committed
270

Dave Kliczbor's avatar
Dave Kliczbor committed
271
272
273
274
275
276
277
278
279
280
@app.route('/delete', methods=['POST'])
def delete():
    return delete_file(secure_filename(request.form['filename']))

@app.route('/deletelist', methods=['POST'])
def deletelist():
    for filename in request.form.getlist('filenames'):
        delete_file(secure_filename(filename))
    return redirect(url_for('index'))

Dave Kliczbor's avatar
Dave Kliczbor committed
281
282
@app.route('/image/<imgname>')
def image(imgname):
283
284
285
286
287
288
289
290
    imgpath = os.path.join(config.imagedir, secure_filename(imgname))
    # print(imgpath)
    if os.path.exists(imgpath):
        with open(imgpath, 'r') as imgfile:
            return Response(imgfile.read(), mimetype="image/png")
    else:
        return "Meh"  # redirect(url_for('index'))

Dave Kliczbor's avatar
Dave Kliczbor committed
291
292
293

@app.route('/thumbnail/<imgname>/<int:maxgeometry>')
def thumbnail(imgname, maxgeometry):
294
295
296
297
298
    imgpath = os.path.join(config.imagedir, secure_filename(imgname))
    thumbpath = make_thumb(imgpath, maxgeometry)
    with open(thumbpath, 'r') as imgfile:
        return Response(imgfile.read(), mimetype="image/png")

Dave Kliczbor's avatar
Dave Kliczbor committed
299
300
301

@app.route('/pdfthumb/<pdfname>/<int:maxgeometry>')
def pdfthumbnail(pdfname, maxgeometry):
302
303
304
305
306
    pdfpath = os.path.join(config.pdfdir, secure_filename(pdfname))
    thumbpath = make_thumb(pdfpath, maxgeometry)
    with open(thumbpath, 'r') as imgfile:
        return Response(imgfile.read(), mimetype="image/png")

Dave Kliczbor's avatar
Dave Kliczbor committed
307

308
309
@app.route('/tplthumb/<tplname>/<int:maxgeometry>')
def tplthumbnail(tplname, maxgeometry):
310
311
312
313
314
    pdfpath = os.path.join(config.cachedir, secure_filename(tplname) + '.pdf')
    try:
        run_pdflatex(
            {'textemplate': secure_filename(tplname),
             'img': 'pictograms-nps-misc-camera.png',
Robin Sonnabend's avatar
Robin Sonnabend committed
315
316
             'headline': 'Überschrift',
             'text': 'Dies ist der Text, der in der UI als Text bezeichnet ist.',
317
             'markup': 'latex',
318
319
320
321
322
323
324
325
326
             }, pdfpath, overwrite=False
        )
    except Exception as e:
        return str(e)
    else:
        thumbpath = make_thumb(pdfpath, maxgeometry)
        with open(thumbpath, 'r') as imgfile:
            return Response(imgfile.read(), mimetype="image/png")

327

Dave Kliczbor's avatar
Dave Kliczbor committed
328
329
@app.route('/pdfdownload/<pdfname>')
def pdfdownload(pdfname):
330
331
332
    pdfpath = os.path.join(config.pdfdir, secure_filename(pdfname))
    with open(pdfpath, 'r') as pdffile:
        return Response(pdffile.read(), mimetype="application/pdf")
Dave Kliczbor's avatar
Dave Kliczbor committed
333

334

335
336
337
338
339
340
def recreate_cache():
    for filename in (glob.glob(os.path.join(config.pdfdir, '*.pdf*')) +
             glob.glob(os.path.join(config.cachedir, '*.pdf*')) +
             glob.glob(os.path.join(config.imagedir, '*.png.*'))):
        try:
            os.unlink(filename)
Robin Sonnabend's avatar
Robin Sonnabend committed
341
            print(("Deleted %s" % filename))
342
        except Exception as e:
Robin Sonnabend's avatar
Robin Sonnabend committed
343
            print(("Could not delete %s: %s" % (filename, str(e))))
344
345
346
    for filename in glob.glob(os.path.join(config.datadir, '*.schild')):
        data = load_data(filename)
        pdfname = os.path.join(config.pdfdir, data['pdfname'])
Robin Sonnabend's avatar
Robin Sonnabend committed
347
        print(("Recreating %s" % pdfname))
348
        run_pdflatex(data, pdfname)
349

Dave Kliczbor's avatar
Dave Kliczbor committed
350
if __name__ == '__main__':
351
352
353
354
355
    if len(sys.argv) > 1 and sys.argv[1] == '--recreate-cache':
        recreate_cache()
    else:
        app.debug = True
        app.run(host=config.listen, port=config.port)