Added applytransforms option to Styles To Layers; fixed layers being

children of other layers; added hairline option
This commit is contained in:
Mario Voigt 2021-04-05 21:47:05 +02:00
parent 0c3f0009a1
commit 93a1009130
2 changed files with 119 additions and 85 deletions

View File

@ -5,6 +5,7 @@
<param name="separateby" gui-text="Separate by" type="optiongroup" appearance="combo">
<option value="stroke">Stroke color</option>
<option value="stroke_width">Stroke width</option>
<option value="stroke_hairline">Stroke hairline</option>
<option value="stroke_opacity">Stroke opacity</option>
<option value="fill">Fill color</option>
<option value="fill_opacity">Fill opacity</option>
@ -15,9 +16,10 @@
<option value="saturation">Saturation</option>
<option value="luminance">Luminance</option>
</param>
<param name="subdividethreshold" gui-text="Number of sub layers" gui-description="A min/max range of the selected style type value will be calculated and you retrieve a set of layer (coarse grouping) with sub-layers (fine grouping). If you have less calculated sub layers than this threshold it will be limited automatically." type="int" min="1" max="9999">1</param>
<param name="decimals" 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)" type="int" min="0" max="10">1</param>
<param name="cleanup" gui-text="Cleanup all unused groups (requires separate extension)" type="boolean" gui-description="This will call the extension 'Remove Empty Groups' if available">true</param>
<param name="subdividethreshold" type="int" min="1" max="9999" gui-text="Number of sub layers" gui-description="A min/max range of the selected style type value will be calculated and you retrieve a set of layer (coarse grouping) with sub-layers (fine grouping). If you have less calculated sub layers than this threshold it will be limited automatically.">1</param>
<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>
<label>Tinkered by Mario Voigt / Stadtfabrikanten e.V. (2020)</label>

View File

