Source code for inkscape.inksvg

#-----------------------------------------------------------------------------
# Copyright 2012-2016 Claude Zervas
# email: claude@utlco.com
#-----------------------------------------------------------------------------
"""
A simple library for SVG output - but more Inkscape-centric.
"""
# Python 3 compatibility boilerplate
from __future__ import (absolute_import, division,
                        print_function, unicode_literals)
from future_builtins import *

import re
import logging
logger = logging.getLogger(__name__)

from lxml import etree

import geom
from geom import transform2d

from svg import svg
from svg.svg import svg_ns, _add_ns

# Dictionary of XML namespaces used in Inkscape documents
INKSCAPE_NS = {
#     None: u'http://www.w3.org/2000/svg',
#     u'svg': u'http://www.w3.org/2000/svg',
#     u'xlink': u'http://www.w3.org/1999/xlink',
#     u'xml': u'http://www.w3.org/XML/1998/namespace',
#     u'cc': u'http://creativecommons.org/ns#',
#     u'ccOLD': u'http://web.resource.org/cc/',
#     u'dc': u'http://purl.org/dc/elements/1.1/',
#     u'rdf': u'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
    u'inkscape': u'http://www.inkscape.org/namespaces/inkscape',
    u'sodipodi': u'http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd',
}
# Add the standard SVG namespaces
INKSCAPE_NS.update(svg.SVG_NS)

# Vendor specific namespace (in this case us)
UTLCO_NS = {
    u'utlco': u'http://www.utlco.com/namespaces/utlco',
}

