Commit b0932f79 authored by Gary Hodgson's avatar Gary Hodgson Committed by Guillaume Seguin

fixed scaling bug; updated cairosvg to latest version

Conflicts:
	printrun/projectlayer.py
parent b07f7855
...@@ -27,7 +27,7 @@ import optparse ...@@ -27,7 +27,7 @@ import optparse
from . import surface from . import surface
VERSION = '0.4.4' VERSION = '1.0.dev0'
SURFACES = { SURFACES = {
'SVG': surface.SVGSurface, # Tell us if you actually use this one! 'SVG': surface.SVGSurface, # Tell us if you actually use this one!
'PNG': surface.PNGSurface, 'PNG': surface.PNGSurface,
...@@ -52,14 +52,14 @@ def main(): ...@@ -52,14 +52,14 @@ def main():
"""Entry-point of the executable.""" """Entry-point of the executable."""
# Get command-line options # Get command-line options
option_parser = optparse.OptionParser( option_parser = optparse.OptionParser(
usage = "usage: %prog filename [options]", version = VERSION) usage="usage: %prog filename [options]", version=VERSION)
option_parser.add_option( option_parser.add_option(
"-f", "--format", help = "output format") "-f", "--format", help="output format")
option_parser.add_option( option_parser.add_option(
"-d", "--dpi", help = "svg resolution", default = 96) "-d", "--dpi", help="ratio between 1in and 1px", default=96)
option_parser.add_option( option_parser.add_option(
"-o", "--output", "-o", "--output",
default = "", help = "output filename") default="", help="output filename")
options, args = option_parser.parse_args() options, args = option_parser.parse_args()
# Print help if no argument is given # Print help if no argument is given
......
...@@ -20,7 +20,7 @@ Optionally handle CSS stylesheets. ...@@ -20,7 +20,7 @@ Optionally handle CSS stylesheets.
""" """
import os
from .parser import HAS_LXML from .parser import HAS_LXML
# Detect optional depedencies # Detect optional depedencies
...@@ -53,13 +53,27 @@ def find_stylesheets(tree): ...@@ -53,13 +53,27 @@ def find_stylesheets(tree):
# TODO: support <?xml-stylesheet ... ?> # TODO: support <?xml-stylesheet ... ?>
def find_stylesheets_rules(stylesheet, url):
"""Find the rules in a stylesheet."""
for rule in stylesheet.rules:
if isinstance(rule, tinycss.css21.ImportRule):
css_path = os.path.normpath(
os.path.join(os.path.dirname(url), rule.uri))
if not os.path.exists(css_path):
continue
with open(css_path) as f:
stylesheet = tinycss.make_parser().parse_stylesheet(f.read())
for rule in find_stylesheets_rules(stylesheet, css_path):
yield rule
if not rule.at_keyword:
yield rule
def find_style_rules(tree): def find_style_rules(tree):
"""Find the style rules in ``tree``.""" """Find the style rules in ``tree``."""
for stylesheet in find_stylesheets(tree): for stylesheet in find_stylesheets(tree.xml_tree):
# TODO: warn for each stylesheet.errors # TODO: warn for each stylesheet.errors
for rule in stylesheet.rules: for rule in find_stylesheets_rules(stylesheet, tree.url):
# TODO: support @import and @media
if not rule.at_keyword:
yield rule yield rule
...@@ -95,7 +109,7 @@ def apply_stylesheets(tree): ...@@ -95,7 +109,7 @@ def apply_stylesheets(tree):
style_by_element = {} style_by_element = {}
for rule in find_style_rules(tree): for rule in find_style_rules(tree):
declarations = list(get_declarations(rule)) declarations = list(get_declarations(rule))
for element, specificity in match_selector(rule, tree): for element, specificity in match_selector(rule, tree.xml_tree):
style = style_by_element.setdefault(element, {}) style = style_by_element.setdefault(element, {})
for name, value, important in declarations: for name, value, important in declarations:
weight = important, specificity weight = important, specificity
...@@ -108,5 +122,4 @@ def apply_stylesheets(tree): ...@@ -108,5 +122,4 @@ def apply_stylesheets(tree):
for element, style in iteritems(style_by_element): for element, style in iteritems(style_by_element):
values = ["%s: %s" % (name, value) values = ["%s: %s" % (name, value)
for name, (value, weight) in iteritems(style)] for name, (value, weight) in iteritems(style)]
values.append(element.get("style", "")) element.set("_style", ";".join(values))
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/>.
"""
Helpers related to SVG conditional processing.
"""
import locale
ROOT = "http://www.w3.org/TR/SVG11/feature"
LOCALE = locale.getdefaultlocale()[0]
SUPPORTED_FEATURES = set(
ROOT + "#" + feature for feature in [
"SVG",
"SVG-static",
"CoreAttribute",
"Structure",
"BasicStructure",
"ConditionalProcessing",
"Image",
"Style",
"ViewportAttribute",
"Shape",
"BasicText",
"BasicPaintAttribute",
"OpacityAttribute",
"BasicGraphicsAttribute",
"Marker",
"Gradient",
"Pattern",
"Clip",
"BasicClip",
"Mask"
])
def has_features(features):
"""Check whether ``features`` are supported by CairoSVG."""
return SUPPORTED_FEATURES >= set(features.strip().split(" "))
def support_languages(languages):
"""Check whether one of ``languages`` is part of the user locales."""
for language in languages.split(","):
language = language.strip()
if language and LOCALE.startswith(language):
return True
return False
def match_features(node):
"""Check the node match the conditional processing attributes."""
if "requiredExtensions" in node.attrib:
return False
if not has_features(node.attrib.get("requiredFeatures", ROOT + "#SVG")):
return False
if not support_languages(node.attrib.get("systemLanguage", LOCALE)):
return False
return True
...@@ -44,10 +44,14 @@ except ImportError: ...@@ -44,10 +44,14 @@ except ImportError:
# pylint: enable=E0611,F0401,W0611 # pylint: enable=E0611,F0401,W0611
import re
import gzip import gzip
import uuid
import os.path import os.path
from .css import apply_stylesheets from .css import apply_stylesheets
from .features import match_features
from .surface.helpers import urls
# Python 2/3 compat # Python 2/3 compat
...@@ -77,9 +81,23 @@ def remove_svg_namespace(tree): ...@@ -77,9 +81,23 @@ def remove_svg_namespace(tree):
element.tag = tag[prefix_len:] element.tag = tag[prefix_len:]
def handle_white_spaces(string, preserve):
"""Handle white spaces in text nodes."""
# http://www.w3.org/TR/SVG/text.html#WhiteSpace
if not string:
return ""
if preserve:
string = re.sub("[\n\r\t]", " ", string)
else:
string = re.sub("[\n\r]", "", string)
string = re.sub("\t", " ", string)
string = re.sub(" +", " ", string)
return string
class Node(dict): class Node(dict):
"""SVG node with dict-like properties and children.""" """SVG node with dict-like properties and children."""
def __init__(self, node, parent = None): def __init__(self, node, parent=None, parent_children=False):
"""Create the Node from ElementTree ``node``, with ``parent`` Node.""" """Create the Node from ElementTree ``node``, with ``parent`` Node."""
super(Node, self).__init__() super(Node, self).__init__()
self.children = () self.children = ()
...@@ -89,27 +107,31 @@ class Node(dict): ...@@ -89,27 +107,31 @@ class Node(dict):
self.text = node.text self.text = node.text
# Inherits from parent properties # Inherits from parent properties
# TODO: drop other attributes that should not be inherited
if parent is not None: if parent is not None:
items = parent.copy() items = parent.copy()
not_inherited = ( not_inherited = (
"transform", "opacity", "style", "viewBox", "stop-color", "transform", "opacity", "style", "viewBox", "stop-color",
"stop-opacity") "stop-opacity", "width", "height", "filter", "mask",
if self.tag in ("tspan", "pattern"): "{http://www.w3.org/1999/xlink}href", "id", "x", "y")
not_inherited += ("x", "y")
for attribute in not_inherited: for attribute in not_inherited:
if attribute in items: if attribute in items:
del items[attribute] del items[attribute]
self.update(items) self.update(items)
self.url = parent.url self.url = parent.url
self.xml_tree = parent.xml_tree
self.parent = parent self.parent = parent
else:
self.url = getattr(self, "url", None)
self.parent = getattr(self, "parent", None)
self.update(dict(node.attrib.items())) self.update(dict(node.attrib.items()))
# Give an id for nodes that don't have one
if "id" not in self:
self["id"] = uuid.uuid4().hex
# Handle the CSS # Handle the CSS
style = self.pop("style", "") style = self.pop("_style", "") + ";" + self.pop("style", "").lower()
for declaration in style.split(";"): for declaration in style.split(";"):
if ":" in declaration: if ":" in declaration:
name, value = declaration.split(":", 1) name, value = declaration.split(":", 1)
...@@ -132,37 +154,97 @@ class Node(dict): ...@@ -132,37 +154,97 @@ class Node(dict):
del self[attribute] del self[attribute]
# Manage text by creating children # Manage text by creating children
if self.tag == "text" or self.tag == "textPath": if self.tag in ("text", "textPath", "a"):
self.children = self.text_children(node) self.children = self.text_children(node)
if not self.children: if parent_children:
self.children = tuple( # TODO: make children inherit attributes from their new parent
Node(child, self) for child in node self.children = parent.children
if isinstance(child.tag, basestring)) elif not self.children:
self.children = []
for child in node:
if isinstance(child.tag, basestring):
if match_features(child):
self.children.append(Node(child, self))
if self.tag == "switch":
break
def text_children(self, node): def text_children(self, node):
"""Create children and return them.""" """Create children and return them."""
children = [] children = []
space = "{http://www.w3.org/XML/1998/namespace}space"
preserve = self.get(space) == "preserve"
self.text = handle_white_spaces(node.text, preserve)
if not preserve:
self.text = self.text.lstrip(" ")
for child in node: for child in node:
children.append(Node(child, parent = self)) if child.tag == "tref":
href = child.get("{http://www.w3.org/1999/xlink}href")
tree_urls = urls(href)
url = tree_urls[0] if tree_urls else None
child_node = Tree(url=url, parent=self)
child_node.tag = "tspan"
child = child_node.xml_tree
else:
child_node = Node(child, parent=self)
child_preserve = child_node.get(space) == "preserve"
child_node.text = handle_white_spaces(child.text, child_preserve)
child_node.children = child_node.text_children(child)
children.append(child_node)
if child.tail: if child.tail:
anonymous = ElementTree.Element('tspan') anonymous = Node(ElementTree.Element("tspan"), parent=self)
anonymous.text = child.tail anonymous.text = handle_white_spaces(child.tail, preserve)
children.append(Node(anonymous, parent = self)) children.append(anonymous)
return list(children) if children:
if not children[-1].children:
if children[-1].get(space) != "preserve":
children[-1].text = children[-1].text.rstrip(" ")
else:
if not preserve:
self.text = self.text.rstrip(" ")
return children
class Tree(Node): class Tree(Node):
"""SVG tree.""" """SVG tree."""
def __new__(cls, **kwargs):
tree_cache = kwargs.get("tree_cache")
if tree_cache:
if "url" in kwargs:
url_parts = kwargs["url"].split("#", 1)
if len(url_parts) == 2:
url, element_id = url_parts
else:
url, element_id = url_parts[0], None
parent = kwargs.get("parent")
if parent and not url:
url = parent.url
if (url, element_id) in tree_cache:
cached_tree = tree_cache[(url, element_id)]
new_tree = Node(cached_tree.xml_tree, parent)
new_tree.xml_tree = cached_tree.xml_tree
new_tree.url = url
new_tree.tag = cached_tree.tag
new_tree.root = True
return new_tree
return dict.__new__(cls)
def __init__(self, **kwargs): def __init__(self, **kwargs):
"""Create the Tree from SVG ``text``.""" """Create the Tree from SVG ``text``."""
if getattr(self, "xml_tree", None) is not None:
# The tree has already been parsed
return
# Make the parameters keyword-only: # Make the parameters keyword-only:
bytestring = kwargs.pop('bytestring', None) bytestring = kwargs.pop("bytestring", None)
file_obj = kwargs.pop('file_obj', None) file_obj = kwargs.pop("file_obj", None)
url = kwargs.pop('url', None) url = kwargs.pop("url", None)
parent = kwargs.pop('parent', None) parent = kwargs.pop("parent", None)
parent_children = kwargs.pop("parent_children", None)
tree_cache = kwargs.pop("tree_cache", None)
if bytestring is not None: if bytestring is not None:
tree = ElementTree.fromstring(bytestring) tree = ElementTree.fromstring(bytestring)
...@@ -172,7 +254,7 @@ class Tree(Node): ...@@ -172,7 +254,7 @@ class Tree(Node):
if url: if url:
self.url = url self.url = url
else: else:
self.url = getattr(file_obj, 'name', None) self.url = getattr(file_obj, "name", None)
elif url is not None: elif url is not None:
if "#" in url: if "#" in url:
url, element_id = url.split("#", 1) url, element_id = url.split("#", 1)
...@@ -196,7 +278,7 @@ class Tree(Node): ...@@ -196,7 +278,7 @@ class Tree(Node):
tree = parent.xml_tree tree = parent.xml_tree
if element_id: if element_id:
iterator = ( iterator = (
tree.iter() if hasattr(tree, 'iter') tree.iter() if hasattr(tree, "iter")
else tree.getiterator()) else tree.getiterator())
for element in iterator: for element in iterator:
if element.get("id") == element_id: if element.get("id") == element_id:
...@@ -207,9 +289,11 @@ class Tree(Node): ...@@ -207,9 +289,11 @@ class Tree(Node):
'No tag with id="%s" found.' % element_id) 'No tag with id="%s" found.' % element_id)
else: else:
raise TypeError( raise TypeError(
'No input. Use one of bytestring, file_obj or url.') "No input. Use one of bytestring, file_obj or url.")
remove_svg_namespace(tree) remove_svg_namespace(tree)
apply_stylesheets(tree)
self.xml_tree = tree self.xml_tree = tree
super(Tree, self).__init__(tree, parent) apply_stylesheets(self)
super(Tree, self).__init__(tree, parent, parent_children)
self.root = True self.root = True
if tree_cache is not None and url is not None:
tree_cache[(self.url, self["id"])] = self
...@@ -20,15 +20,20 @@ Cairo surface creators. ...@@ -20,15 +20,20 @@ Cairo surface creators.
""" """
import cairo
import io import io
try:
import cairocffi as cairo
except ImportError:
import cairo # pycairo
from ..parser import Tree from ..parser import Tree
from .colors import color from .colors import color
from .defs import gradient_or_pattern, parse_def from .defs import (
apply_filter_after, apply_filter_before, gradient_or_pattern, parse_def,
paint_mask)
from .helpers import ( from .helpers import (
node_format, transform, normalize, filter_fill_or_stroke, node_format, transform, normalize, paint, urls, apply_matrix_transform,
apply_matrix_transform, PointError) PointError, rect)
from .path import PATH_TAGS from .path import PATH_TAGS
from .tags import TAGS from .tags import TAGS
from .units import size from .units import size
...@@ -50,7 +55,7 @@ class Surface(object): ...@@ -50,7 +55,7 @@ class Surface(object):
surface_class = None surface_class = None
@classmethod @classmethod
def convert(cls, bytestring = None, **kwargs): def convert(cls, bytestring=None, **kwargs):
"""Convert a SVG document to the format for this class. """Convert a SVG document to the format for this class.
Specify the input by passing one of these: Specify the input by passing one of these:
...@@ -80,7 +85,7 @@ class Surface(object): ...@@ -80,7 +85,7 @@ class Surface(object):
if write_to is None: if write_to is None:
return output.getvalue() return output.getvalue()
def __init__(self, tree, output, dpi): def __init__(self, tree, output, dpi, parent_surface=None):
"""Create the surface from a filename or a file-like object. """Create the surface from a filename or a file-like object.
The rendered content is written to ``output`` which can be a filename, The rendered content is written to ``output`` which can be a filename,
...@@ -95,10 +100,21 @@ class Surface(object): ...@@ -95,10 +100,21 @@ class Surface(object):
self.context_width, self.context_height = None, None self.context_width, self.context_height = None, None
self.cursor_position = 0, 0 self.cursor_position = 0, 0
self.total_width = 0 self.total_width = 0
self.tree_cache = {(tree.url, tree["id"]): tree}
if parent_surface:
self.markers = parent_surface.markers
self.gradients = parent_surface.gradients
self.patterns = parent_surface.patterns
self.masks = parent_surface.masks
self.paths = parent_surface.paths
self.filters = parent_surface.filters
else:
self.markers = {} self.markers = {}
self.gradients = {} self.gradients = {}
self.patterns = {} self.patterns = {}
self.masks = {}
self.paths = {} self.paths = {}
self.filters = {}
self.page_sizes = [] self.page_sizes = []
self._old_parent_node = self.parent_node = None self._old_parent_node = self.parent_node = None
self.output = output self.output = output
...@@ -172,7 +188,7 @@ class Surface(object): ...@@ -172,7 +188,7 @@ class Surface(object):
"""Draw the root ``node``.""" """Draw the root ``node``."""
self.draw(node) self.draw(node)
def draw(self, node, stroke_and_fill = True): def draw(self, node, stroke_and_fill=True):
"""Draw ``node`` and its children.""" """Draw ``node`` and its children."""
old_font_size = self.font_size old_font_size = self.font_size
self.font_size = size(self, node.get("font-size", "12pt")) self.font_size = size(self, node.get("font-size", "12pt"))
...@@ -194,18 +210,20 @@ class Surface(object): ...@@ -194,18 +210,20 @@ class Surface(object):
self._old_parent_node = self.parent_node self._old_parent_node = self.parent_node
self.parent_node = node self.parent_node = node
self.context.save()
# Transform the context according to the ``transform`` attribute
transform(self, node.get("transform"))
masks = urls(node.get("mask"))
mask = masks[0][1:] if masks else None
opacity = float(node.get("opacity", 1)) opacity = float(node.get("opacity", 1))
if opacity < 1: if mask or opacity < 1:
self.context.push_group() self.context.push_group()
self.context.save()
self.context.move_to( self.context.move_to(
size(self, node.get("x"), "x"), size(self, node.get("x"), "x"),
size(self, node.get("y"), "y")) 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: if node.tag in PATH_TAGS:
# Set 1 as default stroke-width # Set 1 as default stroke-width
if not node.get("stroke-width"): if not node.get("stroke-width"):
...@@ -234,6 +252,46 @@ class Surface(object): ...@@ -234,6 +252,46 @@ class Surface(object):
miter_limit = float(node.get("stroke-miterlimit", 4)) miter_limit = float(node.get("stroke-miterlimit", 4))
self.context.set_miter_limit(miter_limit) self.context.set_miter_limit(miter_limit)
# Clip
rect_values = rect(node.get("clip"))
if len(rect_values) == 4:
top = float(size(self, rect_values[0], "y"))
right = float(size(self, rect_values[1], "x"))
bottom = float(size(self, rect_values[2], "y"))
left = float(size(self, rect_values[3], "x"))
x = float(size(self, node.get("x"), "x"))
y = float(size(self, node.get("y"), "y"))
width = float(size(self, node.get("width"), "x"))
height = float(size(self, node.get("height"), "y"))
self.context.save()
self.context.translate(x, y)
self.context.rectangle(
left, top, width - left - right, height - top - bottom)
self.context.restore()
self.context.clip()
clip_paths = urls(node.get("clip-path"))
if clip_paths:
path = self.paths.get(clip_paths[0][1:])
if path:
self.context.save()
if path.get("clipPathUnits") == "objectBoundingBox":
x = float(size(self, node.get("x"), "x"))
y = float(size(self, node.get("y"), "y"))
width = float(size(self, node.get("width"), "x"))
height = float(size(self, node.get("height"), "y"))
self.context.translate(x, y)
self.context.scale(width, height)
path.tag = "g"
self.draw(path, stroke_and_fill=False)
self.context.restore()
if node.get("clip-rule") == "evenodd":
self.context.set_fill_rule(cairo.FILL_RULE_EVEN_ODD)
self.context.clip()
self.context.set_fill_rule(cairo.FILL_RULE_WINDING)
# Filter
apply_filter_before(self, node)
if node.tag in TAGS: if node.tag in TAGS:
try: try:
TAGS[node.tag](self, node) TAGS[node.tag](self, node)
...@@ -241,53 +299,59 @@ class Surface(object): ...@@ -241,53 +299,59 @@ class Surface(object):
# Error in point parsing, do nothing # Error in point parsing, do nothing
pass pass
# Filter
apply_filter_after(self, node)
# Get stroke and fill opacity # Get stroke and fill opacity
stroke_opacity = float(node.get("stroke-opacity", 1)) stroke_opacity = float(node.get("stroke-opacity", 1))
fill_opacity = float(node.get("fill-opacity", 1)) fill_opacity = float(node.get("fill-opacity", 1))
# Manage dispaly and visibility # Manage display and visibility
display = node.get("display", "inline") != "none" display = node.get("display", "inline") != "none"
visible = display and (node.get("visibility", "visible") != "hidden") visible = display and (node.get("visibility", "visible") != "hidden")
if stroke_and_fill and visible: if stroke_and_fill and visible and node.tag in TAGS:
# Fill # Fill
if "url(#" in (node.get("fill") or ""): self.context.save()
name = filter_fill_or_stroke(node.get("fill")) paint_source, paint_color = paint(node.get("fill", "black"))
gradient_or_pattern(self, node, name) if not gradient_or_pattern(self, node, paint_source):
else:
if node.get("fill-rule") == "evenodd": if node.get("fill-rule") == "evenodd":
self.context.set_fill_rule(cairo.FILL_RULE_EVEN_ODD) self.context.set_fill_rule(cairo.FILL_RULE_EVEN_ODD)
self.context.set_source_rgba( self.context.set_source_rgba(*color(paint_color, fill_opacity))
*color(node.get("fill", "black"), fill_opacity))
self.context.fill_preserve() self.context.fill_preserve()
self.context.restore()
# Stroke # Stroke
self.context.save()
self.context.set_line_width(size(self, node.get("stroke-width"))) self.context.set_line_width(size(self, node.get("stroke-width")))
if "url(#" in (node.get("stroke") or ""): paint_source, paint_color = paint(node.get("stroke"))
name = filter_fill_or_stroke(node.get("stroke")) if not gradient_or_pattern(self, node, paint_source):
gradient_or_pattern(self, node, name)
else:
self.context.set_source_rgba( self.context.set_source_rgba(
*color(node.get("stroke"), stroke_opacity)) *color(paint_color, stroke_opacity))
self.context.stroke() self.context.stroke()
self.context.restore()
elif not visible: elif not visible:
self.context.new_path() self.context.new_path()
# Draw children # Draw children
if display and node.tag not in ( if display and node.tag not in (
"linearGradient", "radialGradient", "marker", "pattern"): "linearGradient", "radialGradient", "marker", "pattern",
"mask", "clipPath", "filter"):
for child in node.children: for child in node.children:
self.draw(child, stroke_and_fill) self.draw(child, stroke_and_fill)
if mask or opacity < 1:
self.context.pop_group_to_source()
if mask and mask in self.masks:
paint_mask(self, node, mask, opacity)
else:
self.context.paint_with_alpha(opacity)
if not node.root: if not node.root:
# Restoring context is useless if we are in the root tag, it may # Restoring context is useless if we are in the root tag, it may
# raise an exception if we have multiple svg tags # raise an exception if we have multiple svg tags
self.context.restore() 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.parent_node = self._old_parent_node
self.font_size = old_font_size self.font_size = old_font_size
......
...@@ -200,7 +200,7 @@ COLORS = { ...@@ -200,7 +200,7 @@ COLORS = {
"windowtext": "#000000"} "windowtext": "#000000"}
def color(string = None, opacity = 1): def color(string=None, opacity=1):
"""Replace ``string`` representing a color by a RGBA tuple.""" """Replace ``string`` representing a color by a RGBA tuple."""
if not string or string in ("none", "transparent"): if not string or string in ("none", "transparent"):
return (0, 0, 0, 0) return (0, 0, 0, 0)
......
...@@ -22,36 +22,62 @@ This module handles gradients and patterns. ...@@ -22,36 +22,62 @@ This module handles gradients and patterns.
""" """
import cairo
from math import radians from math import radians
from copy import deepcopy
from . import cairo
from .colors import color from .colors import color
from .helpers import node_format, preserve_ratio, urls, transform from .helpers import node_format, preserve_ratio, paint, urls, transform
from .shapes import rect
from .units import size from .units import size
from ..parser import Tree from ..parser import Tree
from ..features import match_features
BLEND_OPERATORS = {
"normal": 2,
"multiply": 14,
"screen": 15,
"darken": 17,
"lighten": 18}
def update_def_href(surface, def_name, def_dict):
"""Update the attributes of the def according to its href attribute."""
def_node = def_dict[def_name]
href = def_node.get("{http://www.w3.org/1999/xlink}href")
if href and href[0] == "#" and href[1:] in def_dict:
href_urls = urls(href)
href_url = href_urls[0] if href_urls else None
href_name = href_url[1:]
if href_name in def_dict:
update_def_href(surface, href_name, def_dict)
href_node = def_dict[href_name]
def_dict[def_name] = Tree(
url="#%s" % def_name, parent=href_node,
parent_children=(not def_node.children),
tree_cache=surface.tree_cache)
# Inherit attributes generally not inherited
for key, value in href_node.items():
if key not in def_dict[def_name]:
def_dict[def_name][key] = value
def parse_def(surface, node): def parse_def(surface, node):
"""Parse the SVG definitions.""" """Parse the SVG definitions."""
for def_type in ("marker", "gradient", "pattern", "path"): for def_type in (
"marker", "gradient", "pattern", "path", "mask", "filter"):
if def_type in node.tag.lower(): if def_type in node.tag.lower():
def_list = getattr(surface, def_type + "s") getattr(surface, def_type + "s")[node["id"]] = node
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): def gradient_or_pattern(surface, node, name):
"""Gradient or pattern color.""" """Gradient or pattern color."""
if name in surface.gradients: if name in surface.gradients:
update_def_href(surface, name, surface.gradients)
return draw_gradient(surface, node, name) return draw_gradient(surface, node, name)
elif name in surface.patterns: elif name in surface.patterns:
return draw_pattern(surface, name) update_def_href(surface, name, surface.patterns)
return draw_pattern(surface, node, name)
def marker(surface, node): def marker(surface, node):
...@@ -59,6 +85,16 @@ def marker(surface, node): ...@@ -59,6 +85,16 @@ def marker(surface, node):
parse_def(surface, node) parse_def(surface, node)
def mask(surface, node):
"""Store a mask definition."""
parse_def(surface, node)
def filter_(surface, node):
"""Store a filter definition."""
parse_def(surface, node)
def linear_gradient(surface, node): def linear_gradient(surface, node):
"""Store a linear gradient definition.""" """Store a linear gradient definition."""
parse_def(surface, node) parse_def(surface, node)
...@@ -74,6 +110,56 @@ def pattern(surface, node): ...@@ -74,6 +110,56 @@ def pattern(surface, node):
parse_def(surface, node) parse_def(surface, node)
def clip_path(surface, node):
"""Store a clip path definition."""
surface.paths[node["id"]] = node
def paint_mask(surface, node, name, opacity):
"""Paint the mask of the current surface."""
mask_node = surface.masks[name]
mask_node.tag = "g"
mask_node["opacity"] = opacity
if mask_node.get("maskUnits") == "userSpaceOnUse":
width_ref, height_ref = "x", "y"
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 = width
height_ref = height
mask_node["transform"] = "%s scale(%f, %f)" % (
mask_node.get("transform", ""), width, height)
mask_node["x"] = float(
size(surface, mask_node.get("x", "-10%"), width_ref))
mask_node["y"] = float(
size(surface, mask_node.get("y", "-10%"), height_ref))
mask_node["height"] = float(
size(surface, mask_node.get("height", "120%"), width_ref))
mask_node["width"] = float(
size(surface, mask_node.get("width", "120%"), height_ref))
if mask_node.get("maskUnits") == "userSpaceOnUse":
x = mask_node["x"]
y = mask_node["y"]
mask_node["viewBox"] = "%f %f %f %f" % (
mask_node["x"], mask_node["y"],
mask_node["width"], mask_node["height"])
from . import SVGSurface # circular import
mask_surface = SVGSurface(mask_node, None, surface.dpi, surface)
surface.context.save()
surface.context.translate(x, y)
surface.context.scale(
mask_node["width"] / mask_surface.width,
mask_node["height"] / mask_surface.height)
surface.context.mask_surface(mask_surface.cairo)
surface.context.restore()
def draw_gradient(surface, node, name): def draw_gradient(surface, node, name):
"""Gradients colors.""" """Gradients colors."""
gradient_node = surface.gradients[name] gradient_node = surface.gradients[name]
...@@ -107,8 +193,7 @@ def draw_gradient(surface, node, name): ...@@ -107,8 +193,7 @@ def draw_gradient(surface, node, name):
if gradient_node.get("gradientUnits") != "userSpaceOnUse": if gradient_node.get("gradientUnits") != "userSpaceOnUse":
gradient_pattern.set_matrix(cairo.Matrix( gradient_pattern.set_matrix(cairo.Matrix(
1 / width, 0, 0, 1 / height, -x / width, -y / height)) 1 / width, 0, 0, 1 / height, - x / width, - y / height))
gradient_pattern.set_extend(getattr( gradient_pattern.set_extend(getattr(
cairo, "EXTEND_%s" % node.get("spreadMethod", "pad").upper())) cairo, "EXTEND_%s" % node.get("spreadMethod", "pad").upper()))
...@@ -124,26 +209,58 @@ def draw_gradient(surface, node, name): ...@@ -124,26 +209,58 @@ def draw_gradient(surface, node, name):
cairo, "EXTEND_%s" % gradient_node.get("spreadMethod", "pad").upper())) cairo, "EXTEND_%s" % gradient_node.get("spreadMethod", "pad").upper()))
surface.context.set_source(gradient_pattern) surface.context.set_source(gradient_pattern)
return True
def draw_pattern(surface, name): def draw_pattern(surface, node, name):
"""Draw a pattern image.""" """Draw a pattern image."""
pattern_node = surface.patterns[name] pattern_node = surface.patterns[name]
pattern_node.tag = "g" pattern_node.tag = "g"
transform(surface, "translate(%s %s)" % (
pattern_node.get("x"), pattern_node.get("y")))
transform(surface, pattern_node.get("patternTransform")) transform(surface, pattern_node.get("patternTransform"))
if pattern_node.get("viewBox"):
if not (size(surface, pattern_node.get("width", 1), 1) and
size(surface, pattern_node.get("height", 1), 1)):
return False
else:
if not (size(surface, pattern_node.get("width", 0), 1) and
size(surface, pattern_node.get("height", 0), 1)):
return False
if pattern_node.get("patternUnits") == "userSpaceOnUse":
x = float(size(surface, pattern_node.get("x"), "x"))
y = float(size(surface, pattern_node.get("y"), "y"))
pattern_width = \
float(size(surface, pattern_node.get("width", 0), 1))
pattern_height = \
float(size(surface, pattern_node.get("height", 0), 1))
else:
width = float(size(surface, node.get("width"), "x"))
height = float(size(surface, node.get("height"), "y"))
x = float(size(surface, pattern_node.get("x"), 1)) * width
y = float(size(surface, pattern_node.get("y"), 1)) * height
pattern_width = \
size(surface, pattern_node.pop("width", "0"), 1) * width
pattern_height = \
size(surface, pattern_node.pop("height", "0"), 1) * height
if "viewBox" not in pattern_node:
pattern_node["width"] = pattern_width
pattern_node["height"] = pattern_height
if pattern_node.get("patternContentUnits") == "objectBoundingBox":
pattern_node["transform"] = "scale(%s, %s)" % (width, height)
from . import SVGSurface # circular import from . import SVGSurface # circular import
pattern_surface = SVGSurface(pattern_node, None, surface.dpi) pattern_surface = SVGSurface(pattern_node, None, surface.dpi, surface)
pattern_pattern = cairo.SurfacePattern(pattern_surface.cairo) pattern_pattern = cairo.SurfacePattern(pattern_surface.cairo)
pattern_pattern.set_extend(cairo.EXTEND_REPEAT) pattern_pattern.set_extend(cairo.EXTEND_REPEAT)
pattern_pattern.set_matrix(cairo.Matrix(
pattern_surface.width / pattern_width, 0, 0,
pattern_surface.height / pattern_height, -x, -y))
surface.context.set_source(pattern_pattern) surface.context.set_source(pattern_pattern)
return True
def draw_marker(surface, node, position = "mid"): def draw_marker(surface, node, position="mid"):
"""Draw a marker.""" """Draw a marker."""
# TODO: manage markers for other tags than path
if position == "start": if position == "start":
node.markers = { node.markers = {
"start": list(urls(node.get("marker-start", ""))), "start": list(urls(node.get("marker-start", ""))),
...@@ -195,9 +312,13 @@ def draw_marker(surface, node, position = "mid"): ...@@ -195,9 +312,13 @@ def draw_marker(surface, node, position = "mid"):
scale_x, scale_y, translate_x, translate_y = \ scale_x, scale_y, translate_x, translate_y = \
preserve_ratio(surface, marker_node) preserve_ratio(surface, marker_node)
viewbox = node_format(surface, marker_node)[-1] width, height, viewbox = node_format(surface, marker_node)
if viewbox:
viewbox_width = viewbox[2] - viewbox[0] viewbox_width = viewbox[2] - viewbox[0]
viewbox_height = viewbox[3] - viewbox[1] viewbox_height = viewbox[3] - viewbox[1]
else:
viewbox_width = width or 0
viewbox_height = height or 0
surface.context.new_path() surface.context.new_path()
for child in marker_node.children: for child in marker_node.children:
...@@ -216,6 +337,69 @@ def draw_marker(surface, node, position = "mid"): ...@@ -216,6 +337,69 @@ def draw_marker(surface, node, position = "mid"):
node.pending_markers.append(pending_marker) node.pending_markers.append(pending_marker)
def apply_filter_before(surface, node):
if node["id"] in surface.masks:
return
names = urls(node.get("filter"))
name = names[0][1:] if names else None
if name in surface.filters:
filter_node = surface.filters[name]
for child in filter_node.children:
# Offset
if child.tag == "feOffset":
if filter_node.get("primitiveUnits") == "objectBoundingBox":
width = float(size(surface, node.get("width"), "x"))
height = float(size(surface, node.get("height"), "y"))
dx = size(surface, child.get("dx", 0), 1) * width
dy = size(surface, child.get("dy", 0), 1) * height
else:
dx = size(surface, child.get("dx", 0), 1)
dy = size(surface, child.get("dy", 0), 1)
surface.context.translate(dx, dy)
def apply_filter_after(surface, node):
surface.context.set_operator(BLEND_OPERATORS["normal"])
if node["id"] in surface.masks:
return
names = urls(node.get("filter"))
name = names[0][1:] if names else None
if name in surface.filters:
filter_node = surface.filters[name]
for child in filter_node.children:
# Blend
if child.tag == "feBlend":
surface.context.set_operator(BLEND_OPERATORS.get(
child.get("mode", "normal"), BLEND_OPERATORS["normal"]))
# Flood
elif child.tag == "feFlood":
surface.context.new_path()
if filter_node.get("primitiveUnits") == "objectBoundingBox":
x = float(size(surface, node.get("x"), "x"))
y = float(size(surface, node.get("y"), "y"))
x = size(surface, child.get("x", 0), 1) + x
y = size(surface, child.get("y", 0), 1) + y
width = float(size(surface, node.get("width"), "x"))
height = float(size(surface, node.get("height"), "y"))
width = size(surface, child.get("width", 0), 1) * width
height = size(surface, child.get("height", 0), 1) * height
else:
x = size(surface, child.get("x", 0), 1)
y = size(surface, child.get("y", 0), 1)
width = size(surface, child.get("width", 0), 1)
height = size(surface, child.get("height", 0), 1)
rect_node = dict(x=x, y=y, width=width, height=height)
rect(surface, rect_node)
surface.context.set_source_rgba(*color(
paint(child.get("flood-color"))[1],
float(child.get("flood-opacity", 1))))
surface.context.fill()
surface.context.new_path()
def use(surface, node): def use(surface, node):
"""Draw the content of another SVG file.""" """Draw the content of another SVG file."""
surface.context.save() surface.context.save()
...@@ -227,11 +411,25 @@ def use(surface, node): ...@@ -227,11 +411,25 @@ def use(surface, node):
del node["y"] del node["y"]
if "viewBox" in node: if "viewBox" in node:
del node["viewBox"] del node["viewBox"]
if "mask" in node:
del node["mask"]
href = node.get("{http://www.w3.org/1999/xlink}href") href = node.get("{http://www.w3.org/1999/xlink}href")
url = list(urls(href))[0] tree_urls = urls(href)
tree = Tree(url = url, parent = node) url = tree_urls[0] if tree_urls else None
tree = Tree(url=url, parent=node, tree_cache=surface.tree_cache)
if not match_features(tree.xml_tree):
return
if tree.tag == "svg":
# Explicitely specified
# http://www.w3.org/TR/SVG11/struct.html#UseElement
if "width" in node and "height" in node:
tree["width"], tree["height"] = node["width"], node["height"]
surface.set_context_size(*node_format(surface, tree)) surface.set_context_size(*node_format(surface, tree))
surface.draw(tree) surface.draw(tree)
surface.context.restore() surface.context.restore()
# Restore twice, because draw does not restore at the end of svg tags # Restore twice, because draw does not restore at the end of svg tags
if tree.tag != "use":
surface.context.restore() surface.context.restore()
...@@ -20,9 +20,9 @@ Surface helpers. ...@@ -20,9 +20,9 @@ Surface helpers.
""" """
import cairo
from math import cos, sin, tan, atan2, radians from math import cos, sin, tan, atan2, radians
from . import cairo
from .units import size from .units import size
# Python 2/3 management # Python 2/3 management
...@@ -43,17 +43,25 @@ def distance(x1, y1, x2, y2): ...@@ -43,17 +43,25 @@ def distance(x1, y1, x2, y2):
return ((x2 - x1) ** 2 + (y2 - y1) ** 2) ** 0.5 return ((x2 - x1) ** 2 + (y2 - y1) ** 2) ** 0.5
def filter_fill_or_stroke(value): def paint(value):
"""Remove unnecessary characters from fill or stroke value.""" """Extract from value an uri and a color.
See http://www.w3.org/TR/SVG/painting.html#SpecifyingPaint
"""
if not value: if not value:
return return None, None
content = list(urls(value))[0] value = value.strip()
if "url" in value:
if not content.startswith("#"): if value.startswith("url"):
return source = urls(value.split(")")[0])[0][1:]
content = content[1:] color = value.split(")", 1)[-1].strip() or None
return content else:
source = None
color = value.strip() or None
return (source, color)
def node_format(surface, node): def node_format(surface, node):
...@@ -68,7 +76,7 @@ def node_format(surface, node): ...@@ -68,7 +76,7 @@ def node_format(surface, node):
return width, height, viewbox return width, height, viewbox
def normalize(string = None): def normalize(string=None):
"""Normalize a string corresponding to an array of various values.""" """Normalize a string corresponding to an array of various values."""
string = string.replace("-", " -") string = string.replace("-", " -")
string = string.replace(",", " ") string = string.replace(",", " ")
...@@ -77,6 +85,7 @@ def normalize(string = None): ...@@ -77,6 +85,7 @@ def normalize(string = None):
string = string.replace(" ", " ") string = string.replace(" ", " ")
string = string.replace("e -", "e-") string = string.replace("e -", "e-")
string = string.replace("E -", "E-")
values = string.split(" ") values = string.split(" ")
string = "" string = ""
...@@ -91,7 +100,7 @@ def normalize(string = None): ...@@ -91,7 +100,7 @@ def normalize(string = None):
return string.strip() return string.strip()
def point(surface, string = None): def point(surface, string=None):
"""Return ``(x, y, trailing_text)`` from ``string``.""" """Return ``(x, y, trailing_text)`` from ``string``."""
if not string: if not string:
return (0, 0, "") return (0, 0, "")
...@@ -180,8 +189,7 @@ def transform(surface, string): ...@@ -180,8 +189,7 @@ def transform(surface, string):
transformations = string.split(")") transformations = string.split(")")
matrix = cairo.Matrix() matrix = cairo.Matrix()
for transformation in transformations: for transformation in transformations:
for ttype in ( for ttype in ("scale", "translate", "matrix", "rotate", "skewX",
"scale", "translate", "matrix", "rotate", "skewX",
"skewY"): "skewY"):
if ttype in transformation: if ttype in transformation:
transformation = transformation.replace(ttype, "") transformation = transformation.replace(ttype, "")
...@@ -189,8 +197,7 @@ def transform(surface, string): ...@@ -189,8 +197,7 @@ def transform(surface, string):
transformation = normalize(transformation).strip() + " " transformation = normalize(transformation).strip() + " "
values = [] values = []
while transformation: while transformation:
value, transformation = \ value, transformation = transformation.split(" ", 1)
transformation.split(" ", 1)
# TODO: manage the x/y sizes here # TODO: manage the x/y sizes here
values.append(size(surface, value)) values.append(size(surface, value))
if ttype == "matrix": if ttype == "matrix":
...@@ -236,8 +243,23 @@ def apply_matrix_transform(surface, matrix): ...@@ -236,8 +243,23 @@ def apply_matrix_transform(surface, matrix):
def urls(string): def urls(string):
"""Parse a comma-separated list of url() strings.""" """Parse a comma-separated list of url() strings."""
for link in string.split(","): if not string:
link = link.strip() return []
if link.startswith("url"):
link = link[3:] string = string.strip()
yield link.strip("() ") if string.startswith("url"):
string = string[3:]
return [
link.strip("() ") for link in string.rsplit(")")[0].split(",")
if link.strip("() ")]
def rect(string):
"""Parse the rect value of a clip."""
if not string:
return []
string = string.strip()
if string.startswith("rect"):
return string[4:].strip('() ').split(',')
else:
return []
...@@ -21,7 +21,7 @@ Images manager. ...@@ -21,7 +21,7 @@ Images manager.
""" """
import base64 import base64
import cairo import gzip
from io import BytesIO from io import BytesIO
try: try:
from urllib import urlopen, unquote from urllib import urlopen, unquote
...@@ -32,6 +32,8 @@ except ImportError: ...@@ -32,6 +32,8 @@ except ImportError:
from urllib.request import urlopen from urllib.request import urlopen
from urllib import parse as urlparse # Python 3 from urllib import parse as urlparse # Python 3
from urllib.parse import unquote_to_bytes from urllib.parse import unquote_to_bytes
from . import cairo
from .helpers import node_format, size, preserve_ratio from .helpers import node_format, size, preserve_ratio
from ..parser import Tree from ..parser import Tree
...@@ -56,7 +58,7 @@ def open_data_url(url): ...@@ -56,7 +58,7 @@ def open_data_url(url):
if header: if header:
semi = header.rfind(";") semi = header.rfind(";")
if semi >= 0 and "=" not in header[semi:]: if semi >= 0 and "=" not in header[semi:]:
encoding = header[semi + 1:] encoding = header[semi+1:]
else: else:
encoding = "" encoding = ""
else: else:
...@@ -100,8 +102,11 @@ def image(surface, node): ...@@ -100,8 +102,11 @@ def image(surface, node):
surface.context.clip() surface.context.clip()
if image_bytes[:4] == b"\x89PNG": if image_bytes[:4] == b"\x89PNG":
png_bytes = image_bytes png_file = BytesIO(image_bytes)
elif image_bytes[:5] == b"\x3csvg ": elif (image_bytes[:5] in (b"<svg ", b"<?xml", b"<!DOC") or
image_bytes[:2] == b"\x1f\x8b"):
if image_bytes[:2] == b"\x1f\x8b":
image_bytes = gzip.GzipFile(fileobj=BytesIO(image_bytes)).read()
surface.context.save() surface.context.save()
surface.context.translate(x, y) surface.context.translate(x, y)
if "x" in node: if "x" in node:
...@@ -110,7 +115,8 @@ def image(surface, node): ...@@ -110,7 +115,8 @@ def image(surface, node):
del node["y"] del node["y"]
if "viewBox" in node: if "viewBox" in node:
del node["viewBox"] del node["viewBox"]
tree = Tree(bytestring = image_bytes) tree = Tree(
url=url, bytestring=image_bytes, tree_cache=surface.tree_cache)
tree_width, tree_height, viewbox = node_format(surface, tree) tree_width, tree_height, viewbox = node_format(surface, tree)
if not tree_width or not tree_height: if not tree_width or not tree_height:
tree_width = tree["width"] = width tree_width = tree["width"] = width
...@@ -130,13 +136,15 @@ def image(surface, node): ...@@ -130,13 +136,15 @@ def image(surface, node):
return return
else: else:
try: try:
from pystacia import read_blob from PIL import Image
png_bytes = read_blob(image_bytes).get_blob('png') png_file = BytesIO()
Image.open(BytesIO(image_bytes)).save(png_file, 'PNG')
png_file.seek(0)
except: except:
# No way to handle the image # No way to handle the image
return return
image_surface = cairo.ImageSurface.create_from_png(BytesIO(png_bytes)) image_surface = cairo.ImageSurface.create_from_png(png_file)
node.image_width = image_surface.get_width() node.image_width = image_surface.get_width()
node.image_height = image_surface.get_height() node.image_height = image_surface.get_height()
......
...@@ -136,6 +136,7 @@ def path(surface, node): ...@@ -136,6 +136,7 @@ def path(surface, node):
elif letter == "c": elif letter == "c":
# Relative curve # Relative curve
x, y = surface.context.get_current_point()
x1, y1, string = point(surface, string) x1, y1, string = point(surface, string)
x2, y2, string = point(surface, string) x2, y2, string = point(surface, string)
x3, y3, string = point(surface, string) x3, y3, string = point(surface, string)
...@@ -143,6 +144,14 @@ def path(surface, node): ...@@ -143,6 +144,14 @@ def path(surface, node):
point_angle(x2, y2, x1, y1), point_angle(x2, y2, x3, y3))) point_angle(x2, y2, x1, y1), point_angle(x2, y2, x3, y3)))
surface.context.rel_curve_to(x1, y1, x2, y2, x3, y3) surface.context.rel_curve_to(x1, y1, x2, y2, x3, y3)
# Save absolute values for x and y, useful if next letter is s or S
x1 += x
x2 += x
x3 += x
y1 += y
y2 += y
y3 += y
elif letter == "C": elif letter == "C":
# Curve # Curve
x1, y1, string = point(surface, string) x1, y1, string = point(surface, string)
...@@ -186,7 +195,10 @@ def path(surface, node): ...@@ -186,7 +195,10 @@ def path(surface, node):
elif letter == "m": elif letter == "m":
# Current point relative move # Current point relative move
x, y, string = point(surface, string) x, y, string = point(surface, string)
if surface.context.has_current_point():
surface.context.rel_move_to(x, y) surface.context.rel_move_to(x, y)
else:
surface.context.move_to(x, y)
elif letter == "M": elif letter == "M":
# Current point move # Current point move
...@@ -215,21 +227,28 @@ def path(surface, node): ...@@ -215,21 +227,28 @@ def path(surface, node):
elif letter == "s": elif letter == "s":
# Relative smooth curve # Relative smooth curve
# TODO: manage last_letter in "CS" x, y = surface.context.get_current_point()
x1 = x3 - x2 if last_letter in "cs" else 0 x1 = x3 - x2 if last_letter in "csCS" else 0
y1 = y3 - y2 if last_letter in "cs" else 0 y1 = y3 - y2 if last_letter in "csCS" else 0
x2, y2, string = point(surface, string) x2, y2, string = point(surface, string)
x3, y3, string = point(surface, string) x3, y3, string = point(surface, string)
node.tangents.extend(( node.tangents.extend((
point_angle(x2, y2, x1, y1), point_angle(x2, y2, x3, y3))) point_angle(x2, y2, x1, y1), point_angle(x2, y2, x3, y3)))
surface.context.rel_curve_to(x1, y1, x2, y2, x3, y3) surface.context.rel_curve_to(x1, y1, x2, y2, x3, y3)
# Save absolute values for x and y, useful if next letter is s or S
x1 += x
x2 += x
x3 += x
y1 += y
y2 += y
y3 += y
elif letter == "S": elif letter == "S":
# Smooth curve # Smooth curve
# TODO: manage last_letter in "cs"
x, y = surface.context.get_current_point() x, y = surface.context.get_current_point()
x1 = 2 * x3 - x2 if last_letter in "CS" else x x1 = x3 + (x3 - x2) if last_letter in "csCS" else x
y1 = 2 * y3 - y2 if last_letter in "CS" else y y1 = y3 + (y3 - y2) if last_letter in "csCS" else y
x2, y2, string = point(surface, string) x2, y2, string = point(surface, string)
x3, y3, string = point(surface, string) x3, y3, string = point(surface, string)
node.tangents.extend(( node.tangents.extend((
...@@ -294,14 +313,12 @@ def path(surface, node): ...@@ -294,14 +313,12 @@ def path(surface, node):
string = string.strip() 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": if string and letter not in "mMzZ":
draw_marker(surface, node, "mid") draw_marker(surface, node, "mid")
last_letter = letter last_letter = letter
if node.tangents != [None]:
# node.tangents == [None] means empty path
node.tangents.append(node.tangents[-1]) node.tangents.append(node.tangents[-1])
draw_marker(surface, node, "end") draw_marker(surface, node, "end")
...@@ -32,8 +32,8 @@ def circle(surface, node): ...@@ -32,8 +32,8 @@ def circle(surface, node):
return return
surface.context.new_sub_path() surface.context.new_sub_path()
surface.context.arc( surface.context.arc(
size(surface, node.get("x"), "x") + size(surface, node.get("cx"), "x"), size(surface, node.get("cx"), "x"),
size(surface, node.get("y"), "y") + size(surface, node.get("cy"), "y"), size(surface, node.get("cy"), "y"),
r, 0, 2 * pi) r, 0, 2 * pi)
...@@ -83,20 +83,40 @@ def polyline(surface, node): ...@@ -83,20 +83,40 @@ def polyline(surface, node):
def rect(surface, node): def rect(surface, node):
"""Draw a rect ``node`` on ``surface``.""" """Draw a rect ``node`` on ``surface``."""
# TODO: handle ry
x, y = size(surface, node.get("x"), "x"), size(surface, node.get("y"), "y") x, y = size(surface, node.get("x"), "x"), size(surface, node.get("y"), "y")
width = size(surface, node.get("width"), "x") width = size(surface, node.get("width"), "x")
height = size(surface, node.get("height"), "y") height = size(surface, node.get("height"), "y")
if size(surface, node.get("rx"), "x") == 0: rx = node.get("rx")
ry = node.get("ry")
if rx and ry is None:
ry = rx
elif ry and rx is None:
rx = ry
rx = size(surface, rx, "x")
ry = size(surface, ry, "y")
if rx == 0 or ry == 0:
surface.context.rectangle(x, y, width, height) surface.context.rectangle(x, y, width, height)
else: else:
r = size(surface, node.get("rx"), "x") if rx > width / 2.:
a, b, c, d = x, width + x, y, height + y rx = width / 2.
if r > width - r: if ry > height / 2.:
r = width / 2 ry = height / 2.
surface.context.move_to(x, y + height / 2)
surface.context.arc(a + r, c + r, r, 2 * pi / 2, 3 * pi / 2) # Inspired by Cairo Cookbook
surface.context.arc(b - r, c + r, r, 3 * pi / 2, 0 * pi / 2) # http://cairographics.org/cookbook/roundedrectangles/
surface.context.arc(b - r, d - r, r, 0 * pi / 2, 1 * pi / 2) ARC_TO_BEZIER = 4 * (2 ** .5 - 1) / 3
surface.context.arc(a + r, d - r, r, 1 * pi / 2, 2 * pi / 2) c1 = ARC_TO_BEZIER * rx
c2 = ARC_TO_BEZIER * ry
surface.context.new_path()
surface.context.move_to(x + rx, y)
surface.context.rel_line_to(width - 2 * rx, 0)
surface.context.rel_curve_to(c1, 0, rx, c2, rx, ry)
surface.context.rel_line_to(0, height - 2 * ry)
surface.context.rel_curve_to(0, c2, c1 - rx, ry, -rx, ry)
surface.context.rel_line_to(-width + 2 * rx, 0)
surface.context.rel_curve_to(-c1, 0, -rx, -c2, -rx, -ry)
surface.context.rel_line_to(0, -height + 2 * ry)
surface.context.rel_curve_to(0, -c2, rx - c1, -ry, rx, -ry)
surface.context.close_path() surface.context.close_path()
...@@ -21,17 +21,27 @@ Root tag drawer. ...@@ -21,17 +21,27 @@ Root tag drawer.
""" """
from .helpers import preserve_ratio, node_format from .helpers import preserve_ratio, node_format
from .units import size
def svg(surface, node): def svg(surface, node):
"""Draw a svg ``node``.""" """Draw a svg ``node``."""
if node.get("preserveAspectRatio", "none") != "none":
width, height, viewbox = node_format(surface, node) width, height, viewbox = node_format(surface, node)
node.image_width, node.image_height = viewbox[2:] if viewbox:
node.image_width = viewbox[2] - viewbox[0]
node.image_height = viewbox[3] - viewbox[1]
else:
node.image_width = size(surface, node["width"], "x")
node.image_height = size(surface, node["height"], "y")
if node.get("preserveAspectRatio", "none") != "none":
scale_x, scale_y, translate_x, translate_y = \ scale_x, scale_y, translate_x, translate_y = \
preserve_ratio(surface, node) preserve_ratio(surface, node)
surface.context.rectangle(0, 0, width, height) rect_width, rect_height = width, height
surface.context.clip() else:
scale_x, scale_y, translate_x, translate_y = (1, 1, 0, 0)
rect_width, rect_height = node.image_width, node.image_height
surface.context.translate(*surface.context.get_current_point()) surface.context.translate(*surface.context.get_current_point())
surface.context.rectangle(0, 0, rect_width, rect_height)
surface.context.clip()
surface.context.scale(scale_x, scale_y) surface.context.scale(scale_x, scale_y)
surface.context.translate(translate_x, translate_y) surface.context.translate(translate_x, translate_y)
...@@ -20,7 +20,9 @@ SVG tags functions. ...@@ -20,7 +20,9 @@ SVG tags functions.
""" """
from .defs import linear_gradient, marker, pattern, radial_gradient, use from .defs import (
clip_path, filter_, linear_gradient, marker, mask, pattern,
radial_gradient, use)
from .image import image from .image import image
from .path import path from .path import path
from .shapes import circle, ellipse, line, polygon, polyline, rect from .shapes import circle, ellipse, line, polygon, polyline, rect
...@@ -30,11 +32,14 @@ from .text import text, text_path, tspan ...@@ -30,11 +32,14 @@ from .text import text, text_path, tspan
TAGS = { TAGS = {
"a": tspan, "a": tspan,
"circle": circle, "circle": circle,
"clipPath": clip_path,
"ellipse": ellipse, "ellipse": ellipse,
"filter": filter_,
"image": image, "image": image,
"line": line, "line": line,
"linearGradient": linear_gradient, "linearGradient": linear_gradient,
"marker": marker, "marker": marker,
"mask": mask,
"path": path, "path": path,
"pattern": pattern, "pattern": pattern,
"polyline": polyline, "polyline": polyline,
...@@ -44,6 +49,5 @@ TAGS = { ...@@ -44,6 +49,5 @@ TAGS = {
"svg": svg, "svg": svg,
"text": text, "text": text,
"textPath": text_path, "textPath": text_path,
"tref": use,
"tspan": tspan, "tspan": tspan,
"use": use} "use": use}
...@@ -20,7 +20,6 @@ Text drawers. ...@@ -20,7 +20,6 @@ Text drawers.
""" """
import cairo
from math import cos, sin from math import cos, sin
# Python 2/3 management # Python 2/3 management
...@@ -31,6 +30,7 @@ except ImportError: ...@@ -31,6 +30,7 @@ except ImportError:
from itertools import izip_longest as zip_longest from itertools import izip_longest as zip_longest
# pylint: enable=E0611 # pylint: enable=E0611
from . import cairo
from .colors import color from .colors import color
from .helpers import distance, normalize, point_angle from .helpers import distance, normalize, point_angle
from .units import size from .units import size
...@@ -79,11 +79,6 @@ def text(surface, node): ...@@ -79,11 +79,6 @@ def text(surface, node):
if not node.get("fill"): if not node.get("fill"):
node["fill"] = "#000000" 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_size = size(surface, node.get("font-size", "12pt"))
font_family = (node.get("font-family") or "sans-serif").split(",")[0] font_family = (node.get("font-family") or "sans-serif").split(",")[0]
font_style = getattr( font_style = getattr(
...@@ -114,7 +109,6 @@ def text(surface, node): ...@@ -114,7 +109,6 @@ def text(surface, node):
def text_path(surface, node): def text_path(surface, node):
"""Draw text on a path.""" """Draw text on a path."""
surface.context.save()
if "url(#" not in (node.get("fill") or ""): if "url(#" not in (node.get("fill") or ""):
surface.context.set_source_rgba(*color(node.get("fill"))) surface.context.set_source_rgba(*color(node.get("fill")))
...@@ -128,6 +122,7 @@ def text_path(surface, node): ...@@ -128,6 +122,7 @@ def text_path(surface, node):
else: else:
return return
surface.context.save()
surface.draw(path, False) surface.draw(path, False)
cairo_path = surface.context.copy_path_flat() cairo_path = surface.context.copy_path_flat()
surface.context.new_path() surface.context.new_path()
...@@ -137,10 +132,9 @@ def text_path(surface, node): ...@@ -137,10 +132,9 @@ def text_path(surface, node):
surface.total_width += start_offset surface.total_width += start_offset
x, y = point_following_path(cairo_path, surface.total_width) x, y = point_following_path(cairo_path, surface.total_width)
string = (node.text or "").strip(" \n")
letter_spacing = size(surface, node.get("letter-spacing")) letter_spacing = size(surface, node.get("letter-spacing"))
for letter in string: for letter in node.text:
surface.total_width += ( surface.total_width += (
surface.context.text_extents(letter)[4] + letter_spacing) surface.context.text_extents(letter)[4] + letter_spacing)
point_on_path = point_following_path(cairo_path, surface.total_width) point_on_path = point_following_path(cairo_path, surface.total_width)
...@@ -174,27 +168,29 @@ def tspan(surface, node): ...@@ -174,27 +168,29 @@ def tspan(surface, node):
y = [size(surface, i, "y") y = [size(surface, i, "y")
for i in normalize(node["y"]).strip().split(" ")] for i in normalize(node["y"]).strip().split(" ")]
string = (node.text or "").strip() if not node.text:
if not string:
return return
fill = node.get("fill") fill = node.get("fill")
positions = list(zip_longest(x, y)) positions = list(zip_longest(x, y))
letters_positions = list(zip(positions, string)) letters_positions = list(zip(positions, node.text))
letters_positions = letters_positions[:-1] + [ letters_positions = letters_positions[:-1] + [
(letters_positions[-1][0], string[len(letters_positions) - 1:])] (letters_positions[-1][0], node.text[len(letters_positions) - 1:])]
for (x, y), letters in letters_positions: for (x, y), letters in letters_positions:
if x == None: if x is None:
x = surface.cursor_position[0] x = surface.cursor_position[0]
if y == None: if y is None:
y = surface.cursor_position[1] y = surface.cursor_position[1]
node["x"] = str(x + size(surface, node.get("dx"), "x")) node["x"] = str(x + size(surface, node.get("dx"), "x"))
node["y"] = str(y + size(surface, node.get("dy"), "y")) node["y"] = str(y + size(surface, node.get("dy"), "y"))
node["fill"] = fill node["fill"] = fill
node.text = letters node.text = letters
if node.parent.tag == "text": if node.parent.tag in ("text", "tspan"):
text(surface, node) text(surface, node)
else: else:
assert node.parent.tag == "textPath"
node["{http://www.w3.org/1999/xlink}href"] = \
node.parent.get("{http://www.w3.org/1999/xlink}href")
node["x"] = str(x + size(surface, node.get("dx"), "x")) node["x"] = str(x + size(surface, node.get("dx"), "x"))
node["y"] = str(y + size(surface, node.get("dy"), "y")) node["y"] = str(y + size(surface, node.get("dy"), "y"))
text_path(surface, node) text_path(surface, node)
......
...@@ -30,7 +30,7 @@ UNITS = { ...@@ -30,7 +30,7 @@ UNITS = {
"px": None} "px": None}
def size(surface, string, reference = "xy"): def size(surface, string, reference="xy"):
"""Replace a ``string`` with units by a float value. """Replace a ``string`` with units by a float value.
If ``reference`` is a float, it is used as reference for percentages. If it If ``reference`` is a float, it is used as reference for percentages. If it
......
...@@ -96,7 +96,7 @@ class DisplayFrame(wx.Frame): ...@@ -96,7 +96,7 @@ class DisplayFrame(wx.Frame):
layercopy.set('height', str(height*self.scale) + 'mm') layercopy.set('height', str(height*self.scale) + 'mm')
layercopy.set('width', str(width*self.scale) + 'mm') layercopy.set('width', str(width*self.scale) + 'mm')
layercopy.set('viewBox', '0 0 ' + str(height*self.scale) + ' ' + str(width*self.scale)) layercopy.set('viewBox', '0 0 ' + str(width*self.scale) + ' ' + str(height*self.scale))
g = layercopy.find("{http://www.w3.org/2000/svg}g") g = layercopy.find("{http://www.w3.org/2000/svg}g")
g.set('transform', 'scale('+str(self.scale)+')') g.set('transform', 'scale('+str(self.scale)+')')
...@@ -104,12 +104,14 @@ class DisplayFrame(wx.Frame): ...@@ -104,12 +104,14 @@ class DisplayFrame(wx.Frame):
else: else:
stream = cStringIO.StringIO(PNGSurface.convert(dpi=self.dpi, bytestring=xml.etree.ElementTree.tostring(image))) stream = cStringIO.StringIO(PNGSurface.convert(dpi=self.dpi, bytestring=xml.etree.ElementTree.tostring(image)))
image = wx.ImageFromStream(stream) pngImage = wx.ImageFromStream(stream)
#print "w:", pngImage.Width, ", dpi:",self.dpi, ", w (mm): ",(pngImage.Width / self.dpi) * 25.4
if self.layer_red: if self.layer_red:
image = image.AdjustChannels(1,0,0,1) pngImage = pngImage.AdjustChannels(1,0,0,1)
dc.DrawBitmap(wx.BitmapFromImage(image), self.offset[0], self.offset[1], True) dc.DrawBitmap(wx.BitmapFromImage(pngImage), self.offset[0], self.offset[1], True)
elif self.slicer == 'bitmap': elif self.slicer == 'bitmap':
if isinstance(image, str): if isinstance(image, str):
...@@ -349,7 +351,7 @@ class SettingsFrame(wx.Frame): ...@@ -349,7 +351,7 @@ class SettingsFrame(wx.Frame):
fieldsizer.Add(self.offset_Y, pos=(3, 3)) fieldsizer.Add(self.offset_Y, pos=(3, 3))
fieldsizer.Add(wx.StaticText(self.panel, -1, "ProjectedX (mm):"), pos=(4, 2), flag=wx.ALIGN_CENTER_VERTICAL) fieldsizer.Add(wx.StaticText(self.panel, -1, "ProjectedX (mm):"), pos=(4, 2), flag=wx.ALIGN_CENTER_VERTICAL)
self.projected_X_mm = floatspin.FloatSpin(self.panel, -1, value=self._get_setting("project_projected_x", 560.0), increment=1, digits=1, size=(80,-1)) self.projected_X_mm = floatspin.FloatSpin(self.panel, -1, value=self._get_setting("project_projected_x", 505.0), increment=1, digits=1, size=(80,-1))
self.projected_X_mm.Bind(floatspin.EVT_FLOATSPIN, self.update_projected_Xmm) self.projected_X_mm.Bind(floatspin.EVT_FLOATSPIN, self.update_projected_Xmm)
self.projected_X_mm.SetHelpText("The actual width of the entire projected image. Use the Calibrate grid to show the full size of the projected image, and measure the width at the same level where the slice will be projected onto the resin.") self.projected_X_mm.SetHelpText("The actual width of the entire projected image. Use the Calibrate grid to show the full size of the projected image, and measure the width at the same level where the slice will be projected onto the resin.")
fieldsizer.Add(self.projected_X_mm, pos=(4, 3)) fieldsizer.Add(self.projected_X_mm, pos=(4, 3))
...@@ -498,8 +500,8 @@ class SettingsFrame(wx.Frame): ...@@ -498,8 +500,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('viewBox', '0 0 ' + width + ' ' + height)
svgSnippet.set('style','background-color:black') svgSnippet.set('style','background-color:black;fill:white;')
svgSnippet.append(i) svgSnippet.append(i)
ol += [svgSnippet] ol += [svgSnippet]
...@@ -534,7 +536,7 @@ class SettingsFrame(wx.Frame): ...@@ -534,7 +536,7 @@ 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('viewBox', '0 0 ' + width + ' ' + height)
svgSnippet.set('style','background-color:black;fill:white;') svgSnippet.set('style','background-color:black;fill:white;')
svgSnippet.append(g) svgSnippet.append(g)
......
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