Commit b1cf86dc authored by Dave Kliczbor's avatar Dave Kliczbor

Version used in ZKK15

parents
Schildergenerator
=================
A web page to quickly create and print signs using a common design.
Especially useful for events.
Dependencies
------------
* python-flask python-genshi python-pythonmagick
* pdflatex latex-beamer
* libapache2-mod-wsgi (if not used in debug mode)
Config
------
* copy config.py.example to config.py and edit it to your needs.
* copy schildergen.wsgi.example to schildergen.wsgi and edit it.
Apache Config
-------------
See also http://flask.pocoo.org/docs/deploying/mod_wsgi/
LoadModule wsgi_module /usr/lib/apache2/modules/mod_wsgi.so
WSGIRestrictStdout Off
<VirtualHost *:443>
ServerAdmin admin@server.test
DocumentRoot /path/to/schildergen
ServerName server.name.org
AddDefaultCharset utf-8
ErrorLog /path/to/log
CustomLog /path/to/log
SSLEngine on
SSLCertificateFile /path/to/www.example.com.cert
SSLCertificateKeyFile /path/to/www.example.com.key
WSGIDaemonProcess schildergen user=www-data group=www-data threads=2
WSGIScriptAlias / /path/to/schildergen.wsgi
<Directory /path/to/schildergen.wsgi>
AllowOverride All
WSGIProcessGroup schildergen
WSGIApplicationGroup %{GLOBAL}
WSGIScriptReloading On
Order deny,allow
Allow from all
</Directory>
</VirtualHost>
Contributors
============
* Dave Kliczbor <dave@fsinfo.cs.tu-dortmund.de>
* Lars Beckers <larsb@fsmpi.rwth-aachen.de>
* Moritz Holtz <moritz@fsmpi.rwth-aachen.de>
* Konstantin Kotenko <konstantin@fsmpi.rwth-aachen.de>
Image Sources
-------------
* USNPS pictograms taken from the Open Icon Library: http://sourceforge.net/projects/openiconlibrary/
#### BASIC CONFIGURATION
# Secret key (used for session cookie encryption). Needs to be set to some random string.
# Yes, just smash your keyboard for some random characters. No, don't publish them anywhere.
# Yes, you will need this. If you get random RuntimeErrors, you did not set this.
app_secret = ''
## You will need to use absolute paths!
# Base directory. You need to set this again in schilder.wsgi if you use WSGI.
basedir = '/home/dave/Development/schildergenerator'
# Temp directory for imagemagick/pdflatex work files (needs to be writeable)
tmpdir = '/tmp'
## All following directories derive from basedir, you don't really need to alter them
# Data directory (needs to be writeable)
datadir = basedir + '/data'
# HTML template directory
templatedir = basedir + '/templates'
# TeX template directory
textemplatedir = basedir + '/tex'
# TeX support file directory (all files that might be needed by a tex template)
texsupportdir = textemplatedir + '/support'
# PDF data directory (needs to be writeable)
pdfdir = datadir + '/pdf'
# Image data directory (needs to be writeable)
imagedir = datadir + '/images'
# Upload temp directory (needs to be writeable)
uploaddir = datadir + '/upload'
# allowed image upload file extensions
allowed_extensions = set(['png', 'jpg', 'jpeg', 'gif'])
#### PRINTER OPTIONS
# CUPS printer names
printers = {
'Human readable printer description' : 'CUPS-ID-String',
'Color Printer in room 1337' : 'Brother_ColorLaserJet_6V',
'B/W Printer in room 0' : 'HP_HL-38281',
}
printserver = 'localhost'
# additional lpr options. Use an empty list if not needed.
lproptions=['-Fa4g', '-N1', '-o fitplot']
#### DEVELOPERS ONLY
# Listening interface and port, usually '127.0.0.1' or '0.0.0.0'
# Only effective if started from command line (instead via webserver/WSGI),
# therefore these options would only be interesting to a developer.
listen = '127.0.0.1'
port = 5432
{"headline": "Headline", "text": "Additional text.", "pdfname": "Headline-417648766.schild.pdf", "img": "pictograms-nps-showers.png", "textemplate": "headline-top_arrowup_image-right.tex"}
\ No newline at end of file
{"headline": "Headline", "text": "Additional text.", "pdfname": "Headline1053846357.schild.pdf", "img": "__none", "textemplate": "headline-top_arrowright_text-right.tex"}
\ No newline at end of file
{"headline": "Headline", "text": "Additional text.", "pdfname": "Headline2075177871.schild.pdf", "img": "pictograms-nps-showers.png", "textemplate": "headline-top_arrowup_text-right.tex"}
\ No newline at end of file
#!/usr/bin/python
# -*- encoding: utf8 -*-
from flask import Flask, flash, session, redirect, url_for, escape, request, Response, Markup
import sys, os, os.path, glob
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
import warnings
import shutil
import subprocess
from subprocess import CalledProcessError, STDOUT
import PythonMagick
import json
import tempfile
import config
app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = config.uploaddir
app.config['PROPAGATE_EXCEPTIONS'] = True
app.secret_key = config.app_secret
genshi = Genshi(app)
genshi.extensions['html'] = 'html5'
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):
return '.' in filename and filename.rsplit('.', 1)[1] in config.allowed_extensions
@app.route('/')
def index(**kwargs):
data = defaultdict(str)
data.update(**kwargs)
filelist = glob.glob(config.datadir + '/*.schild')
data['files'] = [ unicode(os.path.basename(f)) for f in sorted(filelist) ]
return render_response('index.html', data)
@app.route('/edit')
def edit(**kwargs):
data = defaultdict(str)
data.update(**kwargs)
imagelist = glob.glob(config.imagedir + '/*.png')
data['images'] = [ os.path.basename(f) for f in imagelist ]
templatelist = glob.glob(config.textemplatedir + '/*.tex')
data['templates'] = [ unicode(os.path.basename(f)) for f in sorted(templatelist) ]
return render_response('edit.html', data)
@app.route('/edit/<filename>')
def edit_one(filename):
with open(os.path.join(config.datadir, filename), 'r') as infile:
formdata = json.load(infile)
return edit(form=formdata)
def run_pdflatex(context, outputfilename):
if not context.has_key('textemplate'):
context['textemplate'] = "text-image-quer.tex"
genshitex = TemplateLoader([config.textemplatedir])
template = genshitex.load(context['textemplate'], cls=NewTextTemplate, encoding='utf8')
tmpdir = tempfile.mkdtemp(dir=config.tmpdir)
if context.has_key('img') and context['img'] and context['img'] != '__none':
try:
shutil.copy(os.path.join(config.imagedir, context['img']), os.path.join(tmpdir, context['img']))
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'))
try:
texlog = check_output(['pdflatex', '--halt-on-error', tmptexfile], stderr=STDOUT)
except CalledProcessError as e:
flash(Markup("<p>PDFLaTeX Output:</p><pre>%s</pre>" % e.output), 'log')
raise SyntaxWarning("PDFLaTeX bailed out")
finally:
os.chdir(cwd)
flash(Markup("<p>PDFLaTeX Output:</p><pre>%s</pre>" % texlog), 'log')
shutil.copy(tmppdffile, outputfilename)
shutil.rmtree(tmpdir)
def save_and_convert_image_upload(inputname):
file = request.files[inputname]
if file:
if not allowed_file(file.filename):
raise UserWarning("Uploaded image is not in the list of allowed file types.")
filename = os.path.join(config.uploaddir, secure_filename(file.filename))
file.save(filename)
img = PythonMagick.Image(filename)
imgname = os.path.splitext(secure_filename(file.filename))[0].replace('.', '_') + '.png'
savedfilename = os.path.join(config.imagedir, imgname)
img.write(savedfilename)
os.remove(filename)
return imgname
return None
@app.route('/create', methods=['POST'])
def create():
if request.method == 'POST':
formdata = request.form.to_dict(flat=True)
for a in ('headline', 'text'):
formdata[a] = unicode(formdata[a])
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]))+'.schild'
outpdfname = outfilename + '.pdf'
formdata['pdfname'] = outpdfname
run_pdflatex(formdata, os.path.join(config.pdfdir, outpdfname))
with open(os.path.join(config.datadir, outfilename), 'w') as outfile:
json.dump(formdata, outfile)
flash(Markup(u"""PDF created and data saved. You might create another one. Here's a preview. Click to print.<br/>
<a href="%s"><img src="%s"/></a>""" %
(url_for('schild', filename=outfilename), url_for('pdfthumbnail', pdfname=outpdfname, maxgeometry=200))
))
except Exception as e:
flash(u"Could not create pdf or save data: %s" % str(e), 'error')
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) ]
return render_response('edit.html', data)
flash("No POST data. You've been redirected to the edit page.", 'warning')
return redirect(url_for('edit'))
@app.route('/schild/<filename>')
def schild(filename):
return render_response('schild.html', {'filename':filename, 'printer':[ unicode(f) for f in sorted(config.printers.keys()) ]})
@app.route('/printout', methods=['POST'])
def printout():
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)
flash(u'Schild wurde zum Drucker geschickt!')
except CalledProcessError as e:
flash(Markup("<p>Could not print:</p><pre>%s</pre>" % e.output), 'error')
else:
flash(u'Ungültige Anzahl Kopien!')
return redirect(url_for('index'))
@app.route('/delete', methods=['POST'])
def delete():
filename = secure_filename(request.form['filename'])
try:
os.unlink(os.path.join(config.datadir, filename))
for f in glob.glob(os.path.join(config.pdfdir, filename + '.pdf*')):
os.unlink(f)
flash(u"Schild %s wurde gelöscht" % filename)
return redirect(url_for('index'))
except:
flash(u"Schild %s konnte nicht gelöscht werden." % filename, 'error')
return redirect(url_for('schild', filename=filename))
@app.route('/image/<imgname>')
def image(imgname):
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'))
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('/thumbnail/<imgname>/<int:maxgeometry>')
def thumbnail(imgname, maxgeometry):
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")
@app.route('/pdfthumb/<pdfname>/<int:maxgeometry>')
def pdfthumbnail(pdfname, maxgeometry):
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")
@app.route('/pdfdownload/<pdfname>')
def pdfdownload(pdfname):
pdfpath = os.path.join(config.pdfdir, secure_filename(pdfname))
with open(pdfpath, 'r') as pdffile:
return Response(pdffile.read(), mimetype="application/pdf")
if __name__ == '__main__':
app.debug = True
app.run(host=config.listen, port=config.port)
import logging, sys
path = '/path/to/schilder'
if path not in sys.path:
sys.path.append(path)
sys.stdout = sys.stderr
logging.basicConfig(stream=sys.stderr)
from schilder import app as application
body {
font-family: sans-serif;
font-size: 0.9rem;
}
body > a {
width:25em;
padding:0.5em;
border:1px solid lightgray;
border-radius:0.5em;
background: linear-gradient(to bottom, #fff 0%,#eee 16%,#ddd 92%,#ccc 100%);
}
@media (max-width:767px) {
body, input, select {
font-size: 1.0rem;
}
body > a {
width: 95%;
padding:0.5em;
border:1px solid lightgray;
border-radius:0.5em;
background: linear-gradient(to bottom, #f0f0f0 0%,#e2e2e2 16%,#d1d1d1 92%,#cecece 100%);
}
img.bigpreview {
width: 100%;
}
}
body > *, form > * {
display: block;
}
.flashes {
margin:0;
padding:0;
}
.flashes li {
list-style-type:none;
background:lightgreen;
display:block;
padding:10px;
margin:5px;
border-radius:1em;
}
.flashes li.warning {
background:yellow;
}
.flashes li.error {
background:red;
color: white;
}
.flashes li.log {
background:#ccc;
max-height:2em;
overflow:auto;
transition: max-height 1s ease;
}
.flashes li.log:focus {
max-height:25em;
}
ul.list, form > ul {
margin-left: 10px;
padding: 0;
}
ul.list li a img {
border: 1px solid gray;
display: block;
}
ul.list li {
border: 1px solid gray;
border-radius: 3px;
display:inline-block;
margin-right:10px;
}
img.bigpreview {
border: 1px solid gray;
border-radius: 5px;
margin-top: 0.5em;
margin-bottom:0.5em;
}
.bigpreview + form > * {
display:inline-block;
}
form {
margin-top:1em;
margin-bottom:1em;
}
form li {
list-style-type:none;
display: inline-block;
}
input[type="radio"] {
position:relative;
z-index:5;
margin-bottom:1em;
}
input[type="radio"]+label > img {
position:relative;
z-index:4;
top:0.5em;
left:-1.5em;
margin-right:-1.2em;
max-width: 100%;
max-height: 10em;
border:3px solid #eee;
border-radius: 5px;
}
input[type="radio"]:checked+label > img {
border:3px dotted red;
}
input[type="radio"] + input[type="file"] {
display:none;
}
input[type="radio"]:checked + input[type="file"] {
display:initial;
}
input[type="radio"]:checked + input[type="file"] + label {
display:none;
}
<!DOCTYPE html>
<!-- <html xmlns="http://www.w3.org/1999/xhtml" > -->
<html xmlns:py="http://genshi.edgewall.org/">
<head>
<link rel='stylesheet' type='text/css' href="${ url_for('static', filename='main.css') }"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>Schildergenerator</title>
</head>
<body>
<py:with vars="messages = get_flashed_messages(with_categories=True)">
<ul class="flashes" py:if="messages">
<li class="${ category }" py:for="category,message in messages" tabindex="0">${ message }</li>
</ul>
</py:with>
<a href="${ url_for('index') }">Liste der fertigen Schilder</a>
<form method="post" action="${ url_for('create') }" enctype="multipart/form-data">
<label for="form:headline">Großer Text</label>
<textarea name="headline" id="form:headline" cols="35" rows="5"><py:if test="defined('form')">${form.headline}</py:if></textarea>
<label for="form:text">Zusatztext</label>
<textarea name="text" id="form:text" cols="35" rows="5"><py:if test="defined('form')">${form.text}</py:if></textarea>
<label for="form:template">TeX-Vorlage</label>
<select name="textemplate" id="form:template">
<py:choose>
<py:when test="defined('form')">
<py:for each="template in templates">
<py:choose>
<py:when test="template == form.textemplate">
<option label="${template}" value="${template}" selected="selected">${template}</option>
</py:when>
<py:otherwise>
<option label="${template}" value="${template}">${template}</option>
</py:otherwise>
</py:choose>
</py:for>
</py:when>
<py:otherwise>