2020-08-13 01:28:19 +02:00
#!/usr/bin/env python3
"""
Extension for InkScape 1.0
2020-08-23 14:08:11 +02:00
This extension parses the selection and will put all elements into one single group . If you have a cluster with lots of groups and elements you will clean up this way ( one top level group , all elements below it ) . If you select a single element or a set of elements you just wrap it like using CTRL + G ( like making a usual group ) . You can also use this extension to filter out unwanted SVG elements at all .
2020-08-13 01:28:19 +02:00
Author : Mario Voigt / FabLab Chemnitz
Mail : mario . voigt @stadtfabrikanten.org
Date : 13.08 .2020
2020-09-13 00:26:22 +02:00
Last Patch : 13.09 .2020
2020-08-13 01:28:19 +02:00
License : GNU GPL v3
"""
import inkex
2020-09-13 00:26:22 +02:00
import os
2021-05-15 15:04:22 +02:00
import sys
2020-08-23 14:08:11 +02:00
from lxml import etree
2020-08-13 01:28:19 +02:00
2021-05-15 15:04:22 +02:00
sys . path . append ( " ../cleangroups " )
sys . path . append ( " ../applytransform " )
2021-04-19 20:54:38 +02:00
class MigrateGroups ( inkex . EffectExtension ) :
2020-08-13 01:28:19 +02:00
2020-08-30 12:30:35 +02:00
allElements = [ ] #list of all (sub)elements to process within selection
2020-08-31 21:25:41 +02:00
allGroups = [ ] #list of all groups (svg:g and svg:svg items) to delete for cleanup (for ungrouping)
allDrops = [ ] #list of all other elements except svg:g and svg:svg to drop while migrating (for filtering)
2020-08-20 02:41:10 +02:00
2021-04-19 20:54:38 +02:00
def add_arguments ( self , pars ) :
pars . add_argument ( " --tab " )
pars . add_argument ( " --operationmode " , default = False , help = " Operation mode " )
pars . add_argument ( " --parsechildren " , type = inkex . Boolean , default = True , help = " Perform operations on children of selection " )
pars . add_argument ( " --showdroplist " , type = inkex . Boolean , default = False , help = " Show list of dropped items " )
pars . add_argument ( " --shownewgroupname " , type = inkex . Boolean , default = False , help = " This helps to better identify the generated output. " )
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 ( " --cleanup " , type = inkex . Boolean , default = True , help = " This will call the extension ' Remove Empty Groups ' if available " )
#pars.add_argument("--sodipodi", type=inkex.Boolean, default=True)
#pars.add_argument("--svg", type=inkex.Boolean, default=True)
pars . add_argument ( " --circle " , type = inkex . Boolean , default = True )
pars . add_argument ( " --clipPath " , type = inkex . Boolean , default = True )
pars . add_argument ( " --defs " , type = inkex . Boolean , default = True )
pars . add_argument ( " --ellipse " , type = inkex . Boolean , default = True )
pars . add_argument ( " --image " , type = inkex . Boolean , default = True )
2021-04-19 22:07:01 +02:00
pars . add_argument ( " --guide " , type = inkex . Boolean , default = True )
2021-04-19 20:54:38 +02:00
pars . add_argument ( " --line " , type = inkex . Boolean , default = True )
pars . add_argument ( " --path " , type = inkex . Boolean , default = True )
pars . add_argument ( " --polyline " , type = inkex . Boolean , default = True )
pars . add_argument ( " --polygon " , type = inkex . Boolean , default = True )
pars . add_argument ( " --rect " , type = inkex . Boolean , default = True )
pars . add_argument ( " --text " , type = inkex . Boolean , default = True )
pars . add_argument ( " --tspan " , type = inkex . Boolean , default = True )
pars . add_argument ( " --linearGradient " , type = inkex . Boolean , default = True )
pars . add_argument ( " --radialGradient " , type = inkex . Boolean , default = True )
pars . add_argument ( " --mask " , type = inkex . Boolean , default = True )
pars . add_argument ( " --meshGradient " , type = inkex . Boolean , default = True )
pars . add_argument ( " --meshRow " , type = inkex . Boolean , default = True )
pars . add_argument ( " --meshPatch " , type = inkex . Boolean , default = True )
pars . add_argument ( " --metadata " , type = inkex . Boolean , default = True )
pars . add_argument ( " --script " , type = inkex . Boolean , default = True )
pars . add_argument ( " --symbol " , type = inkex . Boolean , default = True )
pars . add_argument ( " --stop " , type = inkex . Boolean , default = True )
pars . add_argument ( " --switch " , type = inkex . Boolean , default = True )
pars . add_argument ( " --use " , type = inkex . Boolean , default = True )
pars . add_argument ( " --flowRoot " , type = inkex . Boolean , default = True )
pars . add_argument ( " --flowRegion " , type = inkex . Boolean , default = True )
pars . add_argument ( " --flowPara " , type = inkex . Boolean , default = True )
pars . add_argument ( " --marker " , type = inkex . Boolean , default = True )
pars . add_argument ( " --pattern " , type = inkex . Boolean , default = True )
2020-09-08 01:54:51 +02:00
2020-08-23 16:22:46 +02:00
2020-08-13 01:28:19 +02:00
def effect ( self ) :
2020-08-31 21:25:41 +02:00
namespace = [ ] #a list of selected types we are going to process for filtering (dropping items)
#namespace.append("{http://www.w3.org/2000/svg}sodipodi") if self.options.sodipodi else "" #do not do this. it will crash InkScape
#namespace.append("{http://www.w3.org/2000/svg}svg") if self.options.svg else "" #we handle svg:svg the same type like svg:g
2021-04-19 22:07:01 +02:00
namespace . append ( " { http://www.w3.org/2000/svg}circle " ) if self . options . circle else " "
namespace . append ( " { http://www.w3.org/2000/svg}clipPath " ) if self . options . clipPath else " "
namespace . append ( " { http://www.w3.org/2000/svg}defs " ) if self . options . defs else " "
namespace . append ( " { http://www.w3.org/2000/svg}ellipse " ) if self . options . ellipse else " "
namespace . append ( " { http://www.w3.org/2000/svg}image " ) if self . options . image else " "
namespace . append ( " { http://www.w3.org/2000/svg}line " ) if self . options . line else " "
namespace . append ( " { http://www.w3.org/2000/svg}polygon " ) if self . options . polygon else " "
namespace . append ( " { http://www.w3.org/2000/svg}path " ) if self . options . path else " "
namespace . append ( " { http://www.w3.org/2000/svg}polyline " ) if self . options . polyline else " "
namespace . append ( " { http://www.w3.org/2000/svg}rect " ) if self . options . rect else " "
namespace . append ( " { http://www.w3.org/2000/svg}text " ) if self . options . text else " "
namespace . append ( " { http://www.w3.org/2000/svg}tspan " ) if self . options . tspan else " "
namespace . append ( " { http://www.w3.org/2000/svg}linearGradient " ) if self . options . linearGradient else " "
namespace . append ( " { http://www.w3.org/2000/svg}radialGradient " ) if self . options . radialGradient else " "
namespace . append ( " { http://www.w3.org/2000/svg}meshGradient " ) if self . options . meshGradient else " "
namespace . append ( " { http://www.w3.org/2000/svg}meshRow " ) if self . options . meshRow else " "
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 " "
namespace . append ( " { http://www.w3.org/2000/svg}flowPara " ) if self . options . flowPara else " "
namespace . append ( " { http://www.w3.org/2000/svg}marker " ) if self . options . marker else " "
namespace . append ( " { http://www.w3.org/2000/svg}pattern " ) if self . options . pattern else " "
namespace . append ( " { http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd}guide " ) if self . options . guide else " "
2021-04-19 20:54:38 +02:00
#self.msg(namespace)
2020-08-30 12:30:35 +02:00
2020-08-31 21:25:41 +02:00
#in case the user made a manual selection instead of whole document parsing, we need to collect all required elements first
def parseChildren ( element ) :
if element not in selected :
selected . append ( element )
if self . options . parsechildren == True :
children = element . getchildren ( )
if children is not None :
for child in children :
if child not in selected :
selected . append ( child )
parseChildren ( child ) #go deeper and deeper
#check the element for it's type and put it into the according list (either re-group or delete or just nothing)
2020-08-30 12:30:35 +02:00
def parseElement ( self , element ) :
2020-08-31 21:25:41 +02:00
#if we only want to ungroup (flatten) the elements we just collect all elements in a list and put them in a new single group later
if self . options . operationmode == " ungroup_only " :
2020-08-23 14:08:11 +02:00
if element not in self . allElements :
if element . tag != inkex . addNS ( ' g ' , ' svg ' ) and element . tag != inkex . addNS ( ' svg ' , ' svg ' ) and element . tag != inkex . addNS ( ' namedview ' , ' sodipodi ' ) :
self . allElements . append ( element )
2020-08-31 21:25:41 +02:00
#if we dont want to ungroup but filter out elements, or ungroup and filter, we need to divide the elements with respect to the namespace (user selection)
elif self . options . operationmode == " filter_only " or self . options . operationmode == " ungroup_and_filter " :
2021-04-19 20:54:38 +02:00
#self.msg(element.tag)
2020-08-31 21:25:41 +02:00
if element . tag in namespace : #if the element is in namespace and no group type we will regroup the item. so we will not remove it
2020-08-23 14:08:11 +02:00
if element not in self . allElements :
self . allElements . append ( element )
2020-08-31 21:25:41 +02:00
else : #we add all remaining items (except svg:g and svg:svg) into the list for deletion
2021-04-19 20:54:38 +02:00
#self.msg(element.tag)
2020-08-23 14:08:11 +02:00
if element . tag != inkex . addNS ( ' g ' , ' svg ' ) and element . tag != inkex . addNS ( ' svg ' , ' svg ' ) and element . tag != inkex . addNS ( ' namedview ' , ' sodipodi ' ) :
2020-08-31 21:25:41 +02:00
if element not in self . allDrops :
self . allDrops . append ( element )
#finally the groups we want to get rid off are put into a another list. They will be deleted (depending on the mode) after parsing the element tree
if self . options . operationmode == " ungroup_only " or self . options . operationmode == " ungroup_and_filter " :
if element . tag == inkex . addNS ( ' g ' , ' svg ' ) or element . tag == inkex . addNS ( ' svg ' , ' svg ' ) :
if element not in self . allGroups :
self . allGroups . append ( element )
2021-04-19 20:54:38 +02:00
applyTransformAvailable = False # at first we apply external extension
try :
import applytransform
applyTransformAvailable = True
except Exception as e :
# self.msg(e)
self . msg ( " Calling ' Apply Transformations ' extension failed. Maybe the extension is not installed. You can download it from official InkScape Gallery. Skipping ... " )
if self . options . apply_transformations is True and applyTransformAvailable is True :
applytransform . ApplyTransform ( ) . recursiveFuseTransform ( self . document . getroot ( ) )
2020-08-31 21:25:41 +02:00
#check if we have selected elements or if we should parse the whole document instead
selected = [ ] #total list of elements to parse
if len ( self . svg . selected ) == 0 :
for element in self . document . getroot ( ) . iter ( tag = etree . Element ) :
if element != self . document . getroot ( ) :
2021-04-19 20:54:38 +02:00
2020-08-31 21:25:41 +02:00
selected . append ( element )
else :
2021-04-19 20:54:38 +02:00
for element in self . svg . selected . values ( ) :
2020-08-31 21:25:41 +02:00
parseChildren ( element )
#get all elements from the selection.
2020-08-23 14:08:11 +02:00
for element in selected :
2020-08-30 12:30:35 +02:00
parseElement ( self , element )
2020-08-31 21:25:41 +02:00
#some debugging block
#check output
2021-04-19 20:54:38 +02:00
#self.msg("--- Selected items (with or without children) ---")
#self.msg(selected)
#self.msg("--- All elements (except groups)---")
#self.msg(len(self.allElements))
#self.msg(self.allElements)
#self.msg("--- All groups ---")
#self.msg(len(self.allGroups))
#self.msg(self.allGroups)
#self.msg("--- All dropouts ---")
#self.msg(len(self.allDrops))
#self.msg(self.allDrops)
2020-09-13 00:26:22 +02:00
migrate_log = " migrategroups.log "
# Clean up possibly previously generated log file
if os . path . exists ( migrate_log ) :
try :
os . remove ( migrate_log )
except OSError as e :
2021-04-19 20:54:38 +02:00
self . msg ( " Error while deleting previously generated log file " + migrate_log )
2020-09-13 00:26:22 +02:00
2020-08-31 21:25:41 +02:00
# show a list with items to delete. For ungroup mode it does not apply because we are not going to remove anything
if self . options . operationmode == " filter_only " or self . options . operationmode == " ungroup_and_filter " :
if self . options . showdroplist :
2021-04-19 20:54:38 +02:00
self . msg ( str ( len ( self . allDrops ) ) + " elements were removed during nodes while migration. " )
2020-09-13 00:26:22 +02:00
if len ( self . allDrops ) > 100 : #if we print too much to the output stream we will freeze InkScape forever wihtout any visual error message. So we write to file instead
migrate_log_file = open ( ' migrategroups.log ' , ' w ' )
else :
migrate_log_file = None
2020-08-31 21:25:41 +02:00
for i in self . allDrops :
if i . get ( ' id ' ) is not None :
2020-09-13 00:26:22 +02:00
migrateString = i . tag . replace ( " { http://www.w3.org/2000/svg} " , " svg: " ) + " id: " + i . get ( ' id ' )
if migrate_log_file is None :
2021-04-19 20:54:38 +02:00
self . msg ( migrateString )
2020-09-13 00:26:22 +02:00
else :
migrate_log_file . write ( migrateString + " \n " )
if migrate_log_file is not None :
migrate_log_file . close ( )
2021-04-19 20:54:38 +02:00
self . msg ( " Detailed output was dumped into file " + os . path . join ( os . getcwd ( ) , migrate_log ) )
2020-08-30 12:30:35 +02:00
2020-08-31 21:25:41 +02:00
# remove all groups from the selection and form a new single group of it by copying with old IDs.
if self . options . operationmode == " ungroup_only " or self . options . operationmode == " ungroup_and_filter " :
if len ( self . allElements ) > 0 :
newGroup = self . document . getroot ( ) . add ( inkex . Group ( ) ) #make a new group at root level
newGroup . set ( ' id ' , self . svg . get_unique_id ( ' migrate- ' ) ) #generate some known ID with the prefix 'migrate-'
if self . options . shownewgroupname == True :
2021-04-19 20:54:38 +02:00
self . msg ( " The migrated elements are now in group with ID " + str ( newGroup . get ( ' id ' ) ) )
2020-08-31 21:25:41 +02:00
index = 0
for element in self . allElements : #we have a list of elements which does not cotain any other elements like svg:g or svg:svg
newGroup . insert ( index , element ) #we do not copy any elements. we just rearrange them by moving to another place (group index)
index + = 1 #we must count up the index or we would overwrite each previous element
2020-09-13 00:26:22 +02:00
2020-08-31 21:25:41 +02:00
# remove the stuff from drop list list. this has to be done before we drop the groups where they are located in
if self . options . operationmode == " filter_only " or self . options . operationmode == " ungroup_and_filter " :
if len ( self . allDrops ) > 0 :
for dropElement in self . allDrops :
if dropElement . getparent ( ) is not None :
dropElement . getparent ( ) . remove ( dropElement )
2020-08-30 12:30:35 +02:00
2020-08-31 21:25:41 +02:00
# remove all the obsolete groups which are left over from ungrouping (flattening)
if self . options . operationmode == " ungroup_only " or self . options . operationmode == " ungroup_and_filter " :
2020-08-23 14:08:11 +02:00
if len ( self . allGroups ) > 0 :
for group in self . allGroups :
2020-08-30 12:30:35 +02:00
if group . getparent ( ) is not None :
group . getparent ( ) . remove ( group )
2020-08-31 21:25:41 +02:00
# finally removed dangling empty groups using external extension (if installed)
if self . options . cleanup == True :
try :
2020-09-07 17:36:19 +02:00
import cleangroups
cleangroups . CleanGroups . effect ( self )
2020-08-31 21:25:41 +02:00
except :
2021-04-19 20:54:38 +02:00
self . msg ( " Calling ' Remove Empty Groups ' extension failed. Maybe the extension is not installed. You can download it from official InkScape Gallery. " )
2020-08-30 12:30:35 +02:00
2020-08-31 21:25:41 +02:00
if __name__ == ' __main__ ' :
MigrateGroups ( ) . run ( )