1
1
mirror of https://github.com/Kozea/WeasyPrint.git synced 2024-10-05 00:21:15 +03:00
WeasyPrint/weasyprint/images.py

707 lines
28 KiB
Python
Raw Normal View History

"""
weasyprint.images
-----------------
2011-12-08 19:31:03 +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
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
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
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)
class RasterImage:
def __init__(self, pillow_image, optimize_image):
2020-06-03 18:58:53 +03:00
self.pillow_image = pillow_image
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
self.intrinsic_ratio = (
self._intrinsic_width / self._intrinsic_height
if self._intrinsic_height != 0 else float('inf'))
def get_intrinsic_size(self, image_resolution, _font_size):
# Raster images are affected by the 'image-resolution' property.
return (self._intrinsic_width / image_resolution,
self._intrinsic_height / image_resolution)
def draw(self, context, concrete_width, concrete_height, image_rendering):
has_size = (
concrete_width > 0
and concrete_height > 0
and self._intrinsic_width > 0
and self._intrinsic_height > 0
)
if not has_size:
return
image_name = context.add_image(
self.pillow_image, image_rendering, self.optimize_image)
# 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:
def __init__(self, pydyf_svg, base_url, url_fetcher):
self._svg = pydyf_svg
self._base_url = base_url
self._url_fetcher = url_fetcher
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,
self._url_fetcher)
2021-01-06 12:53:45 +03:00
context.pop_state()
def get_image_from_uri(cache, url_fetcher, optimize_images, url,
forced_mime_type=None):
"""Get an Image instance from an image URI."""
missing = object()
image = cache.get(url, missing)
if image is not missing:
return image
2011-12-08 19:31:03 +04:00
try:
with fetch(url_fetcher, url) as result:
if 'string' in result:
string = result['string']
else:
string = result['file_obj'].read()
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
if mime_type == 'image/svg+xml':
try:
image = SVGImage(SVG(string, url), url, url_fetcher)
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:
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
image = SVGImage(SVG(string, url), url, url_fetcher)
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:
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)
image = None
cache[url] = image
return image
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.
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.
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]
# 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
2020-10-24 18:42:13 +03:00
# 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)
2018-01-14 03:48:17 +03:00
for j in range(previous_i + 1, i):
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]
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
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
"""
nb_stops = len(positions)
2013-04-12 17:29:21 +04:00
assert nb_stops > 1
assert nb_stops == len(colors)
total_length = positions[-1] - positions[0]
if total_length == 0:
positions = list(range(nb_stops))
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)
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
def get_intrinsic_size(self, _image_resolution, _font_size):
# Gradients are not affected by image resolution, parent or font size.
return None, None
intrinsic_ratio = None
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:
context.set_alpha(alpha, stroke=False)
2020-06-08 00:40:01 +03:00
context.fill()
return
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]),
'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]),
'C0': pydyf.Array([c0]),
'C1': pydyf.Array([c1]),
2020-11-30 21:12:41 +03:00
'N': 1,
}) 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).
"""
raise NotImplementedError
class LinearGradient(Gradient):
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)
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.
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
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
else:
2020-10-24 18:42:13 +03:00
assert self.direction_type == 'angle'
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 doesnt
# 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)]
# 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)
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
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
# 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
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 doesnt
# 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 doesnt 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, theyre 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):
"""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
size_x = percentage(size_x, width)
size_y = percentage(size_y, height)
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
"""
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