@ -8,7 +8,7 @@ Features
Author: Mario Voigt / FabLab Chemnitz
Mail: mario.voigt@stadtfabrikanten.org
Date: 19.08.2020
Last patch: 10.09.2020
Last patch: 05.04.2021
License: GNU GPL v3
"""
import inkex
@ -18,7 +18,7 @@ import math
from operator import itemgetter
from inkex.colors import Color
class StylesToLayers(inkex.Effect):
class StylesToLayers(inkex.EffectExtension):
def findLayer(self, layerName):
svg_layers = self.document.xpath('//svg:g[@inkscape:groupmode="layer"]', namespaces=inkex.NSS)
@ -41,6 +41,7 @@ class StylesToLayers(inkex.Effect):
def __init__(self):
inkex.Effect.__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")
@ -48,6 +49,14 @@ class StylesToLayers(inkex.Effect):
self.arg_parser.add_argument("--cleanup", type=inkex.Boolean, default = True, help = "Decimal tolerance")
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":
@ -71,89 +80,107 @@ class StylesToLayers(inkex.Effect):
selected = self.svg.selected.values()
for element in selected:
if self.options.apply_transformations is True and applyTransformAvailable is True:
applytransform.ApplyTransform().recursiveFuseTransform(element)
style = element.get('style')
#if no style attributes or stroke/fill are set as extra attribute
stroke = element.get('stroke')
stroke_width = element.get('stroke-width')
stroke_opacity = element.get('stroke-opacity')
fill = element.get('fill')
fill_opacity = element.get('fill-opacity')
# possible values for fill are #HEXCOLOR (like #000000), color name (like purple, black, red) or gradients (URLs)
neutral_value = None #we will use this value to slice the filter result into sub layers (threshold)
if fill is not None:
style = 'fill:'+ fill + ";"
if stroke is not None:
style = style + 'stroke:' + stroke + ";"
if style is not None:
#if no style attributes or stroke/fill are set as extra attribute
stroke = element.get('stroke')
stroke_width = element.get('stroke-width')
stroke_opacity = element.get('stroke-opacity')
fill = element.get('fill')
fill_opacity = element.get('fill-opacity')
#we don't want to destroy elements with gradients (they contain svg:stop elements which have a style too) and we don't want to mess with tspans (text)
if style and element.tag != inkex.addNS('stop','svg') and element.tag != inkex.addNS('tspan','svg'):
if self.options.separateby == "stroke":
stroke = re.search('stroke:(.*?)(;|$)', style)
if stroke is not None:
stroke = stroke[0]
stroke_value = stroke.split("stroke:")[1].split(";")[0]
if stroke_value != "none":
stroke_converted = str(Color(stroke_value).to_rgb()) #the color can be hex code or clear name. we handle both the same
neutral_value = colorsort(stroke_converted)
layer_name = "stroke-" + self.options.parsecolors + "-" + stroke_converted
else:
layer_name = "stroke-" + self.options.parsecolors + "-none"
elif self.options.separateby == "stroke_width":
stroke_width = re.search('stroke-width:(.*?)(;|$)', style)
if stroke_width is not None:
stroke_width = stroke_width[0]
neutral_value = self.svg.unittouu(stroke_width.split("stroke-width:")[1].split(";")[0])
layer_name = stroke_width
else:
layer_name = "stroke-width-none"
elif self.options.separateby == "stroke_opacity":
stroke_opacity = re.search('stroke-opacity:(.*?)(;|$)', style)
if stroke_opacity is not None:
stroke_opacity = stroke_opacity[0]
neutral_value = float(stroke_opacity.split("stroke-opacity:")[1].split(";")[0])
layer_name = stroke_opacity
else:
layer_name = "stroke-opacity-none"
elif self.options.separateby == "fill":
fill = re.search('fill:(.*?)(;|$)', style)
if fill is not None:
fill = fill[0]
fill_value = fill.split("fill:")[1].split(";")[0]
#check if the fill color is a real color or a gradient. if it's a gradient we skip the element
if fill_value != "none" and "url" not in fill_value:
fill_converted = str(Color(fill_value).to_rgb()) #the color can be hex code or clear name. we handle both the same
neutral_value = colorsort(fill_converted)
layer_name = "fill-" + self.options.parsecolors + "-" + fill_converted
elif "url" in fill_value: #okay we found a gradient. we put it to some group
layer_name = "fill-" + self.options.parsecolors + "-gradient"
else:
layer_name = "fill-" + self.options.parsecolors + "-none"
elif self.options.separateby == "fill_opacity":
fill_opacity = re.search('fill-opacity:(.*?)(;|$)', style)
if fill_opacity is not None:
fill_opacity = fill_opacity[0]
neutral_value = float(fill_opacity.split("fill-opacity:")[1].split(";")[0])
layer_name = fill_opacity
else:
layer_name = "fill-opacity-none"
else:
inkex.utils.debug("No proper option selected.")
exit(1)
# possible values for fill are #HEXCOLOR (like #000000), color name (like purple, black, red) or gradients (URLs)
neutral_value = None #we will use this value to slice the filter result into sub layers (threshold)
if fill is not None:
style = 'fill:'+ fill + ";"
if stroke is not None:
style = style + 'stroke:' + stroke + ";"
if neutral_value is not None: #apply decimals filter
neutral_value = float(round(neutral_value, self.options.decimals))
if layer_name is not None:
layer_name = layer_name.split(";")[0] #cut off existing semicolons to avoid duplicated layers with/without semicolon
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), neutral_value, element, self.options.separateby])
#we don't want to destroy elements with gradients (they contain svg:stop elements which have a style too) and we don't want to mess with tspans (text)
#the Styles to Layers extension still might brick the gradients (some tests failed)
if style and element.tag != inkex.addNS('stop','svg') and element.tag != inkex.addNS('tspan','svg'):
if self.options.separateby == "stroke":
stroke = re.search('^stroke:(.*?)(;|$)', style) #we use ^ to exlucde "-inkscape-stroke" which can be "hairline" since InkScape 1.1+
if stroke is not None:
stroke = stroke[0]
stroke_value = stroke.split("stroke:")[1].split(";")[0]
if stroke_value != "none":
stroke_converted = str(Color(stroke_value).to_rgb()) #the color can be hex code or clear name. we handle both the same
neutral_value = colorsort(stroke_converted)
layer_name = "stroke-" + self.options.parsecolors + "-" + stroke_converted
else:
layer_name = "stroke-" + self.options.parsecolors + "-none"
elif self.options.separateby == "stroke_width":
stroke_width = re.search('stroke-width:(.*?)(;|$)', style)
if stroke_width is not None:
stroke_width = stroke_width[0]
neutral_value = self.svg.unittouu(stroke_width.split("stroke-width:")[1].split(";")[0])
layer_name = stroke_width
else:
layer_name = "stroke-width-none"
elif self.options.separateby == "stroke_hairline":
stroke_hairline = re.search('-inkscape-stroke:hairline(;|$)', style)
if stroke_hairline is not None:
neutral_value = 1
layer_name = "stroke-hairline-yes"
else:
neutral_value = 0
layer_name = "stroke-hairline-no"
elif self.options.separateby == "stroke_opacity":
stroke_opacity = re.search('stroke-opacity:(.*?)(;|$)', style)
if stroke_opacity is not None:
stroke_opacity = stroke_opacity[0]
neutral_value = float(stroke_opacity.split("stroke-opacity:")[1].split(";")[0])
layer_name = stroke_opacity
else:
layer_name = "stroke-opacity-none"
elif self.options.separateby == "fill":
fill = re.search('fill:(.*?)(;|$)', style)
if fill is not None:
fill = fill[0]
fill_value = fill.split("fill:")[1].split(";")[0]
#check if the fill color is a real color or a gradient. if it's a gradient we skip the element
if fill_value != "none" and "url" not in fill_value:
fill_converted = str(Color(fill_value).to_rgb()) #the color can be hex code or clear name. we handle both the same
neutral_value = colorsort(fill_converted)
layer_name = "fill-" + self.options.parsecolors + "-" + fill_converted
elif "url" in fill_value: #okay we found a gradient. we put it to some group
layer_name = "fill-" + self.options.parsecolors + "-gradient"
else:
layer_name = "fill-" + self.options.parsecolors + "-none"
elif self.options.separateby == "fill_opacity":
fill_opacity = re.search('fill-opacity:(.*?)(;|$)', style)
if fill_opacity is not None:
fill_opacity = fill_opacity[0]
neutral_value = float(fill_opacity.split("fill-opacity:")[1].split(";")[0])
layer_name = fill_opacity
else:
layer_name = "fill-opacity-none"
else:
layerNodeList.append([currentLayer, neutral_value, element, self.options.separateby]) #layer is existent. append items to this later
inkex.utils.debug("No proper option selected.")
exit(1)
if neutral_value is not None: #apply decimals filter
neutral_value = float(round(neutral_value, self.options.decimals))
if layer_name is not None:
layer_name = layer_name.split(";")[0] #cut off existing semicolons to avoid duplicated layers with/without semicolon
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), neutral_value, element, self.options.separateby])
else:
layerNodeList.append([currentLayer, neutral_value, element, self.options.separateby]) #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:
@ -163,6 +190,11 @@ class StylesToLayers(inkex.Effect):
except:
continue
# we do some cosmetics with layers. Sometimes it can happen that one layer includes another. We don't want that. We move all layers to the top level
for newLayerNode in layerNodeList:
self.document.getroot().append(newLayerNode[0])
# Additionally if threshold was defined re-arrange the previously created layers by putting them into sub layers
if self.options.subdividethreshold > 1 and contentlength > 0: #check if we need to subdivide and if there are items we could rearrange into sub layers
@ -236,7 +268,7 @@ class StylesToLayers(inkex.Effect):
import cleangroups
cleangroups.CleanGroups.effect(self)
except:
inkex.utils.debug("Calling 'Remove Empty Groups' extension failed. Maybe the extension is not installed. You can download it from official InkScape Gallery.")
inkex.utils.debug("Calling 'Remove Empty Groups' extension failed. Maybe the extension is not installed. You can download it from official InkScape Gallery. Skipping ...")
if __name__ == '__main__':
StylesToLayers().run()