CairoSVG-1.0.4/0000755000175000017500000000000012267531147013071 5ustar lizelize00000000000000CairoSVG-1.0.4/bin/0000755000175000017500000000000012267531147013641 5ustar lizelize00000000000000CairoSVG-1.0.4/bin/cairosvg0000755000175000017500000000147312245056753015412 0ustar lizelize00000000000000#!/usr/bin/env python # -*- 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 . """ CairoSVG entry point. """ import cairosvg cairosvg.main() CairoSVG-1.0.4/cairosvg/0000755000175000017500000000000012267531147014706 5ustar lizelize00000000000000CairoSVG-1.0.4/cairosvg/surface/0000755000175000017500000000000012267531147016336 5ustar lizelize00000000000000CairoSVG-1.0.4/cairosvg/surface/__init__.py0000644000175000017500000003642712245057114020454 0ustar lizelize00000000000000# -*- 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 . """ Cairo surface creators. """ import io try: import cairocffi as cairo except ImportError: import cairo # pycairo from ..parser import Tree from .colors import color from .defs import ( apply_filter_after, apply_filter_before, gradient_or_pattern, parse_def, paint_mask) from .helpers import ( node_format, transform, normalize, paint, urls, apply_matrix_transform, PointError, rect) from .path import PATH_TAGS from .tags import TAGS from .units import size from . import units class Surface(object): """Abstract base class for CairoSVG surfaces. The ``width`` and ``height`` attributes are in device units (pixels for PNG, else points). The ``context_width`` and ``context_height`` attributes are in user units (i.e. in pixels), they represent the size of the active viewport. """ # Subclasses must either define this or override _create_surface() surface_class = None @classmethod def convert(cls, bytestring=None, **kwargs): """Convert a SVG document to the format for this class. Specify the input by passing one of these: :param bytestring: The SVG source as a byte-string. :param file_obj: A file-like object. :param url: A filename. And the output with: :param write_to: The filename of file-like object where to write the output. If None or not provided, return a byte string. Only ``source`` can be passed as a positional argument, other parameters are keyword-only. """ dpi = kwargs.pop('dpi', 96) write_to = kwargs.pop('write_to', None) kwargs['bytestring'] = bytestring tree = Tree(**kwargs) if write_to is None: output = io.BytesIO() else: output = write_to cls(tree, output, dpi).finish() if write_to is None: return output.getvalue() def __init__(self, tree, output, dpi, 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, a file-like object, ``None`` (render in memory but do not write anything) or the built-in ``bytes`` as a marker. Call the ``.finish()`` method to make sure that the output is actually written. """ self.cairo = None self.context_width, self.context_height = None, None self.cursor_position = [0, 0] self.cursor_d_position = [0, 0] self.text_path_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 self.dpi = dpi self.font_size = size(self, "12pt") self.stroke_and_fill = True width, height, viewbox = node_format(self, tree) # Actual surface dimensions: may be rounded on raster surfaces types self.cairo, self.width, self.height = self._create_surface( width * self.device_units_per_user_units, height * self.device_units_per_user_units) self.page_sizes.append((self.width, self.height)) self.context = cairo.Context(self.cairo) # We must scale the context as the surface size is using physical units self.context.scale( self.device_units_per_user_units, self.device_units_per_user_units) # Initial, non-rounded dimensions self.set_context_size(width, height, viewbox) self.context.move_to(0, 0) self.draw_root(tree) @property def points_per_pixel(self): """Surface resolution.""" return 1 / (self.dpi * units.UNITS["pt"]) @property def device_units_per_user_units(self): """Ratio between Cairo device units and user units. Device units are points for everything but PNG, and pixels for PNG. User units are pixels. """ return self.points_per_pixel def _create_surface(self, width, height): """Create and return ``(cairo_surface, width, height)``.""" # self.surface_class should not be None when called here # pylint: disable=E1102 cairo_surface = self.surface_class(self.output, width, height) # pylint: enable=E1102 return cairo_surface, width, height def set_context_size(self, width, height, viewbox): """Set the Cairo context size, set the SVG viewport size.""" if viewbox: x, y, x_size, y_size = viewbox self.context_width, self.context_height = x_size, y_size x_ratio, y_ratio = width / x_size, height / y_size matrix = cairo.Matrix() if x_ratio > y_ratio: matrix.translate((width - x_size * y_ratio) / 2, 0) matrix.scale(y_ratio, y_ratio) matrix.translate(-x, -y / y_ratio * x_ratio) elif x_ratio < y_ratio: matrix.translate(0, (height - y_size * x_ratio) / 2) matrix.scale(x_ratio, x_ratio) matrix.translate(-x / x_ratio * y_ratio, -y) else: matrix.scale(x_ratio, y_ratio) matrix.translate(-x, -y) apply_matrix_transform(self, matrix) else: self.context_width, self.context_height = width, height def finish(self): """Read the surface content.""" self.cairo.finish() def draw_root(self, node): """Draw the root ``node``.""" self.draw(node) def draw(self, node): """Draw ``node`` and its children.""" old_font_size = self.font_size self.font_size = size(self, node.get("font-size", "12pt")) # Do not draw defs if node.tag == "defs": for child in node.children: parse_def(self, child) return # Do not draw elements with width or height of 0 if (("width" in node and size(self, node["width"]) == 0) or ("height" in node and size(self, node["height"]) == 0)): return node.tangents = [None] node.pending_markers = [] self._old_parent_node = self.parent_node self.parent_node = node 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 mask or opacity < 1: self.context.push_group() self.context.move_to( size(self, node.get("x"), "x"), size(self, node.get("y"), "y")) if node.tag in PATH_TAGS: # Set 1 as default stroke-width if not node.get("stroke-width"): node["stroke-width"] = "1" # Set node's drawing informations if the ``node.tag`` method exists line_cap = node.get("stroke-linecap") if line_cap == "square": self.context.set_line_cap(cairo.LINE_CAP_SQUARE) if line_cap == "round": self.context.set_line_cap(cairo.LINE_CAP_ROUND) join_cap = node.get("stroke-linejoin") if join_cap == "round": self.context.set_line_join(cairo.LINE_JOIN_ROUND) if join_cap == "bevel": self.context.set_line_join(cairo.LINE_JOIN_BEVEL) dash_array = normalize(node.get("stroke-dasharray", "")).split() if dash_array: dashes = [size(self, dash) for dash in dash_array] if sum(dashes): offset = size(self, node.get("stroke-dashoffset")) self.context.set_dash(dashes, offset) miter_limit = float(node.get("stroke-miterlimit", 4)) self.context.set_miter_limit(miter_limit) # Clip rect_values = rect(node.get("clip")) if len(rect_values) == 4: top = size(self, rect_values[0], "y") right = size(self, rect_values[1], "x") bottom = size(self, rect_values[2], "y") left = size(self, rect_values[3], "x") x = size(self, node.get("x"), "x") y = size(self, node.get("y"), "y") width = size(self, node.get("width"), "x") height = 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 = size(self, node.get("x"), "x") y = size(self, node.get("y"), "y") width = size(self, node.get("width"), "x") height = size(self, node.get("height"), "y") self.context.translate(x, y) self.context.scale(width, height) path.tag = "g" self.stroke_and_fill = False self.draw(path) self.stroke_and_fill = True 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) except PointError: # 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 display and visibility display = node.get("display", "inline") != "none" visible = display and (node.get("visibility", "visible") != "hidden") if self.stroke_and_fill and visible and node.tag in TAGS: # Fill 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(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"))) paint_source, paint_color = paint(node.get("stroke")) if not gradient_or_pattern(self, node, paint_source): self.context.set_source_rgba( *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", "mask", "clipPath", "filter"): for child in node.children: self.draw(child) 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) # Clean cursor's position after 'text' tags if node.tag == "text": self.cursor_position = [0, 0] self.cursor_d_position = [0, 0] self.text_path_width = 0 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() self.parent_node = self._old_parent_node self.font_size = old_font_size class MultipageSurface(Surface): """Abstract base class for surfaces that can handle multiple pages.""" def draw_root(self, node): self.width = None self.height = None svg_children = [child for child in node.children if child.tag == 'svg'] if svg_children: # Multi-page for page in svg_children: width, height, viewbox = node_format(self, page) self.context.save() self.set_context_size(width, height, viewbox) width *= self.device_units_per_user_units height *= self.device_units_per_user_units self.page_sizes.append((width, height)) self.cairo.set_size(width, height) self.draw(page) self.context.restore() self.cairo.show_page() else: self.draw(node) class PDFSurface(MultipageSurface): """A surface that writes in PDF format.""" surface_class = cairo.PDFSurface class PSSurface(MultipageSurface): """A surface that writes in PostScript format.""" surface_class = cairo.PSSurface class PNGSurface(Surface): """A surface that writes in PNG format.""" device_units_per_user_units = 1 def _create_surface(self, width, height): """Create and return ``(cairo_surface, width, height)``.""" width = int(width) height = int(height) cairo_surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height) return cairo_surface, width, height def finish(self): """Read the PNG surface content.""" if self.output is not None: self.cairo.write_to_png(self.output) return super(PNGSurface, self).finish() class SVGSurface(Surface): """A surface that writes in SVG format. It may seem pointless to render SVG to SVG, but this can be used with ``output=None`` to get a vector-based single page cairo surface. """ surface_class = cairo.SVGSurface CairoSVG-1.0.4/cairosvg/surface/colors.py0000644000175000017500000002031212006767442020207 0ustar lizelize00000000000000# -*- 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 . """ SVG colors. """ COLORS = { "aliceblue": "rgb(240, 248, 255)", "antiquewhite": "rgb(250, 235, 215)", "aqua": "rgb(0, 255, 255)", "aquamarine": "rgb(127, 255, 212)", "azure": "rgb(240, 255, 255)", "beige": "rgb(245, 245, 220)", "bisque": "rgb(255, 228, 196)", "black": "rgb(0, 0, 0)", "blanchedalmond": "rgb(255, 235, 205)", "blue": "rgb(0, 0, 255)", "blueviolet": "rgb(138, 43, 226)", "brown": "rgb(165, 42, 42)", "burlywood": "rgb(222, 184, 135)", "cadetblue": "rgb(95, 158, 160)", "chartreuse": "rgb(127, 255, 0)", "chocolate": "rgb(210, 105, 30)", "coral": "rgb(255, 127, 80)", "cornflowerblue": "rgb(100, 149, 237)", "cornsilk": "rgb(255, 248, 220)", "crimson": "rgb(220, 20, 60)", "cyan": "rgb(0, 255, 255)", "darkblue": "rgb(0, 0, 139)", "darkcyan": "rgb(0, 139, 139)", "darkgoldenrod": "rgb(184, 134, 11)", "darkgray": "rgb(169, 169, 169)", "darkgreen": "rgb(0, 100, 0)", "darkgrey": "rgb(169, 169, 169)", "darkkhaki": "rgb(189, 183, 107)", "darkmagenta": "rgb(139, 0, 139)", "darkolivegreen": "rgb(85, 107, 47)", "darkorange": "rgb(255, 140, 0)", "darkorchid": "rgb(153, 50, 204)", "darkred": "rgb(139, 0, 0)", "darksalmon": "rgb(233, 150, 122)", "darkseagreen": "rgb(143, 188, 143)", "darkslateblue": "rgb(72, 61, 139)", "darkslategray": "rgb(47, 79, 79)", "darkslategrey": "rgb(47, 79, 79)", "darkturquoise": "rgb(0, 206, 209)", "darkviolet": "rgb(148, 0, 211)", "deeppink": "rgb(255, 20, 147)", "deepskyblue": "rgb(0, 191, 255)", "dimgray": "rgb(105, 105, 105)", "dimgrey": "rgb(105, 105, 105)", "dodgerblue": "rgb(30, 144, 255)", "firebrick": "rgb(178, 34, 34)", "floralwhite": "rgb(255, 250, 240)", "forestgreen": "rgb(34, 139, 34)", "fuchsia": "rgb(255, 0, 255)", "gainsboro": "rgb(220, 220, 220)", "ghostwhite": "rgb(248, 248, 255)", "gold": "rgb(255, 215, 0)", "goldenrod": "rgb(218, 165, 32)", "gray": "rgb(128, 128, 128)", "grey": "rgb(128, 128, 128)", "green": "rgb(0, 128, 0)", "greenyellow": "rgb(173, 255, 47)", "honeydew": "rgb(240, 255, 240)", "hotpink": "rgb(255, 105, 180)", "indianred": "rgb(205, 92, 92)", "indigo": "rgb(75, 0, 130)", "ivory": "rgb(255, 255, 240)", "khaki": "rgb(240, 230, 140)", "lavender": "rgb(230, 230, 250)", "lavenderblush": "rgb(255, 240, 245)", "lawngreen": "rgb(124, 252, 0)", "lemonchiffon": "rgb(255, 250, 205)", "lightblue": "rgb(173, 216, 230)", "lightcoral": "rgb(240, 128, 128)", "lightcyan": "rgb(224, 255, 255)", "lightgoldenrodyellow": "rgb(250, 250, 210)", "lightgray": "rgb(211, 211, 211)", "lightgreen": "rgb(144, 238, 144)", "lightgrey": "rgb(211, 211, 211)", "lightpink": "rgb(255, 182, 193)", "lightsalmon": "rgb(255, 160, 122)", "lightseagreen": "rgb(32, 178, 170)", "lightskyblue": "rgb(135, 206, 250)", "lightslategray": "rgb(119, 136, 153)", "lightslategrey": "rgb(119, 136, 153)", "lightsteelblue": "rgb(176, 196, 222)", "lightyellow": "rgb(255, 255, 224)", "lime": "rgb(0, 255, 0)", "limegreen": "rgb(50, 205, 50)", "linen": "rgb(250, 240, 230)", "magenta": "rgb(255, 0, 255)", "maroon": "rgb(128, 0, 0)", "mediumaquamarine": "rgb(102, 205, 170)", "mediumblue": "rgb(0, 0, 205)", "mediumorchid": "rgb(186, 85, 211)", "mediumpurple": "rgb(147, 112, 219)", "mediumseagreen": "rgb(60, 179, 113)", "mediumslateblue": "rgb(123, 104, 238)", "mediumspringgreen": "rgb(0, 250, 154)", "mediumturquoise": "rgb(72, 209, 204)", "mediumvioletred": "rgb(199, 21, 133)", "midnightblue": "rgb(25, 25, 112)", "mintcream": "rgb(245, 255, 250)", "mistyrose": "rgb(255, 228, 225)", "moccasin": "rgb(255, 228, 181)", "navajowhite": "rgb(255, 222, 173)", "navy": "rgb(0, 0, 128)", "oldlace": "rgb(253, 245, 230)", "olive": "rgb(128, 128, 0)", "olivedrab": "rgb(107, 142, 35)", "orange": "rgb(255, 165, 0)", "orangered": "rgb(255, 69, 0)", "orchid": "rgb(218, 112, 214)", "palegoldenrod": "rgb(238, 232, 170)", "palegreen": "rgb(152, 251, 152)", "paleturquoise": "rgb(175, 238, 238)", "palevioletred": "rgb(219, 112, 147)", "papayawhip": "rgb(255, 239, 213)", "peachpuff": "rgb(255, 218, 185)", "peru": "rgb(205, 133, 63)", "pink": "rgb(255, 192, 203)", "plum": "rgb(221, 160, 221)", "powderblue": "rgb(176, 224, 230)", "purple": "rgb(128, 0, 128)", "red": "rgb(255, 0, 0)", "rosybrown": "rgb(188, 143, 143)", "royalblue": "rgb(65, 105, 225)", "saddlebrown": "rgb(139, 69, 19)", "salmon": "rgb(250, 128, 114)", "sandybrown": "rgb(244, 164, 96)", "seagreen": "rgb(46, 139, 87)", "seashell": "rgb(255, 245, 238)", "sienna": "rgb(160, 82, 45)", "silver": "rgb(192, 192, 192)", "skyblue": "rgb(135, 206, 235)", "slateblue": "rgb(106, 90, 205)", "slategray": "rgb(112, 128, 144)", "slategrey": "rgb(112, 128, 144)", "snow": "rgb(255, 250, 250)", "springgreen": "rgb(0, 255, 127)", "steelblue": "rgb(70, 130, 180)", "tan": "rgb(210, 180, 140)", "teal": "rgb(0, 128, 128)", "thistle": "rgb(216, 191, 216)", "tomato": "rgb(255, 99, 71)", "turquoise": "rgb(64, 224, 208)", "violet": "rgb(238, 130, 238)", "wheat": "rgb(245, 222, 179)", "white": "rgb(255, 255, 255)", "whitesmoke": "rgb(245, 245, 245)", "yellow": "rgb(255, 255, 0)", "yellowgreen": "rgb(154, 205, 50)", "activeborder": "#0000ff", "activecaption": "#0000ff", "appworkspace": "#ffffff", "background": "#ffffff", "buttonface": "#000000", "buttonhighlight": "#cccccc", "buttonshadow": "#333333", "buttontext": "#000000", "captiontext": "#000000", "graytext": "#333333", "highlight": "#0000ff", "highlighttext": "#cccccc", "inactiveborder": "#333333", "inactivecaption": "#cccccc", "inactivecaptiontext": "#333333", "infobackground": "#cccccc", "infotext": "#000000", "menu": "#cccccc", "menutext": "#333333", "scrollbar": "#cccccc", "threeddarkshadow": "#333333", "threedface": "#cccccc", "threedhighlight": "#ffffff", "threedlightshadow": "#333333", "threedshadow": "#333333", "window": "#cccccc", "windowframe": "#cccccc", "windowtext": "#000000"} def color(string=None, opacity=1): """Replace ``string`` representing a color by a RGBA tuple.""" if not string or string in ("none", "transparent"): return (0, 0, 0, 0) string = string.strip().lower() if string in COLORS: string = COLORS[string] if string.startswith("rgba"): r, g, b, a = tuple( float(i.strip(" %")) * 2.55 if "%" in i else float(i) for i in string.strip(" rgba()").split(",")) return r / 255, g / 255, b / 255, a * opacity elif string.startswith("rgb"): r, g, b = tuple( float(i.strip(" %")) / 100 if "%" in i else float(i) / 255 for i in string.strip(" rgb()").split(",")) return r, g, b, opacity if len(string) in (4, 5): string = "#" + "".join(2 * char for char in string[1:]) if len(string) == 9: opacity *= int(string[7:9], 16) / 255 try: plain_color = tuple( int(value, 16) / 255. for value in ( string[1:3], string[3:5], string[5:7])) except ValueError: # Unknown color, return black return (0, 0, 0, 1) else: return plain_color + (opacity,) CairoSVG-1.0.4/cairosvg/surface/defs.py0000644000175000017500000003747112245061576017645 0ustar lizelize00000000000000# -*- 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 . """ Externally defined elements managers. This module handles gradients and patterns. """ from math import radians from . import cairo from .colors import color from .helpers import node_format, preserve_ratio, paint, urls, transform from .shapes import rect from .units import size 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): """Parse the SVG definitions.""" for def_type in ( "marker", "gradient", "pattern", "path", "mask", "filter"): if def_type in node.tag.lower(): getattr(surface, def_type + "s")[node["id"]] = node def gradient_or_pattern(surface, node, name): """Gradient or pattern color.""" if name in surface.gradients: update_def_href(surface, name, surface.gradients) return draw_gradient(surface, node, name) elif name in surface.patterns: update_def_href(surface, name, surface.patterns) return draw_pattern(surface, node, name) def marker(surface, node): """Store a marker definition.""" 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): """Store a linear gradient definition.""" parse_def(surface, node) def radial_gradient(surface, node): """Store a radial gradient definition.""" parse_def(surface, node) def pattern(surface, node): """Store a pattern definition.""" parse_def(surface, node) def 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 = size(surface, node.get("x"), "x") y = size(surface, node.get("y"), "y") width = size(surface, node.get("width"), "x") height = 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"] = size(surface, mask_node.get("x", "-10%"), width_ref) mask_node["y"] = size(surface, mask_node.get("y", "-10%"), height_ref) mask_node["height"] = size( surface, mask_node.get("height", "120%"), width_ref) mask_node["width"] = 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): """Gradients colors.""" gradient_node = surface.gradients[name] transform(surface, gradient_node.get("gradientTransform")) if gradient_node.get("gradientUnits") == "userSpaceOnUse": width_ref, height_ref = "x", "y" diagonal_ref = "xy" else: x = size(surface, node.get("x"), "x") y = size(surface, node.get("y"), "y") width = size(surface, node.get("width"), "x") height = size(surface, node.get("height"), "y") width_ref = height_ref = diagonal_ref = 1 if gradient_node.tag == "linearGradient": x1 = size(surface, gradient_node.get("x1", "0%"), width_ref) x2 = size(surface, gradient_node.get("x2", "100%"), width_ref) y1 = size(surface, gradient_node.get("y1", "0%"), height_ref) y2 = size(surface, gradient_node.get("y2", "0%"), height_ref) gradient_pattern = cairo.LinearGradient(x1, y1, x2, y2) elif gradient_node.tag == "radialGradient": r = size(surface, gradient_node.get("r", "50%"), diagonal_ref) cx = size(surface, gradient_node.get("cx", "50%"), width_ref) cy = size(surface, gradient_node.get("cy", "50%"), height_ref) fx = size(surface, gradient_node.get("fx", str(cx)), width_ref) fy = size(surface, gradient_node.get("fy", str(cy)), height_ref) gradient_pattern = cairo.RadialGradient(fx, fy, 0, cx, cy, r) if gradient_node.get("gradientUnits") != "userSpaceOnUse": gradient_pattern.set_matrix(cairo.Matrix( 1 / width, 0, 0, 1 / height, - x / width, - y / height)) gradient_pattern.set_extend(getattr( cairo, "EXTEND_%s" % node.get("spreadMethod", "pad").upper())) offset = 0 for child in gradient_node.children: offset = max(offset, size(surface, child.get("offset"), 1)) stop_color = color( child.get("stop-color", "black"), float(child.get("stop-opacity", 1))) gradient_pattern.add_color_stop_rgba(offset, *stop_color) gradient_pattern.set_extend(getattr( cairo, "EXTEND_%s" % gradient_node.get("spreadMethod", "pad").upper())) surface.context.set_source(gradient_pattern) return True def draw_pattern(surface, node, name): """Draw a pattern image.""" pattern_node = surface.patterns[name] pattern_node.tag = "g" 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 = size(surface, pattern_node.get("x"), "x") y = size(surface, pattern_node.get("y"), "y") pattern_width = size(surface, pattern_node.get("width", 0), 1) pattern_height = size(surface, pattern_node.get("height", 0), 1) else: width = size(surface, node.get("width"), "x") height = size(surface, node.get("height"), "y") x = size(surface, pattern_node.get("x"), 1) * width y = 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 pattern_surface = SVGSurface(pattern_node, None, surface.dpi, surface) pattern_pattern = cairo.SurfacePattern(pattern_surface.cairo) 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) return True def draw_marker(surface, node, position="mid"): """Draw a marker.""" if position == "start": node.markers = { "start": list(urls(node.get("marker-start", ""))), "mid": list(urls(node.get("marker-mid", ""))), "end": list(urls(node.get("marker-end", "")))} all_markers = list(urls(node.get("marker", ""))) for markers_list in node.markers.values(): markers_list.extend(all_markers) pending_marker = ( surface.context.get_current_point(), node.markers[position]) if position == "start": node.pending_markers.append(pending_marker) return elif position == "end": node.pending_markers.append(pending_marker) while node.pending_markers: next_point, markers = node.pending_markers.pop(0) angle1 = node.tangents.pop(0) angle2 = node.tangents.pop(0) if angle1 is None: angle1 = angle2 for active_marker in markers: if not active_marker.startswith("#"): continue active_marker = active_marker[1:] if active_marker in surface.markers: marker_node = surface.markers[active_marker] angle = marker_node.get("orient", "0") if angle == "auto": angle = float(angle1 + angle2) / 2 else: angle = radians(float(angle)) temp_path = surface.context.copy_path() current_x, current_y = next_point if node.get("markerUnits") == "userSpaceOnUse": base_scale = 1 else: base_scale = size( surface, surface.parent_node.get("stroke-width")) # Returns 4 values scale_x, scale_y, translate_x, translate_y = \ preserve_ratio(surface, marker_node) width, height, viewbox = node_format(surface, marker_node) if viewbox: viewbox_width = viewbox[2] viewbox_height = viewbox[3] else: viewbox_width = width or 0 viewbox_height = height or 0 surface.context.new_path() for child in marker_node.children: surface.context.save() surface.context.translate(current_x, current_y) surface.context.rotate(angle) surface.context.scale( base_scale / viewbox_width * float(scale_x), base_scale / viewbox_height * float(scale_y)) surface.context.translate(translate_x, translate_y) surface.draw(child) surface.context.restore() surface.context.append_path(temp_path) if position == "mid": node.pending_markers.append(pending_marker) def 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 = size(surface, node.get("width"), "x") height = 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 = size(surface, node.get("x"), "x") y = size(surface, node.get("y"), "y") width = size(surface, node.get("width"), "x") height = size(surface, node.get("height"), "y") else: x, y, width, height = 0, 0, 1, 1 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(surface, dict(x=x, y=y, width=width, height=height)) 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): """Draw the content of another SVG file.""" surface.context.save() surface.context.translate( size(surface, node.get("x"), "x"), size(surface, node.get("y"), "y")) if "x" in node: del node["x"] if "y" in node: del node["y"] if "viewBox" in node: del node["viewBox"] if "mask" in node: del node["mask"] href = node.get("{http://www.w3.org/1999/xlink}href") tree_urls = urls(href) 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.draw(tree) node.pop("fill", None) node.pop("stroke", None) surface.context.restore() # Restore twice, because draw does not restore at the end of svg tags if tree.tag != "use": surface.context.restore() CairoSVG-1.0.4/cairosvg/surface/helpers.py0000644000175000017500000002303412245057114020345 0ustar lizelize00000000000000# -*- 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 . """ Surface helpers. """ from math import cos, sin, tan, atan2, radians from . import cairo from .units import size # Python 2/3 management # pylint: disable=C0103 try: Error = cairo.Error except AttributeError: Error = SystemError # pylint: enable=C0103 class PointError(Exception): """Exception raised when parsing a point fails.""" def distance(x1, y1, x2, y2): """Get the distance between two points.""" return ((x2 - x1) ** 2 + (y2 - y1) ** 2) ** 0.5 def paint(value): """Extract from value an uri and a color. See http://www.w3.org/TR/SVG/painting.html#SpecifyingPaint """ if not value: return None, None 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): """Return ``(width, height, viewbox)`` of ``node``.""" width = size(surface, node.get("width"), "x") height = size(surface, node.get("height"), "y") viewbox = node.get("viewBox") if viewbox: viewbox = tuple(float(position) for position in viewbox.split()) width = width or viewbox[2] height = height or viewbox[3] return width, height, viewbox def normalize(string=None): """Normalize a string corresponding to an array of various values.""" string = string.replace("-", " -") string = string.replace(",", " ") while " " in string: string = string.replace(" ", " ") string = string.replace("e -", "e-") string = string.replace("E -", "E-") values = string.split(" ") string = "" for value in values: if value.count(".") > 1: numbers = value.split(".") string += "%s.%s " % (numbers.pop(0), numbers.pop(0)) string += ".%s " % " .".join(numbers) else: string += value + " " return string.strip() def point(surface, string=None): """Return ``(x, y, trailing_text)`` from ``string``.""" if not string: return (0, 0, "") try: x, y, string = (string.strip() + " ").split(" ", 2) except ValueError: raise PointError("The point cannot be found in string %s" % string) return size(surface, x, "x"), size(surface, y, "y"), string def point_angle(cx, cy, px, py): """Return angle between x axis and point knowing given center.""" return atan2(py - cy, px - cx) def preserve_ratio(surface, node): """Manage the ratio preservation.""" if node.tag == "marker": scale_x = size(surface, node.get("markerWidth", "3"), "x") scale_y = size(surface, node.get("markerHeight", "3"), "y") translate_x = -size(surface, node.get("refX")) translate_y = -size(surface, node.get("refY")) elif node.tag in ("svg", "image"): width, height, _ = node_format(surface, node) scale_x = width / node.image_width scale_y = height / node.image_height align = node.get("preserveAspectRatio", "xMidYMid").split(" ")[0] if align == "none": return scale_x, scale_y, 0, 0 else: mos_properties = node.get("preserveAspectRatio", "").split() meet_or_slice = ( mos_properties[1] if len(mos_properties) > 1 else None) if meet_or_slice == "slice": scale_value = max(scale_x, scale_y) else: scale_value = min(scale_x, scale_y) scale_x = scale_y = scale_value x_position = align[1:4].lower() y_position = align[5:].lower() if x_position == "min": translate_x = 0 if y_position == "min": translate_y = 0 if x_position == "mid": translate_x = (width / scale_x - node.image_width) / 2. if y_position == "mid": translate_y = (height / scale_y - node.image_height) / 2. if x_position == "max": translate_x = width / scale_x - node.image_width if y_position == "max": translate_y = height / scale_y - node.image_height return scale_x, scale_y, translate_x, translate_y def quadratic_points(x1, y1, x2, y2, x3, y3): """Return the quadratic points to create quadratic curves.""" xq1 = x2 * 2 / 3 + x1 / 3 yq1 = y2 * 2 / 3 + y1 / 3 xq2 = x2 * 2 / 3 + x3 / 3 yq2 = y2 * 2 / 3 + y3 / 3 return xq1, yq1, xq2, yq2, x3, y3 def rotate(x, y, angle): """Rotate a point of an angle around the origin point.""" return x * cos(angle) - y * sin(angle), y * cos(angle) + x * sin(angle) def transform(surface, string): """Update ``surface`` matrix according to transformation ``string``.""" if not string: return transformations = string.split(")") matrix = cairo.Matrix() for transformation in transformations: for ttype in ("scale", "translate", "matrix", "rotate", "skewX", "skewY"): if ttype in transformation: transformation = transformation.replace(ttype, "") transformation = transformation.replace("(", "") transformation = normalize(transformation).strip() + " " values = [] while transformation: value, transformation = transformation.split(" ", 1) # TODO: manage the x/y sizes here values.append(size(surface, value)) if ttype == "matrix": matrix = cairo.Matrix(*values).multiply(matrix) elif ttype == "rotate": angle = radians(float(values.pop(0))) x, y = values or (0, 0) matrix.translate(x, y) matrix.rotate(angle) matrix.translate(-x, -y) elif ttype == "skewX": tangent = tan(radians(float(values[0]))) matrix = \ cairo.Matrix(1, 0, tangent, 1, 0, 0).multiply(matrix) elif ttype == "skewY": tangent = tan(radians(float(values[0]))) matrix = \ cairo.Matrix(1, tangent, 0, 1, 0, 0).multiply(matrix) elif ttype == "translate": if len(values) == 1: values += (0,) matrix.translate(*values) elif ttype == "scale": if len(values) == 1: values = 2 * values matrix.scale(*values) apply_matrix_transform(surface, matrix) def apply_matrix_transform(surface, matrix): try: matrix.invert() except Error: # Matrix not invertible, clip the surface to an empty path active_path = surface.context.copy_path() surface.context.new_path() surface.context.clip() surface.context.append_path(active_path) else: matrix.invert() surface.context.transform(matrix) def urls(string): """Parse a comma-separated list of url() strings.""" 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 [] def rotations(node): """Retrieves the original rotations of a `text` or `tspan` node.""" if "rotate" in node: original_rotate = [ float(i) for i in normalize(node["rotate"]).strip().split(" ")] return original_rotate return [] def pop_rotation(node, original_rotate, rotate): """Removes the rotations of a node that are already used.""" node["rotate"] = " ".join( str(rotate.pop(0) if rotate else original_rotate[-1]) for i in range(len(node.text))) def zip_letters(xl, yl, dxl, dyl, rl, word): """Returns a list with the current letter's positions (x, y and rotation). E.g.: for letter 'L' with positions x = 10, y = 20 and rotation = 30: >>> [[10, 20, 30], 'L'] Store the last value of each position and pop the first one in order to avoid setting an x,y or rotation value that have already been used. """ return ( ([pl.pop(0) if pl else None for pl in (xl, yl, dxl, dyl, rl)], char) for char in word) def flatten(node): flattened_text = [node.text or ""] for child in list(node): flattened_text.append(flatten(child)) flattened_text.append(child.tail or "") node.remove(child) return "".join(flattened_text) CairoSVG-1.0.4/cairosvg/surface/image.py0000644000175000017500000001273112245056753017777 0ustar lizelize00000000000000# -*- 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 . """ Images manager. """ import base64 import gzip from io import BytesIO try: from urllib import urlopen, unquote import urlparse unquote_to_bytes = lambda data: unquote( data.encode('ascii') if isinstance(data, unicode) else data) except ImportError: from urllib.request import urlopen from urllib import parse as urlparse # Python 3 from urllib.parse import unquote_to_bytes from . import cairo from .helpers import node_format, size, preserve_ratio from ..parser import Tree def open_data_url(url): """Decode URLs with the 'data' scheme. urllib can handle them in Python 2, but that is broken in Python 3. Inspired from Python 2.7.2’s urllib.py. """ # syntax of data URLs: # dataurl := "data:" [ mediatype ] [ ";base64" ] "," data # mediatype := [ type "/" subtype ] *( ";" parameter ) # data := *urlchar # parameter := attribute "=" value try: header, data = url.split(",", 1) except ValueError: raise IOError("bad data URL") header = header[5:] # len("data:") == 5 if header: semi = header.rfind(";") if semi >= 0 and "=" not in header[semi:]: encoding = header[semi+1:] else: encoding = "" else: encoding = "" data = unquote_to_bytes(data) if encoding == "base64": missing_padding = 4 - len(data) % 4 if missing_padding: data += b"=" * missing_padding return base64.decodestring(data) return data def image(surface, node): """Draw an image ``node``.""" url = node.get("{http://www.w3.org/1999/xlink}href") if not url: return if url.startswith("data:"): image_bytes = open_data_url(url) else: base_url = node.get("{http://www.w3.org/XML/1998/namespace}base") if base_url: url = urlparse.urljoin(base_url, url) if node.url: url = urlparse.urljoin(node.url, url) if urlparse.urlparse(url).scheme: input_ = urlopen(url) else: input_ = open(url, 'rb') # filename image_bytes = input_.read() if len(image_bytes) < 5: return x, y = size(surface, node.get("x"), "x"), size(surface, node.get("y"), "y") width = size(surface, node.get("width"), "x") height = size(surface, node.get("height"), "y") surface.context.rectangle(x, y, width, height) surface.context.clip() if image_bytes[:4] == b"\x89PNG": png_file = BytesIO(image_bytes) elif (image_bytes[:5] in (b". """ Paths manager. """ from math import pi, radians from .defs import draw_marker from .helpers import normalize, point, point_angle, quadratic_points, rotate from .units import size PATH_LETTERS = "achlmqstvzACHLMQSTVZ" PATH_TAGS = ( "circle", "ellipse", "line", "path", "polygon", "polyline", "rect") def path(surface, node): """Draw a path ``node``.""" string = node.get("d", "") if not string.strip(): # Don't draw empty paths at all return draw_marker(surface, node, "start") for letter in PATH_LETTERS: string = string.replace(letter, " %s " % letter) last_letter = None string = normalize(string) while string: string = string.strip() if string.split(" ", 1)[0] in PATH_LETTERS: letter, string = (string + " ").split(" ", 1) elif letter == "M": letter = "L" elif letter == "m": letter = "l" if letter in "aA": # Elliptic curve x1, y1 = surface.context.get_current_point() rx, ry, string = point(surface, string) rotation, string = string.split(" ", 1) rotation = radians(float(rotation)) # The large and sweep values are not always separated from the # following values, here is the crazy parser large, string = string[0], string[1:].strip() while not large[-1].isdigit(): large, string = large + string[0], string[1:].strip() sweep, string = string[0], string[1:].strip() while not sweep[-1].isdigit(): sweep, string = sweep + string[0], string[1:].strip() large, sweep = bool(int(large)), bool(int(sweep)) x3, y3, string = point(surface, string) if letter == "A": # Absolute x3 and y3, convert to relative x3 -= x1 y3 -= y1 # rx=0 or ry=0 means straight line if not rx or not ry: string = "l %f %f %s" % (x3, y3, string) continue radii_ratio = ry / rx # Cancel the rotation of the second point xe, ye = rotate(x3, y3, -rotation) ye /= radii_ratio # Find the angle between the second point and the x axis angle = point_angle(0, 0, xe, ye) # Put the second point onto the x axis xe = (xe ** 2 + ye ** 2) ** .5 ye = 0 # Update the x radius if it is too small rx = max(rx, xe / 2) # Find one circle centre xc = xe / 2 yc = (rx ** 2 - xc ** 2) ** .5 # Choose between the two circles according to flags if not (large ^ sweep): yc = -yc # Define the arc sweep arc = \ surface.context.arc if sweep else surface.context.arc_negative # Put the second point and the center back to their positions xe, ye = rotate(xe, 0, angle) xc, yc = rotate(xc, yc, angle) # Find the drawing angles angle1 = point_angle(xc, yc, 0, 0) angle2 = point_angle(xc, yc, xe, ye) # Store the tangent angles node.tangents.extend((-angle1, -angle2)) # Draw the arc surface.context.save() surface.context.translate(x1, y1) surface.context.rotate(rotation) surface.context.scale(1, radii_ratio) arc(xc, yc, rx, angle1, angle2) surface.context.restore() elif letter == "c": # Relative curve 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) 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 == "C": # Curve x1, y1, string = point(surface, string) x2, y2, string = point(surface, string) x3, y3, string = point(surface, string) node.tangents.extend(( point_angle(x2, y2, x1, y1), point_angle(x2, y2, x3, y3))) surface.context.curve_to(x1, y1, x2, y2, x3, y3) elif letter == "h": # Relative horizontal line x, string = (string + " ").split(" ", 1) old_x, old_y = surface.context.get_current_point() angle = 0 if size(surface, x, "x") > 0 else pi node.tangents.extend((-angle, angle)) surface.context.rel_line_to(size(surface, x, "x"), 0) elif letter == "H": # Horizontal line x, string = (string + " ").split(" ", 1) old_x, old_y = surface.context.get_current_point() angle = 0 if size(surface, x, "x") > old_x else pi node.tangents.extend((-angle, angle)) surface.context.line_to(size(surface, x, "x"), old_y) elif letter == "l": # Relative straight line x, y, string = point(surface, string) angle = point_angle(0, 0, x, y) node.tangents.extend((-angle, angle)) surface.context.rel_line_to(x, y) elif letter == "L": # Straight line x, y, string = point(surface, string) old_x, old_y = surface.context.get_current_point() angle = point_angle(old_x, old_y, x, y) node.tangents.extend((-angle, angle)) surface.context.line_to(x, y) elif letter == "m": # Current point relative move x, y, string = point(surface, string) 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 x, y, string = point(surface, string) surface.context.move_to(x, y) elif letter == "q": # Relative quadratic curve x1, y1 = 0, 0 x2, y2, string = point(surface, string) x3, y3, string = point(surface, string) xq1, yq1, xq2, yq2, xq3, yq3 = quadratic_points( x1, y1, x2, y2, x3, y3) surface.context.rel_curve_to(xq1, yq1, xq2, yq2, xq3, yq3) node.tangents.extend((0, 0)) elif letter == "Q": # Quadratic curve x1, y1 = surface.context.get_current_point() x2, y2, string = point(surface, string) x3, y3, string = point(surface, string) xq1, yq1, xq2, yq2, xq3, yq3 = quadratic_points( x1, y1, x2, y2, x3, y3) surface.context.curve_to(xq1, yq1, xq2, yq2, xq3, yq3) node.tangents.extend((0, 0)) elif letter == "s": # Relative smooth curve 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 x, y = surface.context.get_current_point() 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(( point_angle(x2, y2, x1, y1), point_angle(x2, y2, x3, y3))) surface.context.curve_to(x1, y1, x2, y2, x3, y3) elif letter == "t": # Relative quadratic curve end if last_letter not in "QqTt": x2, y2, x3, y3 = 0, 0, 0, 0 elif last_letter in "QT": x2 -= x1 y2 -= y1 x3 -= x1 y3 -= y1 x2 = x3 - x2 y2 = y3 - y2 x1, y1 = 0, 0 x3, y3, string = point(surface, string) xq1, yq1, xq2, yq2, xq3, yq3 = quadratic_points( x1, y1, x2, y2, x3, y3) node.tangents.extend((0, 0)) surface.context.rel_curve_to(xq1, yq1, xq2, yq2, xq3, yq3) elif letter == "T": # Quadratic curve end abs_x, abs_y = surface.context.get_current_point() if last_letter not in "QqTt": x2, y2, x3, y3 = abs_x, abs_y, abs_x, abs_y elif last_letter in "qt": x2 += x1 y2 += y1 x2 = 2 * abs_x - x2 y2 = 2 * abs_y - y2 x1, y1 = abs_x, abs_y x3, y3, string = point(surface, string) xq1, yq1, xq2, yq2, xq3, yq3 = quadratic_points( x1, y1, x2, y2, x3, y3) node.tangents.extend((0, 0)) surface.context.curve_to(xq1, yq1, xq2, yq2, xq3, yq3) elif letter == "v": # Relative vertical line y, string = (string + " ").split(" ", 1) old_x, old_y = surface.context.get_current_point() angle = pi / 2 if size(surface, y, "y") > 0 else -pi / 2 node.tangents.extend((-angle, angle)) surface.context.rel_line_to(0, size(surface, y, "y")) elif letter == "V": # Vertical line y, string = (string + " ").split(" ", 1) old_x, old_y = surface.context.get_current_point() angle = pi / 2 if size(surface, y, "y") > 0 else -pi / 2 node.tangents.extend((-angle, angle)) surface.context.line_to(old_x, size(surface, y, "y")) elif letter in "zZ": # End of path node.tangents.extend((0, 0)) surface.context.close_path() string = string.strip() if 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") CairoSVG-1.0.4/cairosvg/surface/shapes.py0000644000175000017500000000757112245056753020206 0ustar lizelize00000000000000# -*- 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 . """ Shapes drawers. """ from math import pi from .helpers import normalize, point, size def circle(surface, node): """Draw a circle ``node`` on ``surface``.""" r = size(surface, node.get("r")) if not r: return surface.context.new_sub_path() surface.context.arc( size(surface, node.get("cx"), "x"), size(surface, node.get("cy"), "y"), r, 0, 2 * pi) def ellipse(surface, node): """Draw an ellipse ``node`` on ``surface``.""" rx = size(surface, node.get("rx"), "x") ry = size(surface, node.get("ry"), "y") if not rx or not ry: return ratio = ry / rx surface.context.new_sub_path() surface.context.save() surface.context.scale(1, ratio) surface.context.arc( size(surface, node.get("x"), "x") + size(surface, node.get("cx"), "x"), (size(surface, node.get("y"), "y") + size(surface, node.get("cy"), "y")) / ratio, size(surface, node.get("rx"), "x"), 0, 2 * pi) surface.context.restore() def line(surface, node): """Draw a line ``node``.""" x1, y1, x2, y2 = tuple( size(surface, node.get(position), position[0]) for position in ("x1", "y1", "x2", "y2")) surface.context.move_to(x1, y1) surface.context.line_to(x2, y2) def polygon(surface, node): """Draw a polygon ``node`` on ``surface``.""" polyline(surface, node) surface.context.close_path() def polyline(surface, node): """Draw a polyline ``node``.""" points = normalize(node.get("points")) if points: x, y, points = point(surface, points) surface.context.move_to(x, y) while points: x, y, points = point(surface, points) surface.context.line_to(x, y) def rect(surface, node): """Draw a rect ``node`` on ``surface``.""" 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") 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: 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() CairoSVG-1.0.4/cairosvg/surface/svg.py0000644000175000017500000000352012245062277017506 0ustar lizelize00000000000000# -*- 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 . """ Root tag drawer. """ from .helpers import preserve_ratio, node_format from .units import size def svg(surface, node): """Draw a svg ``node``.""" width, height, viewbox = node_format(surface, node) if viewbox: rect_x, rect_y = viewbox[0:2] node.image_width = viewbox[2] node.image_height = viewbox[3] else: rect_x, rect_y = 0, 0 node.image_width = size(surface, node.get("width"), "x") node.image_height = size(surface, node.get("height"), "y") if node.get("preserveAspectRatio", "none") != "none": scale_x, scale_y, translate_x, translate_y = \ preserve_ratio(surface, node) 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(rect_x, rect_y, rect_width, rect_height) surface.context.clip() surface.context.scale(scale_x, scale_y) surface.context.translate(translate_x, translate_y) CairoSVG-1.0.4/cairosvg/surface/tags.py0000644000175000017500000000275612245057114017651 0ustar lizelize00000000000000# -*- 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 . """ SVG tags functions. """ 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 from .svg import svg from .text import text TAGS = { "a": text, "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, "polygon": polygon, "radialGradient": radial_gradient, "rect": rect, "svg": svg, "text": text, "textPath": text, "tspan": text, "use": use} CairoSVG-1.0.4/cairosvg/surface/text.py0000644000175000017500000001505712267530427017704 0ustar lizelize00000000000000# -*- 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 . """ Text drawers. """ from math import cos, sin, radians # Python 2/3 management # pylint: disable=E0611 # pylint: enable=E0611 from . import cairo from .helpers import distance, normalize, point_angle, zip_letters from .units import size def path_length(path): """Get the length of ``path``.""" total_length = 0 for item in path: if item[0] == cairo.PATH_MOVE_TO: old_point = item[1] elif item[0] == cairo.PATH_LINE_TO: new_point = item[1] length = distance( old_point[0], old_point[1], new_point[0], new_point[1]) total_length += length old_point = new_point return total_length def point_following_path(path, width): """Get the point at ``width`` distance on ``path``.""" total_length = 0 for item in path: if item[0] == cairo.PATH_MOVE_TO: old_point = item[1] elif item[0] == cairo.PATH_LINE_TO: new_point = item[1] length = distance( old_point[0], old_point[1], new_point[0], new_point[1]) total_length += length if total_length < width: old_point = new_point else: length -= total_length - width angle = point_angle( old_point[0], old_point[1], new_point[0], new_point[1]) x = cos(angle) * length + old_point[0] y = sin(angle) * length + old_point[1] return x, y def text(surface, node): """Draw a text ``node``.""" # Set black as default text color if not node.get("fill"): node["fill"] = "#000000" font_size = size(surface, node.get("font-size", "12pt")) font_family = (node.get("font-family") or "sans-serif").split(",")[0] font_style = getattr( cairo, ("font_slant_%s" % node.get("font-style")).upper(), cairo.FONT_SLANT_NORMAL) font_weight = getattr( cairo, ("font_weight_%s" % node.get("font-weight")).upper(), cairo.FONT_WEIGHT_NORMAL) surface.context.select_font_face(font_family, font_style, font_weight) surface.context.set_font_size(font_size) text_path_href = ( node.get("{http://www.w3.org/1999/xlink}href", "") or node.parent.get("{http://www.w3.org/1999/xlink}href", "")) text_path = surface.paths.get(text_path_href.lstrip("#")) letter_spacing = size(surface, node.get("letter-spacing")) text_extents = surface.context.text_extents(node.text) x_bearing = text_extents[0] width = text_extents[2] x, y, dx, dy, rotate = [], [], [], [], [0] if "x" in node: x = [size(surface, i, "x") for i in normalize(node["x"]).strip().split(" ")] if "y" in node: y = [size(surface, i, "y") for i in normalize(node["y"]).strip().split(" ")] if "dx" in node: dx = [size(surface, i, "x") for i in normalize(node["dx"]).strip().split(" ")] if "dy" in node: dy = [size(surface, i, "y") for i in normalize(node["dy"]).strip().split(" ")] if "rotate" in node: rotate = [radians(float(i)) if i else 0 for i in normalize(node["rotate"]).strip().split(" ")] last_r = rotate[-1] letters_positions = zip_letters(x, y, dx, dy, rotate, node.text) text_anchor = node.get("text-anchor") if text_anchor == "middle": x_align = width / 2. + x_bearing elif text_anchor == "end": x_align = width + x_bearing else: x_align = 0 if text_path: surface.stroke_and_fill = False surface.draw(text_path) surface.stroke_and_fill = True cairo_path = surface.context.copy_path_flat() surface.context.new_path() start_offset = size( surface, node.get("startOffset", 0), path_length(cairo_path)) surface.text_path_width += start_offset x1, y1 = point_following_path(cairo_path, surface.text_path_width) if node.text: for [x, y, dx, dy, r], letter in letters_positions: if x: surface.cursor_d_position[0] = 0 if y: surface.cursor_d_position[1] = 0 surface.cursor_d_position[0] += dx or 0 surface.cursor_d_position[1] += dy or 0 extents = surface.context.text_extents(letter)[4] surface.context.save() if text_path: surface.text_path_width += extents + letter_spacing point_on_path = point_following_path( cairo_path, surface.text_path_width + surface.cursor_d_position[0]) if point_on_path: x2, y2 = point_on_path else: continue surface.context.translate(x1, y1) surface.context.rotate(point_angle(x1, y1, x2, y2)) surface.context.translate(0, surface.cursor_d_position[1]) surface.context.move_to(0, 0) x1, y1 = x2, y2 else: x = surface.cursor_position[0] if x is None else x y = surface.cursor_position[1] if y is None else y surface.context.move_to(x + letter_spacing, y) cursor_position = x + letter_spacing + extents, y surface.context.rel_move_to(*surface.cursor_d_position) surface.context.rel_move_to(-x_align, 0) surface.context.rotate(last_r if r is None else r) surface.context.text_path(letter) surface.context.restore() if not text_path: surface.cursor_position = cursor_position else: x = x[0] if x else surface.cursor_position[0] y = y[0] if y else surface.cursor_position[1] dx = dx[0] if dx else 0 dy = dy[0] if dy else 0 surface.cursor_position = (x + dx, y + dy) CairoSVG-1.0.4/cairosvg/surface/units.py0000644000175000017500000000502212245057114020042 0ustar lizelize00000000000000# -*- 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 . """ Units functions. """ UNITS = { "mm": 1 / 25.4, "cm": 1 / 2.54, "in": 1, "pt": 1 / 72., "pc": 1 / 6., "px": None} def size(surface, string, reference="xy"): """Replace a ``string`` with units by a float value. If ``reference`` is a float, it is used as reference for percentages. If it is ``'x'``, we use the viewport width as reference. If it is ``'y'``, we use the viewport height as reference. If it is ``'xy'``, we use ``(viewport_width ** 2 + viewport_height ** 2) ** .5 / 2 ** .5`` as reference. """ if not string: return 0. try: return float(string) except ValueError: # Not a float, try something else pass if "%" in string: if reference == "x": reference = surface.context_width or 0 elif reference == "y": reference = surface.context_height or 0 elif reference == "xy": reference = ( (surface.context_width ** 2 + surface.context_height ** 2) ** .5 / 2 ** .5) return float(string.strip(" %")) * reference / 100 elif "em" in string: return surface.font_size * float(string.strip(" em")) elif "ex" in string: # Assume that 1em == 2ex return surface.font_size * float(string.strip(" ex")) / 2 for unit, coefficient in UNITS.items(): if unit in string: number = float(string.strip(" " + unit)) return number * (surface.dpi * coefficient if coefficient else 1) # Try to return the number at the beginning of the string return_string = "" while string and (string[0].isdigit() or string[0] in "+-."): return_string += string[0] string = string[1:] # Unknown size or multiple sizes return float(return_string or 0) CairoSVG-1.0.4/cairosvg/__init__.py0000644000175000017500000000553212267531102017013 0ustar lizelize00000000000000# -*- 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 . """ CairoSVG - A simple SVG converter for Cairo. """ import os import sys import optparse from . import surface VERSION = '1.0.4' SURFACES = { 'SVG': surface.SVGSurface, # Tell us if you actually use this one! 'PNG': surface.PNGSurface, 'PDF': surface.PDFSurface, 'PS': surface.PSSurface} # Generate the svg2* functions from SURFACES for _output_format, _surface_type in SURFACES.items(): _function = ( # Two lambdas needed for the closure lambda surface_type: lambda *args, **kwargs: # pylint: disable=W0108 surface_type.convert(*args, **kwargs))(_surface_type) _name = 'svg2%s' % _output_format.lower() _function.__name__ = _name _function.__doc__ = surface.Surface.convert.__doc__.replace( 'the format for this class', _output_format) setattr(sys.modules[__name__], _name, _function) def main(): """Entry-point of the executable.""" # Get command-line options option_parser = optparse.OptionParser( usage="usage: %prog filename [options]", version=VERSION) option_parser.add_option( "-f", "--format", help="output format") option_parser.add_option( "-d", "--dpi", help="ratio between 1in and 1px", default=96) option_parser.add_option( "-o", "--output", default="", help="output filename") options, args = option_parser.parse_args() # Print help if no argument is given if not args: option_parser.print_help() sys.exit() kwargs = {'dpi': float(options.dpi)} if not options.output or options.output == '-': # Python 2/3 hack bytes_stdout = getattr(sys.stdout, "buffer", sys.stdout) kwargs['write_to'] = bytes_stdout else: kwargs['write_to'] = options.output url = args[0] if url == "-": # Python 2/3 hack bytes_stdin = getattr(sys.stdin, "buffer", sys.stdin) kwargs['file_obj'] = bytes_stdin else: kwargs['url'] = url output_format = ( options.format or os.path.splitext(options.output)[1].lstrip(".") or "pdf") SURFACES[output_format.upper()].convert(**kwargs) CairoSVG-1.0.4/cairosvg/css.py0000644000175000017500000001154612245057114016050 0ustar lizelize00000000000000# -*- 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 . """ Optionally handle CSS stylesheets. """ import os from .parser import HAS_LXML # Detect optional depedencies # pylint: disable=W0611 try: import tinycss import cssselect CSS_CAPABLE = HAS_LXML except ImportError: CSS_CAPABLE = False # pylint: enable=W0611 # Python 2/3 compat iteritems = getattr(dict, "iteritems", dict.items) # pylint: disable=C0103 def find_stylesheets(tree, url): """Find the stylesheets included in ``tree``.""" # TODO: support contentStyleType on default_type = "text/css" process = tree.getprevious() while process is not None: if (getattr(process, "target", None) == "xml-stylesheet" and process.attrib.get("type", default_type) == "text/css"): # TODO: handle web URLs filename = process.attrib.get("href") if filename: path = os.path.join(os.path.dirname(url), filename) if os.path.isfile(path): yield tinycss.make_parser().parse_stylesheet_file(path) process = process.getprevious() for element in tree.iter(): # http://www.w3.org/TR/SVG/styling.html#StyleElement if (element.tag == "style" and element.get("type", default_type) == "text/css" and element.text): # TODO: pass href for relative URLs # TODO: support media types # TODO: what if