diff --git a/.gitignore b/.gitignore index d1ace526db91403de069bd8c681bf839c39e669d..0f8d756bba90badd24c148003e6bf3532c74cca3 100644 --- a/.gitignore +++ b/.gitignore @@ -38,7 +38,9 @@ savegames config.py schilder.wsgi -*.png.* -*.schild.* +*.png_* +*.schild_* venv/ +data/cache/ +data/pdf/ diff --git a/requirements.txt b/requirements.txt index 93aaf2a934547cd0236a8071ebd4ad1dce5a3c4f..a3238cbd315f51e4da7240de5c3131ff9b50899d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ Flask -Flask-Genshi -Genshi docutils +pillow +pdf2image diff --git a/schilder.py b/schilder.py index 4242b4736c3b766839630c96a7abbf24b9f4be80..b8e63f47910c4a017fa9dbe2b2f8a225c2fd06f2 100755 --- a/schilder.py +++ b/schilder.py @@ -1,25 +1,20 @@ #!/usr/bin/env python3 -# -*- encoding: utf8 -*- -from flask import Flask, flash, session, redirect, url_for, escape, request, Response, Markup +from flask import Flask, flash, session, redirect, url_for, escape, request, Response, Markup, render_template +from jinja2 import Environment, PackageLoader import sys import os import os.path import glob import werkzeug -# genshi expects old location, fix -werkzeug.cached_property = werkzeug.utils.cached_property -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 from docutils.core import publish_parts +from PIL import Image +from pdf2image import convert_from_path import warnings import shutil -import subprocess -from subprocess import CalledProcessError, STDOUT -import PythonMagick +import subprocess as sp import json import tempfile import config @@ -31,43 +26,7 @@ app.config.update( MAX_CONTENT_LENGTH = 8388608 ) 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 @@ -87,11 +46,14 @@ def save_data(formdata, outfilename): def run_pdflatex(context, outputfilename, overwrite=True): if 'textemplate' not in context: 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 + texenv = Environment(loader=PackageLoader("schilder", config.textemplatedir)) + texenv.variable_start_string = "${" + texenv.variable_end_string = "}" + texenv.block_start_string = "${{{{{" + texenv.block_end_string = "}}}}}" + template = texenv.get_template(context['textemplate']) + #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'] #context['headline'] = publish_parts(context['headline'], writer_name='latex')['body'] @@ -108,22 +70,17 @@ def run_pdflatex(context, outputfilename, overwrite=True): 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) + texfile.write(template.render(form=context)) os.symlink(config.texsupportdir, os.path.join(tmpdir, 'support')) try: - texlog = check_output( - ['pdflatex', '--halt-on-error', tmptexfile], stderr=STDOUT) - except CalledProcessError as e: + texlog = sp.check_output(['pdflatex', '--halt-on-error', tmptexfile], stderr=sp.STDOUT, cwd=tmpdir, universal_newlines=True) + except sp.CalledProcessError as e: if overwrite: try: flash(Markup("<p>PDFLaTeX Output:</p><pre>%s</pre>" % e.output), 'log') except: print((e.output)) raise SyntaxWarning("PDFLaTeX bailed out") - finally: - os.chdir(cwd) if overwrite: try: flash(Markup("<p>PDFLaTeX Output:</p><pre>%s</pre>" % texlog), 'log') @@ -141,22 +98,21 @@ def save_and_convert_image_upload(inputname): filename = os.path.join( config.uploaddir, secure_filename(imgfile.filename)) imgfile.save(filename) - img = PythonMagick.Image(filename) + img = Image.open(filename) imgname = os.path.splitext(secure_filename(imgfile.filename))[ 0].replace('.', '_') + '.png' savedfilename = os.path.join(config.imagedir, imgname) - img.write(savedfilename) + img.save(savedfilename) os.remove(filename) return imgname return None def make_thumb(filename, maxgeometry): - thumbpath = filename + '.' + str(maxgeometry) + thumbpath = (filename.replace(config.imagedir, config.cachedir) + '_' + str(maxgeometry)).replace(".", "_") + ".png" 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)) + img = Image.open(str(filename)) + img = img.resize((maxgeometry, maxgeometry)) + img.save(thumbpath) return thumbpath @@ -167,7 +123,7 @@ def index(**kwargs): data.update(**kwargs) filelist = glob.glob(config.datadir + '/*.schild') data['files'] = [str(os.path.basename(f)) for f in sorted(filelist)] - return render_response('index.html', data) + return render_template('index.html', **data) @app.route('/edit') @@ -180,7 +136,7 @@ def edit(**kwargs): data['templates'] = [str(os.path.basename(f)) for f in sorted(templatelist)] data['imageextensions'] = config.allowed_extensions - return render_response('edit.html', data) + return render_template('edit.html', **data) @app.route('/edit/<filename>') @@ -239,7 +195,7 @@ def create(): @app.route('/schild/<filename>') def schild(filename): - return render_response('schild.html', {'filename': filename, 'printer': [str(f) for f in sorted(config.printers.keys())]}) + return render_template('schild.html', filename=filename, printer=[str(f) for f in sorted(config.printers.keys())]) @app.route('/printout', methods=['POST']) @@ -250,10 +206,10 @@ def printout(): 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) + lprout = sp.check_output(['lpr', '-H', str(config.printserver), '-P', str( + printer), '-#', str(copies)] + config.lproptions + [filename], stderr=sp.STDOUT) flash('Schild wurde zum Drucker geschickt!') - except CalledProcessError as e: + except sp.CalledProcessError as e: flash(Markup("<p>Could not print:</p><pre>%s</pre>" % e.output), 'error') else: flash('Ungültige Anzahl Kopien!') @@ -286,7 +242,7 @@ 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: + with open(imgpath, 'rb') as imgfile: return Response(imgfile.read(), mimetype="image/png") else: return "Meh" # redirect(url_for('index')) @@ -296,15 +252,18 @@ def image(imgname): def thumbnail(imgname, maxgeometry): imgpath = os.path.join(config.imagedir, secure_filename(imgname)) thumbpath = make_thumb(imgpath, maxgeometry) - with open(thumbpath, 'r') as imgfile: + with open(thumbpath, 'rb') 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: + pngpath = pdfpath.replace(".", "_") + ".png" + img = convert_from_path(pdfpath)[0] + img.save(pngpath) + thumbpath = make_thumb(pngpath, maxgeometry) + with open(thumbpath, 'rb') as imgfile: return Response(imgfile.read(), mimetype="image/png") @@ -321,17 +280,21 @@ def tplthumbnail(tplname, maxgeometry): }, pdfpath, overwrite=False ) except Exception as e: + raise e return str(e) else: - thumbpath = make_thumb(pdfpath, maxgeometry) - with open(thumbpath, 'r') as imgfile: + pngpath = pdfpath.replace(".", "_") + ".png" + img = convert_from_path(pdfpath)[0] + img.save(pngpath) + thumbpath = make_thumb(pngpath, maxgeometry) + with open(thumbpath, 'rb') 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: + with open(pdfpath, 'rb') as pdffile: return Response(pdffile.read(), mimetype="application/pdf") diff --git a/templates/edit.html b/templates/edit.html index 1bc46b47d0431ed12746e085cd5955f91e8ba8db..e5084fcc3e09bee63b5512946324d4d966c1ece9 100644 --- a/templates/edit.html +++ b/templates/edit.html @@ -1,77 +1,87 @@ <!DOCTYPE html> -<!-- <html xmlns="http://www.w3.org/1999/xhtml" > --> -<html xmlns:py="http://genshi.edgewall.org/"> +<html> <head> - <link rel='stylesheet' type='text/css' href="${ url_for('static', filename='main.css') }"/> + <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"> - <div class="box"> - <label for="form:template">Wähle eine TeX-Vorlage:</label> - <ul py:attrs="{'class':'collapsed'} if defined('form') else {}"> - <li py:for="textemplate in templates"> - <input type="radio" name="textemplate" id="tpl:${textemplate}" value="${textemplate}" py:attrs="{'checked':'checked', 'onfocus':'this.parentElement.parentElement.className=\'\';'} if defined('form') and textemplate == form.textemplate else {}"/> - <label for="tpl:${textemplate}"><img src="${ url_for('tplthumbnail', tplname=textemplate, maxgeometry=80) }" alt="${textemplate}" title="${textemplate}"/></label> - </li> - <li class="onlywhencollapsed"> - <button onclick="this.parentElement.parentElement.className=''; return false;" >Auswahl anzeigen</button> - </li> - </ul> - </div><br/> - <div class="box"> - <label for="form:headline">Überschrift</label> - <textarea name="headline" id="form:headline" cols="35" rows="5"><py:if test="defined('form')">${form.headline}</py:if></textarea> - </div> - <div class="box"> - <label for="form:text">Text</label> - <select name="markup"> - <option value="latex" py:attrs="{'selected':'True'} if defined('form') and form.markup == 'latex' else {}">LaTeX</option> - <option value="rst" py:attrs="{'selected':'True'} if not defined('form') or form.markup == 'rst' or form.markup == '' else {}">Wiki (reStructuredText)</option> - </select> - <textarea name="text" id="form:text" cols="35" rows="5"><py:if test="defined('form')">${form.text}</py:if></textarea> - </div><br/> - <div class="box imageselect"> - <label for="form:img">Wähle ein Bild (falls auf Vorlage anwendbar):</label> - <ul> - <li> - <input type="radio" name="img" id="img--none" value="__none" py:attrs="{'checked':'checked'} if defined('form') and form.img == '__none' else {}"/> - <label for="img--none">Kein Bild</label> - </li> - <li> - <input type="radio" name="img" id="img--upload" value="__upload"/> - <input type="file" name="imgupload"/> - <label for="img--upload">Bild hochladen (${', '.join(imageextensions)})</label> - </li> - </ul> - <ul py:attrs="{'class':'collapsed'} if defined('form') else {}"> - <li py:for="img in images"> - <input type="radio" name="img" id="img:${img}" value="${img}" py:attrs="{'checked':'checked', 'onfocus':'this.parentElement.parentElement.className=\'\';'} if defined('form') and form.img == img else {}"/> - <label for="img:${img}"><img src="${ url_for('thumbnail', imgname=img, maxgeometry=100) }" alt="${img}" title="${img}"/></label> - </li> - <li class="onlywhencollapsed"> - <button onclick="this.parentElement.parentElement.className=''; return false;" >Auswahl anzeigen</button> - </li> - </ul> - </div> + {% with messages = get_flashed_messages(with_categories=True) %} + {% if messages %} + <ul class="flashes"> + {% for category, message in messages %} + <li class="{{ category }}" tabindex="0">{{ message }}</li> + {% endfor %} + </ul> + {% endif %} + {% endwith %} + <a href="{{ url_for('index') }}">Liste der fertigen Schilder</a> + + <form method="post" action="{{ url_for('create') }}" enctype="multipart/form-data"> + <div class="box"> + <label for="form:template">Wähle eine TeX-Vorlage:</label> + <ul class="{% if form %}collapsed{% endif %}"> + {% for textemplate in templates %} + <li> + <input type="radio" name="textemplate" id="tpl:{{textemplate}}" value="{{textemplate}}" {% if form and textemplate == form.textemplate %} checked="checked" onfocus="this.parentElement.parentElement.className='';" {% endif %} /> + <label for="tpl:{{textemplate}}"><img src="{{ url_for('tplthumbnail', tplname=textemplate, maxgeometry=80) }}" alt="{{textemplate}}" title="{{textemplate}}"/></label> + </li> + <li class="onlywhencollapsed"> + <button onclick="this.parentElement.parentElement.className=''; return false;" >Auswahl anzeigen</button> + </li> + {% endfor %} + </ul> + </div><br/> + <div class="box"> + <label for="form:headline">Überschrift</label> + <textarea name="headline" id="form:headline" cols="35" rows="5">{% if form %}{{form.headline}}{% endif %}</textarea> + </div> + <div class="box"> + <label for="form:text">Text</label> + <select name="markup"> + <option value="latex" {% if form and form.markup == "latex" %}selected{% endif %}>LaTeX</option> + <option value="rst" {% if form and (form.markup == "rst" or form.markup == "") %}selected{% endif %}>Wiki (reStructuredText)</option> + </select> + <textarea name="text" id="form:text" cols="35" rows="5">{% if form %}{{form.text}}{% endif %}</textarea> + </div> + <br/> + <div class="box imageselect"> + <label for="form:img">Wähle ein Bild (falls auf Vorlage anwendbar):</label> + <ul> + <li> + <input type="radio" name="img" id="img--none" value="__none" {% if form and form.img == "__none" %}checked{% endif %}/> + <label for="img--none">Kein Bild</label> + </li> + <li> + <input type="radio" name="img" id="img--upload" value="__upload"/> + <input type="file" name="imgupload"/> + <label for="img--upload">Bild hochladen ({{', '.join(imageextensions)}})</label> + </li> + </ul> + <ul {% if form %}class="collapsed"{% endif %}> + {% for img in images %} + <li> + <input type="radio" name="img" id="img:{{img}}" value="{{img}}" {% if form and form.img == img %}checked="checked" onfocus="this.parentElement.parentElement.className='';"{% endif %} /> + <label for="img:{{img}}"> + <img src="{{ url_for('thumbnail', imgname=img, maxgeometry=100) }}" alt="{{img}}" title="{{img}}"/> + </label> + </li> + {% endfor %} + <li class="onlywhencollapsed"> + <button onclick="this.parentElement.parentElement.className=''; return false;" >Auswahl anzeigen</button> + </li> + </ul> + </div> + <br/> + <div class="box"> + {% if form and form.filename|length > 5 %} + <input type="hidden" name="filename" value="{{form.filename}}"/> + <input id="form:reusefilename" type="checkbox" name="reusefilename"/> + <label for="form:reusefilename">Überschreibe bisherige Version von {{form.filename}}.</label> <br/> - <div class="box"> - <py:if test="defined('form') and len(form.filename) > 5"> - <input type="hidden" name="filename" value="${form.filename}"/> - <input id="form:reusefilename" type="checkbox" name="reusefilename"/> - <label for="form:reusefilename">Überschreibe bisherige Version von ${form.filename}.</label> - <br/> - </py:if> - <input type="submit" value="Schild erstellen"/> - </div> - </form> + {% endif %} + <input type="submit" value="Schild erstellen"/> + </div> + </form> </body> </html> diff --git a/templates/index.html b/templates/index.html index 95507ad520d8d617388efaf8601031818724922f..aec1c939b776c22747c687af6efeeef7204c7ff1 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1,32 +1,37 @@ <!DOCTYPE html> -<html xmlns="http://www.w3.org/1999/xhtml" - xmlns:py="http://genshi.edgewall.org/"> +<html> <head> - <link rel='stylesheet' type='text/css' href="${ url_for('static', filename='main.css') }"/> + <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> + {% with messages = get_flashed_messages(with_categories=True) %} + {% if messages %} + <ul class="flashes"> + {% for category, message in messages %} + <li class="{{ category }}" tabindex="0">{{ message }}</li> + {% endfor %} + </ul> + {% endif %} + {% endwith %} - <a href="${ url_for('edit') }">Neues Schild</a> + <a href="{{ url_for('edit') }}">Neues Schild</a> - <py:if test="defined('files')"> - <form method="POST" action="${ url_for('deletelist') }"> - <ul> - <li py:for="file in files"> - <input id="form:${file}" type="checkbox" name="filenames" value="${file}"/> - <a href="${ url_for('schild', filename=file) }" title="${file}"> - <img src="${ url_for('pdfthumbnail', pdfname=file+'.pdf', maxgeometry=150) }"/> - </a> - </li> - </ul> - <input type="submit" value="Ausgewählte Schilder löschen"/> - </form> - </py:if> + {% if files %} + <form method="POST" action="{{ url_for('deletelist') }}"> + <ul> + {% for file in files %} + <li> + <input id="form:{{file}}" type="checkbox" name="filenames" value="{{file}}"/> + <a href="{{ url_for('schild', filename=file) }}" title="{{file}}"> + <img src="{{ url_for('pdfthumbnail', pdfname=file+'.pdf', maxgeometry=150) }}"/> + </a> + </li> + {% endfor %} + </ul> + <input type="submit" value="Ausgewählte Schilder löschen"/> + </form> + {% endif %} </body> </html> diff --git a/templates/schild.html b/templates/schild.html index 403c5313d545a0d31b14ddae858a11ceb3d20868..0b43ac22c0ec97493b9e2059bab528d2b1785c50 100644 --- a/templates/schild.html +++ b/templates/schild.html @@ -1,34 +1,41 @@ <!DOCTYPE html> -<html xmlns="http://www.w3.org/1999/xhtml" - xmlns:py="http://genshi.edgewall.org/"> +<html> <head> - <link rel='stylesheet' type='text/css' href="${ url_for('static', filename='main.css') }"/> + <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> - <a href="${ url_for('index') }">Liste der Schilder</a> - <a href="${ url_for('edit') }">Neues Schild</a> - <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">${ message }</li> - </ul> - </py:with> - <img class="bigpreview" src="${ url_for('pdfthumbnail', pdfname=filename+'.pdf', maxgeometry=300) }"/> - <form method="post" action="${ url_for('printout') }"> - <input type="hidden" name="filename" value="${filename + '.pdf'}" /> + <a href="{{ url_for('index') }}">Liste der Schilder</a> + <a href="{{ url_for('edit') }}">Neues Schild</a> + {% with messages = get_flashed_messages(with_categories=True) %} + {% if messages %} + <ul class="flashes"> + {% for category, message in messages %} + <li class="{{ category }}">{{ message }}</li> + {% endfor %} + </ul> + {% endif %} + {% endwith %} + <img class="bigpreview" src="{{ url_for('pdfthumbnail', pdfname=filename+'.pdf', maxgeometry=300) }}"/> + <form method="post" action="{{ url_for('printout') }}"> + <input type="hidden" name="filename" value="{{filename + '.pdf'}}" /> <select name="copies"> - <option py:for="x in range(1,11)" value="${x}" label="${x} Kopien">${x} Kopien</option> + {% for x in range(1, 11) %} + <option value="{{x}}" label="{{x}} Kopien">{{x}} Kopien</option> + {% endfor %} </select> <select name="printer"> - <option py:for="pr in printer" value="${pr}" label="Drucker ${pr}">Drucker ${pr}</option> - </select> + {% for pr in printer %} + <option value="{{pr}}" label="Drucker {{pr}}">Drucker {{pr}}</option> + {% endfor %} + </select> <input type="submit" value="Schild drucken" /> </form> - <a href="${ url_for('pdfdownload', pdfname=filename + '.pdf') }">Schild als PDF herunterladen.</a> - <a href="${ url_for('edit_one', filename=filename) }">Schild bearbeiten/als Vorlage verwenden.</a> - <form method="post" action="${ url_for('delete') }"> - <input type="hidden" name="filename" value="${filename}" /> + <a href="{{ url_for('pdfdownload', pdfname=filename + '.pdf') }}">Schild als PDF herunterladen.</a> + <a href="{{ url_for('edit_one', filename=filename) }}">Schild bearbeiten/als Vorlage verwenden.</a> + <form method="post" action="{{ url_for('delete') }}"> + <input type="hidden" name="filename" value="{{filename}}" /> <input type="submit" value="Schild löschen" /> </form> </body>