The inkex module defines many custom element classes. The Python files are under the
inkex/elements directory and module names all start with an underscore _. It indicates
that those are internal modules and we should not directly import those modules.
When inkex module loads an SVG file, it uses those custom
element classes and returns objects of custom element class instead of generic
Element class of lxml.
The inkex module creators did the hard work of writing custom element classes. It currently
has 64 classes included in Inkscape 1.1 release. Here are a few examples.
inkex.elements._meta.Defs
inkex.elements._meta.StyleElement
inkex.elements._svg.SvgDocumentElement
inkex.elements._groups.Layer
inkex.elements._polygons.PathElement
inkex.elements._polygons.Polyline
inkex.elements._polygons.Polygon
inkex.elements._polygons.Line
inkex.elements._polygons.Rectangle
inkex.elements._polygons.Circle
......
If an SVG file contains an rect shape element, it will become an Rectangle object in
memory when inkex loads and parses the file. When we write user extensions, we can
create objects of those custom element classes, and add them to a containing
element such as layer or group.
Let’s take a look at the PathElement inheritance tree. The class is derived from ShapeElement
which in turn is derived from BaseElement class.
etree.ElementBase
BaseElement
ShapeElement
PathElement
The code in the Hello extension shows the typical way to add new elements to a drawing. We
create an element object first, set some attributes, and add the element to a
containing group or layer. The new element will become part of the drawing.
The example below shows how to add shape and text elements to a drawing. Here are the
contents in the newelement.inx file.
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension
xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>New Element</name>
<id>user.newelement</id>
<effect>
<object-type>all</object-type>
<effects-menu>
<submenu name="Custom"/>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">
newelements.py</command>
</script>
</inkscape-extension>
Save the following code in a newelement.py file under user extension directory.
import inkex
from inkex import Line, Polyline, Polygon, Rectangle, Circle,\
Ellipse, PathElement
from inkex import TextElement
class NewElement(inkex.EffectExtension):
def effect(self):
self.style = {'fill' : 'none', 'stroke' : '#000000',
'stroke-width' : '0.264583'}
self.text_template = \
'font-size:%dpx;text-align:center;text-anchor:middle;'
layer = self.svg.get_current_layer()
layer.add(*self.add_line(), *self.add_rect())
layer.add(self.add_circle(), self.add_ellipse(),
self.add_polygon(), self.add_path())
layer.add(*self.add_coordinates())
def add_line(self):
el1 = Line()
el1.set('x1', '10')
el1.set('y1', '10')
el1.set('x2', '40')
el1.set('y2', '40')
el1.set('style', self.style)
el2 = Line.new(start=(40, 10), end=(10, 40))
el2.style = self.style
el3 = Line()
el3.update(**{
'x1': '50',
'y1': '10',
'x2': '80',
'y2': '40',
'style': self.style
})
el4 = Line(x1='50', y1='40', x2='80', y2='10')
el4.style = self.style
return el1, el2, el3, el4
def add_rect(self):
el1 = Rectangle(x='10', y='60', width='30', height='20')
el1.style = self.style
el2 = Rectangle.new(50, 60, 30, 20)
el2.style = self.style
return el1, el2
def add_circle(self):
el = Circle.new(center=(105, 25), radius=15)
el.style = self.style
return el
def add_ellipse(self):
el = Ellipse.new(center=(105, 70), radius=(15,10))
el.style = self.style
return el
def add_polygon(self):
el = Polygon()
el.set('points',
'130,10 160,10 160,25 145,25 145,40 130,40')
el.style = self.style
return el
def add_path(self):
el = PathElement()
el.set('d', 'M 130,60 h30 v10 h-15 v10 h-15 z')
el.style = self.style
return el
def add_text(self, x, y, position='top', font_size=3.88):
text = TextElement()
x0, y0 = x, y
# adjust y position
if position == 'top':
y = y - 2
elif position == 'bottom':
y = y + 4
else:
y = y
text.set('x', x)
text.set('y', y)
text.set('style', self.text_template % font_size)
text.set('xml:space', 'preserve')
text.text = f'({x0},{y0})'
return text
def add_coordinates(self):
coordinates = [ (10, 10), (40, 40, 'bottom'),
(40, 10), (10, 40, 'bottom'),
(50, 10), (80, 40, 'bottom'),
(50, 40, 'bottom'), (80, 10, 'top'),
(105, 25, 'top'), (105, 70, 'top'),
(10, 60, 'top'), (50, 60, 'top'),
(130, 10, 'top'), (130, 60, 'top'),
(160, 10, 'top'), (160, 60, 'top'),
]
text_elements = [self.add_text(*c) for c in coordinates]
circle_elements = [self.generate_circle(c[0], c[1]) \
for c in coordinates]
return text_elements + circle_elements
def generate_circle(self, x, y, r=0.66145):
circle_style = 'fill:#000000;stroke:none;stroke-width:0.264583'
el = Circle.new(center=(x, y), radius=r)
el.style = circle_style
return el
if __name__ == '__main__':
NewElement().run()
Click the menu Extensions -> Custom -> NewElement to create elements on the
current layer of a drawing. The drawing below shows the results.
The code logic is simple. The add_line method of the NewElement class shows four
way to create a new Line element and set its attributes. The set method of an
element such as el1 seems to be the most straightforward way to set attributes.
The custom element classes do not have a custom __init__ method. This is due to
a requirement from lxml because they are inherited from ElementBase.
Many system extensions like render_gears and render_barcode inherit from GenerateExtension
class. The class itself is a subclass of EffectExtension, and it already has code to add
elements to the drawing. The source code is in the inkex/extensions.py module. When we
inherit from this class, we only need to override the generate method.
Here is an example of using GenerateExtension to create four lines. We do not need to
write any code to deal with layers. The example below creates a new lines layer
and adds new elements to the layer. This is determined by the two class
variables container_label and container_layer.
import inkex
from inkex import Line
class NewElement(inkex.GenerateExtension):
container_label = 'lines'
container_layer = True
def generate(self):
self.style = {'fill' : 'none', 'stroke' : '#000000',
'stroke-width' : '0.264583'}
lines = self.add_lines()
for l in lines:
yield l
def add_lines(self):
el1 = Line()
el1.set('x1', '10')
el1.set('y1', '10')
el1.set('x2', '40')
el1.set('y2', '40')
el1.set('style', self.style)
el2 = Line.new(start=(40, 10), end=(10, 40))
el2.style = self.style
el3 = Line()
el3.update(**{
'x1': '50',
'y1': '10',
'x2': '80',
'y2': '40',
'style': self.style
})
el4 = Line(x1='50', y1='40', x2='80', y2='10')
el4.style = self.style
return el1, el2, el3, el4
if __name__ == '__main__':
NewElement().run()
When we load an XML file into memory, we usually use the default XML parser that comes with
lxml module.
doc = etree.parse('test.xml')
Or you can invoke the etree.XMLParser method to create a parser and pass the parser object to
etree.parse method. The huge_tree option shown below “disables security restrictions
and supports deep trees and long text content”.
p = etree.XMLParser(huge_tree=True)
doc = etree.parse('test.xml', parser=p)
The lxml documentation has a page regarding
using custom Element classes in lxml. The
“Tree based element class lookup in Python” section has an example like this.
class MyLookup(etree.PythonElementClassLookup):
def lookup(self, document, element):
return MyElementClass
parser = etree.XMLParser()
parser.set_element_class_lookup(MyLookup())
The MyLookup class must have a method lookup as shown above. The document argument
of the lookup method acts like self.document object and the element argument
acts like an Element object. The return value MyElementClass is a custom class
defined elsewhere which must inherit from etree.ElementBase class.
The code in the elements/_base.py module follows this pattern to define the lookup
class NodeBasedLookup. It creates a custom parser SVG_PARSER and defines the load_svg
method which uses the parser.
With the parser in place, the return value from etree.parse method will contain Rectangle
class object instead of general Element object if it is an rect shape element.
This is a simplification. It actually is a Python proxy object because lxml.etree is
based on libxml2, which loads the XML tree into memory in a C structure.
7. Shape Elements