#!/usr/bin/env python # coding=utf-8 # # Copyright (C) 2016 Richard White, rwhite8282@gmail.com # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # """ An Inkscape extension that creates a frame around a selected object. """ from typing import List import inkex from inkex import Group, PathElement, ClipPath from inkex.localization import inkex_gettext as _ class Frame(inkex.EffectExtension): """ An Inkscape extension that creates a frame around a selected object. """ def add_arguments(self, pars): # Parse the options. pars.add_argument("--tab", default="stroke") pars.add_argument("--clip", type=inkex.Boolean, default=False) pars.add_argument("--type", default="rect", choices=["rect", "ellipse"]) pars.add_argument("--corner_radius", type=int, default=0) pars.add_argument("--fill_color", type=inkex.Color, default=inkex.Color(0)) pars.add_argument("--group", type=inkex.Boolean, default=False) pars.add_argument("--stroke_color", type=inkex.Color, default=inkex.Color(0)) pars.add_argument("--width", type=float, default=2.0) pars.add_argument( "--offset_absolute", type=float, default=0, help="Offset in user units, positive = outside", ) pars.add_argument( "--offset_relative", type=float, default=0, help="Relative offset in percentage of the bounding box size", ) pars.add_argument( "--z_position", type=str, default="bottom", choices=["top", "bottom", "split"], ) pars.add_argument("--asgroup", type=inkex.Boolean, default=False) def add_clip(self, node: inkex.BaseElement, clip_path: inkex.PathElement): """Adds a new clip path node to the defs and sets the clip-path on the node. node -- The node that will be clipped. clip_path -- The clip path object. """ clip = ClipPath() # apply the reverse transform to the clip path. no composed_transform here, # since the frame will be appended to the object' parent (or an untransformed # group within). clip.append(PathElement.new(path=clip_path.path, transform=-node.transform)) self.svg.defs.add(clip) node.set("clip-path", clip.get_id(as_url=2)) @staticmethod def generate_frame(name, box: inkex.BoundingBox, rectangle=True, radius=0): """ name -- The name of the new frame object. box -- The boundary box of the node. style -- The style used to draw the path. radius -- The corner radius of the frame. returns a new frame node. """ if rectangle: r = min([radius, abs(box.x.size / 2), abs(box.y.size / 2)]) elem = inkex.Rectangle.new( left=box.x.minimum, top=box.y.minimum, width=box.x.size, height=box.y.size, rx=r, ) else: elem = inkex.Ellipse.new(center=box.center, radius=box.size / 2) elem.label = name return elem def create_frame(self, containedelements: List[inkex.BaseElement]): """generate the frame for an element or a group of elements""" width = self.options.width style = inkex.Style({"stroke-width": width}) style.set_color(self.options.stroke_color, "stroke") elem_top = None elem_bottom = None box = inkex.BoundingBox() for node in containedelements: if isinstance(node, (inkex.TextElement, inkex.Tspan, inkex.FlowRoot)): try: box += node.get_inkscape_bbox() except ValueError: continue else: box += node.bounding_box() box = box.resize( box.x.size * (self.options.offset_relative / 100), box.y.size * (self.options.offset_relative / 100), ) box = box.resize(self.options.offset_absolute) frame = self.generate_frame( "Frame", box, self.options.type == "rect", self.options.corner_radius ) if self.options.z_position != "split": style.set_color(self.options.fill_color, "fill") frame.style = style if self.options.z_position == "bottom": elem_bottom = frame else: elem_top = frame else: fill = frame.copy() elem_top = frame elem_top.style = style elem_bottom = fill elem_bottom.style.set_color(self.options.fill_color, "fill") elem_top.style["fill"] = None return elem_bottom, elem_top def process_elements(self, containedelements: List[inkex.BaseElement]): """Create and append the frame for an object or a set of objects.""" elem_bottom, elem_top = self.create_frame(containedelements) if self.options.clip: for node in containedelements: element = elem_top if elem_top is not None else elem_bottom self.add_clip(node, element) if self.options.group: group = containedelements[0].getparent().add(Group()) if elem_bottom is not None: group.append(elem_bottom) for node in containedelements: group.append(node) if elem_top is not None: group.append(elem_top) else: if elem_bottom is not None: containedelements[0].addprevious(elem_bottom) if elem_top is not None: containedelements[-1].addnext(elem_top) def effect(self): """Performs the effect.""" # Determine common properties. if not self.svg.selection: raise inkex.AbortExtension(_("Select at least one object.")) style = inkex.Style({"stroke-width": self.options.width}) style.set_color(self.options.stroke_color, "stroke") if not self.options.asgroup: for node in self.svg.selection: self.process_elements([node]) else: self.process_elements(self.svg.selection.rendering_order()) if __name__ == "__main__": Frame().run()