From d26b68867b58593d0b07dbc12b4fba97b9454c1e Mon Sep 17 00:00:00 2001 From: j <@> Date: Sun, 10 Sep 2023 18:34:34 +0000 Subject: [PATCH 01/10] Only import png if needed. --- qrcode/image/pure.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/qrcode/image/pure.py b/qrcode/image/pure.py index 690ebe0c..84b6b704 100644 --- a/qrcode/image/pure.py +++ b/qrcode/image/pure.py @@ -1,7 +1,5 @@ from itertools import chain -import png - import qrcode.image.base @@ -15,6 +13,7 @@ class PyPNGImage(qrcode.image.base.BaseImage): needs_drawrect = False def new_image(self, **kwargs): + import png return png.Writer(self.pixel_size, self.pixel_size, greyscale=True, bitdepth=1) def drawrect(self, row, col): From a9833a95e197c8ae57610f8d9c5bd68229d96595 Mon Sep 17 00:00:00 2001 From: j <@> Date: Sun, 10 Sep 2023 18:36:15 +0000 Subject: [PATCH 02/10] Add "svg-compressed" to create a simple classic QR Code with compressed file size. --- qrcode/console_scripts.py | 1 + qrcode/image/styles/moduledrawers/svg.py | 15 +++ qrcode/image/svg.py | 149 ++++++++++++++++++++++- 3 files changed, 164 insertions(+), 1 deletion(-) diff --git a/qrcode/console_scripts.py b/qrcode/console_scripts.py index 424fe6fd..c5c607f0 100755 --- a/qrcode/console_scripts.py +++ b/qrcode/console_scripts.py @@ -25,6 +25,7 @@ "svg": "qrcode.image.svg.SvgImage", "svg-fragment": "qrcode.image.svg.SvgFragmentImage", "svg-path": "qrcode.image.svg.SvgPathImage", + "svg-compressed": "qrcode.image.svg.SvgCompressedImage", # Keeping for backwards compatibility: "pymaging": "qrcode.image.pure.PymagingImage", } diff --git a/qrcode/image/styles/moduledrawers/svg.py b/qrcode/image/styles/moduledrawers/svg.py index 6e629759..7e9cc788 100644 --- a/qrcode/image/styles/moduledrawers/svg.py +++ b/qrcode/image/styles/moduledrawers/svg.py @@ -110,6 +110,21 @@ def subpath(self, box) -> str: ... +class SvgCompressedDrawer(BaseSvgQRModuleDrawer): + img: "SvgPathImage" + + def drawrect(self, box, is_active: bool): + if not is_active: + return + coords = self.coords(box) + x0 = self.img.units(coords.x0, text=False) + y0 = self.img.units(coords.y0, text=False) + assert self.img.units(coords.x1, text=False) - 1 == x0 + assert self.img.units(coords.y1, text=False) - 1 == y0 + self.img._points.append([int(x0),int(y0)]) + + + class SvgPathSquareDrawer(SvgPathQRModuleDrawer): def subpath(self, box) -> str: coords = self.coords(box) diff --git a/qrcode/image/svg.py b/qrcode/image/svg.py index bf0ec870..4c0aff1a 100644 --- a/qrcode/image/svg.py +++ b/qrcode/image/svg.py @@ -1,4 +1,4 @@ -import decimal +import decimal, enum from decimal import Decimal from typing import List, Optional, Type, Union, overload @@ -163,6 +163,153 @@ def process(self): self._img.append(self.path) +class SvgCompressedImage(SvgImage): + """ + SVG image builder with goal of smallest possible output, at least among + algorithms with predictable fast run time. + """ + + needs_processing = True + path: Optional[ET.Element] = None + default_drawer_class: Type[QRModuleDrawer] = svg_drawers.SvgCompressedDrawer + + def __init__(self, *args, **kwargs): + self._points = [] + super().__init__(*args, **kwargs) + + def _svg(self, viewBox=None, **kwargs): + if viewBox is None: + dimension = self.units(self.pixel_size, text=False) + # Save characters by moving real pixels to start at 0,0 with a negative + # offset for the border, with more real pixels having lower digit counts. + viewBox = "-{b} -{b} {d} {d}".format(d=dimension, b=self.border) + return super()._svg(viewBox=viewBox, **kwargs) + + def _generate_subpaths(self): + """ + Yield a series of paths which walk the grid, drawing squares on, + and also drawing reverse transparency holes, to complete the SVG. + """ + # what we should make, juxtaposed against what we currently have + goal = [ [0]*(self.width+2) for i in range(self.width+2) ] + curr = [ [0]*(self.width+2) for i in range(self.width+2) ] + for point in self._points: + # The +1 -1 allows the path walk logic to not worry about image edges. + goal[point[0]-self.border+1][point[1]-self.border+1] = 1 + + def abs_or_delta(cmds, curr, last, curr_2=None, last_2=None): + ''' Use whichever is shorter: the absolute command, or delta command.''' + # The +1 -1 allows the path walk logic to not worry about image edges. + curr -= 1 + last -= 1 + if curr_2 != None: curr_2 -= 1 + if last_2 != None: last_2 -= 1 + def opt_join(a, b=None): + if b == None: + return '%d'%a + return '%d'%a+('' if b < 0 else ' ')+'%d'%b + return min([ + cmds[0]+opt_join(curr-last, curr_2-last_2 if curr_2 != None else None), + cmds[1]+opt_join(curr, curr_2) + ], key=len) + + class WD(enum.IntEnum): + North = 1 + South = 2 + East = 3 + West = 4 + + # Old cursor position allows optimizing with "m" sometimes instead of "M". + # The +1 -1 allows the path walk logic to not worry about image edges. + old_cursor = (1,1) + + # Go over the grid, creating the paths. This ordering seems to work fairly + # well, although it's not necessarily optimal. Unfortunately optimal is a + # traveling salesman problem, and it's not obvious whether there's any + # significantly better possible ordering in general. + for start_y in range(self.width+2): + for start_x in range(self.width+2): + if goal[start_x][start_y] == curr[start_x][start_y]: + continue + + # Note, the 'm' here is starting from the old cursor spot, which (as per SVG + # spec) is not the close path spot. We could test for both, trying a 'z' to + # to save characters for the next 'm'. However, the mathematically first + # opportunity would be a convert of 'm1 100' to 'm1 9', so would require a + # straight line of 91 pairs of identical pixels. I believe the QR spec allows + # for that, but it is essentially impossible by chance. + path = abs_or_delta('mM', start_x, old_cursor[0], start_y, old_cursor[1]) + path_flips = {} + paint_on = goal[start_x][start_y] + path_dir = WD.East if paint_on else WD.South + (curr_x, curr_y) = (last_x, last_y) = (start_x, start_y) + + while True: + match path_dir: + case WD.East: + while goal[curr_x][curr_y] and not goal[curr_x ][curr_y-1]: + if curr_x not in path_flips: + path_flips[curr_x] = [] + path_flips[curr_x].append(curr_y) + curr_x += 1 + assert curr_x != last_x + path_dir = WD.North if goal[curr_x][curr_y-1] else WD.South + if (curr_x, curr_y) == (start_x, start_y): + break # path is done + path += abs_or_delta('hH', curr_x, last_x) + case WD.West: + while not goal[curr_x-1][curr_y] and goal[curr_x-1][curr_y-1]: + curr_x -= 1 + if curr_x not in path_flips: + path_flips[curr_x] = [] + path_flips[curr_x].append(curr_y) + assert curr_x != last_x + path_dir = WD.South if goal[curr_x-1][curr_y] else WD.North + if (curr_x, curr_y) == (start_x, start_y): + break # path is done + path += abs_or_delta('hH', curr_x, last_x) + case WD.North: + while goal[curr_x][curr_y-1] and not goal[curr_x-1][curr_y-1]: + curr_y -= 1 + assert curr_y != last_y + path_dir = WD.West if goal[curr_x-1][curr_y-1] else WD.East + if (curr_x, curr_y) == (start_x, start_y): + break # path is done + path += abs_or_delta('vV', curr_y, last_y) + case WD.South: + while not goal[curr_x][curr_y] and goal[curr_x-1][curr_y]: + curr_y += 1 + assert curr_y != last_y + path_dir = WD.East if goal[curr_x][curr_y] else WD.West + if (curr_x, curr_y) == (start_x, start_y): + break # path is done + path += abs_or_delta('vV', curr_y, last_y) + case _: raise + assert (last_x, last_y) != (curr_x, curr_y), goal + (last_x, last_y) = (curr_x, curr_y) + old_cursor = (last_x, last_y) + yield path + + # Note that only one dimension (which was arbitrary chosen here as + # horizontal) needs to be evaluated to determine all of the pixel flips. + for x,ys in path_flips.items(): + ys = sorted(ys, reverse=True) + while len(ys) > 1: + for y in range(ys.pop(),ys.pop()): + curr[x][y] = paint_on + + + def process(self): + # Store the path just in case someone wants to use it again or in some + # unique way. + self.path = ET.Element( + ET.QName("path"), # type: ignore + d="".join(self._generate_subpaths()), + fill="#000", + ) + self._img.append(self.path) + + class SvgFillImage(SvgImage): """ An SvgImage that fills the background to white. From 15d605900e79c204ca66f38a1d20a252b5975a8e Mon Sep 17 00:00:00 2001 From: j <@> Date: Fri, 15 Sep 2023 16:41:34 +0000 Subject: [PATCH 03/10] Where possible, splice carve-out paths onto existing paths, for further savings by removing M cmds. --- qrcode/image/svg.py | 88 +++++++++++++++++++++++++++++++-------------- 1 file changed, 61 insertions(+), 27 deletions(-) diff --git a/qrcode/image/svg.py b/qrcode/image/svg.py index 4c0aff1a..f2fb4467 100644 --- a/qrcode/image/svg.py +++ b/qrcode/image/svg.py @@ -197,21 +197,19 @@ def _generate_subpaths(self): # The +1 -1 allows the path walk logic to not worry about image edges. goal[point[0]-self.border+1][point[1]-self.border+1] = 1 - def abs_or_delta(cmds, curr, last, curr_2=None, last_2=None): + def abs_or_delta(cmds, curr_x, last_x, curr_y, last_y): ''' Use whichever is shorter: the absolute command, or delta command.''' - # The +1 -1 allows the path walk logic to not worry about image edges. - curr -= 1 - last -= 1 - if curr_2 != None: curr_2 -= 1 - if last_2 != None: last_2 -= 1 - def opt_join(a, b=None): + def opt_join(a, b): + if a == None: + return '%d'%b if b == None: return '%d'%a return '%d'%a+('' if b < 0 else ' ')+'%d'%b - return min([ - cmds[0]+opt_join(curr-last, curr_2-last_2 if curr_2 != None else None), - cmds[1]+opt_join(curr, curr_2) - ], key=len) + return ((curr_x, curr_y), min([ + cmds[0]+opt_join(curr_x-last_x if last_x != None else None, curr_y-last_y if last_y != None else None), + # The +1 -1 allows the path walk logic to not worry about image edges. + cmds[1]+opt_join(curr_x-1 if last_x != None else None, curr_y-1 if last_y != None else None) + ], key=len)) class WD(enum.IntEnum): North = 1 @@ -222,14 +220,15 @@ class WD(enum.IntEnum): # Old cursor position allows optimizing with "m" sometimes instead of "M". # The +1 -1 allows the path walk logic to not worry about image edges. old_cursor = (1,1) + fullpath = [] # Go over the grid, creating the paths. This ordering seems to work fairly # well, although it's not necessarily optimal. Unfortunately optimal is a # traveling salesman problem, and it's not obvious whether there's any # significantly better possible ordering in general. - for start_y in range(self.width+2): - for start_x in range(self.width+2): - if goal[start_x][start_y] == curr[start_x][start_y]: + for search_y in range(self.width+2): + for search_x in range(self.width+2): + if goal[search_x][search_y] == curr[search_x][search_y]: continue # Note, the 'm' here is starting from the old cursor spot, which (as per SVG @@ -238,8 +237,11 @@ class WD(enum.IntEnum): # opportunity would be a convert of 'm1 100' to 'm1 9', so would require a # straight line of 91 pairs of identical pixels. I believe the QR spec allows # for that, but it is essentially impossible by chance. - path = abs_or_delta('mM', start_x, old_cursor[0], start_y, old_cursor[1]) + (start_x, start_y) = (search_x, search_y) + subpath = [abs_or_delta('mM', start_x, old_cursor[0], start_y, old_cursor[1])] path_flips = {} + splice_points = set(i[0] for i in fullpath) + do_splice = None # The point where we are doing a splice, to save on 'M's. paint_on = goal[start_x][start_y] path_dir = WD.East if paint_on else WD.South (curr_x, curr_y) = (last_x, last_y) = (start_x, start_y) @@ -254,9 +256,16 @@ class WD(enum.IntEnum): curr_x += 1 assert curr_x != last_x path_dir = WD.North if goal[curr_x][curr_y-1] else WD.South + if do_splice or (curr_x, curr_y) != (start_x, start_y): + subpath.append(abs_or_delta('hH', curr_x, last_x, curr_y, None)) if (curr_x, curr_y) == (start_x, start_y): - break # path is done - path += abs_or_delta('hH', curr_x, last_x) + break # subpath is done + if not do_splice and (curr_x, curr_y) in splice_points: + subpath = [] + path_flips = {} + do_splice = (start_x, start_y) = (curr_x, curr_y) + continue + case WD.West: while not goal[curr_x-1][curr_y] and goal[curr_x-1][curr_y-1]: curr_x -= 1 @@ -265,30 +274,55 @@ class WD(enum.IntEnum): path_flips[curr_x].append(curr_y) assert curr_x != last_x path_dir = WD.South if goal[curr_x-1][curr_y] else WD.North + if do_splice or (curr_x, curr_y) != (start_x, start_y): + subpath.append(abs_or_delta('hH', curr_x, last_x, curr_y, None)) if (curr_x, curr_y) == (start_x, start_y): - break # path is done - path += abs_or_delta('hH', curr_x, last_x) + break # subpath is done + if not do_splice and (curr_x, curr_y) in splice_points: + subpath = [] + path_flips = {} + do_splice = (start_x, start_y) = (curr_x, curr_y) + continue + case WD.North: while goal[curr_x][curr_y-1] and not goal[curr_x-1][curr_y-1]: curr_y -= 1 assert curr_y != last_y path_dir = WD.West if goal[curr_x-1][curr_y-1] else WD.East + if do_splice or (curr_x, curr_y) != (start_x, start_y): + subpath.append(abs_or_delta('vV', curr_x, None, curr_y, last_y)) if (curr_x, curr_y) == (start_x, start_y): - break # path is done - path += abs_or_delta('vV', curr_y, last_y) + break # subpath is done + if not do_splice and (curr_x, curr_y) in splice_points: + subpath = [] + path_flips = {} + do_splice = (start_x, start_y) = (curr_x, curr_y) + continue + case WD.South: while not goal[curr_x][curr_y] and goal[curr_x-1][curr_y]: curr_y += 1 assert curr_y != last_y path_dir = WD.East if goal[curr_x][curr_y] else WD.West + if do_splice or (curr_x, curr_y) != (start_x, start_y): + subpath.append(abs_or_delta('vV', curr_x, None, curr_y, last_y)) if (curr_x, curr_y) == (start_x, start_y): - break # path is done - path += abs_or_delta('vV', curr_y, last_y) + break # subpath is done + if not do_splice and (curr_x, curr_y) in splice_points: + subpath = [] + path_flips = {} + do_splice = (start_x, start_y) = (curr_x, curr_y) + continue + case _: raise assert (last_x, last_y) != (curr_x, curr_y), goal (last_x, last_y) = (curr_x, curr_y) - old_cursor = (last_x, last_y) - yield path + if do_splice: + splice_index = next(index for (index, i) in enumerate(fullpath) if i[0] == (start_x, start_y))+1 + fullpath[splice_index:splice_index] = subpath + else: + old_cursor = (last_x, last_y) + fullpath += subpath # Note that only one dimension (which was arbitrary chosen here as # horizontal) needs to be evaluated to determine all of the pixel flips. @@ -297,14 +331,14 @@ class WD(enum.IntEnum): while len(ys) > 1: for y in range(ys.pop(),ys.pop()): curr[x][y] = paint_on - + return ''.join(i[1] for i in fullpath) def process(self): # Store the path just in case someone wants to use it again or in some # unique way. self.path = ET.Element( ET.QName("path"), # type: ignore - d="".join(self._generate_subpaths()), + d=self._generate_subpaths(), fill="#000", ) self._img.append(self.path) From 4d202fbbacf4df7786a51ec40e89347870941145 Mon Sep 17 00:00:00 2001 From: j <@> Date: Sat, 16 Sep 2023 15:09:40 +0000 Subject: [PATCH 04/10] Speed improvement. Fix issue where the start of a subpath could never be spliced in. --- qrcode/image/svg.py | 39 +++++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/qrcode/image/svg.py b/qrcode/image/svg.py index f2fb4467..021fa6e1 100644 --- a/qrcode/image/svg.py +++ b/qrcode/image/svg.py @@ -221,6 +221,7 @@ class WD(enum.IntEnum): # The +1 -1 allows the path walk logic to not worry about image edges. old_cursor = (1,1) fullpath = [] + fullpath_splice_points = set() # Go over the grid, creating the paths. This ordering seems to work fairly # well, although it's not necessarily optimal. Unfortunately optimal is a @@ -240,12 +241,24 @@ class WD(enum.IntEnum): (start_x, start_y) = (search_x, search_y) subpath = [abs_or_delta('mM', start_x, old_cursor[0], start_y, old_cursor[1])] path_flips = {} - splice_points = set(i[0] for i in fullpath) - do_splice = None # The point where we are doing a splice, to save on 'M's. + do_splice = False # The point where we are doing a splice, to save on 'M's. paint_on = goal[start_x][start_y] path_dir = WD.East if paint_on else WD.South (curr_x, curr_y) = (last_x, last_y) = (start_x, start_y) + def should_switch_to_splicing(): + nonlocal do_splice, start_x, start_y + if not do_splice and (curr_x, curr_y) in fullpath_splice_points: + subpath.clear() + path_flips.clear() + do_splice |= True + (start_x, start_y) = (curr_x, curr_y) + return True + return False + + # Immediately check for a need to splice in, right from the starting point. + should_switch_to_splicing() + while True: match path_dir: case WD.East: @@ -260,10 +273,7 @@ class WD(enum.IntEnum): subpath.append(abs_or_delta('hH', curr_x, last_x, curr_y, None)) if (curr_x, curr_y) == (start_x, start_y): break # subpath is done - if not do_splice and (curr_x, curr_y) in splice_points: - subpath = [] - path_flips = {} - do_splice = (start_x, start_y) = (curr_x, curr_y) + if should_switch_to_splicing(): continue case WD.West: @@ -278,10 +288,7 @@ class WD(enum.IntEnum): subpath.append(abs_or_delta('hH', curr_x, last_x, curr_y, None)) if (curr_x, curr_y) == (start_x, start_y): break # subpath is done - if not do_splice and (curr_x, curr_y) in splice_points: - subpath = [] - path_flips = {} - do_splice = (start_x, start_y) = (curr_x, curr_y) + if should_switch_to_splicing(): continue case WD.North: @@ -293,10 +300,7 @@ class WD(enum.IntEnum): subpath.append(abs_or_delta('vV', curr_x, None, curr_y, last_y)) if (curr_x, curr_y) == (start_x, start_y): break # subpath is done - if not do_splice and (curr_x, curr_y) in splice_points: - subpath = [] - path_flips = {} - do_splice = (start_x, start_y) = (curr_x, curr_y) + if should_switch_to_splicing(): continue case WD.South: @@ -308,10 +312,7 @@ class WD(enum.IntEnum): subpath.append(abs_or_delta('vV', curr_x, None, curr_y, last_y)) if (curr_x, curr_y) == (start_x, start_y): break # subpath is done - if not do_splice and (curr_x, curr_y) in splice_points: - subpath = [] - path_flips = {} - do_splice = (start_x, start_y) = (curr_x, curr_y) + if should_switch_to_splicing(): continue case _: raise @@ -323,6 +324,8 @@ class WD(enum.IntEnum): else: old_cursor = (last_x, last_y) fullpath += subpath + for i in subpath: + fullpath_splice_points.add(i[0]) # Note that only one dimension (which was arbitrary chosen here as # horizontal) needs to be evaluated to determine all of the pixel flips. From ff2c1957ecce8ea646125cf9afb827620eab22d9 Mon Sep 17 00:00:00 2001 From: j <@> Date: Sat, 16 Sep 2023 18:39:19 +0000 Subject: [PATCH 05/10] Create a linked-list, with dictionary pointers to splice points, in order to avoid the O(n^2)-ish perf hit for splicing a python list repeatedly. Only store spliceable turns, and assert that we dealt with all of them correctly. --- qrcode/image/svg.py | 99 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 76 insertions(+), 23 deletions(-) diff --git a/qrcode/image/svg.py b/qrcode/image/svg.py index 021fa6e1..91646970 100644 --- a/qrcode/image/svg.py +++ b/qrcode/image/svg.py @@ -197,19 +197,17 @@ def _generate_subpaths(self): # The +1 -1 allows the path walk logic to not worry about image edges. goal[point[0]-self.border+1][point[1]-self.border+1] = 1 - def abs_or_delta(cmds, curr_x, last_x, curr_y, last_y): + def abs_or_delta(cmds, curr_1, last_1, curr_2=None, last_2=None): ''' Use whichever is shorter: the absolute command, or delta command.''' def opt_join(a, b): - if a == None: - return '%d'%b if b == None: return '%d'%a return '%d'%a+('' if b < 0 else ' ')+'%d'%b - return ((curr_x, curr_y), min([ - cmds[0]+opt_join(curr_x-last_x if last_x != None else None, curr_y-last_y if last_y != None else None), + return min([ + cmds[0]+opt_join(curr_1-last_1, curr_2-last_2 if curr_2 != None else None), # The +1 -1 allows the path walk logic to not worry about image edges. - cmds[1]+opt_join(curr_x-1 if last_x != None else None, curr_y-1 if last_y != None else None) - ], key=len)) + cmds[1]+opt_join(curr_1-1 , curr_2-1 if curr_2 != None else None) + ], key=len) class WD(enum.IntEnum): North = 1 @@ -217,11 +215,20 @@ class WD(enum.IntEnum): East = 3 West = 4 + class PathChain(): + __slots__ = ['cmds','next'] + def __init__(self): + self.cmds = '' + self.next = None + def create_next(self): + self.next = PathChain() + return self.next + # Old cursor position allows optimizing with "m" sometimes instead of "M". # The +1 -1 allows the path walk logic to not worry about image edges. old_cursor = (1,1) - fullpath = [] - fullpath_splice_points = set() + fullpath_head = fullpath_tail = None + fullpath_splice_points = {} # Go over the grid, creating the paths. This ordering seems to work fairly # well, although it's not necessarily optimal. Unfortunately optimal is a @@ -239,23 +246,35 @@ class WD(enum.IntEnum): # straight line of 91 pairs of identical pixels. I believe the QR spec allows # for that, but it is essentially impossible by chance. (start_x, start_y) = (search_x, search_y) - subpath = [abs_or_delta('mM', start_x, old_cursor[0], start_y, old_cursor[1])] + subpath_head = subpath_tail = PathChain() + subpath_head.cmds = abs_or_delta('mM', start_x, old_cursor[0], start_y, old_cursor[1]) path_flips = {} do_splice = False # The point where we are doing a splice, to save on 'M's. + subpath_splice_points = {} paint_on = goal[start_x][start_y] path_dir = WD.East if paint_on else WD.South (curr_x, curr_y) = (last_x, last_y) = (start_x, start_y) def should_switch_to_splicing(): - nonlocal do_splice, start_x, start_y + nonlocal do_splice, start_x, start_y, subpath_head, subpath_tail if not do_splice and (curr_x, curr_y) in fullpath_splice_points: - subpath.clear() + subpath_head = subpath_tail = PathChain() path_flips.clear() + subpath_splice_points.clear() do_splice |= True (start_x, start_y) = (curr_x, curr_y) return True return False + def add_to_splice_points(): + nonlocal subpath_tail + if (curr_x, curr_y) in subpath_splice_points: + # we hit a splice point a second time, so topology dictates it's done + subpath_splice_points.pop((curr_x, curr_y)) + else: + subpath_splice_points[curr_x, curr_y] = subpath_tail + subpath_tail = subpath_tail.create_next() + # Immediately check for a need to splice in, right from the starting point. should_switch_to_splicing() @@ -270,7 +289,12 @@ def should_switch_to_splicing(): assert curr_x != last_x path_dir = WD.North if goal[curr_x][curr_y-1] else WD.South if do_splice or (curr_x, curr_y) != (start_x, start_y): - subpath.append(abs_or_delta('hH', curr_x, last_x, curr_y, None)) + subpath_tail.cmds += abs_or_delta('hH', curr_x, last_x) + + # only a left turn with a hole coming up on the right is spliceable + if path_dir == WD.North and not goal[curr_x][curr_y]: + add_to_splice_points() + if (curr_x, curr_y) == (start_x, start_y): break # subpath is done if should_switch_to_splicing(): @@ -285,7 +309,12 @@ def should_switch_to_splicing(): assert curr_x != last_x path_dir = WD.South if goal[curr_x-1][curr_y] else WD.North if do_splice or (curr_x, curr_y) != (start_x, start_y): - subpath.append(abs_or_delta('hH', curr_x, last_x, curr_y, None)) + subpath_tail.cmds += abs_or_delta('hH', curr_x, last_x) + + # only a left turn with a hole coming up on the right is spliceable + if path_dir == WD.South and not goal[curr_x-1][curr_y-1]: + add_to_splice_points() + if (curr_x, curr_y) == (start_x, start_y): break # subpath is done if should_switch_to_splicing(): @@ -297,7 +326,12 @@ def should_switch_to_splicing(): assert curr_y != last_y path_dir = WD.West if goal[curr_x-1][curr_y-1] else WD.East if do_splice or (curr_x, curr_y) != (start_x, start_y): - subpath.append(abs_or_delta('vV', curr_x, None, curr_y, last_y)) + subpath_tail.cmds += abs_or_delta('vV', curr_y, last_y) + + # only a left turn with a hole coming up on the right is spliceable + if path_dir == WD.West and not goal[curr_x][curr_y-1]: + add_to_splice_points() + if (curr_x, curr_y) == (start_x, start_y): break # subpath is done if should_switch_to_splicing(): @@ -309,7 +343,12 @@ def should_switch_to_splicing(): assert curr_y != last_y path_dir = WD.East if goal[curr_x][curr_y] else WD.West if do_splice or (curr_x, curr_y) != (start_x, start_y): - subpath.append(abs_or_delta('vV', curr_x, None, curr_y, last_y)) + subpath_tail.cmds += abs_or_delta('vV', curr_y, last_y) + + # only a left turn with a hole coming up on the right is spliceable + if path_dir == WD.East and not goal[curr_x-1][curr_y]: + add_to_splice_points() + if (curr_x, curr_y) == (start_x, start_y): break # subpath is done if should_switch_to_splicing(): @@ -318,14 +357,25 @@ def should_switch_to_splicing(): case _: raise assert (last_x, last_y) != (curr_x, curr_y), goal (last_x, last_y) = (curr_x, curr_y) + if do_splice: - splice_index = next(index for (index, i) in enumerate(fullpath) if i[0] == (start_x, start_y))+1 - fullpath[splice_index:splice_index] = subpath + subpath_tail.next = fullpath_splice_points[start_x, start_y].next + fullpath_splice_points[start_x, start_y].next = subpath_head else: + if not fullpath_head: + fullpath_head = subpath_head + else: + fullpath_tail.next = subpath_head + fullpath_tail = subpath_tail old_cursor = (last_x, last_y) - fullpath += subpath - for i in subpath: - fullpath_splice_points.add(i[0]) + + for k,v in subpath_splice_points.items(): + if k in fullpath_splice_points: + # we hit a splice point a second time, so topology dictates it's done + fullpath_splice_points.pop(k) + else: + # merge new splice point + fullpath_splice_points[k] = v # Note that only one dimension (which was arbitrary chosen here as # horizontal) needs to be evaluated to determine all of the pixel flips. @@ -334,14 +384,17 @@ def should_switch_to_splicing(): while len(ys) > 1: for y in range(ys.pop(),ys.pop()): curr[x][y] = paint_on - return ''.join(i[1] for i in fullpath) + assert fullpath_splice_points == {}, fullpath_splice_points + while fullpath_head: + yield fullpath_head.cmds + fullpath_head = fullpath_head.next def process(self): # Store the path just in case someone wants to use it again or in some # unique way. self.path = ET.Element( ET.QName("path"), # type: ignore - d=self._generate_subpaths(), + d=''.join(self._generate_subpaths()), fill="#000", ) self._img.append(self.path) From 5771911042f75f60affb1b95e2aa2cfa4e6164d9 Mon Sep 17 00:00:00 2001 From: j <@> Date: Fri, 13 Oct 2023 15:10:08 +0000 Subject: [PATCH 06/10] Allow running the script without a package setup install. --- qrcode/console_scripts.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/qrcode/console_scripts.py b/qrcode/console_scripts.py index c5c607f0..881ed7e1 100755 --- a/qrcode/console_scripts.py +++ b/qrcode/console_scripts.py @@ -41,9 +41,12 @@ def main(args=None): if args is None: args = sys.argv[1:] - from pkg_resources import get_distribution + import pkg_resources - version = get_distribution("qrcode").version + try: + version = pkg_resources.get_distribution("qrcode").version + except pkg_resources.DistributionNotFound: + version = 'development' parser = optparse.OptionParser(usage=(__doc__ or "").strip(), version=version) # Wrap parser.error in a typed NoReturn method for better typing. From fdc5ef8cb93014c343033e93edabcfacd197fd58 Mon Sep 17 00:00:00 2001 From: j <@> Date: Sat, 28 Sep 2024 17:00:03 +0000 Subject: [PATCH 07/10] Allow execution from a development environment. --- qrcode/console_scripts.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/qrcode/console_scripts.py b/qrcode/console_scripts.py index 0166f00f..c0955feb 100755 --- a/qrcode/console_scripts.py +++ b/qrcode/console_scripts.py @@ -44,7 +44,11 @@ def main(args=None): if args is None: args = sys.argv[1:] - version = metadata.version("qrcode") + try: + version = metadata.version("qrcode") + except metadata.PackageNotFoundError: + version = 'development' + parser = optparse.OptionParser(usage=(__doc__ or "").strip(), version=version) # Wrap parser.error in a typed NoReturn method for better typing. From ca049dfa648d9b929a5ddd8c86afa2b5fef40da3 Mon Sep 17 00:00:00 2001 From: j <@> Date: Sat, 28 Sep 2024 17:13:21 +0000 Subject: [PATCH 08/10] Comply with use of "ruff", which unfortunately makes some of the code less readable, but oh well. --- qrcode/console_scripts.py | 2 +- qrcode/image/styles/moduledrawers/svg.py | 3 +- qrcode/image/svg.py | 366 ++++++++++++----------- 3 files changed, 201 insertions(+), 170 deletions(-) diff --git a/qrcode/console_scripts.py b/qrcode/console_scripts.py index c0955feb..5509f5d2 100755 --- a/qrcode/console_scripts.py +++ b/qrcode/console_scripts.py @@ -47,7 +47,7 @@ def main(args=None): try: version = metadata.version("qrcode") except metadata.PackageNotFoundError: - version = 'development' + version = "development" parser = optparse.OptionParser(usage=(__doc__ or "").strip(), version=version) diff --git a/qrcode/image/styles/moduledrawers/svg.py b/qrcode/image/styles/moduledrawers/svg.py index 8a723c21..8735fefb 100644 --- a/qrcode/image/styles/moduledrawers/svg.py +++ b/qrcode/image/styles/moduledrawers/svg.py @@ -119,8 +119,7 @@ def drawrect(self, box, is_active: bool): y0 = self.img.units(coords.y0, text=False) assert self.img.units(coords.x1, text=False) - 1 == x0 assert self.img.units(coords.y1, text=False) - 1 == y0 - self.img._points.append([int(x0),int(y0)]) - + self.img._points.append([int(x0), int(y0)]) class SvgPathSquareDrawer(SvgPathQRModuleDrawer): diff --git a/qrcode/image/svg.py b/qrcode/image/svg.py index a33563be..47d1cd50 100644 --- a/qrcode/image/svg.py +++ b/qrcode/image/svg.py @@ -1,4 +1,5 @@ -import decimal, enum +import decimal +import enum from decimal import Decimal from typing import List, Optional, Type, Union, overload, Literal @@ -187,23 +188,32 @@ def _generate_subpaths(self): and also drawing reverse transparency holes, to complete the SVG. """ # what we should make, juxtaposed against what we currently have - goal = [ [0]*(self.width+2) for i in range(self.width+2) ] - curr = [ [0]*(self.width+2) for i in range(self.width+2) ] + goal = [[0] * (self.width + 2) for i in range(self.width + 2)] + curr = [[0] * (self.width + 2) for i in range(self.width + 2)] for point in self._points: # The +1 -1 allows the path walk logic to not worry about image edges. - goal[point[0]-self.border+1][point[1]-self.border+1] = 1 + goal[point[0] - self.border + 1][point[1] - self.border + 1] = 1 def abs_or_delta(cmds, curr_1, last_1, curr_2=None, last_2=None): - ''' Use whichever is shorter: the absolute command, or delta command.''' + """Use whichever is shorter: the absolute command, or delta command.""" + def opt_join(a, b): - if b == None: - return '%d'%a - return '%d'%a+('' if b < 0 else ' ')+'%d'%b - return min([ - cmds[0]+opt_join(curr_1-last_1, curr_2-last_2 if curr_2 != None else None), - # The +1 -1 allows the path walk logic to not worry about image edges. - cmds[1]+opt_join(curr_1-1 , curr_2-1 if curr_2 != None else None) - ], key=len) + if b is None: + return "%d" % a + return "%d" % a + ("" if b < 0 else " ") + "%d" % b + + return min( + [ + cmds[0] + + opt_join( + curr_1 - last_1, curr_2 - last_2 if curr_2 is not None else None + ), + # The +1 -1 allows the path walk logic to not worry about image edges. + cmds[1] + + opt_join(curr_1 - 1, curr_2 - 1 if curr_2 is not None else None), + ], + key=len, + ) class WD(enum.IntEnum): North = 1 @@ -211,18 +221,20 @@ class WD(enum.IntEnum): East = 3 West = 4 - class PathChain(): - __slots__ = ['cmds','next'] + class PathChain: + __slots__ = ["cmds", "next"] + def __init__(self): - self.cmds = '' + self.cmds = "" self.next = None + def create_next(self): self.next = PathChain() return self.next # Old cursor position allows optimizing with "m" sometimes instead of "M". # The +1 -1 allows the path walk logic to not worry about image edges. - old_cursor = (1,1) + old_cursor = (1, 1) fullpath_head = fullpath_tail = None fullpath_splice_points = {} @@ -230,156 +242,176 @@ def create_next(self): # well, although it's not necessarily optimal. Unfortunately optimal is a # traveling salesman problem, and it's not obvious whether there's any # significantly better possible ordering in general. - for search_y in range(self.width+2): - for search_x in range(self.width+2): - if goal[search_x][search_y] == curr[search_x][search_y]: - continue - - # Note, the 'm' here is starting from the old cursor spot, which (as per SVG - # spec) is not the close path spot. We could test for both, trying a 'z' to - # to save characters for the next 'm'. However, the mathematically first - # opportunity would be a convert of 'm1 100' to 'm1 9', so would require a - # straight line of 91 pairs of identical pixels. I believe the QR spec allows - # for that, but it is essentially impossible by chance. - (start_x, start_y) = (search_x, search_y) - subpath_head = subpath_tail = PathChain() - subpath_head.cmds = abs_or_delta('mM', start_x, old_cursor[0], start_y, old_cursor[1]) - path_flips = {} - do_splice = False # The point where we are doing a splice, to save on 'M's. - subpath_splice_points = {} - paint_on = goal[start_x][start_y] - path_dir = WD.East if paint_on else WD.South - (curr_x, curr_y) = (last_x, last_y) = (start_x, start_y) - - def should_switch_to_splicing(): - nonlocal do_splice, start_x, start_y, subpath_head, subpath_tail - if not do_splice and (curr_x, curr_y) in fullpath_splice_points: - subpath_head = subpath_tail = PathChain() - path_flips.clear() - subpath_splice_points.clear() - do_splice |= True - (start_x, start_y) = (curr_x, curr_y) - return True - return False - - def add_to_splice_points(): - nonlocal subpath_tail - if (curr_x, curr_y) in subpath_splice_points: - # we hit a splice point a second time, so topology dictates it's done - subpath_splice_points.pop((curr_x, curr_y)) - else: - subpath_splice_points[curr_x, curr_y] = subpath_tail - subpath_tail = subpath_tail.create_next() - - # Immediately check for a need to splice in, right from the starting point. - should_switch_to_splicing() - - while True: - match path_dir: - case WD.East: - while goal[curr_x][curr_y] and not goal[curr_x ][curr_y-1]: - if curr_x not in path_flips: - path_flips[curr_x] = [] - path_flips[curr_x].append(curr_y) - curr_x += 1 - assert curr_x != last_x - path_dir = WD.North if goal[curr_x][curr_y-1] else WD.South - if do_splice or (curr_x, curr_y) != (start_x, start_y): - subpath_tail.cmds += abs_or_delta('hH', curr_x, last_x) - - # only a left turn with a hole coming up on the right is spliceable - if path_dir == WD.North and not goal[curr_x][curr_y]: - add_to_splice_points() - - if (curr_x, curr_y) == (start_x, start_y): - break # subpath is done - if should_switch_to_splicing(): - continue - - case WD.West: - while not goal[curr_x-1][curr_y] and goal[curr_x-1][curr_y-1]: - curr_x -= 1 - if curr_x not in path_flips: - path_flips[curr_x] = [] - path_flips[curr_x].append(curr_y) - assert curr_x != last_x - path_dir = WD.South if goal[curr_x-1][curr_y] else WD.North - if do_splice or (curr_x, curr_y) != (start_x, start_y): - subpath_tail.cmds += abs_or_delta('hH', curr_x, last_x) - - # only a left turn with a hole coming up on the right is spliceable - if path_dir == WD.South and not goal[curr_x-1][curr_y-1]: - add_to_splice_points() - - if (curr_x, curr_y) == (start_x, start_y): - break # subpath is done - if should_switch_to_splicing(): - continue - - case WD.North: - while goal[curr_x][curr_y-1] and not goal[curr_x-1][curr_y-1]: - curr_y -= 1 - assert curr_y != last_y - path_dir = WD.West if goal[curr_x-1][curr_y-1] else WD.East - if do_splice or (curr_x, curr_y) != (start_x, start_y): - subpath_tail.cmds += abs_or_delta('vV', curr_y, last_y) - - # only a left turn with a hole coming up on the right is spliceable - if path_dir == WD.West and not goal[curr_x][curr_y-1]: - add_to_splice_points() - - if (curr_x, curr_y) == (start_x, start_y): - break # subpath is done - if should_switch_to_splicing(): - continue - - case WD.South: - while not goal[curr_x][curr_y] and goal[curr_x-1][curr_y]: - curr_y += 1 - assert curr_y != last_y - path_dir = WD.East if goal[curr_x][curr_y] else WD.West - if do_splice or (curr_x, curr_y) != (start_x, start_y): - subpath_tail.cmds += abs_or_delta('vV', curr_y, last_y) - - # only a left turn with a hole coming up on the right is spliceable - if path_dir == WD.East and not goal[curr_x-1][curr_y]: - add_to_splice_points() - - if (curr_x, curr_y) == (start_x, start_y): - break # subpath is done - if should_switch_to_splicing(): - continue - - case _: raise - assert (last_x, last_y) != (curr_x, curr_y), goal - (last_x, last_y) = (curr_x, curr_y) - - if do_splice: - subpath_tail.next = fullpath_splice_points[start_x, start_y].next - fullpath_splice_points[start_x, start_y].next = subpath_head - else: - if not fullpath_head: - fullpath_head = subpath_head - else: - fullpath_tail.next = subpath_head - fullpath_tail = subpath_tail - old_cursor = (last_x, last_y) - - for k,v in subpath_splice_points.items(): - if k in fullpath_splice_points: - # we hit a splice point a second time, so topology dictates it's done - fullpath_splice_points.pop(k) + for search_y in range(self.width + 2): + for search_x in range(self.width + 2): + if goal[search_x][search_y] == curr[search_x][search_y]: + continue + + # Note, the 'm' here is starting from the old cursor spot, which (as per SVG + # spec) is not the close path spot. We could test for both, trying a 'z' to + # to save characters for the next 'm'. However, the mathematically first + # opportunity would be a convert of 'm1 100' to 'm1 9', so would require a + # straight line of 91 pairs of identical pixels. I believe the QR spec allows + # for that, but it is essentially impossible by chance. + (start_x, start_y) = (search_x, search_y) + subpath_head = subpath_tail = PathChain() + subpath_head.cmds = abs_or_delta( + "mM", start_x, old_cursor[0], start_y, old_cursor[1] + ) + path_flips = {} + do_splice = ( + False # The point where we are doing a splice, to save on 'M's. + ) + subpath_splice_points = {} + paint_on = goal[start_x][start_y] + path_dir = WD.East if paint_on else WD.South + (curr_x, curr_y) = (last_x, last_y) = (start_x, start_y) + + def should_switch_to_splicing(): + nonlocal do_splice, start_x, start_y, subpath_head, subpath_tail + if not do_splice and (curr_x, curr_y) in fullpath_splice_points: + subpath_head = subpath_tail = PathChain() + path_flips.clear() + subpath_splice_points.clear() + do_splice |= True + (start_x, start_y) = (curr_x, curr_y) + return True + return False + + def add_to_splice_points(): + nonlocal subpath_tail + if (curr_x, curr_y) in subpath_splice_points: + # we hit a splice point a second time, so topology dictates it's done + subpath_splice_points.pop((curr_x, curr_y)) + else: + subpath_splice_points[curr_x, curr_y] = subpath_tail + subpath_tail = subpath_tail.create_next() + + # Immediately check for a need to splice in, right from the starting point. + should_switch_to_splicing() + + while True: + match path_dir: + case WD.East: + while goal[curr_x][curr_y] and not goal[curr_x][curr_y - 1]: + if curr_x not in path_flips: + path_flips[curr_x] = [] + path_flips[curr_x].append(curr_y) + curr_x += 1 + assert curr_x != last_x + path_dir = ( + WD.North if goal[curr_x][curr_y - 1] else WD.South + ) + if do_splice or (curr_x, curr_y) != (start_x, start_y): + subpath_tail.cmds += abs_or_delta("hH", curr_x, last_x) + + # only a left turn with a hole coming up on the right is spliceable + if path_dir == WD.North and not goal[curr_x][curr_y]: + add_to_splice_points() + + if (curr_x, curr_y) == (start_x, start_y): + break # subpath is done + if should_switch_to_splicing(): + continue + + case WD.West: + while ( + not goal[curr_x - 1][curr_y] + and goal[curr_x - 1][curr_y - 1] + ): + curr_x -= 1 + if curr_x not in path_flips: + path_flips[curr_x] = [] + path_flips[curr_x].append(curr_y) + assert curr_x != last_x + path_dir = ( + WD.South if goal[curr_x - 1][curr_y] else WD.North + ) + if do_splice or (curr_x, curr_y) != (start_x, start_y): + subpath_tail.cmds += abs_or_delta("hH", curr_x, last_x) + + # only a left turn with a hole coming up on the right is spliceable + if ( + path_dir == WD.South + and not goal[curr_x - 1][curr_y - 1] + ): + add_to_splice_points() + + if (curr_x, curr_y) == (start_x, start_y): + break # subpath is done + if should_switch_to_splicing(): + continue + + case WD.North: + while ( + goal[curr_x][curr_y - 1] + and not goal[curr_x - 1][curr_y - 1] + ): + curr_y -= 1 + assert curr_y != last_y + path_dir = ( + WD.West if goal[curr_x - 1][curr_y - 1] else WD.East + ) + if do_splice or (curr_x, curr_y) != (start_x, start_y): + subpath_tail.cmds += abs_or_delta("vV", curr_y, last_y) + + # only a left turn with a hole coming up on the right is spliceable + if path_dir == WD.West and not goal[curr_x][curr_y - 1]: + add_to_splice_points() + + if (curr_x, curr_y) == (start_x, start_y): + break # subpath is done + if should_switch_to_splicing(): + continue + + case WD.South: + while not goal[curr_x][curr_y] and goal[curr_x - 1][curr_y]: + curr_y += 1 + assert curr_y != last_y + path_dir = WD.East if goal[curr_x][curr_y] else WD.West + if do_splice or (curr_x, curr_y) != (start_x, start_y): + subpath_tail.cmds += abs_or_delta("vV", curr_y, last_y) + + # only a left turn with a hole coming up on the right is spliceable + if path_dir == WD.East and not goal[curr_x - 1][curr_y]: + add_to_splice_points() + + if (curr_x, curr_y) == (start_x, start_y): + break # subpath is done + if should_switch_to_splicing(): + continue + + case _: + raise + assert (last_x, last_y) != (curr_x, curr_y), goal + (last_x, last_y) = (curr_x, curr_y) + + if do_splice: + subpath_tail.next = fullpath_splice_points[start_x, start_y].next + fullpath_splice_points[start_x, start_y].next = subpath_head else: - # merge new splice point - fullpath_splice_points[k] = v - - # Note that only one dimension (which was arbitrary chosen here as - # horizontal) needs to be evaluated to determine all of the pixel flips. - for x,ys in path_flips.items(): - ys = sorted(ys, reverse=True) - while len(ys) > 1: - for y in range(ys.pop(),ys.pop()): - curr[x][y] = paint_on + if not fullpath_head: + fullpath_head = subpath_head + else: + fullpath_tail.next = subpath_head + fullpath_tail = subpath_tail + old_cursor = (last_x, last_y) + + for k, v in subpath_splice_points.items(): + if k in fullpath_splice_points: + # we hit a splice point a second time, so topology dictates it's done + fullpath_splice_points.pop(k) + else: + # merge new splice point + fullpath_splice_points[k] = v + + # Note that only one dimension (which was arbitrary chosen here as + # horizontal) needs to be evaluated to determine all of the pixel flips. + for x, ys in path_flips.items(): + ys = sorted(ys, reverse=True) + while len(ys) > 1: + for y in range(ys.pop(), ys.pop()): + curr[x][y] = paint_on assert fullpath_splice_points == {}, fullpath_splice_points while fullpath_head: yield fullpath_head.cmds @@ -390,7 +422,7 @@ def process(self): # unique way. self.path = ET.Element( ET.QName("path"), # type: ignore - d=''.join(self._generate_subpaths()), + d="".join(self._generate_subpaths()), fill="#000", ) self._img.append(self.path) From 624c93669ccd512718298eefb2d1e44e08d9fa61 Mon Sep 17 00:00:00 2001 From: j <@> Date: Sat, 28 Sep 2024 17:20:21 +0000 Subject: [PATCH 09/10] Add support for ancient python v3.9 for some reason. --- qrcode/image/svg.py | 179 +++++++++++++++++++++----------------------- 1 file changed, 87 insertions(+), 92 deletions(-) diff --git a/qrcode/image/svg.py b/qrcode/image/svg.py index 47d1cd50..d213ce2b 100644 --- a/qrcode/image/svg.py +++ b/qrcode/image/svg.py @@ -291,98 +291,93 @@ def add_to_splice_points(): should_switch_to_splicing() while True: - match path_dir: - case WD.East: - while goal[curr_x][curr_y] and not goal[curr_x][curr_y - 1]: - if curr_x not in path_flips: - path_flips[curr_x] = [] - path_flips[curr_x].append(curr_y) - curr_x += 1 - assert curr_x != last_x - path_dir = ( - WD.North if goal[curr_x][curr_y - 1] else WD.South - ) - if do_splice or (curr_x, curr_y) != (start_x, start_y): - subpath_tail.cmds += abs_or_delta("hH", curr_x, last_x) - - # only a left turn with a hole coming up on the right is spliceable - if path_dir == WD.North and not goal[curr_x][curr_y]: - add_to_splice_points() - - if (curr_x, curr_y) == (start_x, start_y): - break # subpath is done - if should_switch_to_splicing(): - continue - - case WD.West: - while ( - not goal[curr_x - 1][curr_y] - and goal[curr_x - 1][curr_y - 1] - ): - curr_x -= 1 - if curr_x not in path_flips: - path_flips[curr_x] = [] - path_flips[curr_x].append(curr_y) - assert curr_x != last_x - path_dir = ( - WD.South if goal[curr_x - 1][curr_y] else WD.North - ) - if do_splice or (curr_x, curr_y) != (start_x, start_y): - subpath_tail.cmds += abs_or_delta("hH", curr_x, last_x) - - # only a left turn with a hole coming up on the right is spliceable - if ( - path_dir == WD.South - and not goal[curr_x - 1][curr_y - 1] - ): - add_to_splice_points() - - if (curr_x, curr_y) == (start_x, start_y): - break # subpath is done - if should_switch_to_splicing(): - continue - - case WD.North: - while ( - goal[curr_x][curr_y - 1] - and not goal[curr_x - 1][curr_y - 1] - ): - curr_y -= 1 - assert curr_y != last_y - path_dir = ( - WD.West if goal[curr_x - 1][curr_y - 1] else WD.East - ) - if do_splice or (curr_x, curr_y) != (start_x, start_y): - subpath_tail.cmds += abs_or_delta("vV", curr_y, last_y) - - # only a left turn with a hole coming up on the right is spliceable - if path_dir == WD.West and not goal[curr_x][curr_y - 1]: - add_to_splice_points() - - if (curr_x, curr_y) == (start_x, start_y): - break # subpath is done - if should_switch_to_splicing(): - continue - - case WD.South: - while not goal[curr_x][curr_y] and goal[curr_x - 1][curr_y]: - curr_y += 1 - assert curr_y != last_y - path_dir = WD.East if goal[curr_x][curr_y] else WD.West - if do_splice or (curr_x, curr_y) != (start_x, start_y): - subpath_tail.cmds += abs_or_delta("vV", curr_y, last_y) - - # only a left turn with a hole coming up on the right is spliceable - if path_dir == WD.East and not goal[curr_x - 1][curr_y]: - add_to_splice_points() - - if (curr_x, curr_y) == (start_x, start_y): - break # subpath is done - if should_switch_to_splicing(): - continue - - case _: - raise + if path_dir == WD.East: + while goal[curr_x][curr_y] and not goal[curr_x][curr_y - 1]: + if curr_x not in path_flips: + path_flips[curr_x] = [] + path_flips[curr_x].append(curr_y) + curr_x += 1 + assert curr_x != last_x + path_dir = ( + WD.North if goal[curr_x][curr_y - 1] else WD.South + ) + if do_splice or (curr_x, curr_y) != (start_x, start_y): + subpath_tail.cmds += abs_or_delta("hH", curr_x, last_x) + + # only a left turn with a hole coming up on the right is spliceable + if path_dir == WD.North and not goal[curr_x][curr_y]: + add_to_splice_points() + + if (curr_x, curr_y) == (start_x, start_y): + break # subpath is done + if should_switch_to_splicing(): + continue + elif path_dir == WD.West: + while ( + not goal[curr_x - 1][curr_y] + and goal[curr_x - 1][curr_y - 1] + ): + curr_x -= 1 + if curr_x not in path_flips: + path_flips[curr_x] = [] + path_flips[curr_x].append(curr_y) + assert curr_x != last_x + path_dir = ( + WD.South if goal[curr_x - 1][curr_y] else WD.North + ) + if do_splice or (curr_x, curr_y) != (start_x, start_y): + subpath_tail.cmds += abs_or_delta("hH", curr_x, last_x) + + # only a left turn with a hole coming up on the right is spliceable + if ( + path_dir == WD.South + and not goal[curr_x - 1][curr_y - 1] + ): + add_to_splice_points() + + if (curr_x, curr_y) == (start_x, start_y): + break # subpath is done + if should_switch_to_splicing(): + continue + elif path_dir == WD.North: + while ( + goal[curr_x][curr_y - 1] + and not goal[curr_x - 1][curr_y - 1] + ): + curr_y -= 1 + assert curr_y != last_y + path_dir = ( + WD.West if goal[curr_x - 1][curr_y - 1] else WD.East + ) + if do_splice or (curr_x, curr_y) != (start_x, start_y): + subpath_tail.cmds += abs_or_delta("vV", curr_y, last_y) + + # only a left turn with a hole coming up on the right is spliceable + if path_dir == WD.West and not goal[curr_x][curr_y - 1]: + add_to_splice_points() + + if (curr_x, curr_y) == (start_x, start_y): + break # subpath is done + if should_switch_to_splicing(): + continue + elif path_dir == WD.South: + while not goal[curr_x][curr_y] and goal[curr_x - 1][curr_y]: + curr_y += 1 + assert curr_y != last_y + path_dir = WD.East if goal[curr_x][curr_y] else WD.West + if do_splice or (curr_x, curr_y) != (start_x, start_y): + subpath_tail.cmds += abs_or_delta("vV", curr_y, last_y) + + # only a left turn with a hole coming up on the right is spliceable + if path_dir == WD.East and not goal[curr_x - 1][curr_y]: + add_to_splice_points() + + if (curr_x, curr_y) == (start_x, start_y): + break # subpath is done + if should_switch_to_splicing(): + continue + else: + raise assert (last_x, last_y) != (curr_x, curr_y), goal (last_x, last_y) = (curr_x, curr_y) From 26036861e195c5725e394144af7526c83ec0c0ae Mon Sep 17 00:00:00 2001 From: j <@> Date: Sat, 28 Sep 2024 17:23:39 +0000 Subject: [PATCH 10/10] ruff format --- qrcode/image/svg.py | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/qrcode/image/svg.py b/qrcode/image/svg.py index d213ce2b..e137bb6c 100644 --- a/qrcode/image/svg.py +++ b/qrcode/image/svg.py @@ -298,9 +298,7 @@ def add_to_splice_points(): path_flips[curr_x].append(curr_y) curr_x += 1 assert curr_x != last_x - path_dir = ( - WD.North if goal[curr_x][curr_y - 1] else WD.South - ) + path_dir = WD.North if goal[curr_x][curr_y - 1] else WD.South if do_splice or (curr_x, curr_y) != (start_x, start_y): subpath_tail.cmds += abs_or_delta("hH", curr_x, last_x) @@ -322,17 +320,12 @@ def add_to_splice_points(): path_flips[curr_x] = [] path_flips[curr_x].append(curr_y) assert curr_x != last_x - path_dir = ( - WD.South if goal[curr_x - 1][curr_y] else WD.North - ) + path_dir = WD.South if goal[curr_x - 1][curr_y] else WD.North if do_splice or (curr_x, curr_y) != (start_x, start_y): subpath_tail.cmds += abs_or_delta("hH", curr_x, last_x) # only a left turn with a hole coming up on the right is spliceable - if ( - path_dir == WD.South - and not goal[curr_x - 1][curr_y - 1] - ): + if path_dir == WD.South and not goal[curr_x - 1][curr_y - 1]: add_to_splice_points() if (curr_x, curr_y) == (start_x, start_y): @@ -346,9 +339,7 @@ def add_to_splice_points(): ): curr_y -= 1 assert curr_y != last_y - path_dir = ( - WD.West if goal[curr_x - 1][curr_y - 1] else WD.East - ) + path_dir = WD.West if goal[curr_x - 1][curr_y - 1] else WD.East if do_splice or (curr_x, curr_y) != (start_x, start_y): subpath_tail.cmds += abs_or_delta("vV", curr_y, last_y)