2020-08-22 23:18:11 +02:00
#!/usr/bin/env python3
"""
Extension for InkScape 1.0
Features
- filters the current selection or the whole document for fill or stroke style . Each style will be put onto it ' s own layer. This way you can devide elements by their colors.
Author : Mario Voigt / FabLab Chemnitz
Mail : mario . voigt @stadtfabrikanten.org
Date : 19.08 .2020
2021-04-12 00:06:58 +02:00
Last patch : 11.04 .2021
2020-08-22 23:18:11 +02:00
License : GNU GPL v3
"""
import inkex
import re
from lxml import etree
import math
from operator import itemgetter
from inkex . colors import Color
2021-04-05 21:47:05 +02:00
class StylesToLayers ( inkex . EffectExtension ) :
2020-08-22 23:18:11 +02:00
def findLayer ( self , layerName ) :
svg_layers = self . document . xpath ( ' //svg:g[@inkscape:groupmode= " layer " ] ' , namespaces = inkex . NSS )
for layer in svg_layers :
#inkex.utils.debug(str(layer.get('inkscape:label')) + " == " + layerName)
if layer . get ( ' inkscape:label ' ) == layerName :
return layer
return None
def createLayer ( self , layerNodeList , layerName ) :
svg = self . document . xpath ( ' //svg:svg ' , namespaces = inkex . NSS ) [ 0 ]
for layer in layerNodeList :
#inkex.utils.debug(str(layer[0].get('inkscape:label')) + " == " + layerName)
if layer [ 0 ] . get ( ' inkscape:label ' ) == layerName :
return layer [ 0 ] #already exists. Do not create duplicate
layer = etree . SubElement ( svg , ' g ' )
layer . set ( inkex . addNS ( ' label ' , ' inkscape ' ) , ' %s ' % layerName )
layer . set ( inkex . addNS ( ' groupmode ' , ' inkscape ' ) , ' layer ' )
return layer
2021-04-15 17:03:47 +02:00
def add_arguments ( self , pars ) :
pars . add_argument ( " --apply_transformations " , type = inkex . Boolean , default = False , help = " Run ' Apply Transformations ' extension before running vpype. Helps avoiding geometry shifting " )
pars . add_argument ( " --separateby " , default = " stroke " , help = " Separate by " )
pars . add_argument ( " --parsecolors " , default = " hexval " , help = " Sort colors by " )
pars . add_argument ( " --subdividethreshold " , type = int , default = 1 , help = " Threshold for splitting into sub layers " )
pars . add_argument ( " --decimals " , type = int , default = 1 , help = " Decimal tolerance " )
pars . add_argument ( " --cleanup " , type = inkex . Boolean , default = True , help = " Decimal tolerance " )
pars . add_argument ( " --put_unfiltered " , type = inkex . Boolean , default = False , help = " Put unfiltered elements to a separate layer " )
pars . add_argument ( " --show_info " , type = inkex . Boolean , default = False , help = " Show elements which have no style attributes to filter " )
2020-08-22 23:18:11 +02:00
def effect ( self ) :
def colorsort ( stroke_value ) : #this function applies to stroke or fill (hex colors)
if self . options . parsecolors == " hexval " :
return float ( int ( stroke_value [ 1 : ] , 16 ) )
elif self . options . parsecolors == " hue " :
return float ( Color ( stroke_value ) . to_hsl ( ) [ 0 ] )
elif self . options . parsecolors == " saturation " :
return float ( Color ( stroke_value ) . to_hsl ( ) [ 1 ] )
elif self . options . parsecolors == " luminance " :
return float ( Color ( stroke_value ) . to_hsl ( ) [ 2 ] )
return None
2021-04-12 00:06:58 +02:00
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 ... " )
2020-08-22 23:18:11 +02:00
layer_name = None
layerNodeList = [ ] #list with layer, neutral_value, element and self.options.separateby type
selected = [ ] #list of items to parse
if len ( self . svg . selected ) == 0 :
for element in self . document . getroot ( ) . iter ( " * " ) :
selected . append ( element )
else :
selected = self . svg . selected . values ( )
for element in selected :
2021-04-12 00:06:58 +02:00
# additional option to apply transformations. As we clear up some groups to form new layers, we might lose translations, rotations, etc.
2021-04-05 21:47:05 +02:00
if self . options . apply_transformations is True and applyTransformAvailable is True :
applytransform . ApplyTransform ( ) . recursiveFuseTransform ( element )
2021-04-12 00:06:58 +02:00
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
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 ' )
2021-04-05 21:47:05 +02:00
2021-04-12 00:06:58 +02:00
# 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 + " ; "
#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 )
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 "
2021-04-05 21:47:05 +02:00
else :
2021-04-12 00:06:58 +02:00
inkex . utils . debug ( " No proper option selected. " )
exit ( 1 )
2021-04-05 21:47:05 +02:00
2021-04-12 00:06:58 +02:00
if neutral_value is not None : #apply decimals filter
neutral_value = float ( round ( neutral_value , self . options . decimals ) )
2021-04-05 21:47:05 +02:00
2021-04-12 00:06:58 +02:00
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
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
2020-08-22 23:18:11 +02:00
2020-09-10 09:47:24 +02:00
contentlength = 0 #some counter to track if there are layers inside or if it is just a list with empty children
2020-08-22 23:18:11 +02:00
for layerNode in layerNodeList :
2020-12-02 18:39:31 +01:00
try : #some nasty workaround. Make better code
layerNode [ 0 ] . append ( layerNode [ 2 ] ) #append element to created layer
if layerNode [ 1 ] is not None : contentlength + = 1 #for each found layer we increment +1
except :
continue
2020-08-22 23:18:11 +02:00
2021-04-05 21:47:05 +02:00
# 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 ] )
2020-08-22 23:18:11 +02:00
# Additionally if threshold was defined re-arrange the previously created layers by putting them into sub layers
2020-09-10 09:47:24 +02:00
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
2020-08-22 23:18:11 +02:00
#disabled sorting because it can return NoneType values which will kill the algorithm
#layerNodeList.sort(key=itemgetter(1)) #sort by neutral values from min to max to put them with ease into parent layers
topLevelLayerNodeList = [ ] #list with new layers and sub layers (mapping)
minmax_range = [ ]
for layerNode in layerNodeList :
if layerNode [ 1 ] is not None :
if layerNode [ 1 ] not in minmax_range :
minmax_range . append ( layerNode [ 1 ] ) #get neutral_value
if len ( minmax_range ) > = 3 : #if there are less than 3 distinct values a sub-layering will make no sense
#adjust the subdividethreshold if there are less layers than division threshold value dictates
if len ( minmax_range ) - 1 < self . options . subdividethreshold :
self . options . subdividethreshold = len ( minmax_range ) - 1
#calculate the sub layer slices (sub ranges)
minval = min ( minmax_range )
maxval = max ( minmax_range )
sliceinterval = ( maxval - minval ) / self . options . subdividethreshold
#inkex.utils.debug("neutral values (sorted) = " + str(minmax_range))
#inkex.utils.debug("min neutral_value = " + str(minval))
#inkex.utils.debug("max neutral_value = " + str(maxval))
#inkex.utils.debug("slice value (divide step size) = " + str(sliceinterval))
#inkex.utils.debug("subdivides (parent layers) = " + str(self.options.subdividethreshold))
for layerNode in layerNodeList :
for x in range ( 0 , self . options . subdividethreshold ) : #loop through the sorted neutral_values and determine to which layer they should belong
if layerNode [ 1 ] is None :
layer_name = str ( layerNode [ 3 ] ) + " #parent:unfilterable "
currentLayer = self . findLayer ( layer_name )
if currentLayer is None : #layer does not exist, so create a new one
topLevelLayerNodeList . append ( [ self . createLayer ( topLevelLayerNodeList , layer_name ) , layerNode [ 0 ] ] )
else :
topLevelLayerNodeList . append ( [ currentLayer , layerNode [ 0 ] ] ) #layer is existent. append items to this later
break
else :
layer_name = str ( layerNode [ 3 ] ) + " #parent " + str ( x + 1 )
currentLayer = self . findLayer ( layer_name )
#value example for arranging:
#min neutral_value = 0.07
#max neutral_value = 2.50
#slice value = 0.81
#subdivides = 3
#
#that finally should generate:
# layer #1: (from 0.07) to (0.07 + 0.81 = 0.88)
# layer #2: (from 0.88) to (0.88 + 0.81 = 1.69)
# layer #3: (from 1.69) to (1.69 + 0.81 = 2.50)
#
#now check layerNode[1] (neutral_value) and sort it into the correct layer
if ( layerNode [ 1 ] > = minval + sliceinterval * x ) and ( layerNode [ 1 ] < = minval + sliceinterval + sliceinterval * x ) :
if currentLayer is None : #layer does not exist, so create a new one
topLevelLayerNodeList . append ( [ self . createLayer ( topLevelLayerNodeList , layer_name ) , layerNode [ 0 ] ] )
else :
topLevelLayerNodeList . append ( [ currentLayer , layerNode [ 0 ] ] ) #layer is existent. append items to this later
break
#finally append the sublayers to the slices
#for layer in topLevelLayerNodeList:
#inkex.utils.debug(layer[0].get('inkscape:label'))
#inkex.utils.debug(layer[1])
for newLayerNode in topLevelLayerNodeList :
newLayerNode [ 0 ] . append ( newLayerNode [ 1 ] ) #append newlayer to layer
2020-08-31 21:25:41 +02:00
if self . options . cleanup == True :
try :
2020-09-07 17:37:59 +02:00
import cleangroups
cleangroups . CleanGroups . effect ( self )
2020-08-31 21:25:41 +02:00
except :
2021-04-05 21:47:05 +02:00
inkex . utils . debug ( " Calling ' Remove Empty Groups ' extension failed. Maybe the extension is not installed. You can download it from official InkScape Gallery. Skipping ... " )
2020-08-22 23:18:11 +02:00
2020-08-31 21:25:41 +02:00
if __name__ == ' __main__ ' :
2020-09-10 09:47:24 +02:00
StylesToLayers ( ) . run ( )