Other features - spakin/SimpInkScr GitHub Wiki

This page presents a set of miscellaneous Simple Inkscape Scripting features that don't fit into any of the other sections of the documentation.

Preloaded Python packages

Because they are likely to be used quite frequently for drawing repetitive objects, Simple Inkscape Scripting imports Python's math and random packages and inkex's paths package and transforms.Transform object into the program's namespace with

from math import *
from random import *
from inkex.paths import *
from inkex.transforms import Transform

Hence, programs can invoke functions such as cos(rad) and uniform(a, b), constants such as pi, and path-element constructors such as Move and Line without having to import any packages or prefix those names with their package name.

Debug output

print is redefined to invoke inkex.utils.debug, which presents its output within a dialog box after the script completes. It does not accept any optional arguments (e.g., sep and end) but otherwise mimics Python's print function.

Function: print(s, …)

The following method is defined on all Simple Inkscape Scripting objects and can be useful in conjunction with print:

Method: svg(xmlns, pretty_print)

svg returns a string version of the SVG code underlying a Simple Inkscape Scripting object. Both of svg's arguments are optional Booleans that default to False. If xmlns is True the string will include XML namespace attributes. If pretty_print is True, the string will include line breaks and indentation for child elements. In the current implementation, activating pretty_print implicitly activates xmlns.

As an example, the following code defines a filter based on Inkscape's FiltersDistortRough and Dilate:

Example:

rough_dilate = filter_effect(name='Rough and Dilate',
                             pt1=(-0.0029433708, -0.0049473915),
                             pt2=(1.0028276292, 1.0048986085),
                             color_interpolation_filters='sRGB')
morph = rough_dilate.add('Morphology',
                         src1='SourceGraphic',
                         radius=7,
                         operator='dilate')
comp1 = rough_dilate.add('Composite',
                         src1='SourceGraphic',
                         src2=morph,
                         operator='arithmetic',
                         k1=0.5,
                         k3=0.5)
turb = rough_dilate.add('Turbulence',
                        type='fractalNoise',
                        numOctaves=3,
                        baseFrequency=0.07)
disp_map = rough_dilate.add('DisplacementMap',
                            src1=comp1,
                            src2=turb,
                            xChannelSelector='R',
                            scale=10)
rough_dilate.add('Composite',
                 src1='SourceGraphic',
                 src2=disp_map,
                 operator='arithmetic',
                 k1=0.25,
                 k2=0.25,
                 k3=0.75)

Given that definition, print(rough_dilate.svg()) outputs the following string:

<filter inkscape:label="Rough and Dilate" x="-0.0029433708" y="-0.0049473915" width="1.005771" height="1.009846" style="color-interpolation-filters:sRGB"><feMorphology result="morphology1" in="SourceGraphic" radius="7" operator="dilate"/><feComposite result="composite1" in="SourceGraphic" in2="morphology1" operator="arithmetic" k1="0.5" k3="0.5"/><feTurbulence result="turbulence1" type="fractalNoise" numOctaves="3" baseFrequency="0.07"/><feDisplacementMap result="displacementmap1" in="composite1" in2="turbulence1" xChannelSelector="R" scale="10"/><feComposite result="composite2" in="SourceGraphic" in2="displacementmap1" operator="arithmetic" k1="0.25" k2="0.25" k3="0.75"/></filter>

print(rough_dilate.svg(xmlns=True)) includes the appropriate XML namespaces on the top-level <filter> element:

<filter xmlns="http://www.w3.org/2000/svg" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" inkscape:label="Rough and Dilate" x="-0.0029433708" y="-0.0049473915" width="1.005771" height="1.009846" style="color-interpolation-filters:sRGB"><feMorphology result="morphology1" in="SourceGraphic" radius="7" operator="dilate"/><feComposite result="composite1" in="SourceGraphic" in2="morphology1" operator="arithmetic" k1="0.5" k3="0.5"/><feTurbulence result="turbulence1" type="fractalNoise" numOctaves="3" baseFrequency="0.07"/><feDisplacementMap result="displacementmap1" in="composite1" in2="turbulence1" xChannelSelector="R" scale="10"/><feComposite result="composite2" in="SourceGraphic" in2="displacementmap1" operator="arithmetic" k1="0.25" k2="0.25" k3="0.75"/></filter>

