├── requirements.txt ├── .gitignore ├── app ├── __init__.py ├── templates │ ├── login.html │ ├── upload.html │ ├── base.html │ └── task.html ├── static │ ├── style.css │ └── a5a4.js ├── a5a4.py └── tasks.py ├── a5a4.wsgi ├── run.py ├── config.py ├── cgi ├── pdfjam.html └── pdfjamcgi.py └── README.md /requirements.txt: -------------------------------------------------------------------------------- 1 | flask>=0.11 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | venv/ 2 | __pycache__/ 3 | tasks/ 4 | config_local.py 5 | *.pyc 6 | *.swp 7 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | 3 | app = Flask(__name__) 4 | app.config.from_object('config') 5 | from app import a5a4 6 | -------------------------------------------------------------------------------- /app/templates/login.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block content %} 3 |
4 | 5 |

Password:

6 |
7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /app/templates/upload.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block content %} 3 | {% if error %}

{{ error }}

{% endif %} 4 |
5 |

Upload PDF:

6 |
7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /a5a4.wsgi: -------------------------------------------------------------------------------- 1 | import os, sys 2 | BASE_DIR = os.path.abspath(os.path.dirname(__file__)) 3 | sys.path.insert(0, BASE_DIR) 4 | python_dir = [n for n in os.listdir(os.path.join(BASE_DIR, 'venv', 'lib')) if n.startswith('python')][0] 5 | VENV_DIR = os.path.join(BASE_DIR, 'venv', 'lib', python_dir, 'site-packages') 6 | if os.path.exists(VENV_DIR): 7 | sys.path.insert(1, VENV_DIR) 8 | 9 | from app import app as application 10 | -------------------------------------------------------------------------------- /app/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% block title %}{% endblock %}A4A5 6 | 7 | 8 | 9 | 10 | {% block content %}{% endblock %} 11 | 12 | 13 | -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | import sys 4 | BASE_DIR = os.path.abspath(os.path.dirname(__file__)) 5 | sys.path.insert(0, BASE_DIR) 6 | python_dir = [n for n in os.listdir(os.path.join(BASE_DIR, 'venv', 'lib')) if n.startswith('python')][0] 7 | VENV_DIR = os.path.join(BASE_DIR, 'venv', 'lib', python_dir, 'site-packages') 8 | if os.path.exists(VENV_DIR): 9 | sys.path.insert(1, VENV_DIR) 10 | 11 | from app import app 12 | app.run(debug=True) 13 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | import os 2 | BASE_DIR = os.path.abspath(os.path.dirname(__file__)) 3 | PYTHON = 'python3.6' 4 | 5 | DEBUG = False 6 | 7 | MAX_CONTENT_LENGTH = 10*1024*1024 8 | A5A4_MAXFILES = 5 9 | A5A4_MAXPAGES = 12 10 | 11 | # paths 12 | IDENTIFY = '/usr/bin/identify' 13 | CONVERT = '/usr/bin/convert' 14 | PDFTK = '/usr/bin/pdftk' 15 | PDFTK_NEW = False # True for 1.45+ 16 | PDFJAM = '/usr/bin/pdfjam' 17 | 18 | # Fill these in config_local.py 19 | A5A4_TASKS = '' 20 | A5A4_PASSWORD = '' 21 | SECRET_KEY = 'whatever' 22 | 23 | try: 24 | from config_local import * 25 | except ImportError: 26 | pass 27 | -------------------------------------------------------------------------------- /cgi/pdfjam.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | PDFtk and PDFjam Web Interface 4 | 5 |
6 |

PDF files with A5 pages:
7 | A:
8 | B:
9 | C:
10 | D:
11 | PDFtk line:
12 | hint: A1 B A2 C1L = page 1 of A, then all pages of B, then page 2 of A, and C1 rotated left (counter clockwise)
empty line means simple joining of all files

13 |

