more extensions readded
This commit is contained in:
parent
a1f6bea8de
commit
58ed243ac0
@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
|
||||||
|
<name>Convert Vertical/Horizontal To Line</name>
|
||||||
|
<id>fablabchemnitz.de.convert_vertical_horizontal_to_line</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 location="inx" interpreter="python">convert_vertical_horizontal_to_line.py</command>
|
||||||
|
</script>
|
||||||
|
</inkscape-extension>
|
@ -0,0 +1,64 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
#M 60.403,71.0937 V 89.268022 135.773
|
||||||
|
|
||||||
|
"""
|
||||||
|
Extension for InkScape 1.X
|
||||||
|
|
||||||
|
This extension converts an SVG path's d attribute the following way: find each V (vertical line) and each H (horizontal line) and replace it by a generic line (L type).
|
||||||
|
A lot of extensions do not work with V and H, but with L commands. So this is just a helper extension for other extensions :-)
|
||||||
|
|
||||||
|
Example conversion:
|
||||||
|
from: M 60.403,71.0937 V 89.268022 135.773
|
||||||
|
to: M 60.403 71.0937 L 60.403 89.268 L 60.403 135.773
|
||||||
|
|
||||||
|
Author: Mario Voigt / FabLab Chemnitz
|
||||||
|
Mail: mario.voigt@stadtfabrikanten.org
|
||||||
|
Date: 23.08.2020
|
||||||
|
Last patch: 23.08.2020
|
||||||
|
License: GNU GPL v3
|
||||||
|
"""
|
||||||
|
|
||||||
|
from math import *
|
||||||
|
import inkex
|
||||||
|
from inkex.paths import Path, CubicSuperPath
|
||||||
|
|
||||||
|
class ConvertVerticalHorizontalToLine(inkex.EffectExtension):
|
||||||
|
|
||||||
|
def effect(self):
|
||||||
|
if len(self.svg.selected) == 0: exit("Please select at least one path.")
|
||||||
|
for obj in self.svg.selected: # The objects are the paths, which may be compound
|
||||||
|
if obj.tag == inkex.addNS('path','svg'):
|
||||||
|
curr = self.svg.selected[obj]
|
||||||
|
raw = Path(curr.get("d")).to_arrays()
|
||||||
|
subpaths, prev = [], 0
|
||||||
|
for i in range(len(raw)): # Breaks compound paths into simple paths
|
||||||
|
if raw[i][0] == 'M' and i != 0:
|
||||||
|
subpaths.append(raw[prev:i])
|
||||||
|
prev = i
|
||||||
|
subpaths.append(raw[prev:])
|
||||||
|
|
||||||
|
seg = []
|
||||||
|
for simpath in subpaths:
|
||||||
|
if simpath[-1][0] == 'Z':
|
||||||
|
simpath[-1][0] = 'L'
|
||||||
|
if simpath[-2][0] == 'L': simpath[-1][1] = simpath[0][1]
|
||||||
|
else: simpath.pop()
|
||||||
|
for i in range(len(simpath)):
|
||||||
|
if simpath[i][0] == 'V': # vertical and horizontal lines only have one point in args, but 2 are required
|
||||||
|
#inkex.utils.debug(simpath[i][0])
|
||||||
|
simpath[i][0]='L' #overwrite V with regular L command
|
||||||
|
add=simpath[i-1][1][0] #read the X value from previous segment
|
||||||
|
simpath[i][1].append(simpath[i][1][0]) #add the second (missing) argument by taking argument from previous segment
|
||||||
|
simpath[i][1][0]=add #replace with recent X after Y was appended
|
||||||
|
if simpath[i][0] == 'H': # vertical and horizontal lines only have one point in args, but 2 are required
|
||||||
|
#inkex.utils.debug(simpath[i][0])
|
||||||
|
simpath[i][0]='L' #overwrite H with regular L command
|
||||||
|
simpath[i][1].append(simpath[i-1][1][1]) #add the second (missing) argument by taking argument from previous segment
|
||||||
|
#inkex.utils.debug(simpath[i])
|
||||||
|
seg.append(simpath[i])
|
||||||
|
curr.set("d", Path(seg))
|
||||||
|
else:
|
||||||
|
inkex.utils.debug("Object " + obj.get('id') + " is not a path. Please convert it to a path first.")
|
||||||
|
if __name__ == '__main__':
|
||||||
|
ConvertVerticalHorizontalToLine().run()
|
@ -0,0 +1,20 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "Convert Vertical/Horizontal To Line",
|
||||||
|
"id": "fablabchemnitz.de.convert_vertical_horizontal_to_line",
|
||||||
|
"path": "convert_vertical_horizontal_to_line",
|
||||||
|
"dependent_extensions": null,
|
||||||
|
"original_name": "Convert Vertical/Horizontal To Line",
|
||||||
|
"original_id": "fablabchemnitz.de.convert_vertical_horizontal_to_line",
|
||||||
|
"license": "GNU GPL v3",
|
||||||
|
"license_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.X/src/branch/master/LICENSE",
|
||||||
|
"comment": "Written by Mario Voigt",
|
||||||
|
"source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.X/src/branch/master/extensions/fablabchemnitz/convert_vertical_horizontal_to_line",
|
||||||
|
"fork_url": null,
|
||||||
|
"documentation_url": "https://stadtfabrikanten.org/pages/viewpage.action?pageId=79626259",
|
||||||
|
"inkscape_gallery_url": null,
|
||||||
|
"main_authors": [
|
||||||
|
"github.com/vmario89"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
@ -0,0 +1,37 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
|
||||||
|
<name>Dots To Path Points</name>
|
||||||
|
<id>fablabchemnitz.de.dots_to_path_points</id>
|
||||||
|
<param name="tab" type="notebook">
|
||||||
|
<page name="points" gui-text="Select Points">
|
||||||
|
<param type="bool" name="endpoints" gui-text="End Points">true</param>
|
||||||
|
<param type="bool" name="controlpoints" gui-text="Control Points">false</param>
|
||||||
|
</page>
|
||||||
|
<page name="usage" gui-text="Usage">
|
||||||
|
<label xml:space="preserve">
|
||||||
|
This extension places an arbitrary object at the points
|
||||||
|
of a path.
|
||||||
|
|
||||||
|
Select two Objects:
|
||||||
|
|
||||||
|
1. an object representing the path
|
||||||
|
2. an object representing the dot
|
||||||
|
|
||||||
|
The extension uses the SVG <use>-element which are
|
||||||
|
linked to the second object. Changes of the shape of the
|
||||||
|
source object changes the linked objects.
|
||||||
|
</label>
|
||||||
|
</page>
|
||||||
|
</param>
|
||||||
|
<effect>
|
||||||
|
<object-type>all</object-type>
|
||||||
|
<effects-menu>
|
||||||
|
<submenu name="FabLab Chemnitz">
|
||||||
|
<submenu name="Shape/Pattern from existing Path(s)"/>
|
||||||
|
</submenu>
|
||||||
|
</effects-menu>
|
||||||
|
</effect>
|
||||||
|
<script>
|
||||||
|
<command location="inx" interpreter="python">dots_to_path_points.py</command>
|
||||||
|
</script>
|
||||||
|
</inkscape-extension>
|
@ -0,0 +1,75 @@
|
|||||||
|
#! /usr/bin/env python3
|
||||||
|
'''
|
||||||
|
Copyright (C) 2020 Christian Hoffmann christian@lehrer-hoffmann.de
|
||||||
|
|
||||||
|
##This extension allows you to draw a Cartesian grid in Inkscape.
|
||||||
|
##There is a wide range of options including subdivision, subsubdivions
|
||||||
|
## and logarithmic scales. Custom line widths are also possible.
|
||||||
|
##All elements are grouped with similar elements (eg all x-subdivs)
|
||||||
|
|
||||||
|
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 Fraanklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
'''
|
||||||
|
|
||||||
|
import inkex
|
||||||
|
|
||||||
|
class DotsToPathPoints(inkex.EffectExtension):
|
||||||
|
|
||||||
|
def add_arguments(self, pars):
|
||||||
|
pars.add_argument("--tab")
|
||||||
|
pars.add_argument("--endpoints", type=inkex.Boolean, default=True)
|
||||||
|
pars.add_argument("--controlpoints", type=inkex.Boolean, default=False)
|
||||||
|
|
||||||
|
def effect(self):
|
||||||
|
if len(self.svg.selected) != 2:
|
||||||
|
inkex.errormsg("Please select exact two objects:\n1. object representing path,\n2. object representing dots.")
|
||||||
|
return
|
||||||
|
|
||||||
|
nodes = list(self.svg.selected.values())
|
||||||
|
iddot = nodes[0].get('id')
|
||||||
|
idpath = nodes[1].get('id')
|
||||||
|
dot = self.svg.selected[iddot]
|
||||||
|
path = self.svg.selected[idpath]
|
||||||
|
self.svg.selected.popitem()
|
||||||
|
self.svg.selected.popitem()
|
||||||
|
bb = dot.bounding_box()
|
||||||
|
parent = path.find('..')
|
||||||
|
group = inkex.Group()
|
||||||
|
parent.add(group)
|
||||||
|
|
||||||
|
end_points = list(path.path.end_points)
|
||||||
|
control_points = []
|
||||||
|
for cp in path.path.control_points:
|
||||||
|
is_endpoint = False
|
||||||
|
for ep in end_points:
|
||||||
|
if cp.x == ep.x and cp.y == ep.y:
|
||||||
|
is_endpoint = True
|
||||||
|
break
|
||||||
|
if not is_endpoint:
|
||||||
|
control_points.append(cp)
|
||||||
|
|
||||||
|
pointlist = []
|
||||||
|
if self.options.endpoints:
|
||||||
|
pointlist += end_points
|
||||||
|
if self.options.controlpoints:
|
||||||
|
pointlist += control_points
|
||||||
|
for point in pointlist:
|
||||||
|
clone = inkex.Use()
|
||||||
|
clone.set('xlink:href','#'+iddot)
|
||||||
|
clone.set('x',point.x-bb.center.x)
|
||||||
|
clone.set('y',point.y-bb.center.y)
|
||||||
|
group.add(clone)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
DotsToPathPoints().run()
|
21
extensions/fablabchemnitz/dots_to_path_points/meta.json
Normal file
21
extensions/fablabchemnitz/dots_to_path_points/meta.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "Dots To Path Points",
|
||||||
|
"id": "fablabchemnitz.de.dots_to_path_points",
|
||||||
|
"path": "dots_to_path_points",
|
||||||
|
"dependent_extensions": null,
|
||||||
|
"original_name": "Pathpoints2Dots",
|
||||||
|
"original_id": "pathpoints2dots",
|
||||||
|
"license": "GNU GPL v3",
|
||||||
|
"license_url": "https://github.com/chrille69/Inkscape-Extension-pathpoints2dots/blob/master/LICENSE",
|
||||||
|
"comment": "",
|
||||||
|
"source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.X/src/branch/master/extensions/fablabchemnitz/dots_to_path_points",
|
||||||
|
"fork_url": "https://github.com/chrille69/Inkscape-Extension-pathpoints2dots",
|
||||||
|
"documentation_url": "https://stadtfabrikanten.org/display/IFM/Dots+To+Path+Points",
|
||||||
|
"inkscape_gallery_url": null,
|
||||||
|
"main_authors": [
|
||||||
|
"github.com/chrille69",
|
||||||
|
"github.com/vmario89"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
@ -0,0 +1,27 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
|
||||||
|
<name>Edit Attributes</name>
|
||||||
|
<id>fablabchemnitz.de.edit_attributes</id>
|
||||||
|
<label>Edit value of attribute on selected elements.</label>
|
||||||
|
<label>For namespaces use {namespaceUrl}attributeName</label>
|
||||||
|
<param name="attributeName" type="string" gui-text="Name:"></param>
|
||||||
|
<param name="attributeValue" type="string" gui-text="Value: "></param>
|
||||||
|
<param name="mode" type="optiongroup" appearance="combo" gui-text="Operation on attribute">
|
||||||
|
<option value="set">Set</option>
|
||||||
|
<option value="append">Append</option>
|
||||||
|
<option value="prefix">Prefix</option>
|
||||||
|
<option value="subtract">Remove content</option>
|
||||||
|
<option value="remove">Remove attribute</option>
|
||||||
|
</param>
|
||||||
|
<effect>
|
||||||
|
<object-type>all</object-type>
|
||||||
|
<effects-menu>
|
||||||
|
<submenu name="FabLab Chemnitz">
|
||||||
|
<submenu name="Various"/>
|
||||||
|
</submenu>
|
||||||
|
</effects-menu>
|
||||||
|
</effect>
|
||||||
|
<script>
|
||||||
|
<command location="inx" interpreter="python">edit_attributes.py</command>
|
||||||
|
</script>
|
||||||
|
</inkscape-extension>
|
40
extensions/fablabchemnitz/edit_attributes/edit_attributes.py
Normal file
40
extensions/fablabchemnitz/edit_attributes/edit_attributes.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import inkex
|
||||||
|
import sys
|
||||||
|
|
||||||
|
class EditAttributes(inkex.EffectExtension):
|
||||||
|
def add_arguments(self, pars):
|
||||||
|
pars.add_argument("-a", "--attributeName", help="attribute name to set")
|
||||||
|
pars.add_argument("-v", "--attributeValue", help="attribute value to set")
|
||||||
|
pars.add_argument("-m", "--mode", default="set", help="mode of operation")
|
||||||
|
|
||||||
|
def effect(self):
|
||||||
|
if not self.options.attributeName: # if attributeName is not given
|
||||||
|
inkex.errormsg("Attribute name not given")
|
||||||
|
return
|
||||||
|
if not self.options.attributeValue: # required to make proper behaviour
|
||||||
|
inkex.errormsg("Please define proper attribute value")
|
||||||
|
return
|
||||||
|
|
||||||
|
elements = self.svg.selected.values()
|
||||||
|
for el in elements:
|
||||||
|
currentAtt = el.attrib.get(self.options.attributeName)
|
||||||
|
if currentAtt is None:
|
||||||
|
currentAtt = ""
|
||||||
|
if self.options.mode == "set":
|
||||||
|
el.set(self.options.attributeName, self.options.attributeValue)
|
||||||
|
elif self.options.mode == "append":
|
||||||
|
el.attrib[self.options.attributeName] = currentAtt + self.options.attributeValue
|
||||||
|
elif self.options.mode == "prefix":
|
||||||
|
el.attrib[self.options.attributeName] = self.options.attributeValue + currentAtt
|
||||||
|
elif self.options.mode == "subtract":
|
||||||
|
el.attrib[self.options.attributeName] = currentAtt.replace(self.options.attributeValue, "")
|
||||||
|
elif self.options.mode == "remove":
|
||||||
|
if self.options.attributeName in el.attrib:
|
||||||
|
del el.attrib[self.options.attributeName]
|
||||||
|
else:
|
||||||
|
inkex.errormsg("Invalid mode: " + self.options.mode)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
EditAttributes().run()
|
21
extensions/fablabchemnitz/edit_attributes/meta.json
Normal file
21
extensions/fablabchemnitz/edit_attributes/meta.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "Edit Attributes",
|
||||||
|
"id": "fablabchemnitz.de.edit_attributes",
|
||||||
|
"path": "edit_attributes",
|
||||||
|
"dependent_extensions": null,
|
||||||
|
"original_name": "Edit Attributes",
|
||||||
|
"original_id": "com.mathem.attrib_editor",
|
||||||
|
"license": "GNU LGPL v2",
|
||||||
|
"license_url": "https://inkscape.org/~MatheM/%E2%98%85simple-attribute-editor+1",
|
||||||
|
"comment": "",
|
||||||
|
"source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.X/src/branch/master/extensions/fablabchemnitz/edit_attributes",
|
||||||
|
"fork_url": "https://inkscape.org/~MatheM/%E2%98%85simple-attribute-editor+1",
|
||||||
|
"documentation_url": "https://stadtfabrikanten.org/display/IFM/Edit+Attributes",
|
||||||
|
"inkscape_gallery_url": null,
|
||||||
|
"main_authors": [
|
||||||
|
"inkscape.org/MatheM",
|
||||||
|
"github.com/vmario89"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
@ -0,0 +1,110 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
|
||||||
|
<name>Filter By Length/Area</name>
|
||||||
|
<id>fablabchemnitz.de.filter_by_length_area</id>
|
||||||
|
<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="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="breakapart" type="bool" gui-text="Break apart selected path(s) into segments" gui-description="Performs CTRL + SHIFT + K to break paths into parts">true</param>
|
||||||
|
<param name="breakapart_total" type="bool" gui-text="Break segments to lines" gui-description="Gives the best results for nodes/<interval> filtering">true</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="cm">cm</option>
|
||||||
|
<option value="px">px</option>
|
||||||
|
<option value="pt">pt</option>
|
||||||
|
<option value="pc">pc</option>
|
||||||
|
<option value="in">in</option>
|
||||||
|
</param>
|
||||||
|
<param name="nodes_interval" type="float" min="0.000" max="99999.000" precision="3" gui-text="Interval">10.000</param>
|
||||||
|
<separator/>
|
||||||
|
<param name="min_filter_enable" type="bool" gui-text="Enable filtering min.">false</param>
|
||||||
|
<param name="min_threshold" type="float" min="0.000" precision="3" max="10000000.000" gui-text="Min. length or area">1.000</param>
|
||||||
|
<param name="min_nodes" type="int" min="0" max="99999" gui-text="Min. nodes/<interval>">2</param>
|
||||||
|
<param name="max_filter_enable" type="bool" gui-text="Enable filtering max.">false</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/<interval>">10000000</param>
|
||||||
|
<param name="precision" type="int" min="0" max="16" gui-text="Precision">3</param>
|
||||||
|
</vbox>
|
||||||
|
<separator/>
|
||||||
|
<vbox>
|
||||||
|
<label appearance="header">Filter</label>
|
||||||
|
<param name="measure" type="optiongroup" appearance="combo" gui-text="By">
|
||||||
|
<option value="length">Length (Unit)</option>
|
||||||
|
<option value="nodes">Nodes per length (Unit)</option>
|
||||||
|
<option value="area">Area (Unit^2)</option>
|
||||||
|
</param>
|
||||||
|
<label appearance="header">Actions</label>
|
||||||
|
<param name="delete" type="bool" gui-text="Delete">false</param>
|
||||||
|
<hbox>
|
||||||
|
<param name="color_mode" type="optiongroup" appearance="combo" gui-text="Color mode">
|
||||||
|
<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>
|
||||||
|
<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 - 2022 / 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>
|
||||||
|
<object-type>all</object-type>
|
||||||
|
<effects-menu>
|
||||||
|
<submenu name="FabLab Chemnitz">
|
||||||
|
<submenu name="Paths - Cut/Intersect/Purge"/>
|
||||||
|
</submenu>
|
||||||
|
</effects-menu>
|
||||||
|
</effect>
|
||||||
|
<script>
|
||||||
|
<command location="inx" interpreter="python">filter_by_length_area.py</command>
|
||||||
|
</script>
|
||||||
|
</inkscape-extension>
|
@ -0,0 +1,263 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
'''
|
||||||
|
Extension for InkScape 1.0+
|
||||||
|
|
||||||
|
Author: Mario Voigt / FabLab Chemnitz
|
||||||
|
Mail: mario.voigt@stadtfabrikanten.org
|
||||||
|
Date: 03.08.2020
|
||||||
|
Last patch: 04.11.2021
|
||||||
|
License: GNU GPL v3
|
||||||
|
|
||||||
|
ToDo:
|
||||||
|
- id sorting: handle ids with/without numbers and sort by number
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import colorsys
|
||||||
|
import copy
|
||||||
|
import inkex
|
||||||
|
from inkex import Color, CubicSuperPath
|
||||||
|
from inkex.bezier import csplength, csparea
|
||||||
|
|
||||||
|
sys.path.append("../remove_empty_groups")
|
||||||
|
sys.path.append("../apply_transformations")
|
||||||
|
|
||||||
|
class FilterByLengthArea(inkex.EffectExtension):
|
||||||
|
|
||||||
|
def add_arguments(self, pars):
|
||||||
|
pars.add_argument('--tab')
|
||||||
|
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("--breakapart", type=inkex.Boolean, default=True, help="Break apart selected path(s) into segments")
|
||||||
|
pars.add_argument("--breakapart_total", type=inkex.Boolean, default=True, help="Gives the best results for nodes/<interval> filtering")
|
||||||
|
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('--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('--max_filter_enable', type=inkex.Boolean, default=False, help='Enable filtering max.')
|
||||||
|
pars.add_argument('--max_threshold', type=float, default=10000000.000, help='Remove paths with an threshold bigger than this value')
|
||||||
|
pars.add_argument('--min_nodes', type=int, default=0, help='Min. nodes/<interval>')
|
||||||
|
pars.add_argument('--max_nodes', type=int, default=10000000, help='Max. nodes/<interval>')
|
||||||
|
pars.add_argument('--nodes_interval', type=float, default=10000000.000, help='Interval')
|
||||||
|
pars.add_argument('--precision', type=int, default=3, help='Precision')
|
||||||
|
pars.add_argument('--measure', default="length")
|
||||||
|
pars.add_argument('--delete', type=inkex.Boolean, default=False)
|
||||||
|
pars.add_argument('--color_mode', default="none")
|
||||||
|
pars.add_argument('--color_single', type=Color, default='0xff00ffff')
|
||||||
|
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 breakContours(self, element, breakelements = None): #this does the same as "CTRL + SHIFT + K"
|
||||||
|
if breakelements == None:
|
||||||
|
breakelements = []
|
||||||
|
if element.tag == inkex.addNS('path','svg'):
|
||||||
|
parent = element.getparent()
|
||||||
|
idx = parent.index(element)
|
||||||
|
idSuffix = 0
|
||||||
|
raw = element.path.to_arrays()
|
||||||
|
subPaths, prev = [], 0
|
||||||
|
if self.options.breakapart_total is False:
|
||||||
|
for i in range(len(raw)): # Breaks compound paths into simple paths
|
||||||
|
if raw[i][0] == 'M' and i != 0:
|
||||||
|
subPaths.append(raw[prev:i])
|
||||||
|
prev = i
|
||||||
|
subPaths.append(raw[prev:])
|
||||||
|
else:
|
||||||
|
rawCopy = element.path.to_arrays() #we need another set of the same path
|
||||||
|
for i in range(len(raw)): # Breaks compound paths into simple paths
|
||||||
|
if i != 0:
|
||||||
|
if raw[i][0] == 'C':
|
||||||
|
rawCopy[i][1] = [raw[i][1][-2], raw[i][1][-1]]
|
||||||
|
elif raw[i][0] == 'L':
|
||||||
|
rawCopy[i][1] = [raw[i][1][0], raw[i][1][1]]
|
||||||
|
elif raw[i][0] == 'Z': #replace Z with another L command (which moves to the coordinates of the first M command in path) to have better overview
|
||||||
|
raw[-1][0] = 'L'
|
||||||
|
raw[-1][1] = raw[0][1]
|
||||||
|
rawCopy[i][0] = 'M' #we really need M. Does not matter if 'L' or 'C'.
|
||||||
|
#self.msg("s1={},s2={}".format(rawCopy[i-1], raw[i]))
|
||||||
|
subPaths.append([rawCopy[i-1], raw[i]])
|
||||||
|
prev = i
|
||||||
|
subPaths = subPaths[::-1]
|
||||||
|
|
||||||
|
for subpath in subPaths:
|
||||||
|
#self.msg(subpath)
|
||||||
|
replacedelement = copy.copy(element)
|
||||||
|
oldId = replacedelement.get('id')
|
||||||
|
csp = CubicSuperPath(subpath)
|
||||||
|
if len(subpath) > 1 and csp[0][0] != csp[0][1]: #avoids pointy paths like M "31.4794 57.6024 Z"
|
||||||
|
replacedelement.set('d', csp)
|
||||||
|
if len(subPaths) == 1:
|
||||||
|
replacedelement.set('id', "{}".format(oldId))
|
||||||
|
else:
|
||||||
|
replacedelement.set('id', "{}-{}".format(oldId, str(idSuffix)))
|
||||||
|
idSuffix += 1
|
||||||
|
parent.insert(idx, replacedelement)
|
||||||
|
breakelements.append(replacedelement)
|
||||||
|
parent.remove(element)
|
||||||
|
for child in element.getchildren():
|
||||||
|
self.breakContours(child, breakelements)
|
||||||
|
return breakelements
|
||||||
|
|
||||||
|
def effect(self):
|
||||||
|
global to_sort, so
|
||||||
|
to_sort = []
|
||||||
|
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.max_threshold = self.svg.unittouu(str(so.max_threshold) + self.svg.unit)
|
||||||
|
unit_factor = 1.0 / self.svg.uutounit(1.0, so.unit)
|
||||||
|
if so.min_threshold == 0 or so.max_threshold == 0:
|
||||||
|
inkex.utils.debug("One or both tresholds are zero. Please adjust.")
|
||||||
|
return
|
||||||
|
|
||||||
|
elements = []
|
||||||
|
if len(self.svg.selected) > 0:
|
||||||
|
for element in self.svg.selection.values():
|
||||||
|
elements.extend(self.breakContours(element, None))
|
||||||
|
else:
|
||||||
|
data = self.document.xpath("//svg:path", namespaces=inkex.NSS)
|
||||||
|
for element in data:
|
||||||
|
elements.extend(self.breakContours(element, None))
|
||||||
|
|
||||||
|
if so.debug is True:
|
||||||
|
inkex.utils.debug("Collecting svg:path 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:
|
||||||
|
csp = element.path.transform(element.composed_transform()).to_superpath()
|
||||||
|
|
||||||
|
if so.measure == "area":
|
||||||
|
area = round(-csparea(csp), so.precision) #is returned as negative value. we need to invert with
|
||||||
|
if (so.min_filter_enable is True and area < (so.min_threshold * (unit_factor * unit_factor))) or \
|
||||||
|
(so.max_filter_enable is True and area >= (so.max_threshold * (unit_factor * unit_factor))) or \
|
||||||
|
(so.min_filter_enable is False and so.max_filter_enable is False): #complete selection
|
||||||
|
if so.debug is True:
|
||||||
|
inkex.utils.debug("id={}, area={:0.3f}{}^2".format(element.get('id'), area, so.unit))
|
||||||
|
to_sort.append({'element': element, 'value': area, 'type': 'area'})
|
||||||
|
|
||||||
|
elif so.measure == "length":
|
||||||
|
slengths, stotal = csplength(csp) #get segment lengths and total length of path in document's internal unit
|
||||||
|
stotal = round(stotal, so.precision)
|
||||||
|
if (so.min_filter_enable is True and stotal < (so.min_threshold * unit_factor)) or \
|
||||||
|
(so.max_filter_enable is True and stotal >= (so.max_threshold * unit_factor)) or \
|
||||||
|
(so.min_filter_enable is False and so.max_filter_enable is False): #complete selection
|
||||||
|
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))
|
||||||
|
to_sort.append({'element': element, 'value': stotal, 'type': 'length'})
|
||||||
|
|
||||||
|
elif so.measure == "nodes":
|
||||||
|
slengths, stotal = csplength(csp) #get segment lengths and total length of path in document's internal unit
|
||||||
|
stotal = round(stotal, so.precision)
|
||||||
|
nodes = len(element.path)
|
||||||
|
if (so.min_filter_enable is True and nodes / stotal < so.min_nodes / self.svg.unittouu(str(so.nodes_interval) + so.unit)) or \
|
||||||
|
(so.max_filter_enable is True and nodes / stotal > so.max_nodes / self.svg.unittouu(str(so.nodes_interval) + so.unit)) or \
|
||||||
|
(so.min_filter_enable is False and so.max_filter_enable is False): #complete selection
|
||||||
|
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))
|
||||||
|
to_sort.append({'element': element, 'value': nodes, 'type': 'nodes'})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
#inkex.utils.debug(e)
|
||||||
|
pass
|
||||||
|
|
||||||
|
for i in range(0, len(to_sort)):
|
||||||
|
element = to_sort[i].get('element')
|
||||||
|
if so.delete is True:
|
||||||
|
element.delete()
|
||||||
|
|
||||||
|
if so.delete is True:
|
||||||
|
return #quit here
|
||||||
|
|
||||||
|
if so.sort_by_value is True:
|
||||||
|
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)):
|
||||||
|
element = to_sort[i].get('element')
|
||||||
|
|
||||||
|
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())
|
||||||
|
else:
|
||||||
|
idx = 0
|
||||||
|
element.getparent().insert(idx, element)
|
||||||
|
|
||||||
|
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)
|
||||||
|
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 len(group) == 0:
|
||||||
|
# group.delete()
|
||||||
|
|
||||||
|
if so.cleanup is 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__':
|
||||||
|
FilterByLengthArea().run()
|
23
extensions/fablabchemnitz/filter_by_length_area/meta.json
Normal file
23
extensions/fablabchemnitz/filter_by_length_area/meta.json
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "Filter By Length/Area",
|
||||||
|
"id": "fablabchemnitz.de.filter_by_length_area",
|
||||||
|
"path": "filter_by_length_area",
|
||||||
|
"dependent_extensions": [
|
||||||
|
"apply_transformations",
|
||||||
|
"remove_empty_groups"
|
||||||
|
],
|
||||||
|
"original_name": "Filter By Length/Area",
|
||||||
|
"original_id": "com.filter_by_length_area",
|
||||||
|
"license": "GNU GPL v3",
|
||||||
|
"license_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.X/src/branch/master/LICENSE",
|
||||||
|
"comment": "",
|
||||||
|
"source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.X/src/branch/master/extensions/fablabchemnitz/filter_by_length_area",
|
||||||
|
"fork_url": null,
|
||||||
|
"documentation_url": "https://stadtfabrikanten.org/pages/viewpage.action?pageId=74645969",
|
||||||
|
"inkscape_gallery_url": null,
|
||||||
|
"main_authors": [
|
||||||
|
"github.com/vmario89"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
@ -0,0 +1,26 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
|
||||||
|
<name>Image Triangulation</name>
|
||||||
|
<id>fablabchemnitz.de.image_triangulation</id>
|
||||||
|
<param name="tab" type="notebook">
|
||||||
|
<page name="tab" gui-text="Options">
|
||||||
|
<param name="num_points" type="int" min="20" max="10000" gui-text="Sampled points:">150</param>
|
||||||
|
<param name="edge_thresh_min" type="int" min="0" max="255" gui-text="Edge detection min">200</param>
|
||||||
|
<param name="edge_thresh_max" type="int" min="0" max="255" gui-text="Edge detection max">255</param>
|
||||||
|
<param name="gradient_fill" type="bool" gui-text="Gradient fill">false</param>
|
||||||
|
<param name="add_corners" type="bool" gui-text="Add corners">false</param>
|
||||||
|
</page>
|
||||||
|
</param>
|
||||||
|
|
||||||
|
<effect>
|
||||||
|
<object-type>all</object-type>
|
||||||
|
<effects-menu>
|
||||||
|
<submenu name="FabLab Chemnitz">
|
||||||
|
<submenu name="Tracing/Images/Edge Detection"/>
|
||||||
|
</submenu>
|
||||||
|
</effects-menu>
|
||||||
|
</effect>
|
||||||
|
<script>
|
||||||
|
<command location="inx" interpreter="python">image_triangulation.py</command>
|
||||||
|
</script>
|
||||||
|
</inkscape-extension>
|
@ -0,0 +1,248 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
'''
|
||||||
|
Copyright (C) 2014 Nicola Romano', romano.nicola@gmail.com
|
||||||
|
|
||||||
|
version 0.1
|
||||||
|
0.1: first working version
|
||||||
|
|
||||||
|
------------------------------------------------------------------------
|
||||||
|
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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
------------------------------------------------------------------------
|
||||||
|
|
||||||
|
'''
|
||||||
|
import base64
|
||||||
|
from io import BytesIO
|
||||||
|
import inkex
|
||||||
|
import os
|
||||||
|
from PIL import Image
|
||||||
|
from lxml import etree
|
||||||
|
import numpy as np
|
||||||
|
from scipy.spatial import Delaunay
|
||||||
|
from scipy.cluster.vq import kmeans2
|
||||||
|
import cv2
|
||||||
|
import urllib.request as urllib
|
||||||
|
|
||||||
|
class ImageTriangulation(inkex.EffectExtension):
|
||||||
|
|
||||||
|
def add_arguments(self, pars):
|
||||||
|
pars.add_argument("-n", "--num_points", type=int, default=100, help="Number of points to be sampled")
|
||||||
|
pars.add_argument("-m", "--edge_thresh_min", type=int, default=200, help="Minimum threshold for edge detection")
|
||||||
|
pars.add_argument("-M", "--edge_thresh_max", type=int, default=255, help="Maximum threshold for edge detection")
|
||||||
|
pars.add_argument("-c", "--add_corners", type=inkex.Boolean, default=0, help="Use corners for triangulation?")
|
||||||
|
pars.add_argument("-g", "--gradient_fill", type=inkex.Boolean, default=0, help="Fill triangles with gradient?")
|
||||||
|
pars.add_argument("-b", "--tab", default='', help="The tab of the interface")
|
||||||
|
|
||||||
|
def draw_SVG_path(self, points, closed, style, parent):
|
||||||
|
pathdesc = "M "
|
||||||
|
for p in points:
|
||||||
|
pathdesc = pathdesc + str(p[0]) + "," + str(p[1]) + " "
|
||||||
|
if closed == 1:
|
||||||
|
pathdesc = pathdesc + "Z"
|
||||||
|
path = etree.SubElement(parent, inkex.addNS('path','svg'), {'style' : str(inkex.Style(style)), 'd' : pathdesc})
|
||||||
|
return path
|
||||||
|
|
||||||
|
def checkImagePath(self, node):
|
||||||
|
"""Embed the data of the selected Image Tag element"""
|
||||||
|
xlink = node.get('xlink:href')
|
||||||
|
if xlink and xlink[:5] == 'data:':
|
||||||
|
# No need, data alread embedded
|
||||||
|
return
|
||||||
|
|
||||||
|
url = urllib.urlparse(xlink)
|
||||||
|
href = urllib.url2pathname(url.path)
|
||||||
|
|
||||||
|
# Primary location always the filename itself.
|
||||||
|
path = self.absolute_href(href or '')
|
||||||
|
|
||||||
|
# Backup directory where we can find the image
|
||||||
|
if not os.path.isfile(path):
|
||||||
|
path = node.get('sodipodi:absref', path)
|
||||||
|
|
||||||
|
if not os.path.isfile(path):
|
||||||
|
inkex.errormsg('File not found "{}". Unable to embed image.').format(path)
|
||||||
|
return
|
||||||
|
|
||||||
|
if (os.path.isfile(path)):
|
||||||
|
return path
|
||||||
|
|
||||||
|
def effect(self):
|
||||||
|
|
||||||
|
# Check we have something selected
|
||||||
|
if len(self.svg.selected) == 0:
|
||||||
|
inkex.errormsg("Please select an image.")
|
||||||
|
exit()
|
||||||
|
else:
|
||||||
|
# Check it is an image
|
||||||
|
for id, obj in self.svg.selected.items():
|
||||||
|
if obj.tag[len(obj.tag)-5:] != "image":
|
||||||
|
inkex.errormsg("The selected object (" + id + ") is not an image, skipping.")
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
self.path = self.checkImagePath(obj) # This also ensures the file exists
|
||||||
|
|
||||||
|
grpname = 'img_triangles'
|
||||||
|
# Make sure that the id/name is unique
|
||||||
|
index = 0
|
||||||
|
while (str(self.svg.get_ids()) in grpname):
|
||||||
|
grpname = 'axis' + str(index)
|
||||||
|
index = index + 1
|
||||||
|
|
||||||
|
grp_name = grpname
|
||||||
|
grp_attribs = {inkex.addNS('label','inkscape'):grp_name}
|
||||||
|
# The group to put everything in
|
||||||
|
grp = etree.SubElement(self.svg.get_current_layer(), 'g', grp_attribs)
|
||||||
|
# Find image size and position in Inkscape
|
||||||
|
try:
|
||||||
|
self.img_x_pos = float(obj.get("x"))
|
||||||
|
self.img_y_pos = float(obj.get("y"))
|
||||||
|
except:
|
||||||
|
self.img_x_pos = 0
|
||||||
|
self.img_y_pos = 0
|
||||||
|
self.img_width = float(obj.get("width"))
|
||||||
|
self.img_height = float(obj.get("height"))
|
||||||
|
|
||||||
|
if self.path is None: #check if image is embedded or linked
|
||||||
|
image_string = obj.get('{http://www.w3.org/1999/xlink}href')
|
||||||
|
# find comma position
|
||||||
|
i = 0
|
||||||
|
while i < 40:
|
||||||
|
if image_string[i] == ',':
|
||||||
|
break
|
||||||
|
i = i + 1
|
||||||
|
im = Image.open(BytesIO(base64.b64decode(image_string[i + 1:len(image_string)])))
|
||||||
|
else:
|
||||||
|
im = Image.open(self.path)
|
||||||
|
|
||||||
|
# IMPORTANT!
|
||||||
|
# The numpy array is accessed as im.data[row,column], that is data[y_coord, x_coord]
|
||||||
|
# Be careful not to pass coordinates as (x,y): rather use (y,x)!
|
||||||
|
im.data = np.asarray(im)
|
||||||
|
# The RGB components of all the pixels in the image
|
||||||
|
self.red, self.green, self.blue = im.data[:,:,0], im.data[:,:,1], im.data[:,:,2]
|
||||||
|
|
||||||
|
# Find real image size
|
||||||
|
(self.img_real_width, self.img_real_height) = im.size
|
||||||
|
|
||||||
|
self.doTriangulation(im, grp)
|
||||||
|
|
||||||
|
# Converts image coordinates to screen coordinates
|
||||||
|
def imgToScreen(self, x, y):
|
||||||
|
newx = x / (self.img_real_width/self.img_width) + self.img_x_pos
|
||||||
|
newy = y / (self.img_real_height/self.img_height) + self.img_y_pos
|
||||||
|
return (newx, newy)
|
||||||
|
|
||||||
|
def createLinearGradient(self, x1, y1, x2, y2, color1, color2, gradID):
|
||||||
|
attribs = {
|
||||||
|
'x1' : str(x1),
|
||||||
|
'y1' : str(y1),
|
||||||
|
'x2' : str(x2),
|
||||||
|
'y2' : str(y2),
|
||||||
|
'id' : gradID,
|
||||||
|
'gradientUnits' : "userSpaceOnUse",
|
||||||
|
'{'+inkex.NSS[u'xlink']+'}href': "#"+gradID
|
||||||
|
}
|
||||||
|
|
||||||
|
svgdefs = self.document.getroot().find(inkex.addNS('defs', 'svg'))
|
||||||
|
gradient = etree.SubElement(svgdefs, inkex.addNS('linearGradient','svg'), attribs)
|
||||||
|
|
||||||
|
attribs = {
|
||||||
|
'offset' : "0%",
|
||||||
|
'style' : "stop-color:"+color1+"; stop-opacity:1"
|
||||||
|
}
|
||||||
|
stop1 = etree.SubElement(gradient, inkex.addNS('stop','svg'), attribs)
|
||||||
|
attribs = {
|
||||||
|
'offset' : "100%",
|
||||||
|
'style' : "stop-color:"+color2+"; stop-opacity:1"
|
||||||
|
}
|
||||||
|
stop2 = etree.SubElement(gradient, inkex.addNS('stop','svg'), attribs)
|
||||||
|
return gradient
|
||||||
|
|
||||||
|
def doTriangulation (self, im, grp):
|
||||||
|
# Read image with OpenCV
|
||||||
|
imcv = np.array(im)
|
||||||
|
#imcv = cv2.imread(self.path)
|
||||||
|
# Convert to grayscale
|
||||||
|
gray = cv2.cvtColor(imcv,cv2.COLOR_RGB2GRAY)
|
||||||
|
gray = np.float32(gray)
|
||||||
|
# Find edges
|
||||||
|
edges = cv2.Canny(imcv, self.options.edge_thresh_min, self.options.edge_thresh_max, 100)
|
||||||
|
# Find coordinates of the edges
|
||||||
|
coords = [(float(x),float(y)) for y, row in enumerate(edges) for x, col in enumerate(row) if col>0]
|
||||||
|
try:
|
||||||
|
pt, idx = kmeans2(np.array(coords), self.options.num_points, minit="points")
|
||||||
|
except ValueError:
|
||||||
|
inkex.utils.debug("Too much points. Reduce sampled points and try again!")
|
||||||
|
exit(1)
|
||||||
|
if self.options.add_corners:
|
||||||
|
# Add the four corners
|
||||||
|
corners = [(0, 0),
|
||||||
|
(self.img_real_width-1, 0),
|
||||||
|
(0, self.img_real_height-1),
|
||||||
|
(self.img_real_width-1, self.img_real_height-1)]
|
||||||
|
|
||||||
|
pt = np.vstack((pt, corners))
|
||||||
|
|
||||||
|
# Perform Delaunay triangulation
|
||||||
|
tri = Delaunay(pt)
|
||||||
|
tri_coord = [(pt[t[0]], pt[t[1]], pt[t[2]]) for t in tri.simplices]
|
||||||
|
tri_colors = [(
|
||||||
|
(self.red[int(t[0][1]),int(t[0][0])], self.green[int(t[0][1]),int(t[0][0])], self.blue[int(t[0][1]),int(t[0][0])]),
|
||||||
|
(self.red[int(t[1][1]),int(t[1][0])], self.green[int(t[1][1]),int(t[1][0])], self.blue[int(t[1][1]),int(t[1][0])]),
|
||||||
|
(self.red[int(t[2][1]),int(t[2][0])], self.green[int(t[2][1]),int(t[2][0])], self.blue[int(t[2][1]),int(t[2][0])])
|
||||||
|
)
|
||||||
|
for t in tri_coord]
|
||||||
|
|
||||||
|
for i, c in enumerate(tri_coord):
|
||||||
|
# Convert to screen coordinates
|
||||||
|
v0 = self.imgToScreen(c[0][0], c[0][1])
|
||||||
|
v1 = self.imgToScreen(c[1][0], c[1][1])
|
||||||
|
v2 = self.imgToScreen(c[2][0], c[2][1])
|
||||||
|
col = tri_colors[i]
|
||||||
|
fill = ""
|
||||||
|
|
||||||
|
if self.options.gradient_fill:
|
||||||
|
color1 = "rgb("+str(col[0][0])+","+str(col[0][1])+","+str(col[0][2])+")"
|
||||||
|
color2 = "rgb("+str(0.5*col[1][0]+0.5*col[2][0])+","+ \
|
||||||
|
str(0.5*col[1][1]+0.5*col[2][1])+","+ \
|
||||||
|
str(0.5*col[1][2]+0.5*col[2][2])+")"
|
||||||
|
gradID = 'linearGradient'
|
||||||
|
# Make sure that the id is inique
|
||||||
|
index = 0
|
||||||
|
while (str(self.svg.get_ids()) in gradID):
|
||||||
|
gradID = 'linearGradient' + str(index)
|
||||||
|
index = index + 1
|
||||||
|
|
||||||
|
#self.doc_ids[gradID]=1
|
||||||
|
|
||||||
|
gradient = self.createLinearGradient(v0[0], v0[1],
|
||||||
|
0.5*(v1[0]+v2[0]), 0.5*(v1[1]+v2[1]),
|
||||||
|
color1, color2, gradID)
|
||||||
|
fill = "url(#"+gradient.get("id")+")"
|
||||||
|
else:
|
||||||
|
fill = "rgb("+str(col[0][0])+","+str(col[0][1])+","+str(col[0][2])+")"
|
||||||
|
|
||||||
|
tri_style = {
|
||||||
|
'stroke-width' : '1px',
|
||||||
|
'stroke-linecap' : 'round',
|
||||||
|
'stroke-opacity' : '1',
|
||||||
|
'fill' : fill,
|
||||||
|
'fill-opacity' : '1',
|
||||||
|
'stroke' : fill
|
||||||
|
}
|
||||||
|
|
||||||
|
self.draw_SVG_path([v0, v1, v2], 1, tri_style, grp)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
ImageTriangulation().run()
|
21
extensions/fablabchemnitz/image_triangulation/meta.json
Normal file
21
extensions/fablabchemnitz/image_triangulation/meta.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "Image Triangulation",
|
||||||
|
"id": "fablabchemnitz.de.image_triangulation",
|
||||||
|
"path": "image_triangulation",
|
||||||
|
"dependent_extensions": null,
|
||||||
|
"original_name": "Triangulation",
|
||||||
|
"original_id": "org.inkscape.triangulation",
|
||||||
|
"license": "GNU GPL v2",
|
||||||
|
"license_url": "https://github.com/nicolaromano/triangulate/blob/master/triangulation.py",
|
||||||
|
"comment": "",
|
||||||
|
"source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.X/src/branch/master/extensions/fablabchemnitz/image_triangulation",
|
||||||
|
"fork_url": "https://github.com/nicolaromano/triangulate",
|
||||||
|
"documentation_url": "https://stadtfabrikanten.org/display/IFM/Image+Triangulation",
|
||||||
|
"inkscape_gallery_url": null,
|
||||||
|
"main_authors": [
|
||||||
|
"github.com/nicolaromano",
|
||||||
|
"github.com/vmario89"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
@ -0,0 +1,33 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
|
||||||
|
<name>Inset Alignment</name>
|
||||||
|
<id>fablabchemnitz.de.inset_alignment</id>
|
||||||
|
<param name="anchor_node" type="optiongroup" appearance="combo" gui-text="Inset relative to">
|
||||||
|
<option value="FIRST_SEL">First selected</option>
|
||||||
|
<option value="LAST_SEL">Last selected</option>
|
||||||
|
<option value="LARGEST">Largest</option>
|
||||||
|
</param>
|
||||||
|
<param name="relative_to_v" type="optiongroup" appearance="radio" gui-text="Vertical alignment">
|
||||||
|
<option value="TOP">Top</option>
|
||||||
|
<option value="CENTRE">Centre</option>
|
||||||
|
<option value="BOTTOM">Bottom</option>
|
||||||
|
</param>
|
||||||
|
<param name="relative_to_h" type="optiongroup" appearance="radio" gui-text="Horizontal alignment">
|
||||||
|
<option value="LEFT">Left</option>
|
||||||
|
<option value="MIDDLE">Middle</option>
|
||||||
|
<option value="RIGHT">Right</option>
|
||||||
|
</param>
|
||||||
|
<param name="inset_x" type="float" precision="1" min="-1000" max="1000" gui-text="Horizontal inset (mm)">10</param>
|
||||||
|
<param name="inset_y" type="float" precision="1" min="-1000" max="1000" gui-text="Vertical inset (mm)">10</param>
|
||||||
|
<effect>
|
||||||
|
<object-type>all</object-type>
|
||||||
|
<effects-menu>
|
||||||
|
<submenu name="FabLab Chemnitz">
|
||||||
|
<submenu name="Transformations"/>
|
||||||
|
</submenu>
|
||||||
|
</effects-menu>
|
||||||
|
</effect>
|
||||||
|
<script>
|
||||||
|
<command location="inx" interpreter="python">inset_alignment.py</command>
|
||||||
|
</script>
|
||||||
|
</inkscape-extension>
|
91
extensions/fablabchemnitz/inset_alignment/inset_alignment.py
Normal file
91
extensions/fablabchemnitz/inset_alignment/inset_alignment.py
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
|
||||||
|
# Copyright 2016 Luke Phillips (lukerazor@hotmail.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 St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
|
||||||
|
# Extension dirs
|
||||||
|
# linux:~/.config/inkscape/extensions
|
||||||
|
# windows: D:\Program Files\Inkscape\share\extensions
|
||||||
|
|
||||||
|
import inkex
|
||||||
|
|
||||||
|
def BoundingBoxArea(node):
|
||||||
|
bb = node.bounding_box()
|
||||||
|
return bb.width * bb.height
|
||||||
|
|
||||||
|
class InsetAlignment(inkex.EffectExtension):
|
||||||
|
|
||||||
|
def add_arguments(self, pars):
|
||||||
|
pars.add_argument('-a', '--anchor_node')
|
||||||
|
pars.add_argument('-v', '--relative_to_v')
|
||||||
|
pars.add_argument('-t', '--relative_to_h')
|
||||||
|
pars.add_argument('-x', '--inset_x', type = float)
|
||||||
|
pars.add_argument('-y', '--inset_y', type = float)
|
||||||
|
|
||||||
|
def GetPaths(self):
|
||||||
|
paths = []
|
||||||
|
|
||||||
|
def effect(self):
|
||||||
|
# make sure we have at least 2 nodes selected
|
||||||
|
if len(self.options.ids) < 2:
|
||||||
|
inkex.utils.debug("You must select at least 2 nodes")
|
||||||
|
return
|
||||||
|
|
||||||
|
# pick the achor node
|
||||||
|
anchor_nodeId = self.options.ids[0] # first selected
|
||||||
|
if self.options.anchor_node == "LAST_SEL": # last selected
|
||||||
|
#inkex.utils.debug("last sel")
|
||||||
|
#inkex.utils.debug(self.options.ids)
|
||||||
|
anchor_nodeId = self.options.ids[-1]
|
||||||
|
elif self.options.anchor_node == "LARGEST": # largest
|
||||||
|
anchor_nodeId = None
|
||||||
|
largestArea = 0
|
||||||
|
for nodeId, node in self.svg.selected.items():
|
||||||
|
nodeArea = BoundingBoxArea(node)
|
||||||
|
if nodeArea > largestArea:
|
||||||
|
anchor_nodeId = nodeId
|
||||||
|
largestArea = nodeArea
|
||||||
|
|
||||||
|
anchorBBox = self.svg.selected[anchor_nodeId].bounding_box()
|
||||||
|
|
||||||
|
# calculate the offsets in mm
|
||||||
|
insetH = self.svg.unittouu("{0}mm".format(self.options.inset_x))
|
||||||
|
insetV = self.svg.unittouu("{0}mm".format(self.options.inset_y))
|
||||||
|
|
||||||
|
otherNodes = [n for i, n in self.svg.selected.items() if i != anchor_nodeId]
|
||||||
|
# for every other Node
|
||||||
|
for node in otherNodes:
|
||||||
|
bbox = node.bounding_box()
|
||||||
|
|
||||||
|
# sort out vertical offset
|
||||||
|
deltaV = (anchorBBox.top - bbox.top) + insetV # ALIGN TOP
|
||||||
|
if self.options.relative_to_v in "BOTTOM":
|
||||||
|
deltaV = (anchorBBox.bottom - bbox.bottom) - insetV # ALIGN BOTTOM
|
||||||
|
if self.options.relative_to_v == "CENTRE":
|
||||||
|
deltaV = (anchorBBox.top + anchorBBox.height/2 - bbox.height/2) - bbox.top # ALIGN CENTRE
|
||||||
|
|
||||||
|
# sort out the horizontal offset
|
||||||
|
deltaH = (anchorBBox.left - bbox.left) + insetH # ALIGN LEFT
|
||||||
|
if self.options.relative_to_h == "RIGHT":
|
||||||
|
deltaH = (anchorBBox.right - bbox.right) - insetH # ALIGN RIGHT
|
||||||
|
if self.options.relative_to_h == "MIDDLE":
|
||||||
|
deltaH = (anchorBBox.left + anchorBBox.width/2 - bbox.width/2) - bbox.left # ALIGN MIDDLE
|
||||||
|
|
||||||
|
tform = inkex.Transform("translate({0}, {1})".format(deltaH, deltaV))
|
||||||
|
node.transform = tform @ node.transform
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
InsetAlignment().run()
|
21
extensions/fablabchemnitz/inset_alignment/meta.json
Normal file
21
extensions/fablabchemnitz/inset_alignment/meta.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "Inset Alignment",
|
||||||
|
"id": "fablabchemnitz.de.inset_alignment",
|
||||||
|
"path": "inset_alignment",
|
||||||
|
"dependent_extensions": null,
|
||||||
|
"original_name": "Inset Alignment",
|
||||||
|
"original_id": "phillips.effect.insetalignment",
|
||||||
|
"license": "GNU GPL v2",
|
||||||
|
"license_url": "https://sourceforge.net/p/razorfoss/svn/HEAD/tree/trunk/Inkscape/InsetAlignmentExtension/InsetAlignment.py",
|
||||||
|
"comment": "",
|
||||||
|
"source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.X/src/branch/master/extensions/fablabchemnitz/inset_alignment",
|
||||||
|
"fork_url": "https://sourceforge.net/p/razorfoss/svn/HEAD/tree/trunk/Inkscape/InsetAlignmentExtension/",
|
||||||
|
"documentation_url": "https://stadtfabrikanten.org/display/IFM/Inset+Alignment",
|
||||||
|
"inkscape_gallery_url": null,
|
||||||
|
"main_authors": [
|
||||||
|
"Luke Phillips:lukerazor@hotmail.com",
|
||||||
|
"github.com/vmario89"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
21
extensions/fablabchemnitz/pixels2objects/meta.json
Normal file
21
extensions/fablabchemnitz/pixels2objects/meta.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "Pixels To Objects",
|
||||||
|
"id": "fablabchemnitz.de.pixels2objects",
|
||||||
|
"path": "pixels2objects",
|
||||||
|
"dependent_extensions": null,
|
||||||
|
"original_name": "Pixels To Objects",
|
||||||
|
"original_id": "org.pakin.filter.pixels2objects",
|
||||||
|
"license": "GNU GPL v3",
|
||||||
|
"license_url": "https://inkscape.org/~pakin/%E2%98%85pixels-to-objects",
|
||||||
|
"comment": "",
|
||||||
|
"source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.X/src/branch/master/extensions/fablabchemnitz/pixels2objects",
|
||||||
|
"fork_url": "https://inkscape.org/~pakin/%E2%98%85pixels-to-objects",
|
||||||
|
"documentation_url": "https://stadtfabrikanten.org/display/IFM/Pixels+To+Objects",
|
||||||
|
"inkscape_gallery_url": null,
|
||||||
|
"main_authors": [
|
||||||
|
"inkscape.org/pakin",
|
||||||
|
"github.com/vmario89"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
40
extensions/fablabchemnitz/pixels2objects/pixels2objects.inx
Normal file
40
extensions/fablabchemnitz/pixels2objects/pixels2objects.inx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
|
||||||
|
<name>Pixels To Objects</name>
|
||||||
|
<id>fablabchemnitz.de.pixels2objects</id>
|
||||||
|
<param name="tab" type="notebook">
|
||||||
|
<page name="Options" gui-text="Options">
|
||||||
|
<param name="color-stroke" type="bool" gui-text="Apply color to stroke">false</param>
|
||||||
|
<param name="color-fill" type="bool" gui-text="Apply color to fill">false</param>
|
||||||
|
<param name="ignore-bg" type="bool" gui-text="Ignore background pixels">true</param>
|
||||||
|
<param name="obj-select" type="optiongroup" appearance="combo" gui-text="Instantiation of multiple objects">
|
||||||
|
<option value="coords">By image coordinates</option>
|
||||||
|
<option value="rr">Round robin</option>
|
||||||
|
<option value="random">Random</option>
|
||||||
|
</param>
|
||||||
|
<param name="scale" type="float" min="0.001" max="1000" precision="3" gui-text="Image coordinate scaling">1</param>
|
||||||
|
</page>
|
||||||
|
<page name="Help" gui-text="Help">
|
||||||
|
<label>Select a bitmapped image and one or more other objects. The
|
||||||
|
Pixels to Objects effect will place one copy of an object at
|
||||||
|
each coordinate in the bitmapped image, from (0, 0) through
|
||||||
|
(width−1, height−1). Options enable objects to have their
|
||||||
|
stroke and/or fill color adjusted to match the corresponding
|
||||||
|
image pixel; background-colored pixels to be either considered
|
||||||
|
or ignored; image coordinates to be scaled up or down; and
|
||||||
|
multiple objects to be assigned to coordinates either randomly
|
||||||
|
or deterministically.</label>
|
||||||
|
</page>
|
||||||
|
</param>
|
||||||
|
<effect>
|
||||||
|
<object-type>all</object-type>
|
||||||
|
<effects-menu>
|
||||||
|
<submenu name="FabLab Chemnitz">
|
||||||
|
<submenu name="Tracing/Images/Edge Detection"/>
|
||||||
|
</submenu>
|
||||||
|
</effects-menu>
|
||||||
|
</effect>
|
||||||
|
<script>
|
||||||
|
<command location="inx" interpreter="python">pixels2objects.py</command>
|
||||||
|
</script>
|
||||||
|
</inkscape-extension>
|
174
extensions/fablabchemnitz/pixels2objects/pixels2objects.py
Executable file
174
extensions/fablabchemnitz/pixels2objects/pixels2objects.py
Executable file
@ -0,0 +1,174 @@
|
|||||||
|
#! /usr/bin/python3
|
||||||
|
|
||||||
|
'''
|
||||||
|
Copyright (C) 2021 Scott Pakin, scott-ink@pakin.org
|
||||||
|
|
||||||
|
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 3 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.
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import inkex
|
||||||
|
import io
|
||||||
|
import PIL.Image
|
||||||
|
import random
|
||||||
|
import sys
|
||||||
|
|
||||||
|
class PixelsToObjects(inkex.EffectExtension):
|
||||||
|
"Copy an object to the coordinates of each pixel in an image."
|
||||||
|
|
||||||
|
def add_arguments(self, pars):
|
||||||
|
'Process program parameters passed in from the UI.'
|
||||||
|
pars.add_argument('--tab', help='The selected UI tab when OK was pressed')
|
||||||
|
pars.add_argument('--scale', type=float, help='Factor by which to scale image coordinates')
|
||||||
|
pars.add_argument('--color-stroke', type=inkex.Boolean, help="Color the object's stroke to match the pixel color")
|
||||||
|
pars.add_argument('--color-fill', type=inkex.Boolean, help='Fill the object with the pixel color')
|
||||||
|
pars.add_argument('--ignore-bg', type=inkex.Boolean, help='Ignore background-colored pixels')
|
||||||
|
pars.add_argument('--obj-select', choices=['coords', 'rr', 'random'], help='Specify how to iterate among multiple selected objects')
|
||||||
|
|
||||||
|
def _get_selection(self):
|
||||||
|
'Return an image and an object.'
|
||||||
|
# Bin the objects in the current selection.
|
||||||
|
img = None
|
||||||
|
objs = []
|
||||||
|
for s in self.svg.selection.values():
|
||||||
|
if isinstance(s, inkex.Image):
|
||||||
|
if img == None:
|
||||||
|
img = s # First image encountered
|
||||||
|
else:
|
||||||
|
objs.append(s) # All remaining images are consider "other" objects
|
||||||
|
else:
|
||||||
|
objs.append(s) # All non-images are "other" objects
|
||||||
|
|
||||||
|
# Ensure the selection is valid.
|
||||||
|
if img == None or objs == []:
|
||||||
|
inkex.utils.errormsg(_('Pixels to Objects requires that one image and at least one additional object be selected.'))
|
||||||
|
sys.exit(1)
|
||||||
|
return img, objs
|
||||||
|
|
||||||
|
def _generate_coords2obj(self, objs):
|
||||||
|
'''Return a function that maps pixel coordinates to an object in a
|
||||||
|
given list of SVG objects.'''
|
||||||
|
n_objs = len(objs)
|
||||||
|
obj_select = self.options.obj_select
|
||||||
|
if obj_select == 'coords':
|
||||||
|
return lambda x, y: objs[(x + y)%n_objs]
|
||||||
|
if obj_select == 'rr':
|
||||||
|
idx = 0
|
||||||
|
def coords2obj_rr(x, y):
|
||||||
|
nonlocal idx
|
||||||
|
obj = objs[idx]
|
||||||
|
idx = (idx + 1)%len(objs)
|
||||||
|
return obj
|
||||||
|
return coords2obj_rr
|
||||||
|
if obj_select == 'random':
|
||||||
|
return lambda x, y: objs[random.randint(0, n_objs - 1)]
|
||||||
|
inkex.errormessage(_('internal error: unhandled object-selection type "%s"' % obj_select))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
def _copy_object_to(self, obj, xy):
|
||||||
|
'''Copy an object and center the copy on the given coordinates,
|
||||||
|
optionally recoloring its stroke and/or fill'''
|
||||||
|
# Create a transform that moves the object to the origin.
|
||||||
|
xform = inkex.Transform(obj.get('transform'))
|
||||||
|
pos = obj.bounding_box(obj.composed_transform()).center
|
||||||
|
xform.add_translate(-pos)
|
||||||
|
|
||||||
|
# Modify the transform to move the object to the target coordinates.
|
||||||
|
xform.add_translate(xy)
|
||||||
|
|
||||||
|
# Duplicate the object and apply the transform.
|
||||||
|
new_obj = obj.duplicate()
|
||||||
|
new_obj.update(transform=xform)
|
||||||
|
return new_obj
|
||||||
|
|
||||||
|
def _read_image(self, img_elt):
|
||||||
|
'Read an image from either the SVG file itself or an external file.'
|
||||||
|
# Read the image from an external file.
|
||||||
|
fname = img_elt.get('sodipodi:absref')
|
||||||
|
if fname != None:
|
||||||
|
# Fully qualified filename. Read it directly.
|
||||||
|
return PIL.Image.open(fname)
|
||||||
|
xlink = img_elt.get('xlink:href')
|
||||||
|
if not xlink.startswith('data:'):
|
||||||
|
# Unqualified filename. Try reading it directly although there's a
|
||||||
|
# good chance this will fail.
|
||||||
|
return PIL.Image.open(fname)
|
||||||
|
|
||||||
|
# Read an image embedded in the SVG file itself.
|
||||||
|
try:
|
||||||
|
mime, dtype_data = xlink[5:].split(';', 1)
|
||||||
|
dtype, data64 = dtype_data.split(',', 1)
|
||||||
|
except ValueError:
|
||||||
|
inkex.errormsg('failed to parse embedded image data')
|
||||||
|
sys.exit(1)
|
||||||
|
if dtype != 'base64':
|
||||||
|
inkex.errormsg('embedded image is encoded as %s, but this plugin supports only base64' % dtype)
|
||||||
|
sys.exit(1)
|
||||||
|
raw_data = base64.decodebytes(data64.encode('utf-8'))
|
||||||
|
return PIL.Image.open(io.BytesIO(raw_data))
|
||||||
|
|
||||||
|
def _guess_background_color(self, img):
|
||||||
|
'Return the most commonly occurring color, assuming it represents the background.'
|
||||||
|
wd, ht = img.size
|
||||||
|
tally, bg_color = max(img.getcolors(wd*ht))
|
||||||
|
return bg_color
|
||||||
|
|
||||||
|
def _pixels_to_objects(self, img, coords2obj, bg_color, scale, change_stroke, change_fill):
|
||||||
|
'''Perform the core functionality of this extension, using pixel
|
||||||
|
coordinates to place object replicas.'''
|
||||||
|
# Create a new group for all the objects we'll create.
|
||||||
|
group = inkex.Group()
|
||||||
|
parent = self.svg.selection.first().getparent()
|
||||||
|
parent.add(group)
|
||||||
|
|
||||||
|
# Convert each pixel to a replica of the object, optionally
|
||||||
|
# recoloring it.
|
||||||
|
wd, ht = img.size
|
||||||
|
for y in range(ht):
|
||||||
|
for x in range(wd):
|
||||||
|
xy = (x, y)
|
||||||
|
pix = img.getpixel(xy)
|
||||||
|
if pix == bg_color:
|
||||||
|
continue
|
||||||
|
obj = coords2obj(x, y)
|
||||||
|
new_obj = self._copy_object_to(obj, inkex.Vector2d(xy)*scale)
|
||||||
|
if change_stroke:
|
||||||
|
new_obj.style['stroke'] = '#%02x%02x%02x' % (pix[0], pix[1], pix[2])
|
||||||
|
new_obj.style['stroke-opacity'] = '%.10g' % (pix[3]/255.0)
|
||||||
|
if change_fill:
|
||||||
|
new_obj.style['fill'] = '#%02x%02x%02x' % (pix[0], pix[1], pix[2])
|
||||||
|
new_obj.style['fill-opacity'] = '%.10g' % (pix[3]/255.0)
|
||||||
|
group.add(new_obj)
|
||||||
|
|
||||||
|
def effect(self):
|
||||||
|
'''Given a bitmap image and an object, copy the object to each
|
||||||
|
coordinate of the image that contains a colored pixel.'''
|
||||||
|
img_elt, objs = self._get_selection()
|
||||||
|
img = self._read_image(img_elt).convert('RGBA')
|
||||||
|
if self.options.ignore_bg:
|
||||||
|
bg_color = self._guess_background_color(img)
|
||||||
|
else:
|
||||||
|
bg_color = None
|
||||||
|
scale = self.options.scale
|
||||||
|
change_stroke = self.options.color_stroke
|
||||||
|
change_fill = self.options.color_fill
|
||||||
|
coords2obj = self._generate_coords2obj(objs)
|
||||||
|
self._pixels_to_objects(img, coords2obj, bg_color, scale, change_stroke, change_fill)
|
||||||
|
img.close()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
PixelsToObjects().run()
|
20
extensions/fablabchemnitz/primitive/meta.json
Normal file
20
extensions/fablabchemnitz/primitive/meta.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "Primitive (Michael Fogleman)",
|
||||||
|
"id": "fablabchemnitz.de.primitive",
|
||||||
|
"path": "primitive",
|
||||||
|
"dependent_extensions": null,
|
||||||
|
"original_name": "Primitive (Michael Fogleman)",
|
||||||
|
"original_id": "fablabchemnitz.de.primitive",
|
||||||
|
"license": "GNU GPL v3",
|
||||||
|
"license_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.X/src/branch/master/LICENSE",
|
||||||
|
"comment": "",
|
||||||
|
"source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.X/src/branch/master/extensions/fablabchemnitz/primitive",
|
||||||
|
"fork_url": "https://github.com/fogleman/primitive",
|
||||||
|
"documentation_url": "https://stadtfabrikanten.org/pages/viewpage.action?pageId=78807580",
|
||||||
|
"inkscape_gallery_url": "https://inkscape.org/~MarioVoigt/%E2%98%85primitive-for-inkscape",
|
||||||
|
"main_authors": [
|
||||||
|
"github.com/vmario89"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
BIN
extensions/fablabchemnitz/primitive/primitive
Executable file
BIN
extensions/fablabchemnitz/primitive/primitive
Executable file
Binary file not shown.
BIN
extensions/fablabchemnitz/primitive/primitive.exe
Normal file
BIN
extensions/fablabchemnitz/primitive/primitive.exe
Normal file
Binary file not shown.
72
extensions/fablabchemnitz/primitive/primitive.inx
Normal file
72
extensions/fablabchemnitz/primitive/primitive.inx
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
|
||||||
|
<name>Primitive (Michael Fogleman)</name>
|
||||||
|
<id>fablabchemnitz.de.primitive</id>
|
||||||
|
<param name="tab" type="notebook">
|
||||||
|
<page name="tab_general" gui-text="Primitive">
|
||||||
|
<label appearance="header">Settings</label>
|
||||||
|
<separator/>
|
||||||
|
<param name="keeporiginal" type="bool" gui-text="Keep original image on canvas">false</param>
|
||||||
|
<param name="cliprect" type="bool" gui-text="Draw clipping rectangle">true</param>
|
||||||
|
<param name="n" type="int" min="0" max="99999" gui-text="Number of shapes">100</param>
|
||||||
|
<param name="m" appearance="combo" type="optiongroup" gui-text="Mode">
|
||||||
|
<option value="0">Combo</option>
|
||||||
|
<option value="1">Triangle</option>
|
||||||
|
<option value="2">Rectangle</option>
|
||||||
|
<option value="3">Ellipse</option>
|
||||||
|
<option value="4">Circle</option>
|
||||||
|
<option value="5">Rotated rectangle</option>
|
||||||
|
<option value="6">Beziers</option>
|
||||||
|
<option value="7">Rotated ellipse</option>
|
||||||
|
<option value="8">Polygon</option>
|
||||||
|
</param>
|
||||||
|
<param name="rep" type="int" min="0" max="99999" gui-text="Extra shapes/iteration" gui-description="Sdd N extra shapes each iteration with reduced search (mostly good for beziers)">0</param>
|
||||||
|
<param name="r" type="int" min="1" max="99999" gui-text="Resize to size before processing (px)">256</param>
|
||||||
|
<param name="s" type="int" min="1" max="99999" gui-text="Output image size (px)">1024</param>
|
||||||
|
<param name="a" type="int" min="0" max="255" gui-text="Color alpha" gui-description="Use 0 to let the algorithm choose alpha for each shape">128</param>
|
||||||
|
<param name="bg_enabled" type="bool" gui-text="Use average starting background color">true</param>
|
||||||
|
<param name="bg" type="color" appearance="colorbutton" gui-text="Starting background color" gui-description="You need to disable average starting background to use this option">255</param>
|
||||||
|
<param name="j" type="int" min="0" max="32" gui-text="Number of parallel workers" gui-description="Default (0) uses all cores">0</param>
|
||||||
|
</page>
|
||||||
|
<page name="tab_about" gui-text="About">
|
||||||
|
<label appearance="header">Primitive (Michael Fogleman)</label>
|
||||||
|
<label>Primitive - Reproducing images with geometric primitives written in Go. Wrapped for Inkscape.</label>
|
||||||
|
<label>2020 - 2022 / written by Mario Voigt (Stadtfabrikanten e.V. / FabLab Chemnitz)</label>
|
||||||
|
<spacer/>
|
||||||
|
<label appearance="header">Online Documentation</label>
|
||||||
|
<label appearance="url">https://y.stadtfabrikanten.org/primitive</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">Third Party Modules</label>
|
||||||
|
<label appearance="url">https://github.com/fogleman/primitive</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 needs-live-preview="true">
|
||||||
|
<object-type>all</object-type>
|
||||||
|
<effects-menu>
|
||||||
|
<submenu name="FabLab Chemnitz">
|
||||||
|
<submenu name="Tracing/Images/Edge Detection"/>
|
||||||
|
</submenu>
|
||||||
|
</effects-menu>
|
||||||
|
</effect>
|
||||||
|
<script>
|
||||||
|
<command location="inx" interpreter="python">primitive.py</command>
|
||||||
|
</script>
|
||||||
|
</inkscape-extension>
|
195
extensions/fablabchemnitz/primitive/primitive.py
Normal file
195
extensions/fablabchemnitz/primitive/primitive.py
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import inkex
|
||||||
|
import os
|
||||||
|
import base64
|
||||||
|
import urllib.request as urllib
|
||||||
|
from PIL import Image
|
||||||
|
from io import BytesIO
|
||||||
|
from lxml import etree
|
||||||
|
from inkex import Color
|
||||||
|
|
||||||
|
"""
|
||||||
|
Extension for InkScape 1.X
|
||||||
|
Features
|
||||||
|
- Primitive - Reproducing images with geometric primitives written in Go.
|
||||||
|
|
||||||
|
Author: Mario Voigt / FabLab Chemnitz
|
||||||
|
Mail: mario.voigt@stadtfabrikanten.org
|
||||||
|
Date: 21.08.2020
|
||||||
|
Last patch: 23.08.2020
|
||||||
|
License: GNU GPL v3
|
||||||
|
|
||||||
|
Used version of Primitive: https://github.com/fogleman/primitive/commit/0373c216458be1c4b40655b796a3aefedf8b7d23
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Primitive (inkex.EffectExtension):
|
||||||
|
|
||||||
|
def rgbToHex(self, pickerColor):
|
||||||
|
longcolor = int(pickerColor)
|
||||||
|
if longcolor < 0:
|
||||||
|
longcolor = longcolor & 0xFFFFFFFF
|
||||||
|
return '#' + format(longcolor >> 8, '06X')
|
||||||
|
|
||||||
|
def checkImagePath(self, node):
|
||||||
|
xlink = node.get('xlink:href')
|
||||||
|
if xlink and xlink[:5] == 'data:':
|
||||||
|
# No need, data alread embedded
|
||||||
|
return
|
||||||
|
|
||||||
|
url = urllib.urlparse(xlink)
|
||||||
|
href = urllib.url2pathname(url.path)
|
||||||
|
|
||||||
|
# Primary location always the filename itself.
|
||||||
|
path = self.absolute_href(href or '')
|
||||||
|
|
||||||
|
# Backup directory where we can find the image
|
||||||
|
if not os.path.isfile(path):
|
||||||
|
path = node.get('sodipodi:absref', path)
|
||||||
|
|
||||||
|
if not os.path.isfile(path):
|
||||||
|
inkex.errormsg('File not found "{}". Unable to embed image.').format(path)
|
||||||
|
return
|
||||||
|
|
||||||
|
if (os.path.isfile(path)):
|
||||||
|
return path
|
||||||
|
|
||||||
|
def add_arguments(self, pars):
|
||||||
|
pars.add_argument("--tab")
|
||||||
|
pars.add_argument("--keeporiginal", type=inkex.Boolean, default=False, help="Keep original image on canvas")
|
||||||
|
pars.add_argument("--cliprect", type=inkex.Boolean, default=True, help="Draw clipping rectangle")
|
||||||
|
pars.add_argument("--n", type=int, default=100, help="Number of shapes")
|
||||||
|
pars.add_argument("--m", default=1, help="Mode")
|
||||||
|
pars.add_argument("--rep", type=int, default=0,help="Extra shapes/iteration")
|
||||||
|
pars.add_argument("--r", type=int, default=256, help="Resize to size before processing (px)")
|
||||||
|
pars.add_argument("--s", type=int, default=1024, help="Output image size (px)")
|
||||||
|
pars.add_argument("--a", type=int, default=128, help="Color alpha")
|
||||||
|
pars.add_argument("--bg_enabled", type=inkex.Boolean, default=True, help="Use average starting background color")
|
||||||
|
pars.add_argument("--bg", type=Color, default=255, help="Starting background color")
|
||||||
|
pars.add_argument("--j", type=int, default=0, help="Number of parallel workers")
|
||||||
|
|
||||||
|
def effect(self):
|
||||||
|
|
||||||
|
if (self.options.ids):
|
||||||
|
for node in self.svg.selected.values():
|
||||||
|
if node.tag == inkex.addNS('image', 'svg'):
|
||||||
|
self.path = self.checkImagePath(node) # This also ensures the file exists
|
||||||
|
if self.path is None: # check if image is embedded or linked
|
||||||
|
image_string = node.get('{http://www.w3.org/1999/xlink}href')
|
||||||
|
# find comma position
|
||||||
|
i = 0
|
||||||
|
while i < 40:
|
||||||
|
if image_string[i] == ',':
|
||||||
|
break
|
||||||
|
i = i + 1
|
||||||
|
image = Image.open(BytesIO(base64.b64decode(image_string[i + 1:len(image_string)])))
|
||||||
|
else:
|
||||||
|
image = Image.open(self.path)
|
||||||
|
|
||||||
|
if node.get('width')[-1].isdigit() is False or node.get('height')[-1].isdigit() is False:
|
||||||
|
inkex.utils.debug("Image seems to have some weird dimensions in XML structure. Please remove units from width and height attributes at <svg:image>")
|
||||||
|
return
|
||||||
|
|
||||||
|
parent = node.getparent()
|
||||||
|
if parent is not None and parent != self.document.getroot():
|
||||||
|
tpc = parent.composed_transform()
|
||||||
|
x_offset = tpc.e
|
||||||
|
y_offset = tpc.f
|
||||||
|
else:
|
||||||
|
x_offset = 0.0
|
||||||
|
y_offset = 0.0
|
||||||
|
|
||||||
|
# Write the embedded or linked image to temporary directory
|
||||||
|
if os.name == "nt":
|
||||||
|
exportfile = "Primitive.png"
|
||||||
|
else:
|
||||||
|
exportfile = "/tmp/Primitive.png"
|
||||||
|
|
||||||
|
if image.mode != 'RGB':
|
||||||
|
image = image.convert('RGB')
|
||||||
|
image.save(exportfile, "png")
|
||||||
|
|
||||||
|
## Build up Primitive command according to your settings from extension GUI
|
||||||
|
if os.name == "nt":
|
||||||
|
command = "primitive"
|
||||||
|
else:
|
||||||
|
command = "./primitive"
|
||||||
|
command += " -m " + str(self.options.m)
|
||||||
|
command += " -rep " + str(self.options.rep)
|
||||||
|
command += " -r " + str(self.options.r)
|
||||||
|
command += " -s " + str(self.options.s)
|
||||||
|
command += " -a " + str(self.options.a)
|
||||||
|
if not self.options.bg_enabled:
|
||||||
|
command += " -bg " + self.rgbToHex(self.options.bg)
|
||||||
|
command += " -j " + str(self.options.j)
|
||||||
|
command += " -i " + exportfile
|
||||||
|
command += " -o " + exportfile + ".svg"
|
||||||
|
command += " -n " + str(self.options.n)
|
||||||
|
|
||||||
|
#inkex.utils.debug(command)
|
||||||
|
|
||||||
|
# Create the vector new SVG file
|
||||||
|
with os.popen(command, "r") as proc:
|
||||||
|
result = proc.read()
|
||||||
|
#inkex.utils.debug(result)
|
||||||
|
|
||||||
|
# proceed if new SVG file was successfully created
|
||||||
|
doc = None
|
||||||
|
if os.path.exists(exportfile + ".svg"):
|
||||||
|
# Delete the temporary png file again because we do not need it anymore
|
||||||
|
if os.path.exists(exportfile):
|
||||||
|
os.remove(exportfile)
|
||||||
|
|
||||||
|
# new parse the SVG file and insert it as new group into the current document tree
|
||||||
|
doc = etree.parse(exportfile + ".svg").getroot()
|
||||||
|
|
||||||
|
newGroup = self.document.getroot().add(inkex.Group())
|
||||||
|
newGroup.attrib['transform'] = "matrix({:0.6f}, 0, 0, {:0.6f}, {:0.6f}, {:0.6f})".format(
|
||||||
|
float(node.get('width')) / float(doc.get('width')),
|
||||||
|
float(node.get('height')) / float(doc.get('height')),
|
||||||
|
float(node.get('x')) + x_offset,
|
||||||
|
float(node.get('y')) + y_offset
|
||||||
|
)
|
||||||
|
|
||||||
|
for children in doc:
|
||||||
|
newGroup.append(children)
|
||||||
|
|
||||||
|
# Delete the temporary svg file
|
||||||
|
if os.path.exists(exportfile + ".svg"):
|
||||||
|
try:
|
||||||
|
os.remove(exportfile + ".svg")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
else:
|
||||||
|
inkex.utils.debug("Error while creating output file! :-( The \"primitive\" executable seems to be missing, has no exec permissions or platform is imcompatible.")
|
||||||
|
exit(1)
|
||||||
|
#remove the old image or not
|
||||||
|
if self.options.keeporiginal is not True:
|
||||||
|
node.delete()
|
||||||
|
|
||||||
|
# create clip path to remove the stuffy surroundings
|
||||||
|
if self.options.cliprect:
|
||||||
|
path = '//svg:defs'
|
||||||
|
defslist = self.document.getroot().xpath(path, namespaces=inkex.NSS)
|
||||||
|
if len(defslist) > 0:
|
||||||
|
defs = defslist[0]
|
||||||
|
clipPathData = {inkex.addNS('label', 'inkscape'):'imagetracerClipPath', 'clipPathUnits':'userSpaceOnUse', 'id':'imagetracerClipPath'}
|
||||||
|
clipPath = etree.SubElement(defs, 'clipPath', clipPathData)
|
||||||
|
#inkex.utils.debug(image.width)
|
||||||
|
clipBox = {
|
||||||
|
'x':str(0),
|
||||||
|
'y':str(0),
|
||||||
|
'width':str(doc.get('width')),
|
||||||
|
'height':str(doc.get('height')),
|
||||||
|
'style':'fill:#000000; stroke:none; fill-opacity:1;'
|
||||||
|
}
|
||||||
|
etree.SubElement(clipPath, 'rect', clipBox)
|
||||||
|
#etree.SubElement(newGroup, 'g', {inkex.addNS('label','inkscape'):'imagetracerjs', 'clip-path':"url(#imagetracerClipPath)"})
|
||||||
|
newGroup.set('clip-path','url(#imagetracerClipPath)')
|
||||||
|
else:
|
||||||
|
inkex.utils.debug("No image found for tracing. Please select an image first.")
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
Primitive().run()
|
20
extensions/fablabchemnitz/scale_to_size/meta.json
Normal file
20
extensions/fablabchemnitz/scale_to_size/meta.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "Scale To Size",
|
||||||
|
"id": "fablabchemnitz.de.scale_to_size",
|
||||||
|
"path": "scale_to_size",
|
||||||
|
"dependent_extensions": null,
|
||||||
|
"original_name": "Scale To Size",
|
||||||
|
"original_id": "fablabchemnitz.de.scale_to_size",
|
||||||
|
"license": "GNU GPL v3",
|
||||||
|
"license_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.X/src/branch/master/LICENSE",
|
||||||
|
"comment": "",
|
||||||
|
"source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.X/src/branch/master/extensions/fablabchemnitz/scale_to_size",
|
||||||
|
"fork_url": null,
|
||||||
|
"documentation_url": "https://stadtfabrikanten.org/pages/viewpage.action?pageId=76054572",
|
||||||
|
"inkscape_gallery_url": null,
|
||||||
|
"main_authors": [
|
||||||
|
"github.com/vmario89"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
33
extensions/fablabchemnitz/scale_to_size/scale_to_size.inx
Normal file
33
extensions/fablabchemnitz/scale_to_size/scale_to_size.inx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
|
||||||
|
<name>Scale To Size</name>
|
||||||
|
<id>fablabchemnitz.de.scale_to_size</id>
|
||||||
|
<param name="unit" gui-text="Unit" type="optiongroup" appearance="combo">
|
||||||
|
<option value="mm">mm</option>
|
||||||
|
<option value="cm">cm</option>
|
||||||
|
<option value="in">in</option>
|
||||||
|
<option value="pt">pt</option>
|
||||||
|
<option value="px">px</option>
|
||||||
|
</param>
|
||||||
|
<param name="expected_size" type="float" precision="3" min="0.001" max="10000.000" gui-text="Expected size: ">100.000</param>
|
||||||
|
<param name="copies" type="int" min="0" max="99999" gui-text="Count of copies" gui-description="0 means just the original element is transformed.">0</param>
|
||||||
|
<param name="scale_increment" type="float" precision="3" min="-10000.000" max="10000.000" gui-text="Scale increment (in selected units): ">0.000</param>
|
||||||
|
<param name="scale_type" type="optiongroup" appearance="combo" gui-text="Scaling type:">
|
||||||
|
<option value="Horizontal">Horizontal</option>
|
||||||
|
<option value="Vertical">Vertical</option>
|
||||||
|
<option value="Uniform">Uniform</option>
|
||||||
|
</param>
|
||||||
|
<param name="keep_aspect" type="bool" gui-text="Keep aspect ratio" gui-description="Does not apply for uniform scaling.">true</param>
|
||||||
|
<label>This effect will measure the selected paths and scale them to have the given size. Does not work for objects (you need to convert)!</label>
|
||||||
|
<effect>
|
||||||
|
<object-type>all</object-type>
|
||||||
|
<effects-menu>
|
||||||
|
<submenu name="FabLab Chemnitz">
|
||||||
|
<submenu name="Transformations" />
|
||||||
|
</submenu>
|
||||||
|
</effects-menu>
|
||||||
|
</effect>
|
||||||
|
<script>
|
||||||
|
<command location="inx" interpreter="python">scale_to_size.py</command>
|
||||||
|
</script>
|
||||||
|
</inkscape-extension>
|
108
extensions/fablabchemnitz/scale_to_size/scale_to_size.py
Normal file
108
extensions/fablabchemnitz/scale_to_size/scale_to_size.py
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# Author: Mario Voigt / FabLab Chemnitz
|
||||||
|
# Mail: mario.voigt@stadtfabrikanten.org
|
||||||
|
# Date: 04.08.2020
|
||||||
|
# License: GNU GPL v3
|
||||||
|
|
||||||
|
import inkex
|
||||||
|
from inkex.paths import CubicSuperPath, Path
|
||||||
|
from inkex import Transform
|
||||||
|
from lxml import etree
|
||||||
|
from inkex.styles import Style
|
||||||
|
import copy
|
||||||
|
|
||||||
|
# This extension can scale any object or path on X, Y or both axes. This addon is kind of obsolete because you can do the same from transforms menu
|
||||||
|
|
||||||
|
class ScaleToSize(inkex.EffectExtension):
|
||||||
|
|
||||||
|
def add_arguments(self, pars):
|
||||||
|
pars.add_argument('--unit')
|
||||||
|
pars.add_argument("--keep_aspect", type=inkex.Boolean, default=True, help="Does not apply for uniform scaling")
|
||||||
|
pars.add_argument("--expected_size", type=float, default=100.000, help="The expected size of the object")
|
||||||
|
pars.add_argument("--copies", type=int, default=1, help="Count of copies")
|
||||||
|
pars.add_argument("--scale_increment", type=float, default=0.000, help="Scale increment")
|
||||||
|
pars.add_argument("--scale_type", default="Horizontal", help="Scale type (Uniform, Horizontal, Vertical)")
|
||||||
|
pars.add_argument("--description")
|
||||||
|
|
||||||
|
def effect(self):
|
||||||
|
unit_factor = 1.0 / self.svg.uutounit(1.0,self.options.unit)
|
||||||
|
so = self.options
|
||||||
|
scale_factor = self.svg.unittouu("1px")
|
||||||
|
#get recent XY coordinates (top left corner of the bounding box)
|
||||||
|
for element in self.svg.selected.values():
|
||||||
|
allCopies = []
|
||||||
|
allCopies.append(element)
|
||||||
|
for aCopy in range(so.copies):
|
||||||
|
newElem = copy.copy(element)
|
||||||
|
oldId = element.get('id')
|
||||||
|
newElem.set('id', "{}_scalecopy_{}".format(oldId, aCopy))
|
||||||
|
parent = element.getparent()
|
||||||
|
parent.insert(parent.index(element) + 1 + aCopy, newElem)
|
||||||
|
allCopies.append(newElem)
|
||||||
|
|
||||||
|
i = 1
|
||||||
|
for element in allCopies:
|
||||||
|
i += 1
|
||||||
|
if isinstance (element, inkex.Rectangle) or \
|
||||||
|
isinstance (element, inkex.Circle) or \
|
||||||
|
isinstance (element, inkex.Ellipse):
|
||||||
|
bbox = element.bounding_box() * unit_factor
|
||||||
|
new_horiz_scale = (so.expected_size + so.scale_increment * i) * unit_factor / bbox.width / scale_factor
|
||||||
|
new_vert_scale = (so.expected_size + so.scale_increment * i) * unit_factor / bbox.height / scale_factor
|
||||||
|
else:
|
||||||
|
bbox = element.bounding_box()
|
||||||
|
new_horiz_scale = (so.expected_size + so.scale_increment * i) * unit_factor / bbox.width
|
||||||
|
new_vert_scale = (so.expected_size + so.scale_increment * i) * unit_factor / bbox.height
|
||||||
|
if self.options.scale_type == "Horizontal":
|
||||||
|
if self.options.keep_aspect is False:
|
||||||
|
translation_matrix = [[new_horiz_scale, 0.0, 0.0], [0.0, 1.0, 0.0]]
|
||||||
|
else:
|
||||||
|
translation_matrix = [[new_horiz_scale, 0.0, 0.0], [0.0, new_horiz_scale, 0.0]]
|
||||||
|
elif self.options.scale_type == "Vertical":
|
||||||
|
if self.options.keep_aspect is False:
|
||||||
|
translation_matrix = [[1.0, 0.0, 0.0], [0.0, new_vert_scale, 0.0]]
|
||||||
|
else:
|
||||||
|
translation_matrix = [[new_vert_scale, 0.0, 0.0], [0.0, new_vert_scale, 0.0]]
|
||||||
|
else: #Uniform
|
||||||
|
translation_matrix = [[new_horiz_scale, 0.0, 0.0], [0.0, new_vert_scale, 0.0]]
|
||||||
|
element.transform = Transform(translation_matrix) @ element.composed_transform()
|
||||||
|
|
||||||
|
# now that the element moved we need to get the elements XY coordinates again to fix them
|
||||||
|
bbox_new = element.bounding_box()
|
||||||
|
|
||||||
|
#inkex.utils.debug(cx)
|
||||||
|
#inkex.utils.debug(cy)
|
||||||
|
#inkex.utils.debug(cx_new)
|
||||||
|
#inkex.utils.debug(cy_new)
|
||||||
|
|
||||||
|
# We remove the transformation attribute from SVG XML tree in case it's regular path. In case the element is an object like arc,circle, ellipse or star we have the attribute "sodipodi:type". Then we do not play with it's path because the size transformation will not apply (this code block is ugly)
|
||||||
|
if element.get ('sodipodi:type') is None and 'd' in element.attrib:
|
||||||
|
#inkex.utils.debug("it's a path!")
|
||||||
|
d = element.get('d')
|
||||||
|
p = CubicSuperPath(d)
|
||||||
|
transf = Transform(element.get("transform", None))
|
||||||
|
if 'transform' in element.attrib:
|
||||||
|
del element.attrib['transform']
|
||||||
|
p = Path(p).to_absolute().transform(transf, True)
|
||||||
|
element.set('d', Path(CubicSuperPath(p).to_path()))
|
||||||
|
#else:
|
||||||
|
#inkex.utils.debug("it's an object!")
|
||||||
|
|
||||||
|
#we perform second translation to reset the center of the path
|
||||||
|
if isinstance (element, inkex.Rectangle) or \
|
||||||
|
isinstance (element, inkex.Circle) or \
|
||||||
|
isinstance (element, inkex.Ellipse):
|
||||||
|
translation_matrix = [
|
||||||
|
[1.0, 0.0, scale_factor * (bbox.left - bbox_new.left + (bbox.width - bbox_new.width)/2)],
|
||||||
|
[0.0, 1.0, scale_factor * (bbox.top - bbox_new.top + (bbox.height - bbox_new.height)/2)]
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
translation_matrix = [
|
||||||
|
[1.0, 0.0, bbox.left - bbox_new.left + (bbox.width - bbox_new.width)/2],
|
||||||
|
[0.0, 1.0, bbox.top - bbox_new.top + (bbox.height - bbox_new.height)/2]
|
||||||
|
]
|
||||||
|
element.transform = Transform(translation_matrix) @ element.composed_transform()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
ScaleToSize().run()
|
21
extensions/fablabchemnitz/snap_object_points/meta.json
Normal file
21
extensions/fablabchemnitz/snap_object_points/meta.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "Snap Object Points",
|
||||||
|
"id": "fablabchemnitz.de.snap_object_points",
|
||||||
|
"path": "snap_object_points",
|
||||||
|
"dependent_extensions": null,
|
||||||
|
"original_name": "Snap Object Points",
|
||||||
|
"original_id": "org.pakin.filter.snap_objects",
|
||||||
|
"license": "GNU GPL v3",
|
||||||
|
"license_url": "https://inkscape.org/de/~pakin/%E2%98%85snap-object-points",
|
||||||
|
"comment": "",
|
||||||
|
"source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.X/src/branch/master/extensions/fablabchemnitz/snap_object_points",
|
||||||
|
"fork_url": "https://inkscape.org/de/~pakin/%E2%98%85snap-object-points",
|
||||||
|
"documentation_url": "",
|
||||||
|
"inkscape_gallery_url": null,
|
||||||
|
"main_authors": [
|
||||||
|
"inkscape.org/pakin",
|
||||||
|
"github.com/vmario89"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
@ -0,0 +1,21 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
|
||||||
|
<name>Snap Object Points</name>
|
||||||
|
<id>fablabchemnitz.de.snap_object_points</id>
|
||||||
|
<param name="max_dist" type="float" min="1" max="9999" precision="2" gui-text="Maximum snap distance">25</param>
|
||||||
|
<param name="controls" type="bool" gui-text="Snap control points">true</param>
|
||||||
|
<param name="ends" type="bool" gui-text="Snap endpoints">true</param>
|
||||||
|
<param name="first_only" type="bool" gui-text="Modify only the first selected path">false</param>
|
||||||
|
<label>This effect snaps points in each selected object to nearby points in other selected objects.</label>
|
||||||
|
<effect>
|
||||||
|
<object-type>path</object-type>
|
||||||
|
<effects-menu>
|
||||||
|
<submenu name="FabLab Chemnitz">
|
||||||
|
<submenu name="Modify existing Path(s)"/>
|
||||||
|
</submenu>
|
||||||
|
</effects-menu>
|
||||||
|
</effect>
|
||||||
|
<script>
|
||||||
|
<command location="inx" interpreter="python">snap_object_points.py</command>
|
||||||
|
</script>
|
||||||
|
</inkscape-extension>
|
@ -0,0 +1,136 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
'''
|
||||||
|
Copyright (C) 2020 Scott Pakin, scott-ink@pakin.org
|
||||||
|
|
||||||
|
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 3 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.
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
from collections import defaultdict
|
||||||
|
import inkex
|
||||||
|
from inkex.paths import Arc, Curve, Horz, Line, Move, Quadratic, Smooth, TepidQuadratic, Vert, ZoneClose
|
||||||
|
|
||||||
|
|
||||||
|
class SnapObjectPoints(inkex.EffectExtension):
|
||||||
|
"Snap the points on multiple paths towards each other."
|
||||||
|
|
||||||
|
def add_arguments(self, pars):
|
||||||
|
pars.add_argument('--max_dist', type=float, default=25.0, help='Maximum distance to be considered a "nearby" point')
|
||||||
|
pars.add_argument('--controls', type=inkex.Boolean, default=True, help='Snap control points')
|
||||||
|
pars.add_argument('--ends', type=inkex.Boolean, default=True, help='Snap endpoints')
|
||||||
|
pars.add_argument('--first_only', type=inkex.Boolean, default=True, help='Modify only the first selected path')
|
||||||
|
|
||||||
|
def _bin_points(self):
|
||||||
|
"Associate each path ID with a list of control points and a list of endpoints."
|
||||||
|
cpoints = defaultdict(list)
|
||||||
|
epoints = defaultdict(list)
|
||||||
|
for node in self.svg.selection.filter(inkex.PathElement).values():
|
||||||
|
for cmd in node.path.to_absolute().proxy_iterator():
|
||||||
|
pid = node.get_id()
|
||||||
|
cpoints[pid].extend(cmd.control_points)
|
||||||
|
epoints[pid].append(cmd.end_point)
|
||||||
|
return cpoints, epoints
|
||||||
|
|
||||||
|
def _find_nearest(self, pid, x0, y0, other_points):
|
||||||
|
'''Find the nearest neighbor to a given point, and return the midpoint
|
||||||
|
of the given point and its neighbor.'''
|
||||||
|
max_dist2 = self.options.max_dist**2 # Work with squares instead of wasting time on square roots.
|
||||||
|
bx, by = x0, y0 # Best new point
|
||||||
|
best_dist2 = max_dist2 # Minimal distance observed from (x0, y0)
|
||||||
|
for k, pts in other_points.items():
|
||||||
|
if k == pid:
|
||||||
|
continue # Don't compare to our own points.
|
||||||
|
for vec in pts:
|
||||||
|
x1, y1 = vec.x, vec.y
|
||||||
|
dist2 = (x1 - x0)**2 + (y1 - y0)**2 # Squared distance
|
||||||
|
if dist2 > best_dist2:
|
||||||
|
continue # Not the nearest point
|
||||||
|
best_dist2 = dist2
|
||||||
|
bx, by = x1, y1
|
||||||
|
return (x0 + bx)/2, (y0 + by)/2
|
||||||
|
|
||||||
|
def _simplify_paths(self):
|
||||||
|
'Make all commands absolute, and replace Vert and Horz commands with Line.'
|
||||||
|
for node in self.svg.selection.filter(inkex.PathElement).values():
|
||||||
|
path = node.path.to_absolute()
|
||||||
|
new_path = []
|
||||||
|
prev = inkex.Vector2d()
|
||||||
|
prev_prev = inkex.Vector2d()
|
||||||
|
first = inkex.Vector2d()
|
||||||
|
for i, cmd in enumerate(path):
|
||||||
|
if i == 0:
|
||||||
|
first = cmd.end_point(first, prev)
|
||||||
|
prev, prev_prev = first, first
|
||||||
|
if isinstance(cmd, Vert):
|
||||||
|
cmd = cmd.to_line(prev)
|
||||||
|
elif isinstance(cmd, Horz):
|
||||||
|
cmd = cmd.to_line(prev)
|
||||||
|
new_path.append(cmd)
|
||||||
|
if isinstance(cmd, (Curve, Quadratic, Smooth, TepidQuadratic)):
|
||||||
|
prev_prev = list(cmd.control_points(first, prev, prev_prev))[-2]
|
||||||
|
prev = cmd.end_point(first, prev)
|
||||||
|
node.path = new_path
|
||||||
|
|
||||||
|
def effect(self):
|
||||||
|
"""Snap control points to other objects' control points and endpoints
|
||||||
|
to other objects' endpoints."""
|
||||||
|
# This function uses an O(n^2) algorithm, which shouldn't be too slow
|
||||||
|
# for typical point counts.
|
||||||
|
#
|
||||||
|
# As a preprocessing step, we first simplify the paths to reduce the
|
||||||
|
# number of special cases we'll need to deal with. Then, we associate
|
||||||
|
# each path with all of its control points and endpoints.
|
||||||
|
if len(self.svg.selection.filter(inkex.PathElement)) < 2:
|
||||||
|
raise inkex.utils.AbortExtension(_('Snap Object Points requires that at least two paths be selected.'))
|
||||||
|
self._simplify_paths()
|
||||||
|
cpoints, epoints = self._bin_points()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Process in turn each command on each path.
|
||||||
|
for node in self.svg.selection.filter(inkex.PathElement).values():
|
||||||
|
pid = node.get_id()
|
||||||
|
path = node.path
|
||||||
|
new_path = []
|
||||||
|
for cmd in path:
|
||||||
|
args = cmd.args
|
||||||
|
new_args = list(args)
|
||||||
|
na = len(args)
|
||||||
|
if isinstance(cmd, (Curve, Line, Move, Quadratic, Smooth, TepidQuadratic)):
|
||||||
|
# Zero or more control points followed by an endpoint.
|
||||||
|
if self.options.controls:
|
||||||
|
for i in range(0, na - 2, 2):
|
||||||
|
new_args[i], new_args[i + 1] = self._find_nearest(pid, args[i], args[i + 1], cpoints)
|
||||||
|
if self.options.ends:
|
||||||
|
new_args[na - 2], new_args[na - 1] = self._find_nearest(pid, args[na - 2], args[na - 1], epoints)
|
||||||
|
elif isinstance(cmd, ZoneClose):
|
||||||
|
# No arguments at all.
|
||||||
|
pass
|
||||||
|
elif isinstance(cmd, Arc):
|
||||||
|
# Non-coordinates followed by an endpoint.
|
||||||
|
if self.options.ends:
|
||||||
|
new_args[na - 2], new_args[na - 1] = self._find_nearest(pid, args[na - 2], args[na - 1], epoints)
|
||||||
|
else:
|
||||||
|
# Unexpected command.
|
||||||
|
inkex.errormsg(_('Unexpected path command "%s"' % cmd.name))
|
||||||
|
new_path.append(cmd.__class__(*new_args))
|
||||||
|
node.path = new_path
|
||||||
|
if self.options.first_only:
|
||||||
|
break
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
SnapObjectPoints().run()
|
21
extensions/fablabchemnitz/stroke_color_as_fill/meta.json
Normal file
21
extensions/fablabchemnitz/stroke_color_as_fill/meta.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "Stroke Color As Fill",
|
||||||
|
"id": "fablabchemnitz.de.stroke_color_as_fill",
|
||||||
|
"path": "stroke_color_as_fill",
|
||||||
|
"dependent_extensions": null,
|
||||||
|
"original_name": "Stroke color as fill",
|
||||||
|
"original_id": "com.jabiertxof.strokecolor.as.fill",
|
||||||
|
"license": "GNU GPL v3",
|
||||||
|
"license_url": "https://inkscape.org/de/~jabiertxof/%E2%98%85stroke-color-as-fill",
|
||||||
|
"comment": "ported to Inkscape v1 by Mario Voigt",
|
||||||
|
"source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.X/src/branch/master/extensions/fablabchemnitz/stroke_color_as_fill",
|
||||||
|
"fork_url": "https://inkscape.org/de/~jabiertxof/%E2%98%85stroke-color-as-fill",
|
||||||
|
"documentation_url": "https://stadtfabrikanten.org/display/IFM/Stroke+Color+As+Fill",
|
||||||
|
"inkscape_gallery_url": null,
|
||||||
|
"main_authors": [
|
||||||
|
"inkscape.org/jabiertxof",
|
||||||
|
"github.com/vmario89"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
@ -0,0 +1,34 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
|
||||||
|
<name>Stroke Color As Fill</name>
|
||||||
|
<id>fablabchemnitz.de.stroke_color_as_fill</id>
|
||||||
|
<param name="tab" type="notebook">
|
||||||
|
<page name="swap_tab" gui-text="Fill, Stroke">
|
||||||
|
<param name="fill_stroke_mode" type="optiongroup" appearance="combo" gui-text="Mode:">
|
||||||
|
<option value="fill_stroke">Swap Fill and Stroke</option>
|
||||||
|
<option value="fill">Copy Fill to Stroke</option>
|
||||||
|
<option value="stroke">Copy Stroke to Fill</option>
|
||||||
|
</param>
|
||||||
|
<param name="fill_stroke_copy_alpha" type="bool" gui-text="Copy alpha">true</param>
|
||||||
|
<param name="fill_stroke_copy_none" type="bool" gui-text="Copy 'None' property">true</param>
|
||||||
|
<param name="fill_stroke_copy_unset" type="bool" gui-text="Copy 'Unset' property">true</param>
|
||||||
|
<param name="fill_stroke_convert_unset" type="bool" gui-text="Convert 'Unset' property" gui-description="Convert unset fill to black stroke, unset stroke to 'none' fill.">true</param>
|
||||||
|
<!--<param name="nodash" type="bool" gui-text="Fix dash-stroke to alow no line only markers">false</param>-->
|
||||||
|
</page>
|
||||||
|
<page name="about_tab" gui-text="About">
|
||||||
|
<label appearance="header">Swap</label>
|
||||||
|
<label>Swap or copy fill, stroke paint.</label>
|
||||||
|
</page>
|
||||||
|
</param>
|
||||||
|
<effect needs-document="true">
|
||||||
|
<object-type>all</object-type>
|
||||||
|
<effects-menu>
|
||||||
|
<submenu name="FabLab Chemnitz">
|
||||||
|
<submenu name="Colors/Gradients/Filters"/>
|
||||||
|
</submenu>
|
||||||
|
</effects-menu>
|
||||||
|
</effect>
|
||||||
|
<script>
|
||||||
|
<command location="inx" interpreter="python">stroke_color_as_fill.py</command>
|
||||||
|
</script>
|
||||||
|
</inkscape-extension>
|
@ -0,0 +1,111 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
'''
|
||||||
|
This extension set the stroke of selected elements as fill color and fill opacity (and optional inversed)
|
||||||
|
|
||||||
|
Copyright (C) 2016 Jabiertxo Arraiza, jabier.arraiza@marker.es
|
||||||
|
|
||||||
|
Version 0.2
|
||||||
|
|
||||||
|
|
||||||
|
CHANGE LOG
|
||||||
|
0.2 Added features done by suv in his swamp color extension https://gitlab.com/su-v/inx-modifycolor/
|
||||||
|
0.1 Start 25/11/2016
|
||||||
|
|
||||||
|
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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
'''
|
||||||
|
|
||||||
|
import inkex
|
||||||
|
import sys
|
||||||
|
import re
|
||||||
|
from lxml import etree
|
||||||
|
|
||||||
|
class StrokeColorAsFill(inkex.EffectExtension):
|
||||||
|
|
||||||
|
def add_arguments(self, pars):
|
||||||
|
pars.add_argument("--tab", help="The selected UI-tab")
|
||||||
|
pars.add_argument("--fill_stroke_mode", default="fillstroke", help="Exchange mode fill, stroke")
|
||||||
|
pars.add_argument("--fill_stroke_copy_alpha", type=inkex.Boolean, default=True, help="Copy alpha")
|
||||||
|
pars.add_argument("--fill_stroke_copy_none", type=inkex.Boolean, default=True, help="Copy 'None' property")
|
||||||
|
pars.add_argument("--fill_stroke_copy_unset", type=inkex.Boolean, default=True, help="Copy 'Unset' property")
|
||||||
|
pars.add_argument("--fill_stroke_convert_unset", type=inkex.Boolean, default=True, help="Convert 'Unset' property")
|
||||||
|
pars.add_argument("--nodash", type=inkex.Boolean, default="false", help="Fix dash-stroke to alow no line only markers")
|
||||||
|
|
||||||
|
def color_swapper(self, element):
|
||||||
|
if element.tag == inkex.addNS('g', 'svg'):
|
||||||
|
for e in element:
|
||||||
|
self.color_swaper(e)
|
||||||
|
else:
|
||||||
|
style = element.get('style')
|
||||||
|
if style:
|
||||||
|
fill = re.search('fill:(.*?)(;|$)', style)
|
||||||
|
if fill is not None:
|
||||||
|
style = style.replace(fill.group(),'')
|
||||||
|
fill = fill.group(1)
|
||||||
|
stroke = re.search('stroke:(.*?)(;|$)', style)
|
||||||
|
if stroke is not None:
|
||||||
|
style = style.replace(stroke.group(),'')
|
||||||
|
stroke = stroke.group(1)
|
||||||
|
fill_opacity = re.search('fill-opacity:(.*?)(;|$)', style)
|
||||||
|
if fill_opacity is not None:
|
||||||
|
style = style.replace(fill_opacity.group(),'')
|
||||||
|
fill_opacity = fill_opacity.group()
|
||||||
|
stroke_opacity = re.search('stroke-opacity:(.*?)(;|$)', style)
|
||||||
|
if stroke_opacity is not None:
|
||||||
|
style = style.replace(stroke_opacity.group(),'')
|
||||||
|
stroke_opacity = stroke_opacity.group()
|
||||||
|
if 'fill' in self.options.fill_stroke_mode:
|
||||||
|
if self.options.fill_stroke_copy_none or fill != 'none':
|
||||||
|
if fill is None:
|
||||||
|
if self.options.fill_stroke_convert_unset:
|
||||||
|
style += ';stroke:black'
|
||||||
|
else:
|
||||||
|
style += ';stroke:' + fill
|
||||||
|
if self.options.fill_stroke_copy_alpha and fill_opacity is not None:
|
||||||
|
style += ';stroke-opacity:' + fill_opacity
|
||||||
|
if 'stroke' in self.options.fill_stroke_mode:
|
||||||
|
if self.options.fill_stroke_copy_none or stroke != 'none':
|
||||||
|
if stroke is None:
|
||||||
|
if self.options.fill_stroke_convert_unset:
|
||||||
|
style += ';fill:none'
|
||||||
|
else:
|
||||||
|
style += ';fill:' + stroke
|
||||||
|
if self.options.fill_stroke_copy_alpha and stroke_opacity is not None:
|
||||||
|
style += ';fill-opacity:' + stroke_opacity
|
||||||
|
if self.options.fill_stroke_mode == 'fill':
|
||||||
|
if fill is not None:
|
||||||
|
style += ';fill:' + fill
|
||||||
|
if fill_opacity is not None:
|
||||||
|
style += ';fill-opacity:' + fill_opacity
|
||||||
|
if self.options.fill_stroke_mode == 'stroke':
|
||||||
|
if stroke is not None:
|
||||||
|
style += ';stroke:' + stroke
|
||||||
|
if stroke_opacity is not None:
|
||||||
|
style += ';stroke-opacity:' + stroke_opacity
|
||||||
|
if self.options.nodash:
|
||||||
|
style = re.sub('stroke-dasharray:.*?(;|$)', '', style)
|
||||||
|
style = re.sub('stroke-dashoffset:.*?(;|$)', '', style)
|
||||||
|
style = re.sub('$', ';stroke-dasharray:0, 11;stroke-dashoffset:10$', style)
|
||||||
|
element.set('style',style)
|
||||||
|
|
||||||
|
def effect(self):
|
||||||
|
saveout = sys.stdout
|
||||||
|
sys.stdout = sys.stderr
|
||||||
|
svg = self.document.getroot()
|
||||||
|
for id, element in self.svg.selected.items():
|
||||||
|
self.color_swapper(element)
|
||||||
|
sys.stdout = saveout
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
StrokeColorAsFill().run()
|
Loading…
Reference in New Issue
Block a user