more extensions readded

This commit is contained in:
Mario Voigt 2022-10-03 03:07:44 +02:00
parent a1f6bea8de
commit 58ed243ac0
35 changed files with 2184 additions and 0 deletions

View File

@ -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>

View File

@ -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()

View File

@ -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"
]
}
]

View File

@ -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 &lt;use&gt;-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>

View File

@ -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()

View 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"
]
}
]

View File

@ -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>

View 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()

View 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"
]
}
]

View File

@ -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/&lt;interval&gt; 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/&lt;interval&gt;">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/&lt;interval&gt;">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>

View File

@ -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()

View 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"
]
}
]

View File

@ -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>

View File

@ -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()

View 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"
]
}
]

View File

@ -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>

View 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()

View 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"
]
}
]

View 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"
]
}
]

View 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
(width1, height1). 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>

View 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()

View 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"
]
}
]

Binary file not shown.

Binary file not shown.

View 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>

View 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()

View 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"
]
}
]

View 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>

View 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()

View 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"
]
}
]

View File

@ -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>

View File

@ -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()

View 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"
]
}
]

View File

@ -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>

View File

@ -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()