Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- #!/usr/bin/python3
- import os
- import sys
- import traceback
- import math
- import numpy as np
- import itertools
- import html
- import argparse
- from PIL import Image
- import descale as ds
- VERSION = '1.0.0.0'
- class Error (Exception):
- pass
- class ArgError (Error):
- pass
- class FileError(Error):
- pass
- class DataError(Error):
- pass
- class CmdError(Error):
- pass
- def load_qr_image(filename):
- im = Image.open(filename)
- if im.mode == 'P':
- # Avoid the annoying warning about a transparent colour
- # in a paletted image.
- im = im.convert('LA')
- if im.mode != 'L':
- im = im.convert('L')
- M = np.asarray(im)
- M = ds.bilevel_array(M)
- del im
- M, trimmed = ds.trim_array(M)
- M, margins = ds.descale_trimmed_array(M, trimmed)
- return M, margins
- def svg_path_loop_data(loop):
- result = []
- if len(loop) > 0:
- x0, y0 = loop[0]
- result.append(f'M {x0},{y0}')
- c = 1
- for (x, y) in loop[1:]:
- if False: #c % 50 == 0:
- # Absolute
- result.append(f'L {x},{y}')
- else:
- # Relative
- dx = x - x0
- dy = y - y0
- if dy == 0:
- result.append(f'h {dx}')
- elif dx == 0:
- result.append(f'v {dy}')
- else:
- result.append(f'l {dx},{dy}')
- x0 = x
- y0 = y
- c += 1
- result.append('z')
- return result
- def loops_from_boolimg(M, evenodd):
- A = np.pad(M, 1)
- NotA = np.logical_not(A)
- Ev = np.logical_xor(A[1:, 1:], A[1:, :-1])
- Eh = np.logical_xor(A[1:, 1:], A[:-1, 1:])
- # Edge flags are centred on pixel corners.
- if evenodd:
- Edn = np.logical_xor(A[1:, 1:], A[1:, :-1])
- Eup = np.pad(Ev[:-1, :], [(1, 0), (0, 0)])
- Eri = np.logical_xor(A[1:, 1:], A[:-1, 1:])
- Ele = np.pad(Eh[:, :-1], [(0, 0), (1, 0)])
- else:
- NotA = np.logical_not(A)
- Edn = np.logical_and(Ev, NotA[1:, 1:])
- Eup = np.pad(np.logical_and(Ev[:-1, :], A[1:-1, 1:]), [(1, 0), (0, 0)])
- Eri = np.logical_and(Eh, A[1:, 1:])
- Ele = np.pad(np.logical_and(Eh[:, :-1], NotA[1:, 1:-1]), [(0, 0), (1, 0)])
- del NotA
- E = sum(Ed * d for Ed, d in ((Eri, 1), (Edn, 2), (Ele, 4), (Eup, 8)))
- del Eri, Edn, Ele, Eup, Eh, Ev, A
- if evenodd:
- search_dirs = [[x & 3 for x in (d, d + 1, d - 1)] for d in range(4)]
- else:
- search_dirs = [[(d + 1 - i) & 3 for i in range(3)] for d in range(4)]
- deltas = ((1, 0), (0, 1), (-1, 0), (0, -1))
- # Search for beginnings of rightward paths segments.
- # The extreme bottom and right edges are excluded from
- # the search but wall-following walks to find loops
- # may include segments on those edges.
- h = M.shape[0]
- w = M.shape[1]
- loops = []
- loops_by_y = [list() for i in range(h)]
- for sy in range(h):
- right0 = False
- for sx in range(w):
- right = E[sy, sx] & 1
- if right and not right0:
- (x, y) = (sx, sy)
- d = 0
- loop = [(x, y)]
- while True:
- E[y, x] &= ~(1 << d)
- dxy = deltas[d]
- (x, y) = (x + dxy[0], y + dxy[1])
- E[y, x] &= ~(1 << (d ^ 2))
- d0 = d
- d = None
- if (x, y) != loop[0]:
- dflags_at_xy = E[y,x]
- for d in search_dirs[d0]:
- if dflags_at_xy & (1 << d):
- break
- if d is None:
- raise Error("Dizzy at ({}, {})!".format(x, y))
- if d != d0:
- loop.append((x, y))
- else:
- break
- if evenodd:
- ax, ay = sx, sy
- else:
- # For the fill-rule of 'nonzero', ensure the vertices start at
- # the leftmost part of the top extremity even for anticlockwise
- # (cutting) loops. For the fill-rule of 'evenodd', all loops
- # that are not self-intersecting are clockwise.
- ay = min(p[1] for p in loop)
- ax = min(p[0] for p in loop if p[1] == ay)
- ai = loop.index((ax, ay))
- loop = loop[ai:] + loop[:ai]
- loops_by_y[ay].append(loop)
- right0 = right
- if not evenodd:
- for loops in loops_by_y:
- loops.sort()
- loops = list(itertools.chain.from_iterable(loops_by_y))
- return loops
- def svg_lines_from_loops(loops, evenodd, margins=4,
- title=None, widthstr=None,
- foreground=None, background=None,
- transparent=False, soft=False):
- if title is None:
- title = '(Untitled)'
- if widthstr is None:
- widthstr = '60mm'
- if foreground is None:
- foreground = 'black'
- if background is None:
- background = 'white'
- if not isinstance(margins, dict):
- x = margins
- margins = dict(top=x, bottom=x, left=x, right=x)
- w = 0
- h = 0
- for loop in loops:
- xs = [x for (x, y) in loop]
- ys = [y for (x, y) in loop]
- w = max(w, max(xs))
- h = max(h, max(ys))
- if soft:
- r = 0.1875
- loops0 = loops
- loops = []
- for loop0 in loops0:
- loop = []
- lastp0 = loop0[0]
- loop.append(lastp0)
- for i in range(1, len(loop0)):
- p0 = loop0[i]
- nextp0 = loop0[(i + 1) % len(loop0)]
- dx = r * max(-1, min(1, lastp0[0] - p0[0]))
- dy = r * max(-1, min(1, lastp0[1] - p0[1]))
- loop.append((p0[0] + dx, p0[1] + dy))
- dx = r * max(-1, min(1, nextp0[0] - p0[0]))
- dy = r * max(-1, min(1, nextp0[1] - p0[1]))
- loop.append((p0[0] + dx, p0[1] + dy))
- lastp0 = p0
- loops.append(loop)
- vl = 0
- vr = margins['left'] + w + margins['right']
- vt = 0
- vb = margins['top'] + h + margins['bottom']
- vw = vr - vl
- vh = vb - vt
- i = len(widthstr)
- digits = "0123456789"
- while i > 0 and widthstr[i - 1] not in digits:
- i -= 1
- unitstr = widthstr[i:]
- svgwidth = float(widthstr[:i])
- if svgwidth == '':
- svgwidth = '0'
- svgheight = svgwidth * vh / vw
- heightstr = "{:g}{}".format(svgheight, unitstr)
- ts = html.escape(title)
- ws = html.escape(widthstr)
- hs = html.escape(heightstr)
- fgs = html.escape(foreground)
- bgs = html.escape(background)
- if evenodd:
- eodescstr = 'QRCode as self-intersecting path loops'
- eostr = 'evenodd'
- else:
- eodescstr = 'QRCode as clockwise fill loops and anticlockwise cut loops'
- eostr = 'nonzero'
- SVGLines = []
- SVGLines += [
- '<?xml version="1.0" standalone="no"?>',
- '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"',
- ' "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">',
- f'<svg width="{ws}" height="{hs}" '
- f'viewBox="{vl} {vt} {vw} {vh}"',
- ' preserveAspectRatio="xMidYMid meet"',
- ' xmlns="http://www.w3.org/2000/svg" version="1.1"',
- ' xmlns:xlink="http://www.w3.org/1999/xlink">',
- f'<title>{ts}</title>',
- ]
- if not transparent:
- SVGLines += [
- '<!-- Background -->',
- f'<rect x="{vl}" y="{vt}" width="{vw}" height="{vh}" '
- f'stroke="none" fill="{bgs}"/>',
- ]
- if evenodd:
- eodescstr = 'QRCode as self-intersecting path loops'
- eostr = 'evenodd'
- else:
- eodescstr = 'QRCode as clockwise fill loops and anticlockwise cut loops'
- eostr = 'nonzero'
- SVGLines += [
- '<g transform="translate({}, {})">'.format(
- margins['left'], margins['top']),
- f' <!-- {eodescstr} -->',
- f' <path fill="{fgs}" stroke="none" fill-rule="{eostr}" d="',
- ]
- for loop in loops:
- V = svg_path_loop_data(loop)
- indent1 = 4
- indent2 = 6
- MLFrags = []
- LFrags = []
- MLFrags.append(LFrags)
- indent = indent1
- c = indent1 - 1
- for frag in V:
- if c + 1 + len(frag) > 76:
- LFrags = []
- MLFrags.append(LFrags)
- indent = indent2
- c = indent1 - 1
- LFrags.append(frag)
- c += 1 + len(frag)
- indent = indent1
- for LFrags in MLFrags:
- S = ' ' * indent + ' '.join(LFrags)
- SVGLines.append(S)
- indent = indent2
- SVGLines.append(' "/>')
- SVGLines.append('</g>')
- SVGLines.append('</svg>')
- return SVGLines
- def save_lines(filename, lines):
- with open(filename, 'w') as f:
- for line in lines:
- f.write("{}\n".format(line))
- def get_arguments():
- cmd = os.path.basename(sys.argv[0])
- parser = argparse.ArgumentParser(
- prog=cmd,
- add_help=False,
- description="Converts a bitmapped QRCode image to an SVG."
- )
- parser.add_argument(
- "-h", "--help",
- dest="help", action="store_true",
- help="Display this message and exit.")
- parser.add_argument(
- "-m", "--margin", metavar="MARGIN",
- dest="margin", type=int, action="store",
- help=("Set margin (quiet zone) in modules."))
- parser.add_argument(
- "-t", "--title", metavar="TITLE",
- dest="title", type=str, action="store",
- help=("SVG title."))
- parser.add_argument(
- "-w", "--width", metavar="WIDTH",
- dest="widthstr", type=str, action="store",
- help=("Physical SVG width with unit."))
- parser.add_argument(
- "-i", "--evenodd",
- dest="evenodd", action="store_true",
- help="Allow self-intersections.")
- parser.add_argument(
- "-b", "--bevel",
- dest="bevel", action="store_true",
- help="Cut corners to show path loops.")
- parser.add_argument(
- "-x", "--transparent",
- dest="transparent", action="store_true",
- help="Exclude the filled background rectangle.")
- parser.add_argument(
- "--foreground",
- dest="foreground", action="store",
- help="CSS-style foreground colour.")
- parser.add_argument(
- "--background",
- dest="background", action="store",
- help="CSS-style background colour.")
- parser.add_argument(
- "-V", "--version",
- dest="version", action="store_true",
- help="Display version and exit.")
- parser.add_argument(
- "filename", metavar="QRCODE",
- type=str,
- help=("The bitmapped QRCode image to read."))
- parser.add_argument(
- "outfilename", metavar="SVGFILE",
- type=str,
- help=("The SVG file to create."))
- if "-h" in sys.argv or "--help" in sys.argv:
- parser.print_help()
- sys.exit(0)
- if "-V" in sys.argv or "--version" in sys.argv:
- print(VERSION)
- sys.exit(0)
- args = parser.parse_args()
- return args
- def main():
- result = 0
- err_msg = ''
- cmd = os.path.basename(sys.argv[0])
- try:
- args = get_arguments()
- M, margins = load_qr_image(args.filename)
- if args.margin is not None:
- margins = args.margin
- M = ~(np.array(M, dtype=bool))
- L = loops_from_boolimg(M, args.evenodd)
- S = svg_lines_from_loops(
- L,
- args.evenodd,
- widthstr = args.widthstr,
- title = args.title,
- margins = margins,
- foreground = args.foreground,
- background = args.background,
- transparent = args.transparent,
- soft = args.bevel,
- )
- save_lines(args.outfilename, S)
- except ArgError as E:
- err_msg = 'Error: ' + str(E)
- result = 2
- except FileError as E:
- err_msg = str(E)
- result = 3
- except CmdError as E:
- err_msg = str(E)
- result = 4
- except DataError as E:
- err_msg = str(E)
- result = 5
- except Exception as E:
- exc_type, exc_value, exc_traceback = sys.exc_info()
- err_lines = traceback.format_exc().splitlines()
- err_msg = 'Unhandled exception:\n' + '\n'.join(err_lines)
- result = 1
- if err_msg != '':
- print(cmd + ': ' + err_msg, file=sys.stderr)
- return result
- if __name__ == '__main__':
- main()
Advertisement