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.
|
|
|
|
|
|
2011-12-08 19:31:03 +04:00
|
|
|
|
"""
|
|
|
|
|
|
2017-03-25 02:33:36 +03:00
|
|
|
|
import math
|
2012-02-17 21:49:58 +04:00
|
|
|
|
from io import BytesIO
|
2016-02-26 15:58:47 +03:00
|
|
|
|
from xml.etree import ElementTree
|
2011-12-08 19:31:03 +04:00
|
|
|
|
|
2013-02-25 19:26:39 +04:00
|
|
|
|
import cairosvg.parser
|
|
|
|
|
import cairosvg.surface
|
2020-06-07 01:32:47 +03:00
|
|
|
|
import pydyf
|
2020-06-03 18:58:53 +03:00
|
|
|
|
from PIL import Image
|
2016-01-15 15:15:02 +03:00
|
|
|
|
|
2019-06-02 19:06:25 +03:00
|
|
|
|
from .layout.percentages import percentage
|
2017-03-25 02:33:36 +03:00
|
|
|
|
from .logger import LOGGER
|
|
|
|
|
from .urls import URLFetchingError, fetch
|
2013-02-25 19:26:39 +04:00
|
|
|
|
|
2013-04-03 18:00:31 +04:00
|
|
|
|
|
2013-06-21 00:32:28 +04:00
|
|
|
|
class ImageLoadingError(ValueError):
|
|
|
|
|
"""An error occured when loading an image.
|
|
|
|
|
|
|
|
|
|
The image data is probably corrupted or in an invalid format.
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def from_exception(cls, exception):
|
|
|
|
|
name = type(exception).__name__
|
|
|
|
|
value = str(exception)
|
2020-05-30 16:48:24 +03:00
|
|
|
|
return cls(f'{name}: {value}' if value else name)
|
2013-06-21 00:32:28 +04:00
|
|
|
|
|
|
|
|
|
|
2020-01-02 14:06:58 +03:00
|
|
|
|
class RasterImage:
|
2020-07-31 15:46:36 +03:00
|
|
|
|
def __init__(self, pillow_image, optimize_image):
|
2020-06-03 18:58:53 +03:00
|
|
|
|
self.pillow_image = pillow_image
|
2020-07-31 15:46:36 +03:00
|
|
|
|
self.optimize_image = optimize_image
|
2020-06-03 18:58:53 +03:00
|
|
|
|
self._intrinsic_width = pillow_image.width
|
|
|
|
|
self._intrinsic_height = pillow_image.height
|
2013-04-03 20:15:32 +04:00
|
|
|
|
self.intrinsic_ratio = (
|
2013-07-26 19:26:30 +04:00
|
|
|
|
self._intrinsic_width / self._intrinsic_height
|
|
|
|
|
if self._intrinsic_height != 0 else float('inf'))
|
|
|
|
|
|
2016-02-26 15:58:47 +03:00
|
|
|
|
def get_intrinsic_size(self, image_resolution, _font_size):
|
2013-07-26 19:26:30 +04:00
|
|
|
|
# Raster images are affected by the 'image-resolution' property.
|
|
|
|
|
return (self._intrinsic_width / image_resolution,
|
|
|
|
|
self._intrinsic_height / image_resolution)
|
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):
|
2019-06-01 02:32:13 +03:00
|
|
|
|
has_size = (
|
|
|
|
|
concrete_width > 0
|
|
|
|
|
and concrete_height > 0
|
|
|
|
|
and self._intrinsic_width > 0
|
|
|
|
|
and self._intrinsic_height > 0
|
|
|
|
|
)
|
|
|
|
|
if not has_size:
|
|
|
|
|
return
|
|
|
|
|
|
2020-07-31 15:46:36 +03:00
|
|
|
|
image_name = context.add_image(
|
|
|
|
|
self.pillow_image, image_rendering, self.optimize_image)
|
2019-06-01 02:32:13 +03:00
|
|
|
|
# Use the real intrinsic size here,
|
|
|
|
|
# not affected by 'image-resolution'.
|
2020-06-03 18:58:53 +03:00
|
|
|
|
context.push_state()
|
2020-06-03 20:47:34 +03:00
|
|
|
|
context.transform(
|
|
|
|
|
concrete_width, 0, 0, -concrete_height, 0, concrete_height)
|
2020-06-03 18:58:53 +03:00
|
|
|
|
context.draw_x_object(image_name)
|
|
|
|
|
context.pop_state()
|
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):
|
2019-12-23 17:34:49 +03:00
|
|
|
|
scale = super().device_units_per_user_units
|
2013-02-25 19:26:39 +04:00
|
|
|
|
return scale / 0.75
|
|
|
|
|
|
|
|
|
|
|
2020-01-02 14:06:58 +03:00
|
|
|
|
class FakeSurface:
|
2016-02-26 15:58:47 +03:00
|
|
|
|
"""Fake CairoSVG surface used to get SVG attributes."""
|
|
|
|
|
context_height = 0
|
|
|
|
|
context_width = 0
|
|
|
|
|
font_size = 12
|
|
|
|
|
dpi = 96
|
|
|
|
|
|
|
|
|
|
|
2020-01-02 14:06:58 +03:00
|
|
|
|
class SVGImage:
|
2017-06-07 12:37:53 +03:00
|
|
|
|
def __init__(self, svg_data, base_url, url_fetcher):
|
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
|
2017-06-07 12:37:53 +03:00
|
|
|
|
self._url_fetcher = url_fetcher
|
2013-04-03 15:34:14 +04:00
|
|
|
|
|
2013-06-21 00:32:28 +04:00
|
|
|
|
try:
|
2016-02-26 15:58:47 +03:00
|
|
|
|
self._tree = ElementTree.fromstring(self._svg_data)
|
2013-06-21 00:32:28 +04:00
|
|
|
|
except Exception as e:
|
|
|
|
|
raise ImageLoadingError.from_exception(e)
|
2016-02-26 15:58:47 +03:00
|
|
|
|
|
2017-06-23 13:23:22 +03:00
|
|
|
|
def _cairosvg_url_fetcher(self, src, mimetype):
|
|
|
|
|
data = self._url_fetcher(src)
|
2017-11-11 16:21:16 +03:00
|
|
|
|
if 'string' in data:
|
|
|
|
|
return data['string']
|
|
|
|
|
return data['file_obj'].read()
|
2017-06-23 13:23:22 +03:00
|
|
|
|
|
2016-02-26 15:58:47 +03:00
|
|
|
|
def get_intrinsic_size(self, _image_resolution, font_size):
|
|
|
|
|
# Vector images may be affected by the font size.
|
|
|
|
|
fake_surface = FakeSurface()
|
|
|
|
|
fake_surface.font_size = font_size
|
|
|
|
|
# Percentages don't provide an intrinsic size, we transform percentages
|
|
|
|
|
# into 0 using a (0, 0) context size:
|
|
|
|
|
# http://www.w3.org/TR/SVG/coords.html#IntrinsicSizing
|
|
|
|
|
self._width = cairosvg.surface.size(
|
|
|
|
|
fake_surface, self._tree.get('width'))
|
|
|
|
|
self._height = cairosvg.surface.size(
|
|
|
|
|
fake_surface, self._tree.get('height'))
|
|
|
|
|
_, _, viewbox = cairosvg.surface.node_format(fake_surface, self._tree)
|
|
|
|
|
self._intrinsic_width = self._width or None
|
|
|
|
|
self._intrinsic_height = self._height or None
|
|
|
|
|
self.intrinsic_ratio = None
|
|
|
|
|
if viewbox:
|
|
|
|
|
if self._width and self._height:
|
|
|
|
|
self.intrinsic_ratio = self._width / self._height
|
|
|
|
|
else:
|
2016-02-26 17:28:43 +03:00
|
|
|
|
if viewbox[2] and viewbox[3]:
|
|
|
|
|
self.intrinsic_ratio = viewbox[2] / viewbox[3]
|
|
|
|
|
if self._width:
|
|
|
|
|
self._intrinsic_height = (
|
|
|
|
|
self._width / self.intrinsic_ratio)
|
|
|
|
|
elif self._height:
|
|
|
|
|
self._intrinsic_width = (
|
|
|
|
|
self._height * self.intrinsic_ratio)
|
2016-02-26 15:58:47 +03:00
|
|
|
|
elif self._width and self._height:
|
|
|
|
|
self.intrinsic_ratio = self._width / self._height
|
2013-07-26 19:26:30 +04:00
|
|
|
|
return self._intrinsic_width, self._intrinsic_height
|
2013-04-03 15:34:14 +04:00
|
|
|
|
|
2013-04-04 21:48:23 +04:00
|
|
|
|
def draw(self, context, concrete_width, concrete_height, _image_rendering):
|
2016-02-26 17:29:05 +03:00
|
|
|
|
try:
|
|
|
|
|
svg = ScaledSVGSurface(
|
|
|
|
|
cairosvg.parser.Tree(
|
2017-06-07 12:37:53 +03:00
|
|
|
|
bytestring=self._svg_data, url=self._base_url,
|
2017-06-23 13:23:22 +03:00
|
|
|
|
url_fetcher=self._cairosvg_url_fetcher),
|
2019-05-20 13:28:35 +03:00
|
|
|
|
output=None, dpi=96, output_width=concrete_width,
|
|
|
|
|
output_height=concrete_height)
|
2016-02-26 17:29:05 +03:00
|
|
|
|
if svg.width and svg.height:
|
|
|
|
|
context.scale(
|
|
|
|
|
concrete_width / svg.width, concrete_height / svg.height)
|
|
|
|
|
context.set_source_surface(svg.cairo)
|
|
|
|
|
context.paint()
|
2020-05-30 16:48:24 +03:00
|
|
|
|
except Exception as exception:
|
2017-07-25 14:59:56 +03:00
|
|
|
|
LOGGER.error(
|
2020-05-30 16:48:24 +03:00
|
|
|
|
'Failed to draw an SVG image at %r: %s',
|
|
|
|
|
self._base_url, exception)
|
2012-01-12 22:26:27 +04:00
|
|
|
|
|
|
|
|
|
|
2020-06-22 17:05:14 +03:00
|
|
|
|
def get_image_from_uri(cache, url_fetcher, optimize_images, url,
|
|
|
|
|
forced_mime_type=None):
|
2013-02-26 18:04:52 +04:00
|
|
|
|
"""Get a cairo Pattern from an image URI."""
|
2013-06-20 15:58:24 +04:00
|
|
|
|
missing = object()
|
2013-06-21 00:32:28 +04:00
|
|
|
|
image = cache.get(url, missing)
|
2013-06-20 15:58:24 +04:00
|
|
|
|
if image is not missing:
|
|
|
|
|
return image
|
|
|
|
|
|
2011-12-08 19:31:03 +04:00
|
|
|
|
try:
|
2013-06-21 00:32:28 +04:00
|
|
|
|
with fetch(url_fetcher, url) as result:
|
2016-08-26 15:34:28 +03:00
|
|
|
|
if 'string' in result:
|
|
|
|
|
string = result['string']
|
|
|
|
|
else:
|
|
|
|
|
string = result['file_obj'].read()
|
2013-06-20 15:58:24 +04:00
|
|
|
|
mime_type = forced_mime_type or result['mime_type']
|
2013-04-03 15:34:14 +04:00
|
|
|
|
if mime_type == 'image/svg+xml':
|
2016-08-26 15:34:28 +03:00
|
|
|
|
# No fallback for XML-based mimetypes as defined by MIME
|
|
|
|
|
# Sniffing Standard, see https://mimesniff.spec.whatwg.org/
|
2017-06-07 12:37:53 +03:00
|
|
|
|
image = SVGImage(string, url, url_fetcher)
|
2012-07-29 00:11:28 +04:00
|
|
|
|
else:
|
2016-08-26 15:34:28 +03:00
|
|
|
|
# Try to rely on given mimetype
|
2013-06-21 00:32:28 +04:00
|
|
|
|
try:
|
2020-06-03 18:58:53 +03:00
|
|
|
|
pillow_image = Image.open(BytesIO(string))
|
|
|
|
|
except Exception as exception:
|
|
|
|
|
raise ImageLoadingError.from_exception(exception)
|
|
|
|
|
else:
|
2020-07-31 15:46:36 +03:00
|
|
|
|
image = RasterImage(pillow_image, optimize_images)
|
2020-06-03 18:58:53 +03:00
|
|
|
|
|
2020-05-30 16:48:24 +03:00
|
|
|
|
except (URLFetchingError, ImageLoadingError) as exception:
|
|
|
|
|
LOGGER.error('Failed to load image at %r: %s', url, exception)
|
2012-09-26 18:59:40 +04:00
|
|
|
|
image = None
|
2013-06-21 00:32:28 +04:00
|
|
|
|
cache[url] = image
|
2012-09-26 18:59:40 +04:00
|
|
|
|
return image
|
2013-04-04 21:48:23 +04:00
|
|
|
|
|
|
|
|
|
|
2020-10-24 18:42:13 +03:00
|
|
|
|
def process_color_stops(vector_length, positions):
|
|
|
|
|
"""Give color stops positions on the gradient vector.
|
|
|
|
|
|
|
|
|
|
``vector_length`` is the distance between the starting point and ending
|
|
|
|
|
point of the vector gradient.
|
2013-04-04 21:48:23 +04:00
|
|
|
|
|
2020-10-24 18:42:13 +03:00
|
|
|
|
``positions`` is a list of ``None``, or ``Dimension`` in px or %. 0 is the
|
|
|
|
|
starting point, 1 the ending point.
|
|
|
|
|
|
|
|
|
|
See http://dev.w3.org/csswg/css-images-3/#color-stop-syntax.
|
2013-04-04 21:48:23 +04:00
|
|
|
|
|
|
|
|
|
Return processed color stops, as a list of floats in px.
|
|
|
|
|
|
|
|
|
|
"""
|
2020-10-24 18:42:13 +03:00
|
|
|
|
# Resolve percentages
|
|
|
|
|
positions = [percentage(position, vector_length) 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:
|
2020-10-24 18:42:13 +03:00
|
|
|
|
positions[-1] = vector_length
|
2013-04-04 21:48:23 +04:00
|
|
|
|
|
2020-10-24 18:42:13 +03:00
|
|
|
|
# Make sure positions are increasing
|
2013-04-04 21:48:23 +04:00
|
|
|
|
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)
|
2018-01-14 03:48:17 +03:00
|
|
|
|
for j in range(previous_i + 1, i):
|
2013-04-04 21:48:23 +04:00
|
|
|
|
positions[j] = base + j * increment
|
|
|
|
|
previous_i = i
|
2020-10-24 18:42:13 +03:00
|
|
|
|
|
2013-04-12 17:29:21 +04:00
|
|
|
|
return positions
|
|
|
|
|
|
|
|
|
|
|
2020-10-24 18:42:13 +03:00
|
|
|
|
def normalize_stop_positions(positions):
|
|
|
|
|
"""Normalize stop positions between 0 and 1.
|
|
|
|
|
|
|
|
|
|
Return ``(first, last, positions)``.
|
|
|
|
|
|
|
|
|
|
first: original position of the first position.
|
|
|
|
|
last: original position of the last position.
|
|
|
|
|
positions: list of positions between 0 and 1.
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
first, last = positions[0], positions[-1]
|
2013-04-08 19:28:46 +04:00
|
|
|
|
total_length = last - first
|
2020-10-24 18:42:13 +03:00
|
|
|
|
if total_length == 0:
|
|
|
|
|
positions = [0] * len(positions)
|
2013-04-17 18:57:17 +04:00
|
|
|
|
else:
|
2020-10-24 18:42:13 +03:00
|
|
|
|
positions = [(pos - first) / total_length for pos in positions]
|
2013-04-17 18:57:17 +04:00
|
|
|
|
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):
|
|
|
|
|
"""
|
2020-10-24 18:42:13 +03:00
|
|
|
|
http://dev.w3.org/csswg/css-images-3/#gradient-average-color
|
2013-04-08 19:28:46 +04:00
|
|
|
|
"""
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
2020-01-02 14:06:58 +03:00
|
|
|
|
class Gradient:
|
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
|
2020-10-24 18:42:13 +03:00
|
|
|
|
self.colors = tuple(color for color, position in color_stops)
|
|
|
|
|
self.stop_positions = tuple(position for _, position in color_stops)
|
2013-04-11 12:39:23 +04:00
|
|
|
|
#: bool
|
|
|
|
|
self.repeating = repeating
|
|
|
|
|
|
2016-02-26 15:58:47 +03:00
|
|
|
|
def get_intrinsic_size(self, _image_resolution, _font_size):
|
|
|
|
|
# Gradients are not affected by image resolution, parent or font size.
|
2013-07-26 19:26:30 +04:00
|
|
|
|
return None, None
|
|
|
|
|
|
|
|
|
|
intrinsic_ratio = None
|
|
|
|
|
|
2013-04-08 19:28:46 +04:00
|
|
|
|
def draw(self, context, concrete_width, concrete_height, _image_rendering):
|
2020-10-24 18:42:13 +03:00
|
|
|
|
# TODO: handle alpha and color spaces
|
|
|
|
|
scale_y, type_, points, positions, colors = self.layout(
|
|
|
|
|
concrete_width, concrete_height)
|
2020-06-07 01:32:47 +03:00
|
|
|
|
|
2020-06-08 00:40:01 +03:00
|
|
|
|
if type_ == 'solid':
|
|
|
|
|
context.rectangle(0, 0, concrete_width, concrete_height)
|
2020-10-24 18:42:13 +03:00
|
|
|
|
context.set_color_rgb(*colors[0][:3])
|
2020-06-08 00:40:01 +03:00
|
|
|
|
context.fill()
|
|
|
|
|
return
|
|
|
|
|
|
2020-06-07 01:32:47 +03:00
|
|
|
|
shading = context.add_shading()
|
2020-10-24 18:42:13 +03:00
|
|
|
|
|
|
|
|
|
if self.repeating:
|
|
|
|
|
# TODO: handle repeating gradients
|
|
|
|
|
pass
|
|
|
|
|
else:
|
|
|
|
|
shading['Extend'] = pydyf.Array([b'true', b'true'])
|
|
|
|
|
|
2020-06-07 01:32:47 +03:00
|
|
|
|
shading['ShadingType'] = 2 if type_ == 'linear' else 3
|
|
|
|
|
shading['ColorSpace'] = '/DeviceRGB'
|
2020-10-24 18:42:13 +03:00
|
|
|
|
shading['Coords'] = pydyf.Array(points)
|
|
|
|
|
shading['Function'] = pydyf.Dictionary({
|
|
|
|
|
'FunctionType': 3,
|
|
|
|
|
'Domain': pydyf.Array([positions[0], positions[-1]]), # [0, 1]
|
|
|
|
|
'Encode': pydyf.Array((len(colors) - 1) * [0, 1]),
|
|
|
|
|
'Bounds': pydyf.Array(positions[1:-1]),
|
|
|
|
|
'Functions': pydyf.Array([
|
|
|
|
|
pydyf.Dictionary({
|
|
|
|
|
'FunctionType': 2,
|
|
|
|
|
'Domain': pydyf.Array([0, 1]),
|
|
|
|
|
'C0': pydyf.Array(colors[i][:3]),
|
|
|
|
|
'C1': pydyf.Array(colors[i + 1][:3]),
|
|
|
|
|
'N': 1,
|
|
|
|
|
}) for i in range(len(colors) - 1)
|
|
|
|
|
]),
|
|
|
|
|
})
|
2020-10-24 21:46:38 +03:00
|
|
|
|
context.transform(1, 0, 0, scale_y, 0, 0)
|
2020-06-07 01:32:47 +03:00
|
|
|
|
context.shading(shading.id)
|
|
|
|
|
|
2020-10-24 18:42:13 +03:00
|
|
|
|
def layout(self, width, height):
|
2020-06-07 12:04:12 +03:00
|
|
|
|
"""Get layout information about the gradient.
|
|
|
|
|
|
|
|
|
|
width, height: Gradient box. Top-left is at coordinates (0, 0).
|
2020-10-24 18:42:13 +03:00
|
|
|
|
|
|
|
|
|
Returns (scale_y, type_, points, positions, colors).
|
|
|
|
|
|
|
|
|
|
scale_y: vertical scale of the gradient. float, used for ellipses
|
|
|
|
|
radial gradients. 1 otherwise.
|
|
|
|
|
type_: gradient type.
|
|
|
|
|
points: coordinates of useful points, depending on type_:
|
|
|
|
|
'solid': None.
|
|
|
|
|
'linear': (x0, y0, x1, y1)
|
2013-04-11 12:39:23 +04:00
|
|
|
|
coordinates of the starting and ending points.
|
2020-10-24 18:42:13 +03:00
|
|
|
|
'radial': (cx0, cy0, radius0, cx1, cy1, radius1)
|
2013-04-11 12:39:23 +04:00
|
|
|
|
coordinates of the starting end ending circles
|
2020-10-24 18:42:13 +03:00
|
|
|
|
positions: positions of the color stops. list of floats in between 0
|
|
|
|
|
and 1 (0 at the starting point, 1 at the ending point).
|
|
|
|
|
colors: list of (r, g, b, a).
|
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)
|
2020-10-24 18:42:13 +03:00
|
|
|
|
# ('corner', keyword) or ('angle', radians)
|
2013-04-04 21:48:23 +04:00
|
|
|
|
self.direction_type, self.direction = direction
|
|
|
|
|
|
2020-10-24 18:42:13 +03:00
|
|
|
|
def layout(self, width, height):
|
|
|
|
|
# Only one color, render the gradient as a solid color
|
2013-04-12 17:29:21 +04:00
|
|
|
|
if len(self.colors) == 1:
|
2020-06-08 00:40:01 +03:00
|
|
|
|
return 1, 'solid', None, [], [self.colors[0]]
|
2020-10-24 18:42:13 +03:00
|
|
|
|
|
|
|
|
|
# Define the (dx, dy) unit vector giving the direction of the gradient.
|
2013-04-11 12:39:23 +04:00
|
|
|
|
# Positive dx: right, positive dy: down.
|
2013-04-04 21:48:23 +04:00
|
|
|
|
if self.direction_type == 'corner':
|
2020-10-24 18:42:13 +03:00
|
|
|
|
y, x = self.direction.split('_')
|
|
|
|
|
factor_x = -1 if x == 'top' else 1
|
|
|
|
|
factor_y = -1 if y == 'left' else 1
|
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:
|
2020-10-24 18:42:13 +03:00
|
|
|
|
assert self.direction_type == 'angle'
|
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)
|
2020-10-24 18:42:13 +03:00
|
|
|
|
|
|
|
|
|
# Normalize colors positions
|
|
|
|
|
colors = list(self.colors)
|
|
|
|
|
vector_length = abs(width * dx) + abs(height * dy)
|
|
|
|
|
positions = process_color_stops(vector_length, self.stop_positions)
|
|
|
|
|
if not self.repeating:
|
|
|
|
|
# Add explicit colors at boundaries if needed, because PDF doesn’t
|
|
|
|
|
# extend color stops that are not displayed
|
|
|
|
|
if positions[0] == positions[1]:
|
|
|
|
|
positions.insert(0, positions[0] - 1)
|
|
|
|
|
colors.insert(0, colors[0])
|
|
|
|
|
if positions[-2] == positions[-1]:
|
|
|
|
|
positions.append(positions[-1] + 1)
|
|
|
|
|
colors.append(colors[-1])
|
|
|
|
|
first, last, positions = normalize_stop_positions(positions)
|
|
|
|
|
|
|
|
|
|
# Render as a solid color if the first and last positions are the same
|
|
|
|
|
# See https://drafts.csswg.org/css-images-3/#repeating-gradients
|
|
|
|
|
if first == last and self.repeating:
|
|
|
|
|
color = gradient_average_color(colors, positions)
|
|
|
|
|
return 1, 'solid', None, [], [color]
|
|
|
|
|
|
|
|
|
|
# Define the coordinates of the starting and ending points
|
|
|
|
|
start_x = (width - dx * vector_length) / 2
|
|
|
|
|
start_y = (height - dy * vector_length) / 2
|
|
|
|
|
points = (
|
|
|
|
|
start_x + dx * first, start_y + dy * first,
|
|
|
|
|
start_x + dx * last, start_y + dy * last)
|
|
|
|
|
|
|
|
|
|
return 1, 'linear', points, positions, 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
|
|
|
|
|
|
2020-10-24 18:42:13 +03:00
|
|
|
|
def layout(self, width, height):
|
2020-10-24 21:46:38 +03:00
|
|
|
|
# Only one color, render the gradient as a solid color
|
2013-04-12 17:29:21 +04:00
|
|
|
|
if len(self.colors) == 1:
|
2020-06-08 00:40:01 +03:00
|
|
|
|
return 1, 'solid', None, [], [self.colors[0]]
|
2020-10-24 21:46:38 +03:00
|
|
|
|
|
|
|
|
|
# Define the center of the gradient
|
2013-04-11 18:06:28 +04:00
|
|
|
|
origin_x, center_x, origin_y, center_y = self.center
|
2019-06-02 19:06:25 +03:00
|
|
|
|
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
|
|
|
|
|
2020-10-24 21:46:38 +03:00
|
|
|
|
# Resolve sizes and vertical scale
|
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
|
|
|
|
scale_y = size_y / size_x
|
|
|
|
|
|
2020-10-24 21:46:38 +03:00
|
|
|
|
# Normalize colors positions
|
|
|
|
|
colors = list(self.colors)
|
2013-04-12 17:29:21 +04:00
|
|
|
|
positions = process_color_stops(size_x, self.stop_positions)
|
2020-10-24 21:46:38 +03:00
|
|
|
|
if not self.repeating:
|
|
|
|
|
# Add explicit colors at boundaries if needed, because PDF doesn’t
|
|
|
|
|
# extend color stops that are not displayed
|
|
|
|
|
if positions[0] > 0 and positions[0] == positions[1]:
|
|
|
|
|
positions.insert(0, 0)
|
|
|
|
|
colors.insert(0, colors[0])
|
|
|
|
|
if positions[-2] == positions[-1]:
|
|
|
|
|
positions.append(positions[-1] + 1)
|
|
|
|
|
colors.append(colors[-1])
|
|
|
|
|
if positions[0] < 0 and not self.repeating:
|
|
|
|
|
# All stops are negatives, paint everything is with the last color
|
|
|
|
|
return 1, 'solid', None, [], [self.colors[-1]]
|
|
|
|
|
first, last, positions = normalize_stop_positions(positions)
|
|
|
|
|
|
|
|
|
|
# Render as a solid color if the first and last positions are the same
|
|
|
|
|
# See https://drafts.csswg.org/css-images-3/#repeating-gradients
|
|
|
|
|
if first == last and self.repeating:
|
2013-04-11 12:39:23 +04:00
|
|
|
|
color = gradient_average_color(colors, positions)
|
2020-06-08 00:40:01 +03:00
|
|
|
|
return 1, 'solid', None, [], [color]
|
2013-04-11 12:39:23 +04:00
|
|
|
|
|
2020-10-24 21:46:38 +03:00
|
|
|
|
# Define the coordinates of the gradient circles
|
|
|
|
|
points = (
|
|
|
|
|
center_x, center_y / scale_y, first,
|
|
|
|
|
center_x, center_y / scale_y, last)
|
2013-04-17 18:57:17 +04:00
|
|
|
|
|
2020-10-24 21:46:38 +03:00
|
|
|
|
return scale_y, 'radial', points, positions, colors
|
2013-04-11 12:39:23 +04:00
|
|
|
|
|
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
|
2019-06-02 19:06:25 +03:00
|
|
|
|
size_x = percentage(size_x, width)
|
|
|
|
|
size_y = percentage(size_y, height)
|
2019-06-01 02:32:13 +03:00
|
|
|
|
return size_x, size_y
|
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)
|
2020-10-24 21:46:38 +03:00
|
|
|
|
|
|
|
|
|
def _handle_degenerate(self, size_x, size_y):
|
|
|
|
|
# Handle degenerate radial gradients
|
|
|
|
|
# See https://drafts.csswg.org/css-images-3/#degenerate-radials
|
|
|
|
|
if size_x == size_y == 0:
|
|
|
|
|
size_x = size_y = 1e-7
|
|
|
|
|
elif size_x == 0:
|
|
|
|
|
size_x = 1e-7
|
|
|
|
|
size_y = 1e7
|
|
|
|
|
elif size_y == 0:
|
|
|
|
|
size_x = 1e7
|
|
|
|
|
size_y = 1e-7
|
|
|
|
|
return size_x, size_y
|