2011-12-08 19:31:03 +04:00
|
|
|
|
# coding: utf8
|
2012-03-22 02:19:27 +04:00
|
|
|
|
"""
|
|
|
|
|
weasyprint.images
|
|
|
|
|
-----------------
|
2011-12-08 19:31:03 +04:00
|
|
|
|
|
2012-03-22 02:19:27 +04:00
|
|
|
|
Fetch and decode images in various formats.
|
|
|
|
|
|
2013-04-03 18:23:48 +04:00
|
|
|
|
:copyright: Copyright 2011-2013 Simon Sapin and contributors, see AUTHORS.
|
2012-03-22 02:19:27 +04:00
|
|
|
|
:license: BSD, see LICENSE for details.
|
2011-12-08 19:31:03 +04:00
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
2013-02-25 19:38:54 +04:00
|
|
|
|
from __future__ import division, unicode_literals
|
2012-02-17 21:49:58 +04:00
|
|
|
|
|
|
|
|
|
from io import BytesIO
|
2013-04-04 21:48:23 +04:00
|
|
|
|
import math
|
2011-12-08 19:31:03 +04:00
|
|
|
|
|
2013-02-26 18:04:52 +04:00
|
|
|
|
import cairocffi
|
|
|
|
|
cairocffi.install_as_pycairo() # for CairoSVG
|
2013-06-18 15:48:06 +04:00
|
|
|
|
CAIRO_HAS_MIME_DATA = cairocffi.cairo_version() >= 11000
|
2013-02-25 19:26:39 +04:00
|
|
|
|
|
|
|
|
|
import cairosvg.parser
|
|
|
|
|
import cairosvg.surface
|
2013-02-26 18:04:52 +04:00
|
|
|
|
assert cairosvg.surface.cairo is cairocffi, (
|
2013-02-25 19:26:39 +04:00
|
|
|
|
'CairoSVG is using pycairo instead of cairocffi. '
|
|
|
|
|
'Make sure it is not imported before WeasyPrint.')
|
|
|
|
|
|
2012-12-30 01:59:13 +04:00
|
|
|
|
try:
|
2013-02-26 18:04:52 +04:00
|
|
|
|
from cairocffi import pixbuf
|
2012-12-30 01:59:13 +04:00
|
|
|
|
except OSError:
|
2013-02-26 18:04:52 +04:00
|
|
|
|
pixbuf = None
|
2012-12-29 17:20:38 +04:00
|
|
|
|
|
2013-06-20 15:17:03 +04:00
|
|
|
|
from .urls import fetch
|
2013-02-26 18:04:52 +04:00
|
|
|
|
from .logger import LOGGER
|
2013-04-04 21:48:23 +04:00
|
|
|
|
from .compat import xrange
|
2012-12-29 14:50:11 +04:00
|
|
|
|
|
|
|
|
|
|
2013-04-03 18:00:31 +04:00
|
|
|
|
# Map values of the image-rendering property to cairo FILTER values:
|
|
|
|
|
# Values are normalized to lower case.
|
|
|
|
|
IMAGE_RENDERING_TO_FILTER = dict(
|
|
|
|
|
optimizespeed=cairocffi.FILTER_FAST,
|
|
|
|
|
auto=cairocffi.FILTER_GOOD,
|
|
|
|
|
optimizequality=cairocffi.FILTER_BEST,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2013-04-03 15:34:14 +04:00
|
|
|
|
class RasterImage(object):
|
|
|
|
|
def __init__(self, image_surface):
|
|
|
|
|
self.image_surface = image_surface
|
|
|
|
|
self.intrinsic_width = image_surface.get_width()
|
|
|
|
|
self.intrinsic_height = image_surface.get_height()
|
2013-04-03 20:15:32 +04:00
|
|
|
|
self.intrinsic_ratio = (
|
|
|
|
|
self.intrinsic_width / self.intrinsic_height
|
|
|
|
|
if self.intrinsic_height != 0 else float('inf'))
|
2013-04-03 15:34:14 +04:00
|
|
|
|
|
2013-04-03 18:00:31 +04:00
|
|
|
|
def draw(self, context, concrete_width, concrete_height, image_rendering):
|
|
|
|
|
if self.intrinsic_width > 0 and self.intrinsic_height > 0:
|
|
|
|
|
context.scale(concrete_width / self.intrinsic_width,
|
|
|
|
|
concrete_height / self.intrinsic_height)
|
|
|
|
|
context.set_source_surface(self.image_surface)
|
|
|
|
|
context.get_source().set_filter(
|
|
|
|
|
IMAGE_RENDERING_TO_FILTER[image_rendering])
|
|
|
|
|
context.paint()
|
2011-12-08 19:31:03 +04:00
|
|
|
|
|
|
|
|
|
|
2013-02-25 19:26:39 +04:00
|
|
|
|
class ScaledSVGSurface(cairosvg.surface.SVGSurface):
|
|
|
|
|
"""
|
|
|
|
|
Have the cairo Surface object have intrinsic dimension
|
|
|
|
|
in pixels instead of points.
|
|
|
|
|
"""
|
|
|
|
|
@property
|
|
|
|
|
def device_units_per_user_units(self):
|
|
|
|
|
scale = super(ScaledSVGSurface, self).device_units_per_user_units
|
|
|
|
|
return scale / 0.75
|
|
|
|
|
|
|
|
|
|
|
2013-04-03 15:34:14 +04:00
|
|
|
|
class SVGImage(object):
|
|
|
|
|
def __init__(self, svg_data, base_url):
|
2012-02-21 15:59:06 +04:00
|
|
|
|
# Don’t pass data URIs to CairoSVG.
|
|
|
|
|
# They are useless for relative URIs anyway.
|
2013-04-03 15:34:14 +04:00
|
|
|
|
self._base_url = (
|
2013-04-04 21:48:23 +04:00
|
|
|
|
base_url if not base_url.lower().startswith('data:') else None)
|
2013-04-03 15:34:14 +04:00
|
|
|
|
self._svg_data = svg_data
|
|
|
|
|
|
|
|
|
|
# TODO: find a way of not doing twice the whole rendering.
|
2013-04-03 18:00:31 +04:00
|
|
|
|
svg = self._render()
|
2013-04-03 15:34:14 +04:00
|
|
|
|
# TODO: support SVG images with none or only one of intrinsic
|
|
|
|
|
# width, height and ratio.
|
2013-04-03 18:00:31 +04:00
|
|
|
|
if not (svg.width > 0 and svg.height > 0):
|
2013-04-03 15:34:14 +04:00
|
|
|
|
raise ValueError(
|
2013-04-03 20:15:32 +04:00
|
|
|
|
'SVG images without an intrinsic size are not supported.')
|
2013-04-03 18:00:31 +04:00
|
|
|
|
self.intrinsic_width = svg.width
|
|
|
|
|
self.intrinsic_height = svg.height
|
2013-04-03 15:34:14 +04:00
|
|
|
|
self.intrinsic_ratio = self.intrinsic_width / self.intrinsic_height
|
|
|
|
|
|
|
|
|
|
def _render(self):
|
|
|
|
|
# Draw to a cairo surface but do not write to a file.
|
|
|
|
|
# This is a CairoSVG surface, not a cairo surface.
|
|
|
|
|
return ScaledSVGSurface(
|
2013-04-04 21:48:23 +04:00
|
|
|
|
cairosvg.parser.Tree(
|
|
|
|
|
bytestring=self._svg_data, url=self._base_url),
|
2013-04-03 15:34:14 +04:00
|
|
|
|
output=None, dpi=96)
|
|
|
|
|
|
2013-04-04 21:48:23 +04:00
|
|
|
|
def draw(self, context, concrete_width, concrete_height, _image_rendering):
|
|
|
|
|
# Do not re-use the rendered Surface object,
|
|
|
|
|
# but regenerate it as needed.
|
2012-09-26 18:59:40 +04:00
|
|
|
|
# If a surface for a SVG image is still alive by the time we call
|
|
|
|
|
# show_page(), cairo will rasterize the image instead writing vectors.
|
2013-04-03 18:00:31 +04:00
|
|
|
|
svg = self._render()
|
|
|
|
|
context.scale(concrete_width / svg.width, concrete_height / svg.height)
|
|
|
|
|
context.set_source_surface(svg.cairo)
|
|
|
|
|
context.paint()
|
2012-01-12 22:26:27 +04:00
|
|
|
|
|
|
|
|
|
|
2013-04-03 15:34:14 +04:00
|
|
|
|
def get_image_from_uri(cache, url_fetcher, uri, forced_mime_type=None):
|
2013-02-26 18:04:52 +04:00
|
|
|
|
"""Get a cairo Pattern from an image URI."""
|
2011-12-08 19:31:03 +04:00
|
|
|
|
try:
|
2012-05-29 18:24:14 +04:00
|
|
|
|
missing = object()
|
2012-09-26 18:59:40 +04:00
|
|
|
|
image = cache.get(uri, missing)
|
|
|
|
|
if image is not missing:
|
|
|
|
|
return image
|
2013-06-20 15:17:03 +04:00
|
|
|
|
result = fetch(url_fetcher, uri)
|
2013-04-03 15:34:14 +04:00
|
|
|
|
mime_type = forced_mime_type or result['mime_type']
|
2012-05-29 18:24:14 +04:00
|
|
|
|
try:
|
2013-04-03 15:34:14 +04:00
|
|
|
|
if mime_type == 'image/svg+xml':
|
|
|
|
|
image = SVGImage(
|
|
|
|
|
result.get('string') or result['file_obj'].read(), uri)
|
|
|
|
|
elif mime_type == 'image/png':
|
|
|
|
|
image = RasterImage(cairocffi.ImageSurface.create_from_png(
|
|
|
|
|
result.get('file_obj') or BytesIO(result.get('string'))))
|
2012-07-29 00:11:28 +04:00
|
|
|
|
else:
|
2013-04-03 15:34:14 +04:00
|
|
|
|
if pixbuf is None:
|
|
|
|
|
raise OSError(
|
|
|
|
|
'Could not load GDK-Pixbuf. '
|
|
|
|
|
'PNG and SVG are the only image formats available.')
|
|
|
|
|
string = result.get('string') or result['file_obj'].read()
|
|
|
|
|
surface, format_name = pixbuf.decode_to_image_surface(string)
|
2013-06-18 15:48:06 +04:00
|
|
|
|
if format_name == 'jpeg' and CAIRO_HAS_MIME_DATA:
|
2013-04-03 15:34:14 +04:00
|
|
|
|
surface.set_mime_data('image/jpeg', string)
|
|
|
|
|
image = RasterImage(surface)
|
2012-05-29 18:24:14 +04:00
|
|
|
|
finally:
|
2012-08-03 15:32:42 +04:00
|
|
|
|
if 'file_obj' in result:
|
|
|
|
|
try:
|
|
|
|
|
result['file_obj'].close()
|
|
|
|
|
except Exception: # pragma: no cover
|
|
|
|
|
# May already be closed or something.
|
|
|
|
|
# This is just cleanup anyway.
|
|
|
|
|
pass
|
2012-03-21 17:34:27 +04:00
|
|
|
|
except Exception as exc:
|
|
|
|
|
LOGGER.warn('Error for image at %s : %r', uri, exc)
|
2012-09-26 18:59:40 +04:00
|
|
|
|
image = None
|
|
|
|
|
cache[uri] = image
|
|
|
|
|
return image
|
2013-04-04 21:48:23 +04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def percentage(value, refer_to):
|
|
|
|
|
"""Return the evaluated percentage value, or the value unchanged."""
|
|
|
|
|
if value is None:
|
|
|
|
|
return value
|
|
|
|
|
elif value.unit == 'px':
|
|
|
|
|
return value.value
|
|
|
|
|
else:
|
|
|
|
|
assert value.unit == '%'
|
|
|
|
|
return refer_to * value.value / 100
|
|
|
|
|
|
|
|
|
|
|
2013-04-08 19:28:46 +04:00
|
|
|
|
def process_color_stops(gradient_line_size, positions):
|
2013-04-04 21:48:23 +04:00
|
|
|
|
"""
|
|
|
|
|
Gradient line size: distance between the starting point and ending point.
|
2013-04-08 19:28:46 +04:00
|
|
|
|
Positions: list of None, or Dimension in px or %.
|
|
|
|
|
0 is the starting point, 1 the ending point.
|
2013-04-04 21:48:23 +04:00
|
|
|
|
|
|
|
|
|
http://dev.w3.org/csswg/css-images-3/#color-stop-syntax
|
|
|
|
|
|
|
|
|
|
Return processed color stops, as a list of floats in px.
|
|
|
|
|
|
|
|
|
|
"""
|
2013-04-11 12:39:23 +04:00
|
|
|
|
positions = [percentage(position, gradient_line_size)
|
|
|
|
|
for position in positions]
|
2013-04-04 21:48:23 +04:00
|
|
|
|
# First and last default to 100%
|
|
|
|
|
if positions[0] is None:
|
|
|
|
|
positions[0] = 0
|
|
|
|
|
if positions[-1] is None:
|
|
|
|
|
positions[-1] = gradient_line_size
|
|
|
|
|
|
|
|
|
|
# Make sure positions are increasing.
|
|
|
|
|
previous_pos = positions[0]
|
|
|
|
|
for i, position in enumerate(positions):
|
|
|
|
|
if position is not None:
|
|
|
|
|
if position < previous_pos:
|
|
|
|
|
positions[i] = previous_pos
|
|
|
|
|
else:
|
|
|
|
|
previous_pos = position
|
|
|
|
|
|
|
|
|
|
# Assign missing values
|
|
|
|
|
previous_i = -1
|
|
|
|
|
for i, position in enumerate(positions):
|
|
|
|
|
if position is not None:
|
|
|
|
|
base = positions[previous_i]
|
|
|
|
|
increment = (position - base) / (i - previous_i)
|
|
|
|
|
for j in xrange(previous_i + 1, i):
|
|
|
|
|
positions[j] = base + j * increment
|
|
|
|
|
previous_i = i
|
2013-04-12 17:29:21 +04:00
|
|
|
|
return positions
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def normalize_stop_postions(positions):
|
|
|
|
|
"""Normalize to [0..1]."""
|
|
|
|
|
first = positions[0]
|
|
|
|
|
last = positions[-1]
|
2013-04-08 19:28:46 +04:00
|
|
|
|
total_length = last - first
|
2013-04-17 18:57:17 +04:00
|
|
|
|
if total_length != 0:
|
|
|
|
|
positions = [(pos - first) / total_length for pos in positions]
|
|
|
|
|
else:
|
|
|
|
|
positions = [0 for _ in positions]
|
|
|
|
|
return first, last, positions
|
2013-04-04 21:48:23 +04:00
|
|
|
|
|
|
|
|
|
|
2013-04-08 19:28:46 +04:00
|
|
|
|
def gradient_average_color(colors, positions):
|
|
|
|
|
"""
|
|
|
|
|
http://dev.w3.org/csswg/css-images-3/#find-the-average-color-of-a-gradient
|
|
|
|
|
"""
|
|
|
|
|
nb_stops = len(positions)
|
2013-04-12 17:29:21 +04:00
|
|
|
|
assert nb_stops > 1
|
2013-04-08 19:28:46 +04:00
|
|
|
|
assert nb_stops == len(colors)
|
|
|
|
|
total_length = positions[-1] - positions[0]
|
|
|
|
|
if total_length == 0:
|
2013-04-17 15:10:36 +04:00
|
|
|
|
positions = list(range(nb_stops))
|
2013-04-08 19:28:46 +04:00
|
|
|
|
total_length = nb_stops - 1
|
|
|
|
|
premul_r = [r * a for r, g, b, a in colors]
|
|
|
|
|
premul_g = [g * a for r, g, b, a in colors]
|
|
|
|
|
premul_b = [b * a for r, g, b, a in colors]
|
|
|
|
|
alpha = [a for r, g, b, a in colors]
|
|
|
|
|
result_r = result_g = result_b = result_a = 0
|
|
|
|
|
total_weight = 2 * total_length
|
|
|
|
|
for i, position in enumerate(positions[1:], 1):
|
|
|
|
|
weight = (position - positions[i - 1]) / total_weight
|
|
|
|
|
for j in (i - 1, i):
|
|
|
|
|
result_r += premul_r[j] * weight
|
|
|
|
|
result_g += premul_g[j] * weight
|
|
|
|
|
result_b += premul_b[j] * weight
|
|
|
|
|
result_a += alpha[j] * weight
|
|
|
|
|
# Un-premultiply:
|
|
|
|
|
return (result_r / result_a, result_g / result_a,
|
2013-04-17 18:57:17 +04:00
|
|
|
|
result_b / result_a, result_a) if result_a != 0 else (0, 0, 0, 0)
|
2013-04-08 19:28:46 +04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
PATTERN_TYPES = dict(
|
|
|
|
|
linear=cairocffi.LinearGradient,
|
2013-04-11 12:39:23 +04:00
|
|
|
|
radial=cairocffi.RadialGradient,
|
2013-04-08 19:28:46 +04:00
|
|
|
|
solid=cairocffi.SolidPattern)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Gradient(object):
|
2013-04-04 21:48:23 +04:00
|
|
|
|
intrinsic_width = None
|
|
|
|
|
intrinsic_height = None
|
|
|
|
|
intrinsic_ratio = None
|
|
|
|
|
|
2013-04-11 12:39:23 +04:00
|
|
|
|
def __init__(self, color_stops, repeating):
|
2013-04-12 17:29:21 +04:00
|
|
|
|
assert color_stops
|
2013-04-11 12:39:23 +04:00
|
|
|
|
#: List of (r, g, b, a), list of Dimension
|
2013-04-11 18:06:28 +04:00
|
|
|
|
self.colors = [color for color, position in color_stops]
|
|
|
|
|
self.stop_positions = [position for color, position in color_stops]
|
2013-04-11 12:39:23 +04:00
|
|
|
|
#: bool
|
|
|
|
|
self.repeating = repeating
|
|
|
|
|
|
2013-04-08 19:28:46 +04:00
|
|
|
|
def draw(self, context, concrete_width, concrete_height, _image_rendering):
|
|
|
|
|
scale_y, type_, init, stop_positions, stop_colors = self.layout(
|
2013-04-11 12:39:23 +04:00
|
|
|
|
concrete_width, concrete_height, context.user_to_device_distance)
|
2013-04-08 19:28:46 +04:00
|
|
|
|
context.scale(1, scale_y)
|
|
|
|
|
pattern = PATTERN_TYPES[type_](*init)
|
|
|
|
|
for position, color in zip(stop_positions, stop_colors):
|
|
|
|
|
pattern.add_color_stop_rgba(position, *color)
|
|
|
|
|
pattern.set_extend(cairocffi.EXTEND_REPEAT if self.repeating
|
|
|
|
|
else cairocffi.EXTEND_PAD)
|
|
|
|
|
context.set_source(pattern)
|
|
|
|
|
context.paint()
|
|
|
|
|
|
2013-04-11 18:06:28 +04:00
|
|
|
|
def layout(self, width, height, user_to_device_distance):
|
2013-04-08 19:28:46 +04:00
|
|
|
|
"""width, height: Gradient box. Top-left is at coordinates (0, 0).
|
2013-04-11 18:06:28 +04:00
|
|
|
|
user_to_device_distance: a (dx, dy) -> (ddx, ddy) function
|
2013-04-08 19:28:46 +04:00
|
|
|
|
|
|
|
|
|
Returns (scale_y, type_, init, positions, colors).
|
|
|
|
|
scale_y: float, used for ellipses radial gradients. 1 otherwise.
|
|
|
|
|
positions: list of floats in [0..1].
|
|
|
|
|
0 at the starting point, 1 at the ending point.
|
|
|
|
|
colors: list of (r, g, b, a)
|
|
|
|
|
type_ is either:
|
|
|
|
|
'solid': init is (r, g, b, a). positions and colors are empty.
|
|
|
|
|
'linear': init is (x0, y0, x1, y1)
|
2013-04-11 12:39:23 +04:00
|
|
|
|
coordinates of the starting and ending points.
|
|
|
|
|
'radial': init is (cx0, cy0, radius0, cx1, cy1, radius1)
|
|
|
|
|
coordinates of the starting end ending circles
|
2013-04-08 19:28:46 +04:00
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
raise NotImplementedError
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class LinearGradient(Gradient):
|
2013-04-04 21:48:23 +04:00
|
|
|
|
def __init__(self, color_stops, direction, repeating):
|
2013-04-11 18:06:28 +04:00
|
|
|
|
Gradient.__init__(self, color_stops, repeating)
|
2013-04-11 12:39:23 +04:00
|
|
|
|
#: ('corner', keyword) or ('angle', radians)
|
2013-04-04 21:48:23 +04:00
|
|
|
|
self.direction_type, self.direction = direction
|
|
|
|
|
|
2013-04-11 12:39:23 +04:00
|
|
|
|
def layout(self, width, height, user_to_device_distance):
|
2013-04-12 17:29:21 +04:00
|
|
|
|
if len(self.colors) == 1:
|
|
|
|
|
return 1, 'solid', self.colors[0], [], []
|
2013-04-11 12:39:23 +04:00
|
|
|
|
# (dx, dy) is the unit vector giving the direction of the gradient.
|
|
|
|
|
# Positive dx: right, positive dy: down.
|
2013-04-04 21:48:23 +04:00
|
|
|
|
if self.direction_type == 'corner':
|
|
|
|
|
factor_x, factor_y = {
|
|
|
|
|
'top_left': (-1, -1), 'top_right': (1, -1),
|
|
|
|
|
'bottom_left': (-1, 1), 'bottom_right': (1, 1)}[self.direction]
|
2013-04-08 19:28:46 +04:00
|
|
|
|
diagonal = math.hypot(width, height)
|
2013-04-12 17:29:21 +04:00
|
|
|
|
# Note the direction swap: dx based on height, dy based on width
|
|
|
|
|
# The gradient line is perpendicular to a diagonal.
|
2013-04-11 12:39:23 +04:00
|
|
|
|
dx = factor_x * height / diagonal
|
|
|
|
|
dy = factor_y * width / diagonal
|
2013-04-04 21:48:23 +04:00
|
|
|
|
else:
|
2013-04-08 19:28:46 +04:00
|
|
|
|
angle = self.direction # 0 upwards, then clockwise
|
2013-04-11 12:39:23 +04:00
|
|
|
|
dx = math.sin(angle)
|
|
|
|
|
dy = -math.cos(angle)
|
2013-04-12 17:29:21 +04:00
|
|
|
|
# Distance between center and ending point,
|
|
|
|
|
# ie. half of between the starting point and ending point:
|
|
|
|
|
distance = abs(width * dx) + abs(height * dy)
|
2013-04-17 18:57:17 +04:00
|
|
|
|
positions = process_color_stops(distance, self.stop_positions)
|
|
|
|
|
first, last, positions = normalize_stop_postions(positions)
|
|
|
|
|
device_per_user_units = math.hypot(*user_to_device_distance(dx, dy))
|
|
|
|
|
if (last - first) * device_per_user_units < len(positions):
|
|
|
|
|
if self.repeating:
|
|
|
|
|
color = gradient_average_color(self.colors, positions)
|
|
|
|
|
return 1, 'solid', color, [], []
|
|
|
|
|
else:
|
|
|
|
|
# 100 is an Arbitrary non-zero number of device units.
|
|
|
|
|
offset = 100 / device_per_user_units
|
|
|
|
|
if first != last:
|
|
|
|
|
factor = (offset + last - first) / (last - first)
|
|
|
|
|
positions = [pos / factor for pos in positions]
|
|
|
|
|
last += offset
|
2013-04-12 17:29:21 +04:00
|
|
|
|
start_x = (width - dx * distance) / 2
|
|
|
|
|
start_y = (height - dy * distance) / 2
|
|
|
|
|
points = (start_x + dx * first, start_y + dy * first,
|
|
|
|
|
start_x + dx * last, start_y + dy * last)
|
2013-04-11 12:39:23 +04:00
|
|
|
|
return 1, 'linear', points, positions, self.colors
|
2013-04-08 19:28:46 +04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class RadialGradient(Gradient):
|
2013-04-11 18:06:28 +04:00
|
|
|
|
def __init__(self, color_stops, shape, size, center, repeating):
|
|
|
|
|
Gradient.__init__(self, color_stops, repeating)
|
2013-04-11 12:39:23 +04:00
|
|
|
|
# Center of the ending shape. (origin_x, pos_x, origin_y, pos_y)
|
2013-04-11 18:06:28 +04:00
|
|
|
|
self.center = center
|
2013-04-11 12:39:23 +04:00
|
|
|
|
#: Type of ending shape: 'circle' or 'ellipse'
|
|
|
|
|
self.shape = shape
|
|
|
|
|
# size_type: 'keyword'
|
|
|
|
|
# size: 'closest-corner', 'farthest-corner',
|
|
|
|
|
# 'closest-side', or 'farthest-side'
|
|
|
|
|
# size_type: 'explicit'
|
|
|
|
|
# size: (radius_x, radius_y)
|
|
|
|
|
self.size_type, self.size = size
|
|
|
|
|
|
2013-04-16 18:18:47 +04:00
|
|
|
|
def layout(self, width, height, user_to_device_distance):
|
2013-04-12 17:29:21 +04:00
|
|
|
|
if len(self.colors) == 1:
|
|
|
|
|
return 1, 'solid', self.colors[0], [], []
|
2013-04-11 18:06:28 +04:00
|
|
|
|
origin_x, center_x, origin_y, center_y = self.center
|
|
|
|
|
center_x = percentage(center_x, width)
|
|
|
|
|
center_y = percentage(center_y, height)
|
2013-04-11 12:39:23 +04:00
|
|
|
|
if origin_x == 'right':
|
2013-04-11 18:06:28 +04:00
|
|
|
|
center_x = width - center_x
|
2013-04-11 12:39:23 +04:00
|
|
|
|
if origin_y == 'bottom':
|
2013-04-11 18:06:28 +04:00
|
|
|
|
center_y = height - center_y
|
2013-04-11 12:39:23 +04:00
|
|
|
|
|
2013-04-16 18:18:47 +04:00
|
|
|
|
size_x, size_y = self._resolve_size(width, height, center_x, center_y)
|
2013-04-11 12:39:23 +04:00
|
|
|
|
# http://dev.w3.org/csswg/css-images-3/#degenerate-radials
|
|
|
|
|
if size_x == size_y == 0:
|
2013-04-16 18:18:47 +04:00
|
|
|
|
size_x = size_y = 1e-7
|
2013-04-11 12:39:23 +04:00
|
|
|
|
elif size_x == 0:
|
2013-04-16 18:18:47 +04:00
|
|
|
|
size_x = 1e-7
|
|
|
|
|
size_y = 1e7
|
2013-04-11 12:39:23 +04:00
|
|
|
|
elif size_y == 0:
|
2013-04-16 18:18:47 +04:00
|
|
|
|
size_x = 1e7
|
|
|
|
|
size_y = 1e-7
|
2013-04-11 12:39:23 +04:00
|
|
|
|
scale_y = size_y / size_x
|
|
|
|
|
|
|
|
|
|
colors = self.colors
|
2013-04-12 17:29:21 +04:00
|
|
|
|
positions = process_color_stops(size_x, self.stop_positions)
|
|
|
|
|
gradient_line_size = positions[-1] - positions[0]
|
2013-04-11 12:39:23 +04:00
|
|
|
|
if self.repeating and any(
|
2013-04-17 18:57:17 +04:00
|
|
|
|
gradient_line_size * unit < len(positions)
|
|
|
|
|
for unit in (math.hypot(*user_to_device_distance(1, 0)),
|
|
|
|
|
math.hypot(*user_to_device_distance(0, scale_y)))):
|
2013-04-11 12:39:23 +04:00
|
|
|
|
color = gradient_average_color(colors, positions)
|
|
|
|
|
return 1, 'solid', color, [], []
|
|
|
|
|
|
2013-04-12 17:29:21 +04:00
|
|
|
|
if positions[0] < 0:
|
2013-04-11 12:39:23 +04:00
|
|
|
|
# Cairo does not like negative radiuses,
|
|
|
|
|
# shift into the positive realm.
|
|
|
|
|
if self.repeating:
|
|
|
|
|
offset = gradient_line_size * math.ceil(
|
2013-04-12 17:29:21 +04:00
|
|
|
|
-positions[0] / gradient_line_size)
|
2013-04-11 12:39:23 +04:00
|
|
|
|
positions = [p + offset for p in positions]
|
|
|
|
|
else:
|
|
|
|
|
for i, position in enumerate(positions):
|
2013-04-16 18:18:47 +04:00
|
|
|
|
if position > 0:
|
2013-04-12 17:29:21 +04:00
|
|
|
|
# `i` is the first positive stop.
|
|
|
|
|
# Interpolate with the previous to get the color at 0.
|
|
|
|
|
assert i > 0
|
2013-04-11 12:39:23 +04:00
|
|
|
|
color = colors[i]
|
|
|
|
|
neg_color = colors[i - 1]
|
|
|
|
|
neg_position = positions[i - 1]
|
|
|
|
|
assert neg_position < 0
|
|
|
|
|
intermediate_color = gradient_average_color(
|
2013-04-16 18:18:47 +04:00
|
|
|
|
[neg_color, neg_color, color, color],
|
2013-04-11 12:39:23 +04:00
|
|
|
|
[neg_position, 0, 0, position])
|
|
|
|
|
colors = [intermediate_color] + colors[i:]
|
|
|
|
|
positions = [0] + positions[i:]
|
|
|
|
|
break
|
|
|
|
|
else:
|
2013-04-12 17:29:21 +04:00
|
|
|
|
# All stops are negatives,
|
|
|
|
|
# everything is "padded" with the last color.
|
2013-04-11 12:39:23 +04:00
|
|
|
|
return 1, 'solid', self.colors[-1], [], []
|
|
|
|
|
|
2013-04-12 17:29:21 +04:00
|
|
|
|
first, last, positions = normalize_stop_postions(positions)
|
2013-04-17 18:57:17 +04:00
|
|
|
|
if last == first:
|
|
|
|
|
last += 100 # Arbitrary non-zero
|
|
|
|
|
|
2013-04-16 18:18:47 +04:00
|
|
|
|
circles = (center_x, center_y / scale_y, first,
|
|
|
|
|
center_x, center_y / scale_y, last)
|
2013-04-11 12:39:23 +04:00
|
|
|
|
return scale_y, 'radial', circles, positions, colors
|
|
|
|
|
|
2013-04-11 18:06:28 +04:00
|
|
|
|
def _resolve_size(self, width, height, center_x, center_y):
|
2013-04-11 12:39:23 +04:00
|
|
|
|
if self.size_type == 'explicit':
|
2013-04-16 18:18:47 +04:00
|
|
|
|
size_x, size_y = self.size
|
|
|
|
|
return percentage(size_x, width), percentage(size_y, height)
|
2013-04-11 18:06:28 +04:00
|
|
|
|
left = abs(center_x)
|
|
|
|
|
right = abs(width - center_x)
|
|
|
|
|
top = abs(center_y)
|
|
|
|
|
bottom = abs(height - center_y)
|
2013-04-16 18:18:47 +04:00
|
|
|
|
pick = min if self.size.startswith('closest') else max
|
|
|
|
|
if self.size.endswith('side'):
|
2013-04-11 12:39:23 +04:00
|
|
|
|
if self.shape == 'circle':
|
2013-04-16 18:18:47 +04:00
|
|
|
|
size_xy = pick(left, right, top, bottom)
|
2013-04-11 12:39:23 +04:00
|
|
|
|
return size_xy, size_xy
|
|
|
|
|
# else: ellipse
|
2013-04-16 18:18:47 +04:00
|
|
|
|
return pick(left, right), pick(top, bottom)
|
|
|
|
|
# else: corner
|
|
|
|
|
if self.shape == 'circle':
|
|
|
|
|
size_xy = pick(math.hypot(left, top), math.hypot(left, bottom),
|
|
|
|
|
math.hypot(right, top), math.hypot(right, bottom))
|
|
|
|
|
return size_xy, size_xy
|
|
|
|
|
# else: ellipse
|
|
|
|
|
corner_x, corner_y = pick(
|
|
|
|
|
(left, top), (left, bottom), (right, top), (right, bottom),
|
|
|
|
|
key=lambda a: math.hypot(*a))
|
|
|
|
|
return corner_x * math.sqrt(2), corner_y * math.sqrt(2)
|