14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /app/static/style.css: -------------------------------------------------------------------------------- 1 | body { font-family: "PT Sans", sans-serif; } 2 | #newproject { 3 | font-size: 8pt; 4 | margin-top: 50px; 5 | } 6 | #pages { 7 | max-width: 450px; 8 | float: left; 9 | 10 | } 11 | 12 | .page { 13 | position: relative; 14 | display: inline-block; 15 | } 16 | 17 | .pagelabel { 18 | position: absolute; 19 | left: 10px; 20 | top: 10px; 21 | font-size: 8pt; 22 | background: black; 23 | color: white; 24 | opacity: 0.6; 25 | font-weight: bold; 26 | padding: 0 2px; 27 | border-radius: 3px; 28 | z-index: 100; 29 | } 30 | 31 | .controls { 32 | position: absolute; 33 | background-color: rgba(255, 255, 255, 0.8); 34 | left: 50px; 35 | bottom: 5px; 36 | } 37 | .controls a { 38 | display: inline-block; 39 | text-decoration: none; 40 | font-weight: bold; 41 | color: blue; 42 | padding: 0px 3px; 43 | } 44 | .rotate { 45 | -webkit-transform: rotate(180deg); 46 | -moz-transform: rotate(180deg); 47 | -o-transform: rotate(180deg); 48 | -ms-transform: rotate(180deg); 49 | transform: rotate(180deg); 50 | } 51 | #pgstatus { 52 | color: gray; 53 | } 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Web interface to pdftk and pdfjam 2 | 3 | Those tools are the best for managing PDF pages. But they have two major drawbacks: 4 | 5 | 1. PDFjam installs to 430 MB (because of texlive dependency), which is a lot. 6 | 2. They are not available on Windows. 7 | 8 | For casual PDF processing it would be nice to have a web service, which receives some 9 | PDFs and processes them with those tools. This service was specifically aimed at 10 | stitching A5 pages into A4 printer-ready documents. 11 | 12 | ## CGI 13 | 14 | The simplest is the CGI interface. It consists of a plain HTML file to be placed 15 | anywhere in a document root and of python CGI script for `/cgi-bin` directory. 16 | Check paths to pdftk and pdfjam in the script — and you're ready to go. 17 | 18 | ### Flask application 19 | 20 | The web application allows for visualizing PDF pages, using raster images as pages, 21 | rearranging and rotating them visually, and producing PDF with pdftk + pdfjam. 22 | Run `run.py` and open `http://localhost:5000` to use it. 23 | 24 | ## Author and license 25 | 26 | The script was written by Ilya Zverev and published under WTFPL license. 27 | -------------------------------------------------------------------------------- /app/templates/task.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block title %}Task {{ taskid }} — {% endblock %} 3 | {% block content %} 4 | {% if error %}

{{ error }}

{% endif %} 5 |
6 | {% for page in pages %} 7 |
8 | {{ page[:2] }} 9 | 10 | 11 | 12 | + 13 | × 14 | ¿ 15 | 16 | 17 |
18 | {% endfor %} 19 |
20 |
21 | {% for letter, pdf in files|dictsort %} 22 |
{{ letter }}: {{ pdf.name }} x restore
23 | {% endfor %} 24 |
25 |
26 |

27 |
28 |
29 | Generate PDF 30 |
31 |
 
