Skip to content

Modifying existing objects

Scott Pakin edited this page Dec 25, 2023 · 16 revisions

In addition to adding new objects to an image, Simple Inkscape Scripting provides a few mechanisms for modifying existing objects, even those not created using Simple Inkscape Scripting.

Acquiring lists of shapes

The following two functions return lists of shapes appearing in the image, including shapes not created using Simple Inkscape Scripting.

Function: all_shapes()

This function returns a list of all shapes in the image as Simple Inkscape Scripting objects.

Function: selected_shapes()

This function returns a list of all selected shapes in the image as Simple Inkscape Scripting objects.

For both all_shapes and selected_shapes, groups count as shapes, but layers do not.

Example:

for obj in selected_shapes():
    obj.rotate(uniform(-45, 45), 'center', first=True)

The preceding example rotates each selected shape around its center by a random angle from −45° to +45°.

The following property on all Simple Inkscape Scripting shape objects can be used to filter the lists returned by all_shapes and selected_shapes:

Property: tag (type str)

tag returns the underlying SVG element's tag. For example, an ellipse's tag is ellipse; a rectangle's tag is rect; a group's tag is g; and a clone of any element is use. Most Inkscape-specific shapes such as regular polygons, stars, and spirals have a tag of path; 3‑D boxes comprise multiple path elements.

Applying shape transformations

Simple Inkscape Scripting provides a number of mechanisms for modifying the transformation applied to a shape object.

Applying individual transformations

Individual transformations operations can be applied using the following methods:

Method: translate((x, y), first)

Method: rotate(angle, anchor, first)

Method: scale(factor, anchor, first)

Method: skew((sx, sy), anchor, first)

We demonstrate usage of these methods using the following base shapes:

Example:

blue = rect((90, 0), (170, 50), fill='#55ddff')
red = rect((0, 0), (80, 50), fill='#ff5555')

xform-base

Dashed lines were added to the above to serve as a visual reference point for the transformations.

rotate, scale, and skew each take an optional anchor argument that specifies the point around which the object rotates, scales, or skews. anchor can be either an (x, y) coordinate or one of the following strings:

  • c or center. Transform around the shape's center.
  • ul or nw. Transform around the shape's upper-left (northwest) corner.
  • ur or ne. Transform around the shape's upper-right (northeast) corner.
  • ll or sw. Transform around the shape's lower-left (southwest) corner.
  • lr or se. Transform around the shape's lower-right (southeast) corner.
  • n. Transform around the shape's top-center (north) point.
  • e. Transform around the shape's middle-right (east) point.
  • s. Transform around the shape's bottom-center (south) point.
  • w. Transform around the shape's middle-left (west) point.

Note that all of those positions are computed using the shape's bounding box, not necessarily points on the shape itself:

anchors

At the time of this writing, Inkscape does not compute proper bounding boxes for text objects.

anchor defaults to "center".

All four transformation methods take an optional first parameter that, if True, causes the transformation to be applied before all existing transformations. In the default, False, case, the transformation is applied after all existing transformations. See the advice below on when to use first=True and when to use first=False.

The translate method moves the shape x units to the right and y units down.

Example:

blue = rect((90, 0), (170, 50), fill='#55ddff')
red = rect((0, 0), (80, 50), fill='#ff5555')
red.translate((50, 30))

xform-xlate

The rotate method rotates the shape clockwise by a given number of degrees.

