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
|
2020-10-25 16:54:08 +03:00
|
|
|
|
from itertools import cycle
|
2011-12-08 19:31:03 +04:00
|
|
|
|
|
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
|
2021-02-22 01:25:35 +03:00
|
|
|
|
from .svg import SVG
|
2017-03-25 02:33:36 +03:00
|
|
|
|
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:
|
2021-04-28 15:55:52 +03:00
|
|
|
|
def __init__(self, pillow_image, optimize_size):
|
2020-06-03 18:58:53 +03:00
|
|
|
|
self.pillow_image = pillow_image
|
2021-04-28 15:55:52 +03:00
|
|
|
|
self.optimize_size = optimize_size
|
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(
|
2021-04-28 15:55:52 +03:00
|
|
|
|
self.pillow_image, image_rendering, self.optimize_size)
|
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
|
|
|
|
|
|
|
|
|
|
2021-01-06 12:53:45 +03:00
|
|
|
|
class SVGImage:
|
2021-04-30 19:13:08 +03:00
|
|
|
|
def __init__(self, string, base_url, url_fetcher, context):
|
|
|
|
|
self._svg = SVG(string, base_url)
|
2021-01-06 12:53:45 +03:00
|
|
|
|
self._base_url = base_url
|
|
|
|
|
self._url_fetcher = url_fetcher
|
2021-04-14 17:56:06 +03:00
|
|
|
|
self._context = context
|
2021-01-06 12:53:45 +03:00
|
|
|
|
|
|
|
|
|
def get_intrinsic_size(self, _image_resolution, font_size):
|
|
|
|
|
self._intrinsic_width, self._intrinsic_height = (
|
2021-02-08 23:24:56 +03:00
|
|
|
|
self._svg.get_intrinsic_size(font_size))
|
|
|
|
|
viewbox = self._svg.get_viewbox()
|
2021-01-06 12:53:45 +03:00
|
|
|
|
|
2021-03-12 17:18:44 +03:00
|
|
|
|
if self._intrinsic_width and self._intrinsic_height:
|
2021-01-06 12:53:45 +03:00
|
|
|
|
self.intrinsic_ratio = (
|
|
|
|
|
self._intrinsic_width / self._intrinsic_height)
|
2021-03-12 17:18:44 +03:00
|
|
|
|
elif viewbox and viewbox[2] and viewbox[3]:
|
|
|
|
|
self.intrinsic_ratio = viewbox[2] / viewbox[3]
|
|
|
|
|
if self._intrinsic_width:
|
|
|
|
|
self._intrinsic_height = (
|
|
|
|
|
self._intrinsic_width / self.intrinsic_ratio)
|
|
|
|
|
elif self._intrinsic_height:
|
|
|
|
|
self._intrinsic_width = (
|
|
|
|
|
self._intrinsic_height * self.intrinsic_ratio)
|
|
|
|
|
else:
|
|
|
|
|
self.intrinsic_ratio = None
|
2021-01-06 12:53:45 +03:00
|
|
|
|
|
|
|
|
|
return self._intrinsic_width, self._intrinsic_height
|
|
|
|
|
|
2021-04-11 17:13:59 +03:00
|
|
|
|
def draw(self, context, concrete_width, concrete_height, image_rendering):
|
2021-03-12 17:18:44 +03:00
|
|
|
|
if not concrete_width or not concrete_height:
|
2021-01-06 12:53:45 +03:00
|
|
|
|
return
|
|
|
|
|
|
2021-03-23 15:58:31 +03:00
|
|
|
|
# Use the real intrinsic size here, not affected by 'image-resolution'.
|
2021-01-06 12:53:45 +03:00
|
|
|
|
context.push_state()
|
2021-03-12 18:59:02 +03:00
|
|
|
|
self._svg.draw(
|
|
|
|
|
context, concrete_width, concrete_height, self._base_url,
|
2021-04-14 17:56:06 +03:00
|
|
|
|
self._url_fetcher, self._context)
|
2021-01-06 12:53:45 +03:00
|
|
|
|
context.pop_state()
|
|
|
|
|
|
|
|
|
|
|
2021-04-28 15:55:52 +03:00
|
|
|
|
def get_image_from_uri(cache, url_fetcher, optimize_size, url,
|
2021-04-14 17:56:06 +03:00
|
|
|
|
forced_mime_type=None, context=None):
|
2020-12-11 00:47:27 +03:00
|
|
|
|
"""Get an Image instance 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']
|
2021-03-12 18:59:02 +03:00
|
|
|
|
|
|
|
|
|
image = None
|
|
|
|
|
svg_exceptions = []
|
|
|
|
|
# Try to rely on given mimetype for SVG
|
2013-04-03 15:34:14 +04:00
|
|
|
|
if mime_type == 'image/svg+xml':
|
2021-03-07 22:34:24 +03:00
|
|
|
|
try:
|
2021-04-30 19:13:08 +03:00
|
|
|
|
image = SVGImage(string, url, url_fetcher, context)
|
2021-03-12 18:59:02 +03:00
|
|
|
|
except Exception as svg_exception:
|
|
|
|
|
svg_exceptions.append(svg_exception)
|
|
|
|
|
# Try pillow for raster images, or for failing SVG
|
|
|
|
|
if image is None:
|
2013-06-21 00:32:28 +04:00
|
|
|
|
try:
|
2020-06-03 18:58:53 +03:00
|
|
|
|
pillow_image = Image.open(BytesIO(string))
|
2021-03-12 18:59:02 +03:00
|
|
|
|
except Exception as raster_exception:
|
|
|
|
|
if mime_type == 'image/svg+xml':
|
|
|
|
|
# Tried SVGImage then Pillow for a SVG, abort
|
|
|
|
|
raise ImageLoadingError.from_exception(
|
|
|
|
|
svg_exceptions[0])
|
|
|
|
|
try:
|
|
|
|
|
# Last chance, try SVG
|
2021-04-30 19:13:08 +03:00
|
|
|
|
image = SVGImage(string, url, url_fetcher, context)
|
2021-03-12 18:59:02 +03:00
|
|
|
|
except Exception:
|
|
|
|
|
# Tried Pillow then SVGImage for a raster, abort
|
|
|
|
|
raise ImageLoadingError.from_exception(
|
|
|
|
|
raster_exception)
|
2020-06-03 18:58:53 +03:00
|
|
|
|
else:
|
2021-04-28 15:55:52 +03:00
|
|
|
|
image = RasterImage(pillow_image, optimize_size)
|
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
|
2021-03-19 17:58:22 +03:00
|
|
|
|
# List of (r, g, b, a)
|
|
|
|
|
self.colors = tuple(color for color, _ in color_stops)
|
|
|
|
|
# List of Dimensions
|
2020-10-24 18:42:13 +03:00
|
|
|
|
self.stop_positions = tuple(position for _, position in color_stops)
|
2021-03-19 17:58:22 +03:00
|
|
|
|
# Boolean
|
2013-04-11 12:39:23 +04:00
|
|
|
|
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-11-30 21:12:41 +03:00
|
|
|
|
# TODO: handle color spaces
|
2020-10-24 18:42:13 +03:00
|
|
|
|
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-11-30 21:12:41 +03:00
|
|
|
|
red, green, blue, alpha = colors[0]
|
|
|
|
|
context.set_color_rgb(red, green, blue)
|
|
|
|
|
if alpha != 1:
|
Handle transparent colors in gradients correctly
Gradients must be drawn using a pre-multiplied color space. It means that stop
colors are "weighted" by their alpha channel when we define how transition is
done between two colors.
To explain why this is needed, the best example is "transparent". The
"trasparent" color is set to rgba(0, 0, 0, 0), also known as transparent
black. When there’s a transition from red to transparent, we don’t want to have
a transition to black, as it would be if RGBA values were extrapolated. We want
to have a transition to transparent red.
This problem appears for each transition between colors whose RGB and A
channels are both different.
To fix this, we use the same plain color as the one from the previous stop for
transitions to transparent, and the same plain color as the one from the next
stop for transitions from transparent. For semi-transparent colors, we set a
weight proportional to the color alpha, meaning that opaque colors will take
more place than transparent ones.
This bug appears in Safari: https://bugs.webkit.org/show_bug.cgi?id=150940.
It obviously means that we’re better than Apple. Obviously.
2020-11-30 22:54:26 +03:00
|
|
|
|
context.set_alpha(alpha, stroke=False)
|
2020-06-08 00:40:01 +03:00
|
|
|
|
context.fill()
|
|
|
|
|
return
|
|
|
|
|
|
Handle transparent colors in gradients correctly
Gradients must be drawn using a pre-multiplied color space. It means that stop
colors are "weighted" by their alpha channel when we define how transition is
done between two colors.
To explain why this is needed, the best example is "transparent". The
"trasparent" color is set to rgba(0, 0, 0, 0), also known as transparent
black. When there’s a transition from red to transparent, we don’t want to have
a transition to black, as it would be if RGBA values were extrapolated. We want
to have a transition to transparent red.
This problem appears for each transition between colors whose RGB and A
channels are both different.
To fix this, we use the same plain color as the one from the previous stop for
transitions to transparent, and the same plain color as the one from the next
stop for transitions from transparent. For semi-transparent colors, we set a
weight proportional to the color alpha, meaning that opaque colors will take
more place than transparent ones.
This bug appears in Safari: https://bugs.webkit.org/show_bug.cgi?id=150940.
It obviously means that we’re better than Apple. Obviously.
2020-11-30 22:54:26 +03:00
|
|
|
|
alphas = [color[3] for color in colors]
|
|
|
|
|
alpha_couples = [
|
|
|
|
|
(alphas[i], alphas[i + 1])
|
|
|
|
|
for i in range(len(alphas) - 1)]
|
|
|
|
|
color_couples = [
|
|
|
|
|
[colors[i][:3], colors[i + 1][:3], 1]
|
|
|
|
|
for i in range(len(colors) - 1)]
|
|
|
|
|
|
|
|
|
|
# Premultiply colors
|
|
|
|
|
for i, alpha in enumerate(alphas):
|
|
|
|
|
if alpha == 0:
|
|
|
|
|
if i > 0:
|
|
|
|
|
color_couples[i - 1][1] = color_couples[i - 1][0]
|
|
|
|
|
if i < len(colors) - 1:
|
|
|
|
|
color_couples[i][0] = color_couples[i][1]
|
|
|
|
|
for i, (a0, a1) in enumerate(alpha_couples):
|
|
|
|
|
if 0 not in (a0, a1) and (a0, a1) != (1, 1):
|
|
|
|
|
color_couples[i][2] = a0 / a1
|
|
|
|
|
|
2020-06-07 01:32:47 +03:00
|
|
|
|
shading = context.add_shading()
|
|
|
|
|
shading['ShadingType'] = 2 if type_ == 'linear' else 3
|
|
|
|
|
shading['ColorSpace'] = '/DeviceRGB'
|
2020-10-25 15:41:23 +03:00
|
|
|
|
shading['Domain'] = pydyf.Array([positions[0], positions[-1]])
|
2020-10-24 18:42:13 +03:00
|
|
|
|
shading['Coords'] = pydyf.Array(points)
|
|
|
|
|
shading['Function'] = pydyf.Dictionary({
|
|
|
|
|
'FunctionType': 3,
|
2020-10-25 15:41:23 +03:00
|
|
|
|
'Domain': pydyf.Array([positions[0], positions[-1]]),
|
2020-10-24 18:42:13 +03:00
|
|
|
|
'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]),
|
Handle transparent colors in gradients correctly
Gradients must be drawn using a pre-multiplied color space. It means that stop
colors are "weighted" by their alpha channel when we define how transition is
done between two colors.
To explain why this is needed, the best example is "transparent". The
"trasparent" color is set to rgba(0, 0, 0, 0), also known as transparent
black. When there’s a transition from red to transparent, we don’t want to have
a transition to black, as it would be if RGBA values were extrapolated. We want
to have a transition to transparent red.
This problem appears for each transition between colors whose RGB and A
channels are both different.
To fix this, we use the same plain color as the one from the previous stop for
transitions to transparent, and the same plain color as the one from the next
stop for transitions from transparent. For semi-transparent colors, we set a
weight proportional to the color alpha, meaning that opaque colors will take
more place than transparent ones.
This bug appears in Safari: https://bugs.webkit.org/show_bug.cgi?id=150940.
It obviously means that we’re better than Apple. Obviously.
2020-11-30 22:54:26 +03:00
|
|
|
|
'C0': pydyf.Array(c0),
|
|
|
|
|
'C1': pydyf.Array(c1),
|
|
|
|
|
'N': n,
|
|
|
|
|
}) for c0, c1, n in color_couples
|
2020-10-24 18:42:13 +03:00
|
|
|
|
]),
|
|
|
|
|
})
|
2020-10-25 15:41:23 +03:00
|
|
|
|
if not self.repeating:
|
|
|
|
|
shading['Extend'] = pydyf.Array([b'true', b'true'])
|
2020-10-24 21:46:38 +03:00
|
|
|
|
context.transform(1, 0, 0, scale_y, 0, 0)
|
2020-11-30 21:12:41 +03:00
|
|
|
|
|
|
|
|
|
if any(alpha != 1 for alpha in alphas):
|
|
|
|
|
alpha_stream = context.add_transparency_group(
|
|
|
|
|
[0, 0, concrete_width, concrete_height])
|
|
|
|
|
alpha_state = pydyf.Dictionary({
|
|
|
|
|
'Type': '/ExtGState',
|
|
|
|
|
'SMask': pydyf.Dictionary({
|
|
|
|
|
'Type': '/Mask',
|
|
|
|
|
'S': '/Luminosity',
|
|
|
|
|
'G': alpha_stream,
|
|
|
|
|
}),
|
|
|
|
|
'ca': 1,
|
|
|
|
|
'AIS': 'false',
|
|
|
|
|
})
|
|
|
|
|
alpha_state_id = f'as{len(context._alpha_states)}'
|
|
|
|
|
context._alpha_states[alpha_state_id] = alpha_state
|
|
|
|
|
context.set_state(alpha_state_id)
|
|
|
|
|
|
|
|
|
|
alpha_shading = alpha_stream.add_shading()
|
|
|
|
|
alpha_shading['ShadingType'] = 2 if type_ == 'linear' else 3
|
|
|
|
|
alpha_shading['ColorSpace'] = '/DeviceGray'
|
|
|
|
|
alpha_shading['Domain'] = pydyf.Array(
|
|
|
|
|
[positions[0], positions[-1]])
|
|
|
|
|
alpha_shading['Coords'] = pydyf.Array(points)
|
|
|
|
|
alpha_shading['Function'] = pydyf.Dictionary({
|
|
|
|
|
'FunctionType': 3,
|
|
|
|
|
'Domain': pydyf.Array([positions[0], positions[-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]),
|
Handle transparent colors in gradients correctly
Gradients must be drawn using a pre-multiplied color space. It means that stop
colors are "weighted" by their alpha channel when we define how transition is
done between two colors.
To explain why this is needed, the best example is "transparent". The
"trasparent" color is set to rgba(0, 0, 0, 0), also known as transparent
black. When there’s a transition from red to transparent, we don’t want to have
a transition to black, as it would be if RGBA values were extrapolated. We want
to have a transition to transparent red.
This problem appears for each transition between colors whose RGB and A
channels are both different.
To fix this, we use the same plain color as the one from the previous stop for
transitions to transparent, and the same plain color as the one from the next
stop for transitions from transparent. For semi-transparent colors, we set a
weight proportional to the color alpha, meaning that opaque colors will take
more place than transparent ones.
This bug appears in Safari: https://bugs.webkit.org/show_bug.cgi?id=150940.
It obviously means that we’re better than Apple. Obviously.
2020-11-30 22:54:26 +03:00
|
|
|
|
'C0': pydyf.Array([c0]),
|
|
|
|
|
'C1': pydyf.Array([c1]),
|
2020-11-30 21:12:41 +03:00
|
|
|
|
'N': 1,
|
Handle transparent colors in gradients correctly
Gradients must be drawn using a pre-multiplied color space. It means that stop
colors are "weighted" by their alpha channel when we define how transition is
done between two colors.
To explain why this is needed, the best example is "transparent". The
"trasparent" color is set to rgba(0, 0, 0, 0), also known as transparent
black. When there’s a transition from red to transparent, we don’t want to have
a transition to black, as it would be if RGBA values were extrapolated. We want
to have a transition to transparent red.
This problem appears for each transition between colors whose RGB and A
channels are both different.
To fix this, we use the same plain color as the one from the previous stop for
transitions to transparent, and the same plain color as the one from the next
stop for transitions from transparent. For semi-transparent colors, we set a
weight proportional to the color alpha, meaning that opaque colors will take
more place than transparent ones.
This bug appears in Safari: https://bugs.webkit.org/show_bug.cgi?id=150940.
It obviously means that we’re better than Apple. Obviously.
2020-11-30 22:54:26 +03:00
|
|
|
|
}) for c0, c1 in alpha_couples
|
2020-11-30 21:12:41 +03:00
|
|
|
|
]),
|
|
|
|
|
})
|
|
|
|
|
if not self.repeating:
|
|
|
|
|
alpha_shading['Extend'] = pydyf.Array([b'true', b'true'])
|
|
|
|
|
alpha_stream.transform(1, 0, 0, scale_y, 0, 0)
|
|
|
|
|
alpha_stream.stream = [f'/{alpha_shading.id} sh']
|
|
|
|
|
|
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('_')
|
2020-10-24 22:35:17 +03:00
|
|
|
|
factor_x = -1 if x == 'left' else 1
|
|
|
|
|
factor_y = -1 if y == 'top' 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
|
|
|
|
|
2020-10-25 17:52:58 +03:00
|
|
|
|
# Round dx and dy to avoid floating points errors caused by
|
|
|
|
|
# trigonometry and angle units conversions
|
|
|
|
|
dx, dy = round(dx, 9), round(dy, 9)
|
|
|
|
|
|
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)
|
|
|
|
|
|
2020-10-25 16:54:08 +03:00
|
|
|
|
if self.repeating:
|
|
|
|
|
# Render as a solid color if the first and last positions are equal
|
|
|
|
|
# See https://drafts.csswg.org/css-images-3/#repeating-gradients
|
|
|
|
|
if first == last:
|
|
|
|
|
color = gradient_average_color(colors, positions)
|
|
|
|
|
return 1, 'solid', None, [], [color]
|
|
|
|
|
|
|
|
|
|
# Define defined gradient length and steps between positions
|
|
|
|
|
stop_length = last - first
|
|
|
|
|
assert stop_length > 0
|
|
|
|
|
position_steps = [
|
|
|
|
|
positions[i + 1] - positions[i]
|
|
|
|
|
for i in range(len(positions) - 1)]
|
|
|
|
|
|
2020-10-25 22:18:13 +03:00
|
|
|
|
# Create cycles used to add colors
|
2020-10-25 17:52:58 +03:00
|
|
|
|
next_steps = cycle([0] + position_steps)
|
2020-10-25 16:54:08 +03:00
|
|
|
|
next_colors = cycle(colors)
|
2020-10-25 22:18:13 +03:00
|
|
|
|
previous_steps = cycle([0] + position_steps[::-1])
|
|
|
|
|
previous_colors = cycle(colors[::-1])
|
|
|
|
|
|
|
|
|
|
# Add colors after last step
|
2020-10-25 16:54:08 +03:00
|
|
|
|
while last < vector_length:
|
|
|
|
|
step = next(next_steps)
|
|
|
|
|
colors.append(next(next_colors))
|
|
|
|
|
positions.append(positions[-1] + step)
|
|
|
|
|
last += step * stop_length
|
|
|
|
|
|
|
|
|
|
# Add colors before last step
|
|
|
|
|
while first > 0:
|
2020-10-25 17:52:58 +03:00
|
|
|
|
step = next(previous_steps)
|
2020-10-25 16:54:08 +03:00
|
|
|
|
colors.insert(0, next(previous_colors))
|
|
|
|
|
positions.insert(0, positions[0] - step)
|
|
|
|
|
first -= step * stop_length
|
2020-10-24 18:42:13 +03:00
|
|
|
|
|
|
|
|
|
# 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
|
2020-10-25 09:32:41 +03:00
|
|
|
|
# Type of ending shape: 'circle' or 'ellipse'
|
2013-04-11 12:39:23 +04:00
|
|
|
|
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
|
2020-10-24 21:49:42 +03:00
|
|
|
|
size_x, size_y = self._handle_degenerate(
|
|
|
|
|
*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])
|
2020-10-24 22:35:17 +03:00
|
|
|
|
if positions[0] < 0:
|
|
|
|
|
# PDF doesn’t like negative radiuses, shift into the positive realm
|
|
|
|
|
if self.repeating:
|
|
|
|
|
# Add vector lengths to first position until positive
|
|
|
|
|
vector_length = positions[-1] - positions[0]
|
|
|
|
|
offset = vector_length * (1 + (-positions[0] // vector_length))
|
|
|
|
|
positions = [position + offset for position in positions]
|
|
|
|
|
else:
|
|
|
|
|
# Only keep colors with position >= 0, interpolate if needed
|
|
|
|
|
if positions[-1] <= 0:
|
|
|
|
|
# All stops are negative, fill with the last color
|
|
|
|
|
return 1, 'solid', None, [], [self.colors[-1]]
|
|
|
|
|
for i, position in enumerate(positions):
|
|
|
|
|
if position == 0:
|
|
|
|
|
# Keep colors and positions from this rank
|
|
|
|
|
colors, positions = colors[i:], positions[i:]
|
|
|
|
|
break
|
|
|
|
|
if position > 0:
|
|
|
|
|
# Interpolate with previous rank to get color at 0
|
|
|
|
|
color = colors[i]
|
|
|
|
|
previous_color = colors[i - 1]
|
|
|
|
|
previous_position = positions[i - 1]
|
2020-10-25 15:41:23 +03:00
|
|
|
|
assert previous_position < 0
|
2020-10-24 22:35:17 +03:00
|
|
|
|
intermediate_color = gradient_average_color(
|
|
|
|
|
[previous_color, previous_color, color, color],
|
|
|
|
|
[previous_position, 0, 0, position])
|
|
|
|
|
colors = [intermediate_color] + colors[i:]
|
|
|
|
|
positions = [0] + positions[i:]
|
|
|
|
|
break
|
2020-10-24 21:46:38 +03:00
|
|
|
|
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-25 15:41:23 +03:00
|
|
|
|
if self.repeating:
|
|
|
|
|
points, positions, colors = self._repeat(
|
|
|
|
|
width, height, scale_y, points, positions, colors)
|
|
|
|
|
|
2020-10-24 21:46:38 +03:00
|
|
|
|
return scale_y, 'radial', points, positions, colors
|
2013-04-11 12:39:23 +04:00
|
|
|
|
|
2020-10-25 15:41:23 +03:00
|
|
|
|
def _repeat(self, width, height, scale_y, points, positions, colors):
|
|
|
|
|
# Keep original lists and values, they’re useful
|
|
|
|
|
original_colors = colors.copy()
|
|
|
|
|
original_positions = positions.copy()
|
|
|
|
|
gradient_length = points[5] - points[2]
|
|
|
|
|
|
|
|
|
|
# Get the maximum distance between the center and the corners, to find
|
|
|
|
|
# how many times we have to repeat the colors outside
|
|
|
|
|
max_distance = max(
|
|
|
|
|
math.hypot(width - points[0], height / scale_y - points[1]),
|
|
|
|
|
math.hypot(width - points[0], -points[1] * scale_y),
|
|
|
|
|
math.hypot(-points[0], height / scale_y - points[1]),
|
|
|
|
|
math.hypot(-points[0], -points[1] * scale_y))
|
|
|
|
|
repeat_after = math.ceil((max_distance - points[5]) / gradient_length)
|
|
|
|
|
if repeat_after > 0:
|
|
|
|
|
# Repeat colors and extrapolate positions
|
|
|
|
|
repeat = 1 + repeat_after
|
|
|
|
|
colors *= repeat
|
|
|
|
|
positions = [
|
|
|
|
|
i + position for i in range(repeat) for position in positions]
|
|
|
|
|
points = points[:5] + (points[5] + gradient_length * repeat_after,)
|
|
|
|
|
|
|
|
|
|
if points[2] == 0:
|
|
|
|
|
# Inner circle has 0 radius, no need to repeat inside, return
|
|
|
|
|
return points, positions, colors
|
|
|
|
|
|
|
|
|
|
# Find how many times we have to repeat the colors inside
|
|
|
|
|
repeat_before = points[2] / gradient_length
|
|
|
|
|
|
|
|
|
|
# Set the inner circle size to 0
|
|
|
|
|
points = points[:2] + (0,) + points[3:]
|
|
|
|
|
|
|
|
|
|
# Find how many times the whole gradient can be repeated
|
|
|
|
|
full_repeat = int(repeat_before)
|
|
|
|
|
if full_repeat:
|
|
|
|
|
# Repeat colors and extrapolate positions
|
|
|
|
|
colors += original_colors * full_repeat
|
|
|
|
|
positions = [
|
|
|
|
|
i - full_repeat + position for i in range(full_repeat)
|
|
|
|
|
for position in original_positions] + positions
|
|
|
|
|
|
|
|
|
|
# Find the ratio of gradient that must be added to reach the center
|
|
|
|
|
partial_repeat = repeat_before - full_repeat
|
|
|
|
|
if partial_repeat == 0:
|
|
|
|
|
# No partial repeat, return
|
|
|
|
|
return points, positions, colors
|
|
|
|
|
|
|
|
|
|
# Iterate through positions in reverse order, from the outer
|
|
|
|
|
# circle to the original inner circle, to find positions from
|
|
|
|
|
# the inner circle (including full repeats) to the center
|
|
|
|
|
assert (original_positions[0], original_positions[-1]) == (0, 1)
|
|
|
|
|
assert 0 < partial_repeat < 1
|
|
|
|
|
reverse = original_positions[::-1]
|
|
|
|
|
ratio = 1 - partial_repeat
|
|
|
|
|
for i, position in enumerate(reverse, start=1):
|
|
|
|
|
if position == ratio:
|
|
|
|
|
# The center is a color of the gradient, truncate original
|
|
|
|
|
# colors and positions and prepend them
|
|
|
|
|
colors = original_colors[-i:] + colors
|
|
|
|
|
new_positions = [
|
|
|
|
|
position - full_repeat - 1
|
|
|
|
|
for position in original_positions[-i:]]
|
|
|
|
|
positions = new_positions + positions
|
|
|
|
|
return points, positions, colors
|
|
|
|
|
if position < ratio:
|
|
|
|
|
# The center is between two colors of the gradient,
|
|
|
|
|
# define the center color as the average of these two
|
|
|
|
|
# gradient colors
|
|
|
|
|
color = original_colors[-i]
|
|
|
|
|
next_color = original_colors[-(i - 1)]
|
|
|
|
|
next_position = original_positions[-(i - 1)]
|
|
|
|
|
average_colors = [color, color, next_color, next_color]
|
|
|
|
|
average_positions = [position, ratio, ratio, next_position]
|
|
|
|
|
zero_color = gradient_average_color(
|
|
|
|
|
average_colors, average_positions)
|
|
|
|
|
colors = [zero_color] + original_colors[-(i - 1):] + colors
|
|
|
|
|
new_positions = [
|
|
|
|
|
position - 1 - full_repeat for position
|
|
|
|
|
in original_positions[-(i - 1):]]
|
|
|
|
|
positions = (
|
|
|
|
|
[ratio - 1 - full_repeat] + new_positions + positions)
|
|
|
|
|
return points, positions, colors
|
|
|
|
|
|
2013-04-11 18:06:28 +04:00
|
|
|
|
def _resolve_size(self, width, height, center_x, center_y):
|
2020-10-25 09:32:41 +03:00
|
|
|
|
"""Resolve circle size of the radial gradient."""
|
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):
|
2020-10-25 09:32:41 +03:00
|
|
|
|
"""Handle degenerate radial gradients.
|
|
|
|
|
|
|
|
|
|
See https://drafts.csswg.org/css-images-3/#degenerate-radials
|
|
|
|
|
|
|
|
|
|
"""
|
2020-10-24 21:46:38 +03:00
|
|
|
|
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
|