def run(self): """Main entry point for Inkscape extensions. """ # Set up debug SVG output context. geom.debug.set_svg_context(self.debug_svg) selected_elements = self.get_elements() if not len(selected_elements): # Nothing selected or document is empty return # Create a new layer for the SVG output. layer = None if self.options.new_layer: layer = self.svg.create_layer(self._LAYER_NAME, incr_suffix=True) for n in range(self.options.copies): for element in selected_elements: m_elem = self.svg.parse_transform_attr( element.get('transform')) v = geom.P.from_polar(self.options.interval * (n + 1), -self.options.angle) m_translate = transform2d.matrix_translate(v.x, v.y) m_transform = transform2d.compose_transform( m_elem, m_translate) transform_attr = svg.transform_attr(m_transform) elem_copy = deepcopy(element) elem_copy.set('transform', transform_attr) # elem_copy.set('id', element.get('id') + '_r') self.svg.add_elem(elem_copy, parent=layer)
def parse_transform_attr(self, transform_attr): """Parse an SVG transform attribute. Args: transform_attr: A string containing the SVG transform list. Returns: A single affine transform matrix. """ if (transform_attr is None or not transform_attr or transform_attr.isspace()): return transform2d.IDENTITY_MATRIX transform_attr = transform_attr.strip() transforms = self._TRANSFORM_RE.findall(transform_attr) matrices = [] for transform, args in transforms: matrix = None values = [float(n) for n in args.replace(',', ' ').split()] num_values = len(values) if transform == 'translate': x = values[0] y = values[1] if num_values > 1 else 0.0 matrix = transform2d.matrix_translate(x, y) if transform == 'scale': x = values[0] y = values[1] if num_values > 1 else x matrix = transform2d.matrix_scale(x, y) if transform == 'rotate': a = math.radians(values[0]) cx = values[1] if num_values > 1 else 0.0 cy = values[2] if num_values > 2 else 0.0 matrix = transform2d.matrix_rotate(a, (cx, cy)) if transform == 'skewX': a = math.radians(values[0]) matrix = transform2d.matrix_skew_x(a) if transform == 'skewY': a = math.radians(values[0]) matrix = transform2d.matrix_skew_y(a) if transform == 'matrix': matrix = ((values[0], values[2], values[4]), (values[1], values[3], values[5])) if matrix is not None: matrices.append(matrix) # Compose all the tranforms into one matrix result_matrix = transform2d.IDENTITY_MATRIX for matrix in matrices: result_matrix = transform2d.compose_transform( result_matrix, matrix) return result_matrix
def parse_transform_attr(self, transform_attr): """Parse an SVG transform attribute. Args: transform_attr: A string containing the SVG transform list. Returns: A single affine transform matrix. """ if (transform_attr is None or not transform_attr or transform_attr.isspace()): return transform2d.IDENTITY_MATRIX transform_attr = transform_attr.strip() transforms = self._TRANSFORM_RE.findall(transform_attr) matrices = [] for transform, args in transforms: matrix = None values = [float(n) for n in args.replace(',', ' ').split()] num_values = len(values) if transform == 'translate': x = values[0] y = values[1] if num_values > 1 else 0.0 matrix = transform2d.matrix_translate(x, y) if transform == 'scale': x = values[0] y = values[1] if num_values > 1 else x matrix = transform2d.matrix_scale(x, y) if transform == 'rotate': a = math.radians(values[0]) cx = values[1] if num_values > 1 else 0.0 cy = values[2] if num_values > 2 else 0.0 matrix = transform2d.matrix_rotate(a, (cx, cy)) if transform == 'skewX': a = math.radians(values[0]) matrix = transform2d.matrix_skew_x(a) if transform == 'skewY': a = math.radians(values[0]) matrix = transform2d.matrix_skew_y(a) if transform == 'matrix': matrix = ((values[0], values[2], values[4]), (values[1], values[3], values[5])) if matrix is not None: matrices.append(matrix) # Compose all the tranforms into one matrix result_matrix = transform2d.IDENTITY_MATRIX for matrix in matrices: result_matrix = transform2d.compose_transform(result_matrix, matrix) return result_matrix
def get_element_transform(self, node, root=None): """Get the combined transform of the element and it's combined parent transforms. Args: node: The element node. root: The document root or where to stop searching. Returns: The combined transform matrix or the identity matrix if none found. """ matrix = self.get_parent_transform(node, root) transform_attr = node.get('transform') if transform_attr is not None and transform_attr: node_transform = self.parse_transform_attr(transform_attr) matrix = transform2d.compose_transform(matrix, node_transform) return matrix
def get_parent_transform(self, node, root=None): """Get the combined transform of the node's parents. Args: node: The child node. root: The document root or where to stop searching. Returns: The parent transform matrix or the identity matrix if none found. """ matrix = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]] parent = node.getparent() while parent is not root: parent_transform_attr = parent.get('transform') if parent_transform_attr is not None: parent_matrix = self.parse_transform_attr(parent_transform_attr) matrix = transform2d.compose_transform(parent_matrix, matrix) parent = parent.getparent() return matrix
def get_parent_transform(self, node, root=None): """Get the combined transform of the node's parents. Args: node: The child node. root: The document root or where to stop searching. Returns: The parent transform matrix or the identity matrix if none found. """ matrix = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]] parent = node.getparent() while parent is not root: parent_transform_attr = parent.get('transform') if parent_transform_attr is not None: parent_matrix = self.parse_transform_attr( parent_transform_attr) matrix = transform2d.compose_transform(parent_matrix, matrix) parent = parent.getparent() return matrix
def _get_shape_nodes_recurs(self, node, shapetags, parent_transform, check_parent, skip_layers, accumulate_transform): """Recursively traverse an SVG node tree and flatten it to a list of tuples containing an SVG shape element and its accumulated transform. This does a depth-first traversal of <g> and <use> elements. Anything besides paths, rectangles, circles, ellipses, lines, polygons, and polylines are ignored. Hidden elements are ignored. Args: node: The root of the node tree to traverse and flatten. shapetags: List of shape element tags that can be fetched. parent_transform: Transform matrix to add to each node's transforms. check_parent: Check parent visibility skip_layers: A list of layer names (as regexes) to ignore accumulate_transform: Apply parent transform(s) to element node if True. Returns: A possibly empty list of 2-tuples consisting of SVG element and transform. """ if not self.node_is_visible(node, check_parent=check_parent): return [] if parent_transform is None: parent_transform = self.get_parent_transform(node) nodelist = [] # first apply the current transform matrix to this node's tranform node_transform = self.parse_transform_attr(node.get('transform')) if accumulate_transform: node_transform = transform2d.compose_transform( parent_transform, node_transform) if self.node_is_group(node): if self.is_layer(node) and skip_layers is not None and skip_layers: layer_name = self.get_layer_name(node) # logger.debug('layer: %s', layer_name) for skip_layer in skip_layers: if re.match(skip_layer, layer_name) is not None: # logger.debug('skipping layer: %s', layer_name) return [] # Recursively traverse group children for child_node in node: subnodes = self._get_shape_nodes_recurs( child_node, shapetags, node_transform, False, skip_layers, accumulate_transform) nodelist.extend(subnodes) elif node.tag == svg_ns('use') or node.tag == 'use': # A <use> element refers to another SVG element via an # xlink:href="#id" attribute. refid = node.get(svg.xlink_ns('href')) if refid: # [1:] to ignore leading '#' in reference refnode = self.get_node_by_id(refid[1:]) # TODO: Can the referred node not be visible? if refnode is not None: # and self.node_is_visible(refnode): # Apply explicit x,y translation transform x = float(node.get('x', '0')) y = float(node.get('y', '0')) if x != 0 or y != 0: translation = transform2d.matrix_translate(x, y) node_transform = transform2d.compose_transform( node_transform, translation) subnodes = self._get_shape_nodes_recurs( refnode, shapetags, node_transform, False, skip_layers, accumulate_transform) nodelist.extend(subnodes) elif svg.strip_ns(node.tag) in shapetags: nodelist.append((node, node_transform)) return nodelist
def run(self): """Main entry point for Inkscape plugins. """ random.seed() geom.set_epsilon(self.options.epsilon) geom.debug.set_svg_context(self.debug_svg) doc_size = geom.P(self.svg.get_document_size()) self.doc_center = doc_size / 2 # Determine clipping rectangle which is bounded by the document # or the margins, depending on user options. The margins can # be outside the document bounds. # (Y axis is inverted in Inkscape) clip_rect = None # Default bottom_left = geom.P(self.options.margin_left, self.options.margin_top) top_right = doc_size - geom.P(self.options.margin_right, self.options.margin_bottom) self.margin_clip_rect = geom.box.Box(bottom_left, top_right) doc_clip_rect = geom.box.Box(geom.P(0, 0), doc_size) if self.options.clip_to_doc and self.options.clip_to_margins: clip_rect = doc_clip_rect.intersection(self.margin_clip_rect) elif self.options.clip_to_doc: clip_rect = doc_clip_rect elif self.options.clip_to_margins: clip_rect = self.margin_clip_rect # The clipping region can be a circle or a rectangle if clip_rect is not None and self.options.clip_to_circle: radius = min(clip_rect.width(), clip_rect.height()) / 2.0 self.clip_region = geom.ellipse.Ellipse(clip_rect.center(), radius) else: self.clip_region = clip_rect if self.options.clip_offset_center: clip_offset = clip_rect.center() - self.doc_center self.options.offset_x += clip_offset.x self.options.offset_y += clip_offset.y # Optionally insert spherical point projection if self.options.project_sphere: projector = SphericalProjector(self.doc_center, self.options.project_radius, invert=self.options.project_invert) else: projector = IdentityProjector() # Set up plotter transform for rotation, scale, and offset. # Origin at document center. scale = self.options.scale * self._SCALE_SCALE offset = geom.P(self.doc_center) + geom.P(self.options.offset_x, self.options.offset_y) transform1 = transform2d.matrix_rotate(self.options.rotate) transform2 = transform2d.matrix_scale_translate(scale, scale, offset.x, offset.y) plot_transform = transform2d.compose_transform(transform1, transform2) plotter = _QuasiPlotter(self.clip_region, plot_transform, projector) # Create color LUTs for i in range(255): ci = 255 - i self._FILL_LUT['red'].append('#%02x0000' % ci) self._FILL_LUT['yellow'].append('#%02x%02x00' % (ci, ci)) q = quasi.Quasi() q.offset_salt_x = self.options.salt_x q.offset_salt_y = self.options.salt_y q.skinnyfat_ratio = self.options.skinnyfat_ratio q.segment_ratio = self.options.segment_ratio q.segtype_skinny = self.options.segtype_skinny q.segtype_fat = self.options.segtype_fat q.segment_split_cross = self.options.segment_split_cross q.symmetry = self.options.symmetry q.numlines = self.options.numlines q.plotter = plotter q.color_fill = self.options.polygon_fill q.color_by_polytype = self.options.polygon_zfill q.quasi() # Re-center the quasi polygons to the clip region borders if self.options.clip_recenter and self.clip_region is not None: q.plotter.recenter() polygon_segment_graph = planargraph.Graph() for poly in q.plotter.polygons: polygon_segment_graph.add_poly(poly) polygon_segments = list(polygon_segment_graph.edges) # Optionally sort the polygons to change drawing order. if self.options.polygon_sort != self.POLYGON_SORT_NONE: outer_hull = polygon_segment_graph.boundary_polygon() hull_centroid = polygon.centroid(outer_hull) # Find the distance of the farthest polygon max_d = 0.0 for poly in q.plotter.polygons: d = hull_centroid.distance(polygon.centroid(poly)) if d > max_d: max_d = d segment_size = q.plotter.polygons[0][0].distance(q.plotter.polygons[0][1]) # Secondary sort key is angular location angle_key = lambda poly: hull_centroid.ccw_angle2(hull_centroid + geom.P(1, 0), polygon.centroid(poly)) q.plotter.polygons.sort(key=angle_key) angle_key = lambda segment: hull_centroid.ccw_angle2(hull_centroid + geom.P(1, 0), segment.midpoint()) polygon_segments.sort(key=angle_key) q.plotter.segments.sort(key=angle_key) # Primary sort key is distance from centroid dist_key = lambda poly: int(hull_centroid.distance(polygon.centroid(poly)) / segment_size) dist_key2 = lambda segment: int(hull_centroid.distance(segment.midpoint()) / segment_size) if self.options.polygon_sort == self.POLYGON_SORT_INSIDE_OUT: q.plotter.polygons.sort(key=dist_key) polygon_segments.sort(key=dist_key2) q.plotter.segments.sort(key=dist_key2) elif self.options.polygon_sort == self.POLYGON_SORT_OUTSIDE_IN: q.plotter.polygons.sort(key=dist_key, reverse=True) polygon_segments.sort(key=dist_key2, reverse=True) q.plotter.segments.sort(key=dist_key2, reverse=True) # angle_key = lambda poly: hull_centroid.ccw_angle2(hull_centroid + geom.P(1, 0), polygon.centroid(poly)) # q.plotter.polygons.sort(key=angle_key) # angle_key = lambda segment: hull_centroid.ccw_angle2(hull_centroid + geom.P(1, 0), segment.midpoint()) # polygon_segments.sort(key=angle_key) # Update styles with any command line option values self._styles.update(self.svg.styles_from_templates( self._styles, self._style_defaults, vars(self.options))) # logger.debug('colors: %d' % len(plotter.color_count)) # for color in sorted(plotter.color_count.keys()): # logger.debug('[%.5f]: %d' % (color, plotter.color_count[color])) if self.options.create_info_layer: self._draw_info_layer() if self.options.margin_draw: self._draw_margins(q.plotter.bbox()) if self.options.polygon_draw: self._draw_polygons(q.plotter) # self._draw_polygon_circles(q.plotter.polygons) if self.options.polyseg_draw: self._draw_polygon_segments(polygon_segments) if self.options.polygon_mult > 0: self._draw_inset_polygons(q.plotter.polygons, self.options.polygon_mult_spacing, self.options.polygon_mult) if self.options.ellipse_draw: self._draw_polygon_ellipses(q.plotter.polygons, self.options.ellipse_inset) if self.options.segtype_skinny == quasi.Quasi.SEG_NONE \ and self.options.segtype_fat == quasi.Quasi.SEG_NONE: self.options.segment_draw = False self.options.segpath_draw = False if self.options.segment_draw: self._draw_segments(q.plotter.segments) self._draw_segboxes(q.plotter) if self.options.segpath_draw: self._draw_segment_chains(q.plotter.segments) if self.options.frame_draw and (self.options.frame_width > 0 and self.options.frame_height > 0): self._draw_frame()
def svg_element_to_geometry(element, element_transform=None, parent_transform=None): """Convert the SVG shape element to a list of one or more Line, Arc, and/or CubicBezier segments, and apply node/parent transforms. The coordinates of the segments will be absolute with respect to the parent container. Args: element: An SVG Element shape node. element_transform: An optional transform to apply to the element. Default is None. parent_transform: An optional parent transform to apply to the element. Default is None. Returns: A list of zero or more paths. A path being a list of zero or more Line, Arc, EllipticalArc, or CubicBezier objects. """ # Convert the element to a list of subpaths subpath_list = [] tag = svg.strip_ns(element.tag) # tag stripped of namespace part if tag == 'path': d = element.get('d') if d is not None and d: subpath_list = parse_path_geom(d, ellipse_to_bezier=True) else: subpath = [] if tag == 'line': subpath = convert_line(element) elif tag == 'ellipse': ellipse = convert_ellipse(element) subpath = bezier.bezier_ellipse(ellipse) elif tag == 'rect': subpath = convert_rect(element) elif tag == 'circle': subpath = convert_circle(element) elif tag == 'polyline': subpath = convert_polyline(element) elif tag == 'polygon': subpath = convert_polygon(element) if subpath: subpath_list = [subpath, ] if subpath_list: # Create a transform matrix that is composed of the # parent transform and the element transform # so that control points are in absolute coordinates. if parent_transform is not None: element_transform = transform2d.compose_transform(parent_transform, element_transform) if element_transform is not None: x_subpath_list = [] for subpath in subpath_list: x_subpath = [] for segment in subpath: # Skip zero-length segments. if not segment.p1 == segment.p2: segment = segment.transform(element_transform) x_subpath.append(segment) x_subpath_list.append(x_subpath) return x_subpath_list return subpath_list
def _get_shape_nodes_recurs(self, node, shapetags, parent_transform, check_parent, skip_layers, accumulate_transform): """Recursively traverse an SVG node tree and flatten it to a list of tuples containing an SVG shape element and its accumulated transform. This does a depth-first traversal of <g> and <use> elements. Anything besides paths, rectangles, circles, ellipses, lines, polygons, and polylines are ignored. Hidden elements are ignored. Args: node: The root of the node tree to traverse and flatten. shapetags: List of shape element tags that can be fetched. parent_transform: Transform matrix to add to each node's transforms. check_parent: Check parent visibility skip_layers: A list of layer names (as regexes) to ignore accumulate_transform: Apply parent transform(s) to element node if True. Returns: A possibly empty list of 2-tuples consisting of SVG element and transform. """ if not self.node_is_visible(node, check_parent=check_parent): return [] if parent_transform is None: parent_transform = self.get_parent_transform(node) nodelist = [] # first apply the current transform matrix to this node's tranform node_transform = self.parse_transform_attr(node.get('transform')) if accumulate_transform: node_transform = transform2d.compose_transform(parent_transform, node_transform) if self.node_is_group(node): if self.is_layer(node) and skip_layers is not None and skip_layers: layer_name = self.get_layer_name(node) # logger.debug('layer: %s', layer_name) for skip_layer in skip_layers: if re.match(skip_layer, layer_name) is not None: # logger.debug('skipping layer: %s', layer_name) return [] # Recursively traverse group children for child_node in node: subnodes = self._get_shape_nodes_recurs(child_node, shapetags, node_transform, False, skip_layers, accumulate_transform) nodelist.extend(subnodes) elif node.tag == svg_ns('use') or node.tag == 'use': # A <use> element refers to another SVG element via an # xlink:href="#id" attribute. refid = node.get(svg.xlink_ns('href')) if refid: # [1:] to ignore leading '#' in reference refnode = self.get_node_by_id(refid[1:]) # TODO: Can the referred node not be visible? if refnode is not None: # and self.node_is_visible(refnode): # Apply explicit x,y translation transform x = float(node.get('x', '0')) y = float(node.get('y', '0')) if x != 0 or y != 0: translation = transform2d.matrix_translate(x, y) node_transform = transform2d.compose_transform( node_transform, translation) subnodes = self._get_shape_nodes_recurs(refnode, shapetags, node_transform, False, skip_layers, accumulate_transform) nodelist.extend(subnodes) elif svg.strip_ns(node.tag) in shapetags: nodelist.append((node, node_transform)) return nodelist
def svg_element_to_geometry(element, element_transform=None, parent_transform=None): """Convert the SVG shape element to a list of one or more Line, Arc, and/or CubicBezier segments, and apply node/parent transforms. The coordinates of the segments will be absolute with respect to the parent container. Args: element: An SVG Element shape node. element_transform: An optional transform to apply to the element. Default is None. parent_transform: An optional parent transform to apply to the element. Default is None. Returns: A list of zero or more paths. A path being a list of zero or more Line, Arc, EllipticalArc, or CubicBezier objects. """ # Convert the element to a list of subpaths subpath_list = [] tag = svg.strip_ns(element.tag) # tag stripped of namespace part if tag == 'path': d = element.get('d') if d is not None and d: subpath_list = parse_path_geom(d, ellipse_to_bezier=True) else: subpath = [] if tag == 'line': subpath = convert_line(element) elif tag == 'ellipse': ellipse = convert_ellipse(element) subpath = bezier.bezier_ellipse(ellipse) elif tag == 'rect': subpath = convert_rect(element) elif tag == 'circle': subpath = convert_circle(element) elif tag == 'polyline': subpath = convert_polyline(element) elif tag == 'polygon': subpath = convert_polygon(element) if subpath: subpath_list = [ subpath, ] if subpath_list: # Create a transform matrix that is composed of the # parent transform and the element transform # so that control points are in absolute coordinates. if parent_transform is not None: element_transform = transform2d.compose_transform( parent_transform, element_transform) if element_transform is not None: x_subpath_list = [] for subpath in subpath_list: x_subpath = [] for segment in subpath: # Skip zero-length segments. if not segment.p1 == segment.p2: segment = segment.transform(element_transform) x_subpath.append(segment) x_subpath_list.append(x_subpath) return x_subpath_list return subpath_list