Example (rotation around the object's center):

blue = rect((90, 0), (170, 50), fill='#55ddff')
red = rect((0, 0), (80, 50), fill='#ff5555')
red.rotate(30)

xform-rot-c

Example (rotation around an arbitrary point):

blue = rect((90, 0), (170, 50), fill='#55ddff')
red = rect((0, 0), (80, 50), fill='#ff5555')
red.rotate(30, (85, 25))

xform-rot-pt

A dot was added to the above to label the center of rotation.

Example (rotation around the shape's lower-left corner):

blue = rect((90, 0), (170, 50), fill='#55ddff')
red = rect((0, 0), (80, 50), fill='#ff5555')
red.rotate(30, 'll')

xform-rot-ll

The scale method multiplies the shape's size by a given factor, factor. If factor is a 2-tuple, it scales horizontally and vertically by different factors.

Example (uniform scaling):

blue = rect((90, 0), (170, 50), fill='#55ddff')
red = rect((0, 0), (80, 50), fill='#ff5555')
red.scale(1.5)

xform-scale-unif

Example (nonuniform scaling):

blue = rect((90, 0), (170, 50), fill='#55ddff')
red = rect((0, 0), (80, 50), fill='#ff5555')
red.scale((0.75, 1.5))

xform-scale-xy

Example (uniform scaling relative to the shape's upper-right corner):

blue = rect((90, 0), (170, 50), fill='#55ddff')
red = rect((0, 0), (80, 50), fill='#ff5555')
red.scale(1.5, 'ur')

xform-scale-unif-ur

The skew method skews the shape by a given number of degrees along the x axis and a given number of degrees along the y axis.

Example (skew x only):

blue = rect((90, 0), (170, 50), fill='#55ddff')
red = rect((0, 0), (80, 50), fill='#ff5555')
red.skew((10, 0))

xform-skew-x

Example (skew y only):

blue = rect((90, 0), (170, 50), fill='#55ddff')
red = rect((0, 0), (80, 50), fill='#ff5555')
red.skew((0, 10))

xform-skew-y

Example (skew y only, with the skewing anchored at the shape's lower-right corner):

blue = rect((90, 0), (170, 50), fill='#55ddff')
red = rect((0, 0), (80, 50), fill='#ff5555')
red.skew((0, 10), 'lr')

xform-skew-y-lr

The translate, rotate, scale, and skew methods can be chained, as in the following example.

Example (chained scaling and rotation):

blue = rect((90, 0), (170, 50), fill='#55ddff')
red = rect((0, 0), (80, 50), fill='#ff5555')
red.scale(1.5, 'ul').rotate(14, 'ul')

xform-scale-unif-ul-rot-ul

Advice for the use of first

The following are some general guidelines for the use of the optional first parameter to the translate, rotate, scale, and skew methods:

  • When constructing an object from scratch, consider using first=False (the default).

  • When modifying an existing object, consider specifying first=True.

Constructing. As an example of constructing an object from scratch and applying transformations to it, consider the following base shape:

p = polygon([(0, 0), (-100, 0), (-80, -55), (-20, -55)], fill='#37abc8', opacity=0.8)

first-false-base

(A grid was added to the above to clarify the polygon's position.)

For our set of transformations, we first scale the shape by a factor of 0.75:

p = polygon([(0, 0), (-100, 0), (-80, -55), (-20, -55)], fill='#37abc8', opacity=0.8)
p.scale(0.75)

first-false-sc

Then we rotate the scaled shape by 60°:

p = polygon([(0, 0), (-100, 0), (-80, -55), (-20, -55)], fill='#37abc8', opacity=0.8)
p.scale(0.75)
p.rotate(60)

first-false-sc-rot

Finally, we translate the scaled, rotated shape by (150, 130) units:

p = polygon([(0, 0), (-100, 0), (-80, -55), (-20, -55)], fill='#37abc8', opacity=0.8)
p.scale(0.75)
p.rotate(60)
p.translate((150, 130))

first-false-sc-rot-tr

The above are all based on the default of first=False. Had we instead specified first=True for each transformation, the shape would have wound up in a very different location:

p = polygon([(0, 0), (-100, 0), (-80, -55), (-20, -55)], fill='#37abc8', opacity=0.8)
p.scale(0.75, first=True)
p.rotate(60, first=True)
p.translate((150, 130), first=True)

first-true-sc-rot-tr

This is because the first=True flags cause the transformations to be applied in the reverse of program order: translate is applied first, then rotate (around the translated origin), and finally scale.

Modifying. As an example of modifying an existing object, assume we are given the following shape, which has an arbitrary tranformation applied to it. (It happens to be matrix(0.919785 0.978952 -0.865874 0.448312 198.685 220.311), but that's not important to this example.)

first-true-base

Now suppose we want to rotate that shape counterclockwise by 20° around its center. rotate(-20, 'center') (or, equivalently, rotate(-20, 'center', first=False)) does not behave as one might expect:

first-true-false

This is because the rotation is being applied in a transformed coordinate system. Applying the rotation with rotate(-20, 'center', first=True) ensures the rotation is applied in the original coordinate system:

first-true-true

All of the previously applied transformations are reapplied after the rotation.

Replacing the entire transformation

An alternative to the translate, rotate, scale, and skew methods is the following property, which can be both read and assigned:

Property: transform (type inkex.Transform)

For convenience, a string assigned to transform will be converted automatically to an inkex.Transform. The following examples present equivalent means for drawing a rectangle around the origin then rotating it by 30° and translating it to the center of the page:

Example (providing a string for the transform argument):

rect((-200, -200), (200, 200), fill='#d7f4ee',
     transform='translate(%g, %g) rotate(30)' % (canvas.width/2, canvas.height/2))

transform-property

Example (assigning a string to the transform property):

r = rect((-200, -200), (200, 200), fill='#d7f4ee')
r.transform = 'translate(%g, %g) rotate(30)' % (canvas.width/2, canvas.height/2)

Example (invoking methods on the transform property):

r = rect((-200, -200), (200, 200), fill='#d7f4ee')
r.transform = r.transform.add_translate(canvas.width/2, canvas.height/2)
r.transform = r.transform.add_rotate(30)

Note that transformations are applied left-to-right. Therefore, even though the above invokes add_translate before add_rotate, the effect is first to rotate the rectangle then translate it.

Example (applying inkex.Transforms to the existing transform property):

r = rect((-200, -200), (200, 200), fill='#d7f4ee')
r.transform = inkex.Transform('rotate(30)') * r.transform
r.transform = inkex.Transform('translate(%g, %g)' % (canvas.width/2, canvas.height/2)) * r.transform

With explicit use of the * operator, transformations can be specified in the order they will be applied, as in the above.

Transforming a path

The methods described thus far associate transformations with an arbitrary, unmodified shape. In the specific case of path objects (e.g., those created with the path command), Simple Inkscape Scripting additionally provides methods to transform the path's control points themselves:

Method: translate_path((x, y))

Method: rotate_path(angle, anchor)

Method: scale_path(factor, anchor)

Method: skew_path((sx, sy), anchor)

Each of those methods returns the transformed path object itself. This enables chaining of transformations, e.g., with p = path(…).rotate_path(…).scale_path(…). See the descriptions of translate, rotate, scale, and skew in Applying individual transformations for explanations of the various arguments.

We demonstrate usage of the path-transforming methods using the following base path:

Example:

S = path([Move(57, 13),
          Vert(28),
          Quadratic(52, 25, 46, 24),
          Quadratic(41, 23, 36, 23),
          Quadratic(29, 23, 26, 25),
          Quadratic(23, 26, 23, 30),
          Quadratic(23, 33, 25, 35),
          Quadratic(27, 36, 33, 37),
          Line(40, 39),
          Quadratic(52, 41, 57, 46),
          Quadratic(62, 51, 62, 60),
          Quadratic(62, 71, 55, 77),
          Quadratic(48, 82, 34, 82),
          Quadratic(27, 82, 21, 81),
          Quadratic(14, 80, 7, 77),
          Vert(62),
          Quadratic(14, 66, 20, 68),
          Quadratic(26, 69, 32, 69),
          Quadratic(38, 69, 41, 67),
          Quadratic(44, 65, 44, 62),
          Quadratic(44, 58, 42, 57),
          Quadratic(40, 55, 34, 53),
          Line(27, 52),
          Quadratic(16, 50, 11, 45),
          Quadratic(7, 40, 7, 31),
          Quadratic(7, 21, 13, 15),
          Quadratic(20, 10, 33, 10),
          Quadratic(39, 10, 45, 11),
          Quadratic(51, 12, 57, 13),
          ZoneClose()],
         fill='#decd87', stroke_width=5)

path-xform-base

The translate_path method moves the adjusts the path's control points x units to the right and y units down.

The rotate_path method rotates the path's control points clockwise by a given number of degrees.

Example:

S.rotate_path(-22)

path-xform-rotate

The scale_path method multiplies the path's control-point coordinates by a given factor, factor. If factor is a 2-tuple, it scales horizontally and vertically by different factors.

Example:

S.scale_path((3, 1), 'ul')

path-xform-scale

Scaling a path provides a clear contrast with applying a scale transformation:

Example:

S.scale((3, 1), 'ul')

path-xform-scale-alt

With (3, 1) path scaling, the stroke retains its constant thickness throughout the object. Applying a (3, 1) scale transformation, however, makes the stroke three times as wide as it is tall. A distinction can also be observed in the generated SVG code:

Original:

<path
   d="M 57 13 V 28 Q 52 25 46 24 Q 41 23 36 23 Q 29 23 26 25 Q 23 26 23 30 Q 23 33 25 35 Q 27 36 33 37 L 40 39 Q 52 41 57 46 Q 62 51 62 60 Q 62 71 55 77 Q 48 82 34 82 Q 27 82 21 81 Q 14 80 7 77 V 62 Q 14 66 20 68 Q 26 69 32 69 Q 38 69 41 67 Q 44 65 44 62 Q 44 58 42 57 Q 40 55 34 53 L 27 52 Q 16 50 11 45 Q 7 40 7 31 Q 7 21 13 15 Q 20 10 33 10 Q 39 10 45 11 Q 51 12 57 13 Z"
   style="stroke:black;fill:#decd87;stroke-width:5"
   id="path1" />

Path scaling:

<path
   d="M 171 13 L 171 28 Q 156 25 138 24 Q 123 23 108 23 Q 87 23 78 25 Q 69 26 69 30 Q 69 33 75 35 Q 81 36 99 37 L 120 39 Q 156 41 171 46 Q 186 51 186 60 Q 186 71 165 77 Q 144 82 102 82 Q 81 82 63 81 Q 42 80 21 77 L 21 62 Q 42 66 60 68 Q 78 69 96 69 Q 114 69 123 67 Q 132 65 132 62 Q 132 58 126 57 Q 120 55 102 53 L 81 52 Q 48 50 33 45 Q 21 40 21 31 Q 21 21 39 15 Q 60 10 99 10 Q 117 10 135 11 Q 153 12 171 13 Z"
   style="stroke:black;fill:#decd87;stroke-width:5"
   id="path1" />

Scale transformation:

   d="M 57 13 V 28 Q 52 25 46 24 Q 41 23 36 23 Q 29 23 26 25 Q 23 26 23 30 Q 23 33 25 35 Q 27 36 33 37 L 40 39 Q 52 41 57 46 Q 62 51 62 60 Q 62 71 55 77 Q 48 82 34 82 Q 27 82 21 81 Q 14 80 7 77 V 62 Q 14 66 20 68 Q 26 69 32 69 Q 38 69 41 67 Q 44 65 44 62 Q 44 58 42 57 Q 40 55 34 53 L 27 52 Q 16 50 11 45 Q 7 40 7 31 Q 7 21 13 15 Q 20 10 33 10 Q 39 10 45 11 Q 51 12 57 13 Z"
   style="stroke:black;fill:#decd87;stroke-width:5"
   transform="matrix(3 0 0 1 -14 0)"
   id="path1" />

The Original and Scale transformation code use the same path specification (d attribute), but the Scale transformation code includes an additional transform attribute. In contrast, the Path scaling code contains a modified path specification but no transform attribute.

The skew_path method skews the coordinate of the path's control points by a given number of degrees along the x axis and a given number of degrees along the y axis.

Example:

S.skew_path((-30, 0), 'll')

path-xform-skew

Altering a shape's style

A shape object's style can be read and modified via the following method:

Method: style(key=value, …)

The key=value arguments are as one would pass to a shape-creation function. style returns the shape's current style as a Python dict. Passing no arguments to style is also allowed and is useful for reading a shape's style without modifying it.

The following examples present equivalent means of applying a style to a shape object:

Example (specifying the complete style when creating the object):

polygon([(16, 16), (16, 112), (64, 80), (112, 112), (112, 16), (64, 48)],
        fill='#c8377a', stroke_width=3)

style-method

Example (specifying the complete style after creating the object):

p = polygon([(16, 16), (16, 112), (64, 80), (112, 112), (112, 16), (64, 48)])
p.style(fill='#c8377a', stroke_width=3)

Example (specifying the style in multiple steps after creating the object):

p = polygon([(16, 16), (16, 112), (64, 80), (112, 112), (112, 16), (64, 48)])
p.style(fill='#c8377a')
p.style(stroke_width=3)

The style method can be used to copy a style from one object to another:

Example (copying a style):

# First object
down = -3*pi/2
a = arc((75, 75), 50, (down + pi/5, down - pi/5), arc_type='chord',
        stroke='#677821', fill='#abc837', stroke_width=3)

# Second object, which copies the first object's style
path([Move(224, 112),
      Curve(176,64, 192,32, 256,32),
      Curve(320,32, 336,64, 288,112),
      ZoneClose()],
     **a.style())

style-copy

Because the style method returns a dict, it is easy to alter the style before applying it:

Example (copying and modifying a style):

# First object
down = -3*pi/2
a = arc((75, 75), 50, (down + pi/5, down - pi/5), arc_type='chord',
        stroke='#677821', fill='#abc837', stroke_width=3)

# Second object, which copies the first object's style but triples the
# stroke width
sty = a.style()
sty['stroke_width'] *= 3
path([Move(224, 112),
      Curve(176,64, 192,32, 256,32),
      Curve(320,32, 336,64, 288,112),
      ZoneClose()],
     **sty)

style-modify

Removing an existing object

Method: remove()

Invoking remove on any shape object removes the object from the set of objects displayed in the document.

Method: unremove()

Invoking unremove on any object that previously had been removed with remove re-adds it to the top level of the document.

Example:

c = circle((75, 75), 38, fill='darkturquoise', stroke_width=2)
rect((0, 0), (75, 75), fill='aquamarine', stroke_width=2)
c.remove()
c.unremove()

unremove

The preceding example draws a circle with a square on top of it. It then removes the circle from the image, leaving only the square. Finally, it un-removes the circle, placing it back in the image as if it had just been created. The resulting image is therefore the same as if the script had been,

rect((0, 0), (75, 75), fill='aquamarine', stroke_width=2)
c = circle((75, 75), 38, fill='darkturquoise', stroke_width=2)

SVG-level attribute modifications

Attributes of the SVG element underlying a Simple Inkscape Scripting object can be read and written respectively with the following two methods:

Method: svg_get(attr, as_str)

Method: svg_set(attr, val)

svg_get returns the current value of a named attribute, attr, or None if the element does not define a value for that attribute. If the optional as_str argument is False (the default), the value is returned as an appropriate Python data type. If as_str is True, the value is always returned as a string.

svg_set assigns a value to a named attribute. The value is converted implicitly to a string.

There exist a few special cases for svg_get and svg_set:

  • If val is None, svg_set removes the attribute from the object.

  • If attr is 'transform', svg_get returns an inkex.Transform object. An inkex.Transform provides methods for conversion to a string, conversion to a hexad matrix, composition with other inkex.Transforms, interpolation with other inkex.Transforms, application of additional transformations, and more.

  • If attr is 'transform', svg_set accepts either an inkex.Transform or a string for val.

  • If attr is 'style', svg_get returns a Python dictionary. Hyphens are replaced with underscores in all keys. For example, the SVG style fill:#d45500;stroke:#000000;stroke-width:4;stroke-dasharray:4,8;stop-color:#000000 is returned as {'fill': '#d45500', 'stroke': '#000000', 'stroke_width': 4, 'stroke_dasharray': [4, 8], 'stop_color': '#000000'}. (Note also in this example that stroke_dasharray conveniently maps to a Python list.)

  • If attr is 'style', svg_set accepts for val either a string, a Python dictionary, or an inkex.Style. Either underscores or hyphens can be used as keys; underscores will be converted to hyphens when generating SVG. An inkex.Style provides methods for composing, modifying, interpolating, and performing other operations on styles.

In many cases, the methods and properties described above in Applying shape transformations and Altering a shape's style are a more convenient way of manipulating transforms and styles, respectively.

Changing stacking order

Simple Inkscape Scripting stacks shape objects from back to front. That is, more recently created objects within a layer or level of grouping are drawn on top of less recently created objects in the same layer/group. If Simple Inkscape Scripting is used to modify an existing diagram (using, e.g., with svg_root.selection—see Direct SVG access) or in the rare cases in which it is more convenient to draw objects in a different order from their target Z-order, the following shape-object method can be used:

Method: z_order(target, n)

n is an optional integer. target must be one of the following strings:

  • top. Raise the object above all of its siblings. Equivalent to Inkscape's ObjectRaise to Top.

  • bottom. Lower the object below all of its siblings. Equivalent to Inkscape's ObjectLower to Bottom.

  • raise. Raise the object above its next-higher sibling. Equivalent to Inkscape's ObjectRaise. If n is specified, repeat this operation n times.

  • lower. Lower the object below its next-lower sibling. Equivalent to Inkscape's ObjectLower. If n is specified, repeat this operation n times.

  • to. Make the object the nth lowest of its siblings. Specifying n is required in this case. If n is negative, make the object the (−n)th highest of its siblings.

As examples of the above we start with the following base image of five stacked rectangles, with beige on the bottom, maroon, medium slate blue, and medium sea green in the middle, and tan on top:

Example:

boxes = []
ul = inkex.Vector2d()
for c in ['beige', 'maroon', 'mediumslateblue', 'mediumseagreen', 'tan']:
    boxes.append(rect(ul, ul + (100, 60), fill=c, opacity=0.9))
    ul += (10, 10)

z-order-orig

By invoking the z_order method we can hoist the bottom rectangle to the top:

Example:

boxes = []
ul = inkex.Vector2d()
for c in ['beige', 'maroon', 'mediumslateblue', 'mediumseagreen', 'tan']:
    boxes.append(rect(ul, ul + (100, 60), fill=c, opacity=0.9))
    ul += (10, 10)
boxes[0].z_order('top')

z-order-0-top

We can lower the top rectangle to the bottom:

Example:

boxes = []
ul = inkex.Vector2d()
for c in ['beige', 'maroon', 'mediumslateblue', 'mediumseagreen', 'tan']:
    boxes.append(rect(ul, ul + (100, 60), fill=c, opacity=0.9))
    ul += (10, 10)
boxes[-1].z_order('bottom')

z-order-4-bottom

We can raise the medium slate blue rectangle by one position:

Example:

boxes = []
ul = inkex.Vector2d()
for c in ['beige', 'maroon', 'mediumslateblue', 'mediumseagreen', 'tan']:
    boxes.append(rect(ul, ul + (100, 60), fill=c, opacity=0.9))
    ul += (10, 10)
boxes[2].z_order('raise')

z-order-2-raise

We can lower the medium sea green rectangle by two positions:

Example:

boxes = []
ul = inkex.Vector2d()
for c in ['beige', 'maroon', 'mediumslateblue', 'mediumseagreen', 'tan']:
    boxes.append(rect(ul, ul + (100, 60), fill=c, opacity=0.9))
    ul += (10, 10)
boxes[3].z_order('lower', 2)

z-order-3-lower-2

We can put the maroon rectangle in position 3 so that three other rectangles are beneath it:

Example:

boxes = []
ul = inkex.Vector2d()
for c in ['beige', 'maroon', 'mediumslateblue', 'mediumseagreen', 'tan']:
    boxes.append(rect(ul, ul + (100, 60), fill=c, opacity=0.9))
    ul += (10, 10)
boxes[1].z_order('to', 3)

z-order-1-to-3

SVG objects do not maintain an absolute z-index. Rather, the Z-order is defined hierarchically: layers are drawn from bottom to top, with all items in one layer appearing above the items in all preceding layers; within a layer, groups are drawn from bottom to top, with all shapes in one group appearing above the shapes in all preceding groups; within a group, shapes are drawn from bottom to top, with each shape appearing above all preceding shapes. Groups containing other groups or mixtures of shapes and groups follow the same pattern: the last thing drawn at some level appears atop everything preceding it at the same level.

It nevertheless is sometimes useful to be able to query the order in which a set of shapes will be drawn. The following function provides that capability:

Function: z_sort([obj, …])

Given a list of objects, even those appearing in different groups and/or layers, z_sort returns that list sorted in bottom-to-top drawing order. For example, because all of the rectangles in our example lie at the same level we can reverse their stacking order by lowering each rectangle in turn, from the bottommost through the topmost, to the bottom of the stack:

Example:

boxes = []
ul = inkex.Vector2d()
for c in ['beige', 'maroon', 'mediumslateblue', 'mediumseagreen', 'tan']:
    boxes.append(rect(ul, ul + (100, 60), fill=c, opacity=0.9))
    ul += (10, 10)
shuffle(boxes)    # Demonstrate that z_sort really is sorting by Z-order.
for box in z_sort(boxes):
    box.z_order('bottom')

z-order-reverse