From 877b5fb9d46a2eb3a42f7ab7ebcf128eec972bbd Mon Sep 17 00:00:00 2001 From: Jonathan Neuhauser Date: Tue, 8 Feb 2022 17:19:36 +0100 Subject: [PATCH 1/6] improve test coverage of guide creator --- ...eset__book_right__--vert__3__--horz__2.out | 35 +++++++++++++++++++ ...ical_guides__4__--horizontal_guides__5.out | 35 +++++++++++++++++++ ...m_edges__True__--guides_preset__golden.out | 35 +++++++++++++++++++ tests/test_guides_creator.py | 23 +++++++++++- 4 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 tests/data/refs/guides_creator__--tab__margins__--start_from_edges__False__--margins_preset__book_right__--vert__3__--horz__2.out create mode 100644 tests/data/refs/guides_creator__--tab__regular_guides__--start_from_edges__True__--guides_preset__custom__--vertical_guides__4__--horizontal_guides__5.out create mode 100644 tests/data/refs/guides_creator__--tab__regular_guides__--start_from_edges__True__--guides_preset__golden.out diff --git a/tests/data/refs/guides_creator__--tab__margins__--start_from_edges__False__--margins_preset__book_right__--vert__3__--horz__2.out b/tests/data/refs/guides_creator__--tab__margins__--start_from_edges__False__--margins_preset__book_right__--vert__3__--horz__2.out new file mode 100644 index 00000000..704fd5ab --- /dev/null +++ b/tests/data/refs/guides_creator__--tab__margins__--start_from_edges__False__--margins_preset__book_right__--vert__3__--horz__2.out @@ -0,0 +1,35 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/data/refs/guides_creator__--tab__regular_guides__--start_from_edges__True__--guides_preset__custom__--vertical_guides__4__--horizontal_guides__5.out b/tests/data/refs/guides_creator__--tab__regular_guides__--start_from_edges__True__--guides_preset__custom__--vertical_guides__4__--horizontal_guides__5.out new file mode 100644 index 00000000..26a24926 --- /dev/null +++ b/tests/data/refs/guides_creator__--tab__regular_guides__--start_from_edges__True__--guides_preset__custom__--vertical_guides__4__--horizontal_guides__5.out @@ -0,0 +1,35 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/data/refs/guides_creator__--tab__regular_guides__--start_from_edges__True__--guides_preset__golden.out b/tests/data/refs/guides_creator__--tab__regular_guides__--start_from_edges__True__--guides_preset__golden.out new file mode 100644 index 00000000..40a74495 --- /dev/null +++ b/tests/data/refs/guides_creator__--tab__regular_guides__--start_from_edges__True__--guides_preset__golden.out @@ -0,0 +1,35 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/test_guides_creator.py b/tests/test_guides_creator.py index 03d5c73d..909361a2 100644 --- a/tests/test_guides_creator.py +++ b/tests/test_guides_creator.py @@ -6,6 +6,8 @@ from inkex.tester.filters import CompareNumericFuzzy class GuidesCreatorBasicTest(ComparisonMixin, InkscapeExtensionTestMixin, TestCase): + """Basic tests for GuidesCreator""" + effect_class = GuidesCreator compare_file = "svg/guides.svg" compare_filters = [ @@ -39,7 +41,26 @@ class GuidesCreatorBasicTest(ComparisonMixin, InkscapeExtensionTestMixin, TestCa class GuidesCreatorMillimeterTest(ComparisonMixin, TestCase): + """Test that guides are correctly created in a mm based document""" + effect_class = GuidesCreator compare_file = "svg/complextransform.test.svg" compare_filters = [CompareNumericFuzzy()] - comparisons = [("--vertical_guides=6", "--horizontal_guides=8")] + comparisons = [ + ("--vertical_guides=6", "--horizontal_guides=8"), + ("--tab=regular_guides", "--start_from_edges=True", "--guides_preset=golden"), + ( + "--tab=regular_guides", + "--start_from_edges=True", + "--guides_preset=custom", + "--vertical_guides=4", + "--horizontal_guides=5", + ), + ( + "--tab=margins", + "--start_from_edges=False", + "--margins_preset=book_right", + "--vert=3", + "--horz=2", + ), + ] -- GitLab From 57fe6b958412d923085dc417ca613d9dc4a32de0 Mon Sep 17 00:00:00 2001 From: Jonathan Neuhauser Date: Tue, 8 Feb 2022 12:25:51 +0100 Subject: [PATCH 2/6] refactor guides creator --- guides_creator.py | 262 ++++++++---------- ...m_edges__True__--guides_preset__golden.out | 2 +- 2 files changed, 120 insertions(+), 144 deletions(-) diff --git a/guides_creator.py b/guides_creator.py index 06b96680..df399987 100755 --- a/guides_creator.py +++ b/guides_creator.py @@ -32,6 +32,21 @@ from math import cos, sin, sqrt import inkex from inkex import Guide +from inkex.localization import inkex_gettext as _ + + +class GuidesOpts: + """Value storage for Guides Creator""" + + # pylint: disable=too-few-public-methods + def __init__(self, svg) -> None: + self.width = svg.viewbox_width + self.height = svg.viewbox_height + + # getting edges coordinates + self.h_orientation = (0, round(self.width, 4)) + self.v_orientation = (round(self.height, 4), 0) + class GuidesCreator(inkex.EffectExtension): """Create a set of guides based on the given options""" @@ -65,23 +80,30 @@ class GuidesCreator(inkex.EffectExtension): pars.add_argument( "--lr", type=inkex.Boolean, default=False, help="Lower right corner" ) - pars.add_argument("--margins_preset", default="custom", help="Margins preset") + pars.add_argument( + "--margins_preset", + default="custom", + choices=["custom", "book_left", "book_right"], + help="Margins preset", + ) pars.add_argument("--vert", type=int, default=0, help="Vert subdivisions") pars.add_argument("--horz", type=int, default=0, help="Horz subdivisions") - pars.add_argument("--header_margin", default="10", help="Header margin") - pars.add_argument("--footer_margin", default="10", help="Footer margin") - pars.add_argument("--left_margin", default="10", help="Left margin") - pars.add_argument("--right_margin", default="10", help="Right margin") + pars.add_argument( + "--header_margin", type=int, default="10", help="Header margin" + ) + pars.add_argument( + "--footer_margin", type=int, default="10", help="Footer margin" + ) + pars.add_argument("--left_margin", type=int, default="10", help="Left margin") + pars.add_argument("--right_margin", type=int, default="10", help="Right margin") pars.add_argument("--delete", type=inkex.Boolean, help="Delete existing guides") - def effect(self): - # getting the width and height attributes of the canvas - self.width = float(self.svg.viewbox_width) - self.height = float(self.svg.viewbox_height) + def __init__(self): + super().__init__() + self.opts: GuidesOpts = None - # getting edges coordinates - self.h_orientation = "0," + str(round(self.width, 4)) - self.v_orientation = str(round(self.height, 4)) + ",0" + def effect(self): + self.opts = GuidesOpts(self.svg) if self.options.delete: for guide in self.svg.namedview.get_guides(): @@ -106,28 +128,17 @@ class GuidesCreator(inkex.EffectExtension): elif preset == "golden": gold = (1 + sqrt(5)) / 2 - # horizontal golden guides - position1 = "0," + str(self.height / gold) - position2 = "0," + str(self.height - (self.height / gold)) - - self.draw_guide(position1, self.h_orientation) - self.draw_guide(position2, self.h_orientation) - - # vertical golden guides - position1 = str(self.width / gold) + ",0" - position2 = str(self.width - (self.width / gold)) + ",0" - - self.draw_guide(position1, self.v_orientation) - self.draw_guide(position2, self.v_orientation) + for fraction, index in zip([1 / gold, 1 - 1 / gold] * 2, [1, 1, 0, 0]): + position = fraction * (self.opts.width, self.opts.height)[index] + self.draw_guide( + (0, position) if index == 1 else (position, 0), + (self.opts.v_orientation, self.opts.h_orientation)[index], + ) if from_edges: - # horizontal borders - self.draw_guide("0," + str(self.height), self.h_orientation) - self.draw_guide(str(self.height) + ",0", self.h_orientation) - # vertical borders - self.draw_guide("0," + str(self.width), self.v_orientation) - self.draw_guide(str(self.width) + ",0", self.v_orientation) + self.draw_guides(1, True, vert=False) + self.draw_guides(1, True, vert=True) elif ";" in preset: v_division = int(preset.split(";")[0]) @@ -135,160 +146,125 @@ class GuidesCreator(inkex.EffectExtension): self.draw_guides(v_division, from_edges, vert=True) self.draw_guides(h_division, from_edges, vert=False) else: - raise inkex.AbortExtension("Unknown guide guide preset: {}".format(preset)) + raise inkex.AbortExtension(_("Unknown guide preset: {}").format(preset)) def generate_diagonal_guides(self): """Generate diagonal guides""" # Dimentions left, bottom = (0, 0) - right, top = (self.width, self.height) + right, top = (self.opts.width, self.opts.height) # Diagonal angle angle = 45 - if self.options.ul: - ul_corner = str(top) + "," + str(left) - from_ul_to_lr = str(cos(angle)) + "," + str(cos(angle)) - self.draw_guide(ul_corner, from_ul_to_lr) - - if self.options.ur: - ur_corner = str(right) + "," + str(top) - from_ur_to_ll = str(-sin(angle)) + "," + str(sin(angle)) - self.draw_guide(ur_corner, from_ur_to_ll) + corner_guides = { + "ul": ((top, left), (cos(angle), cos(angle))), + "ur": ((right, top), (-sin(angle), sin(angle))), + "ll": ((bottom, left), (-cos(angle), cos(angle))), + "lr": ((bottom, right), (-sin(angle), -sin(angle))), + } - if self.options.ll: - ll_corner = str(bottom) + "," + str(left) - from_ll_to_ur = str(-cos(angle)) + "," + str(cos(angle)) - self.draw_guide(ll_corner, from_ll_to_ur) - - if self.options.lr: - lr_corner = str(bottom) + "," + str(right) - from_lr_to_ul = str(-sin(angle)) + "," + str(-sin(angle)) - self.draw_guide(lr_corner, from_lr_to_ul) + for key, (position, orientation) in corner_guides.items(): + if getattr(self.options, key): + self.draw_guide(position, orientation) def generate_margins(self): """Generate margin guides""" - header_margin = int(self.options.header_margin) - footer_margin = int(self.options.footer_margin) - left_margin = int(self.options.left_margin) - right_margin = int(self.options.right_margin) - h_subdiv = int(self.options.horz) - v_subdiv = int(self.options.vert) if self.options.start_from_edges: # horizontal borders - self.draw_guide("0," + str(self.height), self.h_orientation) - self.draw_guide(str(self.height) + ",0", self.h_orientation) + self.draw_guide((0, self.opts.height), self.opts.h_orientation) + self.draw_guide((self.opts.height, 0), self.opts.h_orientation) # vertical borders - self.draw_guide("0," + str(self.width), self.v_orientation) - self.draw_guide(str(self.width) + ",0", self.v_orientation) + self.draw_guide((0, self.opts.width), self.opts.v_orientation) + self.draw_guide((self.opts.width, 0), self.opts.v_orientation) if self.options.margins_preset == "custom": - y_header = self.height - y_footer = 0 - x_left = 0 - x_right = self.width - - if header_margin != 0: - y_header = (self.height / header_margin) * (header_margin - 1) - self.draw_guide("0," + str(y_header), self.h_orientation) - - if footer_margin != 0: - y_footer = self.height / footer_margin - self.draw_guide("0," + str(y_footer), self.h_orientation) - - if left_margin != 0: - x_left = self.width / left_margin - self.draw_guide(str(x_left) + ",0", self.v_orientation) - - if right_margin != 0: - x_right = (self.width / right_margin) * (right_margin - 1) - self.draw_guide(str(x_right) + ",0", self.v_orientation) - - elif self.options.margins_preset == "book_left": - # 1/9th header - y_header = (self.height / 9) * 8 - self.draw_guide("0," + str(y_header), self.h_orientation) - - # 2/9th footer - y_footer = (self.height / 9) * 2 - self.draw_guide("0," + str(y_footer), self.h_orientation) - - # 2/9th left margin - x_left = (self.width / 9) * 2 - self.draw_guide(str(x_left) + ",0", self.v_orientation) - - # 1/9th right margin - x_right = (self.width / 9) * 8 - self.draw_guide(str(x_right) + ",0", self.v_orientation) - - elif self.options.margins_preset == "book_right": - # 1/9th header - y_header = (self.height / 9) * 8 - self.draw_guide("0," + str(y_header), self.h_orientation) - - # 2/9th footer - y_footer = (self.height / 9) * 2 - self.draw_guide("0," + str(y_footer), self.h_orientation) - - # 2/9th left margin - x_left = self.width / 9 - self.draw_guide(str(x_left) + ",0", self.v_orientation) - - # 1/9th right margin - x_right = (self.width / 9) * 7 - self.draw_guide(str(x_right) + ",0", self.v_orientation) + margins = [ + (i / j if int(j) != 0 else None) + for i, j in zip( + ( + self.opts.height * (self.options.header_margin - 1), # header + self.opts.height, # footer + self.opts.width, # left + self.opts.width * (self.options.right_margin - 1), # right + ), + ( + self.options.header_margin, + self.options.footer_margin, + self.options.left_margin, + self.options.right_margin, + ), + ) + ] + + book_options = { + "book_left": (8 / 9, 2 / 9, 2 / 9, 8 / 9), + "book_right": (8 / 9, 2 / 9, 1 / 9, 7 / 9), + } + if self.options.margins_preset in book_options: + margins = [ + i * j + for i, j in zip( + book_options[self.options.margins_preset], + 2 * [self.opts.height] + 2 * [self.opts.width], + ) + ] + + y_header, y_footer, x_left, x_right = [ + i or j for i, j in zip(margins, [self.opts.height, 0, 0, self.opts.width]) + ] + + for length, position in zip(margins, [1, 1, 0, 0]): + if length is None: + continue + self.draw_guide( + (length, 0) if position == 0 else (0, length), + (self.opts.v_orientation, self.opts.h_orientation)[position], + ) # setting up properties of the rectangle created between guides rectangle_height = y_header - y_footer rectangle_width = x_right - x_left - if h_subdiv != 0: - begin_from = y_footer - # creating horizontal guides - self._draw_guides( - (rectangle_width, rectangle_height), - h_subdiv, - edges=0, - shift=begin_from, - vert=False, - ) - - if v_subdiv != 0: - begin_from = x_left - # creating vertical guides - self._draw_guides( - (rectangle_width, rectangle_height), - v_subdiv, - edges=0, - shift=begin_from, - vert=True, - ) + for subdiv, vert, begin_from in zip( + (self.options.horz, self.options.vert), (False, True), (y_footer, x_left) + ): + if subdiv != 0: + self._draw_guides( + (rectangle_width, rectangle_height), + subdiv, + edges=0, + shift=begin_from, + vert=vert, + ) def draw_guides(self, division, edges, vert=False): """Draw a vertical or horizontal lines""" - return self._draw_guides((self.width, self.height), division, edges, vert=vert) + return self._draw_guides( + (self.opts.width, self.opts.height), division, edges, vert=vert + ) def _draw_guides(self, vector, division, edges, shift=0, vert=False): if division <= 0: return # Vert controls both ort template and vector calculation - ort = "{},0" if vert else "0,{}" + def ort(x): + return (x, 0) if vert else (0, x) + var = int(bool(edges)) for x in range(0, division - 1 + 2 * var): div = vector[not bool(vert)] / division - position = str(round(div + (x - var) * div + shift, 4)) - orientation = str(round(vector[bool(vert)], 4)) - self.draw_guide(ort.format(position), ort.format(orientation)) + position = round(div + (x - var) * div + shift, 4) + orientation = round(vector[bool(vert)], 4) + self.draw_guide(ort(position), ort(orientation)) def draw_guide(self, position, orientation): """Create a guide directly into the namedview""" if isinstance(position, tuple): x, y = position - elif isinstance(position, str): - x, y = position.split(",") self.svg.namedview.add(Guide().move_to(float(x), float(y), orientation)) diff --git a/tests/data/refs/guides_creator__--tab__regular_guides__--start_from_edges__True__--guides_preset__golden.out b/tests/data/refs/guides_creator__--tab__regular_guides__--start_from_edges__True__--guides_preset__golden.out index 40a74495..a6f8e494 100644 --- a/tests/data/refs/guides_creator__--tab__regular_guides__--start_from_edges__True__--guides_preset__golden.out +++ b/tests/data/refs/guides_creator__--tab__regular_guides__--start_from_edges__True__--guides_preset__golden.out @@ -3,7 +3,7 @@ - + -- GitLab From c75838bc10f149a118900fdbfd42c2c93cf93f91 Mon Sep 17 00:00:00 2001 From: Jonathan Neuhauser Date: Tue, 8 Feb 2022 22:34:19 +0100 Subject: [PATCH 3/6] multipage feature for guides creator --- guides_creator.inx | 9 +- guides_creator.py | 116 ++++++++++++++---- inkex/elements/_meta.py | 63 +++++++++- ...ator__781b3284b91d843346c42568aa6ce409.out | 2 +- tests/test_guides_creator.py | 25 +++- tests/test_inkex_elements.py | 76 ++++++++---- 6 files changed, 234 insertions(+), 57 deletions(-) diff --git a/guides_creator.inx b/guides_creator.inx index e7f5e8ae..37e6eb1a 100644 --- a/guides_creator.inx +++ b/guides_creator.inx @@ -2,6 +2,8 @@ Guides creator org.inkscape.effect.guides_creator + + 1- @@ -23,6 +25,8 @@ + + @@ -72,12 +76,13 @@ - 2 - 3 + 2 + 3 false false + true all diff --git a/guides_creator.py b/guides_creator.py index df399987..a38af7c1 100755 --- a/guides_creator.py +++ b/guides_creator.py @@ -28,9 +28,11 @@ This basic extension allows you to automatically draw guides in inkscape. """ from math import cos, sin, sqrt +import math +from typing import List, Tuple +import re import inkex -from inkex import Guide from inkex.localization import inkex_gettext as _ @@ -39,19 +41,48 @@ class GuidesOpts: """Value storage for Guides Creator""" # pylint: disable=too-few-public-methods - def __init__(self, svg) -> None: - self.width = svg.viewbox_width - self.height = svg.viewbox_height + def __init__(self, svg: inkex.SvgDocumentElement) -> None: + + # get page bounds + self.pages = svg.namedview.get_pages() + self.viewbox = svg.get_viewbox() + # in case the viewbox attribute is not set, fall back to viewport + self.viewbox[2:4] = svg.viewbox_width, svg.viewbox_height + self.set_page(1) + + def set_page(self, pagenumber): + """Update guide origin and width/height based on page number (1-indexed)""" + pagenumber = pagenumber - 1 + if pagenumber < len(self.pages): + self.page_origin = (self.pages[pagenumber].x, self.pages[pagenumber].y) + self.width = self.pages[pagenumber].width + self.height = self.pages[pagenumber].height + elif pagenumber == 0: # Single page document + self.page_origin = ( + self.viewbox[:2] + if not self.pages + else (self.pages[0].x, self.pages[0].y) + ) + self.width = self.viewbox[2] + self.height = self.viewbox[3] + else: + raise ValueError("Invalid page number") # getting edges coordinates - self.h_orientation = (0, round(self.width, 4)) - self.v_orientation = (round(self.height, 4), 0) + self.orientation = ((round(self.height, 4), 0), (0, round(self.width, 4))) class GuidesCreator(inkex.EffectExtension): """Create a set of guides based on the given options""" def add_arguments(self, pars): + pars.add_argument( + "--pages", + type=str, + help='On which pages the guides are created, e.g. "1, 2, 4-6, 8-". ' + "Default: All pages.", + default="1-", + ) pars.add_argument( "--tab", type=self.arg_method("generate"), @@ -83,7 +114,13 @@ class GuidesCreator(inkex.EffectExtension): pars.add_argument( "--margins_preset", default="custom", - choices=["custom", "book_left", "book_right"], + choices=[ + "custom", + "book_left", + "book_right", + "book_alternating_right", + "book_alternating_left", + ], help="Margins preset", ) pars.add_argument("--vert", type=int, default=0, help="Vert subdivisions") @@ -97,19 +134,46 @@ class GuidesCreator(inkex.EffectExtension): pars.add_argument("--left_margin", type=int, default="10", help="Left margin") pars.add_argument("--right_margin", type=int, default="10", help="Right margin") pars.add_argument("--delete", type=inkex.Boolean, help="Delete existing guides") + pars.add_argument( + "--nodup", type=inkex.Boolean, help="Omit duplicated guides", default=True + ) def __init__(self): super().__init__() self.opts: GuidesOpts = None + @staticmethod + def parse_page_descriptor(pages, lastpage): + """Parses a page descriptor. e.g: + 1,2,4-5,7,9- is parsed to 1, 2, 4, 5, 7, 9, 10, ..., lastpage""" + # replace 4-7 with 4, 5, 6, 7 + pages = re.sub( + r"(\d+)\s?-\s?(\d+)", + lambda m: ",".join(map(str, range(int(m.group(1)), int(m.group(2)) + 1))), + pages, + ) + # replace 5- with 5, 6, ..., lastpage + pages = re.sub( + r"(\d+)\s?-", + lambda m: ",".join(map(str, range(int(m.group(1)), lastpage + 1))), + pages, + ) + pages = map(int, re.findall(r"(\d+)", pages)) + pages = tuple({i for i in pages if i <= lastpage}) + return pages + def effect(self): - self.opts = GuidesOpts(self.svg) if self.options.delete: for guide in self.svg.namedview.get_guides(): guide.delete() - return self.options.tab() + self.opts = GuidesOpts(self.svg) + for i in self.parse_page_descriptor( + self.options.pages, max(len(self.svg.namedview.get_pages()), 1) + ): + self.opts.set_page(i) + self.options.tab() def generate_regular_guides(self): """Generate a regular set of guides""" @@ -132,7 +196,7 @@ class GuidesCreator(inkex.EffectExtension): position = fraction * (self.opts.width, self.opts.height)[index] self.draw_guide( (0, position) if index == 1 else (position, 0), - (self.opts.v_orientation, self.opts.h_orientation)[index], + self.opts.orientation[index], ) if from_edges: @@ -158,10 +222,10 @@ class GuidesCreator(inkex.EffectExtension): angle = 45 corner_guides = { - "ul": ((top, left), (cos(angle), cos(angle))), + "ul": ((left, top), (cos(angle), cos(angle))), "ur": ((right, top), (-sin(angle), sin(angle))), - "ll": ((bottom, left), (-cos(angle), cos(angle))), - "lr": ((bottom, right), (-sin(angle), -sin(angle))), + "ll": ((left, bottom), (-cos(angle), cos(angle))), + "lr": ((right, bottom), (-sin(angle), -sin(angle))), } for key, (position, orientation) in corner_guides.items(): @@ -173,12 +237,12 @@ class GuidesCreator(inkex.EffectExtension): if self.options.start_from_edges: # horizontal borders - self.draw_guide((0, self.opts.height), self.opts.h_orientation) - self.draw_guide((self.opts.height, 0), self.opts.h_orientation) + self.draw_guide((0, self.opts.height), self.opts.orientation[1]) + self.draw_guide((self.opts.height, 0), self.opts.orientation[1]) # vertical borders - self.draw_guide((0, self.opts.width), self.opts.v_orientation) - self.draw_guide((self.opts.width, 0), self.opts.v_orientation) + self.draw_guide((0, self.opts.width), self.opts.orientation[0]) + self.draw_guide((self.opts.width, 0), self.opts.orientation[0]) if self.options.margins_preset == "custom": margins = [ @@ -221,7 +285,7 @@ class GuidesCreator(inkex.EffectExtension): continue self.draw_guide( (length, 0) if position == 0 else (0, length), - (self.opts.v_orientation, self.opts.h_orientation)[position], + self.opts.orientation[position], ) # setting up properties of the rectangle created between guides @@ -262,10 +326,18 @@ class GuidesCreator(inkex.EffectExtension): self.draw_guide(ort(position), ort(orientation)) def draw_guide(self, position, orientation): - """Create a guide directly into the namedview""" - if isinstance(position, tuple): - x, y = position - self.svg.namedview.add(Guide().move_to(float(x), float(y), orientation)) + """Draw the guides""" + newpos = [ + position[0] + self.opts.page_origin[0], + position[1] + + self.opts.viewbox[3] + - self.opts.height + - self.opts.page_origin[1], + ] + if self.options.nodup: + self.svg.namedview.new_unique_guide(newpos, orientation) + else: + self.svg.namedview.new_guide(newpos, orientation) if __name__ == "__main__": diff --git a/inkex/elements/_meta.py b/inkex/elements/_meta.py index 93073018..c6facb57 100644 --- a/inkex/elements/_meta.py +++ b/inkex/elements/_meta.py @@ -25,12 +25,15 @@ This is useful for having a common interface for each element which can give path, transform, and property access easily. """ +from __future__ import annotations import math +from typing import Optional, Tuple + from lxml import etree from ..styles import StyleSheet -from ..transforms import Vector2d +from ..transforms import Vector2d, VectorLike, DirectedLineSegment from ._base import BaseElement @@ -102,10 +105,22 @@ class NamedView(BaseElement): elem = Guide().move_to(0, position, (0, 1)) elif orient is False: elem = Guide().move_to(position, 0, (1, 0)) + else: + elem = Guide().move_to(*position, orient) if name: elem.set("inkscape:label", str(name)) return self.add(elem) + def new_unique_guide( + self, position: VectorLike, orientation: VectorLike + ) -> Optional[Guide]: + """Add a guide iif there is no guide that looks the same.""" + elem = Guide().move_to(position[0], position[1], orientation) + for guide in self.get_guides(): + if Guide.guides_coincident(guide, elem): + return None + return self.add(elem) + def get_pages(self): """Returns a list of pages""" return self.findall("inkscape:page") @@ -123,12 +138,30 @@ class Guide(BaseElement): tag_name = "sodipodi:guide" + @property + def orientation(self) -> Vector2d: + """Vector normal to the guide""" + try: + return Vector2d(self.get("orientation")) + except ValueError: + return Vector2d(1, 0) + is_horizontal = property( - lambda self: self.get("orientation").startswith("0,") - and not self.get("orientation") == "0,0" + lambda self: self.orientation[0] == 0 and self.orientation[1] != 0 + ) + is_vertical = property( + lambda self: self.orientation[0] != 0 and self.orientation[1] == 0 ) - is_vertical = property(lambda self: self.get("orientation").endswith(",0")) - point = property(lambda self: Vector2d(self.get("position"))) + + @property + def point(self) -> Vector2d: + """Position of the guide handle. The y coordinate is flipped and relative + to the bottom of the viewbox, this is a remnant of the pre-1.0 coordinate system + """ + try: + return Vector2d(self.get("position")) + except ValueError: + return Vector2d(0, 0) @classmethod def new(cls, pos_x, pos_y, angle, **attrs): @@ -160,6 +193,26 @@ class Guide(BaseElement): self.set("orientation", angle) return self + @staticmethod + def guides_coincident(guide1, guide2): + """Check if two guides defined by (position, orientation) and (opos, oor) look + identical (i.e. the position lies on the other guide AND the guide is + (anti)parallel to the other guide).""" + # normalize orientations first + orientation = guide1.orientation / guide1.orientation.length + oor = guide2.orientation / guide2.orientation.length + + position = guide1.point + opos = guide2.point + + return ( + DirectedLineSegment( + position, position + Vector2d(orientation[1], -orientation[0]) + ).perp_distance(*opos) + < 1e-6 + and abs(abs(orientation[1] * oor[0]) - abs(orientation[0] * oor[1])) < 1e-6 + ) + class Metadata(BaseElement): """Inkscape Metadata element""" diff --git a/tests/data/refs/guides_creator__781b3284b91d843346c42568aa6ce409.out b/tests/data/refs/guides_creator__781b3284b91d843346c42568aa6ce409.out index 2c97acaa..d4155f4b 100644 --- a/tests/data/refs/guides_creator__781b3284b91d843346c42568aa6ce409.out +++ b/tests/data/refs/guides_creator__781b3284b91d843346c42568aa6ce409.out @@ -18,7 +18,7 @@ - + diff --git a/tests/test_guides_creator.py b/tests/test_guides_creator.py index 909361a2..703f89ff 100644 --- a/tests/test_guides_creator.py +++ b/tests/test_guides_creator.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # coding=utf-8 -from guides_creator import GuidesCreator +from guides_creator import GuidesCreator, GuidesOpts from inkex.tester import ComparisonMixin, InkscapeExtensionTestMixin, TestCase from inkex.tester.filters import CompareNumericFuzzy @@ -64,3 +64,26 @@ class GuidesCreatorMillimeterTest(ComparisonMixin, TestCase): "--horz=2", ), ] + + +class GuidesCreatorFunctionalityTests(TestCase): + """Test some methods on their own""" + + def test_page_descriptor(self): + """Test that page descriptions are parsed correctly""" + + def parsetest(string, length, comparison): + self.assertTupleEqual( + GuidesCreator.parse_page_descriptor(string, length), comparison + ) + + parsetest("1, 2,3", 10, (1, 2, 3)) + parsetest("1-3, 5, 10", 10, (1, 2, 3, 5, 10)) + parsetest("2, 4-, 6, 7-", 10, (2, 4, 5, 6, 7, 8, 9, 10)) + parsetest("2-5, 7-9, 10-", 12, (2, 3, 4, 5, 7, 8, 9, 10, 11, 12)) + parsetest("6, 7, 8", 3, tuple()) + parsetest("2, 216-218, 10", 300, (2, 10, 216, 217, 218)) + + +class GuidesTestMulitpage(TestCase): + pass diff --git a/tests/test_inkex_elements.py b/tests/test_inkex_elements.py index b1bd0eaf..59458135 100644 --- a/tests/test_inkex_elements.py +++ b/tests/test_inkex_elements.py @@ -118,7 +118,7 @@ class PathElementTestCase(ElementTestCase): 5, math.pi / 4, math.pi * 6 / 4, - """m 13.535534,23.535534 a 5,5 0 0 1 -6.035534,0.794593 + """m 13.535534,23.535534 a 5,5 0 0 1 -6.035534,0.794593 5,5 0 0 1 -2.3296291,-5.624222 5,5 0 0 1 4.8296291,-3.705905""", ) compare_arc( @@ -128,7 +128,7 @@ class PathElementTestCase(ElementTestCase): 5, math.pi / 4, math.pi * 6 / 4, - """m 13.535534,23.535534 a 5,5 0 0 1 -6.035534,0.794593 + """m 13.535534,23.535534 a 5,5 0 0 1 -6.035534,0.794593 5,5 0 0 1 -2.3296291,-5.624222 5,5 0 0 1 4.8296291,-3.705905 z""", type="chord", ) @@ -139,7 +139,7 @@ class PathElementTestCase(ElementTestCase): 5, math.pi / 4, math.pi * 28 / 18, - """m 13.535534,23.535534 a 5,5 0 0 1 -6.2830789,0.641905 5,5 0 0 1 -1.8991927,-6.02347 + """m 13.535534,23.535534 a 5,5 0 0 1 -6.2830789,0.641905 5,5 0 0 1 -1.8991927,-6.02347 5,5 0 0 1 5.5149786,-3.078008 l -0.868241,4.924039 z""", type="slice", ) @@ -175,7 +175,7 @@ class PathElementTestCase(ElementTestCase): 42, True, 0, - """m 4.9186625,7.1047587 0.7178942,-5.4529467 5.0813373,-2.10475872 + """m 4.9186625,7.1047587 0.7178942,-5.4529467 5.0813373,-2.10475872 4.363443,3.34818802 -0.717894,5.4529467 -5.0813372,2.104759 z""", ) # Test a star @@ -189,10 +189,10 @@ class PathElementTestCase(ElementTestCase): 1.4790556, False, 0, - """m 25.203254,38.580212 -18.6458484,-11.651701 -11.3057927,16.6865 - -2.5158293,-21.842628 -20.0950776,1.564637 15.508661,-15.5856099 - -13.752359,-14.7354284 21.8548124,2.4076898 2.94616632,-19.9394165 - 11.74384528,18.5879506 17.426168,-10.1286176 -7.210477,20.7711057 + """m 25.203254,38.580212 -18.6458484,-11.651701 -11.3057927,16.6865 + -2.5158293,-21.842628 -20.0950776,1.564637 15.508661,-15.5856099 + -13.752359,-14.7354284 21.8548124,2.4076898 2.94616632,-19.9394165 + 11.74384528,18.5879506 17.426168,-10.1286176 -7.210477,20.7711057 18.783909,7.3092373 -20.735163,7.313194 z""", ) # Test a rounded polygon @@ -206,11 +206,11 @@ class PathElementTestCase(ElementTestCase): 42, True, 0.1, - """m 4.9186625,7.1047587 c -0.2104759,-0.5081337 0.3830754,-5.0166024 0.7178942,-5.4529467 - 0.3348188,-0.4363443 4.5360433,-2.17654814 5.0813373,-2.10475872 - 0.545295,0.0717894 4.152968,2.84005422 4.363443,3.34818802 - 0.210476,0.5081337 -0.383075,5.0166024 -0.717894,5.4529467 - -0.334819,0.4363443 -4.5360425,2.176548 -5.0813372,2.104759 + """m 4.9186625,7.1047587 c -0.2104759,-0.5081337 0.3830754,-5.0166024 0.7178942,-5.4529467 + 0.3348188,-0.4363443 4.5360433,-2.17654814 5.0813373,-2.10475872 + 0.545295,0.0717894 4.152968,2.84005422 4.363443,3.34818802 + 0.210476,0.5081337 -0.383075,5.0166024 -0.717894,5.4529467 + -0.334819,0.4363443 -4.5360425,2.176548 -5.0813372,2.104759 -0.5452947,-0.07179 -4.1529674,-2.8400545 -4.3634433,-3.3481883 z""", ) compare_star( @@ -223,19 +223,19 @@ class PathElementTestCase(ElementTestCase): 1.4790556, False, 1, - """m 25.203254,38.580212 c -18.8526653,11.31401 3.036933,-15.296807 -18.6458484,-11.651701 - -19.8769816,3.341532 7.5786704,23.731873 -11.3057927,16.6865 - -20.6000929,-7.685438 13.8530224,-7.163034 -2.5158293,-21.842628 - -15.0056106,-13.4570388 -13.8291026,20.721824 -20.0950776,1.564637 - -6.835231,-20.8975929 14.237504,6.364651 15.508661,-15.5856099 - 1.165291,-20.1221851 -24.823279,2.1078182 -13.752359,-14.7354284 - 12.076699,-18.3734357 3.900854,15.0996232 21.8548124,2.4076898 - 16.4587046,-11.6349155 -17.1250194,-18.0934175 2.94616632,-19.9394165 - 21.89462928,-2.013706 -9.37321772,12.464272 11.74384528,18.5879506 - 19.358377,5.61368249 3.468728,-24.6699406 17.426168,-10.1286176 - 15.225456,15.86238583 -15.589066,0.44307 -7.210477,20.7711057 - 7.680797,18.6350633 21.450453,-12.6694955 18.783909,7.3092373 - -2.908795,21.793777 -10.066029,-11.91177319 -20.735163,7.313194 + """m 25.203254,38.580212 c -18.8526653,11.31401 3.036933,-15.296807 -18.6458484,-11.651701 + -19.8769816,3.341532 7.5786704,23.731873 -11.3057927,16.6865 + -20.6000929,-7.685438 13.8530224,-7.163034 -2.5158293,-21.842628 + -15.0056106,-13.4570388 -13.8291026,20.721824 -20.0950776,1.564637 + -6.835231,-20.8975929 14.237504,6.364651 15.508661,-15.5856099 + 1.165291,-20.1221851 -24.823279,2.1078182 -13.752359,-14.7354284 + 12.076699,-18.3734357 3.900854,15.0996232 21.8548124,2.4076898 + 16.4587046,-11.6349155 -17.1250194,-18.0934175 2.94616632,-19.9394165 + 21.89462928,-2.013706 -9.37321772,12.464272 11.74384528,18.5879506 + 19.358377,5.61368249 3.468728,-24.6699406 17.426168,-10.1286176 + 15.225456,15.86238583 -15.589066,0.44307 -7.210477,20.7711057 + 7.680797,18.6350633 21.450453,-12.6694955 18.783909,7.3092373 + -2.908795,21.793777 -10.066029,-11.91177319 -20.735163,7.313194 -9.7805797,17.623861 23.27955,8.871339 5.996985,19.243087 z""", precision=2, ) @@ -487,6 +487,30 @@ class NamedViewTest(ElementTestCase): self.svg.namedview.add(Guide().move_to(0, 0, "90")) self.assertEqual(len(self.svg.namedview.get_guides()), 2) + def test_guides_coincident(self): + """Test the detection of coincident guides""" + + def coincidence_test(data, result): + guide1 = Guide().move_to(*data[0], angle=data[1]) + guide2 = Guide().move_to(*data[2], angle=data[3]) + self.assertEqual(Guide.guides_coincident(guide1, guide2), result) + + # both are good + coincidence_test(((0, 0), (0, 1), (2, 0), (0, 3)), True) + # antiparallel + coincidence_test(((0, 0), (0, 1), (2, 0), (0, -3)), True) + # point not on guide + coincidence_test(((0, 0), (0, 1), (0, 1), (0, 3)), False) + # different orientation + coincidence_test(((0, 0), (0, 1), (2, 0), (1, 0)), False) + # try the same at an angle + coincidence_test(((1, 1), (2, -1), (3, 5), (-2, 1)), True) + coincidence_test(((1, 1), (2, -1), (3, 6), (-2, 1)), False) + # and vertical + coincidence_test(((0, 1), (1, 0), (0, 2), (3, 0)), True) + coincidence_test(((0, 1), (1, 0), (0, 2), (-3, 0)), True) + coincidence_test(((1, 1), (1, 0), (0, 2), (-3, 0)), False) + def test_pages(self): """Create some extra pages and see a list of them""" self.assertEqual(len(self.svg.namedview.get_pages()), 0) -- GitLab From 5fb64cc809174efb60895301cd98d224089b50ca Mon Sep 17 00:00:00 2001 From: Jonathan Neuhauser Date: Wed, 9 Feb 2022 00:24:57 +0100 Subject: [PATCH 4/6] add unit tests for guides creator --- guides_creator.py | 31 ++++--- tests/data/refs/guides_creator.out | 14 +++ ...ue__--ur__True__--ll__True__--lr__True.out | 14 +++ ...ue__--ur__True__--ll__True__--lr__True.out | 14 +++ ...margins_preset__book_alternating_right.out | 14 +++ ...-margins_preset__book_alternating_left.out | 14 +++ ...ontal_guides__3__--pages__1____3-7__12.out | 14 +++ ...tor__3b35068c24255b2144616d98c2a077bd.out} | 0 tests/data/svg/empty_multipage.svg | 90 +++++++++++++++++++ tests/test_guides_creator.py | 47 +++++++++- 10 files changed, 234 insertions(+), 18 deletions(-) create mode 100644 tests/data/refs/guides_creator.out create mode 100644 tests/data/refs/guides_creator__--tab__diagonal_guides__--nodup__False__--pages__1-3__--ul__True__--ur__True__--ll__True__--lr__True.out create mode 100644 tests/data/refs/guides_creator__--tab__diagonal_guides__--nodup__True__--pages__1-3__--ul__True__--ur__True__--ll__True__--lr__True.out create mode 100644 tests/data/refs/guides_creator__--tab__margins__--start_from_edges__False__--margins_preset__book_alternating_right.out create mode 100644 tests/data/refs/guides_creator__--tab__margins__--start_from_edges__True__--margins_preset__book_alternating_left.out create mode 100644 tests/data/refs/guides_creator__--vertical_guides__4__--horizontal_guides__3__--pages__1____3-7__12.out rename tests/data/refs/{guides_creator__781b3284b91d843346c42568aa6ce409.out => guides_creator__3b35068c24255b2144616d98c2a077bd.out} (100%) create mode 100644 tests/data/svg/empty_multipage.svg diff --git a/guides_creator.py b/guides_creator.py index a38af7c1..2e76e701 100755 --- a/guides_creator.py +++ b/guides_creator.py @@ -28,8 +28,6 @@ This basic extension allows you to automatically draw guides in inkscape. """ from math import cos, sin, sqrt -import math -from typing import List, Tuple import re import inkex @@ -52,6 +50,7 @@ class GuidesOpts: def set_page(self, pagenumber): """Update guide origin and width/height based on page number (1-indexed)""" + self.pagenumber = pagenumber pagenumber = pagenumber - 1 if pagenumber < len(self.pages): self.page_origin = (self.pages[pagenumber].x, self.pages[pagenumber].y) @@ -125,14 +124,10 @@ class GuidesCreator(inkex.EffectExtension): ) pars.add_argument("--vert", type=int, default=0, help="Vert subdivisions") pars.add_argument("--horz", type=int, default=0, help="Horz subdivisions") - pars.add_argument( - "--header_margin", type=int, default="10", help="Header margin" - ) - pars.add_argument( - "--footer_margin", type=int, default="10", help="Footer margin" - ) - pars.add_argument("--left_margin", type=int, default="10", help="Left margin") - pars.add_argument("--right_margin", type=int, default="10", help="Right margin") + pars.add_argument("--header_margin", type=int, default=10, help="Header margin") + pars.add_argument("--footer_margin", type=int, default=10, help="Footer margin") + pars.add_argument("--left_margin", type=int, default=10, help="Left margin") + pars.add_argument("--right_margin", type=int, default=10, help="Right margin") pars.add_argument("--delete", type=inkex.Boolean, help="Delete existing guides") pars.add_argument( "--nodup", type=inkex.Boolean, help="Omit duplicated guides", default=True @@ -238,10 +233,10 @@ class GuidesCreator(inkex.EffectExtension): if self.options.start_from_edges: # horizontal borders self.draw_guide((0, self.opts.height), self.opts.orientation[1]) - self.draw_guide((self.opts.height, 0), self.opts.orientation[1]) + self.draw_guide((self.opts.width, 0), self.opts.orientation[1]) # vertical borders - self.draw_guide((0, self.opts.width), self.opts.orientation[0]) + self.draw_guide((0, self.opts.height), self.opts.orientation[0]) self.draw_guide((self.opts.width, 0), self.opts.orientation[0]) if self.options.margins_preset == "custom": @@ -267,11 +262,19 @@ class GuidesCreator(inkex.EffectExtension): "book_left": (8 / 9, 2 / 9, 2 / 9, 8 / 9), "book_right": (8 / 9, 2 / 9, 1 / 9, 7 / 9), } - if self.options.margins_preset in book_options: + margins_preset = self.options.margins_preset + if margins_preset.startswith("book_alternating"): + margins_preset = ( + "book_left" + if self.opts.pagenumber % 2 == (1 if "left" in margins_preset else 0) + else "book_right" + ) + + if margins_preset in book_options: margins = [ i * j for i, j in zip( - book_options[self.options.margins_preset], + book_options[margins_preset], 2 * [self.opts.height] + 2 * [self.opts.width], ) ] diff --git a/tests/data/refs/guides_creator.out b/tests/data/refs/guides_creator.out new file mode 100644 index 00000000..6749fa04 --- /dev/null +++ b/tests/data/refs/guides_creator.out @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/data/refs/guides_creator__--tab__diagonal_guides__--nodup__False__--pages__1-3__--ul__True__--ur__True__--ll__True__--lr__True.out b/tests/data/refs/guides_creator__--tab__diagonal_guides__--nodup__False__--pages__1-3__--ul__True__--ur__True__--ll__True__--lr__True.out new file mode 100644 index 00000000..e166c8a1 --- /dev/null +++ b/tests/data/refs/guides_creator__--tab__diagonal_guides__--nodup__False__--pages__1-3__--ul__True__--ur__True__--ll__True__--lr__True.out @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/data/refs/guides_creator__--tab__diagonal_guides__--nodup__True__--pages__1-3__--ul__True__--ur__True__--ll__True__--lr__True.out b/tests/data/refs/guides_creator__--tab__diagonal_guides__--nodup__True__--pages__1-3__--ul__True__--ur__True__--ll__True__--lr__True.out new file mode 100644 index 00000000..4c756744 --- /dev/null +++ b/tests/data/refs/guides_creator__--tab__diagonal_guides__--nodup__True__--pages__1-3__--ul__True__--ur__True__--ll__True__--lr__True.out @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/data/refs/guides_creator__--tab__margins__--start_from_edges__False__--margins_preset__book_alternating_right.out b/tests/data/refs/guides_creator__--tab__margins__--start_from_edges__False__--margins_preset__book_alternating_right.out new file mode 100644 index 00000000..baf03de0 --- /dev/null +++ b/tests/data/refs/guides_creator__--tab__margins__--start_from_edges__False__--margins_preset__book_alternating_right.out @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/data/refs/guides_creator__--tab__margins__--start_from_edges__True__--margins_preset__book_alternating_left.out b/tests/data/refs/guides_creator__--tab__margins__--start_from_edges__True__--margins_preset__book_alternating_left.out new file mode 100644 index 00000000..5e468483 --- /dev/null +++ b/tests/data/refs/guides_creator__--tab__margins__--start_from_edges__True__--margins_preset__book_alternating_left.out @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/data/refs/guides_creator__--vertical_guides__4__--horizontal_guides__3__--pages__1____3-7__12.out b/tests/data/refs/guides_creator__--vertical_guides__4__--horizontal_guides__3__--pages__1____3-7__12.out new file mode 100644 index 00000000..cf505659 --- /dev/null +++ b/tests/data/refs/guides_creator__--vertical_guides__4__--horizontal_guides__3__--pages__1____3-7__12.out @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/data/refs/guides_creator__781b3284b91d843346c42568aa6ce409.out b/tests/data/refs/guides_creator__3b35068c24255b2144616d98c2a077bd.out similarity index 100% rename from tests/data/refs/guides_creator__781b3284b91d843346c42568aa6ce409.out rename to tests/data/refs/guides_creator__3b35068c24255b2144616d98c2a077bd.out diff --git a/tests/data/svg/empty_multipage.svg b/tests/data/svg/empty_multipage.svg new file mode 100644 index 00000000..7758938f --- /dev/null +++ b/tests/data/svg/empty_multipage.svg @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + diff --git a/tests/test_guides_creator.py b/tests/test_guides_creator.py index 703f89ff..8a4e6b3d 100644 --- a/tests/test_guides_creator.py +++ b/tests/test_guides_creator.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # coding=utf-8 -from guides_creator import GuidesCreator, GuidesOpts +from guides_creator import GuidesCreator from inkex.tester import ComparisonMixin, InkscapeExtensionTestMixin, TestCase from inkex.tester.filters import CompareNumericFuzzy @@ -30,7 +30,7 @@ class GuidesCreatorBasicTest(ComparisonMixin, InkscapeExtensionTestMixin, TestCa + ("--tab=regular_guides", "--guides_preset=golden", "--delete=True"), old_defaults + ("--tab=regular_guides", "--guides_preset=5;5", "--start_from_edges=True"), - old_defaults + ("--tab=diagonal_guides",), + old_defaults + ("--tab=diagonal_guides", "--nodup=False"), old_defaults + ("--tab=margins", "--start_from_edges=True", "--margins_preset=custom"), old_defaults @@ -85,5 +85,44 @@ class GuidesCreatorFunctionalityTests(TestCase): parsetest("2, 216-218, 10", 300, (2, 10, 216, 217, 218)) -class GuidesTestMulitpage(TestCase): - pass +class GuidesTestMulitpage(ComparisonMixin, TestCase): + """Test multipage functionality""" + + effect_class = GuidesCreator + compare_file = "svg/empty_multipage.svg" + compare_filters = [CompareNumericFuzzy()] + comparisons = [ + (), # by default, all pages + # selection of pages + ("--vertical_guides=4", "--horizontal_guides=3", "--pages=1,,3-7,12"), + # diagonal guides + ( + "--tab=diagonal_guides", + "--nodup=False", + "--pages=1-3", + "--ul=True", + "--ur=True", + "--ll=True", + "--lr=True", + ), + # There is one diagonal guide already in the file, it should be unchanged + ( + "--tab=diagonal_guides", + "--nodup=True", + "--pages=1-3", + "--ul=True", + "--ur=True", + "--ll=True", + "--lr=True", + ), + ( + "--tab=margins", + "--start_from_edges=True", + "--margins_preset=book_alternating_left", + ), + ( + "--tab=margins", + "--start_from_edges=False", + "--margins_preset=book_alternating_right", + ), + ] -- GitLab From e728288e1d54be0732b003acb9fee1859eea0784 Mon Sep 17 00:00:00 2001 From: Jonathan Neuhauser Date: Wed, 9 Feb 2022 10:49:17 +0100 Subject: [PATCH 5/6] move some of the new utilites in guides_creator into inkex --- guides_creator.py | 74 ++++++++++++---------------------- inkex/base.py | 40 ++++++++++++++++++ inkex/elements/_meta.py | 22 +++++----- inkex/transforms.py | 27 ++++++++----- tests/test_guides_creator.py | 19 --------- tests/test_inkex_base.py | 18 +++++++++ tests/test_inkex_transforms.py | 27 +++++++++++++ 7 files changed, 138 insertions(+), 89 deletions(-) diff --git a/guides_creator.py b/guides_creator.py index 2e76e701..d67c35a1 100755 --- a/guides_creator.py +++ b/guides_creator.py @@ -36,7 +36,7 @@ from inkex.localization import inkex_gettext as _ class GuidesOpts: - """Value storage for Guides Creator""" + """Manager of current-page-related values for GuidesCreator""" # pylint: disable=too-few-public-methods def __init__(self, svg: inkex.SvgDocumentElement) -> None: @@ -77,7 +77,7 @@ class GuidesCreator(inkex.EffectExtension): def add_arguments(self, pars): pars.add_argument( "--pages", - type=str, + type=self.arg_number_ranges(), help='On which pages the guides are created, e.g. "1, 2, 4-6, 8-". ' "Default: All pages.", default="1-", @@ -135,27 +135,7 @@ class GuidesCreator(inkex.EffectExtension): def __init__(self): super().__init__() - self.opts: GuidesOpts = None - - @staticmethod - def parse_page_descriptor(pages, lastpage): - """Parses a page descriptor. e.g: - 1,2,4-5,7,9- is parsed to 1, 2, 4, 5, 7, 9, 10, ..., lastpage""" - # replace 4-7 with 4, 5, 6, 7 - pages = re.sub( - r"(\d+)\s?-\s?(\d+)", - lambda m: ",".join(map(str, range(int(m.group(1)), int(m.group(2)) + 1))), - pages, - ) - # replace 5- with 5, 6, ..., lastpage - pages = re.sub( - r"(\d+)\s?-", - lambda m: ",".join(map(str, range(int(m.group(1)), lastpage + 1))), - pages, - ) - pages = map(int, re.findall(r"(\d+)", pages)) - pages = tuple({i for i in pages if i <= lastpage}) - return pages + self.store: GuidesOpts = None def effect(self): @@ -163,11 +143,9 @@ class GuidesCreator(inkex.EffectExtension): for guide in self.svg.namedview.get_guides(): guide.delete() - self.opts = GuidesOpts(self.svg) - for i in self.parse_page_descriptor( - self.options.pages, max(len(self.svg.namedview.get_pages()), 1) - ): - self.opts.set_page(i) + self.store = GuidesOpts(self.svg) + for i in self.options.pages(max(len(self.svg.namedview.get_pages()), 1)): + self.store.set_page(i) self.options.tab() def generate_regular_guides(self): @@ -188,10 +166,10 @@ class GuidesCreator(inkex.EffectExtension): gold = (1 + sqrt(5)) / 2 for fraction, index in zip([1 / gold, 1 - 1 / gold] * 2, [1, 1, 0, 0]): - position = fraction * (self.opts.width, self.opts.height)[index] + position = fraction * (self.store.width, self.store.height)[index] self.draw_guide( (0, position) if index == 1 else (position, 0), - self.opts.orientation[index], + self.store.orientation[index], ) if from_edges: @@ -211,7 +189,7 @@ class GuidesCreator(inkex.EffectExtension): """Generate diagonal guides""" # Dimentions left, bottom = (0, 0) - right, top = (self.opts.width, self.opts.height) + right, top = (self.store.width, self.store.height) # Diagonal angle angle = 45 @@ -232,22 +210,22 @@ class GuidesCreator(inkex.EffectExtension): if self.options.start_from_edges: # horizontal borders - self.draw_guide((0, self.opts.height), self.opts.orientation[1]) - self.draw_guide((self.opts.width, 0), self.opts.orientation[1]) + self.draw_guide((0, self.store.height), self.store.orientation[1]) + self.draw_guide((self.store.width, 0), self.store.orientation[1]) # vertical borders - self.draw_guide((0, self.opts.height), self.opts.orientation[0]) - self.draw_guide((self.opts.width, 0), self.opts.orientation[0]) + self.draw_guide((0, self.store.height), self.store.orientation[0]) + self.draw_guide((self.store.width, 0), self.store.orientation[0]) if self.options.margins_preset == "custom": margins = [ (i / j if int(j) != 0 else None) for i, j in zip( ( - self.opts.height * (self.options.header_margin - 1), # header - self.opts.height, # footer - self.opts.width, # left - self.opts.width * (self.options.right_margin - 1), # right + self.store.height * (self.options.header_margin - 1), # header + self.store.height, # footer + self.store.width, # left + self.store.width * (self.options.right_margin - 1), # right ), ( self.options.header_margin, @@ -266,7 +244,7 @@ class GuidesCreator(inkex.EffectExtension): if margins_preset.startswith("book_alternating"): margins_preset = ( "book_left" - if self.opts.pagenumber % 2 == (1 if "left" in margins_preset else 0) + if self.store.pagenumber % 2 == (1 if "left" in margins_preset else 0) else "book_right" ) @@ -275,12 +253,12 @@ class GuidesCreator(inkex.EffectExtension): i * j for i, j in zip( book_options[margins_preset], - 2 * [self.opts.height] + 2 * [self.opts.width], + 2 * [self.store.height] + 2 * [self.store.width], ) ] y_header, y_footer, x_left, x_right = [ - i or j for i, j in zip(margins, [self.opts.height, 0, 0, self.opts.width]) + i or j for i, j in zip(margins, [self.store.height, 0, 0, self.store.width]) ] for length, position in zip(margins, [1, 1, 0, 0]): @@ -288,7 +266,7 @@ class GuidesCreator(inkex.EffectExtension): continue self.draw_guide( (length, 0) if position == 0 else (0, length), - self.opts.orientation[position], + self.store.orientation[position], ) # setting up properties of the rectangle created between guides @@ -310,7 +288,7 @@ class GuidesCreator(inkex.EffectExtension): def draw_guides(self, division, edges, vert=False): """Draw a vertical or horizontal lines""" return self._draw_guides( - (self.opts.width, self.opts.height), division, edges, vert=vert + (self.store.width, self.store.height), division, edges, vert=vert ) def _draw_guides(self, vector, division, edges, shift=0, vert=False): @@ -331,11 +309,11 @@ class GuidesCreator(inkex.EffectExtension): def draw_guide(self, position, orientation): """Draw the guides""" newpos = [ - position[0] + self.opts.page_origin[0], + position[0] + self.store.page_origin[0], position[1] - + self.opts.viewbox[3] - - self.opts.height - - self.opts.page_origin[1], + + self.store.viewbox[3] + - self.store.height + - self.store.page_origin[1], ] if self.options.nodup: self.svg.namedview.new_unique_guide(newpos, orientation) diff --git a/inkex/base.py b/inkex/base.py index 0da53396..5896eda4 100644 --- a/inkex/base.py +++ b/inkex/base.py @@ -21,6 +21,7 @@ The ultimate base functionality for every Inkscape extension. """ import os +import re import sys import copy @@ -126,6 +127,45 @@ class InkscapeExtension: return _inner + @staticmethod + def arg_number_ranges(): + + """Parses a number descriptor. e.g: + 1,2,4-5,7,9- is parsed to 1, 2, 4, 5, 7, 9, 10, ..., lastvalue + + .. code-block:: python + .. # in add_arguments() + .. pars.add_argument("--pages", type=self.arg_number_ranges(), default=1-) + .. # later on, pages is then a list of ints + .. pages = self.options.pages(lastvalue) + + """ + + def _inner(value): + # replace 4-7 with 4, 5, 6, 7 + pages = re.sub( + r"(\d+)\s?-\s?(\d+)", + lambda m: ",".join( + map(str, range(int(m.group(1)), int(m.group(2)) + 1)) + ), + value, + ) + + def method(lastvalue, pages): + # replace 5- with 5, 6, ..., lastpage + pages = re.sub( + r"(\d+)\s?-", + lambda m: ",".join(map(str, range(int(m.group(1)), lastvalue + 1))), + pages, + ) + pages = map(int, re.findall(r"(\d+)", pages)) + pages = tuple({i for i in pages if i <= lastvalue}) + return pages + + return lambda lastvalue: method(lastvalue, pages) + + return _inner + @staticmethod def arg_class(options: List[Type]) -> Callable[[str], Any]: """Used by add_argument to match an option with a class diff --git a/inkex/elements/_meta.py b/inkex/elements/_meta.py index c6facb57..9047ed37 100644 --- a/inkex/elements/_meta.py +++ b/inkex/elements/_meta.py @@ -116,10 +116,16 @@ class NamedView(BaseElement): ) -> Optional[Guide]: """Add a guide iif there is no guide that looks the same.""" elem = Guide().move_to(position[0], position[1], orientation) + return self.add(elem) if self.get_similar_guide(elem) is None else None + + def get_similar_guide(self, other: Guide) -> Optional[Guide]: + """Check if the namedview contains a guide that looks identical to one + defined by (position, orientation). If such a guide exists, return it; + otherwise, return None.""" for guide in self.get_guides(): - if Guide.guides_coincident(guide, elem): - return None - return self.add(elem) + if Guide.guides_coincident(guide, other): + return guide + return None def get_pages(self): """Returns a list of pages""" @@ -141,10 +147,7 @@ class Guide(BaseElement): @property def orientation(self) -> Vector2d: """Vector normal to the guide""" - try: - return Vector2d(self.get("orientation")) - except ValueError: - return Vector2d(1, 0) + return Vector2d(self.get("orientation"), fallback=(1, 0)) is_horizontal = property( lambda self: self.orientation[0] == 0 and self.orientation[1] != 0 @@ -158,10 +161,7 @@ class Guide(BaseElement): """Position of the guide handle. The y coordinate is flipped and relative to the bottom of the viewbox, this is a remnant of the pre-1.0 coordinate system """ - try: - return Vector2d(self.get("position")) - except ValueError: - return Vector2d(0, 0) + return Vector2d(self.get("position"), fallback=(0, 0)) @classmethod def new(cls, pos_x, pos_y, angle, **attrs): diff --git a/inkex/transforms.py b/inkex/transforms.py index 9427010d..86f25b44 100644 --- a/inkex/transforms.py +++ b/inkex/transforms.py @@ -90,8 +90,8 @@ class ImmutableVector2d: pass @overload - def __init__(self, v): - # type: (Union[VectorLike, str]) -> None + def __init__(self, v, fallback=None): + # type: (Union[VectorLike, str], Optional[Union[VectorLike, str]]) -> None pass @overload @@ -99,15 +99,20 @@ class ImmutableVector2d: # type: (float, float) -> None pass - def __init__(self, *args): - if len(args) == 0: - x, y = 0.0, 0.0 - elif len(args) == 1: - x, y = self._parse(args[0]) - elif len(args) == 2: - x, y = map(float, args) - else: - raise ValueError("too many arguments") + def __init__(self, *args, fallback=None): + try: + if len(args) == 0: + x, y = 0.0, 0.0 + elif len(args) == 1: + x, y = self._parse(args[0]) + elif len(args) == 2: + x, y = map(float, args) + else: + raise ValueError("too many arguments") + except (ValueError, TypeError) as error: + if fallback is None: + raise ValueError("Cannot parse vector and no fallback given") from error + x, y = ImmutableVector2d(fallback) self._x, self._y = float(x), float(y) @staticmethod diff --git a/tests/test_guides_creator.py b/tests/test_guides_creator.py index 8a4e6b3d..4bb66b1b 100644 --- a/tests/test_guides_creator.py +++ b/tests/test_guides_creator.py @@ -66,25 +66,6 @@ class GuidesCreatorMillimeterTest(ComparisonMixin, TestCase): ] -class GuidesCreatorFunctionalityTests(TestCase): - """Test some methods on their own""" - - def test_page_descriptor(self): - """Test that page descriptions are parsed correctly""" - - def parsetest(string, length, comparison): - self.assertTupleEqual( - GuidesCreator.parse_page_descriptor(string, length), comparison - ) - - parsetest("1, 2,3", 10, (1, 2, 3)) - parsetest("1-3, 5, 10", 10, (1, 2, 3, 5, 10)) - parsetest("2, 4-, 6, 7-", 10, (2, 4, 5, 6, 7, 8, 9, 10)) - parsetest("2-5, 7-9, 10-", 12, (2, 3, 4, 5, 7, 8, 9, 10, 11, 12)) - parsetest("6, 7, 8", 3, tuple()) - parsetest("2, 216-218, 10", 300, (2, 10, 216, 217, 218)) - - class GuidesTestMulitpage(ComparisonMixin, TestCase): """Test multipage functionality""" diff --git a/tests/test_inkex_base.py b/tests/test_inkex_base.py index bff7c5df..8bc19190 100644 --- a/tests/test_inkex_base.py +++ b/tests/test_inkex_base.py @@ -126,6 +126,24 @@ class InkscapeExtensionTest(TestCase): self.assertRaises(AbortExtension, ext.absolute_href, "./foo", default=None) +class TestArgumentDatatypes(TestCase): + """Test special argument types for the dataparser""" + + def test_page_descriptor(self): + """Test that page descriptions are parsed correctly""" + + def parsetest(string, length, comparison): + result = InkscapeExtension.arg_number_ranges() + self.assertTupleEqual(result(string)(length), comparison) + + parsetest("1, 2,3", 10, (1, 2, 3)) + parsetest("1-3, 5, 10", 10, (1, 2, 3, 5, 10)) + parsetest("2, 4-, 6, 7-", 10, (2, 4, 5, 6, 7, 8, 9, 10)) + parsetest("2-5, 7-9, 10-", 12, (2, 3, 4, 5, 7, 8, 9, 10, 11, 12)) + parsetest("6, 7, 8", 3, tuple()) + parsetest("2, 216-218, 10", 300, (2, 10, 216, 217, 218)) + + class SvgInputOutputTest(TestCase): """Test SVG Input Mixin""" diff --git a/tests/test_inkex_transforms.py b/tests/test_inkex_transforms.py index fa3dd368..6068631a 100644 --- a/tests/test_inkex_transforms.py +++ b/tests/test_inkex_transforms.py @@ -148,6 +148,33 @@ class Vector2dTest(TestCase): self.assertRaises(ValueError, Vector2d, (1)) self.assertRaises(ValueError, Vector2d, (1, 2, 3)) + self.assertRaises(ValueError, Vector2d, 1, 2, 3) + + def test_vector_default_creation(self): + """Test fallback for vectors""" + + # no fallback + vec0 = Vector2d("1,2", fallback=None) + self.assertEqual(vec0.x, 1) + self.assertEqual(vec0.y, 2) + + self.assertRaises(ValueError, Vector2d, "a,2") + self.assertRaises(ValueError, Vector2d, 1) + # invalid fallback + self.assertRaises(ValueError, Vector2d, 1, fallback="a") + + # fallback + vec0 = Vector2d("a,3", fallback=(1, 2)) + self.assertEqual(vec0.x, 1) + self.assertEqual(vec0.y, 2) + + vec0 = Vector2d(("a", "b"), fallback=(1, 2)) + self.assertEqual(vec0.x, 1) + self.assertEqual(vec0.y, 2) + + vec0 = Vector2d((3, 4, 5), fallback=(1, 2)) + self.assertEqual(vec0.x, 1) + self.assertEqual(vec0.y, 2) def test_binary_operators(self): """Test binary operators for vector2d""" -- GitLab From 6db362a4714f647bd1987325c844ea48ef5d5338 Mon Sep 17 00:00:00 2001 From: Jonathan Neuhauser Date: Wed, 9 Feb 2022 14:10:25 +0100 Subject: [PATCH 6/6] simplify regex in arg_number_range --- inkex/base.py | 32 ++++++++++++++++++-------------- tests/test_inkex_base.py | 9 +++++++-- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/inkex/base.py b/inkex/base.py index 5896eda4..bff90a5e 100644 --- a/inkex/base.py +++ b/inkex/base.py @@ -142,27 +142,31 @@ class InkscapeExtension: """ def _inner(value): - # replace 4-7 with 4, 5, 6, 7 - pages = re.sub( - r"(\d+)\s?-\s?(\d+)", - lambda m: ",".join( - map(str, range(int(m.group(1)), int(m.group(2)) + 1)) - ), - value, - ) - - def method(lastvalue, pages): - # replace 5- with 5, 6, ..., lastpage + def method(pages, lastvalue, startvalue=1): + # replace ranges, such as -3, 10- with startvalue,2,3,10..lastvalue pages = re.sub( - r"(\d+)\s?-", - lambda m: ",".join(map(str, range(int(m.group(1)), lastvalue + 1))), + r"(\d+|)\s?-\s?(\d+|)", + lambda m: ",".join( + map( + str, + range( + int(m.group(1) or startvalue), + int(m.group(2) or lastvalue) + 1, + ), + ) + ) + if not (m.group(1) or m.group(2)) == "" + else "", pages, ) + pages = map(int, re.findall(r"(\d+)", pages)) pages = tuple({i for i in pages if i <= lastvalue}) return pages - return lambda lastvalue: method(lastvalue, pages) + return lambda lastvalue, startvalue=1: method( + value, lastvalue, startvalue=startvalue + ) return _inner diff --git a/tests/test_inkex_base.py b/tests/test_inkex_base.py index 8bc19190..053d041e 100644 --- a/tests/test_inkex_base.py +++ b/tests/test_inkex_base.py @@ -132,9 +132,11 @@ class TestArgumentDatatypes(TestCase): def test_page_descriptor(self): """Test that page descriptions are parsed correctly""" - def parsetest(string, length, comparison): + def parsetest(string, length, comparison, startvalue=1): result = InkscapeExtension.arg_number_ranges() - self.assertTupleEqual(result(string)(length), comparison) + self.assertTupleEqual( + result(string)(length, startvalue=startvalue), comparison + ) parsetest("1, 2,3", 10, (1, 2, 3)) parsetest("1-3, 5, 10", 10, (1, 2, 3, 5, 10)) @@ -142,6 +144,9 @@ class TestArgumentDatatypes(TestCase): parsetest("2-5, 7-9, 10-", 12, (2, 3, 4, 5, 7, 8, 9, 10, 11, 12)) parsetest("6, 7, 8", 3, tuple()) parsetest("2, 216-218, 10", 300, (2, 10, 216, 217, 218)) + parsetest("-3,10-,5", 12, (1, 2, 3, 5, 10, 11, 12)) + parsetest("-5, 7-", 7, (3, 4, 5, 7), 3) + parsetest("-", 7, (), 3) class SvgInputOutputTest(TestCase): -- GitLab