32 |
33 | Start new project 34 |
35 | 38 | {% endblock %} 39 | -------------------------------------------------------------------------------- /app/a5a4.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from app import app 3 | import re 4 | from flask import render_template, send_file, request, session, url_for, redirect 5 | import app.tasks as tasks 6 | 7 | 8 | @app.route('/') 9 | def index(): 10 | if 'logged_in' in session and session['logged_in']: 11 | return render_template('upload.html') 12 | else: 13 | return render_template('login.html') 14 | 15 | 16 | @app.route('/login', methods=['POST']) 17 | def login(): 18 | if 'A5A4_PASSWORD' not in app.config or request.form['password'] == app.config['A5A4_PASSWORD']: 19 | session['logged_in'] = True 20 | if len(request.form['taskid']) > 1: 21 | return redirect(url_for('task', taskid=request.form['taskid'])) 22 | return redirect(url_for('index')) 23 | 24 | 25 | @app.route('/') 26 | def task(taskid, error=None): 27 | if 'logged_in' not in session or not session['logged_in']: 28 | return render_template('login.html', taskid=taskid) 29 | task = tasks.get(taskid) 30 | if not task: 31 | return redirect(url_for('index')) 32 | return render_template('task.html', taskid=taskid, pages=task.pages, 33 | files=task.files, error=error) 34 | 35 | 36 | @app.route('//update') 37 | def task_update(taskid): 38 | pages = request.args.get('pages', '-') 39 | try: 40 | if pages != '-': 41 | tasks.store_pages(taskid, re.split(r'\s+', pages.strip())) 42 | return 'ok' 43 | except Exception as e: 44 | app.logger.error('Error saving pages {}: {}'.format(pages, e)) 45 | return 'fail' 46 | 47 | 48 | @app.route('//upload', methods=['POST']) 49 | @app.route('/upload', methods=['POST']) 50 | def upload(taskid=None): 51 | if 'logged_in' not in session or not session['logged_in']: 52 | return render_template('login.html') 53 | pdf = request.files['pdf'] 54 | if not pdf: 55 | return render_template('upload.html') 56 | newtask = not taskid 57 | if not taskid: 58 | taskid = tasks.create() 59 | result = tasks.addpdf(taskid, pdf) 60 | if result and newtask: 61 | return render_template('upload.html', error=result) 62 | return redirect(url_for('task', taskid=taskid, error=result)) 63 | 64 | 65 | @app.route('//.png') 66 | def getpng(taskid, img): 67 | m = re.match('^([A-Z])(\d+)[LRD]?$', img) 68 | if not m: 69 | return 70 | return send_file(tasks.taskfile(taskid, m.group(1) + str(int(m.group(2))-1) + '.png')) 71 | 72 | 73 | @app.route('//delete/') 74 | def delpdf(taskid, name): 75 | if 'logged_in' not in session or not session['logged_in']: 76 | return render_template('login.html') 77 | tasks.delpdf(taskid, name) 78 | return redirect(url_for('task', taskid=taskid)) 79 | 80 | 81 | @app.route('//result') 82 | def getresult(taskid): 83 | return send_file(tasks.taskfile(taskid, tasks.RESULT)) 84 | 85 | 86 | @app.route('//generate') 87 | def generate(taskid): 88 | if 'logged_in' not in session or not session['logged_in']: 89 | return render_template('login.html') 90 | result = tasks.generate(taskid) 91 | if result: 92 | return send_file( 93 | tasks.taskfile(taskid, tasks.RESULT), 94 | as_attachment=True, 95 | mimetype='application/pdf', 96 | attachment_filename='a5a4_result.pdf') 97 | else: 98 | return redirect(url_for('task', taskid=taskid)) 99 | 100 | 101 | @app.route('//restorepg/') 102 | def restorepg(taskid, name): 103 | tasks.restore_pages(taskid, name) 104 | return redirect(url_for('task', taskid=taskid)) 105 | -------------------------------------------------------------------------------- /app/static/a5a4.js: -------------------------------------------------------------------------------- 1 | function pg(link, action) { 2 | var page = link.parentNode.parentNode; 3 | if( !page.hasAttribute('page') ) 4 | return false; 5 | var pages = buildPages(), 6 | index; 7 | for( index = pages.length - 1; index >= 0; index-- ) 8 | if( pages[index] == page ) 9 | break; 10 | if( index < 0 ) 11 | return false; 12 | 13 | if( action == 'left' && index > 0 ) { 14 | page.parentNode.insertBefore(page, pages[index-1]); 15 | save(); 16 | } else if( action == 'right' && index + 1 < pages.length ) { 17 | page.parentNode.insertBefore(pages[index+1], page); 18 | save(); 19 | } else if( action == 'add' ) { 20 | var copy = page.cloneNode(true); 21 | page.parentNode.insertBefore(copy, page); 22 | save(); 23 | } else if( action == 'delete' ) { 24 | page.parentNode.removeChild(page); 25 | save(); 26 | } else if( action == 'rotate' ) { 27 | var pageId = page.getAttribute('page'), 28 | transform = pageId.length < 3 ? '' : pageId.charAt(2), 29 | rotated = transform == 'R' || transform == 'D', 30 | newTransform, 31 | img = page.getElementsByTagName('img'); 32 | if( !img || !img.length ) 33 | return false; 34 | if( transform == 'R' ) 35 | newTransform = 'L'; 36 | else if( transform == 'L' ) 37 | newTransform = 'R'; 38 | else if( transform == 'D' ) 39 | newTransform = ''; 40 | else 41 | newTransform = 'D'; 42 | img[0].className = rotated ? '' : 'rotate'; 43 | page.setAttribute('page', (pageId.length < 3 ? pageId : pageId.substring(0, 2)) + newTransform); 44 | save(); 45 | } 46 | return false; 47 | } 48 | 49 | function buildPages() { 50 | var root = document.getElementById('pages'), 51 | pages = [], 52 | nodes = root.children, i; 53 | for( i = 0; i < nodes.length; i++ ) { 54 | if( nodes[i].hasAttribute('page') ) 55 | pages.push(nodes[i]); 56 | } 57 | return pages; 58 | } 59 | 60 | function getPagesString() { 61 | var pages = buildPages(), i, str = ''; 62 | for( i = 0; i < pages.length; i++ ) { 63 | if( str.length ) 64 | str += ' '; 65 | str += pages[i].getAttribute('page'); 66 | } 67 | return str; 68 | } 69 | 70 | function setStatus(st) { 71 | var panel = document.getElementById('pgstatus'); 72 | if( st == 'modified' ) { 73 | panel.innerHTML = 'Modified. Save'; 74 | } else if( st == 'saving' ) { 75 | panel.innerHTML = 'Saving...'; 76 | } else if( st == 'saved' ) { 77 | panel.innerHTML = 'Saved.'; 78 | } else if( st == 'fail' ) { 79 | panel.innerHTML = 'Failed to save. Try again'; 80 | } 81 | } 82 | 83 | // schedule an upload 84 | var timeout; 85 | function save() { 86 | setStatus('modified'); 87 | if( timeout ) 88 | window.clearTimeout(timeout); 89 | timeout = window.setTimeout(upload, 2000); 90 | } 91 | 92 | function upload() { 93 | if( timeout ) 94 | window.clearTimeout(timeout); 95 | timeout = false; 96 | setStatus('saving'); 97 | var str = getPagesString(), http, 98 | url = window.uploadRoot + '?pages=' + encodeURIComponent(str); 99 | // copied from mapbbcode :) 100 | if (window.XMLHttpRequest) { 101 | http = new window.XMLHttpRequest(); 102 | } 103 | if( window.XDomainRequest && (!http || !('withCredentials' in http)) ) { 104 | // older IE that does not support CORS 105 | http = new window.XDomainRequest(); 106 | } 107 | if( !http ) { 108 | setStatus('fail'); 109 | return; 110 | } 111 | 112 | function respond() { 113 | var st = http.status, 114 | error = (!st && http.responseText) || (st >= 200 && st < 300) ? false : (st || 499); 115 | setStatus(error ? 'fail' : 'saved'); 116 | } 117 | 118 | if( 'onload' in http ) 119 | http.onload = http.onerror = respond; 120 | else 121 | http.onreadystatechange = function() { if( http.readyState == 4 ) respond(); }; 122 | 123 | try { 124 | http.open('GET', url, true); 125 | http.send(null); 126 | } catch( err ) { 127 | // most likely a security error 128 | setStatus('fail'); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /cgi/pdfjamcgi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # PDFjam CGI interface. 4 | # Written by Ilya Zverev, licensed WTFPL. 5 | 6 | import os, sys, cgi, re 7 | import tempfile, subprocess, shutil 8 | import cgitb 9 | cgitb.enable() 10 | 11 | PDFTK = '/usr/bin/pdftk' 12 | PDFJAM = '/usr/bin/pdfjam' 13 | PDFTK_NEW = False # set to True for pdftk 1.45+ 14 | 15 | def remove_files(fp): 16 | if type(fp) is list: 17 | for fp1 in fp: 18 | remove_files(fp1) 19 | elif type(fp) is dict: 20 | for fp1 in fp.values(): 21 | remove_files(fp1) 22 | elif type(fp) is str: 23 | try: 24 | os.remove(fp) 25 | except OSError: 26 | pass 27 | elif hasattr(fp, 'read') and hasattr(fp, 'name'): 28 | try: 29 | os.remove(fp.name) 30 | except OSError: 31 | pass 32 | 33 | def error_die(msg): 34 | print 'Content-Type: text/plain; charset=utf-8' 35 | print '' 36 | print 'Error: {}.'.format(msg); 37 | sys.exit(1) 38 | 39 | class MyFieldStorage(cgi.FieldStorage): 40 | def make_file(self, binary=None): 41 | return tempfile.NamedTemporaryFile('wb+', delete=False, suffix='.pdf') 42 | 43 | form = MyFieldStorage() 44 | 45 | pdf = {} 46 | for p in 'abcd': 47 | key = 'pdf'+p 48 | if key in form and form[key].file and form[key].filename: 49 | if hasattr(form[key].file, 'name'): 50 | pdf[p.upper()] = form[key].file 51 | else: 52 | # for some reason small files are not make_file'd 53 | fp = tempfile.NamedTemporaryFile('wb+', delete=False, suffix='.pdf') 54 | fp.write(form[key].value) 55 | fp.close() 56 | pdf[p.upper()] = fp 57 | 58 | if not len(pdf): 59 | error_die('At least one PDF file parameter is required') 60 | 61 | if len(pdf) == 1 and len(form.getfirst('pdftk', '')) < 2: 62 | # only one file, no need to run pdftk 63 | tmpName = pdf.values()[0].name 64 | else: 65 | # process files with pdftk 66 | pages = form.getfirst('pdftk', '').strip().upper() 67 | if not len(pages): 68 | pages = ' '.join(sorted(pdf.keys())) 69 | elif not re.match('^[A-D][1-9A-Z-]*(?: +[A-D][1-9A-Z-]*)*$', pages): 70 | remove_files(pdf) 71 | error_die('Incorrect pages format: {}'.format(pages)) 72 | 73 | if PDFTK_NEW: 74 | # replace rotation values with new 75 | rotation = { 'W': 'west', 'E': 'east', 'S': 'south', 'L': 'left', 'R': 'right', 'D': 'down', 'N': 'north' } 76 | def fix_rot(page): 77 | for k, v in rotation.items(): 78 | if page.endswith(k): 79 | return page[:-1] + v 80 | return page 81 | pages = ' '.join([fix_rot(page) for page in re.split('\s+', pages)]) 82 | 83 | tmpfile, tmpName = tempfile.mkstemp(suffix='.pdf') 84 | command = [PDFTK] 85 | command.extend(['{}={}'.format(k, v.name) for k, v in pdf.items()]) 86 | command.append('cat') 87 | command.extend(re.split('\s+', pages)) 88 | command.extend(['output', tmpName]) 89 | process = subprocess.Popen(command, stderr=subprocess.PIPE) 90 | _, err = process.communicate() 91 | result = process.returncode 92 | remove_files(pdf) 93 | if result != 0 or not os.path.isfile(tmpName) or os.stat(tmpName).st_size == 0: 94 | remove_files(tmpName) 95 | error_die('PDFtk returned error code {}\n\n{}'.format(result, err)) 96 | 97 | # now process result with pdfjam 98 | outfile, outputName = tempfile.mkstemp() 99 | command = [PDFJAM, tmpName, '--nup', '2x1', '--landscape', '--a4paper', '--outfile', outputName] 100 | process = subprocess.Popen(command, stderr=subprocess.PIPE) 101 | _, err = process.communicate() 102 | result = process.returncode 103 | remove_files(tmpName) 104 | if result != 0: 105 | error_die('PDFjam returned error code {}\n\n{}'.format(result, err)) 106 | 107 | length = os.stat(outputName).st_size 108 | if length == 0: 109 | remove_files(outputName) 110 | error_die('Resulting PDF is empty for some reason') 111 | 112 | print 'Content-Type: application/pdf' 113 | print 'Content-Disposition: attachment; filename=pdfjam-result.pdf' 114 | print 'Content-Length: {}'.format(length) 115 | print '' 116 | with open(outputName, 'rb') as f: 117 | shutil.copyfileobj(f, sys.stdout) 118 | 119 | remove_files(outputName) 120 | -------------------------------------------------------------------------------- /app/tasks.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import subprocess 4 | import tempfile 5 | import random 6 | import string 7 | from functools import reduce 8 | from app import app 9 | 10 | FILENAME = 'task' 11 | RESULT = 'a5a4.pdf' 12 | 13 | 14 | class Task: 15 | def __init__(self): 16 | self.pages = [] # list of transformations: 'A1', 'B2L' etc. 17 | self.files = {} # dict of 'A': TaskFile() 18 | 19 | 20 | class TaskFile: 21 | def __init__(self, pages, name): 22 | self.name = name 23 | if type(pages) is str: 24 | self.orient = [p == 'l' for p in pages] 25 | elif type(pages) is list: 26 | self.orient = pages 27 | elif type(pages) is int: 28 | self.orient = [False for p in range(pages)] 29 | else: 30 | raise Exception('Incorrect pages value: {}'.format(pages)) 31 | self.pages = len(self.orient) 32 | 33 | def orient_str(self): 34 | """Produces a string out of orient array.""" 35 | return reduce(lambda a, b: a + ('l' if b else 'p'), self.orient, '') 36 | 37 | 38 | def taskfile(taskid, name=''): 39 | """Get absolute file name for a file inside a task folder.""" 40 | if not app.config.get('A5A4_TASKS'): 41 | folder = os.path.join(app.config['BASE_DIR'], 'tasks') 42 | else: 43 | folder = app.config['A5A4_TASKS'] 44 | if not re.match('^[a-z]{3,9}$', taskid): 45 | raise Exception('Task id is wrong: ' + taskid) 46 | if not re.match('^[a-zA-Z0-9_%.-]{0,20}$', name): 47 | raise Exception('File name in task is wrong: ' + name) 48 | return os.path.join(folder, taskid, name) 49 | 50 | 51 | def create(): 52 | """Create new task with random id, initialize its task file.""" 53 | found = False 54 | while not found: 55 | taskid = ''.join(random.choice(string.ascii_lowercase) for i in range(3)) 56 | found = not os.path.isfile(taskfile(taskid, FILENAME)) 57 | os.makedirs(taskfile(taskid)) 58 | store(taskid, Task()) 59 | return taskid 60 | 61 | 62 | def get(taskid): 63 | """Read and parse task file for a given id. Returns Task object.""" 64 | filename = taskfile(taskid, FILENAME) 65 | if not os.path.isfile(filename): 66 | return None 67 | task = Task() 68 | with open(filename, 'r') as f: 69 | task.pages = [p for p in f.readline().strip().split(' ') if len(p)] 70 | for pdf in f.readlines(): 71 | data = pdf.strip().split(',', 2) 72 | if len(data) == 3 and len(data[0]) == 1: 73 | task.files[data[0]] = TaskFile(data[1], data[2]) 74 | return task 75 | 76 | 77 | def store(taskid, newtask): 78 | """Stores a Task object into a task folder for given id.""" 79 | with open(taskfile(taskid, FILENAME), 'w') as f: 80 | f.write(' '.join(newtask.pages) + '\n') 81 | for k, pdf in newtask.files.items(): 82 | f.write(','.join([k, pdf.orient_str(), pdf.name]) + '\n') 83 | 84 | 85 | def store_pages(taskid, pages): 86 | """Stores new page order for a given task. Pages is a list.""" 87 | task = get(taskid) 88 | if task is None: 89 | return 90 | for page in pages: 91 | if not page[0] in task.files: 92 | raise Exception(page[0] + ' is not in PDF list') 93 | if int(page[1]) < 1 or int(page[1]) > task.files[page[0]].pages: 94 | raise Exception(page[0] + ' does not have page ' + page[1]) 95 | if len(page) > 2 and page[2] not in ['L', 'R', 'D']: 96 | raise Exception('Unknown rotation mode: ' + page) 97 | task.pages = pages 98 | store(taskid, task) 99 | 100 | 101 | def restore_pages(taskid, pdf): 102 | """Restores deleted pages at proper places. Pdf is a single-letter identifier.""" 103 | task = get(taskid) 104 | if task and pdf in task.files and task.files[pdf].pages > 0: 105 | torestore = range(1, task.files[pdf].pages + 1) 106 | for p in task.pages: 107 | if p[0] == pdf: 108 | try: 109 | torestore.remove(int(p[1])) 110 | except: 111 | pass 112 | insert_rest_before = 0 113 | foundmax = False 114 | pos = 0 115 | while len(torestore) > 0 and pos < len(task.pages): 116 | if task.pages[pos][0] == pdf: 117 | curpg = int(task.pages[pos][1]) 118 | while len(torestore) > 0 and curpg > torestore[0]: 119 | nextpg = torestore.pop(0) 120 | page = '{}{}{}'.format( 121 | pdf, nextpg, 'L' if task.files[pdf].orient[nextpg-1] else '') 122 | task.pages.insert(pos, page) 123 | pos = pos + 1 124 | insert_rest_before = pos + 1 125 | elif not insert_rest_before and task.pages[pos][0] > pdf: 126 | insert_rest_before = pos 127 | foundmax = True 128 | pos = pos + 1 129 | if not foundmax and insert_rest_before == 0: 130 | insert_rest_before = len(task.pages) 131 | while len(torestore) > 0: 132 | nextpg = torestore.pop(0) 133 | page = '{}{}{}'.format(pdf, nextpg, 'L' if task.files[pdf].orient[nextpg-1] else '') 134 | task.pages.insert(insert_rest_before, page) 135 | insert_rest_before = insert_rest_before + 1 136 | store_pages(taskid, task.pages) 137 | 138 | 139 | def addpdf(taskid, pdf): 140 | """Uploads a pdf / png file, converting it and stuff. 141 | Pdf is a flask uploaded file. 142 | Return error string or None if it's ok.""" 143 | task = get(taskid) 144 | if not task or len(task.files) >= app.config['A5A4_MAXFILES']: 145 | return 'You can have no more than {} files'.format(app.config['A5A4_MAXFILES']) 146 | if not len(task.files): 147 | letter = 'A' 148 | else: 149 | letter = chr(ord(sorted(task.files.keys(), reverse=True)[0]) + 1) 150 | if letter > 'Z': 151 | letter = 'A' 152 | # this won't overflow because of maxfiles (which should be less than 26) 153 | while letter in task.files.keys(): 154 | letter = chr(ord(letter) + 1) 155 | filename = taskfile(taskid, letter + '.pdf') 156 | pdf.save(filename) 157 | 158 | # check that all pages are A5 159 | command = [app.config['IDENTIFY'], filename] 160 | process = subprocess.Popen(command, stdout=subprocess.PIPE) 161 | out, _ = process.communicate() 162 | code = process.returncode 163 | if code != 0 or not out: 164 | os.remove(filename) 165 | return 'Identify failed: {}'.format(code) 166 | 167 | # check page dimensions 168 | # get page rotation 169 | # get number of pages 170 | pages = [] 171 | for line in out.decode('utf-8').split('\n'): 172 | app.logger.debug('Processing {}'.format(line)) 173 | m = re.search(filename + r'(?:\[\d+\])? ([A-Z]{3,4}) (\d+)x(\d+)', line) 174 | if m: 175 | fmt = m.group(1) 176 | width = int(m.group(2)) 177 | height = int(m.group(3)) 178 | landscape = width > height 179 | if landscape: 180 | width, height = height, width 181 | png = fmt != 'PDF' 182 | if not png and (abs(width - 420) > 4 or abs(height - 595) > 4): 183 | os.remove(filename) 184 | return 'Provided PDF is not in A5 format' 185 | pages.append(landscape) 186 | 187 | if len(pages) == 0: 188 | os.remove(filename) 189 | return 'No pages parsed from identify' 190 | 191 | if reduce(lambda c, t: c + t.pages, 192 | task.files.values(), len(pages)) > app.config['A5A4_MAXPAGES']: 193 | os.remove(filename) 194 | return 'Too many pages: {} (maximum is {})'.format(len(pages), app.config['A5A4_MAXPAGES']) 195 | 196 | # check if source is png 197 | if png: 198 | app.logger.debug('converting to png') 199 | # convert to pdf 200 | tmpfile, tmpName = tempfile.mkstemp(suffix='.pdf') 201 | command = [app.config['CONVERT'], filename, '-bordercolor', 'white', 202 | '-border', '6%', '-rotate', '-90>', tmpName] 203 | result = subprocess.call(command) 204 | os.remove(filename) 205 | if result != 0: 206 | return 'Convert to PNG failed: {}'.format(result) 207 | pages = [False for i in pages] 208 | # resize pdf to A5 209 | command = [app.config['PDFJAM'], '--a5paper', '--no-landscape', 210 | '--outfile', filename, tmpName] 211 | result = subprocess.call(command) 212 | os.remove(tmpName) 213 | if result != 0: 214 | os.remove(filename) 215 | return 'Scaling PNG pdf with pdfjam failed: {}'.format(result) 216 | 217 | # generate slides 218 | command = [app.config['CONVERT'], '-density', '200', filename, 219 | '-alpha', 'opaque', '-resize', '200x200^', 220 | taskfile(taskid, letter + '%d.png')] 221 | result = subprocess.call(command) 222 | if result != 0: 223 | os.remove(filename) 224 | return 'Creating png pages from pdf failed: {}'.format(result) 225 | 226 | # rotate landscape images 227 | for i in range(len(pages)): 228 | task.pages.append('{}{}{}'.format(letter, i+1, 'L' if pages[i] else '')) 229 | if pages[i]: 230 | pagename = taskfile(taskid, letter + str(i) + '.png') 231 | command = [app.config['CONVERT'], pagename, '-rotate', '-90', pagename] 232 | result = subprocess.call(command) 233 | if result != 0: 234 | app.logger.warn('Failed to rotate image {}'.format(pagename)) 235 | 236 | # construct TaskFile object and store it 237 | task.files[letter] = TaskFile(pages, pdf.filename) 238 | store(taskid, task) 239 | return None 240 | 241 | 242 | def delpdf(taskid, idx): 243 | """Deletes a PDF file, all png miniatures and all of its pages.""" 244 | task = get(taskid) 245 | if not task or not len(idx) == 1 or idx not in task.files: 246 | return False 247 | pdf = task.files[idx] 248 | # delete pdf 249 | try: 250 | os.remove(taskfile(taskid, '{}.pdf'.format(idx))) 251 | except OSError: 252 | app.logger.warn('Failed to delete {}/{}'.format(taskid, pdf.name)) 253 | # delete pngs 254 | for i in range(pdf.pages): 255 | try: 256 | os.remove(taskfile(taskid, '{}{}.png'.format(idx, i))) 257 | except OSError: 258 | app.logger.warn('Failed to delete {}/{}{}.png'.format(taskid, idx, i)) 259 | # update tasks file 260 | task.pages = [p for p in task.pages if not p.startswith(idx)] 261 | del task.files[idx] 262 | store(taskid, task) 263 | return True 264 | 265 | 266 | def generate(taskid): 267 | """Creates a resulting PDF for a task with pdftk and pdfjam.""" 268 | task = get(taskid) 269 | if not task or not len(task.pages) or not len(task.files): 270 | return False 271 | rotation = {'W': 'west', 'E': 'east', 'S': 'south', 272 | 'L': 'left', 'R': 'right', 273 | 'D': 'down', 'N': 'north'} 274 | if app.config['PDFTK_NEW']: 275 | def fix_rot(page): 276 | for k, v in rotation.items(): 277 | if len(page) >= 3 and page.endswith(k): 278 | return page[:-1] + v 279 | return page 280 | task.pages = [fix_rot(p) for p in task.pages] 281 | 282 | tmpfile, tmpName = tempfile.mkstemp(suffix='.pdf') 283 | command = [app.config['PDFTK']] 284 | # this line leaves only those documents mentioned in pages 285 | command.extend(['{}={}'.format(k, taskfile(taskid, '{}.pdf'.format(k))) 286 | for k in set([page[0:1] for page in task.pages]) 287 | if k in task.files]) 288 | command.append('cat') 289 | command.extend(task.pages) 290 | command.extend(['output', tmpName]) 291 | process = subprocess.Popen(command, stderr=subprocess.PIPE) 292 | _, err = process.communicate() 293 | result = process.returncode 294 | if result != 0 or not os.path.isfile(tmpName) or os.stat(tmpName).st_size == 0: 295 | app.logger.error('PDFtk returned error code {}\n\n{}'.format(result, err.decode('utf-8'))) 296 | else: 297 | # now process result with pdfjam 298 | command = [app.config['PDFJAM'], tmpName, 299 | '--nup', '2x1', '--landscape', '--a4paper', '--outfile', 300 | taskfile(taskid, RESULT)] 301 | process = subprocess.Popen(command, stderr=subprocess.PIPE) 302 | _, err = process.communicate() 303 | result = process.returncode 304 | if result != 0: 305 | app.logger.error('PDFjam returned error code {}\n\n{}'.format( 306 | result, err.decode('utf-8'))) 307 | 308 | try: 309 | os.remove(tmpName) 310 | except OSError: 311 | pass 312 | return result == 0 313 | --------------------------------------------------------------------------------