CairoSVG-1.0.20/ 0000755 0001750 0001750 00000000000 12663122651 013143 5 ustar lize lize 0000000 0000000 CairoSVG-1.0.20/bin/ 0000755 0001750 0001750 00000000000 12663122651 013713 5 ustar lize lize 0000000 0000000 CairoSVG-1.0.20/bin/cairosvg 0000755 0001750 0001750 00000001473 12663121446 015464 0 ustar lize lize 0000000 0000000 #!/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.20/cairosvg/ 0000755 0001750 0001750 00000000000 12663122651 014760 5 ustar lize lize 0000000 0000000 CairoSVG-1.0.20/cairosvg/surface/ 0000755 0001750 0001750 00000000000 12663122651 016410 5 ustar lize lize 0000000 0000000 CairoSVG-1.0.20/cairosvg/surface/__init__.py 0000644 0001750 0001750 00000042241 12663121605 020522 0 ustar lize lize 0000000 0000000 # -*- 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
# OSError means cairocffi is installed,
# but could not load a cairo dynamic library.
# pycairo may still be available with a statically-linked cairo.
except (ImportError, OSError):
import cairo # pycairo
from ..parser import Tree
from .colors import color
from .defs import (
apply_filter_after_painting, apply_filter_before_painting,
gradient_or_pattern, parse_def, paint_mask, prepare_filter)
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
SHAPE_ANTIALIAS = {
"optimizeSpeed": cairo.ANTIALIAS_FAST,
"crispEdges": cairo.ANTIALIAS_NONE,
"geometricPrecision": cairo.ANTIALIAS_BEST}
TEXT_ANTIALIAS = {
"optimizeSpeed": cairo.ANTIALIAS_FAST,
"optimizeLegibility": cairo.ANTIALIAS_GOOD,
"geometricPrecision": cairo.ANTIALIAS_BEST}
TEXT_HINT_STYLE = {
"geometricPrecision": cairo.HINT_STYLE_NONE,
"optimizeLegibility": cairo.HINT_STYLE_FULL}
TEXT_HINT_METRICS = {
"geometricPrecision": cairo.HINT_METRICS_OFF,
"optimizeLegibility": cairo.HINT_METRICS_ON}
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)
parent_width = kwargs.pop('parent_width', None)
parent_height = kwargs.pop('parent_height', None)
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, None, parent_width, parent_height).finish()
if write_to is None:
return output.getvalue()
def __init__(self, tree, output, dpi, parent_surface=None,
parent_width=None, parent_height=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 = parent_width, parent_height
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._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.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 / x_ratio * y_ratio, -y)
elif x_ratio < y_ratio:
matrix.translate(0, (height - y_size * x_ratio) / 2)
matrix.scale(x_ratio, x_ratio)
matrix.translate(-x, -y / y_ratio * x_ratio)
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
filters = urls(node.get("filter"))
filter_ = filters[0][1:] if filters else None
opacity = float(node.get("opacity", 1))
if filter_:
prepare_filter(self, node, filter_)
if filter_ or mask or (opacity < 1 and node.children):
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)
if node.tag in TAGS:
try:
TAGS[node.tag](self, node)
except PointError:
# Error in point parsing, do nothing
pass
# Get stroke and fill opacity
stroke_opacity = float(node.get("stroke-opacity", 1))
fill_opacity = float(node.get("fill-opacity", 1))
if opacity < 1 and not node.children:
stroke_opacity *= opacity
fill_opacity *= opacity
# Manage display and visibility
display = node.get("display", "inline") != "none"
visible = display and (node.get("visibility", "visible") != "hidden")
# Set antialias
self.context.set_antialias(SHAPE_ANTIALIAS.get(
node.get("shape-rendering"), cairo.ANTIALIAS_DEFAULT))
font_options = self.context.get_font_options()
font_options.set_antialias(TEXT_ANTIALIAS.get(
node.get("text-rendering"), cairo.ANTIALIAS_DEFAULT))
font_options.set_hint_style(TEXT_HINT_STYLE.get(
node.get("text-rendering"), cairo.HINT_STYLE_DEFAULT))
font_options.set_hint_metrics(TEXT_HINT_METRICS.get(
node.get("text-rendering"), cairo.HINT_METRICS_DEFAULT))
self.context.set_font_options(font_options)
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 filter_ or mask or (opacity < 1 and node.children):
self.context.pop_group_to_source()
if filter_:
apply_filter_before_painting(self, node, filter_)
if mask and mask in self.masks:
paint_mask(self, node, mask, opacity)
else:
self.context.paint_with_alpha(opacity)
if filter_:
apply_filter_after_painting(self, node, filter_)
# 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.20/cairosvg/surface/colors.py 0000644 0001750 0001750 00000020312 12663121446 020262 0 ustar lize lize 0000000 0000000 # -*- 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.20/cairosvg/surface/defs.py 0000644 0001750 0001750 00000040067 12663121446 017713 0 ustar lize lize 0000000 0000000 # -*- 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", "1"), "x")
height = size(surface, node.get("height", "1"), "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)
viewbox = node_format(surface, marker_node)[2]
width = base_scale * size(
surface, node.get("markerWidth", "3"), "x")
height = base_scale * size(
surface, node.get("markerHeight", "3"), "y")
if viewbox:
viewbox_width = viewbox[2]
viewbox_height = viewbox[3]
else:
viewbox_width = width
viewbox_height = height
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(
width / viewbox_width * float(scale_x),
height / 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 prepare_filter(surface, node, name):
if node["id"] in surface.masks:
return
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_before_painting(surface, node, name):
if node["id"] in surface.masks:
return
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"]))
def apply_filter_after_painting(surface, node, name):
if node["id"] in surface.masks:
return
if name in surface.filters:
filter_node = surface.filters[name]
for child in filter_node.children:
# Flood
if child.tag == "feFlood":
surface.context.save()
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.restore()
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.20/cairosvg/surface/helpers.py 0000644 0001750 0001750 00000023303 12663121447 020427 0 ustar lize lize 0000000 0000000 # -*- 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 = viewbox.replace(",", " ")
while " " in viewbox:
viewbox = viewbox.replace(" ", " ")
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":
width = size(surface, node.get("markerWidth", "3"), "x")
height = size(surface, node.get("markerHeight", "3"), "y")
scale_x = 1
scale_y = 1
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.20/cairosvg/surface/image.py 0000644 0001750 0001750 00000012731 12663121447 020052 0 ustar lize lize 0000000 0000000 # -*- 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"