several addons to filter and some other glitches removed

This commit is contained in:
Mario Voigt 2021-11-04 10:55:18 +01:00
parent 5935a95345
commit 07490f9349
5 changed files with 209 additions and 102 deletions

View File

@ -196,60 +196,22 @@ class LinksCreator(inkex.EffectExtension):
# check if the element has a style attribute. If not we create a blank one with a black stroke and without fill # check if the element has a style attribute. If not we create a blank one with a black stroke and without fill
style = None style = None
default_fill = 'none' default_fill = 'none'
default_stroke_width = '1px'
default_stroke = '#000000' default_stroke = '#000000'
default_stroke_width = str(self.svg.unittouu('1px'))
if element.attrib.has_key('style'): if element.attrib.has_key('style'):
style = element.get('style') element.style['stroke-dasharray'] = stroke_dasharray
if style.endswith(';') is False: element.style['stroke-dashoffset'] = stroke_dashoffset
style += ';'
# if has style attribute and dasharray and/or dashoffset are present we modify it accordingly
declarations = style.split(';') # parse the style content and check what we need to adjust
for i, decl in enumerate(declarations):
parts = decl.split(':', 2)
if len(parts) == 2:
(prop, val) = parts
prop = prop.strip().lower()
#if prop == 'fill':
# declarations[i] = prop + ':{}'.format(default_fill)
#if prop == 'stroke':
# declarations[i] = prop + ':{}'.format(default_stroke)
#if prop == 'stroke-width':
# declarations[i] = prop + ':{}'.format(default_stroke_width)
if prop == 'stroke-dasharray': #comma separated list of one or more float values
declarations[i] = prop + ':{}'.format(stroke_dasharray)
if prop == 'stroke-dashoffset':
declarations[i] = prop + ':{}'.format(stroke_dashoffset)
element.set('style', ';'.join(declarations)) #apply new style to element
#if has style attribute but the style attribute does not contain fill, stroke, stroke-width, stroke-dasharray or stroke-dashoffset yet #if has style attribute but the style attribute does not contain fill, stroke, stroke-width, stroke-dasharray or stroke-dashoffset yet
style = element.style if element.style.get('fill') is None: element.style['fill'] = default_fill
if re.search('fill:(.*?)(;|$)', str(style)) is None: if element.style.get('stroke') is None: element.style['stroke'] = default_stroke
style += 'fill:{};'.format(default_fill) if element.style.get('stroke-width') is None: element.style['stroke-width'] = default_stroke_width
if re.search('(;|^)stroke:(.*?)(;|$)', str(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:{};'.format(default_stroke)
if not 'stroke-width' in style:
style += 'stroke-width:{};'.format(default_stroke_width)
if not 'stroke-dasharray' in style:
style += 'stroke-dasharray:{};'.format(stroke_dasharray)
if not 'stroke-dashoffset' in style:
style += 'stroke-dashoffset:{};'.format(stroke_dashoffset)
element.set('style', style)
else: else:
style = 'fill:{};stroke:{};stroke-width:{};stroke-dasharray:{};stroke-dashoffset:{};'.format(default_fill, default_stroke, default_stroke_width, stroke_dasharray, stroke_dashoffset) element.style = 'fill:{};stroke:{};stroke-width:{};stroke-dasharray:{};stroke-dashoffset:{};'.format(
element.set('style', style) default_fill, default_stroke, default_stroke_width, stroke_dasharray, stroke_dashoffset)
#if enabled, we override stroke color with blue (now, as the element definitely has a style) #if enabled, we override stroke color with blue (now, as the element definitely has a style)
if self.options.weakening_mode is True and self.options.switch_pattern is True: if self.options.weakening_mode is True and self.options.switch_pattern is True:
declarations = element.get('style').split(';') element.style['stroke'] = "#0000ff"
for i, decl in enumerate(declarations):
parts = decl.split(':', 2)
if len(parts) == 2:
(prop, val) = parts
prop = prop.strip().lower()
if prop == 'stroke':
declarations[i] = prop + ':{}'.format("#0000ff")
element.set('style', ';'.join(declarations)) #apply new style to element
# Print some info about values # Print some info about values
if self.options.show_info is True: if self.options.show_info is True:

View File

@ -2,9 +2,15 @@
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension"> <inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Filter By Length/Area</name> <name>Filter By Length/Area</name>
<id>fablabchemnitz.de.filter_by_length_area</id> <id>fablabchemnitz.de.filter_by_length_area</id>
<label>Applies to paths only! Rectangles and other elements are not supported. If your selection is empty, the whole document will be parsed.</label> <param name="tab" type="notebook">
<page name="tab_settings" gui-text="Filter By Length/Area">
<label appearance="header">General Settings</label>
<param name="debug" type="bool" gui-text="Enable debug">false</param> <param name="debug" type="bool" gui-text="Enable debug">false</param>
<param name="unit" gui-text="Unit" type="optiongroup" appearance="combo"> <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>
<hbox>
<vbox>
<label appearance="header">Threshold</label>
<param name="unit" gui-text="Unit" type="optiongroup" appearance="combo" gui-description="The unit applies to interval and thresholds">
<option value="mm">mm</option> <option value="mm">mm</option>
<option value="cm">cm</option> <option value="cm">cm</option>
<option value="px">px</option> <option value="px">px</option>
@ -12,8 +18,6 @@
<option value="pc">pc</option> <option value="pc">pc</option>
<option value="in">in</option> <option value="in">in</option>
</param> </param>
<label>The unit applies to interval and thresholds</label>
<label appearance="header">Threshold</label>
<param name="nodes_interval" type="float" min="0.000" max="99999.000" precision="3" gui-text="Interval">10.000</param> <param name="nodes_interval" type="float" min="0.000" max="99999.000" precision="3" gui-text="Interval">10.000</param>
<separator/> <separator/>
<param name="min_filter_enable" type="bool" gui-text="Enable filtering min.">false</param> <param name="min_filter_enable" type="bool" gui-text="Enable filtering min.">false</param>
@ -23,19 +27,73 @@
<param name="max_threshold" type="float" min="0.000" precision="3" max="10000000.000" gui-text="Max. length or area">10000000.000</param> <param name="max_threshold" type="float" min="0.000" precision="3" max="10000000.000" gui-text="Max. length or area">10000000.000</param>
<param name="max_nodes" type="int" min="0" max="99999" gui-text="Max. nodes/&lt;interval&gt;">10000000</param> <param name="max_nodes" type="int" min="0" max="99999" gui-text="Max. nodes/&lt;interval&gt;">10000000</param>
<param name="precision" type="int" min="0" max="16" gui-text="Precision">3</param> <param name="precision" type="int" min="0" max="16" gui-text="Precision">3</param>
<label>Filter</label> </vbox>
<separator/>
<vbox>
<label appearance="header">Filter</label>
<param name="measure" type="optiongroup" appearance="combo" gui-text="By"> <param name="measure" type="optiongroup" appearance="combo" gui-text="By">
<option value="length">Length (Unit)</option> <option value="length">Length (Unit)</option>
<option value="nodes">Nodes per length (Unit)</option> <option value="nodes">Nodes per length (Unit)</option>
<option value="area">Area (Unit^2)</option> <option value="area">Area (Unit^2)</option>
</param> </param>
<label>Actions</label> <label appearance="header">Actions</label>
<param name="delete" type="bool" gui-text="Delete">false</param> <param name="delete" type="bool" gui-text="Delete">false</param>
<param name="colorize" type="bool" gui-text="Colorize">false</param>
<hbox> <hbox>
<param name="sort_by_asc" type="bool" gui-text="Sort by value">false</param> <param name="color_mode" type="optiongroup" appearance="combo" gui-text="Color mode">
<param name="reverse_sort" type="bool" gui-text="Reverse sorting">false</param> <option value="none">None</option>
<option value="colorize_rainbow">Colorize (Rainbow effect)</option>
<option value="colorize_single">Colorize (Single color)</option>
</param>
<param name="color_single" type="color" appearance="colorbutton" gui-text="Single color">0xff00ffff</param>
</hbox> </hbox>
<hbox>
<param name="sort_by_value" type="bool" gui-text="Sort by value">false</param>
<param name="reverse_sort_value" type="bool" gui-text="Reverse">false</param>
</hbox>
<hbox>
<param name="sort_by_id" type="bool" gui-text="Sort by Id">false</param>
<param name="reverse_sort_id" type="bool" gui-text="Reverse">false</param>
</hbox>
<param name="rename_ids" type="bool" gui-text="Rename (IDs)">false</param>
<hbox>
<param name="set_labels" type="bool" gui-text="Set labels" gui-description="Adds type and value to the element's label">false</param>
<param name="remove_labels" type="bool" gui-text="Remove labels" gui-description="Remove labels (cleaning option for previous applications)">false</param>
</hbox>
<param name="group" type="bool" gui-text="Group elements">false</param>
<param name="cleanup" type="bool" gui-text="Cleanup unused groups/layers" gui-description="This will call the extension 'Remove Empty Groups' if available">false</param>
</vbox>
</hbox>
<label appearance="header">Tips</label>
<label>Applies to paths only! Rectangles and other elements are not supported. If your selection is empty, the whole document will be parsed.
If you did not enable any filter, the actions are applied either to the whole selection or the complete document too.</label>
</page>
<page name="tab_about" gui-text="About">
<label appearance="header">Filter By Length/Area</label>
<label>A tool to filter for paths by different filters. Allows multiple actions to perform on.</label>
<label>2020 - 2021 / written by Mario Voigt (Stadtfabrikanten e.V. / FabLab Chemnitz)</label>
<spacer/>
<label appearance="header">Online Documentation</label>
<label appearance="url">https://y.stadtfabrikanten.org/filterbylengtharea</label>
<spacer/>
<label appearance="header">Contributing</label>
<label appearance="url">https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.X</label>
<label appearance="url">mailto:mario.voigt@stadtfabrikanten.org</label>
<spacer/>
<label appearance="header">MightyScape Extension Collection</label>
<label>This piece of software is part of the MightyScape for Inkscape Extension Collection and is licensed under GNU GPL v3</label>
<label appearance="url">https://y.stadtfabrikanten.org/mightyscape-overview</label>
</page>
<page name="tab_donate" gui-text="Donate">
<label appearance="header">Coffee + Pizza</label>
<label>We are the Stadtfabrikanten, running the FabLab Chemnitz since 2016. A FabLab is an open workshop that gives people access to machines and digital tools like 3D printers, laser cutters and CNC milling machines.</label>
<spacer/>
<label>You like our work and want to support us? You can donate to our non-profit organization by different ways:</label>
<label appearance="url">https://y.stadtfabrikanten.org/donate</label>
<spacer/>
<label>Thanks for using our extension and helping us!</label>
<image>../000_about_fablabchemnitz.svg</image>
</page>
</param>
<effect> <effect>
<object-type>all</object-type> <object-type>all</object-type>
<effects-menu> <effects-menu>

View File

@ -1,25 +1,35 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
''' '''
Extension for InkScape 1.0 Extension for InkScape 1.0+
Features
- Filter paths which are smaller/bigger than a given length or area
Author: Mario Voigt / FabLab Chemnitz Author: Mario Voigt / FabLab Chemnitz
Mail: mario.voigt@stadtfabrikanten.org Mail: mario.voigt@stadtfabrikanten.org
Date: 03.08.2020 Date: 03.08.2020
Last patch: 21.10.2021 Last patch: 04.11.2021
License: GNU GPL v3 License: GNU GPL v3
ToDo:
- id sorting: handle ids with/without numbers and sort by number
''' '''
import sys
import colorsys import colorsys
import inkex import inkex
from inkex import Color
from inkex.bezier import csplength, csparea from inkex.bezier import csplength, csparea
sys.path.append("../remove_empty_groups")
sys.path.append("../apply_transformations")
class FilterByLengthArea(inkex.EffectExtension): class FilterByLengthArea(inkex.EffectExtension):
def add_arguments(self, pars): def add_arguments(self, pars):
pars.add_argument('--tab')
pars.add_argument('--debug', type=inkex.Boolean, default=False) pars.add_argument('--debug', type=inkex.Boolean, default=False)
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 = "Cleanup all unused groups/layers (requires separate extension)")
pars.add_argument('--unit') pars.add_argument('--unit')
pars.add_argument('--min_filter_enable', type=inkex.Boolean, default=True, help='Enable filtering min.') pars.add_argument('--min_filter_enable', type=inkex.Boolean, default=True, help='Enable filtering min.')
pars.add_argument('--min_threshold', type=float, default=0.000, help='Remove paths with an threshold smaller than this value') pars.add_argument('--min_threshold', type=float, default=0.000, help='Remove paths with an threshold smaller than this value')
@ -31,15 +41,30 @@ class FilterByLengthArea(inkex.EffectExtension):
pars.add_argument('--precision', type=int, default=3, help='Precision') pars.add_argument('--precision', type=int, default=3, help='Precision')
pars.add_argument('--measure', default="length") pars.add_argument('--measure', default="length")
pars.add_argument('--delete', type=inkex.Boolean, default=False) pars.add_argument('--delete', type=inkex.Boolean, default=False)
pars.add_argument('--colorize', type=inkex.Boolean, default=False) pars.add_argument('--color_mode', default="none")
pars.add_argument('--sort_by_asc', type=inkex.Boolean, default=False) pars.add_argument('--color_single', type=Color, default='0xff00ffff')
pars.add_argument('--reverse_sort', type=inkex.Boolean, default=False) pars.add_argument('--sort_by_value', type=inkex.Boolean, default=False)
pars.add_argument('--reverse_sort_value', type=inkex.Boolean, default=False)
pars.add_argument('--sort_by_id', type=inkex.Boolean, default=False)
pars.add_argument('--reverse_sort_id', type=inkex.Boolean, default=False)
pars.add_argument('--rename_ids', type=inkex.Boolean, default=False)
pars.add_argument('--set_labels', type=inkex.Boolean, default=False, help="Adds type and value to the element's label")
pars.add_argument('--remove_labels', type=inkex.Boolean, default=False, help="Remove labels (cleaning option for previous applications)")
pars.add_argument('--group', type=inkex.Boolean, default=False)
def effect(self): def effect(self):
global to_sort, so global to_sort, so
to_sort = [] to_sort = []
so = self.options so = self.options
applyTransformationsAvailable = False # at first we apply external extension
try:
import apply_transformations
applyTransformationsAvailable = 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 ...")
so.min_threshold = self.svg.unittouu(str(so.min_threshold) + self.svg.unit) so.min_threshold = self.svg.unittouu(str(so.min_threshold) + self.svg.unit)
so.max_threshold = self.svg.unittouu(str(so.max_threshold) + self.svg.unit) so.max_threshold = self.svg.unittouu(str(so.max_threshold) + self.svg.unit)
unit_factor = 1.0 / self.svg.uutounit(1.0, so.unit) unit_factor = 1.0 / self.svg.uutounit(1.0, so.unit)
@ -48,13 +73,18 @@ class FilterByLengthArea(inkex.EffectExtension):
return return
if len(self.svg.selected) > 0: if len(self.svg.selected) > 0:
elements = self.svg.selection.filter(inkex.PathElement).values() elements = self.svg.selection.filter().values()
else: else:
elements = self.document.xpath("//svg:path", namespaces=inkex.NSS) elements = self.document.xpath("//svg:path", namespaces=inkex.NSS)
if so.debug is True: if so.debug is True:
inkex.utils.debug("Collecting elements ...") inkex.utils.debug("Collecting elements ...")
for element in elements: for element in elements:
# additional option to apply transformations. As we clear up some groups to form new layers, we might lose translations, rotations, etc.
if so.apply_transformations is True and applyTransformationsAvailable is True:
apply_transformations.ApplyTransformations().recursiveFuseTransform(element)
try: try:
csp = element.path.transform(element.composed_transform()).to_superpath() csp = element.path.transform(element.composed_transform()).to_superpath()
@ -65,7 +95,7 @@ class FilterByLengthArea(inkex.EffectExtension):
(so.min_filter_enable is False and so.max_filter_enable is False): #complete selection (so.min_filter_enable is False and so.max_filter_enable is False): #complete selection
if so.debug is True: if so.debug is True:
inkex.utils.debug("id={}, area={:0.3f}{}^2".format(element.get('id'), area, so.unit)) inkex.utils.debug("id={}, area={:0.3f}{}^2".format(element.get('id'), area, so.unit))
to_sort.append({'element': element, 'value': area}) to_sort.append({'element': element, 'value': area, 'type': 'area'})
elif so.measure == "length": elif so.measure == "length":
slengths, stotal = csplength(csp) #get segment lengths and total length of path in document's internal unit slengths, stotal = csplength(csp) #get segment lengths and total length of path in document's internal unit
@ -75,7 +105,7 @@ class FilterByLengthArea(inkex.EffectExtension):
(so.min_filter_enable is False and so.max_filter_enable is False): #complete selection (so.min_filter_enable is False and so.max_filter_enable is False): #complete selection
if so.debug is True: if so.debug is True:
inkex.utils.debug("id={}, length={:0.3f}{}".format(element.get('id'), self.svg.uutounit(str(stotal), so.unit), so.unit)) inkex.utils.debug("id={}, length={:0.3f}{}".format(element.get('id'), self.svg.uutounit(str(stotal), so.unit), so.unit))
to_sort.append({'element': element, 'value': stotal}) to_sort.append({'element': element, 'value': stotal, 'type': 'length'})
elif so.measure == "nodes": elif so.measure == "nodes":
slengths, stotal = csplength(csp) #get segment lengths and total length of path in document's internal unit slengths, stotal = csplength(csp) #get segment lengths and total length of path in document's internal unit
@ -86,7 +116,7 @@ class FilterByLengthArea(inkex.EffectExtension):
(so.min_filter_enable is False and so.max_filter_enable is False): #complete selection (so.min_filter_enable is False and so.max_filter_enable is False): #complete selection
if so.debug is True: if so.debug is True:
inkex.utils.debug("id={}, length={:0.3f}{}, nodes={}".format(element.get('id'), self.svg.uutounit(str(stotal), so.unit), so.unit, nodes)) inkex.utils.debug("id={}, length={:0.3f}{}, nodes={}".format(element.get('id'), self.svg.uutounit(str(stotal), so.unit), so.unit, nodes))
to_sort.append({'element': element, 'value': nodes}) to_sort.append({'element': element, 'value': nodes, 'type': 'nodes'})
except Exception as e: except Exception as e:
#inkex.utils.debug(e) #inkex.utils.debug(e)
@ -96,23 +126,77 @@ class FilterByLengthArea(inkex.EffectExtension):
element = to_sort[i].get('element') element = to_sort[i].get('element')
if so.delete is True: if so.delete is True:
element.delete() element.delete()
if so.delete is True: if so.delete is True:
return #quit here return #quit here
if so.sort_by_asc is True: if so.sort_by_value is True:
to_sort.sort(key=lambda x: x.get('value')) #sort by target value to_sort.sort(key=lambda x: x.get('value')) #sort by target value
if so.sort_by_id is True:
to_sort.sort(key=lambda x: x.get('element').get('id')) #sort by id. will override previous value sort
if so.group is True:
group = inkex.Group(id=self.svg.get_unique_id("filtered"))
self.svg.get_current_layer().add(group)
allIds = self.svg.get_ids()
newIds = [] #we pre-populate this
for i in range(0, len(to_sort)):
newIds.append("{}{}".format(element.tag.replace('{http://www.w3.org/2000/svg}',''), i)) #should be element tag 'path'
for i in range(0, len(to_sort)): for i in range(0, len(to_sort)):
element = to_sort[i].get('element') element = to_sort[i].get('element')
if so.reverse_sort is True:
if so.rename_ids is True:
if newIds[i] in allIds: #already exist. lets rename that one before using it's id for the recent element
try:
renameIdPre = element.get('id') + "-"
renameId = self.svg.get_unique_id(renameIdPre)
#inkex.utils.debug("Trying to rename {} to {}".format(element.get('id'), renameId))
originalElement = self.svg.getElementById(newIds[i])
originalElement.set('id', renameId)
except Exception as e:
pass
#inkex.utils.debug(e)
element.set('id', newIds[i])
if so.sort_by_value is True:
if so.reverse_sort_value is True:
idx = len(element.getparent()) idx = len(element.getparent())
else: else:
idx = 0 idx = 0
element.getparent().insert(idx, element) element.getparent().insert(idx, element)
if so.colorize is True:
if so.sort_by_id is True:
if so.reverse_sort_id is True:
idx = len(element.getparent())
else:
idx = 0
element.getparent().insert(idx, element)
if so.color_mode == "colorize_rainbow":
color = colorsys.hsv_to_rgb(i / float(len(to_sort)), 1.0, 1.0) color = colorsys.hsv_to_rgb(i / float(len(to_sort)), 1.0, 1.0)
element.style['stroke'] = '#%02x%02x%02x' % tuple(int(x * 255) for x in color) element.style['stroke'] = '#%02x%02x%02x' % tuple(int(x * 255) for x in color)
if so.color_mode == "colorize_single":
element.style['stroke'] = so.color_single
if so.set_labels is True:
element.set('inkscape:label', "{}={}".format(to_sort[i].get('type'), to_sort[i].get('value')))
if so.remove_labels is True:
element.pop('inkscape:label')
if so.group is True:
group.append(element)
if so.cleanup == True:
try:
import remove_empty_groups
remove_empty_groups.RemoveEmptyGroups.effect(self)
except:
self.msg("Calling 'Remove Empty Groups' extension failed. Maybe the extension is not installed. You can download it from official InkScape Gallery. Skipping ...")
if __name__ == '__main__': if __name__ == '__main__':
FilterByLengthArea().run() FilterByLengthArea().run()

View File

@ -3,7 +3,10 @@
"name": "Filter By Length/Area", "name": "Filter By Length/Area",
"id": "fablabchemnitz.de.filter_by_length_area", "id": "fablabchemnitz.de.filter_by_length_area",
"path": "filter_by_length_area", "path": "filter_by_length_area",
"dependent_extensions": null, "dependent_extensions": [
"apply_transformations",
"remove_empty_groups"
],
"original_name": "Filter By Length/Area", "original_name": "Filter By Length/Area",
"original_id": "com.filter_by_length_area", "original_id": "com.filter_by_length_area",
"license": "GNU GPL v3", "license": "GNU GPL v3",

View File

@ -50,7 +50,7 @@ class StylesToLayers(inkex.EffectExtension):
pars.add_argument("--parsecolors",default = "hexval", help = "Sort colors 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("--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("--decimals", type=int, default = 1, help = "Decimal tolerance")
pars.add_argument("--cleanup", type=inkex.Boolean, default = True, help = "Decimal tolerance") pars.add_argument("--cleanup", type=inkex.Boolean, default = True, help = "Cleanup all unused groups/layers (requires separate extension)")
pars.add_argument("--put_unfiltered", type=inkex.Boolean, default = False, help = "Put unfiltered elements to a separate layer") 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") pars.add_argument("--show_info", type=inkex.Boolean, default = False, help = "Show elements which have no style attributes to filter")