# -*- coding: utf-8 -*-
#
# Copyright (c) Aaron Spike <aaron@ekips.org>
# Aurélio A. Heckert <aurium(a)gmail.com>
# Bulia Byak <buliabyak@users.sf.net>
# Nicolas Dufour, nicoduf@yahoo.fr
# Peter J. R. Moulder <pjrm@users.sourceforge.net>
# Martin Owens <doctormo@gmail.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# pylint: disable=attribute-defined-outside-init
#
"""
Provide a way to load lxml attributes with an svg API on top.
"""
import random
from collections import OrderedDict
from .units import discover_unit, convert_unit, render_unit
from .transforms import BoundingBox
from .elements import BaseElement, NamedView, Defs
if False: # pylint: disable=using-constant-test
import typing # pylint: disable=unused-import
[docs]class SvgDocumentElement(BaseElement): # pylint: disable=too-many-public-methods
"""Provide access to the document level svg functionality"""
tag_name = 'svg'
def _init(self):
self.current_layer = None
self.view_center = (0.0, 0.0)
self.selected = OrderedDict()
self.ids = {}
[docs] def get_ids(self):
"""Returns a set of unique document ids"""
if not self.ids:
self.ids = set(self.xpath('//@id'))
return self.ids
[docs] def get_unique_id(self, prefix, size=4):
"""Generate a new id from an existing old_id"""
ids = self.get_ids()
new_id = None
_from = 10 ** size - 1
_to = 10 ** size
while new_id is None or new_id in ids:
# Do not use randint because py2/3 incompatibility
new_id = prefix + str(int(random.random() * _from - _to) + _to)
self.ids.add(new_id)
return new_id
[docs] def set_selected(self, *ids):
"""
Sets the currently selected elements to these ids.
Arguments are zero or more ids, element objects or a single xpath expression starting with "//".
All element objects must have an id to be correctly set.
>>> svg.set_selected("rect123", "path456", "text789")
>>> svg.set_selected(elem1, elem2, elem3)
>>> svg.set_selected("//rect")
"""
self.selected = OrderedDict()
# Allow selecting of xpath elements directly
if len(ids) == 1 and isinstance(ids[0], str) and ids[0].startswith('//'):
ids = self.xpath(ids[0])
for elem_id in ids:
if isinstance(elem_id, BaseElement):
# Selection is a list of nodes to select
self.selected[elem_id.get('id')] = elem_id
continue
# Selection is a text element id, find it (or them).
for node in self.xpath('//*[@id="{}"]'.format(elem_id)):
self.selected[elem_id] = node
[docs] def get_z_selected(self):
"""Get the selected elements, but ordered by their apperence in the document"""
sel = self.selected
return OrderedDict((_id, sel[_id]) for _id in self.xpath('//@id') if _id in sel)
[docs] def get_selected_bbox(self):
"""Gets the bounding box of the selected items"""
ret = sum([node.bounding_box() for node in self.selected.values()])
return BoundingBox(None) if ret == 0 else ret
[docs] def get_page_bbox(self):
"""Gets the page dimentions as a bbox"""
return BoundingBox((0, float(self.width)), (0, float(self.height)))
[docs] def get_first_selected(self):
"""Returns the first item in the selected list"""
if self.selected:
return list(self.selected.values())[0]
return None
[docs] def get_current_layer(self):
"""Returns the currently selected layer"""
layer = self.getElementById(self.namedview.current_layer, 'svg:g')
if layer is None:
return self
return layer
[docs] def get_center_position(self):
"""Returns view_center in terms of document units"""
namedview = self.namedview
if namedview.center_x and namedview.center_y:
return (self.unittouu(namedview.center_x),
self.unittouu(namedview.center_y))
# y-coordinate flip, eliminate it when it's gone in Inkscape
# doc_height = self.unittouu(self.height)
# return (float(x), doc_height - float(y))
return 0.0, 0.0
[docs] def getElement(self, xpath): # pylint: disable=invalid-name
"""Gets a single element from the given xpath or returns None"""
return self.findone(xpath)
[docs] def getElementById(self, eid, elm='*'): # pylint: disable=invalid-name
"""Get an element in this svg document by it's ID attribute"""
return self.getElement('//{}[@id="{}"]'.format(elm, eid))
@property
def name(self):
"""Returns the Document Name"""
return self.get('sodipodi:docname', '')
@property
def namedview(self):
"""Return the sp namedview meta information element"""
nvs = self.xpath('//sodipodi:namedview')
if not nvs:
# We auto create a namedview element when needed
nvs = [NamedView()]
self.insert(0, nvs[0])
return nvs[0]
@property
def defs(self):
"""Return the svg defs meta element container"""
defs = self.xpath('//svg:defs')
if not defs:
defs = [Defs()]
self.insert(0, defs[0])
return defs[0]
[docs] def get_viewbox(self):
"""Parse and return the document's viewBox attribute"""
try:
ret = [float(unit) for unit in self.get('viewBox', '0').split()]
except ValueError:
ret = ''
if len(ret) != 4:
return [0, 0, 0, 0]
return ret
@property
def width(self): # getDocumentWidth(self):
"""Fault tolerance for lazily defined SVG"""
return self.unittouu(self.get('width')) or self.get_viewbox()[2]
@property
def height(self): # getDocumentHeight(self):
"""Returns a string corresponding to the height of the document, as
defined in the SVG file. If it is not defined, returns the height
as defined by the viewBox attribute. If viewBox is not defined,
returns the string '0'."""
return self.unittouu(self.get('height')) or self.get_viewbox()[3]
@property
def scale(self):
"""Return the ratio between the page width and the viewBox width"""
try:
scale_x = float(self.width) / float(self.get_viewbox()[2])
scale_y = float(self.height) / float(self.get_viewbox()[3])
return max([scale_x, scale_y])
except (ValueError, ZeroDivisionError):
return 1.0
@property
def unit(self):
"""Returns the unit used for in the SVG document.
In the case the SVG document lacks an attribute that explicitly
defines what units are used for SVG coordinates, it tries to calculate
the unit from the SVG width and viewBox attributes.
Defaults to 'px' units."""
viewbox = self.get_viewbox()
if viewbox and set(viewbox) != {0}:
return discover_unit(self.get('width'), viewbox[2], default='px')
return 'px' # Default is px
[docs] def unittouu(self, value):
"""Convert a unit value into the document's units"""
return convert_unit(value, self.unit)
[docs] def uutounit(self, value, to_unit):
"""Convert from the document's units to the given unit"""
return convert_unit(render_unit(value, self.unit), to_unit)
[docs] def add_unit(self, value):
"""Add document unit when no unit is specified in the string """
return render_unit(value, self.unit)