We will discuss those two lines of code in run method of InksacpeExtension
class in this chapter.
self.load_raw()
self.save_raw(self.effect())
The code of load_raw method of InkscapeExtension class is shown below.
def load_raw(self):
# type: () -> None
"""Load the input stream or filename, save everything to self"""
if isinstance(self.options.input_file, str):
self.file_io = open(self.options.input_file, 'rb')
document = self.load(self.file_io)
else:
document = self.load(self.options.input_file)
self.document = document
We know the value of self.options.input_file is a string from last chapter,
so the if part of the if...else... statement will execute. It calls the
open method to create a file object, and pass it to the self.load method.
The self.load method defined in the SvgInputMixin class is invoked here.
Below is the code of the load method of SvgInputMixin class.
def load(self, stream):
# type: (IO) -> etree
"""Load the stream as an svg xml etree and make a backup"""
document = load_svg(stream)
self.original_document = copy.deepcopy(document)
self.svg = document.getroot()
self.svg.selection.set(*self.options.ids)
if not self.svg.selection and self.select_all:
self.svg.selection =
self.svg.descendants().filter(*self.select_all)
return document
The load method in turn calls a function load_svg imported from another module.
It also adds two new instance variable original_document and svg.
from .elements._base import load_svg, BaseElement
Here is the function load_svg definition in the inkex/elements/_base.py module.
from lxml import etree
......
SVG_PARSER = etree.XMLParser(huge_tree=True, strip_cdata=False)
SVG_PARSER.set_element_class_lookup(NodeBasedLookup())
def load_svg(stream):
"""Load SVG file using the SVG_PARSER"""
if (isinstance(stream, str) and
stream.lstrip().startswith('<'))\
or (isinstance(stream, bytes) and
stream.lstrip().startswith(b'<')):
return etree.ElementTree(etree.fromstring(stream,
parser=SVG_PARSER))
return etree.parse(stream, parser=SVG_PARSER)
Here the last statement return etree.parse(...) is executed because steam
is a file object, not a string or bytes type. The etree.parse method in lxml
module does the actual loading work. The lxml module is not in the standard
library, and it is a third part library. It will be automatically installed
when we install Inkscape. We will discuss lxml module later.
The Triangle object has two instance variables document and svg, and both
reference the same XML tree in memory. We can modify the XML tree via either of
those two variables. It also saves a copy
of document in original_document instance variable.
The actual modification happens in the effect method of Triangle class thru the svg instance variable.
def effect(self):
tri = self.svg.get_current_layer()
offset = self.svg.namedview.center
self.options.s_a = self.svg.unittouu(
str(self.options.s_a) + 'px')
self.options.s_b = self.svg.unittouu(
str(self.options.s_b) + 'px')
self.options.s_c = self.svg.unittouu(
str(self.options.s_c) + 'px')
stroke_width = self.svg.unittouu('2px')
if self.options.mode == '3_sides':
s_a = self.options.s_a
s_b = self.options.s_b
s_c = self.options.s_c
draw_tri_from_3_sides(s_a, s_b, s_c, offset,
stroke_width, tri)
......
The first line of the method calls get_current_layer method of svg object
to get an object representing current layer. The current layer information is saved in
the SVG file itself. The second line get the Inkscape view center coordinates.
The next four lines of code convert number unit from pixel to SVG internal default unit
millimeter. For example, the s_a value is 100 px before the conversion, and
the value is 26.45 mm after. In Inkscape 1 inch is 96 pixels, and 1 inch is also 25.4 mm.
So the conversion is 100/96 in * 25.4 = 26.45 mm.
The effect method calls draw_tri_from_3_sides function defined earlier in the
module. The function in turn calls the draw_SVG_tri function to create an
inkex.PathElement element and add it to the layer. We will discuss the
PathElement and other SVG elements later.
def draw_SVG_tri(point1, point2, point3, offset, width, name, parent):
style = {'stroke': '#000000', 'stroke-width': str(width),
'fill': 'none'}
elem = parent.add(inkex.PathElement())
elem.update(**{
'style': style,
'inkscape:label': name,
'd': 'M ' + str(point1[X] + offset[X]) + ',' +
str(point1[Y] + offset[Y]) +
' L ' + str(point2[X] + offset[X]) + ',' +
str(point2[Y] + offset[Y]) +
' L ' + str(point3[X] + offset[X]) + ',' +
str(point3[Y] + offset[Y]) +
' L ' + str(point1[X] + offset[X]) + ',' +
str(point1[Y] + offset[Y]) + ' z'})
return elem
.....
def draw_tri_from_3_sides(s_a, s_b, s_c, offset, width, parent):
# draw a triangle from three sides (with a given offset
if is_valid_tri_from_sides(s_a, s_b, s_c):
a_b = angle_from_3_sides(s_a, s_c, s_b)
a = (0, 0) # a is the origin
b = v_add(a, (s_c, 0)) #point B is horizontal from origin
c = v_add(b, pt_on_circ(s_a, pi - a_b)) # get point c
c[1] = -c[1]
offx = max(b[0], c[0]) / 2
# b or c could be the furthest right
offy = c[1] / 2 # c is the highest point
offset = (offset[0] - offx, offset[1] - offy)
# add the centre of the triangle to the offset
draw_SVG_tri(a, b, c, offset, width, 'Triangle', parent)
else:
inkex.errormsg('Invalid Triangle Specifications.')
This section discusses the third method call self.save_raw shown at the beginning
of this chapter. The save_raw method is defined in InkscapeExtension class like
this.
def save_raw(self, ret):
# type: (Any) -> None
"""Save to the output stream, use everything from self"""
if self.has_changed(ret):
if isinstance(self.options.output, str):
with open(self.options.output, 'wb') as stream:
self.save(stream)
else:
self.save(self.options.output)
The method tests if the SVG file has changed via the has_changed method in
SvgThroughMixin class. It converts the original_document and document
objects to string and compares if they are the same.
The save_raw method then calls the save method defined in SvgOutputMixin
class. The code of save method is shown below. It calls the write method
of output stream to transmit the modified SVG back to Inkscape.
def save(self, stream):
# type: (IO) -> None
"""Save the svg document to the given stream"""
if isinstance(self.document, (bytes, str)):
document = self.document
elif 'Element' in type(self.document).__name__:
# isinstance can't be used here because etree is broken
doc = cast(etree, self.document)
document = doc.getroot().tostring()
# actually execute this part
else:
raise ValueError(f"Unknown type of document:
{type(self.document).__name__} can not save.")
try:
stream.write(document)
except TypeError:
# we hope that this happens only when
# document needs to be encoded
stream.write(document.encode('utf-8')) # type: ignore
Let’s go back to the two lines of code at the beginning of this chapter.
self.load_raw()
self.save_raw(self.effect())
The load_raw and save_raw methods are already defined in the inkex module, so
we do not need to worry about them when we inherit from inkex.EffectExtension class.
We only need to override the effect method. The second line of code
above implies that the effect method has a return value which is then passed to
the save_raw method.
The effect method of Triangle class does not have a return statement, so
a None value is passed to the save_raw method. The argument ret of
save_raw method is passed as an argument to has_changed method.
Here the has_changed method in SvgThroughMixin class is called. The
SvgThroughMixin class code is listed below. The value ret is not
used here, so we don’t have to return a value in the effect method.
class SvgThroughMixin(SvgInputMixin, SvgOutputMixin):
"""
Combine the input and output svg document handling (usually for effects).
"""
def has_changed(self, ret): # pylint: disable=unused-argument
# type: (Any) -> bool
"""Return true if the svg document has changed"""
original = etree.tostring(self.original_document)
result = etree.tostring(self.document)
return original != result
When we develop an Inkscape extension, we don’t need to care too much about
load and save processes. The inkex module already has code to handle
them. It actually warps around a third party python module lxml, which
does the XML loading, parsing, and saving. The
lxml official website
has lots of useful information. We will also discuss this module in a later chapter.
4. Extension Workflow