Mario Voigt 2021-04-12 21:11:01 +02:00
commit be4b28db3b
18 changed files with 878 additions and 216 deletions

View File

@ -4,8 +4,10 @@
# Copyright Mark "Klowner" Riedesel
# https://github.com/Klowner/inkscape-applytransforms
#
import inkex
import copy
import math
from lxml import etree
import inkex
from inkex.paths import CubicSuperPath, Path
from inkex.transforms import Transform
from inkex.styles import Style
@ -57,7 +59,7 @@ class ApplyTransform(inkex.EffectExtension):
def recursiveFuseTransform(self, node, transf=[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]):
transf = Transform(transf) * Transform(node.get("transform", None))
transf = Transform(transf) * Transform(node.get("transform", None)) #a, b, c, d = linear transformations / e, f = translations
if 'transform' in node.attrib:
del node.attrib['transform']
@ -139,10 +141,56 @@ class ApplyTransform(inkex.EffectExtension):
else:
node.set("r", edgex / 2)
elif node.tag == inkex.addNS("use", "svg"):
href = None
old_href_key = '{http://www.w3.org/1999/xlink}href'
new_href_key = 'href'
if node.attrib.has_key(old_href_key) is True: # {http://www.w3.org/1999/xlink}href (which gets displayed as 'xlink:href') attribute is deprecated. the newer attribute is just 'href'
href = node.attrib.get(old_href_key)
#node.attrib.pop(old_href_key)
if node.attrib.has_key(new_href_key) is True:
href = node.attrib.get(new_href_key) #we might overwrite the previous deprecated xlink:href but it's okay
#node.attrib.pop(new_href_key)
#get the linked object from href attribute
linkedObject = self.document.getroot().xpath("//*[@id = '%s']" % href.lstrip('#')) #we must remove hashtag symbol
linkedObjectCopy = copy.copy(linkedObject[0])
objectType = linkedObject[0].tag
if objectType == inkex.addNS("image", "svg"):
mask = None #image might have an alpha channel
new_mask_id = self.svg.get_unique_id("mask")
newMask = None
if node.attrib.has_key('mask') is True:
mask = node.attrib.get('mask')
#node.attrib.pop('mask')
#get the linked mask from mask attribute. We remove the old and create a new
if mask is not None:
linkedMask = self.document.getroot().xpath("//*[@id = '%s']" % mask.lstrip('url(#').rstrip(')')) #we must remove hashtag symbol
linkedMask[0].getparent().remove(linkedMask[0])
maskAttributes = {'id': new_mask_id}
newMask = etree.SubElement(self.document.getroot(), inkex.addNS('mask', 'svg'), maskAttributes)
width = float(linkedObjectCopy.get('width')) * transf.a
height = float(linkedObjectCopy.get('height')) * transf.d
linkedObjectCopy.set('width', '{:1.6f}'.format(width))
linkedObjectCopy.set('height', '{:1.6f}'.format(height))
linkedObjectCopy.set('x', '{:1.6f}'.format(transf.e))
linkedObjectCopy.set('y', '{:1.6f}'.format(transf.f))
if newMask is not None:
linkedObjectCopy.set('mask', 'url(#' + new_mask_id + ')')
maskRectAttributes = {'x': '{:1.6f}'.format(transf.e), 'y': '{:1.6f}'.format(transf.f), 'width': '{:1.6f}'.format(width), 'height': '{:1.6f}'.format(height), 'style':'fill:#ffffff;'}
maskRect = etree.SubElement(newMask, inkex.addNS('rect', 'svg'), maskRectAttributes)
else:
self.recursiveFuseTransform(linkedObjectCopy, transf)
self.document.getroot().append(linkedObjectCopy) #for each svg:use we append a copy to the document root
node.getparent().remove(node) #then we remove the use object
elif node.tag in [inkex.addNS('rect', 'svg'),
inkex.addNS('text', 'svg'),
inkex.addNS('image', 'svg'),
inkex.addNS('use', 'svg')]:
inkex.addNS('image', 'svg')]:
inkex.utils.errormsg(
"Shape %s (%s) not yet supported, try Object to path first"
% (node.TAG, node.get("id"))

View File

@ -2,20 +2,43 @@
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Cleanup Styles</name>
<id>fablabchemnitz.de.cleanup</id>
<param name="stroke_width" type="float" precision="4" min="0.0000" max="5.0000" gui-text="Stroke width">0.1000</param>
<param name="stroke_units" gui-text="Units" type="optiongroup" appearance="combo">
<param name="main_tabs" type="notebook">
<page name="tab_active" gui-text="Cleanup Styles">
<param name="dedicated_style_attributes" gui-text="Handling of dedicated style attributes" gui-description="We delete dedicated attributes like 'fill' or 'stroke'. Please choose an option what should happen with those properties." type="optiongroup" appearance="combo">
<option value="prefer_composed">Catch dedicated, but prefer composed (master) style</option>
<option value="prefer_dedicated">Catch dedicated and prefer over composed (master) style</option>
<option value="ignore">Ignore dedicated</option>
</param>
<param name="stroke_width_override" type="bool" gui-text="Override stroke width">false</param>
<param name="stroke_width" type="float" precision="3" min="0.0000" max="5.000" gui-text="Stroke width">0.100</param>
<param name="stroke_width_units" gui-text="Units" type="optiongroup" appearance="combo">
<option value="px">px</option>
<option value="pt">pt</option>
<option value="in">in</option>
<option value="cm">cm</option>
<option value="mm">mm</option>
</param>
<param name="opacity" type="float" precision="1" min="0" max="100" gui-text="Opacity (%)">100.0</param>
<param name="reset_style_attributes" type="bool" gui-text="Reset stroke style attributes" gui-description="Remove stroke style attributes like stroke-dasharray, stroke-dashoffset, stroke-linejoin, linecap, stroke-miterlimit">true</param>
<param name="reset_fill_attributes" type="bool" gui-text="Reset fill style attributes" gui-description="Sets 'fill:none;' to style attribute">true</param>
<param name="stroke_opacity_override" type="bool" gui-text="Override stroke opacity">false</param>
<param name="stroke_opacity" type="float" precision="1" min="0.0" max="100.0" gui-text="Stroke opacity (%)">100.0</param>
<param name="reset_stroke_attributes" type="bool" gui-text="Reset stroke style attributes" gui-description="Remove stroke style attributes 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linejoin', 'stroke-linecap', 'stroke-miterlimit'">true</param>
<param name="reset_fill_attributes" type="bool" gui-text="Reset fill style attributes" gui-description="Sets 'fill:none;fill-opacity:1;' to style attribute">true</param>
<param name="apply_hairlines" type="bool" gui-text="Add additional hairline style" gui-description="Adds 'vector-effect:non-scaling-stroke;' and '-inkscape-stroke:hairline;' Hint: stroke-width is kept in background. All hairlines still have a valued width.">true</param>
<param name="apply_black_strokes" type="bool" gui-text="Apply black strokes where strokes missing" gui-description="Adds 'stroke:#000000;' to style attribute">true</param>
<param name="remove_group_styles" type="bool" gui-text="Remove styles from groups" gui-description="Remove style attributes from parent groups. So we have styles directly at the level of visivle nodes!">false</param>
<label>This extension works on current selection or for complete document</label>
</page>
<page name="tab_about" gui-text="About">
<label appearance="header">About</label>
<separator />
<label>Cleanup Styles by Mario Voigt / Stadtfabrikanten e.V. (2021)</label>
<label>This piece of software is part of the MightyScape for InkScape 1.0/1.1dev Extension Collection.</label>
<label>You found a bug or got some fresh code? Just report to mario.voigt@stadtfabrikanten.org. Thanks!</label>
<label appearance="url">https://fablabchemnitz.de</label>
<label>License: GNU GPL v3</label>
<separator />
<label>This extension generates inventory stickers for thermo printers (we use Brother QL-720NW) from our Teedy instance. Teedy is an open source software document management system (DMS). You can find the complete documentation at the Wiki space of https://fablabchemnitz.de</label>
</page>
</param>
<effect needs-live-preview="true">
<object-type>all</object-type>
<effects-menu>

View File

@ -16,23 +16,40 @@ 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
Based on coloreffect.py by Jos Hirth and Aaron C. Spike
Based on
- coloreffect.py by Jos Hirth and Aaron C. Spike
- cleanup.py https://github.com/attoparsec/inkscape-extensions by attoparsec
Author: Mario Voigt / FabLab Chemnitz
Mail: mario.voigt@stadtfabrikanten.org
Last Patch: 12.04.2021
License: GNU GPL v3
Notes:
- This extension does not check if attributes contain duplicates properties like "opacity:1;fill:#393834;fill-opacity:1;opacity:1;fill:#393834;fill-opacity:1". We assume the SVG syntax is correct
'''
import inkex
import re
class Cleanup(inkex.EffectExtension):
groups = []
def __init__(self):
inkex.Effect.__init__(self)
self.arg_parser.add_argument("--stroke_width", type=float, default=0.1, help="Stroke width")
self.arg_parser.add_argument("--stroke_units", default="mm", help="Stroke unit")
self.arg_parser.add_argument("--opacity", type=float, default="100.0", help="Opacity")
self.arg_parser.add_argument("--reset_style_attributes", type=inkex.Boolean, help="Remove stroke style attributes like stroke-dasharray, stroke-dashoffset, stroke-linejoin, linecap, stroke-miterlimit")
self.arg_parser.add_argument("--reset_fill_attributes", type=inkex.Boolean, help="Sets 'fill:none;' to style attribute")
self.arg_parser.add_argument("--main_tabs")
self.arg_parser.add_argument("--dedicated_style_attributes", default="ignore", help="Handling of dedicated style attributes")
self.arg_parser.add_argument("--stroke_width_override", type=inkex.Boolean, default=False, help="Override stroke width")
self.arg_parser.add_argument("--stroke_width", type=float, default=0.100, help="Stroke width")
self.arg_parser.add_argument("--stroke_width_units", default="mm", help="Stroke width unit")
self.arg_parser.add_argument("--stroke_opacity_override", type=inkex.Boolean, default=False, help="Override stroke opacity")
self.arg_parser.add_argument("--stroke_opacity", type=float, default="100.0", help="Stroke opacity (%)")
self.arg_parser.add_argument("--reset_stroke_attributes", type=inkex.Boolean, help="Remove stroke style attributes 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linejoin', 'stroke-linecap', 'stroke-miterlimit'")
self.arg_parser.add_argument("--reset_fill_attributes", type=inkex.Boolean, help="Sets 'fill:none;fill-opacity:1;' to style attribute")
self.arg_parser.add_argument("--apply_hairlines", type=inkex.Boolean, help="Adds 'vector-effect:non-scaling-stroke;' and '-inkscape-stroke:hairline;' Hint: stroke-width is kept in background. All hairlines still have a valued width.")
self.arg_parser.add_argument("--apply_black_strokes", type=inkex.Boolean, help="Adds 'stroke:#000000;' to style attribute")
self.arg_parser.add_argument("--remove_group_styles", type=inkex.Boolean, help="Remove styles from groups")
def effect(self):
if len(self.svg.selected) == 0:
@ -40,6 +57,11 @@ class Cleanup(inkex.EffectExtension):
else:
for id, node in self.svg.selected.items():
self.getAttribs(node)
#finally remove the styles from collected groups (if enabled)
if self.options.remove_group_styles is True:
for group in self.groups:
if group.attrib.has_key('style') is True:
group.attrib.pop('style')
def getAttribs(self, node):
self.changeStyle(node)
@ -48,28 +70,55 @@ class Cleanup(inkex.EffectExtension):
#stroke and fill styles can be included in style attribute or they can exist separately (can occure in older SVG files). We do not parse other attributes than style
def changeStyle(self, node):
nodeDict = []
nodeDict.append(inkex.addNS('line','svg'))
nodeDict.append(inkex.addNS('polyline','svg'))
nodeDict.append(inkex.addNS('polygon','svg'))
nodeDict.append(inkex.addNS('circle','svg'))
nodeDict.append(inkex.addNS('ellipse','svg'))
nodeDict.append(inkex.addNS('rect','svg'))
nodeDict.append(inkex.addNS('path','svg'))
nodeDict.append(inkex.addNS('g','svg'))
if node.tag in nodeDict:
if node.attrib.has_key('style'):
#we check/modify the style of all shapes (not groups)
if isinstance(node, inkex.ShapeElement) and not isinstance(node, inkex.Group):
# the final styles applied to this element (with influence from top level elements like groups)
composed_style = node.composed_style()
composedStyleAttributes = str(composed_style).split(';') #array
composedStyleAttributesDict = {}
if len(composed_style) > 0: #Style "composed_style" might contain just empty '' string which will lead to failing dict update
for composedStyleAttribute in composedStyleAttributes:
composedStyleAttributesDict.update({'{}'.format(composedStyleAttribute.split(':')[0]): composedStyleAttribute.split(':')[1]})
#three options to handle dedicated attributes (attributes not in the "style" attribute, but separate):
# - just delete all dedicated properties
# - merge dedicated properties, and prefer them over those from composed style
# - merge dedicated properties, but prefer properties from composed style
dedicatedStyleAttributesDict = {}
popDict = []
popDict.extend(['stroke', 'stroke-opacity', 'stroke-width', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'fill', 'fill-opacity'])
for popItem in popDict:
if node.attrib.has_key(str(popItem)):
dedicatedStyleAttributesDict.update({'{}'.format(popItem): node.get(popItem)})
node.attrib.pop(popItem)
#inkex.utils.debug("composedStyleAttributesDict = " + str(composedStyleAttributesDict))
#inkex.utils.debug("dedicatedStyleAttributesDict = " + str(dedicatedStyleAttributesDict))
if self.options.dedicated_style_attributes == 'prefer_dedicated':
composedStyleAttributesDict.update(dedicatedStyleAttributesDict)
node.set('style', composedStyleAttributesDict)
elif self.options.dedicated_style_attributes == 'prefer_composed':
dedicatedStyleAttributesDict.update(composedStyleAttributesDict)
node.set('style', dedicatedStyleAttributesDict)
elif self.options.dedicated_style_attributes == 'ignore':
pass
# now parse the style with possibly merged dedicated attributes modded style attribute (dedicatedStyleAttributes)
if node.attrib.has_key('style') is False:
node.set('style', 'stroke:#000000;') #we add basic stroke color black. We cannot set to empty value or just ";" because it will not update properly
style = node.get('style')
if style:
#add missing style attributes if required
if style.endswith(';') is False:
style += ';'
if re.search('(;|^)stroke:(.*?)(;|$)', style) is None: #if "stroke" is None, add one. We need to distinguish because there's also attribute "-inkscape-stroke" that's why we check starting with ^ or ;
style += 'stroke:none;'
if "stroke-width:" not in style:
style += 'stroke-width:{:1.4f};'.format(self.svg.unittouu(str(self.options.stroke_width) + self.options.stroke_units))
if "stroke-opacity:" not in style:
style += 'stroke-opacity:{:1.1f};'.format(self.options.opacity / 100)
if self.options.stroke_width_override is True and "stroke-width:" not in style:
style += 'stroke-width:{:1.4f};'.format(self.svg.unittouu(str(self.options.stroke_width) + self.options.stroke_width_units))
if self.options.stroke_opacity_override is True and "stroke-opacity:" not in style:
style += 'stroke-opacity:{:1.1f};'.format(self.options.stroke_opacity / 100)
if self.options.apply_hairlines is True:
if "vector-effect:non-scaling-stroke" not in style:
@ -87,13 +136,13 @@ class Cleanup(inkex.EffectExtension):
if len(parts) == 2:
(prop, val) = parts
prop = prop.strip().lower()
if prop == 'stroke-width':
new_val = self.svg.unittouu(str(self.options.stroke_width) + self.options.stroke_units)
if prop == 'stroke-width' and self.options.stroke_width_override is True:
new_val = self.svg.unittouu(str(self.options.stroke_width) + self.options.stroke_width_units)
declarations[i] = prop + ':{:1.4f}'.format(new_val)
if prop == 'stroke-opacity':
new_val = self.options.opacity / 100
if prop == 'stroke-opacity' and self.options.stroke_opacity_override is True:
new_val = self.options.stroke_opacity / 100
declarations[i] = prop + ':{:1.1f}'.format(new_val)
if self.options.reset_style_attributes is True:
if self.options.reset_stroke_attributes is True:
if prop == 'stroke-dasharray':
declarations[i] = ''
if prop == 'stroke-dashoffset':
@ -113,7 +162,14 @@ class Cleanup(inkex.EffectExtension):
if prop == 'fill':
new_val = 'none'
declarations[i] = prop + ':' + new_val
if prop == 'fill-opacity':
new_val = '1'
declarations[i] = prop + ':' + new_val
node.set('style', ';'.join(declarations))
# if element is group we add it to collection to remove it's style after parsing all selected items
elif isinstance(node, inkex.ShapeElement) and isinstance(node, inkex.Group):
self.groups.append(node)
if __name__ == '__main__':
Cleanup().run()

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<_name>Export selection as SVG</_name>
<id>fablabchemnitz.de.export_selection_as_svg</id>
<param name="wrap_transform" type="boolean" _gui-text="Wrap final document in transform">false</param>
<param name="export_dir" type="path" mode="folder" gui-text="Location to save exported documents">./inkscape_export/</param>
<effect needs-document="true" needs-live-preview="false">
<object-type>all</object-type>
<menu-tip>Export selection to separate SVG file.</menu-tip>
<effects-menu>
<submenu name="FabLab Chemnitz">
<submenu name="Import/Export/Transfer"/>
</submenu>
</effects-menu>
</effect>
<script>
<command reldir="inx" interpreter="python">export_selection.py</command>
</script>
</inkscape-extension>

View File

@ -0,0 +1,100 @@
# -*- coding: utf-8 -*-
from copy import deepcopy
from pathlib import Path
import logging
import math
import os
import inkex
import inkex.command
from lxml import etree
from scour.scour import scourString
logger = logging.getLogger(__name__)
GROUP_ID = 'export_selection_transform'
class ExportObject(inkex.EffectExtension):
def add_arguments(self, pars):
pars.add_argument("--wrap_transform", type=inkex.Boolean, default=False, help="Wrap final document in transform")
pars.add_argument("--export_dir", default="~/inkscape_export/", help="Location to save exported documents")
def effect(self):
if not self.svg.selected:
return
export_dir = Path(self.absolute_href(self.options.export_dir))
os.makedirs(export_dir, exist_ok=True)
bbox = inkex.BoundingBox()
for elem in self.svg.selected.values():
transform = inkex.Transform()
parent = elem.getparent()
if parent is not None and isinstance(parent, inkex.ShapeElement):
transform = parent.composed_transform()
try:
bbox += elem.bounding_box(transform)
except Exception:
logger.exception("Bounding box not computed")
logger.info("Skipping bounding box")
transform = elem.composed_transform()
x1, y1 = transform.apply_to_point([0, 0])
x2, y2 = transform.apply_to_point([1, 1])
bbox += inkex.BoundingBox((x1, x2), (y1, y2))
template = self.create_document()
filename = None
group = etree.SubElement(template, '{http://www.w3.org/2000/svg}g')
group.attrib['id'] = GROUP_ID
group.attrib['transform'] = str(inkex.Transform(((1, 0, -bbox.left), (0, 1, -bbox.top))))
for elem in self.svg.selected.values():
elem_copy = deepcopy(elem)
elem_copy.attrib['transform'] = str(elem.composed_transform())
group.append(elem_copy)
width = math.ceil(bbox.width)
height = math.ceil(bbox.height)
template.attrib['viewBox'] = f'0 0 {width} {height}'
template.attrib['width'] = f'{width}'
template.attrib['height'] = f'{height}'
if filename is None:
filename = elem.attrib.get('id', None)
if filename:
filename = filename.replace(os.sep, '_') + '.svg'
if not filename:
filename = 'element.svg'
template.append(group)
if not self.options.wrap_transform:
self.load(inkex.command.inkscape_command(template.tostring(), select=GROUP_ID, verbs=['SelectionUnGroup']))
template = self.svg
for child in template.getchildren():
if child.tag == '{http://www.w3.org/2000/svg}metadata':
template.remove(child)
self.save_document(template, export_dir / filename)
def create_document(self):
document = self.svg.copy()
for child in document.getchildren():
if child.tag == '{http://www.w3.org/2000/svg}defs':
continue
document.remove(child)
return document
def save_document(self, document, filename):
with open(filename, 'wb') as fp:
document = document.tostring()
fp.write(scourString(document).encode('utf8'))
if __name__ == '__main__':
ExportObject().run()

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Flip</name>
<id>fablabchemnitz.de.Flip</id>
<effect>
<object-type>path</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz">
<submenu name="Modify existing Path(s)"/>
</submenu>
</effects-menu>
</effect>
<script>
<command reldir="inx" interpreter="python">flip.py</command>
</script>
</inkscape-extension>

View File

@ -0,0 +1,38 @@
import math
from inkex import EffectExtension, PathElement, transforms as T
# https://en.wikipedia.org/wiki/Rotations_and_reflections_in_two_dimensions
def reflection_matrix(theta):
theta2 = 2 * theta
return [
[math.cos(theta2), math.sin(theta2), 0],
[math.sin(theta2), -math.cos(theta2), 0],
]
def svg_matrix_order(mat):
((a, c, e), (b, d, f)) = mat
return a, b, c, d, e, f
class FlipPath(EffectExtension):
"""Extension to flip a path about the line from the start to end node"""
def effect(self):
for node in self.svg.selection.filter(PathElement).values():
points = list(node.path.end_points)
if len(points) < 2 or points[0] == points[-1]:
continue
start = points[0]
end = points[-1]
v = end - start
theta = math.atan2(v.y, v.x)
# transforms go in reverse order
mat = T.Transform()
mat.add_translate(start)
mat.add_matrix(*svg_matrix_order(reflection_matrix(theta)))
mat.add_translate(-start)
node.path = node.path.transform(mat)
if __name__ == '__main__':
FlipPath().run()

View File

@ -25,7 +25,7 @@
<label appearance="header">About</label>
<separator />
<label>Inventory Stickers by Mario Voigt / Stadtfabrikanten e.V. (2021)</label>
<label>This piece of software is part of the MightyScape for InkScape 1.0/1.1dev Extension Collection</label>
<label>This piece of software is part of the MightyScape for InkScape 1.0/1.1dev Extension Collection.</label>
<label>you found a bug or got some fresh code? Just report to mario.voigt@stadtfabrikanten.org. Thanks!</label>
<label appearance="url">https://fablabchemnitz.de</label>
<label>License: GNU GPL v3</label>

View File

@ -39,19 +39,28 @@
<separator/>
<vbox>
<label appearance="header">Objects / Misc</label>
<hbox>
<vbox>
<!--<param name="svg" type="bool" gui-text="svg">true</param>-->
<!--<param name="sodipodi" type="bool" gui-text="sodipodi">true</param>-->
<param name="clipPath" type="bool" gui-text="clipPath">true</param>
<param name="defs" type="bool" gui-text="defs">true</param>
<param name="image" type="bool" gui-text="image">true</param>
<param name="mask" type="bool" gui-text="mask">true</param>
<param name="marker" type="bool" gui-text="marker">true</param>
<param name="metadata" type="bool" gui-text="metadata">true</param>
</vbox>
<spacer/>
<vbox>
<param name="pattern" type="bool" gui-text="pattern">true</param>
<param name="script" type="bool" gui-text="script">true</param>
<param name="switch" type="bool" gui-text="switch">true</param>
<param name="symbol" type="bool" gui-text="symbol">true</param>
<param name="use" type="bool" gui-text="use">true</param>
</vbox>
</hbox>
</vbox>
</hbox>
<separator/>
<param name="operationmode" type="optiongroup" appearance="combo" gui-text="Operation mode">
<option value="ungroup_only">Ungroup (flatten) only</option>

View File

@ -47,6 +47,7 @@ class MigrateGroups(inkex.Effect):
self.arg_parser.add_argument("--tspan", type=inkex.Boolean, default=True)
self.arg_parser.add_argument("--linearGradient", type=inkex.Boolean, default=True)
self.arg_parser.add_argument("--radialGradient", type=inkex.Boolean, default=True)
self.arg_parser.add_argument("--mask", type=inkex.Boolean, default=True)
self.arg_parser.add_argument("--meshGradient", type=inkex.Boolean, default=True)
self.arg_parser.add_argument("--meshRow", type=inkex.Boolean, default=True)
self.arg_parser.add_argument("--meshPatch", type=inkex.Boolean, default=True)
@ -54,6 +55,7 @@ class MigrateGroups(inkex.Effect):
self.arg_parser.add_argument("--script", type=inkex.Boolean, default=True)
self.arg_parser.add_argument("--symbol", type=inkex.Boolean, default=True)
self.arg_parser.add_argument("--stop", type=inkex.Boolean, default=True)
self.arg_parser.add_argument("--switch", type=inkex.Boolean, default=True)
self.arg_parser.add_argument("--use", type=inkex.Boolean, default=True)
self.arg_parser.add_argument("--flowRoot", type=inkex.Boolean, default=True)
self.arg_parser.add_argument("--flowRegion", type=inkex.Boolean, default=True)
@ -85,8 +87,10 @@ class MigrateGroups(inkex.Effect):
namespace.append("{http://www.w3.org/2000/svg}meshPatch") if self.options.meshPatch else ""
namespace.append("{http://www.w3.org/2000/svg}script") if self.options.script else ""
namespace.append("{http://www.w3.org/2000/svg}symbol") if self.options.symbol else ""
namespace.append("{http://www.w3.org/2000/svg}mask") if self.options.mask else ""
namespace.append("{http://www.w3.org/2000/svg}metadata") if self.options.metadata else ""
namespace.append("{http://www.w3.org/2000/svg}stop") if self.options.stop else ""
namespace.append("{http://www.w3.org/2000/svg}switch") if self.options.switch else ""
namespace.append("{http://www.w3.org/2000/svg}use") if self.options.use else ""
namespace.append("{http://www.w3.org/2000/svg}flowRoot") if self.options.flowRoot else ""
namespace.append("{http://www.w3.org/2000/svg}flowRegion") if self.options.flowRegion else ""

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Open closed Path</name>
<id>fablabchemnitz.de.openClosedPath</id>
<effect>
<effects-menu>
<submenu name="FabLab Chemnitz">
<submenu name="Modify existing Path(s)"/>
</submenu>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">openClosedPath.py</command>
</script>
</inkscape-extension>

View File

@ -0,0 +1,37 @@
#!/usr/bin/env python3
# coding=utf-8
#
# Copyright (C) 2020 Ellen Wasboe, ellen@wasbo.net
#
# 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
"""
Remove all z-s from all selected paths
My purpose: to open paths of single line fonts with a temporary closing to fit into .ttf/.otf format files
"""
import inkex, re
from inkex import PathElement
class OpenClosedPath(inkex.EffectExtension):
# Extension to open a closed path by z or by last node
def effect(self):
elements = self.svg.selection.filter(PathElement).values()
for elem in elements:
pp=elem.path.to_absolute() #remove transformation matrix
elem.path = re.sub(r"Z","",str(pp))
if __name__ == '__main__':
OpenClosedPath().run()

View File

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Parabola</name>
<id>fablabchemnitz.de.parabola</id>
<param name="tab" type="notebook">
<page name="common" gui-text="Basic Options">
<param name="height" type="int" min="100" max="1000" gui-text="Shape Height:">120</param>
<param name="width" type="int" min="100" max="1000" gui-text="Shape Width:">120</param>
<param name="seg_count" type="int" min="5" max="100" gui-text="Number of Line Segments:">25</param>
<param name="shape" type="optiongroup" appearance="combo" gui-text="Choose a Shape:">
<option value="cross">Cross</option>
<option value="square">Square</option>
<option value="triangle">Triangle</option></param>
<spacer/>
<label>v1.1.0</label>
</page>
<page name="corners" gui-text="Corners">
<param name="c1" type="bool" gui-text="Corner 1">true</param>
<param name="c2" type="bool" gui-text="Corner 2">true</param>
<param name="c3" type="bool" gui-text="Corner 3">true</param>
<param name="c4" type="bool" gui-text="Corner 4 *">true</param>
<label>* Corner 4 doesn't apply to the Triangle Shape</label>
</page>
</param>
<effect>
<object-type>all</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz">
<submenu name="Shape/Pattern from Generator"/>
</submenu>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">parabola.py</command>
</script>
</inkscape-extension>

View File

@ -0,0 +1,240 @@
#!/usr/bin/env python3
# coding=utf-8
#
# 2/27/2021 - v.1.1.0
# Copyright (C) 2021 Reginald Waters opensourcebear@nthebare.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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
"""
This extension renders a wireframe shape and then draws lines to form a parabola
shape.
The height and width are independently variable. The number of lines will change
the density of the end product.
"""
import inkex
from inkex import turtle as pturtle
class parabola(inkex.GenerateExtension):
container_label = 'Parabola'
def add_arguments(self, pars):
pars.add_argument("--height", type=int, default=300, help="Shape Height")
pars.add_argument("--width", type=int, default=300, help="Shape Width")
pars.add_argument("--seg_count", type=int, default=10, help="Number of line segments")
pars.add_argument("--shape", default="square")
pars.add_argument("--tab", default="common")
pars.add_argument("--c1", default="true")
pars.add_argument("--c2", default="false")
pars.add_argument("--c3", default="false")
pars.add_argument("--c4", default="false")
def generate(self):
# Let's simplify the variable names
ht = int(self.options.height)
wt = int(self.options.width)
sc = int(self.options.seg_count)
shape = self.options.shape
c1 = self.options.c1
c2 = self.options.c2
c3 = self.options.c3
c4 = self.options.c4
point = self.svg.namedview.center
style = inkex.Style({
'stroke-linejoin': 'miter', 'stroke-width': str(self.svg.unittouu('1px')),
'stroke-opacity': '1.0', 'fill-opacity': '1.0',
'stroke': '#000000', 'stroke-linecap': 'butt',
'fill': 'none'
})
# Setting the amount to move across the horizontal and vertical
increaseht = (ht / sc)
increasewt = (wt / sc)
tur = pturtle.pTurtle()
tur.pu() # Pen up
tur.setpos(point) # start in the center
if shape == "cross":
# We draw the cross shape and store the 4 points
# Can this be looped?
# Should I store the coordinates in an array/list?
tur.forward((ht / 2))
toppoint = tur.getpos()
if c3 == 'true' or c4 == 'true':
tur.pd()
tur.backward((ht / 2))
tur.pu()
if c1 == 'true' or c2 == 'true':
tur.pd()
tur.backward((ht / 2))
bottompoint = tur.getpos()
tur.pu()
tur.setpos(point)
tur.right(90)
tur.forward((wt / 2))
rightpoint = tur.getpos()
if c3 == 'true' or c2 == 'true':
tur.pd()
tur.backward((wt / 2))
tur.pu()
if c1 == 'true' or c4 == 'true':
tur.pd()
tur.backward((wt / 2))
leftpoint = tur.getpos()
while sc > 0:
if c1 == 'true':
# Drawing the SE Corner based on SW coordinates
# We always draw this corner
tur.pu()
tur.setpos((bottompoint[0], bottompoint[1] - ( (increaseht / 2) * sc ) ))
tur.pd()
tur.setpos((bottompoint[0] + ( (increasewt / 2) * sc ), bottompoint[1] - (ht / 2) ))
if c2 == 'true': # Drawing the SW Corner based on SE Coordinates
tur.pu()
tur.setpos((bottompoint[0], bottompoint[1] - ( (increaseht / 2) * sc ) ))
tur.pd()
tur.setpos((bottompoint[0] - ( (increasewt / 2) * sc ), bottompoint[1] - (ht / 2) ))
if c3 == 'true': # Drawing the NW Corner based on NE Coordinates
tur.pu()
tur.setpos((toppoint[0], toppoint[1] + ( (increaseht / 2) * sc ) ))
tur.pd()
tur.setpos((toppoint[0] - ( (increasewt / 2) * sc ), toppoint[1] + (ht / 2) ))
if c4 == 'true': # Drawing the NE Corner based on NW Coordinates
tur.pu()
tur.setpos((toppoint[0], toppoint[1] + ( (increaseht / 2) * sc ) ))
tur.pd()
tur.setpos((toppoint[0] + ( (increasewt / 2) * sc ), toppoint[1] + (ht / 2) ))
sc = sc - 1
if shape == "triangle":
# We draw the triangle and store the 3 corner points
# Loopable?
tur.backward((ht / 2))
tur.right(90)
tur.forward((wt /2))
cornera = tur.getpos()
if c3 == 'true' or c2 == 'true':
tur.pd()
tur.backward((wt))
cornerb = tur.getpos()
tur.pu()
if c2 == 'true' or c1 == 'true':
tur.pd()
tur.setpos((point[0], (cornera[1] - ht) ))
cornerc = tur.getpos()
tur.pu()
if c1 == 'true' or c3 == 'true':
tur.pd()
tur.setpos(cornera)
# So.. The math below took a lot of trial and error to figure out...
# I probably need to take some geography classes...
while sc > 0:
if c1 == 'true':
tur.pu()
tur.setpos(( (cornerb[0] + ((increasewt / 2) * (sc)) - (wt / 2)), cornerb[1] + (increaseht * sc) - ht ))
tur.pd()
tur.setpos(( (cornera[0] + (increasewt / 2) * (sc)), cornera[1] - (increaseht * sc) ))
if c2 == 'true':
tur.pu()
tur.setpos((cornerb[0] - (increasewt * sc ) , cornerb[1] ))
tur.pd()
tur.setpos(( (cornerb[0] + ((increasewt / 2) * sc) - (wt / 2)), cornerb[1] + (increaseht * sc) - ht ))
if c3 == 'true':
tur.pu()
tur.setpos((cornera[0] + (increasewt * sc ) , cornera[1] ))
tur.pd()
tur.setpos(( (cornera[0] - ((increasewt / 2) * sc) + (wt / 2)), cornera[1] + (increaseht * sc) - ht ))
sc = sc - 1
if shape == "square":
# We draw out the square shape and store the coordinates for each corner
# Can this be looped?
tur.right(90)
tur.forward((wt / 2))
tur.right(90)
tur.forward((ht / 2))
swcorner = tur.getpos()
if c4 == 'true' or c3 == 'true': # We only draw the 2 lines that are part of these corners
tur.pd() # Pen Down
tur.right(90)
tur.forward(wt)
secorner = tur.getpos()
tur.pu()
if c3 == 'true' or c2 == 'true': # We only draw the 2 lines that are part of these corners
tur.pd()
tur.right(90)
tur.forward(ht)
necorner = tur.getpos()
tur.pu()
if c1 == 'true' or c2 == 'true': # We only draw the 2 lines that are part of these corners
tur.pd()
tur.right(90)
tur.forward(wt)
nwcorner = tur.getpos()
tur.right(90)
tur.pu()
if c4 == 'true' or c1 == 'true': # We only draw the 2 lines that are part of these corners
tur.pd()
tur.forward(ht)
while sc > 0:
if c1 == 'true':
# Drawing the NW Corner based on SW coordinates
# We always draw this corner
tur.pu()
tur.setpos((swcorner[0], swcorner[1] - ( increaseht * sc ) ))
tur.pd()
tur.setpos((swcorner[0] + ( increasewt * sc ), swcorner[1] - ht))
if c2 == 'true': # Drawing the NE Corner based on SE Coordinates
tur.pu()
tur.setpos((secorner[0], secorner[1] - ( increaseht * sc ) ))
tur.pd()
tur.setpos((secorner[0] - ( increasewt * sc ), secorner[1] - ht))
if c3 == 'true': # Drawing the SE Corner based on NE Coordinates
tur.pu()
tur.setpos((necorner[0], necorner[1] + ( increaseht * sc ) ))
tur.pd()
tur.setpos((necorner[0] - ( increasewt * sc ), necorner[1] + ht))
if c4 == 'true': # Drawing the SW Corner based on NW Coordinates
tur.pu()
tur.setpos((nwcorner[0], nwcorner[1] + ( increaseht * sc ) ))
tur.pd()
tur.setpos((nwcorner[0] + ( increasewt * sc ), nwcorner[1] + ht))
sc = sc - 1
return inkex.PathElement(d=tur.getPath(), style=str(style))
if __name__ == "__main__":
# execute only if run as a script
parabola().run()

View File

@ -206,7 +206,7 @@ class PathOps(inkex.Effect):
def get_selected_ids(self):
"""Return a list of valid ids for inkscape path operations."""
id_list = []
if not len(self.svg.selected):
if len(self.svg.selected) == 0:
pass
else:
# level = 0: unlimited recursion into groups
@ -346,14 +346,14 @@ class PathOps(inkex.Effect):
defs = get_defs(self.document.getroot())
inkscape_tagrefs = defs.findall(
"inkscape:tag/inkscape:tagref", namespaces=inkex.NSS)
return True if len(inkscape_tagrefs) else False
return len(inkscape_tagrefs) > 0
def update_tagrefs(self, mode='purge'):
"""Check tagrefs for deleted objects."""
defs = get_defs(self.document.getroot())
inkscape_tagrefs = defs.findall(
"inkscape:tag/inkscape:tagref", namespaces=inkex.NSS)
if len(inkscape_tagrefs):
if len(inkscape_tagrefs) > 0:
for tagref in inkscape_tagrefs:
href = tagref.get(inkex.addNS('href', 'xlink'))[1:]
if self.svg.getElementById(href) is None:

View File

@ -20,8 +20,11 @@
<param name="decimals" type="int" min="0" max="10" gui-text="Decimal tolerance" gui-description="The more decimals the more distinct layers you will get. This only applies for the sub layers (threshold > 1)">1</param>
<param name="apply_transformations" type="bool" gui-text="Apply transformations (requires separate extension)" gui-description="This will call the extension 'Apply Transformations'. Helps avoiding geometry shifting">false</param>
<param name="cleanup" type="bool" gui-text="Cleanup all unused groups/layers (requires separate extension)" gui-description="This will call the extension 'Remove Empty Groups' if available">true</param>
<label>This extension will re-layer your selected items or the whole document according to their style (stroke or fill)</label>
<param name="put_unfiltered" type="bool" gui-text="Put unfiltered elements to a separate layer">false</param>
<param name="show_info" type="bool" gui-text="Show elements which have no style attributes to filter">false</param>
<spacer/>
<label>This extension will re-layer your selected items or the whole document according to their style values (stroke or fill).</label>
<label>The filtering applies only to style attribute of the elements. It does not filter for stroke or fill if they are set separately. You can use the separate 'Cleanup Styles' extension to migrate these separated attributes into style attribute.</label>
<label>Tinkered by Mario Voigt / Stadtfabrikanten e.V. (2020)</label>
<label appearance="url">https://fablabchemnitz.de</label>
<effect>

View File

@ -8,7 +8,7 @@ Features
Author: Mario Voigt / FabLab Chemnitz
Mail: mario.voigt@stadtfabrikanten.org
Date: 19.08.2020
Last patch: 05.04.2021
Last patch: 11.04.2021
License: GNU GPL v3
"""
import inkex
@ -40,23 +40,17 @@ class StylesToLayers(inkex.EffectExtension):
return layer
def __init__(self):
inkex.Effect.__init__(self)
inkex.EffectExtension.__init__(self)
self.arg_parser.add_argument("--apply_transformations", type=inkex.Boolean, default=False, help="Run 'Apply Transformations' extension before running vpype. Helps avoiding geometry shifting")
self.arg_parser.add_argument("--separateby", default = "stroke", help = "Separate by")
self.arg_parser.add_argument("--parsecolors",default = "hexval", help = "Sort colors by")
self.arg_parser.add_argument("--subdividethreshold", type=int, default = 1, help = "Threshold for splitting into sub layers")
self.arg_parser.add_argument("--decimals", type=int, default = 1, help = "Decimal tolerance")
self.arg_parser.add_argument("--cleanup", type=inkex.Boolean, default = True, help = "Decimal tolerance")
self.arg_parser.add_argument("--put_unfiltered", type=inkex.Boolean, default = False, help = "Put unfiltered elements to a separate layer")
self.arg_parser.add_argument("--show_info", type=inkex.Boolean, default = False, help = "Show elements which have no style attributes to filter")
def effect(self):
applyTransformAvailable = False # at first we apply external extension
try:
import applytransform
applyTransformAvailable = True
except Exception as e:
# inkex.utils.debug(e)
inkex.utils.debug("Calling 'Apply Transformations' extension failed. Maybe the extension is not installed. You can download it from official InkScape Gallery. Skipping ...")
def colorsort(stroke_value): #this function applies to stroke or fill (hex colors)
if self.options.parsecolors == "hexval":
@ -69,6 +63,14 @@ class StylesToLayers(inkex.EffectExtension):
return float(Color(stroke_value).to_hsl()[2])
return None
applyTransformAvailable = False # at first we apply external extension
try:
import applytransform
applyTransformAvailable = True
except Exception as e:
# inkex.utils.debug(e)
inkex.utils.debug("Calling 'Apply Transformations' extension failed. Maybe the extension is not installed. You can download it from official InkScape Gallery. Skipping ...")
layer_name = None
layerNodeList = [] #list with layer, neutral_value, element and self.options.separateby type
selected = [] #list of items to parse
@ -80,8 +82,13 @@ class StylesToLayers(inkex.EffectExtension):
selected = self.svg.selected.values()
for element in selected:
# additional option to apply transformations. As we clear up some groups to form new layers, we might lose translations, rotations, etc.
if self.options.apply_transformations is True and applyTransformAvailable is True:
applytransform.ApplyTransform().recursiveFuseTransform(element)
if isinstance(element, inkex.ShapeElement): # Elements which have a visible representation on the canvas (even without a style attribute but by their type); if we do not use that ifInstance Filter we provokate unkown InkScape fatal crashes
style = element.get('style')
if style is not None:
#if no style attributes or stroke/fill are set as extra attribute
@ -181,6 +188,17 @@ class StylesToLayers(inkex.EffectExtension):
layerNodeList.append([self.createLayer(layerNodeList, layer_name), neutral_value, element, self.options.separateby])
else:
layerNodeList.append([currentLayer, neutral_value, element, self.options.separateby]) #layer is existent. append items to this later
else: #if no style attribute in element and not a group
if isinstance(element, inkex.Group) is False:
if self.options.show_info:
inkex.utils.debug(element.get('id') + ' has no style attribute')
if self.options.put_unfiltered:
layer_name = 'without-style-attribute'
currentLayer = self.findLayer(layer_name)
if currentLayer is None: #layer does not exist, so create a new one
layerNodeList.append([self.createLayer(layerNodeList, layer_name), None, element, None])
else:
layerNodeList.append([currentLayer, None, element, None]) #layer is existent. append items to this later
contentlength = 0 #some counter to track if there are layers inside or if it is just a list with empty children
for layerNode in layerNodeList:

View File

@ -340,8 +340,8 @@ class vpypetools (inkex.EffectExtension):
# save the vpype document to new svg file and close it afterwards
output_file = self.options.input_file + ".vpype.svg"
output_fileIO = open(output_file, "w", encoding="utf-8")
vpype.write_svg(output_fileIO, doc, page_size=None, center=False, source_string='', layer_label_format='%d', show_pen_up=self.options.output_trajectories, color_mode='layer', single_path = True)
#vpype.write_svg(output_fileIO, doc, page_size=None, center=False, source_string='', layer_label_format='%d', show_pen_up=self.options.output_trajectories, color_mode='layer')
#vpype.write_svg(output_fileIO, doc, page_size=None, center=False, source_string='', layer_label_format='%d', show_pen_up=self.options.output_trajectories, color_mode='layer', single_path = True)
vpype.write_svg(output_fileIO, doc, page_size=None, center=False, source_string='', layer_label_format='%d', show_pen_up=self.options.output_trajectories, color_mode='layer')
#vpype.write_svg(output_fileIO, doc, page_size=(self.svg.unittouu(self.document.getroot().get('width')), self.svg.unittouu(self.document.getroot().get('height'))), center=False, source_string='', layer_label_format='%d', show_pen_up=self.options.output_trajectories, color_mode='layer')
output_fileIO.close()