diff --git a/guides_creator.inx b/guides_creator.inx index e7f5e8ae9292134bc8d9798610603badea33d614..37e6eb1afd093dc55465c35e88559e10c1ec6a0e 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 06b96680025dd71d9ce749aa48f4b293e1bf9e85..d67c35a1e87c64ff713e3ddbe99c22012eab0962 100755 --- a/guides_creator.py +++ b/guides_creator.py @@ -28,15 +28,60 @@ This basic extension allows you to automatically draw guides in inkscape. """ from math import cos, sin, sqrt +import re import inkex -from inkex import Guide + +from inkex.localization import inkex_gettext as _ + + +class GuidesOpts: + """Manager of current-page-related values for GuidesCreator""" + + # pylint: disable=too-few-public-methods + 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)""" + self.pagenumber = pagenumber + 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.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=self.arg_number_ranges(), + 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"), @@ -65,29 +110,43 @@ 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", + "book_alternating_right", + "book_alternating_left", + ], + 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") + pars.add_argument( + "--nodup", type=inkex.Boolean, help="Omit duplicated guides", default=True + ) - 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.store: 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): if self.options.delete: for guide in self.svg.namedview.get_guides(): guide.delete() - return self.options.tab() + 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): """Generate a regular set of guides""" @@ -106,28 +165,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.store.width, self.store.height)[index] + self.draw_guide( + (0, position) if index == 1 else (position, 0), + self.store.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,161 +183,142 @@ 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.store.width, self.store.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) - - 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) + corner_guides = { + "ul": ((left, top), (cos(angle), cos(angle))), + "ur": ((right, top), (-sin(angle), sin(angle))), + "ll": ((left, bottom), (-cos(angle), cos(angle))), + "lr": ((right, bottom), (-sin(angle), -sin(angle))), + } - 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.store.height), self.store.orientation[1]) + self.draw_guide((self.store.width, 0), self.store.orientation[1]) # 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.store.height), self.store.orientation[0]) + self.draw_guide((self.store.width, 0), self.store.orientation[0]) 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) + margins = [ + (i / j if int(j) != 0 else None) + for i, j in zip( + ( + 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, + 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), + } + margins_preset = self.options.margins_preset + if margins_preset.startswith("book_alternating"): + margins_preset = ( + "book_left" + if self.store.pagenumber % 2 == (1 if "left" in margins_preset else 0) + else "book_right" + ) - # 1/9th right margin - x_right = (self.width / 9) * 7 - self.draw_guide(str(x_right) + ",0", self.v_orientation) + if margins_preset in book_options: + margins = [ + i * j + for i, j in zip( + book_options[margins_preset], + 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.store.height, 0, 0, self.store.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.store.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.store.width, self.store.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)) + """Draw the guides""" + newpos = [ + position[0] + self.store.page_origin[0], + position[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) + else: + self.svg.namedview.new_guide(newpos, orientation) if __name__ == "__main__": diff --git a/inkex/base.py b/inkex/base.py index 0da53396ce917f2e062c7a3175c19bdcf226ebf7..bff90a5ebd1b2f5eb93bdfd3aa6f8250b3a96f72 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,49 @@ 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): + def method(pages, lastvalue, startvalue=1): + # replace ranges, such as -3, 10- with startvalue,2,3,10..lastvalue + pages = re.sub( + 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, startvalue=1: method( + value, lastvalue, startvalue=startvalue + ) + + 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 93073018ebcb90ca00e1b7681d0f59d9134ea5ca..9047ed37d0721b4ff1a73d98f6a550535e4a5651 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,28 @@ 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) + 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, other): + return guide + return None + def get_pages(self): """Returns a list of pages""" return self.findall("inkscape:page") @@ -123,12 +144,24 @@ class Guide(BaseElement): tag_name = "sodipodi:guide" + @property + def orientation(self) -> Vector2d: + """Vector normal to the guide""" + return Vector2d(self.get("orientation"), fallback=(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 + """ + return Vector2d(self.get("position"), fallback=(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/inkex/transforms.py b/inkex/transforms.py index 9427010d6a2c4e12a5780393ff9e857d9722f844..86f25b4429df3a0a98bc21c01a5afdc9a4aa5d25 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/data/refs/guides_creator.out b/tests/data/refs/guides_creator.out new file mode 100644 index 0000000000000000000000000000000000000000..6749fa04ec2177553e15db22e7bc0cf886ce0a36 --- /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 0000000000000000000000000000000000000000..e166c8a10eca62e20d690020c9bd26a15cae1f37 --- /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 0000000000000000000000000000000000000000..4c75674493561ae5db7689fcdb8be1ea8b033a01 --- /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 0000000000000000000000000000000000000000..baf03de00ec178b0c64d3469433841d7f045e4e2 --- /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__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 0000000000000000000000000000000000000000..704fd5ab29a4c069a797e038266a85be90607550 --- /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__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 0000000000000000000000000000000000000000..5e46848384e25745e9fb888d6022a8f6e72a1770 --- /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__--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 0000000000000000000000000000000000000000..26a24926b3a72924b5ba89b02bab5bd9922c648f --- /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 0000000000000000000000000000000000000000..a6f8e494fee3a7ff62279fee17666079104b7c94 --- /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/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 0000000000000000000000000000000000000000..cf505659001248911099732b9da6d904b1a40050 --- /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 94% rename from tests/data/refs/guides_creator__781b3284b91d843346c42568aa6ce409.out rename to tests/data/refs/guides_creator__3b35068c24255b2144616d98c2a077bd.out index 2c97acaa31a6c79fdef0dc43e0e647b8575be752..d4155f4b64432e09a6808e89488e8c5501ca263f 100644 --- a/tests/data/refs/guides_creator__781b3284b91d843346c42568aa6ce409.out +++ b/tests/data/refs/guides_creator__3b35068c24255b2144616d98c2a077bd.out @@ -18,7 +18,7 @@ - + diff --git a/tests/data/svg/empty_multipage.svg b/tests/data/svg/empty_multipage.svg new file mode 100644 index 0000000000000000000000000000000000000000..7758938fa498efd35ab3d453a573496c9dcb3c42 --- /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 03d5c73d8cb07e015d19e439385d7373f806c0c3..4bb66b1b84c13a5a5e326674565e35b3a9664568 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 = [ @@ -28,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 @@ -39,7 +41,69 @@ 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", + ), + ] + + +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", + ), + ] diff --git a/tests/test_inkex_base.py b/tests/test_inkex_base.py index bff7c5dfcd62c6414bc6aa46a80ae8b2e9afaf31..053d041ee3c74a9652095dea77cfd58e0808d349 100644 --- a/tests/test_inkex_base.py +++ b/tests/test_inkex_base.py @@ -126,6 +126,29 @@ 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, startvalue=1): + result = InkscapeExtension.arg_number_ranges() + 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)) + 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)) + 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): """Test SVG Input Mixin""" diff --git a/tests/test_inkex_elements.py b/tests/test_inkex_elements.py index b1bd0eaf3882ff903eb711139b2bcf5c1a562d86..59458135fd52655e593c16d50bea55eb73e7a763 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) diff --git a/tests/test_inkex_transforms.py b/tests/test_inkex_transforms.py index fa3dd368bc20d8b4d0a23089d21c628ac1406863..6068631a02b78d0dffa5cb6e96b74afec6ee6076 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"""