print(rough_dilate.svg(pretty_print=True)) breaks lines before child elements and indents them:

<filter xmlns="http://www.w3.org/2000/svg" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" inkscape:label="Rough and Dilate" x="-0.0029433708" y="-0.0049473915" width="1.005771" height="1.009846" style="color-interpolation-filters:sRGB">
  <feMorphology result="morphology1" in="SourceGraphic" radius="7" operator="dilate"/>
  <feComposite result="composite1" in="SourceGraphic" in2="morphology1" operator="arithmetic" k1="0.5" k3="0.5"/>
  <feTurbulence result="turbulence1" type="fractalNoise" numOctaves="3" baseFrequency="0.07"/>
  <feDisplacementMap result="displacementmap1" in="composite1" in2="turbulence1" xChannelSelector="R" scale="10"/>
  <feComposite result="composite2" in="SourceGraphic" in2="displacementmap1" operator="arithmetic" k1="0.25" k2="0.25" k3="0.75"/>
</filter>

Unit conversions

SVG works in nominal "pixel" units. Because it is sometimes more convenient to work in absolute units, Simple Inkscape Scripting predefines the following conversion constants:

Unit Value
px 1.00000
mm 3.77953
cm 37.79528
pt 1.33333
inch 96.00000

(inch is used instead of in because in is a Python keyword.)

Using those constants, one can write 17*mm to represent 17 millimeters or 3.5*inch to represent 3½ inches. The following example extensively uses constants to draw a metric ruler that spans the width of the page:

Example:

# Define various parameters for rendering a ruler.
ruler_ht = 2.5*cm
short_ht = 4*mm
tall_ht = 7*mm
margin = 5*mm
font_size = 10*pt

# Define the canvas's viewport and viewbox to a 1:1 ratio.
canvas.true_width = 15*cm + 2*margin + 1*mm
canvas.true_height = 2.5*cm
canvas.width = canvas.true_width
canvas.height = canvas.true_height

