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
from . import surface
VERSION = '0.4.4'
VERSION = '1.0.dev0'
SURFACES = {
'SVG': surface.SVGSurface, # Tell us if you actually use this one!
'PNG': surface.PNGSurface,
......@@ -52,14 +52,14 @@ def main():
"""Entry-point of the executable."""
# Get command-line options
option_parser = optparse.OptionParser(
usage = "usage: %prog filename [options]", version = VERSION)
usage="usage: %prog filename [options]", version=VERSION)
option_parser.add_option(
"-f", "--format", help = "output format")
"-f", "--format", help="output format")
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(
"-o", "--output",
default = "", help = "output filename")
default="", help="output filename")
options, args = option_parser.parse_args()
# Print help if no argument is given
......
......@@ -20,7 +20,7 @@ Optionally handle CSS stylesheets.
"""
import os
from .parser import HAS_LXML
# Detect optional depedencies
......@@ -53,13 +53,27 @@ def find_stylesheets(tree):
# 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):
"""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
for rule in stylesheet.rules:
# TODO: support @import and @media
if not rule.at_keyword:
for rule in find_stylesheets_rules(stylesheet, tree.url):
yield rule
......@@ -95,7 +109,7 @@ def apply_stylesheets(tree):
style_by_element = {}
for rule in find_style_rules(tree):
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, {})
for name, value, important in declarations:
weight = important, specificity
......@@ -108,5 +122,4 @@ def apply_stylesheets(tree):
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))
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:
# pylint: enable=E0611,F0401,W0611
import re
import gzip
import uuid
import os.path
from .css import apply_stylesheets
from .features import match_features
from .surface.helpers import urls
# Python 2/3 compat
......@@ -77,9 +81,23 @@ def remove_svg_namespace(tree):
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):
"""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."""
super(Node, self).__init__()
self.children = ()
......@@ -89,27 +107,31 @@ class Node(dict):
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")
"stop-opacity", "width", "height", "filter", "mask",
"{http://www.w3.org/1999/xlink}href", "id", "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
else:
self.url = getattr(self, "url", None)
self.parent = getattr(self, "parent", None)
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
style = self.pop("style", "")
style = self.pop("_style", "") + ";" + self.pop("style", "").lower()
for declaration in style.split(";"):
if ":" in declaration:
name, value = declaration.split(":", 1)
......@@ -132,37 +154,97 @@ class Node(dict):
del self[attribute]
# 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)
if not self.children:
self.children = tuple(
Node(child, self) for child in node
if isinstance(child.tag, basestring))
if parent_children:
# TODO: make children inherit attributes from their new parent
self.children = parent.children
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):
"""Create children and return them."""
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:
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:
anonymous = ElementTree.Element('tspan')
anonymous.text = child.tail
children.append(Node(anonymous, parent = self))
anonymous = Node(ElementTree.Element("tspan"), parent=self)
anonymous.text = handle_white_spaces(child.tail, preserve)
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):
"""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):
"""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:
bytestring = kwargs.pop('bytestring', None)
file_obj = kwargs.pop('file_obj', None)
url = kwargs.pop('url', None)
parent = kwargs.pop('parent', None)
bytestring = kwargs.pop("bytestring", None)
file_obj = kwargs.pop("file_obj", None)
url = kwargs.pop("url", 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:
tree = ElementTree.fromstring(bytestring)
......@@ -172,7 +254,7 @@ class Tree(Node):
if url:
self.url = url
else:
self.url = getattr(file_obj, 'name', None)
self.url = getattr(file_obj, "name", None)
elif url is not None:
if "#" in url:
url, element_id = url.split("#", 1)
......@@ -196,7 +278,7 @@ class Tree(Node):
tree = parent.xml_tree
if element_id:
iterator = (
tree.iter() if hasattr(tree, 'iter')
tree.iter() if hasattr(tree, "iter")
else tree.getiterator())
for element in iterator:
if element.get("id") == element_id:
......@@ -207,9 +289,11 @@ class Tree(Node):
'No tag with id="%s" found.' % element_id)
else:
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)
apply_stylesheets(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
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.
"""
import cairo
import io
try:
import cairocffi as cairo
except ImportError:
import cairo # pycairo
from ..parser import Tree
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 (
node_format, transform, normalize, filter_fill_or_stroke,
apply_matrix_transform, PointError)
node_format, transform, normalize, paint, urls, apply_matrix_transform,
PointError, rect)
from .path import PATH_TAGS
from .tags import TAGS
from .units import size
......@@ -50,7 +55,7 @@ class Surface(object):
surface_class = None
@classmethod
def convert(cls, bytestring = None, **kwargs):
def convert(cls, bytestring=None, **kwargs):
"""Convert a SVG document to the format for this class.
Specify the input by passing one of these:
......@@ -80,7 +85,7 @@ class Surface(object):
if write_to is None:
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.
The rendered content is written to ``output`` which can be a filename,
......@@ -95,10 +100,21 @@ class Surface(object):
self.context_width, self.context_height = None, None
self.cursor_position = 0, 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.gradients = {}
self.patterns = {}
self.masks = {}
self.paths = {}
self.filters = {}
self.page_sizes = []
self._old_parent_node = self.parent_node = None
self.output = output
......@@ -172,7 +188,7 @@ class Surface(object):
"""Draw the root ``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."""
old_font_size = self.font_size
self.font_size = size(self, node.get("font-size", "12pt"))
......@@ -194,18 +210,20 @@ class Surface(object):
self._old_parent_node = self.parent_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))
if opacity < 1:
if mask or 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"):
......@@ -234,6 +252,46 @@ class Surface(object):
miter_limit = float(node.get("stroke-miterlimit", 4))
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:
try:
TAGS[node.tag](self, node)
......@@ -241,53 +299,59 @@ class Surface(object):
# Error in point parsing, do nothing
pass
# Filter
apply_filter_after(self, node)
# 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
# Manage display and visibility
display = node.get("display", "inline") != "none"
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
if "url(#" in (node.get("fill") or ""):
name = filter_fill_or_stroke(node.get("fill"))
gradient_or_pattern(self, node, name)
else:
self.context.save()
paint_source, paint_color = paint(node.get("fill", "black"))
if not gradient_or_pattern(self, node, paint_source):
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.set_source_rgba(*color(paint_color, fill_opacity))
self.context.fill_preserve()
self.context.restore()
# Stroke
self.context.save()
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:
paint_source, paint_color = paint(node.get("stroke"))
if not gradient_or_pattern(self, node, paint_source):
self.context.set_source_rgba(
*color(node.get("stroke"), stroke_opacity))
*color(paint_color, stroke_opacity))
self.context.stroke()
self.context.restore()
elif not visible:
self.context.new_path()
# Draw children
if display and node.tag not in (
"linearGradient", "radialGradient", "marker", "pattern"):
"linearGradient", "radialGradient", "marker", "pattern",
"mask", "clipPath", "filter"):
for child in node.children:
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:
# 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
......
......@@ -200,7 +200,7 @@ COLORS = {
"windowtext": "#000000"}
def color(string = None, opacity = 1):
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)
......
This diff is collapsed.
......@@ -20,9 +20,9 @@ Surface helpers.
"""
import cairo
from math import cos, sin, tan, atan2, radians
from . import cairo
from .units import size
# Python 2/3 management
......@@ -43,17 +43,25 @@ def distance(x1, y1, x2, y2):
return ((x2 - x1) ** 2 + (y2 - y1) ** 2) ** 0.5
def filter_fill_or_stroke(value):
"""Remove unnecessary characters from fill or stroke value."""
def paint(value):
"""Extract from value an uri and a color.
See http://www.w3.org/TR/SVG/painting.html#SpecifyingPaint
"""
if not value:
return
return None, None
content = list(urls(value))[0]
if "url" in value:
if not content.startswith("#"):
return
content = content[1:]
return content
value = value.strip()
if value.startswith("url"):
source = urls(value.split(")")[0])[0][1:]
color = value.split(")", 1)[-1].strip() or None
else:
source = None
color = value.strip() or None
return (source, color)
def node_format(surface, node):
......@@ -68,7 +76,7 @@ def node_format(surface, node):
return width, height, viewbox
def normalize(string = None):
def normalize(string=None):
"""Normalize a string corresponding to an array of various values."""
string = string.replace("-", " -")
string = string.replace(",", " ")
......@@ -77,6 +85,7 @@ def normalize(string = None):
string = string.replace(" ", " ")
string = string.replace("e -", "e-")
string = string.replace("E -", "E-")
values = string.split(" ")
string = ""
......@@ -91,7 +100,7 @@ def normalize(string = None):
return string.strip()
def point(surface, string = None):
def point(surface, string=None):
"""Return ``(x, y, trailing_text)`` from ``string``."""
if not string:
return (0, 0, "")
......@@ -180,8 +189,7 @@ def transform(surface, string):
transformations = string.split(")")
matrix = cairo.Matrix()
for transformation in transformations:
for ttype in (
"scale", "translate", "matrix", "rotate", "skewX",
for ttype in ("scale", "translate", "matrix", "rotate", "skewX",
"skewY"):
if ttype in transformation:
transformation = transformation.replace(ttype, "")
......@@ -189,8 +197,7 @@ def transform(surface, string):
transformation = normalize(transformation).strip() + " "
values = []
while transformation:
value, transformation = \
transformation.split(" ", 1)
value, transformation = transformation.split(" ", 1)
# TODO: manage the x/y sizes here
values.append(size(surface, value))
if ttype == "matrix":
......@@ -236,8 +243,23 @@ def apply_matrix_transform(surface, 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("() ")
if not string:
return []
string = string.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.
"""
import base64
import cairo
import gzip
from io import BytesIO
try:
from urllib import urlopen, unquote
......@@ -32,6 +32,8 @@ except ImportError:
from urllib.request import urlopen
from urllib import parse as urlparse # Python 3
from urllib.parse import unquote_to_bytes
from . import cairo
from .helpers import node_format, size, preserve_ratio
from ..parser import Tree
......@@ -56,7 +58,7 @@ def open_data_url(url):
if header:
semi = header.rfind(";")
if semi >= 0 and "=" not in header[semi:]:
encoding = header[semi + 1:]
encoding = header[semi+1:]
else:
encoding = ""
else:
......@@ -100,8 +102,11 @@ def image(surface, node):
surface.context.clip()
if image_bytes[:4] == b"\x89PNG":
png_bytes = image_bytes
elif image_bytes[:5] == b"\x3csvg ":
png_file = BytesIO(image_bytes)
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.translate(x, y)
if "x" in node:
......@@ -110,7 +115,8 @@ def image(surface, node):
del node["y"]
if "viewBox" in node:
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)
if not tree_width or not tree_height:
tree_width = tree["width"] = width
......@@ -130,13 +136,15 @@ def image(surface, node):
return
else:
try:
from pystacia import read_blob
png_bytes = read_blob(image_bytes).get_blob('png')
from PIL import Image
png_file = BytesIO()
Image.open(BytesIO(image_bytes)).save(png_file, 'PNG')
png_file.seek(0)
except:
# No way to handle the image
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_height = image_surface.get_height()
......
......@@ -136,6 +136,7 @@ def path(surface, node):
elif letter == "c":
# Relative curve
x, y = surface.context.get_current_point()
x1, y1, string = point(surface, string)
x2, y2, string = point(surface, string)
x3, y3, string = point(surface, string)
......@@ -143,6 +144,14 @@ def path(surface, node):
point_angle(x2, y2, x1, y1), point_angle(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":
# Curve
x1, y1, string = point(surface, string)
......@@ -186,7 +195,10 @@ def path(surface, node):
elif letter == "m":
# Current point relative move
x, y, string = point(surface, string)
if surface.context.has_current_point():
surface.context.rel_move_to(x, y)
else:
surface.context.move_to(x, y)
elif letter == "M":
# Current point move
......@@ -215,21 +227,28 @@ def path(surface, node):
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
x, y = surface.context.get_current_point()
x1 = x3 - x2 if last_letter in "csCS" else 0
y1 = y3 - y2 if last_letter in "csCS" 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)
# 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":
# 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
x1 = x3 + (x3 - x2) if last_letter in "csCS" else x
y1 = y3 + (y3 - y2) if last_letter in "csCS" else y
x2, y2, string = point(surface, string)
x3, y3, string = point(surface, string)
node.tangents.extend((
......@@ -294,14 +313,12 @@ def path(surface, node):
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
if node.tangents != [None]:
# node.tangents == [None] means empty path
node.tangents.append(node.tangents[-1])
draw_marker(surface, node, "end")
......@@ -32,8 +32,8 @@ def circle(surface, node):
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"),
size(surface, node.get("cx"), "x"),
size(surface, node.get("cy"), "y"),
r, 0, 2 * pi)
......@@ -83,20 +83,40 @@ def polyline(surface, node):
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:
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)
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)
if rx > width / 2.:
rx = width / 2.
if ry > height / 2.:
ry = height / 2.
# Inspired by Cairo Cookbook
# http://cairographics.org/cookbook/roundedrectangles/
ARC_TO_BEZIER = 4 * (2 ** .5 - 1) / 3
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()
......@@ -21,17 +21,27 @@ Root tag drawer.
"""
from .helpers import preserve_ratio, node_format
from .units import size
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:]
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 = \
preserve_ratio(surface, node)
surface.context.rectangle(0, 0, width, height)
surface.context.clip()
rect_width, rect_height = width, height
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.rectangle(0, 0, rect_width, rect_height)
surface.context.clip()
surface.context.scale(scale_x, scale_y)
surface.context.translate(translate_x, translate_y)
......@@ -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 .path import path
from .shapes import circle, ellipse, line, polygon, polyline, rect
......@@ -30,11 +32,14 @@ from .text import text, text_path, tspan
TAGS = {
"a": tspan,
"circle": circle,
"clipPath": clip_path,
"ellipse": ellipse,
"filter": filter_,
"image": image,
"line": line,
"linearGradient": linear_gradient,
"marker": marker,
"mask": mask,
"path": path,
"pattern": pattern,
"polyline": polyline,
......@@ -44,6 +49,5 @@ TAGS = {
"svg": svg,
"text": text,
"textPath": text_path,
"tref": use,
"tspan": tspan,
"use": use}
......@@ -20,7 +20,6 @@ Text drawers.
"""
import cairo
from math import cos, sin
# Python 2/3 management
......@@ -31,6 +30,7 @@ except ImportError:
from itertools import izip_longest as zip_longest
# pylint: enable=E0611
from . import cairo
from .colors import color
from .helpers import distance, normalize, point_angle
from .units import size
......@@ -79,11 +79,6 @@ def text(surface, node):
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(
......@@ -114,7 +109,6 @@ def text(surface, node):
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")))
......@@ -128,6 +122,7 @@ def text_path(surface, node):
else:
return
surface.context.save()
surface.draw(path, False)
cairo_path = surface.context.copy_path_flat()
surface.context.new_path()
......@@ -137,10 +132,9 @@ def text_path(surface, node):
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:
for letter in node.text:
surface.total_width += (
surface.context.text_extents(letter)[4] + letter_spacing)
point_on_path = point_following_path(cairo_path, surface.total_width)
......@@ -174,27 +168,29 @@ def tspan(surface, node):
y = [size(surface, i, "y")
for i in normalize(node["y"]).strip().split(" ")]
string = (node.text or "").strip()
if not string:
if not node.text:
return
fill = node.get("fill")
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[-1][0], string[len(letters_positions) - 1:])]
(letters_positions[-1][0], node.text[len(letters_positions) - 1:])]
for (x, y), letters in letters_positions:
if x == None:
if x is None:
x = surface.cursor_position[0]
if y == None:
if y is 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":
if node.parent.tag in ("text", "tspan"):
text(surface, node)
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["y"] = str(y + size(surface, node.get("dy"), "y"))
text_path(surface, node)
......
......@@ -30,7 +30,7 @@ UNITS = {
"px": None}
def size(surface, string, reference = "xy"):
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
......
......@@ -96,7 +96,7 @@ class DisplayFrame(wx.Frame):
layercopy.set('height', str(height*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.set('transform', 'scale('+str(self.scale)+')')
......@@ -104,12 +104,14 @@ class DisplayFrame(wx.Frame):
else:
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:
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':
if isinstance(image, str):
......@@ -349,7 +351,7 @@ class SettingsFrame(wx.Frame):
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)
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.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))
......@@ -498,8 +500,8 @@ class SettingsFrame(wx.Frame):
svgSnippet.set('height', height + 'mm')
svgSnippet.set('width', width + 'mm')
svgSnippet.set('viewBox', '0 0 ' + height + ' ' + width)
svgSnippet.set('style','background-color:black')
svgSnippet.set('viewBox', '0 0 ' + width + ' ' + height)
svgSnippet.set('style','background-color:black;fill:white;')
svgSnippet.append(i)
ol += [svgSnippet]
......@@ -534,7 +536,7 @@ class SettingsFrame(wx.Frame):
svgSnippet.set('height', height + '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.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