Commit 4846815e authored by Gary Hodgson's avatar Gary Hodgson Committed by Guillaume Seguin

replace wxpsvg wih cairosvg as it produces correctly scaled images

parent 6db9e428
# -*- coding: utf-8 -*-
# This file is part of CairoSVG
# Copyright © 2010-2012 Kozea
#
# This library is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# This library 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 Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with CairoSVG. If not, see <http://www.gnu.org/licenses/>.
"""
CairoSVG - A simple SVG converter for Cairo.
"""
import os
import sys
import optparse
from . import surface
VERSION = '0.4.4'
SURFACES = {
'SVG': surface.SVGSurface, # Tell us if you actually use this one!
'PNG': surface.PNGSurface,
'PDF': surface.PDFSurface,
'PS': surface.PSSurface}
# Generate the svg2* functions from SURFACES
for _output_format, _surface_type in SURFACES.items():
_function = (
# Two lambdas needed for the closure
lambda surface_type: lambda *args, **kwargs: # pylint: disable=W0108
surface_type.convert(*args, **kwargs))(_surface_type)
_name = 'svg2%s' % _output_format.lower()
_function.__name__ = _name
_function.__doc__ = surface.Surface.convert.__doc__.replace(
'the format for this class', _output_format)
setattr(sys.modules[__name__], _name, _function)
def main():
"""Entry-point of the executable."""
# Get command-line options
option_parser = optparse.OptionParser(
usage="usage: %prog filename [options]", version=VERSION)
option_parser.add_option(
"-f", "--format", help="output format")
option_parser.add_option(
"-d", "--dpi", help="svg resolution", default=96)
option_parser.add_option(
"-o", "--output",
default="", help="output filename")
options, args = option_parser.parse_args()
# Print help if no argument is given
if not args:
option_parser.print_help()
sys.exit()
kwargs = {'dpi': float(options.dpi)}
if not options.output or options.output == '-':
# Python 2/3 hack
bytes_stdout = getattr(sys.stdout, "buffer", sys.stdout)
kwargs['write_to'] = bytes_stdout
else:
kwargs['write_to'] = options.output
url = args[0]
if url == "-":
# Python 2/3 hack
bytes_stdin = getattr(sys.stdin, "buffer", sys.stdin)
kwargs['file_obj'] = bytes_stdin
else:
kwargs['url'] = url
output_format = (
options.format or
os.path.splitext(options.output)[1].lstrip(".") or
"pdf")
SURFACES[output_format.upper()].convert(**kwargs)
# -*- coding: utf-8 -*-
# This file is part of CairoSVG
# Copyright © 2010-2012 Kozea
#
# This library is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# This library 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 Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with CairoSVG. If not, see <http://www.gnu.org/licenses/>.
"""
Optionally handle CSS stylesheets.
"""
from .parser import HAS_LXML
# Detect optional depedencies
# pylint: disable=W0611
try:
import tinycss
import cssselect
CSS_CAPABLE = HAS_LXML
except ImportError:
CSS_CAPABLE = False
# pylint: enable=W0611
# Python 2/3 compat
iteritems = getattr(dict, "iteritems", dict.items) # pylint: disable=C0103
def find_stylesheets(tree):
"""Find the stylesheets included in ``tree``."""
# TODO: support contentStyleType on <svg>
default_type = "text/css"
for element in tree.iter():
# http://www.w3.org/TR/SVG/styling.html#StyleElement
if (element.tag == "style" and
element.get("type", default_type) == "text/css"):
# TODO: pass href for relative URLs
# TODO: support media types
# TODO: what if <style> has children elements?
yield tinycss.make_parser().parse_stylesheet(element.text)
# TODO: support <?xml-stylesheet ... ?>
def find_style_rules(tree):
"""Find the style rules in ``tree``."""
for stylesheet in find_stylesheets(tree):
# TODO: warn for each stylesheet.errors
for rule in stylesheet.rules:
# TODO: support @import and @media
if not rule.at_keyword:
yield rule
def get_declarations(rule):
"""Get the declarations in ``rule``."""
for declaration in rule.declarations:
if declaration.name.startswith("-"):
# Ignore properties prefixed by "-"
continue
# TODO: filter out invalid values
yield (
declaration.name,
declaration.value.as_css(),
bool(declaration.priority))
def match_selector(rule, tree):
"""Yield the ``(element, specificity)`` in ``tree`` matching ``rule``."""
selector_list = cssselect.parse(rule.selector.as_css())
translator = cssselect.GenericTranslator()
for selector in selector_list:
if not selector.pseudo_element:
specificity = selector.specificity()
for element in tree.xpath(translator.selector_to_xpath(selector)):
yield element, specificity
def apply_stylesheets(tree):
"""Apply the stylesheet in ``tree`` to ``tree``."""
if not CSS_CAPABLE:
# TODO: warn?
return
style_by_element = {}
for rule in find_style_rules(tree):
declarations = list(get_declarations(rule))
for element, specificity in match_selector(rule, tree):
style = style_by_element.setdefault(element, {})
for name, value, important in declarations:
weight = important, specificity
if name in style:
_old_value, old_weight = style[name]
if old_weight > weight:
continue
style[name] = value, weight
for element, style in iteritems(style_by_element):
values = ["%s: %s" % (name, value)
for name, (value, weight) in iteritems(style)]
values.append(element.get("style", ""))
element.set("style", ";".join(values))
# -*- coding: utf-8 -*-
# This file is part of CairoSVG
# Copyright © 2010-2012 Kozea
#
# This library is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# This library 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 Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with CairoSVG. If not, see <http://www.gnu.org/licenses/>.
"""
SVG Parser.
"""
# Fallbacks for Python 2/3 and lxml/ElementTree
# pylint: disable=E0611,F0401,W0611
try:
import lxml.etree as ElementTree
from lxml.etree import XMLSyntaxError as ParseError
HAS_LXML = True
except ImportError:
from xml.etree import ElementTree
from xml.parsers import expat
# ElementTree's API changed between 2.6 and 2.7
# pylint: disable=C0103
ParseError = getattr(ElementTree, 'ParseError', expat.ExpatError)
# pylint: enable=C0103
HAS_LXML = False
try:
from urllib import urlopen
import urlparse
except ImportError:
from urllib.request import urlopen
from urllib import parse as urlparse # Python 3
# pylint: enable=E0611,F0401,W0611
import gzip
import os.path
from .css import apply_stylesheets
# Python 2/3 compat
# pylint: disable=C0103,W0622
try:
basestring
except NameError:
basestring = str
# pylint: enable=C0103,W0622
def remove_svg_namespace(tree):
"""Remove the SVG namespace from ``tree`` tags.
``lxml.cssselect`` does not support empty/default namespaces, so remove any
SVG namespace.
"""
prefix = "{http://www.w3.org/2000/svg}"
prefix_len = len(prefix)
iterator = (
tree.iter() if hasattr(tree, 'iter')
else tree.getiterator())
for element in iterator:
tag = element.tag
if hasattr(tag, "startswith") and tag.startswith(prefix):
element.tag = tag[prefix_len:]
class Node(dict):
"""SVG node with dict-like properties and children."""
def __init__(self, node, parent=None):
"""Create the Node from ElementTree ``node``, with ``parent`` Node."""
super(Node, self).__init__()
self.children = ()
self.root = False
self.tag = node.tag
self.text = node.text
# Inherits from parent properties
# TODO: drop other attributes that should not be inherited
if parent is not None:
items = parent.copy()
not_inherited = (
"transform", "opacity", "style", "viewBox", "stop-color",
"stop-opacity")
if self.tag in ("tspan", "pattern"):
not_inherited += ("x", "y")
for attribute in not_inherited:
if attribute in items:
del items[attribute]
self.update(items)
self.url = parent.url
self.xml_tree = parent.xml_tree
self.parent = parent
self.update(dict(node.attrib.items()))
# Handle the CSS
style = self.pop("style", "")
for declaration in style.split(";"):
if ":" in declaration:
name, value = declaration.split(":", 1)
self[name.strip()] = value.strip()
# Replace currentColor by a real color value
color_attributes = (
"fill", "stroke", "stop-color", "flood-color",
"lighting-color")
for attribute in color_attributes:
if self.get(attribute) == "currentColor":
self[attribute] = self.get("color", "black")
# Replace inherit by the parent value
for attribute, value in dict(self).items():
if value == "inherit":
if parent is not None and attribute in parent:
self[attribute] = parent.get(attribute)
else:
del self[attribute]
# Manage text by creating children
if self.tag == "text" or self.tag == "textPath":
self.children = self.text_children(node)
if not self.children:
self.children = tuple(
Node(child, self) for child in node
if isinstance(child.tag, basestring))
def text_children(self, node):
"""Create children and return them."""
children = []
for child in node:
children.append(Node(child, parent=self))
if child.tail:
anonymous = ElementTree.Element('tspan')
anonymous.text = child.tail
children.append(Node(anonymous, parent=self))
return list(children)
class Tree(Node):
"""SVG tree."""
def __init__(self, **kwargs):
"""Create the Tree from SVG ``text``."""
# Make the parameters keyword-only:
bytestring = kwargs.pop('bytestring', None)
file_obj = kwargs.pop('file_obj', None)
url = kwargs.pop('url', None)
parent = kwargs.pop('parent', None)
if bytestring is not None:
tree = ElementTree.fromstring(bytestring)
self.url = url
elif file_obj is not None:
tree = ElementTree.parse(file_obj).getroot()
if url:
self.url = url
else:
self.url = getattr(file_obj, 'name', None)
elif url is not None:
if "#" in url:
url, element_id = url.split("#", 1)
else:
element_id = None
if parent and parent.url:
if url:
url = urlparse.urljoin(parent.url, url)
elif element_id:
url = parent.url
self.url = url
if url:
if urlparse.urlparse(url).scheme:
input_ = urlopen(url)
else:
input_ = url # filename
if os.path.splitext(url)[1].lower() == "svgz":
input_ = gzip.open(url)
tree = ElementTree.parse(input_).getroot()
else:
tree = parent.xml_tree
if element_id:
iterator = (
tree.iter() if hasattr(tree, 'iter')
else tree.getiterator())
for element in iterator:
if element.get("id") == element_id:
tree = element
break
else:
raise TypeError(
'No tag with id="%s" found.' % element_id)
else:
raise TypeError(
'No input. Use one of bytestring, file_obj or url.')
remove_svg_namespace(tree)
apply_stylesheets(tree)
self.xml_tree = tree
super(Tree, self).__init__(tree, parent)
self.root = True
# -*- coding: utf-8 -*-
# This file is part of CairoSVG
# Copyright © 2010-2012 Kozea
#
# This library is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# This library 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 Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with CairoSVG. If not, see <http://www.gnu.org/licenses/>.
"""
Cairo surface creators.
"""
import cairo
import io
from ..parser import Tree
from .colors import color
from .defs import gradient_or_pattern, parse_def
from .helpers import (
node_format, transform, normalize, filter_fill_or_stroke,
apply_matrix_transform, PointError)
from .path import PATH_TAGS
from .tags import TAGS
from .units import size
from . import units
class Surface(object):
"""Abstract base class for CairoSVG surfaces.
The ``width`` and ``height`` attributes are in device units (pixels for
PNG, else points).
The ``context_width`` and ``context_height`` attributes are in user units
(i.e. in pixels), they represent the size of the active viewport.
"""
# Subclasses must either define this or override _create_surface()
surface_class = None
@classmethod
def convert(cls, bytestring=None, **kwargs):
"""Convert a SVG document to the format for this class.
Specify the input by passing one of these:
:param bytestring: The SVG source as a byte-string.
:param file_obj: A file-like object.
:param url: A filename.
And the output with:
:param write_to: The filename of file-like object where to write the
output. If None or not provided, return a byte string.
Only ``source`` can be passed as a positional argument, other
parameters are keyword-only.
"""
dpi = kwargs.pop('dpi', 96)
write_to = kwargs.pop('write_to', None)
kwargs['bytestring'] = bytestring
tree = Tree(**kwargs)
if write_to is None:
output = io.BytesIO()
else:
output = write_to
cls(tree, output, dpi).finish()
if write_to is None:
return output.getvalue()
def __init__(self, tree, output, dpi):
"""Create the surface from a filename or a file-like object.
The rendered content is written to ``output`` which can be a filename,
a file-like object, ``None`` (render in memory but do not write
anything) or the built-in ``bytes`` as a marker.
Call the ``.finish()`` method to make sure that the output is
actually written.
"""
self.cairo = None
self.context_width, self.context_height = None, None
self.cursor_position = 0, 0
self.total_width = 0
self.markers = {}
self.gradients = {}
self.patterns = {}
self.paths = {}
self.page_sizes = []
self._old_parent_node = self.parent_node = None
self.output = output
self.dpi = dpi
self.font_size = size(self, "12pt")
width, height, viewbox = node_format(self, tree)
# Actual surface dimensions: may be rounded on raster surfaces types
self.cairo, self.width, self.height = self._create_surface(
width * self.device_units_per_user_units,
height * self.device_units_per_user_units)
self.page_sizes.append((self.width, self.height))
self.context = cairo.Context(self.cairo)
# We must scale the context as the surface size is using physical units
self.context.scale(
self.device_units_per_user_units, self.device_units_per_user_units)
# Initial, non-rounded dimensions
self.set_context_size(width, height, viewbox)
self.context.move_to(0, 0)
self.draw_root(tree)
@property
def points_per_pixel(self):
"""Surface resolution."""
return 1 / (self.dpi * units.UNITS["pt"])
@property
def device_units_per_user_units(self):
"""Ratio between Cairo device units and user units.
Device units are points for everything but PNG, and pixels for
PNG. User units are pixels.
"""
return self.points_per_pixel
def _create_surface(self, width, height):
"""Create and return ``(cairo_surface, width, height)``."""
# self.surface_class should not be None when called here
# pylint: disable=E1102
cairo_surface = self.surface_class(self.output, width, height)
# pylint: enable=E1102
return cairo_surface, width, height
def set_context_size(self, width, height, viewbox):
"""Set the Cairo context size, set the SVG viewport size."""
if viewbox:
x, y, x_size, y_size = viewbox
self.context_width, self.context_height = x_size, y_size
x_ratio, y_ratio = width / x_size, height / y_size
matrix = cairo.Matrix()
if x_ratio > y_ratio:
matrix.translate((width - x_size * y_ratio) / 2, 0)
matrix.scale(y_ratio, y_ratio)
matrix.translate(-x, -y / y_ratio * x_ratio)
elif x_ratio < y_ratio:
matrix.translate(0, (height - y_size * x_ratio) / 2)
matrix.scale(x_ratio, x_ratio)
matrix.translate(-x / x_ratio * y_ratio, -y)
else:
matrix.scale(x_ratio, y_ratio)
matrix.translate(-x, -y)
apply_matrix_transform(self, matrix)
else:
self.context_width, self.context_height = width, height
def finish(self):
"""Read the surface content."""
self.cairo.finish()
def draw_root(self, node):
"""Draw the root ``node``."""
self.draw(node)
def draw(self, node, stroke_and_fill=True):
"""Draw ``node`` and its children."""
old_font_size = self.font_size
self.font_size = size(self, node.get("font-size", "12pt"))
# Do not draw defs
if node.tag == "defs":
for child in node.children:
parse_def(self, child)
return
# Do not draw elements with width or height of 0
if (("width" in node and size(self, node["width"]) == 0) or
("height" in node and size(self, node["height"]) == 0)):
return
node.tangents = [None]
node.pending_markers = []
self._old_parent_node = self.parent_node
self.parent_node = node
opacity = float(node.get("opacity", 1))
if opacity < 1:
self.context.push_group()
self.context.save()
self.context.move_to(
size(self, node.get("x"), "x"),
size(self, node.get("y"), "y"))
# Transform the context according to the ``transform`` attribute
transform(self, node.get("transform"))
if node.tag in PATH_TAGS:
# Set 1 as default stroke-width
if not node.get("stroke-width"):
node["stroke-width"] = "1"
# Set node's drawing informations if the ``node.tag`` method exists
line_cap = node.get("stroke-linecap")
if line_cap == "square":
self.context.set_line_cap(cairo.LINE_CAP_SQUARE)
if line_cap == "round":
self.context.set_line_cap(cairo.LINE_CAP_ROUND)
join_cap = node.get("stroke-linejoin")
if join_cap == "round":
self.context.set_line_join(cairo.LINE_JOIN_ROUND)
if join_cap == "bevel":
self.context.set_line_join(cairo.LINE_JOIN_BEVEL)
dash_array = normalize(node.get("stroke-dasharray", "")).split()
if dash_array:
dashes = [size(self, dash) for dash in dash_array]
if sum(dashes):
offset = size(self, node.get("stroke-dashoffset"))
self.context.set_dash(dashes, offset)
miter_limit = float(node.get("stroke-miterlimit", 4))
self.context.set_miter_limit(miter_limit)
if node.tag in TAGS:
try:
TAGS[node.tag](self, node)
except PointError:
# Error in point parsing, do nothing
pass
# Get stroke and fill opacity
stroke_opacity = float(node.get("stroke-opacity", 1))
fill_opacity = float(node.get("fill-opacity", 1))
# Manage dispaly and visibility
display = node.get("display", "inline") != "none"
visible = display and (node.get("visibility", "visible") != "hidden")
if stroke_and_fill and visible:
# Fill
if "url(#" in (node.get("fill") or ""):
name = filter_fill_or_stroke(node.get("fill"))
gradient_or_pattern(self, node, name)
else:
if node.get("fill-rule") == "evenodd":
self.context.set_fill_rule(cairo.FILL_RULE_EVEN_ODD)
self.context.set_source_rgba(
*color(node.get("fill", "black"), fill_opacity))
self.context.fill_preserve()
# Stroke
self.context.set_line_width(size(self, node.get("stroke-width")))
if "url(#" in (node.get("stroke") or ""):
name = filter_fill_or_stroke(node.get("stroke"))
gradient_or_pattern(self, node, name)
else:
self.context.set_source_rgba(
*color(node.get("stroke"), stroke_opacity))
self.context.stroke()
elif not visible:
self.context.new_path()
# Draw children
if display and node.tag not in (
"linearGradient", "radialGradient", "marker", "pattern"):
for child in node.children:
self.draw(child, stroke_and_fill)
if not node.root:
# Restoring context is useless if we are in the root tag, it may
# raise an exception if we have multiple svg tags
self.context.restore()
if opacity < 1:
self.context.pop_group_to_source()
self.context.paint_with_alpha(opacity)
self.parent_node = self._old_parent_node
self.font_size = old_font_size
class MultipageSurface(Surface):
"""Abstract base class for surfaces that can handle multiple pages."""
def draw_root(self, node):
self.width = None
self.height = None
svg_children = [child for child in node.children if child.tag == 'svg']
if svg_children:
# Multi-page
for page in svg_children:
width, height, viewbox = node_format(self, page)
self.context.save()
self.set_context_size(width, height, viewbox)
width *= self.device_units_per_user_units
height *= self.device_units_per_user_units
self.page_sizes.append((width, height))
self.cairo.set_size(width, height)
self.draw(page)
self.context.restore()
self.cairo.show_page()
else:
self.draw(node)
class PDFSurface(MultipageSurface):
"""A surface that writes in PDF format."""
surface_class = cairo.PDFSurface
class PSSurface(MultipageSurface):
"""A surface that writes in PostScript format."""
surface_class = cairo.PSSurface
class PNGSurface(Surface):
"""A surface that writes in PNG format."""
device_units_per_user_units = 1
def _create_surface(self, width, height):
"""Create and return ``(cairo_surface, width, height)``."""
width = int(width)
height = int(height)
cairo_surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height)
return cairo_surface, width, height
def finish(self):
"""Read the PNG surface content."""
if self.output is not None:
self.cairo.write_to_png(self.output)
return super(PNGSurface, self).finish()
class SVGSurface(Surface):
"""A surface that writes in SVG format.
It may seem pointless to render SVG to SVG, but this can be used
with ``output=None`` to get a vector-based single page cairo surface.
"""
surface_class = cairo.SVGSurface
# -*- coding: utf-8 -*-
# This file is part of CairoSVG
# Copyright © 2010-2012 Kozea
#
# This library is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# This library 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 Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with CairoSVG. If not, see <http://www.gnu.org/licenses/>.
"""
SVG colors.
"""
COLORS = {
"aliceblue": "rgb(240, 248, 255)",
"antiquewhite": "rgb(250, 235, 215)",
"aqua": "rgb(0, 255, 255)",
"aquamarine": "rgb(127, 255, 212)",
"azure": "rgb(240, 255, 255)",
"beige": "rgb(245, 245, 220)",
"bisque": "rgb(255, 228, 196)",
"black": "rgb(0, 0, 0)",
"blanchedalmond": "rgb(255, 235, 205)",
"blue": "rgb(0, 0, 255)",
"blueviolet": "rgb(138, 43, 226)",
"brown": "rgb(165, 42, 42)",
"burlywood": "rgb(222, 184, 135)",
"cadetblue": "rgb(95, 158, 160)",
"chartreuse": "rgb(127, 255, 0)",
"chocolate": "rgb(210, 105, 30)",
"coral": "rgb(255, 127, 80)",
"cornflowerblue": "rgb(100, 149, 237)",
"cornsilk": "rgb(255, 248, 220)",
"crimson": "rgb(220, 20, 60)",
"cyan": "rgb(0, 255, 255)",
"darkblue": "rgb(0, 0, 139)",
"darkcyan": "rgb(0, 139, 139)",
"darkgoldenrod": "rgb(184, 134, 11)",
"darkgray": "rgb(169, 169, 169)",
"darkgreen": "rgb(0, 100, 0)",
"darkgrey": "rgb(169, 169, 169)",
"darkkhaki": "rgb(189, 183, 107)",
"darkmagenta": "rgb(139, 0, 139)",
"darkolivegreen": "rgb(85, 107, 47)",
"darkorange": "rgb(255, 140, 0)",
"darkorchid": "rgb(153, 50, 204)",
"darkred": "rgb(139, 0, 0)",
"darksalmon": "rgb(233, 150, 122)",
"darkseagreen": "rgb(143, 188, 143)",
"darkslateblue": "rgb(72, 61, 139)",
"darkslategray": "rgb(47, 79, 79)",
"darkslategrey": "rgb(47, 79, 79)",
"darkturquoise": "rgb(0, 206, 209)",
"darkviolet": "rgb(148, 0, 211)",
"deeppink": "rgb(255, 20, 147)",
"deepskyblue": "rgb(0, 191, 255)",
"dimgray": "rgb(105, 105, 105)",
"dimgrey": "rgb(105, 105, 105)",
"dodgerblue": "rgb(30, 144, 255)",
"firebrick": "rgb(178, 34, 34)",
"floralwhite": "rgb(255, 250, 240)",
"forestgreen": "rgb(34, 139, 34)",
"fuchsia": "rgb(255, 0, 255)",
"gainsboro": "rgb(220, 220, 220)",
"ghostwhite": "rgb(248, 248, 255)",
"gold": "rgb(255, 215, 0)",
"goldenrod": "rgb(218, 165, 32)",
"gray": "rgb(128, 128, 128)",
"grey": "rgb(128, 128, 128)",
"green": "rgb(0, 128, 0)",
"greenyellow": "rgb(173, 255, 47)",
"honeydew": "rgb(240, 255, 240)",
"hotpink": "rgb(255, 105, 180)",
"indianred": "rgb(205, 92, 92)",
"indigo": "rgb(75, 0, 130)",
"ivory": "rgb(255, 255, 240)",
"khaki": "rgb(240, 230, 140)",
"lavender": "rgb(230, 230, 250)",
"lavenderblush": "rgb(255, 240, 245)",
"lawngreen": "rgb(124, 252, 0)",
"lemonchiffon": "rgb(255, 250, 205)",
"lightblue": "rgb(173, 216, 230)",
"lightcoral": "rgb(240, 128, 128)",
"lightcyan": "rgb(224, 255, 255)",
"lightgoldenrodyellow": "rgb(250, 250, 210)",
"lightgray": "rgb(211, 211, 211)",
"lightgreen": "rgb(144, 238, 144)",
"lightgrey": "rgb(211, 211, 211)",
"lightpink": "rgb(255, 182, 193)",
"lightsalmon": "rgb(255, 160, 122)",
"lightseagreen": "rgb(32, 178, 170)",
"lightskyblue": "rgb(135, 206, 250)",
"lightslategray": "rgb(119, 136, 153)",
"lightslategrey": "rgb(119, 136, 153)",
"lightsteelblue": "rgb(176, 196, 222)",
"lightyellow": "rgb(255, 255, 224)",
"lime": "rgb(0, 255, 0)",
"limegreen": "rgb(50, 205, 50)",
"linen": "rgb(250, 240, 230)",
"magenta": "rgb(255, 0, 255)",
"maroon": "rgb(128, 0, 0)",
"mediumaquamarine": "rgb(102, 205, 170)",
"mediumblue": "rgb(0, 0, 205)",
"mediumorchid": "rgb(186, 85, 211)",
"mediumpurple": "rgb(147, 112, 219)",
"mediumseagreen": "rgb(60, 179, 113)",
"mediumslateblue": "rgb(123, 104, 238)",
"mediumspringgreen": "rgb(0, 250, 154)",
"mediumturquoise": "rgb(72, 209, 204)",
"mediumvioletred": "rgb(199, 21, 133)",
"midnightblue": "rgb(25, 25, 112)",
"mintcream": "rgb(245, 255, 250)",
"mistyrose": "rgb(255, 228, 225)",
"moccasin": "rgb(255, 228, 181)",
"navajowhite": "rgb(255, 222, 173)",
"navy": "rgb(0, 0, 128)",
"oldlace": "rgb(253, 245, 230)",
"olive": "rgb(128, 128, 0)",
"olivedrab": "rgb(107, 142, 35)",
"orange": "rgb(255, 165, 0)",
"orangered": "rgb(255, 69, 0)",
"orchid": "rgb(218, 112, 214)",
"palegoldenrod": "rgb(238, 232, 170)",
"palegreen": "rgb(152, 251, 152)",
"paleturquoise": "rgb(175, 238, 238)",
"palevioletred": "rgb(219, 112, 147)",
"papayawhip": "rgb(255, 239, 213)",
"peachpuff": "rgb(255, 218, 185)",
"peru": "rgb(205, 133, 63)",
"pink": "rgb(255, 192, 203)",
"plum": "rgb(221, 160, 221)",
"powderblue": "rgb(176, 224, 230)",
"purple": "rgb(128, 0, 128)",
"red": "rgb(255, 0, 0)",
"rosybrown": "rgb(188, 143, 143)",
"royalblue": "rgb(65, 105, 225)",
"saddlebrown": "rgb(139, 69, 19)",
"salmon": "rgb(250, 128, 114)",
"sandybrown": "rgb(244, 164, 96)",
"seagreen": "rgb(46, 139, 87)",
"seashell": "rgb(255, 245, 238)",
"sienna": "rgb(160, 82, 45)",
"silver": "rgb(192, 192, 192)",
"skyblue": "rgb(135, 206, 235)",
"slateblue": "rgb(106, 90, 205)",
"slategray": "rgb(112, 128, 144)",
"slategrey": "rgb(112, 128, 144)",
"snow": "rgb(255, 250, 250)",
"springgreen": "rgb(0, 255, 127)",
"steelblue": "rgb(70, 130, 180)",
"tan": "rgb(210, 180, 140)",
"teal": "rgb(0, 128, 128)",
"thistle": "rgb(216, 191, 216)",
"tomato": "rgb(255, 99, 71)",
"turquoise": "rgb(64, 224, 208)",
"violet": "rgb(238, 130, 238)",
"wheat": "rgb(245, 222, 179)",
"white": "rgb(255, 255, 255)",
"whitesmoke": "rgb(245, 245, 245)",
"yellow": "rgb(255, 255, 0)",
"yellowgreen": "rgb(154, 205, 50)",
"activeborder": "#0000ff",
"activecaption": "#0000ff",
"appworkspace": "#ffffff",
"background": "#ffffff",
"buttonface": "#000000",
"buttonhighlight": "#cccccc",
"buttonshadow": "#333333",
"buttontext": "#000000",
"captiontext": "#000000",
"graytext": "#333333",
"highlight": "#0000ff",
"highlighttext": "#cccccc",
"inactiveborder": "#333333",
"inactivecaption": "#cccccc",
"inactivecaptiontext": "#333333",
"infobackground": "#cccccc",
"infotext": "#000000",
"menu": "#cccccc",
"menutext": "#333333",
"scrollbar": "#cccccc",
"threeddarkshadow": "#333333",
"threedface": "#cccccc",
"threedhighlight": "#ffffff",
"threedlightshadow": "#333333",
"threedshadow": "#333333",
"window": "#cccccc",
"windowframe": "#cccccc",
"windowtext": "#000000"}
def color(string=None, opacity=1):
"""Replace ``string`` representing a color by a RGBA tuple."""
if not string or string in ("none", "transparent"):
return (0, 0, 0, 0)
string = string.strip().lower()
if string in COLORS:
string = COLORS[string]
if string.startswith("rgba"):
r, g, b, a = tuple(
float(i.strip(" %")) * 2.55 if "%" in i else float(i)
for i in string.strip(" rgba()").split(","))
return r / 255, g / 255, b / 255, a * opacity
elif string.startswith("rgb"):
r, g, b = tuple(
float(i.strip(" %")) / 100 if "%" in i else float(i) / 255
for i in string.strip(" rgb()").split(","))
return r, g, b, opacity
if len(string) in (4, 5):
string = "#" + "".join(2 * char for char in string[1:])
if len(string) == 9:
opacity *= int(string[7:9], 16) / 255
try:
plain_color = tuple(
int(value, 16) / 255. for value in (
string[1:3], string[3:5], string[5:7]))
except ValueError:
# Unknown color, return black
return (0, 0, 0, 1)
else:
return plain_color + (opacity,)
# -*- coding: utf-8 -*-
# This file is part of CairoSVG
# Copyright © 2010-2012 Kozea
#
# This library is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# This library 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 Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with CairoSVG. If not, see <http://www.gnu.org/licenses/>.
"""
Externally defined elements managers.
This module handles gradients and patterns.
"""
import cairo
from math import radians
from copy import deepcopy
from .colors import color
from .helpers import node_format, preserve_ratio, urls, transform
from .units import size
from ..parser import Tree
def parse_def(surface, node):
"""Parse the SVG definitions."""
for def_type in ("marker", "gradient", "pattern", "path"):
if def_type in node.tag.lower():
def_list = getattr(surface, def_type + "s")
name = node["id"]
href = node.get("{http://www.w3.org/1999/xlink}href")
if href and href[0] == "#" and href[1:] in def_list:
new_node = deepcopy(def_list[href[1:]])
new_node.update(node)
node = new_node
def_list[name] = node
def gradient_or_pattern(surface, node, name):
"""Gradient or pattern color."""
if name in surface.gradients:
return draw_gradient(surface, node, name)
elif name in surface.patterns:
return draw_pattern(surface, name)
def marker(surface, node):
"""Store a marker definition."""
parse_def(surface, node)
def linear_gradient(surface, node):
"""Store a linear gradient definition."""
parse_def(surface, node)
def radial_gradient(surface, node):
"""Store a radial gradient definition."""
parse_def(surface, node)
def pattern(surface, node):
"""Store a pattern definition."""
parse_def(surface, node)
def draw_gradient(surface, node, name):
"""Gradients colors."""
gradient_node = surface.gradients[name]
transform(surface, gradient_node.get("gradientTransform"))
if gradient_node.get("gradientUnits") == "userSpaceOnUse":
width_ref, height_ref = "x", "y"
diagonal_ref = "xy"
else:
x = float(size(surface, node.get("x"), "x"))
y = float(size(surface, node.get("y"), "y"))
width = float(size(surface, node.get("width"), "x"))
height = float(size(surface, node.get("height"), "y"))
width_ref = height_ref = diagonal_ref = 1
if gradient_node.tag == "linearGradient":
x1 = float(size(surface, gradient_node.get("x1", "0%"), width_ref))
x2 = float(size(surface, gradient_node.get("x2", "100%"), width_ref))
y1 = float(size(surface, gradient_node.get("y1", "0%"), height_ref))
y2 = float(size(surface, gradient_node.get("y2", "0%"), height_ref))
gradient_pattern = cairo.LinearGradient(x1, y1, x2, y2)
elif gradient_node.tag == "radialGradient":
r = float(size(surface, gradient_node.get("r", "50%"), diagonal_ref))
cx = float(size(surface, gradient_node.get("cx", "50%"), width_ref))
cy = float(size(surface, gradient_node.get("cy", "50%"), height_ref))
fx = float(size(surface, gradient_node.get("fx", str(cx)), width_ref))
fy = float(size(surface, gradient_node.get("fy", str(cy)), height_ref))
gradient_pattern = cairo.RadialGradient(fx, fy, 0, cx, cy, r)
if gradient_node.get("gradientUnits") != "userSpaceOnUse":
gradient_pattern.set_matrix(cairo.Matrix(
1 / width, 0, 0, 1 / height, - x / width, - y / height))
gradient_pattern.set_extend(getattr(
cairo, "EXTEND_%s" % node.get("spreadMethod", "pad").upper()))
offset = 0
for child in gradient_node.children:
offset = max(offset, size(surface, child.get("offset"), 1))
stop_color = color(
child.get("stop-color", "black"),
float(child.get("stop-opacity", 1)))
gradient_pattern.add_color_stop_rgba(offset, *stop_color)
gradient_pattern.set_extend(getattr(
cairo, "EXTEND_%s" % gradient_node.get("spreadMethod", "pad").upper()))
surface.context.set_source(gradient_pattern)
def draw_pattern(surface, name):
"""Draw a pattern image."""
pattern_node = surface.patterns[name]
pattern_node.tag = "g"
transform(surface, "translate(%s %s)" % (
pattern_node.get("x"), pattern_node.get("y")))
transform(surface, pattern_node.get("patternTransform"))
from . import SVGSurface # circular import
pattern_surface = SVGSurface(pattern_node, None, surface.dpi)
pattern_pattern = cairo.SurfacePattern(pattern_surface.cairo)
pattern_pattern.set_extend(cairo.EXTEND_REPEAT)
surface.context.set_source(pattern_pattern)
def draw_marker(surface, node, position="mid"):
"""Draw a marker."""
# TODO: manage markers for other tags than path
if position == "start":
node.markers = {
"start": list(urls(node.get("marker-start", ""))),
"mid": list(urls(node.get("marker-mid", ""))),
"end": list(urls(node.get("marker-end", "")))}
all_markers = list(urls(node.get("marker", "")))
for markers_list in node.markers.values():
markers_list.extend(all_markers)
pending_marker = (
surface.context.get_current_point(), node.markers[position])
if position == "start":
node.pending_markers.append(pending_marker)
return
elif position == "end":
node.pending_markers.append(pending_marker)
while node.pending_markers:
next_point, markers = node.pending_markers.pop(0)
angle1 = node.tangents.pop(0)
angle2 = node.tangents.pop(0)
if angle1 is None:
angle1 = angle2
for active_marker in markers:
if not active_marker.startswith("#"):
continue
active_marker = active_marker[1:]
if active_marker in surface.markers:
marker_node = surface.markers[active_marker]
angle = marker_node.get("orient", "0")
if angle == "auto":
angle = float(angle1 + angle2) / 2
else:
angle = radians(float(angle))
temp_path = surface.context.copy_path()
current_x, current_y = next_point
if node.get("markerUnits") == "userSpaceOnUse":
base_scale = 1
else:
base_scale = size(
surface, surface.parent_node.get("stroke-width"))
# Returns 4 values
scale_x, scale_y, translate_x, translate_y = \
preserve_ratio(surface, marker_node)
viewbox = node_format(surface, marker_node)[-1]
viewbox_width = viewbox[2] - viewbox[0]
viewbox_height = viewbox[3] - viewbox[1]
surface.context.new_path()
for child in marker_node.children:
surface.context.save()
surface.context.translate(current_x, current_y)
surface.context.rotate(angle)
surface.context.scale(
base_scale / viewbox_width * float(scale_x),
base_scale / viewbox_height * float(scale_y))
surface.context.translate(translate_x, translate_y)
surface.draw(child)
surface.context.restore()
surface.context.append_path(temp_path)
if position == "mid":
node.pending_markers.append(pending_marker)
def use(surface, node):
"""Draw the content of another SVG file."""
surface.context.save()
surface.context.translate(
size(surface, node.get("x"), "x"), size(surface, node.get("y"), "y"))
if "x" in node:
del node["x"]
if "y" in node:
del node["y"]
if "viewBox" in node:
del node["viewBox"]
href = node.get("{http://www.w3.org/1999/xlink}href")
url = list(urls(href))[0]
tree = Tree(url=url, parent=node)
surface.set_context_size(*node_format(surface, tree))
surface.draw(tree)
surface.context.restore()
# Restore twice, because draw does not restore at the end of svg tags
surface.context.restore()
# -*- coding: utf-8 -*-
# This file is part of CairoSVG
# Copyright © 2010-2012 Kozea
#
# This library is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# This library 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 Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with CairoSVG. If not, see <http://www.gnu.org/licenses/>.
"""
Surface helpers.
"""
import cairo
from math import cos, sin, tan, atan2, radians
from .units import size
# Python 2/3 management
# pylint: disable=C0103
try:
Error = cairo.Error
except AttributeError:
Error = SystemError
# pylint: enable=C0103
class PointError(Exception):
"""Exception raised when parsing a point fails."""
def distance(x1, y1, x2, y2):
"""Get the distance between two points."""
return ((x2 - x1) ** 2 + (y2 - y1) ** 2) ** 0.5
def filter_fill_or_stroke(value):
"""Remove unnecessary characters from fill or stroke value."""
if not value:
return
content = list(urls(value))[0]
if "url" in value:
if not content.startswith("#"):
return
content = content[1:]
return content
def node_format(surface, node):
"""Return ``(width, height, viewbox)`` of ``node``."""
width = size(surface, node.get("width"), "x")
height = size(surface, node.get("height"), "y")
viewbox = node.get("viewBox")
if viewbox:
viewbox = tuple(float(position) for position in viewbox.split())
width = width or viewbox[2]
height = height or viewbox[3]
return width, height, viewbox
def normalize(string=None):
"""Normalize a string corresponding to an array of various values."""
string = string.replace("-", " -")
string = string.replace(",", " ")
while " " in string:
string = string.replace(" ", " ")
string = string.replace("e -", "e-")
values = string.split(" ")
string = ""
for value in values:
if value.count(".") > 1:
numbers = value.split(".")
string += "%s.%s " % (numbers.pop(0), numbers.pop(0))
string += ".%s " % " .".join(numbers)
else:
string += value + " "
return string.strip()
def point(surface, string=None):
"""Return ``(x, y, trailing_text)`` from ``string``."""
if not string:
return (0, 0, "")
try:
x, y, string = (string.strip() + " ").split(" ", 2)
except ValueError:
raise PointError("The point cannot be found in string %s" % string)
return size(surface, x, "x"), size(surface, y, "y"), string
def point_angle(cx, cy, px, py):
"""Return angle between x axis and point knowing given center."""
return atan2(py - cy, px - cx)
def preserve_ratio(surface, node):
"""Manage the ratio preservation."""
if node.tag == "marker":
scale_x = size(surface, node.get("markerWidth", "3"), "x")
scale_y = size(surface, node.get("markerHeight", "3"), "y")
translate_x = -size(surface, node.get("refX"))
translate_y = -size(surface, node.get("refY"))
elif node.tag in ("svg", "image"):
width, height, _ = node_format(surface, node)
scale_x = width / node.image_width
scale_y = height / node.image_height
align = node.get("preserveAspectRatio", "xMidYMid").split(" ")[0]
if align == "none":
return scale_x, scale_y, 0, 0
else:
mos_properties = node.get("preserveAspectRatio", "").split()
meet_or_slice = (
mos_properties[1] if len(mos_properties) > 1 else None)
if meet_or_slice == "slice":
scale_value = max(scale_x, scale_y)
else:
scale_value = min(scale_x, scale_y)
scale_x = scale_y = scale_value
x_position = align[1:4].lower()
y_position = align[5:].lower()
if x_position == "min":
translate_x = 0
if y_position == "min":
translate_y = 0
if x_position == "mid":
translate_x = (width / scale_x - node.image_width) / 2.
if y_position == "mid":
translate_y = (height / scale_y - node.image_height) / 2.
if x_position == "max":
translate_x = width / scale_x - node.image_width
if y_position == "max":
translate_y = height / scale_y - node.image_height
return scale_x, scale_y, translate_x, translate_y
def quadratic_points(x1, y1, x2, y2, x3, y3):
"""Return the quadratic points to create quadratic curves."""
xq1 = x2 * 2 / 3 + x1 / 3
yq1 = y2 * 2 / 3 + y1 / 3
xq2 = x2 * 2 / 3 + x3 / 3
yq2 = y2 * 2 / 3 + y3 / 3
return xq1, yq1, xq2, yq2, x3, y3
def rotate(x, y, angle):
"""Rotate a point of an angle around the origin point."""
return x * cos(angle) - y * sin(angle), y * cos(angle) + x * sin(angle)
def transform(surface, string):
"""Update ``surface`` matrix according to transformation ``string``."""
if not string:
return
transformations = string.split(")")
matrix = cairo.Matrix()
for transformation in transformations:
for ttype in (
"scale", "translate", "matrix", "rotate", "skewX",
"skewY"):
if ttype in transformation:
transformation = transformation.replace(ttype, "")
transformation = transformation.replace("(", "")
transformation = normalize(transformation).strip() + " "
values = []
while transformation:
value, transformation = \
transformation.split(" ", 1)
# TODO: manage the x/y sizes here
values.append(size(surface, value))
if ttype == "matrix":
matrix = cairo.Matrix(*values).multiply(matrix)
elif ttype == "rotate":
angle = radians(float(values.pop(0)))
x, y = values or (0, 0)
matrix.translate(x, y)
matrix.rotate(angle)
matrix.translate(-x, -y)
elif ttype == "skewX":
tangent = tan(radians(float(values[0])))
matrix = \
cairo.Matrix(1, 0, tangent, 1, 0, 0).multiply(matrix)
elif ttype == "skewY":
tangent = tan(radians(float(values[0])))
matrix = \
cairo.Matrix(1, tangent, 0, 1, 0, 0).multiply(matrix)
elif ttype == "translate":
if len(values) == 1:
values += (0,)
matrix.translate(*values)
elif ttype == "scale":
if len(values) == 1:
values = 2 * values
matrix.scale(*values)
apply_matrix_transform(surface, matrix)
def apply_matrix_transform(surface, matrix):
try:
matrix.invert()
except Error:
# Matrix not invertible, clip the surface to an empty path
active_path = surface.context.copy_path()
surface.context.new_path()
surface.context.clip()
surface.context.append_path(active_path)
else:
matrix.invert()
surface.context.transform(matrix)
def urls(string):
"""Parse a comma-separated list of url() strings."""
for link in string.split(","):
link = link.strip()
if link.startswith("url"):
link = link[3:]
yield link.strip("() ")
# -*- coding: utf-8 -*-
# This file is part of CairoSVG
# Copyright © 2010-2012 Kozea
#
# This library is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# This library 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 Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with CairoSVG. If not, see <http://www.gnu.org/licenses/>.
"""
Images manager.
"""
import base64
import cairo
from io import BytesIO
try:
from urllib import urlopen, unquote
import urlparse
unquote_to_bytes = lambda data: unquote(
data.encode('ascii') if isinstance(data, unicode) else data)
except ImportError:
from urllib.request import urlopen
from urllib import parse as urlparse # Python 3
from urllib.parse import unquote_to_bytes
from .helpers import node_format, size, preserve_ratio
from ..parser import Tree
def open_data_url(url):
"""Decode URLs with the 'data' scheme. urllib can handle them
in Python 2, but that is broken in Python 3.
Inspired from Python 2.7.2’s urllib.py.
"""
# syntax of data URLs:
# dataurl := "data:" [ mediatype ] [ ";base64" ] "," data
# mediatype := [ type "/" subtype ] *( ";" parameter )
# data := *urlchar
# parameter := attribute "=" value
try:
header, data = url.split(",", 1)
except ValueError:
raise IOError("bad data URL")
header = header[5:] # len("data:") == 5
if header:
semi = header.rfind(";")
if semi >= 0 and "=" not in header[semi:]:
encoding = header[semi+1:]
else:
encoding = ""
else:
encoding = ""
data = unquote_to_bytes(data)
if encoding == "base64":
missing_padding = 4 - len(data) % 4
if missing_padding:
data += b"=" * missing_padding
return base64.decodestring(data)
return data
def image(surface, node):
"""Draw an image ``node``."""
url = node.get("{http://www.w3.org/1999/xlink}href")
if not url:
return
if url.startswith("data:"):
image_bytes = open_data_url(url)
else:
base_url = node.get("{http://www.w3.org/XML/1998/namespace}base")
if base_url:
url = urlparse.urljoin(base_url, url)
if node.url:
url = urlparse.urljoin(node.url, url)
if urlparse.urlparse(url).scheme:
input_ = urlopen(url)
else:
input_ = open(url, 'rb') # filename
image_bytes = input_.read()
if len(image_bytes) < 5:
return
x, y = size(surface, node.get("x"), "x"), size(surface, node.get("y"), "y")
width = size(surface, node.get("width"), "x")
height = size(surface, node.get("height"), "y")
surface.context.rectangle(x, y, width, height)
surface.context.clip()
if image_bytes[:4] == b"\x89PNG":
png_bytes = image_bytes
elif image_bytes[:5] == b"\x3csvg ":
surface.context.save()
surface.context.translate(x, y)
if "x" in node:
del node["x"]
if "y" in node:
del node["y"]
if "viewBox" in node:
del node["viewBox"]
tree = Tree(bytestring=image_bytes)
tree_width, tree_height, viewbox = node_format(surface, tree)
if not tree_width or not tree_height:
tree_width = tree["width"] = width
tree_height = tree["height"] = height
node.image_width = tree_width or width
node.image_height = tree_height or height
scale_x, scale_y, translate_x, translate_y = \
preserve_ratio(surface, node)
surface.set_context_size(*node_format(surface, tree))
surface.context.translate(*surface.context.get_current_point())
surface.context.scale(scale_x, scale_y)
surface.context.translate(translate_x, translate_y)
surface.draw(tree)
surface.context.restore()
# Restore twice, because draw does not restore at the end of svg tags
surface.context.restore()
return
else:
try:
from pystacia import read_blob
png_bytes = read_blob(image_bytes).get_blob('png')
except:
# No way to handle the image
return
image_surface = cairo.ImageSurface.create_from_png(BytesIO(png_bytes))
node.image_width = image_surface.get_width()
node.image_height = image_surface.get_height()
scale_x, scale_y, translate_x, translate_y = preserve_ratio(surface, node)
surface.context.rectangle(x, y, width, height)
pattern_pattern = cairo.SurfacePattern(image_surface)
surface.context.save()
surface.context.translate(*surface.context.get_current_point())
surface.context.scale(scale_x, scale_y)
surface.context.translate(translate_x, translate_y)
surface.context.set_source(pattern_pattern)
surface.context.fill()
surface.context.restore()
# -*- coding: utf-8 -*-
# This file is part of CairoSVG
# Copyright © 2010-2012 Kozea
#
# This library is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# This library 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 Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with CairoSVG. If not, see <http://www.gnu.org/licenses/>.
"""
Paths manager.
"""
from math import pi, radians
from .defs import draw_marker
from .helpers import normalize, point, point_angle, quadratic_points, rotate
from .units import size
PATH_LETTERS = "achlmqstvzACHLMQSTVZ"
PATH_TAGS = (
"circle", "ellipse", "line", "path", "polygon", "polyline", "rect")
def path(surface, node):
"""Draw a path ``node``."""
string = node.get("d", "")
if not string.strip():
# Don't draw empty paths at all
return
draw_marker(surface, node, "start")
for letter in PATH_LETTERS:
string = string.replace(letter, " %s " % letter)
last_letter = None
string = normalize(string)
while string:
string = string.strip()
if string.split(" ", 1)[0] in PATH_LETTERS:
letter, string = (string + " ").split(" ", 1)
elif letter == "M":
letter = "L"
elif letter == "m":
letter = "l"
if letter in "aA":
# Elliptic curve
x1, y1 = surface.context.get_current_point()
rx, ry, string = point(surface, string)
rotation, string = string.split(" ", 1)
rotation = radians(float(rotation))
# The large and sweep values are not always separated from the
# following values, here is the crazy parser
large, string = string[0], string[1:].strip()
while not large[-1].isdigit():
large, string = large + string[0], string[1:].strip()
sweep, string = string[0], string[1:].strip()
while not sweep[-1].isdigit():
sweep, string = sweep + string[0], string[1:].strip()
large, sweep = bool(int(large)), bool(int(sweep))
x3, y3, string = point(surface, string)
if letter == "A":
# Absolute x3 and y3, convert to relative
x3 -= x1
y3 -= y1
# rx=0 or ry=0 means straight line
if not rx or not ry:
string = "l %f %f %s" % (x3, y3, string)
continue
radii_ratio = ry / rx
# Cancel the rotation of the second point
xe, ye = rotate(x3, y3, -rotation)
ye /= radii_ratio
# Find the angle between the second point and the x axis
angle = point_angle(0, 0, xe, ye)
# Put the second point onto the x axis
xe = (xe ** 2 + ye ** 2) ** .5
ye = 0
# Update the x radius if it is too small
rx = max(rx, xe / 2)
# Find one circle centre
xc = xe / 2
yc = (rx ** 2 - xc ** 2) ** .5
# Choose between the two circles according to flags
if not (large ^ sweep):
yc = -yc
# Define the arc sweep
arc = \
surface.context.arc if sweep else surface.context.arc_negative
# Put the second point and the center back to their positions
xe, ye = rotate(xe, 0, angle)
xc, yc = rotate(xc, yc, angle)
# Find the drawing angles
angle1 = point_angle(xc, yc, 0, 0)
angle2 = point_angle(xc, yc, xe, ye)
# Store the tangent angles
node.tangents.extend((-angle1, -angle2))
# Draw the arc
surface.context.save()
surface.context.translate(x1, y1)
surface.context.rotate(rotation)
surface.context.scale(1, radii_ratio)
arc(xc, yc, rx, angle1, angle2)
surface.context.restore()
elif letter == "c":
# Relative curve
x1, y1, string = point(surface, string)
x2, y2, string = point(surface, string)
x3, y3, string = point(surface, string)
node.tangents.extend((
point_angle(x2, y2, x1, y1), point_angle(x2, y2, x3, y3)))
surface.context.rel_curve_to(x1, y1, x2, y2, x3, y3)
elif letter == "C":
# Curve
x1, y1, string = point(surface, string)
x2, y2, string = point(surface, string)
x3, y3, string = point(surface, string)
node.tangents.extend((
point_angle(x2, y2, x1, y1), point_angle(x2, y2, x3, y3)))
surface.context.curve_to(x1, y1, x2, y2, x3, y3)
elif letter == "h":
# Relative horizontal line
x, string = (string + " ").split(" ", 1)
old_x, old_y = surface.context.get_current_point()
angle = 0 if size(surface, x, "x") > 0 else pi
node.tangents.extend((-angle, angle))
surface.context.rel_line_to(size(surface, x, "x"), 0)
elif letter == "H":
# Horizontal line
x, string = (string + " ").split(" ", 1)
old_x, old_y = surface.context.get_current_point()
angle = 0 if size(surface, x, "x") > old_x else pi
node.tangents.extend((-angle, angle))
surface.context.line_to(size(surface, x, "x"), old_y)
elif letter == "l":
# Relative straight line
x, y, string = point(surface, string)
angle = point_angle(0, 0, x, y)
node.tangents.extend((-angle, angle))
surface.context.rel_line_to(x, y)
elif letter == "L":
# Straight line
x, y, string = point(surface, string)
old_x, old_y = surface.context.get_current_point()
angle = point_angle(old_x, old_y, x, y)
node.tangents.extend((-angle, angle))
surface.context.line_to(x, y)
elif letter == "m":
# Current point relative move
x, y, string = point(surface, string)
surface.context.rel_move_to(x, y)
elif letter == "M":
# Current point move
x, y, string = point(surface, string)
surface.context.move_to(x, y)
elif letter == "q":
# Relative quadratic curve
x1, y1 = 0, 0
x2, y2, string = point(surface, string)
x3, y3, string = point(surface, string)
xq1, yq1, xq2, yq2, xq3, yq3 = quadratic_points(
x1, y1, x2, y2, x3, y3)
surface.context.rel_curve_to(xq1, yq1, xq2, yq2, xq3, yq3)
node.tangents.extend((0, 0))
elif letter == "Q":
# Quadratic curve
x1, y1 = surface.context.get_current_point()
x2, y2, string = point(surface, string)
x3, y3, string = point(surface, string)
xq1, yq1, xq2, yq2, xq3, yq3 = quadratic_points(
x1, y1, x2, y2, x3, y3)
surface.context.curve_to(xq1, yq1, xq2, yq2, xq3, yq3)
node.tangents.extend((0, 0))
elif letter == "s":
# Relative smooth curve
# TODO: manage last_letter in "CS"
x1 = x3 - x2 if last_letter in "cs" else 0
y1 = y3 - y2 if last_letter in "cs" else 0
x2, y2, string = point(surface, string)
x3, y3, string = point(surface, string)
node.tangents.extend((
point_angle(x2, y2, x1, y1), point_angle(x2, y2, x3, y3)))
surface.context.rel_curve_to(x1, y1, x2, y2, x3, y3)
elif letter == "S":
# Smooth curve
# TODO: manage last_letter in "cs"
x, y = surface.context.get_current_point()
x1 = 2 * x3 - x2 if last_letter in "CS" else x
y1 = 2 * y3 - y2 if last_letter in "CS" else y
x2, y2, string = point(surface, string)
x3, y3, string = point(surface, string)
node.tangents.extend((
point_angle(x2, y2, x1, y1), point_angle(x2, y2, x3, y3)))
surface.context.curve_to(x1, y1, x2, y2, x3, y3)
elif letter == "t":
# Relative quadratic curve end
if last_letter not in "QqTt":
x2, y2, x3, y3 = 0, 0, 0, 0
elif last_letter in "QT":
x2 -= x1
y2 -= y1
x3 -= x1
y3 -= y1
x2 = x3 - x2
y2 = y3 - y2
x1, y1 = 0, 0
x3, y3, string = point(surface, string)
xq1, yq1, xq2, yq2, xq3, yq3 = quadratic_points(
x1, y1, x2, y2, x3, y3)
node.tangents.extend((0, 0))
surface.context.rel_curve_to(xq1, yq1, xq2, yq2, xq3, yq3)
elif letter == "T":
# Quadratic curve end
abs_x, abs_y = surface.context.get_current_point()
if last_letter not in "QqTt":
x2, y2, x3, y3 = abs_x, abs_y, abs_x, abs_y
elif last_letter in "qt":
x2 += x1
y2 += y1
x2 = 2 * abs_x - x2
y2 = 2 * abs_y - y2
x1, y1 = abs_x, abs_y
x3, y3, string = point(surface, string)
xq1, yq1, xq2, yq2, xq3, yq3 = quadratic_points(
x1, y1, x2, y2, x3, y3)
node.tangents.extend((0, 0))
surface.context.curve_to(xq1, yq1, xq2, yq2, xq3, yq3)
elif letter == "v":
# Relative vertical line
y, string = (string + " ").split(" ", 1)
old_x, old_y = surface.context.get_current_point()
angle = pi / 2 if size(surface, y, "y") > 0 else -pi / 2
node.tangents.extend((-angle, angle))
surface.context.rel_line_to(0, size(surface, y, "y"))
elif letter == "V":
# Vertical line
y, string = (string + " ").split(" ", 1)
old_x, old_y = surface.context.get_current_point()
angle = pi / 2 if size(surface, y, "y") > 0 else -pi / 2
node.tangents.extend((-angle, angle))
surface.context.line_to(old_x, size(surface, y, "y"))
elif letter in "zZ":
# End of path
node.tangents.extend((0, 0))
surface.context.close_path()
string = string.strip()
if letter in "hHvV":
if string.split(" ", 1)[0] not in PATH_LETTERS:
surface.context.move_to(*surface.context.get_current_point())
if string and letter not in "mMzZ":
draw_marker(surface, node, "mid")
last_letter = letter
node.tangents.append(node.tangents[-1])
draw_marker(surface, node, "end")
# -*- coding: utf-8 -*-
# This file is part of CairoSVG
# Copyright © 2010-2012 Kozea
#
# This library is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# This library 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 Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with CairoSVG. If not, see <http://www.gnu.org/licenses/>.
"""
Shapes drawers.
"""
from math import pi
from .helpers import normalize, point, size
def circle(surface, node):
"""Draw a circle ``node`` on ``surface``."""
r = size(surface, node.get("r"))
if not r:
return
surface.context.new_sub_path()
surface.context.arc(
size(surface, node.get("x"), "x") + size(surface, node.get("cx"), "x"),
size(surface, node.get("y"), "y") + size(surface, node.get("cy"), "y"),
r, 0, 2 * pi)
def ellipse(surface, node):
"""Draw an ellipse ``node`` on ``surface``."""
rx = size(surface, node.get("rx"), "x")
ry = size(surface, node.get("ry"), "y")
if not rx or not ry:
return
ratio = ry / rx
surface.context.new_sub_path()
surface.context.save()
surface.context.scale(1, ratio)
surface.context.arc(
size(surface, node.get("x"), "x") + size(surface, node.get("cx"), "x"),
(size(surface, node.get("y"), "y") +
size(surface, node.get("cy"), "y")) / ratio,
size(surface, node.get("rx"), "x"), 0, 2 * pi)
surface.context.restore()
def line(surface, node):
"""Draw a line ``node``."""
x1, y1, x2, y2 = tuple(
size(surface, node.get(position), position[0])
for position in ("x1", "y1", "x2", "y2"))
surface.context.move_to(x1, y1)
surface.context.line_to(x2, y2)
def polygon(surface, node):
"""Draw a polygon ``node`` on ``surface``."""
polyline(surface, node)
surface.context.close_path()
def polyline(surface, node):
"""Draw a polyline ``node``."""
points = normalize(node.get("points"))
if points:
x, y, points = point(surface, points)
surface.context.move_to(x, y)
while points:
x, y, points = point(surface, points)
surface.context.line_to(x, y)
def rect(surface, node):
"""Draw a rect ``node`` on ``surface``."""
# TODO: handle ry
x, y = size(surface, node.get("x"), "x"), size(surface, node.get("y"), "y")
width = size(surface, node.get("width"), "x")
height = size(surface, node.get("height"), "y")
if size(surface, node.get("rx"), "x") == 0:
surface.context.rectangle(x, y, width, height)
else:
r = size(surface, node.get("rx"), "x")
a, b, c, d = x, width + x, y, height + y
if r > width - r:
r = width / 2
surface.context.move_to(x, y + height / 2)
surface.context.arc(a + r, c + r, r, 2 * pi / 2, 3 * pi / 2)
surface.context.arc(b - r, c + r, r, 3 * pi / 2, 0 * pi / 2)
surface.context.arc(b - r, d - r, r, 0 * pi / 2, 1 * pi / 2)
surface.context.arc(a + r, d - r, r, 1 * pi / 2, 2 * pi / 2)
surface.context.close_path()
# -*- coding: utf-8 -*-
# This file is part of CairoSVG
# Copyright © 2010-2012 Kozea
#
# This library is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# This library 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 Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with CairoSVG. If not, see <http://www.gnu.org/licenses/>.
"""
Root tag drawer.
"""
from .helpers import preserve_ratio, node_format
def svg(surface, node):
"""Draw a svg ``node``."""
if node.get("preserveAspectRatio", "none") != "none":
width, height, viewbox = node_format(surface, node)
node.image_width, node.image_height = viewbox[2:]
scale_x, scale_y, translate_x, translate_y = \
preserve_ratio(surface, node)
surface.context.rectangle(0, 0, width, height)
surface.context.clip()
surface.context.translate(*surface.context.get_current_point())
surface.context.scale(scale_x, scale_y)
surface.context.translate(translate_x, translate_y)
# -*- coding: utf-8 -*-
# This file is part of CairoSVG
# Copyright © 2010-2012 Kozea
#
# This library is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# This library 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 Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with CairoSVG. If not, see <http://www.gnu.org/licenses/>.
"""
SVG tags functions.
"""
from .defs import linear_gradient, marker, pattern, radial_gradient, use
from .image import image
from .path import path
from .shapes import circle, ellipse, line, polygon, polyline, rect
from .svg import svg
from .text import text, text_path, tspan
TAGS = {
"a": tspan,
"circle": circle,
"ellipse": ellipse,
"image": image,
"line": line,
"linearGradient": linear_gradient,
"marker": marker,
"path": path,
"pattern": pattern,
"polyline": polyline,
"polygon": polygon,
"radialGradient": radial_gradient,
"rect": rect,
"svg": svg,
"text": text,
"textPath": text_path,
"tref": use,
"tspan": tspan,
"use": use}
# -*- coding: utf-8 -*-
# This file is part of CairoSVG
# Copyright © 2010-2012 Kozea
#
# This library is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# This library 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 Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with CairoSVG. If not, see <http://www.gnu.org/licenses/>.
"""
Text drawers.
"""
import cairo
from math import cos, sin
# Python 2/3 management
# pylint: disable=E0611
try:
from itertools import zip_longest
except ImportError:
from itertools import izip_longest as zip_longest
# pylint: enable=E0611
from .colors import color
from .helpers import distance, normalize, point_angle
from .units import size
def path_length(path):
"""Get the length of ``path``."""
total_length = 0
for item in path:
if item[0] == cairo.PATH_MOVE_TO:
old_point = item[1]
elif item[0] == cairo.PATH_LINE_TO:
new_point = item[1]
length = distance(
old_point[0], old_point[1], new_point[0], new_point[1])
total_length += length
old_point = new_point
return total_length
def point_following_path(path, width):
"""Get the point at ``width`` distance on ``path``."""
total_length = 0
for item in path:
if item[0] == cairo.PATH_MOVE_TO:
old_point = item[1]
elif item[0] == cairo.PATH_LINE_TO:
new_point = item[1]
length = distance(
old_point[0], old_point[1], new_point[0], new_point[1])
total_length += length
if total_length < width:
old_point = new_point
else:
length -= total_length - width
angle = point_angle(
old_point[0], old_point[1], new_point[0], new_point[1])
x = cos(angle) * length + old_point[0]
y = sin(angle) * length + old_point[1]
return x, y
def text(surface, node):
"""Draw a text ``node``."""
# Set black as default text color
if not node.get("fill"):
node["fill"] = "#000000"
# TODO: find a better way to manage white spaces in text nodes
node.text = (node.text or "").lstrip()
node.text = node.text.rstrip() + " "
# TODO: manage font variant
font_size = size(surface, node.get("font-size", "12pt"))
font_family = (node.get("font-family") or "sans-serif").split(",")[0]
font_style = getattr(
cairo, ("font_slant_%s" % node.get("font-style")).upper(),
cairo.FONT_SLANT_NORMAL)
font_weight = getattr(
cairo, ("font_weight_%s" % node.get("font-weight")).upper(),
cairo.FONT_WEIGHT_NORMAL)
surface.context.select_font_face(font_family, font_style, font_weight)
surface.context.set_font_size(font_size)
text_extents = surface.context.text_extents(node.text)
x_bearing = text_extents[0]
width = text_extents[2]
x, y = size(surface, node.get("x"), "x"), size(surface, node.get("y"), "y")
text_anchor = node.get("text-anchor")
if text_anchor == "middle":
x -= width / 2. + x_bearing
elif text_anchor == "end":
x -= width + x_bearing
surface.context.move_to(x, y)
surface.context.text_path(node.text)
# Remember the absolute cursor position
surface.cursor_position = surface.context.get_current_point()
def text_path(surface, node):
"""Draw text on a path."""
surface.context.save()
if "url(#" not in (node.get("fill") or ""):
surface.context.set_source_rgba(*color(node.get("fill")))
id_path = node.get("{http://www.w3.org/1999/xlink}href", "")
if not id_path.startswith("#"):
return
id_path = id_path[1:]
if id_path in surface.paths:
path = surface.paths.get(id_path)
else:
return
surface.draw(path, False)
cairo_path = surface.context.copy_path_flat()
surface.context.new_path()
start_offset = size(
surface, node.get("startOffset", 0), path_length(cairo_path))
surface.total_width += start_offset
x, y = point_following_path(cairo_path, surface.total_width)
string = (node.text or "").strip(" \n")
letter_spacing = size(surface, node.get("letter-spacing"))
for letter in string:
surface.total_width += (
surface.context.text_extents(letter)[4] + letter_spacing)
point_on_path = point_following_path(cairo_path, surface.total_width)
if point_on_path:
x2, y2 = point_on_path
else:
continue
angle = point_angle(x, y, x2, y2)
surface.context.save()
surface.context.translate(x, y)
surface.context.rotate(angle)
surface.context.translate(0, size(surface, node.get("y"), "y"))
surface.context.move_to(0, 0)
surface.context.show_text(letter)
surface.context.restore()
x, y = x2, y2
surface.context.restore()
# Remember the relative cursor position
surface.cursor_position = \
size(surface, node.get("x"), "x"), size(surface, node.get("y"), "y")
def tspan(surface, node):
"""Draw a tspan ``node``."""
x, y = [[i] for i in surface.cursor_position]
if "x" in node:
x = [size(surface, i, "x")
for i in normalize(node["x"]).strip().split(" ")]
if "y" in node:
y = [size(surface, i, "y")
for i in normalize(node["y"]).strip().split(" ")]
string = (node.text or "").strip()
if not string:
return
fill = node.get("fill")
positions = list(zip_longest(x, y))
letters_positions = list(zip(positions, string))
letters_positions = letters_positions[:-1] + [
(letters_positions[-1][0], string[len(letters_positions) - 1:])]
for (x, y), letters in letters_positions:
if x == None:
x = surface.cursor_position[0]
if y == None:
y = surface.cursor_position[1]
node["x"] = str(x + size(surface, node.get("dx"), "x"))
node["y"] = str(y + size(surface, node.get("dy"), "y"))
node["fill"] = fill
node.text = letters
if node.parent.tag == "text":
text(surface, node)
else:
node["x"] = str(x + size(surface, node.get("dx"), "x"))
node["y"] = str(y + size(surface, node.get("dy"), "y"))
text_path(surface, node)
if node.parent.children[-1] == node:
surface.total_width = 0
# -*- coding: utf-8 -*-
# This file is part of CairoSVG
# Copyright © 2010-2012 Kozea
#
# This library is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# This library 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 Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with CairoSVG. If not, see <http://www.gnu.org/licenses/>.
"""
Units functions.
"""
UNITS = {
"mm": 1 / 25.4,
"cm": 1 / 2.54,
"in": 1,
"pt": 1 / 72.,
"pc": 1 / 6.,
"px": None}
def size(surface, string, reference="xy"):
"""Replace a ``string`` with units by a float value.
If ``reference`` is a float, it is used as reference for percentages. If it
is ``'x'``, we use the viewport width as reference. If it is ``'y'``, we
use the viewport height as reference. If it is ``'xy'``, we use
``(viewport_width ** 2 + viewport_height ** 2) ** .5 / 2 ** .5`` as
reference.
"""
if not string:
return 0
try:
return float(string)
except ValueError:
# Not a float, try something else
pass
if "%" in string:
if reference == "x":
reference = surface.context_width or 0
elif reference == "y":
reference = surface.context_height or 0
elif reference == "xy":
reference = (
(surface.context_width ** 2 + surface.context_height ** 2)
** .5 / 2 ** .5)
return float(string.strip(" %")) * reference / 100
elif "em" in string:
return surface.font_size * float(string.strip(" em"))
elif "ex" in string:
# Assume that 1em == 2ex
return surface.font_size * float(string.strip(" ex")) / 2
for unit, coefficient in UNITS.items():
if unit in string:
number = float(string.strip(" " + unit))
return number * (surface.dpi * coefficient if coefficient else 1)
# Try to return the number at the beginning of the string
return_string = ""
while string and (string[0].isdigit() or string[0] in "+-."):
return_string += string[0]
string = string[1:]
# Unknown size or multiple sizes
return float(return_string) if return_string else 0
...@@ -21,7 +21,8 @@ import time ...@@ -21,7 +21,8 @@ import time
import zipfile import zipfile
import tempfile import tempfile
import shutil import shutil
import svg.document as wxpsvgdocument from cairosvg.surface import PNGSurface
import cStringIO
import imghdr import imghdr
class DisplayFrame(wx.Frame): class DisplayFrame(wx.Frame):
...@@ -33,6 +34,7 @@ class DisplayFrame(wx.Frame): ...@@ -33,6 +34,7 @@ class DisplayFrame(wx.Frame):
self.bitmap = wx.EmptyBitmap(*res) self.bitmap = wx.EmptyBitmap(*res)
self.bbitmap = wx.EmptyBitmap(*res) self.bbitmap = wx.EmptyBitmap(*res)
self.slicer = 'bitmap' self.slicer = 'bitmap'
self.dpi = 96
dc = wx.MemoryDC() dc = wx.MemoryDC()
dc.SelectObject(self.bbitmap) dc.SelectObject(self.bbitmap)
dc.SetBackground(wx.Brush("black")) dc.SetBackground(wx.Brush("black"))
...@@ -87,10 +89,21 @@ class DisplayFrame(wx.Frame): ...@@ -87,10 +89,21 @@ class DisplayFrame(wx.Frame):
points = [wx.Point(*map(lambda x:int(round(float(x) * self.scale)), j.strip().split())) for j in i.strip().split("M")[1].split("L")] points = [wx.Point(*map(lambda x:int(round(float(x) * self.scale)), j.strip().split())) for j in i.strip().split("M")[1].split("L")]
dc.DrawPolygon(points, self.size[0] / 2, self.size[1] / 2) dc.DrawPolygon(points, self.size[0] / 2, self.size[1] / 2)
elif self.slicer == 'Slic3r': elif self.slicer == 'Slic3r':
gc = wx.GraphicsContext_Create(dc)
gc.Translate(*self.offset) if int(self.scale) != 1:
gc.Scale(self.scale, self.scale) height = float(image.get('height').replace('m',''))
wxpsvgdocument.SVGDocument(image).render(gc) width = float(image.get('width').replace('m',''))
image.set('height', str(height*self.scale) + 'mm')
image.set('width', str(width*self.scale) + 'mm')
image.set('viewBox', '0 0 ' + str(height*self.scale) + ' ' + str(width*self.scale))
g = image.find("{http://www.w3.org/2000/svg}g")
g.set('transform', 'scale('+str(self.scale)+')')
stream = cStringIO.StringIO(PNGSurface.convert(dpi=self.dpi, bytestring=xml.etree.ElementTree.tostring(image)))
image = wx.ImageFromStream(stream)
dc.DrawBitmap(wx.BitmapFromImage(image), self.offset[0], -self.offset[1], True)
elif self.slicer == 'bitmap': elif self.slicer == 'bitmap':
if isinstance(image, str): if isinstance(image, str):
image = wx.Image(image) image = wx.Image(image)
...@@ -144,7 +157,7 @@ class DisplayFrame(wx.Frame): ...@@ -144,7 +157,7 @@ class DisplayFrame(wx.Frame):
wx.CallAfter(self.pic.Hide) wx.CallAfter(self.pic.Hide)
wx.CallAfter(self.Refresh) wx.CallAfter(self.Refresh)
def present(self, layers, interval=0.5, pause=0.2, thickness=0.4, scale=20, size=(1024, 768), offset=(0, 0)): def present(self, layers, interval=0.5, pause=0.2, thickness=0.4, scale=1, size=(1024, 768), offset=(0, 0)):
wx.CallAfter(self.pic.Hide) wx.CallAfter(self.pic.Hide)
wx.CallAfter(self.Refresh) wx.CallAfter(self.Refresh)
self.layers = layers self.layers = layers
...@@ -169,7 +182,7 @@ class SettingsFrame(wx.Frame): ...@@ -169,7 +182,7 @@ class SettingsFrame(wx.Frame):
right_label_X_pos = 180 right_label_X_pos = 180
right_value_X_pos = 230 right_value_X_pos = 230
self.panel = wx.Panel(self) self.panel = wx.Panel(self)
self.panel.SetBackgroundColour("red") self.panel.SetBackgroundColour("orange")
self.load_button = wx.Button(self.panel, -1, "Load", pos=(0, 0)) self.load_button = wx.Button(self.panel, -1, "Load", pos=(0, 0))
self.load_button.Bind(wx.EVT_BUTTON, self.load_file) self.load_button.Bind(wx.EVT_BUTTON, self.load_file)
...@@ -264,8 +277,8 @@ class SettingsFrame(wx.Frame): ...@@ -264,8 +277,8 @@ class SettingsFrame(wx.Frame):
zdiff = 0 zdiff = 0
ol = [] ol = []
if (slicer == 'Slic3r'): if (slicer == 'Slic3r'):
height = et.getroot().get('height') height = et.getroot().get('height').replace('m','')
width = et.getroot().get('width') width = et.getroot().get('width').replace('m','')
for i in et.findall("{http://www.w3.org/2000/svg}g"): for i in et.findall("{http://www.w3.org/2000/svg}g"):
z = float(i.get('{http://slic3r.org/namespaces/slic3r}z')) z = float(i.get('{http://slic3r.org/namespaces/slic3r}z'))
...@@ -276,6 +289,8 @@ class SettingsFrame(wx.Frame): ...@@ -276,6 +289,8 @@ class SettingsFrame(wx.Frame):
svgSnippet.set('height', height + 'mm') svgSnippet.set('height', height + 'mm')
svgSnippet.set('width', width + 'mm') svgSnippet.set('width', width + 'mm')
svgSnippet.set('viewBox', '0 0 ' + height + ' ' + width)
svgSnippet.set('style','background-color:black')
svgSnippet.append(i) svgSnippet.append(i)
ol += [svgSnippet] ol += [svgSnippet]
...@@ -447,6 +462,13 @@ class SettingsFrame(wx.Frame): ...@@ -447,6 +462,13 @@ class SettingsFrame(wx.Frame):
self.display_frame.resize((float(self.X.GetValue()), float(self.Y.GetValue()))) self.display_frame.resize((float(self.X.GetValue()), float(self.Y.GetValue())))
self.start_calibrate(event) self.start_calibrate(event)
def get_dpi(self):
resolution_x_pixels = int(self.X.GetValue())
projected_x_mm = float(self.projected_X_mm.GetValue())
projected_x_inches = projected_x_mm / 25.4
return resolution_x_pixels / projected_x_inches
def start_present(self, event): def start_present(self, event):
if not hasattr(self, "layers"): if not hasattr(self, "layers"):
print "No model loaded!" print "No model loaded!"
...@@ -457,6 +479,7 @@ class SettingsFrame(wx.Frame): ...@@ -457,6 +479,7 @@ class SettingsFrame(wx.Frame):
if (self.fullscreen.GetValue()): if (self.fullscreen.GetValue()):
self.display_frame.ShowFullScreen(1) self.display_frame.ShowFullScreen(1)
self.display_frame.slicer = self.layers[2] self.display_frame.slicer = self.layers[2]
self.display_frame.dpi = self.get_dpi()
self.display_frame.present(self.layers[0][:], self.display_frame.present(self.layers[0][:],
thickness=float(self.thickness.GetValue()), thickness=float(self.thickness.GetValue()),
interval=float(self.interval.GetValue()), interval=float(self.interval.GetValue()),
...@@ -491,6 +514,7 @@ class SettingsFrame(wx.Frame): ...@@ -491,6 +514,7 @@ class SettingsFrame(wx.Frame):
self.display_frame.scale = float(self.scale.GetValue()) self.display_frame.scale = float(self.scale.GetValue())
self.display_frame.slicer = self.layers[2] self.display_frame.slicer = self.layers[2]
self.display_frame.dpi = self.get_dpi()
self.display_frame.draw_layer(self.layers[0][0]) self.display_frame.draw_layer(self.layers[0][0])
self.calibrate.SetValue(False) self.calibrate.SetValue(False)
self.bounding_box.SetValue(False) self.bounding_box.SetValue(False)
......
"""
"""
import wx
def AddEllipticalArc(self, x, y, w, h, startAngle, endAngle, clockwise=False):
""" Draws an arc of an ellipse within bounding rect (x,y,w,h)
from startArc to endArc (in radians, relative to the horizontal line of the eclipse)"""
if True:
import warnings
warnings.warn("elliptical arcs are not supported")
w = w/2.0
h = h/2.0
self.AddArc(x+w, y+h, ((w+h)/2), startAngle, endAngle, clockwise)
return
else:
#implement in terms of AddArc by applying a transformation matrix
#Sigh this can't work, still need to patch wx to allow
#either a) AddPath that's not a closed path or
#b) allow pushing and popping of states on a path, not just on a context
#a) is possible in GDI+, need to investigate other renderers.
#b) is possible in Quartz and Cairo, but not in GDI+. It could
#possibly be simulated by combining the current transform with option a.
mtx = wx.GraphicsRenderer_GetDefaultRenderer().CreateMatrix()
path = wx.GraphicsRenderer_GetDefaultRenderer().CreatePath()
mtx.Translate(x+(w/2.0), y+(h/2.0))
mtx.Scale(w/2.0, y/2.0)
path.AddArc(0, 0, 1, startAngle, endAngle, clockwise)
path.Transform(mtx)
self.AddPath(path)
self.MoveToPoint(path.GetCurrentPoint())
self.CloseSubpath()
if not hasattr(wx.GraphicsPath, "AddEllipticalArc"):
wx.GraphicsPath.AddEllipticalArc = AddEllipticalArc
del AddEllipticalArc
\ No newline at end of file
"""
Parsers for specific attributes
"""
import urlparse
from pyparsing import (Literal,
Optional, oneOf, Group, StringEnd, Combine, Word, alphas, hexnums,
CaselessLiteral, SkipTo
)
from css.colour import colourValue
import string
##Paint values
none = CaselessLiteral("none").setParseAction(lambda t: ["NONE", ()])
currentColor = CaselessLiteral("currentColor").setParseAction(lambda t: ["CURRENTCOLOR", ()])
def parsePossibleURL(t):
possibleURL, fallback = t[0]
return [urlparse.urlsplit(possibleURL), fallback]
#Normal color declaration
colorDeclaration = none | currentColor | colourValue
urlEnd = (
Literal(")").suppress() +
Optional(Group(colorDeclaration), default=()) +
StringEnd()
)
url = (
CaselessLiteral("URL")
+
Literal("(").suppress()+
Group(SkipTo(urlEnd, include=True).setParseAction(parsePossibleURL))
)
#paint value will parse into a (type, details) tuple.
#For none and currentColor, the details tuple will be the empty tuple
#for CSS color declarations, it will be (type, (R,G,B))
#for URLs, it will be ("URL", ((url tuple), fallback))
#The url tuple will be as returned by urlparse.urlsplit, and can be
#an empty tuple if the parser has an error
#The fallback will be another (type, details) tuple as a parsed
#colorDeclaration, but may be the empty tuple if it is not present
paintValue = url | colorDeclaration
from __future__ import absolute_import
from .transform import transformList
from .inline import inlineStyle
""" CSS at-rules"""
from pyparsing import Literal, Combine
from .identifier import identifier
atkeyword = Combine(Literal("@") + identifier)
\ No newline at end of file
"""
CSS blocks
"""
from pyparsing import nestedExpr
block = nestedExpr(opener="{", closer="}")
"""
Parsing for CSS colour values.
Supported formats:
hex literal short: #fff
hex literal long: #fafafa
rgb bytes: rgb(255,100,0)
rgb percent: rgb(100%,100%,0%)
named color: black
"""
import wx
import string
import urlparse
from pyparsing import nums, Literal, Optional, oneOf, Group, StringEnd, Combine, Word, alphas, hexnums
from ..pathdata import number, sign
number = number.copy()
integerConstant = Word(nums+"+-").setParseAction(lambda t:int(t[0]))
#rgb format parser
comma = Literal(",").suppress()
def clampColourByte(val):
val = int(val)
return min(max(0,val), 255)
def clampColourPerc(val):
val = float(val)
return min(max(0,val), 100)
def parseColorPerc(token):
val = token[0]
val = clampColourPerc(val)
#normalize to bytes
return int(255 * (val / 100.0))
colorByte = Optional(sign) + integerConstant.setParseAction(lambda t: clampColourByte(t[0]))
colorPerc = number.setParseAction(parseColorPerc) + Literal("%").suppress()
rgb = (
Literal("rgb(").setParseAction(lambda t: "RGB") +
(
#integer constants, ie 255,255,255
Group(colorByte + comma + colorByte + comma + colorByte) ^
#percentage values, ie 100%, 50%
Group(colorPerc + comma + colorPerc + comma + colorPerc)
)
+
Literal(")").suppress() + StringEnd()
)
def parseShortHex(t):
return tuple(int(x*2, 16) for x in t[0])
doubleHex = Word(hexnums, exact=2).setParseAction(lambda t: int(t[0], 16))
hexLiteral = (Literal("#").setParseAction(lambda t: "RGB") +
(
Group(doubleHex + doubleHex + doubleHex) |
Word(hexnums, exact=3).setParseAction(parseShortHex)
) + StringEnd()
)
def parseNamedColour(t):
try:
return ["RGB", NamedColours[t[0].lower()]]
except KeyError:
return ["RGB", (0,0,0)]
namedColour = Word(alphas).setParseAction(parseNamedColour)
colourValue = rgb | hexLiteral | namedColour
##constants
NamedColours = {
#~ #html named colours
#~ "black":(0,0,0),
#~ "silver": (0xc0, 0xc0, 0xc0, 255),
#~ "gray": (0x80, 0x80, 0x80),
#~ "white":(255,255,255),
#~ "maroon":(0x80, 0, 0),
#~ "red":(0xff, 0, 0),
#~ "purple":(0x80, 0, 0x80),
#~ "fuchsia":(0xff, 0, 0xff),
#~ "green": (0, 0x80, 0),
#~ "lime": (0, 0xff, 0),
#~ "olive": (0x80, 0x80, 00),
#~ "yellow":(0xff, 0xff, 00),
#~ "navy": (0, 0, 0x80),
#~ "blue": (0, 0, 0xff),
#~ "teal": (0, 0x80, 0x80),
#~ "aqua": (0, 0xff, 0xff),
#expanded named colors from SVG spc
'aliceblue' : (240, 248, 255) ,
'antiquewhite' : (250, 235, 215) ,
'aqua' : ( 0, 255, 255) ,
'aquamarine' : (127, 255, 212) ,
'azure' : (240, 255, 255) ,
'beige' : (245, 245, 220) ,
'bisque' : (255, 228, 196) ,
'black' : ( 0, 0, 0) ,
'blanchedalmond' : (255, 235, 205) ,
'blue' : ( 0, 0, 255) ,
'blueviolet' : (138, 43, 226) ,
'brown' : (165, 42, 42) ,
'burlywood' : (222, 184, 135) ,
'cadetblue' : ( 95, 158, 160) ,
'chartreuse' : (127, 255, 0) ,
'chocolate' : (210, 105, 30) ,
'coral' : (255, 127, 80) ,
'cornflowerblue' : (100, 149, 237) ,
'cornsilk' : (255, 248, 220) ,
'crimson' : (220, 20, 60) ,
'cyan' : ( 0, 255, 255) ,
'darkblue' : ( 0, 0, 139) ,
'darkcyan' : ( 0, 139, 139) ,
'darkgoldenrod' : (184, 134, 11) ,
'darkgray' : (169, 169, 169) ,
'darkgreen' : ( 0, 100, 0) ,
'darkgrey' : (169, 169, 169) ,
'darkkhaki' : (189, 183, 107) ,
'darkmagenta' : (139, 0, 139) ,
'darkolivegreen' : ( 85, 107, 47) ,
'darkorange' : (255, 140, 0) ,
'darkorchid' : (153, 50, 204) ,
'darkred' : (139, 0, 0) ,
'darksalmon' : (233, 150, 122) ,
'darkseagreen' : (143, 188, 143) ,
'darkslateblue' : ( 72, 61, 139) ,
'darkslategray' : ( 47, 79, 79) ,
'darkslategrey' : ( 47, 79, 79) ,
'darkturquoise' : ( 0, 206, 209) ,
'darkviolet' : (148, 0, 211) ,
'deeppink' : (255, 20, 147) ,
'deepskyblue' : ( 0, 191, 255) ,
'dimgray' : (105, 105, 105) ,
'dimgrey' : (105, 105, 105) ,
'dodgerblue' : ( 30, 144, 255) ,
'firebrick' : (178, 34, 34) ,
'floralwhite' : (255, 250, 240) ,
'forestgreen' : ( 34, 139, 34) ,
'fuchsia' : (255, 0, 255) ,
'gainsboro' : (220, 220, 220) ,
'ghostwhite' : (248, 248, 255) ,
'gold' : (255, 215, 0) ,
'goldenrod' : (218, 165, 32) ,
'gray' : (128, 128, 128) ,
'grey' : (128, 128, 128) ,
'green' : ( 0, 128, 0) ,
'greenyellow' : (173, 255, 47) ,
'honeydew' : (240, 255, 240) ,
'hotpink' : (255, 105, 180) ,
'indianred' : (205, 92, 92) ,
'indigo' : ( 75, 0, 130) ,
'ivory' : (255, 255, 240) ,
'khaki' : (240, 230, 140) ,
'lavender' : (230, 230, 250) ,
'lavenderblush' : (255, 240, 245) ,
'lawngreen' : (124, 252, 0) ,
'lemonchiffon' : (255, 250, 205) ,
'lightblue' : (173, 216, 230) ,
'lightcoral' : (240, 128, 128) ,
'lightcyan' : (224, 255, 255) ,
'lightgoldenrodyellow' : (250, 250, 210) ,
'lightgray' : (211, 211, 211) ,
'lightgreen' : (144, 238, 144) ,
'lightgrey' : (211, 211, 211) ,
'lightpink' : (255, 182, 193) ,
'lightsalmon' : (255, 160, 122) ,
'lightseagreen' : ( 32, 178, 170) ,
'lightskyblue' : (135, 206, 250) ,
'lightslategray' : (119, 136, 153) ,
'lightslategrey' : (119, 136, 153) ,
'lightsteelblue' : (176, 196, 222) ,
'lightyellow' : (255, 255, 224) ,
'lime' : ( 0, 255, 0) ,
'limegreen' : ( 50, 205, 50) ,
'linen' : (250, 240, 230) ,
'magenta' : (255, 0, 255) ,
'maroon' : (128, 0, 0) ,
'mediumaquamarine' : (102, 205, 170) ,
'mediumblue' : ( 0, 0, 205) ,
'mediumorchid' : (186, 85, 211) ,
'mediumpurple' : (147, 112, 219) ,
'mediumseagreen' : ( 60, 179, 113) ,
'mediumslateblue' : (123, 104, 238) ,
'mediumspringgreen' : ( 0, 250, 154) ,
'mediumturquoise' : ( 72, 209, 204) ,
'mediumvioletred' : (199, 21, 133) ,
'midnightblue' : ( 25, 25, 112) ,
'mintcream' : (245, 255, 250) ,
'mistyrose' : (255, 228, 225) ,
'moccasin' : (255, 228, 181) ,
'navajowhite' : (255, 222, 173) ,
'navy' : ( 0, 0, 128) ,
'oldlace' : (253, 245, 230) ,
'olive' : (128, 128, 0) ,
'olivedrab' : (107, 142, 35) ,
'orange' : (255, 165, 0) ,
'orangered' : (255, 69, 0) ,
'orchid' : (218, 112, 214) ,
'palegoldenrod' : (238, 232, 170) ,
'palegreen' : (152, 251, 152) ,
'paleturquoise' : (175, 238, 238) ,
'palevioletred' : (219, 112, 147) ,
'papayawhip' : (255, 239, 213) ,
'peachpuff' : (255, 218, 185) ,
'peru' : (205, 133, 63) ,
'pink' : (255, 192, 203) ,
'plum' : (221, 160, 221) ,
'powderblue' : (176, 224, 230) ,
'purple' : (128, 0, 128) ,
'red' : (255, 0, 0) ,
'rosybrown' : (188, 143, 143) ,
'royalblue' : ( 65, 105, 225) ,
'saddlebrown' : (139, 69, 19) ,
'salmon' : (250, 128, 114) ,
'sandybrown' : (244, 164, 96) ,
'seagreen' : ( 46, 139, 87) ,
'seashell' : (255, 245, 238) ,
'sienna' : (160, 82, 45) ,
'silver' : (192, 192, 192) ,
'skyblue' : (135, 206, 235) ,
'slateblue' : (106, 90, 205) ,
'slategray' : (112, 128, 144) ,
'slategrey' : (112, 128, 144) ,
'snow' : (255, 250, 250) ,
'springgreen' : ( 0, 255, 127) ,
'steelblue' : ( 70, 130, 180) ,
'tan' : (210, 180, 140) ,
'teal' : ( 0, 128, 128) ,
'thistle' : (216, 191, 216) ,
'tomato' : (255, 99, 71) ,
'turquoise' : ( 64, 224, 208) ,
'violet' : (238, 130, 238) ,
'wheat' : (245, 222, 179) ,
'white' : (255, 255, 255) ,
'whitesmoke' : (245, 245, 245) ,
'yellow' : (255, 255, 0) ,
'yellowgreen' : (154, 205, 50) ,
}
def fillCSS2SystemColours():
#The system colours require a wxApp to be present to retrieve,
#so if you wnat support for them you'll need
#to call this function after your wxApp instance starts
systemColors = {
"ActiveBorder": wx.SYS_COLOUR_ACTIVEBORDER,
"ActiveCaption": wx.SYS_COLOUR_ACTIVECAPTION,
"AppWorkspace": wx.SYS_COLOUR_APPWORKSPACE,
"Background": wx.SYS_COLOUR_BACKGROUND,
"ButtonFace": wx.SYS_COLOUR_BTNFACE,
"ButtonHighlight": wx.SYS_COLOUR_BTNHIGHLIGHT,
"ButtonShadow": wx.SYS_COLOUR_BTNSHADOW,
"ButtonText": wx.SYS_COLOUR_BTNTEXT,
"CaptionText": wx.SYS_COLOUR_CAPTIONTEXT,
"GrayText": wx.SYS_COLOUR_GRAYTEXT,
"Highlight": wx.SYS_COLOUR_HIGHLIGHT,
"HighlightText": wx.SYS_COLOUR_HIGHLIGHTTEXT,
"InactiveBorder": wx.SYS_COLOUR_INACTIVEBORDER,
"InactiveCaption": wx.SYS_COLOUR_INACTIVECAPTION,
"InfoBackground": wx.SYS_COLOUR_INFOBK,
"InfoText": wx.SYS_COLOUR_INFOTEXT,
"Menu": wx.SYS_COLOUR_MENU,
"MenuText": wx.SYS_COLOUR_MENUTEXT,
"Scrollbar": wx.SYS_COLOUR_SCROLLBAR,
"ThreeDDarkShadow": wx.SYS_COLOUR_3DDKSHADOW,
"ThreeDFace": wx.SYS_COLOUR_3DFACE,
"ThreeDHighlight": wx.SYS_COLOUR_3DHIGHLIGHT,
"ThreeDLightShadow": wx.SYS_COLOUR_3DLIGHT,
"ThreeDShadow": wx.SYS_COLOUR_3DSHADOW,
"Window": wx.SYS_COLOUR_WINDOW,
"WindowFrame": wx.SYS_COLOUR_WINDOWFRAME,
"WindowText": wx.SYS_COLOUR_WINDOWTEXT
}
NamedColours.update(
#strip the alpha from the system colors. Is this really what we want to do?
(k.lower(), wx.SystemSettings.GetColour(v)[:3]) for (k,v) in systemColors.iteritems()
)
\ No newline at end of file
""" Parse CSS identifiers. More complicated than it sounds"""
from pyparsing import Word, Literal, Regex, Combine, Optional, White, oneOf, ZeroOrMore
import string
import re
class White(White):
""" Customize whitespace to match the CSS spec values"""
def __init__(self, ws=" \t\r\n\f", min=1, max=0, exact=0):
super(White, self).__init__(ws, min, max, exact)
escaped = (
Literal("\\").suppress() +
#chr(20)-chr(126) + chr(128)-unichr(sys.maxunicode)
Regex(u"[\u0020-\u007e\u0080-\uffff]", re.IGNORECASE)
)
def convertToUnicode(t):
return unichr(int(t[0], 16))
hex_unicode = (
Literal("\\").suppress() +
Regex("[0-9a-f]{1,6}", re.IGNORECASE) +
Optional(White(exact=1)).suppress()
).setParseAction(convertToUnicode)
escape = hex_unicode | escaped
#any unicode literal outside the 0-127 ascii range
nonascii = Regex(u"[^\u0000-\u007f]")
#single character for starting an identifier.
nmstart = Regex(u"[A-Z]", re.IGNORECASE) | nonascii | escape
nmchar = Regex(u"[0-9A-Z-]", re.IGNORECASE) | nonascii | escape
identifier = Combine(nmstart + ZeroOrMore(nmchar))
\ No newline at end of file
""" Parser for inline CSS in style attributes """
def inlineStyle(styleString):
if not styleString:
return {}
styles = styleString.split(";")
rv = dict(style.split(":") for style in styles if len(style) != 0)
return rv
\ No newline at end of file
"""
Parsing for CSS and CSS-style values, such as transform and filter attributes.
"""
from pyparsing import (Literal, Word, CaselessLiteral,
Optional, Combine, Forward, ZeroOrMore, nums, oneOf, Group, delimitedList)
#some shared definitions from pathdata
from ..pathdata import number, maybeComma
paren = Literal("(").suppress()
cparen = Literal(")").suppress()
def Parenthised(exp):
return Group(paren + exp + cparen)
skewY = Literal("skewY") + Parenthised(number)
skewX = Literal("skewX") + Parenthised(number)
rotate = Literal("rotate") + Parenthised(
number + Optional(maybeComma + number + maybeComma + number)
)
scale = Literal("scale") + Parenthised(
number + Optional(maybeComma + number)
)
translate = Literal("translate") + Parenthised(
number + Optional(maybeComma + number)
)
matrix = Literal("matrix") + Parenthised(
#there's got to be a better way to write this
number + maybeComma +
number + maybeComma +
number + maybeComma +
number + maybeComma +
number + maybeComma +
number
)
transform = (skewY | skewX | rotate | scale | translate | matrix)
transformList = delimitedList(Group(transform), delim=maybeComma)
if __name__ == '__main__':
from tests.test_css import *
unittest.main()
\ No newline at end of file
"""
Parser for various kinds of CSS values as per CSS2 spec section 4.3
"""
from pyparsing import Word, Combine, Optional, Literal, oneOf, CaselessLiteral, StringEnd
def asInt(s,l,t):
return int(t[0])
def asFloat(s,l,t):
return float(t[0])
def asFloatOrInt(s,l,t):
""" Return an int if possible, otherwise a float"""
v = t[0]
try:
return int(v)
except ValueError:
return float(v)
integer = Word("0123456789").setParseAction(asInt)
number = Combine(
Optional(Word("0123456789")) + Literal(".") + Word("01234567890")
| integer
)
number.setName('number')
sign = oneOf("+ -")
signedNumber = Combine(Optional(sign) + number).setParseAction(asFloat)
lengthValue = Combine(Optional(sign) + number).setParseAction(asFloatOrInt)
lengthValue.setName('lengthValue')
#TODO: The physical units like in, mm
lengthUnit = oneOf(['em', 'ex', 'px', 'pt', '%'], caseless=True)
#the spec says that the unit is only optional for a 0 length, but
#there are just too many places where a default is permitted.
#TODO: Maybe should use a ctor like optional to let clients declare it?
length = lengthValue + Optional(lengthUnit, default=None) + StringEnd()
length.leaveWhitespace()
#set the parse action aftward so it doesn't "infect" the parsers that build on it
number.setParseAction(asFloat)
\ No newline at end of file
"""
SVGDocument
"""
import wx
from cStringIO import StringIO
import warnings
import math
from functools import wraps
import pathdata
import css
from svg.css.colour import colourValue
from svg.css import values
from attributes import paintValue
document = """<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="4cm" height="4cm" viewBox="0 0 400 400"
xmlns="http://www.w3.org/2000/svg" version="1.1">
<title>Example triangle01- simple example of a 'path'</title>
<desc>A path that draws a triangle</desc>
<rect x="1" y="1" width="398" height="398"
fill="none" stroke="blue" />
<path d="M 100 100 L 300 100 L 200 300 z"
fill="red" stroke="blue" stroke-width="3" />
</svg>"""
makePath = lambda: wx.GraphicsRenderer_GetDefaultRenderer().CreatePath()
def attrAsFloat(node, attr, defaultValue="0"):
val = node.get(attr, defaultValue)
#TODO: process stuff like "inherit" by walking back up the nodes
#fast path optimization - if it's a valid float, don't
#try to parse it.
try:
return float(val)
except ValueError:
return valueToPixels(val)
def valueToPixels(val, defaultUnits="px"):
#TODO manage default units
from pyparsing import ParseException
try:
val, unit = values.length.parseString(val)
except ParseException:
print "***", val
raise
#todo: unit conversion to something other than pixels
return val
def pathHandler(func):
"""decorator for methods which return a path operation
Creates the path they will fill,
and generates the path operations for the node
"""
@wraps(func)
def inner(self, node):
#brush = self.getBrushFromState()
#pen = self.getPenFromState()
#if not (brush or pen):
# return None, []
path = wx.GraphicsRenderer_GetDefaultRenderer().CreatePath()
func(self, node, path)
ops = self.generatePathOps(path)
return path, ops
return inner
class SVGDocument(object):
lastControl = None
brushCache = {}
penCache = {}
def __init__(self, element):
"""
Create an SVG document from an ElementTree node.
"""
self.handlers = {
'{http://www.w3.org/2000/svg}svg':self.addGroupToDocument,
'{http://www.w3.org/2000/svg}a':self.addGroupToDocument,
'{http://www.w3.org/2000/svg}g':self.addGroupToDocument,
'{http://www.w3.org/2000/svg}rect':self.addRectToDocument,
'{http://www.w3.org/2000/svg}circle': self.addCircleToDocument,
'{http://www.w3.org/2000/svg}ellipse': self.addEllipseToDocument,
'{http://www.w3.org/2000/svg}line': self.addLineToDocument,
'{http://www.w3.org/2000/svg}polyline': self.addPolyLineToDocument,
'{http://www.w3.org/2000/svg}polygon': self.addPolygonToDocument,
'{http://www.w3.org/2000/svg}path':self.addPathDataToDocument,
'{http://www.w3.org/2000/svg}text':self.addTextToDocument
}
assert element.tag == '{http://www.w3.org/2000/svg}svg', 'Not an SVG fragment'
self.tree = element
self.paths = {}
self.stateStack = [{}]
path, ops = self.processElement(element)
self.ops = ops
@property
def state(self):
""" Retrieve the current state, without popping"""
return self.stateStack[-1]
def processElement(self, element):
""" Process one element of the XML tree.
Returns the path representing the node,
and an operation list for drawing the node.
Parent nodes should return a path (for hittesting), but
no draw operations
"""
#copy the current state
current = dict(self.state)
current.update(element.items())
current.update(css.inlineStyle(element.get("style", "")))
self.stateStack.append(current)
handler = self.handlers.get(element.tag, lambda *any: (None, None))
path, ops = handler(element)
self.paths[element] = path
self.stateStack.pop()
return path, ops
def createTransformOpsFromNode(self, node):
""" Returns an oplist for transformations.
This applies to a node, not the current state because
the transform stack is saved in the wxGraphicsContext.
This oplist does *not* include the push/pop state commands
"""
ops = []
transform = node.get('transform')
#todo: replace this with a mapping list
if transform:
for transform, args in css.transformList.parseString(transform):
if transform == 'scale':
if len(args) == 1:
x = y = args[0]
else:
x, y = args
ops.append(
(wx.GraphicsContext.Scale, (x, y))
)
if transform == 'translate':
if len(args) == 1:
x = args[0]
y = 0
else:
x, y = args
ops.append(
(wx.GraphicsContext.Translate, (x, y))
)
if transform == 'rotate':
if len(args) == 3:
angle, cx, cy = args
angle = math.radians(angle)
ops.extend([
(wx.GraphicsContext.Translate, (cx, cy)),
(wx.GraphicsContext.Rotate, (angle,)),
(wx.GraphicsContext.Translate, (-cx, -cy)),
])
else:
angle = args[0]
angle = math.radians(angle)
ops.append(
(wx.GraphicsContext.Rotate, (angle,))
)
if transform == 'matrix':
matrix = wx.GraphicsRenderer_GetDefaultRenderer().CreateMatrix(
*args
)
ops.append(
(wx.GraphicsContext.ConcatTransform, (matrix,))
)
if transform == 'skewX':
matrix = wx.GraphicsRenderer_GetDefaultRenderer().CreateMatrix(
1,0,math.tan(math.radians(args[0])),1,0,0
)
ops.append(
(wx.GraphicsContext.ConcatTransform, (matrix,))
)
if transform == 'skewY':
matrix = wx.GraphicsRenderer_GetDefaultRenderer().CreateMatrix(
1,math.tan(math.radians(args[0])),0,1,0,0
)
ops.append(
(wx.GraphicsContext.ConcatTransform, (matrix,))
)
return ops
def addGroupToDocument(self, node):
""" For parent elements: push on a state,
then process all child elements
"""
ops = [
(wx.GraphicsContext.PushState, ())
]
path = makePath()
ops.extend(self.createTransformOpsFromNode(node))
for child in node.getchildren():
cpath, cops = self.processElement(child)
if cpath:
path.AddPath(cpath)
if cops:
ops.extend(cops)
ops.append(
(wx.GraphicsContext.PopState, ())
)
return path, ops
def getFontFromState(self):
font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT)
family = self.state.get("font-family")
if False:#family:
#print "setting font", family
font.SetFaceName(family)
size = self.state.get("font-size")
#I'm not sure if this is right or not
if size:
val, unit = values.length.parseString(size)
if '__WXMSW__' in wx.PlatformInfo:
i = int(val)
font.SetPixelSize((i, i))
else:
font.SetPointSize(int(val))
return font
def addTextToDocument(self, node):
x, y = [attrAsFloat(node, attr) for attr in ('x', 'y')]
def DoDrawText(context, text, x, y, brush=wx.NullGraphicsBrush):
#SVG spec appears to originate text from the bottom
#rather than the top as with our API. This function
#will measure and then re-orient the text as needed.
w, h = context.GetTextExtent(text)
y -= h
context.DrawText(text, x, y, brush)
font = self.getFontFromState()
brush = self.getBrushFromState()
if not (brush and brush.IsOk()):
brush = wx.BLACK_BRUSH
#normalize whitespace
#TODO: SVG probably has rules for this
text = ' '.join(node.text.split() if node.text else "")
if text is None:
return None, []
ops = [
(wx.GraphicsContext.SetFont, (font, brush.Colour)),
(DoDrawText, (text, x, y))
]
return None, ops
@pathHandler
def addRectToDocument(self, node, path):
x, y, w, h = (attrAsFloat(node, attr) for attr in ['x', 'y', 'width', 'height'])
rx = node.get('rx')
ry = node.get('ry')
if not (w and h):
path.MoveToPoint(x,y) #keep the current point correct
return
if rx or ry:
if rx and ry:
rx, ry = float(rx), float(ry)
elif rx:
rx = ry = float(rx)
elif ry:
rx = ry = float(ry)
#value clamping as per spec section 9.2
rx = min(rx, w/2)
ry = min(ry, h/2)
#origin
path.MoveToPoint(x+rx, y)
path.AddLineToPoint(x+w-rx, y)
#top right
cx = rx * 2
cy = ry * 2
path.AddEllipticalArc(
x+w-cx, y,
cx, cy,
math.radians(270), math.radians(0),
True
)
path.AddLineToPoint(x+w, y+h-ry)
#bottom right
path.AddEllipticalArc(
x+w-cx, y+h-cy,
cx, cy,
math.radians(0), math.radians(90),
True
)
path.AddLineToPoint(x+rx, y+h)
#bottom left
path.AddEllipticalArc(
x, y+h-cy,
cx, cy,
math.radians(90),
math.radians(180),
True
)
path.AddLineToPoint(x, y+ry)
#bottom right
path.AddEllipticalArc(
x, y,
cx, cy,
math.radians(180),
math.radians(270),
True
)
path.CloseSubpath()
else:
path.AddRectangle(
x, y, w, h
)
@pathHandler
def addCircleToDocument(self, node, path):
cx, cy, r = [attrAsFloat(node, attr) for attr in ('cx', 'cy', 'r')]
path.AddCircle(cx, cy, r)
@pathHandler
def addEllipseToDocument(self, node, path):
cx, cy, rx, ry = [float(node.get(attr, 0)) for attr in ('cx', 'cy', 'rx', 'ry')]
#cx, cy are centerpoint.
#rx, ry are radius.
#wxGC coords are the rect of the ellipse bounding box.
if rx <= 0 or ry <= 0:
return
x = cx - rx
y = cy - ry
path.AddEllipse(x, y, rx*2, ry*2)
@pathHandler
def addLineToDocument(self, node, path):
x1, y1, x2, y2 = [attrAsFloat(node, attr) for attr in ('x1', 'y1', 'x2', 'y2')]
path.MoveToPoint(x1, y1)
path.AddLineToPoint(x2, y2)
@pathHandler
def addPolyLineToDocument(self, node, path):
#translate to pathdata and render that
data = "M " + node.get("points")
self.addPathDataToPath(data, path)
@pathHandler
def addPolygonToDocument(self, node, path):
#translate to pathdata and render that
data = "M " + node.get("points") + " Z"
self.addPathDataToPath(data, path)
@pathHandler
def addPathDataToDocument(self, node, path):
self.addPathDataToPath(node.get('d', ''), path)
def addPathDataToPath(self, data, path):
self.lastControl = None
self.lastControlQ = None
self.firstPoints = []
def normalizeStrokes(parseResults):
""" The data comes from the parser in the
form of (command, [list of arguments]).
We translate that to [(command, args[0]), (command, args[1])]
via a generator.
M is special cased because its subsequent arguments
become linetos.
"""
for command, arguments in parseResults:
if not arguments:
yield (command, ())
else:
arguments = iter(arguments)
if command == 'm':
yield (command, arguments.next())
command = "l"
elif command == "M":
yield (command, arguments.next())
command = "L"
for arg in arguments:
yield (command, arg)
#print "data length", len(data)
import time
t = time.time()
parsed = pathdata.svg.parseString(data)
#print "parsed in", time.time()-t
for stroke in normalizeStrokes(parsed):
self.addStrokeToPath(path, stroke)
def generatePathOps(self, path):
""" Look at the current state and generate the
draw operations (fill, stroke, neither) for the path"""
ops = []
brush = self.getBrushFromState(path)
fillRule = self.state.get('fill-rule', 'nonzero')
frMap = {'nonzero':wx.WINDING_RULE, 'evenodd': wx.ODDEVEN_RULE}
fr = frMap.get(fillRule, wx.ODDEVEN_RULE)
if brush:
ops.append(
(wx.GraphicsContext.SetBrush, (brush,))
)
ops.append(
(wx.GraphicsContext.FillPath, (path, fr))
)
pen = self.getPenFromState()
if pen:
ops.append(
(wx.GraphicsContext.SetPen, (pen,))
)
ops.append(
(wx.GraphicsContext.StrokePath, (path,))
)
return ops
def getPenFromState(self):
pencolour = self.state.get('stroke', 'none')
if pencolour == 'currentColor':
pencolour = self.state.get('color', 'none')
if pencolour == 'transparent':
return wx.TRANSPARENT_PEN
if pencolour == 'none':
return wx.NullPen
type, value = colourValue.parseString(pencolour)
if type == 'URL':
warnings.warn("Color servers for stroking not implemented")
return wx.NullPen
else:
if value[:3] == (-1, -1, -1):
return wx.NullPen
pen = wx.Pen(wx.Colour(*value))
width = self.state.get('stroke-width')
if width:
width, units = values.length.parseString(width)
pen.SetWidth(width)
capmap = {
'butt':wx.CAP_BUTT,
'round':wx.CAP_ROUND,
'square':wx.CAP_PROJECTING
}
joinmap = {
'miter':wx.JOIN_MITER,
'round':wx.JOIN_ROUND,
'bevel':wx.JOIN_BEVEL
}
pen.SetCap(capmap.get(self.state.get('stroke-linecap', None), wx.CAP_BUTT))
pen.SetJoin(joinmap.get(self.state.get('stroke-linejoin', None), wx.JOIN_MITER))
return wx.GraphicsRenderer_GetDefaultRenderer().CreatePen(pen)
def getBrushFromState(self, path=None):
brushcolour = self.state.get('fill', 'black').strip()
type, details = paintValue.parseString(brushcolour)
if type == "URL":
url, fallback = details
element = self.resolveURL(url)
if element is not None:
if fallback:
type, details = fallback
else:
r, g, b, = 0, 0, 0
else:
if element.tag == '{http://www.w3.org/2000/svg}linearGradient':
box = path.GetBox()
x, y, w, h = box.Get()
return wx.GraphicsRenderer.GetDefaultRenderer().CreateLinearGradientBrush(
x,y,x+w,y+h,wx.Colour(0,0,255,128), wx.RED
)
elif element.tag == '{http://www.w3.org/2000/svg}radialGradient':
box = path.GetBox()
x, y, w, h = box.Get()
#print w
mx = wx.GraphicsRenderer.GetDefaultRenderer().CreateMatrix(x,y,w,h)
cx, cy = mx.TransformPoint(0.5, 0.5)
fx, fy = cx, cy
return wx.GraphicsRenderer.GetDefaultRenderer().CreateRadialGradientBrush(
cx,cy,
fx,fy,
(max(w,h))/2,
wx.Colour(0,0,255,128), wx.RED
)
else:
#invlid gradient specified
return wx.NullBrush
r,g,b = 0,0,0
if type == 'CURRENTCOLOR':
type, details = paintValue.parseString(self.state.get('color', 'none'))
if type == 'RGB':
r,g,b = details
elif type == "NONE":
return wx.NullBrush
opacity = self.state.get('fill-opacity', self.state.get('opacity', '1'))
opacity = float(opacity)
opacity = min(max(opacity, 0.0), 1.0)
a = 255 * opacity
#using try/except block instead of
#just setdefault because the wxBrush and wxColour would
#be created every time anyway in order to pass them,
#defeating the purpose of the cache
try:
return SVGDocument.brushCache[(r,g,b,a)]
except KeyError:
return SVGDocument.brushCache.setdefault((r,g,b,a), wx.Brush(wx.Colour(r,g,b,a)))
def resolveURL(self, urlData):
"""
Resolve a URL and return an elementTree Element from it.
Return None if unresolvable
"""
scheme, netloc, path, query, fragment = urlData
if scheme == netloc == path == '':
#horrible. There's got to be a better way?
if self.tree.get("id") == fragment:
return self.tree
else:
for child in self.tree.getiterator():
#print child.get("id")
if child.get("id") == fragment:
return child
return None
else:
return self.resolveRemoteURL(urlData)
def resolveRemoteURL(self, url):
return None
def addStrokeToPath(self, path, stroke):
""" Given a stroke from a path command
(in the form (command, arguments)) create the path
commands that represent it.
TODO: break out into (yet another) class/module,
especially so we can get O(1) dispatch on type?
"""
type, arg = stroke
relative = False
if type == type.lower():
relative = True
ox, oy = path.GetCurrentPoint().Get()
else:
ox = oy = 0
def normalizePoint(arg):
x, y = arg
return x+ox, y+oy
def reflectPoint(point, relativeTo):
x, y = point
a, b = relativeTo
return ((a*2)-x), ((b*2)-y)
type = type.upper()
if type == 'M':
pt = normalizePoint(arg)
self.firstPoints.append(pt)
path.MoveToPoint(pt)
elif type == 'L':
path.AddLineToPoint(normalizePoint(arg))
elif type == 'C':
#control1, control2, endpoint = arg
control1, control2, endpoint = map(
normalizePoint, arg
)
self.lastControl = control2
path.AddCurveToPoint(
control1,
control2,
endpoint
)
#~ cp = path.GetCurrentPoint()
#~ path.AddCircle(c1x, c1y, 5)
#~ path.AddCircle(c2x, c2y, 3)
#~ path.AddCircle(x,y, 7)
#~ path.MoveToPoint(cp)
#~ print "C", control1, control2, endpoint
elif type == 'S':
#control2, endpoint = arg
control2, endpoint = map(
normalizePoint, arg
)
if self.lastControl:
control1 = reflectPoint(self.lastControl, path.GetCurrentPoint())
else:
control1 = path.GetCurrentPoint()
#~ print "S", self.lastControl,":",control1, control2, endpoint
self.lastControl = control2
path.AddCurveToPoint(
control1,
control2,
endpoint
)
elif type == "Q":
(cx, cy), (x,y) = map(normalizePoint, arg)
self.lastControlQ = (cx, cy)
path.AddQuadCurveToPoint(cx, cy, x, y)
elif type == "T":
x, y, = normalizePoint(arg)
if self.lastControlQ:
cx, cy = reflectPoint(self.lastControlQ, path.GetCurrentPoint())
else:
cx, cy = path.GetCurrentPoint()
self.lastControlQ = (cx, cy)
path.AddQuadCurveToPoint(cx, cy, x, y)
elif type == "V":
_, y = normalizePoint((0, arg))
x, _ = path.GetCurrentPoint()
path.AddLineToPoint(x,y)
elif type == "H":
x, _ = normalizePoint((arg, 0))
_, y = path.GetCurrentPoint()
path.AddLineToPoint(x,y)
elif type == "A":
#wxGC currently only supports circular arcs,
#not eliptical ones
(
(rx, ry), #radii of ellipse
angle, #angle of rotation on the ellipse in degrees
(fa, fs), #arc and stroke angle flags
(x, y) #endpoint on the arc
) = arg
x, y = normalizePoint((x,y))
cx, cy = path.GetCurrentPoint()
if (cx, cy) == (x, y):
return #noop
if (rx == 0 or ry == 0):
#no radius is effectively a line
path.AddLineToPoint(x,y)
return
#find the center point for the ellipse
#translation via the angle
angle = angle % 360
angle = math.radians(angle)
#translated endpoint
xPrime = math.cos(angle) * ((cx-x)/2)
xPrime += math.sin(angle) * ((cx-x)/2)
yPrime = -(math.sin(angle)) * ((cy-y)/2)
yPrime += (math.cos(angle)) * ((cy-y)/2)
temp = ((rx**2) * (ry**2)) - ((rx**2) * (yPrime**2)) - ((ry**2) * (xPrime**2))
temp /= ((rx**2) * (yPrime**2)) + ((ry**2)*(xPrime**2))
temp = abs(temp)
try:
temp = math.sqrt(temp)
except ValueError:
import pdb
pdb.set_trace()
cxPrime = temp * ((rx * yPrime) / ry)
cyPrime = temp * -((ry * xPrime) / rx)
if fa == fs:
cxPrime, cyPrime = -cxPrime, -cyPrime
#reflect backwards now for the origin
cnx = math.cos(angle) * cxPrime
cnx += math.sin(angle) * cxPrime
cny = -(math.sin(angle)) * cyPrime
cny += (math.cos(angle)) * cyPrime
cnx += ((cx+x)/2.0)
cny += ((cy+y)/2.0)
#calculate the angle between the two endpoints
lastArc = wx.Point2D(x-cnx, y-cny).GetVectorAngle()
firstArc = wx.Point2D(cx-cnx, cy-cny).GetVectorAngle()
lastArc = math.radians(lastArc)
firstArc = math.radians(firstArc)
#aargh buggines.
#AddArc draws a straight line between
#the endpoints of the arc.
#putting it in a subpath makes the strokes come out
#correctly, but it still only fills the slice
#taking out the MoveToPoint fills correctly.
path.AddEllipse(cnx-rx, cny-ry, rx*2, ry*2)
path.MoveToPoint(x, y)
#~ npath = makePath()
#~ npath.AddEllipticalArc(cnx-rx, cny-ry, rx*2, ry*2, firstArc, lastArc, False)
#~ npath.MoveToPoint(x,y)
#~ path.AddPath(npath)
elif type == 'Z':
#~ Bugginess:
#~ CloseSubpath() doesn't change the
#~ current point, as SVG spec requires.
#~ However, manually moving to the endpoint afterward opens a new subpath
#~ and (apparently) messes with stroked but not filled paths.
#~ This is possibly a bug in GDI+?
#~ Manually closing the path via AddLineTo gives incorrect line join
#~ results
#~ Manually closing the path *and* calling CloseSubpath() appears
#~ to give correct results on win32
pt = self.firstPoints.pop()
path.AddLineToPoint(pt)
path.CloseSubpath()
def render(self, context):
if not hasattr(self, "ops"):
return
for op, args in self.ops:
op(context, *args)
if __name__ == '__main__':
from tests.test_document import *
unittest.main()
"""
SVG path data parser
Usage:
steps = svg.parseString(pathdata)
for command, arguments in steps:
pass
"""
from pyparsing import (ParserElement, Literal, Word, CaselessLiteral,
Optional, Combine, Forward, ZeroOrMore, nums, oneOf, Group, ParseException, OneOrMore)
#ParserElement.enablePackrat()
def Command(char):
""" Case insensitive but case preserving"""
return CaselessPreservingLiteral(char)
def Arguments(token):
return Group(token)
class CaselessPreservingLiteral(CaselessLiteral):
""" Like CaselessLiteral, but returns the match as found
instead of as defined.
"""
def __init__( self, matchString ):
super(CaselessPreservingLiteral,self).__init__( matchString.upper() )
self.name = "'%s'" % matchString
self.errmsg = "Expected " + self.name
self.myException.msg = self.errmsg
def parseImpl( self, instring, loc, doActions=True ):
test = instring[ loc:loc+self.matchLen ]
if test.upper() == self.match:
return loc+self.matchLen, test
#~ raise ParseException( instring, loc, self.errmsg )
exc = self.myException
exc.loc = loc
exc.pstr = instring
raise exc
def Sequence(token):
""" A sequence of the token"""
return OneOrMore(token+maybeComma)
digit_sequence = Word(nums)
sign = oneOf("+ -")
def convertToFloat(s, loc, toks):
try:
return float(toks[0])
except:
raise ParseException(loc, "invalid float format %s"%toks[0])
exponent = CaselessLiteral("e")+Optional(sign)+Word(nums)
#note that almost all these fields are optional,
#and this can match almost anything. We rely on Pythons built-in
#float() function to clear out invalid values - loosely matching like this
#speeds up parsing quite a lot
floatingPointConstant = Combine(
Optional(sign) +
Optional(Word(nums)) +
Optional(Literal(".") + Optional(Word(nums)))+
Optional(exponent)
)
floatingPointConstant.setParseAction(convertToFloat)
number = floatingPointConstant
#same as FP constant but don't allow a - sign
nonnegativeNumber = Combine(
Optional(Word(nums)) +
Optional(Literal(".") + Optional(Word(nums)))+
Optional(exponent)
)
nonnegativeNumber.setParseAction(convertToFloat)
coordinate = number
#comma or whitespace can seperate values all over the place in SVG
maybeComma = Optional(Literal(',')).suppress()
coordinateSequence = Sequence(coordinate)
coordinatePair = (coordinate + maybeComma + coordinate).setParseAction(lambda t: tuple(t))
coordinatePairSequence = Sequence(coordinatePair)
coordinatePairPair = coordinatePair + maybeComma + coordinatePair
coordinatePairPairSequence = Sequence(Group(coordinatePairPair))
coordinatePairTriple = coordinatePair + maybeComma + coordinatePair + maybeComma + coordinatePair
coordinatePairTripleSequence = Sequence(Group(coordinatePairTriple))
#commands
lineTo = Group(Command("L") + Arguments(coordinatePairSequence))
moveTo = Group(Command("M") + Arguments(coordinatePairSequence))
closePath = Group(Command("Z")).setParseAction(lambda t: ('Z', (None,)))
flag = oneOf("1 0").setParseAction(lambda t: bool(int((t[0]))))
arcRadius = (
nonnegativeNumber + maybeComma + #rx
nonnegativeNumber #ry
).setParseAction(lambda t: tuple(t))
arcFlags = (flag + maybeComma + flag).setParseAction(lambda t: tuple(t))
ellipticalArcArgument = Group(
arcRadius + maybeComma + #rx, ry
number + maybeComma +#rotation
arcFlags + #large-arc-flag, sweep-flag
coordinatePair #(x,y)
)
ellipticalArc = Group(Command("A") + Arguments(Sequence(ellipticalArcArgument)))
smoothQuadraticBezierCurveto = Group(Command("T") + Arguments(coordinatePairSequence))
quadraticBezierCurveto = Group(Command("Q") + Arguments(coordinatePairPairSequence))
smoothCurve = Group(Command("S") + Arguments(coordinatePairPairSequence))
curve = Group(Command("C") + Arguments(coordinatePairTripleSequence))
horizontalLine = Group(Command("H") + Arguments(coordinateSequence))
verticalLine = Group(Command("V") + Arguments(coordinateSequence))
drawToCommand = (
lineTo | moveTo | closePath | ellipticalArc | smoothQuadraticBezierCurveto |
quadraticBezierCurveto | smoothCurve | curve | horizontalLine | verticalLine
)
#~ number.debug = True
moveToDrawToCommands = moveTo + ZeroOrMore(drawToCommand)
svg = ZeroOrMore(moveToDrawToCommands)
svg.keepTabs = True
def profile():
import cProfile
p = cProfile.Profile()
p.enable()
ptest()
ptest()
ptest()
p.disable()
p.print_stats()
bpath = """M204.33 139.83 C196.33 133.33 206.68 132.82 206.58 132.58 C192.33 97.08 169.35
81.41 167.58 80.58 C162.12 78.02 159.48 78.26 160.45 76.97 C161.41 75.68 167.72 79.72 168.58
80.33 C193.83 98.33 207.58 132.33 207.58 132.33 C207.58 132.33 209.33 133.33 209.58 132.58
C219.58 103.08 239.58 87.58 246.33 81.33 C253.08 75.08 256.63 74.47 247.33 81.58 C218.58 103.58
210.34 132.23 210.83 132.33 C222.33 134.83 211.33 140.33 211.83 139.83 C214.85 136.81 214.83 145.83 214.83
145.83 C214.83 145.83 231.83 110.83 298.33 66.33 C302.43 63.59 445.83 -14.67 395.83 80.83 C393.24 85.79 375.83
105.83 375.83 105.83 C375.83 105.83 377.33 114.33 371.33 121.33 C370.3 122.53 367.83 134.33 361.83 140.83 C360.14 142.67
361.81 139.25 361.83 140.83 C362.33 170.83 337.76 170.17 339.33 170.33 C348.83 171.33 350.19 183.66 350.33 183.83 C355.83
190.33 353.83 191.83 355.83 194.83 C366.63 211.02 355.24 210.05 356.83 212.83 C360.83 219.83 355.99 222.72 357.33 224.83
C360.83 230.33 354.75 233.84 354.83 235.33 C355.33 243.83 349.67 240.73 349.83 244.33 C350.33 255.33 346.33 250.83 343.83 254.83
C336.33 266.83 333.46 262.38 332.83 263.83 C329.83 270.83 325.81 269.15 324.33 270.83 C320.83 274.83 317.33 274.83 315.83 276.33
C308.83 283.33 304.86 278.39 303.83 278.83 C287.83 285.83 280.33 280.17 277.83 280.33 C270.33 280.83 271.48 279.67 269.33 277.83
C237.83 250.83 219.33 211.83 215.83 206.83 C214.4 204.79 211.35 193.12 212.33 195.83 C214.33 201.33 213.33 250.33 207.83 250.33
C202.33 250.33 201.83 204.33 205.33 195.83 C206.43 193.16 204.4 203.72 201.79 206.83 C196.33 213.33 179.5 250.83 147.59 277.83
C145.42 279.67 146.58 280.83 138.98 280.33 C136.46 280.17 128.85 285.83 112.65 278.83 C111.61 278.39 107.58 283.33 100.49 276.33
C98.97 274.83 95.43 274.83 91.88 270.83 C90.39 269.15 86.31 270.83 83.27 263.83 C82.64 262.38 79.73 266.83 72.13 254.83 C69.6 250.83
65.54 255.33 66.05 244.33 C66.22 240.73 60.48 243.83 60.99 235.33 C61.08 233.84 54.91 230.33 58.45 224.83 C59.81 222.72 54.91 219.83
58.96 212.83 C60.57 210.05 49.04 211.02 59.97 194.83 C62 191.83 59.97 190.33 65.54 183.83 C65.69 183.66 67.06 171.33 76.69 170.33
C78.28 170.17 53.39 170.83 53.9 140.83 C53.92 139.25 55.61 142.67 53.9 140.83 C47.82 134.33 45.32 122.53 44.27 121.33 C38.19 114.33
39.71 105.83 39.71 105.83 C39.71 105.83 22.08 85.79 19.46 80.83 C-31.19 -14.67 114.07 63.59 118.22 66.33 C185.58 110.83 202 145.83
202 145.83 C202 145.83 202.36 143.28 203 141.83 C203.64 140.39 204.56 140.02 204.33 139.83 z"""
def ptest():
svg.parseString(bpath)
if __name__ == '__main__':
#~ from tests.test_pathdata import *
#~ unittest.main()
profile()
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment