|
| 1 | +#!/usr/bin/python3 |
| 2 | + |
| 3 | +# |
| 4 | +# create a pdf with barcodes to backup text files on paper |
| 5 | +# designed to backup ascii-armored key files and ciphertext |
| 6 | +# |
| 7 | + |
| 8 | +# Copyright 2017 by Intra2net AG, Germany |
| 9 | +# |
| 10 | +# Permission is hereby granted, free of charge, to any person obtaining |
| 11 | +# a copy of this software and associated documentation files (the |
| 12 | +# "Software"), to deal in the Software without restriction, including |
| 13 | +# without limitation the rights to use, copy, modify, merge, publish, |
| 14 | +# distribute, sublicense, and/or sell copies of the Software, and to |
| 15 | +# permit persons to whom the Software is furnished to do so, subject to |
| 16 | +# the following conditions: |
| 17 | +# |
| 18 | +# The above copyright notice and this permission notice shall be |
| 19 | +# included in all copies or substantial portions of the Software. |
| 20 | +# |
| 21 | +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
| 22 | +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
| 23 | +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
| 24 | +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE |
| 25 | +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION |
| 26 | +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION |
| 27 | +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
| 28 | +# |
| 29 | + |
| 30 | +import os |
| 31 | +import re |
| 32 | +import sys |
| 33 | +import shlex |
| 34 | +import qrencode |
| 35 | +from tempfile import mkstemp |
| 36 | +from PIL import Image |
| 37 | +from pyx import * |
| 38 | + |
| 39 | +# constants for the size and layout of the barcodes on page |
| 40 | +max_bytes_in_barcode = 140 |
| 41 | +barcodes_per_page = 6 |
| 42 | +barcode_height = 8 |
| 43 | +barcode_x_positions = [1.5, 11, 1.5, 11, 1.5, 11] |
| 44 | +barcode_y_positions = [18.7, 18.7, 10, 10, 1.2, 1.2] |
| 45 | +text_x_offset = 0 |
| 46 | +text_y_offset = 8.2 |
| 47 | + |
| 48 | +# the paperformat to use, activate the one you want |
| 49 | +paperformat_obj = document.paperformat.A4 |
| 50 | +paperformat_str = "A4" |
| 51 | +# paperformat_obj=document.paperformat.Letter |
| 52 | +# paperformat_str="Letter" |
| 53 | + |
| 54 | + |
| 55 | +def create_barcode(chunkdata): |
| 56 | + version, size, im = qrencode.encode(chunkdata, |
| 57 | + level=qrencode.QR_ECLEVEL_H, |
| 58 | + case_sensitive=True) |
| 59 | + return im |
| 60 | + |
| 61 | + |
| 62 | +def finish_page(pdf, canv, pageno): |
| 63 | + canv.text(10, 0.6, "Page %i" % (pageno+1)) |
| 64 | + pdf.append(document.page(canv, paperformat=paperformat_obj, |
| 65 | + fittosize=0, centered=0)) |
| 66 | + |
| 67 | +# main code |
| 68 | + |
| 69 | +if len(sys.argv) != 2: |
| 70 | + raise RuntimeError('Usage {} FILENAME.asc'.format(sys.argv[0])) |
| 71 | + |
| 72 | +input_path = sys.argv[1] |
| 73 | +if not os.path.isfile(input_path): |
| 74 | + raise RuntimeError('File {} not found'.format(input_path)) |
| 75 | +just_filename = os.path.basename(input_path) |
| 76 | + |
| 77 | +with open(input_path) as inputfile: |
| 78 | + ascdata = inputfile.read() |
| 79 | + |
| 80 | +# only allow some harmless characters |
| 81 | +# this is much more strict than neccessary, but good enough for key files |
| 82 | +# you really need to forbid ^, NULL and anything that could upset enscript |
| 83 | +allowedchars = re.compile(r"^[A-Za-z0-9/=+:., #@!()\n-]*") |
| 84 | +allowedmatch = allowedchars.match(ascdata) |
| 85 | +if allowedmatch.group() != ascdata: |
| 86 | + raise RuntimeError('Illegal char found at %d >%s<' |
| 87 | + % (len(allowedmatch.group()), |
| 88 | + ascdata[len(allowedmatch.group())])) |
| 89 | + |
| 90 | +# split the ascdata into chunks of max_bytes_in_barcode size |
| 91 | +# each chunk begins with ^<sequence number><space> |
| 92 | +# this allows to easily put them back together in the correct order |
| 93 | +barcode_blocks = [] |
| 94 | +chunkdata = "^1 " |
| 95 | +for char in list(ascdata): |
| 96 | + if len(chunkdata)+1 > max_bytes_in_barcode: |
| 97 | + # chunk is full -> create barcode from it |
| 98 | + barcode_blocks.append(create_barcode(chunkdata)) |
| 99 | + chunkdata = "^" + str(len(barcode_blocks)+1) + " " |
| 100 | + |
| 101 | + chunkdata += char |
| 102 | + |
| 103 | +# handle the last, non filled chunk too |
| 104 | +barcode_blocks.append(create_barcode(chunkdata)) |
| 105 | + |
| 106 | +# init PyX |
| 107 | +unit.set(defaultunit="cm") |
| 108 | +pdf = document.document() |
| 109 | + |
| 110 | +# place barcodes on pages |
| 111 | +pgno = 0 # page number |
| 112 | +ppos = 0 # position id on page |
| 113 | +c = canvas.canvas() |
| 114 | +for bc in range(len(barcode_blocks)): |
| 115 | + # page full? |
| 116 | + if ppos >= barcodes_per_page: |
| 117 | + finish_page(pdf, c, pgno) |
| 118 | + c = canvas.canvas() |
| 119 | + pgno += 1 |
| 120 | + ppos = 0 |
| 121 | + |
| 122 | + c.text(barcode_x_positions[ppos] + text_x_offset, |
| 123 | + barcode_y_positions[ppos] + text_y_offset, |
| 124 | + "%s (%i/%i)" % (text.escapestring(just_filename), |
| 125 | + bc+1, len(barcode_blocks))) |
| 126 | + c.insert(bitmap.bitmap(barcode_x_positions[ppos], |
| 127 | + barcode_y_positions[ppos], |
| 128 | + barcode_blocks[bc], height=barcode_height)) |
| 129 | + ppos += 1 |
| 130 | + |
| 131 | +finish_page(pdf, c, pgno) |
| 132 | +pgno += 1 |
| 133 | + |
| 134 | +fd, temp_barcode_path = mkstemp('.pdf', 'qr_', '.') |
| 135 | +# will use pdf as the tmpfile has a .pdf suffix |
| 136 | +pdf.writetofile(temp_barcode_path) |
| 137 | + |
| 138 | +# use "enscript" to create postscript with the plaintext |
| 139 | +fd, temp_text_path = mkstemp('.ps', 'text_', '.') |
| 140 | +ret = os.system("enscript -p" + shlex.quote(temp_text_path) + |
| 141 | + " -f Courier12 -M" + paperformat_str + |
| 142 | + " " + shlex.quote(input_path)) |
| 143 | +if ret != 0: |
| 144 | + raise RuntimeError('error calling enscript') |
| 145 | + |
| 146 | +# combine both files with ghostscript |
| 147 | +ret = os.system("gs -dBATCH -dNOPAUSE -q -sDEVICE=pdfwrite -sOutputFile=" + |
| 148 | + shlex.quote(just_filename) + |
| 149 | + ".pdf " + shlex.quote(temp_barcode_path) + " " + |
| 150 | + shlex.quote(temp_text_path)) |
| 151 | +if ret != 0: |
| 152 | + raise RuntimeError('error calling ghostscript') |
| 153 | + |
| 154 | +# using enscript and ghostscript to create the plaintext output is a hack, |
| 155 | +# using PyX and LaTeX would be more elegant. But I could not find an easy |
| 156 | +# solution to flow the text over several pages with PyX. |
| 157 | + |
| 158 | +os.remove(temp_text_path) |
| 159 | +os.remove(temp_barcode_path) |
0 commit comments