# Draw a ruler with labeled tick marks.
n_ticks = (canvas.width - 2*margin)/mm
n_ticks = int(n_ticks/10)*10 + 1
rect((0, 0), (canvas.width, ruler_ht), stroke_width=2, fill='#fff6d5')
for i in range(0, n_ticks):
    pos = i*mm + margin
    if i%5 == 0:
        line((pos, 0), (pos, tall_ht))
    else:
        line((pos, 0), (pos, short_ht))
    if i%10 == 0:
        text(str(i//10), (pos, tall_ht + font_size),
             font_family=['DejaVu Sans', 'Calibri', 'sans-serif'],
             font_height=font_size, text_anchor='middle')

A ruler with labeled tick marks

Although the ruler is specified in terms of absolute units, there unfortunately is no guarantee that Inkscape, your web browser, or any other viewing application will honor those when displaying the image.

Object bounding boxes

All of the shapes created by functions described in the Shape construction section support the following method:

Method: bounding_box()

This returns an inkex.BoundingBox object, which provides a number of useful properties, including width, height, top, left, bottom, right, center_x, and center_y, all with their expected interpretation.

Computing the bounding box of a piece of text is challenging because the bounding box cannot be inferred solely from the contents of the SVG file. Rather, a particular font needs to be loaded, and the text needs to be rendered using that font. When asked to take the bounding box of a piece of text—or a group containing text—Simple Inkscape Scripting writes the object to a separate, temporary SVG file, runs inkscape -X -Y -W -H on that file, parses the output into a bounding box, and removes the temporary file. This approach is not only slow but also fragile. It requires Inkscape 1.2+ and does not appear to work from AppImage builds of Inkscape. If anything goes wrong, a best-effort bounding box is returned, but this may indicate zero width and/or height.

Example:

pts = []
for i in range(100):
    pts.append((uniform(canvas.width/4, canvas.width*3/4), uniform(canvas.height/4, canvas.height*3/4)))
scribble = polyline(pts, stroke='#008000', stroke_width=2)
bb = scribble.bounding_box()
rect(bb.minimum, bb.maximum, stroke='#ff4040', stroke_width=4)

Drawing a tight bounding_box around an object

The preceding code draws a random polyline with 100 segments in green. It then computes the polyline's bounding box and renders it as a red rectangle.

Random color generation

Simple Inkscape Scripting provides a convenience function for selecting a random color and returning it as an inkex.colors.Color.

Function: randcolor([values, …], [space])

space specifies the color space. It must be one of rgb (the default) or named. rgb represents colors expressed in terms of red, green, and blue color components; named represents colors by name. The function accepts zero or more values, which are lists of values that can be assigned to each color channel. The following table provides information about each color space:

Space #Channels Channel values Default channel values
rgb 3 [0, 255] range(256), range(256), range(256)
named 1 SVG named colors ['aliceblue', 'antiquewhite', …, 'yellowgreen']

In the case of rgb, each channel defaults to the previous channel's range. Hence, randcolor(range(0, 256, 32)) is shorthand for randcolor(range(0, 256, 32), range(0, 256, 32), range(0, 256, 32)).

The following example draws a 10×10 grid of completely random RGB colors:

Example:

edge = 3*mm
for y in range(10):
    for x in range(10):
        rect((x*edge, y*edge), ((x + 1)*edge, (y + 1)*edge), stroke='none',
             fill=randcolor())

A 10×10 grid of random colors

randcolor can be instructed as follows to select from a list of named colors:

Example:

edge = 3*mm
for y in range(10):
    for x in range(10):
        rect((x*edge, y*edge), ((x + 1)*edge, (y + 1)*edge), stroke='none',
             fill=randcolor(['blueviolet', 'moccasin', 'lightseagreen', 'skyblue', 'crimson'], space='named'))

A 10×10 grid of colors taken from a palette of named colors

Colors can be limited to "web-safe" colors by restricting the three RGB channels to certain values:

Example:

edge = 3*mm
for y in range(10):
    for x in range(10):
        rect((x*edge, y*edge), ((x + 1)*edge, (y + 1)*edge), stroke='none',
             fill=randcolor(range(0, 256, 51)))

A 10×10 grid of "web-safe" colors

Colors can be limited to pastels by requiring each color channel to have a value of at least 128:

Example:

edge = 3*mm
for y in range(10):
    for x in range(10):
        rect((x*edge, y*edge), ((x + 1)*edge, (y + 1)*edge), stroke='none',
             fill=randcolor(range(128, 256)))

A 10×10 grid of pastel colors

Separately specifying per-channel RGB values enables colors to be drawn from different palettes:

Example:

edge = 3*mm
for y in range(10):
    for x in range(10):
        rect((x*edge, y*edge), ((x + 1)*edge, (y + 1)*edge), stroke='none',
             fill=randcolor(range(256), range(128), [0]))

A 10×10 grid of autumnal colors

Hyperlinks

Shape objects created by the functions in Shape construction can be converted to hyperlinks, which will be active when the SVG file is viewed in a Web browser. (They can also be viewed in Inkscape by right-clicking on an object and selecting Link Properties….) The following function wraps a hyperlink around one or more existing objects:

Function: hyperlink(objs, href, title, target, mime_type)

The first two arguments are required. objs is either a single shape object or a list of shape objects. href is the URL to which the hyperlink points. The remaining arguments are optional. title is a title for the hyperlink and is typically shown when the user mouses over the object. Its use is highly recommended. target indicates where the link target should open. If it is _self (the default), the link will open in the current browsing context; if it is _parent, the target replaces the SVG file's parent context; if it is _top, the target replaces the entire contents of the current browser tab; if it is _blank, a new browser tab or window will be opened to show the link target; if it is any other string, it names an existing browsing context (tab, inline frame, object, etc.) to replace with the link target. mime_type informs the Web browser of the MIME media type that the link target is expected to return.

Example:

house = rect((32, 64), (96, 112), fill='#ff0000', stroke_width=2)
roof = polygon([(16, 64), (64, 16), (112, 64)], fill='#008000', stroke_width=2)
hyperlink([house, roof], 'https://www.pakin.org/', title='My home page')

An image that hyperlinks to a web page

The preceding example creates a hyperlink that wraps a picture of a house.

Importing external SVG objects

Function: objects_from_svg_file(file, keep_layers)

Add all SVG objects read from a file to the current image and return a list of the top-level objects that were added as Simple Inkscape Scripting objects. file specifies either the name of an SVG file name or an open file object. At the time of this writing, only shapes, groups, and, if keep_layers is True (it defaults to False), layers are processed. Like any other Simple Inkscape Scripting objects, the objects named in the list can be modified, removed, ungrouped, etc.

Saving the current document to a file

Function: save_file(file)

Save the current document to an SVG file. If file is None (the default), use the document's current filename. (If the document has not yet been given a filename, throw a RuntimeError.) If file is a string, use that as the filename and write (or overwrite) that file. Finally, if file is an open file object, write the current document to that file object.

Source filename

Simple Inkscape Scripting makes available via the following variable the absolute filename of the script itself:

Variable: script_filename (type str)

If the script is entered into the GUI's Python code box rather than read from a file, script_filename will be None.

Inkscape actions

The Inkscape command line can apply various "actions" to a document. Run

inkscape --action-list

for a complete list. Most of these correspond to operations in the Inkscape GUI. Simple Inkscape Scripting provides the following function for invoking actions from a script:

Function: apply_action(action, [obj])

action is a string or list of strings represent the action(s) to apply, e.g., page-fit-to-selection to crop the current page to fit a set of objects. The optional [obj] argument represents a Simple Inkscape Scripting object or list of objects to use as the current selection. apply_action returns a list of Simple Inkscape Scripting objects that were created as a result of the actions.

There are a number of caveats regarding the use of apply_action:

  1. apply_action is implemented by spawning a child copy of Inkscape that performs the requested action. The function therefore can be rather slow to execute.

  2. Objects that are deleted by an action can no longer be used. To aid debugging, Simple Inkscape Scripting sets such objects' underlying inkex object to None. Consequently, a Python error involving NoneType may indicate an attempted access of a deleted object.

  3. The set of available actions varies across versions of Inkscape. As some actions correspond to particular extensions, the set of available actions also depends on which extensions are installed. Hence, scripts that use apply_action may be less portable than scripts that do not.

The following example demonstrates the use of apply_action:

Example:

from pathlib import Path

# Draw a filled and a hollow rectangle.
r = rect((100, 100), (200, 200), stroke_width=16, stroke='#000080', fill='#add8e6')
new_objs = apply_action('object-stroke-to-path', r)
for obj in new_objs:
    try:
        if obj.style()['fill'] == '#000080':
            obj.translate_path((50, 50))
    except KeyError:
        pass

# Export the two rectangles to a PNG file.
png_name = Path.home().joinpath("rectangles.png")
apply_action([
    'export-area-drawing',
    'export-type:png',
    f'export-filename:{png_name}',
    'export-overwrite',
    'export-do'
])

Applying an Inkscape "Stroke to Path" action

The r = rect(…) line creates a rectangle r containing a thick, dark-blue stroke and filled with light blue. The following line spawns another instance of Inkscape, instructing it to apply to r the object-stroke-to-path action (which corresponds to the PathStroke to Path function in the GUI). apply_action returns three objects from this call: the filled rectangle (as a path) with no stroke, a path representing the stroke, and a group containing those two paths. r itself is invalidated by the call.

The for loop finds the path representing the dark-blue frame and shifts it 50 units to the right and 50 units down to demonstrate that the fill and the stroke indeed have been separated by object-stroke-to-path.

The final stanza invokes apply_action a second time. This call is passed a list of actions and no object arguments. The actions instruct Inkscape to export the drawing to a file called rectangles.png in the user's home directory.

⚠️ **GitHub.com Fallback** ⚠️