[docs]def inkscape_ns(tag): """Prepend the `inkscape` namespace to an element tag.""" return _add_ns(tag, INKSCAPE_NS, 'inkscape')
[docs]def sodipodi_ns(tag): """Prepend the `sodipodi` namespace to an element tag.""" return _add_ns(tag, INKSCAPE_NS, 'sodipodi')
[docs]def utlco_ns(tag): """Prepend the `utlco` namespace to an element tag""" return _add_ns(tag, UTLCO_NS, 'utlco')
[docs]class InkscapeSVGContext(svg.SVGContext): """""" _DEFAULT_SHAPES = ('path', 'rect', 'line', 'circle', 'ellipse', 'polyline', 'polygon') _DEFAULT_DOC_UNITS = 'px' def __init__(self, document, *args, **kwargs): """""" super(InkscapeSVGContext, self).__init__(document, *args, **kwargs) #: Inkscape document name self.doc_name = self.docroot.get('sodipodi:docname', 'untitled.svg') # The Inkscape doc unit overrides the implicit SVG unit basedoc = self.find('//sodipodi:namedview') basedoc_units = basedoc.get('units', self._DEFAULT_DOC_UNITS) #: Inkscape GUI document units self.doc_units = basedoc.get(inkscape_ns('document-units'), basedoc_units) #: Document clipping rectangle self.cliprect = geom.Box((0, 0), self.get_document_size()) # Current Inkscape layer self._current_layer_id = basedoc.get(inkscape_ns('current-layer')) layer = self.get_selected_layer() if layer is None: layer = self.docroot self.set_default_parent(layer)
[docs] def get_document_size(self): """Return width and height of document in user units as a tuple (W, H). """ return (self.view_width, self.view_height)
[docs] def margin_cliprect(self, mtop, *args): """ Create a clipping rectangle based on document bounds with the specified margins. Margin argument order follows CSS margin property rules. Args: mtop: Top margin (user units) mright: Right margin (user units) mbottom: Bottom margin (user units) mleft: Left margin (user units) Returns: A geom.Box clipping rectangle """ doc_size = self.get_document_size() mright = mtop mbottom = mtop mleft = mtop if len(args) > 0: mright = args[0] mleft = args[0] if len(args) > 1: mbottom = args[1] if len(args) > 2: mleft = args[2] return geom.Box((mleft, mbottom), (doc_size[0] - mright, doc_size[1] - mtop))
[docs] def get_document_name(self): """Return the name of this document. Default is 'untitled'.""" self.doc_name
[docs] def get_document_units(self): """Return the Inkscape document unit string ('in', 'mm', etc.). This is the document unit used for the UI (dialogs etc.). It might not be the same as the document user unit. """ return self.doc_units
[docs] def get_selected_layer(self): """Get the currently selected Inkscape layer element. Returns: The currently selected layer element or None if no layers are selected. """ if self._current_layer_id is not None: return self.get_node_by_id(self._current_layer_id) return None
[docs] def find_layer(self, layer_name): """Find an Inkscape layer by Inkscape layer name. If there is more than one layer by that name then just the first one will be returned. :param layer_name: The Inkscape layer name to find. :return: The layer Element node or None. """ return self.find('//svg:g[@inkscape:label="%s"]' % layer_name)
# def clear_layer(self, layer_name): # """Delete the contents of the specified layer. # Does nothing if the layer doesn't exist. # """ # layer = self.find_layer(layer_name) # if layer is not None: # del layer[:]
[docs] def create_layer(self, name, opacity=None, clear=True, incr_suffix=False, flipy=False, tag=None): """Create an Inkscape layer or return an existing layer. Args: name: The name of the layer to create. opacity: Layer opacity (0.0 to 1.0). clear: If a layer of the same name already exists then erase it first if True otherwise just return it. Default is True. incr_suffix: If a layer of the same name already exists and it is non-empty then add an auto-incrementing numeric suffix to the name (overrides *clear*). flipy: Add transform to flip Y axis. tag (str): A layer tag added as an extended attribute. Uses `utlco` namespace. This can be used to tag layers with a custom label. Returns: A new layer or an existing layer of the same name. """ layer_name = name layer = self.find_layer(name) if layer is not None and incr_suffix and len(layer) > 0: suffix_n = 0 while layer is not None and len(layer) > 0: layer_name = '%s_%02d' % (name, suffix_n) suffix_n += 1 layer = self.find_layer(layer_name) if layer is None: layer_attrs = {inkscape_ns('groupmode'): 'layer', inkscape_ns('label'): layer_name} if tag is not None: layer_attrs[utlco_ns('tag')] = tag if opacity is not None: opacity = min(max(opacity, 0.0), 1.0) layer_attrs['style'] = 'opacity: %.2f;' % opacity if flipy: transfrm = 'translate(0, %g) scale(1, -1)' % self.view_height layer_attrs['transform'] = transfrm layer = etree.SubElement(self.docroot, 'g', layer_attrs) elif clear: # Remove subelements del layer[:] # if 'transform' in layer.attrib: # del layer.attrib['transform'] return layer
# def _find_auto_incr_layer_name(self, name, force_new=False): # """ # """ # layer_name = name # layer = self.find_layer(layer_name) # suffix_n = 0 # while layer is not None and (len(layer) > 0 or force_new): # layer_name = '%s %02d' % (name, suffix_n) # suffix_n += 1 # layer = self.find_layer(layer_name) # return layer_name # # def duplicate_layer(self, layer, new_name=None): # """Create a copy of an Inkscape layer and all of its sub-elements. # # Args: # layer: The layer to copy # new_name: Name of the copy. # By default the name will be the name of the source layer # followed by an auto-incremented suffix. # # Returns: # A copy of the specified layer. # """ # raise NotImplementedError() # src_name = self.get_layer_name(layer) # if new_name is None: # new_name = src_name
[docs] def set_layer_name(self, layer, name): """Rename an Inkscape layer. """ layer.set(inkscape_ns('label'), name)
[docs] def get_layer_name(self, layer): """Return the name of the Inkscape layer. """ return layer.get(inkscape_ns('label'))
[docs] def get_parent_layer(self, node): """Return the layer that the node resides in. Returns None if the node is not in a layer. """ # TODO: it's probably better/faster to recursively climb # the parent chain until docroot or layer is found. # This assumes that Inkscape still doesn't support sub-layers layers = self.document.xpath('//svg:g[@inkscape:groupmode="layer"]', namespaces=INKSCAPE_NS) for layer in layers: if node in layer.iter(): return layer return None
[docs] def layer_is_locked(self, layer): """ Returns: True if the layer is locked, otherwise False. """ val = layer.get(sodipodi_ns('insensitive')) return val is not None and val.lower() == 'true'
[docs] def find(self, path): """Find an element in the current document. Args: path: XPath path. Returns: The first matching element or None if not found. """ try: node = self.document.xpath(path, namespaces=INKSCAPE_NS)[0] except IndexError: node = None return node
[docs] def get_visible_layers(self): """Get a list of visible layers """ layers = [] for node in self.docroot: if self.is_layer(node) and self.node_is_visible(node): layers.append(node) return layers
[docs] def get_layer_elements(self, layer): """Get document elements by layer. Returns all the visible child elements of the given layer. Args: layer: The layer root element. Returns: A (possibly empty) list of visible elements. """ elements = [] if self.node_is_visible(layer): for node in layer: if self.node_is_visible(node, check_parent=False): elements.append(node) return elements
[docs] def is_layer(self, node): """Determine if the element is an Inkscape layer node. """ if self.node_is_group(node): layer_name = self.get_layer_name(node) return layer_name is not None and layer_name return False
[docs] def get_shape_elements(self, rootnode, shapetags=_DEFAULT_SHAPES, parent_transform=None, skip_layers=None, accumulate_transform=True): """ Traverse a tree of SVG nodes and flatten it to a list of tuples containing an SVG shape element and its accumulated transform. This does a depth-first traversal of <g> and <use> elements. Hidden elements are ignored. Args: rootnode: The root of the node tree to traverse and flatten. This can be the document root, a layer, or simply a list of element nodes. shapetags: List of shape element tags that can be fetched. Default is ('path', 'rect', 'line', 'circle', 'ellipse', 'polyline', 'polygon'). Anything else is ignored. parent_transform: Transform matrix to add to each node's transforms. If None the node's parent transform is used. skip_layers: A list of layer names (as regexes) to ignore accumulate_transform: Apply parent transform(s) to element node if True. Default is True. Returns: A possibly empty list of 2-tuples consisting of SVG element and accumulated transform. """ if etree.iselement(rootnode): if not self.node_is_visible(rootnode): return [] check_parent = False else: # rootnode will be a list of possibly non-sibling element nodes # so the parent's visibility should be checked for each node. check_parent = True nodes = [] for node in rootnode: nodes.extend(self._get_shape_nodes_recurs(node, shapetags, parent_transform, check_parent, skip_layers, accumulate_transform)) return nodes
def _get_shape_nodes_recurs(self, node, shapetags, parent_transform, check_parent, skip_layers, accumulate_transform): """Recursively traverse an SVG node tree and flatten it to a list of tuples containing an SVG shape element and its accumulated transform. This does a depth-first traversal of <g> and <use> elements. Anything besides paths, rectangles, circles, ellipses, lines, polygons, and polylines are ignored. Hidden elements are ignored. Args: node: The root of the node tree to traverse and flatten. shapetags: List of shape element tags that can be fetched. parent_transform: Transform matrix to add to each node's transforms. check_parent: Check parent visibility skip_layers: A list of layer names (as regexes) to ignore accumulate_transform: Apply parent transform(s) to element node if True. Returns: A possibly empty list of 2-tuples consisting of SVG element and transform. """ if not self.node_is_visible(node, check_parent=check_parent): return [] if parent_transform is None: parent_transform = self.get_parent_transform(node) nodelist = [] # first apply the current transform matrix to this node's tranform node_transform = self.parse_transform_attr(node.get('transform')) if accumulate_transform: node_transform = transform2d.compose_transform(parent_transform, node_transform) if self.node_is_group(node): if self.is_layer(node) and skip_layers is not None and skip_layers: layer_name = self.get_layer_name(node) # logger.debug('layer: %s', layer_name) for skip_layer in skip_layers: if re.match(skip_layer, layer_name) is not None: # logger.debug('skipping layer: %s', layer_name) return [] # Recursively traverse group children for child_node in node: subnodes = self._get_shape_nodes_recurs(child_node, shapetags, node_transform, False, skip_layers, accumulate_transform) nodelist.extend(subnodes) elif node.tag == svg_ns('use') or node.tag == 'use': # A <use> element refers to another SVG element via an # xlink:href="#id" attribute. refid = node.get(svg.xlink_ns('href')) if refid: # [1:] to ignore leading '#' in reference refnode = self.get_node_by_id(refid[1:]) # TODO: Can the referred node not be visible? if refnode is not None: # and self.node_is_visible(refnode): # Apply explicit x,y translation transform x = float(node.get('x', '0')) y = float(node.get('y', '0')) if x != 0 or y != 0: translation = transform2d.matrix_translate(x, y) node_transform = transform2d.compose_transform( node_transform, translation) subnodes = self._get_shape_nodes_recurs(refnode, shapetags, node_transform, False, skip_layers, accumulate_transform) nodelist.extend(subnodes) elif svg.strip_ns(node.tag) in shapetags: nodelist.append((node, node_transform)) return nodelist
[docs]def create_inkscape_document(width, height, doc_units='px', doc_id=None, doc_name=None, layer_name=None, layer_id='defaultlayer'): """Create a minimal Inkscape-compatible SVG document. Args: width: The width of the document in user units. height: The height of the document in user units. doc_units: The user unit type (i.e. 'in', 'mm', 'pt', 'em', etc.) By default this will be 'px'. doc_id: The id attribute of the enclosing svg element. If None (default) then a random id will be generated. doc_name: The name of the document (i.e. 'MyDrawing.svg'). layer_name: Display name of default layer. By default no default layer will be created. layer_id: Id attribute value of default layer. Default id is 'defaultlayer'. Returns: An lxml.etree.ElementTree """ document = svg.create_svg_document(width, height, doc_units, doc_id) docroot = document.getroot() # Add Inkscape-specific elements... if doc_name is not None and doc_name: docroot.set(sodipodi_ns('docname'), doc_name) namedview = etree.SubElement(docroot, sodipodi_ns('namedview'), id='namedview') namedview.set(inkscape_ns('document-units'), doc_units) if layer_name is not None and layer_name: layer = etree.SubElement(docroot, svg_ns('g'), id=layer_id) layer.set(inkscape_ns('groupmode'), 'layer') layer.set(inkscape_ns('label'), layer_name) namedview.set(inkscape_ns('current-layer'), layer_id) return document